"""a note can carry its own check — verify_with, expires_when, verified_at (milestone 317 step 1) Revision ID: 0092 Revises: 0091 Create Date: 2026-08-28 The sibling of 0090, which gave rules the same three columns. Same distinction, one table over: A NORM is a decision — 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, and nobody is present when it goes false. Notes hold far more constraints than rules do, and hold them for longer. A cross-project reference note asserting what a signing service does on a duplicate upload, or how a forge numbers its CI runs, is believed by every project that reads it, and there is nothing in the record that says when anyone last looked. `note_supersessions` only fires once a human has read the note, disagreed, and written the correction — which is the case where the note was already believed. Three nullable columns: - `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. WHICH ROWS THESE ARE FOR. `notes` is one table holding notes, tasks, snippets and processes, so these columns land on all of them. Only non-task, non-snippet records are OFFERED them (milestone 317 decisions 1 and 2, gated at the service in step 2): a task's decay is its status, and a snippet already carries a richer, location-aware verdict in `data.verification`. The columns exist on the other rows and stay null there; a gate that lives in the schema would have meant a partial index or a CHECK across three columns to express something the write path can say in two lines. All three optional, because most notes should set none of them — the whole value of the sweep is that its output is short. 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." No CHECK constraint is involved, so rule 36 does not apply. Nothing is backfilled: a migration cannot invent a check. """ import sqlalchemy as sa from alembic import op revision = "0092" down_revision = "0091" branch_labels = None depends_on = None def upgrade() -> None: op.add_column("notes", sa.Column("verify_with", sa.Text(), nullable=True)) op.add_column("notes", sa.Column("expires_when", sa.Text(), nullable=True)) op.add_column( "notes", sa.Column("verified_at", sa.DateTime(timezone=True), nullable=True), ) # No index, for 0090's reason — the sweep runs when a human asks, never on # a request path — but the margin is thinner here and worth naming. `rules` # is hundreds of rows; `notes` is thousands and grows with every session. # # Still a sequential scan's job at this size, and an index on # (verified_at) filtered to `verify_with IS NOT NULL` would be maintained # on every note write to serve one operator-initiated query. If step 3's # live acceptance measures otherwise, add it there against a real plan # rather than guessing here. def downgrade() -> None: op.drop_column("notes", "verified_at") op.drop_column("notes", "expires_when") op.drop_column("notes", "verify_with")