clearotron 0.4.0-beta.0 → 0.4.0-beta.2

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 (293) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +4 -31
  3. package/README.md +5 -5
  4. package/bin/brandowner.mjs +1 -1
  5. package/bin/onboard.mjs +18 -74
  6. package/bin/start.mjs +58 -14
  7. package/bin/update.mjs +10 -2
  8. package/build-info.json +2 -2
  9. package/demo/full-country-search/run/_driver/search-policy.json +1 -1
  10. package/demo/full-country-search/run/status.json +1 -1
  11. package/demo/global-preliminary-search/run/_driver/search-policy.json +1 -1
  12. package/demo/global-preliminary-search/run/status.json +1 -1
  13. package/demo/knockout-search/run/_driver/search-policy.json +1 -1
  14. package/demo/knockout-search/run/status.json +1 -1
  15. package/demo/multi-country-focus-search/run/_driver/search-policy.json +1 -1
  16. package/demo/multi-country-focus-search/run/status.json +1 -1
  17. package/docs/README.md +1 -0
  18. package/docs/SECURITY-OWASP.md +298 -0
  19. package/docs/SECURITY.md +18 -13
  20. package/docs/architecture/02-architecture.md +5 -5
  21. package/docs/architecture/03-run-lifecycle.md +28 -29
  22. package/docs/architecture/04-configuration-reference.md +5 -10
  23. package/docs/architecture/05-config-governance.md +5 -7
  24. package/docs/architecture/06-operations-runbook.md +0 -12
  25. package/docs/architecture/07-quality-and-audit.md +12 -13
  26. package/docs/architecture/09-security-and-data.md +9 -4
  27. package/driver/CHANGELOG.md +70 -0
  28. package/driver/ask-ledger.mjs +4 -2
  29. package/driver/authority-trees.mjs +90 -8
  30. package/driver/band-shape.mjs +13 -13
  31. package/driver/blind-frame-model.mjs +1 -1
  32. package/driver/case-law-ledger.mjs +19 -3
  33. package/driver/claim-liveness.mjs +30 -1
  34. package/driver/clearance-variants-record.mjs +10 -3
  35. package/driver/common-law-receipts.mjs +64 -14
  36. package/driver/commonlaw-carry.mjs +2 -2
  37. package/driver/compare.mjs +2 -2
  38. package/driver/connotation-search.mjs +33 -104
  39. package/driver/contract-arm2-baseline.json +6 -5
  40. package/driver/contract-e3-backlog.mjs +62 -62
  41. package/driver/contract-vocabulary.mjs +38 -31
  42. package/driver/coverage-form-io.mjs +13 -1
  43. package/driver/coverage-form.mjs +42 -4
  44. package/driver/coverage-ledger.mjs +3 -5
  45. package/driver/cross-check-wait.mjs +43 -0
  46. package/driver/declination-call.mjs +34 -10
  47. package/driver/declination-tool.mjs +27 -15
  48. package/driver/deferral-row.mjs +139 -0
  49. package/driver/degraded-parts.mjs +377 -0
  50. package/driver/deliver-trigger.sh +1 -1
  51. package/driver/demo-container.mjs +4 -1
  52. package/driver/dev-portal.mjs +5 -5
  53. package/driver/dispatch-record.mjs +2 -2
  54. package/driver/driver.config.mjs +193 -96
  55. package/driver/e2e/README.md +1 -1
  56. package/driver/effective-scope.mjs +58 -2
  57. package/driver/engine/anthropic-agent.mjs +103 -27
  58. package/driver/engine/auth.mjs +2 -2
  59. package/driver/engine/child-record.mjs +11 -0
  60. package/driver/engine/cli-version.mjs +4 -3
  61. package/driver/engine/common.mjs +17 -2
  62. package/driver/engine/deny-authority-write.mjs +3 -1
  63. package/driver/engine/engine-env.mjs +174 -0
  64. package/driver/engine/engine-spawn.mjs +179 -0
  65. package/driver/engine/mcp/band-server.mjs +1 -1
  66. package/driver/engine/mcp/clarivate-server.mjs +2 -1
  67. package/driver/engine/mcp/codex-config.mjs +112 -3
  68. package/driver/engine/mcp/corsearch-server.mjs +1 -0
  69. package/driver/engine/mcp/declination-server.mjs +9 -8
  70. package/driver/engine/mcp/dispositions-server.mjs +2 -1
  71. package/driver/engine/mcp/euipo-server.mjs +1 -0
  72. package/driver/engine/mcp/fetch-server.mjs +14 -11
  73. package/driver/engine/mcp/free-tier-server.mjs +1 -1
  74. package/driver/engine/mcp/gather-config.mjs +23 -11
  75. package/driver/engine/mcp/perplexity-server.mjs +97 -17
  76. package/driver/engine/mcp/probe-server.mjs +43 -21
  77. package/driver/engine/mcp/public-fetch.mjs +195 -0
  78. package/driver/engine/mcp/recording-server.mjs +79 -14
  79. package/driver/engine/mcp/signa-server.mjs +1 -0
  80. package/driver/engine/mcp/supplemental.mjs +14 -14
  81. package/driver/engine/mcp/unit-note-server.mjs +50 -1
  82. package/driver/engine/mcp/uspto-local-server.mjs +1 -0
  83. package/driver/engine/openai-agent.mjs +109 -28
  84. package/driver/engine/probe.mjs +152 -19
  85. package/driver/enqueue-schema.mjs +5 -5
  86. package/driver/feedback-issues.mjs +1 -1
  87. package/driver/feedback-store.mjs +1 -1
  88. package/driver/findings-model.mjs +67 -16
  89. package/driver/flag-snapshot.mjs +1 -1
  90. package/driver/floor-duty.mjs +23 -7
  91. package/driver/form-neighbourhood.mjs +52 -17
  92. package/driver/frame-diff-model.mjs +3 -3
  93. package/driver/framework-method.mjs +304 -0
  94. package/driver/gateway.mjs +49 -21
  95. package/driver/hand-off-exits.mjs +129 -0
  96. package/driver/jx-lanes.mjs +1 -1
  97. package/driver/jx.mjs +8 -5
  98. package/driver/knockout-assess-record.mjs +7 -1
  99. package/driver/knockout-frame-record.mjs +96 -34
  100. package/driver/knockout-review-record.mjs +7 -2
  101. package/driver/log.mjs +2 -2
  102. package/driver/matter-frame-record.mjs +31 -2
  103. package/driver/methodology-witness.mjs +6 -3
  104. package/driver/named-band.mjs +1 -1
  105. package/driver/owner-use-check.mjs +45 -10
  106. package/driver/package.json +1 -1
  107. package/driver/partial-payload-baseline.json +4 -0
  108. package/driver/phase0.mjs +2 -2
  109. package/driver/pipeline-knockout.mjs +256 -129
  110. package/driver/pipeline.mjs +417 -614
  111. package/driver/placement-carry.mjs +22 -3
  112. package/driver/placement-form-io.mjs +18 -4
  113. package/driver/placement-form.mjs +53 -4
  114. package/driver/placement-union.mjs +32 -5
  115. package/driver/portal-report.mjs +3 -3
  116. package/driver/portal-service.mjs +6 -5
  117. package/driver/portal-static.mjs +11 -18
  118. package/driver/predelivery-lint.mjs +10 -10
  119. package/driver/profile-page.html +5 -5
  120. package/driver/profiles.mjs +5 -5
  121. package/driver/progress.mjs +6 -4
  122. package/driver/provider-usage.mjs +25 -2
  123. package/driver/publish/index.mjs +97 -25
  124. package/driver/publish/knockout.mjs +136 -14
  125. package/driver/publish/profiles-page.mjs +2 -0
  126. package/driver/publish/publish-inputs.mjs +10 -4
  127. package/driver/publish/render-knockout.mjs +48 -21
  128. package/driver/publish/render.mjs +13 -8
  129. package/driver/publish/report-data.mjs +2 -0
  130. package/driver/publish/search-depth.mjs +77 -4
  131. package/driver/publish/templates/report.css +1 -1
  132. package/driver/publish/xlsx.mjs +34 -15
  133. package/driver/reasoning-tripwires.mjs +2 -160
  134. package/driver/recall-receipt.mjs +27 -0
  135. package/driver/recall-reconciliation.mjs +1 -1
  136. package/driver/record-carry.mjs +14 -14
  137. package/driver/recording-agreement.mjs +2 -2
  138. package/driver/reference-score.mjs +169 -47
  139. package/driver/register-count.mjs +61 -4
  140. package/driver/register-digest-record.mjs +14 -1
  141. package/driver/register-plan.mjs +161 -80
  142. package/driver/register-records.mjs +37 -4
  143. package/driver/registration-scripts.mjs +22 -0
  144. package/driver/registry-fidelity.mjs +4 -4
  145. package/driver/repair-composers.mjs +23 -7
  146. package/driver/repair-contract.mjs +1 -1
  147. package/driver/repairs.mjs +5 -2
  148. package/driver/reviewer-open-points.mjs +33 -0
  149. package/driver/rule-shape.mjs +8 -8
  150. package/driver/run-economics.mjs +3 -3
  151. package/driver/run-integrity.mjs +2 -2
  152. package/driver/runner.mjs +17 -5
  153. package/driver/scope-facts.mjs +89 -12
  154. package/driver/score-redaction.mjs +247 -0
  155. package/driver/screen-gate.mjs +1 -1
  156. package/driver/search-policy.mjs +17 -0
  157. package/driver/skills/README.md +2 -2
  158. package/driver/skills/blind-frame/SKILL.md +2 -2
  159. package/driver/skills/clearance-common-law/SKILL.md +29 -36
  160. package/driver/skills/clearance-common-law/perplexity-prompts.md +8 -30
  161. package/driver/skills/clearance-register/SKILL.md +15 -16
  162. package/driver/skills/clearance-register/digest.md +15 -31
  163. package/driver/skills/clearance-register/register-recipes.md +14 -56
  164. package/driver/skills/clearance-register/unit.md +37 -48
  165. package/driver/skills/clearance-search/SKILL.md +6 -8
  166. package/driver/skills/clearance-search/delivery-contract.md +0 -7
  167. package/driver/skills/clearance-search/firm-wide-reasoning.md +5 -4
  168. package/driver/skills/clearance-search/phase2-execution.md +5 -6
  169. package/driver/skills/clearance-search/report-prose.md +5 -5
  170. package/driver/skills/clearance-search/risk-framework-triage.md +2 -2
  171. package/driver/skills/clearance-search/synthesis-rules.md +9 -9
  172. package/driver/skills/clearance-search/template-formatting.md +2 -2
  173. package/driver/skills/clearance-variants/SKILL.md +8 -8
  174. package/driver/skills/clearance-variants/transliteration-scripts.md +2 -2
  175. package/driver/skills/frame-diff/SKILL.md +2 -2
  176. package/driver/skills/knockout-assess/SKILL.md +40 -10
  177. package/driver/skills/knockout-frame/SKILL.md +42 -10
  178. package/driver/skills/matter-frame/SKILL.md +12 -5
  179. package/driver/skills/matter-frame/watchlist-reference.md +1 -1
  180. package/driver/skills/narrative-refutation/SKILL.md +5 -3
  181. package/driver/skills/placement-inquiry/SKILL.md +5 -1
  182. package/driver/stage-context.mjs +27 -6
  183. package/driver/stages-knockout.mjs +92 -79
  184. package/driver/stages.mjs +111 -68
  185. package/driver/stray-artifacts.mjs +9 -5
  186. package/driver/suite-census.json +696 -150
  187. package/driver/synthesis-record.mjs +26 -4
  188. package/driver/systemd/README.md +5 -6
  189. package/driver/systemd/clearotron-client-mcp.service +24 -0
  190. package/driver/systemd/clearotron-mcp-face.service +24 -0
  191. package/driver/systemd/clearotron-portal.service +24 -0
  192. package/driver/systemd/clearotron-worker.service +29 -0
  193. package/driver/tokens.mjs +10 -9
  194. package/driver/turnaround-bands.mjs +1 -1
  195. package/driver/unit-inventory.mjs +24 -43
  196. package/driver/variant-manifest-model.mjs +22 -3
  197. package/driver/verify-knockout.mjs +163 -4
  198. package/driver/verify.mjs +42 -16
  199. package/driver/web-grid.mjs +150 -0
  200. package/driver/whatif-memo-run.mjs +1 -1
  201. package/driver/withheld-families.mjs +113 -4
  202. package/driver/worker-heartbeat.mjs +13 -2
  203. package/mcp-server/CHANGELOG.md +8 -0
  204. package/mcp-server/README.md +1 -1
  205. package/mcp-server/lib/knockout.mjs +1 -1
  206. package/mcp-server/lib/ops.mjs +10 -10
  207. package/mcp-server/lib/scrub.mjs +1 -1
  208. package/mcp-server/lib/trace.mjs +3 -2
  209. package/mcp-server/package.json +1 -1
  210. package/mcp-server/server.mjs +3 -3
  211. package/package.json +1 -1
  212. package/portal-ui/dist/assets/{index-DVtz44vH.js → index-BPAUjcI0.js} +3 -2
  213. package/portal-ui/dist/assets/{index-5CCwiJG7.css → index-D2wrw9cH.css} +18 -2
  214. package/portal-ui/dist/assets/plus-jakarta-sans-BUCHxqJ-.woff2 +0 -0
  215. package/portal-ui/dist/index.html +2 -15
  216. package/portal-ui/package.json +1 -1
  217. package/providers/_shared/README.md +1 -1
  218. package/providers/_shared/answer-memory.mjs +199 -0
  219. package/providers/_shared/enumerate.mjs +155 -50
  220. package/providers/_shared/execute-plan.mjs +62 -16
  221. package/providers/_shared/ledger-path.mjs +1 -1
  222. package/providers/_shared/ledger.mjs +48 -6
  223. package/providers/_shared/plan-guards.mjs +7 -0
  224. package/providers/_shared/script-form.mjs +24 -5
  225. package/providers/_shared/term-shape.mjs +5 -5
  226. package/providers/clarivate/src/capabilities.js +11 -0
  227. package/providers/clarivate/src/core.js +193 -18
  228. package/providers/corsearch/README.md +1 -2
  229. package/providers/corsearch/src/core.js +2 -2
  230. package/providers/jx-subclass/lookup.mjs +1 -1
  231. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  232. package/providers/oauth-mcp-bridge/README.md +8 -7
  233. package/providers/oauth-mcp-bridge/bridge.mjs +21 -5
  234. package/providers/oauth-mcp-bridge/package.json +1 -1
  235. package/providers/oauth-mcp-bridge/systemd/courtlistener-mcp.service +1 -1
  236. package/providers/oauth-mcp-bridge/warm-server.mjs +16 -5
  237. package/providers/perplexity/src/core.js +246 -21
  238. package/providers/signa/src/capabilities.js +24 -0
  239. package/providers/signa/src/core.js +93 -25
  240. package/scripts/README.md +2 -1
  241. package/scripts/added-reference-check.mjs +2 -1
  242. package/scripts/backup-recall-stores.mjs +5 -5
  243. package/scripts/demo-evidence.mjs +2 -1
  244. package/scripts/deprecate-below.mjs +114 -2
  245. package/scripts/drive-env-check.mjs +2 -1
  246. package/scripts/e2e-first-time.mjs +469 -0
  247. package/scripts/e2e-scenario-ops.mjs +441 -0
  248. package/scripts/e2e.mjs +104 -16
  249. package/scripts/env-audit.mjs +8 -0
  250. package/scripts/freeze-example-run.mjs +7 -7
  251. package/scripts/generated-files-are-current.mjs +2 -1
  252. package/scripts/hand-off-exits-probe.mjs +75 -0
  253. package/scripts/import-cycle-check.mjs +2 -1
  254. package/scripts/merge-shape-check.mjs +2 -1
  255. package/scripts/package-size-budget.mjs +2 -1
  256. package/scripts/record-carry-probe.mjs +1 -1
  257. package/scripts/release-entry-catch-up.mjs +77 -8
  258. package/scripts/release-install-check.mjs +4 -1
  259. package/scripts/release-note-required.mjs +139 -9
  260. package/scripts/release-rehearsal-version.mjs +60 -0
  261. package/scripts/release-sbom.mjs +104 -0
  262. package/scripts/release-visible-check.mjs +7 -5
  263. package/scripts/render-check.mjs +5 -2
  264. package/scripts/report-offline-render-check.mjs +169 -0
  265. package/scripts/score.mjs +79 -8
  266. package/scripts/strip-titles-and-attributions.mjs +2 -1
  267. package/scripts/strip-tracker-citations.mjs +2 -1
  268. package/scripts/test-full.mjs +2 -1
  269. package/scripts/test-run.mjs +10 -3
  270. package/scripts/third-party-notices.mjs +3 -1
  271. package/scripts/travelling-predicates.mjs +1 -1
  272. package/scripts/writing-standard-check.mjs +2 -1
  273. package/shared/brand-fonts.mjs +59 -0
  274. package/shared/brand.mjs +6 -6
  275. package/shared/browser-temp-root.mjs +3 -2
  276. package/shared/doctrine-overlay.mjs +1 -1
  277. package/shared/driver-dir.mjs +47 -11
  278. package/shared/fonts/OFL-fira-code.txt +93 -0
  279. package/shared/fonts/OFL-plus-jakarta-sans.txt +93 -0
  280. package/shared/fonts/README.md +37 -0
  281. package/shared/fonts/fira-code.woff2 +0 -0
  282. package/shared/fonts/plus-jakarta-sans.woff2 +0 -0
  283. package/shared/names-in-force.mjs +2 -0
  284. package/shared/npm-cli.mjs +23 -0
  285. package/shared/os-advice.mjs +14 -2
  286. package/shared/path-seps.mjs +42 -0
  287. package/shared/process-table.mjs +71 -3
  288. package/shared/reference-guard-classes.mjs +30 -6
  289. package/shared/root-doc-commands.mjs +6 -2
  290. package/shared/running-start.mjs +24 -7
  291. package/shared/scope.mjs +2 -2
  292. package/shared/wsl.mjs +1 -1
  293. package/driver/known-conflicts.mjs +0 -327
package/scripts/score.mjs CHANGED
@@ -3,7 +3,13 @@
3
3
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
4
4
  // score.mjs — score a completed run against the lawyer's reference answer for its scenario.
5
5
  //
6
- // node scripts/score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json]
6
+ // node scripts/score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json] [--names]
7
+ //
8
+ // `--names` prints the reference's marks, proprietors and sentences. WITHOUT it they are withheld and
9
+ // each is shown as a stable `«name N»` token, because scoring a run is routine work done in sessions
10
+ // that record everything they print, and a reference answer is a real lawyer's answer to a real matter.
11
+ // Every number, axis and bucket is unaffected: an entry's index answers every question this tool asks.
12
+ // The flag is for the one job that needs the words, reading a surprising result against the finding text.
7
13
  //
8
14
  // `--previous auto` resolves the OTHER HALF OF A PAIR: the run of the round immediately before
9
15
  // this one, same scenario, same door. It reads the scenario's declared refs from the config store and
@@ -56,6 +62,9 @@ import { recordQids } from "../driver/named-band.mjs";
56
62
  // reason it is not in scripts/e2e.mjs: this scorer is deliberately offline, and importing the harness
57
63
  // would drag portal-mcp-client, enqueue-schema and door-gates in behind it.
58
64
  import { previousRunDir, scenarioRefs } from "../driver/e2e-rounds.mjs";
65
+ // The names rule, kept out of this file so it is testable without a run directory — the same reason
66
+ // reference-score.mjs holds the scoring rules rather than this script.
67
+ import { protectedStrings, redactor, installRedaction, unclassifiedNotice, REDACTION_NOTICE } from "../driver/score-redaction.mjs";
59
68
  import { readSettleStamp } from "../driver/settle-stamp.mjs";
60
69
  import { envFrom } from "../shared/env-aliases.mjs"; // — resolves EITHER spelling; names the retired one because that is the live-writable half
61
70
 
@@ -133,6 +142,9 @@ function findingsOf(runDir) {
133
142
  // layer is load-bearing alone.
134
143
  ordinal: Number.isInteger(f.ordinal) ? f.ordinal : null,
135
144
  mark: f.mark ?? "", owner: ownerName(f.owner), band: f.band?.label ?? null,
145
+ // The offices the finding's records are filed in, for the owner-or-country join: the office
146
+ // segment of each registration's `/mark/<office>/…` uri, else the owner's country.
147
+ territories: officesOf(f.owner),
136
148
  disposition: f.disposition ?? null, ruled_out: f.ruled_out === true,
137
149
  ruled_out_reason: f.ruled_out_reason ?? null,
138
150
  // — the EVIDENCE CLASS, derived at the artifact boundary so the scorer stays pure and
@@ -204,6 +216,18 @@ function findingsOf(runDir) {
204
216
  return { lane: null, rows: [], subjects: null };
205
217
  }
206
218
 
219
+ /** The office a record id names: `/mark/<office>/…` → `<office>`, else null. */
220
+ function officeOfRecord(recordId) {
221
+ const m = /^\/mark\/([a-z]{2,3})\//i.exec(String(recordId ?? ""));
222
+ return m ? m[1] : null;
223
+ }
224
+
225
+ /** The offices a finding's owner is filed in, from its registration uris; else the owner's country. */
226
+ function officesOf(owner) {
227
+ const offices = [...new Set((owner?.registrations ?? []).map((r) => officeOfRecord(r?.uri)).filter(Boolean))];
228
+ return offices.length ? offices : (owner?.country ? [owner.country] : []);
229
+ }
230
+
207
231
  /**
208
232
  * Every label the run RETRIEVED — the corpus `withheld` is measured against.
209
233
  *
@@ -227,7 +251,7 @@ function retrievedOf(runDir) {
227
251
  const k = `${mark} ${record_id ?? ""}`;
228
252
  if (!mark) return;
229
253
  const prior = out.get(k);
230
- if (!prior) { out.set(k, { mark, record_id: record_id ?? null, owner: owner ?? null, qids: [...new Set(qids ?? [])] }); return; }
254
+ if (!prior) { out.set(k, { mark, record_id: record_id ?? null, owner: owner ?? null, territory: officeOfRecord(record_id), qids: [...new Set(qids ?? [])] }); return; }
231
255
  for (const q of qids ?? []) if (!prior.qids.includes(q)) prior.qids.push(q); // union across both sources
232
256
  };
233
257
 
@@ -271,6 +295,17 @@ function searchedTextOf(runDir) {
271
295
  const d = driverDir(runDir);
272
296
  for (const f of readdirSync(d)) if (/^grid-spec.*\.json$/.test(f)) parts.push(readText(join(d, f)));
273
297
  } catch { /* no _driver — reported by the caller */ }
298
+ // THE KNOCKOUT LANE WRITES NONE OF THE ABOVE. It keeps no plan-execution record and no grid ledger:
299
+ // where it looked is recorded in what it cites, the evidence addresses on each finding in
300
+ // knockout-findings.json and the pages its research notes name, one file per mark under research/.
301
+ // Read only the files above, a knockout printed every channel ABSENT while its findings cited the
302
+ // very store the reference names. Neither file exists on the clearance lane, so its reading is
303
+ // unchanged.
304
+ parts.push(readText(join(runDir, "knockout-findings.json")));
305
+ try {
306
+ const r = join(runDir, "research");
307
+ for (const f of readdirSync(r)) if (/\.md$/.test(f)) parts.push(readText(join(r, f)));
308
+ } catch { /* no research/ — a clearance run, or a knockout that wrote no notes */ }
274
309
  return parts.join("\n");
275
310
  }
276
311
 
@@ -435,7 +470,7 @@ function scoreOne(run, ref) {
435
470
  //
436
471
  // Refusing the unshaped pairing was only half of it. A gold set that gains a `counts` block usually
437
472
  // KEEPS its lawyer sheet — it is a real answer to a real matter and deleting it loses history — and
438
- // scoring both prints `LOST · TIKI PUNCH` beside the count that measures the product. That is the same
473
+ // scoring both prints `LOST · WAVO PUNCH` beside the count that measures the product. That is the same
439
474
  // structural zero the issue is about, one row smaller, and it lands in the bucket table a reader
440
475
  // scans first.
441
476
  //
@@ -446,7 +481,7 @@ function scoreOne(run, ref) {
446
481
  const buckets = scoreRecall({
447
482
  reference: countShaped ? [] : ref.register, findings: run.findings, retrieved: run.retrieved,
448
483
  scopeClasses: run.scopeClasses, scopeTerritories: run.scopeTerritories, registerOnly, collapseReason, preAccepted: ref.pre_accepted ?? [],
449
- coverage,
484
+ coverage, searchedMarks: [...(ref.covers_marks ?? []), ref.mark].filter(Boolean),
450
485
  });
451
486
  return {
452
487
  registerOnly, collapseReason, coverage, buckets, countShaped,
@@ -644,7 +679,7 @@ function print(id, ref, run, s, delta, refPath) {
644
679
  console.log(renderCarryThrough(run.dir, { indent: " " }).lines.join("\n"));
645
680
 
646
681
  // — a record in two buckets is a defect in THIS TOOL, and it is printed as loudly as any score.
647
- // Silence here is what let `DELPHI GENETICS` sit in LOST and `DG DELPHI GENETICS` in NOISE on the same
682
+ // Silence here is what let `KORPHI GENETICS` sit in LOST and `DG KORPHI GENETICS` in NOISE on the same
648
683
  // page, and the recall number that reached the round handover was wrong for a week because nobody
649
684
  // reads two lists against each other.
650
685
  if (B.collisions?.length) {
@@ -656,7 +691,7 @@ function print(id, ref, run, s, delta, refPath) {
656
691
  //
657
692
  // These used to print under the line above, which tells the reader not to read the recall numbers.
658
693
  // One large filer anywhere in a matter therefore suppressed a whole run's measurement, and did: R2's
659
- // real 88% → 63% recall movement went unquoted on the regression issue because `Novartis AG` held one
694
+ // real 88% → 63% recall movement went unquoted on the regression issue because `Norvanta AG` held one
660
695
  // withheld reference mark and one surfaced non-reference mark. Those two rows are both true.
661
696
  //
662
697
  // Kept visible because a reader may still want the pairing, and deliberately WITHOUT a verdict: this
@@ -877,7 +912,7 @@ function print(id, ref, run, s, delta, refPath) {
877
912
 
878
913
  // ── main ─────────────────────────────────────────────────────────────────────────────────────────────
879
914
 
880
- const USAGE = `usage: score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json]`;
915
+ const USAGE = `usage: score.mjs <ID> --run <runDir> [--previous <runDir>|auto] [--json] [--names]`;
881
916
 
882
917
  /**
883
918
  * `--previous auto` — the other half of the pair, resolved rather than typed.
@@ -907,9 +942,10 @@ function resolvePreviousAuto(id, runDir) {
907
942
  }
908
943
 
909
944
  // One pass, so a directory named like a flag's value can never be mistaken for the scenario id.
910
- const opts = { json: false, run: null, previous: null, id: null };
945
+ const opts = { json: false, run: null, previous: null, id: null, names: false };
911
946
  for (let i = 0, a = process.argv.slice(2); i < a.length; i++) {
912
947
  if (a[i] === "--json") opts.json = true;
948
+ else if (a[i] === "--names") opts.names = true;
913
949
  else if (a[i] === "--run") opts.run = a[++i];
914
950
  else if (a[i] === "--previous") opts.previous = a[++i];
915
951
  else if (a[i].startsWith("--")) die(`unknown flag ${a[i]}\n${USAGE}`);
@@ -929,11 +965,40 @@ const previousDir = opts.previous === "auto" ? resolvePreviousAuto(id, runDir) :
929
965
  const prev = previousDir ? scoreOne(readRun(previousDir, ref), ref) : null;
930
966
  const delta = prev ? bucketDelta(scored.buckets, prev.buckets) : null;
931
967
 
968
+ // ── NAMES ARE WITHHELD UNLESS SOMEBODY ASKS FOR THEM ─────────────────────────────────────────────────
969
+ //
970
+ // A reference answer is a real lawyer's answer to a real matter, and scoring a run is routine work done
971
+ // inside sessions that record everything they print. So the default output carries the axes, the buckets
972
+ // and which entry fell in which, with every mark, proprietor and reference sentence swapped for a stable
973
+ // token; `--names` prints them as before.
974
+ //
975
+ // INSTALLED OVER THE STREAM, not at each of the twenty-odd sites that interpolate a name. Redacting
976
+ // per-site is the shape that leaves one site out, and it would have to be revisited by every later edit
977
+ // that adds a print. Here `renderCarryThrough`'s lines and anything added after this was written are
978
+ // covered without anyone remembering to.
979
+ //
980
+ // The two inputs are the reference AND the scored buckets: a proprietor this run surfaced that the
981
+ // lawyer never named is still somebody's name, and it reaches the page through `noise`.
982
+ let unclassifiedKeys = [];
983
+ if (!opts.names) {
984
+ const { names, prose, unclassified } = protectedStrings({ reference: ref, scored });
985
+ unclassifiedKeys = unclassified;
986
+ installRedaction(redactor({ names, prose }));
987
+ }
988
+
932
989
  if (opts.json) {
933
990
  console.log(JSON.stringify({
934
991
  // — the instrument that produced these numbers, so a reader comparing two archived scores can
935
992
  // tell whether the comparison is valid. A score with no `scorer_version` predates this stamp.
936
993
  scorer_version: SCORER_VERSION,
994
+ // — WHETHER THE NAMES IN THIS PAYLOAD ARE THE REAL ONES. `--json` goes to the same terminal the
995
+ // human path does, so it is redacted on the same rule; stamped rather than left to be inferred,
996
+ // because a consumer that cannot tell a redacted score from a named one will compare the two.
997
+ names: opts.names ? "printed" : "redacted",
998
+ // THE SAME WARNING THE HUMAN PATH PRINTS, AS A FIELD RATHER THAN A LINE. It cannot be printed here:
999
+ // this payload is parsed, and a sentence above it makes the whole document unreadable — which is how
1000
+ // it first went wrong. Keys only, never their values.
1001
+ names_unclassified: unclassifiedKeys,
937
1002
  // — the OTHER half of the same question. A consumer comparing two archived scores needs
938
1003
  // both instruments: which scorer read the run, and which engine produced it.
939
1004
  engine_commit: run.engineCommit,
@@ -949,6 +1014,12 @@ if (opts.json) {
949
1014
  carry_through: (() => { const ct = carryThrough(run.dir); return { ...ct, coverage: coverageConflicts(run.dir, ct.lost) }; })(),
950
1015
  }, null, 2));
951
1016
  } else {
1017
+ // Said before the first number, not after the last: a reader who stops at the top screen must know
1018
+ // which of the two readings they are holding.
1019
+ if (!opts.names) console.log(`\n ${REDACTION_NOTICE}`);
1020
+ // A reference that has grown a field this module does not classify is unprotected in exactly that
1021
+ // field, so the reader learns it before anything below, not after.
1022
+ if (unclassifiedKeys.length) console.log(` ${unclassifiedNotice(unclassifiedKeys)}`);
952
1023
  print(String(id).toUpperCase(), ref, run, scored, delta, refPath);
953
1024
  // — THE LAWYER'S OWN STATEMENTS OF WHAT THE RUN MUST DEMONSTRATE. The buckets cannot carry
954
1025
  // these: an assertion says WHY a mark matters, and that reasoning is what tells a reader which lane to
@@ -63,6 +63,7 @@ import { fileURLToPath } from "node:url";
63
63
  import { isScannable, EXCLUDED } from "./strip-tracker-citations.mjs";
64
64
  import { publishedOf } from "../shared/reference-guard-classes.mjs";
65
65
  import { newBareCitations } from "./citation-line-check.mjs";
66
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
66
67
 
67
68
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
68
69
  const APPLY = process.argv.includes("--apply");
@@ -357,7 +358,7 @@ export function surveyOf(files, read) {
357
358
 
358
359
  const total = (o) => Object.values(o).reduce((a, n) => a + n, 0);
359
360
 
360
- if (import.meta.url === `file://${process.argv[1]}`) {
361
+ if (isEntrypoint(import.meta.url)) {
361
362
  // THE PUBLISHED POPULATION, NOT THE INDEX — the same helper, for the same reason, as the opener sweep.
362
363
  const all = execFileSync("git", ["-C", ROOT, "ls-files"], { encoding: "utf8", maxBuffer: 1 << 28 }).split("\n").filter(Boolean);
363
364
  const pub = publishedOf(all, ROOT);
@@ -61,6 +61,7 @@ import { execFileSync } from "node:child_process";
61
61
  import { publishedOf, wrapsInto } from "../shared/reference-guard-classes.mjs";
62
62
  import { join, dirname } from "node:path";
63
63
  import { fileURLToPath } from "node:url";
64
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
64
65
 
65
66
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
66
67
  const APPLY = process.argv.includes("--apply");
@@ -239,7 +240,7 @@ export function surveyOf(files, read) {
239
240
  return { strippedTotal, remainingTotal, stripped, handoff, unreadable };
240
241
  }
241
242
 
242
- if (import.meta.url === `file://${process.argv[1]}`) {
243
+ if (isEntrypoint(import.meta.url)) {
243
244
  // THE PUBLISHED POPULATION, NOT THE INDEX. The counts this prints are read as a statement about the
244
245
  // public tree, and under an overlay `git ls-files` would have made them a statement about the
245
246
  // withheld corpus instead. Same helper as the residue floor and the backlog minter.
@@ -43,6 +43,7 @@ import { spawnSync } from "node:child_process";
43
43
  import { readFileSync, readdirSync, existsSync } from "node:fs";
44
44
  import { join, dirname } from "node:path";
45
45
  import { fileURLToPath } from "node:url";
46
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
46
47
 
47
48
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
48
49
 
@@ -316,4 +317,4 @@ function main() {
316
317
  if (bad.length) process.exit(1);
317
318
  }
318
319
 
319
- if (import.meta.url === `file://${process.argv[1]}`) main();
320
+ if (isEntrypoint(import.meta.url)) main();
@@ -399,8 +399,11 @@ const isContained = (value) => {
399
399
  // The acceptance asks the refusal to name the value's ROOT, because that is the part that says "this is
400
400
  // somebody's live estate" at a glance, where a long path does not.
401
401
  const rootOf = (value) => {
402
- const parts = resolve(value).split(sep).filter(Boolean);
403
- return parts.length ? sep + parts[0] : sep;
402
+ // From the path's own root, so a Windows path names its drive and first folder (D:\a), not "\D:".
403
+ const p = resolve(value);
404
+ const { root } = parsePath(p);
405
+ const first = p.slice(root.length).split(sep).find(Boolean);
406
+ return first ? root + first : root;
404
407
  };
405
408
 
406
409
  // A guard with no way through gets deleted the first time somebody genuinely needs it, and then nothing
@@ -436,7 +439,7 @@ if (escaped.length) {
436
439
  // ── WHAT THE NEXT TWENTY MINUTES OF OUTPUT IS, SAID BEFORE IT STARTS ───────────────────────
437
440
  //
438
441
  // The suites print thousands of lines shaped like
439
- // [register-unit:primary-sweep] attempt 1/1 (engine=anthropic-agent agent=clawdi model=opus … timeout=1500s)
442
+ // [register-unit:primary-sweep] attempt 1/1 (engine=anthropic-agent agent=mailagent model=opus … timeout=1500s)
440
443
  // and to somebody who cloned this repo ten minutes ago that is their subscription being spent, at
441
444
  // speed, for reasons nobody explained. It is the mock engine's dispatch record. Nothing here can tell
442
445
  // them that except a line that says it, and it has to come FIRST — an explanation after the scroll is
@@ -791,6 +794,10 @@ child = spawn(argv[0], argv.slice(1), {
791
794
  env: {
792
795
  ...process.env,
793
796
  TMPDIR: root,
797
+ // Windows' os.tmpdir() reads TEMP and TMP, not TMPDIR, so without these a Windows run's fixtures
798
+ // landed in the machine's own temp folder.
799
+ TEMP: root,
800
+ TMP: root,
794
801
  // AND THE REAL BASE, so a NESTED run does not root itself inside this one. The line above hands the
795
802
  // child a TMPDIR pointing at this run's own root; `os.tmpdir()` honours it, so a child that starts
796
803
  // its own runner would put its root INSIDE the parent's and the parent's cleanup — or its sweep of
@@ -23,6 +23,7 @@ import { join, dirname } from "node:path";
23
23
  import { fileURLToPath } from "node:url";
24
24
  import { isEntrypoint } from "../shared/is-entrypoint.mjs"; // — realpath both sides, or a symlinked invocation exits 0 silently
25
25
  import { nonEmpty } from "../shared/vacuous-pass.mjs"; // — an empty row set would confirm compliance over nothing
26
+ import { npmInvocation } from "../shared/npm-cli.mjs";
26
27
 
27
28
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
28
29
  export const OUTPUT = join(ROOT, "THIRD-PARTY-NOTICES.md");
@@ -80,7 +81,8 @@ export function npmTree(root = ROOT, run = execFileSync) {
80
81
  const opts = { cwd: root, encoding: "utf8", maxBuffer: 128 * 1024 * 1024 };
81
82
  let out;
82
83
  try {
83
- out = run("npm", ["ls", "--omit=dev", "--all", "--json"], opts);
84
+ const npm = npmInvocation(["ls", "--omit=dev", "--all", "--json"]); // npm.cmd on Windows needs a shell; npm-cli.js does not
85
+ out = run(npm.command, npm.args, opts);
84
86
  } catch (e) {
85
87
  out = e?.stdout;
86
88
  if (typeof out !== "string" || !out.trim()) throw e;
@@ -102,7 +102,7 @@ function isPlumbing(node, child) {
102
102
  // This was the whole of the unresolved bucket — three sites, all one shape:
103
103
  //
104
104
  // const wrote = files.length ? files.some(…) : null; gateway.mjs (by name)
105
- // const inScope = scope.size ? tokens.some(…) : (…); reasoning-tripwires.mjs:82
105
+ // const inScope = scope.size ? tokens.some(…) : (…); findRecallFloorViolations() in reasoning-tripwires.mjs
106
106
  // const reached = b.layer === "national" ? (…) : regions.some(…); resolveRegions() in register-plan.mjs
107
107
  //
108
108
  // The climb stopped at the ternary and reported "unresolved", which reads as a limit of the pattern
@@ -42,6 +42,7 @@
42
42
  import { execFileSync } from "node:child_process";
43
43
  import { readFileSync } from "node:fs";
44
44
  import { CLASSES, fileOffences, isExempt } from "../shared/writing-standard-classes.mjs";
45
+ import { isEntrypoint } from "../shared/is-entrypoint.mjs";
45
46
 
46
47
  const baseArg = () => {
47
48
  const i = process.argv.indexOf("--base");
@@ -119,4 +120,4 @@ function main() {
119
120
  process.exit(1);
120
121
  }
121
122
 
122
- if (import.meta.url === `file://${process.argv[1]}`) main();
123
+ if (isEntrypoint(import.meta.url)) main();
@@ -0,0 +1,59 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only
2
+ // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
+ //
4
+ // THE FONTS TRAVEL WITH THE PAGE. A report used to link its typefaces from two font services, so opening
5
+ // one told those services the reader's address and that a report had been opened, from the portal, from
6
+ // disk or from an email alike. Every page this product writes now carries its fonts inside itself, as
7
+ // data, and fetches nothing to draw its text.
8
+ //
9
+ // ONE PLACE. The files are shared/fonts/*.woff2 (their provenance is in shared/fonts/README.md), and every
10
+ // surface that sets text takes its @font-face rules from here: the report renderers, the pool index pages,
11
+ // the portal's own sign-in pages and the staff profile page. The portal's browser bundle is the one
12
+ // exception: it imports the same files through its own stylesheet, because a bundle serves files rather
13
+ // than inlining them.
14
+ //
15
+ // EACH LICENCE RIDES BESIDE ITS FONT. Both are under the SIL Open Font License 1.1, which asks that every
16
+ // copy of the font carry its copyright notice and the licence. The font files carry both in their name
17
+ // records, and the full licence text goes into the page as a stylesheet comment right before the rules
18
+ // that embed the font, so a copy saved out of a report still has it.
19
+ //
20
+ // NOT FOR EMAIL. A mail client strips web fonts, and the embedded face would add tens of kilobytes to
21
+ // every message. Email keeps its font stack by name only.
22
+
23
+ import { readFileSync } from "node:fs";
24
+
25
+ const at = (name) => new URL(`./fonts/${name}`, import.meta.url);
26
+
27
+ /** The text face, as every stylesheet names it. */
28
+ export const TEXT_FACE = "Plus Jakarta Sans";
29
+ /** The code face, as every stylesheet names it. */
30
+ export const CODE_FACE = "Fira Code";
31
+
32
+ // The code points each file holds, from shared/fonts/README.md. Stated so a browser never asks a face for
33
+ // a character it does not have: Japanese, Greek and the rest go straight to the reader's system fonts.
34
+ export const FONT_UNICODE_RANGE = "U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,"
35
+ + "U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2190-2193,U+2212,U+2215,U+FEFF,U+FFFD,U+0100-02BA,U+02BD-02C5,"
36
+ + "U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,"
37
+ + "U+2113,U+2C60-2C7F,U+A720-A7FF";
38
+
39
+ export const FONT_FILES = [
40
+ { family: TEXT_FACE, file: "plus-jakarta-sans.woff2", licence: "OFL-plus-jakarta-sans.txt", weight: "200 800" },
41
+ { family: CODE_FACE, file: "fira-code.woff2", licence: "OFL-fira-code.txt", weight: "300 700" },
42
+ ];
43
+
44
+ function faceCss({ family, file, licence, weight }) {
45
+ const licenceText = readFileSync(at(licence), "utf8").trim();
46
+ // A licence containing the comment terminator would end the comment early and spill into the stylesheet.
47
+ if (licenceText.includes("*/")) throw new Error(`${licence} contains "*/" and cannot be carried in a stylesheet comment`);
48
+ const data = readFileSync(at(file)).toString("base64");
49
+ return `/*\n${family}\n\n${licenceText}\n*/\n`
50
+ + `@font-face{font-family:'${family}';font-style:normal;font-weight:${weight};font-display:swap;`
51
+ + `src:url(data:font/woff2;base64,${data}) format('woff2');unicode-range:${FONT_UNICODE_RANGE}}\n`;
52
+ }
53
+
54
+ const [TEXT, CODE] = FONT_FILES.map(faceCss);
55
+
56
+ /** The text face alone, for pages that set no code: the pool index, the sign-in pages, the profile page. */
57
+ export const TEXT_FONT_STYLE = `<style>${TEXT}</style>`;
58
+ /** Both faces, for a report, which sets identifiers and dates in the code face. */
59
+ export const REPORT_FONT_STYLE = `<style>${TEXT}${CODE}</style>`;
package/shared/brand.mjs CHANGED
@@ -7,7 +7,7 @@
7
7
  // • the Run status page (writer retired with the quality subsystem in)
8
8
  //
9
9
  // Built on the Cordillera brand package (v3, legacy — the values below are the record): limestone-cream + crimson,
10
- // Satoshi. Crimson is the ONLY strong accent; warm health/risk tones replace traffic-light colours.
10
+ // Plus Jakarta Sans. Crimson is the ONLY strong accent; warm health/risk tones replace traffic-light colours.
11
11
  // Hex values are canonical here; CSS is case-insensitive, so casing in any rendered :root is irrelevant.
12
12
  //
13
13
  // Two ready-made :root blocks are exported because the two surface families use different token names:
@@ -253,8 +253,8 @@ export const PALETTE_DARK = {
253
253
  codeBg: 'rgba(236,229,216,.08)',// parchment at 8%
254
254
  };
255
255
 
256
- // Satoshi via Fontshare. Used by index + staff pages (the report head loads its own weights incl. 900 inline).
257
- export const FONT_LINK = `<link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700&display=swap" rel="stylesheet">`;
256
+ // The typefaces themselves are not linked from anywhere: brand-fonts.mjs embeds them in each page that
257
+ // sets text, and this module only names them, so it stays free of file reads for the tools that import it.
258
258
 
259
259
  // The canonical Cordillera ridge mark — the exact path from the v3 brand package (legacy; this constant IS the record now),
260
260
  // — THE RIDGE IS GONE FROM EVERY SURFACE THIS PRODUCT RENDERS, and the constants went with it.
@@ -434,7 +434,7 @@ export const REPORT_ROOT =
434
434
  // The mark-itself disclosure toggles. A DEDICATED token: --crimson has no dark variant, and
435
435
  // reusing it left the toggles barely legible on the dark ground.
436
436
  `--ma-toggle:#860F09;` +
437
- `--font:'Satoshi','Helvetica Neue',system-ui,sans-serif;` +
437
+ `--font:'Plus Jakarta Sans','Helvetica Neue',system-ui,sans-serif;` +
438
438
  `--mono:'Fira Code',ui-monospace,monospace;` +
439
439
  `--shadow:0 1px 2px rgba(37,9,2,.05), 0 10px 30px rgba(37,9,2,.07);` +
440
440
  `}`;
@@ -575,7 +575,7 @@ export const DOOR_ROOT =
575
575
  `--code-bg:${P.codeBg};` +
576
576
  // The doors' own font stack, not brand's: they must render with no bundle and no webfont, so the
577
577
  // fallbacks are the platform's rather than Helvetica Neue / Fira Code.
578
- `--font:"Satoshi",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;` +
578
+ `--font:"Plus Jakarta Sans",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;` +
579
579
  `--mono:ui-monospace,SFMono-Regular,Menlo,monospace}`;
580
580
 
581
581
  // Same token names, dark values. Nothing here is a scoped rule override: every value the dark scheme
@@ -718,7 +718,7 @@ export const CHROME_CSS = `
718
718
  thing. The three body elements that carried the class drop it too, and --wm-alpha goes with them:
719
719
  a token nothing reads is a knob somebody re-wires.
720
720
  NOTE FOR ANY FUTURE DECORATION ON body: the rule here pinned right:0 and never a negative inset,
721
- because right:-2% put 26px of horizontal scrollbar on a 1280px report (#485) and pointer-events:none
721
+ because right:-2% put 26px of horizontal scrollbar on a 1280px report and pointer-events:none
722
722
  does not exempt a box from scrollable overflow. That is a property of the position, not of this
723
723
  rule, and scripts/report-overflow-check.mjs still measures it. */
724
724
  /* Floating settings stack (theme + privacy) — pinned bottom-right, OUT of the nav bar. Icon-only: the toggle keeps its .tt-lbl/.anon-lbl text for screen readers but clips it visually.
@@ -75,9 +75,10 @@ export const ROOT_PREFIX = "ctb-";
75
75
  * Returned rather than thrown so a caller can decide: a check that has already produced a result
76
76
  * should say this and exit, not lose the result to an exception.
77
77
  */
78
- export function rootRefusal(dir) {
78
+ export function rootRefusal(dir, { platform = process.platform } = {}) {
79
79
  if (typeof dir !== "string" || dir === "") return "a browser temp root must be a non-empty path";
80
- if (dir.length <= MAX_ROOT_LENGTH) return null;
80
+ // On Windows the browser's singleton is a named pipe, not a socket at a file path, so no length binds.
81
+ if (platform === "win32" || dir.length <= MAX_ROOT_LENGTH) return null;
81
82
  return (
82
83
  `browser temp root is ${dir.length} characters and the limit is ${MAX_ROOT_LENGTH}: ${dir}\n` +
83
84
  ` A browser writes its process-singleton socket at <root>${LOCK_SUFFIX}, and a unix socket ` +
@@ -6,7 +6,7 @@
6
6
  // is explicit that an edit for brevity changes what a clearance concludes. So an override that has
7
7
  // silently gone stale is a run answering from old doctrine with nothing to say so.
8
8
  //
9
- // WE RAN THIS EXPERIMENT ON OURSELVES. `driver/driver.config.mjs:170` records the repo split forking
9
+ // WE RAN THIS EXPERIMENT ON OURSELVES. the SKILL RESOLUTION note above `skillsBaseDir` in `driver/driver.config.mjs` records the repo split forking
10
10
  // the whole tree: **30 of 37 shared files had silently drifted apart, in BOTH directions**. The
11
11
  // overlay fixed the mechanism for us and fixes nothing for a self-hoster, whose copies win and whose
12
12
  // upstream moves underneath them with nothing comparing the two.
@@ -40,15 +40,23 @@
40
40
  //
41
41
  // ── WHAT THIS DELIBERATELY DOES NOT DO ────────────────────────────────────────────────────────────
42
42
  //
43
- // IT DOES NOT SET A MODE, and this change alters no behaviour on disk. Pinning 0o750 here in the same
44
- // commit would be a semantic change riding inside a seam, and the seam is the part that has to be
45
- // reviewable by reading nineteen call sites and agreeing they still mean what they meant. The mode is
46
- // That business, and when it is taken it is one line in this file.
43
+ // ── THE MODE, TAKEN HERE ONCE (2026-09-23) ────────────────────────────────────────────────────────
47
44
  //
48
- // IT WOULD NOT RESTRAIN THE SEAT IF IT DID. The agent runs as the same account that owns these
49
- // directories, so owner bits apply whatever the group bits say. A tighter mode removes GROUP write,
50
- // which is worth having on a box with a shared pool group, and nothing more. Said here because a module
51
- // named for a boundary invites the stronger reading.
45
+ // A new run folder is created 0750: its owner, and the group the deployment reads reports through, and
46
+ // no other account. It used to take whatever the umask gave, 775 on a real run, so on a machine whose
47
+ // home or data folder other accounts can enter, any of them could read client matter. The folder that
48
+ // holds a run is created by the call below (measured by tracing a whole clearance: `ensureDriverDir` is
49
+ // the first write into a new run folder, and the archive is a rename that keeps the mode), so this one
50
+ // line closes the live run and its archived copy. The published copy takes the same constant.
51
+ //
52
+ // A MODE GIVEN TO mkdir, NEVER A chmod. The kernel sets a new folder's set-GID bit from its parent
53
+ // whatever mode is asked for, which is what the set-GID pool root relies on; a chmod by an account
54
+ // outside that group strips the bit silently and every report then answers 403. So nothing here ever
55
+ // changes an existing folder: a folder that already exists keeps the mode it has.
56
+ //
57
+ // IT DOES NOT RESTRAIN THE SEAT. The agent runs as the same account that owns these directories, so owner
58
+ // bits apply whatever the group bits say. Said here because a module named for a boundary invites the
59
+ // stronger reading.
52
60
  //
53
61
  // IT IS NOT `driverDirs`. `scripts/seat-retry-report.mjs` exports `driverDirs(root)`, which WALKS a tree
54
62
  // to find every `_driver` beneath it. Discovery, not construction — a different concern that happens to
@@ -60,6 +68,34 @@ import { join } from "node:path";
60
68
  /** The directory's name. The one place the string lives, so renaming it is an edit rather than a sweep. */
61
69
  export const DRIVER_DIR = "_driver";
62
70
 
71
+ /** The mode a new run folder is created with: owner and group, no other account. See the header. */
72
+ export const RUN_DIR_MODE = 0o750;
73
+
74
+ /**
75
+ * A name inside `_driver/` as it is written on `platform`.
76
+ *
77
+ * ON WINDOWS A COLON CANNOT BE IN A FILE NAME, and a stage's label carries one (`register-unit:primary-
78
+ * sweep`, `common-law-half:a`, `report-card:1`), which names its attempt log, its dispatch record and its
79
+ * input stamp. NTFS does not refuse the name: it reads `register-unit:primary-sweep.jsonl` as a hidden
80
+ * stream of a file called `register-unit`, so every register unit's log lands in that one file, reading it
81
+ * back by the same name appears to work, and the copy into an experiment's sandbox fails with EINVAL.
82
+ * Measured on a Windows runner, 2026-09-23. There the colon is written `%3A`, which no label contains, so
83
+ * the name still reads back to its label. Everywhere else the name is left exactly as it was.
84
+ */
85
+ export function driverFileName(name, platform = process.platform) {
86
+ return platform === "win32" ? String(name).replaceAll(":", "%3A") : name;
87
+ }
88
+
89
+ /**
90
+ * The stage label a `_driver/` file was written for: `register-unit%3Aprimary-sweep.jsonl` →
91
+ * `register-unit:primary-sweep`. Every platform decodes, because no label holds `%`, so a record written
92
+ * on Windows reads back under its label wherever it is read. `ext` is the suffix to take off.
93
+ */
94
+ export function labelOfDriverFile(name, ext = ".jsonl") {
95
+ const base = String(name ?? "");
96
+ return (base.endsWith(ext) ? base.slice(0, -ext.length) : base).replaceAll("%3A", ":");
97
+ }
98
+
63
99
  /**
64
100
  * The path to a run's `_driver/`, or to something inside it.
65
101
  *
@@ -67,7 +103,7 @@ export const DRIVER_DIR = "_driver";
67
103
  * @param {...string} parts optional path segments beneath it
68
104
  */
69
105
  export function driverDir(base, ...parts) {
70
- return join(base, DRIVER_DIR, ...parts);
106
+ return join(base, DRIVER_DIR, ...parts.map((part) => driverFileName(part)));
71
107
  }
72
108
 
73
109
  /**
@@ -79,7 +115,7 @@ export function driverDir(base, ...parts) {
79
115
  * reaching this directory by one.
80
116
  */
81
117
  export function driverRel(...parts) {
82
- return join(DRIVER_DIR, ...parts);
118
+ return join(DRIVER_DIR, ...parts.map((part) => driverFileName(part)));
83
119
  }
84
120
 
85
121
  /**
@@ -91,6 +127,6 @@ export function driverRel(...parts) {
91
127
  */
92
128
  export function ensureDriverDir(base, ...parts) {
93
129
  const dir = driverDir(base, ...parts);
94
- mkdirSync(dir, { recursive: true });
130
+ mkdirSync(dir, { recursive: true, mode: RUN_DIR_MODE });
95
131
  return dir;
96
132
  }
@@ -0,0 +1,93 @@
1
+ Copyright 2014-2020 The Fira Code Project Authors (https://github.com/tonsky/FiraCode)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.