Files
FabledScribe/tests/test_lesson_document_shape.py
T
bvandeusenandClaude Opus 5.5 66e21a6c60
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / integration (push) Successful in 52s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m35s
CI & Build / Build & push image (push) Successful in 32s
refactor(notes): a snippet's and lesson's stored title is its name; the trigger joins it only in the embedded document (milestone 427)
The title was `subject — trigger` because the stored title WAS the
embedded one, and the join is what makes these kinds rank on the
situation they apply to (#2485). Every surface that shows a title then
showed the trigger too -- menus, lists and search rows ran to kilobytes.

- embeddings.document_title(title, note_type, data, body) joins the
  trigger from `data` (body fallback) at embed time. Idempotent: an
  un-migrated composed title comes out the same, never doubled. The
  embed path, the startup backfill and the dedup gate's semantic signal
  all use it, so the embedded text -- and every vector -- is unchanged.
- Writers store the subject: snippet create/update (service, REST, MCP)
  and lesson_document. Both compose_title helpers are removed.
- Readers: dedup takes `data`; the menus strip the embedded title from a
  passage; list rows project `when_to_use`, which SnippetListView reads.
- 0108 rewrites existing rows on an exact `' — ' || <own trigger>`
  suffix with raw SQL, leaving updated_at alone so the backfill does not
  re-embed the corpus for identical vectors. Downgrade recomposes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 16:48:28 -04:00

152 lines
6.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The document a lesson is embedded as (milestone 385 step 3; milestone 427).
WHY THIS IS THE STEP THAT DECIDES THE MILESTONE
Everything before this is storage. A lesson stored with a trigger but embedded
as ordinary prose is a note wearing a label: it would look right in every
listing and simply never be retrieved at the moment it applies, and nothing
anywhere would report that.
WHERE THE SHARP SHAPE LIVES
A snippet, which note #2485 measured as the only sharp record in the corpus (a
0.153 top-to-second gap against 0.010–0.023 for everything else), is sharp
because its document states its purpose twice: `name — when to use` as the
title, and again as the body's first line. A lesson follows the snippet.
Until milestone 427 that `subject — trigger` title was also the STORED title,
so every listing, menu and search row showed the trigger too — kilobytes of it.
Now the stored title is the subject, and `embeddings.document_title` joins the
trigger back from `data` at embed time. The embedded TEXT is what it always
was, which is the property these guards pin: nothing re-embeds, and the floors
tuned against these vectors stay calibrated.
No similarity number is asserted anywhere: a threshold pins the embedder's
behaviour rather than this code's, and breaks on a model change that is not a
regression.
"""
from __future__ import annotations
from scribe.services import lessons as lessons_svc
from scribe.services.embeddings import chunk_document, document_title, embedding_text
TRIGGER = "a test fails on code you believe is correct"
SUBJECT = "Suspect the guard before the code"
INSIGHT = "Check whether the assertion still describes the property it was written for."
def _embedded(what: str, trigger: str, insight: str) -> tuple[str, str]:
"""The (title, body) the write path EMBEDS a lesson as — built the way
`notes.embed_note` builds it, from what `create_lesson` stores."""
title, body = lessons_svc.lesson_document(what, trigger, insight)
data = lessons_svc.compose_data(what, trigger)
return document_title(title, lessons_svc.LESSON_NOTE_TYPE, data, body), body
def test_the_stored_title_is_the_subject_alone():
"""Milestone 427. The trigger lives in `data` and the body's first line; the
title a listing shows is what the lesson is ABOUT."""
title, _ = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
assert title == SUBJECT
def test_the_embedded_document_is_the_one_it_always_was():
"""THE no-re-embed guard. The document text must be byte-identical to what
a lesson embedded as when its stored title carried the trigger — and an
un-migrated row, whose stored title still does, must come out the same
rather than with the trigger twice."""
legacy_title = f"{SUBJECT} — {TRIGGER}"
title, body = _embedded(SUBJECT, TRIGGER, INSIGHT)
assert title == legacy_title
data = lessons_svc.compose_data(SUBJECT, TRIGGER)
assert document_title(legacy_title, lessons_svc.LESSON_NOTE_TYPE, data, body) == legacy_title
def test_the_trigger_appears_twice_in_the_document():
"""Purpose stated twice in a short document is the entire measured cause of
a snippet's sharpness, and it is the one property that distinguishes a
lesson's vector from a plain note's."""
title, body = _embedded(SUBJECT, TRIGGER, INSIGHT)
document = embedding_text(title, body)
assert document.count(TRIGGER) == 2
# Once in each half, not twice in one of them.
assert TRIGGER in title
assert TRIGGER in body
def test_the_document_leads_with_when_it_applies():
"""The embedded title is `{what} — {when}` and the body's FIRST line
restates it, so the opening of the document is about the situation rather
than the topic."""
title, body = _embedded(SUBJECT, TRIGGER, INSIGHT)
assert title == f"{SUBJECT} — {TRIGGER}"
assert body.splitlines()[0] == f"**When to apply:** {TRIGGER}"
def test_the_trigger_reaches_the_document_even_without_the_mirror():
"""A row whose `data` lost its mirror still embeds sharply: the trigger is
read back from the body, the same fallback `lesson_trigger` has."""
title, body = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
assert document_title(title, lessons_svc.LESSON_NOTE_TYPE, None, body) == (
f"{SUBJECT} — {TRIGGER}"
)
def test_a_short_lesson_is_exactly_one_chunk():
"""`chunk_document`'s first contract line: a record inside the window
yields one chunk identical to the historical `title\\nbody`."""
title, body = _embedded(SUBJECT, TRIGGER, INSIGHT)
chunks = chunk_document(title, body)
assert len(chunks) == 1
assert chunks[0].count(TRIGGER) == 2
def test_a_long_lesson_keeps_the_trigger_on_every_chunk():
"""Every chunk is prefixed with the embedded title, which carries the
trigger — so a long story occupies its own vectors instead of averaging
itself into the trigger's, and each is still anchored to when it applies.
"""
narrative = "\n\n".join(
f"## Section {i}\n" + ("An unrelated sentence about deployment. " * 40)
for i in range(6)
)
title, body = _embedded(SUBJECT, TRIGGER, narrative)
chunks = chunk_document(title, body)
assert len(chunks) > 1, "the fixture must actually exceed the chunk budget"
assert all(TRIGGER in chunk for chunk in chunks)
def test_a_lesson_with_no_trigger_still_embeds():
"""Degrades to title + insight, the way a rule with no trigger does — less
sharply, and still findable."""
title, body = _embedded(SUBJECT, "", INSIGHT)
assert title == SUBJECT
assert body == INSIGHT
assert chunk_document(title, body) == [f"{SUBJECT}\n{INSIGHT}"]
def test_the_composed_body_is_the_one_the_reader_is_parsed_back_from():
"""`compose_body` writes the trigger line and `lesson_trigger` reads it. A
lesson whose mirror in `data` is missing still answers correctly, so the
two must agree on the exact markdown."""
from types import SimpleNamespace
_, body = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
no_mirror = SimpleNamespace(data=None, body=body)
assert lessons_svc.lesson_trigger(no_mirror) == TRIGGER
def test_the_title_and_body_are_composed_by_one_call():
"""`lesson_document` returns both halves so they cannot be built apart."""
assert lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT) == (
SUBJECT,
lessons_svc.compose_body(INSIGHT, TRIGGER),
)