clearotron 0.3.2-beta.7 → 0.3.2-beta.9
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 +58 -26
- package/CONTRIBUTING.md +8 -8
- package/INSTALL.md +148 -81
- package/README.md +3 -3
- package/SECURITY.md +3 -3
- package/bin/brandowner.mjs +3 -3
- package/bin/framework-preflight.mjs +1 -1
- package/bin/onboard.mjs +637 -216
- package/bin/start.mjs +151 -27
- package/bin/update.mjs +58 -11
- package/build-info.json +2 -2
- package/docs/DELIVERY.md +2 -1
- package/docs/INTAKE.md +1 -1
- package/docs/ONBOARDING.md +1 -1
- package/docs/architecture/03-run-lifecycle.md +6 -6
- package/docs/architecture/04-configuration-reference.md +32 -14
- package/docs/architecture/05-config-governance.md +23 -8
- package/docs/architecture/05-customer-profiles.md +2 -2
- package/docs/architecture/06-operations-runbook.md +3 -3
- package/docs/architecture/08-development-guide.md +6 -6
- package/docs/configuration.md +5 -5
- package/docs/decisions/0003-credential-model.md +1 -1
- package/docs/writing-standard.md +4 -0
- package/driver/CHANGELOG.md +124 -0
- package/driver/README.md +3 -3
- package/driver/band-size.mjs +59 -0
- package/driver/binding-layers.mjs +1 -1
- package/driver/citation-census.json +3 -3
- package/driver/{prelim-variants-record.mjs → clearance-variants-record.mjs} +24 -24
- package/driver/common-law-receipts.mjs +2 -2
- package/driver/company-bundle.mjs +3 -3
- package/driver/compose-read.mjs +8 -14
- package/driver/config-inventory.mjs +112 -9
- package/driver/consumption-ledger.mjs +2 -2
- package/driver/contract-arm2-baseline.json +2 -5
- package/driver/contract-dictation-registry.mjs +19 -19
- package/driver/contract-e3-backlog.mjs +43 -43
- package/driver/contract-e3-baseline.json +14 -14
- package/driver/contract-vocabulary.mjs +68 -27
- package/driver/deliver-trigger.sh +16 -16
- package/driver/demo-container.mjs +3 -3
- package/driver/dev-portal.mjs +3 -3
- package/driver/disposition-call.mjs +1 -1
- package/driver/door-gates.mjs +41 -7
- package/driver/doubt-ledger.mjs +2 -2
- package/driver/drainer-identity.mjs +34 -8
- package/driver/driver.config.mjs +367 -104
- 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/README.md +1 -1
- package/driver/engine/mcp/dispositions-server.mjs +3 -3
- package/driver/engine/mcp/gather-config.mjs +9 -9
- package/driver/engine/mcp/perplexity-server.mjs +2 -2
- package/driver/engine/mcp/recording-server.mjs +18 -5
- package/driver/engine/openai-agent.mjs +4 -2
- package/driver/engine/probe.mjs +110 -23
- package/driver/enqueue-schema.mjs +6 -2
- package/driver/findings-model.mjs +6 -3
- package/driver/flag-snapshot.mjs +34 -8
- package/driver/form-neighbourhood.mjs +54 -7
- package/driver/framework.mjs +4 -4
- package/driver/gateway.mjs +36 -24
- package/driver/jx-lanes.mjs +23 -4
- package/driver/jx-units.mjs +7 -4
- package/driver/jx.mjs +34 -4
- package/driver/knockout-review-record.mjs +56 -4
- package/driver/known-conflicts.mjs +1 -1
- package/driver/matter-frame-record.mjs +90 -1
- package/driver/named-band.mjs +1 -1
- package/driver/ordinary-words.mjs +51 -0
- package/driver/outbox-backoff.mjs +31 -16
- package/driver/package.json +1 -1
- package/driver/partial-payload-baseline.json +2 -2
- package/driver/phase0.mjs +3 -3
- package/driver/pipeline-knockout.mjs +5 -5
- package/driver/pipeline.mjs +396 -81
- package/driver/placement-form.mjs +77 -1
- package/driver/placement-model.mjs +1 -1
- package/driver/portal-config-view.mjs +30 -1
- package/driver/portal-report.mjs +107 -6
- package/driver/portal-service.mjs +80 -14
- package/driver/portal-upstream.mjs +1 -1
- package/driver/predelivery-lint.mjs +12 -2
- package/driver/preserve-merge.mjs +3 -3
- package/driver/product-rows.mjs +2 -2
- package/driver/products.mjs +1 -1
- package/driver/profiles/README.md +3 -3
- package/driver/profiles/demo-brand-owner.json +2 -2
- package/driver/profiles.mjs +55 -17
- package/driver/progress.mjs +18 -8
- package/driver/provider-usage.mjs +8 -8
- package/driver/publish/index.mjs +154 -8
- package/driver/publish/knockout.mjs +39 -5
- package/driver/publish/pool-admin.mjs +1 -1
- package/driver/publish/publish-inputs.mjs +18 -2
- package/driver/publish/render-knockout.mjs +184 -31
- package/driver/publish/render.mjs +323 -93
- package/driver/publish/report-data.mjs +4 -1
- package/driver/publish/report-topbar.mjs +58 -0
- package/driver/publish/search-depth.mjs +133 -4
- package/driver/publish/templates/report.css +78 -4
- package/driver/publish/xlsx.mjs +20 -1
- package/driver/queue-order.mjs +2 -2
- package/driver/recording-agreement.mjs +1 -1
- package/driver/reference-score.mjs +1 -1
- package/driver/register-availability.mjs +2 -2
- package/driver/register-count.mjs +50 -5
- package/driver/register-coverage.mjs +161 -1
- package/driver/register-digest-record.mjs +236 -11
- package/driver/register-grant-vocabulary.mjs +1 -1
- package/driver/register-plan.mjs +189 -2
- package/driver/registry-fidelity.mjs +3 -3
- package/driver/repair-composers.mjs +1 -1
- package/driver/repair-contract.mjs +1 -1
- package/driver/replay-archive.mjs +6 -6
- package/driver/report-overview-record.mjs +2 -2
- 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 +5 -5
- package/driver/scope-facts.mjs +20 -5
- package/driver/scope-ledger.mjs +5 -5
- package/driver/search-policy.mjs +22 -12
- package/driver/skills/README.md +15 -15
- package/driver/skills/blind-frame/SKILL.md +2 -2
- package/driver/skills/case-law-citation/SKILL.md +4 -4
- package/driver/skills/case-law-citation/sources/eurlex.md +1 -1
- package/driver/skills/{prelim-common-law → clearance-common-law}/SKILL.md +22 -22
- package/driver/skills/{prelim-common-law → clearance-common-law}/perplexity-prompts.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/SKILL.md +10 -10
- package/driver/skills/{prelim-register → clearance-register}/digest.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/providers/README.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/providers/clarivate.md +37 -35
- package/driver/skills/{prelim-register → clearance-register}/providers/corsearch.md +20 -11
- package/driver/skills/{prelim-register → clearance-register}/providers/signa.md +5 -5
- package/driver/skills/{prelim-register → clearance-register}/register-recipes.md +3 -3
- package/driver/skills/{prelim-register → clearance-register}/status-rules.md +2 -2
- package/driver/skills/{prelim-register → clearance-register}/stealth-filer-indicators.md +1 -1
- package/driver/skills/{prelim-register → clearance-register}/unit.md +2 -2
- package/driver/skills/{prelim-search → clearance-search}/SKILL.md +31 -31
- package/driver/skills/{prelim-search → clearance-search}/delivery-contract.md +1 -1
- package/driver/skills/{prelim-search → clearance-search}/phase2-execution.md +18 -18
- package/driver/skills/{prelim-search → clearance-search}/synthesis-rules.md +7 -7
- package/driver/skills/{prelim-variants → clearance-variants}/SKILL.md +18 -18
- package/driver/skills/{prelim-variants → clearance-variants}/transliteration-scripts.md +5 -5
- package/driver/skills/frame-diff/SKILL.md +1 -1
- package/driver/skills/knockout-assess/SKILL.md +10 -7
- package/driver/skills/matter-frame/SKILL.md +3 -3
- package/driver/skills/narrative-refutation/SKILL.md +9 -9
- package/driver/skills/placement-inquiry/SKILL.md +5 -5
- package/driver/stage-context.mjs +1 -1
- package/driver/stages-knockout.mjs +4 -4
- package/driver/stages.mjs +65 -61
- package/driver/status-snapshot.mjs +2 -2
- package/driver/suite-census.json +340 -136
- package/driver/surface-exit-verdict.mjs +58 -0
- package/driver/systemd/README.md +9 -6
- package/driver/systemd/clearotron-worker.service +1 -1
- package/driver/terminal-clamp.mjs +109 -1
- package/driver/tokens.mjs +169 -3
- package/driver/unit-environment.mjs +42 -15
- package/driver/unit-inventory.mjs +19 -2
- package/driver/usage-ledger.mjs +1 -1
- package/driver/variant-manifest-model.mjs +4 -4
- package/driver/verify-knockout.mjs +27 -0
- package/driver/verify.mjs +94 -6
- package/driver/whatif-queue.mjs +1 -1
- package/driver/wordlists/en.txt +63906 -0
- package/mcp-server/CHANGELOG.md +8 -0
- package/mcp-server/README.md +1 -1
- package/mcp-server/lib/README.md +1 -1
- package/mcp-server/lib/options.mjs +8 -7
- package/mcp-server/lib/plan.mjs +18 -2
- package/mcp-server/lib/runs.mjs +1 -1
- package/mcp-server/lib/usage.mjs +3 -3
- package/mcp-server/lib/whatif.mjs +1 -1
- package/mcp-server/package.json +1 -1
- package/mcp-server/server.mjs +18 -1
- package/package.json +12 -11
- package/portal-ui/dist/assets/{index-CVOIvdhc.css → index-CtvwLCti.css} +207 -3
- package/portal-ui/dist/assets/{index-5UyqAyNM.js → index-EVaSo5-g.js} +1580 -527
- package/portal-ui/dist/index.html +2 -2
- package/portal-ui/package.json +1 -1
- package/providers/README.md +1 -1
- package/providers/_shared/enumerate.mjs +6 -6
- package/providers/_shared/execute-plan.mjs +3 -3
- package/providers/_shared/ledger.mjs +119 -5
- package/providers/_shared/provider-text.mjs +2 -2
- package/providers/_shared/screen.mjs +2 -2
- package/providers/_shared/script-form.mjs +3 -3
- package/providers/_shared/territory-codes.mjs +23 -3
- package/providers/clarivate/README.md +1 -1
- package/providers/clarivate/src/capabilities.js +12 -12
- package/providers/clarivate/src/core.js +37 -43
- package/providers/corsearch/README.md +1 -1
- package/providers/corsearch/src/capabilities.js +5 -5
- package/providers/corsearch/src/core.js +3 -3
- package/providers/jx/README.md +2 -1
- package/providers/jx/src/turn-envelope.mjs +8 -3
- package/providers/oauth-mcp-bridge/CHANGELOG.md +8 -0
- package/providers/oauth-mcp-bridge/package.json +1 -1
- package/providers/perplexity/src/core.js +1 -1
- package/providers/signa/README.md +1 -1
- package/providers/signa/src/capabilities.js +42 -49
- package/providers/signa/src/core.js +106 -29
- package/providers/uspto-local/README.md +1 -1
- package/providers/uspto-local/src/sync.js +1 -1
- package/scripts/README.md +1 -0
- package/scripts/ask-ai-render-check.mjs +127 -1
- package/scripts/authority-boundary-probe.mjs +8 -6
- package/scripts/backfill-started-at.mjs +2 -2
- package/scripts/census-merge-driver.mjs +33 -2
- package/scripts/citation-anchor-report.mjs +181 -0
- package/scripts/dead-names.mjs +1 -1
- package/scripts/deprecate-below.mjs +66 -8
- package/scripts/drain-preflight.mjs +1 -1
- package/scripts/e2e.mjs +174 -0
- package/scripts/env-audit.mjs +39 -6
- package/scripts/env-classify.mjs +20 -2
- package/scripts/freeze-example-run.mjs +61 -18
- package/scripts/generated-files-are-current.mjs +69 -4
- package/scripts/live-surface-check.mjs +124 -41
- package/scripts/markdown-link-check.mjs +1 -1
- package/scripts/merge-shape-check.mjs +242 -0
- package/scripts/mint-names-in-force.mjs +5 -5
- package/scripts/mint-offered-territories.mjs +72 -0
- package/scripts/mint-public-residue.mjs +2 -2
- package/scripts/mint-reference-strip-backlog.mjs +2 -2
- package/scripts/mint-suite-census.mjs +75 -2
- package/scripts/mint-writing-standard-backlog.mjs +2 -2
- package/scripts/purge-runs.mjs +7 -7
- package/scripts/reconcile-runs.mjs +2 -2
- package/scripts/release-approve-parked.mjs +20 -2
- package/scripts/release-await-cut.mjs +120 -1
- package/scripts/release-note-required.mjs +76 -8
- package/scripts/report-header-render-check.mjs +164 -0
- package/scripts/settings-render-check.mjs +75 -2
- package/scripts/test-full.mjs +96 -3
- package/scripts/test-run.mjs +10 -0
- package/shared/brand.mjs +27 -0
- package/shared/connect-clients.mjs +39 -11
- package/shared/deployment-box.mjs +7 -2
- package/shared/driver-dir.mjs +1 -1
- package/shared/env-aliases.mjs +1 -1
- package/shared/identifier-scan.mjs +65 -9
- package/shared/identifier-sentinels.mjs +22 -0
- package/shared/names-in-force.mjs +4 -2
- package/shared/offered-territories.json +738 -0
- package/shared/pre-rename-spellings.mjs +53 -0
- package/shared/reference-guard-classes.mjs +40 -2
- package/shared/stdio-connect.mjs +39 -4
- package/shared/tree-commit.mjs +48 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/euipo.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/free-tier.md +0 -0
- /package/driver/skills/{prelim-register → clearance-register}/providers/uspto-local.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/field-doctrine-pharma.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/firm-wide-reasoning.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/report-prose.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework-triage.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.manifest.json +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/risk-framework.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/template-formatting.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/email/generic.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/templates/search-request-form.html +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples-demo.md +0 -0
- /package/driver/skills/{prelim-search → clearance-search}/worked-examples.md +0 -0
|
@@ -7,20 +7,21 @@
|
|
|
7
7
|
// self-reported trail), meta.json batch stamps, and the email composer. Deliberately separate from publishReport (single-clearance-shaped); shares the exported
|
|
8
8
|
// palette (TONE_TIER, resolved against the run's FROZEN framework) so driver and interactive output
|
|
9
9
|
// can never drift and a knockout never reads the clearance composer's module state.
|
|
10
|
-
import { readFileSync, writeFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
10
|
+
import { readFileSync, writeFileSync, copyFileSync, mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
11
11
|
import { join } from 'node:path';
|
|
12
12
|
import { driverDir, ensureDriverDir } from '../../shared/driver-dir.mjs'; //
|
|
13
13
|
import { riskTier, TONE_TIER, regenIndex, regenSurfaces, auditRouteFor, markReportRouteFor, reportRouteFor } from './index.mjs';
|
|
14
14
|
import { runKnockoutLint, deliveryFlagLines } from '../predelivery-lint.mjs';
|
|
15
15
|
import { note } from '../log.mjs';
|
|
16
16
|
import { addSheet } from './xlsx.mjs';
|
|
17
|
-
import { COUNT_PREDICATES, COUNT_BASIS, countsForMark, countLine, countedMarks, variantFormsLine } from '../register-count.mjs';
|
|
17
|
+
import { COUNT_PREDICATES, COUNT_BASIS, countsForMark, countLine, countedMarks, variantFormsLine, disclosedFloor, moreThan } from '../register-count.mjs';
|
|
18
18
|
import { RECORD_BASIS, recordsForMark, recordsLine, listedMarks, normalizeRegisterRecordLinks } from '../register-records.mjs';
|
|
19
19
|
import { reportIdentityFor, productCoverageNote, kebab } from '../search-policy.mjs';
|
|
20
20
|
import { batchMarkName } from '../mark-name.mjs';
|
|
21
21
|
import { renderKnockoutHtml, knockoutReportData } from './render-knockout.mjs';
|
|
22
22
|
import { resolveDemoData, demoBannerMd } from './demo-marking.mjs'; //
|
|
23
23
|
import { engineCommit } from '../engine-build.mjs';
|
|
24
|
+
import { servedModels } from '../tokens.mjs';
|
|
24
25
|
import { knockoutFindingViews, knockoutFindingRange } from '../findings-model.mjs';
|
|
25
26
|
import { knockoutReceipts, worstBand } from '../verify-knockout.mjs';
|
|
26
27
|
|
|
@@ -142,7 +143,8 @@ export async function buildKnockoutWorkbook(findings, receipts, outPath, registe
|
|
|
142
143
|
if (noteRows.length) addSheet(wb, 'Working Notes', ['Mark', 'Type', 'Note'], noteRows);
|
|
143
144
|
|
|
144
145
|
if (registerCounts?.marks?.length) {
|
|
145
|
-
|
|
146
|
+
// A floor the register disclosed is its own figure, printed as the report prints it.
|
|
147
|
+
const cell = (c) => (Number.isFinite(c?.total) ? c.total : disclosedFloor(c) !== null ? moreThan(disclosedFloor(c)) : 'not available');
|
|
146
148
|
const countRows = (findings.marks ?? []).map((m) => {
|
|
147
149
|
const e = countsForMark(registerCounts, m.name);
|
|
148
150
|
const row = {
|
|
@@ -160,7 +162,7 @@ export async function buildKnockoutWorkbook(findings, receipts, outPath, registe
|
|
|
160
162
|
row['Basis'] = COUNT_BASIS;
|
|
161
163
|
// The verbatim provider reason for anything missing — the auditable half of "not available".
|
|
162
164
|
row['Notes'] = COUNT_PREDICATES
|
|
163
|
-
.filter((p) => !Number.isFinite(e?.counts?.[p.key]?.total))
|
|
165
|
+
.filter((p) => !Number.isFinite(e?.counts?.[p.key]?.total) && disclosedFloor(e?.counts?.[p.key]) === null)
|
|
164
166
|
.map((p) => `${p.label}: ${e?.counts?.[p.key]?.unavailable ?? 'no count recorded'}`)
|
|
165
167
|
.join(' · ');
|
|
166
168
|
return row;
|
|
@@ -334,6 +336,30 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
334
336
|
const poolRunDir = join(poolRoot, runId);
|
|
335
337
|
mkdirSync(poolRunDir, { recursive: true });
|
|
336
338
|
const writeRO = (name, data) => { const p = join(poolRunDir, name); writeFileSync(p, data); grpRead(p, 0o640); };
|
|
339
|
+
|
|
340
|
+
// ── THE RUN'S WORKING RECORD TRAVELS WITH THE REPORT ──────────────────────────────────────────────
|
|
341
|
+
//
|
|
342
|
+
// The delivered directory held the report, its data and the workbook, and nothing the engine itself
|
|
343
|
+
// wrote. That is what the acceptance read needs to tell a line the ENGINE wrote from a line the
|
|
344
|
+
// RENDERER invented: without it the check can still place a line as report text or as a sanctioned
|
|
345
|
+
// label, so it passes — and cannot fail in the way it exists to fail. A clearance run's record is
|
|
346
|
+
// `audit.md` and `findings.json`; this lane's own names are these two, and they are copied rather
|
|
347
|
+
// than rebuilt so the pool carries the bytes the stages wrote.
|
|
348
|
+
//
|
|
349
|
+
// AN ABSENT ONE IS SAID, NOT SKIPPED. A run whose stage never wrote its assessment is a different
|
|
350
|
+
// thing from a run that wrote one, and a publisher that copies what it finds and says nothing about
|
|
351
|
+
// what it does not makes the two look identical to the next reader — which is the whole defect this
|
|
352
|
+
// closes, one directory along.
|
|
353
|
+
for (const name of ['knockout-findings.json', 'knockout-assessment.md']) {
|
|
354
|
+
const src = join(runDir, name);
|
|
355
|
+
if (existsSync(src)) {
|
|
356
|
+
const dest = join(poolRunDir, name);
|
|
357
|
+
copyFileSync(src, dest);
|
|
358
|
+
grpRead(dest, 0o640);
|
|
359
|
+
} else {
|
|
360
|
+
note(`knockout working record: ${name} is not in the run directory, so the delivered report has no engine text to be checked against`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
337
363
|
const issued = (() => {
|
|
338
364
|
try {
|
|
339
365
|
const d = new Date();
|
|
@@ -518,6 +544,10 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
518
544
|
// — resolved once for the whole publish, off the same two sources the clearance
|
|
519
545
|
// publisher uses (frozen sidecar, then roster; either marks, neither un-marks).
|
|
520
546
|
const demoData = resolveDemoData({ runDir, customerKey });
|
|
547
|
+
// The models that served this batch, named as a client may read them — read once, like the demo answer,
|
|
548
|
+
// because it is a property of the run. A read that throws records nothing rather than failing a publish.
|
|
549
|
+
let served = null;
|
|
550
|
+
try { served = runDir ? servedModels(runDir) : null; } catch { served = null; }
|
|
521
551
|
const reports = markRows.map((m) => {
|
|
522
552
|
const slug = kebab(m.name);
|
|
523
553
|
const file = single ? 'report.html' : `report-${slug}.html`;
|
|
@@ -558,12 +588,13 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
558
588
|
// document says it did not search is what the run was instructed not to search.
|
|
559
589
|
searchPolicy: searchPolicy ?? null,
|
|
560
590
|
delivery,
|
|
591
|
+
servedModels: served,
|
|
561
592
|
}));
|
|
562
593
|
// The run as data — what the assistant drafts client-facing mail from, and the shape the portal's
|
|
563
594
|
// native-render path has been reading for since before anything wrote it. One per report, so the
|
|
564
595
|
// native render of a per-mark document is that mark's data and not the batch's.
|
|
565
596
|
writeRO(dataFile, JSON.stringify(knockoutReportData(one, framework, {
|
|
566
|
-
runId, codename, overall: markBand, issued, registerCounts, registerRecords, ownerChecks, auditFile, customerKey, matter: runId,
|
|
597
|
+
runId, codename, overall: markBand, issued, registerCounts, registerRecords, ownerChecks, auditFile, customerKey, matter: runId, servedModels: served,
|
|
567
598
|
identity: { ...identity, level: searchPolicy?.level ?? null },
|
|
568
599
|
url: reportUrlFor(slug),
|
|
569
600
|
}), null, 2));
|
|
@@ -625,6 +656,9 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
625
656
|
.filter(Boolean).join(' — '),
|
|
626
657
|
markName: batchMarkName(markNames) ?? undefined,
|
|
627
658
|
engineCommit: engineCommit(),
|
|
659
|
+
// The models that served the batch, named as a client may read them (tokens.mjs servedModels). Absent when
|
|
660
|
+
// nothing was read, so a meta from before the record keeps its shape; [] when turns ran and named none a client may read.
|
|
661
|
+
servedModels: served ?? undefined,
|
|
628
662
|
client: null, customerKey: customerKey || 'generic',
|
|
629
663
|
// WHICH ORGANISATION'S GENERIC this batch was filed under, read from the frozen sidecar exactly as
|
|
630
664
|
// the clearance publisher reads it. Absent on a company's batch and on one filed before
|
|
@@ -116,7 +116,7 @@ function linkHome(poolDir) {
|
|
|
116
116
|
|
|
117
117
|
// Locate an archived run's WORKSPACE dir (the publishReport input set: raw report.md, findings.json,
|
|
118
118
|
// _driver/ sidecars) across every agent workspace. Layout (pipeline archive step):
|
|
119
|
-
// <workspaceRoot>/<workspacePrefix><agent>/studio/
|
|
119
|
+
// <workspaceRoot>/<workspacePrefix><agent>/studio/clearance-search/archive/<month|legacy-*>/<matter-slug>/<date-codename>/
|
|
120
120
|
// where `<matter-slug>-<date-codename>` === runId. Returns every match (ambiguity is the caller's error).
|
|
121
121
|
// The roots route through driver.config.mjs's workspace helpers (never inline the `workspace-` literal —
|
|
122
122
|
// a deployment picks its own prefix via CLEAROTRON_WORKSPACE_PREFIX / CLEAROTRON_WORK_DIR).
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// contract the publication gate held before it was retired into tests at — pass, fail, and COULD
|
|
18
18
|
// NOT RUN, never folded into two — carried to the seam where a run becomes a client artifact.
|
|
19
19
|
//
|
|
20
|
-
// Modelled on mcp-server/lib/coverage.mjs
|
|
20
|
+
// Modelled on assertValidatorCoverage() in mcp-server/lib/coverage.mjs (lines 65-75): a closed partition asserted
|
|
21
21
|
// at LOAD, so a new store cannot be added without consciously choosing its gating. The failure that
|
|
22
22
|
// mechanism exists to prevent is the — a hand-copied mirror that silently stopped covering an
|
|
23
23
|
// artifact and still answered "complete: yes" about a file it never checked.
|
|
@@ -57,10 +57,26 @@ export const PUBLISH_INPUTS = {
|
|
|
57
57
|
"_driver/verdict.json": "optional",
|
|
58
58
|
// doc 50 — the frozen band vocabulary. Present on band-doctrine runs only, by design.
|
|
59
59
|
"_driver/framework.json": "optional",
|
|
60
|
+
// The recall net's receipt. Its overflow[] carries the probes the run minted and did not dispatch,
|
|
61
|
+
// one row each on the workbook's gaps sheet. `optional`, and the reason is the recall net's own
|
|
62
|
+
// switches: the lane is env-gated for rollback and mints nothing when a matter has no remembered
|
|
63
|
+
// conflict, so an absent receipt is a run that had nothing to record rather than one that lost it.
|
|
64
|
+
// `required` would close the gate on every knockout and every matter with a clean history.
|
|
65
|
+
"_driver/register-recall.json": "optional",
|
|
60
66
|
// T6 (D4) — the frozen register plan; the render falls back to the coverage prose without it.
|
|
61
67
|
"_driver/register-plan.json": "optional",
|
|
68
|
+
// The band the register returned, read ONLY where the run has no `_records/` archive, to count what was
|
|
69
|
+
// read and where. Optional: a knockout, a legacy run and a register-less run have none, and an absent
|
|
70
|
+
// band leaves the count as "cannot say" exactly as before.
|
|
71
|
+
"register-named-band.json": "optional",
|
|
62
72
|
// The instructed scope, read only as the register plan's fallback for the searched-jurisdiction set.
|
|
63
73
|
"_driver/instructed-scope.json": "optional",
|
|
74
|
+
// The frozen local-language lane decision, and the units the fold wrote. Read to derive how deep that
|
|
75
|
+
// investigation went against what the matter configured — through the engine's own asked-versus-ran
|
|
76
|
+
// reader, not by re-deciding it here. Optional and genuinely so: a plain clearance never runs the
|
|
77
|
+
// component and carries neither file, which is the state that folds to not-in-scope.
|
|
78
|
+
"_driver/jx-lanes.json": "optional",
|
|
79
|
+
"_driver/jx/units.json": "optional",
|
|
64
80
|
// T7 (E5) — the grounded case-law profiles. A run with no case-law layer legitimately has none.
|
|
65
81
|
"case-law-findings.md": "optional",
|
|
66
82
|
// T7 (E6) — Corsearch enforcement telemetry; presentation-only, absent ⇒ no lines.
|
|
@@ -104,7 +120,7 @@ export const CALLER_SUPPLIED = {
|
|
|
104
120
|
};
|
|
105
121
|
|
|
106
122
|
/**
|
|
107
|
-
* Load-time gate, on
|
|
123
|
+
* Load-time gate, modelled on assertValidatorCoverage() in mcp-server/lib/coverage.mjs (lines 64-73).
|
|
108
124
|
*
|
|
109
125
|
* Asserts the table is a well-formed closed partition: every gating is one of the two words, no store
|
|
110
126
|
* is declared in more than one table, and no declaration is blank. It deliberately does NOT scan
|
|
@@ -40,14 +40,22 @@ import { join, dirname } from 'node:path';
|
|
|
40
40
|
import { fileURLToPath } from 'node:url';
|
|
41
41
|
import {
|
|
42
42
|
REPORT_ROOT, REPORT_ROOT_DARK_EXPLICIT, THEME_INIT_EXPLICIT, themeButton,
|
|
43
|
-
THEME_BTN_CSS, CHROME_CSS, FAVICON_LINK, logoLockup, BRAND, confPosture,
|
|
43
|
+
THEME_BTN_CSS, CHROME_CSS, FAVICON_LINK, logoLockup, BRAND, confPosture, sectionStrip,
|
|
44
44
|
} from '../../shared/brand.mjs';
|
|
45
45
|
import { SUMMARY_BLOCK_LINE, parseSummaryBlocks } from '../../shared/summary-blocks.mjs';
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
// THIS BOARD'S FOUR ENTRIES, in its order. Its third reads "Also considered" and resolves at the filings
|
|
48
|
+
// section, which is what that section is; the rule that filters them against the finished document lives
|
|
49
|
+
// in shared/brand.mjs, beside the clearance report's own five.
|
|
50
|
+
const STRIP = Object.freeze([
|
|
51
|
+
['summary', 'Summary'], ['findings', 'Findings'], ['filings', 'Also considered'], ['next', 'Next steps'],
|
|
52
|
+
]);
|
|
53
|
+
import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLine, disclosedFloor, moreThan } from '../register-count.mjs';
|
|
47
54
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
48
55
|
import { officeLinkSentences } from './office-record-links.mjs';
|
|
49
56
|
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
50
|
-
import { demoBannerHtml } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
57
|
+
import { demoBannerHtml, servedModelsLine } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
58
|
+
import { EXPORT_TOGGLE, exportPopover, EXPORT_MENU_JS } from './report-topbar.mjs'; // — the export menu's shell and behaviour, shared with the clearance template // — the SAME banner the clearance template renders, not a second wording
|
|
51
59
|
// — the two facts the register card is allowed to read off a raw record, and NEITHER is minted
|
|
52
60
|
// here. `makeClassifyStatus` and `isAllClass` are the screening lane's own, already shipped, already
|
|
53
61
|
// fail-open; re-deriving either in a renderer would be this file starting a second status vocabulary,
|
|
@@ -163,6 +171,39 @@ function qualifierHtml(q) {
|
|
|
163
171
|
// The layout this report adds on top of report.css. Additive and `ko-`prefixed: report.css is shared with
|
|
164
172
|
// the frozen clearance renderer, and a selector that is inert there cannot regress it.
|
|
165
173
|
const KO_CSS = `
|
|
174
|
+
/* THE BOARD'S SECTION STRIP AND ITS SECTION FURNITURE, lifted from the approved knockout board rather
|
|
175
|
+
than written here. Four entries; the third reads "Also considered" and resolves at the filings
|
|
176
|
+
section. Marked not to print, because the board marks it so: a strip that follows the reader down
|
|
177
|
+
the page is a control, and paper has nothing for it to follow.
|
|
178
|
+
NO BACKTICK BELOW, and none in this comment — one closes the template literal this sits inside, and
|
|
179
|
+
it has cost this file two silent breakages already.
|
|
180
|
+
The number span is drawn and then hidden, which is the board's own doing: it emits the span on every
|
|
181
|
+
section and sets it to none further down its own stylesheet. Carried so the delivered document holds
|
|
182
|
+
the same elements as the approved one; no pixel moves. */
|
|
183
|
+
/* THE BREADCRUMB IS A ROW OF THE HEADER, not a bar under it (owner, 2026-09-18).
|
|
184
|
+
It used to be a SIBLING of .rep-stickyhead, pinned on its own at top:var(--tb-h,52px) — and
|
|
185
|
+
--tb-h is set nowhere in this product, so the fallback was a guess at a bar that measures ~46px:
|
|
186
|
+
content showed through the slit between the two, and z-index:20 put the strip UNDER the header's
|
|
187
|
+
100 whenever the guess was wrong. The strip is now emitted inside .rep-stickyhead, so the header
|
|
188
|
+
and the breadcrumb are one sticky surface that pins and unpins together and can never gap or
|
|
189
|
+
overlap, whatever the bar measures or how it wraps.
|
|
190
|
+
No background and no bottom hairline of its own: the wrapper carries the blurred surface and the
|
|
191
|
+
single edge, and the border-top here is the divider between the two rows. It is on .strip rather
|
|
192
|
+
than on any wrapper because sectionStrip() emits NOTHING when fewer than two of its sections are
|
|
193
|
+
live — a wrapper would leave a stray line on those reports.
|
|
194
|
+
The gutter matches the topbar's, so the first entry lines up under the back button. */
|
|
195
|
+
.strip{display:flex;gap:4px;align-items:center;padding:4px max(26px,calc((100% - 1120px)/2)) 6px;
|
|
196
|
+
border-top:1px solid var(--line);font:600 12px/1 'Satoshi','Helvetica Neue',Arial,sans-serif;
|
|
197
|
+
letter-spacing:.04em;overflow-x:auto;scrollbar-width:none}
|
|
198
|
+
.strip::-webkit-scrollbar{display:none}
|
|
199
|
+
.strip a{display:inline-flex;align-items:center;gap:7px;padding:7px 10px;border-radius:999px;color:#6b5d50;text-decoration:none;white-space:nowrap}
|
|
200
|
+
.strip a i{width:8px;height:8px;border-radius:50%;border:1.5px solid currentColor;box-sizing:border-box}
|
|
201
|
+
.strip a.done{color:#4c7a4c}
|
|
202
|
+
.strip a.done i{background:#4c7a4c;border-color:#4c7a4c}
|
|
203
|
+
.strip a.now{color:#250902;background:rgba(0,0,0,.06)}
|
|
204
|
+
.strip a.now i{border-color:#860F09;border-width:3px}
|
|
205
|
+
.sec .num{display:none}
|
|
206
|
+
.sec .note{display:inline;margin-left:10px;font-size:12px;color:var(--faint)}
|
|
166
207
|
.ko-glance{padding:6px 0 2px}
|
|
167
208
|
/* — framework attribution, captioning the rows. Same gutter and hairline as.ko-row so it reads
|
|
168
209
|
as the table's head, not a floating note. */
|
|
@@ -263,14 +304,55 @@ const KO_CSS = `
|
|
|
263
304
|
.ko-scope{font-size:13.5px;color:var(--slate);line-height:1.65;margin:0;padding:18px 24px}
|
|
264
305
|
.ko-scope b{color:var(--ink)}
|
|
265
306
|
/* A wide counts table must scroll inside its own panel, never push the page sideways. */
|
|
307
|
+
/* AND IT SHOWS THAT IT SCROLLS. A panel that continues off its right edge and says nothing leaves a
|
|
308
|
+
reader who never drags it believing they have seen the whole table; the columns past the edge are
|
|
309
|
+
reachable and invisible at the same time. Ruled 2026-09-17: a scrollbar, always visible, and no text.
|
|
310
|
+
The -webkit-appearance:none is the part that does the work — without it the platform draws an OVERLAY
|
|
311
|
+
scrollbar that appears only once a scroll is already under way, which is the state this rule ends.
|
|
312
|
+
scrollbar-width and scrollbar-color are the same instruction for the engines that take the standard
|
|
313
|
+
properties. (NO BACKTICK IN THIS COMMENT: it sits inside the stylesheet's own template literal, and
|
|
314
|
+
one closes it. That is written above the export menu block too, and I still did it — so it is here
|
|
315
|
+
as well, beside the second rule anyone adds.) The colours are tokens, not fixed values, for the reason the ladder below this block
|
|
316
|
+
records: a fixed colour here reads on one ground and goes invisible on the other. */
|
|
266
317
|
.ko-scroll{overflow-x:auto}
|
|
267
|
-
|
|
318
|
+
.ko-scroll::-webkit-scrollbar{height:9px;-webkit-appearance:none}
|
|
319
|
+
.ko-scroll::-webkit-scrollbar-track{background:transparent}
|
|
320
|
+
.ko-scroll::-webkit-scrollbar-thumb{background:var(--faint);border-radius:5px}
|
|
321
|
+
/* THE TWO INSTRUCTIONS CANNOT SIT TOGETHER, and this was measured rather than reasoned. Setting the
|
|
322
|
+
standard scrollbar-width alongside the pseudo-elements makes the engine take the standard path and
|
|
323
|
+
ignore them, and on this one the standard path draws an overlay bar with no layout height at all.
|
|
324
|
+
Driven three ways on a delivered report at 390px, reading the height the bar takes out of the panel:
|
|
325
|
+
both together 0px, the pseudo-elements alone 9px, the standard properties alone 0px. So the standard
|
|
326
|
+
ones go behind a support query that the engines carrying the pseudo-elements never enter, which
|
|
327
|
+
leaves each engine exactly one instruction. */
|
|
328
|
+
@supports not selector(::-webkit-scrollbar){
|
|
329
|
+
.ko-scroll{scrollbar-width:thin;scrollbar-color:var(--faint) transparent}}
|
|
330
|
+
/* A finding's bullets carry bare source URLs, and a URL offers a line no place to break. On a phone
|
|
331
|
+
that unbreakable run becomes the minimum width of the grid item, so the single column floors at
|
|
332
|
+
443px inside a 360px viewport and the whole page scrolls sideways -- 135px of it, measured
|
|
333
|
+
2026-09-17. Only the value "anywhere" lowers that minimum: "break-word" breaks the line but
|
|
334
|
+
leaves the minimum where it was, which is why the blocks beside this one that already set
|
|
335
|
+
word-break were never the cause. Phone widths only: nothing at or above 700px changes.
|
|
336
|
+
NO BACKTICK IN THIS COMMENT -- it sits inside the stylesheet's own template literal. */
|
|
337
|
+
@media(max-width:700px){.ko-row{grid-template-columns:1fr;gap:10px}
|
|
338
|
+
.ko-bul{overflow-wrap:anywhere}}
|
|
268
339
|
|
|
269
340
|
/* The band ladder, the territories fold and the run's own caveats (the 2026-09-16 report redesign). Ported from
|
|
270
341
|
the design's stylesheet with its fixed colours replaced by this page's tokens, so the ladder reads
|
|
271
342
|
in both themes: the tick row was #a89a8a on white and the fold's summary and prose a brown pair,
|
|
272
343
|
all three of which go invisible on the dark ground this report also renders on. */
|
|
273
|
-
|
|
344
|
+
/* THE BAND PILL HANGS ABOVE THE BAR, so the space above the bar is what keeps it off the heading.
|
|
345
|
+
NO BACKTICK IN THIS COMMENT — it sits inside the stylesheet's own template literal.
|
|
346
|
+
.kmarker sits at top:-30px and the pill is ~23px tall (5+5 padding on a 13px line), so it starts
|
|
347
|
+
30px above the bar and ends 7px above it. At the LEFTMOST band the pill is also drawn flush left
|
|
348
|
+
(translateX(-12px) at left:0%) — directly under the left-aligned "Overall risk" — so that is the
|
|
349
|
+
rung where the two touch, and the house ladder's leftmost rung is Low.
|
|
350
|
+
THE ROOM IS ONE MARGIN, NOT TWO, and that is what made the arithmetic wrong twice. This margin and
|
|
351
|
+
.gauge .label's 18px bottom margin are ADJACENT SIBLINGS, so they collapse to the larger of the two
|
|
352
|
+
rather than adding: 10px here gave 18px of room against a pill that needs 30, and the first attempt
|
|
353
|
+
at 24 still left 6. Measured in a browser at every rung (scripts/report-header-render-check.mjs),
|
|
354
|
+
not reasoned: 44 gives 14px of daylight under the heading at every band. */
|
|
355
|
+
.kscale{position:relative;margin:44px 8px 34px}
|
|
274
356
|
.kbar{position:relative;height:8px;border-radius:4px}
|
|
275
357
|
.kmarker{position:absolute;top:-30px;text-align:center}
|
|
276
358
|
.kpill{display:inline-block;padding:5px 12px;border-radius:999px;color:#fff;
|
|
@@ -433,9 +515,9 @@ function territoriesLine(registerCounts) {
|
|
|
433
515
|
return `Counted on ${regions.length} registers, listed on the workbook's Register Counts sheet, on ${provider}.`;
|
|
434
516
|
}
|
|
435
517
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
518
|
+
// The reader line that used to sit under the counts table ("a count is not a conflict…") is off the page:
|
|
519
|
+
// the approved knockout board carries no caveat under the table, and the writing standard lists it among
|
|
520
|
+
// the caveat sentences a page does not print. The column headers say what each count is.
|
|
439
521
|
|
|
440
522
|
function countsSection(marks, registerCounts, positions = '') {
|
|
441
523
|
// ── THE DEFINITION MOVES INTO THE COLUMN HEADER ──────────────────────────────────────────────────
|
|
@@ -470,9 +552,10 @@ function countsSection(marks, registerCounts, positions = '') {
|
|
|
470
552
|
const why = c?.unavailable ?? (c
|
|
471
553
|
? 'no count recorded'
|
|
472
554
|
: 'not counted on this run — it predates this column, and an archived report re-renders as what it was');
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
555
|
+
if (Number.isFinite(c?.total)) return `<td class="num">${esc(String(c.total))}</td>`;
|
|
556
|
+
// The register answered with its own floor: that is a figure, and it is printed as one.
|
|
557
|
+
if (disclosedFloor(c) !== null) return `<td class="num">${esc(moreThan(disclosedFloor(c)))}</td>`;
|
|
558
|
+
return `<td class="na" title="${escAttr(why)}">not available</td>`;
|
|
476
559
|
}).join('');
|
|
477
560
|
const scoped = e?.classScope !== 'all-classes' && (e?.classes?.length > 0);
|
|
478
561
|
const scope = scoped
|
|
@@ -490,7 +573,7 @@ function countsSection(marks, registerCounts, positions = '') {
|
|
|
490
573
|
// thing in three words each.
|
|
491
574
|
return `<div class="panel">
|
|
492
575
|
<div class="ko-counts ko-scroll"><table><thead><tr><th>Name</th>${head}</tr></thead><tbody>${rows}</tbody></table></div>
|
|
493
|
-
<p class="ko-basis">${esc(
|
|
576
|
+
<p class="ko-basis">${esc(territoriesLine(registerCounts))}${
|
|
494
577
|
positions ? `<br>${esc(positions)}` : ''}</p>
|
|
495
578
|
</div>`;
|
|
496
579
|
}
|
|
@@ -1156,16 +1239,16 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1156
1239
|
const cut = note.lastIndexOf(' \u2014 ');
|
|
1157
1240
|
const stage = cut > 0 ? note.slice(0, cut).trim() : note;
|
|
1158
1241
|
const context = String(productContext ?? '').trim();
|
|
1159
|
-
// REGISTERS COUNTED IS A ROW
|
|
1160
|
-
//
|
|
1161
|
-
// its source are what they read, and the list is one click away when they want it.
|
|
1242
|
+
// REGISTERS COUNTED IS A ROW (the 2026-09-16 report redesign). A line of country names running through
|
|
1243
|
+
// the middle of the panel is the thing a reader skips; the count and its source are what they read.
|
|
1162
1244
|
const regions = (registerCounts?.scope?.regions ?? []).filter(Boolean);
|
|
1163
1245
|
const provider = registerCounts?.providerLabel ?? registerCounts?.provider ?? '';
|
|
1164
|
-
|
|
1246
|
+
// THE BOARD'S OWN ROW: "186 registers, on Clarivate Compumark." — registers, because that is what was
|
|
1247
|
+
// counted, and no fold: the board lists no territories here, and the workbook's counts sheet carries
|
|
1248
|
+
// every register a figure was taken over.
|
|
1165
1249
|
const counted = regions.length
|
|
1166
|
-
? `${regions.length} ${regions.length === 1 ? '
|
|
1250
|
+
? `${regions.length} ${regions.length === 1 ? 'register' : 'registers'}${provider ? `, on ${provider}` : ''}.`
|
|
1167
1251
|
: (registerCounts ? `Counted worldwide${provider ? `, on ${provider}` : ''}` : '');
|
|
1168
|
-
const territories = named.length ? listWords(named) : '';
|
|
1169
1252
|
if (!asked && !where && !classLine && !stage && !context && !counted) return '';
|
|
1170
1253
|
// ONE PANEL, THE SAME ON BOTH REPORTS (the 2026-09-16 report redesign). The rows carry what was asked for; the
|
|
1171
1254
|
// counts row says what was counted and hides the territory list behind a fold rather than running a
|
|
@@ -1178,7 +1261,7 @@ function aboutRequestBlock(scope, requestNotes, depthNote = '', productContext =
|
|
|
1178
1261
|
${row('Classes', classLine)}
|
|
1179
1262
|
${row('Where searched', where)}
|
|
1180
1263
|
${row('Context', context)}
|
|
1181
|
-
${row('Registers counted', counted
|
|
1264
|
+
${row('Registers counted', counted)}
|
|
1182
1265
|
${row('Searched on', searched)}
|
|
1183
1266
|
${/* item 18 — a note for the reviewing lawyer does not reach the delivered page */''}
|
|
1184
1267
|
</div>`;
|
|
@@ -1335,7 +1418,8 @@ function coverageClause(mark, registerCounts, probeRan) {
|
|
|
1335
1418
|
? 'Register: hit-counts are part of this search and none could be taken for this name — the reason is in the audit workbook.'
|
|
1336
1419
|
: 'Register: not included in this product tier — no register was counted for this name.';
|
|
1337
1420
|
}
|
|
1338
|
-
|
|
1421
|
+
// A floor the register disclosed is a figure it gave, so a name counted only that way was counted.
|
|
1422
|
+
const anyFigure = COUNT_PREDICATES.some((p) => Number.isFinite(entry.counts[p.key]?.total) || disclosedFloor(entry.counts[p.key]) !== null);
|
|
1339
1423
|
if (!anyFigure) return 'Register: no count could be taken for this name — the reason is in the audit workbook.';
|
|
1340
1424
|
// COVERAGE, NOT THE FIGURES. asks the renderer to own the register-tier caveat, and the numbers
|
|
1341
1425
|
// are already on this page twice — in the glance line at the top and in the counts table above this
|
|
@@ -1658,6 +1742,10 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1658
1742
|
// renders the same plain "Privileged & Confidential" this template always printed — so the ~15 unit
|
|
1659
1743
|
// fixtures and both render-check scripts, none of which pass one, are unchanged by its arrival.
|
|
1660
1744
|
delivery = null,
|
|
1745
|
+
// The models that served the batch, as tokens.mjs servedModels names them for a client: the scope
|
|
1746
|
+
// section's closing line. Defaults to null, so a fixture or an archived run that passes none renders
|
|
1747
|
+
// as it always did.
|
|
1748
|
+
servedModels = null,
|
|
1661
1749
|
// — IS THIS AN INVENTED MARK? This template had no demo handling of any kind, so
|
|
1662
1750
|
// every demo knockout shipped looking real: an invented mark, a real register basis, a real-looking
|
|
1663
1751
|
// risk assessment, a published URL, and nothing saying the matter is fiction. The clearance template
|
|
@@ -1668,6 +1756,11 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1668
1756
|
const marks = findings?.marks ?? [];
|
|
1669
1757
|
const title = batchTitle(marks);
|
|
1670
1758
|
const productName = identity?.identity ?? null;
|
|
1759
|
+
// A DATE, NOT A TIMESTAMP. `issued` is composed as `<date> · <time>` at publish, and the time is the
|
|
1760
|
+
// minute the file was written — it tells a reader nothing and implies a precision the work does not
|
|
1761
|
+
// have. Every mock issues the date alone. Taken by pattern rather than by splitting on the
|
|
1762
|
+
// separator, so a format that changes falls through whole instead of being cut in the wrong place.
|
|
1763
|
+
const issuedDate = (String(issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? issued;
|
|
1671
1764
|
const overallStop = bandStop(framework, overall);
|
|
1672
1765
|
const summary = findings?.batch?.executiveSummary ?? '';
|
|
1673
1766
|
const caveats = findings?.batch?.standardCaveats ?? [];
|
|
@@ -1748,11 +1841,11 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1748
1841
|
n + registerCardsOnPage(registerCardViews(m, framework, registerRecords).cards, m, framework).length, 0);
|
|
1749
1842
|
const hasRecords = Boolean(registerRecords && (registerRecords.marks?.length || registerRecords.unavailable));
|
|
1750
1843
|
const filings = hasRecords ? filingsSection(marks, registerRecords) : '';
|
|
1751
|
-
const onFieldSec = `<div class="sec" id="findings"><h2>Conflicts</h2></div>
|
|
1844
|
+
const onFieldSec = `<div class="sec" id="findings"><span class="num"></span><h2>Conflicts</h2></div>
|
|
1752
1845
|
${counts}
|
|
1753
1846
|
${analysis}`;
|
|
1754
1847
|
const filingsSec = filings
|
|
1755
|
-
? `<div class="sec" id="filings"><h2>Also considered</h2></div>
|
|
1848
|
+
? `<div class="sec" id="filings"><span class="num"></span><h2>Also considered</h2><span class="note">the filings behind the counts</span></div>
|
|
1756
1849
|
${filings}`
|
|
1757
1850
|
: '';
|
|
1758
1851
|
|
|
@@ -1764,7 +1857,7 @@ ${filings}`
|
|
|
1764
1857
|
const cssInline = REPORT_BASE + '\n' + CHROME_CSS + '\n' + THEME_BTN_CSS + '\n' + KO_CSS;
|
|
1765
1858
|
const chromeLinkTag = chromeHref ? `<link rel="stylesheet" href="${escAttr(chromeHref)}">` : '';
|
|
1766
1859
|
|
|
1767
|
-
|
|
1860
|
+
const doc = `<!DOCTYPE html>
|
|
1768
1861
|
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1769
1862
|
<title>${productName ? `${esc(productName)} — ` : ''}${esc(title)} · ${esc(BRAND.name)}</title>
|
|
1770
1863
|
<link rel="preconnect" href="https://api.fontshare.com" crossorigin>
|
|
@@ -1776,10 +1869,46 @@ ${filings}`
|
|
|
1776
1869
|
${homeHref ? `<a class="homebtn tb-back no-print" href="${escAttr(homeHref)}" title="All reports"><span aria-hidden="true">←</span> <span class="tb-back-lbl">All reports</span></a>` : ''}
|
|
1777
1870
|
${logoLockup({ mark: 20, tag: '' })}<span class="sp"></span>
|
|
1778
1871
|
<span class="tb-risk" style="background:var(${overallStop})">${esc(overall ?? '—')}</span>
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1872
|
+
${/* THE IDENTITY LINE NAMES THE MARK AND THE SEARCH, NOT THE RUN. It printed `matter || runId`,
|
|
1873
|
+
and the caller passes the runId AS the matter, so what a client read was the engine's own run
|
|
1874
|
+
identifier — on a real run a temporary-directory name carrying a capture suffix. It is ours, it
|
|
1875
|
+
means nothing to them, and it is the one string on the page that could not be shown to anyone.
|
|
1876
|
+
The mock reads mark then search type, and both are already in hand here.
|
|
1877
|
+
There is NO fall back to the identifier: with no product name the mark stands alone, and with
|
|
1878
|
+
neither the line does not render. A run whose registry identity is missing is exactly the case
|
|
1879
|
+
that used to print the raw name, so falling back would keep the defect for the runs that have
|
|
1880
|
+
it. */''}
|
|
1881
|
+
${title || productName ? `<span class="mono tb-matter" style="font-size:11px;color:var(--faint)">${
|
|
1882
|
+
[title, productName].filter(Boolean).map(esc).join(' / ')}</span>` : ''}
|
|
1883
|
+
${issuedDate ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued on ${esc(issuedDate)}</span>` : ''}
|
|
1884
|
+
${/* THE EXPORT CONTROL, which this template had never emitted. The approved header carries it and the
|
|
1885
|
+
clearance report has it; the knockout offered a reader who opens the file no route to a PDF at
|
|
1886
|
+
all, while its own stylesheet still described the utility buttons as "used by the topbar Export
|
|
1887
|
+
popover" — the styling for a control that was not there.
|
|
1888
|
+
NO TICK WORDING AND NO SELECT-ALL. The clearance's entry reads "Export PDF (ticked findings)"
|
|
1889
|
+
and its popover offers Select all / Select none, because its exportPDF filters to the ticked
|
|
1890
|
+
ones. This template has no pickbox anywhere, so `pickAll` is deliberately not defined here and a
|
|
1891
|
+
tick verb would name a control that cannot exist. What prints is the whole document.
|
|
1892
|
+
EVERY WORD IN IT IS ALREADY THE PRODUCT'S. The board draws the Export button and its caret and
|
|
1893
|
+
stops there — it does not draw what the menu contains — so the entries are the clearance
|
|
1894
|
+
report's own, verbatim, minus the tick wording that names a control this template does not have.
|
|
1895
|
+
The heading this first carried ("Export") was written here and is gone: a word on a page a
|
|
1896
|
+
client reads is the owner's to choose, and the board does not ask for one.
|
|
1897
|
+
THE MARKUP IS THE CLEARANCE'S, RE-EMITTED RATHER THAN SHARED, and that is a debt this change
|
|
1898
|
+
takes on knowingly: `render.mjs` is frozen at a content hash, and extracting its top bar is its
|
|
1899
|
+
own change with its own byte-comparison against a real archived run. Filed as a follow-up. The
|
|
1900
|
+
class names are the shared vocabulary this file already emits, so report.css styles both. */''}
|
|
1901
|
+
<div class="tb-menu">
|
|
1902
|
+
<button type="button" class="tbbtn tb-ask no-print">\u2726 <span class="tb-lbl">Ask AI</span></button>
|
|
1903
|
+
${EXPORT_TOGGLE}
|
|
1904
|
+
${exportPopover(`
|
|
1905
|
+
<button class="util primary" onclick="exportPDF()">\u2b07 Export PDF</button>
|
|
1906
|
+
<div class="tb-sep"></div>
|
|
1907
|
+
<div class="tb-row"><button class="util" onclick="openAll(true)">Expand all</button><button class="util" onclick="openAll(false)">Collapse all</button></div>
|
|
1908
|
+
`)}
|
|
1909
|
+
</div>
|
|
1782
1910
|
</div>
|
|
1911
|
+
<!--SECTION-STRIP-->
|
|
1783
1912
|
</div>
|
|
1784
1913
|
<div class="fab-stack">${themeButton()}</div>
|
|
1785
1914
|
<script>
|
|
@@ -1799,9 +1928,12 @@ function openAll(v){document.querySelectorAll('details.ko-full').forEach(functio
|
|
|
1799
1928
|
function exportPDF(){window.print();}
|
|
1800
1929
|
(function(){var o=function(){openAll(true);};
|
|
1801
1930
|
if(window.matchMedia){var m=window.matchMedia('print');if(m.addEventListener)m.addEventListener('change',function(e){if(e.matches)o();});}
|
|
1802
|
-
window.addEventListener('beforeprint',o);})()
|
|
1931
|
+
window.addEventListener('beforeprint',o);})();
|
|
1932
|
+
/* The popover opens on its own button, closes on a click outside it and on Escape. Same two listeners
|
|
1933
|
+
the clearance template carries, for the same markup; they move together when the top bar is shared. */
|
|
1934
|
+
${EXPORT_MENU_JS}</script>
|
|
1803
1935
|
<div class="wrap">
|
|
1804
|
-
<header class="hero">
|
|
1936
|
+
<header class="hero" id="summary">
|
|
1805
1937
|
${demoBannerHtml(demoData === true)}
|
|
1806
1938
|
${confLineHtml(delivery, productName)}
|
|
1807
1939
|
<h1 class="mark">${esc(title)}</h1>
|
|
@@ -1818,7 +1950,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1818
1950
|
if (!outs.length) return '';
|
|
1819
1951
|
const label = outs.length > 1;
|
|
1820
1952
|
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>
|
|
1953
|
+
return `<div class="sec" id="next"><span class="num"></span><h2>What happens next</h2></div>
|
|
1822
1954
|
<div class="panel actions"><div class="actgrp act-you">${body}</div></div>`;
|
|
1823
1955
|
})()}
|
|
1824
1956
|
|
|
@@ -1839,11 +1971,23 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1839
1971
|
newCaveats.map((c) => inlineMd(c)).join('<br>')}</p></div>` : ''}
|
|
1840
1972
|
|
|
1841
1973
|
<footer>
|
|
1842
|
-
|
|
1974
|
+
${/* THE FOOTER NAMES THE SAME THREE THINGS THE HEADER DOES. It read "<product>. / Matter
|
|
1975
|
+
<runId>. Issued <date · time>." — the identifier again, under a label that calls it the
|
|
1976
|
+
client's matter when it is the engine's run directory, and the publish clock again. The mock
|
|
1977
|
+
reads mark · search · issued on date. Built from the same parts as the identity line so the
|
|
1978
|
+
two cannot drift, and with the same rule: no part of it falls back to the identifier. */''}
|
|
1979
|
+
<!-- WHICH MODELS SERVED THIS RUN — the same line, from the same function, as the clearance
|
|
1980
|
+
report's footer. It used to ride the end of the scope block, which the 2026-09-16 redesign took
|
|
1981
|
+
off this page; the line is provenance rather than the narration that was ruled out, so it moved
|
|
1982
|
+
here. Owner ruling, 2026-09-17. Adjacent on purpose, as on the clearance side. -->${servedModelsLine(servedModels)}
|
|
1983
|
+
<span>${[title, productName].filter(Boolean).map(esc).join(' \u00b7 ')}${
|
|
1984
|
+
issuedDate ? `${title || productName ? ' \u00b7 ' : ''}issued on ${esc(issuedDate)}` : ''}${
|
|
1985
|
+
title || productName || issuedDate ? '.' : ''}</span>
|
|
1843
1986
|
${logoLockup({ mark: 16 })}
|
|
1844
1987
|
</footer>
|
|
1845
1988
|
</div>
|
|
1846
1989
|
</body></html>`;
|
|
1990
|
+
return doc.replace('<!--SECTION-STRIP-->', sectionStrip(doc, STRIP));
|
|
1847
1991
|
}
|
|
1848
1992
|
|
|
1849
1993
|
/**
|
|
@@ -1859,7 +2003,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1859
2003
|
* where the working notes live, and a data file that quietly re-admitted them would reopen exactly the
|
|
1860
2004
|
* leak this series closed.
|
|
1861
2005
|
*/
|
|
1862
|
-
export function knockoutReportData(findings, framework, { runId, codename, overall, issued, identity, registerCounts, registerRecords = null, ownerChecks = [], url, auditFile, customerKey, matter }) {
|
|
2006
|
+
export function knockoutReportData(findings, framework, { runId, codename, overall, issued, identity, registerCounts, registerRecords = null, ownerChecks = [], url, auditFile, customerKey, matter, servedModels = null }) {
|
|
1863
2007
|
const marks = findings?.marks ?? [];
|
|
1864
2008
|
return {
|
|
1865
2009
|
schema: 'report-data/1',
|
|
@@ -1870,6 +2014,9 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1870
2014
|
issued: issued || null,
|
|
1871
2015
|
url: url || null,
|
|
1872
2016
|
auditFile: auditFile || null,
|
|
2017
|
+
// The models that served the run, in first-use order, as tokens.mjs servedModels names them for a
|
|
2018
|
+
// client: a deployment's own name never, its tier instead. null when nothing was read.
|
|
2019
|
+
servedModels: Array.isArray(servedModels) ? servedModels : null,
|
|
1873
2020
|
level: {
|
|
1874
2021
|
searchLevel: identity?.level ?? null,
|
|
1875
2022
|
stageLabel: identity?.stageLabel ?? null,
|
|
@@ -1991,6 +2138,12 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1991
2138
|
classScope: counted.classScope ?? null,
|
|
1992
2139
|
classes: counted.classes ?? [],
|
|
1993
2140
|
...Object.fromEntries(COUNT_PREDICATES.map((p) => [p.key, Number.isFinite(counted.counts[p.key]?.total) ? counted.counts[p.key].total : null])),
|
|
2141
|
+
// The figure stays null for a floor — a floor is never a count to be summed — and the floor
|
|
2142
|
+
// rides beside it, so a reader of this file has the register's own figure and not only a null.
|
|
2143
|
+
...(COUNT_PREDICATES.some((p) => disclosedFloor(counted.counts[p.key]) !== null)
|
|
2144
|
+
? { floors: Object.fromEntries(COUNT_PREDICATES.filter((p) => disclosedFloor(counted.counts[p.key]) !== null)
|
|
2145
|
+
.map((p) => [p.key, disclosedFloor(counted.counts[p.key])])) }
|
|
2146
|
+
: {}),
|
|
1994
2147
|
// The close column is an aggregate, so the forms under it ride with it — a consumer that
|
|
1995
2148
|
// got the number and not the forms could restate the figure but never explain it, and this
|
|
1996
2149
|
// file is what the assistant drafts client mail from.
|