fix: require runtime evidence in budget gate
This commit is contained in:
@@ -10,13 +10,16 @@ import {
|
||||
classifyArtifact,
|
||||
evaluateBudgetTotals,
|
||||
evaluateMediaAssetBudget,
|
||||
resolveBudgetGateMode,
|
||||
validateRuntimeBudgetTraceReport,
|
||||
} from './lib/bundle-budgets.mjs';
|
||||
|
||||
const root = resolve(process.argv[2] ?? 'dist');
|
||||
const transferOnly = process.argv.includes('--transfer-only');
|
||||
const runtimeTraceIndex = process.argv.indexOf('--runtime-trace');
|
||||
const runtimeTracePath = runtimeTraceIndex === -1 ? null : process.argv[runtimeTraceIndex + 1];
|
||||
if (runtimeTraceIndex !== -1 && runtimeTracePath === undefined) throw new Error('--runtime-trace requires one explicit drive-trace JSON path.');
|
||||
const gateMode = resolveBudgetGateMode({ transferOnly, runtimeTracePath });
|
||||
if (!(await stat(root)).isDirectory()) throw new Error('Bundle budget target must be a directory.');
|
||||
|
||||
const listFiles = async (directory) => {
|
||||
@@ -121,8 +124,8 @@ if (!/import\([^)]*environment-/u.test(entrySource)) throw new Error('The live s
|
||||
if (/WebGLRenderer|WebGLProgram|ShaderMaterial/u.test(entrySource)) throw new Error('Three.js/live-scene code leaked into the reduced-motion initial chunk.');
|
||||
|
||||
let runtime = null;
|
||||
if (runtimeTracePath !== null) {
|
||||
const trace = await readJson(resolve(runtimeTracePath), 'Runtime drive trace');
|
||||
if (gateMode.kind === 'full') {
|
||||
const trace = await readJson(resolve(gateMode.runtimeTracePath), 'Runtime drive trace');
|
||||
runtime = validateRuntimeBudgetTraceReport(trace);
|
||||
}
|
||||
const violations = [...evaluateBudgetTotals(totals).violations, ...mediaViolations, ...(runtime?.violations ?? [])];
|
||||
@@ -130,7 +133,9 @@ const report = {
|
||||
schema: 'portfolio-bundle-budget/v1',
|
||||
compression: 'brotli-quality-11-or-raw-already-compressed',
|
||||
totals,
|
||||
runtime: runtime === null ? { status: 'pending-production-path-trace' } : { status: 'verified-production-path-trace', tier: runtime.tier },
|
||||
runtime: runtime === null
|
||||
? { status: 'transfer-only-non-gating', gating: false, runtimeEvidence: 'pending' }
|
||||
: { status: 'verified-production-path-trace', gating: true, tier: runtime.tier },
|
||||
records: records.sort((left, right) => left.path.localeCompare(right.path)),
|
||||
violations,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
export const PERFORMANCE_BUDGETS: Readonly<Record<string, number>>;
|
||||
export function resolveBudgetGateMode(options: {
|
||||
transferOnly: boolean;
|
||||
runtimeTracePath: string | null;
|
||||
}): { kind: 'transfer-only' } | { kind: 'full'; runtimeTracePath: string };
|
||||
export function classifyArtifact(path: string, source?: string): string;
|
||||
export function evaluateBudgetTotals(totals: Readonly<Record<string, number>>): {
|
||||
violations: Array<{ budget: string; actual: number; threshold: number }>;
|
||||
|
||||
@@ -20,6 +20,13 @@ export const PERFORMANCE_BUDGETS = Object.freeze({
|
||||
highTierGpuEstimate: 192 * 1024 * 1024,
|
||||
});
|
||||
|
||||
export const resolveBudgetGateMode = ({ transferOnly, runtimeTracePath }) => {
|
||||
if (transferOnly && runtimeTracePath !== null) throw new Error('Transfer-only mode cannot be combined with runtime evidence.');
|
||||
if (transferOnly) return { kind: 'transfer-only' };
|
||||
if (runtimeTracePath === null) throw new Error('A runtime trace is required for the full performance budget gate. Use --transfer-only only for a non-gating static build check.');
|
||||
return { kind: 'full', runtimeTracePath };
|
||||
};
|
||||
|
||||
export const classifyArtifact = (path, source = '') => {
|
||||
if (path === 'index.html') return 'initial-html';
|
||||
if (/^assets\/index-[^/]+\.css$/u.test(path)) return 'critical-css';
|
||||
|
||||
35
scripts/verify-runtime-budgets.mjs
Normal file
35
scripts/verify-runtime-budgets.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
import { spawn } from 'node:child_process';
|
||||
import { access } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const repositoryRoot = process.cwd();
|
||||
const tracePath = resolve(repositoryRoot, 'test-results/runtime-budget-trace.v1.json');
|
||||
|
||||
const runNode = (args) => new Promise((resolveRun, rejectRun) => {
|
||||
const child = spawn(process.execPath, args, {
|
||||
cwd: repositoryRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
child.once('error', rejectRun);
|
||||
child.once('exit', (code, signal) => {
|
||||
if (code === 0) resolveRun();
|
||||
else rejectRun(new Error(`Command failed${signal === null ? ` with exit code ${code}` : ` from signal ${signal}`}.`));
|
||||
});
|
||||
});
|
||||
|
||||
await runNode([
|
||||
resolve(repositoryRoot, 'node_modules/@playwright/test/cli.js'),
|
||||
'test',
|
||||
'tests/performance/runtime-budget-trace.spec.ts',
|
||||
'--project=runtime-budget-desktop',
|
||||
'--workers=1',
|
||||
]);
|
||||
await access(tracePath);
|
||||
await runNode([
|
||||
resolve(repositoryRoot, 'scripts/check-bundle-budgets.mjs'),
|
||||
'dist',
|
||||
'--runtime-trace',
|
||||
tracePath,
|
||||
]);
|
||||
Reference in New Issue
Block a user