Repository navigation
feat(check): one finding when call sites in several workspaces trace to one declaration - #51
Merged
Merged
Conversation
…to one declaration A mismatch at an argument traces to the repository parameter it lands on (parameterCause); the compile signal anchored the finding there only when two or more sites in the same workspace tripped it. tldraw's useTransform(ref) is called from three workspaces, each with one site, so check reported three findings beside the one at the hook's parameter that upstream actually edited. - The diagnostic keeps the declaration it traces to (`root`), and so does the call-site finding (`Finding.root`, repository-relative), whether or not its workspace folds it. - After the workspaces are merged, foldSharedRoots (check/root-cause.ts) compares roots across them: two or more sites at one declaration, or a site plus the anchor another workspace already produced, become one anchored finding at the declaration, with the sites as `downstream` (each with its `workspace`) and `details` opening with "N call sites in M workspaces"; the pack rule the sites agreed on is kept. - The terminal (--details) prints that line with a few sites as examples; the HTML report lists the call sites with their workspaces under the anchor; the JSON report only gains fields (`root`, `downstream[].workspace`). - The pack test scorer counts the anchor as the predicted site and never the evidence under it, even when another finding lands on an evidence line. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
…fixes it once A root was prefixed by the workspace when the finding was made and again when the workspaces were merged (apps/examples/packages/editor/...), so roots never matched across workspaces. It is now kept like usage.file, and prefixed and normalized by the merge alone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U Signed-off-by: Lucas Braum <lucas.braum@deskfy.io>
lucasbraum
marked this pull request as ready for review
October 9, 2026 16:56
Merged
8 tasks done
lucasbraum
pushed a commit
that referenced
this pull request
Oct 9, 2026
…g precision 100%) pack test react --write: excalidraw and tldraw, 0 false positives among breaking findings. queue.json: react verified. The two cli fix tests that used react as their example of a package without a pack now use vitest. Known: packages/core/src/fix/isolate.test.ts:600 still uses react as the no-pack package and fails. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CECy7agtzRrX4SJWSAUHfS Signed-off-by: Claude <noreply@anthropic.com>
lucasbraum
added a commit
that referenced
this pull request
Oct 9, 2026
## Pack `react`, `>=18 <19` → `>=19 <20`. Companions move with it: `react-dom`, `@types/react` and `@types/react-dom`, as the engine's companion rules decide (not listed by the pack). Supersedes #45: same pack and ground truth, brought onto current main (merge of `main` at 0de525c, no conflict), with the changeset, the README table and a test fixed up. ## Sources - React 19 Upgrade Guide: https://react.dev/blog/2024/04/25/react-19-upgrade-guide - React 19.0.0 changelog: https://github.com/facebook/react/blob/main/CHANGELOG.md#1900-december-5-2024 - types-react-codemod (preset-19): https://github.com/eps1lon/types-react-codemod ## Rules and behavior notes Rewrites by rule: - `use-ref-argument`: `useRef<T>()` → `useRef<T>(undefined)`. - `ref-callback-return`: `ref={(el) => (x = el)}` → a block body (sites from `detect`). - `mutable-ref-object`: `React.MutableRefObject` → `React.RefObject`. To the agent, with a guide: - `ref-object-nullable`: a parameter or prop typed `RefObject<T>` once `useRef` returns `RefObject<T | null>`. - `global-jsx-namespace`: the removed global `JSX` namespace (one finding at the tsconfig's `jsx` line, the elements as evidence). - `element-props-unknown`: untyped `element.props`. - `removed-react-dom`: `ReactDOM.render`, `hydrate`, `unmountComponentAtNode`, `findDOMNode`. - `props-with-ref`. Behavior notes, listed for review (the compiler cannot see them): `errors-not-rethrown`, `removed-legacy-apis`, `act-moved-to-react`, `element-ref-deprecated`, `strict-mode-memoization`, `suspense-fallback-first`, `umd-removed`, `javascript-urls-blocked`, `use-reducer-types`. ## Ground-truth repositories | Repository | Pinned commit (before the upgrade) | Upgrade | With | Expected findings | |---|---|---|---|---| | excalidraw/excalidraw | 9ee0b8ffcbd3664a47748a93262860321a203821 | react 18.2.0 → 19.0.0 | react-dom 19.0.0, @types/react 19.0.10, @types/react-dom 19.0.4 | 73 | | tldraw/tldraw | 5957867a9c5c205a0b0c3665be443138b8014a79 | react 18.3.1 → 19.2.1 | react-dom 19.2.1, @types/react 19.2.7, @types/react-dom 19.2.3 | 60 | Expected findings were drafted with `pnpm packs:truth` from each repository's own TypeScript and its upgrade commit, and read by hand; tldraw's 1,745 JSX element diagnostics are one site at the tsconfig that causes them (#45). Rules, expectations, ground truth and companions are unchanged from #45's last state. ## `uptide pack test` output `verification.json` on this branch records the run on main after #51 (commit 8573cc0); `pack test react` was not rerun here, since the merge touched neither `packages/core/src/check` nor the pack. That run: ``` uptide pack test react · verified fixtures 3 cases, 6 sites · pass ground truth excalidraw/excalidraw@9ee0b8ffcbd3 react 18.2.0 → 19.0.0 predicted 60, expected 73, false positives 1, false negatives 14 tldraw/tldraw@5957867a9c5c react 18.3.1 → 19.2.1 predicted 39, expected 60, false positives 6, false negatives 26 rule precision recall tp fp fn element-props-unknown 100% 60% 6 0 4 generic 4% 33% 1 26 2 global-jsx-namespace 100% 51% 20 0 19 mutable-ref-object 100% 100% 2 0 0 props-with-ref 100% 100% 1 0 0 ref-callback-return 100% 100% 1 0 0 ref-object-nullable 100% 37% 20 0 34 removed-react-dom 100% 100% 18 0 0 use-ref-argument 100% 80% 4 0 1 overall (sites) 93% 70% 92 7 40 breaking (sites) 100% 69% 89 0 40 ``` Breaking findings: precision 100% (89 sites, none false), recall 69%, on 2 public repositories. The 7 non-breaking false positives are deprecated or unverified findings and do not count toward the verdict. ## What is not covered - Recall on breaking sites is 69%: 40 expected sites are missed, most of them `ref-object-nullable` (34) and `global-jsx-namespace` (19 in the per-rule table, counted at the tsconfig anchor) sites the compiler does not reach, plus 4 `element-props-unknown` and 1 `use-ref-argument`. They are listed by `pack test react` as false negatives. - Behavior changes the compiler cannot see are reported as notes for review, never rewritten. - The engine links one copy of the target types; a nested second `@types/react` copy, as a real install can keep, is not modeled. ## This PR on top of #45 - `.changeset/react-pack.md` is `minor` (a new verified pack) and opens with the two repositories; numbers unchanged. - README "Verified packs" regenerated with `pnpm docs:packs`; `pnpm docs:packs --check` and `pnpm docs:links` pass. - `packages/core/src/fix/isolate.test.ts` used react as its example of a package without a pack; it is vitest now, like the two `cli.test.ts` cases. No other test did. - `pnpm lint`, `pnpm typecheck`, core (919 tests) and CLI (350 tests) pass. ## Checklist - [x] Every rule and note comes from a source listed in `meta.sources` - [x] Every rule that rewrites or detects has a fixture case, with a site it must leave alone (`keep`) - [x] 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 - [x] At least two public repositories for `verified`; otherwise the pack ships as a candidate - [x] `pnpm uptide pack test <package>` passes and its output is pasted above; `verification.json` written with `--write` - [x] `pnpm lint && pnpm typecheck && pnpm test` pass - [x] Changeset added (`pnpm changeset`): a new pack is something users notice - [x] No private code, names or paths anywhere: fixtures are synthetic, ground truth is public Supersedes #45. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U --- _Generated by [Claude Code](https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U)_ --------- Signed-off-by: Claude <noreply@anthropic.com> Signed-off-by: Lucas Braum <lucas.braum@deskfy.io> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Lucas Braum <lucas.braum@deskfy.io>
lucasbraum
pushed a commit
that referenced
this pull request
Oct 9, 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.6.0 ### Minor Changes - [#46](#46) [`458aa14`](458aa14) Thanks [@lucasbraum](https://github.com/lucasbraum)! - What changed in `check`: - **Breaking means confirmed.** A type-surface change is reported as breaking only when the compiler rejects your code against the target at that site, the runtime probe saw the export go, or a migration pack found it, in the verified tier as in the generic one. What nothing confirmed is listed as "possible impact", apart from the breaking count. - **Coverage, under every package.** `compiled 355 of 356 files in 5 workspaces; skipped: <reason (count)>` says how much of the code that uses the package the compiler judged, and the verdict says "types partly verified" when not every file was. - **TypeScript-version-aware defaults.** A repository on TypeScript 5 is read with TypeScript 5's defaults for what its tsconfig leaves unset (`strict` off, automatic `@types`), not the bundled TypeScript 6's. Monorepos compile far more of their files: workspace packages declared `workspace:*` under Yarn and npm resolve to their source, scoped programs keep the tsconfig's ambient declaration files, and a workspace that only peer-depends on a package finds the hoisted copy. - **Companions.** `check` and `fix` move `@types/<pkg>` with `<pkg>`, and a package released in lockstep with it (react-dom with react): `check react` 18 → 19 moves react-dom, @types/react and @types/react-dom together. - **One finding for a root cause that is one edit.** A compiler option (the global `JSX` namespace `@types/react` 19 removes, read by `"jsx": "preserve"`) and a repository parameter several call sites trip are each one finding at the place to edit, with the compiler errors as evidence. - [#51](#51) [`a18bebd`](a18bebd) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` reports one breaking finding when call sites in different workspaces trace back to the same local declaration (a parameter or prop whose type names something from the upgraded package): the finding is anchored at the declaration, where the one edit is, and the call sites are listed under it as evidence with an `N call sites in M workspaces` line, in the terminal, the HTML report and the JSON report. Before, a site alone in its workspace stayed a finding of its own, so a hook typed `RefObject<HTMLElement>` and called from three workspaces was three findings plus the one that mattered. The JSON report gains `root` on call-site findings and `workspace` on evidence sites; nothing is renamed or removed. The pack test scorer counts the anchor as the predicted site and never the evidence under it. - [#49](#49) [`2e5c510`](2e5c510) Thanks [@lucasbraum](https://github.com/lucasbraum)! - Two fixes to `check`: - **A project whose tsconfig cannot be resolved is not compiled.** A tsconfig that `extends` a package or file that is not installed describes options the repository never builds with, so what a compiler reports there is not a place to change. `check` skips it, never turns its diagnostics into findings, and says so in the coverage line (a file governed by such a nested project is dropped from the workspace that happens to reach it): `compiled 5 of 7 files in 2 workspaces; not compiled: docs/tsconfig.json (extends "@tsconfig/docusaurus/tsconfig.json" cannot be resolved)`. - **A package that only peers on the package is left in place when its peer range rejects the target.** What always moves with the package stays as before: the release group published at the target's own version (`react-dom`), `@types/*`, the exact pins of the target, and the packages a pack names in `companions` (each with the official page that says so), even when their installed range already accepts the target. Any other package whose installed peer range rejects the target is not moved and not compiled at another version; `check` lists it under possible impact, never as breaking: `? possible impact, peer conflict: next-mdx-remote-client 1.1.2 declares react >= 18.3.0 < 19.0.0`. - [#53](#53) [`d33bdd0`](d33bdd0) Thanks [@lucasbraum](https://github.com/lucasbraum)! - A verified migration pack for React 18 → 19, scored against excalidraw/excalidraw and tldraw/tldraw at the commit before their own upgrade. Rewrites by rule: `useRef<T>()` → `useRef<T>(undefined)`, `ref={(el) => (x = el)}` → a block body, and `React.MutableRefObject` → `React.RefObject`. `RefObject<T | null>`, the removed global `JSX` namespace, untyped `element.props`, the removed react-dom APIs and `PropsWithRef` go to the agent with the guide; what the compiler cannot see (errors no longer re-thrown, removed legacy APIs, `act` moved to "react", Strict Mode and Suspense changes) is listed for review. 2 public repositories, 100% precision on breaking findings (89 sites, none false), recall 69% on breaking sites; the sites it misses are listed in the pack test output. ### Patch Changes - [#48](#48) [`94611a9`](94611a9) Thanks [@lucasbraum](https://github.com/lucasbraum)! - `check` compiles your code with the repository's own TypeScript (the `node_modules/typescript` its build uses, found the way `fix` has found it since verification), and falls back to the bundled compiler only when the repository installs none. Errors and their positions are the ones your `tsc` would print: a repository on TypeScript 4.9 is no longer judged by TypeScript 6's rules. The coverage line says which compiler judged: `compiled 355 of 356 files in 5 workspaces with the repo's TypeScript 4.9.5`. A workspace without a tsconfig of its own is now configured by the nearest one above it (a monorepo root whose `include` covers the workspace), for its options only; synthetic defaults apply only when no tsconfig exists anywhere. Co-authored-by: uptide-release[bot] <338861287+uptide-release[bot]@users.noreply.github.com>
lucasbraum
added a commit
that referenced
this pull request
Oct 10, 2026
…n a repository augmentation (#60) ## What and why **1. Shared root through a setup or declaration file.** In the vitest pack test (#57, ground truth votingworks/vxsuite), `check` reported a breaking finding at `libs/image-utils/src/jest_pdf_snapshot.ts:42` (TS2551, `toMatchImageSnapshot` missing on `Assertion`) beside the declaration at `libs/image-utils/src/vitest_setup.ts:10` the pack already reports. The tracer did not link them: #51 handles parameters only, and this is a global augmentation. Two things turned out to be true on inspection: - The failing member is not a parameter and not declared in the setup file either: `toMatchImageSnapshot` comes from `jest-image-snapshot`'s own types, which declare it on the global `jest.Matchers`. The setup file augments that same interface (`declare global { namespace jest { interface Matchers<R> { toMatchImage…; toMatchPdfSnapshot… } } }`) and registers all three matchers with `expect.extend`. Vitest 5 stops reading the global namespace, so every matcher on it has to move to `Matchers<R, T>` of "vitest" together, and the repository's augmentation is where that edit goes. The cause tracer (`adapters/typescript/cause.ts`, `augmentationCause`) now handles missing-member diagnostics (TS2339, TS2551): the member is resolved in the baseline program, where it still exists, and - when its declaration is in a repository file, the diagnostic is anchored there: a `declare global` block at the block, a `declare module "x"` augmentation at the interface inside it, any other repository declaration at itself; - when its declaration is a package's (or another fixture's) on an interface the repository augments, it is anchored at the repository's augmentation of that interface, this workspace's first, with the package named in the reason; - one site is enough, unlike a parameter: the edit is never at the call. The anchor is also the diagnostic's `root`, so sites in other workspaces fold with it after the merge (#51's model, same report fields: `anchorOnly`, `downstream`, `root`, "N call sites"). It is tried after the compiler-option cause and before the parameter cause. On vxsuite the finding is now one site at `vitest_setup.ts:10`, with the `jest_pdf_snapshot.ts` call as evidence. **2. README line limit.** `readme.test.ts` capped the file at 150 lines and every pack row pushed against it. The test now excludes the generated block between `<!-- packs:start -->` and `<!-- packs:end -->` from the count. #57's two joined documentation lines are not on `main` (that change lives on #57's branch), so there is nothing to restore here; once this merges, #57 can drop that edit. No pack's rules, expectations or ground truth were edited. ## How it was verified Unit tests in `cause.test.ts` on a new fixture pair, `fixtures/deps/matchers-v1` (an assertion that reads the global `checks.Matchers`) and `matchers-v2` (its own `Matchers<R, T>`, the global no longer read), plus `matchers-extra-v1` (a package declaring a matcher on that global): - three call sites in two files, for a matcher the setup file declares, fold into one anchor at the setup file's `declare global` line, each site carrying the cause and the root; - a call site for a matcher another package declares on the interface the setup file augments anchors at that augmentation, naming the package; - a member the package itself dropped is left at the site. `pnpm lint`, `pnpm typecheck`, `pnpm docs:links`, `pnpm docs:packs --check`, the private-material guard, core (930 tests) and CLI (350 tests) pass. Pack tests, before (main at 45677eb) and after (this branch), breaking findings; vitest run in a scratch worktree with #57's pack copied in uncommitted: | pack | before: precision / recall, breaking FP | after: precision / recall, breaking FP | |---|---|---| | vitest (#57, candidate) | 94% / 50%, 1 (`jest_pdf_snapshot.ts:42`) | **100% / 50%, 0** | | zod | 100% / 71%, 0 | 100% / 71%, 0, identical | | stripe | 100% / 90%, 0 | 100% / 90%, 0, identical | | ai | 100% / 36%, 0 | 100% / 36%, 0, identical | | react | 100% / 69%, 0 | 100% / 69%, 0, identical | vitest per rule after: `matcher-types` 100% / 100% (3 of 3), was 75% / 100%. `pack test vitest` still exits non-zero on this branch only because #57's `verification.json` records 94% / 1 false positive and no longer matches; that file is #57's to rewrite with `--write`. Its 16 false negatives and the 18,900-odd `generic (unverified)` sites (not breaking) are unchanged. ## 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/architecture.md`, `docs/concepts.md`) - [x] Changeset added if a user would notice (`pnpm changeset`): minor - [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_018u9b3Pp5H68DdYZGp5Qg1U --- _Generated by [Claude Code](https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U)_ --------- Signed-off-by: Lucas Braum <lucas.braum@deskfy.io> Co-authored-by: Lucas Braum <lucas.braum@deskfy.io> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
Engine item from the #45 thread: cause not traced across workspaces. A mismatch at an argument traces to the repository parameter it lands on (
parameterCause), but the compile signal anchored the finding there only when two or more sites in the same workspace tripped it. tldraw'suseTransform(ref)is called from three workspaces;apps/examplesandpackages/tldrawhave one site each, socheckreported those two as findings of their own beside the anchor inpackages/editor, the one place upstream edited (useTransform.ts:6). Those two were the last breaking false positives of the react pack.Now, when diagnostics in different workspaces trace back to the same local declaration (same file and symbol),
checkemits one breaking finding anchored at that declaration and lists the call sites under it as evidence.adapters/typescript/compile.ts): a diagnostic keeps the declaration it traces to asroot, whether or not enough sites in its workspace share it forcauseto be set.parameterCauseitself is unchanged.check/unattributed.ts): a call-site finding carriesFinding.root(workspace-relative, likeusage.file); an in-workspace parameter anchor carries it too.check/check.ts→check/root-cause.tsfoldSharedRoots): after the workspaces are merged and roots are prefixed to repository paths, two or more sites at one root, or a site plus the anchor another workspace already produced, become (or join) one anchored finding:anchorOnly,downstream= the sites (each with itsworkspace),detailsopening withN call sites in M workspaces,fixability: assisted, the pack rule the sites agreed on. The anchor counts as one site in the verdict.rooton call-site and anchored findings andworkspaceondownstreamentries; nothing is renamed or removed. The terminal--detailsprintsN call sites in M workspaces, for examplewith up to 8 sites under the anchor (instead ofevidence: N errors at the targetfor a declaration root; config anchors keep that wording). The HTML report lists the call sites with their workspaces under the anchor site.packs/tooling/pack-test.tspredictedSites): the anchor is the predicted site, so a ground-truth finding at the declaration matches it; a call site listed as evidence under an anchored finding is neither a true nor a false positive, even when another finding lands on that line.On tldraw the merged finding reads:
packages/editor/src/lib/hooks/useTransform.ts:6·7 call sites in 3 workspaces(5 inpackages/editor, 1 inapps/examples, 1 inpackages/tldraw).No pack rule, expectation, ground truth or companion was edited.
How it was verified
New tests:
adapters/typescript/cause.test.ts(a single site keeps itsroot),check/root-cause.test.ts(fold across workspaces, join an existing anchor, a lone site stays, the call-sites line),check/check.test.ts(mergeAcrossWorkspacesprefixes../editor/...roots from two workspaces to one repository path and folds them),packs/tooling/pack-test.test.ts(anchor scored, evidence excluded even under another finding),format-check-details.test.tsandhtml/render.test.ts(theN call sitesline and the listed sites).pnpm lint && pnpm typecheck && pnpm testpass (core 917 tests, cli 349).Pack tests, before (main at 2e5c510) and after, run in a scratch worktree with the react pack from #45's branch (
claude/pensive-galileo-mw5yx8) copied in uncommitted; breaking findings:useTransformsites)react overall (every severity): 91% / 70% → 93% / 70%; the 7 remaining non-breaking false positives (6 deprecated, 1 unverified) are unchanged.
pack test reactstill exits non-zero only because #45'sverification.jsonrecords 96% / 4 false positives and no longer matches; it is not edited here.Checklist
pnpm lint && pnpm typecheck && pnpm testpassdocs/architecture.md,docs/cli.md)pnpm changeset): minor🤖 Generated with Claude Code
https://claude.ai/code/session_018u9b3Pp5H68DdYZGp5Qg1U
Generated by Claude Code