@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,235 @@
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.enrichImpactWithAI = enrichImpactWithAI;
6
+ const diff_loader_js_1 = require("./diff_loader.js");
7
+ const MAX_SPEC_LIST = 50;
8
+ function normalizePriority(value) {
9
+ if (value === 'P0' || value === 'P1' || value === 'P2') {
10
+ return value;
11
+ }
12
+ return 'P2';
13
+ }
14
+ function buildPrompt(options) {
15
+ const { deterministicImpact, diffs, specList, manifestSummary } = options;
16
+ const { changedFiles, impactedFeatures, unboundFiles } = deterministicImpact;
17
+ const lines = [];
18
+ // Optional manifest summary
19
+ if (manifestSummary) {
20
+ lines.push('## Application Overview');
21
+ lines.push(manifestSummary);
22
+ lines.push('');
23
+ }
24
+ // Changed files section
25
+ lines.push(`## Changed Files (${changedFiles.length} total)`);
26
+ for (const f of changedFiles) {
27
+ lines.push(`- ${f}`);
28
+ }
29
+ lines.push('');
30
+ // Diffs section
31
+ lines.push('## Code Diffs');
32
+ lines.push((0, diff_loader_js_1.formatDiffsForPrompt)(diffs));
33
+ lines.push('');
34
+ // Deterministic features summary
35
+ lines.push('## Deterministic Impact Analysis');
36
+ lines.push('The following features/flows have been deterministically identified as impacted:');
37
+ lines.push('');
38
+ for (const feature of impactedFeatures) {
39
+ const featureIdPart = feature.featureId ? `featureId=${feature.featureId}` : 'featureId=undefined';
40
+ const specCount = feature.playwrightSpecs.length + feature.cypressSpecs.length;
41
+ const specList2 = [...feature.playwrightSpecs, ...feature.cypressSpecs];
42
+ const specsDisplay = specList2.length > 0 ? specList2.join(', ') : 'none';
43
+ lines.push(`- familyId=${feature.familyId} ${featureIdPart} (${feature.priority}): ${specCount} files, coverage=${feature.coverageStatus}, specs=[${specsDisplay}]`);
44
+ }
45
+ lines.push('');
46
+ // Unbound files
47
+ if (unboundFiles.length > 0) {
48
+ lines.push('## Unbound Files (not mapped to any feature)');
49
+ for (const f of unboundFiles) {
50
+ lines.push(`- ${f}`);
51
+ }
52
+ lines.push('');
53
+ }
54
+ // Spec list (capped at 50)
55
+ if (specList.length > 0) {
56
+ const cappedSpecs = specList.slice(0, MAX_SPEC_LIST);
57
+ lines.push(`## Available Test Specs (showing ${cappedSpecs.length} of ${specList.length})`);
58
+ for (const s of cappedSpecs) {
59
+ lines.push(`- ${s}`);
60
+ }
61
+ lines.push('');
62
+ }
63
+ // Instructions
64
+ lines.push('## Instructions');
65
+ lines.push('Return ONLY valid JSON (no markdown fences, no explanation) in this exact shape:');
66
+ lines.push('');
67
+ lines.push(JSON.stringify({
68
+ impactedFlows: [
69
+ {
70
+ id: '<featureId or familyId from the deterministic list above>',
71
+ name: '<human-readable flow name>',
72
+ priority: 'P0|P1|P2',
73
+ reasons: ['<specific reason why this flow is impacted by the changes>'],
74
+ coveredBy: ['<spec file paths that cover this flow>'],
75
+ missingScenarios: ['<specific test scenarios that are missing or should be added>'],
76
+ },
77
+ ],
78
+ unboundFileAnalysis: [
79
+ {
80
+ file: '<path to unbound file>',
81
+ likelyFeature: '<best guess at which feature/family this affects>',
82
+ reason: '<why you think this file belongs to that feature>',
83
+ },
84
+ ],
85
+ }, null, 2));
86
+ return lines.join('\n');
87
+ }
88
+ function extractJSON(text) {
89
+ // Try markdown fenced block first
90
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
91
+ const candidates = fenced ? [fenced[1], text] : [text];
92
+ for (const candidate of candidates) {
93
+ const start = candidate.indexOf('{');
94
+ const end = candidate.lastIndexOf('}');
95
+ if (start >= 0 && end > start) {
96
+ return candidate.slice(start, end + 1).trim();
97
+ }
98
+ }
99
+ // Fallback: return trimmed text
100
+ return text.trim();
101
+ }
102
+ function toEnrichedFeature(det, aiFlow) {
103
+ return {
104
+ familyId: det.familyId,
105
+ featureId: det.featureId,
106
+ priority: normalizePriority(det.priority),
107
+ changedFiles: det.changedFiles,
108
+ coverageStatus: det.coverageStatus,
109
+ playwrightSpecs: det.playwrightSpecs,
110
+ cypressSpecs: det.cypressSpecs,
111
+ userFlows: det.userFlows,
112
+ aiReasons: aiFlow?.reasons ?? [],
113
+ aiMissingScenarios: aiFlow?.missingScenarios ?? [],
114
+ aiCoveredBy: aiFlow?.coveredBy ?? [],
115
+ };
116
+ }
117
+ /**
118
+ * Enriches a deterministic impact result with AI-generated reasons,
119
+ * missing test scenarios, and coverage insights.
120
+ */
121
+ async function enrichImpactWithAI(options) {
122
+ const { deterministicImpact, provider } = options;
123
+ const warnings = [];
124
+ let tokenUsage = { input: 0, output: 0 };
125
+ const prompt = buildPrompt(options);
126
+ let aiResponse = null;
127
+ let unboundFileInsights = [];
128
+ try {
129
+ const response = await provider.generateText(prompt, {
130
+ maxTokens: 4000,
131
+ temperature: 0,
132
+ timeout: 45000,
133
+ systemPrompt: 'You are an expert E2E test analyst. Return only valid JSON.',
134
+ });
135
+ tokenUsage = {
136
+ input: response.usage?.inputTokens ?? 0,
137
+ output: response.usage?.outputTokens ?? 0,
138
+ };
139
+ const rawJSON = extractJSON(response.text);
140
+ try {
141
+ const parsed = JSON.parse(rawJSON);
142
+ // Validate that impactedFlows is an array
143
+ if (!Array.isArray(parsed.impactedFlows)) {
144
+ warnings.push('AI response parsed but impactedFlows is not an array; returning empty enrichedFeatures');
145
+ return {
146
+ enrichedFeatures: [],
147
+ unboundFileInsights: [],
148
+ warnings,
149
+ providerName: provider.name,
150
+ tokenUsage,
151
+ };
152
+ }
153
+ aiResponse = parsed;
154
+ unboundFileInsights = (parsed.unboundFileAnalysis ?? []).map((item) => ({
155
+ file: item.file,
156
+ likelyFeature: item.likelyFeature,
157
+ reason: item.reason,
158
+ }));
159
+ }
160
+ catch (parseErr) {
161
+ warnings.push(`Failed to parse AI response as JSON: ${parseErr instanceof Error ? parseErr.message : String(parseErr)}`);
162
+ return {
163
+ enrichedFeatures: [],
164
+ unboundFileInsights: [],
165
+ warnings,
166
+ providerName: provider.name,
167
+ tokenUsage,
168
+ };
169
+ }
170
+ }
171
+ catch (err) {
172
+ warnings.push(`AI provider error: ${err instanceof Error ? err.message : String(err)}`);
173
+ return {
174
+ enrichedFeatures: [],
175
+ unboundFileInsights: [],
176
+ warnings,
177
+ providerName: provider.name,
178
+ tokenUsage,
179
+ };
180
+ }
181
+ // Build a map of AI flows by id (featureId or familyId)
182
+ const aiFlowMap = new Map();
183
+ if (aiResponse?.impactedFlows) {
184
+ for (const flow of aiResponse.impactedFlows) {
185
+ aiFlowMap.set(flow.id, flow);
186
+ }
187
+ }
188
+ // Build a set of all deterministic ids for unmatched-flow detection
189
+ const deterministicIds = new Set();
190
+ for (const det of deterministicImpact.impactedFeatures) {
191
+ if (det.featureId) {
192
+ deterministicIds.add(det.featureId);
193
+ }
194
+ deterministicIds.add(det.familyId);
195
+ }
196
+ // Warn on AI flows that don't match any deterministic feature
197
+ for (const flow of aiFlowMap.values()) {
198
+ if (!deterministicIds.has(flow.id)) {
199
+ warnings.push(`AI returned flow '${flow.id}' with no matching deterministic feature (using as-is)`);
200
+ }
201
+ }
202
+ // Merge deterministic features with AI data
203
+ const enrichedFeatures = deterministicImpact.impactedFeatures.map((det) => {
204
+ // Match by featureId first, then by familyId
205
+ const aiFlow = det.featureId
206
+ ? (aiFlowMap.get(det.featureId) ?? aiFlowMap.get(det.familyId))
207
+ : aiFlowMap.get(det.familyId);
208
+ return toEnrichedFeature(det, aiFlow);
209
+ });
210
+ // Include AI flows that had no deterministic match (as-is, with empty deterministic fields)
211
+ for (const flow of aiFlowMap.values()) {
212
+ if (!deterministicIds.has(flow.id)) {
213
+ enrichedFeatures.push({
214
+ familyId: flow.id,
215
+ featureId: undefined,
216
+ priority: normalizePriority(flow.priority),
217
+ changedFiles: [],
218
+ coverageStatus: 'uncovered',
219
+ playwrightSpecs: [],
220
+ cypressSpecs: [],
221
+ userFlows: [],
222
+ aiReasons: flow.reasons ?? [],
223
+ aiMissingScenarios: flow.missingScenarios ?? [],
224
+ aiCoveredBy: flow.coveredBy ?? [],
225
+ });
226
+ }
227
+ }
228
+ return {
229
+ enrichedFeatures,
230
+ unboundFileInsights,
231
+ warnings,
232
+ providerName: provider.name,
233
+ tokenUsage,
234
+ };
235
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Loads git diffs for the given changed files relative to the given since ref.
3
+ * Uses `git merge-base` to find the accurate base ref first.
4
+ * Individual diffs are truncated at 8000 chars and total output is capped at 60000 chars.
5
+ */
6
+ export declare function loadDiffs(appRoot: string, since: string, changedFiles: string[]): Map<string, string>;
7
+ /**
8
+ * Formats a diffs map into a human-readable string suitable for an AI prompt.
9
+ */
10
+ export declare function formatDiffsForPrompt(diffs: Map<string, string>): string;
11
+ //# sourceMappingURL=diff_loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff_loader.d.ts","sourceRoot":"","sources":["../../src/engine/diff_loader.ts"],"names":[],"mappings":"AAqBA;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CA0CrG;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAUvE"}
@@ -0,0 +1,74 @@
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.loadDiffs = loadDiffs;
6
+ exports.formatDiffsForPrompt = formatDiffsForPrompt;
7
+ const child_process_1 = require("child_process");
8
+ const MAX_DIFF_CHARS = 8000;
9
+ const MAX_TOTAL_CHARS = 60000;
10
+ const TRUNCATION_NOTICE = '\n... (diff truncated)';
11
+ function runGitRaw(args, cwd) {
12
+ const result = (0, child_process_1.spawnSync)('git', args, {
13
+ cwd,
14
+ encoding: 'utf-8',
15
+ timeout: 30000,
16
+ });
17
+ if (result.error || result.status !== 0) {
18
+ return null;
19
+ }
20
+ return result.stdout;
21
+ }
22
+ /**
23
+ * Loads git diffs for the given changed files relative to the given since ref.
24
+ * Uses `git merge-base` to find the accurate base ref first.
25
+ * Individual diffs are truncated at 8000 chars and total output is capped at 60000 chars.
26
+ */
27
+ function loadDiffs(appRoot, since, changedFiles) {
28
+ const result = new Map();
29
+ if (changedFiles.length === 0) {
30
+ return result;
31
+ }
32
+ // Try to get accurate merge base
33
+ let baseRef = since;
34
+ const mergeBaseOutput = runGitRaw(['merge-base', since, 'HEAD'], appRoot);
35
+ if (mergeBaseOutput) {
36
+ const candidate = mergeBaseOutput
37
+ .split('\n')
38
+ .map((line) => line.trim())
39
+ .find(Boolean);
40
+ if (candidate) {
41
+ baseRef = candidate;
42
+ }
43
+ }
44
+ let totalChars = 0;
45
+ for (const file of changedFiles) {
46
+ if (totalChars >= MAX_TOTAL_CHARS) {
47
+ break;
48
+ }
49
+ const diffOutput = runGitRaw(['diff', `${baseRef}..HEAD`, '--', file], appRoot);
50
+ if (diffOutput === null) {
51
+ continue;
52
+ }
53
+ let diff = diffOutput;
54
+ if (diff.length > MAX_DIFF_CHARS) {
55
+ diff = diff.slice(0, MAX_DIFF_CHARS) + TRUNCATION_NOTICE;
56
+ }
57
+ result.set(file, diff);
58
+ totalChars += diff.length;
59
+ }
60
+ return result;
61
+ }
62
+ /**
63
+ * Formats a diffs map into a human-readable string suitable for an AI prompt.
64
+ */
65
+ function formatDiffsForPrompt(diffs) {
66
+ if (diffs.size === 0) {
67
+ return 'No diffs available.';
68
+ }
69
+ const sections = [];
70
+ for (const [file, diff] of diffs) {
71
+ sections.push(`--- ${file} ---\n${diff}`);
72
+ }
73
+ return sections.join('\n\n');
74
+ }
@@ -0,0 +1,36 @@
1
+ import type { FeaturePriority } from '../knowledge/route_families.js';
2
+ import type { RouteFamiliesConfig } from '../agent/config.js';
3
+ export type CoverageStatus = 'covered' | 'partial' | 'uncovered';
4
+ export interface ImpactedFeature {
5
+ familyId: string;
6
+ featureId?: string;
7
+ priority: FeaturePriority;
8
+ changedFiles: string[];
9
+ playwrightSpecs: string[];
10
+ cypressSpecs: string[];
11
+ userFlows: string[];
12
+ coverageStatus: CoverageStatus;
13
+ }
14
+ export interface ImpactResult {
15
+ changedFiles: string[];
16
+ expandedFiles: string[];
17
+ impactedFeatures: ImpactedFeature[];
18
+ unboundFiles: string[];
19
+ warnings: string[];
20
+ }
21
+ export interface ImpactEngineOptions {
22
+ testsRoot: string;
23
+ cypressRoot?: string;
24
+ routeFamilies?: RouteFamiliesConfig;
25
+ expandedFiles?: string[];
26
+ }
27
+ export declare function analyzeImpact(changedFiles: string[], options: ImpactEngineOptions): ImpactResult;
28
+ /**
29
+ * Get gaps: P0/P1 features with 'uncovered' status.
30
+ */
31
+ export declare function getGaps(result: ImpactResult): ImpactedFeature[];
32
+ /**
33
+ * Get partial gaps: P0/P1 features with 'partial' status (advisory).
34
+ */
35
+ export declare function getPartialGaps(result: ImpactResult): ImpactedFeature[];
36
+ //# sourceMappingURL=impact_engine.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"impact_engine.d.ts","sourceRoot":"","sources":["../../src/engine/impact_engine.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAGR,eAAe,EAClB,MAAM,gCAAgC,CAAC;AASxC,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,oBAAoB,CAAC;AAE5D,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,eAAe,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,cAAc,EAAE,cAAc,CAAC;CAClC;AAED,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,gBAAgB,EAAE,eAAe,EAAE,CAAC;IACpC,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,mBAAmB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC5B;AAuGD,wBAAgB,aAAa,CACzB,YAAY,EAAE,MAAM,EAAE,EACtB,OAAO,EAAE,mBAAmB,GAC7B,YAAY,CA2Ed;AAYD;;GAEG;AACH,wBAAgB,OAAO,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAI/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,YAAY,GAAG,eAAe,EAAE,CAItE"}
@@ -0,0 +1,196 @@
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.analyzeImpact = analyzeImpact;
6
+ exports.getGaps = getGaps;
7
+ exports.getPartialGaps = getPartialGaps;
8
+ const fs_1 = require("fs");
9
+ const path_1 = require("path");
10
+ const route_families_js_1 = require("../knowledge/route_families.js");
11
+ function scanDirForSpecs(baseDir, specDir, extension) {
12
+ const fullDir = (0, path_1.join)(baseDir, specDir);
13
+ if (!(0, fs_1.existsSync)(fullDir)) {
14
+ return [];
15
+ }
16
+ const specs = [];
17
+ try {
18
+ const items = (0, fs_1.readdirSync)(fullDir, { withFileTypes: true });
19
+ for (const item of items) {
20
+ const itemPath = (0, path_1.join)(fullDir, item.name);
21
+ if (item.isDirectory()) {
22
+ specs.push(...scanDirForSpecsRecursive(itemPath, extension));
23
+ }
24
+ else if (item.name.endsWith(extension)) {
25
+ specs.push((0, path_1.join)(specDir, item.name));
26
+ }
27
+ }
28
+ }
29
+ catch {
30
+ // Directory not readable
31
+ }
32
+ return specs;
33
+ }
34
+ function scanDirForSpecsRecursive(dir, extension) {
35
+ const specs = [];
36
+ try {
37
+ const items = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
38
+ for (const item of items) {
39
+ const fullPath = (0, path_1.join)(dir, item.name);
40
+ if (item.isDirectory()) {
41
+ specs.push(...scanDirForSpecsRecursive(fullPath, extension));
42
+ }
43
+ else if (item.name.endsWith(extension)) {
44
+ specs.push(fullPath);
45
+ }
46
+ }
47
+ }
48
+ catch {
49
+ // Directory not readable
50
+ }
51
+ return specs;
52
+ }
53
+ function resolvePlaywrightSpecs(testsRoot, specDirs) {
54
+ const specs = [];
55
+ for (const dir of specDirs) {
56
+ specs.push(...scanDirForSpecs(testsRoot, dir, '.spec.ts'));
57
+ }
58
+ return specs;
59
+ }
60
+ function resolveCypressSpecs(cypressRoot, specDirs) {
61
+ const specs = [];
62
+ for (const dir of specDirs) {
63
+ // cypressSpecDirs are relative to testsRoot (e.g. ../cypress/tests/integration/channels/search/)
64
+ // Resolve them relative to the cypress root
65
+ const resolvedDir = (0, path_1.join)(cypressRoot, dir.replace(/^\.\.\/cypress\//, ''));
66
+ if (!(0, fs_1.existsSync)(resolvedDir)) {
67
+ continue;
68
+ }
69
+ const found = scanDirForSpecsRecursive(resolvedDir, '.js');
70
+ const tsFound = scanDirForSpecsRecursive(resolvedDir, '.ts');
71
+ specs.push(...found, ...tsFound);
72
+ }
73
+ return specs;
74
+ }
75
+ function computeCoverageStatus(pwSpecs, cySpecs) {
76
+ const hasPw = pwSpecs.length > 0;
77
+ const hasCy = cySpecs.length > 0;
78
+ if (hasPw && hasCy) {
79
+ return 'covered';
80
+ }
81
+ if (hasPw || hasCy) {
82
+ return 'partial';
83
+ }
84
+ return 'uncovered';
85
+ }
86
+ /**
87
+ * Group file bindings into a deduplicated map of family/feature → changed files.
88
+ */
89
+ function groupBindings(fileBindings) {
90
+ const groups = new Map();
91
+ for (const fb of fileBindings) {
92
+ for (const binding of fb.bindings) {
93
+ const key = binding.feature || binding.family;
94
+ const existing = groups.get(key);
95
+ if (existing) {
96
+ if (!existing.files.includes(fb.file)) {
97
+ existing.files.push(fb.file);
98
+ }
99
+ }
100
+ else {
101
+ groups.set(key, {
102
+ familyId: binding.family,
103
+ featureId: binding.feature,
104
+ files: [fb.file],
105
+ });
106
+ }
107
+ }
108
+ }
109
+ return groups;
110
+ }
111
+ function analyzeImpact(changedFiles, options) {
112
+ const { testsRoot, routeFamilies } = options;
113
+ const warnings = [];
114
+ // Load manifest
115
+ const manifest = (0, route_families_js_1.loadRouteFamilyManifest)(testsRoot, routeFamilies);
116
+ if (!manifest) {
117
+ return {
118
+ changedFiles,
119
+ expandedFiles: options.expandedFiles || [],
120
+ impactedFeatures: [],
121
+ unboundFiles: [...changedFiles],
122
+ warnings: ['Route family manifest not found. All files are unbound.'],
123
+ };
124
+ }
125
+ // Combine original + expanded files
126
+ const allFiles = [...new Set([...changedFiles, ...(options.expandedFiles || [])])];
127
+ // Bind files to families
128
+ const fileBindings = (0, route_families_js_1.bindFilesToFamilies)(allFiles, manifest);
129
+ // Find unbound files
130
+ const unboundFiles = fileBindings
131
+ .filter((fb) => fb.bindings.length === 0)
132
+ .map((fb) => fb.file);
133
+ // Group bindings into features
134
+ const groups = groupBindings(fileBindings.filter((fb) => fb.bindings.length > 0));
135
+ // Determine cypress root
136
+ const cypressRoot = options.cypressRoot || inferCypressRoot(testsRoot);
137
+ // Resolve specs and compute coverage for each feature
138
+ const impactedFeatures = [];
139
+ for (const group of groups.values()) {
140
+ const binding = { family: group.familyId, feature: group.featureId };
141
+ const specDirs = (0, route_families_js_1.getSpecDirsForBinding)(manifest, binding);
142
+ const cypressSpecDirs = (0, route_families_js_1.getCypressSpecDirsForBinding)(manifest, binding);
143
+ const priority = (0, route_families_js_1.getPriorityForBinding)(manifest, binding);
144
+ const userFlows = (0, route_families_js_1.getUserFlowsForBinding)(manifest, binding);
145
+ const playwrightSpecs = resolvePlaywrightSpecs(testsRoot, specDirs);
146
+ const cypressSpecs = cypressRoot ? resolveCypressSpecs(cypressRoot, cypressSpecDirs) : [];
147
+ const coverageStatus = computeCoverageStatus(playwrightSpecs, cypressSpecs);
148
+ impactedFeatures.push({
149
+ familyId: group.familyId,
150
+ featureId: group.featureId,
151
+ priority,
152
+ changedFiles: group.files,
153
+ playwrightSpecs,
154
+ cypressSpecs,
155
+ userFlows,
156
+ coverageStatus,
157
+ });
158
+ }
159
+ // Sort by priority (P0 first, then P1, then P2)
160
+ const priorityOrder = { P0: 0, P1: 1, P2: 2 };
161
+ impactedFeatures.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
162
+ if (unboundFiles.length > 0 && unboundFiles.length <= 5) {
163
+ warnings.push(`${unboundFiles.length} file(s) not mapped to any route family: ${unboundFiles.join(', ')}`);
164
+ }
165
+ else if (unboundFiles.length > 5) {
166
+ warnings.push(`${unboundFiles.length} file(s) not mapped to any route family`);
167
+ }
168
+ return {
169
+ changedFiles,
170
+ expandedFiles: options.expandedFiles || [],
171
+ impactedFeatures,
172
+ unboundFiles,
173
+ warnings,
174
+ };
175
+ }
176
+ function inferCypressRoot(testsRoot) {
177
+ // testsRoot is typically the Playwright tests directory
178
+ // Cypress tests are at a sibling path: e2e-tests/cypress/tests/integration/channels/
179
+ const candidate = (0, path_1.join)(testsRoot, '..', 'cypress');
180
+ if ((0, fs_1.existsSync)(candidate)) {
181
+ return candidate;
182
+ }
183
+ return undefined;
184
+ }
185
+ /**
186
+ * Get gaps: P0/P1 features with 'uncovered' status.
187
+ */
188
+ function getGaps(result) {
189
+ return result.impactedFeatures.filter((f) => (f.priority === 'P0' || f.priority === 'P1') && f.coverageStatus === 'uncovered');
190
+ }
191
+ /**
192
+ * Get partial gaps: P0/P1 features with 'partial' status (advisory).
193
+ */
194
+ function getPartialGaps(result) {
195
+ return result.impactedFeatures.filter((f) => (f.priority === 'P0' || f.priority === 'P1') && f.coverageStatus === 'partial');
196
+ }
@@ -0,0 +1,10 @@
1
+ import type { PolicyConfig } from '../agent/config.js';
2
+ import type { ImpactResult } from './impact_engine.js';
3
+ import type { AIEnrichmentResult } from './ai_enrichment.js';
4
+ import type { PlanReport, GapDetail, CoveredFlowSummary } from '../agent/plan.js';
5
+ export type { PlanReport, GapDetail, CoveredFlowSummary };
6
+ export declare function buildPlanFromImpact(impact: ImpactResult, policyOverride?: Partial<PolicyConfig>, aiEnrichment?: AIEnrichmentResult): PlanReport;
7
+ export declare function writePlanReport(appRoot: string, plan: PlanReport): string;
8
+ export declare function renderCiSummaryMarkdown(plan: PlanReport): string;
9
+ export declare function writeCiSummary(appRoot: string, markdown: string, relativePath?: string): string;
10
+ //# sourceMappingURL=plan_builder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plan_builder.d.ts","sourceRoot":"","sources":["../../src/engine/plan_builder.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrD,OAAO,KAAK,EAAC,YAAY,EAAkB,MAAM,oBAAoB,CAAC;AAEtE,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,oBAAoB,CAAC;AAG3D,OAAO,KAAK,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAIrB,MAAM,kBAAkB,CAAC;AAE1B,YAAY,EAAC,UAAU,EAAE,SAAS,EAAE,kBAAkB,EAAC,CAAC;AAqOxD,wBAAgB,mBAAmB,CAC/B,MAAM,EAAE,YAAY,EACpB,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,EACtC,YAAY,CAAC,EAAE,kBAAkB,GAClC,UAAU,CA8HZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CAoDhE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}