Add exact Process selection and atomic column-slot group reorder
Preserve the full-desktop-1 captured source. Qualified previews inspect their exact subject read-only; group movement preserves source order and other columns. Native acceptance and strict-lint payload-size corrections follow separately.
This commit is contained in:
@@ -0,0 +1,544 @@
|
||||
//! Deterministic Process native fixture and independent complete-Project oracle.
|
||||
//! Expected order is literal; no desktop reorder/layout planner is used here.
|
||||
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 {
|
||||
Id::new_v5(
|
||||
&Id::from_u128(0xc1192026_0923_5000_8000_000000002930),
|
||||
alias.as_bytes(),
|
||||
)
|
||||
}
|
||||
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 occurrence(
|
||||
p: &mut Project,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
layer: Layer,
|
||||
owner: &str,
|
||||
value: OccurrenceBody,
|
||||
) {
|
||||
element(p, alias, name, Kind::Occurrence, layer, Some(id(owner)));
|
||||
let Some(SharedBody::Occurrence { body, .. }) =
|
||||
p.elements.get_mut(&id(alias)).unwrap().shared_mut()
|
||||
else {
|
||||
panic!("Occurrence body");
|
||||
};
|
||||
*body = value;
|
||||
}
|
||||
fn set_steps(p: &mut Project, owner: &str, aliases: &[&str]) {
|
||||
*p.elements.get_mut(&id(owner)).unwrap().steps_mut().unwrap() =
|
||||
aliases.iter().map(|alias| id(alias)).collect();
|
||||
}
|
||||
fn sequence_link(
|
||||
p: &mut Project,
|
||||
alias: &str,
|
||||
name: &str,
|
||||
source: &str,
|
||||
target: &str,
|
||||
) -> Result<()> {
|
||||
let old = p.relate(RelationKind::SequenceLink, name, id(source), id(target));
|
||||
reidentify(p, old, id(alias))?;
|
||||
let relation = p.relations.get_mut(&id(alias)).unwrap();
|
||||
relation.alias = alias.into();
|
||||
relation.description = format!("Preserve link {alias} exactly.");
|
||||
if let RelationBody::SequenceLink { guard, .. } = &mut relation.body {
|
||||
*guard = "ready".into();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn fixture() -> Result<Project> {
|
||||
let mut p = Project::empty();
|
||||
p.id = id("project");
|
||||
p.alias = "process_multiselect_native".into();
|
||||
p.name = "Process group native".into();
|
||||
p.description = "Select Card C then Shift-select Card A; drop after Card B. Other columns and reusable engineering records remain exact.".into();
|
||||
for (layer, prefix, owner, name, owner_kind, work_kind) in [
|
||||
(
|
||||
Layer::Oa,
|
||||
"oa",
|
||||
"oa_process",
|
||||
"01 OA group order",
|
||||
Kind::Process,
|
||||
Kind::Activity,
|
||||
),
|
||||
(
|
||||
Layer::Sa,
|
||||
"sa",
|
||||
"sa_chain",
|
||||
"SA chain - preserve",
|
||||
Kind::FunctionalChain,
|
||||
Kind::Function,
|
||||
),
|
||||
] {
|
||||
element(&mut p, owner, name, owner_kind, layer, None);
|
||||
let shared = format!("{prefix}_shared_work");
|
||||
let downstream = format!("{prefix}_downstream_work");
|
||||
element(
|
||||
&mut p,
|
||||
&shared,
|
||||
"Shared request work",
|
||||
work_kind,
|
||||
layer,
|
||||
None,
|
||||
);
|
||||
element(
|
||||
&mut p,
|
||||
&downstream,
|
||||
"Downstream verification",
|
||||
work_kind,
|
||||
layer,
|
||||
None,
|
||||
);
|
||||
for (suffix, label) in [
|
||||
("a", "Card A"),
|
||||
("u", "Downstream U"),
|
||||
("b", "Card B"),
|
||||
("v", "Downstream V"),
|
||||
("c", "Card C"),
|
||||
] {
|
||||
let body = if suffix == "b" {
|
||||
OccurrenceBody::Control {
|
||||
kind: ControlKind::Choice,
|
||||
}
|
||||
} else {
|
||||
OccurrenceBody::Action {
|
||||
action: Some(id(if suffix == "a" || suffix == "c" {
|
||||
&shared
|
||||
} else {
|
||||
&downstream
|
||||
})),
|
||||
lifeline: None,
|
||||
}
|
||||
};
|
||||
occurrence(
|
||||
&mut p,
|
||||
&format!("{prefix}_{suffix}"),
|
||||
label,
|
||||
layer,
|
||||
owner,
|
||||
body,
|
||||
);
|
||||
}
|
||||
let order: Vec<_> = ["a", "u", "b", "v", "c"]
|
||||
.map(|suffix| format!("{prefix}_{suffix}"))
|
||||
.into_iter()
|
||||
.collect();
|
||||
set_steps(
|
||||
&mut p,
|
||||
owner,
|
||||
&order.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
);
|
||||
sequence_link(
|
||||
&mut p,
|
||||
&format!("{prefix}_a_u"),
|
||||
"A to U",
|
||||
&format!("{prefix}_a"),
|
||||
&format!("{prefix}_u"),
|
||||
)?;
|
||||
sequence_link(
|
||||
&mut p,
|
||||
&format!("{prefix}_b_v"),
|
||||
"B to V",
|
||||
&format!("{prefix}_b"),
|
||||
&format!("{prefix}_v"),
|
||||
)?;
|
||||
}
|
||||
element(
|
||||
&mut p,
|
||||
"reusable",
|
||||
"Reusable request detail",
|
||||
Kind::Process,
|
||||
Layer::Oa,
|
||||
None,
|
||||
);
|
||||
occurrence(
|
||||
&mut p,
|
||||
"reused_leaf",
|
||||
"Shared detail",
|
||||
Layer::Oa,
|
||||
"reusable",
|
||||
OccurrenceBody::Action {
|
||||
action: Some(id("oa_shared_work")),
|
||||
lifeline: None,
|
||||
},
|
||||
);
|
||||
set_steps(&mut p, "reusable", &["reused_leaf"]);
|
||||
element(
|
||||
&mut p,
|
||||
"reuse_trial",
|
||||
"02 Read-only preview trial",
|
||||
Kind::Process,
|
||||
Layer::Oa,
|
||||
None,
|
||||
);
|
||||
for (alias, name) in [
|
||||
("reference_one", "First use"),
|
||||
("reference_two", "Second use"),
|
||||
] {
|
||||
occurrence(
|
||||
&mut p,
|
||||
alias,
|
||||
name,
|
||||
Layer::Oa,
|
||||
"reuse_trial",
|
||||
OccurrenceBody::SequenceReference {
|
||||
sequence: Some(id("reusable")),
|
||||
},
|
||||
);
|
||||
}
|
||||
set_steps(&mut p, "reuse_trial", &["reference_one", "reference_two"]);
|
||||
p.traces.insert(
|
||||
id("trace"),
|
||||
Trace {
|
||||
id: id("trace"),
|
||||
alias: "preserved_work_refinement".into(),
|
||||
name: "Shared request refinement".into(),
|
||||
description: "Process arrangement must preserve cross-layer correspondence.".into(),
|
||||
oa: id("oa_shared_work"),
|
||||
sa: id("sa_shared_work"),
|
||||
kind: TraceKind::Refinement,
|
||||
baseline: Some(TraceBaseline {
|
||||
oa: p.properties(id("oa_shared_work")).unwrap(),
|
||||
sa: p.properties(id("sa_shared_work")).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())?;
|
||||
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 expected = serde_json::to_value(expected)?;
|
||||
let actual = serde_json::to_value(actual)?;
|
||||
let fields: Vec<_> = expected
|
||||
.as_object()
|
||||
.unwrap()
|
||||
.keys()
|
||||
.filter(|key| expected[*key] != actual[*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 ids: std::collections::BTreeMap<_, _> = [
|
||||
"project",
|
||||
"oa_process",
|
||||
"oa_a",
|
||||
"oa_u",
|
||||
"oa_b",
|
||||
"oa_v",
|
||||
"oa_c",
|
||||
"oa_a_u",
|
||||
"oa_b_v",
|
||||
"oa_shared_work",
|
||||
"oa_downstream_work",
|
||||
"sa_chain",
|
||||
"sa_shared_work",
|
||||
"sa_downstream_work",
|
||||
"sa_a",
|
||||
"sa_u",
|
||||
"sa_b",
|
||||
"sa_v",
|
||||
"sa_c",
|
||||
"sa_a_u",
|
||||
"sa_b_v",
|
||||
"trace",
|
||||
"reusable",
|
||||
"reused_leaf",
|
||||
"reuse_trial",
|
||||
"reference_one",
|
||||
"reference_two",
|
||||
]
|
||||
.into_iter()
|
||||
.map(|name| (name, id(name)))
|
||||
.collect();
|
||||
write_new(
|
||||
&out.join("manifest.json"),
|
||||
&serde_json::to_vec_pretty(&serde_json::json!({
|
||||
"fixture": "process-multiselect-native-20260923", "ids": ids, "source_entry": bundle.entry,
|
||||
"original_oa_steps": ["oa_a", "oa_u", "oa_b", "oa_v", "oa_c"],
|
||||
"reordered_oa_steps": ["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"],
|
||||
"gesture": "Select Card C then Shift-select Card A; drag selected card to After Card B",
|
||||
"fixed_slots": {"1": "oa_u", "3": "oa_v"},
|
||||
"read_only_trial": "02 Read-only preview trial has two qualified tiles of reused_leaf through distinct references",
|
||||
"receipt": "Pin baseline.json, manifest.json and generation command externally before interaction. Never replace baseline with a later saved snapshot.",
|
||||
"evaluation": "Literal expected OA steps and complete Project equality; all SA/reuse/trace records exact",
|
||||
"limits": "No pointer, focus, selection-paint, history or source-byte acceptance claim",
|
||||
}))?,
|
||||
)?;
|
||||
equal(&p, &load(&out.join("baseline.json"))?)?;
|
||||
equal(&p, &load(&out.join("source").join(&bundle.entry))?)?;
|
||||
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)?;
|
||||
// Pin the exact deterministic original, not merely a reusable project UUID.
|
||||
equal(&fixture()?, &before)
|
||||
.map_err(|e| format!("Baseline is not the immutable original: {e}"))?;
|
||||
let mut expected = before;
|
||||
match mode {
|
||||
"unchanged" => {}
|
||||
"reordered" => set_steps(
|
||||
&mut expected,
|
||||
"oa_process",
|
||||
&["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"],
|
||||
),
|
||||
_ => return Err("Mode must be reordered 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; literal expected order",
|
||||
"limits": "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: process_multiselect_native generate DIRECTORY | check reordered|unchanged BASELINE SAVED".into()),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
fn reordered(before: &Project) -> Project {
|
||||
let mut session = Session::new(before.clone());
|
||||
session
|
||||
.execute(Command::SetSteps(
|
||||
id("oa_process"),
|
||||
["oa_b", "oa_u", "oa_a", "oa_v", "oa_c"].map(id).to_vec(),
|
||||
))
|
||||
.unwrap();
|
||||
session.project().clone()
|
||||
}
|
||||
fn through_json(mode: &str, baseline: &Project, actual: &Project) -> Result<()> {
|
||||
validate_candidate(baseline).expect("Baseline must be valid");
|
||||
validate_candidate(actual).expect("Corruption must remain semantically valid");
|
||||
let dir = tempfile::tempdir()?;
|
||||
let before = dir.path().join("before.json");
|
||||
let saved = dir.path().join("saved.json");
|
||||
persistence::save(&before, baseline)?;
|
||||
persistence::save(&saved, actual)?;
|
||||
check(mode, &before, &saved)
|
||||
}
|
||||
fn mismatch(before: &Project, actual: &Project) {
|
||||
let error = through_json("reordered", before, actual)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(
|
||||
error.contains("Whole Project mismatch"),
|
||||
"Unexpected rejection: {error}"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn generate_is_deterministic_create_only_and_checks_real_source() {
|
||||
equal(&fixture().unwrap(), &fixture().unwrap()).unwrap();
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let out = dir.path().join("fixture");
|
||||
generate(&out).unwrap();
|
||||
let baseline = out.join("baseline.json");
|
||||
let bytes = fs::read(&baseline).unwrap();
|
||||
check("unchanged", &baseline, &out.join("source/project.arcadia")).unwrap();
|
||||
assert!(generate(&out).is_err());
|
||||
assert_eq!(fs::read(&baseline).unwrap(), bytes);
|
||||
let invalid = dir.path().join("invalid.arcadia");
|
||||
fs::write(&invalid, "project {").unwrap();
|
||||
assert!(load(&invalid).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn real_set_steps_passes_but_unchanged_and_mutated_baseline_do_not() {
|
||||
let before = fixture().unwrap();
|
||||
let actual = reordered(&before);
|
||||
through_json("reordered", &before, &actual).unwrap();
|
||||
through_json("unchanged", &before, &before).unwrap();
|
||||
mismatch(&before, &before);
|
||||
assert!(through_json("unchanged", &before, &actual).is_err());
|
||||
let error = through_json("unchanged", &actual, &actual)
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
assert!(error.contains("immutable original"), "{error}");
|
||||
}
|
||||
#[test]
|
||||
fn global_splice_reversed_block_and_primary_only_results_are_rejected() {
|
||||
let before = fixture().unwrap();
|
||||
for order in [
|
||||
["oa_u", "oa_b", "oa_a", "oa_c", "oa_v"],
|
||||
["oa_b", "oa_u", "oa_c", "oa_v", "oa_a"],
|
||||
["oa_a", "oa_u", "oa_b", "oa_c", "oa_v"],
|
||||
] {
|
||||
let mut actual = before.clone();
|
||||
set_steps(&mut actual, "oa_process", &order);
|
||||
mismatch(&before, &actual);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn semantic_link_work_sa_reuse_and_trace_changes_are_rejected() {
|
||||
let before = fixture().unwrap();
|
||||
let correct = reordered(&before);
|
||||
let mutations: [fn(&mut Project); 7] = [
|
||||
|p| {
|
||||
if let RelationBody::SequenceLink { guard, .. } =
|
||||
&mut p.relations.get_mut(&id("oa_a_u")).unwrap().body
|
||||
{
|
||||
*guard = "changed".into();
|
||||
}
|
||||
},
|
||||
|p| {
|
||||
if let RelationBody::SequenceLink { target, .. } =
|
||||
&mut p.relations.get_mut(&id("oa_a_u")).unwrap().body
|
||||
{
|
||||
*target = id("oa_v");
|
||||
}
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("oa_shared_work"))
|
||||
.unwrap()
|
||||
.name
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("sa_shared_work"))
|
||||
.unwrap()
|
||||
.description
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
set_steps(p, "sa_chain", &["sa_b", "sa_u", "sa_a", "sa_v", "sa_c"]);
|
||||
},
|
||||
|p| {
|
||||
p.elements
|
||||
.get_mut(&id("reused_leaf"))
|
||||
.unwrap()
|
||||
.description
|
||||
.push_str(" drift");
|
||||
},
|
||||
|p| {
|
||||
p.traces.get_mut(&id("trace")).unwrap().baseline = None;
|
||||
},
|
||||
];
|
||||
for mutate in mutations {
|
||||
let mut bad = correct.clone();
|
||||
mutate(&mut bad);
|
||||
mismatch(&before, &bad);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn changed_source_roundtrip_preserves_both_layers_and_checker_rejects_source_drift() {
|
||||
let before = fixture().unwrap();
|
||||
let actual = reordered(&before);
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let baseline = dir.path().join("baseline.json");
|
||||
persistence::save(&baseline, &before).unwrap();
|
||||
for corrupt in [false, true] {
|
||||
let mut saved = actual.clone();
|
||||
if corrupt {
|
||||
saved
|
||||
.elements
|
||||
.get_mut(&id("sa_c"))
|
||||
.unwrap()
|
||||
.name
|
||||
.push_str(" drift");
|
||||
}
|
||||
let bundle = roundtrip(&saved).unwrap();
|
||||
let folder = dir.path().join(if corrupt { "corrupt" } else { "saved" });
|
||||
for (path, text) in &bundle.files {
|
||||
write_new(&folder.join(path), text.as_bytes()).unwrap();
|
||||
}
|
||||
let parsed = load(&folder.join(&bundle.entry)).unwrap();
|
||||
assert_eq!(parsed.elements[&id("oa_process")].kind(), Kind::Process);
|
||||
assert_eq!(
|
||||
parsed.elements[&id("sa_chain")].kind(),
|
||||
Kind::FunctionalChain
|
||||
);
|
||||
let result = check("reordered", &baseline, &folder.join(&bundle.entry));
|
||||
if corrupt {
|
||||
assert!(
|
||||
result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Whole Project mismatch")
|
||||
);
|
||||
} else {
|
||||
result.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user