A Play Store release shipped an AAB whose index.html declared globalThis.__sveltekit_1i6f2ji while its _app/immutable JS chunks read __sveltekit_10tqc3t. kit.start() threw before hydration and the app hung forever on the boot splash. Root cause: two concurrent vite builds in the same checkout (a release build and a peer agent's bare npm run build) interleaved writes to the shared build/ + .svelte-kit/ output dirs. Each build exits 0; the corruption is only visible in the assembled artifact.
Transferable findings:
Detection signature. SvelteKit embeds a per-build version token
__sveltekit_<hash>(fromkit.version.name, defaultDate.now()) in bothindex.htmland client chunks. A healthy bundle has exactly one distinct token acrossindex.html+_app/immutable/**/*.js; a mixed-source build has two.grep -ohE '__sveltekit_[a-z0-9]+' ... | sort -u | wc -lmust print 1. (Exclude server-side chunks:__sveltekit_swis a static service-worker-env literal in kit's server code, a false positive.)Verify the zip that ships, not the directory. Build dirs are TOCTOU-broken — another build can overwrite them minutes later. Integrity-check inside the final APK/AAB with stdlib
zipfile(AAB prefixbase/assets/public/, APK prefixassets/public/): (a) exactly one__sveltekit_*token in index.html and no divergent token in any_app/immutable/**/*.js; (b) every(?:src|href)="/(_app/[^"]+)"ref present in the zip; (c)_app/version.jsonversion== the git commit baked at build time (catches a fully self-consistent foreign build swapped in whole, which token equality alone passes).Serialize builds with a ~60-line vite plugin, zero deps. Lock file acquired via
fs.writeFileSync(path, json, {flag:'wx'})in an asyncbuildStarthook (apply:'build'so dev is untouched). Critical detail: SvelteKit runs SSR + client sub-builds in one process andbuildStart/closeBundlefire per sub-build, so the holder check must be re-entrant (holder.pid === process.pid→ proceed). Stale detection:process.kill(pid, 0)→ ESRCH means dead holder, unlink and retry; unparseable lock JSON is also stale. Release incloseBundle+exit/SIGINT/SIGTERMhandlers, unlinking only when the lock's pid is your own. Poll 2s, log holder every 30s, hard timeout with an actionable message.Locks don't stop mid-build source edits. Complement with a clean-tree gate on release commands:
git status --porcelain -unosnapshot before the build (fail hard,--allow-dirtyescape hatch, checked before any interactive credential prompt), re-check after the build excluding the version file the release itself bumps. HEAD moved or tracked files changed → artifact provenance unknown, refuse.Make the failure visible on device. A corrupt bundle means no framework code runs, so any hydration-dependent error UI is dead. A classic inline
<script>inapp.html(outside the module graph) with a 10ssetTimeoutthat checks ahydratedbody class set in root layoutonMount, and rewrites the boot overlay to an error +location.reload()Retry button, works even when every chunk 404s.