@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/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
|
|
479
|
-
import * as
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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
|
|
793
|
-
import * as
|
|
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
|
|
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 =
|
|
966
|
+
const absPath = path6.isAbsolute(erroredFile) ? erroredFile : path6.join(context.workspaceRoot, erroredFile);
|
|
823
967
|
let fileContent;
|
|
824
968
|
try {
|
|
825
|
-
fileContent =
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
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
|
-
|
|
864
|
-
|
|
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
|
-
|
|
882
|
-
|
|
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:
|
|
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
|
|
929
|
-
import * as
|
|
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
|
|
1109
|
+
return path7.isAbsolute(diagnosticFile) ? diagnosticFile : path7.resolve(workspaceRoot, diagnosticFile);
|
|
947
1110
|
}
|
|
948
1111
|
function shouldSkipFile(file, workspaceRoot) {
|
|
949
|
-
const rel =
|
|
950
|
-
if (rel.startsWith("node_modules") || rel.includes(`${
|
|
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 (!
|
|
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 =
|
|
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
|
-
|
|
1058
|
-
logger.info(`[stub-and-continue] stubbed ${entries.length} site(s) in ${
|
|
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 ${
|
|
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 {
|
|
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 =
|
|
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(
|
|
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(
|
|
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 (!
|
|
1460
|
+
if (!fs8.existsSync(workspaceRoot)) {
|
|
1249
1461
|
throw new Error(`workspace not found: ${workspaceRoot}`);
|
|
1250
1462
|
}
|
|
1251
|
-
if (!
|
|
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({
|
|
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,
|