A rulebook holds two kinds of row in one table. A NORM is a decision: no
truth value, changes only when its author changes it, and they know they
did. A CONSTRAINT asserts a fact about someone else's software, and goes
false with nobody present. Milestone 307's audit found nine stale sites;
every one was a constraint, and not one norm had rotted.
Three nullable columns so a rule can say how to check itself. expires_when
is a STATE, not a date — constraints expire when the ground moves, not on a
schedule. verified_at NULL means never checked and sorts FIRST in the sweep
to come: unexamined outranks examined-long-ago. Most rules set none of the
three; a null verify_with is the marker for "this is a decision, there is
nothing to go and check," and it only reads that way while it stays honest.
Nothing is backfilled and nothing is indexed. A migration cannot invent a
check any more than 0088 could invent a trigger, and the sweep reads a whole
rulebook — hundreds of rows, on operator demand, never on a request path.
Also, in the backup service the fields had to pass through:
- Restore now remaps arose_from_id through note_id_map. It has been exported
since 0088 and silently dropped on the way back in ever since, so every
restore lost every rule's provenance link.
- _dt_or_none, because _dt substitutes now() for an absent value. That is
right for created_at/updated_at and wrong here: a rule nobody ever checked
would restore looking freshly checked and fall to the bottom of the sweep
it should top.
Column additions do not move BACKUP_VERSION; only new sections do, as when
0088 added when_to_apply/tier/arose_from_id to the same helper.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>