clearotron 0.2.1 → 0.3.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALL.md +5 -4
- package/bin/example.mjs +9 -5
- package/bin/onboard.mjs +19 -19
- package/bin/stop.mjs +65 -3
- package/build-info.json +2 -2
- package/driver/CHANGELOG.md +21 -0
- package/driver/declination-call.mjs +32 -0
- package/driver/driver.config.mjs +20 -0
- package/driver/engine/mcp/recording-server.mjs +4 -0
- package/driver/gateway.mjs +8 -3
- package/driver/knockout-assess-record.mjs +5 -1
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +83 -2
- package/driver/predelivery-lint.mjs +22 -4
- package/driver/publish/knockout.mjs +12 -6
- package/driver/publish/render-knockout.mjs +133 -23
- package/driver/publish/report-data.mjs +13 -3
- package/driver/record-carry.mjs +2 -2
- package/driver/reference-score.mjs +1 -1
- package/driver/result-noun-fields.mjs +7 -0
- package/driver/skills/knockout-assess/SKILL.md +10 -4
- package/driver/stages-knockout.mjs +1 -1
- package/driver/stages.mjs +1 -1
- package/driver/suite-census.json +66 -18
- package/driver/unit-inventory.mjs +47 -0
- package/driver/unit-state-verdict.mjs +8 -8
- package/driver/verify-knockout.mjs +9 -1
- package/driver/whatif-memo-run.mjs +45 -4
- package/mcp-server/CHANGELOG.md +2 -0
- package/mcp-server/lib/brief.mjs +15 -0
- package/mcp-server/lib/driver.mjs +6 -0
- package/mcp-server/lib/knockout.mjs +435 -0
- package/mcp-server/lib/scrub.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +69 -4
- package/package.json +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/oauth-mcp-bridge/CHANGELOG.md +2 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/scripts/drain-preflight.mjs +2 -2
- package/scripts/freeze-example-run.mjs +3 -3
- package/scripts/headless-page.mjs +51 -2
- package/scripts/live-surface-check.mjs +86 -17
- package/scripts/render-check.mjs +61 -2
- package/scripts/deploy-test.sh +0 -309
|
@@ -2454,6 +2454,13 @@ export function flagLines(failures) {
|
|
|
2454
2454
|
// vendor vocabulary, no ids, no paths, no field names, no instructions to whoever regenerates the run,
|
|
2455
2455
|
// never "certified"/"signed". deliveryVocabViolations() below enforces that mechanically.
|
|
2456
2456
|
const DELIVERY_LINES = {
|
|
2457
|
+
// Keyed on the FULL check id, which deliveryFlagLines resolves before the base — see the note there.
|
|
2458
|
+
// The sentence states the CONSEQUENCE, in the receipt's own terms: not "a check failed" but "the rule
|
|
2459
|
+
// was applied to nothing". Eleven graded runs delivered carrying this, two of them orderable product
|
|
2460
|
+
// demos, and the only place it was written was one row of a fifty-odd-row receipt nobody routes.
|
|
2461
|
+
"narrative-write-ups:could-not-read":
|
|
2462
|
+
"The depth rules for the written-up findings were applied to nothing on this run — the narrative "
|
|
2463
|
+
+ "carries no block this check can read, so neither the ranking cut nor the length cap was verified.",
|
|
2457
2464
|
// template — the summary table against the names actually searched
|
|
2458
2465
|
"names-cell-populated": "The summary table does not name the mark that was searched.",
|
|
2459
2466
|
"assessment-row": "A searched name has no assessment of its own in the summary table.",
|
|
@@ -2555,15 +2562,26 @@ export function deliveryVocabViolations(line) {
|
|
|
2555
2562
|
export function deliveryFlagLines(failures) {
|
|
2556
2563
|
const groups = new Map();
|
|
2557
2564
|
for (const f of failures ?? []) {
|
|
2558
|
-
const
|
|
2565
|
+
const id = String(f?.id ?? "");
|
|
2566
|
+
const base = id.split(":")[0];
|
|
2559
2567
|
const family = String(f?.family ?? "");
|
|
2560
|
-
|
|
2561
|
-
|
|
2568
|
+
// A CHECK WHOSE DISTINCTION LIVES IN ITS SUFFIX COULD NOT BE SAID HERE (tracker issue 267).
|
|
2569
|
+
//
|
|
2570
|
+
// Grouping on `base` alone is right for the common case — a word-cap violation on nine write-ups is
|
|
2571
|
+
// one delivery line, not nine. But it also collapsed `narrative-write-ups:could-not-read` into the
|
|
2572
|
+
// same bucket as an ordinary depth violation, and those are different facts: one says a rule was
|
|
2573
|
+
// broken, the other says the rule was ENFORCED ON NOTHING. The second had no sentence of its own to
|
|
2574
|
+
// reach, so it degraded to the generic "a machine check did not pass" and became invisible.
|
|
2575
|
+
//
|
|
2576
|
+
// So: an entry keyed on the FULL id wins where one exists, and everything else groups by base
|
|
2577
|
+
// exactly as before. This adds no line to any run that did not already produce one.
|
|
2578
|
+
const key = DELIVERY_LINES[id] ? id : (base || `family/${family}`);
|
|
2579
|
+
const g = groups.get(key) ?? { base, family, id: DELIVERY_LINES[id] ? id : null, n: 0 };
|
|
2562
2580
|
g.n += 1;
|
|
2563
2581
|
groups.set(key, g);
|
|
2564
2582
|
}
|
|
2565
2583
|
return [...groups.values()].map((g) => {
|
|
2566
|
-
const sentence = DELIVERY_LINES[g.base] ?? DELIVERY_FAMILY_LINES[g.family] ?? DELIVERY_GENERIC;
|
|
2584
|
+
const sentence = (g.id && DELIVERY_LINES[g.id]) ?? DELIVERY_LINES[g.base] ?? DELIVERY_FAMILY_LINES[g.family] ?? DELIVERY_GENERIC;
|
|
2567
2585
|
// Belt and braces: a table entry that trips the house rules degrades to the generic instead of
|
|
2568
2586
|
// shipping. This is what makes "no engine vocabulary reaches a reader" structural rather than a
|
|
2569
2587
|
// matter of everyone remembering the rule when they add a check.
|
|
@@ -68,9 +68,13 @@ export const knockoutStatement = (framework, marks) =>
|
|
|
68
68
|
// 11pt, no <!DOCTYPE>, no <head>, no <meta charset> — and they rendered a Depth 2 report as a grey
|
|
69
69
|
// spreadsheet with the register hit-counts the customer paid for buried in a table cell. The report now
|
|
70
70
|
// renders through publish/render-knockout.mjs in the product's own design language, off the same shared
|
|
71
|
-
// stylesheet and brand tokens as the clearance report.
|
|
72
|
-
//
|
|
73
|
-
//
|
|
71
|
+
// stylesheet and brand tokens as the clearance report.
|
|
72
|
+
//
|
|
73
|
+
// THE REVIEWER'S NOTES ARE ON THE REPORT SINCE 2026-09-07 (owner ruling, tracker issue 274). This
|
|
74
|
+
// paragraph used to end "internal working material (the purple staff notes, the model's registerEstimate)
|
|
75
|
+
// is not IN the report; it lives in the audit workbook". That is now true of `registerEstimate` only: the
|
|
76
|
+
// notes render on the page, labelled, and the workbook keeps its copy. See render-knockout.mjs's header
|
|
77
|
+
// for the ruling and reviewerNotesBlock for the shape.
|
|
74
78
|
|
|
75
79
|
// ── The workbook: the skill's three sheet names + columns, plus the conditional sheets below ────────
|
|
76
80
|
// ── ONE DRILL-THROUGH KEY: `<MARK> #<ordinal>`, per mark ──────────────────────────────────────
|
|
@@ -250,9 +254,11 @@ export async function buildKnockoutWorkbook(findings, receipts, outPath, registe
|
|
|
250
254
|
// ── The email: a COVER NOTE. Headline band, one line per mark, the report link. Nothing else. ────────
|
|
251
255
|
// The report is the deliverable and the email points at it — the same doctrine the clearance lane
|
|
252
256
|
// follows. The old `delivery.email === 'table'` overlay inlined the full review table into the mail
|
|
253
|
-
// body,
|
|
254
|
-
//
|
|
255
|
-
//
|
|
257
|
+
// body, which put the findings in front of whoever the mail reached rather than whoever opened the
|
|
258
|
+
// report, and made a second rendering of them in a second dialect — a second thing to keep true.
|
|
259
|
+
// (That overlay's other charge, that it shipped an INTERNAL variant, no longer describes anything: the
|
|
260
|
+
// 2026-09-07 ruling put the reviewer's notes on the one report. The reason above is the load-bearing one
|
|
261
|
+
// and it is unaffected — the mail stays a cover note.) Per-lawyer client formatting is drafted by the assistant from the
|
|
256
262
|
// run's report-data.json, not by a template knob in here.
|
|
257
263
|
// `reports` is publishKnockout's own list — `[{mark, url}]`, one per published document — and
|
|
258
264
|
// `auditUrl` is composed by the publisher from the pool URL it already holds. Neither is derived here.
|
|
@@ -21,10 +21,20 @@
|
|
|
21
21
|
// ~60 lines of duplicated document shell, and it is the right price: the alternative is exporting the
|
|
22
22
|
// shell out of the frozen file, which is a bigger break than the duplication saves.
|
|
23
23
|
//
|
|
24
|
-
// ONE report. There is no client variant and no internal variant
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
24
|
+
// ONE report. There is no client variant and no internal variant, and that is
|
|
25
|
+
// still the rule — it is what removes the whole class of "the wrong link got sent".
|
|
26
|
+
//
|
|
27
|
+
// WHAT CHANGED, 2026-09-07 (owner ruling, tracker issue 274): the one report now CARRIES the reviewer's
|
|
28
|
+
// notes rather than routing them to the audit workbook alone. The earlier reading of "one report" was
|
|
29
|
+
// that internal working material is simply not part of it; the ruling is that there is one report and the
|
|
30
|
+
// person who ran Clearotron reads it, so holding material back "just adds confusion for where data is
|
|
31
|
+
// lost". Six notes written for the reviewing lawyer on a delivered run reached the spreadsheet and
|
|
32
|
+
// nothing else, which is the measurement behind the ruling.
|
|
33
|
+
//
|
|
34
|
+
// So: the notes render, LABELLED, in report.css's existing `.internal` purple convention (see
|
|
35
|
+
// reviewerNotesBlock) — never merged into the client-voiced body. The workbook keeps them as well; this
|
|
36
|
+
// added a surface and moved none. The model's `registerEstimate` is NOT covered by the ruling and stays
|
|
37
|
+
// off the page.
|
|
28
38
|
import { readFileSync } from 'node:fs';
|
|
29
39
|
import { join, dirname } from 'node:path';
|
|
30
40
|
import { fileURLToPath } from 'node:url';
|
|
@@ -176,6 +186,14 @@ const KO_CSS = `
|
|
|
176
186
|
.ko-findev{margin:0;font-size:12.5px;line-height:1.6;word-break:break-word}
|
|
177
187
|
.ko-count{font-family:var(--mono);font-size:13px;color:var(--ink);margin:0}
|
|
178
188
|
.ko-degraded{font-size:13px;color:var(--med-tx,#82550A);margin:8px 0 0;font-style:italic}
|
|
189
|
+
/* The reviewer-notes legend. NOT .ko-legend — that name is taken by the framework attribution row
|
|
190
|
+
above, and reusing it would restyle the caption. It names the purple convention report.css already
|
|
191
|
+
draws for .internal, so the colour is stated once and never re-specified. */
|
|
192
|
+
.ko-refnote{margin:0;padding:11px 24px 0;font-size:12px;color:#6a2b6e;font-style:italic}
|
|
193
|
+
/* The notes sit inside a mark's column, so the shared .internal block's bullets keep the column's
|
|
194
|
+
own list indent and do not fight the paragraph above them. */
|
|
195
|
+
.ko-row .internal .ko-bul{margin:0 0 4px}
|
|
196
|
+
.ko-row .internal .ko-bul:last-child{margin-bottom:0}
|
|
179
197
|
.ko-counts table{width:100%;border-collapse:collapse;font-size:14px}
|
|
180
198
|
.ko-counts th{text-align:left;font-size:10.5px;letter-spacing:.06em;text-transform:uppercase;color:var(--faint);
|
|
181
199
|
font-weight:700;padding:14px 24px 8px;border-bottom:1px solid var(--line)}
|
|
@@ -728,9 +746,13 @@ function registerCardViews(mark, framework, registerRecords) {
|
|
|
728
746
|
* is gone; what tells the two apart on the page is where the finding came from, which is the vocabulary
|
|
729
747
|
* the clearance reports already use for exactly this.
|
|
730
748
|
*
|
|
731
|
-
*
|
|
732
|
-
* here
|
|
733
|
-
*
|
|
749
|
+
* THE BAND CHIP IS DRAWN ONLY WHEN THE RATER SENT ONE, and that condition is the whole rule. The old note
|
|
750
|
+
* here said "no band chip and no `bandStop`, ever: a rung of the run's ladder would be a rating nobody
|
|
751
|
+
* performed" — which was true for as long as nothing could carry the rater's rating of an individual
|
|
752
|
+
* filing. `registerReads[].band` now can (tracker issue 274), so the objection is answered where it was
|
|
753
|
+
* raised: with a band, the rating WAS performed, by the seat, in the framework's own words, and the chip
|
|
754
|
+
* states it. With no band the old rule stands unchanged — --faint rail, no chip, nothing claimed — and
|
|
755
|
+
* that is still what every archived run renders.
|
|
734
756
|
*
|
|
735
757
|
* THE LINE UNDER THE STATEMENT DESCRIBES THE CARD, NOT THE RATING. It used to deny
|
|
736
758
|
* that the rating turned on this filing, which this function has no way to know and which a delivered
|
|
@@ -759,24 +781,56 @@ function sourceChips(v) {
|
|
|
759
781
|
return `${commonLaw ? '<span class="src cl">Common law</span>' : ''}${register ? '<span class="src reg">Register</span>' : ''}`;
|
|
760
782
|
}
|
|
761
783
|
|
|
762
|
-
|
|
784
|
+
// ── THE REVIEWER'S NOTES, ON THE REPORT ─────────────────────────────────────────────────
|
|
785
|
+
//
|
|
786
|
+
// OWNER RULING, 2026-09-07: there is one report and the person who ran Clearotron reads it — keeping
|
|
787
|
+
// this material off the page "just adds confusion for where data is lost". This replaces the earlier
|
|
788
|
+
// split under which the notes went to the audit workbook alone (the header of this file and
|
|
789
|
+
// publish/knockout.mjs carried that rule; both now say what replaced it). The workbook keeps them too:
|
|
790
|
+
// this adds a surface, it does not move one.
|
|
791
|
+
//
|
|
792
|
+
// LABELLED, NEVER MERGED. They render in the established `.internal` purple convention report.css
|
|
793
|
+
// already defines — the same one the clearance report uses for this material — so a reader can see whose
|
|
794
|
+
// voice a line is in. Merging them into the client-voiced body would make the reviewer's asides read as
|
|
795
|
+
// findings about the mark, which is the one way this ruling could produce a worse document.
|
|
796
|
+
const REVIEWER_NOTES_LEGEND = 'Information for your reference/context that likely does not need to be shared with the business is shown in purple.';
|
|
797
|
+
|
|
798
|
+
function reviewerNotesBlock(m) {
|
|
799
|
+
const notes = (Array.isArray(m?.purpleNotes) ? m.purpleNotes : [])
|
|
800
|
+
.map((n) => String(n ?? '').trim()).filter(Boolean);
|
|
801
|
+
if (!notes.length) return '';
|
|
802
|
+
return `<div class="internal">
|
|
803
|
+
<span class="tag">For your reference</span>
|
|
804
|
+
${notes.map((n) => `<p class="ko-bul">${inlineMd(n)}</p>`).join('')}
|
|
805
|
+
</div>`;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function registerFindingBlock(v, markIndex, reads = null, framework = null) {
|
|
763
809
|
const r = v.record;
|
|
764
810
|
// — THE RATER'S OWN READ OF THIS FILING, when it recorded one. `registerReads` is
|
|
765
811
|
// joined to the record store by the validator, so a row that reaches here names a filing this run
|
|
766
812
|
// actually holds. With no row the neutral line stands, and the neutral line is TRUE: it describes the
|
|
767
813
|
// card. That is why the field can be optional without the page ever asserting something false.
|
|
768
|
-
const
|
|
769
|
-
.map((x) => ({ id: String(x?.recordId ?? '').trim(), text: String(x?.read ?? '').trim() }))
|
|
770
|
-
.find((x) => x.id && x.id === String(r?.recordId ?? '').trim())
|
|
814
|
+
const row = (Array.isArray(reads) ? reads : [])
|
|
815
|
+
.map((x) => ({ id: String(x?.recordId ?? '').trim(), text: String(x?.read ?? '').trim(), band: String(x?.band ?? '').trim() }))
|
|
816
|
+
.find((x) => x.id && x.id === String(r?.recordId ?? '').trim());
|
|
817
|
+
const read = row?.text;
|
|
818
|
+
// The band rides the SAME row as the read and is independently optional: a read with no band prints
|
|
819
|
+
// exactly as it did before this field existed. The validator has already checked the word against the
|
|
820
|
+
// frozen ladder, so `bandStop` resolves a real stop rather than falling through to a default that would
|
|
821
|
+
// colour an unknown word as though it were rated.
|
|
822
|
+
const band = row?.band || null;
|
|
823
|
+
const stop = band ? bandStop(framework, band) : null;
|
|
771
824
|
const meta = [r.owner ? esc(r.owner) : 'proprietor not stated', r.territory ? esc(r.territory) : null]
|
|
772
825
|
.filter(Boolean).join(' · ');
|
|
773
826
|
const receipt = isHttpUrl(r.url) ? linkOrText(r.url) : esc(r.recordId ?? 'no record address supplied');
|
|
774
827
|
return `<div class="card ko-find" data-ko-mark="${Number(markIndex)}" data-ko-ord="${Number(v.ordinal)}">
|
|
775
828
|
<div class="top">
|
|
776
|
-
<div class="rail" style="background:var(--faint)"></div>
|
|
829
|
+
<div class="rail" style="background:var(${stop ?? '--faint'})"></div>
|
|
777
830
|
<div class="body">
|
|
778
831
|
<div class="cardhead">
|
|
779
|
-
<span class="fnum">${esc(v.ref)}</span><span class="who">${esc(r.mark ?? 'Unnamed filing')}</span
|
|
832
|
+
<span class="fnum">${esc(v.ref)}</span><span class="who">${esc(r.mark ?? 'Unnamed filing')}</span>${
|
|
833
|
+
band ? `<span class="ko-findband" style="background:var(${stop})">${esc(band)}</span>` : ''}<span class="src reg">Register</span>
|
|
780
834
|
</div>
|
|
781
835
|
<p class="ko-findmeta">${meta}</p>
|
|
782
836
|
<p class="ko-findnet">${esc(v.statement)}</p>
|
|
@@ -925,7 +979,7 @@ function analysisSection(marks, framework, { registerCounts = null, probeRan = f
|
|
|
925
979
|
// under compareKnockoutBlockingPower, so appending them is the ladder's own rule applied to a row
|
|
926
980
|
// that has no rung, not a new one invented for the register.
|
|
927
981
|
const reg = registerCardViews(m, framework, registerRecords);
|
|
928
|
-
const regBlocks = reg.cards.map((v) => registerFindingBlock(v, markIndex, m?.registerReads)).join('');
|
|
982
|
+
const regBlocks = reg.cards.map((v) => registerFindingBlock(v, markIndex, m?.registerReads, framework)).join('');
|
|
929
983
|
const overflow = reg.promoted > reg.cards.length
|
|
930
984
|
? `<p class="ko-bul">${esc(`${reg.promoted - reg.cards.length} further filing${reg.promoted - reg.cards.length === 1 ? '' : 's'} for this name met the same test — every one of them is in the filings section below.`)}</p>`
|
|
931
985
|
: '';
|
|
@@ -950,11 +1004,17 @@ function analysisSection(marks, framework, { registerCounts = null, probeRan = f
|
|
|
950
1004
|
<div>
|
|
951
1005
|
${body || '<p class="ko-bul">No adverse signals recorded for this name on this screen.</p>'}
|
|
952
1006
|
${m.degraded ? `<p class="ko-degraded">${esc(DEGRADED_NOTE)}</p>` : ''}
|
|
1007
|
+
${reviewerNotesBlock(m)}
|
|
953
1008
|
<p class="ko-reg">${esc(registerLine(m, registerCounts, probeRan, registerRecords, reg.cards))}</p>
|
|
954
1009
|
</div>
|
|
955
1010
|
</div>`;
|
|
956
1011
|
}).join('');
|
|
957
|
-
|
|
1012
|
+
// The legend rides the panel and only when a note is actually on it — a standing sentence explaining a
|
|
1013
|
+
// colour no reader can see would be the report describing a convention it did not use.
|
|
1014
|
+
const anyNotes = marks.some((m) => (Array.isArray(m?.purpleNotes) ? m.purpleNotes : [])
|
|
1015
|
+
.some((n) => String(n ?? '').trim()));
|
|
1016
|
+
const legend = anyNotes ? `<p class="ko-refnote">${esc(REVIEWER_NOTES_LEGEND)}</p>` : '';
|
|
1017
|
+
return `<div class="panel ko-glance">${legend}${cards}</div>`;
|
|
958
1018
|
}
|
|
959
1019
|
|
|
960
1020
|
// The method line names EXACTLY what ran, and its wording is doctrine — with counts on the page, "register
|
|
@@ -1324,6 +1384,39 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1324
1384
|
classesDriving: m.classesDriving ?? [],
|
|
1325
1385
|
degraded: Boolean(m.degraded),
|
|
1326
1386
|
points: m.bullets ?? [],
|
|
1387
|
+
// ── THE ASSESSMENT THE RUN ALREADY WROTE (tracker issue 274) ──────────────────────────────
|
|
1388
|
+
//
|
|
1389
|
+
// Seven keys the assess stage writes into knockout-findings.json reached this file as nothing.
|
|
1390
|
+
// They were not withheld by a rule — no code decided against them; this projection is an explicit
|
|
1391
|
+
// whitelist and nobody had added them, so an assistant drafting from this file could not see the
|
|
1392
|
+
// reasoning behind a rating it was drafting about.
|
|
1393
|
+
//
|
|
1394
|
+
// `assessment` is the sharpest case: publish substitutes it into `batch.executiveSummary` ONLY on
|
|
1395
|
+
// a multi-mark batch, so on a single-mark run — the common knockout — the mark's own opening read
|
|
1396
|
+
// was written, validated, and then dropped by both surfaces at once.
|
|
1397
|
+
//
|
|
1398
|
+
// `purpleNotes` is the reviewer's notes, and it is here by the owner's ruling of 2026-09-07: one
|
|
1399
|
+
// report, read by the person who ran Clearotron, because keeping material off it "just adds
|
|
1400
|
+
// confusion for where data is lost". They are LABELLED on the page rather than merged into the
|
|
1401
|
+
// client-voiced body — see reviewerNotesBlock — so a reader can tell whose voice they are in.
|
|
1402
|
+
assessment: (() => { const t = String(m.assessment ?? '').trim(); return t || null; })(),
|
|
1403
|
+
counterFactors: m.counterFactors ?? [],
|
|
1404
|
+
mitigation: (() => { const t = String(m.mitigation ?? '').trim(); return t || null; })(),
|
|
1405
|
+
// `null`, not `false`, when the rater said nothing: a crowded field is a mitigant the reasoning
|
|
1406
|
+
// turns on, and an unstated one must not read as a stated "no".
|
|
1407
|
+
crowdedField: typeof m.crowdedField === 'boolean' ? m.crowdedField : null,
|
|
1408
|
+
reviewerNotes: m.purpleNotes ?? [],
|
|
1409
|
+
// The proof-of-search rows — the same defensibility record list_searches answers from.
|
|
1410
|
+
negatives: (m.negatives ?? []).map((n) => ({
|
|
1411
|
+
term: n?.term ?? null, source: n?.source ?? null, note: n?.note ?? null,
|
|
1412
|
+
})),
|
|
1413
|
+
// The rater's read of a filing it weighed, keyed by the record id the card prints, so a consumer
|
|
1414
|
+
// can join a read to its filing without matching on prose.
|
|
1415
|
+
registerReads: (m.registerReads ?? []).map((x) => ({
|
|
1416
|
+
recordId: x?.recordId ?? null,
|
|
1417
|
+
read: x?.read ?? null,
|
|
1418
|
+
band: String(x?.band ?? '').trim() || null,
|
|
1419
|
+
})),
|
|
1327
1420
|
// — the typed finding, ranked, with the reference the report prints. The old key was
|
|
1328
1421
|
// `evidence[]` and it was `(m.findings ?? []).filter((f) => f?.url)`: a typed finding has no
|
|
1329
1422
|
// `url`, so this file — the one the assistant drafts client mail from — silently carried zero
|
|
@@ -1345,14 +1438,31 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1345
1438
|
// A promoted filing the brief omitted would be the report and the brief disagreeing about
|
|
1346
1439
|
// what the run found.
|
|
1347
1440
|
//
|
|
1348
|
-
// `
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1441
|
+
// `shape: 'register'` is the load-bearing field: a consumer can tell a weighed conflict from a
|
|
1442
|
+
// pointed-at filing without parsing prose.
|
|
1443
|
+
//
|
|
1444
|
+
// `band` AND `basis` NOW CARRY THE RATER'S READ WHEN IT SENT ONE (tracker issue 274). Before
|
|
1445
|
+
// this, both were constants — `band: null` and the not-weighed sentence — on every filing of
|
|
1446
|
+
// every run, including runs where the seat had written a full read of that exact filing and the
|
|
1447
|
+
// HTML card was already printing it. The page and this file disagreed, and this file is the one
|
|
1448
|
+
// the MCP brief answers from, so the surface a reader asks "what did you make of that
|
|
1449
|
+
// registration?" was the surface that had dropped the answer.
|
|
1450
|
+
//
|
|
1451
|
+
// The unmatched case is unchanged and must stay that way: no row for this recordId ⇒ `band:
|
|
1452
|
+
// null` and the not-weighed line, which describes the card and claims nothing.
|
|
1453
|
+
...registerCardViews(m, framework, registerRecords).cards.map((v) => {
|
|
1454
|
+
const row = (Array.isArray(m?.registerReads) ? m.registerReads : [])
|
|
1455
|
+
.find((x) => String(x?.recordId ?? '').trim() && String(x?.recordId ?? '').trim() === String(v.record?.recordId ?? '').trim());
|
|
1456
|
+
const read = String(row?.read ?? '').trim() || null;
|
|
1457
|
+
const band = String(row?.band ?? '').trim() || null;
|
|
1458
|
+
return {
|
|
1459
|
+
ref: v.ref, ordinal: v.ordinal, name: v.record.mark, owner: v.record.owner, band,
|
|
1460
|
+
type: 'Register filing',
|
|
1461
|
+
net: read ? v.statement : `${v.statement} It was ${NOT_WEIGHED}.`,
|
|
1462
|
+
basis: read ?? NOT_WEIGHED,
|
|
1463
|
+
evidence: v.evidence, shape: 'register',
|
|
1464
|
+
};
|
|
1465
|
+
}),
|
|
1356
1466
|
],
|
|
1357
1467
|
registerCounts: counted?.counts
|
|
1358
1468
|
? {
|
|
@@ -4,9 +4,19 @@
|
|
|
4
4
|
// report-data.json since the one-report collapse; the clearance lane's native-render branch existed in
|
|
5
5
|
// the portal with NO producer anywhere (B3's "the aspirational path") — this is that producer.
|
|
6
6
|
//
|
|
7
|
-
// Two readers, one file (the knockout doctrine,
|
|
8
|
-
// it, and the portal's native-render path reads it instead of iframing baked HTML.
|
|
9
|
-
//
|
|
7
|
+
// Two readers, one file (the knockout doctrine, as it stood): the assistant drafts client-facing mail
|
|
8
|
+
// from it, and the portal's native-render path reads it instead of iframing baked HTML.
|
|
9
|
+
//
|
|
10
|
+
// THE TWO LANES DIVERGED ON 2026-09-07 AND THIS FILE DID NOT MOVE. The owner ruled (tracker issue 274)
|
|
11
|
+
// that the KNOCKOUT report carries the reviewer's notes, on the ground that one report is read by the
|
|
12
|
+
// person who ran Clearotron and holding material back confuses where data went; render-knockout.mjs
|
|
13
|
+
// projects them under `reviewerNotes`. That ruling was about those notes on that lane. It did not touch
|
|
14
|
+
// what this producer strips — withdrawn findings, verification receipts, `::p::` tails, `[internal]`
|
|
15
|
+
// labels and engine-internal narration are all still cut here, and the guard test still polices them.
|
|
16
|
+
// Do not read the knockout change as licence to relax this list; if the ruling is meant to extend, it
|
|
17
|
+
// extends by being asked, not by inference.
|
|
18
|
+
//
|
|
19
|
+
// It remains the CLIENT CUT by construction, not by serve-time stripping:
|
|
10
20
|
// • only LIVE findings (a withdrawn finding renders nowhere — it does not exist here either);
|
|
11
21
|
// • no verification receipts (use_check / own_rights / bears_on — those are the reviewer's evidence
|
|
12
22
|
// trail and live in the audit workbook), no withdrawn_reason, no quarantine records;
|
package/driver/record-carry.mjs
CHANGED
|
@@ -840,7 +840,7 @@ export function silentlyLostFindings({ reconciliation = null, carryRows = null,
|
|
|
840
840
|
* It also anticipated this gap in writing: "nine divergences from a digest finding-ending, every one of
|
|
841
841
|
* them `step-stated`". Nine of the shape nothing checked.
|
|
842
842
|
*
|
|
843
|
-
* MEASURED ON R2
|
|
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
845
|
* list, `OSLER DELPHI` and `DELFITY`, 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:
|
|
@@ -880,7 +880,7 @@ export function statedDivergenceFindings({ reconciliation = null, carryRows = nu
|
|
|
880
880
|
//
|
|
881
881
|
// The first cut of this function gated on the reconciliation's finding-ended positions, mirroring the
|
|
882
882
|
// sibling. That inherited the sibling's BLIND SPOT along with its shape, and the check was inert on
|
|
883
|
-
// the very delivery it was written for. Replayed against
|
|
883
|
+
// the very delivery it was written for. Replayed against that delivery:
|
|
884
884
|
//
|
|
885
885
|
// silentlyLostFindings checked=5 matched=5 lost=0
|
|
886
886
|
// statedDivergenceFindings checked=5 matched=5 diverged=0 ← should have named two marks
|
|
@@ -674,7 +674,7 @@ export function scoreRecall({ reference, findings = [], retrieved = [], scopeCla
|
|
|
674
674
|
// proprietor with more than one mark. A large filer can perfectly well have one mark the run withheld
|
|
675
675
|
// and a DIFFERENT mark, not in the reference, that it surfaced. Both rows are true.
|
|
676
676
|
//
|
|
677
|
-
// It fired on R2
|
|
677
|
+
// It fired on a delivered R2 run: `<large filer>: reference "DELFITY" is withheld, surfaced "DELPHINA"
|
|
678
678
|
// is noise`. Different marks, different records, one proprietor that files a great many. And because
|
|
679
679
|
// score.mjs prints a collision as "do not read the recall numbers above", ONE such proprietor
|
|
680
680
|
// suppressed the whole run's recall measurement — a real 88% → 63% movement went unquoted on the
|
|
@@ -95,6 +95,13 @@ export const RESULT_NOUN_FIELDS = Object.freeze([
|
|
|
95
95
|
{ file: "driver/skills-store-provenance.mjs", noun: "outcome", sites: 6, atWriteSite: 0, verdict: "out-of-scope" },
|
|
96
96
|
{ file: "driver/status-snapshot.mjs", noun: "outcome", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
97
97
|
{ file: "mcp-server/lib/events.mjs", noun: "recovered", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
98
|
+
// Read at the writing site, as this table requires. All three sites are in `knockoutSearches`, a
|
|
99
|
+
// READ-ONLY projection that composes the rows `list_searches` answers with: it opens no artifact and
|
|
100
|
+
// writes nothing, so nothing here can reach `_driver/*.json` or `run.jsonl`. Each value is a literal
|
|
101
|
+
// chosen from the run's own stored record — "found" or "no-hit" from whether the mark has findings,
|
|
102
|
+
// and "recorded" for a register term whose stored row says it did NOT answer. None is a call's return
|
|
103
|
+
// read back as a verdict, which is the defect this table exists to find.
|
|
104
|
+
{ file: "mcp-server/lib/knockout.mjs", noun: "outcome", sites: 3, atWriteSite: 0, verdict: "out-of-scope" },
|
|
98
105
|
{ file: "mcp-server/lib/ops.mjs", noun: "settled", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
99
106
|
{ file: "mcp-server/server.mjs", noun: "outcome", sites: 1, atWriteSite: 0, verdict: "out-of-scope" },
|
|
100
107
|
{ file: "scripts/backfill-started-at.mjs", noun: "outcome", sites: 5, atWriteSite: 0, verdict: "out-of-scope" },
|
|
@@ -162,9 +162,14 @@ your job. These two fields are how what you concluded reaches the reader instead
|
|
|
162
162
|
every id against that store and refuses one it does not hold — so the report can state a filing's
|
|
163
163
|
source as a fact rather than as a word you typed about your own sourcing.
|
|
164
164
|
|
|
165
|
-
- **`registerReads`** — on the MARK. Rows of `{recordId, read}` for a filing you weighed that did
|
|
166
|
-
become a `findings[]` record. `read` is what you concluded about *that* filing: whether it bears
|
|
167
|
-
rating, and why. It prints on that filing's card.
|
|
165
|
+
- **`registerReads`** — on the MARK. Rows of `{recordId, read, band?}` for a filing you weighed that did
|
|
166
|
+
**not** become a `findings[]` record. `read` is what you concluded about *that* filing: whether it bears
|
|
167
|
+
on the rating, and why. It prints on that filing's card. `band` is **optional** and is how you rate that
|
|
168
|
+
filing on its own — in the framework's band words, the same vocabulary as the mark's rating. Send it
|
|
169
|
+
whenever you formed a view on the filing itself: a registered right is often the one hard legal fact on
|
|
170
|
+
the page, and with no band its card is the only one carrying no rating while softer uses around it carry
|
|
171
|
+
one, which reads as though the registration mattered least. Omit it and the read still prints, claiming
|
|
172
|
+
no rating.
|
|
168
173
|
- **`weighedFilings`** — on a FINDING. The `recordId`s whose evidence your reasoning for that conflict
|
|
169
174
|
actually used. The report derives the finding's source labelling from this, so list one only if you
|
|
170
175
|
reasoned from it.
|
|
@@ -205,7 +210,8 @@ line, which describes the card and claims nothing about the rating. So:
|
|
|
205
210
|
"classesDriving": [8], "bullets": ["..."], "purpleNotes": ["..."],
|
|
206
211
|
"registerEstimate": "...", "parodyNote": null, "crowdedField": false,
|
|
207
212
|
"registerReads": [ { "recordId": "<verbatim from the filings you were given>",
|
|
208
|
-
"read": "<what you concluded about THAT filing>"
|
|
213
|
+
"read": "<what you concluded about THAT filing>",
|
|
214
|
+
"band": "<OPTIONAL — your rating of THAT filing, in the ladder's words>" } ],
|
|
209
215
|
"basis": "<ONE sentence: why this band, for this name, in these classes>",
|
|
210
216
|
"factors": ["<2-4 load-bearing observations, one line each>"],
|
|
211
217
|
"counterFactors": ["<1-3: what holds it at this band rather than the next>"],
|
|
@@ -301,7 +301,7 @@ export const KO_STAGES = {
|
|
|
301
301
|
// a finding with no register evidence. Omitting either field is always safe: the card keeps its
|
|
302
302
|
// neutral line, which describes the card and is true.
|
|
303
303
|
existsSync(K.registerRecords)
|
|
304
|
-
? `WHEN YOU WEIGH ONE OF THOSE FILINGS, SAY SO BY ITS OWN ID — copy "recordId" verbatim from the file above. "registerReads" on the MARK: rows of {recordId, read} for a filing you weighed that did NOT become a findings[] record; "read" is what you concluded about THAT filing — whether it bears on the rating and why — and it prints on that filing's card in the reader's own report. "weighedFilings" on a FINDING: the recordIds whose evidence your reasoning for that conflict actually used, because the report derives that finding's source labelling from it. Both are optional and both are joined against the filings you were given, so an id we do not hold is refused by name. A filing you did not weigh simply gets no row: do not invent a read to fill one, and never write "not weighed" as a read.`
|
|
304
|
+
? `WHEN YOU WEIGH ONE OF THOSE FILINGS, SAY SO BY ITS OWN ID — copy "recordId" verbatim from the file above. "registerReads" on the MARK: rows of {recordId, read, band?} for a filing you weighed that did NOT become a findings[] record; "read" is what you concluded about THAT filing — whether it bears on the rating and why — and it prints on that filing's card in the reader's own report. "band" is OPTIONAL and is your rating of THAT filing on its own, in the framework's band words: send it whenever you formed a view on the filing itself, because a registered right left unbanded is the only card on the page with no rating while softer uses beside it carry one, which reads as though the registration mattered least. Omit it and your read still prints, claiming no rating. "weighedFilings" on a FINDING: the recordIds whose evidence your reasoning for that conflict actually used, because the report derives that finding's source labelling from it. Both are optional and both are joined against the filings you were given, so an id we do not hold is refused by name. A filing you did not weigh simply gets no row: do not invent a read to fill one, and never write "not weighed" as a read.`
|
|
305
305
|
: "",
|
|
306
306
|
`A DEGRADED mark's row must carry degraded:true and the purple "Manual verification recommended"`,
|
|
307
307
|
`note; a mark WITH a payload must never claim degraded (the validator joins both against the disk).`,
|
package/driver/stages.mjs
CHANGED
|
@@ -3267,7 +3267,7 @@ export const STAGES = {
|
|
|
3267
3267
|
`DECLINATIONS: what you do not deliver, you decline BY NAME through the \`record_declination\` tool. A record that reached your findings surface leaves this stage as a finding in ${P.findings} or as a declination, and there is no third way out.`,
|
|
3268
3268
|
...(Array.isArray(findingsSurface) && findingsSurface.length ? [
|
|
3269
3269
|
`DECLINATIONS (MANDATORY): the register digest carried ${findingsSurface.length} record(s) onto your findings surface. Each one leaves this stage by one of two routes and there is no third: it becomes a finding in ${P.findings}, or you decline it BY NAME with a reason and a ground. A record you simply do not mention is reported as a defect of this run, named individually in its trace and shipped as an open doubt, so silence costs you more than a declination ever will.`,
|
|
3270
|
-
`Decline by calling the \`record_declination\` tool. Its schema names the fields it takes and what each is for — read them there, not here. The one thing to know before you call: you cite a record by its POSITION in the list below — there is no field for a mark name or a uri, so a record you were not handed cannot be expressed at all. The reason vocabulary is closed: ${DECLINATION_REASON_TOKENS.join(" / ")}, each an omission synthesis-rules.md already authorises, and if your ground is none of them the rules do not let you omit the record. The grounds you write are one or two lines in your OWN words on why THIS record does not earn a line — never machine-parsed, and what the reviewing lawyer reads. Send them in one batch where you can; a refused row never voids its neighbours, and the answer names what is still undecided so you can finish in this turn.`,
|
|
3270
|
+
`Decline by calling the \`record_declination\` tool. Its schema names the fields it takes and what each is for — read them there, not here. The one thing to know before you call: you cite a record by its POSITION in the list below — there is no field for a mark name or a uri, so a record you were not handed cannot be expressed at all. The reason vocabulary is closed: ${DECLINATION_REASON_TOKENS.join(" / ")}, each an omission synthesis-rules.md already authorises, and if your ground is none of them the rules do not let you omit the record. ONE OF THEM IS NOT AVAILABLE ON A LIVE RECORD IN AN INSTRUCTED CLASS: \`not-worth-the-line\` is a statement about the report's budget rather than about the mark, and a live registration inside one of the matter's own classes is never omitted for want of room — carry it, or decline it on something true of THAT MARK. The call is refused if you try, so it costs you a turn. The grounds you write are one or two lines in your OWN words on why THIS record does not earn a line — never machine-parsed, and what the reviewing lawyer reads. Send them in one batch where you can; a refused row never voids its neighbours, and the answer names what is still undecided so you can finish in this turn.`,
|
|
3271
3271
|
`A REFUSAL FROM THAT TOOL IS ABOUT BOOKKEEPING, NEVER ABOUT YOUR LEGAL JUDGMENT. There is exactly ONE case: declining a mark IDENTICAL to the applied-for mark, live, in one of the matter's own filed classes, on a discretionary ground — because synthesis-rules.md orders that an on-point identical mark in the relevant class is never dropped, "regardless of filer profile", so such a declination contradicts the instruction you are already following. Nothing else is refused. If that record really is the applicant's own, or already delivered under another record, say exactly that with own-right or duplicate-of-delivered and it is accepted. Whether goods are related, whether a name is off-field, whether a conflict is worth the line — those are your calls and the tool does not have an opinion about any of them.`,
|
|
3272
3272
|
`The records on your findings surface, by position:`,
|
|
3273
3273
|
...findingsSurface.map((r, i) => ` ${i}. ${r.mark ?? "(unnamed)"}${r.owner ? ` — ${r.owner}` : ""}${r.tier ? ` [${r.tier}]` : ""}${r.uri ? ` ${r.uri}` : ""}`),
|