Skip to content

Fix #205/#206: recover csstype values + token records reached through DeepPartial - #209

Merged
jagguji merged 2 commits into
mainfrom
worktree-fix-205-token-widening
Aug 21, 2026
Merged

Fix #205/#206: recover csstype values + token records reached through DeepPartial#209
jagguji merged 2 commits into
mainfrom
worktree-fix-205-token-widening

Conversation

@jagguji

@jagguji jagguji commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Fixes #205, #206. Follow-up: #208.

Root cause (one, shared by both issues)

blend 0.0.38-beta.1 added ComponentTokenOverrides = DeepPartial<ComponentTokenType> — a homomorphic mapped type that re-projects csstype Property.* values, stripping their alias + declaration-file provenance. The path-based isCssType then missed them, so:

Confirmed with a live TS-compiler probe: a csstype value reached directly keeps its csstype alias/path (→ string); the same value through DeepPartial<> comes back as an alias-less synthesized union.

Changes (src/extract.mjs)

  1. Structural csstype detection — a union whose string literals include the whole csstype Globals set (-moz-initial | inherit | initial | unset) is a CSS value → string, however it was reached. An isForeignArm guard preserves a union that also carries a genuine record / dict / array / callable arm (chart ColorString | GradientColorObject | PatternObject, responsive T | { [breakpoint]: T }) instead of collapsing and dropping it — this also closes a pre-existing silent data-loss of chart color record arms. The (string & {}) / (number & {}) autocomplete open arms are recognised (prototype fingerprint) and still collapse.
  2. Bounded heal relaxationhealGhostRecords accepts a rebuild that registers new entries when the record is provably bounded (boundedPastDepth), so blend's finite token tree heals while Highcharts' cyclic graph stays truncated.
  3. Anonymous bounded-record escape — the materialize-past-MAX_DEPTH escape now covers the anonymous {…} records DeepPartial produces (safe via path-scoped stable naming, Structural type names churn across versions when shape is unchanged (numbered suffixes shift on unrelated upstream edits) #90/Fidelity defects in generated bindings (key-stripping, flattened unions, domProps spread, fn-as-component) + unstable anonymous-record naming — found validating #62 on blend 0.0.37-beta.6 #63).

Result (blend 0.0.38-beta.1, bindgen constant)

before after
#206 csstype enumeration / CSS-keyword leakage / monstrous names present gone
#205 record→string ghosts 350 111
total ⚪ loose lines 843 392
chart gradient/pattern record arms (latent data-loss) dropped preserved

Verification

  • npm test green (122 goldens + public-name-stability + representation-flip + module-move + html-attrs).
  • Full benchmark: 0 FAIL, 0 broken, every package compiles; 5 WARN (metrics equal-or-better), 6 non-css packages byte-identical. Baselines regenerated in this PR.
  • Maintenance loop honored: new fixture csstype-deeppartial-widening (csstype leaf, DeepPartial leaf, and the record/dict/array over-match cases), docs/TYPE_MAPPING.md updated, goldens regenerated (generic-record-dedup improved: two string ghosts → real records).
  • Two adversarial review rounds; one MAJOR "flag-don't-fake" finding (index-dict / array arms dropping unflagged) found and fixed.

Known residual (tracked in #208)

~71 of the 350 ghosts remain (beta.0's floor is ~40). They are partially-degraded records blocked by the heal's 80% threshold, which is load-bearing for the #177 overload-ambiguity suppression — lowering it fakes an exact polytag in an ambiguous overload slot (a flag-don't-fake violation). Left as a focused follow-up rather than regressing #177.

🤖 Generated with Claude Code

… DeepPartial

blend 0.0.38-beta.1's `ComponentTokenOverrides = DeepPartial<ComponentTokenType>`
is a homomorphic mapped type that re-projects csstype `Property.*` values, stripping
their alias + declaration-file provenance. The path-based `isCssType` then missed them,
so csstype value-unions were ENUMERATED into giant `@unboxed` bodies with CSS-keyword
constructors leaking into shared modules (#206), and the wrapping token records deepened
the graph, were first reached past MAX_DEPTH, and truncated to all-`string` ghosts (#205).

Three changes in src/extract.mjs:
1. Structural csstype detection: a union whose literals include the whole csstype
   `Globals` set is a CSS value -> `string`, however reached. An `isForeignArm` guard
   preserves a union that also carries a genuine record/dict/array/callable arm (chart
   `ColorString | GradientColorObject`, responsive `T | { [bp]: T }`) instead of dropping
   it to a bare `string` — this also closes a pre-existing SILENT drop of chart color
   record arms. The `(string & {})`/`(number & {})` open arms are recognised (prototype
   fingerprint) and still collapse.
2. Bounded heal relaxation: healGhostRecords accepts a rebuild that registers new
   entries when the record is provably bounded (boundedPastDepth), so blend's finite
   token tree heals while Highcharts' cyclic graph stays truncated.
3. Anonymous bounded-record escape: the materialize-past-MAX_DEPTH escape now covers the
   anonymous `{…}` records DeepPartial produces (safe via path-scoped stable naming).

Result on blend 0.0.38-beta.1 (bindgen constant): csstype enumeration gone, monstrous
names gone, record->string ghosts 350 -> 111, total loose lines 843 -> 392. Full
benchmark equal-or-better (0 FAIL, 0 broken, all compile); 6 non-css packages
byte-identical. New fixture csstype-deeppartial-widening; TYPE_MAPPING.md updated;
generic-record-dedup improved (two ghosts -> real records). Baselines regenerated.

The residual ~71 ghosts (vs beta.0's ~40 floor) are partially-degraded records blocked
by the heal's 80% threshold, which is load-bearing for the #177 overload-ambiguity
suppression — lowering it fakes an exact polytag in an ambiguous slot. Tracked separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jagguji
jagguji deployed to benchmark August 20, 2026 14:54 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown

Benchmark: ✅ PASS

Package Compile Diff vs baseline usable review broken Verdict
@juspay/blend-design-system@0.0.36 identical 102 5 0 ✅ PASS
@juspay/blend-design-system@0.0.37-beta.8 identical 215 7 0 ✅ PASS
@juspay/blend-design-system@0.0.37 identical 219 7 0 ✅ PASS
@juspay/blend-design-system@0.0.38-beta.1 identical 226 9 0 ✅ PASS
react-day-picker@10.0.1 identical 19 7 0 ✅ PASS
react-tooltip@6.0.7 identical 1 0 0 ✅ PASS
react-markdown@10.1.0 identical 0 2 0 ✅ PASS
@smastrom/react-rating@1.5.0 identical 1 0 0 ✅ PASS
clsx@2.1.1 identical 0 0 0 ✅ PASS
hono@4.12.25 identical 0 0 0 ✅ PASS
@base-ui-components/react@1.0.0-rc.0 identical 174 21 0 ✅ PASS

@pkg-pr-new

pkg-pr-new Bot commented Aug 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@juspay/rescript-bindgen@209

commit: d6e67f4

Review Finding A: in healGhostRecords the `bounded` check was
`boundedPastDepth(type, …)` on the ghost record itself — but that record is
registered under its own type.id, so the predicate hit the "registered → link"
short-circuit and returned true unconditionally. The accept gate
`newEntries === 0 || bounded` therefore collapsed to `fallbacks < bad`, the
`newEntries === 0` requirement was silently gone, and the doc/comment claim that
"Highcharts' cyclic graph stays false → rejected" was factually false.

Replace it with `healFieldsBounded`: recurse `boundedPastDepth` over the record's
FIELDS with the record's own id pre-seeded into the cycle guard. A field that
links to an already-registered entry is zero-growth (safe); a field the rebuild
would register anew gets the real structural check — a finite token subtree
passes, a cyclic/too-deep graph trips the cycle/budget guard → false. Also read
DeepPartial's SYNTHESIZED property symbols via getTypeOfSymbol (they have no
declaration node, which the field recursion would otherwise reject).

The gate is now honest and actually discriminates (returned 149 vs 111 before the
synthesized-symbol handling was added, proving it rejects on structure). Net
output is unchanged: all 122 goldens match and all 11 benchmark packages are
byte-identical to the prior baseline — confirming the inert gate's practical
effect was only ever bounded token records (no Highcharts was wrongly healed),
so this is a pure correctness/contract fix.

Also (review Finding B): document that lifting the anonymous-record name gate
(change #3) applies to every package — bounded Highcharts option records now
materialize (fidelity gain, 0 dangling), cyclic ones still truncate — rather than
implying it is scoped to blend. Finding C (the `(string & {})` → `{ ...domProps }`
open-arm shape, pre-existing) tracked in #210.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant