clearotron 0.3.3 → 0.4.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +9 -0
- package/INSTALL.md +1 -14
- package/bin/brandowner.mjs +5 -5
- package/bin/connect.mjs +4 -4
- package/bin/onboard.mjs +5 -7
- package/bin/start.mjs +20 -5
- package/bin/update.mjs +6 -1
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +2 -6
- package/driver/CHANGELOG.md +28 -0
- package/driver/ask-ledger.mjs +2 -2
- package/driver/band-shape.mjs +8 -8
- package/driver/blind-frame-model.mjs +1 -1
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/commonlaw-carry.mjs +2 -2
- package/driver/company-bundle.mjs +11 -18
- package/driver/connotation-search.mjs +4 -4
- package/driver/contract-e3-backlog.mjs +3 -3
- package/driver/declination-call.mjs +1 -1
- package/driver/declination-tool.mjs +1 -1
- package/driver/dev-portal.mjs +1 -1
- package/driver/door-call-verdict.mjs +27 -0
- package/driver/driver.config.mjs +17 -11
- package/driver/e2e/README.md +1 -1
- package/driver/engine/mcp/clarivate-server.mjs +2 -1
- package/driver/engine/mcp/corsearch-server.mjs +1 -0
- package/driver/engine/mcp/euipo-server.mjs +1 -0
- package/driver/engine/mcp/free-tier-server.mjs +1 -1
- package/driver/engine/mcp/gather-config.mjs +1 -1
- package/driver/engine/mcp/perplexity-server.mjs +1 -1
- package/driver/engine/mcp/recording-server.mjs +1 -1
- package/driver/engine/mcp/signa-server.mjs +1 -0
- package/driver/engine/mcp/supplemental.mjs +1 -1
- package/driver/engine/mcp/uspto-local-server.mjs +1 -0
- package/driver/enqueue-schema.mjs +2 -2
- package/driver/feedback-issues.mjs +1 -1
- package/driver/feedback-store.mjs +1 -1
- package/driver/findings-model.mjs +3 -3
- package/driver/flag-snapshot.mjs +1 -1
- package/driver/floor-duty.mjs +2 -2
- package/driver/form-neighbourhood.mjs +47 -15
- package/driver/frame-diff-model.mjs +3 -3
- package/driver/gateway.mjs +5 -5
- package/driver/jx-lanes.mjs +1 -1
- package/driver/known-conflicts.mjs +18 -0
- package/driver/log.mjs +2 -2
- package/driver/package.json +1 -1
- package/driver/pipeline-knockout.mjs +129 -95
- package/driver/pipeline.mjs +69 -32
- package/driver/placement-carry.mjs +2 -2
- package/driver/placement-form.mjs +1 -1
- package/driver/portal-mcp-client.mjs +1 -1
- package/driver/portal-request-origin.mjs +79 -0
- package/driver/portal-service.mjs +45 -17
- package/driver/predelivery-lint.mjs +10 -10
- package/driver/profile-page.html +9 -13
- package/driver/profile-service.mjs +25 -13
- package/driver/profiles.mjs +17 -4
- package/driver/progress.mjs +1 -1
- package/driver/provider-usage.mjs +24 -1
- package/driver/publish/index.mjs +17 -5
- package/driver/publish/knockout.mjs +3 -2
- package/driver/publish/render-knockout.mjs +1 -1
- package/driver/publish/render.mjs +14 -3
- package/driver/publish/search-depth.mjs +4 -2
- package/driver/recall-reconciliation.mjs +1 -1
- package/driver/record-carry.mjs +6 -6
- package/driver/recording-agreement.mjs +2 -2
- package/driver/reference-score.mjs +27 -27
- package/driver/register-count.mjs +56 -1
- package/driver/register-digest-record.mjs +1 -1
- package/driver/register-plan.mjs +5 -5
- package/driver/register-records.mjs +10 -1
- package/driver/registry-fidelity.mjs +4 -4
- package/driver/repair-composers.mjs +6 -6
- package/driver/run-economics.mjs +8 -19
- package/driver/screen-gate.mjs +1 -1
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/SKILL.md +2 -2
- package/driver/skills/clearance-common-law/perplexity-prompts.md +4 -4
- package/driver/skills/clearance-register/digest.md +1 -1
- package/driver/skills/clearance-register/unit.md +1 -1
- package/driver/skills/clearance-search/report-prose.md +5 -5
- package/driver/skills/clearance-search/synthesis-rules.md +4 -4
- package/driver/skills/clearance-variants/SKILL.md +2 -2
- package/driver/skills/clearance-variants/transliteration-scripts.md +1 -1
- package/driver/skills/frame-diff/SKILL.md +2 -2
- package/driver/skills/knockout-assess/SKILL.md +9 -9
- package/driver/skills/matter-frame/watchlist-reference.md +1 -1
- package/driver/skills/narrative-refutation/SKILL.md +2 -2
- package/driver/skills/placement-inquiry/SKILL.md +1 -1
- package/driver/stage-context.mjs +4 -4
- package/driver/stages.mjs +23 -15
- package/driver/suite-census.json +138 -42
- package/driver/systemd/clearotron-client-mcp.service +24 -0
- package/driver/systemd/clearotron-mcp-face.service +24 -0
- package/driver/systemd/clearotron-portal.service +24 -0
- package/driver/systemd/clearotron-worker.service +24 -0
- package/driver/tokens.mjs +26 -17
- package/driver/turnaround-bands.mjs +1 -1
- package/driver/unit-inventory.mjs +3 -3
- package/driver/variant-manifest-model.mjs +1 -1
- package/driver/verify.mjs +2 -2
- package/driver/whatif-memo-run.mjs +1 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/audit.mjs +9 -2
- package/mcp-server/lib/http-handler.mjs +7 -3
- package/mcp-server/mint-token.mjs +8 -6
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +11 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-GBbbyQxc.js → index-D_O_55vK.js} +59 -9
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/_shared/README.md +1 -1
- package/providers/_shared/answer-memory.mjs +199 -0
- package/providers/_shared/ledger-path.mjs +1 -1
- package/providers/_shared/ledger.mjs +47 -5
- package/providers/_shared/script-form.mjs +24 -5
- package/providers/_shared/term-shape.mjs +5 -5
- package/providers/clarivate/src/capabilities.js +11 -0
- package/providers/clarivate/src/core.js +140 -13
- package/providers/jx-subclass/lookup.mjs +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/signa/src/capabilities.js +24 -0
- package/providers/signa/src/core.js +66 -0
- package/scripts/deprecate-below.mjs +114 -2
- package/scripts/freeze-example-run.mjs +1 -1
- package/scripts/live-surface-check.mjs +11 -2
- package/scripts/release-entry-catch-up.mjs +211 -0
- package/scripts/release-note-required.mjs +102 -6
- package/scripts/release-rehearsal-version.mjs +60 -0
- package/scripts/release-sbom.mjs +104 -0
- package/scripts/release-visible-check.mjs +7 -5
- package/scripts/score.mjs +3 -3
- package/shared/brand.mjs +1 -1
- package/shared/client-door.mjs +15 -8
- package/shared/driver-dir.mjs +20 -9
- package/shared/names-in-force.mjs +2 -0
- package/shared/scope.mjs +25 -10
- package/shared/store-in-repo.mjs +38 -17
package/driver/driver.config.mjs
CHANGED
|
@@ -668,13 +668,12 @@ export const config = {
|
|
|
668
668
|
// Owner's ruling, 2026-09-23: record the tier. What a run asked for is a tier, what it was served is
|
|
669
669
|
// recorded separately and already is, and the report names the model that ran — none of that moves.
|
|
670
670
|
//
|
|
671
|
-
// WHAT IT COSTS, RULED ON AND ACCEPTED RATHER THAN DISCOVERED LATER.
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
677
|
-
// fix it properly, which is larger than this and not what was ruled.
|
|
671
|
+
// WHAT IT COSTS, RULED ON AND ACCEPTED RATHER THAN DISCOVERED LATER. A stage's rows are keyed on the
|
|
672
|
+
// tier it asked for, and a native-language lane's rows on the model that served them (`modelKey` in
|
|
673
|
+
// tokens.mjs). So one model reached both ways lands in two buckets: `anthropic/claude-haiku` from a
|
|
674
|
+
// stage, `anthropic/claude-haiku-4-5` from a native-language lane. That is a real split in a per-model
|
|
675
|
+
// total and it was accepted with the ruling; keying every row on what served it is the change that
|
|
676
|
+
// would close it, which is larger than this and not what was ruled.
|
|
678
677
|
export const MODELS = {
|
|
679
678
|
haiku: "anthropic/claude-haiku",
|
|
680
679
|
sonnet: "anthropic/claude-sonnet",
|
|
@@ -693,8 +692,7 @@ export const MODELS = {
|
|
|
693
692
|
// alias → full id; a value that's already a full provider/model id (contains "/") passes through.
|
|
694
693
|
//
|
|
695
694
|
// A BARE Anthropic id (dated or not — "claude-haiku-4-5-20251001", "claude-opus-5") normalises to the
|
|
696
|
-
// catalog form too.
|
|
697
|
-
// name their model that way because that is what the Messages API takes, so without this one model named
|
|
695
|
+
// catalog form too. Without this, one model named
|
|
698
696
|
// in two spellings — dated and undated — would key apart in a rollup. The date suffix is dropped;
|
|
699
697
|
// anything that does not look like a bare claude id is returned untouched, so a genuinely unknown model
|
|
700
698
|
// still keys as-is rather than being guessed at.
|
|
@@ -1320,7 +1318,15 @@ export const PROVIDERS = {
|
|
|
1320
1318
|
// The office's own numbers come off the full record Signa returns on search, through the
|
|
1321
1319
|
// provider's own normaliser, so publish can address the office's page for each filing
|
|
1322
1320
|
// (publish/office-record-links.mjs) instead of showing the handle.
|
|
1323
|
-
|
|
1321
|
+
//
|
|
1322
|
+
// THE TOTAL RIDES THE SAME ANSWER and is carried, not dropped: `include_total` puts the register's
|
|
1323
|
+
// own count for this exact question on every search, so the listing of a term already holds the
|
|
1324
|
+
// figure the count lane would ask for separately. An approximation stays one: no number in
|
|
1325
|
+
// `total`, the floor beside it — the count lane's rule, unchanged.
|
|
1326
|
+
return { ok: true,
|
|
1327
|
+
total: Number.isFinite(p.total_hits) ? p.total_hits : null,
|
|
1328
|
+
...(p.total_approximate === true ? { approximate: true, floor: Number.isFinite(p.total_floor) ? p.total_floor : null } : {}),
|
|
1329
|
+
records: (Array.isArray(p.results) ? p.results : []).map((row) => {
|
|
1324
1330
|
const rec = row?.raw && typeof row.raw === "object" ? core.normalizeRecord(row.raw, row.office || null) : null;
|
|
1325
1331
|
return {
|
|
1326
1332
|
record_id: row?.record_id ?? null,
|
|
@@ -2362,7 +2368,7 @@ export function preflightEngineBinary(env = process.env, { platform = process.pl
|
|
|
2362
2368
|
|
|
2363
2369
|
if (r.relative) {
|
|
2364
2370
|
throw new Error(`[preflight] ${where} is the RELATIVE path "${bin}", which cannot work: the engine is `
|
|
2365
|
-
+ "spawned with the RUN DIRECTORY as its cwd
|
|
2371
|
+
+ "spawned with the RUN DIRECTORY as its cwd, not the repo, so a relative command is looked "
|
|
2366
2372
|
+ `for inside the run. Give an absolute path — e.g. ${join(REPO_ROOT, bin)} — or a bare name on PATH.`);
|
|
2367
2373
|
}
|
|
2368
2374
|
|
package/driver/e2e/README.md
CHANGED
|
@@ -37,7 +37,7 @@ other, which is the only proof available where no gold set exists.
|
|
|
37
37
|
|
|
38
38
|
A knockout scenario graded against a similar-marks sheet scores a **structural zero**. Knockout answers
|
|
39
39
|
one narrow question — how many register filings exist for the exact mark and its close variations in the
|
|
40
|
-
named classes — and never retrieves similar marks, so a lawyer's list of
|
|
40
|
+
named classes — and never retrieves similar marks, so a lawyer's list of WAVO PUNCH and WAVO TROPICS is
|
|
41
41
|
unreachable by construction. R3 and R4 scored 0/8 and 0/9 on both free-tier and clarivate on the same
|
|
42
42
|
day with the same engine, which measured the pairing rather than the engine.
|
|
43
43
|
|
|
@@ -121,6 +121,7 @@ serve({
|
|
|
121
121
|
plan_path: { type: "string", description: "Absolute path to the frozen _driver/register-plan.json" },
|
|
122
122
|
axis: { type: "string", description: "The register axis to execute (e.g. primary-sweep)" },
|
|
123
123
|
output_path: { type: "string", description: "Absolute path of the named band to write (register-units/<axis>-band.json)" },
|
|
124
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
124
125
|
} },
|
|
125
126
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
126
127
|
handler: guard((a) => doExecutePlan(API_KEY, BASE, a, tctx("execute_plan"))),
|
|
@@ -134,7 +135,7 @@ serve({
|
|
|
134
135
|
proposals: { type: "array", minItems: 1, items: { type: "object", properties: {
|
|
135
136
|
predicate: { type: "string", enum: ["exact", "default", "wildcard", "phonetic", "owner"] },
|
|
136
137
|
term: { type: "string" }, terms: { type: "array", items: { type: "string" } },
|
|
137
|
-
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term — plain ASCII letters/digits, syllable-separated by single spaces, no tone marks or diacritics (华威豹 → \"HUA WEI BAO\",
|
|
138
|
+
romanization: { type: "string", description: "The Latin-script form of a NON-LATIN term — plain ASCII letters/digits, syllable-separated by single spaces, no tone marks or diacritics (华威豹 → \"HUA WEI BAO\", ワボスラッシュ → \"WABO SURASSHU\"). MANDATORY beside a non-Latin term: without it this register cannot answer the characters and the slice defers. Single-term proposals only (never an OR-stack, never predicate:owner), and never on a term that is already Latin." },
|
|
138
139
|
owner: { type: "string", description: "OPTIONAL owner scope field on a MARK-TEXT proposal: the query is the owner×term intersection (the owner's filings within the term band). Not allowed on predicate:owner (there the owner name IS the term)." },
|
|
139
140
|
nice_classes: { type: "array", items: {} },
|
|
140
141
|
// The narrowing fields every register serves (proposal-fields.mjs); this register's two facts ride in.
|
|
@@ -104,6 +104,7 @@ serve({
|
|
|
104
104
|
plan_path: { type: "string", description: "Absolute path to the frozen _driver/register-plan.json" },
|
|
105
105
|
axis: { type: "string", description: "The register axis to execute (e.g. primary-sweep)" },
|
|
106
106
|
output_path: { type: "string", description: "Absolute path of the named band to write (register-units/<axis>-band.json)" },
|
|
107
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
107
108
|
} },
|
|
108
109
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
109
110
|
handler: guard((a) => doExecutePlan(COOKIE, a, tctx("execute_plan"))),
|
|
@@ -139,6 +139,7 @@ serve({
|
|
|
139
139
|
plan_path: { type: "string", description: "Absolute path to the frozen _driver/register-plan.json" },
|
|
140
140
|
axis: { type: "string", description: "The register axis to execute (e.g. primary-sweep)" },
|
|
141
141
|
output_path: { type: "string", description: "Absolute path of the named band to write (register-units/<axis>-band.json)" },
|
|
142
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
142
143
|
} },
|
|
143
144
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
144
145
|
handler: guard((a) => doExecutePlan(AUTH, a, tctx("execute_plan"))),
|
|
@@ -151,7 +151,7 @@ serve({
|
|
|
151
151
|
description: `Execute the frozen register plan for ONE axis across ${CAPABILITY_OFFICES} and write the named band. One qid per plan entry, spanning both offices — the execution-receipt shape is identical to a single-source provider's.`,
|
|
152
152
|
inputSchema: { type: "object", required: ["plan_path", "axis", "output_path"], properties: {
|
|
153
153
|
plan_path: { type: "string" }, axis: { type: "string" }, output_path: { type: "string" },
|
|
154
|
-
qids: { type: "array", items: { type: "string" } },
|
|
154
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
155
155
|
} },
|
|
156
156
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
157
157
|
handler: guard((a) => doExecutePlan(AUTH, a, tctx("execute_plan"))),
|
|
@@ -903,7 +903,7 @@ export function buildGatherMcpConfig(groups = [], { sessionKey, agent, runDir, r
|
|
|
903
903
|
localEntry("register");
|
|
904
904
|
if (!runDir) {
|
|
905
905
|
throw new Error("gather-config: a register server needs the run it is fetching for — record bodies "
|
|
906
|
-
+ "belong to their run
|
|
906
|
+
+ "belong to their run, and the box-global ledger is retired. Pass runDir.");
|
|
907
907
|
}
|
|
908
908
|
}
|
|
909
909
|
const env = serverEnv({ sessionKey, agent, runDir, recordAxis });
|
|
@@ -122,7 +122,7 @@ function tellObligations(spec, ledgerJson) {
|
|
|
122
122
|
writeFileSync(
|
|
123
123
|
obligationsSidecarPath(spec.output_path),
|
|
124
124
|
JSON.stringify({
|
|
125
|
-
_provenance: `what the seat was TOLD it owed, computed by connotationObligations() from ${basename(spec.output_path)} at the moment the grid tool returned — the same calculation the validator judges with
|
|
125
|
+
_provenance: `what the seat was TOLD it owed, computed by connotationObligations() from ${basename(spec.output_path)} at the moment the grid tool returned — the same calculation the validator judges with`,
|
|
126
126
|
floor: ob.floor,
|
|
127
127
|
queriesOwed: ob.queries.map((e) => e.query),
|
|
128
128
|
rowsTold,
|
|
@@ -700,7 +700,7 @@ serve({
|
|
|
700
700
|
type: "object", required: ["text"],
|
|
701
701
|
properties: {
|
|
702
702
|
text: { type: "string", description: "The result, in one line — what was checked and how it came back." },
|
|
703
|
-
source_link: { type: "string", description: "An http(s) URL for the page it came from. OMIT it when the source is a SEARCH rather than a page — a citation label in a link destination ships a link to nowhere
|
|
703
|
+
source_link: { type: "string", description: "An http(s) URL for the page it came from. OMIT it when the source is a SEARCH rather than a page — a citation label in a link destination ships a link to nowhere." },
|
|
704
704
|
internal: { type: "boolean", description: "true marks this an INTERNAL reviewer note; the driver renders the ::p:: marker and the client export strips it." },
|
|
705
705
|
},
|
|
706
706
|
},
|
|
@@ -86,6 +86,7 @@ serve({
|
|
|
86
86
|
plan_path: { type: "string", description: "Absolute path to the frozen _driver/register-plan.json" },
|
|
87
87
|
axis: { type: "string", description: "The register axis to execute (e.g. primary-sweep)" },
|
|
88
88
|
output_path: { type: "string", description: "Absolute path of the named band to write (register-units/<axis>-band.json)" },
|
|
89
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
89
90
|
} },
|
|
90
91
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
91
92
|
handler: guard((a) => doExecutePlan({ apiKey: API_KEY, base: BASE }, a, tctx("execute_plan"))),
|
|
@@ -112,7 +112,7 @@ export function mintSupplementalEntries(axis, proposals, { existingQids = new Se
|
|
|
112
112
|
// rather than guessed about.
|
|
113
113
|
const owner = typeof p.owner === "string" ? p.owner.trim() : "";
|
|
114
114
|
if (p.owner != null && !owner) { issue("owner, when present, must be a non-empty string"); continue; }
|
|
115
|
-
if (owner && predicate === "owner") { issue(`predicate "owner" carries the owner name as its term — the owner scope field rides a MARK-TEXT predicate ({predicate:"default", term:"
|
|
115
|
+
if (owner && predicate === "owner") { issue(`predicate "owner" carries the owner name as its term — the owner scope field rides a MARK-TEXT predicate ({predicate:"default", term:"WAVO", owner:"…"})`); continue; }
|
|
116
116
|
// ── romanization (2026-07-30 review round — the enforcement-without-invitation fix) ────────────
|
|
117
117
|
// Half the live incident's non-Latin deferrals were supp: entries, because the guard's remediation
|
|
118
118
|
// ("supply the romanisation on the entry's romanizedTerms") pointed at a field this proposing
|
|
@@ -124,6 +124,7 @@ serve({
|
|
|
124
124
|
plan_path: { type: "string", description: "Absolute path to the frozen _driver/register-plan.json" },
|
|
125
125
|
axis: { type: "string", description: "The register axis to execute (e.g. primary-sweep)" },
|
|
126
126
|
output_path: { type: "string", description: "Absolute path of the named band to write (register-units/<axis>-band.json)" },
|
|
127
|
+
qids: { type: "array", items: { type: "string" }, description: "Optional: the qids of the plan entries to run. Only those run, and every other block in the band stays as it is. Leave it out only on the axis's first run: without it every entry on the axis runs again." },
|
|
127
128
|
} },
|
|
128
129
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
129
130
|
handler: guard((a) => doExecutePlan(AUTH, a, tctx("execute_plan"))),
|
|
@@ -242,14 +242,14 @@ function niceClassErrors(list, label) {
|
|
|
242
242
|
// A zero-width joiner makes "IRON\u200DWHISK" render as IRONWHISK and compare unequal to it, so
|
|
243
243
|
// `selfExclusionOwners` and dedup both miss a mark a human reads as the account's own.
|
|
244
244
|
//
|
|
245
|
-
// NFD "
|
|
245
|
+
// NFD "FALCÓN" !== NFC "FALCÓN", so one mark submitted two ways is two matters, two searches, two bills.
|
|
246
246
|
//
|
|
247
247
|
// REFUSED, NOT STRIPPED, for the display-control classes. Silently altering the mark a client asked
|
|
248
248
|
// about is its own defect and a worse one: the deliverable would then name a mark nobody ordered. NFC is
|
|
249
249
|
// the exception because it changes no character a reader can see — it is the same string, spelled once.
|
|
250
250
|
//
|
|
251
251
|
// WHAT THIS DELIBERATELY DOES NOT TOUCH, because an over-broad filter here is worse than the defect:
|
|
252
|
-
// • ACCENTED AND NON-LATIN LETTERS.
|
|
252
|
+
// • ACCENTED AND NON-LATIN LETTERS. FALCÓN, СOLA and an Arabic or Hebrew mark are all legitimate things
|
|
253
253
|
// to clear. The refusal is for explicit direction-OVERRIDE controls, never for letters that happen to
|
|
254
254
|
// be right-to-left — an Arabic mark contains no U+202x at all.
|
|
255
255
|
// • HOMOGLYPHS. A Cyrillic ES in СOLA is measured in and explicitly NOT claimed as a defect;
|
|
@@ -26,7 +26,7 @@ export const BASE_LABELS = ["source:report-feedback", "area:report"];
|
|
|
26
26
|
* describe, which is triage — the thing this issue forbids. A human adds the real area during triage.
|
|
27
27
|
*/
|
|
28
28
|
export const REQUIRED_LABELS = [
|
|
29
|
-
{ name: "source:report-feedback", color: "c5def5", description: "Raised by a reviewing lawyer on a delivered report
|
|
29
|
+
{ name: "source:report-feedback", color: "c5def5", description: "Raised by a reviewing lawyer on a delivered report." },
|
|
30
30
|
{ name: "feedback:good", color: "0e8a16", description: "A lawyer marked this finding RIGHT. Not a defect — a confirmation worth keeping." },
|
|
31
31
|
{ name: "feedback:bad", color: "b60205", description: "A lawyer marked this finding WRONG. Input for triage, not yet buildable work." },
|
|
32
32
|
{ name: "area:report", color: "d4c5f9", description: "The delivered report and what it says." },
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// disposition — read SERVER-SIDE from the run's own artifacts, never taken from the request. Mark and band
|
|
19
19
|
// come from report-data.json; the disposition comes from findings.json beside it in the same run dir,
|
|
20
20
|
// because report-data.json is the CLIENT cut and stopped serving the engine's placement key. A
|
|
21
|
-
// reader who finds the ordinal now points at `
|
|
21
|
+
// reader who finds the ordinal now points at `KOLEMA / Manageable / rebuttable` when the flag says
|
|
22
22
|
// `BURROWELL / Medium / conceded` knows the run was republished, which is exactly the thing a bare id
|
|
23
23
|
// would have hidden.
|
|
24
24
|
|
|
@@ -139,7 +139,7 @@ export const POSITION_REQUIRED_DISPOSITIONS = ["adversarial", "coexistence-partn
|
|
|
139
139
|
export const OFF_FIELD_GROUNDS = ["different-field", "no-material-risk"];
|
|
140
140
|
// Context notes (A1) — the valid HOME for a knowledge-cited reference that is NOT a fetched-record finding:
|
|
141
141
|
// a famous one-keystroke neighbour kept for diligence per digest.md's "never dropped" rule but with NO
|
|
142
|
-
// register record to ground a finding (e.g.
|
|
142
|
+
// register record to ground a finding (e.g. NOVAPULSO on a NOVAPULSE clearance). A context note is NEVER a finding
|
|
143
143
|
// (no composite/level/meters/registration), never drives the rating, and carries no uri — so it cannot
|
|
144
144
|
// launder an ungrounded mark into the findings contract: the F-14 URI guard (validateOwner) stays whole, and
|
|
145
145
|
// "the famous neighbour is never dropped" is honoured HERE rather than via a broken empty-uri finding.
|
|
@@ -246,7 +246,7 @@ export function deriveDisplayVerdict({ verdict, reasons, kinds, findings, manife
|
|
|
246
246
|
}
|
|
247
247
|
|
|
248
248
|
// ── wp50 — THE deterministic client-summary-block → finding join ────────────────────────────────────────
|
|
249
|
-
// The old first-match substring-containment join mis-bound "DEMVENZY —
|
|
249
|
+
// The old first-match substring-containment join mis-bound "DEMVENZY — Norvanta" to the VENZY finding
|
|
250
250
|
// (the head CONTAINS "venzy"), so the validator and the lint auto-correct VALIDATED — and would have
|
|
251
251
|
// ENFORCED — the wrong finding's tier, rating two Composite-3 conflicts at the top band. One join,
|
|
252
252
|
// used by the validator, the auto-correct and the email
|
|
@@ -728,7 +728,7 @@ const CORRECTION_ENTRY_KEYS = ["entity", "disposition", "note"];
|
|
|
728
728
|
// blob. The frozen renderer receives a deterministic STRING PROJECTION (projectAssessmentField); the
|
|
729
729
|
// structured rows render in the audit workbook + report-data, both unfrozen.
|
|
730
730
|
// spec 2026-07-30 §3: `read` — the one-to-two-sentence consequence the reader sees ("A weak name to
|
|
731
|
-
// own. SLUSH is simply what the product is, so the whole mark rests on
|
|
731
|
+
// own. SLUSH is simply what the product is, so the whole mark rests on WAVO."). The renderer shows it
|
|
732
732
|
// as the visible prose and collapses the typed rows behind toggles; absent (archived structured runs)
|
|
733
733
|
// the renderer falls back to spectrum/acquired/note.
|
|
734
734
|
const ASSESSMENT_FIELD_KEYS = ["spectrum", "read", "per_class", "per_market", "counter_registrations", "acquired", "note"];
|
package/driver/flag-snapshot.mjs
CHANGED
|
@@ -238,7 +238,7 @@ export function buildFlagSnapshot(env, { capturedAt, registerProvider = null, re
|
|
|
238
238
|
* reports a delta that does not exist, which is the same class of lie as reporting none.
|
|
239
239
|
*/
|
|
240
240
|
export const PRODUCTION_POSTURE = {
|
|
241
|
-
source: "production unit environment, read 2026-08-04 with owner authorisation
|
|
241
|
+
source: "production unit environment, read 2026-08-04 with owner authorisation; the flag half "
|
|
242
242
|
+ "retired 2026-08-20 when #1149 item 8 deleted every switch it named",
|
|
243
243
|
// The three it named are gone — deleted BECAUSE production ran them on while the source shipped them
|
|
244
244
|
// off, which is the delta this block existed to expose. There is nothing left to differ about, and
|
package/driver/floor-duty.mjs
CHANGED
|
@@ -64,8 +64,8 @@ export function armFloorDuty(runDir, { now = () => new Date().toISOString() } =
|
|
|
64
64
|
try {
|
|
65
65
|
ensureDriverDir(String(runDir ?? ""));
|
|
66
66
|
writeFileSync(driverDir(String(runDir ?? ""), FLOOR_DUTY_STAMP), JSON.stringify({
|
|
67
|
-
_provenance: "the placement pass on this run ran under code that orders the floors by record id
|
|
68
|
-
+ "
|
|
67
|
+
_provenance: "the placement pass on this run ran under code that orders the floors by record id"
|
|
68
|
+
+ ", so its floor account may be held to that order",
|
|
69
69
|
ts: now(),
|
|
70
70
|
}, null, 2) + "\n");
|
|
71
71
|
return true;
|
|
@@ -33,7 +33,7 @@ import { dominantElementFromManifest, formativeRootFromManifest } from "./scope-
|
|
|
33
33
|
// mark-restatement check already share (providers/_shared/script-form.mjs — itself PURE, no node
|
|
34
34
|
// imports, so this module stays offline-testable). The floor/model merge below keys on it for the
|
|
35
35
|
// same reason those two do: a private second fold is how two definitions of "the same term" drift.
|
|
36
|
-
import { formKey } from "../providers/_shared/script-form.mjs";
|
|
36
|
+
import { formKey, mixesLatinWithGreekOrCyrillic } from "../providers/_shared/script-form.mjs";
|
|
37
37
|
|
|
38
38
|
const LOWER = "abcdefghijklmnopqrstuvwxyz";
|
|
39
39
|
const AZ = LOWER.split("");
|
|
@@ -87,11 +87,11 @@ export function editNeighbourhood(element) {
|
|
|
87
87
|
// ── A one-letter neighbour that is an ordinary word with a DIFFERENT SOUND is not searched ────────────
|
|
88
88
|
//
|
|
89
89
|
// THE REVIEWING LAWYER'S TEST IS CONFUSING SIMILARITY, and edit-1 over a short ordinary word is mostly other
|
|
90
|
-
// ordinary words:
|
|
90
|
+
// ordinary words: BANE, CAGE, CAKE, CAME, LANE beside CANE, each a common mark with its own crowd. Measured
|
|
91
91
|
// on a delivered four-letter run, 2026-09-18: 1,037 of 2,098 records (49%) were reached only by the one-letter
|
|
92
92
|
// lists; on the dense production matter of 2026-09-16, 1,154 of 2,146 (54%). Conceptually different words are
|
|
93
93
|
// not confused by consumers, so fetching them is cost and not coverage. A respelling that sounds the same —
|
|
94
|
-
//
|
|
94
|
+
// KANE for CANE — is exactly what a search must find, and so is every neighbour of a made-up word, because two
|
|
95
95
|
// unknown words that sound alike have nothing conceptual to keep them apart (MALENA beside VALENA).
|
|
96
96
|
//
|
|
97
97
|
// So a neighbour is dropped only when BOTH hold: it is in the ordinary-word list, AND no Double-Metaphone key
|
|
@@ -101,8 +101,8 @@ export function editNeighbourhood(element) {
|
|
|
101
101
|
// missing from it is searched: over-search is the safe direction.
|
|
102
102
|
//
|
|
103
103
|
// DOUBLE METAPHONE KEEPS VOWELS ONLY AT THE START, so a vowel change inside the word does not change the key:
|
|
104
|
-
//
|
|
105
|
-
// search, and it is recorded here so nobody reads those
|
|
104
|
+
// CONE and CANOE both key KN, as CANE does, and are KEPT. That is the rule as specified erring towards the
|
|
105
|
+
// search, and it is recorded here so nobody reads those two as a defect of the list.
|
|
106
106
|
//
|
|
107
107
|
// `ordinaryWords` is a Set of lowercase words, or null. Null drops nothing — the behaviour before this rule —
|
|
108
108
|
// so a caller that does not load the list (every test that predates it) is unchanged. PURE.
|
|
@@ -259,7 +259,13 @@ export function transliterations(element, { scripts = SUPPORTED_SCRIPTS } = {})
|
|
|
259
259
|
// droppedVariantFamilies). The floor stays exhaustive WITHIN the families judgment kept — this is the funnel
|
|
260
260
|
// honouring a scope decision that was already written and, until 2026-07-18, ignored. edit-1 is never
|
|
261
261
|
// droppable: it is the doctrine floor (radiusFor), not a family.
|
|
262
|
-
|
|
262
|
+
//
|
|
263
|
+
// `mixedScriptQuery` is the active register's declaration of the same name (capabilities.js). `false`
|
|
264
|
+
// means the register cannot search a term mixing Latin with Greek or Cyrillic letters as written, so the
|
|
265
|
+
// whole-word look-alike swaps that leave some letters Latin are not searched there: they leave the band
|
|
266
|
+
// and are listed in `mixedScriptNotSearched` instead. Anything else — the value of every register that
|
|
267
|
+
// does not declare it — changes nothing, and the band carries no new field.
|
|
268
|
+
export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], ordinaryWords = null, mixedScriptQuery = null } = {}) {
|
|
263
269
|
const radius = radiusFor(element);
|
|
264
270
|
const el = radius.element;
|
|
265
271
|
if (!el) return { element: "", radius, exactQueries: [], wildcardPatterns: [], phoneticKeys: [], confusables: [], transliterations: [], ordinaryWordDifferentSound: [], ledger: { disclosed: radius.note, axes: [] } };
|
|
@@ -272,7 +278,11 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
|
|
|
272
278
|
const skip = new Set(notSearched);
|
|
273
279
|
const edits = generatedEdits.filter((t) => !skip.has(t));
|
|
274
280
|
const confs = drop.has("visual-confusable") ? [] : visualConfusables(el);
|
|
275
|
-
const
|
|
281
|
+
const generatedTrans = drop.has("transliteration") ? [] : transliterations(el, { scripts });
|
|
282
|
+
// Only the transliteration generator can write a mixed-alphabet term: edit-1 works over [a-z] and the
|
|
283
|
+
// confusable table maps Latin to Latin, digits and `@`.
|
|
284
|
+
const mixedNotSearched = mixedScriptQuery === false ? generatedTrans.filter(mixesLatinWithGreekOrCyrillic) : [];
|
|
285
|
+
const trans = generatedTrans.filter((t) => !mixedNotSearched.includes(t));
|
|
276
286
|
const wildcards = drop.has("phonetic-family") ? [] : skeletonPatterns(el);
|
|
277
287
|
const keys = drop.has("phonetic-family") ? [] : doubleMetaphone(el).filter(Boolean);
|
|
278
288
|
|
|
@@ -297,6 +307,7 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
|
|
|
297
307
|
confusables: confs,
|
|
298
308
|
transliterations: trans,
|
|
299
309
|
ordinaryWordDifferentSound: notSearched,
|
|
310
|
+
...(mixedScriptQuery === false ? { mixedScriptNotSearched: mixedNotSearched } : {}),
|
|
300
311
|
ledger: {
|
|
301
312
|
disclosed: radius.note,
|
|
302
313
|
dropped_axes: [...drop].sort(),
|
|
@@ -324,7 +335,9 @@ export function formNeighbourhood(element, { markets = [], scripts = SUPPORTED_S
|
|
|
324
335
|
: keys.length ? `NO PATTERN — "${el}" has too few consonants to anchor a skeleton wildcard, so the family has no retrieval pattern; Double-Metaphone key(s) [${keys.join(",")}] still verify what the other axes return`
|
|
325
336
|
: `NO PATTERN — "${el}" has too few consonants to anchor a skeleton wildcard and yields no Double-Metaphone key, so this axis contributes nothing for this element and verifies nothing` },
|
|
326
337
|
{ axis: "visual-confusable", count: confs.length, mechanism: drop.has("visual-confusable") ? "DROPPED — judgment's variant-layer scope decision" : "Unicode-confusable homoglyph + multigraph table" },
|
|
327
|
-
{ axis: "transliteration", count: trans.length,
|
|
338
|
+
{ axis: "transliteration", count: trans.length,
|
|
339
|
+
...(mixedScriptQuery === false ? { generated: generatedTrans.length, not_searched: mixedNotSearched.length } : {}),
|
|
340
|
+
mechanism: drop.has("transliteration") ? "DROPPED — judgment's variant-layer scope decision" : `scoped scripts: ${scripts.join(", ")}` },
|
|
328
341
|
],
|
|
329
342
|
total_exact: exactQueries.length,
|
|
330
343
|
},
|
|
@@ -388,7 +401,7 @@ export function coverageGaps(band, { dispatched = [], explained = [] } = {}) {
|
|
|
388
401
|
// exhaustive edit-1 neighbourhood: 1,736 junk exact queries on AquaPlus 2026-07-17, 4,524 on the 07-16 run.
|
|
389
402
|
// Measured 2026-07-18: 10 of 20 recent runs carried one of these. The JSON field is a validated scalar and
|
|
390
403
|
// cannot swallow a sentence.
|
|
391
|
-
export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], model = null, mark = "", ordinaryWords = null } = {}) {
|
|
404
|
+
export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts = SUPPORTED_SCRIPTS, droppedAxes = [], model = null, mark = "", ordinaryWords = null, mixedScriptQuery = null, nativeScriptIndex = null } = {}) {
|
|
392
405
|
const { seeds, seededFrom, rejected } = floorSeeds(manifestMd, { model, mark });
|
|
393
406
|
// The reason travels WITH the throw. It used to say only "the job states no mark", which is one
|
|
394
407
|
// of two causes and not the one that actually fires: a non-Latin mark states a mark perfectly well and
|
|
@@ -400,8 +413,8 @@ export function renderFormNeighbourhoodJson(manifestMd, { markets = [], scripts
|
|
|
400
413
|
|| "manifest names no Dominant element / Formative root, and the job states no mark";
|
|
401
414
|
throw new Error(`form_neighbourhood_no_element: ${why} — nothing to seed the mechanical form band`);
|
|
402
415
|
}
|
|
403
|
-
const elements = seeds.map(({ element, role }) => ({ element, role, band: formNeighbourhood(element, { markets, scripts, droppedAxes, ordinaryWords }) }));
|
|
404
|
-
const families = variantFloorFamilies(elements, { mark, droppedAxes });
|
|
416
|
+
const elements = seeds.map(({ element, role }) => ({ element, role, band: formNeighbourhood(element, { markets, scripts, droppedAxes, ordinaryWords, mixedScriptQuery }) }));
|
|
417
|
+
const families = variantFloorFamilies(elements, { mark, droppedAxes, mixedScriptReason: mixedScriptNotSearchedReason(nativeScriptIndex) });
|
|
405
418
|
return JSON.stringify({
|
|
406
419
|
schema_version: 2,
|
|
407
420
|
generated: "deterministic (model-free) — edit-1 ∪ phonetic-family ∪ visual-confusable ∪ transliteration ∪ spacing-punctuation",
|
|
@@ -632,14 +645,27 @@ export function spacingPunctuationForms(mark) {
|
|
|
632
645
|
* dispatches), assigned to the first family that claims each term — so the family terms sum EXACTLY
|
|
633
646
|
* to what gets searched, never to a parallel list that drifts from it. PURE.
|
|
634
647
|
*/
|
|
635
|
-
|
|
648
|
+
/**
|
|
649
|
+
* Why the mixed-alphabet look-alikes were not searched, in the terms of what the register's index holds
|
|
650
|
+
* (`nativeScriptIndex`): a register that indexes the characters answers such a spelling as if its
|
|
651
|
+
* non-Latin letters were not there; a register that holds non-Latin filings by transliteration only
|
|
652
|
+
* cannot take the spelling at all. PURE.
|
|
653
|
+
*/
|
|
654
|
+
export function mixedScriptNotSearchedReason(nativeScriptIndex) {
|
|
655
|
+
return nativeScriptIndex === true
|
|
656
|
+
? "not searched — the register answers a mixed-alphabet spelling as if the non-Latin letters were absent (measured 2026-09-23)"
|
|
657
|
+
: "not searched — the register holds non-Latin filings by their transliteration only, so a mixed-alphabet spelling cannot be searched as written";
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export function variantFloorFamilies(elements, { mark = "", droppedAxes = [], mixedScriptReason = mixedScriptNotSearchedReason(true) } = {}) {
|
|
636
661
|
const drop = new Set(droppedAxes ?? []);
|
|
637
662
|
const edit = new Set(), visual = new Set(), translit = new Set(), other = new Set();
|
|
638
|
-
const wildcards = new Set(), keys = new Set(), notSearched = new Set();
|
|
663
|
+
const wildcards = new Set(), keys = new Set(), notSearched = new Set(), mixedNotSearched = new Set();
|
|
639
664
|
for (const el of elements ?? []) {
|
|
640
665
|
const band = el?.band;
|
|
641
666
|
if (!band) continue;
|
|
642
667
|
for (const t of band.ordinaryWordDifferentSound ?? []) notSearched.add(t);
|
|
668
|
+
for (const t of band.mixedScriptNotSearched ?? []) mixedNotSearched.add(t);
|
|
643
669
|
const edits = new Set(editNeighbourhood(el.element));
|
|
644
670
|
const confs = new Set(band.confusables ?? []);
|
|
645
671
|
const trans = new Set(band.transliterations ?? []);
|
|
@@ -685,6 +711,12 @@ export function variantFloorFamilies(elements, { mark = "", droppedAxes = [] } =
|
|
|
685
711
|
...(notSearched.size ? [{ family: "ordinary-word-different-sound", category: "phonetic", generator: "editNeighbourhood",
|
|
686
712
|
enumeration: "edit-1 neighbours that are ordinary English words and share no Double-Metaphone key with the element",
|
|
687
713
|
dispatch: "not searched — ordinary word, different sound", searched: false, dropped: true, terms: sorted(notSearched) }] : []),
|
|
714
|
+
// NOT SEARCHED ON THIS REGISTER, listed the same way and kept out of the floor for the same reason.
|
|
715
|
+
// Present only when the active register declares `mixedScriptQuery: false` (formNeighbourhood).
|
|
716
|
+
...(mixedNotSearched.size ? [{ family: "mixed-script-look-alike", category: "transliteration", generator: "transliterations",
|
|
717
|
+
enumeration: "whole-word Greek and Cyrillic look-alike swaps whose other letters stay Latin",
|
|
718
|
+
dispatch: mixedScriptReason,
|
|
719
|
+
searched: false, dropped: true, count: mixedNotSearched.size, terms: sorted(mixedNotSearched) }] : []),
|
|
688
720
|
];
|
|
689
721
|
}
|
|
690
722
|
|
|
@@ -759,9 +791,9 @@ export function mergeVariantFloor(floorFamilies, modelVariants, { rejectedSeeds
|
|
|
759
791
|
/**
|
|
760
792
|
* Is `root` a plausible formative root, or a prose-parse artifact?
|
|
761
793
|
*
|
|
762
|
-
* A stem is never LONGER than the element it stems from —
|
|
794
|
+
* A stem is never LONGER than the element it stems from — VOLEM(5) for ZOLEMA(6), HYDR(4) for HYDRA(5),
|
|
763
795
|
* VELTRI(6) for VELTRIN(7). Substring containment is NOT the test: a real root can differ in its leading
|
|
764
|
-
* character (
|
|
796
|
+
* character (ZOLEMA → VOLEM reaches the VOLEMA family), which is exactly the widening the root exists for.
|
|
765
797
|
* The swallowed-sentence artifacts are always the element PLUS a clause — 32-36 characters against a 5-char
|
|
766
798
|
* element — so length alone separates them cleanly without narrowing any legitimate root. PURE.
|
|
767
799
|
*/
|
|
@@ -299,9 +299,9 @@ export function deriveDirectiveRemedy(directive, { dominantElement = "", inScope
|
|
|
299
299
|
const item = String(d.item ?? "").trim();
|
|
300
300
|
if (!item || !inScopeStr.length) return null;
|
|
301
301
|
// A2 (PR-1, the 2026-07-28 label-as-term class): the item is the directive's display LABEL, and a
|
|
302
|
-
// label is only usable as a search term when it is mark-shaped. "Reverse-order
|
|
303
|
-
// (TROPICAL
|
|
304
|
-
// clean — TROPICAL
|
|
302
|
+
// label is only usable as a search term when it is mark-shaped. "Reverse-order WAVO composites
|
|
303
|
+
// (TROPICAL WAVO, ISLAND WAVO)" dispatched verbatim as `exact` is a nil search that reads as a
|
|
304
|
+
// clean — TROPICAL WAVO and ISLAND WAVO were ONE label, never two terms. The mint would now reject
|
|
305
305
|
// it (same shared lint), but the honest ending is decided HERE: no code remedy ⇒ the directive is
|
|
306
306
|
// DISCLOSED (the caller's defer lane), never swept blind. A directive that knows its terms says so
|
|
307
307
|
// in a structured `remedy` (precedence 1 above) — that is the documented fix (SKILL.md).
|
package/driver/gateway.mjs
CHANGED
|
@@ -703,7 +703,7 @@ export function syncCoverageForm(files) {
|
|
|
703
703
|
// was ever observed; this is the report. The axis set is UNCHANGED — filtering it would silently
|
|
704
704
|
// shrink the form, which is worse than the row it would avoid.
|
|
705
705
|
for (const a of (input.unknownAxisUnits ?? []))
|
|
706
|
-
note(`[coverage-form] register-units carries ${a}.md, which is not one of the register axes — its row is DRIVER-written and a seat cannot repair it. This is a driver fault, not a seat one
|
|
706
|
+
note(`[coverage-form] register-units carries ${a}.md, which is not one of the register axes — its row is DRIVER-written and a seat cannot repair it. This is a driver fault, not a seat one.`);
|
|
707
707
|
const prior = readCoverageForm(runDir, formName).rows;
|
|
708
708
|
const u = unionCoverageForm({ rows: prior }, { rows: null }, input);
|
|
709
709
|
// Said out loud on failure rather than swallowed — but UNLIKE this is not the fail-open leg:
|
|
@@ -1567,7 +1567,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1567
1567
|
// silences the REFUSAL, never the record — `modelActual`/`modelMismatch` keep landing on every row.
|
|
1568
1568
|
if (modelMismatch === true && !fail && envGateOn("CLEAROTRON_MODEL_WIRE_CHECK")) {
|
|
1569
1569
|
fail = `model_mismatch:${famRequested}->${famActual}`;
|
|
1570
|
-
note(`[${name}] MODEL MISMATCH: asked for ${modelRequested} (${famRequested}), the wire says ${modelActual} (${famActual}) — failing the turn. An unhonoured model override is an error, not a substitution
|
|
1570
|
+
note(`[${name}] MODEL MISMATCH: asked for ${modelRequested} (${famRequested}), the wire says ${modelActual} (${famActual}) — failing the turn. An unhonoured model override is an error, not a substitution; every number attributed to this turn would name the wrong model.`);
|
|
1571
1571
|
}
|
|
1572
1572
|
|
|
1573
1573
|
// AD-4 house rule — both computed UNCONDITIONALLY, three-valued on purpose:
|
|
@@ -1987,7 +1987,7 @@ async function runStageLadder(name, opts, stageCodexHome = null) {
|
|
|
1987
1987
|
// falls into the break below, and two rows for one decision would double every count taken off them.
|
|
1988
1988
|
const cutFired = identicalContent && producedNothing && warm && attempt < maxRetries + 1;
|
|
1989
1989
|
if (cutFired) {
|
|
1990
|
-
note(`[${name}] attempt ${attempt} [warm patch] reproduced attempt ${attempt - 1}'s failure byte-for-byte and the seat produced NOTHING either time (${fail.slice(0, 140)}) — NOT escalating to a fresh attempt ${attempt + 1}: measured 0 of 6 convergences on this shape
|
|
1990
|
+
note(`[${name}] attempt ${attempt} [warm patch] reproduced attempt ${attempt - 1}'s failure byte-for-byte and the seat produced NOTHING either time (${fail.slice(0, 140)}) — NOT escalating to a fresh attempt ${attempt + 1}: measured 0 of 6 convergences on this shape. A seat whose output was merely wrong still escalates.`);
|
|
1991
1991
|
// THE CUT IS RECORDED, NOT JUST NOTED. `note` is stderr only (log.mjs) and reaches no artifact, so
|
|
1992
1992
|
// the run record held no trace of a decision that PREVENTS an attempt — and because the cut
|
|
1993
1993
|
// returns, the prevented attempt never runs. n=6 would therefore be permanent: a seat or model
|
|
@@ -2255,7 +2255,7 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
|
|
|
2255
2255
|
if (/finding_registration_invalid/.test(lastFail)) {
|
|
2256
2256
|
extra = ` SPECIFIC FIX: a registration.uri is NEVER empty. If this finding has a fetched register record, ` +
|
|
2257
2257
|
`put its real record URI. If it has NO fetched record (a common-law finding, or a famous mark known only ` +
|
|
2258
|
-
`from general knowledge — e.g.
|
|
2258
|
+
`from general knowledge — e.g. KODAK on a CODAK clearance), it is NOT a register finding: set ` +
|
|
2259
2259
|
`"registrations":[]. And if it is a famous one-keystroke/homophone NEIGHBOUR kept only for diligence, move ` +
|
|
2260
2260
|
`it OUT of findings[] into the top-level "context_notes" array ({"type":"famous-neighbour-ungrounded",` +
|
|
2261
2261
|
`"mark","owner","context"}). NEVER invent an empty-uri registration to satisfy the schema.`;
|
|
@@ -2675,7 +2675,7 @@ export function correctionHint(lastFail, { gridLedgerName = "common-law-grid.jso
|
|
|
2675
2675
|
"Run the CONNOTATION / MEANING sweep — the mark AND its near-forms on the general web (Urban Dictionary, " +
|
|
2676
2676
|
"Wikipedia, news), query shapes \"[name] gang/slang/offensive/meaning\" — record EVERY query (even " +
|
|
2677
2677
|
"zero-result ones) into extras.pr_risk, and cite a `Connotation-search source:` line in the PR section. A " +
|
|
2678
|
-
"dictionary gloss is never a clearance (a mark can read as a benign given name yet sit one letter off \"
|
|
2678
|
+
"dictionary gloss is never a clearance (a mark can read as a benign given name yet sit one letter off \"Mara\", a street-gang label). You may " +
|
|
2679
2679
|
"instead report a real connotation hit — but a clean claim needs the search behind it";
|
|
2680
2680
|
} else if (/named_band_collapsed/.test(lastFail)) {
|
|
2681
2681
|
const slices = (lastFail.match(/named_band_collapsed:(.+)$/s) || [])[1] || "";
|
package/driver/jx-lanes.mjs
CHANGED
|
@@ -252,7 +252,7 @@ export function candidateRefusal(lane, cand) {
|
|
|
252
252
|
// have always taken it from jx-lanes.
|
|
253
253
|
export { romanizationRefusal, romanizationSpellings };
|
|
254
254
|
|
|
255
|
-
// ── Billing path for a
|
|
255
|
+
// ── Billing path for a jx ledger row ──────────────────────────────────────────────────────
|
|
256
256
|
// The jx model calls (fold completions, serp-judge, nativeread) are the ONLY dispatches in the driver
|
|
257
257
|
// that produce a per-token provider invoice. This comment used to name their executor as a
|
|
258
258
|
// `@anthropic-ai/sdk` Messages call; that has not been true since the lane lost its own destination
|
|
@@ -61,6 +61,24 @@ export function canonicalUri(uri) {
|
|
|
61
61
|
return normalizeRecordUri(uri) || String(uri ?? "").trim();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* The recall receipt as ONE company's audit and workbook may list it. Ruled 2026-09-23: the recall
|
|
66
|
+
* searches keep reading every company's remembered conflicts, so a new company's clearance of a mark
|
|
67
|
+
* still re-finds them, but no audit or workbook lists a recall check whose source row is another
|
|
68
|
+
* company's. The line itself says someone cleared this mark before. The receipt names the company the
|
|
69
|
+
* run is delivered for (`customer`) and each entry the companies whose deliveries remembered it
|
|
70
|
+
* (`customers`); an entry is listed when one of them is the run's own. A row that names no company
|
|
71
|
+
* counts only for a run that names none. A receipt written before it named companies lists as it
|
|
72
|
+
* always did, so an archived run republishes unchanged. Only removes entries; PURE.
|
|
73
|
+
*/
|
|
74
|
+
export function recallReceiptForOwnCompany(receipt) {
|
|
75
|
+
if (!isPlainObject(receipt) || !Object.hasOwn(receipt, "customer")) return receipt;
|
|
76
|
+
const own = receipt.customer || null;
|
|
77
|
+
const mine = (e) => Array.isArray(e?.customers) && e.customers.map((c) => c || null).includes(own);
|
|
78
|
+
const pick = (xs) => (Array.isArray(xs) ? xs.filter(mine) : xs);
|
|
79
|
+
return { ...receipt, directives: pick(receipt.directives), overflow: pick(receipt.overflow), refused: pick(receipt.refused) };
|
|
80
|
+
}
|
|
81
|
+
|
|
64
82
|
const ROW_KEYS = ["uri", "mark_text", "classes", "status", "source", "ts",
|
|
65
83
|
"owner", "jurisdiction", "customer", "opposition_end", "deadline_source_uri", "source_url",
|
|
66
84
|
"terminal"]; // v2 — all optional; v3 adds `terminal` (see acceptedConflicts below)
|
package/driver/log.mjs
CHANGED
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
import { appendFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
8
|
import { dirname, join, basename } from "node:path";
|
|
9
|
-
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
9
|
+
import { driverDir, RUN_DIR_MODE } from "../shared/driver-dir.mjs"; //
|
|
10
10
|
|
|
11
11
|
function appendLine(file, obj) {
|
|
12
|
-
mkdirSync(dirname(file), { recursive: true });
|
|
12
|
+
mkdirSync(dirname(file), { recursive: true, mode: RUN_DIR_MODE }); // a first log line can be what creates the run folder
|
|
13
13
|
appendFileSync(file, JSON.stringify({ ts: new Date().toISOString(), ...obj }) + "\n");
|
|
14
14
|
}
|
|
15
15
|
|
package/driver/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "clearotron-driver",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0-beta.1",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "Deterministic driver for the trademark clearance workflow: orchestration in code (fan-out, fan-in barrier, gating, retries); the model does judgment leaves only, through a reasoning CLI spawned per stage.",
|
|
8
8
|
"engines": {
|