673 lines
21 KiB
Rust
673 lines
21 KiB
Rust
//! Independent B2/B3 native fixture; no desktop implementation is linked.
|
|
//! generate DIR | check CHECKPOINT ENTRY | same VERIFIED_ENTRY RESTORED_ENTRY | self-check
|
|
use oa_core::{source_project::*, *};
|
|
use std::{
|
|
collections::{BTreeMap, BTreeSet},
|
|
fs,
|
|
path::Path,
|
|
};
|
|
|
|
fn id(n: u128) -> Id {
|
|
Id::from_u128(0x5ceaa100000040008000000000000000 + n)
|
|
}
|
|
#[derive(Clone, Copy)]
|
|
struct Side {
|
|
layer: Layer,
|
|
word: &'static str,
|
|
base: u128,
|
|
}
|
|
const SIDES: [Side; 2] = [
|
|
Side {
|
|
layer: Layer::Oa,
|
|
word: "OA",
|
|
base: 100,
|
|
},
|
|
Side {
|
|
layer: Layer::Sa,
|
|
word: "SA",
|
|
base: 200,
|
|
},
|
|
];
|
|
impl Side {
|
|
fn id(self, n: u128) -> Id {
|
|
id(self.base + n)
|
|
}
|
|
fn name(self, text: &str) -> String {
|
|
format!("{} {text}", self.word)
|
|
}
|
|
}
|
|
fn seed(p: &mut Project, side: Side, n: u128, kind: Kind, text: &str, parent: Option<Id>) {
|
|
let mut element = p
|
|
.make_element(kind, side.layer, &side.name(text), parent)
|
|
.unwrap();
|
|
element.id = side.id(n);
|
|
element.alias = format!("scenario_{}_{}", side.word.to_lowercase(), n);
|
|
p.elements.insert(element.id, element);
|
|
}
|
|
fn seed_occurrence(
|
|
p: &mut Project,
|
|
side: Side,
|
|
n: u128,
|
|
text: &str,
|
|
parent: u128,
|
|
body: OccurrenceBody,
|
|
) {
|
|
seed(p, side, n, Kind::Occurrence, text, Some(side.id(parent)));
|
|
*p.elements
|
|
.get_mut(&side.id(n))
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
};
|
|
}
|
|
fn action(side: Side, work: u128, line: u128) -> OccurrenceBody {
|
|
OccurrenceBody::Action {
|
|
action: Some(side.id(work)),
|
|
lifeline: Some(side.id(line)),
|
|
}
|
|
}
|
|
fn reuse(side: Side, target: u128, remote: [u128; 2], local: [u128; 2]) -> OccurrenceBody {
|
|
OccurrenceBody::ScenarioUse {
|
|
scenario: Some(side.id(target)),
|
|
bindings: remote
|
|
.into_iter()
|
|
.zip(local)
|
|
.map(|(referenced, local)| LifelineBinding {
|
|
referenced: side.id(referenced),
|
|
local: side.id(local),
|
|
})
|
|
.collect(),
|
|
}
|
|
}
|
|
fn fixture() -> Project {
|
|
let mut p = Project::empty();
|
|
p.id = id(1);
|
|
p.alias = "scenario_authoring".into();
|
|
p.name = "Scenario authoring contract".into();
|
|
p.description = "Native clicked-slot and caller-return contract.".into();
|
|
for side in SIDES {
|
|
let work = if side.layer == Layer::Oa {
|
|
Kind::Activity
|
|
} else {
|
|
Kind::Function
|
|
};
|
|
seed(&mut p, side, 10, work, "Inspect ticket", None);
|
|
seed(&mut p, side, 11, work, "Record result", None);
|
|
seed(&mut p, side, 20, Kind::StateMachine, "Ticket modes", None);
|
|
seed(
|
|
&mut p,
|
|
side,
|
|
21,
|
|
Kind::Region,
|
|
"Ticket region",
|
|
Some(side.id(20)),
|
|
);
|
|
seed(&mut p, side, 22, Kind::State, "Ready", Some(side.id(21)));
|
|
for (n, text) in [(30, "A caller"), (31, "B verification"), (32, "C detail")] {
|
|
seed(&mut p, side, n, Kind::Scenario, text, None);
|
|
}
|
|
for (n, owner, represented, text) in [
|
|
(40, 30, 10, "A inspector"),
|
|
(41, 30, 11, "A recorder"),
|
|
(42, 31, 10, "B inspector"),
|
|
(43, 31, 11, "B recorder"),
|
|
(44, 32, 10, "C inspector"),
|
|
(45, 32, 11, "C recorder"),
|
|
] {
|
|
seed(&mut p, side, n, Kind::Lifeline, text, Some(side.id(owner)));
|
|
*p.elements
|
|
.get_mut(&side.id(n))
|
|
.unwrap()
|
|
.shared_mut()
|
|
.unwrap() = SharedBody::Lifeline {
|
|
represented: Some(side.id(represented)),
|
|
};
|
|
}
|
|
seed_occurrence(&mut p, side, 50, "Root entry", 30, action(side, 10, 40));
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
51,
|
|
"Outer ALT",
|
|
30,
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![side.id(52), side.id(53)],
|
|
},
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
52,
|
|
"Outer chosen operand",
|
|
51,
|
|
OccurrenceBody::Branch {
|
|
guard: "ticket.present".into(),
|
|
occurrences: vec![side.id(54)],
|
|
},
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
53,
|
|
"Outer fallback operand",
|
|
51,
|
|
OccurrenceBody::Branch {
|
|
guard: "else".into(),
|
|
occurrences: vec![side.id(59)],
|
|
},
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
54,
|
|
"Inner ALT",
|
|
52,
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![side.id(55), side.id(56)],
|
|
},
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
55,
|
|
"Inner chosen operand",
|
|
54,
|
|
OccurrenceBody::Branch {
|
|
guard: "ticket.ready".into(),
|
|
occurrences: vec![side.id(57)],
|
|
},
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
56,
|
|
"Inner other operand",
|
|
54,
|
|
OccurrenceBody::Branch {
|
|
guard: "else".into(),
|
|
occurrences: vec![side.id(58)],
|
|
},
|
|
);
|
|
for (n, text, parent, line) in [
|
|
(57, "Inner anchor", 55, 40),
|
|
(58, "Inner other anchor", 56, 40),
|
|
(59, "Fallback anchor", 53, 41),
|
|
(62, "Root exit", 30, 41),
|
|
(63, "B entry", 31, 42),
|
|
(65, "B exit", 31, 43),
|
|
(66, "C entry", 32, 44),
|
|
] {
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
n,
|
|
text,
|
|
parent,
|
|
action(side, if line % 2 == 0 { 10 } else { 11 }, line),
|
|
);
|
|
}
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
60,
|
|
"First use of B",
|
|
30,
|
|
reuse(side, 31, [42, 43], [40, 41]),
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
61,
|
|
"Second use of B",
|
|
30,
|
|
reuse(side, 31, [42, 43], [40, 41]),
|
|
);
|
|
seed_occurrence(
|
|
&mut p,
|
|
side,
|
|
64,
|
|
"B use of C",
|
|
31,
|
|
reuse(side, 32, [44, 45], [42, 43]),
|
|
);
|
|
for (owner, steps) in [
|
|
(30, vec![50, 51, 60, 61, 62]),
|
|
(31, vec![63, 64, 65]),
|
|
(32, vec![66]),
|
|
] {
|
|
*p.elements
|
|
.get_mut(&side.id(owner))
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap() = steps.into_iter().map(|n| side.id(n)).collect();
|
|
}
|
|
}
|
|
execute_batch(&p, &[]).expect("deterministic fixture must satisfy core admission")
|
|
}
|
|
fn bundle(p: &Project) -> CompiledSourceProject {
|
|
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(), "// Retain caller/operand evidence: ticket 雨.\r\n// Navigation never edits this note.\r\n".into()),
|
|
]),
|
|
};
|
|
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
|
assert_eq!(compiled.decoded.project, *p);
|
|
assert_eq!(compiled.bundle, raw);
|
|
compiled
|
|
}
|
|
fn load(entry: &Path) -> CompiledSourceProject {
|
|
let files = source_files::load_project(entry, ProjectLimits::default()).unwrap();
|
|
let raw = files.reachable_bundle();
|
|
let compiled = compile_bundle(&raw, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
compiled.bundle, raw,
|
|
"No migration or source normalization permitted"
|
|
);
|
|
execute_batch(&compiled.decoded.project, &[]).unwrap();
|
|
compiled
|
|
}
|
|
fn advance(base: &mut CompiledSourceProject, commands: Vec<Command>) {
|
|
let expected = execute_batch(&base.decoded.project, &[Command::Batch(commands)]).unwrap();
|
|
*base = update_bundle(base, &expected, ProjectLimits::default()).unwrap();
|
|
}
|
|
struct Roles<'a> {
|
|
actual: &'a Project,
|
|
baseline: Project,
|
|
used: BTreeSet<Id>,
|
|
}
|
|
impl Roles<'_> {
|
|
fn occurrence(&mut self, side: Side, text: &str) -> Id {
|
|
let name = side.name(text);
|
|
let found: Vec<_> = self
|
|
.actual
|
|
.elements
|
|
.values()
|
|
.filter(|e| e.kind() == Kind::Occurrence && e.layer() == side.layer && e.name == name)
|
|
.map(|e| e.id)
|
|
.collect();
|
|
assert_eq!(
|
|
found.len(),
|
|
1,
|
|
"Exactly one fresh occurrence for role {name}"
|
|
);
|
|
let id = found[0];
|
|
assert_eq!(
|
|
id.get_version_num(),
|
|
4,
|
|
"Native occurrence needs UUIDv4: {name}"
|
|
);
|
|
assert!(!self.baseline.contains(id), "Role reused a seed: {name}");
|
|
assert!(
|
|
self.used.insert(id),
|
|
"Role identities must be distinct: {name}"
|
|
);
|
|
id
|
|
}
|
|
}
|
|
fn authored(
|
|
p: &Project,
|
|
side: Side,
|
|
id: Id,
|
|
text: &str,
|
|
parent: Id,
|
|
body: OccurrenceBody,
|
|
) -> Element {
|
|
let name = side.name(text);
|
|
Element {
|
|
id,
|
|
alias: p.fresh_alias(&name, "element", id),
|
|
name,
|
|
description: String::new(),
|
|
parent: Some(parent),
|
|
body: ElementBody::shared_in(
|
|
side.layer,
|
|
SharedBody::Occurrence {
|
|
body,
|
|
precondition: String::new(),
|
|
postcondition: String::new(),
|
|
},
|
|
),
|
|
}
|
|
}
|
|
fn insert_before(base: &mut CompiledSourceProject, parent: Id, anchor: Id, elements: Vec<Element>) {
|
|
let mut steps = base.decoded.project.elements[&parent]
|
|
.steps()
|
|
.unwrap()
|
|
.clone();
|
|
let position = steps
|
|
.iter()
|
|
.position(|id| *id == anchor)
|
|
.expect("declared insertion anchor");
|
|
let inserted: Vec<_> = elements
|
|
.iter()
|
|
.filter(|e| e.parent == Some(parent))
|
|
.map(|e| e.id)
|
|
.collect();
|
|
steps.splice(position..position, inserted);
|
|
let mut commands: Vec<_> = elements.into_iter().map(Command::CreateElement).collect();
|
|
commands.push(Command::SetSteps(parent, steps));
|
|
advance(base, commands);
|
|
}
|
|
fn checkpoint(mode: &str) -> Option<(Side, usize)> {
|
|
if mode == "baseline" {
|
|
return None;
|
|
}
|
|
let (prefix, stage) = mode
|
|
.split_once('-')
|
|
.expect("checkpoint is baseline or oa/sa-stage");
|
|
let side = match prefix {
|
|
"oa" => SIDES[0],
|
|
"sa" => SIDES[1],
|
|
_ => panic!("Unknown layer"),
|
|
};
|
|
let count = match stage {
|
|
"action" => 1,
|
|
"state" => 2,
|
|
"reuse" => 3,
|
|
"fragment" => 4,
|
|
_ => panic!("Unknown checkpoint"),
|
|
};
|
|
Some((side, count))
|
|
}
|
|
fn expected(mode: &str, actual: &Project) -> CompiledSourceProject {
|
|
let baseline = fixture();
|
|
let mut base = bundle(&baseline);
|
|
let Some((side, count)) = checkpoint(mode) else {
|
|
return base;
|
|
};
|
|
let mut roles = Roles {
|
|
actual,
|
|
baseline,
|
|
used: BTreeSet::new(),
|
|
};
|
|
let key = roles.occurrence(side, "native action");
|
|
let e = authored(
|
|
&base.decoded.project,
|
|
side,
|
|
key,
|
|
"native action",
|
|
side.id(30),
|
|
action(side, 10, 40),
|
|
);
|
|
insert_before(&mut base, side.id(30), side.id(50), vec![e]);
|
|
if count == 1 {
|
|
return base;
|
|
}
|
|
let key = roles.occurrence(side, "native state");
|
|
let e = authored(
|
|
&base.decoded.project,
|
|
side,
|
|
key,
|
|
"native state",
|
|
side.id(55),
|
|
OccurrenceBody::State {
|
|
state: Some(side.id(22)),
|
|
lifeline: Some(side.id(40)),
|
|
},
|
|
);
|
|
insert_before(&mut base, side.id(55), side.id(57), vec![e]);
|
|
if count == 2 {
|
|
return base;
|
|
}
|
|
let key = roles.occurrence(side, "native reuse");
|
|
let e = authored(
|
|
&base.decoded.project,
|
|
side,
|
|
key,
|
|
"native reuse",
|
|
side.id(55),
|
|
reuse(side, 31, [42, 43], [40, 41]),
|
|
);
|
|
insert_before(&mut base, side.id(55), side.id(57), vec![e]);
|
|
if count == 3 {
|
|
return base;
|
|
}
|
|
let fragment = roles.occurrence(side, "native alternative");
|
|
let branch1 = roles.occurrence(side, "native alternative · operand 1");
|
|
let branch2 = roles.occurrence(side, "native alternative · operand 2");
|
|
let p = &base.decoded.project;
|
|
let elements = vec![
|
|
authored(
|
|
p,
|
|
side,
|
|
fragment,
|
|
"native alternative",
|
|
side.id(30),
|
|
OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![],
|
|
},
|
|
),
|
|
authored(
|
|
p,
|
|
side,
|
|
branch1,
|
|
"native alternative · operand 1",
|
|
fragment,
|
|
OccurrenceBody::Branch {
|
|
guard: "ticket.priority > 2".into(),
|
|
occurrences: vec![],
|
|
},
|
|
),
|
|
authored(
|
|
p,
|
|
side,
|
|
branch2,
|
|
"native alternative · operand 2",
|
|
fragment,
|
|
OccurrenceBody::Branch {
|
|
guard: "ticket.priority <= 2".into(),
|
|
occurrences: vec![],
|
|
},
|
|
),
|
|
];
|
|
insert_before(&mut base, side.id(30), side.id(62), elements);
|
|
base
|
|
}
|
|
fn same_snapshot(verified: &CompiledSourceProject, restored: &CompiledSourceProject) -> bool {
|
|
verified.decoded.project == restored.decoded.project && verified.bundle == restored.bundle
|
|
}
|
|
fn self_check() {
|
|
let baseline = fixture();
|
|
assert_eq!(
|
|
baseline,
|
|
fixture(),
|
|
"seed identities and fields must be deterministic"
|
|
);
|
|
let base = bundle(&baseline);
|
|
assert_eq!(expected("baseline", &baseline).bundle, base.bundle);
|
|
for side in SIDES {
|
|
// Role carrier intentionally has wrong parent, alias and occurrence body.
|
|
// Only its fresh IDs may survive in any prediction.
|
|
let mut carrier = baseline.clone();
|
|
for text in [
|
|
"native action",
|
|
"native state",
|
|
"native reuse",
|
|
"native alternative",
|
|
"native alternative · operand 1",
|
|
"native alternative · operand 2",
|
|
] {
|
|
let mut e = carrier
|
|
.make_element(Kind::Occurrence, side.layer, &side.name(text), None)
|
|
.unwrap();
|
|
e.alias = "wrong_carrier_alias".into();
|
|
e.description = "must never enter the prediction".into();
|
|
carrier.elements.insert(e.id, e);
|
|
}
|
|
for stage in ["action", "state", "reuse", "fragment"] {
|
|
let mode = format!("{}-{stage}", side.word.to_lowercase());
|
|
let correct = expected(&mode, &carrier);
|
|
assert_ne!(correct.decoded.project, carrier);
|
|
let again = expected(&mode, &correct.decoded.project);
|
|
assert_eq!(again.decoded.project, correct.decoded.project);
|
|
assert_eq!(
|
|
again.bundle, correct.bundle,
|
|
"full source checkpoint fixed point"
|
|
);
|
|
let mut wrong = correct.decoded.project.clone();
|
|
wrong.elements.get_mut(&side.id(10)).unwrap().description =
|
|
"unrequested seed edit".into();
|
|
assert_ne!(expected(&mode, &wrong).decoded.project, wrong);
|
|
let native = wrong
|
|
.elements
|
|
.values()
|
|
.find(|e| e.name == side.name("native action"))
|
|
.unwrap()
|
|
.id;
|
|
let mut wrong = correct.decoded.project.clone();
|
|
wrong.elements.get_mut(&native).unwrap().parent = Some(side.id(55));
|
|
assert_ne!(
|
|
expected(&mode, &wrong).decoded.project,
|
|
wrong,
|
|
"parent is predicted, not read back"
|
|
);
|
|
let mut wrong = correct.decoded.project.clone();
|
|
wrong
|
|
.elements
|
|
.get_mut(&side.id(30))
|
|
.unwrap()
|
|
.steps_mut()
|
|
.unwrap()
|
|
.swap(0, 1);
|
|
assert_ne!(
|
|
expected(&mode, &wrong).decoded.project,
|
|
wrong,
|
|
"clicked order is independently fixed"
|
|
);
|
|
}
|
|
let mode = format!("{}-fragment", side.word.to_lowercase());
|
|
let correct = expected(&mode, &carrier);
|
|
// Both semantic checks may legitimately discover fresh native IDs. An
|
|
// Undo/Redo check must additionally pin them to the earlier snapshot.
|
|
let mut other_carrier = carrier.clone();
|
|
let old = other_carrier
|
|
.elements
|
|
.values()
|
|
.find(|e| e.name == side.name("native action"))
|
|
.unwrap()
|
|
.id;
|
|
let mut replaced = other_carrier.elements.remove(&old).unwrap();
|
|
replaced.id = Id::new_v4();
|
|
other_carrier.elements.insert(replaced.id, replaced);
|
|
let recreated = expected(&mode, &other_carrier);
|
|
let correct_prediction = expected(&mode, &correct.decoded.project);
|
|
let recreated_prediction = expected(&mode, &recreated.decoded.project);
|
|
assert!(same_snapshot(&correct_prediction, &correct));
|
|
assert!(same_snapshot(&recreated_prediction, &recreated));
|
|
assert!(
|
|
!same_snapshot(&correct, &recreated),
|
|
"recreated native IDs pass semantic roles but must fail exact history restoration"
|
|
);
|
|
let mut wrong = correct.decoded.project.clone();
|
|
let e = wrong
|
|
.elements
|
|
.values_mut()
|
|
.find(|e| e.name == side.name("native reuse"))
|
|
.unwrap();
|
|
let Some(SharedBody::Occurrence {
|
|
body: OccurrenceBody::ScenarioUse { bindings, .. },
|
|
..
|
|
}) = e.shared_mut()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
bindings[0].local = side.id(41);
|
|
assert_ne!(
|
|
expected(&mode, &wrong).decoded.project,
|
|
wrong,
|
|
"bindings are never copied from actual"
|
|
);
|
|
let mut changed_source = correct.bundle.clone();
|
|
changed_source
|
|
.files
|
|
.get_mut("rationale.arcadia")
|
|
.unwrap()
|
|
.push_str("// unrequested note\n");
|
|
let changed = compile_bundle(&changed_source, ProjectLimits::default()).unwrap();
|
|
assert_eq!(changed.decoded.project, correct.decoded.project);
|
|
assert_ne!(
|
|
changed.bundle,
|
|
expected(&mode, &changed.decoded.project).bundle
|
|
);
|
|
}
|
|
println!(
|
|
"PASS self-check: deterministic OA/SA baseline, eight semantic checkpoints, independent fields/source bytes and exact history identity discrimination"
|
|
);
|
|
}
|
|
fn main() {
|
|
assert_eq!(SCHEMA_VERSION, 7);
|
|
let args: Vec<_> = std::env::args().collect();
|
|
if args.get(1).is_some_and(|s| s == "self-check") {
|
|
assert_eq!(args.len(), 2);
|
|
self_check();
|
|
return;
|
|
}
|
|
if args.get(1).is_some_and(|s| s == "generate") {
|
|
assert_eq!(args.len(), 3);
|
|
let dir = Path::new(&args[2]);
|
|
assert!(
|
|
!dir.exists(),
|
|
"Generate only into a fresh directory; preserve existing evidence"
|
|
);
|
|
let p = fixture();
|
|
let base = bundle(&p);
|
|
fs::create_dir_all(dir).unwrap();
|
|
for (name, bytes) in base.bundle.files {
|
|
fs::write(dir.join(name), bytes).unwrap();
|
|
}
|
|
fs::write(
|
|
dir.join("expected-baseline.json"),
|
|
serde_json::to_vec_pretty(&p).unwrap(),
|
|
)
|
|
.unwrap();
|
|
println!(
|
|
"PASS generated deterministic OA/SA fixture: {} elements, 2 sources; native roles absent",
|
|
p.elements.len()
|
|
);
|
|
return;
|
|
}
|
|
if args.get(1).is_some_and(|s| s == "same") {
|
|
assert_eq!(args.len(), 4, "same VERIFIED_SAVED_ENTRY RESTORED_ENTRY");
|
|
let verified = load(Path::new(&args[2]));
|
|
let restored = load(Path::new(&args[3]));
|
|
assert!(
|
|
same_snapshot(&verified, &restored),
|
|
"Restored snapshot must preserve every Project identity/field and every SourceBundle byte"
|
|
);
|
|
println!(
|
|
"PASS same: exact saved/restored Project and SourceBundle, including all native identities"
|
|
);
|
|
return;
|
|
}
|
|
assert_eq!(
|
|
args.len(),
|
|
4,
|
|
"check baseline|oa-action|oa-state|oa-reuse|oa-fragment|sa-action|sa-state|sa-reuse|sa-fragment ENTRY"
|
|
);
|
|
assert_eq!(args[1], "check");
|
|
let actual = load(Path::new(&args[3]));
|
|
let prediction = expected(&args[2], &actual.decoded.project);
|
|
assert_eq!(
|
|
actual.decoded.project, prediction.decoded.project,
|
|
"Complete Project differs from independent checkpoint"
|
|
);
|
|
assert_eq!(
|
|
actual.bundle, prediction.bundle,
|
|
"Complete source bundle differs, including retained note bytes"
|
|
);
|
|
println!(
|
|
"PASS {}: whole model and {} exact reachable sources; only unique fresh role IDs borrowed",
|
|
args[2],
|
|
actual.bundle.files.len()
|
|
);
|
|
}
|