Repository navigation
feat(router): cached-scope header guard — only middleware writes headers on ppr routes (#713) - #725
Merged
Merged
Conversation
commit: |
…ers on ppr routes; handlers throw in ppr and cache() scopes (closes #713) Doctrine (six-framework survey, recorded on #713): ppr = document-scoped cache(), and no framework in the ecosystem replays page-code headers from a document cache entry — Next app pages expose no mutable API (cookies().set() throws), SvelteKit/Astro silently drop (self-admitted warts), RR7 errors at build when no live lane exists. Replay is ecosystem-normal only for raw-Response endpoint surfaces, where set-cookie is an open wound (Nitro session-fixation issue #3468). Our prior behavior was the SvelteKit/Astro shape: handler headers rode MISSes and silently vanished on HITs — the divergence class behind a production session bug. This PR makes it deterministic and loud. One mechanism: a cachedHeaderScope latch on the request context ALS, set by the four segment-resolution funnels when the entry chain declares ppr (document-scoped, via the isPprEntry() predicate now shared with resolvePprConfig — guard and serve cannot drift) and by the positional cache() latch. Guarded surfaces: the ctx.headers proxy and the reqCtx header/setCookie/deleteCookie/setStatus/onResponse wrappers; one message family naming route + layer + the fix (route middleware). The funnels run inside the middleware chain's next(), so middleware writes structurally cannot trip the guard. Physics-shaped, not blanket: loader writes inside PLAIN cache() stay ALLOWED — a pinned consumer contract (cache-scope-guard e2e; demo shop cart loaders) and the commit order permits them. Under ppr the stored-prelude flush commits headers at TTFB before any loader settles (dead letters on every HIT), so ppr loader writes throw. Handlers throw in both cached scopes on every render — deterministic first-MISS failure, dev and prod. Middleware pinned as THE header lane: custom header + Set-Cookie asserted byte-equal on MISS and HIT in both apps, plus an action POST -> GET(HIT) session-cookie continuity test (the storefront basket shape). No replay machinery, no derived headers, no exclusion taxonomy (rejected design recorded on the issue). Red->green: guard e2es fail 4/4 on main (handler header + loader cookie reach the response), pass with the guard. Migration sweep: zero in-repo call sites needed changes; the nonce-via-middleware fixture is the positive control. Batteries: guard suites 6+5 / 5+5 (both apps), shell battery 48 dev / 47 prod, semantic-matrix 21+24, cache-scope-guard 16+14, cloudflare ppr 32+32, demo shop 3+3, 14 new unit; gate green. /simplify applied. External simplify pass (applied on review): the guard now sits at ONE choke point — a guarding Headers proxy shadowed onto the stub Response instance — instead of enumerated per-method wrappers. This closed two real holes: ctx.setTheme (its Set-Cookie append was unguarded — a ppr-route handler could write a theme cookie that vanished on HITs) and the raw ctx.res.headers escape; setStatus/onResponse stay enumerated, internal serve writers exempt via the raw Headers. The ppr latch now checks the LEAF route entry only (same predicate AND input as resolvePprConfig's serve path); the intercept funnels gained defense-in-depth latches (ppr+intercepts don't compose on the shell path — full matches skip intercepts). New pins: setTheme red->green unit pair, raw-res-escape unit, leaf-parity unit, serve-drain-exempt unit (19 unit total), and a /mw-post-next e2e (dev+prod) asserting post-next() middleware header + cookie land on MISS and HIT (the latch dies with the funnel scope; runWithStore never copies it — now annotated). Deleted the browser EQUIVALENCE e2e (verbatim re-assert of the unit pin). Re-gated: batteries 69+1f dev / 67+1f prod (flake = shell-cache bake-lane locator race, retried green, passes 5x standalone on base), semantic-matrix 21+24, cloudflare 37+37, demo shop 3+3, guard suite 6+6.
ivogt
force-pushed
the
feat/cached-header-guard
branch
from
July 7, 2026 09:57
0a8ab7a to
e2448eb
Compare
ivogt
added a commit
that referenced
this pull request
Jul 7, 2026
…ites (closes #725 gap) The cached-scope header guard (#725) exempted every loader body under a cache() boundary via isInsideAnyLoaderScope(). That let a handler-invoked loader — await ctx.use(Loader) from a handler, never registered with loader() — write a cookie/header on the MISS. On a HIT the handler is skipped and only DSL-registered loaders re-run, so that loader never executes and its write silently vanishes — the exact MISS/HIT divergence #725 was built to eliminate. Narrow the exemption to isInsideLoaderScope() (DSL scope only): registered loaders re-run on every HIT and merge into every response, so their writes stay allowed; a handler-invoked loader now throws deterministically like a handler write. The DSL scope ALS survives nested ctx.use bodies, so a handler-invoked loader nested under a DSL loader stays exempt (its parent re-runs it every HIT). The broad predicate is deferred to the throw path for the error label so the PPR handler-invoked-loader message stays "loader", and the throw reason keys on scope.kind so the cache() case explains the handler-skip cause. Re-ground the neighbouring read-purity guard comment, which this change makes contradictory: reads stay on the broad predicate under the consumption-lane baked-copy tradeoff (#672/#674), only writes narrow to DSL scope. Unit (cached-header-guard.test.ts): DSL-allowed pin, handler-invoked-throws pin (red before this change, green after), nested-under-DSL-allowed pin. E2e (cache-scope-guard, dev + production): /handler-loader-cookie-blocked asserts the error boundary renders and the cookie never reaches the wire; the DSL /loader-cookie-allowed control stays green in both modes.
ivogt
added a commit
that referenced
this pull request
Jul 8, 2026
…ites (#725 gap) + drop removed __debug_manifest docs (#730) * fix(router): handler-invoked loaders under cache() throw on header writes (closes #725 gap) The cached-scope header guard (#725) exempted every loader body under a cache() boundary via isInsideAnyLoaderScope(). That let a handler-invoked loader — await ctx.use(Loader) from a handler, never registered with loader() — write a cookie/header on the MISS. On a HIT the handler is skipped and only DSL-registered loaders re-run, so that loader never executes and its write silently vanishes — the exact MISS/HIT divergence #725 was built to eliminate. Narrow the exemption to isInsideLoaderScope() (DSL scope only): registered loaders re-run on every HIT and merge into every response, so their writes stay allowed; a handler-invoked loader now throws deterministically like a handler write. The DSL scope ALS survives nested ctx.use bodies, so a handler-invoked loader nested under a DSL loader stays exempt (its parent re-runs it every HIT). The broad predicate is deferred to the throw path for the error label so the PPR handler-invoked-loader message stays "loader", and the throw reason keys on scope.kind so the cache() case explains the handler-skip cause. Re-ground the neighbouring read-purity guard comment, which this change makes contradictory: reads stay on the broad predicate under the consumption-lane baked-copy tradeoff (#672/#674), only writes narrow to DSL scope. Unit (cached-header-guard.test.ts): DSL-allowed pin, handler-invoked-throws pin (red before this change, green after), nested-under-DSL-allowed pin. E2e (cache-scope-guard, dev + production): /handler-loader-cookie-blocked asserts the error boundary renders and the cookie never reaches the wire; the DSL /loader-cookie-allowed control stays green in both modes. * docs: drop the removed __debug_manifest browser endpoint from the skill and guide #728 deleted the ?__debug_manifest HTTP endpoint and its routeAncestry data, but the debug-manifest skill and docs guide still told users to open http://localhost:PORT/?__debug_manifest and documented the deleted { routerId, routeManifest, routeAncestry, routeTrie, precomputedEntries } shape. Remove both browser-endpoint sections; both files already carry the correct router.debugManifest() programmatic path ({ routes, layouts, totalRoutes, totalLayouts }), now the primary documented route.
ivogt
added a commit
that referenced
this pull request
Jul 8, 2026
… the header doctrine (#735) (#736) The cached-scope header guard (#725) forbids handler header/cookie writes on ppr routes because they ride MISSes and vanish on HITs — non- deterministic. But ctx.dynamic() (#734) opts the render off the shell axis: it never HITs, every request is live, so its header writes ARE deterministic. The guard's reason to forbid them is gone. ctx.dynamic() now clears the cached-header latch (server/context.ts clearPprHeaderScope, called from request-context.ts dynamic() after _dynamic is set). Ordering: a write BEFORE dynamic() still throws (latch live); writes AFTER pass. This completes the #726 design — #734 shipped the shell-opt-out half, this is the header-re-permit half. Composition hardening (surfaced by the altitude simplify pass, closed not shipped): a ppr route nested UNDER a cache() boundary runs the handler with insideCacheScope true; a cache() HIT still skips it, so a cleared latch would drop the write on that HIT. clearPprHeaderScope therefore UNMASKS to kind:"cache" when insideCacheScope is active instead of clearing — ppr writes re-permitted, cache()-scope writes stay guarded. A plain cache() boundary entered after dynamic() likewise re-latches "cache". Middleware dynamic() is a no-op (runs outside the funnel scope, already exempt); loaders unchanged (no dynamic() on their surface). Consumer payoff — the SFRA-proxy lane collapses to one causal chain: ctx.dynamic(); // off the shell axis ctx.headers.set("x-rango-sfra-proxy", reason); // now legal, always live retiring the context-var + post-next-middleware relay the workaround needed. Tests: unit clearPprHeaderScope (clear-ppr, write-before-throws, not- cache, cache-after-clear re-latch, nested-under-cache unmask, no-op) + guarded-surfaces dynamic() re-permit; userland renderHandler dogfood (the markSfraProxy collapse); e2e both apps dev+prod (a dynamic()+header route: header on every request, never x-rango-shell HIT). Doctrine table gains the re-permitted row (ppr-shell-resume.md + ppr skill). Batteries: router unit 5195, e2e 44/44 dev+prod, cloudflare 35/35; simplify applied. Closes #735.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(ppr): the pure header guard — handler/loader
ctx.headerswrites throw in cached scopes; middleware is THE header lane (#713)Problem statement
A handler on a
pprroute could callctx.headers.set()/cookies().set()and see it work — on the MISS. On every shell HIT the handler is replayed from the cached shell and never runs, so the header silently vanished: two visitors to the same URL got different response headers depending on cache state, with no error anywhere (field evidence: a storefront session bug caused by exactly this MISS/HIT cookie divergence). Loader writes were worse-by-physics: shell-first streaming flushes the response headers at TTFB before any loader settles, so loader header writes are dead letters on HITs. The equivalent hazard inside acache()boundary already threw; the ppr surface did not.Doctrine (final spec on #713, post six-framework survey):
ppris a document-scopedcache()— in any cached scenario, ONLY MIDDLEWARE writes headers. No replay machinery, no derived headers, no exclusion taxonomy.Before vs. after
Before (silent divergence):
After (deterministic first-render error, dev AND prod):
The fix (the live lane):
End-to-end consumer example (the storefront basket)
Add-to-basket POST → the next GET serves the cached shell (
x-rango-shell: HIT) and the middleware still reads the rotated cookie and emitsx-basket-count: 1— session continuity through the cache, pinned by e2e in the cloudflare app.Semantics table
cache()next)ctx.headers.*header/setCookie/setStatus(reqCtx lane)cookies().set, reqCtx lane)cookies().setx-rango-shell,content-type, state cookie)The one deliberate deviation from the literal spec text ("handler AND loader writes throw in ppr and cache() scopes"): loader writes inside a plain
cache()boundary stay allowed. They are a pinned, working consumer contract —cache-scope-guard.test.ts"loader calling cookies().set() inside cache() should be allowed" (dev + production) and vite-rsc-demo's shop cart (getOrCreateCartIdfromCartLoader/ProductCartLoaderinside the shopcache();shop-cache-scope.test.tsasserts it must not throw). The dead-letter physics does not apply there: those loaders re-run on every request including cache hits and their writes merge into each response — no MISS/HIT divergence exists to guard against. On ppr the physics does apply (headers flush with the shell prelude before loaders settle), so ppr loader writes throw. Both suites re-run green.Code-change summary
One guard, one latch field, six latch sites, ONE guarded mutation surface (a choke point, not enumerated wrappers) — no new files in
src/outside tests.src/server/context.ts— the whole mechanism:CachedHeaderScopetype +cachedHeaderScopefield on the RangoContext ALS store (runWithStore deliberately never copies it — the middleware exemption depends on the latch dying with the funnel scope, now annotated at the copy list);latchCachedHeaderScope(scope)(first latch wins — a nestedcache()never downgrades a ppr scope);latchPprHeaderScopeForEntries(entries, routeKey)— checks the LEAF route entry (entries[entries.length - 1], themanifestEntry) with the sharedisPprEntrypredicate: guard and serve (resolvePprConfig(manifestEntry)) now share predicate AND input, so they cannot drift (apprdeclaration on a non-leaf ancestor neither shell-serves nor latches). Still guards on declaration, so a missing shell store or gating nonce doesn't turn the error off;assertCachedHeaderWriteAllowed(surface)— the unified check + one message family naming surface, layer (handler/loader via the loader ALS scopes), route, and the fix.kind: "cache"+ loader returns early (the pinned exemption).src/server/request-context.ts— the choke point: the stubResponse's Headers are only reachable through a guarding proxy shadowed onto the instance (Object.defineProperty(res, "headers", ...)— the stub stays a REAL Response, no facade), whoseset/append/deleteconsult the guard first. This coversctx.header/setCookie/deleteCookie(their per-method asserts are deleted),cookies().set(),ctx.setTheme(a real hole in the previous revision — its Set-Cookie append was unguarded), the handlerctx.headersproxy, and rawctx.res.headers.set(...)(the bypass documented in the previous revision's notes — now closed).setStatus/onResponseare not Headers mutations and stay enumerated. Internal serve writers (_rotateStateCookie,_setKeepCacheDirective) write to the raw Headers —invalidateClientCache()/keepClientCache()are documented callable from loaders and during capture.src/router/segment-resolution/fresh.ts— ppr latch at the top ofresolveAllSegments(document-scoped: the whole chain, root layout included, bakes into the shell — unlike the positionalcache()latch) andresolveLoadersOnly(loaders on shell-HIT tails / cached serves); the existing in-loopcache()latch also setscachedHeaderScopeso the message can name the route.src/router/segment-resolution/revalidation.ts— same two latches for the partial/action funnels (resolveAllSegmentsWithRevalidation,resolveLoadersOnlyWithRevalidation) — "on EVERY render" includes client navigations and action re-renders.src/router/intercept-resolution.ts— defense-in-depth latches inresolveInterceptEntry(after intercept middleware — middleware stays the live lane) andresolveInterceptLoadersOnly, keyed off the intercepted TARGET route's manifest entry. Reachability: ppr and intercepts do not compose on the shell path (shells are captured/served only for document requests;withInterceptResolutionreturns early onisFullMatch), but a partial nav CAN render an intercept over a ppr-declared target route in its own store scope where the main funnel's latch does not apply — these latches close that seam by declaration.src/router/handler-context.ts— thectx.headersproxy's mutating methods call the unified assert (kept for the precisectx.headers.set()message and the no-request-context fallback); the"use cache"exec stamp check is unchanged. Writes flow through to the request context's guarded stub headers underneath.Why this seam: the funnels are where handler/loader execution begins and they run strictly INSIDE the middleware chain's
next(), so middleware pre-nextwrites precede the latch and post-nextwrites execute outside the resolution ALS scope — middleware can never trip the guard, by construction rather than by exclusion list. The fast-path HIT replays handlers without running them, so the guard's job on HITs is the loader funnels + the deterministic failure already thrown on the very first MISS render.Timing note (same shape as the existing
cache()guard): the latch is per-ALS-resolution-scope and timing-based, not layer-tagged — a post-next()middleware write through the reqCtx lane cannot trip it because the resolution scope has exited by then.Test plan
src/files reverted to main, the new App-A guard e2es fail 4/4 — the route renders happily,x-phg-handler: should-never-applyreaches the response, andphg-loader=should-never-applyappears in Set-Cookie (the exact silent hazard); with the guard they pass. Output captured in the session.src/rsc/__tests__/cached-header-guard.test.ts, 19 tests): the seam (both kinds x both layers, first-latch-wins, no-latch), the latch predicate (ppr: true/{ttl}latch;false/undefined/layout don't; leaf-input parity — ppr on a non-leaf ancestor does not latch), the guarded surfaces (handler proxy set/append/delete + reqCtx header/setCookie/deleteCookie/setStatus; setTheme under ppr throws / outside cached scopes works — red→green: both new tests fail with the choke point reverted, 2 failed | 17 passed; rawctx.res.headersmutations throw, reads stay open; the serve-path drain stays exempt after the latched scope exits), class+message-family equivalence (ppr error ≡ cache() error, oneFAMILY_RE), the cache()-loader exemption, and middleware merge on the HIT serve path (createResponseWithMergedHeaders: serve-owned headers win, middleware header merges, Set-Cookie appends, stub drained).packages/rangojs-router/e2e/ppr-header-guard.test.ts, dev 6 + production 6): handler write → error boundary on first AND second GET (deterministic, no poisoned shell —x-rango-shellnever HIT); loader write → error with the loader wording; writes never reach the response; middleware header + cookie on MISS and HIT (static value byte-equal, per-request value advancing across HITs = live, not replayed); NEW/mw-post-next: route middleware writing header +cookies().set()AFTERawait next()on a ppr route lands on MISS and HIT — pins the middleware exemption's mechanism (the latch dies with the funnel scope). The dev-only browser EQUIVALENCE test was deleted — it re-asserted the unit-level EQUIVALENCE pin verbatim.tests/cloudflare-basic/e2e/ppr-header-guard.test.ts, dev 5 + production 5, workerd/KV): same guard + middleware-live pins, plus basket continuity: action POST rotates the cookie → GET is a shell HIT and middleware emitsx-cf-basket-count: 1, then2after a second action.shell-cache shell-secure shell-cache-action shell-capture-timeout prerender-ppr— dev 48 passed; production 47 passed + 1 flaky (shell-capture-timeoutppr capture budget must be configurable — deferred shell material (async Meta / top-level handle pushes) beyond 5s makes a route uncapturable #715 negative, ECONNRESET on a raw GET against the isolated preview server, passed on retry; the test exercises capture-budget refusal, no header writes anywhere near it). cloudflare-basicppr-shell ppr-tag-eviction ppr-capture-timeout— dev 32, production 32.semantic-matrix(segment-resolution touched): dev 21, production 24.cache-scope-guarddev 16 + production 14 (message change compatible — the family keeps the "cache() boundary" fragment). vite-rsc-demoshop-cache-scopedev 3 + production 3 (the pinned loader-cookie contract).pnpm run typecheck(16/16),pnpm run test:unit:all(all packages, 0 failures),pnpm run lint(0 warnings),pnpm run format(clean),pnpm check:e2e-bucketing(OK).*.gen.tsregenerated and included for both apps that gained routes (test-app, cloudflare-basic).Consumer migration note
Handler
ctx.headers.set/cookies().set/setStatuson a cached route (apprroute or insidecache()) is a runtime error after this — deterministic on the first render, dev and prod. Move the write into route middleware (e.g. the storefront PDP'sCache-Control/x-page-typelines), or delete it: ans-maxageset from a holed ppr document was already edge-freezing per-request material on MISSes. Noteppr.ttl/swrgoverns the worker-internal shell entry, never HTTP cacheability — ppr responses are dynamic by design. Repo sweep found ZERO in-repo migrations needed: no fixture/example sets handler headers on a ppr or cache() route (the only such routes are the guard-test fixtures themselves); the nonce route (middleware-setctx.set(nonce, ...)on a ppr route) is untouched and green as the positive control.Notes
isPprEntry(entry)as the ONE ppr opt-in predicate shared byresolvePprConfigand the latch (kills the hand-synced mirror);latchCachedHeaderScopetakes scalars (no discarded allocation on the already-latched path);assertCachedHeaderWriteAllowedbuilds the surface string lazily on the throw path only (the pre-diff code allocated nothing on the success path — restored); extracted privateisInsideAnyLoaderScope()(one home for the loader-exemption predicate, also used byisInsideCacheScope); renamed the request-context wrapper toassertResponseWriteAllowed(old name said cache-scope-only; it now also guards ppr) — it additionally guardsctx.onResponse(), which was already on the same guarded lane; dropped a deaddata-testidin the cloudflare fixture. Skips: per-filewarmToHit/HTML_HEADERScopies are the documented house convention (8+ suites); the funnel-top latch seam was verified as the right depth (full-render and loader-only paths never share a post-middleware node); the request-context wrapper was kept (5 call sites) rather than inlined.ctx.resis closed: the stubResponse'sheadersproperty is shadowed with the guarding proxy on the instance itself, so the raw getter-then-mutate escape (ctx.res.headers.set(...)) hits the same guard as every other surface. The stub remains a realResponse(no facade proxy), so platform hand-offs are unaffected.pprin the path options), not on cache-store availability: a ppr route gated to axis 1 by a missing shell family or an active nonce still throws, keeping dev(memory store) → prod(KV) behavior identical.packages/rangojs-router/docs/design/ppr-shell-resume.md("The header doctrine"); constraint-table row updated;docs/internal/feature-map.mdgained the doctrine bullet;skills/ppr/SKILL.md"hard edges" now leads with the header rule.headers()use-item on the live lane, if a request-dependent need materializes.Simplify review (external pass)
A second, external consolidation pass over the PR diff. Applied findings (re-gated green: full typecheck,
test:unit:all, lint, format, and the dev+production e2e batteries below):ctx.setThemewas a real hole — fixed at the choke point, not with a sixth wrapper.setThemeappended its theme cookie to the stub headers with only the"use cache"taint check, so a ppr-route handler calling it wrote a cookie that silently vanished on HITs — the exact divergence this PR forbids. Instead of enumerating another wrapper, the stubResponse's Headers mutation surface itself is now guarded: a proxy whoseset/append/deleteconsultassertCachedHeaderWriteAllowed("response headers", method)is shadowed onto the stub instance (Object.defineProperty— the stub stays a realResponse). This automatically coversctx.header/setCookie/deleteCookie(their per-method asserts were deleted),cookies().set(),setTheme, the handlerctx.headersproxy, and the rawctx.res.headersescape (bypass note above rewritten to "closed").setStatus/onResponsestay enumerated (not Headers writes); internal serve writers (_rotateStateCookie,_setKeepCacheDirective) use the raw Headers (documented loader/capture-callable). Every legitimate stub mutation site was audited for exemption: the serve/commit drain (rsc/helpers.ts:130/:306) and post-next()middleware writes run outside latched funnel scopes (guard no-ops), intercept middleware runs before the intercept latch, and the theme/nonce middleware fixtures write from middleware (never latched). Red→green: with the choke point reverted, the two new unit tests fail (2 failed | 17 passed); with it, 19/19.withInterceptResolutionreturns early onisFullMatch, so intercept content can never bake into a shell. But a partial nav CAN render an intercept over a ppr-declared target route, in its ownStore.runscope where the main funnel's latch does not apply —resolveInterceptEntry(after intercept middleware) andresolveInterceptLoadersOnlynow latch off the target route's manifest entry, closing that seam by declaration.runWithStore's field-copy list now carries the annotation thatcachedHeaderScopeis deliberately NOT copied — the exemption depends on the latch dying with the funnel scope. (b) New e2e (/ppr-header-guard/mw-post-next, dev + production): route middleware doingawait next(); cookies().set(...); ctx.header(...)on a ppr route — both writes land on MISS and HIT.latchPprHeaderScopeForEntriesno longer scans the whole entry chain per request; it checks the LEAF route entry (entries[entries.length - 1]— verified equal to the snapshot'smanifestEntry, the exact inputresolvePprConfiggets on the serve path). Kills the latent drift where apprdeclaration on a non-leaf ancestor latched the guard but never shell-served. Unit test pins the parity.cachedHeaderScopefield JSDoc and the guard's "Layer rules" block inserver/context.tsare now RULE + one-line pointer todocs/design/ppr-shell-resume.md"The header doctrine" (which gained a "Mechanics" subsection as the single home). Runtime error strings unchanged in shape; the reqCtx-lane surface string is nowresponse headers.<method>()(same family — tests pin the family, not surfaces).packages/rangojs-router/e2e/ppr-header-guard.test.tswas a pure re-assertion of the unit EQUIVALENCE test (cached-header-guard.test.ts) — removed.Skipped (with rationale):
isPprValue()extraction for the two non-EntryData.pprchecks — different layers and shapes; forcing one predicate across them couples unrelated types.warmToHit/routeEntry/FAMILY_REtest-helper duplication — pre-existing per-file convention (~7 copies predate this PR).isPprEntryto a leaf module — cohesion polish with import-cycle risk; not worth the churn.mw-post-nexttwin — the mechanism under test (ALS store scope lifetime) is runtime-agnostic and the rangojs app pins it in both modes; cloudflare keeps its middleware-live + basket-continuity pins.