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
@@ -51,7 +51,7 @@
51
51
  // once per snapshot, by a writer that is already async. So that import is DYNAMIC, and search-policy
52
52
  // never pulls the plan layer in to answer a question about a product menu.
53
53
  import { PROMPT_TERRITORIES } from "./compose-read.mjs";
54
- import { territoryTier } from "./territory-tiers.mjs";
54
+ import { territoryTier, territoryKey } from "./territory-tiers.mjs";
55
55
 
56
56
  /**
57
57
  * The composer display names this register can actually search.
@@ -157,3 +157,163 @@ export function registerCoverageCause(geography, territories, all = PROMPT_TERRI
157
157
  if (rule(territories, all)) return null;
158
158
  return geography === "worldwide, and nothing else" ? "register-not-worldwide" : "register-coverage";
159
159
  }
160
+
161
+ // ── the REQUEST half ────────────────────────────────────────────────────────────────────────────────
162
+ //
163
+ // The product half above asks whether a PRODUCT can be ordered at all. This asks whether THIS request's
164
+ // territories can be searched. Two questions, two rulings, deliberately not folded together:
165
+ //
166
+ // · the PRODUCT-level refusal was retired on 2026-08-31 and stays retired — a worldwide search is
167
+ // orderable on a partial register and the gap is DISCLOSED in the report.
168
+ // `coverage-is-disclosed-never-refused.test.mjs` pins that, and this function must never make it red.
169
+ // · the REQUEST-level refusal was ruled on 2026-09-17: a requester who NAMES a territory the wired
170
+ // register cannot search is told so BEFORE the run. That gap is not disclosable — there is no result
171
+ // to caveat, only a territory the client asked about and would never hear another word on.
172
+ //
173
+ // AN EMPTY LIST IS THE "NAMES NO TERRITORY" STATE, BY CONSTRUCTION, and that is why no mode string is
174
+ // read here. `effective-scope.mjs`'s ladder answers `[]` for a worldwide stamp — worldwide accepts no
175
+ // narrowing, so the account's defaults are not consulted — and `[]` again when no layer set a territory.
176
+ // So "a search that names no territory always runs" falls out of the empty set passing. A saved account
177
+ // default that DID reach the ladder is in the list and is judged with the rest, which is the ruling of
178
+ // 2026-09-17 on that question: a default counts as named once it reaches the engine.
179
+ //
180
+ // AND A NAME THE SNAPSHOT CANNOT SPEAK TO FAILS OPEN. `registerTerritories` is scoped to the composer's
181
+ // 37 display names. The other doors are not: `normalizeTerritory` passes any two-letter token through, so
182
+ // the CLI and start_run can name `VN`, which is inside clarivate's own 186-office enum and outside those
183
+ // 37. A bare membership test would refuse a search this engine runs today. Only a territory the
184
+ // snapshot's vocabulary can SPEAK TO is judged, and everything else is the pipeline's to defer as now.
185
+
186
+ /** Composer display name for a canonical key, so a refusal says "China" whether the requester wrote
187
+ * "China", "cn" or "CN". Built per call from the same list the covered set is scoped to. */
188
+ function displayByKey(all) {
189
+ const out = new Map();
190
+ for (const name of all) out.set(territoryKey(name), name);
191
+ return out;
192
+ }
193
+
194
+ /**
195
+ * The territories this request NAMES that the wired register cannot search, as composer display names.
196
+ *
197
+ * @param territories the resolved territories — `effective-scope.mjs`'s ladder answer
198
+ * @param registerTerritories what `coveredTerritoryNames` returned: `[...]`, `null` (no declared
199
+ * restriction) or `undefined` (the snapshot does not say)
200
+ * @returns `[]` — nothing to refuse, INCLUDING on `null` and `undefined`, which fail open exactly as
201
+ * they do at every other layer. Never treat an empty return as "the check did not run".
202
+ *
203
+ * Both sides are compared through `territoryKey`, which is the identity function this tree already has
204
+ * for "what makes two spellings the same place" — it folds EM/EUTM/EUIPO to EU and UK to GB. That fold
205
+ * is the whole of requirement 2: an EU-covering register refusing a request that names "European Union"
206
+ * was the defect measured on 2026-09-17, and it cannot recur while both sides go through one authority.
207
+ */
208
+ export function uncoveredTerritories(territories, registerTerritories, all = PROMPT_TERRITORIES) {
209
+ if (registerTerritories === null || registerTerritories === undefined) return [];
210
+ const covered = new Set(registerTerritories.map(territoryKey));
211
+ const display = displayByKey(all);
212
+ const out = [];
213
+ const seen = new Set();
214
+ for (const t of territories ?? []) {
215
+ const key = territoryKey(t);
216
+ // Outside the snapshot's vocabulary ⇒ this register's covered list says nothing about it. Fail open.
217
+ if (!key || !display.has(key) || covered.has(key) || seen.has(key)) continue;
218
+ seen.add(key);
219
+ out.push(display.get(key));
220
+ }
221
+ return out;
222
+ }
223
+
224
+ // One-line join rather than an import: this module's header makes its static graph part of the design
225
+ // ("search-policy.mjs imports this file … a cycle is one careless import away"), and the only other
226
+ // spelling of this lives behind scope-facts.mjs, which is not a leaf.
227
+ const joinAnd = (parts) => (parts.length <= 1 ? parts.join("") : `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`);
228
+
229
+ /**
230
+ * The door's sentence for a request naming territories the wired register cannot search — or null.
231
+ *
232
+ * WRITTEN ONCE, FOR EVERY DOOR. Wording approved by the owner on 2026-09-17, including naming the wired
233
+ * register, which the composer's own design note otherwise forbids on screen. The remedy clause is his
234
+ * addition of the same day: a caller with no screen — start_run, the CLI — has to be able to act on this
235
+ * sentence alone, on the next call.
236
+ *
237
+ * IT POINTS BACK RATHER THAN REPEATING. The first draft named the territories twice ("remove China and
238
+ * Japan"); the owner ruled on the plural the same day — "remove them", no need to repeat the countries —
239
+ * and the singular follows the same reason, since it repeated its one country for no better cause. The
240
+ * sentence already names them, so a screenless caller still has every territory it must drop.
241
+ *
242
+ * @param registerLabel the register's display label. Absent on a snapshot written before it was carried,
243
+ * and the sentence simply does not name it then rather than naming a key.
244
+ */
245
+ export function registerReachRefusal(uncovered, registerLabel = null) {
246
+ if (!uncovered?.length) return null;
247
+ const names = joinAnd(uncovered);
248
+ const register = String(registerLabel ?? "").trim();
249
+ const where = register ? `${register}, the register configured here` : "the register configured here";
250
+ return `${names} ${uncovered.length === 1 ? "is" : "are"} not available with ${where}`
251
+ + ` — remove ${uncovered.length === 1 ? "it" : "them"} to run this search.`;
252
+ }
253
+
254
+ // ── WHAT THE FORM SHOULD OFFER: EVERY PLACE A REGISTER THIS PRODUCT SUPPORTS CAN SEARCH ────────────────
255
+ //
256
+ // The form offered 37 places with no rule behind them: Bulgaria and Greece, but not Denmark, Portugal,
257
+ // Vietnam or Colombia, while a worldwide search on the wider register already swept every one of its 186
258
+ // offices. The rule is the registers' own reach: a place is offered when at least one supported register
259
+ // provider can search it, decided by the SAME resolution the plan compiler runs (`resolveRegions`, one code
260
+ // at a time), so the offer cannot promise a place the compiler would defer everywhere. What THIS install's
261
+ // register does not reach is marked per deployment by `coveredTerritoryNames` and the door's refusal — the
262
+ // offer is the product's; the marking is the install's.
263
+ //
264
+ // A provider with no enumerable coverage (a global aggregator declaring `covered: null`) widens nothing:
265
+ // "everywhere" is not a list, and letting it in would offer every code the engine holds, including places
266
+ // no register answers for.
267
+ //
268
+ // NAMES ARE NEVER WRITTEN HERE. A place the form already offers keeps its shipped label; any other is named
269
+ // by the runtime's standard English region names (CLDR), the same data the territory vocabulary already
270
+ // resolves typed names against. A code with neither — the regional systems no label exists for — is left
271
+ // out and listed, never given a name composed in code. Provider extension codes (X-, ZZ) are not places.
272
+ //
273
+ // @returns {Promise<{ offered: {code: string, name: string}[], unnamed: string[] }>} regions first, then
274
+ // countries by name.
275
+ export async function searchableTerritories() {
276
+ const [{ KNOWN_JURISDICTION_CODES, canonicalJurisdictionCode }, { normalizeTerritory }, { PROVIDER_CAPABILITIES }, { resolveRegions }]
277
+ = await Promise.all([import("./jurisdiction-codes.mjs"), import("../providers/_shared/territory-codes.mjs"),
278
+ import("./register-capabilities.mjs"), import("./register-plan.mjs")]);
279
+ const enumerable = Object.values(PROVIDER_CAPABILITIES).filter((caps) => Array.isArray(caps?.offices?.covered));
280
+ const shippedName = new Map(LABELS_OFFERED_BEFORE_THE_RULE.map((n) => [canonicalJurisdictionCode(normalizeTerritory(n) ?? ""), n]));
281
+ let cldr = null;
282
+ try { cldr = new Intl.DisplayNames(["en"], { type: "region" }); } catch { cldr = null; }
283
+ const offered = [], unnamed = [];
284
+ const seen = new Set();
285
+ for (const raw of KNOWN_JURISDICTION_CODES) {
286
+ const code = canonicalJurisdictionCode(raw);
287
+ if (!code || seen.has(code) || /^(X.|ZZ)$/.test(code)) continue;
288
+ seen.add(code);
289
+ const searched = enumerable.some((caps) => {
290
+ const { regions, deferred } = resolveRegions([code], caps);
291
+ return deferred.length === 0 && regions.length > 0;
292
+ });
293
+ if (!searched) continue;
294
+ let name = shippedName.get(code) ?? null;
295
+ if (!name && cldr) { try { const n = cldr.of(code); if (n && n !== code && !/^unknown/i.test(n)) name = n; } catch { /* no name */ } }
296
+ if (!name) { unnamed.push(code); continue; }
297
+ offered.push({ code, name });
298
+ }
299
+ // Regions first, in the order the form already showed them; then countries by name.
300
+ const regional = new Set(["EU", "BX", "AP", "OA", "EA", "WO"]);
301
+ const shippedAt = (t) => { const i = LABELS_OFFERED_BEFORE_THE_RULE.indexOf(t.name); return i < 0 ? Infinity : i; };
302
+ offered.sort((a, b) => (regional.has(b.code) - regional.has(a.code))
303
+ || (regional.has(a.code) ? shippedAt(a) - shippedAt(b) : 0) || a.name.localeCompare(b.name, "en"));
304
+ return { offered, unnamed: unnamed.sort() };
305
+ }
306
+
307
+ // THE 37 LABELS THE FORM CARRIED BEFORE THIS RULE, frozen. They are shipped strings, so each place keeps
308
+ // the label a client already reads — "Hong Kong", "Macau", "Turkey", where the runtime's standard names
309
+ // differ — and the two regional systems whose only names are these. Frozen HERE rather than read from the
310
+ // form's live list, because that list is now minted FROM this function, and a rule that read its own
311
+ // output would lose its labels the first time the file was regenerated from scratch.
312
+ export const LABELS_OFFERED_BEFORE_THE_RULE = Object.freeze([
313
+ "European Union", "Benelux", "African Regional (ARIPO)",
314
+ "United States", "United Kingdom", "Ireland", "France", "Germany", "Spain", "Italy", "Netherlands",
315
+ "Switzerland", "Austria", "Sweden", "Norway", "Poland", "Bulgaria", "Greece", "Turkey", "Canada",
316
+ "Mexico", "Brazil", "Argentina", "China", "Hong Kong", "Taiwan", "Macau", "Japan", "South Korea",
317
+ "Singapore", "India", "Thailand", "Australia", "New Zealand", "United Arab Emirates", "Saudi Arabia",
318
+ "South Africa",
319
+ ]);
@@ -88,6 +88,153 @@ export function accountingArmed(runDir) {
88
88
  return existsSync(driverDir(String(runDir ?? ""), ACCOUNTING_STAMP));
89
89
  }
90
90
 
91
+ // ── THE BAND REACHES THE SEAT IN BATCHES, AND A BATCH IS THE UNIT OF ACCOUNTING ───────────────────
92
+ //
93
+ // A dense matter carried 1,161 records into one digest turn (2026-09-16). The seat ran out of turn
94
+ // before it had accounted for them all, the call was refused with 902 outstanding, and the ladder
95
+ // re-sent the same shape: 35 minutes and 172,900 output tokens for a document that was never written.
96
+ // Nothing was wrong with the judgment. The stage was handed more records than one turn holds.
97
+ //
98
+ // SO THE DRIVER SPLITS THE OWED SET AND THE SEAT RECORDS ONE CALL PER BATCH. What made that impossible
99
+ // was not the absence of a split — the transport has taken several calls since the patch path was
100
+ // built — but WHERE THE REFUSAL LOOKED. It looked at the whole owed set on every call, and a refused
101
+ // call stores no model, so the accumulator could never start: batch 1 was refused for not being
102
+ // batches 2 to 12, and the work in it was discarded. Driven before this change, three calls of one
103
+ // record each against an owed set of three: every call refused "2 of 3", no model stored, no document
104
+ // written, and the same refusal on call 3 as on call 1.
105
+ //
106
+ // A CALL NAMING A BATCH IS JUDGED ON THAT BATCH. A call naming none is judged on the whole owed set,
107
+ // exactly as before, which is what keeps the existing rungs working unchanged: the re-classify rung
108
+ // sends the COMPLETE set of rows and the recall-reconciliation flush sends a patch of the rows it is
109
+ // ending, and neither names a batch.
110
+ export const DIGEST_BATCH_RECORDS = 100;
111
+
112
+ /**
113
+ * The owed set split into batches, in order. PURE, and deterministic across a resume: `owed` is derived
114
+ * from placements.json, not from anything the run accumulates, so batch 7 holds the same records on the
115
+ * retry as it did on the attempt that was killed.
116
+ */
117
+ export function batchesOf(owed, size = DIGEST_BATCH_RECORDS) {
118
+ const list = [...new Set((Array.isArray(owed) ? owed : []).map(joinKey).filter(Boolean))];
119
+ const n = Math.max(1, Number(size) || DIGEST_BATCH_RECORDS);
120
+ const out = [];
121
+ for (let i = 0; i < list.length; i += n) out.push(list.slice(i, i + n));
122
+ return out;
123
+ }
124
+
125
+ /**
126
+ * The records a model accounts for, by the three exits this seam recognises. PURE.
127
+ *
128
+ * ONE READER FOR TWO GATES, and that is the point of exporting it. The call-time refusal and the
129
+ * stage's exit gate have to be counting the same thing; two implementations of "accounted" would drift
130
+ * and the drift would show up as a stage that passed with records ended nowhere.
131
+ */
132
+ export function accountedUris(model, owed = []) {
133
+ const accounted = new Set();
134
+ for (const r of [...(model?.findings_rows ?? []), ...(model?.incumbent_rows ?? []), ...(model?.negative_rows ?? [])])
135
+ accounted.add(joinKey(r?.uri));
136
+ // A disagreement resolution accounts for a record when its subject names that record's uri — the
137
+ // third exit, and the one a reader is least likely to expect, so it is joined rather than assumed.
138
+ for (const d of model?.disagreement_resolutions ?? [])
139
+ for (const k of (Array.isArray(owed) ? owed : [])) if (k && lc(d?.subject).includes(k)) accounted.add(k);
140
+ accounted.delete("");
141
+ return accounted;
142
+ }
143
+
144
+ /** Every record uri a raw call carries, across the three row lists. PURE. */
145
+ export function callUris(call) {
146
+ const out = [];
147
+ for (const r of [...(call?.findings_rows ?? []), ...(call?.incumbent_rows ?? []), ...(call?.negative_rows ?? [])]) {
148
+ const k = joinKey(r?.uri);
149
+ if (k) out.push(k);
150
+ }
151
+ return out;
152
+ }
153
+
154
+ /**
155
+ * Which batch accounted each record, after this call. PURE. Returns `{ batchOf, doubled }`.
156
+ *
157
+ * DOUBLE-COUNTING IS A BATCH COLLISION, NEVER A REPEATED URI, and the distinction is the whole reason
158
+ * this is keyed rather than a membership test. `mergeDigestPatch` replaces a row by uri on purpose —
159
+ * "refreshing an ending is idempotent" — and the recall-reconciliation flush rung depends on it,
160
+ * telling the seat to re-send rows it is changing. So a uri arriving again UNDER ITS OWN BATCH is that
161
+ * legitimate refresh and is kept; the same uri arriving under a DIFFERENT batch is a record ended
162
+ * twice, which is what inflates a count nobody can reconcile, and it is refused naming both batches.
163
+ */
164
+ export function batchLedger(storedBatchOf, call) {
165
+ const batchOf = { ...(storedBatchOf ?? {}) };
166
+ const doubled = [];
167
+ const batch = call?.batch;
168
+ if (!Number.isInteger(batch) || batch < 1) return { batchOf, doubled };
169
+ for (const k of callUris(call)) {
170
+ const was = batchOf[k];
171
+ if (was !== undefined && was !== batch) { doubled.push({ uri: k, was, now: batch }); continue; }
172
+ batchOf[k] = batch;
173
+ }
174
+ return { batchOf, doubled };
175
+ }
176
+
177
+ /**
178
+ * The batch block the digest dispatch carries: how the driver split this run's band, which batches are
179
+ * outstanding, and the one rule that makes a batch call different from a whole-document one. `null` when
180
+ * the run has no owed population, because a brief that enumerates nothing reads as a rule with no work.
181
+ *
182
+ * ON A RESUME IT IS THE RESUME INSTRUCTION, and that is why it is computed rather than fixed: the
183
+ * batches already accounted for are named as done, so the seat re-reads none of them. A retry that
184
+ * starts at batch 1 is how the stage burned 35 minutes twice on the same matter.
185
+ */
186
+ export function digestBatchBrief(gap) {
187
+ if (!gap?.armed || !Array.isArray(gap.owed) || !gap.owed.length) return null;
188
+ const plan = batchesOf(gap.owed);
189
+ const outstanding = new Set(gap.unaccounted ?? []);
190
+ const todo = [];
191
+ for (let i = 0; i < plan.length; i++) if (plan[i].some((k) => outstanding.has(k))) todo.push(i + 1);
192
+ const done = plan.length - todo.length;
193
+ const lines = [
194
+ `## Your records, in ${plan.length} batch${plan.length === 1 ? "" : "es"}`,
195
+ "",
196
+ `This run carried ${gap.owed.length} record${gap.owed.length === 1 ? "" : "s"} into the digest. The driver has split them into `
197
+ + `${plan.length} batch${plan.length === 1 ? "" : "es"} of up to ${DIGEST_BATCH_RECORDS}. Record ONE `
198
+ + "`record_register_digest` call per batch, carrying `batch: <the number>` — not one call for the whole band.",
199
+ "",
200
+ "Every record in the batch you name must end in that same call: a findings row, an incumbent row, a "
201
+ + "Negative-results drop with its ground token, or a Disagreement resolution. The call is refused if "
202
+ + "one of them ends nowhere, and the refusal lists exactly which — send only those; everything you "
203
+ + "have already recorded is kept. A record ends in ONE batch: ending it again under a different "
204
+ + "batch number is refused.",
205
+ "",
206
+ `Your prose sections (opposition, merch_sweep, cross_checks, open_flags) ride any batch call and are `
207
+ + "kept when a later call omits them.",
208
+ "",
209
+ ];
210
+ if (done) lines.push(`${done} of these batches ${done === 1 ? "is" : "are"} already recorded and complete. `
211
+ + `Outstanding: batch ${todo.join(", ")}. Do not re-read or re-send the batches that are done.`, "");
212
+ else lines.push(`Outstanding: every batch, 1 to ${plan.length}.`, "");
213
+ return lines.join("\n");
214
+ }
215
+
216
+ /**
217
+ * What this run's digest still owes, read off the stored model. `{ armed, owed, accounted, unaccounted }`.
218
+ *
219
+ * THE EXIT GATE'S READ. Before batching, a digest that ended nothing failed because no document was
220
+ * ever written — the refusal on the last call was the gate. Once a batch call is accepted, the document
221
+ * EXISTS from batch 1 onwards, so that failure stops firing, and a seat that stopped after batch 6 would
222
+ * ship a document missing half the band with nothing refusing it. That inversion is what this closes,
223
+ * and it is armed by the same era stamp as the call-time refusal so archived runs are judged as they
224
+ * always were.
225
+ */
226
+ export function digestAccountingGap(runDir) {
227
+ const armed = accountingArmed(runDir);
228
+ if (!armed) return { armed: false, owed: [], accounted: [], unaccounted: [] };
229
+ const facts = readDigestFacts(runDir);
230
+ if (!Array.isArray(facts.owed)) return { armed: true, owed: null, accounted: [], unaccounted: null };
231
+ const model = lastAcceptedModel(runDir);
232
+ if (!model) return { armed: true, owed: facts.owed, accounted: [], unaccounted: facts.owed, no_model: true };
233
+ const accounted = accountedUris(model, facts.owed);
234
+ return { armed: true, owed: facts.owed, accounted: [...accounted],
235
+ unaccounted: facts.owed.filter((k) => !accounted.has(k)) };
236
+ }
237
+
91
238
  /**
92
239
  * The document's section headings, EXPORTED because three separate readers key on them and a heading
93
240
  * changed here without changing them is the failure this constant exists to make impossible.
@@ -597,6 +744,10 @@ export function acceptRegisterDigest(params, facts = emptyFacts()) {
597
744
  open_flags: str(params?.open_flags),
598
745
  instructed_checks,
599
746
  disagreement_resolutions,
747
+ // WRITTEN ONLY WHEN THERE IS ONE, so a run that never batched stores the model it always stored and
748
+ // an archived model replays byte-identical. It is the accumulator's own bookkeeping and no renderer
749
+ // reads it: `renderRegisterFindings` takes the keys it names and ignores the rest.
750
+ ...(params?.batch_of && Object.keys(params.batch_of).length ? { batch_of: params.batch_of } : {}),
600
751
  };
601
752
 
602
753
  // ASK WHAT THE ZERO MEANS. A digest that surfaced nothing AND dropped nothing has not judged the
@@ -610,16 +761,39 @@ export function acceptRegisterDigest(params, facts = emptyFacts()) {
610
761
  // Disagreement-resolutions row. Nothing new is invented here — the join is over lists this call
611
762
  // already carries.
612
763
  if (facts.armed) {
613
- const accounted = new Set();
614
- for (const r of [...model.findings_rows, ...model.incumbent_rows, ...model.negative_rows]) accounted.add(joinKey(r.uri));
615
- // A disagreement resolution accounts for a record when its subject names that record's uri — the
616
- // third exit, and the one a reader is least likely to expect, so it is joined rather than assumed.
617
- for (const d of model.disagreement_resolutions)
618
- for (const k of facts.owed) if (k && lc(d.subject).includes(k)) accounted.add(k);
619
- const unaccounted = facts.owed.filter((k) => !accounted.has(k));
764
+ // A RECORD ENDED TWICE IS ANSWERED BEFORE A RECORD ENDED NOWHERE. The ledger is the driver's
765
+ // (batchLedger, from the raw call), so this reads a decision rather than making one.
766
+ const doubled = Array.isArray(params?.batch_doubled) ? params.batch_doubled : [];
767
+ if (doubled.length) {
768
+ const show = doubled.slice(0, 5).map((d) => `${d.uri} (batch ${d.was}, again in batch ${d.now})`).join("; ");
769
+ return { ok: false, reason: `registerdigest_double_counted:${doubled.length} record(s) this call ends were already ended by another batch: ${show}${doubled.length > 5 ? ` (+${doubled.length - 5} more)` : ""}. Each record ends exactly once, in its own batch — re-send the batch that owns it if the ending was wrong, and drop it from this one` };
770
+ }
771
+ const accounted = accountedUris(model, facts.owed);
772
+ // ── THE SCOPE IS THE BATCH WHEN THE CALL NAMES ONE, AND THE WHOLE OWED SET WHEN IT DOES NOT ─────
773
+ //
774
+ // Both arms use the same accounted set and the same three exits; only the population moves. A call
775
+ // naming no batch is judged exactly as it was before batching existed, which is what every rung
776
+ // that re-sends a complete document depends on.
777
+ const batch = Number.isInteger(params?.batch) && params.batch >= 1 ? params.batch : null;
778
+ let scope = facts.owed, where = "this run carried into the digest";
779
+ if (batch !== null) {
780
+ const plan = batchesOf(facts.owed);
781
+ const slice = plan[batch - 1];
782
+ if (!slice) {
783
+ return { ok: false, reason: `registerdigest_batch_unknown:batch ${batch} — this run's band splits into ${plan.length} batch(es) of up to ${DIGEST_BATCH_RECORDS} records, so there is no batch ${batch} to record. The dispatch names the batch count; send the batches it lists` };
784
+ }
785
+ scope = slice;
786
+ where = `batch ${batch} of ${plan.length} carried`;
787
+ }
788
+ const unaccounted = scope.filter((k) => !accounted.has(k));
620
789
  if (unaccounted.length) {
621
- const show = unaccounted.slice(0, 5).join(", ");
622
- return { ok: false, reason: `registerdigest_unaccounted_records:${unaccounted.length} of ${facts.owed.length} record(s) this run carried into the digest end nowhere — neither a findings row, nor a Negative-results drop, nor a Disagreement resolution: ${show}${unaccounted.length > 5 ? ` (+${unaccounted.length - 5} more)` : ""}. Each needs one of the three, and a drop needs its ground token` };
790
+ // THE REFUSAL IS THE WORK LIST. It used to show five of them and leave the seat to infer the rest,
791
+ // which is what made "retry the digest" the only move it could read off the refusal. A batch is at
792
+ // most DIGEST_BATCH_RECORDS records, so its outstanding set is quotable in full and the seat can
793
+ // act on THIS refusal without re-reading the band.
794
+ const cap = batch !== null ? DIGEST_BATCH_RECORDS : 5;
795
+ const show = unaccounted.slice(0, cap).join(", ");
796
+ return { ok: false, reason: `registerdigest_unaccounted_records:${unaccounted.length} of ${scope.length} record(s) ${where} into the digest end nowhere — neither a findings row, nor a Negative-results drop, nor a Disagreement resolution: ${show}${unaccounted.length > cap ? ` (+${unaccounted.length - cap} more)` : ""}. Each needs one of the three, and a drop needs its ground token. Send ONLY these — everything you have already recorded is kept` };
623
797
  }
624
798
  }
625
799
 
@@ -652,7 +826,20 @@ export function recordRegisterDigest(runDir, received, { facts = null, now = ()
652
826
 
653
827
  // A PATCH call merges onto the stored model BEFORE acceptance, so the whole document is validated
654
828
  // as one thing every time — a patch cannot slip a row past a check by arriving alone.
655
- const params = received?.patch === true ? mergeDigestPatch(lastAcceptedModel(runDir), received) : received;
829
+ //
830
+ // A BATCH CALL IS A PATCH, AND IT IS NOT OPTIONAL THAT IT IS. Batches accumulate by definition: if
831
+ // batch 2 replaced the stored model rather than merging onto it, batch 1's rows would leave the
832
+ // document the moment batch 2 was accepted, and the run would reach delivery a batch short with every
833
+ // call reading as accepted. The batch ledger travels the same way — read off the stored model, so a
834
+ // resume that re-enters the stage carries what the killed attempt had already accounted for.
835
+ const batching = Number.isInteger(received?.batch) && received.batch >= 1;
836
+ const accumulates = received?.patch === true || batching;
837
+ const stored = accumulates ? lastAcceptedModel(runDir) : null;
838
+ const merged = accumulates ? mergeDigestPatch(stored, received) : received;
839
+ const ledger = batchLedger(stored?.batch_of, received);
840
+ const params = batching
841
+ ? { ...merged, batch: received.batch, batch_of: ledger.batchOf, batch_doubled: ledger.doubled }
842
+ : merged;
656
843
  const verdict = acceptRegisterDigest(params, facts ?? readDigestFacts(runDir));
657
844
  if (!verdict.ok) {
658
845
  try { writeFileSync(paths.refusals, `${JSON.stringify({ at: now(), reason: verdict.reason })}\n`, { flag: "a" }); }
@@ -682,8 +869,46 @@ export function recordRegisterDigest(runDir, received, { facts = null, now = ()
682
869
  const at = join(String(runDir ?? ""), FINDINGS_FILE);
683
870
  // The model lands BEFORE the document: a later patch merges onto what was accepted, so a write that
684
871
  // fails must not leave a stored model describing a document nobody has.
872
+ //
873
+ // ── AND UNDER BATCHING THE MODEL IS THE ACCUMULATOR, SO LOSING IT IS FATAL HERE ──────────────────
874
+ //
875
+ // Best-effort was right while one call carried the whole document: a lost model cost the next patch
876
+ // its base and said so by refusing. It is wrong once the batches ARE the document. Lose it after
877
+ // batch 3 and batch 4 merges onto nothing, batch 3's records stop being accounted, the union never
878
+ // closes, and the run ends refusing over records the seat accounted for correctly twenty minutes
879
+ // earlier — a loop with a true-looking refusal at the end of it.
880
+ let modelWriteFailed = null;
685
881
  try { writeFileSync(paths.model, JSON.stringify(verdict.model, null, 2) + "\n"); }
686
- catch { /* best-effort; a lost model costs the next patch its base, and it says so by refusing */ }
882
+ catch (e) { modelWriteFailed = String(e?.message ?? e).slice(0, 200); }
883
+ if (modelWriteFailed && Number.isInteger(received?.batch)) {
884
+ const reason = `registerdigest_model_write_failed:this run accounts for its records in batches and the driver could not store what this batch accepted (${modelWriteFailed}), so the next batch would merge onto a base missing these rows and the run would refuse over records you have already accounted for (driver-written: this is a bug, not a model defect, and re-stating the call cannot fix it)`;
885
+ try { writeFileSync(paths.refusals, `${JSON.stringify({ at: now(), reason })}\n`, { flag: "a" }); }
886
+ catch { /* the refusal record is best-effort; the refusal itself is returned regardless */ }
887
+ return { written: null, refused: reason, captured: closeCapture({ ok: false, refused: reason }), capture_failed: captureFailed };
888
+ }
889
+
890
+ // ── THE CLIENT'S DOCUMENT STAYS ALL-OR-NOTHING WHILE BATCHES ARE OUTSTANDING ─────────────────────
891
+ //
892
+ // `register-findings.md` is read by nine parsers, by the gateway through `toolWrittenArtifact` to
893
+ // decide whether this stage produced anything at all, and by a lawyer. Writing it on each accepted
894
+ // batch puts a page on disk carrying the title, every heading and a third of the records — complete
895
+ // to every one of those readers. The exit gate in `validators.registerFindings` refuses such a
896
+ // document, which catches a seat that stops half way; it catches it AFTER the partial page exists.
897
+ //
898
+ // So while any owed record is still outstanding the batch accumulates into the MODEL, which is the
899
+ // driver's own state, and the document is rendered on the call that closes the union. A run that dies
900
+ // at batch 6 then leaves no document at all — exactly what an unfinished digest has always left, so
901
+ // the older "no document" stage failure keeps working unchanged and the exit gate becomes the second
902
+ // lock rather than the only one.
903
+ const gap = digestAccountingGap(runDir);
904
+ if (Array.isArray(gap.unaccounted) && gap.unaccounted.length) {
905
+ return {
906
+ written: null, refused: null, batch_accepted: received?.batch ?? null, remaining: gap.unaccounted.length,
907
+ accounted: gap.accounted.length,
908
+ captured: closeCapture({ ok: true, batch_accepted: received?.batch ?? null, remaining: gap.unaccounted.length }),
909
+ capture_failed: captureFailed,
910
+ };
911
+ }
687
912
  try { writeFileSync(at, verdict.content); }
688
913
  catch (e) {
689
914
  return { written: null, refused: null, write_failed: String(e?.message ?? e).slice(0, 200),
@@ -69,5 +69,5 @@ export function grantVocabularySentence(provider = null) {
69
69
  + `whole, and the frozen plan's entries are fetched by the executor, not by you. So reaching for one `
70
70
  + `of these means the query is wrong, not that a capability is missing: go back to the sweep and fix `
71
71
  + `its scope. This list is the tools THIS provider serves; another deployment's differs, and `
72
- + `\`skills/prelim-register/providers/<name>.md\` is where the provider-specific vocabulary lives.`;
72
+ + `\`skills/clearance-register/providers/<name>.md\` is where the provider-specific vocabulary lives.`;
73
73
  }