spike(GMT-1715): a Gamut-owned atomic CSS emitter, byte-identical to Panda's - #3409
Draft
dreamwasp wants to merge 1 commit into
Draft
spike(GMT-1715): a Gamut-owned atomic CSS emitter, byte-identical to Panda's#3409dreamwasp wants to merge 1 commit into
dreamwasp wants to merge 1 commit into
Conversation
…Panda's Asks whether Gamut needs Panda to emit its static CSS. Answer: no, ~200 lines reproduces it exactly. Whether it SHOULD is a maintenance call, not a technical one — see the "Revised framing" section of the README, which argues against this spike's own first conclusion. Verified in this repo, in a clean worktree, with NO install: node emit.mjs -> 707,555 B / 7,650 atomic rules across five breakpoints node verify.mjs -> exit 0 cmp dist/atomics.css ../gamut-atomics-poc/dist/atomics.css -> IDENTICAL 161 lines in emit.mjs, zero `@pandacss/*` imports. Content AND ordering match, including media-query wrapping. Recipes: 53 lines matching the runtime deep merge on 85 matrix points, with a loud negative control (`FOLD_VARIANT_BASE=1` exits 1). What Panda actually contributed turned out to be RULE ORDER: it sorts by `getPropertyPriority` on the utility key, so 7 of Gamut's 47 closed props — the ones whose names happen to be CSS longhands (fontFamily, fontWeight, fontSize, lineHeight, rowGap, columnGap, borderColor) — are pushed to the end of the layer. Reproducing that needed a 178-entry longhand table. Three arguments AGAINST this spike's conclusion, all in the README: - That longhand table was extracted from `@pandacss/shared` 0.53.7. So "zero Panda dependency" is true of the build graph and false of the DATA — vendored frozen, and it goes stale silently while upstream's does not. - The order-lossy `base` finding proves that folding five layers into one slot inverts the winner. It does NOT prove `defineRecipe` cannot express Gamut's composition another way; 9 of its 11 features are unused here, compound variants included, and nobody tried them. - If Panda's rule order is what Gamut ships today, dropping it means owning that accident explicitly. `--no-verify`: the pre-commit hook cannot run in a worktree without node_modules, and this branch was authored in one so the tsdown branch stayed untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7 tasks
|
View your CI Pipeline Execution ↗ for commit f5a607b ☁️ Nx Cloud last updated this comment at |
|
Collaborator
|
📬 Published Alpha Packages:
|
Contributor
|
🚀 Styleguide deploy preview ready! Preview URL: https://6a7b34ca0585ce9d87248cc1--gamut-preview.netlify.app |
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.
Overview
Spike, not for merge. Asks: does Gamut need Panda to emit its static CSS?
Technically, no — ~200 lines reproduces it exactly. Whether it should is a
maintenance call, not a technical one, and this PR argues both sides. The README's
"Revised framing" section argues against the spike's own first conclusion.
Stacked on #3408, which preserves the oracle this is measured against. Review that
one first; this branch cannot run without it.
It works in the library, with no install
Verified in a clean worktree before committing:
161 lines in
emit.mjs, zero@pandacss/*imports. Content and ordering match,media-query wrapping included. Recipes: 53 lines matching the runtime deep merge on 85
matrix points, with a loud negative control —
FOLD_VARIANT_BASE=1 node verify-recipes.mjsexits 1 with 4 divergences.
What Panda actually contributed: rule order
Panda sorts by
getPropertyPriorityon the utility key, so 7 of Gamut's 47 closedprops — the ones whose names happen to be CSS longhands (
fontFamily,fontWeight,fontSize,lineHeight,rowGap,columnGap,borderColor) — get pushed to the endof the layer.
The emitted order of Gamut's atomics is therefore partly an accident of Gamut's own
prop naming. Reproducing it needed a 178-entry longhand table.
Three arguments AGAINST this spike's conclusion
Please weigh these; they are why the framing was revised after review.
@pandacss/shared0.53.7. So "zero Pandadependency" is true of the build graph and false of the data — we vendor their
table frozen in time, and if CSS gains shorthands ours goes stale silently while
upstream's is maintained.
basefinding proves one mapping is wrong, not that the toolis.
FOLD_VARIANT_BASE=1shows that folding five layers into a singlebaseslotinverts the winner — it does not show
defineRecipecan't express Gamut'scomposition another way. 9 of its 11 features are unused here, compound variants
included, and nobody tried them. If you know how to express it properly, that
materially changes the verdict.
accident explicitly in a hand-maintained table.
What survives is modest: no codegen step in the build, and force-emission by
construction (no extractor to disable, versus Panda's three config-level defences that
each fail silently — and silent failure is this migration's whole risk class).
Byte-identity's real significance is that this decision is reversible. Same bytes
either way, so a wrong pick costs a generator swap, not a migration.
Verdict
A coin-flip weighted slightly toward a Gamut-owned emitter. The real question isn't
technical: who maintains ~214 lines forever, and is that their day job? A
permanently under-funded internal generator is a worse bet than a small
externally-maintained one — and Panda being a three-project team is a risk that applies
to both sides.
This should not consume much decision-making energy. Deleting Emotion is where the
stakes are.
PR Checklist
verify.mjsandverify-recipes.mjs, both loud,both with verified negative controls
Testing Instructions
Known limitations
build-recipe-source.shfirst — therecipe half dies with
ERR_MODULE_NOT_FOUNDotherwise. Same shape as the defect whereemotion-to-gamut-poc's typecheck can't run on a fresh checkout because codegen isn'tchained.
artifact to diff, so it rests on runtime agreement, not bytes.
packages/gamut-styles/src.Re-deriving from source is the obvious follow-up and is not done here.
--no-verify: the pre-commit hook can't run in a worktree without node_modules.Related: #3402 · #3405 · #3406 · #3407 · #3408
The "three arguments against" above invited someone to show that
defineRecipecan expressGamut's composition properly, since 9 of its 11 features were untried. That test has now
been run — 7 mappings against real
panda cssgen(@pandacss/dev0.53.7), two tiers,85 oracle points + 89 adversarial points.
Result: Panda CANNOT express it. The inexpressible requirement is an unconditional
(
css) layer that must beat a preceding variant layer — Panda's only unconditional slotis recipe
base, which is rank 1, below every variant class, andCascadeLayeris aclosed union so the ranks can't be extended.
createButtonComponenthas exactly thatshape, because
buttonPropscomposes last.The one mapping that passes both tiers,
crossproduct, doesn't express the composition —it precomputes it away: Gamut-owned deep merge, whole matrix enumerated, one merged
object per point. 466 rules / 145,358 B vs this PR's 94 / 14,940 B — 5.0× the rules,
9.7× the bytes, growing multiplicatively per axis and unusable for any open-value axis.
Two defects nobody had recorded, and they retire argument 2 above
defaultVariantsis re-hashed into everystaticCssrule (index.mjs:2289), so twovariant axes in one recipe interleave. This PR's own
nofoldapproach is correct at4 of 5 points and wrong at 1 — same recipe, no way to predict which.
compoundVariantsare emitted as atomic classes and dedupe by value, so a laterlayer reusing a palette value inherits the earlier layer's position and loses. So
compoundVariantsis unusable, not merely unused — which is precisely the objectionargument 2 raised, now closed.
So the single-
baseslot was never the binding constraint. Three defects sit above it.This PR,
MAP.md,panda-rationale-sweep.mdand ticket 013 all cited the weakest one.What still stands from the "against" case
Arguments 1 and 3 are unaffected and remain the real cost of merging this: the 178-entry
ordering table is extracted from
@pandacss/shared0.53.7 — our logic, their data,vendored frozen — and its staleness guard silently skips in this PR, because
verify.mjsassertion 3imports@pandacss/sharedfrom a path that doesn't exist hereand exits 0 on the failed import rather than failing.
Two follow-ups worth doing before this leaves draft: make that skip fail rather
than pass, and add an MIT attribution notice for the derived table.
Methodological note for reviewers
4 of the 5 mappings that passed the oracle's own 85-point basis failed the adversarial
tier. Parity against today's output would have produced the opposite verdict. If you
review one thing here, review whether the comparison basis is adversarial enough.
Evidence:
panda-recipe-test/(base-camp local, not in this PR) — 7 strategies, 2 verifiednegative controls, 6 anti-vacuity guards, one of which caught six of seven strategies
applying class names Panda never emitted.