@shipispec/tsfix 0.5.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/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(
@@ -475,8 +512,8 @@ function resetLSPFixerCache() {
475
512
  }
476
513
 
477
514
  // src/index.ts
478
- import * as fs7 from "node:fs";
479
- import * as path7 from "node:path";
515
+ import * as fs8 from "node:fs";
516
+ import * as path8 from "node:path";
480
517
 
481
518
  // src/typeContext.ts
482
519
  import * as fs3 from "node:fs";
@@ -554,9 +591,20 @@ function isLibFile(fileName) {
554
591
  }
555
592
  function findTypeDeclaration(checker, startNode, maxWalkUp = 4) {
556
593
  const tryResolve = (n) => {
557
- const type = checker.getTypeAtLocation(n);
558
- const symbol = type.getSymbol() ?? type.aliasSymbol;
559
- const declarations = symbol?.getDeclarations();
594
+ let type;
595
+ try {
596
+ type = checker.getTypeAtLocation(n);
597
+ } catch {
598
+ return void 0;
599
+ }
600
+ let symbol;
601
+ let declarations;
602
+ try {
603
+ symbol = type.getSymbol() ?? type.aliasSymbol;
604
+ declarations = symbol?.getDeclarations();
605
+ } catch {
606
+ return void 0;
607
+ }
560
608
  if (!declarations || declarations.length === 0) return void 0;
561
609
  const nonLib = declarations.find((d) => !isLibFile(d.getSourceFile().fileName));
562
610
  if (!nonLib) return void 0;
@@ -789,10 +837,79 @@ function applyEditBlocks(opts) {
789
837
  }
790
838
 
791
839
  // src/mendAgent.ts
792
- import * as fs5 from "node:fs";
793
- import * as path5 from "node:path";
840
+ import * as fs6 from "node:fs";
841
+ import * as path6 from "node:path";
794
842
  import { generateText } from "ai";
795
843
  import { createAnthropic } from "@ai-sdk/anthropic";
844
+ import { createOpenAI } from "@ai-sdk/openai";
845
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
846
+
847
+ // src/libraryMigrations.ts
848
+ import * as fs5 from "node:fs";
849
+ import * as path5 from "node:path";
850
+ var BUILT_IN_LIBRARY_MIGRATIONS = [
851
+ {
852
+ match: { name: "vite-plugin-svgr", minMajor: 4 },
853
+ hint: "vite-plugin-svgr v4+ (released 2023-09-20) changed how SVG imports work. The PREVIOUS form `import { ReactComponent as X } from './x.svg'` no longer works \u2014 the ambient module declaration now only matches `*.svg?react`. Correct fix: `import X from './x.svg?react'` (default import + ?react query suffix). DO NOT use tsc's quick-fix `import X from './x.svg'` (no query) \u2014 that type-checks but resolves to the asset URL string at runtime, not a component."
854
+ },
855
+ {
856
+ match: { name: "next", minMajor: 15 },
857
+ hint: "Next.js 15 changed dynamic-route page props: `params` and `searchParams` are now `Promise<...>` instead of plain objects. The fix shape is: change the page's `params` type to `Promise<{...}>`, mark the page component `async`, and `await params` inside. See https://nextjs.org/docs/app/api-reference/file-conventions/page."
858
+ },
859
+ {
860
+ match: { name: "ai", minMajor: 3, maxMajor: 4 },
861
+ hint: "Vercel AI SDK v3.x has overload-narrowing issues with `generateObject`. If passing a schema through an object widened with `satisfies Record<K, z.ZodTypeAny>`, the typed overload silently falls back to `output: 'no-schema'` (which forbids the `schema` property). Fix: drop the `satisfies Record<...>` widener, or cast the schema at the call site."
862
+ },
863
+ {
864
+ match: { name: "drizzle-orm" },
865
+ hint: "Drizzle ORM table access has two distinct surfaces. `db.<table>` is for `select/insert/update/delete` builders. `db.query.<table>` is the Relational Queries API for `findFirst`/`findMany` with relation loading. If you see `Property '<table>' does not exist on type 'PostgresJsDatabase<...>'` when trying to call `.findFirst`/`.findMany`, use `db.query.<table>` instead."
866
+ }
867
+ ];
868
+ function parseMajor(spec) {
869
+ const m = spec.match(/(\d+)(?:\.\d+)*/);
870
+ return m ? parseInt(m[1], 10) : null;
871
+ }
872
+ function detectLibraryMigrations(workspaceRoot, registry = BUILT_IN_LIBRARY_MIGRATIONS) {
873
+ let pkg;
874
+ try {
875
+ const pkgPath = path5.join(workspaceRoot, "package.json");
876
+ if (!fs5.existsSync(pkgPath)) return [];
877
+ pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
878
+ } catch {
879
+ return [];
880
+ }
881
+ const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
882
+ const hints = [];
883
+ for (const entry of registry) {
884
+ const { match, hint } = entry;
885
+ const versionSpec = allDeps[match.name];
886
+ if (!versionSpec) continue;
887
+ const major = parseMajor(versionSpec);
888
+ if (match.minMajor != null && (major == null || major < match.minMajor)) continue;
889
+ if (match.maxMajor != null && (major == null || major > match.maxMajor)) continue;
890
+ hints.push({ name: `${match.name}@${versionSpec}`, hint });
891
+ }
892
+ return hints;
893
+ }
894
+ function formatLibraryMigrationsBlock(hints) {
895
+ if (hints.length === 0) return "";
896
+ const lines = ["### library-migrations", ""];
897
+ lines.push(
898
+ "These migrations apply to your workspace's installed deps. When tsc's quick-fix conflicts with the migration target below, PREFER the migration target. tsc only checks types, not runtime semantics \u2014 these hints encode runtime constraints tsc cannot see."
899
+ );
900
+ lines.push("");
901
+ for (const h of hints) {
902
+ lines.push(`- [${h.name}] ${h.hint}`);
903
+ }
904
+ return lines.join("\n");
905
+ }
906
+ function formatLibraryMigrationsTaskDescription(hints) {
907
+ if (hints.length === 0) return void 0;
908
+ const names = hints.map((h) => h.name).join(", ");
909
+ return `Library migration: ${names}`;
910
+ }
911
+
912
+ // src/mendAgent.ts
796
913
  var SYSTEM_INSTRUCTIONS = `You are a TypeScript code-repair tool. You receive a TypeScript file with one or more compiler errors and resolve them.
797
914
 
798
915
  Output ONLY SEARCH/REPLACE blocks. No prose, no explanations, no XML wrappers.
@@ -813,16 +930,43 @@ Rules:
813
930
  - REPLACE must be valid TypeScript that resolves the diagnostic.
814
931
  - Do not invent imports, types, properties, or values. Use only what the type-context section shows.
815
932
  - One SEARCH/REPLACE block per logical change.
816
- - If you cannot resolve a diagnostic with the information given, omit a block for it.`;
933
+ - If you cannot resolve a diagnostic with the information given, omit a block for it.
934
+
935
+ Anti-patterns \u2014 these silence the type error but break runtime semantics, lose type safety, or introduce security regressions. Do NOT emit a patch that does any of the following:
936
+
937
+ 1. Type-assertion escape-hatches that hide the error rather than fix it:
938
+ - \`x as any\` / \`x as unknown as T\` to dodge a real mismatch.
939
+ - \`key as keyof T\` to silence a TS7053 index-signature error when \`key\` is a runtime \`string\` (not a statically-known literal). Narrow the parameter type to \`keyof T\` at the function signature instead, OR widen the object type to include an index signature, OR perform a runtime \`if (key in obj)\` guard. \`as keyof T\` keeps the call site type-passing while losing all the runtime safety the index signature gave.
940
+ - \`!\` non-null assertions to dodge TS18047/TS2532 \u2014 narrow with a truthiness check or optional-chaining + nullish-coalesce that actually preserves the narrow on the true branch.
941
+
942
+ 2. Removing or substituting a declared dependency to dodge a missing-import error. If \`package.json\` lists the package and the source uses it, RESTORE the import. Do not substitute a different library (e.g. \`bcrypt\` \u2192 \`crypto.subtle.digest\`) \u2014 that is a security regression even when tsc accepts it.
943
+
944
+ 3. SQL / NoSQL / shell injection patterns:
945
+ - String concatenation of user-controlled values into raw query strings (\`db.execute("WHERE id = " + userId)\`). Use the library's tagged-template / parameterized form (\`db.execute(sql\\\`WHERE id = \\\${userId}\\\`)\` for Drizzle; placeholders for Prisma / mysql2; etc).
946
+ - Never use template literals to interpolate user input into a raw SQL string unless the literal is itself a parameterizing tagged template.
947
+
948
+ 4. React XSS escape-hatches:
949
+ - \`dangerouslySetInnerHTML\` to dodge a children-type error. If a component expects \`children: string\` and you have arbitrary HTML, render it as text (JSX \`{value}\` auto-escapes) or sanitize via a library (DOMPurify) and document the assumption.
950
+ - Setting \`innerHTML\` directly on a DOM element from user input.
951
+
952
+ These anti-patterns apply only to the listed shapes. For other diagnostics, follow the regular Rules above and pick the smallest valid fix \u2014 including legitimate uses of \`as unknown as T\`, \`keyof typeof T\` (as a type annotation, not a cast), or restructuring a type union. Do not omit a block just because the fix involves an \`as\` cast or a structural change \u2014 only omit when the fix would match one of the four anti-patterns above.
953
+
954
+ When a type, union variant, or interface property has been removed or renamed, consumer code that referenced the old shape needs FULL cleanup, not partial cleanup:
955
+
956
+ - TS2322 / TS2353 (excess property in object literal): REMOVE the excess property from the literal. Do not retain it. Example: if a \`{ type: 'archived', userId, reason, at }\` object now needs \`type: 'created'\` and the \`created\` variant has no \`reason\` field, the fix is to drop \`reason\` from the object \u2014 keeping it produces a fresh TS2353. This is field deletion, not "silencing an error" \u2014 there is no error to silence; the property genuinely no longer belongs.
957
+ - Function parameters and return types that exist solely to support the removed variant (e.g. a \`reason: string\` parameter on a function that no longer needs reasons) should be dropped along with their use sites in the same SEARCH/REPLACE block.
958
+ - TS2367 (comparison with no overlap): if comparing against a removed literal, EITHER pick a still-valid literal that preserves the function's spirit, OR delete the comparison and its branch if neither makes sense. Don't leave a comparison against a now-invalid literal.
959
+
960
+ The goal is internal consistency: if you change one reference to a removed variant/property, sweep ALL references in this file in the same patch. A half-cleanup leaves new tsc errors and is worse than the original state.`;
817
961
  function workspaceRelative(workspaceRoot, p) {
818
- return path5.isAbsolute(p) ? path5.relative(workspaceRoot, p) : p;
962
+ return path6.isAbsolute(p) ? path6.relative(workspaceRoot, p) : p;
819
963
  }
820
964
  function buildSystemBlock(context, erroredFile) {
821
965
  const wsRel = workspaceRelative(context.workspaceRoot, erroredFile);
822
- const absPath = path5.isAbsolute(erroredFile) ? erroredFile : path5.join(context.workspaceRoot, erroredFile);
966
+ const absPath = path6.isAbsolute(erroredFile) ? erroredFile : path6.join(context.workspaceRoot, erroredFile);
823
967
  let fileContent;
824
968
  try {
825
- fileContent = fs5.readFileSync(absPath, "utf-8");
969
+ fileContent = fs6.readFileSync(absPath, "utf-8");
826
970
  } catch {
827
971
  fileContent = "(file unreadable)";
828
972
  }
@@ -832,10 +976,15 @@ function buildSystemBlock(context, erroredFile) {
832
976
  const typeContexts = [];
833
977
  const seen = /* @__PURE__ */ new Set();
834
978
  for (const diag of fileDiags) {
835
- const ctx = getTypeContext({
836
- workspaceRoot: context.workspaceRoot,
837
- diagnostic: diag
838
- });
979
+ let ctx;
980
+ try {
981
+ ctx = getTypeContext({
982
+ workspaceRoot: context.workspaceRoot,
983
+ diagnostic: diag
984
+ });
985
+ } catch {
986
+ continue;
987
+ }
839
988
  if (!ctx.typeDeclaration) continue;
840
989
  const key = `${ctx.typeDeclaration.file}:${ctx.typeDeclaration.symbol}`;
841
990
  if (seen.has(key)) continue;
@@ -846,22 +995,21 @@ function buildSystemBlock(context, erroredFile) {
846
995
  ` + ctx.typeDeclaration.lines
847
996
  );
848
997
  }
849
- const parts = [
850
- SYSTEM_INSTRUCTIONS,
851
- "",
852
- `### file: ${wsRel}`,
853
- "```ts",
854
- fileContent.replace(/\n$/, ""),
855
- "```"
856
- ];
998
+ const parts = [SYSTEM_INSTRUCTIONS, ""];
999
+ const libMigrations = context.libraryMigrations ?? [];
1000
+ if (libMigrations.length > 0) {
1001
+ parts.push(formatLibraryMigrationsBlock(libMigrations), "");
1002
+ }
1003
+ parts.push(`### file: ${wsRel}`, "```ts", fileContent.replace(/\n$/, ""), "```");
857
1004
  if (typeContexts.length > 0) {
858
1005
  parts.push("", "### type-context");
859
1006
  for (const tc of typeContexts) {
860
1007
  parts.push("```ts", tc, "```");
861
1008
  }
862
1009
  }
863
- if (context.taskDescription) {
864
- parts.push("", `### task`, context.taskDescription);
1010
+ const taskHeadline = formatLibraryMigrationsTaskDescription(libMigrations) ?? context.taskDescription;
1011
+ if (taskHeadline) {
1012
+ parts.push("", `### task`, taskHeadline);
865
1013
  }
866
1014
  return parts.join("\n");
867
1015
  }
@@ -878,10 +1026,24 @@ ${lines.join("\n")}
878
1026
 
879
1027
  Emit SEARCH/REPLACE blocks to resolve.`;
880
1028
  }
881
- var defaultLLMCall = async ({ systemBlock, userBlock, model, apiKey }) => {
882
- const anthropic = createAnthropic({ apiKey });
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);
883
1045
  const result = await generateText({
884
- model: anthropic(model),
1046
+ model: llmModel,
885
1047
  system: systemBlock,
886
1048
  messages: [{ role: "user", content: userBlock }]
887
1049
  });
@@ -903,6 +1065,7 @@ async function mendSingleFile(opts) {
903
1065
  const llmResult = await _callLLM({
904
1066
  systemBlock,
905
1067
  userBlock,
1068
+ provider: llm.provider,
906
1069
  model: llm.model,
907
1070
  apiKey: llm.apiKey
908
1071
  });
@@ -925,8 +1088,8 @@ async function mendSingleFile(opts) {
925
1088
  }
926
1089
 
927
1090
  // src/stubAndContinue.ts
928
- import * as fs6 from "node:fs";
929
- import * as path6 from "node:path";
1091
+ import * as fs7 from "node:fs";
1092
+ import * as path7 from "node:path";
930
1093
  var noopLogger = { info: () => {
931
1094
  }, warn: () => {
932
1095
  }, error: () => {
@@ -943,17 +1106,17 @@ function groupByLine(diagnostics) {
943
1106
  return groups;
944
1107
  }
945
1108
  function resolveFile(diagnosticFile, workspaceRoot) {
946
- return path6.isAbsolute(diagnosticFile) ? diagnosticFile : path6.resolve(workspaceRoot, diagnosticFile);
1109
+ return path7.isAbsolute(diagnosticFile) ? diagnosticFile : path7.resolve(workspaceRoot, diagnosticFile);
947
1110
  }
948
1111
  function shouldSkipFile(file, workspaceRoot) {
949
- const rel = path6.relative(workspaceRoot, file);
950
- if (rel.startsWith("node_modules") || rel.includes(`${path6.sep}node_modules${path6.sep}`)) {
1112
+ const rel = path7.relative(workspaceRoot, file);
1113
+ if (rel.startsWith("node_modules") || rel.includes(`${path7.sep}node_modules${path7.sep}`)) {
951
1114
  return "node_modules";
952
1115
  }
953
1116
  if (file.endsWith(".d.ts")) {
954
1117
  return "declaration_file";
955
1118
  }
956
- if (!fs6.existsSync(file)) {
1119
+ if (!fs7.existsSync(file)) {
957
1120
  return "file_not_found";
958
1121
  }
959
1122
  return null;
@@ -1012,7 +1175,7 @@ function stubAndContinue(opts) {
1012
1175
  }
1013
1176
  continue;
1014
1177
  }
1015
- const source = fs6.readFileSync(file, "utf-8");
1178
+ const source = fs7.readFileSync(file, "utf-8");
1016
1179
  const eol = source.includes("\r\n") ? "\r\n" : "\n";
1017
1180
  const lines = source.split(/\r?\n/);
1018
1181
  entries.sort((a, b) => b.line - a.line);
@@ -1054,10 +1217,10 @@ function stubAndContinue(opts) {
1054
1217
  if (edited) {
1055
1218
  filesEditedSet.add(file);
1056
1219
  if (!dryRun) {
1057
- fs6.writeFileSync(file, lines.join(eol), "utf-8");
1058
- logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${path6.relative(workspaceRoot, file)}`);
1220
+ fs7.writeFileSync(file, lines.join(eol), "utf-8");
1221
+ logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
1059
1222
  } else {
1060
- logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${path6.relative(workspaceRoot, file)}`);
1223
+ logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
1061
1224
  }
1062
1225
  }
1063
1226
  }
@@ -1107,9 +1270,37 @@ function refreshDiagnostics(workspaceRoot, files) {
1107
1270
  });
1108
1271
  return result.diagnostics.filter((d) => d.category === "error");
1109
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
+ }
1110
1292
  async function runMendLoop(opts) {
1111
- const { context, llm, maxIterations = 3, dryRun = false, stubOnFailure = false, _callLLM } = opts;
1293
+ const {
1294
+ context: rawContext,
1295
+ llm,
1296
+ maxIterations = 3,
1297
+ dryRun = false,
1298
+ stubOnFailure = false,
1299
+ onLayerEvent,
1300
+ _callLLM
1301
+ } = opts;
1112
1302
  const startMs = Date.now();
1303
+ const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
1113
1304
  const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
1114
1305
  if (diagnosticsBefore.length === 0) {
1115
1306
  return {
@@ -1158,6 +1349,13 @@ async function runMendLoop(opts) {
1158
1349
  latencyMs: mend.latencyMs,
1159
1350
  rawResponse: mend.rawResponse
1160
1351
  });
1352
+ onLayerEvent?.({
1353
+ layer: 2,
1354
+ errorCode: dominantErrorCode(currentDiags),
1355
+ fixed: newDiags.length === 0,
1356
+ latencyMs: mend.latencyMs,
1357
+ ts: Date.now()
1358
+ });
1161
1359
  if (dryRun) {
1162
1360
  currentDiags = newDiags;
1163
1361
  stopReason = "maxIterations";
@@ -1188,6 +1386,20 @@ async function runMendLoop(opts) {
1188
1386
  diagnostics: currentDiags
1189
1387
  });
1190
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
+ }
1191
1403
  const postStubDiags = refreshDiagnostics(context.workspaceRoot, filesInScope);
1192
1404
  if (postStubDiags.length === 0) {
1193
1405
  stopReason = "stubbed";
@@ -1222,7 +1434,7 @@ function discoverTsFiles(workspaceRoot) {
1222
1434
  const walk = (dir) => {
1223
1435
  let entries;
1224
1436
  try {
1225
- entries = fs7.readdirSync(dir, { withFileTypes: true });
1437
+ entries = fs8.readdirSync(dir, { withFileTypes: true });
1226
1438
  } catch {
1227
1439
  return;
1228
1440
  }
@@ -1231,10 +1443,10 @@ function discoverTsFiles(workspaceRoot) {
1231
1443
  if (skip.has(e.name)) {
1232
1444
  continue;
1233
1445
  }
1234
- walk(path7.join(dir, e.name));
1446
+ walk(path8.join(dir, e.name));
1235
1447
  } else if (e.isFile() && !e.name.endsWith(".d.ts")) {
1236
1448
  if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
1237
- out.push(path7.relative(workspaceRoot, path7.join(dir, e.name)));
1449
+ out.push(path8.relative(workspaceRoot, path8.join(dir, e.name)));
1238
1450
  }
1239
1451
  }
1240
1452
  }
@@ -1245,10 +1457,10 @@ function discoverTsFiles(workspaceRoot) {
1245
1457
  function runValidationLoop(opts) {
1246
1458
  const { workspaceRoot, skipLSPFixer = false, dryRun = false } = opts;
1247
1459
  const logger = opts.logger ?? noopLogger3;
1248
- if (!fs7.existsSync(workspaceRoot)) {
1460
+ if (!fs8.existsSync(workspaceRoot)) {
1249
1461
  throw new Error(`workspace not found: ${workspaceRoot}`);
1250
1462
  }
1251
- if (!fs7.existsSync(path7.join(workspaceRoot, "tsconfig.json"))) {
1463
+ if (!fs8.existsSync(path8.join(workspaceRoot, "tsconfig.json"))) {
1252
1464
  throw new Error(`no tsconfig.json in ${workspaceRoot}`);
1253
1465
  }
1254
1466
  const targetFiles = opts.targetFiles ?? discoverTsFiles(workspaceRoot);
@@ -1264,7 +1476,13 @@ function runValidationLoop(opts) {
1264
1476
  iterations: 0
1265
1477
  };
1266
1478
  if (errorsBefore > 0 && !skipLSPFixer) {
1267
- const lsp = runLSPFixerPass({ workspaceRoot, targetFiles, logger, dryRun });
1479
+ const lsp = runLSPFixerPass({
1480
+ workspaceRoot,
1481
+ targetFiles,
1482
+ logger,
1483
+ dryRun,
1484
+ onLayerEvent: opts.onLayerEvent
1485
+ });
1268
1486
  lspFixer = {
1269
1487
  ran: true,
1270
1488
  fixesApplied: lsp.fixesApplied,
@@ -1295,10 +1513,108 @@ function runValidationLoop(opts) {
1295
1513
  elapsedMs: Date.now() - startMs
1296
1514
  };
1297
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
+ }
1298
1610
  export {
1611
+ BUILT_IN_LIBRARY_MIGRATIONS,
1299
1612
  applyEditBlocks,
1300
1613
  applySingleBlock,
1614
+ detectLibraryMigrations,
1301
1615
  discoverTsFiles,
1616
+ formatLibraryMigrationsBlock,
1617
+ formatLibraryMigrationsTaskDescription,
1302
1618
  getTypeContext,
1303
1619
  isInProcessTscEnabled,
1304
1620
  isLSPFixerEnabled,
@@ -1307,6 +1623,7 @@ export {
1307
1623
  resetInProcessTscCache,
1308
1624
  resetLSPFixerCache,
1309
1625
  resetTypeContextCache,
1626
+ runFullStack,
1310
1627
  runInProcessTsc,
1311
1628
  runLSPFixerPass,
1312
1629
  runMendLoop,