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.
|
||||
|
||||
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
|
||||
@@ -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, ¤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