Integrate release recovery, semantic review, coverage and Windows packaging
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
//! Advisory presence of explicit behavior evidence, never inferred execution.
|
||||
use super::*;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(super) struct Index {
|
||||
pub grouping: BTreeSet<Id>,
|
||||
pub supported_capabilities: BTreeSet<Id>,
|
||||
contextual_capabilities: BTreeSet<Id>,
|
||||
supported_missions: BTreeSet<Id>,
|
||||
}
|
||||
|
||||
impl Index {
|
||||
pub fn new(p: &Project) -> Self {
|
||||
let mut index = Self::default();
|
||||
for element in p.elements.values() {
|
||||
if let Some(parent) = element.parent.and_then(|id| p.elements.get(&id))
|
||||
&& parent.kind() == element.kind()
|
||||
&& parent.layer() == element.layer()
|
||||
{
|
||||
index.grouping.insert(parent.id);
|
||||
}
|
||||
if let Some(SharedBody::Scenario {
|
||||
context: Some(id), ..
|
||||
}) = element.shared()
|
||||
&& p.elements.get(id).is_some_and(|context| {
|
||||
matches!(context.kind(), Kind::Capability | Kind::SystemCapability)
|
||||
&& context.layer() == element.layer()
|
||||
})
|
||||
{
|
||||
index.contextual_capabilities.insert(*id);
|
||||
index.supported_capabilities.insert(*id);
|
||||
}
|
||||
}
|
||||
for relation in p.relations.values() {
|
||||
let (Some(source), Some(target)) = (
|
||||
p.elements.get(&relation.source()),
|
||||
p.elements.get(&relation.target()),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
if source.layer() != target.layer()
|
||||
|| !relation.kind().permits(source.kind(), target.kind())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
match relation.kind() {
|
||||
RelationKind::CapabilityInvolvement => {
|
||||
index.supported_capabilities.insert(target.id);
|
||||
}
|
||||
RelationKind::ChainInvolvement => {
|
||||
index.supported_capabilities.insert(target.id);
|
||||
index.contextual_capabilities.insert(target.id);
|
||||
}
|
||||
RelationKind::MissionCapability => {
|
||||
index.supported_missions.insert(source.id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
index
|
||||
}
|
||||
|
||||
pub fn validate(&self, p: &Project, out: &mut Vec<Issue>) {
|
||||
let (work, exchanges) = included_evidence(p);
|
||||
for element in p.elements.values() {
|
||||
if element.kind().is_work()
|
||||
&& !self.grouping.contains(&element.id)
|
||||
&& !work.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"work_without_behavior_context",
|
||||
"",
|
||||
"Include this leaf activity/function in a process/chain or scenario through a work action or exchange occurrence.",
|
||||
);
|
||||
}
|
||||
if matches!(element.kind(), Kind::OperationalMission | Kind::Mission)
|
||||
&& !self.supported_missions.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"mission_without_capability",
|
||||
"",
|
||||
"Connect this mission to a capability that delivers it.",
|
||||
);
|
||||
}
|
||||
if matches!(element.kind(), Kind::Capability | Kind::SystemCapability)
|
||||
&& !self.grouping.contains(&element.id)
|
||||
&& self.supported_capabilities.contains(&element.id)
|
||||
&& !self.contextual_capabilities.contains(&element.id)
|
||||
{
|
||||
warning(
|
||||
out,
|
||||
element.id,
|
||||
"capability_without_behavior_context",
|
||||
"",
|
||||
"Add an explicit behavior context: link a process/chain with Chain involvement, or set a scenario's Context to this capability.",
|
||||
);
|
||||
}
|
||||
}
|
||||
for relation in p.relations.values().filter(|r| work_exchange(r.kind())) {
|
||||
let mut non_leaf = false;
|
||||
for (field, endpoint) in [("source", relation.source()), ("target", relation.target())]
|
||||
{
|
||||
if self.grouping.contains(&endpoint) {
|
||||
non_leaf = true;
|
||||
warning(
|
||||
out,
|
||||
relation.id,
|
||||
"non_leaf_work_exchange",
|
||||
field,
|
||||
"Choose the leaf activity/function responsible for this exchange; keep any typed port owned by that function.",
|
||||
);
|
||||
}
|
||||
}
|
||||
if !non_leaf && !exchanges.contains(&relation.id) {
|
||||
warning(
|
||||
out,
|
||||
relation.id,
|
||||
"exchange_without_behavior_context",
|
||||
"",
|
||||
"Use this exchange in a process/chain or scenario.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn work_exchange(kind: RelationKind) -> bool {
|
||||
matches!(
|
||||
kind,
|
||||
RelationKind::Interaction | RelationKind::FunctionalExchange
|
||||
)
|
||||
}
|
||||
|
||||
/// Only ordered, owned occurrences contribute. Each identity is visited once,
|
||||
/// including reused containers; this remains bounded even on malformed drafts.
|
||||
fn included_evidence(p: &Project) -> (BTreeSet<Id>, BTreeSet<Id>) {
|
||||
let mut pending: Vec<_> = p
|
||||
.elements
|
||||
.values()
|
||||
.filter(|e| {
|
||||
matches!(
|
||||
e.kind(),
|
||||
Kind::Process | Kind::FunctionalChain | Kind::Scenario
|
||||
)
|
||||
})
|
||||
.map(|e| e.id)
|
||||
.collect();
|
||||
let mut seen = BTreeSet::new();
|
||||
let mut work = BTreeSet::new();
|
||||
let mut exchanges = BTreeSet::new();
|
||||
while let Some(id) = pending.pop() {
|
||||
if !seen.insert(id) {
|
||||
continue;
|
||||
}
|
||||
let Some(element) = p.elements.get(&id) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(steps) = element.steps() {
|
||||
pending.extend(
|
||||
steps
|
||||
.iter()
|
||||
.filter(|step| {
|
||||
p.elements.get(step).is_some_and(|child| {
|
||||
child.kind() == Kind::Occurrence
|
||||
&& child.parent == Some(id)
|
||||
&& child.layer() == element.layer()
|
||||
})
|
||||
})
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
let Some(SharedBody::Occurrence { body, .. }) = element.shared() else {
|
||||
continue;
|
||||
};
|
||||
match body {
|
||||
OccurrenceBody::Action {
|
||||
action: Some(action),
|
||||
..
|
||||
} => {
|
||||
if p.elements
|
||||
.get(action)
|
||||
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
||||
{
|
||||
work.insert(*action);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::Exchange {
|
||||
exchange: Some(exchange),
|
||||
..
|
||||
} => {
|
||||
if let Some(relation) = p.relations.get(exchange)
|
||||
&& work_exchange(relation.kind())
|
||||
&& [relation.source(), relation.target()].iter().all(|id| {
|
||||
p.elements
|
||||
.get(id)
|
||||
.is_some_and(|e| e.kind().is_work() && e.layer() == element.layer())
|
||||
})
|
||||
{
|
||||
exchanges.insert(*exchange);
|
||||
work.extend([relation.source(), relation.target()]);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::Fragment { branches, .. } => {
|
||||
pending.extend(
|
||||
branches
|
||||
.iter()
|
||||
.filter(|branch| {
|
||||
p.elements.get(branch).is_some_and(|child| {
|
||||
child.parent == Some(id)
|
||||
&& child.layer() == element.layer()
|
||||
&& matches!(
|
||||
child.shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Branch { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
.copied(),
|
||||
);
|
||||
}
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(sequence),
|
||||
} => {
|
||||
if let Some(owner) = element.parent.and_then(|id| p.elements.get(&id))
|
||||
&& matches!(owner.kind(), Kind::Process | Kind::FunctionalChain)
|
||||
&& p.elements.get(sequence).is_some_and(|target| {
|
||||
target.kind() == owner.kind() && target.layer() == element.layer()
|
||||
})
|
||||
{
|
||||
pending.push(*sequence);
|
||||
}
|
||||
}
|
||||
OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
..
|
||||
} if p.elements.get(scenario).is_some_and(|target| {
|
||||
target.kind() == Kind::Scenario && target.layer() == element.layer()
|
||||
}) =>
|
||||
{
|
||||
pending.push(*scenario);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
(work, exchanges)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn malformed_self_reuse_is_bounded_even_without_structural_gate() {
|
||||
let mut p = Project::empty();
|
||||
let scenario = p
|
||||
.add_element(Kind::Scenario, Layer::Oa, "Self reuse", None)
|
||||
.unwrap();
|
||||
let occurrence = p
|
||||
.add_element(Kind::Occurrence, Layer::Oa, "Cycle", Some(scenario))
|
||||
.unwrap();
|
||||
if let Some(SharedBody::Occurrence { body, .. }) =
|
||||
p.elements.get_mut(&occurrence).unwrap().shared_mut()
|
||||
{
|
||||
*body = OccurrenceBody::ScenarioUse {
|
||||
scenario: Some(scenario),
|
||||
bindings: vec![],
|
||||
};
|
||||
}
|
||||
p.elements
|
||||
.get_mut(&scenario)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(occurrence);
|
||||
assert_eq!(included_evidence(&p), (BTreeSet::new(), BTreeSet::new()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user