@yasserkhanorg/e2e-agents 0.5.15 → 0.6.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 (105) 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 +0 -12
  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 +10 -14
  10. package/dist/api.d.ts.map +1 -1
  11. package/dist/api.js +29 -59
  12. package/dist/cli.js +41 -174
  13. package/dist/engine/impact_engine.d.ts +36 -0
  14. package/dist/engine/impact_engine.d.ts.map +1 -0
  15. package/dist/engine/impact_engine.js +196 -0
  16. package/dist/engine/plan_builder.d.ts +9 -0
  17. package/dist/engine/plan_builder.d.ts.map +1 -0
  18. package/dist/engine/plan_builder.js +329 -0
  19. package/dist/esm/agent/plan.js +1 -360
  20. package/dist/esm/agent/types.js +3 -0
  21. package/dist/esm/api.js +27 -56
  22. package/dist/esm/cli.js +40 -173
  23. package/dist/esm/engine/impact_engine.js +191 -0
  24. package/dist/esm/engine/plan_builder.js +323 -0
  25. package/dist/esm/index.js +6 -3
  26. package/dist/esm/knowledge/route_families.js +57 -0
  27. package/dist/index.d.ts +9 -4
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +14 -5
  30. package/dist/knowledge/route_families.d.ts +19 -0
  31. package/dist/knowledge/route_families.d.ts.map +1 -1
  32. package/dist/knowledge/route_families.js +60 -0
  33. package/package.json +1 -1
  34. package/dist/agent/ai_flow_analysis.d.ts +0 -13
  35. package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
  36. package/dist/agent/ai_flow_analysis.js +0 -334
  37. package/dist/agent/ai_mapping.d.ts +0 -14
  38. package/dist/agent/ai_mapping.d.ts.map +0 -1
  39. package/dist/agent/ai_mapping.js +0 -559
  40. package/dist/agent/analysis.d.ts +0 -64
  41. package/dist/agent/analysis.d.ts.map +0 -1
  42. package/dist/agent/analysis.js +0 -292
  43. package/dist/agent/blast_radius.d.ts +0 -4
  44. package/dist/agent/blast_radius.d.ts.map +0 -1
  45. package/dist/agent/blast_radius.js +0 -37
  46. package/dist/agent/dependency_graph.d.ts +0 -14
  47. package/dist/agent/dependency_graph.d.ts.map +0 -1
  48. package/dist/agent/dependency_graph.js +0 -227
  49. package/dist/agent/flags.d.ts +0 -23
  50. package/dist/agent/flags.d.ts.map +0 -1
  51. package/dist/agent/flags.js +0 -171
  52. package/dist/agent/flow_catalog.d.ts +0 -25
  53. package/dist/agent/flow_catalog.d.ts.map +0 -1
  54. package/dist/agent/flow_catalog.js +0 -115
  55. package/dist/agent/flow_mapping.d.ts +0 -10
  56. package/dist/agent/flow_mapping.d.ts.map +0 -1
  57. package/dist/agent/flow_mapping.js +0 -84
  58. package/dist/agent/framework.d.ts +0 -13
  59. package/dist/agent/framework.d.ts.map +0 -1
  60. package/dist/agent/framework.js +0 -149
  61. package/dist/agent/gap_suggestions.d.ts +0 -14
  62. package/dist/agent/gap_suggestions.d.ts.map +0 -1
  63. package/dist/agent/gap_suggestions.js +0 -101
  64. package/dist/agent/generator.d.ts +0 -10
  65. package/dist/agent/generator.d.ts.map +0 -1
  66. package/dist/agent/generator.js +0 -115
  67. package/dist/agent/operational_insights.d.ts +0 -41
  68. package/dist/agent/operational_insights.d.ts.map +0 -1
  69. package/dist/agent/operational_insights.js +0 -127
  70. package/dist/agent/report.d.ts +0 -97
  71. package/dist/agent/report.d.ts.map +0 -1
  72. package/dist/agent/report.js +0 -159
  73. package/dist/agent/runner.d.ts +0 -7
  74. package/dist/agent/runner.d.ts.map +0 -1
  75. package/dist/agent/runner.js +0 -898
  76. package/dist/agent/selectors.d.ts +0 -10
  77. package/dist/agent/selectors.d.ts.map +0 -1
  78. package/dist/agent/selectors.js +0 -75
  79. package/dist/agent/subsystem_risk.d.ts +0 -23
  80. package/dist/agent/subsystem_risk.d.ts.map +0 -1
  81. package/dist/agent/subsystem_risk.js +0 -207
  82. package/dist/agent/tests.d.ts +0 -19
  83. package/dist/agent/tests.d.ts.map +0 -1
  84. package/dist/agent/tests.js +0 -116
  85. package/dist/agent/traceability.d.ts +0 -22
  86. package/dist/agent/traceability.d.ts.map +0 -1
  87. package/dist/agent/traceability.js +0 -183
  88. package/dist/esm/agent/ai_flow_analysis.js +0 -331
  89. package/dist/esm/agent/ai_mapping.js +0 -556
  90. package/dist/esm/agent/analysis.js +0 -287
  91. package/dist/esm/agent/blast_radius.js +0 -34
  92. package/dist/esm/agent/dependency_graph.js +0 -224
  93. package/dist/esm/agent/flags.js +0 -160
  94. package/dist/esm/agent/flow_catalog.js +0 -112
  95. package/dist/esm/agent/flow_mapping.js +0 -81
  96. package/dist/esm/agent/framework.js +0 -145
  97. package/dist/esm/agent/gap_suggestions.js +0 -98
  98. package/dist/esm/agent/generator.js +0 -112
  99. package/dist/esm/agent/operational_insights.js +0 -124
  100. package/dist/esm/agent/report.js +0 -156
  101. package/dist/esm/agent/runner.js +0 -894
  102. package/dist/esm/agent/selectors.js +0 -71
  103. package/dist/esm/agent/subsystem_risk.js +0 -204
  104. package/dist/esm/agent/tests.js +0 -111
  105. 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
- }