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>
63 lines
2.5 KiB
Python
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")
|