Keep transition decisions visible and add native lifecycle oracle

This commit is contained in:
2026-09-22 15:06:46 -07:00
parent d0b7c28501
commit 05e4c83054
6 changed files with 1576 additions and 17 deletions
+53 -17
View File
@@ -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()
@@ -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"
@@ -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"
@@ -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.
@@ -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
}
]
@@ -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<T> = std::result::Result<T, Box<dyn std::error::Error>>;
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<Id>,
exchange: Id,
placements: Vec<(Id, Id)>,
artifacts: Vec<Id>,
}
struct Fixture {
project: Project,
oa: Side,
sa: Side,
}
fn add(
p: &mut Project,
kind: Kind,
layer: Layer,
alias: &str,
name: &str,
parent: Option<Id>,
) -> 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<Vec<validation::Issue>> {
// 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::<Vec<_>>();
expected.sort();
actual.sort();
if expected != actual {
return Err(
format!("Unexpected warnings: expected {expected:?}; actual {actual:?}").into(),
);
}
Ok(issues)
}
fn compile(source: String) -> Result<Project> {
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<String> {
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<u8>)> = 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<String> {
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<BTreeMap<String, Id>> = 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::<Vec<_>>();
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::<Vec<_>>();
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::<String, Id>::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::<Vec<_>>();
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<PlacementExpectation> = 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::<Vec<_>>();
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::<Vec<_>>();
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);
}
}