8de97b3190b00f9e7cf7b0f5ad3feb65e6927c34
1
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
8de97b3190 |
fix(lessons): strip a lesson's trigger by what was stored, not by matching a line (#4272)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 51s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m3s
CI & Build / Build & push image (push) Skipped
`update_lesson` is field-wise: an edit that touches only `system_ids`, or only the title, passes `insight=None`. The service then derives the insight from the stored body and hands it straight back to `compose_body`, which re-adds the trigger and the provenance line. The derivation and the composition have to be inverses, and they were not. `_strip_composed_lines` filtered out body LINES matching the trigger marker's pattern. That pattern is `re.M`, so it matches one line. `when_to_apply` is free text and is routinely several paragraphs — `compose_body` writes all of it after the marker, so paragraphs 2..n were ordinary lines the filter had no reason to touch. They came back as part of "the insight", the whole trigger was prepended again on top of them, and the record gained (n-1) paragraphs per update. Silent, compounding, and in the half of the document that does the retrieving: a lesson edited a few times ends up stating when it applies several times over and burying the insight below it. No marker could fix this. There is no end-of-trigger marker in the body, and adding one would not repair a row already written without it. What makes an exact strip possible is that the trigger is mirrored in `data[TRIGGER_KEY]`, so the composed head can be rebuilt byte for byte and removed by subtraction rather than matched by pattern. Hence `lesson_insight(note)` rather than `_strip_composed_lines(body)` — it takes the record because the body alone does not say where the trigger ends. The line-wise removal survives as the fallback for a row whose mirror is missing, where it is right for a one-paragraph trigger and is the best available without one; a mirror that disagrees with the body falls back rather than cutting into the insight. tests/test_lesson_insight_round_trip.py asserts the property rather than the strip: `lesson_document(what, trigger(n), insight(n), sources(n)) == (n.title, n.body)` across six shapes, and again over ten consecutive no-op edits. Falsified against the old code before committing: 11 of 23 fail on HEAD, the headline one reporting a body grown by 3230 characters over ten edits. The one damaged row in the corpus (#4207, two paragraphs duplicated by a `system_ids` edit) was repaired by hand against its authored text. #4221 is the only other lesson with a multi-paragraph trigger and has never been updated, so its body is intact — its derived `insight` was wrong, its storage was not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy |