Keep transition decisions visible and add native lifecycle oracle
This commit is contained in:
@@ -1407,16 +1407,6 @@ impl Workflow {
|
|||||||
.color(style::MUTED),
|
.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 {
|
let enabled = match self {
|
||||||
@@ -1433,12 +1423,50 @@ impl Workflow {
|
|||||||
.color(iced::Color::from_rgb8(178, 54, 58)),
|
.color(iced::Color::from_rgb8(178, 54, 58)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
column![
|
let mut footer = column![].spacing(8);
|
||||||
scrollable(content.padding(iced::Padding {
|
if let Self::Transition(d) = self {
|
||||||
right: 14.,
|
let boundary_choices = d
|
||||||
..Default::default()
|
.plan
|
||||||
}))
|
.conflicts
|
||||||
.height(480),
|
.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![
|
row![
|
||||||
Space::new().width(Fill),
|
Space::new().width(Fill),
|
||||||
button(text("Cancel").size(12))
|
button(text("Cancel").size(12))
|
||||||
@@ -1458,7 +1486,15 @@ impl Workflow {
|
|||||||
.padding(10)
|
.padding(10)
|
||||||
.on_press_maybe(enabled.then_some(Message::Workflow(Event::Apply)))
|
.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)
|
.spacing(18)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
+434
@@ -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.
|
||||||
+9
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user