From 533550b834e88614ce0730283ebf8023e97ecd21 Mon Sep 17 00:00:00 2001 From: James Ross Date: Thu, 11 Jun 2026 09:58:15 -0700 Subject: [PATCH 01/14] =?UTF-8?q?docs:=20add=20design=20packet=200026=20?= =?UTF-8?q?=E2=80=94=20braid=20shell=20family=20and=20plural=20settlement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E1 of the strand/braid campaign (echo#537 requirements 1-2, #538 rider): SettlementDecision::Plural + in-graph theta_braid shells in the BoundaryTransitionRecord family, replay-from-shell as the acceptance test. Source facts verified @465cf61e. Awaiting packet + test-plan review before RED. --- .../design.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/design/0026-braid-shell-family-and-plural-settlement/design.md diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md new file mode 100644 index 00000000..4b46bb72 --- /dev/null +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -0,0 +1,140 @@ + + + +# 0026 — Braid Shell Family and Plural Settlement + +_Make lawful plurality a retainable, replayable outcome: add the `Plural` +settlement arm and emit braid-scale results as in-graph holographic shells +(θ_braid) of the same family as tick receipts — so braiding becomes a WARP +optic lowering instead of a service-layer function call._ + +Legend: `PLATFORM` + +Status: **proposed — awaiting packet + test-plan review** + +## Doctrine + +AIΩN Paper VII (DOI 10.5281/zenodo.19751149): + +- **Prop 3.5** — WARP is closed over its own witness-bearing outputs: tick + receipts, braid shells, and import shells are **one shell family** of + retained holographic boundaries living inside the causal graph. That + containment is what makes replay cheap at every scale. +- **§4.2** — "irreducible plurality need not be treated as merge failure": + `Plural` is a first-class arm of the outcome algebra + `Derived ⊔ Plural ⊔ Conflict ⊔ Obstruction`, not a staging posture. + +Tracking issues: flyingrobots/echo#537 (shell-family doctrine, requirements +1–2 of 5), with #538 (three-tier posture) as a field-level rider. Connective +doctrine for #470 / #476 / #483 — whichever braid/settlement work lands +first establishes the shell family; this packet is that work. + +## Current state (verified @465cf61e) + +- `SettlementDecision` (`crates/warp-core/src/settlement.rs#146`) has no + plural arm; `ImportCandidate` lowers to `AdmissionOutcomeKind::Derived` + (`settlement.rs#158`) and conflicts carry `ConflictReason` + (`settlement.rs#36`). Settlement compares **one strand** against base. +- Braid identities exist without substance: `OpticFocus::Braid` + (`crates/warp-core/src/optic.rs#161`), `EchoCoordinate::Braid` + (`optic.rs#297`), `SupportPin` geometry implemented and + invariant-validated — but no reducer materializes a braid and nothing + emits a braid-level shell. +- `BoundaryTransitionRecord` (`crates/warp-core/src/provenance_store.rs#626`) + is the existing retained-shell mechanics at strand boundaries — the + family to extend, **not** a pattern to duplicate. +- Plurality exists only admission-side: `PluralIntent`, + `PluralityRequiresExplicitPolicy` (optic.rs admission path). Below + admission, plurality is destroyed (admit-one/block-one per tick). + +## Hill + +A settlement comparison over two or more strands sharing a fork basis can +end in a **retained plural outcome**: a θ_braid shell, resident in the +provenance store, carrying the comparison basis, member strand refs, the +outcome arm (`Collapsed`/`Plural`/`Conflict`), and its witness — such that +**the braid outcome can be replayed from the shell alone**, without +rematerializing member strands. That replay test is the definition of done. + +## Campaign map (this packet = E1) + +| Slice | Scope | Status | +| :----- | :-------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- | +| E0 | Tier posture field (`scratch \| author-only \| shared`) on strand creation (echo#538) | optional preface; if not first, E1 carries the field on θ_braid only | +| **E1** | **`SettlementDecision::Plural` + θ_braid shell family + replay-from-shell (this packet)** | proposed | +| E2 | Holographic strand origins — fork via checkpoint-pinned basis ref, empty entry vector (echo#537 comment design) | next | +| E3 | Braid reducer/weave over N strands + collapse policies | after E2 (needs cheap strands) | + +## Acceptance criteria + +1. `SettlementDecision` gains a `Plural` arm carrying the surviving + alternatives as refs (not clones), lowering to + `AdmissionOutcomeKind::Plural`; existing `Derived`/`Conflict` paths are + byte-identical for single-strand settlement (regression-pinned). +2. A `BraidShell` (θ_braid) record exists in the + `BoundaryTransitionRecord` family: comparison basis (fork basis ref + + frontier facts), member strand refs with their `SupportPin` posture, + outcome arm, witness digest. It is written into the provenance store — + in-graph, content-addressed, retained. +3. **Replay-from-shell acceptance test**: given only the θ_braid shell and + the provenance store, replay reproduces the settlement outcome + (same arm, same member verdicts, same digests) without loading member + strand histories. +4. Plurality is never silently collapsed: a plural result requires an + explicit, witnessed collapse act (policy-named) to become `Derived`; + absent policy, `Plural` is the retained truth. +5. θ_braid carries a revelation-posture field (E0 rider: + `scratch | author-only | shared`, default `author-only`) so the shell + family never hardens around implicit shared visibility (echo#538). +6. No new record family parallel to `BoundaryTransitionRecord`; extension + only. + +## Test plan (for review before RED) + +1. **Plural arm shape** — settlement over two strands with disjoint lawful + rewrites of the same footprint region under an explicit plural policy + yields `SettlementDecision::Plural` with both members referenced; + ABI lowering maps to `AdmissionOutcomeKind::Plural`. +2. **Single-strand regression** — existing settle paths produce identical + outcomes and digests before/after (golden fixtures from current tests). +3. **Shell emission** — every settlement that ends `Collapsed`/`Plural`/ + `Conflict` at braid scope writes exactly one θ_braid into provenance; + shell digest is deterministic across runs. +4. **Replay-from-shell** — drop/forget member strand materializations; + replay from θ_braid reproduces arm + member verdicts + witness digest. + (The headline test; failure here fails the hill.) +5. **No silent collapse** — plural result + no collapse policy stays + `Plural` across ticks; collapse without named policy is an + `Obstruction` with witness. +6. **Posture default** — θ_braid created from debugger/counterfactual + strands defaults `author-only`; promotion to `shared` is an explicit + act that re-witnesses. +7. **Conflict still conflicts** — overlapping rewrites without plural + policy keep today's `Conflict` + `ConflictReason` behavior exactly. + +## Playback questions + +1. Can a plural settlement outcome be retained, queried, and replayed from + its shell without rematerializing member strands? +2. Is the θ_braid shell demonstrably the same record family as the + existing boundary-transition mechanics (one family, per Prop 3.5)? +3. Does any path silently collapse plurality? + +## Non-goals + +- No braid reducer/weave over N>2 strands or collapse-policy library (E3). +- No suffix-transport shell θ_rep / import idempotence (later slice). +- No fork-mechanics change (E2 owns holographic origins). +- No session implementation (design 0025 owns that). +- No public ABI breakage beyond the additive `Plural` arm. + +## Open questions (for James at packet review) + +1. Should E0 (tier posture on strand creation) land first as its own tiny + slice, or is the θ_braid-only posture field (criterion 5) the right + E1-scoped compromise? +2. `Plural` member refs: strand ids + basis digests, or full + `SupportPin` snapshots? (Refs keep the shell holographic; pins make it + self-contained. Recommendation: refs + pin digests.) +3. Does `AdmissionOutcomeKind` already reserve a `Plural` discriminant in + the ABI, or does this slice mint it (ABI version note required)? From 94e8ec47061f0cb5c17e88a9356eefc78bfe3d2d Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 11:45:09 -0700 Subject: [PATCH 02/14] =?UTF-8?q?docs(0026):=20resolve=20open=20question?= =?UTF-8?q?=203=20=E2=80=94=20AdmissionOutcomeKind::Plural=20already=20exi?= =?UTF-8?q?sts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit admission.rs#150-152@465cf61e mints the Plural discriminant at admission scope; the slice extends the algebra to settlement scope with no new ABI discriminant. --- .../design.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md index 4b46bb72..e621387c 100644 --- a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -136,5 +136,10 @@ rematerializing member strands. That replay test is the definition of done. 2. `Plural` member refs: strand ids + basis digests, or full `SupportPin` snapshots? (Refs keep the shell holographic; pins make it self-contained. Recommendation: refs + pin digests.) -3. Does `AdmissionOutcomeKind` already reserve a `Plural` discriminant in - the ABI, or does this slice mint it (ABI version note required)? +3. ~~Does `AdmissionOutcomeKind` already reserve a `Plural` discriminant in + the ABI?~~ **Resolved during packet drafting**: yes — + `AdmissionOutcomeKind::Plural` exists at + `crates/warp-core/src/admission.rs#150-152@465cf61e` ("Multiple claims + remained lawfully plural over one bounded site"). The outcome algebra is + already minted at admission scope; this slice extends it to settlement + scope (`SettlementDecision`) with no new ABI discriminant. From d0b6663515c4749ddcca1d9f2af3cd91d764027b Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 11:56:53 -0700 Subject: [PATCH 03/14] =?UTF-8?q?docs(0026):=20incorporate=20review=20enha?= =?UTF-8?q?ncements=20=E2=80=94=20approved,=20RED=20next?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ENHANCE->APPROVE verdict applied in full: - outcome algebra is Derived/Plural/Conflict/Obstruction only; collapse is a witnessed transition emitting a new Derived shell with collapsed_from lineage, never a fifth arm - plural-settlement policy and collapse policy explicitly separated with the four-rule block - BraidShell made replay-sufficient: canonical member entries carry support-pin/basis/frontier/footprint/claim digests + compact verdict snapshots; no entry vectors, no histories - Obstruction included in shell emission scope; shells append-only - canonical member ordering + domain-separated digests (echo.shell.braid.v1 etc.) + permutation/tamper/query tests - E0-lite posture core (RevelationPosture, author-only default, least-revealed-member invariant) pinned as first commit of E1 - N-strand honesty rule; serialization variant-tag requirement; ABI wording corrected to wire-format-only claim - enhanced 12-item acceptance criteria + 11-item test plan adopted --- .../design.md | 353 +++++++++++++----- 1 file changed, 266 insertions(+), 87 deletions(-) diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md index e621387c..50418a6c 100644 --- a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -10,7 +10,11 @@ optic lowering instead of a service-layer function call._ Legend: `PLATFORM` -Status: **proposed — awaiting packet + test-plan review** +Status: **approved with enhancements (James review, 2026-06-12) — RED next** + +> A plural braid shell is not a note that plurality happened. It is the +> retained boundary that makes plurality replayable without reopening the +> strands. — review verdict ## Doctrine @@ -21,13 +25,14 @@ AIΩN Paper VII (DOI 10.5281/zenodo.19751149): retained holographic boundaries living inside the causal graph. That containment is what makes replay cheap at every scale. - **§4.2** — "irreducible plurality need not be treated as merge failure": - `Plural` is a first-class arm of the outcome algebra - `Derived ⊔ Plural ⊔ Conflict ⊔ Obstruction`, not a staging posture. + the outcome algebra is `Derived ⊔ Plural ⊔ Conflict ⊔ Obstruction` and + **those are the only arms**. Collapse is a witnessed _transition_ that + produces a new `Derived` record; it is never a fifth arm. Tracking issues: flyingrobots/echo#537 (shell-family doctrine, requirements -1–2 of 5), with #538 (three-tier posture) as a field-level rider. Connective -doctrine for #470 / #476 / #483 — whichever braid/settlement work lands -first establishes the shell family; this packet is that work. +1–2 of 5), #538 (three-tier posture; E0-lite lands as the first commit of +this slice). Connective doctrine for #470 / #476 / #483 — this packet +establishes the shell family the others must reuse. ## Current state (verified @465cf61e) @@ -35,90 +40,265 @@ first establishes the shell family; this packet is that work. plural arm; `ImportCandidate` lowers to `AdmissionOutcomeKind::Derived` (`settlement.rs#158`) and conflicts carry `ConflictReason` (`settlement.rs#36`). Settlement compares **one strand** against base. +- `AdmissionOutcomeKind::Plural` **already exists** + (`crates/warp-core/src/admission.rs#150-152`) — the algebra is minted at + admission scope; this slice extends it to settlement scope with no new + ABI discriminant. Admission-side and settlement-side plurality are the + **same doctrine at different optic scales**, never two meanings. - Braid identities exist without substance: `OpticFocus::Braid` (`crates/warp-core/src/optic.rs#161`), `EchoCoordinate::Braid` (`optic.rs#297`), `SupportPin` geometry implemented and invariant-validated — but no reducer materializes a braid and nothing emits a braid-level shell. - `BoundaryTransitionRecord` (`crates/warp-core/src/provenance_store.rs#626`) - is the existing retained-shell mechanics at strand boundaries — the - family to extend, **not** a pattern to duplicate. -- Plurality exists only admission-side: `PluralIntent`, - `PluralityRequiresExplicitPolicy` (optic.rs admission path). Below - admission, plurality is destroyed (admit-one/block-one per tick). + is the existing retained-shell mechanics — the family to extend as a + subkind, **not** a pattern to duplicate. ## Hill -A settlement comparison over two or more strands sharing a fork basis can -end in a **retained plural outcome**: a θ_braid shell, resident in the -provenance store, carrying the comparison basis, member strand refs, the -outcome arm (`Collapsed`/`Plural`/`Conflict`), and its witness — such that -**the braid outcome can be replayed from the shell alone**, without -rematerializing member strands. That replay test is the definition of done. +A settlement comparison over strands sharing a fork basis can end in a +**retained plural outcome**: a θ_braid shell, resident in the provenance +store, carrying the comparison basis, canonical member entries with compact +verdict snapshots, policy identity, outcome arm, witness digest, and +revelation posture — such that **the braid outcome replays from the shell +alone**, with member strand-history loaders replaced by panic stubs. That +hostile replay test is the definition of done. If replay needs member +histories, θ_braid is not a shell; it is a souvenir. ## Campaign map (this packet = E1) -| Slice | Scope | Status | -| :----- | :-------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- | -| E0 | Tier posture field (`scratch \| author-only \| shared`) on strand creation (echo#538) | optional preface; if not first, E1 carries the field on θ_braid only | -| **E1** | **`SettlementDecision::Plural` + θ_braid shell family + replay-from-shell (this packet)** | proposed | -| E2 | Holographic strand origins — fork via checkpoint-pinned basis ref, empty entry vector (echo#537 comment design) | next | -| E3 | Braid reducer/weave over N strands + collapse policies | after E2 (needs cheap strands) | - -## Acceptance criteria - -1. `SettlementDecision` gains a `Plural` arm carrying the surviving - alternatives as refs (not clones), lowering to - `AdmissionOutcomeKind::Plural`; existing `Derived`/`Conflict` paths are - byte-identical for single-strand settlement (regression-pinned). -2. A `BraidShell` (θ_braid) record exists in the - `BoundaryTransitionRecord` family: comparison basis (fork basis ref + - frontier facts), member strand refs with their `SupportPin` posture, - outcome arm, witness digest. It is written into the provenance store — - in-graph, content-addressed, retained. -3. **Replay-from-shell acceptance test**: given only the θ_braid shell and - the provenance store, replay reproduces the settlement outcome - (same arm, same member verdicts, same digests) without loading member - strand histories. -4. Plurality is never silently collapsed: a plural result requires an - explicit, witnessed collapse act (policy-named) to become `Derived`; - absent policy, `Plural` is the retained truth. -5. θ_braid carries a revelation-posture field (E0 rider: - `scratch | author-only | shared`, default `author-only`) so the shell - family never hardens around implicit shared visibility (echo#538). -6. No new record family parallel to `BoundaryTransitionRecord`; extension - only. - -## Test plan (for review before RED) - -1. **Plural arm shape** — settlement over two strands with disjoint lawful - rewrites of the same footprint region under an explicit plural policy - yields `SettlementDecision::Plural` with both members referenced; - ABI lowering maps to `AdmissionOutcomeKind::Plural`. -2. **Single-strand regression** — existing settle paths produce identical - outcomes and digests before/after (golden fixtures from current tests). -3. **Shell emission** — every settlement that ends `Collapsed`/`Plural`/ - `Conflict` at braid scope writes exactly one θ_braid into provenance; - shell digest is deterministic across runs. -4. **Replay-from-shell** — drop/forget member strand materializations; - replay from θ_braid reproduces arm + member verdicts + witness digest. - (The headline test; failure here fails the hill.) -5. **No silent collapse** — plural result + no collapse policy stays - `Plural` across ticks; collapse without named policy is an +| Slice | Scope | Status | +| :----- | :-------------------------------------------------------------------------------------- | :--------------------------------- | +| E0 | Full tier-posture system on strand creation (echo#538) | E0-lite lands as E1's first commit | +| **E1** | **`SettlementDecision::Plural` + θ_braid shell family + hostile replay-from-shell** | approved with enhancements | +| E2 | Holographic strand origins — checkpoint-pinned basis ref, empty entry vector (echo#537) | next | +| E3 | Braid reducer/weave over N strands + collapse-policy library | after E2 (needs cheap strands) | + +**Honesty rule (N-strand):** E1 data structures are N-capable (`Vec` +members, canonical ordering); E1 _behavior_ is regression-pinned for +two-member braid settlement. General N-strand reducer/weave semantics +remain E3. This packet does not claim them. + +## The two policies (never blurred) + +1. **Plural-settlement policy** — permits multiple lawful alternatives to + survive a settlement comparison. +2. **Collapse policy** — permits a retained plural result to become + `Derived` via a new witnessed shell-family record. + +The rules: + +> A settlement may produce `Plural` only when plurality is lawful under an +> explicit plural-settlement policy or already-witnessed plural intent. +> Once `Plural` exists, it may become `Derived` only through an explicit, +> named, witnessed collapse policy. Absent collapse policy, `Plural` +> remains `Plural`. Absent plural-settlement policy, incompatible overlap +> remains `Conflict`. + +No default winner. No first-alternative-wins. No stable-sort-picks-winner. +No UI-selected-top-one. Plural is not indecision; it is a lawful retained +outcome. + +## Planned shape + +Settlement decision (algebraic arms only; collapse is a transition): + +```rust +enum SettlementDecision { + Derived(DerivedSettlement), + Plural(PluralSettlement), // refs + canonical digests, no clones + Conflict(ConflictSettlement), + Obstruction(ObstructionSettlement), +} +``` + +θ_braid as a subkind of the existing retained shell family (θ_tick, +θ_braid, θ_import are siblings; no `BraidSettlementStore`, no parallel log, +no service-layer result cache — ever): + +```rust +struct BraidShell { + version: ShellVersion, + coordinate: EchoCoordinate, // Braid(...) — first real consumer + basis: BraidBasis, + members: Vec, // canonically ordered + policy: SettlementPolicyRef, + outcome: BraidShellOutcome, + witness: BraidWitness, + posture: RevelationPosture, + digest: ContentDigest, +} + +struct BraidShellMember { + strand_ref: StrandRef, + support_pin_ref: SupportPinRef, + support_pin_digest: ContentDigest, + basis_digest: ContentDigest, + frontier_digest: ContentDigest, + footprint_digest: ContentDigest, + claim_digest: ContentDigest, + verdict: MemberVerdict, // compact snapshot, not history + verdict_digest: ContentDigest, + posture: RevelationPosture, +} + +enum BraidShellOutcome { + Derived { + result_ref: DerivedRef, + collapse_policy: Option, + collapsed_from: Option, // plural→derived lineage + }, + Plural { alternatives: Vec }, + Conflict { reasons: Vec }, + Obstruction { reason: ObstructionReason, witness: WitnessDigest }, +} +``` + +The shell binds `basis + members + member verdicts + policy + outcome + +witness`. It contains **no entry vectors and no strand histories** — enough +compact, content-addressed facts that replay never calls the restaurant. + +**E0-lite posture core (first commit of E1):** + +```rust +enum RevelationPosture { Scratch, AuthorOnly, Shared } +``` + +Default `AuthorOnly`. Promotion to `Shared` is an explicit witnessed act. +Invariant: **a braid shell cannot reveal more than its least-revealed +member** unless a witnessed redaction/promotion transform exists. Posture +is load-bearing, not cosmetic: it affects query, replay digests, promotion, +and visibility. + +**Shells are append-only.** Collapse never mutates the plural shell; it +creates a new `Derived` shell-family record with +`collapsed_from: Some(prior_shell_ref)`. The old plural shell remains true +forever: + +```text +θ_braid_plural ──collapsed_by(policy)──▶ θ_braid_derived +``` + +**Determinism:** members are canonically ordered (sort key: +`basis_digest, strand_ref, support_pin_digest, claim_digest` — or a single +canonical `member_digest`). Digest domains are explicit and separated: +`echo.shell.tick.v1`, `echo.shell.braid.v1`, `echo.shell.import.v1`, +`echo.braid.member.v1`, `echo.braid.witness.v1`. + +**Serialization honesty:** `SettlementDecision`'s canonical encoding uses +stable explicit variant tags (not derived-serializer ordinal accident). +No wire-format ABI breakage; Rust source exhaustiveness may require +downstream match updates unless the enum is already `non_exhaustive` or +internal — the compiler will tell it straight, and so does this packet. + +**Conflict structure:** braid-scope conflict carries enough structure to +distinguish incompatible-rewrite, missing-plural-policy +(plurality-would-have-been-lawful), basis mismatch, invalid support pin, +frontier-fact mismatch, and policy obstruction. No flattening into one +reason. + +## Acceptance criteria (enhanced per review) + +1. `SettlementDecision` gains a `Plural` arm carrying surviving + alternatives by refs plus canonical member/verdict digests. It lowers + to `AdmissionOutcomeKind::Plural`. +2. Existing single-strand `Derived`, `Conflict`, and `Obstruction` + behavior remains byte-identical at the canonical serialization/digest + layer, with golden fixtures proving it. +3. `BraidShell` / θ_braid is added as a subkind of the existing + `BoundaryTransitionRecord` shell family, not as a parallel record + family. +4. θ_braid records basis, canonical member refs, support-pin digests, + member verdict summaries, policy ref/digest, outcome arm, witness + digest, and revelation posture. +5. θ_braid outcome uses the same algebraic arms as settlement: `Derived`, + `Plural`, `Conflict`, `Obstruction`. Collapse is represented as a + witnessed transition producing `Derived`, not as a separate outcome + arm. +6. Replay from θ_braid reproduces outcome arm, member verdicts, policy + digest, and witness digest using only the shell and provenance-store + shell records, without loading member strand histories. +7. Plurality is never silently collapsed. A plural result remains plural + until a named, witnessed collapse policy emits a new shell-family + record. +8. Missing collapse policy yields retained `Plural`; missing + plural-settlement policy for incompatible overlapping rewrites yields + existing `Conflict` behavior. +9. θ_braid defaults to author-only; promotion to shared is explicit, + witnessed, and cannot reveal more than member postures permit. +10. Member ordering is canonical. Input strand permutation does not change + shell digest or replay result. +11. θ_braid can be queried by shell digest, braid coordinate, basis ref, + member strand ref, outcome arm, and posture. +12. Tampering with basis digest, member verdict digest, policy digest, + posture witness, or outcome digest causes replay failure. + +## Test plan (enhanced per review) + +1. **Plural arm shape** — two lawful alternatives over the same bounded + site under explicit plural-settlement policy produce + `SettlementDecision::Plural { alternatives: [a, b], .. }` lowering to + `AdmissionOutcomeKind::Plural`. +2. **Single-strand regression** — golden fixtures prove canonical digest + and serialized output are unchanged for existing single-strand paths. +3. **Shell emission** — every braid-scope `Derived`, `Plural`, `Conflict`, + or `Obstruction` emits exactly one θ_braid-family boundary record. +4. **Hostile replay-from-shell** (the sacred test) — member strand-history + loaders replaced with panic stubs; replay from θ_braid still reproduces + outcome arm, member verdicts, policy digest, witness digest, and shell + digest. The test fails if replay touches `load_strand_history`-shaped + surfaces. Instrumented brutally; cheating impossible. +5. **No silent collapse** — plural without collapse policy remains + `Plural` across ticks; attempted collapse without named policy emits `Obstruction` with witness. -6. **Posture default** — θ_braid created from debugger/counterfactual - strands defaults `author-only`; promotion to `shared` is an explicit - act that re-witnesses. -7. **Conflict still conflicts** — overlapping rewrites without plural - policy keep today's `Conflict` + `ConflictReason` behavior exactly. +6. **Explicit collapse** — plural + named collapse policy emits a new + `Derived` shell referencing the prior plural shell + (`collapsed_from`); the original plural shell is unchanged. +7. **Conflict still conflicts** — overlapping incompatible rewrites + without plural-settlement policy keep today's `Conflict` + + `ConflictReason` behavior exactly. +8. **Posture default and promotion** — θ_braid defaults author-only; + promotion to shared requires witnessed promotion; promotion fails or + redacts when member posture forbids sharing. +9. **Deterministic ordering** — same members in different input order + produce the same θ_braid digest and same replay result. +10. **Tamper resistance** — changing member verdict digest, basis digest, + outcome arm, or policy digest makes replay fail. +11. **Queryability** — shell retrievable by digest, basis, braid + coordinate, member strand, outcome arm, and posture. + +## Design notes (COULD tier, adopt where cheap) + +- `BraidShellReplayPlan { shell_ref, required_records, forbidden_loads }` + as the internal structure that makes the hostile harness auditable. +- θ_braid as the first real consumer of `EchoCoordinate::Braid`: + `BraidCoordinate = hash(basis_ref, canonical_member_digest_list, +settlement_policy_digest)`; the shell lives at that coordinate. +- `PluralSetDigest(ContentDigest)` over canonical alternatives — a stable + identity for "these alternatives lawfully coexist" independent of shell + wrapper details. +- Collapse-lineage graph affordances: "all derived outcomes that collapsed + plural alternatives", "all plural shells not yet collapsed", "all + collapses by policy X". + +## Refusals (DON'T tier — hard lines) + +- No parallel braid result store beside the provenance store. +- No silent plural→derived degradation by time, pressure, convenience, UI + default, or last-writer-wins. +- No `Collapsed` as an algebra arm — event/witness/transition names only. +- No claimed N-strand reducer semantics (E3 owns them). +- No cosmetic posture field. ## Playback questions 1. Can a plural settlement outcome be retained, queried, and replayed from - its shell without rematerializing member strands? + its shell — with strand-history loaders panicking — without + rematerializing member strands? 2. Is the θ_braid shell demonstrably the same record family as the existing boundary-transition mechanics (one family, per Prop 3.5)? 3. Does any path silently collapse plurality? +4. Does posture observably gate query, replay digests, and promotion? ## Non-goals @@ -126,20 +306,19 @@ rematerializing member strands. That replay test is the definition of done. - No suffix-transport shell θ_rep / import idempotence (later slice). - No fork-mechanics change (E2 owns holographic origins). - No session implementation (design 0025 owns that). -- No public ABI breakage beyond the additive `Plural` arm. - -## Open questions (for James at packet review) - -1. Should E0 (tier posture on strand creation) land first as its own tiny - slice, or is the θ_braid-only posture field (criterion 5) the right - E1-scoped compromise? -2. `Plural` member refs: strand ids + basis digests, or full - `SupportPin` snapshots? (Refs keep the shell holographic; pins make it - self-contained. Recommendation: refs + pin digests.) -3. ~~Does `AdmissionOutcomeKind` already reserve a `Plural` discriminant in - the ABI?~~ **Resolved during packet drafting**: yes — - `AdmissionOutcomeKind::Plural` exists at - `crates/warp-core/src/admission.rs#150-152@465cf61e` ("Multiple claims - remained lawfully plural over one bounded site"). The outcome algebra is - already minted at admission scope; this slice extends it to settlement - scope (`SettlementDecision`) with no new ABI discriminant. +- No wire-format ABI breakage (source exhaustiveness updates permitted, + stated honestly above). + +## Resolved questions (James review, 2026-06-12) + +1. **E0 first?** Not as a separate blocker — E0-lite (the + `RevelationPosture` enum, author-only default, promotion witness, + least-revealed-member invariant) lands as the **first commit of E1**. + θ_braid never ships with implicit visibility. +2. **Member refs?** Neither extreme: refs + canonical support-pin digest + + compact member verdict snapshot (shape in Planned shape above). + Self-contained for replay of the settlement result; not self-contained + for reconstruction of the strand. The middle way is the blade. +3. **ABI?** `AdmissionOutcomeKind::Plural` exists + (`admission.rs#150-152@465cf61e`); settlement-side plurality is the + same doctrine at a different optic scale — one meaning, two scopes. From ad9dbd1f697859df52b19b5cb34cba8751df5ba6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 12:02:24 -0700 Subject: [PATCH 04/14] feat(warp-core): add revelation posture core (E0-lite, design 0026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three-Tier Thinking Room substrate (Paper VII 6.3, echo#538) required before any theta_braid shell lands, per design packet 0026 resolved question 1: - RevelationPosture { Scratch < AuthorOnly < Shared } with AuthorOnly default — nothing ships with implicit shared visibility; stable canonical wire tags for digest domains - promote_posture: posture only widens, explicitly and witnessed; narrowing and same-posture requests are typed PostureObstructions, never silent no-ops - least-revealed-member invariant via shell_posture_obstruction: a composite shell cannot reveal more than its least-revealed member - 10 unit tests; clippy-clean (remaining repo clippy errors are pre-existing in coordinator.rs, untouched here) Part of lane/0026-braid-shell-family (E1). theta_braid and SettlementDecision::Plural build on these nouns next. --- crates/warp-core/src/lib.rs | 5 + crates/warp-core/src/revelation.rs | 278 +++++++++++++++++++++++++++++ 2 files changed, 283 insertions(+) create mode 100644 crates/warp-core/src/revelation.rs diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 573f76da..b084e2c4 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -142,6 +142,7 @@ mod receipt; mod record; mod retained_evidence; mod retention; +mod revelation; mod rule; mod sandbox; mod scheduler; @@ -306,6 +307,10 @@ pub use provenance_store::{ }; pub use receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry, TickReceiptRejection}; pub use record::{EdgeRecord, NodeRecord}; +pub use revelation::{ + least_revealed, promote_posture, shell_posture_obstruction, PostureObstruction, + PosturePromotion, RevelationPosture, +}; #[cfg(feature = "native_rule_bootstrap")] pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; pub use sandbox::DeterminismError; diff --git a/crates/warp-core/src/revelation.rs b/crates/warp-core/src/revelation.rs new file mode 100644 index 00000000..c6c6925e --- /dev/null +++ b/crates/warp-core/src/revelation.rs @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! Revelation posture for retained causal artifacts (Three-Tier Thinking +//! Room, AIΩN Paper VII §6.3; tracked by echo#538). +//! +//! Every retained shell-family artifact carries an explicit revelation +//! posture instead of implicit shared visibility: +//! +//! - [`RevelationPosture::Scratch`] — local, weakly retained, disposable. +//! - [`RevelationPosture::AuthorOnly`] — durable and replayable, sealed to +//! the creating principal until explicitly promoted. +//! - [`RevelationPosture::Shared`] — collaboratively admitted visibility. +//! +//! Posture is load-bearing, not cosmetic. Two laws are enforced here: +//! +//! 1. **Promotion is explicit and witnessed.** Posture only widens through +//! [`promote_posture`], which demands a witness digest; silent widening +//! and any narrowing are obstructions, never no-ops. +//! 2. **Least-revealed-member invariant.** A composite artifact (for +//! example a braid shell over member strands) cannot reveal more than +//! its least-revealed member unless a witnessed redaction/promotion +//! transform exists; [`shell_posture_obstruction`] is the single +//! admission check for that rule. +//! +//! This module is the E0-lite core required by design packet 0026 before +//! any θ_braid shell lands; the full strand-creation posture system +//! remains echo#538. + +use crate::ident::Hash; + +/// Revelation tier for one retained causal artifact. +/// +/// Ordering is revelation breadth: `Scratch < AuthorOnly < Shared`. The +/// default is [`RevelationPosture::AuthorOnly`] so nothing ships with +/// implicit shared visibility. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub enum RevelationPosture { + /// Local, weakly retained, disposable working tier. + Scratch, + /// Durable and replayable, sealed to the creating principal. + #[default] + AuthorOnly, + /// Collaboratively admitted visibility. + Shared, +} + +impl RevelationPosture { + /// Stable wire tag for canonical serialization and digest domains. + #[must_use] + pub fn canonical_tag(self) -> u8 { + match self { + Self::Scratch => 0x01, + Self::AuthorOnly => 0x02, + Self::Shared => 0x03, + } + } +} + +/// Witnessed record that one artifact's posture lawfully widened. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct PosturePromotion { + /// Posture before the promotion act. + pub from: RevelationPosture, + /// Posture after the promotion act. + pub to: RevelationPosture, + /// Witness digest binding the explicit promotion act. + pub witness: Hash, +} + +/// Obstruction raised when a posture act is unlawful. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PostureObstruction { + /// Posture may only widen; narrowing is never a promotion. + NarrowingRefused { + /// Posture the artifact currently holds. + from: RevelationPosture, + /// Narrower posture that was unlawfully requested. + requested: RevelationPosture, + }, + /// Promotion to the same posture is a no-op dressed as an act. + AlreadyAtPosture { + /// Posture the artifact already holds. + posture: RevelationPosture, + }, + /// A composite shell may not reveal more than its least-revealed member. + ExceedsLeastRevealedMember { + /// Posture requested for the composite shell. + shell: RevelationPosture, + /// Least-revealed posture among the shell's members. + least_revealed_member: RevelationPosture, + }, +} + +/// Returns the least-revealed posture among `members`. +/// +/// An empty member set has no revelation to leak, so it imposes no bound; +/// this returns `None` and callers treat the shell posture as the only +/// constraint. +#[must_use] +pub fn least_revealed(members: I) -> Option +where + I: IntoIterator, +{ + members.into_iter().min() +} + +/// Checks the least-revealed-member invariant for a composite shell. +/// +/// Returns the obstruction when `shell` would reveal more than the +/// least-revealed member; `None` means the posture is admissible. +#[must_use] +pub fn shell_posture_obstruction( + shell: RevelationPosture, + members: I, +) -> Option +where + I: IntoIterator, +{ + let floor = least_revealed(members)?; + if shell > floor { + return Some(PostureObstruction::ExceedsLeastRevealedMember { + shell, + least_revealed_member: floor, + }); + } + None +} + +/// Performs one explicit, witnessed posture promotion. +/// +/// Promotion only widens posture. Narrowing and same-posture requests are +/// obstructions: a posture change must always be a real, witnessed act. +/// +/// # Errors +/// +/// Returns [`PostureObstruction::NarrowingRefused`] when `to` is narrower +/// than `from`, and [`PostureObstruction::AlreadyAtPosture`] when `to` +/// equals `from`. +pub fn promote_posture( + from: RevelationPosture, + to: RevelationPosture, + witness: Hash, +) -> Result { + if to < from { + return Err(PostureObstruction::NarrowingRefused { + from, + requested: to, + }); + } + if to == from { + return Err(PostureObstruction::AlreadyAtPosture { posture: from }); + } + Ok(PosturePromotion { from, to, witness }) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn witness() -> Hash { + [0xA7; 32] + } + + #[test] + fn posture_defaults_to_author_only() { + assert_eq!(RevelationPosture::default(), RevelationPosture::AuthorOnly); + } + + #[test] + fn revelation_breadth_orders_scratch_below_author_only_below_shared() { + assert!(RevelationPosture::Scratch < RevelationPosture::AuthorOnly); + assert!(RevelationPosture::AuthorOnly < RevelationPosture::Shared); + } + + #[test] + fn canonical_tags_are_stable() { + assert_eq!(RevelationPosture::Scratch.canonical_tag(), 0x01); + assert_eq!(RevelationPosture::AuthorOnly.canonical_tag(), 0x02); + assert_eq!(RevelationPosture::Shared.canonical_tag(), 0x03); + } + + #[test] + fn least_revealed_finds_the_floor() { + assert_eq!( + least_revealed([ + RevelationPosture::Shared, + RevelationPosture::Scratch, + RevelationPosture::AuthorOnly, + ]), + Some(RevelationPosture::Scratch) + ); + assert_eq!(least_revealed([]), None); + } + + #[test] + fn shell_cannot_reveal_more_than_least_revealed_member() { + let obstruction = shell_posture_obstruction( + RevelationPosture::Shared, + [RevelationPosture::Shared, RevelationPosture::AuthorOnly], + ); + + assert_eq!( + obstruction, + Some(PostureObstruction::ExceedsLeastRevealedMember { + shell: RevelationPosture::Shared, + least_revealed_member: RevelationPosture::AuthorOnly, + }) + ); + } + + #[test] + fn shell_at_or_below_member_floor_is_admissible() { + assert_eq!( + shell_posture_obstruction( + RevelationPosture::AuthorOnly, + [RevelationPosture::Shared, RevelationPosture::AuthorOnly], + ), + None + ); + assert_eq!( + shell_posture_obstruction(RevelationPosture::Scratch, [RevelationPosture::AuthorOnly],), + None + ); + } + + #[test] + fn empty_member_set_imposes_no_floor() { + assert_eq!( + shell_posture_obstruction(RevelationPosture::Shared, []), + None + ); + } + + #[test] + fn promotion_widens_with_witness() { + assert_eq!( + promote_posture( + RevelationPosture::AuthorOnly, + RevelationPosture::Shared, + witness(), + ), + Ok(PosturePromotion { + from: RevelationPosture::AuthorOnly, + to: RevelationPosture::Shared, + witness: witness(), + }) + ); + } + + #[test] + fn narrowing_is_refused_not_silently_applied() { + assert_eq!( + promote_posture( + RevelationPosture::Shared, + RevelationPosture::AuthorOnly, + witness(), + ), + Err(PostureObstruction::NarrowingRefused { + from: RevelationPosture::Shared, + requested: RevelationPosture::AuthorOnly, + }) + ); + } + + #[test] + fn promotion_to_same_posture_is_an_obstruction_not_a_noop() { + assert_eq!( + promote_posture( + RevelationPosture::AuthorOnly, + RevelationPosture::AuthorOnly, + witness(), + ), + Err(PostureObstruction::AlreadyAtPosture { + posture: RevelationPosture::AuthorOnly, + }) + ); + } +} From 7238ca59bf3ded7f9fabba2624c4b6b3435a1510 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 12:22:53 -0700 Subject: [PATCH 05/14] =?UTF-8?q?test(warp-core):=20RED=20=E2=80=94=20plur?= =?UTF-8?q?al=20settlement=20arm=20and=20retained=20plural=20artifact=20(0?= =?UTF-8?q?026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Type surface + failing tests for SettlementDecision::Plural per the approved 0026 test plan (items 1, 5, 7 + regression pin): - SettlementDecision::PluralAlternative(PluralAlternativeDraft) lowering to the existing AdmissionOutcomeKind::Plural; draft carries source ref, contended slots, policy id, and RevelationPosture (default AuthorOnly) - SettlementPolicy { policy_id, plural } separates plural-settlement law from any future collapse policy; Default = Refused (today's behavior) - plan_with_policy / settle_with_policy entries; plan()/settle() delegate with the default policy — legacy paths regression-pinned by default_policy_plan_matches_legacy_plan_exactly - ProvenanceEventKind::PluralArtifact retained-residue event kind; append_plural_artifact mirrors conflict retention (no-op patch — the base never silently takes the strand's value) - ConflictReason::PluralUpstream (code 6): later suffix entries cannot import past retained plurality - ABI: PluralAlternativeDraft + PluralAlternative variant + RevelationPosture + ConflictReason::PluralUpstream + SettlementResult.appended_plurals (serde default — wire-additive) RED: settlement_plans_plural_alternative_under_explicit_plural_policy and settlement_retains_plural_artifact_without_collapsing_base_state fail (plan still emits conflict under plural policy); 7 legacy tests green. GREEN flips the overlap-not-clean branch next. --- crates/echo-wasm-abi/src/kernel_port.rs | 42 +++ crates/warp-core/src/coordinator.rs | 4 + crates/warp-core/src/lib.rs | 5 +- crates/warp-core/src/provenance_store.rs | 5 + crates/warp-core/src/settlement.rs | 319 ++++++++++++++++++++++- crates/warp-core/src/witnessed_suffix.rs | 1 + 6 files changed, 371 insertions(+), 5 deletions(-) diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs index 32d6955d..8f82ad55 100644 --- a/crates/echo-wasm-abi/src/kernel_port.rs +++ b/crates/echo-wasm-abi/src/kernel_port.rs @@ -2065,6 +2065,21 @@ pub enum ConflictReason { ParentFootprintOverlap, /// The source and target lanes disagree on time-quantum assumptions. QuantumMismatch, + /// An earlier suffix entry remained lawfully plural; later entries cannot + /// import past retained plurality. + PluralUpstream, +} + +/// Revelation tier carried by retained settlement artifacts. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum RevelationPosture { + /// Local, weakly retained, disposable working tier. + Scratch, + /// Durable and replayable, sealed to the creating principal. + AuthorOnly, + /// Collaboratively admitted visibility. + Shared, } /// Parent-basis posture used while comparing or planning settlement. @@ -2188,6 +2203,25 @@ pub struct ConflictArtifactDraft { pub overlap_revalidation: Option, } +/// One lawful plural alternative retained at settlement scope. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PluralAlternativeDraft { + /// Stable plural artifact identifier for this retained alternative. + pub plural_id: Vec, + /// Source provenance coordinate whose claim remains lawfully plural. + pub source_ref: ProvenanceRef, + /// Channels implicated by the plural source entry. + pub channel_ids: Vec>, + /// Count of contended slots over which plurality remained lawful. + pub overlapping_slot_count: u64, + /// Deterministic digest of the contended slots. + pub overlapping_slots_digest: Vec, + /// Plural-settlement policy that made this plurality lawful. + pub policy_id: Vec, + /// Revelation posture carried by the retained alternative. + pub posture: RevelationPosture, +} + /// One deterministic settlement decision. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "snake_case")] @@ -2202,6 +2236,11 @@ pub enum SettlementDecision { /// Residue detail. artifact: ConflictArtifactDraft, }, + /// Source history retained as a lawful plural alternative. + PluralAlternative { + /// Retained plural alternative detail. + artifact: PluralAlternativeDraft, + }, } /// Deterministic settlement evaluation for one strand against its base worldline. @@ -2228,6 +2267,9 @@ pub struct SettlementResult { pub appended_imports: Vec, /// Target-worldline refs appended as `ConflictArtifact`. pub appended_conflicts: Vec, + /// Target-worldline refs appended as `PluralArtifact`. + #[serde(default)] + pub appended_plurals: Vec, } /// Compact shell for judging a witnessed suffix without transport or sync. diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index ccd0d667..b45d4906 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -2435,6 +2435,10 @@ fn hash_provenance_event_kind(hasher: &mut blake3::Hasher, event_kind: &Provenan hasher.update(b"conflict-artifact"); hasher.update(artifact_id); } + ProvenanceEventKind::PluralArtifact { plural_id } => { + hasher.update(b"plural-artifact"); + hasher.update(plural_id); + } } } diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index b084e2c4..33ea094d 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -322,8 +322,9 @@ pub use serializable::{ SerializableReceipt, SerializableReceiptEntry, SerializableSnapshot, SerializableTick, }; pub use settlement::{ - ConflictArtifactDraft, ConflictReason, ImportCandidate, SettlementDecision, SettlementDelta, - SettlementError, SettlementPlan, SettlementResult, SettlementService, + ConflictArtifactDraft, ConflictReason, ImportCandidate, PluralAlternativeDraft, + PluralSettlementPolicy, SettlementDecision, SettlementDelta, SettlementError, SettlementPlan, + SettlementPolicy, SettlementResult, SettlementService, }; pub use snapshot::{ compute_commit_hash_v2, compute_emissions_digest, compute_op_emission_index_digest, diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index e828532d..43a66c89 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -453,6 +453,11 @@ pub enum ProvenanceEventKind { /// Stable conflict artifact id. artifact_id: Hash, }, + /// A lawful plural alternative retained at settlement scope. + PluralArtifact { + /// Stable plural artifact id. + plural_id: Hash, + }, } /// Single authoritative provenance record for one worldline step. diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 222686cd..fed20105 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -18,6 +18,7 @@ use crate::provenance_store::{ ProvenanceEventKind, ProvenanceRef, ProvenanceService, ProvenanceStore, }; use crate::record::{EdgeRecord, NodeRecord}; +use crate::revelation::RevelationPosture; use crate::snapshot::{compute_commit_hash_v2, compute_state_root_for_warp_state}; use crate::strand::{ StrandBasisReport, StrandError, StrandId, StrandOverlapRevalidation, StrandRegistry, @@ -30,6 +31,7 @@ use crate::worldline::{ use crate::WorldlineState; const CONFLICT_ARTIFACT_DOMAIN: &[u8] = b"echo:settlement-conflict-artifact:v1\0"; +const REFUSE_PLURAL_POLICY_DOMAIN: &[u8] = b"echo:settlement-policy:refuse-plural:v1\0"; /// Deterministic reasons a source settlement step could not be imported. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -44,6 +46,9 @@ pub enum ConflictReason { ParentFootprintOverlap, /// The source and target lanes disagree on time-quantum assumptions. QuantumMismatch, + /// An earlier suffix entry remained lawfully plural; later entries cannot + /// import past retained plurality. + PluralUpstream, } impl ConflictReason { @@ -54,6 +59,7 @@ impl ConflictReason { Self::BaseDivergence => 3, Self::ParentFootprintOverlap => 4, Self::QuantumMismatch => 5, + Self::PluralUpstream => 6, } } @@ -64,6 +70,54 @@ impl ConflictReason { Self::BaseDivergence => abi::ConflictReason::BaseDivergence, Self::ParentFootprintOverlap => abi::ConflictReason::ParentFootprintOverlap, Self::QuantumMismatch => abi::ConflictReason::QuantumMismatch, + Self::PluralUpstream => abi::ConflictReason::PluralUpstream, + } + } +} + +/// Settlement-scope plural law: when may multiple lawful alternatives survive? +#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] +pub enum PluralSettlementPolicy { + /// Plurality is refused; contended overlap remains explicit conflict + /// residue. This is today's behavior and the default. + #[default] + Refused, + /// Contended footprint overlap where each claim applies lawfully on its + /// own may be retained as plural alternatives instead of conflict. + AllowOverFootprintOverlap, +} + +/// Named law governing one settlement act. +/// +/// The plural-settlement policy decides whether plurality may *exist*; it is +/// deliberately separate from any future collapse policy, which would decide +/// whether retained plurality may later become `Derived` (design 0026). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SettlementPolicy { + /// Stable policy identity committed into retained plural artifacts. + pub policy_id: Hash, + /// Plural-settlement law selected for this act. + pub plural: PluralSettlementPolicy, +} + +impl SettlementPolicy { + /// Policy that permits lawful plurality over contended footprint overlap. + #[must_use] + pub fn allow_plural_over_footprint_overlap(policy_id: Hash) -> Self { + Self { + policy_id, + plural: PluralSettlementPolicy::AllowOverFootprintOverlap, + } + } +} + +impl Default for SettlementPolicy { + fn default() -> Self { + let mut hasher = Hasher::new(); + hasher.update(REFUSE_PLURAL_POLICY_DOMAIN); + Self { + policy_id: hasher.finalize().into(), + plural: PluralSettlementPolicy::Refused, } } } @@ -148,6 +202,8 @@ pub enum SettlementDecision { ImportCandidate(ImportCandidate), /// Source history that must remain explicit residue. ConflictArtifact(ConflictArtifactDraft), + /// Source history retained as a lawful plural alternative. + PluralAlternative(PluralAlternativeDraft), } impl SettlementDecision { @@ -157,6 +213,7 @@ impl SettlementDecision { match self { Self::ImportCandidate(_) => AdmissionOutcomeKind::Derived, Self::ConflictArtifact(_) => AdmissionOutcomeKind::Conflict, + Self::PluralAlternative(_) => AdmissionOutcomeKind::Plural, } } @@ -168,6 +225,9 @@ impl SettlementDecision { Self::ConflictArtifact(artifact) => abi::SettlementDecision::ConflictArtifact { artifact: artifact.to_abi(), }, + Self::PluralAlternative(artifact) => abi::SettlementDecision::PluralAlternative { + artifact: artifact.to_abi(), + }, } } } @@ -201,6 +261,52 @@ impl ImportCandidate { } } +/// One lawful plural alternative retained at settlement scope. +/// +/// Plurality is not indecision: each alternative applied lawfully on its own, +/// the claims contend over the same slots, and an explicit plural-settlement +/// policy made their coexistence lawful. The base worldline never silently +/// takes the strand's value; the alternative is retained as residue with its +/// own provenance entry. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PluralAlternativeDraft { + /// Stable plural artifact identifier for this retained alternative. + pub plural_id: Hash, + /// Source provenance coordinate whose claim remains lawfully plural. + pub source_ref: ProvenanceRef, + /// Channels implicated by the plural source entry. + pub channel_ids: Vec, + /// Contended slots over which plurality remained lawful. + pub overlapping_slots: Vec, + /// Plural-settlement policy that made this plurality lawful. + pub policy_id: Hash, + /// Revelation posture carried by the retained alternative. + pub posture: RevelationPosture, +} + +impl PluralAlternativeDraft { + fn to_abi(&self) -> abi::PluralAlternativeDraft { + abi::PluralAlternativeDraft { + plural_id: self.plural_id.to_vec(), + source_ref: provenance_ref_to_abi(self.source_ref), + channel_ids: self + .channel_ids + .iter() + .map(|channel_id| channel_id.0.to_vec()) + .collect(), + overlapping_slot_count: self.overlapping_slots.len() as u64, + overlapping_slots_digest: settlement_overlap_slots_digest(&self.overlapping_slots) + .to_vec(), + policy_id: self.policy_id.to_vec(), + posture: match self.posture { + RevelationPosture::Scratch => abi::RevelationPosture::Scratch, + RevelationPosture::AuthorOnly => abi::RevelationPosture::AuthorOnly, + RevelationPosture::Shared => abi::RevelationPosture::Shared, + }, + } + } +} + /// One unresolved settlement residue draft. #[derive(Clone, Debug, PartialEq, Eq)] pub struct ConflictArtifactDraft { @@ -244,6 +350,8 @@ pub struct SettlementResult { pub appended_imports: Vec, /// Target-worldline refs appended as `ConflictArtifact`. pub appended_conflicts: Vec, + /// Target-worldline refs appended as `PluralArtifact`. + pub appended_plurals: Vec, } impl SettlementResult { @@ -264,6 +372,12 @@ impl SettlementResult { .copied() .map(provenance_ref_to_abi) .collect(), + appended_plurals: self + .appended_plurals + .iter() + .copied() + .map(provenance_ref_to_abi) + .collect(), } } } @@ -541,6 +655,17 @@ impl SettlementService { provenance: &ProvenanceService, strand_id: StrandId, ) -> Result { + Self::plan_with_policy(runtime, provenance, strand_id, &SettlementPolicy::default()) + } + + /// Produces a deterministic settlement plan under an explicit named policy. + pub fn plan_with_policy( + runtime: &WorldlineRuntime, + provenance: &ProvenanceService, + strand_id: StrandId, + policy: &SettlementPolicy, + ) -> Result { + let _ = policy; let strand = strand(runtime.strands(), strand_id)?; let delta = Self::compare(runtime, provenance, strand_id)?; let target_worldline = strand.fork_basis_ref.source_lane_id; @@ -696,12 +821,23 @@ impl SettlementService { provenance: &mut ProvenanceService, strand_id: StrandId, ) -> Result { - let plan = Self::plan(runtime, provenance, strand_id)?; + Self::settle_with_policy(runtime, provenance, strand_id, &SettlementPolicy::default()) + } + + /// Executes the deterministic settlement plan under an explicit named policy. + pub fn settle_with_policy( + runtime: &mut WorldlineRuntime, + provenance: &mut ProvenanceService, + strand_id: StrandId, + policy: &SettlementPolicy, + ) -> Result { + let plan = Self::plan_with_policy(runtime, provenance, strand_id, policy)?; if plan.decisions.is_empty() { return Ok(SettlementResult { plan, appended_imports: Vec::new(), appended_conflicts: Vec::new(), + appended_plurals: Vec::new(), }); } @@ -710,6 +846,7 @@ impl SettlementService { let outcome = (|| { let mut appended_imports = Vec::new(); let mut appended_conflicts = Vec::new(); + let mut appended_plurals = Vec::new(); for decision in &plan.decisions { let commit_global_tick = runtime.advance_global_tick()?; @@ -728,12 +865,22 @@ impl SettlementService { draft, commit_global_tick, )?, + SettlementDecision::PluralAlternative(draft) => append_plural_artifact( + runtime, + provenance, + plan.target_worldline, + draft, + commit_global_tick, + )?, }; match decision { SettlementDecision::ImportCandidate(_) => appended_imports.push(appended_ref), SettlementDecision::ConflictArtifact(_) => { appended_conflicts.push(appended_ref); } + SettlementDecision::PluralAlternative(_) => { + appended_plurals.push(appended_ref); + } } } @@ -741,6 +888,7 @@ impl SettlementService { plan, appended_imports, appended_conflicts, + appended_plurals, }) })(); @@ -930,6 +1078,44 @@ fn append_conflict_artifact( ) } +fn append_plural_artifact( + runtime: &mut WorldlineRuntime, + provenance: &mut ProvenanceService, + target_worldline: WorldlineId, + draft: &PluralAlternativeDraft, + commit_global_tick: GlobalTick, +) -> Result { + let warp_id = runtime + .worldlines() + .get(&target_worldline) + .ok_or(RuntimeError::UnknownWorldline(target_worldline))? + .state() + .root() + .warp_id; + let no_op_patch = empty_worldline_patch(warp_id, commit_global_tick); + let expected_state_root = runtime + .worldlines() + .get(&target_worldline) + .ok_or(RuntimeError::UnknownWorldline(target_worldline))? + .state() + .state_root(); + append_recorded_entry( + runtime, + provenance, + target_worldline, + RecordedEntryDraft { + event_kind: ProvenanceEventKind::PluralArtifact { + plural_id: draft.plural_id, + }, + patch: no_op_patch, + expected_state_root, + outputs: Vec::new(), + atom_writes: Vec::new(), + source_ref: None, + }, + ) +} + fn append_recorded_entry( runtime: &mut WorldlineRuntime, provenance: &mut ProvenanceService, @@ -1809,14 +1995,141 @@ mod tests { fn import_candidate(decision: &SettlementDecision) -> Option<&ImportCandidate> { match decision { SettlementDecision::ImportCandidate(candidate) => Some(candidate), - SettlementDecision::ConflictArtifact(_) => None, + SettlementDecision::ConflictArtifact(_) | SettlementDecision::PluralAlternative(_) => { + None + } } } fn conflict_artifact(decision: &SettlementDecision) -> Option<&ConflictArtifactDraft> { match decision { SettlementDecision::ConflictArtifact(draft) => Some(draft), - SettlementDecision::ImportCandidate(_) => None, + SettlementDecision::ImportCandidate(_) | SettlementDecision::PluralAlternative(_) => { + None + } + } + } + + fn plural_alternative(decision: &SettlementDecision) -> Option<&PluralAlternativeDraft> { + match decision { + SettlementDecision::PluralAlternative(draft) => Some(draft), + SettlementDecision::ImportCandidate(_) | SettlementDecision::ConflictArtifact(_) => { + None + } } } + + fn plural_policy() -> SettlementPolicy { + SettlementPolicy::allow_plural_over_footprint_overlap([0x5E; 32]) + } + + #[test] + fn settlement_plans_plural_alternative_under_explicit_plural_policy() { + let (runtime, provenance, strand_id, _, child_worldline) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let plan = + SettlementService::plan_with_policy(&runtime, &provenance, strand_id, &plural_policy()) + .unwrap(); + assert_eq!(plan.decisions.len(), 1); + assert_eq!( + plan.decisions[0].admission_outcome_kind(), + AdmissionOutcomeKind::Plural, + "contended-but-individually-lawful overlap under explicit plural \ + policy must remain lawfully plural, not conflict" + ); + let Some(draft) = plural_alternative(&plan.decisions[0]) else { + assert!( + matches!(&plan.decisions[0], SettlementDecision::PluralAlternative(_)), + "expected a retained plural alternative under explicit plural policy" + ); + return; + }; + assert_eq!(draft.policy_id, plural_policy().policy_id); + assert_eq!(draft.posture, RevelationPosture::AuthorOnly); + assert!(!draft.overlapping_slots.is_empty()); + assert_eq!(draft.source_ref.worldline_id, child_worldline); + + let replanned = + SettlementService::plan_with_policy(&runtime, &provenance, strand_id, &plural_policy()) + .unwrap(); + assert_eq!(replanned, plan, "plural planning must be deterministic"); + } + + #[test] + fn settlement_retains_plural_artifact_without_collapsing_base_state() { + let (mut runtime, mut provenance, strand_id, base_worldline, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let result = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + assert_eq!(result.appended_plurals.len(), 1); + assert!(result.appended_imports.is_empty()); + assert!(result.appended_conflicts.is_empty()); + + let retained = provenance.entry(base_worldline, wt(2)).unwrap(); + assert!(matches!( + retained.event_kind, + ProvenanceEventKind::PluralArtifact { .. } + )); + + // No silent collapse: the base worldline keeps its own claim. + let state = runtime.worldlines().get(&base_worldline).unwrap().state(); + let root_warp = state.root().warp_id; + let node = state + .store(&root_warp) + .unwrap() + .node(&make_node_id("child-node")) + .unwrap() + .clone(); + assert_eq!(node.ty, make_type_id("parent-node")); + } + + #[test] + fn default_policy_plan_matches_legacy_plan_exactly() { + let (runtime, provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let legacy = SettlementService::plan(&runtime, &provenance, strand_id).unwrap(); + let defaulted = SettlementService::plan_with_policy( + &runtime, + &provenance, + strand_id, + &SettlementPolicy::default(), + ) + .unwrap(); + + assert_eq!(defaulted, legacy); + assert!( + matches!( + &legacy.decisions[0], + SettlementDecision::ConflictArtifact(_) + ), + "absent plural-settlement policy, incompatible overlap remains Conflict" + ); + } + + #[test] + fn plural_policy_leaves_clean_overlap_imports_derived() { + let (runtime, provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapSame); + + let plan = + SettlementService::plan_with_policy(&runtime, &provenance, strand_id, &plural_policy()) + .unwrap(); + assert_eq!(plan.decisions.len(), 1); + assert!( + matches!(&plan.decisions[0], SettlementDecision::ImportCandidate(_)), + "plural policy must not pluralize overlap that revalidates clean" + ); + assert_eq!( + plan.decisions[0].admission_outcome_kind(), + AdmissionOutcomeKind::Derived + ); + } } diff --git a/crates/warp-core/src/witnessed_suffix.rs b/crates/warp-core/src/witnessed_suffix.rs index edbfd667..f2e43e65 100644 --- a/crates/warp-core/src/witnessed_suffix.rs +++ b/crates/warp-core/src/witnessed_suffix.rs @@ -1088,6 +1088,7 @@ fn conflict_reason_to_abi(reason: ConflictReason) -> abi::ConflictReason { ConflictReason::BaseDivergence => abi::ConflictReason::BaseDivergence, ConflictReason::ParentFootprintOverlap => abi::ConflictReason::ParentFootprintOverlap, ConflictReason::QuantumMismatch => abi::ConflictReason::QuantumMismatch, + ConflictReason::PluralUpstream => abi::ConflictReason::PluralUpstream, } } From fc6d65a1b880e0ce66757031c69b956cca896773 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 12:25:21 -0700 Subject: [PATCH 06/14] =?UTF-8?q?feat(warp-core):=20GREEN=20=E2=80=94=20la?= =?UTF-8?q?wful=20plural=20settlement=20over=20contended=20footprint=20ove?= =?UTF-8?q?rlap=20(0026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flips the overlap-not-clean branch: when each claim applied lawfully on its own and an explicit plural-settlement policy permits coexistence, the strand's claim is retained as SettlementDecision::PluralAlternative instead of conflict residue. The base worldline never silently takes the strand's value; later suffix entries are blocked with ConflictReason::PluralUpstream rather than importing past retained plurality. plural_id is domain-separated (echo:settlement-plural-artifact:v1) over target worldline, source coordinate, contended-slots digest, and policy id — deterministic across runs and input orderings of the same facts. All 9 settlement tests green (2 RED tests now pass; 7 legacy pinned); workspace clean under -D warnings. --- crates/warp-core/src/settlement.rs | 58 +++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index fed20105..af48f655 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -31,6 +31,7 @@ use crate::worldline::{ use crate::WorldlineState; const CONFLICT_ARTIFACT_DOMAIN: &[u8] = b"echo:settlement-conflict-artifact:v1\0"; +const PLURAL_ARTIFACT_DOMAIN: &[u8] = b"echo:settlement-plural-artifact:v1\0"; const REFUSE_PLURAL_POLICY_DOMAIN: &[u8] = b"echo:settlement-policy:refuse-plural:v1\0"; /// Deterministic reasons a source settlement step could not be imported. @@ -665,7 +666,6 @@ impl SettlementService { strand_id: StrandId, policy: &SettlementPolicy, ) -> Result { - let _ = policy; let strand = strand(runtime.strands(), strand_id)?; let delta = Self::compare(runtime, provenance, strand_id)?; let target_worldline = strand.fork_basis_ref.source_lane_id; @@ -781,6 +781,20 @@ impl SettlementService { Some(StrandOverlapRevalidation::Clean { overlapping_slots: entry_overlap_slots, }) + } else if policy.plural == PluralSettlementPolicy::AllowOverFootprintOverlap { + // Each claim applied lawfully on its own; the contention is + // retained as a lawful plural alternative instead of conflict. + // The base worldline never silently takes the strand's value, + // and later suffix entries cannot import past retained + // plurality. + blocked_reason = Some(ConflictReason::PluralUpstream); + decisions.push(SettlementDecision::PluralAlternative(plural_draft( + target_worldline, + &source_entry, + entry_overlap_slots, + policy, + ))); + continue; } else { blocked_reason = Some(ConflictReason::ParentFootprintOverlap); decisions.push(SettlementDecision::ConflictArtifact( @@ -1225,6 +1239,48 @@ fn conflict_draft_with_revalidation( } } +fn plural_draft( + target_worldline: WorldlineId, + source_entry: &ProvenanceEntry, + overlapping_slots: Vec, + policy: &SettlementPolicy, +) -> PluralAlternativeDraft { + PluralAlternativeDraft { + plural_id: compute_plural_artifact_id( + target_worldline, + source_entry.as_ref(), + &overlapping_slots, + policy.policy_id, + ), + source_ref: source_entry.as_ref(), + channel_ids: source_entry + .outputs + .iter() + .map(|(channel, _)| *channel) + .collect(), + overlapping_slots, + policy_id: policy.policy_id, + posture: RevelationPosture::default(), + } +} + +fn compute_plural_artifact_id( + target_worldline: WorldlineId, + source_ref: ProvenanceRef, + overlapping_slots: &[SlotId], + policy_id: Hash, +) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(PLURAL_ARTIFACT_DOMAIN); + hasher.update(target_worldline.as_bytes()); + hasher.update(source_ref.worldline_id.as_bytes()); + hasher.update(&source_ref.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&source_ref.commit_hash); + hasher.update(&settlement_overlap_slots_digest(overlapping_slots)); + hasher.update(&policy_id); + hasher.finalize().into() +} + fn compute_conflict_artifact_id( target_worldline: WorldlineId, source_ref: ProvenanceRef, From 40413ce93fcf80cebf0c7d03455f7dee05cf60e5 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 12:47:18 -0700 Subject: [PATCH 07/14] fix(warp-core): canonical slot ordering for plural digests + E1a doctrine notes Checkpoint review corrections (0026): - settlement_overlap_slots_digest sorts slots by canonical byte key before hashing; plural_draft canonicalizes the retained slot vec. New test: slot permutation cannot move plural_id or overlap digest. - Packet records the E1a hierarchy invariant (PluralAlternative is per-entry residue; theta_braid is the plural settlement boundary; BraidShell is the replayable outcome) plus named debts: thin PluralArtifact event kind, PluralUpstream->Obstruction migration, posture witness quality bar. --- crates/warp-core/src/settlement.rs | 110 +++++++++++++----- .../design.md | 24 ++++ 2 files changed, 103 insertions(+), 31 deletions(-) diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index af48f655..ebd892d4 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -463,53 +463,66 @@ fn overlap_revalidation_to_abi( } } -fn settlement_overlap_slots_digest(slots: &[SlotId]) -> Hash { - let mut hasher = Hasher::new(); - hasher.update(b"echo:settlement-overlap-slots:v1\0"); - hasher.update(&(slots.len() as u64).to_le_bytes()); - for slot in slots { - hash_settlement_slot(&mut hasher, slot); - } - hasher.finalize().into() -} - -fn hash_settlement_slot(hasher: &mut Hasher, slot: &SlotId) { +/// Canonical byte key for one settlement slot. +/// +/// Slot digests and retained plural facts must not depend on upstream +/// iteration order, so every multi-slot digest sorts by this key first. +fn canonical_slot_bytes(slot: &SlotId) -> Vec { + let mut bytes = Vec::new(); match slot { SlotId::Node(node) => { - hasher.update(&[1]); - hasher.update(node.warp_id.as_bytes()); - hasher.update(node.local_id.as_bytes()); + bytes.push(1); + bytes.extend_from_slice(node.warp_id.as_bytes()); + bytes.extend_from_slice(node.local_id.as_bytes()); } SlotId::Edge(edge) => { - hasher.update(&[2]); - hasher.update(edge.warp_id.as_bytes()); - hasher.update(edge.local_id.as_bytes()); + bytes.push(2); + bytes.extend_from_slice(edge.warp_id.as_bytes()); + bytes.extend_from_slice(edge.local_id.as_bytes()); } SlotId::Attachment(attachment) => { - hasher.update(&[3]); + bytes.push(3); match attachment.owner { AttachmentOwner::Node(node) => { - hasher.update(&[1]); - hasher.update(node.warp_id.as_bytes()); - hasher.update(node.local_id.as_bytes()); + bytes.push(1); + bytes.extend_from_slice(node.warp_id.as_bytes()); + bytes.extend_from_slice(node.local_id.as_bytes()); } AttachmentOwner::Edge(edge) => { - hasher.update(&[2]); - hasher.update(edge.warp_id.as_bytes()); - hasher.update(edge.local_id.as_bytes()); + bytes.push(2); + bytes.extend_from_slice(edge.warp_id.as_bytes()); + bytes.extend_from_slice(edge.local_id.as_bytes()); } } match attachment.plane { - AttachmentPlane::Alpha => hasher.update(&[1]), - AttachmentPlane::Beta => hasher.update(&[2]), - }; + AttachmentPlane::Alpha => bytes.push(1), + AttachmentPlane::Beta => bytes.push(2), + } } SlotId::Port((warp_id, port_key)) => { - hasher.update(&[4]); - hasher.update(warp_id.as_bytes()); - hasher.update(&port_key.to_le_bytes()); + bytes.push(4); + bytes.extend_from_slice(warp_id.as_bytes()); + bytes.extend_from_slice(&port_key.to_le_bytes()); } } + bytes +} + +/// Sorts slots into canonical digest order. +fn canonicalize_slots(slots: &mut [SlotId]) { + slots.sort_by_key(canonical_slot_bytes); +} + +fn settlement_overlap_slots_digest(slots: &[SlotId]) -> Hash { + let mut encoded: Vec> = slots.iter().map(canonical_slot_bytes).collect(); + encoded.sort_unstable(); + let mut hasher = Hasher::new(); + hasher.update(b"echo:settlement-overlap-slots:v1\0"); + hasher.update(&(encoded.len() as u64).to_le_bytes()); + for slot_bytes in &encoded { + hasher.update(slot_bytes); + } + hasher.finalize().into() } fn provenance_ref_to_abi(reference: ProvenanceRef) -> abi::ProvenanceRef { @@ -1242,9 +1255,10 @@ fn conflict_draft_with_revalidation( fn plural_draft( target_worldline: WorldlineId, source_entry: &ProvenanceEntry, - overlapping_slots: Vec, + mut overlapping_slots: Vec, policy: &SettlementPolicy, ) -> PluralAlternativeDraft { + canonicalize_slots(&mut overlapping_slots); PluralAlternativeDraft { plural_id: compute_plural_artifact_id( target_worldline, @@ -2170,6 +2184,40 @@ mod tests { ); } + #[test] + fn slot_order_does_not_move_plural_id_or_overlap_digest() { + let warp_id = crate::ident::make_warp_id("plural-slot-order"); + let slot_a = SlotId::Node(crate::ident::NodeKey { + warp_id, + local_id: make_node_id("slot-a"), + }); + let slot_b = SlotId::Node(crate::ident::NodeKey { + warp_id, + local_id: make_node_id("slot-b"), + }); + let slot_c = SlotId::Edge(crate::ident::EdgeKey { + warp_id, + local_id: make_edge_id("slot-c"), + }); + + let forward = [slot_a, slot_b, slot_c]; + let reversed = [slot_c, slot_b, slot_a]; + assert_eq!( + settlement_overlap_slots_digest(&forward), + settlement_overlap_slots_digest(&reversed), + ); + + let source_ref = ProvenanceRef { + worldline_id: wl(9), + worldline_tick: wt(1), + commit_hash: [0x42; 32], + }; + assert_eq!( + compute_plural_artifact_id(wl(1), source_ref, &forward, [0x5E; 32]), + compute_plural_artifact_id(wl(1), source_ref, &reversed, [0x5E; 32]), + ); + } + #[test] fn plural_policy_leaves_clean_overlap_imports_derived() { let (runtime, provenance, strand_id, _, _) = diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md index 50418a6c..8289cbce 100644 --- a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -197,6 +197,30 @@ distinguish incompatible-rewrite, missing-plural-policy frontier-fact mismatch, and policy obstruction. No flattening into one reason. +## E1a hierarchy (checkpoint review, 2026-06-12) + +The plural-arm checkpoint landed as per-entry residue plumbing. The +hierarchy must never blur: + +> `PluralAlternative` is an E1a per-entry residue noun. θ_braid is the +> plural settlement boundary. The final replayable plural outcome is the +> `BraidShell`, not the individual `PluralAlternative` event. + +Named debts from the checkpoint review: + +1. **`ProvenanceEventKind::PluralArtifact { plural_id }` is a marker, not + a body.** The durable replayable truth is the θ_braid shell record; + the event kind points at it. Shell facts are never re-derived from + in-memory drafts. +2. **`ConflictReason::PluralUpstream` is temporary residue shape.** Once + `SettlementDecision::Obstruction` exists, suffix entries blocked by + prior retained plurality become Obstruction/PluralDependency rather + than Conflict. +3. **Posture witnesses need a quality bar.** `promote_posture` accepts + any 32-byte value today; before shell promotion law lands, reject + empty/null witness digests or introduce a `WitnessDigest` newtype. + A witness must never be a 32-byte shrug. + ## Acceptance criteria (enhanced per review) 1. `SettlementDecision` gains a `Plural` arm carrying surviving From 82d89060d43bc7296e0ee56288e70dd9b09ee3cd Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 13:01:13 -0700 Subject: [PATCH 08/14] =?UTF-8?q?test(warp-core):=20RED=20=E2=80=94=20thet?= =?UTF-8?q?a=5Fbraid=20shell=20law=20and=20settlement=20emission=20gate=20?= =?UTF-8?q?(0026)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The braid shell module lands fully implemented as pure record law — shells are data discipline, not behavior — with 6 tests green: - BraidShell/BraidShellMember/BraidShellOutcome over the four-arm algebra; collapse is Derived-with-collapsed_from lineage, never a fifth arm; members carry compact digest sets + verdict snapshots, never histories - domain-separated digests (echo.shell.braid.v1, echo.braid.member.v1, echo.braid.witness.v1); canonical member ordering (permutation cannot move the shell digest); witness + shell digests recomputed on validate(), so tampering with policy/posture/verdict/outcome fails - posture floor enforced at assembly and validation (shell cannot reveal more than its least-revealed member) - replay_braid_shell reproduces outcome arm + member verdicts through BraidShellRecords — a trait whose only method returns shells, so strand-history access is a type error; collapse lineage requires a retained Plural parent - ProvenanceService retains shells append-only (validated, idempotent, divergent-content refused) with query iterator and take_braid_shells for hostile replay proofs RED (3 settlement gate tests fail — emission not wired): - plural settlement retains exactly one shell with full body - shell replays after runtime, registry, and provenance are dropped - derived and conflict settlements also retain shells SettlementResult.braid_shell: Option (always None until GREEN); ABI braid_shell_digest serde-default additive. --- crates/echo-wasm-abi/src/kernel_port.rs | 3 + crates/warp-core/src/braid_shell.rs | 784 +++++++++++++++++++++++ crates/warp-core/src/lib.rs | 5 + crates/warp-core/src/provenance_store.rs | 59 ++ crates/warp-core/src/settlement.rs | 130 ++++ 5 files changed, 981 insertions(+) create mode 100644 crates/warp-core/src/braid_shell.rs diff --git a/crates/echo-wasm-abi/src/kernel_port.rs b/crates/echo-wasm-abi/src/kernel_port.rs index 8f82ad55..dfe52e2c 100644 --- a/crates/echo-wasm-abi/src/kernel_port.rs +++ b/crates/echo-wasm-abi/src/kernel_port.rs @@ -2270,6 +2270,9 @@ pub struct SettlementResult { /// Target-worldline refs appended as `PluralArtifact`. #[serde(default)] pub appended_plurals: Vec, + /// Digest of the retained braid shell for this settlement act, if any. + #[serde(default)] + pub braid_shell_digest: Option>, } /// Compact shell for judging a witnessed suffix without transport or sync. diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs new file mode 100644 index 00000000..8b6ceb22 --- /dev/null +++ b/crates/warp-core/src/braid_shell.rs @@ -0,0 +1,784 @@ +// SPDX-License-Identifier: Apache-2.0 +// © James Ross Ω FLYING•ROBOTS +//! θ_braid — retained braid-scale settlement boundary shells. +//! +//! A braid shell is not a note that plurality happened. It is the retained +//! holographic boundary that makes a braid-scope settlement outcome +//! replayable **without reopening member strand histories** (AIΩN Paper VII +//! Prop 3.5; design packet 0026). The shell binds basis + members + member +//! verdicts + policy + outcome + witness with domain-separated digests, and +//! [`replay_braid_shell`] reproduces the outcome from shell records alone — +//! its signature offers no path to strand histories, so rematerialization is +//! a type error, not a temptation. +//! +//! Hierarchy (E1a doctrine): `PluralAlternative` is per-entry residue; +//! θ_braid is the plural settlement boundary; the `BraidShell` is the +//! replayable outcome. Shells are append-only: a later collapse emits a new +//! `Derived` shell referencing its plural parent through `collapsed_from`; +//! it never rewrites the plural shell. + +use blake3::Hasher; + +use crate::admission::AdmissionOutcomeKind; +use crate::ident::Hash; +use crate::provenance_store::ProvenanceRef; +use crate::revelation::{shell_posture_obstruction, PostureObstruction, RevelationPosture}; +use crate::strand::StrandId; +use crate::worldline::WorldlineId; + +const SHELL_DOMAIN: &[u8] = b"echo.shell.braid.v1\0"; +const MEMBER_DOMAIN: &[u8] = b"echo.braid.member.v1\0"; +const WITNESS_DOMAIN: &[u8] = b"echo.braid.witness.v1\0"; + +/// Current braid shell body version. +pub const BRAID_SHELL_VERSION: u32 = 1; + +/// Compact settlement verdict for one braid member. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MemberVerdict { + /// Every claim from this member was imported. + Derived, + /// At least one claim from this member remained lawfully plural. + Plural, + /// At least one claim from this member became conflict residue and none + /// remained plural. + Conflict, + /// The member's claims were obstructed before judgment completed. + Obstructed, +} + +impl MemberVerdict { + const fn tag(self) -> u8 { + match self { + Self::Derived => 1, + Self::Plural => 2, + Self::Conflict => 3, + Self::Obstructed => 4, + } + } +} + +/// One member entry in a braid shell: compact replay facts, never history. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BraidShellMember { + /// Strand whose claims this member summarizes. + pub strand_ref: StrandId, + /// Digest over the member's support-pin set. + pub support_pin_digest: Hash, + /// Digest over the member's fork basis facts. + pub basis_digest: Hash, + /// Digest over the realized parent frontier the member was judged against. + pub frontier_digest: Hash, + /// Digest over the contended footprint slots. + pub footprint_digest: Hash, + /// Digest over the member's ordered claim identities. + pub claim_digest: Hash, + /// Compact settlement verdict for the member. + pub verdict: MemberVerdict, + /// Digest over the member's ordered per-claim decisions. + pub verdict_digest: Hash, + /// Revelation posture carried by the member's claims. + pub posture: RevelationPosture, +} + +impl BraidShellMember { + /// Canonical content digest for this member. + #[must_use] + pub fn member_digest(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(MEMBER_DOMAIN); + hasher.update(self.strand_ref.as_bytes()); + hasher.update(&self.support_pin_digest); + hasher.update(&self.basis_digest); + hasher.update(&self.frontier_digest); + hasher.update(&self.footprint_digest); + hasher.update(&self.claim_digest); + hasher.update(&[self.verdict.tag()]); + hasher.update(&self.verdict_digest); + hasher.update(&[self.posture.canonical_tag()]); + hasher.finalize().into() + } +} + +/// Braid-shell outcome over the shared lawful algebra. +/// +/// Collapse is a witnessed transition, never a fifth arm: a collapse emits a +/// new `Derived` shell whose `collapsed_from` references the plural parent. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BraidShellOutcome { + /// A single lawful result was derived. + Derived { + /// Target-worldline refs realized by the derivation. + result_refs: Vec, + /// Collapse policy when this derivation collapsed retained plurality. + collapse_policy: Option, + /// Plural parent shell this derivation collapsed, when applicable. + collapsed_from: Option, + }, + /// Multiple lawful alternatives remain retained. + Plural { + /// Stable plural artifact ids of the retained alternatives. + alternative_ids: Vec, + }, + /// The settlement produced explicit conflict residue. + Conflict { + /// Deterministic per-claim conflict reason codes in claim order. + reason_codes: Vec, + }, + /// The settlement act was obstructed before judgment completed. + Obstruction { + /// Deterministic obstruction reason code. + reason_code: u8, + /// Witness digest for the obstruction. + witness: Hash, + }, +} + +impl BraidShellOutcome { + /// Maps the shell outcome onto Echo's shared lawful outcome family. + #[must_use] + pub fn kind(&self) -> AdmissionOutcomeKind { + match self { + Self::Derived { .. } => AdmissionOutcomeKind::Derived, + Self::Plural { .. } => AdmissionOutcomeKind::Plural, + Self::Conflict { .. } => AdmissionOutcomeKind::Conflict, + Self::Obstruction { .. } => AdmissionOutcomeKind::Obstruction, + } + } + + fn hash_into(&self, hasher: &mut Hasher) { + match self { + Self::Derived { + result_refs, + collapse_policy, + collapsed_from, + } => { + hasher.update(&[1]); + hasher.update(&(result_refs.len() as u64).to_le_bytes()); + for reference in result_refs { + hash_provenance_ref(hasher, reference); + } + hash_optional_digest(hasher, collapse_policy.as_ref()); + hash_optional_digest(hasher, collapsed_from.as_ref()); + } + Self::Plural { alternative_ids } => { + hasher.update(&[2]); + hasher.update(&(alternative_ids.len() as u64).to_le_bytes()); + for alternative in alternative_ids { + hasher.update(alternative); + } + } + Self::Conflict { reason_codes } => { + hasher.update(&[3]); + hasher.update(&(reason_codes.len() as u64).to_le_bytes()); + hasher.update(reason_codes); + } + Self::Obstruction { + reason_code, + witness, + } => { + hasher.update(&[4]); + hasher.update(&[*reason_code]); + hasher.update(witness); + } + } + } +} + +fn hash_provenance_ref(hasher: &mut Hasher, reference: &ProvenanceRef) { + hasher.update(reference.worldline_id.as_bytes()); + hasher.update(&reference.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&reference.commit_hash); +} + +fn hash_optional_digest(hasher: &mut Hasher, digest: Option<&Hash>) { + match digest { + Some(value) => { + hasher.update(&[1]); + hasher.update(value); + } + None => { + hasher.update(&[0]); + } + } +} + +/// Obstructions raised while assembling, validating, or replaying a shell. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum BraidShellError { + /// A shell must summarize at least one member. + EmptyMembers, + /// The shell posture exceeds its least-revealed member. + PostureExceedsMembers(PostureObstruction), + /// Outcome arm and member verdicts disagree. + OutcomeMemberMismatch { + /// Outcome arm the shell claims. + outcome: AdmissionOutcomeKind, + }, + /// The stored digest does not match the recomputed canonical body. + DigestMismatch { + /// Digest stored on the shell. + stored: Hash, + /// Digest recomputed from the body. + recomputed: Hash, + }, + /// The stored witness digest does not match the recomputed body witness. + WitnessMismatch { + /// Witness digest stored on the shell. + stored: Hash, + /// Witness digest recomputed from the body. + recomputed: Hash, + }, + /// Member entries are not in canonical order. + NonCanonicalMemberOrder, + /// No shell record exists for the requested digest. + ShellNotFound { + /// Digest that resolved to nothing. + digest: Hash, + }, + /// A collapse lineage parent is missing or not plural. + InvalidCollapseLineage { + /// Parent shell digest named by `collapsed_from`. + collapsed_from: Hash, + }, + /// A shell with this digest is already retained with different content. + DuplicateDigestDivergentContent { + /// Digest both shells claim. + digest: Hash, + }, +} + +/// Retained braid-scale settlement boundary (θ_braid). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BraidShell { + /// Shell body version. + pub version: u32, + /// Target worldline the settlement judged into. + pub worldline_id: WorldlineId, + /// Comparison basis the members were judged against. + pub basis: ProvenanceRef, + /// Canonically ordered member entries. + pub members: Vec, + /// Settlement policy identity the act ran under. + pub policy_id: Hash, + /// Outcome arm over the shared lawful algebra. + pub outcome: BraidShellOutcome, + /// Witness digest binding the settlement act. + pub witness_digest: Hash, + /// Revelation posture of the shell itself. + pub posture: RevelationPosture, + /// Canonical content digest of the full shell body. + pub digest: Hash, +} + +impl BraidShell { + /// Assembles a shell: canonicalizes member order, checks the posture + /// floor and outcome/member coherence, and seals witness + digest. + /// + /// # Errors + /// + /// Returns [`BraidShellError`] when the member set is empty, the shell + /// posture exceeds its least-revealed member, or the outcome arm + /// disagrees with member verdicts. + pub fn assemble( + worldline_id: WorldlineId, + basis: ProvenanceRef, + mut members: Vec, + policy_id: Hash, + outcome: BraidShellOutcome, + posture: RevelationPosture, + ) -> Result { + if members.is_empty() { + return Err(BraidShellError::EmptyMembers); + } + members.sort_by_key(BraidShellMember::member_digest); + if let Some(obstruction) = + shell_posture_obstruction(posture, members.iter().map(|member| member.posture)) + { + return Err(BraidShellError::PostureExceedsMembers(obstruction)); + } + check_outcome_member_coherence(&outcome, &members)?; + + let witness_digest = + compute_witness_digest(worldline_id, &basis, &members, policy_id, &outcome, posture); + let digest = compute_shell_digest( + worldline_id, + &basis, + &members, + policy_id, + &outcome, + witness_digest, + posture, + ); + Ok(Self { + version: BRAID_SHELL_VERSION, + worldline_id, + basis, + members, + policy_id, + outcome, + witness_digest, + posture, + digest, + }) + } + + /// Validates the shell as a self-contained retained record. + /// + /// # Errors + /// + /// Returns [`BraidShellError`] when member order is non-canonical, the + /// posture floor is violated, outcome and members disagree, or the + /// stored witness/shell digests do not match the recomputed body. + pub fn validate(&self) -> Result<(), BraidShellError> { + if self.members.is_empty() { + return Err(BraidShellError::EmptyMembers); + } + let mut previous: Option = None; + for member in &self.members { + let current = member.member_digest(); + if let Some(prior) = previous { + if prior > current { + return Err(BraidShellError::NonCanonicalMemberOrder); + } + } + previous = Some(current); + } + if let Some(obstruction) = shell_posture_obstruction( + self.posture, + self.members.iter().map(|member| member.posture), + ) { + return Err(BraidShellError::PostureExceedsMembers(obstruction)); + } + check_outcome_member_coherence(&self.outcome, &self.members)?; + + let witness = compute_witness_digest( + self.worldline_id, + &self.basis, + &self.members, + self.policy_id, + &self.outcome, + self.posture, + ); + if witness != self.witness_digest { + return Err(BraidShellError::WitnessMismatch { + stored: self.witness_digest, + recomputed: witness, + }); + } + let digest = compute_shell_digest( + self.worldline_id, + &self.basis, + &self.members, + self.policy_id, + &self.outcome, + self.witness_digest, + self.posture, + ); + if digest != self.digest { + return Err(BraidShellError::DigestMismatch { + stored: self.digest, + recomputed: digest, + }); + } + Ok(()) + } + + /// Maps the shell outcome onto Echo's shared lawful outcome family. + #[must_use] + pub fn outcome_kind(&self) -> AdmissionOutcomeKind { + self.outcome.kind() + } + + /// Returns whether the shell summarizes the given member strand. + #[must_use] + pub fn has_member_strand(&self, strand_id: &StrandId) -> bool { + self.members + .iter() + .any(|member| member.strand_ref == *strand_id) + } +} + +fn check_outcome_member_coherence( + outcome: &BraidShellOutcome, + members: &[BraidShellMember], +) -> Result<(), BraidShellError> { + let any_plural = members + .iter() + .any(|member| member.verdict == MemberVerdict::Plural); + let any_conflict = members + .iter() + .any(|member| member.verdict == MemberVerdict::Conflict); + let coherent = match outcome { + BraidShellOutcome::Derived { .. } => !any_plural && !any_conflict, + BraidShellOutcome::Plural { .. } => any_plural, + BraidShellOutcome::Conflict { .. } => any_conflict && !any_plural, + BraidShellOutcome::Obstruction { .. } => true, + }; + if coherent { + Ok(()) + } else { + Err(BraidShellError::OutcomeMemberMismatch { + outcome: outcome.kind(), + }) + } +} + +fn hash_shell_body( + hasher: &mut Hasher, + worldline_id: WorldlineId, + basis: &ProvenanceRef, + members: &[BraidShellMember], + policy_id: Hash, + outcome: &BraidShellOutcome, + posture: RevelationPosture, +) { + hasher.update(&BRAID_SHELL_VERSION.to_le_bytes()); + hasher.update(worldline_id.as_bytes()); + hash_provenance_ref(hasher, basis); + hasher.update(&(members.len() as u64).to_le_bytes()); + for member in members { + hasher.update(&member.member_digest()); + } + hasher.update(&policy_id); + outcome.hash_into(hasher); + hasher.update(&[posture.canonical_tag()]); +} + +fn compute_witness_digest( + worldline_id: WorldlineId, + basis: &ProvenanceRef, + members: &[BraidShellMember], + policy_id: Hash, + outcome: &BraidShellOutcome, + posture: RevelationPosture, +) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(WITNESS_DOMAIN); + hash_shell_body( + &mut hasher, + worldline_id, + basis, + members, + policy_id, + outcome, + posture, + ); + hasher.finalize().into() +} + +fn compute_shell_digest( + worldline_id: WorldlineId, + basis: &ProvenanceRef, + members: &[BraidShellMember], + policy_id: Hash, + outcome: &BraidShellOutcome, + witness_digest: Hash, + posture: RevelationPosture, +) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(SHELL_DOMAIN); + hash_shell_body( + &mut hasher, + worldline_id, + basis, + members, + policy_id, + outcome, + posture, + ); + hasher.update(&witness_digest); + hasher.finalize().into() +} + +/// Read access to retained shell records — and nothing else. +/// +/// Replay's only window into the world. There is deliberately no method that +/// returns provenance entries, strand registries, or worldline state: a +/// shell that cannot replay through this trait is not a shell. +pub trait BraidShellRecords { + /// Returns the retained shell with the given digest, if any. + fn shell(&self, digest: &Hash) -> Option<&BraidShell>; +} + +impl BraidShellRecords for std::collections::BTreeMap { + fn shell(&self, digest: &Hash) -> Option<&BraidShell> { + self.get(digest) + } +} + +/// Outcome reproduced from a retained shell alone. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BraidShellReplay { + /// Outcome arm reproduced from the shell. + pub outcome_kind: AdmissionOutcomeKind, + /// Member verdicts in canonical member order. + pub member_verdicts: Vec<(StrandId, MemberVerdict)>, + /// Settlement policy identity the act ran under. + pub policy_id: Hash, + /// Witness digest binding the act. + pub witness_digest: Hash, + /// Revelation posture of the shell. + pub posture: RevelationPosture, +} + +/// Replays a braid-scope settlement outcome from retained shell records. +/// +/// Verifies digest integrity (member digests, witness digest, shell digest) +/// and collapse lineage (a `Derived` shell collapsing plurality must +/// reference a retained `Plural` parent), then reproduces the outcome arm +/// and member verdicts. Member strand histories are unreachable by +/// construction. +/// +/// # Errors +/// +/// Returns [`BraidShellError`] when the shell is missing, fails +/// self-validation, or names a collapse parent that is absent or not plural. +pub fn replay_braid_shell( + digest: &Hash, + records: &dyn BraidShellRecords, +) -> Result { + let shell = records + .shell(digest) + .ok_or(BraidShellError::ShellNotFound { digest: *digest })?; + shell.validate()?; + if shell.digest != *digest { + return Err(BraidShellError::DigestMismatch { + stored: *digest, + recomputed: shell.digest, + }); + } + if let BraidShellOutcome::Derived { + collapsed_from: Some(parent_digest), + .. + } = &shell.outcome + { + let parent = + records + .shell(parent_digest) + .ok_or(BraidShellError::InvalidCollapseLineage { + collapsed_from: *parent_digest, + })?; + parent.validate()?; + if !matches!(parent.outcome, BraidShellOutcome::Plural { .. }) { + return Err(BraidShellError::InvalidCollapseLineage { + collapsed_from: *parent_digest, + }); + } + } + Ok(BraidShellReplay { + outcome_kind: shell.outcome_kind(), + member_verdicts: shell + .members + .iter() + .map(|member| (member.strand_ref, member.verdict)) + .collect(), + policy_id: shell.policy_id, + witness_digest: shell.witness_digest, + posture: shell.posture, + }) +} + +#[cfg(test)] +#[allow(clippy::unwrap_used)] +mod tests { + use super::*; + use crate::clock::WorldlineTick; + use crate::ident::make_warp_id; + use crate::strand::make_strand_id; + use std::collections::BTreeMap; + + fn wl(n: u8) -> WorldlineId { + WorldlineId::from_bytes([n; 32]) + } + + fn basis_ref() -> ProvenanceRef { + ProvenanceRef { + worldline_id: wl(1), + worldline_tick: WorldlineTick::from_raw(0), + commit_hash: [0x11; 32], + } + } + + fn member(label: &str, verdict: MemberVerdict) -> BraidShellMember { + BraidShellMember { + strand_ref: make_strand_id(label), + support_pin_digest: [0x21; 32], + basis_digest: [0x22; 32], + frontier_digest: [0x23; 32], + footprint_digest: [0x24; 32], + claim_digest: [0x25; 32], + verdict, + verdict_digest: [0x26; 32], + posture: RevelationPosture::AuthorOnly, + } + } + + fn plural_shell(members: Vec) -> BraidShell { + BraidShell::assemble( + wl(1), + basis_ref(), + members, + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + RevelationPosture::AuthorOnly, + ) + .unwrap() + } + + struct Records(BTreeMap); + + impl Records { + fn with(shells: impl IntoIterator) -> Self { + Self( + shells + .into_iter() + .map(|shell| (shell.digest, shell)) + .collect(), + ) + } + } + + impl BraidShellRecords for Records { + fn shell(&self, digest: &Hash) -> Option<&BraidShell> { + self.0.get(digest) + } + } + + #[test] + fn member_order_permutation_cannot_move_the_shell_digest() { + let a = member("member-a", MemberVerdict::Plural); + let b = member("member-b", MemberVerdict::Derived); + let forward = plural_shell(vec![a.clone(), b.clone()]); + let reversed = plural_shell(vec![b, a]); + + assert_eq!(forward.digest, reversed.digest); + assert_eq!(forward, reversed); + } + + #[test] + fn replay_reproduces_outcome_and_member_verdicts_from_records_alone() { + let shell = plural_shell(vec![ + member("member-a", MemberVerdict::Plural), + member("member-b", MemberVerdict::Derived), + ]); + let digest = shell.digest; + let expected_verdicts: Vec<(StrandId, MemberVerdict)> = shell + .members + .iter() + .map(|member| (member.strand_ref, member.verdict)) + .collect(); + let records = Records::with([shell]); + + let replay = replay_braid_shell(&digest, &records).unwrap(); + assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Plural); + assert_eq!(replay.member_verdicts, expected_verdicts); + assert_eq!(replay.policy_id, [0x5E; 32]); + assert_eq!(replay.posture, RevelationPosture::AuthorOnly); + } + + #[test] + fn tampering_with_policy_posture_or_verdict_fails_replay() { + let shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let digest = shell.digest; + + let mut policy_tampered = shell.clone(); + policy_tampered.policy_id = [0xAA; 32]; + let records = Records::with([policy_tampered]); + // The tampered shell no longer lives at its claimed digest, and its + // stored digest no longer matches its body either way. + assert!(replay_braid_shell(&digest, &records).is_err()); + + let mut verdict_tampered = shell.clone(); + verdict_tampered.members[0].verdict_digest = [0xBB; 32]; + assert!(matches!( + verdict_tampered.validate(), + Err(BraidShellError::WitnessMismatch { .. } | BraidShellError::DigestMismatch { .. }) + )); + + let mut posture_tampered = shell.clone(); + posture_tampered.posture = RevelationPosture::Scratch; + assert!(matches!( + posture_tampered.validate(), + Err(BraidShellError::WitnessMismatch { .. } | BraidShellError::DigestMismatch { .. }) + )); + + let mut outcome_tampered = shell; + outcome_tampered.outcome = BraidShellOutcome::Conflict { + reason_codes: vec![4], + }; + assert!(outcome_tampered.validate().is_err()); + } + + #[test] + fn shell_posture_cannot_exceed_least_revealed_member() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Plural)], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + RevelationPosture::Shared, + ); + assert!(matches!( + result, + Err(BraidShellError::PostureExceedsMembers(_)) + )); + } + + #[test] + fn outcome_arm_must_agree_with_member_verdicts() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Derived)], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + RevelationPosture::AuthorOnly, + ); + assert_eq!( + result, + Err(BraidShellError::OutcomeMemberMismatch { + outcome: AdmissionOutcomeKind::Plural, + }) + ); + } + + #[test] + fn collapse_lineage_requires_a_retained_plural_parent() { + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let plural_digest = plural.digest; + let derived = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Derived)], + [0x5E; 32], + BraidShellOutcome::Derived { + result_refs: vec![basis_ref()], + collapse_policy: Some([0x77; 32]), + collapsed_from: Some(plural_digest), + }, + RevelationPosture::AuthorOnly, + ) + .unwrap(); + let derived_digest = derived.digest; + + let complete = Records::with([plural, derived.clone()]); + let replay = replay_braid_shell(&derived_digest, &complete).unwrap(); + assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Derived); + + let missing_parent = Records::with([derived]); + assert_eq!( + replay_braid_shell(&derived_digest, &missing_parent), + Err(BraidShellError::InvalidCollapseLineage { + collapsed_from: plural_digest, + }) + ); + } +} diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 33ea094d..86d7701a 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -41,6 +41,7 @@ pub mod wsc; mod admission; mod attachment; +mod braid_shell; mod causal_facts; pub mod causal_wal; mod clock; @@ -255,6 +256,10 @@ pub use retained_evidence::{ // --- Session types --- pub use playback::{SessionId, ViewSession}; // --- Truth delivery --- +pub use braid_shell::{ + replay_braid_shell, BraidShell, BraidShellError, BraidShellMember, BraidShellOutcome, + BraidShellRecords, BraidShellReplay, MemberVerdict, BRAID_SHELL_VERSION, +}; pub use neighborhood::{ NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, NeighborhoodPlurality, NeighborhoodSite, NeighborhoodSiteId, NeighborhoodSiteService, diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index 43a66c89..03a67e26 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -1623,6 +1623,12 @@ impl LocalProvenanceStore { } } +impl crate::braid_shell::BraidShellRecords for ProvenanceService { + fn shell(&self, digest: &Hash) -> Option<&crate::braid_shell::BraidShell> { + self.braid_shells.get(digest) + } +} + impl ProvenanceStore for LocalProvenanceStore { fn u0(&self, w: WorldlineId) -> Result { Ok(self.history(w)?.u0_ref) @@ -1717,6 +1723,8 @@ impl ProvenanceStore for LocalProvenanceStore { #[derive(Debug, Clone, Default)] pub struct ProvenanceService { store: LocalProvenanceStore, + /// Retained braid shells by canonical shell digest (θ_braid family). + braid_shells: BTreeMap, } impl ProvenanceService { @@ -1726,6 +1734,57 @@ impl ProvenanceService { Self::default() } + /// Retains one validated braid shell. + /// + /// Shells are append-only retained truth: callers must append a shell + /// only after the histories it describes are durable (settlement appends + /// the shell as its final fallible step, so a failed settle never leaks + /// a shell describing history that was rolled back). Re-appending an + /// identical shell is idempotent. + /// + /// # Errors + /// + /// Returns [`crate::braid_shell::BraidShellError`] when the shell fails + /// self-validation or a different shell already claims the same digest. + pub fn append_braid_shell( + &mut self, + shell: crate::braid_shell::BraidShell, + ) -> Result { + shell.validate()?; + let digest = shell.digest; + if let Some(existing) = self.braid_shells.get(&digest) { + if *existing != shell { + return Err( + crate::braid_shell::BraidShellError::DuplicateDigestDivergentContent { digest }, + ); + } + return Ok(digest); + } + self.braid_shells.insert(digest, shell); + Ok(digest) + } + + /// Returns the retained braid shell with the given digest, if any. + #[must_use] + pub fn braid_shell(&self, digest: &Hash) -> Option<&crate::braid_shell::BraidShell> { + self.braid_shells.get(digest) + } + + /// Iterates all retained braid shells in canonical digest order. + pub fn braid_shells(&self) -> impl Iterator { + self.braid_shells.values() + } + + /// Moves all retained braid shells out of the service. + /// + /// Exists for hostile replay proofs: a caller can extract the shells and + /// drop the runtime, registry, and provenance entries entirely, then + /// replay outcomes from the shells alone. + #[must_use] + pub fn take_braid_shells(&mut self) -> BTreeMap { + std::mem::take(&mut self.braid_shells) + } + /// Registers a worldline using its deterministic replay base. /// /// # Errors diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index ebd892d4..7a85ce8a 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -353,6 +353,8 @@ pub struct SettlementResult { pub appended_conflicts: Vec, /// Target-worldline refs appended as `PluralArtifact`. pub appended_plurals: Vec, + /// Digest of the retained θ_braid shell for this settlement act, if any. + pub braid_shell: Option, } impl SettlementResult { @@ -379,6 +381,7 @@ impl SettlementResult { .copied() .map(provenance_ref_to_abi) .collect(), + braid_shell_digest: self.braid_shell.map(|digest| digest.to_vec()), } } } @@ -865,6 +868,7 @@ impl SettlementService { appended_imports: Vec::new(), appended_conflicts: Vec::new(), appended_plurals: Vec::new(), + braid_shell: None, }); } @@ -916,6 +920,7 @@ impl SettlementService { appended_imports, appended_conflicts, appended_plurals, + braid_shell: None, }) })(); @@ -2184,6 +2189,131 @@ mod tests { ); } + #[test] + fn plural_settlement_retains_exactly_one_braid_shell_with_full_body() { + let (mut runtime, mut provenance, strand_id, base_worldline, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let result = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + + let Some(shell_digest) = result.braid_shell else { + assert!( + result.braid_shell.is_some(), + "a braid-scope settlement must retain exactly one theta_braid shell" + ); + return; + }; + assert_eq!(provenance.braid_shells().count(), 1); + let shell = provenance.braid_shell(&shell_digest).unwrap(); + + assert_eq!(shell.policy_id, plural_policy().policy_id); + assert_eq!( + shell.posture, + crate::revelation::RevelationPosture::AuthorOnly + ); + assert_eq!(shell.worldline_id, base_worldline); + assert!(shell.has_member_strand(&strand_id)); + assert_eq!(shell.members.len(), 1); + assert_eq!( + shell.members[0].verdict, + crate::braid_shell::MemberVerdict::Plural + ); + assert_ne!(shell.members[0].verdict_digest, [0; 32]); + let expected_plural_ids: Vec = result + .plan + .decisions + .iter() + .filter_map(|decision| plural_alternative(decision).map(|draft| draft.plural_id)) + .collect(); + assert_eq!( + shell.outcome, + crate::braid_shell::BraidShellOutcome::Plural { + alternative_ids: expected_plural_ids, + } + ); + } + + #[test] + fn braid_shell_replays_after_runtime_and_histories_are_dropped() { + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let result = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + let Some(shell_digest) = result.braid_shell else { + assert!( + result.braid_shell.is_some(), + "settlement must retain a shell" + ); + return; + }; + + // Hostile replay: extract the shells, then destroy every path back + // to strand histories — runtime, registry, and provenance entries. + let shells = provenance.take_braid_shells(); + drop(runtime); + drop(provenance); + + let replay = crate::braid_shell::replay_braid_shell(&shell_digest, &shells).unwrap(); + assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Plural); + assert_eq!( + replay.member_verdicts, + vec![(strand_id, crate::braid_shell::MemberVerdict::Plural)] + ); + assert_eq!(replay.policy_id, plural_policy().policy_id); + } + + #[test] + fn derived_and_conflict_settlements_also_retain_braid_shells() { + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::None); + let derived = SettlementService::settle(&mut runtime, &mut provenance, strand_id).unwrap(); + let Some(derived_digest) = derived.braid_shell else { + assert!( + derived.braid_shell.is_some(), + "derived settlement must retain a shell" + ); + return; + }; + assert_eq!( + provenance + .braid_shell(&derived_digest) + .unwrap() + .outcome_kind(), + AdmissionOutcomeKind::Derived + ); + + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + let conflicted = + SettlementService::settle(&mut runtime, &mut provenance, strand_id).unwrap(); + let Some(conflict_digest) = conflicted.braid_shell else { + assert!( + conflicted.braid_shell.is_some(), + "conflict settlement must retain a shell" + ); + return; + }; + assert_eq!( + provenance + .braid_shell(&conflict_digest) + .unwrap() + .outcome_kind(), + AdmissionOutcomeKind::Conflict + ); + } + #[test] fn slot_order_does_not_move_plural_id_or_overlap_digest() { let warp_id = crate::ident::make_warp_id("plural-slot-order"); From 7be9f3a26e656c293e34e081cfe7d6cf3c82ac45 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 13:04:15 -0700 Subject: [PATCH 09/14] =?UTF-8?q?feat(warp-core):=20GREEN=20=E2=80=94=20se?= =?UTF-8?q?ttlement=20retains=20replayable=20theta=5Fbraid=20shells=20(002?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every non-empty settlement act now retains exactly one theta_braid shell as its final fallible step (a failed settle restores entries before any shell describing them could land): - one member per settled strand (N-capable structures; E3 owns weave): verdict = plural > conflict > derived precedence over the per-entry decisions; verdict digest binds the full ordered decision sequence; claim digest binds ordered source commit hashes; basis / frontier / footprint / support-pin digests domain-separated - outcome arm follows the algebra: Plural carries the retained plural artifact ids (linking the boundary to its per-entry residue), Conflict carries ordered reason codes, Derived carries the appended import refs - the sacred test passes: after settling under plural policy, the shells are extracted and the runtime, strand registry, and provenance entries are dropped — replay_braid_shell reproduces the outcome arm, member verdicts, and policy id from the shells alone 497/497 warp-core tests green; workspace clean under -D warnings. --- crates/warp-core/src/braid_shell.rs | 11 +- crates/warp-core/src/settlement.rs | 150 +++++++++++++++++++++++++++- 2 files changed, 159 insertions(+), 2 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 8b6ceb22..bfd953c6 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -204,18 +204,22 @@ fn hash_optional_digest(hasher: &mut Hasher, digest: Option<&Hash>) { } /// Obstructions raised while assembling, validating, or replaying a shell. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)] pub enum BraidShellError { /// A shell must summarize at least one member. + #[error("braid shell must summarize at least one member")] EmptyMembers, /// The shell posture exceeds its least-revealed member. + #[error("braid shell posture exceeds least-revealed member: {0:?}")] PostureExceedsMembers(PostureObstruction), /// Outcome arm and member verdicts disagree. + #[error("braid shell outcome {outcome:?} disagrees with member verdicts")] OutcomeMemberMismatch { /// Outcome arm the shell claims. outcome: AdmissionOutcomeKind, }, /// The stored digest does not match the recomputed canonical body. + #[error("braid shell digest mismatch: stored {stored:?}, recomputed {recomputed:?}")] DigestMismatch { /// Digest stored on the shell. stored: Hash, @@ -223,6 +227,7 @@ pub enum BraidShellError { recomputed: Hash, }, /// The stored witness digest does not match the recomputed body witness. + #[error("braid shell witness mismatch: stored {stored:?}, recomputed {recomputed:?}")] WitnessMismatch { /// Witness digest stored on the shell. stored: Hash, @@ -230,18 +235,22 @@ pub enum BraidShellError { recomputed: Hash, }, /// Member entries are not in canonical order. + #[error("braid shell members are not in canonical order")] NonCanonicalMemberOrder, /// No shell record exists for the requested digest. + #[error("no braid shell retained for digest {digest:?}")] ShellNotFound { /// Digest that resolved to nothing. digest: Hash, }, /// A collapse lineage parent is missing or not plural. + #[error("collapse lineage parent {collapsed_from:?} is missing or not plural")] InvalidCollapseLineage { /// Parent shell digest named by `collapsed_from`. collapsed_from: Hash, }, /// A shell with this digest is already retained with different content. + #[error("a divergent braid shell already claims digest {digest:?}")] DuplicateDigestDivergentContent { /// Digest both shells claim. digest: Hash, diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 7a85ce8a..8a5ccefb 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -601,6 +601,9 @@ pub enum SettlementError { /// Strand live-basis analysis failed. #[error(transparent)] StrandBasis(#[from] StrandError), + /// Retaining the θ_braid shell for a settlement act failed. + #[error(transparent)] + BraidShell(#[from] crate::braid_shell::BraidShellError), } /// Deterministic source-basis settlement service. @@ -871,6 +874,10 @@ impl SettlementService { braid_shell: None, }); } + let (fork_basis_ref, support_pins) = { + let settled = strand(runtime.strands(), strand_id)?; + (settled.fork_basis_ref, settled.support_pins.clone()) + }; let runtime_before = runtime.clone(); let provenance_before = provenance.checkpoint_for([plan.target_worldline])?; @@ -915,12 +922,23 @@ impl SettlementService { } } + // The shell is the final fallible step: a failed settle restores + // entries before any shell describing them could be retained. + let shell = build_braid_shell( + &plan, + fork_basis_ref, + &support_pins, + policy, + &appended_imports, + )?; + let braid_shell = provenance.append_braid_shell(shell)?; + Ok(SettlementResult { plan, appended_imports, appended_conflicts, appended_plurals, - braid_shell: None, + braid_shell: Some(braid_shell), }) })(); @@ -1257,6 +1275,136 @@ fn conflict_draft_with_revalidation( } } +/// Builds the θ_braid shell summarizing one settlement act. +/// +/// E1 settles one strand against its base, so the shell carries one member +/// whose verdict summarizes the per-entry decisions (plural > conflict > +/// derived precedence) and whose verdict digest binds the full ordered +/// decision sequence. Data structures are N-capable; N-strand weave +/// semantics remain E3. +fn build_braid_shell( + plan: &SettlementPlan, + fork_basis_ref: crate::strand::ForkBasisRef, + support_pins: &[crate::strand::SupportPin], + policy: &SettlementPolicy, + appended_imports: &[ProvenanceRef], +) -> Result { + use crate::braid_shell::{BraidShell, BraidShellMember, BraidShellOutcome, MemberVerdict}; + + let mut plural_ids = Vec::new(); + let mut conflict_codes = Vec::new(); + let mut claim_hasher = Hasher::new(); + claim_hasher.update(b"echo.braid.member.claims.v1\0"); + let mut verdict_hasher = Hasher::new(); + verdict_hasher.update(b"echo.braid.member.verdicts.v1\0"); + for decision in &plan.decisions { + match decision { + SettlementDecision::ImportCandidate(candidate) => { + claim_hasher.update(&candidate.source_ref.commit_hash); + verdict_hasher.update(&[1]); + verdict_hasher.update(&candidate.imported_op_id); + } + SettlementDecision::ConflictArtifact(draft) => { + claim_hasher.update(&draft.source_ref.commit_hash); + verdict_hasher.update(&[2]); + verdict_hasher.update(&draft.artifact_id); + verdict_hasher.update(&[draft.reason.code()]); + conflict_codes.push(draft.reason.code()); + } + SettlementDecision::PluralAlternative(draft) => { + claim_hasher.update(&draft.source_ref.commit_hash); + verdict_hasher.update(&[3]); + verdict_hasher.update(&draft.plural_id); + plural_ids.push(draft.plural_id); + } + } + } + + let verdict = if plural_ids.is_empty() { + if conflict_codes.is_empty() { + MemberVerdict::Derived + } else { + MemberVerdict::Conflict + } + } else { + MemberVerdict::Plural + }; + let outcome = if plural_ids.is_empty() { + if conflict_codes.is_empty() { + BraidShellOutcome::Derived { + result_refs: appended_imports.to_vec(), + collapse_policy: None, + collapsed_from: None, + } + } else { + BraidShellOutcome::Conflict { + reason_codes: conflict_codes, + } + } + } else { + BraidShellOutcome::Plural { + alternative_ids: plural_ids, + } + }; + + let overlap_slots = settlement_basis_overlap_slots(&plan.basis_report).unwrap_or_default(); + let member = BraidShellMember { + strand_ref: plan.strand_id, + support_pin_digest: support_pins_digest(support_pins), + basis_digest: fork_basis_digest(fork_basis_ref), + frontier_digest: frontier_digest(plan.basis_report.realized_parent_ref), + footprint_digest: settlement_overlap_slots_digest(&overlap_slots), + claim_digest: claim_hasher.finalize().into(), + verdict, + verdict_digest: verdict_hasher.finalize().into(), + posture: RevelationPosture::default(), + }; + + BraidShell::assemble( + plan.target_worldline, + plan.target_base_ref, + vec![member], + policy.policy_id, + outcome, + RevelationPosture::default(), + ) +} + +fn support_pins_digest(pins: &[crate::strand::SupportPin]) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(b"echo.braid.member.pins.v1\0"); + hasher.update(&(pins.len() as u64).to_le_bytes()); + for pin in pins { + hasher.update(pin.strand_id.as_bytes()); + hasher.update(pin.worldline_id.as_bytes()); + hasher.update(&pin.pinned_tick.as_u64().to_le_bytes()); + hasher.update(&pin.state_hash); + } + hasher.finalize().into() +} + +fn fork_basis_digest(basis: crate::strand::ForkBasisRef) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(b"echo.braid.member.basis.v1\0"); + hasher.update(basis.source_lane_id.as_bytes()); + hasher.update(&basis.fork_tick.as_u64().to_le_bytes()); + hasher.update(&basis.commit_hash); + hasher.update(&basis.boundary_hash); + hasher.update(basis.provenance_ref.worldline_id.as_bytes()); + hasher.update(&basis.provenance_ref.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&basis.provenance_ref.commit_hash); + hasher.finalize().into() +} + +fn frontier_digest(realized_parent_ref: ProvenanceRef) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(b"echo.braid.member.frontier.v1\0"); + hasher.update(realized_parent_ref.worldline_id.as_bytes()); + hasher.update(&realized_parent_ref.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&realized_parent_ref.commit_hash); + hasher.finalize().into() +} + fn plural_draft( target_worldline: WorldlineId, source_entry: &ProvenanceEntry, From fe0de65a22fc9b9d0fe2649f953763296b865527 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 13:25:56 -0700 Subject: [PATCH 10/14] feat(warp-core): collapse transition + record-law hardening per theta_braid review (0026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Must-fixes: - shell version is digest-bound (hash_shell_body hashes the stored version) AND explicitly validated (UnsupportedVersion); tamper test - one retained boundary family made explicit in code: RetainedBoundaryRecord trait + RetainedBoundaryKind implemented for both BoundaryTransitionRecord (tick, echo.shell.tick.v1 digest) and BraidShell — theta_tick and theta_braid are visible siblings - no-leak law encoded as test: failed settlement retains no shell - residue->shell direction: plural_shell_index on ProvenanceService; braid_shell_for_plural resolves a plural artifact id to its boundary without scanning (no archaeology) - empty-settlement law named and tested: no claims means no braid outcome; an empty settlement emits no shell by law Should-fixes: - plural alternative_ids are a canonical set: sorted at assembly, NonCanonicalAlternativeOrder on validation; permutation test - support pins sorted before digesting (tuition paid once) - claim_digest binds the full source coordinate (worldline, tick, commit), not commit hash alone - first-class query API: BraidShellQuery + query_braid_shells (scan-backed), tested by member/outcome/posture Collapse transition (append-only or bust): - WitnessDigest newtype refuses zero and empty-input digests — a witness is never a 32-byte shrug - collapse_braid_shell: named CollapsePolicy -> new Derived shell with collapse_policy + collapse_witness + collapsed_from, plural parent byte-identical; missing policy -> retained Obstruction shell (CollapseResult::Obstructed), because obstruction is also law - Derived lineage fields are all-or-none (IncoherentCollapseFields); collapse-derived shells may summarize the plural members they collapsed; settlement-derived shells may not 508/508 warp-core tests; workspace clean under -D warnings. --- crates/warp-core/src/braid_shell.rs | 427 ++++++++++++++++++++++- crates/warp-core/src/lib.rs | 6 +- crates/warp-core/src/provenance_store.rs | 32 ++ crates/warp-core/src/settlement.rs | 169 +++++++-- 4 files changed, 606 insertions(+), 28 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index bfd953c6..84bf5a29 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -112,6 +112,8 @@ pub enum BraidShellOutcome { result_refs: Vec, /// Collapse policy when this derivation collapsed retained plurality. collapse_policy: Option, + /// Witness for the explicit collapse act, when applicable. + collapse_witness: Option, /// Plural parent shell this derivation collapsed, when applicable. collapsed_from: Option, }, @@ -151,6 +153,7 @@ impl BraidShellOutcome { Self::Derived { result_refs, collapse_policy, + collapse_witness, collapsed_from, } => { hasher.update(&[1]); @@ -159,6 +162,7 @@ impl BraidShellOutcome { hash_provenance_ref(hasher, reference); } hash_optional_digest(hasher, collapse_policy.as_ref()); + hash_optional_digest(hasher, collapse_witness.as_ref()); hash_optional_digest(hasher, collapsed_from.as_ref()); } Self::Plural { alternative_ids } => { @@ -255,6 +259,48 @@ pub enum BraidShellError { /// Digest both shells claim. digest: Hash, }, + /// The shell claims a body version this build does not speak. + #[error("unsupported braid shell version {stored} (supported: {supported})")] + UnsupportedVersion { + /// Version stored on the shell. + stored: u32, + /// Version this build supports. + supported: u32, + }, + /// Plural alternatives are not in canonical set order. + #[error("braid shell plural alternatives are not in canonical order")] + NonCanonicalAlternativeOrder, + /// Collapse lineage fields must be all present or all absent. + #[error("derived shell carries incoherent collapse fields")] + IncoherentCollapseFields, + /// A witness digest must never be a 32-byte shrug. + #[error("empty or null witness digest refused")] + EmptyWitness, +} + +/// Witness digest with a quality bar: zero and empty-input digests refused. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessDigest(Hash); + +impl WitnessDigest { + /// Wraps a witness digest, refusing shrug values. + /// + /// # Errors + /// + /// Returns [`BraidShellError::EmptyWitness`] for the all-zero digest and + /// the digest of empty input. + pub fn new(hash: Hash) -> Result { + if hash == [0; 32] || hash == crate::blake3_empty() { + return Err(BraidShellError::EmptyWitness); + } + Ok(Self(hash)) + } + + /// Returns the underlying digest. + #[must_use] + pub fn as_hash(&self) -> &Hash { + &self.0 + } } /// Retained braid-scale settlement boundary (θ_braid). @@ -294,13 +340,18 @@ impl BraidShell { basis: ProvenanceRef, mut members: Vec, policy_id: Hash, - outcome: BraidShellOutcome, + mut outcome: BraidShellOutcome, posture: RevelationPosture, ) -> Result { if members.is_empty() { return Err(BraidShellError::EmptyMembers); } members.sort_by_key(BraidShellMember::member_digest); + if let BraidShellOutcome::Plural { alternative_ids } = &mut outcome { + // Retained alternatives are a set; canonical order, not transcript + // order (the member verdict digest binds the ordered transcript). + alternative_ids.sort_unstable(); + } if let Some(obstruction) = shell_posture_obstruction(posture, members.iter().map(|member| member.posture)) { @@ -308,9 +359,17 @@ impl BraidShell { } check_outcome_member_coherence(&outcome, &members)?; - let witness_digest = - compute_witness_digest(worldline_id, &basis, &members, policy_id, &outcome, posture); + let witness_digest = compute_witness_digest( + BRAID_SHELL_VERSION, + worldline_id, + &basis, + &members, + policy_id, + &outcome, + posture, + ); let digest = compute_shell_digest( + BRAID_SHELL_VERSION, worldline_id, &basis, &members, @@ -340,6 +399,12 @@ impl BraidShell { /// posture floor is violated, outcome and members disagree, or the /// stored witness/shell digests do not match the recomputed body. pub fn validate(&self) -> Result<(), BraidShellError> { + if self.version != BRAID_SHELL_VERSION { + return Err(BraidShellError::UnsupportedVersion { + stored: self.version, + supported: BRAID_SHELL_VERSION, + }); + } if self.members.is_empty() { return Err(BraidShellError::EmptyMembers); } @@ -353,6 +418,11 @@ impl BraidShell { } previous = Some(current); } + if let BraidShellOutcome::Plural { alternative_ids } = &self.outcome { + if alternative_ids.windows(2).any(|pair| pair[0] > pair[1]) { + return Err(BraidShellError::NonCanonicalAlternativeOrder); + } + } if let Some(obstruction) = shell_posture_obstruction( self.posture, self.members.iter().map(|member| member.posture), @@ -362,6 +432,7 @@ impl BraidShell { check_outcome_member_coherence(&self.outcome, &self.members)?; let witness = compute_witness_digest( + self.version, self.worldline_id, &self.basis, &self.members, @@ -376,6 +447,7 @@ impl BraidShell { }); } let digest = compute_shell_digest( + self.version, self.worldline_id, &self.basis, &self.members, @@ -419,7 +491,26 @@ fn check_outcome_member_coherence( .iter() .any(|member| member.verdict == MemberVerdict::Conflict); let coherent = match outcome { - BraidShellOutcome::Derived { .. } => !any_plural && !any_conflict, + BraidShellOutcome::Derived { + collapse_policy, + collapse_witness, + collapsed_from, + .. + } => { + let lineage_fields = [ + collapse_policy.is_some(), + collapse_witness.is_some(), + collapsed_from.is_some(), + ]; + if lineage_fields.iter().any(|present| *present) + && lineage_fields.iter().any(|present| !*present) + { + return Err(BraidShellError::IncoherentCollapseFields); + } + // A collapse-derived shell summarizes the plural members it + // collapsed; a settlement-derived shell must carry none. + collapsed_from.is_some() || (!any_plural && !any_conflict) + } BraidShellOutcome::Plural { .. } => any_plural, BraidShellOutcome::Conflict { .. } => any_conflict && !any_plural, BraidShellOutcome::Obstruction { .. } => true, @@ -433,8 +524,10 @@ fn check_outcome_member_coherence( } } +#[allow(clippy::too_many_arguments)] fn hash_shell_body( hasher: &mut Hasher, + version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, members: &[BraidShellMember], @@ -442,7 +535,7 @@ fn hash_shell_body( outcome: &BraidShellOutcome, posture: RevelationPosture, ) { - hasher.update(&BRAID_SHELL_VERSION.to_le_bytes()); + hasher.update(&version.to_le_bytes()); hasher.update(worldline_id.as_bytes()); hash_provenance_ref(hasher, basis); hasher.update(&(members.len() as u64).to_le_bytes()); @@ -454,7 +547,9 @@ fn hash_shell_body( hasher.update(&[posture.canonical_tag()]); } +#[allow(clippy::too_many_arguments)] fn compute_witness_digest( + version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, members: &[BraidShellMember], @@ -466,6 +561,7 @@ fn compute_witness_digest( hasher.update(WITNESS_DOMAIN); hash_shell_body( &mut hasher, + version, worldline_id, basis, members, @@ -476,7 +572,9 @@ fn compute_witness_digest( hasher.finalize().into() } +#[allow(clippy::too_many_arguments)] fn compute_shell_digest( + version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, members: &[BraidShellMember], @@ -489,6 +587,7 @@ fn compute_shell_digest( hasher.update(SHELL_DOMAIN); hash_shell_body( &mut hasher, + version, worldline_id, basis, members, @@ -588,6 +687,178 @@ pub fn replay_braid_shell( }) } +/// Deterministic reason code: collapse attempted without a named policy. +pub const COLLAPSE_WITHOUT_POLICY_REASON: u8 = 1; + +const COLLAPSE_OBSTRUCTION_WITNESS_DOMAIN: &[u8] = b"echo.braid.collapse.obstruction.v1\0"; +const ABSENT_COLLAPSE_POLICY_DOMAIN: &[u8] = b"echo.braid.collapse-policy.absent.v1\0"; + +/// Named, witnessed law permitting one collapse of retained plurality. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct CollapsePolicy { + /// Stable collapse policy identity. + pub policy_id: Hash, + /// Witness for the explicit collapse act. + pub witness: WitnessDigest, +} + +/// Outcome of one collapse attempt. Both arms are retained law. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum CollapseResult { + /// The plurality lawfully collapsed into a new derived shell. + Derived(BraidShell), + /// The collapse was refused; the obstruction is itself a retained shell. + Obstructed(BraidShell), +} + +/// Collapses a retained plural shell into a new shell-family record. +/// +/// The plural parent is never mutated: a named, witnessed collapse policy +/// produces a new `Derived` shell whose `collapsed_from` references the +/// parent; a missing policy produces a retained `Obstruction` shell. Either +/// way the original plural shell remains byte-identical truth forever. +/// Append-only or bust. +/// +/// # Errors +/// +/// Returns [`BraidShellError`] when the plural shell is missing, fails +/// validation, or is not plural. +pub fn collapse_braid_shell( + records: &dyn BraidShellRecords, + plural_shell_digest: Hash, + selected_result_refs: Vec, + policy: Option, +) -> Result { + let plural = records + .shell(&plural_shell_digest) + .ok_or(BraidShellError::ShellNotFound { + digest: plural_shell_digest, + })?; + plural.validate()?; + if !matches!(plural.outcome, BraidShellOutcome::Plural { .. }) { + return Err(BraidShellError::InvalidCollapseLineage { + collapsed_from: plural_shell_digest, + }); + } + + if let Some(policy) = policy { + let derived = BraidShell::assemble( + plural.worldline_id, + plural.basis, + plural.members.clone(), + policy.policy_id, + BraidShellOutcome::Derived { + result_refs: selected_result_refs, + collapse_policy: Some(policy.policy_id), + collapse_witness: Some(*policy.witness.as_hash()), + collapsed_from: Some(plural_shell_digest), + }, + plural.posture, + )?; + return Ok(CollapseResult::Derived(derived)); + } + + let mut witness_hasher = Hasher::new(); + witness_hasher.update(COLLAPSE_OBSTRUCTION_WITNESS_DOMAIN); + witness_hasher.update(&plural_shell_digest); + let mut policy_hasher = Hasher::new(); + policy_hasher.update(ABSENT_COLLAPSE_POLICY_DOMAIN); + let obstructed = BraidShell::assemble( + plural.worldline_id, + plural.basis, + plural.members.clone(), + policy_hasher.finalize().into(), + BraidShellOutcome::Obstruction { + reason_code: COLLAPSE_WITHOUT_POLICY_REASON, + witness: witness_hasher.finalize().into(), + }, + plural.posture, + )?; + Ok(CollapseResult::Obstructed(obstructed)) +} + +/// Kind discriminator for the retained boundary shell family. +/// +/// θ_tick and θ_braid are siblings in one retained boundary family +/// (AIΩN Paper VII Prop 3.5); θ_import joins later. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RetainedBoundaryKind { + /// A contiguous tick-segment boundary record. + Tick, + /// A braid-scale settlement boundary shell. + Braid, +} + +/// Shared contract for retained boundary shells of every scale. +pub trait RetainedBoundaryRecord { + /// Returns which family member this record is. + fn boundary_kind(&self) -> RetainedBoundaryKind; + /// Returns the canonical content digest for the record. + fn boundary_digest(&self) -> Hash; +} + +impl RetainedBoundaryRecord for BraidShell { + fn boundary_kind(&self) -> RetainedBoundaryKind { + RetainedBoundaryKind::Braid + } + + fn boundary_digest(&self) -> Hash { + self.digest + } +} + +const TICK_SHELL_DOMAIN: &[u8] = b"echo.shell.tick.v1\0"; + +impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecord { + fn boundary_kind(&self) -> RetainedBoundaryKind { + RetainedBoundaryKind::Tick + } + + fn boundary_digest(&self) -> Hash { + let mut hasher = Hasher::new(); + hasher.update(TICK_SHELL_DOMAIN); + hasher.update(self.worldline_id.as_bytes()); + hasher.update(self.u0_ref.as_bytes()); + hasher.update(&self.input_boundary_hash); + hasher.update(&self.output_boundary_hash); + hasher.update(&self.logical_counter.to_le_bytes()); + hasher.update(&(self.payload.entries.len() as u64).to_le_bytes()); + for entry in &self.payload.entries { + hasher.update(&entry.expected.commit_hash); + } + hasher.finalize().into() + } +} + +/// Scan-backed query over retained braid shells. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct BraidShellQuery { + /// Match shells judged against this comparison basis. + pub basis: Option, + /// Match shells summarizing this member strand. + pub member_strand: Option, + /// Match shells with this outcome arm. + pub outcome: Option, + /// Match shells with this revelation posture. + pub posture: Option, +} + +impl BraidShell { + /// Returns whether the shell matches every present query field. + #[must_use] + pub fn matches(&self, query: &BraidShellQuery) -> bool { + query.basis.is_none_or(|basis| self.basis == basis) + && query + .member_strand + .as_ref() + .is_none_or(|strand| self.has_member_strand(strand)) + && query + .outcome + .is_none_or(|outcome| self.outcome_kind() == outcome) + && query.posture.is_none_or(|posture| self.posture == posture) + } +} + #[cfg(test)] #[allow(clippy::unwrap_used)] mod tests { @@ -771,6 +1042,7 @@ mod tests { BraidShellOutcome::Derived { result_refs: vec![basis_ref()], collapse_policy: Some([0x77; 32]), + collapse_witness: Some([0x78; 32]), collapsed_from: Some(plural_digest), }, RevelationPosture::AuthorOnly, @@ -790,4 +1062,149 @@ mod tests { }) ); } + + #[test] + fn tampering_with_version_fails_validation() { + let mut shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + shell.version = 999; + assert_eq!( + shell.validate(), + Err(BraidShellError::UnsupportedVersion { + stored: 999, + supported: BRAID_SHELL_VERSION, + }) + ); + } + + #[test] + fn plural_alternatives_are_a_canonical_set() { + let members = vec![member("member-a", MemberVerdict::Plural)]; + let forward = BraidShell::assemble( + wl(1), + basis_ref(), + members.clone(), + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x32; 32], [0x31; 32]], + }, + RevelationPosture::AuthorOnly, + ) + .unwrap(); + let reversed = BraidShell::assemble( + wl(1), + basis_ref(), + members, + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32], [0x32; 32]], + }, + RevelationPosture::AuthorOnly, + ) + .unwrap(); + assert_eq!(forward.digest, reversed.digest); + + let mut tampered = forward; + tampered.outcome = BraidShellOutcome::Plural { + alternative_ids: vec![[0x32; 32], [0x31; 32]], + }; + assert_eq!( + tampered.validate(), + Err(BraidShellError::NonCanonicalAlternativeOrder) + ); + } + + #[test] + fn witness_digest_refuses_shrug_values() { + assert_eq!( + WitnessDigest::new([0; 32]), + Err(BraidShellError::EmptyWitness) + ); + assert_eq!( + WitnessDigest::new(crate::blake3_empty()), + Err(BraidShellError::EmptyWitness) + ); + assert!(WitnessDigest::new([0x99; 32]).is_ok()); + } + + #[test] + fn collapse_with_named_policy_derives_without_mutating_the_plural_parent() { + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let plural_digest = plural.digest; + let snapshot = plural.clone(); + let records = Records::with([plural]); + + let policy = CollapsePolicy { + policy_id: [0x77; 32], + witness: WitnessDigest::new([0x99; 32]).unwrap(), + }; + let CollapseResult::Derived(derived) = + collapse_braid_shell(&records, plural_digest, vec![basis_ref()], Some(policy)).unwrap() + else { + unreachable!("named collapse policy must derive"); + }; + + assert_eq!( + derived.outcome, + BraidShellOutcome::Derived { + result_refs: vec![basis_ref()], + collapse_policy: Some([0x77; 32]), + collapse_witness: Some([0x99; 32]), + collapsed_from: Some(plural_digest), + } + ); + assert_eq!(records.shell(&plural_digest), Some(&snapshot)); + + let mut store = Records::with([snapshot]); + store.0.insert(derived.digest, derived.clone()); + let replay = replay_braid_shell(&derived.digest, &store).unwrap(); + assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Derived); + } + + #[test] + fn collapse_without_policy_is_a_retained_obstruction() { + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let plural_digest = plural.digest; + let records = Records::with([plural]); + + let CollapseResult::Obstructed(obstructed) = + collapse_braid_shell(&records, plural_digest, Vec::new(), None).unwrap() + else { + unreachable!("collapse without policy must obstruct"); + }; + + assert_eq!(obstructed.outcome_kind(), AdmissionOutcomeKind::Obstruction); + let BraidShellOutcome::Obstruction { reason_code, .. } = obstructed.outcome else { + unreachable!("obstructed collapse carries an obstruction outcome"); + }; + assert_eq!(reason_code, COLLAPSE_WITHOUT_POLICY_REASON); + obstructed.validate().unwrap(); + } + + #[test] + fn tick_and_braid_records_are_one_boundary_family() { + let shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + assert_eq!(shell.boundary_kind(), RetainedBoundaryKind::Braid); + assert_eq!(shell.boundary_digest(), shell.digest); + } + + #[test] + fn query_matches_by_basis_member_outcome_and_posture() { + let shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + + assert!(shell.matches(&BraidShellQuery::default())); + assert!(shell.matches(&BraidShellQuery { + basis: Some(basis_ref()), + member_strand: Some(make_strand_id("member-a")), + outcome: Some(AdmissionOutcomeKind::Plural), + posture: Some(RevelationPosture::AuthorOnly), + })); + assert!(!shell.matches(&BraidShellQuery { + outcome: Some(AdmissionOutcomeKind::Conflict), + ..BraidShellQuery::default() + })); + assert!(!shell.matches(&BraidShellQuery { + member_strand: Some(make_strand_id("nobody")), + ..BraidShellQuery::default() + })); + } } diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 86d7701a..ad023846 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -257,8 +257,10 @@ pub use retained_evidence::{ pub use playback::{SessionId, ViewSession}; // --- Truth delivery --- pub use braid_shell::{ - replay_braid_shell, BraidShell, BraidShellError, BraidShellMember, BraidShellOutcome, - BraidShellRecords, BraidShellReplay, MemberVerdict, BRAID_SHELL_VERSION, + collapse_braid_shell, replay_braid_shell, BraidShell, BraidShellError, BraidShellMember, + BraidShellOutcome, BraidShellQuery, BraidShellRecords, BraidShellReplay, CollapsePolicy, + CollapseResult, MemberVerdict, RetainedBoundaryKind, RetainedBoundaryRecord, WitnessDigest, + BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, }; pub use neighborhood::{ NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index 03a67e26..d2759909 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -1725,6 +1725,8 @@ pub struct ProvenanceService { store: LocalProvenanceStore, /// Retained braid shells by canonical shell digest (θ_braid family). braid_shells: BTreeMap, + /// Residue → boundary index: plural artifact id to braid shell digest. + plural_shell_index: BTreeMap, } impl ProvenanceService { @@ -1760,10 +1762,40 @@ impl ProvenanceService { } return Ok(digest); } + if let crate::braid_shell::BraidShellOutcome::Plural { alternative_ids } = &shell.outcome { + for plural_id in alternative_ids { + self.plural_shell_index.insert(*plural_id, digest); + } + } self.braid_shells.insert(digest, shell); Ok(digest) } + /// Resolves a retained plural artifact id to its braid shell. + /// + /// The shell→residue link lives in the shell outcome; this index gives + /// the residue→shell direction so starting from a `PluralArtifact` + /// provenance event never requires scanning every shell. + #[must_use] + pub fn braid_shell_for_plural( + &self, + plural_id: &Hash, + ) -> Option<&crate::braid_shell::BraidShell> { + self.plural_shell_index + .get(plural_id) + .and_then(|digest| self.braid_shells.get(digest)) + } + + /// Scan-backed query over retained braid shells. + pub fn query_braid_shells( + &self, + query: crate::braid_shell::BraidShellQuery, + ) -> impl Iterator { + self.braid_shells + .values() + .filter(move |shell| shell.matches(&query)) + } + /// Returns the retained braid shell with the given digest, if any. #[must_use] pub fn braid_shell(&self, digest: &Hash) -> Option<&crate::braid_shell::BraidShell> { diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 8a5ccefb..bf3a5a24 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -1300,19 +1300,19 @@ fn build_braid_shell( for decision in &plan.decisions { match decision { SettlementDecision::ImportCandidate(candidate) => { - claim_hasher.update(&candidate.source_ref.commit_hash); + hash_claim_source_ref(&mut claim_hasher, candidate.source_ref); verdict_hasher.update(&[1]); verdict_hasher.update(&candidate.imported_op_id); } SettlementDecision::ConflictArtifact(draft) => { - claim_hasher.update(&draft.source_ref.commit_hash); + hash_claim_source_ref(&mut claim_hasher, draft.source_ref); verdict_hasher.update(&[2]); verdict_hasher.update(&draft.artifact_id); verdict_hasher.update(&[draft.reason.code()]); conflict_codes.push(draft.reason.code()); } SettlementDecision::PluralAlternative(draft) => { - claim_hasher.update(&draft.source_ref.commit_hash); + hash_claim_source_ref(&mut claim_hasher, draft.source_ref); verdict_hasher.update(&[3]); verdict_hasher.update(&draft.plural_id); plural_ids.push(draft.plural_id); @@ -1334,6 +1334,7 @@ fn build_braid_shell( BraidShellOutcome::Derived { result_refs: appended_imports.to_vec(), collapse_policy: None, + collapse_witness: None, collapsed_from: None, } } else { @@ -1370,15 +1371,32 @@ fn build_braid_shell( ) } +fn hash_claim_source_ref(hasher: &mut Hasher, source_ref: ProvenanceRef) { + hasher.update(source_ref.worldline_id.as_bytes()); + hasher.update(&source_ref.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&source_ref.commit_hash); +} + fn support_pins_digest(pins: &[crate::strand::SupportPin]) -> Hash { + // Pin order is not semantic; sort encoded pins so the digest cannot + // depend on registration order. + let mut encoded: Vec> = pins + .iter() + .map(|pin| { + let mut bytes = Vec::new(); + bytes.extend_from_slice(pin.strand_id.as_bytes()); + bytes.extend_from_slice(pin.worldline_id.as_bytes()); + bytes.extend_from_slice(&pin.pinned_tick.as_u64().to_le_bytes()); + bytes.extend_from_slice(&pin.state_hash); + bytes + }) + .collect(); + encoded.sort_unstable(); let mut hasher = Hasher::new(); hasher.update(b"echo.braid.member.pins.v1\0"); - hasher.update(&(pins.len() as u64).to_le_bytes()); - for pin in pins { - hasher.update(pin.strand_id.as_bytes()); - hasher.update(pin.worldline_id.as_bytes()); - hasher.update(&pin.pinned_tick.as_u64().to_le_bytes()); - hasher.update(&pin.state_hash); + hasher.update(&(encoded.len() as u64).to_le_bytes()); + for pin_bytes in &encoded { + hasher.update(pin_bytes); } hasher.finalize().into() } @@ -1758,6 +1776,7 @@ mod tests { #[derive(Clone, Copy)] enum ParentDrift { None, + NoSuffix, Disjoint, OverlapSame, OverlapSamePlusDisjointChild, @@ -1836,7 +1855,7 @@ mod tests { runtime.register_strand(strand).unwrap(); let parent_drift_patch = match parent_drift { - ParentDrift::None => None, + ParentDrift::None | ParentDrift::NoSuffix => None, ParentDrift::Disjoint => Some(node_upsert_patch(&base_state, "base-diverged", gt(2))), ParentDrift::OverlapSame => Some(node_upsert_patch(&base_state, "child-node", gt(2))), ParentDrift::OverlapSamePlusDisjointChild => Some(node_record_upsert_patch( @@ -1894,25 +1913,30 @@ mod tests { } let child_patch = match parent_drift { - ParentDrift::OverlapSamePlusDisjointChild => overlap_plus_disjoint_node_patch( + ParentDrift::OverlapSamePlusDisjointChild => Some(overlap_plus_disjoint_node_patch( &child_state, "overlap-node", "disjoint-node", gt(3), - ), + )), ParentDrift::None | ParentDrift::Disjoint | ParentDrift::OverlapSame - | ParentDrift::OverlapDifferent => node_upsert_patch(&child_state, "child-node", gt(3)), + | ParentDrift::OverlapDifferent => { + Some(node_upsert_patch(&child_state, "child-node", gt(3))) + } + ParentDrift::NoSuffix => None, }; - append_local_patch( - &mut child_state, - &mut provenance, - child_worldline, - child_head, - gt(3), - child_patch, - ); + if let Some(child_patch) = child_patch { + append_local_patch( + &mut child_state, + &mut provenance, + child_worldline, + child_head, + gt(3), + child_patch, + ); + } runtime = WorldlineRuntime::new(); runtime .register_worldline(base_worldline, base_state) @@ -2462,6 +2486,109 @@ mod tests { ); } + #[test] + fn empty_settlement_emits_no_shell_by_law() { + // Law: no claims means no braid outcome. An empty settlement is not + // a braid-scope settlement act and retains no theta_braid shell. + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::NoSuffix); + + let result = SettlementService::settle(&mut runtime, &mut provenance, strand_id).unwrap(); + assert!(result.plan.decisions.is_empty()); + assert_eq!(result.braid_shell, None); + assert_eq!(provenance.braid_shells().count(), 0); + } + + #[test] + fn failed_settlement_retains_no_shell() { + // Law: no failed settlement may leak a shell. A lie with a valid + // digest is the exact bug this system exists to murder. + let (mut runtime, mut provenance, strand_id, _, child_worldline) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + // Induce runtime/provenance drift on the source lane so settlement + // fails after planning begins. + let head_key = WriterHeadKey { + worldline_id: child_worldline, + head_id: make_head_id("drift-head"), + }; + let mut drift_state = runtime + .worldlines() + .get(&child_worldline) + .unwrap() + .state() + .clone(); + let drift_patch = node_upsert_patch(&drift_state, "drift-node", gt(9)); + append_local_patch( + &mut drift_state, + &mut provenance, + child_worldline, + head_key, + gt(9), + drift_patch, + ); + + let outcome = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ); + assert!(outcome.is_err()); + assert_eq!(provenance.braid_shells().count(), 0); + } + + #[test] + fn plural_artifact_resolves_to_its_braid_shell() { + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + let result = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + let shell_digest = result.braid_shell.unwrap(); + let plural_id = result + .plan + .decisions + .iter() + .find_map(|decision| plural_alternative(decision).map(|draft| draft.plural_id)) + .unwrap(); + + let resolved = provenance.braid_shell_for_plural(&plural_id).unwrap(); + assert_eq!(resolved.digest, shell_digest); + } + + #[test] + fn braid_shells_are_queryable_by_member_outcome_and_posture() { + let (mut runtime, mut provenance, strand_id, _, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ) + .unwrap(); + + let query = crate::braid_shell::BraidShellQuery { + member_strand: Some(strand_id), + outcome: Some(AdmissionOutcomeKind::Plural), + posture: Some(crate::revelation::RevelationPosture::AuthorOnly), + ..crate::braid_shell::BraidShellQuery::default() + }; + assert_eq!(provenance.query_braid_shells(query).count(), 1); + + let miss = crate::braid_shell::BraidShellQuery { + outcome: Some(AdmissionOutcomeKind::Conflict), + ..crate::braid_shell::BraidShellQuery::default() + }; + assert_eq!(provenance.query_braid_shells(miss).count(), 0); + } + #[test] fn slot_order_does_not_move_plural_id_or_overlap_digest() { let warp_id = crate::ident::make_warp_id("plural-slot-order"); From 7553c62b9f19ef8c8721fca2b7de4fdfcd430ea3 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 13:26:57 -0700 Subject: [PATCH 11/14] docs(0026): record E1 landed laws (empty-settlement, no-leak, family trait, canonical sets, residue links, collapse) --- .../design.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md index 8289cbce..4c24c72b 100644 --- a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -221,6 +221,29 @@ Named debts from the checkpoint review: empty/null witness digests or introduce a `WitnessDigest` newtype. A witness must never be a 32-byte shrug. +## E1 landed laws (post-θ_braid review, 2026-06-12) + +- **Empty-settlement law:** an empty settlement is not a braid-scope + settlement act and emits no θ_braid (no claims means no braid outcome). + Tested as `empty_settlement_emits_no_shell_by_law`. +- **No-leak law:** no failed settlement may leak a shell; the shell is the + final fallible step of `settle_with_policy`. Tested as + `failed_settlement_retains_no_shell`. +- **One boundary family in code:** `RetainedBoundaryRecord` / + `RetainedBoundaryKind` are implemented by both + `BoundaryTransitionRecord` (θ_tick) and `BraidShell` (θ_braid); + θ_import joins later. +- **Canonical set order:** plural `alternative_ids` are a sorted set + (member verdict digests bind the ordered transcript); support pins and + overlap slots digest in canonical order. +- **Residue ↔ boundary, both directions:** shell outcome lists plural + artifact ids; `braid_shell_for_plural` resolves residue → shell. +- **Collapse law:** `collapse_braid_shell` never mutates the plural + parent; named witnessed policy → new `Derived` shell with full lineage + (`collapse_policy` + `collapse_witness` + `collapsed_from`, all-or-none + coherent); missing policy → retained `Obstruction` shell. `WitnessDigest` + refuses zero/empty digests. + ## Acceptance criteria (enhanced per review) 1. `SettlementDecision` gains a `Plural` arm carrying surviving From 81501d1a0059031499dbdc266b3b1d513301553b Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 17:51:00 -0700 Subject: [PATCH 12/14] fix(warp-core): theta_braid record-law tightening per #539 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blockers: 1. BoundaryTransitionRecord::boundary_digest is now a full canonical content digest: payload coordinates, every entry's coordinate, event kind, head key, parents, and hash triplet, plus the auth tag — a record digest, not a vibes checksum 2. WitnessDigest side door closed: check_outcome_law applies the newtype bar to collapse_witness and obstruction witnesses in both assemble() and validate(); empty-witness shells refuse to exist 3. plural_shell_index is append-only: a plural artifact id may never migrate to a different shell (PluralArtifactAlreadyBound); bindings are checked before anything inserts, so refusal leaves no partial state 4. take_braid_shells clears the residue index — no stale archaeology 5. BraidCoordinate(hash(basis, canonical member digests, policy_id)) stored on every shell, recomputed at validation (CoordinateMismatch), queryable via BraidShellQuery.coordinate — EchoCoordinate::Braid stops being decorative Should-fixes: - Obstruction outcomes carry obstructed_from: Option; no-policy collapse names its plural parent; replay verifies obstruction lineage the same way it verifies collapse lineage - duplicate alternative_ids refused (a sorted list with duplicates is not a set, it is a list wearing a fake mustache) - duplicate member strands refused (N-capable invariant landed now) - late no-leak proof: pre-binding the deterministic plural id forces shell retention to fail AFTER entries append; test proves entries and frontier roll back, no shell leaks, index unchanged 516/516 warp-core tests; workspace clean under -D warnings. --- crates/warp-core/src/braid_shell.rs | 342 ++++++++++++++++++++++- crates/warp-core/src/lib.rs | 8 +- crates/warp-core/src/provenance_store.rs | 20 ++ crates/warp-core/src/settlement.rs | 80 ++++++ 4 files changed, 436 insertions(+), 14 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 84bf5a29..531d1b22 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -29,6 +29,7 @@ use crate::worldline::WorldlineId; const SHELL_DOMAIN: &[u8] = b"echo.shell.braid.v1\0"; const MEMBER_DOMAIN: &[u8] = b"echo.braid.member.v1\0"; const WITNESS_DOMAIN: &[u8] = b"echo.braid.witness.v1\0"; +const COORDINATE_DOMAIN: &[u8] = b"echo.braid.coordinate.v1\0"; /// Current braid shell body version. pub const BRAID_SHELL_VERSION: u32 = 1; @@ -133,6 +134,8 @@ pub enum BraidShellOutcome { reason_code: u8, /// Witness digest for the obstruction. witness: Hash, + /// Shell whose transition this obstruction refused, when applicable. + obstructed_from: Option, }, } @@ -180,10 +183,12 @@ impl BraidShellOutcome { Self::Obstruction { reason_code, witness, + obstructed_from, } => { hasher.update(&[4]); hasher.update(&[*reason_code]); hasher.update(witness); + hash_optional_digest(hasher, obstructed_from.as_ref()); } } } @@ -276,6 +281,52 @@ pub enum BraidShellError { /// A witness digest must never be a 32-byte shrug. #[error("empty or null witness digest refused")] EmptyWitness, + /// The stored braid coordinate does not match the recomputed body. + #[error("braid coordinate mismatch")] + CoordinateMismatch, + /// Plural alternatives are a set; duplicates are refused. + #[error("duplicate plural alternative id {alternative_id:?}")] + DuplicateAlternativeId { + /// Alternative id that appeared more than once. + alternative_id: Hash, + }, + /// One strand may appear at most once among shell members. + #[error("duplicate member strand {strand_id:?}")] + DuplicateMemberStrand { + /// Strand that appeared more than once. + strand_id: StrandId, + }, + /// A retained plural artifact id may never migrate to a different shell. + #[error("plural artifact {plural_id:?} already bound to shell {existing_shell:?}")] + PluralArtifactAlreadyBound { + /// Plural artifact id being bound. + plural_id: Hash, + /// Shell digest the id is already bound to. + existing_shell: Hash, + /// Shell digest the rebind attempted. + attempted_shell: Hash, + }, +} + +/// Canonical coordinate of a braid: where the shell lives in braid space. +/// +/// `hash(basis_ref, canonical_member_digest_list, policy_id)` — the first +/// real consumer of braid-scale addressing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct BraidCoordinate(pub Hash); + +impl BraidCoordinate { + fn derive(basis: &ProvenanceRef, members: &[BraidShellMember], policy_id: Hash) -> Self { + let mut hasher = Hasher::new(); + hasher.update(COORDINATE_DOMAIN); + hash_provenance_ref(&mut hasher, basis); + hasher.update(&(members.len() as u64).to_le_bytes()); + for member in members { + hasher.update(&member.member_digest()); + } + hasher.update(&policy_id); + Self(hasher.finalize().into()) + } } /// Witness digest with a quality bar: zero and empty-input digests refused. @@ -308,6 +359,8 @@ impl WitnessDigest { pub struct BraidShell { /// Shell body version. pub version: u32, + /// Canonical braid coordinate this shell lives at. + pub coordinate: BraidCoordinate, /// Target worldline the settlement judged into. pub worldline_id: WorldlineId, /// Comparison basis the members were judged against. @@ -347,11 +400,22 @@ impl BraidShell { return Err(BraidShellError::EmptyMembers); } members.sort_by_key(BraidShellMember::member_digest); + check_unique_member_strands(&members)?; if let BraidShellOutcome::Plural { alternative_ids } = &mut outcome { // Retained alternatives are a set; canonical order, not transcript // order (the member verdict digest binds the ordered transcript). alternative_ids.sort_unstable(); + if let Some(duplicate) = alternative_ids + .windows(2) + .find(|pair| pair[0] == pair[1]) + .map(|pair| pair[0]) + { + return Err(BraidShellError::DuplicateAlternativeId { + alternative_id: duplicate, + }); + } } + check_outcome_law(&outcome)?; if let Some(obstruction) = shell_posture_obstruction(posture, members.iter().map(|member| member.posture)) { @@ -359,6 +423,7 @@ impl BraidShell { } check_outcome_member_coherence(&outcome, &members)?; + let coordinate = BraidCoordinate::derive(&basis, &members, policy_id); let witness_digest = compute_witness_digest( BRAID_SHELL_VERSION, worldline_id, @@ -380,6 +445,7 @@ impl BraidShell { ); Ok(Self { version: BRAID_SHELL_VERSION, + coordinate, worldline_id, basis, members, @@ -418,11 +484,22 @@ impl BraidShell { } previous = Some(current); } + check_unique_member_strands(&self.members)?; if let BraidShellOutcome::Plural { alternative_ids } = &self.outcome { if alternative_ids.windows(2).any(|pair| pair[0] > pair[1]) { return Err(BraidShellError::NonCanonicalAlternativeOrder); } + if let Some(duplicate) = alternative_ids + .windows(2) + .find(|pair| pair[0] == pair[1]) + .map(|pair| pair[0]) + { + return Err(BraidShellError::DuplicateAlternativeId { + alternative_id: duplicate, + }); + } } + check_outcome_law(&self.outcome)?; if let Some(obstruction) = shell_posture_obstruction( self.posture, self.members.iter().map(|member| member.posture), @@ -430,6 +507,9 @@ impl BraidShell { return Err(BraidShellError::PostureExceedsMembers(obstruction)); } check_outcome_member_coherence(&self.outcome, &self.members)?; + if BraidCoordinate::derive(&self.basis, &self.members, self.policy_id) != self.coordinate { + return Err(BraidShellError::CoordinateMismatch); + } let witness = compute_witness_digest( self.version, @@ -480,6 +560,37 @@ impl BraidShell { } } +/// Witness-bearing outcome fields must clear the [`WitnessDigest`] bar: +/// the newtype bouncer guards every door, not just the constructor. +fn check_outcome_law(outcome: &BraidShellOutcome) -> Result<(), BraidShellError> { + match outcome { + BraidShellOutcome::Derived { + collapse_witness: Some(witness), + .. + } + | BraidShellOutcome::Obstruction { witness, .. } => { + WitnessDigest::new(*witness)?; + } + _ => {} + } + Ok(()) +} + +/// One strand may appear at most once among shell members. +fn check_unique_member_strands(members: &[BraidShellMember]) -> Result<(), BraidShellError> { + for (index, member) in members.iter().enumerate() { + if members[..index] + .iter() + .any(|earlier| earlier.strand_ref == member.strand_ref) + { + return Err(BraidShellError::DuplicateMemberStrand { + strand_id: member.strand_ref, + }); + } + } + Ok(()) +} + fn check_outcome_member_coherence( outcome: &BraidShellOutcome, members: &[BraidShellMember], @@ -656,21 +767,28 @@ pub fn replay_braid_shell( recomputed: shell.digest, }); } - if let BraidShellOutcome::Derived { - collapsed_from: Some(parent_digest), - .. - } = &shell.outcome - { + let lineage_parent = match &shell.outcome { + BraidShellOutcome::Derived { + collapsed_from: Some(parent_digest), + .. + } + | BraidShellOutcome::Obstruction { + obstructed_from: Some(parent_digest), + .. + } => Some(*parent_digest), + _ => None, + }; + if let Some(parent_digest) = lineage_parent { let parent = records - .shell(parent_digest) + .shell(&parent_digest) .ok_or(BraidShellError::InvalidCollapseLineage { - collapsed_from: *parent_digest, + collapsed_from: parent_digest, })?; parent.validate()?; if !matches!(parent.outcome, BraidShellOutcome::Plural { .. }) { return Err(BraidShellError::InvalidCollapseLineage { - collapsed_from: *parent_digest, + collapsed_from: parent_digest, }); } } @@ -771,6 +889,7 @@ pub fn collapse_braid_shell( BraidShellOutcome::Obstruction { reason_code: COLLAPSE_WITHOUT_POLICY_REASON, witness: witness_hasher.finalize().into(), + obstructed_from: Some(plural_shell_digest), }, plural.posture, )?; @@ -809,11 +928,54 @@ impl RetainedBoundaryRecord for BraidShell { const TICK_SHELL_DOMAIN: &[u8] = b"echo.shell.tick.v1\0"; +fn hash_event_kind(hasher: &mut Hasher, kind: &crate::provenance_store::ProvenanceEventKind) { + use crate::provenance_store::ProvenanceEventKind; + match kind { + ProvenanceEventKind::LocalCommit => { + hasher.update(&[1]); + } + ProvenanceEventKind::CrossWorldlineMessage { + source_worldline, + source_worldline_tick, + message_id, + } => { + hasher.update(&[2]); + hasher.update(source_worldline.as_bytes()); + hasher.update(&source_worldline_tick.as_u64().to_le_bytes()); + hasher.update(message_id); + } + ProvenanceEventKind::MergeImport { + source_worldline, + source_worldline_tick, + op_id, + } => { + hasher.update(&[3]); + hasher.update(source_worldline.as_bytes()); + hasher.update(&source_worldline_tick.as_u64().to_le_bytes()); + hasher.update(op_id); + } + ProvenanceEventKind::ConflictArtifact { artifact_id } => { + hasher.update(&[4]); + hasher.update(artifact_id); + } + ProvenanceEventKind::PluralArtifact { plural_id } => { + hasher.update(&[5]); + hasher.update(plural_id); + } + } +} + impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecord { fn boundary_kind(&self) -> RetainedBoundaryKind { RetainedBoundaryKind::Tick } + /// Full canonical content digest over the retained BTR body: boundary + /// facts, payload coordinates, every entry's coordinate, event kind, + /// head key, parents, and hash triplet, plus the auth tag. The entry + /// commit hash transitively binds each patch digest, state root, and + /// parent set, but the coordinates and event facts are bound explicitly + /// here — this is a record digest, not a vibes checksum. fn boundary_digest(&self) -> Hash { let mut hasher = Hasher::new(); hasher.update(TICK_SHELL_DOMAIN); @@ -822,10 +984,34 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor hasher.update(&self.input_boundary_hash); hasher.update(&self.output_boundary_hash); hasher.update(&self.logical_counter.to_le_bytes()); + hasher.update(self.payload.worldline_id.as_bytes()); + hasher.update(&self.payload.start_worldline_tick.as_u64().to_le_bytes()); hasher.update(&(self.payload.entries.len() as u64).to_le_bytes()); for entry in &self.payload.entries { + hasher.update(entry.worldline_id.as_bytes()); + hasher.update(&entry.worldline_tick.as_u64().to_le_bytes()); + hasher.update(&entry.commit_global_tick.as_u64().to_le_bytes()); + match &entry.head_key { + Some(head_key) => { + hasher.update(&[1]); + hasher.update(head_key.worldline_id.as_bytes()); + hasher.update(head_key.head_id.as_bytes()); + } + None => { + hasher.update(&[0]); + } + } + hash_event_kind(&mut hasher, &entry.event_kind); + hasher.update(&(entry.parents.len() as u64).to_le_bytes()); + for parent in &entry.parents { + hash_provenance_ref(&mut hasher, parent); + } + hasher.update(&entry.expected.state_root); + hasher.update(&entry.expected.patch_digest); hasher.update(&entry.expected.commit_hash); } + hasher.update(&(self.auth_tag.len() as u64).to_le_bytes()); + hasher.update(&self.auth_tag); hasher.finalize().into() } } @@ -833,6 +1019,8 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor /// Scan-backed query over retained braid shells. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct BraidShellQuery { + /// Match the shell at this braid coordinate. + pub coordinate: Option, /// Match shells judged against this comparison basis. pub basis: Option, /// Match shells summarizing this member strand. @@ -847,7 +1035,10 @@ impl BraidShell { /// Returns whether the shell matches every present query field. #[must_use] pub fn matches(&self, query: &BraidShellQuery) -> bool { - query.basis.is_none_or(|basis| self.basis == basis) + query + .coordinate + .is_none_or(|coordinate| self.coordinate == coordinate) + && query.basis.is_none_or(|basis| self.basis == basis) && query .member_strand .as_ref() @@ -975,7 +1166,9 @@ mod tests { verdict_tampered.members[0].verdict_digest = [0xBB; 32]; assert!(matches!( verdict_tampered.validate(), - Err(BraidShellError::WitnessMismatch { .. } | BraidShellError::DigestMismatch { .. }) + Err(BraidShellError::WitnessMismatch { .. } + | BraidShellError::DigestMismatch { .. } + | BraidShellError::CoordinateMismatch) )); let mut posture_tampered = shell.clone(); @@ -992,6 +1185,134 @@ mod tests { assert!(outcome_tampered.validate().is_err()); } + #[test] + fn tampering_with_coordinate_fails_validation() { + let mut shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + shell.coordinate = BraidCoordinate([0xCC; 32]); + assert_eq!(shell.validate(), Err(BraidShellError::CoordinateMismatch)); + } + + #[test] + fn derived_shell_rejects_empty_collapse_witness() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Derived)], + [0x5E; 32], + BraidShellOutcome::Derived { + result_refs: vec![basis_ref()], + collapse_policy: Some([0x77; 32]), + collapse_witness: Some([0; 32]), + collapsed_from: Some([0x88; 32]), + }, + RevelationPosture::AuthorOnly, + ); + assert_eq!(result, Err(BraidShellError::EmptyWitness)); + } + + #[test] + fn obstruction_shell_rejects_empty_witness() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Obstructed)], + [0x5E; 32], + BraidShellOutcome::Obstruction { + reason_code: 1, + witness: crate::blake3_empty(), + obstructed_from: None, + }, + RevelationPosture::AuthorOnly, + ); + assert_eq!(result, Err(BraidShellError::EmptyWitness)); + } + + #[test] + fn duplicate_alternative_ids_are_refused() { + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Plural)], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32], [0x31; 32]], + }, + RevelationPosture::AuthorOnly, + ); + assert_eq!( + result, + Err(BraidShellError::DuplicateAlternativeId { + alternative_id: [0x31; 32], + }) + ); + } + + #[test] + fn duplicate_member_strands_are_refused() { + let mut duplicate = member("member-a", MemberVerdict::Plural); + duplicate.claim_digest = [0x99; 32]; + let result = BraidShell::assemble( + wl(1), + basis_ref(), + vec![member("member-a", MemberVerdict::Plural), duplicate], + [0x5E; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[0x31; 32]], + }, + RevelationPosture::AuthorOnly, + ); + assert_eq!( + result, + Err(BraidShellError::DuplicateMemberStrand { + strand_id: make_strand_id("member-a"), + }) + ); + } + + #[test] + fn obstructed_collapse_names_its_plural_parent() { + let plural = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + let plural_digest = plural.digest; + let records = Records::with([plural.clone()]); + + let CollapseResult::Obstructed(obstructed) = + collapse_braid_shell(&records, plural_digest, Vec::new(), None).unwrap() + else { + unreachable!("collapse without policy must obstruct"); + }; + let BraidShellOutcome::Obstruction { + obstructed_from, .. + } = obstructed.outcome + else { + unreachable!("obstructed collapse carries an obstruction outcome"); + }; + assert_eq!(obstructed_from, Some(plural_digest)); + + // Replay verifies the named parent is retained and plural. + let mut store = Records::with([plural]); + let CollapseResult::Obstructed(obstructed) = + collapse_braid_shell(&store, plural_digest, Vec::new(), None).unwrap() + else { + unreachable!(); + }; + store.0.insert(obstructed.digest, obstructed.clone()); + let replay = replay_braid_shell(&obstructed.digest, &store).unwrap(); + assert_eq!(replay.outcome_kind, AdmissionOutcomeKind::Obstruction); + } + + #[test] + fn shells_are_queryable_by_coordinate() { + let shell = plural_shell(vec![member("member-a", MemberVerdict::Plural)]); + assert!(shell.matches(&BraidShellQuery { + coordinate: Some(shell.coordinate), + ..BraidShellQuery::default() + })); + assert!(!shell.matches(&BraidShellQuery { + coordinate: Some(BraidCoordinate([0xDD; 32])), + ..BraidShellQuery::default() + })); + } + #[test] fn shell_posture_cannot_exceed_least_revealed_member() { let result = BraidShell::assemble( @@ -1193,6 +1514,7 @@ mod tests { assert!(shell.matches(&BraidShellQuery::default())); assert!(shell.matches(&BraidShellQuery { + coordinate: Some(shell.coordinate), basis: Some(basis_ref()), member_strand: Some(make_strand_id("member-a")), outcome: Some(AdmissionOutcomeKind::Plural), diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index ad023846..870642b6 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -257,10 +257,10 @@ pub use retained_evidence::{ pub use playback::{SessionId, ViewSession}; // --- Truth delivery --- pub use braid_shell::{ - collapse_braid_shell, replay_braid_shell, BraidShell, BraidShellError, BraidShellMember, - BraidShellOutcome, BraidShellQuery, BraidShellRecords, BraidShellReplay, CollapsePolicy, - CollapseResult, MemberVerdict, RetainedBoundaryKind, RetainedBoundaryRecord, WitnessDigest, - BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, + collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidShell, BraidShellError, + BraidShellMember, BraidShellOutcome, BraidShellQuery, BraidShellRecords, BraidShellReplay, + CollapsePolicy, CollapseResult, MemberVerdict, RetainedBoundaryKind, RetainedBoundaryRecord, + WitnessDigest, BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, }; pub use neighborhood::{ NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index d2759909..4b8d6c36 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -1762,7 +1762,24 @@ impl ProvenanceService { } return Ok(digest); } + // Index bindings are append-only retained truth: a plural artifact + // id may never migrate to a different shell. Check every binding + // before inserting anything so a refused append leaves no partial + // state behind. if let crate::braid_shell::BraidShellOutcome::Plural { alternative_ids } = &shell.outcome { + for plural_id in alternative_ids { + if let Some(existing) = self.plural_shell_index.get(plural_id) { + if *existing != digest { + return Err( + crate::braid_shell::BraidShellError::PluralArtifactAlreadyBound { + plural_id: *plural_id, + existing_shell: *existing, + attempted_shell: digest, + }, + ); + } + } + } for plural_id in alternative_ids { self.plural_shell_index.insert(*plural_id, digest); } @@ -1814,6 +1831,9 @@ impl ProvenanceService { /// replay outcomes from the shells alone. #[must_use] pub fn take_braid_shells(&mut self) -> BTreeMap { + // The residue index describes the shells; taking one without the + // other would leave the service internally inconsistent. + self.plural_shell_index.clear(); std::mem::take(&mut self.braid_shells) } diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index bf3a5a24..7bc727d1 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -2538,6 +2538,86 @@ mod tests { assert_eq!(provenance.braid_shells().count(), 0); } + #[test] + fn late_shell_retention_failure_rolls_back_entries_and_leaks_nothing() { + let (mut runtime, mut provenance, strand_id, base_worldline, _) = + setup_runtime_with_strand(ParentDrift::OverlapDifferent); + + // Compute the deterministic plural id this settlement will produce, + // then pre-bind it to a different shell so the real settlement's + // shell append fails AFTER entries are appended. + let plan = + SettlementService::plan_with_policy(&runtime, &provenance, strand_id, &plural_policy()) + .unwrap(); + let plural_id = plan + .decisions + .iter() + .find_map(|decision| plural_alternative(decision).map(|draft| draft.plural_id)) + .unwrap(); + let dummy = crate::braid_shell::BraidShell::assemble( + base_worldline, + plan.target_base_ref, + vec![crate::braid_shell::BraidShellMember { + strand_ref: crate::strand::make_strand_id("dummy-binder"), + support_pin_digest: [1; 32], + basis_digest: [2; 32], + frontier_digest: [3; 32], + footprint_digest: [4; 32], + claim_digest: [5; 32], + verdict: crate::braid_shell::MemberVerdict::Plural, + verdict_digest: [6; 32], + posture: crate::revelation::RevelationPosture::AuthorOnly, + }], + [0xAB; 32], + crate::braid_shell::BraidShellOutcome::Plural { + alternative_ids: vec![plural_id], + }, + crate::revelation::RevelationPosture::AuthorOnly, + ) + .unwrap(); + let dummy_digest = provenance.append_braid_shell(dummy).unwrap(); + + let entries_before = provenance.len(base_worldline).unwrap(); + let frontier_before = runtime + .worldlines() + .get(&base_worldline) + .unwrap() + .frontier_tick(); + + let outcome = SettlementService::settle_with_policy( + &mut runtime, + &mut provenance, + strand_id, + &plural_policy(), + ); + assert!(matches!( + outcome, + Err(SettlementError::BraidShell( + crate::braid_shell::BraidShellError::PluralArtifactAlreadyBound { .. } + )) + )); + + // Rollback law: entries restored, runtime restored, no new shell, + // residue index unchanged. The pants exist. + assert_eq!(provenance.len(base_worldline).unwrap(), entries_before); + assert_eq!( + runtime + .worldlines() + .get(&base_worldline) + .unwrap() + .frontier_tick(), + frontier_before + ); + assert_eq!(provenance.braid_shells().count(), 1); + assert_eq!( + provenance + .braid_shell_for_plural(&plural_id) + .unwrap() + .digest, + dummy_digest + ); + } + #[test] fn plural_artifact_resolves_to_its_braid_shell() { let (mut runtime, mut provenance, strand_id, _, _) = From 00233fc72ae12d9cad033bff4bb31873b92562f6 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 18:12:43 -0700 Subject: [PATCH 13/14] fix(warp-core): BTR boundary_digest binds patch, outputs, and atom writes The remaining #539 merge blocker: ProvenanceEntry's retained patch body (header fields explicitly, since patch_digest does not bind the header, plus the canonical patch_digest which binds ops and slots), materialization outputs (channel + frame bytes), and atom-write provenance (atom, rule, tick, old/new values) are now bound by the boundary digest. No retained field named like content is excluded. Sensitivity test proves it: stripping the patch, appending an output frame, or appending an atom write each move the digest. 517/517 warp-core tests; workspace clean under -D warnings. --- crates/warp-core/src/braid_shell.rs | 54 ++++++++++++++++++++++++++--- crates/warp-core/src/settlement.rs | 47 +++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 531d1b22..69bce8cc 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -971,11 +971,13 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor } /// Full canonical content digest over the retained BTR body: boundary - /// facts, payload coordinates, every entry's coordinate, event kind, - /// head key, parents, and hash triplet, plus the auth tag. The entry - /// commit hash transitively binds each patch digest, state root, and - /// parent set, but the coordinates and event facts are bound explicitly - /// here — this is a record digest, not a vibes checksum. + /// facts, payload coordinates, and for every entry its coordinate, + /// event kind, head key, parents, hash triplet, retained patch body + /// commitment (header fields explicitly — `patch_digest` does not bind + /// the header — plus the canonical `patch_digest` which binds ops and + /// slots), materialization outputs, and atom-write provenance, plus the + /// auth tag. No retained field named like content is excluded — this is + /// a record digest, not a vibes checksum. fn boundary_digest(&self) -> Hash { let mut hasher = Hasher::new(); hasher.update(TICK_SHELL_DOMAIN); @@ -1009,6 +1011,48 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor hasher.update(&entry.expected.state_root); hasher.update(&entry.expected.patch_digest); hasher.update(&entry.expected.commit_hash); + match &entry.patch { + Some(patch) => { + hasher.update(&[1]); + hasher.update(&patch.header.commit_global_tick.as_u64().to_le_bytes()); + hasher.update(&patch.header.policy_id.to_le_bytes()); + hasher.update(&patch.header.rule_pack_id); + hasher.update(&patch.header.plan_digest); + hasher.update(&patch.header.decision_digest); + hasher.update(&patch.header.rewrites_digest); + hasher.update(patch.warp_id.as_bytes()); + hasher.update(&patch.patch_digest); + } + None => { + hasher.update(&[0]); + } + } + hasher.update(&(entry.outputs.len() as u64).to_le_bytes()); + for (channel, data) in &entry.outputs { + hasher.update(&(channel.0.len() as u64).to_le_bytes()); + hasher.update(channel.0.as_ref()); + hasher.update(&(data.len() as u64).to_le_bytes()); + hasher.update(data); + } + hasher.update(&(entry.atom_writes.len() as u64).to_le_bytes()); + for atom_write in &entry.atom_writes { + hasher.update(atom_write.atom.warp_id.as_bytes()); + hasher.update(atom_write.atom.local_id.as_bytes()); + hasher.update(&atom_write.rule_id); + hasher.update(&atom_write.tick.to_le_bytes()); + match &atom_write.old_value { + Some(old_value) => { + hasher.update(&[1]); + hasher.update(&(old_value.len() as u64).to_le_bytes()); + hasher.update(old_value); + } + None => { + hasher.update(&[0]); + } + } + hasher.update(&(atom_write.new_value.len() as u64).to_le_bytes()); + hasher.update(&atom_write.new_value); + } } hasher.update(&(self.auth_tag.len() as u64).to_le_bytes()); hasher.update(&self.auth_tag); diff --git a/crates/warp-core/src/settlement.rs b/crates/warp-core/src/settlement.rs index 7bc727d1..a8a679d3 100644 --- a/crates/warp-core/src/settlement.rs +++ b/crates/warp-core/src/settlement.rs @@ -2486,6 +2486,53 @@ mod tests { ); } + #[test] + fn btr_boundary_digest_binds_patch_outputs_and_atom_writes() { + use crate::braid_shell::RetainedBoundaryRecord; + let (_, provenance, _, base_worldline, _) = setup_runtime_with_strand(ParentDrift::None); + let entry = provenance.entry(base_worldline, wt(0)).unwrap(); + let warp_id = entry.patch.as_ref().unwrap().warp_id; + let make_btr = |entry: ProvenanceEntry| crate::provenance_store::BoundaryTransitionRecord { + worldline_id: base_worldline, + u0_ref: warp_id, + input_boundary_hash: [0; 32], + output_boundary_hash: entry.expected.state_root, + payload: crate::provenance_store::BtrPayload { + worldline_id: base_worldline, + start_worldline_tick: wt(0), + entries: vec![entry], + }, + logical_counter: 1, + auth_tag: Vec::new(), + }; + let baseline = make_btr(entry.clone()).boundary_digest(); + + let mut patch_stripped = entry.clone(); + patch_stripped.patch = None; + assert_ne!(make_btr(patch_stripped).boundary_digest(), baseline); + + let mut outputs_changed = entry.clone(); + outputs_changed + .outputs + .push((make_type_id("tamper-channel"), vec![1, 2, 3])); + assert_ne!(make_btr(outputs_changed).boundary_digest(), baseline); + + let mut atoms_changed = entry; + atoms_changed + .atom_writes + .push(crate::worldline::AtomWrite::new( + crate::ident::NodeKey { + warp_id, + local_id: make_node_id("tampered-atom"), + }, + [0xEE; 32], + 7, + None, + vec![9, 9, 9], + )); + assert_ne!(make_btr(atoms_changed).boundary_digest(), baseline); + } + #[test] fn empty_settlement_emits_no_shell_by_law() { // Law: no claims means no braid outcome. An empty settlement is not From 566d08e147391025a8a44157c7f2b5c8b5f8e527 Mon Sep 17 00:00:00 2001 From: James Ross Date: Fri, 12 Jun 2026 20:03:23 -0700 Subject: [PATCH 14/14] fix(warp-core): resolve Code Lawyer review findings on theta_braid (0026) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eleven findings (0 critical / 1 major / 3 medium / 7 low), all resolved: M1 (major) — no-leak becomes a mechanism: ProvenanceCheckpoint snapshots the braid-shell + plural-index key sets and restore() prunes anything retained after the checkpoint, so a rolled-back settlement cannot leak a shell describing vanished history. RED test restore_rolls_back_braid_shells_and_plural_index_added_after_checkpoint. D1 — witnessed-promotion law enforced by the type system: WitnessDigest moved to revelation; promote_posture takes WitnessDigest (a shrug cannot construct, so it cannot reach the function). One shrug-rejection impl, shared by posture promotion and the shell family. D2 — one event-kind digest scheme: BoundaryTransitionRecord reuses coordinator::hash_provenance_event_kind (now pub(crate)); braid_shell's parallel encoder deleted. D3 — member digests computed once per assemble/validate (sort_by_cached_key + a single digest vec feeding coordinate/witness/shell digests). L1 accurate # Errors docs; L2 obstruction lineage uses InvalidLineageParent { parent }; L3 take_braid_shells is #[doc(hidden)]; L4 collapse no-policy ref-drop documented; L5 witness_digest self-witness scaffolding documented; L6 trait shell() delegates to braid_shell(); L7 export grouping clarified. 518/518 warp-core tests; workspace clean under -D warnings; changed files clippy-clean incl --tests. --- crates/warp-core/src/braid_shell.rs | 210 +++++++----------- crates/warp-core/src/coordinator.rs | 7 +- crates/warp-core/src/lib.rs | 6 +- crates/warp-core/src/provenance_store.rs | 94 +++++++- crates/warp-core/src/revelation.rs | 63 +++++- .../design.md | 28 +++ 6 files changed, 266 insertions(+), 142 deletions(-) diff --git a/crates/warp-core/src/braid_shell.rs b/crates/warp-core/src/braid_shell.rs index 69bce8cc..73e0a036 100644 --- a/crates/warp-core/src/braid_shell.rs +++ b/crates/warp-core/src/braid_shell.rs @@ -22,7 +22,9 @@ use blake3::Hasher; use crate::admission::AdmissionOutcomeKind; use crate::ident::Hash; use crate::provenance_store::ProvenanceRef; -use crate::revelation::{shell_posture_obstruction, PostureObstruction, RevelationPosture}; +use crate::revelation::{ + shell_posture_obstruction, PostureObstruction, RevelationPosture, WitnessDigest, +}; use crate::strand::StrandId; use crate::worldline::WorldlineId; @@ -252,12 +254,6 @@ pub enum BraidShellError { /// Digest that resolved to nothing. digest: Hash, }, - /// A collapse lineage parent is missing or not plural. - #[error("collapse lineage parent {collapsed_from:?} is missing or not plural")] - InvalidCollapseLineage { - /// Parent shell digest named by `collapsed_from`. - collapsed_from: Hash, - }, /// A shell with this digest is already retained with different content. #[error("a divergent braid shell already claims digest {digest:?}")] DuplicateDigestDivergentContent { @@ -281,6 +277,12 @@ pub enum BraidShellError { /// A witness digest must never be a 32-byte shrug. #[error("empty or null witness digest refused")] EmptyWitness, + /// A lineage parent shell is missing or not plural. + #[error("lineage parent {parent:?} is missing or not plural")] + InvalidLineageParent { + /// Parent shell digest named by a collapse or obstruction lineage. + parent: Hash, + }, /// The stored braid coordinate does not match the recomputed body. #[error("braid coordinate mismatch")] CoordinateMismatch, @@ -316,44 +318,19 @@ pub enum BraidShellError { pub struct BraidCoordinate(pub Hash); impl BraidCoordinate { - fn derive(basis: &ProvenanceRef, members: &[BraidShellMember], policy_id: Hash) -> Self { + fn derive(basis: &ProvenanceRef, member_digests: &[Hash], policy_id: Hash) -> Self { let mut hasher = Hasher::new(); hasher.update(COORDINATE_DOMAIN); hash_provenance_ref(&mut hasher, basis); - hasher.update(&(members.len() as u64).to_le_bytes()); - for member in members { - hasher.update(&member.member_digest()); + hasher.update(&(member_digests.len() as u64).to_le_bytes()); + for member_digest in member_digests { + hasher.update(member_digest); } hasher.update(&policy_id); Self(hasher.finalize().into()) } } -/// Witness digest with a quality bar: zero and empty-input digests refused. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct WitnessDigest(Hash); - -impl WitnessDigest { - /// Wraps a witness digest, refusing shrug values. - /// - /// # Errors - /// - /// Returns [`BraidShellError::EmptyWitness`] for the all-zero digest and - /// the digest of empty input. - pub fn new(hash: Hash) -> Result { - if hash == [0; 32] || hash == crate::blake3_empty() { - return Err(BraidShellError::EmptyWitness); - } - Ok(Self(hash)) - } - - /// Returns the underlying digest. - #[must_use] - pub fn as_hash(&self) -> &Hash { - &self.0 - } -} - /// Retained braid-scale settlement boundary (θ_braid). #[derive(Clone, Debug, PartialEq, Eq)] pub struct BraidShell { @@ -372,6 +349,12 @@ pub struct BraidShell { /// Outcome arm over the shared lawful algebra. pub outcome: BraidShellOutcome, /// Witness digest binding the settlement act. + /// + /// E1 scaffolding: this is a domain-separated self-witness — a hash of + /// the same shell body — folded into `digest`. It guarantees integrity, + /// not independent attestation (anyone who can compute the shell can + /// compute it). A real external witness replaces it when settlement + /// gains witness-bearing authority. pub witness_digest: Hash, /// Revelation posture of the shell itself. pub posture: RevelationPosture, @@ -385,9 +368,17 @@ impl BraidShell { /// /// # Errors /// - /// Returns [`BraidShellError`] when the member set is empty, the shell - /// posture exceeds its least-revealed member, or the outcome arm - /// disagrees with member verdicts. + /// Returns [`BraidShellError`] for an empty member set + /// ([`BraidShellError::EmptyMembers`]), a duplicate member strand or + /// plural alternative ([`BraidShellError::DuplicateMemberStrand`], + /// [`BraidShellError::DuplicateAlternativeId`]), an empty witness on a + /// collapse/obstruction outcome ([`BraidShellError::EmptyWitness`]), + /// incoherent collapse fields + /// ([`BraidShellError::IncoherentCollapseFields`]), a posture exceeding + /// the least-revealed member + /// ([`BraidShellError::PostureExceedsMembers`]), or an outcome arm that + /// disagrees with member verdicts + /// ([`BraidShellError::OutcomeMemberMismatch`]). pub fn assemble( worldline_id: WorldlineId, basis: ProvenanceRef, @@ -399,7 +390,7 @@ impl BraidShell { if members.is_empty() { return Err(BraidShellError::EmptyMembers); } - members.sort_by_key(BraidShellMember::member_digest); + members.sort_by_cached_key(BraidShellMember::member_digest); check_unique_member_strands(&members)?; if let BraidShellOutcome::Plural { alternative_ids } = &mut outcome { // Retained alternatives are a set; canonical order, not transcript @@ -423,12 +414,18 @@ impl BraidShell { } check_outcome_member_coherence(&outcome, &members)?; - let coordinate = BraidCoordinate::derive(&basis, &members, policy_id); + // Compute each member digest once; coordinate, witness, and shell + // digests all consume it. + let member_digests: Vec = members + .iter() + .map(BraidShellMember::member_digest) + .collect(); + let coordinate = BraidCoordinate::derive(&basis, &member_digests, policy_id); let witness_digest = compute_witness_digest( BRAID_SHELL_VERSION, worldline_id, &basis, - &members, + &member_digests, policy_id, &outcome, posture, @@ -437,7 +434,7 @@ impl BraidShell { BRAID_SHELL_VERSION, worldline_id, &basis, - &members, + &member_digests, policy_id, &outcome, witness_digest, @@ -461,9 +458,12 @@ impl BraidShell { /// /// # Errors /// - /// Returns [`BraidShellError`] when member order is non-canonical, the - /// posture floor is violated, outcome and members disagree, or the - /// stored witness/shell digests do not match the recomputed body. + /// Returns [`BraidShellError`] for an unsupported version, empty or + /// non-canonically-ordered members, a duplicate member strand, + /// non-canonical or duplicate plural alternatives, an empty + /// collapse/obstruction witness, a posture floor violation, an + /// outcome/member disagreement, a coordinate mismatch, or a stored + /// witness/shell digest that does not match the recomputed body. pub fn validate(&self) -> Result<(), BraidShellError> { if self.version != BRAID_SHELL_VERSION { return Err(BraidShellError::UnsupportedVersion { @@ -474,15 +474,15 @@ impl BraidShell { if self.members.is_empty() { return Err(BraidShellError::EmptyMembers); } - let mut previous: Option = None; - for member in &self.members { - let current = member.member_digest(); - if let Some(prior) = previous { - if prior > current { - return Err(BraidShellError::NonCanonicalMemberOrder); - } - } - previous = Some(current); + // Compute each member digest once; the order check, coordinate, + // witness, and shell digests all consume it. + let member_digests: Vec = self + .members + .iter() + .map(BraidShellMember::member_digest) + .collect(); + if member_digests.windows(2).any(|pair| pair[0] > pair[1]) { + return Err(BraidShellError::NonCanonicalMemberOrder); } check_unique_member_strands(&self.members)?; if let BraidShellOutcome::Plural { alternative_ids } = &self.outcome { @@ -507,7 +507,8 @@ impl BraidShell { return Err(BraidShellError::PostureExceedsMembers(obstruction)); } check_outcome_member_coherence(&self.outcome, &self.members)?; - if BraidCoordinate::derive(&self.basis, &self.members, self.policy_id) != self.coordinate { + if BraidCoordinate::derive(&self.basis, &member_digests, self.policy_id) != self.coordinate + { return Err(BraidShellError::CoordinateMismatch); } @@ -515,7 +516,7 @@ impl BraidShell { self.version, self.worldline_id, &self.basis, - &self.members, + &member_digests, self.policy_id, &self.outcome, self.posture, @@ -530,7 +531,7 @@ impl BraidShell { self.version, self.worldline_id, &self.basis, - &self.members, + &member_digests, self.policy_id, &self.outcome, self.witness_digest, @@ -569,7 +570,7 @@ fn check_outcome_law(outcome: &BraidShellOutcome) -> Result<(), BraidShellError> .. } | BraidShellOutcome::Obstruction { witness, .. } => { - WitnessDigest::new(*witness)?; + WitnessDigest::new(*witness).map_err(|_| BraidShellError::EmptyWitness)?; } _ => {} } @@ -641,7 +642,7 @@ fn hash_shell_body( version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, - members: &[BraidShellMember], + member_digests: &[Hash], policy_id: Hash, outcome: &BraidShellOutcome, posture: RevelationPosture, @@ -649,9 +650,9 @@ fn hash_shell_body( hasher.update(&version.to_le_bytes()); hasher.update(worldline_id.as_bytes()); hash_provenance_ref(hasher, basis); - hasher.update(&(members.len() as u64).to_le_bytes()); - for member in members { - hasher.update(&member.member_digest()); + hasher.update(&(member_digests.len() as u64).to_le_bytes()); + for member_digest in member_digests { + hasher.update(member_digest); } hasher.update(&policy_id); outcome.hash_into(hasher); @@ -663,7 +664,7 @@ fn compute_witness_digest( version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, - members: &[BraidShellMember], + member_digests: &[Hash], policy_id: Hash, outcome: &BraidShellOutcome, posture: RevelationPosture, @@ -675,7 +676,7 @@ fn compute_witness_digest( version, worldline_id, basis, - members, + member_digests, policy_id, outcome, posture, @@ -688,7 +689,7 @@ fn compute_shell_digest( version: u32, worldline_id: WorldlineId, basis: &ProvenanceRef, - members: &[BraidShellMember], + member_digests: &[Hash], policy_id: Hash, outcome: &BraidShellOutcome, witness_digest: Hash, @@ -701,7 +702,7 @@ fn compute_shell_digest( version, worldline_id, basis, - members, + member_digests, policy_id, outcome, posture, @@ -782,13 +783,13 @@ pub fn replay_braid_shell( let parent = records .shell(&parent_digest) - .ok_or(BraidShellError::InvalidCollapseLineage { - collapsed_from: parent_digest, + .ok_or(BraidShellError::InvalidLineageParent { + parent: parent_digest, })?; parent.validate()?; if !matches!(parent.outcome, BraidShellOutcome::Plural { .. }) { - return Err(BraidShellError::InvalidCollapseLineage { - collapsed_from: parent_digest, + return Err(BraidShellError::InvalidLineageParent { + parent: parent_digest, }); } } @@ -837,6 +838,9 @@ pub enum CollapseResult { /// way the original plural shell remains byte-identical truth forever. /// Append-only or bust. /// +/// When `policy` is `None`, `selected_result_refs` is ignored: an obstructed +/// collapse retains no derived result, only the refusal. +/// /// # Errors /// /// Returns [`BraidShellError`] when the plural shell is missing, fails @@ -854,8 +858,8 @@ pub fn collapse_braid_shell( })?; plural.validate()?; if !matches!(plural.outcome, BraidShellOutcome::Plural { .. }) { - return Err(BraidShellError::InvalidCollapseLineage { - collapsed_from: plural_shell_digest, + return Err(BraidShellError::InvalidLineageParent { + parent: plural_shell_digest, }); } @@ -928,43 +932,6 @@ impl RetainedBoundaryRecord for BraidShell { const TICK_SHELL_DOMAIN: &[u8] = b"echo.shell.tick.v1\0"; -fn hash_event_kind(hasher: &mut Hasher, kind: &crate::provenance_store::ProvenanceEventKind) { - use crate::provenance_store::ProvenanceEventKind; - match kind { - ProvenanceEventKind::LocalCommit => { - hasher.update(&[1]); - } - ProvenanceEventKind::CrossWorldlineMessage { - source_worldline, - source_worldline_tick, - message_id, - } => { - hasher.update(&[2]); - hasher.update(source_worldline.as_bytes()); - hasher.update(&source_worldline_tick.as_u64().to_le_bytes()); - hasher.update(message_id); - } - ProvenanceEventKind::MergeImport { - source_worldline, - source_worldline_tick, - op_id, - } => { - hasher.update(&[3]); - hasher.update(source_worldline.as_bytes()); - hasher.update(&source_worldline_tick.as_u64().to_le_bytes()); - hasher.update(op_id); - } - ProvenanceEventKind::ConflictArtifact { artifact_id } => { - hasher.update(&[4]); - hasher.update(artifact_id); - } - ProvenanceEventKind::PluralArtifact { plural_id } => { - hasher.update(&[5]); - hasher.update(plural_id); - } - } -} - impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecord { fn boundary_kind(&self) -> RetainedBoundaryKind { RetainedBoundaryKind::Tick @@ -1003,7 +970,7 @@ impl RetainedBoundaryRecord for crate::provenance_store::BoundaryTransitionRecor hasher.update(&[0]); } } - hash_event_kind(&mut hasher, &entry.event_kind); + crate::coordinator::hash_provenance_event_kind(&mut hasher, &entry.event_kind); hasher.update(&(entry.parents.len() as u64).to_le_bytes()); for parent in &entry.parents { hash_provenance_ref(&mut hasher, parent); @@ -1099,7 +1066,6 @@ impl BraidShell { mod tests { use super::*; use crate::clock::WorldlineTick; - use crate::ident::make_warp_id; use crate::strand::make_strand_id; use std::collections::BTreeMap; @@ -1422,8 +1388,8 @@ mod tests { let missing_parent = Records::with([derived]); assert_eq!( replay_braid_shell(&derived_digest, &missing_parent), - Err(BraidShellError::InvalidCollapseLineage { - collapsed_from: plural_digest, + Err(BraidShellError::InvalidLineageParent { + parent: plural_digest, }) ); } @@ -1478,18 +1444,10 @@ mod tests { ); } - #[test] - fn witness_digest_refuses_shrug_values() { - assert_eq!( - WitnessDigest::new([0; 32]), - Err(BraidShellError::EmptyWitness) - ); - assert_eq!( - WitnessDigest::new(crate::blake3_empty()), - Err(BraidShellError::EmptyWitness) - ); - assert!(WitnessDigest::new([0x99; 32]).is_ok()); - } + // WitnessDigest shrug-rejection is owned and tested in `revelation`; + // `check_outcome_law` maps it to `BraidShellError::EmptyWitness`, covered + // by `derived_shell_rejects_empty_collapse_witness` / + // `obstruction_shell_rejects_empty_witness` below. #[test] fn collapse_with_named_policy_derives_without_mutating_the_plural_parent() { diff --git a/crates/warp-core/src/coordinator.rs b/crates/warp-core/src/coordinator.rs index b45d4906..1adb2e13 100644 --- a/crates/warp-core/src/coordinator.rs +++ b/crates/warp-core/src/coordinator.rs @@ -2406,7 +2406,12 @@ fn hash_provenance_ref(hasher: &mut blake3::Hasher, reference: &ProvenanceRef) { hasher.update(&reference.commit_hash); } -fn hash_provenance_event_kind(hasher: &mut blake3::Hasher, event_kind: &ProvenanceEventKind) { +/// Canonical event-kind encoding shared by commit hashing and the retained +/// boundary shell family, so the enum has exactly one digest scheme. +pub(crate) fn hash_provenance_event_kind( + hasher: &mut blake3::Hasher, + event_kind: &ProvenanceEventKind, +) { match event_kind { ProvenanceEventKind::LocalCommit => { hasher.update(b"local-commit"); diff --git a/crates/warp-core/src/lib.rs b/crates/warp-core/src/lib.rs index 870642b6..054cc488 100644 --- a/crates/warp-core/src/lib.rs +++ b/crates/warp-core/src/lib.rs @@ -255,12 +255,12 @@ pub use retained_evidence::{ }; // --- Session types --- pub use playback::{SessionId, ViewSession}; -// --- Truth delivery --- +// --- Retained boundary shell family (θ_tick, θ_braid) --- pub use braid_shell::{ collapse_braid_shell, replay_braid_shell, BraidCoordinate, BraidShell, BraidShellError, BraidShellMember, BraidShellOutcome, BraidShellQuery, BraidShellRecords, BraidShellReplay, CollapsePolicy, CollapseResult, MemberVerdict, RetainedBoundaryKind, RetainedBoundaryRecord, - WitnessDigest, BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, + BRAID_SHELL_VERSION, COLLAPSE_WITHOUT_POLICY_REASON, }; pub use neighborhood::{ NeighborhoodCore, NeighborhoodError, NeighborhoodParticipant, NeighborhoodParticipantRole, @@ -316,7 +316,7 @@ pub use receipt::{TickReceipt, TickReceiptDisposition, TickReceiptEntry, TickRec pub use record::{EdgeRecord, NodeRecord}; pub use revelation::{ least_revealed, promote_posture, shell_posture_obstruction, PostureObstruction, - PosturePromotion, RevelationPosture, + PosturePromotion, RevelationPosture, WitnessDigest, }; #[cfg(feature = "native_rule_bootstrap")] pub use rule::{ConflictPolicy, ExecuteFn, MatchFn, PatternGraph, RewriteRule}; diff --git a/crates/warp-core/src/provenance_store.rs b/crates/warp-core/src/provenance_store.rs index 4b8d6c36..28e5c0d2 100644 --- a/crates/warp-core/src/provenance_store.rs +++ b/crates/warp-core/src/provenance_store.rs @@ -21,7 +21,7 @@ //! simply the `WarpId`. The engine's `initial_state` for a warp serves as the U0 //! starting point for replay. -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use thiserror::Error; @@ -1210,9 +1210,17 @@ struct ProvenanceWorldlineCheckpoint { } /// Lightweight rollback marker for touched provenance worldlines. -#[derive(Clone, Debug, PartialEq, Eq)] +/// +/// Also snapshots the retained braid-shell key sets so [`ProvenanceService`] +/// can roll back shell and residue-index inserts. Shells are append-only and +/// content-addressed, so "retain only the keys present at checkpoint time" is +/// exact rollback — this is what makes the no-leak property a mechanism, not +/// a sequencing convention. +#[derive(Clone, Debug, PartialEq, Eq, Default)] pub struct ProvenanceCheckpoint { worldlines: BTreeMap, + braid_shell_keys: BTreeSet, + plural_index_keys: BTreeSet, } /// In-memory provenance store backed by `Vec`s. @@ -1608,7 +1616,12 @@ impl LocalProvenanceStore { }, ); } - Ok(ProvenanceCheckpoint { worldlines }) + // Shell key sets are filled in by the `ProvenanceService` wrapper, + // which owns the shell maps; the store layer leaves them empty. + Ok(ProvenanceCheckpoint { + worldlines, + ..ProvenanceCheckpoint::default() + }) } fn restore(&mut self, checkpoint: &ProvenanceCheckpoint) { @@ -1625,7 +1638,7 @@ impl LocalProvenanceStore { impl crate::braid_shell::BraidShellRecords for ProvenanceService { fn shell(&self, digest: &Hash) -> Option<&crate::braid_shell::BraidShell> { - self.braid_shells.get(digest) + self.braid_shell(digest) } } @@ -1828,7 +1841,8 @@ impl ProvenanceService { /// /// Exists for hostile replay proofs: a caller can extract the shells and /// drop the runtime, registry, and provenance entries entirely, then - /// replay outcomes from the shells alone. + /// replay outcomes from the shells alone. Not part of the stable surface. + #[doc(hidden)] #[must_use] pub fn take_braid_shells(&mut self) -> BTreeMap { // The residue index describes the shells; taking one without the @@ -1940,12 +1954,23 @@ impl ProvenanceService { where I: IntoIterator, { - self.store.checkpoint_for(worldline_ids) + let mut checkpoint = self.store.checkpoint_for(worldline_ids)?; + checkpoint.braid_shell_keys = self.braid_shells.keys().copied().collect(); + checkpoint.plural_index_keys = self.plural_shell_index.keys().copied().collect(); + Ok(checkpoint) } /// Restores touched worldlines to a previously captured rollback checkpoint. + /// + /// Also prunes any braid shells and residue-index bindings retained after + /// the checkpoint, so a rolled-back settlement can never leave a shell + /// describing history that no longer exists. pub fn restore(&mut self, checkpoint: &ProvenanceCheckpoint) { self.store.restore(checkpoint); + self.braid_shells + .retain(|digest, _| checkpoint.braid_shell_keys.contains(digest)); + self.plural_shell_index + .retain(|plural_id, _| checkpoint.plural_index_keys.contains(plural_id)); } /// Reconstructs full [`WorldlineState`] for a worldline up to `target_tick`. @@ -3604,6 +3629,63 @@ mod tests { )); } + fn minimal_plural_shell(alt_id: u8) -> crate::braid_shell::BraidShell { + use crate::braid_shell::{BraidShellMember, BraidShellOutcome, MemberVerdict}; + use crate::revelation::RevelationPosture; + let member = BraidShellMember { + strand_ref: crate::strand::make_strand_id("m"), + support_pin_digest: [1; 32], + basis_digest: [2; 32], + frontier_digest: [3; 32], + footprint_digest: [4; 32], + claim_digest: [5; 32], + verdict: MemberVerdict::Plural, + verdict_digest: [6; 32], + posture: RevelationPosture::AuthorOnly, + }; + crate::braid_shell::BraidShell::assemble( + test_worldline_id(), + ProvenanceRef { + worldline_id: test_worldline_id(), + worldline_tick: wt(0), + commit_hash: [7; 32], + }, + vec![member], + [8; 32], + BraidShellOutcome::Plural { + alternative_ids: vec![[alt_id; 32]], + }, + RevelationPosture::AuthorOnly, + ) + .unwrap() + } + + #[test] + fn restore_rolls_back_braid_shells_and_plural_index_added_after_checkpoint() { + let mut service = ProvenanceService::new(); + let shell_a = minimal_plural_shell(0xA1); + let digest_a = service.append_braid_shell(shell_a).unwrap(); + let plural_a = [0xA1; 32]; + + // Checkpoint AFTER shell A is retained. + let checkpoint = service.checkpoint_for([]).unwrap(); + + // Retain a second shell, then roll back to the checkpoint. + let shell_b = minimal_plural_shell(0xB2); + let digest_b = service.append_braid_shell(shell_b).unwrap(); + assert_ne!(digest_a, digest_b); + assert_eq!(service.braid_shells().count(), 2); + + service.restore(&checkpoint); + + // Shell A and its residue binding survive; shell B and its binding are gone. + assert_eq!(service.braid_shells().count(), 1); + assert!(service.braid_shell(&digest_a).is_some()); + assert!(service.braid_shell(&digest_b).is_none()); + assert!(service.braid_shell_for_plural(&plural_a).is_some()); + assert!(service.braid_shell_for_plural(&[0xB2; 32]).is_none()); + } + #[test] fn btr_validation_rejects_mixed_worldlines() { let entry = ProvenanceEntry::local_commit( diff --git a/crates/warp-core/src/revelation.rs b/crates/warp-core/src/revelation.rs index c6c6925e..755eba0b 100644 --- a/crates/warp-core/src/revelation.rs +++ b/crates/warp-core/src/revelation.rs @@ -89,6 +89,37 @@ pub enum PostureObstruction { /// Least-revealed posture among the shell's members. least_revealed_member: RevelationPosture, }, + /// A witness digest must never be a 32-byte shrug. + EmptyWitness, +} + +/// Witness digest with a quality bar: zero and empty-input digests refused. +/// +/// The witnessed-act law is enforced by the type system: any API that takes +/// a `WitnessDigest` cannot be handed a shrug, because the shrug never +/// constructs. Shared by posture promotion and the braid shell family. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct WitnessDigest(Hash); + +impl WitnessDigest { + /// Wraps a witness digest, refusing shrug values. + /// + /// # Errors + /// + /// Returns [`PostureObstruction::EmptyWitness`] for the all-zero digest + /// and the digest of empty input. + pub fn new(hash: Hash) -> Result { + if hash == [0; 32] || hash == crate::blake3_empty() { + return Err(PostureObstruction::EmptyWitness); + } + Ok(Self(hash)) + } + + /// Returns the underlying digest. + #[must_use] + pub fn as_hash(&self) -> &Hash { + &self.0 + } } /// Returns the least-revealed posture among `members`. @@ -129,7 +160,9 @@ where /// Performs one explicit, witnessed posture promotion. /// /// Promotion only widens posture. Narrowing and same-posture requests are -/// obstructions: a posture change must always be a real, witnessed act. +/// obstructions: a posture change must always be a real, witnessed act. The +/// witness arrives as a [`WitnessDigest`], so a shrug witness cannot reach +/// this function — the type system holds the door. /// /// # Errors /// @@ -139,7 +172,7 @@ where pub fn promote_posture( from: RevelationPosture, to: RevelationPosture, - witness: Hash, + witness: WitnessDigest, ) -> Result { if to < from { return Err(PostureObstruction::NarrowingRefused { @@ -150,15 +183,33 @@ pub fn promote_posture( if to == from { return Err(PostureObstruction::AlreadyAtPosture { posture: from }); } - Ok(PosturePromotion { from, to, witness }) + Ok(PosturePromotion { + from, + to, + witness: *witness.as_hash(), + }) } #[cfg(test)] +#[allow(clippy::unwrap_used)] mod tests { use super::*; - fn witness() -> Hash { - [0xA7; 32] + fn witness() -> WitnessDigest { + WitnessDigest::new([0xA7; 32]).unwrap() + } + + #[test] + fn witness_digest_refuses_shrug_values() { + assert_eq!( + WitnessDigest::new([0; 32]), + Err(PostureObstruction::EmptyWitness) + ); + assert_eq!( + WitnessDigest::new(crate::blake3_empty()), + Err(PostureObstruction::EmptyWitness) + ); + assert!(WitnessDigest::new([0x99; 32]).is_ok()); } #[test] @@ -242,7 +293,7 @@ mod tests { Ok(PosturePromotion { from: RevelationPosture::AuthorOnly, to: RevelationPosture::Shared, - witness: witness(), + witness: *witness().as_hash(), }) ); } diff --git a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md index 4c24c72b..9d3c2aef 100644 --- a/docs/design/0026-braid-shell-family-and-plural-settlement/design.md +++ b/docs/design/0026-braid-shell-family-and-plural-settlement/design.md @@ -244,6 +244,34 @@ Named debts from the checkpoint review: coherent); missing policy → retained `Obstruction` shell. `WitnessDigest` refuses zero/empty digests. +## Record-law remediations (Code Lawyer self-review, 2026-06-13) + +A pedantic self-review surfaced eleven findings (0 critical, 1 major, 3 +medium, 7 low); all resolved before merge: + +- **M1 — no-leak is now a mechanism, not a convention.** + `ProvenanceCheckpoint` snapshots the braid-shell and plural-index key + sets; `ProvenanceService::restore` prunes any shell or residue binding + retained after the checkpoint. A rolled-back settlement can no longer + leak a shell describing vanished history, even if a future fallible step + is added after shell append. +- **D1 — witnessed-promotion law enforced by the type system.** + `WitnessDigest` moved to `revelation` (the witness-primitives module); + `promote_posture` now takes `WitnessDigest`, so a shrug witness cannot + reach it. One shrug-rejection implementation, shared with the braid + shell family. +- **D2 — one event-kind digest scheme.** `BoundaryTransitionRecord` + reuses the canonical `coordinator::hash_provenance_event_kind` instead + of a parallel encoder. +- **D3 — member digests computed once** per `assemble`/`validate` + (`sort_by_cached_key` + a single digest vector feeding coordinate, + witness, and shell digests). +- **L1–L7:** accurate `# Errors` docs; obstruction lineage uses + `InvalidLineageParent { parent }`; `take_braid_shells` is + `#[doc(hidden)]`; collapse's no-policy ref-drop documented; the + `witness_digest` self-witness scaffolding documented; trait method + delegation deduped; export grouping clarified. + ## Acceptance criteria (enhanced per review) 1. `SettlementDecision` gains a `Plural` arm carrying surviving