feat(notes): the sweep — which notes assert a fact nobody has confirmed (#3166, milestone 317 step 3)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 28s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Failing after 46s
CI & Build / Build & push image (push) Skipped

The read half. `notes_due_for_verification` + `mark_note_verified` + the MCP
pair, ordered `verified_at ASC NULLS FIRST`: never-checked outranks
checked-long-ago, because a note nobody has ever confirmed is a claim with no
evidence behind it at all. Postgres sorts NULLs LAST on ASC by default, so
getting this wrong would not error — it would silently invert the one signal
the sweep exists to carry, which is why it has a test of its own.

A SIBLING of rules_due_for_verification, not a shared implementation, and this
milestone is a deliberate self-application of note 3163: the row could have
been shared, the QUERY could not. That sweep scopes by rulebook ownership XOR
project ownership because rules have no sharing ACL at all; a note scopes by
the note ACL — browse, not read, so a record shared one-to-one never arrives
in a passive surface unasked (decision 2094).

What genuinely IS common moved to services/verification.py: how a stamp reads,
how old it is, and the three states `last_verified` distinguishes — None ("a
decision, the question does not apply"), "never" ("a fact nobody has
confirmed"), a date. Rulebooks now imports it rather than defining it, so this
is a consolidation and not a third copy.

A failed check writes NOTHING, carried over from 312: there is no "verified
false" state, because a note whose check failed is not in a special condition
— it is WRONG, and recording the failure as a flag would let it sit there
being false with the sweep quietly satisfied that somebody had looked.

Two decisions worth naming:

The sweep does NOT filter to non-task, non-snippet records even though the
write path permits a check on nothing else. Such a row would be in an ILLEGAL
state and this is the one surface that could say so; hiding it to match the
invariant would make the sweep agree with a database it had stopped
describing.

A negative `older_than_days` raises instead of meaning "everything" — silently
answering a different question is the failure shape this guards.

`notes_due_for_verification` is classified read-only in server.py, spelled out
because its name matches none of the prefixes the completeness test derives
from. `rules_due_for_verification` is in the same position and is NOT listed,
so it fails closed for read keys today — filed as #3191 rather than fixed
here, since widening an auth boundary on a tool I did not write is the
operator's call.
This commit is contained in:
2026-08-28 16:39:50 -04:00
parent 4736a0a0ba
commit 8489206224
6 changed files with 537 additions and 21 deletions
+11 -21
View File
@@ -16,6 +16,10 @@ from sqlalchemy import and_, delete as sql_delete, insert, or_, select
from scribe.models import async_session
from scribe.models.system import System
from scribe.models.rulebook import Rulebook
from scribe.services.verification import (
days_since_verified as _days_since_verified,
last_verified_label as _last_verified_label,
)
logger = logging.getLogger(__name__)
@@ -311,19 +315,12 @@ def _valid_tier(tier: str) -> str:
return tier if tier in TIERS else "always_on"
def last_verified_label(rule: Rule) -> str | None:
"""How long ago the rule's check passed — None when it carries no check.
One helper because two surfaces need the same answer and the brief-dict
lesson in rule_brief's docstring is what happens otherwise: three copies
that had already drifted. `None` means "this rule is a decision, the
question does not apply"; "never" means "it is a fact and nobody has
confirmed it" — a distinction worth keeping, because the second is the
one worth acting on.
"""
if not rule.verify_with:
return None
return rule.verified_at.date().isoformat() if rule.verified_at else "never"
# Re-exported, not redefined. Notes gained the same trio in milestone 317 and
# this reading of it is genuinely common, so it moved to services/verification
# — the DRY win note 3163 names, as against sharing the QUERY, which the two
# record types cannot (a rule scopes by rulebook ownership, a note by the note
# ACL). Kept importable from here because callers already reach for it here.
last_verified_label = _last_verified_label
def rule_brief(rule: Rule, **extra) -> dict:
@@ -1455,14 +1452,7 @@ def verification_row(rule: Rule) -> dict:
because "2026-06-14" and "74 days" prompt different reactions and only
one of them is the question being asked.
"""
from datetime import datetime, timezone
days = None
if rule.verified_at is not None:
stamp = rule.verified_at
if stamp.tzinfo is None:
stamp = stamp.replace(tzinfo=timezone.utc)
days = (datetime.now(timezone.utc) - stamp).days
days = _days_since_verified(rule)
return {
"id": rule.id,
"title": rule.title,