The approvals row
A verdict someone announced is not an approval. An approval is a row in a file that the acting pass reads.
The failure it prevents
A reviewer finishes a review and posts APPROVE. Everyone involved now believes the change is approved. Nothing ships. The merge pass does not read comment threads — it reads a file, and that file is empty. The work sits green and reviewed for hours until somebody notices.
The inverse costs more. The approval lives in the memory of the process that granted it: a watcher waiting for checks to go green, a callback armed after review. The process cycles — a deploy, a restart, an OOM kill — and the approval dies with it. Nobody gets an error, because nothing failed. Something simply stopped existing.
Both are the same bug: the approval was stored somewhere the actor does not read, or somewhere that does not outlive the process. A file that is appended to and replayed is neither.
The shape
Three operations, one append-only file, no locks, no daemon:
import { approve, revoke, consume, pending } from './approvals.mjs';
const FILE = '.state/approvals.jsonl';
// The reviewer, in whatever process it happens to be running in.
const { id } = approve(FILE, { subject: 'deploy-api', revision: 'a1b2c3d', actor: 'reviewer' });
// The actor, later, in a different process, maybe on a different day.
for (const row of pending(FILE, { subject: 'deploy-api', revision: headSha() })) {
if (!checksGreen(row.revision)) continue;
if (consume(FILE, { id: row.id, actor: 'merge-pass', result: 'merged' })) merge(row.revision);
}
consume returning null is the load-bearing part. It means the approval was already claimed, or revoked, and the caller must not act. That single null is what stands between a retried job and a double execution.
Four properties, each of which exists because its absence has cost somebody a night:
- Append-only. Nothing rewrites or truncates. Consumption and revocation are new rows, so no crash can lose a fact that was already durable, and the file doubles as the audit trail: who approved what, at what revision, who acted on it, and when.
- Replayed, not tailed. Current state is a fold over the rows. The last row about an approval does not win — a terminal state is terminal, so a late or duplicated verdict cannot reopen something that already shipped.
- Pinned to an exact revision. An approval names the revision it approved. When the subject moves, the approval does not follow it. Review-then-amend is how unreviewed code ships past a review gate, and a revision field is the cheapest possible stop.
- Hostile-input tolerant on read. A row that does not parse is skipped and counted, never thrown. One corrupt line must not take down every pipeline that reads the file.
When NOT to use this
- When you need approvals in under a millisecond, or thousands per second. This replays the whole file on every read. That is free at thousands of rows and wrong at millions. Rotate the file, or use a database.
- When the file is not on one filesystem. Append atomicity is a local-filesystem property. Over NFS or a network share, two concurrent appenders can interleave bytes inside a row. Same machine, or a real database.
- When approval is a workflow, not a fact. Multi-party sign-off, quorum, escalation, delegation — those are state machines and deserve a real one. This models one decision by one actor about one revision.
- When the actor and the approver are the same process and always will be. Then you do not have this problem yet. You will when the process cycles, but there is no need to build for it now.
- When the approval must be secret or authenticated. A file row proves what was written, not who wrote it. Anyone with write access can append any actor name. If the identity is load-bearing, sign the rows.
Tests
node kit/approvals-row/test.mjs
Eight tests, and then a second pass that matters more: the same suite is re-run against seven deliberately broken copies of the implementation, and every mutant must be caught by at least one test. Each mutation is also proven to have changed the source, and to match exactly one place in it — a substitution that quietly matched nothing would produce a mutant identical to the original and a green run that proved nothing.
That pass has already paid for itself. The suite was green and looked thorough on the first run, and two mutants walked straight through it: consumption could be reopened by a later row, and revocation of an already-shipped approval reported success. Both are now tested. Green-on-write is not evidence; nothing here had ever been observed failing.
One mutant is declared untestable rather than deleted: removing the fsync cannot be caught in process, because SIGKILL destroys the process but leaves the page cache intact, so the un-synced row is still readable afterwards. Catching it needs power loss or a fault-injecting filesystem. It stays in the list, and the run prints it as a known limit, because a limit you publish is a limit and a limit you delete is a lie.
Part of the durable-agent-work kit. Dependency-free, Node ≥ 18, MIT.
Source: approvals.mjs · test.mjs