diff --git a/crates/oa-desktop/src/workflow.rs b/crates/oa-desktop/src/workflow.rs index 244b518b..1384a824 100644 --- a/crates/oa-desktop/src/workflow.rs +++ b/crates/oa-desktop/src/workflow.rs @@ -1407,16 +1407,6 @@ impl Workflow { .color(style::MUTED), ); } - content = content.push( - text(format!( - "{} changes · {} required boundary choices · {} unresolved conflicts · {} modeling warnings", - d.plan.changes.len(), - d.plan.conflicts.iter().filter(|c| d.boundary_choice(p, c)).count(), - d.plan.conflicts.iter().filter(|c| !d.boundary_choice(p, c)).count(), - d.plan.warnings.len() - )) - .size(12), - ); } } let enabled = match self { @@ -1433,12 +1423,50 @@ impl Workflow { .color(iced::Color::from_rgb8(178, 54, 58)), ); } - column![ - scrollable(content.padding(iced::Padding { - right: 14., - ..Default::default() - })) - .height(480), + let mut footer = column![].spacing(8); + if let Self::Transition(d) = self { + let boundary_choices = d + .plan + .conflicts + .iter() + .filter(|c| d.boundary_choice(p, c)) + .count(); + footer = footer.push( + text(format!( + "{} reviewed items · {} boundary choices · {} conflicts · {} warnings", + d.plan.changes.len(), + boundary_choices, + d.plan.conflicts.len() - boundary_choices, + d.plan.warnings.len() + )) + .size(12), + ); + let guidance = if d.destination_error(p).is_some() { + Some("Choose a compatible diagram in Show result in, above the mappings.".into()) + } else if boundary_choices > 0 { + Some("Choose the counterpart kind in each mapping that needs a system boundary decision.".into()) + } else if let Some(field) = d.plan.conflicts.iter().find_map(|c| c.field) { + let field = match field { + TransitionField::Name => "Name", + TransitionField::Description => "Description", + }; + Some(format!( + "Review {field} below the mappings: choose Keep target, Take source, or merged text." + )) + } else if !d.plan.conflicts.is_empty() { + Some("Review the conflict messages below the mappings and complete the required decisions.".into()) + } else { + None + }; + if let Some(guidance) = guidance { + footer = footer.push( + text(guidance) + .size(12) + .color(iced::Color::from_rgb8(178, 54, 58)), + ); + } + } + footer = footer.push( row![ Space::new().width(Fill), button(text("Cancel").size(12)) @@ -1458,7 +1486,15 @@ impl Workflow { .padding(10) .on_press_maybe(enabled.then_some(Message::Workflow(Event::Apply))) ] - .spacing(10) + .spacing(10), + ); + column![ + scrollable(content.padding(iced::Padding { + right: 14., + ..Default::default() + })) + .height(480), + footer ] .spacing(18) .into() diff --git a/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.lock b/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.lock new file mode 100644 index 00000000..ecc87faf --- /dev/null +++ b/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.lock @@ -0,0 +1,434 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "num-bigint" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "oa-core" +version = "0.2.0" +dependencies = [ + "libc", + "num-bigint", + "num-integer", + "num-traits", + "same-file", + "serde", + "serde_json", + "tempfile", + "thiserror", + "uuid", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12df2e0110f65b775f769bb17ef989067a1d931b2eb822bd4346631eeada89f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.5", +] + +[[package]] +name = "transition-native-lifecycle" +version = "0.1.0" +dependencies = [ + "oa-core", + "serde", + "serde_json", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "uuid" +version = "1.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" +dependencies = [ + "getrandom", + "js-sys", + "serde_core", + "sha1_smol", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml b/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml new file mode 100644 index 00000000..6ede31ba --- /dev/null +++ b/harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "transition-native-lifecycle" +version = "0.1.0" +edition = "2024" +publish = false + +[workspace] + +[dependencies] +oa-core = { path = "../../../../crates/oa-core" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" diff --git a/harness/experiments/transition-native-lifecycle-20260913/producer/README.md b/harness/experiments/transition-native-lifecycle-20260913/producer/README.md new file mode 100644 index 00000000..95fa503e --- /dev/null +++ b/harness/experiments/transition-native-lifecycle-20260913/producer/README.md @@ -0,0 +1,154 @@ +# Native transition lifecycle fixture and oracle + +This isolated producer/checker prepares native OA↔SA acceptance work. It does +not automate the application. The project owns no production code. It has its +own Cargo workspace and depends on this checkout's `oa-core` by relative path. + +## Commands + +From the repository root, with the configured Rust toolchain and compiler gate: + +```powershell +cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- generate .local/transition-native/lifecycle-input-1 +cargo run --manifest-path harness/experiments/transition-native-lifecycle-20260913/producer/Cargo.toml -- check original .local/transition-native/lifecycle-input-1/original.arcadia +``` + +The parent owns compilation, execution and native verification. These commands +are instructions, not evidence that they have run. Generation refuses to +overwrite an existing deliverable; use a new output directory for a new run. +All fixture identities are reproducible UUIDv5 values of fixed aliases in one +fixed namespace. Generating into a second directory must reproduce file bytes. + +`generate OUTDIR` validates and round-trips each expected project, then emits: + +- `original.arcadia`, `original.json`, and `manifest.json` with owned removal IDs. +- `oa-deleted.arcadia` / `.json` and `sa-deleted.arcadia` / `.json`. +- `oa-recreated.arcadia` / `.json` and `sa-recreated.arcadia` / `.json`. +- `oa-recreate-root.snippet` and `sa-recreate-root.snippet`. + +The deleted/recreated files are independent expected artifacts. Opening them +does not establish that the native deletion, Undo or editing operation works. +Each snippet contains only its replacement root declaration, with the original +alias, name and description, a deterministic new UUID, no children, and no +placement. Generation verifies that inserting it immediately before the deleted +project's final closing brace compiles to the exact expected recreated Project. + +`check STATE SAVED_ENTRY [--placements PATH] [--identities PATH] +[--write-identities PATH]` loads the actual saved entry and its reachable +includes through the public disk loader/compiler. It rejects any source bundle +changed by compilation, including declarations with missing UUIDs. An identity +inserted only into the compiler's memory cannot prove a saved identity. States: + +| State | Required saved model | +| --- | --- | +| `original` | Entire original Project, including every original UUID | +| `oa-deleted`, `sa-deleted` | Explicit source-owned removals; opposite design and shared definitions retained exactly | +| `oa-recreated`, `sa-recreated` | Deleted state plus only the original source root under its pinned replacement UUID | +| `oa-relinked`, `sa-relinked` | First relink: recreated state plus exactly one new refinement trace, with both existing prose values acknowledged | +| `oa-reopened`, `sa-reopened` | Same relinked model; requires `--identities` with its previously accepted trace UUID | +| `oa-noop`, `sa-noop` | Same relinked model after unchanged retry; requires `--identities` with its previously accepted trace UUID | + +The checker compares every Project field, including IDs, aliases, prose, typed +bodies, ownership, item/port contracts, relations, trace baselines, diagram +geometry/routes and artifacts. A mismatch reports its first differing JSON path. +No deletion impact helper, transition planner, or observed post-deletion state +constructs the expected removal set. Relinking expectations construct a new +Trace directly. The source UUID is always the deterministic snippet UUID unless +an explicit `--identities` file supplies another source UUID. Only the first +`*-relinked` check can observe a fresh trace UUID. Use `--write-identities PATH` +on that successful check to write a receipt, then pass that exact receipt using +`--identities PATH` for reopening and no-op checks. Those stages refuse to run +without a pinned trace UUID and reject any replacement of source, trace or +declared placement identities. Receipt writes refuse overwrite and happen only +after complete-model comparison succeeds. + +An identity receipt is a JSON object with `source` and, after relinking, `trace` +UUID strings. Optional declared placements add `placement:ALIAS` UUID entries. +An explicitly authored source-only identity file can establish a different +GUI-created root UUID for initial recreation/relinking. Do not re-extract it +from later saves. The checker rejects unused identity entries and reports whether +trace identity was pinned; the first relink establishes identity evidence but +does not itself prove preservation across save/reopen. + +Example using the already compiled producer executable: + +```powershell +transition-native-lifecycle check oa-relinked saved-first-relink.arcadia --write-identities first-relink.identities.json +transition-native-lifecycle check oa-reopened saved-after-reopen.arcadia --identities first-relink.identities.json +transition-native-lifecycle check oa-noop saved-after-noop.arcadia --identities first-relink.identities.json +``` + +## Native workflow + +| Layer | Root to select | Alias | Diagrams | +| --- | --- | --- | --- | +| OA | Coordinate distress response | `oa_response` | `oa_work`, `oa_overview` | +| SA | Deliver coordinated response | `sa_response` | `sa_work`, `sa_overview` | + +Both roots own two work children with an internal report exchange. The SA +children own typed report input/output ports. Shared report definitions have +Flow mechanisms and String payload fields. OA intentionally has no ports. +Both layers have an expanded decomposition and overview diagram, authored +placements, exchange routing/labels, and artifacts on roots, children, +exchanges, placements and shared items. The initial correspondence and its +artifact must disappear when either root is deleted. Shared item definitions, +their fields/evidence, diagrams and the independent System remain. + +1. Open `original.arcadia`. Save a native snapshot and check `original`. +2. Graphically delete **Coordinate distress response** (`oa_response`). Save a + snapshot and check `oa-deleted`. Inspect the complete retained SA design. +3. Native Undo, save another snapshot and check `original`. This checks the + actual saved result, including original IDs and all owned records. +4. Delete OA again. In the native Textual model editor, insert the inspected + `oa-recreate-root.snippet` immediately before the project's final `}`. Apply, + save, and check `oa-recreated`. Do not open the generated recreated fixture + as a substitute for this editing step. +5. Select the newly recreated `oa_response` and start OA→SA transition. Choose + existing `sa_response` explicitly. Select **Keep target** for both Name and + Description; do not request child/required dependency creation. The existing + target already has placements. Apply, save, and check `oa-relinked` with + `--write-identities first-relink.identities.json`. +6. Reopen the saved result and save a fresh snapshot; check `oa-reopened` with + `--identities first-relink.identities.json`. + Re-preview unchanged and inspect reuse/no changes. Applying any no-op and + saving must pass `oa-noop` with the same original identity receipt. Do not + substitute another unpinned `oa-relinked` observation for these later checks. +7. For the reverse workflow, start again from `original.arcadia`, delete + `sa_response`, use the SA snippet, and explicitly relink to retained + `oa_response`, using the corresponding `sa-*` checker states. + +These instructions separate source recreation in the text editor from relinking +through the graphical transition dialog, avoiding unpredictable canvas placement +side effects. The newly recreated root deliberately has no children or placement. + +## Optional explicitly authored source placement + +If a separate GUI recreation task intentionally creates a placement, pass +`--placements` with an expectation file shaped like `oa-placement.example.json`. Set its diagram, alias, +position, size and collapsed state to the intended interaction inputs. Only the +new placement UUID is observed during the first check; all its other fields are +compared with the authored expectation. It must reference the new source root +and use the existing source-layer work/overview diagram. Undeclared placements, +changed target geometry and extra artifacts fail comparison. The snippet +workflow requires no placement expectation file. Later checks must supply the +same placement specifications and the saved identity receipt; the placement UUID +is then pinned rather than re-learned from the new saved model. + +## Explicit incompleteness and limits + +The architecture fixture deliberately omits performer allocations and +process/chain/scenario contexts. Admission requires exactly `unallocated_leaf` +(`allocation`) and `work_without_behavior_context` (no field) on each surviving +leaf work UUID, and `exchange_without_behavior_context` (no field) on each +surviving work exchange UUID. Errors, extra warnings, wrong warning owners and +duplicate warnings fail. System and data contracts are modeled explicitly. +The disk loader and source compiler may report these same engineering warnings. +Their diagnostic multisets must exactly match the validated model warnings by +severity, code, object UUID and field. Additional loading warnings, missing or +duplicate warnings, and all source-loading errors remain failures. + +Whole-model comparison and source/JSON round trips do not prove usability, +authored comment/whitespace preservation, native Undo without saving, or HTTP +retry behavior. The parent must record actual native interactions, executable +provenance, saved snapshots and checker results. This tool does not perform +native interaction or publish an acceptance result itself. diff --git a/harness/experiments/transition-native-lifecycle-20260913/producer/oa-placement.example.json b/harness/experiments/transition-native-lifecycle-20260913/producer/oa-placement.example.json new file mode 100644 index 00000000..4a3e5dac --- /dev/null +++ b/harness/experiments/transition-native-lifecycle-20260913/producer/oa-placement.example.json @@ -0,0 +1,9 @@ +[ + { + "diagram": "oa_work", + "alias": "view_coordinate_distress_response", + "position": { "x": 60.0, "y": 60.0 }, + "size": { "width": 224.0, "height": 94.0 }, + "collapsed": false + } +] diff --git a/harness/experiments/transition-native-lifecycle-20260913/producer/src/main.rs b/harness/experiments/transition-native-lifecycle-20260913/producer/src/main.rs new file mode 100644 index 00000000..31bc8fe6 --- /dev/null +++ b/harness/experiments/transition-native-lifecycle-20260913/producer/src/main.rs @@ -0,0 +1,914 @@ +use oa_core::{ + language, source_files, + source_project::{ProjectLimits, SourceBundle, compile_bundle}, + validation::{self, Severity}, + *, +}; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, fs, io::Write, path::Path}; + +type Result = std::result::Result>; + +fn id(alias: &str) -> Id { + Id::new_v5( + &Id::from_u128(0xc1192026_0913_5000_8000_000000000028), + alias.as_bytes(), + ) +} + +#[derive(Serialize)] +struct Side { + prefix: String, + root: Id, + elements: Vec, + exchange: Id, + placements: Vec<(Id, Id)>, + artifacts: Vec, +} + +struct Fixture { + project: Project, + oa: Side, + sa: Side, +} + +fn add( + p: &mut Project, + kind: Kind, + layer: Layer, + alias: &str, + name: &str, + parent: Option, +) -> Id { + let key = id(alias); + assert!( + p.elements + .insert( + key, + Element { + id: key, + alias: alias.into(), + name: name.into(), + description: String::new(), + parent, + body: ElementBody::new(kind, layer).unwrap(), + } + ) + .is_none() + ); + key +} + +fn artifact(p: &mut Project, owner: Id, alias: &str, name: &str) -> Id { + let key = id(alias); + p.artifact_links.insert( + key, + ArtifactLink { + id: key, + alias: alias.into(), + name: name.into(), + description: "Reviewed evidence must survive opposite-layer deletion.".into(), + owner, + relation: ArtifactRelation::References, + uri: "git:response/design.md".into(), + revision: Some("review-17".into()), + fragment: Some("response-handling".into()), + }, + ); + key +} + +fn port(p: &mut Project, alias: &str, owner: Id, item: Id, output: bool) -> Id { + let key = id(alias); + p.elements.insert( + key, + Element { + id: key, + alias: alias.into(), + name: if output { + "Report output" + } else { + "Report input" + } + .into(), + description: "Retain the typed report contract.".into(), + parent: Some(owner), + body: ElementBody::shared_in( + Layer::Sa, + SharedBody::Port { + contract: if output { + PortContract::FunctionOutput { + outgoing_items: vec![item], + } + } else { + PortContract::FunctionInput { + incoming_items: vec![item], + } + }, + legacy: None, + }, + ), + }, + ); + key +} + +fn side(p: &mut Project, layer: Layer) -> Side { + let (prefix, kind, root_name, send_name, receive_name, description) = match layer { + Layer::Oa => ( + "oa", + Kind::Activity, + "Coordinate distress response", + "Report distress", + "Assess incident", + "Coordinate responders while preserving operational responsibility.", + ), + Layer::Sa => ( + "sa", + Kind::Function, + "Deliver coordinated response", + "Transmit report", + "Classify incident", + "Technical design: preserve independent processing and report contracts.", + ), + }; + let root = add( + p, + kind, + layer, + &format!("{prefix}_response"), + root_name, + None, + ); + p.elements.get_mut(&root).unwrap().description = description.into(); + let send = add( + p, + kind, + layer, + &format!("{prefix}_send"), + send_name, + Some(root), + ); + let receive = add( + p, + kind, + layer, + &format!("{prefix}_receive"), + receive_name, + Some(root), + ); + p.elements.get_mut(&send).unwrap().description = + "Keep the detailed sending responsibility.".into(); + p.elements.get_mut(&receive).unwrap().description = + "Keep the detailed receiving responsibility.".into(); + let item = add( + p, + Kind::ExchangeItem, + layer, + &format!("{prefix}_report"), + "Incident report", + None, + ); + *p.elements.get_mut(&item).unwrap().shared_mut().unwrap() = SharedBody::ExchangeItem { + kind: ExchangeItemKind::Information, + mechanism: ExchangeMechanism::Flow, + supertype: None, + }; + add( + p, + Kind::DataField, + layer, + &format!("{prefix}_report_text"), + "Report text", + Some(item), + ); + let mut elements = vec![root, send, receive]; + let body = if layer == Layer::Sa { + let output = port(p, "sa_send_output", send, item, true); + let input = port(p, "sa_receive_input", receive, item, false); + elements.extend([output, input]); + RelationBody::FunctionalExchange { + source: send, + target: receive, + source_port: output, + target_port: input, + items: vec![item], + } + } else { + RelationBody::Interaction { + source: send, + target: receive, + items: vec![item], + } + }; + let exchange_alias = format!("{prefix}_report_delivery"); + let exchange = id(&exchange_alias); + p.relations.insert( + exchange, + Relation { + id: exchange, + alias: exchange_alias, + name: "Deliver incident report".into(), + description: "Preserve direction, item identity and endpoint contracts.".into(), + body, + }, + ); + let mut placements = vec![]; + let mut artifacts = vec![ + artifact( + p, + root, + &format!("{prefix}_root_evidence"), + "Response rationale", + ), + artifact( + p, + send, + &format!("{prefix}_send_evidence"), + "Sending requirement", + ), + artifact( + p, + exchange, + &format!("{prefix}_exchange_evidence"), + "Report contract evidence", + ), + ]; + for (suffix, title, projection) in [ + ( + "work", + "Response decomposition", + ProjectionKind::Decomposition, + ), + ("overview", "Response overview", ProjectionKind::Overview), + ] { + let alias = format!("{prefix}_{suffix}"); + let diagram = id(&alias); + let mut nodes = BTreeMap::new(); + for (index, (element, short)) in [(root, "root"), (send, "send"), (receive, "receive")] + .into_iter() + .enumerate() + { + let node_alias = format!("{prefix}_{suffix}_{short}"); + let node_id = id(&node_alias); + nodes.insert( + node_id, + NodePlacement { + id: node_id, + alias: node_alias, + element, + position: match index { + 0 => Position { x: 40., y: 40. }, + 1 => Position { x: 80., y: 120. }, + _ => Position { x: 400., y: 120. }, + }, + size: if index == 0 { + Size { + width: 660., + height: 240., + } + } else { + Size { + width: 224., + height: 94., + } + }, + collapsed: false, + port_anchors: vec![], + }, + ); + placements.push((diagram, node_id)); + if index == 0 { + artifacts.push(artifact( + p, + node_id, + &format!("{prefix}_{suffix}_layout_evidence"), + "Reviewed layout", + )); + } + } + p.diagrams.insert( + diagram, + Diagram { + id: diagram, + alias, + name: format!("{} {title}", layer), + description: "Inspect the response root, its children and their original exchange." + .into(), + layer, + projection, + view_options: Default::default(), + nodes, + routes: vec![EdgeRoute { + edge: exchange, + waypoints: vec![Position { x: 360., y: 167. }], + label: Some(EdgeLabel { + fraction: 0.55, + offset: Position { x: 0., y: -18. }, + }), + }], + }, + ); + } + artifact( + p, + item, + &format!("{prefix}_item_evidence"), + "Shared report definition", + ); + Side { + prefix: prefix.into(), + root, + elements, + exchange, + placements, + artifacts, + } +} + +fn fixture() -> Fixture { + let mut project = Project::empty(); + project.id = id("response_lifecycle"); + project.alias = "response_lifecycle".into(); + project.name = "Distress response: transition lifecycle".into(); + project.description = + "Native acceptance fixture for preserving opposite-layer engineering work.".into(); + add( + &mut project, + Kind::System, + Layer::Sa, + "response_system", + "Response support system", + None, + ); + let oa = side(&mut project, Layer::Oa); + let sa = side(&mut project, Layer::Sa); + let key = id("response_refinement"); + project.traces.insert( + key, + Trace { + id: key, + alias: "response_refinement".into(), + name: "Reviewed response refinement".into(), + description: "Operational and technical prose are intentionally different.".into(), + oa: oa.root, + sa: sa.root, + kind: TraceKind::Refinement, + baseline: Some(TraceBaseline { + oa: project.properties(oa.root).unwrap(), + sa: project.properties(sa.root).unwrap(), + semantic: None, + }), + }, + ); + artifact( + &mut project, + key, + "response_trace_evidence", + "Review decision", + ); + Fixture { project, oa, sa } +} + +// The removal oracle enumerates fixture-owned records. No deletion command, +// impact helper, transition planner or observed post-deletion model is used. +fn deleted(f: &Fixture, side: &Side) -> Project { + let mut p = f.project.clone(); + for key in &side.elements { + assert!(p.elements.remove(key).is_some()); + } + assert!(p.relations.remove(&side.exchange).is_some()); + assert!(p.traces.remove(&id("response_refinement")).is_some()); + for key in side + .artifacts + .iter() + .copied() + .chain([id("response_trace_evidence")]) + { + assert!(p.artifact_links.remove(&key).is_some()); + } + for (diagram, node) in &side.placements { + assert!( + p.diagrams + .get_mut(diagram) + .unwrap() + .nodes + .remove(node) + .is_some() + ); + } + for suffix in ["work", "overview"] { + let diagram = p + .diagrams + .get_mut(&id(&format!("{}_{suffix}", side.prefix))) + .unwrap(); + assert_eq!(diagram.routes.len(), 1); + assert_eq!(diagram.routes[0].edge, side.exchange); + diagram.routes.clear(); + } + p +} + +fn accepted(p: &Project) -> Result> { + // Intentional isolated architecture: no allocations, processes or scenarios. + // Require the exact warning multiset by record UUID, code and field. + let issues = validation::validate(p); + if issues.iter().any(|i| i.severity != Severity::Warning) { + return Err(format!("Structural validation failed: {issues:?}").into()); + } + let mut expected = vec![]; + for e in p.elements.values().filter(|e| { + matches!(e.kind(), Kind::Activity | Kind::Function) + && !p + .elements + .values() + .any(|child| child.parent == Some(e.id) && child.kind() == e.kind()) + }) { + expected.push(("unallocated_leaf", Some(e.id), Some("allocation"))); + expected.push(("work_without_behavior_context", Some(e.id), None)); + } + for r in p.relations.values().filter(|r| { + matches!( + r.kind(), + RelationKind::Interaction | RelationKind::FunctionalExchange + ) + }) { + expected.push(("exchange_without_behavior_context", Some(r.id), None)); + } + let mut actual = issues + .iter() + .map(|i| (i.code.as_str(), i.element, i.field.as_deref())) + .collect::>(); + expected.sort(); + actual.sort(); + if expected != actual { + return Err( + format!("Unexpected warnings: expected {expected:?}; actual {actual:?}").into(), + ); + } + Ok(issues) +} + +fn compile(source: String) -> Result { + let bundle = SourceBundle { + entry: "project.arcadia".into(), + files: BTreeMap::from([("project.arcadia".into(), source)]), + }; + let result = compile_bundle(&bundle, ProjectLimits::default()) + .map_err(|e| format!("Source compile: {e:?}"))?; + Ok(result.decoded.project) +} + +fn round_trip(p: &Project) -> Result { + accepted(p)?; + let bytes = serde_json::to_vec_pretty(p)?; + compare(p, &persistence::decode(&bytes)?)?; + let source = language::format(p); + compare(p, &compile(source.clone())?)?; + Ok(source) +} + +fn recreated(f: &Fixture, side: &Side, new_id: Id) -> Project { + assert!(!f.project.contains(new_id)); + let mut p = deleted(f, side); + let mut root = f.project.elements[&side.root].clone(); + root.id = new_id; + p.elements.insert(new_id, root); + p +} + +fn snippet(f: &Fixture, side: &Side) -> String { + let e = &f.project.elements[&side.root]; + let keyword = if side.prefix == "oa" { + "oa_activity" + } else { + "sa_function" + }; + format!( + " {keyword} {} {} id \"{}\" {{\n description {};\n }}\n", + e.alias, + serde_json::to_string(&e.name).unwrap(), + id(&format!("{}_recreated_response", side.prefix)), + serde_json::to_string(&e.description).unwrap() + ) +} + +fn generate(out: &Path) -> Result<()> { + let f = fixture(); + let mut files: Vec<(String, Vec)> = vec![ + ( + "original.arcadia".into(), + round_trip(&f.project)?.into_bytes(), + ), + ( + "original.json".into(), + serde_json::to_vec_pretty(&f.project)?, + ), + ( + "manifest.json".into(), + serde_json::to_vec_pretty(&serde_json::json!({ + "fixture": "transition-native-lifecycle-20260913", "oa": &f.oa, "sa": &f.sa, + "trace": id("response_refinement"), "trace_artifact": id("response_trace_evidence"), + "recreated_oa_id": id("oa_recreated_response"), "recreated_sa_id": id("sa_recreated_response") + }))?, + ), + ]; + for side in [&f.oa, &f.sa] { + let removal = deleted(&f, side); + let removal_source = round_trip(&removal)?; + let replacement = recreated(&f, side, id(&format!("{}_recreated_response", side.prefix))); + let replacement_source = round_trip(&replacement)?; + let fragment = snippet(&f, side); + let (body, tail) = removal_source + .rsplit_once('}') + .ok_or("Missing project closing brace")?; + compare(&replacement, &compile(format!("{body}{fragment}}}{tail}"))?)?; + files.extend([ + ( + format!("{}-deleted.json", side.prefix), + serde_json::to_vec_pretty(&removal)?, + ), + ( + format!("{}-deleted.arcadia", side.prefix), + removal_source.into_bytes(), + ), + ( + format!("{}-recreated.json", side.prefix), + serde_json::to_vec_pretty(&replacement)?, + ), + ( + format!("{}-recreated.arcadia", side.prefix), + replacement_source.into_bytes(), + ), + ( + format!("{}-recreate-root.snippet", side.prefix), + fragment.into_bytes(), + ), + ]); + } + if files.iter().any(|(name, _)| out.join(name).exists()) { + return Err( + "Generation refuses to overwrite fixture files; use a fresh output directory".into(), + ); + } + fs::create_dir_all(out)?; + for (name, bytes) in files { + fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(out.join(name))? + .write_all(&bytes)?; + } + println!( + "Generated and round-trip verified both directions in {}", + out.display() + ); + Ok(()) +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct PlacementExpectation { + diagram: String, + alias: String, + position: Position, + size: Size, + collapsed: bool, +} + +fn first_difference( + expected: &serde_json::Value, + actual: &serde_json::Value, + path: &str, +) -> Option { + use serde_json::Value; + if expected == actual { + return None; + } + match (expected, actual) { + (Value::Object(a), Value::Object(b)) => { + for key in a.keys().chain(b.keys()) { + match (a.get(key), b.get(key)) { + (Some(x), Some(y)) => { + if let Some(diff) = first_difference(x, y, &format!("{path}.{key}")) { + return Some(diff); + } + } + _ => { + return Some(format!( + "{path}.{key}: expected presence {}, actual presence {}", + a.contains_key(key), + b.contains_key(key) + )); + } + } + } + } + (Value::Array(a), Value::Array(b)) if a.len() == b.len() => { + for (index, (x, y)) in a.iter().zip(b).enumerate() { + if let Some(diff) = first_difference(x, y, &format!("{path}[{index}]")) { + return Some(diff); + } + } + } + _ => return Some(format!("{path}: expected {expected}, actual {actual}")), + } + Some(format!("{path}: values differ")) +} + +fn compare(expected: &Project, actual: &Project) -> Result<()> { + if expected != actual { + return Err(first_difference( + &serde_json::to_value(expected)?, + &serde_json::to_value(actual)?, + "Project", + ) + .unwrap_or_else(|| "Project differs".into()) + .into()); + } + Ok(()) +} + +fn check( + state: &str, + entry: &Path, + placement_path: Option<&Path>, + identity_path: Option<&Path>, + receipt_path: Option<&Path>, +) -> Result<()> { + let f = fixture(); + let identity_expectations: Option> = identity_path + .map(|path| -> Result<_> { Ok(serde_json::from_slice(&fs::read(path)?)?) }) + .transpose()?; + let disk = source_files::load_project(entry, ProjectLimits::default())?; + if disk + .diagnostics + .iter() + .any(|located| located.diagnostic.severity != Severity::Warning) + { + return Err(format!("Source loading errors: {:?}", disk.diagnostics).into()); + } + let disk_bundle = disk.reachable_bundle(); + let compiled = compile_bundle(&disk_bundle, ProjectLimits::default()) + .map_err(|e| format!("Saved source compile: {e:?}"))?; + if compiled.bundle != disk_bundle { + return Err("Saved source is not identity-stable: compilation inserted UUIDs or changed source; save explicit identities before checking".into()); + } + let actual = &compiled.decoded.project; + let validated_warnings = accepted(actual)?; + let mut expected_diagnostics = validated_warnings + .iter() + .map(|issue| { + ( + issue.severity == Severity::Warning, + issue.code.as_str(), + issue.element, + issue.field.as_deref(), + ) + }) + .collect::>(); + expected_diagnostics.sort(); + // Loading/compilation may return the fixture's intentional engineering + // warnings. Admit exactly the independently validated warning multiset, + // retaining owner, field and severity checks; no extra discovery warning or + // error is silently discarded. Source location spelling is not semantic. + for (stage, diagnostics) in [ + ("loading", &disk.diagnostics), + ("compilation", &compiled.diagnostics), + ] { + let mut actual_diagnostics = diagnostics + .iter() + .map(|located| { + let diagnostic = &located.diagnostic; + ( + diagnostic.severity == Severity::Warning, + diagnostic.code.as_str(), + diagnostic.object_id, + diagnostic.field.as_deref(), + ) + }) + .collect::>(); + actual_diagnostics.sort(); + if actual_diagnostics != expected_diagnostics { + return Err(format!("Unexpected source {stage} diagnostics: expected {expected_diagnostics:?}; actual {actual_diagnostics:?}").into()); + } + } + let mut generated_ids = BTreeMap::::new(); + let expected = if state == "original" { + if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() { + return Err("original does not accept placement/identity options".into()); + } + f.project.clone() + } else { + let (prefix, phase) = state + .split_once('-') + .ok_or("State must be original or oa-/sa-deleted/recreated/relinked/reopened/noop")?; + let side = match prefix { + "oa" => &f.oa, + "sa" => &f.sa, + _ => return Err("Unknown source layer".into()), + }; + if phase == "deleted" { + if placement_path.is_some() || identity_path.is_some() || receipt_path.is_some() { + return Err("deleted does not accept placement/identity options".into()); + } + deleted(&f, side) + } else if matches!(phase, "recreated" | "relinked" | "reopened" | "noop") { + if matches!(phase, "reopened" | "noop") + && identity_expectations + .as_ref() + .and_then(|ids| ids.get("trace")) + .is_none() + { + return Err("Reopened/noop checks require --identities with the first accepted relink trace UUID".into()); + } + let original_root = &f.project.elements[&side.root]; + let new_source = match &identity_expectations { + Some(ids) => *ids + .get("source") + .ok_or("Identity expectations must explicitly include source")?, + None => id(&format!("{prefix}_recreated_response")), + }; + let roots = actual + .elements + .values() + .filter(|e| e.alias == original_root.alias) + .collect::>(); + if roots.len() != 1 || roots[0].id != new_source || f.project.contains(new_source) { + return Err( + "Expected exactly one recreated root with the original alias and the pinned new UUID (snippet UUID unless --identities supplied)" + .into(), + ); + } + generated_ids.insert("source".into(), new_source); + let mut candidate = recreated(&f, side, new_source); + if let Some(path) = placement_path { + let specs: Vec = serde_json::from_slice(&fs::read(path)?)?; + for spec in specs { + if ![format!("{prefix}_work"), format!("{prefix}_overview")] + .contains(&spec.diagram) + { + return Err("A new source placement must use its existing source-layer work/overview diagram".into()); + } + let diagram = id(&spec.diagram); + let nodes = actual + .diagrams + .get(&diagram) + .ok_or("Expected diagram is missing")? + .nodes + .values() + .filter(|node| node.alias == spec.alias) + .collect::>(); + if nodes.len() != 1 || f.project.contains(nodes[0].id) { + return Err( + "Expected one newly identified declared source placement".into() + ); + } + let receipt_key = format!("placement:{}", spec.alias); + let key = match &identity_expectations { + Some(ids) => *ids.get(&receipt_key).ok_or( + "Identity receipt lacks an explicitly declared placement UUID", + )?, + None => nodes[0].id, + }; + if nodes[0].id != key { + return Err( + "Source placement UUID changed from its identity expectation".into(), + ); + } + if candidate + .diagrams + .get_mut(&diagram) + .unwrap() + .nodes + .insert( + key, + NodePlacement { + id: key, + alias: spec.alias.clone(), + element: new_source, + position: spec.position, + size: spec.size, + collapsed: spec.collapsed, + port_anchors: vec![], + }, + ) + .is_some() + { + return Err("Duplicate placement expectation".into()); + } + generated_ids.insert(receipt_key, key); + } + } + if phase != "recreated" { + if actual.traces.len() != 1 { + return Err("Relinking must create exactly one trace".into()); + } + let actual_key = actual.traces.values().next().unwrap().id; + let key = identity_expectations + .as_ref() + .and_then(|ids| ids.get("trace")) + .copied() + .unwrap_or(actual_key); + if actual_key != key { + return Err( + "Refinement trace UUID changed from its accepted identity receipt".into(), + ); + } + if f.project.contains(key) || candidate.contains(key) { + return Err("Expected a fresh trace UUID".into()); + } + let (oa, sa) = if prefix == "oa" { + (new_source, f.sa.root) + } else { + (f.oa.root, new_source) + }; + candidate.traces.insert( + key, + Trace { + id: key, + alias: "trace_oa_response_sa_response".into(), + name: format!( + "{} ↔ {}", + candidate.elements[&oa].name, candidate.elements[&sa].name + ), + description: String::new(), + oa, + sa, + kind: TraceKind::Refinement, + baseline: Some(TraceBaseline { + oa: candidate.properties(oa).unwrap(), + sa: candidate.properties(sa).unwrap(), + semantic: None, + }), + }, + ); + generated_ids.insert("trace".into(), key); + } + candidate + } else { + return Err("Unknown lifecycle phase".into()); + } + }; + // No actual body, prose, ownership, geometry, alias or baseline is copied + // into the expected model: only explicitly permitted fresh identities. + compare(&expected, actual)?; + round_trip(&expected)?; + if let Some(ids) = &identity_expectations { + for (label, expected_id) in ids { + if generated_ids.get(label) != Some(expected_id) { + return Err(format!("Unused or mismatching identity expectation: {label}").into()); + } + } + } + if let Some(path) = receipt_path { + // Publish a receipt only after complete-model validation succeeds. Never + // replace earlier accepted identity evidence with a new observation. + fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)? + .write_all(&serde_json::to_vec_pretty(&generated_ids)?)?; + } + println!( + "{}", + serde_json::to_string_pretty(&serde_json::json!({ + "status": "PASS", "state": state, "entry": entry, + "comparison": "complete Project equality", "new_ids": generated_ids, + "trace_identity_pinned": identity_expectations.as_ref().is_some_and(|ids| ids.contains_key("trace")), + "identity_receipt": receipt_path, + "recreated_id_matches_snippet": generated_ids.get("source").map(|key| + *key == id(if state.starts_with("oa-") { "oa_recreated_response" } else { "sa_recreated_response" })) + }))? + ); + Ok(()) +} + +fn run() -> Result<()> { + let args = std::env::args().skip(1).collect::>(); + match args.as_slice() { + [command, out] if command == "generate" => generate(Path::new(out)), + [command, state, entry, options @ ..] if command == "check" => { + if options.len() % 2 != 0 { return Err("Every check option requires a path".into()); } + let (mut placements, mut identities, mut receipt) = (None, None, None); + for pair in options.chunks_exact(2) { + let slot = match pair[0].as_str() { + "--placements" => &mut placements, + "--identities" => &mut identities, + "--write-identities" => &mut receipt, + _ => return Err(format!("Unknown check option: {}", pair[0]).into()), + }; + if slot.replace(Path::new(&pair[1])).is_some() { return Err("Duplicate check option".into()); } + } + check(state, Path::new(entry), placements, identities, receipt) + }, + _ => Err("Usage: transition-native-lifecycle generate OUTDIR | check STATE SAVED_ENTRY [--placements PATH] [--identities PATH] [--write-identities PATH]".into()), + } +} + +fn main() { + if let Err(error) = run() { + eprintln!("FAIL: {error}"); + std::process::exit(1); + } +}