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
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:
@@ -320,6 +320,96 @@ async def delete_note(note_id: int) -> dict:
|
||||
"message": f"Note {note_id} moved to trash. Restore with restore('{batch}')."}
|
||||
|
||||
|
||||
async def notes_due_for_verification(
|
||||
older_than_days: int = 0, project_id: int = 0, never_only: bool = False,
|
||||
) -> dict:
|
||||
"""Which notes assert a FACT that nobody has confirmed lately.
|
||||
|
||||
A corpus of notes holds two kinds of thing. Most are DECISIONS or records
|
||||
of what happened — they have no truth value and cannot rot. A few assert a
|
||||
fact about someone else's software: what a signing service does on a
|
||||
duplicate upload, how a forge numbers its CI runs, what an updater
|
||||
compares. Those go false silently, with nobody present, and a
|
||||
cross-project reference note keeps being read as current by every project
|
||||
that cites it.
|
||||
|
||||
This lists the second kind, oldest verification first, NEVER-CHECKED AT
|
||||
THE TOP — a note nobody has ever confirmed is a claim with no evidence
|
||||
behind it at all. Each row carries `verify_with` in full, because you are
|
||||
about to go and run it, plus `expires_when` and `days_since_verified`.
|
||||
|
||||
Reach for it when curating, when a note's claim just contradicted what you
|
||||
observed, or periodically. Then, per row: run the check, and call
|
||||
mark_note_verified with what you found.
|
||||
|
||||
Notes with no `verify_with` never appear, and that is correct — they are
|
||||
decisions, and there is nothing to go and check. Do not "fix" their
|
||||
absence by giving them checks: this list is only worth reading while
|
||||
everything on it genuinely can go false.
|
||||
|
||||
Args:
|
||||
older_than_days: only notes last verified longer ago than this.
|
||||
Never-checked notes always qualify — they are the most overdue
|
||||
thing there is. 0 = no age filter.
|
||||
project_id: narrow to one project. 0 = every project. Unlike the rules
|
||||
sweep, this filter is safe: a note belongs to at most one project
|
||||
outright, with none of the subscription and always-on paths that
|
||||
would make a project filter UNDER-report a rule.
|
||||
never_only: only notes nobody has ever verified.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
notes = await notes_svc.notes_due_for_verification(
|
||||
uid,
|
||||
older_than_days=older_than_days,
|
||||
project_id=project_id or None,
|
||||
never_only=never_only,
|
||||
)
|
||||
return {
|
||||
"notes": [notes_svc.verification_row(n) for n in notes],
|
||||
"total": len(notes),
|
||||
}
|
||||
|
||||
|
||||
async def mark_note_verified(note_id: int, still_true: bool = True) -> dict:
|
||||
"""Record that you ran a note's check — and what it said.
|
||||
|
||||
Call this AFTER actually running the note's `verify_with`, never on the
|
||||
strength of the claim sounding plausible. A stamp nobody earned is worse
|
||||
than no stamp: it moves the note to the bottom of the sweep and buys the
|
||||
claim another long silence.
|
||||
|
||||
`still_true=False` writes NOTHING. A note whose check failed is not in a
|
||||
special state to be recorded — it is WRONG, and the honest next moves are
|
||||
to correct it, supersede it, or find out why. So it stays at the top of
|
||||
the sweep until someone deals with it, and the response tells you what the
|
||||
note said would end it.
|
||||
|
||||
Args:
|
||||
note_id: the note whose check you ran.
|
||||
still_true: True if the check passed. False if the fact it asserts is
|
||||
no longer true — say so, that is the outcome worth having.
|
||||
"""
|
||||
uid = current_user_id()
|
||||
note = await notes_svc.mark_note_verified(note_id, uid, still_true)
|
||||
if note is None:
|
||||
raise ValueError(
|
||||
f"note {note_id} not found, not writable by you, or carries no "
|
||||
f"verify_with (nothing to verify is not the same as verified)"
|
||||
)
|
||||
data = notes_svc.verification_row(note)
|
||||
data["verified"] = bool(still_true)
|
||||
if not still_true:
|
||||
data["next"] = (
|
||||
"This note is no longer true and is still being read as current "
|
||||
"by anything that cites it. Correct it with update_note, write "
|
||||
"the replacement with create_note(supersedes=[...]), or clear its "
|
||||
"check with update_note(clear=[\"verify_with\"]) if it has stopped "
|
||||
"asserting a fact at all. It stays at the top of "
|
||||
"notes_due_for_verification until one of those happens."
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
def register(mcp) -> None:
|
||||
for fn in (
|
||||
list_notes,
|
||||
@@ -328,5 +418,7 @@ def register(mcp) -> None:
|
||||
update_note,
|
||||
find_duplicate_records,
|
||||
delete_note,
|
||||
notes_due_for_verification,
|
||||
mark_note_verified,
|
||||
):
|
||||
mcp.tool(name=fn.__name__)(fn)
|
||||
|
||||
Reference in New Issue
Block a user