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
+87 -6
View File
@@ -649,15 +649,23 @@ def test_a_reply_that_is_all_working_stores_no_working():
about their beer instead of any advice at all.
"""
working_only = (
"Let me think about what this reading means.\n\n"
"Let me think about what this reading means. The original gravity is "
"1.055 and the current reading is 1.030, which is a drop of 25 points "
"over nine days at 19 degrees, and I need to work out what that means "
"for the expected attenuation.\n\n"
"Perhaps the drop is smaller than expected, and it could be that the "
"yeast is stressed.\n\n"
"yeast is stressed by the temperature rather than actually done, "
"because most ale strains slow down considerably below 18 degrees.\n\n"
"Wait, I need to keep in mind that the hydrometer was read at a "
"different temperature.\n\n"
"different temperature than the sample, which would make the reading "
"read lower than it really is, and I have no way of knowing from the "
"data alone what the correction factor was.\n\n"
"I need to keep in mind that the original gravity here was measured "
"before the hops went in.\n\n"
"So the question is what the expected attenuation should be, and I "
"am not sure without more data."
"before the hops went in, and the reading on brew day was 1.055, so "
"the drop is twenty-five points against a potential of fifty-five.\n\n"
"So the question is what the expected attenuation should be for this "
"style at this temperature, and I am not sure without more data or a "
"temperature-corrected reading to compare against."
)
stripped = brew_advice._strip_reasoning(working_only)
@@ -667,6 +675,39 @@ def test_a_reply_that_is_all_working_stores_no_working():
)
def test_a_short_reply_is_advice_even_without_a_marker_word():
"""Real advice is often phrased without any marker phrase.
"Actually the drop is fine. Wait a couple of days and check again."
is advice - and judging it paragraph by paragraph emptied it, so the
brewer saw nothing at all.
"""
reply = ("Actually the drop is fine.\n\n"
"Wait a couple of days and check again.")
assert brew_advice._strip_reasoning(reply) == reply
def test_a_long_reply_with_no_instruction_is_dropped():
"""Long, and nothing in it reads like an instruction - all working."""
working = ("Let me think about this.\n\n" +
"Perhaps the yeast is stressed, and it could be that the "
"temperature is too low. " * 20)
assert brew_advice._strip_reasoning(working) == ""
def test_a_long_reply_with_an_instruction_keeps_it():
"""The working is trimmed from it, but the advice stays."""
mixed = ("Let me think about this.\n\n" +
"Perhaps the yeast is stressed. " * 15 +
"\n\nWarm the fermenter to 20C and give the yeast a gentle "
"swirl to rouse it.")
out = brew_advice._strip_reasoning(mixed)
assert "Warm the fermenter" in out
def test_a_paragraph_about_keeping_something_in_mind_is_not_advice():
""""Keep" alone is not an advice marker.
@@ -745,6 +786,46 @@ def test_the_advice_is_written_for_a_reading_that_predates_the_token(repo):
assert after["comment"] == "a comment"
def test_a_pre_token_reading_is_still_guarded_against_a_stale_write(repo):
"""A reading that has never been edited has no token at all.
The guard matches on the ABSENCE of a token, so an edit still tells
the generations apart. Falling back to the gravity here would
re-open the hole, because a comment-only correction leaves the
gravity unchanged.
"""
repo.readings.insert_one({
"_id": "preToken0000000000000010",
"beer_run_id": "1",
"beer_type": "Ale",
"sg": "1050",
"date": "2026-11-01",
"final_reading": "False",
"comment": "first comment",
})
# The advice thread starts on this generation - no token.
assert repo.set_reading_advice("preToken0000000000000010", "FRESH ADVICE",
True, expected_sg=None,
expected_token="") == 1
# The brewer corrects the comment. The edit writes a token.
repo.upsert_reading(beer_run_id="1", beer_type="Ale", sg=1050,
reading_date="2026-11-01", final_reading=False,
comment="corrected comment")
# The late thread for the OLD generation lands. It carries no token,
# and the reading now HAS one - so it must be refused.
stale = repo.set_reading_advice("preToken0000000000000010", "STALE ADVICE",
True, expected_sg=None,
expected_token="")
assert stale == 0, "the stale advice overwrote the correction"
after = repo.readings.find_one({"beer_run_id": "1"})
assert after.get("advice") is None
assert after["comment"] == "corrected comment"
def test_a_reply_cut_off_at_the_token_ceiling_is_marked(repo):
"""A reply that stopped at the token limit is incomplete.