src/store-guards.ts
196 lines and 13 functions in DrDustinEdwards/capsid, at commit daa875b on 2026-10-24. This file on GitHub.
Functions
| Function | Lines | Complexity | Covered | CRAP | Mutation |
|---|---|---|---|---|---|
| requireExists | 7 to 15 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| requireJobUnchanged | 22 to 36 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| requireBenchRunRunning | 40 to 48 | 1 | 11% | OkCRAP 1.7 | No data: mutation testing has not run on this repo yet |
| isMissingRowAbort | 50 to 52 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| requireBodyUnchanged | 56 to 69 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| requireMissing | 71 to 79 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| snapshotLive | 84 to 92 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| snapshotTaken | 96 to 98 | 2 | 100% | OkCRAP 2 | No data: mutation testing has not run on this repo yet |
| guardedCommit | 111 to 161 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| precheckIfMatch | 121 to 127 | 5 | 100% | OkCRAP 5 | No data: mutation testing has not run on this repo yet |
| run | 131 to 159 | 10 | 100% | WatchCRAP 10 | No data: mutation testing has not run on this repo yet |
| documentUpsert | 169 to 192 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
| auditStatement | 198 to 209 | 1 | 100% | OkCRAP 1 | No data: mutation testing has not run on this repo yet |
Imports
It imports src/auth.ts. Imported by src/access-sync.ts, src/admin-client-audit.ts, src/agents-admin.ts, src/agents-archive.ts, src/bench.ts, src/canon.ts, src/controls-perform.ts, src/doc-statements.ts, src/job-breaker.ts, src/jobs-holder.ts, src/jobs-mirror.ts, src/jobs-transition.ts, src/maintenance-branches.ts, src/maintenance-releases.ts, src/merge-safety.ts, src/merge-units.ts, src/namespace-delete.ts, src/ops-feed.ts, src/ops-packages.ts, src/ops-sites.ts, src/outcome-prs.ts, src/overnight.ts, src/policy-sign.ts, src/pr-approvals.ts, src/push.ts, src/routine-fire.ts, src/shadow.ts, src/site-key.ts, src/site-repair.ts, src/supersede.ts, src/tools/docs-lifecycle.ts, src/tools/docs-write.ts, src/tools/lint.ts, src/tools/namespaces.ts, src/tools/repo-guards.ts.
Source
import { sha256Hex } from "./auth";
// INSERT that violates NOT NULL, guarded by NOT EXISTS, so a missing row aborts
// the batch. A pre-read is a different transaction. meta.changes is inflated by
// FTS5 triggers and cannot count what a batch did.
const GUARD_VIOLATION = "document_versions.document_id";
export function requireExists(db: D1Database, namespace: string, path: string): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path)
SELECT NULL, ?1, ?2
WHERE NOT EXISTS (SELECT 1 FROM documents WHERE namespace = ?1 AND path = ?2)`
)
.bind(namespace, path);
}
// The same abort for a job transition, as the first statement of the batch that
// carries the UPDATE and every record of it. It fires unless the row is still in the
// status and holder the caller read, at the updated_at the caller read, so the
// mirror, audit and outcome rows built from that read commit only with the UPDATE
// they describe. updated_at moves on every job UPDATE, so any change in between aborts.
export function requireJobUnchanged(
db: D1Database,
id: string,
status: string,
claimedBy: string | null,
updatedAt: string
): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path)
SELECT NULL, 'jobs', ?1
WHERE NOT EXISTS (SELECT 1 FROM jobs WHERE id = ?1 AND status = ?2 AND claimed_by IS ?3 AND updated_at = ?4)`
)
.bind(id, status, claimedBy, updatedAt);
}
// The same abort for a bench run (src/bench.ts finishRun): fires unless the run is still
// running, so a run's close and its job's close commit together or not at all.
export function requireBenchRunRunning(db: D1Database, runId: string): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path)
SELECT NULL, 'bench', ?1
WHERE NOT EXISTS (SELECT 1 FROM bench_runs WHERE id = ?1 AND status = 'running')`
)
.bind(runId);
}
export function isMissingRowAbort(err: unknown): boolean {
return (err instanceof Error ? err.message : String(err)).includes(GUARD_VIOLATION);
}
// Body equality rather than a stored sha column. `IS` rather than `=` because a
// NULL body is legitimate and `body = NULL` is never true.
export function requireBodyUnchanged(
db: D1Database,
namespace: string,
path: string,
expectedBody: string | null
): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path)
SELECT NULL, ?1, ?2
WHERE NOT EXISTS (SELECT 1 FROM documents WHERE namespace = ?1 AND path = ?2 AND body IS ?3)`
)
.bind(namespace, path, expectedBody);
}
export function requireMissing(db: D1Database, namespace: string, path: string): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path)
SELECT NULL, ?1, ?2
WHERE EXISTS (SELECT 1 FROM documents WHERE namespace = ?1 AND path = ?2)`
)
.bind(namespace, path);
}
// The snapshot of the live row, one spelling. It selects the row the table holds when
// the batch runs, not a body read earlier, so a write landing in between is
// snapshotted rather than lost. It inserts nothing when no row exists.
export function snapshotLive(db: D1Database, namespace: string, path: string): D1PreparedStatement {
return db
.prepare(
`INSERT INTO document_versions (document_id, namespace, path, title, body)
SELECT id, namespace, path, title, body FROM documents WHERE namespace = ?1 AND path = ?2
RETURNING id`
)
.bind(namespace, path);
}
// Whether a snapshotLive statement inserted a row, from its batch result. Read from
// RETURNING rather than meta.changes, which the FTS5 triggers inflate.
export function snapshotTaken(result: D1Result | undefined): boolean {
return (result?.results?.length ?? 0) > 0;
}
type WriteGuard = "none" | "body" | "missing";
type CommitRefusals = {
ifMatchOnMissing: string;
ifMatchMismatch: (currentSha: string, passed: string) => string;
createCollision: string;
deletedInFlight: string;
bodyChanged: (currentSha: string, elicited: boolean) => string;
batchFailed: (reason: string) => string;
};
export function guardedCommit(opts: {
db: D1Database;
namespace: string;
path: string;
prior: { body: string | null } | null;
if_match: string | undefined;
refusals: CommitRefusals;
}) {
const { db, namespace, path, prior, if_match, refusals } = opts;
return {
async precheckIfMatch(): Promise<string | null> {
if (if_match === undefined) return null;
if (!prior) return refusals.ifMatchOnMissing;
const currentSha = await sha256Hex(prior.body ?? "");
const passed = if_match.trim().toLowerCase();
return currentSha === passed ? null : refusals.ifMatchMismatch(currentSha, passed);
},
// Either the refusal, or the batch results of the caller's own statements in the
// order given (the armed guard's result is dropped), so a caller can report what a
// statement with RETURNING did.
async run(
elicited: boolean,
statements: D1PreparedStatement[]
): Promise<{ refusal: string } | { results: D1Result[] }> {
let guard: WriteGuard = "none";
const armed: D1PreparedStatement[] = [];
if (!prior) {
guard = "missing";
armed.push(requireMissing(db, namespace, path));
} else if (if_match !== undefined || elicited) {
guard = "body";
armed.push(requireBodyUnchanged(db, namespace, path, prior.body));
}
try {
const results = await db.batch([...armed, ...statements]);
return { results: results.slice(armed.length) };
} catch (err) {
if (!isMissingRowAbort(err)) {
return { refusal: refusals.batchFailed(err instanceof Error ? err.message : String(err)) };
}
if (guard === "missing") return { refusal: refusals.createCollision };
const current = await db
.prepare("SELECT body FROM documents WHERE namespace = ?1 AND path = ?2")
.bind(namespace, path)
.first<{ body: string | null }>();
if (!current) return { refusal: refusals.deletedInFlight };
return { refusal: refusals.bodyChanged(await sha256Hex(current.body ?? ""), elicited) };
}
},
};
}
// The document upsert, one spelling, shared by `capsid_write_doc` and `capsid_lint_namespace` mode `report`.
// COALESCE on every optional column, so an omitted argument leaves the stored value
// alone. docStatements (src/doc-statements.ts) sets every column and is not folded in.
//
// test/mutation-guard-coverage.test.ts and test/tool-annotations.test.ts both match
// this call as a mutation marker, the same way they match pathMutation().
export function documentUpsert(
db: D1Database,
namespace: string,
path: string,
title: string | null,
body: string,
type: string | null,
tags: string | null,
status: string | null
): D1PreparedStatement {
return db
.prepare(
`INSERT INTO documents (namespace, path, title, body, type, tags, status)
VALUES (?1, ?2, ?3, ?4, COALESCE(?5, 'note'), ?6, COALESCE(?7, 'published'))
ON CONFLICT(namespace, path) DO UPDATE SET
title = COALESCE(?3, documents.title),
body = excluded.body,
type = COALESCE(?5, documents.type),
tags = COALESCE(?6, documents.tags),
status = COALESCE(?7, documents.status),
updated_at = datetime('now')`
)
.bind(namespace, path, title, body, type, tags, status);
}
// The audit row, spelled once. Every write path appends one
// (snapshot invariant, test/write-invariants.test.ts). params is stored as JSON. An audit row whose params come from inside the batch (delete's
// edges, a skill transition guarded by its new status) is an INSERT ... SELECT and
// keeps its own statement.
export function auditStatement(
db: D1Database,
actor: string,
action: string,
namespace: string | null,
path: string | null,
params: unknown
): D1PreparedStatement {
return db
.prepare("INSERT INTO audit_log (actor, action, namespace, path, params) VALUES (?1, ?2, ?3, ?4, ?5)")
.bind(actor, action, namespace, path, JSON.stringify(params));
}