Files
ARCADIA-Studio/experiments/r3-facet-carrier

R3 facet carrier experiment

Status: isolated design/probe, not R3 implementation or an accepted persistence contract.

Follow-up: 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

Prefer B: existing typed own-record carriers plus finite facet selectors for further review. A carrier holds independently acknowledged values, not a coherent historical model. This removes the main reason to add immutable observation histories/cursors. One narrow extension is justified by a demonstrated overlap: store a separately acknowledged order vector for each ordered collection.

This is a proposal to investigate further. The probe covers three concrete cases, not every model constructor, mapping direction, source baseline format, or transition command lifecycle.

Design Benefits Cost or failure demonstrated
A: SemanticPath(Vec<String>) + recursive SemanticValue Each leaf has independent storage; arbitrary nested values can be presented uniformly Adds a second public model of numbers, enums, records, lists, references, and absence. Every read/write still needs an exhaustive typed adapter and closed path catalog. Wrong path/value combinations become runtime errors. Constructor groups, mapping, collection deltas, and validation remain necessary. Its flexibility supplies no demonstrated benefit for the three cases.
B0: one typed carrier per side + reviewed selectors Reuses ElementBody, RelationBody, serde validation of shapes, and existing payload types; handles partial scalar review and historical references Insufficient for independently acknowledged membership and order. A new step appended to the carrier can falsely acknowledge its pending placement.
B1: B0 + typed OrderCheckpoint Preserves the older reviewed order while independently changing member presence; passes the counterexample Duplicates only an order's identity vector. Other overlapping projections must be identified before adoption.
Immutable observation pairs + facet cursors Preserves actual whole-record observations and independently selects review epochs Adds observation IDs/indexes, retention, compaction, duplicate snapshots, and cursor validation. Mixed-time carrier values are legitimate, so the tested scalar cases do not justify this machinery. Order needs only its own typed checkpoint. Rejected for now, not proved universally unnecessary.

A is not intrinsically incorrect. The rejection is unnecessary public representation and validation cost for a closed typed model, not a claim that it cannot implement the semantics.

Smallest public shape under test

The exact compiled types are in src/lib.rs. They deliberately leave current prose TransitionField::{Name,Description}, FieldDecision::Value(String), and TracedProperties unchanged. A production extension could add TraceBaseline.semantic: Option<SemanticBaseline> and TransitionField::Semantic(Facet); old absent semantic baselines remain Unreviewed.

enum OwnRecord {
    Element { parent: Option<Id>, body: ElementBody },
    Relation { body: RelationBody },
}
struct MemberPair { oa: Option<Id>, sa: Option<Id> }
struct SemanticBaseline {
    oa: OwnRecord,
    sa: OwnRecord,
    reviewed: Vec<Facet>,
    orders: Vec<OrderCheckpoint>,
}
struct OrderCheckpoint {
    collection: OrderedCollection,
    oa: Vec<Id>,
    sa: Vec<Id>,
}
enum OrderedCollection { Steps } // finite catalog to extend
enum Facet {
    IntegerMinimum, IntegerMaximum, DataUnit, FieldMinimum,
    StateGuard, Trigger(MemberPair), Step(MemberPair), StepsOrder,
    DataConstructor,
}
enum Decision { TakeSource, KeepTarget, Candidate }
struct Resolution { facet: Facet, decision: Decision }
struct Review {
    source: Id,
    target: Id,
    candidate: Option<OwnRecord>,
    resolutions: Vec<Resolution>,
}

Facet is the closed proof subset, not a proposed complete catalog. A final catalog needs typed enum branches for element/relation scalar fields, parent/endpoints, reference members, order, and constructor scopes. It must not accept string paths. Message annotations need their existing (path, field) member identity; binding keys and other non-UUID members likewise need a finite typed key variant. MemberPair uses fixed OA/SA coordinates so reverse transition does not rename persistent selectors. At least one side must exist; conflicting pairs must be rejected. Historical pairs permit removal deltas even after a source object and its ordinary trace have been deleted.

All carrier types derive PartialEq; bodies can contain finite floats. Runtime admission must reject duplicate selectors, invalid selector/body combinations, non-finite values and excessive sizes. It must not impose live graph existence or cross-facet invariants on historical carriers. A baseline with independently acknowledged minimum=120 and maximum=100 is valid history of those two facets even though that combination was never a valid live range. Unreviewed slots are carrier storage, not evidence of acknowledgement.

DataConstructor is a grouped decision: the probe copies definition and default together. Production drift should compare constructor discriminants separately from descendant values; accepting a switch must refresh the complete new constructor group and retire old descendant selectors. That full catalog/acknowledgement expansion is not implemented by this subset. Constructor scope does not include unrelated unit/prose or silently delete target children.

A typed candidate is supplied once per target record, not a recursive merged value per leaf. The adapter selects only acknowledged facets. The probe reconstructs the expected final record and rejects a supplied candidate that changes any unselected slot. Identity/alias are absent; the existing body layer/kind must remain consistent with the explicit mapping. Full model validation still decides whether all accepted choices can coexist.

Preview can carry source_before, source_now, target_before, target_now, and candidate as OwnRecord, plus finite changed-facet rows. The GUI reads typed selected slots from these carriers. Historical before carriers must be labeled as per-facet reviewed values, never as a single past project state. Existing prose FieldChange can stay unchanged; a sibling semantic review payload avoids replacing its strings with a new value language.

Executed cases

  1. Bounded data and field multiplicity. Source integer 0..100 m becomes 120..200 cm; target remains 0..100 ft. Keep minimum and unit acknowledges those facets only. The source carrier intentionally becomes 120..100, while maximum still reports drift. Taking maximum produces target 0..200 ft. A second application is unchanged. Taking a field minimum preserves its independently larger target maximum. Replacing a target Record with Integer is rejected atomically while its target-only field remains; applying an Integer-minimum selector across Record/Integer constructors also rejects.
  2. State guard and triggers. Delete the old OA Event; source then points to a new Event and changes guard. Target has an extra trigger and its own guard. Keep guard, take old-trigger removal and new-trigger addition. The historical MemberPair still identifies the removed source reference. Result preserves target-only trigger and guard, unlinks the old mapped target trigger, and leaves the target Event object alive. Reapplication is unchanged. Baseline JSON still roundtrips the deleted UUID.
  3. Ordered steps. Source reverses A/B while target inserts independent step X. Take-source ordering reports that an explicit final order is needed. Candidate [B,A] rejects because X is missing; [B,X,A] passes and X's record stays byte-for-byte equal. Duplicate resolutions and a hidden parent edit reject. The same test proves the B0 counterexample: starting from [A,B], independently acknowledging appended C changes the carrier to [A,B,C], falsely hiding unreviewed order. B1 keeps order checkpoint [A,B] and reports the pending change.

The merge functions are intentionally small OA→SA experiments. They use real prepare_command(Command::Batch(...)) validation before returning either the project or updated baseline, so the rejected constructor leaves both inputs intact. The baseline is outside the real Project; undo/redo, revision, ETag, fingerprint and on-disk atomic baseline integration are not claimed proved.

Source and JSON parity implication

The tests prove serde JSON roundtrip for the new carrier and for each live candidate. Each live candidate also roundtrips through the existing language::format / language::parse typed Arcadia 4 implementation. No public arbitrary value tree or escaped JSON blob is persisted by this proposal.

They do not prove persisted R3 baseline source parity. The source language currently resolves live body reference fields through @alias and rejects a quoted UUID there (language.rs, Ast::resolve). A negative test transplants the deleted-Event historical body into a live declaration: its formatted source is rejected. This demonstrates the need for an explicit historical context; inventing a live stub or alias would alter semantics.

The smallest integration route is to reuse the existing typed body serde decoder/constructor catalog inside a new baseline-carrier source context. In that context, references should use a dedicated literal such as uuid "...", resolve directly to Id, and bypass live-reference existence checks. Context is essential: a live @alias must not become valid after its object disappears, and historical carrier fields must not be pruned by ordinary deletion. The same historical visitor must explicitly remap UUIDs during real identity remapping, including member pairs and order vectors. A generic field-name-only decoder cannot distinguish these meanings.

The new wrapper/cursor-free types still require source constructor registration, historical token ownership/comment behavior, OpenAPI declarations, resource budgets, and an explicit schema/source migration. Accepted data4 stays unchanged in this experiment. Whether all current typed decoder logic can be reused without duplication needs a targeted follow-up inspection; this report does not assume that work is already solved.

Open questions before adoption

  • Exhaustive scalar/reference/constructor coverage and overlap audit. The proof's DataConstructor comparison is a group-copy shortcut; production needs separate discriminant drift and complete descendant acknowledgement. Other composite fields may need similar finite split storage, not a new arbitrary history mechanism by default.
  • Per-member edits to annotated records, bindings and repeated occurrence contributions. Stable identity is not always a single UUID. Ownership and incident-relation membership plus artifact facets need explicit typed storage outside the own body.
  • Automatic ordering when anchors are genuinely unambiguous. The probe conservatively requires an explicit order; it does not implement the contract's allowed automatic insertions.
  • New/changed mappings, many-to-one conflict agreement, reverse direction, deleted mapped targets, historic remapping, missing/ambiguous reference counterparts, and validation of client-supplied masks and member pairs.
  • Coherent constructor transitions with target-only dependents: one blocking example passed, but successful transitions and every nested constructor group still need coverage.
  • Canonical processing of membership before order and constructor groups before descendants, independent of request array order. The bounded probe consumes resolutions in request order.
  • Drift caused by shared referenced records, child membership rollups, bounds on historical membership, migration from old prose-only baselines, and production atomic baseline/history/source persistence.

Reproduce and evidence

Run from the worktree in PowerShell. The standalone lockfile is included; the vendor configuration is generated locally and points read-only to the pre-existing vendor directory. The target belongs only to this experiment.

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

Result: 3 passed, 0 failed. The final captured command additionally pipes output through Tee-Object -FilePath target/r3-probe/tests.log and exits with $LASTEXITCODE.

target/r3-probe/cargo.toml:

[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "D:/CodexBuilds/Capella2-01a06e2e/vendor"

Evidence is under target/r3-probe/: tests.log, and generated evidence/*-baseline.json / *-candidate.arcadia for mixed range, deleted trigger and target-only step. Generated IDs differ between runs; the asserted behavior does not depend on them. No native UI was used, no package installed, and no other checkout or build target was changed.