From b2ab2b1feb9b0ada03cd9d0974749bd7fd28c7f8 Mon Sep 17 00:00:00 2001 From: "sorokin.oleg.edu" Date: Sun, 6 Sep 2026 04:19:13 +0300 Subject: [PATCH] Prove R3 self-tagged facet reviews against carrier baseline --- experiments/r3-facet-carrier/C-REPORT.md | 106 ++ experiments/r3-facet-carrier/README.md | 2 + experiments/r3-facet-carrier/measure-code.ps1 | 28 + experiments/r3-facet-carrier/src/lib.rs | 19 +- .../r3-facet-carrier/src/typed_reviews.rs | 909 ++++++++++++++ .../src/typed_reviews_tests.rs | 1046 +++++++++++++++++ 6 files changed, 2104 insertions(+), 6 deletions(-) create mode 100644 experiments/r3-facet-carrier/C-REPORT.md create mode 100644 experiments/r3-facet-carrier/measure-code.ps1 create mode 100644 experiments/r3-facet-carrier/src/typed_reviews.rs create mode 100644 experiments/r3-facet-carrier/src/typed_reviews_tests.rs diff --git a/experiments/r3-facet-carrier/C-REPORT.md b/experiments/r3-facet-carrier/C-REPORT.md new file mode 100644 index 00000000..c6ecb941 --- /dev/null +++ b/experiments/r3-facet-carrier/C-REPORT.md @@ -0,0 +1,106 @@ +# C: finite typed facet reviews — executable comparison + +Status: isolated proof, pending root freeze. Production remains at `959b83c78fcaa82b0184aade81a0508b47eb1e41`; B1 evidence is commit `8ac9f464eaf0ec813ee3ecf903404891ae2a3841`. No R6/schema/source contract changed. + +## Result and proposed shape + +**11 tests pass: the original 3 B1 tests plus 8 C tests.** C stores reviewed facts directly, without an own-record carrier, reviewed mask, order shadow, observation version or recursive value tree. `OwnRecord` is reused only for transient source/target candidates and existing fixture helpers. The exact types/adapter are in `src/typed_reviews.rs`; assertions are in `src/typed_reviews_tests.rs`. + +```rust +enum Observed { NotApplicable, Value(T) } +struct Reviewed { oa: Observed, sa: Observed } +struct KeyPair { oa: Option, sa: Option } +struct ValueKey { path: Vec, field: Id } +struct Baseline { facets: Vec } +``` + +`FacetReview` is self-tagged: Data family leaves carry `Reviewed>`, `Reviewed>`, `Reviewed>`, etc.; the tag identifies the facet. Other proof leaves are field minimum, state guard, mapped trigger/step membership, step order, atomic keyed annotations and atomic keyed bindings. Existing `MessageValue`, `LifelineBinding`, `DataValue` and `DataTypeDefinition` are reused. `T` is a concrete closed type in each variant, not an arbitrary recursively nested value. + +An absent entry means Unreviewed. `Value(None)` means an applicable optional slot has no value (for example an unbounded Real minimum). `NotApplicable` means a selected constructor branch does not exist on that side. A mixed review keeping an SA Integer while acknowledging an OA Real records Real-minimum OA=`Value(None)`, SA=`NotApplicable`; Integer-maximum records the reverse applicability. A both-side-inapplicable leaf is rejected. + +Recommendation remains **C for the full contract**, because facts are explicit and independent. This proof does **not** show fewer lines of implementation: see the measured cost below. B1 remains viable at restricted granularity; C removes its storage-overlap invariants rather than eliminating exhaustive adapter work. + +## What the executable proof establishes + +| Case | Observed assertions | +| --- | --- | +| Bounds/unit and field minimum | Keep acknowledges only selected facts; unselected maximum still drifts. Take maximum retains target unit/minimum. Field minimum retains target maximum. Repeat application is unchanged. | +| Guard/triggers after deletion | Deleted source UUID remains in a typed historical key. Removal unlinks the mapped trigger but leaves its target object; extra target trigger and local guard survive. | +| Ordered steps | Accepting membership leaves the order fact byte-for-byte unchanged. Explicit final order must contain all surviving target members exactly once. Target-only step object survives unchanged. | +| Qualified annotation rows | Two paths with the same terminal latitude UUID remain separate. Updating/removing the first row does not acknowledge or overwrite the second row's expression. Removal acknowledges the whole selected row. | +| Binding rows | Key is the referenced lifeline; changing `local` updates that row through frozen mapping. Other binding remains unchanged. Missing local mapping rejects. | +| Mixed constructor group | Keep can acknowledge Real/Integer as intentionally different constructors, including correct applicable/not-applicable leaves. Take changes definition+default while preserving unit. A later explicit target switch can reconsider a previous Keep. Retained Record child blocks an incompatible switch atomically. | +| Prepared induced changes | A real inherited-field removal causes core `prepare_command` to prune annotations. C compares the actual prepared project with the reviewed expected project and rejects these unreviewed changes. Project and baseline inputs remain unchanged. | +| Adversarial admission | Exact duplicates, keys colliding on either side, mismatched frozen key mapping, changed pairing against old baseline, wrong-family/both-side-NA Keep, unchanged-constructor acknowledgement, invalid historical row/key combinations, empty key pairs and nonfinite values reject. Collision permutations both reject. Returned baseline is revalidated. | + +Constructor grouping copies the definition and default in this subset; accepting a switch retires old definition facts and records the union of both resulting constructor branches. Unit stays independent. An unchanged constructor cannot be used as a shortcut to acknowledge changed bounds/defaults. Constructor and descendant decisions in one request reject as overlapping. Membership operations precede order regardless of request order. + +Pair remapping is deliberately conservative: a new KeyPair colliding with an old baseline pair rejects with an explicit review-invalidation requirement. This probe does not implement the subsequent mapping-reset workflow or agreement-based many-to-one merges. It cannot silently accumulate a baseline that fails on its next application. + +## Measured implementation cost + +`measure-code.ps1` counts nonblank, non-comment rustfmt lines. B1 is read from its committed version, not a moving file. These are code-size measurements, not a normalized complexity or speed benchmark. + +| Portion | B1 | C | Difference | +| --- | ---: | ---: | ---: | +| Public types/requests | 69 | 118 | +49 | +| Adapter/admission/application | 256 | 778 | +522 | +| Tests | 512 | 1,035 | +523 | + +Both use the same 52 lines of basic own-record/list helpers; C does not call B1's carrier copy/compare/apply logic. C covers 18 selector cases versus B1's 9, including both keyed row kinds and all DataType constructor branches. C also adds admission and prepared-project checks absent from B1. Therefore attributing the entire difference to representation would be misleading. C's repeated capture/tag/write matches are real maintenance cost; one finite catalog should supply selector/type metadata during implementation, without introducing a general metamodel framework. + +## Full coverage and minimal granularity recommendation + +| Family | Facets required by the current model | +| --- | --- | +| Headers | Name, description, element parent. No incidental identity/alias/layer edits. | +| OA/SA primitive bodies | No payload; Process/FunctionalChain step membership and order. | +| ExchangeItem / Interface | Nature, mechanism, supertype; interface supertype. Allocations remain real relation records. | +| DataType | Constructor, integer/real bounds, string lengths, enum literal membership, Record supertype, default, unit. | +| DataField | Whole TypeRef, multiplicity bounds, unit, direction, storage, whole optional default vector. No unstable per-index default selection. | +| Port | Contract constructor; input/output item membership; component kind/orientation; legacy direction/interface. Existing typed/Legacy resolution and split semantics remain authoritative. | +| Scenario / Lifeline | Viewpoint, context, pre/postconditions, step membership/order; represented reference. | +| Occurrence envelope | Constructor and separate common pre/postconditions. | +| Exchange occurrence | Exchange, sender, receiver, message kind/Timer expression, reply correlation, annotations keyed by full `(path,field)`. | +| Remaining occurrences | Action/action-lifeline; control kind; sequence reference; scenario reference/bindings; state/state-lifeline; fragment operator/branches; branch guard/ordered occurrences. | +| Event / State | Event constructor and Signal items/Time kind+expression/Change expression; StateKind structural change and entry/exit/do prose. Machine/Region have no own payload. | +| Every relation | Source and target endpoints, plus name/description. | +| Relation payloads | Interface allocation order/protocols; association roles/multiplicities/navigation; exchange items/ports/orientation; capability-extend condition; sequence guard; transition kind/trigger+effect sets and three prose fields; StateAction phase. All other relation kinds are endpoint-only. | +| External membership/artifacts | Direct children; incident relation identity+endpoint role; attached artifact membership and name/description/owner/relation/URI/revision/fragment. | + +Atomic annotation and binding rows are the smallest useful contract: key changes are explicit remove/add, while all other rows remain independent. A separate expression-versus-presence checkpoint is unnecessary at this granularity. If such controls are later required, C can add separate typed facts without a second carrier state. + +Steps/branch occurrences are ordered. Trigger alternatives, bindings and annotations must not accidentally acquire execution ordering semantics from vector position. Enum literals need string-keyed membership to preserve target-only literals; standalone whole-list `TakeSource` is explicitly rejected by this probe, pending that finite selector. A constructor conversion that would overwrite retained target enum literals also requires an explicit candidate. Field default vectors remain atomic. + +Constructor scopes own discriminator and introduced/removed variant fields. Dependent changes outside that scope require explicit resolutions. In particular, changing Occurrence's constructor must not acknowledge its outer pre/postconditions; Call→Timer must expose exchange/reply/value effects; StateKind is semantic even though it changes `Element::kind()`. Prepared induced changes must be reviewed for every representation. + +Current Trace endpoints/properties do not accept artifacts. Prefer attached artifact review facts owned by the semantic trace instead of silently widening trace compatibility. Missing artifact versus present artifact with `revision=None` needs distinct typed observations. + +## Reuse the existing source decoder, with typed context + +Baseline JSON roundtrips, including deleted historical UUIDs and mixed constructor observations. All accepted live candidates roundtrip through current Arcadia 4 source. **C baseline source syntax is not implemented or proved.** + +The concrete reuse route is: + +1. `language.rs::Parser::value_inner`: recognize a distinct historical `uuid "..."` AST literal. Do not convert it to an ordinary quoted string too early. +2. `Ast::resolve`: accept an explicit slot context supplied by the finite facet catalog. Historical reference slots accept UUID literals; live reference slots keep `@alias` requirements. A new context must be entered only through the semantic-baseline subtree of a Trace declaration. +3. Keep `resolve_declarations` and its existing `serde_json::from_value::`/body decoding as the typed authority. Do not duplicate the live body factory/default merge logic for historical facts. +4. `write_fields`/`write_value`: use the same slot metadata to emit historical UUIDs without current alias lookup. Generic `Reviewed{oa,sa}` cannot pass through current field-name-only rules: `oa`/`sa` are currently classified as references even when T is a unit string. Renaming keys alone does not distinguish reference/string/enum types. +5. `edit_positions`/`EditKind` and `source_project.rs::item_key`: retain full UUID token spans, historical identity remapping, and full annotation/binding keys for comment ownership. +6. Extend finite historical validation/admission counts and explicit remap visitors; exclude baseline IDs from live dangling-reference and deletion traversal. Historical carrier/facet validation must not fetch old objects or impose cross-facet live range invariants. + +This needs explicit schema/source migration and OpenAPI descriptions before acceptance. No source4 or data4 changes were made here. + +## Commands, evidence, and stopping point + +From `D:/CodexBuilds/Capella2-transition-probe`: + +```powershell +cargo test --manifest-path experiments/r3-facet-carrier/Cargo.toml --locked --offline --config target/r3-probe/cargo.toml --target-dir D:/CodexBuilds/Capella2-transition-probe-target -- --nocapture 2>&1 | Tee-Object -FilePath target/r3-probe/c-tests.log; exit $LASTEXITCODE +``` + +Exit 0: **11 passed, 0 failed**, no compiler warnings. The vendor config is unchanged from the B1 report. `& experiments/r3-facet-carrier/measure-code.ps1` reproduces counts. Logs, `code-counts.json` and generated `evidence/c-*-baseline.json` / `c-*-candidate.arcadia` remain under `target/r3-probe/`. + +Still outside this proof: complete facet catalog; reverse direction; multiple target records; historical identity remapping; mapping-reset and agreeing many-to-one resolution; unambiguous automatic ordering; source baseline parser/formatter; OpenAPI/migration; size budgets for every new fact; and production baseline+undo/revision/ETag/disk atomicity. String literal/row keys and historical UUID admission need full adversarial coverage at implementation time. No claim of full R3 implementation or acceptance is made. + +Stop here for the root's freeze decision. No native UI or other checkout/target was used. diff --git a/experiments/r3-facet-carrier/README.md b/experiments/r3-facet-carrier/README.md index 63d31aed..3d47882b 100644 --- a/experiments/r3-facet-carrier/README.md +++ b/experiments/r3-facet-carrier/README.md @@ -2,6 +2,8 @@ Status: isolated design/probe, not R3 implementation or an accepted persistence contract. +Follow-up: [C-REPORT.md](C-REPORT.md) contains the executable self-tagged typed-facet comparison, current recommendation, additional adversarial cases and measured code costs. This document preserves the earlier B0/B1 analysis. + Base: `959b83c78fcaa82b0184aade81a0508b47eb1e41`. Worktree: `D:/CodexBuilds/Capella2-transition-probe`, branch `codex/transition-probe`. The standalone crate depends on the unchanged local `oa-core`. It changes no accepted data4 shape or production code. ## Finding diff --git a/experiments/r3-facet-carrier/measure-code.ps1 b/experiments/r3-facet-carrier/measure-code.ps1 new file mode 100644 index 00000000..f5ab8083 --- /dev/null +++ b/experiments/r3-facet-carrier/measure-code.ps1 @@ -0,0 +1,28 @@ +# Run from the isolated worktree root. Counts rustfmt lines, not complexity. +$r3B = @(git show 8ac9f46:experiments/r3-facet-carrier/src/lib.rs) +$r3C = @(Get-Content -LiteralPath experiments/r3-facet-carrier/src/typed_reviews.rs) +$r3Tests = @(Get-Content -LiteralPath experiments/r3-facet-carrier/src/typed_reviews_tests.rs) +function Count-R3Lines($Lines) { + @($Lines | Where-Object { $_.Trim().Length -gt 0 -and -not $_.Trim().StartsWith('//') }).Count +} +function Find-R3Line($Lines, $Prefix) { + for ($r3Index = 0; $r3Index -lt $Lines.Length; $r3Index++) { + if ($Lines[$r3Index].StartsWith($Prefix)) { return $r3Index } + } + throw "Missing section marker: $Prefix" +} +$r3BOwn = Find-R3Line $r3B 'pub fn own(' +$r3BCopy = Find-R3Line $r3B 'fn copy(' +$r3BTests = Find-R3Line $r3B '#[cfg(test)]' +$r3CAdapter = Find-R3Line $r3C '// ADAPTER:' +$r3CTestStart = Find-R3Line $r3C '#[cfg(test)]' +[ordered]@{ + method = 'Nonblank non-comment rustfmt lines; excludes test module registration' + b_public = Count-R3Lines $r3B[5..($r3BOwn-1)] + shared_record_helpers = Count-R3Lines $r3B[$r3BOwn..($r3BCopy-1)] + b_adapter = Count-R3Lines $r3B[$r3BCopy..($r3BTests-1)] + b_tests = Count-R3Lines $r3B[($r3BTests+1)..($r3B.Length-1)] + c_public = Count-R3Lines $r3C[5..($r3CAdapter-1)] + c_adapter = Count-R3Lines $r3C[$r3CAdapter..($r3CTestStart-1)] + c_tests = Count-R3Lines $r3Tests +} | ConvertTo-Json diff --git a/experiments/r3-facet-carrier/src/lib.rs b/experiments/r3-facet-carrier/src/lib.rs index 8af163bc..2d52c208 100644 --- a/experiments/r3-facet-carrier/src/lib.rs +++ b/experiments/r3-facet-carrier/src/lib.rs @@ -2,6 +2,7 @@ //! A carrier stores independently reviewed facets; it is NOT a past valid model. use oa_core::*; use serde::{Deserialize, Serialize}; +pub mod typed_reviews; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(deny_unknown_fields)] @@ -403,13 +404,19 @@ pub fn apply( #[cfg(test)] mod tests { use super::*; - fn add(p: &mut Project, kind: Kind, layer: Layer, name: &str, parent: Option) -> Id { + pub(crate) fn add( + p: &mut Project, + kind: Kind, + layer: Layer, + name: &str, + parent: Option, + ) -> Id { p.add_element(kind, layer, name, parent).unwrap() } - fn set(p: &mut Project, id: Id, body: SharedBody) { + pub(crate) fn set(p: &mut Project, id: Id, body: SharedBody) { *p.elements.get_mut(&id).unwrap().shared_mut().unwrap() = body; } - fn valid(p: &Project) { + pub(crate) fn valid(p: &Project) { let errors: Vec<_> = validation::validate(p) .into_iter() .filter(|i| i.severity == validation::Severity::Error) @@ -463,7 +470,7 @@ mod tests { ) .unwrap(); } - fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody { + pub(crate) fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody { SharedBody::DataType { definition: DataTypeDefinition::Integer { minimum: Some(minimum), @@ -650,7 +657,7 @@ mod tests { ); } - fn state_side(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) { + pub(crate) fn state_side(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) { let owner = add( p, if layer == Layer::Oa { @@ -773,7 +780,7 @@ mod tests { ); } - fn sequence(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) { + pub(crate) fn sequence(p: &mut Project, layer: Layer, prefix: &str) -> (Id, Id, Id) { let sequence = add( p, if layer == Layer::Oa { diff --git a/experiments/r3-facet-carrier/src/typed_reviews.rs b/experiments/r3-facet-carrier/src/typed_reviews.rs new file mode 100644 index 00000000..d442f45b --- /dev/null +++ b/experiments/r3-facet-carrier/src/typed_reviews.rs @@ -0,0 +1,909 @@ +//! C: closed typed reviewed facts. Whole records are transient candidates only. +use crate::{Decision, OwnRecord, list, list_mut, own, shared, shared_mut}; +use oa_core::*; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +// PUBLIC TYPES: persisted facts have no carrier, review mask or shadow order. +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Observed { + NotApplicable, + Value(T), +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Reviewed { + pub oa: Observed, + pub sa: Observed, +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct KeyPair { + pub oa: Option, + pub sa: Option, +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct ValueKey { + pub path: Vec, + pub field: Id, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DataConstructor { + Boolean, + Integer, + Real, + String, + Enumeration, + Record, +} +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum DataFacet { + Constructor, + IntegerMinimum, + IntegerMaximum, + RealMinimum, + RealMaximum, + MinimumLength, + MaximumLength, + Literals, + Supertype, + Default, + Unit, +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum DataReview { + Constructor(Reviewed), + IntegerMinimum(Reviewed>), + IntegerMaximum(Reviewed>), + RealMinimum(Reviewed>), + RealMaximum(Reviewed>), + MinimumLength(Reviewed), + MaximumLength(Reviewed>), + Literals(Reviewed>), + Supertype(Reviewed>), + Default(Reviewed>), + Unit(Reviewed), +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum Selector { + Data(DataFacet), + FieldMinimum, + StateGuard, + Trigger(KeyPair), + Step(KeyPair), + StepOrder, + Annotation(KeyPair), + Binding(KeyPair), +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub enum FacetReview { + Data(DataReview), + FieldMinimum(Reviewed), + StateGuard(Reviewed), + Trigger { + keys: KeyPair, + values: Reviewed, + }, + Step { + keys: KeyPair, + values: Reviewed, + }, + StepOrder(Reviewed>), + Annotation { + keys: KeyPair, + values: Reviewed>, + }, + Binding { + keys: KeyPair, + values: Reviewed>, + }, +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)] +#[serde(deny_unknown_fields)] +pub struct Baseline { + pub facets: Vec, +} +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Resolution { + pub facet: Selector, + pub decision: Decision, +} +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct Review { + pub source: Id, + pub target: Id, + pub candidate: Option, + pub resolutions: Vec, + pub references: BTreeMap, +} + +// ADAPTER: exhaustive typed capture, comparison, and application for this subset. +fn observed(v: Option) -> Observed { + v.map_or(Observed::NotApplicable, Observed::Value) +} +fn pair(a: &OwnRecord, b: &OwnRecord, f: impl Fn(&OwnRecord) -> Option) -> Reviewed { + Reviewed { + oa: observed(f(a)), + sa: observed(f(b)), + } +} +fn data(r: &OwnRecord) -> Option<(&DataTypeDefinition, &Option, &String)> { + match shared(r).ok()? { + SharedBody::DataType { + definition, + default, + unit, + } => Some((definition, default, unit)), + _ => None, + } +} +fn constructor(d: &DataTypeDefinition) -> DataConstructor { + match d { + DataTypeDefinition::Boolean => DataConstructor::Boolean, + DataTypeDefinition::Integer { .. } => DataConstructor::Integer, + DataTypeDefinition::Real { .. } => DataConstructor::Real, + DataTypeDefinition::String { .. } => DataConstructor::String, + DataTypeDefinition::Enumeration { .. } => DataConstructor::Enumeration, + DataTypeDefinition::Record { .. } => DataConstructor::Record, + } +} +fn annotation(r: &OwnRecord, key: &Option) -> Option> { + let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { values, .. }, + .. + } = shared(r).ok()? + else { + return None; + }; + Some(key.as_ref().and_then(|key| { + values + .iter() + .find(|v| v.path == key.path && v.field == key.field) + .cloned() + })) +} +fn binding(r: &OwnRecord, key: Option) -> Option> { + let SharedBody::Occurrence { + body: OccurrenceBody::ScenarioUse { bindings, .. }, + .. + } = shared(r).ok()? + else { + return None; + }; + Some(key.and_then(|key| bindings.iter().find(|v| v.referenced == key).cloned())) +} +impl DataReview { + fn facet(&self) -> DataFacet { + match self { + Self::Constructor(_) => DataFacet::Constructor, + Self::IntegerMinimum(_) => DataFacet::IntegerMinimum, + Self::IntegerMaximum(_) => DataFacet::IntegerMaximum, + Self::RealMinimum(_) => DataFacet::RealMinimum, + Self::RealMaximum(_) => DataFacet::RealMaximum, + Self::MinimumLength(_) => DataFacet::MinimumLength, + Self::MaximumLength(_) => DataFacet::MaximumLength, + Self::Literals(_) => DataFacet::Literals, + Self::Supertype(_) => DataFacet::Supertype, + Self::Default(_) => DataFacet::Default, + Self::Unit(_) => DataFacet::Unit, + } + } +} +impl FacetReview { + pub fn selector(&self) -> Selector { + match self { + Self::Data(v) => Selector::Data(v.facet()), + Self::FieldMinimum(_) => Selector::FieldMinimum, + Self::StateGuard(_) => Selector::StateGuard, + Self::Trigger { keys, .. } => Selector::Trigger(keys.clone()), + Self::Step { keys, .. } => Selector::Step(keys.clone()), + Self::StepOrder(_) => Selector::StepOrder, + Self::Annotation { keys, .. } => Selector::Annotation(keys.clone()), + Self::Binding { keys, .. } => Selector::Binding(keys.clone()), + } + } +} +pub fn capture(a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> FacetReview { + match selector { + Selector::Data(f) => FacetReview::Data(match f { + DataFacet::Constructor => { + DataReview::Constructor(pair(a, b, |r| Some(constructor(data(r)?.0)))) + } + DataFacet::IntegerMinimum => DataReview::IntegerMinimum(pair(a, b, |r| { + if let DataTypeDefinition::Integer { minimum, .. } = data(r)?.0 { + Some(*minimum) + } else { + None + } + })), + DataFacet::IntegerMaximum => DataReview::IntegerMaximum(pair(a, b, |r| { + if let DataTypeDefinition::Integer { maximum, .. } = data(r)?.0 { + Some(*maximum) + } else { + None + } + })), + DataFacet::RealMinimum => DataReview::RealMinimum(pair(a, b, |r| { + if let DataTypeDefinition::Real { minimum, .. } = data(r)?.0 { + Some(*minimum) + } else { + None + } + })), + DataFacet::RealMaximum => DataReview::RealMaximum(pair(a, b, |r| { + if let DataTypeDefinition::Real { maximum, .. } = data(r)?.0 { + Some(*maximum) + } else { + None + } + })), + DataFacet::MinimumLength => DataReview::MinimumLength(pair(a, b, |r| { + if let DataTypeDefinition::String { minimum_length, .. } = data(r)?.0 { + Some(*minimum_length) + } else { + None + } + })), + DataFacet::MaximumLength => DataReview::MaximumLength(pair(a, b, |r| { + if let DataTypeDefinition::String { maximum_length, .. } = data(r)?.0 { + Some(*maximum_length) + } else { + None + } + })), + DataFacet::Literals => DataReview::Literals(pair(a, b, |r| { + if let DataTypeDefinition::Enumeration { literals } = data(r)?.0 { + Some(literals.clone()) + } else { + None + } + })), + DataFacet::Supertype => DataReview::Supertype(pair(a, b, |r| { + if let DataTypeDefinition::Record { supertype } = data(r)?.0 { + Some(*supertype) + } else { + None + } + })), + DataFacet::Default => DataReview::Default(pair(a, b, |r| Some(data(r)?.1.clone()))), + DataFacet::Unit => DataReview::Unit(pair(a, b, |r| Some(data(r)?.2.clone()))), + }), + Selector::FieldMinimum => FacetReview::FieldMinimum(pair(a, b, |r| { + if let SharedBody::DataField { minimum, .. } = shared(r).ok()? { + Some(*minimum) + } else { + None + } + })), + Selector::StateGuard => FacetReview::StateGuard(pair(a, b, |r| { + if let OwnRecord::Relation { + body: RelationBody::StateTransition { guard, .. }, + } = r + { + Some(guard.clone()) + } else { + None + } + })), + Selector::Trigger(keys) | Selector::Step(keys) => { + let triggers = matches!(selector, Selector::Trigger(_)); + let values = Reviewed { + oa: observed( + list(a, triggers) + .ok() + .map(|v| keys.oa.is_some_and(|id| v.contains(&id))), + ), + sa: observed( + list(b, triggers) + .ok() + .map(|v| keys.sa.is_some_and(|id| v.contains(&id))), + ), + }; + if triggers { + FacetReview::Trigger { + keys: keys.clone(), + values, + } + } else { + FacetReview::Step { + keys: keys.clone(), + values, + } + } + } + Selector::StepOrder => FacetReview::StepOrder(pair(a, b, |r| list(r, false).ok().cloned())), + Selector::Annotation(keys) => FacetReview::Annotation { + keys: keys.clone(), + values: Reviewed { + oa: observed(annotation(a, &keys.oa)), + sa: observed(annotation(b, &keys.sa)), + }, + }, + Selector::Binding(keys) => FacetReview::Binding { + keys: keys.clone(), + values: Reviewed { + oa: observed(binding(a, keys.oa)), + sa: observed(binding(b, keys.sa)), + }, + }, + } +} +impl Baseline { + pub fn capture( + p: &Project, + source: Id, + target: Id, + selectors: &[Selector], + ) -> Result { + unique(selectors)?; + let result = Self { + facets: selectors + .iter() + .map(|f| capture(&own(p, source), &own(p, target), f)) + .collect(), + }; + result.validate()?; + Ok(result) + } + pub fn changed(&self, a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> Option { + self.facets + .iter() + .find(|v| v.selector() == *selector) + .map(|v| *v != capture(a, b, selector)) + } + fn upsert(&mut self, review: FacetReview) { + self.facets.retain(|v| v.selector() != review.selector()); + self.facets.push(review); + } + fn validate(&self) -> Result<(), String> { + unique( + &self + .facets + .iter() + .map(FacetReview::selector) + .collect::>(), + )?; + for fact in &self.facets { + if !has_applicable_value(fact) { + return Err("Facet is inapplicable on both sides".into()); + } + match fact { + FacetReview::Annotation { keys, values } => { + if keys.oa.is_none() && keys.sa.is_none() { + return Err("Empty keyed facet".into()); + } + for (key, value) in [(&keys.oa, &values.oa), (&keys.sa, &values.sa)] { + if let Observed::Value(Some(value)) = value + && key.as_ref() + != Some(&ValueKey { + path: value.path.clone(), + field: value.field, + }) + { + return Err("Historical annotation payload disagrees with key".into()); + } + } + } + FacetReview::Binding { keys, values } => { + if keys.oa.is_none() && keys.sa.is_none() { + return Err("Empty keyed facet".into()); + } + for (key, value) in [(&keys.oa, &values.oa), (&keys.sa, &values.sa)] { + if let Observed::Value(Some(value)) = value + && *key != Some(value.referenced) + { + return Err("Historical binding payload disagrees with key".into()); + } + } + } + FacetReview::Trigger { keys, .. } | FacetReview::Step { keys, .. } + if keys.oa.is_none() && keys.sa.is_none() => + { + return Err("Empty keyed facet".into()); + } + FacetReview::Data(DataReview::RealMinimum(v) | DataReview::RealMaximum(v)) => { + for v in [&v.oa, &v.sa] { + if let Observed::Value(Some(v)) = v + && !v.is_finite() + { + return Err("Historical real must be finite".into()); + } + } + } + FacetReview::Data(DataReview::Default(v)) => { + for v in [&v.oa, &v.sa] { + if let Observed::Value(Some(DataValue::Real(v))) = v + && !v.is_finite() + { + return Err("Historical real default must be finite".into()); + } + } + } + _ => {} + } + } + Ok(()) + } +} +fn has_applicable_value(fact: &FacetReview) -> bool { + fn any(v: &Reviewed) -> bool { + matches!(v.oa, Observed::Value(_)) || matches!(v.sa, Observed::Value(_)) + } + match fact { + FacetReview::Data(v) => match v { + DataReview::Constructor(v) => any(v), + DataReview::IntegerMinimum(v) | DataReview::IntegerMaximum(v) => any(v), + DataReview::RealMinimum(v) | DataReview::RealMaximum(v) => any(v), + DataReview::MinimumLength(v) => any(v), + DataReview::MaximumLength(v) => any(v), + DataReview::Literals(v) => any(v), + DataReview::Supertype(v) => any(v), + DataReview::Default(v) => any(v), + DataReview::Unit(v) => any(v), + }, + FacetReview::FieldMinimum(v) => any(v), + FacetReview::StateGuard(v) => any(v), + FacetReview::Trigger { values, .. } | FacetReview::Step { values, .. } => any(values), + FacetReview::StepOrder(v) => any(v), + FacetReview::Annotation { values, .. } => any(values), + FacetReview::Binding { values, .. } => any(values), + } +} +fn unique(selectors: &[Selector]) -> Result<(), String> { + fn overlap(a: &KeyPair, b: &KeyPair) -> bool { + a.oa.as_ref().is_some_and(|k| b.oa.as_ref() == Some(k)) + || a.sa.as_ref().is_some_and(|k| b.sa.as_ref() == Some(k)) + } + for (index, selector) in selectors.iter().enumerate() { + if selectors[..index].iter().any(|old| { + old == selector + || match (old, selector) { + (Selector::Trigger(a), Selector::Trigger(b)) + | (Selector::Step(a), Selector::Step(b)) + | (Selector::Binding(a), Selector::Binding(b)) => overlap(a, b), + (Selector::Annotation(a), Selector::Annotation(b)) => overlap(a, b), + _ => false, + } + }) { + return Err("Duplicate facet or conflicting keyed slot".into()); + } + } + Ok(()) +} +fn applicable(a: &OwnRecord, b: &OwnRecord, selector: &Selector) -> bool { + [a, b].into_iter().all(|r| match selector { + Selector::Data(_) => data(r).is_some(), + Selector::FieldMinimum => matches!(shared(r), Ok(SharedBody::DataField { .. })), + Selector::StateGuard | Selector::Trigger(_) => matches!( + r, + OwnRecord::Relation { + body: RelationBody::StateTransition { .. } + } + ), + Selector::Step(_) | Selector::StepOrder => list(r, false).is_ok(), + Selector::Annotation(_) => annotation(r, &None).is_some(), + Selector::Binding(_) => binding(r, None).is_some(), + }) +} +fn check_keys(selector: &Selector, refs: &BTreeMap) -> Result<(), String> { + fn ids(keys: &KeyPair, refs: &BTreeMap) -> Result<(), String> { + match (keys.oa, keys.sa) { + (None, None) => Err("Empty keyed facet".into()), + (Some(a), Some(b)) if refs.get(&a) != Some(&b) => { + Err("Key pair disagrees with frozen reference mapping".into()) + } + _ => Ok(()), + } + } + match selector { + Selector::Trigger(k) | Selector::Step(k) | Selector::Binding(k) => ids(k, refs), + Selector::Annotation(k) => match (&k.oa, &k.sa) { + (None, None) => Err("Empty keyed facet".into()), + (Some(a), Some(b)) => { + if a.path.len() != b.path.len() { + return Err("Annotation path mapping differs".into()); + } + for (oa, sa) in a + .path + .iter() + .chain([&a.field]) + .zip(b.path.iter().chain([&b.field])) + { + ids( + &KeyPair { + oa: Some(*oa), + sa: Some(*sa), + }, + refs, + )?; + } + Ok(()) + } + _ => Ok(()), + }, + _ => Ok(()), + } +} +fn chosen(v: &Reviewed, source: bool) -> Result { + match if source { &v.oa } else { &v.sa } { + Observed::Value(v) => Ok(v.clone()), + Observed::NotApplicable => { + Err("Facet is not applicable; explicit constructor review required".into()) + } + } +} +fn definition_mut(r: &mut OwnRecord) -> Result<&mut DataTypeDefinition, String> { + match shared_mut(r)? { + SharedBody::DataType { definition, .. } => Ok(definition), + _ => Err("Wrong data constructor scope".into()), + } +} +fn map_ref(id: Id, refs: &BTreeMap) -> Result { + refs.get(&id) + .copied() + .ok_or_else(|| format!("Missing reference mapping for {id}")) +} +fn write( + target: &mut OwnRecord, + fact: &FacetReview, + source: bool, + refs: &BTreeMap, +) -> Result<(), String> { + macro_rules! bound { + ($v:expr, $variant:ident, $field:ident) => {{ + let value = chosen($v, source)?; + let DataTypeDefinition::$variant { $field, .. } = definition_mut(target)? else { + return Err("Explicit constructor review required".into()); + }; + *$field = value; + }}; + } + match fact { + FacetReview::Data(v) => match v { + DataReview::IntegerMinimum(v) => bound!(v, Integer, minimum), + DataReview::IntegerMaximum(v) => bound!(v, Integer, maximum), + DataReview::RealMinimum(v) => bound!(v, Real, minimum), + DataReview::RealMaximum(v) => bound!(v, Real, maximum), + DataReview::MinimumLength(v) => bound!(v, String, minimum_length), + DataReview::MaximumLength(v) => bound!(v, String, maximum_length), + DataReview::Literals(v) => bound!(v, Enumeration, literals), + DataReview::Supertype(v) => { + let mut value = chosen(v, source)?; + if source { + value = value.map(|id| map_ref(id, refs)).transpose()?; + } + let DataTypeDefinition::Record { supertype } = definition_mut(target)? else { + return Err("Explicit constructor review required".into()); + }; + *supertype = value; + } + DataReview::Default(v) => { + let SharedBody::DataType { default, .. } = shared_mut(target)? else { + return Err("Wrong facet".into()); + }; + *default = chosen(v, source)?; + } + DataReview::Unit(v) => { + let SharedBody::DataType { unit, .. } = shared_mut(target)? else { + return Err("Wrong facet".into()); + }; + *unit = chosen(v, source)?; + } + DataReview::Constructor(_) => return Err("Constructor is a grouped operation".into()), + }, + FacetReview::FieldMinimum(v) => { + let SharedBody::DataField { minimum, .. } = shared_mut(target)? else { + return Err("Wrong facet".into()); + }; + *minimum = chosen(v, source)?; + } + FacetReview::StateGuard(v) => { + let OwnRecord::Relation { + body: RelationBody::StateTransition { guard, .. }, + } = target + else { + return Err("Wrong facet".into()); + }; + *guard = chosen(v, source)?; + } + FacetReview::Trigger { keys, values } | FacetReview::Step { keys, values } => { + let present = chosen(values, source)?; + let Some(id) = keys.sa else { + return if present { + Err("Missing member mapping".into()) + } else { + Ok(()) + }; + }; + let items = list_mut(target, matches!(fact, FacetReview::Trigger { .. }))?; + if present && !items.contains(&id) { + items.push(id); + } else if !present { + items.retain(|v| *v != id); + } + } + FacetReview::StepOrder(values) => { + if source { + return Err("Explicit final order required by this probe".into()); + } + let value = chosen(values, false)?; + let mut expected = list(target, false)?.clone(); + let mut proposed = value.clone(); + expected.sort(); + proposed.sort(); + if expected != proposed || proposed.windows(2).any(|p| p[0] == p[1]) { + return Err("Final order must contain every surviving member exactly once".into()); + } + *list_mut(target, false)? = value; + } + FacetReview::Annotation { keys, values } => { + let mut value = chosen(values, source)?; + let Some(key) = &keys.sa else { + return if value.is_some() { + Err("Missing annotation mapping".into()) + } else { + Ok(()) + }; + }; + if let Some(value) = &mut value { + value.path = key.path.clone(); + value.field = key.field; + } + let SharedBody::Occurrence { + body: OccurrenceBody::Exchange { values, .. }, + .. + } = shared_mut(target)? + else { + return Err("Wrong annotation scope".into()); + }; + if let Some(index) = values + .iter() + .position(|v| v.path == key.path && v.field == key.field) + { + if let Some(value) = value { + values[index] = value; + } else { + values.remove(index); + } + } else if let Some(value) = value { + values.push(value); + } + } + FacetReview::Binding { keys, values } => { + let mut value = chosen(values, source)?; + let Some(key) = keys.sa else { + return if value.is_some() { + Err("Missing binding mapping".into()) + } else { + Ok(()) + }; + }; + if let Some(value) = &mut value { + value.referenced = key; + if source { + value.local = map_ref(value.local, refs)?; + } + } + let SharedBody::Occurrence { + body: OccurrenceBody::ScenarioUse { bindings, .. }, + .. + } = shared_mut(target)? + else { + return Err("Wrong binding scope".into()); + }; + if let Some(index) = bindings.iter().position(|v| v.referenced == key) { + if let Some(value) = value { + bindings[index] = value; + } else { + bindings.remove(index); + } + } else if let Some(value) = value { + bindings.push(value); + } + } + } + Ok(()) +} +fn in_constructor(selector: &Selector) -> bool { + matches!(selector, Selector::Data(f) if *f != DataFacet::Unit) +} +fn constructor_group(a: &OwnRecord, b: &OwnRecord) -> Vec { + use DataFacet::*; + let mut facets = vec![Constructor, Default]; + for r in [a, b] { + if let Some((definition, ..)) = data(r) { + for f in match definition { + DataTypeDefinition::Integer { .. } => vec![IntegerMinimum, IntegerMaximum], + DataTypeDefinition::Real { .. } => vec![RealMinimum, RealMaximum], + DataTypeDefinition::String { .. } => vec![MinimumLength, MaximumLength], + DataTypeDefinition::Enumeration { .. } => vec![Literals], + DataTypeDefinition::Record { .. } => vec![Supertype], + DataTypeDefinition::Boolean => vec![], + } { + if !facets.contains(&f) { + facets.push(f); + } + } + } + } + facets + .into_iter() + .map(|f| capture(a, b, &Selector::Data(f))) + .collect() +} +pub fn apply(p: &Project, base: &Baseline, review: &Review) -> Result<(Project, Baseline), String> { + base.validate()?; + unique( + &review + .resolutions + .iter() + .map(|r| r.facet.clone()) + .collect::>(), + )?; + let switch = review + .resolutions + .iter() + .any(|r| r.facet == Selector::Data(DataFacet::Constructor)); + if switch + && review + .resolutions + .iter() + .any(|r| in_constructor(&r.facet) && r.facet != Selector::Data(DataFacet::Constructor)) + { + return Err("Overlapping constructor and descendant decisions".into()); + } + let source = own(p, review.source); + let current = own(p, review.target); + let mut target = current.clone(); + let constructor_selector = Selector::Data(DataFacet::Constructor); + for r in &review.resolutions { + if !applicable(&source, ¤t, &r.facet) { + return Err("Facet does not belong to this record family".into()); + } + check_keys(&r.facet, &review.references)?; + if !has_applicable_value(&capture(&source, ¤t, &r.facet)) { + return Err("Facet is inapplicable on both sides".into()); + } + for previous in &base.facets { + if previous.selector() != r.facet + && unique(&[previous.selector(), r.facet.clone()]).is_err() + { + return Err( + "Mapping changed; explicitly invalidate the affected previous facet review" + .into(), + ); + } + } + if r.facet == Selector::Data(DataFacet::Literals) && r.decision == Decision::TakeSource { + return Err("String-keyed literal deltas are outside this probe; no whole-list TakeSource overwrite".into()); + } + } + if switch && base.changed(&source, ¤t, &constructor_selector) == Some(false) { + let decision = review + .resolutions + .iter() + .find(|r| r.facet == constructor_selector) + .unwrap() + .decision; + let proposed = match decision { + Decision::TakeSource => Some(&source), + Decision::Candidate => review.candidate.as_ref(), + Decision::KeepTarget => None, + }; + let actual_target_switch = proposed + .and_then(data) + .zip(data(¤t)) + .is_some_and(|(new, old)| constructor(new.0) != constructor(old.0)); + if !actual_target_switch { + return Err("No constructor change; review descendant facets individually".into()); + } + } + if !switch + && base.changed(&source, ¤t, &constructor_selector) == Some(true) + && review.resolutions.iter().any(|r| in_constructor(&r.facet)) + { + return Err("Explicit constructor review required".into()); + } + let mut resolutions = review.resolutions.iter().collect::>(); + resolutions.sort_by_key(|r| { + if r.facet == constructor_selector { + 0 + } else if r.facet == Selector::StepOrder { + 2 + } else { + 1 + } + }); + for r in &resolutions { + if r.decision == Decision::KeepTarget { + continue; + } + let from_source = r.decision == Decision::TakeSource; + let supplied = if from_source { + &source + } else { + review.candidate.as_ref().ok_or("Missing candidate")? + }; + if r.facet == constructor_selector { + let (definition, default, _) = data(supplied).ok_or("Wrong constructor group")?; + let mut definition = definition.clone(); + if from_source + && let DataTypeDefinition::Enumeration { literals } = &definition + && let Some((DataTypeDefinition::Enumeration { literals: old }, ..)) = data(&target) + && old.iter().any(|v| !literals.contains(v)) + { + return Err( + "Constructor candidate must explicitly review target-only enum literals".into(), + ); + } + if from_source + && let DataTypeDefinition::Record { + supertype: Some(id), + } = &mut definition + { + *id = map_ref(*id, &review.references)?; + } + let SharedBody::DataType { + definition: to_definition, + default: to_default, + .. + } = shared_mut(&mut target)? + else { + return Err("Wrong constructor group".into()); + }; + *to_definition = definition; + *to_default = default.clone(); + } else { + let fact = capture(&source, supplied, &r.facet); + write(&mut target, &fact, from_source, &review.references)?; + } + } + if review.candidate.as_ref().is_some_and(|v| *v != target) { + return Err("Candidate changes an unselected facet".into()); + } + let mut expected = p.clone(); + let command = match target { + OwnRecord::Element { parent, body } => { + let e = expected.elements.get_mut(&review.target).unwrap(); + e.parent = parent; + e.body = body; + Command::EditElement(review.target, e.clone()) + } + OwnRecord::Relation { body } => { + let r = expected.relations.get_mut(&review.target).unwrap(); + r.body = body; + Command::EditRelation(review.target, r.clone()) + } + }; + let prepared = prepare_command(p, &Command::Batch(vec![command])).map_err(|e| e.to_string())?; + if prepared.project != expected { + return Err("Prepared candidate induces unreviewed changes outside selected facets".into()); + } + let target = own(&prepared.project, review.target); + let mut acknowledged = base.clone(); + for r in resolutions { + if r.facet == constructor_selector { + acknowledged + .facets + .retain(|f| !in_constructor(&f.selector())); + for f in constructor_group(&source, &target) { + acknowledged.upsert(f); + } + } else { + acknowledged.upsert(capture(&source, &target, &r.facet)); + } + } + acknowledged.validate()?; + Ok((prepared.project, acknowledged)) +} + +#[cfg(test)] +#[path = "typed_reviews_tests.rs"] +mod tests; diff --git a/experiments/r3-facet-carrier/src/typed_reviews_tests.rs b/experiments/r3-facet-carrier/src/typed_reviews_tests.rs new file mode 100644 index 00000000..16929cfb --- /dev/null +++ b/experiments/r3-facet-carrier/src/typed_reviews_tests.rs @@ -0,0 +1,1046 @@ +use super::*; +use crate::tests::{add, integer, sequence, set, state_side, valid}; + +fn d(f: DataFacet) -> Selector { + Selector::Data(f) +} +fn r(facet: Selector, decision: Decision) -> Resolution { + Resolution { facet, decision } +} +fn review(source: Id, target: Id, resolutions: Vec) -> Review { + let mut references = BTreeMap::new(); + for r in &resolutions { + match &r.facet { + Selector::Trigger(k) | Selector::Step(k) | Selector::Binding(k) => { + if let (Some(a), Some(b)) = (k.oa, k.sa) { + references.insert(a, b); + } + } + Selector::Annotation(k) => { + if let (Some(a), Some(b)) = (&k.oa, &k.sa) { + for (a, b) in a + .path + .iter() + .chain([&a.field]) + .zip(b.path.iter().chain([&b.field])) + { + references.insert(*a, *b); + } + } + } + _ => {} + } + } + Review { + source, + target, + candidate: None, + resolutions, + references, + } +} +fn roundtrip(p: &Project, baseline: &Baseline, name: &str) { + valid(p); + let json = serde_json::to_string_pretty(baseline).unwrap(); + assert_eq!(serde_json::from_str::(&json).unwrap(), *baseline); + assert!( + !json.contains("\"reviewed\"") + && !json.contains("\"orders\"") + && !json.contains("\"body\"") + ); + assert_eq!( + serde_json::from_str::(&serde_json::to_string(p).unwrap()).unwrap(), + *p + ); + assert_eq!(language::parse(&language::format(p)).unwrap(), *p); + let folder = + std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target/r3-probe/evidence"); + std::fs::create_dir_all(&folder).unwrap(); + std::fs::write(folder.join(format!("c-{name}-baseline.json")), json).unwrap(); + std::fs::write( + folder.join(format!("c-{name}-candidate.arcadia")), + language::format(p), + ) + .unwrap(); +} +fn field(p: &mut Project, layer: Layer, owner: Id, name: &str, type_ref: TypeRef, max: u32) -> Id { + let id = add(p, Kind::DataField, layer, name, Some(owner)); + set( + p, + id, + SharedBody::DataField { + type_ref, + minimum: 0, + maximum: Some(max), + unit: String::new(), + direction: ParameterDirection::Unspecified, + storage: FieldStorage::Value, + default: None, + }, + ); + id +} +fn record(p: &mut Project, layer: Layer, name: &str, supertype: Option) -> Id { + let id = add(p, Kind::DataType, layer, name, None); + set( + p, + id, + SharedBody::DataType { + definition: DataTypeDefinition::Record { supertype }, + default: None, + unit: String::new(), + }, + ); + id +} +#[test] +fn c_bounded_data_partial_keep_take_and_duplicate_rejection() { + let mut p = Project::empty(); + let oa = add(&mut p, Kind::DataType, Layer::Oa, "OA bounded", None); + let sa = add(&mut p, Kind::DataType, Layer::Sa, "SA bounded", None); + set(&mut p, oa, integer(0, 100, "m")); + set(&mut p, sa, integer(0, 100, "ft")); + let selectors = vec![ + d(DataFacet::Constructor), + d(DataFacet::IntegerMinimum), + d(DataFacet::IntegerMaximum), + d(DataFacet::Unit), + ]; + let base = Baseline::capture(&p, oa, sa, &selectors).unwrap(); + set(&mut p, oa, integer(120, 200, "cm")); + let keep = review( + oa, + sa, + vec![ + r(d(DataFacet::IntegerMinimum), Decision::KeepTarget), + r(d(DataFacet::Unit), Decision::KeepTarget), + ], + ); + let (p, partial) = apply(&p, &base, &keep).unwrap(); + assert_eq!( + partial.changed(&own(&p, oa), &own(&p, sa), &d(DataFacet::IntegerMinimum)), + Some(false) + ); + assert_eq!( + partial.changed(&own(&p, oa), &own(&p, sa), &d(DataFacet::IntegerMaximum)), + Some(true) + ); + assert!( + partial + .facets + .contains(&FacetReview::Data(DataReview::IntegerMinimum(Reviewed { + oa: Observed::Value(Some(120)), + sa: Observed::Value(Some(0)) + }))) + ); + let take = review( + oa, + sa, + vec![r(d(DataFacet::IntegerMaximum), Decision::TakeSource)], + ); + let (p, base) = apply(&p, &partial, &take).unwrap(); + assert!( + matches!(shared(&own(&p,sa)).unwrap(),SharedBody::DataType {definition:DataTypeDefinition::Integer {minimum:Some(0),maximum:Some(200)},unit,..} if unit=="ft") + ); + let (again, same) = apply(&p, &base, &take).unwrap(); + assert_eq!(again, p); + assert_eq!(same, base); + let mut duplicate = take.clone(); + duplicate.resolutions.push(duplicate.resolutions[0].clone()); + assert!( + apply(&p, &base, &duplicate) + .unwrap_err() + .contains("Duplicate") + ); + let mut duplicate_base = base.clone(); + duplicate_base.facets.push(duplicate_base.facets[0].clone()); + assert!( + apply(&p, &duplicate_base, &take) + .unwrap_err() + .contains("Duplicate") + ); + assert!( + apply( + &p, + &base, + &review(oa, sa, vec![r(Selector::StateGuard, Decision::KeepTarget)]) + ) + .unwrap_err() + .contains("record family") + ); + assert!( + apply( + &p, + &base, + &review( + oa, + sa, + vec![r(d(DataFacet::Constructor), Decision::KeepTarget)] + ) + ) + .unwrap_err() + .contains("No constructor change") + ); + roundtrip(&p, &partial, "range"); + + let mut p = p; + let a = record(&mut p, Layer::Oa, "OA record", None); + let b = record(&mut p, Layer::Sa, "SA record", None); + let af = field(&mut p, Layer::Oa, a, "OA field", TypeRef::Named(oa), 5); + let bf = field(&mut p, Layer::Sa, b, "SA field", TypeRef::Named(sa), 9); + let base = Baseline::capture(&p, af, bf, &[Selector::FieldMinimum]).unwrap(); + if let Some(SharedBody::DataField { minimum, .. }) = + p.elements.get_mut(&af).unwrap().shared_mut() + { + *minimum = 2; + } + let (p, base) = apply( + &p, + &base, + &review( + af, + bf, + vec![r(Selector::FieldMinimum, Decision::TakeSource)], + ), + ) + .unwrap(); + assert!(matches!( + p.elements[&bf].shared(), + Some(SharedBody::DataField { + minimum: 2, + maximum: Some(9), + .. + }) + )); + roundtrip(&p, &base, "field"); +} + +#[test] +fn c_state_guard_and_deleted_trigger_delta() { + let mut p = Project::empty(); + let (oa, old_oa, new_oa) = state_side(&mut p, Layer::Oa, "OA"); + let (sa, old_sa, new_sa) = state_side(&mut p, Layer::Sa, "SA"); + let extra = add(&mut p, Kind::Event, Layer::Sa, "Target only trigger", None); + let old = Selector::Trigger(KeyPair { + oa: Some(old_oa), + sa: Some(old_sa), + }); + let new = Selector::Trigger(KeyPair { + oa: Some(new_oa), + sa: Some(new_sa), + }); + let base = Baseline::capture( + &p, + oa, + sa, + &[Selector::StateGuard, old.clone(), new.clone()], + ) + .unwrap(); + p = prepare_command(&p, &Command::Delete(old_oa)) + .unwrap() + .project; + if let RelationBody::StateTransition { + triggers, guard, .. + } = &mut p.relations.get_mut(&oa).unwrap().body + { + *triggers = vec![new_oa]; + *guard = "armed".into(); + } + if let RelationBody::StateTransition { + triggers, guard, .. + } = &mut p.relations.get_mut(&sa).unwrap().body + { + triggers.push(extra); + *guard = "local inhibit".into(); + } + let request = review( + oa, + sa, + vec![ + r(Selector::StateGuard, Decision::KeepTarget), + r(old, Decision::TakeSource), + r(new, Decision::TakeSource), + ], + ); + let (p, after) = apply(&p, &base, &request).unwrap(); + assert_eq!(list(&own(&p, sa), true).unwrap(), &vec![extra, new_sa]); + assert!(p.elements.contains_key(&old_sa)); + assert!(!p.elements.contains_key(&old_oa)); + assert!( + matches!(&p.relations[&sa].body,RelationBody::StateTransition {guard,..} if guard=="local inhibit") + ); + let (again, same) = apply(&p, &after, &request).unwrap(); + assert_eq!(again, p); + assert_eq!(same, after); + let mut conflict = request.clone(); + conflict.resolutions.push(r( + Selector::Trigger(KeyPair { + oa: Some(new_oa), + sa: Some(old_sa), + }), + Decision::TakeSource, + )); + assert!( + apply(&p, &after, &conflict) + .unwrap_err() + .contains("keyed slot") + ); + conflict.resolutions.reverse(); + assert!( + apply(&p, &after, &conflict) + .unwrap_err() + .contains("keyed slot") + ); + let mut bad_map = request.clone(); + bad_map.references.insert(new_oa, old_sa); + assert!( + apply(&p, &after, &bad_map) + .unwrap_err() + .contains("frozen reference") + ); + let retarget = review( + oa, + sa, + vec![r( + Selector::Trigger(KeyPair { + oa: Some(old_oa), + sa: Some(extra), + }), + Decision::KeepTarget, + )], + ); + assert!( + apply(&p, &after, &retarget) + .unwrap_err() + .contains("Mapping changed") + ); + roundtrip(&p, &base, "deleted-trigger"); +} + +#[test] +fn c_order_is_an_independent_fact_and_preserves_target_only_steps() { + let mut p = Project::empty(); + let (oa, oa_a, oa_b) = sequence(&mut p, Layer::Oa, "OA flow"); + let (sa, sa_a, sa_b) = sequence(&mut p, Layer::Sa, "SA flow"); + let base = Baseline::capture(&p, oa, sa, &[Selector::StepOrder]).unwrap(); + let oa_c = add(&mut p, Kind::Occurrence, Layer::Oa, "OA C", Some(oa)); + let sa_c = add(&mut p, Kind::Occurrence, Layer::Sa, "SA C", Some(sa)); + for id in [oa_c, sa_c] { + set( + &mut p, + id, + SharedBody::Occurrence { + body: OccurrenceBody::Control { + kind: ControlKind::Merge, + }, + precondition: String::new(), + postcondition: String::new(), + }, + ); + } + p.elements + .get_mut(&oa) + .unwrap() + .steps_mut() + .unwrap() + .push(oa_c); + let member = Selector::Step(KeyPair { + oa: Some(oa_c), + sa: Some(sa_c), + }); + let (mut p, partial) = apply( + &p, + &base, + &review(oa, sa, vec![r(member, Decision::TakeSource)]), + ) + .unwrap(); + assert_eq!( + partial.changed(&own(&p, oa), &own(&p, sa), &Selector::StepOrder), + Some(true) + ); + assert_eq!( + base.facets[0], partial.facets[0], + "member review cannot mutate the order fact" + ); + *p.elements.get_mut(&oa).unwrap().steps_mut().unwrap() = vec![oa_b, oa_a, oa_c]; + let extra = add( + &mut p, + Kind::Occurrence, + Layer::Sa, + "Target only X", + Some(sa), + ); + set( + &mut p, + extra, + SharedBody::Occurrence { + body: OccurrenceBody::Control { + kind: ControlKind::Choice, + }, + precondition: String::new(), + postcondition: String::new(), + }, + ); + *p.elements.get_mut(&sa).unwrap().steps_mut().unwrap() = vec![sa_a, extra, sa_b, sa_c]; + assert!( + apply( + &p, + &partial, + &review(oa, sa, vec![r(Selector::StepOrder, Decision::TakeSource)]) + ) + .unwrap_err() + .contains("Explicit final order") + ); + let mut request = review(oa, sa, vec![r(Selector::StepOrder, Decision::Candidate)]); + let mut candidate = own(&p, sa); + *list_mut(&mut candidate, false).unwrap() = vec![sa_b, sa_a, sa_c]; + request.candidate = Some(candidate); + assert!( + apply(&p, &partial, &request) + .unwrap_err() + .contains("every surviving") + ); + *list_mut(request.candidate.as_mut().unwrap(), false).unwrap() = vec![sa_b, extra, sa_a, sa_c]; + let extra_before = p.elements[&extra].clone(); + let (p, base) = apply(&p, &partial, &request).unwrap(); + assert_eq!(p.elements[&extra], extra_before); + let mut hidden = request; + if let Some(OwnRecord::Element { parent, .. }) = &mut hidden.candidate { + *parent = Some(extra); + } + assert!( + apply(&p, &base, &hidden) + .unwrap_err() + .contains("unselected") + ); + roundtrip(&p, &base, "order"); +} + +#[test] +fn c_mixed_constructor_group_distinguishes_not_applicable_and_unbounded() { + let mut p = Project::empty(); + let oa = add(&mut p, Kind::DataType, Layer::Oa, "OA type", None); + let sa = add(&mut p, Kind::DataType, Layer::Sa, "SA type", None); + set(&mut p, oa, integer(0, 100, "m")); + set(&mut p, sa, integer(0, 100, "ft")); + let base = Baseline { + facets: constructor_group(&own(&p, oa), &own(&p, sa)) + .into_iter() + .chain([capture(&own(&p, oa), &own(&p, sa), &d(DataFacet::Unit))]) + .collect(), + }; + set( + &mut p, + oa, + SharedBody::DataType { + definition: DataTypeDefinition::Real { + minimum: None, + maximum: Some(10.5), + }, + default: Some(DataValue::Real(1.5)), + unit: "cm".into(), + }, + ); + assert!( + apply( + &p, + &base, + &review( + oa, + sa, + vec![r(d(DataFacet::IntegerMaximum), Decision::TakeSource)] + ) + ) + .unwrap_err() + .contains("constructor") + ); + let keep = review( + oa, + sa, + vec![r(d(DataFacet::Constructor), Decision::KeepTarget)], + ); + let (unchanged, mixed) = apply(&p, &base, &keep).unwrap(); + assert_eq!(unchanged, p); + assert!( + mixed + .facets + .contains(&FacetReview::Data(DataReview::RealMinimum(Reviewed { + oa: Observed::Value(None), + sa: Observed::NotApplicable + }))) + ); + assert!( + mixed + .facets + .contains(&FacetReview::Data(DataReview::IntegerMaximum(Reviewed { + oa: Observed::NotApplicable, + sa: Observed::Value(Some(100)) + }))) + ); + assert_eq!( + mixed.changed(&own(&p, oa), &own(&p, sa), &d(DataFacet::Unit)), + Some(true) + ); + let take = review( + oa, + sa, + vec![r(d(DataFacet::Constructor), Decision::TakeSource)], + ); + let (converted, converted_base) = apply(&p, &base, &take).unwrap(); + let (reconsidered, _) = apply(&p, &mixed, &take).unwrap(); + assert_eq!( + reconsidered, converted, + "an explicit real target switch may reconsider a previous Keep" + ); + assert!( + matches!(converted.elements[&sa].shared(),Some(SharedBody::DataType {definition:DataTypeDefinition::Real {minimum:None,maximum:Some(10.5)},default:Some(DataValue::Real(1.5)),unit}) if unit=="ft") + ); + assert!(!converted_base.facets.iter().any(|v| matches!( + v, + FacetReview::Data(DataReview::IntegerMinimum(_) | DataReview::IntegerMaximum(_)) + ))); + let mut overlapping = take; + overlapping + .resolutions + .push(r(d(DataFacet::Default), Decision::KeepTarget)); + assert!( + apply(&p, &base, &overlapping) + .unwrap_err() + .contains("Overlapping") + ); + roundtrip(&p, &mixed, "mixed-constructor"); + roundtrip(&converted, &converted_base, "taken-constructor"); + + let mut p = Project::empty(); + let oa = record(&mut p, Layer::Oa, "OA record", None); + let sa = record(&mut p, Layer::Sa, "SA record", None); + let extra = field( + &mut p, + Layer::Sa, + sa, + "Target only field", + TypeRef::Integer, + 1, + ); + let base = Baseline { + facets: constructor_group(&own(&p, oa), &own(&p, sa)), + }; + set(&mut p, oa, integer(0, 10, "")); + let before = p.clone(); + let before_base = base.clone(); + assert!( + apply( + &p, + &base, + &review( + oa, + sa, + vec![r(d(DataFacet::Constructor), Decision::TakeSource)] + ) + ) + .is_err() + ); + assert_eq!(p, before); + assert_eq!(base, before_base); + assert!(p.elements.contains_key(&extra)); +} + +fn occurrence(p: &mut Project, layer: Layer, owner: Id, name: &str, body: OccurrenceBody) -> Id { + let id = add(p, Kind::Occurrence, layer, name, Some(owner)); + set( + p, + id, + SharedBody::Occurrence { + body, + precondition: String::new(), + postcondition: String::new(), + }, + ); + p.elements + .get_mut(&owner) + .unwrap() + .steps_mut() + .unwrap() + .push(id); + id +} +#[test] +fn c_historical_admission_rejects_inapplicable_and_nonfinite_facts() { + let mut p = Project::empty(); + let oa = add(&mut p, Kind::DataType, Layer::Oa, "OA Boolean", None); + let sa = add(&mut p, Kind::DataType, Layer::Sa, "SA Boolean", None); + for id in [oa, sa] { + set( + &mut p, + id, + SharedBody::DataType { + definition: DataTypeDefinition::Boolean, + default: None, + unit: String::new(), + }, + ); + } + let base = Baseline::capture(&p, oa, sa, &[d(DataFacet::Constructor)]).unwrap(); + assert!( + apply( + &p, + &base, + &review( + oa, + sa, + vec![r(d(DataFacet::IntegerMinimum), Decision::KeepTarget)] + ) + ) + .unwrap_err() + .contains("inapplicable on both sides") + ); + let poison = Baseline { + facets: vec![FacetReview::Data(DataReview::RealMinimum(Reviewed { + oa: Observed::Value(Some(f64::NAN)), + sa: Observed::NotApplicable, + }))], + }; + assert!( + apply(&p, &poison, &review(oa, sa, vec![])) + .unwrap_err() + .contains("finite") + ); + let invalid_empty = Baseline { + facets: vec![FacetReview::Trigger { + keys: KeyPair { oa: None, sa: None }, + values: Reviewed { + oa: Observed::Value(false), + sa: Observed::Value(false), + }, + }], + }; + assert!( + apply(&p, &invalid_empty, &review(oa, sa, vec![])) + .unwrap_err() + .contains("Empty keyed") + ); +} +struct Payload { + message: Id, + record: Id, + leaf: Id, + first: ValueKey, + second: ValueKey, +} +fn payload(p: &mut Project, layer: Layer, prefix: &str) -> Payload { + let coord = record(p, layer, &format!("{prefix} Coordinate"), None); + let leaf = field( + p, + layer, + coord, + &format!("{prefix} latitude"), + TypeRef::Real, + 1, + ); + let item = add( + p, + Kind::ExchangeItem, + layer, + &format!("{prefix} Item"), + None, + ); + let origin = field( + p, + layer, + item, + &format!("{prefix} origin"), + TypeRef::Named(coord), + 1, + ); + let destination = field( + p, + layer, + item, + &format!("{prefix} destination"), + TypeRef::Named(coord), + 1, + ); + let a = add( + p, + if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }, + layer, + &format!("{prefix} A"), + None, + ); + let b = add( + p, + if layer == Layer::Oa { + Kind::Activity + } else { + Kind::Function + }, + layer, + &format!("{prefix} B"), + None, + ); + let exchange = p.relate( + if layer == Layer::Oa { + RelationKind::Interaction + } else { + RelationKind::FunctionalExchange + }, + &format!("{prefix} exchange"), + a, + b, + ); + match &mut p.relations.get_mut(&exchange).unwrap().body { + RelationBody::Interaction { items, .. } + | RelationBody::FunctionalExchange { items, .. } => items.push(item), + _ => unreachable!(), + } + if let RelationBody::FunctionalExchange { + source_port, + target_port, + .. + } = p.relations[&exchange].body + { + for id in [source_port, target_port] { + match p.elements.get_mut(&id).unwrap().shared_mut().unwrap() { + SharedBody::Port { + contract: PortContract::FunctionInput { incoming_items }, + .. + } => incoming_items.push(item), + SharedBody::Port { + contract: PortContract::FunctionOutput { outgoing_items }, + .. + } => outgoing_items.push(item), + _ => unreachable!(), + } + } + } + let scenario = add( + p, + Kind::Scenario, + layer, + &format!("{prefix} scenario"), + None, + ); + let first = ValueKey { + path: vec![origin], + field: leaf, + }; + let second = ValueKey { + path: vec![destination], + field: leaf, + }; + let message = occurrence( + p, + layer, + scenario, + &format!("{prefix} message"), + OccurrenceBody::Exchange { + exchange: Some(exchange), + sender: None, + receiver: None, + message_kind: MessageKind::Asynchronous, + reply_to: None, + values: vec![ + MessageValue { + path: first.path.clone(), + field: leaf, + expression: "first old".into(), + }, + MessageValue { + path: second.path.clone(), + field: leaf, + expression: "second independent".into(), + }, + ], + }, + ); + Payload { + message, + record: coord, + leaf, + first, + second, + } +} +fn values_mut(p: &mut Project, id: Id) -> &mut Vec { + let Some(SharedBody::Occurrence { + body: OccurrenceBody::Exchange { values, .. }, + .. + }) = p.elements.get_mut(&id).unwrap().shared_mut() + else { + panic!() + }; + values +} +#[test] +fn c_atomic_annotation_rows_use_full_path_and_keep_unselected_row() { + let mut p = Project::empty(); + let oa = payload(&mut p, Layer::Oa, "OA"); + let sa = payload(&mut p, Layer::Sa, "SA"); + let first = Selector::Annotation(KeyPair { + oa: Some(oa.first.clone()), + sa: Some(sa.first.clone()), + }); + let second = Selector::Annotation(KeyPair { + oa: Some(oa.second.clone()), + sa: Some(sa.second.clone()), + }); + let base = + Baseline::capture(&p, oa.message, sa.message, &[first.clone(), second.clone()]).unwrap(); + let mut wrong_key = base.clone(); + if let FacetReview::Annotation { + values: Reviewed { + oa: Observed::Value(Some(value)), + .. + }, + .. + } = &mut wrong_key.facets[0] + { + value.path = oa.second.path.clone(); + } + assert!( + apply(&p, &wrong_key, &review(oa.message, sa.message, vec![])) + .unwrap_err() + .contains("payload disagrees with key") + ); + values_mut(&mut p, oa.message)[0].expression = "first incoming".into(); + values_mut(&mut p, oa.message)[1].expression = "second incoming".into(); + values_mut(&mut p, sa.message)[1].expression = "second target only edit".into(); + let second_before = values_mut(&mut p, sa.message)[1].clone(); + let take = review( + oa.message, + sa.message, + vec![r(first.clone(), Decision::TakeSource)], + ); + let (mut p, after) = apply(&p, &base, &take).unwrap(); + assert_eq!( + values_mut(&mut p, sa.message)[0].expression, + "first incoming" + ); + assert_eq!(values_mut(&mut p, sa.message)[1], second_before); + assert_eq!( + after.changed(&own(&p, oa.message), &own(&p, sa.message), &second), + Some(true) + ); + values_mut(&mut p, oa.message).remove(0); + let (mut p, after) = apply(&p, &after, &take).unwrap(); + assert_eq!(values_mut(&mut p, sa.message), &vec![second_before]); + // Atomic removal acknowledges the entire row. The other qualified latitude + // row retains independent expression drift, without presence-shadow state. + assert_eq!( + after.changed(&own(&p, oa.message), &own(&p, sa.message), &first), + Some(false) + ); + assert_eq!( + after.changed(&own(&p, oa.message), &own(&p, sa.message), &second), + Some(true) + ); + let mut conflict = review( + oa.message, + sa.message, + vec![ + r(first.clone(), Decision::KeepTarget), + r( + Selector::Annotation(KeyPair { + oa: Some(oa.second.clone()), + sa: Some(sa.first.clone()), + }), + Decision::TakeSource, + ), + ], + ); + assert!( + apply(&p, &after, &conflict) + .unwrap_err() + .contains("keyed slot") + ); + conflict.resolutions.reverse(); + assert!( + apply(&p, &after, &conflict) + .unwrap_err() + .contains("keyed slot") + ); + roundtrip(&p, &after, "annotation"); +} + +struct Use { + use_id: Id, + referenced: Id, + old_local: Id, + alternate: Id, + extra_referenced: Id, +} +fn scenario_use(p: &mut Project, layer: Layer, prefix: &str) -> Use { + let caller = add(p, Kind::Scenario, layer, &format!("{prefix} caller"), None); + let callee = add(p, Kind::Scenario, layer, &format!("{prefix} callee"), None); + let mut line = |owner, name: &str| { + add( + p, + Kind::Lifeline, + layer, + &format!("{prefix} {name}"), + Some(owner), + ) + }; + let referenced = line(callee, "referenced"); + let extra_referenced = line(callee, "extra referenced"); + let old_local = line(caller, "local"); + let alternate = line(caller, "alternate"); + let extra_local = line(caller, "extra local"); + let use_id = occurrence( + p, + layer, + caller, + &format!("{prefix} use"), + OccurrenceBody::ScenarioUse { + scenario: Some(callee), + bindings: vec![ + LifelineBinding { + referenced, + local: old_local, + }, + LifelineBinding { + referenced: extra_referenced, + local: extra_local, + }, + ], + }, + ); + Use { + use_id, + referenced, + old_local, + alternate, + extra_referenced, + } +} +fn bindings_mut(p: &mut Project, id: Id) -> &mut Vec { + let Some(SharedBody::Occurrence { + body: OccurrenceBody::ScenarioUse { bindings, .. }, + .. + }) = p.elements.get_mut(&id).unwrap().shared_mut() + else { + panic!() + }; + bindings +} +#[test] +fn c_atomic_binding_key_is_referenced_lifeline_not_endpoint_pair() { + let mut p = Project::empty(); + let oa = scenario_use(&mut p, Layer::Oa, "OA"); + let sa = scenario_use(&mut p, Layer::Sa, "SA"); + let key = Selector::Binding(KeyPair { + oa: Some(oa.referenced), + sa: Some(sa.referenced), + }); + let extra = Selector::Binding(KeyPair { + oa: Some(oa.extra_referenced), + sa: Some(sa.extra_referenced), + }); + let base = Baseline::capture(&p, oa.use_id, sa.use_id, &[key.clone(), extra]).unwrap(); + bindings_mut(&mut p, oa.use_id)[0].local = oa.alternate; + let target_only = bindings_mut(&mut p, sa.use_id)[1].clone(); + let keep = review( + oa.use_id, + sa.use_id, + vec![r(key.clone(), Decision::KeepTarget)], + ); + let (p, kept) = apply(&p, &base, &keep).unwrap(); + assert_eq!( + binding(&own(&p, sa.use_id), Some(sa.referenced)) + .unwrap() + .unwrap() + .local, + sa.old_local + ); + assert_eq!( + kept.changed(&own(&p, oa.use_id), &own(&p, sa.use_id), &key), + Some(false) + ); + let mut take = review( + oa.use_id, + sa.use_id, + vec![r(key.clone(), Decision::TakeSource)], + ); + assert!( + apply(&p, &base, &take) + .unwrap_err() + .contains("Missing reference mapping") + ); + take.references.insert(oa.alternate, sa.alternate); + let (mut p, after) = apply(&p, &base, &take).unwrap(); + assert_eq!(bindings_mut(&mut p, sa.use_id)[0].local, sa.alternate); + assert_eq!(bindings_mut(&mut p, sa.use_id)[1], target_only); + let mut conflict = take; + conflict.resolutions.push(r( + Selector::Binding(KeyPair { + oa: Some(oa.extra_referenced), + sa: Some(sa.referenced), + }), + Decision::KeepTarget, + )); + assert!( + apply(&p, &after, &conflict) + .unwrap_err() + .contains("keyed slot") + ); + roundtrip(&p, &after, "binding"); +} + +#[test] +fn c_actual_prepared_annotation_cleanup_requires_explicit_review() { + let mut p = Project::empty(); + let oa = payload(&mut p, Layer::Oa, "OA"); + let sa = payload(&mut p, Layer::Sa, "SA"); + for (layer, part) in [(Layer::Oa, &oa), (Layer::Sa, &sa)] { + let base = record(&mut p, layer, &format!("{layer:?} base"), None); + p.elements.get_mut(&part.leaf).unwrap().parent = Some(base); + set( + &mut p, + part.record, + SharedBody::DataType { + definition: DataTypeDefinition::Record { + supertype: Some(base), + }, + default: None, + unit: String::new(), + }, + ); + } + valid(&p); + let baseline = Baseline::capture( + &p, + oa.record, + sa.record, + &[d(DataFacet::Constructor), d(DataFacet::Supertype)], + ) + .unwrap(); + let mut edited = p.elements[&oa.record].clone(); + *edited.shared_mut().unwrap() = SharedBody::DataType { + definition: DataTypeDefinition::Record { supertype: None }, + default: None, + unit: String::new(), + }; + p = prepare_command(&p, &Command::EditElement(oa.record, edited)) + .unwrap() + .project; + assert!(values_mut(&mut p, oa.message).is_empty()); + assert_eq!(values_mut(&mut p, sa.message).len(), 2); + let before = p.clone(); + let before_base = baseline.clone(); + let result = apply( + &p, + &baseline, + &review( + oa.record, + sa.record, + vec![r(d(DataFacet::Supertype), Decision::TakeSource)], + ), + ); + assert!(result.unwrap_err().contains("induces unreviewed")); + assert_eq!(p, before); + assert_eq!(baseline, before_base); +}