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