refactor(ve2): retire per-family state contracts for the literal ones - #6
Merged
Merged
Conversation
The `dropdownItemActive` report is right about the facts and wrong about the
cause. The four contracts were not forgotten — they are the unwired remainder
of a whole category.
A per-family alias collapses a compound source selector into one identifier:
`.dropdown-item.active` → `dropdownItemActive`, `.btn.show` → `btnShowHook`.
33 identifiers stood for 9 Bootstrap state classes. The literal emitter
translates 1:1, so each alias needed a divergence-manifest entry to exist in
the CSS at all — 12 remapSymbols plus 8 mirrors, hand-written, one per family
that happened to have a component demanding it. Families without a demo never
got an entry, so 18 of the 33 shipped with no CSS behind them. Nothing
compared the two hand-maintained lists.
Emitting both (the report's first suggestion) is the addMirrorRule pattern
that already produced 8 of those entries; it would take the list from 15 to 19
and keep the failure mode. An alias is a pure rename — `${navLink}${active}`
has identical specificity and match set — so it buys no isolation the compound
selector doesn't already give. Plan §8.6 said this; T8 reverted it on the
wrong side by teaching the emitter to produce the aliases instead of
regenerating the CSS.
- divergence manifest 22 → 2 entries (body-split, legend-element-mirror)
- 36 contracts deleted: the 33 aliases, tooltipVe/popoverVe, and the duplicate
fade/show/collapse copies in ui/navs, ui/modal, ui/navbar. literal/ is now
the single owner of every shared state token.
- kept modalOpenHook and carouselSlide: `.modal-open` and `.slide` have no
rule in any bootstrap.css, so they are runtime markers, not aliases
- 27 themes regenerated onto ${component}${state}: −4290 lines, the per-family
.fade / .fade:not(.show) mirrors collapse into one generic rule each
- 8 adapters + ~90 components stamp the literal hashes, dropping the
double-stamp workarounds that had accumulated in 58 files
Gate: check-contract-wiring.mjs (static, pre-build) plus a liveness assertion
in build-contract.mjs (authoritative, real hashes vs emitted CSS). It keys on
(module, symbol), not name — `fade` is declared twice and a name-keyed check
reads the live copy as proof the dead one is fine. Deliberate inertness goes
in contract-wiring-allowlist.mjs with a reason; a state hook never qualifies.
Two further bugs the gate surfaced: build-contract only checked liveness for
duplicate candidates, so a name declared by exactly one module was published
unchecked — the precise mechanism that shipped the four; and contractModules()
filtered by filename, excluding layout/container.css.ts, so containerFluid
resolved to literal's dead copy while the themes styled layout's.
Also regenerates theme-runtime.ts, which still imported the utilities/used
family retired in dd753ef — ve-project2 did not build without it.
Package dead exports 90 → 68, all allowlisted. `active` keeps hash b17c3vgbe3.
Verified by rule-set equivalence across all 27 themes: 0 distinct rules
dropped, 0 added (scripts/_cssdiff.mjs).
BREAKING CHANGE: `dropdownItemActive`, `listGroupItemActive`, `navLinkActive`
and the other per-family state aliases are gone. Compose the literal contract
instead: `cx(dropdownItem, isActive && active)`, with `active` from the
package's `global` entry. The removed names now fail to compile rather than
failing to paint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
…emplates
The state-alias codemod deduped repeated `${token}` per template literal, but
the tooltip and popover `template:` strings hold several elements in ONE
literal. `${theme}` legitimately repeats there — once per element — so the
dedupe dropped it from the inner nodes and `${scope}${tooltipInner}` stopped
matching: the bubble rendered as unstyled text.
Caught by verifying against baselines recaptured with the same browser, which
removes the font-metric noise that was masking it: 9 tooltip/popover routes
failed on this branch and passed on main.
Also repoints `frame`/`frameColumn`/`justify*` in the tooltip demos back to
ui/tooltips. Both families declare those names with byte-identical bodies, so
resolving them to ui/popovers was harmless but wrong to read.
scripts/_fixdedupe.mjs reports any `${token}` a file lost relative to a base
revision without being a renamed alias — it is what found the 10 files, and it
now reports zero.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
…d baselines Comparing against the committed baselines measured the browser, not the CSS: the repo pins chromium_headless_shell-1217 and this container only has 1194, so an untouched spinner already differed by 0.43% and text-heavy routes by 2–16%. Recapturing the baselines with the same binary — from the root app, which renders real Bootswatch CSS and is unaffected by ve-project2 — makes the noise cancel on both sides. bootstrap 330/339, darkly 113/119, materia 111/118, most at 0.000000. The 13 remaining mismatches are pre-existing: the same routes fail on main with ratios identical to six decimal places. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
package.json declares `packageManager: npm@11.6.2` and pnpm refuses to run a
project that names another manager, so every baseline capture died at server
startup:
ERROR This project is configured to use npm
Only the baseline path was affected — the VE1/VE2 preview servers spawn vite
through run-ve-vite.mjs directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
…ty CSS dd753ef removed the utilities/used chunks but left every consumer pointing at them: 350 components declared 'utilities/used' in ve2RequiredStyleFamilies, and style-families.ts still listed it as a valid family id. Ve2GranularShell logs a DEV-only warning for an unknown family and returns — silent in a production build — so in granular mode the utilities chunk was requested by nobody and no utility rule ever loaded. Every route was affected; it showed wherever a utility drove layout. On /ui/pagination/large-pagination the `.flex-wrap` on the `<ul>` never applied, so the pagination overflowed instead of wrapping; on /ui/navs/tabbed-nav `.mb-3` lost to `.nav { margin-bottom: 0 }` because the `!important` rule was not there to win. This is the third consequence of that half-done retirement, after the theme-runtime.ts imports of the deleted chunk. Verified: bootstrap, all 339 routes across the affected families, granular loader, against baselines recaptured with the same browser — 333 matched, 0 mismatched, most at 0.000000. card-tabs, navbar and large-pagination went from mismatched to pixel-identical. The 6 skipped are routes whose @screenshot directive height is pinned to the metrics of the browser build this container does not have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
… failures The 13 routes that failed on both this branch and main were not a contract problem at all: in granular mode no utility rule loaded on any route, because dd753ef retired utilities/used without updating its 350 consumers. Fixed in 6f5eb01; bootstrap now runs 333/339 with zero mismatches and darkly 119/119. Also stops the capture harness from creating a file named `nul` in the repo root on every POSIX run — `killPortWindows` shelled out to a Windows-only `2>nul` redirect unconditionally — and removes the one that got committed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4
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.
The
dropdownItemActivereport is right about the facts and wrong about thecause. The four contracts were not forgotten — they are the unwired remainder
of a whole category.
A per-family alias collapses a compound source selector into one identifier:
.dropdown-item.active→dropdownItemActive,.btn.show→btnShowHook.33 identifiers stood for 9 Bootstrap state classes. The literal emitter
translates 1:1, so each alias needed a divergence-manifest entry to exist in
the CSS at all — 12 remapSymbols plus 8 mirrors, hand-written, one per family
that happened to have a component demanding it. Families without a demo never
got an entry, so 18 of the 33 shipped with no CSS behind them. Nothing
compared the two hand-maintained lists.
Emitting both (the report's first suggestion) is the addMirrorRule pattern
that already produced 8 of those entries; it would take the list from 15 to 19
and keep the failure mode. An alias is a pure rename —
${navLink}${active}has identical specificity and match set — so it buys no isolation the compound
selector doesn't already give. Plan §8.6 said this; T8 reverted it on the
wrong side by teaching the emitter to produce the aliases instead of
regenerating the CSS.
fade/show/collapse copies in ui/navs, ui/modal, ui/navbar. literal/ is now
the single owner of every shared state token.
.modal-openand.slidehave norule in any bootstrap.css, so they are runtime markers, not aliases
.fade / .fade:not(.show) mirrors collapse into one generic rule each
double-stamp workarounds that had accumulated in 58 files
Gate: check-contract-wiring.mjs (static, pre-build) plus a liveness assertion
in build-contract.mjs (authoritative, real hashes vs emitted CSS). It keys on
(module, symbol), not name —
fadeis declared twice and a name-keyed checkreads the live copy as proof the dead one is fine. Deliberate inertness goes
in contract-wiring-allowlist.mjs with a reason; a state hook never qualifies.
Two further bugs the gate surfaced: build-contract only checked liveness for
duplicate candidates, so a name declared by exactly one module was published
unchecked — the precise mechanism that shipped the four; and contractModules()
filtered by filename, excluding layout/container.css.ts, so containerFluid
resolved to literal's dead copy while the themes styled layout's.
Also regenerates theme-runtime.ts, which still imported the utilities/used
family retired in dd753ef — ve-project2 did not build without it.
Package dead exports 90 → 68, all allowlisted.
activekeeps hash b17c3vgbe3.Verified by rule-set equivalence across all 27 themes: 0 distinct rules
dropped, 0 added (scripts/_cssdiff.mjs).
BREAKING CHANGE:
dropdownItemActive,listGroupItemActive,navLinkActiveand the other per-family state aliases are gone. Compose the literal contract
instead:
cx(dropdownItem, isActive && active), withactivefrom thepackage's
globalentry. The removed names now fail to compile rather thanfailing to paint.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01DjPPs9bCYcKGGVAhbzQNX4