Written with Claude.
The sentinel that existed to remember what the system forgot
The clearest example of a ghost-guard (a check that protects against a condition that no longer exists, or never should have) I’ve found is one the field-assessment platform created for itself. On every re-cost pass, the cost agent (the automated step that recalculates repair costs) re-emits an engineer’s years override under its own author tag. That one round-trip erases the human-authored signal from the append-only event log (a tamper-evident journal of every change, keyed by author and timestamp). The system then stamps a sentinel flag called _years_auto (a presence marker the gateway reads to infer ownership), so it can later infer whether an engineer or the agent wrote the value, because the round-trip already destroyed that information.
A flag invented to recover provenance the system just destroyed. The event log always had an author field; the round-trip made it useless, then the sentinel patched around the uselessness. The cure is to stop the round-trip: have the agent skip re-emitting overrides it didn’t originate, and derive ownership from the author field directly. No sentinel.
A cleaner variant of the same pattern landed in commit c4428a15 (“refactor(gateway): drop building.delete + engineer.override event kinds (ADR-093)”) earlier today. Two event kinds, building.delete and engineer.override, had been handled by the gateway (the server layer that validates and applies every write to project data). Deleting a building is now a direct rm -rf on the data server, backed by restic snapshots and a replay-capable event log. Engineer overrides folded into the author tag on normal event kinds. The flows those two kinds guarded were genuinely gone, so we deleted the kinds and added one negative assertion in the schema test:
// apps/meap-web/src/lib/server/events/schema.test.ts:103
for (const dropped of [
'field-capture', 'full-replace', 'audit.note',
'building.delete', 'engineer.override'
]) {
The cheapest possible residue: a test that proves rejection.
Three shapes of defensive data
Both of those guards are the same mistake in different clothes: the system stored or flagged something it could have derived, then built machinery to defend the stored copy. Once you notice it, the pattern sorts into three shapes.
Compute-don’t-store. The gateway keeps cycle_cost (a line item’s total repair cost) as a saved field, so every change to unit_cost or quantity has to remember to recompute it, and a guard has to catch the cases that forget. But cycle_cost is just unit_cost × quantity. Compute it when the value is read and both the saved field and its sync machinery disappear.
Derive-don’t-flag. The _years_auto sentinel is this shape: a flag that remembers who owns a value, a judgment the event log already records in its author field. Read the author, delete the flag.
Drop-the-retired-kind. The building.delete and engineer.override event kinds were this shape: handling kept alive for a workflow that no longer runs. When the workflow is gone the handling is dead, and ADR-093 deleted it.
The first two are safe by construction. The source of truth, the operands or the author, is already present, so deriving from it can’t lose information. The third is where it gets dangerous, because “the workflow is gone” is a claim about the live system, and claims about the live system have to be checked.
The necessity-descent says “keep it” just as often
Committing those deletions the same week I was auditing other guards made me think the pre-cutover guards were next. A necessity-descent is exactly what it sounds like: for each guard you’re considering deleting, ask “what would actually make this fire today?” and verify against the live system, not a design diagram. The field-assessment platform migrated from a flat-file format to an event-source substrate (where the append-only log is the primary record, not a flat JSON file) some time ago, and the gateway has a hard stop for buildings still on the old format:
// tools/lib/writeBuilding.ts:213-223
if (!hasEvents && hasView) {
return {
success: false,
error: `Project at ${viewDir} is on the pre-cutover substrate. ...`,
code: 'SUBSTRATE_NOT_MIGRATED',
};
}
The preflight script (a sanity-check that every writes-capable command runs before touching data) reinforces this at the shell level:
# deploy/scripts/gateway-preflight.sh:79-89
pre-cutover)
echo "STOP: $PROJECT is on the pre-cutover substrate. The gateway"
echo " rejects every write with SUBSTRATE_NOT_MIGRATED ..."
exit 1 ;;
My read before probing: old format, guards probably stale, candidate for deletion. So I asked the live gateway.
The gateway reported 26 buildings across 12 projects still returning substrate='pre-cutover' right now. Several of those projects are delivered and closed. If someone opens one with /a-update to add a late finding, the missing guard lets an unmigratable write through. Of the 26, 19 are in a shape the migration tool itself refuses:
# tools/migrate-to-event-source.py:738-741
if not isinstance(pre_view, dict):
die(f"{view_path} is a legacy v4 flat-array building.json (delivered "
f"pre-v6 project). Convert it to the v6 schema before event-source "
f"migration; this tool only migrates v6 views.")
No v4-to-v6 converter exists. Those 19 buildings can’t be migrated through the current tooling regardless. The migration tool also runs on every deploy (deploy/scripts/deploy-client.sh:744), which means it’s not a spent one-time bridge, it’s a permanent runtime dependency. Verdict on six candidate guards: zero deletable. All load-bearing.
I kept them.
The probe is the discipline
Running the necessity-descent on the _years_auto sentinel and ADR-093 event kinds produced “delete it,” because the conditions that fire those guards no longer exist or were self-inflicted. Running the same descent on the pre-cutover guards produced “keep it,” because the condition fires against 26 real buildings today.
The probe is identical in both cases, and the answer varies. Sometimes the guard is protecting against something the system caused itself and can stop causing. Sometimes it’s blocking a class of write that will remain dangerous for years because no migration path exists.
Neither outcome is visible from reading the guard. Running the probe is the only way to find out which one you have.