feat(retrieval): a menu line is a name, its kind and System, and the whole passage that matched (#4364)
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 58s
CI & Build / Python tests (push) Successful in 1m36s
CI & Build / Build & push image (push) Successful in 26s

Injected lines rendered a snippet's or lesson's title, which carries its
whole trigger by construction (the embedding shape) and ran past 1,500
characters -- again on every `seen` repeat. The passage under a line was
cut to 200 chars from the middle, keeping its head (the title again) and
losing where the match was.

Now, on both the prompt menu and the write-path prior-art menu:
- the line shows the record's NAME (snippet data.name / lesson subject),
  with its kind and System (`[issue (done) · Plugin & hooks]`);
- the passage is the whole matched chunk, title prefix stripped, on one
  line so the blockquote holds; a title-only match hands over the trigger;
- a `seen` record is a one-line pointer to what is already in context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 16:29:48 -04:00
co-authored by Claude Opus 5.5
parent 20227ebb5d
commit bb632c4196
4 changed files with 262 additions and 46 deletions
+93 -1
View File
@@ -12,13 +12,18 @@ from __future__ import annotations
import json
import subprocess
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from scribe.services.plugin_context import (
_AUTOINJECT_CONTEXT_MAX,
_AUTOINJECT_CONTEXT_PROMPT_MAX,
_autoinject_query,
_menu_name,
_menu_passage,
)
from tests.helpers import need_tools
from tests.helpers import fake_note, need_tools
DEFS = Path(__file__).resolve().parents[1] / "plugin" / "hooks" / "scribe_defs.sh"
@@ -96,3 +101,90 @@ def test_the_hook_caps_what_it_sends(tmp_path):
def test_no_transcript_is_silence_not_failure(tmp_path):
assert recent(tmp_path, []) == ""
# --- what a menu line carries (#4364) ----------------------------------------
#
# The name, the kind and System, and the WHOLE matched passage — once. A repeat
# is a pointer. These pin the shape against the three ways it had gone wrong:
# a trigger-composed title rendered as the line (1,500+ chars), the passage cut
# to its head (which was the title again), and a `seen` repeat re-rendered whole.
TRIGGER = "Adding a record type that is semantically searchable. " * 20
def test_a_snippet_line_shows_its_name_not_its_trigger():
assert _menu_name(f"embed_x — {TRIGGER}", "snippet", {"name": "embed_x"}) == "embed_x"
# With no mirror, the first separator is the seam (snippets.py's inverse).
assert _menu_name(f"embed_x — {TRIGGER}", "snippet", None) == "embed_x"
def test_a_lesson_line_shows_its_subject_not_its_trigger():
title = f"A guard does not undo a stored value — {TRIGGER.strip()}"
name = _menu_name(title, "lesson", {"when_to_apply": TRIGGER.strip()})
assert name == "A guard does not undo a stored value"
def test_a_plain_note_keeps_its_title_dashes_and_all():
t = "Dev-log 2026-07-29 — milestone #232 closed"
assert _menu_name(t, "note", None) == t
def test_the_passage_is_the_whole_chunk_without_its_title_prefix():
body = "section " * 150 # ~1.2 KB: nothing of it is cut
out = _menu_passage("Pool sizing", f"Pool sizing\n{body}\nsecond line")
assert not out.startswith("Pool sizing")
assert out.endswith("second line") and "\n" not in out
assert len(out) > 1100
def test_a_title_only_match_hands_over_the_trigger_it_matched_on():
title = "embed_x — when adding a searchable record"
assert _menu_passage(title, title, "embed_x") == "when adding a searchable record"
async def _menu(hits, seen, chunks, systems=None):
from scribe.services import plugin_context as pc
calls: list[int] = []
async def _search(*_a, **kw):
calls.append(1)
if len(calls) > 1:
return []
if kw.get("report") is not None:
kw["report"]["best_chunk"] = chunks
return hits
with patch.object(pc, "get_autoinject_config",
AsyncMock(return_value={"enabled": True, "threshold": 0.55, "top_k": 3})), \
patch.object(pc, "semantic_search_notes", _search), \
patch.object(pc, "superseded_ids", AsyncMock(return_value=set())), \
patch.object(pc, "system_names_for", AsyncMock(return_value=systems or {})), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "record_surfaced", MagicMock()):
return (await pc.build_autoinject_hint(1, "q", project_id=2, exclude_ids=seen))["context"]
@pytest.mark.asyncio
async def test_a_first_sighting_carries_name_system_and_passage():
title = f"embed_x — {TRIGGER}"
hits = [(0.8, fake_note(id=11, title=title, note_type="snippet",
data={"name": "embed_x"}, user_id=1))]
out = await _menu(hits, [], {11: {"index": 1, "text": f"{title}\nthe matched section"}},
systems={11: ["Retrieval & recall"]})
line = next(ln for ln in out.splitlines() if "#11" in ln)
assert '[snippet · Retrieval & recall] "embed_x"' in line
assert TRIGGER[:40] not in line
assert "> ↳ the matched section" in out
@pytest.mark.asyncio
async def test_a_seen_record_is_a_pointer_not_a_copy():
title = f"embed_x — {TRIGGER}"
hits = [(0.8, fake_note(id=11, title=title, note_type="snippet",
data={"name": "embed_x"}, user_id=1))]
out = await _menu(hits, [11], {11: {"index": 1, "text": f"{title}\nthe matched section"}})
line = next(ln for ln in out.splitlines() if "#11" in ln)
assert line == "> - #11 [snippet · seen] embed_x"
assert "" not in out