clearotron 0.3.2-beta.6 → 0.3.2-beta.8

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 (98) hide show
  1. package/.env.example +24 -23
  2. package/INSTALL.md +142 -75
  3. package/README.md +3 -3
  4. package/bin/onboard.mjs +637 -216
  5. package/bin/start.mjs +133 -23
  6. package/bin/update.mjs +82 -11
  7. package/build-info.json +2 -2
  8. package/docs/architecture/04-configuration-reference.md +26 -11
  9. package/docs/architecture/05-config-governance.md +17 -7
  10. package/driver/CHANGELOG.md +83 -0
  11. package/driver/band-size.mjs +59 -0
  12. package/driver/config-inventory.mjs +112 -9
  13. package/driver/connotation-search.mjs +45 -0
  14. package/driver/contract-arm2-baseline.json +1 -3
  15. package/driver/contract-e3-backlog.mjs +29 -29
  16. package/driver/contract-vocabulary.mjs +59 -25
  17. package/driver/door-gates.mjs +41 -7
  18. package/driver/driver.config.mjs +272 -59
  19. package/driver/engine/CONTRACT.md +10 -3
  20. package/driver/engine/README.md +2 -2
  21. package/driver/engine/anthropic-agent.mjs +77 -21
  22. package/driver/engine/auth.mjs +129 -10
  23. package/driver/engine/jx-turn.mjs +7 -6
  24. package/driver/engine/mcp/recording-server.mjs +13 -0
  25. package/driver/engine/openai-agent.mjs +4 -2
  26. package/driver/engine/probe.mjs +110 -23
  27. package/driver/findings-model.mjs +1 -1
  28. package/driver/flag-snapshot.mjs +28 -5
  29. package/driver/gateway.mjs +30 -21
  30. package/driver/jx-lanes.mjs +21 -2
  31. package/driver/jx-units.mjs +6 -3
  32. package/driver/jx.mjs +4 -2
  33. package/driver/matter-frame-record.mjs +90 -1
  34. package/driver/named-band.mjs +34 -2
  35. package/driver/package.json +1 -1
  36. package/driver/pipeline.mjs +391 -26
  37. package/driver/portal-config-view.mjs +30 -1
  38. package/driver/portal-report.mjs +15 -1
  39. package/driver/portal-service.mjs +46 -6
  40. package/driver/predelivery-lint.mjs +12 -2
  41. package/driver/publish/index.mjs +46 -5
  42. package/driver/publish/knockout.mjs +10 -1
  43. package/driver/publish/render-knockout.mjs +69 -7
  44. package/driver/publish/render.mjs +170 -59
  45. package/driver/publish/report-data.mjs +4 -1
  46. package/driver/publish/report-topbar.mjs +58 -0
  47. package/driver/publish/templates/report.css +28 -2
  48. package/driver/publish/xlsx.mjs +13 -1
  49. package/driver/register-availability.mjs +2 -2
  50. package/driver/register-coverage.mjs +94 -1
  51. package/driver/register-digest-record.mjs +236 -11
  52. package/driver/register-plan.mjs +170 -0
  53. package/driver/result-noun-fields.mjs +7 -4
  54. package/driver/run-economics.mjs +41 -10
  55. package/driver/run-requirements.mjs +173 -9
  56. package/driver/runner.mjs +3 -3
  57. package/driver/stages.mjs +12 -8
  58. package/driver/suite-census.json +162 -72
  59. package/driver/systemd/README.md +7 -4
  60. package/driver/terminal-clamp.mjs +107 -1
  61. package/driver/tokens.mjs +169 -3
  62. package/driver/unit-environment.mjs +42 -15
  63. package/driver/unit-inventory.mjs +19 -2
  64. package/driver/verify.mjs +50 -5
  65. package/mcp-server/CHANGELOG.md +8 -0
  66. package/mcp-server/http-server.mjs +4 -0
  67. package/mcp-server/lib/audit.mjs +11 -1
  68. package/mcp-server/lib/http-handler.mjs +6 -2
  69. package/mcp-server/package.json +1 -1
  70. package/mcp-server/server.mjs +16 -2
  71. package/package.json +1 -1
  72. package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
  73. package/portal-ui/dist/index.html +1 -1
  74. package/portal-ui/package.json +1 -1
  75. package/providers/clarivate/src/capabilities.js +5 -5
  76. package/providers/clarivate/src/core.js +1 -1
  77. package/providers/corsearch/src/core.js +2 -2
  78. package/providers/jx/README.md +2 -1
  79. package/providers/jx/src/turn-envelope.mjs +8 -3
  80. package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
  81. package/providers/oauth-mcp-bridge/package.json +1 -1
  82. package/providers/perplexity/src/core.js +3 -3
  83. package/providers/signa/src/capabilities.js +5 -6
  84. package/providers/signa/src/core.js +1 -1
  85. package/providers/uspto-local/README.md +1 -1
  86. package/scripts/authority-boundary-probe.mjs +4 -2
  87. package/scripts/env-audit.mjs +12 -6
  88. package/scripts/freeze-example-run.mjs +49 -16
  89. package/scripts/generated-files-are-current.mjs +69 -4
  90. package/scripts/release-duplicate-notes.mjs +246 -0
  91. package/scripts/release-publish-guard.mjs +64 -6
  92. package/scripts/report-print-check.mjs +194 -0
  93. package/scripts/settings-render-check.mjs +75 -2
  94. package/scripts/test-full.mjs +96 -3
  95. package/scripts/test-run.mjs +10 -0
  96. package/shared/deployment-box.mjs +7 -2
  97. package/shared/driver-dir.mjs +1 -1
  98. package/shared/names-in-force.mjs +1 -1
@@ -69,7 +69,7 @@ export function evictOldest(sessions) {
69
69
  * presents another identity's mcp-session-id is refused (403) — a leaked/guessed session id must never
70
70
  * let one CF-authed person attach to another's session (which may carry an ops-scoped inner token).
71
71
  */
72
- export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions, createSession, ns = "trademark-artifacts", sessionMax = 500, maxBody = 4 * 1024 * 1024, authHeader = "cf-access-jwt-assertion", firmDomains = [], clientSurface = false, devMode = false, tokenOnly = false, keyDoorPath = null,
72
+ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions, createSession, ns = "trademark-artifacts", sessionMax = 500, maxBody = 4 * 1024 * 1024, authHeader = "cf-access-jwt-assertion", firmDomains = [], clientSurface = false, door: doorName = null, devMode = false, tokenOnly = false, keyDoorPath = null,
73
73
  // Is this identity still on the guest list? ASKED PER REQUEST on an ACCOUNT session, cached on the
74
74
  // grants file's mtime, so it costs a stat between edits. Injected so an arm can move the answer
75
75
  // without a file; the default is the real read, because a door composed without this seam would
@@ -96,7 +96,11 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
96
96
  // caller who does not exist. Those lines carry no email and no principal, and say a call was refused
97
97
  // at this door at this time — which is true, and is the shape the local route already uses for the
98
98
  // same reason: a synthesized identity would match somebody who did nothing.
99
- const door = clientSurface ? "client" : undefined;
99
+ // WHICH SURFACE THIS HANDLER IS, named rather than inferred. `clientSurface` answers a scope question
100
+ // and was doing double duty as the door's name, which left the key door — built with neither
101
+ // `clientSurface` nor a name — writing lines with no door at all. A caller that knows it is a third
102
+ // thing passes `door`; the two that do not get the surface they already declare.
103
+ const door = doorName ?? (clientSurface ? "client" : "portal");
100
104
  const refuse = (res, status, obj, who = {}) => {
101
105
  try { appendAudit({ email: who.email ?? null, sub: who.sub ?? null, body: who.body ?? null, status: "refused", door }); }
102
106
  catch { /* best-effort: a write failure must never change what the caller is told */ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trademark-artifacts-mcp",
3
- "version": "0.3.2-beta.6",
3
+ "version": "0.3.2-beta.8",
4
4
  "license": "AGPL-3.0-only",
5
5
  "private": true,
6
6
  "description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
@@ -31,7 +31,7 @@ import { join, basename } from "node:path";
31
31
  import { driverDir } from "../shared/driver-dir.mjs"; //
32
32
  import { fileURLToPath } from "node:url";
33
33
 
34
- import { enumerateRuns, resolveRun, runAccountKey, runOrganisation, runProfileFacts } from "./lib/runs.mjs";
34
+ import { enumerateRuns, resolveRun, runAccountKey, runOrganisation, runProfileFacts, productIdentityFor } from "./lib/runs.mjs";
35
35
  import { ORDERABLE_PRODUCTS } from "../driver/search-policy.mjs";
36
36
  import { PRODUCTS } from "../driver/products.mjs";
37
37
 
@@ -193,6 +193,20 @@ function runSummary(run) {
193
193
  ? { key: facts.account, name: facts.clientName }
194
194
  : { key: null, name: null, known: false, note: "this run's client could not be read from its own record" },
195
195
  project: facts.projectKey ? { key: facts.projectKey, name: facts.projectName ?? facts.projectKey } : null,
196
+ // WHAT KIND OF SEARCH IT WAS, on every row, and it is the same defect as the client above one turn
197
+ // later. A mark and a date do not name one run: on the bundled demo data alone, one mark and one day
198
+ // return nine rows across four products — a full country search, a multi-country focus search, a
199
+ // global preliminary search and two knockouts. A session bound to one report is safe whatever the
200
+ // row says, because the scope carries the runId; an account-scoped session has only these rows, and
201
+ // nothing in them told the two apart except the runId string, which is an internal slug a client has
202
+ // never seen and which this tool's own description warns is not theirs to read.
203
+ //
204
+ // RESOLVED AT READ TIME THROUGH THE REGISTRY, never a stored string, for the reason `runs.mjs` gives
205
+ // where this resolver lives: a run freezes its product ID and the name is today's name, so the row,
206
+ // the brief and the report masthead cannot disagree and a renamed product renames everywhere at
207
+ // once. null where the registry cannot name it — the row says nothing rather than guessing, because
208
+ // a hardcoded fallback is how a knockout once announced itself as a product it provably was not.
209
+ product: productIdentityFor(run),
196
210
  state: run.state, location: run.location, verdict: run.verdict, url: run.url,
197
211
  markName: run.markName, ref: run.ref, classes: run.classes,
198
212
  step: s.stepN ? `${s.stepN}/${s.stepTotal} ${s.stepLabel ?? ""}`.trim() : null,
@@ -1096,7 +1110,7 @@ if (isMain) {
1096
1110
  // route. On an install with no hosted client door the reader IS the operator, which is the same
1097
1111
  // split `stdioConnectOffer` already trusts, so that fact answers the question by itself; on a
1098
1112
  // hosted install it answers for nobody and the portal ignores it.
1099
- appendAudit({ email: null, sub: null, body: { method: "initialize" }, status: "connected", transport: "stdio" });
1113
+ appendAudit({ email: null, sub: null, body: { method: "initialize" }, status: "connected", transport: "stdio", door: "local" });
1100
1114
  })
1101
1115
  .catch((e) => { log(`fatal: ${e?.stack ?? e}`); process.exit(1); });
1102
1116
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clearotron",
3
3
  "type": "module",
4
- "version": "0.3.2-beta.6",
4
+ "version": "0.3.2-beta.8",
5
5
  "license": "AGPL-3.0-only",
6
6
  "repository": {
7
7
  "type": "git",
@@ -11926,6 +11926,7 @@ var api = {
11926
11926
  };
11927
11927
  }),
11928
11928
  ...!("territories" in b) ? {} : { registerTerritories: b["territories"] === null ? null : asArray(b["territories"]).filter((t) => typeof t === "string") },
11929
+ ...asString(b["registerLabel"]) ? { registerLabel: asString(b["registerLabel"]) } : {},
11929
11930
  recipes: asArray(b["recipes"]).map((x) => {
11930
11931
  const r = x;
11931
11932
  return {
@@ -12193,11 +12194,38 @@ var api = {
12193
12194
  billing: {
12194
12195
  mode: asString(bill["mode"]) ?? "unknown",
12195
12196
  apiBilled: bill["apiBilled"] === true,
12196
- missing: asStrings(bill["missing"])
12197
+ missing: asStrings(bill["missing"]),
12198
+ cloud: asString(bill["cloud"]),
12199
+ cloudName: asString(bill["cloudName"]),
12200
+ reason: bill["reason"] != null ? (() => {
12201
+ const raw = bill["reason"];
12202
+ const r = typeof raw === "object" && !Array.isArray(raw) ? asRecord(raw) : {};
12203
+ return {
12204
+ kind: asString(r["kind"]) ?? "unclassified",
12205
+ mode: asString(r["mode"]),
12206
+ defaulted: r["defaulted"] === true,
12207
+ setting: asString(r["setting"]) ?? "",
12208
+ clouds: asArray(r["clouds"]).flatMap((c) => {
12209
+ const x = asRecord(c);
12210
+ const name = asString(x["name"]);
12211
+ const setting = asString(x["setting"]);
12212
+ return name && setting ? [{
12213
+ name,
12214
+ setting
12215
+ }] : [];
12216
+ }),
12217
+ modes: asStrings(r["modes"]),
12218
+ gateway: asString(r["gateway"]),
12219
+ engineSetting: asString(r["engineSetting"]),
12220
+ engineChoice: asString(r["engineChoice"])
12221
+ };
12222
+ })() : null
12197
12223
  },
12198
12224
  binaryPresent: e["binaryPresent"] === true,
12199
12225
  program: asString(e["program"]),
12200
- install: asString(e["install"])
12226
+ install: asString(e["install"]),
12227
+ programSetting: asString(e["programSetting"]),
12228
+ setupRoute: e["setupRoute"] === "packaged" ? "packaged" : e["setupRoute"] === "checkout" ? "checkout" : null
12201
12229
  };
12202
12230
  })() : null,
12203
12231
  providers: Array.isArray(b["providers"]) ? asArray(b["providers"]).map((p) => {
@@ -12330,13 +12358,6 @@ var NAV = [
12330
12358
  icon: "panel-left",
12331
12359
  scope: "account"
12332
12360
  },
12333
- {
12334
- id: "ai",
12335
- label: "Connect your AI",
12336
- path: "/portal/ai",
12337
- icon: "sparkles",
12338
- scope: "account"
12339
- },
12340
12361
  {
12341
12362
  id: "people",
12342
12363
  label: "People",
@@ -12424,6 +12445,14 @@ var NAV = [
12424
12445
  }
12425
12446
  ]
12426
12447
  },
12448
+ {
12449
+ id: "ai",
12450
+ label: "Connect your AI",
12451
+ path: "/portal/ai",
12452
+ icon: "sparkles",
12453
+ scope: "account",
12454
+ rail: "below"
12455
+ },
12427
12456
  {
12428
12457
  id: "new-company",
12429
12458
  label: "New company",
@@ -12463,11 +12492,12 @@ function navFor(who, entries = NAV) {
12463
12492
  children: e.children.filter((c) => visible(c, who) && !c.hidden)
12464
12493
  } : e);
12465
12494
  }
12495
+ var railSideOf = (e) => e.rail ?? ((e.scope ?? "account") === "account" ? "above" : "below");
12466
12496
  function navGroupsFor(who, entries = NAV) {
12467
12497
  const visibleEntries = navFor(who, entries);
12468
12498
  return {
12469
- account: visibleEntries.filter((e) => (e.scope ?? "account") === "account"),
12470
- owner: visibleEntries.filter((e) => e.scope === "owner")
12499
+ account: visibleEntries.filter((e) => railSideOf(e) === "above"),
12500
+ owner: visibleEntries.filter((e) => railSideOf(e) === "below")
12471
12501
  };
12472
12502
  }
12473
12503
  function scopeOf(id, entries = NAV) {
@@ -13818,6 +13848,7 @@ function readableFailure(failedStage, reason) {
13818
13848
  var EMPTY_DRAFT = {
13819
13849
  product: null,
13820
13850
  territories: [],
13851
+ replacesOwnTerritories: false,
13821
13852
  nativeLanguage: false
13822
13853
  };
13823
13854
  var REGIONS = [
@@ -13941,9 +13972,6 @@ function reachesTerritory(name, covered) {
13941
13972
  if (!Array.isArray(covered)) return true;
13942
13973
  return covered.includes(name);
13943
13974
  }
13944
- function vocabularyFor(product, covered) {
13945
- return offerableFor(product).filter((n) => reachesTerritory(n, covered));
13946
- }
13947
13975
  function territoryMatches(query, chosen, product, limit = 8, _covered) {
13948
13976
  return matchTerritoriesIn(offerableFor(product), query, chosen, limit);
13949
13977
  }
@@ -13959,8 +13987,9 @@ function matchTerritoriesIn(vocabulary, query, chosen, limit = 8) {
13959
13987
  };
13960
13988
  return vocabulary.filter((n) => !taken.has(n.toLowerCase()) && hit(n)).slice(0, limit);
13961
13989
  }
13962
- function addTerritory(d, name, product, _covered) {
13990
+ function addTerritory(d, name, product, covered) {
13963
13991
  if (!offerableFor(product).includes(name)) return d;
13992
+ if (!reachesTerritory(name, covered)) return d;
13964
13993
  if (d.territories.includes(name)) return d;
13965
13994
  if (product?.geography === "exactly one country") return {
13966
13995
  ...d,
@@ -13977,14 +14006,35 @@ function removeTerritory(d, name) {
13977
14006
  territories: d.territories.filter((t) => t !== name)
13978
14007
  };
13979
14008
  }
13980
- function geographyFor(d) {
13981
- return d.territories.length ? {
14009
+ function notAvailableLine(names, registerLabel) {
14010
+ const named = names.join(", ");
14011
+ const who = typeof registerLabel === "string" && registerLabel.trim() ? registerLabel.trim() : null;
14012
+ return `${named ? `${named} \u00b7 ` : ""}not available${who ? ` with register ${who}` : ""}`;
14013
+ }
14014
+ function takeOverOwnTerritories(d, own, without) {
14015
+ return {
14016
+ ...d,
14017
+ territories: own.filter((t) => t !== without),
14018
+ replacesOwnTerritories: true
14019
+ };
14020
+ }
14021
+ function geographyFor(d, product = null, resolved = []) {
14022
+ if (product?.geography === "worldwide, and nothing else") return {
14023
+ mode: "worldwide",
14024
+ territories: []
14025
+ };
14026
+ if (d.territories.length) return {
13982
14027
  mode: "named",
13983
14028
  territories: [...d.territories]
13984
- } : {
14029
+ };
14030
+ if (d.replacesOwnTerritories) return {
13985
14031
  mode: "worldwide",
13986
14032
  territories: []
13987
14033
  };
14034
+ return {
14035
+ mode: "account-default",
14036
+ territories: [...resolved]
14037
+ };
13988
14038
  }
13989
14039
  function nativeLanguageControl(product) {
13990
14040
  if (product?.nativeLanguage === "offered") return "toggle";
@@ -14003,6 +14053,7 @@ function chooseProduct(d, product) {
14003
14053
  return {
14004
14054
  product: product?.key ?? null,
14005
14055
  territories,
14056
+ replacesOwnTerritories: d.replacesOwnTerritories,
14006
14057
  nativeLanguage: nativeLanguageControl(product) === "toggle" ? d.nativeLanguage : false
14007
14058
  };
14008
14059
  }
@@ -14485,6 +14536,7 @@ function draftFromSaved(recipe, products) {
14485
14536
  return chooseProduct({
14486
14537
  product: base,
14487
14538
  territories: Array.isArray(scope["jurisdictions"]) ? scope["jurisdictions"].filter((j) => typeof j === "string") : [],
14539
+ replacesOwnTerritories: false,
14488
14540
  nativeLanguage: recipe["nativeLanguage"] === true
14489
14541
  }, product);
14490
14542
  }
@@ -15510,7 +15562,7 @@ function resumeWording(iso) {
15510
15562
  });
15511
15563
  return t.toDateString() === new Date().toDateString() ? `at ${time}` : `${t.toLocaleDateString([], { weekday: "long" })} at ${time}`;
15512
15564
  }
15513
- function StatusCell({ state, step, stepN, stepTotal, reason, failedStage, pausedKind = null, resetsAt = null, stopRequestedAt = null, detailed = false, count = null }) {
15565
+ function StatusCell({ state, step, reason, failedStage, pausedKind = null, resetsAt = null, stopRequestedAt = null, detailed = false, count = null }) {
15514
15566
  if (stopRequestedAt && state !== "delivered" && state !== "failed" && state !== "cancelled") return (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsxs)("span", {
15515
15567
  className: "status",
15516
15568
  children: [(0, import_jsx_runtime.jsx)("span", {
@@ -15577,7 +15629,7 @@ function StatusCell({ state, step, stepN, stepTotal, reason, failedStage, paused
15577
15629
  ] });
15578
15630
  }
15579
15631
  const label = state === "queued" ? "Queued" : state === "paused" ? "Paused" : "Running";
15580
- const detail = state === "paused" ? pausedKind === "recovering" ? "Held after a problem — retrying on its own" : pausedKind === "operator" ? "Paused by a system restart — resumes on its own" : resetsAt ? `Paused by a provider limit — resumes ${resumeWording(resetsAt)}` : "Paused by a provider limit — resumes on its own" : step ? stepN != null && stepTotal != null ? `${step} · ${stepN} of ${stepTotal}` : step : null;
15632
+ const detail = state === "paused" ? pausedKind === "recovering" ? "Held after a problem — retrying on its own" : pausedKind === "operator" ? "Paused by a system restart — resumes on its own" : resetsAt ? `Paused by a provider limit — resumes ${resumeWording(resetsAt)}` : "Paused by a provider limit — resumes on its own" : step;
15581
15633
  return (0, import_jsx_runtime.jsxs)("span", { children: [(0, import_jsx_runtime.jsxs)("span", {
15582
15634
  className: "status",
15583
15635
  children: [(0, import_jsx_runtime.jsx)("span", { className: `dot ${state}` }), count ? (0, import_jsx_runtime.jsxs)("span", { children: [
@@ -15602,7 +15654,7 @@ function allowanceLine(usage, brand) {
15602
15654
  if (left == null) return null;
15603
15655
  if (left > 5) return null;
15604
15656
  const who = operatorName(brand);
15605
- if (left === 0) return `You have used all ${usage?.dailyRuns ?? 0} of this account's searches for today. The allowance resets at midnight UTC — or ask your ${who} contact to run this one for you.`;
15657
+ if (left === 0) return `You have used all ${usage?.dailyRuns ?? 0} of this account's searches for today. The allowance resets at midnight UTC — or ask ${who} to run this one for you.`;
15606
15658
  return `${left} search${left === 1 ? "" : "es"} left today. The allowance resets at midnight UTC, or ask ${who} to run one for you.`;
15607
15659
  }
15608
15660
  //#endregion
@@ -16688,8 +16740,6 @@ function FamilyRows({ family, isOpen, onToggle, go, picking, showOwner = false,
16688
16740
  }) : (0, import_jsx_runtime.jsx)(StatusCell, {
16689
16741
  state: family.state,
16690
16742
  step: null,
16691
- stepN: null,
16692
- stepTotal: null,
16693
16743
  reason: null,
16694
16744
  failedStage: null
16695
16745
  }) }),
@@ -16798,8 +16848,6 @@ function MarkRow({ mark, open, onToggle, go, indent = false, picking = false, sh
16798
16848
  (0, import_jsx_runtime.jsx)("td", { children: (0, import_jsx_runtime.jsx)(StatusCell, {
16799
16849
  state: run.state,
16800
16850
  step: run.step,
16801
- stepN: run.stepN,
16802
- stepTotal: run.stepTotal,
16803
16851
  reason: run.reason,
16804
16852
  failedStage: run.failedStage,
16805
16853
  pausedKind: run.pausedKind,
@@ -16936,8 +16984,6 @@ function ReadRow({ read, go, picking, indent, showOwner = false, latest = false,
16936
16984
  (0, import_jsx_runtime.jsx)("td", { children: (0, import_jsx_runtime.jsx)(StatusCell, {
16937
16985
  state: read.state,
16938
16986
  step: read.step,
16939
- stepN: read.stepN,
16940
- stepTotal: read.stepTotal,
16941
16987
  reason: read.reason,
16942
16988
  failedStage: read.failedStage,
16943
16989
  stopRequestedAt: read.stopRequestedAt,
@@ -17764,26 +17810,68 @@ function Missing({ go, reason }) {
17764
17810
  }
17765
17811
  //#endregion
17766
17812
  //#region src/contract/engineState.ts
17813
+ var SETUP_COMMAND = "`npx clearotron install`";
17814
+ var listed = (xs, and = "and") => xs.length <= 1 ? xs[0] ?? "" : `${xs.slice(0, -1).join(", ")} ${and} ${xs[xs.length - 1]}`;
17815
+ var PAID_BY = {
17816
+ subscription: "subscription",
17817
+ "api-key": "an API key",
17818
+ cloud: "a cloud account"
17819
+ };
17820
+ var paidBy = (mode) => mode !== null && Object.hasOwn(PAID_BY, mode) ? PAID_BY[mode] : mode ?? "a way of paying";
17821
+ var COUNT = [
17822
+ "no",
17823
+ "one",
17824
+ "two",
17825
+ "three",
17826
+ "four"
17827
+ ];
17828
+ var NAMES_CLOUDS = new Set([
17829
+ "switch-beside-mode",
17830
+ "two-clouds",
17831
+ "no-cloud"
17832
+ ]);
17833
+ function billingRefusalSentence(r) {
17834
+ const setting = r.setting || "the billing setting";
17835
+ const generic = `Searches will be refused over how this machine is set to pay. Check ${setting} and the settings beside it.`;
17836
+ if (NAMES_CLOUDS.has(r.kind) && r.clouds.length === 0) return generic;
17837
+ const paid = r.defaulted ? `${paidBy(r.mode)}, the default while ${setting} is not set` : `set to ${paidBy(r.mode)}`;
17838
+ switch (r.kind) {
17839
+ case "switch-beside-mode": {
17840
+ const one = r.clouds.length === 1;
17841
+ const names = listed(r.clouds.map((c) => c.name));
17842
+ const switches = listed(r.clouds.map((c) => c.setting));
17843
+ const keep = r.mode === "api-key" ? "to pay with the API key" : "to pay by subscription";
17844
+ return `Searches will be refused: payment is ${r.defaulted ? "by " : ""}${paid}, but the ${names} ${one ? "switch" : "switches"} (${switches}) ${one ? "is" : "are"} also on. ` + (one ? `Turn ${switches} off ${keep}, or set ${setting} to cloud to pay through ${names}.` : `Turn them off ${keep}, or set ${setting} to cloud and leave one on.`);
17845
+ }
17846
+ case "two-clouds": return `Searches will be refused: payment is set to a cloud account, but ${COUNT[r.clouds.length] ?? r.clouds.length} clouds are switched on, ${listed(r.clouds.map((c) => `${c.name} (${c.setting})`))}. Turn off all but the one you pay through.`;
17847
+ case "no-cloud": return `Searches will be refused: payment is set to a cloud account, but no cloud is chosen. Turn on your cloud's switch (${listed(r.clouds.map((c) => `${c.setting} for ${c.name}`), "or")}) with the settings that cloud needs` + (r.gateway ? `, set ${r.gateway} for a gateway,` : ",") + ` or set ${setting} to subscription.`;
17848
+ case "not-a-mode": return `Searches will be refused: ${setting} is set to a word Clearotron does not know. Set it to ${listed(r.modes, "or")}.`;
17849
+ case "cloud-on-codex": return `Searches will be refused: payment is set to a cloud account, which pays only for Claude, and this machine runs the Codex engine. Set ${setting} to ${listed(r.modes, "or")}` + (r.engineSetting && r.engineChoice ? `, or set ${r.engineSetting} to ${r.engineChoice} to pay for Claude through your cloud.` : ".");
17850
+ default: return generic;
17851
+ }
17852
+ }
17767
17853
  function engineRowFaults(engine, { programDisputed = false } = {}) {
17854
+ const reason = engine.billing.reason ?? null;
17768
17855
  return [
17769
17856
  ...engine.known ? [] : [`This build does not ship an engine called ${engine.id}.`],
17770
17857
  ...engine.billing.missing.length ? ["Set to bill an API key, and no key is set — a run is refused rather than billed to the subscription."] : [],
17771
- ...engine.binaryPresent ? [] : engine.program && engine.install ? [`The engine program \`${engine.program}\` cannot be found or run on this machine. Install it with \`${engine.install}\`, then restart this service so it looks for the program again.`] : ["The engine program cannot be found or run on this machine."],
17772
- ...programDisputed && engine.binaryPresent ? ["The engine program is on this machine, but the engine could not find it when it last started — so a new search will refuse. Restart the engine service, or install the CLI where the service can see it."] : []
17858
+ ...reason && !engine.billing.missing.length ? [billingRefusalSentence(reason)] : [],
17859
+ ...engine.binaryPresent ? [] : engine.program ? [`The engine program \`${engine.program}\` cannot be found or run on this machine. Run the setup wizard, ${SETUP_COMMAND}: it offers to install the program and proves it with one turn. Then restart Clearotron's services, which look for the program when they start.`] : ["The engine program cannot be found or run on this machine."],
17860
+ ...programDisputed && engine.binaryPresent ? [`The engine program is on this machine, but the engine could not find it when it last started — so a new search will refuse. Restart the engine service so it looks again. If it still cannot find it, set ${engine.programSetting ?? "the engine's program setting"} to the program's full path in the settings file the service reads, then restart it again.`] : []
17773
17861
  ];
17774
17862
  }
17775
17863
  function engineNotice({ programDisputed }) {
17776
17864
  if (programDisputed === true) return {
17777
17865
  state: "disputed",
17778
17866
  headline: "The engine program is on this machine, but the engine could not find it when it last started.",
17779
- before: "So a new search will refuse. Restart the engine service, or install the CLI where the service can see it — installing it again where it already is will not change this.",
17867
+ before: "So a new search will refuse. Restart the engine service so it looks again. If it still cannot find it, set the engine's program setting to the program's full path in the settings file the service reads — installing it again where it already is will not change this.",
17780
17868
  namesSetupCommand: false,
17781
17869
  after: ""
17782
17870
  };
17783
17871
  return {
17784
17872
  state: "absent",
17785
17873
  headline: "No search engine is attached to this install.",
17786
- before: "Everything else works — the example report, its audit trail and the assistant connection are live right now. To start new searches, install a reasoning CLI and sign in, then run the setup wizard again",
17874
+ before: "Everything else works — the example report, its audit trail and the assistant connection are live right now. The setup wizard offers to install a reasoning CLI if this machine has none, and asks how it is paid for. To start new searches, run the setup wizard",
17787
17875
  namesSetupCommand: true,
17788
17876
  after: "Then restart the service: this reading was taken when it started, and it will not notice a new install until it starts again."
17789
17877
  };
@@ -17794,7 +17882,7 @@ function engineRow(engine, { programDisputed = false } = {}) {
17794
17882
  ok: faults.length === 0,
17795
17883
  name: engine.vendor ?? engine.id,
17796
17884
  mono: engine.id,
17797
- state: engine.billing.apiBilled ? "API key" : "Subscription",
17885
+ state: engine.billing.missing.length || engine.billing.reason ? "Refused" : engine.billing.cloudName ?? (engine.billing.apiBilled ? "API key" : "Subscription"),
17798
17886
  faults
17799
17887
  };
17800
17888
  }
@@ -18270,6 +18358,7 @@ function NewClearance({ ctx }) {
18270
18358
  const { result: usageRes } = useLoad(() => api.usage(account), [account]);
18271
18359
  const levels = searches?.kind === "ok" ? searches.value.products : [];
18272
18360
  const registerTerritories = searches?.kind === "ok" ? searches.value.registerTerritories : void 0;
18361
+ const registerLabel = searches?.kind === "ok" ? searches.value.registerLabel : void 0;
18273
18362
  const savedSearches = searches?.kind === "ok" ? searches.value.recipes : [];
18274
18363
  const readCan = searches?.kind === "ok" ? searches.value.read : {
18275
18364
  available: false,
@@ -18387,10 +18476,11 @@ function NewClearance({ ctx }) {
18387
18476
  setSaveNote(null);
18388
18477
  };
18389
18478
  const setPick = (next) => edit({ pick: next });
18390
- const recommendation = (0, import_react.useMemo)(() => recommendSearch(levels, names.length, draft.pick.territories), [
18479
+ const recommendation = (0, import_react.useMemo)(() => recommendSearch(levels, names.length, draft.pick.territories.length ? draft.pick.territories : own.territories), [
18391
18480
  levels,
18392
18481
  names.length,
18393
- draft.pick.territories
18482
+ draft.pick.territories,
18483
+ own.territories
18394
18484
  ]);
18395
18485
  (0, import_react.useEffect)(() => {
18396
18486
  if (pickedByHand || draft.savedSearch || editingSlug || !recommendation) return;
@@ -18427,7 +18517,7 @@ function NewClearance({ ctx }) {
18427
18517
  deadline: d.deadline
18428
18518
  };
18429
18519
  let after = applyRead(before, read, own.classes, { worldwide });
18430
- const fits = recommendSearch(levels, parseNames(after.names).length, after.draft.territories);
18520
+ const fits = recommendSearch(levels, parseNames(after.names).length, after.draft.territories.length ? after.draft.territories : own.territories);
18431
18521
  if (read.product == null && !pickedByHand && !d.savedSearch && fits) after = {
18432
18522
  ...after,
18433
18523
  draft: chooseProduct(after.draft, fits.product)
@@ -18456,7 +18546,7 @@ function NewClearance({ ctx }) {
18456
18546
  ...draft.savedSearch ? { recipeKey: draft.savedSearch } : draft.pick.product ? { product: draft.pick.product } : {},
18457
18547
  ...draft.project ? { projectKey: draft.project } : {},
18458
18548
  ...draft.pick.territories.length ? { jurisdictions: [...draft.pick.territories] } : {},
18459
- geography: { mode: geographyFor(draft.pick).mode },
18549
+ geography: { mode: geographyFor(draft.pick, activeLevel, own.territories).mode },
18460
18550
  ...marketplacesApply && parseList(draft.platforms).length ? { platforms: parseList(draft.platforms) } : {},
18461
18551
  ...!draft.savedSearch && nativeControl === "toggle" && draft.pick.nativeLanguage ? { nativeLanguage: true } : {},
18462
18552
  ...draft.ref.trim() ? { ref: draft.ref.trim() } : {},
@@ -18795,10 +18885,20 @@ function NewClearance({ ctx }) {
18795
18885
  gap: 6,
18796
18886
  margin: "9px 0 12px"
18797
18887
  },
18798
- children: draft.pick.territories.length === 0 ? own.territories.length ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [own.territories.map((t) => (0, import_jsx_runtime.jsx)("span", {
18799
- className: "chip",
18888
+ children: draft.pick.territories.length === 0 ? own.territories.length ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [own.territories.map((t) => (0, import_jsx_runtime.jsxs)("span", {
18800
18889
  "data-anon": "mark",
18801
- children: t
18890
+ className: reachesTerritory(t, registerTerritories) ? "chip" : "chip chip-deferred",
18891
+ title: reachesTerritory(t, registerTerritories) ? void 0 : notAvailableLine([t], registerLabel),
18892
+ children: [reachesTerritory(t, registerTerritories) ? t : notAvailableLine([t], registerLabel), (0, import_jsx_runtime.jsx)("button", {
18893
+ type: "button",
18894
+ "aria-label": `Remove ${t}`,
18895
+ className: "chip-x",
18896
+ onClick: () => setPick(takeOverOwnTerritories(draft.pick, own.territories, t)),
18897
+ children: (0, import_jsx_runtime.jsx)(Icon, {
18898
+ name: "x",
18899
+ size: 13
18900
+ })
18901
+ })]
18802
18902
  }, t)), (0, import_jsx_runtime.jsx)("span", {
18803
18903
  style: {
18804
18904
  fontSize: 11,
@@ -18811,21 +18911,17 @@ function NewClearance({ ctx }) {
18811
18911
  children: "Worldwide"
18812
18912
  }) : draft.pick.territories.map((t) => (0, import_jsx_runtime.jsxs)("span", {
18813
18913
  className: reachesTerritory(t, registerTerritories) ? "chip chip-own" : "chip chip-own chip-deferred",
18814
- title: reachesTerritory(t, registerTerritories) ? void 0 : "The register wired to this deployment does not reach this territory — it is disclosed in the report as deferred coverage rather than searched.",
18815
- children: [
18816
- t,
18817
- reachesTerritory(t, registerTerritories) ? "" : " · register deferred",
18818
- (0, import_jsx_runtime.jsx)("button", {
18819
- type: "button",
18820
- "aria-label": `Remove ${t}`,
18821
- className: "chip-x",
18822
- onClick: () => setPick(removeTerritory(draft.pick, t)),
18823
- children: (0, import_jsx_runtime.jsx)(Icon, {
18824
- name: "x",
18825
- size: 13
18826
- })
18914
+ title: reachesTerritory(t, registerTerritories) ? void 0 : notAvailableLine([t], registerLabel),
18915
+ children: [reachesTerritory(t, registerTerritories) ? t : notAvailableLine([t], registerLabel), (0, import_jsx_runtime.jsx)("button", {
18916
+ type: "button",
18917
+ "aria-label": `Remove ${t}`,
18918
+ className: "chip-x",
18919
+ onClick: () => setPick(removeTerritory(draft.pick, t)),
18920
+ children: (0, import_jsx_runtime.jsx)(Icon, {
18921
+ name: "x",
18922
+ size: 13
18827
18923
  })
18828
- ]
18924
+ })]
18829
18925
  }, t))
18830
18926
  }),
18831
18927
  (0, import_jsx_runtime.jsx)("div", {
@@ -18845,33 +18941,18 @@ function NewClearance({ ctx }) {
18845
18941
  className: "typeahead",
18846
18942
  children: territoryMatches(territoryQuery, draft.pick.territories, whereLevel, 8, registerTerritories).map((t) => (0, import_jsx_runtime.jsxs)("button", {
18847
18943
  type: "button",
18944
+ disabled: !reachesTerritory(t, registerTerritories),
18848
18945
  onClick: () => {
18849
18946
  setPick(addTerritory(draft.pick, t, whereLevel, registerTerritories));
18850
18947
  setTerritoryQuery("");
18851
18948
  },
18852
18949
  children: [t, reachesTerritory(t, registerTerritories) ? null : (0, import_jsx_runtime.jsx)("span", {
18853
18950
  className: "typeahead-note",
18854
- children: "register deferred"
18951
+ children: notAvailableLine([], registerLabel)
18855
18952
  })]
18856
18953
  }, t))
18857
18954
  }) : null]
18858
18955
  }),
18859
- Array.isArray(registerTerritories) ? (0, import_jsx_runtime.jsxs)("p", {
18860
- className: "section-hint",
18861
- style: { marginTop: 10 },
18862
- children: [
18863
- "The trademark register wired to this deployment reaches",
18864
- " ",
18865
- vocabularyFor(whereLevel, registerTerritories).length,
18866
- " of the",
18867
- " ",
18868
- offerableFor(whereLevel).length,
18869
- " territories you can name here:",
18870
- " ",
18871
- vocabularyFor(whereLevel, registerTerritories).join(", "),
18872
- ". Anywhere else can still be ordered — it is disclosed in the report as deferred coverage rather than searched at the register."
18873
- ]
18874
- }) : null,
18875
18956
  whereLevel?.geography === "exactly one country" && draft.pick.territories.length === 1 ? (0, import_jsx_runtime.jsxs)("div", {
18876
18957
  className: "callout-accent",
18877
18958
  children: [draft.pick.territories[0], " — naming another country replaces it, because this search reads one at a time."]
@@ -19397,6 +19478,7 @@ function NewClearance({ ctx }) {
19397
19478
  plan ? (0, import_jsx_runtime.jsx)(ReviewDialog, {
19398
19479
  uses: runCount(effort),
19399
19480
  left: searchesLeft(usage),
19481
+ registerLabel,
19400
19482
  plan,
19401
19483
  busy,
19402
19484
  owner: ownerLabel,
@@ -19933,7 +20015,7 @@ function PickRow({ selected, onPick, title, tagline, description, chips, recomme
19933
20015
  }) : null]
19934
20016
  });
19935
20017
  }
19936
- function ReviewDialog({ plan, busy, owner, project, names, goods, nativeOn, onStart, onBack, failure, onReview, uses, left }) {
20018
+ function ReviewDialog({ plan, busy, owner, project, names, goods, nativeOn, onStart, onBack, failure, onReview, uses, left, registerLabel }) {
19937
20019
  (0, import_react.useEffect)(() => {
19938
20020
  const on = (e) => {
19939
20021
  if (e.key === "Escape") onBack();
@@ -20018,13 +20100,7 @@ function ReviewDialog({ plan, busy, owner, project, names, goods, nativeOn, onSt
20018
20100
  }),
20019
20101
  (0, import_jsx_runtime.jsx)(Row$3, {
20020
20102
  label: "Registers to search",
20021
- children: plan.coverage ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [plan.coverage.reached.length ? joinAnd(plan.coverage.reached) : "None", plan.coverage.missing.length ? (0, import_jsx_runtime.jsxs)(Muted, { children: [
20022
- ". ",
20023
- joinAnd(plan.coverage.missing),
20024
- " ",
20025
- plan.coverage.missing.length === 1 ? "is" : "are",
20026
- " not reached by the register wired here, and will be disclosed in the report as deferred coverage rather than searched."
20027
- ] }) : null] }) : places.length ? joinAnd(places) : "Worldwide"
20103
+ children: plan.coverage ? (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [plan.coverage.reached.length ? joinAnd(plan.coverage.reached) : "None", plan.coverage.missing.length ? (0, import_jsx_runtime.jsxs)(Muted, { children: [". ", notAvailableLine(plan.coverage.missing, registerLabel)] }) : null] }) : places.length ? joinAnd(places) : "Worldwide"
20028
20104
  }),
20029
20105
  scope && scope.classes.length ? (0, import_jsx_runtime.jsxs)(Row$3, {
20030
20106
  label: "Classes",
@@ -25476,7 +25552,7 @@ function About() {
25476
25552
  (0, import_jsx_runtime.jsx)("dt", { children: "Copyright" }),
25477
25553
  (0, import_jsx_runtime.jsx)("dd", { children: info.copyright.replace(/^Copyright\s+/i, "") }),
25478
25554
  (0, import_jsx_runtime.jsx)("dt", { children: "Model access" }),
25479
- (0, import_jsx_runtime.jsx)("dd", { children: "This runs on your own model access — a subscription or your own API key — and the paid registers on your own agreements with those providers. The reasoning stages are a proprietary third-party CLI that you install and license under that vendor’s own terms; this licence grants nothing over any of them." }),
25555
+ (0, import_jsx_runtime.jsx)("dd", { children: "This runs on your own model access — a subscription, an API key, or your own cloud account — and the paid registers on your own agreements with those providers. The reasoning stages are a third-party command-line program, Claude Code (proprietary) or the Codex CLI (Apache-2.0), that you install and license under that vendor’s own terms; this licence grants nothing over any of them." }),
25480
25556
  (0, import_jsx_runtime.jsx)("dt", { children: "Trade marks" }),
25481
25557
  (0, import_jsx_runtime.jsxs)("dd", { children: [
25482
25558
  (0, import_jsx_runtime.jsx)("strong", { children: info.name }),
@@ -49,7 +49,7 @@
49
49
  -->
50
50
  <link rel="preconnect" href="https://api.fontshare.com" crossorigin />
51
51
  <link href="https://api.fontshare.com/v2/css?f[]=satoshi@400,500,700,900&display=swap" rel="stylesheet" />
52
- <script type="module" crossorigin src="/portal/assets/index-5UyqAyNM.js"></script>
52
+ <script type="module" crossorigin src="/portal/assets/index-6jzO9HiX.js"></script>
53
53
  <link rel="stylesheet" crossorigin href="/portal/assets/index-CVOIvdhc.css">
54
54
  </head>
55
55
  <body>
@@ -2,7 +2,7 @@
2
2
  "name": "portal-ui",
3
3
  "private": true,
4
4
  "type": "module",
5
- "version": "0.3.2-beta.6",
5
+ "version": "0.3.2-beta.8",
6
6
  "license": "AGPL-3.0-only",
7
7
  "description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
8
8
  "engines": {
@@ -217,11 +217,11 @@ export const CAPABILITIES = Object.freeze({
217
217
  // ── WHICH FORM OF A NON-LATIN MARK DOES THE INDEX HOLD? ──────────────────────────────────────────
218
218
  // `false` = the TRANSLITERATION ONLY. The characters are not indexed, so searching them returns 0
219
219
  // with no error — the exact false-clean shape a reader calls CLEAN:
220
- // 华威豹 → 0 HUA WEI BAO → 32 (and the 32 CONTAIN 华威豹)
221
- // 小米 → 0 XIAOMI → 57632
222
- // 스타벅스 → 0
220
+ // Searching the characters of a non-Latin mark returns nothing; searching its transliteration
221
+ // returns records, and those records carry the characters. Both halves are needed to see it —
222
+ // the empty answer alone is indistinguishable from a mark nobody has filed.
223
223
  // Universal, not a CJK-specific behaviour: non-Latin records across CN/TW/JP/KR/TH/GR/UA/EG/IL/SA
224
- // carried a populated markTransliteration.
224
+ // carry a populated markTransliteration.
225
225
  //
226
226
  // This declaration is what makes the refusal a CONTRACT rather than one vendor file's hand-written
227
227
  // check: the shared executor (providers/_shared/execute-plan.mjs, via script-form.mjs) reads it and
@@ -250,7 +250,7 @@ export const CAPABILITIES = Object.freeze({
250
250
  // makeEnumerate({ capabilities: {...CAPABILITIES.kernel} }) — these values are the LIVE seam settings,
251
251
  // no longer a design note. pageGuard is 1 because /search is single-shot: there is no page 2 to
252
252
  // fetch, so the guard can only ever be a backstop. namesChunkDefault = maxOrWidth (500): the kernel
253
- // chunks a wide OR-stack to the parser's probed nesting bound before it reaches the wire.
253
+ // chunks a wide OR-stack to the parser's nesting bound before it reaches the wire.
254
254
  kernel: Object.freeze({
255
255
  countProbe: "endpoint",
256
256
  screenSource: "billed-record-fetch",
@@ -1211,7 +1211,7 @@ export async function doBatchScreen(apiKey, base, params, tctx) {
1211
1211
  // judgment was shown a native-script mark beside its own romanised query with no reading on
1212
1212
  // either and concluded they were different marks. A delivered report told a client a
1213
1213
  // jurisdiction had not been searched in its own script while the run held both the query and
1214
- // this value. On the measured round 558 of 1,937 records had one to carry.
1214
+ // this value. A large share of records carry one, so a dropped reading is not a rare edge.
1215
1215
  //
1216
1216
  // Null where the office records none, which is most Latin-script filings: this says what the
1217
1217
  // register says, and inventing a romanisation here would be this row certifying a reading
@@ -192,7 +192,7 @@ export const DEFAULT_SEARCH_FIELDS = [
192
192
  // ONE JSON envelope away: a 200 whose body is `{"message":"upstream search cluster unavailable"}`
193
193
  // is valid JSON, so parseError never fires — and the old fallback coerced "an object with no
194
194
  // totalHitCount" to 0, on a comment claiming a present body is "the provider answering". It is not.
195
- // A SEARCH RESPONSE is a body that carries the search-response shape this endpoint was probed to
195
+ // A SEARCH RESPONSE is a body that carries the search-response shape this endpoint is documented to
196
196
  // return: totalHitCount (the count), rows (the records) or nextRequest (the paging cursor). A
197
197
  // parseable body with none of the three — an error envelope, a gateway stub — is the provider
198
198
  // saying something OTHER than an answer, and it rides out as a non-answer (null total, an error
@@ -435,7 +435,7 @@ export async function doExpandPhoneme(sessionKey, params, tctx) {
435
435
  // provider); they are RE-EXPORTED here unchanged so every existing importer — engine/mcp/corsearch-server.mjs,
436
436
  // register-plan.mjs, the driver — keeps resolving them from this module with identical semantics.
437
437
  // classifyStatus carries brand-json's vocabulary (Valid/Pending/GracePeriod live; Invalid/Expired dead;
438
- // anything else AMBIGUOUS → never auto-drop). BATCH_SCREEN_CHUNK = 100 = the observed brand-json page size.
438
+ // anything else AMBIGUOUS → never auto-drop). BATCH_SCREEN_CHUNK = 100 matches the brand-json page size.
439
439
  export { BATCH_SCREEN_CHUNK, chunk, classifyStatus, isAllClass, normalizeBrandRow, screenVerdict };
440
440
 
441
441
  export async function doBatchScreen(sessionKey, params, tctx) {