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
@@ -49,8 +49,8 @@
49
49
  -->
50
50
  <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
51
51
  <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
52
- <script type="module" crossorigin src="/portal/assets/index-5UyqAyNM.js"></script>
53
- <link rel="stylesheet" crossorigin href="/portal/assets/index-CVOIvdhc.css">
52
+ <script type="module" crossorigin src="/portal/assets/index-EVaSo5-g.js"></script>
53
+ <link rel="stylesheet" crossorigin href="/portal/assets/index-CtvwLCti.css">
54
54
  </head>
55
55
  <body>
56
56
  <div id="root"></div>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.7",
5
+ "version": "0.3.2-beta.9",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -141,7 +141,7 @@ Case-law setup is an OAuth flow, **not** an environment variable — see
141
141
 
142
142
  ## Adding or changing a register adapter
143
143
 
144
- Read [`../driver/skills/prelim-register/providers/README.md`](../driver/skills/prelim-register/providers/README.md)
144
+ Read [`../driver/skills/clearance-register/providers/README.md`](../driver/skills/clearance-register/providers/README.md)
145
145
  first — it carries the mandatory empirical-verification checklist. The short version: **never inherit a
146
146
  capability claim by analogy from another provider.** Every operator, filter, pagination shape and
147
147
  composition rule is probed against the live API before it is written down, and the observed figure is
@@ -60,8 +60,8 @@ import { clipProviderText } from "./provider-text.mjs"; // — keep the discri
60
60
 
61
61
  // ── — HOW MUCH OF A PROVIDER ERROR SURVIVES INTO THE BAND BLOCK ───────────────────────────────
62
62
  //
63
- // Both were 140, and 140 is where the defect lived: the Clarivate Near/Adj refusal is 144 characters
64
- // and its verdict — `are not allowed` — is the last two words. Cut at 140 it arrived as `are not all`,
63
+ // Both were 140, and 140 is where the defect lived: one register's Near/Adj refusal runs just past that
64
+ // bound and its verdict — `are not allowed` — is the last two words. Cut at 140 it arrived truncated,
65
65
  // That structural predicate could not match, and a refusal that recurs byte-identically forever was
66
66
  // filed as weather and retried on every future run of that shape.
67
67
  //
@@ -299,9 +299,9 @@ export function makeEnumerate(deps) {
299
299
  }
300
300
 
301
301
  // ── count-first per-CLASS rescue ──────────────────────────────────────────────────────────────────
302
- // The per-term rescue's exact sibling, on the OTHER axis a stack can crowd along. A multi-class
303
- // owner query [cl 5,29,30,32,33,35,43] came back 805 > 600 and shipped as one blind count — "Cl. 30
304
- // leg unopened" became the whole of the residual risk story — when per-class counts would have made
302
+ // The per-term rescue's exact sibling, on the OTHER axis a stack can crowd along. A multi-class owner
303
+ // query crowded over the ceiling and shipped as one blind count — a single unopened class leg became
304
+ // the whole of the residual risk story — when per-class counts would have made
305
305
  // EVERY leg individually enumerable. So: an OWNER-SCOPED query (a bare-owner sweep or an owner×term
306
306
  // slice — the only shapes whose crowds are portfolio-shaped rather than name-shaped) that crowds over
307
307
  // the ceiling across >1 class is counted per class with the SAME shared count kernel, and every
@@ -371,7 +371,7 @@ export function makeEnumerate(deps) {
371
371
  // A provider with no total anywhere cannot run a count-first rescue — there is nothing to count.
372
372
  const countFirst = countProbe !== "none";
373
373
  // The per-class rescue's trigger shape: an owner-scoped query (bare-owner sweep or owner×term
374
- // slice) spanning >1 class — the portfolio-shaped crowd the 805 count died as. The per-term rescue
374
+ // slice) spanning >1 class — the portfolio-shaped crowd a single blind count dies as. The per-term rescue
375
375
  // keeps precedence on multi-name stacks (its accounting is the finer truth there).
376
376
  const ownerScoped = isOwnerScoped(params);
377
377
  const splitClasses = ownerScoped
@@ -172,9 +172,9 @@ export const unresolvedOwnerCountReason = (term, coveredBy) =>
172
172
  *
173
173
  * WHAT IT NO LONGER CLAIMS. It used to say "this owner is answered record-by-record by the owner×term
174
174
  * slice(s) …". `covered_by` is stamped at PLAN COMPILE time and names the slices that were DICTATED for
175
- * this owner — it says nothing about whether they enumerated. Measured across a delivered round: true
176
- * for 4 of 14 owners and false for 10, covering 39,302 hits (31% of the untraced total). The descriptor
177
- * was asserting coverage it is not in a position to observe, on the largest class in the artifact.
175
+ * this owner — it says nothing about whether they enumerated. Measured across a delivered round, it was
176
+ * true for a minority of owners and false for the rest, and the hits it covered were the largest single
177
+ * class in the artifact. The descriptor was asserting coverage it is not in a position to observe.
178
178
  *
179
179
  * The pointer is the part worth keeping, so it stays and the CLAIM around it goes: these are where the
180
180
  * records were sought, and their own state is what says whether they were found. PURE.
@@ -29,9 +29,10 @@
29
29
  //
30
30
  // Telemetry is fully isolated in try/catch: a ledger failure must NEVER affect a search.
31
31
 
32
- import { appendFileSync, mkdirSync } from "node:fs";
33
- import { dirname } from "node:path";
34
- import { ledgerPath } from "./ledger-path.mjs";
32
+ import { appendFileSync, mkdirSync, statSync, openSync, readSync, closeSync, readFileSync, writeFileSync, renameSync, rmdirSync } from "node:fs";
33
+ import { dirname, basename } from "node:path";
34
+ import { createHash } from "node:crypto";
35
+ import { ledgerPath, RUN_RECORD_LOG_FILE } from "./ledger-path.mjs";
35
36
 
36
37
  export const CALL_LOG_PATH = ledgerPath("call");
37
38
  // Spec A1 (citation fidelity): every fetched record's BODY is persisted alongside the call ledger, so the
@@ -97,7 +98,7 @@ export function makeLedger(provider) {
97
98
  // written into the row.
98
99
  const dest = typeof tctx?.recordLog === "string" && tctx.recordLog.trim()
99
100
  ? tctx.recordLog.trim() : RECORD_LOG_PATH;
100
- append(dest, JSON.stringify({
101
+ const row = {
101
102
  ts: new Date().toISOString(),
102
103
  provider,
103
104
  agentId: tctx?.agentId ?? null,
@@ -105,8 +106,121 @@ export function makeLedger(provider) {
105
106
  sessionId: tctx?.sessionId ?? null,
106
107
  target,
107
108
  body,
108
- }));
109
+ };
110
+ if (basename(dest) === RUN_RECORD_LOG_FILE) writeRecordOnce(dest, row);
111
+ else append(dest, JSON.stringify(row));
109
112
  } catch { /* record persistence must never break a search */ }
110
113
  };
111
114
  return { logCall, logRecordBody, tctxOf };
112
115
  }
116
+
117
+ // ── EACH REGISTER RECORD ONCE PER RUN ─────────────────────────────────────────────────────────────────
118
+ //
119
+ // A record that several queries return — an OR-list, a compound, the mark itself — was appended once per
120
+ // query. Measured on a delivered four-letter run: 2,898 lines for 2,098 distinct records, 800 of them a
121
+ // record already in the file, 21.6 MB of a 106 MB ledger, and the band server reads the whole file on a
122
+ // cache miss. 786 of the 800 were byte-identical bodies.
123
+ //
124
+ // So a body for a record already written is not appended. A body that DIFFERS from the one written (a
125
+ // status moved between two queries) replaces it ONCE, in place, and the replacement says so: `refreshed:
126
+ // true` and the timestamp of the body it replaced. Once only, so a field that varies between answers
127
+ // cannot rewrite the file on every query. Every field stays, the vendor's raw copy included.
128
+ //
129
+ // SEVERAL PROCESSES WRITE ONE RUN'S LEDGER — the register servers of parallel stages and the driver's own
130
+ // fetches — so "already written" is read off the FILE, never off this process's memory alone: each write
131
+ // indexes whatever was appended since this process last looked. Writes take a lock directory beside the
132
+ // file, so an in-place replacement (written to a temporary file and renamed over, which readers see
133
+ // atomically) cannot drop a line another process was appending. A lock that cannot be had in time falls
134
+ // back to a plain append: a duplicate line is the old behaviour, a lost record is not acceptable.
135
+ //
136
+ // Run-scoped ledgers only. The box-wide fallback file holds many runs, and a record one run fetched is not
137
+ // a record another run holds.
138
+ const RUN_INDEXES = new Map(); // dest → { offset, byTarget: Map<key, { hash, refreshed, ts }> }
139
+ const targetKey = (t) => String(t ?? "").toLowerCase(); // the key every reader of this file matches on
140
+ const bodyHash = (b) => createHash("sha1").update(JSON.stringify(b ?? null)).digest("hex");
141
+
142
+ function indexRow(ix, line) {
143
+ if (!line.trim()) return;
144
+ try {
145
+ const r = JSON.parse(line);
146
+ ix.byTarget.set(targetKey(r?.target), { hash: bodyHash(r?.body), refreshed: r?.refreshed === true, ts: r?.ts ?? null });
147
+ } catch { /* a torn or foreign line indexes nothing, and is left where it is */ }
148
+ }
149
+
150
+ /** Index every whole line appended since this process last looked; a file that shrank is re-read. */
151
+ function indexOf(dest) {
152
+ let size = 0, ino = null;
153
+ try { const st = statSync(dest); size = st.size; ino = st.ino; } catch { size = 0; }
154
+ let ix = RUN_INDEXES.get(dest);
155
+ // A replacement elsewhere renames a NEW file over this one: same path, different inode, and an offset
156
+ // into the old file means nothing in the new one. Re-read from the start.
157
+ if (!ix || size < ix.offset || ix.ino !== ino) { ix = { offset: 0, ino, byTarget: new Map() }; RUN_INDEXES.set(dest, ix); }
158
+ if (size <= ix.offset) return ix;
159
+ const fd = openSync(dest, "r");
160
+ try {
161
+ const CHUNK = 8 * 1024 * 1024;
162
+ let carry = Buffer.alloc(0);
163
+ let pos = ix.offset;
164
+ while (pos < size) {
165
+ const buf = Buffer.alloc(Math.min(CHUNK, size - pos));
166
+ const n = readSync(fd, buf, 0, buf.length, pos);
167
+ if (n <= 0) break;
168
+ pos += n;
169
+ const data = carry.length ? Buffer.concat([carry, buf.subarray(0, n)]) : buf.subarray(0, n);
170
+ const last = data.lastIndexOf(0x0a);
171
+ if (last < 0) { carry = data; continue; }
172
+ for (const line of data.subarray(0, last).toString("utf8").split("\n")) indexRow(ix, line);
173
+ carry = data.subarray(last + 1); // a line still being written waits for the next look
174
+ ix.offset = pos - carry.length;
175
+ }
176
+ } finally { closeSync(fd); }
177
+ return ix;
178
+ }
179
+
180
+ const sleepMs = (ms) => { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* no sleep available */ } };
181
+
182
+ /** Run `fn` holding the ledger's lock; false (and `fn` not run) when the lock could not be had in time. */
183
+ function withLedgerLock(dest, fn) {
184
+ const lock = `${dest}.lock`;
185
+ const deadline = Date.now() + 3000;
186
+ for (;;) {
187
+ try { mkdirSync(dirname(dest), { recursive: true }); mkdirSync(lock); break; }
188
+ catch {
189
+ // A holder that died leaves the directory behind; nothing holds this lock for more than a rewrite.
190
+ try { if (Date.now() - statSync(lock).mtimeMs > 30_000) { rmdirSync(lock); continue; } } catch { continue; }
191
+ if (Date.now() > deadline) return false;
192
+ sleepMs(10);
193
+ }
194
+ }
195
+ try { fn(); } finally { try { rmdirSync(lock); } catch { /* already gone */ } }
196
+ return true;
197
+ }
198
+
199
+ function writeRecordOnce(dest, row) {
200
+ const done = withLedgerLock(dest, () => {
201
+ const ix = indexOf(dest);
202
+ const key = targetKey(row.target);
203
+ const hash = bodyHash(row.body);
204
+ const prev = ix.byTarget.get(key);
205
+ if (prev && (prev.hash === hash || prev.refreshed)) return; // written already; replaced at most once
206
+ if (!prev) {
207
+ append(dest, JSON.stringify(row));
208
+ indexOf(dest);
209
+ return;
210
+ }
211
+ // A CHANGED BODY REPLACES THE ONE WRITTEN, in place, recorded as such. Written whole to a temporary
212
+ // file and renamed over, so a reader sees the old file or the new one and never half of either.
213
+ const next = { ...row, refreshed: true, refreshed_from: prev.ts ?? null };
214
+ const kept = readFileSync(dest, "utf8").split("\n").filter((line) => {
215
+ if (!line.trim()) return false;
216
+ try { return targetKey(JSON.parse(line)?.target) !== key; } catch { return true; }
217
+ });
218
+ kept.push(JSON.stringify(next));
219
+ const tmp = `${dest}.${process.pid}.tmp`;
220
+ writeFileSync(tmp, kept.join("\n") + "\n");
221
+ renameSync(tmp, dest);
222
+ RUN_INDEXES.delete(dest);
223
+ indexOf(dest);
224
+ });
225
+ if (!done) append(dest, JSON.stringify(row));
226
+ }
@@ -18,8 +18,8 @@
18
18
  // ── WHY TAIL-PRESERVING, RATHER THAN A BIGGER NUMBER ────────────────────────────────────────────────
19
19
  //
20
20
  // Raising 140 to 400 fixes this string and leaves the defect. Vendor messages put the STATUS at the
21
- // front and the DISCRIMINATOR at the back — "…are not allowed", "…exceeds the maximum allowed (1000)",
22
- // "…Maximum number of results is 30000." A head-only clip is therefore biased against exactly the part
21
+ // front and the DISCRIMINATOR at the back — the operator that is not allowed, the bound that was
22
+ // exceeded, the maximum that was passed. A head-only clip is therefore biased against exactly the part
23
23
  // a classifier needs, at whatever length it is set to. The bias is the bug; the number is not.
24
24
  //
25
25
  // So: keep the head, keep the tail, elide the middle. The head carries the HTTP status and the office
@@ -9,8 +9,8 @@
9
9
 
10
10
  export const BATCH_SCREEN_CHUNK = 100; // corsearch brand-json page size; clarivate /text caps at exactly 100 too.
11
11
 
12
- // Live/dead status map for brand-json's vocabulary (HAR-counted across 702 rows: Valid 337, Invalid 187,
13
- // Pending 103, Expired 69, GracePeriod 4, Unknown 2). FAIL-OPEN: only Invalid/Expired are confidently dead;
12
+ // Live/dead status map for brand-json's vocabulary, counted across a screening sample so that every token
13
+ // the endpoint uses is mapped and none is left to chance. FAIL-OPEN: only Invalid/Expired are confidently dead;
14
14
  // Valid/Pending/GracePeriod are LIVE (Pending = a live in-progress application; GracePeriod = the post-expiry
15
15
  // renewal window — both real senior-rights risks that must NEVER be batch-dropped); anything else (Unknown or
16
16
  // an unrecognized token) is AMBIGUOUS → the skill must fall through to record_fetch, never auto-drop on it.
@@ -16,9 +16,9 @@
16
16
  // hand-written check. That is what `capabilities.nativeScriptIndex` is:
17
17
  //
18
18
  // true — the index holds the CHARACTERS. A native-script term is a legitimate, productive query
19
- // and must be sent (the table in driver/jx.mjs): 小米 = 553,
20
- // 华威豹 = 6, 스타벅스 = 15 on such a provider, against 0/0/0 on a romanisation index; and
21
- // archived executed bands returned non-zero on native characters across Han, Katakana,
19
+ // and must be sent (the comparison in driver/jx.mjs): native-script terms answer on such a
20
+ // provider and answer nothing on a romanisation index; and
21
+ // archived executed bands returned records on native characters across Han, Katakana,
22
22
  // Cyrillic and Greek. Guarding it here would convert evidenced coverage into deferrals —
23
23
  // the OPPOSITE defect, and a worse one, because a deferral is at least visible.
24
24
  // false — the index holds only the TRANSLITERATION. Sending the characters is a silent zero.
@@ -6,8 +6,8 @@
6
6
  // territories by display name ("United States", "European Union" — driver/compose-read.mjs
7
7
  // PROMPT_TERRITORIES, mirroring portal-ui composerLevers), and those names flowed verbatim into
8
8
  // `region:` clauses because corsearch's offices.translate is an ISO passthrough that ASSUMES codes.
9
- // Corsearch answers an unknown multi-word region value with HTTP 500 — not a 400 — so auto-recovery
10
- // classified the failure transient and burned its park budget re-sending the same malformed query.
9
+ // That provider answers an unknown multi-word region value as a server error rather than a bad request,
10
+ // so auto-recovery classified the failure transient and burned its park budget re-sending it.
11
11
  // The run died at fan-in with terminalKind:repeat-signature.
12
12
  //
13
13
  // The register wire vocabulary is CODES (providers/corsearch/src/index.js: "UPPERCASE 2-letter
@@ -98,6 +98,26 @@ export const TERRITORY_TO_CODE = Object.freeze({
98
98
  // two Benelux members — and they must not depend on which internationalisation data a customer's
99
99
  // runtime happens to carry. What is derived is a WIDENING: it can only add names that would otherwise
100
100
  // have resolved to nothing, and a build whose data is thinner loses names it never promised.
101
+ // A WITHDRAWN CODE ANSWERS TO THE SAME NAME AND IS REACHED FIRST. The runtime still knows the codes ISO
102
+ // has retired, and it gives them the CURRENT country's name: `VD` (North Vietnam, withdrawn 1977) and
103
+ // `VN` both answer "Vietnam". The scan runs AA to ZZ, so the dead code claims the name and the live one
104
+ // finds it taken — seven territories resolved that way, Vietnam, Yemen, Serbia, Zimbabwe, Vanuatu,
105
+ // Myanmar and Curaçao. None of them is a name this engine holds a jurisdiction for, so a requester who
106
+ // wrote "Vietnam" was pointed at a code no register can answer, and one who wrote "Vietnam" AND "VN" had
107
+ // two countries and was refused a search they had described correctly.
108
+ //
109
+ // Asking the runtime to canonicalise the region fixes all seven from the runtime's own data rather than
110
+ // from a hand-written list of retirements that would go stale the next time ISO withdraws one. A code
111
+ // that is current canonicalises to itself, so this is inert for every other entry — measured over the
112
+ // whole AA-ZZ scan, the only entries that move are those seven.
113
+ //
114
+ // It does not touch the direct-code branch in normalizeTerritory, where UK deliberately stays UK: that
115
+ // is an alias the providers own, not a withdrawal, and canonicalising there would take the decision
116
+ // away from the translate step that is supposed to make it.
117
+ const canonicalRegion = (code) => {
118
+ try { return new Intl.Locale(`und-${code}`).region || code; } catch { return code; }
119
+ };
120
+
101
121
  const DERIVED_NAME_TO_CODE = (() => {
102
122
  const out = Object.create(null);
103
123
  let names;
@@ -109,7 +129,7 @@ const DERIVED_NAME_TO_CODE = (() => {
109
129
  try { name = names.of(code); } catch { continue; }
110
130
  if (!name || name === code || /^unknown/i.test(name)) continue;
111
131
  const key = strip(name);
112
- if (key && !(key in TERRITORY_TO_CODE) && !(key in out)) out[key] = code;
132
+ if (key && !(key in TERRITORY_TO_CODE) && !(key in out)) out[key] = canonicalRegion(code);
113
133
  }
114
134
  }
115
135
  return out;
@@ -57,4 +57,4 @@ canonical schema, and the header names the traps — `EU` vs `EM`, `CONTAINS` on
57
57
  hard 400, and `cardinalityRefusal`, the third way this provider says "that would match too much". Then
58
58
  `src/core.js` for how those declarations are built into requests. The model-facing operator vocabulary
59
59
  lives in
60
- [`../../driver/skills/prelim-register/providers/clarivate.md`](../../driver/skills/prelim-register/providers/clarivate.md).
60
+ [`../../driver/skills/clearance-register/providers/clarivate.md`](../../driver/skills/clearance-register/providers/clarivate.md).
@@ -6,9 +6,9 @@
6
6
  //
7
7
  // This contract is what the plan compiler reads, and the core implements exactly what is declared here:
8
8
  //
9
- // * classFilter is "native", NOT "fanout". INT_CLASS_NUMBER value "9 OR 28 OR 41 OR 42" = 18, byte-for-
10
- // byte the deduplicated result of the old per-class fan-out. The fan-out is DELETED (phase 4): N
11
- // classes now cost ONE call.
9
+ // * classFilter is "native", NOT "fanout". An INT_CLASS_NUMBER value of "9 OR 28 OR 41 OR 42" answers
10
+ // byte-for-byte the deduplicated result of the old per-class fan-out. The fan-out is DELETED
11
+ // (phase 4): N classes now cost ONE call.
12
12
  // * predicates.default is a TRUE contains, expressed as an INFIX TERM WILDCARD (`*TERM*`) on
13
13
  // WORD_MARK_SPECIFICATION — NOT the bare EQUALS the old MATCH_MODE_TO_FIELD.default emitted (which
14
14
  // lost recall), and NOT the CONTAINS operator (supported on the mark field, but see the owner note).
@@ -105,12 +105,12 @@ export const CAPABILITIES = Object.freeze({
105
105
  // Stage 0.5 uses it on NEITHER, so the number means the same thing on every deployment. See the
106
106
  // matching note in providers/corsearch/src/capabilities.js and driver/register-count.mjs.
107
107
  countStatusFilter: "live",
108
- // JSON body, not a URI: the bound is the parser's document-nesting cap. 80/200/500 terms all HTTP 200;
109
- // 1000 → HTTP 500 "Document nesting depth (1001) exceeds the maximum allowed (1000)". Safe chunk = 500.
108
+ // JSON body, not a URI: the bound is the parser's own document-nesting cap, which the vendor names in
109
+ // the refusal it answers a stack wider than this with. Not a URI length, so widening is not the fix.
110
110
  maxOrWidth: 500,
111
111
  // ONE call: INT_CLASS_NUMBER value "9 OR 28 OR 41 OR 42" (or "9,28,41,42") = the deduplicated union.
112
112
  classFilter: "native",
113
- // POST /text, EXACTLY 100 ids per call (101+ → HTTP 400), and the call is BILLED — screening an
113
+ // POST /text, EXACTLY 100 ids per call — a longer list is refused — and the call is BILLED: screening an
114
114
  // enumerated band also fully hydrates it. (test:true is obfuscated + unbilled + NOT persisted to the
115
115
  // record ledger: dev only, it can never back a real finding.)
116
116
  screenSource: "billed-record-fetch",
@@ -127,8 +127,8 @@ export const CAPABILITIES = Object.freeze({
127
127
  // Semantically it is the SAME CONDITION as `resultCeiling` — this query would match too much — and the
128
128
  // engine already knows what to do with that: record a count+sample descriptor and hand it to judgment.
129
129
  // It arrives as a 500 on the COUNT PROBE instead of a 400 on the search, so it landed in the generic
130
- // provider-error arm and became a hard coverage hole: 7 of 161 slices on one delivered run, all
131
- // `incumbent-class`, every one of them an owner or owner×term slice against a very large portfolio.
130
+ // provider-error arm and became a hard coverage hole — a small but real share of one delivery's slices,
131
+ // every one of them an owner or owner×term slice against a very large portfolio.
132
132
  //
133
133
  // AND THE EXISTING WIDTH DEFENCE CANNOT HELP, which is why nobody noticed the gap. `maxOrWidth`
134
134
  // chunks against NESTING DEPTH. Splitting a wide OR-stack into narrower ones does nothing about
@@ -152,9 +152,9 @@ export const CAPABILITIES = Object.freeze({
152
152
  // compiled to an ADJ chain — `*CORAL ADJ PUP*` — which is an ordered phrase match.
153
153
  exact: "EXACT_WORD_MARK_SPECIFICATION", // case-insensitive but PUNCTUATION-SENSITIVE → strip punctuation client-side
154
154
  default: "WORD_MARK_SPECIFICATION:*TERM*", // a TRUE contains via the term wildcard, not the recall-losing bare EQUALS
155
- wildcardPrefix: "WORD_MARK_SPECIFICATION:TERM*", // native `*` in the value (NIK* = 128)
156
- wildcardSuffix: "WORD_MARK_SPECIFICATION:*TERM", // (*NIKE = 36)
157
- wildcardInfix: "WORD_MARK_SPECIFICATION:*TERM*", // (*NIK* = 806)
155
+ wildcardPrefix: "WORD_MARK_SPECIFICATION:TERM*", // native `*` in the value
156
+ wildcardSuffix: "WORD_MARK_SPECIFICATION:*TERM",
157
+ wildcardInfix: "WORD_MARK_SPECIFICATION:*TERM*", // the widest of the three
158
158
  phonetic: "PHONETIC_WORD_MARK_SPECIFICATION",
159
159
  owner: "APPLICANT_NAME", // EQUALS (+ wildcards); CONTAINS is a hard 400 — never emit it
160
160
  }),
@@ -231,7 +231,7 @@ export const CAPABILITIES = Object.freeze({
231
231
  //
232
232
  // The entry-level RESCUE is the other half and it comes first: substituteRomanizedNames swaps the
233
233
  // plan entry's `romanizedTerms` in (and relaxes the predicate to contains, because `exact` on a
234
- // transliteration is itself a silent zero — GR "POLITIKI PROSTASIA" exact 0 / contains 10). A slice
234
+ // transliteration is itself a silent zero, where the same term under contains answers). A slice
235
235
  // rescued that way is answerable and is never refused; only a native term with no romanisation defers.
236
236
  nativeScriptIndex: false,
237
237
  // No phoneme expansion knob: PHONETIC_WORD_MARK_SPECIFICATION is the whole surface; the client cannot
@@ -120,7 +120,7 @@ export const OWNER_FIELD = "APPLICANT_NAME";
120
120
  /**
121
121
  * match_mode → { name, operator, pre, post, … }.
122
122
  *
123
- * `default` is a TRUE contains (`*term*`, probe: *NIK* = 806) — the old EQUALS mapping silently lost
123
+ * `default` is a TRUE contains (`*term*`) — the old EQUALS mapping silently lost
124
124
  * recall and is gone. `exact` strips punctuation client-side (the EXACT field is case-insensitive but
125
125
  * punctuation-SENSITIVE). `wildcard` passes the caller's metacharacters through untouched — no
126
126
  * BEGINS_WITH/ENDS_WITH remapping is needed because `*`/`?` are native in the value; its only
@@ -159,24 +159,23 @@ export const MATCH_MODE_TO_FIELD = Object.freeze({
159
159
  // the space is NOT is a phrase match — and a phrase operator exists, absent from the vendor's schema but
160
160
  // solid on the wire:
161
161
  //
162
- // MONSTER ADJ ENERGY = 67 ENERGY ADJ MONSTER = 0 ordered adjacency == phrase-contains
163
- // CORAL ADJ PUP = 11 PUP ADJ CORAL = 0
164
- // MOUNTAIN ADJ DEW ADJ VISIONARY = 1 chains past two tokens (that mark was READ,
165
- // not guessed — /search + /text, 49 texts)
166
- // *MOUNTAIN ADJ DEW* OR *MONSTER ADJ ENERGY* = 157 = 90 + 67 survives an OR-stack, exact sum, and
167
- // (…) OR (…) = 157 too ADJ binds TIGHTER than OR — no parens needed
162
+ // · ordered adjacency IS phrase-contains — the tokens match in the order written, and the reversed
163
+ // pair matches nothing. A space cannot express that, because a space is order-blind.
164
+ // · it chains past two tokens, and the record that shows it was READ rather than inferred (/search
165
+ // for the hit, /text for the mark itself).
166
+ // · it survives an OR-stack, and the stack answers the exact sum of its legs — so ADJ binds TIGHTER
167
+ // than OR and no parentheses are needed.
168
168
  //
169
- // A phrase is therefore EXPRESSIBLE, so doctrine 2 says express it. Precision beats the space's AND
170
- // (67 ≤ 75), which matters: the AND would have quietly widened every multi-word slice.
169
+ // A phrase is therefore EXPRESSIBLE, so doctrine 2 says express it. Precision beats the space's AND,
170
+ // which matters: the AND would have quietly widened every multi-word slice.
171
171
  //
172
172
  // Two caveats, both recorded rather than hidden:
173
- // · There is NO string anchor for a phrase. `BEGINS_WITH "DIET MOUNTAIN"` = 9 > `DIET ADJ MOUNTAIN`
174
- // = 7, i.e. the operator degrades to per-token AND. So multi-word `starts_with`/`ends_with` become
173
+ // · There is NO string anchor for a phrase: `BEGINS_WITH` on a multi-word value degrades to per-token
174
+ // AND and answers WIDER than the phrase operator. So multi-word `starts_with`/`ends_with` become
175
175
  // phrase-CONTAINS — a strict superset of what was asked. Safe in a clearance sweep, which fails by
176
176
  // MISSING a mark and never by surfacing an extra one, but it is a widening and it is disclosed.
177
- // · Punctuation is not indexed (`MOUNTAIN ADJ DEW ADJ HI-RES` == `… HI ADJ RES` == 1), so a
178
- // punctuation-only token is dropped from the chain. Kept, it matches nothing and would zero the
179
- // whole phrase — `TIKI ADJ & ADJ SLUSH` = 0.
177
+ // · Punctuation is not indexed — a hyphenated token matches as its split form — so a punctuation-only
178
+ // token is dropped from the chain. Kept, it matches nothing and would zero the whole phrase.
180
179
  const PHRASE_OPERATOR = "ADJ";
181
180
  // ── A TERM MUST BE ONE MARK, ON EVERY MARK-FIELD PREDICATE ────────────────────────────────────────
182
181
  // The supplemental/cross-check lanes sometimes mint a LIST or a DESCRIPTION into the term field. All
@@ -316,7 +315,7 @@ export function compilePhraseValue(term, { pre = "", post = "", dropReserved = f
316
315
  //
317
316
  // `allowReserved` is set on the PHRASE path only, where the term is tokenised and each operator word is
318
317
  // dropped with the adjacency across it widened — so "BLACK AND DECKER" goes out as
319
- // `*BLACK ADJ2 DECKER*` (55 hits; the register writes it "BLACK & DECKER") rather than being refused.
318
+ // `*BLACK ADJ2 DECKER*` — the register writes such a mark with the ampersand — rather than being refused.
320
319
  // That is an EXPRESSION of the term, not a rewrite of it, and it is a SUPERSET of the literal phrase.
321
320
  // `exact` still rejects, and correctly: that field answers HTTP 400 "Use of operators (AND, NOT, ADJ,
322
321
  // NEAR) is not allowed in this field" and offers no escape at all. The owner field is not ADJ-joined,
@@ -333,23 +332,17 @@ export function stripPunctuation(term) {
333
332
  }
334
333
 
335
334
  // ── NON-LATIN MARK TERMS ARE NOT SEARCHABLE — AND THE FAILURE IS A SILENT ZERO ─────────────────────
336
- // Compumark indexes a non-Latin filing by its ROMANISATION, never by its characters. The record
337
- // carries both, and only the romanisation is a search key:
335
+ // Compumark indexes a non-Latin filing by its ROMANISATION, never by its characters. The record carries
336
+ // both — `markVerbalElementText` holds the characters, `markTransliteration` the romanisation — and only
337
+ // the romanisation is a search key. The characters answer nothing; the romanisation answers records that
338
+ // carry those very characters, so both halves are needed to see the behaviour at all.
338
339
  //
339
- // markVerbalElementText "华威豹" markTransliteration "HUA WEI BAO"
340
+ // Universal, not a CJK-specific behaviour: every non-Latin record read across CN/TW/JP/KR/TH/GR/UA/EG/
341
+ // IL/SA carried a populated `markTransliteration`.
340
342
  //
341
- // Established against records actually fetched and read, so this is not inference:
342
- //
343
- // 华威豹 → 0 HUA WEI BAO → 32 (and the 32 contain 华威豹)
344
- // 小米 → 0 XIAOMI → 57632
345
- //
346
- // Universal, not a CJK-specific behaviour — non-Latin records across CN/TW/JP/KR/TH/GR/UA/EG/IL/SA
347
- // carried a populated markTransliteration (JP 7/7, KR 7/7, TW 12/12, TH 10/10, GR 6/6, UA 10/10,
348
- // EG 11/11, SA 6/6, IL 1/1, CN 12/12).
349
- //
350
- // The romanisation is also STRICTLY BETTER for clearance than a character search would be:
351
- // `HUA WEI BAO` returns 华威豹, 华味宝 AND 华为爆破 — three character sets, one pronunciation. Chinese
352
- // squatting is overwhelmingly homophone-based, and a literal character match finds one of the three.
343
+ // The romanisation is also STRICTLY BETTER for clearance than a character search would be: one
344
+ // romanisation returns several distinct character sets that share its pronunciation. Chinese squatting
345
+ // is overwhelmingly homophone-based, and a literal character match finds one of them.
353
346
  //
354
347
  // So a native-script term reaching the wire is a CLIENT-SIDE REFUSAL, never a query: 0 with no error
355
348
  // is the exact false-clean this provider swap exists to prevent, and it is the shape a caller is most
@@ -415,10 +408,11 @@ export function substituteRomanizedNames(e, pp, plan) {
415
408
  //
416
409
  // Both are EXPRESSIBLE, which is why neither is a reason to drop the name (same call as the
417
410
  // apostrophe,). The shapes that work:
418
- // a quoted name literal → 400, the quotes stripped → answers, `?`-substituted → answers
419
- // an accented name literal → 400, ASCII-folded → answers
420
- // `COMUNICAÇÕES` literal → 400, ASCII-folded → 6605
421
- // `KEY COMÉRCIO` literal → 23, ASCII-folded → 23 (folding never narrows)
411
+ // a quoted name literal → refused, the quotes stripped → answers, `?`-substituted → answers
412
+ // an accented name literal → refused, ASCII-folded → answers
413
+ //
414
+ // Folding never NARROWS, either: a name whose accents are already absent from the index answers the same
415
+ // either way, so the fold is safe to apply to every name rather than only the ones that fail.
422
416
  //
423
417
  // Strip rather than `?`-substitute: a bare space is an implicit AND on this field so the tokens still
424
418
  // have to co-occur, and stripping cannot produce the `??` adjacency that a substitution would when two
@@ -542,8 +536,8 @@ export function resolveOffices(regions) {
542
536
  /**
543
537
  * ONE /search (or /count) body — the SAME SearchRequest shape for both endpoints.
544
538
  *
545
- * Multi-class is ONE searchField whose value is the class OR-list ("9 OR 28 OR 41 OR 42"), probe-
546
- * verified identical (18 hits) to the deleted 4-call per-class fan-out.
539
+ * Multi-class is ONE searchField whose value is the class OR-list ("9 OR 28 OR 41 OR 42"), which
540
+ * answers the deduplicated union of the deleted 4-call per-class fan-out.
547
541
  */
548
542
  // ── THE APOSTROPHE IS NOT SEARCHABLE IN APPLICANT_NAME ────────────────────────────────────────────
549
543
  // An apostrophe is not searchable: "TRADER VIC'S", "MCDONALD'S CORPORATION" and
@@ -1150,7 +1144,7 @@ async function fetchText(apiKey, base, group, testMode, tctx) {
1150
1144
  // Accepts record_ids that are EITHER synthetic `/mark/<office>/<guid>` refs OR bare guids (so the
1151
1145
  // skill flow is identical to Corsearch: search → record_id → record_fetch(record_id)). Returns the
1152
1146
  // NORMALIZED records and persists each (keyed by its synthetic ref) for the driver's A1 citation gate.
1153
- // /text takes EXACTLY 100 ids per call (101+ → HTTP 400) — chunked here, at the bound the probe pins.
1147
+ // /text takes EXACTLY 100 ids per call and refuses a longer list, so the chunking happens here.
1154
1148
  export async function doRecordFetch(apiKey, base, params, tctx) {
1155
1149
  const inputs = Array.isArray(params.record_ids) ? params.record_ids : [];
1156
1150
  if (inputs.length === 0) return { type: "text", text: "ERROR: clarivate_record_fetch — record_ids is required (non-empty array of refs or guids)." };
@@ -1262,10 +1256,10 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
1262
1256
  // names with per-office trademark counts, so the owner cross-check sweeps the applicant names the
1263
1257
  // register actually holds instead of a guess at the entity's styling.
1264
1258
  //
1265
- // THRESHOLD (a judgment call, recorded): accept confidenceScore >= 50. The probe's control returned one
1266
- // applicant styling at 74.0/156 marks, a second at 50.0/1, then 32/31/31 — a clean break below 50. The
1267
- // scores are the probe's; the names are not, and the fixture spells them PAKA IZHUSEDI C.V. and IZHUSEDI
1268
- // ATQUDCGOXET LIMITED (fixtures/README.md). Over-inclusive is the SAFE direction here: a
1259
+ // THRESHOLD (a judgment call, recorded): accept confidenceScore >= 50. The control that set it showed a
1260
+ // clean break at that value — the stylings above it held the portfolio, the ones below it held almost
1261
+ // nothing — rather than a gradient a cut-off would have to be invented for. The fixture's names are
1262
+ // invented and spelled out in fixtures/README.md. Over-inclusive is the SAFE direction here: a
1269
1263
  // surplus applicant name adds marks a lawyer can discard, a missing one is invisible.
1270
1264
  // And the expansion is strictly ADDITIVE — the caller's raw term is ALWAYS swept as well, so
1271
1265
  // resolution can only ever gain recall, never lose it (a failed or empty resolution degrades to exactly
@@ -1389,8 +1383,8 @@ export async function expandOwnerTerms(apiKey, base, params, tctx) {
1389
1383
  // against the provider's own owner vocabulary at all.
1390
1384
  //
1391
1385
  // That matters because APPLICANT_NAME EQUALS is not a full-string equality: a bare space in the value
1392
- // is an IMPLICIT AND over the tokens (a two-word owner term = 156 == that owner's
1393
- // full `… C.V.` styling = 156, and the mark field behaves the same way). So an un-resolved owner term is usually
1386
+ // is an IMPLICIT AND over the tokens, so a two-word owner term answers exactly what that owner's full
1387
+ // legal styling answers, and the mark field behaves the same way. So an un-resolved owner term is usually
1394
1388
  // BROADER than the register's own styling, not narrower — which is why this went unnoticed. But an AND
1395
1389
  // still requires EVERY token the caller wrote to appear in the applicant string, and a manifest names
1396
1390
  // an owner the way the world writes it, not the way the register spells it. One token the register does
@@ -56,4 +56,4 @@ and discloses, and never silently degrades into a weaker search wearing the righ
56
56
  `src/core.js`, beginning at `MATCH_MODE_PREFIX`: the match mode is a prefix character on a backtick-quoted
57
57
  clause, and the contract's match-mode predicates are those keys unchanged. `predicates.owner` is the
58
58
  exception — `owner:` is a real field clause, not a match mode. The model-facing operator vocabulary is in
59
- [`../../driver/skills/prelim-register/providers/corsearch.md`](../../driver/skills/prelim-register/providers/corsearch.md).
59
+ [`../../driver/skills/clearance-register/providers/corsearch.md`](../../driver/skills/clearance-register/providers/corsearch.md).
@@ -66,8 +66,8 @@ export const CAPABILITIES = Object.freeze({
66
66
  // this provider's vocabulary is ISO, where the EU is `EU`. territory-codes.mjs states the rule the
67
67
  // alias follows — "provider translate owns provider-specific spelling, e.g. EU→EM on clarivate" —
68
68
  // and the binding-layer pass in register-plan.mjs names the EU register `EM` because that is the
69
- // canonical office code, so without this the region clause would carry a value Corsearch answers
70
- // with an HTTP 500 rather than a 400 (the copper-bastion shape: a malformed region reads as
69
+ // canonical office code, so without this the region clause would carry a value this provider answers
70
+ // as a server error rather than a bad request (the copper-bastion shape: a malformed region reads as
71
71
  // transient and burns the park budget).
72
72
  translate: (code) => {
73
73
  const c = String(code ?? "").trim().toUpperCase();
@@ -114,9 +114,9 @@ export const CAPABILITIES = Object.freeze({
114
114
  // sent — the shared executor's script-form refusal (providers/_shared/script-form.mjs) is switched
115
115
  // off by this declaration, and switching it on would convert evidenced coverage into deferrals.
116
116
  //
117
- // Carried in driver/jx.mjs as the provider comparison: 小米 = 553 exact /
118
- // 127414 contains, 华威豹 = 6, 스타벅스 = 15 — where the same three terms answer 0/0/0 on clarivate.
119
- // Corroborated by archived executed bands, which returned non-zero hit counts on native characters
117
+ // Carried in driver/jx.mjs as the provider comparison: native-script terms answer here and answer
118
+ // nothing on a romanisation index, which is the whole of the difference between the two.
119
+ // Corroborated by archived executed bands, which returned records on native characters
120
120
  // across Han, Katakana, Cyrillic and Greek. Structurally corroborated too: `name` and
121
121
  // `nameTransliteration` are TWO SEPARATE fields on a row (core.js returns both), so `name:` holds the
122
122
  // mark AS FILED and the transliteration is an extra returned field, not the search key. assembleQuery
@@ -138,8 +138,8 @@ export function assembleQuery(p) {
138
138
  // Filters (each value must be backtick-quoted)
139
139
  if (Array.isArray(p.nice_classes)) for (const c of p.nice_classes) parts.push(clause("", "nice-class", c));
140
140
  if (Array.isArray(p.registries)) for (const r of p.registries) parts.push(clause("", "registry", r));
141
- // Regions go to the wire as CODES only (copper-bastion incident: Corsearch answers an unknown
142
- // multi-word region value with HTTP 500 — not a 400 — so a display name here poisons every retry).
141
+ // Regions go to the wire as CODES only (copper-bastion incident: an unknown multi-word region value
142
+ // is answered as a server error rather than a bad request, so a display name here poisons every retry).
143
143
  // Known display names are translated; Worldwide drops the clause; anything else fails loudly with
144
144
  // a message the composing model can act on in-session.
145
145
  if (Array.isArray(p.regions)) for (const r of p.regions) {
@@ -427,7 +427,7 @@ export async function doExpandPhoneme(sessionKey, params, tctx) {
427
427
  // brand-json hydrates ~100 candidate URIs in ONE POST with the screening data the thin search row lacks
428
428
  // (classes/status/owner/dates/jurisdictions/image/transliteration) — but NOT goodsAndServices. It replaces
429
429
  // the per-candidate record_fetch for the SCREENING majority (status/class/owner keep-or-drop); finalists +
430
- // any G&S-dependent decision still deep-fetch (the skill enforces that — skills/prelim-register/unit.md).
430
+ // any G&S-dependent decision still deep-fetch (the skill enforces that — skills/clearance-register/unit.md).
431
431
  // The tool is registered but left OUT of every agent's tools.allow until the live probe + Alex recall A/B +
432
432
  // sign-off (the GATE) — so it is inert today.
433
433