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:
2026-09-23 05:39:10 -07:00
parent bed7f825fd
commit 1711bff650
25 changed files with 3993 additions and 145 deletions
@@ -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();
}
}
}
}