clearotron 0.3.2-beta.6 → 0.3.2-beta.8
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 +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +82 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +83 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/connotation-search.mjs +45 -0
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +29 -29
- package/driver/contract-vocabulary.mjs +59 -25
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +30 -21
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +391 -26
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +28 -2
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +7 -4
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +162 -72
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +50 -5
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/http-server.mjs +4 -0
- package/mcp-server/lib/audit.mjs +11 -1
- package/mcp-server/lib/http-handler.mjs +6 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +16 -2
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/clarivate/src/capabilities.js +5 -5
- package/providers/clarivate/src/core.js +1 -1
- package/providers/corsearch/src/core.js +2 -2
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +3 -3
- package/providers/signa/src/capabilities.js +5 -6
- package/providers/signa/src/core.js +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/release-duplicate-notes.mjs +246 -0
- package/scripts/release-publish-guard.mjs +64 -6
- package/scripts/report-print-check.mjs +194 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
|
@@ -141,6 +141,16 @@ export function renderMatterFrame(model) {
|
|
|
141
141
|
// is owed against — so the frame states it where a reader can see it rather than only in a field.
|
|
142
142
|
if ((model.ratified_forms ?? []).length > 1)
|
|
143
143
|
out.push(`- **Ratified forms:** ${model.ratified_forms.join(", ")}`);
|
|
144
|
+
// THE EXCLUSION IS EVIDENCED ON THE DOCUMENT A READER SEES, and it is stated as a PROPOSAL because
|
|
145
|
+
// that is what it is at this point in the run. A reader meeting "excluded" here would believe the
|
|
146
|
+
// search was narrowed on the frame's authority; the driver's verification has not run yet, and if it
|
|
147
|
+
// cannot confirm the client's ownership the element is searched in full and this line is the only
|
|
148
|
+
// place the question was ever raised.
|
|
149
|
+
if (model.house_element_candidate)
|
|
150
|
+
out.push(`- **Client's own element, proposed for exclusion:** ${model.house_element_candidate.element}`
|
|
151
|
+
+ ` — the analysis would be limited to ${model.house_element_candidate.remainder}.`
|
|
152
|
+
+ ` Basis: ${model.house_element_candidate.owner_basis}.`
|
|
153
|
+
+ " Excluded only if the driver confirms the client's own registrations on the register; otherwise searched in full.");
|
|
144
154
|
out.push("");
|
|
145
155
|
|
|
146
156
|
// `Search channels:` — domains only; the grid site-restricts to them and the general web is always
|
|
@@ -172,9 +182,10 @@ export function renderMatterFrame(model) {
|
|
|
172
182
|
*/
|
|
173
183
|
/** The shape this tool declares, at every depth — what the ACCEPTOR enforces. */
|
|
174
184
|
const DECLARED = Object.freeze({
|
|
175
|
-
"": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms"],
|
|
185
|
+
"": ["prose_body", "scope_basis", "scope_jurisdictions", "excluded_jurisdictions", "search_channels", "meaning_angles", "meaning_angles_none", "intake_asks", "identified_classes", "ratified_forms", "house_element_candidate"],
|
|
176
186
|
intake_asks: ["ask", "owner"],
|
|
177
187
|
identified_classes: ["class", "reason"],
|
|
188
|
+
house_element_candidate: ["element", "remainder", "owner_basis"],
|
|
178
189
|
});
|
|
179
190
|
|
|
180
191
|
/** Refuse an undeclared key by path, at depth. Shared walk; the table above is what is this tool's. */
|
|
@@ -218,6 +229,26 @@ export function frameRatifiedForms(runDir) {
|
|
|
218
229
|
return (Array.isArray(rows) ? rows : []).map((r) => String(r ?? "").trim()).filter(Boolean);
|
|
219
230
|
}
|
|
220
231
|
|
|
232
|
+
/**
|
|
233
|
+
* The house element this run's frame PROPOSED, or null. IMPURE (reads the run's own accepted call).
|
|
234
|
+
*
|
|
235
|
+
* A PROPOSAL, AND THE CALLER MUST TREAT IT AS ONE. Nothing here has been checked against the register:
|
|
236
|
+
* the frame runs before the plan, holds no band tool, and is reporting how it reads the matter. The
|
|
237
|
+
* caller verifies ownership by owner-scoped lookup and writes its own receipt; the plan excludes on
|
|
238
|
+
* that receipt. A caller that excluded on this return would be dropping an element from the search on a
|
|
239
|
+
* model's say-so, which is the one direction that reaches a client as a clean answer over unswept
|
|
240
|
+
* ground rather than as a visible failure.
|
|
241
|
+
*
|
|
242
|
+
* Null on every archived and replayed run whose accepted call predates the field, so none of them moves.
|
|
243
|
+
*/
|
|
244
|
+
export function frameHouseElementCandidate(runDir) {
|
|
245
|
+
const h = lastAcceptedMatterFrame(runDir)?.house_element_candidate;
|
|
246
|
+
const element = String(h?.element ?? "").trim();
|
|
247
|
+
const remainder = String(h?.remainder ?? "").trim();
|
|
248
|
+
if (!element || !remainder) return null;
|
|
249
|
+
return { element, remainder, owner_basis: String(h?.owner_basis ?? "").trim() };
|
|
250
|
+
}
|
|
251
|
+
|
|
221
252
|
/** The last ACCEPTED call for this run, or null. */
|
|
222
253
|
export function lastAcceptedMatterFrame(runDir) {
|
|
223
254
|
return lastAccepted(matterFrameCallPaths(String(runDir ?? "")).accepted, readFileSync);
|
|
@@ -252,6 +283,15 @@ export function mergeMatterFrameCall(stored, received) {
|
|
|
252
283
|
// omission here is a repair that did not mention them, never a decision to withdraw them.
|
|
253
284
|
identified_classes: keepIfAbsent(received?.identified_classes, base.identified_classes),
|
|
254
285
|
ratified_forms: keepIfAbsent(received?.ratified_forms, base.ratified_forms),
|
|
286
|
+
// KEEP-IF-ABSENT, and the direction of its failure is the OPPOSITE of the two above — which is
|
|
287
|
+
// worth saying, because the reasoning that protects them does not transfer and a reader who assumed
|
|
288
|
+
// it did would mis-rank this key. Dropping the identified classes NARROWS the next compile, towards
|
|
289
|
+
// missing rights. Dropping this one WIDENS it: the house element goes back to being searched as a
|
|
290
|
+
// conflict axis, which is the band this field exists to shrink, so a partial call that lost it costs
|
|
291
|
+
// a slower run and the report's one sentence explaining what was excluded and why — never coverage.
|
|
292
|
+
// It is kept because a repair turn that did not mention the element is not a withdrawal of it, which
|
|
293
|
+
// is the same rule, reached by a different road.
|
|
294
|
+
house_element_candidate: keepIfAbsent(received?.house_element_candidate, base.house_element_candidate),
|
|
255
295
|
};
|
|
256
296
|
}
|
|
257
297
|
|
|
@@ -346,6 +386,54 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
|
|
|
346
386
|
ratified_forms.push(form);
|
|
347
387
|
}
|
|
348
388
|
|
|
389
|
+
// ── THE CLIENT'S OWN HOUSE ELEMENT — A CANDIDATE, NEVER A DECISION ───────────────────────────────
|
|
390
|
+
//
|
|
391
|
+
// THE DEFECT (production run, 2026-09-16). The mark was the client's own famous house mark followed by
|
|
392
|
+
// a tagline, and the plan treated the house element as a conflict axis: exact, variants, one-letter
|
|
393
|
+
// mutations, transliterations, incumbent checks. Over half the band came from that element. The
|
|
394
|
+
// reviewing lawyer's method for the same matter was three queries — the whole phrase, the shorter
|
|
395
|
+
// phrase, the last word alone — because an element the client already owns outright is not what the
|
|
396
|
+
// analysis is about. The engine planned thirty-six.
|
|
397
|
+
//
|
|
398
|
+
// WHY THE FIELD IS NAMED `candidate`, AND WHY THE NAME IS LOAD-BEARING. This frame CANNOT verify
|
|
399
|
+
// ownership: `BAND_READING_STAGES` is placement-inquiry, register-digest and synthesis, and the band
|
|
400
|
+
// does not exist yet when the frame runs. So everything here is the seat's reading of the matter, and
|
|
401
|
+
// an exclusion taken on a seat's say-so is an unsearched element justified by an assertion — a clean
|
|
402
|
+
// report over ground nobody swept, which is the one failure that reaches a client as a wrong answer
|
|
403
|
+
// rather than as no answer. The driver verifies against the register by owner and writes the receipt;
|
|
404
|
+
// the plan excludes on the RECEIPT and never on this field. Requirement 4 ("when the frame cannot
|
|
405
|
+
// verify, it does not exclude, and says so") is then the write order rather than a branch someone has
|
|
406
|
+
// to remember: no receipt, no exclusion.
|
|
407
|
+
//
|
|
408
|
+
// TYPED RATHER THAN PARSED, for the reason `identified_classes` gives and measures: a list derived
|
|
409
|
+
// from judgment prose dropped the primary entry in 19 of 21 runs.
|
|
410
|
+
let house_element_candidate = null;
|
|
411
|
+
if (params?.house_element_candidate !== undefined && params?.house_element_candidate !== null) {
|
|
412
|
+
const h = params.house_element_candidate;
|
|
413
|
+
const element = str(h?.element), remainder = str(h?.remainder), owner_basis = str(h?.owner_basis);
|
|
414
|
+
if (!element)
|
|
415
|
+
return { ok: false, reason: "matterframe_house_element_empty: name the element of the mark the client already owns, or omit the field entirely — a blank row is not an answer" };
|
|
416
|
+
if (!owner_basis)
|
|
417
|
+
return { ok: false, reason: `matterframe_house_element_basis_missing:${element} — say why you read this as the client's own registered element. It is not taken on your word (the driver verifies it against the register by owner), but the reader of the report is owed the ground, and an unverifiable basis is how a wrong exclusion would be argued for` };
|
|
418
|
+
// ── THE FLOOR, AND IT IS ON THE POPULATION RATHER THAN ON THE RULE ────────────────────────────
|
|
419
|
+
//
|
|
420
|
+
// The catastrophic direction here is naming too MUCH as the house element: mark "ACME WIDGETS",
|
|
421
|
+
// element "ACME WIDGETS", remainder nothing — and the plan becomes three queries that do not exist.
|
|
422
|
+
// Ownership can verify perfectly in that case, so requirement 4 does not catch it and no refusal
|
|
423
|
+
// downstream would either: "queries on the house element: 0" is satisfied by a plan with no queries
|
|
424
|
+
// at all. So the remainder is checked for being something a search can be built on, here, where the
|
|
425
|
+
// claim is made.
|
|
426
|
+
if (!remainder)
|
|
427
|
+
return { ok: false, reason: `matterframe_house_element_no_remainder:${element} — excluding it would leave nothing to search. The remainder is what the analysis is about; if the mark IS the client's own element with nothing distinctive after it, there is no exclusion to make and the field is omitted` };
|
|
428
|
+
if (remainder.toLowerCase() === element.toLowerCase())
|
|
429
|
+
return { ok: false, reason: `matterframe_house_element_remainder_same:${element} — the remainder must be the part of the mark that is NOT the house element` };
|
|
430
|
+
// The whole mark cannot be the house element by another spelling: an element that swallows the
|
|
431
|
+
// remainder leaves the same empty plan, arriving as two fields that merely look different.
|
|
432
|
+
if (element.toLowerCase().includes(remainder.toLowerCase()))
|
|
433
|
+
return { ok: false, reason: `matterframe_house_element_swallows_remainder:${element} — the element you named contains the remainder, so excluding it excludes the whole mark` };
|
|
434
|
+
house_element_candidate = { element, remainder, owner_basis };
|
|
435
|
+
}
|
|
436
|
+
|
|
349
437
|
const model = {
|
|
350
438
|
schema_version: SCHEMA_VERSION,
|
|
351
439
|
instructed_scope: instructedScope ?? null,
|
|
@@ -358,6 +446,7 @@ export function acceptMatterFrame(params, { instructedScope = null } = {}) {
|
|
|
358
446
|
intake_asks,
|
|
359
447
|
identified_classes,
|
|
360
448
|
ratified_forms,
|
|
449
|
+
house_element_candidate,
|
|
361
450
|
};
|
|
362
451
|
return { ok: true, model, content: renderMatterFrame(model) };
|
|
363
452
|
}
|
package/driver/named-band.mjs
CHANGED
|
@@ -29,6 +29,23 @@ import { abbrev } from "./repair-contract.mjs";
|
|
|
29
29
|
|
|
30
30
|
export const BAND_STATES = ["enumerated", "incomplete"];
|
|
31
31
|
|
|
32
|
+
// ── WHAT ONE QUERY MAY PUT INTO THE BAND ─────────────────────────────────────────────────────────
|
|
33
|
+
//
|
|
34
|
+
// THE DEFECT (production run, 2026-09-16). A 2,146-record band, of which 1,154 — 54% — were reachable
|
|
35
|
+
// from two queries and nothing else. Both were machine-built forms of an ordinary short word, so they
|
|
36
|
+
// matched every mark containing that word across four registers. Measured on the preserved band: the
|
|
37
|
+
// three biggest queries returned 589, 583 and 271 records; the fourth returned 185. A ceiling at 200
|
|
38
|
+
// therefore bites exactly those three and leaves the rest of that plan untouched, which is why it is
|
|
39
|
+
// the number rather than a rounder one.
|
|
40
|
+
//
|
|
41
|
+
// AN OVER-CAP QUERY IS NOT TRUNCATED, IT IS RECLASSIFIED. The band already has a word for "this query
|
|
42
|
+
// matched more than we carried": `incomplete`, which produces a crowd descriptor carrying the full
|
|
43
|
+
// count. So the excess is DISCLOSED with its number rather than dropped — judgment reads the crowd and
|
|
44
|
+
// can say the ground was too broad to enumerate, which is a true statement about the search. Silently
|
|
45
|
+
// keeping the first two hundred would be the one outcome worse than the flood: a narrower band that
|
|
46
|
+
// reads as complete.
|
|
47
|
+
export const BAND_QUERY_CAP = 200;
|
|
48
|
+
|
|
32
49
|
/**
|
|
33
50
|
* Parse + lightly validate the named-band artifact. Returns { enumerated:[…records], crowds:[…descriptors] }.
|
|
34
51
|
* Throws `named_band_*` tokens (token FIRST) so the stage validator + corrective-retry can key on the defect,
|
|
@@ -95,7 +112,22 @@ export function parseNamedBand(raw) {
|
|
|
95
112
|
// at band-shape.mjs's `record_id` filter — the same loss, one step further from anything that
|
|
96
113
|
// could name it.
|
|
97
114
|
const recs = Array.isArray(b.records) ? b.records : [];
|
|
98
|
-
|
|
115
|
+
const kept = [];
|
|
116
|
+
for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) kept.push({ ...r, ...prov }); }
|
|
117
|
+
if (kept.length > BAND_QUERY_CAP) {
|
|
118
|
+
// The count the provider reported is the truth about the ground; `kept.length` is only what this
|
|
119
|
+
// block carried. Prefer the reported total and fall back to what we hold, so the descriptor never
|
|
120
|
+
// claims a smaller crowd than it can prove.
|
|
121
|
+
const total = countOrNull(b.total_hits) ?? kept.length;
|
|
122
|
+
for (const r of kept.slice(0, BAND_QUERY_CAP)) enumerated.push(r);
|
|
123
|
+
crowds.push({
|
|
124
|
+
query, total_hits: total, fetched: BAND_QUERY_CAP, sample: [],
|
|
125
|
+
reason: `one query returned ${kept.length} record(s), over the ${BAND_QUERY_CAP}-record ceiling any single query may add to this band; the first ${BAND_QUERY_CAP} are carried and the rest are disclosed here as a crowd rather than enumerated`,
|
|
126
|
+
...(typeof b.qid === "string" && b.qid ? { qid: b.qid } : {}),
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
for (const r of kept) enumerated.push(r);
|
|
130
|
+
}
|
|
99
131
|
} else {
|
|
100
132
|
// count-first rescue (2026-07-10, copper-lattice): a crowd descriptor may carry per-term truth —
|
|
101
133
|
// `term_counts` (each term's tool-derived count + disposition) and the fully-enumerated tractable
|
|
@@ -137,7 +169,7 @@ export function parseNamedBand(raw) {
|
|
|
137
169
|
// byte-identical to a slice the plan deliberately counted without fetching. Measured on a real
|
|
138
170
|
// run: four capability-gap blocks carried `error:true, deferred:true` into this function and
|
|
139
171
|
// reached record-carry.json with both fields gone and a sentence claiming the run "has a hit
|
|
140
|
-
// COUNT for this slice". register-plan.mjs:
|
|
172
|
+
// COUNT for this slice". register-plan.mjs:1594 validatePlanFeasibility already enforces the same rule one layer up
|
|
141
173
|
// ("a transient must not ship indistinguishable from a sanctioned descriptor") — it reads the
|
|
142
174
|
// RAW blocks, which is why it could. Every consumer that reads THIS projection could not.
|
|
143
175
|
// Conditional like the four keys above, so old bands carry neither key and nothing shifts.
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.8",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|