@nextcommerce/campaigns-os 1.50.0 → 1.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/CHANGELOG.md +474 -0
  2. package/agents/claude/CLAUDE.md +4 -2
  3. package/agents/codex/AGENTS.md +3 -1
  4. package/agents/copilot/copilot-instructions.md +3 -1
  5. package/agents/cursor/campaigns-os.mdc +3 -1
  6. package/campaign-spec/dist/rules/campaign-metadata.d.ts +5 -1
  7. package/campaign-spec/dist/rules/campaign-metadata.js +9 -2
  8. package/campaign-spec/dist/rules/design-source-shape.js +13 -3
  9. package/campaign-spec/dist/rules/sdk-version.js +2 -1
  10. package/compatibility.json +1 -1
  11. package/contracts/commerce-surface-catalog.json +26 -46
  12. package/contracts/effects.v1.json +282 -14
  13. package/contracts/release-ledger.json +1233 -0
  14. package/contracts/supported-surface.json +2 -2
  15. package/contracts/template-brand-contract.shared-commerce.v0.json +2 -2
  16. package/contracts/template-slot-manifest.shared-content-core.v0.json +24 -0
  17. package/docs/brand-theme-bridge.md +2 -0
  18. package/docs/build-packet.md +148 -23
  19. package/docs/campaign-build-brief.md +57 -2
  20. package/docs/effects.md +14 -0
  21. package/docs/local-setup.md +1 -1
  22. package/docs/orientation-contract-reference.md +1 -1
  23. package/docs/polish-evidence.md +12 -0
  24. package/docs/progress-snapshots.md +2 -4
  25. package/docs/qa-and-test-orders.md +47 -4
  26. package/docs/readback.md +19 -2
  27. package/docs/runtime-readiness.md +1 -1
  28. package/docs/sdk-storage-compatibility.md +1 -1
  29. package/docs/skills-revision.md +10 -10
  30. package/package.json +1 -1
  31. package/skills/campaign-lifecycle-orientation/SKILL.md +3 -3
  32. package/skills/campaign-readback-classification/SKILL.md +3 -3
  33. package/skills/campaign-run-evidence/SKILL.md +3 -3
  34. package/skills/contribution-intake/SKILL.md +3 -3
  35. package/skills/next-campaigns-build/SKILL.md +5 -3
  36. package/skills/next-campaigns-os/SKILL.md +11 -20
  37. package/skills/next-campaigns-os/references/session-intake.md +17 -9
  38. package/skills/next-campaigns-os-setup/SKILL.md +4 -3
  39. package/skills/next-campaigns-polish/SKILL.md +7 -5
  40. package/skills/next-campaigns-qa/SKILL.md +8 -5
  41. package/skills.json +10 -10
  42. package/src/adapter-decision-contract.mjs +1 -1
  43. package/src/brand-theme.mjs +33 -20
  44. package/src/brief-summary.mjs +275 -0
  45. package/src/build-brief.mjs +229 -30
  46. package/src/built-site-scope.mjs +39 -6
  47. package/src/built-smoke-qc.mjs +1117 -0
  48. package/src/campaign-identity.mjs +36 -2
  49. package/src/cart-placeholders.mjs +730 -0
  50. package/src/cli.mjs +467 -143
  51. package/src/commercial-journey.mjs +65 -4
  52. package/src/commercial-parity.mjs +6 -1
  53. package/src/contrast.mjs +649 -0
  54. package/src/design-source-publication.mjs +110 -3
  55. package/src/doctor/checks.mjs +415 -33
  56. package/src/doctor/inspect.mjs +53 -2
  57. package/src/doctor/next-step.mjs +16 -7
  58. package/src/input-currency.mjs +394 -0
  59. package/src/input-refresh.mjs +708 -0
  60. package/src/invocation.mjs +5 -2
  61. package/src/local-preview-policy.mjs +1 -1
  62. package/src/local-proof.mjs +4 -1
  63. package/src/polish-browser.mjs +660 -141
  64. package/src/polish-capture.mjs +1 -1
  65. package/src/polish-deadline.mjs +7 -0
  66. package/src/polish-media-weight.mjs +492 -0
  67. package/src/polish-node.mjs +400 -28
  68. package/src/polish-readability.mjs +828 -0
  69. package/src/progress-node.mjs +19 -3
  70. package/src/qa-browser.mjs +439 -180
  71. package/src/qa-content-params.mjs +889 -0
  72. package/src/qa-node.mjs +116 -13
  73. package/src/qa-order-bump.mjs +22 -1
  74. package/src/qa-policy-links.mjs +1019 -0
  75. package/src/qa-tracking-params.mjs +1389 -0
  76. package/src/qa-url-privacy.mjs +168 -0
  77. package/src/qc-accept.mjs +482 -0
  78. package/src/qc-check-registry.mjs +91 -0
  79. package/src/qc-results.mjs +1341 -0
  80. package/src/readback.mjs +58 -10
  81. package/src/sdk-attribute-index.mjs +71 -0
  82. package/src/sdk-markup.mjs +2 -2
  83. package/src/sdk-storage-compatibility.mjs +63 -3
  84. package/src/source-prep.mjs +37 -7
  85. package/src/stage-ledger.mjs +235 -44
  86. package/src/stage-record.mjs +165 -33
  87. package/src/tooling-setup.mjs +1 -1
@@ -0,0 +1,1341 @@
1
+ // The shared QC results: one result shape,
2
+ // one storage rule per leg, and the readers that re-derive every stored result
3
+ // from its raw package capture on every read.
4
+ //
5
+ // Storage per leg:
6
+ // - doctor: derived.qc_results[], recomputed from built HTML on every run;
7
+ // - polish: one package-owned record per stages.polish.evidence.visual_review
8
+ // key (POLISH_RECORDS): media_weight, re-derived from its cells and
9
+ // cross-checked against the page_load capture, and readability, whose
10
+ // elements are re-derived through the shared contrast helper and whose
11
+ // route set is recomputed from the built output;
12
+ // - qa: stages.qa.evidence.qc_results[] beside qc_build_fingerprint,
13
+ // re-derived from the qc.* assertions of the full QA verdict.
14
+ //
15
+ // A reader never reports `pass` for an input it could not re-derive: a failed
16
+ // check reads `unexercised` with stale_binding or evidence_not_reproducible,
17
+ // and a leg with no rows for an applicable check is listed in the handoff
18
+ // coverage (silence is not a result). The committed QA sidecar,
19
+ // stages.qa.waivers and report.waivers[] are never read here.
20
+ import { createHash } from "node:crypto";
21
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
22
+ import { isAbsolute, relative, resolve } from "node:path";
23
+
24
+ import { campaignSidecarPaths } from "./campaign-workspace.mjs";
25
+ import { checkpointStateFingerprint } from "./checkpoint-waiver.mjs";
26
+ import { contrastToolkit } from "./contrast.mjs";
27
+ import { sameFile } from "./fs-identity.mjs";
28
+ import { buildPolishCaptureIntegrity, canonicalJson, captureOrigin, mediaFetchedResources } from "./polish-capture.mjs";
29
+ import { currentBuildFingerprint, currentSourcePackageMaterialFingerprint } from "./polish-gate.mjs";
30
+ import { sidecarPathForPacket } from "./qa-sidecar.mjs";
31
+ import { QA_ASSERTION_FAMILY_VOCABULARY, QA_SCHEMA_VERSION, SEVERITY, STATUS } from "./qa-verdict.mjs";
32
+ import { QC_CHECK_REGISTRY, loadedQcRederivers, qcChecksForLeg, qcChecksForRecord } from "./qc-check-registry.mjs";
33
+ import { STORE_PAGE_MATCHERS } from "./spec-derive-store.mjs";
34
+
35
+ export const QC_RESULT_SCHEMA = "campaigns-os-qc-result/v0";
36
+ export const QC_RESULTS = Object.freeze(["pass", "warning", "review", "unexercised", "excluded"]);
37
+ export const QC_LEGS = Object.freeze(["doctor", "polish", "qa"]);
38
+ export const QC_REASON = Object.freeze({
39
+ STALE_BINDING: "stale_binding",
40
+ EVIDENCE_NOT_REPRODUCIBLE: "evidence_not_reproducible",
41
+ LEG_NOT_RUN: "leg_not_run",
42
+ NOT_CAPTURED: "not_captured_by_this_version",
43
+ });
44
+ export const QC_PRODUCERS = Object.freeze({
45
+ doctor: "campaigns-os doctor",
46
+ polish: "campaigns-os polish capture",
47
+ qa: "campaigns-os qa run",
48
+ });
49
+ export const MEDIA_WEIGHT_SCHEMA = "campaigns-os-polish-media-weight/v0";
50
+ const PAGE_LOAD_SCHEMA = "campaigns-os-polish-page-load/v0";
51
+ const ROUTE_CAPTURE_SCHEMA = "campaigns-os-polish-route-capture/v0";
52
+ // The binding fields of the page_load subject (pageLoadCheckpointSubject) and
53
+ // of one route capture's subject (captureSubject), exactly, besides
54
+ // build_fingerprint. The build binding is read apart: absent, null or another
55
+ // build is staleness (stale_binding), never a malformed subject.
56
+ const PAGE_LOAD_SUBJECT_FIELDS = Object.freeze(["campaign_slug", "route_scope", "routes", "viewports"]);
57
+ const CAPTURE_SUBJECT_FIELDS = Object.freeze(["campaign_slug", "requested_route", "final_document_route", "viewport"]);
58
+ const QA_RUNTIME_PREFIX = "campaigns-os-node-qa@";
59
+ const FINGERPRINT = /^sha256:[a-f0-9]{64}$/;
60
+ const E = QC_REASON.EVIDENCE_NOT_REPRODUCIBLE;
61
+
62
+ // Result precedence when members aggregate into one row (excluded is ignored).
63
+ const PRECEDENCE = Object.freeze(["warning", "review", "unexercised", "pass"]);
64
+
65
+ // QA verdict mapping per result. unexercised is
66
+ // manual_review + warn, after src/qa-browser.mjs's unexercised-path precedent,
67
+ // never skipped: skipped is invisible to computeDisposition and exceptions[].
68
+ export const QC_QA_ASSERTION_STATUS = Object.freeze({
69
+ pass: Object.freeze({ status: STATUS.PASS, severity: SEVERITY.INFO }),
70
+ warning: Object.freeze({ status: STATUS.WARN, severity: SEVERITY.WARN }),
71
+ review: Object.freeze({ status: STATUS.MANUAL_REVIEW, severity: SEVERITY.WARN }),
72
+ unexercised: Object.freeze({ status: STATUS.MANUAL_REVIEW, severity: SEVERITY.WARN }),
73
+ excluded: Object.freeze({ status: STATUS.SKIPPED, severity: SEVERITY.INFO }),
74
+ });
75
+
76
+ const isPlainObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
77
+ const isNonEmptyString = (value) => typeof value === "string" && value.trim() !== "";
78
+ const sameJson = (a, b) => canonicalJson(a ?? null) === canonicalJson(b ?? null);
79
+ const validTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Result shape
83
+
84
+ export function qcResultId(subject) {
85
+ return [subject?.check, subject?.page, ...(subject?.viewport == null ? [] : [subject.viewport]), subject?.key].map((part) => String(part ?? "")).join(":");
86
+ }
87
+
88
+ export function qcStateFingerprint({ subject, state }) {
89
+ return checkpointStateFingerprint({ scope: "qc", subject, state });
90
+ }
91
+
92
+ export const fingerprint12 = (fingerprint) => String(fingerprint || "").replace(/^sha256:/, "").slice(0, 12);
93
+ export const qcResultRef = (result) => `${result.id}@${fingerprint12(result.state_fingerprint)}`;
94
+
95
+ // Only a warning whose members are all resolved can be accepted.
96
+ export function qcAcceptEligible(result, members = []) {
97
+ return result === "warning" && !(Array.isArray(members) ? members : []).some((member) => member?.result === "review" || member?.result === "unexercised");
98
+ }
99
+
100
+ function validMember(member) {
101
+ return isPlainObject(member)
102
+ && member.key != null
103
+ && QC_RESULTS.includes(member.result)
104
+ && (member.result === "pass" ? member.reason_code == null : isNonEmptyString(member.reason_code));
105
+ }
106
+
107
+ export function buildQcResult({ check, leg, subject, result, reason_code = null, state, observation = {}, members = [], accept_eligible = undefined, coverage, measured_at, producer = QC_PRODUCERS[leg] }) {
108
+ if (!isNonEmptyString(check) || !isPlainObject(subject) || subject.check !== check) throw new TypeError("buildQcResult needs a check and a subject naming it.");
109
+ if (!QC_LEGS.includes(leg)) throw new TypeError(`buildQcResult leg must be one of ${QC_LEGS.join(", ")}.`);
110
+ if (!QC_RESULTS.includes(result)) throw new TypeError(`buildQcResult result must be one of ${QC_RESULTS.join(", ")}.`);
111
+ if (result !== "pass" && !isNonEmptyString(reason_code)) throw new TypeError("buildQcResult needs a reason_code unless the result is pass.");
112
+ if (!Array.isArray(members) || !members.every(validMember)) throw new TypeError("buildQcResult members must be {key, result, reason_code} entries.");
113
+ const eligible = qcAcceptEligible(result, members) && accept_eligible !== false;
114
+ return {
115
+ schema: QC_RESULT_SCHEMA,
116
+ id: qcResultId(subject),
117
+ check,
118
+ leg,
119
+ result,
120
+ reason_code: result === "pass" ? null : reason_code,
121
+ subject,
122
+ state_fingerprint: qcStateFingerprint({ subject, state }),
123
+ observation,
124
+ members,
125
+ accept_eligible: eligible,
126
+ coverage: coverage ?? { observed: 1, expected: 1, limits: [] },
127
+ measured_at,
128
+ producer,
129
+ };
130
+ }
131
+
132
+ // Members aggregate as warning > review > unexercised > pass; excluded members
133
+ // are ignored, and a row of only excluded members is excluded. A capped page
134
+ // adds the page_coverage member, so its kept results are never accept-eligible.
135
+ export function aggregateQcResults(members, { capReason = null } = {}) {
136
+ const all = [...(Array.isArray(members) ? members : []), ...(capReason ? [{ key: "page_coverage", result: "unexercised", reason_code: capReason }] : [])];
137
+ const counted = all.filter((member) => member.result !== "excluded");
138
+ const result = counted.length ? PRECEDENCE.find((value) => counted.some((member) => member.result === value)) || "unexercised" : "excluded";
139
+ const lead = result === "pass" ? null : all.find((member) => member.result === result);
140
+ return { result, reason_code: lead ? lead.reason_code : null, members: all, accept_eligible: qcAcceptEligible(result, all) };
141
+ }
142
+
143
+ // A QA verdict assertion for a result, in an existing family, id prefixed qc.
144
+ export function toQaAssertion(row, { family = "browser-runtime", expected = null, actual = null } = {}) {
145
+ if (!QA_ASSERTION_FAMILY_VOCABULARY.includes(family)) throw new TypeError(`toQaAssertion family must be an existing QA assertion family (got ${family}).`);
146
+ const { status, severity } = QC_QA_ASSERTION_STATUS[row.result];
147
+ return {
148
+ id: `qc.${row.id}`,
149
+ family,
150
+ page: row.subject?.page ?? null,
151
+ status,
152
+ severity,
153
+ ...(expected == null ? {} : { expected }),
154
+ actual: actual ?? `${row.check} ${row.result}${row.reason_code ? ` (${row.reason_code})` : ""}`,
155
+ evidence: { qc: { result_id: row.id, observation: row.observation, ...(row.result === "excluded" ? { reason_code: row.reason_code } : {}) } },
156
+ };
157
+ }
158
+
159
+ export function toDoctorIssue(row) {
160
+ return {
161
+ code: `built_output.${row.check}.${row.reason_code}`,
162
+ message: `${row.check} ${row.result} on ${row.subject?.page ?? "(no page)"} (${row.subject?.key ?? "(no key)"}): ${row.reason_code}.`,
163
+ detail: { qc_result: row },
164
+ };
165
+ }
166
+
167
+ // The single doctor wiring point: every result lands in derived.qc_results;
168
+ // warning and review results also become warnings[] issues. Doctor status
169
+ // rules are unchanged, and unexercised/excluded stay in derived only.
170
+ export function recordQcResults({ derived, warnings, results }) {
171
+ if (!Array.isArray(derived.qc_results)) derived.qc_results = [];
172
+ for (const row of Array.isArray(results) ? results : []) {
173
+ derived.qc_results.push(row);
174
+ if (row.result === "warning" || row.result === "review") warnings.push(toDoctorIssue(row));
175
+ }
176
+ return derived.qc_results;
177
+ }
178
+
179
+ // ---------------------------------------------------------------------------
180
+ // Rederivers: an in-process stand-in (tests only) or the registry module.
181
+
182
+ function qaRederiver(check, { qcStandIns, rederivers }) {
183
+ const standIn = qcStandIns?.qa?.[check];
184
+ if (typeof standIn === "function") return { status: "loaded", rederive: standIn };
185
+ const entry = (rederivers || loadedQcRederivers())?.[check];
186
+ if (!entry) return { status: Object.hasOwn(QC_CHECK_REGISTRY, check) ? "unresolved" : "unknown" };
187
+ if (entry.status === "loaded" && typeof entry.rederive !== "function") return { status: "failed" };
188
+ return entry;
189
+ }
190
+
191
+ // The rules of the record a Polish check is read from. A stand-in is
192
+ // `qcStandIns.polish[check]`, or `qcStandIns.polish` itself for the
193
+ // media_weight checks.
194
+ export function resolvePolishRules({ check = "media.weight", qcStandIns, rederivers } = {}) {
195
+ const standIn = isPlainObject(qcStandIns?.polish?.[check]) ? qcStandIns.polish[check]
196
+ : QC_CHECK_REGISTRY[check]?.record === "media_weight" && isPlainObject(qcStandIns?.polish) ? qcStandIns.polish : null;
197
+ if (standIn) return { status: "loaded", rules: standIn };
198
+ const entry = (rederivers || loadedQcRederivers())?.[check];
199
+ if (!entry) return { status: "unresolved" };
200
+ if (entry.status !== "loaded") return entry;
201
+ const rules = entry.rederive;
202
+ return isPlainObject(rules) && typeof rules.evaluate === "function" && isPlainObject(rules.thresholds) && isPlainObject(rules.vocabulary)
203
+ ? { status: "loaded", rules }
204
+ : { status: "failed" };
205
+ }
206
+
207
+ // A result the reader could not re-derive. What the stored row claims is kept
208
+ // only to name it (id, subject, fingerprint); its result never survives.
209
+ function unreproducedRow(base, reasonCode, { leg, measuredAt = null }) {
210
+ const subject = isPlainObject(base?.subject) ? base.subject : { check: base?.check ?? null, page: base?.page ?? null, key: base?.key ?? null };
211
+ const check = isNonEmptyString(base?.check) ? base.check : subject.check ?? null;
212
+ return {
213
+ schema: QC_RESULT_SCHEMA,
214
+ id: isNonEmptyString(base?.id) ? base.id : qcResultId(subject),
215
+ check,
216
+ leg,
217
+ result: "unexercised",
218
+ reason_code: reasonCode,
219
+ subject,
220
+ state_fingerprint: FINGERPRINT.test(base?.state_fingerprint || "") ? base.state_fingerprint : qcStateFingerprint({ subject, state: { reason_code: reasonCode } }),
221
+ observation: {},
222
+ members: [],
223
+ accept_eligible: false,
224
+ coverage: { observed: 0, expected: null, limits: [reasonCode] },
225
+ measured_at: validTime(measuredAt) ? measuredAt : validTime(base?.measured_at) ? base.measured_at : null,
226
+ producer: QC_PRODUCERS[leg],
227
+ };
228
+ }
229
+
230
+ const staleRow = (row) => ({
231
+ ...row,
232
+ result: "unexercised",
233
+ reason_code: QC_REASON.STALE_BINDING,
234
+ accept_eligible: false,
235
+ coverage: { ...(isPlainObject(row.coverage) ? row.coverage : {}), limits: [QC_REASON.STALE_BINDING] },
236
+ });
237
+
238
+ // A check re-deriver's output, checked against the QC result vocabulary.
239
+ function validDerived(derived, check) {
240
+ return isPlainObject(derived)
241
+ && derived.check === check
242
+ && isPlainObject(derived.subject)
243
+ && derived.subject.check === check
244
+ && QC_RESULTS.includes(derived.result)
245
+ && (derived.result === "pass" ? derived.reason_code == null : isNonEmptyString(derived.reason_code))
246
+ && (derived.members === undefined || (Array.isArray(derived.members) && derived.members.every(validMember)))
247
+ && typeof derived.accept_eligible === "boolean"
248
+ && (derived.coverage === undefined || isPlainObject(derived.coverage))
249
+ && Object.hasOwn(derived, "state");
250
+ }
251
+
252
+ // ---------------------------------------------------------------------------
253
+ // QA reader site
254
+
255
+ // The Inputs checks on the full QA verdict. Returns null when it passes.
256
+ // Every qc.* assertion takes part in pairing, so none may be skipped: an
257
+ // entry that is not an assertion object, a qc.* assertion whose evidence.qc
258
+ // is not {result_id: non-empty string, observation: object}, or QC evidence
259
+ // on an assertion whose id is not qc.* fails the whole verdict.
260
+ export function qaFullVerdictProblem(verdict, stage) {
261
+ if (!isPlainObject(verdict)) return "full_verdict_absent";
262
+ if (verdict.schema_version !== QA_SCHEMA_VERSION) return "verdict_schema";
263
+ if (typeof verdict.runtime !== "string" || !verdict.runtime.startsWith(QA_RUNTIME_PREFIX)) return "verdict_runtime";
264
+ const runId = stage?.identity?.verdict_run_id;
265
+ if (!isNonEmptyString(runId) || verdict.run_id !== runId) return "verdict_run_id";
266
+ if (!Array.isArray(verdict.assertions)) return "verdict_assertions";
267
+ for (const assertion of verdict.assertions) {
268
+ if (!isPlainObject(assertion)) return "verdict_assertions";
269
+ const isQc = typeof assertion.id === "string" && assertion.id.startsWith("qc.");
270
+ const carriesQc = isPlainObject(assertion.evidence) && Object.hasOwn(assertion.evidence, "qc");
271
+ if (!isQc && !carriesQc) continue;
272
+ const qc = assertion.evidence?.qc;
273
+ if (!isQc || !isPlainObject(qc) || !isNonEmptyString(qc.result_id) || !isPlainObject(qc.observation)) return "qc_assertion_malformed";
274
+ }
275
+ return null;
276
+ }
277
+
278
+ // The full verdict the QA stage names: its outputs[] entry under
279
+ // {target}/qa-output/. Evidence refers to itself when that file is the
280
+ // committed QA sidecar or the Assembly Report. That is decided on the file
281
+ // itself, after every symlink and directory alias resolves (a symlinked
282
+ // qa-output/ included), and on its device and inode (a hard link), whatever
283
+ // the stage's other outputs[] say; such a verdict reads as absent. This is the
284
+ // only file a QC reader opens from a path held in a record.
285
+ export function readQaFullVerdict({ stage, targetRepo, packetPath = null, reportPath = null }) {
286
+ if (!isPlainObject(stage) || !isNonEmptyString(targetRepo)) return null;
287
+ const outputDir = resolve(targetRepo, "qa-output");
288
+ const inside = (root, path) => {
289
+ const rel = relative(root, path);
290
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
291
+ };
292
+ const outputs = (Array.isArray(stage.outputs) ? stage.outputs : []).filter(isNonEmptyString).map((path) => resolve(targetRepo, path));
293
+ const candidates = outputs.filter((path) => inside(outputDir, path));
294
+ if (candidates.length !== 1 || !existsSync(candidates[0]) || !existsSync(outputDir)) return null;
295
+ try {
296
+ const real = realpathSync(candidates[0]);
297
+ const stats = statSync(real);
298
+ if (!stats.isFile()) return null;
299
+ if (!inside(realpathSync(outputDir), real)) return null;
300
+ const forbidden = [
301
+ reportPath,
302
+ campaignSidecarPaths(targetRepo).reportPath,
303
+ isNonEmptyString(packetPath) ? sidecarPathForPacket(packetPath) : null,
304
+ ...outputs.filter((path) => path !== candidates[0]),
305
+ ].filter(isNonEmptyString);
306
+ for (const path of forbidden) {
307
+ if (sameFile(real, path)) return null;
308
+ if (!existsSync(path)) continue;
309
+ const other = statSync(path);
310
+ if (other.dev === stats.dev && other.ino === stats.ino) return null;
311
+ }
312
+ return JSON.parse(readFileSync(real, "utf8"));
313
+ } catch {
314
+ return null;
315
+ }
316
+ }
317
+
318
+ // `qaCurrency` is QA's read-time input currency
319
+ // (derived.input_currency.stages.qa). While QA is owed again or its inputs
320
+ // cannot be confirmed, every row that reproduces reads stale_binding, as a
321
+ // row bound to another build does; a row that fails reproduction still reads
322
+ // evidence_not_reproducible first.
323
+ export function readQaResults({ stageEvidence, stage = null, fullVerdict = null, currentBuild = null, qcStandIns = null, rederivers = null, qaCurrency = null } = {}) {
324
+ const stored = Array.isArray(stageEvidence?.qc_results) ? stageEvidence.qc_results : null;
325
+ if (!stored) return [];
326
+ const verdictOk = qaFullVerdictProblem(fullVerdict, stage) === null;
327
+ const measuredAt = verdictOk ? (validTime(fullVerdict.completed_at) ? fullVerdict.completed_at : fullVerdict.started_at) : null;
328
+ const inputsCurrent = qaCurrency !== "owed" && qaCurrency !== "unknown";
329
+ const bound = inputsCurrent && isNonEmptyString(currentBuild) && stageEvidence.qc_build_fingerprint === currentBuild;
330
+
331
+ // Rows pair with assertions by evidence.qc.result_id, never by assertion id.
332
+ // A passing verdict holds only well-formed QC evidence (qaFullVerdictProblem),
333
+ // so the only assertions passed over here carry none.
334
+ const paired = new Map();
335
+ const duplicated = new Set();
336
+ for (const assertion of verdictOk ? fullVerdict.assertions : []) {
337
+ if (typeof assertion.id !== "string" || !assertion.id.startsWith("qc.")) continue;
338
+ const resultId = assertion.evidence.qc.result_id;
339
+ if (paired.has(resultId)) duplicated.add(resultId);
340
+ paired.set(resultId, assertion);
341
+ }
342
+
343
+ const read = new Map();
344
+ stored.forEach((row, index) => {
345
+ const id = isPlainObject(row) && isNonEmptyString(row.id) ? row.id : `qc_results[${index}]`;
346
+ if (read.has(id)) {
347
+ read.set(id, unreproducedRow(read.get(id), E, { leg: "qa", measuredAt }));
348
+ return;
349
+ }
350
+ read.set(id, readQaRow(isPlainObject(row) ? row : { id }, { verdictOk, assertion: duplicated.has(id) ? null : paired.get(id), measuredAt, qcStandIns, rederivers, bound }));
351
+ });
352
+ // A verdict result_id with no row: synthesized, never silent.
353
+ for (const [resultId, assertion] of paired) {
354
+ if (read.has(resultId)) continue;
355
+ const observation = assertion.evidence.qc.observation;
356
+ const check = isNonEmptyString(observation?.check) ? observation.check : resultId.split(":")[0];
357
+ read.set(resultId, unreproducedRow({ id: resultId, check, subject: { check, page: assertion.page ?? null, key: null } }, E, { leg: "qa", measuredAt }));
358
+ }
359
+ return [...read.values()];
360
+ }
361
+
362
+ function readQaRow(row, { verdictOk, assertion, measuredAt, qcStandIns, rederivers, bound }) {
363
+ const fail = () => unreproducedRow(row, E, { leg: "qa", measuredAt });
364
+ if (!verdictOk || !assertion) return fail();
365
+ if (row.schema !== QC_RESULT_SCHEMA || row.producer !== QC_PRODUCERS.qa || row.leg !== "qa" || !isNonEmptyString(row.check)) return fail();
366
+ const observation = assertion.evidence.qc.observation;
367
+ if (!sameJson(row.observation, observation)) return fail();
368
+ if (!QA_ASSERTION_FAMILY_VOCABULARY.includes(assertion.family)) return fail();
369
+ const rederiver = qaRederiver(row.check, { qcStandIns, rederivers });
370
+ if (rederiver.status === "missing") {
371
+ const notCaptured = unreproducedRow(row, QC_REASON.NOT_CAPTURED, { leg: "qa", measuredAt });
372
+ return bound ? notCaptured : staleRow(notCaptured);
373
+ }
374
+ if (rederiver.status !== "loaded") return fail();
375
+ let derived = null;
376
+ try {
377
+ derived = rederiver.rederive(observation);
378
+ } catch {
379
+ derived = null;
380
+ }
381
+ if (!validDerived(derived, row.check)) return fail();
382
+ const members = derived.members ?? [];
383
+ const coverage = derived.coverage ?? { observed: 1, expected: 1, limits: [] };
384
+ const stateFingerprint = qcStateFingerprint({ subject: derived.subject, state: derived.state });
385
+ const reasonCode = derived.result === "pass" ? null : derived.reason_code;
386
+ const expected = QC_QA_ASSERTION_STATUS[derived.result];
387
+ if (row.id !== qcResultId(derived.subject)
388
+ || !sameJson(row.subject, derived.subject)
389
+ || row.result !== derived.result
390
+ || (row.reason_code ?? null) !== reasonCode
391
+ || !sameJson(row.members ?? [], members)
392
+ || row.accept_eligible !== derived.accept_eligible
393
+ || !sameJson(row.coverage, coverage)
394
+ || row.state_fingerprint !== stateFingerprint
395
+ || assertion.status !== expected.status
396
+ || assertion.severity !== expected.severity) return fail();
397
+ const result = {
398
+ schema: QC_RESULT_SCHEMA,
399
+ id: row.id,
400
+ check: row.check,
401
+ leg: "qa",
402
+ result: derived.result,
403
+ reason_code: reasonCode,
404
+ subject: derived.subject,
405
+ state_fingerprint: stateFingerprint,
406
+ observation,
407
+ members,
408
+ accept_eligible: derived.accept_eligible && qcAcceptEligible(derived.result, members),
409
+ coverage,
410
+ measured_at: measuredAt,
411
+ producer: QC_PRODUCERS.qa,
412
+ };
413
+ return bound ? result : staleRow(result);
414
+ }
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Polish reader sites (media_weight against its page_load capture, and
418
+ // readability)
419
+
420
+ // Unkeyed tamper evidence of a package-owned Polish record over every field
421
+ // but `integrity`, the canonical-JSON + sha256 pattern of
422
+ // buildPolishCaptureIntegrity.
423
+ export function polishRecordIntegrity(record) {
424
+ const { integrity: _ignored, ...rest } = record || {};
425
+ return `sha256:${createHash("sha256").update(canonicalJson(rest)).digest("hex")}`;
426
+ }
427
+ export const mediaWeightIntegrity = polishRecordIntegrity;
428
+
429
+ const inVocabulary = (vocabulary, field, value) => Array.isArray(vocabulary?.[field]) && vocabulary[field].includes(value);
430
+ const isPair = (value) => Array.isArray(value) && value.length === 2 && value.every((n) => typeof n === "number" && Number.isFinite(n) && n >= 0);
431
+
432
+ // The subjects a cell lists whatever happens to it: one weight result per
433
+ // resource, one per unfetched <video>, one per probed <img> whose currentSrc
434
+ // binds to no resource (mediaChainBinder: "img:<element_path>"), one oversize result per
435
+ // <img>, and one oversize result keyed "cell" for a cell that lists no <img>
436
+ // (so an image-free page is never silent).
437
+ function cellSubjects(cell) {
438
+ const route = cell?.route ?? null;
439
+ const viewport = cell?.viewport ?? null;
440
+ const subjects = [];
441
+ const resources = Array.isArray(cell?.resources) ? cell.resources : [];
442
+ const images = Array.isArray(cell?.images) ? cell.images : [];
443
+ for (const resource of resources) {
444
+ subjects.push({ check: "media.weight", page: route, viewport, key: resource?.resource_id ?? null });
445
+ }
446
+ for (const video of Array.isArray(cell?.videos) ? cell.videos : []) {
447
+ if (Array.isArray(video?.resource_ids) && video.resource_ids.length) continue;
448
+ subjects.push({ check: "media.weight", page: route, viewport, key: `video:${video?.element_index}` });
449
+ }
450
+ const bind = mediaChainBinder(resources);
451
+ const unledgered = new Set();
452
+ for (const image of images) {
453
+ if (bind(image?.resource_id).status !== "absent") continue;
454
+ const key = `img:${image?.element_path}`;
455
+ if (unledgered.has(key)) continue;
456
+ unledgered.add(key);
457
+ subjects.push({ check: "media.weight", page: route, viewport, key });
458
+ }
459
+ for (const image of images) {
460
+ subjects.push({ check: "media.oversize", page: route, viewport, key: `${image?.resource_id}:${image?.element_path}` });
461
+ }
462
+ if (!images.length) subjects.push({ check: "media.oversize", page: route, viewport, key: "cell" });
463
+ return subjects;
464
+ }
465
+
466
+ // A value the image probe observed, or null where the cell's probe did not
467
+ // complete and so observed nothing (dpr and image geometry have no raw
468
+ // counterpart; only their vocabulary is checked).
469
+ const observedOrNull = (cell, value, check) => check(value) || (cell.probe_status !== "complete" && value === null);
470
+
471
+ function recordVocabularyOk(record, vocabulary) {
472
+ return record.cells.every((cell) => isPlainObject(cell)
473
+ && isNonEmptyString(cell.route)
474
+ && isNonEmptyString(cell.viewport)
475
+ && Array.isArray(record.subject?.routes) && record.subject.routes.includes(cell.route)
476
+ && Array.isArray(record.subject?.viewports) && record.subject.viewports.includes(cell.viewport)
477
+ && observedOrNull(cell, cell.dpr, (dpr) => typeof dpr === "number" && dpr > 0)
478
+ && isNonEmptyString(cell.page_load_integrity)
479
+ && inVocabulary(vocabulary, "capture_status", cell.capture_status)
480
+ && inVocabulary(vocabulary, "probe_status", cell.probe_status)
481
+ && Array.isArray(cell.resources) && cell.resources.every((resource) => isPlainObject(resource)
482
+ && isNonEmptyString(resource.resource_id)
483
+ && inVocabulary(vocabulary, "measurement", resource.measurement)
484
+ && typeof resource.failed === "boolean"
485
+ && typeof resource.final_origin_equal === "boolean"
486
+ && Number.isFinite(resource.transferred_bytes)
487
+ && (resource.declared_bytes === null || Number.isFinite(resource.declared_bytes))
488
+ && Array.isArray(resource.chain) && resource.chain.length > 0 && resource.chain.every(isPlainObject))
489
+ && Array.isArray(cell.images) && cell.images.every((image) => isPlainObject(image)
490
+ && (image.resource_id === null || isNonEmptyString(image.resource_id))
491
+ && isNonEmptyString(image.element_path)
492
+ && observedOrNull(cell, image.complete, (value) => typeof value === "boolean")
493
+ && observedOrNull(cell, image.hidden, (value) => typeof value === "boolean")
494
+ && observedOrNull(cell, image.natural, isPair)
495
+ && observedOrNull(cell, image.rendered, isPair)
496
+ && observedOrNull(cell, image.object_fit, (value) => inVocabulary(vocabulary, "object_fit", value))
497
+ && inVocabulary(vocabulary, "loading", image.loading))
498
+ && Array.isArray(cell.videos) && cell.videos.every((video) => isPlainObject(video)
499
+ && Number.isInteger(video.element_index)
500
+ && Array.isArray(video.resource_ids) && video.resource_ids.every(isNonEmptyString)
501
+ && typeof video.declared_origin_equal === "boolean"));
502
+ }
503
+
504
+ // The page_load fields the cell checks read, each required present with the
505
+ // producer's type. A missing or ill-typed field never stands in for 0, false,
506
+ // complete or an empty list: the cell does not re-derive.
507
+ const isCount = (value) => Number.isSafeInteger(value) && value >= 0;
508
+ const LEDGER_COUNTS = Object.freeze([
509
+ "transferred_bytes",
510
+ "declared_bytes",
511
+ "request_count",
512
+ "declared_request_count",
513
+ "failed_request_count",
514
+ "cache_request_count",
515
+ "unmeasured_request_count",
516
+ "canceled_request_count",
517
+ "partial_request_count",
518
+ "cross_origin_request_count",
519
+ ]);
520
+ const MEDIA_TAGS = Object.freeze(["video", "audio"]);
521
+ const SOURCE_KINDS = Object.freeze(["current_src", "src_attribute", "source_src_attribute", "observed_source"]);
522
+
523
+ // A resource_ledger entry: its identity, URL and type, every byte count and
524
+ // request counter (mediaFetchedResources copies request_count and
525
+ // declared_bytes too), its statuses, and the identity set it matched (which
526
+ // always holds its own resource_id).
527
+ function ledgerEntryOk(entry) {
528
+ return isPlainObject(entry)
529
+ && isNonEmptyString(entry.resource_id)
530
+ && isNonEmptyString(entry.url)
531
+ && isNonEmptyString(entry.resource_type)
532
+ && LEDGER_COUNTS.every((field) => isCount(entry[field]))
533
+ && Array.isArray(entry.statuses) && entry.statuses.every(Number.isInteger)
534
+ && Array.isArray(entry.match_resource_ids) && entry.match_resource_ids.every(isNonEmptyString)
535
+ && entry.match_resource_ids.includes(entry.resource_id);
536
+ }
537
+
538
+ // A media[] element: its tag, its index, every source reference's kind, index,
539
+ // URL and resolved resource_id (null when the ledger cannot identify the
540
+ // source), and every fetched resource's identity and the source identities it
541
+ // matched. The capture checksum covers the source and fetched-resource
542
+ // associations, so each of those fields is read too.
543
+ function mediaElementOk(element) {
544
+ return isPlainObject(element)
545
+ && MEDIA_TAGS.includes(element.tag_name)
546
+ && isCount(element.element_index)
547
+ && Array.isArray(element.source_references)
548
+ && element.source_references.every((reference) => isPlainObject(reference)
549
+ && SOURCE_KINDS.includes(reference.source_kind)
550
+ && isCount(reference.source_index)
551
+ && isNonEmptyString(reference.url)
552
+ && Object.hasOwn(reference, "resource_id")
553
+ && (reference.resource_id === null || isNonEmptyString(reference.resource_id)))
554
+ && Array.isArray(element.fetched_resources)
555
+ && element.fetched_resources.every((resource) => isPlainObject(resource)
556
+ && isNonEmptyString(resource.resource_id)
557
+ && Array.isArray(resource.matched_source_resource_ids)
558
+ && resource.matched_source_resource_ids.every(isNonEmptyString));
559
+ }
560
+
561
+ // A cell resource's own URL and type, and each chain hop's identity, URL and
562
+ // status, which the cell checks compare with the ledger. A null status is
563
+ // left to the chain check, which allows it only on a final hop that failed
564
+ // with no HTTP response.
565
+ function cellResourceShapeOk(resource) {
566
+ return isNonEmptyString(resource.url)
567
+ && isNonEmptyString(resource.type)
568
+ && resource.chain.every((hop) => isNonEmptyString(hop.resource_id) && isNonEmptyString(hop.url) && (Number.isInteger(hop.status) || hop.status === null));
569
+ }
570
+
571
+ function ledgerMeasurement(entry) {
572
+ if (entry.cache_request_count > 0) return "cached";
573
+ if (entry.unmeasured_request_count > 0) return "unmeasured";
574
+ if (entry.canceled_request_count > 0 || entry.partial_request_count > 0) return "lower_bound";
575
+ return "complete";
576
+ }
577
+
578
+ // A redirect answer: the hop continues to another URL. Every other status
579
+ // (a 2xx, a 304, an error) ends the chain.
580
+ const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
581
+ const isRedirectEntry = (entry) => Array.isArray(entry.statuses) && entry.statuses.length > 0 && entry.statuses.every((status) => REDIRECT_STATUSES.has(status));
582
+ const mixesRedirect = (entry) => Array.isArray(entry.statuses) && entry.statuses.some((status) => REDIRECT_STATUSES.has(status)) && !isRedirectEntry(entry);
583
+
584
+ // The one binding of a requested href (the identity an element used) to a
585
+ // request chain, read from the page_load ledger alone, so it is the same
586
+ // whatever order the requests came in. The producer builds every resource's
587
+ // chain with it and the reader checks every resource's chain against it.
588
+ // - "bound": the ledger singles out one chain. Its final hop is the one entry
589
+ // that names the requested href in its match_resource_ids and answered
590
+ // with a non-redirect status (the requested entry itself when it was not
591
+ // redirected); its hops are that final entry plus every redirect entry it
592
+ // names. The ledger keeps no Location, so it fixes the hop set and both
593
+ // ends; it cannot order intermediate hops.
594
+ // - "ambiguous": the href stands for more than one chain, or a hop of its
595
+ // chain also belongs to another chain. No result may be read from any one
596
+ // of those chains. That is the case when:
597
+ // - any entry of the chain, or any entry its final hop names, mixes a
598
+ // redirect and a non-redirect status (one request through that URL was
599
+ // answered, another redirected);
600
+ // - it only redirected and no single final hop names it;
601
+ // - it was answered directly and a redirect entry names it too (it is one
602
+ // chain's final hop and another chain's start);
603
+ // - a hop answered more requests than the requested href did (that hop
604
+ // also started a chain of its own).
605
+ // - "absent": no ledger entry has that id.
606
+ export function bindLedgerChain(requestedId, entries, byId = new Map(entries.map((entry) => [entry?.resource_id, entry]))) {
607
+ const requested = byId.get(requestedId);
608
+ if (!requested) return { status: "absent" };
609
+ if (mixesRedirect(requested)) return { status: "ambiguous" };
610
+ let final = requested;
611
+ if (isRedirectEntry(requested)) {
612
+ const finals = entries.filter((entry) => !isRedirectEntry(entry) && Array.isArray(entry.match_resource_ids) && entry.match_resource_ids.includes(requestedId));
613
+ if (finals.length !== 1) return { status: "ambiguous" };
614
+ [final] = finals;
615
+ }
616
+ if (!Array.isArray(final.match_resource_ids)) return { status: "ambiguous" };
617
+ const hops = new Set([final.resource_id, requestedId]);
618
+ for (const id of final.match_resource_ids) {
619
+ const entry = byId.get(id);
620
+ if (entry && mixesRedirect(entry)) return { status: "ambiguous" };
621
+ if (entry && entry !== final && isRedirectEntry(entry)) hops.add(id);
622
+ }
623
+ if (final === requested && hops.size > 1) return { status: "ambiguous" };
624
+ if ([...hops].some((id) => byId.get(id)?.request_count !== requested.request_count)) return { status: "ambiguous" };
625
+ return { status: "bound", final, hops };
626
+ }
627
+
628
+ // The one binding of a cell's elements to its resources, read from the
629
+ // record: an element identity (an <img>'s currentSrc id, a <video>'s fetched
630
+ // ledger ids) binds to the resource whose chain holds it. The 1.3 rules
631
+ // (src/polish-media-weight.mjs) and the reader's subject list both bind
632
+ // through it. Returns { status: "bound", resource } when exactly one
633
+ // resource's chain holds the id, { status: "ambiguous" } when more than one
634
+ // does, and { status: "absent" } when none does.
635
+ export function mediaChainBinder(resources) {
636
+ const holders = new Map();
637
+ for (const resource of Array.isArray(resources) ? resources : []) {
638
+ for (const hop of Array.isArray(resource?.chain) ? resource.chain : []) {
639
+ const id = hop?.resource_id;
640
+ if (!holders.has(id)) holders.set(id, new Set());
641
+ holders.get(id).add(resource);
642
+ }
643
+ }
644
+ return (id) => {
645
+ const found = id === null || id === undefined ? undefined : holders.get(id);
646
+ if (!found) return { status: "absent" };
647
+ return found.size === 1 ? { status: "bound", resource: [...found][0] } : { status: "ambiguous" };
648
+ };
649
+ }
650
+
651
+ // The ledger types an <img> request can be recorded under: its own (image),
652
+ // a type merged with another load of the same URL (unknown), or a hint that
653
+ // fetched it first (other, prefetch). An <img> bound to a document, script,
654
+ // stylesheet or any other entry did not load from it.
655
+ const IMAGE_REQUEST_TYPES = new Set(["image", "other", "prefetch", "unknown"]);
656
+
657
+ // Every field of a cell that has a raw counterpart, checked against the
658
+ // page_load capture for the same route and viewport. True when it re-derives.
659
+ // Every set the reader consumes is derived from the capture and required to
660
+ // match exactly: each resource's hop set and final hop (bindLedgerChain; a
661
+ // requested href the ledger binds to more than one chain, or whose chain
662
+ // shares a hop with another chain, does not re-derive, whatever the request
663
+ // order), the cell's resource set (the chains partition
664
+ // the capture's ledger: every entry in exactly one resource's chain), each
665
+ // <img> identity (null, or one resource's requested href), and the cell's
666
+ // video set (every <video> media element). A truncated chain, a dropped resource or a dropped video
667
+ // does not re-derive. Every raw field read is first required present and well
668
+ // typed (ledgerEntryOk, mediaElementOk): a ledger, media list, counter, byte
669
+ // count, status list, origin or identity set that is missing or ill typed
670
+ // does not re-derive either.
671
+ function cellReproduces(cell, capture) {
672
+ if (!isPlainObject(capture) || !isPlainObject(capture.integrity)) return false;
673
+ if (!sameJson(buildPolishCaptureIntegrity(capture), capture.integrity)) return false;
674
+ if (cell.page_load_integrity !== capture.integrity.projection_fingerprint) return false;
675
+ if (!isNonEmptyString(capture.measurement_status) || cell.capture_status !== capture.measurement_status) return false;
676
+ const finalOrigin = isPlainObject(capture.document_response) ? capture.document_response.final_origin : null;
677
+ if (!isNonEmptyString(finalOrigin) || captureOrigin(finalOrigin) !== finalOrigin || cell.document_origin !== finalOrigin) return false;
678
+ if (!isPlainObject(capture.resource_ledger) || !Array.isArray(capture.resource_ledger.entries) || !capture.resource_ledger.entries.every(ledgerEntryOk)) return false;
679
+ if (!Array.isArray(capture.media) || !capture.media.every(mediaElementOk)) return false;
680
+ const entries = capture.resource_ledger.entries;
681
+ const byId = new Map(entries.map((entry) => [entry.resource_id, entry]));
682
+ if (byId.size !== entries.length) return false;
683
+ const media = capture.media;
684
+ if (new Set(media.map((element) => element.element_index)).size !== media.length) return false;
685
+ const covered = new Set();
686
+ for (const resource of cell.resources) {
687
+ if (!cellResourceShapeOk(resource)) return false;
688
+ const { chain } = resource;
689
+ if (chain[0].resource_id !== resource.resource_id || chain[0].url !== resource.url) return false;
690
+ const expected = bindLedgerChain(resource.resource_id, entries, byId);
691
+ if (expected.status !== "bound") return false;
692
+ const { final } = expected;
693
+ const chainIds = chain.map((hop) => hop.resource_id);
694
+ if (chain.at(-1).resource_id !== final.resource_id
695
+ || new Set(chainIds).size !== chainIds.length
696
+ || chainIds.length !== expected.hops.size
697
+ || !chainIds.every((id) => expected.hops.has(id))) return false;
698
+ for (const [index, hop] of chain.entries()) {
699
+ const entry = byId.get(hop.resource_id);
700
+ if (!entry || hop.url !== entry.url || !Array.isArray(entry.statuses)) return false;
701
+ // A final hop whose request failed with no HTTP response carries no status.
702
+ const noResponse = index === chain.length - 1 && entry.statuses.length === 0 && entry.failed_request_count > 0 && hop.status === null;
703
+ if (!noResponse && !entry.statuses.includes(hop.status)) return false;
704
+ if (!final.match_resource_ids.includes(hop.resource_id)) return false;
705
+ // Every hop but the last answered with a redirect; the last did not.
706
+ if (REDIRECT_STATUSES.has(hop.status) !== (index < chain.length - 1)) return false;
707
+ if (covered.has(hop.resource_id)) return false;
708
+ covered.add(hop.resource_id);
709
+ }
710
+ if (resource.type !== final.resource_type
711
+ || resource.transferred_bytes !== final.transferred_bytes
712
+ || resource.declared_bytes !== (final.declared_request_count > 0 ? final.declared_bytes : null)
713
+ || resource.failed !== (final.failed_request_count > 0)
714
+ || resource.measurement !== ledgerMeasurement(final)
715
+ || resource.final_origin_equal !== (final.cross_origin_request_count === 0)) return false;
716
+ }
717
+ if (covered.size !== byId.size) return false;
718
+ // Every <img> identity is null (its currentSrc has no ledger entry: weight
719
+ // not_in_ledger) or exactly the requested href of one resource, whose chain
720
+ // the ledger binds (above) and whose entry an <img> request can produce. A
721
+ // re-cased or unknown id, a redirect or final hop, or a document or script
722
+ // entry does not re-derive.
723
+ const heads = new Set(cell.resources.map((resource) => resource.resource_id));
724
+ for (const image of cell.images) {
725
+ if (image.resource_id === null) continue;
726
+ if (!heads.has(image.resource_id) || !IMAGE_REQUEST_TYPES.has(byId.get(image.resource_id).resource_type)) return false;
727
+ }
728
+ const videoIndexes = media.filter((element) => element.tag_name === "video").map((element) => element.element_index).sort((a, b) => a - b);
729
+ const listedIndexes = cell.videos.map((video) => video.element_index).sort((a, b) => a - b);
730
+ if (!sameJson(listedIndexes, videoIndexes)) return false;
731
+ for (const video of cell.videos) {
732
+ const element = media.find((candidate) => candidate.element_index === video.element_index);
733
+ if (!element) return false;
734
+ const fetched = mediaFetchedResources(element, entries).map((entry) => entry.resource_id).sort();
735
+ if (!sameJson([...video.resource_ids].sort(), fetched)) return false;
736
+ const declaredSameOrigin = element.source_references.some((reference) => captureOrigin(reference.url) === finalOrigin);
737
+ if (video.declared_origin_equal !== declaredSameOrigin) return false;
738
+ }
739
+ return true;
740
+ }
741
+
742
+ const cellKey = (route, viewport) => JSON.stringify([route, viewport]);
743
+ const uniqueStrings = (list) => Array.isArray(list) && list.length > 0 && list.every(isNonEmptyString) && new Set(list).size === list.length;
744
+ // A subject without its build binding, and whether its other fields are
745
+ // exactly `fields`.
746
+ const unbound = (subject) => {
747
+ if (!isPlainObject(subject)) return subject;
748
+ const { build_fingerprint: _build, ...rest } = subject;
749
+ return rest;
750
+ };
751
+ const hasExactly = (object, fields) => isPlainObject(object) && sameJson(Object.keys(unbound(object)).sort(), [...fields].sort());
752
+
753
+ // The routes of spec pages the run did not capture (no source mapping),
754
+ // listed in the record as uncaptured_routes (absent reads as none). The
755
+ // page_load capture records that pages were skipped only as route_scope
756
+ // "selected", so a list is accepted only then, and only of routes it did not
757
+ // capture. Null when the list is malformed.
758
+ function uncapturedRoutesOf(record) {
759
+ if (!Object.hasOwn(record, "uncaptured_routes")) return [];
760
+ const routes = record.uncaptured_routes;
761
+ if (!Array.isArray(routes)) return null;
762
+ if (!routes.length) return routes;
763
+ return uniqueStrings(routes)
764
+ && record.subject.route_scope === "selected"
765
+ && routes.every((route) => route.startsWith("/") && !record.subject.routes.includes(route))
766
+ ? routes
767
+ : null;
768
+ }
769
+
770
+ // The page ids of skipped spec pages whose public route the run could not
771
+ // resolve, listed as uncaptured_page_ids (absent reads as none); accepted, like
772
+ // uncaptured_routes, only under route_scope "selected". Null when malformed.
773
+ function uncapturedPageIdsOf(record) {
774
+ if (!Object.hasOwn(record, "uncaptured_page_ids")) return [];
775
+ const pageIds = record.uncaptured_page_ids;
776
+ if (!Array.isArray(pageIds)) return null;
777
+ if (!pageIds.length) return pageIds;
778
+ return uniqueStrings(pageIds) && record.subject.route_scope === "selected" ? pageIds : null;
779
+ }
780
+
781
+ // The record's declared subject: exactly the page_load subject fields, each
782
+ // well formed. Its routes × viewports is the declared cell grid.
783
+ function declaredSubjectOk(subject) {
784
+ return hasExactly(subject, PAGE_LOAD_SUBJECT_FIELDS)
785
+ && isNonEmptyString(subject.campaign_slug)
786
+ && ["all", "selected"].includes(subject.route_scope)
787
+ && uniqueStrings(subject.routes)
788
+ && uniqueStrings(subject.viewports);
789
+ }
790
+
791
+ const declaredGrid = (subject) => (Array.isArray(subject?.routes) ? subject.routes : [])
792
+ .flatMap((route) => (Array.isArray(subject?.viewports) ? subject.viewports : []).map((viewport) => ({ route, viewport })));
793
+
794
+ // The page_load measurement summary agrees with the declared grid and the
795
+ // captures: it expects every declared cell, counts every capture, and lists
796
+ // nothing missing, duplicated or unexpected; any cell it calls incomplete is
797
+ // a declared one.
798
+ function measurementSummaryOk(measurement, grid, captureCount) {
799
+ const gridKeys = new Set(grid.map(({ route, viewport }) => cellKey(route, viewport)));
800
+ return isPlainObject(measurement)
801
+ && measurement.expected_capture_count === grid.length
802
+ && measurement.captured_count === captureCount
803
+ && ["missing", "duplicate", "unexpected"].every((field) => Array.isArray(measurement[field]) && measurement[field].length === 0)
804
+ && Array.isArray(measurement.incomplete)
805
+ && measurement.incomplete.every((entry) => isPlainObject(entry) && gridKeys.has(cellKey(entry.route, entry.viewport)));
806
+ }
807
+
808
+ // A route capture's own identity and binding, against the record's declared
809
+ // subject: producer and schema, exactly the capture subject fields, the same
810
+ // campaign, a document that stayed on the requested route, and a declared
811
+ // route and viewport. Returns "fail" when any of that does not hold, else
812
+ // "stale" when the capture's build is absent, null or not the current build,
813
+ // else null.
814
+ function captureBindingProblem(capture, subject, currentBuild) {
815
+ const own = capture?.subject;
816
+ if (capture?.schema_version !== ROUTE_CAPTURE_SCHEMA
817
+ || capture?.performed_by !== QC_PRODUCERS.polish
818
+ || !hasExactly(own, CAPTURE_SUBJECT_FIELDS)
819
+ || own.campaign_slug !== subject.campaign_slug
820
+ || !isNonEmptyString(own.requested_route)
821
+ || own.final_document_route !== own.requested_route
822
+ || !subject.routes.includes(own.requested_route)
823
+ || !subject.viewports.includes(own.viewport)) return "fail";
824
+ return isNonEmptyString(currentBuild) && own.build_fingerprint === currentBuild ? null : "stale";
825
+ }
826
+
827
+ const PAGE_NOT_CAPTURED = "page_not_captured";
828
+
829
+ // Built output that no longer matches the recorded build: doctor's
830
+ // derived.build_output_fingerprint, whose status is pass only when the
831
+ // output under _site/<slug>/ is the one `record build` recorded. Any other
832
+ // value, null included, is drift. A caller that passes no value (undefined)
833
+ // reads without the output check.
834
+ const outputDrifted = (buildOutput) => buildOutput !== undefined && buildOutput?.status !== "pass";
835
+
836
+ export function readMediaWeight({ record, pageLoad, currentBuild = null, buildOutput = undefined, qcStandIns = null, rederivers = null } = {}) {
837
+ if (record === undefined || record === null) return [];
838
+ const checks = POLISH_RECORDS.media_weight.checks;
839
+ const measuredAt = validTime(record?.measured_at) ? record.measured_at : null;
840
+ const cells = Array.isArray(record?.cells) ? record.cells.filter(isPlainObject) : [];
841
+ const unreproduced = (subject) => unreproducedRow({ subject, check: subject.check }, E, { leg: "polish", measuredAt });
842
+ const cellResult = (check, route, viewport) => unreproduced({ check, page: route, viewport, key: "cell" });
843
+ // A failed cell: one result per subject it lists, and one per media_weight
844
+ // check it lists no subject for, so a failed cell, even one listing
845
+ // nothing, is never silent.
846
+ const failCell = (cell) => {
847
+ const subjects = cellSubjects(cell);
848
+ return [
849
+ ...subjects.map(unreproduced),
850
+ ...checks.filter((check) => !subjects.some((subject) => subject.check === check)).map((check) => cellResult(check, cell?.route ?? null, cell?.viewport ?? null)),
851
+ ];
852
+ };
853
+ // A record-level failure: every listed cell fails, and every route ×
854
+ // viewport that the record or page_load declares or captured but no cell
855
+ // lists gets one result per media_weight check, so a missing route is never
856
+ // silent.
857
+ const failAll = () => {
858
+ const listed = new Set(cells.map((cell) => cellKey(cell.route, cell.viewport)));
859
+ const unlisted = new Map();
860
+ const note = (route, viewport) => {
861
+ const key = cellKey(route, viewport);
862
+ if (isNonEmptyString(route) && isNonEmptyString(viewport) && !listed.has(key)) unlisted.set(key, { route, viewport });
863
+ };
864
+ for (const { route, viewport } of [...declaredGrid(record?.subject), ...declaredGrid(pageLoad?.subject)]) note(route, viewport);
865
+ for (const capture of Array.isArray(pageLoad?.captures) ? pageLoad.captures : []) note(capture?.subject?.requested_route, capture?.subject?.viewport);
866
+ for (const entry of Array.isArray(pageLoad?.measurement?.missing) ? pageLoad.measurement.missing : []) note(entry?.route, entry?.viewport);
867
+ const rows = [
868
+ ...cells.flatMap(failCell),
869
+ ...[...unlisted.values()].flatMap(({ route, viewport }) => checks.map((check) => cellResult(check, route, viewport))),
870
+ ];
871
+ return rows.length ? rows : [unreproduced({ check: "media.weight", page: null, key: "media_weight" })];
872
+ };
873
+ const resolved = resolvePolishRules({ check: checks[0], qcStandIns, rederivers });
874
+ if (resolved.status === "missing") return [];
875
+ if (resolved.status !== "loaded") return failAll();
876
+ const { rules } = resolved;
877
+
878
+ // Record level: producer, integrity, thresholds, the declared subject and
879
+ // its binding to page_load, the page_load measurement summary, and the
880
+ // record vocabulary. Any failure makes every result unreproducible.
881
+ if (!isPlainObject(record)
882
+ || record.schema_version !== MEDIA_WEIGHT_SCHEMA
883
+ || record.performed_by !== QC_PRODUCERS.polish
884
+ || !measuredAt
885
+ || record.integrity !== mediaWeightIntegrity(record)
886
+ || !sameJson(record.thresholds, rules.thresholds)
887
+ || !declaredSubjectOk(record.subject)
888
+ || !isPlainObject(pageLoad)
889
+ || pageLoad.schema_version !== PAGE_LOAD_SCHEMA
890
+ || pageLoad.performed_by !== QC_PRODUCERS.polish
891
+ || !sameJson(unbound(record.subject), unbound(pageLoad.subject))
892
+ || !Array.isArray(pageLoad.captures)
893
+ || !measurementSummaryOk(pageLoad.measurement, declaredGrid(record.subject), pageLoad.captures.length)
894
+ || !Array.isArray(record.cells)
895
+ || cells.length !== record.cells.length
896
+ || !recordVocabularyOk(record, rules.vocabulary)
897
+ || uncapturedRoutesOf(record) === null
898
+ || uncapturedPageIdsOf(record) === null) return failAll();
899
+
900
+ // The declared grid (routes × viewports) is the capture set and the cell
901
+ // set: exactly one page_load capture and one cell per declared route and
902
+ // viewport, none missing, none added, none twice.
903
+ const gridKeys = declaredGrid(record.subject).map(({ route, viewport }) => cellKey(route, viewport));
904
+ const captureByKey = new Map();
905
+ for (const capture of pageLoad.captures) {
906
+ const key = cellKey(capture?.subject?.requested_route, capture?.subject?.viewport);
907
+ if (!isPlainObject(capture) || captureByKey.has(key)) return failAll();
908
+ captureByKey.set(key, capture);
909
+ }
910
+ const cellKeys = cells.map((cell) => cellKey(cell.route, cell.viewport));
911
+ if (new Set(cellKeys).size !== cellKeys.length
912
+ || cellKeys.length !== gridKeys.length
913
+ || captureByKey.size !== gridKeys.length
914
+ || !gridKeys.every((key) => captureByKey.has(key) && cellKeys.includes(key))) return failAll();
915
+
916
+ // Both enclosing subjects are bound to the current build, and the built
917
+ // output is still that build's; an absent, null or other build on either,
918
+ // or output drift, reads stale_binding.
919
+ const recordBound = isNonEmptyString(currentBuild)
920
+ && record.subject.build_fingerprint === currentBuild
921
+ && pageLoad.subject.build_fingerprint === currentBuild
922
+ && !outputDrifted(buildOutput);
923
+ const results = [];
924
+ // A spec page with no source mapping: one result per 1.3 check and viewport,
925
+ // unexercised / page_not_captured; keyed "cell" on its public route, or
926
+ // "page:<page_id>" with no page when it has no resolvable route.
927
+ const uncaptured = [
928
+ ...uncapturedRoutesOf(record).map((route) => ({ page: route, key: "cell" })),
929
+ ...uncapturedPageIdsOf(record).map((pageId) => ({ page: null, key: `page:${pageId}` })),
930
+ ];
931
+ for (const { page, key } of uncaptured) {
932
+ for (const viewport of record.subject.viewports) {
933
+ for (const check of checks) {
934
+ const subject = { check, page, viewport, key };
935
+ const row = unreproducedRow({ subject, check }, PAGE_NOT_CAPTURED, { leg: "polish", measuredAt });
936
+ results.push(recordBound ? row : staleRow(row));
937
+ }
938
+ }
939
+ }
940
+ for (const cell of cells) {
941
+ const capture = captureByKey.get(cellKey(cell.route, cell.viewport));
942
+ const binding = captureBindingProblem(capture, record.subject, currentBuild);
943
+ if (binding === "fail" || !cellReproduces(cell, capture)) {
944
+ results.push(...failCell(cell));
945
+ continue;
946
+ }
947
+ const bound = recordBound && binding === null;
948
+ let derived;
949
+ try {
950
+ derived = rules.evaluate(cell, record.thresholds);
951
+ } catch {
952
+ derived = null;
953
+ }
954
+ // Every derived subject is one the cell lists (cellSubjects), exactly:
955
+ // its own page, viewport and key, and no other field.
956
+ const own = new Set(cellSubjects(cell).map((subject) => canonicalJson(subject)));
957
+ if (!Array.isArray(derived) || !derived.every((item) => checks.includes(item?.check) && validDerived(item, item.check) && own.has(canonicalJson(item.subject)))) {
958
+ results.push(...failCell(cell));
959
+ continue;
960
+ }
961
+ for (const item of derived) {
962
+ const members = item.members ?? [];
963
+ const row = {
964
+ schema: QC_RESULT_SCHEMA,
965
+ id: qcResultId(item.subject),
966
+ check: item.check,
967
+ leg: "polish",
968
+ result: item.result,
969
+ reason_code: item.result === "pass" ? null : item.reason_code,
970
+ subject: item.subject,
971
+ state_fingerprint: qcStateFingerprint({ subject: item.subject, state: item.state }),
972
+ observation: isPlainObject(item.observation) ? item.observation : {},
973
+ members,
974
+ accept_eligible: item.accept_eligible && qcAcceptEligible(item.result, members),
975
+ coverage: item.coverage ?? { observed: 1, expected: 1, limits: [] },
976
+ measured_at: measuredAt,
977
+ producer: QC_PRODUCERS.polish,
978
+ };
979
+ results.push(bound ? row : staleRow(row));
980
+ }
981
+ }
982
+ return results;
983
+ }
984
+
985
+ // The readability record's subject fields besides build_fingerprint.
986
+ const READABILITY_SUBJECT_FIELDS = Object.freeze(["source_package_material_fingerprint", "campaign_slug", "route_source", "routes", "viewports"]);
987
+ const READABILITY_DERIVED_FIELDS = Object.freeze(["fg_srgb", "bg_srgb", "gamut_clipped", "ratio", "size_class", "required"]);
988
+ const contrast = contrastToolkit();
989
+ const canonicalTime = (value) => validTime(value) && new Date(value).toISOString() === value;
990
+
991
+ // Every derived field of a stored element re-derives exactly from its raw
992
+ // computed colours and font through the shared contrast helper.
993
+ function readabilityElementReproduces(element) {
994
+ let derived;
995
+ try {
996
+ derived = contrast.deriveElementMeasurement({
997
+ fg_raw: element.fg_raw,
998
+ fill_raw: element.fill_raw,
999
+ bg_layers_raw: element.bg_layers_raw,
1000
+ font_size_px: element.font_size_px,
1001
+ font_weight: element.font_weight,
1002
+ });
1003
+ } catch {
1004
+ return false;
1005
+ }
1006
+ return READABILITY_DERIVED_FIELDS.every((field) => Object.hasOwn(element, field) && sameJson(element[field], derived[field]));
1007
+ }
1008
+
1009
+ // A crop reference resolves when its file under the target repo holds bytes
1010
+ // whose sha256 is the one recorded.
1011
+ function readabilityCropResolves(ref, targetRepo) {
1012
+ if (!isNonEmptyString(targetRepo) || !isPlainObject(ref) || !isNonEmptyString(ref.path) || !isNonEmptyString(ref.sha256)) return false;
1013
+ try {
1014
+ const file = resolve(targetRepo, ref.path);
1015
+ if (!statSync(file).isFile()) return false;
1016
+ return `sha256:${createHash("sha256").update(readFileSync(file)).digest("hex")}` === ref.sha256;
1017
+ } catch {
1018
+ return false;
1019
+ }
1020
+ }
1021
+
1022
+ // The readability record (contract 2.4 Result rules), read in precedence
1023
+ // order:
1024
+ // 1. the record and cell reader checks (producer, schema, helper version,
1025
+ // integrity, canonical measured_at, the constant thresholds and limits,
1026
+ // the fixed producer values, the vocabulary, the exact routes × viewports
1027
+ // grid, every element re-derived through the shared helper, and each
1028
+ // shared cell's page_load integrity) → evidence_not_reproducible;
1029
+ // 2. the binding (build, source package, and doctor's output fingerprint)
1030
+ // → every row stale_binding, before any route-set result;
1031
+ // 3. the built route set, recomputed from _site/<slug>/: a captured route
1032
+ // the record lacks, or one over the route cap, reads page_not_captured;
1033
+ // a recorded route outside it, or different uncaptured routes or
1034
+ // enumeration cap, → evidence_not_reproducible;
1035
+ // 4. the rows READABILITY_QC_RULES.evaluate derives from each cell.
1036
+ // `campaignSlug` is the packet's slug; a missing one never matches.
1037
+ export function readReadability({ record, pageLoad = null, currentBuild = null, currentSource = null, buildOutput = null, campaignSlug = null, targetRepo = null, qcStandIns = null, rederivers = null } = {}) {
1038
+ if (record === undefined || record === null) return [];
1039
+ const [check] = POLISH_RECORDS.readability.checks;
1040
+ const measuredAt = canonicalTime(record?.measured_at) ? record.measured_at : null;
1041
+ const cells = Array.isArray(record?.cells) ? record.cells.filter(isPlainObject) : [];
1042
+ const subjectOf = (page, viewport, key) => ({ check, page, viewport, key });
1043
+ const rowOf = (subject, reasonCode) => unreproducedRow({ subject, check }, reasonCode, { leg: "polish", measuredAt });
1044
+ const unique = (rows) => [...new Map(rows.map((row) => [row.id, row])).values()];
1045
+ let rules = null;
1046
+ // A failed cell: one result per row the cell names and its `cell` row, so
1047
+ // a failed cell is never silent and an accept of one of its rows lapses
1048
+ // rather than orphans. The keys come from rules.rowKeys, which reads
1049
+ // neither the record's thresholds nor any field the checks reject.
1050
+ const failCell = (cell) => {
1051
+ let named = [];
1052
+ try {
1053
+ named = typeof rules?.rowKeys === "function" ? rules.rowKeys(cell) : [];
1054
+ } catch {
1055
+ named = [];
1056
+ }
1057
+ const keys = [...new Set([...(Array.isArray(named) ? named.filter(isNonEmptyString) : []), "cell"])];
1058
+ return keys.map((key) => rowOf(subjectOf(cell?.route ?? null, cell?.viewport ?? null, key), E));
1059
+ };
1060
+ // A record-level failure: every listed cell fails; every route the subject
1061
+ // or a cell names, at every recorded or fixed viewport, that no cell lists
1062
+ // gets its `cell` row; and the uncaptured routes and the enumeration cap
1063
+ // keep the rows they would read.
1064
+ const failAll = () => {
1065
+ const listed = new Set(cells.map((cell) => cellKey(cell.route, cell.viewport)));
1066
+ const routes = [...(Array.isArray(record?.subject?.routes) ? record.subject.routes : []), ...cells.map((cell) => cell.route)].filter(isNonEmptyString);
1067
+ const uncaptured = (Array.isArray(record?.uncaptured_routes) ? record.uncaptured_routes : []).filter(isNonEmptyString);
1068
+ let fixed = [];
1069
+ try {
1070
+ fixed = Array.isArray(rules?.viewports) ? rules.viewports : [];
1071
+ } catch {
1072
+ fixed = [];
1073
+ }
1074
+ const viewports = [...(Array.isArray(record?.subject?.viewports) ? record.subject.viewports : []), ...fixed].filter(isNonEmptyString);
1075
+ const grid = [...new Set([...routes, ...uncaptured])].flatMap((route) => [...new Set(viewports)].map((viewport) => ({ route, viewport })));
1076
+ const rows = [
1077
+ ...cells.flatMap(failCell),
1078
+ ...grid.filter(({ route, viewport }) => !listed.has(cellKey(route, viewport))).map(({ route, viewport }) => rowOf(subjectOf(route, viewport, "cell"), E)),
1079
+ ...(record?.route_enumeration_capped === true ? [rowOf(subjectOf(null, null, "scope"), E)] : []),
1080
+ ];
1081
+ return unique(rows.length ? rows : [rowOf(subjectOf(null, null, "readability"), E)]);
1082
+ };
1083
+ const evaluate = (cell, cropAvailable) => {
1084
+ try {
1085
+ return rules.evaluate(cell, record.thresholds, { cropAvailable });
1086
+ } catch {
1087
+ return null;
1088
+ }
1089
+ };
1090
+ const resolved = resolvePolishRules({ check, qcStandIns, rederivers });
1091
+ if (resolved.status === "missing") return [];
1092
+ if (resolved.status !== "loaded") return failAll();
1093
+ ({ rules } = resolved);
1094
+ if (typeof rules.routes !== "function" || typeof rules.cellShapeOk !== "function") return failAll();
1095
+
1096
+ // 1. Record level.
1097
+ const subject = record?.subject;
1098
+ const viewports = rules.viewports;
1099
+ if (!isPlainObject(record)
1100
+ || record.schema_version !== rules.schema_version
1101
+ || record.performed_by !== QC_PRODUCERS.polish
1102
+ || record.helper_version !== rules.helper_version
1103
+ || record.helper_version !== contrast.version
1104
+ || !measuredAt
1105
+ || record.integrity !== polishRecordIntegrity(record)
1106
+ || !sameJson(record.thresholds, rules.thresholds)
1107
+ || !sameJson(record.limits, rules.limits)
1108
+ || !hasExactly(subject, READABILITY_SUBJECT_FIELDS)
1109
+ || !(subject.source_package_material_fingerprint === null || isNonEmptyString(subject.source_package_material_fingerprint))
1110
+ || !sameJson(subject.viewports, viewports)
1111
+ || subject.route_source !== rules.route_source
1112
+ || !isNonEmptyString(campaignSlug)
1113
+ || subject.campaign_slug !== campaignSlug
1114
+ || !uniqueStrings(subject.routes)
1115
+ || !Array.isArray(record.uncaptured_routes)
1116
+ || !(record.uncaptured_routes.length === 0 || uniqueStrings(record.uncaptured_routes))
1117
+ || record.uncaptured_routes.some((route) => subject.routes.includes(route))
1118
+ || typeof record.route_enumeration_capped !== "boolean"
1119
+ || !Array.isArray(record.cells)
1120
+ || cells.length !== record.cells.length
1121
+ || !cells.every((cell) => rules.cellShapeOk(cell))) return failAll();
1122
+ // The cell set is exactly routes × viewports: none missing, none added,
1123
+ // none twice, every cell's route and viewport in the subject.
1124
+ const gridKeys = declaredGrid(subject).map(({ route, viewport }) => cellKey(route, viewport));
1125
+ const cellKeys = cells.map((cell) => cellKey(cell.route, cell.viewport));
1126
+ if (new Set(cellKeys).size !== cellKeys.length
1127
+ || cellKeys.length !== gridKeys.length
1128
+ || !cells.every((cell) => subject.routes.includes(cell.route) && subject.viewports.includes(cell.viewport))) return failAll();
1129
+
1130
+ // Cell level: every element re-derives, and a shared cell's page_load
1131
+ // integrity is that route × viewport's capture integrity.
1132
+ const captures = Array.isArray(pageLoad?.captures) ? pageLoad.captures : [];
1133
+ const cellReadable = (cell) => {
1134
+ if (!cell.elements.every(readabilityElementReproduces)) return false;
1135
+ if (cell.page_load_integrity === null) return true;
1136
+ const matching = captures.filter((capture) => capture?.subject?.requested_route === cell.route && capture?.subject?.viewport === cell.viewport);
1137
+ if (matching.length !== 1) return false;
1138
+ const [capture] = matching;
1139
+ return isPlainObject(capture.integrity)
1140
+ && sameJson(buildPolishCaptureIntegrity(capture), capture.integrity)
1141
+ && capture.integrity.projection_fingerprint === cell.page_load_integrity;
1142
+ };
1143
+
1144
+ // 2. Binding.
1145
+ const bound = isNonEmptyString(currentBuild)
1146
+ && subject.build_fingerprint === currentBuild
1147
+ && subject.source_package_material_fingerprint === (isNonEmptyString(currentSource) ? currentSource : null)
1148
+ && !outputDrifted(buildOutput ?? null);
1149
+
1150
+ // 3. The recomputed route set, read only when the output is the build's.
1151
+ const results = [];
1152
+ const notCaptured = (route) => subject.viewports.map((viewport) => rowOf(subjectOf(route, viewport, "cell"), PAGE_NOT_CAPTURED));
1153
+ const scopeRow = () => rowOf(subjectOf(null, null, "scope"), "route_enumeration_capped");
1154
+ if (bound) {
1155
+ let built;
1156
+ try {
1157
+ built = rules.routes(targetRepo, campaignSlug);
1158
+ } catch {
1159
+ built = null;
1160
+ }
1161
+ if (!isPlainObject(built) || built.ok !== true
1162
+ || !subject.routes.every((route) => built.routes.includes(route))
1163
+ || !sameJson(record.uncaptured_routes, built.uncaptured_routes)
1164
+ || record.route_enumeration_capped !== built.route_enumeration_capped) return failAll();
1165
+ for (const route of built.routes.filter((route) => !subject.routes.includes(route))) results.push(...notCaptured(route));
1166
+ }
1167
+ for (const route of record.uncaptured_routes) results.push(...notCaptured(route).map((row) => (bound ? row : staleRow(row))));
1168
+ if (record.route_enumeration_capped) results.push(bound ? scopeRow() : staleRow(scopeRow()));
1169
+
1170
+ // 4. The rows.
1171
+ const cropAvailable = (ref) => rules.cropRefShapeOk?.(ref) === true && readabilityCropResolves(ref, targetRepo);
1172
+ for (const cell of cells) {
1173
+ if (!cellReadable(cell)) {
1174
+ results.push(...failCell(cell));
1175
+ continue;
1176
+ }
1177
+ const derived = evaluate(cell, cropAvailable);
1178
+ // Every derived subject is the cell's own page and viewport, each key once.
1179
+ const keys = Array.isArray(derived) ? derived.map((item) => item?.subject?.key) : [];
1180
+ if (!Array.isArray(derived) || new Set(keys).size !== keys.length || !derived.every((item) => item?.check === check
1181
+ && validDerived(item, check)
1182
+ && hasExactly(item.subject, ["check", "page", "viewport", "key"])
1183
+ && item.subject.page === cell.route
1184
+ && item.subject.viewport === cell.viewport
1185
+ && isNonEmptyString(item.subject.key))) {
1186
+ results.push(...failCell(cell));
1187
+ continue;
1188
+ }
1189
+ for (const item of derived) {
1190
+ const members = item.members ?? [];
1191
+ const row = {
1192
+ schema: QC_RESULT_SCHEMA,
1193
+ id: qcResultId(item.subject),
1194
+ check,
1195
+ leg: "polish",
1196
+ result: item.result,
1197
+ reason_code: item.result === "pass" ? null : item.reason_code,
1198
+ subject: item.subject,
1199
+ state_fingerprint: qcStateFingerprint({ subject: item.subject, state: item.state }),
1200
+ observation: isPlainObject(item.observation) ? item.observation : {},
1201
+ members,
1202
+ accept_eligible: item.accept_eligible && qcAcceptEligible(item.result, members),
1203
+ coverage: item.coverage ?? { observed: 1, expected: 1, limits: [] },
1204
+ measured_at: measuredAt,
1205
+ producer: QC_PRODUCERS.polish,
1206
+ };
1207
+ results.push(bound ? row : staleRow(row));
1208
+ }
1209
+ }
1210
+ return results;
1211
+ }
1212
+
1213
+ // ---------------------------------------------------------------------------
1214
+ // Every current result, per leg, and the silence rule
1215
+
1216
+ const QA_ALWAYS = Object.freeze(["tracking.url", "tracking.order", "tracking.tag"]);
1217
+ const QA_CONTENT = Object.freeze(["content_param"]);
1218
+ const QA_POLICY = Object.freeze(["policy.presence", "policy.availability"]);
1219
+ const POLISH_CHECKS = Object.freeze(qcChecksForLeg("polish"));
1220
+ // The Polish dispatch: each package-owned visual_review record, the checks
1221
+ // read from it, and its reader. Each record is read, and its silence judged,
1222
+ // on its own.
1223
+ const POLISH_RECORDS = Object.freeze({
1224
+ media_weight: Object.freeze({ checks: Object.freeze(qcChecksForRecord("media_weight")), read: readMediaWeight }),
1225
+ readability: Object.freeze({ checks: Object.freeze(qcChecksForRecord("readability")), read: readReadability }),
1226
+ });
1227
+ const STORE_POLICY_FIELDS = Object.freeze(Object.keys(STORE_PAGE_MATCHERS));
1228
+
1229
+ // Applicability is decided without the leg running: the tracking checks
1230
+ // always, the content-param check when analytics.params.content is non-empty,
1231
+ // the policy-link checks when a store policy field is.
1232
+ export function applicableQaChecks(spec) {
1233
+ const content = spec?.analytics?.params?.content;
1234
+ const hasContent = Array.isArray(content) ? content.length > 0 : isPlainObject(content) && Object.keys(content).length > 0;
1235
+ const hasPolicy = STORE_POLICY_FIELDS.some((field) => isNonEmptyString(spec?.campaign?.[field]));
1236
+ return [...QA_ALWAYS, ...(hasContent ? QA_CONTENT : []), ...(hasPolicy ? QA_POLICY : [])];
1237
+ }
1238
+
1239
+ // A leg has a stage record unless the stage is absent or is the pending
1240
+ // placeholder prepare-build seeds (status pending, nothing recorded). Any other
1241
+ // stage, whatever its status, is a recorded leg.
1242
+ function stageRecorded(stage) {
1243
+ if (!isPlainObject(stage)) return false;
1244
+ if (stage.status !== "pending") return true;
1245
+ return isPlainObject(stage.evidence)
1246
+ || (isPlainObject(stage.identity) && Object.keys(stage.identity).length > 0)
1247
+ || (Array.isArray(stage.outputs) && stage.outputs.length > 0);
1248
+ }
1249
+
1250
+ const silence = (leg, check, reasonCode) => ({ check, leg, result: "unexercised", reason_code: reasonCode, count: 0, pages: [] });
1251
+
1252
+ // The handoff coverage entries for applicable checks a leg holds no row for.
1253
+ // A leg with no stage record reads leg_not_run. A recorded leg never makes an
1254
+ // applicable check silent: no row reads not_captured_by_this_version (the
1255
+ // record predates the check, its module is missing or not yet loaded, or the
1256
+ // loaded rules gave no row), except that a module that failed to load reads
1257
+ // evidence_not_reproducible when the record holds the leg's QC evidence.
1258
+ export function handoffCoverage({ report, spec, results = [], qcStandIns = null, rederivers = null }) {
1259
+ const entries = [];
1260
+ const hasRow = (leg, check) => results.some((row) => row.leg === leg && row.check === check);
1261
+ const silentReason = (failed) => (failed ? E : QC_REASON.NOT_CAPTURED);
1262
+
1263
+ const qaStage = report?.stages?.qa;
1264
+ const qaChecks = applicableQaChecks(spec);
1265
+ if (!stageRecorded(qaStage)) {
1266
+ for (const check of qaChecks) entries.push(silence("qa", check, QC_REASON.LEG_NOT_RUN));
1267
+ } else {
1268
+ const captured = Array.isArray(qaStage.evidence?.qc_results);
1269
+ for (const check of qaChecks) {
1270
+ if (hasRow("qa", check)) continue;
1271
+ entries.push(silence("qa", check, silentReason(captured && qaRederiver(check, { qcStandIns, rederivers }).status === "failed")));
1272
+ }
1273
+ }
1274
+
1275
+ const polishStage = report?.stages?.polish;
1276
+ if (!stageRecorded(polishStage)) {
1277
+ for (const check of POLISH_CHECKS) entries.push(silence("polish", check, QC_REASON.LEG_NOT_RUN));
1278
+ } else {
1279
+ const visual = polishStage.evidence?.visual_review;
1280
+ for (const [name, { checks }] of Object.entries(POLISH_RECORDS)) {
1281
+ const captured = name === "media_weight" ? isPlainObject(visual?.page_load) && visual.media_weight != null : visual?.[name] != null;
1282
+ for (const check of checks) {
1283
+ if (hasRow("polish", check)) continue;
1284
+ const failed = captured && resolvePolishRules({ check, qcStandIns, rederivers }).status === "failed";
1285
+ entries.push(silence("polish", check, silentReason(failed)));
1286
+ }
1287
+ }
1288
+ }
1289
+ return entries;
1290
+ }
1291
+
1292
+ // The campaign slug the Polish records bind to: the packet's, read from
1293
+ // packetPath; with no readable packet, the Assembly Report's (which capture
1294
+ // requires to equal the packet's). Null when neither names one.
1295
+ function packetCampaignSlug({ packetPath, report }) {
1296
+ if (isNonEmptyString(packetPath)) {
1297
+ try {
1298
+ const slug = JSON.parse(readFileSync(packetPath, "utf8"))?.campaign?.public_route_slug;
1299
+ return isNonEmptyString(slug) ? slug : null;
1300
+ } catch {
1301
+ // No readable packet: fall through to the report.
1302
+ }
1303
+ }
1304
+ const slug = report?.identity?.public_route_slug;
1305
+ return isNonEmptyString(slug) ? slug : null;
1306
+ }
1307
+
1308
+ // Every current QC result `next` and `checkpoint accept` judge, read from
1309
+ // data already on disk: the doctor run, the Assembly Report, and the full QA
1310
+ // verdict the QA stage names. No request is made.
1311
+ export function readCurrentQcResults({ report, doctor, spec = null, targetRepo, packetPath = null, reportPath = null, qcStandIns = null, rederivers = null }) {
1312
+ const currentBuild = currentBuildFingerprint(report);
1313
+ const doctorResults = (Array.isArray(doctor?.derived?.qc_results) ? doctor.derived.qc_results : []).filter((row) => isPlainObject(row) && row.leg === "doctor");
1314
+ const visual = report?.stages?.polish?.evidence?.visual_review;
1315
+ const recorded = isPlainObject(visual) ? Object.entries(POLISH_RECORDS).filter(([name]) => visual[name] != null) : [];
1316
+ const polishContext = recorded.length ? {
1317
+ pageLoad: visual.page_load,
1318
+ currentBuild,
1319
+ currentSource: currentSourcePackageMaterialFingerprint(report),
1320
+ buildOutput: doctor?.derived?.build_output_fingerprint ?? null,
1321
+ campaignSlug: packetCampaignSlug({ packetPath, report }),
1322
+ targetRepo,
1323
+ qcStandIns,
1324
+ rederivers,
1325
+ } : null;
1326
+ const polishResults = recorded.flatMap(([name, { read }]) => read({ ...polishContext, record: visual[name] }));
1327
+ const qaStage = report?.stages?.qa;
1328
+ const qaResults = Array.isArray(qaStage?.evidence?.qc_results)
1329
+ ? readQaResults({
1330
+ stageEvidence: qaStage.evidence,
1331
+ stage: qaStage,
1332
+ fullVerdict: readQaFullVerdict({ stage: qaStage, targetRepo, packetPath, reportPath }),
1333
+ currentBuild,
1334
+ qaCurrency: doctor?.derived?.input_currency?.stages?.qa,
1335
+ qcStandIns,
1336
+ rederivers,
1337
+ })
1338
+ : [];
1339
+ const results = [...doctorResults, ...polishResults, ...qaResults];
1340
+ return { results, coverage: handoffCoverage({ report, spec, results, qcStandIns, rederivers }) };
1341
+ }