clearotron 0.3.2-beta.4 → 0.3.2-beta.6
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/INSTALL.md +1 -0
- package/bin/onboard.mjs +65 -0
- package/bin/start.mjs +14 -0
- package/build-info.json +2 -2
- package/demo/knockout-search/run/_driver/predelivery-lint.json +26 -2
- package/driver/CHANGELOG.md +16 -0
- package/driver/common-law-receipts.mjs +26 -0
- package/driver/contract-e3-backlog.mjs +3 -3
- package/driver/contract-vocabulary.mjs +6 -6
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +4 -2
- package/driver/publish/render-knockout.mjs +231 -98
- package/driver/publish/render.mjs +399 -152
- package/driver/publish/templates/report.css +103 -3
- package/driver/registry-fidelity.mjs +28 -0
- package/driver/stages.mjs +3 -3
- package/driver/suite-census.json +31 -19
- package/driver/verify.mjs +27 -1
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/lib/audit.mjs +14 -2
- package/mcp-server/lib/http-handler.mjs +52 -13
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/shared/driver-dir.mjs +1 -1
|
@@ -313,8 +313,13 @@
|
|
|
313
313
|
rows (.rrow), secondary groups (.rgroup), the scope section title (details.scope) — print as static
|
|
314
314
|
headers; the old blanket details>summary hide deleted every region/group header and the scope title
|
|
315
315
|
from the exported PDF.
|
|
316
|
-
EVERY <details> THE RENDERERS EMIT MUST BE IN ONE LIST OR THE OTHER
|
|
317
|
-
a
|
|
316
|
+
EVERY <details> THE RENDERERS EMIT MUST BE IN ONE LIST OR THE OTHER. The instrument that says so
|
|
317
|
+
is a browser under print media, and it is NOT in this repository: `render-css.test.mjs`, named
|
|
318
|
+
here until 2026-09-16, exists in no tree. Nothing in the public suite can see this — the frozen
|
|
319
|
+
hash pins the renderer's bytes and the render arms grep its HTML, and on screen a mis-ruled
|
|
320
|
+
disclosure is correct. It is the private print control that walks a rendered page on both lanes
|
|
321
|
+
and refuses a disclosure ruled by neither mechanism, and it runs by hand. Two folds have now
|
|
322
|
+
shipped mis-ruled and been caught there rather than in CI. `details.sub-more` (#470) was in NEITHER, so the exported PDF — the
|
|
318
323
|
client-facing legal document — printed an inert "MORE" label and a right-pointing COLLAPSED marker
|
|
319
324
|
in the hero, directly above text that was already fully expanded. Measured in Chrome: the caption's
|
|
320
325
|
own content was carried correctly and only the control leaked, which is why the commit that added
|
|
@@ -327,7 +332,14 @@
|
|
|
327
332
|
and internal notes (.int-note, hidden above) are the only two things print ever drops. */
|
|
328
333
|
details>*:not(summary){display:block!important}
|
|
329
334
|
details>summary{list-style:none;cursor:default}
|
|
330
|
-
.
|
|
335
|
+
/* `details.searched` is a SECTION TITLE, so it keeps its summary in print like the region rows and
|
|
336
|
+
the secondary groups — a printed report that drops "What was searched" loses the heading over the
|
|
337
|
+
counts, not just a control. What it must not print is the ▸/▾ toggle, which on paper is a
|
|
338
|
+
right-pointing arrow over content that is already fully expanded. Found by the print check, which
|
|
339
|
+
walks a rendered page and refuses any disclosure that is in neither list; the public suite does
|
|
340
|
+
not run it, so the commit that added this fold could ship the half it did not think about. */
|
|
341
|
+
.rrow>summary::before,.rgroup>summary::before,details.scope>summary::before,
|
|
342
|
+
details.searched>summary::before{content:""!important}
|
|
331
343
|
details{border-top:1px solid var(--line)}
|
|
332
344
|
/* …AND THE HERO'S FOLD IS NOT A SECTION. The rule above separates the disclosures that ARE sections
|
|
333
345
|
of the document; `details.sub-more` is one paragraph of the verdict caption folded away from
|
|
@@ -404,3 +416,91 @@
|
|
|
404
416
|
.panel.actions.scope-ran{margin-top:22px}
|
|
405
417
|
.panel.actions.scope-ran h4{margin:0 0 6px;font-size:14px;letter-spacing:-.01em;
|
|
406
418
|
display:flex;align-items:center;gap:9px}
|
|
419
|
+
|
|
420
|
+
/* ── The 2026-09-16 report redesign — the approved report presentation ─────────────────────────────────────────
|
|
421
|
+
Ported from the design's own stylesheet, with its literal colours mapped onto this sheet's tokens
|
|
422
|
+
so the page follows the theme rather than carrying a second palette. Layout only: every string on
|
|
423
|
+
the page is the delivered file's own, a label from the specification, or a count. */
|
|
424
|
+
.panel.about{margin:10px 0 18px;padding:12px 18px;max-width:900px}
|
|
425
|
+
.panel.about .label{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:6px}
|
|
426
|
+
.panel.about .row{display:grid;grid-template-columns:170px 1fr;gap:12px;padding:5px 0;border-top:1px solid var(--line);font-size:13.5px;line-height:1.5}
|
|
427
|
+
/* The goods as registered and the record's own dates, inside a finding's Full detail fold on a full
|
|
428
|
+
country search (the 2026-09-16 report redesign). Ported from the design with its fixed greys replaced by tokens.
|
|
429
|
+
`word-break` is load-bearing and not cosmetic: the goods are the register's own wording, which on a
|
|
430
|
+
Japanese or Korean record is one unbroken run of characters with no spaces to wrap at, and without
|
|
431
|
+
it that row sets the width of the whole fold. */
|
|
432
|
+
.record{margin:6px 0 2px}
|
|
433
|
+
.record .row{display:grid;grid-template-columns:150px 1fr;gap:12px;padding:4px 0;border-top:1px solid var(--line);font-size:13px;line-height:1.5}
|
|
434
|
+
.record .k{font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
435
|
+
.record .v{overflow-wrap:anywhere;word-break:break-word}
|
|
436
|
+
@media(max-width:700px){.record .row{grid-template-columns:1fr;gap:2px}}
|
|
437
|
+
.panel.about .row:first-of-type{border-top:0}
|
|
438
|
+
.panel.about .k{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
439
|
+
.panel.about .v{color:var(--ink)}
|
|
440
|
+
header.hero p.sub .fmark{font-weight:800;color:var(--ink)}
|
|
441
|
+
|
|
442
|
+
.heroGrid{display:block}.panel.gauge{max-width:none}
|
|
443
|
+
.gconc .grow{display:grid;grid-template-columns:150px 1fr;gap:14px;align-items:start}
|
|
444
|
+
.gconc .gv{text-align:left;display:block}
|
|
445
|
+
.gconds-wrap{display:grid;grid-template-columns:150px 1fr;gap:14px;align-items:start;margin-top:10px}
|
|
446
|
+
.gconds-wrap .gk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
447
|
+
.gconds{margin:0;padding-left:18px;font-size:13px;line-height:1.45;font-weight:500}
|
|
448
|
+
.gconds li{margin:3px 0}
|
|
449
|
+
.gwhy{border-top:1px solid var(--line);margin:12px 0 0;padding:12px 0 0}
|
|
450
|
+
.gwhy .label{font-size:10.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:8px}
|
|
451
|
+
.gwhy .fa-row{display:grid;grid-template-columns:150px 1fr;gap:14px;align-items:start;padding:7px 0;font-size:13.5px;line-height:1.5}
|
|
452
|
+
.gwhy .fa-k{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
453
|
+
.fa-token{padding:2px 8px;border-radius:999px;color:#fff;font-weight:700}
|
|
454
|
+
.fa-token.tone-good{background:var(--clear)}.fa-token.tone-mid{background:var(--med)}.fa-token.tone-bad{background:var(--high)}
|
|
455
|
+
.fa-based{margin:4px 0 2px;font-size:12.5px;line-height:1.45;color:var(--slate)}
|
|
456
|
+
.fa-bk{font-size:10px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-right:6px}
|
|
457
|
+
.gexp,.gconf{display:grid;grid-template-columns:150px 1fr;gap:14px;border-top:1px solid var(--line);margin-top:12px;padding-top:10px;font-size:13px;line-height:1.5}
|
|
458
|
+
.gexp .gk,.gconf .gk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
459
|
+
.gexp .gv,.gconf .gv{text-align:left}
|
|
460
|
+
.gframe{margin-top:10px;font-size:11.5px;color:var(--faint)}
|
|
461
|
+
|
|
462
|
+
.panel.where{padding:10px 18px}
|
|
463
|
+
.wrow{display:grid;grid-template-columns:44px 1fr auto;gap:14px;align-items:center;padding:8px 0;border-bottom:1px solid var(--line);font-size:13.5px}
|
|
464
|
+
.wrow .wname{font-weight:600}.wrow .rcode{font-weight:700;color:var(--faint);font-size:12px}
|
|
465
|
+
.wclean{display:flex;flex-wrap:wrap;gap:6px;align-items:center;padding:10px 0 4px}
|
|
466
|
+
.wk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--clear);margin-right:8px}
|
|
467
|
+
.wchip{padding:3px 9px;border-radius:999px;background:var(--clear-soft);color:var(--clear-tx);font:600 12px/1.4 var(--font)}
|
|
468
|
+
.panel.courtp{padding:14px 18px}.panel.courtp p{margin:0;font-size:14px;line-height:1.5}
|
|
469
|
+
|
|
470
|
+
.ask-fi{margin-left:auto;font:600 12px/1 var(--font);padding:7px 10px;border-radius:999px;border:1px solid var(--line2);background:transparent;color:var(--ink);cursor:pointer;white-space:nowrap}
|
|
471
|
+
.tb-ask{margin-right:8px}
|
|
472
|
+
.cardhead .who.mark-first{font-size:17px;font-weight:800}
|
|
473
|
+
.cardhead .who .where{font-weight:500;color:var(--slate);font-size:14px}
|
|
474
|
+
.owner-line{padding:0 16px 6px 16px;margin-top:-4px;font-size:13px;color:var(--slate)}
|
|
475
|
+
.owner-line .ok{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-right:6px}
|
|
476
|
+
|
|
477
|
+
.panel.alsocons{padding:12px 18px}
|
|
478
|
+
.card.ruled .rail{background:var(--tan)}.tier.ruled{background:var(--bg2);color:var(--slate)}
|
|
479
|
+
.ruled-cards .card{margin-bottom:10px}
|
|
480
|
+
.card.ruled .roff{margin:6px 16px 12px;font-size:13px}
|
|
481
|
+
.card.ruled .roff summary{cursor:pointer;color:var(--slate);font-size:12px;letter-spacing:.06em;text-transform:uppercase}
|
|
482
|
+
.card.ruled .roff p{margin:6px 0;line-height:1.5}
|
|
483
|
+
.cgroups{margin:8px 0 0}
|
|
484
|
+
.cgroup{margin:6px 0;background:var(--card);border-radius:12px;box-shadow:0 1px 2px rgba(37,9,2,.06)}
|
|
485
|
+
.cgroup>summary{display:flex;align-items:center;gap:10px;padding:12px 18px;cursor:pointer;font:600 14px/1.3 var(--font);list-style:none}
|
|
486
|
+
.cgroup>summary::before{content:'▸';color:var(--faint);font-size:12px}
|
|
487
|
+
.cgroup[open]>summary::before{content:'▾'}
|
|
488
|
+
.cgroup .gcount{margin-left:auto;color:var(--slate);font-weight:500;font-size:12.5px}
|
|
489
|
+
.cgroup .gbody{padding:0 18px 12px}
|
|
490
|
+
.crow{display:grid;grid-template-columns:200px 1fr;gap:12px;align-items:start;padding:8px 0;border-top:1px solid var(--line);font-size:13px}
|
|
491
|
+
.crow .cm-mark{font-weight:700}.crow .cwho{color:var(--slate);line-height:1.45}
|
|
492
|
+
.crow .curl{color:var(--crimson);text-decoration:none;font-size:12.5px;word-break:break-all}
|
|
493
|
+
.cmore{padding:10px 0 2px;font-size:12.5px}.wb{color:var(--crimson);text-decoration:none;font-weight:600}
|
|
494
|
+
|
|
495
|
+
details.searched{margin:14px 0;background:var(--card);border-radius:12px;box-shadow:var(--shadow)}
|
|
496
|
+
details.searched>summary{display:flex;align-items:center;gap:10px;padding:12px 18px;cursor:pointer;font:600 14px/1.3 var(--font);list-style:none}
|
|
497
|
+
details.searched>summary::before{content:'▸';color:var(--faint);font-size:12px}
|
|
498
|
+
details.searched[open]>summary::before{content:'▾'}
|
|
499
|
+
details.searched .gcount{margin-left:auto;color:var(--slate);font-weight:500;font-size:12.5px}
|
|
500
|
+
details.searched .gbody{padding:0 18px 12px}
|
|
501
|
+
details.searched .row{display:grid;grid-template-columns:200px 1fr;gap:12px;padding:7px 0;border-top:1px solid var(--line);font-size:13.5px}
|
|
502
|
+
details.searched .k{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);padding-top:3px}
|
|
503
|
+
.openrows,.provwrap{margin-top:12px;border-top:1px solid var(--line);padding-top:10px}
|
|
504
|
+
.openrows .rk,.provwrap .rk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-bottom:6px}
|
|
505
|
+
.provnote{margin:0;font-size:12.5px;line-height:1.5;color:var(--slate)}
|
|
506
|
+
.topbar .tb-lockup{margin-right:10px;flex:0 0 auto}
|
|
@@ -478,6 +478,34 @@ export const REC = {
|
|
|
478
478
|
// designated countries; never imply "international = global". Corsearch bodies carry the
|
|
479
479
|
// per-designated-country statuses as `onomaticsJurisdictionsStatuses` (array of strings or of
|
|
480
480
|
// {jurisdiction,status} objects — fail-open on either shape; null when the record lacks it).
|
|
481
|
+
// The 2026-09-16 report redesign — the goods the mark is REGISTERED for, in the register's own words. A finding
|
|
482
|
+
// argues about goods, and until now the only goods on the page were the class numbers; the record
|
|
483
|
+
// carried the wording all along. NOTHING IS TRANSLATED HERE. Where a record holds an English entry
|
|
484
|
+
// the display picks it (the register wrote it); where it holds only the local language, the local
|
|
485
|
+
// language is what "as registered" means, and a translation composed at render time would be this
|
|
486
|
+
// renderer asserting the scope of somebody's right in words no register ever granted.
|
|
487
|
+
goods: (r) => {
|
|
488
|
+
const gs = Array.isArray(r.goodsServices) ? r.goodsServices : [];
|
|
489
|
+
const rows = gs.map((g) => ({
|
|
490
|
+
classes: (Array.isArray(g?.classes) ? g.classes : []).map(Number).filter((x) => !Number.isNaN(x)),
|
|
491
|
+
language: String(g?.language ?? "").trim().toLowerCase(),
|
|
492
|
+
description: String(g?.description ?? "").trim(),
|
|
493
|
+
})).filter((g) => g.description);
|
|
494
|
+
if (!rows.length) return null;
|
|
495
|
+
const en = rows.filter((g) => g.language === "en" || g.language === "eng");
|
|
496
|
+
return (en.length ? en : rows).map((g) => ({ classes: g.classes, description: g.description }));
|
|
497
|
+
},
|
|
498
|
+
// The three dates a reader acts on, as the record holds them and never re-derived. `filingYear` and
|
|
499
|
+
// `regYear` above answer a different question — the one-line summary — and a year cannot say whether
|
|
500
|
+
// a registration lapses this month.
|
|
501
|
+
dates: (r) => {
|
|
502
|
+
const out = [];
|
|
503
|
+
const add = (label, v) => { const s = String(v ?? "").trim(); if (/^\d{4}-\d{2}-\d{2}/.test(s)) out.push([label, s.slice(0, 10)]); };
|
|
504
|
+
add("filed", r.applicationDate);
|
|
505
|
+
add("registered", r.registrationDate);
|
|
506
|
+
add("expires", r.expiryDate ?? r.renewalDate);
|
|
507
|
+
return out.length ? out : null;
|
|
508
|
+
},
|
|
481
509
|
designations: (r) => {
|
|
482
510
|
const j = r.onomaticsJurisdictionsStatuses ?? r.jurisdictions ?? null;
|
|
483
511
|
if (!Array.isArray(j) || !j.length) return null;
|
package/driver/stages.mjs
CHANGED
|
@@ -1920,7 +1920,7 @@ export const STAGES = {
|
|
|
1920
1920
|
},
|
|
1921
1921
|
"Findings section prose (both branches — on seat m, every loaded reading as its own finding with its receipt)": {
|
|
1922
1922
|
class: "judgment", tokens: ["missing", "too_short", "declared_unavailable"],
|
|
1923
|
-
why: "#850 rules the prose J. The findings-heading arm differs per seat: verify.mjs
|
|
1923
|
+
why: "#850 rules the prose J. The findings-heading arm differs per seat: `commonLawStructural()` in verify.mjs for a/b, `commonLawMeaningSeat()` for m (which also accepts meaning/connotation)",
|
|
1924
1924
|
},
|
|
1925
1925
|
"Negative-results matrix — one receipt-carrying row per (variant x platform) cell (seats a/b only)": {
|
|
1926
1926
|
class: "judgment", tokens: ["missing"],
|
|
@@ -2071,7 +2071,7 @@ export const STAGES = {
|
|
|
2071
2071
|
contractElements: {
|
|
2072
2072
|
"execute the frozen plan — ONE register_execute_plan call with {plan_path, axis, output_path}": {
|
|
2073
2073
|
class: "mechanical:tool-written", tokens: ["named_band_missing", "tool_timeout"],
|
|
2074
|
-
why: "All three args are driver values interpolated into the message; the tool writes every band block. #850 calls this already right. Note #793: `named_band_missing` and `tool_timeout` are one evidence state with two causes, and registerPlanCallKilled (verify.mjs:
|
|
2074
|
+
why: "All three args are driver values interpolated into the message; the tool writes every band block. #850 calls this already right. Note #793: `named_band_missing` and `tool_timeout` are one evidence state with two causes, and registerPlanCallKilled (verify.mjs:1291) separates them from the call log, not from the model. [citation unverified]",
|
|
2075
2075
|
},
|
|
2076
2076
|
"the dictated entry list — qid, predicate, terms, owner, nice_classes, regions, when-guard, expected_kind, covered_by": {
|
|
2077
2077
|
class: "mechanical:pre-bound", tokens: [],
|
|
@@ -2123,7 +2123,7 @@ export const STAGES = {
|
|
|
2123
2123
|
},
|
|
2124
2124
|
"layer-execution declaration — whether the prose says the register layer / provider tools were not executed or not bound": {
|
|
2125
2125
|
class: "mechanical:code-extracted", tokens: ["declared_not_executed"],
|
|
2126
|
-
why: "_driver/plan-execution.json and the tool-call log already hold whether the call ran — registerPlanCallKilled (verify.mjs:
|
|
2126
|
+
why: "_driver/plan-execution.json and the tool-call log already hold whether the call ran — registerPlanCallKilled (verify.mjs:1291) reads exactly that to settle the same question one arm below. This arm still decides it from the model's sentence. [citation unverified]",
|
|
2127
2127
|
},
|
|
2128
2128
|
"`CROSS-CHECK REQUIRED: <what> — <why>` — the check that is needed and why": {
|
|
2129
2129
|
class: "judgment", tokens: [],
|
package/driver/suite-census.json
CHANGED
|
@@ -573,6 +573,12 @@
|
|
|
573
573
|
"skips": 0,
|
|
574
574
|
"todos": 0
|
|
575
575
|
},
|
|
576
|
+
"a-query-is-not-crossed-with-its-own-variant.test.mjs": {
|
|
577
|
+
"tests": 4,
|
|
578
|
+
"asserts": 6,
|
|
579
|
+
"skips": 0,
|
|
580
|
+
"todos": 0
|
|
581
|
+
},
|
|
576
582
|
"a-queued-job-is-visible-before-a-worker-claims-it.test.mjs": {
|
|
577
583
|
"tests": 6,
|
|
578
584
|
"asserts": 16,
|
|
@@ -669,6 +675,12 @@
|
|
|
669
675
|
"skips": 0,
|
|
670
676
|
"todos": 0
|
|
671
677
|
},
|
|
678
|
+
"a-refused-query-says-whether-the-provider-declined-it.test.mjs": {
|
|
679
|
+
"tests": 3,
|
|
680
|
+
"asserts": 11,
|
|
681
|
+
"skips": 0,
|
|
682
|
+
"todos": 0
|
|
683
|
+
},
|
|
672
684
|
"a-refused-unit-says-what-is-half-started.test.mjs": {
|
|
673
685
|
"tests": 9,
|
|
674
686
|
"asserts": 31,
|
|
@@ -1246,8 +1258,8 @@
|
|
|
1246
1258
|
"todos": 0
|
|
1247
1259
|
},
|
|
1248
1260
|
"browser-check-membership.test.mjs": {
|
|
1249
|
-
"tests":
|
|
1250
|
-
"asserts":
|
|
1261
|
+
"tests": 11,
|
|
1262
|
+
"asserts": 26,
|
|
1251
1263
|
"skips": 8,
|
|
1252
1264
|
"todos": 0
|
|
1253
1265
|
},
|
|
@@ -1912,8 +1924,8 @@
|
|
|
1912
1924
|
"todos": 0
|
|
1913
1925
|
},
|
|
1914
1926
|
"doctor-reads-the-units-not-the-shell.test.mjs": {
|
|
1915
|
-
"tests":
|
|
1916
|
-
"asserts":
|
|
1927
|
+
"tests": 40,
|
|
1928
|
+
"asserts": 115,
|
|
1917
1929
|
"skips": 1,
|
|
1918
1930
|
"todos": 0
|
|
1919
1931
|
},
|
|
@@ -2669,7 +2681,7 @@
|
|
|
2669
2681
|
},
|
|
2670
2682
|
"knockout-structured-read.test.mjs": {
|
|
2671
2683
|
"tests": 11,
|
|
2672
|
-
"asserts":
|
|
2684
|
+
"asserts": 55,
|
|
2673
2685
|
"skips": 0,
|
|
2674
2686
|
"todos": 0
|
|
2675
2687
|
},
|
|
@@ -3059,7 +3071,7 @@
|
|
|
3059
3071
|
},
|
|
3060
3072
|
"pipeline.mock.test.mjs": {
|
|
3061
3073
|
"tests": 94,
|
|
3062
|
-
"asserts":
|
|
3074
|
+
"asserts": 795,
|
|
3063
3075
|
"skips": 0,
|
|
3064
3076
|
"todos": 0
|
|
3065
3077
|
},
|
|
@@ -3718,8 +3730,8 @@
|
|
|
3718
3730
|
"todos": 0
|
|
3719
3731
|
},
|
|
3720
3732
|
"render.test.mjs": {
|
|
3721
|
-
"tests":
|
|
3722
|
-
"asserts":
|
|
3733
|
+
"tests": 144,
|
|
3734
|
+
"asserts": 572,
|
|
3723
3735
|
"skips": 0,
|
|
3724
3736
|
"todos": 0
|
|
3725
3737
|
},
|
|
@@ -3802,8 +3814,8 @@
|
|
|
3802
3814
|
"todos": 0
|
|
3803
3815
|
},
|
|
3804
3816
|
"report-record-link-host.test.mjs": {
|
|
3805
|
-
"tests":
|
|
3806
|
-
"asserts":
|
|
3817
|
+
"tests": 4,
|
|
3818
|
+
"asserts": 28,
|
|
3807
3819
|
"skips": 0,
|
|
3808
3820
|
"todos": 0
|
|
3809
3821
|
},
|
|
@@ -4822,8 +4834,8 @@
|
|
|
4822
4834
|
"todos": 0
|
|
4823
4835
|
},
|
|
4824
4836
|
"the-knockout-page-leads-with-the-read.test.mjs": {
|
|
4825
|
-
"tests":
|
|
4826
|
-
"asserts":
|
|
4837
|
+
"tests": 30,
|
|
4838
|
+
"asserts": 81,
|
|
4827
4839
|
"skips": 0,
|
|
4828
4840
|
"todos": 0
|
|
4829
4841
|
},
|
|
@@ -5033,7 +5045,7 @@
|
|
|
5033
5045
|
},
|
|
5034
5046
|
"the-report-carries-what-the-assessment-wrote.test.mjs": {
|
|
5035
5047
|
"tests": 10,
|
|
5036
|
-
"asserts":
|
|
5048
|
+
"asserts": 39,
|
|
5037
5049
|
"skips": 0,
|
|
5038
5050
|
"todos": 0
|
|
5039
5051
|
},
|
|
@@ -5189,13 +5201,13 @@
|
|
|
5189
5201
|
},
|
|
5190
5202
|
"the-writing-standard-backlog-is-a-floor.test.mjs": {
|
|
5191
5203
|
"tests": 3,
|
|
5192
|
-
"asserts":
|
|
5204
|
+
"asserts": 9,
|
|
5193
5205
|
"skips": 2,
|
|
5194
5206
|
"todos": 0
|
|
5195
5207
|
},
|
|
5196
5208
|
"the-writing-standard-check-refuses-five-classes.test.mjs": {
|
|
5197
5209
|
"tests": 15,
|
|
5198
|
-
"asserts":
|
|
5210
|
+
"asserts": 61,
|
|
5199
5211
|
"skips": 0,
|
|
5200
5212
|
"todos": 0
|
|
5201
5213
|
},
|
|
@@ -5721,8 +5733,8 @@
|
|
|
5721
5733
|
"todos": 0
|
|
5722
5734
|
},
|
|
5723
5735
|
"http-handler.test.mjs": {
|
|
5724
|
-
"tests":
|
|
5725
|
-
"asserts":
|
|
5736
|
+
"tests": 17,
|
|
5737
|
+
"asserts": 40,
|
|
5726
5738
|
"skips": 0,
|
|
5727
5739
|
"todos": 0
|
|
5728
5740
|
},
|
|
@@ -5859,8 +5871,8 @@
|
|
|
5859
5871
|
"todos": 0
|
|
5860
5872
|
},
|
|
5861
5873
|
"the-portal-can-tell-who-has-connected-an-assistant.test.mjs": {
|
|
5862
|
-
"tests":
|
|
5863
|
-
"asserts":
|
|
5874
|
+
"tests": 12,
|
|
5875
|
+
"asserts": 33,
|
|
5864
5876
|
"skips": 0,
|
|
5865
5877
|
"todos": 0
|
|
5866
5878
|
},
|
package/driver/verify.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
11
11
|
import { dirname, join, basename } from "node:path";
|
|
12
12
|
import { driverDir } from "../shared/driver-dir.mjs"; //
|
|
13
|
-
import { findReceiptViolations, findGridLedgerViolations, findPlatformIdentityViolations, parsePrRiskQueries, MEANING_SEAT } from "./common-law-receipts.mjs";
|
|
13
|
+
import { findReceiptViolations, findGridLedgerViolations, findPlatformIdentityViolations, parsePrRiskQueries, MEANING_SEAT, erroredConnotationQueriesAmong } from "./common-law-receipts.mjs";
|
|
14
14
|
// Conversion 2 — the discriminator the two rulings above key on. PURE-ish: one existsSync-shaped read.
|
|
15
15
|
import { matterFrameWasRecorded, frameRatifiedForms } from "./matter-frame-record.mjs";
|
|
16
16
|
import { findConnotationViolations, parsePrRiskResults, MEANING_ANGLES_RE,
|
|
@@ -413,7 +413,33 @@ function commonLawMeaningSeat(p, c) {
|
|
|
413
413
|
// The gate cannot tell the two apart and is not being asked to. There is no identity to join on —
|
|
414
414
|
// that is the entire reason the dictated-versus-recorded comparison exists. So the label states the
|
|
415
415
|
// observation, the remedy carries both cases, and no threshold decides which one a seat is told.
|
|
416
|
+
// ── A QUERY THE PROVIDER REFUSED IS NOT A QUERY NOBODY RAN ────────────────────────────────────
|
|
417
|
+
//
|
|
418
|
+
// The plugin's contract is to append `<query> | connotation | <exception>` to the ledger's gaps and
|
|
419
|
+
// carry on, so a query it threw on says so IN THE FILE THIS GATE JUST READ. The gate did not look:
|
|
420
|
+
// it refused on receipt membership alone, and a query the provider had already refused got the same
|
|
421
|
+
// sentence as one nobody ever issued — the two repairs, "edit the wording" and "run it", neither of
|
|
422
|
+
// which is the remedy when the provider itself declined.
|
|
423
|
+
//
|
|
424
|
+
// It still FAILS, and deliberately: `findErroredConnotationQueries` says so in its own words —
|
|
425
|
+
// laundering an honest error into a clean receipt would be a worse defect than this one. What
|
|
426
|
+
// changes is only what the seat is told, and therefore how many attempts it spends being told the
|
|
427
|
+
// wrong thing. On the clearance that prompted this, the half failed eight attempts across two
|
|
428
|
+
// recovery cycles.
|
|
429
|
+
//
|
|
430
|
+
// The missing list is THIS gate's own, joined per half on `queryKey`; the helper joins gap rows on
|
|
431
|
+
// `norm`, which is the one author of what a gap row names. Two populations, one key.
|
|
432
|
+
let ledgerParsed = null;
|
|
433
|
+
try { ledgerParsed = JSON.parse(ledgerRaw); } catch { /* unparseable was refused above */ }
|
|
434
|
+
const gapRows = (Array.isArray(ledgerParsed) ? ledgerParsed : [ledgerParsed])
|
|
435
|
+
.flatMap((b) => (Array.isArray(b?.gaps) ? b.gaps : []));
|
|
436
|
+
const reportedError = new Map(
|
|
437
|
+
erroredConnotationQueriesAmong(dropped, { gaps: gapRows }).map((e) => [e.query, e.error]));
|
|
416
438
|
const parts = dropped.slice(0, 3).map((q) => {
|
|
439
|
+
const reported = reportedError.get(q);
|
|
440
|
+
// Named separately because the remedy is different: the search was attempted and the provider
|
|
441
|
+
// declined it, so re-running it unchanged is the one repair that cannot work.
|
|
442
|
+
if (reported) return `${abbrev(q, 40)} [the provider REPORTED an error on this query: ${abbrev(reported, 60)}]`;
|
|
417
443
|
const n = nearest(q);
|
|
418
444
|
return n
|
|
419
445
|
? `${abbrev(q, 40)} [unmatched; nearest recorded: ${abbrev(n, 40)}]`
|
package/mcp-server/CHANGELOG.md
CHANGED
package/mcp-server/lib/audit.mjs
CHANGED
|
@@ -40,14 +40,26 @@ export function summarize(body) {
|
|
|
40
40
|
return out;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function appendAudit({ email, sub, body, status, transport, path = DEFAULT_AUDIT_PATH }) {
|
|
43
|
+
export function appendAudit({ email, sub, body, status, transport, door, path = DEFAULT_AUDIT_PATH }) {
|
|
44
44
|
// `sub` = the inner-token PRINCIPAL (ops-token issuance, INSTALL.md §8) — distinguishes
|
|
45
45
|
// two automations sharing a transport identity. null for internal/user sessions without a sub claim.
|
|
46
46
|
//
|
|
47
47
|
// `transport` is written only where it changes an answer: "stdio" marks a call that arrived over the
|
|
48
48
|
// local route, which has no signed-in identity to record and therefore no email. Omitted on the HTTP
|
|
49
49
|
// doors, whose records are identified by the email they already carry.
|
|
50
|
-
|
|
50
|
+
//
|
|
51
|
+
// `door` NAMES WHICH SURFACE TOOK THE CALL, and it is a separate field from `transport` on purpose.
|
|
52
|
+
// `transport` answers "how did this arrive" and the reader tests it for exactly one value, `"stdio"`,
|
|
53
|
+
// to mean the local route. Two HTTP doors write to this one file — the staff surface and the client
|
|
54
|
+
// surface a customer's own assistant connects through — and "which door" is a different question from
|
|
55
|
+
// "which route". Folding the second into the first would make a client-key call read as a new kind of
|
|
56
|
+
// transport, and any reader that tested `transport` for the local route would keep working while
|
|
57
|
+
// meaning something it was never asked.
|
|
58
|
+
//
|
|
59
|
+
// WRITTEN ONLY WHEN GIVEN, the same rule as `transport` and for the same reason: the existing log
|
|
60
|
+
// shape must not move for records that have no answer to this. A door that does not name itself is a
|
|
61
|
+
// record with no `door` key, not a record claiming to be from nowhere.
|
|
62
|
+
const line = JSON.stringify({ ts: new Date().toISOString(), email: email ?? null, sub: sub ?? null, ...summarize(body), status: status ?? null, ...(transport ? { transport } : {}), ...(door ? { door } : {}) }) + "\n";
|
|
51
63
|
try { mkdirSync(dirname(path), { recursive: true }); appendFileSync(path, line); } catch { /* best-effort */ }
|
|
52
64
|
}
|
|
53
65
|
|
|
@@ -83,6 +83,26 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
83
83
|
if (tokenOnly && devMode) throw new Error("makeHttpHandler: tokenOnly and devMode are mutually exclusive (devMode's synthetic identity would defeat the mandatory key)");
|
|
84
84
|
if (tokenOnly && verify) throw new Error("makeHttpHandler: tokenOnly is for a door with no auth proxy in front — pass verify:null");
|
|
85
85
|
|
|
86
|
+
// ── A TURNED-AWAY CALL IS A RECORD, NOT A GAP ─────────────────────────────────────────────────────
|
|
87
|
+
//
|
|
88
|
+
// Every refusal below used to return without writing anything, so this log held calls that SUCCEEDED
|
|
89
|
+
// and nothing else. An absent line then meant either "never asked" or "asked and was turned away",
|
|
90
|
+
// and nobody reading it back can tell those apart — which is the one question the log exists to
|
|
91
|
+
// answer. Measured on the production access log, 2026-09-16: zero lines in its most recent five
|
|
92
|
+
// thousand carry a refused or error status, which is what never being able to write one looks like.
|
|
93
|
+
//
|
|
94
|
+
// IT INVENTS NO IDENTITY. The key refusals fire BEFORE any identity is established. `user` is in
|
|
95
|
+
// scope there, but only as the placeholder this function starts with, and writing that would name a
|
|
96
|
+
// caller who does not exist. Those lines carry no email and no principal, and say a call was refused
|
|
97
|
+
// at this door at this time — which is true, and is the shape the local route already uses for the
|
|
98
|
+
// same reason: a synthesized identity would match somebody who did nothing.
|
|
99
|
+
const door = clientSurface ? "client" : undefined;
|
|
100
|
+
const refuse = (res, status, obj, who = {}) => {
|
|
101
|
+
try { appendAudit({ email: who.email ?? null, sub: who.sub ?? null, body: who.body ?? null, status: "refused", door }); }
|
|
102
|
+
catch { /* best-effort: a write failure must never change what the caller is told */ }
|
|
103
|
+
return send(res, status, obj);
|
|
104
|
+
};
|
|
105
|
+
|
|
86
106
|
/**
|
|
87
107
|
* Has this identity been taken off the guest list since its session was opened? Null when it has not,
|
|
88
108
|
* or when the question does not apply — and the body to send back when it has.
|
|
@@ -182,7 +202,7 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
182
202
|
// reach it — the protection is the filesystem — so it is disclosure rather than exposure,
|
|
183
203
|
// and it buys nothing: an operator needs to know a local socket is where to look, and
|
|
184
204
|
// already has the path in the boot line and the unit file. A stranger gets nothing usable.
|
|
185
|
-
return
|
|
205
|
+
return refuse(res, 401, { error: "this listener takes an auth-proxy JWT and never an access key — a key has no door here"
|
|
186
206
|
+ (keyDoorPath ? ". A key is taken on this deployment's local socket; the engine's boot line names it" : ". This deployment has no key door configured") });
|
|
187
207
|
}
|
|
188
208
|
log(`auth reject ${status}: ${e.message}`);
|
|
@@ -197,7 +217,7 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
197
217
|
const tok = readInnerToken(url, req.headers, { allowAuthorization: true });
|
|
198
218
|
if (!tok) {
|
|
199
219
|
log("auth reject 401: no key presented on the token-only door");
|
|
200
|
-
return
|
|
220
|
+
return refuse(res, 401, { error: "this address needs an access key — put it in your assistant's API-key field, or add ?token=<key> to the URL" });
|
|
201
221
|
}
|
|
202
222
|
let t;
|
|
203
223
|
try { t = verifyToken(tok); }
|
|
@@ -209,26 +229,26 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
209
229
|
// the original defect survived, silently, on every default install.
|
|
210
230
|
if (e.code === "REVOCATION_UNCHECKABLE") {
|
|
211
231
|
log(`DOOR FAULT — refusing every key: ${e.message} Create it, or point TRADEMARK_MCP_TOKEN_DENYLIST at the list this install actually uses; \`clearotron doctor\` reports the state.`);
|
|
212
|
-
return
|
|
232
|
+
return refuse(res, 401, { error: `this install cannot check whether keys have been revoked, so it is refusing all of them: ${e.message}` });
|
|
213
233
|
}
|
|
214
234
|
log(`auth reject 401: ${e.message}`);
|
|
215
|
-
return
|
|
235
|
+
return refuse(res, 401, { error: `invalid access key: ${e.message}` });
|
|
216
236
|
}
|
|
217
237
|
user = { email: t.sub || t.runId || t.jti || "unnamed-key" };
|
|
218
238
|
}
|
|
219
|
-
if (!limiter.take(user.email)) return
|
|
239
|
+
if (!limiter.take(user.email)) return refuse(res, 429, { error: "rate limit exceeded — retry shortly" }, { email: user.email });
|
|
220
240
|
|
|
221
241
|
if (req.method === "POST") {
|
|
222
242
|
let body;
|
|
223
243
|
try { body = await readJsonBody(req, maxBody); }
|
|
224
|
-
catch (e) { return
|
|
244
|
+
catch (e) { return refuse(res, 400, { error: `bad request body: ${e.message}` }, { email: user.email }); }
|
|
225
245
|
|
|
226
246
|
const sid = hdr(req.headers["mcp-session-id"]);
|
|
227
247
|
let entry = sid ? sessions.get(sid) : null;
|
|
228
248
|
let stampScope = null;
|
|
229
249
|
if (!entry) {
|
|
230
|
-
if (sid) return
|
|
231
|
-
if (!isInitializeRequest(body)) return
|
|
250
|
+
if (sid) return refuse(res, 404, { error: "unknown or expired session" }, { email: user.email, body });
|
|
251
|
+
if (!isInitializeRequest(body)) return refuse(res, 400, { error: "no session — the first request must be an MCP initialize" }, { email: user.email, body });
|
|
232
252
|
if (sessions.size >= sessionMax) evictOldest(sessions);
|
|
233
253
|
// INNER authz token (rides the /mcp?token= query or the X-Trademark-Token header) → the session's
|
|
234
254
|
// scope: ops (full), run-bound user (read-only one run), or — no token but firm staff — internal
|
|
@@ -274,20 +294,39 @@ export function makeHttpHandler({ verify, limiter, opsLimiter = null, sessions,
|
|
|
274
294
|
} else {
|
|
275
295
|
if (entry.email && entry.email !== user.email) {
|
|
276
296
|
log(`session owner mismatch: ${user.email} presented a session created by another identity`);
|
|
277
|
-
return
|
|
297
|
+
return refuse(res, 403, { error: "session belongs to another identity" }, { email: user.email, sub: entry.sub ?? null, body });
|
|
278
298
|
}
|
|
279
299
|
const gone = revokedMidSession(entry);
|
|
280
|
-
if (gone) return
|
|
300
|
+
if (gone) return refuse(res, 403, gone, { email: user.email, sub: entry.sub ?? null, body });
|
|
281
301
|
entry.lastSeen = Date.now();
|
|
282
302
|
}
|
|
283
303
|
// OPS-TOKENS item 6 — automation principals get their own (lower) bucket, keyed by the token's
|
|
284
304
|
// sub, ON TOP of the transport-identity limit above: a runaway connector throttles itself, not
|
|
285
305
|
// the interactive staff sharing the proxy identity.
|
|
286
306
|
if (opsLimiter && entry.kind === "ops" && !opsLimiter.take(`ops:${entry.sub ?? "unnamed"}`))
|
|
287
|
-
return
|
|
307
|
+
return refuse(res, 429, { error: "ops principal rate limit exceeded — retry shortly" }, { email: user.email, sub: entry.sub ?? null, body });
|
|
288
308
|
// Audit AFTER scope resolution so the line names the PRINCIPAL (token sub), not just the
|
|
289
|
-
// transport identity
|
|
290
|
-
|
|
309
|
+
// transport identity. Best-effort, never blocks.
|
|
310
|
+
//
|
|
311
|
+
// ── ONE LINE, WRITTEN WHEN THE OUTCOME IS KNOWN ────────────────────────────────────────────
|
|
312
|
+
//
|
|
313
|
+
// This used to be written here, before dispatch, and therefore with `status: null` — a record
|
|
314
|
+
// that a call was made and no record of what happened to it. On a deployed log every one of the
|
|
315
|
+
// five thousand most recent lines carries a null status, so "did this key read that report" was
|
|
316
|
+
// answerable only as "it asked".
|
|
317
|
+
//
|
|
318
|
+
// THE TRADE MADE, SAID PLAINLY: writing before dispatch survives a crash mid-call, and writing
|
|
319
|
+
// on `finish` does not. One line carrying the outcome is what a reader needs and what this was
|
|
320
|
+
// asked for, and a crash that loses the line also loses the response, so the caller is not left
|
|
321
|
+
// believing a lost call succeeded. The refusals above are recorded at their own sites and do not
|
|
322
|
+
// depend on this hook at all, which is the case a review actually asks about.
|
|
323
|
+
//
|
|
324
|
+
// `finish` fires once the response is fully sent, which is when `res.statusCode` is the answer
|
|
325
|
+
// the caller got rather than the default it started as.
|
|
326
|
+
res.once("finish", () => {
|
|
327
|
+
try { appendAudit({ email: user.email, sub: entry.sub ?? null, body, status: res.statusCode, door }); }
|
|
328
|
+
catch { /* best-effort */ }
|
|
329
|
+
});
|
|
291
330
|
const answered = entry.transport.handleRequest(req, res, body);
|
|
292
331
|
if (stampScope) { try { await answered; } finally { stampScope(); } }
|
|
293
332
|
return answered;
|
package/mcp-server/lib/scrub.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// because a lawyer defending a filing decision needs the records, and it is bounded by a different
|
|
14
14
|
// mechanism (a structured-field projection, never prose) in lib/evidence.mjs. Read that file's header
|
|
15
15
|
// before widening anything here; the line it draws is evidence vs METHOD, and method stays sealed.
|
|
16
|
-
// publish/render.mjs renders
|
|
16
|
+
// publish/render.mjs no longer renders a Methodology section to the client (the 2026-09-16 report redesign) and
|
|
17
17
|
// deliberately names the register provider (provenance honesty, receipts, enforcement telemetry). A scrubber
|
|
18
18
|
// stricter than the report would delete content the client was already sent, and would quietly make the MCP
|
|
19
19
|
// a different product from the PDF in their inbox.
|
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.6",
|
|
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.",
|
package/package.json
CHANGED
package/portal-ui/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "portal-ui",
|
|
3
3
|
"private": true,
|
|
4
4
|
"type": "module",
|
|
5
|
-
"version": "0.3.2-beta.
|
|
5
|
+
"version": "0.3.2-beta.6",
|
|
6
6
|
"license": "AGPL-3.0-only",
|
|
7
7
|
"description": "The unified trademark portal UI. One address, one login: who you are decides what you see. Built as a static bundle, served by driver/portal-service.mjs — the browser never reaches profile-service or recipe-service.",
|
|
8
8
|
"engines": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trademark-oauth-mcp-bridge",
|
|
3
|
-
"version": "0.3.2-beta.
|
|
3
|
+
"version": "0.3.2-beta.6",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"private": true,
|
|
6
6
|
"description": "OAuth 2.1 MCP stdio bridge used by the engine's case-law gather stage (courtlistener / legaldatahunter).",
|
package/shared/driver-dir.mjs
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
// its own population needs an exception list — which would rebuild this issue's defect inside its fix:
|
|
36
36
|
//
|
|
37
37
|
// · `driver/stage-freshness.mjs` creates a CHILD, `join(runDir, "_driver", STAMP_DIR)`.
|
|
38
|
-
// · `driver/pipeline.mjs:
|
|
38
|
+
// · `driver/pipeline.mjs:15757 shadowDir` passes a shadow dispatch sandbox under `_experiments/`, not a run
|
|
39
39
|
// directory. It is a run-dir-SHAPED base, which is why the parameter is `base` and not `runDir`.
|
|
40
40
|
//
|
|
41
41
|
// ── WHAT THIS DELIBERATELY DOES NOT DO ────────────────────────────────────────────────────────────
|