clearotron 0.2.4 → 0.3.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (235) hide show
  1. package/.env.example +13 -2
  2. package/CONTRIBUTING.md +1 -1
  3. package/INSTALL.md +62 -38
  4. package/bin/brandowner.mjs +18 -169
  5. package/bin/clearotron.mjs +3 -1
  6. package/bin/connect.mjs +28 -19
  7. package/bin/disconnect.mjs +3 -3
  8. package/bin/example.mjs +7 -7
  9. package/bin/framework-preflight.mjs +49 -0
  10. package/bin/grant.mjs +151 -93
  11. package/bin/onboard.mjs +220 -138
  12. package/bin/start.mjs +146 -137
  13. package/bin/stop.mjs +2 -2
  14. package/bin/update.mjs +1 -1
  15. package/build-info.json +2 -2
  16. package/docs/CLIENT-MCP.md +2 -2
  17. package/docs/E2E.md +12 -2
  18. package/docs/ONBOARDING.md +1 -1
  19. package/docs/PORTAL.md +14 -13
  20. package/docs/SECURITY.md +23 -24
  21. package/docs/architecture/04-configuration-reference.md +12 -5
  22. package/docs/architecture/05-config-governance.md +7 -7
  23. package/docs/architecture/07-quality-and-audit.md +1 -1
  24. package/docs/architecture/08-development-guide.md +5 -0
  25. package/docs/configuration.md +118 -0
  26. package/docs/decisions/0004-documentation-structure.md +2 -2
  27. package/docs/decisions/0006-what-the-public-repository-carries.md +2 -2
  28. package/driver/CHANGELOG.md +70 -0
  29. package/driver/ask-ledger.mjs +2 -2
  30. package/driver/cancel.mjs +27 -0
  31. package/driver/case-law-sources.mjs +3 -3
  32. package/driver/company-bundle.mjs +261 -0
  33. package/driver/compare.mjs +1 -1
  34. package/driver/compose-read.mjs +2 -2
  35. package/driver/config-inventory.mjs +2 -2
  36. package/driver/contract-audit.mjs +1 -1
  37. package/driver/contract-e3-backlog.mjs +1 -1
  38. package/driver/contract-vocabulary.mjs +1 -1
  39. package/driver/declination-call.mjs +1 -1
  40. package/driver/deliver-trigger.sh +3 -3
  41. package/driver/dev-portal.mjs +3 -1
  42. package/driver/digest-queue.mjs +1 -1
  43. package/driver/disposition-tool.mjs +1 -1
  44. package/driver/doc-constants.mjs +1 -1
  45. package/driver/drain-posture.mjs +2 -2
  46. package/driver/drainer-identity.mjs +1 -1
  47. package/driver/driver.config.mjs +44 -9
  48. package/driver/effective-scope.mjs +30 -1
  49. package/driver/effort-model.mjs +6 -6
  50. package/driver/engine/CONTRACT.md +2 -2
  51. package/driver/engine/anthropic-agent.mjs +11 -11
  52. package/driver/engine/jx-turn.mjs +1 -1
  53. package/driver/engine/mcp/gather-config.mjs +29 -4
  54. package/driver/engine/mcp/recording-server.mjs +73 -1
  55. package/driver/engine/openai-agent.mjs +1 -1
  56. package/driver/engine/probe.mjs +28 -4
  57. package/driver/enqueue-schema.mjs +23 -3
  58. package/driver/findings-model.mjs +2 -2
  59. package/driver/flag-snapshot.mjs +2 -2
  60. package/driver/floor-duty.mjs +2 -2
  61. package/driver/frame-diff-model.mjs +1 -1
  62. package/driver/framework-preflight.mjs +143 -0
  63. package/driver/gateway.mjs +9 -1
  64. package/driver/hit-list.mjs +1 -1
  65. package/driver/jx-lanes.mjs +1 -1
  66. package/driver/jx.mjs +1 -1
  67. package/driver/knockout-assess-record.mjs +1 -1
  68. package/driver/knockout-review-record.mjs +435 -0
  69. package/driver/order-probe.mjs +1 -1
  70. package/driver/outbox-backoff.mjs +2 -2
  71. package/driver/owner-use-check.mjs +2 -2
  72. package/driver/package.json +1 -1
  73. package/driver/pipeline-knockout.mjs +105 -8
  74. package/driver/pipeline.mjs +81 -30
  75. package/driver/plain-register.mjs +77 -3
  76. package/driver/portal-access.mjs +141 -74
  77. package/driver/portal-config-view.mjs +59 -70
  78. package/driver/portal-report.mjs +4 -4
  79. package/driver/portal-service.mjs +348 -141
  80. package/driver/portal-upstream.mjs +105 -17
  81. package/driver/predelivery-lint.mjs +43 -18
  82. package/driver/product-rows.mjs +1 -1
  83. package/driver/products.mjs +1 -1
  84. package/driver/profile-page.html +30 -5
  85. package/driver/profile-service.mjs +197 -26
  86. package/driver/profiles.mjs +48 -1
  87. package/driver/publish/index.mjs +31 -13
  88. package/driver/publish/knockout.mjs +9 -5
  89. package/driver/publish/office-record-links.mjs +189 -0
  90. package/driver/publish/parse.mjs +3 -3
  91. package/driver/publish/publish-inputs.mjs +26 -0
  92. package/driver/publish/render-knockout.mjs +42 -42
  93. package/driver/publish/render.mjs +29 -4
  94. package/driver/publish/report-data.mjs +2 -2
  95. package/driver/publish/seed-pool.mjs +1 -1
  96. package/driver/publish/templates/report.css +8 -8
  97. package/driver/publish/xlsx.mjs +49 -7
  98. package/driver/queue-watch-verdict.mjs +2 -2
  99. package/driver/recipe-service.mjs +1 -1
  100. package/driver/record-carry.mjs +1 -1
  101. package/driver/reference-score.mjs +1 -1
  102. package/driver/reference-strip-signatures.mjs +1 -1
  103. package/driver/register-availability.mjs +4 -3
  104. package/driver/register-count.mjs +3 -3
  105. package/driver/register-records.mjs +1 -1
  106. package/driver/repair-composers.mjs +1 -1
  107. package/driver/repairs.mjs +3 -3
  108. package/driver/replay-archive.mjs +1 -1
  109. package/driver/report-card-record.mjs +1 -1
  110. package/driver/result-noun-fields.mjs +5 -0
  111. package/driver/roster-verdict.mjs +48 -5
  112. package/driver/run-activity.mjs +1 -1
  113. package/driver/run-requirements.mjs +18 -5
  114. package/driver/runner.mjs +24 -15
  115. package/driver/search-policy.mjs +8 -8
  116. package/driver/senior-rights.mjs +1 -1
  117. package/driver/skills/prelim-search/delivery-contract.md +1 -1
  118. package/driver/skills/prelim-search/risk-framework-triage.md +10 -7
  119. package/driver/stages-knockout.mjs +72 -6
  120. package/driver/stages.mjs +10 -10
  121. package/driver/suite-census.json +238 -70
  122. package/driver/synthesis-record.mjs +2 -2
  123. package/driver/systemd/clearotron-client-mcp.service +3 -3
  124. package/driver/systemd/clearotron-deploy.service +2 -2
  125. package/driver/systemd/clearotron-mcp-face.service +1 -1
  126. package/driver/systemd/clearotron-portal.service +3 -3
  127. package/driver/systemd/clearotron-worker.service +5 -5
  128. package/driver/systemd/install-census.mjs +1 -1
  129. package/driver/systemd/render-units.mjs +9 -9
  130. package/driver/terminal-clamp.mjs +1 -1
  131. package/driver/trigger-cap.mjs +18 -2
  132. package/driver/unit-inventory.mjs +8 -8
  133. package/driver/usage-ledger.mjs +5 -3
  134. package/driver/verify-knockout.mjs +7 -7
  135. package/driver/verify.mjs +5 -5
  136. package/driver/whatif-memo-run.mjs +1 -1
  137. package/driver/whatif-queue.mjs +3 -3
  138. package/driver/whatif-worker.mjs +2 -2
  139. package/examples/README.md +1 -1
  140. package/examples/grants.example.json +25 -24
  141. package/mcp-server/CHANGELOG.md +10 -0
  142. package/mcp-server/http-server.mjs +3 -3
  143. package/mcp-server/key-socket.mjs +1 -1
  144. package/mcp-server/lib/audit-view.mjs +3 -3
  145. package/mcp-server/lib/brief.mjs +3 -3
  146. package/mcp-server/lib/driver.mjs +1 -1
  147. package/mcp-server/lib/events.mjs +1 -1
  148. package/mcp-server/lib/http-handler.mjs +2 -2
  149. package/mcp-server/lib/instructions.mjs +2 -2
  150. package/mcp-server/lib/knockout.mjs +1 -1
  151. package/mcp-server/lib/ops.mjs +6 -3
  152. package/mcp-server/lib/options.mjs +15 -4
  153. package/mcp-server/lib/plan.mjs +7 -6
  154. package/mcp-server/lib/runs.mjs +10 -0
  155. package/mcp-server/lib/whatif.mjs +5 -5
  156. package/mcp-server/package.json +1 -1
  157. package/mcp-server/packs/README.md +1 -1
  158. package/mcp-server/remote/client-mcp-apikey.service +1 -1
  159. package/mcp-server/remote/client-mcp.service +2 -2
  160. package/mcp-server/remote/trademark-artifacts-http.service +1 -1
  161. package/mcp-server/server.mjs +38 -24
  162. package/package.json +2 -2
  163. package/portal-ui/dist/assets/{index-KFAHMgdT.js → index-CWTHP0sH.js} +3471 -1901
  164. package/portal-ui/dist/assets/{index-1ziUJX1E.css → index-KpytsmNH.css} +79 -26
  165. package/portal-ui/dist/index.html +2 -2
  166. package/portal-ui/package.json +1 -1
  167. package/providers/_shared/lane-probe.mjs +9 -3
  168. package/providers/jx-subclass/lookup.mjs +1 -1
  169. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  170. package/providers/oauth-mcp-bridge/package.json +1 -1
  171. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  172. package/scripts/citation-drift-report.mjs +1 -1
  173. package/scripts/citation-line-check.mjs +2 -2
  174. package/scripts/drive-env-check.mjs +1 -1
  175. package/scripts/e2e.mjs +5 -5
  176. package/scripts/env-audit.mjs +13 -1
  177. package/scripts/headless-page.mjs +5 -5
  178. package/scripts/live-surface-check.mjs +26 -2
  179. package/scripts/mint-names-in-force.mjs +19 -5
  180. package/scripts/mint-reference-strip-backlog.mjs +1 -1
  181. package/scripts/mint-suite-census.mjs +37 -10
  182. package/scripts/pack-publishable.mjs +1 -1
  183. package/scripts/preinstall-node-check.mjs +1 -1
  184. package/scripts/release-await-cut.mjs +3 -3
  185. package/scripts/release-cut-decision.mjs +1 -1
  186. package/scripts/release-dist-tag.mjs +1 -1
  187. package/scripts/release-install-check.mjs +1 -1
  188. package/scripts/release-notes-lint.mjs +1 -1
  189. package/scripts/release-publish-guard.mjs +1 -1
  190. package/scripts/release-version-pr-checks.mjs +2 -2
  191. package/scripts/release-version.mjs +61 -5
  192. package/scripts/render-brand-banner.mjs +1 -1
  193. package/scripts/render-check.mjs +2 -2
  194. package/scripts/repo-writes.mjs +1 -1
  195. package/scripts/report-frame-check.mjs +1 -1
  196. package/scripts/report-screenshot.mjs +2 -2
  197. package/scripts/retire-bare-refs.mjs +1 -1
  198. package/scripts/revisit-render-check.mjs +1 -1
  199. package/scripts/score.mjs +1 -1
  200. package/scripts/strip-titles-and-attributions.mjs +389 -0
  201. package/scripts/strip-tracker-citations.mjs +122 -5
  202. package/scripts/test-run.mjs +4 -4
  203. package/scripts/third-party-notices.mjs +1 -1
  204. package/scripts/verify-publishable.mjs +1 -1
  205. package/shared/access-audience.mjs +2 -2
  206. package/shared/anon-overlay.mjs +1 -1
  207. package/shared/brand.mjs +15 -1
  208. package/shared/bundle-freshness.mjs +1 -1
  209. package/shared/bundle-rebuild.mjs +1 -1
  210. package/shared/checkout-move.mjs +2 -2
  211. package/shared/client-door.mjs +8 -8
  212. package/shared/connect-clients.mjs +7 -7
  213. package/shared/connector-signin-probe.mjs +1 -1
  214. package/shared/env-aliases.mjs +1 -1
  215. package/shared/env-local.mjs +5 -5
  216. package/shared/grants-edit.mjs +76 -0
  217. package/shared/install-auth.mjs +1 -1
  218. package/shared/listen.mjs +3 -3
  219. package/shared/mcp-challenge.mjs +1 -1
  220. package/shared/names-in-force.mjs +2 -1
  221. package/shared/onboarding-store.mjs +19 -2
  222. package/shared/reference-guard-classes.mjs +44 -2
  223. package/shared/register-selection.mjs +1 -1
  224. package/shared/scope.mjs +223 -56
  225. package/shared/secret-file.mjs +1 -1
  226. package/shared/server-units.mjs +1 -1
  227. package/shared/staff-domain.mjs +45 -78
  228. package/shared/summary-blocks.mjs +2 -2
  229. package/shared/systemd-failure.mjs +3 -3
  230. package/shared/tracked-files.mjs +1 -1
  231. package/shared/trigger-lane.mjs +1 -1
  232. package/shared/tty-style.mjs +1 -1
  233. package/shared/usage-block.mjs +1 -1
  234. package/shared/vacuous-pass.mjs +1 -1
  235. package/shared/verb-shim.mjs +1 -1
@@ -188,7 +188,7 @@ html.anon-on [data-anon='mark']:not(.anon-clear) {
188
188
  max-width: var(--screen-measure);
189
189
  }
190
190
 
191
- /* ── THE WIDTH VOCABULARY (tracker issue 2086) ────────────────────────────────────────────────────
191
+ /* ── THE WIDTH VOCABULARY ─────────────────────────────────────────────────────────────────────────
192
192
  Owner, on his own screens: "we INCONSISTENTLY use screen width … consistency is important."
193
193
  Measured: seventeen inline maxWidth literals across seven screens, in nine different values — each
194
194
  its author's guess. Width is now a zone-type decision made HERE, once:
@@ -437,7 +437,7 @@ table.data td {
437
437
  vertical-align: top;
438
438
  }
439
439
 
440
- /* tracker issue 276 — THE GRID IS DECLARED, NOT EMERGENT.
440
+ /* THE GRID IS DECLARED, NOT EMERGENT.
441
441
  Without `table-layout: fixed` a table's columns are sized by their content, so every cell in every row
442
442
  is a vote on where the columns sit. That is why opening a row on Clearances could re-flow the parent
443
443
  table and truncate the `Updated` header to `Upd`, and why the alignment of an expanded read was
@@ -449,7 +449,7 @@ table.data.fixed {
449
449
  table-layout: fixed;
450
450
  }
451
451
 
452
- /* tracker issue 280 — THE FIXED TABLE HAS NO FLOOR OF ITS OWN.
452
+ /* THE FIXED TABLE HAS NO FLOOR OF ITS OWN.
453
453
  `table.data` sets `min-width: 760px` so a content-sized table cannot collapse into unreadable slivers.
454
454
  Under fixed layout that floor is not a safety net, it is the thing that overflows: the columns are
455
455
  already declared as percentages of the container, so the table has a shape at any width, and a floor
@@ -460,7 +460,7 @@ table.data.fixed {
460
460
  min-width: 0;
461
461
  }
462
462
 
463
- /* tracker issue 280 — THE HEADER MUST BE ALLOWED TO WRAP, or the font decides whether the table scrolls.
463
+ /* THE HEADER MUST BE ALLOWED TO WRAP, or the font decides whether the table scrolls.
464
464
  `table.data th` sets `white-space: nowrap`, which is right for a content-sized table and wrong here:
465
465
  under fixed layout the cells can shrink (overflow-wrap: anywhere) but the header cannot, so the
466
466
  letterspaced caps alone set an unshrinkable floor. When the brand webfont is unavailable — no network,
@@ -509,7 +509,7 @@ table.data tr.read-row > td {
509
509
  background: var(--surface-sunken);
510
510
  }
511
511
 
512
- /* tracker issue 278 — the whole read row opens its report.
512
+ /* the whole read row opens its report.
513
513
  The affordance is on the thing you can click, across the WHOLE row, which is the point: the only way
514
514
  in used to be a text button at the far right edge, furthest from the run title the reader has just
515
515
  read, on a row with `cursor: auto` and no hover state at all. People concluded there was no report.
@@ -548,7 +548,7 @@ table.data tr.row {
548
548
  cursor: pointer;
549
549
  }
550
550
 
551
- /* tracker issue 284 — the row disclosure is a real button.
551
+ /* the row disclosure is a real button.
552
552
  It was a bare span with aria-hidden, inside a <tr onClick>: silent to a screen reader AND with no
553
553
  keyboard path to open a row at all. The button carries the name and the state; these rules make it
554
554
  look exactly as the span did, so the fix is behavioural rather than visual.
@@ -577,7 +577,7 @@ button.twisty:focus-visible {
577
577
  outline-offset: 1px;
578
578
  }
579
579
 
580
- /* tracker issue 281 — the grouping toggle. In the toolbar, beside the other view controls, so its state is visible
580
+ /* the grouping toggle. In the toolbar, beside the other view controls, so its state is visible
581
581
  WITHOUT opening a menu: a toggle whose state you have to go looking for is a toggle that explains
582
582
  nothing about why the sort looked wrong. */
583
583
  .group-toggle {
@@ -595,7 +595,7 @@ table.data tr.row:hover td {
595
595
  background: var(--surface-sunken);
596
596
  }
597
597
 
598
- /* tracker issue 277 — A SECTION HEADER, not a decorative divider.
598
+ /* A SECTION HEADER, not a decorative divider.
599
599
  Brand owner is the organising principle of the whole list, and a reader had to work it out: the name
600
600
  was set in the smallest, lowest-contrast, most letterspaced type on the page, which reads as a rule
601
601
  BETWEEN rows rather than as "everything below this belongs to Foxglade Interactive".
@@ -747,7 +747,7 @@ table.data tr.group-head ~ tr.row > td:first-child {
747
747
  border-left-color: var(--border-strong);
748
748
  }
749
749
 
750
- /* tracker issue 282 — A TRANSIENT SURFACE, which this codebase did not have one of.
750
+ /* A TRANSIENT SURFACE, which this codebase did not have one of.
751
751
  The selection bar used `.notice`. That treatment is not a one-off: it is what the error and empty
752
752
  states on the same screen use — which is exactly why it was wrong here. A notice is a persistent
753
753
  thing you READ; a selection bar is a transient thing you ACT on, and it appears and vanishes as you
@@ -773,7 +773,7 @@ table.data tr.group-head ~ tr.row > td:first-child {
773
773
  box-shadow: 0 8px 28px rgba(0, 0, 0, 0.18);
774
774
  }
775
775
 
776
- /* tracker issue 282 — THE CHECKBOX, in the palette, in every state.
776
+ /* THE CHECKBOX, in the palette, in every state.
777
777
  It rendered in the browser default blue: the only blue on an otherwise warm and maroon page, and the
778
778
  single clearest sign that a control had been dropped in and never dressed.
779
779
  `accent-color` handles unchecked and checked in one property. Focus and disabled are separate because
@@ -965,11 +965,11 @@ input.filter {
965
965
  font-style: italic;
966
966
  }
967
967
 
968
- /* tracker issue 2061 — three labelled rungs where there used to be one line. The requirement is that a
968
+ /* three labelled rungs where there used to be one line. The requirement is that a
969
969
  reader can tell the legal read from the consequences AT A GLANCE, so the label is its own strong
970
970
  column rather than a lead-in sentence: three sentences run together in the old single-line cell would
971
971
  be more text and no more legible, which is the failure this replaces. */
972
- /* ONE grid for ALL rungs, not one per row (tracker issue 2085): each row being its own grid sized
972
+ /* ONE grid for ALL rungs, not one per row: each row being its own grid sized
973
973
  its label column with max-content PER ROW, so the text column started at a different x on every
974
974
  line and a long qualifier ("(one or more)") deformed its own row's grid — the owner's screenshot.
975
975
  The rows become display:contents so every label shares a single column, sized to the longest label
@@ -998,7 +998,7 @@ input.filter {
998
998
  }
999
999
 
1000
1000
  .footer-demo-note {
1001
- max-width: 420px; /* tracker issue 2086 — the note's width is the note's own, not its call site's */
1001
+ max-width: 420px; /* the note's width is the note's own, not its call site's */
1002
1002
  }
1003
1003
 
1004
1004
  /* The line beside the primary action: what happens next when it is available, and why it is not when
@@ -1168,7 +1168,7 @@ input.filter {
1168
1168
  max-height: 230px;
1169
1169
  }
1170
1170
 
1171
- /* ── Refs tracker issue 2075 — a territory the wired register does not reach ────────────────────────
1171
+ /* ── a territory the wired register does not reach ────────────────────────
1172
1172
  Shown and selectable, never absent: an absent entry teaches a reader nothing, because they cannot
1173
1173
  tell an unsupported territory from one they mistyped. The marker is a statement, not a warning — the
1174
1174
  choice is legitimate and the report discloses it. */
@@ -1257,6 +1257,18 @@ input.filter {
1257
1257
  line-height: 1.5;
1258
1258
  }
1259
1259
 
1260
+ /* The house account, in the pick panel: first in the list and marked as the one you can always run
1261
+ under. The wash rather than a chip or a badge — it is not a warning about the entry, it is the entry
1262
+ a reader should land on when they have no reason to pick another. */
1263
+ .pick-generic {
1264
+ background: var(--accent-wash);
1265
+ border-color: var(--accent-ink);
1266
+ }
1267
+
1268
+ .pick-generic .entry-title {
1269
+ color: var(--text-accent);
1270
+ }
1271
+
1260
1272
  .start-pill {
1261
1273
  padding: 8px 14px;
1262
1274
  border: 1px solid var(--border-strong);
@@ -1312,7 +1324,7 @@ input.filter {
1312
1324
 
1313
1325
  /* The column the wizard's steps live in. 720px, the design's measure for a form — NOT the footer's,
1314
1326
  which spans the screen. */
1315
- /* ── Refs tracker issue 1937 §B — THE MEASURE MOVES OFF THE COLUMN AND ONTO ITS BLOCKS ─────────────
1327
+ /* ── §B — THE MEASURE MOVES OFF THE COLUMN AND ONTO ITS BLOCKS ────────────────────────────────────
1316
1328
  The owner: the New clearance page "is bound to an artificially narrow width; the table side-scrolls
1317
1329
  or wraps and looks worse than its content deserves. The top panel already fits the screen width."
1318
1330
  720px is the right measure for a COLUMN OF FIELDS and the wrong one for a five-column comparison. So
@@ -1340,7 +1352,7 @@ input.filter {
1340
1352
  max-width: none;
1341
1353
  }
1342
1354
 
1343
- /* ── Refs tracker issue 1937 §B — "what this search includes", in or out at a glance ────────────────
1355
+ /* ── §B — "what this search includes", in or out at a glance ─────────────────────────────────────────
1344
1356
  The list read as three sentences of equal weight, and whether an axis ran at all was buried in the
1345
1357
  clause after the dash. The tick and the cross carry the answer; the colour is a reinforcement, never
1346
1358
  the only signal, because a marker distinguishable by hue alone is not a marker for every reader. */
@@ -1371,7 +1383,7 @@ input.filter {
1371
1383
  .carries-out .carries-mark { color: var(--text-faint); }
1372
1384
  .carries-out { color: var(--text-faint); }
1373
1385
 
1374
- /* Refs tracker issue 2144 — THE SELECTOR'S DEPTH ICONS ARE GONE, by the owner's ruling of
1386
+ /* THE SELECTOR'S DEPTH ICONS ARE GONE, by the owner's ruling of
1375
1387
  2026-09-03: "in the summary things rescale and work great. in product selector … we can remove
1376
1388
  these next to the product description, tiny fixed icons. barely visible." The ladder they drew was
1377
1389
  correct and unreadable at the size the row gave it. The FINAL SUMMARY's effort bars stay exactly as
@@ -1442,7 +1454,7 @@ input.filter {
1442
1454
  background: var(--accent-wash);
1443
1455
  }
1444
1456
 
1445
- /* ── Refs tracker issue 2075 — A ROW THAT CANNOT BE PICKED HAS TO LOOK LIKE ONE ─────────────────────
1457
+ /* ── A ROW THAT CANNOT BE PICKED HAS TO LOOK LIKE ONE ─────────────────────
1446
1458
  The class was applied by the component and NO RULE EXISTED for it, so a disabled row rendered
1447
1459
  identically to a live one: same border, same surface, same hover. That is exactly what the owner
1448
1460
  reported — "it doesnt appear disabled, no message etc - but i cant select it."
@@ -1481,7 +1493,7 @@ input.filter {
1481
1493
  color: var(--text-muted);
1482
1494
  }
1483
1495
 
1484
- /* ── Refs tracker issue 2087 — a lane this search needs and this deployment does not have ───────────
1496
+ /* ── a lane this search needs and this deployment does not have ───────────
1485
1497
  Louder than the coverage note beside it: coverage narrows what a report covers, this removes a whole
1486
1498
  section of the reasoning. Still not a refusal — the search runs and the report states the gap. */
1487
1499
  .pick-row-capability {
@@ -1837,7 +1849,7 @@ input.filter {
1837
1849
  border-bottom: 1px solid var(--border-hairline);
1838
1850
  }
1839
1851
 
1840
- /* ── Refs tracker issue 2076 — the two trades, side by side at the press ────────────────────────────
1852
+ /* ── the two trades, side by side at the press ────────────────────────────
1841
1853
  Each option is a BLOCK a reader can read before clicking, not a button with a word on it: the whole
1842
1854
  point of the question is that the two cost different things, and a row of verbs would hide that.
1843
1855
  The safe one is first and plain; "Stop now" is marked but not alarmed — the owner asked for it
@@ -2025,7 +2037,7 @@ input.filter {
2025
2037
  white-space: nowrap; cursor: pointer;
2026
2038
  }
2027
2039
  .home2-stop:hover, .home2-cancel:hover, .home2-ack:hover { border-color: var(--border-strong); color: var(--text-body); }
2028
- /* tracker issue 1986 — the honest face of a Stop this deployment cannot serve. NOT home2-stop: the
2040
+ /* the honest face of a Stop this deployment cannot serve. NOT home2-stop: the
2029
2041
  browser check counts that class as a Stop that can act. */
2030
2042
  .home2-stop-unavailable {
2031
2043
  font-size: 12px;
@@ -2037,7 +2049,7 @@ input.filter {
2037
2049
  cursor: not-allowed;
2038
2050
  }
2039
2051
 
2040
- /* tracker issue 2076 — the sentence that replaces the Stop button while a stop is in flight. */
2052
+ /* the sentence that replaces the Stop button while a stop is in flight. */
2041
2053
  .home2-stopping {
2042
2054
  font-size: 12px;
2043
2055
  color: var(--text-muted);
@@ -2045,7 +2057,7 @@ input.filter {
2045
2057
  }
2046
2058
  .home2-stop:disabled, .home2-cancel:disabled, .home2-ack:disabled { opacity: 0.55; cursor: default; }
2047
2059
 
2048
- /* ── tracker issue 613: acknowledged, behind a count ──────────────────────────────────────────────────────────
2060
+ /* ── acknowledged, behind a count ──────────────────────────────────────────────────────────
2049
2061
  Quiet on purpose. These are runs the reader has already dealt with, and the whole point of the
2050
2062
  feature is that they stop competing with live work for attention — a panel styled like the queue
2051
2063
  would put them back in the fight it just took them out of. */
@@ -2158,7 +2170,7 @@ input.filter {
2158
2170
  .home2-qowner, .home2-qdepth { display: none; }
2159
2171
  }
2160
2172
 
2161
- /* ── Refs tracker issue 2143 — THE USE-YOUR-AI PAGE ─────────────────────────────────────────────────
2173
+ /* ── THE USE-YOUR-AI PAGE ─────────────────────────────────────────────────
2162
2174
  The owner walked this page against the standing test — "I am a lawyer, only slightly technical" —
2163
2175
  and failed it. Four of the seven defects he listed were layout, and one of them was one word: the
2164
2176
  buttons carried `className="btn"`, and there is no `.btn` in this file. They rendered as bare
@@ -2170,7 +2182,7 @@ input.filter {
2170
2182
  the wrong one for a page of controls: the buttons stacked one per row down a 640px column with two
2171
2183
  thirds of the frame empty beside them. The text keeps a readable measure (`--screen-measure`, the
2172
2184
  vocabulary's own, not a ninth invented number) and the buttons take the frame and wrap. No new width
2173
- is declared here, deliberately — tracker issue 2086 rules that a new one needs its own reason, and
2185
+ is declared here, deliberately — the width vocabulary rules that a new one needs its own reason, and
2174
2186
  this page did not need one, it needed to stop using the narrowest one for everything. */
2175
2187
  .ai-screen h2 {
2176
2188
  font-size: 17px;
@@ -2190,7 +2202,7 @@ input.filter {
2190
2202
 
2191
2203
  .ai-connect-head { margin-top: 28px; }
2192
2204
 
2193
- /* ── ONE QUESTION, WHEN THE DEPLOYMENT LEAVES IT OPEN (tracker issue 147) ─────────────────────────── */
2205
+ /* ── ONE QUESTION, WHEN THE DEPLOYMENT LEAVES IT OPEN ─────────────────────────── */
2194
2206
  .ai-where { margin: 0 0 14px; max-width: var(--screen-measure); }
2195
2207
 
2196
2208
  .ai-where-q { margin: 0 0 8px; font-size: 15px; color: var(--text-strong); }
@@ -2333,3 +2345,44 @@ input.filter {
2333
2345
  .ai-help-body p { margin: 0 0 10px; }
2334
2346
 
2335
2347
  .ai-help-doc { font-size: 13px; }
2348
+
2349
+ /* ── Give someone access: the points on the tree a new person can be given ─────────────────────────────
2350
+ One row per point. A BUTTON, because choosing a point is an action and a keyboard has to reach it; the
2351
+ resets below are what make a button read as a row. `.sub` indents a company under the organisation
2352
+ it sits in. A row covered by a wider choice above it — everything, or its whole organisation — is
2353
+ disabled and drawn on, never unticked on its own. */
2354
+ .attach-row {
2355
+ display: flex;
2356
+ align-items: center;
2357
+ gap: 11px;
2358
+ width: 100%;
2359
+ padding: 9px 12px;
2360
+ border: 1px solid var(--border-hairline);
2361
+ border-radius: 8px;
2362
+ background: var(--surface-raised);
2363
+ color: inherit;
2364
+ font: inherit;
2365
+ font-size: 13.5px;
2366
+ text-align: left;
2367
+ cursor: pointer;
2368
+ }
2369
+
2370
+ .attach-row.on {
2371
+ border-color: var(--accent);
2372
+ background: var(--accent-wash);
2373
+ }
2374
+
2375
+ .attach-row.sub {
2376
+ width: auto;
2377
+ margin-left: 26px;
2378
+ }
2379
+
2380
+ .attach-row:disabled {
2381
+ cursor: default;
2382
+ }
2383
+
2384
+ .attach-row .sub-t {
2385
+ margin-left: auto;
2386
+ color: var(--text-faint);
2387
+ font-size: 12px;
2388
+ }
@@ -49,8 +49,8 @@
49
49
  -->
50
50
  <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
51
51
  <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
52
- <script type="module" crossorigin src="/portal/assets/index-KFAHMgdT.js"></script>
53
- <link rel="stylesheet" crossorigin href="/portal/assets/index-1ziUJX1E.css">
52
+ <script type="module" crossorigin src="/portal/assets/index-CWTHP0sH.js"></script>
53
+ <link rel="stylesheet" crossorigin href="/portal/assets/index-KpytsmNH.css">
54
54
  </head>
55
55
  <body>
56
56
  <div id="root"></div>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.2.4",
5
+ "version": "0.3.0-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": {
@@ -27,8 +27,8 @@
27
27
  //
28
28
  // `proven` · `failed` (with the provider's own reason) · `cannot-prove` (with why).
29
29
  //
30
- // Reporting a provider that CANNOT be probed as `failed` is the could-not-look defect that tracker
31
- // issues 1864 and 1874 both closed this week — a check that could not run reported as a check that
30
+ // Reporting a provider that CANNOT be probed as `failed` is the could-not-look defect that two
31
+ // separate filings closed this week — a check that could not run reported as a check that
32
32
  // found something wrong. The count kernel beside this file sets the precedent: it returns `total: null`
33
33
  // with a reason rather than a zero it did not count.
34
34
  //
@@ -42,6 +42,9 @@
42
42
  // call — so it gets no arm in CI, and ships with a green that means nothing. Which is precisely the
43
43
  // class of instrument this issue exists to complain about.
44
44
 
45
+ import { join } from "node:path";
46
+ import { pathToFileURL } from "node:url";
47
+
45
48
  // The marks are DATA, overridable, and each says why it was chosen. A control that stops returning hits
46
49
  // turns this probe into a permanent false alarm, so the reason has to survive for whoever re-picks it.
47
50
  export const DEFAULT_CONTROLS = Object.freeze({
@@ -79,7 +82,10 @@ export function probeSpend(capabilities) {
79
82
  export async function loadProviderCapabilities(repoRoot, providerId, importer = (u) => import(u)) {
80
83
  if (!providerId) return null;
81
84
  try {
82
- const m = await importer(new URL(`file://${repoRoot}/providers/${providerId}/src/capabilities.js`).href);
85
+ // pathToFileURL, not a hand-built `file://${repoRoot}/…`: a URL reads everything after a # as a
86
+ // fragment and decodes a literal %xx, so under such a folder this read the wrong file and the lane's
87
+ // cost came back UNKNOWN. On Windows the hand-built form held only by the URL parser's leniency.
88
+ const m = await importer(pathToFileURL(join(repoRoot, "providers", providerId, "src", "capabilities.js")).href);
83
89
  return m?.CAPABILITIES ?? m?.default ?? null;
84
90
  } catch { return null; }
85
91
  }
@@ -41,7 +41,7 @@ export function subclassesFor(db, { country, term, niceClass }) {
41
41
  //
42
42
  // ── — THE 13TH EDITION GOVERNS THE CODE. CNIPA'S OWN WORDING STILL FINDS THE GOOD. ──────────
43
43
  //
44
- // Owner ruling, 2026-08-20: "13th edition. done." — option 2, accepting that the 12th's assignments
44
+ // Ruling, 2026-08-20: "13th edition. done." — option 2, accepting that the 12th's assignments
45
45
  // lose authority WHEREVER THE EDITIONS DISAGREE. That last clause is the whole design: disagreement is
46
46
  // the case being decided, and it is not the same case as "the 13th has never heard of this good".
47
47
  //
@@ -1,5 +1,9 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.0-beta.1
4
+
5
+ No changes in this release.
6
+
3
7
  ## 0.2.4
4
8
 
5
9
  ## 0.2.3
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.2.4",
3
+ "version": "0.3.0-beta.1",
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).",
@@ -7,7 +7,7 @@ Wants=network-online.target
7
7
  [Service]
8
8
  # Reads the OAuth cache at ~/.config/clawdi/oauth-mcp/courtlistener.json (no ~/.env needed).
9
9
  Environment=PATH=/usr/local/bin:/usr/bin:/bin
10
- # ── RESOLVED AT INSTALL (tracker issue 1876 option B) ────────────────────────────────────────
10
+ # ── RESOLVED AT INSTALL (option B) ───────────────────────────────────────────────────────────
11
11
  # `@CLEAROTRON_CHECKOUT_DIR@` is substituted by render-units.mjs when it writes the resolved copy.
12
12
  # This unit deliberately loads no EnvironmentFile — one would let ~/.env override the PATH set above —
13
13
  # so it cannot use the `${CLEAROTRON_CHECKOUT_DIR}` form the lane services use.
@@ -193,7 +193,7 @@ if (isEntrypoint(import.meta.url)) {
193
193
  const rows = driftRows(citationsIntoChanged(corpus, byBase, changed, (pth) => trackedSet.has(pth)), changed);
194
194
 
195
195
  if (argv.includes("--json")) { console.log(JSON.stringify({ range, changed: [...changed.keys()], rows }, null, 2)); process.exit(0); }
196
- console.log(`citation-drift-report (tracker issue 1950): ${range} (base ${base.slice(0, 8)})`);
196
+ console.log(`citation-drift-report: ${range} (base ${base.slice(0, 8)})`);
197
197
  console.log(` ${changed.size} modified file(s); ${rows.length} citation(s) affected`);
198
198
  if (!rows.length) {
199
199
  console.log(" ✓ no citation on the base points below a net insertion in a file this change touches.");
@@ -232,7 +232,7 @@ export function decidableHits(citations, lineCountOf, ships = sourceCrossesTheCu
232
232
  }
233
233
 
234
234
  /**
235
- * A FLOOR UNDER THE POPULATION, RELATIVE TO THIS TREE — owner ruling 2026-09-02, his words: "keep the
235
+ * A FLOOR UNDER THE POPULATION, RELATIVE TO THIS TREE — ruling 2026-09-02, his words: "keep the
236
236
  * verifier, kill the magic."
237
237
  *
238
238
  * The reason for a floor is unchanged and it is a good one: if the extractor breaks, every count goes to
@@ -633,7 +633,7 @@ const CALLS = /^\s*\(/;
633
633
  const OWN_NUMBER = /^\s*(?:at\s+\d|:\d)/;
634
634
 
635
635
  /** The symbol-shaped tokens adjoining a citation, in BOTH grammars: `cite SYM` and `SYM (cite)`. PURE. */
636
- // ── THE RATCHET: A NEWLY ADDED CITATION CARRIES A SYMBOL (tracker issue 125) ────────────────────────
636
+ // ── THE RATCHET: A NEWLY ADDED CITATION CARRIES A SYMBOL ────────────────────────────────────────────
637
637
  //
638
638
  // The blindness above is not closeable on the existing corpus. Of the line citations in this tree only a
639
639
  // minority name a symbol beside the number, and the rest are checked for EXISTENCE and nothing more — a
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // A test that drives a real command and expects that command's `.env` to configure it gets the BUILT-IN
10
10
  // DEFAULTS instead, silently, including default PORTS. Two independent mechanisms produce it and a drive
11
- // has to defeat both (tracker issue 204):
11
+ // has to defeat both:
12
12
  //
13
13
  // 1. `scripts/test-run.mjs` sets `CLEAROTRON_NO_ENV_FILE=1` for every child of the suite. Correct on
14
14
  // its own terms — no test should be configured by a file on the developer's box.
package/scripts/e2e.mjs CHANGED
@@ -1353,7 +1353,7 @@ function evalAssertion(a, runDir) {
1353
1353
  // carries no runId, and this assertion is reading that exact status.json.
1354
1354
  // ── — THIS IS A DELIVERY CONTRACT, AND A RUN THAT DID NOT DELIVER NEVER ENTERED IT ────────
1355
1355
  //
1356
- // Owner ruling, 2026-08-22, verbatim: "clean up the failed runs. they owe the client nothing."
1356
+ // Ruling, 2026-08-22, verbatim: "clean up the failed runs. they owe the client nothing."
1357
1357
  // (Recorded here because the rule this line encodes is a product
1358
1358
  // decision, not a harness preference.)
1359
1359
  //
@@ -1373,7 +1373,7 @@ function evalAssertion(a, runDir) {
1373
1373
  //
1374
1374
  // ── 2026-08-24, AND THIS SCOPE SURVIVED IT ────────────────────────────────────────────────────
1375
1375
  //
1376
- // A second owner ruling that day ordered failed runs' notification packets onto the same re-drop
1376
+ // A second ruling that day ordered failed runs' notification packets onto the same re-drop
1377
1377
  // cover as delivered ones, and `driver/outbox-backoff.mjs`'s `owedANotification` now says so. That
1378
1378
  // is NOT a reversal of the line below and the two must not be read as one rule: what a failed run
1379
1379
  // owes the CLIENT is still nothing, which is this assertion's subject; what it owes the REQUESTER
@@ -1502,7 +1502,7 @@ function evalAssertion(a, runDir) {
1502
1502
  //
1503
1503
  // Three states, not two: "cannot tell" is reported as itself rather than as breakage, because
1504
1504
  // systemctl --user needs XDG_RUNTIME_DIR and a plain `sudo -u <user> node …` does not set it.
1505
- // ── IT ASKS ABOUT THE WORKER FIRST, BECAUSE THE WORKER IS THE DRAIN (tracker issue 181) ────────────
1505
+ // ── IT ASKS ABOUT THE WORKER FIRST, BECAUSE THE WORKER IS THE DRAIN ────────────────────────────────
1506
1506
  //
1507
1507
  // This function knew only about `prelim-driver.timer` and `prelim-driver.path` — the two RETIRED units.
1508
1508
  // On a deployment drained by `clearotron-worker.service` it therefore found neither, concluded nothing
@@ -1908,7 +1908,7 @@ async function cmdRun(id) {
1908
1908
  console.log(`\n${queuedIds.length} job(s) queued across ${doors.length} door(s)${
1909
1909
  queuedIds.some((x) => !x) ? ` — ${queuedIds.filter((x) => !x).length} did not report an id` : ""
1910
1910
  }. The QUEUE is the source of truth for what is in flight; watch it, not this list.`);
1911
- // THE COMMAND IS PRINTED ONLY WHEN NO DRAINER HOLDS THIS QUEUE (tracker issue 181), and the unit it
1911
+ // THE COMMAND IS PRINTED ONLY WHEN NO DRAINER HOLDS THIS QUEUE, and the unit it
1912
1912
  // names is the one this deployment actually uses. It used to be printed on the strength of two RETIRED
1913
1913
  // units being inactive, which on a worker-drained box meant telling the operator to start a second
1914
1914
  // drainer beside a running one. Suggesting a second drainer is the failure here, not the absence of a
@@ -2200,7 +2200,7 @@ function queueOutcomes(ref, queueDir = QUEUE_DIR, knownRuns = []) {
2200
2200
  return out;
2201
2201
  }
2202
2202
 
2203
- // ── tracker issue 514 · HAS THIS ROUND FINISHED, AND IS THAT KNOWN? ───────────────────────────────────────────────
2203
+ // ── HAS THIS ROUND FINISHED, AND IS THAT KNOWN? ───────────────────────────────────────────────────────────────────
2204
2204
  //
2205
2205
  // THE ONLY PLACE SETTLEDNESS IS DECIDED. Both the launch pre-flight and `report`'s stamp call this, so
2206
2206
  // they can never disagree about whether the previous round is done — which is the disagreement that
@@ -244,7 +244,19 @@ export const SYSTEM_OWNED = new Set([
244
244
  // pushed back is right to have made this an explicit decision rather than an omission: they ARE read
245
245
  // by product code, and the only honest answers were a row or this list.
246
246
  "NO_COLOR", "FORCE_COLOR",
247
- // ── THE GITHUB ACTIONS RUNTIME (tracker issue 213) ──────────────────────────────────────────────
247
+ // ── WINDOWS SUBSYSTEM FOR LINUX ─────────────────────────────────────────────────────────────────
248
+ //
249
+ // Set by WSL itself on every session inside it, which is what makes them worth reading: `isWsl()` in
250
+ // bin/onboard.mjs tests both, because under WSL a Windows binary on PATH is not the engine and the
251
+ // installer has to say so. Nobody configures a Clearotron install by setting them, and an
252
+ // .env.example row would invite somebody to try — the same argument NO_COLOR gets above, and TERM
253
+ // before it.
254
+ //
255
+ // The alternative was a governance row, and it would have been a false claim about a document: the
256
+ // row would describe a decision this product does not make. Excluded rather than documented, which
257
+ // is what this list is for.
258
+ "WSL_DISTRO_NAME", "WSL_INTEROP",
259
+ // ── THE GITHUB ACTIONS RUNTIME ──────────────────────────────────────────────────────────────────
248
260
  //
249
261
  // `CI` above is already here for exactly this reason; these two arrived with the release scripts and
250
262
  // want the same answer. GitHub sets both INSIDE a workflow run — `GITHUB_OUTPUT` is the step-output
@@ -3,9 +3,9 @@
3
3
  //
4
4
  // headless-page.mjs — did the browser open the page we asked for, or something of its own?
5
5
  //
6
- // (and, since tracker issue 227 criteria 3-4, whether this box can draw what that page says)
6
+ // (and, since criteria 3-4, whether this box can draw what that page says)
7
7
  //
8
- // ── WHY THIS EXISTS (tracker issue 227) ─────────────────────────────────────────────────────────────
8
+ // ── WHY THIS EXISTS ─────────────────────────────────────────────────────────────────────────────────
9
9
  //
10
10
  // Seven scripts drive headless Chrome and none of them asked. They cannot ask the obvious way: Chrome is
11
11
  // launched with the file URL as a COMMAND-LINE ARGUMENT, so there is no navigation call whose response
@@ -80,7 +80,7 @@ export function pageVerdict({ href = "", expected = "", marker = null, markerNam
80
80
  + "captured is Chrome's interstitial, not the artefact — and that page carries an `<h1>`, a "
81
81
  + "`<title>` and a body, so a content check alone reads it as a success." };
82
82
  }
83
- // ── THE BROWSER'S START PAGE IS NOT A WRONG DOCUMENT (tracker issue 273) ────────────────────────
83
+ // ── THE BROWSER'S START PAGE IS NOT A WRONG DOCUMENT ────────────────────────────────────────────
84
84
  //
85
85
  // `about:blank` is what Chrome shows before it has navigated anywhere. Reaching the check below, it
86
86
  // compares unequal to the expected URL and was reported as `wrong-document` — "a redirect, a stale tab
@@ -131,7 +131,7 @@ export function pageVerdict({ href = "", expected = "", marker = null, markerNam
131
131
  export async function assertPageLoaded(evaluate, { expected, marker = null, markerName = "the page's own content", what = "this page", errorText = null,
132
132
  graceMs = NAVIGATION_GRACE_MS, pollMs = NAVIGATION_POLL_MS,
133
133
  sleep = (ms) => new Promise((r) => setTimeout(r, ms)), now = () => Date.now() } = {}) {
134
- // ── WAIT FOR THE BROWSER TO LEAVE ITS START PAGE, THEN JUDGE (tracker issue 273) ─────────────────
134
+ // ── WAIT FOR THE BROWSER TO LEAVE ITS START PAGE, THEN JUDGE ─────────────────────────────────────
135
135
  //
136
136
  // Three of these scripts launch Chrome with the URL as an argument and get no response to wait on, so
137
137
  // the first read of `location.href` can land before the browser has moved. On an idle box it never
@@ -204,7 +204,7 @@ export function chromeErrorPage(dom = "") {
204
204
  || /jstcache=|<body[^>]*\bid="?neterror"?/.test(t);
205
205
  }
206
206
 
207
- // ── CAN THIS BOX DRAW WHAT THE PAGE SAYS? (tracker issue 227, criteria 3 and 4) ──────────────────────
207
+ // ── CAN THIS BOX DRAW WHAT THE PAGE SAYS? (criteria 3 and 4) ─────────────────────────────────────────
208
208
  //
209
209
  // The default demo product is a full-country search, and its report carries the mark's native-script
210
210
  // renderings — ベンクリ, ベンコリ, ヴェンコリ. They are LOAD-BEARING: the verdict sentence reads "A live
@@ -85,6 +85,7 @@ import { mcpToolCall } from "../driver/portal-mcp-client.mjs";
85
85
  import { readFlagSnapshot, builtFor, isStale, postureDelta, PRODUCTION_POSTURE } from "../driver/flag-snapshot.mjs";
86
86
  import { policyFor, productAvailability, ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
87
87
  import { rosterVerdict } from "../driver/roster-verdict.mjs";
88
+ import { triggerCapGap, triggerCapWarning } from "../driver/trigger-cap.mjs"; // the portal's own sentence about a key's gap
88
89
  import { bundledDemoKeys } from "../driver/bundled-demos.mjs";
89
90
  import { unitsActiveVerdict } from "../driver/unit-state-verdict.mjs";
90
91
  import { deploymentBox } from "../shared/deployment-box.mjs"; // — extracted; one allowlist, two readers
@@ -517,10 +518,33 @@ try {
517
518
  try { bundledDemos = bundledDemoKeys({ profilesDir: BUNDLED_PROFILES_DIR }); }
518
519
  catch (e) { fail("roster resolves", `the bundled roster could not be derived, so #83 cannot be ruled out: ${e.message}`); }
519
520
 
521
+ // THE DOOR ANSWERED FOR THIS KEY, so the comparison is made within this key's cap. Its claims are read
522
+ // the way doctor and the brand-owner command read them. A key that cannot be read leaves the verdict
523
+ // unable to tell a narrowing from a fault, and it says so rather than guessing either way.
524
+ let caller = { readable: false, accounts: null };
525
+ try { caller = (await import("../driver/portal-service.mjs")).opsTokenPosture(OPS_TOKEN); }
526
+ catch { /* stays unreadable — the verdict reports what it could not compare */ }
520
527
  if (bundledDemos) {
521
- const { state, message } = rosterVerdict({ keys, onDisk, bundledDemos, expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
528
+ const { state, message } = rosterVerdict({ keys, onDisk, bundledDemos, caller,
529
+ expectDemos: process.env.CLEAROTRON_E2E_EXPECT_DEMO_ROSTER === "1" });
522
530
  ({ pass, fail, skip })[state]("roster resolves", message);
523
531
  }
532
+ // A COMPANY IN THE STORE THAT THIS KEY CANNOT START is its own finding, not a roster disagreement. A
533
+ // warning, not a failure: the portal re-takes its credential at the start of every call, so a Start for
534
+ // it is refused only when that re-mint fails. The sentence is the portal's own.
535
+ if (onDisk) {
536
+ if (!caller.readable)
537
+ skip("trigger key covers the roster", OPS_TOKEN
538
+ ? "the key's claims could not be read, so its cap was NOT compared with the store"
539
+ : "no trigger key was found, so there is no cap to compare — see \"trigger lane wired\"");
540
+ else {
541
+ const gap = triggerCapGap({ accounts: caller.accounts, roster: onDisk });
542
+ if (gap.uncovered.length) record("trigger key covers the roster", "warn", triggerCapWarning(gap));
543
+ else pass("trigger key covers the roster", gap.capped
544
+ ? `every company in the store (${onDisk.length}) is within the key's cap`
545
+ : "the key carries no account cap, so it covers every company");
546
+ }
547
+ }
524
548
  } catch (e) {
525
549
  fail("roster resolves", `list_profiles failed: ${e.message}`);
526
550
  }
@@ -562,7 +586,7 @@ try {
562
586
  fail("ops-MCP reachable", `${MCP_URL}: ${e.message}`);
563
587
  }
564
588
 
565
- // ── 3b. THE TRIGGER LANE, AS ITS OWN SURFACE (tracker issue 112) ─────────────────────────────────────
589
+ // ── 3b. THE TRIGGER LANE, AS ITS OWN SURFACE ─────────────────────────────────────────────────────────
566
590
  //
567
591
  // The lane the portal's Start button calls is `PORTAL_MCP_URL` + `PORTAL_OPS_TOKEN`, and NOTHING on
568
592
  // this box exercised it. On 2026-09-02 the owner could not start a run: every surface here was green,