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
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:
@@ -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
|
||||
Reference in New Issue
Block a user