fix: batch large-thread candidates + stop negative-caching uncapped names (closes task/chunk-large-thread-candidates)

Source: task/chunk-large-thread-candidates (plan/steward-linker-quality) — content.js sent all candidates in one call, the server capped at 100, and names past the cap were permanently null-cached and never retried — silently breaking large threads.

- content.js: resolve the per-scan ask set in batches of BATCH (100, <= server MAX_CANDIDATES) via a new pure Extract.chunk helper, so every candidate is actually answered and none is dropped by the resolver's slice.
- content.js: cache only keys actually present in a response (hasOwnProperty); cap-overflow / partial-error / resolver-down names are left out of `resolved` and retried next scan instead of stuck at null.
- extract.js: add pure chunk(arr, size) on the RSLExtract global; extract.test.cjs covers exact-multiple, remainder, under-size, empty, and lossless in-order concatenation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
paul
2026-06-23 12:28:08 +00:00
co-authored by Claude Opus 4.8
parent b0f7116493
commit 5a3abf6c89
3 changed files with 50 additions and 5 deletions
+24 -1
View File
@@ -2,7 +2,7 @@
// CJS so it can require() extract.js, which is a UMD script (also a content script).
const { test } = require("node:test");
const assert = require("node:assert/strict");
const { matchesIn, cueMatchesIn, albumMatchesIn, allMatchesIn, isSafeHttpUrl } = require("./extract.js");
const { matchesIn, cueMatchesIn, albumMatchesIn, allMatchesIn, isSafeHttpUrl, chunk } = require("./extract.js");
test("matchesIn finds Title-Case / ALL-CAPS runs, skips stopwords", () => {
// Note: "and" is a connector, so "X and Y" greedily joins into one run (by design,
@@ -277,6 +277,29 @@ test("isSafeHttpUrl accepts http/https and rejects javascript/data/garbage/empty
assert.ok(!isSafeHttpUrl(undefined));
});
test("chunk splits an array into <= size batches, in order, losslessly", () => {
// exact multiple: 200 -> [100, 100]
const a = Array.from({ length: 200 }, (_, i) => i);
const ca = chunk(a, 100);
assert.deepEqual(ca.map((b) => b.length), [100, 100]);
assert.deepEqual([].concat(...ca), a); // concatenation reproduces the input in order
// remainder: 250 -> [100, 100, 50]
const b = Array.from({ length: 250 }, (_, i) => i);
const cb = chunk(b, 100);
assert.deepEqual(cb.map((x) => x.length), [100, 100, 50]);
assert.deepEqual([].concat(...cb), b);
// under-size: 5 -> [5]
const c = [1, 2, 3, 4, 5];
const cc = chunk(c, 100);
assert.deepEqual(cc.map((x) => x.length), [5]);
assert.deepEqual([].concat(...cc), c);
// empty: [] -> []
assert.deepEqual(chunk([], 100), []);
});
test("allMatchesIn merges cue + Title-Case + album and sorts ascending by index", () => {
const text = `Check out devourment and Cattle Decapitation; the album "Human Jerky" rules`;
const ms = allMatchesIn(text);