clearotron 0.3.3 → 0.4.0-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/.env.example +9 -0
- package/INSTALL.md +1 -14
- package/bin/brandowner.mjs +5 -5
- package/bin/connect.mjs +4 -4
- package/bin/onboard.mjs +5 -7
- package/bin/start.mjs +20 -5
- package/bin/update.mjs +6 -1
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +2 -6
- package/driver/CHANGELOG.md +28 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/band-shape.mjs +8 -8
- package/driver/blind-frame-model.mjs +1 -1
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/commonlaw-carry.mjs +2 -2
- package/driver/company-bundle.mjs +11 -18
- package/driver/connotation-search.mjs +4 -4
- package/driver/contract-e3-backlog.mjs +3 -3
- package/driver/declination-call.mjs +1 -1
- package/driver/declination-tool.mjs +1 -1
- package/driver/dev-portal.mjs +1 -1
- package/driver/door-call-verdict.mjs +27 -0
- package/driver/driver.config.mjs +17 -11
- package/driver/e2e/README.md +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +2 -1
- package/driver/engine/mcp/corsearch-server.mjs +1 -0
- package/driver/engine/mcp/euipo-server.mjs +1 -0
- package/driver/engine/mcp/free-tier-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/perplexity-server.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/signa-server.mjs +1 -0
- package/driver/engine/mcp/supplemental.mjs +1 -1
- package/driver/engine/mcp/uspto-local-server.mjs +1 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/feedback-issues.mjs +1 -1
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +3 -3
- package/driver/flag-snapshot.mjs +1 -1
- package/driver/floor-duty.mjs +2 -2
- package/driver/form-neighbourhood.mjs +47 -15
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/gateway.mjs +5 -5
- package/driver/jx-lanes.mjs +1 -1
- package/driver/known-conflicts.mjs +18 -0
- package/driver/log.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +129 -95
- package/driver/pipeline.mjs +69 -32
- package/driver/placement-carry.mjs +2 -2
- package/driver/placement-form.mjs +1 -1
- package/driver/portal-mcp-client.mjs +1 -1
- package/driver/portal-request-origin.mjs +79 -0
- package/driver/portal-service.mjs +45 -17
- package/driver/predelivery-lint.mjs +10 -10
- package/driver/profile-page.html +9 -13
- package/driver/profile-service.mjs +25 -13
- package/driver/profiles.mjs +17 -4
- package/driver/progress.mjs +1 -1
- package/driver/provider-usage.mjs +24 -1
- package/driver/publish/index.mjs +17 -5
- package/driver/publish/knockout.mjs +3 -2
- package/driver/publish/render-knockout.mjs +1 -1
- package/driver/publish/render.mjs +14 -3
- package/driver/publish/search-depth.mjs +4 -2
- package/driver/recall-reconciliation.mjs +1 -1
- package/driver/record-carry.mjs +6 -6
- package/driver/recording-agreement.mjs +2 -2
- package/driver/reference-score.mjs +27 -27
- package/driver/register-count.mjs +56 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/register-plan.mjs +5 -5
- package/driver/register-records.mjs +10 -1
- package/driver/registry-fidelity.mjs +4 -4
- package/driver/repair-composers.mjs +6 -6
- package/driver/run-economics.mjs +8 -19
- package/driver/screen-gate.mjs +1 -1
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/perplexity-prompts.md +4 -4
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/report-prose.md +5 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-variants/SKILL.md +2 -2
- package/driver/skills/clearance-variants/transliteration-scripts.md +1 -1
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/skills/knockout-assess/SKILL.md +9 -9
- package/driver/skills/matter-frame/watchlist-reference.md +1 -1
- package/driver/skills/narrative-refutation/SKILL.md +2 -2
- package/driver/skills/placement-inquiry/SKILL.md +1 -1
- package/driver/stage-context.mjs +4 -4
- package/driver/stages.mjs +23 -15
- package/driver/suite-census.json +138 -42
- package/driver/systemd/clearotron-client-mcp.service +24 -0
- package/driver/systemd/clearotron-mcp-face.service +24 -0
- package/driver/systemd/clearotron-portal.service +24 -0
- package/driver/systemd/clearotron-worker.service +24 -0
- package/driver/tokens.mjs +26 -17
- package/driver/turnaround-bands.mjs +1 -1
- package/driver/unit-inventory.mjs +3 -3
- package/driver/variant-manifest-model.mjs +1 -1
- package/driver/verify.mjs +2 -2
- package/driver/whatif-memo-run.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/audit.mjs +9 -2
- package/mcp-server/lib/http-handler.mjs +7 -3
- package/mcp-server/mint-token.mjs +8 -6
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +11 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-GBbbyQxc.js → index-D_O_55vK.js} +59 -9
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -1
- package/providers/_shared/answer-memory.mjs +199 -0
- package/providers/_shared/ledger-path.mjs +1 -1
- package/providers/_shared/ledger.mjs +47 -5
- package/providers/_shared/script-form.mjs +24 -5
- package/providers/_shared/term-shape.mjs +5 -5
- package/providers/clarivate/src/capabilities.js +11 -0
- package/providers/clarivate/src/core.js +140 -13
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +24 -0
- package/providers/signa/src/core.js +66 -0
- package/scripts/deprecate-below.mjs +114 -2
- package/scripts/freeze-example-run.mjs +1 -1
- package/scripts/live-surface-check.mjs +11 -2
- package/scripts/release-entry-catch-up.mjs +211 -0
- package/scripts/release-note-required.mjs +102 -6
- package/scripts/release-rehearsal-version.mjs +60 -0
- package/scripts/release-sbom.mjs +104 -0
- package/scripts/release-visible-check.mjs +7 -5
- package/scripts/score.mjs +3 -3
- package/shared/brand.mjs +1 -1
- package/shared/client-door.mjs +15 -8
- package/shared/driver-dir.mjs +20 -9
- package/shared/names-in-force.mjs +2 -0
- package/shared/scope.mjs +25 -10
- package/shared/store-in-repo.mjs +38 -17
|
@@ -31,7 +31,8 @@
|
|
|
31
31
|
// `register-records.jsonl`; providers/_shared/ledger-path.mjs still resolves the older
|
|
32
32
|
// `corsearch-*` names on a box that already has them.)
|
|
33
33
|
|
|
34
|
-
import { makeLedger } from "../../_shared/ledger.mjs";
|
|
34
|
+
import { makeLedger, heldRecordBodies } from "../../_shared/ledger.mjs";
|
|
35
|
+
import { answerKey, compareAnswers, forgetAnswer, noteAnswer, openAnswerMemory, recallAnswer, rememberAnswer } from "../../_shared/answer-memory.mjs";
|
|
35
36
|
import { nonAnswerBodyError, parseJsonBody, unparsedBodyError } from "../../_shared/http-body.mjs";
|
|
36
37
|
import {
|
|
37
38
|
BATCH_SCREEN_CHUNK, chunk, isAllClass, makeClassifyStatus, screenVerdict,
|
|
@@ -76,13 +77,36 @@ export function refToOffice(ref) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// ── HTTP helper (with the metered chokepoint) ──────────────────────────────────────────────────────
|
|
79
|
-
|
|
80
|
+
//
|
|
81
|
+
// THE RUN MEMORY (providers/_shared/answer-memory.mjs) answers an identical count, search or owner lookup
|
|
82
|
+
// from what this run already asked: the question is the base address, the path and the whole body, never
|
|
83
|
+
// the key. Records are held by id instead (heldRecords, below), because a record is the same whichever
|
|
84
|
+
// search found it. Images and filing dates are not repeated in a run and go to the register every time.
|
|
85
|
+
// With no memory for the run, this is the plain fetch it always was.
|
|
86
|
+
//
|
|
87
|
+
// `ledgerExtra` rides on the call ledger's row: a /text call names the record ids it fetched, which is
|
|
88
|
+
// what lets a run's usage count a record fetched twice (driver/provider-usage.mjs).
|
|
89
|
+
const REMEMBERED_PATHS = new Set(["/count", "/search", "/resolution/company"]);
|
|
90
|
+
|
|
91
|
+
export async function clarivateFetch(apiKey, base, path, { body = null, method = "POST", retries = 1, tctx = null, ledgerExtra = null } = {}) {
|
|
80
92
|
const url = `${base}${path}`;
|
|
81
93
|
const headers = { "X-ApiKey": apiKey, "Accept": "application/json" };
|
|
82
94
|
const init = { method, headers };
|
|
83
95
|
if (body !== null) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); }
|
|
84
96
|
|
|
85
97
|
const t0 = Date.now();
|
|
98
|
+
const mem = REMEMBERED_PATHS.has(path) ? openAnswerMemory(tctx?.recordLog) : null;
|
|
99
|
+
const key = mem ? answerKey({ base, method, path, body }) : null;
|
|
100
|
+
const held = mem ? recallAnswer(mem, key) : null;
|
|
101
|
+
const note = (row) => noteAnswer(mem, { ts: new Date().toISOString(), mode: mem.mode, key, method, path, tool: tctx?.kind ?? null,
|
|
102
|
+
via: tctx?.recordLog ? "driver" : "tool-server", session: tctx?.sessionKey ?? null, ...row });
|
|
103
|
+
if (held && mem.mode === "on") {
|
|
104
|
+
const { body: parsed, parseError } = parseJsonBody(held.raw);
|
|
105
|
+
const ok = held.status >= 200 && held.status < 300;
|
|
106
|
+
logCall(tctx, { http_status: held.status, ok, attempts: 0, took_ms: Date.now() - t0, bytes: held.raw.length, cache_hit: true });
|
|
107
|
+
note({ held: true, served: true, status: held.status });
|
|
108
|
+
return { status: held.status, ok, url, body: parsed, raw: held.raw, parseError };
|
|
109
|
+
}
|
|
86
110
|
let attempts = 0;
|
|
87
111
|
let resp;
|
|
88
112
|
try {
|
|
@@ -96,17 +120,86 @@ export async function clarivateFetch(apiKey, base, path, { body = null, method =
|
|
|
96
120
|
await new Promise((r) => setTimeout(r, 1500 * (i + 1)));
|
|
97
121
|
}
|
|
98
122
|
} catch (err) {
|
|
99
|
-
logCall(tctx, { http_status: 0, ok: false, attempts, took_ms: Date.now() - t0, bytes: 0, cache_hit: false });
|
|
123
|
+
logCall(tctx, { http_status: 0, ok: false, attempts, took_ms: Date.now() - t0, bytes: 0, cache_hit: false, ...(ledgerExtra ?? {}) });
|
|
124
|
+
if (mem) note({ held: Boolean(held), status: 0 });
|
|
100
125
|
throw err;
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
const raw = await resp.text();
|
|
104
129
|
// The parse failure travels on `parseError` instead of being swallowed — providers/_shared/http-body.mjs.
|
|
105
130
|
const { body: parsed, parseError } = parseJsonBody(raw);
|
|
106
|
-
logCall(tctx, { http_status: resp.status, ok: resp.ok, attempts, took_ms: Date.now() - t0, bytes: raw.length, cache_hit: false });
|
|
131
|
+
logCall(tctx, { http_status: resp.status, ok: resp.ok, attempts, took_ms: Date.now() - t0, bytes: raw.length, cache_hit: false, ...(ledgerExtra ?? {}) });
|
|
132
|
+
if (mem) {
|
|
133
|
+
const summary = rememberableAnswer(path, resp.status, parsed, parseError);
|
|
134
|
+
const row = { held: Boolean(held), status: resp.status, rememberable: Boolean(summary) };
|
|
135
|
+
if (held && summary) Object.assign(row, compareAnswers(held.summary, summary));
|
|
136
|
+
if (summary && !held) row.stored = rememberAnswer(mem, key, { status: resp.status, raw, summary });
|
|
137
|
+
note(row);
|
|
138
|
+
}
|
|
107
139
|
return { status: resp.status, ok: resp.ok, url, body: parsed, raw, parseError };
|
|
108
140
|
}
|
|
109
141
|
|
|
142
|
+
/**
|
|
143
|
+
* What the run memory may keep from a count, a search or an owner lookup, as the short summary a watch
|
|
144
|
+
* compares — or null when the question must be asked again next time.
|
|
145
|
+
*
|
|
146
|
+
* KEPT: a 200 that parsed into the answer its path gives — `counts{}` for /count, `ids{}` for /search
|
|
147
|
+
* (isSearchResponseBody), `companies[]` for /resolution/company.
|
|
148
|
+
*
|
|
149
|
+
* NEVER KEPT: anything else, the refusals included — a result past the ceiling, a query the service
|
|
150
|
+
* refused as written, an error envelope served with a 200, a body that did not parse. Those are the
|
|
151
|
+
* answers this core reports as failures, and remembering one would turn a passing fault into the run's
|
|
152
|
+
* permanent answer. No answer here points at a next page: /search returns the whole set at once.
|
|
153
|
+
*/
|
|
154
|
+
export function rememberableAnswer(path, status, body, parseError) {
|
|
155
|
+
if (status !== 200 || parseError || body == null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
156
|
+
if (path === "/count") {
|
|
157
|
+
if (!body.counts || typeof body.counts !== "object" || Array.isArray(body.counts)) return null;
|
|
158
|
+
const entries = Object.entries(body.counts).filter(([, n]) => Number.isFinite(Number(n)));
|
|
159
|
+
return { total: entries.reduce((t, [, n]) => t + Number(n), 0), ids: entries.map(([o, n]) => `${o}:${Number(n)}`).sort(), next_page: false };
|
|
160
|
+
}
|
|
161
|
+
if (path === "/search") {
|
|
162
|
+
if (!isSearchResponseBody(body)) return null;
|
|
163
|
+
const ids = Object.entries(body.ids).flatMap(([o, gs]) => (Array.isArray(gs) ? gs : []).map((g) => `${o}/${g}`));
|
|
164
|
+
return { total: ids.length, ids, next_page: false };
|
|
165
|
+
}
|
|
166
|
+
if (path === "/resolution/company") {
|
|
167
|
+
if (!Array.isArray(body.companies)) return null;
|
|
168
|
+
const ids = body.companies.map((c) => `${c?.applicantName ?? ""}|${c?.registrationOfficeCode ?? ""}|${c?.confidenceScore ?? ""}`);
|
|
169
|
+
return { total: ids.length, ids, next_page: false };
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* THE RECORDS THIS RUN ALREADY HOLDS, by id, for a /text call about to be made — Map<lowercased guid, raw
|
|
176
|
+
* record>. Read off the run's record log, which every process that fetches a record for the run writes
|
|
177
|
+
* to (providers/_shared/ledger.mjs heldRecordBodies), and only while the run memory is `on`.
|
|
178
|
+
*
|
|
179
|
+
* The raw vendor record is what comes back, not the normalized one stored beside it, so the caller runs
|
|
180
|
+
* it through exactly the normalization and screening a fetched record gets, with this request's own office
|
|
181
|
+
* hint. A held record therefore yields the row a fresh fetch would, and a record whose office segment
|
|
182
|
+
* differs between two searches is addressed the way this search addresses it.
|
|
183
|
+
*
|
|
184
|
+
* Why no watch period (the owner's ruling): across 38 past runs that fetched the same record twice, every
|
|
185
|
+
* second copy was byte-identical apart from the order of one list inside the vendor's raw record, and
|
|
186
|
+
* every normalized field matched.
|
|
187
|
+
*/
|
|
188
|
+
function heldRecords(tctx, guids) {
|
|
189
|
+
const out = new Map();
|
|
190
|
+
if (openAnswerMemory(tctx?.recordLog)?.mode !== "on") return out;
|
|
191
|
+
const log = typeof tctx?.recordLog === "string" && tctx.recordLog.trim()
|
|
192
|
+
? tctx.recordLog.trim() : String(process.env.CLEAROTRON_REGISTER_RECORD_LOG ?? "").trim();
|
|
193
|
+
for (const [g, body] of heldRecordBodies(log, guids)) if (body?._raw && typeof body._raw === "object") out.set(g, body._raw);
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** The one ledger row for records served from the run's own store: no call made, none paid for. */
|
|
198
|
+
function logHeldRecords(tctx, held) {
|
|
199
|
+
if (!held.size) return;
|
|
200
|
+
logCall({ ...tctx, target: `held:${held.size}` }, { http_status: 200, ok: true, attempts: 0, took_ms: 0, bytes: 0, cache_hit: true, records: [...held.keys()] });
|
|
201
|
+
}
|
|
202
|
+
|
|
110
203
|
const errText = (r) => r?.body?.errorMessage ?? r?.body?.message ?? (r?.raw ? String(r.raw).slice(0, 200) : "");
|
|
111
204
|
|
|
112
205
|
// ══ QUERY BUILDING ═════════════════════════════════════════════════════════════════════════════════
|
|
@@ -192,7 +285,7 @@ const PHRASE_OPERATOR = "ADJ";
|
|
|
192
285
|
// once `exact`:
|
|
193
286
|
// "SLUSH FREEZE, SLUSH ICE, SLUSH POP" three terms crammed into one
|
|
194
287
|
// "TIKTOK / TIK- famous-neighbour family" a description of a family, not a name
|
|
195
|
-
// "
|
|
288
|
+
// "WAVU, WAPO one-keystroke neighbours of WAVO"
|
|
196
289
|
//
|
|
197
290
|
// Searched, they return 0 — and a 0 here reads as CLEAN, which is the one outcome this contract must
|
|
198
291
|
// never produce by accident. On that run the `default` twins deferred loudly while the `exact` twins
|
|
@@ -305,7 +398,7 @@ export function compilePhraseValue(term, { pre = "", post = "", dropReserved = f
|
|
|
305
398
|
const last = kept.length - 1;
|
|
306
399
|
const tailWrap = (kept.length > 1 && kept[last].length === 1) ? "" : post;
|
|
307
400
|
// THE WILDCARD PREDICATE CARRIES ITS OWN STAR, so `post` is empty there and the two rules above
|
|
308
|
-
// cannot reach it: the caller writes `
|
|
401
|
+
// cannot reach it: the caller writes `FOLD A*` and the star is part of the token. Same shape, same
|
|
309
402
|
// refusal, so the same subtraction — the star comes off a one-character final token. Nothing else is
|
|
310
403
|
// touched: a longer final token keeps the caller's pattern exactly as written, and a `?` is a
|
|
311
404
|
// single-character class rather than a sub-query, so it is not this.
|
|
@@ -1107,6 +1200,7 @@ export async function doSearch(apiKey, base, params, tctx) {
|
|
|
1107
1200
|
if (!isSearchResponseBody(r.body)) {
|
|
1108
1201
|
return { type: "text", text: nonAnswerBodyError("clarivate_search", r, "a search response (no ids{} — the one key POST /search answers with)", ` query=${String(echo).slice(0, 200)}`) };
|
|
1109
1202
|
}
|
|
1203
|
+
forgetContradictedAnswers(base, body, r.body, tctx);
|
|
1110
1204
|
const out = normalizeSearchResponse(r.body, echo, p.match_mode || "default");
|
|
1111
1205
|
// The resolution note rides on the answer, exactly as it does on the enumerate result: a reader has
|
|
1112
1206
|
// to be able to see WHICH applicant styling this sweep actually asked for, and a zero over an owner
|
|
@@ -1115,6 +1209,22 @@ export async function doSearch(apiKey, base, params, tctx) {
|
|
|
1115
1209
|
return { type: "text", text: JSON.stringify(out, null, 2) };
|
|
1116
1210
|
}
|
|
1117
1211
|
|
|
1212
|
+
// A SEARCH SHORTER THAN ITS OWN COUNT IS NOT KEPT, AND NEITHER IS THE COUNT. The enumerate kernel reads the
|
|
1213
|
+
// shortfall as the register contradicting itself and hands the slice to the repair ladder to ask again
|
|
1214
|
+
// (the count/search reconciliation in providers/_shared/enumerate.mjs). Kept, both answers would be served
|
|
1215
|
+
// again on every retry, and the slice could not be cured within the attempt. The two share one body, so
|
|
1216
|
+
// the count held for this search is found under the same question.
|
|
1217
|
+
function forgetContradictedAnswers(base, body, searchBody, tctx) {
|
|
1218
|
+
const mem = openAnswerMemory(tctx?.recordLog);
|
|
1219
|
+
if (!mem) return;
|
|
1220
|
+
const countKey = answerKey({ base, method: "POST", path: "/count", body });
|
|
1221
|
+
const counted = recallAnswer(mem, countKey)?.summary?.total;
|
|
1222
|
+
const returned = rememberableAnswer("/search", 200, searchBody, null)?.total;
|
|
1223
|
+
if (!Number.isFinite(counted) || !Number.isFinite(returned) || returned >= counted) return;
|
|
1224
|
+
forgetAnswer(mem, countKey);
|
|
1225
|
+
forgetAnswer(mem, answerKey({ base, method: "POST", path: "/search", body }));
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1118
1228
|
// ── Count ─────────────────────────────────────────────────────────────────────────────────────────
|
|
1119
1229
|
// POST /count takes the SAME SearchRequest body as /search, is cheap, works at ANY magnitude and
|
|
1120
1230
|
// returns PER-OFFICE counts in one call. This is the enumerate
|
|
@@ -1269,7 +1379,7 @@ async function fetchText(apiKey, base, group, testMode, tctx) {
|
|
|
1269
1379
|
const body = { ids: group };
|
|
1270
1380
|
if (testMode) body.test = true;
|
|
1271
1381
|
const target = group.slice(0, 5).join(",") + (group.length > 5 ? `+${group.length - 5}` : "");
|
|
1272
|
-
const r = await clarivateFetch(apiKey, base, "/text", { body, tctx: { ...tctx, target } });
|
|
1382
|
+
const r = await clarivateFetch(apiKey, base, "/text", { body, tctx: { ...tctx, target }, ledgerExtra: { records: group.map((g) => String(g).toLowerCase()) } });
|
|
1273
1383
|
if (!r.ok) return { ok: false, raw: [], error: `HTTP ${r.status} for a ${group.length}-id chunk: ${errText(r)}` };
|
|
1274
1384
|
// On this provider /text is the ONLY source of mark text, classes, status and owner (the search
|
|
1275
1385
|
// returns bare guids), and the kernel's contentFromScreen seam reads a chunk error to decide whether
|
|
@@ -1305,21 +1415,31 @@ export async function doRecordFetch(apiKey, base, params, tctx) {
|
|
|
1305
1415
|
if (inputs.length === 0) return { type: "text", text: "ERROR: clarivate_record_fetch — record_ids is required (non-empty array of refs or guids)." };
|
|
1306
1416
|
|
|
1307
1417
|
const { officeByGuid, guids } = splitRefs(inputs);
|
|
1308
|
-
|
|
1418
|
+
// A record this run already holds is not fetched again (heldRecords); test-mode bodies are obfuscated
|
|
1419
|
+
// and are never held.
|
|
1420
|
+
const held = params.test_mode ? new Map() : heldRecords(tctx, guids);
|
|
1421
|
+
logHeldRecords(tctx, held);
|
|
1422
|
+
const groups = chunk(guids.filter((g) => !held.has(String(g).toLowerCase())), TEXT_BATCH_MAX);
|
|
1309
1423
|
const records = [];
|
|
1424
|
+
const fetched = [];
|
|
1310
1425
|
const errors = [];
|
|
1426
|
+
for (const rec of held.values()) { const nr = normalizeRecord(rec, rec?.id ? officeByGuid[rec.id] : null); records.push(nr); fetched.push(nr); }
|
|
1311
1427
|
for (const group of groups) {
|
|
1312
1428
|
const { ok, raw, error } = await fetchText(apiKey, base, group, params.test_mode, tctx);
|
|
1313
1429
|
if (!ok) { errors.push(error); continue; }
|
|
1314
|
-
for (const rec of raw)
|
|
1430
|
+
for (const rec of raw) { const nr = normalizeRecord(rec, rec?.id ? officeByGuid[rec.id] : null); records.push(nr); fetched.push(nr); }
|
|
1315
1431
|
}
|
|
1316
1432
|
if (!records.length && errors.length) {
|
|
1317
1433
|
return { type: "text", text: `ERROR: clarivate_record_fetch — ${errors.join("; ")}` };
|
|
1318
1434
|
}
|
|
1319
1435
|
// A1: persist each normalized record keyed by its synthetic ref so the driver can field-verify
|
|
1320
1436
|
// registry identifiers and archive the record into the run. (test_mode bodies are obfuscated — skip.)
|
|
1437
|
+
// A HELD RECORD IS WRITTEN TOO, under the address this request gives it. The screen gate matches exact
|
|
1438
|
+
// addresses against this log, and a record first fetched under another office's address was otherwise
|
|
1439
|
+
// absent under this one, so a drop citing it read as a record nobody examined. The log keeps one row per
|
|
1440
|
+
// address (writeRecordOnce), so a held record already logged under this address writes nothing.
|
|
1321
1441
|
if (!params.test_mode) {
|
|
1322
|
-
for (const nr of
|
|
1442
|
+
for (const nr of fetched) if (nr?.uri) logRecordBody({ ...tctx, kind: "record_fetch" }, nr.uri, nr);
|
|
1323
1443
|
}
|
|
1324
1444
|
return { type: "text", text: JSON.stringify({ count: records.length, records, errors: errors.length ? errors : undefined }, null, 2) };
|
|
1325
1445
|
}
|
|
@@ -1337,15 +1457,21 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
|
|
|
1337
1457
|
? params.in_scope_classes.map(Number).filter(Number.isFinite) : [];
|
|
1338
1458
|
|
|
1339
1459
|
const { officeByGuid, guids } = splitRefs(inputs);
|
|
1340
|
-
|
|
1460
|
+
// A record this run already holds is screened from its stored raw copy and not fetched again
|
|
1461
|
+
// (heldRecords); only the rest go to /text. Test-mode bodies are obfuscated and are never held.
|
|
1462
|
+
const held = params.test_mode ? new Map() : heldRecords(tctx, guids);
|
|
1463
|
+
logHeldRecords(tctx, held);
|
|
1464
|
+
const groups = chunk(guids.filter((g) => !held.has(String(g).toLowerCase())), TEXT_BATCH_MAX);
|
|
1341
1465
|
const rows = [];
|
|
1342
1466
|
const errors = [];
|
|
1343
1467
|
const normalized = [];
|
|
1344
|
-
|
|
1345
|
-
|
|
1468
|
+
const answers = held.size ? [{ ok: true, raw: [...held.values()], held: true }] : [];
|
|
1469
|
+
for (const group of groups) answers.push({ ...(await fetchText(apiKey, base, group, params.test_mode, tctx)), held: false });
|
|
1470
|
+
for (const { ok, raw, error } of answers) {
|
|
1346
1471
|
if (!ok) { errors.push(error); continue; }
|
|
1347
1472
|
for (const rec of raw) {
|
|
1348
1473
|
const nr = normalizeRecord(rec, rec?.id ? officeByGuid[rec.id] : null);
|
|
1474
|
+
// A held record is logged as well, under this request's address, for the reason doRecordFetch gives.
|
|
1349
1475
|
normalized.push(nr);
|
|
1350
1476
|
const row = {
|
|
1351
1477
|
uri: nr.uri,
|
|
@@ -1398,6 +1524,7 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
|
|
|
1398
1524
|
for (const row of rows) verdict_summary[row.screen_verdict] = (verdict_summary[row.screen_verdict] ?? 0) + 1;
|
|
1399
1525
|
return { type: "text", text: JSON.stringify({
|
|
1400
1526
|
requested: inputs.length, returned: rows.length, chunks: groups.length,
|
|
1527
|
+
...(held.size ? { held: held.size } : {}),
|
|
1401
1528
|
in_scope_classes: inScopeClasses.length ? inScopeClasses : "NOT PROVIDED — live marks fail-safe to surface:in-scope-live (no class-drop)",
|
|
1402
1529
|
errors: errors.length ? errors : undefined,
|
|
1403
1530
|
note: "Per-row `screen_verdict` (CLOSED SET) is the keep/drop authority — NOT the mark name or owner. drop:dead = a confidently dead status; drop:out-of-class = live but no in-scope-class overlap and not all_class — these two are batch-screen-authoritative drops. surface:in-scope-live and surface:all-class = a real in-scope candidate: decide it on the record's goods & services (this provider returns them on this very call). deepfetch:ambiguous = status unrecognised, never auto-drop.",
|
|
@@ -83,7 +83,7 @@ export function subclassesFor(db, { country, term, niceClass }) {
|
|
|
83
83
|
cn_goods_code: row.group_code, // what the 12th says, kept even when the 13th governs
|
|
84
84
|
})),
|
|
85
85
|
source: from13 === ruled.length
|
|
86
|
-
? "NCL 13-2026 concordance — the 13th edition governs the group code
|
|
86
|
+
? "NCL 13-2026 concordance — the 13th edition governs the group code; CNIPA's 区分表 found the good"
|
|
87
87
|
: from13 === 0
|
|
88
88
|
? "CNIPA 区分表, 12th edition (2023 text) — the 13th edition holds no group for this good, so there is nothing it disagrees with"
|
|
89
89
|
: `mixed: ${from13} of ${ruled.length} matched goods take the 13th edition's group, the rest have no 13th-edition group at all`,
|
|
@@ -337,6 +337,30 @@ export const CAPABILITIES = Object.freeze({
|
|
|
337
337
|
// search rows therefore finds no scripts anywhere and reads as "the index holds none" — which is
|
|
338
338
|
// how this probe failed on its first attempt, and it failed by returning an empty set, not an error.
|
|
339
339
|
nativeScriptIndex: true,
|
|
340
|
+
// ── A TERM THAT MIXES ALPHABETS IS NOT SEARCHED AS WRITTEN ──────────────────────────────────────
|
|
341
|
+
//
|
|
342
|
+
// A term mixing Latin letters with Greek or Cyrillic ones, such as `τιmbεr`, is answered as if the
|
|
343
|
+
// non-Latin letters were absent: the rows that come back share only the Latin remainder, so none of
|
|
344
|
+
// them is the spelling asked for. The form band's whole-word look-alike swap writes exactly these
|
|
345
|
+
// terms, because it leaves every letter without a Greek or Cyrillic twin in Latin, and on a common
|
|
346
|
+
// remainder the unrelated rows arrive as a crowd that a report then reads as unread coverage.
|
|
347
|
+
//
|
|
348
|
+
// `false` says so. driver/form-neighbourhood.mjs then leaves those spellings out of the form band on
|
|
349
|
+
// this register and lists them there as not searched. A wholly Greek or Cyrillic term is a different
|
|
350
|
+
// question, answered by `nativeScriptIndex` above, and still goes out.
|
|
351
|
+
//
|
|
352
|
+
// OPTIONAL, like `queryableStatuses`: a provider that does not declare it keeps sending these terms,
|
|
353
|
+
// as every provider did before the field existed.
|
|
354
|
+
mixedScriptQuery: false,
|
|
355
|
+
// ── A KNOCKOUT'S LISTING ALREADY HOLDS ITS COUNT ─────────────────────────────────────────────────
|
|
356
|
+
//
|
|
357
|
+
// The knockout listing asks the count lane's exact question (the same term, classes and territories)
|
|
358
|
+
// and `include_total` puts the register's own total on the answer. So a knockout lists first and takes
|
|
359
|
+
// the identical and close counts from those totals (driver/register-count.mjs listingAnswers), rather
|
|
360
|
+
// than asking each of them twice. A term the listing did not answer is counted as before.
|
|
361
|
+
//
|
|
362
|
+
// OPTIONAL: a provider that does not declare it counts first and lists second, as before.
|
|
363
|
+
listingAnswersCount: true,
|
|
340
364
|
// Opposition data IS on the record, and in three places: `opposition_window` on every search row,
|
|
341
365
|
// `proceedings_count` on the full record, and the filters `has_proceedings`,
|
|
342
366
|
// `opposition_status` and `opposition_closes_before/after`. GET /v1/trademarks/{id}/proceedings
|
|
@@ -27,6 +27,7 @@ import { goodsTermsList } from "../../_shared/term-shape.mjs"; // the shared r
|
|
|
27
27
|
|
|
28
28
|
import { makeLedger } from "../../_shared/ledger.mjs";
|
|
29
29
|
import { nonAnswerBodyError, parseJsonBody, unparsedBodyError } from "../../_shared/http-body.mjs";
|
|
30
|
+
import { answerKey, compareAnswers, noteAnswer, openAnswerMemory, recallAnswer, rememberAnswer } from "../../_shared/answer-memory.mjs";
|
|
30
31
|
import { makeEnumerate, isOwnerScoped } from "../../_shared/enumerate.mjs";
|
|
31
32
|
import { makeExecutePlan } from "../../_shared/execute-plan.mjs";
|
|
32
33
|
import CAPABILITIES, { SIGNA_OFFICE_KEYS, OWNER_SCOPED_WINDOW } from "./capabilities.js";
|
|
@@ -76,6 +77,11 @@ export function refToOffice(ref) {
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
// ── HTTP helper (Bearer auth + the metered chokepoint) ─────────────────────────────────────────────
|
|
80
|
+
//
|
|
81
|
+
// THE RUN MEMORY sits here because every request this provider makes passes through here — see
|
|
82
|
+
// providers/_shared/answer-memory.mjs. The question is the base address, the method, the path and the
|
|
83
|
+
// whole body, and never the credential; the base is in it so a test server and the register never share
|
|
84
|
+
// an answer. With no memory for the run (the default), this is the plain fetch it always was.
|
|
79
85
|
export async function signaFetch(apiKey, base, path, { method = "GET", body = null, retries = 1, tctx = null } = {}) {
|
|
80
86
|
const url = `${base}${path}`;
|
|
81
87
|
const headers = { Authorization: `Bearer ${apiKey}`, Accept: "application/json" };
|
|
@@ -83,6 +89,26 @@ export async function signaFetch(apiKey, base, path, { method = "GET", body = nu
|
|
|
83
89
|
if (body !== null) { headers["Content-Type"] = "application/json"; init.body = JSON.stringify(body); }
|
|
84
90
|
|
|
85
91
|
const t0 = Date.now();
|
|
92
|
+
const mem = openAnswerMemory(tctx?.recordLog);
|
|
93
|
+
const key = mem ? answerKey({ base, method, path, body }) : null;
|
|
94
|
+
const recalled = mem ? recallAnswer(mem, key) : null;
|
|
95
|
+
// A held answer too old to trust its next-page cursor is not used, and is replaced by the fresh one.
|
|
96
|
+
const held = recalled && !recalled.stale ? recalled : null;
|
|
97
|
+
// `via` says which kind of process asked: the driver hands its calls the run's record log, and a tool
|
|
98
|
+
// server a stage spawned finds the run through its environment. The watch log must show both, because a
|
|
99
|
+
// memory only one of them can find removes only half the repeats.
|
|
100
|
+
const note = (row) => noteAnswer(mem, { ts: new Date().toISOString(), mode: mem.mode, key, method, path, tool: tctx?.kind ?? null,
|
|
101
|
+
via: tctx?.recordLog ? "driver" : "tool-server", session: tctx?.sessionKey ?? null,
|
|
102
|
+
...(recalled?.stale ? { held_stale: true, held_age_ms: recalled.age_ms } : {}), ...row });
|
|
103
|
+
if (held && mem.mode === "on") {
|
|
104
|
+
// Served from the memory: nothing goes to the register. The ledger still gets its line, marked as a
|
|
105
|
+
// cache hit, so the run's usage shows the request was made and was not paid for.
|
|
106
|
+
const { body: parsed, parseError } = parseJsonBody(held.raw);
|
|
107
|
+
const ok = held.status >= 200 && held.status < 300;
|
|
108
|
+
logCall(tctx, { http_status: held.status, ok, attempts: 0, took_ms: Date.now() - t0, bytes: held.raw.length, cache_hit: true });
|
|
109
|
+
note({ held: true, served: true, status: held.status });
|
|
110
|
+
return { status: held.status, ok, url, body: parsed, raw: held.raw, parseError };
|
|
111
|
+
}
|
|
86
112
|
let attempts = 0, resp;
|
|
87
113
|
try {
|
|
88
114
|
for (let i = 0; i <= retries; i++) {
|
|
@@ -93,15 +119,55 @@ export async function signaFetch(apiKey, base, path, { method = "GET", body = nu
|
|
|
93
119
|
}
|
|
94
120
|
} catch (err) {
|
|
95
121
|
logCall(tctx, { http_status: 0, ok: false, attempts, took_ms: Date.now() - t0, bytes: 0, cache_hit: false });
|
|
122
|
+
if (mem) note({ held: Boolean(held), status: 0 });
|
|
96
123
|
throw err;
|
|
97
124
|
}
|
|
98
125
|
const raw = await resp.text();
|
|
99
126
|
// The parse failure travels on `parseError` instead of being swallowed — providers/_shared/http-body.mjs.
|
|
100
127
|
const { body: parsed, parseError } = parseJsonBody(raw);
|
|
101
128
|
logCall(tctx, { http_status: resp.status, ok: resp.ok, attempts, took_ms: Date.now() - t0, bytes: raw.length, cache_hit: false });
|
|
129
|
+
if (mem) {
|
|
130
|
+
// In `watch` every request reaches this line, held or not, and the line says what the memory would
|
|
131
|
+
// have done. The FIRST answer is the one kept: a later identical request is compared against it
|
|
132
|
+
// and never replaces it, so every comparison in a run is against the answer that would be served.
|
|
133
|
+
const summary = rememberableAnswer(method, resp.status, parsed, parseError);
|
|
134
|
+
const row = { held: Boolean(held), status: resp.status, rememberable: Boolean(summary) };
|
|
135
|
+
if (held && summary) Object.assign(row, compareAnswers(held.summary, summary));
|
|
136
|
+
if (recalled?.stale && summary) Object.assign(row, compareAnswers(recalled.summary, summary));
|
|
137
|
+
if (summary && !held) row.stored = rememberAnswer(mem, key, { status: resp.status, raw, summary });
|
|
138
|
+
note(row);
|
|
139
|
+
}
|
|
102
140
|
return { status: resp.status, ok: resp.ok, url, body: parsed, raw, parseError };
|
|
103
141
|
}
|
|
104
142
|
|
|
143
|
+
/**
|
|
144
|
+
* What the run memory may keep, as the short summary a watch compares — or null when the answer must
|
|
145
|
+
* be asked again next time.
|
|
146
|
+
*
|
|
147
|
+
* KEPT: a search page that is a whole answer (it parsed, it carries `data[]` — isSearchResponseBody —
|
|
148
|
+
* and it carries its total); a record that parsed and names itself; and a `validation_error`, which is
|
|
149
|
+
* the register saying the question itself is malformed and will say so every time.
|
|
150
|
+
*
|
|
151
|
+
* NEVER KEPT: a body that did not parse, a 200 without `data[]`, an authentication, payment, permission
|
|
152
|
+
* or rate refusal, a cursor refusal, a server error, and anything else. Those are exactly the answers
|
|
153
|
+
* doSearch and doRecordFetch refuse or report as failures, and remembering one would turn a passing
|
|
154
|
+
* fault into the run's permanent answer.
|
|
155
|
+
*/
|
|
156
|
+
export function rememberableAnswer(method, status, body, parseError) {
|
|
157
|
+
if (parseError || body == null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
158
|
+
if (status === 200 && method === "POST") {
|
|
159
|
+
if (!isSearchResponseBody(body) || !Number.isFinite(body.pagination?.total_count)) return null;
|
|
160
|
+
return { total: body.pagination.total_count, ids: body.data.map((row) => row?.id ?? null), next_page: body.has_more === true };
|
|
161
|
+
}
|
|
162
|
+
if (status === 200 && method === "GET") {
|
|
163
|
+
const rec = body.data ?? body;
|
|
164
|
+
if (!rec || typeof rec !== "object" || Array.isArray(rec) || !rec.id) return null;
|
|
165
|
+
return { total: null, ids: [rec.id], next_page: false };
|
|
166
|
+
}
|
|
167
|
+
if (status === 400 && body.error?.type === "validation_error") return { total: null, ids: [], next_page: false, refusal: "validation_error" };
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
105
171
|
// ── Search-request assembly ────────────────────────────────────────────────────────────────────────
|
|
106
172
|
// THE TWO MATCH SHAPES ARE MUTUALLY EXCLUSIVE, and the live specification is explicit:
|
|
107
173
|
// "Deterministic modes require a query and disallow strategies/ranking_profile."
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
3
|
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
4
4
|
//
|
|
5
|
-
// deprecate-below.mjs — put one warning on every published version below a named stable
|
|
5
|
+
// deprecate-below.mjs — put one warning on every published version below a named stable, or on a named
|
|
6
|
+
// range of published versions.
|
|
6
7
|
//
|
|
7
8
|
// node scripts/deprecate-below.mjs --below 0.3.1 [--dry-run]
|
|
9
|
+
// node scripts/deprecate-below.mjs --range "0.2.0..0.4.0-beta.0 except 0.3.3" [--dry-run]
|
|
8
10
|
//
|
|
9
11
|
// ── WHY THIS IS A SCRIPT IN CI AND NOT SOMETHING A PERSON RUNS ──────────────────────────────────────
|
|
10
12
|
//
|
|
@@ -70,6 +72,56 @@ export function compareVersions(a, b) {
|
|
|
70
72
|
return 0;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
// ── THE RANGE MODE: A NAMED SET OF VERSIONS, ONE SENTENCE, NO REASON GIVEN ──────────────────────────
|
|
76
|
+
//
|
|
77
|
+
// Some versions have to be warned that are not simply "everything below a stable": a stretch of betas
|
|
78
|
+
// and stables with one version held out because an install still runs it. The range says exactly that,
|
|
79
|
+
// inclusive at both ends in npm's own order, and the exceptions are named one by one.
|
|
80
|
+
//
|
|
81
|
+
// ITS SENTENCE IS FIXED HERE AND NOT TYPED AT DISPATCH. It is public text on every version page, so it
|
|
82
|
+
// is the owner's sentence, approved word for word, and a dispatch box is no place to redraft it.
|
|
83
|
+
//
|
|
84
|
+
// IT REPLACES AN OLDER MESSAGE IN THE RANGE, unlike the mode above. A range is warned for one reason, so
|
|
85
|
+
// every version in it reads the same; a version that already says exactly this is left alone.
|
|
86
|
+
//
|
|
87
|
+
// IT NEVER TOUCHES A VERSION A DIST-TAG POINTS AT. `latest` and `beta` are what `npm install` resolves
|
|
88
|
+
// to, and a warning on the version a reader is told to install is worse than none.
|
|
89
|
+
export const RANGE_MESSAGE = "This version is no longer supported. Please upgrade to the latest version of clearotron.";
|
|
90
|
+
|
|
91
|
+
const VERSION = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?$/;
|
|
92
|
+
|
|
93
|
+
/** Read "<from>..<through>[ except <v>[,<v>…]]". Throws on anything else. */
|
|
94
|
+
export function parseRangeSpec(spec) {
|
|
95
|
+
const m = String(spec ?? "").trim().match(/^(\S+)\.\.(\S+?)(?:\s+except\s+(\S+))?$/);
|
|
96
|
+
if (!m) throw new Error(`"${spec}" is not a range: write <from>..<through>, optionally followed by " except <v>,<v>"`);
|
|
97
|
+
const [, from, through, ex = ""] = m;
|
|
98
|
+
const except = ex ? ex.split(",").map((v) => v.trim()).filter(Boolean) : [];
|
|
99
|
+
for (const v of [from, through, ...except]) if (!VERSION.test(v)) throw new Error(`"${v}" is not a version`);
|
|
100
|
+
if (compareVersions(from, through) > 0) throw new Error(`${from} is above ${through}: the range is empty`);
|
|
101
|
+
return { from, through, except };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The published versions a range names, in order: from and through inclusive, the exceptions out.
|
|
106
|
+
* Throws, before anything is written, when a bound is not published, an exception lies outside the
|
|
107
|
+
* range or is not published, or a selected version is one a dist-tag points at.
|
|
108
|
+
*/
|
|
109
|
+
export function selectRange(all, { from, through, except }, distTags = {}) {
|
|
110
|
+
for (const b of [from, through]) if (!all.includes(b)) throw new Error(`${b} is not a published version of ${PKG}`);
|
|
111
|
+
const inRange = (v) => compareVersions(v, from) >= 0 && compareVersions(v, through) <= 0;
|
|
112
|
+
for (const e of except) {
|
|
113
|
+
if (!all.includes(e)) throw new Error(`the exception ${e} is not a published version of ${PKG}`);
|
|
114
|
+
if (!inRange(e)) throw new Error(`the exception ${e} lies outside ${from}..${through}`);
|
|
115
|
+
}
|
|
116
|
+
const picked = all.filter((v) => inRange(v) && !except.includes(v)).sort(compareVersions);
|
|
117
|
+
const tagged = Object.entries(distTags).filter(([, v]) => picked.includes(v));
|
|
118
|
+
if (tagged.length) {
|
|
119
|
+
throw new Error(`the range takes ${tagged.map(([t, v]) => `${v} (${t})`).join(", ")}, which a dist-tag points at; `
|
|
120
|
+
+ "move the tag or hold the version out");
|
|
121
|
+
}
|
|
122
|
+
return picked;
|
|
123
|
+
}
|
|
124
|
+
|
|
73
125
|
const npm = (...args) => execFileSync("npm", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
74
126
|
|
|
75
127
|
/** Every published version, and the deprecation message each carries. Read once, as a list. */
|
|
@@ -130,9 +182,11 @@ async function main() {
|
|
|
130
182
|
const argv = process.argv.slice(2);
|
|
131
183
|
const at = (flag) => { const i = argv.indexOf(flag); return i >= 0 ? argv[i + 1] : null; };
|
|
132
184
|
const DRY = argv.includes("--dry-run");
|
|
185
|
+
const range = at("--range");
|
|
186
|
+
if (range) return deprecateRange(range, DRY);
|
|
133
187
|
const below = at("--below");
|
|
134
188
|
if (!below) {
|
|
135
|
-
console.error("deprecate-below: --below <version>
|
|
189
|
+
console.error("deprecate-below: --below <version> or --range \"<from>..<through> except <v>\" is required.");
|
|
136
190
|
return 2;
|
|
137
191
|
}
|
|
138
192
|
const MESSAGE = `This version is superseded. Please upgrade to ${below} or later: npm install ${PKG}@${below}`;
|
|
@@ -201,4 +255,62 @@ async function main() {
|
|
|
201
255
|
return failed ? 1 : 0;
|
|
202
256
|
}
|
|
203
257
|
|
|
258
|
+
/** The range mode's run: select, write the fixed sentence where it is not already, read each back. */
|
|
259
|
+
async function deprecateRange(spec, DRY) {
|
|
260
|
+
let picked;
|
|
261
|
+
try {
|
|
262
|
+
const parsed = parseRangeSpec(spec);
|
|
263
|
+
const all = published();
|
|
264
|
+
if (!all.length) throw new Error("the registry listed no versions");
|
|
265
|
+
const distTags = JSON.parse(npm("view", PKG, "dist-tags", "--json"));
|
|
266
|
+
picked = selectRange(all, parsed, distTags);
|
|
267
|
+
console.log(`deprecate-below: ${all.length} published version(s); ${picked.length} in ${spec}.`);
|
|
268
|
+
} catch (e) {
|
|
269
|
+
console.error(`deprecate-below: refusing — ${e.message}. Nothing was written.`);
|
|
270
|
+
return 2;
|
|
271
|
+
}
|
|
272
|
+
let done = 0, skipped = 0, failed = 0;
|
|
273
|
+
const wrote = [];
|
|
274
|
+
for (const v of picked) {
|
|
275
|
+
let existing;
|
|
276
|
+
try { existing = deprecationOf(v); }
|
|
277
|
+
catch (e) {
|
|
278
|
+
console.error(` ${v}: could NOT be read (${String(e.message).split("\n")[0]}) — left alone.`);
|
|
279
|
+
failed += 1;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (existing === RANGE_MESSAGE) { console.log(` ${v}: already carries this message`); skipped += 1; continue; }
|
|
283
|
+
const was = existing ? ` (replacing "${existing.slice(0, 60)}")` : "";
|
|
284
|
+
if (DRY) { console.log(` ${v}: would deprecate${was}`); done += 1; continue; }
|
|
285
|
+
try {
|
|
286
|
+
npm("deprecate", `${PKG}@${v}`, RANGE_MESSAGE);
|
|
287
|
+
} catch (e) {
|
|
288
|
+
console.error(` ${v}: deprecate FAILED — ${String(e.stderr ?? e.message).split("\n")[0]}`);
|
|
289
|
+
failed += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
console.log(` ${v}: deprecate accepted${was}`);
|
|
293
|
+
wrote.push(v);
|
|
294
|
+
}
|
|
295
|
+
let unconfirmed = [];
|
|
296
|
+
if (wrote.length && !DRY) {
|
|
297
|
+
// A replaced message is confirmed only once THIS sentence is served, not merely any sentence.
|
|
298
|
+
const r = await confirmWrites({
|
|
299
|
+
versions: wrote,
|
|
300
|
+
read: (v) => (deprecationOf(v) === RANGE_MESSAGE ? RANGE_MESSAGE : null),
|
|
301
|
+
sleep: (ms) => new Promise((res) => setTimeout(res, ms)),
|
|
302
|
+
});
|
|
303
|
+
unconfirmed = r.unconfirmed;
|
|
304
|
+
done += r.confirmed.length;
|
|
305
|
+
for (const v of r.confirmed) console.log(` ${v}: serving the message`);
|
|
306
|
+
for (const v of unconfirmed) {
|
|
307
|
+
console.error(` ${v}: WRITE ACCEPTED, not yet served after ${Math.round(r.waitedMs / 1000)}s — read it from `
|
|
308
|
+
+ "outside before treating it as undone; re-running is safe and reports it as already carrying the message.");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
console.log(`deprecate-below: ${done} ${DRY ? "would be " : ""}deprecated, ${skipped} already carried this message, `
|
|
312
|
+
+ `${unconfirmed.length} accepted but not yet served, ${failed} failed.`);
|
|
313
|
+
return failed ? 1 : 0;
|
|
314
|
+
}
|
|
315
|
+
|
|
204
316
|
if (process.argv[1] && process.argv[1].endsWith("deprecate-below.mjs")) process.exit(await main());
|
|
@@ -237,7 +237,7 @@ const substituteVendorKey = (key) => {
|
|
|
237
237
|
|
|
238
238
|
// meta.json keys the freeze is EXPECTED to change, with the reason. Anything else differing is a finding.
|
|
239
239
|
const EXPECTED_META_DELTA = {
|
|
240
|
-
tokens: "telemetry pruned — _driver/*.jsonl is the only source (driver/tokens.mjs
|
|
240
|
+
tokens: "telemetry pruned — _driver/*.jsonl is the only source (rollupTokens in driver/tokens.mjs)",
|
|
241
241
|
servedModels: "telemetry pruned — the attempt rows in _driver/*.jsonl are the only source (servedModels in driver/tokens.mjs)",
|
|
242
242
|
};
|
|
243
243
|
|
|
@@ -107,6 +107,7 @@ import { envFrom } from "../shared/env-aliases.mjs";
|
|
|
107
107
|
import { gitTry, treeOf } from "../shared/tree-commit.mjs"; // — a packaged install has no git, and says its commit in build-info.json
|
|
108
108
|
import { exitFor } from "../driver/surface-exit-verdict.mjs"; // — a check that could not look is not a drift, and they want different things done // — the name a reader is told to set is the one in force
|
|
109
109
|
import { planRunAgreementVerdict } from "../driver/plan-run-agreement-verdict.mjs";
|
|
110
|
+
import { doorCallVerdict } from "../driver/door-call-verdict.mjs"; // — a refused or unanswered door call compared nothing
|
|
110
111
|
|
|
111
112
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
112
113
|
const asJson = process.argv.includes("--json");
|
|
@@ -605,7 +606,11 @@ try {
|
|
|
605
606
|
}
|
|
606
607
|
} catch (e) {
|
|
607
608
|
if (e?.message === "__door_unset__") skip("roster resolves", "this instance does not say where its ops-MCP is, so the roster was NOT PROBED");
|
|
608
|
-
else
|
|
609
|
+
else {
|
|
610
|
+
const v = doorCallVerdict(e, { asked: "list_profiles", notCompared: "the roster was not compared" });
|
|
611
|
+
if (v) record("roster resolves", v.state, v.message, v.blocked === true);
|
|
612
|
+
else fail("roster resolves", `list_profiles failed: ${e.message}`);
|
|
613
|
+
}
|
|
609
614
|
}
|
|
610
615
|
|
|
611
616
|
// 3. THE LOAD-BEARING CHECK — every door's availability answer vs the engine's own, recomputed here
|
|
@@ -644,7 +649,11 @@ try {
|
|
|
644
649
|
}
|
|
645
650
|
} catch (e) {
|
|
646
651
|
if (e?.message === "__door_unset__") skip("ops-MCP reachable", "this instance does not say where its ops-MCP is — NOT PROBED");
|
|
647
|
-
else
|
|
652
|
+
else {
|
|
653
|
+
const v = doorCallVerdict(e, { asked: `${MCP_URL} for describe_options`, notCompared: "the door's answers were not compared" });
|
|
654
|
+
if (v) record("ops-MCP reachable", v.state, v.message, v.blocked === true);
|
|
655
|
+
else fail("ops-MCP reachable", `${MCP_URL}: ${e.message}`);
|
|
656
|
+
}
|
|
648
657
|
}
|
|
649
658
|
|
|
650
659
|
// ── 3b. THE TRIGGER LANE, AS ITS OWN SURFACE ─────────────────────────────────────────────────────────
|