//! Bounded static admission for the actual Project catalog and analysis records. use super::typecheck::BindingContext; use super::*; use crate::analysis_numeric::{ResolvedKind, Unit}; use crate::{Id, Kind, Layer, Project, SharedBody}; use std::collections::{BTreeMap, BTreeSet}; /// Provisional ceiling; final acceptance requires the root-owned measured gate. /// This is separate from runtime work limits and must be recorded by consumers. /// Events cover analysis record/node visits, queued child slots, scanned text /// bytes, parameter lookups and dependency/relation hops. Fixed-size type and /// exact-number operations have bounded cost within their containing event; /// this does not meter the existing architectural validator or wire allocation. pub const MAX_STATIC_EVENTS: u64 = 1_000_000; #[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] #[serde(deny_unknown_fields)] pub struct AdmissionLimits { pub max_static_events: u64, } impl AdmissionLimits { pub const REFERENCE: Self = Self { max_static_events: MAX_STATIC_EVENTS, }; } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] pub struct AdmissionUsage { pub static_events: u64, pub stored_expression_nodes: usize, } pub(super) struct Budget { limit: u64, pub(super) usage: AdmissionUsage, exhausted: bool, } impl Budget { pub(super) fn charge( &mut self, owner: Id, field: &str, events: u64, ) -> Result<(), AnalysisError> { if self.exhausted || events > self.limit.saturating_sub(self.usage.static_events) { self.exhausted = true; return Err(AnalysisError::new( owner, field, "analysis_limit", "Static admission work limit exhausted.", )); } self.usage.static_events += events; Ok(()) } } /// Admits a complete Project; this neither evaluates nor chooses a case. /// Caller lowering is explicit. Passing a ceiling above the profile is an error. pub fn admit( project: &Project, limits: AdmissionLimits, ) -> Result, AnalysisError> { let environment = admit_structure_checked(project, limits)?; if let Some(issue) = crate::validation::validate_structure(project) .into_iter() .find(|issue| issue.severity == crate::validation::Severity::Error) { return Err(AnalysisError::new( issue.element.unwrap_or(project.id), issue.field.unwrap_or_default(), &issue.code, issue.message, )); } Ok(environment) } pub(crate) fn admit_structure_checked( project: &Project, limits: AdmissionLimits, ) -> Result, AnalysisError> { if limits.max_static_events > MAX_STATIC_EVENTS { return Err(AnalysisError::new( project.id, "analysis_limits", "analysis_limit", "Static event limit exceeds the provisional reference profile.", )); } let mut budget = Budget { limit: limits.max_static_events, usage: AdmissionUsage::default(), exhausted: false, }; budget.charge(project.id, "analysis", 1)?; let mut environment = ResolvedAnalysis { project, kinds: BTreeMap::new(), units: BTreeMap::new(), limits, usage: AdmissionUsage::default(), }; resolve_definitions(&mut environment, &mut budget)?; let index = Index::build(&environment, &mut budget)?; check_bindings(&environment, &mut budget)?; check_graphs(&environment, &index, &mut budget)?; environment.usage = budget.usage; Ok(environment) } fn resolve_definitions( environment: &mut ResolvedAnalysis<'_>, budget: &mut Budget, ) -> Result<(), AnalysisError> { let project = environment.project; for (id, definition) in &project.definitions { budget.charge(*id, "definition", 1)?; if id != &definition.id { return Err(AnalysisError::new( *id, "id", "identity_mismatch", "Definition map key must equal its identity.", )); } for (field, value) in [ ("name", &definition.name), ("description", &definition.description), ] { budget.charge(*id, field, value.len() as u64)?; if !crate::validation::valid_text(value) { return Err(AnalysisError::new( *id, field, "invalid_text", "Definition text contains a forbidden character.", )); } } if let DefinitionBody::QuantityKind(kind) = &definition.body && kind.algebra == QuantityAlgebra::Linear { environment.kinds.insert( *id, ResolvedKind::linear(KindHandle::local(project.id, *id), kind.dimension), ); } } for (id, definition) in &project.definitions { budget.charge(*id, "definition", 1)?; if let DefinitionBody::QuantityKind(QuantityKindDefinition { dimension, algebra: QuantityAlgebra::Affine { difference }, }) = &definition.body { let difference = environment.kind(*difference).ok_or_else(|| { AnalysisError::new( *id, "difference", "analysis_kind", "Affine difference must reference a linear quantity kind.", ) })?; if difference.is_affine() || difference.dimension() != *dimension { return Err(AnalysisError::new( *id, "difference", "analysis_kind", "Affine point and linear difference kinds must have identical dimensions.", )); } let resolved = ResolvedKind::affine(KindHandle::local(project.id, *id), difference) .map_err(|error| { AnalysisError::new(*id, "difference", "analysis_kind", error.to_string()) })?; environment.kinds.insert(*id, resolved); } } for (id, definition) in &project.definitions { budget.charge(*id, "definition", 1)?; if let DefinitionBody::Unit(unit) = &definition.body { let kind = environment .kind(unit.kind) .ok_or_else(|| { AnalysisError::new( *id, "kind", "analysis_kind", "Unit must reference a quantity kind.", ) })? .clone(); budget.charge(*id, "symbol", unit.symbol.len() as u64)?; if !crate::validation::valid_text(&unit.symbol) { return Err(AnalysisError::new( *id, "symbol", "invalid_text", "Unit symbol contains a forbidden character.", )); } let resolved = Unit::new(kind, unit.scale.clone(), unit.offset.clone()).map_err(|error| { AnalysisError::new(*id, "unit", "analysis_unit", error.to_string()) })?; environment.units.insert(*id, resolved); } } Ok(()) } #[derive(Default)] struct Info { nodes: usize, properties: BTreeSet, calls: BTreeSet, } impl Info { fn read( owner: Id, field: &str, binding: &Binding, budget: &mut Budget, ) -> Result { let mut info = Self::default(); let Binding::Expression(expression) = binding else { return Ok(info); }; let mut pending = vec![(expression, 1usize)]; while let Some((node, depth)) = pending.pop() { budget.charge(owner, field, 1)?; if depth > MAX_EXPRESSION_DEPTH { return Err(AnalysisError::new( owner, field, "analysis_depth", "Expression exceeds logical depth 128.", )); } info.nodes += 1; budget.usage.stored_expression_nodes += 1; if info.nodes > MAX_CLOSURE_NODES { return Err(AnalysisError::new( owner, field, "analysis_nodes", "Expression exceeds the stored closure node limit.", )); } let children = match node { Expr::List(items) => { if items.len() > MAX_LIST_LENGTH { return Err(AnalysisError::new( owner, field, "analysis_list", "List exceeds 1024 entries.", )); } items.len() } Expr::Call { arguments, .. } => { if arguments.len() > MAX_CLOSURE_NODES { return Err(AnalysisError::new( owner, field, "analysis_nodes", "Call argument count exceeds the stored closure limit.", )); } arguments.len() } Expr::Unary { .. } | Expr::Power { .. } | Expr::AsKind { .. } => 1, Expr::Binary { .. } | Expr::Sum { .. } => 2, Expr::If { .. } => 3, _ => 0, }; if info .nodes .saturating_add(pending.len()) .saturating_add(children) > MAX_CLOSURE_NODES { return Err(AnalysisError::new( owner, field, "analysis_nodes", "Visited and pending expression nodes exceed the stored closure limit.", )); } // Precharge variable-width iteration and allocation before enqueue. budget.charge(owner, field, children as u64)?; let mut push = |child| pending.push((child, depth + 1)); match node { Expr::Property(id) => { info.properties.insert(*id); } Expr::List(items) => { for child in items.iter().rev() { push(child); } } Expr::Unary { operand, .. } => push(operand), Expr::Binary { left, right, .. } => { push(right); push(left); } Expr::If { condition, when_true, when_false, } => { push(when_false); push(when_true); push(condition); } Expr::Call { definition, arguments, } => { info.calls.insert(definition.local()); for child in arguments.values().rev() { push(child); } } Expr::Sum { values, initial } => { push(values); push(initial); } Expr::Power { base, .. } => push(base), Expr::AsKind { value, .. } => push(value), _ => {} } } Ok(info) } } #[derive(Default)] struct Index { calculations: BTreeMap, properties: BTreeMap, checks: BTreeMap, cases: BTreeMap>, } impl Index { fn build( environment: &ResolvedAnalysis<'_>, budget: &mut Budget, ) -> Result { let mut index = Self::default(); for (id, definition) in &environment.project.definitions { budget.charge(*id, "definition", 1)?; if let DefinitionBody::Calculation(calculation) = &definition.body { index .calculations .insert(*id, Info::read(*id, "body", &calculation.body, budget)?); } } for (id, element) in &environment.project.elements { budget.charge(*id, "element", 1)?; if element.kind().is_analysis() && element.parent.is_some() { return Err(AnalysisError::new( *id, "parent", "analysis_parent", "Analysis records are layer roots; subject is an explicit reference.", )); } match element.shared() { Some(SharedBody::EngineeringProperty { base, .. }) => { index .properties .insert(*id, Info::read(*id, "base", base, budget)?); } Some(SharedBody::AnalysisCheck { predicate, .. }) => { index .checks .insert(*id, Info::read(*id, "predicate", predicate, budget)?); } Some(SharedBody::AnalysisCase { overrides, .. }) => { let mut cases = BTreeMap::new(); for (property, binding) in overrides { budget.charge(*id, "overrides", 1)?; cases.insert(*property, Info::read(*id, "overrides", binding, budget)?); } index.cases.insert(*id, cases); } _ => {} } } Ok(index) } } fn subject( environment: &ResolvedAnalysis<'_>, owner: Id, layer: Layer, value: AnalysisSubject, budget: &mut Budget, ) -> Result<(), AnalysisError> { let project = environment.project; budget.charge(owner, "subject", 1)?; let valid = match value { AnalysisSubject::Element(id) => project .elements .get(&id) .is_some_and(|element| element.layer() == layer && !element.kind().is_analysis()), AnalysisSubject::Relation(id) => { project.relations.contains_key(&id) && relation_layer(project, id, owner, budget)? == Some(layer) } }; if valid { Ok(()) } else { Err(AnalysisError::new( owner, "subject", "analysis_subject", "Subject must be an existing architectural element or relation in the same layer.", )) } } /// Match Project::layer_of without unmetered relation-chain traversal before /// architectural validation has had a chance to reject malformed endpoints. fn relation_layer( project: &Project, id: Id, owner: Id, budget: &mut Budget, ) -> Result, AnalysisError> { let mut current = id; let mut seen = BTreeSet::new(); loop { budget.charge(owner, "subject", 1)?; if !seen.insert(current) { return Ok(None); } if let Some(element) = project.elements.get(¤t) { return Ok(Some(element.layer())); } let Some(relation) = project.relations.get(¤t) else { return Ok(None); }; current = relation.source(); } } fn declared( environment: &ResolvedAnalysis<'_>, owner: Id, field: &str, value: &AnalysisType, ) -> Result { environment.declared_type(value).ok_or_else(|| { AnalysisError::new( owner, field, "analysis_kind", "Declared quantity kind does not resolve to a quantity-kind definition.", ) }) } fn check_bindings( environment: &ResolvedAnalysis<'_>, budget: &mut Budget, ) -> Result<(), AnalysisError> { let no_parameters = BTreeMap::new(); for (id, definition) in &environment.project.definitions { budget.charge(*id, "definition", 1)?; if let DefinitionBody::Calculation(calculation) = &definition.body { let mut parameters = BTreeMap::new(); let mut names = BTreeSet::new(); for parameter in &calculation.parameters { budget.charge(*id, "parameters", 1)?; budget.charge(*id, "parameters", parameter.name.len() as u64)?; if !crate::valid_alias(¶meter.name) || !names.insert(¶meter.name) { return Err(AnalysisError::new( *id, "parameters", "analysis_parameter", "Parameter names must be unique valid identifiers in their calculation.", )); } let value = declared(environment, *id, "parameters", ¶meter.value_type)?; if parameters.insert(parameter.id, value).is_some() { return Err(AnalysisError::new( *id, "parameters", "analysis_parameter", "Parameter identities must be unique within their calculation.", )); } } let expected = declared(environment, *id, "result", &calculation.result)?; super::typecheck::binding( environment, BindingContext { owner: *id, field: "body", layer: None, parameters: ¶meters, }, &calculation.body, &expected, budget, )?; } } for (id, element) in &environment.project.elements { budget.charge(*id, "element", 1)?; let layer = element.layer(); match element.shared() { Some(SharedBody::EngineeringProperty { subject: target, value_type, display_unit, base, }) => { subject(environment, *id, layer, *target, budget)?; let expected = declared(environment, *id, "value_type", value_type)?; if let Some(reference) = display_unit { let unit = environment.unit(*reference).ok_or_else(|| { AnalysisError::new( *id, "display_unit", "analysis_unit", "Display unit must reference a unit definition.", ) })?; let scalar = match &expected { ResolvedType::Scalar(value) | ResolvedType::List(value) => value, }; if scalar != &ResolvedScalar::Numeric(crate::analysis_numeric::NumericType::Named( unit.kind().clone(), )) { return Err(AnalysisError::new( *id, "display_unit", "analysis_unit", "Display unit must have the property's declared nominal quantity kind.", )); } } super::typecheck::binding( environment, BindingContext { owner: *id, field: "base", layer: Some(layer), parameters: &no_parameters, }, base, &expected, budget, )?; } Some(SharedBody::AnalysisCase { subject: target, overrides, .. }) => { if let Some(target) = target { subject(environment, *id, layer, *target, budget)?; } for (property, binding) in overrides { budget.charge(*id, "overrides", 1)?; let target = environment.project.elements.get(property).ok_or_else(|| { AnalysisError::new( *id, "overrides", "analysis_property", "Override target does not exist.", ) })?; let Some(SharedBody::EngineeringProperty { value_type, .. }) = target.shared() else { return Err(AnalysisError::new( *id, "overrides", "analysis_property", "Override target must be an engineering property.", )); }; if target.layer() != layer { return Err(AnalysisError::new( *id, "overrides", "analysis_layer", "Case and overridden property must share a layer.", )); } let expected = declared(environment, *id, "overrides", value_type)?; super::typecheck::binding( environment, BindingContext { owner: *id, field: "overrides", layer: Some(layer), parameters: &no_parameters, }, binding, &expected, budget, )?; } } Some(SharedBody::AnalysisCheck { subject: target, documented_constraint, predicate, }) => { if let Some(target) = target { subject(environment, *id, layer, *target, budget)?; } if let Some(constraint) = documented_constraint && !environment .project .elements .get(constraint) .is_some_and(|target| { target.kind() == Kind::Constraint && target.layer() == layer }) { return Err(AnalysisError::new( *id, "documented_constraint", "analysis_constraint", "Check documentation must reference a constraint in the same layer.", )); } super::typecheck::binding( environment, BindingContext { owner: *id, field: "predicate", layer: Some(layer), parameters: &no_parameters, }, predicate, &ResolvedType::boolean(), budget, )?; } _ => {} } } Ok(()) } /// Iterative graph admission; neither model depth nor call depth grows this stack. fn acyclic( owner: Id, field: &str, graph: &BTreeMap>, call_graph: bool, budget: &mut Budget, ) -> Result<(), AnalysisError> { let mut degrees = BTreeMap::new(); let mut users: BTreeMap> = BTreeMap::new(); let mut heights = BTreeMap::new(); let mut ready = BTreeSet::new(); for (id, dependencies) in graph { budget.charge(owner, field, 1)?; degrees.insert(*id, dependencies.len()); heights.insert(*id, 1usize); if dependencies.is_empty() { ready.insert(*id); } for dependency in dependencies { budget.charge(owner, field, 1)?; if !graph.contains_key(dependency) { return Err(AnalysisError::new( owner, field, "analysis_reference", "Dependency is outside its typed graph.", )); } users.entry(*dependency).or_default().push(*id); } } let mut visited = 0usize; while let Some(id) = ready.pop_first() { budget.charge(owner, field, 1)?; visited += 1; let height = heights[&id]; if call_graph && height > MAX_CALL_DEPTH { return Err(AnalysisError::new( owner, field, "analysis_call_depth", "Calculation call graph exceeds 32 active definition calls.", )); } if let Some(dependents) = users.get(&id) { for dependent in dependents { budget.charge(owner, field, 1)?; let degree = degrees.get_mut(dependent).expect("indexed graph node"); *degree -= 1; let current = heights.get_mut(dependent).expect("indexed graph node"); *current = (*current).max(height + 1); if *degree == 0 { ready.insert(*dependent); } } } } if visited != graph.len() { return Err(AnalysisError::new( owner, field, "analysis_cycle", "Stored dependencies contain a cycle, including statically unused branches.", )); } Ok(()) } fn closure( owner: Id, base_nodes: usize, calls: impl IntoIterator, index: &Index, budget: &mut Budget, ) -> Result<(), AnalysisError> { let mut nodes = base_nodes; let mut pending = Vec::new(); for id in calls { budget.charge(owner, "closure", 1)?; pending.push(id); } let mut seen = BTreeSet::new(); while let Some(id) = pending.pop() { budget.charge(owner, "closure", 1)?; if !seen.insert(id) { continue; } let info = index.calculations.get(&id).ok_or_else(|| { AnalysisError::new( owner, "closure", "analysis_calculation", "Calculation dependency does not resolve.", ) })?; nodes = nodes.checked_add(info.nodes).ok_or_else(|| { AnalysisError::new( owner, "closure", "analysis_nodes", "Stored node count overflow.", ) })?; if nodes > MAX_CLOSURE_NODES { return Err(AnalysisError::new( owner, "closure", "analysis_nodes", "Stored effective closure exceeds 65,536 expression nodes.", )); } for dependency in &info.calls { budget.charge(owner, "closure", 1)?; pending.push(*dependency); } } if nodes > MAX_CLOSURE_NODES { return Err(AnalysisError::new( owner, "closure", "analysis_nodes", "Stored effective closure exceeds 65,536 expression nodes.", )); } Ok(()) } fn check_graphs( environment: &ResolvedAnalysis<'_>, index: &Index, budget: &mut Budget, ) -> Result<(), AnalysisError> { let project = environment.project; let mut calls = BTreeMap::new(); for (id, info) in &index.calculations { budget.charge(*id, "calls", 1 + info.calls.len() as u64)?; calls.insert(*id, info.calls.clone()); } acyclic(project.id, "calculations", &calls, true, budget)?; for id in index.calculations.keys() { closure(*id, 0, [*id], index, budget)?; } for layer in [Layer::Oa, Layer::Sa] { let mut contexts = vec![EvaluationContext::ModelBaseline]; for id in index.cases.keys() { budget.charge(*id, "context", 1)?; if project.elements[id].layer() == layer { contexts.push(EvaluationContext::Case(*id)); } } for context in contexts { let owner = match context { EvaluationContext::ModelBaseline => project.id, EvaluationContext::Case(id) => id, }; let mut graph = BTreeMap::new(); let mut nodes = 0usize; let mut definitions = BTreeSet::new(); for (id, base) in &index.properties { budget.charge(owner, "effective_properties", 1)?; if project.elements[id].layer() != layer { continue; } let effective = match context { EvaluationContext::ModelBaseline => base, EvaluationContext::Case(case) => index.cases[&case].get(id).unwrap_or(base), }; budget.charge( owner, "effective_properties", (effective.properties.len() + effective.calls.len()) as u64, )?; nodes = nodes.checked_add(effective.nodes).ok_or_else(|| { AnalysisError::new( owner, "closure", "analysis_nodes", "Stored node count overflow.", ) })?; graph.insert(*id, effective.properties.clone()); definitions.extend(&effective.calls); } for (id, info) in &index.checks { budget.charge(owner, "effective_checks", 1)?; if project.elements[id].layer() == layer { budget.charge(owner, "effective_checks", info.calls.len() as u64)?; nodes = nodes.checked_add(info.nodes).ok_or_else(|| { AnalysisError::new( owner, "closure", "analysis_nodes", "Stored node count overflow.", ) })?; definitions.extend(&info.calls); } } acyclic(owner, "effective_properties", &graph, false, budget)?; closure(owner, nodes, definitions, index, budget)?; } } Ok(()) } #[cfg(test)] mod meter_tests { use super::*; fn budget(limit: u64) -> Budget { Budget { limit, usage: AdmissionUsage::default(), exhausted: false, } } #[test] fn static_meter_exact_boundary_zero_charge_and_exhaustion_latch() { let owner = Id::nil(); let mut budget = Budget { limit: 3, usage: AdmissionUsage::default(), exhausted: false, }; budget.charge(owner, "test", 0).unwrap(); budget.charge(owner, "test", 3).unwrap(); budget.charge(owner, "test", 0).unwrap(); assert_eq!(budget.usage.static_events, 3); assert!(budget.charge(owner, "test", 1).is_err()); assert!(budget.charge(owner, "test", 0).is_err()); assert!(budget.charge(owner, "test", u64::MAX).is_err()); assert_eq!(budget.usage.static_events, 3); let mut zero = Budget { limit: 0, usage: AdmissionUsage::default(), exhausted: false, }; zero.charge(owner, "test", 0).unwrap(); assert!(zero.charge(owner, "test", 1).is_err()); assert!(zero.exhausted); } #[test] fn wide_expression_children_are_charged_before_they_are_queued() { let owner = Id::nil(); let width = 1024; for value in [ Binding::Expression(Expr::List(vec![Expr::Boolean(true); width])), Binding::Expression(Expr::Call { definition: DefinitionRef::Local(Id::from_u128(1)), arguments: (0..width) .map(|id| (ParameterId(Id::from_u128(id as u128)), Expr::Boolean(true))) .collect(), }), ] { let mut short = budget(width as u64); let failure = match Info::read(owner, "probe", &value, &mut short) { Ok(_) => panic!("Child queue must be precharged"), Err(error) => error, }; assert_eq!(failure.code, "analysis_limit"); assert_eq!(short.usage.static_events, 1); assert_eq!(short.usage.stored_expression_nodes, 1); assert!(short.exhausted); let mut exact = budget((2 * width + 1) as u64); let info = Info::read(owner, "probe", &value, &mut exact).unwrap(); assert_eq!(info.nodes, width + 1); assert_eq!(exact.usage.static_events, (2 * width + 1) as u64); } } #[test] fn call_key_scan_and_argument_lookup_have_separate_precharges() { let mut project = Project::empty(); let calculation = Id::from_u128(1); let parameters: Vec<_> = (0..64) .map(|n| Parameter { id: ParameterId(Id::from_u128(100 + n)), name: format!("argument_{n}"), value_type: AnalysisType::Scalar(ScalarType::Boolean), }) .collect(); let mut arguments: BTreeMap<_, _> = parameters .iter() .map(|parameter| (parameter.id, Expr::Boolean(true))) .collect(); project.definitions.insert( calculation, Definition { id: calculation, alias: "call_probe".into(), name: "Call probe".into(), description: String::new(), body: DefinitionBody::Calculation(CalculationDefinition { parameters, result: AnalysisType::Scalar(ScalarType::Boolean), body: Binding::Unbound, }), }, ); let environment = admit(&project, AdmissionLimits::REFERENCE).unwrap(); let missing = ParameterId(Id::from_u128(163)); arguments.remove(&missing); arguments.insert(ParameterId(Id::from_u128(999)), Expr::Boolean(true)); let mut value = Binding::Expression(Expr::Call { definition: DefinitionRef::Local(calculation), arguments, }); let no_parameters = BTreeMap::new(); let check = |value: &Binding, budget: &mut Budget| { super::super::typecheck::binding( &environment, BindingContext { owner: project.id, field: "probe", layer: None, parameters: &no_parameters, }, value, &ResolvedType::boolean(), budget, ) }; let mut short = budget(64); assert_eq!( check(&value, &mut short).unwrap_err().code, "analysis_limit" ); assert_eq!(short.usage.static_events, 1); let mut keys_only = budget(65); assert_eq!( check(&value, &mut keys_only).unwrap_err().code, "analysis_arguments" ); assert_eq!(keys_only.usage.static_events, 65); let Binding::Expression(Expr::Call { arguments, .. }) = &mut value else { panic!() }; arguments.remove(&ParameterId(Id::from_u128(999))); arguments.insert(missing, Expr::Boolean(true)); let mut exact = budget(193); // One Call + 64 keys + 64 lookups + 64 leaf visits. check(&value, &mut exact).unwrap(); assert_eq!(exact.usage.static_events, 193); let mut one_short = budget(192); assert_eq!( check(&value, &mut one_short).unwrap_err().code, "analysis_limit" ); } #[test] fn relation_subject_resolution_charges_each_hop_and_bounds_cycles() { let mut project = Project::empty(); let element = Id::from_u128(2000); project.elements.insert( element, crate::Element { id: element, alias: "subject".into(), name: "Subject".into(), description: String::new(), parent: None, body: crate::ElementBody::Oa(crate::OaBody::Activity), }, ); let first = Id::from_u128(3000); let last = Id::from_u128(3063); // These deliberately malformed relation endpoints reach analysis before // architecture admission; no valid architectural chain is claimed. for n in 3000..3064 { let id = Id::from_u128(n); project.relations.insert( id, crate::Relation { id, alias: format!("relation_{n}"), name: "Probe".into(), description: String::new(), body: crate::RelationBody::Allocation { source: if id == last { element } else { Id::from_u128(n + 1) }, target: element, }, }, ); } let mut short = budget(64); assert_eq!( relation_layer(&project, first, project.id, &mut short) .unwrap_err() .code, "analysis_limit" ); assert_eq!(short.usage.static_events, 64); let mut exact = budget(65); assert_eq!( relation_layer(&project, first, project.id, &mut exact).unwrap(), Some(Layer::Oa) ); assert_eq!(exact.usage.static_events, 65); project.relations.get_mut(&last).unwrap().body = crate::RelationBody::Allocation { source: first, target: element, }; let mut cycle = budget(65); assert_eq!( relation_layer(&project, first, project.id, &mut cycle).unwrap(), None ); assert_eq!(cycle.usage.static_events, 65); project.relations.get_mut(&last).unwrap().body = crate::RelationBody::Allocation { source: Id::from_u128(9999), target: element, }; let mut missing = budget(65); assert_eq!( relation_layer(&project, first, project.id, &mut missing).unwrap(), None ); assert_eq!(missing.usage.static_events, 65); } }