@shipispec/tsfix 0.6.0 → 0.6.1
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 +43 -1
- package/README.md +1 -1
- package/dist/cli.js +207 -25
- package/dist/index.d.ts +85 -1
- package/dist/index.js +209 -6
- package/dist/types/index.d.ts +85 -1
- package/dist/types/mendAgent.d.ts +14 -1
- package/dist/types/runMendLoop.d.ts +10 -3
- package/dist/types/tsLanguageServiceFixer.d.ts +7 -0
- package/package.json +3 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,47 @@ All notable changes to `@shipispec/tsfix` are documented here. Format follows [K
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.6.1] - 2026-05-19
|
|
8
|
+
|
|
9
|
+
**Integration release.** Combines v0.6.0's library-aware error recovery with the multi-provider + telemetry work that landed on `main` between v0.5.0 and v0.6.0. The npm-published v0.6.0 was built from a stale local checkout and shipped without the Tier 2 (multi-provider) and Tier 3 (onLayerEvent + runFullStack) features that were already on `main`. v0.6.1 is the canonical "everything-since-0.5.0" release; users upgrading from v0.5.0 should jump straight to v0.6.1.
|
|
10
|
+
|
|
11
|
+
### Added (Tier 3 — telemetry + unified entrypoint)
|
|
12
|
+
- **`onLayerEvent?: (event: LayerEvent) => void`** callback option on `ValidationLoopOptions`, `RunMendLoopOptions`, and (new) `RunFullStackOptions`. Wires the `LayerEvent` type that's been published since v0.3.0 but never had a callback. Optional — undefined callback costs nothing.
|
|
13
|
+
- **Layer 1** emits one event per fixable-error attempt: `{layer: 1, errorCode, fixed, latencyMs, ts}`. `fixed: true` when a safe LSP fix landed; `fixed: false` when the fixer abstained (no candidate, ambiguous candidates, or zero-fix response).
|
|
14
|
+
- **Layer 2** emits one event per `runMendLoop` iteration: `{layer: 2, errorCode: <dominant code in iteration input>, fixed: <iteration cleared all errors>, latencyMs, ts}`. `costUsd` intentionally omitted from the per-event payload — callers can compute it from `result.layer2.totalInputTokens` + `totalOutputTokens` plus their own pricing.
|
|
15
|
+
- **Layer 4** emits one event per stub applied: `{layer: 4, errorCode: <parsed from "TSNNNN">, fixed: true, latencyMs: 0, ts}`. Multi-error coalesced stubs emit one event per `(stub × errorCode)` pair.
|
|
16
|
+
- **`runFullStack(opts)`** — new top-level entrypoint that composes Layer 0/1 → Layer 2 (opt-in via `llm`) → Layer 4 (opt-in via `stubOnFailure`) and returns a unified `RunFullStackResult`. Callers who want "run the whole stack" no longer need to compose `runValidationLoop` + `runMendLoop` + (post-`runInProcessTsc` re-check) by hand. Library equivalent of the CLI's existing all-layers flow.
|
|
17
|
+
- **`RunFullStackResult`** flat shape: `passed`, `errorsBefore`, `errorsAfterLayer1`, `errorsAfterAllLayers`, `layer1` (LSPFixer sub-result), `layer2` (RunMendLoopResult | null), `layer4` (`{stubsApplied: AppliedStub[]} | null`), `totalCostUsd`, `totalLatencyMs`, `remainingByCode`, `remainingByFile`. Matches the v0.3.0 roadmap sketch for the "unified result" type with cost + telemetry rolled in.
|
|
18
|
+
- **10 new unit tests** in `src/runFullStack.test.ts` covering: clean workspace, Layer-1-only fix, unfixable-no-LLM, mocked-Layer-2 + cost math, unknown-model fallback, Layer-4 stubOnFailure path, per-error Layer-1 events, per-iteration Layer-2 events, per-stub Layer-4 events, undefined-callback smoke.
|
|
19
|
+
|
|
20
|
+
### Added (multi-provider — Tier 2)
|
|
21
|
+
- **OpenAI and Google providers** for Layer 2. `runMendLoop` and `mendSingleFile` now accept `llm.provider: "anthropic" | "openai" | "google"` (was: `"anthropic"` only). Each provider uses its corresponding `@ai-sdk/X` package via a small `buildLanguageModel` factory in `mendAgent.ts`. The factory's `switch` is exhaustive — TypeScript flags missing cases if a new provider is added to the `LLMProvider` union.
|
|
22
|
+
- **`LLMProvider` type** exported from `src/index.ts`. Re-exportable for callers building their own CLI / pipeline integrations.
|
|
23
|
+
- **CLI `--llm-provider <name>`** flag — `anthropic` (default, back-compat), `openai`, or `google`. Invalid values exit 2 with a clear message.
|
|
24
|
+
- **Per-provider default models** when `--llm-model` is omitted: `claude-haiku-4-5` for anthropic, `gpt-5-mini` for openai, `gemini-2.5-flash` for google.
|
|
25
|
+
- **Per-provider env var routing** in the CLI: `--llm-provider anthropic` → `ANTHROPIC_API_KEY`, `openai` → `OPENAI_API_KEY`, `google` → `GOOGLE_GENERATIVE_AI_API_KEY`. The error message names the exact missing var.
|
|
26
|
+
- **Pricing table refreshed against current provider pricing pages (snapshot 2026-05-16):**
|
|
27
|
+
- **OpenAI:** `gpt-5-nano`, `gpt-5-mini`, `gpt-5`, `gpt-5.1`, `gpt-5.2`, `o3-mini`, `o4-mini`, `o3`. Default `--llm-model` for openai is `gpt-5-mini`.
|
|
28
|
+
- **Google:** `gemini-2.5-flash-lite`, `gemini-2.5-flash`, `gemini-2.5-pro`. Default for google is `gemini-2.5-flash`.
|
|
29
|
+
- **Anthropic (corrects v0.5.0 bugs):** `claude-haiku-4-5` was listed at `$0.80 / $4.00` — actual is **`$1.00 / $5.00`** (v0.5.0 carried the older Haiku 3.5 numbers). `claude-opus-4-7` was listed at `$15.00 / $75.00` — actual is **`$5.00 / $25.00`** (the 4.5 release dropped Opus pricing 3×; v0.5.0 carried the Opus 4.1 numbers). Now also lists `-sonnet-4-6`, `-opus-4-5`, `-opus-4-6`, `-opus-4-1` so callers pinning any 4.x model get accurate cost estimates.
|
|
30
|
+
- **Cost impact for v0.5.0 users:** `--llm-budget-usd` enforcement on `claude-haiku-4-5` was ~20% under-estimating actual spend; on `claude-opus-4-7` was ~3× over-estimating (your budget triggered earlier than it should have). Both fixed.
|
|
31
|
+
- Newer / unlisted models still fall back to cost=0 with a logger warning — `--llm-budget-usd` won't trigger for unpriced models.
|
|
32
|
+
- **CLI JSON report** `layer2.provider` field added.
|
|
33
|
+
- **CLI human report** Layer-2 line now shows `<provider>/<model>` instead of just `<model>`.
|
|
34
|
+
- **2 new cache tests** in `benchmark/cache.test.ts` covering provider-discrimination in the cache key + back-compat default to `anthropic` when provider is omitted (preserves v0.5.0 cache entries on upgrade).
|
|
35
|
+
- **5 new CLI tests** in `cli/run-stack.test.ts` covering `--help` listing all three providers + env-var names, invalid `--llm-provider` rejection, per-provider env-var routing, and the default-provider-is-anthropic back-compat case.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **`tsLanguageServiceFixer.ts`** internal loop now emits a `LayerEvent` per fixable error attempt when `onLayerEvent` is provided to `LSPFixerOptions`. Loop control unchanged; if the callback is undefined the only cost is one optional-chaining check per fix.
|
|
39
|
+
- **`runMendLoop.ts`** emits per-iteration Layer-2 events and per-stub Layer-4 events when `onLayerEvent` is provided. New internal helpers `parseTsCode("TS2304") → 2304` and `dominantErrorCode(diags) → 2304` for event payload assembly.
|
|
40
|
+
- **`LLMCall` type** input gains optional `provider?: LLMProvider`. Optional so v0.5.0 callers' `LLMCall` injections still type-check (their callbacks just ignore the new field).
|
|
41
|
+
- **Cache key** at `benchmark/cache.ts` now includes provider: `sha256(systemBlock + " " + userBlock + " " + provider + " " + model)`. Provider defaults to `"anthropic"` when not passed → v0.5.0 cache entries remain valid for unchanged anthropic prompts.
|
|
42
|
+
- **`scripts/build.mjs`** externalizes `@ai-sdk/openai` and `@ai-sdk/google` (in addition to `@ai-sdk/anthropic` and `ai`). Consumers who never invoke Layer 2 still don't load any AI SDK; consumers who do get whichever provider package they hit.
|
|
43
|
+
- **Runtime dependencies added:** `@ai-sdk/openai@^3.0.64`, `@ai-sdk/google@^3.0.75`. Both are loaded lazily — the AI SDK package only loads when its corresponding provider is actually called.
|
|
44
|
+
|
|
45
|
+
### Note on v0.6.0 npm tarball
|
|
46
|
+
The `0.6.0` tarball on the npm registry was published from a stale local checkout that was based on `8921356 (chore: release v0.5.0)` and never fetched the `feat/multi-provider` and `feat/tier-3-onlayerevent` PRs that had already merged to `main`. As a result, npm `0.6.0` contains library-aware error recovery (see [0.6.0] below) but **not** multi-provider or onLayerEvent. v0.6.1 is the first release that combines all three feature sets. We did not unpublish `0.6.0` to avoid leaving an unpublish tombstone in the registry; please upgrade directly to `0.6.1` or later.
|
|
47
|
+
|
|
7
48
|
## [0.6.0] - 2026-05-19
|
|
8
49
|
|
|
9
50
|
**Library-aware error recovery.** Layer 2 now auto-detects breaking-change hints for known libraries from your `package.json` and steers the LLM away from tsc's misleading quick-fixes when a library migration is the real cause. Plus a hardened type-context walk (no more crashes on rename cascades or branded types) and a meaningful set of security anti-patterns in the system prompt.
|
|
@@ -265,7 +306,8 @@ Initial public release. **Layers 0–1 only** (deterministic detection + auto-fi
|
|
|
265
306
|
- Node `>=20.9.0` (matches VS Code Extension Host runtime)
|
|
266
307
|
- TypeScript `>=5.0.0` (peer dep, must be installed in the consuming workspace)
|
|
267
308
|
|
|
268
|
-
[Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.6.
|
|
309
|
+
[Unreleased]: https://github.com/owgreen-dev/tsfix/compare/v0.6.1...HEAD
|
|
310
|
+
[0.6.1]: https://github.com/owgreen-dev/tsfix/compare/v0.6.0...v0.6.1
|
|
269
311
|
[0.6.0]: https://github.com/owgreen-dev/tsfix/compare/v0.5.0...v0.6.0
|
|
270
312
|
[0.5.0]: https://github.com/owgreen-dev/tsfix/compare/v0.4.0...v0.5.0
|
|
271
313
|
[0.4.0]: https://github.com/owgreen-dev/tsfix/compare/v0.3.0...v0.4.0
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
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 layers:
|
|
6
6
|
|
|
7
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
|
|
8
|
+
- **Layer 2** — Opt-in. A single-file LLM mend agent (Vercel AI SDK) 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. **Multi-provider** (Anthropic / OpenAI / Google) via `--llm-provider`. As of v0.6.0, also **library-aware**: tsfix reads your `package.json` and injects breaking-change hints for known libraries (`vite-plugin-svgr`, `next`, `ai`, `drizzle-orm`) so the model picks the runtime-correct fix instead of tsc's misleading quick-fix.
|
|
9
9
|
- **Layer 4** — Escape hatch. When Layer 2 can't resolve the last few errors, opt in to `// @ts-expect-error - tsfix: ...` directives that self-destruct once the underlying issue is fixed elsewhere. tsfix never leaves the workspace worse than it found it.
|
|
10
10
|
|
|
11
11
|
Layer 2 only runs if you explicitly call its API or set `ANTHROPIC_API_KEY` and pass `--llm` to the CLI. The default `tsfix --workspace ...` CLI is still **Layer 0/1 only**.
|
package/dist/cli.js
CHANGED
|
@@ -192,7 +192,7 @@ var SAFE_FIX_NAMES = /* @__PURE__ */ new Set([
|
|
|
192
192
|
// alternate spelling-fix name some TS versions emit
|
|
193
193
|
]);
|
|
194
194
|
function runLSPFixerPass(opts) {
|
|
195
|
-
const { workspaceRoot, targetFiles, logger } = opts;
|
|
195
|
+
const { workspaceRoot, targetFiles, logger, onLayerEvent } = opts;
|
|
196
196
|
const maxIterations = opts.maxIterations ?? 5;
|
|
197
197
|
const dryRun = opts.dryRun ?? false;
|
|
198
198
|
const tsconfigPath = path2.join(workspaceRoot, "tsconfig.json");
|
|
@@ -279,15 +279,37 @@ function runLSPFixerPass(opts) {
|
|
|
279
279
|
lastErrorSignatures = signatures;
|
|
280
280
|
let appliedThisIter = 0;
|
|
281
281
|
for (const err of fixableErrors) {
|
|
282
|
+
const errStartMs = Date.now();
|
|
282
283
|
const fixes = safeGetCodeFixes(service, err);
|
|
283
284
|
if (!fixes || fixes.length === 0) {
|
|
285
|
+
onLayerEvent?.({
|
|
286
|
+
layer: 1,
|
|
287
|
+
errorCode: err.code,
|
|
288
|
+
fixed: false,
|
|
289
|
+
latencyMs: Date.now() - errStartMs,
|
|
290
|
+
ts: Date.now()
|
|
291
|
+
});
|
|
284
292
|
continue;
|
|
285
293
|
}
|
|
286
294
|
const safeFixes = fixes.filter((f) => SAFE_FIX_NAMES.has(f.fixName));
|
|
287
295
|
if (safeFixes.length === 0) {
|
|
296
|
+
onLayerEvent?.({
|
|
297
|
+
layer: 1,
|
|
298
|
+
errorCode: err.code,
|
|
299
|
+
fixed: false,
|
|
300
|
+
latencyMs: Date.now() - errStartMs,
|
|
301
|
+
ts: Date.now()
|
|
302
|
+
});
|
|
288
303
|
continue;
|
|
289
304
|
}
|
|
290
305
|
if (safeFixes.length > 1 && !fixesAreEquivalent(safeFixes)) {
|
|
306
|
+
onLayerEvent?.({
|
|
307
|
+
layer: 1,
|
|
308
|
+
errorCode: err.code,
|
|
309
|
+
fixed: false,
|
|
310
|
+
latencyMs: Date.now() - errStartMs,
|
|
311
|
+
ts: Date.now()
|
|
312
|
+
});
|
|
291
313
|
continue;
|
|
292
314
|
}
|
|
293
315
|
const fix = safeFixes[0];
|
|
@@ -298,6 +320,21 @@ function runLSPFixerPass(opts) {
|
|
|
298
320
|
for (const change of fix.changes) {
|
|
299
321
|
filesEdited.add(change.fileName);
|
|
300
322
|
}
|
|
323
|
+
onLayerEvent?.({
|
|
324
|
+
layer: 1,
|
|
325
|
+
errorCode: err.code,
|
|
326
|
+
fixed: true,
|
|
327
|
+
latencyMs: Date.now() - errStartMs,
|
|
328
|
+
ts: Date.now()
|
|
329
|
+
});
|
|
330
|
+
} else {
|
|
331
|
+
onLayerEvent?.({
|
|
332
|
+
layer: 1,
|
|
333
|
+
errorCode: err.code,
|
|
334
|
+
fixed: false,
|
|
335
|
+
latencyMs: Date.now() - errStartMs,
|
|
336
|
+
ts: Date.now()
|
|
337
|
+
});
|
|
301
338
|
}
|
|
302
339
|
}
|
|
303
340
|
logger.info(
|
|
@@ -799,6 +836,8 @@ import * as fs6 from "node:fs";
|
|
|
799
836
|
import * as path6 from "node:path";
|
|
800
837
|
import { generateText } from "ai";
|
|
801
838
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
839
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
840
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
802
841
|
|
|
803
842
|
// src/libraryMigrations.ts
|
|
804
843
|
import * as fs5 from "node:fs";
|
|
@@ -982,10 +1021,24 @@ ${lines.join("\n")}
|
|
|
982
1021
|
|
|
983
1022
|
Emit SEARCH/REPLACE blocks to resolve.`;
|
|
984
1023
|
}
|
|
985
|
-
|
|
986
|
-
|
|
1024
|
+
function buildLanguageModel(provider, model, apiKey) {
|
|
1025
|
+
switch (provider) {
|
|
1026
|
+
case "anthropic":
|
|
1027
|
+
return createAnthropic({ apiKey })(model);
|
|
1028
|
+
case "openai":
|
|
1029
|
+
return createOpenAI({ apiKey })(model);
|
|
1030
|
+
case "google":
|
|
1031
|
+
return createGoogleGenerativeAI({ apiKey })(model);
|
|
1032
|
+
default: {
|
|
1033
|
+
const _exhaustive = provider;
|
|
1034
|
+
throw new Error(`unknown provider: ${_exhaustive}`);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
var defaultLLMCall = async ({ systemBlock, userBlock, provider = "anthropic", model, apiKey }) => {
|
|
1039
|
+
const llmModel = buildLanguageModel(provider, model, apiKey);
|
|
987
1040
|
const result = await generateText({
|
|
988
|
-
model:
|
|
1041
|
+
model: llmModel,
|
|
989
1042
|
system: systemBlock,
|
|
990
1043
|
messages: [{ role: "user", content: userBlock }]
|
|
991
1044
|
});
|
|
@@ -1007,6 +1060,7 @@ async function mendSingleFile(opts) {
|
|
|
1007
1060
|
const llmResult = await _callLLM({
|
|
1008
1061
|
systemBlock,
|
|
1009
1062
|
userBlock,
|
|
1063
|
+
provider: llm.provider,
|
|
1010
1064
|
model: llm.model,
|
|
1011
1065
|
apiKey: llm.apiKey
|
|
1012
1066
|
});
|
|
@@ -1211,8 +1265,35 @@ function refreshDiagnostics(workspaceRoot, files) {
|
|
|
1211
1265
|
});
|
|
1212
1266
|
return result.diagnostics.filter((d) => d.category === "error");
|
|
1213
1267
|
}
|
|
1268
|
+
function parseTsCode(code) {
|
|
1269
|
+
const m = /^TS(\d+)$/.exec(code);
|
|
1270
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
1271
|
+
}
|
|
1272
|
+
function dominantErrorCode(diags) {
|
|
1273
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1274
|
+
for (const d of diags) {
|
|
1275
|
+
counts.set(d.code, (counts.get(d.code) ?? 0) + 1);
|
|
1276
|
+
}
|
|
1277
|
+
let bestCode = "";
|
|
1278
|
+
let bestCount = 0;
|
|
1279
|
+
for (const [code, count] of counts) {
|
|
1280
|
+
if (count > bestCount) {
|
|
1281
|
+
bestCount = count;
|
|
1282
|
+
bestCode = code;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
return parseTsCode(bestCode);
|
|
1286
|
+
}
|
|
1214
1287
|
async function runMendLoop(opts) {
|
|
1215
|
-
const {
|
|
1288
|
+
const {
|
|
1289
|
+
context: rawContext,
|
|
1290
|
+
llm,
|
|
1291
|
+
maxIterations = 3,
|
|
1292
|
+
dryRun = false,
|
|
1293
|
+
stubOnFailure = false,
|
|
1294
|
+
onLayerEvent,
|
|
1295
|
+
_callLLM
|
|
1296
|
+
} = opts;
|
|
1216
1297
|
const startMs = Date.now();
|
|
1217
1298
|
const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
|
|
1218
1299
|
const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
|
|
@@ -1263,6 +1344,13 @@ async function runMendLoop(opts) {
|
|
|
1263
1344
|
latencyMs: mend.latencyMs,
|
|
1264
1345
|
rawResponse: mend.rawResponse
|
|
1265
1346
|
});
|
|
1347
|
+
onLayerEvent?.({
|
|
1348
|
+
layer: 2,
|
|
1349
|
+
errorCode: dominantErrorCode(currentDiags),
|
|
1350
|
+
fixed: newDiags.length === 0,
|
|
1351
|
+
latencyMs: mend.latencyMs,
|
|
1352
|
+
ts: Date.now()
|
|
1353
|
+
});
|
|
1266
1354
|
if (dryRun) {
|
|
1267
1355
|
currentDiags = newDiags;
|
|
1268
1356
|
stopReason = "maxIterations";
|
|
@@ -1293,6 +1381,20 @@ async function runMendLoop(opts) {
|
|
|
1293
1381
|
diagnostics: currentDiags
|
|
1294
1382
|
});
|
|
1295
1383
|
stubs = stubResult.stubsApplied;
|
|
1384
|
+
if (onLayerEvent) {
|
|
1385
|
+
const stubTs = Date.now();
|
|
1386
|
+
for (const stub of stubResult.stubsApplied) {
|
|
1387
|
+
for (const code of stub.codes) {
|
|
1388
|
+
onLayerEvent({
|
|
1389
|
+
layer: 4,
|
|
1390
|
+
errorCode: parseTsCode(code),
|
|
1391
|
+
fixed: true,
|
|
1392
|
+
latencyMs: 0,
|
|
1393
|
+
ts: stubTs
|
|
1394
|
+
});
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1296
1398
|
const postStubDiags = refreshDiagnostics(context.workspaceRoot, filesInScope);
|
|
1297
1399
|
if (postStubDiags.length === 0) {
|
|
1298
1400
|
stopReason = "stubbed";
|
|
@@ -1369,7 +1471,13 @@ function runValidationLoop(opts) {
|
|
|
1369
1471
|
iterations: 0
|
|
1370
1472
|
};
|
|
1371
1473
|
if (errorsBefore > 0 && !skipLSPFixer) {
|
|
1372
|
-
const lsp = runLSPFixerPass({
|
|
1474
|
+
const lsp = runLSPFixerPass({
|
|
1475
|
+
workspaceRoot,
|
|
1476
|
+
targetFiles,
|
|
1477
|
+
logger,
|
|
1478
|
+
dryRun,
|
|
1479
|
+
onLayerEvent: opts.onLayerEvent
|
|
1480
|
+
});
|
|
1373
1481
|
lspFixer = {
|
|
1374
1482
|
ran: true,
|
|
1375
1483
|
fixesApplied: lsp.fixesApplied,
|
|
@@ -1402,16 +1510,59 @@ function runValidationLoop(opts) {
|
|
|
1402
1510
|
}
|
|
1403
1511
|
|
|
1404
1512
|
// cli/run-stack.ts
|
|
1405
|
-
var
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1513
|
+
var PRICING = {
|
|
1514
|
+
anthropic: {
|
|
1515
|
+
// All 4.5+ models share the same tier (the 4.5 release brought a
|
|
1516
|
+
// significant price drop on Opus). 4.1 retains the older Opus tier.
|
|
1517
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
1518
|
+
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
1519
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
1520
|
+
"claude-opus-4-5": { input: 5, output: 25 },
|
|
1521
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
1522
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
1523
|
+
"claude-opus-4-1": { input: 15, output: 75 }
|
|
1524
|
+
},
|
|
1525
|
+
openai: {
|
|
1526
|
+
// Mini / nano tiers — well-matched to TypeScript repair (small
|
|
1527
|
+
// context, structured output). Default model uses one of these.
|
|
1528
|
+
"gpt-5-nano": { input: 0.05, output: 0.4 },
|
|
1529
|
+
"gpt-5-mini": { input: 0.25, output: 2 },
|
|
1530
|
+
// gpt-5 flagship + recent point releases (all $1.25 / $10).
|
|
1531
|
+
"gpt-5": { input: 1.25, output: 10 },
|
|
1532
|
+
"gpt-5.1": { input: 1.25, output: 10 },
|
|
1533
|
+
"gpt-5.2": { input: 1.75, output: 14 },
|
|
1534
|
+
// Reasoning models — sometimes better at semantic repair, more expensive.
|
|
1535
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
1536
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
1537
|
+
"o3": { input: 2, output: 8 }
|
|
1538
|
+
},
|
|
1539
|
+
google: {
|
|
1540
|
+
// Lite < flash < pro, matching the haiku/sonnet/opus mental model.
|
|
1541
|
+
"gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
|
|
1542
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5 },
|
|
1543
|
+
// Standard tier (≤200k tokens). 2.5-pro doubles to $2.50/$15.00 above
|
|
1544
|
+
// 200k — not modeled here since our prompts are well below that.
|
|
1545
|
+
"gemini-2.5-pro": { input: 1.25, output: 10 }
|
|
1546
|
+
}
|
|
1409
1547
|
};
|
|
1410
|
-
function estimateCostUsd(model, inputTokens, outputTokens) {
|
|
1411
|
-
const p =
|
|
1548
|
+
function estimateCostUsd(provider, model, inputTokens, outputTokens) {
|
|
1549
|
+
const p = PRICING[provider]?.[model];
|
|
1412
1550
|
if (!p) return 0;
|
|
1413
1551
|
return (inputTokens * p.input + outputTokens * p.output) / 1e6;
|
|
1414
1552
|
}
|
|
1553
|
+
var ENV_KEY_BY_PROVIDER = {
|
|
1554
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
1555
|
+
openai: "OPENAI_API_KEY",
|
|
1556
|
+
google: "GOOGLE_GENERATIVE_AI_API_KEY"
|
|
1557
|
+
};
|
|
1558
|
+
var DEFAULT_MODEL_BY_PROVIDER = {
|
|
1559
|
+
anthropic: "claude-haiku-4-5",
|
|
1560
|
+
openai: "gpt-5-mini",
|
|
1561
|
+
google: "gemini-2.5-flash"
|
|
1562
|
+
};
|
|
1563
|
+
function isLLMProvider(s) {
|
|
1564
|
+
return s === "anthropic" || s === "openai" || s === "google";
|
|
1565
|
+
}
|
|
1415
1566
|
function parseArgs(argv) {
|
|
1416
1567
|
const args = {
|
|
1417
1568
|
workspace: "",
|
|
@@ -1421,7 +1572,11 @@ function parseArgs(argv) {
|
|
|
1421
1572
|
files: void 0,
|
|
1422
1573
|
verbose: false,
|
|
1423
1574
|
llm: false,
|
|
1424
|
-
|
|
1575
|
+
llmProvider: "anthropic",
|
|
1576
|
+
// llmModel default depends on provider — we set it AFTER parsing so
|
|
1577
|
+
// `--llm-provider openai` without `--llm-model` picks gpt-4o-mini, etc.
|
|
1578
|
+
// An empty string here means "use the provider's default".
|
|
1579
|
+
llmModel: "",
|
|
1425
1580
|
llmMaxIterations: 3,
|
|
1426
1581
|
llmBudgetUsd: void 0,
|
|
1427
1582
|
noLibraryHints: false
|
|
@@ -1442,6 +1597,13 @@ function parseArgs(argv) {
|
|
|
1442
1597
|
args.verbose = true;
|
|
1443
1598
|
} else if (a === "--llm") {
|
|
1444
1599
|
args.llm = true;
|
|
1600
|
+
} else if (a === "--llm-provider") {
|
|
1601
|
+
const p = argv[++i] ?? "";
|
|
1602
|
+
if (!isLLMProvider(p)) {
|
|
1603
|
+
console.error(`error: --llm-provider expects one of: anthropic, openai, google. Got '${p}'`);
|
|
1604
|
+
process.exit(2);
|
|
1605
|
+
}
|
|
1606
|
+
args.llmProvider = p;
|
|
1445
1607
|
} else if (a === "--llm-model") {
|
|
1446
1608
|
args.llmModel = argv[++i] ?? args.llmModel;
|
|
1447
1609
|
} else if (a === "--llm-max-iterations") {
|
|
@@ -1470,6 +1632,9 @@ function parseArgs(argv) {
|
|
|
1470
1632
|
printHelp();
|
|
1471
1633
|
process.exit(2);
|
|
1472
1634
|
}
|
|
1635
|
+
if (args.llmModel === "") {
|
|
1636
|
+
args.llmModel = DEFAULT_MODEL_BY_PROVIDER[args.llmProvider];
|
|
1637
|
+
}
|
|
1473
1638
|
return args;
|
|
1474
1639
|
}
|
|
1475
1640
|
function printHelp() {
|
|
@@ -1485,12 +1650,23 @@ Layer 0/1 (default \u2014 deterministic, no network):
|
|
|
1485
1650
|
--verbose, -v Stream layer logs to stderr
|
|
1486
1651
|
--help, -h Show this help
|
|
1487
1652
|
|
|
1488
|
-
Layer 2 (opt-in \u2014 single-file LLM mend
|
|
1653
|
+
Layer 2 (opt-in \u2014 single-file LLM mend):
|
|
1489
1654
|
--llm Enable Layer 2 on errors that survive Layer 0/1
|
|
1490
|
-
--llm-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1655
|
+
--llm-provider <name> anthropic | openai | google (default: anthropic)
|
|
1656
|
+
--llm-model <name> Model name. Defaults per provider:
|
|
1657
|
+
anthropic \u2192 claude-haiku-4-5
|
|
1658
|
+
openai \u2192 gpt-5-mini
|
|
1659
|
+
google \u2192 gemini-2.5-flash
|
|
1660
|
+
Known-priced models per provider:
|
|
1661
|
+
anthropic: claude-haiku-4-5, -sonnet-4-5,
|
|
1662
|
+
-sonnet-4-6, -opus-4-5, -opus-4-6,
|
|
1663
|
+
-opus-4-7, -opus-4-1
|
|
1664
|
+
openai: gpt-5-nano, gpt-5-mini, gpt-5,
|
|
1665
|
+
gpt-5.1, gpt-5.2, o3-mini, o4-mini, o3
|
|
1666
|
+
google: gemini-2.5-flash-lite, gemini-2.5-flash,
|
|
1667
|
+
gemini-2.5-pro
|
|
1668
|
+
Cost estimate is 0 for unlisted models (the
|
|
1669
|
+
warning suggests pinning a listed one).
|
|
1494
1670
|
--llm-max-iterations <N> Cap on LLM retries (default: 3)
|
|
1495
1671
|
--llm-budget-usd <amount> Soft cost cap. Exits with code 3 if exceeded.
|
|
1496
1672
|
--no-library-hints Disable auto-detection of library breaking-change
|
|
@@ -1500,7 +1676,10 @@ Layer 2 (opt-in \u2014 single-file LLM mend via Anthropic):
|
|
|
1500
1676
|
tsfix injects hints into Layer 2's prompt + skips
|
|
1501
1677
|
Layer 0/1 (whose quick-fix would conflict).
|
|
1502
1678
|
|
|
1503
|
-
Layer 2 requires
|
|
1679
|
+
Layer 2 requires the provider's API key in env:
|
|
1680
|
+
anthropic \u2192 ANTHROPIC_API_KEY
|
|
1681
|
+
openai \u2192 OPENAI_API_KEY
|
|
1682
|
+
google \u2192 GOOGLE_GENERATIVE_AI_API_KEY
|
|
1504
1683
|
|
|
1505
1684
|
Exit codes:
|
|
1506
1685
|
0 no errors after stack
|
|
@@ -1551,7 +1730,7 @@ TSC Defense Stack \u2014 ${r.workspace}${r.dryRun ? " (dry-run)" : ""}
|
|
|
1551
1730
|
` Layer 2 (LLM): ${l2.errorsBefore} \u2192 ${l2.errorsAfter} errors ${l2.iterations}\xD7 iter ${l2.totalInputTokens}\u2192${l2.totalOutputTokens} tokens $${l2.totalCostUsd.toFixed(4)} ${l2.budgetExceeded ? "\u26A0\uFE0F budget exceeded" : ""}
|
|
1552
1731
|
`
|
|
1553
1732
|
);
|
|
1554
|
-
w.write(`
|
|
1733
|
+
w.write(` ${l2.provider}/${l2.model} \xB7 stopReason=${l2.stopReason}
|
|
1555
1734
|
`);
|
|
1556
1735
|
}
|
|
1557
1736
|
w.write(` errors after: ${r.errorsAfter}
|
|
@@ -1622,14 +1801,15 @@ async function main() {
|
|
|
1622
1801
|
console.error("error: --llm and --dry-run are mutually exclusive (Layer 2 writes patches to disk)");
|
|
1623
1802
|
return 2;
|
|
1624
1803
|
}
|
|
1625
|
-
const
|
|
1804
|
+
const envKeyName = ENV_KEY_BY_PROVIDER[args.llmProvider];
|
|
1805
|
+
const apiKey = process.env[envKeyName];
|
|
1626
1806
|
if (!apiKey) {
|
|
1627
|
-
console.error(
|
|
1807
|
+
console.error(`error: --llm with provider '${args.llmProvider}' requires ${envKeyName} in the environment`);
|
|
1628
1808
|
return 2;
|
|
1629
1809
|
}
|
|
1630
|
-
if (!
|
|
1810
|
+
if (!PRICING[args.llmProvider]?.[args.llmModel]) {
|
|
1631
1811
|
logger.warn(
|
|
1632
|
-
`unknown model '${args.llmModel}' \u2014 cost estimates will be 0; budget cap will not trigger`
|
|
1812
|
+
`unknown model '${args.llmProvider}/${args.llmModel}' \u2014 cost estimates will be 0; budget cap will not trigger`
|
|
1633
1813
|
);
|
|
1634
1814
|
}
|
|
1635
1815
|
const errorDiags = loop.diagnostics.filter((d) => d.category === "error");
|
|
@@ -1645,11 +1825,12 @@ async function main() {
|
|
|
1645
1825
|
const layer2Start = Date.now();
|
|
1646
1826
|
const mend = await runMendLoop({
|
|
1647
1827
|
context,
|
|
1648
|
-
llm: { provider:
|
|
1828
|
+
llm: { provider: args.llmProvider, model: args.llmModel, apiKey },
|
|
1649
1829
|
maxIterations: args.llmMaxIterations
|
|
1650
1830
|
});
|
|
1651
1831
|
void layer2Start;
|
|
1652
1832
|
const totalCostUsd = estimateCostUsd(
|
|
1833
|
+
args.llmProvider,
|
|
1653
1834
|
args.llmModel,
|
|
1654
1835
|
mend.totalInputTokens,
|
|
1655
1836
|
mend.totalOutputTokens
|
|
@@ -1665,6 +1846,7 @@ async function main() {
|
|
|
1665
1846
|
totalOutputTokens: mend.totalOutputTokens,
|
|
1666
1847
|
totalCostUsd,
|
|
1667
1848
|
budgetExceeded,
|
|
1849
|
+
provider: args.llmProvider,
|
|
1668
1850
|
model: args.llmModel
|
|
1669
1851
|
};
|
|
1670
1852
|
const post = runInProcessTsc({
|
package/dist/index.d.ts
CHANGED
|
@@ -82,6 +82,12 @@ export interface ValidationLoopOptions {
|
|
|
82
82
|
dryRun?: boolean;
|
|
83
83
|
/** Default: a no-op logger. Pass your own to capture layer events. */
|
|
84
84
|
logger?: Logger;
|
|
85
|
+
/**
|
|
86
|
+
* Per-error telemetry callback for Layer 1 (LSP fixer). Fires once per
|
|
87
|
+
* fixable error with `{layer: 1, errorCode, fixed, latencyMs, ts}`. Optional;
|
|
88
|
+
* undefined callback costs nothing. See `LayerEvent`.
|
|
89
|
+
*/
|
|
90
|
+
onLayerEvent?: (event: LayerEvent) => void;
|
|
85
91
|
}
|
|
86
92
|
export interface ValidationLoopResult {
|
|
87
93
|
passed: boolean;
|
|
@@ -203,10 +209,88 @@ export type { TypeContextOptions, TypeContext } from "./typeContext.js";
|
|
|
203
209
|
export { parseEditBlocks, applySingleBlock, applyEditBlocks } from "./applyEditBlock.js";
|
|
204
210
|
export type { EditBlock, ApplyEditBlocksOptions, ApplyResult, SingleBlockResult, } from "./applyEditBlock.js";
|
|
205
211
|
export { mendSingleFile } from "./mendAgent.js";
|
|
206
|
-
export type { MendSingleFileOptions, MendSingleFileResult, LLMCall } from "./mendAgent.js";
|
|
212
|
+
export type { MendSingleFileOptions, MendSingleFileResult, LLMCall, LLMProvider, } from "./mendAgent.js";
|
|
207
213
|
export { runMendLoop } from "./runMendLoop.js";
|
|
208
214
|
export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
|
|
209
215
|
export { stubAndContinue } from "./stubAndContinue.js";
|
|
210
216
|
export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.js";
|
|
211
217
|
export { BUILT_IN_LIBRARY_MIGRATIONS, detectLibraryMigrations, formatLibraryMigrationsBlock, formatLibraryMigrationsTaskDescription, } from "./libraryMigrations.js";
|
|
212
218
|
export type { LibraryMigrationHint } from "./libraryMigrations.js";
|
|
219
|
+
import { type RunMendLoopResult } from "./runMendLoop.js";
|
|
220
|
+
import type { AppliedStub } from "./stubAndContinue.js";
|
|
221
|
+
import type { LLMProvider } from "./mendAgent.js";
|
|
222
|
+
export interface RunFullStackOptions {
|
|
223
|
+
/** Absolute path to the workspace (must contain `tsconfig.json`). */
|
|
224
|
+
workspaceRoot: string;
|
|
225
|
+
/** Files to scope to. If omitted, all `.ts`/`.tsx` files under workspaceRoot. */
|
|
226
|
+
targetFiles?: string[];
|
|
227
|
+
/** Skip Layer 0/1 LSP auto-fixer. Default false. */
|
|
228
|
+
skipLSPFixer?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Layer 2 config. If omitted, the loop stops after Layer 0/1 (matches
|
|
231
|
+
* `runValidationLoop` behavior — no LLM calls).
|
|
232
|
+
*/
|
|
233
|
+
llm?: {
|
|
234
|
+
provider: LLMProvider;
|
|
235
|
+
model: string;
|
|
236
|
+
apiKey: string;
|
|
237
|
+
maxIterations?: number;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* After Layer 2 (if any), insert `// @ts-expect-error - tsfix: …` above
|
|
241
|
+
* each unresolved error site so tsc exits 0. Opt-in. Default false.
|
|
242
|
+
*/
|
|
243
|
+
stubOnFailure?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Run all layers in memory; report counts but don't write. Default false.
|
|
246
|
+
* Layer 2 is auto-skipped under dryRun (it writes patches; would be a
|
|
247
|
+
* no-op).
|
|
248
|
+
*/
|
|
249
|
+
dryRun?: boolean;
|
|
250
|
+
/** Logger. Default no-op. */
|
|
251
|
+
logger?: Logger;
|
|
252
|
+
/** Per-layer telemetry stream. Forwarded to Layer 1, 2, 4. */
|
|
253
|
+
onLayerEvent?: (event: LayerEvent) => void;
|
|
254
|
+
/** @internal — LLM call override. Tests inject a fake; real callers leave it. */
|
|
255
|
+
_callLLM?: import("./mendAgent.js").LLMCall;
|
|
256
|
+
}
|
|
257
|
+
export interface RunFullStackResult {
|
|
258
|
+
/** True if `errorsAfterAllLayers === 0`. */
|
|
259
|
+
passed: boolean;
|
|
260
|
+
/** Errors detected before any fix attempt. */
|
|
261
|
+
errorsBefore: number;
|
|
262
|
+
/** Errors remaining after Layer 0/1 (before Layer 2 + 4). */
|
|
263
|
+
errorsAfterLayer1: number;
|
|
264
|
+
/** Errors remaining after every layer that ran. */
|
|
265
|
+
errorsAfterAllLayers: number;
|
|
266
|
+
/** Per-layer sub-results. `layer2` is null when `llm` was omitted; `layer4` is null when `stubOnFailure` was false (or had no candidates). */
|
|
267
|
+
layer1: ValidationLoopResult["lspFixer"];
|
|
268
|
+
layer2: RunMendLoopResult | null;
|
|
269
|
+
layer4: {
|
|
270
|
+
stubsApplied: AppliedStub[];
|
|
271
|
+
} | null;
|
|
272
|
+
/** USD spent on Layer 2. 0 when Layer 2 didn't run. */
|
|
273
|
+
totalCostUsd: number;
|
|
274
|
+
/** Wall-clock total across all layers. */
|
|
275
|
+
totalLatencyMs: number;
|
|
276
|
+
/** Remaining diagnostics, grouped by TS code (e.g. `{TS2339: 3}`). */
|
|
277
|
+
remainingByCode: Record<string, number>;
|
|
278
|
+
/** Remaining diagnostics, grouped by file path (relative to workspaceRoot). */
|
|
279
|
+
remainingByFile: Record<string, number>;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Run the full tsfix stack (Layer 0/1 → Layer 2 → Layer 4) end-to-end.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* const result = await runFullStack({
|
|
287
|
+
* workspaceRoot: "/path/to/project",
|
|
288
|
+
* llm: { provider: "anthropic", model: "claude-haiku-4-5", apiKey: KEY },
|
|
289
|
+
* stubOnFailure: true,
|
|
290
|
+
* onLayerEvent: (e) => console.log(e),
|
|
291
|
+
* });
|
|
292
|
+
* if (!result.passed) { ... }
|
|
293
|
+
* console.log(`Spent $${result.totalCostUsd.toFixed(4)}`);
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export declare function runFullStack(opts: RunFullStackOptions): Promise<RunFullStackResult>;
|
package/dist/index.js
CHANGED
|
@@ -189,7 +189,7 @@ var SAFE_FIX_NAMES = /* @__PURE__ */ new Set([
|
|
|
189
189
|
// alternate spelling-fix name some TS versions emit
|
|
190
190
|
]);
|
|
191
191
|
function runLSPFixerPass(opts) {
|
|
192
|
-
const { workspaceRoot, targetFiles, logger } = opts;
|
|
192
|
+
const { workspaceRoot, targetFiles, logger, onLayerEvent } = opts;
|
|
193
193
|
const maxIterations = opts.maxIterations ?? 5;
|
|
194
194
|
const dryRun = opts.dryRun ?? false;
|
|
195
195
|
const tsconfigPath = path2.join(workspaceRoot, "tsconfig.json");
|
|
@@ -276,15 +276,37 @@ function runLSPFixerPass(opts) {
|
|
|
276
276
|
lastErrorSignatures = signatures;
|
|
277
277
|
let appliedThisIter = 0;
|
|
278
278
|
for (const err of fixableErrors) {
|
|
279
|
+
const errStartMs = Date.now();
|
|
279
280
|
const fixes = safeGetCodeFixes(service, err);
|
|
280
281
|
if (!fixes || fixes.length === 0) {
|
|
282
|
+
onLayerEvent?.({
|
|
283
|
+
layer: 1,
|
|
284
|
+
errorCode: err.code,
|
|
285
|
+
fixed: false,
|
|
286
|
+
latencyMs: Date.now() - errStartMs,
|
|
287
|
+
ts: Date.now()
|
|
288
|
+
});
|
|
281
289
|
continue;
|
|
282
290
|
}
|
|
283
291
|
const safeFixes = fixes.filter((f) => SAFE_FIX_NAMES.has(f.fixName));
|
|
284
292
|
if (safeFixes.length === 0) {
|
|
293
|
+
onLayerEvent?.({
|
|
294
|
+
layer: 1,
|
|
295
|
+
errorCode: err.code,
|
|
296
|
+
fixed: false,
|
|
297
|
+
latencyMs: Date.now() - errStartMs,
|
|
298
|
+
ts: Date.now()
|
|
299
|
+
});
|
|
285
300
|
continue;
|
|
286
301
|
}
|
|
287
302
|
if (safeFixes.length > 1 && !fixesAreEquivalent(safeFixes)) {
|
|
303
|
+
onLayerEvent?.({
|
|
304
|
+
layer: 1,
|
|
305
|
+
errorCode: err.code,
|
|
306
|
+
fixed: false,
|
|
307
|
+
latencyMs: Date.now() - errStartMs,
|
|
308
|
+
ts: Date.now()
|
|
309
|
+
});
|
|
288
310
|
continue;
|
|
289
311
|
}
|
|
290
312
|
const fix = safeFixes[0];
|
|
@@ -295,6 +317,21 @@ function runLSPFixerPass(opts) {
|
|
|
295
317
|
for (const change of fix.changes) {
|
|
296
318
|
filesEdited.add(change.fileName);
|
|
297
319
|
}
|
|
320
|
+
onLayerEvent?.({
|
|
321
|
+
layer: 1,
|
|
322
|
+
errorCode: err.code,
|
|
323
|
+
fixed: true,
|
|
324
|
+
latencyMs: Date.now() - errStartMs,
|
|
325
|
+
ts: Date.now()
|
|
326
|
+
});
|
|
327
|
+
} else {
|
|
328
|
+
onLayerEvent?.({
|
|
329
|
+
layer: 1,
|
|
330
|
+
errorCode: err.code,
|
|
331
|
+
fixed: false,
|
|
332
|
+
latencyMs: Date.now() - errStartMs,
|
|
333
|
+
ts: Date.now()
|
|
334
|
+
});
|
|
298
335
|
}
|
|
299
336
|
}
|
|
300
337
|
logger.info(
|
|
@@ -804,6 +841,8 @@ import * as fs6 from "node:fs";
|
|
|
804
841
|
import * as path6 from "node:path";
|
|
805
842
|
import { generateText } from "ai";
|
|
806
843
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
844
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
845
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
807
846
|
|
|
808
847
|
// src/libraryMigrations.ts
|
|
809
848
|
import * as fs5 from "node:fs";
|
|
@@ -987,10 +1026,24 @@ ${lines.join("\n")}
|
|
|
987
1026
|
|
|
988
1027
|
Emit SEARCH/REPLACE blocks to resolve.`;
|
|
989
1028
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1029
|
+
function buildLanguageModel(provider, model, apiKey) {
|
|
1030
|
+
switch (provider) {
|
|
1031
|
+
case "anthropic":
|
|
1032
|
+
return createAnthropic({ apiKey })(model);
|
|
1033
|
+
case "openai":
|
|
1034
|
+
return createOpenAI({ apiKey })(model);
|
|
1035
|
+
case "google":
|
|
1036
|
+
return createGoogleGenerativeAI({ apiKey })(model);
|
|
1037
|
+
default: {
|
|
1038
|
+
const _exhaustive = provider;
|
|
1039
|
+
throw new Error(`unknown provider: ${_exhaustive}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
var defaultLLMCall = async ({ systemBlock, userBlock, provider = "anthropic", model, apiKey }) => {
|
|
1044
|
+
const llmModel = buildLanguageModel(provider, model, apiKey);
|
|
992
1045
|
const result = await generateText({
|
|
993
|
-
model:
|
|
1046
|
+
model: llmModel,
|
|
994
1047
|
system: systemBlock,
|
|
995
1048
|
messages: [{ role: "user", content: userBlock }]
|
|
996
1049
|
});
|
|
@@ -1012,6 +1065,7 @@ async function mendSingleFile(opts) {
|
|
|
1012
1065
|
const llmResult = await _callLLM({
|
|
1013
1066
|
systemBlock,
|
|
1014
1067
|
userBlock,
|
|
1068
|
+
provider: llm.provider,
|
|
1015
1069
|
model: llm.model,
|
|
1016
1070
|
apiKey: llm.apiKey
|
|
1017
1071
|
});
|
|
@@ -1216,8 +1270,35 @@ function refreshDiagnostics(workspaceRoot, files) {
|
|
|
1216
1270
|
});
|
|
1217
1271
|
return result.diagnostics.filter((d) => d.category === "error");
|
|
1218
1272
|
}
|
|
1273
|
+
function parseTsCode(code) {
|
|
1274
|
+
const m = /^TS(\d+)$/.exec(code);
|
|
1275
|
+
return m ? parseInt(m[1], 10) : 0;
|
|
1276
|
+
}
|
|
1277
|
+
function dominantErrorCode(diags) {
|
|
1278
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1279
|
+
for (const d of diags) {
|
|
1280
|
+
counts.set(d.code, (counts.get(d.code) ?? 0) + 1);
|
|
1281
|
+
}
|
|
1282
|
+
let bestCode = "";
|
|
1283
|
+
let bestCount = 0;
|
|
1284
|
+
for (const [code, count] of counts) {
|
|
1285
|
+
if (count > bestCount) {
|
|
1286
|
+
bestCount = count;
|
|
1287
|
+
bestCode = code;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
return parseTsCode(bestCode);
|
|
1291
|
+
}
|
|
1219
1292
|
async function runMendLoop(opts) {
|
|
1220
|
-
const {
|
|
1293
|
+
const {
|
|
1294
|
+
context: rawContext,
|
|
1295
|
+
llm,
|
|
1296
|
+
maxIterations = 3,
|
|
1297
|
+
dryRun = false,
|
|
1298
|
+
stubOnFailure = false,
|
|
1299
|
+
onLayerEvent,
|
|
1300
|
+
_callLLM
|
|
1301
|
+
} = opts;
|
|
1221
1302
|
const startMs = Date.now();
|
|
1222
1303
|
const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
|
|
1223
1304
|
const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
|
|
@@ -1268,6 +1349,13 @@ async function runMendLoop(opts) {
|
|
|
1268
1349
|
latencyMs: mend.latencyMs,
|
|
1269
1350
|
rawResponse: mend.rawResponse
|
|
1270
1351
|
});
|
|
1352
|
+
onLayerEvent?.({
|
|
1353
|
+
layer: 2,
|
|
1354
|
+
errorCode: dominantErrorCode(currentDiags),
|
|
1355
|
+
fixed: newDiags.length === 0,
|
|
1356
|
+
latencyMs: mend.latencyMs,
|
|
1357
|
+
ts: Date.now()
|
|
1358
|
+
});
|
|
1271
1359
|
if (dryRun) {
|
|
1272
1360
|
currentDiags = newDiags;
|
|
1273
1361
|
stopReason = "maxIterations";
|
|
@@ -1298,6 +1386,20 @@ async function runMendLoop(opts) {
|
|
|
1298
1386
|
diagnostics: currentDiags
|
|
1299
1387
|
});
|
|
1300
1388
|
stubs = stubResult.stubsApplied;
|
|
1389
|
+
if (onLayerEvent) {
|
|
1390
|
+
const stubTs = Date.now();
|
|
1391
|
+
for (const stub of stubResult.stubsApplied) {
|
|
1392
|
+
for (const code of stub.codes) {
|
|
1393
|
+
onLayerEvent({
|
|
1394
|
+
layer: 4,
|
|
1395
|
+
errorCode: parseTsCode(code),
|
|
1396
|
+
fixed: true,
|
|
1397
|
+
latencyMs: 0,
|
|
1398
|
+
ts: stubTs
|
|
1399
|
+
});
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1301
1403
|
const postStubDiags = refreshDiagnostics(context.workspaceRoot, filesInScope);
|
|
1302
1404
|
if (postStubDiags.length === 0) {
|
|
1303
1405
|
stopReason = "stubbed";
|
|
@@ -1374,7 +1476,13 @@ function runValidationLoop(opts) {
|
|
|
1374
1476
|
iterations: 0
|
|
1375
1477
|
};
|
|
1376
1478
|
if (errorsBefore > 0 && !skipLSPFixer) {
|
|
1377
|
-
const lsp = runLSPFixerPass({
|
|
1479
|
+
const lsp = runLSPFixerPass({
|
|
1480
|
+
workspaceRoot,
|
|
1481
|
+
targetFiles,
|
|
1482
|
+
logger,
|
|
1483
|
+
dryRun,
|
|
1484
|
+
onLayerEvent: opts.onLayerEvent
|
|
1485
|
+
});
|
|
1378
1486
|
lspFixer = {
|
|
1379
1487
|
ran: true,
|
|
1380
1488
|
fixesApplied: lsp.fixesApplied,
|
|
@@ -1405,6 +1513,100 @@ function runValidationLoop(opts) {
|
|
|
1405
1513
|
elapsedMs: Date.now() - startMs
|
|
1406
1514
|
};
|
|
1407
1515
|
}
|
|
1516
|
+
var PRICING = {
|
|
1517
|
+
anthropic: {
|
|
1518
|
+
"claude-haiku-4-5": { input: 1, output: 5 },
|
|
1519
|
+
"claude-sonnet-4-5": { input: 3, output: 15 },
|
|
1520
|
+
"claude-sonnet-4-6": { input: 3, output: 15 },
|
|
1521
|
+
"claude-opus-4-5": { input: 5, output: 25 },
|
|
1522
|
+
"claude-opus-4-6": { input: 5, output: 25 },
|
|
1523
|
+
"claude-opus-4-7": { input: 5, output: 25 },
|
|
1524
|
+
"claude-opus-4-1": { input: 15, output: 75 }
|
|
1525
|
+
},
|
|
1526
|
+
openai: {
|
|
1527
|
+
"gpt-5-nano": { input: 0.05, output: 0.4 },
|
|
1528
|
+
"gpt-5-mini": { input: 0.25, output: 2 },
|
|
1529
|
+
"gpt-5": { input: 1.25, output: 10 },
|
|
1530
|
+
"gpt-5.1": { input: 1.25, output: 10 },
|
|
1531
|
+
"gpt-5.2": { input: 1.75, output: 14 },
|
|
1532
|
+
"o3-mini": { input: 1.1, output: 4.4 },
|
|
1533
|
+
"o4-mini": { input: 1.1, output: 4.4 },
|
|
1534
|
+
"o3": { input: 2, output: 8 }
|
|
1535
|
+
},
|
|
1536
|
+
google: {
|
|
1537
|
+
"gemini-2.5-flash-lite": { input: 0.1, output: 0.4 },
|
|
1538
|
+
"gemini-2.5-flash": { input: 0.3, output: 2.5 },
|
|
1539
|
+
"gemini-2.5-pro": { input: 1.25, output: 10 }
|
|
1540
|
+
}
|
|
1541
|
+
};
|
|
1542
|
+
function costUsd(provider, model, inputTokens, outputTokens) {
|
|
1543
|
+
const p = PRICING[provider]?.[model];
|
|
1544
|
+
if (!p) return 0;
|
|
1545
|
+
return (inputTokens * p.input + outputTokens * p.output) / 1e6;
|
|
1546
|
+
}
|
|
1547
|
+
async function runFullStack(opts) {
|
|
1548
|
+
const startMs = Date.now();
|
|
1549
|
+
const { workspaceRoot, llm, stubOnFailure = false, dryRun = false, onLayerEvent } = opts;
|
|
1550
|
+
const layer1 = runValidationLoop({
|
|
1551
|
+
workspaceRoot,
|
|
1552
|
+
targetFiles: opts.targetFiles,
|
|
1553
|
+
skipLSPFixer: opts.skipLSPFixer,
|
|
1554
|
+
dryRun,
|
|
1555
|
+
logger: opts.logger,
|
|
1556
|
+
onLayerEvent
|
|
1557
|
+
});
|
|
1558
|
+
let layer2 = null;
|
|
1559
|
+
let layer4 = null;
|
|
1560
|
+
let totalCostUsd = 0;
|
|
1561
|
+
let finalDiagnostics = layer1.diagnostics;
|
|
1562
|
+
const shouldRunLayer2 = llm && !dryRun && layer1.errorsAfter > 0;
|
|
1563
|
+
if (shouldRunLayer2) {
|
|
1564
|
+
const errorDiags = layer1.diagnostics.filter((d) => d.category === "error");
|
|
1565
|
+
layer2 = await runMendLoop({
|
|
1566
|
+
context: {
|
|
1567
|
+
workspaceRoot,
|
|
1568
|
+
diagnostics: errorDiags,
|
|
1569
|
+
erroredFiles: Array.from(new Set(errorDiags.map((d) => d.file)))
|
|
1570
|
+
},
|
|
1571
|
+
llm: { provider: llm.provider, model: llm.model, apiKey: llm.apiKey },
|
|
1572
|
+
maxIterations: llm.maxIterations,
|
|
1573
|
+
stubOnFailure,
|
|
1574
|
+
onLayerEvent,
|
|
1575
|
+
_callLLM: opts._callLLM
|
|
1576
|
+
});
|
|
1577
|
+
totalCostUsd = costUsd(llm.provider, llm.model, layer2.totalInputTokens, layer2.totalOutputTokens);
|
|
1578
|
+
if (layer2.stubs && layer2.stubs.length > 0) {
|
|
1579
|
+
layer4 = { stubsApplied: layer2.stubs };
|
|
1580
|
+
}
|
|
1581
|
+
resetInProcessTscCache();
|
|
1582
|
+
const post = runInProcessTsc({
|
|
1583
|
+
workspaceRoot,
|
|
1584
|
+
generatedFiles: opts.targetFiles ?? discoverTsFiles(workspaceRoot),
|
|
1585
|
+
logger: opts.logger ?? noopLogger3
|
|
1586
|
+
});
|
|
1587
|
+
finalDiagnostics = post.diagnostics;
|
|
1588
|
+
}
|
|
1589
|
+
const finalErrorDiags = finalDiagnostics.filter((d) => d.category === "error");
|
|
1590
|
+
const remainingByCode = {};
|
|
1591
|
+
const remainingByFile = {};
|
|
1592
|
+
for (const d of finalErrorDiags) {
|
|
1593
|
+
remainingByCode[d.code] = (remainingByCode[d.code] ?? 0) + 1;
|
|
1594
|
+
remainingByFile[d.file] = (remainingByFile[d.file] ?? 0) + 1;
|
|
1595
|
+
}
|
|
1596
|
+
return {
|
|
1597
|
+
passed: finalErrorDiags.length === 0,
|
|
1598
|
+
errorsBefore: layer1.errorsBefore,
|
|
1599
|
+
errorsAfterLayer1: layer1.errorsAfter,
|
|
1600
|
+
errorsAfterAllLayers: finalErrorDiags.length,
|
|
1601
|
+
layer1: layer1.lspFixer,
|
|
1602
|
+
layer2,
|
|
1603
|
+
layer4,
|
|
1604
|
+
totalCostUsd,
|
|
1605
|
+
totalLatencyMs: Date.now() - startMs,
|
|
1606
|
+
remainingByCode,
|
|
1607
|
+
remainingByFile
|
|
1608
|
+
};
|
|
1609
|
+
}
|
|
1408
1610
|
export {
|
|
1409
1611
|
BUILT_IN_LIBRARY_MIGRATIONS,
|
|
1410
1612
|
applyEditBlocks,
|
|
@@ -1421,6 +1623,7 @@ export {
|
|
|
1421
1623
|
resetInProcessTscCache,
|
|
1422
1624
|
resetLSPFixerCache,
|
|
1423
1625
|
resetTypeContextCache,
|
|
1626
|
+
runFullStack,
|
|
1424
1627
|
runInProcessTsc,
|
|
1425
1628
|
runLSPFixerPass,
|
|
1426
1629
|
runMendLoop,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -82,6 +82,12 @@ export interface ValidationLoopOptions {
|
|
|
82
82
|
dryRun?: boolean;
|
|
83
83
|
/** Default: a no-op logger. Pass your own to capture layer events. */
|
|
84
84
|
logger?: Logger;
|
|
85
|
+
/**
|
|
86
|
+
* Per-error telemetry callback for Layer 1 (LSP fixer). Fires once per
|
|
87
|
+
* fixable error with `{layer: 1, errorCode, fixed, latencyMs, ts}`. Optional;
|
|
88
|
+
* undefined callback costs nothing. See `LayerEvent`.
|
|
89
|
+
*/
|
|
90
|
+
onLayerEvent?: (event: LayerEvent) => void;
|
|
85
91
|
}
|
|
86
92
|
export interface ValidationLoopResult {
|
|
87
93
|
passed: boolean;
|
|
@@ -203,10 +209,88 @@ export type { TypeContextOptions, TypeContext } from "./typeContext.js";
|
|
|
203
209
|
export { parseEditBlocks, applySingleBlock, applyEditBlocks } from "./applyEditBlock.js";
|
|
204
210
|
export type { EditBlock, ApplyEditBlocksOptions, ApplyResult, SingleBlockResult, } from "./applyEditBlock.js";
|
|
205
211
|
export { mendSingleFile } from "./mendAgent.js";
|
|
206
|
-
export type { MendSingleFileOptions, MendSingleFileResult, LLMCall } from "./mendAgent.js";
|
|
212
|
+
export type { MendSingleFileOptions, MendSingleFileResult, LLMCall, LLMProvider, } from "./mendAgent.js";
|
|
207
213
|
export { runMendLoop } from "./runMendLoop.js";
|
|
208
214
|
export type { RunMendLoopOptions, RunMendLoopResult, MendLoopIteration, StopReason, } from "./runMendLoop.js";
|
|
209
215
|
export { stubAndContinue } from "./stubAndContinue.js";
|
|
210
216
|
export type { StubAndContinueOptions, StubAndContinueResult, AppliedStub, SkippedStub, } from "./stubAndContinue.js";
|
|
211
217
|
export { BUILT_IN_LIBRARY_MIGRATIONS, detectLibraryMigrations, formatLibraryMigrationsBlock, formatLibraryMigrationsTaskDescription, } from "./libraryMigrations.js";
|
|
212
218
|
export type { LibraryMigrationHint } from "./libraryMigrations.js";
|
|
219
|
+
import { type RunMendLoopResult } from "./runMendLoop.js";
|
|
220
|
+
import type { AppliedStub } from "./stubAndContinue.js";
|
|
221
|
+
import type { LLMProvider } from "./mendAgent.js";
|
|
222
|
+
export interface RunFullStackOptions {
|
|
223
|
+
/** Absolute path to the workspace (must contain `tsconfig.json`). */
|
|
224
|
+
workspaceRoot: string;
|
|
225
|
+
/** Files to scope to. If omitted, all `.ts`/`.tsx` files under workspaceRoot. */
|
|
226
|
+
targetFiles?: string[];
|
|
227
|
+
/** Skip Layer 0/1 LSP auto-fixer. Default false. */
|
|
228
|
+
skipLSPFixer?: boolean;
|
|
229
|
+
/**
|
|
230
|
+
* Layer 2 config. If omitted, the loop stops after Layer 0/1 (matches
|
|
231
|
+
* `runValidationLoop` behavior — no LLM calls).
|
|
232
|
+
*/
|
|
233
|
+
llm?: {
|
|
234
|
+
provider: LLMProvider;
|
|
235
|
+
model: string;
|
|
236
|
+
apiKey: string;
|
|
237
|
+
maxIterations?: number;
|
|
238
|
+
};
|
|
239
|
+
/**
|
|
240
|
+
* After Layer 2 (if any), insert `// @ts-expect-error - tsfix: …` above
|
|
241
|
+
* each unresolved error site so tsc exits 0. Opt-in. Default false.
|
|
242
|
+
*/
|
|
243
|
+
stubOnFailure?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Run all layers in memory; report counts but don't write. Default false.
|
|
246
|
+
* Layer 2 is auto-skipped under dryRun (it writes patches; would be a
|
|
247
|
+
* no-op).
|
|
248
|
+
*/
|
|
249
|
+
dryRun?: boolean;
|
|
250
|
+
/** Logger. Default no-op. */
|
|
251
|
+
logger?: Logger;
|
|
252
|
+
/** Per-layer telemetry stream. Forwarded to Layer 1, 2, 4. */
|
|
253
|
+
onLayerEvent?: (event: LayerEvent) => void;
|
|
254
|
+
/** @internal — LLM call override. Tests inject a fake; real callers leave it. */
|
|
255
|
+
_callLLM?: import("./mendAgent.js").LLMCall;
|
|
256
|
+
}
|
|
257
|
+
export interface RunFullStackResult {
|
|
258
|
+
/** True if `errorsAfterAllLayers === 0`. */
|
|
259
|
+
passed: boolean;
|
|
260
|
+
/** Errors detected before any fix attempt. */
|
|
261
|
+
errorsBefore: number;
|
|
262
|
+
/** Errors remaining after Layer 0/1 (before Layer 2 + 4). */
|
|
263
|
+
errorsAfterLayer1: number;
|
|
264
|
+
/** Errors remaining after every layer that ran. */
|
|
265
|
+
errorsAfterAllLayers: number;
|
|
266
|
+
/** Per-layer sub-results. `layer2` is null when `llm` was omitted; `layer4` is null when `stubOnFailure` was false (or had no candidates). */
|
|
267
|
+
layer1: ValidationLoopResult["lspFixer"];
|
|
268
|
+
layer2: RunMendLoopResult | null;
|
|
269
|
+
layer4: {
|
|
270
|
+
stubsApplied: AppliedStub[];
|
|
271
|
+
} | null;
|
|
272
|
+
/** USD spent on Layer 2. 0 when Layer 2 didn't run. */
|
|
273
|
+
totalCostUsd: number;
|
|
274
|
+
/** Wall-clock total across all layers. */
|
|
275
|
+
totalLatencyMs: number;
|
|
276
|
+
/** Remaining diagnostics, grouped by TS code (e.g. `{TS2339: 3}`). */
|
|
277
|
+
remainingByCode: Record<string, number>;
|
|
278
|
+
/** Remaining diagnostics, grouped by file path (relative to workspaceRoot). */
|
|
279
|
+
remainingByFile: Record<string, number>;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Run the full tsfix stack (Layer 0/1 → Layer 2 → Layer 4) end-to-end.
|
|
283
|
+
*
|
|
284
|
+
* @example
|
|
285
|
+
* ```ts
|
|
286
|
+
* const result = await runFullStack({
|
|
287
|
+
* workspaceRoot: "/path/to/project",
|
|
288
|
+
* llm: { provider: "anthropic", model: "claude-haiku-4-5", apiKey: KEY },
|
|
289
|
+
* stubOnFailure: true,
|
|
290
|
+
* onLayerEvent: (e) => console.log(e),
|
|
291
|
+
* });
|
|
292
|
+
* if (!result.passed) { ... }
|
|
293
|
+
* console.log(`Spent $${result.totalCostUsd.toFixed(4)}`);
|
|
294
|
+
* ```
|
|
295
|
+
*/
|
|
296
|
+
export declare function runFullStack(opts: RunFullStackOptions): Promise<RunFullStackResult>;
|
|
@@ -16,10 +16,17 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import type { MendContext } from "./index.js";
|
|
18
18
|
import { type ApplyResult, type EditBlock } from "./applyEditBlock.js";
|
|
19
|
+
/**
|
|
20
|
+
* Provider identifier. Each corresponds to one `@ai-sdk/<provider>` adapter
|
|
21
|
+
* in the Vercel AI SDK. Adding a provider requires (1) the corresponding
|
|
22
|
+
* `@ai-sdk/X` dep in package.json, (2) a case in `buildLanguageModel`, and
|
|
23
|
+
* (3) the appropriate API-key env-var name documented in the CLI.
|
|
24
|
+
*/
|
|
25
|
+
export type LLMProvider = "anthropic" | "openai" | "google";
|
|
19
26
|
export interface MendSingleFileOptions {
|
|
20
27
|
context: MendContext;
|
|
21
28
|
llm: {
|
|
22
|
-
provider:
|
|
29
|
+
provider: LLMProvider;
|
|
23
30
|
model: string;
|
|
24
31
|
apiKey: string;
|
|
25
32
|
};
|
|
@@ -39,6 +46,12 @@ export interface MendSingleFileResult {
|
|
|
39
46
|
export type LLMCall = (params: {
|
|
40
47
|
systemBlock: string;
|
|
41
48
|
userBlock: string;
|
|
49
|
+
/**
|
|
50
|
+
* Provider name. Optional for backward compatibility — when omitted,
|
|
51
|
+
* the default impl treats it as "anthropic" (matches v0.5.0 behavior).
|
|
52
|
+
* Test injection points can ignore this field.
|
|
53
|
+
*/
|
|
54
|
+
provider?: LLMProvider;
|
|
42
55
|
model: string;
|
|
43
56
|
apiKey: string;
|
|
44
57
|
}) => Promise<{
|
|
@@ -22,13 +22,13 @@
|
|
|
22
22
|
* returns. We can't iterate without writing to disk because re-validation
|
|
23
23
|
* needs the actual file changes.
|
|
24
24
|
*/
|
|
25
|
-
import type { Diagnostic, MendContext } from "./index.js";
|
|
26
|
-
import { type LLMCall } from "./mendAgent.js";
|
|
25
|
+
import type { Diagnostic, LayerEvent, MendContext } from "./index.js";
|
|
26
|
+
import { type LLMCall, type LLMProvider } from "./mendAgent.js";
|
|
27
27
|
import { type AppliedStub } from "./stubAndContinue.js";
|
|
28
28
|
export interface RunMendLoopOptions {
|
|
29
29
|
context: MendContext;
|
|
30
30
|
llm: {
|
|
31
|
-
provider:
|
|
31
|
+
provider: LLMProvider;
|
|
32
32
|
model: string;
|
|
33
33
|
apiKey: string;
|
|
34
34
|
};
|
|
@@ -43,6 +43,13 @@ export interface RunMendLoopOptions {
|
|
|
43
43
|
* Default false. Ignored when `dryRun: true`.
|
|
44
44
|
*/
|
|
45
45
|
stubOnFailure?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Per-iteration / per-stub telemetry callback. Layer 2 emits one event per
|
|
48
|
+
* iteration with the dominant error code (`fixed: true` if the iteration
|
|
49
|
+
* cleared all errors); Layer 4 emits one event per stubbed `(line, code)`
|
|
50
|
+
* pair. Both forwarded to the same callback. Optional.
|
|
51
|
+
*/
|
|
52
|
+
onLayerEvent?: (event: LayerEvent) => void;
|
|
46
53
|
/** @internal — LLM call override for tests. */
|
|
47
54
|
_callLLM?: LLMCall;
|
|
48
55
|
}
|
|
@@ -51,6 +51,13 @@ export interface LSPFixerOptions {
|
|
|
51
51
|
* before letting it modify a workspace.
|
|
52
52
|
*/
|
|
53
53
|
dryRun?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Per-error telemetry callback. One event per `(errorCode, fix-attempt)`
|
|
56
|
+
* with `fixed: true` when the fix landed and `fixed: false` when the LSP
|
|
57
|
+
* abstained (no safe candidate). Events fire even on dry runs.
|
|
58
|
+
* Optional — undefined callback costs nothing.
|
|
59
|
+
*/
|
|
60
|
+
onLayerEvent?: (event: import("./index.js").LayerEvent) => void;
|
|
54
61
|
}
|
|
55
62
|
export interface LSPFixerResult {
|
|
56
63
|
/** Number of fixes successfully applied across all iterations. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipispec/tsfix",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@ai-sdk/anthropic": "^3.0.44",
|
|
68
|
+
"@ai-sdk/google": "^3.0.75",
|
|
69
|
+
"@ai-sdk/openai": "^3.0.64",
|
|
68
70
|
"ai": "^6.0.86"
|
|
69
71
|
},
|
|
70
72
|
"devDependencies": {
|