feat(notes): a note can carry its own check — verify_with, expires_when, verified_at (#3165, milestone 317 step 1)
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
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.
This commit is contained in:
@@ -50,8 +50,12 @@ logger = logging.getLogger(__name__)
|
||||
# ones (reference/hook) travel too, cheaply, and the next refresh refreshes them.
|
||||
# v10 (2026-08) added projects.inception + project_rulebook_exclusions
|
||||
# (milestone 297): the WHY a project inherits what it does, and its opt-outs.
|
||||
# v11 (2026-08) added the note verification trio — notes.verify_with /
|
||||
# expires_when / verified_at (milestone 317). NOT an audit of the notes
|
||||
# section: it carries 16 of the 26 `notes` columns, and #3182 tracks the nine that
|
||||
# have been missing since long before this.
|
||||
# Bump when the serialized schema changes.
|
||||
BACKUP_VERSION = 10
|
||||
BACKUP_VERSION = 11
|
||||
|
||||
# Every table this backup carries, by its REAL name. Paired with _NOT_INCLUDED
|
||||
# below, these two lists must together account for the entire schema — which is
|
||||
@@ -291,6 +295,19 @@ def _milestone_rows(rows) -> list[dict]:
|
||||
|
||||
|
||||
def _note_rows(rows) -> list[dict]:
|
||||
# INCOMPLETE, and knowingly so — see #3182. This carries 16 of the
|
||||
# `notes` table's 26 columns. `note_type`, `task_kind`, `arose_from_id`, `data`,
|
||||
# `description`, `recurrence_rule`, `recurrence_next_spawn_at`,
|
||||
# `started_at` and `completed_at` are all absent, so a restore flattens
|
||||
# every snippet and process into a plain note and every issue and spike
|
||||
# into `work`. That predates the verification trio below and is tracked
|
||||
# separately rather than fixed in passing: `arose_from_id` points at
|
||||
# another note and needs the same second pass `parent_id` gets, which is
|
||||
# a change with its own trap and deserves its own tests.
|
||||
#
|
||||
# The table-coverage guard cannot see this. It asserts that every TABLE in
|
||||
# Base.metadata is either backed up or declared not-included; nothing
|
||||
# checks columns, which is exactly how nine of them went missing quietly.
|
||||
return [
|
||||
{
|
||||
"id": n.id, "user_id": n.user_id, "title": n.title, "body": n.body,
|
||||
@@ -300,6 +317,12 @@ def _note_rows(rows) -> list[dict]:
|
||||
"due_date": n.due_date.isoformat() if n.due_date else None,
|
||||
"created_at": n.created_at.isoformat(),
|
||||
"updated_at": n.updated_at.isoformat(),
|
||||
# The verification trio (milestone 317, migration 0092). These
|
||||
# travel because they are operator judgment — "somebody checked
|
||||
# this fact, and this is when" — which nothing can recompute.
|
||||
"verify_with": n.verify_with,
|
||||
"expires_when": n.expires_when,
|
||||
"verified_at": n.verified_at.isoformat() if n.verified_at else None,
|
||||
}
|
||||
for n in rows
|
||||
]
|
||||
@@ -752,6 +775,13 @@ async def _restore_v1(data: dict) -> dict:
|
||||
due_date=_d(n_data.get("due_date")),
|
||||
created_at=_dt(n_data.get("created_at")),
|
||||
updated_at=_dt(n_data.get("updated_at")),
|
||||
verify_with=n_data.get("verify_with"),
|
||||
expires_when=n_data.get("expires_when"),
|
||||
# _dt_or_none, NOT _dt: an absent stamp must stay absent. _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.
|
||||
verified_at=_dt_or_none(n_data.get("verified_at")),
|
||||
)
|
||||
session.add(note)
|
||||
await session.flush()
|
||||
@@ -888,6 +918,10 @@ async def _restore_v2(data: dict) -> dict:
|
||||
due_date=_d(n_data.get("due_date")),
|
||||
created_at=_dt(n_data.get("created_at")),
|
||||
updated_at=_dt(n_data.get("updated_at")),
|
||||
verify_with=n_data.get("verify_with"),
|
||||
expires_when=n_data.get("expires_when"),
|
||||
# _dt_or_none — see the note on the other restore path.
|
||||
verified_at=_dt_or_none(n_data.get("verified_at")),
|
||||
)
|
||||
session.add(note)
|
||||
await session.flush()
|
||||
|
||||
Reference in New Issue
Block a user