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
package/driver/portal-report.mjs
CHANGED
|
@@ -362,6 +362,12 @@ const MCP_HOST_RE = /https?:\/\/[A-Za-z0-9.-]+\/mcp/gi;
|
|
|
362
362
|
// introduces it and the full stop that closes it — leaving either behind puts a stray break or an orphan
|
|
363
363
|
// "." in the footer of every report.
|
|
364
364
|
const RATED_UNDER_RE = /<br\s*\/?>Rated under:\s*<span class="mono">[^<]*<\/span>\./gi;
|
|
365
|
+
// THE PROJECT LINE IS THE SAME CLASS AND WAS MISSING ITS STRIP. "Run under project: <name>" is
|
|
366
|
+
// internal provenance — an end-to-end arm reads it out of the published internal report — and it sat
|
|
367
|
+
// beside "Rated under" in the footer with nothing removing it here, so it reached every embedded
|
|
368
|
+
// reader exactly as written: the engine's phrase for the folder a job was filed in, and the folder's
|
|
369
|
+
// name. Same shape, same removal, one line apart.
|
|
370
|
+
const RUN_UNDER_PROJECT_RE = /<br\s*\/?>Run under project:\s*<span class="mono">[^<]*<\/span>\./gi;
|
|
365
371
|
|
|
366
372
|
const CONNECTOR_TOKEN_RE = /([?&])token=v1\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+(&)?/g;
|
|
367
373
|
|
|
@@ -796,8 +802,16 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
796
802
|
|
|
797
803
|
// The footer fingerprint. Counted like the rest so a renderer change that moves it shows up as a zero
|
|
798
804
|
// rather than as a line that quietly starts shipping again.
|
|
805
|
+
//
|
|
806
|
+
// ONE COUNTER PER LINE, AND THAT IS THE WHOLE POINT OF COUNTING. These two strips shared a counter, so
|
|
807
|
+
// the check that exists to catch a renderer moving this markup could not tell WHICH line moved: the
|
|
808
|
+
// renderer could rename one of them, that strip would match nothing, the other would still fire, and
|
|
809
|
+
// the count would read 1 exactly as it does when both work. A count that cannot fall to zero for one
|
|
810
|
+
// of the two things it counts is not a measurement of either.
|
|
799
811
|
let ratedUnderDropped = 0;
|
|
812
|
+
let runUnderProjectDropped = 0;
|
|
800
813
|
out = out.replace(RATED_UNDER_RE, () => { ratedUnderDropped += 1; return ""; });
|
|
814
|
+
out = out.replace(RUN_UNDER_PROJECT_RE, () => { runUnderProjectDropped += 1; return ""; });
|
|
801
815
|
|
|
802
816
|
// ── THE ASK-AI BAND COMES OUT FOR EVERY READER ──────────────────────────────────────────────────
|
|
803
817
|
//
|
|
@@ -854,7 +868,7 @@ export function prepareReportForEmbed(html, { staff = false, poolRoot = null, fe
|
|
|
854
868
|
out = injectEmbedLayer(out, { feedback });
|
|
855
869
|
|
|
856
870
|
return {
|
|
857
|
-
html: out, strippedNav, neutralised, mcpLeaks, tokensDropped, ratedUnderDropped,
|
|
871
|
+
html: out, strippedNav, neutralised, mcpLeaks, tokensDropped, ratedUnderDropped, runUnderProjectDropped,
|
|
858
872
|
internalTailsDropped, reviewerCodesDropped, unbalanced, missingCss,
|
|
859
873
|
};
|
|
860
874
|
}
|
|
@@ -98,6 +98,24 @@ import { fileURLToPath } from "node:url";
|
|
|
98
98
|
// re-declared: one number, one count, and every existing importer of this module keeps working.
|
|
99
99
|
export { DEFAULT_CLIENT_DAILY_RUNS, accountUsage };
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* What a requester is told when the day's allowance is gone.
|
|
103
|
+
*
|
|
104
|
+
* ONE SENTENCE, TWO PLACES. The server refuses with it and the screen warns with it, and a reader who
|
|
105
|
+
* meets both must not meet two different sentences about the same fact. The browser cannot import this
|
|
106
|
+
* module, so the portal composes its own copy and `portal-ui/test/allowanceParity.test.ts` pins the two
|
|
107
|
+
* together — the same arrangement the effort model already has.
|
|
108
|
+
*
|
|
109
|
+
* NAMED, NOT ADDRESSED. It read "ask your <name> contact", which puts a possessive and a noun around a
|
|
110
|
+
* name that is already the answer: on a deployment that has configured no brand the portal's fallback is
|
|
111
|
+
* the words "the operator", and the sentence rendered "ask your the operator contact to run this one for
|
|
112
|
+
* you". The line beside it on the same screen had always said "ask <name>", and that is the one the
|
|
113
|
+
* approved design draws.
|
|
114
|
+
*/
|
|
115
|
+
export const allowanceExhaustedLine = (cap, who) =>
|
|
116
|
+
`You have used all ${cap} of this account's searches for today. `
|
|
117
|
+
+ `The allowance resets at midnight UTC — or ask ${who} to run this one for you.`;
|
|
118
|
+
|
|
101
119
|
// One sentence, two places (the capability payload and the route that refuses). Never names the
|
|
102
120
|
// missing credential: this reaches a client screen, and our plumbing is not their business.
|
|
103
121
|
//
|
|
@@ -130,7 +148,7 @@ import { productRows, productRow, baseTurnaroundFor } from "./product-rows.mjs";
|
|
|
130
148
|
import { resolveForDoor, gateResolvedRequest } from "./door-gates.mjs";
|
|
131
149
|
import { resolveEffectiveScope } from "./effective-scope.mjs";
|
|
132
150
|
import { quoteForJob } from "./run-quote.mjs";
|
|
133
|
-
import { readFlagSnapshot, builtFor, registerCanCountFor, registerTerritoriesFor, caseLawReadyFor } from "./flag-snapshot.mjs";
|
|
151
|
+
import { readFlagSnapshot, builtFor, registerCanCountFor, registerTerritoriesFor, registerLabelFor, caseLawReadyFor } from "./flag-snapshot.mjs";
|
|
134
152
|
import { isDemo, demoPostureLine } from "./demo-posture.mjs";
|
|
135
153
|
import { triggerCapGap, triggerCapWarning } from "./trigger-cap.mjs"; // F51 — one answer, three surfaces
|
|
136
154
|
import { makeUpstream } from "./portal-upstream.mjs";
|
|
@@ -468,7 +486,16 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
468
486
|
// zombie face this state exists to end. pausedKind "operator" tells the UI which words to use.
|
|
469
487
|
const paused = s.state === "postponed" || s.state === "recovering" || s.state === "parked-for-human";
|
|
470
488
|
out.push({ ...(liveRetired ? { retired: true } : {}),
|
|
471
|
-
runId: s.runId, account: owner, ...(owner === "generic" ? { organisation: p.organisation ?? null } : {}), title: s.markName ?? s.slug,
|
|
489
|
+
runId: s.runId, account: owner, ...(owner === "generic" ? { organisation: p.organisation ?? null } : {}), title: s.markName ?? s.slug,
|
|
490
|
+
// THE RUN’S OWN PIPELINE, off the frozen policy sidecar this branch already reads — the same
|
|
491
|
+
// source the `product` field two lines down takes, so a row cannot contradict its own product.
|
|
492
|
+
// `s.lane` is the runner’s queue directory and it is not that: a knockout claimed from any other
|
|
493
|
+
// lane arrived here as a clearance, and Home then quoted it the clearance turnaround — hours,
|
|
494
|
+
// against a search that finishes in minutes. The queued rows below were fixed for exactly this
|
|
495
|
+
// and this row was left on the weaker field. The lane stays as the fallback for a run whose
|
|
496
|
+
// sidecar predates the stamp, and an unplaceable product stays a clearance, which is what every
|
|
497
|
+
// listing has always shown for something it could not identify.
|
|
498
|
+
kind: (policyFor(sp?.level)?.pipeline ?? (s.lane === "knockout" ? "knockout" : null)) === "knockout" ? "knockout-batch" : "clearance",
|
|
472
499
|
markName: typeof s.markName === "string" ? s.markName : null,
|
|
473
500
|
// The project, straight off the frozen sidecar this branch already reads as `p`. A LIVE run needs
|
|
474
501
|
// no publish stamp and no back-fill — the sidecar is right there, and freezeProfile has written
|
|
@@ -684,7 +711,7 @@ export function scanAccountRuns({ poolRoot, workspaceRoot, account = null, gener
|
|
|
684
711
|
// A failure `reason` is NOT client-safe, and the listing must not hand it to a browser.
|
|
685
712
|
//
|
|
686
713
|
// The engine writes `reason = String(e?.stack ?? e)` truncated to 200 characters
|
|
687
|
-
// (pipeline.mjs
|
|
714
|
+
// (pipeline.mjs, pipeline-knockout.mjs). That is a raw stack trace: absolute filesystem
|
|
688
715
|
// paths, internal module names, provider error text. Useful to staff, and none of a client's business
|
|
689
716
|
// — and redacting it in the UI would be theatre, because the field still travels over the wire and
|
|
690
717
|
// sits in devtools whatever React chooses to render.
|
|
@@ -1230,6 +1257,10 @@ export function makePortalService({
|
|
|
1230
1257
|
// THREE answers survive the read: null (unrestricted), an array, or undefined (the snapshot does not
|
|
1231
1258
|
// say — fail open). registerTerritoriesFor is what keeps them apart.
|
|
1232
1259
|
readTerritories = () => registerTerritoriesFor(readFlagSnapshot(poolRoot)),
|
|
1260
|
+
// What to CALL the wired register on screen. Beside `readTerritories` and injected for the same
|
|
1261
|
+
// reason: the covered set says a territory cannot be searched, and this says by what — the screen
|
|
1262
|
+
// cannot name the register from the key without printing an engine identifier at a client.
|
|
1263
|
+
readRegisterLabel = () => registerLabelFor(readFlagSnapshot(poolRoot)),
|
|
1233
1264
|
// — whether this deployment has a case-law source enrolled at all. Same
|
|
1234
1265
|
// injection and same snapshot as the two above, and the same tri-state: null is "the snapshot does not
|
|
1235
1266
|
// say", which must not render as a warning on a working box.
|
|
@@ -1280,6 +1311,7 @@ export function makePortalService({
|
|
|
1280
1311
|
const built = readBuilt();
|
|
1281
1312
|
const canCount = readCanCount();
|
|
1282
1313
|
const territories = readTerritories();
|
|
1314
|
+
const registerLabel = readRegisterLabel();
|
|
1283
1315
|
const caseLawReady = readCaseLaw();
|
|
1284
1316
|
return {
|
|
1285
1317
|
// Every product is LISTED whatever its state. A product that vanishes when unavailable leaves a
|
|
@@ -1335,6 +1367,16 @@ export function makePortalService({
|
|
|
1335
1367
|
// not told me", which fails open. A `covered ?? []` anywhere on this path offers zero territories
|
|
1336
1368
|
// on a production box.
|
|
1337
1369
|
...(territories === undefined ? {} : { territories }),
|
|
1370
|
+
// THE REGISTER'S NAME, sent whenever there is one. The covered set alone lets the screen mark a
|
|
1371
|
+
// territory as unreachable; it cannot say by WHAT, and the sentence the door refuses with names
|
|
1372
|
+
// the register ("China is not available with Signa, the register configured here"). A screen that
|
|
1373
|
+
// marked a chip without naming the register would be a second, vaguer wording for one fact.
|
|
1374
|
+
//
|
|
1375
|
+
// OMITTED, never nulled, exactly as `territories` above: absent means this deployment did not say,
|
|
1376
|
+
// and the screen must then mark the territory without naming a register rather than invent one.
|
|
1377
|
+
// It is the register's own display label, never the provider key — the key is an engine
|
|
1378
|
+
// identifier and has no place on a client's screen.
|
|
1379
|
+
...(registerLabel ? { registerLabel } : {}),
|
|
1338
1380
|
// `base` and `nativeLanguage` ride the LIST row, not just the record: the composer has to say what
|
|
1339
1381
|
// geography a saved search accepts while the row is being clicked, and fetching the record per
|
|
1340
1382
|
// selection would put that answer one round trip behind the Review button. Only `true` travels for
|
|
@@ -1842,9 +1884,7 @@ export function makePortalService({
|
|
|
1842
1884
|
return null;
|
|
1843
1885
|
}
|
|
1844
1886
|
if (used.today + 1 <= limit) return null;
|
|
1845
|
-
return { status: 429, json: { ok: false, errors: [
|
|
1846
|
-
`You have used all ${limit} of this account's searches for today. The allowance resets at midnight UTC — or ask your ${BRAND.name} contact to run this one for you.`,
|
|
1847
|
-
] } };
|
|
1887
|
+
return { status: 429, json: { ok: false, errors: [allowanceExhaustedLine(limit, BRAND.name)] } };
|
|
1848
1888
|
};
|
|
1849
1889
|
|
|
1850
1890
|
// /portal/api/run/plan — the confirmation gate (no spend)
|
|
@@ -32,7 +32,7 @@ import { writeUpViolations, writeUpMessage } from "./narrative-write-ups.mjs";
|
|
|
32
32
|
import { findRegistryArithmeticIssues, findRegistryViolations, splitBlocks } from "./registry-fidelity.mjs";
|
|
33
33
|
import { CLIENT_TIER_BY_COMPOSITE, joinFindingToBlock, parseBlockOrd, worstLiveBand, NO_RATED_CONFLICTS, deriveActionConditions, isUnconditionalProceed, verdictStance, joinAskToAnswer, projectAssessmentField, POSITION_REQUIRED_DISPOSITIONS, OFF_FIELD_GROUNDS, FINDINGS_SCHEMA_VERSION, netChainMarkers, STATEMENT_CLAUSE_MAX } from "./findings-model.mjs";
|
|
34
34
|
import { normalizeBand } from "./framework.mjs";
|
|
35
|
-
import { clientConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
35
|
+
import { clientConditions, unrenderableConditions, ENGINE_TOKEN_RE } from "./terminal-clamp.mjs"; // the reader's clause per condition, and the token shape it may never carry
|
|
36
36
|
import { knockoutNoteView, REQUEST_NOTE_WORDS, REQUEST_SUBJECT_WORDS } from "./findings-model.mjs"; // one reader for where a note prints
|
|
37
37
|
import { isEngineAppendedCaveat } from "./verify-knockout.mjs"; // ONE derivation for "the engine appended this caveat, not a seat"
|
|
38
38
|
|
|
@@ -1976,10 +1976,20 @@ export function clientConditionVoiceChecks({ verdictDoc }) {
|
|
|
1976
1976
|
const carrying = clientConditions(verdictDoc)
|
|
1977
1977
|
.map((c) => ({ c, m: String(c).match(ENGINE_TOKEN_RE) }))
|
|
1978
1978
|
.filter((x) => x.m);
|
|
1979
|
+
// THE SECOND CHECK IS WHAT KEEPS THE FIRST ONE HONEST. The fallback now composes the reader's
|
|
1980
|
+
// sentence from a pre-split reason's own counts, and DROPS a token-bearing reason it cannot compose
|
|
1981
|
+
// one for. Both outcomes leave the first check passing, and they mean opposite things: one is the
|
|
1982
|
+
// condition reaching the client in a lawyer's nouns, the other is the condition reaching nobody. A
|
|
1983
|
+
// single check reporting a clean page for both would be an absence read as a pass.
|
|
1984
|
+
const dropped = unrenderableConditions(verdictDoc);
|
|
1979
1985
|
return [check("client-condition-voice", "verdict", "report", carrying.length === 0,
|
|
1980
1986
|
carrying.length
|
|
1981
1987
|
? `${carrying.length} delivered condition${carrying.length === 1 ? "" : "s"} carr${carrying.length === 1 ? "ies" : "y"} an engine identifier, so the client's "conditional on:" list reads the run record rather than the reader's sentence: ${carrying.slice(0, 3).map((x) => `"${x.m[0]}"`).join(", ")}${carrying.length > 3 ? ` (+${carrying.length - 3} more)` : ""}. The clause exists at the clamp site; persist it and this list takes it. A sidecar written before clauses were persisted flags until the run is re-generated.`
|
|
1982
|
-
: "")
|
|
1988
|
+
: ""),
|
|
1989
|
+
check("client-condition-dropped", "verdict", "report", dropped.length === 0,
|
|
1990
|
+
dropped.length
|
|
1991
|
+
? `${dropped.length} condition${dropped.length === 1 ? " was" : "s were"} recorded for this run and reach no client surface: the run-record reason carries an engine identifier and no reader's sentence was stored or could be composed from it, so it is dropped rather than printed in engine voice. The report ships without it and the run record keeps it: ${dropped.slice(0, 2).map((r) => `"${r.slice(0, 80)}…"`).join(", ")}${dropped.length > 2 ? ` (+${dropped.length - 2} more)` : ""}. Add the defect's sentence to the clause authority in terminal-clamp.mjs, or persist a clause at the clamp site.`
|
|
1992
|
+
: "")];
|
|
1983
1993
|
}
|
|
1984
1994
|
|
|
1985
1995
|
export function statementCoherenceChecks({ verdictDoc }) {
|
package/driver/publish/index.mjs
CHANGED
|
@@ -18,10 +18,12 @@ import { readStore, requiredAbsent, nonClosingAbsences } from './publish-inputs.
|
|
|
18
18
|
import { clearanceReportData } from './report-data.mjs';
|
|
19
19
|
import { searchDepthRecord } from './search-depth.mjs'; // how much was read to reach the answer, as counts and tokens
|
|
20
20
|
import { parseFrameworkManifest } from '../framework.mjs';
|
|
21
|
-
import { rollupTokens } from '../tokens.mjs';
|
|
21
|
+
import { rollupTokens, servedModels } from '../tokens.mjs';
|
|
22
22
|
import { reportIdentityFor, productCoverageNote, isRegisterOnly } from '../search-policy.mjs';
|
|
23
23
|
import { readRecordArtifacts, bindFindingsToRecords, joinEvidenceStatus } from '../registry-fidelity.mjs';
|
|
24
24
|
import { deliveryFlagLines } from '../predelivery-lint.mjs';
|
|
25
|
+
import { unrenderableConditions } from '../terminal-clamp.mjs'; // — the conditions that reach no client surface
|
|
26
|
+
import { runLog } from '../log.mjs'; // — one line in the run record when one does
|
|
25
27
|
import { PROVIDERS, config } from '../driver.config.mjs';
|
|
26
28
|
import { declaredRecordOrigins } from '../record-origins.mjs';
|
|
27
29
|
import { NEUTRAL_DELIVERY, loadProfiles } from '../profiles.mjs';
|
|
@@ -791,6 +793,30 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
791
793
|
// the narrative, never in a status line). Best-effort; legacy runs have no sidecar.
|
|
792
794
|
let verdictInfo = null;
|
|
793
795
|
try { verdictInfo = JSON.parse(readFileSync(driverDir(runDir, 'verdict.json'), 'utf8')); } catch { /* legacy */ }
|
|
796
|
+
// ── THE CONDITIONS THAT REACH NO PAGE, RECORDED RATHER THAN LOST ────────────────────────────────
|
|
797
|
+
//
|
|
798
|
+
// A condition whose reader-facing sentence was never stored and cannot be composed from the run
|
|
799
|
+
// record's own counts is dropped instead of printed in the engine's words. That is right for the
|
|
800
|
+
// reader and it is a silent loss for everyone else, so the drop reports itself twice: an ordinary
|
|
801
|
+
// row on the workbook's gaps sheet, and one line in the run record here.
|
|
802
|
+
//
|
|
803
|
+
// THE WORDS ARE THE RUN RECORD'S OWN. The reason is what the clamp site wrote; the state word and the
|
|
804
|
+
// column headings are the gaps sheet's. Nothing on this path composes a sentence.
|
|
805
|
+
//
|
|
806
|
+
// A FRESH RUN NEVER REACHES THIS. Every clamp site stores its clause, so the list is empty and the
|
|
807
|
+
// sheet grows no row and the log gains no line. It bites on a republished archive, which is exactly
|
|
808
|
+
// where the operator has no other way to learn the page is short of a point.
|
|
809
|
+
const droppedConditions = unrenderableConditions(verdictInfo || {}).map((reason) => ({
|
|
810
|
+
area: 'Conditions', state: 'open', note: reason,
|
|
811
|
+
}));
|
|
812
|
+
// Best-effort, in the house pattern: a publish never fails for want of a log line, and a republish
|
|
813
|
+
// from a directory this process cannot write is still a publish.
|
|
814
|
+
if (droppedConditions.length && runDir) {
|
|
815
|
+
try {
|
|
816
|
+
runLog(runDir, { event: 'client-condition-dropped', n: droppedConditions.length,
|
|
817
|
+
reasons: droppedConditions.map((c) => c.note) });
|
|
818
|
+
} catch { /* the workbook row is the record that matters; this line is the second copy */ }
|
|
819
|
+
}
|
|
794
820
|
// doc 50 — the run's FROZEN framework manifest (band vocabulary). Present on band-doctrine runs;
|
|
795
821
|
// absent on every archived run (they render byte-identically on the legacy paths).
|
|
796
822
|
let framework = null;
|
|
@@ -902,7 +928,14 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
902
928
|
const reviewReceipts = {
|
|
903
929
|
// `family` rides along for deliveryFlagLines' fallback: a check whose id is not in the projection
|
|
904
930
|
// table still projects to its family's sentence rather than to anything the engine wrote.
|
|
905
|
-
|
|
931
|
+
// ONE CHECK IS DELIBERATELY NOT PROJECTED. `client-condition-dropped` has no sentence of its own
|
|
932
|
+
// in the projection table, so it fell through to its family's — "the delivered outcome and the
|
|
933
|
+
// report's own text do not agree" — which describes a contradiction. This is an omission: the
|
|
934
|
+
// outcome and the text agree perfectly and both are short of a point. It says what happened on the
|
|
935
|
+
// gaps sheet and in the run record now, in the run record's own words, so the borrowed sentence
|
|
936
|
+
// comes off rather than being replaced by a better one (ruled 2026-09-17).
|
|
937
|
+
lint: (lintSink?.checks ?? []).filter(c => !c.pass && c.id !== 'client-condition-dropped')
|
|
938
|
+
.map(c => ({ id: c.id ?? '', family: c.family ?? '', detail: c.detail ?? '' })),
|
|
906
939
|
engagement: integritySink?.engagement ?? null,
|
|
907
940
|
registryCorrections: Array.isArray(integritySink?.registryCorrections)
|
|
908
941
|
? integritySink.registryCorrections.filter((c) => c && c.from != null && c.to != null) : [],
|
|
@@ -1032,7 +1065,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1032
1065
|
// the same rule (the workbook's own BANNED gate had already started firing on the raw detail —
|
|
1033
1066
|
// advisory, so CI stayed green). reviewReceipts.lint keeps its raw detail for the internal
|
|
1034
1067
|
// readers above (fetchState reads registry-record-coverage's URIs out of it).
|
|
1035
|
-
counts = await buildAudit({ findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1068
|
+
counts = await buildAudit({ droppedConditions, findings, coverage, contextNotes, coverageJudgment, markAssessment, corrections: correctionsDoc, fetchState, verdict: verdictInfo, jurisdiction, commonLawJoinedTerms, registerOnly, clientGate, lintFailures: deliveryFlagLines(reviewReceipts.lint), productName, registerPublishesRecordPages: runOrigins == null ? null : runOrigins.length > 0, recordLinks: officeLinks?.byUri ?? null }, auditParsed, join(poolRunDir, auditFile), fm.title, fm);
|
|
1036
1069
|
grpRead(join(poolRunDir, auditFile), 0o640);
|
|
1037
1070
|
if (counts?.gateViolations?.length) console.warn(`[audit-workbook] advisory: ${counts.gateViolations.join(' | ')}`);
|
|
1038
1071
|
} catch (e) {
|
|
@@ -1053,6 +1086,11 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1053
1086
|
// rather than wire the overlay in: client names saturate privileged report prose — a partial blur is a
|
|
1054
1087
|
// worse demo than no toggle.
|
|
1055
1088
|
const reportNav = siteNav(poolRoot, 'report', null, '../', { anon: false });
|
|
1089
|
+
// The models that served this run, named as a client may read them (tokens.mjs servedModels). The closing
|
|
1090
|
+
// line of the report's scope section, the data file and the meta below all take this one read; a read
|
|
1091
|
+
// that throws records nothing rather than failing the publish.
|
|
1092
|
+
let served = null;
|
|
1093
|
+
try { served = runDir ? servedModels(runDir) : null; } catch { served = null; }
|
|
1056
1094
|
// `demoData` is resolved above the report.md write — one answer, every surface.
|
|
1057
1095
|
// ── HOW MUCH WAS READ TO REACH THE ANSWER ──────────────────────────────────────────────────────
|
|
1058
1096
|
// Derived here rather than in a stage so a REPUBLISH of an archived run picks the fields up with no
|
|
@@ -1076,7 +1114,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1076
1114
|
writeRO('search-depth.json', JSON.stringify(searchDepth, null, 2));
|
|
1077
1115
|
} catch { /* the depth record is additive — a publish never fails for want of it */ }
|
|
1078
1116
|
|
|
1079
|
-
writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
|
|
1117
|
+
writeRO('report.html', renderHtml(parsed, findings, coverage, { demoData, servedModels: served, productName, depthNote, scopeBasis, auditFile: auditFile || undefined, runId, delivery: deliv, recordsByUri, contextNotes, coverageJudgment: coverageJudgmentDisplay, markAssessment, fourAnswers, homeHref: '../index.html', nav: reportNav, chromeHref: '../assets/chrome.css', issued, asOf, verdictInfo, framework, searchedJurisdictions, caseLawByOrdinal, caseLawNotice, enforcerSignals, recordOrigin, recordOrigins: runOrigins, recordCitation: runProviderConf?.recordCitation ?? null, recordLinks: officeLinks?.byUri ?? null, providerLabel, seniorRights, findingsSchemaVersion, searchDepth }));
|
|
1080
1118
|
// ONE report (spec 2026-07-30 §5): report.client.html is no longer written. The knockout lane's own
|
|
1081
1119
|
// collapse note is the precedent: "two renderings of one run is how the wrong link gets sent". The
|
|
1082
1120
|
// client host serves the same report.html through the portal's readReport() (cleaning built in) — its
|
|
@@ -1150,7 +1188,7 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1150
1188
|
// is the LAST resort (a level today's registry no longer knows). meta.json below keeps the frozen
|
|
1151
1189
|
// stamp BY DESIGN — its readers re-derive.
|
|
1152
1190
|
auditFile, searchLevel: searchPolicy?.level ?? null, stageLabel: stageLabel ?? searchPolicy?.stageLabel ?? null,
|
|
1153
|
-
engineCommit: engineCommit(),
|
|
1191
|
+
engineCommit: engineCommit(), servedModels: served,
|
|
1154
1192
|
framework, verdictInfo, findings, coverage, contextNotes, markAssessment, fourAnswers, askAnswers,
|
|
1155
1193
|
actions: actionsRegister, jurisdiction, searchedJurisdictions, scopeBasis, caption: fm.overall_caption ?? null,
|
|
1156
1194
|
});
|
|
@@ -1175,6 +1213,9 @@ export async function publishReport({ runId, codename, reportMd, auditMd, findin
|
|
|
1175
1213
|
issuedAt,
|
|
1176
1214
|
// WHICH BUILD produced this. null off a git checkout — a provenance stamp never fails a publish.
|
|
1177
1215
|
engineCommit: engineCommit(),
|
|
1216
|
+
// The models that served the run, named as a client may read them (tokens.mjs servedModels). Absent when
|
|
1217
|
+
// nothing was read, so a meta from before the record keeps its shape; [] when turns ran and named none a client may read.
|
|
1218
|
+
servedModels: served ?? undefined,
|
|
1178
1219
|
kind: 'clearance', recordLinks: officeLinks?.tally ?? undefined, // per office: linked, or cited by number and why; only where the register has no record pages
|
|
1179
1220
|
searchLevel: searchPolicy?.level ?? undefined,
|
|
1180
1221
|
// Display-only face of the level ("Depth 4"), frozen alongside it so the list can show which reads
|
|
@@ -21,6 +21,7 @@ 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
|
|
|
@@ -518,6 +519,10 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
518
519
|
// — resolved once for the whole publish, off the same two sources the clearance
|
|
519
520
|
// publisher uses (frozen sidecar, then roster; either marks, neither un-marks).
|
|
520
521
|
const demoData = resolveDemoData({ runDir, customerKey });
|
|
522
|
+
// The models that served this batch, named as a client may read them — read once, like the demo answer,
|
|
523
|
+
// because it is a property of the run. A read that throws records nothing rather than failing a publish.
|
|
524
|
+
let served = null;
|
|
525
|
+
try { served = runDir ? servedModels(runDir) : null; } catch { served = null; }
|
|
521
526
|
const reports = markRows.map((m) => {
|
|
522
527
|
const slug = kebab(m.name);
|
|
523
528
|
const file = single ? 'report.html' : `report-${slug}.html`;
|
|
@@ -558,12 +563,13 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
558
563
|
// document says it did not search is what the run was instructed not to search.
|
|
559
564
|
searchPolicy: searchPolicy ?? null,
|
|
560
565
|
delivery,
|
|
566
|
+
servedModels: served,
|
|
561
567
|
}));
|
|
562
568
|
// The run as data — what the assistant drafts client-facing mail from, and the shape the portal's
|
|
563
569
|
// native-render path has been reading for since before anything wrote it. One per report, so the
|
|
564
570
|
// native render of a per-mark document is that mark's data and not the batch's.
|
|
565
571
|
writeRO(dataFile, JSON.stringify(knockoutReportData(one, framework, {
|
|
566
|
-
runId, codename, overall: markBand, issued, registerCounts, registerRecords, ownerChecks, auditFile, customerKey, matter: runId,
|
|
572
|
+
runId, codename, overall: markBand, issued, registerCounts, registerRecords, ownerChecks, auditFile, customerKey, matter: runId, servedModels: served,
|
|
567
573
|
identity: { ...identity, level: searchPolicy?.level ?? null },
|
|
568
574
|
url: reportUrlFor(slug),
|
|
569
575
|
}), null, 2));
|
|
@@ -625,6 +631,9 @@ export async function publishKnockout({ runId, codename, runDir, findings, plan,
|
|
|
625
631
|
.filter(Boolean).join(' — '),
|
|
626
632
|
markName: batchMarkName(markNames) ?? undefined,
|
|
627
633
|
engineCommit: engineCommit(),
|
|
634
|
+
// The models that served the batch, named as a client may read them (tokens.mjs servedModels). Absent when
|
|
635
|
+
// nothing was read, so a meta from before the record keeps its shape; [] when turns ran and named none a client may read.
|
|
636
|
+
servedModels: served ?? undefined,
|
|
628
637
|
client: null, customerKey: customerKey || 'generic',
|
|
629
638
|
// WHICH ORGANISATION'S GENERIC this batch was filed under, read from the frozen sidecar exactly as
|
|
630
639
|
// the clearance publisher reads it. Absent on a company's batch and on one filed before
|
|
@@ -47,7 +47,8 @@ import { COUNT_BASIS, COUNT_PREDICATES, countsForMark, countLine, variantFormsLi
|
|
|
47
47
|
import { RECORD_BASIS, recordsForMark, recordsLine } from '../register-records.mjs';
|
|
48
48
|
import { officeLinkSentences } from './office-record-links.mjs';
|
|
49
49
|
import { knockoutFindingViews, splitKnockoutNotes } from '../findings-model.mjs';
|
|
50
|
-
import { demoBannerHtml } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
50
|
+
import { demoBannerHtml, servedModelsLine } from './render.mjs'; // — the SAME banner the clearance template renders, not a second wording
|
|
51
|
+
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
52
|
// — the two facts the register card is allowed to read off a raw record, and NEITHER is minted
|
|
52
53
|
// here. `makeClassifyStatus` and `isAllClass` are the screening lane's own, already shipped, already
|
|
53
54
|
// fail-open; re-deriving either in a renderer would be this file starting a second status vocabulary,
|
|
@@ -1658,6 +1659,10 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1658
1659
|
// renders the same plain "Privileged & Confidential" this template always printed — so the ~15 unit
|
|
1659
1660
|
// fixtures and both render-check scripts, none of which pass one, are unchanged by its arrival.
|
|
1660
1661
|
delivery = null,
|
|
1662
|
+
// The models that served the batch, as tokens.mjs servedModels names them for a client: the scope
|
|
1663
|
+
// section's closing line. Defaults to null, so a fixture or an archived run that passes none renders
|
|
1664
|
+
// as it always did.
|
|
1665
|
+
servedModels = null,
|
|
1661
1666
|
// — IS THIS AN INVENTED MARK? This template had no demo handling of any kind, so
|
|
1662
1667
|
// every demo knockout shipped looking real: an invented mark, a real register basis, a real-looking
|
|
1663
1668
|
// risk assessment, a published URL, and nothing saying the matter is fiction. The clearance template
|
|
@@ -1668,6 +1673,11 @@ export function renderKnockoutHtml(findings, framework, {
|
|
|
1668
1673
|
const marks = findings?.marks ?? [];
|
|
1669
1674
|
const title = batchTitle(marks);
|
|
1670
1675
|
const productName = identity?.identity ?? null;
|
|
1676
|
+
// A DATE, NOT A TIMESTAMP. `issued` is composed as `<date> · <time>` at publish, and the time is the
|
|
1677
|
+
// minute the file was written — it tells a reader nothing and implies a precision the work does not
|
|
1678
|
+
// have. Every mock issues the date alone. Taken by pattern rather than by splitting on the
|
|
1679
|
+
// separator, so a format that changes falls through whole instead of being cut in the wrong place.
|
|
1680
|
+
const issuedDate = (String(issued ?? '').match(/^\d{4}-\d{2}-\d{2}/) || [])[0] ?? issued;
|
|
1671
1681
|
const overallStop = bandStop(framework, overall);
|
|
1672
1682
|
const summary = findings?.batch?.executiveSummary ?? '';
|
|
1673
1683
|
const caveats = findings?.batch?.standardCaveats ?? [];
|
|
@@ -1776,9 +1786,44 @@ ${filings}`
|
|
|
1776
1786
|
${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
1787
|
${logoLockup({ mark: 20, tag: '' })}<span class="sp"></span>
|
|
1778
1788
|
<span class="tb-risk" style="background:var(${overallStop})">${esc(overall ?? '—')}</span>
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1789
|
+
${/* THE IDENTITY LINE NAMES THE MARK AND THE SEARCH, NOT THE RUN. It printed `matter || runId`,
|
|
1790
|
+
and the caller passes the runId AS the matter, so what a client read was the engine's own run
|
|
1791
|
+
identifier — on a real run a temporary-directory name carrying a capture suffix. It is ours, it
|
|
1792
|
+
means nothing to them, and it is the one string on the page that could not be shown to anyone.
|
|
1793
|
+
The mock reads mark then search type, and both are already in hand here.
|
|
1794
|
+
There is NO fall back to the identifier: with no product name the mark stands alone, and with
|
|
1795
|
+
neither the line does not render. A run whose registry identity is missing is exactly the case
|
|
1796
|
+
that used to print the raw name, so falling back would keep the defect for the runs that have
|
|
1797
|
+
it. */''}
|
|
1798
|
+
${title || productName ? `<span class="mono tb-matter" style="font-size:11px;color:var(--faint)">${
|
|
1799
|
+
[title, productName].filter(Boolean).map(esc).join(' / ')}</span>` : ''}
|
|
1800
|
+
${issuedDate ? `<span class="mono tb-issued"><span aria-hidden="true">🗓 </span>Issued on ${esc(issuedDate)}</span>` : ''}
|
|
1801
|
+
${/* THE EXPORT CONTROL, which this template had never emitted. The approved header carries it and the
|
|
1802
|
+
clearance report has it; the knockout offered a reader who opens the file no route to a PDF at
|
|
1803
|
+
all, while its own stylesheet still described the utility buttons as "used by the topbar Export
|
|
1804
|
+
popover" — the styling for a control that was not there.
|
|
1805
|
+
NO TICK WORDING AND NO SELECT-ALL. The clearance's entry reads "Export PDF (ticked findings)"
|
|
1806
|
+
and its popover offers Select all / Select none, because its exportPDF filters to the ticked
|
|
1807
|
+
ones. This template has no pickbox anywhere, so `pickAll` is deliberately not defined here and a
|
|
1808
|
+
tick verb would name a control that cannot exist. What prints is the whole document.
|
|
1809
|
+
EVERY WORD IN IT IS ALREADY THE PRODUCT'S. The board draws the Export button and its caret and
|
|
1810
|
+
stops there — it does not draw what the menu contains — so the entries are the clearance
|
|
1811
|
+
report's own, verbatim, minus the tick wording that names a control this template does not have.
|
|
1812
|
+
The heading this first carried ("Export") was written here and is gone: a word on a page a
|
|
1813
|
+
client reads is the owner's to choose, and the board does not ask for one.
|
|
1814
|
+
THE MARKUP IS THE CLEARANCE'S, RE-EMITTED RATHER THAN SHARED, and that is a debt this change
|
|
1815
|
+
takes on knowingly: `render.mjs` is frozen at a content hash, and extracting its top bar is its
|
|
1816
|
+
own change with its own byte-comparison against a real archived run. Filed as a follow-up. The
|
|
1817
|
+
class names are the shared vocabulary this file already emits, so report.css styles both. */''}
|
|
1818
|
+
<div class="tb-menu">
|
|
1819
|
+
<button type="button" class="tbbtn tb-ask no-print">\u2726 <span class="tb-lbl">Ask AI</span></button>
|
|
1820
|
+
${EXPORT_TOGGLE}
|
|
1821
|
+
${exportPopover(`
|
|
1822
|
+
<button class="util primary" onclick="exportPDF()">\u2b07 Export PDF</button>
|
|
1823
|
+
<div class="tb-sep"></div>
|
|
1824
|
+
<div class="tb-row"><button class="util" onclick="openAll(true)">Expand all</button><button class="util" onclick="openAll(false)">Collapse all</button></div>
|
|
1825
|
+
`)}
|
|
1826
|
+
</div>
|
|
1782
1827
|
</div>
|
|
1783
1828
|
</div>
|
|
1784
1829
|
<div class="fab-stack">${themeButton()}</div>
|
|
@@ -1799,7 +1844,10 @@ function openAll(v){document.querySelectorAll('details.ko-full').forEach(functio
|
|
|
1799
1844
|
function exportPDF(){window.print();}
|
|
1800
1845
|
(function(){var o=function(){openAll(true);};
|
|
1801
1846
|
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);})()
|
|
1847
|
+
window.addEventListener('beforeprint',o);})();
|
|
1848
|
+
/* The popover opens on its own button, closes on a click outside it and on Escape. Same two listeners
|
|
1849
|
+
the clearance template carries, for the same markup; they move together when the top bar is shared. */
|
|
1850
|
+
${EXPORT_MENU_JS}</script>
|
|
1803
1851
|
<div class="wrap">
|
|
1804
1852
|
<header class="hero">
|
|
1805
1853
|
${demoBannerHtml(demoData === true)}
|
|
@@ -1839,7 +1887,18 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1839
1887
|
newCaveats.map((c) => inlineMd(c)).join('<br>')}</p></div>` : ''}
|
|
1840
1888
|
|
|
1841
1889
|
<footer>
|
|
1842
|
-
|
|
1890
|
+
${/* THE FOOTER NAMES THE SAME THREE THINGS THE HEADER DOES. It read "<product>. / Matter
|
|
1891
|
+
<runId>. Issued <date · time>." — the identifier again, under a label that calls it the
|
|
1892
|
+
client's matter when it is the engine's run directory, and the publish clock again. The mock
|
|
1893
|
+
reads mark · search · issued on date. Built from the same parts as the identity line so the
|
|
1894
|
+
two cannot drift, and with the same rule: no part of it falls back to the identifier. */''}
|
|
1895
|
+
<!-- WHICH MODELS SERVED THIS RUN — the same line, from the same function, as the clearance
|
|
1896
|
+
report's footer. It used to ride the end of the scope block, which the 2026-09-16 redesign took
|
|
1897
|
+
off this page; the line is provenance rather than the narration that was ruled out, so it moved
|
|
1898
|
+
here. Owner ruling, 2026-09-17. Adjacent on purpose, as on the clearance side. -->${servedModelsLine(servedModels)}
|
|
1899
|
+
<span>${[title, productName].filter(Boolean).map(esc).join(' \u00b7 ')}${
|
|
1900
|
+
issuedDate ? `${title || productName ? ' \u00b7 ' : ''}issued on ${esc(issuedDate)}` : ''}${
|
|
1901
|
+
title || productName || issuedDate ? '.' : ''}</span>
|
|
1843
1902
|
${logoLockup({ mark: 16 })}
|
|
1844
1903
|
</footer>
|
|
1845
1904
|
</div>
|
|
@@ -1859,7 +1918,7 @@ window.addEventListener('beforeprint',o);})();</script>
|
|
|
1859
1918
|
* where the working notes live, and a data file that quietly re-admitted them would reopen exactly the
|
|
1860
1919
|
* leak this series closed.
|
|
1861
1920
|
*/
|
|
1862
|
-
export function knockoutReportData(findings, framework, { runId, codename, overall, issued, identity, registerCounts, registerRecords = null, ownerChecks = [], url, auditFile, customerKey, matter }) {
|
|
1921
|
+
export function knockoutReportData(findings, framework, { runId, codename, overall, issued, identity, registerCounts, registerRecords = null, ownerChecks = [], url, auditFile, customerKey, matter, servedModels = null }) {
|
|
1863
1922
|
const marks = findings?.marks ?? [];
|
|
1864
1923
|
return {
|
|
1865
1924
|
schema: 'report-data/1',
|
|
@@ -1870,6 +1929,9 @@ export function knockoutReportData(findings, framework, { runId, codename, overa
|
|
|
1870
1929
|
issued: issued || null,
|
|
1871
1930
|
url: url || null,
|
|
1872
1931
|
auditFile: auditFile || null,
|
|
1932
|
+
// The models that served the run, in first-use order, as tokens.mjs servedModels names them for a
|
|
1933
|
+
// client: a deployment's own name never, its tier instead. null when nothing was read.
|
|
1934
|
+
servedModels: Array.isArray(servedModels) ? servedModels : null,
|
|
1873
1935
|
level: {
|
|
1874
1936
|
searchLevel: identity?.level ?? null,
|
|
1875
1937
|
stageLabel: identity?.stageLabel ?? null,
|