Files
FabledScribe/src/scribe/services/verification.py
T
bvandeusen 8489206224
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
feat(notes): the sweep — which notes assert a fact nobody has confirmed (#3166, milestone 317 step 3)
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.
2026-08-28 16:39:50 -04:00

58 lines
2.6 KiB
Python

"""What a record's own check MEANS — shared by rules and notes.
Two record types carry `verify_with` / `expires_when` / `verified_at`: rules
(milestone 312) and notes (milestone 317). What they share is BEHAVIOUR — how
a stamp is read, how old it is, what "never" means — not storage and not the
query. Note 3163 is the rule this file is an instance of: `semantic_search_*`
could never have been shared between them because a rule scopes by rulebook
ownership and a note scopes by the note ACL, so the two sweeps are siblings.
These functions are the part that genuinely is common, factored so it exists
once rather than twice.
Everything here is pure, sync and DUCK-TYPED: it reads `verify_with` and
`verified_at` off whatever it is handed. That is deliberate. A shared base
class or a protocol would tie two SQLAlchemy models together to share four
lines of date arithmetic — the DRY costume, in the same note's words.
"""
from datetime import datetime, timezone
def last_verified_label(record) -> str | None:
"""How long ago this record's check passed — None when it carries none.
Three states, and the distinction between the last two is the whole point:
- `None` — this record is a DECISION. There is nothing to go and check,
and the question does not apply. Most records.
- "never" — it asserts a fact and NOBODY HAS EVER CONFIRMED IT. The one
worth acting on, and why the sweep sorts these first.
- a date — somebody checked, then.
Callers attach this to a payload only when it is not None, so "no key" and
"never verified" do not become two states every client has to tell apart.
"""
if not getattr(record, "verify_with", None):
return None
stamp = getattr(record, "verified_at", None)
return stamp.date().isoformat() if stamp else "never"
def days_since_verified(record) -> int | None:
"""Whole days since the check last passed; None if it never has.
Computed rather than left to the reader, because "2026-06-14" and "74
days" prompt different reactions and only one of them is the question
being asked.
Naive stamps are read as UTC. Postgres hands these back tz-aware, but a
restore, a fixture or a sqlite-backed test may not, and subtracting an
aware datetime from a naive one raises — which would make the sweep fail
on exactly the rows it exists to surface.
"""
stamp = getattr(record, "verified_at", None)
if stamp is None:
return None
if stamp.tzinfo is None:
stamp = stamp.replace(tzinfo=timezone.utc)
return (datetime.now(timezone.utc) - stamp).days