fix(write-path): disclose cross-language prior art instead of hiding it
CI & Build / TypeScript typecheck (push) Failing after 2s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 16s
CI & Build / Python tests (push) Successful in 42s
CI & Build / Build & push image (push) Has been skipped

Closes #2244. Retrieval matches on concept, and concepts are language-agnostic:
asking about a TypeScript union-find scores 0.72-0.73 against a PYTHON snippet,
comfortably over the 0.68 bar. That is useful — a different-language solution
gives you the shape even when the code isn't reusable — but the menu line said
nothing about it, so the reader either dismissed a good structural reference or
pasted Python into a .ts file.

Worth noting this predates the concept-query change: raw TS code already matched
the Python snippet at 0.73, because the embedder reads identifiers and structure
semantically rather than syntactically. The fail state has been shipping quietly;
#2242 only made it an intended use rather than an accident.

- knowledge._note_to_item projects `language` from the data mirror, same shape as
  the existing verification projection — a plain column read, no body parsing.
- The semantic arm carries language through on the item it builds; it is the arm
  where these arise, since a snippet recorded AT the path you're editing is
  almost never in another language.
- _prior_art_line folds it into the marker: [similar 0.72 · python]. Together
  with the score rather than after the title, because the two jointly are the
  judgement being offered.
- One explanatory line is added to the menu, and only when something on it is
  actually tagged.

Two deliberate calls:

LABEL, DON'T FILTER. A stricter threshold for foreign-language hits would
suppress exactly the shape-borrowing this exists for. They were never the
problem; their being undisclosed was.

ONLY CLAIM A MISMATCH YOU CAN ESTABLISH. _foreign_language returns "" when either
side is unknown — unrecognised extension, or a snippet with no recorded language.
A wrong "· python" is worse than no tag. Same-language hits stay unlabelled, so
the common case keeps a clean line and the preamble stays off the menu entirely.
Operator-typed language names fold through an alias table first (py/python3 →
python, tsx → typescript, c++ → cpp); unrecognised names pass through lowercased,
which still makes an unknown-but-equal pair compare equal.

Trap found while building: _note() in the tests is a MagicMock, so `note.data`
auto-created a truthy mock that would have rendered its repr into a menu line.
Both test helpers now set data = None explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
2026-07-30 10:31:56 -04:00
co-authored by Claude Opus 5
parent 57781770c3
commit 390846a3d5
5 changed files with 226 additions and 10 deletions
+3
View File
@@ -22,6 +22,9 @@ def _note(nid, title, user_id=1):
n = MagicMock()
n.id, n.title, n.user_id = nid, title, user_id
n.note_type = "snippet"
# See the same note in test_write_path_trigger: an auto-created mock on
# `.data` is truthy and would leak into a rendered menu line (#2244).
n.data = None
return n
+92
View File
@@ -26,6 +26,10 @@ def _snippet_item(nid, title, user_id=1):
def _note(nid, title, user_id=1):
n = MagicMock()
n.id, n.title, n.user_id = nid, title, user_id
# Explicitly None, not left to MagicMock's auto-attribute: the semantic arm
# reads `note.data` for the snippet's language (#2244), and an auto-created
# mock there is truthy, so it would render its repr into the menu line.
n.data = None
return n
@@ -585,6 +589,94 @@ async def test_the_floor_judges_the_raw_payload_not_the_concept():
search2.assert_not_called()
# --- cross-language prior art is disclosed, not filtered (#2244) --------------
def test_language_for_path_and_canonicalisation():
from scribe.services import plugin_context as pc
assert pc._language_for_path("src/a/b.ts") == "typescript"
assert pc._language_for_path("src/a/b.py") == "python"
assert pc._language_for_path("ops/Dockerfile") == "dockerfile"
assert pc._language_for_path("README") == ""
assert pc._language_for_path("weird/thing.qqq") == ""
# Operator-typed spellings that mean the same thing must compare equal.
assert pc._canonical_language("Python3") == pc._canonical_language("py")
assert pc._canonical_language("TSX") == pc._canonical_language("typescript")
assert pc._canonical_language("C++") == "cpp"
# Unknown-but-equal still compares equal, which is all this must get right.
assert pc._canonical_language("Brainfuck") == pc._canonical_language("brainfuck")
def test_foreign_language_only_claims_a_mismatch_it_can_establish():
from scribe.services import plugin_context as pc
assert pc._foreign_language({"language": "python"}, "typescript") == "python"
assert pc._foreign_language({"language": "py"}, "python") == "" # same, aliased
assert pc._foreign_language({"language": ""}, "typescript") == "" # theirs unknown
assert pc._foreign_language({"language": "python"}, "") == "" # target unknown
assert pc._foreign_language({}, "typescript") == "" # no field at all
@pytest.mark.asyncio
async def test_a_cross_language_hit_is_labelled_and_explained():
"""The fail state this closes: an unlabelled Python hit offered while writing
TypeScript is either dismissed as irrelevant or pasted into the .ts file."""
from scribe.services import plugin_context as pc
note = _note(7, "group_pairs — collapse related pairs into groups")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[(0.72, note)])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
out = await pc.build_write_path_hint(1, "frontend/src/lib/cluster.ts", code=REAL_CODE)
assert "[similar 0.72 · python]" in out["context"]
assert "shape of a solution to adapt, not code to copy" in out["context"]
# Disclosed, NOT filtered — a stricter bar for foreign hits would suppress
# exactly the shape-borrowing this exists for.
assert out["note_ids"] == [7]
@pytest.mark.asyncio
async def test_a_same_language_hit_is_not_labelled_and_gets_no_preamble():
"""The common case keeps a clean line; the explanation only appears when
there is something on the menu it explains."""
from scribe.services import plugin_context as pc
note = _note(7, "debounce — rate-limit a callback")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[(0.72, note)])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
out = await pc.build_write_path_hint(1, "src/scribe/services/x.py", code=REAL_CODE)
assert "[similar 0.72]" in out["context"]
assert "·" not in out["context"]
assert "shape of a solution" not in out["context"]
@pytest.mark.asyncio
async def test_an_unknown_target_extension_never_invents_a_mismatch():
"""A wrong "· python" tag is worse than no tag at all."""
from scribe.services import plugin_context as pc
note = _note(7, "helper — does a thing")
note.data = {"language": "python"}
with patch.object(pc, "get_writepath_config", AsyncMock(return_value=_cfg())), \
patch.object(pc.snippets_svc, "list_snippets", AsyncMock(return_value=([], 0))), \
patch.object(pc, "semantic_search_notes", AsyncMock(return_value=[(0.72, note)])), \
patch.object(pc, "record_retrieval", MagicMock()), \
patch.object(pc, "owner_names_for", AsyncMock(return_value={})):
out = await pc.build_write_path_hint(1, "scripts/thing.qqq", code=REAL_CODE)
assert "· python" not in out["context"]
def test_prior_art_line_keeps_language_and_attribution_together():
from scribe.services.plugin_context import _prior_art_line
item = {"id": 12, "title": "group_pairs — collapse pairs"}
assert _prior_art_line(item, "similar 0.72", None, "python") == \
'> - #12 [similar 0.72 · python] "group_pairs — collapse pairs"'
both = _prior_art_line(item, "similar 0.72", "alex", "python")
assert "· python" in both and "shared by alex" in both
# --- the route between them --------------------------------------------------
def test_route_reads_every_arg_the_hook_sends():