clearotron 0.3.3 → 0.4.0-beta.1

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 (142) hide show
  1. package/.env.example +9 -0
  2. package/INSTALL.md +1 -14
  3. package/bin/brandowner.mjs +5 -5
  4. package/bin/connect.mjs +4 -4
  5. package/bin/onboard.mjs +5 -7
  6. package/bin/start.mjs +20 -5
  7. package/bin/update.mjs +6 -1
  8. package/build-info.json +2 -2
  9. package/docs/architecture/04-configuration-reference.md +2 -6
  10. package/driver/CHANGELOG.md +28 -0
  11. package/driver/ask-ledger.mjs +2 -2
  12. package/driver/band-shape.mjs +8 -8
  13. package/driver/blind-frame-model.mjs +1 -1
  14. package/driver/common-law-receipts.mjs +2 -2
  15. package/driver/commonlaw-carry.mjs +2 -2
  16. package/driver/company-bundle.mjs +11 -18
  17. package/driver/connotation-search.mjs +4 -4
  18. package/driver/contract-e3-backlog.mjs +3 -3
  19. package/driver/declination-call.mjs +1 -1
  20. package/driver/declination-tool.mjs +1 -1
  21. package/driver/dev-portal.mjs +1 -1
  22. package/driver/door-call-verdict.mjs +27 -0
  23. package/driver/driver.config.mjs +17 -11
  24. package/driver/e2e/README.md +1 -1
  25. package/driver/engine/mcp/clarivate-server.mjs +2 -1
  26. package/driver/engine/mcp/corsearch-server.mjs +1 -0
  27. package/driver/engine/mcp/euipo-server.mjs +1 -0
  28. package/driver/engine/mcp/free-tier-server.mjs +1 -1
  29. package/driver/engine/mcp/gather-config.mjs +1 -1
  30. package/driver/engine/mcp/perplexity-server.mjs +1 -1
  31. package/driver/engine/mcp/recording-server.mjs +1 -1
  32. package/driver/engine/mcp/signa-server.mjs +1 -0
  33. package/driver/engine/mcp/supplemental.mjs +1 -1
  34. package/driver/engine/mcp/uspto-local-server.mjs +1 -0
  35. package/driver/enqueue-schema.mjs +2 -2
  36. package/driver/feedback-issues.mjs +1 -1
  37. package/driver/feedback-store.mjs +1 -1
  38. package/driver/findings-model.mjs +3 -3
  39. package/driver/flag-snapshot.mjs +1 -1
  40. package/driver/floor-duty.mjs +2 -2
  41. package/driver/form-neighbourhood.mjs +47 -15
  42. package/driver/frame-diff-model.mjs +3 -3
  43. package/driver/gateway.mjs +5 -5
  44. package/driver/jx-lanes.mjs +1 -1
  45. package/driver/known-conflicts.mjs +18 -0
  46. package/driver/log.mjs +2 -2
  47. package/driver/package.json +1 -1
  48. package/driver/pipeline-knockout.mjs +129 -95
  49. package/driver/pipeline.mjs +69 -32
  50. package/driver/placement-carry.mjs +2 -2
  51. package/driver/placement-form.mjs +1 -1
  52. package/driver/portal-mcp-client.mjs +1 -1
  53. package/driver/portal-request-origin.mjs +79 -0
  54. package/driver/portal-service.mjs +45 -17
  55. package/driver/predelivery-lint.mjs +10 -10
  56. package/driver/profile-page.html +9 -13
  57. package/driver/profile-service.mjs +25 -13
  58. package/driver/profiles.mjs +17 -4
  59. package/driver/progress.mjs +1 -1
  60. package/driver/provider-usage.mjs +24 -1
  61. package/driver/publish/index.mjs +17 -5
  62. package/driver/publish/knockout.mjs +3 -2
  63. package/driver/publish/render-knockout.mjs +1 -1
  64. package/driver/publish/render.mjs +14 -3
  65. package/driver/publish/search-depth.mjs +4 -2
  66. package/driver/recall-reconciliation.mjs +1 -1
  67. package/driver/record-carry.mjs +6 -6
  68. package/driver/recording-agreement.mjs +2 -2
  69. package/driver/reference-score.mjs +27 -27
  70. package/driver/register-count.mjs +56 -1
  71. package/driver/register-digest-record.mjs +1 -1
  72. package/driver/register-plan.mjs +5 -5
  73. package/driver/register-records.mjs +10 -1
  74. package/driver/registry-fidelity.mjs +4 -4
  75. package/driver/repair-composers.mjs +6 -6
  76. package/driver/run-economics.mjs +8 -19
  77. package/driver/screen-gate.mjs +1 -1
  78. package/driver/skills/blind-frame/SKILL.md +2 -2
  79. package/driver/skills/clearance-common-law/SKILL.md +2 -2
  80. package/driver/skills/clearance-common-law/perplexity-prompts.md +4 -4
  81. package/driver/skills/clearance-register/digest.md +1 -1
  82. package/driver/skills/clearance-register/unit.md +1 -1
  83. package/driver/skills/clearance-search/report-prose.md +5 -5
  84. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  85. package/driver/skills/clearance-variants/SKILL.md +2 -2
  86. package/driver/skills/clearance-variants/transliteration-scripts.md +1 -1
  87. package/driver/skills/frame-diff/SKILL.md +2 -2
  88. package/driver/skills/knockout-assess/SKILL.md +9 -9
  89. package/driver/skills/matter-frame/watchlist-reference.md +1 -1
  90. package/driver/skills/narrative-refutation/SKILL.md +2 -2
  91. package/driver/skills/placement-inquiry/SKILL.md +1 -1
  92. package/driver/stage-context.mjs +4 -4
  93. package/driver/stages.mjs +23 -15
  94. package/driver/suite-census.json +138 -42
  95. package/driver/systemd/clearotron-client-mcp.service +24 -0
  96. package/driver/systemd/clearotron-mcp-face.service +24 -0
  97. package/driver/systemd/clearotron-portal.service +24 -0
  98. package/driver/systemd/clearotron-worker.service +24 -0
  99. package/driver/tokens.mjs +26 -17
  100. package/driver/turnaround-bands.mjs +1 -1
  101. package/driver/unit-inventory.mjs +3 -3
  102. package/driver/variant-manifest-model.mjs +1 -1
  103. package/driver/verify.mjs +2 -2
  104. package/driver/whatif-memo-run.mjs +1 -1
  105. package/mcp-server/CHANGELOG.md +8 -0
  106. package/mcp-server/lib/audit.mjs +9 -2
  107. package/mcp-server/lib/http-handler.mjs +7 -3
  108. package/mcp-server/mint-token.mjs +8 -6
  109. package/mcp-server/package.json +1 -1
  110. package/mcp-server/server.mjs +11 -1
  111. package/package.json +1 -1
  112. package/portal-ui/dist/assets/{index-GBbbyQxc.js → index-D_O_55vK.js} +59 -9
  113. package/portal-ui/dist/index.html +1 -1
  114. package/portal-ui/package.json +1 -1
  115. package/providers/_shared/README.md +1 -1
  116. package/providers/_shared/answer-memory.mjs +199 -0
  117. package/providers/_shared/ledger-path.mjs +1 -1
  118. package/providers/_shared/ledger.mjs +47 -5
  119. package/providers/_shared/script-form.mjs +24 -5
  120. package/providers/_shared/term-shape.mjs +5 -5
  121. package/providers/clarivate/src/capabilities.js +11 -0
  122. package/providers/clarivate/src/core.js +140 -13
  123. package/providers/jx-subclass/lookup.mjs +1 -1
  124. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  125. package/providers/oauth-mcp-bridge/package.json +1 -1
  126. package/providers/signa/src/capabilities.js +24 -0
  127. package/providers/signa/src/core.js +66 -0
  128. package/scripts/deprecate-below.mjs +114 -2
  129. package/scripts/freeze-example-run.mjs +1 -1
  130. package/scripts/live-surface-check.mjs +11 -2
  131. package/scripts/release-entry-catch-up.mjs +211 -0
  132. package/scripts/release-note-required.mjs +102 -6
  133. package/scripts/release-rehearsal-version.mjs +60 -0
  134. package/scripts/release-sbom.mjs +104 -0
  135. package/scripts/release-visible-check.mjs +7 -5
  136. package/scripts/score.mjs +3 -3
  137. package/shared/brand.mjs +1 -1
  138. package/shared/client-door.mjs +15 -8
  139. package/shared/driver-dir.mjs +20 -9
  140. package/shared/names-in-force.mjs +2 -0
  141. package/shared/scope.mjs +25 -10
  142. package/shared/store-in-repo.mjs +38 -17
@@ -9,7 +9,7 @@
9
9
  // can never drift and a knockout never reads the clearance composer's module state.
10
10
  import { readFileSync, writeFileSync, copyFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
- import { driverDir, ensureDriverDir } from '../../shared/driver-dir.mjs'; //
12
+ import { driverDir, ensureDriverDir, RUN_DIR_MODE } from '../../shared/driver-dir.mjs'; //
13
13
  import { riskTier, TONE_TIER, regenIndex, regenSurfaces, auditRouteFor, markReportRouteFor, reportRouteFor } from './index.mjs';
14
14
  import { runKnockoutLint, deliveryFlagLines } from '../predelivery-lint.mjs';
15
15
  import { note } from '../log.mjs';
@@ -334,7 +334,8 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
334
334
  // assumed: `urls: 0` in the receipt below means no finding cited anything.
335
335
  note(`knockout receipts: ${receipts.checked.urls} citation(s) across ${receipts.checked.findings} finding(s) on ${receipts.checked.citing}/${receipts.checked.marks} mark(s) traced to held evidence`);
336
336
  const poolRunDir = join(poolRoot, runId);
337
- mkdirSync(poolRunDir, { recursive: true });
337
+ // Owner and group only: a mode given to mkdir, which keeps the set-GID the pool root passes down.
338
+ mkdirSync(poolRunDir, { recursive: true, mode: RUN_DIR_MODE });
338
339
  const writeRO = (name, data) => { const p = join(poolRunDir, name); writeFileSync(p, data); grpRead(p, 0o640); };
339
340
 
340
341
  // ── THE RUN'S WORKING RECORD TRAVELS WITH THE REPORT ──────────────────────────────────────────────
@@ -536,7 +536,7 @@ function countsSection(marks, registerCounts, positions = '') {
536
536
  // Close variations — did not say what they counted, so the definition had to go somewhere. Put it in
537
537
  // the header and the paragraph has nothing left to do.
538
538
  //
539
- // ONE NAME ⇒ THE HEADER NAMES IT, because "Exactly ORBIT" needs no gloss at all. Several names share
539
+ // ONE NAME ⇒ THE HEADER NAMES IT, because "Exactly ACME" needs no gloss at all. Several names share
540
540
  // one table and no header can name one of them, so they keep the general form and each row's own
541
541
  // forms line (already rendered, unchanged) carries that row's near-spellings.
542
542
  const single = marks.length === 1 ? String(marks[0]?.name ?? '').trim() : '';
@@ -70,6 +70,9 @@ let CASE_LAW_BY_ORD = new Map(); // E5: ordinal → grounded case-law profile
70
70
  // is full-country only), so suppressing there would take case-law off the page with nothing left
71
71
  // saying so. Absence of a state is not a statement that nothing was found.
72
72
  let COURT_DECISIONS = null;
73
+ // The company picked no marketplaces (search-depth counts.sweep): the use-check's nothing-found line names the
74
+ // general web and the stores chosen for the matter instead of a marketplace search. Set per render.
75
+ let NO_MARKETPLACES_PICKED = false;
73
76
  let ENFORCER_SIGNALS = new Map(); // E6: registration uri (lowercase) → {aggression, oppositions, owner}
74
77
  // WP-receipts W2 — per-render provider record-link origin + label, resolved by publish from the run's
75
78
  // OWN _driver/receipts.json provider (never the currently-configured provider — a re-published archive
@@ -552,6 +555,9 @@ const useEvidence = (m) => [USE_EVIDENCE_LABEL[m?._status], USE_SOURCE_LABEL[m?.
552
555
  // runs carry the old value forever and a fourth spelling of it would have to be accepted everywhere.
553
556
  const USE_CHECK_NO_RESULT = 'perplexity_research — no result';
554
557
  const USE_CHECK_NO_RESULT_CITE = 'Nothing found in the marketplaces searched.';
558
+ // The same "searched, nothing found" line for a run whose company picked no marketplaces (the owner's
559
+ // ruling of 2026-09-23): there was no marketplace search to name, and the general web still ran.
560
+ const USE_CHECK_NO_RESULT_CITE_NO_MARKETPLACES = 'Nothing found in the general web search or in any store chosen for this matter.';
555
561
  // — MATCHED ON NORMALISED PUNCTUATION, NOT ONE SPELLING. The constant itself does not
556
562
  // move (archived runs carry it forever, the validators name it), but the SEAT emitted a hyphen where
557
563
  // the doctrine writes an em dash, and exact equality let the raw tool name through to a delivered
@@ -1222,7 +1228,9 @@ function whatWasSearchedSection(opts, coverage = [], findings = [], recordsByUri
1222
1228
  }
1223
1229
  const sw = c.sweep || {};
1224
1230
  if (sw.spellings) rows.push(['Spellings searched', String(sw.spellings)]);
1225
- if (sw.checks) rows.push(['Marketplace and web', `${sw.checks.toLocaleString('en-GB')} checks on ${sw.platforms} platforms`]);
1231
+ if (sw.checks) rows.push(['Marketplace and web', sw.noMarketplacesPicked
1232
+ ? `${sw.checks.toLocaleString('en-GB')} checks: the general web, plus any stores chosen for this matter.`
1233
+ : `${sw.checks.toLocaleString('en-GB')} checks on ${sw.platforms} platforms`]);
1226
1234
  if (sw.reputation) rows.push(['Reputation and meaning', `${sw.reputation.toLocaleString('en-GB')} checks`]);
1227
1235
  rows.push(['Local-script spellings', c.localScriptSearched ? 'Searched' : 'Not searched']);
1228
1236
  // HOW DEEP THE LOCAL-LANGUAGE INVESTIGATION WENT, which is a different question from the row above it.
@@ -1803,7 +1811,9 @@ function fullDetail(f, card, recordsByUri = new Map()) {
1803
1811
  const useStatus = useEvidence(f.meters?.use);
1804
1812
  // D7 — the code-owned "searched, nothing found" sentinel becomes client words HERE, by exact
1805
1813
  // equality against the one constant. Any other value is a source string and rides through untouched.
1806
- const useSrc = isUseCheckNoResult(f.use_check?.source) ? USE_CHECK_NO_RESULT_CITE : f.use_check?.source;
1814
+ const useSrc = isUseCheckNoResult(f.use_check?.source)
1815
+ ? (NO_MARKETPLACES_PICKED ? USE_CHECK_NO_RESULT_CITE_NO_MARKETPLACES : USE_CHECK_NO_RESULT_CITE)
1816
+ : f.use_check?.source;
1807
1817
  // NO EVIDENCE TAG ON AN EMPTY RESULT. The line read "Use checked. Marketplace search run — no result
1808
1818
  // found. Evidence: inferred", and "inferred" beside "no result" reads as a contradiction: it qualifies
1809
1819
  // how a FINDING was established, and there is no finding here. Nothing was found, and that is the
@@ -2276,7 +2286,7 @@ ${EXPORT_MENU_JS}`;
2276
2286
  // Render a parsed report + findings.json to a full self-contained HTML string.
2277
2287
  // A1 — famous-neighbour context notes: knowledge-cited references kept for diligence (digest.md's "never
2278
2288
  // dropped" rule) that carry NO fetched register record, so they are NOT findings and do NOT score. Rendered
2279
- // on BOTH the internal and client report (legitimate completeness — "we checked CHROME, it isn't a
2289
+ // on BOTH the internal and client report (legitimate completeness — "we checked NOVAPULSO, it isn't a
2280
2290
  // conflict"), clearly marked as not-a-conflict so a famous neighbour is surfaced, never silently lost.
2281
2291
  // WP-56 B2 — the standing "mark itself" section: typed mark_assessment (findings-model, both parsers) →
2282
2292
  // code render at the TOP of the page (after the hero, before the conflict landscape), on THE report.
@@ -2511,6 +2521,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
2511
2521
  SCOPE_WORLDWIDE = opts.scopeBasis === 'worldwide' ? true : null; // the plan's scope_basis; null ⇒ fall back to the ledger-prose sniff
2512
2522
  CASE_LAW_BY_ORD = opts.caseLawByOrdinal instanceof Map ? opts.caseLawByOrdinal : new Map(); // T7 (E5)
2513
2523
  COURT_DECISIONS = opts.searchDepth?.counts?.courtDecisions ?? null; // gates the card's case-law strand
2524
+ NO_MARKETPLACES_PICKED = opts.searchDepth?.counts?.sweep?.noMarketplacesPicked === true;
2514
2525
  ENFORCER_SIGNALS = new Map((Array.isArray(opts.enforcerSignals) ? opts.enforcerSignals : []).map((e) => [String(e.uri ?? '').toLowerCase(), e])); // T7 (E6)
2515
2526
  RECORD_ORIGIN = opts.recordOrigin ?? null; // WP-receipts W2
2516
2527
  // — `null` and `` mean DIFFERENT things and the render must not collapse them. `` is an
@@ -275,7 +275,7 @@ export function localScriptSearched(registerPlan) {
275
275
  *
276
276
  * @returns {{schemaVersion: number, cleared: object, counts: object}}
277
277
  */
278
- export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNames = [], bandRecordIds = null, commonLawGrid = null, caseLawText = "", registerPlan = null, laneDepthVerdicts = null } = {}) {
278
+ export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNames = [], bandRecordIds = null, commonLawGrid = null, caseLawText = "", registerPlan = null, laneDepthVerdicts = null, noMarketplacesPicked = false } = {}) {
279
279
  // `recordFileNames: null` travels all the way to the page — see recordsByCountry. The default stays `[]`
280
280
  // because that is "the caller said nothing", not "the store is absent"; only the publish path knows the
281
281
  // difference and it is the one producer.
@@ -295,7 +295,9 @@ export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNa
295
295
  counts: {
296
296
  recordsByCountry: recordsByCountry(recordFileNames),
297
297
  recordsRead: recordFileNames === null ? null : recordFileNames.length,
298
- sweep: sweepCounts(commonLawGrid, auditMd),
298
+ // The company picked no marketplaces: the report says the general web ran plus any stores chosen for
299
+ // the matter. Stamped only when true, so every other run's record is unchanged.
300
+ sweep: { ...sweepCounts(commonLawGrid, auditMd), ...(noMarketplacesPicked ? { noMarketplacesPicked: true } : {}) },
299
301
  localScriptSearched: localScriptSearched(registerPlan),
300
302
  courtDecisions: courtDecisionsState(caseLawText),
301
303
  // `localScriptSearched` above answers whether the spellings were searched; this answers how deep
@@ -30,7 +30,7 @@
30
30
  // The first cut had two contracts joined only by prose: digest.md told the digest to write ONE
31
31
  // Sheet-1 row per POSITION (the exact-identity collapse), while this join credited an ending only to
32
32
  // the URIs literally cited, with no notion of positions. Proven on real records: /mark/cn/CHINIC4DC…
33
- // and /mark/cn/CHINIC7788… (both TIKI TWIST, NORTHCOMEX USA LLC, class 32, REGISTERED) are ONE
33
+ // and /mark/cn/CHINIC7788… (both WAVO TWIST, NORTHCOMEX USA LLC, class 32, REGISTERED) are ONE
34
34
  // position AND both sit in the code-ranked top slice — a compliant digest writing one position row
35
35
  // citing the senior URI left the other unended and the delivery died. The only thing standing
36
36
  // between a correct report and a blocked run was a prose clause ("the URI cell listing EVERY
@@ -10,7 +10,7 @@
10
10
  // `register-named-band.json`, all ten screened `surface:in-scope-live`, and not one of them appears in
11
11
  // `placements.json` or in `findings.json`. The delivered report never names the token. The record was
12
12
  // retrieved, screened and banded, and then it was gone, and NOTHING anywhere recorded why. The same
13
- // shape had already been paid for four times (TIKI TWIST / TIKI TROPICS on R3, DELPHIC / OSLER DELPHI
13
+ // shape had already been paid for four times (WAVO TWIST / WAVO TROPICS on R3, KORPHIC / HALVER KORPHI
14
14
  // on R2). A capability that retrieves and cannot deliver is indistinguishable, in the report, from one
15
15
  // that was never built — and the two have completely different fixes.
16
16
  //
@@ -842,10 +842,10 @@ export function silentlyLostFindings({ reconciliation = null, carryRows = null,
842
842
  *
843
843
  * MEASURED ON A DELIVERED R2 RUN, 2026-09-06. The sibling ran and reported
844
844
  * `{checked:5, matched:5, lost:0}` — correctly. On that same delivery two marks from the lawyer's final
845
- * list, `OSLER DELPHI` and `DELFITY`, one rated HIGH, are absent from `findings.json`. They were dropped
845
+ * list, `HALVER KORPHI` and `KORFITY`, one rated HIGH, are absent from `findings.json`. They were dropped
846
846
  * WITH a reason, so they sat outside the sibling's population by design:
847
847
  *
848
- * IMMATERIAL ask:recall:recall-osler-delphi: … — OSLER DELPHI / Osler Diagnostics Limited is
848
+ * IMMATERIAL ask:recall:recall-halver-korphi: … — HALVER KORPHI / Halver Diagnostics Limited is
849
849
  * already reasoned on the incumbent sheet in register-findings.md.
850
850
  *
851
851
  * WHY THE STATED CASE IS THE MORE DANGEROUS ONE. A silent drop leaves a hole. A stated drop leaves a
@@ -886,11 +886,11 @@ export function statedDivergenceFindings({ reconciliation = null, carryRows = nu
886
886
  // statedDivergenceFindings checked=5 matched=5 diverged=0 ← should have named two marks
887
887
  //
888
888
  // The reconciliation names five finding-ended positions and they are five OTHER marks — VELTRIN
889
- // bioenergetische Kosmetik, DELPHIC HSE, DELPHI, DELPHIN & EMERENCE, DELPHI DIAGNOSTICS. The two that
889
+ // bioenergetische Kosmetik, KORPHIC HSE, KORPHI, DELPHIN & EMERENCE, KORPHI DIAGNOSTICS. The two that
890
890
  // were lost sit in the CARRY rows and the reconciliation never mentions them:
891
891
  //
892
- // OSLER DELPHI reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
893
- // DELFITY reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
892
+ // HALVER KORPHI reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
893
+ // KORFITY reach=placed stopped_at=digest reason_source=step-stated reason=digest:reasoned-negative
894
894
  //
895
895
  // The unit arms all passed because their fixtures put the mark in BOTH populations, which the real run
896
896
  // does not. That is the lesson worth keeping: a fixture that satisfies two joins at once cannot tell
@@ -205,7 +205,7 @@ export function agreementFindings({ stage, granted, artifacts, union, toolUniver
205
205
  if (!PHASES.includes(m.phase)) {
206
206
  throw new Error(`recording-agreement: instruction member "${m.surface}" carries phase `
207
207
  + `${JSON.stringify(m.phase)} — every INSTRUCTION member must declare ${ATTEMPT_1} or ${REPAIR}, `
208
- + "because direction (a) counts only what the seat reads on attempt 1 (#1190)");
208
+ + "because direction (a) counts only what the seat reads on attempt 1");
209
209
  }
210
210
  }
211
211
  const attempt1 = instructions.filter((m) => m.phase === ATTEMPT_1);
@@ -240,7 +240,7 @@ export function agreementFindings({ stage, granted, artifacts, union, toolUniver
240
240
  why: `${stage} holds ${tool} and no attempt-1 instruction names it. The seat is not told the `
241
241
  + "capability exists, so it reaches for whatever the doctrine DOES name — which after a "
242
242
  + "conversion is a tool its grant no longer carries. Name it in the dispatch or in the skill doc, "
243
- + "or drop it from the grant. A repair rung naming it does NOT count (#1190): the seat that "
243
+ + "or drop it from the grant. A repair rung naming it does NOT count: the seat that "
244
244
  + "needed to know had already acted by the time it read one.",
245
245
  });
246
246
  }
@@ -117,11 +117,11 @@ export function labelTokens(label) {
117
117
  *
118
118
  * The two separator classes do different work and conflating them is the bug worth stating. `/ , · & |`
119
119
  * separate ALTERNATIVES for the same record: `VENZAL / VENZALMONO / VENZALKOMB` is one relabelled entry,
120
- * and `CHROMA / & Device` is one mark plus a device note. Whitespace and hyphens separate WORDS WITHIN
121
- * one name: `TIKI TWIST` is not `TIKI`.
120
+ * and `LUMIVANE / & Device` is one mark plus a device note. Whitespace and hyphens separate WORDS WITHIN
121
+ * one name: `WAVO TWIST` is not `WAVO`.
122
122
  *
123
123
  * Treating a word separator as an alias separator makes every multi-word mark match its own first word —
124
- * so a reference `TIKI` would claim the run's `TIKI TWIST`, and a genuinely withheld mark would be
124
+ * so a reference `WAVO` would claim the run's `WAVO TWIST`, and a genuinely withheld mark would be
125
125
  * reported as found. That is a false clean on the exact pair this scorer was built for.
126
126
  */
127
127
  /*
@@ -151,7 +151,7 @@ export function labelAliases(label) {
151
151
  .filter(Boolean);
152
152
  }
153
153
 
154
- /** The consonant skeleton is only discriminating once there is enough of it. TIKI and TIKA are both "tk". */
154
+ /** The consonant skeleton is only discriminating once there is enough of it. WAVO and WAVA are both "wv". */
155
155
  const MIN_SKELETON = 4;
156
156
 
157
157
  // asked for a minimum ALIAS length, or a stated reason there is none. THERE IS NONE, deliberately.
@@ -190,20 +190,20 @@ const MIN_SKELETON = 4;
190
190
  * can tell an exact CJK match from an owner-gated one.
191
191
  * 2. ALIAS. Any alias of one label equals any alias of the other, compared as whole names. This is what
192
192
  * makes `VENZAL` match `VENZAL / VENZALMONO / VENZALKOMB` — the relabelling an exact diff misreads
193
- * as a drop plus a find — while keeping `TIKI` and `TIKI TWIST` apart.
193
+ * as a drop plus a find — while keeping `WAVO` and `WAVO TWIST` apart.
194
194
  * 3. SKELETON. Consonant skeletons of 4+ characters agree. Reaches the one-vowel spelling pairs a variant
195
- * sweep must not split, without letting short marks collide — 4 is the floor because TIKI and TIKA
195
+ * sweep must not split, without letting short marks collide — 4 is the floor because WAVO and WAVA
196
196
  * both skeletonise to "tk".
197
197
  * 4. CONTAINED — ONLY when the caller has established that both sides carry the SAME OWNER.
198
198
  * One label's word sequence sits contiguously inside the other's. Rules 2 and 3 handle a reference
199
199
  * mark being an ALIAS of, or a spelling neighbour of, a surfaced one; neither handles it being
200
- * CONTAINED in one, and that is how `DELPHI GENETICS` and `DG DELPHI GENETICS` — identical owner,
200
+ * CONTAINED in one, and that is how `KORPHI GENETICS` and `DG KORPHI GENETICS` — identical owner,
201
201
  * one record — were filed as a `lost` and a `noise` on the same run. A record cannot be both never
202
202
  * retrieved and surfaced-but-unknown, and the entry it split was the reference's highest-risk one.
203
203
  *
204
- * READ WITH RULE 2, NOT AGAINST IT (2026-08-14). Rule 2's block says the alias rule "keeps `TIKI`
205
- * and `TIKI TWIST` apart", and this rule joins exactly that pair under a matched owner —
206
- * `matchesReference("TIKI", "TIKI TWIST", {sameOwner:true})` returns `contained`. Both are right
204
+ * READ WITH RULE 2, NOT AGAINST IT (2026-08-14). Rule 2's block says the alias rule "keeps `WAVO`
205
+ * and `WAVO TWIST` apart", and this rule joins exactly that pair under a matched owner —
206
+ * `matchesReference("WAVO", "WAVO TWIST", {sameOwner:true})` returns `contained`. Both are right
207
207
  * and they read as contradictory side by side. Rule 2 asks whether two labels are the SAME NAME,
208
208
  * where a word separator must never collapse a longer mark into its first word. Rule 4 asks a
209
209
  * different question that only an established owner match makes answerable: whether ONE PROPRIETOR
@@ -211,7 +211,7 @@ const MIN_SKELETON = 4;
211
211
  * resolving the apparent conflict by narrowing either one would break the case it was built for.
212
212
  *
213
213
  * OWNER IDENTITY IS THE DISCRIMINATOR, and it is the whole reason this rule is safe. Containment on
214
- * its own would pull `Delphi Pharmaceuticals` and `Delphi Laboratories` into `found` and manufacture
214
+ * its own would pull `Korphi Pharmaceuticals` and `Korphi Laboratories` into `found` and manufacture
215
215
  * recall the run does not have — five such marks sit in this very scenario's own results. Gated on
216
216
  * `ownersMatch`, it reaches exactly the case it is for: one proprietor, one record, two renderings.
217
217
  * The caller establishes the owner agreement; this function never guesses it.
@@ -577,8 +577,8 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
577
577
  // This was `retrieved.find(heldRule)` — the FIRST match in band order, with nothing preferring the
578
578
  // entry's own proprietor. Measured on a 2026-08-27 test run against its lawyer reference: of eight entries,
579
579
  // five matched more than one band record and three cited the wrong company. For two of those three
580
- // the RIGHT record was already in the match set and was passed over on position alone — DELPHIC's at
581
- // index 1, DELPHYS's at index 3.
580
+ // the RIGHT record was already in the match set and was passed over on position alone — KORPHIC's at
581
+ // index 1, VELTRYS's at index 3.
582
582
  //
583
583
  // The looseness of the matcher is NOT the fault here and is deliberately left alone. Only one of the
584
584
  // three wrong citations came from a skeleton collision; the other two were `alias` matches — the
@@ -598,8 +598,8 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
598
598
  const entryNamesOwner = Boolean(ownerKey(e?.owner));
599
599
  if (heldAll.length) {
600
600
  // AMONG THE OWNER'S OWN RECORDS, PREFER THE CLOSEST NAME. `heldOwned[0]` is band order again, one
601
- // level down: DELPHIC's owner holds several records and the first is `DELPHIC ADAPTABLE`, while the
602
- // lawyer named plain `DELPHIC`. Right proprietor, wrong record of theirs. An `alias` rule is an
601
+ // level down: KORPHIC's owner holds several records and the first is `KORPHIC ADAPTABLE`, while the
602
+ // lawyer named plain `KORPHIC`. Right proprietor, wrong record of theirs. An `alias` rule is an
603
603
  // identity match on the name; `skeleton` and `contained` are near-forms. Take an identity match
604
604
  // when the owner has one.
605
605
  const strongest = (rows) => rows.find((r) => heldRule(r) === "alias") ?? rows.find((r) => heldRule(r) === "script") ?? rows[0] ?? null;
@@ -681,7 +681,7 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
681
681
  // proprietor with more than one mark. A large filer can perfectly well have one mark the run withheld
682
682
  // and a DIFFERENT mark, not in the reference, that it surfaced. Both rows are true.
683
683
  //
684
- // It fired on a delivered R2 run: `<large filer>: reference "DELFITY" is withheld, surfaced "DELPHINA"
684
+ // It fired on a delivered R2 run: `<large filer>: reference "KORFITY" is withheld, surfaced "KORPHINA"
685
685
  // is noise`. Different marks, different records, one proprietor that files a great many. And because
686
686
  // score.mjs prints a collision as "do not read the recall numbers above", ONE such proprietor
687
687
  // suppressed the whole run's recall measurement — a real 88% → 63% movement went unquoted on the
@@ -701,10 +701,10 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
701
701
  //
702
702
  // So the predicate is deliberately weaker than the matcher and stronger than the owner: same owner
703
703
  // AND one mark contained in the other once normalised. That is the shape of the case this check was
704
- // built for — `DELPHI GENETICS` in LOST beside `DG DELPHI GENETICS` in NOISE — and it is not the
705
- // shape of `DELFITY` beside `DELPHINA`.
704
+ // built for — `KORPHI GENETICS` in LOST beside `DG KORPHI GENETICS` in NOISE — and it is not the
705
+ // shape of `KORFITY` beside `KORPHINA`.
706
706
  const collisionKey = (s) => String(s ?? "").normalize("NFKC").toUpperCase().replace(/[^A-Z0-9]/g, "");
707
- // A FLOOR, because containment on a short string matches everything. `DEL` inside `DELPHINA` is not
707
+ // A FLOOR, because containment on a short string matches everything. `KOR` inside `KORPHINA` is not
708
708
  // evidence of a shared record; four characters is the shortest reference mark shape worth trusting
709
709
  // here, and a pair below it drops to the advisory rather than being dropped entirely.
710
710
  const CONTAIN_FLOOR = 4;
@@ -783,7 +783,7 @@ export function withheldScope({ reference = [], retrieved = [], registerOnly = f
783
783
  : !marks.length
784
784
  ? `${scopeOf} — and the retrieved corpus is EMPTY, so this number rests on nothing; it is not a clean result`
785
785
  : outside
786
- ? `${scopeOf} — ${outside} other retrieved mark${outside === 1 ? "" : "s"} are outside this measure entirely (#1322)`
786
+ ? `${scopeOf} — ${outside} other retrieved mark${outside === 1 ? "" : "s"} are outside this measure entirely`
787
787
  : `${scopeOf} — all ${marks.length} retrieved mark${marks.length === 1 ? " is" : "s are"} named by the reference, so nothing sits outside it`;
788
788
  return { referenceEntries: n, retrievedMarks: marks.length, outside, note };
789
789
  }
@@ -859,7 +859,7 @@ export function scoreField({ reference = [], findings = [] }) {
859
859
  if (e.on_field !== true) continue;
860
860
  const label = e.mark ?? e.name ?? "";
861
861
  // — the SAME question axis A asks, owner agreement included. Left out, this axis reported
862
- // `DELPHI GENETICS` as "not-surfaced — cannot be scored on field" on the very run where axis A had
862
+ // `KORPHI GENETICS` as "not-surfaced — cannot be scored on field" on the very run where axis A had
863
863
  // just matched it. One record, two axes, two answers is the defect this issue names, and the axis
864
864
  // that cannot see the finding is the one that decides whether its GOODS were routed correctly.
865
865
  const f = findings.find((x) => matchesReference(label, x.mark, { sameOwner: ownersMatch(e?.owner, x?.owner) }));
@@ -1515,7 +1515,7 @@ export function concludeDepth({ instructed = [], rows = [], resolved = true, why
1515
1515
  *
1516
1516
  * Common and Inherited are excluded from the run and every run must contain a real LETTER, so an
1517
1517
  * accented Latin mark in NFD (`CAFE` + U+0301, whose combining mark is Inherited) is not a script
1518
- * segment, and neither is punctuation, a device note or a digit. `CHROMA / & Device` yields nothing;
1518
+ * segment, and neither is punctuation, a device note or a digit. `LUMIVANE / & Device` yields nothing;
1519
1519
  * `色度 / SEDU` yields the segment the jx lane has to generate. PURE.
1520
1520
  */
1521
1521
  export function scriptSegments(label) {
@@ -1534,8 +1534,8 @@ export function scriptSegments(label) {
1534
1534
  /**
1535
1535
  * CORPORATE LEGAL FORMS, DROPPED FROM AN OWNER KEY ONLY —.
1536
1536
  *
1537
- * `NOISE_TOKENS` covers US, UK and German forms and almost nothing else, so `BePharBel Manufacturing`
1538
- * and `BePharBel Manufacturing, Société anonyme` read as two companies. Measured: of twenty common forms
1537
+ * `NOISE_TOKENS` covers US, UK and German forms and almost nothing else, so `DuPharVel Manufacturing`
1538
+ * and `DuPharVel Manufacturing, Société anonyme` read as two companies. Measured: of twenty common forms
1539
1539
  * appended to an otherwise identical name, NINETEEN broke the match — only `S.A.` survived, and only
1540
1540
  * because `sa` happens to be on that list.
1541
1541
  *
@@ -1608,11 +1608,11 @@ export function ownerKey(owner) {
1608
1608
  const n = ownerName(owner);
1609
1609
  if (!n) return null;
1610
1610
  // — DROP A TRAILING PARENTHETICAL ANNOTATION. A gold set is lawyer-typed, and the convention in
1611
- // it is a jurisdiction hint after the name: `Delphi Genetics S.A. (BX)`, `Delphi Diagnostics, Inc.
1611
+ // it is a jurisdiction hint after the name: `Korphi Genetics S.A. (BX)`, `Korphi Diagnostics, Inc.
1612
1612
  // (US)` — three of R2's nine entries carry one and six do not, which is what makes it an annotation
1613
1613
  // rather than part of any name. A run finding carries the typed owner object and never one of these,
1614
1614
  // so the strict token-set equality below could not match the two sides of the SAME proprietor, and
1615
- // `Delphi Genetics S.A.` scored against `Delphi Genetics S.A. (BX)` as a different company.
1615
+ // `Korphi Genetics S.A.` scored against `Korphi Genetics S.A. (BX)` as a different company.
1616
1616
  //
1617
1617
  // ONE trailing group, and only at the end. This is not a general parenthesis fold: a parenthetical
1618
1618
  // inside a name is part of the name, and `Shanghai <A> Network Technology` vs `Shanghai <B> Network
@@ -1911,7 +1911,7 @@ export function scoreScriptTargets({ reference = [], buckets = {}, findings = []
1911
1911
  // ── — A KNOCKOUT IS GRADED ON WHAT A KNOCKOUT PROMISES ─────────────────────────────────────────
1912
1912
  //
1913
1913
  // R3 and R4 are knockout scenarios and their gold sets are clearance-grade lawyer reviews listing
1914
- // SIMILAR marks — TIKI PUNCH, TIKI TROPICS — which a count of the exact string and its close variations
1914
+ // SIMILAR marks — WAVO PUNCH, WAVO TROPICS — which a count of the exact string and its close variations
1915
1915
  // can never retrieve. The 2026-08-12 round scored them 0/8 and 0/9 on BOTH free-tier and clarivate, same
1916
1916
  // day, same engine. That zero is baked in by the product definition, and it costs twice: the two
1917
1917
  // cheapest scenarios cannot detect a recall regression because they are already at the floor, and every
@@ -301,7 +301,7 @@ function cellSettled(cell, predicate, forms = null) {
301
301
  export async function countRegisterHits({
302
302
  marks, classes = null, jurisdictions = null, provider, capabilities,
303
303
  counter, concurrency = 3, ledgerPath = null, prior = null, now = () => new Date(),
304
- variantCap = VARIANT_CAP, unreachable = [],
304
+ variantCap = VARIANT_CAP, unreachable = [], listed = null,
305
305
  }) {
306
306
  const { regions: coveredRegions, deferred, worldwide } = resolveRegions(jurisdictions, capabilities);
307
307
  // — the office split the plan lane does at compile, done here, because this lane compiles no
@@ -318,6 +318,7 @@ export async function countRegisterHits({
318
318
  ? { counted: regions, uncounted: dropped.map((d) => ({ office: d.office, memberId: d.memberId, missing: [...(d.missing ?? [])] })) }
319
319
  : null;
320
320
  const priorByName = new Map((prior?.marks ?? []).map((m) => [String(m.name), m]));
321
+ const answeredByListing = listingAnswers(listed, { regions });
321
322
  const batchClasses = (Array.isArray(classes) ? classes : []).filter((n) => Number.isInteger(n));
322
323
 
323
324
  const rows = await runBatched(marks ?? [], concurrency, async (m) => {
@@ -326,11 +327,25 @@ export async function countRegisterHits({
326
327
  const scoped = own.length ? own : batchClasses;
327
328
  const reused = priorByName.get(name);
328
329
  const variants = variantForms(name, { cap: variantCap });
330
+ // The listing's answers for THIS mark, used only when it was asked over the same classes — see
331
+ // listingAnswers for why a different scope is a different question.
332
+ const listedForMark = answeredByListing?.get(name.toLowerCase()) ?? null;
333
+ const fromListing = listedForMark && sameScope(listedForMark.classes, scoped.length ? scoped : null) ? listedForMark.terms : null;
329
334
 
330
335
  // ONE PROBE = ONE PROVIDER CALL = ONE LEDGER LINE. Extracted so the aggregate predicate below bills,
331
336
  // records and degrades through exactly the same path as the two simple ones — a second copy of this
332
337
  // for the variant loop is how the two would come to disagree about what a failure means.
333
338
  const probe = async (term, p, { form = null } = {}) => {
339
+ // ANSWERED BY THE LISTING, and not asked again. The listing ran this exact question — the
340
+ // provider's exact predicate, this term, these classes, these territories — and its answer carried
341
+ // the register's total. No call is made, so no receipt line is written; the cell says where its
342
+ // figure came from.
343
+ const listedAnswer = p.matchMode === "exact" ? fromListing?.get(String(term).trim().toUpperCase()) : null;
344
+ if (listedAnswer) {
345
+ return listedAnswer.approximate === true
346
+ ? { total: null, approximate: true, floor: listedAnswer.floor ?? null, source: "listing" }
347
+ : { total: listedAnswer.total, source: "listing" };
348
+ }
334
349
  const started = Date.now();
335
350
  let r;
336
351
  try { r = await counter(term, p, { classes: scoped, regions }); }
@@ -493,6 +508,46 @@ export async function countRegisterHits({
493
508
  };
494
509
  }
495
510
 
511
+ /**
512
+ * The listing's answers, by mark and by term, for the count lane to take instead of asking again.
513
+ *
514
+ * WHY A LISTING CAN ANSWER A COUNT. The identical and close columns are the provider's exact predicate
515
+ * over a term, and the knockout's listing (register-records.mjs) asks that same predicate over the same
516
+ * terms. On a provider whose search answer carries the register's own total, the listing therefore
517
+ * already holds the count, and asking again is a second bill for a figure in hand.
518
+ *
519
+ * ONLY THE SAME QUESTION. A term is taken only when the listing answered it (asked, answered, with a
520
+ * total or a disclosed approximation), and only when the territories match here and the classes match
521
+ * per mark (the caller checks those with `sameScope`). A term the listing did not reach — the cap, a
522
+ * failure — is counted by the count lane as it always was. `containing` is never answered here: the
523
+ * listing never asks it. Terms are keyed upper-case, because register-variants.mjs asks them upper-case
524
+ * and name predicates are case-insensitive on every wired provider.
525
+ *
526
+ * Null when there is no listing: the caller passes one only on a register that declares
527
+ * `listingAnswersCount` (pipeline-knockout.mjs). PURE.
528
+ */
529
+ export function listingAnswers(listed, { regions = [] } = {}) {
530
+ if (!listed || !Array.isArray(listed.marks)) return null;
531
+ if (!sameScope(listed.scope?.regions ?? null, regions.length ? regions : null)) return null;
532
+ const byMark = new Map();
533
+ for (const m of listed.marks) {
534
+ const terms = new Map();
535
+ for (const t of Array.isArray(m?.terms) ? m.terms : []) {
536
+ if (t?.ok !== true || t.notAsked) continue;
537
+ if (!Number.isFinite(t.total) && t.approximate !== true) continue;
538
+ terms.set(String(t.term ?? "").trim().toUpperCase(), t);
539
+ }
540
+ byMark.set(String(m?.name ?? "").trim().toLowerCase(), { classes: Array.isArray(m?.classes) ? m.classes : null, terms });
541
+ }
542
+ return byMark;
543
+ }
544
+
545
+ /** Two scopes are the same question when they list the same values in the same order, or are both unset. */
546
+ export function sameScope(a, b) {
547
+ const norm = (v) => (Array.isArray(v) && v.length ? v.map(String) : null);
548
+ return JSON.stringify(norm(a)) === JSON.stringify(norm(b));
549
+ }
550
+
496
551
  /** How many marks got at least ONE number. Zero of them means the product did not happen. */
497
552
  export function countedMarks(doc) {
498
553
  return (doc?.marks ?? []).filter((m) => COUNT_PREDICATES.some((p) => Number.isFinite(m?.counts?.[p.key]?.total))).length;
@@ -336,7 +336,7 @@ const NEGATIVE_COLUMNS = Object.freeze(["Mark", "Search Term / Variant", "Result
336
336
  *
337
337
  * The office comes from the record uri (`/mark/wo/…` -> `WO`); every uri in the archived corpus carries
338
338
  * a two-letter office in that position. When it cannot be read the BARE MARK is printed rather than a
339
- * broken qualifier — a missing qualifier is a smaller defect than "OSLER DELPHI — UNDEFINED record".
339
+ * broken qualifier — a missing qualifier is a smaller defect than "HALVER KORPHI — UNDEFINED record".
340
340
  */
341
341
  export function negativeMarkCell(row) {
342
342
  const mark = String(row?.cells?.mark ?? "").trim();
@@ -319,7 +319,7 @@ export function variantTermIssue(value) {
319
319
  // over marks that may exist. Worse, the disclosure rides on THIS verdict — a null here means no
320
320
  // deferred row either, so the nil search shipped as a clean with nothing saying otherwise.
321
321
  //
322
- // The floor is still doing real work and stays: `DOLPHIN DEVICE` is two words and a perfectly good
322
+ // The floor is still doing real work and stays: `PANGOLIN DEVICE` is two words and a perfectly good
323
323
  // term, and refusing ordinary two-word marks is the failure this arm must not cause. What separates
324
324
  // them is not length, it is the ANNOTATION — and an annotation always has a remedy (delete the
325
325
  // note, keep the term), which is why hoisting THIS arm is safe where hoisting the length arm above
@@ -1067,8 +1067,8 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1067
1067
  // lookup collapses to one entry and hands every term the first romanisation in the manifest.
1068
1068
  // - the PREVIOUS formKey (NFKD + strip ALL combining marks) was subtler and worse (2026-07-30
1069
1069
  // review, proven by repro): in most non-Latin scripts a combining mark selects WHICH LETTER
1070
- // this is, so mark-distinguished siblings — ティキスラッシュ (TIKI SURASSHU) and ディキスラッシュ
1071
- // (DIKI SURASSHU), Thai vowel signs, Devanagari matras, Arabic diacritics — keyed identically
1070
+ // this is, so mark-distinguished siblings — ワボスラッシュ (WABO SURASSHU) and ワホスラッシュ
1071
+ // (WAHO SURASSHU), Thai vowel signs, Devanagari matras, Arabic diacritics — keyed identically
1072
1072
  // and a variant silently received its SIBLING's romanisation. The provider then executed a
1073
1073
  // look-alike query and recorded state:enumerated while the dictated form was never searched
1074
1074
  // anywhere: the exact silent-wrong-query false-clean class this carriage fix exists to kill,
@@ -1137,8 +1137,8 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1137
1137
  };
1138
1138
 
1139
1139
  // ── A1, fixed at the EMITTER (the freeze-lint below must never refuse the compiler's own output) ──
1140
- // The 2026-07-28 plan carried {predicate:"exact", term:"TIKI*"} ×4: wildcard-shaped VARIANT values
1141
- // (`TIKI*`, `*TIKI`, `SLUSH*`, `*SLUSH`) paired with the hardcoded exact — dispatched literally,
1140
+ // The 2026-07-28 plan carried {predicate:"exact", term:"WAVO*"} ×4: wildcard-shaped VARIANT values
1141
+ // (`WAVO*`, `*WAVO`, `SLUSH*`, `*SLUSH`) paired with the hardcoded exact — dispatched literally,
1142
1142
  // returned 0, shipped as schema-level confident cleans. A manifest value with an ANCHORED star is a
1143
1143
  // wildcard pattern and compiles to the wildcard predicate (whose per-anchor capability check then
1144
1144
  // stamps `unsupported` on a provider that lacks that anchor — the honest deferred row, decided by the
@@ -247,6 +247,7 @@ export async function listRegisterRecords({
247
247
  ts: now().toISOString(), stage: "records", mark: name, term: t.term, basis: t.basis,
248
248
  classes: scoped, regions, provider, ok, requested: want, fetched,
249
249
  total: Number.isFinite(r?.total) ? r.total : null, took_ms: Date.now() - started,
250
+ ...(ok && r?.approximate === true ? { approximate: true, floor: Number.isFinite(r?.floor) ? r.floor : null } : {}),
250
251
  ...(ok ? {} : { cause: String(r?.reason ?? "unknown").slice(0, 300) }),
251
252
  }) + "\n");
252
253
  } catch { /* receipts are best-effort, never fatal */ }
@@ -256,6 +257,9 @@ export async function listRegisterRecords({
256
257
  // How many the register HOLDS under this term, where it said. `fetched` under `total` is the
257
258
  // truncation, and it is stated rather than left for a reader to notice.
258
259
  total: Number.isFinite(r?.total) ? r.total : null,
260
+ // An approximation is carried as one, exactly as the count lane records it: no number in
261
+ // `total`, the register's floor beside it. Only a provider whose listing states it sets it.
262
+ ...(ok && r?.approximate === true ? { approximate: true, floor: Number.isFinite(r?.floor) ? r.floor : null } : {}),
259
263
  ...(ok ? {} : { reason: String(r?.reason ?? "the filings could not be fetched").slice(0, 300) }),
260
264
  };
261
265
  });
@@ -269,7 +273,12 @@ export async function listRegisterRecords({
269
273
  // The three numbers a reader needs to trust the list: how many are here, how many the register
270
274
  // said there are under the listed terms, and whether the cap cut it.
271
275
  fetched: records.length,
272
- available: termRows.reduce((n, t) => (Number.isFinite(t.total) ? n + t.total : n), 0),
276
+ // NO SUM WHEN ANY ANSWERED TERM IS APPROXIMATE. An approximation carries a floor and no number, so a
277
+ // sum around it counts only the exact terms: a name the register answered as more than 10,000 read
278
+ // "out of 800 hits" beside its two close forms. With no honest total none is stated (recordsLine
279
+ // drops the clause for a non-number), and the counts line still carries the approximation.
280
+ available: termRows.some((t) => t.ok && t.approximate === true) ? null
281
+ : termRows.reduce((n, t) => (Number.isFinite(t.total) ? n + t.total : n), 0),
273
282
  capped: capped || records.length >= markCap,
274
283
  cap: markCap,
275
284
  // — which registers this listing covers, present only when one was dropped. Per-mark for the
@@ -467,7 +467,7 @@ export const REC = {
467
467
  isLive: (r) => r.statusClass === "live" || /\b(valid|live|registered)\b/i.test(String(r.onomaticsStatus ?? r.corsearchStatusCode ?? "")),
468
468
  statusStr: (r) => String(r.statusText ?? r.onomaticsStatus ?? r.corsearchStatusCode ?? r.statusClass ?? ""),
469
469
  // doc-31 step 4: the proprietor/applicant name as the record holds it — the AUTHORITATIVE owner display, so a
470
- // model-typed/​invented variant ("Lo.Li. Pharma International" for a record that says "Lo.Li. Pharma S.r.l.")
470
+ // model-typed/​invented variant ("Be.Ma. Pharma International" for a record that says "Be.Ma. Pharma S.r.l.")
471
471
  // never becomes the card's owner. Provider-blind: every normalizer writes `owner` (legacy: ownerName/proprietor).
472
472
  owner: (r) => String(r.owner ?? r.ownerName ?? r.proprietor ?? "").trim(),
473
473
  // — the owner's name in its ORIGINAL script, when the record draws that distinction. Its presence
@@ -658,7 +658,7 @@ export function joinEvidenceStatus(findings, recordsByUri = new Map(), fetchFail
658
658
  // weight, and it happened silently over the seat's correct answer on four of five rows of one
659
659
  // delivered report: classifyUseSource needs the full de-suffixed owner token as a contiguous
660
660
  // substring of the host, and real brand domains are shorter than corporate names
661
- // ("propperdocs" is not inside "propperai"). What survives exactly: the register-mirror
661
+ // ("acmedocs" is not inside "acmeai"). What survives exactly: the register-mirror
662
662
  // demotion's precedence — attested OR host-detected, a mirror wins over everything, because
663
663
  // that direction can only weaken the evidence.
664
664
  const host = /^https?:\/\//i.test(f.use_check.source) ? classifyUseSource(f.use_check.source, f.owner?.name) : null;
@@ -1028,8 +1028,8 @@ export function bindFindingsToRecords(findings, recordsByUri) {
1028
1028
  }
1029
1029
  // ── the owner's name: one asserted fact, one presentation choice ──────────────────────────
1030
1030
  //
1031
- // doc-31 step 4 binds owner.name from the record so a model-typed variant ("Lo.Li. Pharma
1032
- // International" for a record that says "Lo.Li. Pharma S.r.l.") never becomes the card's owner. That
1031
+ // doc-31 step 4 binds owner.name from the record so a model-typed variant ("Be.Ma. Pharma
1032
+ // International" for a record that says "Be.Ma. Pharma S.r.l.") never becomes the card's owner. That
1033
1033
  // still holds, and the test that pins it is unchanged.
1034
1034
  //
1035
1035
  // What it got wrong is the CJK case. The provider's Latin field is a ROMANISATION there — for a
@@ -226,7 +226,7 @@ export const REPAIR_COMPOSERS = [
226
226
  // 2026-07-04 (the VENZY corrective thrash): three attempts died inventing three different keys
227
227
  // while trying to EXPRESS a hold. The contract is closed and every correction is expressible
228
228
  // inside it — say so.
229
- `THE FINDINGS CONTRACT IS CLOSED — MINIMAL CHANGE ONLY: start from the finding objects as you last sent them and change the smallest set of existing fields the flags require; NEVER invent a key, a state, or an enum value (any unknown key fails the file and, repeated, fails the WHOLE RUN). Every correction the reviewer can ask for is expressible with existing fields: an unsourced/confabulated attribution ⇒ that finding gets "disposition":"withdrawn" + "withdrawn_reason", OR its owner/prose is re-attributed to what the sources actually support — an identity that needs the applicant's confirmation is stated in the finding's prose/impact text, NEVER as a new field or note-type; use_check.quality is EXACTLY one of owner-site | independent | register-mirror or omitted; context_notes entries are EXACTLY {"type","mark","owner","context"}. RE-TYPING A DISPOSITION CARRIES ITS FIELDS WITH IT (#242) — this is the one case where a minimal edit MUST add a key, and these are the only keys it may add: re-typing a finding TO "off-field" also sets "off_field_ground" (EXACTLY "different-field" — only where that finding's own goods_proximity meter reads "low" — or "no-material-risk"), and re-typing AWAY from "off-field" REMOVES it; any finding that is not "withdrawn" keeps a non-empty "legal_position" and "practical_position", so a re-type that lands on a finding missing either must write both. Nothing else may be added.`,
229
+ `THE FINDINGS CONTRACT IS CLOSED — MINIMAL CHANGE ONLY: start from the finding objects as you last sent them and change the smallest set of existing fields the flags require; NEVER invent a key, a state, or an enum value (any unknown key fails the file and, repeated, fails the WHOLE RUN). Every correction the reviewer can ask for is expressible with existing fields: an unsourced/confabulated attribution ⇒ that finding gets "disposition":"withdrawn" + "withdrawn_reason", OR its owner/prose is re-attributed to what the sources actually support — an identity that needs the applicant's confirmation is stated in the finding's prose/impact text, NEVER as a new field or note-type; use_check.quality is EXACTLY one of owner-site | independent | register-mirror or omitted; context_notes entries are EXACTLY {"type","mark","owner","context"}. RE-TYPING A DISPOSITION CARRIES ITS FIELDS WITH IT — this is the one case where a minimal edit MUST add a key, and these are the only keys it may add: re-typing a finding TO "off-field" also sets "off_field_ground" (EXACTLY "different-field" — only where that finding's own goods_proximity meter reads "low" — or "no-material-risk"), and re-typing AWAY from "off-field" REMOVES it; any finding that is not "withdrawn" keeps a non-empty "legal_position" and "practical_position", so a re-type that lands on a finding missing either must write both. Nothing else may be added.`,
230
230
  rulingsTail ? lines(
231
231
  `PLACEMENT RULINGS TAIL (verbatim from ${placement}, provided AS DATA — do NOT re-read the placement file; where a flagged correction touches a coverage disposition, a coverage[] row, or a placement call, adjudicate it against these rulings — adopt each ruling or counter-reason it, never silently drop one):`,
232
232
  "```markdown",
@@ -386,7 +386,7 @@ export const REPAIR_COMPOSERS = [
386
386
  `You are RESUMING your own synthesis session — your narrative and inputs are already in your context; do NOT redo the analysis.`,
387
387
  `Exactly ${quarantined.length} finding object(s) in ${findings} failed the strict parse. Fix ONLY these objects and change NOTHING else:`,
388
388
  ...quarantined.map((q) => `- "${q.mark}" (index ${q.index}): ${String(q.error ?? "invalid shape").slice(0, 160)}`),
389
- `THE FINDINGS CONTRACT IS CLOSED — MINIMAL EDIT ONLY: change the smallest set of existing fields these errors require; NEVER invent a key, a state, or an enum value. A correction that cannot be expressed in existing fields goes in the finding's prose/impact text. ONE exception (#242): a finding re-typed TO "off-field" also gets "off_field_ground" ("different-field" only where its goods_proximity reads "low", else "no-material-risk"), re-typing away from off-field removes it, and every finding that is not "withdrawn" carries a non-empty "legal_position" and "practical_position".`,
389
+ `THE FINDINGS CONTRACT IS CLOSED — MINIMAL EDIT ONLY: change the smallest set of existing fields these errors require; NEVER invent a key, a state, or an enum value. A correction that cannot be expressed in existing fields goes in the finding's prose/impact text. ONE exception: a finding re-typed TO "off-field" also gets "off_field_ground" ("different-field" only where its goods_proximity reads "low", else "no-material-risk"), re-typing away from off-field removes it, and every finding that is not "withdrawn" carries a non-empty "legal_position" and "practical_position".`,
390
390
  `Send the correction with \`record_synthesis\`: a PATCH call carrying \`findings_patch\` — the complete corrected finding object(s), each with the \`ordinal\` it replaces — and nothing else. The driver is holding every value you already sent and re-renders both files from them, so what you do not name comes back byte-identical. There is no file for you to write or edit and nothing you write by hand is read.`,
391
391
  ),
392
392
  samples: [{ name: "one quarantined finding object", tail: "tool", args: { findings: "findings.json", quarantined: [{ mark: "NOVA", index: 2, error: "band missing" }] } }],
@@ -458,11 +458,11 @@ export const REPAIR_COMPOSERS = [
458
458
  stage: "register-unit",
459
459
  key: "register-unit:plan-join-fresh",
460
460
  route: "freshMessage",
461
- compose: ({ axis, registerPlan, bandPath }) => lines(
462
- `Execute the FROZEN register plan for axis "${axis}": call register_execute_plan ONCE with {"plan_path": "${registerPlan}", "axis": "${axis}", "output_path": "${bandPath}"} — the tool runs this axis's dictated entries and MERGES the band itself (existing blocks survive; the missing dictated blocks land, qids stamped). Do NOT run the entries manually, do NOT edit the band yourself, author NO clearance verdict.`,
461
+ compose: ({ axis, registerPlan, bandPath, entries = [] }) => lines(
462
+ `Execute the FROZEN register plan for axis "${axis}": call register_execute_plan ONCE with {"plan_path": "${registerPlan}", "axis": "${axis}", "output_path": "${bandPath}"${entries.length ? `, "qids": ${JSON.stringify(entries.map((e) => e.qid))}` : ""}} — the tool runs ${entries.length ? "ONLY those dictated entries, the ones with no band block yet," : "this axis's dictated entries"} and MERGES the band itself (every other block stays as it is; the missing dictated blocks land, qids stamped). Do NOT run the entries manually, do NOT edit the band yourself, author NO clearance verdict.`,
463
463
  `Return ONLY: the band path + the tool's summary line.`,
464
464
  ),
465
- samples: [{ name: "a fresh plan execution", tail: "tool", args: { axis: "eu", registerPlan: "register-plan.json", bandPath: "bands/eu.md" } }],
465
+ samples: [{ name: "a fresh plan execution", tail: "tool", args: { axis: "eu", registerPlan: "register-plan.json", bandPath: "bands/eu.md", entries: [{ qid: "q1" }] } }],
466
466
  },
467
467
  {
468
468
  trigger: "plan-join",
@@ -472,7 +472,7 @@ export const REPAIR_COMPOSERS = [
472
472
  `You are RESUMING your own register-unit session (axis "${axis}"). Your unit digest stands — do NOT redo it.`,
473
473
  `These DICTATED plan entries have no band block yet:`,
474
474
  ...entries.map((e) => `- qid "${e.qid}": ${e.predicate} ${e.terms ? `names ${JSON.stringify(e.terms)}` : `"${e.term}"`} · nice_classes ${JSON.stringify(e.nice_classes)}${e.regions?.length ? ` · regions ${JSON.stringify(e.regions)}` : ""} · expected: ${e.expected_kind}`),
475
- `Close them by calling register_execute_plan ONCE with {"plan_path": "${registerPlan}", "axis": "${axis}", "output_path": "${bandPath}"} — the tool re-runs this axis's dictated entries and MERGES the band itself (your judgment blocks survive; the missing dictated blocks land, qids stamped). Do NOT run the entries manually or edit the band yourself.`,
475
+ `Close them by calling register_execute_plan ONCE with {"plan_path": "${registerPlan}", "axis": "${axis}", "output_path": "${bandPath}", "qids": ${JSON.stringify(entries.map((e) => e.qid))}} — the tool runs ONLY those entries and MERGES the band itself (your judgment blocks and every other block stay as they are; the missing dictated blocks land, qids stamped). Do NOT run the entries manually or edit the band yourself, and do not call it without qids: that re-runs every entry on the axis.`,
476
476
  `Return ONLY: the band path + the tool's summary line.`,
477
477
  ),
478
478
  samples: [{ name: "one dictated entry with no band block", tail: "tool", args: { axis: "eu", registerPlan: "register-plan.json", bandPath: "bands/eu.md", entries: [{ qid: "q1", predicate: "identical", term: "NOVA", nice_classes: [9], expected_kind: "exact" }] } }],