clearotron 0.4.0 → 0.4.1-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.
Files changed (132) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/bin/README.md +1 -1
  3. package/bin/clearotron.mjs +2 -0
  4. package/bin/mcp.mjs +80 -0
  5. package/bin/start.mjs +14 -1
  6. package/build-info.json +2 -2
  7. package/demo/README.md +1 -1
  8. package/docs/SECURITY-OWASP.md +21 -13
  9. package/docs/SECURITY.md +20 -14
  10. package/docs/architecture/04-configuration-reference.md +3 -3
  11. package/docs/architecture/05-config-governance.md +1 -1
  12. package/docs/architecture/06-operations-runbook.md +5 -4
  13. package/docs/architecture/08-development-guide.md +3 -1
  14. package/docs/architecture/09-security-and-data.md +16 -11
  15. package/driver/CHANGELOG.md +29 -0
  16. package/driver/binding-layers.mjs +28 -6
  17. package/driver/citation-census.json +3 -3
  18. package/driver/common-law-coverage-status.mjs +52 -0
  19. package/driver/common-law-receipts.mjs +1 -1
  20. package/driver/compare.mjs +1 -1
  21. package/driver/connotation-search.mjs +5 -5
  22. package/driver/contract-audit.mjs +6 -6
  23. package/driver/contract-dictation-registry.mjs +1 -1
  24. package/driver/contract-e3-backlog.mjs +18 -17
  25. package/driver/contract-vocabulary.mjs +145 -132
  26. package/driver/declination-call.mjs +5 -1
  27. package/driver/degraded-parts.mjs +10 -3
  28. package/driver/dev-portal.mjs +2 -1
  29. package/driver/dispatch-record.mjs +19 -0
  30. package/driver/doubt-closure-tool.mjs +2 -2
  31. package/driver/driver.config.mjs +39 -6
  32. package/driver/engagement-receipt.mjs +1 -1
  33. package/driver/engine/CONTRACT.md +41 -1
  34. package/driver/engine/anthropic-agent.mjs +56 -4
  35. package/driver/engine/mcp/codex-config.mjs +8 -3
  36. package/driver/engine/mcp/coverage-server.mjs +1 -1
  37. package/driver/engine/mcp/gather-config.mjs +1 -1
  38. package/driver/engine/mcp/recording-server.mjs +32 -5
  39. package/driver/engine/mcp/stdio-server.mjs +45 -7
  40. package/driver/engine/mcp/supplemental.mjs +26 -1
  41. package/driver/engine/openai-agent.mjs +113 -20
  42. package/driver/engine/session-record.mjs +223 -0
  43. package/driver/enqueue-schema.mjs +2 -2
  44. package/driver/findings-model.mjs +4 -4
  45. package/driver/flag-snapshot.mjs +2 -1
  46. package/driver/form-neighbourhood.mjs +7 -7
  47. package/driver/frame-diff-model.mjs +3 -3
  48. package/driver/framework.mjs +22 -0
  49. package/driver/gateway.mjs +96 -14
  50. package/driver/jx.mjs +1 -1
  51. package/driver/knockout-review-record.mjs +55 -1
  52. package/driver/named-band.mjs +4 -0
  53. package/driver/owner-screen.mjs +7 -4
  54. package/driver/package.json +1 -1
  55. package/driver/phase0.mjs +13 -2
  56. package/driver/pipeline-knockout.mjs +15 -13
  57. package/driver/pipeline.mjs +132 -48
  58. package/driver/portal-families.mjs +3 -3
  59. package/driver/portal-service.mjs +6 -5
  60. package/driver/predelivery-lint.mjs +19 -19
  61. package/driver/products.mjs +1 -1
  62. package/driver/profiles.mjs +4 -3
  63. package/driver/publish/index.mjs +1 -1
  64. package/driver/publish/knockout.mjs +5 -4
  65. package/driver/publish/office-record-links.mjs +17 -2
  66. package/driver/publish/publish-inputs.mjs +3 -3
  67. package/driver/publish/render-knockout.mjs +17 -5
  68. package/driver/publish/render.mjs +4 -2
  69. package/driver/publish/xlsx.mjs +1 -1
  70. package/driver/record-fold.mjs +177 -0
  71. package/driver/register-count.mjs +20 -3
  72. package/driver/register-coverage.mjs +1 -1
  73. package/driver/register-plan.mjs +83 -18
  74. package/driver/register-records.mjs +7 -0
  75. package/driver/registry-fidelity.mjs +2 -2
  76. package/driver/repairs.mjs +1 -1
  77. package/driver/report-card-record.mjs +4 -4
  78. package/driver/result-noun-fields.mjs +3 -3
  79. package/driver/scope-facts.mjs +11 -3
  80. package/driver/scope-rules.mjs +2 -2
  81. package/driver/search-policy.mjs +20 -1
  82. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  83. package/driver/skills/clearance-register/digest.md +3 -3
  84. package/driver/skills/clearance-register/register-recipes.md +11 -11
  85. package/driver/skills/clearance-register/status-rules.md +1 -1
  86. package/driver/skills/clearance-register/unit.md +1 -1
  87. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  88. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  89. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  90. package/driver/skills/clearance-search/worked-examples.md +39 -39
  91. package/driver/skills/clearance-variants/SKILL.md +16 -16
  92. package/driver/skills/frame-diff/SKILL.md +2 -2
  93. package/driver/stage-context.mjs +36 -1
  94. package/driver/stages-knockout.mjs +6 -5
  95. package/driver/stages.mjs +30 -30
  96. package/driver/suite-census.json +259 -55
  97. package/driver/synthesis-record.mjs +5 -0
  98. package/driver/unit-inventory.mjs +17 -28
  99. package/driver/verify.mjs +7 -7
  100. package/mcp-server/CHANGELOG.md +8 -0
  101. package/mcp-server/CONNECT.md +9 -3
  102. package/mcp-server/demo-mode.mjs +10 -0
  103. package/mcp-server/lib/evidence.mjs +8 -8
  104. package/mcp-server/lib/knockout.mjs +1 -1
  105. package/mcp-server/lib/runs.mjs +7 -1
  106. package/mcp-server/lib/scrub.mjs +1 -1
  107. package/mcp-server/package.json +2 -2
  108. package/mcp-server/packs/README.md +1 -1
  109. package/mcp-server/server.mjs +16 -2
  110. package/package.json +4 -3
  111. package/portal-ui/package.json +3 -3
  112. package/providers/_shared/count.mjs +9 -2
  113. package/providers/_shared/enumerate.mjs +125 -41
  114. package/providers/_shared/execute-plan.mjs +15 -4
  115. package/providers/corsearch/src/capabilities.js +1 -1
  116. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  117. package/providers/oauth-mcp-bridge/package.json +2 -2
  118. package/providers/signa/src/capabilities.js +32 -2
  119. package/providers/signa/src/core.js +262 -36
  120. package/scripts/band-shape-probe.mjs +2 -2
  121. package/scripts/deprecate-below.mjs +28 -4
  122. package/scripts/e2e-first-time.mjs +17 -2
  123. package/scripts/env-audit.mjs +3 -2
  124. package/scripts/freeze-example-run.mjs +10 -10
  125. package/scripts/mcp-registry-listing.mjs +199 -0
  126. package/scripts/mint-plugin-folder.mjs +65 -0
  127. package/scripts/release-version.mjs +44 -0
  128. package/scripts/score.mjs +1 -1
  129. package/shared/register-floor.mjs +19 -0
  130. package/shared/scope.mjs +18 -2
  131. package/shared/trigger-lane.mjs +1 -1
  132. package/skills/clearotron-ops/SKILL.md +81 -24
@@ -52,6 +52,7 @@ import { findCoverageRecommendations } from "./verify.mjs";
52
52
  import { declinationCallPaths, readDeclinations } from "./declination-tool.mjs"; // — the seat's own declines, read by the driver never asserted by the seat
53
53
  import { reconcileDeclinationDuty, declinationDutyRefusal } from "./declination-duty.mjs";
54
54
  import { findingUris } from "./record-carry.mjs"; // one derivation of "which records did the findings name", called not copied
55
+ import { foldAfterSave } from "./record-fold.mjs"; // once the delivery seam is passed, the saved record is folded again
55
56
 
56
57
  export const NARRATIVE_FILE = "narrative.md";
57
58
  export const FINDINGS_FILE = "findings.json";
@@ -670,6 +671,10 @@ export function recordSynthesis(runDir, received, opts = {}) {
670
671
  captured: closeCapture({ ok: true }), capture_failed: captureFailed,
671
672
  };
672
673
  }
674
+ // ONCE THE DELIVERY SEAM IS PASSED, THE RECORD IS FOLDED AGAIN ON ACCEPTANCE (design, 2026-10-03). The
675
+ // call records above stay the model's own, the base for its next patch; findings.json becomes the folded
676
+ // record the reviewer, the cards and the delivery read. Before the seam this leaves the record as written.
677
+ foldAfterSave(dir0, findingsAt);
673
678
 
674
679
  return {
675
680
  written: [findingsAt, narrativeAt], refused: null,
@@ -159,7 +159,7 @@ export const UNIT_INVENTORY = Object.freeze([
159
159
  // stamped and says where its verdict lives, and driver/updater-identity.mjs makes that verdict from
160
160
  // the digests the updater records about itself.
161
161
  attribution: "stamp",
162
- measured: "2026-09-08, read on the test deployment: the timer and its service are installed, the timer is enabled and ACTIVE, and it last fired within the hour — `systemctl --user list-timers`. The service's own state says nothing about this: it is `inactive` between runs and `inactive` when the timer has been stopped. The timer's ActiveState says the timer itself is still up. It is NOT a next elapse and does not promise one: `active` means UP, never SCHEDULED. Proving something will still fire takes a real next elapse, which `systemctl list-timers --all` supplies and `show -p ActiveState` cannot. Measured in testing 2026-09-09: a timer reading `ActiveState=active` whose `OnCalendar` names a date already past, so `list-timers` reports its next fire as null and it will never run again.",
162
+ measured: "2026-09-08. The service's own state says nothing about the timer: it is `inactive` between runs and `inactive` when the timer has been stopped. The timer's ActiveState says the timer itself is still up. It is NOT a next elapse and does not promise one: `active` means UP, never SCHEDULED. Proving something will still fire takes a real next elapse, which `systemctl list-timers --all` supplies and `show -p ActiveState` cannot. Measured in testing 2026-09-09: a timer reading `ActiveState=active` whose `OnCalendar` names a date already past, so `list-timers` reports its next fire as null and it will never run again.",
163
163
  tracked: ["clearotron-deploy.service", "clearotron-deploy.timer"],
164
164
  note: "the install's own updater.",
165
165
  // IT WAS AN ORPHAN, AND THE CONDITION IT NAMED HAS BEEN MET. This entry carried an `orphanReason`
@@ -212,8 +212,7 @@ export const UNIT_INVENTORY = Object.freeze([
212
212
  // prose references in driver/systemd/render-units.mjs also describe this file as though it were in
213
213
  // the tree; they are stale today either way.
214
214
  unit: "profile-service", runsOn: [],
215
- measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only; "
216
- + "no unit contents were read.",
215
+ measured: "2026-09-08",
217
216
  tracked: null,
218
217
  untrackedReason: "the repository has never carried this file — `git log --all` on the path is empty. "
219
218
  + "It was described as deploying from a copy held on the deployment itself, in the same "
@@ -243,11 +242,8 @@ export const UNIT_INVENTORY = Object.freeze([
243
242
  // REPORTED, never a fault, for exactly this case. It becomes `["prod"]` in the change that DEPLOYS
244
243
  // it, against a fresh enumeration, which is the ratchet working rather than being edited around.
245
244
  unit: "clearotron-portal", runsOn: ["test"], tracked: ["clearotron-portal.service"],
246
- measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
247
- note: "NEW, SHIPPED BUT NOT YET DEPLOYED. The portal had NO unit at all — it "
248
- + "and the MCP face are the two children bin/start.mjs supervises, and neither ran under systemd, "
249
- + "so a hosted deployment kept them alive with a hand launcher and `clearotron start` had nothing "
250
- + "to refuse in favour of.",
245
+ measured: "2026-09-07",
246
+ note: "The portal had NO unit at all — it and the MCP face are the two children bin/start.mjs supervises, and neither ran under systemd.",
251
247
  orphanReason: "THE WEB PORTAL, SHIPPED WITH NO BOX CARRYING IT YET — the same KIND of orphan as "
252
248
  + "clearotron-deploy (never installed), a different unit and a different install path. It is not "
253
249
  + "courtlistener-mcp, which runs on production under another name, and not feedback-mint, which "
@@ -260,9 +256,8 @@ export const UNIT_INVENTORY = Object.freeze([
260
256
  // Same as clearotron-portal above: shipped, running nowhere, declared as an orphan rather than
261
257
  // asserting a production state no enumeration has ever shown.
262
258
  unit: "clearotron-mcp-face", runsOn: ["test"], tracked: ["clearotron-mcp-face.service"],
263
- measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
264
- note: "NEW, SHIPPED BUT NOT YET DEPLOYED. The engine door the portal calls over "
265
- + "MCP; the other of the two processes that had no unit.",
259
+ measured: "2026-09-07",
260
+ note: "The engine door the portal calls over MCP; the other of the two processes that had no unit.",
266
261
  orphanReason: "THE ENGINE DOOR THE PORTAL CALLS, shipped with no box carrying it yet. Same kind as "
267
262
  + "clearotron-portal beside it and as clearotron-deploy — never installed — and distinct from "
268
263
  + "both other kinds in this list. It arrives with the PRODUCTION REBUILD and then `systemctl "
@@ -289,7 +284,7 @@ export const UNIT_INVENTORY = Object.freeze([
289
284
  // door beside it: it is in SERVER_INSTALL_SET, and it leaves this list on the day a measured
290
285
  // enumeration of a box shows it running.
291
286
  unit: "clearotron-client-mcp", runsOn: ["test"], tracked: ["clearotron-client-mcp.service"],
292
- measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
287
+ measured: "2026-09-07",
293
288
  note: "INSTALLED WITH THE PRODUCT since 2026-09-03. The client door "
294
289
  + "— the one surface that accepts an account-scoped key, and a separate process from the engine "
295
290
  + "door, which refuses one outright.",
@@ -307,9 +302,8 @@ export const UNIT_INVENTORY = Object.freeze([
307
302
  // Written as part of retiring the path-watcher posture, because retiring `prelim-driver.*` without
308
303
  // it leaves a hosted box healthy-looking and draining nothing.
309
304
  unit: "clearotron-worker", runsOn: ["test"], tracked: ["clearotron-worker.service"],
310
- measured: "2026-09-07, read on the test deployment with a working user bus: ActiveState=active, UnitFileState=enabled, its fragment under the account's own unit directory.",
311
- note: "NEW. The drain, as one plain service unit invoking the entrypoint "
312
- + "directly (owner ruling 2026-08-31). Ships tracked and runs nowhere yet.",
305
+ measured: "2026-09-07",
306
+ note: "The drain, as one plain service unit invoking the entrypoint directly.",
313
307
  orphanReason: "AWAITING A BOX, not an orphan by design. It is the hosted posture's drainer, and the "
314
308
  + "hosted posture is an opt-in step no install performs. It must NOT be "
315
309
  + "started beside `clearotron start`, which supervises its own worker — the two postures are "
@@ -320,7 +314,7 @@ export const UNIT_INVENTORY = Object.freeze([
320
314
  orphanReason: "THE PRE-RENAME PORTAL. Production runs `clearotron-portal`; this name is "
321
315
  + "what that service used to be called, and the 2026-09-08 reading finds no unit of this name under "
322
316
  + "any account. It ran on production until the rename and the entry was never re-measured after it.",
323
- measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
317
+ measured: "2026-09-08", tracked: null,
324
318
  untrackedReason: "the deployed copy carries real Cloudflare Access team/AUD/domain values inline. "
325
319
  + "A tracked file would be the placeholder TEMPLATE, merged by hand after a "
326
320
  + "diff — writing one carelessly replaces working auth with placeholders that look configured.",
@@ -335,7 +329,7 @@ export const UNIT_INVENTORY = Object.freeze([
335
329
  orphanReason: "THE PRE-RENAME OPERATOR DOOR. Production runs `clearotron-mcp-face`; the "
336
330
  + "2026-09-08 reading finds no unit of this name under any account. Same rename as the portal it "
337
331
  + "serves, and the same claim carried across it unmeasured.",
338
- measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: null,
332
+ measured: "2026-09-08", tracked: null,
339
333
  untrackedReason: "same CF Access inline-values shape as the portal it serves.",
340
334
  },
341
335
  {
@@ -343,7 +337,7 @@ export const UNIT_INVENTORY = Object.freeze([
343
337
  orphanReason: "THE PRE-RENAME CLIENT DOOR. `clearotron-client-mcp` runs on both the production and "
344
338
  + "the test deployments; the 2026-09-08 reading finds no unit of THIS name under any account. The "
345
339
  + "tracked template stays because the repository still ships it — see the note.",
346
- measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only. This line previously carried a 2026-09-07 reading of the test deployment — LoadState=not-found there, with the production claim left alone because production had not been measured. It has been now, and this is that reading.", tracked: ["client-mcp.service"],
340
+ measured: "2026-09-08", tracked: ["client-mcp.service"],
347
341
  note: "TRACKED, in mcp-server/remote/ — this entry said it had no file, and the file was there. It is "
348
342
  + "a banner-marked TEMPLATE, so the live copy differing from it is the arrangement, not drift. "
349
343
  + "It was reported as shipping no unit; a unit did ship, in mcp-server/remote/ rather than "
@@ -353,10 +347,7 @@ export const UNIT_INVENTORY = Object.freeze([
353
347
  },
354
348
  {
355
349
  unit: "client-mcp-apikey", runsOn: ["test"],
356
- measured: "2026-09-08, read on the test deployment: enabled and present, beside the renamed "
357
- + "`clearotron-client-mcp`, and INACTIVE — enabled with no running process. Absent from "
358
- + "production. The entry declared production and the reading puts it on the other deployment "
359
- + "— the claim was not stale, it named the wrong machine.",
350
+ measured: "2026-09-08",
360
351
  tracked: ["client-mcp-apikey.service"],
361
352
  // THE SEPARATE QUESTION, ANSWERED. The line above used to end by saying that why it runs beside the
362
353
  // renamed door was a question for somebody else. It is a survivor of the rename, not a posture
@@ -396,7 +387,7 @@ export const UNIT_INVENTORY = Object.freeze([
396
387
  orphanReason: "A PRE-RENAME NAME OF THE RETIRED DEPLOYMENT, and the one of them whose tracked file "
397
388
  + "is GENERIC rather than a template — so the file is ready to be compared against a live copy the "
398
389
  + "day something runs it, and nothing does. Read as absent under every account on 2026-09-08.",
399
- measured: "2026-09-08, read on production: no unit of this name is present. Names and enable state only; no unit contents were read.", tracked: ["trademark-artifacts-http.service"],
390
+ measured: "2026-09-08", tracked: ["trademark-artifacts-http.service"],
400
391
  note: "TRACKED, in mcp-server/remote/. GENERIC, not a template: it carries no banner and no "
401
392
  + "placeholder, and defers its CF Access values to the EnvironmentFile — so its live copy is "
402
393
  + "expected to MATCH the tracked file, and a difference is real drift.",
@@ -453,8 +444,7 @@ export const UNIT_INVENTORY = Object.freeze([
453
444
  orphanReason: "NOT THIS REPOSITORY'S SERVICE, and no longer running under this name: production holds it "
454
445
  + "only under a retired-suffixed filename, a suffix systemd never reads. "
455
446
  + "Declared here so a unit sharing a machine with ours is not invisible, and now declared as gone.",
456
- measured: "2026-09-08, read on production: present ONLY as a retired-suffixed filename, which systemd "
457
- + "does not load. No loadable unit of this name.", tracked: null,
447
+ measured: "2026-09-08", tracked: null,
458
448
  untrackedReason: "it is not this repo's service. Its ExecStart runs a script from a different "
459
449
  + "product's checkout on the same box, not under this clone — so there is no code here to "
460
450
  + "template, and the old reason (CF Access values inline) would send the next reader hunting for a "
@@ -519,8 +509,7 @@ export const UNIT_INVENTORY = Object.freeze([
519
509
  + "flag was ever captured, so stopping it dropped nothing. The decision owed, if any, is whether the "
520
510
  + "feedback machinery is removed rather than parked; driver/run-activity.mjs still carries a note "
521
511
  + "addressed to whoever picks that up, and it is still unclaimed.",
522
- note: "STOPPED against an input nothing writes to any more; the owner's word came 2026-08-20 "
523
- + "and it was disabled the same evening.",
512
+ note: "STOPPED against an input nothing writes to any more.",
524
513
  },
525
514
  // ── ANOTHER PRODUCT'S UNITS ARE NOT DECLARED HERE, AND THAT IS THE RULE (ruling 594, 2026-09-27) ──
526
515
  //
@@ -544,7 +533,7 @@ export const UNIT_INVENTORY = Object.freeze([
544
533
  // deployments. Not here. This file ships.
545
534
  {
546
535
  unit: "trademark-test-deploy", runsOn: [],
547
- measured: "2026-09-07, read on the test deployment: LoadState=not-found. The hourly deploy runs as clearotron-deploy.timer, which has its own entry; this name is what that one used to be called.", tracked: null,
536
+ measured: "2026-09-07. The hourly deploy runs as clearotron-deploy.timer, which has its own entry; this name is what that one used to be called.", tracked: null,
548
537
  // AN EMPTY runsOn OWES A REASON, and this one is a supersession rather than an orphan: the hourly
549
538
  // deploy still runs on the test deployment, under the name `clearotron-deploy`, which has its own entry
550
539
  // its own measurement. This name is what that unit used to be called. It is kept rather than deleted
package/driver/verify.mjs CHANGED
@@ -16,7 +16,7 @@ import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-recor
16
16
  import { findConnotationViolations, parsePrRiskResults, prRiskPopulation,
17
17
  CONNOTATION_UNMATCHED_MARK, CONNOTATION_NO_RESEMBLANCE_MARK, MEANING_ANGLES_RE,
18
18
  parseDispositionForm, CONNOTATION_UNRULED_REASONS, queryKey } from "./connotation-search.mjs";
19
- import { formSidecarName, formSidecarPath } from "./disposition-union.mjs"; import { coverageStatusAsData } from "./common-law-coverage-status.mjs";
19
+ import { formSidecarName, formSidecarPath } from "./disposition-union.mjs";
20
20
  // B — the transport's own four failure states. The audit reads the run's records; this file locates them.
21
21
  import { auditDispositionCalls, CALL_FAILURE_REASONS } from "./disposition-call-audit.mjs";
22
22
  import { callRecordPaths } from "./disposition-tool.mjs";
@@ -37,7 +37,7 @@ import { registerPlanCallKilled } from "./tool-calls.mjs"; // — did the dict
37
37
  import { parseFindingsJson, parseFindingsJsonLenient, CLIENT_TIER_BY_COMPOSITE, isUnconditionalProceed, joinFindingToBlock, parseBlockOrd } from "./findings-model.mjs";
38
38
  import { parsePlacementsJson } from "./placement-model.mjs"; import { placementFormSidecarName, parsePlacementForm, placementRenderAccount } from "./placement-form.mjs";
39
39
  import { parseCaseLawLedger, findCaseLawLedgerViolations, caseLawLedgerFail } from "./case-law-ledger.mjs";
40
- import { parseFrameworkManifest, aboveLowestBand, normalizeBand } from "./framework.mjs"; import { readFrozenMethod, FROZEN_METHOD_FILE } from "./framework-method.mjs";
40
+ import { parseFrameworkManifest, materialBand, normalizeBand } from "./framework.mjs"; import { readFrozenMethod, FROZEN_METHOD_FILE } from "./framework-method.mjs";
41
41
  import { parseNamedBand, findCollapsedBands } from "./named-band.mjs";
42
42
  import { parseBlindFrameModel } from "./blind-frame-model.mjs";
43
43
  import { parseFrameDiff } from "./frame-diff-model.mjs";
@@ -158,7 +158,7 @@ function nonEmpty(content, min = MIN) {
158
158
  * true for only one lane, and put back. Appending keeps every existing matcher matching — they all test
159
159
  * substrings — and gives the seat the one word it was missing.
160
160
  *
161
- * It also sharpens `noChange` (`gateway.mjs:1220`, "this attempt's token equals the previous attempt's"):
161
+ * It also sharpens `noChange` (in runStageLadder() in gateway.mjs, "this attempt's token equals the previous attempt's"):
162
162
  * a file that starts failing a DIFFERENT member now reads as changed instead of as a stalled retry.
163
163
  *
164
164
  * `names` is positional against `markers` and optional; a call site that omits it emits exactly the token
@@ -681,7 +681,7 @@ function commonLawStructural(c, p = null) {
681
681
  needsSection(c, "coverage-ledger", [/coverage[\s-]ledger/i], "coverage-ledger"),
682
682
  needsSection(c, "audit-trail", [/audit[\s-]trail/i], "audit-trail"),
683
683
  needs(c, [/\|/], "platform matrix"),
684
- coverageStatusAsData(p) || hasCoverageLedgerRow(c) ? ok() : fail("no_coverage_status_row"), // the recorded status first, the word as fallback: coverageStatusAsData() in common-law-coverage-status.mjs
684
+ // no status check here (ruling 2026-10-01): a ledger with no status word and no record passes, and code records what it states — recordStatusesFromFindings() in common-law-coverage-status.mjs
685
685
  );
686
686
  }
687
687
 
@@ -1203,9 +1203,9 @@ function checkFindingsSibling(p, c) {
1203
1203
  const v4 = (parsed.schemaVersion ?? 1) >= 4;
1204
1204
  if (v4 && !fw.manifest) return fail("framework_manifest_missing_for_v4: schema_version 4 findings need the frozen _driver/framework.json to judge band words");
1205
1205
  const v3 = (parsed.schemaVersion ?? 1) >= 3; // gates key on schema_version — v2/archived runs and replay never flip
1206
- // "material finding" — the line the receipt gates key on: composite>=3 on the legacy scale; on v4,
1207
- // banded ABOVE the framework's lowest band (judgment-free re-expression of the same line).
1208
- const material = (f) => v4 ? (f.band != null && aboveLowestBand(fw.manifest, f.band)) : f.composite >= 3;
1206
+ // "material finding" — the line the receipt gates key on: composite>=3 on the legacy scale; on v4, banded on
1207
+ // a rung whose own tone is medium risk or higher (materialBand — by tone, never by position on the ladder).
1208
+ const material = (f) => v4 ? (f.band != null && materialBand(fw.manifest, f.band)) : f.composite >= 3;
1209
1209
  for (const f of parsed.findings) {
1210
1210
  if (material(f) && f.use_check != null && !String(f.use_check.source ?? "").trim())
1211
1211
  return fail(`finding_use_check_source_missing:${f.ordinal}`);
@@ -1,5 +1,13 @@
1
1
  # trademark-artifacts-mcp
2
2
 
3
+ ## 0.4.1-beta.1
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.4.1-beta.0
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.4.0
4
12
 
5
13
  No changes in this release.
@@ -21,9 +21,9 @@ It reads run directories off your disk. So:
21
21
 
22
22
  - **It shows you runs you have already produced.** `CLEAROTRON_WORK_DIR` is where the engine writes
23
23
  them, and that is the one variable the server needs.
24
- - **`npx clearotron demo` runs are not visible to it.** The demo publishes a *report* into a pool; it does not
25
- create a run directory. There is nothing dishonest happening if `list_runs` comes back empty — you have
26
- no runs yet.
24
+ - **With no install, it shows you the demo.** `npx clearotron mcp` starts this server from the npm package.
25
+ On a machine with no install it answers from the four sample runs `npx clearotron demo` replays, and every
26
+ row says it is the demo. On a machine with an install it reads that install's runs.
27
27
  - **Nothing here spends money on its own,** with two exceptions that are gated and named: `start_run`
28
28
  enqueues a real clearance, and `what_if_run` re-runs one pipeline stage. Both are refused by default on
29
29
  a sensible allowlist — see "Hold two tools back" below.
@@ -51,6 +51,12 @@ claude mcp add trademark-artifacts --scope user \
51
51
 
52
52
  Check it: `claude mcp list` prints `trademark-artifacts: … - √ Connected`.
53
53
 
54
+ With no clone, start it from the npm package instead, naming the version:
55
+
56
+ ```sh
57
+ claude mcp add trademark-artifacts --scope user -- npx -y clearotron@<version> mcp
58
+ ```
59
+
54
60
  `--scope user` makes it available in every project, not only the directory you ran it in. In Windows
55
61
  PowerShell 5.1, write the separator as `"--"`: that shell drops a bare `--`. Remove it with
56
62
  `claude mcp remove trademark-artifacts`.
@@ -0,0 +1,10 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // demo-mode.mjs — whether this server is serving the demo's sample runs.
5
+ //
6
+ // `clearotron mcp` decides it and the server reads it, in one process: bin/mcp.mjs sets it before it
7
+ // loads the server, and the server reads it as it answers. It is a module and not an environment
8
+ // variable because nothing outside this process sets it or reads it, and every environment variable the
9
+ // product reads is a setting an operator is told about.
10
+ export const demoMode = { on: false };
@@ -294,7 +294,7 @@ const MATCH_SHAPES = [
294
294
  // `*` wildcard, and the term extractor must see the word, not the markup.
295
295
  const deMarkup = (s) => String(s ?? "").replace(/\*\*|__/g, "");
296
296
 
297
- // Record COUNTS ride along in parentheses — `PARADISE (424)`, `パラダイス (34) / 파라다이스 (20)`. They are
297
+ // Record COUNTS ride along in parentheses — `HAVEN (424)`, `ヘイヴン (34) / 헤이븐 (20)`. They are
298
298
  // process exhaust rather than the query, and dropping them first is what lets the rest of the string be
299
299
  // recognised as the bare term it is.
300
300
  const deCount = (s) => String(s ?? "").replace(/\s*\(\s*~?\s*[\d,]+\s*\)/g, "");
@@ -302,26 +302,26 @@ const deCount = (s) => String(s ?? "").replace(/\s*\(\s*~?\s*[\d,]+\s*\)/g, "");
302
302
  // A searched term is recognised three ways, in order, and NOT recognised otherwise:
303
303
  // 1. explicitly quoted — `exact "Drivers Haven" (default)`
304
304
  // 2. the whole string, when it carries no query scaffolding at all. This is how the common-law sweep
305
- // writes them and it is the bulk of the corpus: `레이서스 파라다이스`, `赛车天堂`, `RACERZ`,
306
- // `PARADISE WORLD`. Those scripts are caseless, so a capitals rule cannot see them at all.
305
+ // writes them and it is the bulk of the corpus: `드라이버스 헤이븐`, `车手港湾`, `DRIVERZ`,
306
+ // `HAVEN WORLD`. Those scripts are caseless, so a capitals rule cannot see them at all.
307
307
  // "No scaffolding" is checked positively: no match-shape keyword, no residual parenthetical, and
308
308
  // none of the structural punctuation the audit uses for anything OTHER than a query (a record path,
309
309
  // a bracketed class list, an em-dash clause, a key: value tail).
310
- // 3. an ALL-CAPS token — `exact PARADISE`, `*PARADISE / fuzzy`. Two-letter tokens (ZH, JP, DE) are
310
+ // 3. an ALL-CAPS token — `exact HAVEN`, `*HAVEN / fuzzy`. Two-letter tokens (ZH, JP, DE) are
311
311
  // language/jurisdiction codes, not marks, and the register offices above are offices, not marks.
312
312
  const SCAFFOLD = /[[\]`:—]|\/mark\//;
313
313
 
314
314
  // "Does this read as a NAME?" — the property that separates a searched mark from a description of a
315
315
  // search. Every alphabetic token must start upper-case or be in a caseless script (CJK, Hangul, Kana,
316
- // Arabic); a token that is entirely lower-case Latin is prose. So `Racer's Paradise`, `PARADISE WORLD`,
317
- // `Вензи` and `레이서스 파라다이스` qualify and `national register`, `formative phrases` and
316
+ // Arabic); a token that is entirely lower-case Latin is prose. So `Driver's Haven`, `HAVEN WORLD`,
317
+ // `Вензи` and `드라이버스 헤이븐` qualify and `national register`, `formative phrases` and
318
318
  // `one-swap lead-noun neighbours` do not. A property, not a list of banned words — a description nobody
319
319
  // anticipated still fails it.
320
320
  function looksLikeName(s) {
321
321
  const tokens = String(s).split(/\s+/).filter((t) => /\p{L}/u.test(t));
322
322
  if (!tokens.length || tokens.length > 5) return false;
323
323
  // A lone two-letter capital is a language or jurisdiction code (ZH, DE, JP), never a mark. Caseless
324
- // scripts are exempt from the length floor — 天堂 is a whole word in two characters.
324
+ // scripts are exempt from the length floor — 港湾 is a whole word in two characters.
325
325
  const cased = tokens.filter((t) => /\p{Lu}|\p{Ll}/u.test(t));
326
326
  if (cased.length && cased.every((t) => t.replace(/[^\p{L}]/gu, "").length <= 2)) return false;
327
327
  return tokens.every((t) => !/^\p{Ll}/u.test(t.replace(/^[^\p{L}\p{N}]+/u, "")));
@@ -361,7 +361,7 @@ function extractTerm(raw) {
361
361
  const asClasses = (nums) => [...new Set(nums.map(Number).filter((n) => Number.isInteger(n) && n >= 1 && n <= 45))];
362
362
 
363
363
  // In a SEARCH TERM, classes are read only where they are LABELLED ("cl 9/28/41/42", "cl.25", "class 35") —
364
- // never from a bare parenthesised number, which in this corpus is a record COUNT ("PARADISE (424)").
364
+ // never from a bare parenthesised number, which in this corpus is a record COUNT ("HAVEN (424)").
365
365
  function extractClasses(s) {
366
366
  const m = String(s ?? "").match(/\bcl(?:ass(?:es)?)?\.?\s*([\d]{1,2}(?:\s*[/,]\s*\d{1,2})*)/i);
367
367
  return m ? asClasses(m[1].split(/[/,]/)) : [];
@@ -388,7 +388,7 @@ export function traceKnockout(run, target, events = []) {
388
388
  kind: "verdict",
389
389
  verdict: doc?.batch?.overall ?? runVerdictFromEvents(events) ?? null,
390
390
  marks: marksOf(doc).map((m) => ({ mark: m.name, band: m.rating ?? null, basis: m.basis ?? null })),
391
- note: "A Knockout verdict is the batch's worst band across its marks; each mark's own band and the "
391
+ note: "A Knockout verdict is the highest rating across the batch's marks; each mark's own rating and the "
392
392
  + "one-sentence ground for it are listed here.",
393
393
  });
394
394
 
@@ -58,7 +58,13 @@ export function tierFromRecord(runDir) {
58
58
  } catch { return null; }
59
59
  }
60
60
 
61
- export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet }) {
61
+ export function unreadableRunsReason({ workSet, workRoot, workExists, poolSet, workReadError = null }) {
62
+ // A work folder that was named and is not there, or cannot be read, is said, never listed as empty.
63
+ if (workSet && workReadError) {
64
+ return `no searches can be read here: CLEAROTRON_WORK_DIR is ${workRoot}, which `
65
+ + (workReadError === "ENOENT" ? "does not exist. " : `cannot be read (${workReadError}). `)
66
+ + "`npx clearotron doctor` prints where an install keeps them.";
67
+ }
62
68
  if (workSet || workExists || poolSet) return null;
63
69
  return `no searches can be read here: CLEAROTRON_WORK_DIR is unset and ${workRoot} does not exist, `
64
70
  + "and CLEAROTRON_REPORTS_DIR is unset. Set them to the install's directories — `npx clearotron doctor` "
@@ -158,7 +158,7 @@ export function scrubMarkdown(text) {
158
158
  // at least announced itself as internal.
159
159
  //
160
160
  // The ruling needed no new product decision, because the product had already made it:
161
- // `publish/report-data.mjs:74` (`const live`) filters to live findings, "a withdrawn finding renders nowhere — it does
161
+ // clearanceReportData() in report-data.mjs (`const live`) filters to live findings, "a withdrawn finding renders nowhere — it does
162
162
  // not exist here either". Two client surfaces, one question, two answers. So the block is DROPPED, and
163
163
  // the two agree by construction rather than key by key — which is the thing asked not to repeat.
164
164
  //
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.1-beta.1",
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.",
@@ -13,7 +13,7 @@
13
13
  "test:full": "npm run test"
14
14
  },
15
15
  "dependencies": {
16
- "@modelcontextprotocol/sdk": "^1.29.0",
16
+ "@modelcontextprotocol/sdk": "^1.31.0",
17
17
  "jose": "^6.2.3"
18
18
  },
19
19
  "engines": {
@@ -37,7 +37,7 @@ saying the same thing.
37
37
 
38
38
  The `SKILL.md` (and for ops, `COURIER.md`) is portable prompt text: paste it into Claude project
39
39
  instructions, a ChatGPT GPT/system prompt, or any agent's system context — or, in Claude Code, install
40
- the whole thing at once by opening this repository as a plugin (`.claude-plugin/plugin.json`).
40
+ the whole thing at once as the plugin in `plugin/`, which carries copies of the three skills.
41
41
  `CONNECT.md` is for the human doing the wiring. `manifest.json` names the pack version, the files that
42
42
  stayed here, and the `skill.directory` its prompt text moved to, so an integrator can pin + diff
43
43
  upgrades.
@@ -26,13 +26,14 @@ import {
26
26
  ListResourcesRequestSchema, ReadResourceRequestSchema, ListResourceTemplatesRequestSchema,
27
27
  ListPromptsRequestSchema, GetPromptRequestSchema,
28
28
  } from "@modelcontextprotocol/sdk/types.js";
29
- import { existsSync, readFileSync } from "node:fs";
29
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
30
30
  import { join, basename } from "node:path";
31
31
  import { driverDir } from "../shared/driver-dir.mjs"; //
32
32
  import { fileURLToPath } from "node:url";
33
33
 
34
34
  import { enumerateRuns, resolveRun, runAccountKey, runOrganisation, runProfileFacts, productIdentityFor, unreadableRunsReason } from "./lib/runs.mjs";
35
35
  import { ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
36
+ import { demoMode } from "./demo-mode.mjs";
36
37
  import { PRODUCTS } from "../driver/products.mjs";
37
38
 
38
39
  // The offering writes its own schema prose. A hand-typed "up to 20 names" beside a wall that refuses at
@@ -178,6 +179,17 @@ function getStages(runDir) {
178
179
  return { stages, failover };
179
180
  }
180
181
 
182
+ // THE DEMO SAYS IT IS THE DEMO, on every row, in the demo's own two sentences (bin/example.mjs prints both
183
+ // before anything else). `clearotron mcp` serves the demo's sample runs where a machine has no install,
184
+ // and an assistant reading a row must not take a sample for one of the person's own searches.
185
+ export const DEMO_LABEL = "Real engine output for the fictional mark VENQORI, captured against Clarivate Compumark. "
186
+ + "It is an example, not advice.";
187
+
188
+ /** The code a folder's read fails with, or null where it reads. */
189
+ function readError(dir) {
190
+ try { readdirSync(dir); return null; } catch (e) { return e?.code ?? "unreadable"; }
191
+ }
192
+
181
193
  function runSummary(run) {
182
194
  const s = run.status ?? {};
183
195
  // WHO THE SEARCH WAS FOR, on every row. A session that holds several clients was handed eight rows
@@ -188,6 +200,7 @@ function runSummary(run) {
188
200
  // field is what made the list unanswerable in the first place.
189
201
  const facts = runProfileFacts(run);
190
202
  return {
203
+ ...(demoMode.on ? { demo: DEMO_LABEL } : {}),
191
204
  runId: run.runId, slug: run.slug, codename: run.codename, date: run.date, agent: run.agent,
192
205
  client: facts.known
193
206
  ? { key: facts.account, name: facts.clientName }
@@ -272,6 +285,7 @@ const tools = {
272
285
  const unreadable = unreadableRunsReason({
273
286
  workSet: !!config.envValue("CLEAROTRON_WORK_DIR"), workRoot: config.workspaceRoot,
274
287
  workExists: existsSync(config.workspaceRoot), poolSet: !!config.poolRootOrNull,
288
+ workReadError: readError(config.workspaceRoot),
275
289
  });
276
290
  if (unreadable) throw new Error(unreadable);
277
291
  let out = enumerateRuns({ agent, state, slug, mark }).map(runSummary);
@@ -1102,7 +1116,7 @@ export function makeServer({ scope = null, local = null, readonly = false } = {}
1102
1116
  // pack was previously a file no connecting client ever saw. Guidance only; never a substitute for the
1103
1117
  // authorize()/scrub gates. Staff and ops get no instructions (see lib/instructions.mjs).
1104
1118
  const server = new Server(
1105
- { name: NS, version: "0.1.0" },
1119
+ { name: NS, version: productIdentity().version ?? "unknown" }, // the package's own version, the one server_info reports
1106
1120
  // `prompts` is declared for every kind; promptsFor() returns [] for the audiences that get none, so
1107
1121
  // an ops client sees an empty list rather than a protocol error on a capability we did not announce.
1108
1122
  { capabilities: { tools: {}, resources: {}, prompts: {} }, instructions: instructionsFor(eff) },
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "clearotron",
3
+ "mcpName": "io.github.CordilleraSarl/clearotron",
3
4
  "type": "module",
4
- "version": "0.4.0",
5
+ "version": "0.4.1-beta.1",
5
6
  "license": "AGPL-3.0-only",
6
7
  "repository": {
7
8
  "type": "git",
@@ -58,7 +59,7 @@
58
59
  "sync:uspto": "node bin/uspto-sync.mjs",
59
60
  "bench:uspto": "node providers/uspto-local/bench/bench.mjs",
60
61
  "verify:uspto": "node providers/uspto-local/bin/verify-index.mjs",
61
- "lint:driver": "eslint --no-error-on-unmatched-pattern --max-warnings 0 --config eslint.driver.config.mjs \"driver/**/*.mjs\" \"bin/**/*.mjs\" \"shared/**/*.mjs\" \"mcp-server/**/*.mjs\" \"scripts/**/*.mjs\" \"cut/**/*.mjs\"",
62
+ "lint:driver": "eslint --no-error-on-unmatched-pattern --max-warnings 0 --config eslint.driver.config.mjs \"driver/**/*.mjs\" \"bin/**/*.mjs\" \"shared/**/*.mjs\" \"mcp-server/**/*.mjs\" \"scripts/**/*.mjs\" \"cut/**/*.mjs\" \"plugin/**/*.mjs\"",
62
63
  "doctrine-report": "node scripts/doctrine-report.mjs",
63
64
  "notices": "node scripts/third-party-notices.mjs",
64
65
  "notices:check": "node scripts/third-party-notices.mjs --check",
@@ -73,7 +74,7 @@
73
74
  "globals": "^17.11.0"
74
75
  },
75
76
  "dependencies": {
76
- "@modelcontextprotocol/sdk": "^1.29.0",
77
+ "@modelcontextprotocol/sdk": "^1.31.0",
77
78
  "buffers": "file:vendor/buffers",
78
79
  "exceljs": "^4.4.0",
79
80
  "jose": "^6.2.3",
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.4.0",
5
+ "version": "0.4.1-beta.1",
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": {
@@ -22,8 +22,8 @@
22
22
  "devDependencies": {
23
23
  "@types/react": "^19.3.0",
24
24
  "@types/react-dom": "^19.3.0",
25
- "@vitejs/plugin-react": "^6.1.1",
25
+ "@vitejs/plugin-react": "^6.1.2",
26
26
  "typescript": "^7.0.2",
27
- "vite": "^8.3.1"
27
+ "vite": "^8.3.3"
28
28
  }
29
29
  }
@@ -47,6 +47,7 @@
47
47
 
48
48
  import { guardCountCall, guardToolCall } from "./transport-guard.mjs";
49
49
  import { clipProviderText } from "./provider-text.mjs"; // — keep the discriminator
50
+ import { floorOf } from "../../shared/register-floor.mjs";
50
51
 
51
52
  export const parseToolText = (r) => { try { return JSON.parse(r?.text ?? ""); } catch { return null; } };
52
53
  export const isToolError = (r) => !!(r?.isError) || (typeof r?.text === "string" && r.text.startsWith("ERROR"));
@@ -109,9 +110,15 @@ export function makeCountProbe(deps) {
109
110
  // ("a provider that cannot count must never read as zero") broken from the inside, by its own
110
111
  // comment. The number now has to BE a number; nothing else is an answer.
111
112
  if (!Number.isFinite(parsed.total_hits)) {
112
- return { ok: false, total: null, probe: "cheap",
113
+ // A register's floor ("at least 10,000") is still no count, so the probe stays not-ok; the figure
114
+ // rides beside it for a caller that can carry one (enumerate.mjs's rescues: a crowd, never an error).
115
+ const floor = floorOf(parsed);
116
+ return { ok: false, total: null, probe: "cheap", ...(floor !== null ? { floor } : {}),
113
117
  reason: "the search response carried no usable total_hits, so nothing was counted — the number is UNKNOWN, which is not the same as none" };
114
118
  }
115
- return { ok: true, total: parsed.total_hits, probe: "cheap", reason: null };
119
+ // What the total counts rides with it when the register said (a provider's `total_counts`, see
120
+ // enumerate.mjs): a per-class or per-spelling count keeps the register's number and says so.
121
+ return { ok: true, total: parsed.total_hits, probe: "cheap", reason: null,
122
+ ...(typeof parsed.total_counts === "string" ? { total_counts: parsed.total_counts } : {}) };
116
123
  };
117
124
  }