Files
ARCADIA-Studio/crates/oa-desktop/src/views/traceability.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

216 lines
7.3 KiB
Rust

//! Transient browsing over the complete trace/artifact inventory.
use oa_core::{Id, Project};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) enum Filter {
#[default]
All,
Traces,
Artifacts,
}
#[derive(Debug, Clone)]
pub(crate) enum Event {
Filter(Filter),
Search(String),
}
#[derive(Default)]
pub(crate) struct State {
pub filter: Filter,
pub query: String,
}
impl State {
pub fn update(&mut self, event: Event) {
match event {
Event::Filter(filter) => self.filter = filter,
Event::Search(query) => self.query = query,
}
}
pub fn matching(&self, p: &Project) -> (Vec<Id>, Vec<Id>) {
let query = self.query.trim().to_lowercase();
let aliases: std::collections::BTreeMap<_, _> = p.identities().into_iter().collect();
let identity = |id| {
format!(
"{} {} {id}",
p.named(id),
aliases.get(&id).copied().unwrap_or_default()
)
};
let mut traces = if self.filter == Filter::Artifacts {
vec![]
} else {
p.traces
.values()
.filter(|t| {
format!(
"{} {} {} {} {}",
t.name,
t.alias,
t.description,
identity(t.oa),
identity(t.sa)
)
.to_lowercase()
.contains(&query)
})
.map(|t| t.id)
.collect::<Vec<_>>()
};
let mut artifacts = if self.filter == Filter::Traces {
vec![]
} else {
p.artifact_links
.values()
.filter(|a| {
format!(
"{} {} {} {} {} {} {} {} {:?}",
a.name,
a.alias,
a.description,
a.uri,
a.revision.as_deref().unwrap_or_default(),
a.fragment.as_deref().unwrap_or_default(),
identity(a.owner),
owner_label(p, a.owner),
a.relation
)
.to_lowercase()
.contains(&query)
})
.map(|a| a.id)
.collect::<Vec<_>>()
};
traces.sort_by(|a, b| p.traces[a].name.cmp(&p.traces[b].name).then(a.cmp(b)));
artifacts.sort_by(|a, b| {
p.artifact_links[a]
.name
.cmp(&p.artifact_links[b].name)
.then(a.cmp(b))
});
(traces, artifacts)
}
}
pub(super) fn owner_label(p: &Project, owner: Id) -> String {
p.diagrams
.values()
.find_map(|d| {
d.nodes
.get(&owner)
.map(|node| format!("Placement of {} · {}", p.named(node.element), d.name))
})
.unwrap_or_else(|| p.named(owner).to_owned())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::app::{App, Message};
use oa_core::*;
fn fixture() -> (Project, Id, [Id; 3]) {
let mut p = Project::empty();
let oa = p
.add_element(Kind::Activity, Layer::Oa, "Rescue dispatch", None)
.unwrap();
let sa = p
.add_element(Kind::Function, Layer::Sa, "Dispatch controller", None)
.unwrap();
for n in 0..68 {
let target = if n == 0 {
sa
} else {
p.add_element(
Kind::Function,
Layer::Sa,
&format!("Dispatch function {n:02}"),
None,
)
.unwrap()
};
let id = Id::new_v4();
p.traces.insert(
id,
Trace {
id,
alias: format!("review_{n}"),
name: format!("Review {n:02}"),
description: String::new(),
oa,
sa: target,
kind: TraceKind::Manual,
baseline: None,
},
);
}
let ids = [Id::from_u128(502), Id::from_u128(501), Id::from_u128(503)];
for (index, id) in ids.iter().copied().enumerate() {
p.artifact_links.insert(
id,
ArtifactLink {
id,
alias: format!("evidence_{index}"),
name: if index == 2 {
"Zulu schematic"
} else {
"Acceptance evidence"
}
.into(),
description: "Engineering evidence".into(),
owner: oa,
relation: ArtifactRelation::References,
uri: format!("./evidence/document_{index}.md"),
revision: Some(format!("revision-{index}")),
fragment: None,
},
);
}
validate_candidate(&p).unwrap();
(p, oa, ids)
}
#[test]
fn traceability_filter_reaches_artifacts_after_68_traces_and_searches_all_metadata() {
let (p, _, ids) = fixture();
let mut state = State::default();
assert_eq!(state.matching(&p).0.len(), 68);
state.update(Event::Filter(Filter::Artifacts));
assert_eq!(state.matching(&p), (vec![], vec![ids[1], ids[0], ids[2]]));
for query in ["document_2", "evidence_2", "revision-2", "Zulu schematic"] {
state.update(Event::Search(query.into()));
assert_eq!(state.matching(&p), (vec![], vec![ids[2]]));
}
state.update(Event::Search("Rescue dispatch".into()));
assert_eq!(state.matching(&p).1.len(), 3);
state.update(Event::Filter(Filter::Traces));
assert_eq!(state.matching(&p).0.len(), 68);
assert!(state.matching(&p).1.is_empty());
}
#[test]
fn traceability_browsing_retains_invalid_property_draft_and_existing_select_guard() {
let (p, owner, ids) = fixture();
let mut app = App::with_project(p);
let _ = app.update(Message::Select(owner));
let _ = app.update(Message::Field("/alias".into(), "invalid alias".into()));
let raw = serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap();
let before = app.document.snapshot();
for event in [
Event::Filter(Filter::Artifacts),
Event::Search("document_2".into()),
Event::Filter(Filter::All),
] {
let _ = app.update(Message::Traceability(event));
assert_eq!(app.selected, owner);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &before));
}
let _ = app.update(Message::Select(ids[2]));
assert_eq!(app.selected, owner);
assert_eq!(
serde_json::to_value(app.draft.as_ref().unwrap().recovery_snapshot()).unwrap(),
raw
);
assert!(std::sync::Arc::ptr_eq(&app.document.snapshot(), &before));
}
}