@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.
@@ -0,0 +1,68 @@
1
+ /**
2
+ * SEARCH/REPLACE block parser + fuzzy applier (Aider's `editblock` format).
3
+ *
4
+ * The format an LLM emits when asked to repair a file:
5
+ *
6
+ * path/to/file.ts
7
+ * <<<<<<< SEARCH
8
+ * // exact text to find
9
+ * =======
10
+ * // replacement text
11
+ * >>>>>>> REPLACE
12
+ *
13
+ * Fenced code blocks (```ts ... ```) around the markers are tolerated.
14
+ * Multiple blocks per file and multiple files per LLM output are allowed.
15
+ *
16
+ * Match algorithm (3 tiers, abstain on ambiguity):
17
+ * 1. Exact substring match.
18
+ * 2. Right-strip per line (trailing-whitespace tolerance), retry.
19
+ * 3. Full strip per line (leading + trailing), retry.
20
+ *
21
+ * If a tier finds multiple matches, we surface "ambiguous: N matches" rather
22
+ * than guess. Better to drop the patch and let the LLM emit a more specific
23
+ * SEARCH block on the next iteration than to silently corrupt the file.
24
+ */
25
+ export interface EditBlock {
26
+ file: string;
27
+ search: string;
28
+ replace: string;
29
+ }
30
+ export interface ApplyEditBlocksOptions {
31
+ workspaceRoot: string;
32
+ blocks: EditBlock[];
33
+ /** Compute new content, return successes/failures, but skip writing to disk. */
34
+ dryRun?: boolean;
35
+ }
36
+ export interface ApplyResult {
37
+ blocks: EditBlock[];
38
+ applied: number;
39
+ filesEdited: string[];
40
+ failures: Array<{
41
+ block: EditBlock;
42
+ reason: string;
43
+ }>;
44
+ }
45
+ /**
46
+ * Extract every well-formed SEARCH/REPLACE block from raw LLM output.
47
+ * Malformed / truncated blocks at the tail are skipped silently.
48
+ */
49
+ export declare function parseEditBlocks(llmOutput: string): EditBlock[];
50
+ export type SingleBlockResult = {
51
+ newContent: string;
52
+ matchedTier: "exact" | "rstrip" | "strip";
53
+ } | {
54
+ error: string;
55
+ };
56
+ /**
57
+ * Apply one search/replace to a single file's content. Pure — doesn't
58
+ * touch disk.
59
+ */
60
+ export declare function applySingleBlock(fileContent: string, search: string, replace: string): SingleBlockResult;
61
+ /**
62
+ * Top-level: apply a list of edit blocks. Stacks multiple blocks against
63
+ * the same file in memory before writing, so block N+1 sees block N's edit.
64
+ *
65
+ * Failures are collected, not thrown — the mend loop wants to know what
66
+ * succeeded so it can re-run tsc on the partial fix.
67
+ */
68
+ export declare function applyEditBlocks(opts: ApplyEditBlocksOptions): ApplyResult;
@@ -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,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
- * ## What it doesn't do (yet)
35
+ * ## Public types for the LLM-mend layer
35
36
  *
36
- * LLM-driven repair (the mend-agent layers from the spectoship pipeline) is
37
- * not exported here yet. They depend on internal types (ParsedTask) that need
38
- * to be redesigned as opaque interfaces before they can be moved into this
39
- * package. v0.2 target.
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";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Single-file LLM mend (Layer 2).
3
+ *
4
+ * Builds a prompt of:
5
+ * - System block: instructions + the erroring file's full content + type
6
+ * context resolved through the TS Language Service for each diagnostic.
7
+ * - User block: the diagnostics themselves (changes per iteration; cheap).
8
+ *
9
+ * Sends to Anthropic via Vercel AI SDK, parses the SEARCH/REPLACE response,
10
+ * applies via `applyEditBlocks`. Multi-file scope is Layer 3 (deferred to
11
+ * tsmend v0.2).
12
+ *
13
+ * Prompt-cache breakpoint placement is intentionally simple in v0.1.0 — we
14
+ * pass the whole system block as one cached unit. Future tuning belongs in
15
+ * `runMendLoop` once we have benchmark data on hit rates.
16
+ */
17
+ import type { MendContext } from "./index.js";
18
+ import { type ApplyResult, type EditBlock } from "./applyEditBlock.js";
19
+ export interface MendSingleFileOptions {
20
+ context: MendContext;
21
+ llm: {
22
+ provider: "anthropic";
23
+ model: string;
24
+ apiKey: string;
25
+ };
26
+ /** Compute and parse patches but skip writing to disk. Default false. */
27
+ dryRun?: boolean;
28
+ /** @internal — LLM call override. Tests inject a fake; real callers leave it. */
29
+ _callLLM?: LLMCall;
30
+ }
31
+ export interface MendSingleFileResult {
32
+ rawResponse: string;
33
+ blocks: EditBlock[];
34
+ apply: ApplyResult;
35
+ inputTokens: number;
36
+ outputTokens: number;
37
+ latencyMs: number;
38
+ }
39
+ export type LLMCall = (params: {
40
+ systemBlock: string;
41
+ userBlock: string;
42
+ model: string;
43
+ apiKey: string;
44
+ }) => Promise<{
45
+ text: string;
46
+ inputTokens: number;
47
+ outputTokens: number;
48
+ }>;
49
+ /** @internal — exported for unit tests. */
50
+ export declare function buildSystemBlock(context: MendContext, erroredFile: string): string;
51
+ /** @internal — exported for unit tests. */
52
+ export declare function buildUserBlock(context: MendContext, erroredFile: string): string;
53
+ export declare function mendSingleFile(opts: MendSingleFileOptions): Promise<MendSingleFileResult>;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Bounded mend loop with no-progress detection.
3
+ *
4
+ * 1. Run tsc (`runInProcessTsc` from tsfix) to capture baseline diagnostics.
5
+ * 2. If clean → return immediately with `stopReason: "noErrors"`.
6
+ * 3. For up to `maxIterations`:
7
+ * a. Build a per-iteration MendContext scoped to the current errors.
8
+ * b. Call `mendSingleFile` (LLM → SEARCH/REPLACE → apply).
9
+ * c. Re-run tsc.
10
+ * d. Compare error-signature set:
11
+ * - empty → "fixed"
12
+ * - same as previous → "noProgress" (LLM made no useful change)
13
+ * - larger → "regressed" (LLM made it worse)
14
+ * - shrunk / changed → continue
15
+ * 4. Hit maxIterations → `stopReason: "maxIterations"`.
16
+ *
17
+ * The signature is `(file, line, column, code)` — same shape tsfix's Layer 0
18
+ * fixer uses internally. We don't import that helper because it's an
19
+ * `@internal` export of tsfix; reimplementing here is ~10 lines.
20
+ *
21
+ * dryRun: runs a single iteration with mendSingleFile in dry-run mode, then
22
+ * returns. We can't iterate without writing to disk because re-validation
23
+ * needs the actual file changes.
24
+ */
25
+ import type { Diagnostic, MendContext } from "./index.js";
26
+ import { type LLMCall } from "./mendAgent.js";
27
+ export interface RunMendLoopOptions {
28
+ context: MendContext;
29
+ llm: {
30
+ provider: "anthropic";
31
+ model: string;
32
+ apiKey: string;
33
+ };
34
+ /** Hard cap on LLM calls. Default 3. */
35
+ maxIterations?: number;
36
+ /** Single dry-run pass — call LLM, parse, but don't write to disk. Default false. */
37
+ dryRun?: boolean;
38
+ /** @internal — LLM call override for tests. */
39
+ _callLLM?: LLMCall;
40
+ }
41
+ export interface MendLoopIteration {
42
+ index: number;
43
+ diagnosticsBefore: number;
44
+ diagnosticsAfter: number;
45
+ patchesApplied: number;
46
+ patchesFailed: number;
47
+ inputTokens: number;
48
+ outputTokens: number;
49
+ latencyMs: number;
50
+ /** Raw LLM response for this iteration — useful for debugging failed patches. */
51
+ rawResponse: string;
52
+ }
53
+ export type StopReason = "noErrors" | "fixed" | "noProgress" | "regressed" | "maxIterations";
54
+ export interface RunMendLoopResult {
55
+ iterations: MendLoopIteration[];
56
+ diagnosticsBefore: Diagnostic[];
57
+ diagnosticsAfter: Diagnostic[];
58
+ passed: boolean;
59
+ stopReason: StopReason;
60
+ totalInputTokens: number;
61
+ totalOutputTokens: number;
62
+ totalLatencyMs: number;
63
+ }
64
+ export declare function runMendLoop(opts: RunMendLoopOptions): Promise<RunMendLoopResult>;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * TypeScript Language Service context injection.
3
+ *
4
+ * The architectural moat for the Layer 2 mend agent. When a tsc diagnostic
5
+ * says "Property 'foo' doesn't exist on type 'Bar'", this resolves the `Bar`
6
+ * declaration to its exact source location and slices ±N lines around it.
7
+ *
8
+ * Every other LLM-driven repair tool (Aider, Cline, Cursor, OpenHands,
9
+ * bolt.diy) uses generic grep or repo-maps to assemble context. Calling the
10
+ * actual TypeChecker is what closes the gap between 30% and 70% on semantic
11
+ * TS errors (per SWE-bench TS/JS data).
12
+ *
13
+ * Mirrors the lib-path workaround pattern from `validatorInProcess.ts`.
14
+ */
15
+ import type { Diagnostic } from "./index.js";
16
+ export interface TypeContextOptions {
17
+ /** Absolute path to the workspace (must contain tsconfig.json). */
18
+ workspaceRoot: string;
19
+ /** A diagnostic from `runInProcessTsc` (or any compatible source). */
20
+ diagnostic: Diagnostic;
21
+ /** Lines of context around the error site. Default 3. */
22
+ errorPadding?: number;
23
+ /** Lines of context around the resolved type declaration. Default 20. */
24
+ declarationPadding?: number;
25
+ }
26
+ export interface TypeContext {
27
+ /** Numbered lines around the error site. Always present. */
28
+ errorSite: {
29
+ file: string;
30
+ lines: string;
31
+ };
32
+ /** Numbered lines around the resolved type declaration. Present when the
33
+ * error node (or one of its first 4 ancestors) has a non-lib symbol with
34
+ * at least one declaration. */
35
+ typeDeclaration?: {
36
+ file: string;
37
+ lines: string;
38
+ symbol: string;
39
+ };
40
+ }
41
+ /** Reset the per-workspace Program cache. Tests should call this in `beforeEach`. */
42
+ export declare function resetTypeContextCache(): void;
43
+ /**
44
+ * Resolve a tsc diagnostic to its surrounding code context — error site
45
+ * always, plus the declaring type when one can be resolved through the
46
+ * TypeChecker.
47
+ */
48
+ export declare function getTypeContext(opts: TypeContextOptions): TypeContext;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@shipispec/tsfix",
3
- "version": "0.2.0",
4
- "description": "Reusable TypeScript error-recovery agent. Validates LLM-generated TS code, auto-fixes deterministic error classes via the TS Language Service, and exposes hooks for LLM-driven repair.",
3
+ "version": "0.4.0",
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",
7
7
  "tsc",
@@ -11,6 +11,9 @@
11
11
  "auto-fix",
12
12
  "llm",
13
13
  "ai-codegen",
14
+ "ai-sdk",
15
+ "anthropic",
16
+ "code-repair",
14
17
  "validator",
15
18
  "linter"
16
19
  ],
@@ -48,18 +51,27 @@
48
51
  "scripts": {
49
52
  "build": "node scripts/build.mjs",
50
53
  "matrix": "node scripts/run-matrix.mjs",
54
+ "capture": "node scripts/capture-fixture.mjs",
55
+ "pregenerate-fixtures": "npm run build",
56
+ "generate-fixtures": "node scripts/generate-fixtures.mjs",
51
57
  "prepack": "npm run build",
52
58
  "setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
53
59
  "prebenchmark": "npm run setup-fixtures",
54
60
  "pretest": "npm run setup-fixtures",
55
61
  "benchmark": "tsx benchmark/run-benchmark.ts",
62
+ "benchmark:llm": "tsx benchmark/run-llm-benchmark.ts",
56
63
  "run-stack": "tsx cli/run-stack.ts",
57
64
  "test": "vitest run",
58
65
  "check-types": "tsc --noEmit"
59
66
  },
67
+ "dependencies": {
68
+ "@ai-sdk/anthropic": "^3.0.44",
69
+ "ai": "^6.0.86"
70
+ },
60
71
  "devDependencies": {
61
72
  "@types/node": "^20.0.0",
62
73
  "esbuild": "^0.28.0",
74
+ "ts-morph": "^28.0.0",
63
75
  "tsx": "^4.20.6",
64
76
  "typescript": "^5.9.3",
65
77
  "vitest": "^3.2.4"