Files
FabledGrimoire/alembic/versions/0090_rule_verification.py
T
bvandeusenandClaude Opus 5.5 9e1da4af3d docs(6207): migrations 0088 and 0090-0094 state their rationale in terms of the data
Milestone/step labels, record ids, the rescoped-rule date and the 296-call
measurement go; each header states what the change protects. "rule 36"
citations become the DROP + ADD practice they named.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:46:36 -04:00

63 lines
2.5 KiB
Python

"""a rule can carry its own check — verify_with, expires_when, verified_at
Revision ID: 0090
Revises: 0089
Create Date: 2026-08-27
A rulebook holds two kinds of row in one table. A NORM is a decision: it has
no truth value, and it changes only when its author changes it — which they
know they did. A CONSTRAINT is a fact about someone else's software: a
runner's shell, a bot's config, a tool that exists. Nobody is present when
that goes false.
What goes stale in a rulebook is the constraints, not the norms — and a
stale constraint keeps instructing every session that loads it, long after
the world it described has moved.
Three nullable columns, so a rule can say how to check itself:
- `verify_with` — how to tell whether this is still true. A command, a path,
a URL, a query. Prose is allowed; something runnable is better.
- `expires_when` — the STATE under which it stops being true. Deliberately
not a date: constraints do not expire on a schedule, they expire when the
world underneath them moves.
- `verified_at` — when the check last passed. NULL means never checked, and
sorts FIRST in the sweep: unexamined outranks examined-long-ago.
All three nullable and all three optional, because most rules should set
none of them. A null `verify_with` is not an omission — it is the honest
marker of "this one is a decision, and there is nothing to go and check."
That signal only works if the field stays empty wherever it belongs empty.
No CHECK constraint is involved. Nothing is
backfilled: a migration cannot invent a check any more than 0088 could
invent a trigger.
"""
import sqlalchemy as sa
from alembic import op
revision = "0090"
down_revision = "0089"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.add_column("rules", sa.Column("verify_with", sa.Text(), nullable=True))
op.add_column("rules", sa.Column("expires_when", sa.Text(), nullable=True))
op.add_column(
"rules",
sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True),
)
# No index on (verify_with, verified_at). The sweep this exists for reads
# an operator's whole rulebook — hundreds of rows, not millions — and runs
# when a human asks for it, never on a request path. An index here would
# be maintained on every rule write to serve a query that a sequential
# scan answers instantly.
def downgrade() -> None:
op.drop_column("rules", "verified_at")
op.drop_column("rules", "expires_when")
op.drop_column("rules", "verify_with")