@shipispec/tsfix 0.2.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 +66 -2
- package/README.md +188 -118
- package/dist/cli.js +5 -0
- package/dist/index.d.ts +91 -6
- package/dist/index.js +35192 -17
- package/dist/types/applyEditBlock.d.ts +68 -0
- package/dist/types/index.d.ts +91 -6
- package/dist/types/mendAgent.d.ts +53 -0
- package/dist/types/runMendLoop.d.ts +64 -0
- package/dist/types/typeContext.d.ts +48 -0
- package/package.json +14 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,8 +4,70 @@ 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
|
+
|
|
52
|
+
## [0.3.0] - 2026-05-07
|
|
53
|
+
|
|
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.
|
|
55
|
+
|
|
7
56
|
### Added
|
|
8
|
-
-
|
|
57
|
+
- **`MendContext` interface** — public type defining the input contract for a Layer 2–4 LLM-mend agent. Required fields: `workspaceRoot`, `diagnostics`, `erroredFiles`. Optional fields: `taskDescription`, `featureSpecText`, `acceptanceCriteria`, `siblingTasks`, `priorTaskExports`, `installedTypes`.
|
|
58
|
+
- **`LayerEvent` interface** — per-layer event shape for streaming telemetry. Designed for an `onLayerEvent` callback in a future minor release; the type is published now so downstream callers can construct events themselves.
|
|
59
|
+
- **`Diagnostic` type alias** — public re-export of `InProcessTscResult["diagnostics"][number]`. Convenience for consumers building `MendContext`.
|
|
60
|
+
- **Project-shape matrix** (`scripts/run-matrix.mjs`, `npm run matrix`) — pre-publish gate that builds the local tarball and exercises it cold against 6 distinct project shapes: `monorepo-refs` (project references — pinned as a documented limitation), `next-app` (App Router, `paths` alias, `jsx: preserve`), `plain-ts-bundler` (esnext + bundler), `plain-ts-commonjs` (legacy CJS + ES2015 + node10), `plain-ts-nodenext` (nodenext resolution), `react-vite` (TSX + `jsx: react-jsx`). 6/6 pass. Dev-only — not shipped in the tarball.
|
|
61
|
+
- **Capture script** (`scripts/capture-fixture.mjs`, `npm run capture`) — Phase 3b tooling for snapshotting real broken workspaces into `fixtures/real-<name>/`. Awaits first real failure to produce fixtures.
|
|
62
|
+
- **GitHub Actions CI** (`.github/workflows/test.yml`) — runs check-types, vitest, benchmark, and the matrix on every PR + main push.
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
- **Repository moved.** `tsc-defense-stack/` was extracted from the `spectoship-meta` monorepo into its own repository at <https://github.com/owgreen-dev/tsfix>. All `repository.url`, `homepage`, `bugs.url` fields point at the new repo. Internal git history pre-2026-05-06 lives in the original monorepo; the CHANGELOG narrates v0.1.0–v0.2.0 in detail.
|
|
66
|
+
- **Public README rewritten** for an OSS audience — tagline, before/after, 30-second cold start, four-layer model, library API, trust model, contributing protocol. Previous internal-orientation README preserved at `docs/internal-orientation.md`.
|
|
67
|
+
|
|
68
|
+
### Engines
|
|
69
|
+
- Node `>=20.9.0` (unchanged)
|
|
70
|
+
- TypeScript `>=5.0.0` peer (unchanged)
|
|
9
71
|
|
|
10
72
|
## [0.2.0] - 2026-05-04
|
|
11
73
|
|
|
@@ -80,7 +142,9 @@ Initial public release. **Layers 0–1 only** (deterministic detection + auto-fi
|
|
|
80
142
|
- Node `>=20.9.0` (matches VS Code Extension Host runtime)
|
|
81
143
|
- TypeScript `>=5.0.0` (peer dep, must be installed in the consuming workspace)
|
|
82
144
|
|
|
83
|
-
[Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.
|
|
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
|
|
147
|
+
[0.3.0]: https://github.com/owgreen-dev/tsfix/compare/v0.2.0...v0.3.0
|
|
84
148
|
[0.2.0]: https://github.com/owgreen-dev/tsfix/compare/v0.1.1...v0.2.0
|
|
85
149
|
[0.1.1]: https://github.com/owgreen-dev/tsfix/compare/v0.1.0...v0.1.1
|
|
86
150
|
[0.1.0]: https://github.com/owgreen-dev/tsfix/releases/tag/v0.1.0
|
package/README.md
CHANGED
|
@@ -1,170 +1,240 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tsfix
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
6
|
-
- `STATUS.md` — what's working, what's planned, current gaps
|
|
7
|
-
- `ARCHITECTURE.md` — why the package is shaped the way it is
|
|
8
|
-
- `tsc-defense-roadmap.md` — phased plan with open decisions
|
|
9
|
-
- `CLAUDE.md` — working principles (small allowlist, fixture-pinned trust model)
|
|
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:
|
|
10
6
|
|
|
11
|
-
|
|
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.
|
|
12
9
|
|
|
13
|
-
|
|
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**.
|
|
14
11
|
|
|
15
|
-
|
|
12
|
+
## Before / after (Layer 0)
|
|
16
13
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
| `cli/run-stack.ts` | CLI: `tsx cli/run-stack.ts --workspace <path>` |
|
|
23
|
-
| `benchmark/run-benchmark.ts` | Fixture harness (auto-discovers `fixtures/*/`) |
|
|
24
|
-
| `fixtures/` | 14 hand-authored synthetic fixtures across 3 tiers |
|
|
25
|
-
| `spectoship2/src/pipeline/validatorInProcess.ts` | **Re-export shim** → `@shipispec/tsfix` |
|
|
26
|
-
| `spectoship2/src/pipeline/tsLanguageServiceFixer.ts` | **Re-export shim** → `@shipispec/tsfix` |
|
|
14
|
+
```
|
|
15
|
+
$ tsc --noEmit
|
|
16
|
+
src/api.ts:5:2 - error TS2552: Cannot find name 'consol'. Did you mean 'console'?
|
|
17
|
+
src/api.ts:8:5 - error TS2305: Module '"react"' has no exported member 'ueState'.
|
|
18
|
+
src/api.ts:12:14 - error TS2551: Property 'lenght' does not exist on type 'string[]'. Did you mean 'length'?
|
|
27
19
|
|
|
28
|
-
|
|
20
|
+
Found 3 errors in 1 file.
|
|
29
21
|
|
|
30
|
-
|
|
22
|
+
$ npx @shipispec/tsfix --workspace .
|
|
23
|
+
[ts-lsp-fixer] applied 3 fixes across 1 file
|
|
24
|
+
|
|
25
|
+
$ tsc --noEmit
|
|
26
|
+
$ # 0 errors
|
|
27
|
+
```
|
|
31
28
|
|
|
32
|
-
|
|
29
|
+
## 30-second cold start
|
|
33
30
|
|
|
31
|
+
```bash
|
|
32
|
+
cd your-broken-project
|
|
33
|
+
npx @shipispec/tsfix --workspace .
|
|
34
34
|
```
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
│ │ signature-set progress check, max 5 iters │ │
|
|
55
|
-
│ └─────────────────────┬───────────────────────┘ │
|
|
56
|
-
│ │ re-validate; if errors remain │
|
|
57
|
-
└─────────────────────────┼─────────────────────────────────────────┘
|
|
58
|
-
▼
|
|
59
|
-
┌─────────────────────────────────┐
|
|
60
|
-
│ Layers 2-4: LLM MEND │
|
|
61
|
-
│ mendAgent / mendArchitect / │
|
|
62
|
-
│ multiFileMend / repairAgent │
|
|
63
|
-
│ (in spectoship2/, not here; │
|
|
64
|
-
│ moves to @shipispec/tsmend│
|
|
65
|
-
│ in v0.2 per roadmap) │
|
|
66
|
-
└─────────────────────────────────┘
|
|
35
|
+
|
|
36
|
+
No config file. Exit code conventions:
|
|
37
|
+
|
|
38
|
+
| Code | Meaning |
|
|
39
|
+
|---|---|
|
|
40
|
+
| 0 | Workspace is clean |
|
|
41
|
+
| 1 | Errors remain (printed to stderr) |
|
|
42
|
+
| 2 | Bad arguments / harness error |
|
|
43
|
+
|
|
44
|
+
Preview what *would* change without writing to disk:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx @shipispec/tsfix --workspace . --dry-run
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Machine-readable output for piping into other tools:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx @shipispec/tsfix --workspace . --json
|
|
67
54
|
```
|
|
68
55
|
|
|
69
|
-
|
|
56
|
+
### All flags
|
|
57
|
+
|
|
58
|
+
| Flag | Meaning |
|
|
59
|
+
|---|---|
|
|
60
|
+
| `--workspace <path>` | Required. Directory containing your `tsconfig.json`. |
|
|
61
|
+
| `--dry-run` | Run the fixer in memory, report counts, write nothing. |
|
|
62
|
+
| `--no-lsp` | Validate only — skip auto-fix. |
|
|
63
|
+
| `--files <a.ts,b.ts>` | Restrict fixing to a comma-separated list. |
|
|
64
|
+
| `--json` | Machine-readable output. |
|
|
65
|
+
| `--verbose` | Per-fix logging. |
|
|
66
|
+
| `--help` | Print usage. |
|
|
67
|
+
|
|
68
|
+
The CLI does not run Layer 2 — call the library API for that (below).
|
|
70
69
|
|
|
71
|
-
## What
|
|
70
|
+
## What Layer 0 fixes
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
72
|
+
| TS code | Meaning | What tsfix does |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| `TS2304` | Cannot find name | Auto-imports |
|
|
75
|
+
| `TS2305` | Module has no exported member | Did-you-mean rename |
|
|
76
|
+
| `TS2551` | Property does not exist on T, did you mean Y | Spelling fix |
|
|
77
|
+
| `TS2552` | Cannot find name, did you mean Y | Spelling fix |
|
|
78
|
+
| `TS2724` | Module member did-you-mean | Import rename |
|
|
79
79
|
|
|
80
|
-
|
|
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.
|
|
81
81
|
|
|
82
|
-
##
|
|
82
|
+
## What Layer 0 does *not* fix (Layer 2 picks these up)
|
|
83
|
+
|
|
84
|
+
By design, Layer 0 only applies fixes that are **deterministic** and **non-structural**. It refuses to:
|
|
85
|
+
|
|
86
|
+
- Add or remove function declarations
|
|
87
|
+
- Insert type annotations or change types
|
|
88
|
+
- Modify control flow (`await` insertions, async propagation)
|
|
89
|
+
- Rewrite JSX trees
|
|
90
|
+
- Add object-literal stub properties
|
|
91
|
+
|
|
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.
|
|
93
|
+
|
|
94
|
+
Layer 2 is built for the cases the LSP can't statically resolve:
|
|
95
|
+
|
|
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
|
|
83
103
|
|
|
84
104
|
```
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
api-drift-*/ # 4 version-drift fixtures (Zod 3 vs 4, React 18 vs 19, etc.)
|
|
105
|
+
Layer 0 — Prevention (prompt rules, exported-API injection — your problem)
|
|
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)
|
|
92
111
|
```
|
|
93
112
|
|
|
94
|
-
|
|
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.
|
|
95
114
|
|
|
96
|
-
|
|
115
|
+
## Library API
|
|
97
116
|
|
|
98
|
-
|
|
99
|
-
npm install @shipispec/tsfix typescript
|
|
100
|
-
```
|
|
117
|
+
### Layer 0/1 — deterministic loop
|
|
101
118
|
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
import { runValidationLoop } from "@shipispec/tsfix";
|
|
119
|
+
```typescript
|
|
120
|
+
import { runValidationLoop } from '@shipispec/tsfix';
|
|
105
121
|
|
|
106
|
-
const result = runValidationLoop({
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
const result = runValidationLoop({
|
|
123
|
+
workspaceRoot: '/path/to/your/project',
|
|
124
|
+
// Optional:
|
|
125
|
+
// targetFiles: ['src/api.ts'],
|
|
126
|
+
// dryRun: true,
|
|
127
|
+
// logger: { info: console.log, warn: console.warn, error: console.error },
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
result.errorsBefore; // number
|
|
131
|
+
result.errorsAfter; // number
|
|
132
|
+
result.lspFixer.fixesApplied; // number
|
|
133
|
+
result.lspFixer.filesEdited; // string[]
|
|
134
|
+
result.passed; // boolean — true if errorsAfter === 0
|
|
109
135
|
```
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
|
|
137
|
+
Other Layer 0/1 exports:
|
|
138
|
+
|
|
139
|
+
- `runInProcessTsc(opts)` — validation only, no fixer. Returns structured diagnostics.
|
|
140
|
+
- `runLSPFixerPass(opts)` — Layer 0 fixer alone, no validation loop wrapper.
|
|
141
|
+
- `discoverTsFiles(workspaceRoot)` — file-walking helper. Skips `node_modules`, `.next`, `dist`, `build`, `out`, `coverage`, `.git`.
|
|
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
|
+
}
|
|
113
175
|
```
|
|
114
176
|
|
|
115
|
-
|
|
177
|
+
Other Layer 2 exports:
|
|
116
178
|
|
|
117
|
-
|
|
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.
|
|
118
183
|
|
|
119
|
-
|
|
120
|
-
git clone https://github.com/owgreen-dev/spectoship-meta
|
|
121
|
-
cd spectoship-meta/tsc-defense-stack
|
|
122
|
-
npm install
|
|
123
|
-
npm link
|
|
184
|
+
## Trust model
|
|
124
185
|
|
|
125
|
-
|
|
126
|
-
```
|
|
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.
|
|
127
187
|
|
|
128
|
-
|
|
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.
|
|
129
189
|
|
|
130
|
-
|
|
190
|
+
**Network surface (Layer 0/1):** none. No telemetry, no calls home, no background processes, no config files written outside `--workspace`.
|
|
131
191
|
|
|
132
|
-
|
|
133
|
-
```sh
|
|
134
|
-
npm run benchmark # all 14 fixtures
|
|
135
|
-
npm run benchmark -- --fixture synthetic-typo-ts2552 # one fixture
|
|
136
|
-
```
|
|
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.
|
|
137
193
|
|
|
138
|
-
|
|
194
|
+
## Engines
|
|
139
195
|
|
|
140
|
-
|
|
196
|
+
- Node `>=20.9.0`
|
|
197
|
+
- TypeScript `>=5.0.0` (peer dep — must be installed in your workspace)
|
|
141
198
|
|
|
142
|
-
|
|
199
|
+
If your workspace has no `node_modules/typescript`, tsfix will fail with a clear error:
|
|
143
200
|
|
|
144
|
-
|
|
201
|
+
```
|
|
202
|
+
error: this workspace has no TypeScript installed.
|
|
203
|
+
run: npm install --save-dev typescript
|
|
204
|
+
```
|
|
145
205
|
|
|
146
|
-
|
|
206
|
+
## Contributing
|
|
147
207
|
|
|
148
|
-
|
|
208
|
+
### Adding a Layer-0 fix
|
|
149
209
|
|
|
150
|
-
|
|
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`.
|
|
211
|
+
2. **Verify** — run `npm run benchmark -- --fixture <name>` and inspect what the language service offers (the `fix.fixName` field).
|
|
212
|
+
3. **Allowlist change** — if `fixName` is unsafe (`fixMissingFunctionDeclaration`, `addMissingPropertyAndOptional`, etc.), document why we don't trust it. Otherwise, add the error code to `SAFE_FIXABLE_CODES` and the fix name to `SAFE_FIX_NAMES` in `src/tsLanguageServiceFixer.ts`.
|
|
213
|
+
4. **Lock it in** — confirm all existing fixtures still pass (`npm run benchmark`). Open a PR.
|
|
151
214
|
|
|
152
|
-
|
|
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.
|
|
153
216
|
|
|
154
|
-
|
|
217
|
+
### Adding a Layer-2 fixture
|
|
155
218
|
|
|
156
|
-
|
|
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.
|
|
157
220
|
|
|
158
|
-
|
|
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).
|
|
159
223
|
|
|
160
|
-
|
|
224
|
+
### Pre-publish gates
|
|
161
225
|
|
|
162
|
-
|
|
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.
|
|
163
229
|
|
|
164
|
-
|
|
230
|
+
## License
|
|
165
231
|
|
|
166
|
-
|
|
232
|
+
MIT.
|
|
167
233
|
|
|
168
|
-
|
|
234
|
+
## See also
|
|
169
235
|
|
|
170
|
-
|
|
236
|
+
- `CHANGELOG.md` — release notes per version.
|
|
237
|
+
- `ARCHITECTURE.md` — internal design rationale (the four-layer model, the workspace lib-path workaround).
|
|
238
|
+
- `STATUS.md` — current snapshot, gaps, and roadmap state.
|
|
239
|
+
- `tsc-defense-roadmap.md` — phased plan.
|
|
240
|
+
- `docs/internal-orientation.md` — the original SpecToShip-context README, kept for contributors who want the design history.
|
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
|
-
*
|
|
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,12 +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
|
-
* ##
|
|
35
|
+
* ## Public types for the LLM-mend layer
|
|
35
36
|
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* - `Diagnostic` — single tsc error (re-exported from `runInProcessTsc`)
|
|
38
|
+
* - `MendContext` — input contract for the Layer 2–4 LLM-mend agent
|
|
39
|
+
* - `LayerEvent` — per-layer event shape for streaming telemetry
|
|
40
|
+
*
|
|
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
|
|
40
47
|
*/
|
|
41
48
|
export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
|
|
42
49
|
export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
|
|
@@ -101,3 +108,81 @@ export declare function discoverTsFiles(workspaceRoot: string): string[];
|
|
|
101
108
|
* Throws on missing `tsconfig.json` or workspace path.
|
|
102
109
|
*/
|
|
103
110
|
export declare function runValidationLoop(opts: ValidationLoopOptions): ValidationLoopResult;
|
|
111
|
+
/**
|
|
112
|
+
* Single tsc diagnostic. Re-exported from `runInProcessTsc`'s result type
|
|
113
|
+
* so consumers building a `MendContext` don't have to dig the shape out of
|
|
114
|
+
* `InProcessTscResult["diagnostics"][number]`.
|
|
115
|
+
*/
|
|
116
|
+
export type Diagnostic = InProcessTscResult["diagnostics"][number];
|
|
117
|
+
/**
|
|
118
|
+
* Input contract for a Layer 2–4 LLM-mend agent.
|
|
119
|
+
*
|
|
120
|
+
* Pattern:
|
|
121
|
+
* 1. Run `runValidationLoop` (Layer 0/1).
|
|
122
|
+
* 2. If `result.errorsAfter > 0`, build a `MendContext` from the
|
|
123
|
+
* surviving diagnostics + whatever task/spec context your pipeline has.
|
|
124
|
+
* 3. Hand off to a mend agent (e.g. `@shipispec/tsmend`).
|
|
125
|
+
*
|
|
126
|
+
* Required fields: `workspaceRoot`, `diagnostics`, `erroredFiles`.
|
|
127
|
+
* Everything else is optional — leave fields out if your pipeline doesn't
|
|
128
|
+
* carry them.
|
|
129
|
+
*/
|
|
130
|
+
export interface MendContext {
|
|
131
|
+
/** Absolute path to the workspace (must contain `tsconfig.json`). */
|
|
132
|
+
workspaceRoot: string;
|
|
133
|
+
/** Diagnostics that survived Layer 0/1 and need higher-layer repair. */
|
|
134
|
+
diagnostics: Diagnostic[];
|
|
135
|
+
/** Absolute paths of files containing the surviving diagnostics. */
|
|
136
|
+
erroredFiles: string[];
|
|
137
|
+
/** Optional one-line summary of what the failing code was supposed to do. */
|
|
138
|
+
taskDescription?: string;
|
|
139
|
+
/** Optional Markdown spec the code is implementing. Helps the LLM understand intent. */
|
|
140
|
+
featureSpecText?: string;
|
|
141
|
+
/** Optional testable acceptance criteria from the spec. */
|
|
142
|
+
acceptanceCriteria?: string;
|
|
143
|
+
/** Other tasks in the same feature, with their files and current status. */
|
|
144
|
+
siblingTasks?: Array<{
|
|
145
|
+
description: string;
|
|
146
|
+
files: string[];
|
|
147
|
+
status: "pending" | "completed" | "failed";
|
|
148
|
+
}>;
|
|
149
|
+
/** Public API surface from earlier completed tasks (helps prevent re-defining symbols). */
|
|
150
|
+
priorTaskExports?: string;
|
|
151
|
+
/** Compact type signatures of installed npm dependencies (helps prevent API hallucination). */
|
|
152
|
+
installedTypes?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Per-layer event for streaming telemetry across the validate → fix → mend
|
|
156
|
+
* chain. Designed for an `onLayerEvent` callback (added in a future minor
|
|
157
|
+
* release) rather than accumulating in a result array — a workspace with
|
|
158
|
+
* 200 errors emits ~1000 events.
|
|
159
|
+
*
|
|
160
|
+
* Layer assignments:
|
|
161
|
+
* 0 = prevention (prompt rules, exported-API injection — caller's problem)
|
|
162
|
+
* 1 = tsfix LSP fixer (this package)
|
|
163
|
+
* 2 = single-file LLM mend
|
|
164
|
+
* 3 = multi-file LLM mend (blast-radius search/replace)
|
|
165
|
+
* 4 = stub-and-continue (escape hatch)
|
|
166
|
+
*/
|
|
167
|
+
export interface LayerEvent {
|
|
168
|
+
/** Which layer ran. */
|
|
169
|
+
layer: 0 | 1 | 2 | 3 | 4;
|
|
170
|
+
/** TypeScript error code being acted on (e.g. 2304, 2339, 7006). */
|
|
171
|
+
errorCode: number;
|
|
172
|
+
/** True if the error was resolved by this layer. */
|
|
173
|
+
fixed: boolean;
|
|
174
|
+
/** Wall-clock time spent on this attempt. */
|
|
175
|
+
latencyMs: number;
|
|
176
|
+
/** USD cost (LLM tokens). Undefined for deterministic layers. */
|
|
177
|
+
costUsd?: number;
|
|
178
|
+
/** `Date.now()` at emission. */
|
|
179
|
+
ts: number;
|
|
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";
|