Files
ARCADIA-Studio/crates/oa-desktop/src/behavior/topology/contextual.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

788 lines
28 KiB
Rust

//! Lazy qualified authoring over the core address resolver. A folded reference
//! never replaces its retained endpoint with the reference or a same-named leaf.
use super::*;
use iced::widget::column;
use oa_core::topology_context::TopologyIndex;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
pub enum Purpose {
#[default]
Precedence,
Exchange,
CommonWork,
}
impl std::fmt::Display for Purpose {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Precedence => "Precedence",
Self::Exchange => "Exchange assembly",
Self::CommonWork => "Common work incidence",
})
}
}
#[derive(Debug, Clone)]
pub enum Event {
Side(bool),
Pick(OccurrenceEndpoint),
Expand(OccurrenceEndpoint),
Up,
Root,
Purpose(Purpose),
Evidence(Choice),
Guard(String),
Search(String),
Page(usize),
}
#[derive(Default, serde::Serialize, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Draft {
edit: Option<Id>,
source: Option<OccurrenceEndpoint>,
target: Option<OccurrenceEndpoint>,
target_side: bool,
purpose: Purpose,
evidence: Option<Id>,
guard: String,
search: String,
page: usize,
#[serde(default)]
scope: Vec<Id>,
}
impl Draft {
pub(super) fn seed_source(&mut self, endpoint: OccurrenceEndpoint) {
if self.edit.is_none() {
self.source = Some(endpoint);
}
}
pub(super) fn new(p: &Project, owner: Id, selected: Option<Id>) -> Self {
if let Some(r) = selected.and_then(|id| p.relations.get(&id))
&& let RelationBody::ContextualLink {
owner: caller,
source,
target,
purpose,
} = &r.body
&& *caller == owner
{
let (purpose, evidence, guard) = match purpose {
ContextualLinkPurpose::Precedence { guard } => {
(Purpose::Precedence, None, guard.clone())
}
ContextualLinkPurpose::Exchange { exchange } => {
(Purpose::Exchange, Some(*exchange), String::new())
}
ContextualLinkPurpose::CommonWork { work } => {
(Purpose::CommonWork, Some(*work), String::new())
}
};
return Self {
edit: Some(r.id),
source: Some(source.clone()),
target: Some(target.clone()),
purpose,
evidence,
guard,
..Self::default()
};
}
Self {
source: selected
.filter(|id| is_contribution(p, owner, *id))
.map(|occurrence| OccurrenceEndpoint {
references: vec![],
occurrence,
}),
..Self::default()
}
}
pub(super) fn update(&mut self, event: Event) {
match event {
Event::Side(target) => self.target_side = target,
Event::Pick(endpoint) => {
if self.target_side {
self.target = Some(endpoint);
} else {
self.source = Some(endpoint);
}
}
Event::Expand(endpoint) => {
self.scope = endpoint.references;
self.scope.push(endpoint.occurrence);
self.search.clear();
self.page = 0;
}
Event::Up => {
self.scope.pop();
self.search.clear();
self.page = 0;
}
Event::Root => {
self.scope.clear();
self.search.clear();
self.page = 0;
}
Event::Purpose(purpose) => self.purpose = purpose,
Event::Evidence(choice) => self.evidence = Some(choice.id),
Event::Guard(raw) => self.guard = raw,
Event::Search(raw) => {
self.search = raw;
self.page = 0;
}
Event::Page(page) => self.page = page,
}
}
fn purpose(&self) -> Result<ContextualLinkPurpose, String> {
Ok(match self.purpose {
Purpose::Precedence => ContextualLinkPurpose::Precedence {
guard: self.guard.clone(),
},
Purpose::Exchange => ContextualLinkPurpose::Exchange {
exchange: self
.evidence
.ok_or("Choose the exact exchange used for assembly.")?,
},
Purpose::CommonWork => ContextualLinkPurpose::CommonWork {
work: self.evidence.ok_or("Choose the shared work identity.")?,
},
})
}
pub(super) fn command(&self, p: &Project, owner: Id, name: &str) -> Result<Command, String> {
let source = self
.source
.as_ref()
.ok_or("Choose the qualified source contribution.")?;
let target = self
.target
.as_ref()
.ok_or("Choose the qualified target contribution.")?;
let purpose = self.purpose()?;
TopologyIndex::new(p)
.validate_link(owner, source, target, &purpose)
.map_err(|e| e.to_string())?;
let mut relation = if let Some(id) = self.edit {
p.relations.get(&id).filter(|r| matches!(&r.body, RelationBody::ContextualLink { owner: caller, .. } if *caller==owner)).ok_or("This caller-owned link no longer exists.")?.clone()
} else {
let id = Id::new_v4();
Relation {
id,
alias: p.fresh_alias("contextual_link", "relation", id),
name: self.purpose.to_string(),
description: String::new(),
body: RelationBody::ContextualLink {
owner,
source: source.clone(),
target: target.clone(),
purpose: purpose.clone(),
},
}
};
if !name.trim().is_empty() {
relation.name = name.to_owned();
}
relation.body = RelationBody::ContextualLink {
owner,
source: source.clone(),
target: target.clone(),
purpose,
};
let command = if self.edit.is_some() {
Command::EditRelation(relation.id, relation)
} else {
Command::CreateRelation(relation)
};
execute_batch(p, std::slice::from_ref(&command)).map_err(|e| e.to_string())?;
Ok(command)
}
}
pub(crate) fn address_label(p: &Project, owner: Id, endpoint: &OccurrenceEndpoint) -> String {
std::iter::once(&owner)
.chain(endpoint.references.iter())
.chain(std::iter::once(&endpoint.occurrence))
.map(|id| {
p.elements
.get(id)
.map(|e| format!("{} (@{})", e.name, e.alias))
.unwrap_or_else(|| format!("Unavailable {id}"))
})
.collect::<Vec<_>>()
.join(" → ")
}
struct AddressRow {
endpoint: OccurrenceEndpoint,
reference: bool,
}
fn rows(p: &Project, owner: Id, scope: &[Id], search: &str) -> Result<Vec<AddressRow>, String> {
let index = TopologyIndex::new(p);
let sequence = index
.resolve_scope(owner, scope)
.map_err(|e| e.to_string())?;
let query = search.trim().to_lowercase();
Ok(index
.contributions(sequence)
.into_iter()
.filter_map(|occurrence| {
let endpoint = OccurrenceEndpoint {
references: scope.to_vec(),
occurrence,
};
if !address_label(p, owner, &endpoint)
.to_lowercase()
.contains(&query)
{
return None;
}
let reference = matches!(
p.elements.get(&occurrence).and_then(|e| e.shared()),
Some(SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { sequence: Some(_) },
..
})
);
Some(AddressRow {
endpoint,
reference,
})
})
.collect())
}
fn event(value: Event) -> Message {
super::event(super::Event::Contextual(value))
}
pub(super) fn preview(
index: &TopologyIndex<'_>,
p: &Project,
owner: Id,
reference: Id,
expanded: &BTreeSet<OccurrenceEndpoint>,
limit: usize,
) -> Vec<OccurrenceEndpoint> {
let children = |endpoint: &OccurrenceEndpoint| {
if index.resolve(owner, endpoint).is_err() {
return vec![];
}
let sequence = p
.elements
.get(&endpoint.occurrence)
.and_then(|e| e.shared())
.and_then(|body| match body {
SharedBody::Occurrence {
body: OccurrenceBody::SequenceReference { sequence },
..
} => *sequence,
_ => None,
});
let Some(sequence) = sequence else {
return vec![];
};
let available: BTreeSet<_> = index.contributions(sequence).into_iter().collect();
let mut references = endpoint.references.clone();
references.push(endpoint.occurrence);
p.elements
.get(&sequence)
.and_then(|e| e.steps())
.into_iter()
.flatten()
.filter(|id| available.contains(id))
.map(|occurrence| OccurrenceEndpoint {
references: references.clone(),
occurrence: *occurrence,
})
.collect::<Vec<_>>()
};
let root = OccurrenceEndpoint {
references: vec![],
occurrence: reference,
};
let mut pending = children(&root);
pending.reverse();
let mut visible = vec![];
while let Some(endpoint) = pending.pop() {
if visible.len() == limit {
break;
}
if index.resolve(owner, &endpoint).is_err() {
continue;
}
if expanded.contains(&endpoint) {
pending.extend(children(&endpoint).into_iter().rev());
}
visible.push(endpoint);
}
visible
}
pub(super) fn view<'a>(p: &'a Project, owner: Id, d: &'a Draft) -> Element<'a, Message> {
let mut out = column![
text("Choose each endpoint in its caller context. Enter a reference to browse all its contributions; repeated uses stay distinct. Browsing preserves both selected endpoints.").size(12),
row![button("Choose source").style(if !d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(false))), button("Choose target").style(if d.target_side { button::primary } else { style::nav }).on_press(event(Event::Side(true)))].spacing(6),
text(format!("Source: {}", d.source.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12),
text(format!("Target: {}", d.target.as_ref().map(|a| address_label(p,owner,a)).unwrap_or("Choose a contribution".into()))).size(12),
pick_list(vec![Purpose::Precedence,Purpose::Exchange,Purpose::CommonWork],Some(d.purpose),|p| event(Event::Purpose(p))).width(Fill).text_size(12).padding(8)
].spacing(7);
match d.purpose {
Purpose::Precedence => {
out = out.push(
text_input("Precedence guard (optional)", &d.guard)
.on_input(|v| event(Event::Guard(v)))
.padding(8)
.size(12),
)
}
purpose => {
let choices = if purpose == Purpose::Exchange {
targets(p, owner, Tool::Exchange)
} else {
targets(p, owner, Tool::Work)
};
let selected = d.evidence.map(|id| Choice {
id,
label: p.named(id).to_owned(),
});
out = out
.push(
pick_list(choices, selected, |c| event(Event::Evidence(c)))
.placeholder("Choose exact engineering identity…")
.width(Fill)
.text_size(12)
.padding(8),
)
.push(
text("Assembly incidence does not imply execution order or fuse executions.")
.size(11),
);
}
}
out = out.push(
text_input("Search all contributions in this scope…", &d.search)
.on_input(|v| event(Event::Search(v)))
.padding(7)
.size(12),
);
let scope_label = std::iter::once(&owner)
.chain(d.scope.iter())
.map(|id| {
p.elements
.get(id)
.map(|e| format!("{} (@{})", e.name, e.alias))
.unwrap_or_else(|| format!("Unavailable {id}"))
})
.collect::<Vec<_>>()
.join(" → ");
out = out
.push(text(format!("Browsing: {scope_label}")).size(12))
.push(
row![
button("Root").on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Root))),
button("Up one reference")
.on_press_maybe((!d.scope.is_empty()).then(|| event(Event::Up)))
]
.spacing(6),
);
let rows = match rows(p, owner, &d.scope, &d.search) {
Ok(rows) => rows,
Err(error) => return out.push(text(error).size(12)).into(),
};
let range = super::view::choice_page(rows.len(), d.page);
let page = range.start / 24;
out = out.push(
row![
button("Previous").on_press_maybe((page > 0).then(|| event(Event::Page(page - 1)))),
text(format!(
"{} matching contributions · page {}",
rows.len(),
page + 1
))
.size(11),
button("Next")
.on_press_maybe((range.end < rows.len()).then(|| event(Event::Page(page + 1))))
]
.spacing(6),
);
for r in &rows[range] {
let mut line = row![
button(text(address_label(p, owner, &r.endpoint)).size(12))
.width(Fill)
.on_press(event(Event::Pick(r.endpoint.clone())))
]
.spacing(5);
if r.reference {
line = line
.push(button("Enter reference").on_press(event(Event::Expand(r.endpoint.clone()))));
}
out = out.push(line);
}
out.into()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::document::Document;
use oa_core::source_project::ProjectLimits;
fn fixture(layer: Layer) -> (Project, Id, Id, Id, Id, Id) {
let f = super::super::direct_tests::fixture(layer);
let mut p = f.p;
let sequence = p
.make_element(
if layer == Layer::Oa {
Kind::Process
} else {
Kind::FunctionalChain
},
layer,
"Reusable check",
None,
)
.unwrap();
let sequence_id = sequence.id;
p = execute_batch(&p, &[Command::CreateElement(sequence)]).unwrap();
let leaf = occurrence(
&p,
sequence_id,
"Verify pressure",
OccurrenceBody::Action {
action: Some(f.work[0]),
lifeline: None,
},
);
let leaf_id = leaf.id;
p = execute_batch(&p, &[Command::CreateElement(leaf)]).unwrap();
let a = occurrence(
&p,
f.owner,
"First check",
OccurrenceBody::SequenceReference {
sequence: Some(sequence_id),
},
);
let b = occurrence(
&p,
f.owner,
"Second check",
OccurrenceBody::SequenceReference {
sequence: Some(sequence_id),
},
);
let (a_id, b_id) = (a.id, b.id);
p = execute_batch(&p, &[Command::CreateElement(a), Command::CreateElement(b)]).unwrap();
(p, f.owner, a_id, b_id, leaf_id, f.work[0])
}
#[test]
fn qualified_link_gui_repeated_sources_remain_distinct_and_apply_has_exact_source_history() {
for layer in [Layer::Oa, Layer::Sa] {
let (p, owner, a, b, leaf, work) = fixture(layer);
let mut state = State::default();
state
.update(
&p,
super::super::Event::Start(owner, Tool::Contextual, None),
)
.unwrap();
let source = OccurrenceEndpoint {
references: vec![a],
occurrence: leaf,
};
let target = OccurrenceEndpoint {
references: vec![b],
occurrence: leaf,
};
for event in [
Event::Pick(source.clone()),
Event::Side(true),
Event::Pick(target.clone()),
Event::Purpose(Purpose::CommonWork),
Event::Evidence(Choice {
id: work,
label: String::new(),
}),
] {
state
.update(&p, super::super::Event::Contextual(event))
.unwrap();
}
let raw = state.recovery_snapshot().unwrap().unwrap();
let restored = State::from_recovery(raw.clone()).unwrap();
assert_eq!(restored.recovery_snapshot().unwrap(), Some(raw));
let command = restored.prepare_pending(&p).unwrap();
let Command::CreateRelation(record) = &command else {
panic!("one caller-owned record")
};
assert_eq!(
record.body,
RelationBody::ContextualLink {
owner,
source: source.clone(),
target: target.clone(),
purpose: ContextualLinkPurpose::CommonWork { work }
}
);
let expected = execute_batch(&p, std::slice::from_ref(&command)).unwrap();
let mut doc = Document::from_project(p.clone(), ProjectLimits::default()).unwrap();
let before = doc.bundle().clone();
doc.execute(command.clone()).unwrap();
assert_eq!(doc.project(), &expected);
assert_eq!(
doc.project().elements,
p.elements,
"no copied work or source occurrences"
);
let after = doc.bundle().clone();
let reopened = Document::from_bundle(after.clone(), ProjectLimits::default()).unwrap();
assert_eq!(reopened.project(), &expected);
assert!(doc.undo());
assert_eq!(doc.bundle(), &before);
assert!(doc.redo());
assert_eq!(doc.bundle(), &after);
assert_ne!(
address_label(&p, owner, &source),
address_label(&p, owner, &target)
);
assert!(
Graph::new(&expected, owner, record.id)
.edges
.iter()
.any(|edge| edge.id == record.id && edge.incidence)
);
}
}
#[test]
fn qualified_link_gui_rejected_endpoint_keeps_exact_raw_intent_after_recovery() {
let (p, owner, a, _, leaf, _) = fixture(Layer::Oa);
let mut state = State::default();
state
.update(
&p,
super::super::Event::Start(owner, Tool::Contextual, None),
)
.unwrap();
for event in [
Event::Pick(OccurrenceEndpoint {
references: vec![a],
occurrence: leaf,
}),
Event::Side(true),
Event::Pick(OccurrenceEndpoint {
references: vec![Id::new_v4()],
occurrence: leaf,
}),
Event::Guard(" pressure > && ".into()),
] {
state
.update(&p, super::super::Event::Contextual(event))
.unwrap();
}
let before = state.recovery_snapshot().unwrap();
assert!(state.prepare_pending(&p).is_err());
assert_eq!(state.recovery_snapshot().unwrap(), before);
let restored = State::from_recovery(before.clone().unwrap()).unwrap();
assert!(restored.prepare_pending(&p).is_err());
assert_eq!(restored.recovery_snapshot().unwrap(), before);
}
#[test]
fn qualified_link_gui_deleted_transient_selection_does_not_seed_or_hide_retained_authored_endpoint()
{
let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa);
let endpoint = OccurrenceEndpoint {
references: vec![reference],
occurrence: leaf,
};
let mut state = State::default();
state.select_context(oa_core::topology_context::ContextAddress {
owner,
endpoint: endpoint.clone(),
});
state
.update(
&p,
super::super::Event::Start(owner, Tool::Contextual, None),
)
.unwrap();
let before = state.recovery_snapshot().unwrap();
let deleted = execute_batch(&p, &[Command::Delete(leaf)]).unwrap();
assert!(state.qualified_selection(&deleted, owner).is_none());
assert_eq!(
state.recovery_snapshot().unwrap(),
before,
"Authored endpoint is retained for explicit repair"
);
assert!(state.prepare_pending(&deleted).is_err());
state.remember(owner, reference);
assert_eq!(state.back(&deleted), Some((owner, reference)));
assert!(state.selected_context.is_none());
state.draft = None;
state
.update(
&deleted,
super::super::Event::Start(owner, Tool::Contextual, None),
)
.unwrap();
assert!(
state
.draft
.as_ref()
.unwrap()
.contextual
.as_ref()
.unwrap()
.source
.is_none()
);
}
#[test]
fn qualified_link_gui_scope_paging_and_search_reach_tail_beyond_512_without_losing_endpoints() {
let (p, owner, reference, _, leaf, _) = fixture(Layer::Oa);
let sequence = p.elements[&leaf].parent.unwrap();
let mut commands = Vec::new();
let mut tails = Vec::new();
for (scope_index, scope) in [owner, sequence].into_iter().enumerate() {
for n in 0..520 {
let mut record = occurrence(
&p,
scope,
&format!("Contribution {n:04}"),
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
record.alias = format!("scope_{scope_index}_contribution_{n:04}");
if n == 519 {
tails.push(record.id);
}
commands.push(Command::CreateElement(record));
}
}
let p = execute_batch(&p, &commands).unwrap();
let all = rows(&p, owner, &[], "").unwrap();
assert!(all.len() > 512);
let last_page = super::super::view::choice_page(all.len(), usize::MAX);
assert!(
all[last_page]
.iter()
.any(|row| row.endpoint.occurrence == tails[0])
);
let found = rows(&p, owner, &[], "Contribution 0519").unwrap();
assert_eq!(found.len(), 1);
assert_eq!(found[0].endpoint.occurrence, tails[0]);
let mut draft = Draft::default();
draft.update(Event::Pick(OccurrenceEndpoint {
references: vec![reference],
occurrence: leaf,
}));
draft.update(Event::Side(true));
draft.update(Event::Pick(found[0].endpoint.clone()));
let chosen = (draft.source.clone(), draft.target.clone());
draft.update(Event::Expand(OccurrenceEndpoint {
references: vec![],
occurrence: reference,
}));
draft.update(Event::Search("Contribution 0519".into()));
let nested = rows(&p, owner, &draft.scope, &draft.search).unwrap();
assert_eq!(nested.len(), 1);
assert_eq!(
nested[0].endpoint,
OccurrenceEndpoint {
references: vec![reference],
occurrence: tails[1]
}
);
let saved = serde_json::to_value(&draft).unwrap();
let mut restored: Draft = serde_json::from_value(saved.clone()).unwrap();
assert_eq!(serde_json::to_value(&restored).unwrap(), saved);
restored.update(Event::Up);
assert!(restored.scope.is_empty());
assert_eq!((restored.source.clone(), restored.target.clone()), chosen);
assert!(restored.command(&p, owner, "Tail constraint").is_ok());
let mut historical = saved;
historical.as_object_mut().unwrap().remove("scope");
let restored: Draft = serde_json::from_value(historical).unwrap();
assert!(restored.scope.is_empty());
assert_eq!((restored.source, restored.target), chosen);
}
#[test]
fn qualified_link_gui_nested_preview_folds_only_the_exact_reference_instance() {
let (mut p, owner, a, b, leaf, _) = fixture(Layer::Oa);
let source = p.elements[&leaf].parent.unwrap();
let deep = p
.make_element(Kind::Process, Layer::Oa, "Nested definition", None)
.unwrap();
let deep_id = deep.id;
p = execute_batch(&p, &[Command::CreateElement(deep)]).unwrap();
let deep_leaf = occurrence(
&p,
deep_id,
"Nested control",
OccurrenceBody::Control {
kind: ControlKind::Choice,
},
);
let deep_leaf_id = deep_leaf.id;
let nested = occurrence(
&p,
source,
"Nested use",
OccurrenceBody::SequenceReference {
sequence: Some(deep_id),
},
);
let nested_id = nested.id;
p = execute_batch(
&p,
&[
Command::CreateElement(deep_leaf),
Command::CreateElement(nested),
],
)
.unwrap();
let path = OccurrenceEndpoint {
references: vec![a],
occurrence: nested_id,
};
let mut state = State::default();
assert!(state.expand_context(&p, owner, path.clone()));
state.expanded.insert(b);
let graph = Graph::expanded_qualified(
&p,
owner,
owner,
&state.expanded,
&state.contextual_expanded,
);
assert!(
graph
.nodes
.iter()
.find(|n| n.id == a)
.unwrap()
.preview
.iter()
.any(|(v, _)| v.references == vec![a, nested_id] && v.occurrence == deep_leaf_id)
);
assert!(
!graph
.nodes
.iter()
.find(|n| n.id == b)
.unwrap()
.preview
.iter()
.any(|(v, _)| v.occurrence == deep_leaf_id)
);
assert!(state.expand_context(&p, owner, path));
let graph = Graph::expanded_qualified(
&p,
owner,
owner,
&state.expanded,
&state.contextual_expanded,
);
assert!(
!graph
.nodes
.iter()
.any(|n| n.preview.iter().any(|(v, _)| v.occurrence == deep_leaf_id))
);
}
}