@devness/coverit 0.1.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 (147) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +169 -0
  3. package/dist/agents/orchestrator.d.ts +21 -0
  4. package/dist/agents/orchestrator.d.ts.map +1 -0
  5. package/dist/agents/orchestrator.js +235 -0
  6. package/dist/agents/orchestrator.js.map +1 -0
  7. package/dist/agents/reporter.d.ts +13 -0
  8. package/dist/agents/reporter.d.ts.map +1 -0
  9. package/dist/agents/reporter.js +323 -0
  10. package/dist/agents/reporter.js.map +1 -0
  11. package/dist/ai/anthropic-provider.d.ts +19 -0
  12. package/dist/ai/anthropic-provider.d.ts.map +1 -0
  13. package/dist/ai/anthropic-provider.js +83 -0
  14. package/dist/ai/anthropic-provider.js.map +1 -0
  15. package/dist/ai/claude-cli-provider.d.ts +22 -0
  16. package/dist/ai/claude-cli-provider.d.ts.map +1 -0
  17. package/dist/ai/claude-cli-provider.js +197 -0
  18. package/dist/ai/claude-cli-provider.js.map +1 -0
  19. package/dist/ai/index.d.ts +15 -0
  20. package/dist/ai/index.d.ts.map +1 -0
  21. package/dist/ai/index.js +16 -0
  22. package/dist/ai/index.js.map +1 -0
  23. package/dist/ai/ollama-provider.d.ts +17 -0
  24. package/dist/ai/ollama-provider.d.ts.map +1 -0
  25. package/dist/ai/ollama-provider.js +88 -0
  26. package/dist/ai/ollama-provider.js.map +1 -0
  27. package/dist/ai/openai-provider.d.ts +20 -0
  28. package/dist/ai/openai-provider.d.ts.map +1 -0
  29. package/dist/ai/openai-provider.js +74 -0
  30. package/dist/ai/openai-provider.js.map +1 -0
  31. package/dist/ai/prompts.d.ts +36 -0
  32. package/dist/ai/prompts.d.ts.map +1 -0
  33. package/dist/ai/prompts.js +259 -0
  34. package/dist/ai/prompts.js.map +1 -0
  35. package/dist/ai/provider-factory.d.ts +26 -0
  36. package/dist/ai/provider-factory.d.ts.map +1 -0
  37. package/dist/ai/provider-factory.js +111 -0
  38. package/dist/ai/provider-factory.js.map +1 -0
  39. package/dist/ai/types.d.ts +37 -0
  40. package/dist/ai/types.d.ts.map +1 -0
  41. package/dist/ai/types.js +10 -0
  42. package/dist/ai/types.js.map +1 -0
  43. package/dist/analysis/code-scanner.d.ts +9 -0
  44. package/dist/analysis/code-scanner.d.ts.map +1 -0
  45. package/dist/analysis/code-scanner.js +409 -0
  46. package/dist/analysis/code-scanner.js.map +1 -0
  47. package/dist/analysis/dependency-graph.d.ts +9 -0
  48. package/dist/analysis/dependency-graph.d.ts.map +1 -0
  49. package/dist/analysis/dependency-graph.js +149 -0
  50. package/dist/analysis/dependency-graph.js.map +1 -0
  51. package/dist/analysis/diff-analyzer.d.ts +9 -0
  52. package/dist/analysis/diff-analyzer.d.ts.map +1 -0
  53. package/dist/analysis/diff-analyzer.js +232 -0
  54. package/dist/analysis/diff-analyzer.js.map +1 -0
  55. package/dist/analysis/index.d.ts +5 -0
  56. package/dist/analysis/index.d.ts.map +1 -0
  57. package/dist/analysis/index.js +5 -0
  58. package/dist/analysis/index.js.map +1 -0
  59. package/dist/analysis/strategy-planner.d.ts +11 -0
  60. package/dist/analysis/strategy-planner.d.ts.map +1 -0
  61. package/dist/analysis/strategy-planner.js +384 -0
  62. package/dist/analysis/strategy-planner.js.map +1 -0
  63. package/dist/cli/index.d.ts +9 -0
  64. package/dist/cli/index.d.ts.map +1 -0
  65. package/dist/cli/index.js +288 -0
  66. package/dist/cli/index.js.map +1 -0
  67. package/dist/executors/base-executor.d.ts +35 -0
  68. package/dist/executors/base-executor.d.ts.map +1 -0
  69. package/dist/executors/base-executor.js +138 -0
  70. package/dist/executors/base-executor.js.map +1 -0
  71. package/dist/executors/browser-runner.d.ts +24 -0
  72. package/dist/executors/browser-runner.d.ts.map +1 -0
  73. package/dist/executors/browser-runner.js +194 -0
  74. package/dist/executors/browser-runner.js.map +1 -0
  75. package/dist/executors/cloud-runner.d.ts +41 -0
  76. package/dist/executors/cloud-runner.d.ts.map +1 -0
  77. package/dist/executors/cloud-runner.js +153 -0
  78. package/dist/executors/cloud-runner.js.map +1 -0
  79. package/dist/executors/index.d.ts +12 -0
  80. package/dist/executors/index.d.ts.map +1 -0
  81. package/dist/executors/index.js +28 -0
  82. package/dist/executors/index.js.map +1 -0
  83. package/dist/executors/local-runner.d.ts +40 -0
  84. package/dist/executors/local-runner.d.ts.map +1 -0
  85. package/dist/executors/local-runner.js +395 -0
  86. package/dist/executors/local-runner.js.map +1 -0
  87. package/dist/executors/reporter.d.ts +6 -0
  88. package/dist/executors/reporter.d.ts.map +1 -0
  89. package/dist/executors/reporter.js +6 -0
  90. package/dist/executors/reporter.js.map +1 -0
  91. package/dist/executors/simulator-runner.d.ts +30 -0
  92. package/dist/executors/simulator-runner.d.ts.map +1 -0
  93. package/dist/executors/simulator-runner.js +339 -0
  94. package/dist/executors/simulator-runner.js.map +1 -0
  95. package/dist/generators/api-test.d.ts +22 -0
  96. package/dist/generators/api-test.d.ts.map +1 -0
  97. package/dist/generators/api-test.js +235 -0
  98. package/dist/generators/api-test.js.map +1 -0
  99. package/dist/generators/base-generator.d.ts +79 -0
  100. package/dist/generators/base-generator.d.ts.map +1 -0
  101. package/dist/generators/base-generator.js +234 -0
  102. package/dist/generators/base-generator.js.map +1 -0
  103. package/dist/generators/desktop-test.d.ts +22 -0
  104. package/dist/generators/desktop-test.d.ts.map +1 -0
  105. package/dist/generators/desktop-test.js +290 -0
  106. package/dist/generators/desktop-test.js.map +1 -0
  107. package/dist/generators/e2e-browser.d.ts +19 -0
  108. package/dist/generators/e2e-browser.d.ts.map +1 -0
  109. package/dist/generators/e2e-browser.js +233 -0
  110. package/dist/generators/e2e-browser.js.map +1 -0
  111. package/dist/generators/index.d.ts +21 -0
  112. package/dist/generators/index.d.ts.map +1 -0
  113. package/dist/generators/index.js +66 -0
  114. package/dist/generators/index.js.map +1 -0
  115. package/dist/generators/mobile-test.d.ts +22 -0
  116. package/dist/generators/mobile-test.d.ts.map +1 -0
  117. package/dist/generators/mobile-test.js +286 -0
  118. package/dist/generators/mobile-test.js.map +1 -0
  119. package/dist/generators/unit-test.d.ts +23 -0
  120. package/dist/generators/unit-test.d.ts.map +1 -0
  121. package/dist/generators/unit-test.js +282 -0
  122. package/dist/generators/unit-test.js.map +1 -0
  123. package/dist/index.d.ts +12 -0
  124. package/dist/index.d.ts.map +1 -0
  125. package/dist/index.js +13 -0
  126. package/dist/index.js.map +1 -0
  127. package/dist/mcp/server.d.ts +8 -0
  128. package/dist/mcp/server.d.ts.map +1 -0
  129. package/dist/mcp/server.js +217 -0
  130. package/dist/mcp/server.js.map +1 -0
  131. package/dist/types/index.d.ts +295 -0
  132. package/dist/types/index.d.ts.map +1 -0
  133. package/dist/types/index.js +8 -0
  134. package/dist/types/index.js.map +1 -0
  135. package/dist/utils/framework-detector.d.ts +28 -0
  136. package/dist/utils/framework-detector.d.ts.map +1 -0
  137. package/dist/utils/framework-detector.js +184 -0
  138. package/dist/utils/framework-detector.js.map +1 -0
  139. package/dist/utils/git.d.ts +33 -0
  140. package/dist/utils/git.d.ts.map +1 -0
  141. package/dist/utils/git.js +82 -0
  142. package/dist/utils/git.js.map +1 -0
  143. package/dist/utils/logger.d.ts +17 -0
  144. package/dist/utils/logger.d.ts.map +1 -0
  145. package/dist/utils/logger.js +47 -0
  146. package/dist/utils/logger.js.map +1 -0
  147. package/package.json +86 -0
@@ -0,0 +1,232 @@
1
+ import simpleGit from "simple-git";
2
+ import { resolve, extname } from "path";
3
+ const EXTENSION_TO_LANGUAGE = {
4
+ ".ts": "typescript",
5
+ ".tsx": "typescript",
6
+ ".js": "javascript",
7
+ ".jsx": "javascript",
8
+ ".mjs": "javascript",
9
+ ".cjs": "javascript",
10
+ ".py": "python",
11
+ ".go": "go",
12
+ ".rs": "rust",
13
+ ".java": "java",
14
+ };
15
+ /**
16
+ * Path-based rules evaluated in order. First match wins.
17
+ * Each rule is [pattern, fileType] where pattern is a regex tested against the normalized posix path.
18
+ */
19
+ const FILE_TYPE_RULES = [
20
+ // Tests first — they override everything
21
+ [/\.(test|spec)\.[^/]+$/, "test"],
22
+ // Platform-specific
23
+ [/\/src-tauri\//, "desktop-window"],
24
+ [/\/desktop\/.*components?\//, "desktop-component"],
25
+ [/\/desktop\//, "desktop-window"],
26
+ [/\/mobile\/.*screens?\//, "mobile-screen"],
27
+ [/\/mobile\/.*components?\//, "mobile-component"],
28
+ [/\/expo\/.*screens?\//, "mobile-screen"],
29
+ [/\/expo\/.*components?\//, "mobile-component"],
30
+ [/\/mobile\//, "mobile-screen"],
31
+ [/\/expo\//, "mobile-screen"],
32
+ // API layer
33
+ [/\/routes?\//, "api-route"],
34
+ [/\/api\//, "api-route"],
35
+ [/\/controllers?\//, "api-controller"],
36
+ [/\/middleware\//, "middleware"],
37
+ // React patterns
38
+ [/\/pages?\//, "react-page"],
39
+ [/\/screens?\//, "react-page"],
40
+ [/\/hooks?\//, "react-hook"],
41
+ [/\/components?\//, "react-component"],
42
+ // Backend patterns
43
+ [/\/services?\//, "service"],
44
+ [/\/utils?\//, "utility"],
45
+ [/\/helpers?\//, "utility"],
46
+ [/\/models?\//, "model"],
47
+ [/\/entities\//, "model"],
48
+ [/\/schemas?\//, "schema"],
49
+ [/\/migrations?\//, "migration"],
50
+ // Config files
51
+ [/\.(config|rc)\.[^/]+$/, "config"],
52
+ [/\/config\//, "config"],
53
+ // Style files
54
+ [/\.(css|scss|sass|less|styl)$/, "style"],
55
+ ];
56
+ function detectLanguage(filePath) {
57
+ const ext = extname(filePath).toLowerCase();
58
+ return EXTENSION_TO_LANGUAGE[ext] ?? "unknown";
59
+ }
60
+ function detectFileType(filePath) {
61
+ // Normalize to forward slashes for consistent matching
62
+ const normalized = filePath.replace(/\\/g, "/");
63
+ for (const [pattern, fileType] of FILE_TYPE_RULES) {
64
+ if (pattern.test(normalized)) {
65
+ return fileType;
66
+ }
67
+ }
68
+ // TSX files with no other pattern match are likely React components
69
+ if (normalized.endsWith(".tsx")) {
70
+ return "react-component";
71
+ }
72
+ return "unknown";
73
+ }
74
+ /**
75
+ * Parse unified diff hunks from raw diff text for a single file.
76
+ */
77
+ function parseHunks(rawDiff, _filePath) {
78
+ const hunks = [];
79
+ // Find the section for this file and extract @@ hunk headers
80
+ const hunkHeaderRegex = /^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,(\d+))?\s+@@(.*)$/gm;
81
+ let match;
82
+ while ((match = hunkHeaderRegex.exec(rawDiff)) !== null) {
83
+ const startLine = parseInt(match[1], 10);
84
+ const lineCount = match[2] !== undefined ? parseInt(match[2], 10) : 1;
85
+ const endLine = startLine + Math.max(lineCount - 1, 0);
86
+ // Capture the hunk content (lines between this header and the next one)
87
+ const headerEnd = match.index + match[0].length;
88
+ const nextHunkIdx = rawDiff.indexOf("\n@@", headerEnd);
89
+ const nextFileIdx = rawDiff.indexOf("\ndiff --git", headerEnd);
90
+ let sliceEnd;
91
+ if (nextHunkIdx === -1 && nextFileIdx === -1) {
92
+ sliceEnd = rawDiff.length;
93
+ }
94
+ else if (nextHunkIdx === -1) {
95
+ sliceEnd = nextFileIdx;
96
+ }
97
+ else if (nextFileIdx === -1) {
98
+ sliceEnd = nextHunkIdx;
99
+ }
100
+ else {
101
+ sliceEnd = Math.min(nextHunkIdx, nextFileIdx);
102
+ }
103
+ const content = rawDiff.slice(headerEnd, sliceEnd).trim();
104
+ hunks.push({ startLine, endLine, content });
105
+ }
106
+ return hunks;
107
+ }
108
+ function mapDiffStatus(status) {
109
+ switch (status) {
110
+ case "A":
111
+ return "added";
112
+ case "M":
113
+ return "modified";
114
+ case "D":
115
+ return "deleted";
116
+ case "R":
117
+ case "R100":
118
+ return "renamed";
119
+ default:
120
+ // Renames with similarity (R075 etc.) or copies
121
+ if (status.startsWith("R"))
122
+ return "renamed";
123
+ return "modified";
124
+ }
125
+ }
126
+ /**
127
+ * Analyze git diff to identify changed files with metadata.
128
+ *
129
+ * @param projectRoot - Absolute path to the git repository root
130
+ * @param baseBranch - Branch to diff against (defaults to HEAD for uncommitted changes)
131
+ */
132
+ export async function analyzeDiff(projectRoot, baseBranch) {
133
+ const root = resolve(projectRoot);
134
+ const git = simpleGit(root);
135
+ // Verify this is a git repository
136
+ const isRepo = await git.checkIsRepo();
137
+ if (!isRepo) {
138
+ throw new Error(`Not a git repository: ${root}`);
139
+ }
140
+ const currentBranch = (await git.revparse(["--abbrev-ref", "HEAD"])).trim() || "HEAD";
141
+ const base = baseBranch ?? currentBranch;
142
+ let diffSummaryFiles;
143
+ let rawDiff;
144
+ if (baseBranch) {
145
+ // Diff between base branch and current HEAD
146
+ const summary = await git.diffSummary([baseBranch]);
147
+ diffSummaryFiles = summary.files;
148
+ rawDiff = await git.diff([baseBranch]);
149
+ }
150
+ else {
151
+ // Combine staged + unstaged changes against HEAD
152
+ const stagedSummary = await git.diffSummary(["--cached"]);
153
+ const unstagedSummary = await git.diffSummary();
154
+ // Merge results, preferring staged if both exist
155
+ const fileMap = new Map();
156
+ for (const f of unstagedSummary.files) {
157
+ fileMap.set(f.file, f);
158
+ }
159
+ for (const f of stagedSummary.files) {
160
+ const existing = fileMap.get(f.file);
161
+ if (existing) {
162
+ // Combine additions/deletions from both staged and unstaged
163
+ fileMap.set(f.file, {
164
+ ...f,
165
+ insertions: f.insertions + existing.insertions,
166
+ deletions: f.deletions + existing.deletions,
167
+ changes: f.changes + existing.changes,
168
+ });
169
+ }
170
+ else {
171
+ fileMap.set(f.file, f);
172
+ }
173
+ }
174
+ diffSummaryFiles = Array.from(fileMap.values());
175
+ // Get raw diff for hunk parsing
176
+ const stagedRaw = await git.diff(["--cached"]);
177
+ const unstagedRaw = await git.diff();
178
+ rawDiff = stagedRaw + "\n" + unstagedRaw;
179
+ }
180
+ // Filter out binary files and non-source artifacts
181
+ const sourceFiles = diffSummaryFiles.filter((f) => !f.binary && !f.file.includes("node_modules/") && !f.file.includes("dist/"));
182
+ const files = sourceFiles.map((f) => {
183
+ // simple-git provides status info in diffSummary via the status property on StatusResult,
184
+ // but DiffResultTextFile does not carry it. We infer from insertions/deletions.
185
+ let status = "modified";
186
+ if (f.deletions === 0 && f.insertions > 0) {
187
+ status = "added";
188
+ }
189
+ return {
190
+ path: f.file,
191
+ status,
192
+ additions: f.insertions,
193
+ deletions: f.deletions,
194
+ hunks: parseHunks(rawDiff, f.file),
195
+ language: detectLanguage(f.file),
196
+ fileType: detectFileType(f.file),
197
+ };
198
+ });
199
+ // Augment with status from `git status` for more accurate added/deleted detection
200
+ const statusResult = await git.status();
201
+ const statusMap = new Map();
202
+ for (const f of statusResult.created)
203
+ statusMap.set(f, "A");
204
+ for (const f of statusResult.deleted)
205
+ statusMap.set(f, "D");
206
+ for (const f of statusResult.renamed)
207
+ statusMap.set(f.to, "R");
208
+ for (const file of files) {
209
+ const st = statusMap.get(file.path);
210
+ if (st) {
211
+ file.status = mapDiffStatus(st);
212
+ }
213
+ }
214
+ const addedCount = files.filter((f) => f.status === "added").length;
215
+ const modifiedCount = files.filter((f) => f.status === "modified").length;
216
+ const deletedCount = files.filter((f) => f.status === "deleted").length;
217
+ const summary = [
218
+ `${files.length} file(s) changed`,
219
+ addedCount > 0 ? `${addedCount} added` : null,
220
+ modifiedCount > 0 ? `${modifiedCount} modified` : null,
221
+ deletedCount > 0 ? `${deletedCount} deleted` : null,
222
+ ]
223
+ .filter(Boolean)
224
+ .join(", ");
225
+ return {
226
+ files,
227
+ summary,
228
+ baseBranch: base,
229
+ headBranch: currentBranch,
230
+ };
231
+ }
232
+ //# sourceMappingURL=diff-analyzer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff-analyzer.js","sourceRoot":"","sources":["../../src/analysis/diff-analyzer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ export { analyzeDiff } from "./diff-analyzer.js";
2
+ export { scanCode } from "./code-scanner.js";
3
+ export { buildDependencyGraph } from "./dependency-graph.js";
4
+ export { planStrategy } from "./strategy-planner.js";
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,5 @@
1
+ export { analyzeDiff } from "./diff-analyzer.js";
2
+ export { scanCode } from "./code-scanner.js";
3
+ export { buildDependencyGraph } from "./dependency-graph.js";
4
+ export { planStrategy } from "./strategy-planner.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/analysis/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,uBAAuB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,11 @@
1
+ import type { DiffResult, CodeScanResult, DependencyGraph, TestStrategy } from "../types/index.js";
2
+ /**
3
+ * Plan a testing strategy based on diff analysis, code scanning, and dependency graph.
4
+ *
5
+ * @param diff - Results from analyzeDiff
6
+ * @param scans - Results from scanCode for each changed file
7
+ * @param graph - Dependency graph from buildDependencyGraph
8
+ * @param projectRoot - Absolute path to the project root
9
+ */
10
+ export declare function planStrategy(diff: DiffResult, scans: CodeScanResult[], graph: DependencyGraph, projectRoot: string): Promise<TestStrategy>;
11
+ //# sourceMappingURL=strategy-planner.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"strategy-planner.d.ts","sourceRoot":"","sources":["../../src/analysis/strategy-planner.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,UAAU,EACV,cAAc,EACd,eAAe,EACf,YAAY,EAWb,MAAM,mBAAmB,CAAC;AA8M3B;;;;;;;GAOG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,UAAU,EAChB,KAAK,EAAE,cAAc,EAAE,EACvB,KAAK,EAAE,eAAe,EACtB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,YAAY,CAAC,CAiIvB"}
@@ -0,0 +1,384 @@
1
+ import { resolve, basename } from "path";
2
+ import { readFile, access } from "fs/promises";
3
+ import fg from "fast-glob";
4
+ // ─── File type → test type mapping ──────────────────────────────
5
+ const FILE_TYPE_TEST_MAP = {
6
+ "api-route": ["unit", "api"],
7
+ "api-controller": ["unit", "api"],
8
+ "react-component": ["unit", "e2e-browser"],
9
+ "react-page": ["unit", "e2e-browser"],
10
+ "react-hook": ["unit"],
11
+ service: ["unit"],
12
+ utility: ["unit"],
13
+ "mobile-screen": ["unit", "e2e-mobile"],
14
+ "mobile-component": ["unit", "e2e-mobile"],
15
+ "desktop-window": ["unit", "e2e-desktop"],
16
+ "desktop-component": ["unit", "e2e-desktop"],
17
+ middleware: ["unit", "integration"],
18
+ model: ["unit"],
19
+ schema: ["unit"],
20
+ };
21
+ // ─── Test type → execution environment mapping ──────────────────
22
+ const TEST_TYPE_ENVIRONMENT = {
23
+ unit: "local",
24
+ integration: "local",
25
+ api: "local",
26
+ "e2e-browser": "browser",
27
+ "e2e-mobile": "mobile-simulator",
28
+ "e2e-desktop": "desktop-app",
29
+ snapshot: "local",
30
+ performance: "cloud-sandbox",
31
+ };
32
+ // ─── Estimated test counts by file type ─────────────────────────
33
+ const ESTIMATED_TESTS_PER_TYPE = {
34
+ unit: 5,
35
+ integration: 3,
36
+ api: 4,
37
+ "e2e-browser": 2,
38
+ "e2e-mobile": 2,
39
+ "e2e-desktop": 2,
40
+ snapshot: 1,
41
+ performance: 1,
42
+ };
43
+ // ─── Project detection helpers ──────────────────────────────────
44
+ async function fileExists(path) {
45
+ try {
46
+ await access(path);
47
+ return true;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ async function readJson(path) {
54
+ try {
55
+ const content = await readFile(path, "utf-8");
56
+ return JSON.parse(content);
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ async function detectPackageManager(root) {
63
+ if (await fileExists(resolve(root, "bun.lockb")))
64
+ return "bun";
65
+ if (await fileExists(resolve(root, "bun.lock")))
66
+ return "bun";
67
+ if (await fileExists(resolve(root, "pnpm-lock.yaml")))
68
+ return "pnpm";
69
+ if (await fileExists(resolve(root, "yarn.lock")))
70
+ return "yarn";
71
+ return "npm";
72
+ }
73
+ async function detectFramework(root) {
74
+ const pkg = await readJson(resolve(root, "package.json"));
75
+ if (!pkg)
76
+ return "unknown";
77
+ const deps = {
78
+ ...pkg["dependencies"],
79
+ ...pkg["devDependencies"],
80
+ };
81
+ // Order matters: more specific frameworks first
82
+ if (deps["@nestjs/core"])
83
+ return "nestjs";
84
+ if (deps["next"])
85
+ return "next";
86
+ if (deps["expo"])
87
+ return "expo";
88
+ if (deps["react-native"])
89
+ return "react-native";
90
+ if (deps["@tauri-apps/api"])
91
+ return "tauri";
92
+ if (deps["electron"])
93
+ return "electron";
94
+ if (deps["hono"])
95
+ return "hono";
96
+ if (deps["fastify"])
97
+ return "fastify";
98
+ if (deps["express"])
99
+ return "express";
100
+ if (deps["react"])
101
+ return "react";
102
+ return "none";
103
+ }
104
+ async function detectTestFramework(root) {
105
+ const pkg = await readJson(resolve(root, "package.json"));
106
+ if (!pkg)
107
+ return "unknown";
108
+ const deps = {
109
+ ...pkg["dependencies"],
110
+ ...pkg["devDependencies"],
111
+ };
112
+ if (deps["vitest"])
113
+ return "vitest";
114
+ if (deps["jest"] || deps["@jest/core"])
115
+ return "jest";
116
+ if (deps["playwright"] || deps["@playwright/test"])
117
+ return "playwright";
118
+ if (deps["cypress"])
119
+ return "cypress";
120
+ if (deps["detox"])
121
+ return "detox";
122
+ if (deps["mocha"])
123
+ return "mocha";
124
+ if (deps["pytest"])
125
+ return "pytest";
126
+ return "unknown";
127
+ }
128
+ async function detectExistingTests(root) {
129
+ const testFiles = await fg(["**/*.test.{ts,tsx,js,jsx}", "**/*.spec.{ts,tsx,js,jsx}", "**/__tests__/**/*.{ts,tsx,js,jsx}"], {
130
+ cwd: root,
131
+ ignore: ["**/node_modules/**", "**/dist/**"],
132
+ });
133
+ const patterns = new Set();
134
+ for (const f of testFiles) {
135
+ if (f.includes(".test."))
136
+ patterns.add("*.test.*");
137
+ if (f.includes(".spec."))
138
+ patterns.add("*.spec.*");
139
+ if (f.includes("__tests__"))
140
+ patterns.add("__tests__/");
141
+ }
142
+ return {
143
+ hasTests: testFiles.length > 0,
144
+ patterns: Array.from(patterns),
145
+ };
146
+ }
147
+ async function detectProjectInfo(root) {
148
+ const pkg = await readJson(resolve(root, "package.json"));
149
+ const name = pkg?.["name"] ?? basename(root);
150
+ const packageManager = await detectPackageManager(root);
151
+ const framework = await detectFramework(root);
152
+ const testFramework = await detectTestFramework(root);
153
+ const { hasTests, patterns } = await detectExistingTests(root);
154
+ // Detect primary language from tsconfig presence
155
+ const hasTsConfig = await fileExists(resolve(root, "tsconfig.json"));
156
+ return {
157
+ name,
158
+ root,
159
+ language: hasTsConfig ? "typescript" : "javascript",
160
+ framework,
161
+ testFramework,
162
+ packageManager,
163
+ hasExistingTests: hasTests,
164
+ existingTestPatterns: patterns,
165
+ };
166
+ }
167
+ // ─── Plan generation ────────────────────────────────────────────
168
+ let planIdCounter = 0;
169
+ function generatePlanId() {
170
+ planIdCounter++;
171
+ return `plan_${planIdCounter.toString().padStart(3, "0")}`;
172
+ }
173
+ function determinePriority(filePath, changedFiles, graph) {
174
+ // Directly changed files are critical
175
+ if (changedFiles.has(filePath))
176
+ return "critical";
177
+ // Files that directly depend on a changed file are high priority
178
+ const node = graph.get(filePath);
179
+ if (node) {
180
+ for (const dep of node.dependsOn) {
181
+ if (changedFiles.has(dep))
182
+ return "high";
183
+ }
184
+ }
185
+ return "medium";
186
+ }
187
+ function buildTestTarget(filePath, scan) {
188
+ return {
189
+ files: [filePath],
190
+ functions: scan?.functions.filter((f) => f.isExported).map((f) => f.name) ?? [],
191
+ endpoints: scan?.endpoints ?? [],
192
+ components: scan?.components.map((c) => c.name) ?? [],
193
+ };
194
+ }
195
+ /**
196
+ * Plan a testing strategy based on diff analysis, code scanning, and dependency graph.
197
+ *
198
+ * @param diff - Results from analyzeDiff
199
+ * @param scans - Results from scanCode for each changed file
200
+ * @param graph - Dependency graph from buildDependencyGraph
201
+ * @param projectRoot - Absolute path to the project root
202
+ */
203
+ export async function planStrategy(diff, scans, graph, projectRoot) {
204
+ const root = resolve(projectRoot);
205
+ // Reset plan counter for deterministic IDs within a single strategy
206
+ planIdCounter = 0;
207
+ const project = await detectProjectInfo(root);
208
+ const scanMap = new Map();
209
+ for (const scan of scans) {
210
+ scanMap.set(scan.file, scan);
211
+ }
212
+ const changedPaths = new Set(diff.files.map((f) => f.path));
213
+ const plans = [];
214
+ // Generate plans for each changed file (skip deleted files and test files)
215
+ for (const file of diff.files) {
216
+ if (file.status === "deleted")
217
+ continue;
218
+ if (file.fileType === "test")
219
+ continue;
220
+ if (file.fileType === "config" || file.fileType === "style" || file.fileType === "migration")
221
+ continue;
222
+ const testTypes = FILE_TYPE_TEST_MAP[file.fileType] ?? ["unit"];
223
+ const scan = scanMap.get(file.path);
224
+ const priority = determinePriority(file.path, changedPaths, graph);
225
+ for (const testType of testTypes) {
226
+ const planId = generatePlanId();
227
+ const target = buildTestTarget(file.path, scan);
228
+ // Describe what we're testing
229
+ const targetDesc = target.endpoints.length > 0
230
+ ? `${target.endpoints.length} endpoint(s)`
231
+ : target.components.length > 0
232
+ ? `${target.components.length} component(s)`
233
+ : target.functions.length > 0
234
+ ? `${target.functions.length} function(s)`
235
+ : "module";
236
+ plans.push({
237
+ id: planId,
238
+ type: testType,
239
+ target,
240
+ priority,
241
+ description: `${testType} tests for ${file.path} — ${targetDesc}`,
242
+ estimatedTests: ESTIMATED_TESTS_PER_TYPE[testType] ?? 3,
243
+ dependencies: [],
244
+ });
245
+ }
246
+ }
247
+ // Also plan tests for files that depend on changed files (ripple effect)
248
+ for (const file of diff.files) {
249
+ if (file.status === "deleted")
250
+ continue;
251
+ const node = graph.get(file.path);
252
+ if (!node)
253
+ continue;
254
+ for (const dependentPath of node.dependedBy) {
255
+ // Skip if already in our changed files or if it's a test file
256
+ if (changedPaths.has(dependentPath))
257
+ continue;
258
+ if (/\.(test|spec)\./.test(dependentPath))
259
+ continue;
260
+ const depNode = graph.get(dependentPath);
261
+ if (!depNode)
262
+ continue;
263
+ // Only add unit tests for ripple-effect files
264
+ const planId = generatePlanId();
265
+ plans.push({
266
+ id: planId,
267
+ type: "unit",
268
+ target: {
269
+ files: [dependentPath],
270
+ functions: [],
271
+ endpoints: [],
272
+ components: [],
273
+ },
274
+ priority: "high",
275
+ description: `unit tests for ${dependentPath} (depends on changed file ${file.path})`,
276
+ estimatedTests: 3,
277
+ dependencies: [],
278
+ });
279
+ }
280
+ }
281
+ // Wire up dependencies: integration/api/e2e plans depend on their unit plan for the same file
282
+ const unitPlansByFile = new Map();
283
+ for (const plan of plans) {
284
+ if (plan.type === "unit" && plan.target.files.length > 0) {
285
+ unitPlansByFile.set(plan.target.files[0], plan.id);
286
+ }
287
+ }
288
+ for (const plan of plans) {
289
+ if (plan.type !== "unit" && plan.target.files.length > 0) {
290
+ const unitPlanId = unitPlansByFile.get(plan.target.files[0]);
291
+ if (unitPlanId) {
292
+ plan.dependencies.push(unitPlanId);
293
+ }
294
+ }
295
+ }
296
+ // Build execution phases: group non-dependent plans into parallel phases
297
+ const executionOrder = buildExecutionPhases(plans);
298
+ // Estimate total duration (rough: 2s per test for unit, 5s for integration, 10s for e2e)
299
+ const durationPerType = {
300
+ unit: 2,
301
+ integration: 5,
302
+ api: 5,
303
+ "e2e-browser": 10,
304
+ "e2e-mobile": 15,
305
+ "e2e-desktop": 15,
306
+ snapshot: 1,
307
+ performance: 20,
308
+ };
309
+ const estimatedDuration = plans.reduce((total, plan) => total + plan.estimatedTests * (durationPerType[plan.type] ?? 5), 0);
310
+ return {
311
+ project,
312
+ plans,
313
+ executionOrder,
314
+ estimatedDuration,
315
+ };
316
+ }
317
+ /**
318
+ * Group test plans into parallel execution phases using topological ordering.
319
+ * Plans with no dependencies run in the earliest phase. Plans with dependencies
320
+ * run in the phase after their last dependency completes.
321
+ */
322
+ function buildExecutionPhases(plans) {
323
+ const planMap = new Map();
324
+ for (const plan of plans) {
325
+ planMap.set(plan.id, plan);
326
+ }
327
+ // Compute phase number for each plan
328
+ const phaseAssignment = new Map();
329
+ const visited = new Set();
330
+ function getPhase(planId) {
331
+ if (phaseAssignment.has(planId))
332
+ return phaseAssignment.get(planId);
333
+ if (visited.has(planId))
334
+ return 0; // Circular dependency guard
335
+ visited.add(planId);
336
+ const plan = planMap.get(planId);
337
+ if (!plan || plan.dependencies.length === 0) {
338
+ phaseAssignment.set(planId, 0);
339
+ return 0;
340
+ }
341
+ let maxDepPhase = 0;
342
+ for (const depId of plan.dependencies) {
343
+ maxDepPhase = Math.max(maxDepPhase, getPhase(depId) + 1);
344
+ }
345
+ phaseAssignment.set(planId, maxDepPhase);
346
+ return maxDepPhase;
347
+ }
348
+ for (const plan of plans) {
349
+ getPhase(plan.id);
350
+ }
351
+ // Group plans by phase
352
+ const phaseGroups = new Map();
353
+ for (const [planId, phase] of phaseAssignment) {
354
+ const group = phaseGroups.get(phase) ?? [];
355
+ group.push(planId);
356
+ phaseGroups.set(phase, group);
357
+ }
358
+ // Sort phases and determine environment per phase
359
+ const sortedPhases = Array.from(phaseGroups.entries()).sort(([a], [b]) => a - b);
360
+ return sortedPhases.map(([phase, planIds]) => {
361
+ // Determine the heaviest environment needed in this phase
362
+ const environments = planIds
363
+ .map((id) => {
364
+ const plan = planMap.get(id);
365
+ return plan ? TEST_TYPE_ENVIRONMENT[plan.type] : "local";
366
+ })
367
+ .filter((e) => e !== undefined);
368
+ // Pick the most "heavyweight" environment for the phase
369
+ const envPriority = [
370
+ "mobile-simulator",
371
+ "desktop-app",
372
+ "browser",
373
+ "cloud-sandbox",
374
+ "local",
375
+ ];
376
+ const environment = envPriority.find((e) => environments.includes(e)) ?? "local";
377
+ return {
378
+ phase,
379
+ plans: planIds,
380
+ environment,
381
+ };
382
+ });
383
+ }
384
+ //# sourceMappingURL=strategy-planner.js.map