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
@@ -405,7 +405,7 @@ export function nativeLanguageMode(product) {
405
405
  *
406
406
  * Exported because two surfaces outside this module have to say it and both used to say something else:
407
407
  * the resolution-time recommendation (jx-lanes.mjs zhScopeDepthNotes) and the DELIVERED REPORT's own
408
- * coverage row (pipeline.mjs scriptScopeDisclosure). Both named `prelim-jx` and `Depth 5` — an internal
408
+ * coverage row (pipeline.mjs scriptScopeDisclosure). Both named `clearance-jx` and `Depth 5` — an internal
409
409
  * product key and a rung on a ladder — so the coverage row named a remedy that was a product
410
410
  * deleted, quoted at a number that no longer exists, and could not have ordered either.
411
411
  *
@@ -53,8 +53,8 @@ consumes it; a unit test greps each symbol, so a field with no live consumer fai
53
53
  | `delivery` | delivery | `{ email: "summary", privileged: bool }` — the "Privileged & Confidential" header. Two further optional sub-keys are accepted: `style` (a prose string, guarded by the same anti-rule check as a context pack, dictated into the `report-overview` and `report-card` stages as presentation tone and never to `synthesis`, the rating stage) and `template` (a report-template name; `"standard"` is the only one that exists, and it is the default). **Absent ⇒ neutral default** (`{email:"summary"}` — deliberately SILENT on `privileged`, which is three-state on every surface that reads it (`confPosture`): `true` extends the marking to "Attorney Work Product", `false` is a deliberate OFF, and absent is no opinion, which gets the plain "Privileged & Confidential" every legal deliverable carries. Saying `false` in the neutral overlay read as an instruction to strip the marking, and a Generic-default clearance shipped with no line at all). `email` no longer selects anything: every run's mail is a COVER NOTE pointing at the one report. The old `"table"` value — a full review table inlined into the mail body — is **retired**; it is still accepted at load so stored profiles keep validating, and folded to `"summary"` by `normalizeDelivery`. A customer who wants their own house format gets it drafted by the assistant from the run's `report-data.json`, where a person reads it before it goes |
54
54
  | `riskAppetite` | context | a PROSE-POSTURE string (optional) that flavours **emphasis + recommended follow-up** in delivery curation — the two stages that are dictated it, `report-overview` and `report-card` — **never the Level/Composite**. A load-time anti-threshold guard rejects numeric/threshold phrasing (`>50%`, `Level C or above`, `threshold`); the "never decides" invariance itself is gated by review |
55
55
  | `marketplaceDensity` | structural | `"sparse"` (default) \| `"dense"` — selects the per-profile grid cell budget so a byte-heavy marketplace's verbatim stdout fits the worker output channel (sparse ⇒ 98-cell budget; dense ⇒ 16). Dense fits long retail listings (beverages and supplements); gaming stores stay sparse |
56
- | `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/prelim-search/<file>.md`, path-escape-blocked). **The framework in force RATES the matter** — the customer's own if on file, else the Generic default `risk-framework.md`; nothing in between. Each framework is a prose deck (the client's own rubric, reasoned WITH) plus a `.manifest.json` sidecar carrying its band vocabulary (`test/framework-lint.test.mjs` guards the pair). Git + legal-team gated; the config UI shows which is in force, read-only |
57
- | `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/prelim-search/<file>.md`). The analysis DEPTH TARGET in `synthesis`, calibrated under that customer's framework; **absent ⇒ the Generic default `worked-examples.md`** |
56
+ | `frameworkPath` | rating authority | optional path to the customer's OWN risk framework (`skills/clearance-search/<file>.md`, path-escape-blocked). **The framework in force RATES the matter** — the customer's own if on file, else the Generic default `risk-framework.md`; nothing in between. Each framework is a prose deck (the client's own rubric, reasoned WITH) plus a `.manifest.json` sidecar carrying its band vocabulary (`test/framework-lint.test.mjs` guards the pair). Git + legal-team gated; the config UI shows which is in force, read-only |
57
+ | `workedExamplesPath` | context | optional path to a per-customer worked-examples set (`skills/clearance-search/<file>.md`). The analysis DEPTH TARGET in `synthesis`, calibrated under that customer's framework; **absent ⇒ the Generic default `worked-examples.md`** |
58
58
  | `defaultProduct` | entitlement | which of the four searches runs when a request names none (`search-policy.mjs`). May be left unset, and that is not a gap — a clearance that names no product is then named by its own resolved territories |
59
59
  | `allowedRecipes[]` | entitlement | the closed menu of searches this account may trigger; non-empty when present, and **absent ⇒ everything allowed** |
60
60
  | `jxPolicy` | entitlement | the native-language deepening POSTURE — declared lanes / escalation / provider stance. Policy, never capability; frozen into the run sidecar so a resume keeps the posture the run started under |
@@ -90,7 +90,7 @@ value into a profile file is a load-time error.
90
90
  the write door: the same profile saved through the editor is a validated auto-commit with no PR in
91
91
  it, and the shared load-time validators are what both paths cannot get past.
92
92
  3. **Rewrite check:** the skills are platform-agnostic (they follow the dictated list), but read
93
- `skills/prelim-common-law/SKILL.md` + `perplexity-prompts.md`, `skills/prelim-search/SKILL.md` +
93
+ `skills/clearance-common-law/SKILL.md` + `perplexity-prompts.md`, `skills/clearance-search/SKILL.md` +
94
94
  `phase2-execution.md`, and the driver message prose in `stages.mjs` once against the new
95
95
  industry — worked examples are gaming-flavored by history.
96
96
  4. ONE validation run against a known/synthetic matter: confirm the grid swept exactly the
@@ -17,8 +17,8 @@
17
17
  "delivery": { "email": "summary", "privileged": false },
18
18
  "riskAppetite": "Evidence-first. Lead with the register position, then say plainly what the common-law picture adds or fails to settle. Name coverage limits in the body rather than in a footnote — an unread source is a fact about the search, not a caveat about the report. Keep crowded-field noise brief.",
19
19
  "defaultProduct": "multi-country-focus-search",
20
- "frameworkPath": "skills/prelim-search/risk-framework-demo.md",
21
- "workedExamplesPath": "skills/prelim-search/worked-examples-demo.md",
20
+ "frameworkPath": "skills/clearance-search/risk-framework-demo.md",
21
+ "workedExamplesPath": "skills/clearance-search/worked-examples-demo.md",
22
22
  "jxPolicy": { "laneDepth": { "ja": "full", "ko": "candidates" } },
23
23
  "runCaps": { "dailyRuns": 12, "maxQueued": 2 }
24
24
  }
@@ -346,9 +346,12 @@ export const FIELD_CONSUMERS = {
346
346
 
347
347
  // Per-customer reasoning-skill selection. frameworkPath is the customer's OWN risk framework — under doc 50
348
348
  // it RATES the matter; absent ⇒ the Generic default rates it (DEFAULT_FRAMEWORK in framework.mjs). Constrained to
349
- // the prelim-search skill dir + a .md suffix so a profile cannot point the synthesis read at an arbitrary
349
+ // the clearance-search skill dir + a .md suffix so a profile cannot point the synthesis read at an arbitrary
350
350
  // path. The SHARED doctrine lives identically across the per-customer frameworks; only the examples diverge.
351
- const SKILL_PATH_RE = /^skills\/prelim-search\/[A-Za-z0-9._-]+\.md$/;
351
+ // The folder's pre-rename spelling is accepted as the same folder: a store written before the rename names it,
352
+ // and one unmigrated profile must not take a company, or the whole roster, off the list. The file is read
353
+ // from whichever spelling the store holds (config.resolveSkillPath).
354
+ const SKILL_PATH_RE = /^skills\/(?:clearance|prelim)-search\/[A-Za-z0-9._-]+\.md$/;
352
355
 
353
356
  // The delivery overlay every run gets. `email` is no longer a choice: every run's mail is a COVER NOTE
354
357
  // pointing at the report (one report, one shape, per-lawyer client mail drafted by the assistant).
@@ -566,11 +569,11 @@ function validateProfileShape(key, p, { sparse = false } = {}) {
566
569
  die(`marketplaceDensity must be ${MARKETPLACE_DENSITIES.map((v) => `"${v}"`).join(" or ")} `
567
570
  + `(got "${p.marketplaceDensity}")`);
568
571
  // frameworkPath / workedExamplesPath (optional, Phase 2): a per-customer reasoning-skill file. Constrained
569
- // to skills/prelim-search/*.md (no path escape) — a profile selects a SHIPPED skill, never an arbitrary path.
572
+ // to skills/clearance-search/*.md (no path escape) — a profile selects a SHIPPED skill, never an arbitrary path.
570
573
  for (const k of ["frameworkPath", "workedExamplesPath"]) {
571
574
  if (p[k] == null) continue;
572
575
  if (typeof p[k] !== "string" || !SKILL_PATH_RE.test(p[k]) || p[k].includes(".."))
573
- die(`${k} must be a path of the form "skills/prelim-search/<file>.md" (got ${JSON.stringify(p[k])})`);
576
+ die(`${k} must be a path of the form "skills/clearance-search/<file>.md" (got ${JSON.stringify(p[k])})`);
574
577
  }
575
578
  // defaultProduct (optional): one of the four in the offering, by id. A typo hard-fails at load rather
576
579
  // than silently running the wrong-priced product on every job.
@@ -679,25 +682,42 @@ let cache = null;
679
682
  /** Read one directory of profiles/<key>.json → Map(key → profile). No generic requirement and no
680
683
  * matchDomains check here: both are properties of the MERGED roster, not of one layer, and asserting
681
684
  * them per-layer would refuse a base+overlay pair that is perfectly valid once combined. */
682
- function readProfilesLayer(dir) {
685
+ // ONE COMPANY THAT CANNOT BE READ MUST NOT TAKE THE OTHERS WITH IT — on the deployment's own store. Strict,
686
+ // every file or none, is right for an explicit directory: that is how this loader's rules are checked. On the
687
+ // configured store it refused every company over one file, and the portal then offered `generic` alone with
688
+ // no error, so a client's clearances looked deleted. So `tolerant` isolates each file: a company that fails is
689
+ // left out, its reason is kept on the returned map as `unreadable`, and asking for it by key refuses with that
690
+ // reason (resolveProfile). `generic` is never tolerated: it is the fallback every unprofiled job rates under,
691
+ // and replacing a deployment's own with the bundled one would change every such answer without a word.
692
+ function readProfilesLayer(dir, { tolerant = false } = {}) {
683
693
  const profiles = new Map();
694
+ const unreadable = [];
684
695
  for (const f of readdirSync(dir).filter((n) => n.endsWith(".json")).sort()) {
685
696
  const key = f.replace(/\.json$/, "");
686
- let p;
687
- try { p = JSON.parse(readFileSync(join(dir, f), "utf8")); }
688
- catch (e) { throw new Error(`profiles/${f}: unparseable JSON (${e.message})`); }
689
- validateProfileShape(key, p);
690
- // Context pack (Phase 1): a sibling `<key>.context.md`, attached AFTER validateProfileShape so the
691
- // deny-unknown-key gate (which governs the JSON shape) never sees it. Optional — absent ⇒ no pack.
692
- // Validated at load (rule-shape + size budget) so a bad pack fails loudly here, like riskAppetite (F8).
693
- const packPath = join(dir, CONTEXT_PACK_FILE(key));
694
- const contextPack = existsSync(packPath) ? readFileSync(packPath, "utf8").trim() : "";
695
- if (contextPack) assertContextPackShape(contextPack, `profiles/${CONTEXT_PACK_FILE(key)}`);
696
- profiles.set(key, { key, ...p, ...(contextPack ? { contextPack } : {}) });
697
+ try {
698
+ let p;
699
+ try { p = JSON.parse(readFileSync(join(dir, f), "utf8")); }
700
+ catch (e) { throw new Error(`profiles/${f}: unparseable JSON (${e.message})`); }
701
+ validateProfileShape(key, p);
702
+ // Context pack (Phase 1): a sibling `<key>.context.md`, attached AFTER validateProfileShape so the
703
+ // deny-unknown-key gate (which governs the JSON shape) never sees it. Optional — absent ⇒ no pack.
704
+ // Validated at load (rule-shape + size budget) so a bad pack fails loudly here, like riskAppetite (F8).
705
+ const packPath = join(dir, CONTEXT_PACK_FILE(key));
706
+ const contextPack = existsSync(packPath) ? readFileSync(packPath, "utf8").trim() : "";
707
+ if (contextPack) assertContextPackShape(contextPack, `profiles/${CONTEXT_PACK_FILE(key)}`);
708
+ profiles.set(key, { key, ...p, ...(contextPack ? { contextPack } : {}) });
709
+ } catch (e) {
710
+ if (!tolerant || key === "generic") throw e;
711
+ unreadable.push({ key, file: f, reason: String(e?.message ?? e) });
712
+ }
697
713
  }
714
+ Object.defineProperty(profiles, "unreadable", { value: unreadable, enumerable: false });
698
715
  return profiles;
699
716
  }
700
717
 
718
+ /** The companies a roster left out because their file would not load: `[{ key, file, reason }]`. */
719
+ export const unreadableProfiles = (profiles) => (Array.isArray(profiles?.unreadable) ? profiles.unreadable : []);
720
+
701
721
  /** Load every profiles/<key>.json → Map(key → profile), OVERLAY OVER BASE. Hard-fails loudly
702
722
  * on a generic.json missing from BOTH layers (the universal fallback — its absence would mis-profile
703
723
  * EVERY job), on a configured-but-unreadable overlay, and on any matchDomains overlap in the merged
@@ -746,7 +766,7 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
746
766
  // the universal fallback the module REQUIRES by name, the thing every unprofiled job resolves to. That
747
767
  // is the one file whose absence makes an empty store a refusal, so that is the only one that falls
748
768
  // through. Everything else in a deployment's roster is the deployment's own.
749
- const profiles = overlay ? readProfilesLayer(overlay) : readProfilesLayer(baseDir);
769
+ const profiles = overlay ? readProfilesLayer(overlay, { tolerant: true }) : readProfilesLayer(baseDir);
750
770
 
751
771
  // ── A TEST FIXTURE IS PRESENT AND NEVER OFFERED ─────────────────────────────────────────────────
752
772
  //
@@ -855,6 +875,15 @@ export function loadProfiles({ dir, force = false, includeTestFixtures, includeD
855
875
  export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
856
876
  const key = String(job?.profileKey ?? "").trim();
857
877
  if (key && profiles.has(key)) return profiles.get(key);
878
+ // A company whose file would not load is not an unknown company: say which, and why.
879
+ const unread = unreadableProfiles(profiles);
880
+ const failed = key ? unread.find((u) => u.key === key) : null;
881
+ if (failed) {
882
+ const err = new Error(`profile_unreadable:${key} — ${failed.reason}. The company exists but its profile could not be read, `
883
+ + `so this run is refused rather than rated under another company's settings.`);
884
+ err.code = "profile_unreadable";
885
+ throw err;
886
+ }
858
887
  // A NAMED-but-unknown key is not a graceful-degradation case, it is a roster mismatch — the two
859
888
  // sides disagree about which config store is real, and falling back to `generic` silently strips
860
889
  // the client's platforms, their self-exclusion seed and the framework that RATES the matter. That
@@ -875,6 +904,15 @@ export function resolveProfile(job, { profiles = loadProfiles() } = {}) {
875
904
  if (dom === dl || dom.endsWith(`.${dl}`)) return p;
876
905
  }
877
906
  }
907
+ // WITH A COMPANY UNREAD, "no company matched" is not known: the unread one's domains are not in hand, and
908
+ // this job may be its. Falling to `generic` here is the silent wrong-company deliverable, so refuse.
909
+ if (unread.length) {
910
+ const err = new Error(`profile_roster_incomplete — ${unread.length} company profile(s) could not be read `
911
+ + `(${unread.map((u) => u.file).join(", ")}), so whether this job belongs to one of them cannot be decided. `
912
+ + `Name the company on the job, or fix the file.`);
913
+ err.code = "profile_roster_incomplete";
914
+ throw err;
915
+ }
878
916
  return profiles.get("generic");
879
917
  }
880
918
 
@@ -1,6 +1,6 @@
1
1
  // SPDX-License-Identifier: AGPL-3.0-only
2
2
  // Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
3
- // progress.mjs — live run status for the prelim-search driver.
3
+ // progress.mjs — live run status for the clearance-search driver.
4
4
  //
5
5
  // Two artifacts, both written by the driver into the FORWARDING agent's own workspace
6
6
  // (so the agent's sandboxed read tool can see them — agents can't exec, so on-demand status is a
@@ -14,7 +14,7 @@
14
14
  // blindly incremented — so the resumable pipeline can re-drive a run without corrupting either file.
15
15
 
16
16
  import { readFileSync, writeFileSync, renameSync, unlinkSync, readdirSync, statSync, existsSync } from "node:fs";
17
- import { join, dirname } from "node:path";
17
+ import { join, dirname } from "node:path"; import { STUDIO_SEGMENT_RE } from "../shared/pre-rename-spellings.mjs";
18
18
  import { DRIVER_DIR } from "../shared/driver-dir.mjs"; //
19
19
  import { config } from "./driver.config.mjs";
20
20
  import { batchMarkName } from "./mark-name.mjs";
@@ -29,7 +29,7 @@ import { engineCommit, engineCommitSource } from "./engine-build.mjs"; // —
29
29
  // driver's execution units (fan-out register axes, skeptic-escalation re-runs, corrective re-synthesis,
30
30
  // two refutation passes) onto a clean forward-only sequence, so the displayed step never jumps backward.
31
31
  export const DISPLAY_STEPS = [
32
- "Framing the matter", // 1 matter-frame, prelim-variants
32
+ "Framing the matter", // 1 matter-frame, clearance-variants
33
33
  "Register sweeps", // 2 common-law + register-unit:* (fan-out + escalation re-runs collapse here)
34
34
  "Placement & digest", // 3 placement-inquiry, register-digest (+ re-digest)
35
35
  "Skeptic review", // 4 skeptic
@@ -48,7 +48,7 @@ export const DISPLAY_STEPS = [
48
48
  // UNLABELLED GAP on the stepper the client watches — the run looks stalled while it is working. Three
49
49
  // stages were sitting in that state (blind-frame, frame-diff, doubt-closure); each now says so by name.
50
50
  export const STAGE_TO_STEP = {
51
- "matter-frame": 0, "prelim-variants": 0,
51
+ "matter-frame": 0, "clearance-variants": 0,
52
52
  "common-law": 1, "common-law-half": 1, "register-unit": 1,
53
53
  "placement-inquiry": 2, "register-digest": 2,
54
54
  skeptic: 3,
@@ -398,11 +398,21 @@ export function seedRunStatus(ctx, { resume = false } = {}) {
398
398
  }, null, { critical: true }); // door B — the clearance lane's identity seed, same rule
399
399
  }
400
400
 
401
- // Convenience: advance the run to the step for `rawStageKey` (no-op for unmapped keys) then refresh STATUS.md.
401
+ // Record that the run is at `rawStageKey`: advance the displayed step where the stage has one, and say
402
+ // what the run is doing either way, then refresh STATUS.md.
403
+ //
404
+ // AN UNMAPPED STAGE MOVES NO STEP AND IS STILL SOMETHING THE RUN IS DOING. This returned early for one,
405
+ // so it wrote no `lastStage` at all — and `lastStage` is what status-snapshot publishes as "what the run
406
+ // is actually doing". Three stages have no display step ON PURPOSE (STAGE_NO_STEP names each and why),
407
+ // so for the whole of any of them every surface went on naming the PREVIOUS stage, which is the same
408
+ // defect as a stale step wearing a different field. The step fields are still withheld — that part of
409
+ // the early return was right, and an unmapped stage must never touch the displayed step.
402
410
  export function recordTransition(ctx, rawStageKey) {
403
411
  const step = stepForStage(rawStageKey);
404
- if (!step) return;
405
- writeRunStatus(ctx, { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total, lastStage: rawStageKey });
412
+ writeRunStatus(ctx, {
413
+ ...(step ? { stepIndex: step.index, stepLabel: step.label, stepN: step.n, stepTotal: step.total } : {}),
414
+ lastStage: rawStageKey,
415
+ });
406
416
  rollupStatus(ctx?.run?.studioRoot);
407
417
  }
408
418
 
@@ -424,7 +434,7 @@ function findStatusFiles(root, depth, acc) {
424
434
  }
425
435
 
426
436
  function agentFromStudioRoot(studioRoot) {
427
- const m = new RegExp(`${config.workspacePrefixRe}([^/]+)/studio/prelim-search/?$`).exec(studioRoot ?? "");
437
+ const m = new RegExp(`${config.workspacePrefixRe}([^/]+)/studio/${STUDIO_SEGMENT_RE}/?$`).exec(studioRoot ?? "");
428
438
  return m ? m[1] : "";
429
439
  }
430
440
 
@@ -14,9 +14,9 @@
14
14
  //
15
15
  // This module is read-only and defensive: a missing/unreadable ledger or a torn last line (two gateway
16
16
  // turns appending concurrently) returns zeros / skips that line — it never throws. The driver calls it at
17
- // publish time to attribute THIS run's calls by the session-key prefix `prelim-<slug>-<codename>-`.
17
+ // publish time to attribute THIS run's calls by the session-key prefix `clearance-<slug>-<codename>-`.
18
18
 
19
- import { readFileSync, existsSync } from "node:fs";
19
+ import { readFileSync, existsSync } from "node:fs"; import { runPrefixSpellings } from "../shared/pre-rename-spellings.mjs";
20
20
  // Aliased: three functions below take a parameter literally named `ledgerPath`, and an unaliased
21
21
  // import would be shadowed by it inside its own default-value expression (a TDZ ReferenceError
22
22
  // at the first call, not at load).
@@ -46,7 +46,7 @@ export const DEFAULT_LEDGER_PATH = resolveLedgerPath("call");
46
46
  export const KINDS = ["search", "record_fetch", "image", "phoneme", "batch_screen", "enumerate", "execute_plan", "propose_supplemental"];
47
47
 
48
48
  // The gateway namespaces the driver's --session-key as `agent:<agentId>:<key>` before it reaches the
49
- // plugin (confirmed on the first live run: sessionKey = `agent:clawdi:prelim-<slug>-<codename>-…`). Strip
49
+ // plugin (confirmed on the first live run: sessionKey = `agent:clawdi:clearance-<slug>-<codename>-…`). Strip
50
50
  // that leading `agent:<id>:` namespace so the run prefix anchors at the real start of the caller's key —
51
51
  // otherwise a bare startsWith("clearotron-…") matches nothing.
52
52
  function stripGatewayNs(s) {
@@ -54,11 +54,11 @@ function stripGatewayNs(s) {
54
54
  }
55
55
 
56
56
  // Does this ledger row belong to the run identified by `runPrefix`? The driver's --session-key is
57
- // `prelim-<slug>-<codename>-<stage><axis>` (+ optional `-rerunN`), so a prefix match catches every stage +
57
+ // `clearance-<slug>-<codename>-<stage><axis>` (+ optional `-rerunN`), so a prefix match catches every stage +
58
58
  // axis + retry of the run. We check sessionKey (carries the key) and, defensively, sessionId.
59
59
  function rowMatchesRun(row, runPrefix) {
60
- return stripGatewayNs(row.sessionKey).startsWith(runPrefix)
61
- || stripGatewayNs(row.sessionId).startsWith(runPrefix);
60
+ return runPrefixSpellings(runPrefix).some((rp) => stripGatewayNs(row.sessionKey).startsWith(rp)
61
+ || stripGatewayNs(row.sessionId).startsWith(rp)); // either spelling: a run resumed across the rename
62
62
  }
63
63
 
64
64
  // ── band-truth gate (2026-07-14, teal-foundry): count the ledger rows attributed to ONE unit lane ──────
@@ -146,7 +146,7 @@ function emptyTally() {
146
146
  /**
147
147
  * Tally every ledger line whose gateway id starts with `runPrefix`.
148
148
  * @param {string} ledgerPath path to the JSONL ledger
149
- * @param {string} runPrefix e.g. `prelim-acme-bluejay-` (note the trailing hyphen)
149
+ * @param {string} runPrefix e.g. `clearance-acme-bluejay-` (note the trailing hyphen)
150
150
  * @returns {object} the tally (see emptyTally) — never throws
151
151
  */
152
152
  export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix) {
@@ -208,7 +208,7 @@ export function tallyRegisterCalls(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix)
208
208
  * (missing/unreadable ledger or a torn last line ⇒ skips that line, never throws). A cache_hit record_fetch
209
209
  * still counts — the URI WAS fetched this run, which is exactly what the gate asks.
210
210
  * @param {string} ledgerPath path to the JSONL ledger
211
- * @param {string} runPrefix e.g. `prelim-acme-bluejay-` (note the trailing hyphen)
211
+ * @param {string} runPrefix e.g. `clearance-acme-bluejay-` (note the trailing hyphen)
212
212
  * @returns {Set<string>} the set of fetched record URIs
213
213
  */
214
214
  export function fetchedRecordUris(ledgerPath = DEFAULT_LEDGER_PATH, runPrefix) {
@@ -16,12 +16,14 @@ import { buildAudit } from './xlsx.mjs';
16
16
  import { parseFindingsJson, parseFindingsJsonLenient, deriveDisplayVerdict, joinFindingToBlock, CLIENT_TIER_BY_COMPOSITE, projectCoverageJudgment } from '../findings-model.mjs';
17
17
  import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.mjs'; // — and why an absence did not close
18
18
  import { clearanceReportData } from './report-data.mjs';
19
- import { searchDepthRecord } from './search-depth.mjs'; // how much was read to reach the answer, as counts and tokens
19
+ import { searchDepthRecord, planTerritoriesOf } from './search-depth.mjs'; import { bandRecords } from '../named-band.mjs'; // how much was read to reach the answer, as counts and tokens
20
20
  import { parseFrameworkManifest } from '../framework.mjs';
21
- import { rollupTokens } from '../tokens.mjs';
21
+ import { rollupTokens, servedModels } from '../tokens.mjs';
22
22
  import { reportIdentityFor, productCoverageNote, isRegisterOnly } from '../search-policy.mjs';
23
23
  import { readRecordArtifacts, bindFindingsToRecords, joinEvidenceStatus } from '../registry-fidelity.mjs';
24
24
  import { deliveryFlagLines } from '../predelivery-lint.mjs';
25
+ import { unrenderableConditions } from '../terminal-clamp.mjs'; // — the conditions that reach no client surface
26
+ import { runLog } from '../log.mjs'; // — one line in the run record when one does
25
27
  import { PROVIDERS, config } from '../driver.config.mjs';
26
28
  import { declaredRecordOrigins } from '../record-origins.mjs';
27
29
  import { NEUTRAL_DELIVERY, loadProfiles } from '../profiles.mjs';
@@ -791,6 +793,85 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
791
793
  // the narrative, never in a status line). Best-effort; legacy runs have no sidecar.
792
794
  let verdictInfo = null;
793
795
  try { verdictInfo = JSON.parse(readFileSync(driverDir(runDir, 'verdict.json'), 'utf8')); } catch { /* legacy */ }
796
+ // ── THE CONDITIONS THAT REACH NO PAGE, RECORDED RATHER THAN LOST ────────────────────────────────
797
+ //
798
+ // A condition whose reader-facing sentence was never stored and cannot be composed from the run
799
+ // record's own counts is dropped instead of printed in the engine's words. That is right for the
800
+ // reader and it is a silent loss for everyone else, so the drop reports itself twice: an ordinary
801
+ // row on the workbook's gaps sheet, and one line in the run record here.
802
+ //
803
+ // THE WORDS ARE THE RUN RECORD'S OWN. The reason is what the clamp site wrote; the state word and the
804
+ // column headings are the gaps sheet's. Nothing on this path composes a sentence.
805
+ //
806
+ // A FRESH RUN NEVER REACHES THIS. Every clamp site stores its clause, so the list is empty and the
807
+ // sheet grows no row and the log gains no line. It bites on a republished archive, which is exactly
808
+ // where the operator has no other way to learn the page is short of a point.
809
+ const droppedConditions = unrenderableConditions(verdictInfo || {}).map((reason) => ({
810
+ area: 'Conditions', state: 'open', note: reason,
811
+ }));
812
+ // Best-effort, in the house pattern: a publish never fails for want of a log line, and a republish
813
+ // from a directory this process cannot write is still a publish.
814
+ if (droppedConditions.length && runDir) {
815
+ try {
816
+ runLog(runDir, { event: 'client-condition-dropped', n: droppedConditions.length,
817
+ reasons: droppedConditions.map((c) => c.note) });
818
+ } catch { /* the workbook row is the record that matters; this line is the second copy */ }
819
+ }
820
+ // ── THE CHECKS THE RUN DECIDED ON AND DID NOT MAKE ─────────────────────────────────────────────
821
+ //
822
+ // The recall net mints a probe per remembered conflict and a probe per owner behind one, then
823
+ // dispatches at most five owner probes. The excess is recorded in the run's own receipt and nothing
824
+ // downstream carried it to a reader, so a search that decided on nineteen ownership checks, made five
825
+ // and said nothing about the other fourteen read as a search that made the checks it wanted.
826
+ //
827
+ // The cap is not the defect: it is deliberate and the rows it drops are ranked material-first, so the
828
+ // five that run are the five that matter most. What was missing is the disclosure, and it lands where
829
+ // the 2026-09-17 ruling put the same class of fact — an ordinary row on the workbook's gaps sheet and
830
+ // one line in the run record. Nothing reaches the report page: the approved boards draw a forward
831
+ // decision and an open question back to the client, and a check nobody made is neither.
832
+ //
833
+ // THE WORDS ARE THE RECEIPT'S OWN. The party and the probe id are read from it verbatim; "over the
834
+ // cap" and "never dispatched" are the vocabulary the ask ledger already ships for these same rows.
835
+ // Nothing here composes a sentence, and the note carries no seam, so the gaps sheet's own splitter
836
+ // leaves "What was done" empty — which is the fact: nothing was done.
837
+ // READ THROUGH THE DECLARED HELPER, three states and not two. An absent receipt is a run whose recall
838
+ // net minted nothing — env-gated off, or a matter with no remembered conflict — and there is nothing
839
+ // to disclose. A DAMAGED one is a different fact: the probes may have overflowed and this publish
840
+ // cannot tell, so it says so in the run record instead of shipping the same empty sheet an
841
+ // everything-dispatched run ships. Collapsing those two is the defect publish-inputs.mjs exists for.
842
+ //
843
+ // NOT PUSHED ONTO `inputsAbsent`. That list rides meta.json's clientGate record, whose own note two
844
+ // hundred lines down is that it stays undefined when every store was found so a complete run's meta
845
+ // is byte-identical to one written before the list existed. Most runs have no recall receipt, so
846
+ // adding this store to it would change the meta of every archived run on re-render for a store that
847
+ // feeds a workbook row and not the gate -- the same reason the other presentation-only stores are
848
+ // not on it either.
849
+ // `runDir ?? dirname(reportMd)`, the same base its neighbours take and not a defensive flourish:
850
+ // publishReport's `runDir` is OPTIONAL, and a bare `readStore(runDir, …)` throws TypeError on an
851
+ // undefined base rather than reporting an absent store. Caught in CI by the graceful-stop runner arm,
852
+ // which publishes without one: the throw left publishReport at `published → null` and the runner
853
+ // exited 1. Reading from the report's own directory is also the right answer for a republish.
854
+ const recallStore = readStore(runDir ?? dirname(reportMd), '_driver/register-recall.json');
855
+ const undispatchedProbes = (recallStore.value?.overflow ?? [])
856
+ .filter((o) => o && (o.term || o.qid))
857
+ .map((o) => ({
858
+ area: String(o.term ?? o.qid),
859
+ state: 'not-searched',
860
+ note: `over the cap, never dispatched (${String(o.qid ?? 'no probe id recorded')})`,
861
+ }));
862
+ if (recallStore.state === 'damaged' && runDir) {
863
+ try {
864
+ runLog(runDir, { event: 'probe-over-cap-unreadable', store: recallStore.name, error: recallStore.error });
865
+ } catch { /* best-effort, as below */ }
866
+ }
867
+ // Best-effort, as above: a publish never fails for want of a log line.
868
+ if (undispatchedProbes.length && runDir) {
869
+ try {
870
+ runLog(runDir, { event: 'probe-over-cap-undispatched', n: undispatchedProbes.length,
871
+ parties: undispatchedProbes.map((p) => p.area) });
872
+ } catch { /* the workbook row is the record that matters; this line is the second copy */ }
873
+ }
874
+
794
875
  // doc 50 — the run's FROZEN framework manifest (band vocabulary). Present on band-doctrine runs;
795
876
  // absent on every archived run (they render byte-identically on the legacy paths).
796
877
  let framework = null;
@@ -819,11 +900,25 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
819
900
  // two must not render the same way. Null on archived runs ⇒ render keeps the prose fallback.
820
901
  let scopeBasis = null;
821
902
  let searchedJurisdictions = [];
903
+ // WHAT WAS SEARCHED IS THE PLAN'S ANSWER, NOT THE ARCHIVE'S, and it is carried separately from the
904
+ // header's list below because the two are used for different things and one of them is deliberately
905
+ // emptied. The coverage presentation asks which territories the run searched and which it could not
906
+ // reach; the plan compiles both — entries carry the regions it will query, `deferred_coverage` carries
907
+ // the ones this provider does not cover with the reason each. A register that archives no records
908
+ // still has both, which is the whole point: keyed off the record store, a provider that keeps nothing
909
+ // looked like a provider nobody asked.
910
+ //
911
+ // THREE-VALUED like the record listing above it: null means there is no plan to read, so the run
912
+ // cannot say — an archived or legacy run — and it must not be confused with a plan that named nothing.
913
+ let planTerritories = null;
822
914
  try {
823
915
  const plan = JSON.parse(readFileSync(driverDir(runDir ?? dirname(reportMd), 'register-plan.json'), 'utf8'));
824
916
  if (plan?.scope_basis === 'worldwide') scopeBasis = 'worldwide';
825
917
  searchedJurisdictions = [...new Set((plan?.entries ?? []).flatMap((e) => Array.isArray(e.regions) ? e.regions : []))];
826
918
  if (!searchedJurisdictions.length && Array.isArray(plan?.regions)) searchedJurisdictions = [...new Set(plan.regions)];
919
+ // Derived here, BEFORE the worldwide clearing below — that clearing exists for the header and would
920
+ // otherwise empty this on exactly the runs that cover the most ground.
921
+ planTerritories = planTerritoriesOf(plan);
827
922
  } catch { /* no plan sidecar — try the instructed scope */ }
828
923
  if (!searchedJurisdictions.length) {
829
924
  try {
@@ -902,7 +997,14 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
902
997
  const reviewReceipts = {
903
998
  // `family` rides along for deliveryFlagLines' fallback: a check whose id is not in the projection
904
999
  // table still projects to its family's sentence rather than to anything the engine wrote.
905
- lint: (lintSink?.checks ?? []).filter(c => !c.pass).map(c => ({ id: c.id ?? '', family: c.family ?? '', detail: c.detail ?? '' })),
1000
+ // ONE CHECK IS DELIBERATELY NOT PROJECTED. `client-condition-dropped` has no sentence of its own
1001
+ // in the projection table, so it fell through to its family's — "the delivered outcome and the
1002
+ // report's own text do not agree" — which describes a contradiction. This is an omission: the
1003
+ // outcome and the text agree perfectly and both are short of a point. It says what happened on the
1004
+ // gaps sheet and in the run record now, in the run record's own words, so the borrowed sentence
1005
+ // comes off rather than being replaced by a better one (ruled 2026-09-17).
1006
+ lint: (lintSink?.checks ?? []).filter(c => !c.pass && c.id !== 'client-condition-dropped')
1007
+ .map(c => ({ id: c.id ?? '', family: c.family ?? '', detail: c.detail ?? '' })),
906
1008
  engagement: integritySink?.engagement ?? null,
907
1009
  registryCorrections: Array.isArray(integritySink?.registryCorrections)
908
1010
  ? integritySink.registryCorrections.filter((c) => c && c.from != null && c.to != null) : [],
@@ -1032,7 +1134,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1032
1134
  // the same rule (the workbook's own BANNED gate had already started firing on the raw detail —
1033
1135
  // advisory, so CI stayed green). reviewReceipts.lint keeps its raw detail for the internal
1034
1136
  // readers above (fetchState reads registry-record-coverage's URIs out of it).
1035
- counts = await buildAudit({ findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
1137
+ counts = await buildAudit({ droppedConditions, undispatchedProbes, findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
1036
1138
  grpRead(join(poolRunDir, auditFile), 0o640);
1037
1139
  if (counts?.gateViolations?.length) console.warn(`[audit-workbook] advisory: ${counts.gateViolations.join(' | ')}`);
1038
1140
  } catch (e) {
@@ -1053,6 +1155,11 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1053
1155
  // rather than wire the overlay in: client names saturate privileged report prose — a partial blur is a
1054
1156
  // worse demo than no toggle.
1055
1157
  const reportNav = siteNav(poolRoot, 'report', null, '../', { anon: false });
1158
+ // The models that served this run, named as a client may read them (tokens.mjs servedModels). The closing
1159
+ // line of the report's scope section, the data file and the meta below all take this one read; a read
1160
+ // that throws records nothing rather than failing the publish.
1161
+ let served = null;
1162
+ try { served = runDir ? servedModels(runDir) : null; } catch { served = null; }
1056
1163
  // `demoData` is resolved above the report.md write — one answer, every surface.
1057
1164
  // ── HOW MUCH WAS READ TO REACH THE ANSWER ──────────────────────────────────────────────────────
1058
1165
  // Derived here rather than in a stage so a REPUBLISH of an archived run picks the fields up with no
@@ -1060,6 +1167,35 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1060
1167
  // pattern — a run with no grid or no case-law layer still gets its register counts, and the absent
1061
1168
  // ones report themselves as zero or `not-in-scope` rather than as a gap nobody can see.
1062
1169
  let searchDepth = null;
1170
+ // HOW DEEP THE LOCAL-LANGUAGE INVESTIGATION WENT, and it is derived by the one author of that
1171
+ // question rather than re-read here. The import is LAZY and gated on the run's own sidecar existing,
1172
+ // which keeps the property the pipeline's own fold keeps: a plain clearance never loads the jx
1173
+ // machinery at all. No sidecar means the component never ran, which the fold reports as not-in-scope.
1174
+ let laneDepthVerdicts = null;
1175
+ try {
1176
+ const jxSidecar = driverDir(runDir ?? dirname(reportMd), 'jx-lanes.json');
1177
+ if (existsSync(jxSidecar)) {
1178
+ const sidecar = JSON.parse(readFileSync(jxSidecar, 'utf8'));
1179
+ const { laneDepthOfRun } = await import('../jx.mjs');
1180
+ let units = null;
1181
+ try { units = JSON.parse(readFileSync(driverDir(runDir ?? dirname(reportMd), 'jx/units.json'), 'utf8')); } catch { /* the statement handles an absent units file */ }
1182
+ // THE RUN ALREADY ANSWERED THIS, AND THE STAMPED ANSWER IS THE ONE THAT COUNTS.
1183
+ //
1184
+ // `deriveJxSliceStatement` writes `fold.depth` at delivery, from the run's OWN environment, for the
1185
+ // reason the seam exists at all: the arms are environment, so a verdict derived later can only speak
1186
+ // for the box it is derived on. Publishing re-derived it here — a second author for a fact the run
1187
+ // had already stated — and the two disagreed on a delivered client report.
1188
+ //
1189
+ // The disagreement was not subtle. This call passed `deriveJxSliceStatement`'s whole return,
1190
+ // `{executes, slices}`, where the function reads `slices.candidates` — so the lookup found nothing,
1191
+ // every lane's `ran` came back null whatever it had really done, and a full-country JP run that
1192
+ // searched in Japanese printed "Local-language investigation · Not run this run". The stamped
1193
+ // verdict on the same run's sidecar said `ran: "candidates"`, correctly.
1194
+ //
1195
+ // So: read what the run stated, and derive only for a run delivered before the stamp existed.
1196
+ laneDepthVerdicts = laneDepthOfRun({ sidecar, units });
1197
+ }
1198
+ } catch { /* an unreadable sidecar reports as not-in-scope rather than failing a publish */ }
1063
1199
  try {
1064
1200
  const runBase = runDir ?? dirname(reportMd);
1065
1201
  const recDir = join(runBase, '_records');
@@ -1068,15 +1204,22 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1068
1204
  searchDepth = searchDepthRecord({
1069
1205
  auditMd: (auditMd && existsSync(auditMd)) ? rdText(auditMd) : '',
1070
1206
  recordIndex: recordsByUri ?? {},
1071
- recordFileNames: existsSync(recDir) ? readdirSync(recDir) : [],
1207
+ // null, NOT []: a run whose provider archives no records has no `_records/` at all, and an empty
1208
+ // array is a register that was searched and returned nothing. They are different facts and the
1209
+ // page says different things about them, so the distinction this line already computes is kept
1210
+ // rather than thrown away one character later.
1211
+ recordFileNames: existsSync(recDir) ? readdirSync(recDir) : null,
1212
+ // …and where there is no archive, what the register returned: the band's own record ids.
1213
+ bandRecordIds: existsSync(recDir) ? null : (() => { try { const b = rdJson(join(runBase, 'register-named-band.json')); return b ? bandRecords(b).map((r) => r?.record_id).filter(Boolean) : null; } catch { return null; } })(),
1072
1214
  commonLawGrid: rdJson(join(runBase, 'common-law-grid.json')),
1073
1215
  caseLawText: rdText(join(dirname(reportMd), 'case-law-findings.md')),
1074
1216
  registerPlan: rdJson(driverDir(runBase, 'register-plan.json')),
1217
+ laneDepthVerdicts,
1075
1218
  });
1076
1219
  writeRO('search-depth.json', JSON.stringify(searchDepth, null, 2));
1077
1220
  } catch { /* the depth record is additive — a publish never fails for want of it */ }
1078
1221
 
1079
- writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
1222
+ writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, servedModels: served, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, planTerritories, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
1080
1223
  // ONE report (spec 2026-07-30 §5): report.client.html is no longer written. The knockout lane's own
1081
1224
  // collapse note is the precedent: "two renderings of one run is how the wrong link gets sent". The
1082
1225
  // client host serves the same report.html through the portal's readReport() (cleaning built in) — its
@@ -1150,7 +1293,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1150
1293
  // is the LAST resort (a level today's registry no longer knows). meta.json below keeps the frozen
1151
1294
  // stamp BY DESIGN — its readers re-derive.
1152
1295
  auditFile, searchLevel: searchPolicy?.level ?? null, stageLabel: stageLabel ?? searchPolicy?.stageLabel ?? null,
1153
- engineCommit: engineCommit(),
1296
+ engineCommit: engineCommit(), servedModels: served,
1154
1297
  framework, verdictInfo, findings, coverage, contextNotes, markAssessment, fourAnswers, askAnswers,
1155
1298
  actions: actionsRegister, jurisdiction, searchedJurisdictions, scopeBasis, caption: fm.overall_caption ?? null,
1156
1299
  });
@@ -1175,6 +1318,9 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
1175
1318
  issuedAt,
1176
1319
  // WHICH BUILD produced this. null off a git checkout — a provenance stamp never fails a publish.
1177
1320
  engineCommit: engineCommit(),
1321
+ // The models that served the run, named as a client may read them (tokens.mjs servedModels). Absent when
1322
+ // nothing was read, so a meta from before the record keeps its shape; [] when turns ran and named none a client may read.
1323
+ servedModels: served ?? undefined,
1178
1324
  kind: 'clearance', recordLinks: officeLinks?.tally ?? undefined, // per office: linked, or cited by number and why; only where the register has no record pages
1179
1325
  searchLevel: searchPolicy?.level ?? undefined,
1180
1326
  // Display-only face of the level ("Depth 4"), frozen alongside it so the list can show which reads
@@ -1445,7 +1591,7 @@ export function composeEmailBody(reportMdPath, url, auditFile, productName = nul
1445
1591
  const FONT = "font-family:Calibri,'Segoe UI',Arial,sans-serif";
1446
1592
 
1447
1593
  // 5-tier reporting-template risk scheme — the whole left "NAME / RISK RATING" cell takes the bg colour.
1448
- // Matches skills/prelim-search/templates/search-request-form.html + knockout-searches/template-formatting.md.
1594
+ // Matches skills/clearance-search/templates/search-request-form.html + knockout-searches/template-formatting.md.
1449
1595
  // `txt` = a readable text-colour version of the tier (for the EXECUTIVE SUMMARY risk phrase, where a bright
1450
1596
  // fill like yellow/red is unreadable as text). Order matters: VERY HIGH before HIGH.
1451
1597
  export const RISK_TIERS = [