Files
ARCADIA-Studio/crates/oa-core/src/topology_context.rs
T
Codex e2ecfea69b
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
Checkpoint source 7 workflows and in-progress diagram authoring redesign
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.
2026-09-10 10:06:55 -07:00

353 lines
13 KiB
Rust

//! 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<Id>,
pub exchange: Option<Id>,
}
#[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<Id>,
layer: Layer,
kind: Kind,
reference: Option<Id>,
work: Option<Id>,
exchange: Option<Id>,
contribution: bool,
}
pub struct TopologyIndex<'a> {
project: &'a Project,
records: BTreeMap<Id, Record>,
children: BTreeMap<Id, Vec<Id>>,
link_definitions: BTreeMap<Id, Vec<Id>>,
contextual_callers: BTreeSet<Id>,
cache: RefCell<BTreeMap<ContextAddress, ResolvedOccurrence>>,
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<Id, ContextError> {
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<ResolvedOccurrence, ContextError> {
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<Id> {
self.children.get(&sequence).cloned().unwrap_or_default()
}
}