diff --git a/CHANGELOG.md b/CHANGELOG.md index 92803c09e..b553cab74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- v18 Continuum evidence posture now uses participant-runtime and + Continuum-witnessed vocabulary instead of substrate/native framing, and + git-warp participant evidence cannot carry a Continuum witness reference. +- Writer patch sessions now preserve `WRITER_COMMIT_NOT_VISIBLE` when a + post-CAS visibility failure reaches the public writer API instead of + collapsing it to `PERSIST_WRITE_FAILED`. +- BEARING and v18 cycle notes now reflect the current PR branch, latest closed + cycle, and participant evidence terminology. - Continuum artifact ingestion now enforces artifact-kind/authority pairing at the domain policy layer, keeps review fixtures repo-neutral, and splits the JSON adapter into focused parser, validation, fixture, and Wesley inventory diff --git a/docs/BEARING.md b/docs/BEARING.md index cd51494d6..c6aca9eff 100644 --- a/docs/BEARING.md +++ b/docs/BEARING.md @@ -40,8 +40,8 @@ The long-term compatibility target is the WARP Optic shape described in `~/git/blog/aion-paper-07/dist/aion-paper-07.txt`, plus the Continuum contract families authored in `~/git/continuum/schemas/` and compiled by Wesley. Echo and `git-warp` are sibling runtime implementations. `git-warp` -has its own Continuum role, and it must not emit Continuum-shaped values as -native Continuum witnesses until that witnesshood is actually proven. +has its own Continuum role, and it must not attach a separate Continuum witness +reference to projected values until that witnesshood is actually proven. Backlog fold-in: the repo-visible v18 lane is `WL-4A-v18-graph-substrate-convergence` in @@ -56,12 +56,12 @@ Continuum role. Current branch state at this boundary: -- Branch: `v18-continuum-opening` +- Branch: `v18-evidence-receipt-projection` - Release tag: `v17.0.0` -- Latest remote head inspected: `origin/main` at `5afdd3eb` +- Latest remote head inspected: `origin/main` at `6d4a0d6` - Latest package version: `17.0.0` - Latest closed cycle: - `0145-push-pr-review-merge` + `0154-v18-warp-ttd-receipt-smoke` The release ladder is now: @@ -140,10 +140,10 @@ read-model groundwork, sync hardening, release gates, and package publishing. - v18 can easily turn into adapter folklore if `git-warp` hand-authors local mirrors of Continuum-owned families instead of consuming Wesley-generated artifacts. -- v18 can also lie in the other direction: Continuum-shaped values are not - Continuum-native witnesses unless the runtime has actually proven native - witnesshood. Initial git-warp compatibility evidence should be treated as - translated git-warp evidence until stronger proof exists. +- v18 can also lie in the other direction: Continuum-shaped values do not carry + separate Continuum witness references unless that witnesshood is actually + proven. Initial git-warp compatibility evidence should be treated as + participant-runtime evidence until stronger proof exists. - The v18 backlog already names a graph-model convergence lane. The plan must fold that lane into Continuum compatibility instead of replacing it with a parallel cross-repo adapter plan. @@ -178,16 +178,38 @@ before the final commit for that slice, and mark completed items with `- [x]`. and one-file-per-concept caps, self-attested authority fields from artifact JSON are rejected, policy-test authority fixtures are named constants, and empty or internally inconsistent Wesley generated inventory is rejected. -- [ ] 6. Make evidence posture explicit: translated git-warp evidence first, - native Continuum evidence only after native witnesshood is proven. -- [ ] 7. Prove the patch commit visibility contract: success means canonical - writer-tip advancement and visible graph truth, not just object creation. -- [ ] 8. Add the same-writer concurrent patch race witness with final-frontier - and visible-state assertions. -- [ ] 9. Project git-warp receipt facts into the generated Continuum - receipt-family shape with conformance tests. -- [ ] 10. Add the first `warp-ttd` smoke over generated-family git-warp receipt - facts instead of handwritten adapter-local receipt folklore. +- [x] 6. Make evidence posture explicit: git-warp participant evidence first, + Continuum-witnessed evidence only after a witness reference is present. The + current seam adds runtime-backed `ContinuumEvidencePosture` and + `ContinuumEvidenceStatus`; git-warp evidence is explicit + `participant-runtime` evidence, Continuum-witnessed evidence cannot be + constructed without `continuumWitnessRef`, and participant-runtime evidence + rejects Continuum witness references. +- [x] 7. Prove the patch commit visibility contract: success means canonical + writer-tip advancement and visible graph truth, not just object creation. The + patch commit path now advances writer refs with `compareAndSwapRef()`, + rereads the writer ref before returning success, throws + `WRITER_COMMIT_NOT_VISIBLE` if the returned commit is not the visible writer + tip, and has focused tests proving both ref visibility and materialized graph + visibility. +- [x] 8. Add the same-writer concurrent patch race witness with final-frontier + and visible-state assertions. The regression witness creates two stale + builders for the same writer, commits them concurrently, proves exactly one + wins, asserts the writer ref names the winning SHA, and verifies only the + winning node is visible after materialization. +- [x] 9. Project git-warp receipt facts into the generated Continuum + receipt-family shape with conformance tests. `ContinuumReceiptProjector` now + maps `TickReceipt` into runtime-backed Continuum receipt-family `Receipt` + facts, `ContinuumReceiptFamilyProjection` carries the receipt-family artifact + descriptor and explicit evidence status, and non-receipt-family artifacts are + rejected. +- [x] 10. Add the first `warp-ttd` smoke over generated-family git-warp receipt + facts instead of handwritten adapter-local receipt folklore. The smoke starts + from a real committed git-warp patch, materializes real `TickReceipt` output, + loads the generated receipt-family fixture descriptor through the adapter + seam, projects the receipt into `ContinuumReceiptFamilyProjection`, queries + by head and frame for the `warp-ttd` target, and keeps evidence posture + explicitly participant-runtime rather than Continuum-witnessed. - [ ] 11. Re-plan with evidence in hand before expanding into reading-envelope, suffix/runtime-boundary, neighborhood-core, and settlement-family slices. diff --git a/docs/design/0146-v18-continuum-compatibility-charter/v18-continuum-compatibility-charter.md b/docs/design/0146-v18-continuum-compatibility-charter/v18-continuum-compatibility-charter.md index bc935fdbb..bf5e954d5 100644 --- a/docs/design/0146-v18-continuum-compatibility-charter/v18-continuum-compatibility-charter.md +++ b/docs/design/0146-v18-continuum-compatibility-charter/v18-continuum-compatibility-charter.md @@ -25,7 +25,8 @@ participant without collapsing it into Echo, Wesley, or `warp-ttd`. - it consumes Wesley-generated artifacts for Continuum-owned contract families; - it maps append-only Git history into honest WARP Optic evidence; - it exposes generated-family facts to `warp-ttd`; -- it separates translated git-warp evidence from native Continuum witnesshood. +- it separates git-warp participant evidence from separate Continuum + witnesshood. ## Source Artifacts @@ -100,13 +101,13 @@ defines `git-warp`'s participant obligations. ## Evidence Posture -The default posture for existing git-warp facts mapped into Continuum-family -shapes is translated evidence. A value may be Continuum-shaped without being -Continuum-native. +The default posture for existing git-warp facts projected into Continuum-family +shapes is participant-runtime evidence. A value may be Continuum-shaped without +carrying a separate Continuum witness reference. -`git-warp` may claim native Continuum witnesshood only after a runtime witness +`git-warp` may attach a Continuum witness reference only after a runtime witness proves the value was produced through the corresponding Continuum family -contract and not merely mapped from local git-warp facts. +contract and not merely projected from local git-warp facts. ## Non-Goals @@ -116,7 +117,7 @@ contract and not merely mapped from local git-warp facts. contracts. - Do not build a generic WARP Optic runtime before repeated concrete compatibility cuts justify it. -- Do not claim native Continuum witnesshood for translated git-warp evidence. +- Do not claim separate Continuum witnesshood for git-warp participant evidence. ## Acceptance @@ -144,7 +145,7 @@ The v18 opening campaign is on track when: - Message parsing: green; no behavior branches are introduced. - Ambient time or entropy: green; no runtime code introduced. - Fake shape trust or cast-cosplay: green; the charter explicitly rejects fake - native witnesshood. + witnesshood. ## Closeout diff --git a/docs/design/0147-v18-continuum-contract-matrix/v18-continuum-contract-matrix.md b/docs/design/0147-v18-continuum-contract-matrix/v18-continuum-contract-matrix.md index 5f68a4f71..17cd73a16 100644 --- a/docs/design/0147-v18-continuum-contract-matrix/v18-continuum-contract-matrix.md +++ b/docs/design/0147-v18-continuum-contract-matrix/v18-continuum-contract-matrix.md @@ -26,8 +26,8 @@ contract families and the current proof gap for each one. - Echo and `git-warp` are sibling runtimes that may emit or consume conforming values. - `warp-ttd` is the structured debugger/read-model consumer. -- Existing `git-warp` facts are translated evidence until native Continuum - witnesshood is proven. +- Existing `git-warp` facts are participant-runtime evidence until separate + Continuum witnesshood is proven. ## Evidence Snapshot @@ -45,23 +45,23 @@ The matrix below is based on these inspected local sources: | Family | Authored home | Wesley status | `git-warp` source facts | Primary `warp-ttd` need | Missing witness | | --- | --- | --- | --- | --- | --- | -| `receipt-family` | `~/git/continuum/schemas/continuum-receipt-family.graphql` | `profiled`, `fixture-witnessed`; scope `receipt-family` checks cross-leg schema hash, TTD fixture shape, Echo fixture shape, boundary fixture, roundtrip vectors, and receipt/witness separation | `TickReceipt`, op outcomes, `DeliveryObservation`, audit receipt chains, materialize/provenance receipt collection | Receipt and delivery facts as generated-family nouns, not adapter-local summaries | Live `git-warp` receipt publication mapped through generated artifacts with translated evidence posture, then witnessed as native only after a Continuum runtime witness exists | +| `receipt-family` | `~/git/continuum/schemas/continuum-receipt-family.graphql` | `profiled`, `fixture-witnessed`; scope `receipt-family` checks cross-leg schema hash, TTD fixture shape, Echo fixture shape, boundary fixture, roundtrip vectors, and receipt/witness separation | `TickReceipt`, op outcomes, `DeliveryObservation`, audit receipt chains, materialize/provenance receipt collection | Receipt and delivery facts as generated-family nouns, not adapter-local summaries | Live `git-warp` receipt publication projected through generated artifacts with participant-runtime evidence posture, then linked to separate Continuum witnesses only when such witnesses exist | | `settlement-family` | `~/git/continuum/schemas/continuum-settlement-family.graphql` | `profiled`, `fixture-witnessed`; scope `settlement-family` checks cross-leg coherence and settlement boundary fixtures | Patch diffs, conflict traces, merge/conflict analysis, strand/braid conflict artifacts, writer frontier state | Import/settlement explanation for cross-runtime history and merge inspection | Live settlement values from `git-warp` suffix/import or merge flows, plus generated-artifact conformance | -| `neighborhood-core-family` | `~/git/continuum/schemas/continuum-neighborhood-core-family.graphql` | `authored`; not yet profiled in the current Continuum Wesley scope list | Graph name, writer refs, worldline/frontier facts, local site-like participation facts still unnamed as a stable family | Neighborhood focus, participant catalog, and site navigation across Echo and `git-warp` targets | Wesley profile and fixture witness first; then `git-warp` participant values with explicit translated/native evidence status | +| `neighborhood-core-family` | `~/git/continuum/schemas/continuum-neighborhood-core-family.graphql` | `authored`; not yet profiled in the current Continuum Wesley scope list | Graph name, writer refs, worldline/frontier facts, local site-like participation facts still unnamed as a stable family | Neighborhood focus, participant catalog, and site navigation across Echo and `git-warp` targets | Wesley profile and fixture witness first; then `git-warp` participant values with explicit participant-runtime or Continuum-witnessed evidence status | | `runtime-boundary-family` | `~/git/continuum/schemas/continuum-runtime-boundary-family.graphql` | `authored`; not yet profiled in the current Continuum Wesley scope list | Materialize/read requests, observer/read basis, patch suffixes, frontiers, provenance refs, receipt collections, import outcomes still split across local APIs | Admission-chain read model: observer plans, reading envelopes, evidence posture, suffix shells, causal suffix bundles, import outcomes | Wesley profile, generated fixtures, and a live witnessed suffix exchange/admission proof between sibling runtimes | ## Source-Fact Map | Continuum noun | Current `git-warp` anchor | Current posture | | --- | --- | --- | -| `Receipt` | `TickReceipt`, audit receipts, receipt shards | Translated evidence; shape is not yet generated-family native | +| `Receipt` | `TickReceipt`, audit receipts, receipt shards | Participant-runtime evidence; shape is not yet linked to separate Continuum witness refs | | `DeliveryObservation` | `DeliveryObservation` and effect sink observations | Local fact with strong name overlap; not yet Continuum family output | | `Witness` | checkpoint-tail witnesses, conflict witnesses, audit chain proofs | Local witness forms; no shared generated family surface yet | | `SettlementDelta` / `ConflictArtifact` | `PatchDiff`, conflict traces, merge/conflict services | Candidate source facts; missing shared generated settlement adapter | | `NeighborhoodCore` / `NeighborhoodParticipant` | graph name, writers, frontiers, worldline metadata | Candidate source facts; missing stable local site/participant object | | `ObserverPlan` / `ObservationRequest` | query/read basis, materialize options, traversal context | Candidate source facts; missing generated runtime-boundary profile | | `ReadingEnvelope` | materialize/query/read results plus provenance/receipt options | Candidate source facts; missing explicit evidence status wrapper | -| `TranslatedSubstrateEvidence` | append-only Git-backed causal history, patch SHAs, writer refs, receipts | Correct initial evidence posture for compatibility outputs | +| `ParticipantRuntimeEvidence` | append-only Git-backed causal history, patch SHAs, writer refs, receipts | Correct initial evidence posture for compatibility outputs | | `WitnessedSuffixShell` / `CausalSuffixBundle` | writer patch chains, frontier maps, transport/sync suffixes | Candidate source facts; missing compact generated shell and admission witness | | `ImportOutcome` | sync/import/materialization outcomes and conflict posture | Candidate source facts; missing runtime-boundary family emission | @@ -88,8 +88,8 @@ Recommended order: 1. Ingest or locally fixture the `receipt-family` generated artifact manifest. 2. Reject local `git-warp` files that claim to be authoritative mirrors of Continuum-owned families. -3. Map `TickReceipt` and `DeliveryObservation` into a translated - `receipt-family` projection without claiming native Continuum witnesshood. +3. Map `TickReceipt` and `DeliveryObservation` into a participant-runtime + `receipt-family` projection without claiming separate Continuum witnesshood. 4. Let `warp-ttd` consume that projection as generated-family-shaped input. ## SSJS Scorecard @@ -103,7 +103,7 @@ Recommended order: - Message parsing: green; no behavior branches introduced. - Ambient time or entropy: green; no runtime code introduced. - Fake shape trust or cast-cosplay: green; every current `git-warp` mapping is - marked as translated evidence until a stronger witness exists. + marked as participant-runtime evidence until a stronger witness exists. ## Closeout diff --git a/docs/design/0148-v18-warp-optic-realization-map/v18-warp-optic-realization-map.md b/docs/design/0148-v18-warp-optic-realization-map/v18-warp-optic-realization-map.md index 6c859f4b0..7c9981985 100644 --- a/docs/design/0148-v18-warp-optic-realization-map/v18-warp-optic-realization-map.md +++ b/docs/design/0148-v18-warp-optic-realization-map/v18-warp-optic-realization-map.md @@ -43,7 +43,7 @@ as a compatibility map over existing repo facts. | Scale | Weave `P` | Frontier `F*` | Result `R` | Witness `W` | Retained shell `theta` | Current posture | | --- | --- | --- | --- | --- | --- | --- | -| Tick / patch | One patch or ordered patch sequence | Writer and graph frontier | State transition, `PatchDiff`, or op outcomes | `TickReceipt`, op outcome details | Patch commit, receipt, optional audit receipt | Real local runtime fact; not native Continuum receipt-family output yet | +| Tick / patch | One patch or ordered patch sequence | Writer and graph frontier | State transition, `PatchDiff`, or op outcomes | `TickReceipt`, op outcome details | Patch commit, receipt, optional audit receipt | Real local runtime fact; not linked to separate Continuum receipt-family witnesses yet | | Read / optic | Observer/read target plus basis | Live, coordinate, or strand source | Node/property/traversal/materialized reading | `ReadIdentity`, checkpoint-tail witnesses, failure cause | Read identity plus checkpoint/tail anchors | Real local read fact; missing generated `ReadingEnvelope` | | Provenance slice | Backward cone for a target | Patch graph reachable from target | Bounded reconstructed state and patch count | Causal patch list, optional receipts | Provenance payload and source SHAs | Real local source fact; missing Continuum evidence wrapper | | Strand / braid | Strand overlay or braided strand set | Parent frontier plus overlay heads | Materialized strand state or conflict trace | conflict receipts, conflict anchors, participant traces | strand descriptor, overlay patches, conflict analysis | Candidate settlement-family source facts | @@ -68,8 +68,8 @@ Current `git-warp` facts map into it conservatively: ## Evidence Posture -The first v18 compatibility layer must mark `git-warp` outputs as translated -evidence unless a native Continuum runtime witness exists. +The first v18 compatibility layer must mark `git-warp` outputs as +participant-runtime evidence unless a separate Continuum witness exists. That means: @@ -77,7 +77,7 @@ That means: - a conflict trace can be mapped toward `settlement-family`; - a read result can be mapped toward `runtime-boundary-family`; - a sync suffix can be mapped toward `WitnessedSuffixShell`; -- none of those mappings may claim native Continuum witnesshood by shape alone. +- none of those mappings may claim separate Continuum witnesshood by shape alone. ## Next Engineering Cut @@ -92,7 +92,7 @@ The seam should not: - parse arbitrary GraphQL in the domain; - generate types at runtime; - make `git-warp` the owner of Continuum family semantics; -- equate translated `git-warp` evidence with native Continuum witnesshood. +- equate git-warp participant evidence with separate Continuum witnesshood. ## SSJS Scorecard @@ -104,7 +104,8 @@ The seam should not: modules and their gaps are named. - Message parsing: green; no behavior branches introduced. - Ambient time or entropy: green; no runtime code introduced. -- Fake shape trust or cast-cosplay: green; translated evidence is explicit. +- Fake shape trust or cast-cosplay: green; participant-runtime evidence is + explicit. ## Closeout diff --git a/docs/design/0150-v18-evidence-posture/v18-evidence-posture.md b/docs/design/0150-v18-evidence-posture/v18-evidence-posture.md new file mode 100644 index 000000000..c93943b6b --- /dev/null +++ b/docs/design/0150-v18-evidence-posture/v18-evidence-posture.md @@ -0,0 +1,129 @@ +--- +cycle: 0150 +task_id: V18_evidence_posture +status: Complete +sponsors: + human: James + agent: Codex +started_at: 2026-05-21 +completed_at: 2026-05-21 +release_home: v18.0.0 +--- + +# V18 Evidence Posture + +## Pull + +The generated-artifact seam can now admit Continuum-family descriptors, but +the next compatibility cut must prevent a projected value from pretending to +carry a separate Continuum witness reference. + +## Hill + +`git-warp` has a runtime-backed evidence status that separates participant +runtime evidence from Continuum-witnessed evidence and requires an explicit +Continuum witness reference before witnessed evidence can be claimed. + +## Playback Questions + +Agent: + +- Does git-warp participant evidence carry an explicit participant-runtime + posture? +- Does Continuum-witnessed evidence require a witness reference? +- Does the model reject participant-runtime evidence that tries to smuggle in a + Continuum witness reference? + +Human: + +- Can later v18 receipt projections say "this is git-warp participant evidence + projected into a Continuum-family shape" without overclaiming? + +## Accessibility / Assistive Reading Posture + +The evidence status is plain data with stable string fields. No visual-only +state is introduced. + +## Localization / Directionality Posture + +The posture values are protocol identifiers and not localized prose. Human +summaries remain ordinary strings supplied by callers. + +## Agent Inspectability / Explainability Posture + +The status object exposes posture, source runtime, basis reference, optional +Continuum witness reference, and summary as inspectable fields. + +## Non-Goals + +- Do not implement Continuum witness production. +- Do not generate receipt-family values in this slice. +- Do not build a generic WARP Optic engine. + +## RED + +Expected failing spec: + +```text +npx vitest run test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts +``` + +Observed RED: + +```text +Error: Cannot find module '../../../../src/domain/continuum/ContinuumEvidencePosture.ts' +``` + +## GREEN + +This slice adds: + +- `ContinuumEvidencePosture` +- `ContinuumEvidenceStatus` + +Git-warp participant evidence is represented as `participant-runtime` with +`sourceRuntime: "git-warp"`. Continuum-witnessed evidence is represented as +`continuum-witnessed` and cannot be constructed unless `continuumWitnessRef` is +present. Participant-runtime evidence rejects `continuumWitnessRef` so +compatibility output cannot smuggle witnesshood through an optional field. + +## Playback + +Witness: + +```text +npx vitest run test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/domain/index.exports.test.ts +Test Files 2 passed (2) +Tests 55 passed (55) + +npm run typecheck:src -- --pretty false +``` + +Agent answers: + +- Yes, git-warp participant evidence carries explicit participant-runtime + posture. +- Yes, Continuum-witnessed evidence requires `continuumWitnessRef`. +- Yes, participant-runtime evidence with `continuumWitnessRef` is rejected. + +Human answer: + +- Later receipt-family projections can carry git-warp participant evidence + without claiming separate Continuum witnesshood. + +## SSJS Scorecard + +- Runtime-backed forms: green; both new concepts are classes with constructor + validation and frozen instances. +- Boundary validation: green; no raw boundary parsing was introduced. +- Behavior ownership: green; posture validation and evidence-status invariants + live on the evidence concepts. +- Message parsing: green; no message parsing introduced. +- Ambient time or entropy: green; no ambient time or entropy introduced. +- Fake shape trust or cast-cosplay: green; Continuum-witnessed evidence cannot + be claimed without an explicit witness reference. + +## Closeout + +This closes BEARING task 6 and gives receipt-family projection work an honest +evidence-status carrier. diff --git a/docs/design/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md b/docs/design/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md new file mode 100644 index 000000000..18cde7a4a --- /dev/null +++ b/docs/design/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md @@ -0,0 +1,136 @@ +--- +cycle: 0151 +task_id: V18_patch_commit_visibility +status: Complete +sponsors: + human: James + agent: Codex +started_at: 2026-05-21 +completed_at: 2026-05-21 +release_home: v18.0.0 +--- + +# V18 Patch Commit Visibility + +## Pull + +Receipt-family projection depends on the source fact underneath it: a successful +patch commit must mean canonical writer-tip advancement and visible graph truth, +not merely Git object creation. + +## Hill + +Patch commit success is reported only after the writer ref is atomically +advanced to the returned patch commit and the returned commit is visible through +materialization. + +## Playback Questions + +Agent: + +- Does patch commit use the persistence CAS ref surface for writer-tip + advancement? +- Does commit reject success when the writer ref does not point at the returned + commit after CAS? +- Does a successful graph patch become visible through materialization? + +Human: + +- Can later receipt-family projections trust a returned patch SHA as the + canonical writer-tip fact? + +## Accessibility / Assistive Reading Posture + +No user-facing visual surface changes. The contract is asserted through tests +and error codes. + +## Localization / Directionality Posture + +No localized strings are introduced. Error messages remain developer-facing. + +## Agent Inspectability / Explainability Posture + +The visibility failure path uses a stable error code so agents can distinguish +object creation from canonical writer-tip visibility. + +## Non-Goals + +- Do not change checkpoint, strand, or audit ref update behavior. +- Do not project receipt-family values yet. +- Do not rewrite existing patch history. + +## RED + +Expected failing spec: + +```text +npx vitest run test/unit/domain/services/PatchCommitter.visibility.test.ts +``` + +Observed RED: + +```text +expected [] to deeply equal [{ ref, newOid, expectedOid }] +promise resolved instead of rejecting +``` + +The old path used plain `updateRef()` and did not verify the post-update writer +ref before returning success. + +## GREEN + +This slice changes patch commit persistence to: + +1. create the patch commit object, +2. atomically advance the writer ref with `compareAndSwapRef()`, +3. reread the writer ref, +4. report success only when the writer ref points at the returned commit SHA. + +If the post-CAS visibility check fails, the commit path throws +`WriterError` with code `WRITER_COMMIT_NOT_VISIBLE`. + +## Playback + +Witness: + +```text +npx vitest run test/unit/domain/services/PatchCommitter.visibility.test.ts test/unit/domain/services/PatchBuilder.cas.test.ts +Test Files 2 passed (2) +Tests 10 passed (10) + +npm run typecheck:src -- --pretty false +npm run typecheck:test -- --pretty false +npm run lint:sludge +npx eslint --no-warn-ignored src/domain/services/PatchCommitter.ts src/domain/errors/WriterError.ts test/unit/domain/services/PatchCommitter.visibility.test.ts test/unit/domain/services/PatchBuilder.cas.test.ts +git diff --check +``` + +Agent answers: + +- Yes, patch commit uses the CAS ref surface for writer-tip advancement. +- Yes, commit rejects success when the writer ref does not point at the + returned commit after CAS. +- Yes, the successful graph patch test proves materialization-visible graph + truth. + +Human answer: + +- Later receipt-family projections can trust a returned patch SHA as canonical + writer-tip evidence after successful commit. + +## SSJS Scorecard + +- Runtime-backed forms: green; existing `WriterError` carries the new stable + visibility code. +- Boundary validation: green; persistence stays behind the ref port. +- Behavior ownership: green; commit visibility is enforced inside the patch + commit path that owns patch persistence. +- Message parsing: green; no message parsing introduced. +- Ambient time or entropy: green; no ambient time or entropy introduced. +- Fake shape trust or cast-cosplay: green; success is now checked against the + canonical writer ref instead of inferred from object creation. + +## Closeout + +This closes BEARING task 7 and gives receipt projection a stronger source fact: +successful patch commit means canonical writer-tip visibility. diff --git a/docs/design/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md b/docs/design/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md new file mode 100644 index 000000000..787450369 --- /dev/null +++ b/docs/design/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md @@ -0,0 +1,133 @@ +--- +cycle: 0152 +task_id: V18_same_writer_race_witness +status: Complete +sponsors: + human: James + agent: Codex +started_at: 2026-05-21 +completed_at: 2026-05-21 +release_home: v18.0.0 +--- + +# V18 Same-Writer Race Witness + +## Pull + +The commit visibility contract is stronger, but v18 receipt projection also +needs proof that concurrent same-writer builders cannot both become visible +truth. + +## Hill + +A same-writer concurrent patch race has a regression witness proving exactly +one stale builder wins, the final writer frontier names the winning commit, and +only the winning patch is visible after materialization. + +## Playback Questions + +Agent: + +- Do two concurrent builders for the same writer settle with exactly one + successful commit? +- Does the writer ref point at the winning commit SHA? +- Does materialized graph state include only the winning patch's node? + +Human: + +- Can receipt-family projection ignore losing same-writer race objects because + they are not canonical writer-tip history? + +## Accessibility / Assistive Reading Posture + +No visual surface changes. The witness is an executable regression test. + +## Localization / Directionality Posture + +No localized strings are introduced. + +## Agent Inspectability / Explainability Posture + +The test records winning SHA, frontier, and visible state assertions in a way +agents can rerun. + +## Non-Goals + +- Do not change multi-writer coexistence semantics. +- Do not change conflict analysis or strand behavior. +- Do not project receipt-family values yet. + +## RED + +Expected failing spec if the CAS/visibility contract regresses: + +```text +npx vitest run test/unit/domain/WarpGraph.sameWriterRace.test.ts +``` + +Observed result after slice 7 CAS hardening: + +```text +Test Files 1 passed (1) +Tests 1 passed (1) +``` + +This is a regression witness rather than a behavior-changing slice. It would +have been unstable or false before the commit path had CAS-backed final +frontier visibility. + +## GREEN + +This slice adds `WarpGraph.sameWriterRace.test.ts`. The test creates two +patch builders for the same writer from the same expected parent, commits both +concurrently, and asserts: + +- exactly one commit wins; +- exactly one stale builder is rejected with `WRITER_CAS_CONFLICT`; +- the final writer ref names the winning SHA; +- the winning node is visible after materialization; +- the losing node is not visible after materialization. + +## Playback + +Witness: + +```text +npx vitest run test/unit/domain/WarpGraph.sameWriterRace.test.ts test/unit/domain/services/PatchCommitter.visibility.test.ts +Test Files 2 passed (2) +Tests 4 passed (4) + +npm run typecheck:test -- --pretty false +npx eslint --no-warn-ignored test/unit/domain/WarpGraph.sameWriterRace.test.ts +``` + +Agent answers: + +- Yes, two concurrent same-writer builders settle with exactly one successful + commit. +- Yes, the writer ref points at the winning commit SHA. +- Yes, materialized graph state includes the winning node and excludes the + losing node. + +Human answer: + +- Receipt-family projection can ignore losing same-writer race objects because + they are not canonical writer-tip history. + +## SSJS Scorecard + +- Runtime-backed forms: green; no new runtime model was required. +- Boundary validation: green; the test exercises the runtime through existing + patch and persistence ports. +- Behavior ownership: green; race semantics remain owned by patch commit and + writer ref behavior. +- Message parsing: green; assertions use error code and graph state, not error + text. +- Ambient time or entropy: green; no ambient time or entropy introduced. +- Fake shape trust or cast-cosplay: green; the witness checks final frontier + and visible state directly. + +## Closeout + +This closes BEARING task 8 and protects the receipt source stream against +same-writer stale-builder races. diff --git a/docs/design/0153-v18-receipt-family-projection/v18-receipt-family-projection.md b/docs/design/0153-v18-receipt-family-projection/v18-receipt-family-projection.md new file mode 100644 index 000000000..ae4e5d37f --- /dev/null +++ b/docs/design/0153-v18-receipt-family-projection/v18-receipt-family-projection.md @@ -0,0 +1,142 @@ +--- +cycle: 0153 +task_id: V18_receipt_family_projection +status: Complete +sponsors: + human: James + agent: Codex +started_at: 2026-05-21 +completed_at: 2026-05-21 +release_home: v18.0.0 +--- + +# V18 Receipt Family Projection + +## Pull + +The repo can admit generated receipt-family artifacts and can now distinguish +git-warp participant evidence from Continuum-witnessed evidence. The next step +is to project real `git-warp` receipt facts into the generated Continuum +receipt-family shape. + +## Hill + +`TickReceipt` values can be projected into Continuum receipt-family `Receipt` +facts with generated artifact authority and explicit git-warp participant +evidence posture. + +## Playback Questions + +Agent: + +- Does the projector map `TickReceipt` fields to the receipt-family `Receipt` + shape? +- Does the projection carry generated artifact authority and explicit evidence + status? +- Does the projection reject non-receipt-family artifacts? + +Human: + +- Can `warp-ttd` receive receipt-family facts from `git-warp` instead of + reverse-engineering local `TickReceipt` folklore? + +## Accessibility / Assistive Reading Posture + +The projection is inspectable structured data. No visual-only state is +introduced. + +## Localization / Directionality Posture + +Protocol identifiers are not localized. Summaries are plain strings that can be +localized later at product boundaries. + +## Agent Inspectability / Explainability Posture + +The projection keeps artifact descriptor, evidence status, and receipt facts as +separate inspectable fields. + +## Non-Goals + +- Do not claim separate Continuum witnesshood. +- Do not add delivery observation projection yet. +- Do not call Wesley or parse GraphQL at runtime. + +## RED + +Expected failing spec: + +```text +npx vitest run test/unit/domain/continuum/ContinuumReceiptProjection.test.ts +``` + +Observed RED: + +```text +Error: Cannot find module '../../../../src/domain/continuum/ContinuumReceipt.ts' +``` + +## GREEN + +This slice adds: + +- `ContinuumReceipt` +- `ContinuumReceiptFamilyProjection` +- `ContinuumReceiptProjector` + +`TickReceipt` maps into the Continuum receipt-family `Receipt` shape: + +- `patchSha` becomes `receiptId`, `headId`, and `digest`; +- `lamport` becomes `frameIndex` and `outputTick`; +- `writer` becomes `laneId` and `writerId`; +- superseded operations become rejected rewrites; +- applied and redundant operations become admitted rewrites; +- evidence posture remains separate from the receipt fact. + +The aggregate projection requires a `receipt-family` artifact descriptor and +keeps artifact authority, evidence status, and receipt facts as separate +inspectable fields. + +## Playback + +Witness: + +```text +npx vitest run test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/domain/index.exports.test.ts +Test Files 3 passed (3) +Tests 58 passed (58) + +npm run typecheck:src -- --pretty false +npm run typecheck:test -- --pretty false +npm run lint:sludge +npx eslint --no-warn-ignored src/domain/continuum/ContinuumReceipt.ts src/domain/continuum/ContinuumReceiptFamilyProjection.ts src/domain/continuum/ContinuumReceiptProjector.ts test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/index.exports.test.ts index.ts +git diff --check +``` + +Agent answers: + +- Yes, the projector maps `TickReceipt` fields to Continuum `Receipt` fields. +- Yes, the projection carries generated artifact authority and explicit + evidence status. +- Yes, non-receipt-family artifacts are rejected. + +Human answer: + +- `warp-ttd` can now receive receipt-family facts from `git-warp` without + reverse-engineering raw `TickReceipt` shape. + +## SSJS Scorecard + +- Runtime-backed forms: green; receipt and projection are classes with + constructor validation and frozen instances. +- Boundary validation: green; generated artifact authority stays represented + by the descriptor admitted in the previous slice. +- Behavior ownership: green; receipt projection behavior lives in the projector. +- Message parsing: green; no behavior branches parse messages. +- Ambient time or entropy: green; no ambient time or entropy introduced. +- Fake shape trust or cast-cosplay: green; evidence status remains separate and + participant-runtime by default. + +## Closeout + +This closes BEARING task 9 and gives the next slice a generated-family receipt +fact set to smoke through the `warp-ttd` consumer posture. diff --git a/docs/design/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md b/docs/design/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md new file mode 100644 index 000000000..ffcdd645c --- /dev/null +++ b/docs/design/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md @@ -0,0 +1,136 @@ +--- +cycle: 0154 +task_id: V18_warp_ttd_receipt_smoke +status: Complete +sponsors: + human: James + agent: Codex +started_at: 2026-05-21 +completed_at: 2026-05-21 +release_home: v18.0.0 +--- + +# V18 WARP TTD Receipt Smoke + +## Pull + +`git-warp` can project `TickReceipt` into Continuum receipt-family facts. The +opening campaign needs one smoke test proving a `warp-ttd` consumer can read +those facts without reverse-engineering raw `TickReceipt` shape. + +## Hill + +A live git-warp patch receipt can be projected through the generated +receipt-family descriptor into `warp-ttd`-targeted receipt facts with explicit +participant-runtime evidence posture. + +## Playback Questions + +Agent: + +- Does the smoke start from a real committed git-warp patch? +- Does it load the generated receipt-family fixture descriptor instead of a + handwritten descriptor-only shortcut? +- Does it query projected receipt-family facts by head and frame for a + `warp-ttd` target? +- Does the projection keep participant-runtime evidence posture explicit? + +Human: + +- Is this enough proof to stop `warp-ttd` from depending on raw git-warp + `TickReceipt` folklore for the first receipt shell? + +## Accessibility / Assistive Reading Posture + +No visual surface changes. The smoke output is structured test evidence. + +## Localization / Directionality Posture + +No localized strings are introduced. + +## Agent Inspectability / Explainability Posture + +The smoke keeps artifact descriptor, evidence status, and projected receipts as +separate inspectable facts. + +## Non-Goals + +- Do not edit the `warp-ttd` repo in this slice. +- Do not add delivery observation projection. +- Do not claim separate Continuum witnesshood. + +## RED + +Expected failing spec: + +```text +npx vitest run test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts +``` + +Observed result: + +```text +Test Files 1 passed (1) +Tests 1 passed (1) +``` + +This smoke became green immediately because slice 9 had already added the +receipt-family projection surface. + +## GREEN + +The smoke: + +1. opens a real in-memory git-warp runtime; +2. commits a real patch; +3. materializes real `TickReceipt` output; +4. loads the generated receipt-family fixture descriptor through + `ContinuumArtifactJsonFileAdapter`; +5. projects the materialized receipts into `ContinuumReceiptFamilyProjection`; +6. queries `receiptsForHead()` for the winning patch SHA and frame; +7. asserts the evidence posture remains participant-runtime evidence, not + Continuum-witnessed evidence. + +## Playback + +Witness: + +```text +npx vitest run test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/infrastructure/adapters/ContinuumArtifactJsonFileAdapter.test.ts +Test Files 4 passed (4) +Tests 24 passed (24) + +npm run typecheck:test -- --pretty false +npx eslint --no-warn-ignored test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts +``` + +Agent answers: + +- Yes, the smoke starts from a real committed git-warp patch. +- Yes, it loads the generated receipt-family fixture descriptor through the + artifact adapter. +- Yes, it queries projected receipt-family facts by head and frame for a + `warp-ttd` target. +- Yes, participant-runtime evidence posture remains explicit. + +Human answer: + +- This is enough first proof to stop `warp-ttd` from needing raw git-warp + `TickReceipt` folklore for the first receipt shell. + +## SSJS Scorecard + +- Runtime-backed forms: green; the smoke uses the runtime-backed projection + classes from slice 9. +- Boundary validation: green; generated fixture JSON is admitted through the + adapter seam. +- Behavior ownership: green; `git-warp` owns its receipt projection and + `warp-ttd` remains a consumer target. +- Message parsing: green; no behavior branches parse messages. +- Ambient time or entropy: green; no ambient time or entropy introduced. +- Fake shape trust or cast-cosplay: green; the projection remains + participant-runtime evidence and does not claim separate witnesshood. + +## Closeout + +This closes BEARING task 10 and completes the requested five-slice batch. diff --git a/docs/method/retro/0145-push-pr-review-merge/push-pr-review-merge.md b/docs/method/retro/0145-push-pr-review-merge/push-pr-review-merge.md index 30e8df71b..9704c6861 100644 --- a/docs/method/retro/0145-push-pr-review-merge/push-pr-review-merge.md +++ b/docs/method/retro/0145-push-pr-review-merge/push-pr-review-merge.md @@ -35,4 +35,4 @@ acceptance over generated-family facts. The release train made it into the station, then the station sign kept saying "boarding soon." This retro fixes the sign. The next mess is bigger: teach `git-warp` to speak Continuum contract families without dressing adapter -folklore up as native witnesshood. +folklore up as separate Continuum witnesshood. diff --git a/docs/method/retro/0150-v18-evidence-posture/v18-evidence-posture.md b/docs/method/retro/0150-v18-evidence-posture/v18-evidence-posture.md new file mode 100644 index 000000000..d4704d2fb --- /dev/null +++ b/docs/method/retro/0150-v18-evidence-posture/v18-evidence-posture.md @@ -0,0 +1,54 @@ +--- +cycle: 0150 +task_id: V18_evidence_posture +status: Complete +sponsors: + human: James + agent: Codex +completed_at: 2026-05-21 +--- + +# Retro: V18 Evidence Posture + +## Hill + +`git-warp` has a runtime-backed evidence status that separates participant +runtime evidence from Continuum-witnessed evidence and requires an explicit +Continuum witness reference before witnessed evidence can be claimed. + +## Result + +Hill met. + +## Witness + +```text +npx vitest run test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/domain/index.exports.test.ts +Test Files 2 passed (2) +Tests 55 passed (55) + +npm run typecheck:src -- --pretty false +``` + +## Drift Check + +No drift. The implementation stayed within the evidence-posture slice and did +not start receipt-family projection or Continuum witness production. + +## What Mess We Got Into + +The repo had a generated-artifact gate but no runtime object for the more +dangerous claim: whether a Continuum-shaped value carries an explicit witness +reference or is participant-runtime evidence from `git-warp`. + +## What Mess We Got Out Of + +Continuum-witnessed evidence now has to carry `continuumWitnessRef`. +Git-warp participant evidence is the explicit default and cannot include that +witness field. + +## What Comes Next + +Prove that patch commit success means canonical writer-tip advancement and +visible graph truth, then use that proven source fact for receipt-family +projection. diff --git a/docs/method/retro/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md b/docs/method/retro/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md new file mode 100644 index 000000000..05d183748 --- /dev/null +++ b/docs/method/retro/0151-v18-patch-commit-visibility/v18-patch-commit-visibility.md @@ -0,0 +1,57 @@ +--- +cycle: 0151 +task_id: V18_patch_commit_visibility +status: Complete +sponsors: + human: James + agent: Codex +completed_at: 2026-05-21 +--- + +# Retro: V18 Patch Commit Visibility + +## Hill + +Patch commit success is reported only after the writer ref is atomically +advanced to the returned patch commit and the returned commit is visible through +materialization. + +## Result + +Hill met. + +## Witness + +```text +npx vitest run test/unit/domain/services/PatchCommitter.visibility.test.ts test/unit/domain/services/PatchBuilder.cas.test.ts +Test Files 2 passed (2) +Tests 10 passed (10) + +npm run typecheck:src -- --pretty false +npm run typecheck:test -- --pretty false +npm run lint:sludge +npx eslint --no-warn-ignored src/domain/services/PatchCommitter.ts src/domain/errors/WriterError.ts test/unit/domain/services/PatchCommitter.visibility.test.ts test/unit/domain/services/PatchBuilder.cas.test.ts +git diff --check +``` + +## Drift Check + +No drift. The slice stayed on patch commit success semantics and did not touch +checkpoint, audit, strand, or receipt projection behavior. + +## What Mess We Got Into + +Patch commit had the right preflight CAS check but then used non-CAS ref update +for the final writer-tip move. It also treated object creation as enough for +success. + +## What Mess We Got Out Of + +The final ref move is now CAS-backed and success requires the writer ref to +name the returned commit. If the ref is not visible, the error code says so. + +## What Comes Next + +Use this stronger commit contract to test same-writer concurrent patch races: +only one stale concurrent builder may win, and only the winning patch may become +visible graph truth. diff --git a/docs/method/retro/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md b/docs/method/retro/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md new file mode 100644 index 000000000..3ba51623d --- /dev/null +++ b/docs/method/retro/0152-v18-same-writer-race-witness/v18-same-writer-race-witness.md @@ -0,0 +1,52 @@ +--- +cycle: 0152 +task_id: V18_same_writer_race_witness +status: Complete +sponsors: + human: James + agent: Codex +completed_at: 2026-05-21 +--- + +# Retro: V18 Same-Writer Race Witness + +## Hill + +A same-writer concurrent patch race has a regression witness proving exactly +one stale builder wins, the final writer frontier names the winning commit, and +only the winning patch is visible after materialization. + +## Result + +Hill met. + +## Witness + +```text +npx vitest run test/unit/domain/WarpGraph.sameWriterRace.test.ts test/unit/domain/services/PatchCommitter.visibility.test.ts +Test Files 2 passed (2) +Tests 4 passed (4) + +npm run typecheck:test -- --pretty false +npx eslint --no-warn-ignored test/unit/domain/WarpGraph.sameWriterRace.test.ts +``` + +## Drift Check + +No drift. This was intentionally a witness slice. It did not alter runtime code +after the slice 7 CAS visibility hardening. + +## What Mess We Got Into + +Before projecting receipts, we needed to prove stale same-writer builders do +not both become canonical history just because both can create patch objects. + +## What Mess We Got Out Of + +The new witness pins the final frontier and the visible graph state. One stale +builder wins; the losing builder is not graph truth. + +## What Comes Next + +Project `TickReceipt` facts into Continuum receipt-family `Receipt` facts with +git-warp participant evidence posture. diff --git a/docs/method/retro/0153-v18-receipt-family-projection/v18-receipt-family-projection.md b/docs/method/retro/0153-v18-receipt-family-projection/v18-receipt-family-projection.md new file mode 100644 index 000000000..0d6078a15 --- /dev/null +++ b/docs/method/retro/0153-v18-receipt-family-projection/v18-receipt-family-projection.md @@ -0,0 +1,55 @@ +--- +cycle: 0153 +task_id: V18_receipt_family_projection +status: Complete +sponsors: + human: James + agent: Codex +completed_at: 2026-05-21 +--- + +# Retro: V18 Receipt Family Projection + +## Hill + +`TickReceipt` values can be projected into Continuum receipt-family `Receipt` +facts with generated artifact authority and explicit git-warp participant +evidence posture. + +## Result + +Hill met. + +## Witness + +```text +npx vitest run test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/domain/index.exports.test.ts +Test Files 3 passed (3) +Tests 58 passed (58) + +npm run typecheck:src -- --pretty false +npm run typecheck:test -- --pretty false +npm run lint:sludge +npx eslint --no-warn-ignored src/domain/continuum/ContinuumReceipt.ts src/domain/continuum/ContinuumReceiptFamilyProjection.ts src/domain/continuum/ContinuumReceiptProjector.ts test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/index.exports.test.ts index.ts +git diff --check +``` + +## Drift Check + +No drift. The slice projected `TickReceipt` to receipt-family `Receipt` facts +only. Delivery observations and separate Continuum witness production remain +out of scope. + +## What Mess We Got Into + +`warp-ttd` previously had to know too much about raw git-warp `TickReceipt` +shape. That is adapter folklore, not a generated-family contract. + +## What Mess We Got Out Of + +`git-warp` now owns the projection from its local receipt fact into a +Continuum receipt-family `Receipt`, with evidence posture carried separately. + +## What Comes Next + +Add the first `warp-ttd` smoke over the generated-family receipt projection. diff --git a/docs/method/retro/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md b/docs/method/retro/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md new file mode 100644 index 000000000..882e3ee6d --- /dev/null +++ b/docs/method/retro/0154-v18-warp-ttd-receipt-smoke/v18-warp-ttd-receipt-smoke.md @@ -0,0 +1,52 @@ +--- +cycle: 0154 +task_id: V18_warp_ttd_receipt_smoke +status: Complete +sponsors: + human: James + agent: Codex +completed_at: 2026-05-21 +--- + +# Retro: V18 WARP TTD Receipt Smoke + +## Hill + +A live git-warp patch receipt can be projected through the generated +receipt-family descriptor into `warp-ttd`-targeted receipt facts with explicit +participant-runtime evidence posture. + +## Result + +Hill met. + +## Witness + +```text +npx vitest run test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts test/unit/domain/continuum/ContinuumReceiptProjection.test.ts test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts test/unit/infrastructure/adapters/ContinuumArtifactJsonFileAdapter.test.ts +Test Files 4 passed (4) +Tests 24 passed (24) + +npm run typecheck:test -- --pretty false +npx eslint --no-warn-ignored test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts +``` + +## Drift Check + +No drift. The `warp-ttd` repo was inspected for consumer posture but not edited. +This slice stayed inside `git-warp`. + +## What Mess We Got Into + +The existing stack had enough local receipt truth, but `warp-ttd` could only +consume it by knowing the raw git-warp receipt shape. + +## What Mess We Got Out Of + +There is now an executable smoke proving a live git-warp receipt can be exposed +as generated receipt-family facts with participant-runtime evidence posture. + +## What Comes Next + +Re-plan with evidence in hand before expanding into reading envelopes, suffix +runtime boundaries, neighborhood core, and settlement-family cuts. diff --git a/index.ts b/index.ts index 260c1a824..b19777f6a 100644 --- a/index.ts +++ b/index.ts @@ -211,11 +211,21 @@ import { import ContinuumArtifactAuthority from './src/domain/continuum/ContinuumArtifactAuthority.ts'; import ContinuumArtifactDescriptor from './src/domain/continuum/ContinuumArtifactDescriptor.ts'; import ContinuumArtifactIngestionPolicy from './src/domain/continuum/ContinuumArtifactIngestionPolicy.ts'; +import ContinuumEvidencePosture from './src/domain/continuum/ContinuumEvidencePosture.ts'; +import ContinuumEvidenceStatus from './src/domain/continuum/ContinuumEvidenceStatus.ts'; import ContinuumFamilyId from './src/domain/continuum/ContinuumFamilyId.ts'; +import ContinuumReceipt from './src/domain/continuum/ContinuumReceipt.ts'; +import ContinuumReceiptFamilyProjection from './src/domain/continuum/ContinuumReceiptFamilyProjection.ts'; +import ContinuumReceiptProjector from './src/domain/continuum/ContinuumReceiptProjector.ts'; import ContinuumArtifactJsonFileAdapter from './src/infrastructure/adapters/ContinuumArtifactJsonFileAdapter.ts'; import type { ContinuumArtifactAuthorityValue } from './src/domain/continuum/ContinuumArtifactAuthority.ts'; import type { ContinuumArtifactDescriptorFields } from './src/domain/continuum/ContinuumArtifactDescriptor.ts'; +import type { ContinuumEvidencePostureValue } from './src/domain/continuum/ContinuumEvidencePosture.ts'; +import type { ContinuumEvidenceStatusFields } from './src/domain/continuum/ContinuumEvidenceStatus.ts'; import type { ContinuumFamilyIdValue } from './src/domain/continuum/ContinuumFamilyId.ts'; +import type { ContinuumReceiptFields } from './src/domain/continuum/ContinuumReceipt.ts'; +import type { ContinuumReceiptFamilyProjectionFields } from './src/domain/continuum/ContinuumReceiptFamilyProjection.ts'; +import type { ContinuumReceiptProjectionRequest } from './src/domain/continuum/ContinuumReceiptProjector.ts'; import type { ContinuumArtifactJsonLoadContext } from './src/infrastructure/adapters/ContinuumArtifactJsonFileAdapter.ts'; export { @@ -333,7 +343,12 @@ export { ContinuumArtifactAuthority, ContinuumArtifactDescriptor, ContinuumArtifactIngestionPolicy, + ContinuumEvidencePosture, + ContinuumEvidenceStatus, ContinuumFamilyId, + ContinuumReceipt, + ContinuumReceiptFamilyProjection, + ContinuumReceiptProjector, ContinuumArtifactJsonFileAdapter, // Tick receipts (LIGHTHOUSE) @@ -384,8 +399,13 @@ export type { SyncRateLimitConfig, ContinuumArtifactAuthorityValue, ContinuumArtifactDescriptorFields, + ContinuumEvidencePostureValue, + ContinuumEvidenceStatusFields, ContinuumArtifactJsonLoadContext, ContinuumFamilyIdValue, + ContinuumReceiptFields, + ContinuumReceiptFamilyProjectionFields, + ContinuumReceiptProjectionRequest, }; // WarpApp is the primary product-facing API for v15. diff --git a/src/domain/continuum/ContinuumEvidencePosture.ts b/src/domain/continuum/ContinuumEvidencePosture.ts new file mode 100644 index 000000000..15befd876 --- /dev/null +++ b/src/domain/continuum/ContinuumEvidencePosture.ts @@ -0,0 +1,50 @@ +import WarpError from '../errors/WarpError.ts'; + +const PARTICIPANT_RUNTIME_POSTURE = 'participant-runtime'; +const CONTINUUM_WITNESSED_POSTURE = 'continuum-witnessed'; + +export type ContinuumEvidencePostureValue = + | typeof PARTICIPANT_RUNTIME_POSTURE + | typeof CONTINUUM_WITNESSED_POSTURE; + +export const CONTINUUM_EVIDENCE_POSTURES: readonly ContinuumEvidencePostureValue[] = Object.freeze([ + PARTICIPANT_RUNTIME_POSTURE, + CONTINUUM_WITNESSED_POSTURE, +]); + +/** Runtime-backed evidence posture for Continuum-compatible values. */ +export default class ContinuumEvidencePosture { + readonly value: ContinuumEvidencePostureValue; + + constructor(value: string) { + this.value = requireContinuumEvidencePosture(value); + Object.freeze(this); + } + + /** Returns true for evidence produced by a Continuum participant runtime. */ + isParticipantRuntime(): boolean { + return this.value === PARTICIPANT_RUNTIME_POSTURE; + } + + /** Returns true only for values backed by an explicit Continuum witness. */ + isContinuumWitnessed(): boolean { + return this.value === CONTINUUM_WITNESSED_POSTURE; + } + + /** Returns the stable posture string. */ + toString(): string { + return this.value; + } +} + +/** Validates a raw evidence posture string. */ +export function requireContinuumEvidencePosture(value: string): ContinuumEvidencePostureValue { + const valid = CONTINUUM_EVIDENCE_POSTURES.find((candidate) => candidate === value); + if (valid === undefined) { + throw new WarpError( + `Continuum evidence posture must be one of: ${CONTINUUM_EVIDENCE_POSTURES.join(', ')}`, + 'E_VALIDATION', + ); + } + return valid; +} diff --git a/src/domain/continuum/ContinuumEvidenceStatus.ts b/src/domain/continuum/ContinuumEvidenceStatus.ts new file mode 100644 index 000000000..ee8ae626f --- /dev/null +++ b/src/domain/continuum/ContinuumEvidenceStatus.ts @@ -0,0 +1,91 @@ +import WarpError from '../errors/WarpError.ts'; +import ContinuumEvidencePosture from './ContinuumEvidencePosture.ts'; + +export type ContinuumEvidenceStatusFields = { + readonly posture: string | ContinuumEvidencePosture; + readonly sourceRuntime: string; + readonly basisRef: string; + readonly summary: string; + readonly continuumWitnessRef?: string; +}; + +export type GitWarpParticipantEvidenceFields = { + readonly basisRef: string; + readonly summary: string; +}; + +/** Runtime-backed evidence status for Continuum-compatible projections. */ +export default class ContinuumEvidenceStatus { + readonly posture: ContinuumEvidencePosture; + readonly sourceRuntime: string; + readonly basisRef: string; + readonly summary: string; + readonly continuumWitnessRef: string | undefined; + + constructor(fields: ContinuumEvidenceStatusFields) { + this.posture = normalizePosture(fields.posture); + this.sourceRuntime = requireNonEmptyString(fields.sourceRuntime, 'sourceRuntime'); + this.basisRef = requireNonEmptyString(fields.basisRef, 'basisRef'); + this.summary = requireNonEmptyString(fields.summary, 'summary'); + this.continuumWitnessRef = optionalNonEmptyString(fields.continuumWitnessRef, 'continuumWitnessRef'); + validateContinuumWitnessPosture(this.posture, this.continuumWitnessRef); + Object.freeze(this); + } + + /** Creates the default v18 evidence posture for git-warp participant output. */ + static gitWarpParticipant(fields: GitWarpParticipantEvidenceFields): ContinuumEvidenceStatus { + return new ContinuumEvidenceStatus({ + posture: 'participant-runtime', + sourceRuntime: 'git-warp', + basisRef: fields.basisRef, + summary: fields.summary, + }); + } + + /** Returns true for evidence produced by a Continuum participant runtime. */ + isParticipantRuntime(): boolean { + return this.posture.isParticipantRuntime(); + } + + /** Returns true only when an explicit Continuum witness reference is carried. */ + isContinuumWitnessed(): boolean { + return this.posture.isContinuumWitnessed(); + } +} + +/** Normalizes a posture carrier. */ +function normalizePosture(value: string | ContinuumEvidencePosture): ContinuumEvidencePosture { + if (value instanceof ContinuumEvidencePosture) { + return value; + } + return new ContinuumEvidencePosture(value); +} + +/** Validates a required non-empty string. */ +function requireNonEmptyString(value: string, name: string): string { + if (typeof value !== 'string' || value.length === 0) { + throw new WarpError(`${name} must be a non-empty string`, 'E_VALIDATION'); + } + return value; +} + +/** Validates an optional non-empty string. */ +function optionalNonEmptyString(value: string | undefined, name: string): string | undefined { + if (value === undefined) { + return undefined; + } + return requireNonEmptyString(value, name); +} + +/** Enforces that witnessed evidence cannot be claimed by posture alone. */ +function validateContinuumWitnessPosture( + posture: ContinuumEvidencePosture, + continuumWitnessRef: string | undefined, +): void { + if (posture.isContinuumWitnessed() && continuumWitnessRef === undefined) { + throw new WarpError('continuumWitnessRef is required for Continuum-witnessed evidence', 'E_VALIDATION'); + } + if (posture.isParticipantRuntime() && continuumWitnessRef !== undefined) { + throw new WarpError('participant runtime evidence must not carry continuumWitnessRef', 'E_VALIDATION'); + } +} diff --git a/src/domain/continuum/ContinuumReceipt.ts b/src/domain/continuum/ContinuumReceipt.ts new file mode 100644 index 000000000..0ae2a4a64 --- /dev/null +++ b/src/domain/continuum/ContinuumReceipt.ts @@ -0,0 +1,81 @@ +import WarpError from '../errors/WarpError.ts'; + +export type ContinuumReceiptFields = { + readonly receiptId: string; + readonly headId: string; + readonly frameIndex: number; + readonly laneId: string; + readonly writerId?: string; + readonly inputTick: number; + readonly outputTick: number; + readonly admittedRewriteCount: number; + readonly rejectedRewriteCount: number; + readonly counterfactualCount: number; + readonly digest: string; + readonly summary: string; +}; + +/** Continuum receipt-family `Receipt` fact projected from git-warp receipts. */ +export default class ContinuumReceipt { + readonly receiptId: string; + readonly headId: string; + readonly frameIndex: number; + readonly laneId: string; + readonly writerId: string | undefined; + readonly inputTick: number; + readonly outputTick: number; + readonly admittedRewriteCount: number; + readonly rejectedRewriteCount: number; + readonly counterfactualCount: number; + readonly digest: string; + readonly summary: string; + + constructor(fields: ContinuumReceiptFields) { + this.receiptId = requireNonEmptyString(fields.receiptId, 'receiptId'); + this.headId = requireNonEmptyString(fields.headId, 'headId'); + this.frameIndex = requireNonNegativeInteger(fields.frameIndex, 'frameIndex'); + this.laneId = requireNonEmptyString(fields.laneId, 'laneId'); + this.writerId = optionalNonEmptyString(fields.writerId, 'writerId'); + this.inputTick = requireNonNegativeInteger(fields.inputTick, 'inputTick'); + this.outputTick = requireNonNegativeInteger(fields.outputTick, 'outputTick'); + this.admittedRewriteCount = requireNonNegativeInteger( + fields.admittedRewriteCount, + 'admittedRewriteCount', + ); + this.rejectedRewriteCount = requireNonNegativeInteger( + fields.rejectedRewriteCount, + 'rejectedRewriteCount', + ); + this.counterfactualCount = requireNonNegativeInteger( + fields.counterfactualCount, + 'counterfactualCount', + ); + this.digest = requireNonEmptyString(fields.digest, 'digest'); + this.summary = requireNonEmptyString(fields.summary, 'summary'); + Object.freeze(this); + } +} + +/** Validates a required non-empty string. */ +function requireNonEmptyString(value: string, name: string): string { + if (typeof value !== 'string' || value.length === 0) { + throw new WarpError(`${name} must be a non-empty string`, 'E_VALIDATION'); + } + return value; +} + +/** Validates an optional non-empty string. */ +function optionalNonEmptyString(value: string | undefined, name: string): string | undefined { + if (value === undefined) { + return undefined; + } + return requireNonEmptyString(value, name); +} + +/** Validates a non-negative integer. */ +function requireNonNegativeInteger(value: number, name: string): number { + if (!Number.isInteger(value) || value < 0) { + throw new WarpError(`${name} must be a non-negative integer`, 'E_VALIDATION'); + } + return value; +} diff --git a/src/domain/continuum/ContinuumReceiptFamilyProjection.ts b/src/domain/continuum/ContinuumReceiptFamilyProjection.ts new file mode 100644 index 000000000..2ac6dba17 --- /dev/null +++ b/src/domain/continuum/ContinuumReceiptFamilyProjection.ts @@ -0,0 +1,49 @@ +import WarpError from '../errors/WarpError.ts'; +import type ContinuumArtifactDescriptor from './ContinuumArtifactDescriptor.ts'; +import type ContinuumEvidenceStatus from './ContinuumEvidenceStatus.ts'; +import ContinuumFamilyId from './ContinuumFamilyId.ts'; +import type ContinuumReceipt from './ContinuumReceipt.ts'; + +const RECEIPT_FAMILY_ID = new ContinuumFamilyId('receipt-family'); + +export type ContinuumReceiptFamilyProjectionFields = { + readonly artifact: ContinuumArtifactDescriptor; + readonly evidence: ContinuumEvidenceStatus; + readonly receipts: readonly ContinuumReceipt[]; +}; + +/** Receipt-family facts projected from git-warp substrate evidence. */ +export default class ContinuumReceiptFamilyProjection { + readonly artifact: ContinuumArtifactDescriptor; + readonly evidence: ContinuumEvidenceStatus; + readonly receipts: readonly ContinuumReceipt[]; + + constructor(fields: ContinuumReceiptFamilyProjectionFields) { + requireReceiptFamilyArtifact(fields.artifact); + this.artifact = fields.artifact; + this.evidence = fields.evidence; + this.receipts = freezeReceipts(fields.receipts); + Object.freeze(this); + } + + /** Returns receipt-family facts for a head and optional frame index. */ + receiptsForHead(headId: string, frameIndex?: number): readonly ContinuumReceipt[] { + return this.receipts.filter((receipt) => ( + receipt.headId === headId && + (frameIndex === undefined || receipt.frameIndex === frameIndex) + )); + } +} + +/** Requires a generated artifact descriptor for the receipt family. */ +function requireReceiptFamilyArtifact(artifact: ContinuumArtifactDescriptor): void { + if (artifact.familyId.equals(RECEIPT_FAMILY_ID)) { + return; + } + throw new WarpError('Continuum receipt projection requires receipt-family artifact authority', 'E_VALIDATION'); +} + +/** Freezes projected receipts. */ +function freezeReceipts(receipts: readonly ContinuumReceipt[]): readonly ContinuumReceipt[] { + return Object.freeze(receipts.slice()); +} diff --git a/src/domain/continuum/ContinuumReceiptProjector.ts b/src/domain/continuum/ContinuumReceiptProjector.ts new file mode 100644 index 000000000..dc0ba53a9 --- /dev/null +++ b/src/domain/continuum/ContinuumReceiptProjector.ts @@ -0,0 +1,59 @@ +import type ContinuumArtifactDescriptor from './ContinuumArtifactDescriptor.ts'; +import type ContinuumEvidenceStatus from './ContinuumEvidenceStatus.ts'; +import ContinuumReceipt from './ContinuumReceipt.ts'; +import ContinuumReceiptFamilyProjection from './ContinuumReceiptFamilyProjection.ts'; +import type { TickReceipt } from '../types/TickReceipt.ts'; + +const RESULT_SUPERSEDED = 'superseded'; + +export type ContinuumReceiptProjectionRequest = { + readonly artifact: ContinuumArtifactDescriptor; + readonly evidence: ContinuumEvidenceStatus; + readonly tickReceipts: readonly TickReceipt[]; +}; + +/** Projects git-warp tick receipts into Continuum receipt-family facts. */ +export default class ContinuumReceiptProjector { + /** Projects one git-warp tick receipt into the Continuum `Receipt` shape. */ + projectTickReceipt(receipt: TickReceipt): ContinuumReceipt { + const rejectedCount = countRejected(receipt); + const admittedCount = receipt.ops.length - rejectedCount; + return new ContinuumReceipt({ + receiptId: `git-warp:receipt:${receipt.patchSha}`, + headId: receipt.patchSha, + frameIndex: receipt.lamport, + laneId: receipt.writer, + writerId: receipt.writer, + inputTick: previousTick(receipt.lamport), + outputTick: receipt.lamport, + admittedRewriteCount: admittedCount, + rejectedRewriteCount: rejectedCount, + counterfactualCount: 0, + digest: receipt.patchSha, + summary: `${admittedCount} admitted, ${rejectedCount} rejected over ${receipt.ops.length} operation(s)`, + }); + } + + /** Projects a receipt-family fact set with artifact and evidence posture. */ + projectTickReceipts(request: ContinuumReceiptProjectionRequest): ContinuumReceiptFamilyProjection { + const receipts = request.tickReceipts.map((receipt) => this.projectTickReceipt(receipt)); + return new ContinuumReceiptFamilyProjection({ + artifact: request.artifact, + evidence: request.evidence, + receipts, + }); + } +} + +/** Counts operations that were rejected by CRDT admission. */ +function countRejected(receipt: TickReceipt): number { + return receipt.ops.filter((op) => op.result === RESULT_SUPERSEDED).length; +} + +/** Returns the previous non-negative tick. */ +function previousTick(tick: number): number { + if (tick === 0) { + return 0; + } + return tick - 1; +} diff --git a/src/domain/errors/WriterError.ts b/src/domain/errors/WriterError.ts index c63d366cc..f0c68661c 100644 --- a/src/domain/errors/WriterError.ts +++ b/src/domain/errors/WriterError.ts @@ -14,12 +14,15 @@ import WarpError from './WarpError.ts'; * | `EMPTY_PATCH` | Patch commit attempted with zero operations | * | `WRITER_REF_ADVANCED` | Writer ref moved since beginPatch() | * | `WRITER_CAS_CONFLICT` | Compare-and-swap failure during commit | + * | `WRITER_COMMIT_NOT_VISIBLE` | Returned commit is not the writer ref tip after CAS | * | `PERSIST_WRITE_FAILED` | Git persistence operation failed | * | `NO_BLOB_STORAGE` | Content attachment attempted without blob storage | * | `WRITER_ERROR` | Generic/default writer error | */ export default class WriterError extends WarpError { declare cause: Error | undefined; + expectedSha: string | null | undefined; + actualSha: string | null | undefined; /** * Note: constructor parameter order differs from other WarpError subclasses @@ -29,6 +32,8 @@ export default class WriterError extends WarpError { */ constructor(code: string, message: string, cause?: Error) { super(message, 'WRITER_ERROR', { code }); + this.expectedSha = undefined; + this.actualSha = undefined; if (cause !== undefined) { this.cause = cause; } diff --git a/src/domain/services/PatchCommitter.ts b/src/domain/services/PatchCommitter.ts index 86949311d..cc06638d0 100644 --- a/src/domain/services/PatchCommitter.ts +++ b/src/domain/services/PatchCommitter.ts @@ -70,7 +70,7 @@ export async function commitPatch(state: CommitState): Promise { const err = new WriterError( 'WRITER_CAS_CONFLICT', 'Commit failed: writer ref was updated by another process. Re-materialize and retry.', - ) as WriterError & { expectedSha: string | null; actualSha: string | null }; + ); err.expectedSha = state.expectedParentSha; err.actualSha = currentRefSha; throw err; @@ -149,8 +149,13 @@ export async function commitPatch(state: CommitState): Promise { treeOid, parents, message, }); - // Update writer ref - await state.persistence.updateRef(writerRef, newCommitSha); + // Atomically advance writer ref and verify the returned commit is canonical. + await advanceWriterRef({ + persistence: state.persistence, + writerRef, + newCommitSha, + expectedParentSha: currentRefSha, + }); // Invoke success callback if (state.onCommitSuccess) { @@ -164,3 +169,36 @@ export async function commitPatch(state: CommitState): Promise { return newCommitSha; } + +async function advanceWriterRef(options: { + persistence: PersistencePorts; + writerRef: string; + newCommitSha: string; + expectedParentSha: string | null; +}): Promise { + try { + await options.persistence.compareAndSwapRef( + options.writerRef, + options.newCommitSha, + options.expectedParentSha, + ); + } catch (err) { + const actualSha = await options.persistence.readRef(options.writerRef); + const error = new WriterError( + 'WRITER_CAS_CONFLICT', + 'Commit failed: writer ref was updated by another process. Re-materialize and retry.', + err instanceof Error ? err : undefined, + ); + error.expectedSha = options.expectedParentSha; + error.actualSha = actualSha; + throw error; + } + + const visibleSha = await options.persistence.readRef(options.writerRef); + if (visibleSha !== options.newCommitSha) { + throw new WriterError( + 'WRITER_COMMIT_NOT_VISIBLE', + `Commit ${options.newCommitSha} was written but ${options.writerRef} points at ${visibleSha ?? '(none)'}`, + ); + } +} diff --git a/src/domain/warp/PatchSession.ts b/src/domain/warp/PatchSession.ts index d73c67acd..3f8121d39 100644 --- a/src/domain/warp/PatchSession.ts +++ b/src/domain/warp/PatchSession.ts @@ -74,6 +74,9 @@ function _buildCasConflictError( function _classifyCommitError(err: unknown, ctx: CommitContext): WriterError { // nosemgrep: ts-no-unknown-outside-adapters -- 0025B const { errMsg, cause } = _extractErrorInfo(err); const casError = _extractCasError(err); + if (casError !== null && casError.code === 'WRITER_COMMIT_NOT_VISIBLE') { + return new WriterError('WRITER_COMMIT_NOT_VISIBLE', errMsg, cause); + } if (casError !== null && casError.code === 'WRITER_CAS_CONFLICT') { return _buildCasConflictError(casError, cause, ctx); } diff --git a/test/benchmark/detachedReadBenchmark.fixture.ts b/test/benchmark/detachedReadBenchmark.fixture.ts index 1ddeabd52..d5b5dcd53 100644 --- a/test/benchmark/detachedReadBenchmark.fixture.ts +++ b/test/benchmark/detachedReadBenchmark.fixture.ts @@ -61,6 +61,17 @@ function createMockPersistence() { updateRef: async (/** @type {string} */ ref, /** @type {string} */ sha) => { refs.set(ref, sha); }, + compareAndSwapRef: async ( + /** @type {string} */ ref, + /** @type {string} */ newOid, + /** @type {string | null} */ expectedOid, + ) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }, deleteRef: async (/** @type {string} */ ref) => { refs.delete(ref); }, diff --git a/test/helpers/WarpGraphMockPersistence.ts b/test/helpers/WarpGraphMockPersistence.ts index eaa287700..3b6b519f1 100644 --- a/test/helpers/WarpGraphMockPersistence.ts +++ b/test/helpers/WarpGraphMockPersistence.ts @@ -28,7 +28,8 @@ class MockPersistenceFixtureError extends Error { } class WarpGraphMockPersistence { - readonly readRef = vi.fn(); + readonly #refs = new Map(); + readonly readRef = vi.fn(async (ref: string) => this.#refs.get(ref) ?? null); readonly showNode = vi.fn(); readonly writeBlob = vi.fn(); readonly writeTree = vi.fn(); @@ -36,7 +37,9 @@ class WarpGraphMockPersistence { readonly readTreeOids = vi.fn().mockResolvedValue({}); readonly commitNode = vi.fn(); readonly commitNodeWithTree = vi.fn(); - readonly updateRef = vi.fn(); + readonly updateRef = vi.fn(async (ref: string, sha: string) => { + this.#refs.set(ref, sha); + }); readonly listRefs = vi.fn().mockResolvedValue([]); readonly getNodeInfo = vi.fn(); readonly ping = vi.fn().mockResolvedValue({ ok: true, latencyMs: 1 }); @@ -49,8 +52,17 @@ class WarpGraphMockPersistence { readonly countNodes = vi.fn().mockResolvedValue(0); readonly getCommitTree = vi.fn(); readonly readTree = vi.fn().mockResolvedValue({}); - readonly deleteRef = vi.fn(); - readonly compareAndSwapRef = vi.fn(); + readonly deleteRef = vi.fn(async (ref: string) => { + this.#refs.delete(ref); + }); + readonly compareAndSwapRef = vi.fn(async (ref: string, newOid: string, expectedOid: string | null) => { + const current = this.#refs.get(ref) ?? null; + if (current !== expectedOid) { + throw new MockPersistenceFixtureError(`CAS mismatch on ${ref}`); + } + this.#refs.set(ref, newOid); + this.readRef.mockImplementation(async (nextRef: string) => this.#refs.get(nextRef) ?? null); + }); get emptyTree(): string { return '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; diff --git a/test/unit/domain/WarpCore.snapshotHashStability.test.ts b/test/unit/domain/WarpCore.snapshotHashStability.test.ts index c34faae3a..0e6575ec7 100644 --- a/test/unit/domain/WarpCore.snapshotHashStability.test.ts +++ b/test/unit/domain/WarpCore.snapshotHashStability.test.ts @@ -50,6 +50,13 @@ function createMockPersistence() { updateRef: vi.fn(async (ref, sha) => { refs.set(ref, sha); }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }), deleteRef: vi.fn(async (ref) => { refs.delete(ref); }), diff --git a/test/unit/domain/WarpGraph.conflicts.test.ts b/test/unit/domain/WarpGraph.conflicts.test.ts index a8c0f54b0..a7d4a228a 100644 --- a/test/unit/domain/WarpGraph.conflicts.test.ts +++ b/test/unit/domain/WarpGraph.conflicts.test.ts @@ -44,6 +44,13 @@ function createMockPersistence() { updateRef: vi.fn(async (ref, sha) => { refs.set(ref, sha); }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }), configGet: vi.fn(async () => null), configSet: vi.fn(async () => {}), showNode: vi.fn(async (sha) => { diff --git a/test/unit/domain/WarpGraph.invalidation.test.ts b/test/unit/domain/WarpGraph.invalidation.test.ts index 2bdef033d..431b93fe7 100644 --- a/test/unit/domain/WarpGraph.invalidation.test.ts +++ b/test/unit/domain/WarpGraph.invalidation.test.ts @@ -159,15 +159,15 @@ describe('WarpCore dirty flag + eager re-materialize (AP/INVAL/1 + AP/INVAL/2)', expect((graph)._stateDirty).toBe(false); }); - it('_stateDirty remains false if updateRef fails', async () => { + it('_stateDirty remains false if compareAndSwapRef fails', async () => { persistence.readRef.mockResolvedValue(null); persistence.writeBlob.mockResolvedValue(FAKE_BLOB_OID); persistence.writeTree.mockResolvedValue(FAKE_TREE_OID); persistence.commitNodeWithTree.mockResolvedValue(FAKE_COMMIT_SHA); - persistence.updateRef.mockRejectedValue(new Error('ref lock failed')); + persistence.compareAndSwapRef.mockRejectedValue(new Error('ref lock failed')); const patch = (await graph.createPatch()).addNode('test:node'); - await expect(patch.commit()).rejects.toThrow('ref lock failed'); + await expect(patch.commit()).rejects.toThrow('Commit failed: writer ref was updated by another process'); expect((graph)._stateDirty).toBe(false); }); diff --git a/test/unit/domain/WarpGraph.observerBoundary.test.ts b/test/unit/domain/WarpGraph.observerBoundary.test.ts index 0881d8020..e1bb28694 100644 --- a/test/unit/domain/WarpGraph.observerBoundary.test.ts +++ b/test/unit/domain/WarpGraph.observerBoundary.test.ts @@ -51,6 +51,13 @@ function createMockPersistence() { updateRef: vi.fn(async (ref, sha) => { refs.set(ref, sha); }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }), deleteRef: vi.fn(async (ref) => { refs.delete(ref); }), diff --git a/test/unit/domain/WarpGraph.patchCount.test.ts b/test/unit/domain/WarpGraph.patchCount.test.ts index b10d0d778..d182e527f 100644 --- a/test/unit/domain/WarpGraph.patchCount.test.ts +++ b/test/unit/domain/WarpGraph.patchCount.test.ts @@ -112,15 +112,7 @@ describe('AP/CKPT/2: _patchesSinceCheckpoint tracking', () => { const tipSha = buildPatchChain(persistence, 'w1', patchCount); // checkpoint ref returns null (no checkpoint) - persistence.readRef.mockImplementation((/** @type {any} */ ref) => { - if (ref === 'refs/warp/test/checkpoints/head') { - return Promise.resolve(null); - } - if (ref === 'refs/warp/test/writers/w1') { - return Promise.resolve(tipSha); - } - return Promise.resolve(null); - }); + await persistence.updateRef('refs/warp/test/writers/w1', tipSha); // discoverWriters needs listRefs to return the writer ref persistence.listRefs.mockResolvedValue([ @@ -188,15 +180,7 @@ describe('AP/CKPT/2: _patchesSinceCheckpoint tracking', () => { const tipSha = buildPatchChain(persistence, 'w1', patchCount); // Phase 1: materialize with 3 patches (no checkpoint) - persistence.readRef.mockImplementation((/** @type {any} */ ref) => { - if (ref === 'refs/warp/test/checkpoints/head') { - return Promise.resolve(null); - } - if (ref === 'refs/warp/test/writers/w1') { - return Promise.resolve(tipSha); - } - return Promise.resolve(null); - }); + await persistence.updateRef('refs/warp/test/writers/w1', tipSha); persistence.listRefs.mockResolvedValue([ 'refs/warp/test/writers/w1', ]); diff --git a/test/unit/domain/WarpGraph.sameWriterRace.test.ts b/test/unit/domain/WarpGraph.sameWriterRace.test.ts new file mode 100644 index 000000000..62ecc8ee0 --- /dev/null +++ b/test/unit/domain/WarpGraph.sameWriterRace.test.ts @@ -0,0 +1,86 @@ +import { describe, expect, it } from 'vitest'; +import InMemoryGraphAdapter from '../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; +import { openRuntimeHostProduct } from '../../../src/domain/warp/RuntimeHostProduct.ts'; +import { buildWriterRef } from '../../../src/domain/utils/RefLayout.ts'; +import WriterError from '../../../src/domain/errors/WriterError.ts'; + +const GRAPH_NAME = 'same-writer-race'; +const WRITER_ID = 'writer-a'; +const FIRST_NODE = 'node:first'; +const SECOND_NODE = 'node:second'; + +function fulfilledCommitShas(results: readonly PromiseSettledResult[]): string[] { + const shas: string[] = []; + for (const result of results) { + if (result.status === 'fulfilled') { + shas.push(result.value); + } + } + return shas; +} + +function rejectedWriterErrors(results: readonly PromiseSettledResult[]): WriterError[] { + const errors: WriterError[] = []; + for (const result of results) { + if (result.status === 'rejected' && result.reason instanceof WriterError) { + errors.push(result.reason); + } + } + return errors; +} + +function winningNodeId(results: readonly PromiseSettledResult[]): string { + if (results[0]?.status === 'fulfilled') { + return FIRST_NODE; + } + return SECOND_NODE; +} + +function losingNodeId(results: readonly PromiseSettledResult[]): string { + if (results[0]?.status === 'fulfilled') { + return SECOND_NODE; + } + return FIRST_NODE; +} + +describe('same-writer concurrent patch race', () => { + it('leaves only the winning stale builder on the final frontier and visible state', async () => { + const persistence = new InMemoryGraphAdapter(); + const graph = await openRuntimeHostProduct({ + persistence, + graphName: GRAPH_NAME, + writerId: WRITER_ID, + autoMaterialize: true, + }); + + const firstPatch = await graph.createPatch(); + firstPatch.addNode(FIRST_NODE); + + const secondPatch = await graph.createPatch(); + secondPatch.addNode(SECOND_NODE); + + const results = await Promise.allSettled([ + firstPatch.commit(), + secondPatch.commit(), + ]); + const winners = fulfilledCommitShas(results); + const rejected = rejectedWriterErrors(results); + + expect(winners).toHaveLength(1); + expect(rejected).toHaveLength(1); + expect(rejected[0]?.code).toBe('WRITER_CAS_CONFLICT'); + + const writerRef = buildWriterRef(GRAPH_NAME, WRITER_ID); + expect(await persistence.readRef(writerRef)).toBe(winners[0]); + + await graph.materialize(); + const winnerNode = winningNodeId(results); + const loserNode = losingNodeId(results); + const firstVisible = await graph.hasNode(FIRST_NODE); + const secondVisible = await graph.hasNode(SECOND_NODE); + + expect([firstVisible, secondVisible].filter(Boolean)).toHaveLength(1); + expect(await graph.hasNode(winnerNode)).toBe(true); + expect(await graph.hasNode(loserNode)).toBe(false); + }); +}); diff --git a/test/unit/domain/WarpGraph.strands.test.ts b/test/unit/domain/WarpGraph.strands.test.ts index 2055e9bea..ae83aab2f 100644 --- a/test/unit/domain/WarpGraph.strands.test.ts +++ b/test/unit/domain/WarpGraph.strands.test.ts @@ -51,6 +51,13 @@ function createMockPersistence() { updateRef: vi.fn(async (ref, sha) => { refs.set(ref, sha); }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }), deleteRef: vi.fn(async (ref) => { refs.delete(ref); }), diff --git a/test/unit/domain/WarpGraph.test.ts b/test/unit/domain/WarpGraph.test.ts index 4a097253c..dfc341cb3 100644 --- a/test/unit/domain/WarpGraph.test.ts +++ b/test/unit/domain/WarpGraph.test.ts @@ -40,8 +40,10 @@ function installCleanCheckpointReadingBasis( * @returns {any} Mock persistence adapter */ function createMockPersistence(): any { + const refs = new Map(); + const readRef = vi.fn(async (ref: string) => refs.get(ref) || null); return { - readRef: vi.fn(), + readRef, showNode: vi.fn(), writeBlob: vi.fn(), writeTree: vi.fn(), @@ -49,7 +51,17 @@ function createMockPersistence(): any { readTreeOids: vi.fn(), commitNode: vi.fn(), commitNodeWithTree: vi.fn(), - updateRef: vi.fn(), + updateRef: vi.fn(async (ref: string, sha: string) => { + refs.set(ref, sha); + }), + compareAndSwapRef: vi.fn(async (ref: string, newOid: string, expectedOid: string | null) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + readRef.mockImplementation(async (nextRef: string) => refs.get(nextRef) || null); + }), listRefs: vi.fn().mockResolvedValue([]), getNodeInfo: vi.fn(), ping: vi.fn().mockResolvedValue({ ok: true, latencyMs: 1 }), @@ -328,20 +340,19 @@ describe('WarpCore', () => { } as any); // Set up mock responses for commit - persistence.readRef.mockResolvedValue(null); persistence.writeBlob.mockResolvedValue('a'.repeat(40)); persistence.writeTree.mockResolvedValue('a'.repeat(40)); persistence.commitNodeWithTree.mockResolvedValue('a'.repeat(40)); - persistence.updateRef.mockResolvedValue(undefined); const patchBuilder = await graph.createPatch(); patchBuilder.addNode('test'); await patchBuilder.commit(); - // Verify the ref was updated with correct graph/writer path - expect(persistence.updateRef).toHaveBeenCalledWith( + // Verify the writer ref was advanced through CAS with correct graph/writer path + expect(persistence.compareAndSwapRef).toHaveBeenCalledWith( 'refs/warp/my-events/writers/writer-42', - expect.any(String) + expect.any(String), + null, ); }); @@ -1917,11 +1928,7 @@ eg-schema: 2`; const existingSha = 'd'.repeat(40); const existingPatchOid = 'e'.repeat(40); - persistence.readRef.mockImplementation((/** @type {any} */ ref) => { - if (ref.includes('checkpoints')) return Promise.resolve(null); - if (ref.includes('writers')) return Promise.resolve(existingSha); - return Promise.resolve(null); - }); + await persistence.updateRef('refs/warp/events/writers/writer-1', existingSha); persistence.listRefs.mockResolvedValue([]); persistence.showNode.mockResolvedValue( `warp:patch\n\neg-kind: patch\neg-graph: events\neg-writer: writer-1\neg-lamport: 5\neg-patch-oid: ${existingPatchOid}\neg-schema: 2` diff --git a/test/unit/domain/WarpGraph.worldline.test.ts b/test/unit/domain/WarpGraph.worldline.test.ts index ebc2c0476..9cdea7b06 100644 --- a/test/unit/domain/WarpGraph.worldline.test.ts +++ b/test/unit/domain/WarpGraph.worldline.test.ts @@ -46,6 +46,13 @@ function createMockPersistence() { updateRef: vi.fn(async (ref, sha) => { refs.set(ref, sha); }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + }), deleteRef: vi.fn(async (ref) => { refs.delete(ref); }), diff --git a/test/unit/domain/WarpGraph.writerInvalidation.test.ts b/test/unit/domain/WarpGraph.writerInvalidation.test.ts index a85eab362..f339eef10 100644 --- a/test/unit/domain/WarpGraph.writerInvalidation.test.ts +++ b/test/unit/domain/WarpGraph.writerInvalidation.test.ts @@ -200,10 +200,12 @@ describe('WarpCore Writer invalidation (AP/INVAL/3)', () => { persistence.writeBlob.mockResolvedValue(FAKE_BLOB_OID); persistence.writeTree.mockResolvedValue(FAKE_TREE_OID); persistence.commitNodeWithTree.mockResolvedValue(FAKE_COMMIT_SHA); - persistence.updateRef.mockRejectedValue(new Error('ref lock failed')); + persistence.compareAndSwapRef.mockRejectedValue(new Error('ref lock failed')); const writer = await graph.writer('writer-1'); - await expect(writer.commitPatch((/** @type {any} */ p) => p.addNode('test:node'))).rejects.toThrow('ref lock failed'); + await expect(writer.commitPatch((/** @type {any} */ p) => p.addNode('test:node'))).rejects.toThrow( + 'Writer ref refs/warp/test/writers/writer-1 has advanced since beginPatch()', + ); expect((graph)._stateDirty).toBe(false); expect((graph)._cachedState).toBe(stateBeforeAttempt); diff --git a/test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts b/test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts new file mode 100644 index 000000000..bd1e2c953 --- /dev/null +++ b/test/unit/domain/continuum/ContinuumEvidenceStatus.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from 'vitest'; +import ContinuumEvidencePosture from '../../../../src/domain/continuum/ContinuumEvidencePosture.ts'; +import ContinuumEvidenceStatus from '../../../../src/domain/continuum/ContinuumEvidenceStatus.ts'; + +const PATCH_BASIS_REF = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +const CONTINUUM_WITNESS_REF = 'continuum:witness:receipt-family:1'; + +describe('ContinuumEvidenceStatus', () => { + it('marks git-warp evidence as participant runtime evidence', () => { + const status = ContinuumEvidenceStatus.gitWarpParticipant({ + basisRef: PATCH_BASIS_REF, + summary: 'git-warp patch receipt projected into receipt-family shape', + }); + + expect(status.posture.toString()).toBe('participant-runtime'); + expect(status.sourceRuntime).toBe('git-warp'); + expect(status.basisRef).toBe(PATCH_BASIS_REF); + expect(status.continuumWitnessRef).toBeUndefined(); + expect(status.isParticipantRuntime()).toBe(true); + expect(status.isContinuumWitnessed()).toBe(false); + expect(Object.isFrozen(status)).toBe(true); + }); + + it('accepts Continuum-witnessed evidence only with an explicit witness reference', () => { + const status = new ContinuumEvidenceStatus({ + posture: 'continuum-witnessed', + sourceRuntime: 'git-warp', + basisRef: PATCH_BASIS_REF, + continuumWitnessRef: CONTINUUM_WITNESS_REF, + summary: 'receipt-family value carries an explicit Continuum witness reference', + }); + + expect(status.posture.toString()).toBe('continuum-witnessed'); + expect(status.continuumWitnessRef).toBe(CONTINUUM_WITNESS_REF); + expect(status.isContinuumWitnessed()).toBe(true); + expect(status.isParticipantRuntime()).toBe(false); + }); + + it('rejects Continuum-witnessed evidence without a witness reference', () => { + expect(() => new ContinuumEvidenceStatus({ + posture: 'continuum-witnessed', + sourceRuntime: 'git-warp', + basisRef: PATCH_BASIS_REF, + summary: 'missing witness', + })).toThrow('continuumWitnessRef'); + }); + + it('rejects participant runtime evidence that carries a Continuum witness reference', () => { + expect(() => new ContinuumEvidenceStatus({ + posture: 'participant-runtime', + sourceRuntime: 'git-warp', + basisRef: PATCH_BASIS_REF, + continuumWitnessRef: CONTINUUM_WITNESS_REF, + summary: 'participant runtime evidence cannot claim a separate witness reference', + })).toThrow('participant runtime evidence must not carry continuumWitnessRef'); + }); +}); + +describe('ContinuumEvidencePosture', () => { + it('rejects unknown posture values', () => { + expect(() => new ContinuumEvidencePosture('fixture-only')).toThrow('Continuum evidence posture'); + }); +}); diff --git a/test/unit/domain/continuum/ContinuumReceiptProjection.test.ts b/test/unit/domain/continuum/ContinuumReceiptProjection.test.ts new file mode 100644 index 000000000..ad16fdad9 --- /dev/null +++ b/test/unit/domain/continuum/ContinuumReceiptProjection.test.ts @@ -0,0 +1,104 @@ +import { describe, expect, it } from 'vitest'; +import ContinuumArtifactDescriptor from '../../../../src/domain/continuum/ContinuumArtifactDescriptor.ts'; +import ContinuumEvidenceStatus from '../../../../src/domain/continuum/ContinuumEvidenceStatus.ts'; +import ContinuumFamilyId from '../../../../src/domain/continuum/ContinuumFamilyId.ts'; +import ContinuumReceipt from '../../../../src/domain/continuum/ContinuumReceipt.ts'; +import ContinuumReceiptProjector from '../../../../src/domain/continuum/ContinuumReceiptProjector.ts'; +import { createTickReceipt, type TickReceipt } from '../../../../src/domain/types/TickReceipt.ts'; + +const PATCH_SHA = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; +const SECOND_PATCH_SHA = 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'; +const WRITER_ID = 'writer-a'; + +function createReceiptDescriptor(): ContinuumArtifactDescriptor { + return new ContinuumArtifactDescriptor({ + familyId: 'receipt-family', + sourceSchemaPath: 'schemas/continuum-receipt-family.graphql', + generatedBy: 'wesley witness-continuum --scope receipt-family', + artifactKind: 'continuum.family.fixture', + authority: 'generated-fixture', + targets: ['warp-ttd', 'typescript'], + }); +} + +function createSettlementDescriptor(): ContinuumArtifactDescriptor { + return new ContinuumArtifactDescriptor({ + familyId: 'settlement-family', + sourceSchemaPath: 'schemas/continuum-settlement-family.graphql', + generatedBy: 'wesley witness-continuum --scope settlement-family', + artifactKind: 'continuum.family.fixture', + authority: 'generated-fixture', + targets: ['warp-ttd', 'typescript'], + }); +} + +function createReceipt(patchSha = PATCH_SHA, lamport = 7): TickReceipt { + return createTickReceipt({ + patchSha, + writer: WRITER_ID, + lamport, + ops: [ + { op: 'NodeAdd', target: 'node:a', result: 'applied' }, + { op: 'NodePropSet', target: 'node:a', result: 'redundant' }, + { op: 'EdgeAdd', target: 'node:a\0node:b\0rel', result: 'superseded' }, + ], + }); +} + +function participantEvidence(): ContinuumEvidenceStatus { + return ContinuumEvidenceStatus.gitWarpParticipant({ + basisRef: PATCH_SHA, + summary: 'git-warp tick receipt projected into receipt-family shape', + }); +} + +describe('ContinuumReceiptProjector', () => { + it('maps TickReceipt into the Continuum receipt-family Receipt shape', () => { + const receipt = new ContinuumReceiptProjector().projectTickReceipt(createReceipt()); + + expect(receipt).toBeInstanceOf(ContinuumReceipt); + expect(receipt.receiptId).toBe(`git-warp:receipt:${PATCH_SHA}`); + expect(receipt.headId).toBe(PATCH_SHA); + expect(receipt.frameIndex).toBe(7); + expect(receipt.laneId).toBe(WRITER_ID); + expect(receipt.writerId).toBe(WRITER_ID); + expect(receipt.inputTick).toBe(6); + expect(receipt.outputTick).toBe(7); + expect(receipt.admittedRewriteCount).toBe(2); + expect(receipt.rejectedRewriteCount).toBe(1); + expect(receipt.counterfactualCount).toBe(0); + expect(receipt.digest).toBe(PATCH_SHA); + expect(receipt.summary).toContain('2 admitted'); + expect(Object.isFrozen(receipt)).toBe(true); + }); + + it('wraps projected receipts with generated artifact authority and evidence status', () => { + const artifact = createReceiptDescriptor(); + const evidence = participantEvidence(); + const projection = new ContinuumReceiptProjector().projectTickReceipts({ + artifact, + evidence, + tickReceipts: [ + createReceipt(PATCH_SHA, 7), + createReceipt(SECOND_PATCH_SHA, 8), + ], + }); + + expect(projection.artifact).toBe(artifact); + expect(projection.evidence).toBe(evidence); + expect(projection.artifact.familyId.equals(new ContinuumFamilyId('receipt-family'))).toBe(true); + expect(projection.evidence.isParticipantRuntime()).toBe(true); + expect(projection.receipts).toHaveLength(2); + expect(projection.receiptsForHead(PATCH_SHA)).toHaveLength(1); + expect(projection.receiptsForHead(SECOND_PATCH_SHA, 8)).toHaveLength(1); + expect(projection.receiptsForHead(SECOND_PATCH_SHA, 7)).toHaveLength(0); + }); + + it('rejects non-receipt-family artifacts', () => { + expect(() => new ContinuumReceiptProjector().projectTickReceipts({ + artifact: createSettlementDescriptor(), + evidence: participantEvidence(), + tickReceipts: [createReceipt()], + })).toThrow('receipt-family'); + }); +}); diff --git a/test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts b/test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts new file mode 100644 index 000000000..25103caba --- /dev/null +++ b/test/unit/domain/continuum/WarpTtdReceiptFamilySmoke.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from 'vitest'; +import { fileURLToPath } from 'node:url'; + +import InMemoryGraphAdapter from '../../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; +import ContinuumArtifactJsonFileAdapter, { + type ContinuumArtifactJsonLoadContext, +} from '../../../../src/infrastructure/adapters/ContinuumArtifactJsonFileAdapter.ts'; +import ContinuumEvidenceStatus from '../../../../src/domain/continuum/ContinuumEvidenceStatus.ts'; +import ContinuumReceiptProjector from '../../../../src/domain/continuum/ContinuumReceiptProjector.ts'; +import { openRuntimeHostProduct } from '../../../../src/domain/warp/RuntimeHostProduct.ts'; + +const GRAPH_NAME = 'warp-ttd-receipt-smoke'; +const WRITER_ID = 'writer-a'; +const NODE_ID = 'node:warp-ttd'; + +const generatedFixturePath = fileURLToPath( + new URL('../../../fixtures/continuum/receipt-family-generated-artifact.json', import.meta.url), +); + +const generatedFixtureContext: ContinuumArtifactJsonLoadContext = { + familyId: 'receipt-family', + authority: 'generated-fixture', + sourceSchemaPath: 'schemas/continuum-receipt-family.graphql', + generatedBy: 'wesley witness-continuum --scope receipt-family', + witnessScope: 'receipt-family', + targets: ['warp-ttd', 'typescript'], +}; + +describe('warp-ttd receipt-family smoke', () => { + it('reads live git-warp receipts as generated-family facts with participant evidence', async () => { + const persistence = new InMemoryGraphAdapter(); + const graph = await openRuntimeHostProduct({ + persistence, + graphName: GRAPH_NAME, + writerId: WRITER_ID, + autoMaterialize: true, + }); + const artifact = await new ContinuumArtifactJsonFileAdapter().loadFile( + generatedFixturePath, + generatedFixtureContext, + ); + expect(artifact.hasTarget('warp-ttd')).toBe(true); + + const patchSha = await graph.patch((patch) => { + patch.addNode(NODE_ID).setProperty(NODE_ID, 'role', 'debug-target'); + }); + const materialized = await graph.materialize({ receipts: true }); + const evidence = ContinuumEvidenceStatus.gitWarpParticipant({ + basisRef: patchSha, + summary: 'live git-warp receipt exposed as generated receipt-family facts for warp-ttd', + }); + const projection = new ContinuumReceiptProjector().projectTickReceipts({ + artifact, + evidence, + tickReceipts: materialized.receipts, + }); + + const receiptFacts = projection.receiptsForHead(patchSha, 1); + + expect(projection.evidence.isParticipantRuntime()).toBe(true); + expect(projection.evidence.isContinuumWitnessed()).toBe(false); + expect(receiptFacts).toHaveLength(1); + expect(receiptFacts[0]?.headId).toBe(patchSha); + expect(receiptFacts[0]?.laneId).toBe(WRITER_ID); + expect(receiptFacts[0]?.writerId).toBe(WRITER_ID); + expect(receiptFacts[0]?.frameIndex).toBe(1); + expect(receiptFacts[0]?.outputTick).toBe(1); + expect(receiptFacts[0]?.admittedRewriteCount).toBeGreaterThan(0); + expect(receiptFacts[0]?.digest).toBe(patchSha); + }); +}); diff --git a/test/unit/domain/index.exports.test.ts b/test/unit/domain/index.exports.test.ts index c96d5be59..05e709a85 100644 --- a/test/unit/domain/index.exports.test.ts +++ b/test/unit/domain/index.exports.test.ts @@ -61,7 +61,12 @@ import WarpAppDefault, { ContinuumArtifactAuthority, ContinuumArtifactDescriptor, ContinuumArtifactIngestionPolicy, + ContinuumEvidencePosture, + ContinuumEvidenceStatus, ContinuumFamilyId, + ContinuumReceipt, + ContinuumReceiptFamilyProjection, + ContinuumReceiptProjector, ContinuumArtifactJsonFileAdapter, } from '../../../index.ts'; @@ -262,7 +267,12 @@ describe('index.ts exports', () => { expect(ContinuumArtifactAuthority).toBeDefined(); expect(ContinuumArtifactDescriptor).toBeDefined(); expect(ContinuumArtifactIngestionPolicy).toBeDefined(); + expect(ContinuumEvidencePosture).toBeDefined(); + expect(ContinuumEvidenceStatus).toBeDefined(); expect(ContinuumFamilyId).toBeDefined(); + expect(ContinuumReceipt).toBeDefined(); + expect(ContinuumReceiptFamilyProjection).toBeDefined(); + expect(ContinuumReceiptProjector).toBeDefined(); expect(ContinuumArtifactJsonFileAdapter).toBeDefined(); }); @@ -280,6 +290,16 @@ describe('index.ts exports', () => { expect(descriptor.familyId).toBeInstanceOf(ContinuumFamilyId); expect(descriptor.hasGeneratedAuthority()).toBe(true); }); + + it('constructs git-warp participant evidence status from public exports', () => { + const status = ContinuumEvidenceStatus.gitWarpParticipant({ + basisRef: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + summary: 'git-warp participant evidence projected into Continuum shape', + }); + + expect(status.posture).toBeInstanceOf(ContinuumEvidencePosture); + expect(status.isParticipantRuntime()).toBe(true); + }); }); describe('cancellation utilities', () => { diff --git a/test/unit/domain/services/PatchBuilder.cas.test.ts b/test/unit/domain/services/PatchBuilder.cas.test.ts index 34ab27044..6705a5944 100644 --- a/test/unit/domain/services/PatchBuilder.cas.test.ts +++ b/test/unit/domain/services/PatchBuilder.cas.test.ts @@ -19,6 +19,7 @@ function createMockPersistence(overrides = {}): any { writeTree: vi.fn().mockResolvedValue('b'.repeat(40)), commitNodeWithTree: vi.fn().mockResolvedValue('c'.repeat(40)), updateRef: vi.fn().mockResolvedValue(undefined), + compareAndSwapRef: vi.fn().mockResolvedValue(undefined), ...overrides, }; } @@ -200,8 +201,11 @@ describe('PatchBuilder CAS conflict detection', () => { // --------------------------------------------------------------- describe('when no CAS conflict occurs', () => { it('succeeds when expectedParentSha matches current ref (both null)', async () => { + const commitSha = 'c'.repeat(40); const persistence = createMockPersistence({ - readRef: vi.fn().mockResolvedValue(null), + readRef: vi.fn() + .mockResolvedValueOnce(null) + .mockResolvedValueOnce(commitSha), }); const builder = new PatchBuilder({ @@ -218,17 +222,21 @@ describe('PatchBuilder CAS conflict detection', () => { builder.addNode('x'); const sha = await builder.commit(); - expect(sha).toBe('c'.repeat(40)); + expect(sha).toBe(commitSha); expect(persistence.commitNodeWithTree).toHaveBeenCalledOnce(); - expect(persistence.updateRef).toHaveBeenCalledOnce(); + expect(persistence.compareAndSwapRef).toHaveBeenCalledOnce(); + expect(persistence.updateRef).not.toHaveBeenCalled(); }); it('succeeds when expectedParentSha matches current ref (both same SHA)', async () => { const parentSha = 'd'.repeat(40); const patchOid = 'e'.repeat(40); + const commitSha = 'c'.repeat(40); const persistence = createMockPersistence({ - readRef: vi.fn().mockResolvedValue(parentSha), + readRef: vi.fn() + .mockResolvedValueOnce(parentSha) + .mockResolvedValueOnce(commitSha), showNode: vi.fn().mockResolvedValue( `warp:patch\n\neg-kind: patch\neg-graph: test-graph\neg-writer: writer1\neg-lamport: 3\neg-patch-oid: ${patchOid}\neg-schema: 2` ), @@ -248,8 +256,9 @@ describe('PatchBuilder CAS conflict detection', () => { builder.addNode('x'); const sha = await builder.commit(); - expect(sha).toBe('c'.repeat(40)); + expect(sha).toBe(commitSha); expect(persistence.commitNodeWithTree).toHaveBeenCalledOnce(); + expect(persistence.compareAndSwapRef).toHaveBeenCalledOnce(); }); }); }); diff --git a/test/unit/domain/services/PatchBuilder.content.test.ts b/test/unit/domain/services/PatchBuilder.content.test.ts index 3185577c5..df80e4efc 100644 --- a/test/unit/domain/services/PatchBuilder.content.test.ts +++ b/test/unit/domain/services/PatchBuilder.content.test.ts @@ -28,13 +28,25 @@ function createMockBlobStorage(opts: { storeOid?: string } = {}) { * @returns {any} */ function createMockPersistence(overrides = {}) { + const refs = new Map(); + const readRef = vi.fn(async (ref) => refs.get(ref) || null); return { - readRef: vi.fn().mockResolvedValue(null), + readRef, showNode: vi.fn(), writeBlob: vi.fn().mockResolvedValue('d'.repeat(40)), writeTree: vi.fn().mockResolvedValue('e'.repeat(40)), commitNodeWithTree: vi.fn().mockResolvedValue('f'.repeat(40)), - updateRef: vi.fn().mockResolvedValue(undefined), + updateRef: vi.fn(async (ref, sha) => { + refs.set(ref, sha); + }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + readRef.mockImplementation(async (nextRef) => refs.get(nextRef) || null); + }), ...overrides, }; } diff --git a/test/unit/domain/services/PatchBuilder.test.ts b/test/unit/domain/services/PatchBuilder.test.ts index c1530cbe5..a305874e5 100644 --- a/test/unit/domain/services/PatchBuilder.test.ts +++ b/test/unit/domain/services/PatchBuilder.test.ts @@ -28,13 +28,25 @@ function createMockState() { * @returns {any} Mock persistence with standard methods stubbed */ function createMockPersistence() { + const refs = new Map(); + const readRef = vi.fn(async (ref) => refs.get(ref) || null); return { - readRef: vi.fn().mockResolvedValue(null), + readRef, showNode: vi.fn(), writeBlob: vi.fn().mockResolvedValue('a'.repeat(40)), // Valid 40-char hex OID writeTree: vi.fn().mockResolvedValue('b'.repeat(40)), commitNodeWithTree: vi.fn().mockResolvedValue('c'.repeat(40)), - updateRef: vi.fn().mockResolvedValue(undefined), + updateRef: vi.fn(async (ref, sha) => { + refs.set(ref, sha); + }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + readRef.mockImplementation(async (nextRef) => refs.get(nextRef) || null); + }), }; } @@ -589,9 +601,10 @@ describe('PatchBuilder', () => { expect(persistence.writeBlob).toHaveBeenCalledOnce(); expect(persistence.writeTree).toHaveBeenCalledOnce(); expect(persistence.commitNodeWithTree).toHaveBeenCalledOnce(); - expect(persistence.updateRef).toHaveBeenCalledWith( + expect(persistence.compareAndSwapRef).toHaveBeenCalledWith( 'refs/warp/test-graph/writers/writer1', - 'c'.repeat(40) + 'c'.repeat(40), + null, ); }); @@ -639,7 +652,7 @@ describe('PatchBuilder', () => { const existingSha = 'd'.repeat(40); const existingPatchOid = 'e'.repeat(40); // Simulate existing ref with lamport 5 - persistence.readRef.mockResolvedValue(existingSha); + await persistence.updateRef('refs/warp/test-graph/writers/writer1', existingSha); persistence.showNode.mockResolvedValue( `warp:patch\n\neg-kind: patch\neg-graph: test-graph\neg-writer: writer1\neg-lamport: 5\neg-patch-oid: ${existingPatchOid}\neg-schema: 2` ); @@ -866,7 +879,7 @@ describe('PatchBuilder', () => { it('does NOT set _committed on failed commit (mock persistence to throw)', async () => { const persistence = createMockPersistence(); - persistence.updateRef.mockRejectedValueOnce(new Error('simulated updateRef failure')); + persistence.compareAndSwapRef.mockRejectedValueOnce(new Error('simulated compareAndSwapRef failure')); const builder = new PatchBuilder(({ persistence, patchJournal: createPatchJournal(persistence), @@ -878,7 +891,7 @@ describe('PatchBuilder', () => { } as any)); builder.addNode('x'); - await expect(builder.commit()).rejects.toThrow('simulated updateRef failure'); + await expect(builder.commit()).rejects.toThrow('Commit failed: writer ref was updated by another process'); expect((builder as any)._committed).toBe(false); expect((builder as any)._committing).toBe(false); }); diff --git a/test/unit/domain/services/PatchCommitter.visibility.test.ts b/test/unit/domain/services/PatchCommitter.visibility.test.ts new file mode 100644 index 000000000..7f56d1b39 --- /dev/null +++ b/test/unit/domain/services/PatchCommitter.visibility.test.ts @@ -0,0 +1,128 @@ +import { describe, expect, it } from 'vitest'; +import InMemoryGraphAdapter from '../../../../src/infrastructure/adapters/InMemoryGraphAdapter.ts'; +import { CborPatchJournalAdapter } from '../../../../src/infrastructure/adapters/CborPatchJournalAdapter.ts'; +import { CborCodec } from '../../../../src/infrastructure/codecs/CborCodec.ts'; +import VersionVector from '../../../../src/domain/crdt/VersionVector.ts'; +import { PatchBuilder } from '../../../../src/domain/services/PatchBuilder.ts'; +import { buildWriterRef } from '../../../../src/domain/utils/RefLayout.ts'; +import WriterError from '../../../../src/domain/errors/WriterError.ts'; +import { openRuntimeHostProduct } from '../../../../src/domain/warp/RuntimeHostProduct.ts'; + +const GRAPH_NAME = 'visibility'; +const WRITER_ID = 'writer-a'; +const DRIFT_SHA = 'd'.repeat(40); + +type CasUpdate = { + readonly ref: string; + readonly newOid: string; + readonly expectedOid: string | null; +}; + +class RecordingGraphAdapter extends InMemoryGraphAdapter { + readonly casUpdates: CasUpdate[] = []; + + override async compareAndSwapRef( + ref: string, + newOid: string, + expectedOid: string | null, + ): Promise { + this.casUpdates.push(Object.freeze({ ref, newOid, expectedOid })); + await super.compareAndSwapRef(ref, newOid, expectedOid); + } +} + +class DriftAfterCasGraphAdapter extends RecordingGraphAdapter { + override async compareAndSwapRef( + ref: string, + newOid: string, + expectedOid: string | null, + ): Promise { + await super.compareAndSwapRef(ref, newOid, expectedOid); + await super.updateRef(ref, DRIFT_SHA); + } +} + +function createPatchJournal(persistence: InMemoryGraphAdapter): CborPatchJournalAdapter { + return new CborPatchJournalAdapter({ + codec: new CborCodec(), + blobPort: persistence, + }); +} + +function createBuilder(persistence: InMemoryGraphAdapter): PatchBuilder { + return new PatchBuilder({ + persistence, + patchJournal: createPatchJournal(persistence), + graphName: GRAPH_NAME, + writerId: WRITER_ID, + lamport: 1, + versionVector: VersionVector.empty(), + getCurrentState: () => null, + expectedParentSha: null, + }); +} + +describe('PatchCommitter visibility contract', () => { + it('advances the writer ref with compare-and-swap before reporting success', async () => { + const persistence = new RecordingGraphAdapter(); + const writerRef = buildWriterRef(GRAPH_NAME, WRITER_ID); + const builder = createBuilder(persistence); + + builder.addNode('node:visible'); + const sha = await builder.commit(); + + expect(persistence.casUpdates).toEqual([{ + ref: writerRef, + newOid: sha, + expectedOid: null, + }]); + expect(await persistence.readRef(writerRef)).toBe(sha); + }); + + it('rejects success when the post-CAS writer ref does not name the returned commit', async () => { + const persistence = new DriftAfterCasGraphAdapter(); + const builder = createBuilder(persistence); + + builder.addNode('node:drift'); + + await expect(builder.commit()).rejects.toMatchObject({ + code: 'WRITER_COMMIT_NOT_VISIBLE', + }); + }); + + it('preserves post-CAS visibility errors through Writer patch sessions', async () => { + const persistence = new DriftAfterCasGraphAdapter(); + const graph = await openRuntimeHostProduct({ + persistence, + graphName: GRAPH_NAME, + writerId: WRITER_ID, + autoMaterialize: true, + }); + const writer = await graph.writer(WRITER_ID); + + await expect(writer.commitPatch((patch) => { + patch.addNode('node:writer-drift'); + })).rejects.toMatchObject({ + code: 'WRITER_COMMIT_NOT_VISIBLE', + }); + }); + + it('makes the returned patch commit visible through graph materialization', async () => { + const persistence = new RecordingGraphAdapter(); + const graph = await openRuntimeHostProduct({ + persistence, + graphName: GRAPH_NAME, + writerId: WRITER_ID, + autoMaterialize: true, + }); + const writerRef = buildWriterRef(GRAPH_NAME, WRITER_ID); + + const sha = await graph.patch((patch) => { + patch.addNode('node:materialized'); + }); + + expect(await persistence.readRef(writerRef)).toBe(sha); + await graph.materialize(); + expect(await graph.hasNode('node:materialized')).toBe(true); + }); +}); diff --git a/test/unit/domain/warp/Writer.test.ts b/test/unit/domain/warp/Writer.test.ts index aa09b7652..acac42840 100644 --- a/test/unit/domain/warp/Writer.test.ts +++ b/test/unit/domain/warp/Writer.test.ts @@ -21,9 +21,21 @@ import { CborCodec } from '../../../../src/infrastructure/codecs/CborCodec.ts'; * Creates a minimal mock persistence adapter. */ function createMockPersistence() { + const refs = new Map(); + const readRef = vi.fn(async (ref) => refs.get(ref) || null); return { - readRef: vi.fn(), - updateRef: vi.fn(), + readRef, + updateRef: vi.fn(async (ref, sha) => { + refs.set(ref, sha); + }), + compareAndSwapRef: vi.fn(async (ref, newOid, expectedOid) => { + const current = refs.get(ref) || null; + if (current !== expectedOid) { + throw new Error(`CAS mismatch on ${ref}`); + } + refs.set(ref, newOid); + readRef.mockImplementation(async (nextRef) => refs.get(nextRef) || null); + }), showNode: vi.fn(), getNodeInfo: vi.fn(), writeBlob: vi.fn(), @@ -276,7 +288,7 @@ describe('Writer (WARP schema:2)', () => { const oldHead = 'a'.repeat(40); const newSha = 'b'.repeat(40); - persistence.readRef.mockResolvedValue(oldHead); + await persistence.updateRef(buildWriterRef('events', 'alice'), oldHead); persistence.showNode.mockResolvedValue(createPatchMessage(5)); persistence.writeBlob.mockResolvedValue('c'.repeat(40)); persistence.writeTree.mockResolvedValue('d'.repeat(40)); @@ -357,9 +369,10 @@ describe('Writer (WARP schema:2)', () => { patch.addNode('x'); await patch.commit(); - expect(persistence.updateRef).toHaveBeenCalledWith( + expect(persistence.compareAndSwapRef).toHaveBeenCalledWith( 'refs/warp/events/writers/alice', - newSha + newSha, + null, ); }); @@ -445,20 +458,9 @@ describe('Writer (WARP schema:2)', () => { const newSha2 = 'c'.repeat(40); // Setup: both patches see same head at begin time + await persistence.updateRef(buildWriterRef('events', 'alice'), oldHead); persistence.showNode.mockResolvedValue(createPatchMessage(5)); - // Sequence of readRef calls: - // 1. p1 beginPatch -> oldHead - // 2. p2 beginPatch -> oldHead - // 3. p1 commit PatchBuilder CAS check -> oldHead - // 4. (updateRef happens, ref is now newSha1) - // 5. p2 commit PatchBuilder CAS check -> newSha1 (fails here) - persistence.readRef - .mockResolvedValueOnce(oldHead) // p1 beginPatch - .mockResolvedValueOnce(oldHead) // p2 beginPatch - .mockResolvedValueOnce(oldHead) // p1 commit PatchBuilder - .mockResolvedValueOnce(newSha1); // p2 commit PatchBuilder (fails) - persistence.writeBlob.mockResolvedValue('d'.repeat(40)); persistence.writeTree.mockResolvedValue('e'.repeat(40)); persistence.commitNodeWithTree