Skip to content

feat(packs): verified React Router 6 → 7 migration pack - #132

Merged
lucasbraum merged 7 commits into
mainfrom
claude/magical-albattani-vkb7nh
Oct 15, 2026
Merged

lucasbraum merged 7 commits into
mainfrom
claude/magical-albattani-vkb7nh

Conversation

@lucasbraum

@lucasbraum lucasbraum commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #141 (the compile-signal fix); its base is that branch until #141 merges.

Pack

react-router >=6 <7 → >=7 <8 (default target 7.18.4), queue entry react-router. react-router-dom is a releaseGroup member (published in lockstep, pinned exactly). It is also the pack's alternate lead (#134), so the pack runs for the usual v6 setup that declares only react-router-dom: check react-router and check react-router-dom both run it, and fix declares react-router at the target beside it.

The queue asked for 6 → 8. The evidence says that is two upgrades:

  • The v6 guide exists only in the v7 docs.
  • Each guide assumes the previous major's future flags were adopted on that major.
  • react-router-dom has no 8.x.

So this PR ships 6 → 7. packs/queue.json retargets the entry to >=7 <8 (verified) and adds a 7 → 8 entry (todo).

Also here: one commit fixes pack new's registry renderer, which ordered ./react-router/ before ./react/ against biome. It comes with a regression test, on top of #128's wrapping fix.

Sources

All pinned to release commits and read 2026-10-14. Each change is classified in packages/core/src/packs/react-router/RESEARCH.md.

Rules and behavior notes

Mechanical (a rewrite at the reported site), exercised by ground truth:

  • dom-imports (deprecated, detect + rewrite): moves a react-router-dom import or re-export to react-router when every name it takes is there. It declines when a test mocks react-router-dom by name. Phoenix exercises it with 29 sites.

Assisted, rewrite ready (detection runs; the edit is unit-tested but not wired until a ground-truth repository exercises it; RESEARCH.md lists them):

  • future-prop (breaking, TS2322): removes the future prop of a router (aliases resolved through the import), inline v7_* literals only. The only inline-literal upgrades seen, wire-webapp and OpenMetadata, are excluded by the parent rule below. Lago's sites pass a variable.
  • dom-server-entry (breaking, TS2307 + detect): react-router-dom/server becomes react-router.
  • future-option (deprecated, detect): the stale future option of a router factory, which v7 types {}.
  • form-method-case (breaking, TS2367/2678/2820 + detect): a lowercase method compared with .formMethod. This is the syntactic detect for comparisons the compiler is blind to through any/string.

Assisted (a guide for the agent; guide options go to "Decisions for you"):

  • dom-entry-imports (deprecated, detect): RouterProvider/HydratedRouter from react-router/dom, which needs moduleResolution bundler/node16/nodenext.
  • future-prop-untyped (deprecated, detect): a future prop passed through untyped createElement (jest mocks); v7 ignores it.
  • removed-json and removed-defer (breaking, compiler + binding-aware detect).
  • removed-exports, fallback-element, stabilized-options (breaking).
  • generate-path-params (breaking): generatePath rejects null for a required param since 7.14.2. Lago has 8 such sites.

Behavior notes (decision, each with applies() evidence from the inventory). A flag note is omitted when the repository already enabled that flag:

  • relative-splat-path, start-transition, fetcher-persist, normalize-form-method, partial-hydration, skip-action-error-revalidation
  • data-strategy, navigation-promises, platform-minimums, remix-router-package, dom-package, also-review

requiresBuild is not set. @react-router/* has no 6.x release, so it is not declared. No framework mode and no route-structure change.

Ground-truth repositories

Final truth: Arize-ai/phoenix and getlago/lago-front. Expectations were drafted with pnpm packs:truth and the upgrade diff, then reviewed by hand before scoring, and none changed afterwards. Installs ran with scripts off. Nothing was pushed, commented or opened on these repositories.

  • Arize-ai/phoenix
    • app/, pnpm. Parent 4d6c1dfb01e9406cf769efab14f0350ea2d0a0f8, upgrade 3e9dc43bcc686e8ff3d57a028a9fcc15f3935490, 6.25.1 → 7.4.0.
    • 30 expected: 29 dom-imports, 1 dom-entry-imports.
  • getlago/lago-front
    • pnpm, declares only react-router-dom. Parent 84aa05d535edc326a4d104923e4e7293d1be6646, upgrade 6055a5a3c8076522235fe2da452a209e99af8541, 6.30.6 → 7.18.3.
    • 18 expected: 6 future-prop, 8 generate-path-params, 1 removed-exports, 3 generic.

Parents that do not type-check (one rule, applied to all four drafted repositories, RESEARCH.md "Parents that do not type-check"):

  • The rule: run the parent's own project compile (its type-check script and tsconfig). A parent that fails is excluded, unless every error comes from an unbuilt workspace package the upgrade does not touch, shown by building it.
  • Phoenix: tsc --noEmit in app/ reports 0 errors. Kept.
  • Lago: scripts/check-types.js reports 188 errors, all from lago-design-system before it is built. With its declarations emitted as its own build does, the parent compiles clean and so does the upgrade commit. The parent's code against the upgrade's dependencies then reports exactly the truth's 14 compiler sites. Kept, confined.
  • wireapp/wire-webapp (#22299): 1979 errors, including a gitignored generated icon module and 5 in testUtil.tsx, which the upgrade edits. Excluded.
  • open-metadata/OpenMetadata (#31641): 1481 errors, including ones unrelated to any build and one in AppRoot.tsx, which the upgrade edits. Excluded.

Screened out (see RESEARCH.md): feishin (out-of-sync lockfile, and git-hosted dependencies this environment cannot reach), medusa (a 79-file mixed commit), and others with few compiler-visible sites.

uptide pack test output

On this branch, rebased on main 9ba005a with #141 under it:

uptide pack test react-router · verified
fixtures      9 cases, 29 sites · pass
ground truth
  Arize-ai/phoenix@4d6c1dfb01e9                react-router 6.25.1 → 7.4.0  predicted 31, expected 30, false positives 1, false negatives 0
    with react-router-dom 6.25.1 → 7.4.0
  getlago/lago-front@84aa05d535ed              react-router 6.30.6 → 7.18.3  predicted 351, expected 18, false positives 335, false negatives 2
    with react-router-dom 6.30.6 → 7.18.3

  rule                         precision  recall    tp   fp   fn
  dom-entry-imports                 100%    100%     1    0    0
  dom-imports                         8%    100%    29  336    0
  future-prop                       100%     67%     4    0    2
  future-prop-untyped                 0%    100%     0    2    0
  generate-path-params              100%    100%     8    0    0
  generic                           100%     33%     1    0    2
  removed-exports                   100%    100%     1    0    0
  overall (sites)                    12%     96%    46  336    2
  breaking (sites)                  100%     88%    14    0    2

false negatives (2)
  getlago/lago-front@84aa05d535ed  src/components/activityLogs/__tests__/utils.test.tsx:348  generic
  getlago/lago-front@84aa05d535ed  src/core/router/__tests__/Link.test.tsx:94  generic

reported under another rule (2)
  getlago/lago-front@84aa05d535ed  src/test-utils/routerFutureMock.ts:12  future-prop-untyped, expected future-prop
  getlago/lago-front@84aa05d535ed  src/test-utils/routerFutureMock.ts:14  future-prop-untyped, expected future-prop

PASS react-router: verified
  • Breaking: 100% precision, 88% recall (14 TP, 0 FP, 2 FN).
  • The 336 false positives are all deprecated dom-imports moves the upgrades did not make. Lago kept all 335 of its react-router-dom imports, which v7 still serves; Phoenix kept one. fix applies them only with --include-deprecated.
  • Misses. The two breaking misses are Lago's deleted jest mock (routerFutureMock.ts). The pack finds it but files it as future-prop-untyped (deprecated), because the prop goes through untyped createElement and v7 ignores it. The two generic misses are test assertions whose v7 behavior changed (generatePath escaping, Link href normalization). No compiler site and no syntax match exists for those.
  • fix(check): an error the upgrade only rewords is not new #141's effect here: with fix(check): an error the upgrade only rewords is not new #141's change reverted, the same final truth gives identical findings.

What the engine could not express

  1. A workspace whose lockfile sits at the monorepo root cannot be ground truth with directory (UNSUPPORTED_PACKAGE_MANAGER); wire-webapp hit this.
  2. A package the upgrade removed (Phoenix's react-router-dom) cannot be stated in the truth, so it is linked at the target. packs:truth also omits the lines of deleted files.
  3. Fixture keep means neither detected nor rewritten, so a detected-but-declined site needs a rule of its own.
  4. applies() reads the inventory as text, so one router's flag silences its note for the whole repository.
  5. Behavior only tests observe has no finding to attach to.
  6. A parent whose own compile needs a build step is scored unbuilt (scripts off). For Lago, the built-world check shows this hides nothing.

Checklist

  • Every rule and note comes from a source listed in meta.sources
  • Every rule that rewrites or detects has a fixture case, with a site it must leave alone (keep); the rewrite-ready edits are unit-tested
  • Ground truth: public repositories at the commit before their upgrade; expected findings read from their own upgrade commit and the compiler, never copied from Uptide's output
  • At least two public repositories for verified
  • pnpm uptide pack test react-router passes and its output is pasted above; verification.json written with --write
  • pnpm lint && pnpm typecheck && pnpm test pass: full suite on an idle machine with default timeouts gives 2,120 tests, none failing. The lint warning and info are pre-existing on main.
  • Changeset added (.changeset/react-router-seven.md)
  • No private code, names or paths anywhere: fixtures are synthetic, ground truth is public

🤖 Generated with Claude Code

https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY

@lucasbraum
lucasbraum force-pushed the claude/magical-albattani-vkb7nh branch from d2b9699 to a882fa2 Compare October 14, 2026 17:12
@lucasbraum
lucasbraum force-pushed the claude/magical-albattani-vkb7nh branch from a882fa2 to 02280a4 Compare October 14, 2026 17:39
@lucasbraum
lucasbraum changed the base branch from main to claude/alternate-pack-lead October 14, 2026 17:39
lucasbraum added a commit that referenced this pull request Oct 14, 2026
…another (#134)

## What and why

Most React Router 6 applications declare only `react-router-dom`, which
installs `react-router` at its own exact version. On those repositories,
`uptide check react-router` answered "react-router is not a dependency",
so a pack for `react-router` never ran. This blocks the React Router
pack
([#132](#132)),
which will be stacked on this PR. The change is generic: no pack in
`main` declares an alternate lead yet.

A pack can now name `alternateLeads`. These are packages a repository
may declare instead of the pack's own. Each needs an official https
`source`; `pack test` validates it the same way as companions.

**`check`**
- Asked for by an alternate's name (`check react-router-dom`), it uses
the pack and reports under the lead.
- In a workspace that declares only the alternate, it analyzes the lead
at the version installed through the alternate:
  - hoisted (npm, Yarn);
- or, with pnpm, beside the alternate's copy in the store, where the
workspace itself cannot resolve the lead.
- The lead is reported as `undeclared` `via` the alternate, with a note
that `fix` declares it.
- Only packages asked for by name are re-attributed. A pack named after
the alternate wins over one that lists it as an alternate.

**`fix`**
- Asked for by an alternate's name, it fixes the lead.
- Before the upgrade, it declares the lead beside the alternate: same
manifest section, same range style (`^6.30.1` → `^7.18.4`), and sorted
position when the section is sorted.
- The peer/companion preflight plans from the lead installed through the
alternate, so the alternate moves with the lead (release group) in one
install.
- A catalog, `npm:` alias or workspace reference to the alternate is
refused, with the exact text to write by hand.
- Removing the alternate is left to a later upgrade that requires it;
for React Router that is v8.

**CLI**
- `--target <alternate>@<version>` is treated as the lead's target, for
both `check` and `fix`.
- `check` and `fix` accept a lead that is present only through a
declared alternate.
- When neither is declared, the CLI still names what is missing.

## How it was verified

New tests, all on synthetic packages (`router` and a `router-dom` that
re-exports it by name):
- `check/alternate-lead.test.ts`:
- `check router` and `check router-dom` both resolve to the pack, in
hoisted and pnpm-store layouts, with the alternate moving to the target.
- In the hoisted layout, the report equals the one for the same
repository declaring the lead.
- The store cases fail when the stand-in directory fallback is removed
(confirmed by hand).
  - The breaking call is reported when code imports the lead.
  - Without a pack, nothing changes.
- `standInLeads`: only for leads asked for, only when undeclared, never
for `workspace:` specs.
- `fix/alternate-lead.test.ts`:
- `fix router-dom` runs as `fix router`. It declares `"router":
"^2.0.0"` sorted beside `"router-dom": "^2.0.0"`, installs once with
`router-dom` in `also`, commits `chore: upgrade router to 2.0.0 with
router-dom 2.0.0`, applies the rule and verifies.
- `declareLead`: range styles, no-op cases, unsorted sections, and
refusal of a catalog spec.
- `cli/src/alternate-lead.test.ts`: names and `--target` normalized for
`check` and `fix`; still "not a dependency" when neither package is
declared.
- `packs/leads.test.ts` and `pack-test.test.ts`: lead resolution and
`alternateLeads` source validation.

Also ran by hand on scratch npm and pnpm repositories that declare only
`react-router-dom@6.30.1`. Both `check react-router` and `check
react-router-dom` report the v7 removals (`json`/`defer` TS2305,
`react-router-dom/server` TS2307, `future` TS2322) with
`react-router-dom` moving to 7.18.4.

Checks:
- `pnpm lint`: the one remaining warning is pre-existing
(`next/platform.test.ts`).
- `pnpm typecheck` passes.
- `pnpm test`: core 1588 passed and CLI 442 passed.

`docs/packs.md` gains an "Alternate leads" section. A changeset is
included.

## Limits

- A call that reaches the lead only through the alternate's re-export
(`import { navigate } from 'router-dom'`) gets the same findings the
engine already gives when the lead is declared. In the synthetic case
that is none from the type diff; this PR does not change how re-exports
are attributed. The real React Router pack gets compiler findings at
those sites.
- `list` still shows `react-router-dom` as its own row; its "Next"
suggestion `check react-router-dom` now resolves to the pack.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY

---
_Generated by [Claude
Code](https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY)_

Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Base automatically changed from claude/alternate-pack-lead to main October 14, 2026 19:40
The compile signal subtracts baseline errors matched on file, code and
message. A pre-existing error whose message prints a type the upgrade
changes no longer matched, and was reported as a breaking site the upgrade
made. OpenMetadata's sixteen `Property 'backHandler' does not exist` errors
did so once React Router 7's navigate turned `() => void` into
`() => void | Promise<void>` in the elaborated type.

Both programs check the same source text, so an error at the same file,
code and position whose chain of messages is the same once the type
expressions it prints are masked is the same error, and now counts as
pre-existing too. Quoted plain names stay, so a chain that names another
member is still reported: an assignment that already failed on property
`a` and now fails first on `b` ends on the same last message, and is new.
A different reason at the place (one more missing property) is reported
too. Moved lines still match on the full message, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
Every registered pack's ground truth installed at once is about 24 GB,
measured per pack while re-scoring them all; a standard hosted runner
documents 14 GB of disk. Scored one pack at a time with that pack's
repositories evicted before the next, the cache stays under 5 GB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
`pack new` sorted registry imports bytewise, so `./react-router/` came before
`./react/` (`-` sorts before `/`), which biome's organize-imports rejects.
Imports now compare segment by segment. #128 already renders wrapped rows
and parses them back; this adds the ordering and a test that runs biome on
the rendered file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
react-router >=6 <7 to >=7 <8, with react-router-dom as a release-group
member. Sources are the v6 upgrade guide, the v6 future flags page and the
7.x changelogs, pinned to release commits and read 2026-10-14
(react-router/RESEARCH.md classifies every change).

6 to 8 is two upgrades: the v6 guide exists only in the v7 docs, each guide
assumes the previous major's future flags were adopted on that major, and
react-router-dom has no 8.x. This pack is 6 to 7; packs/queue.json now
carries 7 to 8 as its own entry.

Mechanical: react-router-dom imports to react-router (declined where tests
mock react-router-dom by name), react-router-dom/server to react-router, the
v6 `future` prop and router option (inline v6 flag literals only), and
lowercase formMethod comparisons (found by syntax, where the compiler is
blind through any/string). Assisted: RouterProvider's react-router/dom entry,
json, defer, removed type exports, fallbackElement, stabilized options.
Behavior notes for the six flags that become defaults apply only where the
repository's router setup has not enabled them, with files and lines.

Verified against Arize-ai/phoenix and wireapp/wire-webapp: breaking
precision 100%, recall 50%; overall precision 58%, recall 97%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
…writes assisted

The react-router pack names react-router-dom as its alternate lead (#134), so
it runs for the usual v6 setup that declares only react-router-dom. The
rewrites of dom-server-entry, form-method-case and future-option go back to
assisted until a ground-truth repository exercises them; their detection is
unchanged and the edits stay unit-tested for when one does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
…sitories

Two more public React Router 6 to 7 upgrades, both from repositories that
declare only react-router-dom and so reach the pack through its alternate
lead: getlago/lago-front (18 expected sites: future props, the removed
FutureConfig type, generatePath null params, a default import) and
open-metadata/OpenMetadata (2 future props its upgrade left, which its
compiler rejects). Expectations were drafted with packs:truth and the
upgrade diffs and read before scoring; RESEARCH.md and the ground truth's
comment record every inclusion and exclusion, including sixteen
OpenMetadata errors a real install shows were already there.

Two rules from the sources: generate-path-params (7.14.2 rejects null for a
required param; assisted, the choice goes to decisions) and
future-prop-untyped (a syntactic detect for the v6 future prop passed
through untyped createElement props, which the compiler cannot see).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
One rule for every drafted repository: the parent's own project compile
(its type-check script and tsconfig) must pass, or fail only because of an
unbuilt workspace package the upgrade does not touch, shown by building it.

Phoenix compiles clean. Lago's 188 errors all come from lago-design-system
before it is built; built, the parent and the upgrade both compile clean and
the parent's code against the upgrade's dependencies reports exactly the
fourteen compiler sites in the truth. wire-webapp (a generated icon module,
errors in a file the upgrade edits) and OpenMetadata (errors unrelated to any
build, one in a file the upgrade edits) are left out.

With them goes the only real-world site of the future-prop rewrite, so it
joins the rewrites kept assisted until ground truth exercises one; the edit
stays unit-tested. Re-verified on Phoenix and Lago: breaking precision 100%,
recall 88% (14 TP, 0 FP, 2 FN).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY
Signed-off-by: Claude <noreply@anthropic.com>
@lucasbraum
lucasbraum force-pushed the claude/magical-albattani-vkb7nh branch from 02280a4 to 9b775e5 Compare October 14, 2026 21:53
@lucasbraum
lucasbraum changed the base branch from main to claude/check-preexisting-position October 14, 2026 21:54
@lucasbraum
lucasbraum marked this pull request as ready for review October 14, 2026 21:54
lucasbraum added a commit that referenced this pull request Oct 15, 2026
Based on main. #132 (the react-router pack) stacks on this one.

## What and why

Signal B (`compile.ts`) subtracts the baseline's errors from the
target's, matched on file, code and full message. A pre-existing error
whose message *prints a type the upgrade changes* no longer matched. It
was then reported as a breaking site the upgrade made. Real case:
OpenMetadata's sixteen `Property 'backHandler' does not exist on type
'IntrinsicAttributes & object & …'` in
`EntityVersionPage.component.tsx`. All sixteen exist at the parent: a
real scripts-off install of `react-router-dom@7.18.1` and the
repository's own `tsc` show them. Only their wording changes, because
React Router 7's navigate turns `() => void` into `() => void |
Promise<void>` in the elaborated type. `check react-router` reported one
of them as a breaking false positive.

The fix: an error at the **same file, code and position** whose **chain
of messages is the same once the type expressions it prints are masked**
is pre-existing. Both programs check the same source text, so a position
names one construct.

- Masking replaces a quoted printed type expression (`'{ backHandler: ()
=> void; … }'`, `'IntrinsicAttributes & object'`) with `'…'`.
- A quoted plain name (a property, a parameter, a named type such as
`'Opts'`) is kept. So the chain still says *which* member fails.
- Moved lines still match on the full message, as before.

**Can it hide a real new error?** (review item 3) Not when the chain
says something different. Two cases are tested, each at the same file,
code and position as a baseline error:

1. **Same last message, another symbol.** `const opts: Opts = value`
already fails on property `a`; the target also breaks `b` and checks it
first. tsc reports one error per assignment, so the chain differs only
in `Types of property 'b' are incompatible`. Its last message (`Type
'string' is not assignable to type 'number'`) is the same. A first
version of this fix matched on the last message alone and **hid it**:
that is the third test, and it fails on that version. The full masked
chain reports it.
2. **A different reason.** `{}` missed `a, b` and now misses `a, b, c`.
The reason differs, so it is reported.

The remaining blind spot is inherent to tsc and was already there with
the full-message key. If the upgrade adds a second incompatibility
*after* the first one tsc reports, tsc prints the same error and says
nothing about the second.

## How it was verified

Failing first, each test against the code without its part of the fix:

| Test (`compile.test.ts`) | main (full-message key) | Position + last
message (first draft) | This PR |
| --- | --- | --- | --- |
| does not report a pre-existing error as new because the upgrade
rewords it | **fails** (reported as new) | passes | passes |
| reports a different reason at the place of a pre-existing error |
passes | passes | passes |
| reports a new error at the place of a pre-existing one that ends on
the same reason | passes | **fails** (hidden) | passes |

(The second test fails against a position-only key, which was the
version before the reason was added.)

Full suite (`pnpm test`) on an idle machine with default timeouts, on
#132's branch (which contains this one): 2,120 tests pass (CLI 452; core
1,668, 32 skipped), none fail.

### Every pack, re-scored

Every pack registered on main, including `@sentry/react` (#135): `pnpm
uptide pack test <pack> --json` on main `578a042` (before) and on this
PR (after). Both sides ran on the same freshly fetched repositories
(scripts off, the global pnpm, corepack for Yarn Berry only), and each
pack's repositories were evicted before the next. Status, then TP/FP/FN
for breaking and overall.

Main moved by #139 after the re-score (not analysis-related: an opt-in
candidate flag and its warning label; `pack test` passes its own packs
to `check`, so scoring is unchanged). This branch is rebased on it; CI's
Pack test re-checks at merge.

| Pack | Before (main) | After (this PR) | Change |
| --- | --- | --- | --- |
| ai | verified; breaking 9/0/2; overall 54/0/7 | verified; breaking
9/0/2; overall 54/0/7 | none (same predicted findings) |
| lucide-react | verified; breaking 11/0/0; overall 11/0/0 | verified;
breaking 11/0/0; overall 11/0/0 | none (same predicted findings) |
| next | verified; breaking 11/0/2; overall 12/0/3 | verified; breaking
11/0/2; overall 12/0/3 | none (same predicted findings) |
| react | verified; breaking 89/0/40; overall 92/6/40 | verified;
breaking 89/0/40; overall 92/6/40 | none (same predicted findings) |
| sentry-browser | verified; breaking 3/0/0; overall 3/0/0 | verified;
breaking 3/0/0; overall 3/0/0 | none (same predicted findings) |
| sentry-nextjs | verified; breaking 8/0/0; overall 8/0/0 | verified;
breaking 8/0/0; overall 8/0/0 | none (same predicted findings) |
| sentry-react | verified; breaking 2/0/0; overall 2/0/0 | verified;
breaking 2/0/0; overall 2/0/0 | none (same predicted findings) |
| stripe | verified; breaking 18/0/2; overall 18/0/2 | verified;
breaking 18/0/2; overall 18/0/2 | none (same predicted findings) |
| vitest | verified; breaking 16/0/16; overall 16/18938/16 | verified;
breaking 16/0/16; overall 16/18938/16 | none (same predicted findings) |
| zod | verified; breaking 59/0/21; overall 120/0/21 | verified;
breaking 59/0/21; overall 120/0/21 | none (same predicted findings) |

**No pack changes.** The fix only matches an error that is already in
the baseline at the same place with the same masked chain. No registered
pack's ground truth has such a pair; the case came from OpenMetadata, a
react-router candidate repository later excluded (#132's RESEARCH.md).
vitest's 18938 and react's 6 overall false positives are non-breaking
findings (both have 0 breaking false positives), identical on both
sides. On #132's branch, react-router scores the same with and without
this fix on its final truth.

**Cache size** (review item 4, now in docs/packs.md): measured per pack
while re-scoring, the largest are react and @sentry/nextjs at 4.3 GB
each. All packs at once is about 24 GB, against the 14 GB of disk a
standard hosted runner documents. Evicting per pack keeps it under 5 GB.

## Checklist

- [x] Tests for the change (a bug fix starts with a failing test)
- [x] `pnpm lint && pnpm typecheck && pnpm test` pass
- [x] README and docs updated if behavior changed (docs/packs.md:
ground-truth cache size and per-pack eviction)
- [x] Changeset added if a user would notice (`pnpm changeset`)
- [x] No new dependency, or it was agreed in an issue
- [x] No private code, names or paths in fixtures, tests or docs
- [x] The rules in CONTRIBUTING.md ("Rules that do not bend") still hold

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY

---
_Generated by [Claude
Code](https://claude.ai/code/session_01EV6fvvLupC2Hdbwcg7iovY)_

---------

Signed-off-by: Claude <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
Base automatically changed from claude/check-preexisting-position to main October 15, 2026 00:08
@lucasbraum
lucasbraum merged commit 107c107 into main Oct 15, 2026
10 checks passed
@lucasbraum
lucasbraum deleted the claude/magical-albattani-vkb7nh branch October 15, 2026 00:09
@uptide-release uptide-release Bot mentioned this pull request Oct 15, 2026
lucasbraum added a commit that referenced this pull request Oct 15, 2026
…ops reading it stale

`pack test` compares the recorded verification.json with the run byte for
byte. The react-router record from #132 predates the truePositives,
falseNegatives and behavior fields, so every full run read it as stale and
the Packs workflow failed, on main since 883e587 too. Re-recorded with
`uptide pack test react-router --write`: status, ground-truth digest,
precision (1), recall (0.875) and false positives (0) are unchanged; only the
counts are added. Ground truth untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
lucasbraum added a commit that referenced this pull request Oct 15, 2026
## Pack

zod `>=3 <4` → `>=4 <5` (verified pack). Closes UPT-26.

`ZodError#errors` was an alias of `.issues` and is gone in zod 4
(https://zod.dev/v4/changelog#drops-errors, already in `meta.sources`).
Every typed read of it went to the agent.

## Rules

- **`error-issues`** (new, breaking, mechanical). It claims the type
diff's own finding, `removed ZodError#errors`. That finding exists only
where the checker resolved the member's declaration on the receiver's
type, so the rule never matches by name. It covers inferred
`result.error`, `SafeParseError['error']`, `ZodError<T>` and local
aliases of them.
- Reads: `e.errors[0].message`, `e?.errors?.length`, `e!.errors`. Only
the member name changes, to `issues`.
- Destructuring: `const { errors } = e` becomes `const { issues: errors
} = e`, and `{ errors: list = [] }` becomes `{ issues: list = [] }`. The
local names stay as they were.
- Declined, so they stay with the agent: assignments, `delete`,
`++`/`--`, rest elements, and quoted or computed property names.
- **`error-members`** (the untyped-catch path from #127). `.errors` read
off an untyped catch binding is now renamed too, under the same gate as
`formErrors`: the rewrite re-runs detection, so it only edits a read off
a binding caught from a zod `parse`/`parseAsync`. A `JSON.parse` try,
writes, and `instanceof`-narrowed bindings are still declined.

## Verification

### `uptide pack test zod`

```
fixtures      8 cases, 20 sites · pass
  fixtures/repos/storefront                    predicted 38, expected 38, false positives 0, false negatives 0
  ugurkocde/AwesomeIntune@dcf38603fd55         predicted 13, expected 15, false positives 0, false negatives 2
  howardyang2009/PATH@bb171417d911             predicted 40, expected 51, false positives 0, false negatives 11
  band-ai/band-sdk-typescript@5d9a66a1dde4     predicted 29, expected 37, false positives 0, false negatives 8

  rule                         precision  recall    tp   fp   fn
  error-issues                        0%     n/a     0    2    0
  error-params                      100%     89%    24    0    3
  generic                            80%     73%    32    8   12
  ...
  overall (sites)                   100%     85%   120    0   21
  breaking (sites)                  100%     74%    59    0   21
PASS zod: verified
```

Breaking precision stays at 100% and recall is unchanged (59/0/21, the
same as `main`), so `verification.json` is unchanged.

**Decision for you: the `error-issues 0% / 2 fp` row.** These are two
AwesomeIntune sites (`src/app/api/subscribe/route.ts:23`,
`src/app/api/v1/keys/route.ts:30`). They are still true positives at the
site level, but the ground truth files them under `generic` because no
rule claimed them when it was written. They are now listed under
"reported under another rule". I did not relabel the ground truth, since
this PR's own scoring is what motivates the change. If you want the
per-rule row to read 100%, relabel those two entries to `error-issues`
in a separate commit.

### Fixtures

New case `fixtures/error-issues`. It has the papermark shape
(`validation.error.errors[0].message`), optional chaining, destructuring
with and without a rename, a write marked `keep`, and an unmarked
non-zod `form.errors`.

### Real type diff

I ran `check` on a scratch repo with zod 3.25.76 installed and the
fixture's code. All four typed shapes come back as `removed
ZodError#errors` and are mechanical. The write stays manual, and the
non-zod `form.errors` is not reported.

### papermark/papermark @ed19717

This was run locally on a shallow clone with lifecycle scripts off.
Nothing was pushed or posted.

- `check`, `main` vs this branch: **34 sites move from agent to
mechanical** (`ZodError#errors` 34 agent → `error-issues` 34 by rule).
No other group changes.
- I applied all 34 rewrites (25 files; each line changes only `errors` →
`issues`), installed zod 4.6.5 with `--no-save --ignore-scripts`, and
ran `tsc --noEmit`. None of the 34 lines has an error.
- tsc still reports 6 `ZodError.errors` reads in 3 files that `check`
does not report on `main` either. Example:
`pages/api/teams/[teamId]/documents/index.ts:412`. Those files never
import zod themselves; the schema comes from `@/lib/zod/...`, and the
call-site finder only follows a non-distinctive member like `errors` in
importing files. That detection gap predates this PR and is out of scope
here.

### Suite

`pnpm lint`, `pnpm typecheck` and `pnpm docs:packs --check` pass. The
full `@uptide/core` suite has 3 failures, all test timeouts in
`fix/assisted.test.ts` / `pack-test.test.ts`, with a different set on
each run. Both files pass in isolation (30/30), and `origin/main` shows
the same 3 timeouts in a full run on this machine.

## Also: Packs workflow fix (react-router)

The Packs check failed on react-router, not zod, and it has failed on
`main` since 883e587. `pack test` compares `verification.json` with the
run byte for byte, and the react-router record from #132 had no
`truePositives`/`falseNegatives`/`behavior` fields. 5b861d0 re-records
it with `uptide pack test react-router --write`. Status, digest,
precision 1, recall 0.875 and fp 0 are unchanged; only the counts are
added, and ground truth is untouched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
lucasbraum pushed a commit that referenced this pull request Oct 15, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and publish to npm
yourself or [setup this action to publish
automatically](https://github.com/changesets/action#with-publishing). If
you're not ready to do a release yet, that's fine, whenever you add more
changesets to main, this PR will be updated.


# Releases
## uptide@0.13.0

### Minor Changes

- [#134](#134)
[`b795705`](b795705)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Packs can name
alternate leads: packages a repository declares instead of the pack's
own (`react-router-dom` for `react-router`). `check` and `fix` asked for
either name use the pack; where only the alternate is declared, the lead
is analyzed at the version installed through it, pnpm stores included,
and `fix` declares it beside the alternate at the target before
upgrading both.

- [#149](#149)
[`3303118`](3303118)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a candidate
express 4 to 5 migration pack. Route strings that path-to-regexp 8
rejects or reads differently are found on calls of an Express app or
router (including lists registered in a loop); `req.params` values
widened to `string | string[]`, `req.param`, `res.sendfile`,
`express.static.mime` and `express.query` are claimed from the compiler;
`res.redirect(url, status)`, `app.param(fn)`, `res.redirect('back')` and
`res.send(<number>)` are found in the source; runtime defaults (async
errors, `req.body`, the query parser, `urlencoded`, dotfiles, Node 18)
go to decisions. No rule rewrites. With the pack in use, `check` no
longer adds its text-matched express 5 runtime findings.

- [#148](#148)
[`5143c46`](5143c46)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a candidate
joi 17 to 18 migration pack. `new ValidationError(message, {},
original)`, which stops compiling for code written against joi
17.0–17.6, is rewritten to pass `[]`; the Node.js 20 requirement and the
top-level domains joi 18 no longer accepts in `email()`, `domain()` and
`uri()` are reported as decisions. Scored against two public
repositories, one of them a negative control.

- [#131](#131)
[`413d3da`](413d3da)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a verified
lucide-react 0.x to 1.x migration pack. Brand icons removed in 1.x are
found at their imports and every use, with a guide whose replacement
choice goes to the owner; icon names dropped during 0.x are renamed at
the import or member; deep `dist/` imports, untyped icon lookups, the
`absoluteStrokeWidth` and `IconNode` deprecations, and the `aria-hidden`
default for repositories before 0.489.0 are reported. Lucide's lockstep
packages move together and the consumer's build is required. Verified
against three public repositories, one of them a negative control.

- [#132](#132)
[`107c107`](107c107)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a verified
React Router 6 → 7 migration pack, which also runs for applications that
declare only `react-router-dom`. It moves `react-router-dom` imports to
`react-router`. It reports, with a guide for the agent, the rest of the
upgrade: the removed `future` prop and router option,
`react-router-dom/server`, lowercase `formMethod` comparisons and
`json`/`defer` calls (found by syntax where the compiler is blind), the
other removed exports, and `generatePath` calls that pass `null` for a
required parameter. The six v6 future flags that become defaults are
reported as decisions where the repository's router setup has not
enabled them. React Router 7 → 8 is a separate upgrade.

- [#142](#142)
[`9c57934`](9c57934)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` and `fix`
plan what the target version requires: a package the target, or anything
moving with it, declares as a peer or dependency at a range the
installed copy rejects moves to the lowest release of its major that
satisfies it (openai 7 moves zod 3.24.2 to 3.25.0), shown apart from
release-group companions ("moved because openai 7.4.0 requires zod ^3.25
|| ^4.0"). A move only a new major makes is never made: it is reported
as a blocker with the exact range in `check`, the PR body and `pack
test`. The npm install scope guard no longer lets a declared root
dependency change because a moved package peers on it.

- [#128](#128)
[`06f11ab`](06f11ab)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add verified
Sentry JavaScript SDK 10 to 11 migration packs for `@sentry/nextjs` and
`@sentry/browser`, with the SDK's lockstep release group, a required
build for Next.js, and inventory-scoped decisions for data collection,
span streaming, sampling and platform minimums. Mechanical rules move
`withSentryConfig` to `@sentry/nextjs/config` and the removed build
options under `webpack`, remove `enableLogs`/`enableMetrics: true`, and
rename exports, integration names and `httpIntegration` options;
privacy, sampling, profiling and OpenTelemetry choices stay assisted and
are reported for the owner. Fix `uptide pack new` dropping registry
entries long enough to be wrapped.

- [#155](#155)
[`922c5ab`](922c5ab)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide pack
test` applies a stricter verified gate: at least five expected breaking
sites across the public ground-truth repositories, besides two
repositories and no breaking false positive. Upgrades the compiler
cannot see are gated on their behavior notes instead. Mechanical rules
no real site exercises are recorded as unexercised, and `check` and
`fix` run their sites as assisted. `@sentry/browser` and `@sentry/react`
become candidates (fewer than five real breaking sites). A pack can
lower the minimum only with a written reason in its RESEARCH.md. The
README packs table shows each pack's breaking-site count. The output
shows the gate's class, site counts and every reason a pack is a
candidate.

- [#147](#147)
[`0b88170`](0b88170)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` and `fix`
remove a deprecated `@types` stub instead of upgrading to it. When the
release the upgrade would move `@types/<pkg>` to is deprecated on npm,
and `<pkg>`'s target declares its own types (`types`, `typings` or a
`types` export), the plan removes it: "Removed: @types/ioredis, ioredis
5 ships its own types", in `check` and in the PR body. `check` now diffs
such a package against its own target types instead of failing on a stub
with no declarations, and `fix` removes the entry in the same install,
with the install scope guard allowing exactly that removal.

- [#150](#150)
[`4c3212b`](4c3212b)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check
typescript` judges a TypeScript 5 to 6 upgrade with both compilers:
every project of the workspace is compiled with the installed TypeScript
and with the target's, and what is new at the target is a finding; the
bundled compiler never stands in, and a target without a JavaScript API
(TypeScript 7) is reported as not compiled. A candidate typescript 5 to
6 pack reports the tsconfig options 6.0 deprecates where they are set,
the rootDir default, module-keyword namespaces and import assertions. A
peer that rejects the target (typescript-eslint on TypeScript 6) is now
reported as a peer conflict even when it is not grouped with the
package.

- [#137](#137)
[`21894d7`](21894d7)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add
`verify.build` to uptide.yml: the package.json script a migration pack's
required build runs. Unset, Uptide runs `build:ci`, then `build-ci`,
then `build`. When a build fails before the framework's build starts
because it needs an environment variable or a database (`prisma db push`
without `DATABASE_URL`), the pull request and the report say so ("build
needs DATABASE_URL; set verify.build") instead of a bare "build failed".
`uptide init` writes the setting as a comment.

### Patch Changes

- [#141](#141)
[`9debb6d`](9debb6d)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` no longer
reports a pre-existing type error as a breaking site when the upgrade
only changes its wording, such as a type the message prints in full. An
error at the same place with a different reason is still reported.

- [#129](#129)
[`e796e4c`](e796e4c)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a candidate
ioredis 5 to 6 migration pack: client constructions that inherit the
RESP3, reconnect and keep-alive defaults ioredis 6 changed,
compiler-blind sites (`mode === 'subscriber'`, commands in a `connect`
listener with the offline queue off, AUTH recognized by command name),
dependents that bundle ioredis 5, instrumentation that stops patching
ioredis 6, and Node 20. It is scored against five public upgrades and
stays a candidate: none of them has a breaking site to verify it with.

- [#139](#139)
[`9ba005a`](9ba005a)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Allow local check
and fix smoke tests of registered candidate packs with
--allow-candidate, with an explicit unverified banner. Reject candidate
opt-in in the Action and with publication flags, and prevent stored
candidate runs from being published. Label pack-claimed assisted sites
by rule id even with --no-llm.

- [#157](#157)
[`f3af6a1`](f3af6a1)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` no longer
reports a blocker for a direct dependency whose plain dependency range
rejects the target (`@azure/ai-projects` on `openai ^6`, verdaccio
pinning express 4): the package manager gives it its own nested copy, so
it is listed as staying, with the new major that would accept the target
when there is one. `pack test` counts a `with` move that `check` leaves
and explains (a blocker, a new-major stay or a peer conflict) as
explained, and one of a package that does not depend on the lead as the
repository's choice; neither fails the run.

- [#130](#130)
[`5ced450`](5ced450)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a candidate
openai 4 to 7 migration pack: named path parameters, `del` → `delete`,
promoted beta chat helpers, renamed runner events, removed entry points
and request-option overloads are rewritten where unambiguous; tool-call
unions, `httpAgent`, `fileFromPath` and the other removals get agent
guides; `APIError.headers`, Web `Response` bodies and double-encoded
path ids are detected even in code typed `any`; Node 22, zod, fetch and
client-subclass decisions carry repository evidence. It stays a
candidate (the dependency is still checked and fixed as generic) until
two engine gaps its ground truth exposed are closed.

- [#136](#136)
[`578a042`](578a042)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Fold implicit-any
errors (TS7006, TS7031, TS7019) inside an option the target removed into
the pack finding on that option, as downstream evidence: the callbacks
lost the types the option gave them, and the same edit resolves them.
They no longer stand as separate breaking sites.

- [#143](#143)
[`883e587`](883e587)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide pack
test` scores each expected site at the severity of the rule or behavior
note it belongs to, so a missed note that only needs a look, or a
decision, no longer counts as a missed break. Behavior-note sites get
their own precision and recall in the output and in `verification.json`
(`behavior`, with the counts behind each rate), false negatives show
their severity, and a rate with nothing to measure prints `n/a` instead
of 100%, in `pack test` and in the README's packs table.

- [#154](#154)
[`7aa3d58`](7aa3d58)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide pack
test` fails a pack's `verification.json` only when it claims more than
the run measures (recorded verified, measured candidate). A pack
recorded as candidate on purpose passes, with `recorded candidate by
choice; measures verified`, and a record whose numbers differ gets a
note instead of a failure. A `generic` ground-truth site now counts as a
true positive for the rule that claims it.

- [#152](#152)
[`ca31aee`](ca31aee)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide pack
test` takes several packs: `uptide pack test zod ai`.

- [#159](#159)
[`1e157fd`](1e157fd)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - `uptide pack test
--json` times each ground-truth repository: `timing` gives the fetch,
install and check wall time and the process's peak memory.

- [#144](#144)
[`0881185`](0881185)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Ground truth can
name a site for an upgrade that only adds lines inside an existing call,
such as an option added to a constructor: the site is the call's start
line, so a pack finding there scores as a true positive instead of a
false positive. `pnpm packs:truth` drafts these anchors from insert-only
hunks for review, and docs/packs.md and CONTRIBUTING describe the rule.

- [#156](#156)
[`9039e3c`](9039e3c)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Public ground
truth now exercises three rules that ran as assisted: zod
`error-params`, stripe `api-version-unpinned` and next
`stable-cache-exports`. Their rewrites run mechanically again. All three
packs stay verified.

- [#138](#138)
[`0db5640`](0db5640)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Respect
repository tsconfig file selection, recursive project references, and
workspace project options when checking dependency upgrades. Compiler
diagnostics outside every TypeScript project no longer produce breaking
findings; pack rules and build-configuration findings retain their own
scope. Repositories without a tsconfig retain importer-based
compilation, with that fallback stated in the report.
  
Anchor missing members supplied by dependency augmentations at their
setup import, retaining affected calls as evidence instead of counting
each call as a separate breaking edit.

- [#151](#151)
[`9ddaaff`](9ddaaff)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Add a candidate
redis (node-redis) 4 to 5 and 5 to 6 migration pack, one rule set per
major: an upgrade gets the rules of the majors it crosses, so 4 to 6
gets both. 4 to 5 covers `legacyMode`, `commandOptions` and isolation,
scan iterators that yield batches, string SCAN cursors, boolean replies
that became numbers, enums that became constants, `keepAlive`,
`@redis/graph` and internal `dist` imports; 5 to 6 covers the RESP3
client type, `unstableResp3`, and the client defaults 6 changes (RESP3,
command timeout, keep-alive delay, maintenance notifications) with a
decision per client. Scored against five public upgrades; it stays a
candidate until the stricter verified gate lands, since the core 4 to 5
API changes have no real site yet.

- [#135](#135)
[`42a3b54`](42a3b54)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Move only
packages released with the upgraded one. A pack's declared release group
now bounds the lead's npm scope, and without one a same-scope package
moves only when a pin, a peer or a release at the target relates it to
the group. Packages of the scope that are versioned on their own
(`@sentry/vite-plugin` 5.x, `@sentry/cli`, the old `@sentry/utils` 8.x)
are reported as "stays at X" in `check`, the pull request and `pack
test` instead of making `fix` refuse the whole upgrade. Registers the
verified `@sentry/react` 10 to 11 pack.

- [#146](#146)
[`09b8682`](09b8682)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Report Stripe
clients pinned to different API versions at the top of `check` and of
the PR body, as a decision for the owner: every `new Stripe(...)` in
TypeScript and JavaScript sources, scripts included, with the version it
speaks after the upgrade (an unpinned client speaks the SDK default).
The PR's decisions list, for each client behind the target, the
changelog entries it would cross for the resources it calls. No pin is
rewritten.

- [#140](#140)
[`bc428d1`](bc428d1)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - Keep migration PR
bodies within their size budget while preserving summaries,
representative diffs, decisions, review warnings and verification ahead
of changelog and transitive detail. Keep every evidenced Stripe change
visible and uncapped; group and cap only other changes to used
resources, link the full changelog, and identify omitted text in the
retained report.

- [#145](#145)
[`3c8902d`](3c8902d)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - The zod pack
migrates `ZodError#errors` to `.issues` by rule: reads
(`error.errors[0].message`, `error?.errors`) and destructuring (`const {
errors } = error` becomes `const { issues: errors } = error`) where the
type checker resolves the receiver to `ZodError`, and reads off an
untyped catch binding of a zod parse. Writes are left to the agent.

- [#127](#127)
[`c4ead4f`](c4ead4f)
Thanks [@lucasbraum](https://github.com/lucasbraum)! - zod: `check` now
reports `ZodError#formErrors` and `#errors` read off an untyped catch
binding of a zod `parse`/`parseAsync` (`catch (error: any) { return
error.formErrors.fieldErrors }`), which zod 4 removed and the compiler
cannot see. Files that only import a schema from the repository's own
module (`@/lib/zod`) are covered too. `fix` rewrites
`<err>.formErrors.fieldErrors` and `<err>.formErrors.formErrors` to
`z.flattenError(<err>)`, importing `z` where it is missing; every other
read goes to the agent.

Co-authored-by: uptide-release[bot] <338861287+uptide-release[bot]@users.noreply.github.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

Development

Successfully merging this pull request may close these issues.

2 participants