Files
ARCADIA-Studio/harness/lifecycle-native-contract.rs
T

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]"
),
}
}