mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
Accept build-output proof for vite.config.ts commits
The deployment-installability verification policy routed
frontend-modern/vite.config.ts through the dev-runtime orchestration
proof set, all of which exercise the unbuilt hot-dev runtime. No
accepted proof could observe production build output, which is why
c4af728c0 (preload posture change) needed
PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT.
Split vite.config.ts into its own frontend-build-output path policy:
the accepted set keeps every dev-runtime proof, so dev-server-facing
edits are unchanged, and adds
frontend-modern/scripts/check-bundle-size.mjs, which now also asserts
the built index.html posture the contract clause pins: modulepreload
links limited to the entry's static imports (no lazy route chunks) and
import map integrity coverage of every built JS asset. The guard test
pins the new policy's accepted set.
Verified against the built output: flipping preloadDynamicChunks to
true fails the check with 47 lazy-chunk preload violations; the
healthy build passes. Full canonical-governance chain run locally, all
exit 0.
This commit is contained in:
@@ -984,6 +984,12 @@ upgrade, update, release, or artifact-selection behavior.
|
||||
effective on slow devices), while dynamic-import integrity remains enforced
|
||||
through the generated import map `integrity` block. Reintroducing whole-app
|
||||
preloading is a governed regression, not a tuning knob.
|
||||
`frontend-modern/scripts/check-bundle-size.mjs` pins this posture against the
|
||||
built output (modulepreload links limited to the entry's static imports,
|
||||
import map `integrity` coverage of every built JS asset) and is the accepted
|
||||
build-output verification proof for `frontend-modern/vite.config.ts` changes
|
||||
under the `frontend-build-output` path policy, alongside the dev-runtime
|
||||
orchestration proofs for dev-server-facing edits to the same file.
|
||||
Managed browser verification must also restart an existing hot-dev session
|
||||
when a verification lock is active or the runtime auth file no longer matches
|
||||
the deterministic dev user/hash. `tests/integration/scripts/run-playwright.mjs`
|
||||
|
||||
@@ -4713,12 +4713,31 @@
|
||||
"scripts/installtests/build_release_assets_test.go"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "frontend-build-output",
|
||||
"label": "frontend production build output proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/vite.config.ts"
|
||||
],
|
||||
"allow_same_subsystem_tests": false,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/scripts/check-bundle-size.mjs",
|
||||
"scripts/release_control/ssh_host_key_policy_test.py",
|
||||
"scripts/tests/test-hot-dev-auth.sh",
|
||||
"scripts/tests/test-hot-dev-bg.sh",
|
||||
"scripts/tests/test-hot-dev-runtime.sh",
|
||||
"scripts/tests/test-toggle-mock.sh",
|
||||
"tests/integration/scripts/managed-local-backend.test.mjs",
|
||||
"tests/integration/tests/16-dev-runtime-recovery.spec.ts"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "dev-runtime-orchestration",
|
||||
"label": "dev runtime orchestration proof",
|
||||
"match_prefixes": [],
|
||||
"match_files": [
|
||||
"frontend-modern/vite.config.ts",
|
||||
"go.mod",
|
||||
"go.sum",
|
||||
"Makefile",
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
* 3. Groups files sharing the same logical name (e.g. two "Dashboard" chunks)
|
||||
* 4. Computes gzip size for each file using Node zlib (level 6, default)
|
||||
* 5. Compares per-chunk and total gzip sizes against .bundlesize.json thresholds
|
||||
* 6. Exits 0 on pass, 1 on any violation
|
||||
* 6. Asserts the built index.html preload posture: modulepreload links are
|
||||
* limited to the entry's static imports (no lazy route chunks), and the
|
||||
* import map integrity block covers every built JS asset
|
||||
* 7. Exits 0 on pass, 1 on any violation
|
||||
*/
|
||||
|
||||
import { readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
@@ -90,6 +93,103 @@ function measureBuild() {
|
||||
return { groups, totalJsGzip, totalCssGzip };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Built index.html preload posture
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Verify the deployment-installability preload invariant against the built
|
||||
* index.html (see the subsystem contract clause on modulepreload posture):
|
||||
* - the import map integrity block exists and covers every JS asset in
|
||||
* dist/assets, so dynamic-import SRI stays enforced without preloading;
|
||||
* - modulepreload links reference only the entry chunk's static imports —
|
||||
* preloading lazy route chunks (preloadDynamicChunks: true) would fetch
|
||||
* the whole app at cold start and defeat route-level code splitting.
|
||||
* Returns a list of violation messages (empty on pass).
|
||||
*/
|
||||
function checkPreloadPosture() {
|
||||
const errors = [];
|
||||
let html;
|
||||
try {
|
||||
html = readFileSync(join(ROOT, 'dist', 'index.html'), 'utf8');
|
||||
} catch {
|
||||
errors.push('dist/index.html not found. Run "npx vite build" first.');
|
||||
return errors;
|
||||
}
|
||||
|
||||
const importmapMatch = html.match(/<script type="importmap">([\s\S]*?)<\/script>/);
|
||||
let integrity = null;
|
||||
if (!importmapMatch) {
|
||||
errors.push('importmap script block missing from index.html (dynamic-import SRI unenforced)');
|
||||
} else {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(importmapMatch[1]);
|
||||
} catch {
|
||||
errors.push('importmap block in index.html is not valid JSON');
|
||||
}
|
||||
if (parsed) {
|
||||
if (!parsed.integrity || typeof parsed.integrity !== 'object' || Object.keys(parsed.integrity).length === 0) {
|
||||
errors.push('importmap in index.html has no integrity block (dynamic-import SRI unenforced)');
|
||||
} else {
|
||||
integrity = parsed.integrity;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (integrity) {
|
||||
for (const file of readdirSync(DIST_ASSETS)) {
|
||||
if (!file.endsWith('.js')) continue;
|
||||
if (!(`/assets/${file}` in integrity)) {
|
||||
errors.push(`importmap integrity missing entry for /assets/${file}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const entryMatch = html.match(/<script type="module"[^>]*\bsrc="\/assets\/([^"]+\.js)"[^>]*>/);
|
||||
if (!entryMatch) {
|
||||
errors.push('module entry script not found in index.html');
|
||||
return errors;
|
||||
}
|
||||
const entryFile = entryMatch[1];
|
||||
if (!/\bintegrity="/.test(entryMatch[0])) {
|
||||
errors.push(`entry script /assets/${entryFile} missing integrity attribute`);
|
||||
}
|
||||
|
||||
let entrySource;
|
||||
try {
|
||||
entrySource = readFileSync(join(DIST_ASSETS, entryFile), 'utf8');
|
||||
} catch {
|
||||
errors.push(`entry chunk ${entryFile} not found in dist/assets/`);
|
||||
return errors;
|
||||
}
|
||||
|
||||
// Static imports/re-exports in Rollup output: import ... from"./x.js",
|
||||
// import"./x.js", export ... from"./x.js". Dynamic imports never match:
|
||||
// their specifier follows an opening parenthesis, not the keyword.
|
||||
const allowedPreloads = new Set([entryFile]);
|
||||
const staticImportRe = /\b(?:import|export)\s*(?:[\w$*{},\s]+?\s*from\s*)?["']\.\/([^"']+\.js)["']/g;
|
||||
let m;
|
||||
while ((m = staticImportRe.exec(entrySource))) allowedPreloads.add(m[1]);
|
||||
|
||||
for (const tag of html.match(/<link rel="modulepreload"[^>]*>/g) ?? []) {
|
||||
const href = tag.match(/\bhref="\/assets\/([^"]+)"/);
|
||||
if (!href) {
|
||||
errors.push(`modulepreload link with unrecognized href in index.html: ${tag}`);
|
||||
continue;
|
||||
}
|
||||
if (!allowedPreloads.has(href[1])) {
|
||||
errors.push(
|
||||
`modulepreload of lazy chunk /assets/${href[1]} — only the entry's static imports may be preloaded (keep preloadDynamicChunks: false in vite.config.ts)`,
|
||||
);
|
||||
}
|
||||
if (!/\bintegrity="/.test(tag)) {
|
||||
errors.push(`modulepreload of /assets/${href[1]} missing integrity attribute`);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Update baseline mode
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -266,11 +366,27 @@ function check() {
|
||||
|
||||
console.log();
|
||||
|
||||
if (violations.length === 0) {
|
||||
const postureErrors = checkPreloadPosture();
|
||||
console.log('index.html preload posture');
|
||||
console.log('==========================');
|
||||
if (postureErrors.length === 0) {
|
||||
console.log('ok: modulepreload limited to entry static imports; importmap integrity covers all JS assets.');
|
||||
} else {
|
||||
for (const message of postureErrors) {
|
||||
console.log(` ${message}`);
|
||||
}
|
||||
}
|
||||
console.log();
|
||||
|
||||
if (violations.length === 0 && postureErrors.length === 0) {
|
||||
console.log('All chunks within budget.');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log(`${violations.length} violation(s) found:\n`);
|
||||
}
|
||||
if (postureErrors.length > 0) {
|
||||
console.log(`${postureErrors.length} preload posture violation(s) found (listed above).`);
|
||||
}
|
||||
if (violations.length > 0) {
|
||||
console.log(`${violations.length} bundle size violation(s) found:\n`);
|
||||
for (const v of violations) {
|
||||
if (v.missing) {
|
||||
console.log(
|
||||
@@ -286,8 +402,8 @@ function check() {
|
||||
'\nTo update the baseline after an intentional size increase:',
|
||||
);
|
||||
console.log(' npx vite build && node scripts/check-bundle-size.mjs --update-baseline');
|
||||
process.exit(1);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -875,6 +875,42 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
|
||||
],
|
||||
)
|
||||
|
||||
def test_vite_config_change_uses_frontend_build_output_policy(self):
|
||||
required = infer_impacted_subsystems(["frontend-modern/vite.config.ts"])
|
||||
self.assertEqual(set(required), {"deployment-installability"})
|
||||
|
||||
installability = required["deployment-installability"]
|
||||
self.assertEqual(
|
||||
installability["contract"],
|
||||
"docs/release-control/v6/internal/subsystems/deployment-installability.md",
|
||||
)
|
||||
self.assertEqual(
|
||||
installability["touched_runtime_files"],
|
||||
["frontend-modern/vite.config.ts"],
|
||||
)
|
||||
self.assertEqual(
|
||||
installability["verification_requirements"],
|
||||
[
|
||||
{
|
||||
"id": "frontend-build-output",
|
||||
"label": "frontend production build output proof",
|
||||
"touched_runtime_files": ["frontend-modern/vite.config.ts"],
|
||||
"allow_same_subsystem_tests": False,
|
||||
"test_prefixes": [],
|
||||
"exact_files": [
|
||||
"frontend-modern/scripts/check-bundle-size.mjs",
|
||||
"scripts/release_control/ssh_host_key_policy_test.py",
|
||||
"scripts/tests/test-hot-dev-auth.sh",
|
||||
"scripts/tests/test-hot-dev-bg.sh",
|
||||
"scripts/tests/test-hot-dev-runtime.sh",
|
||||
"scripts/tests/test-toggle-mock.sh",
|
||||
"tests/integration/scripts/managed-local-backend.test.mjs",
|
||||
"tests/integration/tests/16-dev-runtime-recovery.spec.ts",
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
|
||||
def test_sensor_proxy_uninstall_change_uses_trust_policy(self):
|
||||
required = infer_impacted_subsystems(["scripts/uninstall-sensor-proxy.sh"])
|
||||
self.assertEqual(set(required), {"deployment-installability"})
|
||||
|
||||
Reference in New Issue
Block a user