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
@@ -0,0 +1,189 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ // office-record-links.mjs — the page a trade mark office publishes for one record, addressed from the
4
+ // numbers on the record the run fetched.
5
+ //
6
+ // WHY THIS EXISTS. A register vendor with no per-record page of its own left every register finding with
7
+ // nothing a reader could open. The card showed the engine's handle, `/mark/<office>/<id>`, and the id in
8
+ // it is the vendor's, not the office's. Most offices do publish a page per record, keyed on their own
9
+ // application or registration number, and the fetched record carries that number. So the link is built
10
+ // here, in code, from a table of what each office's address takes: never by the model, and never from
11
+ // the handle.
12
+ //
13
+ // WHAT THIS DOES NOT TOUCH. The model's own record URLs are still refused for such a vendor: its
14
+ // allow-list (record-origins.mjs) stays empty, and normalizeRecordLinks still strips a host it does not
15
+ // publish. This is a second source of links with one input, the fetched record, and it only ever emits
16
+ // an address from the table below.
17
+ //
18
+ // A NUMBER THAT DOES NOT FIT GETS NO LINK. Each entry states the form its address takes, and a number in
19
+ // any other form is cited, not linked: a guessed address on a legal deliverable is worse than an admitted
20
+ // absence. The only reshaping is what the office's own format defines: Swiss application serials are
21
+ // five digits, EU application numbers nine, French numbers carry FR.
22
+ //
23
+ // Each address was checked on 2026-09-10 against a real record, and each row says how.
24
+
25
+ const pad = (s, n) => String(s).padStart(n, "0");
26
+ const clean = (v) => (v == null ? "" : String(v).trim());
27
+
28
+ /** The first of `values` matching `re`, handed to `build`; null when none does. */
29
+ function addressFrom(values, re, build) {
30
+ for (const v of values) {
31
+ const m = re.exec(clean(v));
32
+ if (m) return build(m);
33
+ }
34
+ return null;
35
+ }
36
+
37
+ // An international registration, at WIPO's Madrid Monitor. Opened a real record. WIPO has said it will
38
+ // retire Madrid Monitor once eMadrid covers everything it does, and has given no date.
39
+ const madridMonitor = (values) => addressFrom(values, /^0*([1-9]\d{4,6})$/, (m) => ({
40
+ number: m[1], href: `https://www3.wipo.int/madrid/monitor/en/showData.jsp?ID=ROM.${m[1]}` }));
41
+
42
+ /**
43
+ * One entry per office code the handle carries (`/mark/<office>/...`, lower case). `address(rec)` returns
44
+ * `{ number, href }` when the record's number is in the form the office's page takes, and null when it is
45
+ * not. An office with `address: null` publishes no page for a single record.
46
+ */
47
+ export const OFFICE_RECORD_PAGES = Object.freeze({
48
+ // Opened a real record. Australia reuses the trade mark number as the registration number.
49
+ au: { name: "Australia", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(\d{1,8})$/, (m) => ({
50
+ number: m[1], href: `https://search.ipaustralia.gov.au/trademarks/search/view/${m[1]}` })) },
51
+ // Opened a real record. `-00` is the extension CIPO gives the base application.
52
+ ca: { name: "Canada", address: (r) => addressFrom([r.applicationNumber], /^([1-9]\d{0,6})(?:-(\d{2}))?$/, (m) => ({
53
+ number: m[2] ? `${m[1]}-${m[2]}` : m[1], href: `https://ised-isde.canada.ca/cipo/trademark-search/${m[1]}-${m[2] ?? "00"}?lang=eng` })) },
54
+ // Rendered in a browser: a real number draws the record, and one that does not exist draws the bare
55
+ // search page. National marks only; a Madrid designation of Switzerland goes to WIPO, below.
56
+ ch: { name: "Switzerland", address: (r) => addressFrom([r.applicationNumber], /^(\d{1,5})\/(\d{4})$/, (m) => {
57
+ const n = `${pad(m[1], 5)}/${m[2]}`;
58
+ return { number: n, href: `https://www.swissreg.ch/database-client/register/detail?type=trademark&lang=en&no=${encodeURIComponent(n)}` };
59
+ }) },
60
+ // The address the EUIPO provider already links (providers/euipo/src/row.js).
61
+ eu: { name: "European Union", address: (r) => addressFrom([r.applicationNumber], /^(\d{1,9})$/, (m) => {
62
+ const n = pad(m[1], 9);
63
+ return { number: n, href: `https://euipo.europa.eu/eSearch/#details/trademarks/${n}` };
64
+ }) },
65
+ // INPI's own record pages at this address are indexed; INPI blocks automated fetches, so none was
66
+ // opened from a server. The vendor keeps INPI's FR prefix on the number.
67
+ fr: { name: "France", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(?:FR)?(\d{7})$/i, (m) => ({
68
+ number: `FR${m[1]}`, href: `https://data.inpi.fr/marques/FR${m[1]}` })) },
69
+ // The office's record address as indexed; it challenges automated fetches, so none was opened from a
70
+ // server. UK numbers are UK plus eleven digits (UK000..., UK008..., UK009...).
71
+ gb: { name: "United Kingdom", address: (r) => addressFrom([r.applicationNumber, r.registrationNumber], /^(UK\d{11})$/i, (m) => ({
72
+ number: m[1].toUpperCase(), href: `https://trademarks.ipo.gov.uk/ipo-tmcase/page/Results/1/${m[1].toUpperCase()}` })) },
73
+ // The register's own search opens records at exactly this address: the application number, then the
74
+ // registration number when there is one. Opened a real record. The older search.patentstyret.no
75
+ // addresses now answer 410 Gone.
76
+ no: { name: "Norway", address: (r) => addressFrom([r.applicationNumber], /^((?:19|20)\d{7})$/, (m) => {
77
+ const reg = /^\d{1,7}$/.test(clean(r.registrationNumber)) ? clean(r.registrationNumber) : null;
78
+ return { number: m[1], href: `https://services.patentstyret.no/search-details/trademark/${m[1]}${reg ? `-${reg}` : ""}?lang=en` };
79
+ }) },
80
+ // PRV's new search, in beta since 2026-01-26. Rendered in a browser: a real number draws the record,
81
+ // and one that does not exist draws the bare search page.
82
+ se: { name: "Sweden", address: (r) => addressFrom([r.applicationNumber], /^(\d{4}-\d{5})$/, (m) => ({
83
+ number: m[1], href: `https://search.prv.se/#/trademark/${m[1]}` })) },
84
+ // IPOS Digital Hub shows a record only inside a search session: there is no address for one record.
85
+ sg: { name: "Singapore", address: null },
86
+ // The address the USPTO provider already links. The number is the serial.
87
+ us: { name: "United States", address: (r) => addressFrom([r.applicationNumber], /^(\d{8})$/, (m) => ({
88
+ number: m[1], href: `https://tsdr.uspto.gov/#caseNumber=${m[1]}&caseType=SERIAL_NO&searchType=statusSearch` })) },
89
+ wo: { name: "WIPO (Madrid)", address: (r) => madridMonitor([r.irNumber, r.registrationNumber, r.applicationNumber]) },
90
+ });
91
+
92
+ const handleOffice = (uri) => (/^\/mark\/([a-z]{2,4})\//i.exec(clean(uri))?.[1] ?? "").toLowerCase();
93
+
94
+ // A number that already carries its office's letters (FR4123456, UK00003456789) stands alone; a bare
95
+ // one is shown with its office (CH 08133/2025), so the label always says whose number it is.
96
+ const labelOf = (office, number) => (number ? (/^[a-z]{2}/i.test(number) ? number : `${office.toUpperCase()} ${number}`) : null);
97
+
98
+ /**
99
+ * The office's page for one fetched record. `{ office, label, href, reason: null }` when the office
100
+ * publishes a page and the number is in the form its address takes. Otherwise `href` is null and
101
+ * `reason` says why: `no-page` (the office publishes none), `unaddressable` (the number is not in the
102
+ * form the address takes, or is missing), `unknown-office` (this table holds no address for the office).
103
+ *
104
+ * A MADRID DESIGNATION IS ADDRESSED AT WIPO, whatever office it sits under. On a designation the
105
+ * vendor's registration number is the IR number, so a national address built from it opens the wrong
106
+ * record or none.
107
+ */
108
+ export function officeRecordLink(rec, uri = "") {
109
+ if (!rec || typeof rec !== "object") return null;
110
+ const designation = clean(rec.filingRoute) === "madrid_designation";
111
+ const office = designation ? "wo" : (clean(rec.office).toLowerCase() || handleOffice(uri));
112
+ const page = OFFICE_RECORD_PAGES[office];
113
+ const own = designation ? clean(rec.irNumber) : clean(rec.applicationNumber) || clean(rec.registrationNumber);
114
+ const unlinked = (reason) => ({ office, label: labelOf(office, own), href: null, reason });
115
+ if (!page) return unlinked("unknown-office");
116
+ if (!page.address) return unlinked("no-page");
117
+ const a = designation ? madridMonitor([rec.irNumber]) : page.address(rec);
118
+ return a ? { office, label: labelOf(office, a.number), href: a.href, reason: null } : unlinked("unaddressable");
119
+ }
120
+
121
+ /** The registers whose records carry no page of the vendor's own, so the office's page is the link. */
122
+ export const VENDORS_WITHOUT_RECORD_PAGES = Object.freeze(["signa"]);
123
+
124
+ /**
125
+ * Every registration the findings cite, keyed by its lower-cased uri, for a run whose register is
126
+ * `provider`. Returns null for any other register, so those runs render exactly as before: their records
127
+ * carry links of their own. `tally` is what publish records, per office, so that a register whose numbers
128
+ * never fit shows up as a count instead of as a report that quietly looks the way it always did.
129
+ */
130
+ export function recordLinksFor(findings, recordsByUri, provider) {
131
+ if (!VENDORS_WITHOUT_RECORD_PAGES.includes(clean(provider).toLowerCase())) return null;
132
+ const byUri = new Map();
133
+ const tally = { linked: {}, cited: {}, notRetrieved: 0 };
134
+ const bump = (o, k) => { o[k] = (o[k] ?? 0) + 1; };
135
+ for (const f of Array.isArray(findings) ? findings : []) {
136
+ for (const r of f?.owner?.registrations ?? []) {
137
+ const key = clean(r?.uri).toLowerCase();
138
+ if (!key || byUri.has(key)) continue;
139
+ const rec = recordsByUri instanceof Map ? recordsByUri.get(key) : null;
140
+ if (!rec) { byUri.set(key, null); tally.notRetrieved += 1; continue; }
141
+ const link = officeRecordLink(rec, key);
142
+ byUri.set(key, link);
143
+ if (link.href) bump(tally.linked, link.office);
144
+ else bump((tally.cited[link.office] ??= {}), link.reason);
145
+ }
146
+ }
147
+ const sum = (xs) => xs.reduce((a, b) => a + b, 0);
148
+ const linked = Object.entries(tally.linked).map(([o, n]) => `${o} ${n}`).join(", ");
149
+ const cited = Object.entries(tally.cited).flatMap(([o, rs]) => Object.entries(rs).map(([r, n]) => `${o} ${n} ${r}`)).join(", ");
150
+ const summary = `linked ${sum(Object.values(tally.linked))}${linked ? ` (${linked})` : ""}`
151
+ + ` · cited by number ${sum(Object.values(tally.cited).flatMap((rs) => Object.values(rs)))}${cited ? ` (${cited})` : ""}`
152
+ + ` · record not retrieved ${tally.notRetrieved}`;
153
+ return { byUri, tally, summary };
154
+ }
155
+
156
+ const nameOf = (office) => OFFICE_RECORD_PAGES[office]?.name ?? office.toUpperCase();
157
+
158
+ /**
159
+ * The report's once-per-office account of the registrations it cites by number rather than links.
160
+ *
161
+ * NO COUNT. The cards list only some findings' registrations (an off-field finding lists none), so a
162
+ * number here could name more registrations than the reader can find on the page. The workbook lists
163
+ * them all, each with its reason.
164
+ */
165
+ export function officeReasonSentences(byUri) {
166
+ const keys = new Set();
167
+ for (const l of byUri instanceof Map ? byUri.values() : []) if (l && !l.href) keys.add(`${l.office} ${l.reason}`);
168
+ return [...keys].sort().map((k) => {
169
+ const [office, reason] = k.split(" ");
170
+ const name = nameOf(office);
171
+ if (reason === "no-page") return `${name}: the register publishes no page for a single record, so its registrations are cited by number.`;
172
+ if (reason === "unknown-office") return `${name}: we hold no page address for this register, so its registrations are cited by number.`;
173
+ return `${name}: registrations whose numbers are not in the form the register's page address takes are cited by number, not linked.`;
174
+ });
175
+ }
176
+
177
+ /** The workbook's Link cell for a finding: its first linked registration, else the first stated reason. */
178
+ export function linkCellFor(registrations, byUri) {
179
+ if (!(byUri instanceof Map)) return "";
180
+ const links = (registrations ?? []).map((r) => byUri.get(clean(r?.uri).toLowerCase())).filter(Boolean);
181
+ const linked = links.find((l) => l.href);
182
+ if (linked) return linked.href;
183
+ const first = links[0];
184
+ if (!first) return "";
185
+ const name = nameOf(first.office);
186
+ if (first.reason === "no-page") return `No page for a single record at this register (${name}); cited by number`;
187
+ if (first.reason === "unknown-office") return `No page address held for this register (${name}); cited by number`;
188
+ return `Number not in the form this register's page address takes (${name}); cited by number`;
189
+ }
@@ -243,7 +243,7 @@ export function stripTelemetry(md) {
243
243
  // packaged MCP and any future consumer get it from one definition.
244
244
  //
245
245
  // WHY NOT RE-WORD THE BUILDER. The audit workbook and the internal surfaces keep the engine's vocabulary
246
- // by ruling (tracker issue 831 "Out of scope"), and `resolution` is the audit trail's own record of which finding a
246
+ // by ruling ("Out of scope"), and `resolution` is the audit trail's own record of which finding a
247
247
  // claim resolved to. Changing what buildAuditMd stamps would take the word off an internal surface that is
248
248
  // entitled to it. The boundary is where the audience changes, so the boundary is where the cut belongs —
249
249
  // the same shape as scrubCards' `rated_under` → ratedUnderForClient, which is the precedent in that file.
@@ -256,7 +256,7 @@ export function stripTelemetry(md) {
256
256
  // Each rule is ANCHORED to one position in a grammar THIS ENGINE writes: the disposition token at the head
257
257
  // of `resolution`, and the disposition token inside the `(finding #N, …)` parenthetical the contradiction
258
258
  // line builds. A mark named ADVERSARIAL survives both — it is not at position 0 of a resolution line, and
259
- // it is not inside a finding-reference parenthetical. That is the tracker issue 656 lesson applied: match a POSITION in
259
+ // it is not inside a finding-reference parenthetical. That is the lesson applied: match a POSITION in
260
260
  // a grammar we own, never a word anywhere in a string we do not.
261
261
  //
262
262
  // The enum is IMPORTED (top of file), never retyped: a sixth disposition added to findings-model must not
@@ -292,7 +292,7 @@ export function contradictionResolutionForClient(v) {
292
292
 
293
293
  // ── ENGINE_PLAIN AND plainify ARE DELETED ────────────────────────────────────────────────────
294
294
  //
295
- // doc-52 put nineteen find-and-replace rules on the rendered client surface. tracker issue 656 is what that cost:
295
+ // doc-52 put nineteen find-and-replace rules on the rendered client surface. This is what that cost:
296
296
  // `axis` -> `group` turned "AXIS Bank filed in class 36" into "group Bank filed in class 36" — a report
297
297
  // naming a mark that does not exist, inside the report that clears it. AXIS and SLICE are both live
298
298
  // trademarks. The ban list and the trademark register overlap, and this engine exists to search the
@@ -167,3 +167,29 @@ export function readStore(base, name, { path: override = null } = {}) {
167
167
  // the divergence between those two is exactly what was.
168
168
  export const requiredAbsent = (names = []) =>
169
169
  (Array.isArray(names) ? names : []).filter((n) => PUBLISH_INPUTS[n] === "required");
170
+
171
+ /**
172
+ * WHY AN ABSENCE DID NOT HOLD THE RELEASE — the other half of `requiredAbsent`, and the half that was
173
+ * missing from every receipt.
174
+ *
175
+ * A delivered run's record read `released: true`, `reasons: []`, and `inputsAbsent` naming a store that
176
+ * was not there. All three were correct: every store above is declared `optional`, deliberately, so that
177
+ * re-rendering an archived run cannot rewrite the released status of delivered client work. But the
178
+ * reason lived in the table above and nowhere else, so a reader holding the receipt could see only a gate
179
+ * that had released while recording something. This states it where the gate states everything else.
180
+ *
181
+ * It EXPLAINS and decides nothing: `released` is `reasons.length === 0` and stays so. A store the table
182
+ * does not name reads `undeclared` rather than being given a ruling it never got.
183
+ *
184
+ * PURE, and it lives beside the table it reads so the two cannot drift.
185
+ */
186
+ export const nonClosingAbsences = (names = []) =>
187
+ (Array.isArray(names) ? names : [])
188
+ .filter((n) => PUBLISH_INPUTS[n] !== "required")
189
+ .map((n) => ({
190
+ input: n,
191
+ declared: PUBLISH_INPUTS[n] ?? "undeclared",
192
+ why: PUBLISH_INPUTS[n] === "optional"
193
+ ? "declared optional: a run that legitimately has no such store, and every archived run, must still publish"
194
+ : "not a declared publish input — the absence is recorded and nothing gates on it",
195
+ }));
@@ -24,7 +24,7 @@
24
24
  // ONE report. There is no client variant and no internal variant, and that is
25
25
  // still the rule — it is what removes the whole class of "the wrong link got sent".
26
26
  //
27
- // WHAT CHANGED, 2026-09-07 (owner ruling, tracker issue 274): the one report now CARRIES the reviewer's
27
+ // WHAT CHANGED, 2026-09-07 (ruling): the one report now CARRIES the reviewer's
28
28
  // notes rather than routing them to the audit workbook alone. The earlier reading of "one report" was
29
29
  // that internal working material is simply not part of it; the ruling is that there is one report and the
30
30
  // person who ran Clearotron reads it, so holding material back "just adds confusion for where data is
@@ -87,9 +87,9 @@ export function inlineMd(s) {
87
87
  .replace(/(^|[^*])\*([^*]+)\*/g, '$1<i>$2</i>')
88
88
  .replace(/`([^`]+)`/g, '<span class="mono">$1</span>');
89
89
  }
90
- // tracker issue 1934 / tracker issue 2056 — BLOCKS, NOT ONLY PARAGRAPHS.
90
+ // BLOCKS, NOT ONLY PARAGRAPHS.
91
91
  //
92
- // The writer emits sub-headers and bullets inside the summary now (owner ruling 2026-08-31: "keep the
92
+ // The writer emits sub-headers and bullets inside the summary now (ruling 2026-08-31: "keep the
93
93
  // length, add the structure"). This function read exactly two shapes — a blank line as a paragraph
94
94
  // break, a single newline as a wrap — so a `## CORAL FREEZE` line reached the client as those literal
95
95
  // characters, and a `- point` list as one run of text with hyphens in it. The structure the ruling asks
@@ -318,8 +318,8 @@ function glanceSection(marks, framework, registerCounts) {
318
318
  // There is ONE figure per predicate per row, at ONE scope, and the table says which. A second
319
319
  // all-classes figure alongside a class-scoped one would be a second count — one more provider call per
320
320
  // mark per predicate, billable on Corsearch — and that is a spend the owner has not ruled on. Raised as
321
- // a follow-up rather than assumed here (tracker issue 717).
322
- // ── TERRITORIES IN WORDS, NEVER CODES (tracker issue 331 C) ─────────────────────────────────────────
321
+ // a follow-up rather than assumed here.
322
+ // ── TERRITORIES IN WORDS, NEVER CODES ───────────────────────────────────────────────────────────────
323
323
  //
324
324
  // The line read "territories: EM, US, WO". On a worldwide run the same line printed every register code
325
325
  // the provider offers — roughly two hundred, AD through ZZ, internal groupings among them — which the
@@ -373,7 +373,7 @@ function listWords(items) {
373
373
  }
374
374
 
375
375
  /**
376
- * The counted scope, in one sentence (tracker issue 331 C). Three shapes, and the shape is chosen by
376
+ * The counted scope, in one sentence. Three shapes, and the shape is chosen by
377
377
  * how many registers were counted rather than by which they were:
378
378
  *
379
379
  * every register the provider offers -> "Counted worldwide, 190 registers, on Clarivate Compumark."
@@ -405,7 +405,7 @@ const COUNTS_READER_LINE = 'Counts include live, pending and dead filings. '
405
405
  + 'A count is not a conflict; the cards above say which filings matter.';
406
406
 
407
407
  function countsSection(marks, registerCounts) {
408
- // ── THE DEFINITION MOVES INTO THE COLUMN HEADER (tracker issue 331 B) ────────────────────────────
408
+ // ── THE DEFINITION MOVES INTO THE COLUMN HEADER ──────────────────────────────────────────────────
409
409
  //
410
410
  // The 70-word count-basis paragraph existed because three one-word headers — Identical, Containing,
411
411
  // Close variations — did not say what they counted, so the definition had to go somewhere. Put it in
@@ -451,7 +451,7 @@ function countsSection(marks, registerCounts) {
451
451
  return `<tr><td><b>${esc(m.name)}</b><br><span class="ko-classes">${esc(scope)}</span></td>${cells}</tr>`
452
452
  + (forms ? `<tr class="ko-forms"><td colspan="${COUNT_PREDICATES.length + 1}">${esc(forms)}</td></tr>` : '');
453
453
  }).join('');
454
- // COUNT_BASIS IS OFF THE PAGE, NOT OUT OF THE RECORD (tracker issue 331 B). It is still written to
454
+ // COUNT_BASIS IS OFF THE PAGE, NOT OUT OF THE RECORD. It is still written to
455
455
  // report-data.json (knockoutReportData -> registerCountBasis) and still on the workbook, which is where
456
456
  // 331 says it belongs; what it stops doing is printing twice on a page whose headers now say the same
457
457
  // thing in three words each.
@@ -461,7 +461,7 @@ function countsSection(marks, registerCounts) {
461
461
  </div>`;
462
462
  }
463
463
 
464
- // tracker issue 706 part 5 — the filings behind the narrow numbers, as a compact appendix under the counts.
464
+ // The filings behind the narrow numbers, as a compact appendix under the counts.
465
465
  //
466
466
  // FIRST N ROWS, then the workbook. The full list is capped at 100 per name and a hundred rows of
467
467
  // register data on a triage document would bury the page it is an appendix to; the workbook is where a
@@ -474,7 +474,7 @@ function countsSection(marks, registerCounts) {
474
474
  // of them is a finding. recordsLine owns all three wordings so the report, the workbook and
475
475
  // report-data.json cannot phrase them differently.
476
476
  /**
477
- * MATERIALITY, FOR BOTH REGISTER SURFACES (tracker issue 1927).
477
+ * MATERIALITY, FOR BOTH REGISTER SURFACES.
478
478
  *
479
479
  * Two caps decide which filings a client sees — the appendix table and the promoted cards — and both
480
480
  * took the provider's arrival order. Measured on a delivered four-mark run: one name fetched 23 records,
@@ -591,7 +591,7 @@ function filingsSection(marks, registerRecords) {
591
591
  return `<div class="ko-row"><div><p class="ko-name">${esc(m.name)}</p></div>
592
592
  <div><p class="ko-bul">${esc(line ?? 'No filings listing was recorded for this name.')}</p></div></div>`;
593
593
  }
594
- // RANKED, AND DELIBERATELY NOT FILTERED (tracker issue 1927). This table is the evidence behind a
594
+ // RANKED, AND DELIBERATELY NOT FILTERED. This table is the evidence behind a
595
595
  // COUNT: dropping a cancelled or out-of-class row would leave a reader unable to reconcile the table
596
596
  // with the number above it, and "we did not show you that one" is a different promise from "it is
597
597
  // not material". The cards promote and therefore filter; the appendix evidences and therefore ranks.
@@ -660,7 +660,7 @@ function tierAbsenceLine(registerCounts, probeRan) {
660
660
  // nicely, but portal-ui/src/contract/reportFrame.ts refuses a mark from the document side on purpose,
661
661
  // and the composite key IS a mark name. The server reads `ref` back off disk once it has resolved.
662
662
  /**
663
- * THE CARD'S DETAIL, FOLDED (tracker issue 331 A.3).
663
+ * THE CARD'S DETAIL, FOLDED.
664
664
  *
665
665
  * Visible on a card: the reference, the name, the band chip, the source chip, the one-sentence read and
666
666
  * the evidence links. Folded: the paragraph that argues the band, and on a register card the use-check
@@ -685,8 +685,8 @@ const WHY_BAND_LABEL = 'Why this band';
685
685
  function findingBlock(v, framework, markIndex) {
686
686
  const meta = [v.type, v.owner].filter(Boolean).map((s) => esc(s)).join(' · ');
687
687
  const ev = v.evidence.map((u) => linkOrText(u)).join(' · ');
688
- // THE CLEARANCE REPORT'S CARD, AND THE KNOCKOUT'S LOCATOR — both classes, deliberately (tracker
689
- // issue 1935). report.css is already inlined here and already defines .card, .top, .rail, .cardhead,
688
+ // THE CLEARANCE REPORT'S CARD, AND THE KNOCKOUT'S LOCATOR — both classes, deliberately. report.css
689
+ // is already inlined here and already defines .card, .top, .rail, .cardhead,
690
690
  // .fnum and .who; this uses them rather than a parallel set, so a client who has read one report
691
691
  // recognises the other.
692
692
  //
@@ -808,9 +808,9 @@ const NOT_WEIGHED = NOT_WEIGHED_LINE;
808
808
  /**
809
809
  * The register card's basis line, which cannot be written without knowing whether the card is rated.
810
810
  *
811
- * THE TWO RULINGS MEET HERE AND THEY ALMOST CONTRADICTED EACH OTHER ON A CLIENT PAGE. tracker issue
812
- * 1935 retired a code-owned placeholder because a rating nobody performed must not appear; tracker
813
- * issue 274 then ruled that the rater's OWN band for a promoted filing must appear, because a filing
811
+ * THE TWO RULINGS MEET HERE AND THEY ALMOST CONTRADICTED EACH OTHER ON A CLIENT PAGE. One ruling
812
+ * retired a code-owned placeholder because a rating nobody performed must not appear; the other then
813
+ * ruled that the rater's OWN band for a promoted filing must appear, because a filing
814
814
  * the rater weighed into the verdict was rendering as though it had not been. Both hold, and they hold
815
815
  * over different cards.
816
816
  *
@@ -844,7 +844,7 @@ function basisLine(band, read) {
844
844
  * Neither test is a judgment about the filing. Whether any of it blocks anything is lawyer work this
845
845
  * product does not do — the same line register-records.mjs rule 3 has held since the lane shipped.
846
846
  */
847
- // EXPORTED so the pipeline can ask the same question the page asks (tracker issue 276). The owner
847
+ // EXPORTED so the pipeline can ask the same question the page asks. The owner
848
848
  // use-check runs on PROMOTED filings only, and "promoted" has to mean here what it means on the report —
849
849
  // a second predicate would let the engine search an owner whose filing the report never shows, or skip
850
850
  // one it does.
@@ -864,7 +864,7 @@ export function promotableRecords(entry, mark) {
864
864
  /** One sentence of what the filing IS, composed from the register's own fields. No adjective that the
865
865
  * record did not supply, and every absent field says it is absent rather than rendering as a blank —
866
866
  * a blank cell and an unstated field read alike to a human and only one is honest. */
867
- // ── THE OFFICE, NOT THE VENDOR (tracker issue 331 D) ────────────────────────────────────────────────
867
+ // ── THE OFFICE, NOT THE VENDOR ──────────────────────────────────────────────────────────────────────
868
868
  //
869
869
  // The sentence read "A filing of the identical name on Clarivate Compumark — held by ..., status
870
870
  // PENDING, classes 9, 38, 45, territory us." Clarivate Compumark is the search vendor; the filing is at
@@ -926,7 +926,7 @@ function registerStatement(r, provider) {
926
926
  * which the card links.
927
927
  */
928
928
  /**
929
- * WHICH OF A MARK'S REGISTER CARDS THE PAGE DRAWS (tracker issue 331 A.3).
929
+ * WHICH OF A MARK'S REGISTER CARDS THE PAGE DRAWS.
930
930
  *
931
931
  * THIS FILTERS THE PAGE, NEVER THE RECORD, and that separation is the whole point. 331 rejects dropping
932
932
  * anything from report-data.json or the workbook to make the page shorter, so `registerCardViews` still
@@ -951,7 +951,7 @@ function registerStatement(r, provider) {
951
951
  * is kept, because "the rater said something, and the safe reading of something we cannot rank is that
952
952
  * it is worth pointing at". An absent band is strictly LESS information than an unrankable one, so if
953
953
  * unknown keeps, absent keeps. Dropping it would be this renderer deciding a filing is immaterial on
954
- * the rater's behalf — the same claim tracker issue 274 removed from these cards for the same reason.
954
+ * the rater's behalf — the same claim a later ruling removed from these cards for the same reason.
955
955
  *
956
956
  * A run that carries no bands at all still renders every card it was delivered with, which is what
957
957
  * clause F asks for; it now falls out of the rule instead of being carved around it.
@@ -961,7 +961,7 @@ function registerCardsOnPage(cards, mark, framework) {
961
961
  }
962
962
 
963
963
  /**
964
- * DID THE RATER PUT THIS FILING ON THE LADDER'S LOWEST RUNG? (tracker issue 331 A.3)
964
+ * DID THE RATER PUT THIS FILING ON THE LADDER'S LOWEST RUNG?
965
965
  *
966
966
  * The ladder runs worst-first, so the lowest rung is the LAST entry. A filing the rater put there is one
967
967
  * the rater called manageable, and 331 keeps those in the filings table rather than on a card: three
@@ -1017,7 +1017,7 @@ function registerCardViews(mark, framework, registerRecords) {
1017
1017
  /**
1018
1018
  * ONE promoted filing, in findingBlock's own anatomy — the SAME anatomy, now, not a parallel one.
1019
1019
  *
1020
- * OWNER RULING: one combined card, the same head as every other conflict, tagged by
1020
+ * RULING: one combined card, the same head as every other conflict, tagged by
1021
1021
  * SOURCE. "Weighed" and "not weighed" are not terms we use with a client, so the band slot's neutral word
1022
1022
  * is gone; what tells the two apart on the page is where the finding came from, which is the vocabulary
1023
1023
  * the clearance reports already use for exactly this.
@@ -1025,7 +1025,7 @@ function registerCardViews(mark, framework, registerRecords) {
1025
1025
  * THE BAND CHIP IS DRAWN ONLY WHEN THE RATER SENT ONE, and that condition is the whole rule. The old note
1026
1026
  * here said "no band chip and no `bandStop`, ever: a rung of the run's ladder would be a rating nobody
1027
1027
  * performed" — which was true for as long as nothing could carry the rater's rating of an individual
1028
- * filing. `registerReads[].band` now can (tracker issue 274), so the objection is answered where it was
1028
+ * filing. `registerReads[].band` now can, so the objection is answered where it was
1029
1029
  * raised: with a band, the rating WAS performed, by the seat, in the framework's own words, and the chip
1030
1030
  * states it. With no band the old rule stands unchanged — --faint rail, no chip, nothing claimed — and
1031
1031
  * that is still what every archived run renders.
@@ -1059,7 +1059,7 @@ function sourceChips(v) {
1059
1059
 
1060
1060
  // ── THE REVIEWER'S NOTES, ON THE REPORT ─────────────────────────────────────────────────
1061
1061
  //
1062
- // OWNER RULING, 2026-09-07: there is one report and the person who ran Clearotron reads it — keeping
1062
+ // RULING, 2026-09-07: there is one report and the person who ran Clearotron reads it — keeping
1063
1063
  // this material off the page "just adds confusion for where data is lost". This replaces the earlier
1064
1064
  // split under which the notes went to the audit workbook alone (the header of this file and
1065
1065
  // publish/knockout.mjs carried that rule; both now say what replaced it). The workbook keeps them too:
@@ -1071,7 +1071,7 @@ function sourceChips(v) {
1071
1071
  // findings about the mark, which is the one way this ruling could produce a worse document.
1072
1072
  const REVIEWER_NOTES_LEGEND = 'Purple notes are for the reviewing lawyer. Remove them before this goes to the client.';
1073
1073
 
1074
- // The clearance lane's own label, copied rather than re-worded (tracker issue 276). One spelling across
1074
+ // The clearance lane's own label, copied rather than re-worded. One spelling across
1075
1075
  // both products is the point: a reader who has seen it on a clearance report knows what it means here.
1076
1076
  const USE_CHECK_LABEL = 'Use-check source:';
1077
1077
 
@@ -1087,7 +1087,7 @@ function ownerCheckFor(ownerChecks, recordId) {
1087
1087
  // needs the same answer: the page files each note, and the reviewer warns a writer whose note will file
1088
1088
  // the way they did not intend. One reader, so the two can never disagree.
1089
1089
  /**
1090
- * "About this request" — what was asked, and any flag on the asking (tracker issue 331 A.1).
1090
+ * "About this request" — what was asked, and any flag on the asking.
1091
1091
  *
1092
1092
  * WHAT WAS ASKED IS CODE-OWNED. It is read off the run's own frozen instructed scope, never off model
1093
1093
  * prose, so the line states the request the run was given rather than a paraphrase of it. An archived run
@@ -1143,7 +1143,7 @@ function registerFindingBlock(v, markIndex, reads = null, framework = null, owne
1143
1143
  // colour an unknown word as though it were rated.
1144
1144
  const band = row?.band || null;
1145
1145
  const stop = band ? bandStop(framework, band) : null;
1146
- // ── WHERE THE OWNER'S TRADE WAS LOOKED UP (tracker issue 276) ──────────────────────────────────
1146
+ // ── WHERE THE OWNER'S TRADE WAS LOOKED UP ──────────────────────────────────────────────────────
1147
1147
  //
1148
1148
  // The source is the DRIVER'S, joined by this filing's own recordId — never a URL the seat typed. That
1149
1149
  // is the whole reason a reader can trust it: the row exists because the driver made the call, so
@@ -1288,7 +1288,7 @@ function coverageClause(mark, registerCounts, probeRan) {
1288
1288
  // under a collapsed "Full narrative" — the reader who wants the long form still has it, one click away,
1289
1289
  // and no sentence the stage wrote is dropped on the floor.
1290
1290
  /**
1291
- * THE LABELS, IN THE READER'S WORDS (tracker issue 331 A.2).
1291
+ * THE LABELS, IN THE READER'S WORDS.
1292
1292
  *
1293
1293
  * The old pair was "What holds it there" and "What would move it", 9.5px grey capitals. The owner read
1294
1294
  * the first one and asked "what holds what?" — so the fix is not a synonym in the same style, it is a
@@ -1361,7 +1361,7 @@ function readBlock(m, framework) {
1361
1361
  counter.length ? `<div class="ko-counter"><span class="ko-lbl2">${esc(counterLabel(framework, band))}</span><ul class="ko-bul">${
1362
1362
  counter.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
1363
1363
  m.mitigation ? `<div class="ko-mitig"><span class="ko-lbl2">What would lower the risk</span><p>${inlineMd(m.mitigation)}</p></div>` : '',
1364
- // ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD (tracker issue 331 A.2) ────────────────────
1364
+ // ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD ────────────────────────────────────────────
1365
1365
  //
1366
1366
  // The model writes both. `bullets` (five) say in other words what `factors` (four) already say
1367
1367
  // visible above; `assessment` is the long-form read under its own four headings, and until now the
@@ -1509,7 +1509,7 @@ const CAVEAT_LEAD = 'This screen also carries the following limits:';
1509
1509
  export function renderKnockoutHtml(findings, framework, {
1510
1510
  runId, overall, issued = null, auditFile = null, probeRan = false, registerCounts = null,
1511
1511
  registerRecords = null,
1512
- // The driver's own record of the owner lookups it ran (tracker issue 276). Defaults to [] so an
1512
+ // The driver's own record of the owner lookups it ran. Defaults to [] so an
1513
1513
  // archived run that predates the lane renders exactly as it was delivered.
1514
1514
  ownerChecks = [],
1515
1515
  identity = null, matter = null, homeHref = null, chromeHref = null, depthNote = null,
@@ -1517,7 +1517,7 @@ export function renderKnockoutHtml(findings, framework, {
1517
1517
  // instructed to do rather than what its artifacts happen to show. Defaults to null, and an archived
1518
1518
  // run that carries none keeps the artifact-derived sentence it was delivered with.
1519
1519
  searchPolicy = null,
1520
- // — the run's own frozen instructed scope (tracker issue 331 A.1): the goods, classes and
1520
+ // — the run's own frozen instructed scope: the goods, classes and
1521
1521
  // territories the requester asked for, in the requester's words. Defaults to null, and an archived run
1522
1522
  // that carries no such sidecar renders "About this request" from its request-level notes alone, or not
1523
1523
  // at all when it has none. Never derived from model prose — this is what was ASKED, not what was said
@@ -1549,7 +1549,7 @@ export function renderKnockoutHtml(findings, framework, {
1549
1549
  const allViews = marks.flatMap((m) => knockoutFindingViews(m, { manifest: framework }));
1550
1550
  const citedFindings = allViews.filter((v) => v.evidence.length).length;
1551
1551
  const uncitedFindings = allViews.length - citedFindings;
1552
- // ── A ONE-NAME PAGE HAS NO INDEX (tracker issue 331 A.6) ────────────────────────────────────────
1552
+ // ── A ONE-NAME PAGE HAS NO INDEX ────────────────────────────────────────────────────────────────
1553
1553
  //
1554
1554
  // The glance row exists to let a reader of a BATCH find the name they care about. With one name there
1555
1555
  // is nothing to index, and the row printed that name, its band and its classes for the first of three
@@ -1563,7 +1563,7 @@ export function renderKnockoutHtml(findings, framework, {
1563
1563
  const counts = hasCounts
1564
1564
  ? countsSection(marks, registerCounts)
1565
1565
  : `<div class="panel"><p class="ko-tier">${esc(tierLine)}</p></div>`;
1566
- // ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED (tracker issue 331 E) ──
1566
+ // ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED ────────────────────────
1567
1567
  //
1568
1568
  // The four model-written caveats on the measured run overlap the fixed text completely — 331 item 9,
1569
1569
  // and 333 rule 4 ("say it once") fixes it at the source for runs written under that doctrine. This is
@@ -1631,7 +1631,7 @@ ${filings}`
1631
1631
  </div>
1632
1632
  <div class="fab-stack">${themeButton()}</div>
1633
1633
  <script>
1634
- /* THE DOCUMENT'S OWN CONTROLS, and they are GLOBAL on purpose (tracker issue 1922).
1634
+ /* THE DOCUMENT'S OWN CONTROLS, and they are GLOBAL on purpose.
1635
1635
  The portal frames this document and drives it by postMessage. The bridge injected at serve time looks
1636
1636
  up exportPDF, pickAll and openAll BY NAME and replies "this report has no <verb>" when a name is not
1637
1637
  there. Every one of these lived inside an IIFE, so a knockout answered that to all three and every
@@ -1664,7 +1664,7 @@ window.addEventListener('beforeprint',o);})();</script>
1664
1664
  ${filingsSec}
1665
1665
 
1666
1666
  <div class="sec"><span class="num">${num()}</span><h2>Scope &amp; what we didn't search</h2><span class="note">what this screen is</span></div>
1667
- <!-- COLLAPSED, like the clearance report's (tracker issue 1935). Both documents already inline the
1667
+ <!-- COLLAPSED, like the clearance report's. Both documents already inline the
1668
1668
  same report.css, and details.scope is its vocabulary — this section was the one place the two
1669
1669
  lanes presented the same thing differently, with the knockout's open panel pushing the filings
1670
1670
  table and the footer down the page on every read. What is inside is unchanged; a reader who
@@ -1674,10 +1674,10 @@ window.addEventListener('beforeprint',o);})();</script>
1674
1674
  <details class="scope"><summary>Scope &amp; what we didn't search</summary>
1675
1675
  <div class="panel drillbody">
1676
1676
  ${(() => {
1677
- // FROM THE POLICY WHERE THERE IS ONE (tracker issue 1935). The fallback is not a tidy default: an
1677
+ // FROM THE POLICY WHERE THERE IS ONE. The fallback is not a tidy default: an
1678
1678
  // archived run froze no policy, and re-rendering it must not invent a scope claim about a run
1679
1679
  // nobody can now ask. It keeps the sentence it was delivered with.
1680
- // ── THE SCOPE BLOCK, WHOLE (tracker issue 331 E) ──────────────────────────────────────────────
1680
+ // ── THE SCOPE BLOCK, WHOLE ────────────────────────────────────────────────────────────────────
1681
1681
  //
1682
1682
  // The composed lines said one thing five times in 362 words: "not a clearance" three times,
1683
1683
  // "proceeds to clearance" twice, and the count-basis sentence a second time after the counts table
@@ -1771,7 +1771,7 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
1771
1771
  classesDriving: m.classesDriving ?? [],
1772
1772
  degraded: Boolean(m.degraded),
1773
1773
  points: m.bullets ?? [],
1774
- // ── THE ASSESSMENT THE RUN ALREADY WROTE (tracker issue 274) ──────────────────────────────
1774
+ // ── THE ASSESSMENT THE RUN ALREADY WROTE ──────────────────────────────────────────────────
1775
1775
  //
1776
1776
  // Seven keys the assess stage writes into knockout-findings.json reached this file as nothing.
1777
1777
  // They were not withheld by a rule — no code decided against them; this projection is an explicit
@@ -1828,7 +1828,7 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
1828
1828
  // `shape: 'register'` is the load-bearing field: a consumer can tell a weighed conflict from a
1829
1829
  // pointed-at filing without parsing prose.
1830
1830
  //
1831
- // `band` AND `basis` NOW CARRY THE RATER'S READ WHEN IT SENT ONE (tracker issue 274). Before
1831
+ // `band` AND `basis` NOW CARRY THE RATER'S READ WHEN IT SENT ONE. Before
1832
1832
  // this, both were constants — `band: null` and the not-weighed sentence — on every filing of
1833
1833
  // every run, including runs where the seat had written a full read of that exact filing and the
1834
1834
  // HTML card was already printing it. The page and this file disagreed, and this file is the one
@@ -1842,8 +1842,8 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
1842
1842
  .find((x) => String(x?.recordId ?? '').trim() && String(x?.recordId ?? '').trim() === String(v.record?.recordId ?? '').trim());
1843
1843
  const read = String(row?.read ?? '').trim() || null;
1844
1844
  const band = String(row?.band ?? '').trim() || null;
1845
- // The driver's own record of where this filing's proprietor was looked up (tracker issue
1846
- // 276), joined by recordId. `null` where the run owed no check — the same three states the
1845
+ // The driver's own record of where this filing's proprietor was looked up, joined by
1846
+ // recordId. `null` where the run owed no check — the same three states the
1847
1847
  // card has, so a consumer can tell "searched and found nothing" from "never searched".
1848
1848
  const oc = ownerCheckFor(ownerChecks, v.record?.recordId);
1849
1849
  return {