Skip to content

refactor(ve2): retire per-family state contracts for the literal ones - #6

Merged
rhengles merged 6 commits into
mainfrom
claude/bootswatch-vanilla-extract-a9fxeb
Aug 9, 2026
Merged

refactor(ve2): retire per-family state contracts for the literal ones#6
rhengles merged 6 commits into
mainfrom
claude/bootswatch-vanilla-extract-a9fxeb

Conversation

@rhengles

@rhengles rhengles commented Aug 9, 2026

Copy link
Copy Markdown
Member

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.activedropdownItemActive, .btn.showbtnShowHook.
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

claude added 6 commits August 9, 2026 01:25
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
@rhengles
rhengles merged commit 0dbdbec into main Aug 9, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants