refactor(notes): a snippet's and lesson's stored title is its name; the trigger joins it only in the embedded document (milestone 427)
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
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
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>
This commit is contained in:
+9
-10
@@ -212,10 +212,10 @@ def fake_snippet(**attrs) -> MagicMock:
|
||||
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 title is the subject alone and the trigger lives in `data`, because
|
||||
that is the record the product creates (milestone 427) — the trigger joins
|
||||
the title only in the embedded document. A default whose title carried the
|
||||
trigger would be testing a row only an un-migrated database holds.
|
||||
|
||||
The check fields and `arose_from_id` are explicitly None for the reason
|
||||
`fake_snippet`'s `data` is: `update_note` reads `verify_with` and
|
||||
@@ -223,12 +223,11 @@ def fake_lesson(**attrs) -> MagicMock:
|
||||
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("title", "Give absolutely-positioned siblings an explicit stacking order")
|
||||
attrs.setdefault("data", {
|
||||
"what": "Give absolutely-positioned siblings an explicit stacking order",
|
||||
"when_to_apply": "placing two absolutely-positioned elements in the same area",
|
||||
})
|
||||
attrs.setdefault("status", None)
|
||||
attrs.setdefault("arose_from_id", None)
|
||||
attrs.setdefault("verify_with", None)
|
||||
|
||||
@@ -56,7 +56,7 @@ async def test_the_backfill_embeds_the_text_as_it_is_now_not_as_it_was_scanned()
|
||||
with (
|
||||
patch.object(emb, "async_session", return_value=_ctx(scan)),
|
||||
patch.object(emb, "_current_row",
|
||||
AsyncMock(return_value=(42, "T", *edited))),
|
||||
AsyncMock(return_value=(42, "T", *edited, "note", None))),
|
||||
patch.object(emb, "upsert_note_embedding", AsyncMock()) as upsert,
|
||||
patch.object(emb.asyncio, "sleep", AsyncMock()),
|
||||
):
|
||||
@@ -75,7 +75,7 @@ async def test_a_record_deleted_between_the_scan_and_the_loop_is_skipped():
|
||||
with (
|
||||
patch.object(emb, "async_session", return_value=_ctx(scan)),
|
||||
patch.object(emb, "_current_row",
|
||||
AsyncMock(side_effect=[None, (42, "T", "body")])),
|
||||
AsyncMock(side_effect=[None, (42, "T", "body", "note", None)])),
|
||||
patch.object(emb, "upsert_note_embedding", AsyncMock()) as upsert,
|
||||
patch.object(emb.asyncio, "sleep", AsyncMock()),
|
||||
):
|
||||
@@ -99,7 +99,7 @@ async def test_a_record_whose_text_outran_its_vectors_is_re_embedded():
|
||||
|
||||
with (
|
||||
patch.object(emb, "async_session", return_value=_ctx(scan)),
|
||||
patch.object(emb, "_current_row", AsyncMock(return_value=(42, "T", "b"))),
|
||||
patch.object(emb, "_current_row", AsyncMock(return_value=(42, "T", "b", "note", None))),
|
||||
patch.object(emb, "upsert_note_embedding", AsyncMock()) as upsert,
|
||||
patch.object(emb.asyncio, "sleep", AsyncMock()),
|
||||
):
|
||||
@@ -119,7 +119,7 @@ async def test_a_task_logged_since_its_vectors_is_re_embedded():
|
||||
|
||||
with (
|
||||
patch.object(emb, "async_session", return_value=_ctx(scan)),
|
||||
patch.object(emb, "_current_row", AsyncMock(return_value=(42, "T", "b"))),
|
||||
patch.object(emb, "_current_row", AsyncMock(return_value=(42, "T", "b", "note", None))),
|
||||
patch.object(emb, "upsert_note_embedding", AsyncMock()) as upsert,
|
||||
patch.object(emb.asyncio, "sleep", AsyncMock()),
|
||||
):
|
||||
|
||||
@@ -306,7 +306,7 @@ async def test_backfill_reembeds_notes_with_a_stale_chunker_version():
|
||||
with (
|
||||
patch.object(emb, "async_session", return_value=ctx),
|
||||
patch.object(emb, "_current_row",
|
||||
AsyncMock(return_value=(42, "stale-version", "body"))),
|
||||
AsyncMock(return_value=(42, "stale-version", "body", "note", None))),
|
||||
patch.object(emb, "upsert_note_embedding", AsyncMock()) as upsert,
|
||||
patch.object(emb.asyncio, "sleep", AsyncMock()),
|
||||
):
|
||||
|
||||
@@ -164,11 +164,11 @@ def _lesson_body(trigger, insight="Read the job log.", sources=None):
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_body_write_moves_a_lessons_trigger_with_it():
|
||||
from scribe.services.lessons import TRIGGER_KEY, compose_title
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
|
||||
what = "Read the job log before waiting longer"
|
||||
note = fake_lesson(
|
||||
title=compose_title(what, NEW_TRIGGER),
|
||||
title=what,
|
||||
data={TRIGGER_KEY: "a CI run is slow", "what": what},
|
||||
project_id=None,
|
||||
)
|
||||
@@ -183,13 +183,16 @@ async def test_a_body_write_moves_a_lessons_trigger_with_it():
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_subject_containing_an_em_dash_still_splits():
|
||||
"""Why `untrigger_title` is given the trigger instead of splitting on the
|
||||
separator: a subject may legitimately contain one."""
|
||||
from scribe.services.lessons import TRIGGER_KEY, compose_title
|
||||
separator: a subject may legitimately contain one. The title here is an
|
||||
UN-MIGRATED one, still carrying its trigger (milestone 427), because that
|
||||
is the row the inverse still has to read correctly."""
|
||||
from scribe.services.embeddings import trigger_title
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
|
||||
what = "A wait with no deadline — the shape, not the symptom"
|
||||
trigger = "you are about to await something crossing a process boundary"
|
||||
note = fake_lesson(
|
||||
title=compose_title(what, trigger), data=None, project_id=None,
|
||||
title=trigger_title(what, trigger), data=None, project_id=None,
|
||||
)
|
||||
await _update(note, body=_lesson_body(trigger))
|
||||
assert note.data["what"] == what
|
||||
@@ -202,10 +205,10 @@ async def test_dropping_the_provenance_line_drops_it_from_the_mirror():
|
||||
the failure this recompose exists to prevent, not a courtesy — the
|
||||
opposite call from a snippet's `verification`, which is carried because it
|
||||
was never in the body to delete."""
|
||||
from scribe.services.lessons import SOURCES_KEY, compose_title
|
||||
from scribe.services.lessons import SOURCES_KEY
|
||||
|
||||
note = fake_lesson(
|
||||
title=compose_title("Something learned", "a situation"),
|
||||
title="Something learned",
|
||||
data={SOURCES_KEY: [999]},
|
||||
project_id=None,
|
||||
)
|
||||
@@ -229,7 +232,7 @@ async def test_an_explicit_data_wins_for_a_lesson_too():
|
||||
async def test_a_lesson_title_change_reaches_the_mirror():
|
||||
"""A lesson's subject lives in its title, so a title edit is a trigger for
|
||||
recomposition exactly as it is for a snippet's name."""
|
||||
from scribe.services.lessons import TRIGGER_KEY, compose_title
|
||||
from scribe.services.lessons import TRIGGER_KEY
|
||||
|
||||
trigger = "two absolute siblings overlap"
|
||||
note = fake_lesson(
|
||||
@@ -237,7 +240,7 @@ async def test_a_lesson_title_change_reaches_the_mirror():
|
||||
data={TRIGGER_KEY: trigger, "what": "the old subject"},
|
||||
project_id=None,
|
||||
)
|
||||
await _update(note, title=compose_title("the new subject", trigger))
|
||||
await _update(note, title="the new subject")
|
||||
assert note.data["what"] == "the new subject"
|
||||
assert note.data[TRIGGER_KEY] == trigger
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ async def test_a_lesson_is_a_row_the_database_accepts(owner_id):
|
||||
value, and stays correct if it is gated with it."""
|
||||
lesson = await notes_svc.create_note(
|
||||
owner_id,
|
||||
title=lessons_svc.compose_title(SUBJECT, TRIGGER),
|
||||
title=SUBJECT,
|
||||
body=f"**When to apply:** {TRIGGER}\n\nOne change at a time.",
|
||||
note_type=lessons_svc.LESSON_NOTE_TYPE,
|
||||
data={lessons_svc.TRIGGER_KEY: TRIGGER},
|
||||
@@ -83,7 +83,8 @@ async def test_a_lesson_is_a_row_the_database_accepts(owner_id):
|
||||
# and the readable body — because the vector is built from the text and
|
||||
# the queries are built from the mirror.
|
||||
assert lessons_svc.lesson_trigger(stored) == TRIGGER
|
||||
assert stored.title == f"{SUBJECT} — {TRIGGER}"
|
||||
# The subject alone (milestone 427): the trigger is in `data` and the body.
|
||||
assert stored.title == SUBJECT
|
||||
assert "**When to apply:**" in (stored.body or "")
|
||||
|
||||
|
||||
@@ -97,7 +98,7 @@ async def test_a_lesson_is_not_a_task(owner_id):
|
||||
"""
|
||||
lesson = await notes_svc.create_note(
|
||||
owner_id,
|
||||
title=lessons_svc.compose_title(SUBJECT, TRIGGER),
|
||||
title=SUBJECT,
|
||||
body="One change at a time.",
|
||||
note_type=lessons_svc.LESSON_NOTE_TYPE,
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""The document a lesson is embedded as (milestone 385 step 3).
|
||||
"""The document a lesson is embedded as (milestone 385 step 3; milestone 427).
|
||||
|
||||
WHY THIS IS THE STEP THAT DECIDES THE MILESTONE
|
||||
|
||||
@@ -7,26 +7,20 @@ 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.
|
||||
|
||||
WHY THERE IS NO `lesson_document()` BESIDE `rule_document()`
|
||||
WHERE THE SHARP SHAPE LIVES
|
||||
|
||||
The step anticipated one. There isn't, and the difference is where the sharp
|
||||
shape LIVES rather than whether it exists.
|
||||
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.
|
||||
|
||||
A rule keeps its trigger in a column and its title is a plain name, so the
|
||||
`{title} — {trigger}` document has to be synthesised at embed time and exists
|
||||
nowhere else — that is what `rule_document` is for. 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), gets there the other way: its STORED
|
||||
title is already the join and its stored body already opens with the trigger,
|
||||
so the ordinary `title\\nbody` join is the sharp document. A lesson follows the
|
||||
snippet, which is what step 1 decided and step 2 built.
|
||||
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.
|
||||
|
||||
The consequence worth stating: `chunk_document` is untouched, so
|
||||
`CHUNKER_VERSION` does not move and nothing re-embeds. The step's "Re-embed"
|
||||
section describes a change this design does not make.
|
||||
|
||||
These guards therefore assert the composed record, then assert that the generic
|
||||
chunker turns it into the intended document — the two halves of the same claim.
|
||||
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.
|
||||
@@ -34,18 +28,46 @@ regression.
|
||||
from __future__ import annotations
|
||||
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services.embeddings import chunk_document, embedding_text
|
||||
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():
|
||||
"""THE guard. 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 = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
|
||||
"""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
|
||||
@@ -55,22 +77,28 @@ def test_the_trigger_appears_twice_in_the_document():
|
||||
|
||||
|
||||
def test_the_document_leads_with_when_it_applies():
|
||||
"""The 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.
|
||||
A lesson buried behind a paragraph of narrative would rank on the
|
||||
narrative."""
|
||||
title, body = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
|
||||
"""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`. A lesson that
|
||||
split into several would spread the trigger's weight across vectors that
|
||||
each carry less of it."""
|
||||
title, body = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
|
||||
yields one chunk identical to the historical `title\\nbody`."""
|
||||
title, body = _embedded(SUBJECT, TRIGGER, INSIGHT)
|
||||
chunks = chunk_document(title, body)
|
||||
|
||||
assert len(chunks) == 1
|
||||
@@ -78,26 +106,15 @@ def test_a_short_lesson_is_exactly_one_chunk():
|
||||
|
||||
|
||||
def test_a_long_lesson_keeps_the_trigger_on_every_chunk():
|
||||
"""The narrative question, answered by the chunker rather than by holding
|
||||
the story out of the record.
|
||||
|
||||
`rule_document` excludes a rule's `why` because long dated narrative made
|
||||
sixteen dev-logs land on the centroid of "development". That finding
|
||||
predates chunking (#280): a body over budget is now split, and EVERY chunk
|
||||
is prefixed with the title — which for a lesson carries the trigger. So the
|
||||
story occupies its own vectors instead of averaging itself into the
|
||||
trigger's, and each of those vectors is still anchored to when the lesson
|
||||
applies.
|
||||
|
||||
This is why the insight stays in the body where a reader can see it. Holding
|
||||
it out would cost the reader the only part that explains the lesson, to buy
|
||||
a sharpness the chunker already provides.
|
||||
"""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 = lessons_svc.lesson_document(SUBJECT, TRIGGER, narrative)
|
||||
title, body = _embedded(SUBJECT, TRIGGER, narrative)
|
||||
chunks = chunk_document(title, body)
|
||||
|
||||
assert len(chunks) > 1, "the fixture must actually exceed the chunk budget"
|
||||
@@ -106,10 +123,8 @@ def test_a_long_lesson_keeps_the_trigger_on_every_chunk():
|
||||
|
||||
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. That is an argument for prompting hard for a
|
||||
trigger at write time, not for padding the document with whatever text is
|
||||
to hand."""
|
||||
title, body = lessons_svc.lesson_document(SUBJECT, "", INSIGHT)
|
||||
sharply, and still findable."""
|
||||
title, body = _embedded(SUBJECT, "", INSIGHT)
|
||||
|
||||
assert title == SUBJECT
|
||||
assert body == INSIGHT
|
||||
@@ -119,8 +134,7 @@ def test_a_lesson_with_no_trigger_still_embeds():
|
||||
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 — which is why neither is written by
|
||||
hand at a call site."""
|
||||
two must agree on the exact markdown."""
|
||||
from types import SimpleNamespace
|
||||
|
||||
_, body = lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT)
|
||||
@@ -130,10 +144,8 @@ def test_the_composed_body_is_the_one_the_reader_is_parsed_back_from():
|
||||
|
||||
|
||||
def test_the_title_and_body_are_composed_by_one_call():
|
||||
"""`lesson_document` returns both halves so they cannot be built apart. A
|
||||
title carrying the trigger over a body that does not would embed as an
|
||||
ordinary note, and every listing would still look correct."""
|
||||
"""`lesson_document` returns both halves so they cannot be built apart."""
|
||||
assert lessons_svc.lesson_document(SUBJECT, TRIGGER, INSIGHT) == (
|
||||
lessons_svc.compose_title(SUBJECT, TRIGGER),
|
||||
SUBJECT,
|
||||
lessons_svc.compose_body(INSIGHT, TRIGGER),
|
||||
)
|
||||
|
||||
@@ -29,7 +29,6 @@ from types import SimpleNamespace
|
||||
|
||||
from scribe.services import knowledge as knowledge_svc
|
||||
from scribe.services import lessons as lessons_svc
|
||||
from scribe.services import snippets as snippets_svc
|
||||
from scribe.services.embeddings import trigger_title
|
||||
|
||||
|
||||
@@ -76,15 +75,18 @@ def test_one_join_builds_every_trigger_title():
|
||||
expected = f"{subject} — {trigger}"
|
||||
|
||||
assert trigger_title(subject, trigger) == expected
|
||||
assert lessons_svc.compose_title(subject, trigger) == expected
|
||||
assert snippets_svc.compose_title(subject, trigger) == expected
|
||||
# The two note kinds reach it through the EMBEDDED title (milestone 427),
|
||||
# from their own mirror key — never a hand-rolled join.
|
||||
from scribe.services.embeddings import document_title
|
||||
assert document_title(subject, "lesson", {"when_to_apply": trigger}) == expected
|
||||
assert document_title(subject, "snippet", {"when_to_use": trigger}) == expected
|
||||
|
||||
|
||||
def test_a_subject_with_no_trigger_degrades_to_the_subject():
|
||||
"""It still embeds, just less sharply — an argument for backfilling
|
||||
triggers, not for padding the title with whatever text is to hand."""
|
||||
assert trigger_title("debounce", "") == "debounce"
|
||||
assert lessons_svc.compose_title(" debounce ") == "debounce"
|
||||
assert lessons_svc.lesson_document(" debounce ")[0] == "debounce"
|
||||
assert trigger_title("", "when it applies") == "when it applies"
|
||||
|
||||
|
||||
|
||||
@@ -214,13 +214,13 @@ def test_the_payload_reads_back_the_composed_fields():
|
||||
"""A caller that wrote `when_to_apply` reads `when_to_apply` back, not a
|
||||
body it has to parse."""
|
||||
from tests.helpers import fake_lesson
|
||||
from scribe.services.lessons import compose_body, compose_title, lesson_to_dict
|
||||
from scribe.services.lessons import compose_body, lesson_to_dict
|
||||
|
||||
what = "Read the job log before waiting longer"
|
||||
trigger = "a CI run has sat in_progress longer than its suite takes"
|
||||
note = fake_lesson(
|
||||
id=7,
|
||||
title=compose_title(what, trigger),
|
||||
title=what,
|
||||
body=compose_body("The work is usually done.", trigger, [4181]),
|
||||
data={"what": what, "when_to_apply": trigger, "taught_by": [4181]},
|
||||
project_id=None,
|
||||
|
||||
@@ -117,7 +117,8 @@ async def test_the_duplicate_gate_runs_before_anything_is_created():
|
||||
@pytest.mark.asyncio
|
||||
async def test_the_gate_compares_the_composed_document_not_the_raw_fields():
|
||||
"""What reaches the gate is the title and body a lesson will actually be
|
||||
stored as. Comparing `what` alone would miss that the trigger is half the
|
||||
stored as, plus the `data` its EMBEDDED title is built from (milestone
|
||||
427). Comparing `what` alone would miss that the trigger is half the
|
||||
document, and would judge two lessons alike that rank nothing alike."""
|
||||
_user_id_ctx.set(7)
|
||||
gate = AsyncMock(return_value=None)
|
||||
@@ -128,8 +129,9 @@ async def test_the_gate_compares_the_composed_document_not_the_raw_fields():
|
||||
await create_lesson(what=SUBJECT, when_to_apply=TRIGGER, insight="Look.")
|
||||
|
||||
title, body = gate.await_args.args[1], gate.await_args.args[2]
|
||||
assert title == f"{SUBJECT} — {TRIGGER}"
|
||||
assert title == SUBJECT
|
||||
assert body.startswith(f"**When to apply:** {TRIGGER}")
|
||||
assert gate.await_args.kwargs["data"]["when_to_apply"] == TRIGGER
|
||||
assert gate.await_args.kwargs["note_type"] == "lesson"
|
||||
|
||||
|
||||
@@ -157,9 +159,11 @@ def test_a_lesson_is_judged_at_the_trigger_dominated_bar():
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_an_update_recomposes_both_halves_of_the_document():
|
||||
"""A new trigger has to reach the title AND the head of the body. Patching
|
||||
one would leave a lesson that reads correctly and ranks on the old
|
||||
situation — the failure mode with no symptom."""
|
||||
"""A new trigger has to reach the mirror AND the head of the body — the two
|
||||
places the embedded document reads it from (milestone 427). Patching one
|
||||
would leave a lesson that reads correctly and ranks on the old situation —
|
||||
the failure mode with no symptom. The title stays the subject, even when
|
||||
the stored one was an un-migrated composed title."""
|
||||
_user_id_ctx.set(7)
|
||||
stored = _stub_note(
|
||||
title=f"{SUBJECT} — {TRIGGER}",
|
||||
@@ -172,7 +176,7 @@ async def test_an_update_recomposes_both_halves_of_the_document():
|
||||
await lessons_svc.update_lesson(7, 1, when_to_apply="a guard goes red")
|
||||
|
||||
fields = updated.await_args.kwargs
|
||||
assert fields["title"] == f"{SUBJECT} — a guard goes red"
|
||||
assert fields["title"] == SUBJECT
|
||||
assert fields["body"].startswith("**When to apply:** a guard goes red")
|
||||
assert fields["data"]["when_to_apply"] == "a guard goes red"
|
||||
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
from scribe.services import snippets as s
|
||||
|
||||
|
||||
def test_compose_title_with_and_without_usage():
|
||||
assert s.compose_title("debounce", "rate-limit a callback") == "debounce — rate-limit a callback"
|
||||
assert s.compose_title(" debounce ", "") == "debounce"
|
||||
assert s.compose_title("debounce") == "debounce"
|
||||
def test_the_embedded_title_joins_the_trigger_the_stored_one_does_not_carry():
|
||||
"""Milestone 427: stored title = name; the trigger joins it at embed time,
|
||||
idempotently, so an old composed title comes out the same."""
|
||||
from scribe.services.embeddings import document_title
|
||||
|
||||
data = {"name": "debounce", "when_to_use": "rate-limit a callback"}
|
||||
assert document_title("debounce", "snippet", data) == "debounce — rate-limit a callback"
|
||||
assert document_title("debounce — rate-limit a callback", "snippet", data) == (
|
||||
"debounce — rate-limit a callback"
|
||||
)
|
||||
assert document_title("debounce", "snippet", {"name": "debounce"}) == "debounce"
|
||||
assert document_title("a — note", "note", data) == "a — note"
|
||||
|
||||
|
||||
def test_compose_tags_lowercases_language_and_dedups():
|
||||
@@ -32,7 +40,7 @@ def test_compose_body_bare_code_only():
|
||||
|
||||
|
||||
def test_parse_round_trips_a_composed_snippet():
|
||||
title = s.compose_title("useDebouncedRef", "debounce a reactive ref")
|
||||
title = "useDebouncedRef"
|
||||
body = s.compose_body(
|
||||
code="const x = 1", language="ts", signature="useDebouncedRef(v, ms)",
|
||||
when_to_use="debounce a reactive ref", repo="scribe",
|
||||
@@ -241,9 +249,9 @@ def test_data_and_body_round_trip_to_the_same_fields():
|
||||
name, when, sig, lang = ("debounce", "rate-limit a callback",
|
||||
"debounce(fn, ms)", "ts")
|
||||
locs = [{"repo": "web", "path": "src/util.ts", "symbol": "debounce"}]
|
||||
# compose_body takes no `name` — the name lives in the title — so the two
|
||||
# serializers get their own argument lists rather than a shared spread.
|
||||
title = s.compose_title(name, when)
|
||||
# compose_body takes no `name` — the name IS the title (milestone 427) — so
|
||||
# the two serializers get their own argument lists rather than a shared spread.
|
||||
title = name
|
||||
body = s.compose_body(code="const x = 1", language=lang, signature=sig,
|
||||
when_to_use=when, locations=locs, merged_from=[41, 42])
|
||||
tags = s.compose_tags(lang)
|
||||
|
||||
Reference in New Issue
Block a user