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.
216 lines
7.3 KiB
Rust
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));
|
|
}
|
|
}
|