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:
@@ -10,12 +10,11 @@ form and the thing that gets embedded, so a snippet inherits everything a note
|
||||
has (embeddings, ACL, project/System association, dedup) and, crucially, becomes
|
||||
eligible for semantic recall the moment it's embedded:
|
||||
|
||||
- ``title`` = ``"{name} — {when_to_use}"``. The title is exactly what the
|
||||
title-first auto-inject surfaces, so this one line self-describes the snippet
|
||||
in a recall menu.
|
||||
- ``title`` = ``name``. The trigger joins it only in the embedded document
|
||||
(``embeddings.document_title``, milestone 427).
|
||||
- ``tags`` = ``[language, "snippet", *caller_tags]``.
|
||||
- ``body`` = templated markdown (When to use / Signature / Location, then a
|
||||
fenced code block).
|
||||
fenced code block, then an optional ``## Notes`` section).
|
||||
|
||||
Since migration 0070 the same fields are ALSO written to ``notes.data`` (see
|
||||
``compose_data``) — a queryable mirror, not a second source of truth: it carries
|
||||
@@ -45,6 +44,10 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
SNIPPET_NOTE_TYPE = "snippet"
|
||||
SNIPPET_TAG = "snippet"
|
||||
# The body section free-text notes live under (#4378). A heading rather than a
|
||||
# `**Notes:**` line because notes are paragraphs, and a heading is the boundary
|
||||
# `embeddings.chunk_document` splits at.
|
||||
NOTES_HEADING = "## Notes"
|
||||
|
||||
# Sentinel for "argument not supplied" on update, so None stays available as a
|
||||
# real value meaning "clear this". Needed for project_id, where 0 is not a valid
|
||||
@@ -192,10 +195,19 @@ def compose_body(
|
||||
symbol: str = "",
|
||||
locations: list[dict] | None = None,
|
||||
merged_from: list[int] | None = None,
|
||||
notes: str = "",
|
||||
) -> str:
|
||||
"""Render structured fields into the snippet body markdown. Empty fields are
|
||||
omitted so the body stays clean.
|
||||
|
||||
``notes`` is the free text a snippet had no home for (#4378): why it is
|
||||
shaped this way, what superseded what, the caveats. Without it that prose
|
||||
went into ``when_to_use`` — the trigger, which is joined onto the title of
|
||||
EVERY chunk the snippet is embedded as, so a 3 KB write-up there blurred
|
||||
the one line the snippet is ranked on. It goes AFTER the code under its own
|
||||
heading: short, it shares the snippet's one chunk; long, the chunker splits
|
||||
it off at the heading into vectors of its own.
|
||||
|
||||
Locations: pass ``locations`` (a list of {repo,path,symbol}) for the general
|
||||
multi-location case; the single ``repo``/``path``/``symbol`` params remain as
|
||||
a back-compat shorthand for one location and are used only when ``locations``
|
||||
@@ -224,9 +236,10 @@ def compose_body(
|
||||
)
|
||||
fence_lang = (language or "").strip().lower()
|
||||
code_block = f"```{fence_lang}\n{(code or '').rstrip()}\n```"
|
||||
tail = f"\n\n{NOTES_HEADING}\n\n{notes.strip()}\n" if (notes or "").strip() else "\n"
|
||||
if header:
|
||||
return "\n\n".join(header) + "\n\n" + code_block + "\n"
|
||||
return code_block + "\n"
|
||||
return "\n\n".join(header) + "\n\n" + code_block + tail
|
||||
return code_block + tail
|
||||
|
||||
|
||||
# --- parse: note -> structured fields (best-effort, never raises) ------------
|
||||
@@ -240,6 +253,7 @@ _LOCS_RE = re.compile(
|
||||
_MERGED_RE = re.compile(r"^\*\*Merged from:\*\*\s*(.+?)\s*$", re.MULTILINE)
|
||||
_CODE_RE = re.compile(r"```([\w+.#-]*)\n(.*?)\n```", re.DOTALL)
|
||||
_ID_RE = re.compile(r"#(\d+)")
|
||||
_NOTES_RE = re.compile(r"^## Notes[ \t]*\n(.*)\Z", re.MULTILINE | re.DOTALL)
|
||||
|
||||
|
||||
def _parse_location_str(s: str) -> dict | None:
|
||||
@@ -289,6 +303,7 @@ def parse_snippet_fields(
|
||||
"locations": [],
|
||||
"merged_from": [],
|
||||
"code": "",
|
||||
"notes": "",
|
||||
}
|
||||
|
||||
m = _WHEN_RE.search(body)
|
||||
@@ -331,6 +346,11 @@ def parse_snippet_fields(
|
||||
if m:
|
||||
fields["language"] = m.group(1).strip()
|
||||
fields["code"] = m.group(2)
|
||||
# Searched only AFTER the code, so a `## Notes` line inside the code
|
||||
# (a markdown snippet) is never read as the notes section.
|
||||
n = _NOTES_RE.search(body, m.end())
|
||||
if n:
|
||||
fields["notes"] = n.group(1).strip()
|
||||
|
||||
# Language fallback for a body whose code fence lost its language. Only the
|
||||
# FIRST tag can be trusted: compose_tags emits [language, "snippet", *caller],
|
||||
@@ -545,6 +565,40 @@ def compose_data(
|
||||
return out
|
||||
|
||||
|
||||
# Past this, a trigger is advised to move its explanation into `notes` (#4378).
|
||||
# The trigger is joined onto the title of EVERY chunk (`chunk_document`), so at
|
||||
# 600 characters it is already over 40% of a 1,400-character chunk — each
|
||||
# vector is then more about the trigger's prose than about the code or the
|
||||
# section it carries. A situation stated in a sentence or two sits well under.
|
||||
TRIGGER_ADVISE_CHARS = 600
|
||||
|
||||
|
||||
def trigger_advice(when_to_use: str | None) -> str | None:
|
||||
"""A nudge when `when_to_use` reads like a write-up rather than a situation,
|
||||
or None. Advice, not a refusal: the write has already happened, and a long
|
||||
trigger can be deliberate — several distinct situations, each one a moment
|
||||
the snippet should surface. What it catches is the explanation that had
|
||||
nowhere else to go before `notes` existed."""
|
||||
text = (when_to_use or "").strip()
|
||||
if not text:
|
||||
return None
|
||||
reasons = []
|
||||
if len(text) > TRIGGER_ADVISE_CHARS:
|
||||
reasons.append(f"it is {len(text)} characters")
|
||||
if re.search(r"^#{1,6}\s", text, re.MULTILINE):
|
||||
reasons.append("it has headings")
|
||||
elif "\n\n" in text:
|
||||
reasons.append("it runs to several paragraphs")
|
||||
if not reasons:
|
||||
return None
|
||||
return (
|
||||
f"when_to_use reads like a write-up ({', '.join(reasons)}). It is joined "
|
||||
"onto every chunk this snippet is ranked by, so keep it to the situation "
|
||||
"the snippet is for — a sentence or two — and move the explanation "
|
||||
"(why, history, caveats) into `notes` with update_snippet."
|
||||
)
|
||||
|
||||
|
||||
def recompose_data(note) -> dict:
|
||||
"""Rebuild a snippet's `data` mirror from its own body, title and tags.
|
||||
|
||||
@@ -718,6 +772,7 @@ async def create_snippet(
|
||||
tags: list[str] | None = None,
|
||||
project_id: int | None = None,
|
||||
commit_sha: str = "",
|
||||
notes: str = "",
|
||||
):
|
||||
"""Create a snippet note (embedded on create for immediate recall). Returns
|
||||
the created Note. Pass ``locations`` for the multi-location case; the single
|
||||
@@ -732,7 +787,7 @@ async def create_snippet(
|
||||
title=name.strip(),
|
||||
body=compose_body(
|
||||
code=code, language=language, signature=signature,
|
||||
when_to_use=when_to_use, locations=locations,
|
||||
when_to_use=when_to_use, locations=locations, notes=notes,
|
||||
),
|
||||
note_type=SNIPPET_NOTE_TYPE,
|
||||
tags=compose_tags(language, tags),
|
||||
@@ -822,6 +877,7 @@ async def update_snippet(
|
||||
tags: list[str] | None = None,
|
||||
project_id: int | None | object = UNSET,
|
||||
commit_sha: str | None = None,
|
||||
notes: str | None = None,
|
||||
):
|
||||
"""Partial update: only fields passed (not None) change. Re-serializes the
|
||||
merged field set back into title/body/tags. Returns the Note, or None if the
|
||||
@@ -858,7 +914,7 @@ async def update_snippet(
|
||||
cur = snippet_fields(note)
|
||||
overlay = {
|
||||
"name": name, "code": code, "language": language,
|
||||
"signature": signature, "when_to_use": when_to_use,
|
||||
"signature": signature, "when_to_use": when_to_use, "notes": notes,
|
||||
}
|
||||
merged = {**cur, **{k: v for k, v in overlay.items() if v is not None}}
|
||||
|
||||
@@ -894,6 +950,7 @@ async def update_snippet(
|
||||
# Carried, never set here: an ordinary edit must not erase the record
|
||||
# of what was folded in, and only a merge may add to it.
|
||||
merged_from=merged.get("merged_from"),
|
||||
notes=merged.get("notes") or "",
|
||||
),
|
||||
# Re-derived from the same merged field set as the body, so an edit can't
|
||||
# leave the indexed mirror describing the previous version.
|
||||
@@ -1459,6 +1516,9 @@ async def merge_snippets(user_id: int, target_id: int, source_ids: list[int]):
|
||||
code=tgt_fields["code"], language=tgt_fields["language"],
|
||||
signature=tgt_fields["signature"], when_to_use=tgt_fields["when_to_use"],
|
||||
locations=locations, merged_from=merged_from,
|
||||
# The survivor's own notes; a source's go to the trash with it and
|
||||
# come back on un-merge, like its code.
|
||||
notes=tgt_fields.get("notes") or "",
|
||||
),
|
||||
tags=compose_tags(tgt_fields["language"], extra_tags),
|
||||
# The survivor's location set grew, so its mirror has to grow with it —
|
||||
@@ -1579,6 +1639,7 @@ async def unmerge_snippet(user_id: int, survivor_id: int, source_id: int):
|
||||
code=fields["code"], language=fields["language"],
|
||||
signature=fields["signature"], when_to_use=fields["when_to_use"],
|
||||
locations=kept_locations, merged_from=remaining,
|
||||
notes=fields.get("notes") or "",
|
||||
),
|
||||
tags=compose_tags(fields["language"], kept_extra),
|
||||
data=compose_data(
|
||||
|
||||
Reference in New Issue
Block a user