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
@@ -6,7 +6,7 @@
6
6
  //
7
7
  // ── WHY THIS IS DERIVED AND NOT WRITTEN BY HAND ──────────────────────────────────────────────────
8
8
  //
9
- // It is the replacement half of the retired-spelling check (tracker issue 168). The DETECTION half
9
+ // It is the replacement half of the retired-spelling check. The DETECTION half
10
10
  // needs nothing from this file: no product code reads a `PRELIM_*` name any more, so a `PRELIM_*` that
11
11
  // is set is dead whatever this list says. What the list decides is the second half of the sentence —
12
12
  // which name to tell the operator to use instead — and that is where a hand list does damage. A hand
@@ -31,6 +31,7 @@
31
31
  // --check exit 1 if the committed file disagrees with a fresh derivation, printing the difference
32
32
 
33
33
  import { execFileSync } from "node:child_process";
34
+ import { publishedOf } from "../shared/reference-guard-classes.mjs"; // the published population, one helper for every minter
34
35
  import { readFileSync, writeFileSync } from "node:fs";
35
36
  import { dirname, join } from "node:path";
36
37
  import { fileURLToPath } from "node:url";
@@ -45,13 +46,26 @@ const OUT = join(REPO, "shared", "names-in-force.mjs");
45
46
  * and a name only a test mentions is not a name this build reads. Including them would mint
46
47
  * replacements for settings the product does not have. `.js` and `.ts` are in with `.mjs` because
47
48
  * the providers' own `src` directories carry non-test `.js` — a `.mjs`-only derivation has produced
48
- * wrong answers on this tree before (tracker issue 96).
49
+ * wrong answers on this tree before.
49
50
  */
50
51
  export function sourceFiles(root = REPO) {
51
- const tracked = execFileSync("git", ["-C", root, "ls-files", "*.mjs", "*.js", "*.ts"], {
52
+ // THE PUBLISHED POPULATION, NOT THE INDEX. `git ls-files` reads the index, and an overlay run stages
53
+ // the withheld corpus over a clone without committing it — so minting there counted withheld names
54
+ // into this public floor with nothing reporting it: the number is simply too high, and the next
55
+ // person to re-mint from a clean checkout is shown a shrink and told to record it. Same helper as the
56
+ // other three minters, because two spellings of this rule disagree under the overlay and `--check`
57
+ // then reports a difference that is only the two instruments asking different questions.
58
+ //
59
+ // Found by deriving the population of this guard rather than listing it: the check that holds the
60
+ // other three named them by hand and this file was not among them, so the guard passed over the
61
+ // fourth caller of the defect it exists to stop.
62
+ const all = execFileSync("git", ["-C", root, "ls-files", "*.mjs", "*.js", "*.ts"], {
52
63
  encoding: "utf8", maxBuffer: 64 * 1024 * 1024,
53
64
  }).split("\n").filter(Boolean);
54
- return tracked.filter((f) => !/(^|\/)(test|tests|bench)\//.test(f) && !/\.test\.(mjs|ts|js)$/.test(f));
65
+ const p = publishedOf(all, root);
66
+ if (p.error) { console.error(`mint-names-in-force: ${p.error}`); process.exit(2); }
67
+ if (p.laid) console.log(`mint-names-in-force: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
68
+ return p.files.filter((f) => !/(^|\/)(test|tests|bench)\//.test(f) && !/\.test\.(mjs|ts|js)$/.test(f));
55
69
  }
56
70
 
57
71
  const NAME_RE = /\bCLEAROTRON_[A-Z0-9_]+/g;
@@ -78,7 +92,7 @@ function render(names) {
78
92
  //
79
93
  // Every \`CLEAROTRON_*\` name this build reads. It exists so the retired-spelling check can name the
80
94
  // replacement for an old \`PRELIM_*\` line rather than only saying the old one is dead — see
81
- // shared/env-aliases.mjs and tracker issue 168. Re-mint with:
95
+ // shared/env-aliases.mjs. Re-mint with:
82
96
  //
83
97
  // node scripts/mint-names-in-force.mjs
84
98
  //
@@ -3,7 +3,7 @@
3
3
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
4
  //
5
5
  // Mints `driver/test/fixtures/reference-strip-backlog.json` — the per-file floor under the repair of
6
- // the sentences the reference strip broke (tracker issue 185).
6
+ // the sentences the reference strip broke.
7
7
  //
8
8
  // Run it AFTER repairing lines, never to make a red arm green: the arm it feeds refuses any count that
9
9
  // went UP, so re-minting is how a repair is recorded, not how a regression is absorbed.
@@ -41,6 +41,7 @@ import { CENSUS_WORKSPACES, CENSUS_ROOT_SCRIPTS, countTestSites, collectionFromM
41
41
  import { providerTestFiles } from "./test-full.mjs";
42
42
  import { withheldEntryFor, announceWithheldMode } from "../shared/withheld-paths-access.mjs"; // — withheld is a stated absence, not a loss.: the record does not ship, and without it every absence is a loss
43
43
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
44
+ import { publishedOf } from "../shared/reference-guard-classes.mjs";
44
45
 
45
46
  // — say which mode this run is in, so a relaxed check is never silent.
46
47
  announceWithheldMode();
@@ -72,14 +73,44 @@ const README = [
72
73
  "RE-STAMP DELIBERATELY: node scripts/mint-suite-census.mjs --apply",
73
74
  ];
74
75
 
76
+ /**
77
+ * The tracked files matching `globs` that HEAD actually carries.
78
+ *
79
+ * `git ls-files` reads the INDEX. The private control stages a withheld corpus over a clone without
80
+ * committing it, so under that overlay the index holds files HEAD does not — and this census, minted
81
+ * on the published tree and checked against the overlaid one, reported every laid test file as an
82
+ * addition. The stamp was current and the check said stale, permanently, because the two runs were
83
+ * counting different populations rather than disagreeing about one.
84
+ *
85
+ * `publishedOf` is the discriminator the residue floor and the reference-strip backlog already use.
86
+ * One helper rather than three spellings: three drift, and the drift shows only under the overlay,
87
+ * which is the place nobody runs by accident.
88
+ *
89
+ * THE TWO CASES ARE NOT THE SAME and are not treated the same. A path in the index and absent from
90
+ * HEAD is LAID and is not counted. A path in HEAD and modified in the index is ordinary work in
91
+ * progress and is counted exactly as before — refusing that would make this refuse a normal edit.
92
+ * `publishedOf` filters on membership of HEAD, so a modified-but-committed file stays in.
93
+ *
94
+ * A tree with no HEAD cannot say what it published. That is a failure to look, and it exits rather
95
+ * than reading as "nothing is laid here", which is the permissive answer and the one that passes.
96
+ */
97
+ function publishedFiles(root, globs) {
98
+ let all = [];
99
+ try {
100
+ all = execFileSync("git", ["-C", root, "ls-files", "--", ...globs], { encoding: "utf8" })
101
+ .split("\n").map((x) => x.trim()).filter(Boolean);
102
+ } catch { return []; }
103
+ if (!all.length) return [];
104
+ const p = publishedOf(all, root);
105
+ if (p.error) { console.error(`mint-suite-census: ${p.error}`); process.exit(2); }
106
+ if (p.laid) console.log(`mint-suite-census: ${p.laid} tracked path(s) are not in HEAD — laid over this checkout, not published in it, and not counted`);
107
+ return p.files.slice().sort();
108
+ }
109
+
75
110
  export function buildCensus(root = ROOT) {
76
111
  const out = { _README: README, workspaces: {}, rootScripts: {} };
77
112
  for (const { ws, ext } of CENSUS_WORKSPACES) {
78
- let files = [];
79
- try {
80
- files = execFileSync("git", ["-C", root, "ls-files", "--", `${ws}/test/*.test.${ext}`], { encoding: "utf8" })
81
- .split("\n").map((s) => s.trim()).filter(Boolean).sort();
82
- } catch { files = []; }
113
+ const files = publishedFiles(root, [`${ws}/test/*.test.${ext}`]);
83
114
  const perFile = {};
84
115
  for (const f of files) {
85
116
  const abs = join(root, f);
@@ -99,11 +130,7 @@ export function buildCensus(root = ROOT) {
99
130
  // one: these globs span two shapes (`providers/_shared/test/` and `providers/<id>/test/`), and a
100
131
  // stripped key would collide the moment two providers shipped a file of the same name.
101
132
  for (const { script, globs } of CENSUS_ROOT_SCRIPTS) {
102
- let files = [];
103
- try {
104
- files = execFileSync("git", ["-C", root, "ls-files", "--", ...globs], { encoding: "utf8" })
105
- .split("\n").map((x) => x.trim()).filter(Boolean).sort();
106
- } catch { files = []; }
133
+ const files = publishedFiles(root, globs);
107
134
  const perFile = {};
108
135
  for (const f of files) {
109
136
  const abs = join(root, f);
@@ -201,7 +201,7 @@ async function main() {
201
201
  : " nothing stripped — no publish-only keys were present");
202
202
  // Prove the repo manifest still has them, here, on every pack.
203
203
  //
204
- // ── WHICH KEYS THIS MAY REFUSE OVER, AND WHY IT IS NOT ALL OF THEM (tracker issue 196) ───────────
204
+ // ── WHICH KEYS THIS MAY REFUSE OVER, AND WHY IT IS NOT ALL OF THEM ───────────────────────────────
205
205
  //
206
206
  // This asserted every entry of STRIP_KEYS was present in the repo manifest, and `private` has not
207
207
  // been in it since publishing moved into CI — the STRIP_KEYS comment above says so in its own words.
@@ -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 install refuses an unsupported Node BEFORE anything is written — tracker issue 364.
4
+ // The install refuses an unsupported Node BEFORE anything is written.
5
5
  //
6
6
  // Three declarations already stated the requirement and none of them bound anyone: `engines.node` is a
7
7
  // WARNING unless the person installing has set `engine-strict`, `.nvmrc` is read by nvm and nothing
@@ -55,7 +55,7 @@ import { cutDecision, versionAtHead, tagsHere } from "./release-cut-decision.mjs
55
55
  * Default bound: twenty-five minutes at thirty-second steps. Both are arguments so an arm can drive
56
56
  * the loop.
57
57
  *
58
- * TWENTY-FIVE, RAISED FROM FIFTEEN (tracker issue 247), because this waits for the version pull
58
+ * TWENTY-FIVE, RAISED FROM FIFTEEN, because this waits for the version pull
59
59
  * request's OWN CI and that is what it must clear. Measured over the first three cuts, the wait held
60
60
  * 552 s, 622 s and 686 s against a 900 s budget — rising every time, and the thing it waits on is the
61
61
  * offline suite, which grows on purpose every time anybody adds an arm. The margin was one slow queue.
@@ -139,7 +139,7 @@ const git = (args) => execFileSync("git", args, { encoding: "utf8" });
139
139
  * One read of `main`: the version it carries, whether that version is tagged, and WHICH COMMIT said so.
140
140
  *
141
141
  * THE COMMIT IS READ IN THE SAME PASS AS THE VERSION, and that is the whole point of this function
142
- * existing rather than being three calls at the call site (tracker issue 238). The job below used to
142
+ * existing rather than being three calls at the call site. The job below used to
143
143
  * check out `main` by name after this loop returned, so a commit landing in between — one that moves no
144
144
  * version, an instrument fix with no note — was packed and published under a number whose changelog
145
145
  * never described it. Nothing downstream could see it: the tip check compares VERSIONS, and the version
@@ -212,7 +212,7 @@ function main() {
212
212
  sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
213
213
  now: () => Date.now() - started,
214
214
  }).catch((e) => {
215
- // ── A FAILURE TO LOOK IS NOT "NOT MERGED YET" (tracker issue 208) ───────────────────────────────
215
+ // ── A FAILURE TO LOOK IS NOT "NOT MERGED YET" ───────────────────────────────────────────────────
216
216
  //
217
217
  // Giving up quietly is the ordinary outcome of this loop and stays exit 0: checks that went red, a
218
218
  // pull request somebody dismissed, a re-cut mid-flight. None of those is a release gone missing.
@@ -49,7 +49,7 @@ export function tagsHere({ run = (args) => execFileSync("git", args, { encoding:
49
49
  }
50
50
 
51
51
  /**
52
- * WHICH COMMIT THE QUESTION IS ABOUT — tracker issue 208, measured on run 34050690448.
52
+ * WHICH COMMIT THE QUESTION IS ABOUT — measured on run 34050690448.
53
53
  *
54
54
  * `versionAtHead()` reads a COMMIT rather than the working tree, deliberately, and the header above says
55
55
  * why at length. That is necessary and it was not sufficient: in the `version` job the changesets action
@@ -65,7 +65,7 @@ export function distTag(version, { preMode = false } = {}) {
65
65
  throw new Error(`release-dist-tag: "${version}" is not a version this can read, so the channel it `
66
66
  + "belongs on cannot be derived. Publishing it would default to `latest`.");
67
67
  }
68
- // ── THE PRE-MODE OVERRIDE IS GONE, AND `preMode` NO LONGER DECIDES ANYTHING (tracker issue 230) ──
68
+ // ── THE PRE-MODE OVERRIDE IS GONE, AND `preMode` NO LONGER DECIDES ANYTHING ──────────────────────
69
69
  //
70
70
  // It read `if (preMode) return STABLE`, putting every pre-release on `latest` under the owner's ruling
71
71
  // of 2026-09-05. That was right while nothing installed the package and wrong once things did: nine
@@ -7,7 +7,7 @@
7
7
  //
8
8
  // ── WHAT IT IS FOR, AND WHY IT IS NOT ANOTHER MANIFEST ASSERTION ────────────────────────────────────
9
9
  //
10
- // Five consecutive releases published a package npm refused to install (tracker issue 180). Every
10
+ // Five consecutive releases published a package npm refused to install. Every
11
11
  // check in the pipeline passed on every one of them, because each asked a question about the tarball
12
12
  // and none asked the only question a visitor asks: does `npm install clearotron` work.
13
13
  //
@@ -50,7 +50,7 @@ export const EMPTY_PHRASES = ["now correctly", "as expected", "properly"];
50
50
  * A path whose first segment is one of these is a SOURCE-TREE path — `driver/test/…`, `scripts/…` — and
51
51
  * means nothing to somebody who has never opened this repository. A path that starts at the reader's own
52
52
  * home does not: `~/.config/clearotron/` is where THEIR settings are, and telling them is the note's job.
53
- * Owner ruling 2026-09-05, narrowing the contract's flat ban on "file paths": what a reader types or
53
+ * Ruling 2026-09-05, narrowing the contract's flat ban on "file paths": what a reader types or
54
54
  * opens is allowed; our tree is not.
55
55
  *
56
56
  * Derived from the tree rather than typed, so a directory added next month is covered without anybody
@@ -89,7 +89,7 @@ export function refusals({ workflow, rootPkg }) {
89
89
  if (!/id-token:\s*write/.test(live)) add("the release workflow cannot request an OIDC token (`id-token: write` is gone)");
90
90
  if (!/environment:\s*npm\b/.test(live)) add("the release workflow no longer runs in the `npm` environment the publisher is registered under");
91
91
 
92
- // ── AND EVERY PUBLISHING JOB CARRIES BOTH, not merely the file somewhere (tracker issue 208) ────────
92
+ // ── AND EVERY PUBLISHING JOB CARRIES BOTH, not merely the file somewhere ────────────────────────────
93
93
  //
94
94
  // The two checks above ask whether the strings appear ANYWHERE. That was sufficient while one job
95
95
  // published. It stopped being sufficient the moment a second publishing job was added: a workflow
@@ -134,7 +134,7 @@ export function checksVerdict({ checkRuns = [], workflowRuns = [], policy = null
134
134
  /**
135
135
  * This file's own window: 32 attempts at 15 s, or eight minutes.
136
136
  *
137
- * NAMED SO AN ARM CAN COUPLE THE JOB'S BUDGET TO IT (tracker issue 247). The `version` job's
137
+ * NAMED SO AN ARM CAN COUPLE THE JOB'S BUDGET TO IT. The `version` job's
138
138
  * `timeout-minutes` has to contain THIS wait, and the arm that claimed to check that compared the job
139
139
  * against `release-await-cut.mjs`'s constant instead — a different wait, in a different job, which this
140
140
  * job never runs. It held by luck and would have failed the moment that unrelated number was raised.
@@ -231,7 +231,7 @@ async function main() {
231
231
  console.log(seen.reason);
232
232
  return;
233
233
  }
234
- // ── A PARKED RUN IS THE ORDINARY STATE OF A CUT, NOT A FAULT (tracker issue 208) ─────────────────
234
+ // ── A PARKED RUN IS THE ORDINARY STATE OF A CUT, NOT A FAULT ─────────────────────────────────────
235
235
  //
236
236
  // This failed the job, and failing it was worse than the thing it reported. Measured 2026-09-06:
237
237
  // a merge cut version pull request 59, its run parked awaiting approval as EVERY version branch's
@@ -54,7 +54,8 @@ export function groupVersion(root = ROOT) {
54
54
  * fixed group means one note commonly lands in several files unchanged.
55
55
  */
56
56
  export function assembleRoot(version, root = ROOT) {
57
- const bullets = [];
57
+ const entries = [];
58
+ const seen = new Set();
58
59
  for (const w of GROUP) {
59
60
  const p = join(root, w, "CHANGELOG.md");
60
61
  if (!existsSync(p)) continue;
@@ -68,17 +69,72 @@ export function assembleRoot(version, root = ROOT) {
68
69
  const sections = text.split(/^## /m).slice(1);
69
70
  const mine = sections.find((sec) => sec.split("\n", 1)[0].trim() === version);
70
71
  if (!mine) continue;
71
- for (const line of mine.split("\n")) {
72
- const b = /^\s*-\s+(.*\S)\s*$/.exec(line);
72
+ for (const note of notesOf(mine)) {
73
+ const key = note.join("\n");
74
+ if (seen.has(key)) continue;
75
+ seen.add(key);
76
+ entries.push(...note);
77
+ }
78
+ }
79
+ return { version, ...group(entries) };
80
+ }
81
+
82
+ /**
83
+ * One version section of a workspace changelog, as the notes it holds. Each note is the list of its
84
+ * entries in order, and each entry opens with the group it belongs under, unless none was named.
85
+ *
86
+ * EVERY PARAGRAPH IS AN ENTRY. Changesets writes a note's first line as a bullet and indents every later
87
+ * line beneath it. Reading only the bullets kept each note's first line and dropped the rest without a
88
+ * word: the 0.2.3 page reads "Five things a first-time reader could not act on." with none of the five
89
+ * under it, and a dry run of 0.3.0 put 8 of the 75 sentences in eight pending notes on the page
90
+ * (measured 2026-09-10). So each later paragraph is an entry of its own, and so is each list item.
91
+ *
92
+ * AN ENTRY THAT NAMES NO GROUP TAKES THE GROUP OF THE ENTRY BEFORE IT. A note opens with its group, and a
93
+ * paragraph that opens with another group starts that group's entries: `Fixed:` two paragraphs into a
94
+ * `New:` note is a fix, and so is the paragraph after it that explains the fix. A note whose first line
95
+ * names no group gives the rest of it none either, so the whole note is refused, not only its first line.
96
+ */
97
+ export function notesOf(section) {
98
+ const notes = [];
99
+ let note = null, lines = null, current = null;
100
+ const close = () => {
101
+ if (!lines) return;
102
+ const text = lines.join(" ");
103
+ lines = null;
104
+ const own = /^(New|Fixed|For operators):\s/.exec(text)?.[1];
105
+ if (own) current = own;
106
+ note.push(own || !current ? text : `${current}: ${text}`);
107
+ };
108
+ for (const line of section.split("\n").slice(1)) {
109
+ const top = /^-\s+(.*\S)\s*$/.exec(line);
110
+ const item = /^\s+-\s+(.*\S)\s*$/.exec(line);
111
+ if (top) {
112
+ close();
113
+ note = [];
114
+ notes.push(note);
115
+ current = null;
73
116
  // THE COMMIT SHA COMES OFF. Changesets' default generator prefixes every bullet with the commit
74
117
  // that carried the note, so a squashed release writes the SAME seven characters at the head of
75
118
  // every line — `- f7c1570:` seven times over, telling a reader nothing they can use. This is the
76
119
  // file a customer opens to decide whether to upgrade; the provenance they can act on is the tag
77
120
  // and the release page, both of which name that commit once.
78
- if (b && !bullets.includes(b[1])) bullets.push(b[1].replace(/^[0-9a-f]{7,40}: /, ""));
121
+ lines = [top[1].replace(/^[0-9a-f]{7,40}: /, "")];
122
+ } else if (!note) {
123
+ continue; // the version heading, or anything before the first note
124
+ } else if (line.startsWith("#")) {
125
+ close(); // `### Patch Changes`: a new set of notes starts here
126
+ note = null;
127
+ } else if (!line.trim()) {
128
+ close(); // a blank line ends a paragraph
129
+ } else if (item) {
130
+ close();
131
+ lines = [item[1]];
132
+ } else {
133
+ (lines ??= []).push(line.trim()); // a paragraph's next line, or its first after a blank line
79
134
  }
80
135
  }
81
- return { version, ...group(bullets) };
136
+ close();
137
+ return notes;
82
138
  }
83
139
 
84
140
  /**
@@ -14,7 +14,7 @@
14
14
  // retired it from every surface this product renders. Fronting the README with it would put a Swiss
15
15
  // firm's logo on an open-source product — the exact thing that retirement was about.
16
16
  //
17
- // The wordmark renders LOWERCASE (owner ruling 2026-08-21) while the name in prose does not, and it
17
+ // The wordmark renders LOWERCASE (ruling 2026-08-21) while the name in prose does not, and it
18
18
  // renders as SVG <text> in a GENERIC monospace stack: GitHub serves a README image sandboxed, so a
19
19
  // webfont would not load and a named family the reader lacks would fall back to something unrelated.
20
20
 
@@ -216,7 +216,7 @@ const PROBE = `<script>
216
216
  try { parent.postMessage({probeError:String(e && e.message || e)},'*'); } catch (e2) {}
217
217
  }
218
218
  }
219
- // An error in here must not read as "no measurement" — that is the ambiguity tracker issue 1155 was about.
219
+ // An error in here must not read as "no measurement" — that is the ambiguity this was about.
220
220
  window.addEventListener('error', function(ev){
221
221
  try { parent.postMessage({probeError:String(ev && ev.message || ev)},'*'); } catch (e) {}
222
222
  });
@@ -508,7 +508,7 @@ async function main() {
508
508
  console.log(" Expect the sideways assertion to FAIL. A clean run here means the instrument is blind.\n");
509
509
  }
510
510
  let failures = 0;
511
- // A COULD-NOT-LOOK IS NOT A FAILED MEASUREMENT (tracker issue 239). Counted apart from `failures`
511
+ // A COULD-NOT-LOOK IS NOT A FAILED MEASUREMENT. Counted apart from `failures`
512
512
  // because the two mean different things to whoever reads the exit code: 1 says the layout is wrong,
513
513
  // 2 says nothing was measured. Merging them is how a starved runner sends a reader to look at CSS.
514
514
  let unmeasured = 0;
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // ── why this exists ─────────────────────────────────────────────────────────────────────────────────
6
6
  //
7
- // Tracker issue 198. `health-tells-the-truth-about-the-bundle` planted a future mtime on a REAL file in
7
+ // `health-tells-the-truth-about-the-bundle` planted a future mtime on a REAL file in
8
8
  // `portal-ui/src` to prove the staleness check fires, restored it afterwards, and went green. But
9
9
  // `node --test` runs test FILES concurrently against one shared working tree, so for the seconds that
10
10
  // plant was live, any other arm reading that path saw a tree from the future. The suite cannot
@@ -71,7 +71,7 @@ function main() {
71
71
  "--virtual-time-budget=8000", "--dump-dom", `file://${join(work, "verify.html")}`,
72
72
  ], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 120000, env: browserEnv(work) });
73
73
 
74
- // ── IS THIS OUR PAGE, OR CHROME'S ERROR PAGE? (tracker issue 227) ───────────────────────────────
74
+ // ── IS THIS OUR PAGE, OR CHROME'S ERROR PAGE? ───────────────────────────────────────────────────
75
75
  //
76
76
  // `--dump-dom` prints whatever chrome ended up showing, and a file it could not read produces the
77
77
  // interstitial — which has a `<title>` of its own. This was saved from reporting a pass only by the
@@ -77,7 +77,7 @@ const cmd = (method, params = {}) => new Promise((r) => { const n = ++id; pendin
77
77
 
78
78
  await cmd("Page.enable");
79
79
 
80
- // ── IS THIS THE REPORT, OR CHROME'S OWN ERROR PAGE? (tracker issue 227) ──────────────────────────────
80
+ // ── IS THIS THE REPORT, OR CHROME'S OWN ERROR PAGE? ──────────────────────────────────────────────────
81
81
  //
82
82
  // Chrome is launched with the file URL as an ARGUMENT, so there is no navigation response to check and
83
83
  // nothing here ever asked. When the file could not be read, Chrome showed `ERR_ACCESS_DENIED` — a page
@@ -126,7 +126,7 @@ if (typeof top !== "number") {
126
126
  console.error(`report-screenshot: no element matched ${JSON.stringify(ANCHOR)} — nothing to anchor the frame to.`);
127
127
  chrome.kill(); process.exit(1);
128
128
  }
129
- // ── CAN THIS BOX DRAW WHAT THE PAGE SAYS? (tracker issue 227) ───────────────────────────────────────
129
+ // ── CAN THIS BOX DRAW WHAT THE PAGE SAYS? ───────────────────────────────────────────────────────────
130
130
  //
131
131
  // The default demo product's report carries the mark's native-script renderings — ベンクリ, ベンコリ,
132
132
  // ヴェンコリ — and they are load-bearing: the verdict sentence reads "A live Japanese class 9
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
- // Retire bare `#NNN` tracker references from prose (tracker issue 188).
4
+ // Retire bare `#NNN` tracker references from prose.
5
5
  //
6
6
  // node scripts/retire-bare-refs.mjs what would change, per file
7
7
  // node scripts/retire-bare-refs.mjs --apply change it
@@ -146,7 +146,7 @@ const server = createServer((req, res) => {
146
146
  hits.push({ epoch, route: routeKey(p, u.searchParams) })
147
147
  }
148
148
 
149
- if (p === '/portal/api/me') return json(res, { email: 'staff@example-firm.com', role: 'staff', accounts: '*', accountNames: {}, allowance: null })
149
+ if (p === '/portal/api/me') return json(res, { email: 'manager@example-firm.com', permissions: { run: true, manage: true }, access: [{ kind: 'everything' }], accounts: '*', accountNames: {}, allowance: null })
150
150
  if (p === '/portal/admin/roster') return json(res, { customers: [{ key: KEY, name: NAME }] })
151
151
  if (p === '/portal/admin/families') return json(res, { of: {}, names: {} })
152
152
  if (p === '/portal/api/runs') return json(res, { runs: RUNS() })
package/scripts/score.mjs CHANGED
@@ -646,7 +646,7 @@ function print(id, ref, run, s, delta, refPath) {
646
646
  for (const c of B.collisions)
647
647
  console.log(` · ${c.owner}: reference "${c.entry}" is ${c.bucket}, surfaced "${c.noise}" is noise`);
648
648
  }
649
- // ── SAME PROPRIETOR, DIFFERENT MARKS — REPORTED, NEVER SUPPRESSING (tracker issue 249) ───────────
649
+ // ── SAME PROPRIETOR, DIFFERENT MARKS — REPORTED, NEVER SUPPRESSING ───────────────────────────────
650
650
  //
651
651
  // These used to print under the line above, which tells the reader not to read the recall numbers.
652
652
  // One large filer anywhere in a matter therefore suppressed a whole run's measurement, and did: R2's