Compare commits

...
9 changed files with 432 additions and 47 deletions
@@ -1,5 +1,25 @@
{ {
"schemaVersion": 1, "schemaVersion": 1,
"releaseId": "local-release", "releaseId": "local-release",
"environment": "replace-with-production",
"source": {
"system": "",
"exportId": ""
},
"privacy": {
"approved": false,
"approvalRef": ""
},
"window": {
"start": "2026-06-01T00:00:00Z",
"end": "2026-06-29T00:00:00Z"
},
"thresholdDecision": {
"status": "pending",
"minimumEligibleSamples": null,
"owner": "",
"reviewedAt": "",
"evidenceRef": ""
},
"samples": [] "samples": []
} }
+47 -3
View File
@@ -8,6 +8,7 @@
"window", "window",
"context", "context",
"metrics", "metrics",
"thresholds",
"eligibility", "eligibility",
"status", "status",
"passed" "passed"
@@ -18,12 +19,55 @@
"window": { "type": "object", "required": ["days", "start", "end"] }, "window": { "type": "object", "required": ["days", "start", "end"] },
"context": { "context": {
"type": "object", "type": "object",
"required": ["source", "network", "routeAggregation", "releaseId"] "required": [
"source",
"sourceSystem",
"exportId",
"network",
"routeAggregation",
"releaseId",
"privacyApprovalRef",
"thresholdDecisionRef",
"validationFailures"
],
"properties": {
"source": { "type": "string" },
"sourceSystem": { "type": ["string", "null"] },
"exportId": { "type": ["string", "null"] },
"network": { "const": "production-real-user" },
"routeAggregation": { "const": "route-id-only" },
"releaseId": { "type": ["string", "null"] },
"privacyApprovalRef": { "type": ["string", "null"] },
"thresholdDecisionRef": { "type": ["string", "null"] },
"validationFailures": {
"type": "array",
"items": { "type": "string" }
}
},
"additionalProperties": false
},
"thresholds": {
"type": "object",
"required": [
"p75LcpMs",
"p75Cls",
"p75InpMs",
"minimumEligibleSamples"
]
},
"metrics": {
"type": "object",
"required": ["p75LcpMs", "p75Cls", "p75InpMs"]
}, },
"metrics": { "type": "object" },
"eligibility": { "eligibility": {
"type": "object", "type": "object",
"required": ["consentRequired", "eligibleSamples", "minimumEligibleSamples"] "required": [
"consentRequired",
"totalSamples",
"eligibleSamples",
"minimumEligibleSamples",
"routeSamples"
]
}, },
"status": { "status": {
"enum": ["PASS", "FAIL_THRESHOLD", "FAIL_UNVERIFIED"] "enum": ["PASS", "FAIL_THRESHOLD", "FAIL_UNVERIFIED"]
+7 -2
View File
@@ -13,5 +13,10 @@ Performance evidence is deliberately split by measurement context:
The field minimum eligible-sample threshold is intentionally unresolved until The field minimum eligible-sample threshold is intentionally unresolved until
a privacy-approved telemetry baseline exists. Therefore the field command a privacy-approved telemetry baseline exists. Therefore the field command
fails closed with `FAIL_UNVERIFIED` when run against the example input. Provide fails closed with `FAIL_UNVERIFIED` when run against the example input. Provide
`FIELD_WEB_VITALS_INPUT` and a reviewed `MIN_ELIGIBLE_SAMPLES` only after that `FIELD_WEB_VITALS_INPUT` and `MIN_ELIGIBLE_SAMPLES` only after that decision is
decision is recorded. recorded. The external input must identify a production release and an exact
28-day export window, name the source/export, carry privacy-approval and
threshold-decision references, and contain only non-negative route-ID samples.
The environment threshold must be a positive integer equal to the approved
decision embedded in the input. Invalid metadata fails as `FAIL_UNVERIFIED`;
the example can never serve as production evidence.
+22 -15
View File
@@ -1,6 +1,7 @@
import { readFile, writeFile } from "node:fs/promises"; import { readFile, writeFile } from "node:fs/promises";
import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js"; import { evaluateBundleBudget } from "../src/application/policies/performance-budgets.js";
import { classifyViteJavascript } from "./lib/classify-vite-bundle.mjs";
const report = const report =
/** @type {{ /** @type {{
@@ -10,7 +11,7 @@ const report =
JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8")) JSON.parse(await readFile("artifacts/performance/bundle.json", "utf8"))
); );
const viteManifest = const viteManifest =
/** @type {Record<string, { file: string, isEntry?: boolean }>} */ ( /** @type {Record<string, { file: string, isEntry?: boolean, imports?: string[] }>} */ (
JSON.parse(await readFile("dist/.vite/manifest.json", "utf8")) JSON.parse(await readFile("dist/.vite/manifest.json", "utf8"))
); );
const budgets = const budgets =
@@ -21,24 +22,19 @@ const budgets =
const outputByPath = new Map( const outputByPath = new Map(
report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]), report.outputs.map((output) => [output.path.replace(/^dist\//, ""), output]),
); );
const initialFiles = new Set( const classification = classifyViteJavascript(viteManifest);
Object.values(viteManifest) const initialJsGzipBytes = classification.initialFiles.reduce(
.filter((entry) => entry.isEntry)
.map((entry) => entry.file),
);
const lazyFiles = new Set(
Object.values(viteManifest)
.filter((entry) => !entry.isEntry && entry.file.endsWith(".js"))
.map((entry) => entry.file),
);
const initialJsGzipBytes = [...initialFiles].reduce(
(total, file) => total + (outputByPath.get(file)?.gzipBytes ?? 0), (total, file) => total + (outputByPath.get(file)?.gzipBytes ?? 0),
0, 0,
); );
const lazyChunks = [...lazyFiles].map((file) => ({ const lazyChunks = classification.lazyFiles.map((file) => ({
path: file, path: file,
gzipBytes: outputByPath.get(file)?.gzipBytes ?? 0, gzipBytes: outputByPath.get(file)?.gzipBytes ?? 0,
})); }));
const missingOutputs = [
...classification.initialFiles,
...classification.lazyFiles,
].filter((file) => !outputByPath.has(file));
const measurements = { initialJsGzipBytes, lazyChunks }; const measurements = { initialJsGzipBytes, lazyChunks };
const result = evaluateBundleBudget(measurements, budgets); const result = evaluateBundleBudget(measurements, budgets);
const fixtures = [ const fixtures = [
@@ -70,10 +66,16 @@ const fixtures = [
).passed, ).passed,
}, },
]; ];
const passed = result.passed && fixtures.every((fixture) => fixture.passed); const passed =
result.passed &&
fixtures.every((fixture) => fixture.passed) &&
classification.missingImports.length === 0 &&
missingOutputs.length === 0;
const completedReport = { const completedReport = {
...report, ...report,
measurements, measurements,
classification,
missingOutputs,
thresholds: budgets, thresholds: budgets,
results: result, results: result,
fixtures, fixtures,
@@ -85,7 +87,12 @@ await writeFile(
`${JSON.stringify(completedReport, null, 2)}\n`, `${JSON.stringify(completedReport, null, 2)}\n`,
); );
if (!passed) { if (!passed) {
process.stderr.write("Bundle budget exceeded.\n"); process.stderr.write(
`Bundle budget or manifest integrity failed: ${[
...classification.missingImports,
...missingOutputs,
].join(", ")}\n`,
);
process.exit(1); process.exit(1);
} }
process.stdout.write( process.stdout.write(
+31 -27
View File
@@ -4,23 +4,19 @@ import {
evaluateFieldBudget, evaluateFieldBudget,
percentile75, percentile75,
} from "../src/application/policies/performance-budgets.js"; } from "../src/application/policies/performance-budgets.js";
import { validateFieldEvidenceInput } from "./lib/field-vitals-evidence.mjs";
const inputPath = const inputPath =
process.env.FIELD_WEB_VITALS_INPUT ?? process.env.FIELD_WEB_VITALS_INPUT ??
"config/performance/field-input.example.json"; "config/performance/field-input.example.json";
const input = const rawInput = JSON.parse(await readFile(inputPath, "utf8"));
/** @type {{ const now = new Date();
* releaseId: string, const validation = validateFieldEvidenceInput(
* samples: Array<{ rawInput,
* timestamp: string, process.env.MIN_ELIGIBLE_SAMPLES,
* consent: boolean, now,
* releaseId: string, );
* routeId: string, const input = validation.data;
* lcpMs: number,
* cls: number,
* inpMs: number
* }>
* }} */ (JSON.parse(await readFile(inputPath, "utf8")));
const configured = const configured =
/** @type {{ /** @type {{
* p75LcpMs: number, * p75LcpMs: number,
@@ -30,17 +26,17 @@ const configured =
* }} */ ( * }} */ (
JSON.parse(await readFile("config/performance/budgets.json", "utf8")).field JSON.parse(await readFile("config/performance/budgets.json", "utf8")).field
); );
const minimumEligibleSamples = process.env.MIN_ELIGIBLE_SAMPLES const minimumEligibleSamples = validation.minimumEligibleSamples;
? Number(process.env.MIN_ELIGIBLE_SAMPLES) const fallbackEnd = now;
: configured.minimumEligibleSamples; const fallbackStart = new Date(fallbackEnd);
const end = new Date(); fallbackStart.setUTCDate(fallbackStart.getUTCDate() - 28);
const start = new Date(end); const start = input ? new Date(input.window.start) : fallbackStart;
start.setUTCDate(start.getUTCDate() - 28); const end = input ? new Date(input.window.end) : fallbackEnd;
const eligible = input.samples.filter((sample) => { const eligible = (input?.samples ?? []).filter((sample) => {
const timestamp = new Date(sample.timestamp); const timestamp = new Date(sample.timestamp);
return ( return (
sample.consent === true && sample.consent === true &&
sample.releaseId === input.releaseId && sample.releaseId === input?.releaseId &&
timestamp >= start && timestamp >= start &&
timestamp <= end timestamp <= end
); );
@@ -55,6 +51,8 @@ const result = evaluateFieldBudget(
{ metrics, eligibleSamples: eligible.length }, { metrics, eligibleSamples: eligible.length },
thresholds, thresholds,
); );
const passed = validation.passed && result.passed;
const status = validation.passed ? result.status : "FAIL_UNVERIFIED";
const routeSamples = Object.fromEntries( const routeSamples = Object.fromEntries(
Object.entries( Object.entries(
eligible.reduce( eligible.reduce(
@@ -68,24 +66,30 @@ const routeSamples = Object.fromEntries(
); );
const report = { const report = {
schemaVersion: 1, schemaVersion: 1,
generatedAt: end.toISOString(), generatedAt: now.toISOString(),
window: { days: 28, start: start.toISOString(), end: end.toISOString() }, window: { days: 28, start: start.toISOString(), end: end.toISOString() },
context: { context: {
source: inputPath, source: inputPath,
sourceSystem: input?.source.system ?? null,
exportId: input?.source.exportId ?? null,
network: "production-real-user", network: "production-real-user",
routeAggregation: "route-id-only", routeAggregation: "route-id-only",
releaseId: input.releaseId, releaseId: input?.releaseId ?? null,
privacyApprovalRef: input?.privacy.approvalRef ?? null,
thresholdDecisionRef: input?.thresholdDecision.evidenceRef ?? null,
validationFailures: validation.failures,
}, },
metrics, metrics,
thresholds, thresholds,
eligibility: { eligibility: {
consentRequired: true, consentRequired: true,
totalSamples: input?.samples.length ?? 0,
eligibleSamples: eligible.length, eligibleSamples: eligible.length,
minimumEligibleSamples, minimumEligibleSamples,
routeSamples, routeSamples,
}, },
status: result.status, status,
passed: result.passed, passed,
}; };
await mkdir("artifacts/performance", { recursive: true }); await mkdir("artifacts/performance", { recursive: true });
@@ -93,9 +97,9 @@ await writeFile(
"artifacts/performance/field-web-vitals.json", "artifacts/performance/field-web-vitals.json",
`${JSON.stringify(report, null, 2)}\n`, `${JSON.stringify(report, null, 2)}\n`,
); );
if (!result.passed) { if (!passed) {
process.stderr.write( process.stderr.write(
`Field Web Vitals: ${result.status} (minimum eligible sample threshold and 28-day production data are required)\n`, `Field Web Vitals: ${status} (approved threshold decision and valid 28-day production evidence are required)\n`,
); );
process.exit(1); process.exit(1);
} }
+49
View File
@@ -0,0 +1,49 @@
/**
* @typedef {{
* file: string,
* isEntry?: boolean,
* imports?: string[]
* }} ViteManifestEntry
*/
/**
* Static imports of an entry are part of initial JavaScript. Every remaining
* JavaScript output is governed by the lazy-chunk budget.
*
* @param {Record<string, ViteManifestEntry>} manifest
*/
export function classifyViteJavascript(manifest) {
const initialFiles = new Set();
const visitedKeys = new Set();
const pendingKeys = Object.entries(manifest)
.filter(([, entry]) => entry.isEntry)
.map(([key]) => key);
const missingImports = [];
while (pendingKeys.length > 0) {
const key = /** @type {string} */ (pendingKeys.pop());
if (visitedKeys.has(key)) continue;
visitedKeys.add(key);
const entry = manifest[key];
if (!entry) {
missingImports.push(key);
continue;
}
if (entry.file.endsWith(".js")) initialFiles.add(entry.file);
pendingKeys.push(...(entry.imports ?? []));
}
const allJavaScript = new Set(
Object.values(manifest)
.map((entry) => entry.file)
.filter((file) => file.endsWith(".js")),
);
const lazyFiles = [...allJavaScript].filter(
(file) => !initialFiles.has(file),
);
return Object.freeze({
initialFiles: Object.freeze([...initialFiles].sort()),
lazyFiles: Object.freeze(lazyFiles.sort()),
missingImports: Object.freeze(missingImports.sort()),
});
}
+122
View File
@@ -0,0 +1,122 @@
import { z } from "zod";
const WINDOW_MILLISECONDS = 28 * 24 * 60 * 60 * 1000;
const nonEmptyString = z.string().trim().min(1);
const timestamp = nonEmptyString.refine(
(value) => Number.isFinite(Date.parse(value)),
"must be an RFC 3339 timestamp",
);
const sampleSchema = z
.object({
timestamp,
consent: z.boolean(),
releaseId: nonEmptyString,
routeId: nonEmptyString.regex(/^[A-Z][A-Z0-9_]*$/),
lcpMs: z.number().finite().nonnegative(),
cls: z.number().finite().nonnegative(),
inpMs: z.number().finite().nonnegative(),
})
.strict();
const fieldEvidenceInputSchema = z
.object({
schemaVersion: z.literal(1),
environment: z.literal("production"),
releaseId: nonEmptyString.refine(
(value) => value !== "local-release",
"must identify an immutable production release",
),
source: z
.object({
system: nonEmptyString,
exportId: nonEmptyString,
})
.strict(),
privacy: z
.object({
approved: z.literal(true),
approvalRef: nonEmptyString,
})
.strict(),
window: z
.object({
start: timestamp,
end: timestamp,
})
.strict(),
thresholdDecision: z
.object({
status: z.literal("approved"),
minimumEligibleSamples: z.number().int().positive(),
owner: nonEmptyString,
reviewedAt: timestamp,
evidenceRef: nonEmptyString,
})
.strict(),
samples: z.array(sampleSchema),
})
.strict()
.superRefine((input, context) => {
const start = Date.parse(input.window.start);
const end = Date.parse(input.window.end);
if (end - start !== WINDOW_MILLISECONDS) {
context.addIssue({
code: "custom",
path: ["window"],
message: "must cover exactly 28 days",
});
}
});
/**
* @param {unknown} input
* @param {string | undefined} configuredMinimum
* @param {Date} [now]
*/
export function validateFieldEvidenceInput(
input,
configuredMinimum,
now = new Date(),
) {
const parsed = fieldEvidenceInputSchema.safeParse(input);
const failures = parsed.success
? []
: parsed.error.issues.map(
(issue) => `${issue.path.join(".") || "input"}: ${issue.message}`,
);
const minimumEligibleSamples = Number(configuredMinimum);
if (
configuredMinimum === undefined ||
!Number.isInteger(minimumEligibleSamples) ||
minimumEligibleSamples <= 0
) {
failures.push("MIN_ELIGIBLE_SAMPLES: must be a positive integer");
}
if (parsed.success) {
if (
parsed.data.thresholdDecision.minimumEligibleSamples !==
minimumEligibleSamples
) {
failures.push(
"MIN_ELIGIBLE_SAMPLES: does not match the approved threshold decision",
);
}
if (Date.parse(parsed.data.window.end) > now.getTime()) {
failures.push("window.end: must not be in the future");
}
if (Date.parse(parsed.data.thresholdDecision.reviewedAt) > now.getTime()) {
failures.push("thresholdDecision.reviewedAt: must not be in the future");
}
}
return Object.freeze({
data: parsed.success ? parsed.data : null,
failures: Object.freeze(failures),
minimumEligibleSamples:
Number.isInteger(minimumEligibleSamples) && minimumEligibleSamples > 0
? minimumEligibleSamples
: null,
passed: parsed.success && failures.length === 0,
});
}
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { classifyViteJavascript } from "../../scripts/lib/classify-vite-bundle.mjs";
describe("Vite bundle classification", () => {
it("counts transitive static imports as initial and keeps dynamic chunks lazy", () => {
expect(
classifyViteJavascript({
"index.html": {
file: "assets/entry.js",
isEntry: true,
imports: ["_shared.js"],
},
"_shared.js": { file: "assets/shared.js", imports: ["_runtime.js"] },
"_runtime.js": { file: "assets/runtime.js" },
"src/lazy.js": { file: "assets/lazy.js" },
}),
).toEqual({
initialFiles: [
"assets/entry.js",
"assets/runtime.js",
"assets/shared.js",
],
lazyFiles: ["assets/lazy.js"],
missingImports: [],
});
});
it("reports a manifest import that cannot be resolved", () => {
expect(
classifyViteJavascript({
"index.html": {
file: "assets/entry.js",
isEntry: true,
imports: ["_missing.js"],
},
}).missingImports,
).toEqual(["_missing.js"]);
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import { validateFieldEvidenceInput } from "../../scripts/lib/field-vitals-evidence.mjs";
const input = {
schemaVersion: 1,
environment: "production",
releaseId: "release-2026-06-29",
source: {
system: "privacy-approved-rum-export",
exportId: "export-2026-06-29",
},
privacy: {
approved: true,
approvalRef: "PRIVACY-42",
},
window: {
start: "2026-06-01T00:00:00Z",
end: "2026-06-29T00:00:00Z",
},
thresholdDecision: {
status: "approved",
minimumEligibleSamples: 25,
owner: "performance-owner",
reviewedAt: "2026-06-30T00:00:00Z",
evidenceRef: "PERF-BASELINE-7",
},
samples: [
{
timestamp: "2026-06-20T00:00:00Z",
consent: true,
releaseId: "release-2026-06-29",
routeId: "APP_HOME",
lcpMs: 1200,
cls: 0.01,
inpMs: 80,
},
],
};
describe("field Web Vitals evidence input", () => {
it("accepts reviewed, coherent 28-day production metadata", () => {
expect(
validateFieldEvidenceInput(
input,
"25",
new Date("2026-07-01T00:00:00Z"),
),
).toMatchObject({
failures: [],
minimumEligibleSamples: 25,
passed: true,
});
});
it("rejects a threshold that does not match the owner decision", () => {
expect(
validateFieldEvidenceInput(
input,
"10",
new Date("2026-07-01T00:00:00Z"),
),
).toMatchObject({
failures: [
"MIN_ELIGIBLE_SAMPLES: does not match the approved threshold decision",
],
passed: false,
});
});
it("rejects local, unapproved, malformed, or impossible measurements", () => {
const invalid = {
...input,
environment: "local",
releaseId: "local-release",
privacy: { approved: false, approvalRef: "" },
window: { ...input.window, end: "2026-06-28T00:00:00Z" },
samples: [{ ...input.samples[0], lcpMs: -1 }],
};
const validation = validateFieldEvidenceInput(
invalid,
"-1",
new Date("2026-07-01T00:00:00Z"),
);
expect(validation.passed).toBe(false);
expect(validation.failures.join("\n")).toContain("environment");
expect(validation.failures.join("\n")).toContain("releaseId");
expect(validation.failures.join("\n")).toContain("privacy");
expect(validation.failures.join("\n")).toContain("lcpMs");
expect(validation.failures.join("\n")).toContain(
"MIN_ELIGIBLE_SAMPLES: must be a positive integer",
);
});
});