Checkpoint source 7 workflows and in-progress diagram authoring redesign
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Preserve current core, source, REST, desktop, recovery, documentation and validation work, including AGENTS.md and the development summary. This is a work-in-progress checkpoint: the new compact palette has outstanding compilation errors and native acceptance; earlier passing gates retain their documented scope.
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
use super::*;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "analysis_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ReferenceBoundary {
|
||||
Entry,
|
||||
Exit,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ContextVertex {
|
||||
pub address: ContextAddress,
|
||||
pub boundary: Option<ReferenceBoundary>,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ContextEdgeKind {
|
||||
Authored,
|
||||
SyntheticBoundary,
|
||||
LegacyOrder,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContextEdge {
|
||||
pub relation: Option<Id>,
|
||||
pub instance: Vec<Id>,
|
||||
pub source: ContextVertex,
|
||||
pub target: ContextVertex,
|
||||
pub guard: String,
|
||||
pub kind: ContextEdgeKind,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContextAssembly {
|
||||
pub relation: Id,
|
||||
pub instance: Vec<Id>,
|
||||
pub source: ContextAddress,
|
||||
pub target: ContextAddress,
|
||||
pub purpose: ContextualLinkPurpose,
|
||||
}
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ContextNotice {
|
||||
pub address: ContextAddress,
|
||||
pub code: &'static str,
|
||||
pub message: String,
|
||||
pub relations: Vec<Id>,
|
||||
}
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct AnalysisLimits {
|
||||
pub nodes: usize,
|
||||
pub edges: usize,
|
||||
}
|
||||
impl Default for AnalysisLimits {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
nodes: MAX_ANALYSIS_NODES,
|
||||
edges: MAX_ANALYSIS_EDGES,
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextAnalysis {
|
||||
pub nodes: BTreeMap<ContextAddress, ResolvedOccurrence>,
|
||||
pub precedence: Vec<ContextEdge>,
|
||||
pub assembly: Vec<ContextAssembly>,
|
||||
pub entries: Vec<ContextVertex>,
|
||||
pub exits: Vec<ContextVertex>,
|
||||
pub notices: Vec<ContextNotice>,
|
||||
/// A partial expansion must never be used for complete reachability conclusions.
|
||||
pub complete: bool,
|
||||
}
|
||||
struct Builder<'a, 'p> {
|
||||
index: &'a TopologyIndex<'p>,
|
||||
root: Id,
|
||||
limits: AnalysisLimits,
|
||||
result: ContextAnalysis,
|
||||
definitions: BTreeMap<Id, Vec<&'p Relation>>,
|
||||
examined_edges: usize,
|
||||
exhausted: bool,
|
||||
}
|
||||
impl TopologyIndex<'_> {
|
||||
pub fn analyze(&self, owner: Id, limits: AnalysisLimits) -> ContextAnalysis {
|
||||
self.analyze_counted(owner, limits).0
|
||||
}
|
||||
fn analyze_counted(&self, owner: Id, limits: AnalysisLimits) -> (ContextAnalysis, usize) {
|
||||
let mut definitions: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
for (owner, relations) in &self.link_definitions {
|
||||
definitions.insert(
|
||||
*owner,
|
||||
relations
|
||||
.iter()
|
||||
.filter_map(|id| self.project.relations.get(id))
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
let mut b = Builder {
|
||||
index: self,
|
||||
root: owner,
|
||||
limits,
|
||||
result: ContextAnalysis {
|
||||
complete: true,
|
||||
..ContextAnalysis::default()
|
||||
},
|
||||
definitions,
|
||||
examined_edges: 0,
|
||||
exhausted: false,
|
||||
};
|
||||
(b.result.entries, b.result.exits) = b.expand(owner, &[], &mut BTreeSet::new());
|
||||
if b.result.complete {
|
||||
b.cardinality();
|
||||
b.connectivity();
|
||||
}
|
||||
(b.result, b.examined_edges)
|
||||
}
|
||||
pub fn has_contextual_links(&self, owner: Id) -> bool {
|
||||
self.contextual_callers.contains(&owner)
|
||||
}
|
||||
}
|
||||
impl Builder<'_, '_> {
|
||||
fn address(&self, path: &[Id], occurrence: Id) -> ContextAddress {
|
||||
ContextAddress {
|
||||
owner: self.root,
|
||||
endpoint: OccurrenceEndpoint {
|
||||
references: path.to_vec(),
|
||||
occurrence,
|
||||
},
|
||||
}
|
||||
}
|
||||
fn vertex(&self, address: ContextAddress, source: bool) -> ContextVertex {
|
||||
let reference = self
|
||||
.index
|
||||
.project
|
||||
.elements
|
||||
.get(&address.endpoint.occurrence)
|
||||
.and_then(Element::shared)
|
||||
.is_some_and(|s| {
|
||||
matches!(
|
||||
s,
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::SequenceReference { .. },
|
||||
..
|
||||
}
|
||||
)
|
||||
});
|
||||
ContextVertex {
|
||||
address,
|
||||
boundary: reference.then_some(if source {
|
||||
ReferenceBoundary::Exit
|
||||
} else {
|
||||
ReferenceBoundary::Entry
|
||||
}),
|
||||
}
|
||||
}
|
||||
fn incomplete(&mut self, address: ContextAddress, code: &'static str, message: &str) {
|
||||
self.result.complete = false;
|
||||
if !self
|
||||
.result
|
||||
.notices
|
||||
.iter()
|
||||
.any(|n| n.address == address && n.code == code)
|
||||
{
|
||||
self.result.notices.push(ContextNotice {
|
||||
address,
|
||||
code,
|
||||
message: message.into(),
|
||||
relations: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
// Reserve work before resolving an authored endpoint, not merely before storing
|
||||
// its output. Repeated malformed or filtered endpoints consume the same budget.
|
||||
fn reserve_edge(&mut self, address: ContextAddress) -> bool {
|
||||
if self.exhausted {
|
||||
return false;
|
||||
}
|
||||
if self.examined_edges >= self.limits.edges {
|
||||
self.exhausted = true;
|
||||
self.incomplete(
|
||||
address,
|
||||
"context_analysis_limit",
|
||||
"Contextual edge limit reached; connectivity analysis is incomplete.",
|
||||
);
|
||||
return false;
|
||||
}
|
||||
self.examined_edges += 1;
|
||||
true
|
||||
}
|
||||
fn edge(&mut self, edge: ContextEdge) -> bool {
|
||||
if !self.reserve_edge(edge.source.address.clone()) {
|
||||
return false;
|
||||
}
|
||||
self.result.precedence.push(edge);
|
||||
true
|
||||
}
|
||||
fn expand(
|
||||
&mut self,
|
||||
sequence: Id,
|
||||
prefix: &[Id],
|
||||
active: &mut BTreeSet<Id>,
|
||||
) -> (Vec<ContextVertex>, Vec<ContextVertex>) {
|
||||
if self.exhausted {
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
if prefix.len() > MAX_REFERENCE_DEPTH || !active.insert(sequence) {
|
||||
self.incomplete(
|
||||
self.address(prefix, sequence),
|
||||
"context_analysis_incomplete",
|
||||
"Recursive or over-depth reuse has no proven boundary path.",
|
||||
);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
let children = self.index.contributions(sequence);
|
||||
if children.is_empty() {
|
||||
self.incomplete(
|
||||
self.address(prefix, sequence),
|
||||
"context_analysis_incomplete",
|
||||
"An empty or unresolved sequence has no proven boundary path.",
|
||||
);
|
||||
active.remove(&sequence);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
let start = self.result.precedence.len();
|
||||
for id in &children {
|
||||
let address = self.address(prefix, *id);
|
||||
if self.result.nodes.len() >= self.limits.nodes {
|
||||
self.exhausted = true;
|
||||
self.incomplete(
|
||||
address,
|
||||
"context_analysis_limit",
|
||||
"Contextual node limit reached; connectivity analysis is incomplete.",
|
||||
);
|
||||
break;
|
||||
}
|
||||
match self.index.resolve(self.root, &address.endpoint) {
|
||||
Ok(resolved) => {
|
||||
self.result.nodes.insert(address.clone(), resolved);
|
||||
}
|
||||
Err(e) => {
|
||||
self.incomplete(address, "context_analysis_incomplete", &e.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let reference = self.index.project.elements[id]
|
||||
.shared()
|
||||
.and_then(|s| match s {
|
||||
SharedBody::Occurrence {
|
||||
body: OccurrenceBody::SequenceReference { sequence },
|
||||
..
|
||||
} => Some(*sequence),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(reference) = reference {
|
||||
let Some(callee) = reference else {
|
||||
self.incomplete(
|
||||
address,
|
||||
"context_analysis_incomplete",
|
||||
"An unresolved reference has no proven boundary path.",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let mut path = prefix.to_vec();
|
||||
path.push(*id);
|
||||
let (entries, exits) = self.expand(callee, &path, active);
|
||||
if self.exhausted {
|
||||
active.remove(&sequence);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
if entries.is_empty() || exits.is_empty() {
|
||||
self.incomplete(
|
||||
address.clone(),
|
||||
"context_analysis_incomplete",
|
||||
"No referenced entry or exit is proven; no bypass is inferred.",
|
||||
);
|
||||
}
|
||||
for target in entries {
|
||||
if !self.edge(ContextEdge {
|
||||
relation: None,
|
||||
instance: prefix.to_vec(),
|
||||
source: self.vertex(address.clone(), false),
|
||||
target,
|
||||
guard: String::new(),
|
||||
kind: ContextEdgeKind::SyntheticBoundary,
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
for source in exits {
|
||||
if !self.edge(ContextEdge {
|
||||
relation: None,
|
||||
instance: prefix.to_vec(),
|
||||
source,
|
||||
target: self.vertex(address.clone(), true),
|
||||
guard: String::new(),
|
||||
kind: ContextEdgeKind::SyntheticBoundary,
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.exhausted {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if self.exhausted {
|
||||
active.remove(&sequence);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
let relations = self.definitions.get(&sequence).cloned().unwrap_or_default();
|
||||
for r in &relations {
|
||||
if !self.reserve_edge(self.address(prefix, r.source())) {
|
||||
break;
|
||||
}
|
||||
let (source, target, purpose) = match &r.body {
|
||||
RelationBody::SequenceLink {
|
||||
source,
|
||||
target,
|
||||
guard,
|
||||
} => (
|
||||
OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: *source,
|
||||
},
|
||||
OccurrenceEndpoint {
|
||||
references: vec![],
|
||||
occurrence: *target,
|
||||
},
|
||||
ContextualLinkPurpose::Precedence {
|
||||
guard: guard.clone(),
|
||||
},
|
||||
),
|
||||
RelationBody::ContextualLink {
|
||||
source,
|
||||
target,
|
||||
purpose,
|
||||
..
|
||||
} => (source.clone(), target.clone(), purpose.clone()),
|
||||
_ => continue,
|
||||
};
|
||||
let qualify = |mut endpoint: OccurrenceEndpoint| {
|
||||
let mut path = prefix.to_vec();
|
||||
path.extend(endpoint.references);
|
||||
endpoint.references = path;
|
||||
ContextAddress {
|
||||
owner: self.root,
|
||||
endpoint,
|
||||
}
|
||||
};
|
||||
let source = qualify(source);
|
||||
let target = qualify(target);
|
||||
if !self.result.nodes.contains_key(&source) || !self.result.nodes.contains_key(&target)
|
||||
{
|
||||
self.incomplete(
|
||||
source,
|
||||
"context_analysis_incomplete",
|
||||
"A link endpoint is unresolved in this bounded analysis.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
match purpose {
|
||||
ContextualLinkPurpose::Precedence { guard } => {
|
||||
self.result.precedence.push(ContextEdge {
|
||||
relation: Some(r.id),
|
||||
instance: prefix.to_vec(),
|
||||
source: self.vertex(source, true),
|
||||
target: self.vertex(target, false),
|
||||
guard,
|
||||
kind: ContextEdgeKind::Authored,
|
||||
})
|
||||
}
|
||||
purpose => {
|
||||
self.result.assembly.push(ContextAssembly {
|
||||
relation: r.id,
|
||||
instance: prefix.to_vec(),
|
||||
source,
|
||||
target,
|
||||
purpose,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.exhausted {
|
||||
active.remove(&sequence);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
if relations.is_empty()
|
||||
&& children.iter().all(|id| {
|
||||
matches!(
|
||||
self.index.project.elements[id].shared(),
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Exchange { .. },
|
||||
..
|
||||
})
|
||||
)
|
||||
})
|
||||
{
|
||||
for pair in children.windows(2) {
|
||||
if !self.edge(ContextEdge {
|
||||
relation: None,
|
||||
instance: prefix.to_vec(),
|
||||
source: self.vertex(self.address(prefix, pair[0]), true),
|
||||
target: self.vertex(self.address(prefix, pair[1]), false),
|
||||
guard: String::new(),
|
||||
kind: ContextEdgeKind::LegacyOrder,
|
||||
}) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.exhausted {
|
||||
active.remove(&sequence);
|
||||
return (vec![], vec![]);
|
||||
}
|
||||
// Intrinsic boundaries are determined before any enclosing caller links.
|
||||
let edges = &self.result.precedence[start..];
|
||||
let targets: BTreeSet<_> = edges.iter().map(|e| &e.target).collect();
|
||||
let sources: BTreeSet<_> = edges.iter().map(|e| &e.source).collect();
|
||||
let entries = children
|
||||
.iter()
|
||||
.map(|id| self.vertex(self.address(prefix, *id), false))
|
||||
.filter(|v| !targets.contains(v))
|
||||
.collect();
|
||||
let exits = children
|
||||
.iter()
|
||||
.map(|id| self.vertex(self.address(prefix, *id), true))
|
||||
.filter(|v| !sources.contains(v))
|
||||
.collect();
|
||||
active.remove(&sequence);
|
||||
(entries, exits)
|
||||
}
|
||||
fn cardinality(&mut self) {
|
||||
// Index authored incidence once; synthetic boundaries never count as controls.
|
||||
let mut incoming: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
let mut outgoing: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
for edge in &self.result.precedence {
|
||||
if edge.kind == ContextEdgeKind::Authored {
|
||||
incoming.entry(&edge.target.address).or_default().push(edge);
|
||||
outgoing.entry(&edge.source.address).or_default().push(edge);
|
||||
}
|
||||
}
|
||||
for address in self.result.nodes.keys() {
|
||||
let ins = incoming.get(address).cloned().unwrap_or_default();
|
||||
let outs = outgoing.get(address).cloned().unwrap_or_default();
|
||||
let before = ins.len();
|
||||
let after = outs.len();
|
||||
let control = match self.index.project.elements[&address.endpoint.occurrence].shared() {
|
||||
Some(SharedBody::Occurrence {
|
||||
body: OccurrenceBody::Control { kind },
|
||||
..
|
||||
}) => Some(*kind),
|
||||
_ => None,
|
||||
};
|
||||
let issue = match control {
|
||||
Some(kind) => { let valid = match kind { ControlKind::Choice | ControlKind::Fork => before == 1 && after >= 2, ControlKind::Merge | ControlKind::Join => before >= 2 && after == 1, ControlKind::Loop => before >= 1 && after >= 2 }; (!valid).then_some(("control_cardinality", format!("{kind:?} has {before} incoming and {after} outgoing authored precedence links in this caller context. Review the intended paths."))) }
|
||||
None => (before > 1 || after > 1).then_some(("ambiguous_sequence_branch", "This contribution has multiple incoming or outgoing precedence links in this caller context. Review whether explicit merge/join or branching control is needed to describe the intended paths.".into())),
|
||||
};
|
||||
if let Some((code, message)) = issue {
|
||||
self.result.notices.push(ContextNotice {
|
||||
address: address.clone(),
|
||||
code,
|
||||
message,
|
||||
relations: ins.iter().chain(&outs).filter_map(|e| e.relation).collect(),
|
||||
});
|
||||
}
|
||||
if matches!(control, Some(ControlKind::Choice | ControlKind::Loop)) {
|
||||
for edge in outs.iter().filter(|e| e.guard.trim().is_empty()) {
|
||||
self.result.notices.push(ContextNotice { address: address.clone(), code: "missing_sequence_guard", message: "Describe the choice/loop condition for this path, or use an explicit 'else' guard.".into(), relations: edge.relation.into_iter().collect() });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fn connectivity(&mut self) {
|
||||
let mut forward: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
let mut reverse: BTreeMap<_, Vec<_>> = BTreeMap::new();
|
||||
for edge in &self.result.precedence {
|
||||
forward
|
||||
.entry(edge.source.clone())
|
||||
.or_default()
|
||||
.push(edge.target.clone());
|
||||
reverse
|
||||
.entry(edge.target.clone())
|
||||
.or_default()
|
||||
.push(edge.source.clone());
|
||||
}
|
||||
let visit = |starts: &[ContextVertex],
|
||||
graph: &BTreeMap<ContextVertex, Vec<ContextVertex>>| {
|
||||
let mut pending = starts.to_vec();
|
||||
let mut seen = BTreeSet::new();
|
||||
while let Some(vertex) = pending.pop() {
|
||||
if seen.insert(vertex.clone()) {
|
||||
pending.extend(graph.get(&vertex).into_iter().flatten().cloned());
|
||||
}
|
||||
}
|
||||
seen
|
||||
};
|
||||
let from_start = visit(&self.result.entries, &forward);
|
||||
let to_end = visit(&self.result.exits, &reverse);
|
||||
for address in self.result.nodes.keys() {
|
||||
let entry = self.vertex(address.clone(), false);
|
||||
let exit = self.vertex(address.clone(), true);
|
||||
for (missing, code, message) in [
|
||||
(
|
||||
!from_start.contains(&entry),
|
||||
"unreachable_contribution",
|
||||
"No path from a structural caller entry reaches this contribution in this context.",
|
||||
),
|
||||
(
|
||||
!to_end.contains(&exit),
|
||||
"nonterminating_contribution",
|
||||
"No structural caller exit is reachable from this contribution in this context; review the intended loop.",
|
||||
),
|
||||
(
|
||||
self.result.nodes.len() > 1
|
||||
&& !reverse.contains_key(&entry)
|
||||
&& !forward.contains_key(&exit),
|
||||
"isolated_contribution",
|
||||
"This contribution has no incoming or outgoing precedence path in this caller context; assembly incidence is separate.",
|
||||
),
|
||||
] {
|
||||
if missing {
|
||||
self.result.notices.push(ContextNotice {
|
||||
address: address.clone(),
|
||||
code,
|
||||
message: message.into(),
|
||||
relations: vec![],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use super::*;
|
||||
|
||||
fn occurrence(p: &mut Project, owner: Id, name: &str, body: OccurrenceBody) -> Id {
|
||||
let id = p
|
||||
.add_element(Kind::Occurrence, Layer::Sa, name, Some(owner))
|
||||
.unwrap();
|
||||
*p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = SharedBody::Occurrence {
|
||||
body,
|
||||
precondition: String::new(),
|
||||
postcondition: String::new(),
|
||||
};
|
||||
p.elements
|
||||
.get_mut(&owner)
|
||||
.unwrap()
|
||||
.steps_mut()
|
||||
.unwrap()
|
||||
.push(id);
|
||||
id
|
||||
}
|
||||
|
||||
fn repeated(references: usize, links: usize) -> (Project, Id, Id) {
|
||||
let mut p = Project::empty();
|
||||
let root = p
|
||||
.add_element(Kind::FunctionalChain, Layer::Sa, "Caller", None)
|
||||
.unwrap();
|
||||
let procedure = p
|
||||
.add_element(Kind::FunctionalChain, Layer::Sa, "Reusable procedure", None)
|
||||
.unwrap();
|
||||
let mut leaves = Vec::new();
|
||||
for name in ["A", "B"] {
|
||||
let action = p
|
||||
.add_element(Kind::Function, Layer::Sa, name, None)
|
||||
.unwrap();
|
||||
leaves.push(occurrence(
|
||||
&mut p,
|
||||
procedure,
|
||||
name,
|
||||
OccurrenceBody::Action {
|
||||
action: Some(action),
|
||||
lifeline: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
for _ in 0..links {
|
||||
p.relate(
|
||||
RelationKind::SequenceLink,
|
||||
"Authored A to B",
|
||||
leaves[0],
|
||||
leaves[1],
|
||||
);
|
||||
}
|
||||
for _ in 0..references {
|
||||
occurrence(
|
||||
&mut p,
|
||||
root,
|
||||
"Reuse",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(procedure),
|
||||
},
|
||||
);
|
||||
}
|
||||
(p, root, procedure)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edge_work_budget_stops_repeated_expansion_not_just_output_storage() {
|
||||
// Only ~3,000 possible contribution nodes, but a million authored edge
|
||||
// instances. The edge budget must stop actual work before later references.
|
||||
let (p, root, _) = repeated(1_000, 1_000);
|
||||
let limits = AnalysisLimits::default();
|
||||
let index = TopologyIndex::with_cache_capacity(&p, 1);
|
||||
let (first, attempts) = index.analyze_counted(root, limits);
|
||||
assert_eq!(attempts, limits.edges);
|
||||
assert_eq!(first.precedence.len(), limits.edges);
|
||||
assert!(!first.complete);
|
||||
assert!(first.entries.is_empty() && first.exits.is_empty());
|
||||
assert!(
|
||||
first.nodes.len() < 1_000,
|
||||
"later caller references must not be expanded"
|
||||
);
|
||||
assert_eq!(
|
||||
first
|
||||
.notices
|
||||
.iter()
|
||||
.filter(|n| n.code == "context_analysis_limit")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert!(
|
||||
first
|
||||
.notices
|
||||
.iter()
|
||||
.all(|n| n.code == "context_analysis_limit")
|
||||
);
|
||||
let (warm, warm_attempts) = index.analyze_counted(root, limits);
|
||||
assert_eq!(warm_attempts, attempts);
|
||||
assert_eq!(warm.nodes, first.nodes);
|
||||
assert_eq!(warm.precedence, first.precedence);
|
||||
assert_eq!(warm.notices, first.notices);
|
||||
drop(warm);
|
||||
let (uncached, uncached_attempts) =
|
||||
TopologyIndex::with_cache_capacity(&p, 0).analyze_counted(root, limits);
|
||||
assert_eq!(uncached_attempts, attempts);
|
||||
assert_eq!(uncached.precedence, first.precedence);
|
||||
assert_eq!(uncached.notices, first.notices);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unresolved_authored_edges_consume_work_budget_without_stored_output() {
|
||||
let (mut p, root, _) = repeated(20, 100);
|
||||
let missing = Id::nil();
|
||||
for relation in p.relations.values_mut() {
|
||||
relation.body.set_endpoints(relation.source(), missing);
|
||||
}
|
||||
let (result, attempts) = TopologyIndex::new(&p).analyze_counted(
|
||||
root,
|
||||
AnalysisLimits {
|
||||
nodes: 1_000,
|
||||
edges: 17,
|
||||
},
|
||||
);
|
||||
assert_eq!(attempts, 17);
|
||||
assert!(result.precedence.is_empty());
|
||||
assert_eq!(result.nodes.len(), 3);
|
||||
assert!(!result.complete);
|
||||
assert!(result.entries.is_empty() && result.exits.is_empty());
|
||||
assert_eq!(
|
||||
result
|
||||
.notices
|
||||
.iter()
|
||||
.filter(|n| n.code == "context_analysis_limit")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_edge_budget_includes_reference_boundaries_and_node_abort_has_no_bypass() {
|
||||
let (p, root, _) = repeated(1, 2);
|
||||
let index = TopologyIndex::new(&p);
|
||||
let (complete, count) = index.analyze_counted(root, AnalysisLimits { nodes: 3, edges: 4 });
|
||||
assert!(complete.complete);
|
||||
assert_eq!(count, 4); // two authored edges plus entry and exit connectors
|
||||
assert_eq!(
|
||||
complete
|
||||
.precedence
|
||||
.iter()
|
||||
.filter(|e| e.kind == ContextEdgeKind::SyntheticBoundary)
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
for limits in [
|
||||
AnalysisLimits { nodes: 3, edges: 3 },
|
||||
AnalysisLimits {
|
||||
nodes: 1,
|
||||
edges: 10,
|
||||
},
|
||||
] {
|
||||
let (partial, count) = index.analyze_counted(root, limits);
|
||||
assert!(!partial.complete);
|
||||
assert!(partial.entries.is_empty() && partial.exits.is_empty());
|
||||
assert!(count <= limits.edges);
|
||||
assert_eq!(
|
||||
partial
|
||||
.notices
|
||||
.iter()
|
||||
.filter(|n| n.code == "context_analysis_limit")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user