@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
@@ -0,0 +1,374 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.buildPlanFromImpact = buildPlanFromImpact;
6
+ exports.writePlanReport = writePlanReport;
7
+ exports.renderCiSummaryMarkdown = renderCiSummaryMarkdown;
8
+ exports.writeCiSummary = writeCiSummary;
9
+ const fs_1 = require("fs");
10
+ const path_1 = require("path");
11
+ const minimatch_1 = require("minimatch");
12
+ const impact_engine_js_1 = require("./impact_engine.js");
13
+ const DEFAULT_POLICY = {
14
+ minConfidenceForTargeted: 60,
15
+ safeMergeMinConfidence: 85,
16
+ forceFullOnWarningsAtOrAbove: 2,
17
+ forceFullOnP0WithGaps: true,
18
+ forceFullOnRiskyFiles: true,
19
+ riskyFilePatterns: [
20
+ '**/auth/**',
21
+ '**/login/**',
22
+ '**/permissions/**',
23
+ '**/admin/**',
24
+ '**/security/**',
25
+ '**/migrations/**',
26
+ '**/schema/**',
27
+ '**/*.sql',
28
+ '**/webhook/**',
29
+ ],
30
+ enforcementMode: 'advisory',
31
+ blockOnActions: ['must-add-tests'],
32
+ };
33
+ function featureLabel(f) {
34
+ return f.featureId || f.familyId;
35
+ }
36
+ function computeConfidence(impact) {
37
+ const gaps = (0, impact_engine_js_1.getGaps)(impact);
38
+ const totalFeatures = impact.impactedFeatures.length;
39
+ const boundRatio = totalFeatures > 0
40
+ ? (totalFeatures / (totalFeatures + impact.unboundFiles.length))
41
+ : 1;
42
+ // Graph-resolved bindings start at 100
43
+ let confidence = 100;
44
+ // Deduct for unbound files (not mapped to any family)
45
+ if (impact.unboundFiles.length > 0) {
46
+ const unboundPenalty = Math.min(30, impact.unboundFiles.length * 3);
47
+ confidence -= unboundPenalty;
48
+ }
49
+ // Deduct for gaps
50
+ confidence -= Math.min(20, gaps.length * 5);
51
+ // Deduct for warnings
52
+ confidence -= Math.min(15, impact.warnings.length * 5);
53
+ // Bonus for high bound ratio
54
+ if (boundRatio >= 0.9) {
55
+ confidence = Math.min(100, confidence + 5);
56
+ }
57
+ return Math.max(0, Math.min(100, confidence));
58
+ }
59
+ function findRiskyFiles(changedFiles, patterns) {
60
+ return [...new Set(changedFiles.filter((file) => patterns.some((pattern) => (0, minimatch_1.minimatch)(file, pattern, { matchBase: true }))))];
61
+ }
62
+ function pickRunSet(impact, confidence, policy) {
63
+ const gaps = (0, impact_engine_js_1.getGaps)(impact);
64
+ const reasons = [];
65
+ const triggeredRules = [];
66
+ const riskyFiles = findRiskyFiles(impact.changedFiles, policy.riskyFilePatterns);
67
+ const hasP0 = impact.impactedFeatures.some((f) => f.priority === 'P0');
68
+ if (gaps.length > 0) {
69
+ reasons.push(`${gaps.length} uncovered P0/P1 feature(s) detected.`);
70
+ }
71
+ if (hasP0) {
72
+ reasons.push('P0 features are impacted by this change set.');
73
+ }
74
+ if (policy.forceFullOnRiskyFiles && riskyFiles.length > 0) {
75
+ triggeredRules.push('risky-files');
76
+ reasons.push(`Risky file patterns matched: ${riskyFiles.join(', ')}`);
77
+ }
78
+ if (confidence < policy.minConfidenceForTargeted) {
79
+ triggeredRules.push('low-confidence');
80
+ }
81
+ if (impact.warnings.length >= policy.forceFullOnWarningsAtOrAbove) {
82
+ triggeredRules.push('warning-threshold');
83
+ reasons.push('Warning threshold exceeded.');
84
+ }
85
+ if (policy.forceFullOnP0WithGaps && hasP0 && gaps.length > 0) {
86
+ triggeredRules.push('p0-with-gaps');
87
+ }
88
+ if (triggeredRules.length > 0) {
89
+ return {
90
+ runSet: 'full',
91
+ reasons: reasons.length > 0 ? reasons : ['Policy rules triggered full suite.'],
92
+ triggeredRules,
93
+ riskyFiles,
94
+ };
95
+ }
96
+ // If we have impacted features with specs, recommend targeted
97
+ const coveredFeatures = impact.impactedFeatures.filter((f) => f.coverageStatus !== 'uncovered');
98
+ if (coveredFeatures.length > 0) {
99
+ return {
100
+ runSet: 'targeted',
101
+ reasons: reasons.length > 0 ? reasons : ['Impacted features have test coverage.'],
102
+ triggeredRules,
103
+ riskyFiles,
104
+ };
105
+ }
106
+ return {
107
+ runSet: 'smoke',
108
+ reasons: reasons.length > 0 ? reasons : ['No targeted tests mapped from impacted features.'],
109
+ triggeredRules,
110
+ riskyFiles,
111
+ };
112
+ }
113
+ function buildDecision(impact, runSet, confidence, policy) {
114
+ const gaps = (0, impact_engine_js_1.getGaps)(impact);
115
+ if (gaps.length > 0) {
116
+ return {
117
+ action: 'must-add-tests',
118
+ title: 'Must add tests',
119
+ summary: `Detected ${gaps.length} uncovered P0/P1 feature(s). Add or update tests before merge.`,
120
+ };
121
+ }
122
+ if (impact.changedFiles.length === 0 && impact.impactedFeatures.length === 0) {
123
+ return {
124
+ action: 'safe-to-merge',
125
+ title: 'Safe to merge',
126
+ summary: 'No app file changes detected — no E2E coverage required for this change set.',
127
+ };
128
+ }
129
+ if (runSet === 'smoke' && confidence >= policy.safeMergeMinConfidence && impact.warnings.length === 0) {
130
+ return {
131
+ action: 'safe-to-merge',
132
+ title: 'Safe to merge',
133
+ summary: 'No critical coverage gaps were detected and confidence is high.',
134
+ };
135
+ }
136
+ const coveredCount = impact.impactedFeatures.filter((f) => f.coverageStatus !== 'uncovered').length;
137
+ const coveredSuffix = coveredCount > 0
138
+ ? ` All ${coveredCount} impacted feature(s) have test coverage.`
139
+ : '';
140
+ return {
141
+ action: 'run-now',
142
+ title: 'Run now',
143
+ summary: `Impacted features are covered by existing tests.${coveredSuffix} Verify with the E2E suite before merge.`,
144
+ };
145
+ }
146
+ function evaluateEnforcement(decision, policy) {
147
+ const blockOnActions = (policy.blockOnActions && policy.blockOnActions.length > 0)
148
+ ? [...policy.blockOnActions]
149
+ : ['must-add-tests'];
150
+ const matchedAction = blockOnActions.includes(decision.action);
151
+ if (policy.enforcementMode === 'block' && matchedAction) {
152
+ return {
153
+ mode: policy.enforcementMode,
154
+ blockOnActions,
155
+ matchedAction,
156
+ shouldFail: true,
157
+ summary: `Blocking mode active: decision "${decision.action}" is configured to fail CI.`,
158
+ };
159
+ }
160
+ if (policy.enforcementMode === 'warn' && matchedAction) {
161
+ return {
162
+ mode: policy.enforcementMode,
163
+ blockOnActions,
164
+ matchedAction,
165
+ shouldFail: false,
166
+ summary: `Warning mode active: decision "${decision.action}" is advisory-only for CI.`,
167
+ };
168
+ }
169
+ if (policy.enforcementMode === 'block') {
170
+ return {
171
+ mode: policy.enforcementMode,
172
+ blockOnActions,
173
+ matchedAction,
174
+ shouldFail: false,
175
+ summary: `Blocking mode active, but decision "${decision.action}" is not configured for CI failure.`,
176
+ };
177
+ }
178
+ return {
179
+ mode: policy.enforcementMode,
180
+ blockOnActions,
181
+ matchedAction,
182
+ shouldFail: false,
183
+ summary: 'Advisory mode active: recommendations do not fail CI by default.',
184
+ };
185
+ }
186
+ /**
187
+ * Build recommended test list from impacted features' Playwright specs.
188
+ */
189
+ function buildRecommendedTests(impact) {
190
+ const tests = [];
191
+ for (const feature of impact.impactedFeatures) {
192
+ if (feature.coverageStatus !== 'uncovered') {
193
+ for (const spec of feature.playwrightSpecs) {
194
+ if (!tests.includes(spec)) {
195
+ tests.push(spec);
196
+ }
197
+ }
198
+ }
199
+ }
200
+ return tests;
201
+ }
202
+ function buildPlanFromImpact(impact, policyOverride, aiEnrichment) {
203
+ const policy = { ...DEFAULT_POLICY, ...(policyOverride || {}) };
204
+ const confidence = computeConfidence(impact);
205
+ const runSetResult = pickRunSet(impact, confidence, policy);
206
+ const decision = buildDecision(impact, runSetResult.runSet, confidence, policy);
207
+ const enforcement = evaluateEnforcement(decision, policy);
208
+ const gaps = (0, impact_engine_js_1.getGaps)(impact);
209
+ const partialGaps = (0, impact_engine_js_1.getPartialGaps)(impact);
210
+ // Build two separate lookup maps from aiEnrichment: one by featureId, one by familyId.
211
+ // The familyId map stores only the FIRST feature encountered to avoid last-write-wins collisions.
212
+ const aiFeatureByFeatureId = new Map();
213
+ const aiFeatureByFamilyId = new Map();
214
+ if (aiEnrichment) {
215
+ for (const ef of aiEnrichment.enrichedFeatures) {
216
+ if (ef.featureId) {
217
+ aiFeatureByFeatureId.set(ef.featureId, ef);
218
+ }
219
+ if (ef.familyId && !aiFeatureByFamilyId.has(ef.familyId)) {
220
+ aiFeatureByFamilyId.set(ef.familyId, ef);
221
+ }
222
+ }
223
+ }
224
+ const gapDetails = gaps.map((f) => {
225
+ const label = featureLabel(f);
226
+ const aiFeature = f.featureId
227
+ ? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
228
+ : aiFeatureByFamilyId.get(f.familyId);
229
+ const baseReasons = [`No Playwright or Cypress tests found for ${label}`];
230
+ const reasons = aiFeature && aiFeature.aiReasons.length > 0
231
+ ? [...baseReasons, ...aiFeature.aiReasons]
232
+ : baseReasons;
233
+ const missingScenarios = aiFeature && aiFeature.aiMissingScenarios.length > 0
234
+ ? aiFeature.aiMissingScenarios
235
+ : (f.userFlows.length > 0 ? f.userFlows.slice(0, 5) : undefined);
236
+ return {
237
+ id: label,
238
+ name: label,
239
+ priority: f.priority,
240
+ reasons,
241
+ files: f.changedFiles.slice(0, 6),
242
+ missingScenarios,
243
+ source: aiFeature ? 'ai+deterministic' : 'deterministic',
244
+ };
245
+ });
246
+ // Add partial gaps as advisory info
247
+ for (const f of partialGaps) {
248
+ const coverageType = f.playwrightSpecs.length > 0 ? 'Cypress' : 'Playwright';
249
+ const hasOpposite = f.playwrightSpecs.length > 0 ? 'Playwright' : 'Cypress';
250
+ const label = featureLabel(f);
251
+ const aiFeature = f.featureId
252
+ ? (aiFeatureByFeatureId.get(f.featureId) ?? aiFeatureByFamilyId.get(f.familyId))
253
+ : aiFeatureByFamilyId.get(f.familyId);
254
+ const baseReasons = [`Missing ${coverageType} tests for ${label} (has ${hasOpposite} only)`];
255
+ const reasons = aiFeature && aiFeature.aiReasons.length > 0
256
+ ? [...baseReasons, ...aiFeature.aiReasons]
257
+ : baseReasons;
258
+ gapDetails.push({
259
+ id: label,
260
+ name: `${label} (partial)`,
261
+ priority: f.priority,
262
+ reasons,
263
+ files: f.changedFiles.slice(0, 6),
264
+ source: aiFeature ? 'ai+deterministic' : 'deterministic',
265
+ });
266
+ }
267
+ const coveredFlows = impact.impactedFeatures
268
+ .filter((f) => f.coverageStatus === 'covered')
269
+ .map((f) => ({
270
+ id: featureLabel(f),
271
+ name: featureLabel(f),
272
+ priority: f.priority,
273
+ coveredBy: [
274
+ ...(f.playwrightSpecs.length > 0 ? [`${f.playwrightSpecs.length} Playwright spec(s)`] : []),
275
+ ...(f.cypressSpecs.length > 0 ? [`${f.cypressSpecs.length} Cypress spec(s)`] : []),
276
+ ].slice(0, 3),
277
+ }));
278
+ const recommendedTests = buildRecommendedTests(impact);
279
+ const requiredNewTests = gaps.map((f) => `${featureLabel(f)}: Add E2E tests`);
280
+ const p0 = impact.impactedFeatures.filter((f) => f.priority === 'P0').length;
281
+ const p1 = impact.impactedFeatures.filter((f) => f.priority === 'P1').length;
282
+ const p2 = impact.impactedFeatures.filter((f) => f.priority === 'P2').length;
283
+ const runId = `plan-${Date.now().toString(36)}`;
284
+ const planSource = aiEnrichment ? 'ai+deterministic' : 'impact';
285
+ return {
286
+ schemaVersion: '1.0.0',
287
+ runId,
288
+ generatedAt: new Date().toISOString(),
289
+ source: planSource,
290
+ runSet: runSetResult.runSet,
291
+ confidence,
292
+ reasons: runSetResult.reasons,
293
+ recommendedTests,
294
+ requiredNewTests,
295
+ gapDetails,
296
+ coveredFlows,
297
+ policy: {
298
+ riskyFiles: runSetResult.riskyFiles,
299
+ triggeredRules: runSetResult.triggeredRules,
300
+ applied: policy,
301
+ },
302
+ decision,
303
+ enforcement,
304
+ metrics: {
305
+ changedFiles: impact.changedFiles.length,
306
+ impactedFlows: impact.impactedFeatures.length,
307
+ p0Flows: p0,
308
+ p1Flows: p1,
309
+ p2Flows: p2,
310
+ uncoveredP0P1Flows: gaps.length,
311
+ warnings: impact.warnings.length,
312
+ },
313
+ };
314
+ }
315
+ function writePlanReport(appRoot, plan) {
316
+ const baseDir = (0, path_1.join)(appRoot, '.e2e-ai-agents');
317
+ (0, fs_1.mkdirSync)(baseDir, { recursive: true });
318
+ const planPath = (0, path_1.join)(baseDir, 'plan.json');
319
+ (0, fs_1.writeFileSync)(planPath, JSON.stringify(plan, null, 2), 'utf-8');
320
+ return planPath;
321
+ }
322
+ function renderCiSummaryMarkdown(plan) {
323
+ const lines = [];
324
+ const { p0Flows, p1Flows, uncoveredP0P1Flows, changedFiles, impactedFlows } = plan.metrics;
325
+ const mustAddTests = plan.decision.action === 'must-add-tests';
326
+ const statusEmoji = mustAddTests ? '🔴' : plan.decision.action === 'safe-to-merge' ? '🟢' : '🟡';
327
+ lines.push(`## ${statusEmoji} E2E Coverage: ${plan.decision.title}`);
328
+ lines.push('');
329
+ lines.push(`${plan.decision.summary}`);
330
+ lines.push('');
331
+ lines.push(`**${changedFiles}** files changed → **${impactedFlows}** features impacted` +
332
+ (p0Flows > 0 || p1Flows > 0 ? ` (P0: ${p0Flows}, P1: ${p1Flows})` : ''));
333
+ if (mustAddTests && plan.requiredNewTests.length > 0) {
334
+ lines.push('');
335
+ lines.push('### ⚠️ Add E2E tests for these uncovered P0/P1 features');
336
+ lines.push('');
337
+ lines.push(`The following ${uncoveredP0P1Flows} feature(s) have no test coverage and must be covered before merge:`);
338
+ lines.push('');
339
+ for (const gap of plan.gapDetails.filter((g) => !g.name.includes('(partial)'))) {
340
+ const aiLabel = gap.source === 'ai+deterministic' ? ' ✦ AI-enriched' : '';
341
+ lines.push(`- **${gap.name}** [${gap.priority}]${aiLabel}`);
342
+ if (gap.missingScenarios && gap.missingScenarios.length > 0) {
343
+ for (const scenario of gap.missingScenarios) {
344
+ lines.push(` - ${scenario}`);
345
+ }
346
+ }
347
+ // Show AI-provided reasons (skip the first deterministic reason which is always included)
348
+ const aiReasons = gap.reasons.slice(1);
349
+ if (aiReasons.length > 0) {
350
+ lines.push(` - *AI insight*: ${aiReasons.join('; ')}`);
351
+ }
352
+ }
353
+ }
354
+ if (plan.coveredFlows.length > 0) {
355
+ lines.push('');
356
+ lines.push('### ✅ Covered features');
357
+ lines.push('');
358
+ for (const flow of plan.coveredFlows) {
359
+ lines.push(`- **${flow.name}** [${flow.priority}] — ${flow.coveredBy.join(', ')}`);
360
+ }
361
+ }
362
+ if (plan.confidence < 100) {
363
+ lines.push('');
364
+ lines.push(`**Confidence**: ${plan.confidence}%`);
365
+ }
366
+ return lines.join('\n');
367
+ }
368
+ function writeCiSummary(appRoot, markdown, relativePath = '.e2e-ai-agents/ci-summary.md') {
369
+ const fullPath = (0, path_1.join)(appRoot, relativePath);
370
+ const dir = (0, path_1.dirname)(fullPath);
371
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
372
+ (0, fs_1.writeFileSync)(fullPath, markdown, 'utf-8');
373
+ return fullPath;
374
+ }