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.
Files changed (98) hide show
  1. package/.env.example +24 -23
  2. package/INSTALL.md +142 -75
  3. package/README.md +3 -3
  4. package/bin/onboard.mjs +637 -216
  5. package/bin/start.mjs +133 -23
  6. package/bin/update.mjs +82 -11
  7. package/build-info.json +2 -2
  8. package/docs/architecture/04-configuration-reference.md +26 -11
  9. package/docs/architecture/05-config-governance.md +17 -7
  10. package/driver/CHANGELOG.md +83 -0
  11. package/driver/band-size.mjs +59 -0
  12. package/driver/config-inventory.mjs +112 -9
  13. package/driver/connotation-search.mjs +45 -0
  14. package/driver/contract-arm2-baseline.json +1 -3
  15. package/driver/contract-e3-backlog.mjs +29 -29
  16. package/driver/contract-vocabulary.mjs +59 -25
  17. package/driver/door-gates.mjs +41 -7
  18. package/driver/driver.config.mjs +272 -59
  19. package/driver/engine/CONTRACT.md +10 -3
  20. package/driver/engine/README.md +2 -2
  21. package/driver/engine/anthropic-agent.mjs +77 -21
  22. package/driver/engine/auth.mjs +129 -10
  23. package/driver/engine/jx-turn.mjs +7 -6
  24. package/driver/engine/mcp/recording-server.mjs +13 -0
  25. package/driver/engine/openai-agent.mjs +4 -2
  26. package/driver/engine/probe.mjs +110 -23
  27. package/driver/findings-model.mjs +1 -1
  28. package/driver/flag-snapshot.mjs +28 -5
  29. package/driver/gateway.mjs +30 -21
  30. package/driver/jx-lanes.mjs +21 -2
  31. package/driver/jx-units.mjs +6 -3
  32. package/driver/jx.mjs +4 -2
  33. package/driver/matter-frame-record.mjs +90 -1
  34. package/driver/named-band.mjs +34 -2
  35. package/driver/package.json +1 -1
  36. package/driver/pipeline.mjs +391 -26
  37. package/driver/portal-config-view.mjs +30 -1
  38. package/driver/portal-report.mjs +15 -1
  39. package/driver/portal-service.mjs +46 -6
  40. package/driver/predelivery-lint.mjs +12 -2
  41. package/driver/publish/index.mjs +46 -5
  42. package/driver/publish/knockout.mjs +10 -1
  43. package/driver/publish/render-knockout.mjs +69 -7
  44. package/driver/publish/render.mjs +170 -59
  45. package/driver/publish/report-data.mjs +4 -1
  46. package/driver/publish/report-topbar.mjs +58 -0
  47. package/driver/publish/templates/report.css +28 -2
  48. package/driver/publish/xlsx.mjs +13 -1
  49. package/driver/register-availability.mjs +2 -2
  50. package/driver/register-coverage.mjs +94 -1
  51. package/driver/register-digest-record.mjs +236 -11
  52. package/driver/register-plan.mjs +170 -0
  53. package/driver/result-noun-fields.mjs +7 -4
  54. package/driver/run-economics.mjs +41 -10
  55. package/driver/run-requirements.mjs +173 -9
  56. package/driver/runner.mjs +3 -3
  57. package/driver/stages.mjs +12 -8
  58. package/driver/suite-census.json +162 -72
  59. package/driver/systemd/README.md +7 -4
  60. package/driver/terminal-clamp.mjs +107 -1
  61. package/driver/tokens.mjs +169 -3
  62. package/driver/unit-environment.mjs +42 -15
  63. package/driver/unit-inventory.mjs +19 -2
  64. package/driver/verify.mjs +50 -5
  65. package/mcp-server/CHANGELOG.md +8 -0
  66. package/mcp-server/http-server.mjs +4 -0
  67. package/mcp-server/lib/audit.mjs +11 -1
  68. package/mcp-server/lib/http-handler.mjs +6 -2
  69. package/mcp-server/package.json +1 -1
  70. package/mcp-server/server.mjs +16 -2
  71. package/package.json +1 -1
  72. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
  73. package/portal-ui/dist/index.html +1 -1
  74. package/portal-ui/package.json +1 -1
  75. package/providers/clarivate/src/capabilities.js +5 -5
  76. package/providers/clarivate/src/core.js +1 -1
  77. package/providers/corsearch/src/core.js +2 -2
  78. package/providers/jx/README.md +2 -1
  79. package/providers/jx/src/turn-envelope.mjs +8 -3
  80. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  81. package/providers/oauth-mcp-bridge/package.json +1 -1
  82. package/providers/perplexity/src/core.js +3 -3
  83. package/providers/signa/src/capabilities.js +5 -6
  84. package/providers/signa/src/core.js +1 -1
  85. package/providers/uspto-local/README.md +1 -1
  86. package/scripts/authority-boundary-probe.mjs +4 -2
  87. package/scripts/env-audit.mjs +12 -6
  88. package/scripts/freeze-example-run.mjs +49 -16
  89. package/scripts/generated-files-are-current.mjs +69 -4
  90. package/scripts/release-duplicate-notes.mjs +246 -0
  91. package/scripts/release-publish-guard.mjs +64 -6
  92. package/scripts/report-print-check.mjs +194 -0
  93. package/scripts/settings-render-check.mjs +75 -2
  94. package/scripts/test-full.mjs +96 -3
  95. package/scripts/test-run.mjs +10 -0
  96. package/shared/deployment-box.mjs +7 -2
  97. package/shared/driver-dir.mjs +1 -1
  98. 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
  }
@@ -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
- for (const r of recs) { if (r && typeof r === "object" && !Array.isArray(r)) enumerated.push({ ...r, ...prov }); }
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:1439 validatePlanFeasibility already enforces the same rule one layer up
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.
@@ -2,7 +2,7 @@
2
2
  "name": "clearotron-driver",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.6",
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": {