fix(advice): the pre-token guard, and a stripper that cannot empty real advice

Two findings from the fifth review.

The pre-token fallback re-opened the hole
-----------------------------------------
A reading that has never been edited has no token.  The guard fell back
to the gravity check for those, and a comment-only correction leaves the
gravity unchanged - so a late thread's write matched and overwrote the
fresh advice, for exactly the 47 existing readings.

Reproduced live before the fix:

    pre-token reading token: None  ->  stale thread carries ""
    fresh write modified: 1
    stale write modified: 1   <<< stale advice overwrote the fresh one
    final advice: 'STALE ADVICE'

The guard now matches on the ABSENCE of a token, which still tells the
generations apart: an edit writes a token, so a stale thread's "" stops
matching the moment the reading is corrected.

    fresh write modified: 1 (must be 1)
    stale write modified: 0   (must be 0)

The stripper could empty real advice
------------------------------------
"Actually the drop is fine.\\n\\nWait a couple of days and check again."
is advice, and judging it paragraph by paragraph emptied it - so the
brewer saw nothing at all, with no failure note.

The test is no longer a marker list, which real advice often does not
contain.  A reply is all-working when it is LONG and has no instruction
in it: the model's working runs to thousands of characters, while the
advice is two to five sentences.

    short reply with no marker  -> kept
    long reply, no instruction  -> dropped
    long reply with advice      -> advice kept
    single-paragraph working    -> dropped

Also: the log message named the gravity, but the guard is the token.

182 tests (up from 178).
This commit is contained in:
2026-11-09 19:08:28 +10:00
parent 7d9d718b41
commit d18d037bb9
4 changed files with 153 additions and 14 deletions
+12 -6
View File
@@ -267,14 +267,20 @@ class BeerRepository:
# The editing token is the PRIMARY guard: it changes on every
# edit, so it catches a stale generation whatever else
# changed - the gravity, the comment, or nothing.
#
# An EMPTY token is not a usable guard: it means the reading
# predates the token, so there is no token to match.
# Querying on it would match nothing, and the advice would
# never be written for the existing readings.
query["advice_token"] = str(expected_token)
elif expected_token == "":
# The reading has NEVER been edited, so it has no token at
# all. Match on the ABSENCE of one, which still tells the
# generations apart: an edit writes a token, so a stale
# thread's "" stops matching the moment the reading is
# corrected.
#
# Falling back to the gravity here would re-open the hole
# this guard exists to close - a comment-only correction
# leaves the gravity unchanged.
query["advice_token"] = {"$exists": False}
elif expected_sg is not None:
# No token - a direct call with a gravity to check against.
# A direct call with a gravity to check against.
query["sg"] = str(expected_sg)
# `$in` matches either form. A plain list would be an exact