feat(snippets): a snippet has notes; when_to_use is the situation it is ranked on (#4378)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 34s

A snippet had no field for prose, so what a session learned about one went
into when_to_use — the trigger joined onto every chunk it is embedded as. A
sweep found write-ups of up to 3 KB there, headings and all.

- notes: stored after the code under `## Notes`, parsed back from the body,
  carried by every path that rebuilds it (update, merge, un-merge). A
  snippet with no notes composes the body it always did.
- create/update_snippet (MCP) take notes and return trigger_advice when
  when_to_use is long, headed or multi-paragraph. Advice, not a refusal.
- Tool docs, the reusing-code skill and the editor hint describe the trigger
  as the situation and point the explanation at notes.
- Editor gains a Notes field; the detail view renders it as markdown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 17:27:22 -04:00
co-authored by Claude Opus 5.5
parent 66e21a6c60
commit 4ae18a9dd9
9 changed files with 313 additions and 21 deletions
+154
View File
@@ -0,0 +1,154 @@
"""A snippet's notes — the explanation that had nowhere to go (#4378).
Before this field, a snippet had name, code, signature, locations and
`when_to_use`, and nothing for prose. What a session learned about a snippet
went into `when_to_use` — the trigger, joined onto the title of every chunk the
snippet is embedded as — so a multi-paragraph write-up there blurred the one
situation it is ranked on.
The rules pinned here, each with a way to rot silently:
- Notes live in the BODY, after the code, under `## Notes`, and read back
from it. A `## Notes` line inside the code is code, not the section.
- A snippet with no notes composes the body it always did, byte for byte —
no re-embed for the corpus that has none.
- Every path that rebuilds the body CARRIES the notes. `update_snippet`,
merge and un-merge all compose the body from scratch, so a field one of
them forgets is erased by any edit to something else.
- `trigger_advice` speaks for a write-up and stays quiet for a situation.
"""
import pytest
import pytest_asyncio
from tests.helpers import ensure_user
from scribe.services import snippets as s
CODE = "def helper():\n return 1"
NOTES = "Shaped this way because the caller owns the session.\n\nSuperseded #12."
# --- unit: the body convention -----------------------------------------------
def test_notes_follow_the_code_under_their_heading():
body = s.compose_body(code=CODE, language="python", when_to_use="a helper", notes=NOTES)
code_at = body.index("```python")
notes_at = body.index(s.NOTES_HEADING)
assert code_at < notes_at
assert body.rstrip().endswith("Superseded #12.")
def test_notes_round_trip_through_the_body():
body = s.compose_body(code=CODE, language="python", notes=NOTES)
got = s.parse_snippet_fields("helper", body, ["python", "snippet"])
assert got["notes"] == NOTES
assert got["code"] == CODE
def test_a_snippet_without_notes_composes_the_body_it_always_did():
"""No trailing section, no extra blank line: the corpus that has no notes
must embed exactly as before."""
body = s.compose_body(code=CODE, language="python", when_to_use="a helper")
assert body == "**When to use:** a helper\n\n```python\n" + CODE + "\n```\n"
assert s.parse_snippet_fields("helper", body)["notes"] == ""
def test_a_notes_heading_inside_the_code_is_code():
md = "# Title\n\n## Notes\n\nthis is markdown being recorded"
body = s.compose_body(code=md, language="markdown")
got = s.parse_snippet_fields("md_template", body)
assert got["notes"] == ""
assert got["code"] == md
def test_notes_after_code_that_itself_contains_the_heading():
md = "## Notes\ninside"
body = s.compose_body(code=md, language="markdown", notes="the real notes")
got = s.parse_snippet_fields("md_template", body)
assert got["notes"] == "the real notes"
assert got["code"] == md
def test_long_notes_are_chunked_apart_from_the_code():
"""The heading is the chunker's split point, so a long explanation gets
vectors of its own instead of averaging into the code's."""
from scribe.services.embeddings import chunk_document
long_notes = "\n\n".join(["An explanatory paragraph about the history. " * 12] * 4)
body = s.compose_body(code=CODE, language="python", when_to_use="a helper",
notes=long_notes)
chunks = chunk_document("helper — a helper", body)
assert len(chunks) > 1
assert "```python" in chunks[0]
assert "```python" not in chunks[-1]
# --- unit: the advice ---------------------------------------------------------
def test_a_situation_draws_no_advice():
assert s.trigger_advice("Debouncing a reactive input before it fetches.") is None
assert s.trigger_advice("") is None
assert s.trigger_advice(None) is None
def test_a_long_trigger_is_advised_toward_notes():
advice = s.trigger_advice("x" * (s.TRIGGER_ADVISE_CHARS + 1))
assert advice and "notes" in advice
assert f"{s.TRIGGER_ADVISE_CHARS + 1} characters" in advice
def test_a_headed_or_multi_paragraph_trigger_is_advised_even_when_short():
assert "headings" in s.trigger_advice("When adding a record.\n\n## Why\nbecause")
assert "paragraphs" in s.trigger_advice("When adding a record.\n\nAlso, history.")
# --- integration: every body rebuild carries the notes ------------------------
@pytest_asyncio.fixture
async def user_id(_dispose_engine):
from scribe.models import async_session
async with async_session() as session:
uid = (await ensure_user(session, "snippet_notes_itest")).id
await session.commit()
return uid
async def _fields(uid, note_id):
return s.snippet_fields(await s.get_snippet(uid, note_id))
@pytest.mark.integration
async def test_an_edit_to_another_field_keeps_the_notes(user_id):
note = await s.create_snippet(
user_id, name="notes_keep", code=CODE, language="python",
when_to_use="a helper", notes=NOTES,
)
assert (await _fields(user_id, note.id))["notes"] == NOTES
await s.update_snippet(user_id, note.id, when_to_use="a sharper situation")
got = await _fields(user_id, note.id)
assert got["notes"] == NOTES
assert got["when_to_use"] == "a sharper situation"
# And an empty string clears them, like every other field.
await s.update_snippet(user_id, note.id, notes="")
assert (await _fields(user_id, note.id))["notes"] == ""
@pytest.mark.integration
async def test_a_merge_keeps_the_survivors_notes(user_id):
survivor = await s.create_snippet(
user_id, name="notes_merge_a", code=CODE, language="python",
repo="R", path="a.py", symbol="helper", notes=NOTES,
)
source = await s.create_snippet(
user_id, name="notes_merge_b", code=CODE + " # variant", language="python",
repo="R", path="b.py", symbol="helper",
)
await s.merge_snippets(user_id, survivor.id, [source.id])
assert (await _fields(user_id, survivor.id))["notes"] == NOTES
await s.unmerge_snippet(user_id, survivor.id, source.id)
assert (await _fields(user_id, survivor.id))["notes"] == NOTES