@shipispec/tsfix 0.3.0 → 0.5.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,15 +32,24 @@
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
47
+ *
48
+ * ## Layer 4 escape hatch (v0.5.0+)
49
+ *
50
+ * - `stubAndContinue` — insert `// @ts-expect-error - tsfix: ...` above
51
+ * unresolved error sites so the workspace compiles. Opt-in: set
52
+ * `stubOnFailure: true` on `runMendLoop`, or call directly.
43
53
  */
44
54
  export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
45
55
  export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
@@ -174,3 +184,13 @@ export interface LayerEvent {
174
184
  /** `Date.now()` at emission. */
175
185
  ts: number;
176
186
  }
187
+ export { getTypeContext, resetTypeContextCache } from "./typeContext.js";
188
+ export type { TypeContextOptions, TypeContext } from "./typeContext.js";
189
+ export { parseEditBlocks, applySingleBlock, applyEditBlocks } from "./applyEditBlock.js";
190
+ export type { EditBlock, ApplyEditBlocksOptions, ApplyResult, SingleBlockResult, } from "./applyEditBlock.js";
191
+ export { mendSingleFile } from "./mendAgent.js";
192
+ export type { MendSingleFileOptions, MendSingleFileResult, LLMCall } from "./mendAgent.js";
193
+ export { runMendLoop } from "./runMendLoop.js";
194
+ export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
195
+ export { stubAndContinue } from "./stubAndContinue.js";
196
+ export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.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,79 @@
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
+ import { type AppliedStub } from "./stubAndContinue.js";
28
+ export interface RunMendLoopOptions {
29
+ context: MendContext;
30
+ llm: {
31
+ provider: "anthropic";
32
+ model: string;
33
+ apiKey: string;
34
+ };
35
+ /** Hard cap on LLM calls. Default 3. */
36
+ maxIterations?: number;
37
+ /** Single dry-run pass — call LLM, parse, but don't write to disk. Default false. */
38
+ dryRun?: boolean;
39
+ /**
40
+ * When the loop exits with leftover errors (stopReason !== "fixed"),
41
+ * apply Layer 4 stub-and-continue: insert `// @ts-expect-error - tsfix: ...`
42
+ * comments above each unresolved error site so tsc exits 0. Opt-in.
43
+ * Default false. Ignored when `dryRun: true`.
44
+ */
45
+ stubOnFailure?: boolean;
46
+ /** @internal — LLM call override for tests. */
47
+ _callLLM?: LLMCall;
48
+ }
49
+ export interface MendLoopIteration {
50
+ index: number;
51
+ diagnosticsBefore: number;
52
+ diagnosticsAfter: number;
53
+ patchesApplied: number;
54
+ patchesFailed: number;
55
+ inputTokens: number;
56
+ outputTokens: number;
57
+ latencyMs: number;
58
+ /** Raw LLM response for this iteration — useful for debugging failed patches. */
59
+ rawResponse: string;
60
+ }
61
+ export type StopReason = "noErrors" | "fixed" | "noProgress" | "regressed" | "maxIterations" | "stubbed";
62
+ export interface RunMendLoopResult {
63
+ iterations: MendLoopIteration[];
64
+ diagnosticsBefore: Diagnostic[];
65
+ diagnosticsAfter: Diagnostic[];
66
+ passed: boolean;
67
+ stopReason: StopReason;
68
+ totalInputTokens: number;
69
+ totalOutputTokens: number;
70
+ totalLatencyMs: number;
71
+ /**
72
+ * Layer 4 stubs applied after the LLM loop terminated with leftover
73
+ * errors. Present only when `stubOnFailure: true` was set. Empty array
74
+ * means stubOnFailure ran but nothing was eligible (e.g. all errors
75
+ * were in .d.ts files).
76
+ */
77
+ stubs?: AppliedStub[];
78
+ }
79
+ export declare function runMendLoop(opts: RunMendLoopOptions): Promise<RunMendLoopResult>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Layer 4 — stub-and-continue escape hatch.
3
+ *
4
+ * When Layer 0/1 abstains and Layer 2's `runMendLoop` returns with leftover
5
+ * diagnostics (stopReason `noProgress`, `regressed`, or `maxIterations`),
6
+ * Layer 4 inserts a `// @ts-expect-error` directive immediately above each
7
+ * unresolved error site so `tsc --noEmit` exits 0. Caller's pipeline
8
+ * unblocks; the developer reviews the directive at leisure.
9
+ *
10
+ * Why `@ts-expect-error` and not `@ts-ignore`:
11
+ * `@ts-expect-error` errors out if the next line has NO error — meaning
12
+ * stale directives self-destruct as soon as the underlying issue is
13
+ * fixed by other means. `@ts-ignore` is permissive and rots silently.
14
+ *
15
+ * Trust posture: Layer 4 is opt-in. The CLI default never reaches it.
16
+ * `runMendLoop` only invokes it when `stubOnFailure: true` is set.
17
+ *
18
+ * Idempotency: re-running on a workspace that already has stubs above the
19
+ * same error lines is a no-op. We detect the existing directive on the
20
+ * line above and skip.
21
+ */
22
+ import type { Diagnostic } from "./index.js";
23
+ export interface StubAndContinueOptions {
24
+ /** Absolute path to the workspace (used for resolving / skipping node_modules). */
25
+ workspaceRoot: string;
26
+ /** Unresolved diagnostics (errors only — warnings/suggestions ignored). */
27
+ diagnostics: Diagnostic[];
28
+ /** Report what would be stubbed without writing. Default false. */
29
+ dryRun?: boolean;
30
+ logger?: {
31
+ info(msg: string): void;
32
+ warn(msg: string): void;
33
+ error(msg: string): void;
34
+ };
35
+ /** Override the comment marker (default: "tsfix"). */
36
+ stubMarker?: string;
37
+ /** Cap on message length included in the comment. Default 120. */
38
+ maxMessageLength?: number;
39
+ }
40
+ export interface AppliedStub {
41
+ /** Absolute path of the file edited. */
42
+ file: string;
43
+ /**
44
+ * 1-based line number tsc originally reported the error on (pre-stub).
45
+ * In the file *after* stubbing, the error code lives at `errorLine + 1`
46
+ * and the `@ts-expect-error` comment lives at `errorLine`.
47
+ */
48
+ errorLine: number;
49
+ /** All TS codes on the error line, deduplicated and sorted. */
50
+ codes: string[];
51
+ /** The comment text actually written (without leading whitespace). */
52
+ commentText: string;
53
+ }
54
+ export interface SkippedStub {
55
+ file: string;
56
+ line: number;
57
+ codes: string[];
58
+ reason: "node_modules" | "declaration_file" | "file_not_found" | "already_stubbed" | "file_too_short";
59
+ }
60
+ export interface StubAndContinueResult {
61
+ stubsApplied: AppliedStub[];
62
+ skipped: SkippedStub[];
63
+ filesEdited: string[];
64
+ diagnosticsBefore: number;
65
+ /** Diagnostics still on disk after stubs were applied (excludes the stubbed sites). */
66
+ diagnosticsAfter: number;
67
+ }
68
+ export declare function stubAndContinue(opts: StubAndContinueOptions): StubAndContinueResult;
@@ -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.3.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.5.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
  ],
@@ -49,18 +52,25 @@
49
52
  "build": "node scripts/build.mjs",
50
53
  "matrix": "node scripts/run-matrix.mjs",
51
54
  "capture": "node scripts/capture-fixture.mjs",
55
+ "generate-fixtures": "tsx scripts/generate-fixtures.mjs",
52
56
  "prepack": "npm run build",
53
57
  "setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
54
58
  "prebenchmark": "npm run setup-fixtures",
55
59
  "pretest": "npm run setup-fixtures",
56
60
  "benchmark": "tsx benchmark/run-benchmark.ts",
61
+ "benchmark:llm": "tsx benchmark/run-llm-benchmark.ts",
57
62
  "run-stack": "tsx cli/run-stack.ts",
58
63
  "test": "vitest run",
59
64
  "check-types": "tsc --noEmit"
60
65
  },
66
+ "dependencies": {
67
+ "@ai-sdk/anthropic": "^3.0.44",
68
+ "ai": "^6.0.86"
69
+ },
61
70
  "devDependencies": {
62
71
  "@types/node": "^20.0.0",
63
72
  "esbuild": "^0.28.0",
73
+ "ts-morph": "^28.0.0",
64
74
  "tsx": "^4.20.6",
65
75
  "typescript": "^5.9.3",
66
76
  "vitest": "^3.2.4"