You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/11 12:29:40 UTC

(maka) branch main updated: fix(core): scan fully for a recall term with an unpaired surrogate (#5422)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 882d0402c1 fix(core): scan fully for a recall term with an unpaired surrogate (#5422)
882d0402c1 is described below

commit 882d0402c1a8366aa20a45dd69f2e245d58abc0c
Author: Totoro <ms...@gmail.com>
AuthorDate: Sun Oct 11 20:29:36 2026 +0800

    fix(core): scan fully for a recall term with an unpaired surrogate (#5422)
    
    The candidate source searches the serialized transcript, where
    JSON.stringify writes a lone surrogate as a \u escape, and SQLite binds
    a lone-surrogate term as U+FFFD. A Recall term containing one therefore
    got an empty candidate list, which collectHits treats as authoritative,
    and a passage the verifier would match was silently dropped.
    
    Treat such a term like the other escaped characters and take the full
    scan. A well-formed surrogate pair is one code point under the u flag
    and keeps the candidate path.
    
    Generated-by: Claude Code
    test(core): pin a lone low surrogate in the recall full-scan guard
    
    Also say why a lone surrogate forces a full scan even where it is stored
    literally as half of a pair, name the term in the passage assertion, and
    note in recall-fold that its stable-record predicate relies on recall
    scanning escaped and surrogate terms fully.
    
    Generated-by: Claude Code
---
 packages/core/src/__tests__/recall.test.ts | 44 ++++++++++++++++++++++++++++++
 packages/core/src/recall.ts                | 13 ++++++++-
 packages/storage/src/recall-fold.ts        |  5 ++++
 3 files changed, 61 insertions(+), 1 deletion(-)

diff --git a/packages/core/src/__tests__/recall.test.ts b/packages/core/src/__tests__/recall.test.ts
index 63e97a12e6..10cf2a2a08 100644
--- a/packages/core/src/__tests__/recall.test.ts
+++ b/packages/core/src/__tests__/recall.test.ts
@@ -405,6 +405,50 @@ test('a term the stored form escapes bypasses the candidate source', async () =>
   assert.equal(asked, 0);
 });
 
+test('a term with an unpaired surrogate bypasses the candidate source', async () => {
+  // `JSON.stringify` writes a lone surrogate as a `\ud800` escape, and SQLite
+  // binds one as U+FFFD, so the candidate source cannot vouch for a term
+  // containing one. The parsed text still matches it.
+  const data = corpus([
+    {
+      session: session('s-lone', 'lone surrogate'),
+      messages: [userMessage('m-lone', 't-lone', 'abc\uD800def')],
+    },
+    {
+      session: session('s-emoji', 'emoji'),
+      messages: [userMessage('m-emoji', 't-emoji', 'ship it 🚀 today')],
+    },
+  ]);
+  for (const [term, anchor] of [
+    ['\uD800', 'm-lone'],
+    ['c\uD800d', 'm-lone'],
+    // Either half of the pair in the stored emoji: this double finds it, SQLite
+    // does not.
+    ['\uD83D', 'm-emoji'],
+    ['\uDE80', 'm-emoji'],
+  ] as const) {
+    const result = await runRecall({ terms: [term] }, candidateDeps(data));
+    assert.ok(result.ok);
+    assert.equal(
+      result.scannedFully,
+      true,
+      `${JSON.stringify(term)} must not use the candidate source`,
+    );
+    assert.deepEqual(
+      anchorIds(result.passages),
+      [anchor],
+      `${JSON.stringify(term)} must return its passage`,
+    );
+  }
+
+  // A surrogate pair is one character, stored literally, so the candidate
+  // source can still vouch for it.
+  const paired = await runRecall({ terms: ['🚀'] }, candidateDeps(data));
+  assert.ok(paired.ok);
+  assert.equal(paired.scannedFully, false);
+  assert.deepEqual(anchorIds(paired.passages), ['m-emoji']);
+});
+
 test('a dense tool result does not outrank the answer that explains it', async () => {
   const data = mixedCorpus();
   // `m3` is shaped like a grep result: forty repetitions of every term. BM25's
diff --git a/packages/core/src/recall.ts b/packages/core/src/recall.ts
index 29470f5b1e..30116cf4c8 100644
--- a/packages/core/src/recall.ts
+++ b/packages/core/src/recall.ts
@@ -133,13 +133,24 @@ const JSON_ESCAPED_PRINTABLE_PATTERN = /["\\]/u;
 /** First code point `JSON.stringify` leaves unescaped. */
 const FIRST_UNESCAPED_CODE_POINT = 0x20;
 
+/**
+ * A surrogate without its partner. `JSON.stringify` writes one as a `\u`
+ * escape, and a store binding the term as UTF-8 cannot represent it at all, so
+ * not even a stored pair it is half of is offered. Under the `u` flag a
+ * well-formed pair is one code point, so an emoji does not match.
+ */
+const UNPAIRED_SURROGATE_PATTERN = /\p{Cs}/u;
+
 /**
  * A term containing a character that `JSON.stringify` escapes is stored in a
  * different literal form than it was typed, so a candidate source scanning
- * serialized records would under-select it. Such terms force a full scan.
+ * serialized records would under-select it. A lone surrogate also cannot be
+ * bound as UTF-8, so such a scan misses it even where it is stored literally,
+ * as half of a pair. Such terms force a full scan.
  */
 function hasJsonEscapedCharacter(term: string): boolean {
   if (JSON_ESCAPED_PRINTABLE_PATTERN.test(term)) return true;
+  if (UNPAIRED_SURROGATE_PATTERN.test(term)) return true;
   for (const character of term) {
     if ((character.codePointAt(0) ?? 0) < FIRST_UNESCAPED_CODE_POINT) return true;
   }
diff --git a/packages/storage/src/recall-fold.ts b/packages/storage/src/recall-fold.ts
index a36e7927e1..2be52c26ff 100644
--- a/packages/storage/src/recall-fold.ts
+++ b/packages/storage/src/recall-fold.ts
@@ -31,6 +31,11 @@ import type { DatabaseSync } from 'node:sqlite';
  * that NFC rewrites. On stable records `instr(lower(stored), term)` is exactly
  * recall's predicate, and on unstable ones the record is a candidate anyway,
  * so the scan never under-selects whatever script the transcript is in.
+ *
+ * That holds for terms the serialized record spells the way they were typed.
+ * A term with a character `JSON.stringify` escapes, or a lone surrogate, which
+ * cannot be bound at all, never reaches this scan: recall scans those fully
+ * (`hasJsonEscapedCharacter` in core `recall.ts`).
  */
 export const RECALL_FOLD_UNSTABLE_FUNCTION = 'maka_recall_fold_unstable';
 


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org