Files
ARCADIA-Studio/crates/oa-core/examples/multiselect_native.rs
T

717 lines
24 KiB
Rust

//! Native evidence fixture/checker; install as a temporary oa-core example.
//! This invariant evaluator derives delta from saved A, NOT from pointer input.
use oa_core::{source_project::*, *};
use std::{fs, io::Write, path::Path};
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
fn id(alias: &str) -> Id {
// Make the intended OA view first for default UUID-ordered view selection.
if alias == "oa_view" {
return Id::from_u128(0x29_0001);
}
Id::new_v5(
&Id::from_u128(0xc1192026_0923_5000_8000_000000000029),
alias.as_bytes(),
)
}
fn point(x: f32, y: f32) -> Position {
Position { x, y }
}
fn element(p: &mut Project, alias: &str, name: &str, kind: Kind, layer: Layer, parent: Option<Id>) {
p.elements.insert(
id(alias),
Element {
id: id(alias),
alias: alias.into(),
name: name.into(),
description: format!("Preserve engineering record {alias}."),
parent,
body: ElementBody::new(kind, layer).unwrap(),
},
);
}
fn diagram(p: &mut Project, alias: &str, name: &str, layer: Layer) {
p.diagrams.insert(
id(alias),
Diagram {
id: id(alias),
alias: alias.into(),
name: name.into(),
description: "Selection changes placement geometry only.".into(),
layer,
projection: ProjectionKind::Overview,
view_options: Default::default(),
nodes: Default::default(),
routes: vec![],
},
);
}
fn place(p: &mut Project, view: &str, alias: &str, object: &str, position: Position, size: Size) {
p.diagrams.get_mut(&id(view)).unwrap().nodes.insert(
id(alias),
NodePlacement {
id: id(alias),
alias: alias.into(),
element: id(object),
position,
size,
collapsed: false,
port_anchors: vec![],
},
);
}
fn route(edge: &str, pins: Vec<Position>) -> EdgeRoute {
EdgeRoute {
edge: id(edge),
waypoints: pins,
source_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Right,
offset: 0.25,
}),
target_attachment: Some(EdgeAttachment {
side: PortAnchorSide::Left,
offset: 0.7,
}),
label: Some(EdgeLabel {
fraction: 0.45,
offset: point(7., -19.),
}),
}
}
fn fixture() -> Result<Project> {
let mut p = Project::empty();
p.id = id("project");
p.alias = "multiselect_native".into();
p.name = "Multiselect native".into();
p.description =
"Shift-select A and B; move together, retain C and engineering semantics.".into();
for (alias, name, parent) in [
("activity_a", "A - receive request", None),
("activity_b", "B - assess request", None),
("activity_c", "C - dispatch response", None),
(
"activity_child",
"A.1 - classify request",
Some(id("activity_a")),
),
] {
element(&mut p, alias, name, Kind::Activity, Layer::Oa, parent);
}
element(
&mut p,
"sa_function",
"Technical request processing",
Kind::Function,
Layer::Sa,
None,
);
for (alias, name, source, target) in [
("internal", "Request assessment", "activity_a", "activity_b"),
("external", "Response decision", "activity_b", "activity_c"),
] {
let old = p.relate(RelationKind::Interaction, name, id(source), id(target));
reidentify(&mut p, old, id(alias))?;
p.relations.get_mut(&id(alias)).unwrap().alias = alias.into();
}
diagram(
&mut p,
"oa_view",
"Multiselect native - OA overview",
Layer::Oa,
);
diagram(&mut p, "oa_other", "Other OA view - preserve", Layer::Oa);
diagram(&mut p, "sa_view", "SA engineering - preserve", Layer::Sa);
let size = Size {
width: 224.,
height: 94.,
};
place(
&mut p,
"oa_view",
"node_a",
"activity_a",
point(80., 80.),
Size {
width: 310.,
height: 260.,
},
);
place(
&mut p,
"oa_view",
"node_b",
"activity_b",
point(470., 120.),
size,
);
place(
&mut p,
"oa_view",
"node_c",
"activity_c",
point(780., 270.),
size,
);
place(
&mut p,
"oa_view",
"node_child",
"activity_child",
point(120., 210.),
size,
);
place(
&mut p,
"oa_other",
"other_a",
"activity_a",
point(40., 60.),
size,
);
place(
&mut p,
"sa_view",
"sa_node",
"sa_function",
point(100., 110.),
size,
);
p.diagrams.get_mut(&id("oa_view")).unwrap().routes = vec![
route("internal", vec![point(420., 145.), point(420., 186.)]),
route("external", vec![point(725., 150.), point(725., 336.)]),
];
p.traces.insert(
id("trace"),
Trace {
id: id("trace"),
alias: "preserved_refinement".into(),
name: "Reviewed request refinement".into(),
description: "Group geometry and Hide preserve this trace and both endpoints.".into(),
oa: id("activity_a"),
sa: id("sa_function"),
kind: TraceKind::Refinement,
baseline: Some(TraceBaseline {
oa: p.properties(id("activity_a")).unwrap(),
sa: p.properties(id("sa_function")).unwrap(),
semantic: None,
}),
},
);
validate_candidate(&p)?;
Ok(p)
}
fn load(path: &Path) -> Result<Project> {
if path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("arcadia"))
{
let disk = source_files::load_project(path, ProjectLimits::default())?;
// This deliberately partial engineering fixture has completeness
// warnings (allocation/behavior context). They are not source errors.
// Compilation, candidate validation and whole-Project equality still
// reject malformed source or any unexpected model change.
if disk
.diagnostics
.iter()
.any(|d| d.diagnostic.severity == validation::Severity::Error)
{
return Err(format!("Source discovery diagnostics: {:?}", disk.diagnostics).into());
}
let compiled = compile_bundle(&disk.reachable_bundle(), ProjectLimits::default())
.map_err(|e| format!("Source compile diagnostics: {e:?}"))?;
Ok(compiled.decoded.project)
} else {
Ok(persistence::load(path)?)
}
}
fn equal(expected: &Project, actual: &Project) -> Result<()> {
if expected == actual {
return Ok(());
}
let e = serde_json::to_value(expected)?;
let a = serde_json::to_value(actual)?;
let fields: Vec<_> = e
.as_object()
.unwrap()
.keys()
.filter(|key| e[*key] != a[*key])
.cloned()
.collect();
Err(format!("Whole Project mismatch in {fields:?}; no partial PASS").into())
}
fn roundtrip(p: &Project) -> Result<SourceBundle> {
validate_candidate(p)?;
equal(p, &persistence::decode(&serde_json::to_vec(p)?)?)?;
let bundle = format_bundle(p);
let compiled = compile_bundle(&bundle, ProjectLimits::default())
.map_err(|e| format!("Roundtrip: {e:?}"))?;
equal(p, &compiled.decoded.project)?;
Ok(bundle)
}
fn write_new(path: &Path, bytes: &[u8]) -> Result<()> {
fs::create_dir_all(path.parent().ok_or("Missing parent directory")?)?;
fs::OpenOptions::new()
.write(true)
.create_new(true)
.open(path)?
.write_all(bytes)?;
Ok(())
}
fn generate(out: &Path) -> Result<()> {
if out.exists() {
return Err("Output directory already exists; choose a fresh path".into());
}
let p = fixture()?;
let bundle = roundtrip(&p)?;
write_new(&out.join("baseline.json"), &serde_json::to_vec_pretty(&p)?)?;
for (path, source) in &bundle.files {
write_new(&out.join("source").join(path), source.as_bytes())?;
}
let names = [
"project",
"oa_view",
"oa_other",
"sa_view",
"activity_a",
"activity_b",
"activity_c",
"activity_child",
"node_a",
"node_b",
"node_c",
"node_child",
"internal",
"external",
"trace",
"sa_function",
"sa_node",
"other_a",
];
let ids: std::collections::BTreeMap<_, _> =
names.into_iter().map(|name| (name, id(name))).collect();
write_new(
&out.join("manifest.json"),
&serde_json::to_vec_pretty(&serde_json::json!({
"fixture": "multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry,
"move_members": ["node_a", "node_b", "node_child"], "fixed_member": "node_c",
"evaluation": "Whole Project invariant comparison; saved A supplies bounded shared delta, not an exact pointer oracle",
"hide": "Remove node_a/node_b only; retain child, stored routes, all model records and other views",
}))?,
)?;
equal(&p, &load(&out.join("baseline.json"))?)?;
equal(&p, &load(&out.join("source/project.arcadia"))?)?;
println!("PASS generated JSON and source bundle: {}", out.display());
Ok(())
}
fn check(mode: &str, baseline: &Path, saved: &Path) -> Result<()> {
let before = load(baseline)?;
let actual = load(saved)?;
validate_candidate(&before)?;
validate_candidate(&actual)?;
if before.id != id("project") {
return Err("Not this fixture's baseline project".into());
}
let mut expected = before.clone();
let mut delta = None;
match mode {
"unchanged" => {}
"hide" => {
let view = expected
.diagrams
.get_mut(&id("oa_view"))
.ok_or("Missing OA view")?;
for alias in ["node_a", "node_b"] {
view.nodes
.remove(&id(alias))
.ok_or("Baseline selected placement missing")?;
}
// references::remove_objects removes routes only when their semantic
// edge is deleted. Hide deletes placements; both routes MUST survive.
}
"moved" => {
let old = before
.diagrams
.get(&id("oa_view"))
.ok_or("Missing baseline OA view")?;
let new = actual
.diagrams
.get(&id("oa_view"))
.ok_or("Missing saved OA view")?;
let a = old
.nodes
.get(&id("node_a"))
.ok_or("Missing baseline A")?
.position;
let moved_a = new
.nodes
.get(&id("node_a"))
.ok_or("Missing saved A")?
.position;
let d = point(moved_a.x - a.x, moved_a.y - a.y);
if !d.x.is_finite()
|| !d.y.is_finite()
|| d.x.abs().max(d.y.abs()) > 500.
|| d.x.hypot(d.y) < 10.
{
return Err(format!(
"Move delta must have length >=10 and each axis <=500 world units: {d:?}"
)
.into());
}
if moved_a.x % 10. != 0. || moved_a.y % 10. != 0. {
return Err("Saved anchor is not on 10-unit grid".into());
}
let view = expected.diagrams.get_mut(&id("oa_view")).unwrap();
// Independent explicit fixture enumeration: no group/descendant/
// routing planner is called to predict the result.
for alias in ["node_a", "node_b", "node_child"] {
let node = view
.nodes
.get_mut(&id(alias))
.ok_or("Missing expected moved placement")?;
node.position.x += d.x;
node.position.y += d.y;
}
let internal = view
.routes
.iter_mut()
.find(|route| route.edge == id("internal"))
.ok_or("Missing internal pins")?;
for pin in &mut internal.waypoints {
pin.x += d.x;
pin.y += d.y;
}
delta = Some(d);
}
_ => return Err("Mode must be moved, hide, or unchanged".into()),
}
equal(&expected, &actual)?;
roundtrip(&actual)?;
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"result": "PASS", "mode": mode, "baseline": baseline, "saved": saved,
"comparison": "complete Project equality", "inferred_common_delta": delta,
"limits": "Invariant evaluator only; no pointer/action/focus/Undo/source-byte acceptance claim",
}))?
);
Ok(())
}
fn main() -> Result<()> {
let args: Vec<_> = std::env::args().skip(1).collect();
match args.as_slice() {
[command, out] if command == "generate" => generate(Path::new(out)),
[command, mode, baseline, saved] if command == "check" => check(mode, Path::new(baseline), Path::new(saved)),
_ => Err("Usage: multiselect-native-producer generate DIRECTORY | check moved|hide|unchanged BASELINE SAVED".into()),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::BTreeSet;
// Produce evidence through real validated commands; the checker itself uses
// explicit fixture enumeration and never calls this movement implementation.
fn moved(before: &Project, delta: Position) -> Project {
let command = diagram_edit::translate_placements(
before,
id("oa_view"),
&BTreeSet::from([id("node_a"), id("node_b")]),
delta,
)
.unwrap();
let mut session = Session::new(before.clone());
session.execute(command).unwrap();
session.project().clone()
}
fn hidden(before: &Project) -> Project {
let mut session = Session::new(before.clone());
session
.execute(Command::Batch(vec![
Command::Delete(id("node_a")),
Command::Delete(id("node_b")),
]))
.unwrap();
session.project().clone()
}
fn through_saved_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> {
let dir = tempfile::tempdir()?;
let before = dir.path().join("before.json");
let saved = dir.path().join("saved.json");
// These assertions distinguish meaningful invariant corruptions from
// malformed projects rejected before the evaluator even sees them.
validate_candidate(baseline).expect("test baseline must be valid");
validate_candidate(actual).expect("corruption must remain a valid model");
persistence::save(&before, baseline)?;
persistence::save(&saved, actual)?;
check(mode, &before, &saved)
}
fn mismatch(mode: &str, before: &Project, actual: &Project, case: &str) {
let error = through_saved_json(mode, before, actual)
.expect_err(case)
.to_string();
assert!(
error.contains("Whole Project mismatch"),
"{case}: unexpected rejection: {error}"
);
}
fn edge<'a>(p: &'a mut Project, alias: &str) -> &'a mut EdgeRoute {
p.diagrams
.get_mut(&id("oa_view"))
.unwrap()
.routes
.iter_mut()
.find(|route| route.edge == id(alias))
.unwrap()
}
#[test]
fn generated_json_and_real_source_bundle_are_accepted_unchanged() {
let dir = tempfile::tempdir().unwrap();
let out = dir.path().join("fixture");
generate(&out).unwrap();
let baseline = out.join("baseline.json");
let saved = out.join("saved.json");
persistence::save(&saved, &fixture().unwrap()).unwrap();
check("unchanged", &baseline, &saved).unwrap();
check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap();
let bytes = fs::read(&baseline).unwrap();
assert!(
generate(&out).is_err(),
"generation cannot overwrite evidence"
);
assert_eq!(fs::read(&baseline).unwrap(), bytes);
let invalid = dir.path().join("invalid.arcadia");
fs::write(&invalid, "project {").unwrap();
assert!(load(&invalid).is_err(), "source errors must still reject");
}
#[test]
fn real_group_command_outputs_pass_and_are_not_unchanged() {
let before = fixture().unwrap();
for delta in [point(40., 60.), point(-20., 30.)] {
let actual = moved(&before, delta);
through_saved_json("moved", &before, &actual).unwrap();
mismatch(
"unchanged",
&before,
&actual,
"real movement cannot count as unchanged",
);
}
}
#[test]
fn real_hide_command_outputs_pass_before_and_after_movement() {
let initial = fixture().unwrap();
for before in [initial.clone(), moved(&initial, point(40., 60.))] {
let actual = hidden(&before);
through_saved_json("hide", &before, &actual).unwrap();
mismatch(
"unchanged",
&before,
&actual,
"real Hide cannot count as unchanged",
);
}
}
#[test]
fn omitted_and_double_moved_child_are_rejected() {
let before = fixture().unwrap();
let delta = point(40., 60.);
let correct = moved(&before, delta);
let initial = before.diagrams[&id("oa_view")].nodes[&id("node_child")].position;
for position in [
initial,
point(initial.x + 2. * delta.x, initial.y + 2. * delta.y),
] {
let mut bad = correct.clone();
bad.diagrams
.get_mut(&id("oa_view"))
.unwrap()
.nodes
.get_mut(&id("node_child"))
.unwrap()
.position = position;
mismatch("moved", &before, &bad, "child did not move exactly once");
}
}
#[test]
fn internal_pins_left_behind_and_external_pins_moved_are_rejected() {
let mut before = fixture().unwrap();
let original_pins = edge(&mut before, "internal").waypoints.clone();
let correct = moved(&before, point(40., 60.));
let mut bad = correct.clone();
edge(&mut bad, "internal").waypoints = original_pins;
mismatch("moved", &before, &bad, "internal pins left behind");
let mut bad = correct;
for pin in &mut edge(&mut bad, "external").waypoints {
pin.x += 40.;
pin.y += 60.;
}
mismatch("moved", &before, &bad, "one-ended pins moved");
}
#[test]
fn manual_attachments_and_label_data_must_remain_exact() {
let before = fixture().unwrap();
let correct = moved(&before, point(40., 60.));
let mutations: [fn(&mut EdgeRoute); 4] = [
|r| r.source_attachment.as_mut().unwrap().offset = 0.65,
|r| r.target_attachment.as_mut().unwrap().side = PortAnchorSide::Top,
|r| r.label.as_mut().unwrap().offset.x += 40.,
|r| r.label.as_mut().unwrap().fraction = 0.75,
];
for mutate in mutations {
let mut bad = correct.clone();
mutate(edge(&mut bad, "internal"));
mismatch("moved", &before, &bad, "relative route metadata changed");
}
}
#[test]
fn semantic_other_view_fixed_node_and_trace_drift_are_rejected() {
let before = fixture().unwrap();
let correct = moved(&before, point(40., 60.));
let mutations: [fn(&mut Project); 5] = [
|p| {
p.elements
.get_mut(&id("activity_a"))
.unwrap()
.description
.push_str(" drift")
},
|p| {
p.diagrams
.get_mut(&id("oa_other"))
.unwrap()
.nodes
.get_mut(&id("other_a"))
.unwrap()
.position
.x += 40.
},
|p| {
p.diagrams
.get_mut(&id("sa_view"))
.unwrap()
.nodes
.get_mut(&id("sa_node"))
.unwrap()
.position
.y += 60.
},
|p| {
p.diagrams
.get_mut(&id("oa_view"))
.unwrap()
.nodes
.get_mut(&id("node_c"))
.unwrap()
.position
.x += 40.
},
|p| {
p.traces
.get_mut(&id("trace"))
.unwrap()
.description
.push_str(" drift")
},
];
for mutate in mutations {
let mut bad = correct.clone();
mutate(&mut bad);
mismatch(
"moved",
&before,
&bad,
"record outside permitted movement changed",
);
}
}
#[test]
fn zero_small_excessive_and_offgrid_moves_cannot_satisfy_move_evidence() {
let before = fixture().unwrap();
for (delta, reason) in [
(point(0., 0.), "length >=10"),
(point(3., 0.), "length >=10"),
(point(510., 0.), "each axis <=500"),
(point(20.5, 0.), "10-unit grid"),
] {
let actual = moved(&before, delta);
let error = through_saved_json("moved", &before, &actual)
.unwrap_err()
.to_string();
assert!(error.contains(reason), "{delta:?}: {error}");
}
}
#[test]
fn hide_must_preserve_child_routes_and_semantic_objects() {
let before = fixture().unwrap();
let correct = hidden(&before);
let mut missing_child = correct.clone();
missing_child
.diagrams
.get_mut(&id("oa_view"))
.unwrap()
.nodes
.remove(&id("node_child"));
mismatch(
"hide",
&before,
&missing_child,
"Hide removed unselected child placement",
);
let mut missing_routes = correct;
missing_routes
.diagrams
.get_mut(&id("oa_view"))
.unwrap()
.routes
.clear();
mismatch(
"hide",
&before,
&missing_routes,
"Hide removed persistent route records",
);
let mut semantic_delete = Session::new(before.clone());
semantic_delete
.execute(Command::Batch(vec![
Command::Delete(id("activity_a")),
Command::Delete(id("activity_b")),
]))
.unwrap();
mismatch(
"hide",
&before,
semantic_delete.project(),
"Hide became semantic deletion",
);
}
#[test]
fn unchanged_rejects_valid_semantic_drift_without_geometry_changes() {
let before = fixture().unwrap();
let mut actual = before.clone();
actual
.elements
.get_mut(&id("activity_b"))
.unwrap()
.name
.push_str(" edited");
mismatch(
"unchanged",
&before,
&actual,
"geometry equality is not complete Project equality",
);
}
}