@shipispec/tsfix 0.4.0 → 0.6.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.
@@ -44,6 +44,12 @@
44
44
  * - `mendSingleFile` — single-LLM-call repair via Vercel AI SDK
45
45
  * - `runMendLoop` — bounded retry with no-progress / regression detection
46
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.
47
53
  */
48
54
  export { runInProcessTsc, isInProcessTscEnabled, resetInProcessTscCache } from "./validatorInProcess.js";
49
55
  export type { InProcessTscOptions, InProcessTscResult } from "./validatorInProcess.js";
@@ -150,6 +156,20 @@ export interface MendContext {
150
156
  priorTaskExports?: string;
151
157
  /** Compact type signatures of installed npm dependencies (helps prevent API hallucination). */
152
158
  installedTypes?: string;
159
+ /**
160
+ * Library-migration hints for installed deps whose breaking changes are
161
+ * known to mislead tsc's quick-fix. When non-empty, the Layer 2 prompt
162
+ * leads with these and the `taskDescription` is overridden to name them.
163
+ *
164
+ * Auto-populated by `runMendLoop` from `<workspaceRoot>/package.json` if
165
+ * the caller doesn't set it. Pass `[]` explicitly to opt out.
166
+ *
167
+ * See `libraryMigrations.ts` for the built-in registry.
168
+ */
169
+ libraryMigrations?: Array<{
170
+ name: string;
171
+ hint: string;
172
+ }>;
153
173
  }
154
174
  /**
155
175
  * Per-layer event for streaming telemetry across the validate → fix → mend
@@ -186,3 +206,7 @@ export { mendSingleFile } from "./mendAgent.js";
186
206
  export type { MendSingleFileOptions, MendSingleFileResult, LLMCall } from "./mendAgent.js";
187
207
  export { runMendLoop } from "./runMendLoop.js";
188
208
  export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
209
+ export { stubAndContinue } from "./stubAndContinue.js";
210
+ export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.js";
211
+ export { BUILT_IN_LIBRARY_MIGRATIONS, detectLibraryMigrations, formatLibraryMigrationsBlock, formatLibraryMigrationsTaskDescription, } from "./libraryMigrations.js";
212
+ export type { LibraryMigrationHint } from "./libraryMigrations.js";
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Library breaking-change registry for Layer 2.
3
+ *
4
+ * When an installed dependency has a known migration whose correct fix
5
+ * differs from tsc's own quick-fix suggestion, this module injects a hint
6
+ * into the mend prompt so the LLM doesn't blindly follow tsc.
7
+ *
8
+ * Empirically grounded — each entry corresponds to a concrete bench case
9
+ * where, without the hint, both haiku-4-5 and sonnet-4-5 score 0/3
10
+ * functional+secure and follow tsc's misleading quick-fix; with the hint,
11
+ * both score 3/3 functional+secure on the same fixture.
12
+ *
13
+ * Scope: library MIGRATIONS where tsc's quick-fix is misleading or where
14
+ * the correct fix requires syntax not present in the source. NOT for
15
+ * general TS errors — those are tsfix's deterministic Layer 0/1 surface.
16
+ */
17
+ export interface LibraryMigrationHint {
18
+ /** Display name, e.g., `"vite-plugin-svgr@^4.0.0"`. Populated by detect. */
19
+ name: string;
20
+ /** Instructional text injected into the Layer 2 system prompt. */
21
+ hint: string;
22
+ }
23
+ interface RegistryEntry {
24
+ match: {
25
+ name: string;
26
+ minMajor?: number;
27
+ maxMajor?: number;
28
+ };
29
+ hint: string;
30
+ }
31
+ /**
32
+ * Built-in registry. Keep entries SMALL — only patterns where we have
33
+ * empirical evidence that the model picks the wrong fix without the hint.
34
+ * Don't lard this up with general advice; that belongs in the system prompt.
35
+ */
36
+ export declare const BUILT_IN_LIBRARY_MIGRATIONS: RegistryEntry[];
37
+ /**
38
+ * Read the workspace's package.json, walk dependencies + devDependencies,
39
+ * return the registry entries whose match rule fires.
40
+ *
41
+ * Best-effort: returns `[]` on any failure (missing package.json, parse
42
+ * error, etc.). Never throws.
43
+ */
44
+ export declare function detectLibraryMigrations(workspaceRoot: string, registry?: RegistryEntry[]): LibraryMigrationHint[];
45
+ /**
46
+ * Format an array of hints into a prompt block. Empty input → empty string,
47
+ * caller can short-circuit.
48
+ */
49
+ export declare function formatLibraryMigrationsBlock(hints: LibraryMigrationHint[]): string;
50
+ /**
51
+ * Build the one-line task description from a list of hints. Empty input
52
+ * → undefined. We've found that putting library names in the
53
+ * `taskDescription` (the prompt's headline framing) is dramatically more
54
+ * effective than burying the same content in the body.
55
+ */
56
+ export declare function formatLibraryMigrationsTaskDescription(hints: LibraryMigrationHint[]): string | undefined;
57
+ export {};
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import type { Diagnostic, MendContext } from "./index.js";
26
26
  import { type LLMCall } from "./mendAgent.js";
27
+ import { type AppliedStub } from "./stubAndContinue.js";
27
28
  export interface RunMendLoopOptions {
28
29
  context: MendContext;
29
30
  llm: {
@@ -35,6 +36,13 @@ export interface RunMendLoopOptions {
35
36
  maxIterations?: number;
36
37
  /** Single dry-run pass — call LLM, parse, but don't write to disk. Default false. */
37
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;
38
46
  /** @internal — LLM call override for tests. */
39
47
  _callLLM?: LLMCall;
40
48
  }
@@ -50,7 +58,7 @@ export interface MendLoopIteration {
50
58
  /** Raw LLM response for this iteration — useful for debugging failed patches. */
51
59
  rawResponse: string;
52
60
  }
53
- export type StopReason = "noErrors" | "fixed" | "noProgress" | "regressed" | "maxIterations";
61
+ export type StopReason = "noErrors" | "fixed" | "noProgress" | "regressed" | "maxIterations" | "stubbed";
54
62
  export interface RunMendLoopResult {
55
63
  iterations: MendLoopIteration[];
56
64
  diagnosticsBefore: Diagnostic[];
@@ -60,5 +68,12 @@ export interface RunMendLoopResult {
60
68
  totalInputTokens: number;
61
69
  totalOutputTokens: number;
62
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[];
63
78
  }
64
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipispec/tsfix",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
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",
@@ -52,8 +52,7 @@
52
52
  "build": "node scripts/build.mjs",
53
53
  "matrix": "node scripts/run-matrix.mjs",
54
54
  "capture": "node scripts/capture-fixture.mjs",
55
- "pregenerate-fixtures": "npm run build",
56
- "generate-fixtures": "node scripts/generate-fixtures.mjs",
55
+ "generate-fixtures": "tsx scripts/generate-fixtures.mjs",
57
56
  "prepack": "npm run build",
58
57
  "setup-fixtures": "node -e \"require('fs').existsSync('fixtures/_shared/node_modules')||require('child_process').execSync('npm install --prefix fixtures/_shared',{stdio:'inherit'})\"",
59
58
  "prebenchmark": "npm run setup-fixtures",