Conformance gap against stx-standards §9.10, found while fixing the courthouse tab links.
The rule
9.10 MUST — read route params from
params, never re-parse the URL. In a<script server>block on a dynamic route, use the injectedparamsobject or the bare param name. In a client script, useuseRoute().params. Never reconstruct a param fromrequestContext.url(),location.pathnameor a query-string fallback.
What bench does instead
Every dynamic-route component resolves its id by hand, in the same shape:
function resolveCourtId(): number {
if (typeof window === 'undefined') return 0
const fromRouter = Number((window as any).stx?._rp?.id)
if (Number.isFinite(fromRouter) && fromRouter > 0) return fromRouter
const m = window.location.pathname.match(/^\/court-houses\/(\d+)/)
return m ? Number(m[1]) : 0
}Measured:
.stx files reading location.pathname | 18 |
call sites re-parsing a route param (.match) | 15 |
files using the _rp + pathname fallback | 14 |
files using useRoute() | 0 |
Affected components include Bench/Judge/{JudgeTabs,Profile,ProfileHeader,Reviews,RecentRulings}, Bench/Court/{CourtTabs,CourtHeader,CourtProfile}, Bench/ArticleView, Bench/Comments, Bench/User/ReviewerProfile, Bench/Admin/AdminShell.
Why it has persisted
Two reasons, and both look like good reasons until you check them.
1. A project memory told us to. The note says window.stx._rp is unset on a hard reload, so dynamic-route components must parse location.pathname instead or the fetch never fires. That observation was real. The conclusion was wrong: §9.10 documents useRoute().params as the reactive wrapper over the same _rp object (signals.js:990-1002), so it resolves when _rp arrives — which is precisely the hard-reload case the workaround was invented for. Verified present in bench's installed stx 0.2.176.
2. The strict guard is on, but this category is allowlisted. config/ui.ts sets strict: { enabled: true, failOnViolation: false } — ahead of where the standards found bughq — but allowPatterns contains 'window.location', and allowPatterns is a rule-global substring match. The entry is justified in its own comment by a different problem (bare location gets shadowed by store signals, so bench qualifies with window.), which is a genuine footgun. But one entry suppresses every window.location finding project-wide, including the route-param reads that have a proper replacement. The last build emitted 0 strict warnings against 28 occurrences.
What to do
- Replace the hand-rolled resolvers with
useRoute().params(auto-imported; never hand-import it — rule 9) - Confirm reactivity on a hard reload, not just SPA nav — that is the case the workaround existed for, and the one that must not regress
- Narrow the
window.locationallowPattern so genuine non-route uses stay allowed and route-param reads start warning again - Correct the project memory so the anti-pattern stops being re-applied
Risk
The hard-reload path is the whole point. Each component must be verified with a real reload, not just in-app navigation — the judge tab shells in particular resolve the id from the pathname by design today, because one shell serves all 2,740 judges (see #83).