Prove R3 self-tagged facet reviews against carrier baseline
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-09-06 04:19:13 +03:00
parent 8ac9f464ea
commit b2ab2b1feb
6 changed files with 2104 additions and 6 deletions
+106
View File
@@ -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<T> { NotApplicable, Value(T) }
struct Reviewed<T> { oa: Observed<T>, sa: Observed<T> }
struct KeyPair<K> { oa: Option<K>, sa: Option<K> }
struct ValueKey { path: Vec<Id>, field: Id }
struct Baseline { facets: Vec<FacetReview> }
```
`FacetReview` is self-tagged: Data family leaves carry `Reviewed<Option<i64>>`, `Reviewed<Option<f64>>`, `Reviewed<Option<DataValue>>`, 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::<Trace>`/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<T>{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.
+2
View File
@@ -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
@@ -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
+13 -6
View File
@@ -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>) -> Id {
pub(crate) fn add(
p: &mut Project,
kind: Kind,
layer: Layer,
name: &str,
parent: Option<Id>,
) -> 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 {
@@ -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<T> {
NotApplicable,
Value(T),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Reviewed<T> {
pub oa: Observed<T>,
pub sa: Observed<T>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KeyPair<K> {
pub oa: Option<K>,
pub sa: Option<K>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ValueKey {
pub path: Vec<Id>,
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<DataConstructor>),
IntegerMinimum(Reviewed<Option<i64>>),
IntegerMaximum(Reviewed<Option<i64>>),
RealMinimum(Reviewed<Option<f64>>),
RealMaximum(Reviewed<Option<f64>>),
MinimumLength(Reviewed<u32>),
MaximumLength(Reviewed<Option<u32>>),
Literals(Reviewed<Vec<String>>),
Supertype(Reviewed<Option<Id>>),
Default(Reviewed<Option<DataValue>>),
Unit(Reviewed<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum Selector {
Data(DataFacet),
FieldMinimum,
StateGuard,
Trigger(KeyPair<Id>),
Step(KeyPair<Id>),
StepOrder,
Annotation(KeyPair<ValueKey>),
Binding(KeyPair<Id>),
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub enum FacetReview {
Data(DataReview),
FieldMinimum(Reviewed<u32>),
StateGuard(Reviewed<String>),
Trigger {
keys: KeyPair<Id>,
values: Reviewed<bool>,
},
Step {
keys: KeyPair<Id>,
values: Reviewed<bool>,
},
StepOrder(Reviewed<Vec<Id>>),
Annotation {
keys: KeyPair<ValueKey>,
values: Reviewed<Option<MessageValue>>,
},
Binding {
keys: KeyPair<Id>,
values: Reviewed<Option<LifelineBinding>>,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct Baseline {
pub facets: Vec<FacetReview>,
}
#[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<OwnRecord>,
pub resolutions: Vec<Resolution>,
pub references: BTreeMap<Id, Id>,
}
// ADAPTER: exhaustive typed capture, comparison, and application for this subset.
fn observed<T>(v: Option<T>) -> Observed<T> {
v.map_or(Observed::NotApplicable, Observed::Value)
}
fn pair<T>(a: &OwnRecord, b: &OwnRecord, f: impl Fn(&OwnRecord) -> Option<T>) -> Reviewed<T> {
Reviewed {
oa: observed(f(a)),
sa: observed(f(b)),
}
}
fn data(r: &OwnRecord) -> Option<(&DataTypeDefinition, &Option<DataValue>, &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<ValueKey>) -> Option<Option<MessageValue>> {
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<Id>) -> Option<Option<LifelineBinding>> {
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<Self, String> {
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<bool> {
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::<Vec<_>>(),
)?;
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<T>(v: &Reviewed<T>) -> 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<K: PartialEq>(a: &KeyPair<K>, b: &KeyPair<K>) -> 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<Id, Id>) -> Result<(), String> {
fn ids(keys: &KeyPair<Id>, refs: &BTreeMap<Id, Id>) -> 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<T: Clone>(v: &Reviewed<T>, source: bool) -> Result<T, String> {
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<Id, Id>) -> Result<Id, String> {
refs.get(&id)
.copied()
.ok_or_else(|| format!("Missing reference mapping for {id}"))
}
fn write(
target: &mut OwnRecord,
fact: &FacetReview,
source: bool,
refs: &BTreeMap<Id, Id>,
) -> 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<FacetReview> {
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::<Vec<_>>(),
)?;
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, &current, &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, &current, &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, &current, &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(&current))
.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, &current, &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::<Vec<_>>();
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;
File diff suppressed because it is too large Load Diff