CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / TypeScript typecheck (push) Successful in 11s
CI & Build / integration (push) Successful in 33s
CI & Build / Python tests (push) Failing after 45s
CI & Build / Build & push image (push) Skipped
The sibling of migration 0090, one table over. Same distinction: a NORM is a decision with no truth value; a CONSTRAINT asserts a fact about someone else's software and goes false with nobody watching. Notes hold far more constraints than rules do and hold them longer — a cross-project reference asserting what a signing service does on a duplicate upload is believed by every project that reads it, and nothing in the record says when anyone last looked. note_supersessions only fires once a human has already believed it. Three nullable columns, no backfill, no index. The index margin is thinner than 0090's — thousands of note rows against hundreds of rules — so the comment says to decide it in step 3 against a real query plan rather than guessing here. The columns land on every row in `notes`, but only non-task, non-snippet records will be OFFERED them (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. A schema-level gate would have meant a CHECK across three columns to say what the write path says in two lines. Backup carries the trio (v11), with `verified_at` restored through _dt_or_none — _dt substitutes now(), which would restore every never-checked note as checked at the moment of the restore, inverting the one signal the sweep reads. Found while doing that, NOT fixed here, and now pinned by a test: `_note_rows` carries 16 of the `notes` table's 26 columns. note_type, task_kind, arose_from_id, the recurrence pair, the lifecycle stamps, description and data have all been missing for a long time, so a restore flattens every snippet and process into a plain note and every issue and spike into `work`. The coverage guard cannot see it — it checks TABLES, not columns, which is #2293's failure mode one level down. #3182 tracks it; arose_from_id needs the second id-remapping pass parent_id gets, which is why it is not a drive-by fix.
81 lines
3.5 KiB
Python
81 lines
3.5 KiB
Python
"""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")
|