# Runtime manifest hygiene Based on main/offline-fix tip `0b8e398`; offline-fix was already merged when this worktree was created. Implementation: `dd09f66`. No frontend runtime, loading, service-worker, playback or layout code changed. ## Policy and delivery `server/asset-manifest.js` now selects runtime inputs separately from delivery metadata. It excludes `*.test.js`, `*.test.mjs`, `*.md`, `*.txt`, `*.c`, `*.entry.js`, `/admin.html`, and all files beneath a `perf/` directory. The optional `assets.json` `include` array accepts exact root-relative existing file paths to override this policy. An explicitly declared group cannot silently lose an excluded dependency: boot rejects it unless the path is included. The 49 excluded files here are 44 unit tests, admin HTML, the Plus Jakarta Sans font licence, two WASM C build sources, and the Framework7 source build entry. WASM binaries, fonts, icons, workers and the Framework7 runtime bundle remain. Excluded files remain directly servable, with the existing content hash, ETag, compression and cache rules: plain requests revalidate (font-folder requests keep their 30-day cache rule), matching `?v` is immutable, and stale `?v` is no-store in per-file mode. Delivery hashes are computed separately; they never enter `manifest.files` or build identity. `ASSET_HASHING=0` retains single-tag stamping and its legacy header matrix, but its tag also uses only runtime inputs. `ASSET_SYNC=0` still selects the legacy worker. Fresh installs cache only runtime assets. On an incremental upgrade, excluded entries from the previous manifest may survive for one build under existing N-1 retention, then are pruned as N-2. No new excluded entries are downloaded. This preserves retention for already-open pages instead of changing worker pruning or contracts/pinning. ## Inventory Measured from the untouched before snapshot and the fixed frontend using the respective real server manifest modules. Counts include stamped index and SW. | Metric | Before | After | Reduction | | --- | ---: | ---: | ---: | | Manifest files | 151 | 102 | 49 (32.5%) | | Summed manifest file sizes | 2,044,337 B | 1,807,059 B | 237,278 B | | JSON manifest body | 15,601 B | 12,551 B | 3,050 B | Excluded source bodies total 237,278 B, or 64,694 B using per-file Brotli compression. Actual install/update measurements below include protocol and manifest/index effects; compression estimates are not substituted for those runs. Full inventory: `perf/results/hygiene-assets.json`. ## Verification Commands: ```sh node --test frontend/*.test.js cd server && bun install && bun run test node perf/offline-complete.mjs --browser all --conditions --out perf/results/hygiene-offline.json node perf/migration.mjs --browser all --profile lossy --out perf/results/hygiene-migration.json node perf/offline-complete.mjs --browser all --legacy --out perf/results/hygiene-rollback-offline.json ``` The server test command used a worktree-local Python venv on PATH with yt-dlp installed so both environment-sensitive worker tests ran successfully. Frontend: 209 passed, zero failures. Server: 186 passed, zero failures. Manifest/static/shell tests cover excluded-file identity invariance in both hash modes, app.js identity changes, explicit inclusion, group validation, direct bodies/hash/header behavior, SPA delivery and rollback flags. Offline completeness: Chromium and WebKit each report manifest count 102, missing `[]`, failures `[]`, across all four layouts and lazy features. Page closure/interrupted completion, worker restart/re-registration, Save-Data, two tabs and online-event resumption pass. Linux WebKit uses same-context worker re-registration because persistent CacheStorage restart is broken there; its offline proxy blocks transport because native setOffline breaks cached navigation. Native OPFS-dependent video-edit behavior cannot be exercised in Linux WebKit. Lossy legacy migration passes on both engines: one banner, one explicit-user reload, offline launch, playback guard, legacy URLs, eviction repair and incremental refresh. WebKit records four tolerated offline API transport errors, with the offline assertions passing. Migration caches include retained N-1 entries, so their raw key count is deliberately greater than manifest count. ## Rollback limitation found during extra verification An additional `--legacy` offline-completeness run disables both hashing and sync. It fails on both the untouched base and fixed build: the legacy SHELL omits `piano-engine.mjs`, so its direct offline request returns 503 across layouts. The legacy shell also intentionally does not cache its own `sw.js`; the harness therefore marks that inventory entry missing too, without a corresponding offline UI failure. Before: 151 manifest entries / 51 missing; after: 102 / 2 missing. The other 49 before-only misses are precisely the excluded non-runtime files. Normal sync-mode completeness is 102/102 with no failures. Header/injection rollback tests pass; this change neither fixes nor worsens the existing legacy piano coverage gap. Results: `hygiene-before-rollback-offline.json` and `hygiene-rollback-offline.json`. ## Reduced performance measurements Three runs per scenario/engine, LTE. Before used an isolated untouched snapshot of `0b8e398` inside this worktree; after used the fixed server. No external worktree was modified. Cold wire totals include page requests and install/completion fetches; these are not claimed to be worker-only bytes. Chromium uses CDP LTE throttling; WebKit uses the LTE proxy. ```sh node perf/.tmp/before/perf/baseline.mjs --runs 3 --browser all --profile lte --scenario all --out perf/results/hygiene-before.json node perf/baseline.mjs --runs 3 --browser all --profile lte --scenario all --out perf/results/hygiene-after.json ``` All values below are actual medians; bytes are wire bytes. | Engine | Scenario | Before B | After B | Before ms | After ms | | --- | --- | ---: | ---: | ---: | ---: | | chromium | Cold visit + install | 728,266 | 660,390 | 7101 | 7090 | | chromium | Warm reload | 8,132 | 8,159 | 647 | 445 | | chromium | Offline reload | 0 | 0 | 285 | 209 | | chromium | Core app.js edit | 126,358 | 125,729 | 1008 | 753 | | chromium | theme-glass.css edit | 44,027 | 43,434 | 1600 | 1372 | | chromium | Presenter edit + idle fill | 42,065 | 41,436 | 1392 | 1376 | | webkit | Cold visit + install | 1,189,813 | 1,111,241 | 2321 | 2310 | | webkit | Warm reload | 0 | 0 | 92 | 89 | | webkit | Offline reload | 0 | 0 | 100 | 88 | | webkit | Core app.js edit | 148,709 | 147,955 | 2013 | 2013 | | webkit | theme-glass.css edit | 57,732 | 56,976 | 1479 | 1466 | | webkit | Presenter edit + idle fill | 55,382 | 54,625 | 1494 | 1557 | Cold requests fall 165 → 116 (Chromium) and 209 → 160 (WebKit), exactly 49 fewer each. Cold wire savings are 67,876 B (9.3%) and 78,572 B (6.6%). Ordinary runtime updates already shared unchanged files before this fix, so their savings are mainly smaller manifest metadata: approximately 0.6–0.8 KB. Test-only edits now leave the tag unchanged and trigger no build update. Milliseconds for cold/warm/offline are boot-done; update milliseconds are completion duration. These three-run timing differences are descriptive, not a performance claim. Warm Chromium API traffic varies independently of the asset set. Real output excerpts (after): ```text run 3/3... done (660390 bytes, 116 reqs, boot=7090ms) run 3/3... done (1111241 bytes, 160 reqs, boot=2310ms) run 3/3... done (147955 bytes, 5 reqs, dur=2014ms) run 3/3... done (56976 bytes, 9 reqs, dur=1483ms) ``` ## Real iPhone checks Confirm installed-PWA offline launch and feature/layout first use after update; completion after suspension, storage pressure/OPFS sharing, and old excluded cache entries disappearing after the subsequent build. Linux automation cannot prove iOS worker lifetime, quota eviction or native OPFS behavior.