225 lines
13 KiB
Rust
225 lines
13 KiB
Rust
//! Core-only native deletion oracle. No desktop code or actual-derived expectation.
|
|
//! removed BASE_ENTRY ACTUAL_ENTRY TARGET_UUID | same BASE_ENTRY ACTUAL_ENTRY | self-check | generate NEW_DIR
|
|
use oa_core::{source_project::*, *};
|
|
use std::{collections::BTreeMap, fs, path::Path};
|
|
|
|
fn compile_exact(raw: &SourceBundle) -> CompiledSourceProject {
|
|
let result = compile_bundle(raw, ProjectLimits::default()).expect("source admission");
|
|
assert_eq!(&result.bundle, raw, "source migration/normalization is not permitted");
|
|
validate_candidate(&result.decoded.project).expect("model admission");
|
|
result
|
|
}
|
|
fn load(entry: &Path) -> CompiledSourceProject {
|
|
try_load(entry).unwrap_or_else(|error| panic!("{}: {error}", entry.display()))
|
|
}
|
|
fn try_load(entry: &Path) -> Result<CompiledSourceProject, String> {
|
|
let disk = source_files::load_project(entry, ProjectLimits::default())
|
|
.map_err(|error| format!("load source project: {error}"))?;
|
|
let errors: Vec<_> = disk.diagnostics.iter()
|
|
.filter(|issue| issue.diagnostic.severity == validation::Severity::Error).collect();
|
|
if !errors.is_empty() {
|
|
return Err(format!("source/discovery errors: {errors:?}"));
|
|
}
|
|
for issue in &disk.diagnostics {
|
|
eprintln!("Permitted non-error diagnostic in {}: {:?}", entry.display(), issue);
|
|
}
|
|
let raw = disk.reachable_bundle();
|
|
if disk.bundle != raw {
|
|
return Err("detached/invalid discovered buffers are not admitted".into());
|
|
}
|
|
let compiled = compile_bundle(&raw, ProjectLimits::default())
|
|
.map_err(|issues| format!("source admission: {issues:?}"))?;
|
|
if compiled.bundle != raw {
|
|
return Err("source migration/normalization is not permitted".into());
|
|
}
|
|
validate_candidate(&compiled.decoded.project)
|
|
.map_err(|error| format!("model admission: {error}"))?;
|
|
Ok(compiled)
|
|
}
|
|
fn removed(base: &CompiledSourceProject, target: Id) -> CompiledSourceProject {
|
|
assert!(base.decoded.project.contains(target), "target absent from baseline");
|
|
let candidate = prepare_command(&base.decoded.project, &Command::Delete(target))
|
|
.expect("baseline Delete must be admissible");
|
|
assert!(!candidate.project.contains(target), "Delete must remove requested identity");
|
|
assert_ne!(candidate.project, base.decoded.project, "Delete must change model");
|
|
let result = update_bundle_with_identity_map(base, &candidate.project, &candidate.identity_map,
|
|
ProjectLimits::default()).expect("ordinary source patch must be admissible");
|
|
assert_eq!(result.decoded.project, candidate.project, "source must exactly represent candidate");
|
|
assert_eq!(result.bundle.files.keys().collect::<Vec<_>>(), base.bundle.files.keys().collect::<Vec<_>>(),
|
|
"Delete must retain source files, including newly empty files");
|
|
result
|
|
}
|
|
fn same(expected: &CompiledSourceProject, actual: &CompiledSourceProject) -> bool {
|
|
expected.decoded.project == actual.decoded.project && expected.bundle == actual.bundle
|
|
}
|
|
fn check(expected: &CompiledSourceProject, actual: &CompiledSourceProject) {
|
|
assert_eq!(actual.decoded.project, expected.decoded.project, "complete Project differs");
|
|
assert_eq!(actual.bundle, expected.bundle, "complete SourceBundle differs (paths or exact UTF-8 bytes)");
|
|
}
|
|
fn id(n: u128) -> Id { Id::from_u128(0xde1e7e00000040008000000000000000 + n) }
|
|
fn fixture() -> CompiledSourceProject {
|
|
let mut p = Project::empty();
|
|
p.id = id(1); p.alias = "delete_oracle".into(); p.name = "Delete oracle".into();
|
|
for (n, kind, name, parent) in [
|
|
(10, Kind::Activity, "Target work", None),
|
|
(11, Kind::Activity, "Untouched work", None),
|
|
(20, Kind::Scenario, "Reference scenario", None),
|
|
(21, Kind::Lifeline, "Target reference", Some(id(20))),
|
|
] {
|
|
let mut e = p.make_element(kind, Layer::Oa, name, parent).unwrap();
|
|
e.id = id(n); e.alias = format!("delete_{n}"); p.elements.insert(e.id, e);
|
|
}
|
|
*p.elements.get_mut(&id(21)).unwrap().shared_mut().unwrap() =
|
|
SharedBody::Lifeline { represented: Some(id(10)) };
|
|
for (view, placement) in [(30, 31), (40, 41)] {
|
|
let node = NodePlacement { id: id(placement), alias: format!("node_{placement}"),
|
|
element: id(10), position: Position { x: 80., y: 80. },
|
|
size: Size { width: 180., height: 90. }, collapsed: false, port_anchors: vec![] };
|
|
p.diagrams.insert(id(view), Diagram { id: id(view), alias: format!("view_{view}"),
|
|
name: format!("Work view {view}"), description: String::new(), layer: Layer::Oa,
|
|
projection: ProjectionKind::Allocation, view_options: Default::default(),
|
|
nodes: BTreeMap::from([(node.id, node)]), routes: vec![] });
|
|
}
|
|
for (n, kind, layer, name, parent) in [
|
|
(12, Kind::Entity, Layer::Oa, "OA Dispatch team", None),
|
|
(50, Kind::Function, Layer::Sa, "SA Initialize controller", None),
|
|
(51, Kind::StateMachine, Layer::Sa, "SA Controller modes", None),
|
|
(52, Kind::Region, Layer::Sa, "SA Main region", Some(id(51))),
|
|
(53, Kind::State, Layer::Sa, "SA Ready", Some(id(52))),
|
|
] {
|
|
let mut e = p.make_element(kind, layer, name, parent).unwrap();
|
|
e.id = id(n); e.alias = format!("delete_{n}"); p.elements.insert(e.id, e);
|
|
}
|
|
for (n, kind, name, source, target) in [
|
|
(13, RelationKind::Allocation, "OA performs target work", 12, 10),
|
|
(54, RelationKind::StateAction, "SA Entry initialize", 53, 50),
|
|
] {
|
|
let generated = p.relate(kind, name, id(source), id(target));
|
|
let mut relation = p.relations.remove(&generated).unwrap();
|
|
relation.id = id(n); relation.alias = format!("delete_{n}");
|
|
if let RelationBody::StateAction { phase, .. } = &mut relation.body { *phase = StateActionKind::Entry; }
|
|
p.relations.insert(relation.id, relation);
|
|
}
|
|
for (view, placement) in [(30, 32), (40, 42)] {
|
|
p.diagrams.get_mut(&id(view)).unwrap().nodes.insert(id(placement), NodePlacement {
|
|
id: id(placement), alias: format!("node_{placement}"), element: id(12),
|
|
position: Position { x: 40., y: 40. }, size: Size { width: 380., height: 230. },
|
|
collapsed: false, port_anchors: vec![],
|
|
});
|
|
}
|
|
let nodes = [(61, 51, 40., 40., 620., 460.), (62, 52, 60., 95., 580., 380.),
|
|
(63, 53, 100., 155., 320., 190.)].into_iter().map(|(n, element, x, y, width, height)| {
|
|
(id(n), NodePlacement { id: id(n), alias: format!("node_{n}"), element: id(element),
|
|
position: Position { x, y }, size: Size { width, height }, collapsed: false, port_anchors: vec![] })
|
|
}).collect();
|
|
p.diagrams.insert(id(60), Diagram { id: id(60), alias: "sa_modes_view".into(),
|
|
name: "SA Controller modes view".into(), description: String::new(), layer: Layer::Sa,
|
|
projection: ProjectionKind::StateMachine { machine: id(51) }, view_options: Default::default(),
|
|
nodes, routes: vec![] });
|
|
validate_candidate(&p).unwrap();
|
|
let entry = language::format(&p).replacen("{\n", "{\n include \"notes.arcadia\";\n", 1);
|
|
let raw = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
|
|
("project.arcadia".into(), entry),
|
|
("notes.arcadia".into(), "// Preserve deletion evidence 雨.\r\n// Exact bytes matter.\r\n".into()),
|
|
]) };
|
|
let compiled = compile_exact(&raw);
|
|
assert_eq!(compiled.decoded.project, p);
|
|
compiled
|
|
}
|
|
fn self_check() {
|
|
let base = fixture();
|
|
let expected = removed(&base, id(10));
|
|
check(&expected, &compile_exact(&expected.bundle));
|
|
assert!(!same(&expected, &base), "must reject no-op");
|
|
let wrong = removed(&base, id(11));
|
|
assert!(!same(&expected, &wrong), "must reject wrong deletion");
|
|
let placement = removed(&base, id(31));
|
|
assert!(placement.decoded.project.contains(id(10)));
|
|
assert!(placement.decoded.project.contains(id(41)));
|
|
assert!(!same(&expected, &placement), "placement removal is not semantic deletion");
|
|
assert!(!expected.decoded.project.contains(id(31)));
|
|
assert!(!expected.decoded.project.contains(id(41)));
|
|
assert!(!expected.decoded.project.contains(id(13)), "semantic deletion removes allocation");
|
|
assert!(placement.decoded.project.contains(id(13)), "placement removal retains allocation");
|
|
let entry_row = removed(&base, id(54));
|
|
assert!(entry_row.decoded.project.contains(id(50)), "Entry row removal retains function");
|
|
assert!(entry_row.decoded.project.contains(id(53)), "Entry row removal retains state");
|
|
assert!(!same(&entry_row, &removed(&base, id(50))), "row removal differs from function deletion");
|
|
assert!(matches!(expected.decoded.project.elements[&id(21)].shared(),
|
|
Some(SharedBody::Lifeline { represented: None })), "cross-reference must clear");
|
|
let extra = prepare_command(&expected.decoded.project,
|
|
&Command::Rename(id(11), "Unrelated damage".into())).unwrap();
|
|
let unrelated = update_bundle(&expected, &extra.project, ProjectLimits::default()).unwrap();
|
|
assert!(!same(&expected, &unrelated), "must reject unrelated semantic change");
|
|
for text in ["// Preserve deletion evidence 雨.\n// Exact bytes matter.\n", "// Damaged\r\n"] {
|
|
let mut raw = expected.bundle.clone();
|
|
raw.files.insert("notes.arcadia".into(), text.into());
|
|
let tampered = compile_exact(&raw);
|
|
assert_eq!(tampered.decoded.project, expected.decoded.project);
|
|
assert!(!same(&expected, &tampered), "must reject source-only changes");
|
|
}
|
|
let mut missing = expected.bundle.clone(); missing.files.remove("notes.arcadia");
|
|
assert!(compile_bundle(&missing, ProjectLimits::default()).is_err());
|
|
check(&base, &compile_exact(&base.bundle));
|
|
assert!(!same(&base, &expected), "restoration must reject deleted snapshot");
|
|
assert_eq!(base.bundle.files["notes.arcadia"], expected.bundle.files["notes.arcadia"]);
|
|
disk_self_check(&base);
|
|
println!("PASS self-check: exact candidate/source, wrong target, no-op, unrelated changes, placement/model distinction, crossrefs, source-byte falsifiers, disk warning admission and missing-include/syntax rejection");
|
|
}
|
|
fn disk_self_check(base: &CompiledSourceProject) {
|
|
let nonce = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos();
|
|
let dir = std::env::temp_dir().join(format!("arcadia-delete-oracle-{}-{nonce}", std::process::id()));
|
|
fs::create_dir(&dir).expect("create unique owned disk self-check directory");
|
|
for (path, text) in &base.bundle.files { fs::write(dir.join(path), text).unwrap(); }
|
|
let entry = dir.join(&base.bundle.entry);
|
|
let disk = source_files::load_project(&entry, ProjectLimits::default()).unwrap();
|
|
assert!(disk.diagnostics.iter().any(|issue| issue.diagnostic.severity == validation::Severity::Warning),
|
|
"fixture must exercise legitimate warning admission");
|
|
check(base, &try_load(&entry).expect("valid fixture with completeness warnings must load"));
|
|
fs::remove_file(dir.join("notes.arcadia")).unwrap();
|
|
assert!(try_load(&entry).is_err(), "missing include must fail disk admission");
|
|
fs::write(dir.join("notes.arcadia"), &base.bundle.files["notes.arcadia"]).unwrap();
|
|
fs::write(&entry, "this is not valid Arcadia source").unwrap();
|
|
assert!(try_load(&entry).is_err(), "malformed source must fail disk admission");
|
|
// Remove only exact files created above; no recursive deletion or borrowed fixture mutation.
|
|
for path in base.bundle.files.keys() { fs::remove_file(dir.join(path)).unwrap(); }
|
|
fs::remove_dir(&dir).unwrap();
|
|
}
|
|
fn generate(dir: &Path) {
|
|
let baseline = fixture();
|
|
fs::create_dir(dir).expect("output directory must not already exist; parent must exist");
|
|
for (path, text) in &baseline.bundle.files {
|
|
fs::write(dir.join(path), text).expect("write new fixture");
|
|
}
|
|
check(&baseline, &load(&dir.join(&baseline.bundle.entry)));
|
|
println!("PASS generated exact fixture {}", dir.display());
|
|
for (n, label) in [(10, "OA target activity"), (12, "OA entity"), (13, "OA allocation"),
|
|
(30, "first architecture view"), (31, "first activity placement"), (32, "first entity placement"),
|
|
(40, "second architecture view"), (41, "second activity placement"), (42, "second entity placement"),
|
|
(50, "SA Entry function"), (51, "SA machine"), (52, "SA region"), (53, "SA Ready state"),
|
|
(54, "SA Entry row relation"), (60, "SA state view"), (63, "SA Ready placement")] {
|
|
println!("{label}: {}", id(n));
|
|
}
|
|
}
|
|
fn main() {
|
|
let args: Vec<_> = std::env::args().collect();
|
|
match args.get(1).map(String::as_str) {
|
|
Some("self-check") if args.len() == 2 => self_check(),
|
|
Some("generate") if args.len() == 3 => generate(Path::new(&args[2])),
|
|
Some("same") if args.len() == 4 => {
|
|
let base = load(Path::new(&args[2])); let actual = load(Path::new(&args[3]));
|
|
check(&base, &actual); println!("PASS same: complete Project and exact SourceBundle restored");
|
|
}
|
|
Some("removed") if args.len() == 5 => {
|
|
let base = load(Path::new(&args[2]));
|
|
let target: Id = args[4].parse().expect("TARGET_UUID must be a UUID");
|
|
// Expected is fully fixed before actual is loaded.
|
|
let expected = removed(&base, target);
|
|
let actual = load(Path::new(&args[3]));
|
|
check(&expected, &actual);
|
|
println!("PASS removed {target}: exact baseline-derived Delete candidate and normal source patch");
|
|
}
|
|
_ => panic!("usage: removed BASE_ENTRY ACTUAL_ENTRY TARGET_UUID | same BASE_ENTRY ACTUAL_ENTRY | self-check | generate NEW_DIR"),
|
|
}
|
|
}
|