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
@@ -9,7 +9,8 @@ validated at parse.
9
9
  **One vendor, one billing mode, decided by the run and not by this directory.** The lanes go through
10
10
  `engine.runTurn()` — the same door all fourteen agentic stages use — via
11
11
  [`../../driver/engine/jx-turn.mjs`](../../driver/engine/jx-turn.mjs). Whatever program the customer configured
12
- (`CLEAROTRON_AI`) and whatever billing mode the run is on (API key or subscription) carries these calls too.
12
+ (`CLEAROTRON_AI`) and whatever billing mode the run is on (subscription, API key or, for Claude, a cloud account)
13
+ carries these calls too.
13
14
  That is the owner's standing rule — *one LLM provider only ever, API or auth, no mix* — and until 2026-08-20
14
15
  these three lanes were the one place in the product that broke it: they POSTed to the Anthropic Messages API on
15
16
  `ANTHROPIC_API_KEY` at a hardcoded cheap tier no matter what the rest of the run was doing.
@@ -106,15 +106,20 @@ export function envelopeFromTurnText(text, toolName) {
106
106
  * so the ORDER of those checks lives here once rather than three times.
107
107
  *
108
108
  * `turn` is supplied by the driver and is injectable for tests:
109
- * async ({prompt, kind}) => { ok, text, truncated, usage:{input,output}, model, vendor, authMode, cause }
109
+ * async ({prompt, kind}) => { ok, text, truncated, usage:{input,output}, model, vendor, authMode, cloud, cause }
110
110
  *
111
111
  * ATTRIBUTION RIDES EVERY RETURN, including the failures. A degrade still spent tokens, and without the
112
112
  * model, vendor and billing mode beside them they cannot be attributed in the run's rollup — which is
113
113
  * the half of that says a run must be able to state who did the work.
114
+ *
115
+ * `cloud` is part of that attribution: under the cloud billing mode it names the account that paid
116
+ * ("foundry", "vertex", "bedrock", "gateway"), and it is null under every other mode. The ledger stamp
117
+ * reads it from THIS return, so a field left off here reached every native-language record as
118
+ * `cloud: null` while the main steps of the same run said which cloud paid.
114
119
  */
115
120
  export async function runJxTurn({ body, turn, kind, started, truncatedCause, parse }) {
116
121
  const t0 = Number.isFinite(started) ? started : Date.now();
117
- const blank = { tookMs: 0, model: null, vendor: null, authMode: null, usage: null };
122
+ const blank = { tookMs: 0, model: null, vendor: null, authMode: null, cloud: null, usage: null };
118
123
  if (typeof turn !== "function") return { ok: false, cause: `${kind}: no turn runner was supplied`, ...blank };
119
124
 
120
125
  const { prompt, toolName } = promptFromRequest(body);
@@ -124,7 +129,7 @@ export async function runJxTurn({ body, turn, kind, started, truncatedCause, par
124
129
 
125
130
  const attribution = {
126
131
  tookMs: Date.now() - t0,
127
- model: r?.model ?? null, vendor: r?.vendor ?? null, authMode: r?.authMode ?? null,
132
+ model: r?.model ?? null, vendor: r?.vendor ?? null, authMode: r?.authMode ?? null, cloud: r?.cloud ?? null,
128
133
  // Passed through WHOLE, and `null` stays null. The driver hands over the engine contract's canonical
129
134
  // Usage ({input, output, cacheRead, cacheWrite, total}); re-shaping it to two fields here would drop
130
135
  // cache and total tokens from the rollup, and a zeroed object in place of null would report a
@@ -1,5 +1,13 @@
1
1
  # trademark-oauth-mcp-bridge
2
2
 
3
+ ## 0.3.2-beta.9
4
+
5
+ No changes in this release.
6
+
7
+ ## 0.3.2-beta.8
8
+
9
+ No changes in this release.
10
+
3
11
  ## 0.3.2-beta.7
4
12
 
5
13
  No changes in this release.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-oauth-mcp-bridge",
3
- "version": "0.3.2-beta.7",
3
+ "version": "0.3.2-beta.9",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
@@ -384,7 +384,7 @@ export function connotationQueriesOf(spec) {
384
384
  * Build the sandbox task from the DICTATED spec — the term and platform keys reach Perplexity's program
385
385
  * from code, never re-typed by the calling model (this is what kills re-typed-key corruption (VIBRANTE→VIBRNTE-class typos)).
386
386
  * CRITICAL: this carries the PINNED pplx_sdk access idiom (the same one in
387
- * skills/prelim-common-law/perplexity-prompts.md). Without it the sandbox agent free-styles the SDK and
387
+ * skills/clearance-common-law/perplexity-prompts.md). Without it the sandbox agent free-styles the SDK and
388
388
  * dies with "TypeError: 'pplx_sdk.WebHit' object is not iterable" on every cell (the teal-vault /
389
389
  * a 1-of-154 fold failure). The result object supports ITERATION + ATTRIBUTE
390
390
  * access only. The per-cell try/except keeps one bad cell from aborting the whole grid.
@@ -56,4 +56,4 @@ past `OWNER_SCOPED_WINDOW` (400 rows). Approximate totals saturate at 10000 and
56
56
  on which date, and what is still `null` on purpose. Then `src/core.js` for how a declared predicate
57
57
  reaches the wire — `toSignaParams` → `buildSearchRequest` — because "the vendor supports it" and "our
58
58
  executor sends it" are two different claims. The model-facing operator vocabulary lives in
59
- [`../../driver/skills/prelim-register/providers/signa.md`](../../driver/skills/prelim-register/providers/signa.md).
59
+ [`../../driver/skills/clearance-register/providers/signa.md`](../../driver/skills/clearance-register/providers/signa.md).
@@ -22,17 +22,16 @@
22
22
  // punctuation. Declaring it would be declaring a capability the executor cannot serve, which is
23
23
  // the one thing 's criteria forbid. Stays null; the slice defers, disclosed.
24
24
  // * resultCeiling — there is no single number to put here, and the reason is worth the paragraph.
25
- // A plain term exhausts 685 rows, a class-filtered one 539, an
26
- // unanchored `contains` 2047 — no ceiling. But add `filters.owner_name` and the SAME term stops
27
- // dead at 400: "This cursor points beyond the 400 result pagination window." The bound is in ROWS
28
- // rather than pages (at limit 25 it stopped after 375, before the page that would cross 400), and
29
- // it applies to the owner-scoped shape only.
25
+ // An unscoped term pages to exhaustion whatever its size, on every predicate — no ceiling. But add
26
+ // `filters.owner_name` and the SAME term stops dead at the owner-scoped window (400), which the
27
+ // vendor's own cursor refusal names. The bound is in ROWS rather than pages — the loop stops
28
+ // mid-page, before the page that would cross it — and it applies to the owner-scoped shape only.
30
29
  //
31
30
  // So the ceiling is a property of the QUERY SHAPE, not of the provider, and this field can hold
32
31
  // only one number for both. `null` with the fact written down beats 400, which would turn every
33
- // tractable band over 400 into a crowd and throw away the 2047 this vendor will happily page; and
34
- // it beats a number that is right for one shape and wrong for the other. See
35
- // OWNER_SCOPED_WINDOW below, which is the machine-readable half.
32
+ // tractable band over that window into a crowd and throw away the bands this vendor will happily
33
+ // page to exhaustion; and it beats a number that is right for one shape and wrong for the other.
34
+ // See OWNER_SCOPED_WINDOW below, which is the machine-readable half.
36
35
  //
37
36
  // The total is a separate matter: it saturates at 10000 and flags itself
38
37
  // approximate there — a fact about the count, not about the window.
@@ -107,10 +106,9 @@ export const CAPABILITIES = Object.freeze({
107
106
  // was none — the flag is now set by buildSearchRequest on EVERY call, where no call site can forget
108
107
  // it, because under this seam a response without a total cannot support a completeness claim.
109
108
  //
110
- // Nine queries return exact totals throughout (685, 220, 363, 830, 2047, 21, 101, 18),
111
- // and an empty band answering `total_count: 0, approximate: false` — an exact zero, the only kind
112
- // this repository may render. `limit: 1` returns the same total as the paged query, which is what
113
- // makes the cheap probe cheap.
109
+ // Every narrow band answers an exact total, and an empty band answers `total_count: 0,
110
+ // approximate: false` — an exact zero, the only kind this repository may render. `limit: 1` returns
111
+ // the same total as the paged query, which is what makes the cheap probe cheap.
114
112
  //
115
113
  // The vendor also flags some totals `total_count_approximate` — always at exactly 10000, so it is a
116
114
  // saturation marker rather than an estimate. normalizeSearchResponse reports those as UNKNOWN, never
@@ -175,10 +173,11 @@ export const CAPABILITIES = Object.freeze({
175
173
  // portfolio": that is `filters.owner_id`, which takes a resolved `own_…` id and 400s on an unknown
176
174
  // one rather than answering zero.
177
175
  //
178
- // A zero here is a real zero and not a filter failing silently. Verified by the case that looked
179
- // like one: `query: NIKE` × owner "Nike Innovate" × USPTO returns 0 while the bare owner returns
180
- // 1984 and the bare term 169 — because the exactly-NIKE USPTO marks are held by "NIKE, Inc.",
181
- // which does intersect (131). Both clauses are applied; the empty set is the answer.
176
+ // A zero here is a real zero and not a filter failing silently. Verified on the case that looked
177
+ // like one: a term × owner × territory intersection came back empty while the bare owner and the
178
+ // bare term each answered — because the register holds those marks under a differently styled
179
+ // applicant name, which the owner clause does reach. Both clauses are applied; the empty set is
180
+ // the answer.
182
181
  owner: "owner_name",
183
182
  }),
184
183
 
@@ -195,38 +194,32 @@ export const CAPABILITIES = Object.freeze({
195
194
 
196
195
  // ── WHICH BINDING LAYERS DOES A SEARCH SCOPED TO THIS OFFICE ACTUALLY RETURN? ──────────
197
196
  //
198
- // 's first task, answered for this provider by measurement rather than by reading. Same
199
- // query, same limit, three scopings of France:
197
+ // 's first task, answered for this provider by driving the three scopings against each other rather
198
+ // than by reading the documentation. One term, one limit, France:
200
199
  //
201
- // filters.offices: ["FR"] 19 rows — ALL FR/direct_national
202
- // filters.jurisdictions: ["FR"], territory_match direct 21 rows — FR national + WO/madrid_ir
203
- // filters.jurisdictions: ["FR"], territory_match protection
204
- // 101 rows — FR national + EM/direct_regional + WO
200
+ // filters.offices the national register ALONE
201
+ // filters.jurisdictions + territory_match direct national + the territory's Madrid legs
202
+ // filters.jurisdictions + territory_match protection national + REGIONAL + Madrid
205
203
  //
206
- // and the control that proves `protection` adds a REGIONAL layer rather than simply more rows:
207
- // Switzerland, which sits under no regional register, returns 47 either way.
204
+ // and the control that proves `protection` adds a LAYER rather than simply more rows: a territory
205
+ // that sits under no regional register answers the same either way.
208
206
  //
209
207
  // STAGE 2 MOVED THE TABLE, BECAUSE IT MOVED THE QUERY. `toSignaParams` now sends
210
208
  // `filters.jurisdictions` + `territory_match: "protection"` instead of `filters.offices`, so a
211
- // territory's whole stack of rights comes back in ONE call. The per-territory breakdown that
212
- // justifies every cell below — same term, `filing_route` asked directly rather than sampled:
209
+ // territory's whole stack of rights comes back in ONE call. Every cell below was then asked
210
+ // directly — `filing_route` per territory rather than sampled — and what that walk established is:
213
211
  //
214
- // national regional madrid national regional madrid
215
- // US 10000+ 0 2263 FR 6898 10000+ 2708
216
- // GB 10000+ 0 523 SE 1994 10000+ 2622
217
- // CH 2694 0 2254 EU 0 10000+ 2196
218
- // CA 9894 0 814 WO 0 0 9150
219
- // AU 10000+ 0 1734 SG 2899 0 1323
220
- // NO 1354 0 1149
212
+ // · every territory reaches its Madrid layer;
213
+ // · every EU member reaches the EU register, and no non-member does;
214
+ // · a `regional` absence is not a gap — no regional register binds Switzerland or Canada, so
215
+ // there is no regional layer for `bindingLayersFor` to ask about.
221
216
  //
222
- // A `regional` zero is not a gap: no regional register binds Switzerland or Canada, so there is no
223
- // regional layer for `bindingLayersFor` to ask about. Every territory reaches its Madrid layer, and
224
- // every EU member reaches the EU register — which is the whole of what Stage 2 asked for.
217
+ // Which is the whole of what Stage 2 asked for.
225
218
  //
226
219
  // WHAT THE OFFICE FILTER DID, kept because it is why the change was necessary and because the day
227
- // someone reverts the query shape this table becomes false rather than merely stale:
228
- // filters.offices: ["inpi-fr"] → national 6898, regional 0, MADRID 0. France's national register
229
- // alone, with the EU trade mark that blocks use in France sitting in a register nobody queried.
220
+ // someone reverts the query shape this table becomes false rather than merely stale: scoped to a
221
+ // national office, a France search returns that national register alone — with the EU trade mark
222
+ // that blocks use in France sitting in a register nobody queried.
230
223
  //
231
224
  // (The office filter is not uniform either — `uspto` and `ipi` return madrid-derived rows because
232
225
  // those registers hold the national legs of IRs themselves, while `inpi-fr` and `ukipo` return
@@ -267,12 +260,12 @@ export const CAPABILITIES = Object.freeze({
267
260
  // The owner surface exists (predicates.owner above) and composes with a text query in ONE request,
268
261
  // so the intersection is the vendor's rather than something reassembled here from two sweeps.
269
262
  //
270
- // EVIDENCED ON THE DETERMINISTIC SHAPE, AND ONLY THERE, on purpose. `match: contains` for a term
271
- // returned 2047; the same term with `filters.owner_name` returned 689 — a proper subset, which is
272
- // what an intersection has to be. The ranked shape does NOT show that: the same term under
273
- // `strategies` returned 238 alone and 1054 with an owner filter applied, i.e. MORE with the extra
274
- // clause. Recall under `similar` is not fixed, so ranked totals are not comparable across two
275
- // different requests and cannot evidence a set relation in either direction.
263
+ // EVIDENCED ON THE DETERMINISTIC SHAPE, AND ONLY THERE, on purpose. Under `match: contains`, adding
264
+ // `filters.owner_name` to a term returns a PROPER SUBSET of that term alone — which is what an
265
+ // intersection has to be. The ranked shape does NOT show that: under `strategies` the same pair
266
+ // answers MORE with the extra clause than without it. Recall under `similar` is not fixed, so ranked
267
+ // totals are not comparable across two requests and cannot evidence a set relation in either
268
+ // direction.
276
269
  //
277
270
  // Recorded because the ranked pair is the measurement a reader would take first, and taken alone it
278
271
  // would look like the filter being ignored — a wrong conclusion, reached from real numbers.
@@ -283,9 +276,9 @@ export const CAPABILITIES = Object.freeze({
283
276
  // reason it is a value and not still a null: fetch a record whose script is non-Latin, read its
284
277
  // mark text verbatim, search those exact characters back.
285
278
  //
286
- // record Singapore, `mark_text_script: "Hans"`, `mark_text_language: "zh"`, 8 characters
287
- // query the record's own mark_text, `match: "exact"`
288
- // result total_count 1, and the recalled id is the SAME record
279
+ // record one whose `mark_text_script` is non-Latin, read verbatim
280
+ // query that record's own mark_text, `match: "exact"`
281
+ // result it recalls itself — the same id comes back, and nothing else
289
282
  //
290
283
  // The characters are the index key, so `true`. providers/_shared/script-form.mjs may now send a
291
284
  // native-script slice here instead of deferring it.
@@ -297,7 +290,7 @@ export const CAPABILITIES = Object.freeze({
297
290
  // guessing at it here would put the shrug back in a different field.
298
291
  //
299
292
  // The blind spot to note for the reader who comes to widen this: the search ROW carries no script
300
- // field at all (38 keys); `mark_text_script` is a FULL-RECORD field (60 keys). A sweep over
293
+ // field at all; `mark_text_script` is a FULL-RECORD field. A sweep over
301
294
  // search rows therefore finds no scripts anywhere and reads as "the index holds none" — which is
302
295
  // how this probe failed on its first attempt, and it failed by returning an empty set, not an error.
303
296
  nativeScriptIndex: true,
@@ -117,8 +117,8 @@ const DETERMINISTIC_MATCH = new Set(["similar", "exact", "starts_with", "ends_wi
117
117
  // filter keys outright — `HTTP 400 Unrecognized key: status` — so every status-filtered Signa search
118
118
  // failed on the wire, and it survived review because the kernel does not pass one and no fixture
119
119
  // carried one. The real names are `status_primary` (pending|active|inactive|unknown) and
120
- // `status_stage`. `status_primary:["active"]` narrows 685 → 375; the bogus-key
121
- // control 400s identically, which is what proves the rejection is about the NAME and not the value.
120
+ // `status_stage`. `status_primary:["active"]` narrows a band as a status filter should; the bogus-key
121
+ // control is refused identically, which is what proves the rejection is about the NAME and not the value.
122
122
  //
123
123
  // Two branches building the same object by hand is what let one of them be wrong for two months, so
124
124
  // there is now one function and both branches call it.
@@ -136,7 +136,11 @@ function buildFilters(p) {
136
136
  }
137
137
 
138
138
  export function buildSearchRequest(p) {
139
- const body = { query: p.query };
139
+ // `query` is OMITTED, not sent undefined, when the entry is owner-only: an explicit `query: undefined`
140
+ // serializes away anyway, but writing it conditionally is what makes the owner-only shape legible here
141
+ // rather than an accident of JSON.stringify.
142
+ const body = {};
143
+ if (String(p.query ?? "").trim()) body.query = p.query;
140
144
  const match = typeof p.match === "string" ? p.match.trim() : "";
141
145
  if (match && DETERMINISTIC_MATCH.has(match)) {
142
146
  body.match = match; // sending strategies alongside is a 4xx, not a preference
@@ -240,7 +244,7 @@ export function normalizeRecord(rec, officeHint = null) {
240
244
  imageAvailable: rec.has_media ?? null,
241
245
  resolved_link: null, // Signa exposes no per-record public URL; renderer shows "verify at office"
242
246
  // ── WHICH LAYER THIS RIGHT SITS ON, carried as data ( →) ──────────────────────────
243
- // The normalizer read 18 of the 38 fields a search row carries. Among the 20 it dropped were the
247
+ // The normalizer read under half the fields a search row carries. Among the ones it dropped were the
244
248
  // four that say what KIND of right a record is — and those are not extras, they are the whole
245
249
  // vocabulary the binding-layer disclosure is written in. A France search that returns an EUTM and
246
250
  // a Madrid IR alongside French national marks could not say so, because the three arrived
@@ -281,7 +285,23 @@ export function normalizeRecord(rec, officeHint = null) {
281
285
 
282
286
  // Light search row (record_id + display fields). Signa returns FULL records on search, but we project a
283
287
  // lean row here and let record_fetch return the full normalized record (mirrors corsearch/clarivate).
288
+ //
289
+ // THIS ROW IS ALSO THE BAND ROW, so it carries the band contract's key names. `screenSource: "search-row"`
290
+ // means the kernel lands these rows in the named band as they are — there is no screen call to lift
291
+ // fields across — and every band consumer (band-shape, named-band, the digest, the placement form, the
292
+ // house-mark ownership check) reads `owner_name`, `classes` and `application_date`. This row used to carry
293
+ // only `owner`, `nice_classes` and `filing_date`, and `owner` read a flat `owner_name` the vendor does not
294
+ // send: the owner is `owners[].name`, exactly as normalizeRecord above already reads it. Every Signa
295
+ // record therefore reached the fold with no owner, every register placement failed
296
+ // `placement_owner_missing` in the render, and a pass that had tiered 141 in-class identical and
297
+ // near-identical records delivered none of them while reporting success.
298
+ //
299
+ // The signa-named keys stay: rowScreen and the search tool's own output read them.
284
300
  function normalizeSearchRow(rec) {
301
+ const owner0 = Array.isArray(rec.owners) ? rec.owners[0] : null;
302
+ const owner = rec.owner_name ?? owner0?.name ?? null;
303
+ const classes = normalizeClasses(rec);
304
+ const filed = toIso(rec.filing_date);
285
305
  return {
286
306
  record_id: rec.id ? makeRef(rec.jurisdiction_code || rec.office_code, rec.id) : null,
287
307
  id: rec.id,
@@ -289,9 +309,13 @@ function normalizeSearchRow(rec) {
289
309
  mark_text: rec.mark_text ?? null,
290
310
  status: pickStatusText(rec),
291
311
  status_class: statusClassOf(rec),
292
- nice_classes: normalizeClasses(rec),
293
- owner: rec.owner_name ?? null,
294
- filing_date: toIso(rec.filing_date),
312
+ nice_classes: classes,
313
+ classes,
314
+ owner,
315
+ owner_name: owner,
316
+ owner_country: owner0?.country_code ?? owner0?.country ?? null,
317
+ filing_date: filed,
318
+ application_date: filed,
295
319
  registration_date: toIso(rec.registration_date),
296
320
  relevance_score: Number.isInteger(rec.relevance_score) ? rec.relevance_score : null,
297
321
  raw: rec,
@@ -312,9 +336,9 @@ export function isSearchResponseBody(body) {
312
336
  // ── THE TOTAL, AND THE ONE CASE WHERE THE VENDOR'S NUMBER IS NOT A COUNT ───────────────────────────
313
337
  //
314
338
  // `options.include_total` returns `pagination.total_count` AND `pagination.total_count_approximate`.
315
- // Across nine queries every narrow band answers exact (685, 220, 363, 830, 2047,
316
- // 21, 101, 18) and — the case that matters — an empty band answered `total_count: 0, approximate:
317
- // false`, an EXACT zero, which is the only kind this repository is allowed to render.
339
+ // Every narrow band answers an exact total and — the case that matters — an empty band answers
340
+ // `total_count: 0, approximate: false`, an EXACT zero, which is the only kind this repository is
341
+ // allowed to render.
318
342
  //
319
343
  // An approximate total of exactly 10000 is a saturation marker rather than an estimate.
320
344
  // The vendor is saying "at least ten thousand", and it says so on the broad sweeps (a bare owner
@@ -419,8 +443,16 @@ function loadFixture(name) {
419
443
  // the roster unwidened, which is the direction to err in before an open-source cut. Hence a fallback
420
444
  // LIST rather than one hardcoded term.
421
445
  const DETERMINISTIC_FALLBACK_TERMS = ["nike", "swoosh"];
422
- function resolveSearchFixture({ query, strategies, match }) {
446
+ function resolveSearchFixture({ query, strategies, match, owner }) {
423
447
  const q = String(query || "").toLowerCase();
448
+ // AN OWNER-ONLY SEARCH HAS NO QUERY, so every lookup below — which keys on the term — would miss and
449
+ // the mock would answer "no fixture for query=". That is not a harmless gap: it is the reason the
450
+ // owner-only path could not be driven offline, and a test that cannot reach `doSearch` gets closed by
451
+ // asserting the element predicate instead, which passes while the request gate still refuses.
452
+ if (!q && String(owner || "").trim()) {
453
+ const o = String(owner).trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
454
+ return loadFixture(`signa-search-owner-${o}`) || loadFixture("signa-search-owner");
455
+ }
424
456
  const det = typeof match === "string" ? match.trim() : "";
425
457
  if (det) {
426
458
  return loadFixture(`signa-search-match-${det}-${q}`)
@@ -432,16 +464,54 @@ function resolveSearchFixture({ query, strategies, match }) {
432
464
  return null;
433
465
  }
434
466
 
467
+ // ── WHAT COUNTS AS SOMETHING TO SEARCH ────────────────────────────────────────────────────────────
468
+ //
469
+ // THE DEFECT THIS CLOSES. Two gates decided whether a plan entry had anything to search, and both
470
+ // answered on a free-text query or a names list alone. Neither counted an OWNER. An owner portfolio
471
+ // sweep carries an owner and no query by definition — that is what the shape is — so every one was
472
+ // refused before it was ever dispatched: `doSearch` returned "query is required", and the kernel's
473
+ // element gate returned the enumerate contract's missing-element error without calling the provider.
474
+ // The run continued and disclosed the slices as unsearched, so nothing failed and a client's report
475
+ // said the incumbent portfolios "could not be reached" when nothing had asked for them.
476
+ //
477
+ // It was never a capability gap. `buildFilters` already maps `owner` to this vendor's owner filter and
478
+ // its note names the three populations — term alone, OWNER ALONE, both — as genuinely different from
479
+ // each other. `ownerWindowCeiling` exists solely to cap paging for owner-scoped searches, which is not
480
+ // something written for a shape that cannot be dispatched, and it defers to `isOwnerScoped`, the
481
+ // KERNEL's own predicate, whose note names a bare-owner sweep as one of the two shapes it serves.
482
+ // Only the two element gates were never taught that an owner is an element.
483
+ //
484
+ // ONE DEFINITION, because this file already carries the lesson: `buildFilters` exists because two
485
+ // hand-written copies of the same object let one of them be wrong for two months. Two hand-written
486
+ // copies of "does this carry an element" would drift the same way, and the drift would be silent in
487
+ // the same direction — a shape one gate admits and the other refuses reads as a provider error.
488
+ export const hasSearchElement = (p) => Boolean(
489
+ String(p?.query ?? "").trim()
490
+ || (Array.isArray(p?.names) && p.names.filter(Boolean).length)
491
+ || (typeof p?.owner === "string" && p.owner.trim()));
492
+
493
+ /**
494
+ * What this request was FOR, for a log line and an error message. An owner-only search has no query,
495
+ * and "query=undefined" in a refusal is how a reader concludes the caller forgot one.
496
+ */
497
+ export const searchTargetLabel = (p = {}) => (String(p.query ?? "").trim()
498
+ ? `query=${p.query}`
499
+ : (typeof p.owner === "string" && p.owner.trim() ? `owner=${p.owner.trim()}` : "no element"));
500
+
501
+ /** The refusal, naming every element that would have been accepted. */
502
+ export const MISSING_ELEMENT_ERROR = "ERROR: signa_enumerate — a query, names[] or owner is required.";
503
+
435
504
  // ── Search ─────────────────────────────────────────────────────────────────────────────────────────
436
505
  export async function doSearch(apiKey, base, params, tctx, { mock = false } = {}) {
437
- if (!params.query) return { type: "text", text: "ERROR: query is required." };
506
+ // Refuses only when there is NOTHING to search. An owner alone is a search (see hasSearchElement).
507
+ if (!hasSearchElement(params)) return { type: "text", text: MISSING_ELEMENT_ERROR };
438
508
  if (mock) {
439
509
  const fx = resolveSearchFixture(params);
440
- if (!fx) return { type: "text", text: `ERROR (mock): no fixture for query=${params.query} strategy=${(params.strategies || ["exact"])[0]}` };
510
+ if (!fx) return { type: "text", text: `ERROR (mock): no fixture for ${searchTargetLabel(params)} strategy=${(params.strategies || ["exact"])[0]}` };
441
511
  return { type: "text", text: JSON.stringify({ mock: true, ...normalizeSearchResponse(fx, params.query) }, null, 2) };
442
512
  }
443
513
  const body = buildSearchRequest(params);
444
- const r = await signaFetch(apiKey, base, "/v1/trademarks", { method: "POST", body, tctx: { ...tctx, target: String(params.query).slice(0, 120) } });
514
+ const r = await signaFetch(apiKey, base, "/v1/trademarks", { method: "POST", body, tctx: { ...tctx, target: searchTargetLabel(params).slice(0, 120) } });
445
515
  if (!r.ok) {
446
516
  const msg = r.body?.error?.detail ?? r.body?.message ?? (r.raw ? r.raw.slice(0, 200) : "");
447
517
  return { type: "text", text: `ERROR: signa_search HTTP ${r.status}: ${msg}` };
@@ -451,11 +521,11 @@ export async function doSearch(apiKey, base, params, tctx, { mock = false } = {}
451
521
  // in exactly this case there is no second net. An unparsed 200 was the shortest path in the codebase
452
522
  // from a cut connection to `state:"enumerated"` with zero records. The guard is unchanged; its reason
453
523
  // is corrected — it used to rest on this provider having no count at all, which retired.
454
- if (r.parseError) return { type: "text", text: unparsedBodyError("signa_search", r, ` query=${String(params.query).slice(0, 120)}`) };
524
+ if (r.parseError) return { type: "text", text: unparsedBodyError("signa_search", r, ` ${searchTargetLabel(params).slice(0, 120)}`) };
455
525
  // Parsing is not answering: a 200 carrying an error envelope is the same shortest path one JSON
456
526
  // envelope away — no data[], zero rows, the loop ends, enumerated. See isSearchResponseBody.
457
527
  if (!isSearchResponseBody(r.body)) {
458
- return { type: "text", text: nonAnswerBodyError("signa_search", r, "a search response (no data[] — the key every /v1/trademarks answer carries)", ` query=${String(params.query).slice(0, 120)}`) };
528
+ return { type: "text", text: nonAnswerBodyError("signa_search", r, "a search response (no data[] — the key every /v1/trademarks answer carries)", ` ${searchTargetLabel(params).slice(0, 120)}`) };
459
529
  }
460
530
  return { type: "text", text: JSON.stringify(normalizeSearchResponse(r.body, params.query), null, 2) };
461
531
  }
@@ -528,14 +598,14 @@ export function toSignaParams(p = {}) {
528
598
  // in the French register, and was never searched.
529
599
  //
530
600
  // `filters.jurisdictions` + `territory_match: "protection"` asks the territory question instead:
531
- // every right with effect there, whatever register it sits on. Same term:
532
- //
533
- // filters.offices: ["inpi-fr"] national 6898, regional 0, madrid 0
534
- // filters.jurisdictions: ["FR"], protection national 6898, regional 10000+, madrid 2708
601
+ // every right with effect there, whatever register it sits on. On the same term, an office-scoped
602
+ // France search returns the national register and NOTHING else — no EU right, no Madrid leg — while
603
+ // the protection-scoped one returns the national rows unchanged plus both of the layers the first
604
+ // never saw.
535
605
  //
536
- // and the control that shows `protection` adds a layer rather than merely more rows: Switzerland,
537
- // under no regional register, returns regional 0 either way while its madrid layer arrives all the
538
- // same. Every one of the eleven covered territories reaches its Madrid layer this way; the EU
606
+ // and the control that shows `protection` adds a LAYER rather than merely more rows: a territory
607
+ // under no regional register gains no regional rows either way, while its Madrid layer arrives all
608
+ // the same. Every one of the eleven covered territories reaches its Madrid layer this way; the EU
539
609
  // members additionally reach the EU register.
540
610
  //
541
611
  // IT IS ONE CALL, NOT THREE. No extra queries, no extra spend — which is why this half of Stage 2
@@ -552,7 +622,7 @@ export function toSignaParams(p = {}) {
552
622
  // caller that translates before handing params in — the shape a probe or a driver adapter naturally
553
623
  // takes — gets it applied twice. On the second pass `regions` is still present, so an earlier draft
554
624
  // re-added `offices` beside the `jurisdictions` it had just produced. The API accepts both and the
555
- // office filter WINS: a France order went back to 19 national rows from 101, with `territory_match:
625
+ // office filter WINS: a France order went back to the national rows alone, with `territory_match:
556
626
  // "protection"` sitting in the body doing nothing. The expansion silently undid itself and the run
557
627
  // reported `state: "enumerated"` either way. Caught by comparing before/after on a live call and
558
628
  // finding them identical — the one check that could see it.
@@ -662,9 +732,9 @@ export function toSignaParams(p = {}) {
662
732
  /**
663
733
  * THE ONE QUERY SHAPE WITH A NARROWER RESULT WINDOW.
664
734
  *
665
- * `filters.owner_name` caps paging at 400 ROWS on this vendor; nothing else does. Same term,
666
- * same term and limit: `exact` exhausted at 685, `contains` at 2047, and the owner-scoped one stopped
667
- * dead — "This cursor points beyond the 400 result pagination window."
735
+ * `filters.owner_name` caps paging at 400 ROWS on this vendor; nothing else does. On one term at one
736
+ * limit, both unscoped predicates paged to exhaustion and the owner-scoped one stopped dead at the
737
+ * window, which the vendor's own cursor refusal names.
668
738
  *
669
739
  * `null` for every other shape, and that is the load-bearing half. Returning 400 across the board would
670
740
  * turn every tractable band over 400 into a sanctioned crowd — an UNDER-SEARCH wearing a crowd
@@ -684,8 +754,10 @@ const { enumerate: __enumerate } = makeEnumerate({
684
754
  // own count/search reconciliation exists to adjudicate on the "endpoint" seam. One source, one
685
755
  // number. (`makeCountProbe` throws unless "endpoint" supplies one, so this is asserted, not assumed.)
686
756
  count: null,
687
- hasAnyElement: (p) => Boolean(String(p?.query ?? "").trim() || (Array.isArray(p?.names) && p.names.length)),
688
- missingElementError: "ERROR: signa_enumerate — a query (or names[]) is required.",
757
+ // The SAME predicate `doSearch` uses, not a second copy of it. An owner is an element here: see
758
+ // hasSearchElement for the defect this closes and why one definition rather than two.
759
+ hasAnyElement: hasSearchElement,
760
+ missingElementError: MISSING_ELEMENT_ERROR,
689
761
  capabilities: { ...CAPABILITIES.kernel },
690
762
  ceilingFor: ownerWindowCeiling,
691
763
  // The kernel's default cheap-probe params are CORSEARCH'S — `{limit:1, fields:["uri"]}` — and `uri`
@@ -807,6 +879,11 @@ export async function doCountHits(apiKey, base, params, tctx, { mock = false } =
807
879
  text: JSON.stringify({
808
880
  total_hits: exact ? parsed.total_hits : null,
809
881
  total_approximate: parsed.total_approximate === true,
882
+ // THE FLOOR TRAVELS AS DATA, not only inside the note below. The normalizer already keeps it —
883
+ // `total_floor` on the parsed body — and it was reaching the driver in prose only, so the one
884
+ // number that makes a saturated band answerable had to be read back out of a sentence to be used.
885
+ // A reader that parses it out of the note is a reader that breaks when the note is reworded.
886
+ total_floor: Number.isFinite(parsed.total_floor) ? parsed.total_floor : null,
810
887
  present: exact ? parsed.total_hits > 0 : ((parsed.results?.length ?? 0) > 0 || parsed.has_more === true),
811
888
  seen: parsed.results?.length ?? 0,
812
889
  note: exact
@@ -155,7 +155,7 @@ what an index actually holds, including whether the 1884 backfile is in it (a da
155
155
  identical on every other number and is missing a century).
156
156
 
157
157
  This replaces only the *register* half of a clearance
158
- — the reasoning engine still needs its own subscription or API key, and the unregistered-use half
158
+ — the reasoning engine still needs its own subscription, API key or cloud account, and the unregistered-use half
159
159
  still wants `PERPLEXITY_API_KEY`.
160
160
 
161
161
  **It refuses rather than answering zero.** An absent index, a schema with no rows, or an index whose
@@ -4,7 +4,7 @@
4
4
  //
5
5
  // ── THE CONTRACT, AND HOW MUCH OF IT IS VERIFIED ─────────────────────────────────────────────────
6
6
  // The endpoints, the auth header and the product id below are NOT inherited by analogy from another
7
- // provider (see skills/prelim-register/providers/README.md on why that rule exists). They come from
7
+ // provider (see skills/clearance-register/providers/README.md on why that rule exists). They come from
8
8
  // USPTO's own OpenAPI description of the Open Data Portal, cross-checked against live probes from this
9
9
  // machine:
10
10
  //
package/scripts/README.md CHANGED
@@ -20,6 +20,7 @@ The axis is *what the script assumes exists*, not where it happens to be run.
20
20
  | `report-print-check.mjs` | Shows what the exported PDF actually renders under print media. Run by CI. |
21
21
  | `report-overflow-check.mjs` | The same two report lanes SIDEWAYS, under screen media — the print check cannot see a width, because print sets the watermark to `display:none` and the watermark was the overflow. Run by CI. |
22
22
  | `portal-lifecycle-check.mjs` | Asks whether the portal names a brand owner and whether a person can manage their searches. Run by CI. |
23
+ | `portal-strings.mjs` | Reads every string the portal can show out of its source, as a reader meets it. `--check <approved list>` refuses a string on a screen that is neither approved nor in `portal-ui/strings-backlog.json`; `--backlog-shrinks --base <ref>` refuses a backlog line the base did not have. The approved list is kept with the designs, outside this repository. CI runs the backlog half. |
23
24
  | `validate-profiles.mjs` | Validates a customer-config store's profile bundles. Read-only. |
24
25
  | `record-carry-probe.mjs` | Traces retrieval → findings over any finished run dir. Read-only. |
25
26
  | `placement-diff.mjs` | Shows what the placement tier did between two runs of one matter. |