@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 +47 -1
- package/README.md +88 -26
- package/dist/cli.js +5 -0
- package/dist/index.d.ts +18 -6
- package/dist/index.js +35192 -17
- package/dist/types/applyEditBlock.d.ts +68 -0
- package/dist/types/index.d.ts +18 -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 +13 -2
|
@@ -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;
|
package/dist/types/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,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
|
|
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
|
|
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
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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";
|
|
@@ -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.
|
|
4
|
-
"description": "
|
|
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
|
],
|
|
@@ -49,18 +52,26 @@
|
|
|
49
52
|
"build": "node scripts/build.mjs",
|
|
50
53
|
"matrix": "node scripts/run-matrix.mjs",
|
|
51
54
|
"capture": "node scripts/capture-fixture.mjs",
|
|
55
|
+
"pregenerate-fixtures": "npm run build",
|
|
56
|
+
"generate-fixtures": "node scripts/generate-fixtures.mjs",
|
|
52
57
|
"prepack": "npm run build",
|
|
53
58
|
"setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
|
|
54
59
|
"prebenchmark": "npm run setup-fixtures",
|
|
55
60
|
"pretest": "npm run setup-fixtures",
|
|
56
61
|
"benchmark": "tsx benchmark/run-benchmark.ts",
|
|
62
|
+
"benchmark:llm": "tsx benchmark/run-llm-benchmark.ts",
|
|
57
63
|
"run-stack": "tsx cli/run-stack.ts",
|
|
58
64
|
"test": "vitest run",
|
|
59
65
|
"check-types": "tsc --noEmit"
|
|
60
66
|
},
|
|
67
|
+
"dependencies": {
|
|
68
|
+
"@ai-sdk/anthropic": "^3.0.44",
|
|
69
|
+
"ai": "^6.0.86"
|
|
70
|
+
},
|
|
61
71
|
"devDependencies": {
|
|
62
72
|
"@types/node": "^20.0.0",
|
|
63
73
|
"esbuild": "^0.28.0",
|
|
74
|
+
"ts-morph": "^28.0.0",
|
|
64
75
|
"tsx": "^4.20.6",
|
|
65
76
|
"typescript": "^5.9.3",
|
|
66
77
|
"vitest": "^3.2.4"
|