clearotron 0.4.0 → 0.4.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/bin/README.md +1 -1
- package/bin/clearotron.mjs +2 -0
- package/bin/mcp.mjs +80 -0
- package/bin/start.mjs +14 -1
- package/build-info.json +2 -2
- package/demo/README.md +1 -1
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -3
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +5 -4
- package/docs/architecture/08-development-guide.md +3 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +29 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/compare.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-audit.mjs +6 -6
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +18 -17
- package/driver/contract-vocabulary.mjs +145 -132
- package/driver/declination-call.mjs +5 -1
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dev-portal.mjs +2 -1
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +8 -3
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +32 -5
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/mcp/supplemental.mjs +26 -1
- package/driver/engine/openai-agent.mjs +113 -20
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +4 -4
- package/driver/flag-snapshot.mjs +2 -1
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +96 -14
- package/driver/jx.mjs +1 -1
- package/driver/knockout-review-record.mjs +55 -1
- package/driver/named-band.mjs +4 -0
- package/driver/owner-screen.mjs +7 -4
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +15 -13
- package/driver/pipeline.mjs +132 -48
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +6 -5
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/products.mjs +1 -1
- package/driver/profiles.mjs +4 -3
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +3 -3
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/publish/xlsx.mjs +1 -1
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +20 -3
- package/driver/register-coverage.mjs +1 -1
- package/driver/register-plan.mjs +83 -18
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/result-noun-fields.mjs +3 -3
- package/driver/scope-facts.mjs +11 -3
- package/driver/scope-rules.mjs +2 -2
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +36 -1
- package/driver/stages-knockout.mjs +6 -5
- package/driver/stages.mjs +30 -30
- package/driver/suite-census.json +259 -55
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +7 -7
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/CONNECT.md +9 -3
- package/mcp-server/demo-mode.mjs +10 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/lib/runs.mjs +7 -1
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/mcp-server/packs/README.md +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +4 -3
- package/portal-ui/package.json +3 -3
- package/providers/_shared/count.mjs +9 -2
- package/providers/_shared/enumerate.mjs +125 -41
- package/providers/_shared/execute-plan.mjs +15 -4
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +32 -2
- package/providers/signa/src/core.js +262 -36
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +17 -2
- package/scripts/env-audit.mjs +3 -2
- package/scripts/freeze-example-run.mjs +10 -10
- package/scripts/mcp-registry-listing.mjs +199 -0
- package/scripts/mint-plugin-folder.mjs +65 -0
- package/scripts/release-version.mjs +44 -0
- package/scripts/score.mjs +1 -1
- package/shared/register-floor.mjs +19 -0
- package/shared/scope.mjs +18 -2
- package/shared/trigger-lane.mjs +1 -1
- package/skills/clearotron-ops/SKILL.md +81 -24
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
import {
|
|
55
55
|
BATCH_SCREEN_CHUNK, chunk, classifyStatus, isAllClass, normalizeBrandRow, screenVerdict,
|
|
56
56
|
} from "./screen.mjs";
|
|
57
|
-
import { makeCountProbe, parseToolText, isToolError } from "./count.mjs";
|
|
57
|
+
import { makeCountProbe, parseToolText, isToolError } from "./count.mjs"; import { floorOf, moreThan } from "../../shared/register-floor.mjs";
|
|
58
58
|
import { guardCountCall, guardToolCall } from "./transport-guard.mjs";
|
|
59
59
|
import { clipProviderText } from "./provider-text.mjs"; // — keep the discriminator
|
|
60
60
|
|
|
@@ -220,6 +220,13 @@ export function makeEnumerate(deps) {
|
|
|
220
220
|
// against the tuned ceiling below and can only ever narrow it — `CLEAROTRON_ENUMERATE_CEILING` cannot
|
|
221
221
|
// raise a band past a window the vendor answers with an HTTP 400.
|
|
222
222
|
ceilingFor = null,
|
|
223
|
+
// ── HOW MANY NAMES ONE REQUEST CARRIES, WHEN THAT IS A PROPERTY OF THE QUERY ──────────────────
|
|
224
|
+
//
|
|
225
|
+
// `namesChunkDefault` models a provider with one width for every names band. A register can take a
|
|
226
|
+
// list for one kind of question and not another — signa takes up to 100 exact spellings as one ranked
|
|
227
|
+
// list, and one term per request for everything else. `(params) => number|null`; `null` means the
|
|
228
|
+
// shape declares nothing and the default (or `CLEAROTRON_ENUMERATE_NAMES_CHUNK`) stands.
|
|
229
|
+
namesChunkFor = null,
|
|
223
230
|
} = deps;
|
|
224
231
|
|
|
225
232
|
// ── the I/O seam: a network REJECTION degrades like a 503, it does not abort the stage ────────────
|
|
@@ -249,6 +256,10 @@ export function makeEnumerate(deps) {
|
|
|
249
256
|
// error on the count probe rather than as a countable total. Optional: a provider that does not
|
|
250
257
|
// declare one keeps the previous behaviour exactly.
|
|
251
258
|
cardinalityRefusal = null,
|
|
259
|
+
// — the provider's own words for refusing a LIST of spellings it would answer one by one (a ranked list
|
|
260
|
+
// judged too complex to run). Such a list is asked again one spelling at a time, as it was before lists
|
|
261
|
+
// were sent at all, never reported as a search that could not be made. Optional: absent, nothing changes.
|
|
262
|
+
listRefusal = null,
|
|
252
263
|
} = capabilities;
|
|
253
264
|
|
|
254
265
|
if (countProbe === "endpoint" && typeof count !== "function") {
|
|
@@ -258,8 +269,7 @@ export function makeEnumerate(deps) {
|
|
|
258
269
|
throw new Error('[enumerate-kernel] capabilities.screenSource === "search-row" requires a rowScreen() dependency');
|
|
259
270
|
}
|
|
260
271
|
|
|
261
|
-
const crowdReason = (total, ceiling) =>
|
|
262
|
-
`total_hits ${total} exceeds the enumerate ceiling ${ceiling} — this is a CROWD, not a named exact/near band. Record it as a count+sample descriptor and hand it up to judgment; the funnel does NOT narrow-and-retry a crowd here. Whether a narrower NAMED enumeration is warranted, and whether this slice is material, is judgment's call (Layer B) — never accept it as clean.`;
|
|
272
|
+
const crowdReason = (total, ceiling) => `total_hits ${total} exceeds the enumerate ceiling ${ceiling} — ${CROWD_IS}`;
|
|
263
273
|
|
|
264
274
|
// ── count-first per-term rescue ───────────────────────────────────────────────────────────────────
|
|
265
275
|
// A multi-name OR-stack that crowds over the ceiling used to return ONE blind `incomplete` — a rare
|
|
@@ -288,19 +298,19 @@ export function makeEnumerate(deps) {
|
|
|
288
298
|
// asks a register "how many", so the two can never drift. `null` on any failure is preserved
|
|
289
299
|
// exactly: a term whose probe failed is dispositioned `error`, never `verified-zero`.
|
|
290
300
|
const countHits = makeCountProbe({ search, count, capabilities: { countProbe }, cheapCountParams });
|
|
291
|
-
const probeTermCount = async (auth, params, term, tctx) => {
|
|
292
|
-
const c = await countHits(auth, { ...params, [namesKey]: [term] }, tctx);
|
|
293
|
-
return c.ok ? c.total : null;
|
|
294
|
-
};
|
|
295
301
|
|
|
296
|
-
|
|
302
|
+
// `floor` is the register's "at least N" when it answered the whole stack with one instead of a count
|
|
303
|
+
// (shared/register-floor.mjs): the stack's total then stays null and the figure rides beside it.
|
|
304
|
+
async function countFirstRescue(auth, params, names, ceiling, stackTotal, tctx, incomplete, floor = null) {
|
|
297
305
|
const term_counts = {};
|
|
298
306
|
for (const t of names) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
307
|
+
const probe = await countHits(auth, { ...params, [namesKey]: [t] }, tctx);
|
|
308
|
+
const n = probe.ok ? probe.total : null;
|
|
309
|
+
const said = countsOf(probe); // what this spelling's own count counts, when the register said
|
|
310
|
+
if (n == null) { term_counts[t] = floorCount(probe) ?? { total_hits: null, disposition: "error" }; continue; }
|
|
311
|
+
if (n === 0) term_counts[t] = { total_hits: 0, disposition: "verified-zero", ...said };
|
|
312
|
+
else if (n > ceiling) term_counts[t] = { total_hits: n, disposition: "crowd", ...said };
|
|
313
|
+
else term_counts[t] = { total_hits: n, disposition: "unenumerated", ...said };
|
|
304
314
|
}
|
|
305
315
|
const records = [];
|
|
306
316
|
const tally = { "verified-zero": 0, enumerated: 0, crowd: 0, unenumerated: 0, error: 0 };
|
|
@@ -317,13 +327,14 @@ export function makeEnumerate(deps) {
|
|
|
317
327
|
// the identical question, that is the best case a matter can have (a mark nobody has registered)
|
|
318
328
|
// producing a run that searches almost nothing. The ordinary search path has always returned
|
|
319
329
|
// `enumerated` for a zero-record answer; this rescue path was the one place that did not.
|
|
320
|
-
|
|
330
|
+
// A stack the register answered with a floor is never a complete zero, whatever its spellings say.
|
|
331
|
+
if (unresolved === 0 && floor === null) {
|
|
321
332
|
// every term verified zero ⇒ nobody has filed any of these names: a complete band whose answer is zero.
|
|
322
333
|
return { type: "text", text: JSON.stringify({ state: "enumerated", total_hits: stackTotal, count: records.length, records, term_counts }, null, 2) };
|
|
323
334
|
}
|
|
324
|
-
return incomplete(stackTotal, records.length, records,
|
|
325
|
-
`stack total_hits ${stackTotal} exceeds the enumerate ceiling ${ceiling}; each spelling was counted and none was read (${names.length} terms: ${tally["verified-zero"]} verified-zero, ${tally.unenumerated} counted and not read, ${tally.crowd} crowd, ${tally.error} error). term_counts carries each spelling's count: which to read, narrow or leave is the reading step's decision, and a populated term is never recorded 0.`,
|
|
326
|
-
{ term_counts, records });
|
|
335
|
+
return incomplete(floor === null ? stackTotal : null, records.length, records,
|
|
336
|
+
`stack total_hits ${floor === null ? stackTotal : moreThan(floor)} exceeds the enumerate ceiling ${ceiling}; each spelling was counted and none was read (${names.length} terms: ${tally["verified-zero"]} verified-zero, ${tally.unenumerated} counted and not read, ${tally.crowd} crowd, ${tally.error} error). term_counts carries each spelling's count: which to read, narrow or leave is the reading step's decision, and a populated term is never recorded 0.`,
|
|
337
|
+
{ term_counts, records, ...floorStamp(floor) });
|
|
327
338
|
}
|
|
328
339
|
|
|
329
340
|
// ── count-first per-CLASS rescue ──────────────────────────────────────────────────────────────────
|
|
@@ -338,26 +349,27 @@ export function makeEnumerate(deps) {
|
|
|
338
349
|
// a populated class can never be recorded 0 — it is in `class_counts`, and if tractable its records
|
|
339
350
|
// are carried. NOTE a record filed in several classes sits in each leg's enumeration; the merge
|
|
340
351
|
// dedupes it, so per-class totals may legitimately sum past the stack total.
|
|
341
|
-
async function classSplitRescue(auth, params, classes, ceiling, stackTotal, tctx, incomplete) {
|
|
352
|
+
async function classSplitRescue(auth, params, classes, ceiling, stackTotal, tctx, incomplete, floor = null) {
|
|
342
353
|
const class_counts = {};
|
|
343
354
|
const tractable = [];
|
|
344
355
|
for (const c of classes) {
|
|
345
356
|
const probe = await countHits(auth, { ...params, nice_classes: [c] }, tctx);
|
|
346
357
|
const n = probe.ok ? probe.total : null;
|
|
347
|
-
|
|
348
|
-
if (n
|
|
349
|
-
|
|
350
|
-
else
|
|
358
|
+
const said = countsOf(probe); // what this class's own count counts, when the register said (ruling 712)
|
|
359
|
+
if (n == null) { class_counts[c] = floorCount(probe) ?? { total_hits: null, disposition: "error" }; continue; }
|
|
360
|
+
if (n === 0) class_counts[c] = { total_hits: 0, disposition: "verified-zero", ...said };
|
|
361
|
+
else if (n > ceiling) class_counts[c] = { total_hits: n, disposition: "crowd", ...said };
|
|
362
|
+
else tractable.push({ c, n, said });
|
|
351
363
|
}
|
|
352
364
|
tractable.sort((a, b) => a.n - b.n); // cheapest first — maximizes fully-enumerated legs under the budget
|
|
353
365
|
const merged = new Map();
|
|
354
|
-
for (const { c, n } of tractable) {
|
|
355
|
-
if (merged.size + n > ceiling) { class_counts[c] = { total_hits: n, disposition: "unenumerated" }; continue; }
|
|
366
|
+
for (const { c, n, said } of tractable) {
|
|
367
|
+
if (merged.size + n > ceiling) { class_counts[c] = { total_hits: n, disposition: "unenumerated", ...said }; continue; }
|
|
356
368
|
const r = await enumerate(auth, { ...params, nice_classes: [c] }, tctx);
|
|
357
369
|
const parsed = isToolError(r) ? null : parseToolText(r);
|
|
358
|
-
if (!parsed) { class_counts[c] = { total_hits: n, disposition: "error" }; continue; }
|
|
370
|
+
if (!parsed) { class_counts[c] = { total_hits: n, disposition: "error", ...said }; continue; }
|
|
359
371
|
if (parsed.state === "enumerated") {
|
|
360
|
-
class_counts[c] = { total_hits: n, disposition: "enumerated" };
|
|
372
|
+
class_counts[c] = { total_hits: n, disposition: "enumerated", ...said };
|
|
361
373
|
for (const rec of (parsed.records ?? [])) {
|
|
362
374
|
const k = recordKeyOf(rec);
|
|
363
375
|
if (!merged.has(k)) merged.set(k, rec);
|
|
@@ -365,7 +377,7 @@ export function makeEnumerate(deps) {
|
|
|
365
377
|
} else {
|
|
366
378
|
// drifted past the probe count (provider re-count) or hit the provider window — honest per-leg state
|
|
367
379
|
const m = parsed.total_hits ?? n;
|
|
368
|
-
class_counts[c] = { total_hits: m, disposition: m > ceiling ? "crowd" : "error" };
|
|
380
|
+
class_counts[c] = { total_hits: m, disposition: m > ceiling ? "crowd" : "error", ...(Number.isFinite(parsed.total_hits) ? countsOf(parsed) : said) };
|
|
369
381
|
}
|
|
370
382
|
}
|
|
371
383
|
const records = [...merged.values()];
|
|
@@ -374,14 +386,14 @@ export function makeEnumerate(deps) {
|
|
|
374
386
|
const unresolved = Object.values(class_counts).filter(isUnresolvedCount).length;
|
|
375
387
|
// Same rule, same reason, same correction as the OR-stack rescue above: a stack in which every class
|
|
376
388
|
// came back a verified zero is a complete band whose answer is zero, not a question nobody answered.
|
|
377
|
-
if (unresolved === 0) {
|
|
389
|
+
if (unresolved === 0 && floor === null) {
|
|
378
390
|
// every class resolved to verified-zero or fully-enumerated ⇒ the union of per-class enumerations
|
|
379
391
|
// IS the complete stack (every record carries ≥1 in-filter class) — a true band, the class rescue.
|
|
380
392
|
return { type: "text", text: JSON.stringify({ state: "enumerated", total_hits: stackTotal, count: records.length, records, class_counts }, null, 2) };
|
|
381
393
|
}
|
|
382
|
-
return incomplete(stackTotal, records.length, records,
|
|
383
|
-
`owner-scoped total_hits ${stackTotal} exceeds the enumerate ceiling ${ceiling}; count-first per-CLASS rescue ran (${classes.length} classes: ${tally["verified-zero"]} verified-zero, ${tally.enumerated} enumerated with records carried, ${tally.crowd} crowd, ${tally.unenumerated} unenumerated on budget, ${tally.error} error). Saturated class legs stay a CROWD descriptor for judgment; class_counts is the per-class truth — a populated class leg is never recorded 0 and never "unopened".`,
|
|
384
|
-
{ class_counts, records });
|
|
394
|
+
return incomplete(floor === null ? stackTotal : null, records.length, records,
|
|
395
|
+
`owner-scoped total_hits ${floor === null ? stackTotal : moreThan(floor)} exceeds the enumerate ceiling ${ceiling}; count-first per-CLASS rescue ran (${classes.length} classes: ${tally["verified-zero"]} verified-zero, ${tally.enumerated} enumerated with records carried, ${tally.crowd} crowd, ${tally.unenumerated} unenumerated on budget, ${tally.error} error). Saturated class legs stay a CROWD descriptor for judgment; class_counts is the per-class truth — a populated class leg is never recorded 0 and never "unopened".`,
|
|
396
|
+
{ class_counts, records, ...floorStamp(floor) });
|
|
385
397
|
}
|
|
386
398
|
|
|
387
399
|
// ── A QUESTION SLOWER THAN THE REGISTER'S GATEWAY WAITS IS ASKED AGAIN IN REGION HALVES ───────────────
|
|
@@ -399,7 +411,7 @@ export function makeEnumerate(deps) {
|
|
|
399
411
|
// half's own reason, as a names window does: a clean never ships over a part that did not run.
|
|
400
412
|
// `region_split` rides the answer, naming the timeout that started it and the size of every part that
|
|
401
413
|
// answered, so a reader can see the question was asked in pieces and where.
|
|
402
|
-
async function regionHalves(auth, params, tctx, { ceiling, incomplete, cause }) {
|
|
414
|
+
async function regionHalves(auth, params, tctx, { ceiling, incomplete, cause, hear = () => {} }) {
|
|
403
415
|
const regions = params.regions;
|
|
404
416
|
const mid = Math.ceil(regions.length / 2);
|
|
405
417
|
const halves = [regions.slice(0, mid), regions.slice(mid)];
|
|
@@ -435,9 +447,10 @@ export function makeEnumerate(deps) {
|
|
|
435
447
|
// A half that timed out while its sibling answered is the one place fewer regions may help: it is
|
|
436
448
|
// halved in turn, under the same rule.
|
|
437
449
|
const r = timedOut({ r: first })
|
|
438
|
-
? await regionHalves(auth, { ...params, regions: half }, tctx, { ceiling, incomplete, cause: parseToolText(first)?.reason ?? cause })
|
|
450
|
+
? await regionHalves(auth, { ...params, regions: half }, tctx, { ceiling, incomplete, cause: parseToolText(first)?.reason ?? cause, hear })
|
|
439
451
|
: first;
|
|
440
452
|
const parsed = isToolError(r) ? null : parseToolText(r);
|
|
453
|
+
hear(parsed);
|
|
441
454
|
const which = `the ${half.length}-region half ${half[0]}${half.length > 1 ? `…${half[half.length - 1]}` : ""}`;
|
|
442
455
|
// A stall from a deeper split keeps its own reason, stall first, so the executor reads it however
|
|
443
456
|
// deep the split went and however short the reason is later cut.
|
|
@@ -464,8 +477,22 @@ export function makeEnumerate(deps) {
|
|
|
464
477
|
...(screenLift ? { screen_lift: screenLift } : {}), region_split: { cause, parts } }, null, 2) };
|
|
465
478
|
}
|
|
466
479
|
|
|
467
|
-
|
|
480
|
+
// ── WHAT THE TOTAL COUNTS, STATED ONCE FOR THE WHOLE ANSWER ────────────────────────────────────────
|
|
481
|
+
//
|
|
482
|
+
// A register can say that a total counts records, one per country a mark covers, rather than marks
|
|
483
|
+
// (`total_counts` on a provider's parsed answer). The answer to the question states it, whichever of the
|
|
484
|
+
// exits below produced it: the page loop, the name windows, the region halves and the rescues each tell
|
|
485
|
+
// the question what they heard, and it is stamped here, on the one way out. Recorded only: the ceiling
|
|
486
|
+
// still reads the register's own number (ruled 2026-10-02). An answer with no `total_counts` is one whose
|
|
487
|
+
// register said nothing about it, never a count of marks.
|
|
488
|
+
async function enumerate(auth, params, tctx, opts = {}) {
|
|
489
|
+
const said = { totalCounts: null };
|
|
490
|
+
return stampTotalCounts(await enumerateQuestion(auth, params, tctx, opts, said), said.totalCounts);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function enumerateQuestion(auth, params, tctx, { split = true, oneByOne = false } = {}, said = { totalCounts: null }) {
|
|
468
494
|
if (!hasAnyElement(params)) return { type: "text", text: missingElementError };
|
|
495
|
+
const hear = (p) => { if (typeof p?.total_counts === "string") said.totalCounts = p.total_counts; };
|
|
469
496
|
|
|
470
497
|
// The tuned ceiling, then narrowed by any window the QUERY SHAPE imposes. Min, never max:
|
|
471
498
|
// a shape window is a vendor limit, and a band that pages past it does not return more records, it
|
|
@@ -499,7 +526,9 @@ export function makeEnumerate(deps) {
|
|
|
499
526
|
// descriptor total — `count` is the deduped truth); the resource ceiling applies to the RUNNING total
|
|
500
527
|
// so a crowd still returns an incomplete descriptor. ANY chunk error/incomplete makes the WHOLE slice
|
|
501
528
|
// incomplete — a clean can never ship over a partially-executed slice.
|
|
502
|
-
const
|
|
529
|
+
const shapeChunk = oneByOne ? 1 : typeof namesChunkFor === "function" ? namesChunkFor(params) : null;
|
|
530
|
+
const namesChunk = Number.isFinite(shapeChunk) && shapeChunk >= 1
|
|
531
|
+
? Math.floor(shapeChunk) : envInt("CLEAROTRON_ENUMERATE_NAMES_CHUNK", namesChunkDefault);
|
|
503
532
|
const allNames = Array.isArray(params[namesKey]) ? params[namesKey].filter(Boolean) : null;
|
|
504
533
|
if (allNames && allNames.length > namesChunk) {
|
|
505
534
|
const merged = new Map();
|
|
@@ -507,6 +536,9 @@ export function makeEnumerate(deps) {
|
|
|
507
536
|
const rescueNotes = []; // per-window rescue summaries for the final descriptor
|
|
508
537
|
const enumeratedWindowTerms = []; // terms whose whole window enumerated cleanly (per-term truth by superset)
|
|
509
538
|
let totalSum = 0;
|
|
539
|
+
// The largest floor a window answered with ("at least N"): the slice's total is then unknown, never a
|
|
540
|
+
// sum that leaves the floor out, and the figure rides as the slice's own floor.
|
|
541
|
+
let floorMax = null;
|
|
510
542
|
const chunks = Math.ceil(allNames.length / namesChunk);
|
|
511
543
|
const mergeRecs = (recs) => {
|
|
512
544
|
for (const rec of (recs ?? [])) {
|
|
@@ -518,6 +550,7 @@ export function makeEnumerate(deps) {
|
|
|
518
550
|
const part = allNames.slice(i * namesChunk, (i + 1) * namesChunk);
|
|
519
551
|
const r = await enumerate(auth, { ...params, [namesKey]: part }, tctx);
|
|
520
552
|
const parsed = parseToolText(r);
|
|
553
|
+
hear(parsed);
|
|
521
554
|
if (isToolError(r) || !parsed) {
|
|
522
555
|
return incomplete(totalSum, merged.size, [...merged.values()],
|
|
523
556
|
`chunk ${i + 1}/${chunks} (${part.length} names) failed: ${String(r?.text ?? "unparseable").slice(0, 140)}`,
|
|
@@ -527,6 +560,7 @@ export function makeEnumerate(deps) {
|
|
|
527
560
|
// A rescued window (term_counts attached) carries its per-term truth — aggregate and CONTINUE so
|
|
528
561
|
// the remaining windows still get their accounting; the slice stays incomplete either way, but a
|
|
529
562
|
// partial abort here would leave later windows' terms unaccounted and the clean-gate blind.
|
|
563
|
+
if (Number.isFinite(parsed.total_floor)) floorMax = Math.max(floorMax ?? 0, parsed.total_floor);
|
|
530
564
|
if (countFirst && parsed.term_counts && typeof parsed.term_counts === "object") {
|
|
531
565
|
Object.assign(termCounts, parsed.term_counts);
|
|
532
566
|
totalSum += parsed.total_hits ?? 0;
|
|
@@ -534,9 +568,10 @@ export function makeEnumerate(deps) {
|
|
|
534
568
|
rescueNotes.push(`chunk ${i + 1}/${chunks}: ${String(parsed.reason ?? "").slice(0, 140)}`);
|
|
535
569
|
continue;
|
|
536
570
|
}
|
|
537
|
-
return incomplete(totalSum + (parsed.total_hits ?? 0), merged.size + (parsed.fetched ?? 0),
|
|
571
|
+
return incomplete(floorMax === null ? totalSum + (parsed.total_hits ?? 0) : null, merged.size + (parsed.fetched ?? 0),
|
|
538
572
|
[...merged.values(), ...(parsed.sample ?? [])],
|
|
539
|
-
`chunk ${i + 1}/${chunks} (${part.length} names) ${parsed.state}: ${String(parsed.reason ?? "").slice(0, 180)}
|
|
573
|
+
`chunk ${i + 1}/${chunks} (${part.length} names) ${parsed.state}: ${String(parsed.reason ?? "").slice(0, 180)}`,
|
|
574
|
+
floorStamp(floorMax));
|
|
540
575
|
}
|
|
541
576
|
totalSum += parsed.total_hits ?? 0;
|
|
542
577
|
mergeRecs(parsed.records);
|
|
@@ -555,9 +590,9 @@ export function makeEnumerate(deps) {
|
|
|
555
590
|
for (const t of enumeratedWindowTerms) {
|
|
556
591
|
if (!(t in termCounts)) termCounts[t] = { total_hits: null, disposition: "enumerated" };
|
|
557
592
|
}
|
|
558
|
-
return incomplete(totalSum, merged.size, [...merged.values()],
|
|
593
|
+
return incomplete(floorMax === null ? totalSum : null, merged.size, [...merged.values()],
|
|
559
594
|
`count-first per-term rescue ran in ${rescueNotes.length}/${chunks} name chunks — saturated terms stay CROWD descriptors for judgment; term_counts is the per-term truth (a populated term is never recorded 0). ${rescueNotes.join(" · ")}`.slice(0, 900),
|
|
560
|
-
{ term_counts: termCounts, records: [...merged.values()] });
|
|
595
|
+
{ term_counts: termCounts, records: [...merged.values()], ...floorStamp(floorMax) });
|
|
561
596
|
}
|
|
562
597
|
return { type: "text", text: JSON.stringify({ state: "enumerated", total_hits: totalSum, count: merged.size, records: [...merged.values()], ...(Object.keys(termCounts).length ? { term_counts: termCounts } : {}) }, null, 2) };
|
|
563
598
|
}
|
|
@@ -599,10 +634,11 @@ export function makeEnumerate(deps) {
|
|
|
599
634
|
+ `Provider's own words: ${clipProviderText(c?.reason ?? "", COUNT_PROBE_BUDGET)}`,
|
|
600
635
|
{ crowd_basis: "provider-refused-count", count_unavailable: true });
|
|
601
636
|
}
|
|
602
|
-
if (splittable && isGatewayTimeout(c?.reason)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(c.reason) });
|
|
637
|
+
if (splittable && isGatewayTimeout(c?.reason)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(c.reason), hear });
|
|
603
638
|
return incomplete(0, 0, [],
|
|
604
639
|
`provider error on the count probe before enumeration: ${String(c?.reason ?? "count unavailable")}`, timeoutMark(c?.reason));
|
|
605
640
|
}
|
|
641
|
+
hear(c);
|
|
606
642
|
total = Number.isFinite(c.total) ? c.total : 0;
|
|
607
643
|
probeTotal = total;
|
|
608
644
|
if (total > ceiling) {
|
|
@@ -625,11 +661,15 @@ export function makeEnumerate(deps) {
|
|
|
625
661
|
for (let page = 0; ; page += 1) {
|
|
626
662
|
const r = await search(auth, { ...params, ...pageParams(page, pageSize, prevParsed) }, tctx);
|
|
627
663
|
if (isToolError(r)) {
|
|
628
|
-
if (
|
|
664
|
+
if (page === 0 && !oneByOne && allNames && allNames.length > 1 && listRefusal?.test(String(r.text ?? ""))) {
|
|
665
|
+
return enumerateQuestion(auth, params, tctx, { split, oneByOne: true }, said);
|
|
666
|
+
}
|
|
667
|
+
if (splittable && isGatewayTimeout(r.text)) return regionHalves(auth, params, tctx, { ceiling, incomplete, cause: String(r.text), hear });
|
|
629
668
|
return incomplete(total, results.length, results, `provider error during enumeration (page ${page}): ${String(r.text ?? "")}`, timeoutMark(r.text));
|
|
630
669
|
}
|
|
631
670
|
const parsed = parseToolText(r);
|
|
632
671
|
if (!parsed) return incomplete(total, results.length, results, `unparseable search response during enumeration (page ${page})`);
|
|
672
|
+
hear(parsed);
|
|
633
673
|
// ── the number the completeness claim rests on must BE a number ────────────────────────────────
|
|
634
674
|
// On the "cheap" seam the page response IS the count — the ceiling is tested off `total_hits` and
|
|
635
675
|
// nothing else. A response that parsed but carries no usable total therefore cannot support a
|
|
@@ -638,6 +678,24 @@ export function makeEnumerate(deps) {
|
|
|
638
678
|
// where the "enumerated" verdict is actually minted, so it cannot depend on every future adapter
|
|
639
679
|
// remembering. total_hits rides out NULL — unknown — never a fabricated 0.
|
|
640
680
|
if (countProbe === "cheap" && !Number.isFinite(parsed.total_hits)) {
|
|
681
|
+
// A FLOOR IS AN ANSWER (ruled 2026-10-02). A register that says "at least 10,000" counted, and gave a
|
|
682
|
+
// floor instead of a figure: the search ran and is too large to read. So it is a crowd, with the
|
|
683
|
+
// register's own figure beside a total that stays null. Never a provider error, which would ask it
|
|
684
|
+
// again for the same answer and write a 0 in its place.
|
|
685
|
+
const floor = floorOf(parsed);
|
|
686
|
+
if (floor !== null) {
|
|
687
|
+
// AS FOR ANY CROWD OVER THE CEILING (the page-0 branch below): a stack of spellings, or an owner's
|
|
688
|
+
// classes, is counted part by part first, so a rare spelling never vanishes inside the floor.
|
|
689
|
+
if (page === 0 && countFirst && allNames && allNames.length > 1) {
|
|
690
|
+
return countFirstRescue(auth, params, allNames, ceiling, null, tctx, incomplete, floor);
|
|
691
|
+
}
|
|
692
|
+
if (page === 0 && countFirst && splitClasses.length > 1) {
|
|
693
|
+
return classSplitRescue(auth, params, splitClasses, ceiling, null, tctx, incomplete, floor);
|
|
694
|
+
}
|
|
695
|
+
return incomplete(null, results.length + (parsed.results?.length ?? 0), [...results, ...(parsed.results ?? [])],
|
|
696
|
+
`the register answered "${moreThan(floor)}" for this question: a floor, not a count, so the set is too large to read. ${CROWD_IS[0].toUpperCase()}${CROWD_IS.slice(1)}`,
|
|
697
|
+
floorStamp(floor));
|
|
698
|
+
}
|
|
641
699
|
return incomplete(null, results.length, results,
|
|
642
700
|
`provider error during enumeration (page ${page}): the search response carried no usable total_hits, and on this provider the response IS the count — so the enumerate ceiling could not be tested and this band cannot be read as either a completed enumeration or a sanctioned crowd. The total is UNKNOWN (null, never 0).`);
|
|
643
701
|
}
|
|
@@ -811,3 +869,29 @@ export function makeEnumerate(deps) {
|
|
|
811
869
|
|
|
812
870
|
return { enumerate, countFirstRescue, classSplitRescue };
|
|
813
871
|
}
|
|
872
|
+
|
|
873
|
+
/** The stamps a crowd carries when the register answered with a floor: the figure, and why the total is null. */
|
|
874
|
+
function floorStamp(floor) {
|
|
875
|
+
return floor === null ? {} : { crowd_basis: "register-floor", total_floor: floor };
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/** A part the register answered with a floor is itself a crowd with the register's figure, never an error. */
|
|
879
|
+
function floorCount(probe) {
|
|
880
|
+
return Number.isFinite(probe?.floor) ? { total_hits: null, total_floor: probe.floor, disposition: "crowd" } : null;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/** `{ total_counts }` from a probe or a parsed answer that carries it, else nothing to spread. PURE. */
|
|
884
|
+
function countsOf(p) {
|
|
885
|
+
return typeof p?.total_counts === "string" ? { total_counts: p.total_counts } : {};
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** The answer with what its total counts stated on it; an error, or nothing heard, passes unchanged. */
|
|
889
|
+
function stampTotalCounts(r, totalCounts) {
|
|
890
|
+
if (!totalCounts || isToolError(r)) return r;
|
|
891
|
+
const parsed = parseToolText(r);
|
|
892
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return r;
|
|
893
|
+
return { ...r, text: JSON.stringify({ ...parsed, total_counts: totalCounts }, null, 2) };
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// What a crowd is, in the words every crowd's reason ends with: a band the funnel hands up, never a clean.
|
|
897
|
+
const CROWD_IS = "this is a CROWD, not a named exact/near band. Record it as a count+sample descriptor and hand it up to judgment; the funnel does NOT narrow-and-retry a crowd here. Whether a narrower NAMED enumeration is warranted, and whether this slice is material, is judgment's call (Layer B) — never accept it as clean.";
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { dirname, join } from "node:path";
|
|
19
|
-
import { nativeScriptIndexGap } from "./script-form.mjs";
|
|
19
|
+
import { nativeScriptIndexGap } from "./script-form.mjs"; import { floorOf, moreThan } from "../../shared/register-floor.mjs";
|
|
20
20
|
import { entryTermIssues, goodsTermsList } from "./term-shape.mjs";
|
|
21
21
|
import { awaitsReadingTurn, releasedFamiliesFile } from "./plan-guards.mjs";
|
|
22
22
|
import { faultText, guardToolCall } from "./transport-guard.mjs";
|
|
@@ -500,8 +500,16 @@ export function makeExecutePlan(deps) {
|
|
|
500
500
|
&& Number.isFinite(parsed.total_hits) && Number(parsed.total_hits) === 0
|
|
501
501
|
&& parsed.owner_resolution != null && !ownerNameResolved(parsed.owner_resolution);
|
|
502
502
|
const counted = !!parsed && !ownerUnverified;
|
|
503
|
+
// A floor ("at least 10,000") is the register's answer: the total stays null and its figure rides beside
|
|
504
|
+
// it, never a 0 (ruled 2026-10-02; shared/register-floor.mjs). An answer that carried no number at all
|
|
505
|
+
// keeps a null total as well. `?? 0` wrote it down as a counted zero, which read as an empty field and
|
|
506
|
+
// hid it from the band shape's uncountable-slice reading. A call that failed keeps the 0 it always had
|
|
507
|
+
// beside error:true, the placeholder every refused slice carries.
|
|
508
|
+
const floor = floorOf(parsed);
|
|
509
|
+
const untotalled = counted && !Number.isFinite(parsed.total_hits);
|
|
503
510
|
blocks.push({ state: "incomplete", ...base,
|
|
504
|
-
total_hits: ownerUnverified ? null : (parsed?.total_hits ?? 0), fetched: parsed?.results?.length ?? 0,
|
|
511
|
+
total_hits: ownerUnverified || floor !== null || untotalled ? null : (parsed?.total_hits ?? 0), fetched: parsed?.results?.length ?? 0,
|
|
512
|
+
...(floor !== null ? { total_floor: floor, crowd_basis: "register-floor" } : {}),
|
|
505
513
|
sample: (parsed?.results ?? []).slice(0, 5),
|
|
506
514
|
...(counted ? {} : { error: true }),
|
|
507
515
|
...(countGap || ownerUnverified ? { deferred: true } : {}),
|
|
@@ -509,6 +517,8 @@ export function makeExecutePlan(deps) {
|
|
|
509
517
|
// the resolution the sweep actually ran on, verbatim — a reader must be able to see WHICH
|
|
510
518
|
// applicant styling was asked for, on a verified zero as much as on an unverified one.
|
|
511
519
|
...(parsed?.owner_resolution ? { owner_resolution: parsed.owner_resolution } : {}),
|
|
520
|
+
// what the counted total counts, when the register said (enumerate.mjs, "what the total counts")
|
|
521
|
+
...(counted && typeof parsed?.total_counts === "string" ? { total_counts: parsed.total_counts } : {}),
|
|
512
522
|
reason: ownerUnverified ? unresolvedOwnerCountReason(e.term ?? (e.terms ?? [])[0], coveredBy)
|
|
513
523
|
: parsed ? descriptorReason
|
|
514
524
|
: `provider error on the count probe (after one in-tool retry): ${clipProviderText(r?.text ?? "", 100)}` });
|
|
@@ -643,7 +653,8 @@ export function makeExecutePlan(deps) {
|
|
|
643
653
|
// EACH SPELLING'S COUNT, IN THE REPLY. A crowded stack comes back counted and unread, and the reading
|
|
644
654
|
// step decides which spellings to read, narrow or leave (ruled 2026-09-26), so it is handed the counts
|
|
645
655
|
// here rather than left to find them in the band file: a number for a spelling counted and not read,
|
|
646
|
-
// 0 for a verified zero, "crowd N" for one over the ceiling, "error" for a count that failed.
|
|
656
|
+
// 0 for a verified zero, "crowd N" for one over the ceiling, "error" for a count that failed. A spelling the
|
|
657
|
+
// register answered with a floor reads "crowd more than 10,000", its figure as every page prints it.
|
|
647
658
|
//
|
|
648
659
|
// THE GATE IS "ANYTHING LEFT TO DECIDE", not one disposition, and that is the whole of this block's
|
|
649
660
|
// correctness. Keyed on `unenumerated` alone it dropped the two stacks the rule exists for: one where
|
|
@@ -659,7 +670,7 @@ export function makeExecutePlan(deps) {
|
|
|
659
670
|
const spellingCounts = Object.fromEntries(blocks
|
|
660
671
|
.filter((b) => b?.term_counts && Object.values(b.term_counts).some(isUnresolvedCount))
|
|
661
672
|
.map((b) => [b.qid, Object.fromEntries(Object.entries(b.term_counts).map(([t, v]) => [t,
|
|
662
|
-
v?.disposition === "crowd" ? `crowd ${v.total_hits}` : v?.disposition === "error" ? "error" : (v?.total_hits ?? "error")]))]));
|
|
673
|
+
v?.disposition === "crowd" ? `crowd ${Number.isFinite(v.total_floor) ? moreThan(v.total_floor) : v.total_hits}` : v?.disposition === "error" ? "error" : (v?.total_hits ?? "error")]))]));
|
|
663
674
|
return { type: "text", text: JSON.stringify({
|
|
664
675
|
written: outPath, blocks: blocks.length + preserved.length, executed: blocks.length, preserved: preserved.length, skipped,
|
|
665
676
|
states: Object.fromEntries([...stateByQid].filter(([q]) => !seeded.has(q))),
|
|
@@ -38,7 +38,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
38
38
|
countStatusFilter: "none",
|
|
39
39
|
// HTTP-414 URI guard: the whole query is a GET query string, so ~80 backtick-quoted names (≈2–4KB
|
|
40
40
|
// encoded) is the safe OR-stack. This MIRRORS the executor's ENUMERATE_NAMES_CHUNK_DEFAULT — the
|
|
41
|
-
// planner must never dictate an OR-stack the executor would have to chunk-rescue (
|
|
41
|
+
// planner must never dictate an OR-stack the executor would have to chunk-rescue (Open Country 414).
|
|
42
42
|
maxOrWidth: 80,
|
|
43
43
|
// `nice-class` clauses ride the same query string (implicit OR within the field) — one call, no fan-out.
|
|
44
44
|
classFilter: "native",
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1-beta.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"main": "bridge.mjs",
|
|
9
9
|
"dependencies": {
|
|
10
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
10
|
+
"@modelcontextprotocol/sdk": "^1.31.0"
|
|
11
11
|
},
|
|
12
12
|
"engines": { "node": ">=22.13.0" }
|
|
13
13
|
}
|
|
@@ -120,9 +120,36 @@ export const CAPABILITIES = Object.freeze({
|
|
|
120
120
|
// NOTE the key. `filters.status` does not exist and the API rejects unknown filter keys outright
|
|
121
121
|
// (`HTTP 400 Unrecognized key: status`) — which is what core.js was sending until this issue.
|
|
122
122
|
countStatusFilter: "native",
|
|
123
|
-
// The request carries ONE `query` string
|
|
124
|
-
//
|
|
123
|
+
// The request carries ONE `query` string for every predicate but one, so the general OR width stays 1:
|
|
124
|
+
// a phonetic, wildcard or owner band is one call per term, and anything that reads this width as
|
|
125
|
+
// "no OR at all" (the supplemental batch's one-question-per-name split) keeps reading it that way.
|
|
125
126
|
maxOrWidth: 1,
|
|
127
|
+
// ── THE ONE PREDICATE THAT TAKES A LIST: exact spellings, up to 100 per request ─────────────────
|
|
128
|
+
//
|
|
129
|
+
// A list in `q` is a ranked search over every spelling through the channels the request names, and
|
|
130
|
+
// each record says which spellings found it (`match.terms`). The register takes up to 100 spellings
|
|
131
|
+
// when the channels are only `identical` and `lookalike` — which is exactly what an exact question
|
|
132
|
+
// sends — and 10 otherwise. Measured live against one search per spelling, by record id and paged to
|
|
133
|
+
// exhaustion: the same records on the exact band at 32 and at 100 spellings, look-alike-only records
|
|
134
|
+
// included, with every record's `match.terms` naming exactly the spellings that found it alone. The
|
|
135
|
+
// figures are on the tracker. The one documented difference — a list skips the word-by-word fuzzy
|
|
136
|
+
// retry — belongs to a channel the exact question does not send.
|
|
137
|
+
//
|
|
138
|
+
// So the planner stacks an exact spelling band at this width, and the kernel sends each stack as one
|
|
139
|
+
// request (`namesChunkFor` in core.js); every other predicate stays at `maxOrWidth`.
|
|
140
|
+
exactOrWidth: 100,
|
|
141
|
+
// ── THE SOUND-ALIKE BAND TAKES A LIST TOO, UP TO 10 SPELLINGS AND 30 WORDS ─────────────────────────
|
|
142
|
+
//
|
|
143
|
+
// The register takes up to 10 spellings, and 30 words in all, when a channel other than `identical`
|
|
144
|
+
// and `lookalike` is asked. On the sound-alike channels (`identical`, `phonetic`) it answers a list of
|
|
145
|
+
// 10 with the same records, by id, as one search per spelling, and each record's `match.terms` names the
|
|
146
|
+
// spellings that found it alone (ruled 2026-10-09).
|
|
147
|
+
//
|
|
148
|
+
// The typo channels (`identical`, `fuzzy`, `embedded`, `lookalike`) stay one spelling per request. The
|
|
149
|
+
// register refuses some typo lists of 10 as "too complex to run" where each spelling alone answers, and
|
|
150
|
+
// which lists it refuses depends on the spellings, so no fixed width is safe there.
|
|
151
|
+
phoneticOrWidth: 10,
|
|
152
|
+
listWordBudget: 30,
|
|
126
153
|
// filters.nice_classes[] is a top-level OR filter — one call, no fan-out.
|
|
127
154
|
classFilter: "native",
|
|
128
155
|
// ── THE GOODS-AND-SERVICES TEXT FILTER ──────────────────────────────────────────────────────────
|
|
@@ -395,6 +422,9 @@ export const CAPABILITIES = Object.freeze({
|
|
|
395
422
|
pageGuard: 60,
|
|
396
423
|
ceilingDefault: 600,
|
|
397
424
|
namesChunkDefault: 1,
|
|
425
|
+
// A ranked list the register will not run (HTTP 400, "The search query is too complex to run."). Which
|
|
426
|
+
// lists it refuses depends on the spellings, so a refused one is asked again one spelling at a time.
|
|
427
|
+
listRefusal: /too complex to run/i,
|
|
398
428
|
providerWindow: "cursor window; exact total via options.include_total (approximate totals saturate at 10000 and are reported UNKNOWN, never as a count)",
|
|
399
429
|
}),
|
|
400
430
|
});
|