feat(stories): delivery-record checks — the record against itself, and against GitHub - #308
Open
ophiocus wants to merge 1 commit into
Open
feat(stories): delivery-record checks — the record against itself, and against GitHub#308ophiocus wants to merge 1 commit into
ophiocus wants to merge 1 commit into
Conversation
This was referenced Sep 7, 2026
ophiocus
force-pushed
the
feat/delivery-record-checks
branch
2 times, most recently
from
September 7, 2026 14:26
db3e15b to
f0c9593
Compare
…d against GitHub
Read-only checks over what 0.12 already writes, returning findings; nothing
here gates, and nothing here is surfaced yet.
- completeness: every settled turn (failure and cancellation paths included)
carries a completed capture or an explicit captureError.
- coherence: the turn_git_evidence row agrees with its two story-evidence
events — turn:{id}:context at the start (initialSha, initialBranch,
workspaceId, engineSessionId) and turn:{id}:git at settlement
(initialSha, finalSha, commit sequence, dirty) — and each end is paired
with the right event type: git.changes_recorded for a completed capture,
git.capture_failed for an explicit failure.
- attribution: the head SHAs GitHub reported (branches, pull heads, CI
heads, via the signed webhook or the mirror scan) form a witness. A turn
whose final SHA GitHub has seen is witnessed; when the two internal
tellings disagree on the final SHA and GitHub has seen exactly one, the
finding names the drifted side. Neither or both seen: it stays a
coherence finding. Unwitnessed is not a finding.
Every check enumerates from a ledger other than the one it verifies, so a
deleted row is visible from the ledger that still expects it. Keyset
paginated over turns; every companion query, the GitHub lookup included,
is an IN over the page.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
ophiocus
force-pushed
the
feat/delivery-record-checks
branch
from
September 7, 2026 14:43
f0c9593 to
5d38906
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR — Read-only checks that the delivery record agrees with itself (the
turn_git_evidencerow against its two story-evidence events, at the start and at settlement) and with GitHub (the head SHAs the mirror stored as a witness). One module that returns findings; it surfaces nothing yet, gates nothing, and needs no migration. 16 tests, each "break the promise → the report names it". Where findings should show up is a product choice, proposed separately so it can be discussed on its own.Follow-on to #264, taking @javiertoledo's "up to you" as the brief. #289 retired the receipt machinery and, with it, the only verifier the tree had — deliberately, and I think rightly: the mediation layer was where the hard defects lived (#195, #233, #226 were all bugs in the machinery of distrust, not in the work it guarded). What 0.12 kept instead is a delivery record that Insights, the story timeline, and turn attribution all stand on — and nothing can notice when that record stops agreeing with itself. This PR adds the noticing, as a read, using only what 0.12 already writes.
Credit where it starts: @Julian-Genuario's #226, @javiertoledo's re-verification and diagnosis on #264, and @adrian-lorenzo's #289, whose double-write is the reason this needs no digest of its own.
The class, in one sentence
A verifier that enumerates from the ledger it is verifying can only confirm what survived; absence has no row to iterate. The old fix was a reverse pass in one file. That file is gone, but the new tree writes each turn's facts in more than one place from more than one code path, so the reverse pass becomes cross-ledger reads — every check enumerates from a ledger other than the one it verifies, and reports over that denominator:
turns(settled)turn_git_evidenceturn:{id}:contextandturn:{id}:gitstory-evidence eventsWhat 0.12 already writes (nothing new is written to detect)
TurnGitEvidenceService.start()writes the row's initial branch/SHA and aturn:{id}:contextevent with the same starting point;complete()writes the final SHA, commits, changed files and dirty state intoturn_git_evidenceand aturn:{id}:gitevent with the same facts — or, on the failure path, acaptureErroron the row and agit.capture_failedevent under the same key. Two ledgers, one writer; the comparer was the only thing missing.finalSha == headShato link a pull request to the exact turn that produced it. Read the other way, those heads are a witness: content-addressed, and not ours to rewrite.The checks — each one a promise 0.12 makes, made checkable
Completeness — "Final SHA, commits, changed files, and dirty state are captured when it settles, including failure and cancellation paths." Every settled turn carries either a completed capture or an explicit
captureError. A settled turn with neither ismissing-evidenceorunsettled-capture.Coherence — the row and its events agree. At the start: the context event exists (
missing-context) and matches the row oninitialSha,initialBranch,workspaceId,engineSessionId. At settlement: a completed capture is paired withgit.changes_recordedand agrees oninitialSha,finalSha, the commit sequence anddirty; a capture error is paired withgit.capture_failed. A missing pair ismissing-event; a wrong or disagreeing one isdivergent. Insights and the timeline read from this substrate, so divergence means the surfaces are wrong, not merely inconsistent.Attribution — "GitHub facts are linked to an exact Facility turn when their head SHA matches that turn's recorded final SHA." A settled turn whose recorded final SHA GitHub has reported is witnessed (
witnessedTurnsmakes coverage visible). When the two internal tellings disagree about the final SHA and GitHub has seen exactly one of them, the finding becomeswitness-disagreesand names the side that drifted. Both seen, or neither: it stays a coherence finding — the witness never guesses. An unwitnessed turn is not a finding; it simply hasn't travelled that far yet.Proof — every shape is "break the promise, watch the report"
services/api/test/delivery-record.integration.test.ts(16 tests, scoped fixture org):git.capture_failedeventcompleteness: missing-evidencefinalShaand erasecaptureErrorcompleteness: unsettled-captureturn:{id}:contexteventcoherence: missing-contextcoherence: divergent— "row and context event disagree on initialSha"git.capture_failedevent never landedcoherence: missing-eventcoherence: divergentturn:{id}:giteventcoherence: missing-eventfinalShacoherence: divergentattribution: witness-disagrees— "the evidence event drifted"coherence: divergentlimit: 4and the cursorwitnessedTurns, nothing double-countedSuite:
services/api24 files / 155 tests green (1 pre-existing opt-in skip);tscandbiomeclean.Bounded by construction
Read-only; keyset-paginated over
turnson the existing(orgId, projectId)scope; every companion query — evidence rows, events, and the GitHub witness lookup — is anINover the page. Memory does not grow with org history (#295 absorbed rather than inherited). Findings are counted againstcheckedTurns— all settled turns — never against survivors.Limits, stated rather than papered over
Non-goals — and what is deliberately not in this PR
No receipts, no approvals, no leases, no per-agent profiles, no gating of any operation, ever. No migration.
And no surface: this PR adds no attention items, changes no health, schedules no job. The checks are facts about the record; where those facts should appear — and whether a finding should ever change what a person sees on a story — is a product call I would rather discuss than assume. Those come as separate proposals.
Files
services/api/src/stories/delivery-record.ts—verifyDeliveryRecord(new)services/api/test/delivery-record.integration.test.tsstories/evidence.tsandturns/git-evidence.ts— the two writers — are untouched.Verify locally
🤖 Generated with Claude Code