@shipispec/tsfix 0.3.0 → 0.4.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 CHANGED
@@ -4,6 +4,51 @@ All notable changes to `@shipispec/tsfix` are documented here. Format follows [K
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.4.0] - 2026-05-14
8
+
9
+ **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.
10
+
11
+ ### Added (Layer 2 — single-file LLM mend)
12
+ - **`getTypeContext(opts)`** — TS Language Service helper. Resolves an error site to its declaring type via `getTypeAtLocation()` + `getDeclarations()`, slices ±3 lines around the error site and ±20 lines around the declaration. Bounded walk-up (4 hops) plus a special case for `PropertyAccessExpression` so TS2339 errors resolve to the *receiver's* type, not the non-existent property's. The architectural moat — no other OSS tool does this for TypeScript specifically.
13
+ - **`mendSingleFile(opts)`** — single-LLM repair via Vercel AI SDK + `@ai-sdk/anthropic`. Uses top-level `system:` parameter (v6 pattern), markdown-headered file delimiters in the prompt (XML wrappers caused Claude to mirror them in output and break the parser). Returns `rawResponse`, parsed `blocks`, `apply` result, token counts, latency.
14
+ - **`applySingleBlock(content, search, replace)`** + **`applyEditBlocks(opts)`** + **`parseEditBlocks(text)`** — Aider-style `editblock` parser and 3-tier fuzzy applier (exact → rstrip → strip). Defensive parser handles `<file path="…">` wrappers Claude emits when the system prompt uses XML markers. Abstains on ambiguous matches (multiple hits) rather than guess.
15
+ - **`runMendLoop(opts)`** — bounded retry (default 3 iterations) with no-progress / regression detection via error-signature-set comparison. Streams per-iteration data: `patchesApplied`, `patchesFailed`, `inputTokens`, `outputTokens`, `latencyMs`, `rawResponse`. Stop reasons: `noErrors`, `fixed`, `noProgress`, `regressed`, `maxIterations`.
16
+
17
+ ### Added (fixtures + harness)
18
+ - **3 hand-authored minimal Layer-2 fixtures** — `mend-ts2339-property-typo`, `mend-ts7006-implicit-any`, `mend-ts2741-missing-prop`.
19
+ - **2 realistic Layer-2 fixtures** — `realistic-multi-error-user-helpers` (3 errors, 1 file, `taskDescription` populated), `realistic-rename-ripple` (2 errors, 2 files).
20
+ - **30 auto-generated fixtures** via `scripts/generate-fixtures.mjs` (ts-morph AST mutators × 3 codes × 3 seeds × 10 each). Total Layer-2 fixture corpus: **35**.
21
+ - **`benchmark/run-llm-benchmark.ts`** (`npm run benchmark:llm`) — Layer 2 live LLM benchmark against Anthropic. Skips silently with exit 0 when `ANTHROPIC_API_KEY` is unset.
22
+ - **`scripts/generate-fixtures.mjs`** (`npm run generate-fixtures`) — ts-morph AST mutators that introduce one targeted error per fixture into a valid seed file. Validation gate: every mutation runs through `runInProcessTsc` to confirm the expected error code, then through `runValidationLoop` to confirm Layer 0 abstains. Memory-bounded shared `Project` + tempDir + cache resets to prevent OOMs.
23
+
24
+ ### Added (tests)
25
+ - **33 unit tests** across `typeContext`, `applyEditBlock`, `mendAgent`, `runMendLoop`. Mocked LLM call via injectable `_callLLM` — tests never hit the real API.
26
+
27
+ ### Added (CI)
28
+ - Workflow gains a Layer-2 benchmark step gated on `ANTHROPIC_API_KEY` (skips cleanly when unset). Existing Layer-0 benchmark + matrix steps unchanged.
29
+ - Bumped `actions/checkout` + `actions/setup-node` v4 → v5.
30
+
31
+ ### Changed
32
+ - **Dependencies added (runtime):** `@ai-sdk/anthropic@^3.0.44`, `ai@^6.0.86`. Previous "near-zero deps" north star (Layer 0/1 only) is superseded — package now spans Layer 0/1/2.
33
+ - **Dependencies added (dev):** `ts-morph@^28.0.0` (fixture generation).
34
+ - **Public-API surface** at `src/index.ts` extended with the Layer-2 exports listed above. Layer 0/1 surface unchanged — `runValidationLoop`, `runInProcessTsc`, `runLSPFixerPass`, `discoverTsFiles`, and the contract types stay byte-identical.
35
+ - **Roadmap decision D3 reversed** — previous decision was "mend in sister package"; current decision is "mend in-package." Updated in `tsc-defense-roadmap.md`.
36
+
37
+ ### Engines
38
+ - Node `>=20.9.0` (unchanged)
39
+ - TypeScript `>=5.0.0` peer (unchanged)
40
+
41
+ ### Performance signals (Layer 2, 35-fixture run, claude-haiku-4-5)
42
+
43
+ | Metric | Target | Observed |
44
+ |---|---|---|
45
+ | Pass rate | ≥70% (Haiku floor) | **100%** (35/35) |
46
+ | Iter-1 success | ≥40% | **97%** (34/35) |
47
+ | Cost / fixture | ≤$0.005 | **$0.001 avg** |
48
+ | Latency / fixture | P95 ≤25s | ~1.5s |
49
+
50
+ Caveat: 30 of 35 fixtures are single-error mutations of 3 seeds. Real-world diversity will dent these numbers.
51
+
7
52
  ## [0.3.0] - 2026-05-07
8
53
 
9
54
  Phase 2 contract release. **Establishes the public types `MendContext`, `LayerEvent`, and `Diagnostic` so a downstream LLM-mend package (e.g. `@shipispec/tsmend`) can consume tsfix's output without redefining the shape.** No behavior changes; purely additive types. Also collapses several dev-only improvements that landed since v0.2.0 into a single release.
@@ -97,7 +142,8 @@ Initial public release. **Layers 0–1 only** (deterministic detection + auto-fi
97
142
  - Node `>=20.9.0` (matches VS Code Extension Host runtime)
98
143
  - TypeScript `>=5.0.0` (peer dep, must be installed in the consuming workspace)
99
144
 
100
- [Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.3.0...HEAD
145
+ [Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.4.0...HEAD
146
+ [0.4.0]: https://github.com/owgreen-dev/tsfix/compare/v0.3.0...v0.4.0
101
147
  [0.3.0]: https://github.com/owgreen-dev/tsfix/compare/v0.2.0...v0.3.0
102
148
  [0.2.0]: https://github.com/owgreen-dev/tsfix/compare/v0.1.1...v0.2.0
103
149
  [0.1.1]: https://github.com/owgreen-dev/tsfix/compare/v0.1.0...v0.1.1
package/README.md CHANGED
@@ -1,12 +1,15 @@
1
1
  # tsfix
2
2
 
3
- > Headless TypeScript error recovery auto-resolve `TS2304`, `TS2305`, `TS2551`, `TS2552`, `TS2724` before they reach a human.
3
+ > Two-layer 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, and escalate the rest to a single-file LLM mend.
4
4
 
5
- `@shipispec/tsfix` borrows the same TypeScript Language Service that powers VS Code's "Quick Fix" lightbulb and runs it as a CLI. Point it at a workspace, it fixes typos, missing imports, and did-you-mean errors deterministically no LLM, no calls home, no config.
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 two layers:
6
6
 
7
- Built for the case where you've just generated a few hundred files of TypeScript with an LLM and `tsc --noEmit` is screaming at you.
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
9
 
9
- ## Before / after
10
+ Layer 2 only runs if you explicitly call its API or set `ANTHROPIC_API_KEY` and use the `runMendLoop` entry point. The default `tsfix --workspace ...` CLI is still **Layer 0/1 only**.
11
+
12
+ ## Before / after (Layer 0)
10
13
 
11
14
  ```
12
15
  $ tsc --noEmit
@@ -62,7 +65,9 @@ npx @shipispec/tsfix --workspace . --json
62
65
  | `--verbose` | Per-fix logging. |
63
66
  | `--help` | Print usage. |
64
67
 
65
- ## What it fixes
68
+ The CLI does not run Layer 2 — call the library API for that (below).
69
+
70
+ ## What Layer 0 fixes
66
71
 
67
72
  | TS code | Meaning | What tsfix does |
68
73
  |---|---|---|
@@ -72,11 +77,11 @@ npx @shipispec/tsfix --workspace . --json
72
77
  | `TS2552` | Cannot find name, did you mean Y | Spelling fix |
73
78
  | `TS2724` | Module member did-you-mean | Import rename |
74
79
 
75
- Against a 14-fixture benchmark spanning typos, did-you-mean cases, multi-file ripples, and 4 API-drift scenarios: **14/14 fixtures pass and 14/25 errors are auto-fixed (56%).** The remaining errors are intentionally outside Layer 0's scope (see below).
80
+ Against a 14-fixture benchmark spanning typos, did-you-mean cases, multi-file ripples, and 4 API-drift scenarios: **14/14 fixtures pass and 14/25 errors are auto-fixed (56%).** The remaining errors are intentionally outside Layer 0's scope and escape to Layer 2.
76
81
 
77
- ## What it does *not* fix
82
+ ## What Layer 0 does *not* fix (Layer 2 picks these up)
78
83
 
79
- By design, tsfix only applies fixes that are **deterministic** and **non-structural**. It will refuse to:
84
+ By design, Layer 0 only applies fixes that are **deterministic** and **non-structural**. It refuses to:
80
85
 
81
86
  - Add or remove function declarations
82
87
  - Insert type annotations or change types
@@ -84,25 +89,33 @@ By design, tsfix only applies fixes that are **deterministic** and **non-structu
84
89
  - Rewrite JSX trees
85
90
  - Add object-literal stub properties
86
91
 
87
- The internal allowlist is two-layered: error codes (`SAFE_FIXABLE_CODES`) and Quick Fix names (`SAFE_FIX_NAMES = ['import', 'fixImport', 'spelling', 'fixSpelling']`). When the language service offers anything outside that allowlist, tsfix abstains and surfaces the error in the result so a higher layer (LLM, human) can pick it up.
92
+ The internal allowlist is two-layered: error codes (`SAFE_FIXABLE_CODES`) and Quick Fix names (`SAFE_FIX_NAMES = ['import', 'fixImport', 'spelling', 'fixSpelling']`). When the language service offers anything outside that allowlist, Layer 0 abstains and surfaces the error so Layer 2 (or a human) can pick it up.
88
93
 
89
- ## The four-layer model
94
+ Layer 2 is built for the cases the LSP can't statically resolve:
90
95
 
91
- tsfix is **Layer 0–1** of a larger error-recovery stack. The other layers are LLM-driven and live elsewhere (in your own code, or in companion packages):
96
+ - `TS2339` Property doesn't exist on type. The LLM needs to see *the type's declaration* to decide whether the receiver should grow a field, the call site has a typo with no near-match, or the receiver is the wrong type entirely.
97
+ - `TS7006` — Implicit `any`. The LLM picks the right annotation from surrounding context.
98
+ - `TS2741` — Missing required property. The LLM sees the contextual type and supplies a real value, not a placeholder.
99
+
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
+
102
+ ## The four-layer model
92
103
 
93
104
  ```
94
105
  Layer 0 — Prevention (prompt rules, exported-API injection — your problem)
95
- Layer 1 — tsfix (this package: deterministic auto-fix)
96
- ─────────────────────────────────────────────────────────────────────────
97
- Layer 2 — Single-file LLM mend (architect + editor split)
98
- Layer 3 — Multi-file LLM mend (blast-radius search/replace)
99
- Layer 4 — Stub-and-continue (escape hatch)
106
+ Layer 1 — Deterministic (this package: LSP auto-fix, CLI default)
107
+ Layer 2 — Single-file LLM (this package: opt-in via library API)
108
+ ─────────────────────────────────────────────────────────────────
109
+ Layer 3 — Multi-file LLM (planned: blast-radius search/replace via findReferences)
110
+ Layer 4 — Stub-and-continue (planned: escape hatch)
100
111
  ```
101
112
 
102
- 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.
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.
103
114
 
104
115
  ## Library API
105
116
 
117
+ ### Layer 0/1 — deterministic loop
118
+
106
119
  ```typescript
107
120
  import { runValidationLoop } from '@shipispec/tsfix';
108
121
 
@@ -121,24 +134,62 @@ result.lspFixer.filesEdited; // string[]
121
134
  result.passed; // boolean — true if errorsAfter === 0
122
135
  ```
123
136
 
124
- Other exports:
137
+ Other Layer 0/1 exports:
125
138
 
126
139
  - `runInProcessTsc(opts)` — validation only, no fixer. Returns structured diagnostics.
127
140
  - `runLSPFixerPass(opts)` — Layer 0 fixer alone, no validation loop wrapper.
128
141
  - `discoverTsFiles(workspaceRoot)` — file-walking helper. Skips `node_modules`, `.next`, `dist`, `build`, `out`, `coverage`, `.git`.
129
142
 
143
+ ### Layer 2 — LLM mend (opt-in)
144
+
145
+ ```typescript
146
+ import { runValidationLoop, runMendLoop } from '@shipispec/tsfix';
147
+
148
+ // Layer 0/1 first.
149
+ const layer1 = runValidationLoop({ workspaceRoot });
150
+
151
+ if (!layer1.passed) {
152
+ // Layer 2 escalation.
153
+ const layer2 = await runMendLoop({
154
+ context: {
155
+ workspaceRoot,
156
+ diagnostics: layer1.remainingDiagnostics,
157
+ erroredFiles: layer1.lspFixer.filesWithErrors,
158
+ // Optional fields that improve mend quality:
159
+ // taskDescription: 'Build a user CRUD module',
160
+ // featureSpecText: '...the markdown spec...',
161
+ // acceptanceCriteria: '...',
162
+ // installedTypes: '...', // compact API surface from npm deps
163
+ },
164
+ llm: {
165
+ provider: 'anthropic',
166
+ model: 'claude-haiku-4-5',
167
+ apiKey: process.env.ANTHROPIC_API_KEY,
168
+ },
169
+ maxIterations: 3,
170
+ });
171
+
172
+ console.log(layer2.stopReason); // 'fixed' | 'noProgress' | 'regressed' | 'maxIterations'
173
+ console.log(layer2.totalCostUsd);
174
+ }
175
+ ```
176
+
177
+ Other Layer 2 exports:
178
+
179
+ - `mendSingleFile(opts)` — one LLM call for one file. The building block under `runMendLoop`.
180
+ - `getTypeContext(opts)` — resolve a `Diagnostic` to its declaring type via the TS Language Service and return ±N lines around the declaration. The architectural moat — every other LLM-driven repair tool uses generic grep or repo-maps.
181
+ - `parseEditBlocks(text)` / `applyEditBlocks(opts)` — Aider-style SEARCH/REPLACE patch parser + 3-tier fuzzy applier.
182
+ - Types: `MendContext`, `LayerEvent`, `Diagnostic`, plus the per-function option/result types.
183
+
130
184
  ## Trust model
131
185
 
132
- tsfix loads `typescript` from your workspace's `node_modules` — it does **not** bundle its own. This is intentional: it ensures the fixer behaves identically to the `tsc` your project actually compiles with.
186
+ Layer 0/1 loads `typescript` from your workspace's `node_modules` — it does **not** bundle its own. This ensures the fixer behaves identically to the `tsc` your project actually compiles with.
133
187
 
134
188
  > **Run tsfix only on workspaces you trust.** Loading `typescript` from an attacker-controlled `node_modules` is equivalent to running `node_modules/.bin/tsc` against it.
135
189
 
136
- Other surface:
190
+ **Network surface (Layer 0/1):** none. No telemetry, no calls home, no background processes, no config files written outside `--workspace`.
137
191
 
138
- - No network calls.
139
- - No telemetry.
140
- - No background processes.
141
- - No config files written or modified outside `--workspace`.
192
+ **Network surface (Layer 2):** every `mendSingleFile` call hits Anthropic's API via the Vercel AI SDK. The source files in `MendContext.erroredFiles` and the resolved type-context slices are sent in the prompt. If your code is sensitive, do not call Layer 2 — the CLI never does, and the library exports are explicit.
142
193
 
143
194
  ## Engines
144
195
 
@@ -154,7 +205,7 @@ run: npm install --save-dev typescript
154
205
 
155
206
  ## Contributing
156
207
 
157
- The contract for adding a fix:
208
+ ### Adding a Layer-0 fix
158
209
 
159
210
  1. **Probe** — write a tiny test workspace with the exact error you want fixable. Drop it under `fixtures/<descriptive-name>/` with an `expected.json` declaring `errorsBefore`, `errorsAfterMax`, `lspFixesAppliedMin/Max`, and `mustPass`.
160
211
  2. **Verify** — run `npm run benchmark -- --fixture <name>` and inspect what the language service offers (the `fix.fixName` field).
@@ -163,7 +214,18 @@ The contract for adding a fix:
163
214
 
164
215
  Each new code/fix-name pair gets its own fixture. We don't trust the language service blindly — we trust it under specific, pinned conditions.
165
216
 
166
- `npm run matrix` runs the same package against 6 distinct project shapes (Next.js, Vite + React, plain `nodenext`, plain `bundler`, plain CommonJS, monorepo with project references). It builds the local tarball and exercises it cold; pre-publish gate.
217
+ ### Adding a Layer-2 fixture
218
+
219
+ Layer-2 fixtures live under `fixtures/` alongside Layer-0 ones, identified by `expectedErrorCode` (singular) or `costUsdMax` in their `expected.json`. The Layer-0 benchmark skips them; `npm run benchmark:llm` runs them against Anthropic.
220
+
221
+ - Hand-author one under `fixtures/mend-<descriptive-name>/` for new error classes.
222
+ - Or generate one via `npm run generate-fixtures -- --code=TS2339 --seed=apiRouter.ts --count=10 --rng-seed=42`. The generator validates every mutation through Layer 0 first to confirm Layer 0 abstains (otherwise it's not Layer 2 territory).
223
+
224
+ ### Pre-publish gates
225
+
226
+ - `npm run benchmark` — Layer 0, 14 fixtures, no network.
227
+ - `npm run benchmark:llm` — Layer 2, 35 fixtures, requires `ANTHROPIC_API_KEY`. Total cost ~$0.04 per run.
228
+ - `npm run matrix` — runs the local tarball against 6 distinct project shapes (Next.js, Vite + React, plain `nodenext`, plain `bundler`, plain CommonJS, monorepo with project references). Adds ~3 min; run manually before tagging.
167
229
 
168
230
  ## License
169
231
 
package/dist/cli.js CHANGED
@@ -475,6 +475,11 @@ function applyFixToSnapshots(fix, snapshots) {
475
475
  // src/index.ts
476
476
  import * as fs3 from "node:fs";
477
477
  import * as path3 from "node:path";
478
+
479
+ // src/typeContext.ts
480
+ import * as ts3 from "typescript";
481
+
482
+ // src/index.ts
478
483
  var noopLogger = {
479
484
  info: () => {
480
485
  },
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * A reusable TypeScript error-recovery agent. Validates LLM-generated (or any)
5
5
  * TypeScript code via in-process tsc, auto-fixes deterministic error classes
6
6
  * (TS2304/2305/2552/2724) via TypeScript's built-in code-fix engine, and
7
- * exposes hooks for LLM-driven repair (planned, not yet shipped).
7
+ * runs Layer 2 LLM mend (single-file repair via Vercel AI SDK + Anthropic)
8
+ * on what survives.
8
9
  *
9
10
  * ## Quick start (library)
10
11
  *
@@ -31,15 +32,18 @@
31
32
  * - `runInProcessTsc` — just type-check, returns structured diagnostics
32
33
  * - `runLSPFixerPass` — just the auto-fix pass, edits files in place
33
34
  *
34
- * ## Public types for downstream LLM-mend integrations
35
+ * ## Public types for the LLM-mend layer
35
36
  *
36
37
  * - `Diagnostic` — single tsc error (re-exported from `runInProcessTsc`)
37
- * - `MendContext` — input contract for a Layer 2–4 LLM-mend agent
38
+ * - `MendContext` — input contract for the Layer 2–4 LLM-mend agent
38
39
  * - `LayerEvent` — per-layer event shape for streaming telemetry
39
40
  *
40
- * The mend agents themselves (`@shipispec/tsmend`, planned) consume these
41
- * types but are not shipped from this package — `tsfix` stays Layer 0–1
42
- * deterministic.
41
+ * ## Layer 2 mend API (v0.4.0+)
42
+ *
43
+ * - `getTypeContext` — TS Language Service type-declaration injection
44
+ * - `mendSingleFile` — single-LLM-call repair via Vercel AI SDK
45
+ * - `runMendLoop` — bounded retry with no-progress / regression detection
46
+ * - `parseEditBlocks` / `applyEditBlocks` — Aider-style SEARCH/REPLACE applier
43
47
  */
44
48
  export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
45
49
  export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
@@ -174,3 +178,11 @@ export interface LayerEvent {
174
178
  /** `Date.now()` at emission. */
175
179
  ts: number;
176
180
  }
181
+ export { getTypeContext, resetTypeContextCache } from "./typeContext.js";
182
+ export type { TypeContextOptions, TypeContext } from "./typeContext.js";
183
+ export { parseEditBlocks, applySingleBlock, applyEditBlocks } from "./applyEditBlock.js";
184
+ export type { EditBlock, ApplyEditBlocksOptions, ApplyResult, SingleBlockResult, } from "./applyEditBlock.js";
185
+ export { mendSingleFile } from "./mendAgent.js";
186
+ export type { MendSingleFileOptions, MendSingleFileResult, LLMCall } from "./mendAgent.js";
187
+ export { runMendLoop } from "./runMendLoop.js";
188
+ export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";