Companion to #1769/#1771/#1772 — results of a systematic investigation into why literal {{ }} keeps reaching users despite 8 fixes (c571492, 4c5fb05, d411bf8, 409a2e0, bcd6f2c, da91c34, 8ebb172, 20509da).
Why it recurs
Preserved moustaches are the framework's de-facto error UI: any failure in the detect → extract → bundle → ship → rebind relay collapses into the same visible symptom. The preserve-vs-interpolate decision is re-derived at ~7 sites across 4 phases (expressions.ts, utils.ts, includes.ts, variable-extractor.ts, client-script-bundler.ts, signals.ts) — each regression fix patched one site's heuristic; no invariant anywhere asserts 'zero literal moustaches after hydration'.
The SPA whole-scope class (component renders all-dead: literal moustaches + stuck x-show)
Server-side there is NO divergence (fragments render through the same processDirectives, serve.ts:1544, extracted post-render :2294-2406). The failures are client-side lifecycle:
- D1 (prime suspect, stx-router fragment-swap path silently drops scope IIFEs on revisit — dedup check is missing the
isAlreadyScoped escape #1700 recurrence): doFragSwap script hash-dedup (packages/router/src/client.ts:401-415) skips a scope script on REVISIT — nav-away already disposed the scope (client.ts:339 → signals.ts:5118) — when the script fails the charAt(0) exemption sniff (client.ts:412). f348390 widened the sniff; it remains one emitted-shape change from regressing.
- D2:
if (!scopeVars) return in the stx:load walk (signals.ts:~5229) silently abandons the whole subtree.
- D3: container
__stx_scope guard skips processElement for the next fragment when not cleaned (signals.ts:~5246/5113).
- D4: one top-level throw in a shared emitted script unregisters all later components in it.
- D5: x-if-detached scope roots missed by cleanupContainer → stale
__mounted blocks remount.
Ranked hardening plan
- Post-hydration invariant sweep (end of stx:load + DOMContentLoaded): scan container for leftover
{{ text nodes and unregistered [data-stx-scope]; console.error with scopeId + force-fallback processElement. Converts every future upstream miss into one loud, self-healing event.
- Replace dedup heuristic with a contract: emit
data-stx-run="always|once" on generated scripts; key the client.ts skip off the attribute (extraction regex at client.ts:345 must keep attributes).
- Make the three silent skips loud (signals.ts ~5229, ~5246; eval catch ~1670 — log once per expression).
- Per-component registration isolation (own try/catch or own script element per component in client-script-bundler.ts) — kills D4.
- Single preservation gate: compute preserve-vs-interpolate once, stamp it, have all seven sites consume it.
App-side, ts-analytics now runs a revisit-inclusive WebView sweep + a dev canary asserting the post-hydration invariant per navigation — happy to share either as a reference implementation for (1).
Companion to #1769/#1771/#1772 — results of a systematic investigation into why literal
{{ }}keeps reaching users despite 8 fixes (c571492, 4c5fb05, d411bf8, 409a2e0, bcd6f2c, da91c34, 8ebb172, 20509da).Why it recurs
Preserved moustaches are the framework's de-facto error UI: any failure in the detect → extract → bundle → ship → rebind relay collapses into the same visible symptom. The preserve-vs-interpolate decision is re-derived at ~7 sites across 4 phases (expressions.ts, utils.ts, includes.ts, variable-extractor.ts, client-script-bundler.ts, signals.ts) — each regression fix patched one site's heuristic; no invariant anywhere asserts 'zero literal moustaches after hydration'.
The SPA whole-scope class (component renders all-dead: literal moustaches + stuck x-show)
Server-side there is NO divergence (fragments render through the same processDirectives, serve.ts:1544, extracted post-render :2294-2406). The failures are client-side lifecycle:
isAlreadyScopedescape #1700 recurrence): doFragSwap script hash-dedup (packages/router/src/client.ts:401-415) skips a scope script on REVISIT — nav-away already disposed the scope (client.ts:339 → signals.ts:5118) — when the script fails thecharAt(0)exemption sniff (client.ts:412). f348390 widened the sniff; it remains one emitted-shape change from regressing.if (!scopeVars) returnin the stx:load walk (signals.ts:~5229) silently abandons the whole subtree.__stx_scopeguard skips processElement for the next fragment when not cleaned (signals.ts:~5246/5113).__mountedblocks remount.Ranked hardening plan
{{text nodes and unregistered[data-stx-scope]; console.error with scopeId + force-fallback processElement. Converts every future upstream miss into one loud, self-healing event.data-stx-run="always|once"on generated scripts; key the client.ts skip off the attribute (extraction regex at client.ts:345 must keep attributes).App-side, ts-analytics now runs a revisit-inclusive WebView sweep + a dev canary asserting the post-hydration invariant per navigation — happy to share either as a reference implementation for (1).