clearotron 0.3.2-beta.0 → 0.3.2-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/build-info.json +2 -2
- package/driver/CHANGELOG.md +7 -0
- package/driver/contract-e3-backlog.mjs +1 -1
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +1 -1
- package/driver/pipeline.mjs +3 -3
- package/driver/portal-families.mjs +1 -1
- package/driver/predelivery-lint.mjs +23 -1
- package/driver/publish/index.mjs +24 -1
- package/driver/publish/report-data.mjs +2 -1
- package/driver/publish/search-depth.mjs +178 -0
- package/driver/suite-census.json +14 -2
- package/driver/terminal-clamp.mjs +41 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-ChIQsMYp.js → index-BsbasHjM.js} +3350 -3288
- package/portal-ui/dist/assets/{index-DBIs21e4.css → index-DNQpLYZF.css} +17 -1
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/freeze-example-run.mjs +27 -27
package/build-info.json
CHANGED
package/driver/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# clearotron-driver
|
|
2
2
|
|
|
3
|
+
## 0.3.2-beta.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 2a81abc: New: A report can now show how much was searched to reach its answer. It records the names read and cleared, the records read in each country, and the checks made. Countries where nothing was found are included.
|
|
8
|
+
- 2a81abc: Fixed: The conditions listed on a report are now written in plain legal English, matching the summary line above them. One condition could previously appear as an internal engine note with counts and identifiers in it.
|
|
9
|
+
|
|
3
10
|
## 0.3.2-beta.0
|
|
4
11
|
|
|
5
12
|
### Patch Changes
|
|
@@ -638,7 +638,7 @@ export const E3_BACKLOG = [
|
|
|
638
638
|
where: "driver/stages.mjs:3470",
|
|
639
639
|
surface: "stage-message",
|
|
640
640
|
evidence: "EVERY \"Grounded profile\" section MUST start its body with the line \"- ord: <N>\" naming which finding it grounds (use the ordinal from this list; a profile that grounds no listed finding omits the line)",
|
|
641
|
-
reparsedBy: "driver/publish/parse.mjs:339 parseCaseLawProfiles (\"the optional '- ord: <N>' first body line … gives an EXACT join\"); driver/findings-model.mjs:273 /^-\\s*ord:\\s*(\\d+)\\s*$/m; driver/publish/index.mjs:
|
|
641
|
+
reparsedBy: "driver/publish/parse.mjs:339 parseCaseLawProfiles (\"the optional '- ord: <N>' first body line … gives an EXACT join\"); driver/findings-model.mjs:273 /^-\\s*ord:\\s*(\\d+)\\s*$/m; driver/publish/index.mjs:778 runOrigins",
|
|
642
642
|
removedByMove: "NOTHING ON THE #850 PLAN REMOVES THIS",
|
|
643
643
|
},
|
|
644
644
|
{
|
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.3.2-beta.
|
|
5
|
+
"version": "0.3.2-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": {
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"methodology",
|
|
57
57
|
"handling_note"
|
|
58
58
|
],
|
|
59
|
-
"why": "a caption-only call renders 149B, clears the 120-char floor, and ships a SECTION OF THE CLIENT'S ONE REPORT without the 'Checks we ran' bullets, the methodology or the handling note. Traced: report-overview.md is the head of report.md (assembleReportMd() in pipeline.mjs) which renders to the HTML (publish/index.mjs:
|
|
59
|
+
"why": "a caption-only call renders 149B, clears the 120-char floor, and ships a SECTION OF THE CLIENT'S ONE REPORT without the 'Checks we ran' bullets, the methodology or the handling note. Traced: report-overview.md is the head of report.md (assembleReportMd() in pipeline.mjs) which renders to the HTML (publish/index.mjs:649 publishReport). The 'Only you can close these' register is NOT lost — it is code-built from findings.json under spec 64's 'code wins' ruling."
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
62
|
"tool": "record_prelim_variants",
|
package/driver/pipeline.mjs
CHANGED
|
@@ -10,7 +10,7 @@ import { readFileSync, existsSync, mkdirSync, writeFileSync, renameSync, copyFil
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { join, dirname, basename, resolve } from "node:path"; // resolve: the resume line must work from any cwd
|
|
12
12
|
import { driverDir, driverRel, ensureDriverDir } from "../shared/driver-dir.mjs"; // — one definition of where `_driver/` is
|
|
13
|
-
import { terminalClampDecision, orderClausesForLede } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
13
|
+
import { terminalClampDecision, orderClausesForLede, clientConditions } from "./terminal-clamp.mjs"; // — deliver and clamp, never withhold
|
|
14
14
|
import { recordSpan } from "./attributed-span.mjs"; // — driver work the decomposition can attribute
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
import { runStage, correctionHint, gridLedgerNameFor, draftCarryEligible, toolWrittenArtifact, selectEngine } from "./gateway.mjs";
|
|
@@ -13006,7 +13006,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
13006
13006
|
const statement = riskStatement({ tier: derived.tier, verdict, reasons: reasonsOut, clauses: orderedClauses,
|
|
13007
13007
|
basis: isRegisterOnly(ctx.searchPolicy) ? "register-only" : null });
|
|
13008
13008
|
const tmp = driverDir(run.runDir, "verdict.json.tmp");
|
|
13009
|
-
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, kinds: kindsOut,
|
|
13009
|
+
writeFileSync(tmp, JSON.stringify({ ts: new Date().toISOString(), verdict, reasons: reasonsOut, clauses: orderedClauses, kinds: kindsOut,
|
|
13010
13010
|
tier: derived.tier, badge: derived.badge, gaugeIndex: derived.gaugeIndex, maxComposite: derived.maxComposite,
|
|
13011
13011
|
band: derived.band ?? null, statement, stance: verdictStance(verdict) }, null, 2));
|
|
13012
13012
|
renameSync(tmp, driverDir(run.runDir, "verdict.json"));
|
|
@@ -14859,7 +14859,7 @@ async function pipelineInner(job, opts = {}) {
|
|
|
14859
14859
|
let emailVerdictOpts = { productName: emailProductName ?? undefined };
|
|
14860
14860
|
// SPREAD, never reassign: this used to replace the whole object, which would now drop productName
|
|
14861
14861
|
// above on every run that has a verdict sidecar — i.e. on every healthy run, and on no test.
|
|
14862
|
-
try { const v = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); emailVerdictOpts = { ...emailVerdictOpts, verdict: v.verdict, conditions: v
|
|
14862
|
+
try { const v = JSON.parse(readFileSync(driverDir(run.runDir, "verdict.json"), "utf8")); emailVerdictOpts = { ...emailVerdictOpts, verdict: v.verdict, conditions: clientConditions(v), tier: v.tier, statement: v.statement ?? null }; } catch { /* legacy path — no row */ }
|
|
14863
14863
|
// wp50 — thread the findings too: the table overlay's rating cells are code-bound to the canonical
|
|
14864
14864
|
// ratings (joinFindingToBlock), never the summary's own words.
|
|
14865
14865
|
try { emailVerdictOpts.findings = parseFindingsJsonLenient(readFileSync(P.findings, "utf8"))?.findings ?? undefined; } catch { /* no findings — table falls back to the summary words */ }
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
//
|
|
10
10
|
// ── why this is a sidecar and not a field in meta.json ────────────────────────────────────────────────
|
|
11
11
|
//
|
|
12
|
-
// The same reason `archive-tags.json` is one, recorded at publish/index.mjs:
|
|
12
|
+
// The same reason `archive-tags.json` is one, recorded at publish/index.mjs:377 ARCHIVE_TAGS_FILE: "meta.json is rewritten
|
|
13
13
|
// on every republish so a flag there would be lost." That is not hypothetical here — a `rerender-all`
|
|
14
14
|
// pass over the pool is a live plan, and a family written into meta would be erased by the very operation
|
|
15
15
|
// meant to bring old reports up to date. Curation state that a person entered by hand must outlive a
|
|
@@ -32,6 +32,7 @@ import { writeUpViolations, writeUpMessage } from "./narrative-write-ups.mjs";
|
|
|
32
32
|
import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } from "./registry-fidelity.mjs";
|
|
33
33
|
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
|
+
import { clientConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
35
36
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
36
37
|
import { isEngineAppendedCaveat } from "./verify-knockout.mjs"; // ONE derivation for "the engine appended this caveat, not a seat"
|
|
37
38
|
|
|
@@ -1961,6 +1962,26 @@ export function verdictActionsCoherenceChecks({ actionsRegister, findings, verdi
|
|
|
1961
1962
|
// the conditional FORM riskStatement composes ("<Tier> — conditional on: <facts>"), never a wording
|
|
1962
1963
|
// regex. Legacy sidecars (no stance) get the tier + unconditional-proceed checks only — judged
|
|
1963
1964
|
// against the wording THEIR era composed (the retired-phrase exemption below), never the new form.
|
|
1965
|
+
// ── THE CONDITIONS A CLIENT READS SPEAK A LAWYER'S NOUNS ────────────────────────────────────────────
|
|
1966
|
+
// The sidecar carries two texts per condition — a run-record `reason` that may name tokens, counts and
|
|
1967
|
+
// record ids, and the reader's `clause`. `clientConditions` is the one reader of that pair, so this
|
|
1968
|
+
// check asks the question over what a surface actually renders rather than over either array.
|
|
1969
|
+
//
|
|
1970
|
+
// IT FIRES ON A LEGACY SIDECAR, DELIBERATELY. A run recorded before clauses were persisted has none, so
|
|
1971
|
+
// every condition falls back to its run-record reason and a token-bearing one flags here. That is the
|
|
1972
|
+
// honest reading: the delivered page for that run does carry the token, and re-generating the run is
|
|
1973
|
+
// what clears it. A flag, never a withholding — the report ships and the operator sees where the gap is.
|
|
1974
|
+
export function clientConditionVoiceChecks({ verdictDoc }) {
|
|
1975
|
+
if (!verdictDoc) return [];
|
|
1976
|
+
const carrying = clientConditions(verdictDoc)
|
|
1977
|
+
.map((c) => ({ c, m: String(c).match(ENGINE_TOKEN_RE) }))
|
|
1978
|
+
.filter((x) => x.m);
|
|
1979
|
+
return [check("client-condition-voice", "verdict", "report", carrying.length === 0,
|
|
1980
|
+
carrying.length
|
|
1981
|
+
? `${carrying.length} delivered condition${carrying.length === 1 ? "" : "s"} carr${carrying.length === 1 ? "ies" : "y"} an engine identifier, so the client's "conditional on:" list reads the run record rather than the reader's sentence: ${carrying.slice(0, 3).map((x) => `"${x.m[0]}"`).join(", ")}${carrying.length > 3 ? ` (+${carrying.length - 3} more)` : ""}. The clause exists at the clamp site; persist it and this list takes it. A sidecar written before clauses were persisted flags until the run is re-generated.`
|
|
1982
|
+
: "")];
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1964
1985
|
export function statementCoherenceChecks({ verdictDoc }) {
|
|
1965
1986
|
if (!verdictDoc?.statement) return [];
|
|
1966
1987
|
const st = String(verdictDoc.statement), tier = String(verdictDoc.tier ?? "");
|
|
@@ -2292,7 +2313,8 @@ export function runLint({ depth, commonLawGrid, matterContext, clientPartyName,
|
|
|
2292
2313
|
if (findings && clientSummaryMd) checks.push(...clientTierChecks({ clientSummaryMd, findings, manifest })); // A2 (doc 50: band words via the frozen manifest)
|
|
2293
2314
|
if (verdictDoc?.tier && reportMd) checks.push(...overallTierChecks({ reportMd, verdictDoc })); // wp50/wi2
|
|
2294
2315
|
checks.push(...verdictActionsCoherenceChecks({ actionsRegister, findings, verdictDoc })); // spec 64
|
|
2295
|
-
checks.push(...statementCoherenceChecks({ verdictDoc }));
|
|
2316
|
+
checks.push(...statementCoherenceChecks({ verdictDoc }));
|
|
2317
|
+
checks.push(...clientConditionVoiceChecks({ verdictDoc })); // — the reader's clause, not the run record // spec 64
|
|
2296
2318
|
checks.push(...conditionalTextCoherenceChecks({ reportMd, clientSummaryMd, verdictDoc })); // qw/verdict-text-coherence — CONDITIONAL badge vs clean-outcome prose
|
|
2297
2319
|
checks.push(...prescriptionProseChecks({ reportMd, clientSummaryMd, findings, fourAnswers })); // PR-3 report voice — facts that condition, never advice; flag-only (P5: the findings-derived prose is a delivered surface too)
|
|
2298
2320
|
checks.push(...onlyYouRegisterChecks({ actionsRegister, findings, reportMd })); // spec 64
|
package/driver/publish/index.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import { buildAudit } from './xlsx.mjs';
|
|
|
16
16
|
import { parseFindingsJson, parseFindingsJsonLenient, deriveDisplayVerdict, joinFindingToBlock, CLIENT_TIER_BY_COMPOSITE, projectCoverageJudgment } from '../findings-model.mjs';
|
|
17
17
|
import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.mjs'; // — and why an absence did not close
|
|
18
18
|
import { clearanceReportData } from './report-data.mjs';
|
|
19
|
+
import { searchDepthRecord } from './search-depth.mjs'; // how much was read to reach the answer, as counts and tokens
|
|
19
20
|
import { parseFrameworkManifest } from '../framework.mjs';
|
|
20
21
|
import { rollupTokens } from '../tokens.mjs';
|
|
21
22
|
import { reportIdentityFor, productCoverageNote, isRegisterOnly } from '../search-policy.mjs';
|
|
@@ -1053,7 +1054,29 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1053
1054
|
// worse demo than no toggle.
|
|
1054
1055
|
const reportNav = siteNav(poolRoot, 'report', null, '../', { anon: false });
|
|
1055
1056
|
// `demoData` is resolved above the report.md write — one answer, every surface.
|
|
1056
|
-
|
|
1057
|
+
// ── HOW MUCH WAS READ TO REACH THE ANSWER ──────────────────────────────────────────────────────
|
|
1058
|
+
// Derived here rather than in a stage so a REPUBLISH of an archived run picks the fields up with no
|
|
1059
|
+
// re-run: every source below is something the run already wrote. Best-effort per source, in the house
|
|
1060
|
+
// pattern — a run with no grid or no case-law layer still gets its register counts, and the absent
|
|
1061
|
+
// ones report themselves as zero or `not-in-scope` rather than as a gap nobody can see.
|
|
1062
|
+
let searchDepth = null;
|
|
1063
|
+
try {
|
|
1064
|
+
const runBase = runDir ?? dirname(reportMd);
|
|
1065
|
+
const recDir = join(runBase, '_records');
|
|
1066
|
+
const rdText = (p2) => { try { return readFileSync(p2, 'utf8'); } catch { return ''; } };
|
|
1067
|
+
const rdJson = (p2) => { try { return JSON.parse(readFileSync(p2, 'utf8')); } catch { return null; } };
|
|
1068
|
+
searchDepth = searchDepthRecord({
|
|
1069
|
+
auditMd: (auditMd && existsSync(auditMd)) ? rdText(auditMd) : '',
|
|
1070
|
+
recordIndex: recordsByUri ?? {},
|
|
1071
|
+
recordFileNames: existsSync(recDir) ? readdirSync(recDir) : [],
|
|
1072
|
+
commonLawGrid: rdJson(join(runBase, 'common-law-grid.json')),
|
|
1073
|
+
caseLawText: rdText(join(dirname(reportMd), 'case-law-findings.md')),
|
|
1074
|
+
registerPlan: rdJson(driverDir(runBase, 'register-plan.json')),
|
|
1075
|
+
});
|
|
1076
|
+
writeRO('search-depth.json', JSON.stringify(searchDepth, null, 2));
|
|
1077
|
+
} catch { /* the depth record is additive — a publish never fails for want of it */ }
|
|
1078
|
+
|
|
1079
|
+
writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
|
|
1057
1080
|
// ONE report (spec 2026-07-30 §5): report.client.html is no longer written. The knockout lane's own
|
|
1058
1081
|
// collapse note is the precedent: "two renderings of one run is how the wrong link gets sent". The
|
|
1059
1082
|
// client host serves the same report.html through the portal's readReport() (cleaning built in) — its
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
// testable offline and a republished archived run reproduces its file deterministically.
|
|
33
33
|
import { stripInternal, dropLabelledInternals, stripEngineInternals } from './parse.mjs';
|
|
34
34
|
import { DISPOSITION_GROUP, deriveActionConditions, projectAssessmentField } from '../findings-model.mjs';
|
|
35
|
+
import { clientConditions } from '../terminal-clamp.mjs'; // the reader's clause per condition — one definition, shared with the email
|
|
35
36
|
|
|
36
37
|
// The client scrub choke point: the three existing rules, in the order the client HTML applies them.
|
|
37
38
|
// Structural values (uris, enum tokens, dates, numbers) do not route through here — they carry no prose.
|
|
@@ -101,7 +102,7 @@ export function clearanceReportData({
|
|
|
101
102
|
badge: verdictInfo.badge ?? null,
|
|
102
103
|
band: verdictInfo.band ?? null,
|
|
103
104
|
statement: clientText(verdictInfo.statement),
|
|
104
|
-
conditions: (
|
|
105
|
+
conditions: clientConditions(verdictInfo).map(clientText).filter(Boolean),
|
|
105
106
|
} : null,
|
|
106
107
|
caption: clientText(caption),
|
|
107
108
|
jurisdiction: clientText(jurisdiction),
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
//
|
|
4
|
+
// ── HOW MUCH WAS READ TO REACH THE ANSWER, AS MACHINE FIELDS ────────────────────────────────────────
|
|
5
|
+
//
|
|
6
|
+
// A deeper search reads more and finds no more, and until now the report could not say so: a run that
|
|
7
|
+
// read 1,455 register records and cleared 432 near-names delivered a page showing 13 findings and
|
|
8
|
+
// nothing of the rest. The negative evidence — the work that came back clean — existed only in the
|
|
9
|
+
// audit workbook, in the engine's own working voice.
|
|
10
|
+
//
|
|
11
|
+
// This module derives that body of negative evidence from artifacts the run has ALREADY written. It
|
|
12
|
+
// starts no search, asks no model and composes no sentence: every field here is a count, a token, or a
|
|
13
|
+
// fact copied from a record. That is the whole design constraint, and it is why this lives at publish
|
|
14
|
+
// time rather than in a stage — a republish of an archived run picks the fields up with no re-run.
|
|
15
|
+
//
|
|
16
|
+
// THE GROUP KEY IS THE PART THAT COULD HAVE GONE WRONG. Each cleared near-name is grouped by WHY it was
|
|
17
|
+
// cleared, and the honest source is the register providers' closed screening vocabulary, which every
|
|
18
|
+
// provider computes identically: `drop:dead` (a confidently dead status), `drop:out-of-class` (live but
|
|
19
|
+
// no in-scope-class overlap), `surface:in-scope-live` / `surface:all-class` (a real in-scope candidate),
|
|
20
|
+
// `deepfetch:ambiguous` (status unrecognised). A verdict and the record's own status are facts. The
|
|
21
|
+
// engine's `result` paragraph is not a fact about the record, it is prose about the reasoning, and
|
|
22
|
+
// classifying on it is how a report ends up asserting a reason the record does not support.
|
|
23
|
+
//
|
|
24
|
+
// SO THIS EMITS THREE GROUPS, NOT FOUR, AND THAT IS DELIBERATE. `dead-filing` and `different-goods` fall
|
|
25
|
+
// straight out of the vocabulary. Everything else is `other` — read, in scope, and cleared on judgment.
|
|
26
|
+
// A fourth group, "different word", cannot be derived from a screening verdict or a status: a name
|
|
27
|
+
// cleared because it reads as a different word was `surface:in-scope-live` like any other real
|
|
28
|
+
// candidate, and only the reasoning says otherwise. Splitting it would mean either reading that prose
|
|
29
|
+
// or inventing a similarity rule here, and a wrong group on a client page is worse than a coarse
|
|
30
|
+
// honest one. Recorded on the issue rather than guessed at.
|
|
31
|
+
//
|
|
32
|
+
// BREAK MATRIX:
|
|
33
|
+
// · a dead status groups as dead-filing → break: drop the status arm, arm 1 red
|
|
34
|
+
// · an out-of-class verdict groups as goods → break: map it to other, arm 2 red
|
|
35
|
+
// · an in-scope live name groups as other → break: classify on the result prose, arm 3 red
|
|
36
|
+
// · a per-country count names every country read → break: count only countries with a finding, arm 5 red
|
|
37
|
+
// · court decisions distinguishes four states → break: collapse none-found into not-checked, arm 6 red
|
|
38
|
+
|
|
39
|
+
/** The closed set a group key may take. The renderer's headings are keyed on these, never on prose. */
|
|
40
|
+
export const CLEARED_GROUPS = Object.freeze(["dead-filing", "different-goods", "other"]);
|
|
41
|
+
|
|
42
|
+
/** Statuses a register reports for a filing that is no longer live. Surfaced by the provider, never date-cut. */
|
|
43
|
+
const DEAD_STATUS_RE = /^(?:CANCELLED|CANCELED|EXPIRED|ABANDONED|WITHDRAWN|REFUSED|DEAD|LAPSED|INVALID|SURRENDERED)\b/i;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Why this near-name was cleared, from the two fields the engine records per name. PURE.
|
|
47
|
+
*
|
|
48
|
+
* @param {{screenVerdict?: string, status?: string}} a
|
|
49
|
+
* @returns {"dead-filing"|"different-goods"|"other"}
|
|
50
|
+
*/
|
|
51
|
+
export function groupForCleared({ screenVerdict, status } = {}) {
|
|
52
|
+
const v = String(screenVerdict ?? "").trim().toLowerCase();
|
|
53
|
+
const s = String(status ?? "").trim();
|
|
54
|
+
if (v.startsWith("drop:dead") || DEAD_STATUS_RE.test(s)) return "dead-filing";
|
|
55
|
+
if (v.startsWith("drop:out-of-class")) return "different-goods";
|
|
56
|
+
return "other";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One `## NRn` block's `- key: value` lines. The audit is written by a deterministic builder, so this is a contract. */
|
|
60
|
+
const field = (block, key) => (block.match(new RegExp(`^- ${key}:\\s*(.*)$`, "m")) || [])[1]?.trim() ?? "";
|
|
61
|
+
const noteField = (notes, key) => (notes.match(new RegExp(`${key}=([^;]+)`)) || [])[1]?.trim() ?? "";
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every near-name the run read and cleared, register and web, as facts. PURE.
|
|
65
|
+
*
|
|
66
|
+
* The register rows carry no sentence: mark, owner, country, class, status, group and the record URI.
|
|
67
|
+
* The reasoning stays in the audit workbook, where the engine already wrote it — the owner ruled against
|
|
68
|
+
* a second client-facing sentence per name (2026-09-16), so this deliberately does not carry `result`.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} auditMd the run's `audit.md`
|
|
71
|
+
* @param {Record<string, object>} recordIndex fetched records by URI, for the fuller mark and owner
|
|
72
|
+
*/
|
|
73
|
+
export function clearedNames(auditMd, recordIndex = {}) {
|
|
74
|
+
const out = { register: [], web: [] };
|
|
75
|
+
for (const block of String(auditMd ?? "").split(/^## /m).slice(1)) {
|
|
76
|
+
const title = block.split("\n")[0].trim();
|
|
77
|
+
const layer = field(block, "source_layer");
|
|
78
|
+
if (/^NR\d+/.test(title) && /register/i.test(layer)) {
|
|
79
|
+
const notes = field(block, "notes");
|
|
80
|
+
const uri = (notes.match(/URI (\S+?);/) || [])[1] ?? "";
|
|
81
|
+
const screenVerdict = noteField(notes, "screen_verdict");
|
|
82
|
+
const status = noteField(notes, "status");
|
|
83
|
+
const classes = noteField(notes, "class");
|
|
84
|
+
const country = ((uri.match(/^\/mark\/([a-z]{2})\//) || [])[1] ?? "").toUpperCase();
|
|
85
|
+
const rec = recordIndex[uri] ?? {};
|
|
86
|
+
out.register.push({
|
|
87
|
+
term: field(block, "search_term"),
|
|
88
|
+
mark: rec.markText || rec.mark || field(block, "search_term"),
|
|
89
|
+
owner: rec.owner ?? "", country, classes: rec.classes || classes,
|
|
90
|
+
status: rec.statusText || status,
|
|
91
|
+
group: groupForCleared({ screenVerdict, status: rec.statusText || status }),
|
|
92
|
+
uri,
|
|
93
|
+
});
|
|
94
|
+
} else if (!/^NR\d+/.test(title) && /common-law/i.test(layer) && !/^\(none/i.test(title)) {
|
|
95
|
+
out.web.push({ title, url: field(block, "url"), type: field(block, "type") });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Register records read, per country, from the run's own `_records/` listing. PURE.
|
|
103
|
+
*
|
|
104
|
+
* EVERY COUNTRY THE RUN READ, including the ones that came back clean — those are the whole point. A
|
|
105
|
+
* count keyed off the findings would list only countries with a conflict, which is the gap this closes.
|
|
106
|
+
*
|
|
107
|
+
* @param {string[]} recordFileNames the `_records/` directory listing, named `<cc>-<id>.json`
|
|
108
|
+
*/
|
|
109
|
+
export function recordsByCountry(recordFileNames = []) {
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const name of recordFileNames) {
|
|
112
|
+
const cc = (String(name).match(/^([a-z]{2})-/i) || [])[1];
|
|
113
|
+
if (cc) out[cc.toUpperCase()] = (out[cc.toUpperCase()] ?? 0) + 1;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Marketplace, web, reputation and meaning checks, from the deterministic grid the tools wrote. PURE. */
|
|
119
|
+
export function sweepCounts(commonLawGrid, auditMd = "") {
|
|
120
|
+
const cells = Array.isArray(commonLawGrid?.cells) ? commonLawGrid.cells : [];
|
|
121
|
+
const counts = {
|
|
122
|
+
checks: cells.length,
|
|
123
|
+
platforms: new Set(cells.map((c) => c?.platform).filter(Boolean)).size,
|
|
124
|
+
spellings: new Set(cells.map((c) => c?.term).filter(Boolean)).size,
|
|
125
|
+
reputation: Array.isArray(commonLawGrid?.extras?.pr_risk) ? commonLawGrid.extras.pr_risk.length : 0,
|
|
126
|
+
};
|
|
127
|
+
// A run old enough to predate the grid has no machine record; its per-term log is the audit's own
|
|
128
|
+
// common-law rows. Not a fallback masking a defect — those runs have nothing else to read.
|
|
129
|
+
if (!counts.checks && auditMd) counts.checks = (String(auditMd).match(/^- source_layer: Common-law/gm) || []).length;
|
|
130
|
+
return counts;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What the court-decisions pass came back with, as one token. PURE.
|
|
135
|
+
*
|
|
136
|
+
* FOUR STATES, BECAUSE THREE OF THEM MEAN DIFFERENT THINGS TO A READER. "None found" is a result and
|
|
137
|
+
* "could not be checked" is a gap; collapsing them would let an unreachable source read as a clean
|
|
138
|
+
* negative, which is the one thing a clearance may never do. "Not in scope" is neither — the product
|
|
139
|
+
* offers the pass on a full country search only.
|
|
140
|
+
*
|
|
141
|
+
* @returns {"found"|"none-found"|"not-checked"|"not-in-scope"}
|
|
142
|
+
*/
|
|
143
|
+
export function courtDecisionsState(caseLawText) {
|
|
144
|
+
const t = String(caseLawText ?? "");
|
|
145
|
+
if (!t.trim()) return "not-in-scope";
|
|
146
|
+
if (/source unreachable|could not be reached|CONNECTION_CLOSED|not reachable|quota/i.test(t)) return "not-checked";
|
|
147
|
+
if (/No on-point precedent found|none found|no decisions found/i.test(t)) return "none-found";
|
|
148
|
+
return "found";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Was the name searched in a non-Latin script? Read off the plan's own terms, never asserted. PURE. */
|
|
152
|
+
export function localScriptSearched(registerPlan) {
|
|
153
|
+
const entries = Array.isArray(registerPlan?.entries) ? registerPlan.entries : [];
|
|
154
|
+
return entries.some((e) => /[^\x00-\x7F]/.test(String(e?.term ?? "")));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The whole record, composed from what the run holds. PURE — every argument is already-written material.
|
|
159
|
+
*
|
|
160
|
+
* @returns {{schemaVersion: number, cleared: object, counts: object}}
|
|
161
|
+
*/
|
|
162
|
+
export function searchDepthRecord({ auditMd = "", recordIndex = {}, recordFileNames = [], commonLawGrid = null, caseLawText = "", registerPlan = null } = {}) {
|
|
163
|
+
const cleared = clearedNames(auditMd, recordIndex);
|
|
164
|
+
const groups = {};
|
|
165
|
+
for (const key of CLEARED_GROUPS) groups[key] = 0;
|
|
166
|
+
for (const c of cleared.register) groups[c.group] += 1;
|
|
167
|
+
return {
|
|
168
|
+
schemaVersion: 1,
|
|
169
|
+
cleared: { register: cleared.register, web: cleared.web, groups },
|
|
170
|
+
counts: {
|
|
171
|
+
recordsByCountry: recordsByCountry(recordFileNames),
|
|
172
|
+
recordsRead: recordFileNames.length,
|
|
173
|
+
sweep: sweepCounts(commonLawGrid, auditMd),
|
|
174
|
+
localScriptSearched: localScriptSearched(registerPlan),
|
|
175
|
+
courtDecisions: courtDecisionsState(caseLawText),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
}
|
package/driver/suite-census.json
CHANGED
|
@@ -141,6 +141,12 @@
|
|
|
141
141
|
"skips": 0,
|
|
142
142
|
"todos": 0
|
|
143
143
|
},
|
|
144
|
+
"a-client-condition-never-carries-an-engine-token.test.mjs": {
|
|
145
|
+
"tests": 6,
|
|
146
|
+
"asserts": 10,
|
|
147
|
+
"skips": 0,
|
|
148
|
+
"todos": 0
|
|
149
|
+
},
|
|
144
150
|
"a-codex-login-survives-its-own-refresh.test.mjs": {
|
|
145
151
|
"tests": 4,
|
|
146
152
|
"asserts": 18,
|
|
@@ -1643,7 +1649,7 @@
|
|
|
1643
1649
|
},
|
|
1644
1650
|
"coverage-is-disclosed-never-refused.test.mjs": {
|
|
1645
1651
|
"tests": 10,
|
|
1646
|
-
"asserts":
|
|
1652
|
+
"asserts": 40,
|
|
1647
1653
|
"skips": 0,
|
|
1648
1654
|
"todos": 0
|
|
1649
1655
|
},
|
|
@@ -5001,6 +5007,12 @@
|
|
|
5001
5007
|
"skips": 1,
|
|
5002
5008
|
"todos": 0
|
|
5003
5009
|
},
|
|
5010
|
+
"the-report-can-say-how-much-was-read.test.mjs": {
|
|
5011
|
+
"tests": 8,
|
|
5012
|
+
"asserts": 23,
|
|
5013
|
+
"skips": 0,
|
|
5014
|
+
"todos": 0
|
|
5015
|
+
},
|
|
5004
5016
|
"the-report-carries-what-the-assessment-wrote.test.mjs": {
|
|
5005
5017
|
"tests": 10,
|
|
5006
5018
|
"asserts": 41,
|
|
@@ -5979,7 +5991,7 @@
|
|
|
5979
5991
|
},
|
|
5980
5992
|
"home.test.ts": {
|
|
5981
5993
|
"tests": 56,
|
|
5982
|
-
"asserts":
|
|
5994
|
+
"asserts": 197,
|
|
5983
5995
|
"skips": 0,
|
|
5984
5996
|
"todos": 0
|
|
5985
5997
|
},
|
|
@@ -106,3 +106,44 @@ export function orderClausesForLede(clauses, reasons, guardSet) {
|
|
|
106
106
|
const ordered = [...aligned.filter((x) => !guardSet?.has?.(x.c)), ...aligned.filter((x) => guardSet?.has?.(x.c))];
|
|
107
107
|
return { clauses: ordered.map((x) => x.c), reasons: ordered.map((x) => x.r) };
|
|
108
108
|
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* THE CLIENT'S CONDITION LIST, from a verdict sidecar. PURE.
|
|
112
|
+
*
|
|
113
|
+
* TWO TEXTS, NEVER ONE — stated at the top of this module, and until now honoured at only one of the
|
|
114
|
+
* two ends. Every clamp site composes a run-record `reason` (token, counts, record ids) and a reader
|
|
115
|
+
* `clause` (the same fact in a lawyer's nouns), and `terminalClampDecision` refuses a clause that
|
|
116
|
+
* carries an engine identifier. The sidecar then persisted `reasons` alone and threw the clauses away,
|
|
117
|
+
* so every client surface had only the run-record text to render. A delivered report's conditions
|
|
118
|
+
* opened with `floor_duty_undischarged:4 of 430 floor row(s)…` while the SAME run's risk statement, one
|
|
119
|
+
* line above, read the clean clause: `riskStatement` already prefers `clauses`, so the page contradicted
|
|
120
|
+
* its own headline. This function is the other end of that rule.
|
|
121
|
+
*
|
|
122
|
+
* THE CLAUSE WINS WHERE THERE IS ONE, and the fallback is not politeness. Three machinery sites push
|
|
123
|
+
* the reason AS the clause ("machinery reasons ARE factual open-states"), so for those entries the two
|
|
124
|
+
* texts are one string and this returns it unchanged — correctly, because no second text exists to
|
|
125
|
+
* prefer. A legacy sidecar written before this change carries no `clauses` key at all and falls back
|
|
126
|
+
* entirely, which is what keeps archived runs republishable.
|
|
127
|
+
*
|
|
128
|
+
* ALIGNMENT IS BY INDEX AND THE REASONS ARE THE COUNT AUTHORITY. `orderClausesForLede` pairs the two
|
|
129
|
+
* arrays and reorders them together; a BLOCKING verdict then appends its grounds to the reasons alone,
|
|
130
|
+
* so `clauses` is legitimately SHORTER. Mapping over reasons and reaching for `clauses[i]` is what
|
|
131
|
+
* makes that safe — never `clauses.map`, which would silently drop the appended grounds.
|
|
132
|
+
*
|
|
133
|
+
* BREAK MATRIX:
|
|
134
|
+
* · a clause replaces its token-bearing reason → break: return reasons unchanged, arm 1 red
|
|
135
|
+
* · a clean reason with no clause survives → break: return "" for a missing clause, arm 2 red
|
|
136
|
+
* · a legacy sidecar still yields its conditions → break: require the clauses key, arm 3 red
|
|
137
|
+
* · clauses shorter than reasons loses nothing → break: map over clauses, arm 4 red
|
|
138
|
+
*
|
|
139
|
+
* @param {{reasons?: string[], clauses?: string[]}} sidecar the parsed `_driver/verdict.json`
|
|
140
|
+
* @returns {string[]} one condition per reason, in the sidecar's own order
|
|
141
|
+
*/
|
|
142
|
+
export function clientConditions({ reasons, clauses } = {}) {
|
|
143
|
+
const rs = Array.isArray(reasons) ? reasons : [];
|
|
144
|
+
const cs = Array.isArray(clauses) ? clauses : [];
|
|
145
|
+
return rs.map((r, i) => {
|
|
146
|
+
const clause = typeof cs[i] === "string" ? cs[i].trim() : "";
|
|
147
|
+
return clause || String(r ?? "").trim();
|
|
148
|
+
}).filter(Boolean);
|
|
149
|
+
}
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-artifacts-mcp",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "MCP server to interrogate clearotron trademark-clearance runs — list/read artifacts, trace the full decision flow, telemetry/cost, coverage, single-run search, and a gated single-step what-if. Imports the clearotron-driver read-only; touches no driver/template/deploy files.",
|