@shipispec/tsfix 0.4.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 +126 -1
- package/README.md +65 -11
- package/dist/cli.js +999 -28
- package/dist/index.d.ts +24 -0
- package/dist/index.js +397 -34719
- package/dist/types/index.d.ts +24 -0
- package/dist/types/libraryMigrations.d.ts +57 -0
- package/dist/types/runMendLoop.d.ts +16 -1
- package/dist/types/stubAndContinue.d.ts +68 -0
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,129 @@ 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
|
+
|
|
71
|
+
### Added (Layer 4 — stub-and-continue escape hatch)
|
|
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.
|
|
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.
|
|
74
|
+
- **Idempotency** — re-running `stubAndContinue` on an already-stubbed workspace is a no-op. Detects existing `@ts-expect-error` / `@ts-ignore` directives on the line above and skips.
|
|
75
|
+
- **Safe skips** — `node_modules/`, `.d.ts` files, missing files, and lines beyond file length are recorded as `skipped` (with reason) rather than crashing.
|
|
76
|
+
- **Multi-error coalescing** — multiple diagnostics on the same line collapse into one stub comment listing all TS codes and joined messages.
|
|
77
|
+
- **Indent + CRLF preservation** — comment matches the indentation of the line it's stubbing; CRLF line endings on Windows-authored files survive the rewrite.
|
|
78
|
+
- **`dryRun`** support — same semantics as Layer 2: reports `stubsApplied` without writing.
|
|
79
|
+
|
|
80
|
+
### Added (fixture engine — Day 2/3 mutators)
|
|
81
|
+
- **5 new ts-morph mutators** covering codes the original 3-mutator set didn't reach:
|
|
82
|
+
- `ts2322-incompatible-return.mjs` — replaces a return expression with a wrong-typed primitive literal in a function with a primitive return type
|
|
83
|
+
- `ts2304-cannot-find-name.mjs` — renames a value-position identifier (variable, call, parameter usage) to a no-near-match string; Layer 0's auto-import abstains because there's no candidate
|
|
84
|
+
- `ts2345-arg-type-mismatch.mjs` — replaces a function-call argument with a wrong-typed primitive when the parameter's declared type is `string` / `number` / `boolean`
|
|
85
|
+
- `ts2554-arg-count-mismatch.mjs` — drops the trailing argument from a call that currently satisfies its signature
|
|
86
|
+
- `ts2365-operator-mismatch.mjs` — replaces one operand of a numeric binary expression (`<`, `>`, `<=`, `>=`, `-`, `*`, `/`, `%`) with a string literal
|
|
87
|
+
- **50 new generated fixtures** (10 per new code × 8 codes total). Total Layer-2 fixture corpus: **85** (was 35) — 3 minimal + 2 realistic + 80 generated across 8 codes. Total fixture count across all layers: **99** (14 Layer-0 + 85 Layer-2).
|
|
88
|
+
|
|
89
|
+
### Added (tests)
|
|
90
|
+
- **19 new Layer-4 unit tests** — 16 in `stubAndContinue.test.ts` + 3 in `runMendLoop.test.ts` covering single error, multi-error-same-line, multi-code, indent preservation, descending-order processing, idempotency, node_modules skip, .d.ts skip, missing-file skip, dry-run, message truncation, CRLF preservation, first-line edge case, no-eligible case, warning/suggestion filtering, and the runMendLoop integration (stopReason flip, default-off behavior, dryRun interaction).
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
- **Public surface** at `src/index.ts` extended with `stubAndContinue`, `StubAndContinueOptions`, `StubAndContinueResult`, `AppliedStub`, `SkippedStub`. Layer 0/1/2 surface unchanged.
|
|
94
|
+
- **`RunMendLoopOptions`** gains `stubOnFailure?: boolean`. **`RunMendLoopResult`** gains optional `stubs?: AppliedStub[]`. **`StopReason`** union gains `"stubbed"`. All additive — old callers unaffected.
|
|
95
|
+
- **`scripts/generate-fixtures.mjs`** now runs via `tsx` and imports from `src/index.ts` directly instead of `dist/index.js`. Reason: the v0.4.0 dist bundle inlines `@vercel/oidc` (transitive of `ai`), which uses dynamic `require()` patterns that fail under esbuild's ESM output at module-init time. The generator only needs Layer 0/1 entry points, so importing from source bypasses the AI SDK entirely. Side benefit: no `npm run build` prerequisite — `npm run generate-fixtures` works from a fresh clone.
|
|
96
|
+
- Removed `pregenerate-fixtures: npm run build` hook from `package.json`.
|
|
97
|
+
|
|
98
|
+
### Fixed
|
|
99
|
+
- **`stubAndContinue` resolves relative paths** against `workspaceRoot`. Diagnostics from `runInProcessTsc` use relative paths; consumers may pass absolute. Both work.
|
|
100
|
+
|
|
101
|
+
### Added (Layer-2 benchmark — Day 4)
|
|
102
|
+
- **Parallelism** — `npm run benchmark:llm` now runs fixtures concurrently via an inline `pLimit(N)` semaphore (no new dep). Default concurrency is 8; configurable via `--concurrency=N`. 100 fixtures at ~1.5s/each: sequential ~2 min → parallel ~20s. Per-fixture workspaces are isolated (snapshot/restore is local) so parallelism is safe; tsfix's program cache thrashes harmlessly between fixtures.
|
|
103
|
+
- **File-based response cache** — every LLM call is keyed by `sha256(systemBlock + userBlock + model)` and stored under `.benchmark-cache/<hash>.json`. Re-runs against unchanged fixtures replay cached responses for free. Any change to the system prompt template, fixture content, or model invalidates automatically (it's all in the hash). `--no-cache` bypasses; `--clear-cache` wipes and exits. `.benchmark-cache/` added to `.gitignore`.
|
|
104
|
+
- **Cache module** — extracted to `benchmark/cache.ts` so the logic is unit-testable independent of the full benchmark. 16 new unit tests covering: deterministic keying, key sensitivity per input, hex format, separator-confusion resistance, round-trip read/write, corrupted-entry handling, miss → store → hit cycle, parameter discrimination, bypass behavior, apiKey NOT in the cache key (rotating keys doesn't invalidate), error propagation without poisoning the cache.
|
|
105
|
+
- **Failure reporting** — when fixtures fail, the per-iteration LLM raw response dump is collected and printed in a single block at the end of the run (instead of inline during the loop, which would interleave under concurrency).
|
|
106
|
+
- **Layer-2 fixture filter (LLM benchmark)** — the LLM benchmark now filters fixtures by `expected.json` shape (`costUsdMax` or `expectedErrorCode`), mirroring the Layer-0 benchmark's filter. Prevents accidentally running Layer-0 fixtures through the LLM.
|
|
107
|
+
- **`benchmark/run-llm-benchmark.ts` rewritten** around the parallel + cached worker model. Per-fixture output gets a `[n/m]` progress prefix and prints in completion order; final summary sorted by name for deterministic output. Total wall time, total cost (cache misses only — hits are free), and cache hit rate are reported at the end.
|
|
108
|
+
|
|
109
|
+
### Added (CLI — Layer 2 exposure)
|
|
110
|
+
- **`--llm` flag** — opt-in escalation to Layer 2 (single-file LLM mend) for errors that survive Layer 0/1. Off by default; CLI default path remains zero-network.
|
|
111
|
+
- **`--llm-model <name>`** — Anthropic model (default `claude-haiku-4-5`). Known-priced models hardcoded for cost estimation: `claude-haiku-4-5`, `claude-sonnet-4-5`, `claude-opus-4-7`. Unknown models warn and report cost as 0.
|
|
112
|
+
- **`--llm-max-iterations <N>`** — cap on LLM retries (default 3).
|
|
113
|
+
- **`--llm-budget-usd <amount>`** — soft cost cap. If exceeded, exits with code 3 (Layer 2 still ran; partial work persisted to disk).
|
|
114
|
+
- **Exit code 3** added — Layer 2 budget exceeded.
|
|
115
|
+
- **Validation:** `--llm` without `ANTHROPIC_API_KEY` → exit 2 with helpful error. `--llm + --dry-run` is rejected as mutually exclusive (Layer 2 writes patches to disk).
|
|
116
|
+
- **JSON report extension** — `layer2: { ran, stopReason, errorsBefore, errorsAfter, iterations, totalInputTokens, totalOutputTokens, totalCostUsd, budgetExceeded, model } | null`. Layer 0/1 surface unchanged.
|
|
117
|
+
- **Human report extension** — new "Layer 2 (LLM)" line in the per-run summary when `--llm` was used. Shows error count delta, iteration count, tokens, cost, and stopReason.
|
|
118
|
+
- **13 new CLI integration tests** in `cli/run-stack.test.ts` covering argument validation, exit codes, no-key errors, mutual-exclusion checks, JSON report shape, and the no-Layer-2-when-Layer-0-clean path. Tests spawn the actual `tsx cli/run-stack.ts` process — catches integration issues that pure unit tests of `parseArgs` would miss.
|
|
119
|
+
|
|
120
|
+
### Fixed (latent v0.4.0 bundle bug)
|
|
121
|
+
- **Externalized `ai` and `@ai-sdk/anthropic`** from the esbuild bundle in `scripts/build.mjs`. v0.4.0 inlined them, which (1) bloated `dist/index.js` from ~25 KB to 1.3 MB, and (2) crashed under plain `node` execution at module-init time because `@vercel/oidc` (transitive) uses dynamic `require()` patterns that fail in ESM bundles. Both packages are declared in `dependencies` so npm install pulls them in automatically for consumers using Layer 2. **Bundle sizes after fix:** `dist/index.js` 1.3 MB → 36 KB; `dist/cli.js` ~22 KB → 45 KB. Library import via plain `node` now works (verified with `node -e 'import("./dist/index.js")'`).
|
|
122
|
+
|
|
123
|
+
### Deferred (fixture engine)
|
|
124
|
+
- **TS2532** (Object is possibly undefined) — seeds don't currently contain optional chains or `Map.get()`-style calls that would produce TS2532 deterministically. Mutator deferred until seeds expand or a real-failure capture provides better candidates.
|
|
125
|
+
- **TS2551-negative** (LSP returns multiple equally-close fix candidates → abstains) — engineering a deterministic TS2551 case where Layer 0's `fixesAreEquivalent` check abstains is contrived. Defer until we see a real-world example.
|
|
126
|
+
|
|
127
|
+
### Also changed (CLI)
|
|
128
|
+
- **CLI public surface** at `cli/run-stack.ts` extended with the Layer 2 flags listed above. The bin entry (`dist/cli.js`) now imports `runMendLoop`, `runInProcessTsc` and the contract types from `src/index.ts` in addition to the previous `runValidationLoop` + `discoverTsFiles`. Tree-shaking keeps Layer 2 out of the bundle's code path unless `--llm` is set.
|
|
129
|
+
|
|
7
130
|
## [0.4.0] - 2026-05-14
|
|
8
131
|
|
|
9
132
|
**Layer 2 LLM mend is now in-package.** The previously-planned sister package `@shipispec/tsmend` has been folded into `tsfix` so the deterministic Layer 0/1 stack and the LLM-driven Layer 2 stack ship as one. This reverses the v0.3.0 sister-package decision (D3) — see roadmap update.
|
|
@@ -142,7 +265,9 @@ Initial public release. **Layers 0–1 only** (deterministic detection + auto-fi
|
|
|
142
265
|
- Node `>=20.9.0` (matches VS Code Extension Host runtime)
|
|
143
266
|
- TypeScript `>=5.0.0` (peer dep, must be installed in the consuming workspace)
|
|
144
267
|
|
|
145
|
-
[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
|
|
146
271
|
[0.4.0]: https://github.com/owgreen-dev/tsfix/compare/v0.3.0...v0.4.0
|
|
147
272
|
[0.3.0]: https://github.com/owgreen-dev/tsfix/compare/v0.2.0...v0.3.0
|
|
148
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
|
|