CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 37s
CI & Build / integration (push) Successful in 28s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Successful in 25s
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>
65 lines
2.6 KiB
Python
65 lines
2.6 KiB
Python
"""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")
|