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
@@ -66,7 +66,7 @@ let ENFORCER_SIGNALS = new Map(); // E6: registration uri (lowercase) → {aggr
66
66
  // inference and no receipt line renders ⇒ archived output stays byte-identical.
67
67
  let RECORD_ORIGIN = null;
68
68
  let RECORD_ORIGINS = null; // — the run's ALLOW-LIST, not one origin
69
- let RECORD_CITATION = null; // — what a card shows where a link cannot go (provider table)
69
+ let RECORD_CITATION = null, RECORD_LINKS = null; // — what a card shows where a link cannot go (provider table); the office's own page per fetched record (office-record-links.mjs)
70
70
  let PROVIDER_LABEL = null;
71
71
  // WP-receipts W3/W4 — senior-right rows by finding ordinal (from _driver/senior-rights.json via
72
72
  // publish). The open item is CODE-OWNED: a verdict-driving card whose senior right went unverified
@@ -872,7 +872,7 @@ function scopeSection(ranBucket, coverage, coverageJudgment, methodologyText, co
872
872
  // page had a register-index entry on it — a definition of a label the reader could not see. It renders
873
873
  // now only where such an entry does, and the remaining two say what "inferred" means in the words a
874
874
  // reader would use for it rather than in the renderer's.
875
- if (hasRecordSet || hasCards) parts.push(`<p class="scoperead" style="margin:0 0 4px;font-weight:600">Record provenance</p><p class="provnote" style="margin:0 0 6px;font-size:13px">${hasRecordSet ? 'Registration numbers on the cards were read from the register records. ' : ''}${hasIndexEntry ? 'A registration shown as a register-index entry was seen in the register index; its full record was not pulled. ' : ''}“Inferred” beside an owner’s likelihood to object means we judged it from what the owner sells and holds; we had no enforcement history to read.</p>`);
875
+ if (hasRecordSet || hasCards) parts.push(`<p class="scoperead" style="margin:0 0 4px;font-weight:600">Record provenance</p><p class="provnote" style="margin:0 0 6px;font-size:13px">${hasRecordSet ? 'Registration numbers on the cards were read from the register records. ' : ''}${officeLinkNote()}${hasIndexEntry ? 'A registration shown as a register-index entry was seen in the register index; its full record was not pulled. ' : ''}“Inferred” beside an owner’s likelihood to object means we judged it from what the owner sells and holds; we had no enforcement history to read.</p>`);
876
876
  // — this is the one part of §4 that does NOT fold. Same markup, same heading, same marker; it is
877
877
  // emitted beside the <details> instead of inside it, wrapped in the panel the only-you section already
878
878
  // uses so it reads as a region of the page rather than a stray heading.
@@ -1298,7 +1298,7 @@ function fullDetail(f, card, recordsByUri = new Map()) {
1298
1298
  // "file exists on disk" inference. Absent receipt (archived runs) renders nothing.
1299
1299
  const receiptTail = rec._receipt?.fetched_at
1300
1300
  ? ` · <i class="receipt">verified — ${esc(PROVIDER_LABEL || 'register')} record fetched ${esc(String(rec._receipt.fetched_at).slice(0, 10))}</i>` : '';
1301
- return `<li><b>${regUri(r.uri)}</b>${cls ? ` · Cl.${esc(cls)}` : ''}${status ? ` · ${esc(status)}` : ''}${tail ? ` (${esc(tail)})` : ''}${jur ? ` · ${esc(jur)}${sys ? ` <i class="jsys">(${sys})</i>` : ''}` : ''}${desigHtml}${prio}${receiptTail}</li>`;
1301
+ return `<li><b>${officeRecordCell(r.uri, regUri, RECORD_CITATION ? NO_LINK_NOTE[RECORD_CITATION] : null)}</b>${cls ? ` · Cl.${esc(cls)}` : ''}${status ? ` · ${esc(status)}` : ''}${tail ? ` (${esc(tail)})` : ''}${jur ? ` · ${esc(jur)}${sys ? ` <i class="jsys">(${sys})</i>` : ''}` : ''}${desigHtml}${prio}${receiptTail}</li>`;
1302
1302
  }
1303
1303
  // (2)+(3): a cited record with NO fetched body. NEVER-INVENT when the run has a record set (this URI was
1304
1304
  // not fetched ⇒ the model's fields are unconfirmed); also when there are no findings.json facts to show.
@@ -2072,7 +2072,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
2072
2072
  // The provider table's own answer, not a branch on a vendor name here: `workbook` (no register UI
2073
2073
  // exists — point at the artifact that carries the record) or `placeholder` (a UI exists, its
2074
2074
  // per-record URL is unknown — say so, and say it is unfinished). Unset ⇒ plain text, as before.
2075
- RECORD_CITATION = typeof opts.recordCitation === "string" ? opts.recordCitation : null;
2075
+ RECORD_CITATION = typeof opts.recordCitation === "string" ? opts.recordCitation : null; RECORD_LINKS = opts.recordLinks instanceof Map ? opts.recordLinks : null;
2076
2076
  PROVIDER_LABEL = opts.providerLabel ?? null; // WP-receipts W2
2077
2077
  SENIOR_RIGHTS = new Map((Array.isArray(opts.seniorRights) ? opts.seniorRights : []).map((r) => [r.ordinal, r])); // WP-receipts W3/W4
2078
2078
  // A1 — a review-KILLED finding (disposition "withdrawn") renders NOWHERE: not in the
@@ -2411,3 +2411,28 @@ if (isEntrypoint(import.meta.url)) {
2411
2411
  writeFileSync(out, renderHtml(parsed, findings, coverage, { runId: basename(reportPath), contextNotes, markAssessment, fourAnswers }));
2412
2412
  console.log(`rendered ${findings.length} findings + ${coverage.length} coverage areas + ${contextNotes.length} context notes -> ${out}`);
2413
2413
  }
2414
+
2415
+ // ── The office's own page for a fetched record, where the run's register publishes none of its own ─────
2416
+ // publish addresses each fetched record from its own numbers (office-record-links.mjs) and hands the map
2417
+ // in as `recordLinks`. Kept down here, below every line the rest of the tree cites by number.
2418
+ import { officeReasonSentences } from './office-record-links.mjs';
2419
+
2420
+ // The label is the office and the number, linked where the office has a page. The handle names the
2421
+ // vendor's record, not the office's, and a reader can look it up nowhere. With no link the number stands
2422
+ // on its own beside the workbook note, and Scope says once, per office, why it is not a link.
2423
+ function officeRecordCell(uri, regUri, note) {
2424
+ const l = RECORD_LINKS?.get(String(uri || '').toLowerCase());
2425
+ if (!l?.label) return regUri(uri);
2426
+ if (l.href) return `<a href="${esc(l.href)}" target="_blank" rel="noopener noreferrer">${esc(l.label)}</a>`;
2427
+ return note ? `${esc(l.label)}<span class="reg-nolink">${note}</span>` : esc(l.label);
2428
+ }
2429
+
2430
+ // What a linked registration number opens and, once per office, why the rest are cited by number. Said
2431
+ // in Scope rather than beside every registration, for the reason the placeholder note was removed: a
2432
+ // note repeated on every line reads as a broken report. Empty on every other run.
2433
+ function officeLinkNote() {
2434
+ if (!RECORD_LINKS) return '';
2435
+ const linked = [...RECORD_LINKS.values()].some((l) => l?.href);
2436
+ return (linked ? 'A registration number shown as a link opens the office’s own page for that record. ' : '')
2437
+ + officeReasonSentences(RECORD_LINKS).map((s) => `${esc(s)} `).join('');
2438
+ }
@@ -7,7 +7,7 @@
7
7
  // Two readers, one file (the knockout doctrine, as it stood): the assistant drafts client-facing mail
8
8
  // from it, and the portal's native-render path reads it instead of iframing baked HTML.
9
9
  //
10
- // THE TWO LANES DIVERGED ON 2026-09-07 AND THIS FILE DID NOT MOVE. The owner ruled (tracker issue 274)
10
+ // THE TWO LANES DIVERGED ON 2026-09-07 AND THIS FILE DID NOT MOVE. The owner ruled
11
11
  // that the KNOCKOUT report carries the reviewer's notes, on the ground that one report is read by the
12
12
  // person who ran Clearotron and holding material back confuses where data went; render-knockout.mjs
13
13
  // projects them under `reviewerNotes`. That ruling was about those notes on that lane. It did not touch
@@ -187,7 +187,7 @@ export function clearanceReportData({
187
187
  // filing-routine "(ordinary mechanics are not an ask) — same rule as the code-built only-you
188
188
  // section", and that cross-reference was accurate: the only-you section dropped it too, through
189
189
  // ADVISORY_TAG rather than a named filter. then wrote a watch group for "monitoring and
190
- // filing-routine" and neither copy was told. Owner ruling 2026-08-19: the kind renders.
190
+ // filing-routine" and neither copy was told. Ruling 2026-08-19: the kind renders.
191
191
  //
192
192
  // This surface is the one that matters most for it. Legacy runs are served as baked bytes, but NEW
193
193
  // runs render component-native from report-data.json — so a fix that reached only the markdown
@@ -105,7 +105,7 @@ export function frozenSamples(examplesDir) {
105
105
  */
106
106
  export async function seedPool({ pool, examplesDir, republish }) {
107
107
  const existing = poolRunIds(pool);
108
- // ── A POOL THAT ALREADY HOLDS SOMETHING IS TOPPED UP, NOT SKIPPED (tracker issue 277) ──────────────
108
+ // ── A POOL THAT ALREADY HOLDS SOMETHING IS TOPPED UP, NOT SKIPPED ──────────────────────────────────
109
109
  //
110
110
  // This used to return early on any non-empty pool. That was invisible while `demo/` shipped one child:
111
111
  // seeding one and seeding all were the same act. When the other three landed, every box seeded before
@@ -37,7 +37,7 @@
37
37
  h1.mark{font-weight:900;font-size:58px;line-height:1.0;letter-spacing:-.022em;margin:0 0 14px}
38
38
  .sub{font-size:17.5px;color:var(--slate);max-width:700px;line-height:1.5;margin:0 0 30px}
39
39
  .sub b{color:var(--ink);font-weight:700}
40
- /* tracker issue 470 — the hero caption folds to its first sentence; the remainder sits behind this disclosure.
40
+ /* the hero caption folds to its first sentence; the remainder sits behind this disclosure.
41
41
  The visible sentence loses its bottom margin (the fold row carries the spacing instead) so a folded
42
42
  caption occupies the same vertical run as an unfolded one. `details.sub-more` is styled here and NOT
43
43
  in the frozen renderer, and the existing @media print rule that forces every <details> open already
@@ -56,7 +56,7 @@
56
56
  .panel{background:var(--card);border:1px solid var(--line);border-radius:18px;box-shadow:var(--shadow)}
57
57
  .gauge{padding:22px 24px}
58
58
  .gauge .label{margin-bottom:18px}
59
- /* tracker issue 761 — the framework that owns the ticks, named where they are read. Normal case and un-tracked on
59
+ /* the framework that owns the ticks, named where they are read. Normal case and un-tracked on
60
60
  purpose: `.label` is an uppercase eyebrow, and a proper name in the eyebrow's own letterforms reads
61
61
  as a second heading rather than as the thing being cited. */
62
62
  .gauge .label .gauge-fw{display:block;margin-top:5px;font-size:11px;font-weight:600;letter-spacing:0;
@@ -167,7 +167,7 @@
167
167
  .pips.l3 i:nth-child(-n+3){background:var(--high)} .pips.l2 i:nth-child(-n+2){background:var(--med)} .pips.l1 i:nth-child(-n+1){background:var(--low)}
168
168
  .pips.g3 i{background:var(--clear)} .pips.g2 i:nth-child(-n+2){background:var(--clear)}
169
169
  .mv{font-size:11px;color:var(--slate);margin-top:6px;font-weight:500;line-height:1.3}
170
- /* tracker issue 762 D4 — the meter's SECOND position: how well the fact above it is evidenced. It is subordinate
170
+ /* D4 — the meter's SECOND position: how well the fact above it is evidenced. It is subordinate
171
171
  to .mv on purpose (lighter, smaller): the finding is the fact, the receipt is the footnote. It
172
172
  wraps inside its own 1fr grid track, so a long evidence clause costs height, never overflow. */
173
173
  .mev{font-size:10px;color:var(--faint);margin-top:3px;font-weight:500;line-height:1.3}
@@ -214,7 +214,7 @@
214
214
  details.method[open]>summary::after{content:"▴"}
215
215
  .method .mbody{padding:0 22px 20px;font-size:13px;color:var(--slate);line-height:1.62}
216
216
 
217
- /* Ask-your-AI launcher (PR tracker issue 207 addendum) */
217
+ /* Ask-your-AI launcher */
218
218
  .askai{position:relative;display:inline-block}
219
219
  .askai-pop{position:absolute;z-index:30;top:calc(100% + 8px);left:0;width:340px;max-width:88vw;background:var(--card);
220
220
  border:1px solid var(--line2);border-radius:14px;box-shadow:var(--shadow);padding:14px}
@@ -272,7 +272,7 @@
272
272
  .jchip.ww{font-family:inherit;letter-spacing:.03em;color:var(--clear-tx,#2e6b32);background:var(--clear-soft,#EAF3E6);border-color:transparent}
273
273
  .jnote{font-size:10.5px;color:var(--faint);line-height:1.4}
274
274
 
275
- /* spec-49 T4 retired the review-notes caveat body; tracker issue 265 retired the slim internal toolbar that
275
+ /* spec-49 T4 retired the review-notes caveat body; a later change retired the slim internal toolbar that
276
276
  survived it, because the Flag/Etch capture controls it hosted are gone. The .review rules stay:
277
277
  reports ALREADY DELIVERED are frozen bytes carrying that markup, and portal-report.mjs strips
278
278
  it at serve time against these selectors. */
@@ -320,7 +320,7 @@
320
320
  .gbody{padding:0 12px 8px}
321
321
  .rgroup .card{margin:0 0 10px} .rgroup .card:last-child{margin-bottom:4px}
322
322
 
323
- /* tracker issue 242 — a grouped reasoned negative. The shared ground is in the .fold-lead above the run of rows;
323
+ /* a grouped reasoned negative. The shared ground is in the .fold-lead above the run of rows;
324
324
  each row carries only what is its own, so the line has to stay ONE line's worth of weight: the mark
325
325
  and owner anchor it, the jurisdictions/classes sit quiet, and the finding's own sentence takes the
326
326
  remaining width and wraps rather than truncating. Denser than .rgroup because these are cleared
@@ -349,7 +349,7 @@
349
349
  own content was carried correctly and only the control leaked, which is why the commit that added
350
350
  the fold could check the half it thought about and ship the half it did not. */
351
351
  details.drill>summary,details.method>summary,details.sub-more>summary{display:none!important}
352
- /* tracker issue 470 made this line load-bearing for the document's CONTENT, not just its completeness. The
352
+ /* The caption fold made this line load-bearing for the document's CONTENT, not just its completeness. The
353
353
  legal/practical reads now live inside .drillbody, so without this rule the exported PDF would
354
354
  carry the one-line verdicts and none of the reasoning behind them. The export prints everything a
355
355
  reader could open — ruled on #470, deliberately unchanged. Ticked-card selection (.print-hidden)
@@ -412,7 +412,7 @@
412
412
  .fourans ul.fa-obstacles li{margin:0 0 4px;font-size:13px}
413
413
  @media (max-width:640px){.fourans .fa-row{flex-direction:column;gap:2px}.fourans .fa-k{flex:auto}}
414
414
 
415
- /* ── tracker issue 763 — the coverage ledger stops being the page's smallest print ─────────────────────────────
415
+ /* ── the coverage ledger stops being the page's smallest print ─────────────────────────────
416
416
  `details.scope` had NO screen rule at all: its summary rendered at the browser default, in the
417
417
  browser's default weight, under the browser's default triangle, at the foot of a document whose
418
418
  every other section carries a numbered spine header. The section that states what the search did
@@ -179,7 +179,7 @@ const FINDING_COLS = ['#', 'Conflicting mark', 'Owner', 'Country', 'Source', 'Cl
179
179
  'Status & key dates', 'Record retrieved?', 'Risk band', 'How we treated it',
180
180
  'Mark similarity', 'Goods proximity', 'Use', 'Enforcer', 'Link'];
181
181
 
182
- function findingRows(findings, fetchState) {
182
+ function findingRows(findings, fetchState, recordLinks = null) {
183
183
  return (findings || []).map(f => {
184
184
  const regs = f?.owner?.registrations || [];
185
185
  const ctx = !!f.isContextNote;
@@ -190,7 +190,7 @@ function findingRows(findings, fetchState) {
190
190
  'Country': f.owner?.country || '',
191
191
  'Source': sourceWord(f.source?.source_type || ''),
192
192
  'Class': classCell(f),
193
- 'Registration(s)': regs.length ? regs.map(r => regLabel(r.uri)).join('; ') : (ctx ? '—' : '— (common-law)'),
193
+ 'Registration(s)': regs.length ? regs.map(r => recordLinks?.get(String(r.uri || '').toLowerCase())?.label || regLabel(r.uri)).join('; ') : (ctx ? '—' : '— (common-law)'),
194
194
  'Status & key dates': statusDatesCell(f),
195
195
  'Record retrieved?': ctx ? 'n/a — noted only' : retrievedCell(f, fetchState),
196
196
  'Risk band': f.band || '—',
@@ -200,7 +200,7 @@ function findingRows(findings, fetchState) {
200
200
  'Goods proximity': ctx ? 'n/a' : meterCell(f.meters?.goods_proximity),
201
201
  'Use': ctx ? 'n/a' : meterCell(f.meters?.use),
202
202
  'Enforcer': ctx ? 'n/a' : meterCell(f.meters?.enforcer),
203
- 'Link': f.source?.resolved_link || '',
203
+ 'Link': f.source?.resolved_link || linkCellFor(regs, recordLinks), // the office's own page, or why there is none
204
204
  };
205
205
  });
206
206
  }
@@ -598,7 +598,7 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
598
598
  (row, _d, kept) => { if (kept.has('Field')) row.getCell('Field').font = { bold: true }; });
599
599
 
600
600
  // 2 · Findings — one row per conflict; registrations joined; provenance folded into the driver tags.
601
- addSheet(wb, 'Findings', FINDING_COLS, findingRows(findings, fetchState), (row, _d, kept) => {
601
+ addSheet(wb, 'Findings', FINDING_COLS, findingRows(findings, fetchState, contract?.recordLinks instanceof Map ? contract.recordLinks : null), (row, _d, kept) => {
602
602
  if (kept.has('Link')) {
603
603
  const link = row.getCell('Link');
604
604
  const url = String(link.value || '').trim();
@@ -629,7 +629,8 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
629
629
  // never a reason to drop the Excel. (A gate that deletes the artifact it was meant to protect is the bug
630
630
  // this replaced: a legitimately-sparse run — e.g. no bound Class — must still ship its workbook.)
631
631
  await wb.xlsx.writeFile(outPath);
632
- const gate = validateAudit(wb, { findings, coverage, coverageJudgment, verdict, registerOnly });
632
+ const gate = validateAudit(wb, { findings, coverage, coverageJudgment, verdict, registerOnly,
633
+ registerPublishesRecordPages: contract?.registerPublishesRecordPages ?? null });
633
634
  return {
634
635
  sheets: wb.worksheets.map(w => w.name),
635
636
  findings: findings.length,
@@ -643,7 +644,8 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
643
644
  * validateAudit — the build gate. Reads the assembled workbook back and asserts the spec's MUST-BE-TRUE list.
644
645
  * Returns { ok, violations[] }. Exported so tests assert the same contract the build enforces.
645
646
  */
646
- export function validateAudit(wb, { findings = [], coverage = [], coverageJudgment = null, verdict = null, registerOnly = false } = {}) {
647
+ export function validateAudit(wb, { findings = [], coverage = [], coverageJudgment = null, verdict = null, registerOnly = false,
648
+ registerPublishesRecordPages = null } = {}) {
647
649
  const v = [];
648
650
  const sheets = wb.worksheets.map(w => w.name);
649
651
 
@@ -722,6 +724,13 @@ export function validateAudit(wb, { findings = [], coverage = [], coverageJudgme
722
724
  if (!textAt(findWs.getRow(r), cMark0).trim()) ghost++;
723
725
  }
724
726
  if (ghost) v.push(`Findings has ${ghost} ghost/continuation row(s) with no mark (multi-registration owners must stay on one row)`);
727
+ // the ordinal joins a row back to its finding's typed source; the record identity is office + number
728
+ const cOrd = col(findWs, '#'), cReg = col(findWs, 'Registration(s)'), cCountry = col(findWs, 'Country');
729
+ // ordinal → the finding's OWN typed source_type. Never pattern-matched off the Source cell, which is
730
+ // display text: `sourceCell` composes it and a register row can read "EUIPO" or a vendor label.
731
+ const srcTypeByOrdinal = new Map((Array.isArray(findings) ? findings : [])
732
+ .filter((f) => f?.ordinal != null)
733
+ .map((f) => [String(f.ordinal), String(f?.source?.source_type ?? '')]));
725
734
  const cMark = col(findWs, 'Conflicting mark'), cSrc = col(findWs, 'Source'), cTreat = col(findWs, 'How we treated it'),
726
735
  cLink = col(findWs, 'Link'), cRet = col(findWs, 'Record retrieved?'),
727
736
  cMs = col(findWs, 'Mark similarity'), cGp = col(findWs, 'Goods proximity'), cUse = col(findWs, 'Use'), cEnf = col(findWs, 'Enforcer'),
@@ -737,9 +746,37 @@ export function validateAudit(wb, { findings = [], coverage = [], coverageJudgme
737
746
  if (!cellText(cBand).trim()) v.push(`Findings "${mark}": no band/treatment marker`);
738
747
  // link resolves — but a famous-neighbour context note is knowledge-cited with no fetched record, so it
739
748
  // legitimately carries no link (its row is exempt, exactly as it is from the four-driver check below).
749
+ //
750
+ // WHAT A READER CAN DO WITH THE CITATION, not whether a URL is present. Some registers publish no
751
+ // per-record page at all (`hasPublicRecordUrl: false`), and for those the engine's own publish step
752
+ // EMPTIES any absolute record link a model wrote, because constructing a per-record URL for a
753
+ // register that publishes none would be a fabricated citation on a legal deliverable. Demanding an
754
+ // http(s) link of such a row asked for the one thing the engine is forbidden to supply, and reported
755
+ // the whole register layer of a delivered workbook as defective: every register finding, every run,
756
+ // scaling with how many register hits the matter had. So on such a run the record's IDENTITY is the
757
+ // citation — the registration number and the office — and a row carrying it is followable.
758
+ //
759
+ // FAIL-CLOSED IN BOTH DIRECTIONS OF DOUBT. The exemption needs a register that positively declares
760
+ // it has no record pages (`false`, never `null` — a legacy run with no fetch receipts cannot say
761
+ // what its register publishes), the row's source type to come back `register*` from the findings
762
+ // themselves, AND the row to actually carry the record identity. If the ordinal join misses, or the
763
+ // identity columns are absent from this workbook, nothing is exempt and the row is reported exactly
764
+ // as before.
740
765
  const linkVal = cLink ? row.getCell(cLink).value : null;
741
766
  const href = linkVal && typeof linkVal === 'object' ? linkVal.hyperlink : linkVal;
742
- if (!isCtx && !/^https?:\/\//i.test(String(href ?? ''))) v.push(`Findings "${mark}": link does not resolve to http(s)`);
767
+ if (!isCtx && !/^https?:\/\//i.test(String(href ?? ''))) {
768
+ const isRegisterRow = String(srcTypeByOrdinal.get(cellText(cOrd).trim()) ?? '').startsWith('register');
769
+ // The identity has to be a real one. `Registration(s)` is never blank — findingRows writes the
770
+ // placeholder `— (common-law)` for a finding with no registrations at all — so a bare non-empty
771
+ // test would have exempted exactly the register row that cites nothing.
772
+ const regCell = cellText(cReg).trim();
773
+ const citesRecord = Boolean(regCell && !regCell.startsWith('—') && cellText(cCountry).trim());
774
+ if (registerPublishesRecordPages === false && isRegisterRow) {
775
+ // The register has no record page. The number and the office ARE the citation; without them the
776
+ // row cites nothing at all, which is a worse defect than a missing link and is named as itself.
777
+ if (!citesRecord) v.push(`Findings "${mark}": no record link, and no registration number + office either — nothing identifies the record`);
778
+ } else v.push(`Findings "${mark}": link does not resolve to http(s)`);
779
+ }
743
780
  // fetch-failed visibly unverified
744
781
  const ret = cellText(cRet);
745
782
  if (/not retrieved/i.test(ret) && !/unverified/i.test(ret) && !/n\/a/i.test(ret) && !/read/i.test(ret)) {
@@ -812,3 +849,8 @@ export function validateAudit(wb, { findings = [], coverage = [], coverageJudgme
812
849
 
813
850
  return { ok: v.length === 0, violations: v };
814
851
  }
852
+
853
+ // Where the run's register publishes no record pages of its own, the Findings sheet shows each
854
+ // registration's office and number and links the office's own page (office-record-links.mjs). Kept down
855
+ // here, below every line the rest of the tree cites by number.
856
+ import { linkCellFor } from './office-record-links.mjs';
@@ -66,7 +66,7 @@ import { drainPosture, CONTINUOUS } from "./drain-posture.mjs"; // one rule ab
66
66
  */
67
67
  export function queueWatchVerdict({ queueDirs, watched, unitPath, unitError = null, resolveError = null, timer = null, worker = null } = {}) {
68
68
  // ── THE POSTURE QUESTION COMES FIRST, because after the retirement it decides whether the rest of
69
- // this function is even asking about anything (; owner ruling 2026-08-26, restated
69
+ // this function is even asking about anything (; ruling 2026-08-26, restated
70
70
  // 2026-08-31). The arrival trigger and the timer were two halves of ONE posture. The worker drains
71
71
  // continuously under --watch, so on a box in that posture there is no arrival to trigger and no
72
72
  // schedule to wait for, and every finding below would be describing the absence of machinery this
@@ -77,7 +77,7 @@ export function queueWatchVerdict({ queueDirs, watched, unitPath, unitError = nu
77
77
  // permission error still skips — a privilege-limited read that answers "fine" is the exact failure
78
78
  // this family of checks exists to refuse, and letting a worker unit talk it into a pass would put
79
79
  // that failure back one door along.
80
- // ONE RULE, NOT A SECOND COPY OF IT (tracker issue 206). This predicate used to be spelled out here
80
+ // ONE RULE, NOT A SECOND COPY OF IT. This predicate used to be spelled out here
81
81
  // and nowhere else, and the drainer arm — deciding the same question about the same box — did not
82
82
  // read the units at all and applied the timer-era rule instead. Both arms now consult
83
83
  // drain-posture.mjs, so a box cannot be continuous for one of them and timer-shaped for the other.
@@ -250,7 +250,7 @@ const PORT = PORT_CHOICE.port;
250
250
  //
251
251
  // `makeAccessVerifier` has ALWAYS accepted issuer/jwksUrl/emailClaim — this service simply never
252
252
  // passed them, which is how one product shipped a provider-agnostic API face and three single-vendor
253
- // services beside it. Owner ruling 2026-08-23: "we can't launch with an identity vendor, they bring
253
+ // services beside it. Ruling 2026-08-23: "we can't launch with an identity vendor, they bring
254
254
  // their own… they pick their own."
255
255
  const OIDC_ISSUER = process.env.RECIPE_OIDC_ISSUER || "";
256
256
  const JWKS_URL = process.env.RECIPE_JWKS_URL || "";
@@ -829,7 +829,7 @@ export function silentlyLostFindings({ reconciliation = null, carryRows = null,
829
829
  /**
830
830
  * Positions the DIGEST ended as findings that did not reach the findings, dropped WITH a stated reason.
831
831
  *
832
- * ── THE SIBLING'S BLIND SPOT, AND IT IS THE ONE THAT REACHED A CLIENT (tracker issue 248) ──────────
832
+ * ── THE SIBLING'S BLIND SPOT, AND IT IS THE ONE THAT REACHED A CLIENT ──────────────────────────────
833
833
  *
834
834
  * `silentlyLostFindings` above is correct and must not be widened to cover this. Its population is
835
835
  * `step-silent` — a finding-ending followed by silence — and its own header records why that boundary
@@ -667,7 +667,7 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
667
667
  // what it saw. Auto-promoting a collision to `found` would be the scorer manufacturing recall from
668
668
  // its own confusion, which is the defect one layer up from the one being fixed. score.mjs prints
669
669
  // these; a reader adjudicates.
670
- // ── THE PREDICATE WAS THE OWNER, AND THE HEADING ABOVE SAYS RECORD (tracker issue 249) ──────────
670
+ // ── THE PREDICATE WAS THE OWNER, AND THE HEADING ABOVE SAYS RECORD ──────────────────────────────
671
671
  //
672
672
  // The only condition used to be `ownersMatch`. That implements a different class from the one the
673
673
  // paragraph above states, and the justification — "they cannot both be true" — does not hold for any
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
3
  //
4
- // THE TWO SIGNATURES THE REFERENCE STRIP LEFT BEHIND (tracker issue 185).
4
+ // THE TWO SIGNATURES THE REFERENCE STRIP LEFT BEHIND.
5
5
  //
6
6
  // The strip's job was to remove internal references from this repository before it went public, and it
7
7
  // did that. Where the reference was the SUBJECT of the sentence, it took the subject with it:
@@ -28,7 +28,7 @@
28
28
  // store-wide veto's GRANULARITY (refuse the scenario that does not fit, not every scenario beside
29
29
  // it) and the door not narrowing declared coverage. Whether a matter whose ENTIRE ordered scope is
30
30
  // unreachable on this box should start is a separate question and did not answer it. The
31
- // owner ruling of 2026-08-12 does: partial coverage discloses, empty coverage refuses by name
31
+ // ruling of 2026-08-12 does: partial coverage discloses, empty coverage refuses by name
32
32
  // before spend (reachableRegions below, and the two Depth 2 preflights that read it).
33
33
  // · resolveRegions would then classify the US as "outside the provider's coverage", which is a false
34
34
  // sentence about the product. The free tier covers the US. This install is not configured for it.
@@ -49,8 +49,9 @@
49
49
  // read it: coverage-form.mjs seeded its deferred rows from skeleton qids alone, so an EU+US matter on a
50
50
  // box with no index shipped an EU-only clean with no row saying the US was never searched. A false
51
51
  // clean, reached through the disclosure that was supposed to prevent one, and this sentence is what
52
- // made it look already-handled. driver.config.mjs:789 cited it as the reason preflight could stop
53
- // requiring USPTO_LOCAL_DB.
52
+ // made it look already-handled. driver.config.mjs freeTierMissing cited it as the reason preflight
53
+ // could stop requiring USPTO_LOCAL_DB. Cited by SYMBOL and no line: the number was 84 lines above that
54
+ // comment before anything moved, and a line citation into a file this size stales on the next edit.
54
55
  //
55
56
  // The split is still right and is unchanged. What had to be built is the consumer: coverage-form.mjs
56
57
  // now reads `plan.deferred_coverage` directly and emits an `open` row per active axis. The disclosure
@@ -19,7 +19,7 @@
19
19
  // 2. A COUNT WE COULD NOT TAKE IS NEVER ZERO. Every failure path carries `total: null` plus a reason,
20
20
  // and the surfaces render "not available". A provider that cannot count at all
21
21
  // (capabilities.countProbe "none") does not degrade to zeroes: the lane REFUSES before spending
22
- // anything (see countPreflight) — that is the owner ruling, and it is the difference between a
22
+ // anything (see countPreflight) — that is the ruling, and it is the difference between a
23
23
  // degraded answer and a wrong one.
24
24
  //
25
25
  // 3. THE NUMBER IS NEVER BANDED. No colour, no threshold, no "high/low". The moment a count carries a
@@ -187,7 +187,7 @@ export function countPreflight({ capabilities, jurisdictions, credentialPresent
187
187
  + `read as though it covered the ones ordered. Name a territory ${id} covers, or switch the register provider.`;
188
188
  }
189
189
  }
190
- // — EMPTY COVERAGE REFUSES EARLY; PARTIAL COVERAGE DISCLOSES (owner ruling, 2026-08-12).
190
+ // — EMPTY COVERAGE REFUSES EARLY; PARTIAL COVERAGE DISCLOSES (ruling, 2026-08-12).
191
191
  //
192
192
  // The check above is about the CONTRACT: territories the provider does not cover. This one is about
193
193
  // the BOX: territories it covers and this deployment cannot reach, because a member is unconfigured.
@@ -442,7 +442,7 @@ export async function countRegisterHits({
442
442
  // report of exactly that.
443
443
  ...(deferred.length ? { deferredScope: deferred.map((d) => d.jurisdiction) } : {}),
444
444
  // The generated set, recorded whether or not the aggregate landed: the report prints WHICH forms
445
- // were checked, and a number whose forms are not on the page is unauditable (owner ruling).
445
+ // were checked, and a number whose forms are not on the page is unauditable (ruling).
446
446
  variants: { cap: variantCap, generated: variants.generated, truncated: variants.truncated, forms: variants.forms.map((f) => f.form) },
447
447
  ...(reusedCells === COUNT_PREDICATES.length ? { reused: true } : {}),
448
448
  };
@@ -72,7 +72,7 @@ import { variantForms, VARIANT_CAP } from "./register-variants.mjs";
72
72
  import { recordOriginsFor } from "./record-origins.mjs"; // — provider-derived, never CLEAROTRON_DATABASE
73
73
 
74
74
  /** Records per MARK, across every listed term. A bound on cost, on artifact size and on what a reader
75
- * can usefully hold — and it is stated wherever the list renders, per the owner ruling. */
75
+ * can usefully hold — and it is stated wherever the list renders, per the ruling. */
76
76
  export const RECORD_CAP = 100;
77
77
 
78
78
  /** The floor on a single term's share of the cap. Without it a name generating twelve forms would give
@@ -165,7 +165,7 @@ export const REPAIR_COMPOSERS = [
165
165
  `If applying a flag genuinely REQUIRES changing a finding outside that list — a knock-on the reviewer did not see — make the change and say which one and why in the "corrections" note. A silent edit outside the scope is the one thing this instruction forbids; a reasoned one is not.`,
166
166
  ) : "";
167
167
  return lines(
168
- // — OWNER RULING 2026-08-09: "add caveat" is DELETED, not softened. A repair has exactly
168
+ // — RULING 2026-08-09: "add caveat" is DELETED, not softened. A repair has exactly
169
169
  // four moves and there is no fifth. The standing outward-language rule governs the REPAIR path
170
170
  // exactly as it governs a first draft, and this instruction was the one place the engine invited
171
171
  // a hedge into a deliverable. It fired on 4 of 4 delivered clearances in the 2026-08-09 round.
@@ -26,7 +26,7 @@ import { driverDir } from "../shared/driver-dir.mjs"; //
26
26
  // ── the terminal kind that is not a failure ───────────────────────────────────────────────
27
27
  //
28
28
  // A REFUSAL THE PRODUCT IS DESIGNED TO MAKE. The engine did not break: an order arrived that this
29
- // deployment does not serve, a preflight said so before any spend, and the run stopped. Owner ruling
29
+ // deployment does not serve, a preflight said so before any spend, and the run stopped. Ruling
30
30
  // 2026-08-13 — such a refusal must be distinguishable from a failure IN EVERY SINK IT REACHES, must
31
31
  // never feed failure statistics, and must never trigger recovery machinery.
32
32
  //
@@ -216,7 +216,7 @@ export function classifyFailureReason(reason) {
216
216
  // and a second derivation is a second answer waiting to disagree with the first.
217
217
  // ── BUILD A — WHERE THE CLASSIFICATION CAME FROM ────────────────────────────────────────────
218
218
  //
219
- // Owner ruling 2026-08-19: `classSource` becomes `validator-token` for a token the validator already
219
+ // Ruling 2026-08-19: `classSource` becomes `validator-token` for a token the validator already
220
220
  // named. Budgets identical, no new class value, no lane change — `decideRecovery` takes no
221
221
  // `classSource` argument at all, so this cannot reach a budget branch even by mistake.
222
222
  //
@@ -553,7 +553,7 @@ export function countRecoveryLanes(history, { total = 0 } = {}) {
553
553
  return { weather, defect: defect + shortfall };
554
554
  }
555
555
 
556
- // ── CAP PARKS: A PROVIDER SAYING "NOT YET" IS NOT A STAGE FAILING (tracker issue 103) ──────────────
556
+ // ── CAP PARKS: A PROVIDER SAYING "NOT YET" IS NOT A STAGE FAILING ──────────────────────────────────
557
557
  //
558
558
  // Owner, watching a run spend 4 of its 6 recovery parks against one subscription cap:
559
559
  //
@@ -218,7 +218,7 @@ export function diffSnapshots(prev, curr) {
218
218
  function main() {
219
219
  const args = new Set(process.argv.slice(2));
220
220
  // ON-DISK NAME, NOT A PRODUCT NAME: an install that never set the variable already has this file, so
221
- // renaming the default points the reader at one that does not exist. Owner ruling, tracker issue 308.
221
+ // renaming the default points the reader at one that does not exist. Ruling.
222
222
  const snapshotPath = process.env.CLEAROTRON_REPLAY_SNAPSHOT || join(homedir(), ".prelim-replay-snapshot.json");
223
223
  const roots = process.env.CLEAROTRON_REPLAY_ROOTS
224
224
  ? process.env.CLEAROTRON_REPLAY_ROOTS.split(":").filter(Boolean)
@@ -15,7 +15,7 @@
15
15
  //
16
16
  // ── WHAT MOVES, AND THE LINE THIS CONVERSION MUST NOT CROSS ────────────────────────────────────────
17
17
  //
18
- // OWNER RULING S2 (2026-08-13) re-scoped rather than adopting it: "the mechanical card fields
18
+ // RULING S2 (2026-08-13) re-scoped rather than adopting it: "the mechanical card fields
19
19
  // (headings, ids, links, driver-stamped values) move to code NOW — uncontested. The prose half is
20
20
  // decided by EVIDENCE: after config #40 lands, build one matter both ways and the owner reads the two
21
21
  // cards side by side. If the rendered card reads worse, the model pass stays, on typed inputs."
@@ -61,6 +61,11 @@ export const RESULT_NOUN_FIELDS = Object.freeze([
61
61
  why: "counts read off the coverage/placement unions" },
62
62
  { file: "driver/pipeline-knockout.mjs", noun: "recovered", sites: 1, atWriteSite: 1, verdict: "result",
63
63
  why: "written after the chunk files were actually removed" },
64
+ { file: "driver/pipeline-knockout.mjs", noun: "outcome", sites: 3, atWriteSite: 3, verdict: "result",
65
+ why: "the reviewing pass's own verdict on itself, read at each writing site: `nothing-flagged` after "
66
+ + "the measurement came back empty, `applied` after the rewrite passed the merged gate and was "
67
+ + "written, and the `keep()` forms after the artifact or the gate refused it. Every one is a fact "
68
+ + "about what already happened to the record, never the return of the call being logged" },
64
69
  { file: "driver/pipeline.mjs", noun: "closed", sites: 6, atWriteSite: 4, verdict: "result",
65
70
  why: "closure counts derived from the landed set (`qids.filter(q => landed.has(q)).length`) and from the receipt" },
66
71
  { file: "driver/pipeline.mjs", noun: "executed", sites: 4, atWriteSite: 3, verdict: "result",
@@ -29,9 +29,13 @@
29
29
  * @param {string[]|null} o.onDisk keys in the configured store, sorted — null when none is configured
30
30
  * @param {string[]} o.bundledDemos the roster shipped in the product repo, sorted
31
31
  * @param {boolean} o.expectDemos CLEAROTRON_E2E_EXPECT_DEMO_ROSTER=1 — "this instance is allowed to break"
32
+ * @param {{readable: boolean, accounts: string[]|null}} [o.caller]
33
+ * the claims of the key the door was asked with, in opsTokenPosture's
34
+ * shape. ABSENT, or `accounts: null`, means uncapped — every account — the
35
+ * way triggerCapGap reads a null cap. Never an empty one.
32
36
  * @returns {{state: "pass"|"fail"|"skip", message: string}}
33
37
  */
34
- export function rosterVerdict({ keys, onDisk, bundledDemos, expectDemos }) {
38
+ export function rosterVerdict({ keys, onDisk, bundledDemos, expectDemos, caller }) {
35
39
  const sameSet = (a, b) => a.length === b.length && a.every((k, i) => k === b[i]);
36
40
  const isBundled = sameSet(keys, bundledDemos);
37
41
 
@@ -40,17 +44,56 @@ export function rosterVerdict({ keys, onDisk, bundledDemos, expectDemos }) {
40
44
  return { state: "skip", message: "the session resolved zero accounts — this probe is unscoped for this door, so the roster is NOT probed rather than failed" };
41
45
 
42
46
  if (onDisk) {
43
- if (!sameSet(keys, onDisk))
44
- return { state: "fail", message: `the door sees ${keys.length} customer(s), the configured store holds ${onDisk.length} — they disagree`
45
- + (isBundled ? ". The door resolved exactly the bundled demo roster, so this is #83: CLEAROTRON_CUSTOMERS_DIR is not reaching the service" : "") };
47
+ // THE DOOR ANSWERS FOR THE KEY THAT ASKED. `list_profiles` is narrowed to the caller's account cap,
48
+ // so a key minted before a company existed does not list it, and correctly. Compared with the whole
49
+ // store, that read as the door and the store disagreeing, and the message below then pointed at a
50
+ // stale roster (measured in testing, 2026-09-10). So the store is narrowed by the same cap before the
51
+ // two are compared; a company outside the cap is the key's gap, which the caller reports on its own line.
52
+ const unreadable = caller?.readable === false;
53
+ const cap = !unreadable && Array.isArray(caller?.accounts) ? caller.accounts : null;
54
+ const expected = cap ? onDisk.filter((k) => cap.includes(k)) : onDisk;
55
+ if (!sameSet(keys, expected) && unreadable)
56
+ // A KEY WHOSE CLAIMS CANNOT BE READ may be narrowing the answer, and a narrowing looks exactly like a
57
+ // door that disagrees with its store. Neither passed nor failed: not compared, and said so.
58
+ return { state: "skip", message: `the door sees ${keys.length} customer(s) (${keys.join(", ")}), `
59
+ + `the configured store holds ${onDisk.length} (${onDisk.join(", ")}), and the claims of the key this `
60
+ + "check asked with could not be read — so a narrowing by that key cannot be told from a door that "
61
+ + "disagrees with its store. NOT compared" };
62
+ if (!sameSet(keys, expected))
63
+ return { state: "fail", message: `the door sees ${keys.length} customer(s) (${keys.join(", ")}), `
64
+ + `the configured store holds ${onDisk.length} (${onDisk.join(", ")})`
65
+ + (cap ? `, ${expected.length} of them within the cap of the key this check asked with` : "")
66
+ + " — they disagree"
67
+ // THE CAUSE IS NOT DERIVABLE FROM THIS COMPARISON, and it used to be asserted anyway. `isBundled`
68
+ // is a set equality over NAMES, and a configured store ordinarily CONTAINS the bundled demo
69
+ // names — so "the door resolved exactly the bundled roster" is equally true of a door with no
70
+ // store and of a door whose store simply has not gained the newest company yet. Measured
71
+ // 2026-09-09: a company added to the configured store minutes earlier, a door still holding the
72
+ // roster it read at boot, and this line reporting a variable that was reaching the service
73
+ // perfectly well. What the check can see is the two lists; which of the two causes produced them
74
+ // is a question for whoever reads it.
75
+ + (isBundled
76
+ ? ". The door's set is name-identical to the bundled demo roster, which this comparison cannot "
77
+ + "tell apart from a configured store carrying those same names: it is consistent BOTH with "
78
+ + "CLEAROTRON_CUSTOMERS_DIR not reaching the service AND with the door still holding a roster "
79
+ + "it read before the store changed. Check the door's environment and when it last read the store."
80
+ : "") };
46
81
  return { state: "pass", message: `${keys.length} customer(s), matching the configured store`
82
+ + (cap && expected.length < onDisk.length
83
+ ? ` within the cap of the key this check asked with (${expected.length} of ${onDisk.length})` : "")
47
84
  + (expectDemos ? " — and this instance declares itself a test box, which the store's own CI keeps free of real client bundles" : "") };
48
85
  }
49
86
 
50
87
  if (isBundled && expectDemos)
51
88
  return { state: "pass", message: `the bundled demo roster (${keys.join(", ")}) — correct for a test instance with no configured store, which must never see real client bundles` };
52
89
  if (isBundled)
53
- return { state: "fail", message: `the door resolved exactly the bundled demo roster (${keys.join(", ")}) — this is #83: CLEAROTRON_CUSTOMERS_DIR is not reaching the service, and every real customer will be refused` };
90
+ // NO STORE IS VISIBLE TO THIS PROCESS, so unlike the branch above there is no second list to print
91
+ // and the fallback reading is the likeliest one. It is still a reading: this process not holding
92
+ // CLEAROTRON_CUSTOMERS_DIR is not proof the service does not, and the comparison is over names a
93
+ // configured store would also carry. Named as the probable cause rather than the established one.
94
+ return { state: "fail", message: `the door resolved ${keys.length} account(s) name-identical to the bundled demo roster (${keys.join(", ")}), `
95
+ + `and no configured store is visible from here to compare against — most likely CLEAROTRON_CUSTOMERS_DIR is not reaching the service, `
96
+ + `in which case every real customer will be refused. Confirm at the door's own environment before acting: a store holding these same names would look identical here` };
54
97
  if (expectDemos)
55
98
  return { state: "fail", message: `CLEAROTRON_E2E_EXPECT_DEMO_ROSTER=1 says this is a test instance with no configured store, but the door resolved ${keys.length} NON-demo customers — real client config has reached an instance that must not have it` };
56
99
  return { state: "pass", message: `${keys.length} customers (no CLEAROTRON_CUSTOMERS_DIR in THIS process to compare against)` };
@@ -52,7 +52,7 @@ export function readActivity(ledgerPath, { limit = 50, now = Date.now(), staleAf
52
52
  if (!Number.isFinite(tsMs) || nowMs - tsMs <= staleAfterMs) return r; // fresh (or undatable) → leave as-is
53
53
  return { ...r, state: "failed", stale: true, note: r.note || "no terminal record — presumed interrupted (stale)" };
54
54
  };
55
- // Presentation retire (2026-07-06 owner steer — same reversible pattern as run rows /): the
55
+ // Presentation retire (2026-07-06 steer — same reversible pattern as run rows /): the
56
56
  // ledger stays append-only — APPEND a record with the same id carrying `retired: true` to hide the
57
57
  // row from every activity surface; append another without it to resurface. Forensics untouched.
58
58
  return [...byId.values()].filter((r) => r.retired !== true).map(reap).sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || ""))).slice(0, limit);