fix(lessons): a derived mirror survives the generic note door, by kind not by name (#3734)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 23s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m31s
CI & Build / Build & push image (push) Successful in 23s
Groundwork for step 7, and a data-integrity fix in its own right. Two kinds keep a queryable mirror in `notes.data` derived from their body: snippets and, since milestone 385, lessons. Every read prefers the mirror — deliberately, because parsing markdown to answer what an index can answer is how a hot path rots. So a write that moves the body must move the mirror. #3128 found that hole for snippets and plugged it with a hard-coded `if note.note_type == SNIPPET_NOTE_TYPE`. The plug was correct and did not generalise: lessons arrived with the same design and none of the protection, which is precisely the "don't add a fourth instance" defect #3734 was told to avoid. The cost is higher for a lesson. A stale snippet mirror reports the wrong path. A stale lesson mirror reports the wrong TRIGGER, and the trigger is the whole retrieval story — the lesson goes on firing for the situation it used to name while displaying the one it now names. Silent, and confident. So `update_note` now dispatches through `_mirror_recomposers()`, a note_type -> recomposer table. A kind with a derived mirror is covered by registering it, not by someone remembering to widen an if. `lessons.recompose_data` is the lesson's entry. It recovers the subject with `embeddings.untrigger_title` — new, and deliberately placed beside the join it inverts rather than in the caller that wanted it, because a separator spelled in two files is a separator that will one day be changed in one of them (#3207). `TRIGGER_SEP` is now the one spelling, and `parse_snippet_fields` uses it too; it had the third copy inline. The two inverses stay distinct on purpose: a snippet partitions at the first separator (its name is a symbol), a lesson strips an exact known suffix (its subject may legitimately contain a dash). Different algorithms, one constant, so they cannot disagree about where the seam is. Provenance is DROPPED when the body drops it, which is the opposite call from a snippet's `verification` — that is carried because it was never in the body to delete. The body is the authority; carrying a value the reader just removed is the failure the recompose exists to prevent. Tests: test_snippet_mirror_generic_door.py becomes test_derived_mirror_generic_door.py, since the concern is now plural. The registry property is asserted directly (every kind with a mirror is in the table; the dispatch names no kind inline), plus the lesson cases and the join/inverse round-trip. `fake_lesson` moves to tests/helpers.py — it existed in test_lesson_surfacing.py and a second copy was about to be written — and gains the explicit `None`s `fake_snippet` carries, because update_note reads `verify_with` and a MagicMock is truthy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
@@ -191,6 +191,34 @@ def fake_snippet(**attrs) -> MagicMock:
|
||||
}, attrs)
|
||||
|
||||
|
||||
def fake_lesson(**attrs) -> MagicMock:
|
||||
"""A stand-in lesson: a note whose `note_type` is what makes it one.
|
||||
|
||||
The title carries the trigger because `compose_title` builds it that way —
|
||||
`{what} — {when it applies}` — so a menu line rendering only the title is
|
||||
already showing the reader when this lesson applies. Tests that used a bare
|
||||
title here would be testing a record the product cannot create.
|
||||
|
||||
The check fields and `arose_from_id` are explicitly None for the reason
|
||||
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and
|
||||
`expires_when` to decide whether to run the check-field guard, and an
|
||||
auto-created MagicMock attribute is truthy — so a default lesson driven
|
||||
through the update path would take a branch no real record takes.
|
||||
"""
|
||||
attrs.setdefault(
|
||||
"title",
|
||||
"Give absolutely-positioned siblings an explicit stacking order — "
|
||||
"placing two absolutely-positioned elements in the same area",
|
||||
)
|
||||
attrs.setdefault("data", {"when_to_apply": "two absolute siblings overlap"})
|
||||
attrs.setdefault("status", None)
|
||||
attrs.setdefault("arose_from_id", None)
|
||||
attrs.setdefault("verify_with", None)
|
||||
attrs.setdefault("expires_when", None)
|
||||
attrs.setdefault("verified_at", None)
|
||||
return fake_note(note_type="lesson", **attrs)
|
||||
|
||||
|
||||
def fake_project(**attrs) -> MagicMock:
|
||||
"""design_system_id is explicit: a truthy auto-attribute would route every
|
||||
project through the design-system branch and out to a real database."""
|
||||
|
||||
Reference in New Issue
Block a user