//! Caller-qualified process/chain addresses. Cache residency never affects admission. use crate::*; use std::{ cell::RefCell, collections::{BTreeMap, BTreeSet}, }; mod analysis; pub use analysis::*; pub const MAX_REFERENCE_DEPTH: usize = 64; pub const MAX_ANALYSIS_NODES: usize = 50_000; pub const MAX_ANALYSIS_EDGES: usize = 100_000; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub struct ContextAddress { pub owner: Id, pub endpoint: OccurrenceEndpoint, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct ResolvedOccurrence { pub address: ContextAddress, pub defining_sequence: Id, pub work: Option, pub exchange: Option, } #[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] #[error("{message}")] pub struct ContextError { pub code: &'static str, pub message: String, } fn failure(code: &'static str, message: &str) -> ContextError { ContextError { code, message: message.into(), } } struct Record { parent: Option, layer: Layer, kind: Kind, reference: Option, work: Option, exchange: Option, contribution: bool, } pub struct TopologyIndex<'a> { project: &'a Project, records: BTreeMap, children: BTreeMap>, link_definitions: BTreeMap>, contextual_callers: BTreeSet, cache: RefCell>, cache_capacity: usize, } impl<'a> TopologyIndex<'a> { pub fn new(project: &'a Project) -> Self { Self::with_cache_capacity(project, 4096) } pub fn with_cache_capacity(project: &'a Project, cache_capacity: usize) -> Self { let records: BTreeMap<_, _> = project .elements .values() .map(|e| { let (reference, work, exchange, contribution) = match e.shared() { Some(SharedBody::Occurrence { body, .. }) => match body { OccurrenceBody::SequenceReference { sequence } => { (*sequence, None, None, true) } OccurrenceBody::Action { action, .. } => (None, *action, None, true), OccurrenceBody::Exchange { exchange, .. } => (None, None, *exchange, true), OccurrenceBody::Control { .. } => (None, None, None, true), _ => (None, None, None, false), }, _ => (None, None, None, false), }; ( e.id, Record { parent: e.parent, layer: e.layer(), kind: e.kind(), reference, work, exchange, contribution, }, ) }) .collect(); let mut children: BTreeMap<_, _> = project .elements .values() .filter_map(|e| e.steps().map(|steps| (e.id, steps.clone()))) .collect(); let mut child_sets: BTreeMap<_, BTreeSet<_>> = children .iter() .map(|(owner, children)| (*owner, children.iter().copied().collect())) .collect(); for (id, record) in &records { if record.contribution && let Some(parent) = record.parent { let list = children.entry(parent).or_default(); if child_sets.entry(parent).or_default().insert(*id) { list.push(*id); } } } for (owner, list) in &mut children { list.retain(|id| { records .get(id) .is_some_and(|r| r.parent == Some(*owner) && r.contribution) }); } let mut link_definitions: BTreeMap<_, Vec<_>> = BTreeMap::new(); let mut contextual_callers = BTreeSet::new(); for relation in project.relations.values() { let owner = relation.contextual_owner().or_else(|| { if relation.kind() == RelationKind::SequenceLink { records.get(&relation.source()).and_then(|r| r.parent) } else { None } }); if let Some(owner) = owner { link_definitions.entry(owner).or_default().push(relation.id); } if let Some(owner) = relation.contextual_owner() { contextual_callers.insert(owner); } } let mut callers: BTreeMap<_, Vec<_>> = BTreeMap::new(); for record in records.values() { if let (Some(parent), Some(reference)) = (record.parent, record.reference) { callers.entry(reference).or_default().push(parent); } } let mut pending: Vec<_> = contextual_callers.iter().copied().collect(); while let Some(sequence) = pending.pop() { for caller in callers.get(&sequence).into_iter().flatten() { if contextual_callers.insert(*caller) { pending.push(*caller); } } } Self { project, records, children, link_definitions, contextual_callers, cache: RefCell::new(BTreeMap::new()), cache_capacity, } } /// Resolve a caller path while an endpoint leaf is still being edited. pub fn resolve_scope(&self, owner: Id, references: &[Id]) -> Result { if references.len() > MAX_REFERENCE_DEPTH { return Err(failure( "context_depth", "A contextual path exceeds 64 references.", )); } let root = self.records.get(&owner).ok_or_else(|| { failure( "context_owner", "The caller must be an existing process or functional chain.", ) })?; if !matches!(root.kind, Kind::Process | Kind::FunctionalChain) { return Err(failure( "context_owner", "The caller must be a process or functional chain.", )); } let mut current = owner; let mut visited = BTreeSet::from([owner]); for step in references { let reference = self.records.get(step).ok_or_else(|| { failure( "context_path", "A reference occurrence in the endpoint path no longer exists.", ) })?; if reference.parent != Some(current) || reference.layer != root.layer { return Err(failure( "context_path", "Each reference occurrence must belong directly to the preceding caller context.", )); } let next = reference.reference.ok_or_else(|| { failure( "context_path", "Each path component must be a resolved sequence-reference occurrence.", ) })?; let target = self.records.get(&next).ok_or_else(|| { failure( "context_path", "A referenced process or chain no longer exists.", ) })?; if target.layer != root.layer || target.kind != root.kind { return Err(failure( "context_path", "References must resolve to sequences of the same kind and layer.", )); } if !visited.insert(next) { return Err(failure( "context_cycle", "A contextual endpoint cannot traverse recursive sequence reuse.", )); } current = next; } Ok(current) } pub fn resolve( &self, owner: Id, endpoint: &OccurrenceEndpoint, ) -> Result { if endpoint.references.len() > MAX_REFERENCE_DEPTH { return Err(failure( "context_depth", "A contextual endpoint exceeds the 64-reference depth limit.", )); } let address = ContextAddress { owner, endpoint: endpoint.clone(), }; if let Some(cached) = self.cache.borrow().get(&address) { return Ok(cached.clone()); } let current = self.resolve_scope(owner, &endpoint.references)?; let root = &self.records[&owner]; let leaf = self.records.get(&endpoint.occurrence).ok_or_else(|| { failure( "context_leaf", "The endpoint contribution no longer exists.", ) })?; if leaf.parent != Some(current) || leaf.layer != root.layer || !leaf.contribution { return Err(failure( "context_leaf", "The endpoint must be a direct contribution of its resolved process or chain.", )); } let resolved = ResolvedOccurrence { address: address.clone(), defining_sequence: current, work: leaf.work, exchange: leaf.exchange, }; if self.cache_capacity > 0 { let mut cache = self.cache.borrow_mut(); if cache.len() >= self.cache_capacity { cache.pop_first(); } cache.insert(address, resolved.clone()); } Ok(resolved) } /// Validate a caller-owned semantic connection, independent of graphical expansion. pub fn validate_link( &self, owner: Id, source: &OccurrenceEndpoint, target: &OccurrenceEndpoint, purpose: &ContextualLinkPurpose, ) -> Result<(ResolvedOccurrence, ResolvedOccurrence), ContextError> { let a = self.resolve(owner, source)?; let b = self.resolve(owner, target)?; if a.address == b.address { return Err(failure( "context_self", "A contextual link needs two distinct occurrence addresses.", )); } match purpose { ContextualLinkPurpose::Precedence { guard } => { if source.references.is_empty() && target.references.is_empty() { return Err(failure( "context_direct_precedence", "Use a SequenceLink for precedence between two direct contributions.", )); } if !validation::valid_text(guard) { return Err(failure( "invalid_text", "The precedence guard contains invalid text.", )); } let previous = a.exchange.and_then(|id| self.project.relations.get(&id)); let next = b.exchange.and_then(|id| self.project.relations.get(&id)); let mismatch = match (a.work, previous, b.work, next) { (Some(work), _, _, Some(exchange)) => work != exchange.source(), (_, Some(exchange), Some(work), _) => exchange.target() != work, (_, Some(previous), _, Some(next)) => previous.target() != next.source(), _ => false, }; if mismatch { return Err(failure( "sequence_exchange_endpoint", "Adjacent work/exchange contributions must agree with the referenced exchange endpoints.", )); } } ContextualLinkPurpose::Exchange { exchange } => { let relation = self.project.relations.get(exchange).ok_or_else(|| { failure("context_exchange", "Choose an existing work exchange.") })?; let expected = if self.project.layer_of(owner) == Some(Layer::Oa) { RelationKind::Interaction } else { RelationKind::FunctionalExchange }; if relation.kind() != expected || self.project.layer_of(*exchange) != self.project.layer_of(owner) || a.work != Some(relation.source()) || b.work != Some(relation.target()) { return Err(failure( "context_exchange", "The selected same-layer exchange must connect the exact work referenced by the source and target Actions.", )); } } ContextualLinkPurpose::CommonWork { work } => { if !self.project.elements.get(work).is_some_and(|e| { e.kind().is_work() && Some(e.layer()) == self.project.layer_of(owner) }) || a.work != Some(*work) || b.work != Some(*work) { return Err(failure( "context_common_work", "Both Actions must reference the exact same existing work in this layer.", )); } } } Ok((a, b)) } /// Direct child candidates for lazy UI exploration; never expands a Cartesian reuse tree. pub fn contributions(&self, sequence: Id) -> Vec { self.children.get(&sequence).cloned().unwrap_or_default() } }