@blzsky/variant 0.2.0 → 0.3.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +241 -136
  2. package/dist/{build-2LO7GD2S.js → build-GKWNLU4Q.js} +11 -10
  3. package/dist/{check-ZTKGGF4M.js → check-KO5VY67Z.js} +9 -9
  4. package/dist/{chunk-4LLK2S4R.js → chunk-7NC7AO75.js} +19 -32
  5. package/dist/{chunk-RVXUCJYA.js → chunk-ACTY42RE.js} +27 -0
  6. package/dist/{chunk-SWPVR2DF.js → chunk-CN25SRKD.js} +9 -9
  7. package/dist/{chunk-LXKLCLL6.js → chunk-CWF6QXEW.js} +1 -1
  8. package/dist/{chunk-MZZOE7L4.js → chunk-DMDAJPFJ.js} +1 -1
  9. package/dist/{chunk-O4LZKXNZ.js → chunk-DSQDMFQ6.js} +59 -2
  10. package/dist/{chunk-V22IPJ7A.js → chunk-EISTWXDW.js} +5 -1
  11. package/dist/chunk-H5C4SE73.js +184 -0
  12. package/dist/chunk-IFFZ46YS.js +575 -0
  13. package/dist/chunk-IHXJ72XA.js +31 -0
  14. package/dist/chunk-KCEGA56S.js +670 -0
  15. package/dist/{chunk-LJDDS4RR.js → chunk-MAK3HG46.js} +54 -8
  16. package/dist/chunk-MT4BOAAV.js +167 -0
  17. package/dist/{chunk-VTJ67TT5.js → chunk-NFFXYYM6.js} +122 -27
  18. package/dist/{chunk-ISRF5F3Q.js → chunk-QRPI6TJE.js} +1 -1
  19. package/dist/{chunk-RGD422OM.js → chunk-RJBFCURK.js} +1 -1
  20. package/dist/{chunk-XUBWCTJZ.js → chunk-RSIOYQOQ.js} +85 -8
  21. package/dist/chunk-SGP5JX6I.js +377 -0
  22. package/dist/chunk-X3VVGTEV.js +268 -0
  23. package/dist/{chunk-NSNZOH5P.js → chunk-YII4TAKS.js} +10 -3
  24. package/dist/cli.js +76 -37
  25. package/dist/{diff-GFI6RAEA.js → diff-A3TQWWUJ.js} +9 -5
  26. package/dist/{doctor-GH4RZVEH.js → doctor-6ONS6LKY.js} +5 -5
  27. package/dist/{env-QORDD6FQ.js → env-XF2US3CB.js} +8 -8
  28. package/dist/git-W6N3WMYQ.js +24 -0
  29. package/dist/impact-DPZIAJR4.js +263 -0
  30. package/dist/index.js +2 -2
  31. package/dist/init-VNK5E4H3.js +777 -0
  32. package/dist/{insight-DZ5NQY6D.js → insight-LNYVMCAE.js} +9 -9
  33. package/dist/pr-6CYMM2XU.js +707 -0
  34. package/dist/{run-6PNH4QRA.js → run-44PKMOV5.js} +11 -10
  35. package/dist/{symbol-graph-YYUEMZGR.js → symbol-graph-OB4AJCLW.js} +3 -3
  36. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  37. package/dist/vitest.cjs +629 -0
  38. package/dist/vitest.d.cts +181 -0
  39. package/dist/vitest.d.ts +181 -0
  40. package/dist/vitest.js +356 -0
  41. package/dist/workspace-PIQXG4ZD.js +65 -0
  42. package/package.json +15 -4
  43. package/dist/chunk-DRMASVYN.js +0 -25
  44. package/dist/chunk-KSHWC2XN.js +0 -78
  45. package/dist/chunk-XTYVVUWW.js +0 -279
  46. package/dist/impact-AF453PLK.js +0 -702
  47. package/dist/init-KJIUNGAN.js +0 -158
  48. package/dist/pr-UFZRZX6S.js +0 -152
  49. package/dist/workspace-X5FUBTY3.js +0 -186
@@ -1,7 +1,20 @@
1
+ import {
2
+ resolveCommit
3
+ } from "./chunk-RSIOYQOQ.js";
1
4
  import {
2
5
  collectExportedSurface,
3
6
  significantText
4
- } from "./chunk-O4LZKXNZ.js";
7
+ } from "./chunk-DSQDMFQ6.js";
8
+ import {
9
+ GitRefError
10
+ } from "./chunk-ACTY42RE.js";
11
+
12
+ // src/core/vcs/base-ref.ts
13
+ async function assertBaseRef(cwd, ref) {
14
+ if (await resolveCommit(cwd, ref) === null) {
15
+ throw new GitRefError(`"${ref}" does not name a commit in this repository`);
16
+ }
17
+ }
5
18
 
6
19
  // src/core/semantic/differ.ts
7
20
  function createClassifier() {
@@ -12,18 +25,22 @@ function createClassifier() {
12
25
  const tsm2 = await import("ts-morph");
13
26
  return {
14
27
  tsm: tsm2,
15
- project: new tsm2.Project({ useInMemoryFileSystem: true })
28
+ project: new tsm2.Project({
29
+ useInMemoryFileSystem: true,
30
+ compilerOptions: { allowJs: true }
31
+ })
16
32
  };
17
33
  })();
18
34
  const { tsm, project } = await host;
19
35
  const id = nextId++;
36
+ const ext = sourceExtension(input.filePath);
20
37
  const beforeFile = project.createSourceFile(
21
- `__before_${id}.ts`,
38
+ `__before_${id}${ext}`,
22
39
  input.before,
23
40
  { overwrite: true }
24
41
  );
25
42
  const afterFile = project.createSourceFile(
26
- `__after_${id}.ts`,
43
+ `__after_${id}${ext}`,
27
44
  input.after,
28
45
  { overwrite: true }
29
46
  );
@@ -63,15 +80,23 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
63
80
  if (!afterApi.symbols.has(name)) removed.push(name);
64
81
  }
65
82
  const surfaceChanged = added.length > 0 || removed.length > 0 || changed.some((c) => c.kind !== "body");
83
+ const parseNotes = [
84
+ ...hasParseErrors(beforeFile) ? ["base"] : [],
85
+ ...hasParseErrors(afterFile) ? ["working-tree"] : []
86
+ ].map(
87
+ (side) => `the ${side} version does not parse; classified as breaking without trusting the comparison`
88
+ );
66
89
  let classification;
67
- if (surfaceChanged) {
90
+ if (surfaceChanged || parseNotes.length > 0) {
68
91
  classification = "breaking";
69
92
  } else if (changed.length > 0 || significantText(input.before, tsm) !== significantText(input.after, tsm)) {
70
93
  classification = "internal";
71
94
  } else {
72
95
  classification = "non-impacting";
73
96
  }
74
- const confidenceNotes = [.../* @__PURE__ */ new Set([...beforeApi.notes, ...afterApi.notes])];
97
+ const confidenceNotes = [
98
+ .../* @__PURE__ */ new Set([...parseNotes, ...beforeApi.notes, ...afterApi.notes])
99
+ ];
75
100
  const confidence = Math.max(
76
101
  0.3,
77
102
  Math.round((1 - confidenceNotes.length * 0.15) * 100) / 100
@@ -81,11 +106,32 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
81
106
  classification,
82
107
  exportedSymbols: { added, removed, changed },
83
108
  confidence,
84
- confidenceNotes
109
+ confidenceNotes,
110
+ syntaxErrors: parseNotes.length > 0
85
111
  };
86
112
  }
113
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
114
+ ".ts",
115
+ ".tsx",
116
+ ".mts",
117
+ ".cts",
118
+ ".js",
119
+ ".jsx",
120
+ ".mjs",
121
+ ".cjs"
122
+ ]);
123
+ function sourceExtension(filePath) {
124
+ const dot = filePath.lastIndexOf(".");
125
+ const ext = dot === -1 ? "" : filePath.slice(dot);
126
+ return SOURCE_EXTENSIONS.has(ext) ? ext : ".ts";
127
+ }
128
+ function hasParseErrors(file) {
129
+ const { parseDiagnostics } = file.compilerNode;
130
+ return (parseDiagnostics?.length ?? 0) > 0;
131
+ }
87
132
 
88
133
  export {
89
134
  createClassifier,
90
- classifyChange
135
+ classifyChange,
136
+ assertBaseRef
91
137
  };
@@ -0,0 +1,167 @@
1
+ import {
2
+ appendReconciliation,
3
+ defaultHistoryDir,
4
+ readImpactPredictions
5
+ } from "./chunk-H5C4SE73.js";
6
+ import {
7
+ ImpactReportError
8
+ } from "./chunk-ACTY42RE.js";
9
+
10
+ // src/core/impact/verify.ts
11
+ import { realpathSync } from "fs";
12
+ import { readFile } from "fs/promises";
13
+ import path from "path";
14
+ function realpathOrSelf(target) {
15
+ try {
16
+ return realpathSync.native(target);
17
+ } catch {
18
+ return target;
19
+ }
20
+ }
21
+ function escapes(relative) {
22
+ return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
23
+ }
24
+ function toRelativePosix(cwd, testPath, realpath) {
25
+ let relative = testPath;
26
+ if (path.isAbsolute(testPath)) {
27
+ relative = path.relative(cwd, testPath);
28
+ if (escapes(relative)) {
29
+ relative = path.relative(realpath(cwd), realpath(testPath));
30
+ }
31
+ }
32
+ return relative.replace(/\\/g, "/");
33
+ }
34
+ function parseTestRun(cwd, raw, realpath = realpathOrSelf) {
35
+ let parsed;
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ } catch {
39
+ throw new ImpactReportError("it is not valid JSON");
40
+ }
41
+ if (typeof parsed !== "object" || parsed === null) {
42
+ throw new ImpactReportError("it is not a JSON object");
43
+ }
44
+ const results = parsed.testResults;
45
+ if (!Array.isArray(results)) {
46
+ throw new ImpactReportError("it has no `testResults` array");
47
+ }
48
+ const ran = /* @__PURE__ */ new Set();
49
+ const failed = /* @__PURE__ */ new Set();
50
+ for (const entry of results) {
51
+ if (typeof entry !== "object" || entry === null) continue;
52
+ const { name, status } = entry;
53
+ if (typeof name !== "string") continue;
54
+ const file = toRelativePosix(cwd, name, realpath);
55
+ ran.add(file);
56
+ if (status === "failed") failed.add(file);
57
+ }
58
+ return { ran: [...ran].sort(), failed: [...failed].sort() };
59
+ }
60
+ function testRunFrom(cwd, results, realpath = realpathOrSelf) {
61
+ const ran = /* @__PURE__ */ new Set();
62
+ const failed = /* @__PURE__ */ new Set();
63
+ for (const result of results) {
64
+ const file = toRelativePosix(cwd, result.file, realpath);
65
+ ran.add(file);
66
+ if (result.failed) failed.add(file);
67
+ }
68
+ return { ran: [...ran].sort(), failed: [...failed].sort() };
69
+ }
70
+ function reconcile(prediction, failedTests) {
71
+ const selected = new Set(prediction.affectedTests);
72
+ const caught = [];
73
+ const falseSkips = [];
74
+ for (const test of failedTests) {
75
+ if (prediction.selectAll || selected.has(test)) caught.push(test);
76
+ else falseSkips.push(test);
77
+ }
78
+ return {
79
+ caught,
80
+ falseSkips,
81
+ falseSkipRate: failedTests.length === 0 ? 0 : falseSkips.length / failedTests.length
82
+ };
83
+ }
84
+ function selectPrediction(predictions, headSha) {
85
+ if (headSha !== void 0) {
86
+ for (let i = predictions.length - 1; i >= 0; i--) {
87
+ const candidate = predictions[i];
88
+ if (candidate?.headSha === headSha) {
89
+ return { prediction: candidate, matchedBy: "head-sha" };
90
+ }
91
+ }
92
+ return null;
93
+ }
94
+ const latest = predictions[predictions.length - 1];
95
+ return latest === void 0 ? null : { prediction: latest, matchedBy: "most-recent" };
96
+ }
97
+ async function readReport(cwd, reportPath) {
98
+ try {
99
+ return await readFile(path.resolve(cwd, reportPath), "utf8");
100
+ } catch (error) {
101
+ const code = error instanceof Error && "code" in error ? String(error.code) : "";
102
+ throw new ImpactReportError(
103
+ code === "ENOENT" ? `no file at ${reportPath}` : `${reportPath} could not be read${code === "" ? "" : ` (${code})`}`
104
+ );
105
+ }
106
+ }
107
+ async function verifyImpact(cwd, reportPaths, options = {}) {
108
+ const paths = typeof reportPaths === "string" ? [reportPaths] : reportPaths;
109
+ return verifyRun(
110
+ cwd,
111
+ async () => {
112
+ const reports = await Promise.all(
113
+ paths.map((reportPath) => readReport(cwd, reportPath))
114
+ );
115
+ return reports.map((raw) => parseTestRun(cwd, raw));
116
+ },
117
+ options
118
+ );
119
+ }
120
+ async function verifyRun(cwd, runs, options = {}) {
121
+ const historyDir = defaultHistoryDir(cwd);
122
+ const predictions = options.predictions ?? await readImpactPredictions(historyDir);
123
+ const match = selectPrediction(predictions, options.headSha);
124
+ if (match === null) return null;
125
+ const resolved = typeof runs === "function" ? await runs() : Array.isArray(runs) ? runs : [runs];
126
+ const failedTests = [
127
+ ...new Set(resolved.flatMap((run) => run.failed))
128
+ ].sort();
129
+ const ran = new Set(resolved.flatMap((run) => run.ran));
130
+ const selected = new Set(match.prediction.affectedTests);
131
+ const predictedRan = match.prediction.selectAll ? ran.size : [...ran].filter((test) => selected.has(test)).length;
132
+ const { caught, falseSkips, falseSkipRate } = reconcile(
133
+ match.prediction,
134
+ failedTests
135
+ );
136
+ const historyLogged = options.log === false ? true : await appendReconciliation(historyDir, {
137
+ at: (/* @__PURE__ */ new Date()).toISOString(),
138
+ headSha: match.prediction.headSha,
139
+ baseRef: match.prediction.baseRef,
140
+ predictedTests: match.prediction.affectedTests.length,
141
+ totalTests: match.prediction.totalTests,
142
+ failedTests: failedTests.length,
143
+ caught: caught.length,
144
+ falseSkips: falseSkips.length,
145
+ confidence: match.prediction.confidence,
146
+ selectAll: match.prediction.selectAll,
147
+ ranTests: ran.size,
148
+ predictedRan
149
+ });
150
+ return {
151
+ prediction: match.prediction,
152
+ matchedBy: match.matchedBy,
153
+ failedTests,
154
+ caught,
155
+ falseSkips,
156
+ falseSkipRate,
157
+ ranTests: ran.size,
158
+ predictedRan,
159
+ historyLogged
160
+ };
161
+ }
162
+
163
+ export {
164
+ testRunFrom,
165
+ verifyImpact,
166
+ verifyRun
167
+ };
@@ -1,24 +1,52 @@
1
- import {
2
- GraphError
3
- } from "./chunk-RVXUCJYA.js";
4
1
  import {
5
2
  collectExportedSurface
6
- } from "./chunk-O4LZKXNZ.js";
3
+ } from "./chunk-DSQDMFQ6.js";
4
+ import {
5
+ GraphError
6
+ } from "./chunk-ACTY42RE.js";
7
7
 
8
8
  // src/core/semantic/symbol-graph.ts
9
9
  import { createHash } from "crypto";
10
10
  import { mkdir, readFile, writeFile } from "fs/promises";
11
11
  import path from "path";
12
- var SYMBOL_GRAPH_VERSION = 1;
12
+ var SYMBOL_GRAPH_VERSION = 5;
13
13
  var GRAPH_FILENAME = "symbols.json";
14
14
  var DEFAULT_INCLUDE = ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts"];
15
+ var JAVASCRIPT_INCLUDE = ["**/*.js", "**/*.jsx", "**/*.mjs", "**/*.cjs"];
16
+ var JAVASCRIPT_PATHSPECS = ["*.js", "*.jsx", "*.mjs", "*.cjs"];
17
+ var MAX_JAVASCRIPT_BYTES = 512 * 1024;
15
18
  var DEFAULT_IGNORE = [
16
- "node_modules/**",
19
+ "**/node_modules/**",
17
20
  ".git/**",
18
21
  ".variant/**",
19
- "dist/**",
20
- "**/*.d.ts"
22
+ "**/dist/**",
23
+ "**/*.d.ts",
24
+ "**/*.d.mts",
25
+ "**/*.d.cts"
21
26
  ];
27
+ var JAVASCRIPT_FILE = /\.(?:[cm]?js|jsx)$/;
28
+ function isJavaScript(file) {
29
+ return JAVASCRIPT_FILE.test(file);
30
+ }
31
+ async function listJavaScript(cwd, ignore, fg) {
32
+ const { listRepositoryFiles } = await import("./git-W6N3WMYQ.js");
33
+ const tracked = await listRepositoryFiles(cwd, JAVASCRIPT_PATHSPECS);
34
+ if (tracked === null) {
35
+ return fg(JAVASCRIPT_INCLUDE, { cwd, onlyFiles: true, ignore });
36
+ }
37
+ const ignored = ignore.map(globToRegExp);
38
+ return tracked.filter(
39
+ (file) => !/\.min\.[cm]?js$/.test(file) && !ignored.some((pattern) => pattern.test(file))
40
+ );
41
+ }
42
+ function globToRegExp(pattern) {
43
+ const source = pattern.split("**/").map(
44
+ (part) => part.split("**").map(
45
+ (piece) => piece.split("*").map((text) => text.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]*")
46
+ ).join(".*")
47
+ ).join("(?:.*/)?");
48
+ return new RegExp(`^${source}$`);
49
+ }
22
50
  function defaultGraphDir(cwd) {
23
51
  return path.join(cwd, ".variant", "graph");
24
52
  }
@@ -26,11 +54,14 @@ async function buildSymbolGraph(cwd, options = {}) {
26
54
  const fg = (await import("fast-glob")).default;
27
55
  const include = options.include ?? DEFAULT_INCLUDE;
28
56
  for (const p of include) assertSafePattern(p);
29
- const files = (await fg(include, {
30
- cwd,
31
- onlyFiles: true,
32
- ignore: [...DEFAULT_IGNORE, ...options.ignore ?? []]
33
- })).map(toPosix).sort();
57
+ const ignore = [...DEFAULT_IGNORE, ...options.ignore ?? []];
58
+ const [typescript, javascript] = await Promise.all([
59
+ fg(include, { cwd, onlyFiles: true, ignore }),
60
+ options.include === void 0 ? listJavaScript(cwd, ignore, fg) : []
61
+ ]);
62
+ const files = [
63
+ ...new Set([...typescript, ...javascript].map(toPosix))
64
+ ].sort();
34
65
  const previous = options.previous?.version === SYMBOL_GRAPH_VERSION ? options.previous : null;
35
66
  const stats = {
36
67
  scanned: files.length,
@@ -40,6 +71,7 @@ async function buildSymbolGraph(cwd, options = {}) {
40
71
  };
41
72
  const contents = new Array(files.length);
42
73
  const limit = Math.max(1, options.parallel ?? 32);
74
+ const read = options.readFile ?? ((absolutePath) => readFile(absolutePath, "utf8"));
43
75
  let next = 0;
44
76
  const worker = async () => {
45
77
  while (true) {
@@ -47,7 +79,15 @@ async function buildSymbolGraph(cwd, options = {}) {
47
79
  if (i >= files.length) return;
48
80
  const relPath = files[i];
49
81
  if (relPath === void 0) return;
50
- contents[i] = await readFile(path.join(cwd, relPath), "utf8");
82
+ try {
83
+ const content = await read(path.join(cwd, relPath));
84
+ if (isJavaScript(relPath) && content.length > MAX_JAVASCRIPT_BYTES) {
85
+ continue;
86
+ }
87
+ contents[i] = content;
88
+ } catch (err) {
89
+ if (err.code !== "ENOENT") throw err;
90
+ }
51
91
  }
52
92
  };
53
93
  await Promise.all(
@@ -69,12 +109,19 @@ async function buildSymbolGraph(cwd, options = {}) {
69
109
  }
70
110
  if (tsm === void 0 || project === void 0) {
71
111
  tsm = await import("ts-morph");
72
- project = new tsm.Project({ useInMemoryFileSystem: true });
112
+ project = new tsm.Project({
113
+ useInMemoryFileSystem: true,
114
+ compilerOptions: { allowJs: true }
115
+ });
73
116
  }
74
117
  const sourceFile = project.createSourceFile(relPath, content, {
75
118
  overwrite: true
76
119
  });
77
- out[relPath] = indexSourceFile(sourceFile, contentHash, tsm);
120
+ try {
121
+ out[relPath] = indexSourceFile(sourceFile, contentHash, tsm);
122
+ } finally {
123
+ project.removeSourceFile(sourceFile);
124
+ }
78
125
  stats.parsed++;
79
126
  }
80
127
  if (previous !== null) {
@@ -188,24 +235,72 @@ function collectImports(file, tsm) {
188
235
  names: named.length > 0 ? named : ["*"]
189
236
  });
190
237
  }
191
- for (const call of file.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)) {
192
- if (call.getExpression().getKind() !== tsm.SyntaxKind.ImportKeyword) {
238
+ for (const decl of file.getDescendantsOfKind(
239
+ tsm.SyntaxKind.ImportEqualsDeclaration
240
+ )) {
241
+ const reference = decl.getModuleReference();
242
+ if (!tsm.Node.isExternalModuleReference(reference)) continue;
243
+ const expression = reference.getExpression();
244
+ if (expression === void 0 || !tsm.Node.isStringLiteral(expression)) {
193
245
  continue;
194
246
  }
247
+ imports.push({
248
+ module: expression.getLiteralValue(),
249
+ kind: "static",
250
+ typeOnly: decl.isTypeOnly(),
251
+ names: ["*"]
252
+ });
253
+ }
254
+ for (const call of file.getDescendantsOfKind(tsm.SyntaxKind.CallExpression)) {
255
+ const callee = call.getExpression();
256
+ const isImport = callee.getKind() === tsm.SyntaxKind.ImportKeyword;
257
+ const isRequire = tsm.Node.isIdentifier(callee) && callee.getText() === "require";
258
+ if (!isImport && !isRequire) continue;
195
259
  const arg = call.getArguments()[0];
196
- if (arg !== void 0 && tsm.Node.isStringLiteral(arg)) {
197
- imports.push({
198
- module: arg.getLiteralValue(),
199
- kind: "dynamic",
200
- typeOnly: false,
201
- names: []
202
- });
203
- } else {
204
- notes.push("dynamic import() with a non-literal specifier");
260
+ if (arg !== void 0 && (tsm.Node.isStringLiteral(arg) || tsm.Node.isNoSubstitutionTemplateLiteral(arg))) {
261
+ imports.push(
262
+ isImport ? {
263
+ module: arg.getLiteralValue(),
264
+ kind: "dynamic",
265
+ typeOnly: false,
266
+ names: []
267
+ } : {
268
+ module: arg.getLiteralValue(),
269
+ kind: "static",
270
+ typeOnly: false,
271
+ names: ["*"]
272
+ }
273
+ );
274
+ continue;
275
+ }
276
+ const prefix = arg === void 0 ? "" : staticPrefix(arg, tsm);
277
+ imports.push({
278
+ module: prefix,
279
+ kind: "pattern",
280
+ typeOnly: false,
281
+ names: []
282
+ });
283
+ if (prefix === "") {
284
+ notes.push(
285
+ `${isImport ? "dynamic import()" : "require()"} with a non-literal specifier`
286
+ );
205
287
  }
206
288
  }
207
289
  return { imports, notes };
208
290
  }
291
+ function staticPrefix(arg, tsm) {
292
+ if (tsm.Node.isTemplateExpression(arg)) {
293
+ return arg.getHead().getLiteralText();
294
+ }
295
+ if (tsm.Node.isBinaryExpression(arg) && arg.getOperatorToken().getKind() === tsm.SyntaxKind.PlusToken) {
296
+ const left = arg.getLeft();
297
+ if (tsm.Node.isStringLiteral(left) || tsm.Node.isNoSubstitutionTemplateLiteral(left)) {
298
+ return left.getLiteralValue();
299
+ }
300
+ return staticPrefix(left, tsm);
301
+ }
302
+ return "";
303
+ }
209
304
  function sha256(text) {
210
305
  return createHash("sha256").update(text).digest("hex");
211
306
  }
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-6J4XDG44.js";
7
7
  import {
8
8
  getPrinter
9
- } from "./chunk-V22IPJ7A.js";
9
+ } from "./chunk-EISTWXDW.js";
10
10
 
11
11
  // src/cli/render/insight.ts
12
12
  function statusLabel(result) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ConfigError
3
- } from "./chunk-RVXUCJYA.js";
3
+ } from "./chunk-ACTY42RE.js";
4
4
 
5
5
  // src/core/config/loader.ts
6
6
  import { existsSync } from "fs";
@@ -1,4 +1,5 @@
1
1
  // src/core/vcs/git.ts
2
+ import path from "path";
2
3
  function toPosix(relPath) {
3
4
  return relPath.replace(/\\/g, "/");
4
5
  }
@@ -11,12 +12,31 @@ async function git(cwd, args) {
11
12
  return null;
12
13
  }
13
14
  }
15
+ function isOptionLike(ref) {
16
+ return ref.startsWith("-");
17
+ }
14
18
  function toFileList(stdout) {
15
19
  if (stdout === null) return null;
16
- return stdout.split("\n").map((s) => s.trim()).filter((s) => s.length > 0);
20
+ return stdout.split("\0").filter((file) => file.length > 0);
21
+ }
22
+ async function listDiff(cwd, range) {
23
+ const [stdout, prefix] = await Promise.all([
24
+ git(cwd, ["diff", "--name-only", "-z", "--no-renames", range, "--"]),
25
+ git(cwd, ["rev-parse", "--show-prefix"])
26
+ ]);
27
+ const files = toFileList(stdout);
28
+ const cwdFromRoot = prefix?.trim() ?? "";
29
+ if (files === null || cwdFromRoot === "") return files;
30
+ return files.map(
31
+ (file) => path.posix.relative(`/${cwdFromRoot}`, `/${file}`)
32
+ );
33
+ }
34
+ function blobAt(ref, relPath) {
35
+ return `${ref}:./${toPosix(relPath)}`;
17
36
  }
18
37
  async function readFileAtRef(cwd, ref, relPath) {
19
- return git(cwd, ["show", `${ref}:${toPosix(relPath)}`]);
38
+ if (isOptionLike(ref)) return null;
39
+ return git(cwd, ["show", blobAt(ref, relPath)]);
20
40
  }
21
41
  var CAT_FILE_BATCH_SIZE = 256;
22
42
  async function readFilesAtRef(cwd, ref, relPaths) {
@@ -30,7 +50,7 @@ async function readFilesAtRef(cwd, ref, relPaths) {
30
50
  try {
31
51
  const result = await execa("git", ["cat-file", "--batch"], {
32
52
  cwd,
33
- input: `${chunk.map((p) => `${ref}:${toPosix(p)}`).join("\n")}
53
+ input: `${chunk.map((p) => blobAt(ref, p)).join("\n")}
34
54
  `,
35
55
  encoding: "buffer",
36
56
  // The response framing is byte-exact; execa's default of
@@ -82,19 +102,76 @@ async function readHeadSha(cwd) {
82
102
  const sha = stdout?.trim();
83
103
  return sha === void 0 || sha === "" ? null : sha;
84
104
  }
85
- async function listChangedFiles(cwd, ref) {
86
- return toFileList(await git(cwd, ["diff", "--name-only", ref, "--"]));
105
+ async function resolveCommit(cwd, ref) {
106
+ if (isOptionLike(ref)) return null;
107
+ const stdout = await git(cwd, [
108
+ "rev-parse",
109
+ "--verify",
110
+ "--quiet",
111
+ `${ref}^{commit}`
112
+ ]);
113
+ const sha = stdout?.trim();
114
+ return sha === void 0 || sha === "" ? null : sha;
87
115
  }
88
- async function listChangedFilesSinceMergeBase(cwd, ref) {
116
+ async function listRepositoryFiles(cwd, patterns) {
89
117
  return toFileList(
90
- await git(cwd, ["diff", "--name-only", `${ref}...HEAD`, "--"])
118
+ await git(cwd, [
119
+ "ls-files",
120
+ "-z",
121
+ "--cached",
122
+ "--others",
123
+ "--exclude-standard",
124
+ "--",
125
+ ...patterns
126
+ ])
91
127
  );
92
128
  }
129
+ async function listChangedFiles(cwd, ref) {
130
+ if (isOptionLike(ref)) return null;
131
+ return listDiff(cwd, ref);
132
+ }
133
+ async function listChangedFilesSinceMergeBase(cwd, ref) {
134
+ if (isOptionLike(ref)) return null;
135
+ return listDiff(cwd, `${ref}...HEAD`);
136
+ }
137
+ async function mergeBase(cwd, a, b) {
138
+ if (isOptionLike(a) || isOptionLike(b)) return null;
139
+ const sha = (await git(cwd, ["merge-base", a, b]))?.trim();
140
+ return sha === void 0 || sha === "" ? null : sha;
141
+ }
142
+ var DEFAULT_BRANCH_GUESSES = [
143
+ "origin/main",
144
+ "origin/master",
145
+ "main",
146
+ "master"
147
+ ];
148
+ async function defaultBranchRef(cwd) {
149
+ const head = (await git(cwd, [
150
+ "symbolic-ref",
151
+ "--quiet",
152
+ "--short",
153
+ "refs/remotes/origin/HEAD"
154
+ ]))?.trim();
155
+ if (head !== void 0 && head !== "") return head;
156
+ for (const guess of DEFAULT_BRANCH_GUESSES) {
157
+ if (await resolveCommit(cwd, guess) !== null) return guess;
158
+ }
159
+ return null;
160
+ }
161
+ async function repositoryRoot(cwd) {
162
+ const top = (await git(cwd, ["rev-parse", "--show-toplevel"]))?.trim();
163
+ return top === void 0 || top === "" ? null : top;
164
+ }
93
165
 
94
166
  export {
95
167
  readFileAtRef,
96
168
  readFilesAtRef,
97
169
  readHeadSha,
170
+ resolveCommit,
171
+ listRepositoryFiles,
98
172
  listChangedFiles,
99
- listChangedFilesSinceMergeBase
173
+ listChangedFilesSinceMergeBase,
174
+ mergeBase,
175
+ defaultBranchRef,
176
+ repositoryRoot
100
177
  };