clearotron 0.4.0 → 0.4.1-beta.0

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 (96) hide show
  1. package/THIRD-PARTY-NOTICES.md +2 -2
  2. package/build-info.json +2 -2
  3. package/docs/SECURITY-OWASP.md +21 -13
  4. package/docs/SECURITY.md +20 -14
  5. package/docs/architecture/04-configuration-reference.md +3 -2
  6. package/docs/architecture/05-config-governance.md +1 -1
  7. package/docs/architecture/06-operations-runbook.md +1 -1
  8. package/docs/architecture/09-security-and-data.md +16 -11
  9. package/driver/CHANGELOG.md +18 -0
  10. package/driver/binding-layers.mjs +28 -6
  11. package/driver/citation-census.json +3 -3
  12. package/driver/common-law-coverage-status.mjs +52 -0
  13. package/driver/common-law-receipts.mjs +1 -1
  14. package/driver/connotation-search.mjs +5 -5
  15. package/driver/contract-dictation-registry.mjs +1 -1
  16. package/driver/contract-e3-backlog.mjs +1 -1
  17. package/driver/contract-vocabulary.mjs +18 -18
  18. package/driver/degraded-parts.mjs +10 -3
  19. package/driver/dispatch-record.mjs +19 -0
  20. package/driver/doubt-closure-tool.mjs +2 -2
  21. package/driver/driver.config.mjs +39 -6
  22. package/driver/engagement-receipt.mjs +1 -1
  23. package/driver/engine/CONTRACT.md +41 -1
  24. package/driver/engine/anthropic-agent.mjs +56 -4
  25. package/driver/engine/mcp/codex-config.mjs +1 -1
  26. package/driver/engine/mcp/coverage-server.mjs +1 -1
  27. package/driver/engine/mcp/gather-config.mjs +1 -1
  28. package/driver/engine/mcp/recording-server.mjs +31 -4
  29. package/driver/engine/mcp/stdio-server.mjs +45 -7
  30. package/driver/engine/openai-agent.mjs +100 -17
  31. package/driver/engine/session-record.mjs +223 -0
  32. package/driver/enqueue-schema.mjs +2 -2
  33. package/driver/findings-model.mjs +3 -3
  34. package/driver/form-neighbourhood.mjs +7 -7
  35. package/driver/frame-diff-model.mjs +3 -3
  36. package/driver/framework.mjs +22 -0
  37. package/driver/gateway.mjs +94 -12
  38. package/driver/named-band.mjs +2 -0
  39. package/driver/package.json +1 -1
  40. package/driver/phase0.mjs +13 -2
  41. package/driver/pipeline-knockout.mjs +1 -1
  42. package/driver/pipeline.mjs +46 -33
  43. package/driver/portal-families.mjs +3 -3
  44. package/driver/portal-service.mjs +1 -1
  45. package/driver/predelivery-lint.mjs +19 -19
  46. package/driver/publish/index.mjs +1 -1
  47. package/driver/publish/knockout.mjs +5 -4
  48. package/driver/publish/office-record-links.mjs +17 -2
  49. package/driver/publish/publish-inputs.mjs +1 -1
  50. package/driver/publish/render-knockout.mjs +17 -5
  51. package/driver/publish/render.mjs +4 -2
  52. package/driver/record-fold.mjs +177 -0
  53. package/driver/register-count.mjs +18 -1
  54. package/driver/register-plan.mjs +71 -17
  55. package/driver/register-records.mjs +7 -0
  56. package/driver/registry-fidelity.mjs +2 -2
  57. package/driver/repairs.mjs +1 -1
  58. package/driver/report-card-record.mjs +4 -4
  59. package/driver/scope-facts.mjs +11 -3
  60. package/driver/search-policy.mjs +20 -1
  61. package/driver/skills/clearance-common-law/SKILL.md +22 -22
  62. package/driver/skills/clearance-register/digest.md +3 -3
  63. package/driver/skills/clearance-register/register-recipes.md +11 -11
  64. package/driver/skills/clearance-register/status-rules.md +1 -1
  65. package/driver/skills/clearance-register/unit.md +1 -1
  66. package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
  67. package/driver/skills/clearance-search/phase2-execution.md +6 -5
  68. package/driver/skills/clearance-search/synthesis-rules.md +4 -4
  69. package/driver/skills/clearance-search/worked-examples.md +39 -39
  70. package/driver/skills/clearance-variants/SKILL.md +16 -16
  71. package/driver/skills/frame-diff/SKILL.md +2 -2
  72. package/driver/stage-context.mjs +27 -1
  73. package/driver/stages.mjs +12 -12
  74. package/driver/suite-census.json +175 -31
  75. package/driver/synthesis-record.mjs +5 -0
  76. package/driver/unit-inventory.mjs +17 -28
  77. package/driver/verify.mjs +6 -6
  78. package/mcp-server/CHANGELOG.md +4 -0
  79. package/mcp-server/lib/evidence.mjs +8 -8
  80. package/mcp-server/lib/knockout.mjs +1 -1
  81. package/mcp-server/package.json +2 -2
  82. package/package.json +2 -2
  83. package/portal-ui/package.json +1 -1
  84. package/providers/_shared/count.mjs +4 -1
  85. package/providers/_shared/enumerate.mjs +61 -22
  86. package/providers/_shared/execute-plan.mjs +2 -0
  87. package/providers/corsearch/src/capabilities.js +1 -1
  88. package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
  89. package/providers/oauth-mcp-bridge/package.json +2 -2
  90. package/providers/signa/src/capabilities.js +17 -2
  91. package/providers/signa/src/core.js +234 -37
  92. package/scripts/band-shape-probe.mjs +2 -2
  93. package/scripts/deprecate-below.mjs +28 -4
  94. package/scripts/e2e-first-time.mjs +16 -1
  95. package/scripts/freeze-example-run.mjs +2 -2
  96. package/shared/scope.mjs +18 -2
@@ -366,6 +366,15 @@ export const planMaxOrWidth = (capabilities) =>
366
366
  Number.isFinite(capabilities?.maxOrWidth) && capabilities.maxOrWidth >= 1
367
367
  ? Math.floor(capabilities.maxOrWidth) : PLAN_MAX_OR_WIDTH;
368
368
 
369
+ /**
370
+ * The OR-stack width for an EXACT spelling band. A provider whose general width is narrower than what it
371
+ * can take for exact spellings declares `exactOrWidth` (signa: one term per request in general, up to 100
372
+ * exact spellings as one ranked list); every other provider stacks exact spellings at its general width.
373
+ */
374
+ export const planExactOrWidth = (capabilities) =>
375
+ Number.isFinite(capabilities?.exactOrWidth) && capabilities.exactOrWidth >= 1
376
+ ? Math.floor(capabilities.exactOrWidth) : planMaxOrWidth(capabilities);
377
+
369
378
  // The plan emits ONE `wildcard` predicate; the provider contract splits it into three sub-capabilities.
370
379
  // Mirrors the executor's planPredicateParams anchoring exactly (trailing * → prefix/starts-with,
371
380
  // leading * → suffix/ends-with, both/neither → infix over the raw pattern).
@@ -1111,26 +1120,31 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1111
1120
  // anywhere: the exact silent-wrong-query false-clean class this carriage fix exists to kill,
1112
1121
  // quieter than the loud deferral it replaced. formKey now strips accents from LATIN bases only
1113
1122
  // and preserves every non-Latin combining mark (width still folds via NFKD).
1114
- // - and if two variants STILL share a key while dictating different romanisations (the model
1115
- // contradicting itself about one term), the key is poisoned rather than resolved by position:
1116
- // those entries compile BARE, and the romanisation-index provider's refusal turns the
1117
- // contradiction into a loud disclosed deferral — never into whichever answer came first.
1123
+ // - and two variants that share a key while dictating DIFFERENT romanisations carry EVERY one of
1124
+ // them, on every entry for that key. One string of characters can have two readings: the same
1125
+ // Han characters are read one way in Chinese and another in Japanese, and the manifest lists the
1126
+ // string once per reading. This rule used to read that as the model contradicting itself and
1127
+ // compile the key bare, so the romanisation-index provider refused it and the slice reached the
1128
+ // client as "not searched" — while the model, seeing the refusal, searched both readings as
1129
+ // supplementals and got its answer (a delivered report, 2026-10-08). Carrying every reading still never
1130
+ // resolves by position, and never sends a sibling's form in place of the dictated one: each
1131
+ // reading is asked, in the same request, and a reading that is simply wrong costs one spelling.
1118
1132
  const romanByValue = new Map();
1119
1133
  for (const v of manifest.variants) {
1120
1134
  if (!v.romanization) continue;
1121
1135
  const key = formKey(v.value);
1122
- const prior = romanByValue.get(key);
1123
- if (prior === undefined) romanByValue.set(key, v.romanization);
1124
- else if (prior !== null && romanizationSpellings(prior)[0] !== romanizationSpellings(v.romanization)[0])
1125
- romanByValue.set(key, null); // conflicting dictates ⇒ stamp NOTHING for this key (loud backstop)
1136
+ const readings = romanByValue.get(key) ?? [];
1137
+ const reading = romanizationSpellings(v.romanization)[0];
1138
+ if (reading && !readings.some((r) => romanizationSpellings(r)[0].toUpperCase() === reading.toUpperCase()))
1139
+ readings.push(v.romanization);
1140
+ romanByValue.set(key, readings);
1126
1141
  }
1127
1142
  const romanStamp = (e) => {
1128
1143
  // An owner NAME is not mark text and rides its own field (the executor drops the carrier on an
1129
1144
  // owner query anyway). An OR-stack is chunked, not substituted — one non-Latin member must never
1130
1145
  // silently replace a whole chunk's names, so a `terms[]` entry never carries the field.
1131
1146
  if (e.predicate === "owner" || Array.isArray(e.terms)) return {};
1132
- const roman = romanByValue.get(formKey(e.term));
1133
- const spellings = roman ? romanizationSpellings(roman) : [];
1147
+ const spellings = [...new Set((romanByValue.get(formKey(e.term)) ?? []).flatMap(romanizationSpellings))];
1134
1148
  return spellings.length ? { romanizedTerms: spellings } : {};
1135
1149
  };
1136
1150
 
@@ -1266,7 +1280,7 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1266
1280
  // this compiler's own equivalence: `norm` and `formKey` both strip separators, so "BIO VELTRIS",
1267
1281
  // "BIOVELTRIS", "BIO-VELTRIS" and "BIO.VELTRIS" are ONE key. The variant loop below already drops a
1268
1282
  // model variant that restates the mark that way, on exactly that test. Dispatching the generated set
1269
- // would send the mark's own exact entry three more times under a different spelling — the PARADISE
1283
+ // would send the mark's own exact entry three more times under a different spelling — the Drivers Haven
1270
1284
  // shape (five diacritic terms, same 424 hits, the run's record budget gone), re-imported. The family
1271
1285
  // is enumerated and marked in form-neighbourhood.json because it is genuinely mechanical and the
1272
1286
  // surfaces that do NOT fold spacing are real; wiring it to those is not this change.
@@ -1472,8 +1486,11 @@ export function compileRegisterPlan({ manifest, job, form = null, skillVersion =
1472
1486
  const formTerms = [...formBand.exactQueries];
1473
1487
  const latinFormTerms = formTerms.filter((t) => !isNonLatinTerm(t));
1474
1488
  const nativeFormTerms = formTerms.filter((t) => isNonLatinTerm(t));
1475
- for (let i = 0; i < latinFormTerms.length; i += maxOrWidth)
1476
- push({ axis: "primary-sweep", predicate: "exact", terms: latinFormTerms.slice(i, i + maxOrWidth), expected_kind: "enumerate", provenance: "floor", qidSuffix: "+form" });
1489
+ // An exact band stacks at the provider's EXACT width, which is wider than its general one where the
1490
+ // register takes a list of exact spellings in one request (planExactOrWidth).
1491
+ const exactWidth = planExactOrWidth(caps);
1492
+ for (let i = 0; i < latinFormTerms.length; i += exactWidth)
1493
+ push({ axis: "primary-sweep", predicate: "exact", terms: latinFormTerms.slice(i, i + exactWidth), expected_kind: "enumerate", provenance: "floor", qidSuffix: "+form" });
1477
1494
  for (const t of nativeFormTerms)
1478
1495
  push({ axis: "primary-sweep", predicate: "exact", term: t, expected_kind: "enumerate", provenance: "floor", qidSuffix: "+form" });
1479
1496
  }
@@ -2173,7 +2190,8 @@ export function validatePlanFeasibility(plan, { capabilities = null, maxOrWidth
2173
2190
  if (!PLAN_PREDICATES.includes(e?.predicate)) add("unexecutable", `unknown predicate "${e?.predicate}"`);
2174
2191
  const names = Array.isArray(e?.terms) ? e.terms : e?.term != null ? [e.term] : [];
2175
2192
  if (!names.length || names.some((t) => !String(t ?? "").trim())) add("unexecutable", "empty term(s)");
2176
- if (Array.isArray(e?.terms) && e.terms.length > maxOrWidth) add("repairable", `OR-stack of ${e.terms.length} names exceeds the executor bound (${maxOrWidth}) — the executor runs it chunked`);
2193
+ const width = e?.predicate === "exact" && capabilities ? Math.max(maxOrWidth, planExactOrWidth(capabilities)) : maxOrWidth;
2194
+ if (Array.isArray(e?.terms) && e.terms.length > width) add("repairable", `OR-stack of ${e.terms.length} names exceeds the executor bound (${width}) — the executor runs it chunked`);
2177
2195
  for (const t of names) if (String(t).length > maxNameLength) add("repairable", `name exceeds ${maxNameLength} chars ("${String(t).slice(0, 40)}…") — provider-side truncation risk only`);
2178
2196
  if ((e?.nice_classes ?? []).some((c) => !Number.isFinite(Number(c)))) add("unexecutable", "non-numeric nice_class");
2179
2197
  if (e?.when && guardParentQid(e.when) != null && !qids.has(guardParentQid(e.when))) add("unexecutable", `when-guard targets unknown qid "${guardParentQid(e.when)}"`);
@@ -2359,7 +2377,10 @@ export function joinPlanToBands(plan, bandBlocksByAxis, { released = new Set() }
2359
2377
  // and the report's coverage line claims to: it reads "skipped after a broader search came back
2360
2378
  // crowded", which is true when the parent was `incomplete` and false when the parent FAILED or
2361
2379
  // never ran. The state that decides it is read one line above and was thrown away here.
2362
- if (parentState !== "enumerated") { skipped.push({ qid: e.qid, guard: e.when.runs_if_enumerated, parent_state: parentState || null }); continue; }
2380
+ // AND WHETHER IT ERRED, because the state cannot say: a band has two states, and the executor writes
2381
+ // every provider error as `incomplete` with `error: true` beside it. Read off the state alone, a
2382
+ // broader search that failed was a broader search that crowded.
2383
+ if (parentState !== "enumerated") { skipped.push({ qid: e.qid, guard: e.when.runs_if_enumerated, parent_state: parentState || null, ...(parent?.error === true ? { parent_error: true } : {}) }); continue; }
2363
2384
  }
2364
2385
  }
2365
2386
  const b = byQid.get(e.qid);
@@ -2404,6 +2425,14 @@ export function joinPlanToBands(plan, bandBlocksByAxis, { released = new Set() }
2404
2425
  state: String(b.state ?? "").toLowerCase(),
2405
2426
  records: recs,
2406
2427
  total_hits: Number.isFinite(hits) ? hits : null,
2428
+ // A SLICE THAT RAN AND COULD NOT BE ANSWERED. The executor stamps `deferred: true` with no error on an
2429
+ // owner sweep whose name the register never resolved: the call ran, so it is executed, and the band
2430
+ // says `incomplete` with no count. The deferral is the only thing that tells it from a listing that
2431
+ // overflowed, so it rides with the row rather than stopping at the block.
2432
+ ...(b.deferred === true ? { deferred: true } : {}),
2433
+ // WHAT THE TOTAL COUNTS, when the register said: `records`, one per country a mark covers. Every
2434
+ // reader of this row reads the count as the register's own number (ruled 2026-10-02).
2435
+ ...(typeof b.total_counts === "string" ? { total_counts: b.total_counts } : {}),
2407
2436
  });
2408
2437
  }
2409
2438
  const planQids = new Set(plan.entries.map((e) => e.qid));
@@ -2613,9 +2642,30 @@ export function deriveCoverageSkeleton(plan, join) {
2613
2642
  const deferredQids = new Set((join.deferred ?? []).map((x) => x.qid));
2614
2643
  const awaitingQids = new Set((join.awaiting ?? []).map((x) => x.qid));
2615
2644
  const askedQids = new Set((join.asked ?? []).map((x) => x.qid));
2645
+ // ── A COUNT THE PLAN ALSO READ IN FULL IS NOT A CROWD ──────────────────────────────────────────
2646
+ // A count-only descriptor (the saturation probe's shape) sizes a question and reads none of its
2647
+ // records, so it lands `incomplete` and its axis reads as a crowd. Where another entry asked the
2648
+ // IDENTICAL question — predicate, term, classes, territories, owner and goods — and listed every
2649
+ // record of the same total, nothing the count describes is unread. Leaving the axis `incomplete` told
2650
+ // the reading seat a set was unreviewed that had been read in full, and a delivered report made its
2651
+ // verdict conditional on reviewing it (2026-10-08). Anything short of that stays a crowd.
2652
+ const entryByQid = new Map(plan.entries.map((e) => [e.qid, e]));
2653
+ const question = (e) => (!e || Array.isArray(e.terms) ? null : JSON.stringify([
2654
+ String(e.predicate ?? ""), formKey(String(e.term ?? "")),
2655
+ [...(e.nice_classes ?? [])].map(String).sort(), [...(e.regions ?? [])].map(String).sort(),
2656
+ String(e.owner ?? ""), goodsTermsList(e)]));
2657
+ const readInFull = new Map();
2658
+ for (const x of join.executed) {
2659
+ if (x.state !== "enumerated" || !Number.isInteger(x.total_hits) || x.records !== x.total_hits) continue;
2660
+ const q = question(entryByQid.get(x.qid));
2661
+ if (q) readInFull.set(q, x.total_hits);
2662
+ }
2663
+ const totalByQid = new Map(join.executed.map((x) => [x.qid, x.total_hits]));
2664
+ const readElsewhere = (e) => e.expected_kind === "count" && Number.isInteger(totalByQid.get(e.qid))
2665
+ && readInFull.get(question(e)) === totalByQid.get(e.qid);
2616
2666
  const axes = new Map();
2617
2667
  for (const e of plan.entries) {
2618
- if (!axes.has(e.axis)) axes.set(e.axis, { axis: e.axis, entries: 0, executed: 0, crowds: 0, missing: [], skipped: 0, deferred: [], awaiting: 0, asked: 0 });
2668
+ if (!axes.has(e.axis)) axes.set(e.axis, { axis: e.axis, entries: 0, executed: 0, crowds: 0, missing: [], skipped: 0, deferred: [], awaiting: 0, asked: 0, readElsewhere: 0 });
2619
2669
  const a = axes.get(e.axis);
2620
2670
  a.entries++;
2621
2671
  // COUNTED BEFORE `skipped`, and separately from it. With the families waiting on every matter this is the ordinary state of
@@ -2631,7 +2681,10 @@ export function deriveCoverageSkeleton(plan, join) {
2631
2681
  // a capability gap is NOT executed — it is a disclosed hole in the axis
2632
2682
  if (deferredQids.has(e.qid)) { a.deferred.push(e.qid); continue; }
2633
2683
  a.executed++;
2634
- if (stateByQid.get(e.qid) === "incomplete") a.crowds++;
2684
+ if (stateByQid.get(e.qid) === "incomplete") {
2685
+ if (readElsewhere(e)) a.readElsewhere++;
2686
+ else a.crowds++;
2687
+ }
2635
2688
  }
2636
2689
  return [...axes.values()].map((a) => ({
2637
2690
  axis: a.axis,
@@ -2655,6 +2708,7 @@ export function deriveCoverageSkeleton(plan, join) {
2655
2708
  entries: a.entries, executed: a.executed, crowds: a.crowds, skipped: a.skipped, missing: a.missing,
2656
2709
  ...(a.awaiting ? { awaiting: a.awaiting } : {}),
2657
2710
  ...(a.asked ? { asked: a.asked } : {}),
2711
+ ...(a.readElsewhere ? { read_elsewhere: a.readElsewhere } : {}),
2658
2712
  ...(a.deferred.length ? { deferred: a.deferred } : {}),
2659
2713
  }));
2660
2714
  }
@@ -149,6 +149,9 @@ function toRecord(row, { term, basis, provider }) {
149
149
  registrationNumber: row?.registration_number ?? null,
150
150
  irNumber: row?.ir_number ?? null,
151
151
  filingRoute: row?.filing_route ?? null,
152
+ // The office's page as the register gave it, where it gave one; publish uses it only when the numbers
153
+ // above do not address the office's page.
154
+ officeUrl: row?.office_url ?? null,
152
155
  // WHICH QUESTION FOUND IT. Without this a reader cannot tell a filing on the name from a filing on
153
156
  // a generated variant, and the two mean very different things to the person deciding on the name.
154
157
  matchedForm: term,
@@ -248,6 +251,7 @@ export async function listRegisterRecords({
248
251
  classes: scoped, regions, provider, ok, requested: want, fetched,
249
252
  total: Number.isFinite(r?.total) ? r.total : null, took_ms: Date.now() - started,
250
253
  ...(ok && r?.approximate === true ? { approximate: true, floor: Number.isFinite(r?.floor) ? r.floor : null } : {}),
254
+ ...(ok && Array.isArray(r?.warnings) && r.warnings.length ? { warnings: r.warnings } : {}),
251
255
  ...(ok ? {} : { cause: String(r?.reason ?? "unknown").slice(0, 300) }),
252
256
  }) + "\n");
253
257
  } catch { /* receipts are best-effort, never fatal */ }
@@ -260,6 +264,9 @@ export async function listRegisterRecords({
260
264
  // An approximation is carried as one, exactly as the count lane records it: no number in
261
265
  // `total`, the register's floor beside it. Only a provider whose listing states it sets it.
262
266
  ...(ok && r?.approximate === true ? { approximate: true, floor: Number.isFinite(r?.floor) ? r.floor : null } : {}),
267
+ // The register's own notes on this answer, as it sent them — absent when it sent none, which is
268
+ // not evidence that the answer carried no caveat on a register that sends none at all.
269
+ ...(ok && Array.isArray(r?.warnings) && r.warnings.length ? { warnings: r.warnings } : {}),
263
270
  ...(ok ? {} : { reason: String(r?.reason ?? "the filings could not be fetched").slice(0, 300) }),
264
271
  };
265
272
  });
@@ -1193,8 +1193,8 @@ export function findRegistryArithmeticIssues(text) {
1193
1193
  // most national registers actually do — is TEN YEARS FROM FILING, with registration landing
1194
1194
  // somewhere in between. Two live records from the 2026-07-28 worldwide run, both correct and both
1195
1195
  // failed by the old rule:
1196
- // ZA SUPA STICKY filed 2023-07-12 registered 2025-04-23 expires 2033-07-12 (filing +10)
1197
- // PA TAKIS filed 2023-10-19 registered 2025-04-16 expires 2033-10-19 (filing +10)
1196
+ // ZA FROSTY GRIP filed 2023-07-12 registered 2025-04-23 expires 2033-07-12 (filing +10)
1197
+ // PA WAVA filed 2023-10-19 registered 2025-04-16 expires 2033-10-19 (filing +10)
1198
1198
  // It blocked delivery of a finished report — "expiry 2033 is not a 10-year cycle from claimed
1199
1199
  // registration 2025" — over data that was right. So the cycle may anchor on EITHER year when both
1200
1200
  // are stated; an expiry that fits neither is still a real arithmetic fault and still fails.
@@ -708,7 +708,7 @@ export function decideRecovery({ failClass, sig, reason = "", history = [], prio
708
708
  // the literal remedy for a provider that is down or refusing, so the second occurrence earns the
709
709
  // next rung. Everything else keeps repeat-terminal:
710
710
  // transient-but-wedged — nonzero_exit, unparseable_json, lane_wedge, a stage that times out every
711
- // time: a byte-identical repeat means it is stuck, and three parks is the Wilderness shape
711
+ // time: a byte-identical repeat means it is stuck, and three parks is the Open Country shape
712
712
  // stale — the remedy is a deterministic recompute; if the recompute did not settle it, running
713
713
  // it again cannot (the real fix for the observed stale kill is restamping, not retries)
714
714
  // unknown — its one fresh sample already failed identically, so it has disproved itself
@@ -110,9 +110,9 @@ export function findingsDocFor(runDir, ordinal) {
110
110
  // "jurisdiction"}` — there is no `register` and no `number`, on any record, ever. So both fallbacks
111
111
  // fired on every card and the composed bullet read
112
112
  //
113
- // - Source: [register-euipo · /mark/eu/018575624](https://euipo.europa.eu/…)
113
+ // - Source: [register-euipo · /mark/eu/099999992](https://euipo.europa.eu/…)
114
114
  //
115
- // where the corpus it replaced reads `- Source: [EUIPO · 018575624](…)`. An internal enum token and an
115
+ // where the corpus it replaced reads `- Source: [EUIPO · 099999992](…)`. An internal enum token and an
116
116
  // internal URI path, in the one line of a client-facing card whose job is to say which register holds
117
117
  // the record and under what number. publish/index.mjs quotes the correct shape verbatim in a comment of
118
118
  // its own, which is what makes the divergence provable rather than a matter of taste.
@@ -122,7 +122,7 @@ export function findingsDocFor(runDir, ordinal) {
122
122
  // delivered report.md" — and writing it is what caught this. The guard now lives beside the code it checks, and
123
123
  // this is the argument for building the guard an issue asks for even when the code already looks done.
124
124
  //
125
- // THE NUMBER comes off the URI's last segment: `/mark/eu/018575624` → `018575624`. That is a parse, and
125
+ // THE NUMBER comes off the URI's last segment: `/mark/eu/099999992` → `099999992`. That is a parse, and
126
126
  // a parse is what this file exists to avoid — but the alternative is a field that does not exist, and
127
127
  // the URI's shape is the driver's own (`/mark/<jurisdiction>/<id>`, dictated in gateway.mjs and bound
128
128
  // from the fetched record). A URI that does not match it yields NO number rather than a wrong one, and
@@ -136,7 +136,7 @@ export function findingsDocFor(runDir, ordinal) {
136
136
  // against evidence that does not exist.
137
137
  const REGISTER_OFFICE = Object.freeze({ "register-euipo": "EUIPO" });
138
138
 
139
- /** The registration number inside a driver record URI (`/mark/eu/018575624`), or `""`. PURE. */
139
+ /** The registration number inside a driver record URI (`/mark/eu/099999992`), or `""`. PURE. */
140
140
  export function registrationNumber(uri) {
141
141
  const m = /^\/mark\/[a-z]{2}\/([A-Za-z0-9._-]+)$/.exec(str(uri));
142
142
  return m ? m[1] : "";
@@ -77,9 +77,12 @@ export function deriveScopeFacts({ instructedScope = null, plan = null, planExec
77
77
  // nothing. This reader took the state and dropped the number, and the coverage line then described a
78
78
  // slice that found nothing as one that "returned more records than could be listed in full".
79
79
  const hitsByQid = new Map((planExecution?.executed ?? []).map((x) => [x.qid, Number.isFinite(x.total_hits) ? x.total_hits : null]));
80
+ // A slice that ran and that the executor deferred: the register could not answer it (an owner name it never
81
+ // resolved), which is positive evidence it was not an overflow. See the producer.
82
+ const deferredExecutedSet = new Set((planExecution?.executed ?? []).filter((x) => x?.deferred === true).map((x) => x.qid));
80
83
  const missingSet = new Set(planExecution?.missing ?? []);
81
84
  // The skip's own reason: why the broader search did not enumerate. See the producer.
82
- const skippedByQid = new Map((planExecution?.skipped ?? []).map((x) => [x.qid, String(x.parent_state ?? "")]));
85
+ const skippedByQid = new Map((planExecution?.skipped ?? []).map((x) => [x.qid, { state: String(x.parent_state ?? ""), error: x.parent_error === true }]));
83
86
  const skippedSet = new Set(skippedByQid.keys());
84
87
  const deferredByQid = new Map((planExecution?.deferred ?? []).map((x) => [x.qid, String(x.reason ?? "")]));
85
88
  // A WAITING FAMILY THAT NEVER BECAME A SEARCH OF ITS OWN LEAVES THE COUNT. One the reading turn asked
@@ -131,8 +134,11 @@ export function deriveScopeFacts({ instructedScope = null, plan = null, planExec
131
134
  // AN ABSENT STATE IS NOT EVIDENCE, and this is the same trap as the count below: a receipt written
132
135
  // before the producer carried the parent's state says nothing about why, and reading its silence as
133
136
  // a failure would relabel every archived run's skips. Silence leaves the line as it was.
137
+ //
138
+ // AND `incomplete` IS NOT EVIDENCE OF A CROWD ON ITS OWN: the executor writes a provider error as an
139
+ // `incomplete` band with `error: true` beside it, and the producer now carries that flag with the skip.
134
140
  const parent = skippedByQid.get(e.qid);
135
- if (parent && parent !== "incomplete") a.skipped_after_failure++;
141
+ if (parent && (parent.error || (parent.state && parent.state !== "incomplete"))) a.skipped_after_failure++;
136
142
  continue;
137
143
  }
138
144
  if (missingSet.has(e.qid)) { a.missing.push(e.qid); continue; }
@@ -170,7 +176,9 @@ export function deriveScopeFacts({ instructedScope = null, plan = null, planExec
170
176
  // rather than too much. Only a MEASURED zero demotes it: a null is a count that could not be
171
177
  // taken, which is not evidence either way, and reading absence as evidence is the defect this
172
178
  // whole file keeps meeting.
173
- if (hitsByQid.get(e.qid) === 0) a.incomplete_found_nothing++;
179
+ // An owner sweep the executor deferred is the same fact reached another way: the register could not
180
+ // resolve the name, so the search found nothing to list, and there is no count to measure.
181
+ if (hitsByQid.get(e.qid) === 0 || deferredExecutedSet.has(e.qid)) a.incomplete_found_nothing++;
174
182
  }
175
183
  } else {
176
184
  // a plan entry in NO execution bucket is UNACCOUNTED — the fail-closed reading is "missing"
@@ -1165,7 +1165,26 @@ export function countJobMarks(job) {
1165
1165
  // The mark → filesystem-key derivation (research payloads, fixtures). ONE definition — the knockout
1166
1166
  // lane keys per-mark artifacts on it, so two batch marks that collide here ("MOTO X" / "MOTO-X") would
1167
1167
  // silently share a research payload; every door checks collisions with THIS function.
1168
- export const kebab = (s) => String(s ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "mark";
1168
+ //
1169
+ // THE KEY IS UNIQUE FOR TWO NAMES THAT DIFFER IN ANY LETTER OR NUMBER, in any script. The readable part
1170
+ // keeps only `a-z0-9`, because the key is also a report route segment. It used to be the whole key, so
1171
+ // every name written wholly in Greek, Cyrillic or Han keyed as `mark`, and two different names of that
1172
+ // kind were refused as duplicates at intake; two Latin names differing only in an accented letter merged
1173
+ // the same way. A name whose letters and numbers the readable part does not hold in full now carries a
1174
+ // short hash of them, NFKC-folded and lowercased. Spacing, punctuation, symbols and case still change
1175
+ // nothing, so a true duplicate still collides. A name the readable part already held in full keys exactly
1176
+ // as before, byte for byte: research payloads, published report files and per-name links were keyed on it.
1177
+ // SYMBOLS GO BEFORE THE FOLD. NFKC turns ™ into "TM" and ℠ into "SM", which would move "BRAND™" off the key
1178
+ // it shares with "BRAND"; symbols are the only class NFKC turns into letters or numbers, so dropping them
1179
+ // first leaves the fold acting on letters and numbers alone.
1180
+ const asciiKey = (s) => String(s ?? "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1181
+ const nameLetters = (s) => String(s ?? "").replace(/\p{S}/gu, "").normalize("NFKC").toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
1182
+ export const kebab = (s) => {
1183
+ const ascii = asciiKey(s);
1184
+ const letters = nameLetters(s);
1185
+ if (letters === ascii.replace(/-/g, "")) return ascii.slice(0, 60) || "mark";
1186
+ return `${(ascii || "mark").slice(0, 51).replace(/-+$/, "")}-${createHash("sha256").update(letters).digest("hex").slice(0, 8)}`;
1187
+ };
1169
1188
  export function kebabCollisions(names) {
1170
1189
  const seen = new Map(); const out = [];
1171
1190
  for (const n of names ?? []) {
@@ -137,7 +137,7 @@ they render as nothing: no reader ever sees them.
137
137
  not need to match the template's wording, and you will not be failed for choosing different words.
138
138
 
139
139
  ```markdown
140
- # Common-law findings — Dawn: Legends of Thornmantle (2026-05-11)
140
+ # Common-law findings — Dusk: Tales of Thornmantle (2026-05-11)
141
141
 
142
142
  ## Summary
143
143
 
@@ -146,7 +146,7 @@ not need to match the template's wording, and you will not be failed for choosin
146
146
  - Findings surfaced: 12 risk-relevant + 3 competitor intel + 1 PR risk
147
147
  - Open verification flags: 2
148
148
 
149
- ## Findings — Mark: Dawn: Legends of Thornmantle
149
+ ## Findings — Mark: Dusk: Tales of Thornmantle
150
150
  <!-- clearotron:section=findings -->
151
151
 
152
152
  ### Consumer-confusion risks (gaming-industry overlap)
@@ -155,24 +155,24 @@ For game-title rows, the `developer_of_record` and `publisher_of_record` columns
155
155
 
156
156
  | Finding | Source / Platform | URL | developer_of_record | publisher_of_record | Type | Notes |
157
157
  |---|---|---|---|---|---|---|
158
- | "Chronicles of Ember" | Steam | https://... | Dreamatrix | Topware Interactive | Direct conflict — similar gaming title | "Mostly negative" reviews; commercially active 2013/2015 |
159
- | "Thornmantle" | itch.io | https://... | Thornfall Games | not extracted | Direct conflict — identical game-title | Single-player adventure, "made for SAE Studio 2 Home Brief"; little online presence; not reviewed in 7 years |
158
+ | "Tales of Dusk" | Steam | https://... | Glimmerhouse | Northgale Interactive | Direct conflict — similar gaming title | "Mostly negative" reviews; commercially active 2013/2015 |
159
+ | "Thornmantle" | itch.io | https://... | Thornfall Games | not extracted | Direct conflict — identical game-title | Single-player adventure made for a game-design course brief; little online presence; not reviewed in 7 years |
160
160
  | "Thornmantle: Of Magic and Power" | Moby Games | https://... | not extracted | not extracted | Direct conflict — historical gaming title | Released 2007; status unavailable; verify-publisher flag set |
161
- | "Thornmantle" Astragate ARPG | News / web | https://... | Astragate | not extracted | Direct conflict — gaming title | Browser game; appears cancelled |
162
- | "Ysolde the Thornmantle" | Pathfinder wiki | https://... | N/A | Grimtable Press | Conceptual — deity name in tabletop RPG | Tabletop, not video game; commercial risk in RPG space |
161
+ | "Thornmantle" Starhollow ARPG | News / web | https://... | Starhollow | not extracted | Direct conflict — gaming title | Browser game; appears cancelled |
162
+ | "Ysolde the Thornmantle" | tabletop RPG wiki | https://... | N/A | Grimtable Press | Conceptual — deity name in tabletop RPG | Tabletop, not video game; commercial risk in RPG space |
163
163
 
164
164
  ### Commercial awareness (identical/similar in unrelated fields)
165
165
 
166
166
  | Finding | Source / Platform | URL | Notes |
167
167
  |---|---|---|---|
168
- | "Raising Your Play" | HP marketing | https://... | HP uses tagline for gaming hardware; no register protection found (flagged for clearance-register cross-check) |
169
- | 1,600+ "Dawn" titles on Steam | Steam | https://... | Crowded field — supportive evidence |
168
+ | "Raising Your Play" | Brightwick marketing | https://... | Brightwick uses tagline for gaming hardware; no register protection found (flagged for clearance-register cross-check) |
169
+ | 1,600+ "Dusk" titles on Steam | Steam | https://... | Crowded field — supportive evidence |
170
170
 
171
171
  ### Competitor intelligence
172
172
 
173
173
  | Finding | Source / Platform | URL | Notes |
174
174
  |---|---|---|---|
175
- | Sony "Pulse Elevate" portfolio | Sony products | https://... | Sony uses "Elevate" in audio products; flagged for clearance-register cross-check |
175
+ | Veltrona "Pulse Raise" portfolio | Veltrona products | https://... | Veltrona uses "Raise" in audio products; flagged for clearance-register cross-check |
176
176
  | Foxglade "Borealis" console "Raise Your Play" tagline (prior usage) | Foxglade Interactive marketing | https://... | Client's own prior use — note as supportive |
177
177
 
178
178
  ### PR / reputational risk
@@ -187,7 +187,7 @@ means *blackberry*" is context, not a clearance: a connotation reads clean only
187
187
 
188
188
  | Finding | Source | Notes |
189
189
  |---|---|---|
190
- | meaning readings surfaced | Urban Dictionary / Wikipedia / news / forums | per form, the readings the search returned + benign/loaded label — e.g. `ELEVATE → ordinary verb "raise" (benign)`; near-forms `RAIZE / RAYSE → no slang/gang/offensive reading`. Write `(None identified)` as the *bottom line* only after the readings are laid out and none is loaded. |
190
+ | meaning readings surfaced | Urban Dictionary / Wikipedia / news / forums | per form, the readings the search returned + benign/loaded label — e.g. `RAISE → ordinary verb "lift" (benign)`; near-forms `RAIZE / RAYSE → no slang/gang/offensive reading`. Write `(None identified)` as the *bottom line* only after the readings are laid out and none is loaded. |
191
191
 
192
192
  A clean PR/connotation row MUST cite its search — add a `**Connotation-search source:** <URL | "perplexity_research — no result">` line. The driver dictates the meaning sweep into the grid (the matter frame's meaning questions) and the plugin records every query into the ledger's `extras.pr_risk[]`; the `commonLaw` validator rejects a `(None identified)` claim with no recorded queries (`connotation_search_missing`). An empty-results search is a clean receipt; a *missing* search is not.
193
193
 
@@ -340,13 +340,13 @@ forms (and the gap form) — this is what the driver's receipt gate counts:
340
340
 
341
341
  | Variant | Platform | Result |
342
342
  |---|---|---|
343
- | Dawn: Legends of Thornmantle | Steam | No results |
344
- | Dawn: Legends of Thornmantle | Epic Games Store | No similar listings (6 candidates reviewed) |
345
- | Dawn: Legends of Thornmantle | Google Play | No results |
346
- | Dawn: Legends of Thornmantle | Apple App Store | No similar listings (3 candidates reviewed) |
347
- | Dawn: Legends of Thornmantle | Microsoft Store | No results |
348
- | Dawn: Legends of Thornmantle | itch.io | Similar listing(s) found — see Findings (2 candidates) |
349
- | Dawn: Legends of Thornmantle | web | No similar listings (8 candidates reviewed) |
343
+ | Dusk: Tales of Thornmantle | Steam | No results |
344
+ | Dusk: Tales of Thornmantle | Epic Games Store | No similar listings (6 candidates reviewed) |
345
+ | Dusk: Tales of Thornmantle | Google Play | No results |
346
+ | Dusk: Tales of Thornmantle | Apple App Store | No similar listings (3 candidates reviewed) |
347
+ | Dusk: Tales of Thornmantle | Microsoft Store | No results |
348
+ | Dusk: Tales of Thornmantle | itch.io | Similar listing(s) found — see Findings (2 candidates) |
349
+ | Dusk: Tales of Thornmantle | web | No similar listings (8 candidates reviewed) |
350
350
  | エバーライト | Steam | not executed — coverage-limited (see ledger) |
351
351
 
352
352
  ### Coverage ledger (feeds synthesis coverage-honesty + skeptic audit)
@@ -366,8 +366,8 @@ Record the same statuses by calling `record_coverage_status`, passing `grid_spec
366
366
 
367
367
  | Trigger | Suggested cross-check |
368
368
  |---|---|
369
- | HP appears using "RAISING YOUR PLAY" | Check register: does HP have any elevate-related trademark in target classes? |
370
- | Sony "Pulse Elevate" portfolio | Check register: any Sony elevate filings in target classes? |
369
+ | Brightwick appears using "RAISING YOUR PLAY" | Check register: does Brightwick have any raise-related trademark in target classes? |
370
+ | Veltrona "Pulse Raise" portfolio | Check register: any Veltrona raise filings in target classes? |
371
371
  | Thornfall Games "Thornmantle" (itch.io) | Check register: any Thornfall trademark protection? |
372
372
 
373
373
  ### Audit trail
@@ -377,8 +377,8 @@ Record the same statuses by calling `record_coverage_status`, passing `grid_spec
377
377
  |---|---|---|---|
378
378
  | 1 | Grid (sandbox) | Search-as-code grid: 9 variants × 7 platforms + extras | 63 cells, 0 gaps, 487 candidates recorded; 12 risk-relevant after judgment |
379
379
  | 2 | Famous-mark | None flagged in manifest | (not run) |
380
- | 3 | Follow-up | HP gaming portfolio + "raising your play" deeper trace | 3 additional findings |
381
- | 4 | Follow-up | Sony pulse-elevate portfolio | 2 additional findings |
380
+ | 3 | Follow-up | Brightwick gaming portfolio + "raising your play" deeper trace | 3 additional findings |
381
+ | 4 | Follow-up | Veltrona pulse-raise portfolio | 2 additional findings |
382
382
 
383
383
  **Grid program receipt** — the executed sandbox program returned by the tool, verbatim:
384
384
 
@@ -389,7 +389,7 @@ Record the same statuses by calling `record_coverage_status`, passing `grid_spec
389
389
  ### Open verification flags
390
390
 
391
391
  - Thornfall Games "Thornmantle" itch.io listing — URL needs re-verification (404'd during follow-up call)
392
- - HP "Raising Your Play" marketing presence in EU — coverage was thin, may need a region-specific follow-up
392
+ - Brightwick "Raising Your Play" marketing presence in EU — coverage was thin, may need a region-specific follow-up
393
393
  ```
394
394
 
395
395
  ## Process
@@ -135,8 +135,8 @@ drop can be neither examined nor audited. Batching stays correct for *retrieval*
135
135
  enumeration, `_query` attribution); it is only forbidden as a unit of dismissal.
136
136
 
137
137
  One entry, one record, one reason. A `drop_reason` is the judgment alone — the uri, verdict, class and
138
- status are rendered beside it: `dropped — off-field (relevance gate): DAWN-only hit; the
139
- descriptive-compound risk theory retains DAWN-only hits only in gaming, and this is film production`.
138
+ status are rendered beside it: `dropped — off-field (relevance gate): DUSK-only hit; the
139
+ descriptive-compound risk theory retains DUSK-only hits only in gaming, and this is film production`.
140
140
 
141
141
  ### Dominant-element reconciliation (driver-gated — every screened composite ENDS somewhere)
142
142
 
@@ -435,7 +435,7 @@ read. Keep if plausibly on-point; drop only genuine field-irrelevance, or a near
435
435
  | Archetype | Keep when | Drop when |
436
436
  |---|---|---|
437
437
  | **Coined word** | Mark text contains a phonetic/visual neighbour of the coined element; or is the coined element exact-match | Mark text shares only an unrelated real-word token via tokenisation (e.g., searching coined THORNMANTLE and surfacing a mark whose only overlap is a "LIGHT" token) |
438
- | **Descriptive compound** | Mark contains the distinctive anchor; OR is an identical/near-identical compound-phrase match; OR is a single-element registered mark on a common element with goods/services in the target field | Mark is a 5+ element compound that happens to share one common token (e.g., a film studio's "DAWN OF JUSTICE" filing surfaced when searching "Dawn: Legends of Thornmantle" — DAWN is common, surrounding context unrelated) |
438
+ | **Descriptive compound** | Mark contains the distinctive anchor; OR is an identical/near-identical compound-phrase match; OR is a single-element registered mark on a common element with goods/services in the target field | Mark is a 5+ element compound that happens to share one common token (e.g., a film studio's "DUSK OF VALOR" filing surfaced when searching "Dusk: Tales of Thornmantle" — DUSK is common, surrounding context unrelated) |
439
439
  | **Slogan** | Mark matches the slogan structure (same template, same semantic field, family-pattern hit, verb-swap, slang variant, co-brand wildcard); OR is the unique distinctive token of the slogan if any | Single-word hit on a slogan element in an unrelated context (DAWN alone surfaced when searching "DAWN OF NEW DAY" slogan); slogan in a wildly different semantic field with no template overlap |
440
440
  | **Acronym / initialism** | Mark text contains the letter string in target classes; OR is the expansion form of the acronym | Random word that happens to contain the letters as a substring of a longer word |
441
441
  | **Device-led** (modifier) | Standard tests for primary archetype, plus visual similarity per design-code search | Pure-text noise with no visual signal |
@@ -46,13 +46,13 @@ the cross-cutting relevance / sufficiency / owner-aggregation / prioritisation o
46
46
 
47
47
  ## Recipe 1 — Compound tagline (multi-word mark with distinctive element)
48
48
 
49
- **When to use:** mark has 3+ words where 1 element is distinctive and the others are common (e.g., "Dawn: Legends of Lumengarde" — LUMENGARDE distinctive, DAWN + LEGENDS common).
49
+ **When to use:** mark has 3+ words where 1 element is distinctive and the others are common (e.g., "Dusk: Tales of Lumengarde" — LUMENGARDE distinctive, DUSK + TALES common).
50
50
 
51
51
  **Pattern:**
52
52
 
53
53
  ```
54
54
  1. Crowd descriptor — count-only probe each saturated common element (limit=1, fields=[uri])
55
- → name:DAWN classes 9,28,41,42 → write an `incomplete` crowd-descriptor block (count + reason)
55
+ → name:DUSK classes 9,28,41,42 → write an `incomplete` crowd-descriptor block (count + reason)
56
56
  → name:LEGEND classes 9,28,41,42 → write an `incomplete` crowd-descriptor block
57
57
  (these enumerate nothing — they describe the crowd for judgment)
58
58
 
@@ -60,11 +60,11 @@ the cross-cutting relevance / sufficiency / owner-aggregation / prioritisation o
60
60
  → name:LUMENGARDE classes 9,28,41,42 → enumerated|incomplete block (every record carried forward)
61
61
 
62
62
  3. ENUMERATE each common element as a named query in the in-scope classes
63
- → name:DAWN match=exact classes 9,28,41,42 → register_enumerate (exact-in-class name-list)
63
+ → name:DUSK match=exact classes 9,28,41,42 → register_enumerate (exact-in-class name-list)
64
64
  → name:LEGEND match=exact classes 9,28,41,42 → register_enumerate
65
65
 
66
66
  4. ENUMERATE the compound phrase
67
- → name:"Dawn: Legends of Lumengarde" match=phrase classes 9,28,41,42 → register_enumerate
67
+ → name:"Dusk: Tales of Lumengarde" match=phrase classes 9,28,41,42 → register_enumerate
68
68
 
69
69
  5. ENUMERATE a cross-class merch query if any identical match
70
70
  → name:<exact mark> match=exact classes 25 → register_enumerate
@@ -168,11 +168,11 @@ For each transliteration variant in the manifest:
168
168
 
169
169
  ```
170
170
  1. Crowd descriptors — count-only probes on the saturated common elements (limit=1, fields=[uri])
171
- → name:ELEVATE classes 9/28/41/42 → `incomplete` crowd-descriptor block
172
- → name:GAME classes 9/28/41/42 → `incomplete` crowd-descriptor block
171
+ → name:RAISE classes 9/28/41/42 → `incomplete` crowd-descriptor block
172
+ → name:PLAY classes 9/28/41/42 → `incomplete` crowd-descriptor block
173
173
 
174
174
  2. Crowd descriptor — partial-phrase prefix count (the narrative anchor)
175
- → phrase:"ELEVATE YOUR" classes <target> limit=1 → `incomplete` block ("234 live ELEVATE YOUR ___")
175
+ → phrase:"RAISE YOUR" classes <target> limit=1 → `incomplete` block ("234 live RAISE YOUR ___")
176
176
 
177
177
  3. ENUMERATE the compound phrase, 3 match-modes (register_enumerate each)
178
178
  → exact:"RAISE YOUR PLAY" classes <target> → identical-mark band
@@ -186,12 +186,12 @@ For each transliteration variant in the manifest:
186
186
  → ...
187
187
 
188
188
  5. ENUMERATE wildcard variant queries (per manifest)
189
- → name:"Elevate * game" classes <target> → register_enumerate
190
- → name:"Elevate your *" classes <target> → register_enumerate
191
- → name:"* your game" classes <target> → register_enumerate
189
+ → name:"Raise * play" classes <target> → register_enumerate
190
+ → name:"Raise your *" classes <target> → register_enumerate
191
+ → name:"* your play" classes <target> → register_enumerate
192
192
 
193
193
  6. ENUMERATE each common element's exact-in-class name-list (register_enumerate)
194
- → name:ELEVATE match=exact classes <target> (scope by CLASS + REGION, not a product/goods term)
194
+ → name:RAISE match=exact classes <target> (scope by CLASS + REGION, not a product/goods term)
195
195
 
196
196
  7. ENUMERATE a cross-class merch query if identical match
197
197
  ```
@@ -143,7 +143,7 @@ The order matters. Specifically:
143
143
  | `"PLAYVERSE LEVEL UP GAMING"` | `"Level up together"` | false |
144
144
  | `"It's Your Play"` | `"In Your Play"` | false (apostrophe matters) |
145
145
 
146
- These fixtures should be encoded in test cases for any agent or analyzer implementing the rule. Worked example for the "Dawn: Legends of Lumengarde" matter: "Dawn: Legends of Lumengarde" ≡ "DAWN LEGENDS OF LUMENGARDE" ≡ "dawn legends of lumengarde" — all identical. "Dawn of Lumengarde" or "Dawn: Legends" — NOT identical.
146
+ These fixtures should be encoded in test cases for any agent or analyzer implementing the rule. Worked example for the "Dusk: Tales of Lumengarde" matter: "Dusk: Tales of Lumengarde" ≡ "DUSK TALES OF LUMENGARDE" ≡ "dusk tales of lumengarde" — all identical. "Dusk of Lumengarde" or "Dusk: Tales" — NOT identical.
147
147
 
148
148
  ## Chinese status strings
149
149
 
@@ -150,7 +150,7 @@ band its plan entries call for is missing is refused as `named_band_missing`.
150
150
  ### Per-axis specifics
151
151
 
152
152
  - **`saturation-probe`** — fires when any element is flagged `saturation: high`/`very-high`, plus a partial-phrase structural-prefix probe for slogan/descriptive-compound multi-word marks. Run count-only probes (`limit:1, fields:["uri"]`) capturing `total_hits`, and write **one `incomplete` crowd-descriptor block per probed element** (`fetched:0`, `reason:"crowd descriptor — saturated element <X>, count-only"`). It measures how crowded an element is — it **enumerates nothing and clears nothing**. Skip (not applicable) if all elements are distinctive/low-saturation. **A meaning-translation variant (`translit-*-meaning`) is NOT special-cased into a drop:** an everyday-word-scale count is just a crowd descriptor; whether the field-scoped named slice inside it matters is judgment's call — the funnel surfaces the count, and the **primary-sweep** axis runs the field-scoped named enumeration where the frame says that market matters (see `transliteration-numeric`). Record `translit-too-generic` in the descriptor `reason` as a context note (NOT a drop / re-narrow / swap-to-phonetic signal). An implausibly LOW count on a translation that ought to have peers is the separate `translit-underretrieved` context note — also recorded in the descriptor `reason`, never acted on as a drop. Both are signals the lawyer reads.
153
- - **`primary-sweep`** — always applicable. Run the playbook's named variant queries: the exact mark + each `phrase-substitution`, `visual-substitution`, family-pattern wildcard, and slang variant the manifest lists, **each via `register_enumerate`** (the tool guarantees the page loop). Owns the family-pattern / phrase-substitution / slang / visual-substitution variant queries. **Owns the dead-inclusive named enumeration** (its `register_enumerate` calls carry live AND dead records forward with their status — see step 6; there is no separate "dead-but-identical" or "D4" pass, because the funnel no longer filters dead records). **On a saturated unit, owns the [named-band enumeration of the dangerous category](#exact-in-class-live-floor-primary-sweep-unit-owns-it)** (the exact mark + the **distinctive** formative root / dominant token as the substring-in-scope-class-live named slice, per major+material jurisdiction, plus the phonetic fringe) — enumerated, not sampled. **The named enumeration covers a saturated `translit-*-meaning` meaning token too, where the frame says that market matters (see `transliteration-numeric`)** (scoped to the filed in-scope Nice classes as the `nice_classes` × `regions` filter on the `register_enumerate` call, NOT goods-words ANDed into the search text) — not only the Latin dominant element / formative root. **But a COMMON component that is NOT the distinctive anchor** (a "stripped" common word the manifest marks common/descriptive — DAWN / LEGENDS / GREAT / OUTDOORS) **is count-only, owned by `saturation-probe`, and is NOT enumerated here**: `saturation-probe` already counts it and hands that count up; primary-sweep does **not** run the substring / per-major / phonetic enumeration on it and does **not** author a second crowd block for it (see the linked section — wholesale-enumerating a common component per-jurisdiction is the mega-crowd grind that SIGKILLs the stage).
153
+ - **`primary-sweep`** — always applicable. Run the playbook's named variant queries: the exact mark + each `phrase-substitution`, `visual-substitution`, family-pattern wildcard, and slang variant the manifest lists, **each via `register_enumerate`** (the tool guarantees the page loop). Owns the family-pattern / phrase-substitution / slang / visual-substitution variant queries. **Owns the dead-inclusive named enumeration** (its `register_enumerate` calls carry live AND dead records forward with their status — see step 6; there is no separate "dead-but-identical" or "D4" pass, because the funnel no longer filters dead records). **On a saturated unit, owns the [named-band enumeration of the dangerous category](#exact-in-class-live-floor-primary-sweep-unit-owns-it)** (the exact mark + the **distinctive** formative root / dominant token as the substring-in-scope-class-live named slice, per major+material jurisdiction, plus the phonetic fringe) — enumerated, not sampled. **The named enumeration covers a saturated `translit-*-meaning` meaning token too, where the frame says that market matters (see `transliteration-numeric`)** (scoped to the filed in-scope Nice classes as the `nice_classes` × `regions` filter on the `register_enumerate` call, NOT goods-words ANDed into the search text) — not only the Latin dominant element / formative root. **But a COMMON component that is NOT the distinctive anchor** (a "stripped" common word the manifest marks common/descriptive — DUSK / TALES / GREAT / OUTDOORS) **is count-only, owned by `saturation-probe`, and is NOT enumerated here**: `saturation-probe` already counts it and hands that count up; primary-sweep does **not** run the substring / per-major / phonetic enumeration on it and does **not** author a second crowd block for it (see the linked section — wholesale-enumerating a common component per-jurisdiction is the mega-crowd grind that SIGKILLs the stage).
154
154
  - **`transliteration-numeric`** — fires when the manifest has `translit-*` variants or numeric-substitution variants. Ask one script question per market the frame names. Where the register files foreign marks by their romanised form, and it says so, the Latin question already covers them; do not ask it again in other scripts. A word's meaning in another language is asked only where the frame says that market matters and buyers would read the word as the mark. For each numeric-substitution variant enumerate its query. Owns the foreign-transliteration + numeric-substitution variant queries. Skip if the manifest classifies the mark English-only / single-language. (Multi-script accumulation is no longer a window risk for the unit — `register_enumerate` owns the page loop and a crowd returns `incomplete`, so the unit does not deep-page raw rows itself.)
155
155
  - **`incumbent-class`** — fires when the manifest has an `industry_incumbent_alert`. `register_enumerate` the named band in the matter's in-scope Nice set and markets — **never an all-class incumbent sweep** (that is the unscoped crowd that timed this very axis out live). **Beyond the NAMED watchlist incumbents, the named enumeration is an UNNAMED-OWNER exact-in-class-live enumeration in those classes** — the watchlist seeds prioritise *attention* (judgment's use), they do **not** bound the funnel's enumeration. Every exact-token-in-class-live record crosses the firewall whether or not its owner was pre-listed; the watchlist is provably incomplete, so it never decides what the funnel enumerates. Skip only if there is no incumbent alert AND the frozen plan carries no entries on this axis — the plan is the search authority. **The plan carries no owner lane from the manifest's watchlist.** Read owners from the records this axis and the primary sweep returned. Propose an owner slice only where an owner's other marks could change the assessment, for example when the records show a family of marks sharing the element. A bare-owner count is never written up as a reviewed portfolio or as "portfolio too large, noted". Read an owner's portfolio as far as it answers a question about this owner: can it block, would it, and what kind of filer it is. Never its whole register because it is an incumbent.
156
156
 
@@ -229,12 +229,13 @@ sensitivity, or a commercial partnership cannot lift a finding past what the fra
229
229
  they only change *whether they'd win* or the *character* of the dispute, and the framework then produces
230
230
  the band.
231
231
 
232
- ### Enforcer profiling — mandatory for findings above the lowest band
232
+ ### Enforcer profiling — mandatory for findings rated medium risk or higher
233
233
 
234
234
  Assess: Is this a portfolio brand? TTAB/UDRP history? Institutional vs individual filer? "No large brands
235
235
  or assertive enforcers" is a practical factor the staff lawyer weights heavily. *(`courtlistener__search` covers US
236
236
  federal courts incl. CAFC trademark-appeal opinions — NOT raw TTAB proceedings. For TTAB direct
237
- (oppositions, cancellations, ex parte), source is TTABVUE — separate integration, not yet wired.)*
237
+ (oppositions, cancellations, ex parte), source is TTABVUE — separate integration, not yet wired.)* The Step 3.5 query (`phase2-execution.md`) asks for
238
+ the owner's enforcement record; what it finds is a lead, cited inline, never a verified authority.
238
239
 
239
240
  Profiling informs the **business / practical read only — it never raises the legal read** (the read is the
240
241
  confusion comparison; see *The consumer confusion test*). When the signals cut both ways — a broad