Skip to content

chore: migrate footprinter to zod v4 - #726

Open
DPS0340 wants to merge 6 commits into
tscircuit:mainfrom
DPS0340:chore/zod-v4-compat
Open

chore: migrate footprinter to zod v4#726
DPS0340 wants to merge 6 commits into
tscircuit:mainfrom
DPS0340:chore/zod-v4-compat

Conversation

@DPS0340

@DPS0340 DPS0340 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Summary

  • require zod@^4 and circuit-json@^0.0.455 as peers so composed schemas use one Zod major
  • migrate record, transform-default, and error APIs while preserving generated Circuit JSON
  • add Zod v4 regressions and document the peer-dependency requirement

Dependency order

This is intentionally a draft until circuit-json PR #661 merges and its release workflow publishes [email protected]. The coordinated rollout is circuit-json, footprinter, @tscircuit/props, then core. The exact 0.0.455 range prevents a Zod 3 circuit-json schema from being composed with this package.

Verification

  • bun test
  • bunx tsc --noEmit
  • bun run build
  • biome formatting check
  • npm pack --dry-run
  • byte-for-byte Circuit JSON comparison against the Zod 3 baseline for representative footprints

@DPS0340

DPS0340 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

The failed build job did not reach the build step: bun install failed while sharp downloaded libvips with a GitHub 504 Gateway Time-out. The same branch passes bun run build locally, and the PR test/format checks are green. I attempted to rerun only the failed job, but GitHub requires repository-admin permissions for that action.

@DPS0340

DPS0340 commented Jul 24, 2026

Copy link
Copy Markdown
Contributor Author

Dependency coordination update: commit 123b945 now requires circuit-json@^0.0.455, the Zod 4 release emitted after circuit-json PR #661 merges. The branch is intentionally stacked, so registry-only CI cannot install that version until then. Local Zod 4 validation remains green: bun test, bunx tsc --noEmit, and bun run build.

@DPS0340
DPS0340 marked this pull request as ready for review July 25, 2026 05:51
@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Now green and ready for review — [email protected] is published, so the earlier install failure is resolved.

Verified on the current head:

  • bun install resolves cleanly
  • bun run build succeeds
  • bun test → 463 pass / 0 fail (208 files)

CI on this branch is passing (Build, Bun Test, Format Check). This is the footprinter half of the Zod v4 migration tracked in tscircuit/core#2810.

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Note on the red checks here: this is blocked upstream, not by anything in this PR.

The published [email protected] still ships Zod v3-shaped types (z.ZodEffects, 5-parameter ZodObject), which don't exist in Zod v4. Any package composing those schemas under v4 fails type-check no matter what this PR does.

Unblocked by tscircuit/circuit-json#661, which is green and ready — once that merges and publishes, I'll refresh this branch and the checks should go green.

Happy to close this and reopen after the circuit-json release if you'd rather not have a red PR sitting in the queue.

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Same blocker as tscircuit/props#752, re-verified today.

The failing CI here predates [email protected] being published (run at 16:33 UTC, publish at 21:23 UTC), so the recorded No version matching "^0.0.455" error is stale — install succeeds now.

The real blocker is unchanged: the published 0.0.455 build still ships v3-shaped types.

$ grep -c "ZodEffects" node_modules/circuit-json/dist/index.d.mts
2813

ZodEffects doesn't exist in Zod v4, so type-check fails here no matter what this diff contains.

tscircuit/circuit-json#661 must merge and publish first. It's MERGEABLE/CLEAN, self-contained, and blocks both this and props#752.

Happy to close and reopen after that lands if a red check is unhelpful in the meantime.

@DPS0340

DPS0340 commented Jul 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed 7c3d968, which should clear the one red check.

What was failing and why. Only Format code was red — build, test and format-check were all green. The failure wasn't formatting at all, it was the install step:

npm error Found: [email protected]
npm error Could not resolve dependency:
npm error peer zod@"3" from @tscircuit/[email protected]

The split is that formatbot.yml installs biome with npm, while bun-formatcheck.yml uses bun. npm enforces peer ranges and bun doesn't, so the same tree installs fine in three workflows and hard-fails in the fourth. Once this PR moves the repo to zod@^4.0.0, @tscircuit/circuit-json-util's "zod": "3" peer becomes unsatisfiable under npm.

Worth noting the pin isn't something a version bump escapes — I checked 0.0.90, 0.0.95, 0.0.100 and 0.0.101 (latest) and every one still declares "zod": "3".

Fix is a five-line overrides entry rather than touching any source:

"overrides": { "@tscircuit/circuit-json-util": { "zod": "^4.0.0" } }

That's accurate rather than a suppression: circuit-json-util is only used in tests, and only for transformPcbElementstests/fixtures/compareFootprinterVsKicad.ts and tests/kicad-parity/diodes/diode_kicad_parity.test.ts, no runtime import. @tscircuit/soup-util already declares "zod": "*", so it needs nothing.

Verified both package managers and the full suite, since an override that fixes npm could easily break bun:

npm install @biomejs/[email protected]   added 252 packages   (failed before)
bun install                        173 packages, clean
bun test                           463 pass / 0 fail
bunx biome format .                352 files, no fixes applied
bun run build                      ESM + DTS success

I also confirmed this is specific to the branch and not pre-existing: the same npm install succeeds on main (where zod is ^3.23.8) and fails on this branch without the override.

One sequencing note for the chain: this PR is downstream of circuit-json#661. [email protected] still ships v3-shaped declarations (import { z } from 'zod', 688 ZodEnum<[...]> occurrences), so the full chain is circuit-json#661 → props#752 → this. This one's checks pass independently, but the ecosystem is only coherent once #661 publishes.

@DPS0340
DPS0340 force-pushed the chore/zod-v4-compat branch from 7c3d968 to 5208b29 Compare July 26, 2026 06:13
@DPS0340

DPS0340 commented Jul 26, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main and resolved the conflict — this is MERGEABLE / CLEAN again.

The conflict was in src/fn/pinrow.ts: this branch renamed the transformed length defaults to .prefault(), while main added cols, py and missing to the same object. Kept both — main's new fields, with p / id / od on .prefault().

A merge-clean result was not enough here. After resolving, one test failed that passes on main:

smdpinheader6 creates alternating vertical SMT header pads
-   "p": 2.54,      +   "p": "2.54mm"
-   "pw": 1,        +   "pw": "1mm"

src/fn/smdpinheader.ts landed on main after this branch's migration, so its length.default(...) calls were never converted. On Zod v4 .default() applies to the output type, so the transformed lengths stayed raw strings instead of parsing to numbers. Converted them to .prefault(), matching every other length field here. Literal booleans (male/female/smd) keep .default(), and missing keeps function_call.default([]) — consistent with bga.ts.

This is the migration's whole failure mode, so it's worth flagging for future length fields: on Zod v4, a length default written as .default() silently skips the string→number transform. It fails loudly here only because there was a parity test.

Verified against a main baseline rather than in isolation, since this repo has pre-existing failures:

origin/main (f7d1fe3)   472 pass, 5 fail
this branch             472 pass, 5 fail   ← identical set

The 5 (melf, sod323fl, sod923, diode0603, diode1210) fail on main too and are unrelated. biome format is clean.

DPS0340 added 6 commits July 27, 2026 12:12
Keep transformed footprint defaults on the input path and align Circuit JSON schema composition on Zod v4.
smdpinheader landed on main after this branch's zod v4 migration, so its
`length.default(...)` calls were never converted. On zod v4 `.default()`
applies to the OUTPUT type, so the transformed length values stayed as
raw strings ("2.54mm") instead of numbers, failing the parity assertion.

`.prefault()` applies on the input side, matching every other length
field in this branch. Literal booleans keep `.default()`.
Rebasing onto current main surfaced a real break rather than just a
conflict. fpc, led2835 and led5050 were merged into main after this
branch was cut, so their length fields still use .default() while the
rest of the codebase moved to .prefault().

Under zod v4 that is not a style difference. .default() short-circuits
the transform, so the declared value is handed back verbatim:

  before  {"x": null, "y": null, "w": "2mm", "h": "1.1mm"}
  after   {"x": -2.4, "y": 1.7, "w": 2,     "h": 1.1}

Coordinates came out null and dimensions as strings, which is why the
led5050/led2835 tests and both KiCad parity tests failed on the rebased
branch but pass on main.

Verified the branch now fails exactly what main fails and nothing more:

  main       5 fail  (diode0603, diode1210, melf, sod323fl, sod923)
  this PR    5 fail  (identical list)

479 pass on both.
@DPS0340
DPS0340 force-pushed the chore/zod-v4-compat branch from 5208b29 to 9a91970 Compare July 27, 2026 03:17
@DPS0340

DPS0340 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main — this was dirty, and resolving it surfaced a real break rather than just a textual conflict.

The conflicts themselves

Two files, both the same shape: main added fields while this branch changed .default() to .prefault() on neighbouring lines.

src/fn/pinrow.ts   main added cols/py/missing;  this branch changed p/id/od
src/fn/quad.ts     main added px/py;            this branch changed p

Kept both sides — the new fields with the migration applied to them. No upstream field was dropped or reordered.

The part that mattered

fpc, led2835 and led5050 were merged into main after this branch was cut, so they still use .default() while everything else moved to .prefault().

Under zod v4 that is not a style difference. .default() short-circuits the transform, so the declared value comes back verbatim:

before  {"x": null, "y": null, "w": "2mm", "h": "1.1mm"}
after   {"x": -2.4, "y": 1.7, "w": 2,     "h": 1.1}

Coordinates null, dimensions as strings. That is why the led5050 / led2835 tests and both KiCad parity tests failed on the rebased branch while passing on main.

Migrated those three files too, which is what this branch is for.

Verification

Compared failures on both sides with the same lockfile, since bun install state changes the count:

main       479 pass   5 fail   (diode0603, diode1210, melf, sod323fl, sod923)
this PR    479 pass   5 fail   (identical list)

Same five, and they fail on main independently of this branch. Nothing this PR touches is red.

Worth noting the general risk here: any def merged while this was open will have the same problem, because .default() still compiles and fails silently at runtime. If it sits much longer I will re-check before it lands rather than assume it is still clean.

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.

1 participant