clearotron 0.3.2 → 0.3.3-beta.0
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/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +29 -7
- package/bin/start.mjs +1 -1
- package/build-info.json +2 -2
- package/demo/MANIFEST.json +27 -0
- package/docs/INTAKE.md +8 -0
- package/docs/architecture/04-configuration-reference.md +1 -1
- package/driver/CHANGELOG.md +37 -0
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/contract-audit.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +37 -37
- package/driver/contract-vocabulary.mjs +8 -8
- package/driver/coverage-form-io.mjs +3 -1
- package/driver/coverage-form.mjs +38 -11
- package/driver/coverage-ledger.mjs +37 -7
- package/driver/coverage-union.mjs +2 -2
- package/driver/crowd-context.mjs +19 -6
- package/driver/drainer-identity.mjs +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +4 -2
- package/driver/engine/mcp/corsearch-server.mjs +3 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +47 -5
- package/driver/engine/mcp/euipo-server.mjs +2 -0
- package/driver/engine/mcp/free-tier-server.mjs +2 -0
- package/driver/engine/mcp/gather-config.mjs +8 -2
- package/driver/engine/mcp/probe-server.mjs +28 -0
- package/driver/engine/mcp/proposal-fields.mjs +45 -0
- package/driver/engine/mcp/recording-server.mjs +30 -0
- package/driver/engine/mcp/signa-server.mjs +2 -0
- package/driver/engine/mcp/supplemental.mjs +89 -12
- package/driver/engine/mcp/unit-note-server.mjs +50 -0
- package/driver/engine/mcp/uspto-local-server.mjs +2 -0
- package/driver/engine/openai-agent.mjs +7 -0
- package/driver/engine/probe.mjs +67 -14
- package/driver/engine/tool-refusal.mjs +16 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/envelope-settle.mjs +82 -13
- package/driver/findings-model.mjs +4 -4
- package/driver/gateway.mjs +18 -2
- package/driver/manager-groups-verdict.mjs +1 -1
- package/driver/matter-frame-record.mjs +24 -7
- package/driver/named-band.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +12 -3
- package/driver/pipeline.mjs +141 -37
- package/driver/publish/index.mjs +40 -25
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/register-availability.mjs +2 -2
- package/driver/register-plan.mjs +313 -21
- package/driver/roster-verdict.mjs +1 -1
- package/driver/runner.mjs +26 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -0
- package/driver/skills/clearance-register/SKILL.md +44 -3
- package/driver/skills/clearance-register/digest.md +5 -5
- package/driver/skills/clearance-register/providers/clarivate.md +1 -1
- package/driver/skills/clearance-register/unit.md +39 -0
- package/driver/skills/clearance-variants/SKILL.md +1 -1
- package/driver/skills/matter-frame/SKILL.md +4 -2
- package/driver/stages.mjs +12 -5
- package/driver/status-snapshot.mjs +1 -1
- package/driver/suite-census.json +236 -14
- package/driver/synthesis-record.mjs +80 -2
- package/driver/unit-file-drift.mjs +3 -3
- package/driver/unit-inventory.mjs +2 -2
- package/driver/unit-state-verdict.mjs +1 -1
- package/driver/updater-identity.mjs +2 -3
- package/driver/variant-manifest-model.mjs +11 -1
- package/driver/verify.mjs +5 -5
- package/driver/withheld-families.mjs +104 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/brief.mjs +5 -7
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +3 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-DMthc7PQ.js → index-GBbbyQxc.js} +22 -4
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +2 -2
- package/providers/_shared/enumerate.mjs +15 -2
- package/providers/_shared/execute-plan.mjs +19 -1
- package/providers/_shared/plan-guards.mjs +40 -0
- package/providers/clarivate/src/capabilities.js +15 -5
- package/providers/clarivate/src/core.js +41 -5
- package/providers/corsearch/src/capabilities.js +4 -0
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +22 -8
- package/providers/signa/src/core.js +12 -1
- package/scripts/demo-evidence.mjs +114 -0
- package/scripts/engine-probe.mjs +6 -5
- package/scripts/env-audit.mjs +1 -1
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/live-surface-check.mjs +26 -19
- package/scripts/mint-suite-census.mjs +66 -0
- package/scripts/package-size-budget.mjs +117 -0
- package/scripts/register-plan-shape.mjs +259 -0
- package/scripts/release-note-required.mjs +38 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
|
@@ -676,7 +676,7 @@ export const CHECKED_TIMERS = Object.freeze(
|
|
|
676
676
|
* that reports on the service keeps saying `inactive`, which is what it says when the timer is working.
|
|
677
677
|
*/
|
|
678
678
|
export function timerVerdict(timers, { probeFailed = null } = {}) {
|
|
679
|
-
if (probeFailed) return { state: "
|
|
679
|
+
if (probeFailed) return { state: "skip", blocked: true, stopped: [], absent: [], message: `could not ask systemd about the timers: ${probeFailed}. That is this check failing to look, not a report about them.` };
|
|
680
680
|
const rows = timers ?? [];
|
|
681
681
|
if (!rows.length) return { state: "pass", stopped: [], absent: [], message: "no timer is declared for this box" };
|
|
682
682
|
const absent = rows.filter((t) => t.load === "not-found").map((t) => t.unit).sort();
|
|
@@ -726,7 +726,7 @@ export function unitInventoryVerdict({
|
|
|
726
726
|
inventory = UNIT_INVENTORY,
|
|
727
727
|
} = {}) {
|
|
728
728
|
if (!probe.ok) {
|
|
729
|
-
return { state: "skip", undeclared: [], orphaned: [], absent: [], misdeclared: [],
|
|
729
|
+
return { state: "skip", blocked: true, undeclared: [], orphaned: [], absent: [], misdeclared: [],
|
|
730
730
|
message: `could not enumerate systemd units, so the inventory was NOT checked — ${probe.why ?? "no reason given"}. `
|
|
731
731
|
+ "A failure to look is not a finding about the deployment." };
|
|
732
732
|
}
|
|
@@ -100,7 +100,7 @@ export function classifyActiveState(active) {
|
|
|
100
100
|
export function unitsActiveVerdict({ units, probe }) {
|
|
101
101
|
// — a failure to look is never a finding about the deployment.
|
|
102
102
|
if (!probe?.ok)
|
|
103
|
-
return { state: "skip", message: `could not enumerate systemd --user units — ${probe?.why ?? "no reason reported"}` };
|
|
103
|
+
return { state: "skip", blocked: true, message: `could not enumerate systemd --user units — ${probe?.why ?? "no reason reported"}` };
|
|
104
104
|
|
|
105
105
|
const label = (u) => `${u.unit}=${u.active}`
|
|
106
106
|
+ (u.type ? ` (${u.type}${u.since ? `, since ${u.since}` : ""})` : (u.since ? ` (since ${u.since})` : ""));
|
|
@@ -111,8 +111,7 @@ export function updaterVerdict({ stamp, now = null, deployClone = null, maxAgeSe
|
|
|
111
111
|
return { state: "fail", message:
|
|
112
112
|
`no updater identity stamp at ${UPDATER_STAMP_BASENAME}: the copy of the updater that deploys this `
|
|
113
113
|
+ "box did NOT say what it is, so whether it is the current one was not established. A copy old "
|
|
114
|
-
+ "enough to predate the stamp writes none, which is itself the stale-updater case.
|
|
115
|
-
+ "failure to look, never a pass." };
|
|
114
|
+
+ "enough to predate the stamp writes none, which is itself the stale-updater case." };
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
// — AN UNREADABLE SIDE IS NOT A MISMATCH. A digest that could not be taken arrives as null, and
|
|
@@ -128,7 +127,7 @@ export function updaterVerdict({ stamp, now = null, deployClone = null, maxAgeSe
|
|
|
128
127
|
: !wsha ? "the running copy of the updater could NOT be read as a digest"
|
|
129
128
|
: "the master could NOT be read as a digest";
|
|
130
129
|
const why = stamp.masterCommitError ? ` The updater reported: ${stamp.masterCommitError}` : "";
|
|
131
|
-
return { state: "
|
|
130
|
+
return { state: "skip", blocked: true, message:
|
|
132
131
|
`the updater stamp is present but ${which}, so the running copy was NOT `
|
|
133
132
|
+ `compared against its master.${why} This is a failure to look, never a pass.` };
|
|
134
133
|
}
|
|
@@ -176,8 +176,18 @@ export function parseVariantManifestModel(raw) {
|
|
|
176
176
|
// and refuses it. Each says so in its own capability contract, and the compiler declines to build an
|
|
177
177
|
// entry a register cannot express. Deciding it here would freeze one register's behaviour into a
|
|
178
178
|
// rule about every register.
|
|
179
|
-
|
|
179
|
+
// ── AN ABSENT KEY AND AN EMPTY LIST ARE DIFFERENT ANSWERS ───────────────────────────────────────
|
|
180
|
+
//
|
|
181
|
+
// `null` means the stage was asked and did not answer. `[]` means it answered: it considered the
|
|
182
|
+
// goods and there are no words worth narrowing by. They are the same value to a reader who only
|
|
183
|
+
// checks emptiness, and that is how a production run shipped a narrowing that never ran — the model
|
|
184
|
+
// could not send the key at all, the manifest carried an empty list, the compiler minted nothing,
|
|
185
|
+
// and the run looked exactly like a matter that genuinely had no goods words.
|
|
186
|
+
//
|
|
187
|
+
// Nothing downstream may treat the two the same. A skipped question is a fact about the run.
|
|
188
|
+
let goods_words = null;
|
|
180
189
|
if (m.goods_words != null) {
|
|
190
|
+
goods_words = [];
|
|
181
191
|
if (!Array.isArray(m.goods_words) || !m.goods_words.every((w) => typeof w === "string"))
|
|
182
192
|
throw new Error("variantmodel_goods_words_invalid (an array of single-word strings, or omitted)");
|
|
183
193
|
const seen = new Set();
|
package/driver/verify.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-recor
|
|
|
16
16
|
import { findConnotationViolations, parsePrRiskResults, prRiskPopulation,
|
|
17
17
|
CONNOTATION_UNMATCHED_MARK, CONNOTATION_NO_RESEMBLANCE_MARK, MEANING_ANGLES_RE,
|
|
18
18
|
parseDispositionForm, CONNOTATION_UNRULED_REASONS, queryKey } from "./connotation-search.mjs";
|
|
19
|
-
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
|
|
19
|
+
import { formSidecarName, formSidecarPath } from "./disposition-union.mjs"; import { coverageStatusAsData } from "./common-law-coverage-status.mjs";
|
|
20
20
|
// B — the transport's own four failure states. The audit reads the run's records; this file locates them.
|
|
21
21
|
import { auditDispositionCalls, CALL_FAILURE_REASONS } from "./disposition-call-audit.mjs";
|
|
22
22
|
import { callRecordPaths } from "./disposition-tool.mjs";
|
|
@@ -481,7 +481,7 @@ function commonLawMeaningSeat(p, c) {
|
|
|
481
481
|
// — the EVIDENCE chain for `validators.commonLaw`, lifted out verbatim so the unavailability
|
|
482
482
|
// veto can wrap it instead of pre-empting it. Every return token is unchanged.
|
|
483
483
|
function commonLawEvidence(p, c) {
|
|
484
|
-
const structural = commonLawStructural(c);
|
|
484
|
+
const structural = commonLawStructural(c, p);
|
|
485
485
|
if (!structural.ok) return structural;
|
|
486
486
|
// The dictated grid spec is the SINGLE source of the grid contract when present (the driver wrote it,
|
|
487
487
|
// the plugin ran it, the plugin wrote the ledger from it): the receipts gate joins the ledger against
|
|
@@ -596,7 +596,7 @@ function commonLawHalfEvidence(p, c) {
|
|
|
596
596
|
if (!half) return fail("half_path_unrecognized");
|
|
597
597
|
// — the meaning seat is judged on the meaning work, because that is all it was dictated.
|
|
598
598
|
if (half === MEANING_SEAT) return commonLawMeaningSeat(p, c);
|
|
599
|
-
const structural = commonLawStructural(c);
|
|
599
|
+
const structural = commonLawStructural(c, p);
|
|
600
600
|
if (!structural.ok) return structural;
|
|
601
601
|
const gs = readGridSpecHalf(p, half);
|
|
602
602
|
if (gs.invalid) return fail(`grid_spec_unreadable:_driver/grid-spec.half-${half}.json is corrupt or misshapen (driver-written — this is a bug, not a model defect)`);
|
|
@@ -664,7 +664,7 @@ function commonLawHalfEvidence(p, c) {
|
|
|
664
664
|
// adjacent with exactly one separator between them; it stops rejecting a document that HAS the section
|
|
665
665
|
// under the hyphenated compound the skill itself uses. The failure this removes is a false negative, and
|
|
666
666
|
// its cost was a clearance that had already spent an hour.
|
|
667
|
-
function commonLawStructural(c) {
|
|
667
|
+
function commonLawStructural(c, p = null) {
|
|
668
668
|
return all(
|
|
669
669
|
nonEmpty(c),
|
|
670
670
|
needsSection(c, "findings", [/^#{1,4}\s+[^\n]*\bfindings\b/im], "findings-heading"),
|
|
@@ -672,7 +672,7 @@ function commonLawStructural(c) {
|
|
|
672
672
|
needsSection(c, "coverage-ledger", [/coverage[\s-]ledger/i], "coverage-ledger"),
|
|
673
673
|
needsSection(c, "audit-trail", [/audit[\s-]trail/i], "audit-trail"),
|
|
674
674
|
needs(c, [/\|/], "platform matrix"),
|
|
675
|
-
hasCoverageLedgerRow(c) ? ok() : fail("no_coverage_status_row"),
|
|
675
|
+
coverageStatusAsData(p) || hasCoverageLedgerRow(c) ? ok() : fail("no_coverage_status_row"), // the recorded status first, the word as fallback: coverageStatusAsData() in common-law-coverage-status.mjs
|
|
676
676
|
);
|
|
677
677
|
}
|
|
678
678
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// withheld-families.mjs — the reading turn's record of the waiting families it chose not to ask.
|
|
4
|
+
//
|
|
5
|
+
// On a crowded field the wider register families wait in the plan for the reading turn, which asks the
|
|
6
|
+
// ones worth asking. A family it does not ask was never searched, and until this record nothing said
|
|
7
|
+
// so: the turn's reason, when it gave one, sat in its prose note, where nothing reads it. So the turn
|
|
8
|
+
// records each family it leaves unasked as `withheld-by-judgment` with its reason, the coverage form
|
|
9
|
+
// carries a row per waiting family pre-settled from this record, and a family nobody judged is an
|
|
10
|
+
// unsettled row the digest's gate refuses to pass.
|
|
11
|
+
//
|
|
12
|
+
// The reason is the run's record and the audit workbook's, never the report's: the coverage form keeps
|
|
13
|
+
// these rows out of the ledger the report is built from (coverage-form.mjs, `family`).
|
|
14
|
+
//
|
|
15
|
+
// One file per axis, because the reading turn fans out one seat per axis and two seats writing one file
|
|
16
|
+
// would lose each other's calls. The axis is bound by the server, never taken from the payload.
|
|
17
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, readdirSync, mkdirSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { driverDir } from "../shared/driver-dir.mjs";
|
|
20
|
+
import { awaitsReadingTurn } from "../providers/_shared/plan-guards.mjs";
|
|
21
|
+
import { entryQuestionKey } from "./register-plan.mjs";
|
|
22
|
+
import { seatBannedTokens } from "./coverage-form.mjs";
|
|
23
|
+
|
|
24
|
+
export const WITHHELD_REASON_MAX = 600;
|
|
25
|
+
const FILE_RE = /^withheld-families-(.+)\.json$/;
|
|
26
|
+
|
|
27
|
+
export const withheldFamiliesPath = (runDir, axis) => driverDir(runDir, `withheld-families-${axis}.json`);
|
|
28
|
+
|
|
29
|
+
const readJson = (p) => { try { return existsSync(p) ? JSON.parse(readFileSync(p, "utf8")) : null; } catch { return null; } };
|
|
30
|
+
|
|
31
|
+
/** Every axis's record, merged: `{ [qid]: { axis, reason } }`. An unreadable file contributes nothing. */
|
|
32
|
+
export function readWithheldFamilies(runDir) {
|
|
33
|
+
const out = {};
|
|
34
|
+
let names = [];
|
|
35
|
+
try { names = readdirSync(driverDir(runDir)); } catch { return out; }
|
|
36
|
+
for (const name of names) {
|
|
37
|
+
const m = FILE_RE.exec(name);
|
|
38
|
+
if (!m) continue;
|
|
39
|
+
const doc = readJson(driverDir(runDir, name));
|
|
40
|
+
for (const [qid, v] of Object.entries(doc?.families ?? {})) {
|
|
41
|
+
const reason = String(v?.reason ?? "").trim();
|
|
42
|
+
if (qid && reason) out[qid] = { axis: String(doc?.axis ?? m[1]), reason };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The waiting families on one axis, and which of them the reading turn has asked: a waiting row is asked
|
|
50
|
+
* when a question it did not wait for — a plan entry or one of this axis's supplementals — carries the
|
|
51
|
+
* same question key. PURE over its inputs.
|
|
52
|
+
*/
|
|
53
|
+
export function waitingFamiliesOn(plan, axis, supplementals = []) {
|
|
54
|
+
const entries = Array.isArray(plan?.entries) ? plan.entries : [];
|
|
55
|
+
const askedKeys = new Set([...entries.filter((e) => !awaitsReadingTurn(e?.when)), ...supplementals]
|
|
56
|
+
.filter((e) => e && !e.unsupported).map((e) => entryQuestionKey(e, plan)).filter(Boolean));
|
|
57
|
+
const waiting = entries.filter((e) => e?.axis === axis && awaitsReadingTurn(e?.when));
|
|
58
|
+
return {
|
|
59
|
+
waiting,
|
|
60
|
+
unasked: waiting.filter((e) => !askedKeys.has(entryQuestionKey(e, plan))),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Record families withheld on the bound axis. `families` is `[{ qids: [qid…] | qid, reason }]` — one
|
|
66
|
+
* reason may cover several families. Each qid must be a waiting family of this axis in the frozen plan;
|
|
67
|
+
* a reason must be the reader's words, since the audit workbook prints it. Accepts what is valid, names
|
|
68
|
+
* what it refused, and never throws.
|
|
69
|
+
*/
|
|
70
|
+
export function recordWithheldFamilies(runDir, { axis, families } = {}) {
|
|
71
|
+
const plan = readJson(driverDir(runDir, "register-plan.json"));
|
|
72
|
+
if (!plan || !Array.isArray(plan.entries)) return { refused: "no frozen register plan in this run — there are no waiting families to record against" };
|
|
73
|
+
if (!Array.isArray(families) || !families.length) return { refused: "families must list at least one { qids, reason }" };
|
|
74
|
+
const supp = readJson(join(runDir, "register-units", `${axis}-supplemental-plan.json`));
|
|
75
|
+
const { waiting, unasked } = waitingFamiliesOn(plan, axis, Array.isArray(supp?.entries) ? supp.entries : []);
|
|
76
|
+
const waitingQids = new Set(waiting.map((e) => e.qid));
|
|
77
|
+
const path = withheldFamiliesPath(runDir, axis);
|
|
78
|
+
const doc = readJson(path) ?? { axis, families: {} };
|
|
79
|
+
const recorded = [], rejected = [];
|
|
80
|
+
for (const item of families) {
|
|
81
|
+
const qids = (Array.isArray(item?.qids) ? item.qids : [item?.qids ?? item?.qid]).map((q) => String(q ?? "").trim()).filter(Boolean);
|
|
82
|
+
const reason = String(item?.reason ?? "").replace(/\s+/g, " ").trim();
|
|
83
|
+
if (!qids.length) { rejected.push({ qid: "", issue: "an item names no qid" }); continue; }
|
|
84
|
+
if (!reason) { for (const qid of qids) rejected.push({ qid, issue: "no reason — a withheld family is recorded with why it was not asked" }); continue; }
|
|
85
|
+
if (reason.length > WITHHELD_REASON_MAX) { for (const qid of qids) rejected.push({ qid, issue: `the reason runs to ${reason.length} characters; at most ${WITHHELD_REASON_MAX}` }); continue; }
|
|
86
|
+
const banned = seatBannedTokens(reason);
|
|
87
|
+
if (banned.length) { for (const qid of qids) rejected.push({ qid, issue: `the reason names ${banned.join(", ")} — the audit workbook prints it; say it in a lawyer's words` }); continue; }
|
|
88
|
+
for (const qid of qids) {
|
|
89
|
+
if (!waitingQids.has(qid)) { rejected.push({ qid, issue: `not a waiting family on axis "${axis}" in the frozen plan` }); continue; }
|
|
90
|
+
doc.families[qid] = { reason };
|
|
91
|
+
recorded.push(qid);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (recorded.length) {
|
|
95
|
+
try {
|
|
96
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
97
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
98
|
+
writeFileSync(tmp, JSON.stringify({ axis, families: doc.families }, null, 2) + "\n");
|
|
99
|
+
renameSync(tmp, path);
|
|
100
|
+
} catch (e) { return { write_failed: String(e?.message ?? e).slice(0, 200) }; }
|
|
101
|
+
}
|
|
102
|
+
const still = unasked.filter((e) => !doc.families[e.qid]).map((e) => e.qid);
|
|
103
|
+
return { axis, recorded, rejected, still_to_judge: still, waiting: waiting.length };
|
|
104
|
+
}
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/lib/brief.mjs
CHANGED
|
@@ -92,9 +92,8 @@ export function buildBrief(run) {
|
|
|
92
92
|
const tail = [product, dateStr ? `run ${dateStr}` : null].filter(Boolean).join(", ");
|
|
93
93
|
lines.push(`**${subject}**${tail ? ` — ${tail}` : ""}.`);
|
|
94
94
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
: plainClause(fm.overall_caption ?? "");
|
|
95
|
+
// The report's own conclusion, verbatim, after the rating (ruled 2026-09-22: the report is the master).
|
|
96
|
+
const caption = clearance ? (clearance.caption ?? "") : plainClause(fm.overall_caption ?? "");
|
|
98
97
|
if (overall) lines.push(`**Overall risk: ${titleCase(String(overall))}.** ${caption}`.trim());
|
|
99
98
|
if (run.state) {
|
|
100
99
|
// a park is paused, not finished — say so plainly, and name the clock only where one exists
|
|
@@ -104,9 +103,8 @@ export function buildBrief(run) {
|
|
|
104
103
|
: run.state === "recovering" ? ` — auto-recovery backoff, resumes ${run.recoveryResumesAt ? `at ${String(run.recoveryResumesAt).replace("T", " ").slice(0, 16)} UTC` : "on its own"}`
|
|
105
104
|
: run.state === "parked-for-human" ? ` — parked by a runner stop (deploy/restart), resumes on the next runner activation` : "";
|
|
106
105
|
lines.push(`Status: ${run.state}${paused}.`);
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
if (run.statement) lines.push(String(run.statement));
|
|
106
|
+
// No second summary here: the rating and the report's conclusion above are the run's answer. The
|
|
107
|
+
// composed statement said "on hold" on runs nothing held.
|
|
110
108
|
}
|
|
111
109
|
|
|
112
110
|
let source = "none";
|
|
@@ -204,7 +202,7 @@ export function buildBrief(run) {
|
|
|
204
202
|
return {
|
|
205
203
|
runId: run.runId, markName: run.markName ?? clearance?.markName ?? fm.title ?? null,
|
|
206
204
|
product,
|
|
207
|
-
overall, tier: run.tier ?? null,
|
|
205
|
+
overall, tier: run.tier ?? null, caption: run.caption ?? null, state: run.state ?? null, date: run.date ?? null,
|
|
208
206
|
source, brief: lines.join("\n"),
|
|
209
207
|
};
|
|
210
208
|
}
|
package/mcp-server/lib/runs.mjs
CHANGED
|
@@ -102,7 +102,7 @@ function runFromStatusFile(statusFile, agent) {
|
|
|
102
102
|
// assistant saw the gate's word and nothing beside it, which is the shape this whole item is about.
|
|
103
103
|
// The verdict record holds the band for those runs, so it is read from there.
|
|
104
104
|
state: s.state ?? null,
|
|
105
|
-
tier: s.tier ?? bandWord(s.verdict) ?? tierFromRecord(runDir),
|
|
105
|
+
tier: s.tier ?? bandWord(s.verdict) ?? tierFromRecord(runDir), caption: s.caption ?? null, url: s.url ?? null, // the report's own conclusion, never a second summary (ruled 2026-09-22)
|
|
106
106
|
markName: s.markName ?? null, ref: s.ref ?? null, classes: s.classes ?? null,
|
|
107
107
|
stepN: s.stepN ?? null, stepLabel: s.stepLabel ?? null, stepTotal: s.stepTotal ?? null,
|
|
108
108
|
failedStage: s.failedStage ?? null, reason: s.reason ?? null,
|
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3-beta.0",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|
package/mcp-server/server.mjs
CHANGED
|
@@ -207,8 +207,9 @@ function runSummary(run) {
|
|
|
207
207
|
// once. null where the registry cannot name it — the row says nothing rather than guessing, because
|
|
208
208
|
// a hardcoded fallback is how a knockout once announced itself as a product it provably was not.
|
|
209
209
|
product: productIdentityFor(run),
|
|
210
|
-
// The band and the
|
|
211
|
-
|
|
210
|
+
// The band and the report's own conclusion; the gate's word stays in the run record (824), and no
|
|
211
|
+
// second summary rides beside them (ruled 2026-09-22).
|
|
212
|
+
state: run.state, location: run.location, tier: run.tier, caption: run.caption, url: run.url,
|
|
212
213
|
markName: run.markName, ref: run.ref, classes: run.classes,
|
|
213
214
|
step: s.stepN ? `${s.stepN}/${s.stepTotal} ${s.stepLabel ?? ""}`.trim() : null,
|
|
214
215
|
startedAt: run.startedAt, updatedAt: run.updatedAt, deliveredAt: run.deliveredAt,
|
package/package.json
CHANGED
|
@@ -12751,7 +12751,18 @@ function Logo({ markOnly = false }) {
|
|
|
12751
12751
|
var ACTIVE_MS = 5e3;
|
|
12752
12752
|
var IDLE_MS = 3e4;
|
|
12753
12753
|
var BACKOFF_MS = 6e4;
|
|
12754
|
-
|
|
12754
|
+
var startedEarly = new Map();
|
|
12755
|
+
function startEarly(key, fetcher) {
|
|
12756
|
+
if (!startedEarly.has(key)) startedEarly.set(key, fetcher());
|
|
12757
|
+
}
|
|
12758
|
+
function claimEarly(key) {
|
|
12759
|
+
if (!key) return null;
|
|
12760
|
+
const p = startedEarly.get(key);
|
|
12761
|
+
if (!p) return null;
|
|
12762
|
+
startedEarly.delete(key);
|
|
12763
|
+
return p;
|
|
12764
|
+
}
|
|
12765
|
+
function useLoad(fetcher, deps, early) {
|
|
12755
12766
|
const [result, setResult] = (0, import_react.useState)(null);
|
|
12756
12767
|
const [loading, setLoading] = (0, import_react.useState)(true);
|
|
12757
12768
|
const [nonce, setNonce] = (0, import_react.useState)(0);
|
|
@@ -12764,7 +12775,7 @@ function useLoad(fetcher, deps) {
|
|
|
12764
12775
|
if (seenKey.current !== null && seenKey.current !== depsKey) setResult(null);
|
|
12765
12776
|
seenKey.current = depsKey;
|
|
12766
12777
|
setLoading(true);
|
|
12767
|
-
fetcherRef.current().then((r) => {
|
|
12778
|
+
(claimEarly(early) ?? fetcherRef.current()).then((r) => {
|
|
12768
12779
|
if (!live) return;
|
|
12769
12780
|
setResult(r);
|
|
12770
12781
|
setLoading(false);
|
|
@@ -15487,7 +15498,7 @@ function lastFinishedOf(row) {
|
|
|
15487
15498
|
}
|
|
15488
15499
|
var COMPACT_FROM = 5;
|
|
15489
15500
|
function Home({ ctx }) {
|
|
15490
|
-
const { result, reload } = useLoad(() => api.runsMine(), []);
|
|
15501
|
+
const { result, reload } = useLoad(() => api.runsMine(), [], "runs:mine");
|
|
15491
15502
|
const allowanceOwner = ctx.owner ?? (ctx.me.accounts.length === 1 ? ctx.me.accounts[0] : null);
|
|
15492
15503
|
const { result: usageRes } = useLoad(() => allowanceOwner ? api.usage(allowanceOwner) : Promise.resolve({ kind: "pickAccount" }), [allowanceOwner]);
|
|
15493
15504
|
const allRuns = result?.kind === "ok" ? result.value : [];
|
|
@@ -16920,7 +16931,7 @@ function Clearances({ ctx }) {
|
|
|
16920
16931
|
const [query, setQuery] = (0, import_react.useState)("");
|
|
16921
16932
|
const [open, setOpen] = (0, import_react.useState)(new Set());
|
|
16922
16933
|
const [page, setPage] = (0, import_react.useState)(0);
|
|
16923
|
-
const { result, reload } = useLoad(() => api.runsMine(), []);
|
|
16934
|
+
const { result, reload } = useLoad(() => api.runsMine(), [], "runs:mine");
|
|
16924
16935
|
const allRuns = result?.kind === "ok" ? result.value : [];
|
|
16925
16936
|
const ownerFilter = ctx.owner;
|
|
16926
16937
|
const runs = (0, import_react.useMemo)(() => ownerFilter ? allRuns.filter((r) => runKey(r) === ownerFilter) : allRuns, [allRuns, ownerFilter]);
|
|
@@ -26744,6 +26755,13 @@ function screen(id, ctx) {
|
|
|
26744
26755
|
});
|
|
26745
26756
|
}
|
|
26746
26757
|
}
|
|
26758
|
+
var EARLY_RUNS_PATHS = [
|
|
26759
|
+
"/portal/home",
|
|
26760
|
+
"/portal/clearances",
|
|
26761
|
+
"/portal"
|
|
26762
|
+
];
|
|
26763
|
+
var here = window.location.pathname.replace(/[?#].*$/, "").replace(/\/+$/, "") || "/portal";
|
|
26764
|
+
if (EARLY_RUNS_PATHS.includes(here)) startEarly("runs:mine", () => api.runsMine());
|
|
26747
26765
|
var root = document.getElementById("root");
|
|
26748
26766
|
if (!root) throw new Error("#root is missing from index.html");
|
|
26749
26767
|
(0, import_client.createRoot)(root).render((0, import_jsx_runtime.jsx)(import_react.StrictMode, { children: (0, import_jsx_runtime.jsx)(AppShell, { render: screen }) }));
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
-->
|
|
50
50
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
|
|
51
51
|
<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
|
|
52
|
-
<script type="module" crossorigin src="/portal/assets/index-
|
|
52
|
+
<script type="module" crossorigin src="/portal/assets/index-GBbbyQxc.js"></script>
|
|
53
53
|
<link rel="stylesheet" crossorigin href="/portal/assets/index-5CCwiJG7.css">
|
|
54
54
|
</head>
|
|
55
55
|
<body>
|
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.3-beta.0",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
//
|
|
14
14
|
// SEAM (capabilities.countProbe) — WHERE the number comes from. Same three values enumerate.mjs
|
|
15
15
|
// documents, same meanings:
|
|
16
|
-
// "endpoint" Clarivate. A real POST /count: cheap, works at ANY magnitude
|
|
17
|
-
//
|
|
16
|
+
// "endpoint" Clarivate. A real POST /count: cheap, works at ANY magnitude, fetches nothing. A true
|
|
17
|
+
// count-only call.
|
|
18
18
|
// "cheap" Corsearch. The count rides page 0 of a normal search (totalHitCount) — so it is one
|
|
19
19
|
// BILLABLE search with `limit:1 fields:["uri"]`, the smallest response the API will give.
|
|
20
20
|
// Cheap, not free: every count here is a metered call.
|
|
@@ -288,7 +288,18 @@ export function makeEnumerate(deps) {
|
|
|
288
288
|
const tally = { "verified-zero": 0, enumerated: 0, crowd: 0, unenumerated: 0, error: 0 };
|
|
289
289
|
for (const v of Object.values(term_counts)) tally[v.disposition] += 1;
|
|
290
290
|
const unresolved = tally.crowd + tally.unenumerated + tally.error;
|
|
291
|
-
|
|
291
|
+
// A FULLY RESOLVED STACK IS A COMPLETE BAND, INCLUDING WHEN THE ANSWER IS ZERO. The comment below
|
|
292
|
+
// states the rule and the code then demanded a record anyway: `records.length > 0`. So a stack in
|
|
293
|
+
// which EVERY term resolved to verified-zero — nobody has filed any of these names — fell through
|
|
294
|
+
// to `incomplete`, which says nobody answered the question. The opposite is true: every term was
|
|
295
|
+
// asked and every one came back empty.
|
|
296
|
+
//
|
|
297
|
+
// It matters beyond the label. An `incomplete` parent is terminal for its when-guarded children, so
|
|
298
|
+
// a clean zero here held back everything waiting on it — and with the wider families now waiting on
|
|
299
|
+
// the identical question, that is the best case a matter can have (a mark nobody has registered)
|
|
300
|
+
// producing a run that searches almost nothing. The ordinary search path has always returned
|
|
301
|
+
// `enumerated` for a zero-record answer; this rescue path was the one place that did not.
|
|
302
|
+
if (unresolved === 0) {
|
|
292
303
|
// every term resolved to verified-zero or fully-enumerated ⇒ the union of per-term enumerations IS
|
|
293
304
|
// the complete stack (every record matching ≥1 name sits in some term's enumeration) — a true band.
|
|
294
305
|
return { type: "text", text: JSON.stringify({ state: "enumerated", total_hits: stackTotal, count: records.length, records, term_counts }, null, 2) };
|
|
@@ -344,7 +355,9 @@ export function makeEnumerate(deps) {
|
|
|
344
355
|
const tally = { "verified-zero": 0, enumerated: 0, crowd: 0, unenumerated: 0, error: 0 };
|
|
345
356
|
for (const v of Object.values(class_counts)) tally[v.disposition] += 1;
|
|
346
357
|
const unresolved = tally.crowd + tally.unenumerated + tally.error;
|
|
347
|
-
|
|
358
|
+
// Same rule, same reason, same correction as the OR-stack rescue above: a stack in which every class
|
|
359
|
+
// came back a verified zero is a complete band whose answer is zero, not a question nobody answered.
|
|
360
|
+
if (unresolved === 0) {
|
|
348
361
|
// every class resolved to verified-zero or fully-enumerated ⇒ the union of per-class enumerations
|
|
349
362
|
// IS the complete stack (every record carries ≥1 in-filter class) — a true band, the class rescue.
|
|
350
363
|
return { type: "text", text: JSON.stringify({ state: "enumerated", total_hits: stackTotal, count: records.length, records, class_counts }, null, 2) };
|
|
@@ -18,6 +18,7 @@ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
|
18
18
|
import { dirname } from "node:path";
|
|
19
19
|
import { nativeScriptIndexGap } from "./script-form.mjs";
|
|
20
20
|
import { entryTermIssues, goodsTermsList } from "./term-shape.mjs";
|
|
21
|
+
import { awaitsReadingTurn } from "./plan-guards.mjs";
|
|
21
22
|
import { faultText, guardToolCall } from "./transport-guard.mjs";
|
|
22
23
|
import { clipProviderText } from "./provider-text.mjs"; // — keep the discriminator
|
|
23
24
|
|
|
@@ -210,10 +211,13 @@ export function queryMarkTerms(e, query) {
|
|
|
210
211
|
return Array.isArray(e?.terms) ? e.terms : e?.term != null ? [e.term] : [];
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
// The goods words ride after the class tag (close-verify reads that tag): a goods-narrowed question shares
|
|
215
|
+
// predicate, term and classes with the identical one, and without them the two read as the same question.
|
|
213
216
|
export const describePlanEntry = (e) =>
|
|
214
217
|
`${e.predicate} ${e.terms ? e.terms.join(" OR ") : e.term}`
|
|
215
218
|
+ `${typeof e.owner === "string" && e.owner.trim() && String(e.predicate ?? "") !== "owner" ? ` owner:${e.owner.trim()}` : ""}`
|
|
216
|
-
+ ` [cl ${(e.nice_classes ?? []).join(",")}]
|
|
219
|
+
+ ` [cl ${(e.nice_classes ?? []).join(",")}]`
|
|
220
|
+
+ `${goodsTermsList(e).length ? ` goods:${goodsTermsList(e).join(" OR ")}` : ""}`;
|
|
217
221
|
|
|
218
222
|
// Compile one plan entry + its predicate params into the provider's query params (corsearch shapes by
|
|
219
223
|
// default: names/name/owners/owner + nice_classes + regions).
|
|
@@ -576,6 +580,20 @@ export function makeExecutePlan(deps) {
|
|
|
576
580
|
for (const e of targeted.filter((x) => !x.when)) await runEntry(e);
|
|
577
581
|
const skipped = [];
|
|
578
582
|
for (const e of targeted.filter((x) => x.when)) {
|
|
583
|
+
// A CROWD IS WHAT HOLDS A CHILD BACK, and only a crowd. `enumerated` is the state of a question
|
|
584
|
+
// that was answered — with records or with none: the ordinary search path returns it for a
|
|
585
|
+
// zero-record answer too, so a clean zero has always released its children here.
|
|
586
|
+
//
|
|
587
|
+
// Where that stopped being true was the RESCUE paths, and it is fixed there rather than here: a
|
|
588
|
+
// per-term or per-class stack in which every member came back a verified zero was falling through
|
|
589
|
+
// to `incomplete`, which says nobody answered. See enumerate.mjs — a fully resolved stack is a
|
|
590
|
+
// complete band whose answer is zero. `verified-zero` is a per-term DISPOSITION and never a band
|
|
591
|
+
// state (named-band.mjs BAND_STATES), so a guard testing for it here could never fire.
|
|
592
|
+
// THE READING-TURN WAIT: a family awaiting the reading turn is never released by a RESULT, so there is no
|
|
593
|
+
// state to read here and no seeded prior state that could release it on a warm followup either.
|
|
594
|
+
// The reading turn asks for it by minting a supplemental entry, which arrives as its own
|
|
595
|
+
// ungated entry — this one stands in the plan as the record of a question not asked.
|
|
596
|
+
if (awaitsReadingTurn(e.when)) { skipped.push(e.qid); continue; }
|
|
579
597
|
if (stateByQid.get(e.when.runs_if_enumerated) === "enumerated") await runEntry(e);
|
|
580
598
|
else skipped.push(e.qid); // crowd/failed parent is TERMINAL for the fringe — by design, never an error
|
|
581
599
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// plan-guards.mjs — what a register plan entry's `when` guard means, read from one place.
|
|
5
|
+
//
|
|
6
|
+
// THE TWO WAITS A PLAN ENTRY CAN CARRY, and they are different questions.
|
|
7
|
+
//
|
|
8
|
+
// `runs_if_enumerated: <parent qid>` is a fact about a RESULT: this slice runs if its parent question
|
|
9
|
+
// came back as a list rather than a crowd. A crowd parent is terminal for it.
|
|
10
|
+
//
|
|
11
|
+
// `awaits_reading_turn: true` is a fact about JUDGMENT NOT YET MADE (ruled 2026-09-21): this family runs
|
|
12
|
+
// when the reading turn asks for it, having read the identical mark's own list and found it thin. No
|
|
13
|
+
// result releases it — not the identical question coming back as a comfortable list, not a clean
|
|
14
|
+
// zero, nothing. The ask arrives as a supplemental entry, so the guarded entry itself never runs: it
|
|
15
|
+
// stands in the plan as the record of a question deliberately not asked.
|
|
16
|
+
//
|
|
17
|
+
// WHY THIS IS ITS OWN MODULE, on the provider side, rather than a constant in the compiler. Both the
|
|
18
|
+
// COMPILER (driver/register-plan.mjs, which writes the guard and joins it back to the band) and the
|
|
19
|
+
// EXECUTOR (providers/_shared/execute-plan.mjs, which decides what to dispatch) must agree on what a
|
|
20
|
+
// guard means, and a guard the executor does not recognise is not a guard at all — the entry runs as
|
|
21
|
+
// though it were ungated, which is silently the opposite of what was asked for. The executor cannot
|
|
22
|
+
// import the compiler: nothing under providers/ imports driver/, and inverting that for one predicate
|
|
23
|
+
// would invert it for the whole provider tree. So the meaning lives here, where both sides can read
|
|
24
|
+
// it, and neither side carries a second copy to drift.
|
|
25
|
+
|
|
26
|
+
/** The guard a decision-10 family carries while it waits for the reading turn. Frozen: entries spread a copy. */
|
|
27
|
+
export const AWAITS_READING_TURN = Object.freeze({ awaits_reading_turn: true });
|
|
28
|
+
|
|
29
|
+
/** Is this guard the ruling-204 wait — the one no result can release? */
|
|
30
|
+
export const awaitsReadingTurn = (when) => when?.awaits_reading_turn === true;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The parent qid a guard waits on, or null where it waits on no question at all.
|
|
34
|
+
*
|
|
35
|
+
* Every caller that resolves a guard against a band, validates it against the plan's qids, or reports
|
|
36
|
+
* which question held an entry back goes through this. A ruling-204 wait names no qid, and a caller
|
|
37
|
+
* reading `when.runs_if_enumerated` directly would get `undefined` and then decide something about it
|
|
38
|
+
* — orphan it, look it up and miss, or print it.
|
|
39
|
+
*/
|
|
40
|
+
export const guardParentQid = (when) => (awaitsReadingTurn(when) ? null : when?.runs_if_enumerated ?? null);
|
|
@@ -97,8 +97,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
97
97
|
// fails loud with tooManyResults past 30000. There is no partial mode and no cursor.
|
|
98
98
|
pagination: "single-shot",
|
|
99
99
|
// …which is exactly why the enumerate ceiling must be tested BEFORE the search, via the cheap
|
|
100
|
-
// POST /count — it works at ANY magnitude
|
|
101
|
-
// counts in one call.
|
|
100
|
+
// POST /count — it works at ANY magnitude and returns per-office counts in one call.
|
|
102
101
|
countProbe: "endpoint",
|
|
103
102
|
// A count CAN be narrowed to live filings here — queryOptions.activeOnly (buildSearchRequest's
|
|
104
103
|
// `active_only`). Declared because it diverges from corsearch, which has no status clause at all;
|
|
@@ -107,7 +106,8 @@ export const CAPABILITIES = Object.freeze({
|
|
|
107
106
|
countStatusFilter: "live",
|
|
108
107
|
// JSON body, not a URI: the bound is the parser's own document-nesting cap, which the vendor names in
|
|
109
108
|
// the refusal it answers a stack wider than this with. Not a URI length, so widening is not the fix.
|
|
110
|
-
|
|
109
|
+
// A 498-term stack is refused at that cap and a 496-term stack is not, so 496 is the declared width.
|
|
110
|
+
maxOrWidth: 496,
|
|
111
111
|
// ONE call: INT_CLASS_NUMBER value "9 OR 28 OR 41 OR 42" (or "9,28,41,42") = the deduplicated union.
|
|
112
112
|
classFilter: "native",
|
|
113
113
|
// POST /text, EXACTLY 100 ids per call — a longer list is refused — and the call is BILLED: screening an
|
|
@@ -239,6 +239,16 @@ export const CAPABILITIES = Object.freeze({
|
|
|
239
239
|
goodsTextMultiWord: "ordered-phrase",
|
|
240
240
|
// Several goods terms ride ONE clause joined by OR — this register expresses a list natively.
|
|
241
241
|
goodsTextListOr: true,
|
|
242
|
+
// ── THE SHORTEST TERM THE CONTAINS FORM ACCEPTS: none known, so none declared ───────────────────
|
|
243
|
+
//
|
|
244
|
+
// No vendor document available to this repository states one: the vendor publishes no public
|
|
245
|
+
// reference for this search API, and none was found on 2026-09-22. What is measured is narrower and is
|
|
246
|
+
// handled in core.js: a ONE-character first or last token inside a phrase chain is refused (HTTP
|
|
247
|
+
// 500), and two characters are answered. A two-letter `*TERM*` on its own is not recorded either way.
|
|
248
|
+
//
|
|
249
|
+
// `null` means the compiler keeps asking the contains form at every length, which is what this
|
|
250
|
+
// register has always been sent. A declared number must come from the vendor, not from a guess.
|
|
251
|
+
containsMinLength: null,
|
|
242
252
|
// The operator the goods clause rides. `EQUALS` on WHOLE WORDS: `CONTAINS` is a hard 400 here
|
|
243
253
|
// exactly as it is on APPLICANT_NAME, and so is a mid-word wildcard. Several words are asked for
|
|
244
254
|
// with `OR` inside the value.
|
|
@@ -285,7 +295,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
285
295
|
// WIRED: providers/clarivate/src/core.js builds its enumerate from
|
|
286
296
|
// makeEnumerate({ capabilities: {...CAPABILITIES.kernel} }) — these values are the LIVE seam settings,
|
|
287
297
|
// no longer a design note. pageGuard is 1 because /search is single-shot: there is no page 2 to
|
|
288
|
-
// fetch, so the guard can only ever be a backstop. namesChunkDefault = maxOrWidth (
|
|
298
|
+
// fetch, so the guard can only ever be a backstop. namesChunkDefault = maxOrWidth (496): the kernel
|
|
289
299
|
// chunks a wide OR-stack to the parser's nesting bound before it reaches the wire.
|
|
290
300
|
kernel: Object.freeze({
|
|
291
301
|
countProbe: "endpoint",
|
|
@@ -293,7 +303,7 @@ export const CAPABILITIES = Object.freeze({
|
|
|
293
303
|
pageSize: 100,
|
|
294
304
|
pageGuard: 1, // single-shot: there is no page 2
|
|
295
305
|
ceilingDefault: 600,
|
|
296
|
-
namesChunkDefault:
|
|
306
|
+
namesChunkDefault: 496,
|
|
297
307
|
providerWindow: "30000-result hard ceiling (tooManyResults, fails loud)",
|
|
298
308
|
// POST /search returns BARE GUIDS — the search row carries no mark text, classes, status or owner.
|
|
299
309
|
// POST /text (the screen call) is therefore the SOLE content source for an enumerated band, which
|