clearotron 0.3.2 → 0.3.3-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +12 -0
- package/INSTALL.md +8 -0
- package/bin/onboard.mjs +109 -13
- 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 +29 -11
- package/driver/CHANGELOG.md +49 -0
- package/driver/citation-census.json +3 -3
- package/driver/clearance-variants-record.mjs +12 -1
- package/driver/common-law-coverage-status.mjs +113 -0
- package/driver/config-inventory.mjs +1 -1
- 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/dev-portal.mjs +3 -3
- package/driver/drainer-identity.mjs +1 -1
- package/driver/driver.config.mjs +80 -9
- package/driver/engine/CONTRACT.md +3 -2
- package/driver/engine/anthropic-agent.mjs +34 -7
- 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 +37 -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-knockout.mjs +3 -3
- package/driver/pipeline.mjs +154 -50
- package/driver/plan-run-agreement-verdict.mjs +49 -0
- package/driver/portal-service.mjs +8 -4
- package/driver/progress.mjs +14 -3
- package/driver/publish/index.mjs +41 -26
- package/driver/publish/report-data.mjs +4 -3
- package/driver/publish/xlsx.mjs +26 -4
- package/driver/queue-markers.mjs +44 -0
- package/driver/queue-watch-verdict.mjs +2 -2
- package/driver/reference-score.mjs +10 -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/settle-stamp.mjs +10 -3
- 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 +2 -2
- package/driver/suite-census.json +293 -29
- 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 +8 -0
- package/mcp-server/lib/brief.mjs +16 -12
- 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 +2 -2
- 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 +8 -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/e2e.mjs +1 -1
- 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 +32 -33
- 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/repo-writes.mjs +1 -1
- package/scripts/report-sections-render-check.mjs +7 -3
- package/scripts/score.mjs +7 -1
- package/scripts/settings-render-check.mjs +36 -0
- package/scripts/travelling-predicates.mjs +1 -1
- package/shared/identifier-scan.mjs +22 -5
- package/shared/scroll-settle.mjs +67 -0
|
@@ -102,10 +102,10 @@ export function unionCoverageForm(prior, submitted, input, { parkedIds = null }
|
|
|
102
102
|
let settled = 0, carried = 0, parked = 0;
|
|
103
103
|
for (const row of form.rows) {
|
|
104
104
|
const p = findPrior(row), s = findSubmitted(row);
|
|
105
|
-
const sOk = s && rowIsSettled(s, row), pOk = p && rowIsSettled(p, row);
|
|
105
|
+
const sOk = s && rowIsSettled(s, row), pOk = p && rowIsSettled(p, row), dOk = rowIsSettled(row, row);
|
|
106
106
|
let fields;
|
|
107
107
|
if (sOk) fields = seatFields(s);
|
|
108
|
-
else if (pOk) fields = seatFields(p);
|
|
108
|
+
else if (pOk) fields = seatFields(p); else if (dOk) fields = seatFields(row); // a judgment the driver row arrived with (a family the reading turn withheld)
|
|
109
109
|
else {
|
|
110
110
|
const sf = seatFields(s ?? {}), pf = seatFields(p ?? {});
|
|
111
111
|
fields = { status: sf.status || pf.status, reason: sf.reason || pf.reason };
|
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;
|
package/driver/dev-portal.mjs
CHANGED
|
@@ -145,7 +145,7 @@ function scanRuns(workspaceRoot) {
|
|
|
145
145
|
const s = JSON.parse(readFileSync(join(dir, "status.json"), "utf8"));
|
|
146
146
|
out.push({ runId: s.runId ?? `${slug}-${runName}`, slug: s.slug ?? slug, codename: s.codename ?? null,
|
|
147
147
|
agent: s.agent ?? null, state: s.state ?? null, stepN: s.stepN ?? null, stepLabel: s.stepLabel ?? null,
|
|
148
|
-
stepTotal: s.stepTotal ?? null,
|
|
148
|
+
stepTotal: s.stepTotal ?? null, signoff: s.review?.signoff ?? s.verdict ?? null, tier: s.tier ?? null, sendPending: s.sendPending ?? null,
|
|
149
149
|
markName: s.markName ?? null, updatedAt: s.updatedAt ?? null, archived });
|
|
150
150
|
} catch { /* not a run dir / unreadable status — skip */ }
|
|
151
151
|
};
|
|
@@ -293,8 +293,8 @@ $("#f").addEventListener("submit",async(e)=>{e.preventDefault();const fd=new For
|
|
|
293
293
|
const r=await fetch("/dev/enqueue",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(b)});
|
|
294
294
|
$("#fout").textContent=JSON.stringify(await r.json(),null,2);loadRuns();});
|
|
295
295
|
async function loadRuns(){const r=await(await fetch("/dev/runs")).json();
|
|
296
|
-
$("#runs").innerHTML=r.length?"<table><tr><th>run</th><th>state</th><th>step</th><th>
|
|
297
|
-
'<tr><td>'+(x.markName??x.slug)+' · '+(x.codename??"?")+'</td><td class="'+(x.state==="delivered"?"ok":x.state==="failed"?"err":"warn")+'">'+(x.state??"?")+(x.sendPending?" (sendPending)":"")+'</td><td>'+(x.stepLabel??"")+'</td><td>'+(x.
|
|
296
|
+
$("#runs").innerHTML=r.length?"<table><tr><th>run</th><th>state</th><th>step</th><th>sign-off</th><th>updated</th></tr>"+r.map(x=>
|
|
297
|
+
'<tr><td>'+(x.markName??x.slug)+' · '+(x.codename??"?")+'</td><td class="'+(x.state==="delivered"?"ok":x.state==="failed"?"err":"warn")+'">'+(x.state??"?")+(x.sendPending?" (sendPending)":"")+'</td><td>'+(x.stepLabel??"")+'</td><td>'+(x.signoff??"")+'</td><td>'+(x.updatedAt??"").slice(0,19)+'</td></tr>').join("")+"</table>":"no runs yet";}
|
|
298
298
|
async function loadOutbox(){const r=await(await fetch("/dev/outbox")).json();
|
|
299
299
|
$("#outbox").innerHTML=r.length?r.map(x=>'<details><summary>'+x.file+' <span class="warn">'+(x.packet?.kind??(x.legacyAgent?"delivered (legacy)":"?"))+'</span></summary><pre>'+JSON.stringify(x.packet??{legacyAgent:x.legacyAgent},null,2)+'</pre></details>').join(""):"outbox empty";}
|
|
300
300
|
// ── Searches panel (Phase 3a): registry levels + saved recipes via the /recipes/* proxy. EVERY
|
|
@@ -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
|
|
package/driver/driver.config.mjs
CHANGED
|
@@ -656,10 +656,29 @@ export const config = {
|
|
|
656
656
|
// stage definition and the id stamped on a token-rollup row are the same fact rather than two spellings
|
|
657
657
|
// of it. resolveModel below is the only reader that matters; an engine with its own resolveModelId
|
|
658
658
|
// overrides it, and anything already in catalog form passes through untouched.
|
|
659
|
+
// ── A TIER IS WHAT WAS ASKED FOR, SO A TIER IS WHAT IS RECORDED ─────────────────────────────────────
|
|
660
|
+
//
|
|
661
|
+
// These three named a VERSION — `anthropic/claude-opus-5` — and nothing ever asked for one. A stage
|
|
662
|
+
// names a tier, the tier goes to the program as the vendor's own alias, and the vendor answers with its
|
|
663
|
+
// newest model of that tier. The version written here was a claim about a request nobody made, and it
|
|
664
|
+
// was wrong the day a newer model shipped: a delivered run served throughout by the generation after
|
|
665
|
+
// Opus 5 recorded, on every attempt row, a request for Opus 5, and its token line accounted under the
|
|
666
|
+
// name of a model that did not run. Measured on that run, 2026-09-22.
|
|
667
|
+
//
|
|
668
|
+
// Owner's ruling, 2026-09-23: record the tier. What a run asked for is a tier, what it was served is
|
|
669
|
+
// recorded separately and already is, and the report names the model that ran — none of that moves.
|
|
670
|
+
//
|
|
671
|
+
// WHAT IT COSTS, RULED ON AND ACCEPTED RATHER THAN DISCOVERED LATER. Per-model totals are keyed on what
|
|
672
|
+
// was ASKED for, and the direct-API lanes must name a version because they call the API rather than the
|
|
673
|
+
// program — the API takes model ids, not tier words. So one model reached by both routes now lands in
|
|
674
|
+
// two buckets: `anthropic/claude-haiku` from a stage, `anthropic/claude-haiku-4-5` from those lanes.
|
|
675
|
+
// That is a real split in a per-model total and it was accepted with the ruling: the two are genuinely
|
|
676
|
+
// different requests, and keying the totals on what actually SERVED each turn is the change that would
|
|
677
|
+
// fix it properly, which is larger than this and not what was ruled.
|
|
659
678
|
export const MODELS = {
|
|
660
|
-
haiku: "anthropic/claude-haiku
|
|
661
|
-
sonnet: "anthropic/claude-sonnet
|
|
662
|
-
opus: "anthropic/claude-opus
|
|
679
|
+
haiku: "anthropic/claude-haiku",
|
|
680
|
+
sonnet: "anthropic/claude-sonnet",
|
|
681
|
+
opus: "anthropic/claude-opus",
|
|
663
682
|
gemini: "google/gemini-3.1-pro-preview",
|
|
664
683
|
"gemini-flash": "google/gemini-3-flash-preview",
|
|
665
684
|
"deepseek-v4-pro": "together/deepseek-ai/DeepSeek-V4-Pro",
|
|
@@ -675,11 +694,17 @@ export const MODELS = {
|
|
|
675
694
|
//
|
|
676
695
|
// A BARE Anthropic id (dated or not — "claude-haiku-4-5-20251001", "claude-opus-5") normalises to the
|
|
677
696
|
// catalog form too. The direct-API lanes (jx completions/judge/nativeread, driver.config JX_PROVIDERS)
|
|
678
|
-
// name their model that way because that is what the Messages API takes, so without this
|
|
679
|
-
//
|
|
680
|
-
//
|
|
681
|
-
//
|
|
682
|
-
//
|
|
697
|
+
// name their model that way because that is what the Messages API takes, so without this one model named
|
|
698
|
+
// in two spellings — dated and undated — would key apart in a rollup. The date suffix is dropped;
|
|
699
|
+
// anything that does not look like a bare claude id is returned untouched, so a genuinely unknown model
|
|
700
|
+
// still keys as-is rather than being guessed at.
|
|
701
|
+
//
|
|
702
|
+
// WHAT THIS NO LONGER DOES, SAID PLAINLY BECAUSE THE PARAGRAPH ABOVE USED TO CLAIM IT. It used to unite
|
|
703
|
+
// a stage's rows with those lanes' rows, because the tier resolved to a versioned id and so did they.
|
|
704
|
+
// The tiers now resolve to a tier (MODELS), and these lanes still name a version, so the same model
|
|
705
|
+
// reached both ways keys in two places. That split was ruled on and accepted (see MODELS) — it is not
|
|
706
|
+
// an oversight here, and closing it by collapsing a version to its tier would throw away the one thing
|
|
707
|
+
// these rows can still say about which model was asked for.
|
|
683
708
|
export function resolveModel(model) {
|
|
684
709
|
if (!model) return model;
|
|
685
710
|
if (MODELS[model]) return MODELS[model];
|
|
@@ -1976,7 +2001,12 @@ export const ENGINE_BINARIES = {
|
|
|
1976
2001
|
// newer", with no ceiling. Setup installs it into the engines folder (enginesFolder, below the table)
|
|
1977
2002
|
// when the reader picks this engine, and the resolver uses it only when the machine has no copy of its
|
|
1978
2003
|
// own. The package's own `bin` field names the program, so no path inside it is written down here.
|
|
1979
|
-
|
|
2004
|
+
// 2.1.280 is the floor because it is the oldest release that can run the current generation of this
|
|
2005
|
+
// vendor's top tier: below it the API refuses the model id outright ("version 2.1.280 or newer is
|
|
2006
|
+
// required"), and the tier alias quietly goes on serving the previous generation. Measured 2026-09-22
|
|
2007
|
+
// on 2.1.263 — the alias returned the older model and the pinned id was refused — so a floor that
|
|
2008
|
+
// only asks for a program that starts is a floor that passes a machine this engine cannot run on.
|
|
2009
|
+
package: "@anthropic-ai/claude-code", floor: "2.1.280",
|
|
1980
2010
|
// WHAT THE INSTALL TAKES ON DISK, in MB, which setup states before it asks to install. MEASURED, not
|
|
1981
2011
|
// declared by the vendor: the engines folder after a fresh install of this package into an empty
|
|
1982
2012
|
// folder, on npm 10.9.8 and on 11.19.1, 2026-09-14. A later release can be larger or smaller, so setup
|
|
@@ -2047,6 +2077,47 @@ export function enginesFolder({ env = process.env, home = homedir() } = {}) {
|
|
|
2047
2077
|
return String(env[ENGINES_DIR_ENV] ?? "").trim() || join(home, ".local", "share", "clearotron", "engines");
|
|
2048
2078
|
}
|
|
2049
2079
|
|
|
2080
|
+
/**
|
|
2081
|
+
* Is the copy of an engine's program on this machine older than the version this build asks for?
|
|
2082
|
+
*
|
|
2083
|
+
* THE FLOOR GOVERNED ONE ROUTE OF TWO. Setup passes it to npm, so a program it installs cannot land
|
|
2084
|
+
* under it; a copy already on the machine wins over the installed one by design, and nothing compared
|
|
2085
|
+
* its version to anything. The two routes are not equally common — most machines have their own copy —
|
|
2086
|
+
* so the check that existed covered the case that mostly does not arise.
|
|
2087
|
+
*
|
|
2088
|
+
* What that costs is not a crash. The program carries its own list of the models it accepts, so one
|
|
2089
|
+
* below the floor refuses the model a tier names and serves the previous generation instead: the run
|
|
2090
|
+
* completes, the report is delivered, and the only sign is a model id in the record that nobody chose.
|
|
2091
|
+
* Measured 2026-09-22 on 2.1.263, where the current top tier's id came back a 400 and the tier alias
|
|
2092
|
+
* answered with the generation before it.
|
|
2093
|
+
*
|
|
2094
|
+
* THREE-VALUED, AND THE THIRD VALUE IS THE POINT. `null` means "these two cannot be compared" — no
|
|
2095
|
+
* floor declared, nothing read from the copy, or a version this cannot parse — and it is never "fine".
|
|
2096
|
+
* A caller must say it could not look rather than print a pass, which is the absence-read-as-a-pass
|
|
2097
|
+
* class that the rest of this file keeps naming. Only a version that parses and sorts below the floor
|
|
2098
|
+
* comes back `true`.
|
|
2099
|
+
*
|
|
2100
|
+
* PURE, both arguments injected, so a test drives an old version and a current one without a program.
|
|
2101
|
+
*
|
|
2102
|
+
* @param {string|null|undefined} version what the copy reports, e.g. "2.1.273"
|
|
2103
|
+
* @param {string|null|undefined} floor the engine's declared floor, e.g. "2.1.280"
|
|
2104
|
+
* @returns {boolean|null} true = older than the floor; false = at it or newer; null = not comparable
|
|
2105
|
+
*/
|
|
2106
|
+
export function olderThanFloor(version, floor) {
|
|
2107
|
+
const parts = (v) => {
|
|
2108
|
+
const m = /^\s*v?(\d+)\.(\d+)(?:\.(\d+))?/.exec(String(v ?? ""));
|
|
2109
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3] ?? 0)] : null;
|
|
2110
|
+
};
|
|
2111
|
+
const [got, want] = [parts(version), parts(floor)];
|
|
2112
|
+
if (!got || !want) return null;
|
|
2113
|
+
// Part by part, never as text: "2.1.99" sorts above "2.1.280" as a string, and that comparison would
|
|
2114
|
+
// read a machine two hundred releases behind as being ahead of the floor.
|
|
2115
|
+
for (let i = 0; i < 3; i++) {
|
|
2116
|
+
if (got[i] !== want[i]) return got[i] < want[i];
|
|
2117
|
+
}
|
|
2118
|
+
return false;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2050
2121
|
/** The npm arguments that install, or refresh, an engine's program in `dir`: "this version or newer". */
|
|
2051
2122
|
export function engineInstallArgs(spec, dir = enginesFolder()) {
|
|
2052
2123
|
return ["install", "--prefix", dir, "--no-fund", "--no-audit", `${spec.package}@>=${spec.floor}`];
|
|
@@ -153,8 +153,9 @@ and could not be: the telemetry logged the alias that was ASKED FOR, so an arm r
|
|
|
153
153
|
gemini and ran sonnet. Both tiers are gone — the failover chain was deleted in and both stages
|
|
154
154
|
declare an anthropic tier in `STAGES` — and every engine's model map now **refuses** an alias it cannot
|
|
155
155
|
run (`claudeModel`, `openaiModel`). On the anthropic engine a tier goes as the vendor's alias, a catalog
|
|
156
|
-
id
|
|
157
|
-
|
|
156
|
+
id naming a family and a version (`anthropic/claude-opus-5-5`, `claude-haiku-4-5-20251001`) goes as
|
|
157
|
+
that model so a pin holds, a `claude-*` id naming a family with no version goes as its family's alias
|
|
158
|
+
because the CLI has no model by that name, and anything else throws. To hold a tier on one model, set the vendor's own
|
|
158
159
|
`ANTHROPIC_DEFAULT_OPUS_MODEL` / `_SONNET_MODEL` / `_HAIKU_MODEL`, or `ANTHROPIC_DEFAULT_FABLE_MODEL` for
|
|
159
160
|
`fable`, which no stage asks for unless an override names it, as `CLEAROTRON_SYNTHESIS_MODEL=fable` does; each
|
|
160
161
|
reaches the CLI through the stage's environment.
|
|
@@ -156,11 +156,17 @@ const engineMaxBufferChars = () => Math.max(1024, Number(process.env.CLEAROTRON_
|
|
|
156
156
|
// An alias with no claude equivalent now FAILS LOUD, exactly as `openaiModel` has always done for a
|
|
157
157
|
// non-GPT id. That is the issue's requirement in one line: an unhonoured model override is an error,
|
|
158
158
|
// not a substitution.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
159
|
+
// THE CATALOG IDS ARE NOT LISTED HERE ANY MORE, and removing them is what makes one rule cover every
|
|
160
|
+
// id. Four sat here and two of them disagreed with the other two: `anthropic/claude-opus-5` and
|
|
161
|
+
// `anthropic/claude-sonnet-5` went over as those models, while `anthropic/claude-sonnet-4-6` and
|
|
162
|
+
// `anthropic/claude-haiku-4-5` went over as their tier's alias. A caller naming an exact model got it
|
|
163
|
+
// or lost it depending on which of the four they happened to name, and nothing said which.
|
|
164
|
+
//
|
|
165
|
+
// The rule below now answers all four the same way, and no run changes: a stage names its TIER, and the
|
|
166
|
+
// tier words above are still the whole of what a run passes. These ids reach this function only when a
|
|
167
|
+
// caller names one — an override or an experiment arm — and there, being given the model you named is
|
|
168
|
+
// the behaviour the rest of this function already promises.
|
|
169
|
+
const CLAUDE_MODEL = { opus: "opus", sonnet: "sonnet", haiku: "haiku", fable: "fable" };
|
|
164
170
|
export function claudeModel(model) {
|
|
165
171
|
if (!model) return undefined;
|
|
166
172
|
if (CLAUDE_MODEL[model]) return CLAUDE_MODEL[model];
|
|
@@ -168,8 +174,29 @@ export function claudeModel(model) {
|
|
|
168
174
|
// that is a NAMING form of a model claude can actually run, not a substitution of a different one.
|
|
169
175
|
// The family must be named IN the id: a `claude-*` id whose family this build does not recognise
|
|
170
176
|
// throws too, rather than riding the old else-arm into sonnet.
|
|
171
|
-
|
|
172
|
-
|
|
177
|
+
// FABLE IS READ HERE TOO, and its absence was a live defect rather than a gap in readiness: the bare
|
|
178
|
+
// `fable` alias is in the table above, so `CLEAROTRON_SYNTHESIS_MODEL=fable` worked and hid it, while
|
|
179
|
+
// the pinned id every vendor page names — `claude-fable-5-1` — threw on its way to a program that runs
|
|
180
|
+
// it. Measured 2026-09-22: the program accepts that id and reports serving `claude-fable-5-1`.
|
|
181
|
+
const fam = /opus/i.test(model) ? "opus" : /haiku/i.test(model) ? "haiku" : /sonnet/i.test(model) ? "sonnet"
|
|
182
|
+
: /fable/i.test(model) ? "fable" : null;
|
|
183
|
+
if (fam && /^(?:anthropic\/)?claude-/i.test(model)) {
|
|
184
|
+
const bare = String(model).replace(/^anthropic\//i, "").toLowerCase();
|
|
185
|
+
// A CONCRETE ID GOES TO THE PROGRAM AS ITSELF, AND THAT IS WHAT MAKES A PIN A PIN. It used to come
|
|
186
|
+
// back as the bare family alias, so a caller who named an exact model got whichever model the tier
|
|
187
|
+
// pointed at — the same model on the day it was written, a different one the day a newer one
|
|
188
|
+
// shipped, and nothing to read in between. A silent un-pinning is the substitution this function
|
|
189
|
+
// exists to refuse, in the one form it still allowed.
|
|
190
|
+
//
|
|
191
|
+
// A FAMILY WITH NO VERSION IS THE TIER, not a model: `claude-opus` is what an operator types for a
|
|
192
|
+
// deployment of that tier, and the program has no model by that name. It keeps following the family.
|
|
193
|
+
//
|
|
194
|
+
// Measured against the program rather than assumed (2026-09-22): it accepts `claude-sonnet-5`,
|
|
195
|
+
// `claude-haiku-4-5-20251001` and `claude-fable-5-1` and reports serving each of them, so passing an
|
|
196
|
+
// exact id through costs nothing that the alias was buying. Where a caller names an id the program
|
|
197
|
+
// does not know, it says so and the turn fails loudly — which is the honest end of a bad pin.
|
|
198
|
+
return /^claude-(?:[a-z]+-\d|\d)/.test(bare) ? bare : fam;
|
|
199
|
+
}
|
|
173
200
|
throw new Error(`anthropic-agent: no claude model mapped for "${model}" — this engine runs claude only. Pass opus/sonnet/haiku/fable or a concrete claude-* id. (It used to substitute sonnet silently and log the alias you asked for: #238 corruption 3.)`);
|
|
174
201
|
}
|
|
175
202
|
|
|
@@ -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,37 @@
|
|
|
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. It touches
|
|
13
|
+
// no file, no network and no run.
|
|
14
|
+
//
|
|
15
|
+
// IT IS DECLARED AS THE REGISTER SEARCH IS DECLARED, NOT AS WHAT IT DOES. codex decides from a tool's
|
|
16
|
+
// annotations whether a call needs approval, and `codex exec` refuses every call that does ("MCP tool call
|
|
17
|
+
// requires approval, but approval policy is never") unless its sandbox is bypassed. A read-only tool never
|
|
18
|
+
// needs approval, so a read-only `ping` passed on hosts where `register_execute_plan` — marked not
|
|
19
|
+
// read-only and open-world on every register server — was refused on every call and no search could run.
|
|
20
|
+
// The probe exists to answer for the tools a search calls, so its tool carries that tool's annotations,
|
|
21
|
+
// and a test keeps the two equal. The rule, in codex's own source, identical from 0.150.1 to 0.156.1:
|
|
22
|
+
// read-only → no approval; otherwise approval when destructive, or open-world, or either left unmarked.
|
|
23
|
+
import { serve } from "./stdio-server.mjs";
|
|
24
|
+
|
|
25
|
+
serve({
|
|
26
|
+
name: "probe", version: "0.1.0",
|
|
27
|
+
tools: [{
|
|
28
|
+
name: "ping",
|
|
29
|
+
description: "Return the word this check is waiting for.",
|
|
30
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
31
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
32
|
+
handler: async () => {
|
|
33
|
+
const word = String(process.argv[2] ?? "").trim();
|
|
34
|
+
return word ? word : { isError: true, text: "ping: this server was started without a word to return" };
|
|
35
|
+
},
|
|
36
|
+
}],
|
|
37
|
+
});
|
|
@@ -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
|
+
}
|