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