Skip to content

feat(packs): candidate redis (node-redis) 4 → 5 and 5 → 6 pack (UPT-58) - #151

Merged
lucasbraum merged 5 commits into
mainfrom
lucasbraum/upt-58-pack-redis-node-redis-4-5-and-5-6
Oct 15, 2026
Merged

lucasbraum merged 5 commits into
mainfrom
lucasbraum/upt-58-pack-redis-node-redis-4-5-and-5-6

Conversation

@lucasbraum

@lucasbraum lucasbraum commented Oct 15, 2026 •

Copy link
Copy Markdown
Collaborator

Pack

redis (node-redis), >=4 <6 → >=5 <7, for UPT-58. ioredis is a separate pack (#129).

One pack, two rule sets. 4 → 5 and 5 → 6 are separate rule sets, and an upgrade gets the sets of the majors it crosses: 4 → 6 gets both, 5 → 6 never sees a 4 → 5 site. It is one pack and not two because pack lookup is one pack per package, and because 4.7 → 6.x in one step is the most common real upgrade (Dependabot offers it to every 4.x user; 3 of the 6 upgrades found did it). The gate is appliesTo in index.ts: detection, ruleOf and notes check the crossed majors, and transform refuses a 4 → 5 rewrite on code already on 5.

Status: candidate. verification.json records candidate; since #154 pack test accepts a record that claims less than the run measures.

Sources

Pinned at node-redis b64bacb, read 2026-10-14: docs/v4-to-v5.md, docs/v5-to-v6.md, docs/RESP.md; release notes redis@5.0.0 and redis@6.0.0; published declarations of @redis/client 1.6.1 / 5.12.1 / 6.3.0, @redis/search and @redis/time-series. Release group: the five @redis/* packages redis pins at its own version. @redis/graph is gone in 5.

Rules and behavior notes

4 → 5

  • legacy-mode: compiler + detect; assisted (client.legacy()).
  • command-options: commandOptions(), isolated, executeIsolated; compiler + detect; assisted (withCommandOptions, createClientPool).
  • scan-iterators: detect only; rewrite to a nested loop, declined when the body breaks/continues the loop.
  • keep-alive: compiler + detect; rewrite keepAlive: N → keepAlive: true, keepAliveInitialDelay: N.
  • scan-cursor: string cursors; compiler + detect (cursor !== 0 never ends in JS); assisted.
  • number-replies: boolean → number replies; compiler; assisted.
  • renamed-exports: 17 enums → constants, SearchOptions → FtSearchOptions; rewrite: import under the old local name, declined when the file uses a member whose name or value changed (compared member by member in the declarations) or uses the name as a type.
  • deep-imports: @redis/*/dist internals; compiler only; assisted.
  • graph-removed: compiler + detect; assisted, a decision for the owner.
  • quit-disconnect (deprecated): rewrite to close()/destroy(), only where the result is unused.
  • set-expiration (deprecated): EX/PX/… → expiration; assisted. Not in the guide; from the declarations.
  • Notes: search-dialect-2 (finding at query calls without DIALECT), pipeline-discards-unwritten, node-18 (decisions).

5 → 6

  • client-type: RedisClientType is invariant in RESP and clients default to RESP3, so a client typed RedisClientType / ReturnType<typeof createClient> breaks. Not in the guide; seen in all four real upgrades that reached 6; assisted.
  • unstable-resp3: compiler + detect; rewrite: delete the option.
  • sentinel-node-command-options: compiler; assisted.
  • Notes: client-defaults (finding + decision at each inline-options client that leaves RESP, command timeout, keep-alive delay or maintNotifications to the 6 default; the instructions pin the 5 defaults per client, as the ioredis pack does), node-20, plain-object-replies.

Ground-truth repositories

Drafted with pnpm packs:truth (each repository's own compiler and merged upgrade), reviewed by hand, and committed in ffb4f90 before the first score (71a2172 before the rebases on main).

Repository Hop Pinned Upgrade Sites
deanmoses/tacocat-gallery-sam (app) 4.6.12 → 5.10.0 34e1176 PR #83 4
swoofer/mcp-coordinator 5.12.1 → 6.2.0 8b337f0 PR #251 2
deanmoses/tacocat-gallery-sam (app) 5.12.1 → 6.2.1 30724e2 PR #133 2
UNN-Devotek/FCM-Fallout-Chat-Mod (backend) 4.7.1 → 6.0.0 2801904 PR #125 9
tearingItUp786/blog 4.7.0 → 6.0.0 bb88f3a #223 + 092ea34 5

One change after the first score: FalkorDB/falkordb-ts (4.7.1 → 5.10.0, 20 sites, the richest 4 → 5 upgrade found) was in the committed truth, but the first run could not score it at all. Its package.json declares workspaces: ["./packages/*"] matching no directory, and check refuses the repository. It is removed (437e129), and none of its sites was ever scored. Also screened out: stamp (needs Node ≥ 24.15), semaphore-chat (lockfile outside the project directory), about 30 bump-only upgrades. Details in RESEARCH.md.

uptide pack test output

uptide pack test redis · verified, recorded candidate
fixtures      12 cases, 27 sites · pass
ground truth
  deanmoses/tacocat-gallery-sam@34e11760f6e7   redis 4.6.12 → 5.10.0  predicted 4, expected 4, false positives 0, false negatives 0
  swoofer/mcp-coordinator@8b337f0a081d         redis 5.12.1 → 6.2.0  predicted 3, expected 2, false positives 1, false negatives 0
  deanmoses/tacocat-gallery-sam@30724e21efd6   redis 5.12.1 → 6.2.1  predicted 2, expected 2, false positives 0, false negatives 0
  UNN-Devotek/FCM-Fallout-Chat-Mod@280190489fac redis 4.7.1 → 6.0.0  predicted 5, expected 9, false positives 0, false negatives 4
  tearingItUp786/blog@bb88f3afc40d             redis 4.7.0 → 6.0.0  predicted 1, expected 5, false positives 1, false negatives 5

  rule                         precision  recall    tp   fp   fn
  client-defaults                     0%      0%     0    4    2
  client-type                       100%     33%     2    0    4
  generic                            50%    100%     2    2    0
  renamed-exports                   100%    100%     2    0    0
  scan-cursor                       100%     30%     3    0    7
  overall (sites)                    87%     59%    13    2    9
  behavior notes (sites)             50%     50%     2    2    2
  breaking (sites)                  100%     61%    11    0    7

false positives (2)
  swoofer/mcp-coordinator@8b337f0a081d  src/infra/redis.ts:50  client-defaults (deprecated)
  tearingItUp786/blog@bb88f3afc40d  app/utils/redis.server.ts:15  client-defaults (deprecated)

✗ verification.json does not match this run: `uptide pack test redis --write` records it
FAIL redis

Why the record says candidate. Today's gate (≥ 2 repos, 0 breaking false positives) computes verified, and the stricter UPT-48 gate is not on main yet. The evidence is thin exactly where it matters most. The core 4 → 5 API changes (command-options, legacy-mode, scan-iterators, number-replies, keep-alive) have no real site in the ground truth. The 4 → 5 rule set rests on one 4 → 5 repository (4 sites) plus the scan-cursor sites of the two 4 → 6 repositories. Three rewrites (scan-iterators, keep-alive, unstable-resp3) never met a real site, and under UPT-48 they would run as assisted. So I have not run --write, and CI's Pack test will flag the record as stale until this is decided: rebase on UPT-48 and re-score, or find a scorable 4 → 5 upgrade with those changes.

The 7 breaking false negatives are all outside the rules' reach:

  • blog (5): its files reach the client through a local module and a local RedisScanClient type, so check reports no compile error there.
  • FCM and tacocat 5 → 6 (4 client-type): the repositories' own tsc reports TS2322, check doesn't. Those lines already had a different TS2322 at the installed version, so they were most likely subtracted as pre-existing. Not confirmed in the engine.

Behavior: the two client-defaults false positives are clients whose repositories kept the RESP3 default without pinning it.

Engine follow-ups found here (not in this PR)

  • check refuses a repository whose workspaces patterns match nothing (FalkorDB).
  • Compile errors in files that use the client without importing the package go unreported (blog).
  • Pre-existing errors seem to be subtracted by location, which hides a new error at the same line (FCM, tacocat).

Checklist

  • Every rule and note comes from a source listed in meta.sources. client-type and set-expiration come from the published declarations, which are listed.
  • Every rule that rewrites or detects has a fixture case, with a site it must leave alone (keep)
  • 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; otherwise the pack ships as a candidate. Five scored; kept as a candidate on purpose, see above.
  • pnpm uptide pack test redis passes and its output is pasted above; verification.json written with --write. Intentionally not written yet.
  • pnpm lint && pnpm typecheck pass; vitest run src/packs in core (259 tests) passes. The full pnpm test was not run locally.
  • Changeset added
  • No private code, names or paths anywhere: fixtures are synthetic, ground truth is public

🤖 Generated with Claude Code

@lucasbraum
lucasbraum force-pushed the lucasbraum/upt-58-pack-redis-node-redis-4-5-and-5-6 branch from d6e7ba9 to ed12286 Compare October 15, 2026 04:40
@lucasbraum
lucasbraum marked this pull request as ready for review October 15, 2026 04:51
lucasbraum and others added 5 commits October 14, 2026 21:52
One pack, two rule sets: an upgrade gets the rules and notes of the
majors it crosses (4 → 6 gets both). Ground truth from six merged
public upgrades, fixed before the first score.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
…orts, RESEARCH.md (UPT-58)

client-type covers the RESP3 RedisClientType break the 6 guide does not
list; scan-iterators is found by syntax only; quit() is rewritten only
where its result is unused. FalkorDB/falkordb-ts leaves the truth
unscored: check refuses its empty workspaces declaration.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
…not TS state (UPT-58)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
…e (UPT-58)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Lucas Braum <lucasbraum@gmail.com>
@lucasbraum
lucasbraum force-pushed the lucasbraum/upt-58-pack-redis-node-redis-4-5-and-5-6 branch from 90aa400 to 210d299 Compare October 15, 2026 04:55
@lucasbraum

Copy link
Copy Markdown
Collaborator Author

Rebased on main after #152 and #154. The record stays candidate (pack test now accepts it and scores only this pack). Since the last push: redis is in the README packs table, and the two constant maps carry the shared-state marker. Lint, docs:packs, typecheck and core (1765) and cli (471) tests pass locally. Ready for review; verified waits for UPT-48.

@lucasbraum
lucasbraum merged commit 9ddaaff into main Oct 15, 2026
10 checks passed
@lucasbraum
lucasbraum deleted the lucasbraum/upt-58-pack-redis-node-redis-4-5-and-5-6 branch October 15, 2026 05:11
@uptide-release uptide-release Bot mentioned this pull request Oct 15, 2026
lucasbraum added a commit that referenced this pull request Oct 15, 2026
… rewrites run as assisted (UPT-48) (#155)

## Phase 2 report

1. **Sentry demoted.** `@sentry/browser` (3 real breaking sites) and
`@sentry/react` (2) are recorded `candidate`.
`packages/core/src/packs/sentry/RESEARCH.md` says they need at least 5
real breaking sites each, and lists repositories to search later:
cal.com, midday, rallly, inbox-zero, unkey, twenty.
2. **Unexercised rules run as assisted.** `pack test --write` ran once
on every pack except joi and redis. Only the two Sentry statuses
changed; no precision, recall or count moved. Each record now carries
`class`, `breakingSites`, `behaviorSites` and `unexercised`.
- `activePacks` wraps each pack: `transform` refuses an edit whose rule
is unexercised (fix hands the site to the agent with the guide), and
check demotes such a site to assisted. The wrapper checks the rule of
the applied edit, so the hand-written zod and stripe transforms are
covered.
- Stripe's pin and fixture-cast edits have no `rewrite` of their own. A
new `Pack.mechanical` list makes the gate count them.
- Now assisted: lucide-react `renamed-icons`; next
`stable-cache-exports`; vitest `removed-entrypoints`; zod
`error-members`, `error-params`; stripe `api-version-unpinned`,
`fixture-cast`; and the Sentry rules each record lists.
- **zod `error-members`: no truth added.** I scanned merged PRs that
bump zod 3→4 and remove a `.formErrors`/`.errors` read
(PrefectHQ/prefect#22795, bitwarden/ai-plugins#102,
jesseiancary/plinth#41, Seven74AI/music-library#176 and others). Every
catch-binding read is narrowed by `instanceof z.ZodError`, so it is
typed and compiler-visible (`error-issues`), not read through `any`.
Naeha-S/Bloggonut#2 and ooples/token-optimizer-mcp#167 do read `.errors`
in a catch, but on zod 4 already: they are later crash fixes, not 3→4
upgrades. So `error-members` runs as assisted.
3. **zod `error-params`, 0 of 3 public sites: not fixed.** All three are
found as breaking true positives but filed under `generic`. They are
`errorMap` sites: `z.literal(true, { errorMap })` in AwesomeIntune, and
`{ errorMap }` plus a `z.ZodErrorMap` function in PATH. The
`error-params` rewrite only handles
`required_error`/`invalid_type_error` and skips `errorMap` ("needs
manual merging"). zod has no `ruleOf`, so the plan files a site under
`error-params` only when the rewrite applies or the error text names
those two keys. Re-filing them under `error-params` would count them as
evidence for a rewrite that never ran on them. The right fix is a
separate assisted `error-map` rule with its own guide (zod 4: `error`
replaces `errorMap`, and the map's signature changed). That is a pack
change, left for a follow-up.
4. **joi and redis stay candidate** (not written). Why redis's record
differed on purpose: it is still the scaffold record (0 repos, zero
counts), never written. #151 held it at candidate until this gate
landed, because:
   - breaking recall is 61% (11/18);
- behavior notes are 50/50, with all four `client-defaults` predictions
false positives;
- the core 4→5 API changes (`command-options`, `legacy-mode`,
`scan-iterators`, `number-replies`, `keep-alive`) have no real site;
- two engine gaps cost sites: no compile errors in files that reach the
client through a local wrapper, and errors probably subtracted where the
installed version already had one.

Under this gate it measures verified (5 repos, 20 real breaking sites, 0
breaking FP). `keep-alive`, `quit-disconnect`, `scan-iterators` and
`unstable-resp3` would run as assisted.
5. **Override and README.** `meta.minBreakingSites` lowers the minimum
for one pack, and `pack test` fails a pack that sets it without a reason
under `## Breaking-site minimum` in its RESEARCH.md. No pack uses it.
The README packs table has a "Breaking sites" column, and docs/packs.md
and CONTRIBUTING describe the gate.

Also: `--write` now formats a record the way biome does (a short list
inline), so a written record passes lint. The fix-machinery tests use
zod's `required_error` rewrite as their mechanical example; they mock
the registry wrapper to keep it as written.

Local runs: the `@sentry/react` truth repo pins pnpm 12.8.2, which
corepack 0.36 cannot launch (pnpm 12 ships a native binary its
postinstall fetches). I scored it with a local pnpm 12.11.2 through a
scratch shim. CI's corepack may hit the same thing if the repository is
not in its cache. These tests also fail on main locally (environment):
required-build, peer-preflight, scaffold, release, telemetry sender,
third-party notices.

---

## Phase 1: the gate and its impact (measured before the records were
written)

## The gate

`verifiedGate` (contract.ts) replaces `statusOf`. `gateOf`
(pack-test.ts) feeds it from one run; "real" means public repositories,
so a fixture repository is scored and never counts.

| | breaking class (default) | behavior-only class |
|---|---|---|
| applies when | any real expected site is under a rule or `generic` |
every real expected site is a behavior note's, whatever its severity
(ioredis 6) |
| repositories | ≥ 2 public | ≥ 2 public |
| real sites | ≥ 5 expected breaking sites
(`VERIFIED_MIN_BREAKING_SITES`) | ≥ 5 expected note sites
(`VERIFIED_MIN_BEHAVIOR_SITES`) |
| false positives | 0 among breaking findings | 0 among breaking
findings, 0 on note sites, 0 on mechanical rules |
| found | ≥ 1 breaking site | ≥ 1 note site; per-note recall is in the
rules table (#143) |

**Unexercised rewrites**: a rule with a `rewrite` and no true positive
in public ground truth is listed as unexercised. It never changes the
status.

- `pack test` prints a `gate` line (class, real site counts, status),
each reason a pack is a candidate, and the unexercised rules.
- The computed record carries `class`, `breakingSites`, `behaviorSites`
and `unexercised`, so every record is now stale (a note, not a failure)
until `--write`.
- `overclaims()` is unchanged from #154: a record fails only when it
records verified and the run measures candidate; recorded candidate by
choice passes.

**Not in phase 1**
- `fix` running unexercised sites as assisted: it needs the
`unexercised` list in each record, which only phase 2's `--write`
produces. Wrapping `transform` for the hand-written packs (zod, stripe)
is the work there.
- A per-pack override of the minimum with a written reason in
RESEARCH.md.
- The README breaking-site column.

## Impact: every registered pack, under the new gate

Each pack was run one at a time from source (`pnpm uptide pack test <id>
--json`), each with its own cache directory, deleted after the pack.
Site counts are expected sites in public ground truth. "Breaking
tp/fp/fn" is across every scored repository, fixtures included.

| pack | recorded | measures now | real breaking / note sites | breaking
tp/fp/fn | unexercised mechanical rules | why |
|---|---|---|---|---|---|---|
| ai | verified | verified | 11 / 0 | 9/0/2 | — | |
| lucide-react | verified | verified | 11 / 0 | 11/0/0 | renamed-icons |
|
| next | verified | verified | 13 / 0 | 11/0/2 | stable-cache-exports |
|
| react | verified | verified | 130 / 0 | 89/0/40 | — | |
| react-router | verified | verified | 18 / 0 | 14/0/2 | — | see below |
| stripe | verified | verified | 16 / 0 | 18/0/2 | — | |
| vitest | verified | verified | 32 / 13 | 16/0/16 | removed-entrypoints
| |
| zod | verified | verified | 85 / 0 | 59/0/21 | error-members,
error-params | error-params' TPs are all in the fixture repo; its 3
public sites are missed |
| @sentry/nextjs | verified | verified | 8 / 0 | 8/0/0 | enable-logs,
enable-metrics, http-integration-options, integration-names,
renamed-exports, streamed-span-wrapper | |
| **@sentry/browser** | verified | **candidate** (overclaim, fails) | 3
/ 0 | 3/0/0 | enable-metrics, integration-names, renamed-exports,
streamed-span-wrapper | 3 breaking sites, needs 5 |
| **@sentry/react** | verified | **candidate** (overclaim, fails) | 2 /
0 in truth | — | enable-logs, enable-metrics, integration-names,
renamed-exports, streamed-span-wrapper | 2 breaking sites in its truth,
needs 5. Locally one repo failed to install (pnpm crashed with
MODULE_NOT_FOUND, environmental), but the count alone drops it |
| joi | candidate | verified | 108 / 0 | 108/0/0 | — | recorded
candidate by choice: passes |
| redis | candidate | verified | 20 / 2 | 11/0/7 | keep-alive,
quit-disconnect, scan-iterators, unstable-resp3 | recorded candidate by
choice: passes |
| ioredis | candidate | candidate (behavior class) | 2 / 13 | 0/0/2 | —
| 3 false positives on `client-defaults` note sites |

**Drops**: only @sentry/browser and @sentry/react. Both now fail `pack
test` as overclaims, so the Packs job is red on this PR until phase 2.

**react-router**: on main, the only mechanical rule is `dom-imports` (29
real TPs). `removed-json`, `removed-defer`, `form-method-case`,
`dom-server-entry` and `future-option` have no `rewrite`; they are
already assisted, so the gate turns none of them assisted. Separately,
`dom-imports` has 336 non-breaking false positives at the rule level
(precision 8%), which no gate criterion reads.

**Mechanical rules that would turn assisted** (unexercised): 24 rule
entries across 8 packs, 15 distinct ids. lucide-react `renamed-icons`;
next `stable-cache-exports`; vitest `removed-entrypoints`; zod
`error-members`, `error-params`; redis `keep-alive`, `quit-disconnect`,
`scan-iterators`, `unstable-resp3`; and the Sentry rules listed per pack
above.

## Tests

`contract.test.ts` has a pass and a fail case for each criterion, plus
the unexercised list and the behavior-only class (verified, too few
sites, false positives on notes and rules, never chosen with a rule
site). `pack-test.test.ts` covers `gateOf`: fixture sites never count or
exercise a rule, and an unscored repository is no evidence.
(`scaffold.test.ts` fails locally with `formatted: false` on main as
well; unrelated.)

🤖 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>
lucasbraum added a commit that referenced this pull request Oct 16, 2026
## What

`uptide pack test <pack> --write` on main (`530f90c`, UPT-48's gate),
typescript then express, one at a time, each with its own
`UPTIDE_CACHE_DIR`. Both measure verified, so both records now say
`verified`. Ground truth unchanged (same digests). RESEARCH.md for each
states the numbers and the unexercised rules; README table regenerated
(`docs:packs`); changeset added.

| Pack | Repos (≥2) | Real breaking sites (≥5) | Breaking FP (0) |
Breaking P / R | Behavior-note P / R | Unexercised |
|---|---:|---:|---:|---|---|---|
| typescript 5→6 | 7 | 1,399 | 0 | 100% / 17% | 20% / 50% |
downlevel-iteration, import-assertions, module-keyword |
| express 4→5 | 4 | 86 | 0 | 100% / 85% | n/a / 0% | none |

- typescript recall is 17% because the `types: []` errors fold into one
`changed-defaults` finding per tsconfig while the truth lists each error
line; its 8 note FPs are those anchors at `tsconfig*.json:1`. Its three
unexercised rewrites run assisted.
- express: the http-proxy-middleware peer conflict (netlify) and
fixture-only rules are kept as stated gaps in RESEARCH.md.

## redis: not promoted

`pack test redis` (no `--write`) on the same main: **measures verified**
(5 repos, 20 real breaking sites, 0 breaking FP), recorded candidate.

- Breaking recall **61%** (11 of 18 sites).
- Behavior-note precision **50%** (2 TP, 2 FP). The `client-defaults`
FPs are mcp-coordinator and blog, which kept the RESP3 default without
pinning it.
- Unexercised rewrites: keep-alive, quit-disconnect, scan-iterators,
unstable-resp3 (only renamed-exports' rewrite has a real site). The core
4→5 API rules (command-options, legacy-mode, number-replies) have no
real site.
- Engine gaps from #151:
1. Compile errors in files that use the client without importing `redis`
are not reported. All 7 breaking misses come from this (blog 5, FCM 2
scan-cursor sites through a local wrapper).
2. Errors that already exist at the installed version are probably
subtracted by location, which hides a new error on the same line
(client-type at FCM and tacocat 5→6). In this run those 4 lines do
surface, but under another rule: `client-defaults` at tacocat, `generic`
at FCM. So `client-type` recall is 33%.

Recommendation: **don't promote yet.** The gate passes, but the 4→5 half
rests on one 4→5 repo, and the main API changes there were never met in
real code. Promote once a scorable 4→5 upgrade that exercises
command-options or legacy-mode is added (truth committed before
scoring), or once gap 1 is fixed.

🤖 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 added a commit that referenced this pull request Oct 16, 2026
…ocal module (UPT-66) (#168)

Linear: UPT-66. Redis gap 1 from #151/#160.

## What

- **Finder.** `scan-cursor` also reads files that never import redis but
import a value from a repository module that depends on it
(`getRedisClient` from `../config/redis`, `redisClient` from
`~/utils/redis.server`), resolved the way the compiler does it (tsconfig
`paths`, `export *` chains). This is the #127 approach. zod's resolver
moves to `check/importers.ts` as `packageImports` and `projectFiles`,
parameterized by package; zod's behavior is unchanged. `scan(cursor)`
after `let cursor = 0` counts as a numeric cursor (fixture `scan-cursor`
line 9 marked).
- **Promotion.** `pack test redis --write` on this branch measures
verified under UPT-48's gate, so `verification.json` now says
`verified`. README table regenerated. Ground truth is unchanged: the
record's digest matches the committed file, and the old candidate
record's digest was stale.
- Tests came first (e2904cb): the FCM layout (getter typed off
`createClient`, relative import) and the blog layout (`~/` alias, file
not among the usages).

## Score

| | before (#151/#160) | after |
|---|---|---|
| breaking P / R | 100% / 61% (11/18) | 100% / 83% (15/18) |
| scan-cursor recall | 30% | 70% |
| behavior notes P / R | 50% / 50% | 50% / 50% |

5 repos, 20 real breaking sites, 0 breaking false positives.
`keep-alive`, `quit-disconnect`, `scan-iterators` and `unstable-resp3`
have no real site and run as assisted.

## Remaining misses (3, blog)

- `refresh-cache.ts:204`, `:206`: compile errors only. The importer
memory cap (`IMPORTER_GROWTH` 0.2) leaves the file out of the program;
the run reports it as not analyzed. 0.5 would include it. The cap is not
changed here.
- `utils.ts:106`: a helper with a local structural client type and no
redis-related import. Following imports can't reach it.

## Checks

Core suite: one failure, in `fix/assisted.test.ts` (a known timeout
flake); it passes on its own (24/24). Lint and typecheck are clean (the
one lint warning is already on main). `public:check` is clean.

🤖 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>
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