镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Fix #90: derive stable-name hash from structure, not type.id (no churn across versions/edits) - #140

Merged
jagguji merged 2 commits into
mainfrom
fix/90-structural-name-hash
Jul 13, 2026
Merged

jagguji merged 2 commits into
mainfrom
fix/90-structural-name-hash

Conversation

@jagguji

@jagguji jagguji commented Jul 13, 2026

Copy link
Copy Markdown
Collaborator

Fixes #90.

Problem

The finalization pass (stabilizeNames) gives a genuinely-colliding shape — same base name, same home, distinct shape — a content-hash suffix instead of an encounter-order counter, so a byte-identical shape should keep its name. But the hash was seeded from entrySig/recordSig, which encode a field's referenced type as its entry key — and keys are 'id:' + type.id. The checker assigns type.id in encounter order, so:

  • an unrelated upstream edit (adds/removes a sibling type) renumbers it, and
  • a compiler-version bump (e.g. TS 5.9 → 6.0) renumbers it too,

→ the "stable" hash changes and a byte-identical shape gets a different name (fooConfig3 ⇄ fooConfig). That's source-breaking churn for every downstream consumer who annotated the old name, with zero semantic change upstream — exactly what a typed-API generator must not do.

Fix

A new structuralSig seeds the hash from structure only: field names + optionality, and recursively the shapes of referenced types (not their id-keys), cycle- and diamond-guarded by an add-only seen set (linear on deep DAGs — blend's ~2600-node --webapi graph is fine). The id-based entrySig/recordSig are untouched for dedup and the merge, where type.id is correct within a run. The change is one line in stabilizeNames — swapping the hash seed.

Guarantee

Identical shape ⇒ identical name, across compiler versions and unrelated upstream edits. Only a real shape change changes a name.

Verified three ways:

  • Compiler-version stability: blend@0.0.36 generates byte-identical type names (1749 of them) on TypeScript 5.9.3 and 6.0.3 — before this, they churned.
  • Unrelated-edit stability: adding several unrelated types upstream (renumbering every subsequent type.id) no longer renames the unchanged SizeToken shapes.
  • Smoke guard: a renumber-invariance check builds the same shape (incl. a self-ref cycle) under two totally different key-numberings and asserts the hash is identical; a different shape still hashes differently.

One-time cost

Because the hash function changes, the currently hash-suffixed names rename once in this release (the golden + benchmark-baseline diff — portalStyleQn0e4 → portalStyleV1dkur, byte-identical shapes). This is the last forced rename; ship it with a version bump and a consumer note. All metrics are unchanged (name-only, equal-or-better on all 9 benchmark packages).

Verification

🤖 Generated with Claude Code

The finalization pass gives a genuinely-colliding shape (same base, same home)
a content-hash suffix, but the hash was seeded from entrySig/recordSig — which
embed the checker's `type.id` through entry ref-keys. `type.id` is assigned in
encounter order, so an unrelated upstream edit OR a compiler-version bump
renumbered it and a byte-identical shape got a different name (fooConfig3 <->
fooConfig) — source-breaking churn for downstream consumers who annotated the
old name.

New `structuralSig` seeds the hash from STRUCTURE only: field names/optionality
+ recursively the referenced types' shapes (cycle- and diamond-guarded by an
add-only seen set, so it's linear on deep DAGs), with no `type.id`. The id-based
entrySig/recordSig are untouched for dedup/merge (correct within a run). One line
in stabilizeNames swaps the hash seed.

Identical shape => identical name across compiler versions and unrelated edits.
Verified: blend generates byte-identical type names on TypeScript 5.9 and 6.0.3;
an unrelated upstream type addition no longer renames unchanged shapes. Guarded
by a renumber-invariance smoke check (shapeHash/structuralSig exported for it).

One-time rename of the affected hash-suffixed names in this release (the golden +
benchmark-baseline diff); stable thereafter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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
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 Jul 13, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

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

commit: 8ec607b

@jagguji
jagguji merged commit c0f8d27 into main Jul 13, 2026
11 of 12 checks passed
@jagguji
jagguji deleted the fix/90-structural-name-hash branch July 13, 2026 14:10

This branch was previously deployed

1 inactive deployment
benchmark — 8ec607bf Deployed Jul 13, 2026 by jagguji via Benchmark (pinned real packages) #113
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.

Structural type names churn across versions when shape is unchanged (numbered suffixes shift on unrelated upstream edits)

1 participant