Conversation
f0c9593 to
5d38906
Compare
adrian-lorenzo
left a comment
There was a problem hiding this comment.
Thanks for following this through on the new delivery record.
Please address three issues before merge: normal budget rejections are flagged as missing evidence even though capture never starts; an old GitHub SHA can make the checker blame the correct event when only the row was corrupted; and limit: 1 still loads every CI observation for a matching SHA—1,001 rows in my probe.
Please distinguish unstarted captures, avoid assigning blame from SHA presence alone, and bound the witness queries, with regression tests for each.
…ed witness, bounded reads Addresses the three review findings on theam#308. - Unstarted captures: a turn the dispatcher claimed and then failed or canceled before TurnGitEvidenceService.start() ran (budget rejection, credential issue, environment preparation, cancel while queued) has neither telling. It is counted as `unstartedCaptures` and is not a finding. A succeeded turn without both tellings, or a surviving context event without its row, is still `missing-evidence`. - Corroborated witness: a telling is corroborated only when GitHub reported its final SHA on the turn's own branch (taken from the context event), the SHA is not the turn's starting point unless the telling records no commits, and the telling's commit list ends at that SHA. A verdict needs exactly one side corroborated; presence of a SHA alone convicts nothing. - Bounded reads: witness observations are distinct (sha, branch) pairs server-side, CI joined to its pull for the head ref, exposed as `witnessObservations` so the bound is observable in a test. Tests: one existing fixture corrected — the "row drifted" case witnessed the event's SHA on an unrelated branch, the shape the review rejected; its assertion is unchanged. New suite delivery-record-review.integration.test.ts models each fixture on the writer that produces it (11 tests). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5d38906 to
fe5d305
Compare
|
Thanks @adrian-lorenzo. All three addressed in |
…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>
…ed witness, bounded reads Addresses the three review findings on theam#308. - Unstarted captures: a turn the dispatcher claimed and then failed or canceled before TurnGitEvidenceService.start() ran (budget rejection, credential issue, environment preparation, cancel while queued) has neither telling. It is counted as `unstartedCaptures` and is not a finding. A succeeded turn without both tellings, or a surviving context event without its row, is still `missing-evidence`. - Corroborated witness: a telling is corroborated only when GitHub reported its final SHA on the turn's own branch (taken from the context event), the SHA is not the turn's starting point unless the telling records no commits, and the telling's commit list ends at that SHA. A verdict needs exactly one side corroborated; presence of a SHA alone convicts nothing. - Bounded reads: witness observations are distinct (sha, branch) pairs server-side, CI joined to its pull for the head ref, exposed as `witnessObservations` so the bound is observable in a test. Tests: one existing fixture corrected — the "row drifted" case witnessed the event's SHA on an unrelated branch, the shape the review rejected; its assertion is unchanged. New suite delivery-record-review.integration.test.ts models each fixture on the writer that produces it (11 tests). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
fe5d305 to
0457431
Compare
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. 27 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— except one the dispatcher claimed and then failed or canceled beforeTurnGitEvidenceService.start()ran (budget rejection, credential issue, environment preparation, cancel while queued). Such a turn has neither telling by construction; it is counted asunstartedCapturesand is not a finding. A succeeded turn cannot have skippedstart(), and a context event that survives without its row means the row was destroyed, so both still flag.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." Presence of a SHA somewhere in GitHub's record is not enough — an old head is still a real SHA — so a telling is corroborated only when three things hold: GitHub reported its final SHA on the turn's own branch (taken from the context event, a telling the row did not write); the SHA is not the turn's starting point, unless the telling records no commits (an honest no-op); and the telling's own commit list ends at that SHA (the writer logs
--reverse). A turn whose row is corroborated is witnessed (witnessedTurnsmakes coverage visible). When the two internal tellings disagree about the final SHA and exactly one side is corroborated, the finding becomeswitness-disagreesand names the side that drifted. Both corroborated, 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-countedReview round — @adrian-lorenzo's three findings
services/api/test/delivery-record-review.integration.test.ts(11 tests). Each fixture is modelled on the writer that produces it — the dispatcher claims a turn (running,startedAtset) before the budget check and only reachesevidence.start()after credentials and environment preparation;complete()logs commits--reverse; the mirror stores branch heads by name, pull heads by head ref, and one CI observation per GitHub event.missing-evidenceunstartedCaptures, no findingstartedAtset) and a turn canceled while queued: 0 findings, counted 2; a failed turn whose context event survives but whose row is gone, and a succeeded turn with neither: stillmissing-evidencefinalSha: no conviction; a head seen on an unrelated branch: no conviction; the honest case on the branch: still convicts; an honest no-op turn: still witnessedlimit: 1loaded every CI observation for a matching SHAwitnessObservationsreturns distinct(sha, branch)pairs server-side, CI joined to its pull for the head ref; exported so the bound is observableOne fixture in the original suite changed: the "row drifted" case witnessed the event's SHA on an unrelated branch — the shape the review rejected. The witness now arrives as a pull opened from the story branch and the event's commits end at that SHA; the assertion is unchanged.
Both suites: 27/27, three consecutive runs;
tscandbiomeclean on the rebased branch.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, and the witness lookup returns distinct pairs, never one row per observation. 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
updatedAton every sync and CI observations can be backfilled.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,witnessObservations(new)services/api/test/delivery-record.integration.test.tsservices/api/test/delivery-record-review.integration.test.tsstories/evidence.tsandturns/git-evidence.ts— the two writers — are untouched.Verify locally
🤖 Generated with Claude Code