175 lines
5.9 KiB
Rust
175 lines
5.9 KiB
Rust
use super::*;
|
|
use crate::model::*;
|
|
use crate::transitions::Drift;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct LinkedSemanticImpact {
|
|
pub objects: KeyPair<Id>,
|
|
pub traces: Vec<Id>,
|
|
pub drift: Drift,
|
|
pub unreviewed: bool,
|
|
}
|
|
const MAX_LINKED_OBJECTS: usize = 4_096;
|
|
const MAX_LINKED_EDGES: usize = 32_768;
|
|
|
|
/// Derived dependency diagnostics, never another historical baseline. Follow
|
|
/// direct references/children, then only reusable dictionary/event definitions.
|
|
/// Association endpoints and ordinary architecture neighborhoods do not expand.
|
|
pub fn linked_semantic_impacts(
|
|
project: &Project,
|
|
trace: &Trace,
|
|
references: &BTreeMap<Id, Id>,
|
|
) -> Result<Vec<LinkedSemanticImpact>, String> {
|
|
let mut children: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
|
for element in project.elements.values() {
|
|
if let Some(parent) = element.parent {
|
|
children.entry(parent).or_default().push(element.id);
|
|
}
|
|
}
|
|
let mut contracts: BTreeMap<Id, Vec<Id>> = BTreeMap::new();
|
|
for relation in project.relations.values() {
|
|
if let RelationBody::InterfaceItemAllocation { source, .. } = relation.body {
|
|
contracts.entry(source).or_default().push(relation.id);
|
|
}
|
|
}
|
|
let direct = |id| -> Vec<Id> {
|
|
let mut out = vec![];
|
|
if let Some(element) = project.elements.get(&id) {
|
|
out.extend(
|
|
crate::references::element_references(element)
|
|
.into_iter()
|
|
.filter(|r| Some(*r) != element.parent),
|
|
);
|
|
}
|
|
if let Some(relation) = project.relations.get(&id) {
|
|
out.extend(crate::references::relation_references(relation));
|
|
}
|
|
out.extend(children.get(&id).into_iter().flatten().copied());
|
|
out.extend(contracts.get(&id).into_iter().flatten().copied());
|
|
out
|
|
};
|
|
let expandable = |id| {
|
|
project.elements.get(&id).is_some_and(|e| {
|
|
matches!(
|
|
e.kind(),
|
|
Kind::DataType
|
|
| Kind::DataField
|
|
| Kind::ExchangeItem
|
|
| Kind::Interface
|
|
| Kind::Event
|
|
)
|
|
}) || project
|
|
.relations
|
|
.get(&id)
|
|
.is_some_and(|r| matches!(r.body, RelationBody::InterfaceItemAllocation { .. }))
|
|
};
|
|
let mut edges = 0usize;
|
|
let mut traverse = |root| -> Result<BTreeSet<Id>, String> {
|
|
let mut seen = BTreeSet::from([root]);
|
|
let first = direct(root);
|
|
edges += first.len();
|
|
if edges > MAX_LINKED_EDGES {
|
|
return Err(
|
|
"Linked semantic dependency work limit exceeded; narrow the review scope".into(),
|
|
);
|
|
}
|
|
let mut queue: VecDeque<_> = first.into();
|
|
while let Some(id) = queue.pop_front() {
|
|
if !seen.insert(id) {
|
|
continue;
|
|
}
|
|
if seen.len() > MAX_LINKED_OBJECTS || edges > MAX_LINKED_EDGES {
|
|
return Err(
|
|
"Linked semantic dependency work limit exceeded; narrow the review scope"
|
|
.into(),
|
|
);
|
|
}
|
|
if expandable(id) {
|
|
let next = direct(id);
|
|
edges += next.len();
|
|
if edges > MAX_LINKED_EDGES {
|
|
return Err(
|
|
"Linked semantic dependency work limit exceeded; narrow the review scope"
|
|
.into(),
|
|
);
|
|
}
|
|
queue.extend(next);
|
|
}
|
|
}
|
|
seen.remove(&root);
|
|
Ok(seen)
|
|
};
|
|
let oa = traverse(trace.oa)?;
|
|
let sa = traverse(trace.sa)?;
|
|
let mut traced: BTreeMap<(Id, Id), Vec<&Trace>> = BTreeMap::new();
|
|
for t in project.traces.values() {
|
|
traced.entry((t.oa, t.sa)).or_default().push(t);
|
|
}
|
|
let mut pairs = vec![];
|
|
let mut used = BTreeSet::new();
|
|
for id in oa {
|
|
let target = references.get(&id).copied();
|
|
if let Some(id) = target {
|
|
used.insert(id);
|
|
}
|
|
pairs.push(KeyPair {
|
|
oa: Some(id),
|
|
sa: target,
|
|
});
|
|
}
|
|
for id in sa.difference(&used) {
|
|
pairs.push(KeyPair {
|
|
oa: None,
|
|
sa: Some(*id),
|
|
});
|
|
}
|
|
let mut result = vec![];
|
|
for objects in pairs {
|
|
if objects.oa == Some(trace.oa) && objects.sa == Some(trace.sa) {
|
|
continue;
|
|
}
|
|
let links = objects
|
|
.oa
|
|
.zip(objects.sa)
|
|
.and_then(|pair| traced.get(&pair));
|
|
let mut flags = (false, false, links.is_none());
|
|
let mut ids = vec![];
|
|
for linked in links.into_iter().flatten() {
|
|
ids.push(linked.id);
|
|
let facts = super::catalog::own_semantic_facets(project, linked, references)?;
|
|
if facts.is_empty() {
|
|
flags.2 = true;
|
|
}
|
|
for fact in facts {
|
|
match fact.drift {
|
|
Drift::OaChanged => flags.0 = true,
|
|
Drift::SaChanged => flags.1 = true,
|
|
Drift::BothChanged => {
|
|
flags.0 = true;
|
|
flags.1 = true;
|
|
}
|
|
Drift::Unreviewed => flags.2 = true,
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
let drift = match flags {
|
|
(true, true, _) => Drift::BothChanged,
|
|
(true, false, _) => Drift::OaChanged,
|
|
(false, true, _) => Drift::SaChanged,
|
|
(false, false, true) => Drift::Unreviewed,
|
|
_ => Drift::Unchanged,
|
|
};
|
|
result.push(LinkedSemanticImpact {
|
|
objects,
|
|
traces: ids,
|
|
drift,
|
|
unreviewed: flags.2,
|
|
});
|
|
}
|
|
Ok(result)
|
|
}
|