@blzsky/variant 0.2.1 → 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 (47) hide show
  1. package/README.md +110 -56
  2. package/dist/{build-3DSBMMI3.js → build-GKWNLU4Q.js} +9 -8
  3. package/dist/{check-SIQJU6Z2.js → check-KO5VY67Z.js} +6 -6
  4. package/dist/{chunk-FA64KWJ2.js → chunk-7NC7AO75.js} +15 -28
  5. package/dist/{chunk-VYWPQKMS.js → chunk-ACTY42RE.js} +20 -0
  6. package/dist/{chunk-ZH2PQUNL.js → chunk-CN25SRKD.js} +6 -6
  7. package/dist/{chunk-43VLELTL.js → chunk-CWF6QXEW.js} +1 -1
  8. package/dist/{chunk-7OPTWU6E.js → chunk-DMDAJPFJ.js} +1 -1
  9. package/dist/{chunk-RC23JQKI.js → chunk-DSQDMFQ6.js} +9 -1
  10. package/dist/chunk-H5C4SE73.js +184 -0
  11. package/dist/chunk-IFFZ46YS.js +575 -0
  12. package/dist/chunk-IHXJ72XA.js +31 -0
  13. package/dist/chunk-KCEGA56S.js +670 -0
  14. package/dist/{chunk-FZEXZBXB.js → chunk-MAK3HG46.js} +41 -9
  15. package/dist/chunk-MT4BOAAV.js +167 -0
  16. package/dist/{chunk-TCISS2AS.js → chunk-NFFXYYM6.js} +120 -25
  17. package/dist/{chunk-26DHEXGS.js → chunk-RJBFCURK.js} +1 -1
  18. package/dist/{chunk-6BXSZPHC.js → chunk-RSIOYQOQ.js} +67 -8
  19. package/dist/chunk-SGP5JX6I.js +377 -0
  20. package/dist/chunk-X3VVGTEV.js +268 -0
  21. package/dist/{chunk-Q7RLUZSN.js → chunk-YII4TAKS.js} +10 -3
  22. package/dist/cli.js +62 -27
  23. package/dist/{diff-HSJBZFMC.js → diff-A3TQWWUJ.js} +5 -5
  24. package/dist/{doctor-B7MHFIYJ.js → doctor-6ONS6LKY.js} +4 -4
  25. package/dist/{env-I7JZRWDN.js → env-XF2US3CB.js} +6 -6
  26. package/dist/git-W6N3WMYQ.js +24 -0
  27. package/dist/impact-DPZIAJR4.js +263 -0
  28. package/dist/index.js +2 -2
  29. package/dist/init-VNK5E4H3.js +777 -0
  30. package/dist/{insight-JQLTGFYB.js → insight-LNYVMCAE.js} +7 -7
  31. package/dist/pr-6CYMM2XU.js +707 -0
  32. package/dist/{run-C6VAQOVM.js → run-44PKMOV5.js} +9 -8
  33. package/dist/{symbol-graph-E5GXKDFZ.js → symbol-graph-OB4AJCLW.js} +3 -3
  34. package/dist/{tsconfig-paths-E3UEDGXZ.js → tsconfig-paths-SV3PULCW.js} +3 -1
  35. package/dist/vitest.cjs +629 -0
  36. package/dist/vitest.d.cts +181 -0
  37. package/dist/vitest.d.ts +181 -0
  38. package/dist/vitest.js +356 -0
  39. package/dist/workspace-PIQXG4ZD.js +65 -0
  40. package/package.json +13 -2
  41. package/dist/chunk-DRMASVYN.js +0 -25
  42. package/dist/chunk-KSHWC2XN.js +0 -78
  43. package/dist/chunk-XTYVVUWW.js +0 -279
  44. package/dist/impact-PRZ6HEP5.js +0 -735
  45. package/dist/init-O2BY3FOY.js +0 -158
  46. package/dist/pr-Y724QAHE.js +0 -165
  47. package/dist/workspace-HRTFGWKN.js +0 -186
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  resolveCommit
3
- } from "./chunk-6BXSZPHC.js";
3
+ } from "./chunk-RSIOYQOQ.js";
4
4
  import {
5
5
  collectExportedSurface,
6
6
  significantText
7
- } from "./chunk-RC23JQKI.js";
7
+ } from "./chunk-DSQDMFQ6.js";
8
8
  import {
9
9
  GitRefError
10
- } from "./chunk-VYWPQKMS.js";
10
+ } from "./chunk-ACTY42RE.js";
11
11
 
12
12
  // src/core/vcs/base-ref.ts
13
13
  async function assertBaseRef(cwd, ref) {
@@ -25,18 +25,22 @@ function createClassifier() {
25
25
  const tsm2 = await import("ts-morph");
26
26
  return {
27
27
  tsm: tsm2,
28
- project: new tsm2.Project({ useInMemoryFileSystem: true })
28
+ project: new tsm2.Project({
29
+ useInMemoryFileSystem: true,
30
+ compilerOptions: { allowJs: true }
31
+ })
29
32
  };
30
33
  })();
31
34
  const { tsm, project } = await host;
32
35
  const id = nextId++;
36
+ const ext = sourceExtension(input.filePath);
33
37
  const beforeFile = project.createSourceFile(
34
- `__before_${id}.ts`,
38
+ `__before_${id}${ext}`,
35
39
  input.before,
36
40
  { overwrite: true }
37
41
  );
38
42
  const afterFile = project.createSourceFile(
39
- `__after_${id}.ts`,
43
+ `__after_${id}${ext}`,
40
44
  input.after,
41
45
  { overwrite: true }
42
46
  );
@@ -76,15 +80,23 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
76
80
  if (!afterApi.symbols.has(name)) removed.push(name);
77
81
  }
78
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
+ );
79
89
  let classification;
80
- if (surfaceChanged) {
90
+ if (surfaceChanged || parseNotes.length > 0) {
81
91
  classification = "breaking";
82
92
  } else if (changed.length > 0 || significantText(input.before, tsm) !== significantText(input.after, tsm)) {
83
93
  classification = "internal";
84
94
  } else {
85
95
  classification = "non-impacting";
86
96
  }
87
- const confidenceNotes = [.../* @__PURE__ */ new Set([...beforeApi.notes, ...afterApi.notes])];
97
+ const confidenceNotes = [
98
+ .../* @__PURE__ */ new Set([...parseNotes, ...beforeApi.notes, ...afterApi.notes])
99
+ ];
88
100
  const confidence = Math.max(
89
101
  0.3,
90
102
  Math.round((1 - confidenceNotes.length * 0.15) * 100) / 100
@@ -94,9 +106,29 @@ function compareSurfaces(input, beforeFile, afterFile, tsm) {
94
106
  classification,
95
107
  exportedSymbols: { added, removed, changed },
96
108
  confidence,
97
- confidenceNotes
109
+ confidenceNotes,
110
+ syntaxErrors: parseNotes.length > 0
98
111
  };
99
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
+ }
100
132
 
101
133
  export {
102
134
  createClassifier,
@@ -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
1
  import {
2
2
  collectExportedSurface
3
- } from "./chunk-RC23JQKI.js";
3
+ } from "./chunk-DSQDMFQ6.js";
4
4
  import {
5
5
  GraphError
6
- } from "./chunk-VYWPQKMS.js";
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
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ConfigError
3
- } from "./chunk-VYWPQKMS.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
  }
@@ -16,11 +17,26 @@ function isOptionLike(ref) {
16
17
  }
17
18
  function toFileList(stdout) {
18
19
  if (stdout === null) return null;
19
- 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)}`;
20
36
  }
21
37
  async function readFileAtRef(cwd, ref, relPath) {
22
38
  if (isOptionLike(ref)) return null;
23
- return git(cwd, ["show", `${ref}:${toPosix(relPath)}`]);
39
+ return git(cwd, ["show", blobAt(ref, relPath)]);
24
40
  }
25
41
  var CAT_FILE_BATCH_SIZE = 256;
26
42
  async function readFilesAtRef(cwd, ref, relPaths) {
@@ -34,7 +50,7 @@ async function readFilesAtRef(cwd, ref, relPaths) {
34
50
  try {
35
51
  const result = await execa("git", ["cat-file", "--batch"], {
36
52
  cwd,
37
- input: `${chunk.map((p) => `${ref}:${toPosix(p)}`).join("\n")}
53
+ input: `${chunk.map((p) => blobAt(ref, p)).join("\n")}
38
54
  `,
39
55
  encoding: "buffer",
40
56
  // The response framing is byte-exact; execa's default of
@@ -97,15 +113,54 @@ async function resolveCommit(cwd, ref) {
97
113
  const sha = stdout?.trim();
98
114
  return sha === void 0 || sha === "" ? null : sha;
99
115
  }
116
+ async function listRepositoryFiles(cwd, patterns) {
117
+ return toFileList(
118
+ await git(cwd, [
119
+ "ls-files",
120
+ "-z",
121
+ "--cached",
122
+ "--others",
123
+ "--exclude-standard",
124
+ "--",
125
+ ...patterns
126
+ ])
127
+ );
128
+ }
100
129
  async function listChangedFiles(cwd, ref) {
101
130
  if (isOptionLike(ref)) return null;
102
- return toFileList(await git(cwd, ["diff", "--name-only", ref, "--"]));
131
+ return listDiff(cwd, ref);
103
132
  }
104
133
  async function listChangedFilesSinceMergeBase(cwd, ref) {
105
134
  if (isOptionLike(ref)) return null;
106
- return toFileList(
107
- await git(cwd, ["diff", "--name-only", `${ref}...HEAD`, "--"])
108
- );
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;
109
164
  }
110
165
 
111
166
  export {
@@ -113,6 +168,10 @@ export {
113
168
  readFilesAtRef,
114
169
  readHeadSha,
115
170
  resolveCommit,
171
+ listRepositoryFiles,
116
172
  listChangedFiles,
117
- listChangedFilesSinceMergeBase
173
+ listChangedFilesSinceMergeBase,
174
+ mergeBase,
175
+ defaultBranchRef,
176
+ repositoryRoot
118
177
  };