Make corroboration depend on the claim, not on the size of the library

An independent reviewer broke the quarantine rule twice by running it. In a
two-atom library, a second source repeating the claim word for word could not
vouch for it; 40 unrelated memories could. The old rule handed the choice of
words to rare_terms, which drops words common to the corpus, so the corpus size
and the attack's own words decided whether a stranger was believed.

Corroboration now demands the claim's three longest words, with the instruction
phrases stripped out, ANDed by the new text_index.fts_all_expression. Neither
the library's size nor the attack can flip it, and both cases are tests.

Also from that review:
- A question is a read. The DF cache no longer remembers a word the corpus has
  never used, so an unauthenticated search cannot grow the store one row per
  novel word. "A question is not a write" was false for /api/search and /api/ask.
- The prompt now states that packed evidence is quoted material, not an
  instruction: once a quarantined sentence is corroborated it does reach the model.
- ForgetReport's semantic_cache_cleared reached the frontend as well as Go.
- runtime.py: process_vision_info returns a third element only when
  return_video_kwargs is set, and this call does not set it, so the unpack takes
  the first two. Two pre-existing pyright errors go, behaviour is unchanged.

CHECKLIST corrected against measurement: 13 renamed lines rather than 11, and
10 English / 9 Chinese injection signals rather than 11 / 8.

Gates: pytest 312 passed, ruff clean, mypy 24 errors in 9 files (net zero),
pyright 0 errors on the five touched files, npm build and eslint clean,
scripts/mcp_write_smoke.sh PASS.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 00:37:12 -04:00
1 parent 9145a23f18
commit 203b6d04af
8 files changed
+123 -26

No files matched your search

+20 -4
View File
@@ -54,9 +54,23 @@ def fts_expression(terms: list[str]) -> str:
"""
if not terms:
raise ValueError("fts_expression needs at least one term")
if len(terms) == 1:
return _cjk_phrase(terms[0]) if _is_cjk(terms[0]) else f"{_quote(terms[0])}*"
return " OR ".join(_cjk_phrase(term) if _is_cjk(term) else f"{_quote(term)}*" for term in terms)
return " OR ".join(_term_phrase(term) for term in terms)
def fts_all_expression(terms: list[str]) -> str:
"""Build the MATCH expression that demands every term.
A question ORs because it is longer than the sentence that answers it. Corroboration ANDs: a
second source either carries the claim's own words or it does not, and an OR would let one
shared word vouch for a whole sentence.
"""
if not terms:
raise ValueError("fts_all_expression needs at least one term")
return " AND ".join(_term_phrase(term) for term in terms)
def _term_phrase(term: str) -> str:
return _cjk_phrase(term) if _is_cjk(term) else f"{_quote(term)}*"
def fts_query(query: str) -> str:
@@ -115,7 +129,9 @@ def _term_matches(connection: sqlite3.Connection, expression: str, cap: int) ->
(expression, cap),
).fetchone()[0]
)
connection.execute("INSERT OR REPLACE INTO term_df(term, documents) VALUES (?, ?)", (expression, matches))
if matches:
# A word the corpus has never used is not remembered: a stranger's vocabulary would grow the store one row per novel question.
connection.execute("INSERT OR REPLACE INTO term_df(term, documents) VALUES (?, ?)", (expression, matches))
return matches