571 lines
19 KiB
Rust
571 lines
19 KiB
Rust
//! Independent B4 native source fixture/oracle. No desktop renderer is linked.
|
|
//! generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]
|
|
//! self-check | inventory; modes: baseline, oa-rename, sa-rename. All fixture identities are fixed.
|
|
use oa_core::{source_project::*, *};
|
|
use std::{collections::BTreeMap, fs, path::Path};
|
|
|
|
type Disk = BTreeMap<String, Vec<u8>>;
|
|
fn id(n: u128) -> Id {
|
|
Id::from_u128(0x5ceab400000040008000000000000000 + n)
|
|
}
|
|
#[derive(Clone, Copy)]
|
|
struct Side {
|
|
layer: Layer,
|
|
word: &'static str,
|
|
base: u128,
|
|
}
|
|
const SIDES: [Side; 2] = [
|
|
Side {
|
|
layer: Layer::Oa,
|
|
word: "OA",
|
|
base: 1000,
|
|
},
|
|
Side {
|
|
layer: Layer::Sa,
|
|
word: "SA",
|
|
base: 2000,
|
|
},
|
|
];
|
|
impl Side {
|
|
fn id(self, n: u128) -> Id {
|
|
id(self.base + n)
|
|
}
|
|
fn name(self, text: &str) -> String {
|
|
format!("{} {text}", self.word)
|
|
}
|
|
}
|
|
fn element(p: &mut Project, side: Side, n: u128, kind: Kind, name: &str, parent: Option<u128>) {
|
|
let mut e = p
|
|
.make_element(
|
|
kind,
|
|
side.layer,
|
|
&side.name(name),
|
|
parent.map(|n| side.id(n)),
|
|
)
|
|
.unwrap();
|
|
e.id = side.id(n);
|
|
e.alias = format!("life_{}_{}", side.word.to_lowercase(), n);
|
|
p.elements.insert(e.id, e);
|
|
}
|
|
fn scenario(p: &mut Project, side: Side, n: u128, name: &str) {
|
|
element(p, side, n, Kind::Scenario, name, None);
|
|
let Some(SharedBody::Scenario { viewpoint, .. }) =
|
|
p.elements.get_mut(&side.id(n)).unwrap().shared_mut()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
*viewpoint = if side.layer == Layer::Oa {
|
|
ScenarioViewpoint::OperationalActivity
|
|
} else {
|
|
ScenarioViewpoint::Functional
|
|
};
|
|
}
|
|
fn line(p: &mut Project, side: Side, n: u128, owner: u128, work: Option<u128>, name: &str) {
|
|
element(p, side, n, Kind::Lifeline, name, Some(owner));
|
|
*p.elements
|
|
.get_mut(&side.id(n))
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Lifeline {
|
|
represented: work.map(|n| side.id(n)),
|
|
};
|
|
}
|
|
fn occurrence(
|
|
p: &mut Project,
|
|
side: Side,
|
|
n: u128,
|
|
parent: u128,
|
|
name: &str,
|
|
body: OccurrenceBody,
|
|
) {
|
|
element(p, side, n, Kind::Occurrence, name, Some(parent));
|
|
*p.elements
|
|
.get_mut(&side.id(n))
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
}
|
|
fn steps(p: &mut Project, side: Side, owner: u128, ordered: &[u128]) {
|
|
*p.elements
|
|
.get_mut(&side.id(owner))
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap() = ordered.iter().map(|n| side.id(*n)).collect();
|
|
}
|
|
fn message(
|
|
side: Side,
|
|
sender: u128,
|
|
receiver: u128,
|
|
kind: MessageKind,
|
|
resolved: bool,
|
|
) -> OccurrenceBody {
|
|
OccurrenceBody::Exchange {
|
|
exchange: resolved.then(|| side.id(12)),
|
|
sender: Some(side.id(sender)),
|
|
receiver: Some(side.id(receiver)),
|
|
message_kind: kind,
|
|
operation: None,
|
|
reply_to: None,
|
|
values: vec![],
|
|
}
|
|
}
|
|
fn fixture() -> Project {
|
|
let mut p = Project::empty();
|
|
p.id = id(1);
|
|
p.alias = "lifecycle_native".into();
|
|
p.name = "Lifecycle native contract".into();
|
|
p.description =
|
|
"Fixed semantic inputs; lifecycle geometry is derived and never authored.".into();
|
|
for side in SIDES {
|
|
let work = if side.layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
element(&mut p, side, 10, work, "Controller work", None);
|
|
element(&mut p, side, 11, work, "Receiver work", None);
|
|
if side.layer == Layer::Oa {
|
|
let relation = Relation {
|
|
id: side.id(12),
|
|
alias: "life_oa_service".into(),
|
|
name: "OA Lifecycle service".into(),
|
|
description: String::new(),
|
|
body: RelationBody::new(RelationKind::Interaction, side.id(10), side.id(11)),
|
|
};
|
|
p.relations.insert(relation.id, relation);
|
|
} else {
|
|
// The port planner derives deterministic UUIDv5 endpoints from this fixed exchange ID.
|
|
let commands = ports::plan_functional_exchange(
|
|
&p,
|
|
&ports::FunctionalExchangeRequest {
|
|
id: side.id(12),
|
|
alias: "life_sa_service".into(),
|
|
name: "SA Lifecycle service".into(),
|
|
description: String::new(),
|
|
source: side.id(10),
|
|
target: side.id(11),
|
|
source_port: ports::PortChoice::Create,
|
|
target_port: ports::PortChoice::Create,
|
|
items: vec![],
|
|
},
|
|
)
|
|
.expect("typed fixed-ID functional exchange must be admitted");
|
|
p = execute_batch(&p, &commands).unwrap();
|
|
}
|
|
for (n, name) in [
|
|
(100, "01 Root create action delete"),
|
|
(200, "02 Preexisting empty"),
|
|
(300, "03 Create only"),
|
|
(400, "04 Conditional delete"),
|
|
(500, "05 Unassigned receiver"),
|
|
(600, "06 Self delete"),
|
|
] {
|
|
scenario(&mut p, side, n, name);
|
|
line(&mut p, side, n + 1, n, Some(10), "Controller lane");
|
|
if n != 600 {
|
|
line(
|
|
&mut p,
|
|
side,
|
|
n + 2,
|
|
n,
|
|
(n != 500).then_some(11),
|
|
if n == 500 {
|
|
"Unassigned receiver lane"
|
|
} else {
|
|
"Receiver lane"
|
|
},
|
|
);
|
|
}
|
|
}
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
110,
|
|
100,
|
|
"Create receiver",
|
|
message(side, 101, 102, MessageKind::Create, true),
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
111,
|
|
100,
|
|
"Receiver action",
|
|
OccurrenceBody::Action {
|
|
action: Some(side.id(11)),
|
|
lifeline: Some(side.id(102)),
|
|
},
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
112,
|
|
100,
|
|
"Delete receiver",
|
|
message(side, 101, 102, MessageKind::Delete, true),
|
|
);
|
|
steps(&mut p, side, 100, &[110, 111, 112]);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
310,
|
|
300,
|
|
"Create only receiver",
|
|
message(side, 301, 302, MessageKind::Create, true),
|
|
);
|
|
steps(&mut p, side, 300, &[310]);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
410,
|
|
400,
|
|
"Open call",
|
|
message(side, 401, 402, MessageKind::Call, true),
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
411,
|
|
400,
|
|
"ALT delete or keep",
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![side.id(412), side.id(413)],
|
|
},
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
412,
|
|
411,
|
|
"Left delete operand",
|
|
OccurrenceBody::Branch {
|
|
guard: "stop_requested".into(),
|
|
occurrences: vec![side.id(414)],
|
|
},
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
413,
|
|
411,
|
|
"Right empty operand",
|
|
OccurrenceBody::Branch {
|
|
guard: "else".into(),
|
|
occurrences: vec![],
|
|
},
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
414,
|
|
412,
|
|
"Conditional delete receiver",
|
|
message(side, 401, 402, MessageKind::Delete, true),
|
|
);
|
|
steps(&mut p, side, 400, &[410, 411]);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
510,
|
|
500,
|
|
"Create unassigned receiver",
|
|
message(side, 501, 502, MessageKind::Create, false),
|
|
);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
511,
|
|
500,
|
|
"Delete unassigned receiver",
|
|
message(side, 501, 502, MessageKind::Delete, false),
|
|
);
|
|
steps(&mut p, side, 500, &[510, 511]);
|
|
occurrence(
|
|
&mut p,
|
|
side,
|
|
610,
|
|
600,
|
|
"Self delete controller",
|
|
message(side, 601, 601, MessageKind::Delete, false),
|
|
);
|
|
steps(&mut p, side, 600, &[610]);
|
|
}
|
|
execute_batch(&p, &[]).expect("complete source fixture must satisfy ordinary core admission; never bypass self/incomplete-lane rejection")
|
|
}
|
|
fn baseline() -> CompiledSourceProject {
|
|
let p = fixture();
|
|
let entry = language::format(&p).replacen("{\n", "{\n include \"rationale.arcadia\";\n", 1);
|
|
let raw = SourceBundle { entry: "project.arcadia".into(), files: BTreeMap::from([
|
|
("project.arcadia".into(), entry),
|
|
("rationale.arcadia".into(), "// Preserve lifecycle note: 雨 · receiver.\r\n// Geometry never changes these bytes.\r\n".into()),
|
|
]) };
|
|
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.decoded.project, p);
|
|
assert_eq!(
|
|
compiled.bundle, raw,
|
|
"no identity insertion, normalization or migration"
|
|
);
|
|
compiled
|
|
}
|
|
fn expected(mode: &str) -> CompiledSourceProject {
|
|
let base = baseline();
|
|
let side = match mode {
|
|
"baseline" => return base,
|
|
"oa-rename" => SIDES[0],
|
|
"sa-rename" => SIDES[1],
|
|
_ => panic!("mode: baseline|oa-rename|sa-rename"),
|
|
};
|
|
let next = execute_batch(
|
|
&base.decoded.project,
|
|
&[Command::Rename(
|
|
side.id(112),
|
|
side.name("Delete receiver · reviewed"),
|
|
)],
|
|
)
|
|
.unwrap();
|
|
update_bundle(&base, &next, ProjectLimits::default()).unwrap()
|
|
}
|
|
fn disk(root: &Path, here: &Path, out: &mut Disk) {
|
|
for entry in fs::read_dir(here).unwrap() {
|
|
let entry = entry.unwrap();
|
|
let kind = entry.file_type().unwrap();
|
|
assert!(!kind.is_symlink(), "checkpoint cannot contain symlinks");
|
|
if kind.is_dir() {
|
|
disk(root, &entry.path(), out);
|
|
} else {
|
|
assert!(kind.is_file(), "checkpoint files must be ordinary files");
|
|
let path = entry.path();
|
|
let name = path
|
|
.strip_prefix(root)
|
|
.unwrap()
|
|
.to_str()
|
|
.unwrap()
|
|
.replace('\\', "/");
|
|
out.insert(name, fs::read(path).unwrap());
|
|
}
|
|
}
|
|
}
|
|
struct Snapshot {
|
|
compiled: CompiledSourceProject,
|
|
disk: Disk,
|
|
}
|
|
fn load(entry: &Path) -> Snapshot {
|
|
assert_eq!(entry.file_name().unwrap(), "project.arcadia");
|
|
let raw = source_files::load_project(entry, ProjectLimits::default())
|
|
.unwrap()
|
|
.reachable_bundle();
|
|
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.bundle, raw, "no implicit source normalization");
|
|
execute_batch(&compiled.decoded.project, &[]).unwrap();
|
|
let mut files = Disk::new();
|
|
let root = entry.parent().unwrap();
|
|
disk(root, root, &mut files);
|
|
Snapshot {
|
|
compiled,
|
|
disk: files,
|
|
}
|
|
}
|
|
fn source_disk(bundle: &SourceBundle) -> Disk {
|
|
bundle
|
|
.files
|
|
.iter()
|
|
.map(|(name, text)| (name.clone(), text.as_bytes().to_vec()))
|
|
.collect()
|
|
}
|
|
fn expected_disk(bundle: &SourceBundle, origin: Option<&Disk>) -> Disk {
|
|
let mut files = origin.cloned().unwrap_or_default();
|
|
files.extend(source_disk(bundle));
|
|
files
|
|
}
|
|
fn equal(expected: &CompiledSourceProject, actual: &Snapshot, origin: Option<&Disk>) {
|
|
assert_eq!(
|
|
actual.compiled.decoded.project, expected.decoded.project,
|
|
"whole Project: every identity, field, order, reference and geometry"
|
|
);
|
|
assert_eq!(
|
|
actual.compiled.bundle, expected.bundle,
|
|
"whole reachable SourceBundle including retained note bytes"
|
|
);
|
|
assert_eq!(
|
|
actual.disk,
|
|
expected_disk(&expected.bundle, origin),
|
|
"whole physical file map: only exact pre-Undo retained files are permitted"
|
|
);
|
|
}
|
|
fn self_check() {
|
|
let base = baseline();
|
|
assert_eq!(
|
|
base.decoded.project,
|
|
fixture(),
|
|
"fixture deterministic, including typed ports"
|
|
);
|
|
assert_eq!(base.bundle, baseline().bundle);
|
|
for mode in ["baseline", "oa-rename", "sa-rename"] {
|
|
let prediction = expected(mode);
|
|
let actual = Snapshot {
|
|
disk: source_disk(&prediction.bundle),
|
|
compiled: expected(mode),
|
|
};
|
|
equal(&prediction, &actual, None);
|
|
if mode != "baseline" {
|
|
assert_ne!(prediction.decoded.project, base.decoded.project);
|
|
assert_ne!(prediction.bundle, base.bundle);
|
|
let side = if mode == "oa-rename" {
|
|
SIDES[0]
|
|
} else {
|
|
SIDES[1]
|
|
};
|
|
let mut independently_named = base.decoded.project.clone();
|
|
independently_named
|
|
.elements
|
|
.get_mut(&side.id(112))
|
|
.unwrap()
|
|
.name = side.name("Delete receiver · reviewed");
|
|
assert_eq!(
|
|
prediction.decoded.project, independently_named,
|
|
"rename changes precisely one name; alias and identity stay fixed"
|
|
);
|
|
}
|
|
let mut changed_project = prediction.decoded.project.clone();
|
|
changed_project.description.push_str("unrequested");
|
|
assert_ne!(changed_project, prediction.decoded.project);
|
|
let mut changed_source = prediction.bundle.clone();
|
|
changed_source
|
|
.files
|
|
.get_mut("rationale.arcadia")
|
|
.unwrap()
|
|
.push_str("// unrequested\n");
|
|
assert_ne!(changed_source, prediction.bundle);
|
|
}
|
|
let plain = source_disk(&base.bundle);
|
|
let mut origin = plain.clone();
|
|
origin.insert(
|
|
"detached.arcadia".into(),
|
|
b"// exact pre-Undo bytes\r\n".to_vec(),
|
|
);
|
|
let retained = expected_disk(&base.bundle, Some(&origin));
|
|
assert_eq!(retained, origin);
|
|
assert_ne!(
|
|
retained,
|
|
expected_disk(&base.bundle, None),
|
|
"origin is mandatory to authorize retained files"
|
|
);
|
|
let mut missing = retained.clone();
|
|
missing.remove("detached.arcadia");
|
|
assert_ne!(missing, retained);
|
|
let mut changed = retained.clone();
|
|
changed.get_mut("detached.arcadia").unwrap().push(b'!');
|
|
assert_ne!(changed, retained);
|
|
let mut unknown = retained.clone();
|
|
unknown.insert("unknown.arcadia".into(), vec![]);
|
|
assert_ne!(unknown, retained);
|
|
let mut wrong_reachable = origin.clone();
|
|
wrong_reachable.insert("rationale.arcadia".into(), b"wrong prior contents".to_vec());
|
|
assert_eq!(
|
|
expected_disk(&base.bundle, Some(&wrong_reachable)),
|
|
retained,
|
|
"expected reachable bytes override the origin"
|
|
);
|
|
println!(
|
|
"PASS self-check: fixed OA/SA fixture, exact name-only predictions, explicit retained-file origin policy"
|
|
);
|
|
}
|
|
fn main() {
|
|
assert_eq!(SCHEMA_VERSION, 7);
|
|
let args: Vec<_> = std::env::args().collect();
|
|
match args.get(1).map(String::as_str) {
|
|
Some("self-check") => {
|
|
assert_eq!(args.len(), 2);
|
|
self_check();
|
|
}
|
|
Some("inventory") => {
|
|
assert_eq!(args.len(), 2);
|
|
let base = baseline();
|
|
for e in
|
|
base.decoded.project.elements.values().filter(|e| {
|
|
matches!(e.kind(), Kind::Scenario | Kind::Lifeline | Kind::Occurrence)
|
|
})
|
|
{
|
|
println!(
|
|
"{} | {:?} | {} | parent={:?}",
|
|
e.id,
|
|
e.kind(),
|
|
e.name,
|
|
e.parent
|
|
);
|
|
}
|
|
for issue in validation::validate(&base.decoded.project) {
|
|
println!("DIAGNOSTIC {issue:?}");
|
|
}
|
|
}
|
|
Some("generate") => {
|
|
assert_eq!(args.len(), 4, "generate MODE FRESH_DIR");
|
|
let prediction = expected(&args[2]);
|
|
let dir = Path::new(&args[3]);
|
|
assert!(!dir.exists(), "preserve existing fixture/evidence");
|
|
fs::create_dir_all(dir).unwrap();
|
|
for (name, text) in &prediction.bundle.files {
|
|
let path = dir.join(name);
|
|
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
fs::write(path, text.as_bytes()).unwrap();
|
|
}
|
|
equal(&prediction, &load(&dir.join("project.arcadia")), None);
|
|
println!(
|
|
"PASS generated {}: {} fixed elements, {} relations, {} exact sources",
|
|
args[2],
|
|
prediction.decoded.project.elements.len(),
|
|
prediction.decoded.project.relations.len(),
|
|
prediction.bundle.files.len()
|
|
);
|
|
}
|
|
Some("check") => {
|
|
assert!(
|
|
(4..=5).contains(&args.len()),
|
|
"check MODE ENTRY [PRE_UNDO_ENTRY]"
|
|
);
|
|
let actual = load(Path::new(&args[3]));
|
|
let origin = args.get(4).map(|path| load(Path::new(path)));
|
|
equal(
|
|
&expected(&args[2]),
|
|
&actual,
|
|
origin.as_ref().map(|s| &s.disk),
|
|
);
|
|
println!(
|
|
"PASS {}: complete Project/reachable sources/physical map",
|
|
args[2]
|
|
);
|
|
}
|
|
Some("same") => {
|
|
assert!(
|
|
(4..=5).contains(&args.len()),
|
|
"same VERIFIED_EXPECTED_ENTRY ACTUAL_ENTRY [PRE_UNDO_ENTRY]"
|
|
);
|
|
let wanted = load(Path::new(&args[2]));
|
|
let actual = load(Path::new(&args[3]));
|
|
let origin = args.get(4).map(|path| load(Path::new(path)));
|
|
assert_eq!(
|
|
actual.compiled.decoded.project,
|
|
wanted.compiled.decoded.project
|
|
);
|
|
assert_eq!(actual.compiled.bundle, wanted.compiled.bundle);
|
|
let files = origin.as_ref().map_or_else(
|
|
|| wanted.disk.clone(),
|
|
|s| expected_disk(&wanted.compiled.bundle, Some(&s.disk)),
|
|
);
|
|
assert_eq!(
|
|
actual.disk, files,
|
|
"exact prior checkpoint or explicit retained-file origin"
|
|
);
|
|
println!("PASS same: all identities, fields, reachable source and physical bytes");
|
|
}
|
|
_ => panic!(
|
|
"self-check | inventory | generate MODE DIR | check MODE ENTRY [PRE_UNDO_ENTRY] | same EXPECTED ACTUAL [PRE_UNDO_ENTRY]"
|
|
),
|
|
}
|
|
}
|