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,352 @@
|
||||
//! 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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user