"""a rule can carry its own check — verify_with, expires_when, verified_at
(milestone 312 step 1)

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.

Milestone 307's rulebook audit found nine stale sites. Every one was a
constraint; not one norm had rotted. One of them had been telling every
session to skip database-backed tests for weeks while the integration lane
sat green in the workflow.

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, so rule 36 does not apply here. 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")
