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.
353 lines
13 KiB
Rust
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()
|
|
}
|
|
}
|