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
+9
View File
@@ -211,6 +211,15 @@ def _note_to_item(note: Note) -> dict:
# body parsing and stays a plain projection of the column. Omitted entirely
# when unchecked, so "no key" and "never verified" don't become two states
# the client has to tell apart.
# Snippet language, same reasoning as the verdict below: a plain projection of
# the `data` mirror, no body parsing. Needed because a prior-art hit in a
# DIFFERENT language than the file being written is useful as the shape of a
# solution but must not be mistaken for code to paste (#2244) — and the caller
# can only say "different" if the language is on the item.
language = (note.data or {}).get("language") if note.data else None
if language:
item["language"] = language
verdict = (note.data or {}).get("verification") if note.data else None
if verdict and verdict.get("status"):
item["verification"] = {
+117 -10
View File
@@ -391,15 +391,98 @@ async def build_autoinject_hint(
# this exact file" is a stronger claim than "this resembles something".
def _prior_art_line(item: dict, marker: str, owner: str | None) -> str:
"""One menu line: `- #12 [here] "title"`, attributed when it isn't yours."""
def _prior_art_line(item: dict, marker: str, owner: str | None, foreign_lang: str = "") -> str:
"""One menu line: `- #12 [here] "title"`, attributed when it isn't yours.
A foreign language is folded into the marker (`[similar 0.72 · python]`)
rather than appended after the title, so the reader sees it while still
reading the score — the two together are the judgement being offered.
"""
title = (item.get("title") or "(untitled)").replace("\n", " ").strip()
line = f"> - #{item['id']} [{marker}] \"{title}\""
mark = f"{marker} · {foreign_lang}" if foreign_lang else marker
line = f"> - #{item['id']} [{mark}] \"{title}\""
if owner:
line += f" — shared by {owner}, treat as a suggestion"
return line
# --- cross-language prior art (#2244) ----------------------------------------
# Retrieval is concept-shaped now, and concepts are language-agnostic: a query
# about a TypeScript union-find matches a PYTHON snippet at 0.72-0.73, comfortably
# over the bar. That is a feature — the operator's framing is "borrow the shape of
# the solution even when the code isn't directly reusable" — but only if the line
# SAYS so. An unlabelled Python hit offered while writing TypeScript either gets
# dismissed as irrelevant or, worse, pasted into a .ts file. Measured note: this
# cross-language matching predates concept queries; it was always happening, just
# never disclosed.
#
# Deliberately NOT gated behind a stricter threshold for foreign-language hits: a
# higher bar would suppress exactly the shape-borrowing this is for. Label, don't
# filter.
_LANG_BY_EXT = {
"py": "python", "pyi": "python",
"ts": "typescript", "tsx": "typescript", "mts": "typescript", "cts": "typescript",
"js": "javascript", "jsx": "javascript", "mjs": "javascript", "cjs": "javascript",
"vue": "vue", "svelte": "svelte",
"go": "go", "rs": "rust", "rb": "ruby", "php": "php",
"java": "java", "kt": "kotlin", "kts": "kotlin", "scala": "scala",
"c": "c", "h": "c", "cc": "cpp", "cpp": "cpp", "cxx": "cpp", "hpp": "cpp",
"cs": "csharp", "swift": "swift", "m": "objectivec", "mm": "objectivec",
"sh": "shell", "bash": "shell", "zsh": "shell", "fish": "shell",
"sql": "sql", "css": "css", "scss": "scss", "less": "less",
"html": "html", "htm": "html", "yml": "yaml", "yaml": "yaml",
"toml": "toml", "ini": "ini", "dockerfile": "dockerfile",
"ex": "elixir", "exs": "elixir", "erl": "erlang", "hs": "haskell",
"lua": "lua", "pl": "perl", "r": "r", "dart": "dart", "zig": "zig",
}
# `language` on a snippet is operator-typed free text, so fold the spellings that
# mean the same thing before comparing. Anything unrecognised passes through
# lowercased — an unknown-but-equal pair still compares equal, which is the only
# thing this needs to get right.
_LANG_ALIASES = {
"py": "python", "python3": "python",
"ts": "typescript", "tsx": "typescript",
"js": "javascript", "jsx": "javascript", "node": "javascript",
"sh": "shell", "bash": "shell", "zsh": "shell", "shell-script": "shell",
"c++": "cpp", "cplusplus": "cpp", "c#": "csharp", "objective-c": "objectivec",
"golang": "go", "rs": "rust", "rb": "ruby", "yml": "yaml",
"postgres": "sql", "postgresql": "sql", "psql": "sql",
"vuejs": "vue", "vue3": "vue",
}
def _canonical_language(name: str) -> str:
"""Fold a free-text language name to a comparable token ("" if absent)."""
token = (name or "").strip().lower()
return _LANG_ALIASES.get(token, token)
def _language_for_path(path: str) -> str:
"""The language implied by a file path's extension ("" when unknown)."""
tail = (path or "").rsplit("/", 1)[-1].lower()
if tail.startswith("dockerfile"):
return "dockerfile"
if "." not in tail:
return ""
return _LANG_BY_EXT.get(tail.rsplit(".", 1)[-1], "")
def _foreign_language(item: dict, target: str) -> str:
"""The item's language when it DIFFERS from the target file's, else "".
Returns "" whenever either side is unknown: we can only claim a mismatch we
can actually establish, and a wrong "· python" tag is worse than no tag.
Same-language hits stay unlabelled so the common case keeps a clean line.
"""
if not target:
return ""
theirs = _canonical_language(item.get("language") or "")
if not theirs or theirs == target:
return ""
return theirs
def _concept_doc(code: str) -> str:
"""The first doc-ish prose in `code`: docstring, else JSDoc, else leading comments."""
m = _CONCEPT_PY_DOC.search(code)
@@ -619,7 +702,14 @@ async def build_write_path_hint(
continue
scored.append((
f"similar {score:.2f}",
{"id": int(note.id), "title": note.title, "user_id": note.user_id},
{
"id": int(note.id), "title": note.title, "user_id": note.user_id,
# Carried so the line can disclose a cross-language hit
# (#2244). The semantic arm is where these actually arise —
# a snippet recorded at the path you're editing is almost
# never in another language, but a concept match easily is.
"language": (note.data or {}).get("language") if note.data else None,
},
))
menu = (placed + scored)[:top_k]
@@ -631,19 +721,36 @@ async def build_write_path_hint(
if it.get("user_id") is not None and int(it["user_id"]) != user_id
})
target_lang = _language_for_path(path)
rendered: list[tuple[dict, str, str | None, str]] = []
for marker, item in menu:
owner_id = item.get("user_id")
owner = None
if owner_id is not None and int(owner_id) != user_id:
owner = owners.get(int(owner_id)) or "another user"
rendered.append((item, marker, owner, _foreign_language(item, target_lang)))
lines = [
f"> Prior art already recorded in Scribe for `{path}` — open one with "
"`get_snippet(id)` and reuse it rather than writing a fresh one-off "
"(titles only; shown once per session):",
]
# Say what a language tag MEANS, and only when one is actually on the menu.
# Without this the reader has to infer why "· python" is attached to a hit on
# a .ts file, and the two ways of guessing wrong are both bad: dismiss it as
# irrelevant, or paste Python into TypeScript. Retrieval matches on concept,
# so these are genuinely useful — as the SHAPE of a solution, not as code.
if any(lang for _i, _m, _o, lang in rendered):
lines.append(
"> A tagged language means that snippet is in a DIFFERENT language "
"than this file — it matched on what it does, so treat it as the "
"shape of a solution to adapt, not code to copy."
)
note_ids: list[int] = []
for marker, item in menu:
for item, marker, owner, foreign_lang in rendered:
note_ids.append(int(item["id"]))
owner_id = item.get("user_id")
owner = None
if owner_id is not None and int(owner_id) != user_id:
owner = owners.get(int(owner_id)) or "another user"
lines.append(_prior_art_line(item, marker, owner))
lines.append(_prior_art_line(item, marker, owner, foreign_lang))
# Split by arm, which is the whole reason this table exists. The place arm
# carries no score and so has no home in retrieval_logs; before #2085 a