feat: record evidence and attest it - #8
Merged
Merged
Conversation
koistya
added this pull request to stack #9
September 19, 2026 18:04
koistya
force-pushed
the
feat/evidence-and-files
branch
from
September 19, 2026 18:06
4385f11 to
48b1bfe
Compare
quality-runtime
Bot
force-pushed
the
feat/evidence-and-files
branch
from
September 19, 2026 22:24
48b1bfe to
7f0e780
Compare
Evidence records that a control was operated, dated by when it happened. Attesting it is a signature: it requires If-Match quoting the version that was read, is refused under impersonation, and after it PostgreSQL will not let the application change or remove the row. Files follow separately.
quality-runtime
Bot
force-pushed
the
feat/evidence-and-files
branch
from
September 19, 2026 23:27
7f0e780 to
fc8dc66
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.
Adds the evidence half of
requirement → control → evidence: a record that a control was actually operated, and a person's signature on it. Files and durable storage follow in the next PR; this one is deliberately without attachments.Note
This replaces the earlier combined branch. The three guideline commits merged as #7, and the file/storage work is split out, so this PR is now evidence and attestation only.
The routes
POST/controls/{id}/evidence201with anETag, so it can be attested without a second readGET/controls/{id}/evidenceGET/requirements/{id}/evidenceGET/evidence/{id}ETagPATCH/evidence/{id}If-MatchDELETE/evidence/{id}PUT/evidence/{id}/attestationIf-MatchrequiredoccurredAtis when the thing happened, not when the row was written, and may not be more than five minutes ahead — an allowance for a client clock running fast.Attesting is a signature, not an edit
*, lists and weak tags are refused: a signature is of one version in particular.403, rather than the act being recorded carefully.409.UPDATEandDELETEpolicies admit only unattested rows, so PostgreSQL — not the handler — refuses every later change.Races, and why the code looks the way it does
SELECT … FOR UPDATEis governed by theUPDATEpolicy, so an attested row cannot be locked, and "cannot be locked" would otherwise surface as "does not exist". Amend and discard therefore read unlocked to tell404from409, then lock, and read again when the lock finds nothing. Attestation locks nothing at all: it puts the version in itsUPDATE … WHERE, and when that matches no row it rereads to answer404,409or412— not412for all three.What the real-PostgreSQL suite now forces (
apps/server/concurrency.test.ts)409and404, never the one mistaken for the other;404/409rather than a foreign-key500;Details worth a reviewer's eye
attestedAt(clock_timestamp()), the same clock audit history uses.controlId, so discarded evidence can still say which control it belonged to, and/history?resource=evd_…reads it.Decisions
ADR 0012 records the design: attestation as the first finalised record enforced by policy rather than convention, why
If-Matchis required only here, and why a validity period is not modelled. It builds on ADR 0019 (versions andIf-Match) and ADR 0020 (how these races are forced). The schema, policies and triggers landed earlier in #3; this adds the API and the docs.Verification
On the committed tree, checked out clean:
Each new guarantee was checked by removing it and watching its test fail: the attestation reread, the no-op amendment, the exact-tag rule, the empty
If-Match, the five-minute allowance, and theETagon recording.Not here
verify:files— the next PR, which adds afilesfield to these responses. Links from ADR 0020 to ADR 0013 stay broken until then.