@shipispec/tsfix 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -1
- package/README.md +65 -11
- package/dist/cli.js +181 -50
- package/dist/index.d.ts +16 -0
- package/dist/index.js +155 -41
- package/dist/types/index.d.ts +16 -0
- package/dist/types/libraryMigrations.d.ts +57 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,70 @@ All notable changes to `@shipispec/tsfix` are documented here. Format follows [K
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.0] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
**Library-aware error recovery.** Layer 2 now auto-detects breaking-change hints for known libraries from your `package.json` and steers the LLM away from tsc's misleading quick-fixes when a library migration is the real cause. Plus a hardened type-context walk (no more crashes on rename cascades or branded types) and a meaningful set of security anti-patterns in the system prompt.
|
|
10
|
+
|
|
11
|
+
### Added (library-migration hints)
|
|
12
|
+
- **`detectLibraryMigrations(workspaceRoot, registry?)`** — reads `package.json`, matches installed deps against a built-in registry of known breaking changes, returns matching hints. Auto-invoked by `runMendLoop` when `context.libraryMigrations` is left `undefined`. Pass `[]` explicitly to opt out.
|
|
13
|
+
- **`BUILT_IN_LIBRARY_MIGRATIONS` registry** — initial entries cover `vite-plugin-svgr` (v4 — `?react` query suffix), `next` (15 — `params`/`searchParams` are Promises), `ai` (v3 / v6), `drizzle-orm` (parameterized template literals).
|
|
14
|
+
- **`formatLibraryMigrationsBlock(hints)`** + **`formatLibraryMigrationsTaskDescription(hints)`** — public formatters. The latter produces the `taskDescription` headline (`Library migration: <names>`) that overrides any caller-supplied description when migrations apply — empirically, models follow tsc's quick-fix when the migration is mentioned only in a buried section.
|
|
15
|
+
- **CLI `--no-library-hints`** — opt-out flag. Default behavior auto-detects and injects hints. When a migration matches AND `--llm` is set, the CLI also skips Layer 0/1 (tsc's quick-fix is the misleading path for these cases).
|
|
16
|
+
- **`MendContext.libraryMigrations?: Array<{ name: string; hint: string }>`** — new optional field. `undefined` = auto-detect; `[]` = opt out; populated array = override (skip detection).
|
|
17
|
+
|
|
18
|
+
### Added (system-prompt security anti-patterns)
|
|
19
|
+
- **Type-assertion escape-hatches** — explicitly forbids `as keyof T` for runtime-string TS7053 silencing, `x as any` / `x as unknown as T` to dodge a real mismatch, `!` non-null assertions to dodge TS18047/TS2532. The prompt directs the model to narrow at the function signature, widen with an index signature, or guard with `if (key in obj)` instead.
|
|
20
|
+
- **Dependency removal/substitution** — restoring a missing import is preferred to substituting a different library (e.g. `bcrypt` → `crypto.subtle.digest` is flagged as a security regression even when tsc accepts it).
|
|
21
|
+
- **SQL / NoSQL / shell injection** — forbids string concatenation of user-controlled values into raw query strings; directs the model to Drizzle's tagged template, Prisma / mysql2 placeholders, etc.
|
|
22
|
+
- **React XSS** — forbids `dangerouslySetInnerHTML` as a way to dodge a children-type error; recommends auto-escaping JSX or DOMPurify.
|
|
23
|
+
|
|
24
|
+
### Added (union-cleanup positive guidance)
|
|
25
|
+
- When a type variant or interface property has been removed/renamed, the prompt now directs the model to do a FULL sweep in the same patch instead of partial cleanup. Specific TS2322 / TS2353 / TS2367 guidance: drop the excess property, drop now-orphaned function parameters with their use sites, replace the no-longer-valid comparison or delete it with its branch. Aimed at the "I changed one reference and left three more" failure mode that produces fresh errors on iteration 2.
|
|
26
|
+
|
|
27
|
+
### Added (tests)
|
|
28
|
+
- 14 unit tests in `libraryMigrations.test.ts` covering empty / matching / minMajor / maxMajor / multi-dep / malformed-package.json / custom-registry / formatter shapes / headline generation.
|
|
29
|
+
- 4 tests in `mendAgent.test.ts` for `buildSystemBlock`'s library-migration integration (block present, taskDescription override, empty array, custom description preserved without migrations).
|
|
30
|
+
- 2 tests in `runMendLoop.test.ts` for auto-detect (populates from package.json when omitted; opts out on explicit `[]`).
|
|
31
|
+
- 1 regression test in `typeContext.test.ts` — "does not throw on multi-file rename-cascade (TS2305: unresolvable named import)" with 4 importers.
|
|
32
|
+
- Total: **130/130 tests pass.**
|
|
33
|
+
|
|
34
|
+
### Fixed
|
|
35
|
+
- **`getTypeContext` no longer crashes on multi-file rename cascades or branded types.** `typeContext.ts:tryResolve` now wraps `checker.getTypeAtLocation(n)` and the subsequent `getSymbol()` / `aliasSymbol` / `getDeclarations()` chain in try/catch — TypeScript's internals throw `Cannot read properties of undefined (reading 'kind' / 'flags')` from `isDeclarationNameOrImportPropertyName` on these shapes; tsfix treats those as "no resolvable type" and continues. Belt-and-suspenders try/catch added in `mendAgent.ts` around the per-diagnostic context build — if one diagnostic's context fails for any reason, that diagnostic is skipped instead of killing the whole mend (one bad diag should not lose the LLM's chance to fix the other errors in the file).
|
|
36
|
+
|
|
37
|
+
### Bench results
|
|
38
|
+
Re-measured against the 34-fixture corpus (24 single-file + 10 multi-file) at n=3 per cell:
|
|
39
|
+
|
|
40
|
+
| Surface | v0.5.0 | v0.6.0 | Δ |
|
|
41
|
+
|---|---|---|---|
|
|
42
|
+
| Single-file pass rate | 95.8% | **98.6%** | +2.8pp |
|
|
43
|
+
| Multi-file pass rate | 23.3% | **40.0%** | +16.7pp |
|
|
44
|
+
| Aggregate (102 cells) | 74.5% | **81.4%** | +6.9pp |
|
|
45
|
+
| Hard crashes | 6 cells | **0** | -6 |
|
|
46
|
+
| Cost per full bench | — | **$0.21** | — |
|
|
47
|
+
| Cost per case (haiku-4-5) | — | **<$0.005** | — |
|
|
48
|
+
|
|
49
|
+
Per-fixture flips notable enough to call out:
|
|
50
|
+
|
|
51
|
+
- **`case-ts2614-vite-svgr` (0/3 → 3/3)** — vite-plugin-svgr v4's `?react` query suffix migration. Before: model followed tsc's quick-fix and emitted `import Logo from "./logo.svg"` (type-checks under the `*.svg` ambient, breaks at runtime under vite). After: with the registry hint, the model emits `import Logo from "./logo.svg?react"` and the resulting code works in both tsc and the dev server.
|
|
52
|
+
- **`case-m7-index-signature-removed` (0/3 → 3/3)** — anti-pattern prompts ended the `as keyof T` escape-hatch loop.
|
|
53
|
+
- **`case-m3-union-variant-removed` (2/3 → 3/3)** + **`case-m6-hook-tuple-arity` (2/3 → 3/3)** — union-cleanup guidance fixed the partial-sweep failure mode.
|
|
54
|
+
- **`case-m1` + `case-m10`** — previously errored with the `typeContext` crash; now produce measurable results.
|
|
55
|
+
|
|
56
|
+
Caveats: n=3 per cell is noisy at the per-case level (a single-cell flip from 2→3 may revert); aggregate column totals at 24+ cases are the trustworthy signal. Multi-file scenarios remain the gap — Layer 3 (multi-file mend) is the deferred answer.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
- **`runMendLoop`** auto-populates `context.libraryMigrations` from `rawContext.workspaceRoot`'s `package.json` when the caller leaves it `undefined`. Existing callers that omit the field get the new behavior automatically; existing callers that pass a non-empty array see no change.
|
|
60
|
+
- **`buildSystemBlock`** leads the prompt body with the library-migrations section when any apply, and uses the migration headline as the `taskDescription` (overrides any caller-supplied description). The library section lives between `SYSTEM_INSTRUCTIONS` and the errored-file content — earliest position where the model still sees it before reaching the file.
|
|
61
|
+
- **CLI** — when a library migration matches AND `--llm` is set, Layer 0/1 is skipped (tsc's quick-fix is the misleading path for these cases). Existing zero-LLM CLI behavior unchanged.
|
|
62
|
+
|
|
63
|
+
### Engines
|
|
64
|
+
- Node `>=20.9.0` (unchanged)
|
|
65
|
+
- TypeScript `>=5.0.0` peer (unchanged)
|
|
66
|
+
|
|
67
|
+
## [0.5.0] - 2026-05-16
|
|
68
|
+
|
|
69
|
+
**Layer 4 (stub-and-continue), Day 2/3 fixture mutators, parallel + cached Layer-2 benchmark, and CLI exposure of Layer 2.** This closes the "tsfix never leaves the workspace worse than it found it" property: when Layer 2 can't resolve the last few errors, the workspace can opt-in to `@ts-expect-error` directives that self-destruct once the underlying issue is fixed elsewhere. The CLI now exposes `--llm` end-to-end (was library-API only).
|
|
70
|
+
|
|
7
71
|
### Added (Layer 4 — stub-and-continue escape hatch)
|
|
8
72
|
- **`stubAndContinue(opts)`** — new public API. Inserts `// @ts-expect-error - tsfix: <codes> — <message>` immediately above each unresolved error site so `tsc --noEmit` exits 0. Closes the "tsfix never leaves the workspace worse than it found it" property. Uses `@ts-expect-error` (not `@ts-ignore`) so directives self-destruct once the underlying issue is fixed by other means.
|
|
9
73
|
- **`runMendLoop` opt-in flag** — new `stubOnFailure?: boolean` option (default `false`). When the LLM loop terminates with leftover errors and the flag is set, Layer 4 runs automatically. New `"stubbed"` stop reason; new `stubs?: AppliedStub[]` result field with what was applied.
|
|
@@ -201,7 +265,9 @@ Initial public release. **Layers 0–1 only** (deterministic detection + auto-fi
|
|
|
201
265
|
- Node `>=20.9.0` (matches VS Code Extension Host runtime)
|
|
202
266
|
- TypeScript `>=5.0.0` (peer dep, must be installed in the consuming workspace)
|
|
203
267
|
|
|
204
|
-
[Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.
|
|
268
|
+
[Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.6.0...HEAD
|
|
269
|
+
[0.6.0]: https://github.com/owgreen-dev/tsfix/compare/v0.5.0...v0.6.0
|
|
270
|
+
[0.5.0]: https://github.com/owgreen-dev/tsfix/compare/v0.4.0...v0.5.0
|
|
205
271
|
[0.4.0]: https://github.com/owgreen-dev/tsfix/compare/v0.3.0...v0.4.0
|
|
206
272
|
[0.3.0]: https://github.com/owgreen-dev/tsfix/compare/v0.2.0...v0.3.0
|
|
207
273
|
[0.2.0]: https://github.com/owgreen-dev/tsfix/compare/v0.1.1...v0.2.0
|
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# tsfix
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Library-aware TypeScript error recovery for LLM-generated code. Fix `TS2304`, `TS2305`, `TS2551`, `TS2552`, `TS2724` deterministically with the same engine that powers VS Code's Quick Fix. Escalate the rest to a single-file LLM mend that knows what tsc's quick-fix gets wrong about your installed libraries.
|
|
4
4
|
|
|
5
|
-
`@shipispec/tsfix` is what you reach for when you've just generated a few hundred files of TypeScript with an LLM and `tsc --noEmit` is screaming at you. It runs in
|
|
5
|
+
`@shipispec/tsfix` is what you reach for when you've just generated a few hundred files of TypeScript with an LLM and `tsc --noEmit` is screaming at you. It runs in layers:
|
|
6
6
|
|
|
7
7
|
- **Layer 0/1** — Deterministic. Borrows the same TypeScript Language Service that powers VS Code's "Quick Fix" lightbulb and runs it as a CLI. Fixes typos, missing imports, and did-you-mean errors with no LLM, no network, no config.
|
|
8
|
-
- **Layer 2** — Opt-in. A single-file LLM mend agent (Vercel AI SDK + Anthropic) that picks up what Layer 0 abstains on: TS2339 (property doesn't exist), TS7006 (implicit `any`), TS2741 (missing required prop), and other cases where the LSP can't statically derive the fix. Driven by **type-context injection** — when tsc says "Property 'foo' doesn't exist on type 'Bar'", tsfix resolves the `Bar` declaration via the TypeChecker and feeds its source to the model.
|
|
8
|
+
- **Layer 2** — Opt-in. A single-file LLM mend agent (Vercel AI SDK + Anthropic) that picks up what Layer 0 abstains on: TS2339 (property doesn't exist), TS7006 (implicit `any`), TS2741 (missing required prop), and other cases where the LSP can't statically derive the fix. Driven by **type-context injection** — when tsc says "Property 'foo' doesn't exist on type 'Bar'", tsfix resolves the `Bar` declaration via the TypeChecker and feeds its source to the model. As of v0.6.0, also **library-aware**: tsfix reads your `package.json` and injects breaking-change hints for known libraries (`vite-plugin-svgr`, `next`, `ai`, `drizzle-orm`) so the model picks the runtime-correct fix instead of tsc's misleading quick-fix.
|
|
9
|
+
- **Layer 4** — Escape hatch. When Layer 2 can't resolve the last few errors, opt in to `// @ts-expect-error - tsfix: ...` directives that self-destruct once the underlying issue is fixed elsewhere. tsfix never leaves the workspace worse than it found it.
|
|
9
10
|
|
|
10
|
-
Layer 2 only runs if you explicitly call its API or set `ANTHROPIC_API_KEY` and
|
|
11
|
+
Layer 2 only runs if you explicitly call its API or set `ANTHROPIC_API_KEY` and pass `--llm` to the CLI. The default `tsfix --workspace ...` CLI is still **Layer 0/1 only**.
|
|
11
12
|
|
|
12
13
|
## Before / after (Layer 0)
|
|
13
14
|
|
|
@@ -97,20 +98,73 @@ Layer 2 is built for the cases the LSP can't statically resolve:
|
|
|
97
98
|
- `TS7006` — Implicit `any`. The LLM picks the right annotation from surrounding context.
|
|
98
99
|
- `TS2741` — Missing required property. The LLM sees the contextual type and supplies a real value, not a placeholder.
|
|
99
100
|
|
|
100
|
-
Against a 35-fixture Layer-2 benchmark (3 hand-authored minimal + 2 realistic + 30 ts-morph-generated mutations across TS2339/TS7006/TS2741), **35/35 pass at $0.001/fixture avg, P95 latency ~1.5s on `claude-haiku-4-5`.** Caveat: the 30 generated fixtures are mutations of 3 seeds — real-world diversity will move these numbers.
|
|
101
|
+
Against a 35-fixture Layer-2 benchmark (3 hand-authored minimal + 2 realistic + 30 ts-morph-generated mutations across TS2339/TS7006/TS2741), **35/35 pass at $0.001/fixture avg, P95 latency ~1.5s on `claude-haiku-4-5`.** Caveat: the 30 generated fixtures are mutations of 3 seeds — real-world diversity will move these numbers; see the realistic 34-fixture bench below.
|
|
102
|
+
|
|
103
|
+
## Library-aware error recovery (v0.6.0)
|
|
104
|
+
|
|
105
|
+
A typical TypeScript LLM-repair failure mode: tsc reports `TS2614: Module '"./logo.svg"' has no exported member 'ReactComponent'. Did you mean to use 'import Logo from "./logo.svg"' instead?` The model dutifully follows tsc's quick-fix and emits `import Logo from "./logo.svg"`. **tsc is now green. The dev server is now broken.** Under `vite-plugin-svgr@4`, importing an SVG as a React component requires the `?react` query suffix — `import Logo from "./logo.svg?react"`. The default export is the asset URL, not a component. Quick-fix accuracy ≠ runtime correctness.
|
|
106
|
+
|
|
107
|
+
tsfix v0.6.0 reads your `package.json` on every Layer 2 invocation, matches installed deps against a built-in registry of known breaking changes, and injects library-migration hints into the system prompt's headline (not buried — headline framing matters more than buried context). With `vite-plugin-svgr@^4` installed:
|
|
108
|
+
|
|
109
|
+
```
|
|
110
|
+
### library-migrations
|
|
111
|
+
- vite-plugin-svgr: v4 requires the `?react` query suffix to import an SVG
|
|
112
|
+
as a React component. `import Logo from "./logo.svg"` returns the asset URL.
|
|
113
|
+
`import Logo from "./logo.svg?react"` returns the component.
|
|
114
|
+
|
|
115
|
+
### task
|
|
116
|
+
Library migration: vite-plugin-svgr
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Bench result on this exact case before/after: **0/3 → 3/3**.
|
|
120
|
+
|
|
121
|
+
The built-in registry currently covers four libraries chosen for high LLM-repair confusion ratio:
|
|
122
|
+
|
|
123
|
+
| Library | Hint |
|
|
124
|
+
|---|---|
|
|
125
|
+
| `vite-plugin-svgr` v4+ | `?react` query suffix to import as React component |
|
|
126
|
+
| `next` v15+ | `params` / `searchParams` are now Promises (must `await`) |
|
|
127
|
+
| `ai` v3 / v6 | `generateText` API shape changes |
|
|
128
|
+
| `drizzle-orm` | parameterized `sql` template literals, not string concat |
|
|
129
|
+
|
|
130
|
+
`detectLibraryMigrations(workspaceRoot, registry?)` is also exported as a public API; pass your own registry to extend it. `runMendLoop` auto-invokes detection when you leave `context.libraryMigrations` `undefined`; pass `[]` to opt out, or `--no-library-hints` on the CLI.
|
|
131
|
+
|
|
132
|
+
### Security-aware system prompt
|
|
133
|
+
|
|
134
|
+
The same release hardened the system prompt against the LLM-repair failure modes that silence tsc at the cost of runtime semantics:
|
|
135
|
+
|
|
136
|
+
- **`as keyof T` to silence TS7053** — fix the function signature or guard with `if (key in obj)` instead. Casting away an index-signature error keeps the call type-passing while losing all the runtime safety.
|
|
137
|
+
- **Substituting one library for another to dodge a missing import** — e.g. `bcrypt` → `crypto.subtle.digest`. The fix is to restore the missing import, not swap to a different cryptographic primitive that tsc accepts.
|
|
138
|
+
- **String concatenation of user input into raw SQL** — use Drizzle's tagged template / Prisma placeholders.
|
|
139
|
+
- **`dangerouslySetInnerHTML` to dodge a children-type error** — JSX `{value}` auto-escapes; if you need HTML, sanitize via DOMPurify.
|
|
140
|
+
|
|
141
|
+
### Realistic bench (34 fixtures, single + multi-file)
|
|
142
|
+
|
|
143
|
+
Measured against a 34-fixture corpus drawn from real LLM-repair failures in adjacent projects (24 single-file + 10 multi-file), n=3 per cell:
|
|
144
|
+
|
|
145
|
+
| Surface | v0.5.0 | v0.6.0 | Δ |
|
|
146
|
+
|---|---|---|---|
|
|
147
|
+
| Single-file pass rate | 95.8% | **98.6%** | +2.8pp |
|
|
148
|
+
| Multi-file pass rate | 23.3% | **40.0%** | +16.7pp |
|
|
149
|
+
| Aggregate (102 cells) | 74.5% | **81.4%** | +6.9pp |
|
|
150
|
+
| Hard crashes | 6 cells | **0** | -6 |
|
|
151
|
+
| Cost per full bench | — | **$0.21** | — |
|
|
152
|
+
| Cost per case (`claude-haiku-4-5`) | — | **<$0.005** | — |
|
|
153
|
+
|
|
154
|
+
Multi-file scenarios remain the gap — Layer 3 (multi-file mend with `findReferences`-driven blast-radius search) is the deferred answer.
|
|
101
155
|
|
|
102
156
|
## The four-layer model
|
|
103
157
|
|
|
104
158
|
```
|
|
105
|
-
Layer 0 — Prevention
|
|
106
|
-
Layer 1 — Deterministic
|
|
107
|
-
Layer 2 — Single-file LLM
|
|
159
|
+
Layer 0 — Prevention (prompt rules, exported-API injection — your problem)
|
|
160
|
+
Layer 1 — Deterministic (this package: LSP auto-fix, CLI default)
|
|
161
|
+
Layer 2 — Single-file LLM (this package: opt-in via --llm or runMendLoop)
|
|
162
|
+
Layer 4 — Stub-and-continue (this package: opt-in escape hatch, @ts-expect-error)
|
|
108
163
|
─────────────────────────────────────────────────────────────────
|
|
109
|
-
Layer 3 — Multi-file LLM
|
|
110
|
-
Layer 4 — Stub-and-continue (planned: escape hatch)
|
|
164
|
+
Layer 3 — Multi-file LLM (planned: blast-radius search/replace via findReferences)
|
|
111
165
|
```
|
|
112
166
|
|
|
113
|
-
The bet: roughly half of TypeScript errors in LLM output are deterministically fixable. By catching them in Layer 1 you dodge the LLM tax (latency, cost, nondeterminism) on the easy half. Layer 2 takes the other half — but only when you explicitly invoke it.
|
|
167
|
+
The bet: roughly half of TypeScript errors in LLM output are deterministically fixable. By catching them in Layer 1 you dodge the LLM tax (latency, cost, nondeterminism) on the easy half. Layer 2 takes the other half — but only when you explicitly invoke it. Layer 4 makes sure the workspace is never left worse than it started.
|
|
114
168
|
|
|
115
169
|
## Library API
|
|
116
170
|
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/run-stack.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as path9 from "node:path";
|
|
5
|
+
import * as fs9 from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/validatorInProcess.ts
|
|
8
8
|
import * as fs from "node:fs";
|
|
@@ -473,8 +473,8 @@ function applyFixToSnapshots(fix, snapshots) {
|
|
|
473
473
|
}
|
|
474
474
|
|
|
475
475
|
// src/index.ts
|
|
476
|
-
import * as
|
|
477
|
-
import * as
|
|
476
|
+
import * as fs8 from "node:fs";
|
|
477
|
+
import * as path8 from "node:path";
|
|
478
478
|
|
|
479
479
|
// src/typeContext.ts
|
|
480
480
|
import * as fs3 from "node:fs";
|
|
@@ -549,9 +549,20 @@ function isLibFile(fileName) {
|
|
|
549
549
|
}
|
|
550
550
|
function findTypeDeclaration(checker, startNode, maxWalkUp = 4) {
|
|
551
551
|
const tryResolve = (n) => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
552
|
+
let type;
|
|
553
|
+
try {
|
|
554
|
+
type = checker.getTypeAtLocation(n);
|
|
555
|
+
} catch {
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
let symbol;
|
|
559
|
+
let declarations;
|
|
560
|
+
try {
|
|
561
|
+
symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
562
|
+
declarations = symbol?.getDeclarations();
|
|
563
|
+
} catch {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
555
566
|
if (!declarations || declarations.length === 0) return void 0;
|
|
556
567
|
const nonLib = declarations.find((d) => !isLibFile(d.getSourceFile().fileName));
|
|
557
568
|
if (!nonLib) return void 0;
|
|
@@ -784,10 +795,77 @@ function applyEditBlocks(opts) {
|
|
|
784
795
|
}
|
|
785
796
|
|
|
786
797
|
// src/mendAgent.ts
|
|
787
|
-
import * as
|
|
788
|
-
import * as
|
|
798
|
+
import * as fs6 from "node:fs";
|
|
799
|
+
import * as path6 from "node:path";
|
|
789
800
|
import { generateText } from "ai";
|
|
790
801
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
802
|
+
|
|
803
|
+
// src/libraryMigrations.ts
|
|
804
|
+
import * as fs5 from "node:fs";
|
|
805
|
+
import * as path5 from "node:path";
|
|
806
|
+
var BUILT_IN_LIBRARY_MIGRATIONS = [
|
|
807
|
+
{
|
|
808
|
+
match: { name: "vite-plugin-svgr", minMajor: 4 },
|
|
809
|
+
hint: "vite-plugin-svgr v4+ (released 2023-09-20) changed how SVG imports work. The PREVIOUS form `import { ReactComponent as X } from './x.svg'` no longer works \u2014 the ambient module declaration now only matches `*.svg?react`. Correct fix: `import X from './x.svg?react'` (default import + ?react query suffix). DO NOT use tsc's quick-fix `import X from './x.svg'` (no query) \u2014 that type-checks but resolves to the asset URL string at runtime, not a component."
|
|
810
|
+
},
|
|
811
|
+
{
|
|
812
|
+
match: { name: "next", minMajor: 15 },
|
|
813
|
+
hint: "Next.js 15 changed dynamic-route page props: `params` and `searchParams` are now `Promise<...>` instead of plain objects. The fix shape is: change the page's `params` type to `Promise<{...}>`, mark the page component `async`, and `await params` inside. See https://nextjs.org/docs/app/api-reference/file-conventions/page."
|
|
814
|
+
},
|
|
815
|
+
{
|
|
816
|
+
match: { name: "ai", minMajor: 3, maxMajor: 4 },
|
|
817
|
+
hint: "Vercel AI SDK v3.x has overload-narrowing issues with `generateObject`. If passing a schema through an object widened with `satisfies Record<K, z.ZodTypeAny>`, the typed overload silently falls back to `output: 'no-schema'` (which forbids the `schema` property). Fix: drop the `satisfies Record<...>` widener, or cast the schema at the call site."
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
match: { name: "drizzle-orm" },
|
|
821
|
+
hint: "Drizzle ORM table access has two distinct surfaces. `db.<table>` is for `select/insert/update/delete` builders. `db.query.<table>` is the Relational Queries API for `findFirst`/`findMany` with relation loading. If you see `Property '<table>' does not exist on type 'PostgresJsDatabase<...>'` when trying to call `.findFirst`/`.findMany`, use `db.query.<table>` instead."
|
|
822
|
+
}
|
|
823
|
+
];
|
|
824
|
+
function parseMajor(spec) {
|
|
825
|
+
const m = spec.match(/(\d+)(?:\.\d+)*/);
|
|
826
|
+
return m ? parseInt(m[1], 10) : null;
|
|
827
|
+
}
|
|
828
|
+
function detectLibraryMigrations(workspaceRoot, registry = BUILT_IN_LIBRARY_MIGRATIONS) {
|
|
829
|
+
let pkg;
|
|
830
|
+
try {
|
|
831
|
+
const pkgPath = path5.join(workspaceRoot, "package.json");
|
|
832
|
+
if (!fs5.existsSync(pkgPath)) return [];
|
|
833
|
+
pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
834
|
+
} catch {
|
|
835
|
+
return [];
|
|
836
|
+
}
|
|
837
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
838
|
+
const hints = [];
|
|
839
|
+
for (const entry of registry) {
|
|
840
|
+
const { match, hint } = entry;
|
|
841
|
+
const versionSpec = allDeps[match.name];
|
|
842
|
+
if (!versionSpec) continue;
|
|
843
|
+
const major = parseMajor(versionSpec);
|
|
844
|
+
if (match.minMajor != null && (major == null || major < match.minMajor)) continue;
|
|
845
|
+
if (match.maxMajor != null && (major == null || major > match.maxMajor)) continue;
|
|
846
|
+
hints.push({ name: `${match.name}@${versionSpec}`, hint });
|
|
847
|
+
}
|
|
848
|
+
return hints;
|
|
849
|
+
}
|
|
850
|
+
function formatLibraryMigrationsBlock(hints) {
|
|
851
|
+
if (hints.length === 0) return "";
|
|
852
|
+
const lines = ["### library-migrations", ""];
|
|
853
|
+
lines.push(
|
|
854
|
+
"These migrations apply to your workspace's installed deps. When tsc's quick-fix conflicts with the migration target below, PREFER the migration target. tsc only checks types, not runtime semantics \u2014 these hints encode runtime constraints tsc cannot see."
|
|
855
|
+
);
|
|
856
|
+
lines.push("");
|
|
857
|
+
for (const h of hints) {
|
|
858
|
+
lines.push(`- [${h.name}] ${h.hint}`);
|
|
859
|
+
}
|
|
860
|
+
return lines.join("\n");
|
|
861
|
+
}
|
|
862
|
+
function formatLibraryMigrationsTaskDescription(hints) {
|
|
863
|
+
if (hints.length === 0) return void 0;
|
|
864
|
+
const names = hints.map((h) => h.name).join(", ");
|
|
865
|
+
return `Library migration: ${names}`;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// src/mendAgent.ts
|
|
791
869
|
var SYSTEM_INSTRUCTIONS = `You are a TypeScript code-repair tool. You receive a TypeScript file with one or more compiler errors and resolve them.
|
|
792
870
|
|
|
793
871
|
Output ONLY SEARCH/REPLACE blocks. No prose, no explanations, no XML wrappers.
|
|
@@ -808,16 +886,43 @@ Rules:
|
|
|
808
886
|
- REPLACE must be valid TypeScript that resolves the diagnostic.
|
|
809
887
|
- Do not invent imports, types, properties, or values. Use only what the type-context section shows.
|
|
810
888
|
- One SEARCH/REPLACE block per logical change.
|
|
811
|
-
- If you cannot resolve a diagnostic with the information given, omit a block for it
|
|
889
|
+
- If you cannot resolve a diagnostic with the information given, omit a block for it.
|
|
890
|
+
|
|
891
|
+
Anti-patterns \u2014 these silence the type error but break runtime semantics, lose type safety, or introduce security regressions. Do NOT emit a patch that does any of the following:
|
|
892
|
+
|
|
893
|
+
1. Type-assertion escape-hatches that hide the error rather than fix it:
|
|
894
|
+
- \`x as any\` / \`x as unknown as T\` to dodge a real mismatch.
|
|
895
|
+
- \`key as keyof T\` to silence a TS7053 index-signature error when \`key\` is a runtime \`string\` (not a statically-known literal). Narrow the parameter type to \`keyof T\` at the function signature instead, OR widen the object type to include an index signature, OR perform a runtime \`if (key in obj)\` guard. \`as keyof T\` keeps the call site type-passing while losing all the runtime safety the index signature gave.
|
|
896
|
+
- \`!\` non-null assertions to dodge TS18047/TS2532 \u2014 narrow with a truthiness check or optional-chaining + nullish-coalesce that actually preserves the narrow on the true branch.
|
|
897
|
+
|
|
898
|
+
2. Removing or substituting a declared dependency to dodge a missing-import error. If \`package.json\` lists the package and the source uses it, RESTORE the import. Do not substitute a different library (e.g. \`bcrypt\` \u2192 \`crypto.subtle.digest\`) \u2014 that is a security regression even when tsc accepts it.
|
|
899
|
+
|
|
900
|
+
3. SQL / NoSQL / shell injection patterns:
|
|
901
|
+
- String concatenation of user-controlled values into raw query strings (\`db.execute("WHERE id = " + userId)\`). Use the library's tagged-template / parameterized form (\`db.execute(sql\\\`WHERE id = \\\${userId}\\\`)\` for Drizzle; placeholders for Prisma / mysql2; etc).
|
|
902
|
+
- Never use template literals to interpolate user input into a raw SQL string unless the literal is itself a parameterizing tagged template.
|
|
903
|
+
|
|
904
|
+
4. React XSS escape-hatches:
|
|
905
|
+
- \`dangerouslySetInnerHTML\` to dodge a children-type error. If a component expects \`children: string\` and you have arbitrary HTML, render it as text (JSX \`{value}\` auto-escapes) or sanitize via a library (DOMPurify) and document the assumption.
|
|
906
|
+
- Setting \`innerHTML\` directly on a DOM element from user input.
|
|
907
|
+
|
|
908
|
+
These anti-patterns apply only to the listed shapes. For other diagnostics, follow the regular Rules above and pick the smallest valid fix \u2014 including legitimate uses of \`as unknown as T\`, \`keyof typeof T\` (as a type annotation, not a cast), or restructuring a type union. Do not omit a block just because the fix involves an \`as\` cast or a structural change \u2014 only omit when the fix would match one of the four anti-patterns above.
|
|
909
|
+
|
|
910
|
+
When a type, union variant, or interface property has been removed or renamed, consumer code that referenced the old shape needs FULL cleanup, not partial cleanup:
|
|
911
|
+
|
|
912
|
+
- TS2322 / TS2353 (excess property in object literal): REMOVE the excess property from the literal. Do not retain it. Example: if a \`{ type: 'archived', userId, reason, at }\` object now needs \`type: 'created'\` and the \`created\` variant has no \`reason\` field, the fix is to drop \`reason\` from the object \u2014 keeping it produces a fresh TS2353. This is field deletion, not "silencing an error" \u2014 there is no error to silence; the property genuinely no longer belongs.
|
|
913
|
+
- Function parameters and return types that exist solely to support the removed variant (e.g. a \`reason: string\` parameter on a function that no longer needs reasons) should be dropped along with their use sites in the same SEARCH/REPLACE block.
|
|
914
|
+
- TS2367 (comparison with no overlap): if comparing against a removed literal, EITHER pick a still-valid literal that preserves the function's spirit, OR delete the comparison and its branch if neither makes sense. Don't leave a comparison against a now-invalid literal.
|
|
915
|
+
|
|
916
|
+
The goal is internal consistency: if you change one reference to a removed variant/property, sweep ALL references in this file in the same patch. A half-cleanup leaves new tsc errors and is worse than the original state.`;
|
|
812
917
|
function workspaceRelative(workspaceRoot, p) {
|
|
813
|
-
return
|
|
918
|
+
return path6.isAbsolute(p) ? path6.relative(workspaceRoot, p) : p;
|
|
814
919
|
}
|
|
815
920
|
function buildSystemBlock(context, erroredFile) {
|
|
816
921
|
const wsRel = workspaceRelative(context.workspaceRoot, erroredFile);
|
|
817
|
-
const absPath =
|
|
922
|
+
const absPath = path6.isAbsolute(erroredFile) ? erroredFile : path6.join(context.workspaceRoot, erroredFile);
|
|
818
923
|
let fileContent;
|
|
819
924
|
try {
|
|
820
|
-
fileContent =
|
|
925
|
+
fileContent = fs6.readFileSync(absPath, "utf-8");
|
|
821
926
|
} catch {
|
|
822
927
|
fileContent = "(file unreadable)";
|
|
823
928
|
}
|
|
@@ -827,10 +932,15 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
827
932
|
const typeContexts = [];
|
|
828
933
|
const seen = /* @__PURE__ */ new Set();
|
|
829
934
|
for (const diag of fileDiags) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
935
|
+
let ctx;
|
|
936
|
+
try {
|
|
937
|
+
ctx = getTypeContext({
|
|
938
|
+
workspaceRoot: context.workspaceRoot,
|
|
939
|
+
diagnostic: diag
|
|
940
|
+
});
|
|
941
|
+
} catch {
|
|
942
|
+
continue;
|
|
943
|
+
}
|
|
834
944
|
if (!ctx.typeDeclaration) continue;
|
|
835
945
|
const key = `${ctx.typeDeclaration.file}:${ctx.typeDeclaration.symbol}`;
|
|
836
946
|
if (seen.has(key)) continue;
|
|
@@ -841,22 +951,21 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
841
951
|
` + ctx.typeDeclaration.lines
|
|
842
952
|
);
|
|
843
953
|
}
|
|
844
|
-
const parts = [
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
"```"
|
|
851
|
-
];
|
|
954
|
+
const parts = [SYSTEM_INSTRUCTIONS, ""];
|
|
955
|
+
const libMigrations = context.libraryMigrations ?? [];
|
|
956
|
+
if (libMigrations.length > 0) {
|
|
957
|
+
parts.push(formatLibraryMigrationsBlock(libMigrations), "");
|
|
958
|
+
}
|
|
959
|
+
parts.push(`### file: ${wsRel}`, "```ts", fileContent.replace(/\n$/, ""), "```");
|
|
852
960
|
if (typeContexts.length > 0) {
|
|
853
961
|
parts.push("", "### type-context");
|
|
854
962
|
for (const tc of typeContexts) {
|
|
855
963
|
parts.push("```ts", tc, "```");
|
|
856
964
|
}
|
|
857
965
|
}
|
|
858
|
-
|
|
859
|
-
|
|
966
|
+
const taskHeadline = formatLibraryMigrationsTaskDescription(libMigrations) ?? context.taskDescription;
|
|
967
|
+
if (taskHeadline) {
|
|
968
|
+
parts.push("", `### task`, taskHeadline);
|
|
860
969
|
}
|
|
861
970
|
return parts.join("\n");
|
|
862
971
|
}
|
|
@@ -920,8 +1029,8 @@ async function mendSingleFile(opts) {
|
|
|
920
1029
|
}
|
|
921
1030
|
|
|
922
1031
|
// src/stubAndContinue.ts
|
|
923
|
-
import * as
|
|
924
|
-
import * as
|
|
1032
|
+
import * as fs7 from "node:fs";
|
|
1033
|
+
import * as path7 from "node:path";
|
|
925
1034
|
var noopLogger = { info: () => {
|
|
926
1035
|
}, warn: () => {
|
|
927
1036
|
}, error: () => {
|
|
@@ -938,17 +1047,17 @@ function groupByLine(diagnostics) {
|
|
|
938
1047
|
return groups;
|
|
939
1048
|
}
|
|
940
1049
|
function resolveFile(diagnosticFile, workspaceRoot) {
|
|
941
|
-
return
|
|
1050
|
+
return path7.isAbsolute(diagnosticFile) ? diagnosticFile : path7.resolve(workspaceRoot, diagnosticFile);
|
|
942
1051
|
}
|
|
943
1052
|
function shouldSkipFile(file, workspaceRoot) {
|
|
944
|
-
const rel =
|
|
945
|
-
if (rel.startsWith("node_modules") || rel.includes(`${
|
|
1053
|
+
const rel = path7.relative(workspaceRoot, file);
|
|
1054
|
+
if (rel.startsWith("node_modules") || rel.includes(`${path7.sep}node_modules${path7.sep}`)) {
|
|
946
1055
|
return "node_modules";
|
|
947
1056
|
}
|
|
948
1057
|
if (file.endsWith(".d.ts")) {
|
|
949
1058
|
return "declaration_file";
|
|
950
1059
|
}
|
|
951
|
-
if (!
|
|
1060
|
+
if (!fs7.existsSync(file)) {
|
|
952
1061
|
return "file_not_found";
|
|
953
1062
|
}
|
|
954
1063
|
return null;
|
|
@@ -1007,7 +1116,7 @@ function stubAndContinue(opts) {
|
|
|
1007
1116
|
}
|
|
1008
1117
|
continue;
|
|
1009
1118
|
}
|
|
1010
|
-
const source =
|
|
1119
|
+
const source = fs7.readFileSync(file, "utf-8");
|
|
1011
1120
|
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1012
1121
|
const lines = source.split(/\r?\n/);
|
|
1013
1122
|
entries.sort((a, b) => b.line - a.line);
|
|
@@ -1049,10 +1158,10 @@ function stubAndContinue(opts) {
|
|
|
1049
1158
|
if (edited) {
|
|
1050
1159
|
filesEditedSet.add(file);
|
|
1051
1160
|
if (!dryRun) {
|
|
1052
|
-
|
|
1053
|
-
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${
|
|
1161
|
+
fs7.writeFileSync(file, lines.join(eol), "utf-8");
|
|
1162
|
+
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1054
1163
|
} else {
|
|
1055
|
-
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${
|
|
1164
|
+
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1056
1165
|
}
|
|
1057
1166
|
}
|
|
1058
1167
|
}
|
|
@@ -1103,8 +1212,9 @@ function refreshDiagnostics(workspaceRoot, files) {
|
|
|
1103
1212
|
return result.diagnostics.filter((d) => d.category === "error");
|
|
1104
1213
|
}
|
|
1105
1214
|
async function runMendLoop(opts) {
|
|
1106
|
-
const { context, llm, maxIterations = 3, dryRun = false, stubOnFailure = false, _callLLM } = opts;
|
|
1215
|
+
const { context: rawContext, llm, maxIterations = 3, dryRun = false, stubOnFailure = false, _callLLM } = opts;
|
|
1107
1216
|
const startMs = Date.now();
|
|
1217
|
+
const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
|
|
1108
1218
|
const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
|
|
1109
1219
|
if (diagnosticsBefore.length === 0) {
|
|
1110
1220
|
return {
|
|
@@ -1217,7 +1327,7 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1217
1327
|
const walk = (dir) => {
|
|
1218
1328
|
let entries;
|
|
1219
1329
|
try {
|
|
1220
|
-
entries =
|
|
1330
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1221
1331
|
} catch {
|
|
1222
1332
|
return;
|
|
1223
1333
|
}
|
|
@@ -1226,10 +1336,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1226
1336
|
if (skip.has(e.name)) {
|
|
1227
1337
|
continue;
|
|
1228
1338
|
}
|
|
1229
|
-
walk(
|
|
1339
|
+
walk(path8.join(dir, e.name));
|
|
1230
1340
|
} else if (e.isFile() && !e.name.endsWith(".d.ts")) {
|
|
1231
1341
|
if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
|
|
1232
|
-
out.push(
|
|
1342
|
+
out.push(path8.relative(workspaceRoot, path8.join(dir, e.name)));
|
|
1233
1343
|
}
|
|
1234
1344
|
}
|
|
1235
1345
|
}
|
|
@@ -1240,10 +1350,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1240
1350
|
function runValidationLoop(opts) {
|
|
1241
1351
|
const { workspaceRoot, skipLSPFixer = false, dryRun = false } = opts;
|
|
1242
1352
|
const logger = opts.logger ?? noopLogger3;
|
|
1243
|
-
if (!
|
|
1353
|
+
if (!fs8.existsSync(workspaceRoot)) {
|
|
1244
1354
|
throw new Error(`workspace not found: ${workspaceRoot}`);
|
|
1245
1355
|
}
|
|
1246
|
-
if (!
|
|
1356
|
+
if (!fs8.existsSync(path8.join(workspaceRoot, "tsconfig.json"))) {
|
|
1247
1357
|
throw new Error(`no tsconfig.json in ${workspaceRoot}`);
|
|
1248
1358
|
}
|
|
1249
1359
|
const targetFiles = opts.targetFiles ?? discoverTsFiles(workspaceRoot);
|
|
@@ -1313,7 +1423,8 @@ function parseArgs(argv) {
|
|
|
1313
1423
|
llm: false,
|
|
1314
1424
|
llmModel: "claude-haiku-4-5",
|
|
1315
1425
|
llmMaxIterations: 3,
|
|
1316
|
-
llmBudgetUsd: void 0
|
|
1426
|
+
llmBudgetUsd: void 0,
|
|
1427
|
+
noLibraryHints: false
|
|
1317
1428
|
};
|
|
1318
1429
|
for (let i = 0; i < argv.length; i++) {
|
|
1319
1430
|
const a = argv[i];
|
|
@@ -1347,6 +1458,8 @@ function parseArgs(argv) {
|
|
|
1347
1458
|
process.exit(2);
|
|
1348
1459
|
}
|
|
1349
1460
|
args.llmBudgetUsd = v;
|
|
1461
|
+
} else if (a === "--no-library-hints") {
|
|
1462
|
+
args.noLibraryHints = true;
|
|
1350
1463
|
} else if (a === "--help" || a === "-h") {
|
|
1351
1464
|
printHelp();
|
|
1352
1465
|
process.exit(0);
|
|
@@ -1380,6 +1493,12 @@ Layer 2 (opt-in \u2014 single-file LLM mend via Anthropic):
|
|
|
1380
1493
|
Cost estimate is 0 for unknown models.
|
|
1381
1494
|
--llm-max-iterations <N> Cap on LLM retries (default: 3)
|
|
1382
1495
|
--llm-budget-usd <amount> Soft cost cap. Exits with code 3 if exceeded.
|
|
1496
|
+
--no-library-hints Disable auto-detection of library breaking-change
|
|
1497
|
+
hints (vite-plugin-svgr v4 ?react migration,
|
|
1498
|
+
Next.js 15 async params, etc.). When workspace
|
|
1499
|
+
package.json contains a known migration target,
|
|
1500
|
+
tsfix injects hints into Layer 2's prompt + skips
|
|
1501
|
+
Layer 0/1 (whose quick-fix would conflict).
|
|
1383
1502
|
|
|
1384
1503
|
Layer 2 requires ANTHROPIC_API_KEY in the environment.
|
|
1385
1504
|
|
|
@@ -1454,12 +1573,12 @@ TSC Defense Stack \u2014 ${r.workspace}${r.dryRun ? " (dry-run)" : ""}
|
|
|
1454
1573
|
}
|
|
1455
1574
|
async function main() {
|
|
1456
1575
|
const args = parseArgs(process.argv.slice(2));
|
|
1457
|
-
const workspaceRoot =
|
|
1458
|
-
if (!
|
|
1576
|
+
const workspaceRoot = path9.resolve(args.workspace);
|
|
1577
|
+
if (!fs9.existsSync(workspaceRoot)) {
|
|
1459
1578
|
console.error(`error: workspace not found: ${workspaceRoot}`);
|
|
1460
1579
|
return 2;
|
|
1461
1580
|
}
|
|
1462
|
-
if (!
|
|
1581
|
+
if (!fs9.existsSync(path9.join(workspaceRoot, "tsconfig.json"))) {
|
|
1463
1582
|
console.error(`error: no tsconfig.json in ${workspaceRoot}`);
|
|
1464
1583
|
return 2;
|
|
1465
1584
|
}
|
|
@@ -1470,15 +1589,23 @@ async function main() {
|
|
|
1470
1589
|
console.error("error: no .ts/.tsx files found in workspace");
|
|
1471
1590
|
return 2;
|
|
1472
1591
|
}
|
|
1592
|
+
const libraryMigrations = args.noLibraryHints ? [] : detectLibraryMigrations(workspaceRoot);
|
|
1593
|
+
const migrationApplies = args.llm && libraryMigrations.length > 0;
|
|
1594
|
+
if (migrationApplies && !args.noLsp) {
|
|
1595
|
+
logger.info(
|
|
1596
|
+
`Library migrations detected (${libraryMigrations.map((h) => h.name).join(", ")}) \u2014 skipping Layer 0/1 to let Layer 2 apply the migration target. Use --no-library-hints to disable.`
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1599
|
+
const effectiveNoLsp = args.noLsp || migrationApplies;
|
|
1473
1600
|
const loop = runValidationLoop({
|
|
1474
1601
|
workspaceRoot,
|
|
1475
1602
|
targetFiles,
|
|
1476
|
-
skipLSPFixer:
|
|
1603
|
+
skipLSPFixer: effectiveNoLsp,
|
|
1477
1604
|
dryRun: args.dryRun,
|
|
1478
1605
|
logger
|
|
1479
1606
|
});
|
|
1480
1607
|
const report = {
|
|
1481
|
-
workspace:
|
|
1608
|
+
workspace: path9.relative(process.cwd(), workspaceRoot) || workspaceRoot,
|
|
1482
1609
|
errorsBefore: loop.errorsBefore,
|
|
1483
1610
|
lspFixer: args.noLsp ? { ran: false, fixesApplied: 0, filesEdited: [], iterations: 0 } : loop.lspFixer,
|
|
1484
1611
|
layer2: null,
|
|
@@ -1509,7 +1636,11 @@ async function main() {
|
|
|
1509
1636
|
const context = {
|
|
1510
1637
|
workspaceRoot,
|
|
1511
1638
|
diagnostics: errorDiags,
|
|
1512
|
-
erroredFiles: Array.from(new Set(errorDiags.map((d) => d.file)))
|
|
1639
|
+
erroredFiles: Array.from(new Set(errorDiags.map((d) => d.file))),
|
|
1640
|
+
// Explicitly pass migrations so `runMendLoop` doesn't re-detect.
|
|
1641
|
+
// `[]` is meaningful — it means "we know there are none" — vs
|
|
1642
|
+
// `undefined` which would trigger auto-detect.
|
|
1643
|
+
libraryMigrations
|
|
1513
1644
|
};
|
|
1514
1645
|
const layer2Start = Date.now();
|
|
1515
1646
|
const mend = await runMendLoop({
|
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,20 @@ export interface MendContext {
|
|
|
156
156
|
priorTaskExports?: string;
|
|
157
157
|
/** Compact type signatures of installed npm dependencies (helps prevent API hallucination). */
|
|
158
158
|
installedTypes?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Library-migration hints for installed deps whose breaking changes are
|
|
161
|
+
* known to mislead tsc's quick-fix. When non-empty, the Layer 2 prompt
|
|
162
|
+
* leads with these and the `taskDescription` is overridden to name them.
|
|
163
|
+
*
|
|
164
|
+
* Auto-populated by `runMendLoop` from `<workspaceRoot>/package.json` if
|
|
165
|
+
* the caller doesn't set it. Pass `[]` explicitly to opt out.
|
|
166
|
+
*
|
|
167
|
+
* See `libraryMigrations.ts` for the built-in registry.
|
|
168
|
+
*/
|
|
169
|
+
libraryMigrations?: Array<{
|
|
170
|
+
name: string;
|
|
171
|
+
hint: string;
|
|
172
|
+
}>;
|
|
159
173
|
}
|
|
160
174
|
/**
|
|
161
175
|
* Per-layer event for streaming telemetry across the validate → fix → mend
|
|
@@ -194,3 +208,5 @@ export { runMendLoop } from "./runMendLoop.js";
|
|
|
194
208
|
export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
|
|
195
209
|
export { stubAndContinue } from "./stubAndContinue.js";
|
|
196
210
|
export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.js";
|
|
211
|
+
export { BUILT_IN_LIBRARY_MIGRATIONS, detectLibraryMigrations, formatLibraryMigrationsBlock, formatLibraryMigrationsTaskDescription, } from "./libraryMigrations.js";
|
|
212
|
+
export type { LibraryMigrationHint } from "./libraryMigrations.js";
|
package/dist/index.js
CHANGED
|
@@ -475,8 +475,8 @@ function resetLSPFixerCache() {
|
|
|
475
475
|
}
|
|
476
476
|
|
|
477
477
|
// src/index.ts
|
|
478
|
-
import * as
|
|
479
|
-
import * as
|
|
478
|
+
import * as fs8 from "node:fs";
|
|
479
|
+
import * as path8 from "node:path";
|
|
480
480
|
|
|
481
481
|
// src/typeContext.ts
|
|
482
482
|
import * as fs3 from "node:fs";
|
|
@@ -554,9 +554,20 @@ function isLibFile(fileName) {
|
|
|
554
554
|
}
|
|
555
555
|
function findTypeDeclaration(checker, startNode, maxWalkUp = 4) {
|
|
556
556
|
const tryResolve = (n) => {
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
557
|
+
let type;
|
|
558
|
+
try {
|
|
559
|
+
type = checker.getTypeAtLocation(n);
|
|
560
|
+
} catch {
|
|
561
|
+
return void 0;
|
|
562
|
+
}
|
|
563
|
+
let symbol;
|
|
564
|
+
let declarations;
|
|
565
|
+
try {
|
|
566
|
+
symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
567
|
+
declarations = symbol?.getDeclarations();
|
|
568
|
+
} catch {
|
|
569
|
+
return void 0;
|
|
570
|
+
}
|
|
560
571
|
if (!declarations || declarations.length === 0) return void 0;
|
|
561
572
|
const nonLib = declarations.find((d) => !isLibFile(d.getSourceFile().fileName));
|
|
562
573
|
if (!nonLib) return void 0;
|
|
@@ -789,10 +800,77 @@ function applyEditBlocks(opts) {
|
|
|
789
800
|
}
|
|
790
801
|
|
|
791
802
|
// src/mendAgent.ts
|
|
792
|
-
import * as
|
|
793
|
-
import * as
|
|
803
|
+
import * as fs6 from "node:fs";
|
|
804
|
+
import * as path6 from "node:path";
|
|
794
805
|
import { generateText } from "ai";
|
|
795
806
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
807
|
+
|
|
808
|
+
// src/libraryMigrations.ts
|
|
809
|
+
import * as fs5 from "node:fs";
|
|
810
|
+
import * as path5 from "node:path";
|
|
811
|
+
var BUILT_IN_LIBRARY_MIGRATIONS = [
|
|
812
|
+
{
|
|
813
|
+
match: { name: "vite-plugin-svgr", minMajor: 4 },
|
|
814
|
+
hint: "vite-plugin-svgr v4+ (released 2023-09-20) changed how SVG imports work. The PREVIOUS form `import { ReactComponent as X } from './x.svg'` no longer works \u2014 the ambient module declaration now only matches `*.svg?react`. Correct fix: `import X from './x.svg?react'` (default import + ?react query suffix). DO NOT use tsc's quick-fix `import X from './x.svg'` (no query) \u2014 that type-checks but resolves to the asset URL string at runtime, not a component."
|
|
815
|
+
},
|
|
816
|
+
{
|
|
817
|
+
match: { name: "next", minMajor: 15 },
|
|
818
|
+
hint: "Next.js 15 changed dynamic-route page props: `params` and `searchParams` are now `Promise<...>` instead of plain objects. The fix shape is: change the page's `params` type to `Promise<{...}>`, mark the page component `async`, and `await params` inside. See https://nextjs.org/docs/app/api-reference/file-conventions/page."
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
match: { name: "ai", minMajor: 3, maxMajor: 4 },
|
|
822
|
+
hint: "Vercel AI SDK v3.x has overload-narrowing issues with `generateObject`. If passing a schema through an object widened with `satisfies Record<K, z.ZodTypeAny>`, the typed overload silently falls back to `output: 'no-schema'` (which forbids the `schema` property). Fix: drop the `satisfies Record<...>` widener, or cast the schema at the call site."
|
|
823
|
+
},
|
|
824
|
+
{
|
|
825
|
+
match: { name: "drizzle-orm" },
|
|
826
|
+
hint: "Drizzle ORM table access has two distinct surfaces. `db.<table>` is for `select/insert/update/delete` builders. `db.query.<table>` is the Relational Queries API for `findFirst`/`findMany` with relation loading. If you see `Property '<table>' does not exist on type 'PostgresJsDatabase<...>'` when trying to call `.findFirst`/`.findMany`, use `db.query.<table>` instead."
|
|
827
|
+
}
|
|
828
|
+
];
|
|
829
|
+
function parseMajor(spec) {
|
|
830
|
+
const m = spec.match(/(\d+)(?:\.\d+)*/);
|
|
831
|
+
return m ? parseInt(m[1], 10) : null;
|
|
832
|
+
}
|
|
833
|
+
function detectLibraryMigrations(workspaceRoot, registry = BUILT_IN_LIBRARY_MIGRATIONS) {
|
|
834
|
+
let pkg;
|
|
835
|
+
try {
|
|
836
|
+
const pkgPath = path5.join(workspaceRoot, "package.json");
|
|
837
|
+
if (!fs5.existsSync(pkgPath)) return [];
|
|
838
|
+
pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
839
|
+
} catch {
|
|
840
|
+
return [];
|
|
841
|
+
}
|
|
842
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
843
|
+
const hints = [];
|
|
844
|
+
for (const entry of registry) {
|
|
845
|
+
const { match, hint } = entry;
|
|
846
|
+
const versionSpec = allDeps[match.name];
|
|
847
|
+
if (!versionSpec) continue;
|
|
848
|
+
const major = parseMajor(versionSpec);
|
|
849
|
+
if (match.minMajor != null && (major == null || major < match.minMajor)) continue;
|
|
850
|
+
if (match.maxMajor != null && (major == null || major > match.maxMajor)) continue;
|
|
851
|
+
hints.push({ name: `${match.name}@${versionSpec}`, hint });
|
|
852
|
+
}
|
|
853
|
+
return hints;
|
|
854
|
+
}
|
|
855
|
+
function formatLibraryMigrationsBlock(hints) {
|
|
856
|
+
if (hints.length === 0) return "";
|
|
857
|
+
const lines = ["### library-migrations", ""];
|
|
858
|
+
lines.push(
|
|
859
|
+
"These migrations apply to your workspace's installed deps. When tsc's quick-fix conflicts with the migration target below, PREFER the migration target. tsc only checks types, not runtime semantics \u2014 these hints encode runtime constraints tsc cannot see."
|
|
860
|
+
);
|
|
861
|
+
lines.push("");
|
|
862
|
+
for (const h of hints) {
|
|
863
|
+
lines.push(`- [${h.name}] ${h.hint}`);
|
|
864
|
+
}
|
|
865
|
+
return lines.join("\n");
|
|
866
|
+
}
|
|
867
|
+
function formatLibraryMigrationsTaskDescription(hints) {
|
|
868
|
+
if (hints.length === 0) return void 0;
|
|
869
|
+
const names = hints.map((h) => h.name).join(", ");
|
|
870
|
+
return `Library migration: ${names}`;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// src/mendAgent.ts
|
|
796
874
|
var SYSTEM_INSTRUCTIONS = `You are a TypeScript code-repair tool. You receive a TypeScript file with one or more compiler errors and resolve them.
|
|
797
875
|
|
|
798
876
|
Output ONLY SEARCH/REPLACE blocks. No prose, no explanations, no XML wrappers.
|
|
@@ -813,16 +891,43 @@ Rules:
|
|
|
813
891
|
- REPLACE must be valid TypeScript that resolves the diagnostic.
|
|
814
892
|
- Do not invent imports, types, properties, or values. Use only what the type-context section shows.
|
|
815
893
|
- One SEARCH/REPLACE block per logical change.
|
|
816
|
-
- If you cannot resolve a diagnostic with the information given, omit a block for it
|
|
894
|
+
- If you cannot resolve a diagnostic with the information given, omit a block for it.
|
|
895
|
+
|
|
896
|
+
Anti-patterns \u2014 these silence the type error but break runtime semantics, lose type safety, or introduce security regressions. Do NOT emit a patch that does any of the following:
|
|
897
|
+
|
|
898
|
+
1. Type-assertion escape-hatches that hide the error rather than fix it:
|
|
899
|
+
- \`x as any\` / \`x as unknown as T\` to dodge a real mismatch.
|
|
900
|
+
- \`key as keyof T\` to silence a TS7053 index-signature error when \`key\` is a runtime \`string\` (not a statically-known literal). Narrow the parameter type to \`keyof T\` at the function signature instead, OR widen the object type to include an index signature, OR perform a runtime \`if (key in obj)\` guard. \`as keyof T\` keeps the call site type-passing while losing all the runtime safety the index signature gave.
|
|
901
|
+
- \`!\` non-null assertions to dodge TS18047/TS2532 \u2014 narrow with a truthiness check or optional-chaining + nullish-coalesce that actually preserves the narrow on the true branch.
|
|
902
|
+
|
|
903
|
+
2. Removing or substituting a declared dependency to dodge a missing-import error. If \`package.json\` lists the package and the source uses it, RESTORE the import. Do not substitute a different library (e.g. \`bcrypt\` \u2192 \`crypto.subtle.digest\`) \u2014 that is a security regression even when tsc accepts it.
|
|
904
|
+
|
|
905
|
+
3. SQL / NoSQL / shell injection patterns:
|
|
906
|
+
- String concatenation of user-controlled values into raw query strings (\`db.execute("WHERE id = " + userId)\`). Use the library's tagged-template / parameterized form (\`db.execute(sql\\\`WHERE id = \\\${userId}\\\`)\` for Drizzle; placeholders for Prisma / mysql2; etc).
|
|
907
|
+
- Never use template literals to interpolate user input into a raw SQL string unless the literal is itself a parameterizing tagged template.
|
|
908
|
+
|
|
909
|
+
4. React XSS escape-hatches:
|
|
910
|
+
- \`dangerouslySetInnerHTML\` to dodge a children-type error. If a component expects \`children: string\` and you have arbitrary HTML, render it as text (JSX \`{value}\` auto-escapes) or sanitize via a library (DOMPurify) and document the assumption.
|
|
911
|
+
- Setting \`innerHTML\` directly on a DOM element from user input.
|
|
912
|
+
|
|
913
|
+
These anti-patterns apply only to the listed shapes. For other diagnostics, follow the regular Rules above and pick the smallest valid fix \u2014 including legitimate uses of \`as unknown as T\`, \`keyof typeof T\` (as a type annotation, not a cast), or restructuring a type union. Do not omit a block just because the fix involves an \`as\` cast or a structural change \u2014 only omit when the fix would match one of the four anti-patterns above.
|
|
914
|
+
|
|
915
|
+
When a type, union variant, or interface property has been removed or renamed, consumer code that referenced the old shape needs FULL cleanup, not partial cleanup:
|
|
916
|
+
|
|
917
|
+
- TS2322 / TS2353 (excess property in object literal): REMOVE the excess property from the literal. Do not retain it. Example: if a \`{ type: 'archived', userId, reason, at }\` object now needs \`type: 'created'\` and the \`created\` variant has no \`reason\` field, the fix is to drop \`reason\` from the object \u2014 keeping it produces a fresh TS2353. This is field deletion, not "silencing an error" \u2014 there is no error to silence; the property genuinely no longer belongs.
|
|
918
|
+
- Function parameters and return types that exist solely to support the removed variant (e.g. a \`reason: string\` parameter on a function that no longer needs reasons) should be dropped along with their use sites in the same SEARCH/REPLACE block.
|
|
919
|
+
- TS2367 (comparison with no overlap): if comparing against a removed literal, EITHER pick a still-valid literal that preserves the function's spirit, OR delete the comparison and its branch if neither makes sense. Don't leave a comparison against a now-invalid literal.
|
|
920
|
+
|
|
921
|
+
The goal is internal consistency: if you change one reference to a removed variant/property, sweep ALL references in this file in the same patch. A half-cleanup leaves new tsc errors and is worse than the original state.`;
|
|
817
922
|
function workspaceRelative(workspaceRoot, p) {
|
|
818
|
-
return
|
|
923
|
+
return path6.isAbsolute(p) ? path6.relative(workspaceRoot, p) : p;
|
|
819
924
|
}
|
|
820
925
|
function buildSystemBlock(context, erroredFile) {
|
|
821
926
|
const wsRel = workspaceRelative(context.workspaceRoot, erroredFile);
|
|
822
|
-
const absPath =
|
|
927
|
+
const absPath = path6.isAbsolute(erroredFile) ? erroredFile : path6.join(context.workspaceRoot, erroredFile);
|
|
823
928
|
let fileContent;
|
|
824
929
|
try {
|
|
825
|
-
fileContent =
|
|
930
|
+
fileContent = fs6.readFileSync(absPath, "utf-8");
|
|
826
931
|
} catch {
|
|
827
932
|
fileContent = "(file unreadable)";
|
|
828
933
|
}
|
|
@@ -832,10 +937,15 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
832
937
|
const typeContexts = [];
|
|
833
938
|
const seen = /* @__PURE__ */ new Set();
|
|
834
939
|
for (const diag of fileDiags) {
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
940
|
+
let ctx;
|
|
941
|
+
try {
|
|
942
|
+
ctx = getTypeContext({
|
|
943
|
+
workspaceRoot: context.workspaceRoot,
|
|
944
|
+
diagnostic: diag
|
|
945
|
+
});
|
|
946
|
+
} catch {
|
|
947
|
+
continue;
|
|
948
|
+
}
|
|
839
949
|
if (!ctx.typeDeclaration) continue;
|
|
840
950
|
const key = `${ctx.typeDeclaration.file}:${ctx.typeDeclaration.symbol}`;
|
|
841
951
|
if (seen.has(key)) continue;
|
|
@@ -846,22 +956,21 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
846
956
|
` + ctx.typeDeclaration.lines
|
|
847
957
|
);
|
|
848
958
|
}
|
|
849
|
-
const parts = [
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
"```"
|
|
856
|
-
];
|
|
959
|
+
const parts = [SYSTEM_INSTRUCTIONS, ""];
|
|
960
|
+
const libMigrations = context.libraryMigrations ?? [];
|
|
961
|
+
if (libMigrations.length > 0) {
|
|
962
|
+
parts.push(formatLibraryMigrationsBlock(libMigrations), "");
|
|
963
|
+
}
|
|
964
|
+
parts.push(`### file: ${wsRel}`, "```ts", fileContent.replace(/\n$/, ""), "```");
|
|
857
965
|
if (typeContexts.length > 0) {
|
|
858
966
|
parts.push("", "### type-context");
|
|
859
967
|
for (const tc of typeContexts) {
|
|
860
968
|
parts.push("```ts", tc, "```");
|
|
861
969
|
}
|
|
862
970
|
}
|
|
863
|
-
|
|
864
|
-
|
|
971
|
+
const taskHeadline = formatLibraryMigrationsTaskDescription(libMigrations) ?? context.taskDescription;
|
|
972
|
+
if (taskHeadline) {
|
|
973
|
+
parts.push("", `### task`, taskHeadline);
|
|
865
974
|
}
|
|
866
975
|
return parts.join("\n");
|
|
867
976
|
}
|
|
@@ -925,8 +1034,8 @@ async function mendSingleFile(opts) {
|
|
|
925
1034
|
}
|
|
926
1035
|
|
|
927
1036
|
// src/stubAndContinue.ts
|
|
928
|
-
import * as
|
|
929
|
-
import * as
|
|
1037
|
+
import * as fs7 from "node:fs";
|
|
1038
|
+
import * as path7 from "node:path";
|
|
930
1039
|
var noopLogger = { info: () => {
|
|
931
1040
|
}, warn: () => {
|
|
932
1041
|
}, error: () => {
|
|
@@ -943,17 +1052,17 @@ function groupByLine(diagnostics) {
|
|
|
943
1052
|
return groups;
|
|
944
1053
|
}
|
|
945
1054
|
function resolveFile(diagnosticFile, workspaceRoot) {
|
|
946
|
-
return
|
|
1055
|
+
return path7.isAbsolute(diagnosticFile) ? diagnosticFile : path7.resolve(workspaceRoot, diagnosticFile);
|
|
947
1056
|
}
|
|
948
1057
|
function shouldSkipFile(file, workspaceRoot) {
|
|
949
|
-
const rel =
|
|
950
|
-
if (rel.startsWith("node_modules") || rel.includes(`${
|
|
1058
|
+
const rel = path7.relative(workspaceRoot, file);
|
|
1059
|
+
if (rel.startsWith("node_modules") || rel.includes(`${path7.sep}node_modules${path7.sep}`)) {
|
|
951
1060
|
return "node_modules";
|
|
952
1061
|
}
|
|
953
1062
|
if (file.endsWith(".d.ts")) {
|
|
954
1063
|
return "declaration_file";
|
|
955
1064
|
}
|
|
956
|
-
if (!
|
|
1065
|
+
if (!fs7.existsSync(file)) {
|
|
957
1066
|
return "file_not_found";
|
|
958
1067
|
}
|
|
959
1068
|
return null;
|
|
@@ -1012,7 +1121,7 @@ function stubAndContinue(opts) {
|
|
|
1012
1121
|
}
|
|
1013
1122
|
continue;
|
|
1014
1123
|
}
|
|
1015
|
-
const source =
|
|
1124
|
+
const source = fs7.readFileSync(file, "utf-8");
|
|
1016
1125
|
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1017
1126
|
const lines = source.split(/\r?\n/);
|
|
1018
1127
|
entries.sort((a, b) => b.line - a.line);
|
|
@@ -1054,10 +1163,10 @@ function stubAndContinue(opts) {
|
|
|
1054
1163
|
if (edited) {
|
|
1055
1164
|
filesEditedSet.add(file);
|
|
1056
1165
|
if (!dryRun) {
|
|
1057
|
-
|
|
1058
|
-
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${
|
|
1166
|
+
fs7.writeFileSync(file, lines.join(eol), "utf-8");
|
|
1167
|
+
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1059
1168
|
} else {
|
|
1060
|
-
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${
|
|
1169
|
+
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1061
1170
|
}
|
|
1062
1171
|
}
|
|
1063
1172
|
}
|
|
@@ -1108,8 +1217,9 @@ function refreshDiagnostics(workspaceRoot, files) {
|
|
|
1108
1217
|
return result.diagnostics.filter((d) => d.category === "error");
|
|
1109
1218
|
}
|
|
1110
1219
|
async function runMendLoop(opts) {
|
|
1111
|
-
const { context, llm, maxIterations = 3, dryRun = false, stubOnFailure = false, _callLLM } = opts;
|
|
1220
|
+
const { context: rawContext, llm, maxIterations = 3, dryRun = false, stubOnFailure = false, _callLLM } = opts;
|
|
1112
1221
|
const startMs = Date.now();
|
|
1222
|
+
const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
|
|
1113
1223
|
const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
|
|
1114
1224
|
if (diagnosticsBefore.length === 0) {
|
|
1115
1225
|
return {
|
|
@@ -1222,7 +1332,7 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1222
1332
|
const walk = (dir) => {
|
|
1223
1333
|
let entries;
|
|
1224
1334
|
try {
|
|
1225
|
-
entries =
|
|
1335
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1226
1336
|
} catch {
|
|
1227
1337
|
return;
|
|
1228
1338
|
}
|
|
@@ -1231,10 +1341,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1231
1341
|
if (skip.has(e.name)) {
|
|
1232
1342
|
continue;
|
|
1233
1343
|
}
|
|
1234
|
-
walk(
|
|
1344
|
+
walk(path8.join(dir, e.name));
|
|
1235
1345
|
} else if (e.isFile() && !e.name.endsWith(".d.ts")) {
|
|
1236
1346
|
if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
|
|
1237
|
-
out.push(
|
|
1347
|
+
out.push(path8.relative(workspaceRoot, path8.join(dir, e.name)));
|
|
1238
1348
|
}
|
|
1239
1349
|
}
|
|
1240
1350
|
}
|
|
@@ -1245,10 +1355,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1245
1355
|
function runValidationLoop(opts) {
|
|
1246
1356
|
const { workspaceRoot, skipLSPFixer = false, dryRun = false } = opts;
|
|
1247
1357
|
const logger = opts.logger ?? noopLogger3;
|
|
1248
|
-
if (!
|
|
1358
|
+
if (!fs8.existsSync(workspaceRoot)) {
|
|
1249
1359
|
throw new Error(`workspace not found: ${workspaceRoot}`);
|
|
1250
1360
|
}
|
|
1251
|
-
if (!
|
|
1361
|
+
if (!fs8.existsSync(path8.join(workspaceRoot, "tsconfig.json"))) {
|
|
1252
1362
|
throw new Error(`no tsconfig.json in ${workspaceRoot}`);
|
|
1253
1363
|
}
|
|
1254
1364
|
const targetFiles = opts.targetFiles ?? discoverTsFiles(workspaceRoot);
|
|
@@ -1296,9 +1406,13 @@ function runValidationLoop(opts) {
|
|
|
1296
1406
|
};
|
|
1297
1407
|
}
|
|
1298
1408
|
export {
|
|
1409
|
+
BUILT_IN_LIBRARY_MIGRATIONS,
|
|
1299
1410
|
applyEditBlocks,
|
|
1300
1411
|
applySingleBlock,
|
|
1412
|
+
detectLibraryMigrations,
|
|
1301
1413
|
discoverTsFiles,
|
|
1414
|
+
formatLibraryMigrationsBlock,
|
|
1415
|
+
formatLibraryMigrationsTaskDescription,
|
|
1302
1416
|
getTypeContext,
|
|
1303
1417
|
isInProcessTscEnabled,
|
|
1304
1418
|
isLSPFixerEnabled,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -156,6 +156,20 @@ export interface MendContext {
|
|
|
156
156
|
priorTaskExports?: string;
|
|
157
157
|
/** Compact type signatures of installed npm dependencies (helps prevent API hallucination). */
|
|
158
158
|
installedTypes?: string;
|
|
159
|
+
/**
|
|
160
|
+
* Library-migration hints for installed deps whose breaking changes are
|
|
161
|
+
* known to mislead tsc's quick-fix. When non-empty, the Layer 2 prompt
|
|
162
|
+
* leads with these and the `taskDescription` is overridden to name them.
|
|
163
|
+
*
|
|
164
|
+
* Auto-populated by `runMendLoop` from `<workspaceRoot>/package.json` if
|
|
165
|
+
* the caller doesn't set it. Pass `[]` explicitly to opt out.
|
|
166
|
+
*
|
|
167
|
+
* See `libraryMigrations.ts` for the built-in registry.
|
|
168
|
+
*/
|
|
169
|
+
libraryMigrations?: Array<{
|
|
170
|
+
name: string;
|
|
171
|
+
hint: string;
|
|
172
|
+
}>;
|
|
159
173
|
}
|
|
160
174
|
/**
|
|
161
175
|
* Per-layer event for streaming telemetry across the validate → fix → mend
|
|
@@ -194,3 +208,5 @@ export { runMendLoop } from "./runMendLoop.js";
|
|
|
194
208
|
export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
|
|
195
209
|
export { stubAndContinue } from "./stubAndContinue.js";
|
|
196
210
|
export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.js";
|
|
211
|
+
export { BUILT_IN_LIBRARY_MIGRATIONS, detectLibraryMigrations, formatLibraryMigrationsBlock, formatLibraryMigrationsTaskDescription, } from "./libraryMigrations.js";
|
|
212
|
+
export type { LibraryMigrationHint } from "./libraryMigrations.js";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library breaking-change registry for Layer 2.
|
|
3
|
+
*
|
|
4
|
+
* When an installed dependency has a known migration whose correct fix
|
|
5
|
+
* differs from tsc's own quick-fix suggestion, this module injects a hint
|
|
6
|
+
* into the mend prompt so the LLM doesn't blindly follow tsc.
|
|
7
|
+
*
|
|
8
|
+
* Empirically grounded — each entry corresponds to a concrete bench case
|
|
9
|
+
* where, without the hint, both haiku-4-5 and sonnet-4-5 score 0/3
|
|
10
|
+
* functional+secure and follow tsc's misleading quick-fix; with the hint,
|
|
11
|
+
* both score 3/3 functional+secure on the same fixture.
|
|
12
|
+
*
|
|
13
|
+
* Scope: library MIGRATIONS where tsc's quick-fix is misleading or where
|
|
14
|
+
* the correct fix requires syntax not present in the source. NOT for
|
|
15
|
+
* general TS errors — those are tsfix's deterministic Layer 0/1 surface.
|
|
16
|
+
*/
|
|
17
|
+
export interface LibraryMigrationHint {
|
|
18
|
+
/** Display name, e.g., `"vite-plugin-svgr@^4.0.0"`. Populated by detect. */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Instructional text injected into the Layer 2 system prompt. */
|
|
21
|
+
hint: string;
|
|
22
|
+
}
|
|
23
|
+
interface RegistryEntry {
|
|
24
|
+
match: {
|
|
25
|
+
name: string;
|
|
26
|
+
minMajor?: number;
|
|
27
|
+
maxMajor?: number;
|
|
28
|
+
};
|
|
29
|
+
hint: string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Built-in registry. Keep entries SMALL — only patterns where we have
|
|
33
|
+
* empirical evidence that the model picks the wrong fix without the hint.
|
|
34
|
+
* Don't lard this up with general advice; that belongs in the system prompt.
|
|
35
|
+
*/
|
|
36
|
+
export declare const BUILT_IN_LIBRARY_MIGRATIONS: RegistryEntry[];
|
|
37
|
+
/**
|
|
38
|
+
* Read the workspace's package.json, walk dependencies + devDependencies,
|
|
39
|
+
* return the registry entries whose match rule fires.
|
|
40
|
+
*
|
|
41
|
+
* Best-effort: returns `[]` on any failure (missing package.json, parse
|
|
42
|
+
* error, etc.). Never throws.
|
|
43
|
+
*/
|
|
44
|
+
export declare function detectLibraryMigrations(workspaceRoot: string, registry?: RegistryEntry[]): LibraryMigrationHint[];
|
|
45
|
+
/**
|
|
46
|
+
* Format an array of hints into a prompt block. Empty input → empty string,
|
|
47
|
+
* caller can short-circuit.
|
|
48
|
+
*/
|
|
49
|
+
export declare function formatLibraryMigrationsBlock(hints: LibraryMigrationHint[]): string;
|
|
50
|
+
/**
|
|
51
|
+
* Build the one-line task description from a list of hints. Empty input
|
|
52
|
+
* → undefined. We've found that putting library names in the
|
|
53
|
+
* `taskDescription` (the prompt's headline framing) is dramatically more
|
|
54
|
+
* effective than burying the same content in the body.
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatLibraryMigrationsTaskDescription(hints: LibraryMigrationHint[]): string | undefined;
|
|
57
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipispec/tsfix",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "TypeScript error-recovery for LLM-generated code. Layer 0/1 deterministic auto-fix via the TS Language Service + Layer 2 LLM mend (Vercel AI SDK + Anthropic) in one package.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|