clearotron 0.3.2-beta.12 → 0.3.2-beta.13

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 (95) hide show
  1. package/CONTRIBUTING.md +6 -5
  2. package/INSTALL.md +3 -4
  3. package/README.md +2 -1
  4. package/bin/example.mjs +12 -1
  5. package/bin/onboard.mjs +16 -2
  6. package/bin/start.mjs +6 -3
  7. package/build-info.json +2 -2
  8. package/docs/CLIENT-MCP.md +6 -6
  9. package/docs/DELIVERY.md +3 -3
  10. package/docs/ONBOARDING.md +1 -1
  11. package/docs/PORTAL.md +3 -3
  12. package/docs/RELEASES.md +1 -1
  13. package/docs/SECURITY.md +1 -1
  14. package/docs/architecture/04-configuration-reference.md +4 -4
  15. package/docs/architecture/05-config-governance.md +10 -10
  16. package/docs/architecture/06-operations-runbook.md +3 -3
  17. package/docs/architecture/07-quality-and-audit.md +1 -1
  18. package/docs/architecture/08-development-guide.md +2 -2
  19. package/docs/architecture/09-security-and-data.md +1 -1
  20. package/docs/decisions/0002-no-dark-functionality.md +1 -1
  21. package/docs/decisions/0006-what-the-public-repository-carries.md +3 -3
  22. package/driver/CHANGELOG.md +13 -0
  23. package/driver/ask-ledger.mjs +1 -1
  24. package/driver/band-shape.mjs +1 -1
  25. package/driver/bundled-demos.mjs +2 -2
  26. package/driver/card-budget.mjs +2 -2
  27. package/driver/case-law-ledger.mjs +2 -2
  28. package/driver/connotation-search.mjs +5 -5
  29. package/driver/contract-e3-backlog.mjs +13 -13
  30. package/driver/coverage-form.mjs +2 -2
  31. package/driver/demo-container.mjs +26 -2
  32. package/driver/disposition-tool.mjs +2 -2
  33. package/driver/engine/CONTRACT.md +3 -3
  34. package/driver/engine/mcp/gather-config.mjs +1 -1
  35. package/driver/engine/openai-agent.mjs +2 -2
  36. package/driver/findings-model.mjs +21 -6
  37. package/driver/gateway.mjs +1 -1
  38. package/driver/package.json +1 -1
  39. package/driver/pipeline-knockout.mjs +1 -1
  40. package/driver/pipeline.mjs +4 -4
  41. package/driver/placement-union.mjs +1 -1
  42. package/driver/portal-mcp-client.mjs +1 -1
  43. package/driver/portal-report.mjs +4 -1
  44. package/driver/portal-service.mjs +17 -5
  45. package/driver/predelivery-lint.mjs +9 -4
  46. package/driver/progress.mjs +37 -1
  47. package/driver/publish/render.mjs +6 -6
  48. package/driver/record-discard.mjs +1 -1
  49. package/driver/register-count.mjs +1 -1
  50. package/driver/register-digest-record.mjs +1 -1
  51. package/driver/report-card-record.mjs +2 -2
  52. package/driver/roster-verdict.mjs +2 -2
  53. package/driver/search-policy.mjs +1 -1
  54. package/driver/skeptic-record.mjs +1 -1
  55. package/driver/stages.mjs +1 -1
  56. package/driver/suite-census.json +27 -9
  57. package/driver/systemd/README.md +1 -1
  58. package/driver/unit-inventory.mjs +2 -2
  59. package/driver/verify.mjs +1 -1
  60. package/mcp-server/CHANGELOG.md +4 -0
  61. package/mcp-server/CONNECT.md +8 -8
  62. package/mcp-server/lib/runs.mjs +1 -1
  63. package/mcp-server/package.json +1 -1
  64. package/mcp-server/serve.mjs +27 -0
  65. package/package.json +1 -1
  66. package/portal-ui/package.json +1 -1
  67. package/providers/free-tier/src/capabilities.js +2 -2
  68. package/providers/jx/src/core.js +3 -3
  69. package/providers/jx/src/turn-envelope.mjs +1 -1
  70. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  71. package/providers/oauth-mcp-bridge/package.json +1 -1
  72. package/providers/perplexity/README.md +1 -1
  73. package/providers/signa/src/capabilities.js +2 -2
  74. package/providers/signa/src/core.js +2 -2
  75. package/providers/uspto-local/src/core.js +1 -1
  76. package/providers/uspto-local/src/sync.js +1 -1
  77. package/scripts/README.md +2 -6
  78. package/scripts/citation-anchor-report.mjs +1 -1
  79. package/scripts/citation-line-check.mjs +3 -3
  80. package/scripts/e2e.mjs +1 -1
  81. package/scripts/env-audit.mjs +1 -1
  82. package/scripts/env-classify.mjs +1 -1
  83. package/scripts/pack-publishable.mjs +1 -1
  84. package/scripts/release-artifact-seal.mjs +2 -2
  85. package/scripts/settings-render-check.mjs +5 -1
  86. package/scripts/strip-tracker-citations.mjs +4 -4
  87. package/scripts/test-run.mjs +46 -3
  88. package/shared/browser-temp-root.mjs +10 -3
  89. package/shared/client-door.mjs +18 -6
  90. package/shared/connect-clients.mjs +2 -0
  91. package/shared/identifier-scan.mjs +2 -2
  92. package/shared/invocation.mjs +1 -1
  93. package/shared/reference-guard-classes.mjs +5 -3
  94. package/shared/stdio-connect.mjs +39 -18
  95. package/shared/writing-standard-classes.mjs +2 -3
@@ -97,6 +97,30 @@ export function stepForStage(rawStageKey) {
97
97
  return { index, label: DISPLAY_STEPS[index], n: index + 1, total: DISPLAY_STEPS.length };
98
98
  }
99
99
 
100
+ /**
101
+ * THE STAGE A LIVE RUN IS IN NOW, for the card and the row a person watches (owner, 2026-09-19).
102
+ *
103
+ * status.json keeps two readings. The step fields hold the furthest display step ever reached, for a
104
+ * stepper that never runs backwards. `lastStage` is the stage the run ENTERED last, written at the one
105
+ * choke point every dispatch passes, corrective re-entries included. The card named the first, so a run
106
+ * sent back into synthesis by a correction pass went on reading "Case law & refutation" (measured on a
107
+ * beta, 2026-09-18). This names the stage in hand, a step back included, with its own step number.
108
+ *
109
+ * `currentStep` FIRST, where the run wrote one: the step it is in now, in the stepper's own words, moved by
110
+ * every transition in either direction and left in place through a stage with no display step. A status
111
+ * written before that field existed falls back to mapping `lastStage`, and one with neither keeps the
112
+ * furthest step, which is the best reading there is. PURE.
113
+ */
114
+ export function stageNow(status) {
115
+ const c = status?.currentStep;
116
+ if (c && typeof c.label === "string" && c.label.trim())
117
+ return { step: c.label, stepN: Number.isFinite(c.n) ? c.n : null, stepTotal: Number.isFinite(c.total) ? c.total : null };
118
+ const now = stepForStage(status?.lastStage);
119
+ return now
120
+ ? { step: now.label, stepN: now.n, stepTotal: now.total }
121
+ : { step: status?.stepLabel ?? null, stepN: status?.stepN ?? null, stepTotal: status?.stepTotal ?? null };
122
+ }
123
+
100
124
  // Lifecycle honesty (charter P1 §4): the status patch a TERMINAL delivered write must carry. Nothing runs
101
125
  // after the report: delivery is a packet and publish is code, so no
102
126
  // recordTransition ever advances the stepper past "Drafting the report" — a delivered run (with
@@ -386,6 +410,7 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
386
410
  // — the identity is the SHARED rule, not this stepper's. See identitySeed.
387
411
  ...identitySeed(),
388
412
  stepIndex: first.index, stepLabel: first.label, stepN: first.n, stepTotal: first.total,
413
+ currentStep: currentStepOf(first),
389
414
  lastStage: null,
390
415
  verdict: null,
391
416
  url: null,
@@ -407,15 +432,26 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
407
432
  // so for the whole of any of them every surface went on naming the PREVIOUS stage, which is the same
408
433
  // defect as a stale step wearing a different field. The step fields are still withheld — that part of
409
434
  // the early return was right, and an unmapped stage must never touch the displayed step.
435
+ //
436
+ // `currentStep` IS THE STEP THE RUN IS IN NOW, and it moves both ways. The step fields above keep the
437
+ // furthest step reached (writeRunStatus), so a corrective pass that re-enters synthesis after case law
438
+ // left them reading case law while `lastStage` read synthesis (measured 2026-09-18). `lastStage` is the
439
+ // raw key; this is the same moment in the stepper's own words, for a surface that shows where the run is.
440
+ // A stage with no display step leaves it where it was: those stages run inside the step already shown.
410
441
  export function recordTransition(ctx, rawStageKey) {
411
442
  const step = stepForStage(rawStageKey);
412
443
  writeRunStatus(ctx, {
413
- ...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total } : {}),
444
+ ...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total, currentStep: currentStepOf(step) } : {}),
414
445
  lastStage: rawStageKey,
415
446
  });
416
447
  rollupStatus(ctx?.run?.studioRoot);
417
448
  }
418
449
 
450
+ /** The stepper's own words for one step, as `currentStep` carries it. PURE. */
451
+ function currentStepOf(step) {
452
+ return { index: step.index, label: step.label, n: step.n, total: step.total };
453
+ }
454
+
419
455
  // ---- STATUS.md rollup --------------------------------------------------------------------------------
420
456
  // The archive lives UNDER studioRoot (studioRoot/archive/...), so a recursive walk of studioRoot finds
421
457
  // both in-flight and delivered/failed runs. Cap depth (status.json only sits at the run-dir level) and
@@ -90,7 +90,7 @@ let SENIOR_RIGHTS = new Map();
90
90
  let DISPOSITION_MODE = false;
91
91
  // — per-render flag: true when the record declares the v6 findings contract, which is the one that
92
92
  // guarantees a position on every negative and a TYPED off-field ground. Gates the grouped
93
- // reasoned-negative rendering; false ⇒ the pre- section, byte-identical, for every archived run.
93
+ // reasoned-negative rendering; false ⇒ the pre-change section, byte-identical, for every archived run.
94
94
  let NEGATIVES_GROUPED = false;
95
95
  // doc 50 — the run's FROZEN framework manifest (opts.framework, from _driver/framework.json). When
96
96
  // present the report speaks ITS band words: chips/one-liners read f.band, the gauge ticks show its
@@ -1825,7 +1825,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
1825
1825
  // too. It never did — the grouping's own note promises "each member is a <details> whose body is the
1826
1826
  // same fullDetail block the compact card carries", and until now that was untrue of the one thing v6
1827
1827
  // guarantees on every negative. Suppressing them there would take a conditional whose only job is
1828
- // reproducing the pre- shape, which is the legacy code path this program forbids.
1828
+ // reproducing the pre-change shape, which is the legacy code path this program forbids.
1829
1829
  //
1830
1830
  // Absent fields ⇒ both helpers return '' ⇒ zero bytes, and the interpolation is attached to the
1831
1831
  // template's opening so it leaves no whitespace-only line (review 2026-07-31, problem 8).
@@ -1859,7 +1859,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
1859
1859
  //
1860
1860
  // WHERE THERE IS NO PROSE THE TYPED PAIR IS STILL THE ACCOUNT, and that is most of the surfaces
1861
1861
  // fullDetail feeds: a structured-only finding (no card file), a reasoned-negative row, a compact
1862
- // card whose report-card stage produced nothing. 's promise that a negative's drawer carries its
1862
+ // card whose report-card stage produced nothing. The promise that a negative's drawer carries its
1863
1863
  // positions is untouched — a negative has no Full-detail prose to duplicate.
1864
1864
  //
1865
1865
  // manageableLine is NOT gated: the manageable category is a closed code-owned vocabulary the prose has
@@ -1988,7 +1988,7 @@ function manageableLine(f) {
1988
1988
  //
1989
1989
  // TRIM-TO-EMPTY IS LOAD-BEARING AND IS PRESERVED VERBATIM. `foldClause` returned '' for null, undefined,
1990
1990
  // '' and whitespace-only input, and that empty string is what makes `clause(f.net) || card?.meta?.one ||
1991
- // oneFallback(...)` fall through to exactly the pre- value on an archived run, which carries no
1991
+ // oneFallback(...)` fall through to exactly the pre-change value on an archived run, which carries no
1992
1992
  // `net`. Deleting the function without keeping that behaviour would move every archived card whose
1993
1993
  // `net` key exists but is blank. `clause` is `foldClause` with the budget arm removed and nothing else.
1994
1994
  const clause = (value) => String(value ?? '').trim();
@@ -2314,7 +2314,7 @@ function contextNotesBlock(notes = []) {
2314
2314
  }
2315
2315
 
2316
2316
  // charter ruling 1 (2026-07-30, name-led): the masthead depth strip LEADS with the product's registry
2317
- // name in bold — the same name 's read pills speak, never a rung on our ladder — then the coverage
2317
+ // name in bold — the same name the read pills speak, never a rung on our ladder — then the coverage
2318
2318
  // clauses (what this search covers / omits, from the run's frozen components via productCoverageNote).
2319
2319
  // The seam is the LAST " — ": a registry name may itself carry one ("Preliminary clearance — register
2320
2320
  // only") while the coverage clauses never do (they join on ";" and ","). A note with no seam at all
@@ -2323,7 +2323,7 @@ function contextNotesBlock(notes = []) {
2323
2323
  /**
2324
2324
  * THE HERO VERDICT CAPTION, folded to its first sentence.
2325
2325
  *
2326
- * 's design ruling: above any fold, only a statement, a labelled row, a count or a one-line card —
2326
+ * The design ruling: above any fold, only a statement, a labelled row, a count or a one-line card —
2327
2327
  * "prose never appears until someone opens something, and once opened nothing is ever cut". The finding
2328
2328
  * cards were moved onto that rule by `cf8dd43`; the hero caption was named in the same ruling and left
2329
2329
  * behind, and this is the bullet that PR recorded as still owed.
@@ -5,7 +5,7 @@
5
5
  //
6
6
  // ── why this exists ──────────────────────────────────────────────────────────────────────────────────
7
7
  //
8
- // The shipped trace (/) asked ONE pass to answer a question that spans two moments that never
8
+ // The shipped trace asked ONE pass to answer a question that spans two moments that never
9
9
  // coexist. A record discarded at screening is gone by the time `findings.json` is written; the findings
10
10
  // do not exist at the moment the record is discarded. Whichever end a single pass runs at, it guesses
11
11
  // about the other — and `deriveRecordCarry` ran inside `register-digest`, which is BEFORE `synthesis`
@@ -447,7 +447,7 @@ export async function countRegisterHits({
447
447
  counts,
448
448
  // — which registers these figures cover, present ONLY when one was dropped. countLine renders
449
449
  // it; the scope block below records it. Both, deliberately: `scope.deferredJurisdictions` has sat
450
- // on this artifact since it was written and NOTHING reads it, which is the same shape as 's
450
+ // on this artifact since it was written and NOTHING reads it, which is the same shape as
451
451
  // `deferred_coverage` riding the plan with no consumer and shipping a false clean. A field a
452
452
  // reader never sees is not a disclosure, so this one lands on the rendered line first.
453
453
  ...(officeScope ? { officeScope } : {}),
@@ -366,7 +366,7 @@ export function negativeMarkCell(row) {
366
366
  const MIN_JUDGED_ROWS = 1;
367
367
 
368
368
  /** A pipe row, cells escaped so a value carrying `|` cannot open a column. */
369
- const row = (cells) => `| ${cells.map((c) => str(c).replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
369
+ const row = (cells) => `| ${cells.map((c) => str(c).replace(/\\/g, "\\\\").replace(/\|/g, "\\|").replace(/\n+/g, " ") || "—").join(" | ")} |`;
370
370
  const table = (columns, rows) => [row(columns), `|${columns.map(() => "---").join("|")}|`, ...rows.map(row)].join("\n");
371
371
 
372
372
  /**
@@ -117,9 +117,9 @@ export function findingsDocFor(runDir, ordinal) {
117
117
  // the record and under what number. publish/index.mjs quotes the correct shape verbatim in a comment of
118
118
  // its own, which is what makes the divergence provable rather than a matter of taste.
119
119
  //
120
- // IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. asked for exactly that
120
+ // IT SHIPPED BECAUSE THE CONVERSION WAS NEVER CHECKED AGAINST THE ARTIFACT. The issue asked for exactly that
121
121
  // check — "recompose the bullet for every card in demo and assert byte-equality with the
122
- // delivered report.md" — and writing it is what caught this. The guard now lives beside 's, and
122
+ // delivered report.md" — and writing it is what caught this. The guard now lives beside the code it checks, and
123
123
  // this is the argument for building the guard an issue asks for even when the code already looks done.
124
124
  //
125
125
  // THE NUMBER comes off the URI's last segment: `/mark/eu/018575624` → `018575624`. That is a parse, and
@@ -18,8 +18,8 @@
18
18
  //
19
19
  // no configured store
20
20
  // → the bundled roster governs, exactly as it always did — derived by driver/bundled-demos.mjs
21
- // from the directory that ships it, never a list written down. On a test box it is CORRECT
22
- //; anywhere else it means CLEAROTRON_CUSTOMERS_DIR is not reaching the service (#83).
21
+ // from the directory that ships it, never a list written down. On a test box it is CORRECT;
22
+ // anywhere else it means CLEAROTRON_CUSTOMERS_DIR is not reaching the service.
23
23
  //
24
24
  // Pure: no fs, no env, no network. Every input is a parameter so a test can state the whole world.
25
25
 
@@ -152,7 +152,7 @@ export const COMPONENTS = {
152
152
  // so `basis` under-reports and could not carry the distinction even if we wanted it to.)
153
153
  //
154
154
  // The one mechanical set that stays load-bearing is the FLOORS — a tier-`identical` floor row, live and
155
- // in an instructed class, gets a full card in EVERY product. That is an obligation (/), not a
155
+ // in an instructed class, gets a full card in EVERY product. That is an obligation, not a
156
156
  // priority, and the ladder does not grade it.
157
157
  //
158
158
  // `graded` means: the stage is told what kind of report it is writing and grades ITS OWN written output.
@@ -51,7 +51,7 @@ export function escalatedAxes(flagsText, axes) {
51
51
  const flags = String(flagsText ?? "");
52
52
  if (!flags) return [];
53
53
  return axes.filter((a) =>
54
- new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[-]/g, "\\-")}\\b`, "i").test(flags));
54
+ new RegExp(`(^|\\n)\\s*[-*]?\\s*ESCALATE:\\s*${a.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")}\\b`, "i").test(flags));
55
55
  }
56
56
 
57
57
  /** Where the call's evidence lives — the driver's own record of what the seat handed it. */
package/driver/stages.mjs CHANGED
@@ -2154,7 +2154,7 @@ export const STAGES = {
2154
2154
  const supplementalLane = !!registerPlan?.contract?.supplemental_lane;
2155
2155
  return lines(
2156
2156
  `First, read and follow exactly: skills/clearance-register/SKILL.md (the shared spine) then skills/clearance-register/unit.md (MODE A — UNIT). Do NOT read digest.md (digest-mode judgment a unit must never run).`,
2157
- // WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE ( /).
2157
+ // WHAT THE KEY ALSO CARRIES — COMPOSED, NOT DOCTRINE.
2158
2158
  // `unit.md` used to name three tools flat, and on a deployment withholding two of them the seat was
2159
2159
  // told it holds tools its grant does not carry. The composer derives the list from the same table
2160
2160
  // that does the excluding, and returns NULL — not an empty sentence — when the provider cannot be
@@ -65,7 +65,7 @@
65
65
  },
66
66
  "a-browser-run-gets-its-own-temp-root.test.mjs": {
67
67
  "tests": 12,
68
- "asserts": 29,
68
+ "asserts": 31,
69
69
  "skips": 0,
70
70
  "todos": 0
71
71
  },
@@ -195,9 +195,15 @@
195
195
  "skips": 0,
196
196
  "todos": 0
197
197
  },
198
+ "a-conditional-with-nothing-stated-prints-no-conditional-line.test.mjs": {
199
+ "tests": 2,
200
+ "asserts": 6,
201
+ "skips": 0,
202
+ "todos": 0
203
+ },
198
204
  "a-connect-line-says-where-it-runs.test.mjs": {
199
205
  "tests": 11,
200
- "asserts": 48,
206
+ "asserts": 56,
201
207
  "skips": 0,
202
208
  "todos": 0
203
209
  },
@@ -255,6 +261,12 @@
255
261
  "skips": 0,
256
262
  "todos": 0
257
263
  },
264
+ "a-demo-leaves-no-temp-copies.test.mjs": {
265
+ "tests": 2,
266
+ "asserts": 6,
267
+ "skips": 0,
268
+ "todos": 0
269
+ },
258
270
  "a-demo-report-declares-itself.test.mjs": {
259
271
  "tests": 5,
260
272
  "asserts": 15,
@@ -1528,8 +1540,8 @@
1528
1540
  "todos": 0
1529
1541
  },
1530
1542
  "client-door.test.mjs": {
1531
- "tests": 38,
1532
- "asserts": 163,
1543
+ "tests": 39,
1544
+ "asserts": 168,
1533
1545
  "skips": 1,
1534
1546
  "todos": 0
1535
1547
  },
@@ -2537,7 +2549,7 @@
2537
2549
  },
2538
2550
  "findings-model.test.mjs": {
2539
2551
  "tests": 68,
2540
- "asserts": 257,
2552
+ "asserts": 258,
2541
2553
  "skips": 0,
2542
2554
  "todos": 0
2543
2555
  },
@@ -3028,8 +3040,8 @@
3028
3040
  "todos": 0
3029
3041
  },
3030
3042
  "no-test-writes-inside-the-checkout.test.mjs": {
3031
- "tests": 20,
3032
- "asserts": 52,
3043
+ "tests": 21,
3044
+ "asserts": 58,
3033
3045
  "skips": 0,
3034
3046
  "todos": 0
3035
3047
  },
@@ -3364,8 +3376,8 @@
3364
3376
  "todos": 0
3365
3377
  },
3366
3378
  "portal-service.test.mjs": {
3367
- "tests": 130,
3368
- "asserts": 701,
3379
+ "tests": 131,
3380
+ "asserts": 712,
3369
3381
  "skips": 1,
3370
3382
  "todos": 0
3371
3383
  },
@@ -5373,6 +5385,12 @@
5373
5385
  "skips": 0,
5374
5386
  "todos": 0
5375
5387
  },
5388
+ "the-status-says-which-step-the-run-is-in-now.test.mjs": {
5389
+ "tests": 3,
5390
+ "asserts": 8,
5391
+ "skips": 0,
5392
+ "todos": 0
5393
+ },
5376
5394
  "the-summary-carries-structure.test.mjs": {
5377
5395
  "tests": 14,
5378
5396
  "asserts": 37,
@@ -33,7 +33,7 @@ missing one and writes nothing, because a unit with one placeholder left starts
33
33
  worse than one that does not start. Which units need it is **derived from the files**, not from a list —
34
34
  and `driver/unit-inventory.mjs` declares them, with a test asserting the two agree in both directions.
35
35
 
36
- These are for a **server, not a laptop**. On a workstation there is nothing to install: `npx clearotron start` supervises the
36
+ These are for a **server, not a laptop**. On a workstation there is nothing to install: `clearotron start` supervises the
37
37
  same loop for you, and`node driver/runner.mjs --watch` is what to run by hand if you started with
38
38
  `--no-worker` (`INSTALL.md` §5) — one foreground process that polls for queued jobs and for parked runs whose
39
39
  window has elapsed.
@@ -45,8 +45,8 @@
45
45
  // docs/architecture/05-config-governance.md, tier 2). Getting that wrong replaces working auth with
46
46
  // placeholders that look configured, and it is not work to do beside a running round.
47
47
  //
48
- // So the untracked units are declared as untracked, WITH THE REASON, which is exactly what 's
49
- // acceptance asks for: "either has a tracked file it is compared against, or is named here with the
48
+ // So the untracked units are declared as untracked, WITH THE REASON, which is exactly what the
49
+ // requirement asks for: "either has a tracked file it is compared against, or is named here with the
50
50
  // reason it does not".
51
51
  //
52
52
  // ── THE CORRECTION THIS FILE NEEDED ITSELF ───────────────────────────────────────────────────────
package/driver/verify.mjs CHANGED
@@ -1375,7 +1375,7 @@ function connotationViolations(content, queryCount, opts = {}, dispositionsPath
1375
1375
  // patch — two families emitting a bare `form_damaged` would be indistinguishable there and the repair
1376
1376
  // would rewrite the wrong form.
1377
1377
  //
1378
- // THE SHAPE IS BUILT AROUND THREE CONSUMERS, each of which fails SILENTLY on a mismatch (/):
1378
+ // THE SHAPE IS BUILT AROUND THREE CONSUMERS, each of which fails SILENTLY on a mismatch:
1379
1379
  // - The CAUSE CENSUS IS FRONT-LOADED, `no_ruling=<n>`, before any named list: repairs.mjs CENSUS_RE
1380
1380
  // sums exactly that, pipeline.mjs shows the failing model `tok.slice(0, 160)`, and a census that fell
1381
1381
  // off the end of a slice would read as a missing quantity — i.e. as converged.
@@ -1,5 +1,9 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.3.2-beta.13
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.2-beta.12
4
8
 
5
9
  No changes in this release.
@@ -5,7 +5,7 @@ MCP-capable app. Two ways in, and they are not variants of each other:
5
5
 
6
6
  | | **Local** | **Hosted** |
7
7
  |---|---|---|
8
- | What it is | your app spawns `server.mjs` from your own clone | you were given a URL by whoever operates the engine |
8
+ | What it is | your app spawns `serve.mjs` from your own clone | you were given a URL by whoever operates the engine |
9
9
  | Who it is for | anyone who cloned this repository | a customer or colleague reading someone else's runs |
10
10
  | Setup | copy-paste below, no account, no credential | paste the URL, sign in |
11
11
  | Reach | everything, including the write verbs | read-only, scoped to your own matters |
@@ -46,7 +46,7 @@ One command. Substitute your clone's path and your workspace root:
46
46
  ```sh
47
47
  claude mcp add trademark-artifacts --scope user \
48
48
  -e CLEAROTRON_WORK_DIR=/path/to/your/workspace \
49
- -- node /path/to/clearotron/mcp-server/server.mjs
49
+ -- node /path/to/clearotron/mcp-server/serve.mjs
50
50
  ```
51
51
 
52
52
  Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
@@ -64,7 +64,7 @@ PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Rem
64
64
  "mcpServers": {
65
65
  "trademark-artifacts": {
66
66
  "command": "node",
67
- "args": ["/path/to/clearotron/mcp-server/server.mjs"],
67
+ "args": ["/path/to/clearotron/mcp-server/serve.mjs"],
68
68
  "env": { "CLEAROTRON_WORK_DIR": "/path/to/your/workspace" }
69
69
  }
70
70
  }
@@ -82,7 +82,7 @@ Codex reads `~/.codex/config.toml`:
82
82
  ```toml
83
83
  [mcp_servers.trademark-artifacts]
84
84
  command = "node"
85
- args = ["/path/to/clearotron/mcp-server/server.mjs"]
85
+ args = ["/path/to/clearotron/mcp-server/serve.mjs"]
86
86
  env = { CLEAROTRON_WORK_DIR = "/path/to/your/workspace" }
87
87
  ```
88
88
 
@@ -94,13 +94,13 @@ is a value written to a file.
94
94
 
95
95
  ## Any other MCP host
96
96
 
97
- The contract is the same three things every time: run `node mcp-server/server.mjs`, over stdio, with
97
+ The contract is the same three things every time: run `node mcp-server/serve.mjs`, over stdio, with
98
98
  `CLEAROTRON_WORK_DIR` in its environment.
99
99
 
100
100
  ```json
101
101
  {
102
102
  "command": "node",
103
- "args": ["/path/to/clearotron/mcp-server/server.mjs"],
103
+ "args": ["/path/to/clearotron/mcp-server/serve.mjs"],
104
104
  "env": { "CLEAROTRON_WORK_DIR": "…", "CLEAROTRON_REPORTS_DIR": "…" }
105
105
  }
106
106
  ```
@@ -125,7 +125,7 @@ read them. Spawn the server as that user:
125
125
  ```json
126
126
  {
127
127
  "command": "sudo",
128
- "args": ["-u", "<operator>", "node", "/path/to/clearotron/mcp-server/server.mjs"]
128
+ "args": ["-u", "<operator>", "node", "/path/to/clearotron/mcp-server/serve.mjs"]
129
129
  }
130
130
  ```
131
131
 
@@ -133,7 +133,7 @@ That needs a one-time NOPASSWD rule, or the stdio handshake hangs on a password
133
133
 
134
134
  ```
135
135
  # /etc/sudoers.d/trademark-artifacts-mcp (chmod 0440)
136
- <caller> ALL=(<operator>) NOPASSWD: /usr/bin/node /path/to/clearotron/mcp-server/server.mjs
136
+ <caller> ALL=(<operator>) NOPASSWD: /usr/bin/node /path/to/clearotron/mcp-server/serve.mjs
137
137
  ```
138
138
 
139
139
  ## What to ask it
@@ -40,7 +40,7 @@ export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet })
40
40
  if (workSet || workExists || poolSet) return null;
41
41
  return `no searches can be read here: CLEAROTRON_WORK_DIR is unset and ${workRoot} does not exist, `
42
42
  + "and CLEAROTRON_REPORTS_DIR is unset. Set them to the install's directories — `npx clearotron doctor` "
43
- + "prints where an install keeps them.";
43
+ + "prints where an install keeps them, and mcp-server/CONNECT.md says how to connect this server.";
44
44
  }
45
45
 
46
46
  // Every workspace-<agent>/studio/clearance-search root under the live workspace root.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.2-beta.12",
3
+ "version": "0.3.2-beta.13",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
@@ -0,0 +1,27 @@
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
+ //
5
+ // serve.mjs — the MCP server over stdio, with the Node check in front of it. The connect lines name
6
+ // this file.
7
+ //
8
+ // server.mjs cannot check the Node it runs on. An ES module's imports are all loaded before any of its
9
+ // code runs, so on a Node below the floor the server fails while loading, with an error that names a
10
+ // module and says nothing about the version. Measured on Node 18.20.8: "SyntaxError: The requested
11
+ // module 'node:util' does not provide an export named 'parseEnv'". On a Windows laptop running the
12
+ // connect line through WSL, the distribution's own Node 18 met "SyntaxError: Unexpected token 'with'"
13
+ // (2026-09-19). This file imports only what an old Node can load, refuses in one plain line, and only
14
+ // then loads the server.
15
+ import { fileURLToPath } from "node:url";
16
+ import { nodeFloorVerdict, nodeFloorRefusal } from "../shared/node-floor.mjs"; // one floor, read from package.json
17
+
18
+ const floor = nodeFloorVerdict();
19
+ if (!floor.ok) {
20
+ console.error(`clearotron: ${nodeFloorRefusal(floor)}`);
21
+ process.exit(1);
22
+ }
23
+
24
+ // THE SERVER STARTS AS THE ENTRY IT WOULD HAVE BEEN. It starts its stdio transport, and reads the
25
+ // install's settings, only when it is the process's entry file, so the entry is handed over first.
26
+ process.argv[1] = fileURLToPath(new URL("./server.mjs", import.meta.url));
27
+ await import("./server.mjs");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.2-beta.12",
4
+ "version": "0.3.2-beta.13",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.12",
5
+ "version": "0.3.2-beta.13",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -48,9 +48,9 @@
48
48
  // The question that DOES depend on the box — which offices this deployment can reach right now — is
49
49
  // answered in driver/register-availability.mjs, and the answer rides the plan as a disclosed
50
50
  // `deferred_coverage` row. It is deliberately not answered here, and `covered` is deliberately not
51
- // narrowed there either: 's admission gate reads this field to decide which territories a client may
51
+ // narrowed there either: the admission gate reads this field to decide which territories a client may
52
52
  // ORDER, so narrowing it to the configured half would refuse a US-only matter at the door instead of
53
- // disclosing its US gap ('s ruling is that such a matter must START and disclose).
53
+ // disclosing its US gap (the rule is that such a matter must START and disclose).
54
54
  //
55
55
  // ── THE "NO SHAPE FOR HALF OF THIS RAN" RULE IS UNCHANGED — IT MOVED THE SPLIT, NOT REPEALED IT ──────
56
56
  //
@@ -13,10 +13,10 @@
13
13
  // SEARCH MACHINERY (query terms), never registry facts — the never-invent rule binds registry DATA,
14
14
  // not the queries we choose to run.
15
15
  //
16
- // BILLING RIDES THE RUN, NOT THIS FILE (/ at e49868e3, and the transport deleted at).
16
+ // BILLING RIDES THE RUN, NOT THIS FILE.
17
17
  // These lanes used to POST to the Anthropic Messages API on ANTHROPIC_API_KEY at a hardcoded haiku
18
- // tier while the rest of the run used whatever program the customer chose — the mix the owner's D6
19
- // ruling forbids. They now go through `engine.runTurn()` like every other model call, so whatever
18
+ // tier while the rest of the run used whatever program the customer chose — the mix the product's one-provider-per-run
19
+ // rule forbids. They now go through `engine.runTurn()` like every other model call, so whatever
20
20
  // program and billing mode the run is on carries them, and nothing here selects a vendor.
21
21
  //
22
22
  // `callMessagesAPI` — with its own MESSAGES_API_URL, x-api-key header and retry ladder — is DELETED
@@ -3,7 +3,7 @@
3
3
  // turn-envelope.mjs — render a forced-tool request as a CLI-turn prompt, and read the turn's text back
4
4
  // into the envelope the three lane parsers already understand. PURE. No driver import, no network.
5
5
  //
6
- // ── why this exists ( /) ─────────────────────────────────────────────────────────────────
6
+ // ── why this exists ─────────────────────────────────────────────────────────────────
7
7
  //
8
8
  // The jx lanes called the Anthropic Messages API directly with `tool_choice: {type:"tool"}`, on a key,
9
9
  // at a hardcoded tier — regardless of which AI program the customer configured. The owner's standing
@@ -1,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.2-beta.13
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.3.2-beta.12
4
8
 
5
9
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.2-beta.12",
3
+ "version": "0.3.2-beta.13",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -11,7 +11,7 @@ optional: without `PERPLEXITY_API_KEY` the run door refuses by name
11
11
  (`preflightResearchCredential`, `../../driver/driver.config.mjs`) rather than searching less. A
12
12
  KNOCKOUT is the exception and it is deliberate — its register half is a whole product without the
13
13
  sweep, so `../../driver/pipeline-knockout.mjs` SKIPS this adapter and discloses the half it did not
14
- run ( acceptance 6). Nothing is billed for a sweep that never runs.
14
+ run. Nothing is billed for a sweep that never runs.
15
15
  (`CLEAROTRON_KNOCKOUT_SWEEP_FIXTURES` is the $0 dev route to a knockout WITH a sweep.)
16
16
 
17
17
  The header's `index.js` and `build.js` belong to the plugin packaging this core was extracted from;
@@ -20,7 +20,7 @@
20
20
  // * predicates.wildcardInfix — `contains` would appear to serve it and does not. The kernel hands
21
21
  // the infix case its RAW pattern with the asterisks still in it, so the sweep would search the
22
22
  // punctuation. Declaring it would be declaring a capability the executor cannot serve, which is
23
- // the one thing 's criteria forbid. Stays null; the slice defers, disclosed.
23
+ // the one thing a declaration must never do. Stays null; the slice defers, disclosed.
24
24
  // * resultCeiling — there is no single number to put here, and the reason is worth the paragraph.
25
25
  // An unscoped term pages to exhaustion whatever its size, on every predicate — no ceiling. But add
26
26
  // `filters.owner_name` and the SAME term stops dead at the owner-scoped window (400), which the
@@ -194,7 +194,7 @@ export const CAPABILITIES = Object.freeze({
194
194
 
195
195
  // ── WHICH BINDING LAYERS DOES A SEARCH SCOPED TO THIS OFFICE ACTUALLY RETURN? ──────────
196
196
  //
197
- // 's first task, answered for this provider by driving the three scopings against each other rather
197
+ // The first question for every provider, answered here by driving the three scopings against each other rather
198
198
  // than by reading the documentation. One term, one limit, France:
199
199
  //
200
200
  // filters.offices the national register ALONE
@@ -680,8 +680,8 @@ export function toSignaParams(p = {}) {
680
680
  // and this branch is the only thing standing between that and `strategies: ["exact"]`. With
681
681
  // `predicates.default` declared `"contains"` and no mapping here, every unanchored slice would
682
682
  // have gone to the wire as an EXACT search: narrower than the plan asked for, returning fewer
683
- // rows, and answering as though it were the query requested. That is 's defect exactly, and
684
- // declaring a predicate without wiring it is the one thing this issue's criteria forbid.
683
+ // rows, and answering as though it were the query requested. That is the silent narrowing this branch
684
+ // prevents, and declaring a predicate without wiring it is the one thing a declaration must never do.
685
685
  //
686
686
  // A `*` in the term means this is the infix-wildcard case, which shares the empty {} and which
687
687
  // `contains` cannot serve — the kernel hands the RAW pattern, asterisks included, so a contains
@@ -67,7 +67,7 @@ function resolveDbPath(auth) {
67
67
  // marker added by one consumer is a marker the other consumers do not get. Marking at the throw
68
68
  // means every path out of this provider carries it, including ones written later.
69
69
  //
70
- // 's composite backstop caught a THROW and converted it. That backstop never fired against this
70
+ // The composite backstop caught a THROW and converted it. That backstop never fired against this
71
71
  // provider, because four of its five entry points catch their own throw and return a plain error
72
72
  // first — and the test that claimed it worked drove a stub that throws, which this does not do.
73
73
  throw new Error(
@@ -312,7 +312,7 @@ export async function syncIndex({ dbPath, files, ingest = ingestFile, onFile = n
312
312
  await onPhase?.("ingest");
313
313
  // BLOCKS THE EVENT LOOP FOR THE WHOLE REBUILD — node:sqlite is synchronous, and this is one
314
314
  // transaction over every row in the index. Nothing on this thread runs again until it commits,
315
- // which is why 's disk sampler is a worker thread rather than a timer.
315
+ // which is why the disk sampler is a worker thread rather than a timer.
316
316
  rebuildFts(db);
317
317
  await onPhase?.("fts");
318
318
  const rows = db.prepare("SELECT count(*) AS n FROM mark").get().n;
package/scripts/README.md CHANGED
@@ -52,14 +52,10 @@ than its contents, and the whole reason the browser checks exist is that a fix f
52
52
  scrollbars" shipped a report with two scrollbars past 1,500 passing tests. Those checks run in CI's
53
53
  `build-and-verify` job and nowhere else — so **CI, not your machine, is what covers them.**
54
54
 
55
- `render-check.mjs` is a further step out, running in **neither** — and wiring it up is what found out
56
- why that mattered. CI left it out because it measures a PUBLISHED RUN and had no pool to point at;
57
- `--fixture-pool` removed that reason, so wired it into`build-and-verify` as a blocking step.
58
-
59
- **Its first run anywhere reported 6 of 9 assertions failing with `no-probe` and `null`** — not a layout
55
+ **`render-check.mjs`'s first run anywhere reported 6 of 9 assertions failing with `no-probe` and `null`** — not a layout
60
56
  defect, an inability to *measure*. The three assertions that look inside the report frame (the height
61
57
  bridge, the scrollbar loop, the sideways overflow) depend on a probe script in the sandboxed iframe
62
- posting back to the shell. The pool built fine; the harness cannot see inside its own frame. **.**
58
+ posting back to the shell. The pool built fine; the harness cannot see inside its own frame.
63
59
 
64
60
  **The first explanation written down here was wrong, and how it was wrong is the useful part.** This
65
61
  paragraph used to say the message never arrives under `file://` with production's sandbox. Measured in