@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/CHANGELOG.md +109 -1
- package/README.md +65 -11
- package/dist/cli.js +387 -74
- package/dist/index.d.ts +101 -1
- package/dist/index.js +363 -46
- package/dist/types/index.d.ts +101 -1
- package/dist/types/libraryMigrations.d.ts +57 -0
- 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/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// cli/run-stack.ts
|
|
4
|
-
import * as
|
|
5
|
-
import * as
|
|
4
|
+
import * as path9 from "node:path";
|
|
5
|
+
import * as fs9 from "node:fs";
|
|
6
6
|
|
|
7
7
|
// src/validatorInProcess.ts
|
|
8
8
|
import * as fs from "node:fs";
|
|
@@ -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(
|
|
@@ -473,8 +510,8 @@ function applyFixToSnapshots(fix, snapshots) {
|
|
|
473
510
|
}
|
|
474
511
|
|
|
475
512
|
// src/index.ts
|
|
476
|
-
import * as
|
|
477
|
-
import * as
|
|
513
|
+
import * as fs8 from "node:fs";
|
|
514
|
+
import * as path8 from "node:path";
|
|
478
515
|
|
|
479
516
|
// src/typeContext.ts
|
|
480
517
|
import * as fs3 from "node:fs";
|
|
@@ -549,9 +586,20 @@ function isLibFile(fileName) {
|
|
|
549
586
|
}
|
|
550
587
|
function findTypeDeclaration(checker, startNode, maxWalkUp = 4) {
|
|
551
588
|
const tryResolve = (n) => {
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
589
|
+
let type;
|
|
590
|
+
try {
|
|
591
|
+
type = checker.getTypeAtLocation(n);
|
|
592
|
+
} catch {
|
|
593
|
+
return void 0;
|
|
594
|
+
}
|
|
595
|
+
let symbol;
|
|
596
|
+
let declarations;
|
|
597
|
+
try {
|
|
598
|
+
symbol = type.getSymbol() ?? type.aliasSymbol;
|
|
599
|
+
declarations = symbol?.getDeclarations();
|
|
600
|
+
} catch {
|
|
601
|
+
return void 0;
|
|
602
|
+
}
|
|
555
603
|
if (!declarations || declarations.length === 0) return void 0;
|
|
556
604
|
const nonLib = declarations.find((d) => !isLibFile(d.getSourceFile().fileName));
|
|
557
605
|
if (!nonLib) return void 0;
|
|
@@ -784,10 +832,79 @@ function applyEditBlocks(opts) {
|
|
|
784
832
|
}
|
|
785
833
|
|
|
786
834
|
// src/mendAgent.ts
|
|
787
|
-
import * as
|
|
788
|
-
import * as
|
|
835
|
+
import * as fs6 from "node:fs";
|
|
836
|
+
import * as path6 from "node:path";
|
|
789
837
|
import { generateText } from "ai";
|
|
790
838
|
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
839
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
840
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
841
|
+
|
|
842
|
+
// src/libraryMigrations.ts
|
|
843
|
+
import * as fs5 from "node:fs";
|
|
844
|
+
import * as path5 from "node:path";
|
|
845
|
+
var BUILT_IN_LIBRARY_MIGRATIONS = [
|
|
846
|
+
{
|
|
847
|
+
match: { name: "vite-plugin-svgr", minMajor: 4 },
|
|
848
|
+
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."
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
match: { name: "next", minMajor: 15 },
|
|
852
|
+
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."
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
match: { name: "ai", minMajor: 3, maxMajor: 4 },
|
|
856
|
+
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."
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
match: { name: "drizzle-orm" },
|
|
860
|
+
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."
|
|
861
|
+
}
|
|
862
|
+
];
|
|
863
|
+
function parseMajor(spec) {
|
|
864
|
+
const m = spec.match(/(\d+)(?:\.\d+)*/);
|
|
865
|
+
return m ? parseInt(m[1], 10) : null;
|
|
866
|
+
}
|
|
867
|
+
function detectLibraryMigrations(workspaceRoot, registry = BUILT_IN_LIBRARY_MIGRATIONS) {
|
|
868
|
+
let pkg;
|
|
869
|
+
try {
|
|
870
|
+
const pkgPath = path5.join(workspaceRoot, "package.json");
|
|
871
|
+
if (!fs5.existsSync(pkgPath)) return [];
|
|
872
|
+
pkg = JSON.parse(fs5.readFileSync(pkgPath, "utf-8"));
|
|
873
|
+
} catch {
|
|
874
|
+
return [];
|
|
875
|
+
}
|
|
876
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
877
|
+
const hints = [];
|
|
878
|
+
for (const entry of registry) {
|
|
879
|
+
const { match, hint } = entry;
|
|
880
|
+
const versionSpec = allDeps[match.name];
|
|
881
|
+
if (!versionSpec) continue;
|
|
882
|
+
const major = parseMajor(versionSpec);
|
|
883
|
+
if (match.minMajor != null && (major == null || major < match.minMajor)) continue;
|
|
884
|
+
if (match.maxMajor != null && (major == null || major > match.maxMajor)) continue;
|
|
885
|
+
hints.push({ name: `${match.name}@${versionSpec}`, hint });
|
|
886
|
+
}
|
|
887
|
+
return hints;
|
|
888
|
+
}
|
|
889
|
+
function formatLibraryMigrationsBlock(hints) {
|
|
890
|
+
if (hints.length === 0) return "";
|
|
891
|
+
const lines = ["### library-migrations", ""];
|
|
892
|
+
lines.push(
|
|
893
|
+
"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."
|
|
894
|
+
);
|
|
895
|
+
lines.push("");
|
|
896
|
+
for (const h of hints) {
|
|
897
|
+
lines.push(`- [${h.name}] ${h.hint}`);
|
|
898
|
+
}
|
|
899
|
+
return lines.join("\n");
|
|
900
|
+
}
|
|
901
|
+
function formatLibraryMigrationsTaskDescription(hints) {
|
|
902
|
+
if (hints.length === 0) return void 0;
|
|
903
|
+
const names = hints.map((h) => h.name).join(", ");
|
|
904
|
+
return `Library migration: ${names}`;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/mendAgent.ts
|
|
791
908
|
var SYSTEM_INSTRUCTIONS = `You are a TypeScript code-repair tool. You receive a TypeScript file with one or more compiler errors and resolve them.
|
|
792
909
|
|
|
793
910
|
Output ONLY SEARCH/REPLACE blocks. No prose, no explanations, no XML wrappers.
|
|
@@ -808,16 +925,43 @@ Rules:
|
|
|
808
925
|
- REPLACE must be valid TypeScript that resolves the diagnostic.
|
|
809
926
|
- Do not invent imports, types, properties, or values. Use only what the type-context section shows.
|
|
810
927
|
- One SEARCH/REPLACE block per logical change.
|
|
811
|
-
- If you cannot resolve a diagnostic with the information given, omit a block for it
|
|
928
|
+
- If you cannot resolve a diagnostic with the information given, omit a block for it.
|
|
929
|
+
|
|
930
|
+
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:
|
|
931
|
+
|
|
932
|
+
1. Type-assertion escape-hatches that hide the error rather than fix it:
|
|
933
|
+
- \`x as any\` / \`x as unknown as T\` to dodge a real mismatch.
|
|
934
|
+
- \`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.
|
|
935
|
+
- \`!\` 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.
|
|
936
|
+
|
|
937
|
+
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.
|
|
938
|
+
|
|
939
|
+
3. SQL / NoSQL / shell injection patterns:
|
|
940
|
+
- 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).
|
|
941
|
+
- Never use template literals to interpolate user input into a raw SQL string unless the literal is itself a parameterizing tagged template.
|
|
942
|
+
|
|
943
|
+
4. React XSS escape-hatches:
|
|
944
|
+
- \`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.
|
|
945
|
+
- Setting \`innerHTML\` directly on a DOM element from user input.
|
|
946
|
+
|
|
947
|
+
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.
|
|
948
|
+
|
|
949
|
+
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:
|
|
950
|
+
|
|
951
|
+
- 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.
|
|
952
|
+
- 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.
|
|
953
|
+
- 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.
|
|
954
|
+
|
|
955
|
+
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.`;
|
|
812
956
|
function workspaceRelative(workspaceRoot, p) {
|
|
813
|
-
return
|
|
957
|
+
return path6.isAbsolute(p) ? path6.relative(workspaceRoot, p) : p;
|
|
814
958
|
}
|
|
815
959
|
function buildSystemBlock(context, erroredFile) {
|
|
816
960
|
const wsRel = workspaceRelative(context.workspaceRoot, erroredFile);
|
|
817
|
-
const absPath =
|
|
961
|
+
const absPath = path6.isAbsolute(erroredFile) ? erroredFile : path6.join(context.workspaceRoot, erroredFile);
|
|
818
962
|
let fileContent;
|
|
819
963
|
try {
|
|
820
|
-
fileContent =
|
|
964
|
+
fileContent = fs6.readFileSync(absPath, "utf-8");
|
|
821
965
|
} catch {
|
|
822
966
|
fileContent = "(file unreadable)";
|
|
823
967
|
}
|
|
@@ -827,10 +971,15 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
827
971
|
const typeContexts = [];
|
|
828
972
|
const seen = /* @__PURE__ */ new Set();
|
|
829
973
|
for (const diag of fileDiags) {
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
974
|
+
let ctx;
|
|
975
|
+
try {
|
|
976
|
+
ctx = getTypeContext({
|
|
977
|
+
workspaceRoot: context.workspaceRoot,
|
|
978
|
+
diagnostic: diag
|
|
979
|
+
});
|
|
980
|
+
} catch {
|
|
981
|
+
continue;
|
|
982
|
+
}
|
|
834
983
|
if (!ctx.typeDeclaration) continue;
|
|
835
984
|
const key = `${ctx.typeDeclaration.file}:${ctx.typeDeclaration.symbol}`;
|
|
836
985
|
if (seen.has(key)) continue;
|
|
@@ -841,22 +990,21 @@ function buildSystemBlock(context, erroredFile) {
|
|
|
841
990
|
` + ctx.typeDeclaration.lines
|
|
842
991
|
);
|
|
843
992
|
}
|
|
844
|
-
const parts = [
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
"```"
|
|
851
|
-
];
|
|
993
|
+
const parts = [SYSTEM_INSTRUCTIONS, ""];
|
|
994
|
+
const libMigrations = context.libraryMigrations ?? [];
|
|
995
|
+
if (libMigrations.length > 0) {
|
|
996
|
+
parts.push(formatLibraryMigrationsBlock(libMigrations), "");
|
|
997
|
+
}
|
|
998
|
+
parts.push(`### file: ${wsRel}`, "```ts", fileContent.replace(/\n$/, ""), "```");
|
|
852
999
|
if (typeContexts.length > 0) {
|
|
853
1000
|
parts.push("", "### type-context");
|
|
854
1001
|
for (const tc of typeContexts) {
|
|
855
1002
|
parts.push("```ts", tc, "```");
|
|
856
1003
|
}
|
|
857
1004
|
}
|
|
858
|
-
|
|
859
|
-
|
|
1005
|
+
const taskHeadline = formatLibraryMigrationsTaskDescription(libMigrations) ?? context.taskDescription;
|
|
1006
|
+
if (taskHeadline) {
|
|
1007
|
+
parts.push("", `### task`, taskHeadline);
|
|
860
1008
|
}
|
|
861
1009
|
return parts.join("\n");
|
|
862
1010
|
}
|
|
@@ -873,10 +1021,24 @@ ${lines.join("\n")}
|
|
|
873
1021
|
|
|
874
1022
|
Emit SEARCH/REPLACE blocks to resolve.`;
|
|
875
1023
|
}
|
|
876
|
-
|
|
877
|
-
|
|
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);
|
|
878
1040
|
const result = await generateText({
|
|
879
|
-
model:
|
|
1041
|
+
model: llmModel,
|
|
880
1042
|
system: systemBlock,
|
|
881
1043
|
messages: [{ role: "user", content: userBlock }]
|
|
882
1044
|
});
|
|
@@ -898,6 +1060,7 @@ async function mendSingleFile(opts) {
|
|
|
898
1060
|
const llmResult = await _callLLM({
|
|
899
1061
|
systemBlock,
|
|
900
1062
|
userBlock,
|
|
1063
|
+
provider: llm.provider,
|
|
901
1064
|
model: llm.model,
|
|
902
1065
|
apiKey: llm.apiKey
|
|
903
1066
|
});
|
|
@@ -920,8 +1083,8 @@ async function mendSingleFile(opts) {
|
|
|
920
1083
|
}
|
|
921
1084
|
|
|
922
1085
|
// src/stubAndContinue.ts
|
|
923
|
-
import * as
|
|
924
|
-
import * as
|
|
1086
|
+
import * as fs7 from "node:fs";
|
|
1087
|
+
import * as path7 from "node:path";
|
|
925
1088
|
var noopLogger = { info: () => {
|
|
926
1089
|
}, warn: () => {
|
|
927
1090
|
}, error: () => {
|
|
@@ -938,17 +1101,17 @@ function groupByLine(diagnostics) {
|
|
|
938
1101
|
return groups;
|
|
939
1102
|
}
|
|
940
1103
|
function resolveFile(diagnosticFile, workspaceRoot) {
|
|
941
|
-
return
|
|
1104
|
+
return path7.isAbsolute(diagnosticFile) ? diagnosticFile : path7.resolve(workspaceRoot, diagnosticFile);
|
|
942
1105
|
}
|
|
943
1106
|
function shouldSkipFile(file, workspaceRoot) {
|
|
944
|
-
const rel =
|
|
945
|
-
if (rel.startsWith("node_modules") || rel.includes(`${
|
|
1107
|
+
const rel = path7.relative(workspaceRoot, file);
|
|
1108
|
+
if (rel.startsWith("node_modules") || rel.includes(`${path7.sep}node_modules${path7.sep}`)) {
|
|
946
1109
|
return "node_modules";
|
|
947
1110
|
}
|
|
948
1111
|
if (file.endsWith(".d.ts")) {
|
|
949
1112
|
return "declaration_file";
|
|
950
1113
|
}
|
|
951
|
-
if (!
|
|
1114
|
+
if (!fs7.existsSync(file)) {
|
|
952
1115
|
return "file_not_found";
|
|
953
1116
|
}
|
|
954
1117
|
return null;
|
|
@@ -1007,7 +1170,7 @@ function stubAndContinue(opts) {
|
|
|
1007
1170
|
}
|
|
1008
1171
|
continue;
|
|
1009
1172
|
}
|
|
1010
|
-
const source =
|
|
1173
|
+
const source = fs7.readFileSync(file, "utf-8");
|
|
1011
1174
|
const eol = source.includes("\r\n") ? "\r\n" : "\n";
|
|
1012
1175
|
const lines = source.split(/\r?\n/);
|
|
1013
1176
|
entries.sort((a, b) => b.line - a.line);
|
|
@@ -1049,10 +1212,10 @@ function stubAndContinue(opts) {
|
|
|
1049
1212
|
if (edited) {
|
|
1050
1213
|
filesEditedSet.add(file);
|
|
1051
1214
|
if (!dryRun) {
|
|
1052
|
-
|
|
1053
|
-
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${
|
|
1215
|
+
fs7.writeFileSync(file, lines.join(eol), "utf-8");
|
|
1216
|
+
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1054
1217
|
} else {
|
|
1055
|
-
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${
|
|
1218
|
+
logger.info(`[stub-and-continue] (dry-run) would stub ${entries.length} site(s) in ${path7.relative(workspaceRoot, file)}`);
|
|
1056
1219
|
}
|
|
1057
1220
|
}
|
|
1058
1221
|
}
|
|
@@ -1102,9 +1265,37 @@ function refreshDiagnostics(workspaceRoot, files) {
|
|
|
1102
1265
|
});
|
|
1103
1266
|
return result.diagnostics.filter((d) => d.category === "error");
|
|
1104
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
|
+
}
|
|
1105
1287
|
async function runMendLoop(opts) {
|
|
1106
|
-
const {
|
|
1288
|
+
const {
|
|
1289
|
+
context: rawContext,
|
|
1290
|
+
llm,
|
|
1291
|
+
maxIterations = 3,
|
|
1292
|
+
dryRun = false,
|
|
1293
|
+
stubOnFailure = false,
|
|
1294
|
+
onLayerEvent,
|
|
1295
|
+
_callLLM
|
|
1296
|
+
} = opts;
|
|
1107
1297
|
const startMs = Date.now();
|
|
1298
|
+
const context = rawContext.libraryMigrations === void 0 ? { ...rawContext, libraryMigrations: detectLibraryMigrations(rawContext.workspaceRoot) } : rawContext;
|
|
1108
1299
|
const diagnosticsBefore = context.diagnostics.filter((d) => d.category === "error");
|
|
1109
1300
|
if (diagnosticsBefore.length === 0) {
|
|
1110
1301
|
return {
|
|
@@ -1153,6 +1344,13 @@ async function runMendLoop(opts) {
|
|
|
1153
1344
|
latencyMs: mend.latencyMs,
|
|
1154
1345
|
rawResponse: mend.rawResponse
|
|
1155
1346
|
});
|
|
1347
|
+
onLayerEvent?.({
|
|
1348
|
+
layer: 2,
|
|
1349
|
+
errorCode: dominantErrorCode(currentDiags),
|
|
1350
|
+
fixed: newDiags.length === 0,
|
|
1351
|
+
latencyMs: mend.latencyMs,
|
|
1352
|
+
ts: Date.now()
|
|
1353
|
+
});
|
|
1156
1354
|
if (dryRun) {
|
|
1157
1355
|
currentDiags = newDiags;
|
|
1158
1356
|
stopReason = "maxIterations";
|
|
@@ -1183,6 +1381,20 @@ async function runMendLoop(opts) {
|
|
|
1183
1381
|
diagnostics: currentDiags
|
|
1184
1382
|
});
|
|
1185
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
|
+
}
|
|
1186
1398
|
const postStubDiags = refreshDiagnostics(context.workspaceRoot, filesInScope);
|
|
1187
1399
|
if (postStubDiags.length === 0) {
|
|
1188
1400
|
stopReason = "stubbed";
|
|
@@ -1217,7 +1429,7 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1217
1429
|
const walk = (dir) => {
|
|
1218
1430
|
let entries;
|
|
1219
1431
|
try {
|
|
1220
|
-
entries =
|
|
1432
|
+
entries = fs8.readdirSync(dir, { withFileTypes: true });
|
|
1221
1433
|
} catch {
|
|
1222
1434
|
return;
|
|
1223
1435
|
}
|
|
@@ -1226,10 +1438,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1226
1438
|
if (skip.has(e.name)) {
|
|
1227
1439
|
continue;
|
|
1228
1440
|
}
|
|
1229
|
-
walk(
|
|
1441
|
+
walk(path8.join(dir, e.name));
|
|
1230
1442
|
} else if (e.isFile() && !e.name.endsWith(".d.ts")) {
|
|
1231
1443
|
if (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) {
|
|
1232
|
-
out.push(
|
|
1444
|
+
out.push(path8.relative(workspaceRoot, path8.join(dir, e.name)));
|
|
1233
1445
|
}
|
|
1234
1446
|
}
|
|
1235
1447
|
}
|
|
@@ -1240,10 +1452,10 @@ function discoverTsFiles(workspaceRoot) {
|
|
|
1240
1452
|
function runValidationLoop(opts) {
|
|
1241
1453
|
const { workspaceRoot, skipLSPFixer = false, dryRun = false } = opts;
|
|
1242
1454
|
const logger = opts.logger ?? noopLogger3;
|
|
1243
|
-
if (!
|
|
1455
|
+
if (!fs8.existsSync(workspaceRoot)) {
|
|
1244
1456
|
throw new Error(`workspace not found: ${workspaceRoot}`);
|
|
1245
1457
|
}
|
|
1246
|
-
if (!
|
|
1458
|
+
if (!fs8.existsSync(path8.join(workspaceRoot, "tsconfig.json"))) {
|
|
1247
1459
|
throw new Error(`no tsconfig.json in ${workspaceRoot}`);
|
|
1248
1460
|
}
|
|
1249
1461
|
const targetFiles = opts.targetFiles ?? discoverTsFiles(workspaceRoot);
|
|
@@ -1259,7 +1471,13 @@ function runValidationLoop(opts) {
|
|
|
1259
1471
|
iterations: 0
|
|
1260
1472
|
};
|
|
1261
1473
|
if (errorsBefore > 0 && !skipLSPFixer) {
|
|
1262
|
-
const lsp = runLSPFixerPass({
|
|
1474
|
+
const lsp = runLSPFixerPass({
|
|
1475
|
+
workspaceRoot,
|
|
1476
|
+
targetFiles,
|
|
1477
|
+
logger,
|
|
1478
|
+
dryRun,
|
|
1479
|
+
onLayerEvent: opts.onLayerEvent
|
|
1480
|
+
});
|
|
1263
1481
|
lspFixer = {
|
|
1264
1482
|
ran: true,
|
|
1265
1483
|
fixesApplied: lsp.fixesApplied,
|
|
@@ -1292,16 +1510,59 @@ function runValidationLoop(opts) {
|
|
|
1292
1510
|
}
|
|
1293
1511
|
|
|
1294
1512
|
// cli/run-stack.ts
|
|
1295
|
-
var
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
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
|
+
}
|
|
1299
1547
|
};
|
|
1300
|
-
function estimateCostUsd(model, inputTokens, outputTokens) {
|
|
1301
|
-
const p =
|
|
1548
|
+
function estimateCostUsd(provider, model, inputTokens, outputTokens) {
|
|
1549
|
+
const p = PRICING[provider]?.[model];
|
|
1302
1550
|
if (!p) return 0;
|
|
1303
1551
|
return (inputTokens * p.input + outputTokens * p.output) / 1e6;
|
|
1304
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
|
+
}
|
|
1305
1566
|
function parseArgs(argv) {
|
|
1306
1567
|
const args = {
|
|
1307
1568
|
workspace: "",
|
|
@@ -1311,9 +1572,14 @@ function parseArgs(argv) {
|
|
|
1311
1572
|
files: void 0,
|
|
1312
1573
|
verbose: false,
|
|
1313
1574
|
llm: false,
|
|
1314
|
-
|
|
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: "",
|
|
1315
1580
|
llmMaxIterations: 3,
|
|
1316
|
-
llmBudgetUsd: void 0
|
|
1581
|
+
llmBudgetUsd: void 0,
|
|
1582
|
+
noLibraryHints: false
|
|
1317
1583
|
};
|
|
1318
1584
|
for (let i = 0; i < argv.length; i++) {
|
|
1319
1585
|
const a = argv[i];
|
|
@@ -1331,6 +1597,13 @@ function parseArgs(argv) {
|
|
|
1331
1597
|
args.verbose = true;
|
|
1332
1598
|
} else if (a === "--llm") {
|
|
1333
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;
|
|
1334
1607
|
} else if (a === "--llm-model") {
|
|
1335
1608
|
args.llmModel = argv[++i] ?? args.llmModel;
|
|
1336
1609
|
} else if (a === "--llm-max-iterations") {
|
|
@@ -1347,6 +1620,8 @@ function parseArgs(argv) {
|
|
|
1347
1620
|
process.exit(2);
|
|
1348
1621
|
}
|
|
1349
1622
|
args.llmBudgetUsd = v;
|
|
1623
|
+
} else if (a === "--no-library-hints") {
|
|
1624
|
+
args.noLibraryHints = true;
|
|
1350
1625
|
} else if (a === "--help" || a === "-h") {
|
|
1351
1626
|
printHelp();
|
|
1352
1627
|
process.exit(0);
|
|
@@ -1357,6 +1632,9 @@ function parseArgs(argv) {
|
|
|
1357
1632
|
printHelp();
|
|
1358
1633
|
process.exit(2);
|
|
1359
1634
|
}
|
|
1635
|
+
if (args.llmModel === "") {
|
|
1636
|
+
args.llmModel = DEFAULT_MODEL_BY_PROVIDER[args.llmProvider];
|
|
1637
|
+
}
|
|
1360
1638
|
return args;
|
|
1361
1639
|
}
|
|
1362
1640
|
function printHelp() {
|
|
@@ -1372,16 +1650,36 @@ Layer 0/1 (default \u2014 deterministic, no network):
|
|
|
1372
1650
|
--verbose, -v Stream layer logs to stderr
|
|
1373
1651
|
--help, -h Show this help
|
|
1374
1652
|
|
|
1375
|
-
Layer 2 (opt-in \u2014 single-file LLM mend
|
|
1653
|
+
Layer 2 (opt-in \u2014 single-file LLM mend):
|
|
1376
1654
|
--llm Enable Layer 2 on errors that survive Layer 0/1
|
|
1377
|
-
--llm-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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).
|
|
1381
1670
|
--llm-max-iterations <N> Cap on LLM retries (default: 3)
|
|
1382
1671
|
--llm-budget-usd <amount> Soft cost cap. Exits with code 3 if exceeded.
|
|
1672
|
+
--no-library-hints Disable auto-detection of library breaking-change
|
|
1673
|
+
hints (vite-plugin-svgr v4 ?react migration,
|
|
1674
|
+
Next.js 15 async params, etc.). When workspace
|
|
1675
|
+
package.json contains a known migration target,
|
|
1676
|
+
tsfix injects hints into Layer 2's prompt + skips
|
|
1677
|
+
Layer 0/1 (whose quick-fix would conflict).
|
|
1383
1678
|
|
|
1384
|
-
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
|
|
1385
1683
|
|
|
1386
1684
|
Exit codes:
|
|
1387
1685
|
0 no errors after stack
|
|
@@ -1432,7 +1730,7 @@ TSC Defense Stack \u2014 ${r.workspace}${r.dryRun ? " (dry-run)" : ""}
|
|
|
1432
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" : ""}
|
|
1433
1731
|
`
|
|
1434
1732
|
);
|
|
1435
|
-
w.write(`
|
|
1733
|
+
w.write(` ${l2.provider}/${l2.model} \xB7 stopReason=${l2.stopReason}
|
|
1436
1734
|
`);
|
|
1437
1735
|
}
|
|
1438
1736
|
w.write(` errors after: ${r.errorsAfter}
|
|
@@ -1454,12 +1752,12 @@ TSC Defense Stack \u2014 ${r.workspace}${r.dryRun ? " (dry-run)" : ""}
|
|
|
1454
1752
|
}
|
|
1455
1753
|
async function main() {
|
|
1456
1754
|
const args = parseArgs(process.argv.slice(2));
|
|
1457
|
-
const workspaceRoot =
|
|
1458
|
-
if (!
|
|
1755
|
+
const workspaceRoot = path9.resolve(args.workspace);
|
|
1756
|
+
if (!fs9.existsSync(workspaceRoot)) {
|
|
1459
1757
|
console.error(`error: workspace not found: ${workspaceRoot}`);
|
|
1460
1758
|
return 2;
|
|
1461
1759
|
}
|
|
1462
|
-
if (!
|
|
1760
|
+
if (!fs9.existsSync(path9.join(workspaceRoot, "tsconfig.json"))) {
|
|
1463
1761
|
console.error(`error: no tsconfig.json in ${workspaceRoot}`);
|
|
1464
1762
|
return 2;
|
|
1465
1763
|
}
|
|
@@ -1470,15 +1768,23 @@ async function main() {
|
|
|
1470
1768
|
console.error("error: no .ts/.tsx files found in workspace");
|
|
1471
1769
|
return 2;
|
|
1472
1770
|
}
|
|
1771
|
+
const libraryMigrations = args.noLibraryHints ? [] : detectLibraryMigrations(workspaceRoot);
|
|
1772
|
+
const migrationApplies = args.llm && libraryMigrations.length > 0;
|
|
1773
|
+
if (migrationApplies && !args.noLsp) {
|
|
1774
|
+
logger.info(
|
|
1775
|
+
`Library migrations detected (${libraryMigrations.map((h) => h.name).join(", ")}) \u2014 skipping Layer 0/1 to let Layer 2 apply the migration target. Use --no-library-hints to disable.`
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
const effectiveNoLsp = args.noLsp || migrationApplies;
|
|
1473
1779
|
const loop = runValidationLoop({
|
|
1474
1780
|
workspaceRoot,
|
|
1475
1781
|
targetFiles,
|
|
1476
|
-
skipLSPFixer:
|
|
1782
|
+
skipLSPFixer: effectiveNoLsp,
|
|
1477
1783
|
dryRun: args.dryRun,
|
|
1478
1784
|
logger
|
|
1479
1785
|
});
|
|
1480
1786
|
const report = {
|
|
1481
|
-
workspace:
|
|
1787
|
+
workspace: path9.relative(process.cwd(), workspaceRoot) || workspaceRoot,
|
|
1482
1788
|
errorsBefore: loop.errorsBefore,
|
|
1483
1789
|
lspFixer: args.noLsp ? { ran: false, fixesApplied: 0, filesEdited: [], iterations: 0 } : loop.lspFixer,
|
|
1484
1790
|
layer2: null,
|
|
@@ -1495,30 +1801,36 @@ async function main() {
|
|
|
1495
1801
|
console.error("error: --llm and --dry-run are mutually exclusive (Layer 2 writes patches to disk)");
|
|
1496
1802
|
return 2;
|
|
1497
1803
|
}
|
|
1498
|
-
const
|
|
1804
|
+
const envKeyName = ENV_KEY_BY_PROVIDER[args.llmProvider];
|
|
1805
|
+
const apiKey = process.env[envKeyName];
|
|
1499
1806
|
if (!apiKey) {
|
|
1500
|
-
console.error(
|
|
1807
|
+
console.error(`error: --llm with provider '${args.llmProvider}' requires ${envKeyName} in the environment`);
|
|
1501
1808
|
return 2;
|
|
1502
1809
|
}
|
|
1503
|
-
if (!
|
|
1810
|
+
if (!PRICING[args.llmProvider]?.[args.llmModel]) {
|
|
1504
1811
|
logger.warn(
|
|
1505
|
-
`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`
|
|
1506
1813
|
);
|
|
1507
1814
|
}
|
|
1508
1815
|
const errorDiags = loop.diagnostics.filter((d) => d.category === "error");
|
|
1509
1816
|
const context = {
|
|
1510
1817
|
workspaceRoot,
|
|
1511
1818
|
diagnostics: errorDiags,
|
|
1512
|
-
erroredFiles: Array.from(new Set(errorDiags.map((d) => d.file)))
|
|
1819
|
+
erroredFiles: Array.from(new Set(errorDiags.map((d) => d.file))),
|
|
1820
|
+
// Explicitly pass migrations so `runMendLoop` doesn't re-detect.
|
|
1821
|
+
// `[]` is meaningful — it means "we know there are none" — vs
|
|
1822
|
+
// `undefined` which would trigger auto-detect.
|
|
1823
|
+
libraryMigrations
|
|
1513
1824
|
};
|
|
1514
1825
|
const layer2Start = Date.now();
|
|
1515
1826
|
const mend = await runMendLoop({
|
|
1516
1827
|
context,
|
|
1517
|
-
llm: { provider:
|
|
1828
|
+
llm: { provider: args.llmProvider, model: args.llmModel, apiKey },
|
|
1518
1829
|
maxIterations: args.llmMaxIterations
|
|
1519
1830
|
});
|
|
1520
1831
|
void layer2Start;
|
|
1521
1832
|
const totalCostUsd = estimateCostUsd(
|
|
1833
|
+
args.llmProvider,
|
|
1522
1834
|
args.llmModel,
|
|
1523
1835
|
mend.totalInputTokens,
|
|
1524
1836
|
mend.totalOutputTokens
|
|
@@ -1534,6 +1846,7 @@ async function main() {
|
|
|
1534
1846
|
totalOutputTokens: mend.totalOutputTokens,
|
|
1535
1847
|
totalCostUsd,
|
|
1536
1848
|
budgetExceeded,
|
|
1849
|
+
provider: args.llmProvider,
|
|
1537
1850
|
model: args.llmModel
|
|
1538
1851
|
};
|
|
1539
1852
|
const post = runInProcessTsc({
|