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

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:
2026-09-23 16:48:28 -04:00
co-authored by Claude Opus 5.5
parent bb632c4196
commit 66e21a6c60
24 changed files with 358 additions and 189 deletions
+9 -10
View File
@@ -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)
+4 -4
View File
@@ -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()),
):
+1 -1
View File
@@ -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()),
):
+12 -9
View File
@@ -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
+4 -3
View File
@@ -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,
)
+69 -57
View File
@@ -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.0100.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.0100.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),
)
+6 -4
View File
@@ -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"
+2 -2
View File
@@ -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,
+10 -6
View File
@@ -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"
+16 -8
View File
@@ -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)