@mandujs/ate 0.17.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.
package/src/heal.ts ADDED
@@ -0,0 +1,427 @@
1
+ import { readFileSync, existsSync, writeFileSync, copyFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { getAtePaths, ensureDir } from "./fs";
4
+ import type { HealInput } from "./types";
5
+ import { parseTrace, generateAlternativeSelectors } from "./trace-parser";
6
+ import { execSync } from "node:child_process";
7
+
8
+ export interface HealSuggestion {
9
+ kind: "selector-map" | "test-code" | "note";
10
+ title: string;
11
+ diff: string; // unified diff suggestion (no auto-commit)
12
+ metadata?: {
13
+ selector?: string;
14
+ alternatives?: string[];
15
+ testFile?: string;
16
+ };
17
+ }
18
+
19
+ export type FailureCategory = "selector" | "timeout" | "assertion" | "unknown";
20
+
21
+ export interface FeedbackAnalysis {
22
+ category: FailureCategory;
23
+ suggestions: HealSuggestion[];
24
+ autoApplicable: boolean;
25
+ priority: number; // 1-10, higher = more confident
26
+ reasoning: string;
27
+ }
28
+
29
+ export interface FeedbackInput {
30
+ repoRoot: string;
31
+ runId: string;
32
+ autoApply?: boolean;
33
+ }
34
+
35
+ export interface ApplyHealInput {
36
+ repoRoot: string;
37
+ runId: string;
38
+ healIndex: number;
39
+ createBackup?: boolean;
40
+ }
41
+
42
+ export interface ApplyHealResult {
43
+ success: boolean;
44
+ appliedFile: string;
45
+ backupPath?: string;
46
+ error?: string;
47
+ }
48
+
49
+ /**
50
+ * Healing Engine
51
+ * - Parses Playwright trace/report to find failed locators
52
+ * - Generates alternative selector suggestions
53
+ * - Creates unified diffs for selector-map.json or test files
54
+ * - Does NOT auto-commit or patch files (user must review and apply)
55
+ */
56
+ export function heal(input: HealInput): { attempted: true; suggestions: HealSuggestion[] } {
57
+ const paths = getAtePaths(input.repoRoot);
58
+ const reportDir = join(paths.reportsDir, input.runId || "latest");
59
+ const jsonReportPath = join(reportDir, "playwright-report.json");
60
+
61
+ // Try to read Playwright report
62
+ if (!existsSync(jsonReportPath)) {
63
+ return {
64
+ attempted: true,
65
+ suggestions: [{ kind: "note", title: "No Playwright JSON report found", diff: "" }],
66
+ };
67
+ }
68
+
69
+ const suggestions: HealSuggestion[] = [];
70
+
71
+ try {
72
+ // Parse trace to extract failed locators
73
+ const parseResult = parseTrace(jsonReportPath);
74
+
75
+ if (parseResult.failedLocators.length === 0) {
76
+ suggestions.push({
77
+ kind: "note",
78
+ title: "No failed locators detected in trace",
79
+ diff: "",
80
+ });
81
+ return { attempted: true, suggestions };
82
+ }
83
+
84
+ // Generate healing suggestions for each failed locator
85
+ for (const failed of parseResult.failedLocators) {
86
+ const alternatives = generateAlternativeSelectors(failed.selector, failed.actionType);
87
+
88
+ if (alternatives.length === 0) {
89
+ suggestions.push({
90
+ kind: "note",
91
+ title: `Failed locator: ${failed.selector} (no alternatives)`,
92
+ diff: "",
93
+ metadata: {
94
+ selector: failed.selector,
95
+ alternatives: [],
96
+ },
97
+ });
98
+ continue;
99
+ }
100
+
101
+ // Generate selector-map diff
102
+ const selectorMapDiff = generateSelectorMapDiff(failed.selector, alternatives);
103
+ suggestions.push({
104
+ kind: "selector-map",
105
+ title: `Update selector-map for: ${failed.selector}`,
106
+ diff: selectorMapDiff,
107
+ metadata: {
108
+ selector: failed.selector,
109
+ alternatives,
110
+ },
111
+ });
112
+
113
+ // If we have test file context, generate test code diff
114
+ if (parseResult.metadata.testFile && failed.context) {
115
+ const testCodeDiff = generateTestCodeDiff(
116
+ parseResult.metadata.testFile,
117
+ failed.selector,
118
+ alternatives[0], // Use first alternative
119
+ failed.context,
120
+ );
121
+
122
+ if (testCodeDiff) {
123
+ suggestions.push({
124
+ kind: "test-code",
125
+ title: `Update test code: ${failed.selector} → ${alternatives[0]}`,
126
+ diff: testCodeDiff,
127
+ metadata: {
128
+ selector: failed.selector,
129
+ alternatives,
130
+ testFile: parseResult.metadata.testFile,
131
+ },
132
+ });
133
+ }
134
+ }
135
+ }
136
+ } catch (err) {
137
+ suggestions.push({
138
+ kind: "note",
139
+ title: `Healing failed: ${String(err)}`,
140
+ diff: "",
141
+ });
142
+ }
143
+
144
+ if (suggestions.length === 0) {
145
+ suggestions.push({
146
+ kind: "note",
147
+ title: "No healing suggestions available",
148
+ diff: "",
149
+ });
150
+ }
151
+
152
+ return { attempted: true, suggestions };
153
+ }
154
+
155
+ /**
156
+ * Generate unified diff for selector-map.json
157
+ */
158
+ function generateSelectorMapDiff(originalSelector: string, alternatives: string[]): string {
159
+ const escapedSelector = JSON.stringify(originalSelector);
160
+ const alternativesJson = JSON.stringify(alternatives, null, 2).split("\n").join("\n+ ");
161
+
162
+ const lines = [
163
+ "--- a/.mandu/selector-map.json",
164
+ "+++ b/.mandu/selector-map.json",
165
+ "@@ -1,3 +1,8 @@",
166
+ " {",
167
+ "+ " + escapedSelector + ": {",
168
+ "+ \"fallbacks\": " + alternativesJson,
169
+ "+ },",
170
+ " \"version\": \"1.0.0\"",
171
+ " }",
172
+ "",
173
+ ];
174
+
175
+ return lines.join("\n");
176
+ }
177
+
178
+ /**
179
+ * Generate unified diff for test code file
180
+ */
181
+ function generateTestCodeDiff(
182
+ testFile: string,
183
+ originalSelector: string,
184
+ newSelector: string,
185
+ context: string,
186
+ ): string | null {
187
+ // Escape special regex characters
188
+ const escapeRegex = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
189
+
190
+ // Try to infer the line content from context
191
+ const contextLine = context.trim();
192
+
193
+ if (!contextLine) {
194
+ return null;
195
+ }
196
+
197
+ const lines = [
198
+ `--- a/${testFile}`,
199
+ `+++ b/${testFile}`,
200
+ "@@ -1,3 +1,3 @@",
201
+ ` // ${contextLine}`,
202
+ `-await page.locator('${originalSelector}')`,
203
+ `+await page.locator('${newSelector}')`,
204
+ "",
205
+ ];
206
+
207
+ return lines.join("\n");
208
+ }
209
+
210
+ /**
211
+ * Analyze test failure feedback and categorize for heal suggestions
212
+ */
213
+ export function analyzeFeedback(input: FeedbackInput): FeedbackAnalysis {
214
+ const healResult = heal({
215
+ repoRoot: input.repoRoot,
216
+ runId: input.runId,
217
+ });
218
+
219
+ if (!healResult.attempted || healResult.suggestions.length === 0) {
220
+ return {
221
+ category: "unknown",
222
+ suggestions: [],
223
+ autoApplicable: false,
224
+ priority: 0,
225
+ reasoning: "No healing suggestions available",
226
+ };
227
+ }
228
+
229
+ // Categorize failure based on suggestions
230
+ const hasSelector = healResult.suggestions.some((s) => s.kind === "selector-map");
231
+ const hasTestCode = healResult.suggestions.some((s) => s.kind === "test-code");
232
+ const onlyNotes = healResult.suggestions.every((s) => s.kind === "note");
233
+
234
+ let category: FailureCategory = "unknown";
235
+ let autoApplicable = false;
236
+ let priority = 5;
237
+ let reasoning = "";
238
+
239
+ if (hasSelector) {
240
+ category = "selector";
241
+ // Auto-apply selector-map changes only (safe)
242
+ autoApplicable = true;
243
+ priority = 8;
244
+ reasoning = "Failed locator detected. Selector-map update is safe to auto-apply.";
245
+ } else if (hasTestCode) {
246
+ category = "assertion";
247
+ // Test code changes require manual review
248
+ autoApplicable = false;
249
+ priority = 6;
250
+ reasoning = "Test code modification suggested. Manual review required.";
251
+ } else if (onlyNotes) {
252
+ const noteText = healResult.suggestions[0]?.title.toLowerCase() || "";
253
+ if (noteText.includes("timeout")) {
254
+ category = "timeout";
255
+ priority = 4;
256
+ reasoning = "Timeout detected. May require wait time adjustment.";
257
+ } else {
258
+ category = "unknown";
259
+ priority = 3;
260
+ reasoning = "Unable to categorize failure automatically.";
261
+ }
262
+ autoApplicable = false;
263
+ }
264
+
265
+ return {
266
+ category,
267
+ suggestions: healResult.suggestions,
268
+ autoApplicable: autoApplicable && (input.autoApply ?? false),
269
+ priority,
270
+ reasoning,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * Check if git working directory has uncommitted changes
276
+ */
277
+ function hasUncommittedChanges(repoRoot: string): boolean {
278
+ try {
279
+ const result = execSync("git status --porcelain", {
280
+ cwd: repoRoot,
281
+ encoding: "utf8",
282
+ });
283
+ return result.trim().length > 0;
284
+ } catch {
285
+ // Not a git repo or git not available
286
+ return false;
287
+ }
288
+ }
289
+
290
+ /**
291
+ * Apply a heal suggestion diff to the actual file
292
+ */
293
+ export function applyHeal(input: ApplyHealInput): ApplyHealResult {
294
+ const paths = getAtePaths(input.repoRoot);
295
+ const reportDir = join(paths.reportsDir, input.runId);
296
+
297
+ // Get heal suggestions
298
+ const healResult = heal({
299
+ repoRoot: input.repoRoot,
300
+ runId: input.runId,
301
+ });
302
+
303
+ if (!healResult.attempted || healResult.suggestions.length === 0) {
304
+ return {
305
+ success: false,
306
+ appliedFile: "",
307
+ error: "No heal suggestions available",
308
+ };
309
+ }
310
+
311
+ if (input.healIndex < 0 || input.healIndex >= healResult.suggestions.length) {
312
+ return {
313
+ success: false,
314
+ appliedFile: "",
315
+ error: `Invalid heal index: ${input.healIndex} (available: 0-${healResult.suggestions.length - 1})`,
316
+ };
317
+ }
318
+
319
+ const suggestion = healResult.suggestions[input.healIndex];
320
+
321
+ // Only apply selector-map or test-code suggestions
322
+ if (suggestion.kind === "note") {
323
+ return {
324
+ success: false,
325
+ appliedFile: "",
326
+ error: "Cannot apply note-type suggestions",
327
+ };
328
+ }
329
+
330
+ // Safety check: require backup if working directory is dirty
331
+ const createBackup = input.createBackup ?? true;
332
+ if (!createBackup && hasUncommittedChanges(input.repoRoot)) {
333
+ return {
334
+ success: false,
335
+ appliedFile: "",
336
+ error: "Backup required: git working directory has uncommitted changes",
337
+ };
338
+ }
339
+
340
+ let targetFile: string;
341
+ let backupPath: string | undefined;
342
+
343
+ try {
344
+ if (suggestion.kind === "selector-map") {
345
+ targetFile = paths.selectorMapPath;
346
+
347
+ // Create backup
348
+ if (createBackup) {
349
+ ensureDir(reportDir);
350
+ backupPath = join(reportDir, `selector-map.backup-${Date.now()}.json`);
351
+ if (existsSync(targetFile)) {
352
+ copyFileSync(targetFile, backupPath);
353
+ }
354
+ }
355
+
356
+ // Apply selector-map diff
357
+ const currentContent = existsSync(targetFile)
358
+ ? JSON.parse(readFileSync(targetFile, "utf8"))
359
+ : { version: "1.0.0" };
360
+
361
+ // Extract selector and alternatives from metadata
362
+ const { selector, alternatives } = suggestion.metadata || {};
363
+ if (!selector || !alternatives || alternatives.length === 0) {
364
+ throw new Error("Invalid suggestion metadata");
365
+ }
366
+
367
+ // Update selector-map
368
+ currentContent[selector] = {
369
+ fallbacks: alternatives,
370
+ };
371
+
372
+ writeFileSync(targetFile, JSON.stringify(currentContent, null, 2), "utf8");
373
+ } else if (suggestion.kind === "test-code") {
374
+ // Test code modification - extract file path from metadata
375
+ const testFile = suggestion.metadata?.testFile;
376
+ if (!testFile) {
377
+ throw new Error("No test file specified in suggestion metadata");
378
+ }
379
+
380
+ targetFile = join(input.repoRoot, testFile);
381
+
382
+ if (!existsSync(targetFile)) {
383
+ throw new Error(`Test file not found: ${targetFile}`);
384
+ }
385
+
386
+ // Create backup
387
+ if (createBackup) {
388
+ ensureDir(reportDir);
389
+ backupPath = join(reportDir, `${testFile.replace(/\//g, "_")}.backup-${Date.now()}`);
390
+ copyFileSync(targetFile, backupPath);
391
+ }
392
+
393
+ // Apply test code diff (simple string replacement)
394
+ const { selector, alternatives } = suggestion.metadata || {};
395
+ if (!selector || !alternatives || alternatives.length === 0) {
396
+ throw new Error("Invalid suggestion metadata");
397
+ }
398
+
399
+ const content = readFileSync(targetFile, "utf8");
400
+ const newContent = content.replace(
401
+ new RegExp(`locator\\(['"\`]${selector.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]\\)`, "g"),
402
+ `locator('${alternatives[0]}')`,
403
+ );
404
+
405
+ writeFileSync(targetFile, newContent, "utf8");
406
+ } else {
407
+ return {
408
+ success: false,
409
+ appliedFile: "",
410
+ error: `Unsupported suggestion kind: ${suggestion.kind}`,
411
+ };
412
+ }
413
+
414
+ return {
415
+ success: true,
416
+ appliedFile: targetFile,
417
+ backupPath,
418
+ };
419
+ } catch (err) {
420
+ return {
421
+ success: false,
422
+ appliedFile: targetFile!,
423
+ backupPath,
424
+ error: String(err),
425
+ };
426
+ }
427
+ }
package/src/impact.ts ADDED
@@ -0,0 +1,146 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { resolve } from "node:path";
3
+ import { getAtePaths, readJson } from "./fs";
4
+ import type { ImpactInput, InteractionGraph } from "./types";
5
+ import { buildDependencyGraph, findDependents } from "./dep-graph";
6
+
7
+ function verifyGitRev(repoRoot: string, rev: string): void {
8
+ // Prevent command injection: disallow whitespace and common shell metacharacters.
9
+ if (!/^[0-9A-Za-z._/~-]+$/.test(rev)) {
10
+ throw new Error(`Invalid git revision: ${rev}`);
11
+ }
12
+ // Ensure it resolves (commit-ish)
13
+ execFileSync("git", ["rev-parse", "--verify", `${rev}^{commit}`], {
14
+ cwd: repoRoot,
15
+ stdio: ["ignore", "ignore", "ignore"],
16
+ });
17
+ }
18
+
19
+ function toPosixPath(p: string): string {
20
+ return p.replace(/\\/g, "/");
21
+ }
22
+
23
+ function normalizePath(path: string, rootDir: string): string {
24
+ const abs = resolve(rootDir, path);
25
+ return abs.replace(/\\/g, "/");
26
+ }
27
+
28
+ export async function computeImpact(input: ImpactInput): Promise<{ changedFiles: string[]; selectedRoutes: string[]; warnings: string[] }> {
29
+ const repoRoot = input.repoRoot;
30
+ const base = input.base ?? "HEAD~1";
31
+ const head = input.head ?? "HEAD";
32
+ const warnings: string[] = [];
33
+
34
+ // Validate input
35
+ if (!repoRoot) {
36
+ throw new Error("repoRoot는 필수입니다");
37
+ }
38
+
39
+ // Verify git revisions
40
+ try {
41
+ verifyGitRev(repoRoot, base);
42
+ } catch (err: any) {
43
+ throw new Error(`잘못된 base revision: ${base} (${err.message})`);
44
+ }
45
+
46
+ try {
47
+ verifyGitRev(repoRoot, head);
48
+ } catch (err: any) {
49
+ throw new Error(`잘못된 head revision: ${head} (${err.message})`);
50
+ }
51
+
52
+ let out: string;
53
+ try {
54
+ out = execFileSync("git", ["diff", "--name-only", `${base}..${head}`], {
55
+ cwd: repoRoot,
56
+ stdio: ["ignore", "pipe", "pipe"],
57
+ }).toString("utf8");
58
+ } catch (err: any) {
59
+ throw new Error(`Git diff 실행 실패: ${err.message}`);
60
+ }
61
+
62
+ const changedFiles = out.split("\n").map((s) => toPosixPath(s.trim())).filter(Boolean);
63
+
64
+ if (changedFiles.length === 0) {
65
+ warnings.push(`경고: 변경된 파일이 없습니다 (${base}..${head})`);
66
+ }
67
+
68
+ // Load interaction graph
69
+ const paths = getAtePaths(repoRoot);
70
+
71
+ let graph: InteractionGraph;
72
+ try {
73
+ graph = readJson<InteractionGraph>(paths.interactionGraphPath);
74
+ } catch (err: any) {
75
+ throw new Error(`Interaction graph 읽기 실패: ${err.message}`);
76
+ }
77
+
78
+ if (!graph.nodes || graph.nodes.length === 0) {
79
+ warnings.push("경고: Interaction graph가 비어있습니다");
80
+ return { changedFiles, selectedRoutes: [], warnings };
81
+ }
82
+
83
+ const routes = graph.nodes.filter((n) => n.kind === "route") as Array<{ kind: "route"; id: string; file: string }>;
84
+
85
+ if (routes.length === 0) {
86
+ warnings.push("경고: Route가 없습니다");
87
+ return { changedFiles, selectedRoutes: [], warnings };
88
+ }
89
+
90
+ // Build dependency graph for deep impact analysis
91
+ let depGraph;
92
+ try {
93
+ depGraph = await buildDependencyGraph({
94
+ rootDir: repoRoot,
95
+ include: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
96
+ exclude: ["**/node_modules/**", "**/*.test.ts", "**/*.spec.ts"],
97
+ });
98
+ } catch (err: any) {
99
+ warnings.push(`Dependency graph 빌드 실패: ${err.message}`);
100
+ // Fallback: only direct file matching
101
+ const selected = new Set<string>();
102
+ for (const changedFile of changedFiles) {
103
+ const normalizedChangedFile = normalizePath(changedFile, repoRoot);
104
+ for (const r of routes) {
105
+ const routeFile = normalizePath(r.file, repoRoot);
106
+ if (normalizedChangedFile === routeFile) {
107
+ selected.add(r.id);
108
+ }
109
+ }
110
+ }
111
+ return { changedFiles, selectedRoutes: Array.from(selected), warnings };
112
+ }
113
+
114
+ const selected = new Set<string>();
115
+
116
+ for (const changedFile of changedFiles) {
117
+ try {
118
+ const normalizedChangedFile = normalizePath(changedFile, repoRoot);
119
+
120
+ // Direct match: if the route file itself changed
121
+ for (const r of routes) {
122
+ const routeFile = normalizePath(r.file, repoRoot);
123
+ if (normalizedChangedFile === routeFile) {
124
+ selected.add(r.id);
125
+ }
126
+ }
127
+
128
+ // Transitive impact: find all files that depend on the changed file
129
+ const affectedFiles = findDependents(depGraph, normalizedChangedFile);
130
+
131
+ for (const affectedFile of affectedFiles) {
132
+ for (const r of routes) {
133
+ const routeFile = normalizePath(r.file, repoRoot);
134
+ if (affectedFile === routeFile) {
135
+ selected.add(r.id);
136
+ }
137
+ }
138
+ }
139
+ } catch (err: any) {
140
+ warnings.push(`파일 영향 분석 실패 (${changedFile}): ${err.message}`);
141
+ // Continue with next file
142
+ }
143
+ }
144
+
145
+ return { changedFiles, selectedRoutes: Array.from(selected), warnings };
146
+ }
package/src/index.ts ADDED
@@ -0,0 +1,112 @@
1
+ export * from "./types";
2
+ export * as ATEFS from "./fs";
3
+ export { ATEFileError, ensureDir, readJson, writeJson, fileExists, getAtePaths } from "./fs";
4
+
5
+ export { extract } from "./extractor";
6
+ export { generateAndWriteScenarios } from "./scenario";
7
+ export { generatePlaywrightSpecs } from "./codegen";
8
+ export { runPlaywright } from "./runner";
9
+ export { composeSummary, writeSummary, generateReport } from "./report";
10
+ export type { ReportFormat, GenerateReportOptions } from "./report";
11
+ export { generateHtmlReport } from "./reporter/html";
12
+ export type { HtmlReportOptions, HtmlReportResult } from "./reporter/html";
13
+ export { heal, analyzeFeedback, applyHeal } from "./heal";
14
+ export type {
15
+ HealSuggestion,
16
+ FeedbackAnalysis,
17
+ FeedbackInput,
18
+ ApplyHealInput,
19
+ ApplyHealResult,
20
+ FailureCategory,
21
+ } from "./heal";
22
+ export { computeImpact } from "./impact";
23
+ export * from "./selector-map";
24
+ export { parseTrace, generateAlternativeSelectors } from "./trace-parser";
25
+ export type { TraceAction, FailedLocator, TraceParseResult } from "./trace-parser";
26
+
27
+ // Oracle and domain detection
28
+ export { detectDomain, detectDomainFromRoute, detectDomainFromSource } from "./domain-detector";
29
+ export type { AppDomain, DomainDetectionResult } from "./domain-detector";
30
+ export { generateL1Assertions, upgradeL0ToL1, getAssertionCount, createDefaultOracle } from "./oracle";
31
+ export type { OracleResult } from "./oracle";
32
+
33
+ import type { ExtractInput, GenerateInput, RunInput, ImpactInput, HealInput, OracleLevel } from "./types";
34
+ import { getAtePaths } from "./fs";
35
+
36
+ /**
37
+ * High-level ATE pipeline helpers (JSON in/out)
38
+ */
39
+ export async function ateExtract(input: ExtractInput) {
40
+ const { extract } = await import("./extractor");
41
+ return extract(input);
42
+ }
43
+
44
+ export async function ateGenerate(input: GenerateInput) {
45
+ const paths = getAtePaths(input.repoRoot);
46
+ const oracleLevel = input.oracleLevel ?? ("L1" as OracleLevel);
47
+ // generate scenarios then specs - lazy load codegen and scenario
48
+ const { generateAndWriteScenarios } = await import("./scenario");
49
+ const { generatePlaywrightSpecs } = await import("./codegen");
50
+
51
+ generateAndWriteScenarios(input.repoRoot, oracleLevel);
52
+ const res = generatePlaywrightSpecs(input.repoRoot, { onlyRoutes: input.onlyRoutes });
53
+ return {
54
+ ok: true,
55
+ scenariosPath: paths.scenariosPath,
56
+ generatedSpecs: res.files,
57
+ };
58
+ }
59
+
60
+ export async function ateRun(input: RunInput) {
61
+ const startedAt = new Date().toISOString();
62
+ const { runPlaywright } = await import("./runner");
63
+ const run = await runPlaywright(input);
64
+ const finishedAt = new Date().toISOString();
65
+ return { ok: run.exitCode === 0, ...run, startedAt, finishedAt };
66
+ }
67
+
68
+ export async function ateReport(params: {
69
+ repoRoot: string;
70
+ runId: string;
71
+ startedAt: string;
72
+ finishedAt: string;
73
+ exitCode: number;
74
+ oracleLevel: OracleLevel;
75
+ impact?: { changedFiles: string[]; selectedRoutes: string[]; mode: "full" | "subset" };
76
+ format?: "json" | "html" | "both";
77
+ }) {
78
+ const { composeSummary, writeSummary, generateReport } = await import("./report");
79
+ const summary = composeSummary({
80
+ repoRoot: params.repoRoot,
81
+ runId: params.runId,
82
+ startedAt: params.startedAt,
83
+ finishedAt: params.finishedAt,
84
+ exitCode: params.exitCode,
85
+ oracleLevel: params.oracleLevel,
86
+ impact: params.impact,
87
+ });
88
+ const summaryPath = writeSummary(params.repoRoot, params.runId, summary);
89
+
90
+ const format = params.format ?? "both";
91
+ const reportPaths = await generateReport({
92
+ repoRoot: params.repoRoot,
93
+ runId: params.runId,
94
+ format,
95
+ });
96
+
97
+ return { ok: true, summaryPath, summary, reportPaths };
98
+ }
99
+
100
+ export async function ateImpact(input: ImpactInput) {
101
+ const { computeImpact } = await import("./impact");
102
+ const result = await computeImpact(input);
103
+ return { ok: true, ...result };
104
+ }
105
+
106
+ export async function ateHeal(input: HealInput) {
107
+ const { heal } = await import("./heal");
108
+ return { ok: true, ...heal(input) };
109
+ }
110
+
111
+ export { runFullPipeline } from "./pipeline";
112
+ export type { AutoPipelineOptions, AutoPipelineResult } from "./pipeline";
package/src/ir.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { InteractionGraph, InteractionNode, InteractionEdge } from "./types";
2
+
3
+ export function createEmptyGraph(buildSalt: string): InteractionGraph {
4
+ return {
5
+ schemaVersion: 1,
6
+ generatedAt: new Date().toISOString(),
7
+ buildSalt,
8
+ nodes: [],
9
+ edges: [],
10
+ stats: { routes: 0, navigations: 0, modals: 0, actions: 0 },
11
+ };
12
+ }
13
+
14
+ export function addNode(graph: InteractionGraph, node: InteractionNode): void {
15
+ graph.nodes.push(node);
16
+ if (node.kind === "route") graph.stats.routes++;
17
+ if (node.kind === "modal") graph.stats.modals++;
18
+ if (node.kind === "action") graph.stats.actions++;
19
+ }
20
+
21
+ export function addEdge(graph: InteractionGraph, edge: InteractionEdge): void {
22
+ graph.edges.push(edge);
23
+ if (edge.kind === "navigate") graph.stats.navigations++;
24
+ }