clearotron 0.3.2-beta.4 → 0.3.2-beta.5
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 +6 -0
- 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/suite-census.json +17 -17
- package/mcp-server/CHANGELOG.md +4 -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 +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/shared/driver-dir.mjs +1 -1
|
@@ -265,6 +265,28 @@ const KO_CSS = `
|
|
|
265
265
|
/* A wide counts table must scroll inside its own panel, never push the page sideways. */
|
|
266
266
|
.ko-scroll{overflow-x:auto}
|
|
267
267
|
@media(max-width:700px){.ko-row{grid-template-columns:1fr;gap:10px}}
|
|
268
|
+
|
|
269
|
+
/* The band ladder, the territories fold and the run's own caveats (the 2026-09-16 report redesign). Ported from
|
|
270
|
+
the design's stylesheet with its fixed colours replaced by this page's tokens, so the ladder reads
|
|
271
|
+
in both themes: the tick row was #a89a8a on white and the fold's summary and prose a brown pair,
|
|
272
|
+
all three of which go invisible on the dark ground this report also renders on. */
|
|
273
|
+
.kscale{position:relative;margin:10px 8px 34px}
|
|
274
|
+
.kbar{position:relative;height:8px;border-radius:4px}
|
|
275
|
+
.kmarker{position:absolute;top:-30px;text-align:center}
|
|
276
|
+
.kpill{display:inline-block;padding:5px 12px;border-radius:999px;color:#fff;
|
|
277
|
+
font:700 13px/1 'Satoshi','Helvetica Neue',Arial,sans-serif;white-space:nowrap}
|
|
278
|
+
.kneedle{width:3px;height:14px;margin:4px auto 0;border-radius:2px}
|
|
279
|
+
.kticks{position:relative;height:16px;margin-top:8px}
|
|
280
|
+
.kticks span{position:absolute;transform:translateX(-50%);color:var(--faint);white-space:nowrap;
|
|
281
|
+
font:700 10.5px/1 'Satoshi','Helvetica Neue',Arial,sans-serif;letter-spacing:.1em;text-transform:uppercase}
|
|
282
|
+
.kticks span.on{font-weight:800}
|
|
283
|
+
.terr{margin-top:4px}
|
|
284
|
+
.terr summary{cursor:pointer;color:var(--rose,var(--faint));font-size:12px;font-weight:600;list-style:none}
|
|
285
|
+
.terr summary::-webkit-details-marker{display:none}
|
|
286
|
+
.terr p{margin:4px 0 0;font-size:12px;line-height:1.6;color:var(--slate)}
|
|
287
|
+
.ko-caveats{padding:16px 24px;margin:14px 0 8px}
|
|
288
|
+
.ko-caveats p{margin:0 0 7px;font-size:12px;line-height:1.6;color:var(--faint)}
|
|
289
|
+
.ko-caveats p:last-child{margin-bottom:0}
|
|
268
290
|
`;
|
|
269
291
|
|
|
270
292
|
const REPORT_BASE = REPORT_ROOT + '\n' + readFileSync(join(HERE, 'templates', 'report.css'), 'utf8');
|
|
@@ -311,7 +333,7 @@ function glanceSection(marks, framework, registerCounts) {
|
|
|
311
333
|
// No framework ⇒ NOTHING. A run this render was given no manifest for gets no attribution invented for
|
|
312
334
|
// it, and the chips still carry their own words.
|
|
313
335
|
const legend = framework?.title
|
|
314
|
-
?
|
|
336
|
+
? '' /* The 2026-09-16 report redesign — the framework is named once, on the rating card */
|
|
315
337
|
: '';
|
|
316
338
|
return `<div class="panel ko-glance">${legend}${rows}</div>`;
|
|
317
339
|
}
|
|
@@ -415,7 +437,7 @@ function territoriesLine(registerCounts) {
|
|
|
415
437
|
const COUNTS_READER_LINE = 'Counts include live, pending and dead filings. '
|
|
416
438
|
+ 'A count is not a conflict; the cards above say which filings matter.';
|
|
417
439
|
|
|
418
|
-
function countsSection(marks, registerCounts) {
|
|
440
|
+
function countsSection(marks, registerCounts, positions = '') {
|
|
419
441
|
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER ──────────────────────────────────────────────────
|
|
420
442
|
//
|
|
421
443
|
// The 70-word count-basis paragraph existed because three one-word headers — Identical, Containing,
|
|
@@ -468,7 +490,8 @@ function countsSection(marks, registerCounts) {
|
|
|
468
490
|
// thing in three words each.
|
|
469
491
|
return `<div class="panel">
|
|
470
492
|
<div class="ko-counts ko-scroll"><table><thead><tr><th>Name</th>${head}</tr></thead><tbody>${rows}</tbody></table></div>
|
|
471
|
-
<p class="ko-basis">${esc(COUNTS_READER_LINE)}<br>${esc(territoriesLine(registerCounts))}
|
|
493
|
+
<p class="ko-basis">${esc(COUNTS_READER_LINE)}<br>${esc(territoriesLine(registerCounts))}${
|
|
494
|
+
positions ? `<br>${esc(positions)}` : ''}</p>
|
|
472
495
|
</div>`;
|
|
473
496
|
}
|
|
474
497
|
|
|
@@ -730,11 +753,15 @@ function findingBlock(v, framework, markIndex) {
|
|
|
730
753
|
<div class="cardhead">
|
|
731
754
|
<span class="fnum">${esc(v.ref)}</span><span class="who">${esc(v.name ?? 'Unnamed finding')}</span>${
|
|
732
755
|
v.band ? `<span class="ko-findband" style="background:var(${stop})">${esc(v.band)}</span>` : ''}${sourceChips(v)}
|
|
756
|
+
<button class="ask-fi no-print">\u2726 Ask AI about this finding</button>
|
|
733
757
|
</div>
|
|
734
758
|
${meta ? `<p class="ko-findmeta">${meta}</p>` : ''}
|
|
735
759
|
${v.lead ? `<p class="ko-findnet">${inlineMd(v.lead)}</p>` : ''}
|
|
736
|
-
${
|
|
737
|
-
|
|
760
|
+
${/* The 2026-09-16 report redesign — head, meta and the one-line read stay visible; the rest folds, as on
|
|
761
|
+
a clearance card, so a screen of many names reads as a screen. */''}
|
|
762
|
+
${(v.detail || ev) ? `<details class="drill"><summary>Full detail</summary><div class="drillbody">${
|
|
763
|
+
whyBandFold(v.detail ? `<p class="ko-findbasis">${inlineMd(v.detail)}</p>` : '')}${
|
|
764
|
+
ev ? `<p class="ko-findev">Evidence: ${ev}</p>` : ''}</div></details>` : ''}
|
|
738
765
|
</div>
|
|
739
766
|
</div>
|
|
740
767
|
</div>`;
|
|
@@ -1116,33 +1143,53 @@ function ownerCheckFor(ownerChecks, recordId) {
|
|
|
1116
1143
|
* They keep the purple convention here exactly as they wear it under the cards, which is also what keeps
|
|
1117
1144
|
* them off the export: the strip is one rule on .internal and it reaches both places.
|
|
1118
1145
|
*/
|
|
1119
|
-
function aboutRequestBlock(scope, requestNotes) {
|
|
1146
|
+
function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext = '', searched = '', registerCounts = null) {
|
|
1120
1147
|
const goods = String(scope?.goods ?? '').trim();
|
|
1121
1148
|
const classes = (Array.isArray(scope?.classes) ? scope.classes : []).filter((c) => c || c === 0);
|
|
1122
1149
|
const jx = (Array.isArray(scope?.jurisdictions) ? scope.jurisdictions : []).map((t) => territoryName(t)).filter(Boolean);
|
|
1123
|
-
const asked =
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
const
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1150
|
+
const asked = goods;
|
|
1151
|
+
const classLine = classes.length ? classes.join(', ') : '';
|
|
1152
|
+
const where = jx.length ? listWords(jx) : '';
|
|
1153
|
+
// The type of search is the bolded head of the depth note; the rest of that sentence is the product's
|
|
1154
|
+
// covers line, which the owner ruled off the page.
|
|
1155
|
+
const note = String(depthNote ?? '').trim();
|
|
1156
|
+
const cut = note.lastIndexOf(' \u2014 ');
|
|
1157
|
+
const stage = cut > 0 ? note.slice(0, cut).trim() : note;
|
|
1158
|
+
const context = String(productContext ?? '').trim();
|
|
1159
|
+
// REGISTERS COUNTED IS A ROW, AND THE TERRITORY LIST HIDES BEHIND IT (the 2026-09-16 report redesign). A line of
|
|
1160
|
+
// country names running through the middle of the panel is the thing a reader skips; the count and
|
|
1161
|
+
// its source are what they read, and the list is one click away when they want it.
|
|
1162
|
+
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1163
|
+
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1164
|
+
const named = regions.map(territoryName).filter(Boolean);
|
|
1165
|
+
const counted = regions.length
|
|
1166
|
+
? `${regions.length} ${regions.length === 1 ? 'territory' : 'territories'}${provider ? `, on ${provider}` : ''}`
|
|
1167
|
+
: (registerCounts ? `Counted worldwide${provider ? `, on ${provider}` : ''}` : '');
|
|
1168
|
+
const territories = named.length ? listWords(named) : '';
|
|
1169
|
+
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1170
|
+
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1171
|
+
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
|
1172
|
+
// line of country codes through the middle of the panel.
|
|
1173
|
+
const row = (k, v, extra = '') => v ? `<div class="row"><span class="k">${esc(k)}</span><span class="v">${esc(v)}${extra}</span></div>` : '';
|
|
1174
|
+
return `<div class="panel about">
|
|
1175
|
+
<div class="label">About this request</div>
|
|
1176
|
+
${row('Type of search', stage)}
|
|
1177
|
+
${row('Instructed use', asked)}
|
|
1178
|
+
${row('Classes', classLine)}
|
|
1179
|
+
${row('Where searched', where)}
|
|
1180
|
+
${row('Context', context)}
|
|
1181
|
+
${row('Registers counted', counted, territories ? `<details class="terr"><summary>View territories</summary><p>${esc(territories)}</p></details>` : '')}
|
|
1182
|
+
${row('Searched on', searched)}
|
|
1183
|
+
${/* item 18 — a note for the reviewing lawyer does not reach the delivered page */''}
|
|
1136
1184
|
</div>`;
|
|
1137
1185
|
}
|
|
1138
1186
|
|
|
1139
1187
|
function reviewerNotesBlock(m) {
|
|
1140
1188
|
const notes = splitKnockoutNotes(m).name;
|
|
1141
1189
|
if (!notes.length) return '';
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
</div>`;
|
|
1190
|
+
// Item 18 (owner, 2026-09-16): the reviewing lawyer's notes stay in the working record, the audit
|
|
1191
|
+
// workbook and the assistant. They were styled as a visible block on the page a client opens.
|
|
1192
|
+
return '';
|
|
1146
1193
|
}
|
|
1147
1194
|
|
|
1148
1195
|
function registerFindingBlock(v, markIndex, reads = null, framework = null, ownerChecks = []) {
|
|
@@ -1181,12 +1228,18 @@ function registerFindingBlock(v, markIndex, reads = null, framework = null, owne
|
|
|
1181
1228
|
<div class="cardhead">
|
|
1182
1229
|
<span class="fnum">${esc(v.ref)}</span><span class="who">${esc(r.mark ?? 'Unnamed filing')}</span>${
|
|
1183
1230
|
band ? `<span class="ko-findband" style="background:var(${stop})">${esc(band)}</span>` : ''}<span class="src reg">Register</span>
|
|
1231
|
+
<button class="ask-fi no-print">\u2726 Ask AI about this finding</button>
|
|
1184
1232
|
</div>
|
|
1185
1233
|
<p class="ko-findmeta">${meta}</p>
|
|
1186
1234
|
<p class="ko-findnet">${esc(v.statement)}</p>
|
|
1235
|
+
${/* The 2026-09-16 report redesign — the head, the meta line and the one-line read stay visible; the rest
|
|
1236
|
+
sits behind a fold, as it does on a clearance card, so a screen of many names reads as a
|
|
1237
|
+
screen rather than as a wall. */''}
|
|
1238
|
+
<details class="drill"><summary>Full detail</summary><div class="drillbody">
|
|
1187
1239
|
${basisLine(band, read)}
|
|
1188
1240
|
${useCheck ? `<p class="ko-findev">${esc(USE_CHECK_LABEL)} ${isHttpUrl(useCheckSource) ? linkOrText(useCheckSource) : esc(useCheckSource)}</p>` : ''}
|
|
1189
1241
|
<p class="ko-findev">Register record: ${receipt}</p>
|
|
1242
|
+
</div></details>
|
|
1190
1243
|
</div>
|
|
1191
1244
|
</div>
|
|
1192
1245
|
</div>`;
|
|
@@ -1242,7 +1295,11 @@ function registerLine(mark, registerCounts, probeRan, registerRecords = null, ca
|
|
|
1242
1295
|
* This is the "none exists" arm, and it is only reachable when every
|
|
1243
1296
|
* search actually answered.
|
|
1244
1297
|
*/
|
|
1245
|
-
|
|
1298
|
+
// `where` names where the cards this clause points at actually sit relative to it. It said "above"
|
|
1299
|
+
// unconditionally, which was true while the clause was drawn under the cards. It is drawn under the
|
|
1300
|
+
// COUNTS now — the numbers it qualifies — and the cards are below it, so a fixed word would send a
|
|
1301
|
+
// reader the wrong way up the page. The caller knows the order; this function does not guess it.
|
|
1302
|
+
function registerPositionClause(mark, registerCounts, registerRecords, cards = [], where = 'above') {
|
|
1246
1303
|
if (!registerRecords) {
|
|
1247
1304
|
return registerCounts ? 'The filings behind those counts were not listed on this run.' : '';
|
|
1248
1305
|
}
|
|
@@ -1255,11 +1312,11 @@ function registerPositionClause(mark, registerCounts, registerRecords, cards = [
|
|
|
1255
1312
|
if (cards.length) {
|
|
1256
1313
|
const classes = [...new Set(cards.flatMap((c) => (c.record.classes ?? []).map(Number).filter(Number.isFinite)))]
|
|
1257
1314
|
.sort((a, b) => a - b);
|
|
1258
|
-
const
|
|
1315
|
+
const classWhere = classes.length ? ` in class${classes.length === 1 ? '' : 'es'} ${classes.join(', ')}` : '';
|
|
1259
1316
|
const one = cards.length === 1;
|
|
1260
1317
|
const refs = cards.map((c) => c.ref).join(', ');
|
|
1261
1318
|
return `${one ? 'A filing' : `${cards.length} filings`} of this name or a close variation of it `
|
|
1262
|
-
+ `stand${one ? 's' : ''} on ${provider}${
|
|
1319
|
+
+ `stand${one ? 's' : ''} on ${provider}${classWhere} — ${refs} ${where}.`;
|
|
1263
1320
|
}
|
|
1264
1321
|
const records = entry.records ?? [];
|
|
1265
1322
|
if (!records.length) {
|
|
@@ -1312,21 +1369,20 @@ function coverageClause(mark, registerCounts, probeRan) {
|
|
|
1312
1369
|
* the first one and asked "what holds what?" — so the fix is not a synonym in the same style, it is a
|
|
1313
1370
|
* label that carries the band's own word and answers the question by itself.
|
|
1314
1371
|
*
|
|
1315
|
-
* factors → "Why High"
|
|
1316
|
-
* counterFactors → "
|
|
1317
|
-
* mitigation → "What would
|
|
1372
|
+
* factors → "Why High" (the mark's own band, on the rating card)
|
|
1373
|
+
* counterFactors → "Remaining uncertainties"
|
|
1374
|
+
* mitigation → "What would change this"
|
|
1318
1375
|
*
|
|
1319
|
-
* The
|
|
1320
|
-
*
|
|
1321
|
-
*
|
|
1322
|
-
*
|
|
1376
|
+
* The counter-factor label was "Why not <the rung above>", read off the run's own ladder. It asked the
|
|
1377
|
+
* reader to hold a ladder the page had not given them; at the top rung there was no higher band to name,
|
|
1378
|
+
* so it asked a different question there; and a band the build could not find on the ladder fell to that
|
|
1379
|
+
* same special case. the 2026-09-16 report redesign names what is under the heading instead, at every rung.
|
|
1323
1380
|
*/
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
}
|
|
1381
|
+
// The 2026-09-16 report redesign — the heading names what is UNDER it rather than a band the reader was not shown.
|
|
1382
|
+
// It read "Why not <the rung above>", which asked the reader to hold a ladder they had not been given
|
|
1383
|
+
// and, at the top rung, had no band to name and asked a different question instead. The engine's
|
|
1384
|
+
// bullets are unchanged; only the label is, and it no longer varies with the band.
|
|
1385
|
+
const COUNTER_LABEL = 'Remaining uncertainties';
|
|
1330
1386
|
|
|
1331
1387
|
const ASSESSMENT_FOLD_LABEL = 'Read the full assessment';
|
|
1332
1388
|
|
|
@@ -1343,21 +1399,82 @@ const SCOPE_BLOCK_TEXT = 'What this is. A fast screen for obvious blockers to us
|
|
|
1343
1399
|
// The stopword set and the idempotent stem travel with it; the reference text below stays here, because
|
|
1344
1400
|
// it is this page's own words and nothing else's.
|
|
1345
1401
|
|
|
1402
|
+
// ── THE RATING CARD, ONE DESIGN WITH THE CLEARANCE REPORT (the 2026-09-16 report redesign) ──────────────────────
|
|
1403
|
+
// The company's own ladder drawn as the coloured scale with the band lit, the basis sentence, the
|
|
1404
|
+
// engine's own "why" bullets under a heading that names the band, and the framework named once. The
|
|
1405
|
+
// ticks clamp at the ends so the first and last words do not hang off the bar. Nothing is composed:
|
|
1406
|
+
// the bullets and the basis are the engine's, and the band words are the framework's own.
|
|
1407
|
+
function koScale(framework, band) {
|
|
1408
|
+
const ladder = (Array.isArray(framework?.bands) ? framework.bands : []).map((b) => String(b?.label ?? '').trim()).filter(Boolean).reverse();
|
|
1409
|
+
if (!ladder.length) return '';
|
|
1410
|
+
const n = ladder.length;
|
|
1411
|
+
const idx = Math.max(0, ladder.findIndex((b) => b.toLowerCase() === String(band ?? '').trim().toLowerCase()));
|
|
1412
|
+
const pct = (k) => (n > 1 ? (k / (n - 1)) * 100 : 50);
|
|
1413
|
+
const TONE = ['var(--clear)', 'var(--low)', 'var(--med)', 'var(--high)', 'var(--severe)'];
|
|
1414
|
+
const tone = (k) => TONE[Math.round((k / Math.max(1, n - 1)) * (TONE.length - 1))];
|
|
1415
|
+
const shift = (k) => (k === 0 ? '0' : k === n - 1 ? '-100%' : '-50%');
|
|
1416
|
+
return `<div class="kscale"><div class="kbar" style="background:linear-gradient(90deg,${
|
|
1417
|
+
ladder.map((b, k) => `${tone(k)} ${pct(k)}%`).join(',')})"><div class="kmarker" style="left:${pct(idx)}%;transform:translateX(${
|
|
1418
|
+
idx === 0 ? '-12px' : idx === n - 1 ? 'calc(-100% + 12px)' : '-50%'})"><div class="kpill" style="background:${tone(idx)}">${
|
|
1419
|
+
esc(ladder[idx] || band || '')}</div><div class="kneedle" style="background:${tone(idx)}"></div></div></div><div class="kticks">${
|
|
1420
|
+
ladder.map((b, k) => `<span class="${k === idx ? 'on' : ''}" style="left:${pct(k)}%;transform:translateX(${shift(k)})${
|
|
1421
|
+
k === idx ? `;color:${tone(k)}` : ''}">${esc(b)}</span>`).join('')}</div></div>`;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
// WHAT HAPPENS NEXT, AS ITS OWN SECTION (the 2026-09-16 report redesign). The engine writes this paragraph part-way
|
|
1425
|
+
// down each mark's long assessment, under a heading of its own, where it rendered only inside a fold
|
|
1426
|
+
// that opens at the bottom of the screen. It is the one paragraph a reader acts on, so it closes the
|
|
1427
|
+
// page under its own heading.
|
|
1428
|
+
//
|
|
1429
|
+
// It MOVES, it is not copied: `splitOutcome` returns the assessment without it, and the fold renders
|
|
1430
|
+
// that remainder, so the words appear once.
|
|
1431
|
+
//
|
|
1432
|
+
// THE HEADING IS THE MODEL'S OWN, so this hoists only the LAST block of the assessment. The assessing
|
|
1433
|
+
// skill fixes no heading vocabulary — it asks for sub-headers "where the content divides" and for the
|
|
1434
|
+
// read to close on what to do with the name, and the wording is written fresh each run. Matching a list
|
|
1435
|
+
// of phrases anywhere in the document would let a mark whose read happens to argue under "Recommendation"
|
|
1436
|
+
// in the MIDDLE have that middle lifted to the bottom, handing the client the engine's argument in an
|
|
1437
|
+
// order the engine did not write, with nothing going red. A trailing block is the one position where a
|
|
1438
|
+
// move cannot reorder what is left. So the phrase list only decides whether the last block is the
|
|
1439
|
+
// outcome; anything earlier stays where it was written, and the page renders the assessment whole.
|
|
1440
|
+
const OUTCOME_HEADING_RE = /^#{1,6}\s*(?:what to do with it|what happens next|next steps?|recommendations?)\b\s*$/i;
|
|
1441
|
+
export function splitOutcome(assessment) {
|
|
1442
|
+
const whole = String(assessment ?? '').trim();
|
|
1443
|
+
const lines = whole.split('\n');
|
|
1444
|
+
const heads = lines.map((l, i) => [l.trim(), i]).filter(([l]) => /^#{1,6}\s/.test(l));
|
|
1445
|
+
const last = heads[heads.length - 1];
|
|
1446
|
+
if (!last || !OUTCOME_HEADING_RE.test(last[0])) return { body: whole, outcome: '' };
|
|
1447
|
+
const outcome = lines.slice(last[1] + 1).join('\n').trim();
|
|
1448
|
+
if (!outcome) return { body: whole, outcome: '' };
|
|
1449
|
+
return { body: lines.slice(0, last[1]).join('\n').trim(), outcome };
|
|
1450
|
+
}
|
|
1451
|
+
function markOutcomes(marks) {
|
|
1452
|
+
return (marks ?? [])
|
|
1453
|
+
.map((m) => ({ name: String(m?.name ?? '').trim(), text: splitOutcome(m?.assessment).outcome }))
|
|
1454
|
+
.filter((o) => o.text);
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1346
1457
|
function readBlock(m, framework) {
|
|
1347
1458
|
const factors = (m.factors ?? []).filter((s) => typeof s === 'string' && s.trim());
|
|
1348
1459
|
const counter = (m.counterFactors ?? []).filter((s) => typeof s === 'string' && s.trim());
|
|
1349
1460
|
const structured = Boolean(m.basis || factors.length || counter.length || m.mitigation);
|
|
1350
1461
|
const bullets = (m.bullets ?? []).map((b) => `<li>${inlineMd(b)}</li>`).join('');
|
|
1351
1462
|
if (!structured) return bullets ? `<ul class="ko-bul">${bullets}</ul>` : '';
|
|
1352
|
-
const assessment =
|
|
1463
|
+
const assessment = splitOutcome(m.assessment).body;
|
|
1353
1464
|
const band = String(m.rating ?? '').trim();
|
|
1354
|
-
|
|
1465
|
+
const card = [
|
|
1466
|
+
`<div class="label">Overall risk</div>`,
|
|
1467
|
+
koScale(framework, band),
|
|
1355
1468
|
m.basis ? `<p class="ko-basisline">${inlineMd(m.basis)}</p>` : '',
|
|
1356
|
-
factors.length ? `<div class="
|
|
1469
|
+
factors.length ? `<div class="gwhy">${band ? `<div class="label">Why ${esc(band)}</div>` : ''}<ul class="ko-bul">${
|
|
1357
1470
|
factors.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
|
|
1358
|
-
|
|
1471
|
+
framework?.title ? `<div class="gframe">Rated on the ${esc(framework.title)}</div>` : '',
|
|
1472
|
+
].filter(Boolean).join('');
|
|
1473
|
+
return [
|
|
1474
|
+
card ? `<div class="panel gauge ko-gauge">${card}</div>` : '',
|
|
1475
|
+
counter.length ? `<div class="ko-counter"><span class="ko-lbl2">${esc(COUNTER_LABEL)}</span><ul class="ko-bul">${
|
|
1359
1476
|
counter.map((f) => `<li>${inlineMd(f)}</li>`).join('')}</ul></div>` : '',
|
|
1360
|
-
m.mitigation ? `<div class="ko-mitig"><span class="ko-lbl2">What would
|
|
1477
|
+
m.mitigation ? `<div class="ko-mitig"><span class="ko-lbl2">What would change this</span><p>${inlineMd(m.mitigation)}</p></div>` : '',
|
|
1361
1478
|
// ── THE ASSESSMENT REPLACES THE "FULL NARRATIVE" FOLD ────────────────────────────────────────────
|
|
1362
1479
|
//
|
|
1363
1480
|
// The model writes both. `bullets` (five) say in other words what `factors` (four) already say
|
|
@@ -1407,6 +1524,18 @@ function analysisSection(marks, framework, { registerCounts = null, probeRan = f
|
|
|
1407
1524
|
// The guard is deliberately TIGHT: a listed filing that is dead or out of the counted classes does
|
|
1408
1525
|
// not suppress it, because for that mark the sentence is true and the filings section says so.
|
|
1409
1526
|
const body = [bullets, finds, regBlocks, overflow].filter(Boolean).join('');
|
|
1527
|
+
// A BAND WITH NO LADDER NAMED IS A NUMBER WITH NO SCALE. The framework's name rides the rating card,
|
|
1528
|
+
// and that card is only built for a mark with a structured read — an archived run whose marks carry
|
|
1529
|
+
// bullets and nothing else draws no card, and its band chips were then shown with no statement
|
|
1530
|
+
// anywhere of which ladder they belong to. It was named in the glance panel, which rendered either
|
|
1531
|
+
// way; that line went because the card names it, and this case went with it unnoticed.
|
|
1532
|
+
//
|
|
1533
|
+
// IT SITS OUTSIDE `body`, and that is not tidiness. `body` being empty is what decides whether this
|
|
1534
|
+
// mark says "no adverse signals recorded" — a mark with nothing against it — so anything added to it
|
|
1535
|
+
// silently retires that sentence. Named ONCE: the read above carries it where it drew a card, and
|
|
1536
|
+
// the test for that reads the markup this same function just produced.
|
|
1537
|
+
const frameworkLine = !body.includes('class="gframe"') && framework?.title && String(m.rating ?? '').trim()
|
|
1538
|
+
? `<div class="gframe">Rated on the ${esc(framework.title)}</div>` : '';
|
|
1410
1539
|
return `<div class="ko-row">
|
|
1411
1540
|
<div>
|
|
1412
1541
|
<p class="ko-name">${esc(m.name)}</p>
|
|
@@ -1416,9 +1545,10 @@ function analysisSection(marks, framework, { registerCounts = null, probeRan = f
|
|
|
1416
1545
|
</div>
|
|
1417
1546
|
<div>
|
|
1418
1547
|
${body || '<p class="ko-bul">No adverse signals recorded for this name on this screen.</p>'}
|
|
1548
|
+
${frameworkLine}
|
|
1419
1549
|
${m.degraded ? `<p class="ko-degraded">${esc(DEGRADED_NOTE)}</p>` : ''}
|
|
1420
1550
|
${reviewerNotesBlock(m)}
|
|
1421
|
-
|
|
1551
|
+
${/* The 2026-09-16 report redesign — the register line restated the counts table above it */''}
|
|
1422
1552
|
</div>
|
|
1423
1553
|
</div>`;
|
|
1424
1554
|
}).join('');
|
|
@@ -1483,6 +1613,10 @@ function depthStrip(note) {
|
|
|
1483
1613
|
// complete renderer-owned paragraphs (methodLine above, the audit-workbook sentence below). This is the
|
|
1484
1614
|
// lead-in they had and it did not: it is renderer-owned, so it is there whatever the model wrote, and it
|
|
1485
1615
|
// says what the lines under it are. It is not a filter and it rewrites no caveat.
|
|
1616
|
+
// IT CAME BACK WITH THE BLOCK (2026-09-16). The caveats were rendered nowhere for a while after the
|
|
1617
|
+
// section above them was removed, and the first restoration drew them as bare paragraphs — dropping this
|
|
1618
|
+
// sentence, which is the whole of what the ruling asked for. One paragraph, the lead-in first, the
|
|
1619
|
+
// caveats verbatim after it; only where it sits on the page has changed.
|
|
1486
1620
|
const CAVEAT_LEAD = 'This screen also carries the following limits:';
|
|
1487
1621
|
|
|
1488
1622
|
|
|
@@ -1557,9 +1691,29 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1557
1691
|
// nothing. It renders in the place the numbers would have occupied, at the top of the section that
|
|
1558
1692
|
// owns them, because that is where a reader looks for them and fails to find them.
|
|
1559
1693
|
const tierLine = tierAbsenceLine(registerCounts, probeRan);
|
|
1694
|
+
// WHERE THE NAME STANDS, WHICH THE NUMBERS DO NOT SAY. A table of counts answers how many filings were
|
|
1695
|
+
// seen; the question a reader holds is whether anything STANDS on the register for this name, and two
|
|
1696
|
+
// of that question's six states are absences — nothing was listed, and the listing could not be made
|
|
1697
|
+
// at all. The clause said it from the line under the conflict cards, and that line was removed for
|
|
1698
|
+
// restating the counts table. Only its COVERAGE half restated the table; the position half is a
|
|
1699
|
+
// separate answer, and losing it left an absence as a silence over a table of numbers, which is the
|
|
1700
|
+
// one thing the comment over it says must never happen.
|
|
1701
|
+
//
|
|
1702
|
+
// IT RIDES BOTH BRANCHES. Attaching it to the counts table alone would have dropped it on every run
|
|
1703
|
+
// that has records and no counts — a product tier that lists filings without counting them — and that
|
|
1704
|
+
// is the same silent loss again, one branch further down. The refs come from the same view function
|
|
1705
|
+
// the cards are drawn from: a second derivation here would be a second answer to which filings were
|
|
1706
|
+
// promoted, and the clause would name refs no card carries.
|
|
1707
|
+
const cardsByMark = new Map(marks.map((m) => [m?.name, registerCardViews(m, framework, registerRecords).cards]));
|
|
1708
|
+
const positions = marks.map((m) => {
|
|
1709
|
+
const clause = registerPositionClause(m, registerCounts, registerRecords, cardsByMark.get(m?.name) ?? [], 'below');
|
|
1710
|
+
if (!clause) return '';
|
|
1711
|
+
return marks.length > 1 && m?.name ? `${m.name}: ${clause}` : clause;
|
|
1712
|
+
}).filter(Boolean).join(' ');
|
|
1560
1713
|
const counts = hasCounts
|
|
1561
|
-
? countsSection(marks, registerCounts)
|
|
1562
|
-
: `<div class="panel"><p class="ko-tier">${esc(tierLine)}</p
|
|
1714
|
+
? countsSection(marks, registerCounts, positions)
|
|
1715
|
+
: `<div class="panel"><p class="ko-tier">${esc(tierLine)}</p>${
|
|
1716
|
+
positions ? `<p class="ko-basis">${esc(positions)}</p>` : ''}</div>`;
|
|
1563
1717
|
// ── A CAVEAT THAT SAYS NOTHING THE SCOPE BLOCK HAS NOT SAID IS NOT RENDERED ────────────────────────
|
|
1564
1718
|
//
|
|
1565
1719
|
// The four model-written caveats on the measured run overlap the fixed text completely — 331 item 9,
|
|
@@ -1577,7 +1731,7 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1577
1731
|
// They are about the asking, not about a name, so a batch repeating them per mark would be the same
|
|
1578
1732
|
// sentence three times. reviewerNotesBlock renders the rest, under that mark's own cards.
|
|
1579
1733
|
const requestNotes = marks.flatMap((m) => splitKnockoutNotes(m).request);
|
|
1580
|
-
const aboutRequest = aboutRequestBlock(instructedScope, requestNotes);
|
|
1734
|
+
const aboutRequest = aboutRequestBlock(instructedScope, requestNotes, depthNote, productContext, issued, registerCounts);
|
|
1581
1735
|
const provider = hasCounts ? (registerCounts.providerLabel ?? registerCounts.provider ?? 'the register') : null;
|
|
1582
1736
|
// The filings appendix renders only when the run produced a listing artifact — never on its absence,
|
|
1583
1737
|
// and never as an empty table. A knockout with no sidecar publishes exactly the counts-only document.
|
|
@@ -1594,11 +1748,11 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1594
1748
|
n + registerCardsOnPage(registerCardViews(m, framework, registerRecords).cards, m, framework).length, 0);
|
|
1595
1749
|
const hasRecords = Boolean(registerRecords && (registerRecords.marks?.length || registerRecords.unavailable));
|
|
1596
1750
|
const filings = hasRecords ? filingsSection(marks, registerRecords) : '';
|
|
1597
|
-
const onFieldSec = `<div class="sec"
|
|
1751
|
+
const onFieldSec = `<div class="sec" id="findings"><h2>Conflicts</h2></div>
|
|
1598
1752
|
${counts}
|
|
1599
1753
|
${analysis}`;
|
|
1600
1754
|
const filingsSec = filings
|
|
1601
|
-
? `<div class="sec"
|
|
1755
|
+
? `<div class="sec" id="filings"><h2>Also considered</h2></div>
|
|
1602
1756
|
${filings}`
|
|
1603
1757
|
: '';
|
|
1604
1758
|
|
|
@@ -1623,7 +1777,8 @@ ${filings}`
|
|
|
1623
1777
|
${logoLockup({ mark: 20, tag: '' })}<span class="sp"></span>
|
|
1624
1778
|
<span class="tb-risk" style="background:var(${overallStop})">${esc(overall ?? '—')}</span>
|
|
1625
1779
|
<span class="mono tb-matter" style="font-size:11px;color:var(--faint)">${esc(matter || runId || '')}</span>
|
|
1626
|
-
${issued ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued ${esc(issued)}</span>` : ''}
|
|
1780
|
+
${issued ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued on ${esc(issued)}</span>` : ''}
|
|
1781
|
+
<button type="button" class="tbbtn tb-ask no-print">\u2726 <span class="tb-lbl">Ask AI</span></button>
|
|
1627
1782
|
</div>
|
|
1628
1783
|
</div>
|
|
1629
1784
|
<div class="fab-stack">${themeButton()}</div>
|
|
@@ -1649,9 +1804,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1649
1804
|
<header class="hero">
|
|
1650
1805
|
${demoBannerHtml(demoData === true)}
|
|
1651
1806
|
${confLineHtml(delivery, productName)}
|
|
1652
|
-
${depthStrip(depthNote)}
|
|
1653
1807
|
<h1 class="mark">${esc(title)}</h1>
|
|
1654
|
-
${productContext ? `<p class="ko-classes" style="font-size:13.5px;margin:0 0 16px">${inlineMd(productContext)}</p>` : ''}
|
|
1655
1808
|
${aboutRequest}
|
|
1656
1809
|
${summary ? `<div class="sub">${mdParagraphs(summary)}</div>` : ''}
|
|
1657
1810
|
</header>
|
|
@@ -1660,53 +1813,33 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1660
1813
|
${onFieldSec}
|
|
1661
1814
|
${filingsSec}
|
|
1662
1815
|
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
// cites something.
|
|
1688
|
-
const linked = citedFindings && !uncitedFindings
|
|
1689
|
-
? ' Every conflict above links to the material we found.'
|
|
1690
|
-
: '';
|
|
1691
|
-
const source = registerCounts
|
|
1692
|
-
? ` Register data: ${registerCounts.providerLabel ?? registerCounts.provider ?? 'the register'}.`
|
|
1693
|
-
: '';
|
|
1694
|
-
return `<p class="ko-scope"><b>What this is.</b> A fast screen for obvious blockers to using each `
|
|
1695
|
-
+ `name, from marketplace and web use plus a count of register filings.</p>`
|
|
1696
|
-
+ `<p class="ko-scope"><b>What it is not.</b> A clearance search. We drew no register conclusions `
|
|
1697
|
-
+ `and give no filing advice. A name that passes here is not clear; it goes on to clearance.</p>`
|
|
1698
|
-
+ `<p class="ko-scope">${linked.trim()}${linked ? ' ' : ''}The audit workbook holds every search run, `
|
|
1699
|
-
+ `every empty result and the working notes.${source}</p>`;
|
|
1700
|
-
})()}
|
|
1701
|
-
${newCaveats.length ? `<p class="ko-scope" style="border-top:1px solid var(--line)">${CAVEAT_LEAD}<br>${newCaveats.map((c) => inlineMd(c)).join('<br>')}</p>` : ''}
|
|
1702
|
-
${auditFile ? `<p class="ko-scope" style="border-top:1px solid var(--line)"><a href="${escAttr(auditFile)}">Download the audit workbook (Excel)</a> — every search run, every negative result, and the working notes behind these ratings.${
|
|
1703
|
-
citedFindings ? ` The reference beside each common-law conflict above (for example <span class="mono">${esc(firstRef(marks, framework))}</span>) is its row on the workbook's Findings sheet.${
|
|
1704
|
-
registerCardCount ? ` A <span class="mono">REG</span> reference is a register filing rather than a common-law conflict — it has no Findings row, and its receipt is the register record the card links.` : ''}` : ''}</p>` : ''}
|
|
1705
|
-
</div>
|
|
1706
|
-
</details>
|
|
1816
|
+
${(() => {
|
|
1817
|
+
const outs = markOutcomes(marks);
|
|
1818
|
+
if (!outs.length) return '';
|
|
1819
|
+
const label = outs.length > 1;
|
|
1820
|
+
const body = outs.map((o) => (label && o.name ? `<h3>${esc(o.name)}</h3>` : '') + mdParagraphs(o.text)).join('');
|
|
1821
|
+
return `<div class="sec" id="next"><h2>What happens next</h2></div>
|
|
1822
|
+
<div class="panel actions"><div class="actgrp act-you">${body}</div></div>`;
|
|
1823
|
+
})()}
|
|
1824
|
+
|
|
1825
|
+
${/* The 2026-09-16 report redesign — the scope section and its fold are off the page: what a screen is and
|
|
1826
|
+
what it did not search is the narration the owner ruled out. */''}
|
|
1827
|
+
|
|
1828
|
+
${/* THE MODEL'S CAVEATS OUTLIVE THE BLOCK THEY SAT UNDER. They were rendered after the scope
|
|
1829
|
+
section, and taking that section off the page left them computed and drawn nowhere — on the
|
|
1830
|
+
committed demo run, two caveats a client used to receive, one of them the only sentence on the
|
|
1831
|
+
page saying nothing above is a finding of availability. What the owner ruled out is the fixed
|
|
1832
|
+
boilerplate, not what the engine wrote for this run, so they close the page instead.
|
|
1833
|
+
|
|
1834
|
+
THE SUBSET FILTER STAYS, and so does the text it measures against. A caveat is dropped only
|
|
1835
|
+
when every content word in it is already said elsewhere on the page, which is what the About
|
|
1836
|
+
panel and the tier line now carry; a caveat making any new claim brings a new word with it and
|
|
1837
|
+
is kept. Both of the demo run's survive. */''}
|
|
1838
|
+
${newCaveats.length ? `<div class="panel ko-caveats"><p class="ko-scope">${CAVEAT_LEAD}<br>${
|
|
1839
|
+
newCaveats.map((c) => inlineMd(c)).join('<br>')}</p></div>` : ''}
|
|
1707
1840
|
|
|
1708
1841
|
<footer>
|
|
1709
|
-
<span>${productName ? `${esc(productName)}. ` : ''}
|
|
1842
|
+
<span>${productName ? `${esc(productName)}. ` : ''}<br>Matter ${esc(matter || runId || '')}.${issued ? ` Issued ${esc(issued)}.` : ''}</span>
|
|
1710
1843
|
${logoLockup({ mark: 16 })}
|
|
1711
1844
|
</footer>
|
|
1712
1845
|
</div>
|