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.
- package/THIRD-PARTY-NOTICES.md +2 -2
- package/build-info.json +2 -2
- package/docs/SECURITY-OWASP.md +21 -13
- package/docs/SECURITY.md +20 -14
- package/docs/architecture/04-configuration-reference.md +3 -2
- package/docs/architecture/05-config-governance.md +1 -1
- package/docs/architecture/06-operations-runbook.md +1 -1
- package/docs/architecture/09-security-and-data.md +16 -11
- package/driver/CHANGELOG.md +18 -0
- package/driver/binding-layers.mjs +28 -6
- package/driver/citation-census.json +3 -3
- package/driver/common-law-coverage-status.mjs +52 -0
- package/driver/common-law-receipts.mjs +1 -1
- package/driver/connotation-search.mjs +5 -5
- package/driver/contract-dictation-registry.mjs +1 -1
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/contract-vocabulary.mjs +18 -18
- package/driver/degraded-parts.mjs +10 -3
- package/driver/dispatch-record.mjs +19 -0
- package/driver/doubt-closure-tool.mjs +2 -2
- package/driver/driver.config.mjs +39 -6
- package/driver/engagement-receipt.mjs +1 -1
- package/driver/engine/CONTRACT.md +41 -1
- package/driver/engine/anthropic-agent.mjs +56 -4
- package/driver/engine/mcp/codex-config.mjs +1 -1
- package/driver/engine/mcp/coverage-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +31 -4
- package/driver/engine/mcp/stdio-server.mjs +45 -7
- package/driver/engine/openai-agent.mjs +100 -17
- package/driver/engine/session-record.mjs +223 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/findings-model.mjs +3 -3
- package/driver/form-neighbourhood.mjs +7 -7
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/framework.mjs +22 -0
- package/driver/gateway.mjs +94 -12
- package/driver/named-band.mjs +2 -0
- package/driver/package.json +1 -1
- package/driver/phase0.mjs +13 -2
- package/driver/pipeline-knockout.mjs +1 -1
- package/driver/pipeline.mjs +46 -33
- package/driver/portal-families.mjs +3 -3
- package/driver/portal-service.mjs +1 -1
- package/driver/predelivery-lint.mjs +19 -19
- package/driver/publish/index.mjs +1 -1
- package/driver/publish/knockout.mjs +5 -4
- package/driver/publish/office-record-links.mjs +17 -2
- package/driver/publish/publish-inputs.mjs +1 -1
- package/driver/publish/render-knockout.mjs +17 -5
- package/driver/publish/render.mjs +4 -2
- package/driver/record-fold.mjs +177 -0
- package/driver/register-count.mjs +18 -1
- package/driver/register-plan.mjs +71 -17
- package/driver/register-records.mjs +7 -0
- package/driver/registry-fidelity.mjs +2 -2
- package/driver/repairs.mjs +1 -1
- package/driver/report-card-record.mjs +4 -4
- package/driver/scope-facts.mjs +11 -3
- package/driver/search-policy.mjs +20 -1
- package/driver/skills/clearance-common-law/SKILL.md +22 -22
- package/driver/skills/clearance-register/digest.md +3 -3
- package/driver/skills/clearance-register/register-recipes.md +11 -11
- package/driver/skills/clearance-register/status-rules.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/firm-wide-reasoning.md +3 -2
- package/driver/skills/clearance-search/phase2-execution.md +6 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-search/worked-examples.md +39 -39
- package/driver/skills/clearance-variants/SKILL.md +16 -16
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/stage-context.mjs +27 -1
- package/driver/stages.mjs +12 -12
- package/driver/suite-census.json +175 -31
- package/driver/synthesis-record.mjs +5 -0
- package/driver/unit-inventory.mjs +17 -28
- package/driver/verify.mjs +6 -6
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/lib/evidence.mjs +8 -8
- package/mcp-server/lib/knockout.mjs +1 -1
- package/mcp-server/package.json +2 -2
- package/package.json +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/_shared/count.mjs +4 -1
- package/providers/_shared/enumerate.mjs +61 -22
- package/providers/_shared/execute-plan.mjs +2 -0
- package/providers/corsearch/src/capabilities.js +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +2 -2
- package/providers/signa/src/capabilities.js +17 -2
- package/providers/signa/src/core.js +234 -37
- package/scripts/band-shape-probe.mjs +2 -2
- package/scripts/deprecate-below.mjs +28 -4
- package/scripts/e2e-first-time.mjs +16 -1
- package/scripts/freeze-example-run.mjs +2 -2
- package/shared/scope.mjs +18 -2
package/driver/register-plan.mjs
CHANGED
|
@@ -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
|
|
1115
|
-
//
|
|
1116
|
-
//
|
|
1117
|
-
//
|
|
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
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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
|
|
1197
|
-
// PA
|
|
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.
|
package/driver/repairs.mjs
CHANGED
|
@@ -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
|
|
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/
|
|
113
|
+
// - Source: [register-euipo · /mark/eu/099999992](https://euipo.europa.eu/…)
|
|
114
114
|
//
|
|
115
|
-
// where the corpus it replaced reads `- Source: [EUIPO ·
|
|
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/
|
|
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/
|
|
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] : "";
|
package/driver/scope-facts.mjs
CHANGED
|
@@ -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
|
-
|
|
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"
|
package/driver/search-policy.mjs
CHANGED
|
@@ -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
|
-
|
|
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 —
|
|
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:
|
|
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
|
-
| "
|
|
159
|
-
| "Thornmantle" | itch.io | https://... | Thornfall Games | not extracted | Direct conflict — identical game-title | Single-player adventure
|
|
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"
|
|
162
|
-
| "Ysolde the Thornmantle" |
|
|
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" |
|
|
169
|
-
| 1,600+ "
|
|
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
|
-
|
|
|
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. `
|
|
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
|
-
|
|
|
344
|
-
|
|
|
345
|
-
|
|
|
346
|
-
|
|
|
347
|
-
|
|
|
348
|
-
|
|
|
349
|
-
|
|
|
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
|
-
|
|
|
370
|
-
|
|
|
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 |
|
|
381
|
-
| 4 | Follow-up |
|
|
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
|
-
-
|
|
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):
|
|
139
|
-
descriptive-compound risk theory retains
|
|
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 "
|
|
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., "
|
|
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:
|
|
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:
|
|
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:"
|
|
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:
|
|
172
|
-
→ name:
|
|
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:"
|
|
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:"
|
|
190
|
-
→ name:"
|
|
191
|
-
→ name:"* your
|
|
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:
|
|
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 "
|
|
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 —
|
|
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
|
|
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
|