@yasserkhanorg/e2e-agents 0.5.16 → 0.7.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 (113) hide show
  1. package/dist/agent/pipeline.d.ts +1 -1
  2. package/dist/agent/pipeline.d.ts.map +1 -1
  3. package/dist/agent/plan.d.ts +2 -13
  4. package/dist/agent/plan.d.ts.map +1 -1
  5. package/dist/agent/plan.js +0 -365
  6. package/dist/agent/types.d.ts +42 -0
  7. package/dist/agent/types.d.ts.map +1 -0
  8. package/dist/agent/types.js +4 -0
  9. package/dist/api.d.ts +14 -14
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +67 -59
  12. package/dist/cli.js +86 -176
  13. package/dist/engine/ai_enrichment.d.ts +43 -0
  14. package/dist/engine/ai_enrichment.d.ts.map +1 -0
  15. package/dist/engine/ai_enrichment.js +235 -0
  16. package/dist/engine/diff_loader.d.ts +11 -0
  17. package/dist/engine/diff_loader.d.ts.map +1 -0
  18. package/dist/engine/diff_loader.js +74 -0
  19. package/dist/engine/impact_engine.d.ts +36 -0
  20. package/dist/engine/impact_engine.d.ts.map +1 -0
  21. package/dist/engine/impact_engine.js +196 -0
  22. package/dist/engine/plan_builder.d.ts +10 -0
  23. package/dist/engine/plan_builder.d.ts.map +1 -0
  24. package/dist/engine/plan_builder.js +374 -0
  25. package/dist/esm/agent/plan.js +1 -360
  26. package/dist/esm/agent/types.js +3 -0
  27. package/dist/esm/api.js +62 -54
  28. package/dist/esm/cli.js +87 -177
  29. package/dist/esm/engine/ai_enrichment.js +232 -0
  30. package/dist/esm/engine/diff_loader.js +70 -0
  31. package/dist/esm/engine/impact_engine.js +191 -0
  32. package/dist/esm/engine/plan_builder.js +368 -0
  33. package/dist/esm/index.js +6 -3
  34. package/dist/esm/knowledge/route_families.js +59 -1
  35. package/dist/index.d.ts +9 -4
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +14 -5
  38. package/dist/knowledge/route_families.d.ts +19 -0
  39. package/dist/knowledge/route_families.d.ts.map +1 -1
  40. package/dist/knowledge/route_families.js +62 -1
  41. package/package.json +1 -1
  42. package/dist/agent/ai_flow_analysis.d.ts +0 -13
  43. package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
  44. package/dist/agent/ai_flow_analysis.js +0 -334
  45. package/dist/agent/ai_mapping.d.ts +0 -14
  46. package/dist/agent/ai_mapping.d.ts.map +0 -1
  47. package/dist/agent/ai_mapping.js +0 -560
  48. package/dist/agent/analysis.d.ts +0 -64
  49. package/dist/agent/analysis.d.ts.map +0 -1
  50. package/dist/agent/analysis.js +0 -292
  51. package/dist/agent/blast_radius.d.ts +0 -4
  52. package/dist/agent/blast_radius.d.ts.map +0 -1
  53. package/dist/agent/blast_radius.js +0 -37
  54. package/dist/agent/dependency_graph.d.ts +0 -14
  55. package/dist/agent/dependency_graph.d.ts.map +0 -1
  56. package/dist/agent/dependency_graph.js +0 -227
  57. package/dist/agent/flags.d.ts +0 -23
  58. package/dist/agent/flags.d.ts.map +0 -1
  59. package/dist/agent/flags.js +0 -171
  60. package/dist/agent/flow_catalog.d.ts +0 -25
  61. package/dist/agent/flow_catalog.d.ts.map +0 -1
  62. package/dist/agent/flow_catalog.js +0 -115
  63. package/dist/agent/flow_mapping.d.ts +0 -10
  64. package/dist/agent/flow_mapping.d.ts.map +0 -1
  65. package/dist/agent/flow_mapping.js +0 -84
  66. package/dist/agent/framework.d.ts +0 -13
  67. package/dist/agent/framework.d.ts.map +0 -1
  68. package/dist/agent/framework.js +0 -149
  69. package/dist/agent/gap_suggestions.d.ts +0 -14
  70. package/dist/agent/gap_suggestions.d.ts.map +0 -1
  71. package/dist/agent/gap_suggestions.js +0 -101
  72. package/dist/agent/generator.d.ts +0 -10
  73. package/dist/agent/generator.d.ts.map +0 -1
  74. package/dist/agent/generator.js +0 -115
  75. package/dist/agent/operational_insights.d.ts +0 -41
  76. package/dist/agent/operational_insights.d.ts.map +0 -1
  77. package/dist/agent/operational_insights.js +0 -127
  78. package/dist/agent/report.d.ts +0 -97
  79. package/dist/agent/report.d.ts.map +0 -1
  80. package/dist/agent/report.js +0 -159
  81. package/dist/agent/runner.d.ts +0 -7
  82. package/dist/agent/runner.d.ts.map +0 -1
  83. package/dist/agent/runner.js +0 -898
  84. package/dist/agent/selectors.d.ts +0 -10
  85. package/dist/agent/selectors.d.ts.map +0 -1
  86. package/dist/agent/selectors.js +0 -75
  87. package/dist/agent/subsystem_risk.d.ts +0 -23
  88. package/dist/agent/subsystem_risk.d.ts.map +0 -1
  89. package/dist/agent/subsystem_risk.js +0 -207
  90. package/dist/agent/tests.d.ts +0 -19
  91. package/dist/agent/tests.d.ts.map +0 -1
  92. package/dist/agent/tests.js +0 -116
  93. package/dist/agent/traceability.d.ts +0 -22
  94. package/dist/agent/traceability.d.ts.map +0 -1
  95. package/dist/agent/traceability.js +0 -183
  96. package/dist/esm/agent/ai_flow_analysis.js +0 -331
  97. package/dist/esm/agent/ai_mapping.js +0 -557
  98. package/dist/esm/agent/analysis.js +0 -287
  99. package/dist/esm/agent/blast_radius.js +0 -34
  100. package/dist/esm/agent/dependency_graph.js +0 -224
  101. package/dist/esm/agent/flags.js +0 -160
  102. package/dist/esm/agent/flow_catalog.js +0 -112
  103. package/dist/esm/agent/flow_mapping.js +0 -81
  104. package/dist/esm/agent/framework.js +0 -145
  105. package/dist/esm/agent/gap_suggestions.js +0 -98
  106. package/dist/esm/agent/generator.js +0 -112
  107. package/dist/esm/agent/operational_insights.js +0 -124
  108. package/dist/esm/agent/report.js +0 -156
  109. package/dist/esm/agent/runner.js +0 -894
  110. package/dist/esm/agent/selectors.js +0 -71
  111. package/dist/esm/agent/subsystem_risk.js +0 -204
  112. package/dist/esm/agent/tests.js +0 -111
  113. package/dist/esm/agent/traceability.js +0 -180
@@ -1,112 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { existsSync, mkdirSync, writeFileSync } from 'fs';
4
- import { join, resolve } from 'path';
5
- import { isPathWithinRoot } from './utils.js';
6
- function inferTestDir(patterns) {
7
- if (patterns.length === 0) {
8
- return 'tests';
9
- }
10
- const pattern = patterns[0];
11
- const wildcardIndex = pattern.search(/[*{]/);
12
- const base = wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
13
- const trimmed = base.replace(/\/+$/, '');
14
- return trimmed || 'tests';
15
- }
16
- function inferTestExtension(patterns, framework) {
17
- const joined = patterns.join(' ');
18
- if (joined.includes('.ts') || joined.includes('.tsx')) {
19
- return 'ts';
20
- }
21
- return 'js';
22
- }
23
- function createPlaywrightTest(flow, testIds) {
24
- const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
25
- return [
26
- "import {test, expect} from '@mattermost/playwright-lib';",
27
- '',
28
- '/**',
29
- ` * @objective Validate ${flow.name} flow`,
30
- ' */',
31
- `test('${flow.priority}: ${flow.name} basic flow', {tag: '@ai-assisted'}, async ({pw}) => {`,
32
- ' const {user, team} = await pw.initSetup();',
33
- ' const {channelsPage} = await pw.testBrowser.login(user);',
34
- " await channelsPage.goto(team.name);",
35
- ` ${idsComment}`,
36
- ' // # TODO: implement steps',
37
- ' // * TODO: implement assertions',
38
- ' await expect(channelsPage.page).toHaveURL(/.*/);',
39
- '});',
40
- '',
41
- ].join('\n');
42
- }
43
- function createCypressTest(flow, testIds) {
44
- const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
45
- return [
46
- `describe('Flow: ${flow.name}', () => {`,
47
- ` it('${flow.priority}: ${flow.name} basic flow', () => {`,
48
- " cy.visit('/');",
49
- ` ${idsComment}`,
50
- ' // TODO: implement steps',
51
- ' cy.url().should(\'match\', /.*/);',
52
- ' });',
53
- '});',
54
- '',
55
- ].join('\n');
56
- }
57
- function createSeleniumTest(flow, testIds) {
58
- const idsComment = testIds.length > 0 ? `// Suggested data-testid: ${testIds.join(', ')}` : '// TODO: add data-testid selectors';
59
- return [
60
- "const {Builder, By, until} = require('selenium-webdriver');",
61
- '',
62
- `(async () => {`,
63
- " const driver = await new Builder().forBrowser('chrome').build();",
64
- ' try {',
65
- " await driver.get('http://localhost:3000');",
66
- ` ${idsComment}`,
67
- ' // TODO: implement steps',
68
- ' await driver.wait(until.titleIs(\'\'), 5000);',
69
- ' } finally {',
70
- ' await driver.quit();',
71
- ' }',
72
- '})();',
73
- '',
74
- ].join('\n');
75
- }
76
- export function generateTests(appRoot, flows, framework, testPatterns, testIdsByFlow) {
77
- const inferredTestDir = inferTestDir(testPatterns);
78
- const safeTestDir = isPathWithinRoot(appRoot, resolve(appRoot, inferredTestDir)) ? inferredTestDir : 'tests';
79
- const testDir = safeTestDir;
80
- const extension = inferTestExtension(testPatterns, framework);
81
- const generated = [];
82
- for (const flow of flows) {
83
- if (flow.priority !== 'P0' && flow.priority !== 'P1') {
84
- continue;
85
- }
86
- const testIds = testIdsByFlow.get(flow.id) || [];
87
- const fileName = framework === 'cypress' ? `${flow.id}.cy.${extension}` : `${flow.id}.spec.${extension}`;
88
- const fullPath = resolve(appRoot, testDir, fileName);
89
- if (!isPathWithinRoot(appRoot, fullPath)) {
90
- generated.push({ path: fullPath, flowId: flow.id, created: false, reason: 'outside-root' });
91
- continue;
92
- }
93
- if (existsSync(fullPath)) {
94
- generated.push({ path: fullPath, flowId: flow.id, created: false, reason: 'exists' });
95
- continue;
96
- }
97
- mkdirSync(join(appRoot, testDir), { recursive: true });
98
- let content = '';
99
- if (framework === 'cypress') {
100
- content = createCypressTest(flow, testIds);
101
- }
102
- else if (framework === 'selenium') {
103
- content = createSeleniumTest(flow, testIds);
104
- }
105
- else {
106
- content = createPlaywrightTest(flow, testIds);
107
- }
108
- writeFileSync(fullPath, content, 'utf-8');
109
- generated.push({ path: fullPath, flowId: flow.id, created: true });
110
- }
111
- return generated;
112
- }
@@ -1,124 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { existsSync, readFileSync } from 'fs';
4
- import { join } from 'path';
5
- import { refreshPlanEnforcement } from './plan.js';
6
- import { inferSubsystemFromTestPath } from './test_path.js';
7
- function readJson(path) {
8
- if (!existsSync(path)) {
9
- return null;
10
- }
11
- try {
12
- return JSON.parse(readFileSync(path, 'utf-8'));
13
- }
14
- catch {
15
- return null;
16
- }
17
- }
18
- function normalizeTestName(test) {
19
- return test.replace(/ \(flags:.*\)$/, '').trim();
20
- }
21
- function subsystemForTest(test) {
22
- return inferSubsystemFromTestPath(test);
23
- }
24
- function riskyRate(entry) {
25
- if (entry.flakeRate30d !== undefined) {
26
- return entry.flakeRate30d;
27
- }
28
- return entry.flakeRate;
29
- }
30
- function loadFlakyManifest(appRoot) {
31
- const path = join(appRoot, '.e2e-ai-agents', 'flaky-tests.json');
32
- return readJson(path);
33
- }
34
- function loadQualityGates(appRoot) {
35
- const path = join(appRoot, '.e2e-ai-agents', 'quality-gates.json');
36
- return readJson(path);
37
- }
38
- function loadCalibration(appRoot) {
39
- const path = join(appRoot, '.e2e-ai-agents', 'calibration.json');
40
- return readJson(path);
41
- }
42
- export function applyOperationalInsights(plan, appRoot) {
43
- const enhanced = { ...plan };
44
- const insights = {};
45
- const flaky = loadFlakyManifest(appRoot);
46
- if (flaky && Array.isArray(flaky.tests)) {
47
- const recommended = new Set(plan.recommendedTests.map(normalizeTestName));
48
- const risky = flaky.tests
49
- .filter((entry) => recommended.has(normalizeTestName(entry.test)) && riskyRate(entry) >= 0.2)
50
- .sort((a, b) => riskyRate(b) - riskyRate(a))
51
- .slice(0, 10);
52
- const quarantined = risky.filter((entry) => entry.quarantine).map((entry) => entry.test);
53
- const owners = Array.from(new Set(risky
54
- .flatMap((entry) => entry.owners || [])
55
- .filter(Boolean)));
56
- insights.flaky = {
57
- highRiskRecommendedTests: risky,
58
- quarantinedRecommendedTests: quarantined,
59
- ownerMentions: owners,
60
- };
61
- if (quarantined.length > 0) {
62
- enhanced.reasons = [...enhanced.reasons, `Quarantined flaky tests in recommendation: ${quarantined.join(', ')}`];
63
- }
64
- if (owners.length > 0) {
65
- enhanced.reasons = [...enhanced.reasons, `Subsystem owners to notify for flaky risk: ${owners.join(', ')}`];
66
- }
67
- }
68
- const gates = loadQualityGates(appRoot);
69
- if (gates && Array.isArray(gates.gates)) {
70
- const failed = gates.gates.filter((gate) => gate.status === 'fail');
71
- const warnings = gates.gates.filter((gate) => gate.status === 'warn');
72
- insights.qualityGates = { failed, warnings };
73
- if (failed.length > 0 && enhanced.runSet !== 'full') {
74
- enhanced.runSet = 'full';
75
- enhanced.reasons = [...enhanced.reasons, `Quality gates failed: ${failed.map((gate) => gate.name).join(', ')}`];
76
- enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'quality-gate-failed'])];
77
- enhanced.decision = {
78
- action: 'run-now',
79
- title: 'Run now',
80
- summary: 'Quality gate failures detected. Full suite is required before merge.',
81
- };
82
- }
83
- }
84
- const calibration = loadCalibration(appRoot);
85
- if (calibration) {
86
- insights.calibration = calibration.overall;
87
- if (calibration.overall.falseNegativeRate >= 0.2 && enhanced.runSet !== 'full') {
88
- enhanced.runSet = 'full';
89
- enhanced.reasons = [...enhanced.reasons, 'Historical false-negative rate is high; escalating to full suite.'];
90
- enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'historical-fnr-high'])];
91
- }
92
- const recommendedSubsystems = Array.from(new Set(plan.recommendedTests.map(subsystemForTest)));
93
- const highRiskSubsystems = recommendedSubsystems
94
- .map((subsystem) => {
95
- const metric = calibration.bySubsystem[subsystem];
96
- if (!metric) {
97
- return null;
98
- }
99
- if (metric.samples < 5) {
100
- return null;
101
- }
102
- if (metric.recent30d.falseNegativeRate >= 0.2 || metric.falseNegativeRate >= 0.25) {
103
- return { subsystem, fnr: metric.recent30d.falseNegativeRate || metric.falseNegativeRate };
104
- }
105
- return null;
106
- })
107
- .filter(Boolean);
108
- if (highRiskSubsystems.length > 0 && enhanced.runSet !== 'full') {
109
- enhanced.runSet = 'full';
110
- enhanced.reasons = [
111
- ...enhanced.reasons,
112
- `Historical subsystem false-negative risk is high: ${highRiskSubsystems.map((entry) => `${entry.subsystem}(${entry.fnr})`).join(', ')}`,
113
- ];
114
- enhanced.policy.triggeredRules = [...new Set([...enhanced.policy.triggeredRules, 'subsystem-fnr-high'])];
115
- enhanced.decision = {
116
- action: 'run-now',
117
- title: 'Run now',
118
- summary: 'Subsystem calibration risk is high. Full suite is required before merge.',
119
- };
120
- }
121
- }
122
- enhanced.insights = insights;
123
- return refreshPlanEnforcement(enhanced);
124
- }
@@ -1,156 +0,0 @@
1
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
- // See LICENSE.txt for license information.
3
- import { mkdirSync, writeFileSync } from 'fs';
4
- import { join } from 'path';
5
- import { formatFlags } from './flags.js';
6
- function formatFlow(flow) {
7
- const reasonText = flow.reasons.length > 0 ? flow.reasons.join('; ') : 'No specific reasons';
8
- const audienceText = flow.audience && flow.audience.length > 0 ? `\n Audience: ${flow.audience.join(', ')}` : '';
9
- const flagsText = flow.flags && flow.flags.length > 0 ? `\n Flags: ${formatFlags(flow.flags)}` : '';
10
- const blastText = flow.blastRadius ? `\n Blast radius: ${flow.blastRadius.summary}` : '';
11
- return `- [${flow.priority}] ${flow.name} (${flow.id})\n Score: ${flow.score}\n Reasons: ${reasonText}\n Files: ${flow.files.join(', ')}${audienceText}${flagsText}${blastText}`;
12
- }
13
- function formatGap(flow) {
14
- return `- [${flow.priority}] ${flow.name} (${flow.id})`;
15
- }
16
- function formatSuggestion(suggestion) {
17
- return `- ${suggestion.file}:${suggestion.line} -> ${suggestion.testId}\n ${suggestion.snippet}`;
18
- }
19
- function formatTestSuggestion(suggestion) {
20
- const source = suggestion.sourceFiles.length > 0 ? suggestion.sourceFiles.join(', ') : 'N/A';
21
- return `- [${suggestion.priority}] ${suggestion.flowName} (${suggestion.flowId})\n Path: ${suggestion.suggestedTestPath}\n Source files: ${source}\n Why: ${suggestion.rationale}`;
22
- }
23
- function flowCounts(flows) {
24
- return flows.reduce((acc, flow) => {
25
- if (flow.priority === 'P0')
26
- acc.p0 += 1;
27
- else if (flow.priority === 'P1')
28
- acc.p1 += 1;
29
- else
30
- acc.p2 += 1;
31
- return acc;
32
- }, { p0: 0, p1: 0, p2: 0 });
33
- }
34
- export function writeReport(appRoot, config, data) {
35
- const specsDir = join(appRoot, config.artifacts.specsDir);
36
- const baseDir = join(appRoot, '.e2e-ai-agents');
37
- if (config.artifacts.mode !== 'none') {
38
- mkdirSync(specsDir, { recursive: true });
39
- }
40
- mkdirSync(baseDir, { recursive: true });
41
- const counts = flowCounts(data.flows);
42
- const markdownLines = [];
43
- markdownLines.push(`# ${data.mode === 'impact' ? 'Impact Analysis' : 'Gap Analysis'} Report`);
44
- markdownLines.push('');
45
- if (data.runMetadata) {
46
- markdownLines.push(`Run ID: ${data.runMetadata.runId}`);
47
- markdownLines.push(`Run window: ${data.runMetadata.startedAt} -> ${data.runMetadata.completedAt}`);
48
- markdownLines.push(`Run duration (ms): ${data.runMetadata.durationMs}`);
49
- markdownLines.push(`Since ref: ${data.runMetadata.sinceRef}`);
50
- }
51
- markdownLines.push(`Framework: ${data.framework}`);
52
- markdownLines.push(`Test Patterns: ${data.testPatterns.join(', ') || 'None'}`);
53
- if (data.flowCatalog) {
54
- markdownLines.push(`Flow Catalog: ${data.flowCatalog}`);
55
- }
56
- if (data.impactModel) {
57
- markdownLines.push(`Impact Model: flow=${data.impactModel.flowMapping} test=${data.impactModel.testMapping} confidence=${data.impactModel.confidenceClass}`);
58
- if (data.impactModel.traceability) {
59
- const traceability = data.impactModel.traceability;
60
- markdownLines.push(`Traceability: enabled=${traceability.enabled} manifestFound=${traceability.manifestFound} matchedFlows=${traceability.matchedFlows}/${traceability.totalFlows} matchedTests=${traceability.matchedTests} coverageRatio=${traceability.coverageRatio}`);
61
- }
62
- if (data.impactModel.dependencyGraph) {
63
- const graph = data.impactModel.dependencyGraph;
64
- markdownLines.push(`Dependency Graph: enabled=${graph.enabled} seeds=${graph.seedFiles} expanded=${graph.expandedFiles} files=${graph.analyzedFiles} edges=${graph.analyzedEdges} depth=${graph.maxDepth}${graph.truncated ? ' (truncated)' : ''}`);
65
- }
66
- if (data.impactModel.subsystemRisk) {
67
- const subsystemRisk = data.impactModel.subsystemRisk;
68
- markdownLines.push(`Subsystem Risk: enabled=${subsystemRisk.enabled} mapFound=${subsystemRisk.mapFound} rules=${subsystemRisk.rulesLoaded} filesMatched=${subsystemRisk.filesMatched} ruleMatches=${subsystemRisk.ruleMatches} boostedFlows=${subsystemRisk.boostedFlows}`);
69
- }
70
- }
71
- markdownLines.push(`Changed Files: ${data.changedFiles.length}`);
72
- markdownLines.push(`Flows: P0=${counts.p0} P1=${counts.p1} P2=${counts.p2}`);
73
- if (data.specPDF) {
74
- markdownLines.push(`Spec PDF: ${data.specPDF}`);
75
- }
76
- if (data.warnings.length > 0) {
77
- markdownLines.push('');
78
- markdownLines.push('Warnings:');
79
- markdownLines.push(...data.warnings.map((warning) => `- ${warning}`));
80
- }
81
- if (data.flows.length > 0) {
82
- markdownLines.push('');
83
- markdownLines.push('Impacted Flows:');
84
- markdownLines.push(...data.flows.map(formatFlow));
85
- }
86
- if (data.gaps.length > 0) {
87
- markdownLines.push('');
88
- markdownLines.push('Coverage Gaps (P0/P1 without tests):');
89
- markdownLines.push(...data.gaps.map(formatGap));
90
- }
91
- if (data.recommendedTests && data.recommendedTests.length > 0) {
92
- markdownLines.push('');
93
- markdownLines.push('Recommended Tests to Run:');
94
- markdownLines.push(...data.recommendedTests.map((test) => `- ${test}`));
95
- }
96
- if (data.pipeline) {
97
- markdownLines.push('');
98
- markdownLines.push('Pipeline Results:');
99
- markdownLines.push(`- Runner: ${data.pipeline.runner}`);
100
- if (data.pipeline.mcp) {
101
- markdownLines.push(`- MCP: requested=${data.pipeline.mcp.requested} active=${data.pipeline.mcp.active} backend=${data.pipeline.mcp.backend}`);
102
- }
103
- for (const result of data.pipeline.results) {
104
- const status = result.healStatus ? `${result.generateStatus}/${result.healStatus}` : result.generateStatus;
105
- markdownLines.push(`- ${result.flowId} (${result.flowName}): ${status} -> ${result.generatedDir}`);
106
- if (result.error) {
107
- markdownLines.push(` Error: ${result.error}`);
108
- }
109
- if (result.failureCategory || result.failureCode) {
110
- markdownLines.push(` Failure taxonomy: category=${result.failureCategory || 'unknown'} code=${result.failureCode || 'unknown'}`);
111
- }
112
- }
113
- if (data.pipeline.warnings.length > 0) {
114
- markdownLines.push('Pipeline warnings:');
115
- markdownLines.push(...data.pipeline.warnings.map((warning) => `- ${warning}`));
116
- }
117
- }
118
- if (data.dataTestIds.length > 0) {
119
- markdownLines.push('');
120
- markdownLines.push('data-testid Suggestions:');
121
- markdownLines.push(...data.dataTestIds.map(formatSuggestion));
122
- }
123
- if (data.testSuggestions && data.testSuggestions.length > 0) {
124
- markdownLines.push('');
125
- markdownLines.push('Suggested New Tests (Actionable):');
126
- markdownLines.push(...data.testSuggestions.map(formatTestSuggestion));
127
- }
128
- if (data.applied) {
129
- markdownLines.push('');
130
- markdownLines.push('Applied Changes:');
131
- if (data.applied.patchedFiles.length > 0) {
132
- markdownLines.push(`- Patched files: ${data.applied.patchedFiles.join(', ')}`);
133
- }
134
- if (data.applied.generatedTests.length > 0) {
135
- markdownLines.push(`- Generated tests: ${data.applied.generatedTests.join(', ')}`);
136
- }
137
- if (data.applied.skippedTests.length > 0) {
138
- markdownLines.push(`- Skipped test files: ${data.applied.skippedTests.join(', ')}`);
139
- }
140
- }
141
- const markdownContent = markdownLines.join('\n');
142
- const reportName = data.mode === 'impact' ? 'impact-plan.md' : 'gap-report.md';
143
- const markdownPath = join(specsDir, reportName);
144
- if (config.artifacts.mode !== 'none') {
145
- writeFileSync(markdownPath, markdownContent, 'utf-8');
146
- }
147
- const jsonPath = join(baseDir, data.mode === 'impact' ? 'impact.json' : 'gap.json');
148
- const jsonData = data.mode === 'gap'
149
- ? {
150
- ...data,
151
- suggestedNewTests: data.suggestedNewTests || data.testSuggestions || [],
152
- }
153
- : data;
154
- writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
155
- return { markdownPath, jsonPath };
156
- }