store: guard the card-price cascade over built output in a browser, not over globals.css source text #72
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Spun out of PR #67, which shipped the #57 / #58 fixes with no automated regression guard. This issue is the guard, done in the one shape that can actually work.
Why the obvious approach fails, in detail — so nobody rebuilds it
PR #67 built a source-text guard over
apps/store/app/globals.cssand it went through three shapes across five review rounds, two of which were defeated by measurement on a real browser:!importanton the ancestor, and a track-list swap. A guard for a cascade defect that cannot see!importantis disqualifying.@mediaevaluation,!important→ specificity → source order. Validated at 462 comparisons against Chromium with zero disagreements, including specificity ties, shorthand-vs-longhand, inherited properties and breakpoint boundaries. Then defeated three ways by a reviewer, decisively by.browser .product-card p— a rule its modelled DOM chain never contained, so it was not mis-ranked, it was invisible.Attack 28 is the one that matters, and it kills the whole category. One line of ordinary CSS nesting:
puts #57 back on all five surfaces and all eleven card prices with the suite at 97 pass / 0 fail. The same trick puts #58 back as filed — 0px name column at 320/360/375, three overlapping pairs.
@property --ink { initial-value: #5a6159 }does it too, silently, because the parser recurses into a declarations-only at-rule body, finds no brace, and returns nothing.Root cause: the parser never descended into a non-at-rule body, so a nested rule was swallowed into its parent and never became a rule at all — bypassing the pin, the token forbid clause, the breakpoint contract and the absence guard simultaneously.
And the twist that settles the design question: Next flattens that nested rule to
.product-grid .price{color:var(--ink-muted)}in the shipped bundle — the exact selector an earlier round confirmed the guard does catch in flat form. So the guard's coverage was a property of how the CSS happened to be written, not of the defect class, because it read the authoring source while the browser consumes the compiled output.That is not a missing case. It is the wrong artefact.
The rule to take from it
The defect class is a rendered property. The only sound instrument for a rendered property is rendered output.
Every author and reviewer across five rounds actually verified these fixes by building the store and reading
getComputedStylein a real browser. The guard was an attempt to approximate that in a unit test, and each approximation was defeated by something the real engine does and the model did not. A partial CSS engine will keep losing to the real one — nesting today,@scopeor container queries tomorrow.A guard that grades authoring style is worse than no guard, because the next author trusts it.
What to build
A committed verification script that asserts computed values over the built export, replacing what people have been doing by hand:
schemaVersion: 3catalog fixture,pnpm --filter garage-store build, serveapps/store/out.sitemap.xml<lastmod>must equal the fixture'sgeneratedAt. Since #64 the catalog fetch is keyed per build, so a stale render is no longer possible — but the check is cheap and makes the output self-identifying..pricecomputedcolorequals the in-page resolved--inkon/,/productsand/services/<slug>— equality with the token, not "not muted". The defect renderedrgb(90,97,89); the fix givesrgb(23,32,24).margin-top: autowas also losing to.product-card p { margin: 0 }. Gaps went103.9/83.4/42.3/62.9/42.3→13/42.3/13/42.3/42.3..product-detail .price30.4px vs card 18.4px./cartat 320, 360, 375, 390, 600px, on a stocked-only line and an indent line, with a long product name: name column width non-zero, no overlapping element pairs,overflowX0.& .pricenesting, implicit nesting, nested@media,@property, nested token redefinition, nested-webkit-text-fill-color) are the acceptance suite: every one of them must fail this guard, since every one of them passed the source-text guard.The real obstacle, stated honestly
No CI runs on pull requests here. Both
.forgejo/workflows/*.ymlfire only onpush: branches:[main]+workflow_dispatch. So this cannot gate a PR automatically no matter how it is written — it is a script a human or agent runs and pastes, exactly like every other verification in this repo.That is a reason to keep it one command with readable output, and an argument for fixing the CI gap separately. It is not a reason to prefer a unit test that runs automatically and checks the wrong artefact.
Scope notes
18cefd4, 623 lines) with five rounds of review findings against it. Read it to learn what breaks, not to salvage.-webkit-text-fill-colordeserves a case of its own: it does not compete withcolor, it paints over whatevercolorresolves to — so a ranking-based check cannot see it, and it also slips agetComputedStyle().colorassertion. Assert the rendered pixel or assert the property's absence..product-typeis the first instance of this cascade class, held up by!importantsince6e36a27(#4), and is not currently losing — deliberately left alone by #67 and corroborated independently by biome'snoImportantStylesatglobals.css:506:24. Worth a case so that changes there are noticed.Acceptance
One command builds the store and asserts computed card-price colour, price bottom-alignment, the detail-page hierarchy, and
/cartlayout from 320px up, against the built export in a real browser; the output identifies which fixture produced it; every assertion has been made to fail on purpose at least once; and all six attack-28 variants fail the guard.yuris referenced this issue2026-08-29 06:25:36 +00:00
Closed as stale / YAGNI. The underlying CSS cascade bugs (#57, #58) are resolved on
main. Authoring a standalone headless Chromium runner for historical source-text parser attacks without CI integration adds ongoing maintenance overhead without catching real regression.