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
Rust checks / check (ubuntu-latest) (push) Canceled after 0s
Rust checks / check (windows-latest) (push) Canceled after 0s
This commit is contained in:
@@ -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,6 +2,8 @@
|
|||||||
|
|
||||||
Status: isolated design/probe, not R3 implementation or an accepted persistence contract.
|
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.
|
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
|
## 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
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
//! A carrier stores independently reviewed facets; it is NOT a past valid model.
|
//! A carrier stores independently reviewed facets; it is NOT a past valid model.
|
||||||
use oa_core::*;
|
use oa_core::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
pub mod typed_reviews;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
@@ -403,13 +404,19 @@ pub fn apply(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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()
|
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;
|
*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)
|
let errors: Vec<_> = validation::validate(p)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|i| i.severity == validation::Severity::Error)
|
.filter(|i| i.severity == validation::Severity::Error)
|
||||||
@@ -463,7 +470,7 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody {
|
pub(crate) fn integer(minimum: i64, maximum: i64, unit: &str) -> SharedBody {
|
||||||
SharedBody::DataType {
|
SharedBody::DataType {
|
||||||
definition: DataTypeDefinition::Integer {
|
definition: DataTypeDefinition::Integer {
|
||||||
minimum: Some(minimum),
|
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(
|
let owner = add(
|
||||||
p,
|
p,
|
||||||
if layer == Layer::Oa {
|
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(
|
let sequence = add(
|
||||||
p,
|
p,
|
||||||
if layer == Layer::Oa {
|
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, ¤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::<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
Reference in New Issue
Block a user