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
package/driver/crowd-context.mjs
CHANGED
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
// and the axis bands are never touched — this pass mutates no existing artifact.
|
|
37
37
|
|
|
38
38
|
import { NON_MATERIAL_AXES } from "./coverage-ledger.mjs";
|
|
39
|
+
import { containsFormSubstitution } from "./register-plan.mjs";
|
|
39
40
|
|
|
40
41
|
// ── caps — these bound SPEND, never sufficiency ─────────────────────────────────────────────────────
|
|
41
42
|
// Each cap limits how many provider calls / how many fetched records one evidence pass may buy. They
|
|
@@ -180,14 +181,22 @@ export function selectCrowdSlices(ledgerRows, planContext = {}) {
|
|
|
180
181
|
// the counts describe the SAME crowd the ledger row is about. An owner-scoped slice carries its
|
|
181
182
|
// `owner` onto EVERY minted entry for the same reason — un-owned counts would describe the wider
|
|
182
183
|
// formative crowd while claiming to describe the owner's slice.
|
|
183
|
-
|
|
184
|
+
//
|
|
185
|
+
// A TERM SHORTER THAN THE REGISTER'S CONTAINS FLOOR is counted on the exact form instead, the same
|
|
186
|
+
// rule the frozen plan's goods-narrowed questions and saturation probes follow: a register that
|
|
187
|
+
// refuses the contains form for a term that short answers no count at all, so the crowd this pass
|
|
188
|
+
// exists to describe would read "count unavailable". Same classes, same two counts per term, and the
|
|
189
|
+
// entry carries `contains_substituted` so the figure is never taken for a containing count.
|
|
190
|
+
export function mintSliceCountEntries(slice, i, { maxTerms = CROWD_MAX_TERMS_PER_SLICE, capabilities = null } = {}) {
|
|
184
191
|
const terms = slice.terms.slice(0, maxTerms);
|
|
185
192
|
const base = { axis: CROWD_CONTEXT_AXIS, regions: slice.regions ?? [], expected_kind: "count",
|
|
186
193
|
...(typeof slice.owner === "string" && slice.owner ? { owner: slice.owner } : {}) };
|
|
187
194
|
const out = [];
|
|
188
195
|
terms.forEach((t, j) => {
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
const sub = containsFormSubstitution(t, capabilities);
|
|
197
|
+
const form = sub ? { predicate: "exact", contains_substituted: sub } : { predicate: "default" };
|
|
198
|
+
out.push({ ...base, qid: `crowdctx:s${i}-t${j}-${slug(t)}-all`, ...form, term: t, nice_classes: [] });
|
|
199
|
+
out.push({ ...base, qid: `crowdctx:s${i}-t${j}-${slug(t)}-cls`, ...form, term: t, nice_classes: slice.nice_classes ?? [] });
|
|
191
200
|
});
|
|
192
201
|
out.push({
|
|
193
202
|
...base, qid: `crowdctx:s${i}-exact-count`, predicate: "exact",
|
|
@@ -325,10 +334,12 @@ const compactRecord = (r) => ({
|
|
|
325
334
|
* @param opts.executor INJECTED: async (entries) => band blocks (tests stub it; the pipeline passes
|
|
326
335
|
* the planExec-lane adapter). Null/absent ⇒ no lane ⇒ null (logged, non-fatal).
|
|
327
336
|
* @param opts.caps { maxSlices?, maxTermsPerSlice?, enumCap? } — spend bounds only.
|
|
337
|
+
* @param opts.capabilities the active register's declared capabilities, read for the contains floor
|
|
338
|
+
* only (containsMinLength). Absent ⇒ every term is counted on the contains form.
|
|
328
339
|
* @param opts.note / opts.log observability hooks (default no-ops; pipeline wires note()/runLog).
|
|
329
340
|
* @returns { json, md, stats } | null
|
|
330
341
|
*/
|
|
331
|
-
export async function buildCrowdContext({ ledger, planContext = {}, executor, caps = {}, note = () => {}, log = () => {} } = {}) {
|
|
342
|
+
export async function buildCrowdContext({ ledger, planContext = {}, executor, caps = {}, capabilities = null, note = () => {}, log = () => {} } = {}) {
|
|
332
343
|
const maxSlices = caps.maxSlices ?? CROWD_MAX_SLICES;
|
|
333
344
|
const maxTermsPerSlice = caps.maxTermsPerSlice ?? CROWD_MAX_TERMS_PER_SLICE;
|
|
334
345
|
const enumCap = caps.enumCap ?? CROWD_ENUM_CAP;
|
|
@@ -344,7 +355,7 @@ export async function buildCrowdContext({ ledger, planContext = {}, executor, ca
|
|
|
344
355
|
if (selected.length > slices.length) note(`crowd-context: ${selected.length} qualifying slice(s), gathering the first ${slices.length} (spend cap — the rest keep their ledger disclosure unchanged)`);
|
|
345
356
|
try {
|
|
346
357
|
// ── phase 1: one batched executor call for every count probe ─────────────────────────────────
|
|
347
|
-
const countEntries = slices.flatMap((s, i) => mintSliceCountEntries(s, i, { maxTerms: maxTermsPerSlice }));
|
|
358
|
+
const countEntries = slices.flatMap((s, i) => mintSliceCountEntries(s, i, { maxTerms: maxTermsPerSlice, capabilities }));
|
|
348
359
|
const byQid = new Map((await executor(countEntries) ?? []).filter((b) => b && b.qid).map((b) => [b.qid, b]));
|
|
349
360
|
// ── phase 2: enumerate each exact subset the count proved tractable (0 < hits ≤ cap) ─────────
|
|
350
361
|
// A verified-zero count needs no call (enumerating an empty subset returns the empty subset);
|
|
@@ -366,7 +377,9 @@ export async function buildCrowdContext({ ledger, planContext = {}, executor, ca
|
|
|
366
377
|
const all = byQid.get(`crowdctx:s${i}-t${j}-${slug(t)}-all`);
|
|
367
378
|
const cls = byQid.get(`crowdctx:s${i}-t${j}-${slug(t)}-cls`);
|
|
368
379
|
const bad = !all || all.error || !cls || cls.error;
|
|
369
|
-
|
|
380
|
+
const sub = containsFormSubstitution(t, capabilities);
|
|
381
|
+
return { term: t, all_classes: Number(all?.total_hits) || 0, in_scope: Number(cls?.total_hits) || 0,
|
|
382
|
+
...(bad ? { error: true } : {}), ...(sub ? { contains_substituted: sub } : {}) };
|
|
370
383
|
});
|
|
371
384
|
const c = byQid.get(`crowdctx:s${i}-exact-count`);
|
|
372
385
|
const hits = Number(c?.total_hits) || 0;
|
|
@@ -190,7 +190,7 @@ export function drainerVerdict({ stamp, headCommit, isAlive, processes, ppidOf =
|
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
if (!head) {
|
|
193
|
-
return { state: "
|
|
193
|
+
return { state: "skip", blocked: true, message: `drainer pid ${pid} is alive on ${short(held)}${via}, but the checkout's own HEAD `
|
|
194
194
|
+ `could not be read, so the two could not be compared.${strayNote}` };
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doBatchScreen, doEnumerate, doExecutePlan, DEFAULT_BASE,
|
|
34
34
|
} from "../../../providers/clarivate/src/core.js";
|
|
35
35
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
36
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
36
37
|
|
|
37
38
|
const API_KEY = process.env.CLARIVATE_API_KEY || "";
|
|
38
39
|
const BASE = process.env.CLARIVATE_API_BASE || DEFAULT_BASE;
|
|
@@ -136,8 +137,9 @@ serve({
|
|
|
136
137
|
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term — plain ASCII letters/digits, syllable-separated by single spaces, no tone marks or diacritics (华威豹 → \"HUA WEI BAO\", ティキスラッシュ → \"TIKI SURASSHU\"). MANDATORY beside a non-Latin term: without it this register cannot answer the characters and the slice defers. Single-term proposals only (never an OR-stack, never predicate:owner), and never on a term that is already Latin." },
|
|
137
138
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the query is the owner×term intersection (the owner's filings within the term band). Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
138
139
|
nice_classes: { type: "array", items: {} },
|
|
139
|
-
|
|
140
|
-
|
|
140
|
+
// The narrowing fields every register serves (proposal-fields.mjs); this register's two facts ride in.
|
|
141
|
+
...narrowingFields({ regions: "OPTIONAL. Omit to inherit the frozen plan's regions (the matter's territorial scope) — this provider REQUIRES at least one office on every request, so an omitted list is backfilled from the plan, never treated as a worldwide sweep. Supply it only to search a NARROWER set than the matter's scope.",
|
|
142
|
+
goodsNote: "One office in this provider's vocabulary refuses the field and fails the whole call, so it is left out of that office's request and recorded as asked-without-goods rather than dropped in silence." }),
|
|
141
143
|
rationale: { type: "string" },
|
|
142
144
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
143
145
|
} } },
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { serve } from "./stdio-server.mjs";
|
|
10
10
|
import { CAPABILITIES, doSearch, doRecordFetch, doImageFetch, doExpandPhoneme, doBatchScreen, doEnumerate, doExecutePlan } from "../../../providers/corsearch/src/core.js";
|
|
11
11
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
12
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
12
13
|
|
|
13
14
|
const COOKIE = process.env.CORSEARCH_SESSION_KEY || "";
|
|
14
15
|
const tctx = (kind) => ({
|
|
@@ -118,7 +119,8 @@ serve({
|
|
|
118
119
|
term: { type: "string" }, terms: { type: "array", items: { type: "string" } },
|
|
119
120
|
romanization: { type: "string", description: "OPTIONAL on this provider (its index holds the characters and answers them directly), but STATE IT anyway for a non-Latin term — the plan is provider-neutral and the entry carries both forms for whichever register expresses it. Latin-script form only: plain ASCII letters/digits, syllable-separated by single spaces, no tone marks or diacritics. Single-term proposals only; never on an already-Latin term." },
|
|
120
121
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the query is the owner×term intersection (the owner's filings within the term band). Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
121
|
-
nice_classes: { type: "array", items: {} },
|
|
122
|
+
nice_classes: { type: "array", items: {} },
|
|
123
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
122
124
|
rationale: { type: "string" },
|
|
123
125
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
124
126
|
} } },
|
|
@@ -55,7 +55,7 @@ serve({
|
|
|
55
55
|
description: `Up to ${MAX_ROWS_PER_CALL} rows per call. Send more in a further call; the answer tells you what is left.`,
|
|
56
56
|
items: { type: "object", properties: {
|
|
57
57
|
row_id: { type: "string", description: "A driver row's id, exactly as the dispatch's obligations block lists it. Omit on a seat row you are adding — the driver mints seat row ids." },
|
|
58
|
-
status: { type: "string", description: "EXACTLY one bare token of confirmed-clean / coverage-limited / deferred. Qualifiers go in the reason." },
|
|
58
|
+
status: { type: "string", description: "EXACTLY one bare token of confirmed-clean / coverage-limited / deferred / withheld-by-judgment. Qualifiers go in the reason." },
|
|
59
59
|
reason: { type: "string", description: "The sentence the lawyer reads — say what was searched and what was not, in a lawyer's words, never the engine's." },
|
|
60
60
|
kind: { type: "string", description: "\"seat\" on a row you add for a coverage unit the plan does not contain. Never anything else." },
|
|
61
61
|
axis: { type: "string", description: "Seat rows only: EXACTLY one bare token of the closed register-axis vocabulary the dispatch lists." },
|
|
@@ -56,6 +56,9 @@ import { validateGridSpec } from "../../../providers/perplexity/src/core.js";
|
|
|
56
56
|
// the disk work and disposition-call.mjs owns the decision. One direction of import, no second opinion.
|
|
57
57
|
import { recordDispositions } from "../../disposition-tool.mjs";
|
|
58
58
|
import { MAX_ROWS_PER_CALL } from "../../disposition-call.mjs";
|
|
59
|
+
// The lane's second statement, and a different one: which coverage units were searched to what end. Its
|
|
60
|
+
// own module and its own file, never a row in the disposition form — see record_coverage_status below.
|
|
61
|
+
import { recordCoverageStatus, COMMON_LAW_COVERAGE_STATUSES } from "../../common-law-coverage-status.mjs";
|
|
59
62
|
|
|
60
63
|
// ── B — THE TYPED DISPOSITION TRANSPORT ─────────────────────────────────────────────────────────────
|
|
61
64
|
//
|
|
@@ -68,15 +71,21 @@ import { MAX_ROWS_PER_CALL } from "../../disposition-call.mjs";
|
|
|
68
71
|
// THE SPEC PATH IS THE SEAT'S ONLY PATH ARGUMENT, and it is the same driver-written file the grid tool
|
|
69
72
|
// was given. That rule: the path is the DRIVER'S, taken from the spec it wrote. Two derivations of one
|
|
70
73
|
// filename is the drift that cost weeks.
|
|
71
|
-
|
|
72
|
-
const { grid_spec_path
|
|
74
|
+
function specFrom(params) {
|
|
75
|
+
const { grid_spec_path } = params ?? {};
|
|
73
76
|
if (!grid_spec_path)
|
|
74
|
-
return {
|
|
77
|
+
return { error: "ERROR: grid_spec_path is required — it is the same driver-written spec path the grid tool was given. Do not compose a path." };
|
|
75
78
|
let spec;
|
|
76
79
|
try { spec = validateGridSpec(JSON.parse(readFileSync(grid_spec_path, "utf8"))); }
|
|
77
|
-
catch (err) { return {
|
|
80
|
+
catch (err) { return { error: `ERROR: grid_spec_path unreadable/invalid (${err.message}). The driver writes this file; do not hand-author it.` }; }
|
|
78
81
|
if (!/\/studio\/(?:prelim|clearance)-search\//.test(spec.output_path)) // either spelling: an install keeps the studio segment it has
|
|
79
|
-
return {
|
|
82
|
+
return { error: `ERROR: grid spec.output_path must be within a studio/clearance-search run dir; got ${spec.output_path}` };
|
|
83
|
+
return { spec };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function record_dispositions(params) {
|
|
87
|
+
const { spec, error } = specFrom(params);
|
|
88
|
+
if (error) return { isError: true, text: error };
|
|
80
89
|
// NEVER THROWN PAST THIS POINT. An exception surfaces to the seat as a tool error naming no row, which
|
|
81
90
|
// tells it nothing about what to fix — the failure mode this transport exists to end.
|
|
82
91
|
try {
|
|
@@ -97,6 +106,23 @@ async function record_dispositions(params) {
|
|
|
97
106
|
}
|
|
98
107
|
}
|
|
99
108
|
|
|
109
|
+
// ── THE COVERAGE STATUS, THE LANE'S SECOND TOOL ON ITS OWN KEY ───────────────────────────────────────
|
|
110
|
+
//
|
|
111
|
+
// A sibling of record_dispositions, not a field on it. A meaning ruling is addressed by an obligation's
|
|
112
|
+
// number; a coverage status by a coverage unit. `record_coverage` and `record_register_digest` stay two
|
|
113
|
+
// tools for the same reason. The key is still granted by exactly one lane's group list, so no other seat
|
|
114
|
+
// gains a writer. Both tools resolve the spec through specFrom() above, one resolution for both.
|
|
115
|
+
async function record_coverage_status(params) {
|
|
116
|
+
const { spec, error } = specFrom(params);
|
|
117
|
+
if (error) return { isError: true, text: error };
|
|
118
|
+
try {
|
|
119
|
+
const r = recordCoverageStatus(spec, params);
|
|
120
|
+
return { isError: !r.ok, text: r.text };
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { isError: true, text: `ERROR: the driver could not record this call (${String(e?.message ?? e).slice(0, 200)}). This is a driver fault, not a fault in your statuses — do not re-type them.` };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
100
126
|
serve({
|
|
101
127
|
name: "dispositions", version: "0.1.0",
|
|
102
128
|
tools: [{
|
|
@@ -135,5 +161,21 @@ serve({
|
|
|
135
161
|
} },
|
|
136
162
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
137
163
|
handler: record_dispositions,
|
|
164
|
+
}, {
|
|
165
|
+
name: "record_coverage_status",
|
|
166
|
+
description: "Record the status of each row of your coverage ledger. Send VALUES, not a file: one entry per ledger row, and the driver writes the record. Entries that validate are kept even when others in the same call are refused, and a later entry for the same unit replaces the earlier one.",
|
|
167
|
+
inputSchema: { type: "object", required: ["grid_spec_path", "rows"], properties: {
|
|
168
|
+
grid_spec_path: { type: "string", description: "Absolute path to the driver-written grid spec — the same one the grid tool was given." },
|
|
169
|
+
rows: {
|
|
170
|
+
type: "array",
|
|
171
|
+
description: "One entry per coverage ledger row.",
|
|
172
|
+
items: { type: "object", required: ["unit", "status"], properties: {
|
|
173
|
+
unit: { type: "string", description: "The coverage unit, exactly as your ledger row names it." },
|
|
174
|
+
status: { type: "string", enum: [...COMMON_LAW_COVERAGE_STATUSES], description: `EXACTLY one bare token of ${COMMON_LAW_COVERAGE_STATUSES.join(" / ")}. Qualifiers go in the ledger row.` },
|
|
175
|
+
} },
|
|
176
|
+
},
|
|
177
|
+
} },
|
|
178
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
179
|
+
handler: record_coverage_status,
|
|
138
180
|
}],
|
|
139
181
|
});
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
doEnumerate, doExecutePlan,
|
|
34
34
|
} from "../../../providers/euipo/src/core.js";
|
|
35
35
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
36
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
36
37
|
|
|
37
38
|
// The core resolves credentials from the environment; AUTH stays an object so a future knob (a pinned
|
|
38
39
|
// environment, a second subscription) does not change every call site.
|
|
@@ -154,6 +155,7 @@ serve({
|
|
|
154
155
|
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term. On THIS source it is NOT used to rescue the slice — nativeScriptIndex is true, so the characters are sent as themselves and the romanisation is carried for the reader only." },
|
|
155
156
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the owner×term intersection. Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
156
157
|
nice_classes: { type: "array", items: {} },
|
|
158
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
157
159
|
rationale: { type: "string" },
|
|
158
160
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
159
161
|
} } },
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
CAPABILITIES, doSearch, doRecordFetch, doBatchScreen, doImageFetch, doEnumerate, doExecutePlan,
|
|
35
35
|
} from "../../../providers/free-tier/src/core.js";
|
|
36
36
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
37
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
37
38
|
|
|
38
39
|
// NULL, and it is not a placeholder. Each member core resolves its OWN credentials from the environment
|
|
39
40
|
// — EUIPO its OAuth pair, the index its file path — so there is no single auth object a composite could
|
|
@@ -167,6 +168,7 @@ serve({
|
|
|
167
168
|
romanization: { type: "string" },
|
|
168
169
|
owner: { type: "string" },
|
|
169
170
|
nice_classes: { type: "array", items: {} },
|
|
171
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
170
172
|
rationale: { type: "string" },
|
|
171
173
|
term_literal: { type: "boolean" },
|
|
172
174
|
} } },
|
|
@@ -678,7 +678,13 @@ const LOCAL = {
|
|
|
678
678
|
// `mcp__dispositions__record_dispositions`). That is a real argv-surface change on four stages, so it
|
|
679
679
|
// ships status:merged-awaiting-e2e — the byte pins in recording-grant-preservation.test.mjs move with
|
|
680
680
|
// it and no live run has exercised the new name.
|
|
681
|
-
|
|
681
|
+
//
|
|
682
|
+
// ── AND ITS SECOND TOOL, `record_coverage_status` — an allowlist growing by one token on an
|
|
683
|
+
// ALREADY-TOOLED key that exactly one lane holds, so no other seat gains a writer and no argv-surface
|
|
684
|
+
// transition fires. It is not a field on `record_dispositions`: a meaning ruling and a coverage status are
|
|
685
|
+
// two statements, addressed two ways, as `record_coverage` and `record_register_digest` are. Ordered by
|
|
686
|
+
// driver/skills/clearance-common-law/SKILL.md beside the coverage ledger.
|
|
687
|
+
dispositions: { script: "dispositions-server.mjs", tools: ["record_dispositions", "record_coverage_status"] },
|
|
682
688
|
// ── UNIT-NOTE: the register unit's audit note, and the first own-key transport that MOVES an artifact ─
|
|
683
689
|
//
|
|
684
690
|
// `coverage`'s and `declination`'s shape, chosen for a reason those two did not have. Those stages keep
|
|
@@ -700,7 +706,7 @@ const LOCAL = {
|
|
|
700
706
|
// ONE TOOL ON ITS OWN KEY, not on `register` — that key is the funnel's and a record tool added to it
|
|
701
707
|
// would be enumerated into every register-unit seat's grant AND every other holder's. Same rule the
|
|
702
708
|
// three entries above follow.
|
|
703
|
-
"unit-note": { script: "unit-note-server.mjs", tools: ["record_unit_note"] },
|
|
709
|
+
"unit-note": { script: "unit-note-server.mjs", tools: ["record_unit_note", "record_withheld_families"] },
|
|
704
710
|
// ── RECORDING — DERIVED from the registry above, one entry per stage, in registry order ──────────
|
|
705
711
|
//
|
|
706
712
|
// These rows were hand-written here until the collapse. They are LAST in this object on purpose:
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
|
+
// engine/mcp/probe-server.mjs — the one tool the engine probe asks the engine to call.
|
|
5
|
+
//
|
|
6
|
+
// A turn with no tools proves the credential and the model, and nothing about whether the engine can use
|
|
7
|
+
// the tools every search stage is given. On some hosts codex's own sandbox refuses every tool call while
|
|
8
|
+
// the turn reports success, and a probe with no tool passed there. So the probe hands the engine this
|
|
9
|
+
// server, asks it to call `ping` once, and passes only when the reply carries what `ping` returned.
|
|
10
|
+
//
|
|
11
|
+
// WHAT IT RETURNS IS THE PROOF. Its one argument is a random word the probe mints for each
|
|
12
|
+
// turn and gives only to this process, so a reply that carries it cannot be the model guessing. Read-only;
|
|
13
|
+
// it touches no file, no network and no run.
|
|
14
|
+
import { serve } from "./stdio-server.mjs";
|
|
15
|
+
|
|
16
|
+
serve({
|
|
17
|
+
name: "probe", version: "0.1.0",
|
|
18
|
+
tools: [{
|
|
19
|
+
name: "ping",
|
|
20
|
+
description: "Return the word this check is waiting for.",
|
|
21
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
22
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
23
|
+
handler: async () => {
|
|
24
|
+
const word = String(process.argv[2] ?? "").trim();
|
|
25
|
+
return word ? word : { isError: true, text: "ping: this server was started without a word to return" };
|
|
26
|
+
},
|
|
27
|
+
}],
|
|
28
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
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
|
+
// proposal-fields.mjs — the narrowing fields of `register_propose_supplemental`, ONE definition for every
|
|
4
|
+
// register's server.
|
|
5
|
+
//
|
|
6
|
+
// The reading turn narrows a crowded identical question through this tool: by goods words, by market
|
|
7
|
+
// (`regions`), and it names the crowd it replaces (`narrows`). The shared mint (supplemental.mjs) has
|
|
8
|
+
// handled all three for every register, but only the Clarivate server declared them, and Corsearch
|
|
9
|
+
// declared `regions` alone. A model reads the schema it is served, so on Signa the reading turn could
|
|
10
|
+
// narrow only by class. The product is register-agnostic: every server now spreads these same
|
|
11
|
+
// properties into its proposal schema, and what differs by register is a capability fact the server
|
|
12
|
+
// passes in, never a missing field.
|
|
13
|
+
//
|
|
14
|
+
// The words are the shipped words, moved rather than rewritten: the goods and `narrows` text is what the
|
|
15
|
+
// Clarivate server already served, and each server keeps the `regions` sentence it (or its sibling
|
|
16
|
+
// register) already carried. A register that cannot search goods text, or cannot offer alternatives in
|
|
17
|
+
// one goods clause, is handled in the mint, the same way the compiler handles it.
|
|
18
|
+
|
|
19
|
+
const GOODS_WORDS = "OPTIONAL goods narrowing on a MARK-TEXT proposal: the same question, limited to filings whose "
|
|
20
|
+
+ "goods and services description carries one of these words. This is the FIRST move when the identical mark comes "
|
|
21
|
+
+ "back as a count instead of a list — the words are the ones the variants stage already wrote for this matter, the "
|
|
22
|
+
+ "client's own wording plus the synonyms. Single words or short phrases as a specification would write them, no "
|
|
23
|
+
+ "wildcards. Not allowed on predicate:owner.";
|
|
24
|
+
|
|
25
|
+
const NARROWS = "OPTIONAL: the qid of the CROWDED question this proposal replaces. Put it on a narrowing — the same "
|
|
26
|
+
+ "question limited by goods, by market, by the dominant word or to one class — so the record shows the crowd and the "
|
|
27
|
+
+ "question that answered it side by side, each with its own count. A narrowing that does not name what it replaced "
|
|
28
|
+
+ "leaves the crowd looking unanswered.";
|
|
29
|
+
|
|
30
|
+
/** The `regions` sentence a register whose requests need no office carries (shipped on Corsearch). */
|
|
31
|
+
export const REGIONS_CODES = "UPPERCASE 2-letter region codes, e.g. ['US','EU','CH'] — never spelled-out names "
|
|
32
|
+
+ "(recognized display names are normalized; unknown values are rejected)";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The three narrowing properties, for a proposal item's `properties`.
|
|
36
|
+
* @param opts.regions the register's `regions` description (REGIONS_CODES unless the register states more)
|
|
37
|
+
* @param opts.goodsNote a sentence appended to the goods description, for a register fact the model must know
|
|
38
|
+
*/
|
|
39
|
+
export function narrowingFields({ regions = REGIONS_CODES, goodsNote = "" } = {}) {
|
|
40
|
+
return {
|
|
41
|
+
goods_words: { type: "array", items: { type: "string" }, description: goodsNote ? `${GOODS_WORDS} ${goodsNote}` : GOODS_WORDS },
|
|
42
|
+
regions: { type: "array", items: { type: "string" }, description: regions },
|
|
43
|
+
narrows: { type: "string", description: NARROWS },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -497,6 +497,19 @@ serve({
|
|
|
497
497
|
},
|
|
498
498
|
watchlist_owners: { type: "array", items: { type: "string" },
|
|
499
499
|
description: "Real register owners the plan compiles owner lanes from — never sectors or descriptions." },
|
|
500
|
+
goods_words: { type: "array", items: { type: "string" },
|
|
501
|
+
description:
|
|
502
|
+
"The words the register search is narrowed to: the client's own goods and services wording " +
|
|
503
|
+
"first, then the words other filings use for the same goods that it does not already " +
|
|
504
|
+
"contain. Single words or short phrases as a specification would write them, no wildcards, " +
|
|
505
|
+
"at most 24. Each is matched against the goods and services description of registered " +
|
|
506
|
+
"marks, so use words a specification would contain; a word broader than the goods widens " +
|
|
507
|
+
"the search instead of narrowing it. The register cannot read the words and, or, not, adj " +
|
|
508
|
+
"or near inside an item — an item containing one is searched without it. Send an EMPTY " +
|
|
509
|
+
"LIST when you have considered the goods and no word is worth narrowing by; omit the " +
|
|
510
|
+
"field only when the matter states no goods at all. Those are different answers and the " +
|
|
511
|
+
"run records which one you gave.",
|
|
512
|
+
},
|
|
500
513
|
scope_ledger: {
|
|
501
514
|
type: "array",
|
|
502
515
|
description:
|
|
@@ -826,6 +839,23 @@ serve({
|
|
|
826
839
|
},
|
|
827
840
|
},
|
|
828
841
|
},
|
|
842
|
+
// THE SAME DEFECT TWICE MORE, and the same fix. The dispatch asks for both of these in its own
|
|
843
|
+
// imperative, the acceptor validates and records them, and the plan compile reads the classes —
|
|
844
|
+
// and until now neither was a property here, so a frame following its schema could send neither.
|
|
845
|
+
// A real run's frame carried no added class although the goods reached one. Shape only: the
|
|
846
|
+
// dispatch already says what to send and why, so no sentence the model reads is added here.
|
|
847
|
+
identified_classes: {
|
|
848
|
+
type: "array",
|
|
849
|
+
items: {
|
|
850
|
+
type: "object",
|
|
851
|
+
required: ["class", "reason"],
|
|
852
|
+
properties: {
|
|
853
|
+
class: { type: "integer", minimum: 1, maximum: 45 },
|
|
854
|
+
reason: { type: "string" },
|
|
855
|
+
},
|
|
856
|
+
},
|
|
857
|
+
},
|
|
858
|
+
ratified_forms: { type: "array", items: { type: "string" } },
|
|
829
859
|
// OFFERED HERE, OR NEVER SENT. The acceptor took this field for a beta and the plan acted on it,
|
|
830
860
|
// and no frame ever proposed one, because the schema a model is given did not offer it. Worded as
|
|
831
861
|
// the owner approved it; it is model-facing prose, so its wording is his.
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { serve } from "./stdio-server.mjs";
|
|
12
12
|
import { doSearch, doRecordFetch, doEnumerate, doExecutePlan, DEFAULT_BASE } from "../../../providers/signa/src/core.js";
|
|
13
13
|
import { proposeSupplemental } from "./supplemental.mjs";
|
|
14
|
+
import { narrowingFields } from "./proposal-fields.mjs";
|
|
14
15
|
import { CAPABILITIES } from "../../../providers/signa/src/capabilities.js";
|
|
15
16
|
|
|
16
17
|
const API_KEY = process.env.SIGNA_API_KEY || "";
|
|
@@ -101,6 +102,7 @@ serve({
|
|
|
101
102
|
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term. On THIS source it is NOT used to rescue the slice — nativeScriptIndex is true, so the characters are sent as themselves and the romanisation is carried for the reader only." },
|
|
102
103
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the owner×term intersection, served by filters.owner_name in the same request. Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
103
104
|
nice_classes: { type: "array", items: {} },
|
|
105
|
+
...narrowingFields(), // the narrowing fields every register serves (proposal-fields.mjs)
|
|
104
106
|
rationale: { type: "string" },
|
|
105
107
|
term_literal: { type: "boolean", description: "TRUE only when the term genuinely IS the mark verbatim (a multi-word slogan mark, a mark carrying an anchored star) — it bypasses the term-shape lint. Never use it to push a label through." },
|
|
106
108
|
} } },
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync } from "node:fs";
|
|
31
31
|
import { dirname, join } from "node:path";
|
|
32
32
|
import { driverDir } from "../../../shared/driver-dir.mjs"; //
|
|
33
|
-
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, resolveRegions, houseElementOf, withoutHouseElementTerms } from "../../register-plan.mjs";
|
|
33
|
+
import { PLAN_PREDICATES, PLAN_MAX_OR_WIDTH, PLAN_MAX_NAME_LENGTH, fingerprint, ownerIntersectionGap, goodsTextGap, resolveRegions, houseElementOf, withoutHouseElementTerms, containsFormSubstitution } from "../../register-plan.mjs";
|
|
34
34
|
import { entryTermIssues } from "../../../providers/_shared/term-shape.mjs";
|
|
35
35
|
import { isNonLatinTerm, romanizationRefusal, romanizationSpellings, nativeScriptIndexGap } from "../../../providers/_shared/script-form.mjs";
|
|
36
36
|
|
|
@@ -64,13 +64,37 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
64
64
|
// first, non-priority ones keep their relative order behind them. The cap VALUES are unchanged
|
|
65
65
|
// (no count threshold moves), and with no priorityClasses the order is byte-identical to before.
|
|
66
66
|
const prio = new Set((priorityClasses ?? []).map((c) => String(c).trim()).filter(Boolean));
|
|
67
|
-
|
|
67
|
+
// ONE QUESTION PER GOODS WORD where the register cannot offer alternatives in one goods clause — the
|
|
68
|
+
// compiler's own rule for such a register (register-plan.mjs, goodsTextListOr). Joined, the words would
|
|
69
|
+
// be intersected and the answer would narrow as the list grew; the connector refuses that shape
|
|
70
|
+
// outright. Each word keeps the proposal's index, so a refusal still names the proposal it came from.
|
|
71
|
+
const perWord = capabilities?.goodsTextSearch === true && capabilities?.goodsTextListOr === false;
|
|
72
|
+
// ONE QUESTION PER NAME where the register has no OR at all (maxOrWidth 1) — the compiler's own width
|
|
73
|
+
// for such a register. A batch of names went out as windows of one name each under ONE question, and
|
|
74
|
+
// the enumerate kernel stopped the whole batch at the first name that crowded or failed, so every name
|
|
75
|
+
// after it was never sent. Split, each name is its own question: its own count, its own crowd decision
|
|
76
|
+
// and its own receipt row. The names of one batch still spend ONE slot of the per-call and per-axis
|
|
77
|
+
// caps between them, as the batch did; `split_of` names the batch so the next call counts it once.
|
|
78
|
+
const perName = Number(capabilities?.maxOrWidth) === 1;
|
|
79
|
+
const indexed = (proposals ?? []).flatMap((p, i) => {
|
|
80
|
+
const names = perName && Array.isArray(p?.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : [];
|
|
81
|
+
const splitOf = names.length > 1 && names.length <= PLAN_MAX_OR_WIDTH
|
|
82
|
+
? fingerprint({ axis, predicate: String(p?.predicate ?? "default"), names: [...names].sort(), i }) : null;
|
|
83
|
+
const byName = splitOf ? names.map((t) => { const { terms: _batch, ...rest } = p; return { ...rest, term: t }; }) : [p ?? {}];
|
|
84
|
+
return byName.flatMap((q) => {
|
|
85
|
+
const words = perWord && Array.isArray(q?.goods_words) ? q.goods_words.map((w) => String(w ?? "").trim()).filter(Boolean) : [];
|
|
86
|
+
return words.length > 1
|
|
87
|
+
? words.map((w, k) => ({ p: { ...q, goods_words: [w] }, i, slot: `${i}:${k}`, splitOf }))
|
|
88
|
+
: [{ p: q, i, slot: `${i}:0`, splitOf }];
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
const slots = new Set();
|
|
68
92
|
const ordered = prio.size
|
|
69
93
|
? [...indexed.filter(({ p }) => inPriority(p, prio)), ...indexed.filter(({ p }) => !inPriority(p, prio))]
|
|
70
94
|
: indexed;
|
|
71
|
-
for (const { p, i } of ordered) {
|
|
95
|
+
for (const { p, i, slot, splitOf } of ordered) {
|
|
72
96
|
const issue = (msg) => rejected.push({ index: i, issue: msg, proposal: compactProposal(p) });
|
|
73
|
-
if (
|
|
97
|
+
if (!slots.has(slot) && slots.size >= perCall) { issue(`per-call cap ${perCall} reached`); continue; }
|
|
74
98
|
const predicate = String(p.predicate ?? "default");
|
|
75
99
|
if (!PLAN_PREDICATES.includes(predicate)) { issue(`unknown predicate "${predicate.slice(0, 20)}" (one of: ${PLAN_PREDICATES.join(", ")})`); continue; }
|
|
76
100
|
const terms = Array.isArray(p.terms) ? p.terms.map((t) => String(t ?? "").trim()).filter(Boolean) : null;
|
|
@@ -107,6 +131,15 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
107
131
|
if (!isNonLatinTerm(term)) { issue(`a romanization belongs ONLY on a non-Latin term — "${term.slice(0, 40)}" is already Latin script, so this romanization transliterates a DIFFERENT string; drop it or fix the term`); continue; }
|
|
108
132
|
romanizedTerms = romanizationSpellings(romanRaw);
|
|
109
133
|
}
|
|
134
|
+
// THE GOODS NARROWING, validated where every other field is. Whole words or short phrases as a
|
|
135
|
+
// specification writes them; a wildcard is a hard refusal on this field at the register, and an
|
|
136
|
+
// operator word inside an item is stripped downstream by the shared reader, so nothing here needs
|
|
137
|
+
// to invent a second rule for either.
|
|
138
|
+
const goodsWords = (Array.isArray(p.goods_words) ? p.goods_words : (typeof p.goods_words === "string" ? [p.goods_words] : []))
|
|
139
|
+
.map((w) => String(w ?? "").trim()).filter(Boolean);
|
|
140
|
+
if (p.goods_words != null && !goodsWords.length) { issue("goods_words, when present, must carry at least one word"); continue; }
|
|
141
|
+
if (goodsWords.some((w) => /[*?]/.test(w))) { issue("a goods word carries a wildcard, which this register refuses on that field"); continue; }
|
|
142
|
+
if (goodsWords.length && predicate === "owner") { issue("goods_words narrows a MARK-TEXT question; on predicate:owner the owner name is the term"); continue; }
|
|
110
143
|
const term_literal = p.term_literal === true;
|
|
111
144
|
// A1 — the same term-shape/term-predicate lint the plan freeze enforces, at the PROPOSAL seam:
|
|
112
145
|
// the model gets the reason IN-TURN (rejected[]) and can re-propose the mark-shaped term — a
|
|
@@ -205,6 +238,19 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
205
238
|
for (const r of covered) regions.push(r);
|
|
206
239
|
}
|
|
207
240
|
}
|
|
241
|
+
// A CONTAINS-FORM PROPOSAL ON A TERM SHORTER THAN THE REGISTER'S FLOOR is asked on the exact form,
|
|
242
|
+
// the rule the compiler applies to its own goods-narrowed questions and saturation probes: the
|
|
243
|
+
// register refuses the contains form for a term that short, so the question as proposed would come
|
|
244
|
+
// back as an error rather than an answer. Same classes, goods words and scope, one entry for one,
|
|
245
|
+
// and the entry says so. Decided BEFORE the fingerprint, so the qid names the question actually
|
|
246
|
+
// asked. A stack is switched only when every member is below the floor; a mixed stack keeps the
|
|
247
|
+
// form the model chose, and its short members are refused and disclosed on their own.
|
|
248
|
+
let askedPredicate = predicate;
|
|
249
|
+
let substituted = null;
|
|
250
|
+
if (predicate === "default") {
|
|
251
|
+
const subs = (terms ?? [term]).map((t) => containsFormSubstitution(t, capabilities));
|
|
252
|
+
if (subs.length && subs.every(Boolean)) { askedPredicate = "exact"; substituted = subs[0]; }
|
|
253
|
+
}
|
|
208
254
|
const anchor = terms ? terms[0] : term;
|
|
209
255
|
// The fingerprint (⇒ the qid) deliberately EXCLUDES the romanization: the qid names the QUESTION
|
|
210
256
|
// (which term, which predicate, which scope) and the romanisation is carriage, not a different
|
|
@@ -212,37 +258,68 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
212
258
|
// with the Latin form added — the exact wedge shape the regions inheritance above exists to kill.
|
|
213
259
|
// Instead a re-proposal that adds a romanisation to a stored bare qid ENRICHES it (below), the
|
|
214
260
|
// same field-level, never-term-changing merge extendRegisterPlan applies to the dictated plan.
|
|
215
|
-
|
|
216
|
-
|
|
261
|
+
// …and it deliberately INCLUDES the goods narrowing, for the opposite reason. A goods-limited
|
|
262
|
+
// re-ask of a crowded question is the same term, predicate, classes and scope — it differs only by
|
|
263
|
+
// what the filings must cover. Excluded, it would mint the crowd's own qid and be read as a
|
|
264
|
+
// re-proposal of the question it exists to replace, so the first move against a crowd would
|
|
265
|
+
// silently become no move at all.
|
|
266
|
+
const fp = String(fingerprint({ predicate: askedPredicate, term: term || null, terms: terms || null, nice_classes: nice, regions,
|
|
267
|
+
...(owner ? { owner } : {}), ...(goodsWords.length ? { goods: [...goodsWords].sort() } : {}) })).replace(/^fnv1a:/, "");
|
|
268
|
+
const qid = `supp:${axis}:${askedPredicate}:${slug(anchor)}:${fp.slice(0, 8)}`;
|
|
217
269
|
if (existingQids.has(qid) || minted.some((e) => e.qid === qid)) {
|
|
218
270
|
reused.push(qid);
|
|
219
271
|
if (romanizedTerms && existingQids.has(qid)) enriched.push({ qid, term, romanizedTerms });
|
|
220
272
|
continue;
|
|
221
273
|
}
|
|
222
|
-
if (
|
|
223
|
-
|
|
274
|
+
if (!slots.has(slot)) {
|
|
275
|
+
if (budget <= 0) { issue(`per-axis cap ${axisMax} reached — assess whether an existing supplemental already covers this`); continue; }
|
|
276
|
+
budget -= 1;
|
|
277
|
+
}
|
|
278
|
+
slots.add(slot);
|
|
224
279
|
const entry = {
|
|
225
|
-
qid, axis, predicate,
|
|
280
|
+
qid, axis, predicate: askedPredicate,
|
|
281
|
+
...(substituted ? { contains_substituted: substituted } : {}),
|
|
226
282
|
...(terms ? { terms } : { term }),
|
|
227
283
|
...(romanizedTerms ? { romanizedTerms } : {}),
|
|
228
284
|
...(owner ? { owner } : {}),
|
|
285
|
+
// Carried as `goods_text`, the field the plan and every connector already speak — the tool calls
|
|
286
|
+
// it `goods_words` because that is what the manual and the manifest call it to the model.
|
|
287
|
+
...(goodsWords.length ? { goods_text: goodsWords } : {}),
|
|
288
|
+
// What this question REPLACED. The ledger shows the crowd and its narrowing together, each with
|
|
289
|
+
// its own count, so a crowd never reads as a question nobody answered.
|
|
290
|
+
...(typeof p.narrows === "string" && p.narrows.trim() ? { narrows: p.narrows.trim().slice(0, 200) } : {}),
|
|
229
291
|
...(term_literal ? { term_literal: true } : {}),
|
|
230
292
|
nice_classes: nice, regions,
|
|
231
293
|
expected_kind: "enumerate",
|
|
232
294
|
origin: "supplemental",
|
|
295
|
+
...(splitOf ? { split_of: splitOf } : {}),
|
|
233
296
|
...(typeof p.rationale === "string" && p.rationale.trim() ? { rationale: p.rationale.trim().slice(0, 200) } : {}),
|
|
234
297
|
};
|
|
235
298
|
// F1 — an owner×term slice on a provider that cannot intersect them is minted as an UNSUPPORTED
|
|
236
299
|
// entry (→ the executor's deferred lane → a disclosed coverage row), exactly like a missing
|
|
237
300
|
// predicate at compile time. Never rejected (the gap belongs on the record) and never silently
|
|
238
301
|
// widened into an owner-less sweep.
|
|
239
|
-
|
|
240
|
-
|
|
302
|
+
// …and a goods narrowing on a register that cannot search goods text is the same kind of gap, with
|
|
303
|
+
// the compiler's own reason (goodsTextGap): recorded, never run on the class alone, which would ask the
|
|
304
|
+
// crowd the narrowing exists to cut.
|
|
305
|
+
const gap = ownerIntersectionGap(entry, capabilities) ?? goodsTextGap(entry, capabilities);
|
|
306
|
+
if (gap) { entry.unsupported = true; entry.unsupported_reason = gap; }
|
|
241
307
|
minted.push(entry);
|
|
242
308
|
}
|
|
243
309
|
return { minted, reused, rejected, enriched, narrowed };
|
|
244
310
|
}
|
|
245
311
|
|
|
312
|
+
/** The per-axis cap's count of what is already on file: the names of one split batch are one slot. PURE. */
|
|
313
|
+
export function supplementalSlots(entries) {
|
|
314
|
+
const batches = new Set();
|
|
315
|
+
let n = 0;
|
|
316
|
+
for (const e of (entries ?? [])) {
|
|
317
|
+
if (e?.split_of) { if (!batches.has(e.split_of)) { batches.add(e.split_of); n += 1; } }
|
|
318
|
+
else n += 1;
|
|
319
|
+
}
|
|
320
|
+
return n;
|
|
321
|
+
}
|
|
322
|
+
|
|
246
323
|
/**
|
|
247
324
|
* Append mint-rejection rows to a supplemental-plan doc's `rejected[]` (append-only, beside
|
|
248
325
|
* entries[] — one sidecar per axis holds BOTH what folded into the plan and what died at the seam,
|
|
@@ -326,7 +403,7 @@ export async function proposeSupplemental(params, tctx, deps) {
|
|
|
326
403
|
return { type: "text", text: JSON.stringify({ minted: [], reused: [], rejected: [], excluded_house_element: excludedHouse, executed: false }, null, 2) };
|
|
327
404
|
const existingQids = new Set(supp.entries.map((e) => e.qid));
|
|
328
405
|
const { minted, reused, rejected, enriched, narrowed } = mintSupplementalEntries(axis, offered,
|
|
329
|
-
{ existingQids, perCall, axisMax, existingCount: supp.entries
|
|
406
|
+
{ existingQids, perCall, axisMax, existingCount: supplementalSlots(supp.entries), capabilities: deps.capabilities ?? null, priorityClasses: planClasses });
|
|
330
407
|
|
|
331
408
|
// Field-level romanisation enrichment of a REUSED qid (2026-07-30 review round): the natural retry —
|
|
332
409
|
// a bare non-Latin proposal deferred at the wire, the model re-proposes it WITH the romanisation —
|