clearotron 0.3.2-beta.7 → 0.3.2-beta.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +24 -23
- package/INSTALL.md +142 -75
- package/README.md +3 -3
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +133 -23
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/architecture/04-configuration-reference.md +26 -11
- package/docs/architecture/05-config-governance.md +17 -7
- package/driver/CHANGELOG.md +76 -0
- package/driver/band-size.mjs +59 -0
- package/driver/config-inventory.mjs +112 -9
- package/driver/contract-arm2-baseline.json +1 -3
- package/driver/contract-e3-backlog.mjs +26 -26
- package/driver/contract-vocabulary.mjs +44 -10
- package/driver/door-gates.mjs +41 -7
- package/driver/driver.config.mjs +272 -59
- package/driver/engine/CONTRACT.md +10 -3
- package/driver/engine/README.md +2 -2
- package/driver/engine/anthropic-agent.mjs +77 -21
- package/driver/engine/auth.mjs +129 -10
- package/driver/engine/jx-turn.mjs +7 -6
- package/driver/engine/mcp/recording-server.mjs +13 -0
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/findings-model.mjs +1 -1
- package/driver/flag-snapshot.mjs +28 -5
- package/driver/gateway.mjs +24 -18
- package/driver/jx-lanes.mjs +21 -2
- package/driver/jx-units.mjs +6 -3
- package/driver/jx.mjs +4 -2
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +34 -2
- package/driver/package.json +1 -1
- package/driver/pipeline.mjs +200 -23
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +15 -1
- package/driver/portal-service.mjs +46 -6
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/publish/index.mjs +46 -5
- package/driver/publish/knockout.mjs +10 -1
- package/driver/publish/render-knockout.mjs +69 -7
- package/driver/publish/render.mjs +170 -59
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/templates/report.css +18 -1
- package/driver/publish/xlsx.mjs +13 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-coverage.mjs +94 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-plan.mjs +170 -0
- package/driver/result-noun-fields.mjs +2 -2
- package/driver/run-economics.mjs +41 -10
- package/driver/run-requirements.mjs +173 -9
- package/driver/runner.mjs +3 -3
- package/driver/stages.mjs +12 -8
- package/driver/suite-census.json +142 -64
- package/driver/systemd/README.md +7 -4
- package/driver/terminal-clamp.mjs +107 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/verify.mjs +27 -0
- package/mcp-server/CHANGELOG.md +4 -0
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +15 -1
- package/package.json +1 -1
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-6jzO9HiX.js} +155 -79
- package/portal-ui/dist/index.html +1 -1
- package/portal-ui/package.json +1 -1
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +4 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/uspto-local/README.md +1 -1
- package/scripts/authority-boundary-probe.mjs +4 -2
- package/scripts/env-audit.mjs +12 -6
- package/scripts/freeze-example-run.mjs +49 -16
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/names-in-force.mjs +1 -1
|
@@ -25,7 +25,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
25
|
// its own split, which is how the two rules diverged. It calls stripTelemetry now, so the renderer holds
|
|
26
26
|
// no copy of the RULE either, only a call to it.
|
|
27
27
|
import { parseReport, stripInternal, stripTelemetry } from './parse.mjs';
|
|
28
|
-
import { clientConditions } from '../terminal-clamp.mjs';
|
|
28
|
+
import { clientConditions } from '../terminal-clamp.mjs';
|
|
29
|
+
import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mjs'; // the export menu's shell and behaviour, shared with the knockout template // the reader's clause per condition, shared with the cover note
|
|
29
30
|
import { COMMON_LAW, normRegion, regionName, REGION_NAMES } from './regions.mjs';
|
|
30
31
|
import { parseFindingsJson, bindRecommendation, sentenceCaseLead, CLIENT_TIER_BY_COMPOSITE, bandOf, compareBlockingPower, inDispositionMode, reasonedNegativeGroups } from '../findings-model.mjs';
|
|
31
32
|
import { REC, inPriorityWindow, ownerDisplayName } from '../registry-fidelity.mjs';
|
|
@@ -59,6 +60,16 @@ let SEARCHED_JUR = null;
|
|
|
59
60
|
let SCOPE_WORLDWIDE = null;
|
|
60
61
|
// T7 — the two deterministic evidence joins (set per render from opts):
|
|
61
62
|
let CASE_LAW_BY_ORD = new Map(); // E5: ordinal → grounded case-law profile
|
|
63
|
+
// Whether the case-law pass found anything: 'found' | 'none-found' | 'not-checked' | null.
|
|
64
|
+
// The CARD strand is suppressed on the two states that mean there is no precedent to cite, because the
|
|
65
|
+
// profile body is then the engine's account of WHY — adapter names, session error codes, which sources
|
|
66
|
+
// were out of scope — a true sentence about the machinery and not the client's answer, and the Court
|
|
67
|
+
// decisions section states that outcome in the reader's own words already.
|
|
68
|
+
//
|
|
69
|
+
// NULL IS NOT ONE OF THEM. A report with no court state carries no Court decisions section either (it
|
|
70
|
+
// is full-country only), so suppressing there would take case-law off the page with nothing left
|
|
71
|
+
// saying so. Absence of a state is not a statement that nothing was found.
|
|
72
|
+
let COURT_DECISIONS = null;
|
|
62
73
|
let ENFORCER_SIGNALS = new Map(); // E6: registration uri (lowercase) → {aggression, oppositions, owner}
|
|
63
74
|
// WP-receipts W2 — per-render provider record-link origin + label, resolved by publish from the run's
|
|
64
75
|
// OWN _driver/receipts.json provider (never the currently-configured provider — a re-published archive
|
|
@@ -143,11 +154,19 @@ function frameworkTickIndex(findings) {
|
|
|
143
154
|
// verdict with EVERY condition, "Why <band>" as the four answers with the basis each rests on, the
|
|
144
155
|
// highest exposure, and what was searched to get there. Nothing here is composed — the conditions are
|
|
145
156
|
// the sidecar's own client-voice clauses, the answers are the engine's, and the coverage line is counts.
|
|
146
|
-
function ratingExtras(fm, findings, coverage, fourAnswers, opts, bandWord) {
|
|
157
|
+
function ratingExtras(fm, findings, coverage, fourAnswers, opts, bandWord, conditionsInVerdict = false) {
|
|
147
158
|
const out = [];
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
159
|
+
// THE CONDITIONS HAVE ONE HOME PER PATH, AND IT IS NEVER NONE. Where the verdict row could take
|
|
160
|
+
// them — a sidecar whose composed statement carries the "conditional on:" lede — they render there,
|
|
161
|
+
// inside the verdict, which is where the mock puts them and where they read as the terms the verdict
|
|
162
|
+
// is conditional on rather than a list beside it. Where it could not, this row renders exactly as it
|
|
163
|
+
// did: the legacy gauge has no lede to hang them from, and dropping the row there would hide every
|
|
164
|
+
// condition on precisely the archived runs that cannot be re-rendered with better text.
|
|
165
|
+
if (!conditionsInVerdict) {
|
|
166
|
+
const conds = clientConditions(VERDICT_INFO || {});
|
|
167
|
+
if (conds.length) out.push(`<div class="gconds-wrap"><span class="gk">Conditions</span><ul class="gconds">${
|
|
168
|
+
conds.map((c) => `<li>${esc(c)}</li>`).join('')}</ul></div>`);
|
|
169
|
+
}
|
|
151
170
|
const fa = fourAnswers && typeof fourAnswers === 'object' ? fourAnswers : null;
|
|
152
171
|
if (fa) {
|
|
153
172
|
const rows = FOUR_ANSWER_LABELS.map(([key, label], i) => {
|
|
@@ -240,8 +259,27 @@ function frameworkGauge(fm, findings, coverage = [], fourAnswers = null, opts =
|
|
|
240
259
|
? (bindRecommendation(fm.recommendation, VERDICT_INFO.verdict, []) || VERDICT_INFO.tier || '')
|
|
241
260
|
: (fm.recommendation || fm.overall_label || '');
|
|
242
261
|
const recLabel = VERDICT_INFO?.statement ? 'Verdict' : 'Recommendation';
|
|
262
|
+
// THE VERDICT CARRIES EVERY CONDITION, NOT THE FIRST AND A COUNT. The composed statement ends
|
|
263
|
+
// "(and N more)" because it is also a ONE-LINE surface — the email lede, the registry row — where a
|
|
264
|
+
// list cannot go. On the page there is room for all of them, and a condition a client is told exists
|
|
265
|
+
// but is not told is one they cannot act on. The lede is taken from the statement's OWN prefix rather
|
|
266
|
+
// than re-composed, so the tier wording stays the engine's and only the truncated tail is replaced;
|
|
267
|
+
// the clauses are the sidecar's client-voice ones, the same list the separate row used to carry.
|
|
268
|
+
// The legacy `gauge()` below keeps the composed line as it stands: an archived run with no framework
|
|
269
|
+
// sidecar renders byte-identically, which is the contract stated there.
|
|
270
|
+
// THE LIST RENDERS ON BOTH PATHS. A sidecar with a composed statement gets the lede and its
|
|
271
|
+
// conditions; a LEGACY one — reasons, no statement, no client-voice clause — has no lede to match, and
|
|
272
|
+
// dropping the list there would hide every condition on exactly the archived runs that cannot be
|
|
273
|
+
// re-rendered with better text. Those still render under the bound recommendation, which is where
|
|
274
|
+
// they were before this row moved.
|
|
275
|
+
const verdictConds = clientConditions(VERDICT_INFO || {});
|
|
276
|
+
const ledeMatch = typeof rec === 'string' ? rec.match(/^(.*?conditional on:)/i) : null;
|
|
277
|
+
const conditionsInVerdict = Boolean(ledeMatch && verdictConds.length);
|
|
278
|
+
const recHtml = conditionsInVerdict
|
|
279
|
+
? `${esc(ledeMatch[1])}<ul class="gconds">${verdictConds.map((c) => `<li>${esc(c)}</li>`).join('')}</ul>`
|
|
280
|
+
: esc(rec);
|
|
243
281
|
const conc = [
|
|
244
|
-
rec && `<div class="grow"><span class="gk">${recLabel}</span><span class="gv gv-rec">${
|
|
282
|
+
rec && `<div class="grow"><span class="gk">${recLabel}</span><span class="gv gv-rec">${recHtml}</span></div>`,
|
|
245
283
|
].filter(Boolean).join('');
|
|
246
284
|
// — THE FRAMEWORK IS NAMED WHERE ITS WORDS ARE READ, not only in the footer. `.ticks` below spells
|
|
247
285
|
// a vocabulary ("Manageable", "Moderate") that is meaningless without the framework in force, and the
|
|
@@ -255,7 +293,7 @@ function frameworkGauge(fm, findings, coverage = [], fourAnswers = null, opts =
|
|
|
255
293
|
<div class="scale" style="background:${grad}">${marker}</div>
|
|
256
294
|
<div class="ticks">${ticks}</div>
|
|
257
295
|
<div class="gconc">${conc}</div>
|
|
258
|
-
${ratingExtras(fm, findings, coverage, fourAnswers, opts, label)}
|
|
296
|
+
${ratingExtras(fm, findings, coverage, fourAnswers, opts, label, conditionsInVerdict)}
|
|
259
297
|
</div>`;
|
|
260
298
|
}
|
|
261
299
|
|
|
@@ -514,7 +552,6 @@ const useEvidence = (m) => [USE_EVIDENCE_LABEL[m?._status], USE_SOURCE_LABEL[m?.
|
|
|
514
552
|
// runs carry the old value forever and a fourth spelling of it would have to be accepted everywhere.
|
|
515
553
|
const USE_CHECK_NO_RESULT = 'perplexity_research — no result';
|
|
516
554
|
const USE_CHECK_NO_RESULT_CITE = 'Nothing found in the marketplaces searched.';
|
|
517
|
-
const USE_CHECK_NO_RESULT_SHORT = 'marketplace search — no result found';
|
|
518
555
|
// — MATCHED ON NORMALISED PUNCTUATION, NOT ONE SPELLING. The constant itself does not
|
|
519
556
|
// move (archived runs carry it forever, the validators name it), but the SEAT emitted a hyphen where
|
|
520
557
|
// the doctrine writes an em dash, and exact equality let the raw tool name through to a delivered
|
|
@@ -963,8 +1000,13 @@ function clearedGroupsHtml(searchDepth, auditFile) {
|
|
|
963
1000
|
const items = reg.filter((c) => c.group === g);
|
|
964
1001
|
if (!items.length) return '';
|
|
965
1002
|
const shown = items.slice(0, NAMES_CAP);
|
|
1003
|
+
// "Cl." and a lower-case status, which is how the mock reads and how a lawyer writes it. The
|
|
1004
|
+
// register hands the status back in capitals — REGISTERED, CANCELLED — and shouting a neutral
|
|
1005
|
+
// fact at a reader is the register's habit, not ours. Only the case changes; the word is the
|
|
1006
|
+
// register's own and is not translated.
|
|
966
1007
|
const rows = shown.map((c) => `<div class="crow"><span class="cm-mark">${esc(c.mark || c.term || '')}</span><span class="cwho">${
|
|
967
|
-
esc([c.owner, regionName(c.country) || c.country, c.classes ? `
|
|
1008
|
+
esc([c.owner, regionName(c.country) || c.country, c.classes ? `Cl. ${c.classes}` : '',
|
|
1009
|
+
c.status ? String(c.status).toLowerCase() : ''].filter(Boolean).join(' \u00b7 '))}</span></div>`).join('');
|
|
968
1010
|
return `<details class="cgroup"><summary><span class="gname">${esc(CLEARED_GROUP_LABEL[g] || g)}</span><span class="gcount">${
|
|
969
1011
|
items.length.toLocaleString('en-GB')} ${items.length === 1 ? 'name' : 'names'}</span></summary><div class="gbody">${rows}${
|
|
970
1012
|
items.length > shown.length ? link(items.length) : ''}</div></details>`;
|
|
@@ -1011,7 +1053,14 @@ function whereItStandsSection(findings, opts) {
|
|
|
1011
1053
|
const withF = [], clean = [];
|
|
1012
1054
|
for (const c of codes) {
|
|
1013
1055
|
const key = alias[c] || c;
|
|
1014
|
-
|
|
1056
|
+
// THE NAME IS LOOKED UP ON THE ALIASED KEY, NOT THE RAW CODE. The register writes the EUIPO and
|
|
1057
|
+
// ISO spellings — EM and GB — and `alias` maps those to the codes a reader knows, EU and UK. The
|
|
1058
|
+
// name was resolved from the RAW code, which has no entry under either spelling, so it fell back
|
|
1059
|
+
// to the code itself and the row read "EU EM" and "UK GB": a country column printing a second
|
|
1060
|
+
// code, beside the four rows where the register happened to write the code we already knew.
|
|
1061
|
+
// The raw code is still tried, so anything the alias does not cover resolves exactly as before.
|
|
1062
|
+
const name = regionName(key) || regionName(c) || key;
|
|
1063
|
+
(bandBy.has(c) || bandBy.has(key) ? withF : clean).push({ code: key, name, band: bandBy.get(c) || bandBy.get(key) });
|
|
1015
1064
|
}
|
|
1016
1065
|
if (!withF.length && !clean.length) return '';
|
|
1017
1066
|
const rows = withF.map((c) => `<div class="wrow"><span class="rcode">${esc(c.code)}</span><span class="wname">${esc(c.name)}</span><span class="kc">${esc(c.band)}</span></div>`).join('');
|
|
@@ -1082,7 +1131,16 @@ function alsoConsideredSection(ruledOut, recordsByUri = new Map(), opts = {}) {
|
|
|
1082
1131
|
const sd = opts.searchDepth || null;
|
|
1083
1132
|
const ruledCards = ruledOut.map((f) => {
|
|
1084
1133
|
const who = recordOwner(f, recordsByUri) || f.owner?.name || '';
|
|
1085
|
-
|
|
1134
|
+
// THE ENGINE'S OWN SENTENCE LEADS THE CARD. The face printed a fixed line — "a different name in
|
|
1135
|
+
// a related field" — on every ruled-out card, whatever the run had actually concluded. `net` is the
|
|
1136
|
+
// finding's own one-line reason, written for a reader, and it was rendered NOWHERE: not on the
|
|
1137
|
+
// face, not in the fold, not anywhere on the page. So a client read the same generic sentence
|
|
1138
|
+
// about every name we set aside, while the specific reason we set THAT one aside was discarded at
|
|
1139
|
+
// render time. The fold keeps the legal and practical positions, which are the longer argument.
|
|
1140
|
+
// The fixed line stays as the last resort, for a finding that carries neither.
|
|
1141
|
+
const why = f.ruled_out_reason ? esc(f.ruled_out_reason)
|
|
1142
|
+
: f.net ? esc(String(f.net))
|
|
1143
|
+
: 'a different name in a related field — not a conflict with your mark';
|
|
1086
1144
|
const legal = f.legal_position ? esc(String(f.legal_position)) : '';
|
|
1087
1145
|
const practical = f.practical_position ? esc(String(f.practical_position)) : '';
|
|
1088
1146
|
const fold = (legal || practical)
|
|
@@ -1284,36 +1342,19 @@ function keyPanel(findings, recordsByUri = new Map()) {
|
|
|
1284
1342
|
// complete in one place. The rights-holder landscape panel keeps its "Common-law" group — that
|
|
1285
1343
|
// panel is the index of everything, this section is the reading surface.
|
|
1286
1344
|
function commonLawSection(clSecondary, clOnField, cardFor, recordsByUri = new Map(), allFindings = []) {
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1289
|
-
//
|
|
1290
|
-
//
|
|
1291
|
-
// the
|
|
1292
|
-
//
|
|
1293
|
-
//
|
|
1294
|
-
|
|
1295
|
-
.filter((f) => f && f.disposition !== 'withdrawn' && f.use_check?.source && regionCode(f) !== COMMON_LAW)
|
|
1296
|
-
.map((f) => {
|
|
1297
|
-
// D4 — the evidence pair is LABELLED here too, and it reads out of the one USE_SOURCE_LABEL.
|
|
1298
|
-
const st = useEvidence(f.meters?.use);
|
|
1299
|
-
// D7 — the sentinel is mapped to client words BEFORE the URL parse is attempted. It is not a
|
|
1300
|
-
// URL, so `new URL` threw and the catch printed `host.slice(0, 40)` — and the sentinel is 31
|
|
1301
|
-
// characters, so the page printed the raw tool name, whole.
|
|
1302
|
-
const raw = String(f.use_check.source);
|
|
1303
|
-
let where;
|
|
1304
|
-
if (isUseCheckNoResult(raw)) where = USE_CHECK_NO_RESULT_SHORT;
|
|
1305
|
-
else { try { where = new URL((raw.match(/https?:\/\/[^\s,|]+/) || [raw])[0]).host; } catch { where = raw.slice(0, 40); } }
|
|
1306
|
-
return `<li style="margin:3px 0"><a href="#c${f.ordinal}">#${f.ordinal} ${esc(f.mark)}</a> — use ${esc(humanize(f.meters?.use?.token ?? 'unknown'))} — ${esc(where)}${st ? ` <i class="evstat">(evidence: ${esc(st)})</i>` : ''}</li>`;
|
|
1307
|
-
}).join('');
|
|
1308
|
-
const contribBlock = contrib
|
|
1309
|
-
? `<p style="margin:4px 0 2px;font-size:13px"><b>What the marketplace layer added to register findings</b></p><ul style="margin:0 0 10px;padding-left:20px;font-size:13px">${contrib}</ul>`
|
|
1310
|
-
: '';
|
|
1311
|
-
if (!clSecondary.length && !clOnField.length && !contribBlock) return '';
|
|
1345
|
+
// THE LAYER'S CONTRIBUTION IS ON THE CARD THAT CARRIES IT, NOT ALSO IN A LIST ABOVE THEM. A
|
|
1346
|
+
// block headed "what the marketplace layer added to register findings" restated, per finding, the
|
|
1347
|
+
// use token, the host and the evidence pair that the finding's OWN card already states in its use
|
|
1348
|
+
// line — measured on the delivered reports: one such line per finding with use evidence, on the
|
|
1349
|
+
// card, in every case the block listed. It attributed a layer to itself in the engine's own terms
|
|
1350
|
+
// and made a reader read the same fact twice, the second time out of the context that explains it.
|
|
1351
|
+
// Nothing is lost with it: the cards it linked to sit directly below.
|
|
1352
|
+
if (!clSecondary.length && !clOnField.length) return '';
|
|
1312
1353
|
const links = clOnField.length
|
|
1313
1354
|
? `<p class="clx" style="margin:4px 0 10px;font-size:13.5px">On-field common-law conflicts (full cards above): ${clOnField.map(f => `<a href="#c${f.ordinal}">#${f.ordinal} ${esc(f.mark)}</a>`).join(' · ')}</p>`
|
|
1314
1355
|
: '';
|
|
1315
1356
|
const cards = clSecondary.map(f => compactCard(f, cardFor(f), recordsByUri)).join('\n ');
|
|
1316
|
-
return
|
|
1357
|
+
return links + cards;
|
|
1317
1358
|
}
|
|
1318
1359
|
|
|
1319
1360
|
// Secondary findings → collapsible region groups (§2.4). Same region order as the key panel; each region a
|
|
@@ -1632,11 +1673,20 @@ function fullDetail(f, card, recordsByUri = new Map()) {
|
|
|
1632
1673
|
// preparation is portal-report.mjs's job, never a second render fork here.
|
|
1633
1674
|
const clHead = clProfile ? `<b>Case-law${clProfile.jurisdiction ? ` (${esc(clProfile.jurisdiction)})` : ''}.</b>` : '';
|
|
1634
1675
|
const clBody = !clProfile ? '' : renderProse(clProfile.body); // one report: the full body (renderProse handles ::p:: internal lines)
|
|
1635
|
-
|
|
1676
|
+
// Only a pass that FOUND precedent puts a strand on the card. Where none was found, or the research
|
|
1677
|
+
// could not be completed, the body is the engine's account of the attempt and the Court decisions
|
|
1678
|
+
// section says the outcome plainly on its own; repeating it here said it twice, the second time in
|
|
1679
|
+
// the machinery's voice on the card a client reads most closely.
|
|
1680
|
+
const caseLawStrand = clProfile && COURT_DECISIONS !== 'not-checked' && COURT_DECISIONS !== 'none-found'
|
|
1636
1681
|
? `<div class="clstrand" style="margin:10px 0 0;padding-top:8px;border-top:1px dashed var(--line,#ddd)">${clHead}${clBody}</div>`
|
|
1637
1682
|
: '';
|
|
1638
1683
|
const link = f.source?.resolved_link;
|
|
1639
|
-
|
|
1684
|
+
// NO AUDIT REFERENCE ON A CLIENT'S CARD. "audit ref F1" is the engine's handle for the finding —
|
|
1685
|
+
// it indexes the workbook, it means nothing to the reader holding the report, and the mock carries no
|
|
1686
|
+
// such line. The SOURCE LINK stays: it is the only address a reader has for the record on this card
|
|
1687
|
+
// until the workbook row lands beside it, and dropping both would take a fact away rather than a
|
|
1688
|
+
// label. With no link there is nothing left to say, so the row does not render at all.
|
|
1689
|
+
const prov = link ? `<div class="prov"><a href="${esc(link)}" target="_blank" rel="noopener noreferrer">${esc(link.replace(/^https?:\/\//, '').slice(0, 48))}</a></div>` : '';
|
|
1640
1690
|
// WP-receipts W4 — the code-owned senior-right line (Owner decision 2026-07-05: VERY SIMPLE CLEAR ENGLISH,
|
|
1641
1691
|
// stated qualification, verdict untouched). Verified senior → nothing extra (the W2 receipt line on
|
|
1642
1692
|
// the fetched leg is the proof). Unverified → the open item, plainly, where the finding lives.
|
|
@@ -2059,8 +2109,7 @@ window.addEventListener('hashchange',_cardHashGo);
|
|
|
2059
2109
|
window.addEventListener('load',_cardHashGo);
|
|
2060
2110
|
window.addEventListener('beforeprint',function(){document.querySelectorAll('details').forEach(function(d){d.dataset.o=d.open?'1':'';d.open=true;});});
|
|
2061
2111
|
window.addEventListener('afterprint',function(){document.querySelectorAll('details').forEach(function(d){d.open=d.dataset.o==='1';});_hidden.forEach(function(c){c.classList.remove('print-hidden');});_hidden=[];});
|
|
2062
|
-
|
|
2063
|
-
document.addEventListener('keydown',function(e){if(e.key==='Escape'){var pop=document.querySelector('.tb-exp-pop');if(pop&&!pop.hidden){pop.hidden=true;var b=document.querySelector('.tb-exp-toggle');if(b)b.setAttribute('aria-expanded','false');}}});`;
|
|
2112
|
+
${EXPORT_MENU_JS}`;
|
|
2064
2113
|
|
|
2065
2114
|
// Render a parsed report + findings.json to a full self-contained HTML string.
|
|
2066
2115
|
// A1 — famous-neighbour context notes: knowledge-cited references kept for diligence (digest.md's "never
|
|
@@ -2292,6 +2341,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2292
2341
|
SEARCHED_JUR = Array.isArray(opts.searchedJurisdictions) && opts.searchedJurisdictions.length ? opts.searchedJurisdictions : null; // T6 (D4)
|
|
2293
2342
|
SCOPE_WORLDWIDE = opts.scopeBasis === 'worldwide' ? true : null; // the plan's scope_basis; null ⇒ fall back to the ledger-prose sniff
|
|
2294
2343
|
CASE_LAW_BY_ORD = opts.caseLawByOrdinal instanceof Map ? opts.caseLawByOrdinal : new Map(); // T7 (E5)
|
|
2344
|
+
COURT_DECISIONS = opts.searchDepth?.counts?.courtDecisions ?? null; // gates the card's case-law strand
|
|
2295
2345
|
ENFORCER_SIGNALS = new Map((Array.isArray(opts.enforcerSignals) ? opts.enforcerSignals : []).map((e) => [String(e.uri ?? '').toLowerCase(), e])); // T7 (E6)
|
|
2296
2346
|
RECORD_ORIGIN = opts.recordOrigin ?? null; // WP-receipts W2
|
|
2297
2347
|
// — `null` and `` mean DIFFERENT things and the render must not collapse them. `` is an
|
|
@@ -2530,11 +2580,20 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2530
2580
|
<span class="sp"></span>
|
|
2531
2581
|
<span class="tb-risk" style="background:var(${STOP_VAR[i]})">${riskLabel}</span>
|
|
2532
2582
|
<span class="mono tb-matter" style="font-size:11px;color:var(--faint)">${esc(fm.matter || opts.runId || '')}${fm.title ? ' / ' + esc(fm.title) : ''}</span>
|
|
2533
|
-
${
|
|
2583
|
+
${/* A DATE, NOT A TIMESTAMP. This read "Issued on 2026-09-17 · 08:36 GMT+2" — the minute the file
|
|
2584
|
+
was written, and the zone the machine that wrote it happened to be in. Neither tells a reader
|
|
2585
|
+
anything, and on work that spans days it implies a precision the work does not have. Every mock
|
|
2586
|
+
issues the date alone. Taken by PATTERN rather than by cutting at the separator, so a value in
|
|
2587
|
+
some other shape falls through whole instead of being truncated at whatever character sits
|
|
2588
|
+
there — an archived run's stamp is not this publisher's to assume. */''}
|
|
2589
|
+
${(() => {
|
|
2590
|
+
const d = (String(opts.issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? opts.issued;
|
|
2591
|
+
return d ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued on ${esc(d)}</span>` : '';
|
|
2592
|
+
})()}
|
|
2534
2593
|
<button type="button" class="tbbtn tb-ask no-print">✦ <span class="tb-lbl">Ask AI</span></button>
|
|
2535
2594
|
<div class="tb-menu">
|
|
2536
|
-
|
|
2537
|
-
|
|
2595
|
+
${EXPORT_TOGGLE}
|
|
2596
|
+
${exportPopover(`
|
|
2538
2597
|
<div class="tb-pop-title">Export & audit</div>
|
|
2539
2598
|
<button class="util primary" onclick="exportPDF()">⬇ Export PDF (ticked findings)</button>
|
|
2540
2599
|
${excelBtn2}
|
|
@@ -2542,7 +2601,7 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2542
2601
|
<div class="tb-row"><button class="util" onclick="pickAll(true)">Select all</button><button class="util" onclick="pickAll(false)">Select none</button></div>
|
|
2543
2602
|
<div class="tb-row"><button class="util" onclick="openAll(true)">Expand all</button><button class="util" onclick="openAll(false)">Collapse all</button></div>
|
|
2544
2603
|
<p class="tb-hint">Tick a finding to keep it in the exported PDF; untick to drop it. Internal (review-only) notes are removed on export.</p>
|
|
2545
|
-
|
|
2604
|
+
`)}
|
|
2546
2605
|
</div>
|
|
2547
2606
|
</div>
|
|
2548
2607
|
</div>
|
|
@@ -2598,20 +2657,51 @@ export function renderHtml(parsed, findings = [], coverage = [], opts = {}) {
|
|
|
2598
2657
|
${whatWasSearchedSection(opts, coverage, findings, recordsByUri)}
|
|
2599
2658
|
|
|
2600
2659
|
<footer>
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2660
|
+
${/* ONE CLIENT LINE, AND IT NAMES WHAT EACH DATE IS. It ran to four.
|
|
2661
|
+
"Rated under" STAYS, and it is not an oversight: the document carries the reviewer's
|
|
2662
|
+
provenance and portal-report strips that one line for every embedded reader at serve time,
|
|
2663
|
+
which is stated there and held by an arm. Deleting it here would take provenance off the
|
|
2664
|
+
reviewer's copy to save the portal a job it already does.
|
|
2665
|
+
"Run under project" also STAYS on the document, for the same reason and with the same
|
|
2666
|
+
remedy. It is internal provenance — an end-to-end arm reads it out of the published internal
|
|
2667
|
+
report, through the front-matter seam — and it was reaching clients only because nothing
|
|
2668
|
+
removed it at serve time. It does now, beside the provenance line, so the document keeps what
|
|
2669
|
+
review needs and the client reads neither. Deleting it here would have taken provenance off
|
|
2670
|
+
the internal copy to fix a leak that belongs in the same place the other one is fixed.
|
|
2671
|
+
The product name lead goes: the identity line already says which search this is.
|
|
2672
|
+
The run string is composed as "<date> · <provider>", and it was printed with neither part
|
|
2673
|
+
labelled, so a bare date sat next to a matter identifier meaning nothing in particular. It is
|
|
2674
|
+
split on the DATE by pattern — the same way `dateOf` does it one file over — and the remainder
|
|
2675
|
+
is the provider. A run string with no date in it is not this renderer's to take apart, so it
|
|
2676
|
+
is printed whole and unlabelled rather than guessed at. */''}
|
|
2677
|
+
<!-- WHICH MODELS SERVED THIS RUN. It used to close the scope fold, and the 2026-09-16 redesign
|
|
2678
|
+
deleted that fold — so the line was re-homed rather than dropped with its container, which
|
|
2679
|
+
would have removed a statement of provenance from the client's page as a side effect of a
|
|
2680
|
+
merge. Owner ruling, 2026-09-17: it belongs in the footer, beside the matter and the framework.
|
|
2681
|
+
It renders as '' on a run that recorded no models, so an archived run republishes exactly as
|
|
2682
|
+
delivered, and the call sits ADJACENT so the whitespace around it belongs to the line and goes
|
|
2683
|
+
with it — the freeze tool sets this paragraph aside by a pattern that takes the space before
|
|
2684
|
+
it. -->${servedModelsLine(opts.servedModels)}
|
|
2685
|
+
<span>${(() => {
|
|
2686
|
+
const runStr = String(fm.run ?? '').trim();
|
|
2687
|
+
const searchedOn = (runStr.match(/\d{4}-\d{2}-\d{2}/) || [])[0] ?? null;
|
|
2688
|
+
const provider = searchedOn
|
|
2689
|
+
? runStr.replace(searchedOn, '').replace(/^[\s\u00b7]+|[\s\u00b7]+$/g, '').trim()
|
|
2690
|
+
: runStr;
|
|
2691
|
+
const issuedOn = (String(opts.issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? opts.issued ?? null;
|
|
2692
|
+
const parts = [
|
|
2693
|
+
fm.matter ? `Matter ${esc(fm.matter)}` : '',
|
|
2694
|
+
searchedOn ? `searched on ${esc(searchedOn)}` : '',
|
|
2695
|
+
issuedOn ? `issued on ${esc(issuedOn)}` : '',
|
|
2696
|
+
provider ? esc(provider) : '',
|
|
2697
|
+
].filter(Boolean);
|
|
2698
|
+
const line = parts.length ? `${parts.join(' \u00b7 ')}.` : '';
|
|
2699
|
+
// The reviewer's provenance rides BELOW the client line, where portal-report removes it.
|
|
2700
|
+
// Both provenance lines ride BELOW the client line, where portal-report removes them.
|
|
2701
|
+
return line
|
|
2702
|
+
+ (fm.rated_under ? `<br>Rated under: <span class="mono">${esc(fm.rated_under)}</span>.` : '')
|
|
2703
|
+
+ (fm.run_under_project ? `<br>Run under project: <span class="mono">${esc(fm.run_under_project)}</span>.` : '');
|
|
2704
|
+
})()}</span>
|
|
2615
2705
|
${logoLockup({ mark: 16 })}
|
|
2616
2706
|
</footer>
|
|
2617
2707
|
</div>
|
|
@@ -2658,3 +2748,24 @@ function officeLinkNote() {
|
|
|
2658
2748
|
return (linked ? 'A registration number shown as a link opens the office’s own page for that record. ' : '')
|
|
2659
2749
|
+ officeReasonSentences(RECORD_LINKS).map((s) => `${esc(s)} `).join('');
|
|
2660
2750
|
}
|
|
2751
|
+
|
|
2752
|
+
// THE MODELS THAT SERVED THIS SEARCH, as one closing line of the scope section (2026-09-14). The ids are
|
|
2753
|
+
// the ones the engine reported for its turns (tokens.mjs servedModels), never the tier a stage asked
|
|
2754
|
+
// for in place of a model the engine named: every tier goes to the program as the vendor's alias, and an
|
|
2755
|
+
// alias names no model. Both report
|
|
2756
|
+
// kinds call this, so they say it in the same words. '' when the run recorded none, so a run published
|
|
2757
|
+
// before the record existed renders exactly as it was delivered.
|
|
2758
|
+
//
|
|
2759
|
+
// A TIER WORD IS CLAUDE'S. servedModels lists a turn served under a company's own deployment name as the
|
|
2760
|
+
// tier it asked for ("Opus"), never the name, so the list may read "claude-opus-5, Haiku". Both are
|
|
2761
|
+
// Claude's and the line says so once; in a list that also names another vendor, the word says it itself
|
|
2762
|
+
// ("Claude Opus"). The four words, Fable among them, are the ones servedModels writes. They are kept here
|
|
2763
|
+
// rather than imported, because tokens.mjs loads the driver's settings and this module renders without them.
|
|
2764
|
+
const CLAUDE_TIER_WORD_RE = /^(?:Opus|Sonnet|Haiku|Fable)$/;
|
|
2765
|
+
export function servedModelsLine(ids) {
|
|
2766
|
+
const list = (Array.isArray(ids) ? ids : []).map((s) => String(s ?? '').trim()).filter(Boolean);
|
|
2767
|
+
if (!list.length) return '';
|
|
2768
|
+
const claude = list.every((id) => /^claude-/i.test(id) || CLAUDE_TIER_WORD_RE.test(id));
|
|
2769
|
+
const shown = claude ? list : list.map((id) => (CLAUDE_TIER_WORD_RE.test(id) ? `Claude ${id}` : id));
|
|
2770
|
+
return `<p class="servedby" style="margin:10px 0 0;font-size:13px">Prepared with${claude ? ' Claude' : ''}: ${shown.map(esc).join(', ')}.</p>`;
|
|
2771
|
+
}
|
|
@@ -68,7 +68,7 @@ function assessmentField(v) {
|
|
|
68
68
|
* "no data file this publish" and stamps meta.reportSchema only on success.
|
|
69
69
|
*/
|
|
70
70
|
export function clearanceReportData({
|
|
71
|
-
runId, codename, matter, markName, title, customerKey, issued, url, auditFile, engineCommit = null,
|
|
71
|
+
runId, codename, matter, markName, title, customerKey, issued, url, auditFile, engineCommit = null, servedModels = null,
|
|
72
72
|
searchLevel, stageLabel, framework, verdictInfo, findings, coverage, contextNotes,
|
|
73
73
|
markAssessment, fourAnswers, askAnswers, actions, jurisdiction, searchedJurisdictions, scopeBasis, caption,
|
|
74
74
|
} = {}) {
|
|
@@ -91,6 +91,9 @@ export function clearanceReportData({
|
|
|
91
91
|
issued: issued || null,
|
|
92
92
|
// The engine build that produced this report — the join from a flagged finding to a diff.
|
|
93
93
|
engineCommit: engineCommit || null,
|
|
94
|
+
// The models that served the run, in first-use order, as tokens.mjs servedModels names them for a
|
|
95
|
+
// client: a deployment's own name never, its tier instead. null when nothing was read.
|
|
96
|
+
servedModels: Array.isArray(servedModels) ? servedModels : null,
|
|
94
97
|
url: url || null,
|
|
95
98
|
auditFile: auditFile || null,
|
|
96
99
|
level: { searchLevel: searchLevel ?? null, stageLabel: stageLabel ?? null },
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
// Copyright 2026 Cordillera Sàrl. Additional terms under section 7 of the AGPL-3.0 apply — see ADDITIONAL-TERMS.md
|
|
3
|
+
// report-topbar.mjs — THE EXPORT MENU, ONCE, FOR BOTH REPORT TEMPLATES.
|
|
4
|
+
//
|
|
5
|
+
// Two templates draw a report: the clearance renderer and the knockout renderer. Each emits its own top
|
|
6
|
+
// bar, which is right — the boards draw two different bars, with different items in a different order.
|
|
7
|
+
// What they must not each own is the EXPORT MENU: the toggle, the popover it opens, the aria wiring that
|
|
8
|
+
// says it is a menu, and the two listeners that open and close it. Those are one control, and until this
|
|
9
|
+
// module they were two copies of one control, in two files, with nothing to say when they drifted.
|
|
10
|
+
//
|
|
11
|
+
// ── WHAT IS SHARED AND WHAT IS NOT, AND WHY THE LINE IS THERE ───────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// The SHELL is shared: the button, the panel, the behaviour. The ENTRIES are not. The clearance report
|
|
14
|
+
// filters its export to the findings a reader has ticked, so its menu says so and offers a select-all;
|
|
15
|
+
// the knockout has no tick boxes at all, deliberately, because it has nothing to filter, and a menu
|
|
16
|
+
// offering to tick there would name a control that cannot exist. An entry list is a statement about what
|
|
17
|
+
// a template can do, and the two templates can do different things.
|
|
18
|
+
//
|
|
19
|
+
// ── THE BYTES DO NOT MOVE ───────────────────────────────────────────────────────────────────────────
|
|
20
|
+
//
|
|
21
|
+
// `driver/publish/render.mjs` is frozen at a content hash, and the freeze's checklist asks whether a
|
|
22
|
+
// change is reachable from a republish. This one is: pool-admin re-renders archived runs through that
|
|
23
|
+
// module. So the contract here is stricter than "it looks the same" — the composed markup is the bytes
|
|
24
|
+
// the two templates already emitted, character for character, and the commit that introduces this module
|
|
25
|
+
// records a real archived run rendered through both the old module and the new one and byte-compared, in
|
|
26
|
+
// the internal pass and the client pass. A shared definition that changed a delivered report while
|
|
27
|
+
// tidying it up would be the worst of both.
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The button that opens the export menu. One spelling, both templates.
|
|
31
|
+
*
|
|
32
|
+
* `aria-haspopup` and `aria-expanded` are part of the control rather than decoration: the listener below
|
|
33
|
+
* keeps `aria-expanded` in step, and a second copy of this button that forgot either would announce
|
|
34
|
+
* itself to a screen reader as an ordinary button that does nothing.
|
|
35
|
+
*/
|
|
36
|
+
export const EXPORT_TOGGLE = '<button type="button" class="tbbtn primary tb-exp-toggle" aria-haspopup="true" aria-expanded="false">⬇ <span class="tb-lbl">Export</span> ▾</button>';
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The panel the toggle opens, around whatever entries a template offers. PURE.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} entries the template's own menu contents, already escaped
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export const exportPopover = (entries) => `<div class="tb-pop tb-exp-pop" hidden>${entries}</div>`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Open on the button, close on a click outside and on Escape.
|
|
48
|
+
*
|
|
49
|
+
* GLOBAL, NOT WRAPPED, and that is the same reason the templates' own verbs are global: the portal
|
|
50
|
+
* frames a served report and drives it by looking names up on the page. These two listeners need no
|
|
51
|
+
* name, but they sit in the same script as the verbs that do, and a wrapper around the pair would be one
|
|
52
|
+
* more difference between the two files for no gain.
|
|
53
|
+
*
|
|
54
|
+
* NO BACKTICK IN THIS STRING. It is interpolated into a template literal in both templates, and a
|
|
55
|
+
* backtick ends that literal — the failure arrives at import time naming a token nobody wrote.
|
|
56
|
+
*/
|
|
57
|
+
export const EXPORT_MENU_JS = `document.addEventListener('click',function(e){var t=e.target.closest('.tb-exp-toggle'),pop=document.querySelector('.tb-exp-pop');if(t){if(pop){pop.hidden=!pop.hidden;t.setAttribute('aria-expanded',String(!pop.hidden));}return;}if(pop&&!pop.hidden&&!e.target.closest('.tb-exp-pop')){pop.hidden=true;var b=document.querySelector('.tb-exp-toggle');if(b)b.setAttribute('aria-expanded','false');}});
|
|
58
|
+
document.addEventListener('keydown',function(e){if(e.key==='Escape'){var pop=document.querySelector('.tb-exp-pop');if(pop&&!pop.hidden){pop.hidden=true;var b=document.querySelector('.tb-exp-toggle');if(b)b.setAttribute('aria-expanded','false');}}});`;
|
|
@@ -239,7 +239,19 @@
|
|
|
239
239
|
.tb-hint{font-size:11px;color:var(--faint);line-height:1.4;margin:2px 2px 0}
|
|
240
240
|
@media(max-width:860px){.tb-matter{display:none}}
|
|
241
241
|
@media(max-width:640px){.tb-issued{display:none}}
|
|
242
|
-
@media(max-width:560px){.wm small{display:none}.tb-back-lbl{display:none}.tb-lbl{display:none}
|
|
242
|
+
@media(max-width:560px){.wm small{display:none}.tb-back-lbl{display:none}.tb-lbl{display:none}
|
|
243
|
+
/* THE SAME ROW, CLOSER TOGETHER — the ladder above already collapses this bar's LABELS at these
|
|
244
|
+
widths, and it was still 35 to 51px wider than a phone screen, so the whole document scrolled
|
|
245
|
+
sideways: the reader dragged the page left and right to read a paragraph. Nothing is hidden and
|
|
246
|
+
nothing moves; the gutter, the gaps and one dead margin come in. The margin is dead here by the
|
|
247
|
+
line above — it separated the wordmark from a tagline that is no longer displayed. */
|
|
248
|
+
.topbar{gap:4px;padding-left:12px;padding-right:12px}}
|
|
249
|
+
/* AND ON A SMALL PHONE, ONE THING GOES. Closing 375px and below is not a spacing problem — the row's
|
|
250
|
+
own items are wider than the screen — so something has to go, and which one is a decision about what
|
|
251
|
+
a reader keeps rather than a number to tune. Ruled: the back arrow. The product's name and the risk
|
|
252
|
+
label stay, because they say where the reader is and what the answer was; the arrow is a route back
|
|
253
|
+
to a list, and a reader on a phone has the browser's own. (owner, 2026-09-17) */
|
|
254
|
+
@media(max-width:375px){.topbar .tb-back{display:none}}
|
|
243
255
|
|
|
244
256
|
/* hero: conclusion card (dial + verdict) + scope card jurisdiction chips */
|
|
245
257
|
.heroGrid .gauge{display:flex;flex-direction:column;justify-content:flex-start}
|
|
@@ -513,3 +525,8 @@
|
|
|
513
525
|
.openrows .rk,.provwrap .rk{font-size:10.5px;letter-spacing:.12em;text-transform:uppercase;color:var(--faint);margin-bottom:6px}
|
|
514
526
|
.provnote{margin:0;font-size:12.5px;line-height:1.5;color:var(--slate)}
|
|
515
527
|
.topbar .tb-lockup{margin-right:10px;flex:0 0 auto}
|
|
528
|
+
/* AFTER the rule it overrides, not beside its siblings at the breakpoint above: both selectors carry
|
|
529
|
+
the same weight, so the later one wins and a phone rule written earlier in this file did nothing at
|
|
530
|
+
all — measured, not assumed. The margin separates the wordmark from the tagline, and the tagline is
|
|
531
|
+
already hidden at this width. */
|
|
532
|
+
@media(max-width:560px){.topbar .tb-lockup{margin-right:0}}
|
package/driver/publish/xlsx.mjs
CHANGED
|
@@ -618,7 +618,19 @@ export async function buildAudit(contract, auditParsed, outPath, mark = '', fm =
|
|
|
618
618
|
});
|
|
619
619
|
|
|
620
620
|
// 4 · Coverage & gaps — the honest completeness ledger, state coloured.
|
|
621
|
-
|
|
621
|
+
// ── A CONDITION THAT REACHED NO PAGE IS A GAP, AND IT BELONGS ON THE GAPS SHEET ──────────────────
|
|
622
|
+
//
|
|
623
|
+
// A report republished from a run recorded before conditions carried two texts can hold a condition
|
|
624
|
+
// whose reader-facing sentence was never stored and cannot be composed. It is dropped rather than
|
|
625
|
+
// printed in the engine's own words, and a drop nobody records is the disclosure closing quietly —
|
|
626
|
+
// which is worse than the sentence it replaced. Ruled 2026-09-17: one row here, one line in the run
|
|
627
|
+
// record, no new wording, and delivery never fails for it.
|
|
628
|
+
//
|
|
629
|
+
// THE ROW IS AN ORDINARY COVERAGE ROW, built by the same function as every other, so it carries the
|
|
630
|
+
// same four columns and takes the same State colour. It is not a second shape and not a new sheet.
|
|
631
|
+
// The words in it are the run record's own: nothing here composes prose.
|
|
632
|
+
addSheet(wb, 'Coverage & gaps', COVERAGE_COLS,
|
|
633
|
+
[...coverageRows(coverage), ...coverageRows(contract?.droppedConditions || [])], (row, _d, kept) => {
|
|
622
634
|
if (!kept.has('State')) return;
|
|
623
635
|
const st = row.getCell('State'); const f = STATE_FILL[String(st.value).trim()];
|
|
624
636
|
if (f) { st.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF' + f } }; st.font = { bold: true }; }
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
// CORRECTED 2026-08-11 — THIS PARAGRAPH USED TO SAY "one qid per office … which is the shape
|
|
42
42
|
// joinPlanToBands already has", AND THAT WAS FALSE ABOUT THE COMPILER IT DOCUMENTS. There is no
|
|
43
43
|
// per-office qid. compileRegisterPlan narrows ONE shared `regions` array and hands it to every entry
|
|
44
|
-
// (register-plan.mjs:
|
|
44
|
+
// (register-plan.mjs:779 compileRegisterPlan); the unreachable office produces no entry, so no qid, so no band block, so
|
|
45
45
|
// nothing ever reaches joinPlanToBands' deferred bucket — whose only source is a block stamped
|
|
46
|
-
// `error:true && deferred:true` (register-plan.mjs:
|
|
46
|
+
// `error:true && deferred:true` (register-plan.mjs:1317 extendRegisterPlan).
|
|
47
47
|
//
|
|
48
48
|
// The consequence was not academic. `deferred_coverage` rode the plan and nothing that a reader sees
|
|
49
49
|
// read it: coverage-form.mjs seeded its deferred rows from skeleton qids alone, so an EU+US matter on a
|