clearotron 0.3.2-beta.7 → 0.3.2-beta.9

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 (272) hide show
  1. package/.env.example +58 -26
  2. package/CONTRIBUTING.md +8 -8
  3. package/INSTALL.md +148 -81
  4. package/README.md +3 -3
  5. package/SECURITY.md +3 -3
  6. package/bin/brandowner.mjs +3 -3
  7. package/bin/framework-preflight.mjs +1 -1
  8. package/bin/onboard.mjs +637 -216
  9. package/bin/start.mjs +151 -27
  10. package/bin/update.mjs +58 -11
  11. package/build-info.json +2 -2
  12. package/docs/DELIVERY.md +2 -1
  13. package/docs/INTAKE.md +1 -1
  14. package/docs/ONBOARDING.md +1 -1
  15. package/docs/architecture/03-run-lifecycle.md +6 -6
  16. package/docs/architecture/04-configuration-reference.md +32 -14
  17. package/docs/architecture/05-config-governance.md +23 -8
  18. package/docs/architecture/05-customer-profiles.md +2 -2
  19. package/docs/architecture/06-operations-runbook.md +3 -3
  20. package/docs/architecture/08-development-guide.md +6 -6
  21. package/docs/configuration.md +5 -5
  22. package/docs/decisions/0003-credential-model.md +1 -1
  23. package/docs/writing-standard.md +4 -0
  24. package/driver/CHANGELOG.md +124 -0
  25. package/driver/README.md +3 -3
  26. package/driver/band-size.mjs +59 -0
  27. package/driver/binding-layers.mjs +1 -1
  28. package/driver/citation-census.json +3 -3
  29. package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
  30. package/driver/common-law-receipts.mjs +2 -2
  31. package/driver/company-bundle.mjs +3 -3
  32. package/driver/compose-read.mjs +8 -14
  33. package/driver/config-inventory.mjs +112 -9
  34. package/driver/consumption-ledger.mjs +2 -2
  35. package/driver/contract-arm2-baseline.json +2 -5
  36. package/driver/contract-dictation-registry.mjs +19 -19
  37. package/driver/contract-e3-backlog.mjs +43 -43
  38. package/driver/contract-e3-baseline.json +14 -14
  39. package/driver/contract-vocabulary.mjs +68 -27
  40. package/driver/deliver-trigger.sh +16 -16
  41. package/driver/demo-container.mjs +3 -3
  42. package/driver/dev-portal.mjs +3 -3
  43. package/driver/disposition-call.mjs +1 -1
  44. package/driver/door-gates.mjs +41 -7
  45. package/driver/doubt-ledger.mjs +2 -2
  46. package/driver/drainer-identity.mjs +34 -8
  47. package/driver/driver.config.mjs +367 -104
  48. package/driver/engine/CONTRACT.md +10 -3
  49. package/driver/engine/README.md +2 -2
  50. package/driver/engine/anthropic-agent.mjs +77 -21
  51. package/driver/engine/auth.mjs +129 -10
  52. package/driver/engine/jx-turn.mjs +7 -6
  53. package/driver/engine/mcp/README.md +1 -1
  54. package/driver/engine/mcp/dispositions-server.mjs +3 -3
  55. package/driver/engine/mcp/gather-config.mjs +9 -9
  56. package/driver/engine/mcp/perplexity-server.mjs +2 -2
  57. package/driver/engine/mcp/recording-server.mjs +18 -5
  58. package/driver/engine/openai-agent.mjs +4 -2
  59. package/driver/engine/probe.mjs +110 -23
  60. package/driver/enqueue-schema.mjs +6 -2
  61. package/driver/findings-model.mjs +6 -3
  62. package/driver/flag-snapshot.mjs +34 -8
  63. package/driver/form-neighbourhood.mjs +54 -7
  64. package/driver/framework.mjs +4 -4
  65. package/driver/gateway.mjs +36 -24
  66. package/driver/jx-lanes.mjs +23 -4
  67. package/driver/jx-units.mjs +7 -4
  68. package/driver/jx.mjs +34 -4
  69. package/driver/knockout-review-record.mjs +56 -4
  70. package/driver/known-conflicts.mjs +1 -1
  71. package/driver/matter-frame-record.mjs +90 -1
  72. package/driver/named-band.mjs +1 -1
  73. package/driver/ordinary-words.mjs +51 -0
  74. package/driver/outbox-backoff.mjs +31 -16
  75. package/driver/package.json +1 -1
  76. package/driver/partial-payload-baseline.json +2 -2
  77. package/driver/phase0.mjs +3 -3
  78. package/driver/pipeline-knockout.mjs +5 -5
  79. package/driver/pipeline.mjs +396 -81
  80. package/driver/placement-form.mjs +77 -1
  81. package/driver/placement-model.mjs +1 -1
  82. package/driver/portal-config-view.mjs +30 -1
  83. package/driver/portal-report.mjs +107 -6
  84. package/driver/portal-service.mjs +80 -14
  85. package/driver/portal-upstream.mjs +1 -1
  86. package/driver/predelivery-lint.mjs +12 -2
  87. package/driver/preserve-merge.mjs +3 -3
  88. package/driver/product-rows.mjs +2 -2
  89. package/driver/products.mjs +1 -1
  90. package/driver/profiles/README.md +3 -3
  91. package/driver/profiles/demo-brand-owner.json +2 -2
  92. package/driver/profiles.mjs +55 -17
  93. package/driver/progress.mjs +18 -8
  94. package/driver/provider-usage.mjs +8 -8
  95. package/driver/publish/index.mjs +154 -8
  96. package/driver/publish/knockout.mjs +39 -5
  97. package/driver/publish/pool-admin.mjs +1 -1
  98. package/driver/publish/publish-inputs.mjs +18 -2
  99. package/driver/publish/render-knockout.mjs +184 -31
  100. package/driver/publish/render.mjs +323 -93
  101. package/driver/publish/report-data.mjs +4 -1
  102. package/driver/publish/report-topbar.mjs +58 -0
  103. package/driver/publish/search-depth.mjs +133 -4
  104. package/driver/publish/templates/report.css +78 -4
  105. package/driver/publish/xlsx.mjs +20 -1
  106. package/driver/queue-order.mjs +2 -2
  107. package/driver/recording-agreement.mjs +1 -1
  108. package/driver/reference-score.mjs +1 -1
  109. package/driver/register-availability.mjs +2 -2
  110. package/driver/register-count.mjs +50 -5
  111. package/driver/register-coverage.mjs +161 -1
  112. package/driver/register-digest-record.mjs +236 -11
  113. package/driver/register-grant-vocabulary.mjs +1 -1
  114. package/driver/register-plan.mjs +189 -2
  115. package/driver/registry-fidelity.mjs +3 -3
  116. package/driver/repair-composers.mjs +1 -1
  117. package/driver/repair-contract.mjs +1 -1
  118. package/driver/replay-archive.mjs +6 -6
  119. package/driver/report-overview-record.mjs +2 -2
  120. package/driver/result-noun-fields.mjs +2 -2
  121. package/driver/run-economics.mjs +41 -10
  122. package/driver/run-requirements.mjs +173 -9
  123. package/driver/runner.mjs +5 -5
  124. package/driver/scope-facts.mjs +20 -5
  125. package/driver/scope-ledger.mjs +5 -5
  126. package/driver/search-policy.mjs +22 -12
  127. package/driver/skills/README.md +15 -15
  128. package/driver/skills/blind-frame/SKILL.md +2 -2
  129. package/driver/skills/case-law-citation/SKILL.md +4 -4
  130. package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
  131. package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
  132. package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
  133. package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
  134. package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
  135. package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
  136. package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
  137. package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
  138. package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
  139. package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
  140. package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
  141. package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
  142. package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
  143. package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
  144. package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
  145. package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
  146. package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
  147. package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
  148. package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
  149. package/driver/skills/frame-diff/SKILL.md +1 -1
  150. package/driver/skills/knockout-assess/SKILL.md +10 -7
  151. package/driver/skills/matter-frame/SKILL.md +3 -3
  152. package/driver/skills/narrative-refutation/SKILL.md +9 -9
  153. package/driver/skills/placement-inquiry/SKILL.md +5 -5
  154. package/driver/stage-context.mjs +1 -1
  155. package/driver/stages-knockout.mjs +4 -4
  156. package/driver/stages.mjs +65 -61
  157. package/driver/status-snapshot.mjs +2 -2
  158. package/driver/suite-census.json +340 -136
  159. package/driver/surface-exit-verdict.mjs +58 -0
  160. package/driver/systemd/README.md +9 -6
  161. package/driver/systemd/clearotron-worker.service +1 -1
  162. package/driver/terminal-clamp.mjs +109 -1
  163. package/driver/tokens.mjs +169 -3
  164. package/driver/unit-environment.mjs +42 -15
  165. package/driver/unit-inventory.mjs +19 -2
  166. package/driver/usage-ledger.mjs +1 -1
  167. package/driver/variant-manifest-model.mjs +4 -4
  168. package/driver/verify-knockout.mjs +27 -0
  169. package/driver/verify.mjs +94 -6
  170. package/driver/whatif-queue.mjs +1 -1
  171. package/driver/wordlists/en.txt +63906 -0
  172. package/mcp-server/CHANGELOG.md +8 -0
  173. package/mcp-server/README.md +1 -1
  174. package/mcp-server/lib/README.md +1 -1
  175. package/mcp-server/lib/options.mjs +8 -7
  176. package/mcp-server/lib/plan.mjs +18 -2
  177. package/mcp-server/lib/runs.mjs +1 -1
  178. package/mcp-server/lib/usage.mjs +3 -3
  179. package/mcp-server/lib/whatif.mjs +1 -1
  180. package/mcp-server/package.json +1 -1
  181. package/mcp-server/server.mjs +18 -1
  182. package/package.json +12 -11
  183. package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
  184. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
  185. package/portal-ui/dist/index.html +2 -2
  186. package/portal-ui/package.json +1 -1
  187. package/providers/README.md +1 -1
  188. package/providers/_shared/enumerate.mjs +6 -6
  189. package/providers/_shared/execute-plan.mjs +3 -3
  190. package/providers/_shared/ledger.mjs +119 -5
  191. package/providers/_shared/provider-text.mjs +2 -2
  192. package/providers/_shared/screen.mjs +2 -2
  193. package/providers/_shared/script-form.mjs +3 -3
  194. package/providers/_shared/territory-codes.mjs +23 -3
  195. package/providers/clarivate/README.md +1 -1
  196. package/providers/clarivate/src/capabilities.js +12 -12
  197. package/providers/clarivate/src/core.js +37 -43
  198. package/providers/corsearch/README.md +1 -1
  199. package/providers/corsearch/src/capabilities.js +5 -5
  200. package/providers/corsearch/src/core.js +3 -3
  201. package/providers/jx/README.md +2 -1
  202. package/providers/jx/src/turn-envelope.mjs +8 -3
  203. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  204. package/providers/oauth-mcp-bridge/package.json +1 -1
  205. package/providers/perplexity/src/core.js +1 -1
  206. package/providers/signa/README.md +1 -1
  207. package/providers/signa/src/capabilities.js +42 -49
  208. package/providers/signa/src/core.js +106 -29
  209. package/providers/uspto-local/README.md +1 -1
  210. package/providers/uspto-local/src/sync.js +1 -1
  211. package/scripts/README.md +1 -0
  212. package/scripts/ask-ai-render-check.mjs +127 -1
  213. package/scripts/authority-boundary-probe.mjs +8 -6
  214. package/scripts/backfill-started-at.mjs +2 -2
  215. package/scripts/census-merge-driver.mjs +33 -2
  216. package/scripts/citation-anchor-report.mjs +181 -0
  217. package/scripts/dead-names.mjs +1 -1
  218. package/scripts/deprecate-below.mjs +66 -8
  219. package/scripts/drain-preflight.mjs +1 -1
  220. package/scripts/e2e.mjs +174 -0
  221. package/scripts/env-audit.mjs +39 -6
  222. package/scripts/env-classify.mjs +20 -2
  223. package/scripts/freeze-example-run.mjs +61 -18
  224. package/scripts/generated-files-are-current.mjs +69 -4
  225. package/scripts/live-surface-check.mjs +124 -41
  226. package/scripts/markdown-link-check.mjs +1 -1
  227. package/scripts/merge-shape-check.mjs +242 -0
  228. package/scripts/mint-names-in-force.mjs +5 -5
  229. package/scripts/mint-offered-territories.mjs +72 -0
  230. package/scripts/mint-public-residue.mjs +2 -2
  231. package/scripts/mint-reference-strip-backlog.mjs +2 -2
  232. package/scripts/mint-suite-census.mjs +75 -2
  233. package/scripts/mint-writing-standard-backlog.mjs +2 -2
  234. package/scripts/purge-runs.mjs +7 -7
  235. package/scripts/reconcile-runs.mjs +2 -2
  236. package/scripts/release-approve-parked.mjs +20 -2
  237. package/scripts/release-await-cut.mjs +120 -1
  238. package/scripts/release-note-required.mjs +76 -8
  239. package/scripts/report-header-render-check.mjs +164 -0
  240. package/scripts/settings-render-check.mjs +75 -2
  241. package/scripts/test-full.mjs +96 -3
  242. package/scripts/test-run.mjs +10 -0
  243. package/shared/brand.mjs +27 -0
  244. package/shared/connect-clients.mjs +39 -11
  245. package/shared/deployment-box.mjs +7 -2
  246. package/shared/driver-dir.mjs +1 -1
  247. package/shared/env-aliases.mjs +1 -1
  248. package/shared/identifier-scan.mjs +65 -9
  249. package/shared/identifier-sentinels.mjs +22 -0
  250. package/shared/names-in-force.mjs +4 -2
  251. package/shared/offered-territories.json +738 -0
  252. package/shared/pre-rename-spellings.mjs +53 -0
  253. package/shared/reference-guard-classes.mjs +40 -2
  254. package/shared/stdio-connect.mjs +39 -4
  255. package/shared/tree-commit.mjs +48 -0
  256. /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
  257. /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
  258. /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
  259. /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
  260. /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
  261. /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
  262. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
  263. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
  264. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
  265. /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
  266. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
  267. /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
  268. /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
  269. /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
  270. /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
  271. /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
  272. /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
package/scripts/e2e.mjs CHANGED
@@ -68,6 +68,10 @@ import { doorGates, resolveForDoor } from "../driver/door-gates.mjs";
68
68
  // — the plan-row term screen, shared with the driver rather than restated here, so the harness
69
69
  // can never drift from what the plan freeze and the executor enforce. PURE (no node imports).
70
70
  import { entryTermIssues } from "../providers/_shared/term-shape.mjs";
71
+ // — the mechanical tier vocabulary, imported from the module that classifies into it rather than
72
+ // restated here: a floor naming a tier that does not exist must fail loudly rather than compare against
73
+ // a missing key, and the list must not drift if a tier is ever added. PURE (no node imports) too.
74
+ import { SHAPE_TIERS } from "../driver/band-shape.mjs";
71
75
  // The owner's turnaround benchmarks. The BAND owns the number and a scenario file cannot carry
72
76
  // one — see the lint rule below. Another pure leaf with zero imports of its own, for the same reason
73
77
  // queue-markers.mjs is: nothing in this file may reach driver.config.mjs.
@@ -1256,6 +1260,70 @@ export function pathsAnOpDoesNotRead(scenarios) {
1256
1260
  return out;
1257
1261
  }
1258
1262
 
1263
+ /**
1264
+ * The office a band-shape floor row belongs to, from the row's own fields and never from a wider table.
1265
+ *
1266
+ * TWO SOURCES, IN THE ORDER band-shape.mjs POPULATES THEM. `registry` is the projection's own field and
1267
+ * it is derived, so it can read `unknown`; the record id carries the office as its second path segment.
1268
+ * Measured on a preserved dense clearance run, 2026-09-18: 289 of 289 rows carried `registry`, and every
1269
+ * one agreed with its id's segment. The fallback exists for the row that does not, and a row that
1270
+ * answers neither way is counted separately by the caller rather than dropped.
1271
+ */
1272
+ export const bandRowOffice = (r) => {
1273
+ const reg = String(r?.registry ?? "").trim().toLowerCase();
1274
+ if (reg && reg !== "unknown") return reg;
1275
+ const m = String(r?.record_id ?? "").match(/^\/[^/]+\/([^/]+)\//);
1276
+ return m ? m[1].toLowerCase() : "";
1277
+ };
1278
+
1279
+ /**
1280
+ * Open a clearance run's band shape for a floor op: the document, the mark check, and the two coverage
1281
+ * qualifications — or ONE sentence saying why no floor under it can be read.
1282
+ *
1283
+ * THE MARK IS CHECKED AGAINST `targets`, NOT AGAINST `targets[0]`. The driver shapes a band for the mark
1284
+ * under search PLUS its manifest variants, and it puts the job's own mark in that list unconditionally;
1285
+ * the first entry is the variant lane's to order. Membership is therefore the property that holds while
1286
+ * the variant lane moves, and it still catches the case this check is for — a shape derived for some
1287
+ * other mark entirely. A mismatch says so in its own words rather than folding into a missed floor,
1288
+ * because "the store's mark moved" and "the register thinned" need opposite answers.
1289
+ */
1290
+ function bandShapeUnder(full, file, field) {
1291
+ const bad = (saw) => ({ bad: saw });
1292
+ if (!existsSync(full)) return bad(`${file} absent — the clearance lane derives this after every register band re-merge, so an absent `
1293
+ + `shape is a derivation that did not happen or a lane that never merged a band. It is not a register that holds nothing.`);
1294
+ const doc = readJson(full);
1295
+ if (!doc) return bad(`${file} present but unparseable`);
1296
+ const want = field ? String(field).trim().toLowerCase() : null;
1297
+ if (!want) return bad("no mark given (path must be <file>:<MARK NAME>)");
1298
+ const targets = Array.isArray(doc.targets) ? doc.targets.filter((t) => String(t ?? "").trim()) : [];
1299
+ if (!targets.length) return bad(`${file} carries no targets — nothing in it says which mark the band was shaped for, so no floor under a mark can be read from it`);
1300
+ if (!targets.some((t) => String(t).trim().toLowerCase() === want)) {
1301
+ const shown = targets.slice(0, 6).map((t) => JSON.stringify(String(t))).join(", ");
1302
+ return bad(`this shape was derived for targets that do not include ${JSON.stringify(field)} — it was shaped for ${shown}`
1303
+ + `${targets.length > 6 ? `, and ${targets.length - 6} more` : ""}. The store's mark moved, or this run is another mark's; either way no floor here is about the mark the scenario named.`);
1304
+ }
1305
+ const spots = Array.isArray(doc.blind_spots) ? doc.blind_spots : [];
1306
+ // The register would not answer: a refused slice, or one it would not state a total for. These make a
1307
+ // shortfall genuinely ambiguous, so they qualify a pass and may be the reason for a miss.
1308
+ const unread = spots.filter((b) => b?.kind === "refused-slice" || b?.kind === "uncountable-slice")
1309
+ .map((b) => `${b.count ?? "?"} ${b.kind}`);
1310
+ // The register DID answer, with a number larger than what was fetched. That is evidence of size, not a
1311
+ // gap in it — see the block at the ops — so it is named with the depth read and excuses nothing.
1312
+ const shallow = spots.filter((b) => b?.kind === "unenumerated-crowd")
1313
+ .map((b) => `${b.count ?? "?"} ${b.kind}${Array.isArray(b.read_depth) && b.read_depth.length ? ` (read ${b.read_depth.join("/")} deep)` : ""}`);
1314
+ const verdict = (ok, passBody, missBody) => {
1315
+ const parts = [ok ? passBody : missBody];
1316
+ if (unread.length) parts.push(ok
1317
+ ? `floor met, but the register refused or would not total ${unread.join(", ")}, so this run covered less than it asked for`
1318
+ : `and the register refused or would not total ${unread.join(", ")}, so the shortfall may be what it would not answer rather than what it holds`);
1319
+ if (shallow.length) parts.push(ok
1320
+ ? `${shallow.join(", ")} — the register stated more than the funnel fetched there`
1321
+ : `and ${shallow.join(", ")} — the register stated more than the funnel fetched there, which is a finding about the read depth and NOT a reason to lower the floor`);
1322
+ return { ok, saw: parts.join(" — ") };
1323
+ };
1324
+ return { doc, unread, shallow, verdict };
1325
+ }
1326
+
1259
1327
  function evalAssertion(a, runDir) {
1260
1328
  const [file, field] = String(a.path ?? "").split(":");
1261
1329
  const full = join(runDir, file || "");
@@ -1637,6 +1705,112 @@ function evalAssertion(a, runDir) {
1637
1705
  saw: failed.length ? `${body}, and ${failed.length} term(s) FAILED to fetch, so the shortfall may be the fetch and not the register: ${failed.join(" · ")}`
1638
1706
  : `${body} — the register returned fewer rows than the hit path needs, so screening, hydration and citation ran on less than this scenario exists to give them` };
1639
1707
  }
1708
+ // ── · THE SAME TWO QUESTIONS, ASKED OF THE CLEARANCE LANE'S OWN ARTIFACT ──────────────────────
1709
+ //
1710
+ // WHY A SECOND PAIR RATHER THAN A SECOND PATH ON THE PAIR ABOVE. `register-counts.json` and
1711
+ // `register-records.json` are written by the KNOCKOUT lane and by nothing else. A clearance scenario
1712
+ // asserting the floors above therefore gets `absent` on both, and the sentence those absences print —
1713
+ // "a run whose register lane wrote nothing is not a run that found nothing" — is true of the file and
1714
+ // false of the run: the clearance lane read the band and recorded it under other names. The record it
1715
+ // keeps is `_driver/band-shape.json`, derived after every named-band re-merge, and it is a different
1716
+ // shape entirely rather than the same shape in another place, so it needs its own two ops.
1717
+ //
1718
+ // THREE POPULATIONS, THREE DIFFERENT NUMBERS, AND CONFLATING THEM IS THE TRAP THIS PAIR SITS IN.
1719
+ // Measured on a preserved dense clearance run, 2026-09-18, all three true of one band at one moment:
1720
+ //
1721
+ // totals.records 1554 every in-scope record the merged band holds
1722
+ // totals.by_tier.identical 245 tiered across the WHOLE band, class filter and all
1723
+ // floors.in_class_identical_or_near 289 identical AND near-identical, live, in-class only
1724
+ //
1725
+ // The band's own delivered depth record counted 2098, because it counts everything READ rather than
1726
+ // what stayed in scope. A floor written against one of these and read against another drifts with
1727
+ // nobody noticing, which is the failure the floors exist to catch happening to the floors themselves.
1728
+ // So every number these ops print names its population in words, and neither op mixes two.
1729
+ //
1730
+ // BOTH KEEP THE TWO RULES THE PAIR ABOVE IS BUILT ON. A size that was never taken is never a small
1731
+ // one: an absent shape, a shape with no `totals`, no `by_tier` or no floors array is reported as a
1732
+ // derivation that did not happen, never as a band that holds nothing. And a slice the register refused
1733
+ // or would not total is reduced coverage, named whichever way the floor goes.
1734
+ //
1735
+ // A CROWD THE FUNNEL COULD NOT ENUMERATE IS NOT THAT, AND MUST NEVER EXCUSE A SHORTFALL. Its
1736
+ // `total_hits` is LARGER than what was fetched, which is the register stating that the band is at
1737
+ // least that big — positive evidence of size, the opposite of a coverage gap. A dense band carries
1738
+ // these by construction (19 of them on the run measured above), so letting them soften a miss would
1739
+ // make every genuine thinning report as "the register may not have answered" for ever. They are named
1740
+ // with the depth actually read, and they change no verdict.
1741
+ if (a.op === "band-count-floor" || a.op === "band-records-floor") {
1742
+ const shape = bandShapeUnder(full, file, field);
1743
+ if (shape.bad) return { ok: false, saw: shape.bad };
1744
+ const value = a.value && typeof a.value === "object" && !Array.isArray(a.value) ? a.value : null;
1745
+
1746
+ if (a.op === "band-count-floor") {
1747
+ if (!value) return { ok: false, saw: "value must be an object of floors, e.g. {\"records\": 1000, \"by_tier\": {\"identical\": 120}}" };
1748
+ const unknown = Object.keys(value).filter((k) => k !== "records" && k !== "by_tier");
1749
+ if (unknown.length) return { ok: false, saw: `unknown floor key(s) ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this op takes `
1750
+ + `"records" (the whole in-scope band) and "by_tier" (one floor per mechanical tier). A key nothing reads is a floor nobody asserts.` };
1751
+ const totals = shape.doc.totals && typeof shape.doc.totals === "object" ? shape.doc.totals : null;
1752
+ if (!totals) return { ok: false, saw: `${file} carries no totals — the band was never sized, which is not a band whose size is zero` };
1753
+
1754
+ const saw = [], short = [], untaken = [];
1755
+ if (Object.hasOwn(value, "records")) {
1756
+ const want = value.records;
1757
+ if (!Number.isFinite(want)) return { ok: false, saw: `"records" must be a number, not ${JSON.stringify(want)}` };
1758
+ if (!Number.isFinite(totals.records)) untaken.push(`records: NOT TAKEN — totals.records is ${JSON.stringify(totals.records ?? null)}`);
1759
+ else {
1760
+ saw.push(`${totals.records} record(s) in the whole in-scope band (floor ${want})`);
1761
+ if (!(totals.records >= want)) short.push(`the in-scope band is ${totals.records} record(s), below the floor of ${want}`);
1762
+ }
1763
+ }
1764
+ if (Object.hasOwn(value, "by_tier")) {
1765
+ const tiers = value.by_tier && typeof value.by_tier === "object" && !Array.isArray(value.by_tier) ? value.by_tier : null;
1766
+ if (!tiers) return { ok: false, saw: "\"by_tier\" must be an object of tier floors, e.g. {\"identical\": 120}" };
1767
+ const byTier = totals.by_tier && typeof totals.by_tier === "object" ? totals.by_tier : null;
1768
+ if (!byTier) return { ok: false, saw: `${file} carries no totals.by_tier — the band was never tiered, which is not a band holding no identical marks` };
1769
+ for (const [tier, want] of Object.entries(tiers)) {
1770
+ if (!SHAPE_TIERS.includes(tier)) { untaken.push(`${tier}: NO SUCH TIER — the mechanical tiers are ${SHAPE_TIERS.join(", ")}`); continue; }
1771
+ if (!Number.isFinite(want)) return { ok: false, saw: `the floor for ${tier} must be a number, not ${JSON.stringify(want)}` };
1772
+ const n = byTier[tier];
1773
+ if (!Number.isFinite(n)) { untaken.push(`${tier}: NOT TAKEN — totals.by_tier.${tier} is ${JSON.stringify(n ?? null)}`); continue; }
1774
+ saw.push(`${n} record(s) tiered ${tier} across the whole band (floor ${want})`);
1775
+ if (!(n >= want)) short.push(`${tier} is ${n}, below the floor of ${want}`);
1776
+ }
1777
+ }
1778
+ if (!saw.length && !short.length && !untaken.length)
1779
+ return { ok: false, saw: "no floor stated — give \"records\", \"by_tier\" or both; an op asserting nothing is an assert that looked at nothing" };
1780
+ if (untaken.length) return { ok: false, saw: `the band was never sized that way, so this scenario proved nothing about the size it exists for — ${untaken.join(" · ")}` };
1781
+ return shape.verdict(short.length === 0, saw.join(" · "),
1782
+ `${short.join(" · ")} — either the register has thinned out under this mark or the query narrowed; re-measure before moving the floor`);
1783
+ }
1784
+
1785
+ // The population behind the count, and the two properties the size-dependent paths need from it:
1786
+ // enough rows to work on, and rows from more than one office.
1787
+ //
1788
+ // BOTH NUMBERS COME OUT OF THE SAME LIST, DELIBERATELY. The shape also carries a `by_registry` table
1789
+ // over the whole band, and taking the office span from there while taking the row count from the
1790
+ // floor list would report a multi-office band whose every floor row was one office — which is the
1791
+ // exact fail-open the knockout records floor names: a total-only floor is met while the property it
1792
+ // exists for lapses with every assert green. One list, both properties, or neither is asserted.
1793
+ const floors = value ?? {};
1794
+ const unknown = Object.keys(floors).filter((k) => k !== "records" && k !== "offices");
1795
+ if (unknown.length) return { ok: false, saw: `unknown floor key(s) ${unknown.map((k) => JSON.stringify(k)).join(", ")} — this op takes `
1796
+ + `"records" (rows on the floor list) and "offices" (how many the list spans).` };
1797
+ const minRecords = Number.isFinite(floors.records) ? floors.records : 1;
1798
+ const minOffices = Number.isFinite(floors.offices) ? floors.offices : 1;
1799
+ const rows = shape.doc.floors?.in_class_identical_or_near;
1800
+ if (!Array.isArray(rows)) return { ok: false, saw: `${file} carries no floors.in_class_identical_or_near array — the one list that is `
1801
+ + `complete by construction was never written, which is not a band holding no identical marks` };
1802
+ const offices = [...new Set(rows.map(bandRowOffice).filter(Boolean))].sort();
1803
+ // A row whose office cannot be read is counted and named rather than dropped: it narrows the span
1804
+ // this op can see, and a span that is narrow because the field is missing is not a narrow band.
1805
+ const officeless = rows.filter((r) => !bandRowOffice(r)).length;
1806
+ const met = rows.length >= minRecords && offices.length >= minOffices;
1807
+ const body = `${rows.length} live in-class identical/near-identical floor row(s) (floor ${minRecords}) across `
1808
+ + `${offices.length} office(s) [${offices.join(", ") || "none"}] (floor ${minOffices})`
1809
+ + (officeless ? `, and ${officeless} row(s) carry no office, so the span read here is a floor on the span, not a count of it` : "");
1810
+ return shape.verdict(met, body,
1811
+ `${body} — the band the clearance lane merged is smaller or narrower than this scenario exists to hand the size-dependent paths`);
1812
+ }
1813
+
1640
1814
  // field ops
1641
1815
  const fn = OPS[a.op];
1642
1816
  if (!fn) return { ok: false, saw: `UNIMPLEMENTED op "${a.op}" — not passing by omission`, unimplemented: true };
@@ -203,6 +203,22 @@ export function mergeEnvNameBindings(perFile) {
203
203
  // read as `env[X]`, for the reason READ_RE gives.
204
204
  const CONST_READ_RE = /(?<![.\w$])(?:process\??\.)?env(?:\?\.)?\[\s*([A-Z][A-Z0-9_]*)\s*\](?!\s*=[^=])/g;
205
205
 
206
+ // THE NAME HELD IN A TABLE AS A VALUE, which is a read the three readers above cannot see. A generic
207
+ // resolver takes the name out of a row and reads it — `{ env: "CLEAROTRON_CODEX_PATH", fallback: "codex" }`
208
+ // — so the variable is read on every run and spelled nowhere the audit was looking. Thirteen names are
209
+ // read exactly this way, and two of them are live deployment settings that no rule could ask for a row
210
+ // for, because every ratchet that would is keyed on the name being seen.
211
+ //
212
+ // MATCHED ON THE PROPERTY NAME, NEVER ON THE VALUE'S SHAPE, and that is the whole reason this is safe.
213
+ // A rule that took any uppercase string literal would take every message key, token and enum member in
214
+ // the tree and drown the audit in names nothing reads. The property names are the closed set the
215
+ // resolvers actually use; a new resolver that invents a fourth spelling is invisible again, which is a
216
+ // known limit of this shape rather than a defect in the rule — the same limit the accessor reader has.
217
+ //
218
+ // This is the third time this shape has hidden a read here: the accessor family was the first, `envFrom`
219
+ // the second, and a name held as a value is those two one remove further on.
220
+ const TABLE_NAME_RE = /(?<![.\w$])(?:env|envName|tokenEnv)\s*:\s*["']([A-Z][A-Z0-9_]*)["']/g;
221
+
206
222
  export function namesRead(text, bindings = null) {
207
223
  const found = new Set();
208
224
  const stripped = stripCommentLines(text);
@@ -210,6 +226,8 @@ export function namesRead(text, bindings = null) {
210
226
  for (let m; (m = READ_RE.exec(stripped));) found.add(m[1] || m[2]);
211
227
  ACCESSOR_RE.lastIndex = 0;
212
228
  for (let m; (m = ACCESSOR_RE.exec(stripped));) found.add(m[1] || m[2]);
229
+ TABLE_NAME_RE.lastIndex = 0;
230
+ for (let m; (m = TABLE_NAME_RE.exec(stripped));) found.add(m[1]);
213
231
  // Resolved through the corpus map when one is supplied. Absent map ⇒ this half is simply off, which
214
232
  // is what keeps the function pure and drivable on a single string.
215
233
  if (bindings) {
@@ -357,6 +375,15 @@ export const EFFECT_CLASSES = Object.freeze({
357
375
  credential: "authentication material; absent, the run refuses at preflight by name",
358
376
  deployment: "where input and output live; the conclusion a run reaches is unchanged",
359
377
  tuning: "how long or how hard a run tries; the conclusion a run reaches is unchanged",
378
+ // WHAT THE INSTALL WIZARD WROTE WHEN IT SET THIS MACHINE UP. Added because the classifier had computed
379
+ // this class for years and no declaration could say it: five names computed `setup` against a
380
+ // vocabulary with no word for it, so each was either undeclared or declared as the nearest wrong
381
+ // thing. A declaration that cannot be true is worse than none — it reads as considered.
382
+ //
383
+ // It is its own class rather than a flavour of `deployment` because the two answer different
384
+ // questions. `deployment` says where this machine keeps things; `setup` says what the installer
385
+ // found or made here, which is the fact an operator needs when a program has moved.
386
+ setup: "what the install wizard found or wrote when it set this machine up; the conclusion a run reaches is unchanged",
360
387
  harness: "read only on a fixture, replay or self-test path; no production run reaches it",
361
388
  });
362
389
 
@@ -535,13 +562,18 @@ export function auditEnv(root = ROOT) {
535
562
  // roster, while ADR-0003 had already ruled case-law setup an OAuth flow
536
563
  // and not a variable at all. Evidence of a reader is not evidence of a
537
564
  // READ: the roster is a list of names to look for, not a call site.
538
- // 4 the AZURE_OPENAI_* block, an external contract (below).
565
+ // 4 the AZURE_OPENAI_* block, then kept as an external contract. That call did
566
+ // not hold: nothing the product runs read them, and the rows were removed
567
+ // from `.env.example` on 2026-09-15 (below). They are no longer counted
568
+ // among the wrong deletions.
539
569
  // 1 CLEAROTRON_SEND_TOOL_PREFIX — genuinely dead, and this direction does not
540
570
  // catch it either: its one surviving mention is a governance-doc line, and
541
571
  // a mention is enough to spare a row. Under-firing is the cost of the
542
572
  // trade, taken deliberately. It is the prose-sweep class.
543
573
  //
544
- // So SIX of ten deletions would have been wrong, two of them credential rows.
574
+ // So FOUR of ten deletions would have been wrong, the four live reads above,
575
+ // one of them a credential row. (This said six until 2026-09-15; that count
576
+ // did not follow from the rows as listed, and it included the Azure block.)
545
577
  // Evidence used instead: the bare NAME, on a name boundary, anywhere in the
546
578
  // tracked tree. The accessor family is filed as and does NOT belong here —
547
579
  // see below.
@@ -561,10 +593,11 @@ export function auditEnv(root = ROOT) {
561
593
  // ── AND IT IS NOT A SUPPRESSION LIST ────────────────────────────────────────────────────────────
562
594
  //
563
595
  // forbids one, rightly: "if a row is deliberately readerless, the row goes, not the guard."
564
- // Applied literally that ruling deletes three rows it should not. `.env.example`'s Azure block
565
- // documents the variables an EXTERNAL agent platform consumes — the block says so itself, ruled
566
- // it, and `MODELS.azure` / `CLEAROTRON_AZURE_MODEL` / `jxPolicy.providerStance: "azure-only"` are live.
567
- // There is no reader in this tree and there never was one to retire.
596
+ // Applied literally, that ruling deletes a row whose consumer is not this tree at all: a variable read
597
+ // by a program the product spawns, such as the Claude program's own sign-in token. The row documents a
598
+ // contract with that consumer, and there is no reader here to retire. (The four AZURE_OPENAI_* rows that
599
+ // first raised this were a different case. They named another platform's settings, nothing the product
600
+ // runs read them, and they were removed from `.env.example` on 2026-09-15.)
568
601
  //
569
602
  // So a row is ACCOUNTED FOR two ways, and this is one rule applied to every row rather than a list of
570
603
  // exempt names: something in the tree names it, OR the row carries an inline `# external:` line
@@ -355,6 +355,19 @@ export const DEPLOYMENT_NAMES = new Set([
355
355
  // Set by the install on the copy it starts after moving out of npx's cache, never by an operator. Its
356
356
  // row declares `deployment`, and no shape matches it, so unlisted it would fall through to `tuning`.
357
357
  "CLEAROTRON_RELOCATED",
358
+ // The two register ledgers, listed the day the scanner learned to read a name held in a table as a
359
+ // value — the same shape as the optional-chaining three below, one scanner widening later. Both name
360
+ // WHERE a file lives and resolve by an existence ladder over several directories, which is the
361
+ // definition `CLEAROTRON_JX_SUBCLASS_DB` was moved here on: a row whose own words name a place is
362
+ // `deployment`, and it reaches `tuning` only because `tuning` is the residual.
363
+ //
364
+ // GETTING THIS WRONG IS WORSE THAN THE BLINDNESS IT REPLACES. No deployed box sets either — the
365
+ // ledger module says so and explains why the default is resolved by existence rather than by name —
366
+ // so `tuning` with an empty set-site list is precisely the population step 3 deletes from. Making
367
+ // them visible without listing them here would move the billing-grade call ledger and the record
368
+ // ledger a "verified from the record" claim joins against from unseen to proposed for deletion.
369
+ "CLEAROTRON_REGISTER_CALL_LOG",
370
+ "CLEAROTRON_REGISTER_RECORD_LOG",
358
371
  // Three names read only through optional chaining, so no catalogue check saw them until the scanner
359
372
  // learned `?.`. Each row declares `deployment`, and no shape matches them (`_PORT$` does not take
360
373
  // `_PORTS`), so unlisted each fell through to `tuning`, the bucket step 3 deletes from. The first is set
@@ -502,7 +515,12 @@ export function classify({ catalogue, sources, setup = setupNames(), readSites =
502
515
  };
503
516
 
504
517
  const cls = (name) => OVERRIDES[name] ?? (setup.has(name) ? "setup"
505
- : VENDOR_RE.test(name) ? "vendor-credential"
518
+ // `credential`, the word the declared vocabulary uses, NOT a second word for the same thing. This
519
+ // computed `vendor-credential` while no row could declare it, which is the same gap that left the
520
+ // install wizard's names undeclarable — one side of the contract saying a word the other side has
521
+ // no way to say. The rule settled with that one holds here: the classifier and the vocabulary say
522
+ // the SAME word, and a disagreement is fixed rather than frozen as an exception.
523
+ : VENDOR_RE.test(name) ? "credential"
506
524
  // The listed audience is consulted BEFORE the shapes, so a renamed name keeps the class a human gave
507
525
  // it rather than the one its new spelling happens to match.
508
526
  : deploymentNames.has(name) ? "deployment"
@@ -678,7 +696,7 @@ function build() {
678
696
  _what: "#1838 step 1 — every catalogued variable classified, and for every TUNING name the environments that have ever set it.",
679
697
  _how: "Regenerate with: node scripts/env-classify.mjs --check (the production half is read from docs/architecture/env-set-in-production.txt, which needs `--gather-prod` on the production box to refresh).",
680
698
  _values: "NO VALUE from any environment is read into this artifact. Names only.",
681
- _counts: { rows: rows.length, setup: by("setup"), deployment: by("deployment"), vendorCredential: by("vendor-credential"), tuning: by("tuning") },
699
+ _counts: { rows: rows.length, setup: by("setup"), deployment: by("deployment"), credential: by("credential"), tuning: by("tuning") },
682
700
  _stepThreePopulation: {
683
701
  _what: "What step 3 may act on: a numeric default, and no environment anywhere sets it.",
684
702
  _warning: "A candidate list, not a licence. Each still needs its read site read and its guard checked before deletion.",
@@ -29,9 +29,11 @@
29
29
  // _driver/run.jsonl the event log
30
30
  // _driver/stage-inputs/ what each stage was handed
31
31
  // _history/ pre-reopen snapshots
32
- // Dropping the telemetry drops `meta.tokens` (driver/publish/index.mjs:1136 rollupTokens — the only consumer of
33
- // rollupTokens). That is the one difference step 5 is told to expect, and it says so out loud rather than
34
- // normalising it away in silence.
32
+ // Dropping the telemetry drops `meta.tokens` (driver/publish/index.mjs rollupTokens — the only consumer of
33
+ // rollupTokens), and with it the record of which models served the run (servedModels in
34
+ // driver/tokens.mjs): `servedModels` on meta.json and report-data.json, and the one line that closes the
35
+ // report's footer. Those are the differences step 5 is told to expect, and it says so out loud rather
36
+ // than normalising them away in silence.
35
37
  //
36
38
  // WHAT THIS SCRIPT DOES NOT DO
37
39
  // It does not decide the sample is publishable. It greps for the shapes that must never leave the VM
@@ -45,7 +47,8 @@ import {
45
47
  } from "node:fs";
46
48
  import { join, dirname, relative, basename } from "node:path";
47
49
  import { createHash } from "node:crypto";
48
- import { driverDir } from "../shared/driver-dir.mjs"; //
50
+ import { driverDir } from "../shared/driver-dir.mjs";
51
+ import { PUBLISH_INPUTS } from "../driver/publish/publish-inputs.mjs"; //
49
52
  import { tmpdir } from "node:os";
50
53
  import { fileURLToPath, pathToFileURL } from "node:url";
51
54
 
@@ -59,24 +62,33 @@ const FROZEN_FILES = [
59
62
  { path: "audit.md", why: "publish/index.mjs:1022 auditMd, the audit workbook source" },
60
63
  { path: "findings.json", why: "publish/index.mjs:715 readStore, the per-finding machine contract" },
61
64
  { path: "status.json", why: "publish/index.mjs:913 machineLedgerNote + markName" },
62
- { path: "case-law-findings.md", why: "publish/index.mjs:849 clPath, the case-law section" },
63
- { path: "common-law-grid.json", why: "publish/index.mjs:973 commonLawJoinedTerms, common-law coverage" },
65
+ { path: "case-law-findings.md", why: "`clPath` declared in index.mjs, the case-law section" },
66
+ { path: "common-law-grid.json", why: "`commonLawJoinedTerms` declared in index.mjs, common-law coverage" },
64
67
  // publish/index.mjs — the _driver sidecars it reads by name
65
68
  { path: "_driver/receipts.json", why: "publish/index.mjs:761 fetchReceipts" },
66
69
  { path: "_driver/senior-rights.json", why: "publish/index.mjs:787 seniorRights" },
67
70
  { path: "_driver/verdict.json", why: "publish/index.mjs:792 verdictInfo" },
68
71
  { path: "_driver/framework.json", why: "publish/index.mjs, the frozen band vocabulary the run was rated under" },
69
- { path: "_driver/register-plan.json", why: "publish/index.mjs:820 scopeBasis" },
70
- { path: "_driver/instructed-scope.json", why: "publish/index.mjs:821 searchedJurisdictions, the fallback for register-plan" },
71
- { path: "_driver/enforcer-signals.json", why: "publish/index.mjs:861 esPath" },
72
- { path: "_driver/predelivery-lint.json", why: "publish/index.mjs:170 lintSink" },
73
- { path: "_driver/escalation-state.json", why: "publish/index.mjs:171 escSink" },
74
- { path: "_driver/reasoning-integrity.json", why: "publish/index.mjs:898 integritySink" },
75
- { path: "_driver/corrections-state.json", why: "publish/index.mjs:172 correctionsSink" },
76
- { path: "_driver/search-policy.json", why: "publish/index.mjs:955 searchPolicy, level + stage label" },
72
+ { path: "_driver/register-plan.json", why: "publish/index.mjs:901 scopeBasis" },
73
+ { path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
74
+ { path: "_driver/enforcer-signals.json", why: "`esPath` declared in index.mjs" },
75
+ { path: "_driver/predelivery-lint.json", why: "publish/index.mjs:172 lintSink" },
76
+ { path: "_driver/escalation-state.json", why: "publish/index.mjs:173 escSink" },
77
+ { path: "_driver/reasoning-integrity.json", why: "`integritySink` declared in index.mjs" },
78
+ { path: "_driver/corrections-state.json", why: "publish/index.mjs:174 correctionsSink" },
79
+ { path: "_driver/search-policy.json", why: "`searchPolicy` declared in index.mjs, level + stage label" },
77
80
  { path: "_driver/profile.json", why: "publish/index.mjs reads the frozen profile; report-registry.mjs:42 republishRun, customer key" },
78
81
  ];
79
82
 
83
+ // EVERY INPUT THE PUBLISHER DECLARES TRAVELS, taken from its own closed table rather than restated. The
84
+ // hand-kept list above drifted from that table: register-named-band.json, the recall receipt and the
85
+ // local-language lane's two files were declared publish inputs and were left behind, so a frozen republish
86
+ // drew no register section and no local-language row where the source run drew both. A declared input
87
+ // added later now travels without an edit here; one the publisher does not declare still needs a line above.
88
+ for (const path of Object.keys(PUBLISH_INPUTS)) {
89
+ if (!FROZEN_FILES.some((f) => f.path === path)) FROZEN_FILES.push({ path, why: "declared in publish/publish-inputs.mjs" });
90
+ }
91
+
80
92
  // ── THE KNOCKOUT LANE IS A DIFFERENT WORKSPACE, AND report.md IS NOT IN IT ─
81
93
  //
82
94
  // A knockout run never writes report.md: for that lane the markdown is an OUTPUT of publish, not an
@@ -106,7 +118,7 @@ const KNOCKOUT_FILES = [
106
118
  // knockout report render empty (publish/knockout.mjs:140-155). Named by stages-knockout.mjs:32,41.
107
119
  { path: "_driver/register-counts.json", why: "publish/knockout.mjs:140-155 counted figures + the Register column" },
108
120
  { path: "_driver/register-records.json", why: "stages-knockout.mjs:41 the terms behind the close-variation axis" },
109
- { path: "_driver/instructed-scope.json", why: "publish/index.mjs:821 searchedJurisdictions, the fallback for register-plan" },
121
+ { path: "_driver/instructed-scope.json", why: "publish/index.mjs:902 searchedJurisdictions, the fallback for register-plan" },
110
122
  ];
111
123
 
112
124
  /** The allowlist for a template. One place, so a new template cannot half-exist. */
@@ -162,7 +174,7 @@ const SCRUB = [
162
174
  // hides the next real difference.
163
175
  const VOLATILE = [
164
176
  { id: "issued", re: /\d{4}-\d{2}-\d{2} · \d{2}:\d{2} [A-Z]{2,5}/g, sub: "<issued>", why: "publish/index.mjs, the generation stamp in the firm locale" },
165
- { id: "iso-timestamp", re: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, sub: "<ts>", why: "publish/index.mjs:666 asOf" },
177
+ { id: "iso-timestamp", re: /\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z/g, sub: "<ts>", why: "publish/index.mjs:668 asOf" },
166
178
  ];
167
179
 
168
180
  // ── REWRITES — what is CHANGED on the way out, as opposed to what is refused ───────────────────────
@@ -174,7 +186,7 @@ const VOLATILE = [
174
186
  //
175
187
  // 1. THE BIRTH CODENAME, IN CONTENT. `--codename` rebuilds the run's IDENTITY — the leaf, the runId, the
176
188
  // directory — and the content keeps the old pair: `_receipt.context` in every record artifact reads
177
- // "prelim-<matter>-<codename>-register-unit-primary-sweep", and audit.md and status.json carry it in
189
+ // "clearance-<matter>-<codename>-register-unit-primary-sweep", and audit.md and status.json carry it in
178
190
  // prose. driver/test/no-client-identifiers.test.mjs check 2 sweeps CONTENT as well as paths, so a
179
191
  // tree renamed but not rewritten can never be committed — which is exactly what the old echo check
180
192
  // reported, correctly, and could do nothing about.
@@ -226,8 +238,19 @@ const substituteVendorKey = (key) => {
226
238
  // meta.json keys the freeze is EXPECTED to change, with the reason. Anything else differing is a finding.
227
239
  const EXPECTED_META_DELTA = {
228
240
  tokens: "telemetry pruned — _driver/*.jsonl is the only source (driver/tokens.mjs:82)",
241
+ servedModels: "telemetry pruned — the attempt rows in _driver/*.jsonl are the only source (servedModels in driver/tokens.mjs)",
229
242
  };
230
243
 
244
+ // THE SAME CAUSE, ON THE TWO OTHER SURFACES THAT SHOW IT. report-data.json carries `servedModels` beside the
245
+ // report's content, and the page renders it as the scope section's closing line (render.mjs
246
+ // servedModelsLine, class "servedby"). Only that key and that one paragraph are set aside, on both sides
247
+ // and out loud; a difference anywhere else in either file is still a finding. The paragraph holds escaped
248
+ // text and no markup, so the pattern cannot run past its own closing tag. It takes the whitespace before
249
+ // the paragraph with it: the clearance page joins its scope parts with a line break and an indent, which
250
+ // exists only because the line does.
251
+ const EXPECTED_DATA_DELTA = ["servedModels"];
252
+ const SERVED_LINE_RE = /\s*<p class="servedby"[^>]*>[^<]*<\/p>/g;
253
+
231
254
  // ── args ─────────────────────────────────────────────────────────────────────────────────────────────
232
255
  const argv = process.argv.slice(2);
233
256
  const flag = (name) => { const i = argv.indexOf(name); return i >= 0 ? argv[i + 1] : null; };
@@ -594,7 +617,27 @@ if (proofOk) {
594
617
  }
595
618
  continue;
596
619
  }
597
- if (normalise(rawA) === normalise(rawB)) { note(`${name} identical (${rawB.length} bytes)`); continue; }
620
+ // The served-model record is set aside by name on both sides (EXPECTED_DATA_DELTA, SERVED_LINE_RE),
621
+ // and only when it is what differed does the note say so.
622
+ let sA = normalise(rawA), sB = normalise(rawB), aside = [];
623
+ if (/^report-data(?:-.+)?\.json$/.test(name)) {
624
+ let dA = null, dB = null;
625
+ try { dA = JSON.parse(rawA); dB = JSON.parse(rawB); } catch { dA = dB = null; }
626
+ if (dA && dB) {
627
+ aside = EXPECTED_DATA_DELTA.filter((k) => JSON.stringify(dA[k]) !== JSON.stringify(dB[k]));
628
+ for (const k of EXPECTED_DATA_DELTA) { delete dA[k]; delete dB[k]; }
629
+ sA = normalise(JSON.stringify(dA, null, 2)); sB = normalise(JSON.stringify(dB, null, 2));
630
+ }
631
+ } else if (name.endsWith(".html") && sA !== sB) {
632
+ const tA = sA.replace(SERVED_LINE_RE, ""), tB = sB.replace(SERVED_LINE_RE, "");
633
+ if (tA === tB) { aside = ["the footer's served-models line"]; sA = tA; sB = tB; }
634
+ }
635
+ if (sA === sB) {
636
+ note(aside.length
637
+ ? `${name} identical apart from ${aside.join(", ")}, which differs as expected — ${EXPECTED_META_DELTA.servedModels}`
638
+ : `${name} identical (${rawB.length} bytes)`);
639
+ continue;
640
+ }
598
641
  finding(`${name} differs between the source run and the frozen copy — the allowlist dropped an input the renderer reads`);
599
642
  }
600
643
  }
@@ -53,6 +53,29 @@ export function treeState(root = ROOT) {
53
53
  * The alternative — dirtying a real generated file and restoring it — is a shared-file mutation, and
54
54
  * the test runner runs files in parallel, so it would be a race that reddens somebody else's arm.
55
55
  */
56
+ /**
57
+ * Which paths differ between two `git status --porcelain` readings, named so a reader can see WHAT moved.
58
+ *
59
+ * A porcelain line is a two-character state, a space, and the path. Lines are compared as a multiset so
60
+ * a path whose STATE changed — staged to modified, say — is reported as having moved, and the paths are
61
+ * returned rather than a count, because two numbers agreeing is not the same as two sets agreeing.
62
+ */
63
+ export function movedPaths(before, after) {
64
+ const bag = (s) => {
65
+ const m = new Map();
66
+ for (const line of String(s).split("\n")) {
67
+ if (!line.trim()) continue;
68
+ m.set(line, (m.get(line) ?? 0) + 1);
69
+ }
70
+ return m;
71
+ };
72
+ const [b, a] = [bag(before), bag(after)];
73
+ const out = new Set();
74
+ for (const [line, n] of a) if ((b.get(line) ?? 0) !== n) out.add(line.slice(3).trim() || line.trim());
75
+ for (const [line, n] of b) if ((a.get(line) ?? 0) !== n) out.add(line.slice(3).trim() || line.trim());
76
+ return [...out].sort();
77
+ }
78
+
56
79
  export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree = () => treeState(root) } = {}) {
57
80
  const found = minters(dir);
58
81
  if (!found.length) return { found, stale: [], unreadable: [], wrote: [], empty: true };
@@ -60,6 +83,7 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
60
83
  const stale = [];
61
84
  const unreadable = [];
62
85
  const wrote = [];
86
+ const unattributable = [];
63
87
  for (const m of found) {
64
88
  // ── `--check` IS A CONTRACT, AND NOTHING WAS VERIFYING IT ──────────────────────────────────────
65
89
  //
@@ -73,13 +97,41 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
73
97
  // one ran, it wrote. THE LIMIT, SAID RATHER THAN LEFT: this catches the harmful inert form, the
74
98
  // one that silently repairs. A minter that ignores the flag and does nothing at all still reports
75
99
  // `current`, and no probe from out here can tell that from a file that really is current.
100
+ // ── "DID THE TREE MOVE" IS NOT "DID THIS PROCESS WRITE" ────────────────────────────────────────
101
+ //
102
+ // Those are the same question only where nothing else can write, and this probe does not run
103
+ // there. Inside the suite it is a subprocess of one test file while every other file in its shard
104
+ // runs beside it, deliberately unserialised — dropping `--test-concurrency=1` is what made the
105
+ // suite 2.4 times faster. So a neighbour writing anywhere in the repository moved the snapshot and
106
+ // this reported it as the minter having written.
107
+ //
108
+ // Measured: a commit whose whole diff was one stylesheet's phone-width rules and a release note
109
+ // failed here naming TWO minters, and the same bytes passed on a rerun. Two is the tell — a minter
110
+ // that ignores `--check` and re-mints leaves its repair in the tree, so the NEXT minter's `before`
111
+ // already carries it and the next one is not flagged. Both being named cannot come from either.
112
+ //
113
+ // So the accusation is made only where it can be: from a tree that was CLEAN when this minter
114
+ // started, where a change appearing during its run has no other author available. A tree that was
115
+ // already dirty is one where somebody else is writing, and the honest answer is that this probe
116
+ // could not look — which is this file's own rule one level in, since it already refuses to read a
117
+ // minter that could not look as a pass.
118
+ //
119
+ // THE LIMIT, SAID RATHER THAN LEFT: a neighbour that begins writing after a clean reading and
120
+ // before the minter exits is still attributed here. Closing that needs the probe to own the tree,
121
+ // which is a change to where it runs rather than to what it asks.
76
122
  const before = readTree();
77
123
  const r = spawnSync(process.execPath, [join(dir, m), "--check"], { cwd: root, encoding: "utf8" });
78
124
  const after = readTree();
79
125
  const out = ((r.stdout || "") + (r.stderr || "")).trim();
80
126
  if (before !== null && after !== null && before !== after) {
81
- wrote.push({ m, out });
82
- log(` WROTE ${m} (during --check)`);
127
+ const moved = movedPaths(before, after);
128
+ if (before.trim() === "") {
129
+ wrote.push({ m, out, moved });
130
+ log(` WROTE ${m} (during --check): ${moved.join(", ") || "the tree moved"}`);
131
+ } else {
132
+ unattributable.push({ m, moved });
133
+ log(` ? ${m} — the tree moved and this probe cannot say who moved it: ${moved.join(", ") || "paths unknown"}`);
134
+ }
83
135
  continue;
84
136
  }
85
137
  // 0 is current, 1 is stale, anything else is a minter that could not look — reported separately,
@@ -89,11 +141,11 @@ export function checkAll({ dir = HERE, root = ROOT, log = console.log, readTree
89
141
  unreadable.push({ m, out, code: r.status });
90
142
  log(` ? ${m} (exit ${r.status})`);
91
143
  }
92
- return { found, stale, unreadable, wrote, empty: false, contractChecked: readTree() !== null };
144
+ return { found, stale, unreadable, wrote, unattributable, empty: false, contractChecked: readTree() !== null };
93
145
  }
94
146
 
95
147
  function main() {
96
- const { found, stale, unreadable, wrote, empty, contractChecked } = checkAll();
148
+ const { found, stale, unreadable, wrote, unattributable, empty, contractChecked } = checkAll();
97
149
  if (empty) {
98
150
  console.error("generated-files-are-current: no scripts/mint-*.mjs found. Either they moved or the "
99
151
  + "naming changed — and a pass over nothing is not a pass.");
@@ -111,6 +163,19 @@ function main() {
111
163
  + `commit without the repair, and reports current over a check that did not happen. Fix the minter.`);
112
164
  process.exit(2);
113
165
  }
166
+ // BEFORE the staleness verdict, and exit 2 rather than 1: a tree moving under the probe means the
167
+ // staleness answers were read off a tree that was changing while they were taken, so "out of date" is
168
+ // not a claim this run has the standing to make either. Could-not-look is the whole verdict.
169
+ if (unattributable.length) {
170
+ console.error(`\n${unattributable.length} minter(s) ran while the tree was ALREADY dirty and it moved `
171
+ + `underneath them. This probe cannot say whether the minter wrote or something running beside it `
172
+ + `did, so it names neither. That is a could-not-look, not a pass and not an accusation.\n`);
173
+ for (const { m, moved } of unattributable) {
174
+ console.error(` ${m} — moved: ${moved.join(", ") || "paths unknown"}`);
175
+ }
176
+ console.error(`\nRun it on a tree nobody else is writing to, and it will answer.`);
177
+ process.exit(2);
178
+ }
114
179
  if (unreadable.length) {
115
180
  console.error(`\n${unreadable.length} minter(s) could not look. That is not a pass; fix the minter first.`);
116
181
  process.exit(2);