@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,560 +0,0 @@
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.mapAITestsToFlows = mapAITestsToFlows;
6
- const fs_1 = require("fs");
7
- const path_1 = require("path");
8
- const provider_factory_js_1 = require("../provider_factory.js");
9
- const utils_js_1 = require("./utils.js");
10
- const PRIORITY_RANK = {
11
- P0: 0,
12
- P1: 1,
13
- P2: 2,
14
- };
15
- const MIN_SINGLE_KEYWORD_LENGTH = 6;
16
- const LOW_SIGNAL_FLOW_KEYWORDS = new Set([
17
- 'app',
18
- 'apps',
19
- 'channel',
20
- 'channels',
21
- 'client',
22
- 'common',
23
- 'component',
24
- 'components',
25
- 'detail',
26
- 'details',
27
- 'dialog',
28
- 'feature',
29
- 'files',
30
- 'flow',
31
- 'group',
32
- 'groups',
33
- 'hooks',
34
- 'message',
35
- 'messages',
36
- 'modal',
37
- 'new',
38
- 'page',
39
- 'pages',
40
- 'panel',
41
- 'post',
42
- 'posts',
43
- 'query',
44
- 'result',
45
- 'results',
46
- 'screen',
47
- 'screens',
48
- 'section',
49
- 'src',
50
- 'tsx',
51
- 'ts',
52
- 'jsx',
53
- 'js',
54
- 'ui',
55
- 'use',
56
- 'user',
57
- 'users',
58
- 'view',
59
- 'webapp',
60
- ]);
61
- function extractJson(text) {
62
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
63
- const candidates = fenced ? [fenced[1], text] : [text];
64
- for (const candidate of candidates) {
65
- const start = candidate.indexOf('{');
66
- const end = candidate.lastIndexOf('}');
67
- if (start < 0 || end <= start) {
68
- continue;
69
- }
70
- const raw = candidate.slice(start, end + 1);
71
- try {
72
- const parsed = JSON.parse(raw);
73
- if (parsed && Array.isArray(parsed.mappings)) {
74
- return parsed;
75
- }
76
- }
77
- catch {
78
- // Continue trying other candidates.
79
- }
80
- }
81
- return null;
82
- }
83
- function resolveContextFiles(appRoot, testsRoot, files) {
84
- const resolved = [];
85
- const seen = new Set();
86
- const maxCharsPerFile = 12000;
87
- const maxTotalChars = 30000;
88
- let totalChars = 0;
89
- for (const file of files) {
90
- const candidates = (0, path_1.isAbsolute)(file)
91
- ? [file]
92
- : [(0, path_1.join)(testsRoot, file), (0, path_1.join)(appRoot, file)];
93
- for (const candidate of candidates) {
94
- const normalized = (0, utils_js_1.normalizePath)(candidate);
95
- if (seen.has(normalized) || !(0, fs_1.existsSync)(candidate)) {
96
- continue;
97
- }
98
- const content = (0, fs_1.readFileSync)(candidate, 'utf-8');
99
- const trimmed = content.trim();
100
- if (!trimmed) {
101
- seen.add(normalized);
102
- continue;
103
- }
104
- const remaining = Math.max(0, maxTotalChars - totalChars);
105
- if (remaining <= 0) {
106
- return resolved;
107
- }
108
- const clipped = trimmed.slice(0, Math.min(maxCharsPerFile, remaining));
109
- resolved.push({ path: normalized, content: clipped });
110
- seen.add(normalized);
111
- totalChars += clipped.length;
112
- break;
113
- }
114
- }
115
- return resolved;
116
- }
117
- function flowKeywords(flow) {
118
- return (0, utils_js_1.uniqueTokens)([
119
- ...(0, utils_js_1.tokenize)(flow.id || ''),
120
- ...(0, utils_js_1.tokenize)(flow.name || ''),
121
- ...(flow.keywords || []),
122
- ]).filter((keyword) => (keyword.length >= 3 &&
123
- !LOW_SIGNAL_FLOW_KEYWORDS.has(keyword))).slice(0, 18);
124
- }
125
- function matchedFlowKeywords(flow, testPath) {
126
- const haystack = testPath.toLowerCase();
127
- return flowKeywords(flow).filter((keyword) => keyword && haystack.includes(keyword.toLowerCase()));
128
- }
129
- function scoreTestPath(flow, testPath) {
130
- return matchedFlowKeywords(flow, testPath).length;
131
- }
132
- function isStrongCandidateMatch(flow, matchedKeywords) {
133
- if (matchedKeywords.length >= 2) {
134
- return true;
135
- }
136
- if (matchedKeywords.length !== 1) {
137
- return false;
138
- }
139
- const keywords = flowKeywords(flow);
140
- return keywords.length === 1 && matchedKeywords[0].length >= MIN_SINGLE_KEYWORD_LENGTH;
141
- }
142
- // Stop-words excluded from content-fallback keyword matching.
143
- const CONTENT_FALLBACK_STOP_WORDS = new Set(['and', 'for', 'the', 'to', 'of', 'on', 'at', 'with', 'in', 'a', 'an']);
144
- // Returns raw (unfiltered) tokens for flows where flowKeywords() returns nothing.
145
- // Used exclusively for content-title matching when all standard keywords are low-signal.
146
- // Empty array is returned when the flow already has effective path keywords.
147
- function contentFallbackKeywords(flow) {
148
- if (flowKeywords(flow).length > 0) {
149
- return [];
150
- }
151
- return (0, utils_js_1.uniqueTokens)([
152
- ...(0, utils_js_1.tokenize)(flow.id || ''),
153
- ...(0, utils_js_1.tokenize)(flow.name || ''),
154
- ...(flow.keywords || []),
155
- ]).filter((k) => k.length >= 3 && !CONTENT_FALLBACK_STOP_WORDS.has(k));
156
- }
157
- // Extract test/describe/it title strings from file content for semantic matching.
158
- function extractTestTitles(content) {
159
- const titles = [];
160
- const pattern = /(?:^|\s)(?:test|it|describe)\s*\(\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)/gm;
161
- let match;
162
- while ((match = pattern.exec(content)) !== null) {
163
- const title = match[1] ?? match[2] ?? match[3];
164
- if (title) {
165
- titles.push(title);
166
- }
167
- }
168
- return titles.join(' ');
169
- }
170
- function matchedFlowKeywordsInTitles(flow, testContent) {
171
- const haystack = extractTestTitles(testContent).toLowerCase();
172
- if (!haystack) {
173
- return [];
174
- }
175
- return flowKeywords(flow).filter((keyword) => keyword && haystack.includes(keyword.toLowerCase()));
176
- }
177
- function selectCandidateTests(flows, tests, maxCandidateTests) {
178
- const selected = new Set();
179
- const byFlow = new Map();
180
- const evidence = [];
181
- const warnings = [];
182
- const normalizedTests = tests.map((test) => (0, utils_js_1.normalizePath)(test.path)).filter(Boolean);
183
- const testByNormalizedPath = new Map(tests.map((t) => [(0, utils_js_1.normalizePath)(t.path), t]));
184
- const perFlowLimit = Math.max(2, Math.min(6, Math.floor(maxCandidateTests / Math.max(1, flows.length))));
185
- for (const flow of flows) {
186
- // Pass 1: path-keyword matching
187
- const scored = [];
188
- for (const testPath of normalizedTests) {
189
- const matchedKeywords = matchedFlowKeywords(flow, testPath);
190
- if (matchedKeywords.length === 0) {
191
- continue;
192
- }
193
- scored.push({
194
- path: testPath,
195
- score: matchedKeywords.length,
196
- matchedKeywords,
197
- });
198
- }
199
- const strongCandidates = scored
200
- .filter((candidate) => isStrongCandidateMatch(flow, candidate.matchedKeywords))
201
- .sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
202
- .slice(0, perFlowLimit);
203
- // Pass 2: content-title matching
204
- // For flows without enough path-matched candidates, also search test/describe/it
205
- // title strings for flow keywords. This surfaces semantically related tests even
206
- // when the file name does not match the flow (e.g. a search.spec.ts with a test
207
- // titled "search for message in channel" covers search_messages).
208
- const contentCandidates = [];
209
- if (strongCandidates.length < perFlowLimit) {
210
- const alreadyByPath = new Set(strongCandidates.map((c) => c.path));
211
- for (const testPath of normalizedTests) {
212
- if (alreadyByPath.has(testPath)) {
213
- continue;
214
- }
215
- const testFile = testByNormalizedPath.get(testPath);
216
- if (!testFile?.content) {
217
- continue;
218
- }
219
- const titleKeywords = matchedFlowKeywordsInTitles(flow, testFile.content);
220
- if (!isStrongCandidateMatch(flow, titleKeywords)) {
221
- continue;
222
- }
223
- // Score content matches lower than path matches so path candidates rank higher.
224
- contentCandidates.push({
225
- path: testPath,
226
- score: titleKeywords.length,
227
- matchedKeywords: titleKeywords,
228
- });
229
- }
230
- contentCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
231
- }
232
- // Pass 2b: comprehensive fallback for all-low-signal flows.
233
- // When flowKeywords() is empty (all tokens are low-signal), flowKeywords-based
234
- // matching in both Pass 1 and Pass 2 yields nothing. As a last resort, search
235
- // test titles using the raw unfiltered tokens from the flow ID/name, but require
236
- // ALL tokens to match simultaneously — they are individually weak signals so the
237
- // full conjunction is needed to establish behavioral coverage evidence.
238
- const fallbackCandidates = [];
239
- if (strongCandidates.length === 0 && contentCandidates.length === 0) {
240
- const fallbackKws = contentFallbackKeywords(flow);
241
- if (fallbackKws.length > 0) {
242
- for (const testPath of normalizedTests) {
243
- const testFile = testByNormalizedPath.get(testPath);
244
- if (!testFile?.content) {
245
- continue;
246
- }
247
- const haystack = extractTestTitles(testFile.content).toLowerCase();
248
- if (!haystack) {
249
- continue;
250
- }
251
- const matched = fallbackKws.filter((k) => haystack.includes(k));
252
- // For 3+ tokens, n-1 must match (allows one absent word like "view");
253
- // for 1-2 tokens all must match.
254
- const required = fallbackKws.length >= 3 ? fallbackKws.length - 1 : fallbackKws.length;
255
- if (matched.length < required) {
256
- continue;
257
- }
258
- fallbackCandidates.push({ path: testPath, score: matched.length, matchedKeywords: matched });
259
- }
260
- fallbackCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
261
- }
262
- }
263
- const allCandidates = [
264
- ...strongCandidates,
265
- ...contentCandidates.slice(0, perFlowLimit),
266
- ...fallbackCandidates.slice(0, perFlowLimit),
267
- ];
268
- if (allCandidates.length === 0) {
269
- // Exact-name fallback: if the flow ID has no effective keywords (all tokens are
270
- // low-signal, e.g. view_user_group_modal), look for a test whose path contains
271
- // the exact flow ID as a directory name or filename without extension.
272
- const exactMatchPath = normalizedTests.find((testPath) => {
273
- const segments = testPath.split('/');
274
- return segments.some((seg) => seg === flow.id || seg.replace(/\.spec\.[tj]sx?$/, '') === flow.id);
275
- });
276
- if (exactMatchPath) {
277
- byFlow.set(flow.id, new Set([exactMatchPath]));
278
- evidence.push({ flowId: flow.id, candidates: [{ path: exactMatchPath, score: 999, matchedKeywords: [flow.id] }] });
279
- selected.add(exactMatchPath);
280
- }
281
- else if (scored.length > 0) {
282
- warnings.push(`AI mapping withheld weak path-only candidates for ${flow.id}; traceability evidence is required to reuse existing tests.`);
283
- }
284
- continue;
285
- }
286
- byFlow.set(flow.id, new Set(allCandidates.map((candidate) => candidate.path)));
287
- evidence.push({ flowId: flow.id, candidates: allCandidates });
288
- for (const candidate of allCandidates) {
289
- selected.add(candidate.path);
290
- }
291
- }
292
- return {
293
- tests: Array.from(selected).sort((a, b) => a.localeCompare(b)).slice(0, maxCandidateTests),
294
- byFlow,
295
- evidence,
296
- warnings,
297
- };
298
- }
299
- function readCandidateTestContents(testsRoot, testPaths) {
300
- const result = [];
301
- const maxCharsPerFile = 6000;
302
- const maxTotalChars = 24000;
303
- let totalChars = 0;
304
- for (const testPath of testPaths.slice(0, 6)) {
305
- if (totalChars >= maxTotalChars) {
306
- break;
307
- }
308
- const candidates = (0, path_1.isAbsolute)(testPath) ? [testPath] : [(0, path_1.join)(testsRoot, testPath)];
309
- for (const fullPath of candidates) {
310
- if (!(0, fs_1.existsSync)(fullPath)) {
311
- continue;
312
- }
313
- const content = (0, fs_1.readFileSync)(fullPath, 'utf-8').trim();
314
- if (!content) {
315
- continue;
316
- }
317
- const remaining = Math.max(0, maxTotalChars - totalChars);
318
- const clipped = content.slice(0, Math.min(maxCharsPerFile, remaining));
319
- result.push({ path: testPath, content: clipped });
320
- totalChars += clipped.length;
321
- break;
322
- }
323
- }
324
- return result;
325
- }
326
- function buildCoverage(flows, mapped, scenarioGaps) {
327
- return flows.map((flow) => ({
328
- flowId: flow.id,
329
- flowName: flow.name,
330
- priority: flow.priority,
331
- coveredBy: mapped.get(flow.id) || [],
332
- score: (mapped.get(flow.id) || []).length,
333
- source: 'ai',
334
- missingScenarios: scenarioGaps.get(flow.id) || [],
335
- }));
336
- }
337
- function providerFor(config) {
338
- if (config.provider === 'auto') {
339
- return 'auto';
340
- }
341
- return config.provider;
342
- }
343
- async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
344
- const warnings = [];
345
- const providerName = providerFor(config);
346
- if (!config.enabled) {
347
- return {
348
- enabled: false,
349
- used: false,
350
- provider: providerName,
351
- mappedFlows: 0,
352
- matchedTests: 0,
353
- coverage: [],
354
- warnings,
355
- };
356
- }
357
- const prioritizedFlows = [...flows]
358
- .sort((a, b) => {
359
- const prioDiff = (PRIORITY_RANK[a.priority] ?? 3) - (PRIORITY_RANK[b.priority] ?? 3);
360
- if (prioDiff !== 0) {
361
- return prioDiff;
362
- }
363
- return (b.score || 0) - (a.score || 0);
364
- })
365
- .slice(0, Math.max(1, config.maxFlowsPerRequest));
366
- const candidateSelection = selectCandidateTests(prioritizedFlows, tests, Math.max(20, config.maxCandidateTests));
367
- warnings.push(...candidateSelection.warnings);
368
- const candidateTests = candidateSelection.tests;
369
- if (prioritizedFlows.length === 0 || candidateTests.length === 0) {
370
- warnings.push('AI mapping skipped: no prioritized flows or path-aligned candidate tests were available.');
371
- return {
372
- enabled: true,
373
- used: false,
374
- provider: providerName,
375
- mappedFlows: 0,
376
- matchedTests: 0,
377
- coverage: [],
378
- warnings,
379
- };
380
- }
381
- const contextFiles = resolveContextFiles(appRoot, testsRoot, config.contextFiles || []);
382
- const contextBlock = contextFiles.length > 0
383
- ? contextFiles.map((entry) => `### Context: ${entry.path}\n${entry.content}`).join('\n\n')
384
- : 'No optional markdown context files were found.';
385
- if (contextFiles.length === 0) {
386
- warnings.push('AI mapping context files were not found; continuing without optional markdown context.');
387
- }
388
- // Read candidate test file contents so the AI can reason about what scenarios
389
- // are already covered and which ones are still missing.
390
- const candidateTestContents = readCandidateTestContents(testsRoot, candidateTests);
391
- const testContentBlock = candidateTestContents.length > 0
392
- ? candidateTestContents.map((entry) => `### Test: ${entry.path}\n\`\`\`typescript\n${entry.content}\n\`\`\``).join('\n\n')
393
- : 'No candidate test file contents could be read.';
394
- let provider;
395
- try {
396
- provider = config.provider === 'auto'
397
- ? await provider_factory_js_1.LLMProviderFactory.createFromEnv()
398
- : provider_factory_js_1.LLMProviderFactory.createFromString(config.provider);
399
- }
400
- catch (error) {
401
- const message = error instanceof Error ? error.message : String(error);
402
- warnings.push(`AI mapping unavailable (${providerName}): ${message}`);
403
- return {
404
- enabled: true,
405
- used: false,
406
- provider: providerName,
407
- mappedFlows: 0,
408
- matchedTests: 0,
409
- coverage: [],
410
- warnings,
411
- };
412
- }
413
- const prompt = [
414
- 'You are an expert Mattermost E2E test impact analyst.',
415
- 'Map impacted flows to existing Playwright test file paths.',
416
- 'Only use tests from CANDIDATE_TESTS. Never invent paths.',
417
- 'Prefer no mapping over a broad or generic mapping.',
418
- 'Return strict JSON only with this shape:',
419
- '{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0,"missingScenarios":["scenario description"]}]}',
420
- '',
421
- 'Rules:',
422
- '- Keep at most 5 tests per flow.',
423
- '- Use exact flowId values from FLOWS.',
424
- '- A flow may only map to tests listed under FLOW_CANDIDATE_SIGNALS for that flow.',
425
- '- Map a test when its file path structure OR test content titles specifically indicate it covers the flow scenario. Behavioral specificity is required — "search_user_post_spec.js" in a /search/ directory covers search_messages because it specifically tests searching for messages. A file named "find_channels.spec.ts" does NOT cover search_messages even if it is in a search-related path because it tests channel navigation, not message searching.',
426
- '- Map every candidate that has specific behavioral evidence. Multiple files each covering a different aspect of the same flow should all be mapped.',
427
- '- For candidates whose content you have not read, judge by path structure alone: map only when the path clearly names the specific behavior (not just a general subsystem keyword).',
428
- '- Only return tests: [] when no candidate has specific behavioral connection to the flow.',
429
- '- missingScenarios decision tree based on tests.length AFTER you have determined your test mappings:',
430
- ' * tests.length >= 3: return missingScenarios: [] — three or more specific tests covering different scenarios = comprehensive coverage.',
431
- ' * tests.length 1-2: list only scenarios that are genuinely absent from ALL mapped tests combined.',
432
- ' * tests.length 0: list 3-5 core user-facing scenarios that must be covered.',
433
- ' Write each scenario as a short imperative starting with a verb.',
434
- '',
435
- `FLOWS (${prioritizedFlows.length}):`,
436
- JSON.stringify(prioritizedFlows.map((flow) => ({
437
- flowId: flow.id,
438
- name: flow.name,
439
- priority: flow.priority,
440
- score: flow.score,
441
- files: (flow.files || []).slice(0, 5),
442
- keywords: flowKeywords(flow),
443
- })), null, 2),
444
- '',
445
- `CANDIDATE_TESTS (${candidateTests.length}):`,
446
- JSON.stringify(candidateTests, null, 2),
447
- '',
448
- `FLOW_CANDIDATE_SIGNALS (${candidateSelection.evidence.length}):`,
449
- JSON.stringify(candidateSelection.evidence, null, 2),
450
- '',
451
- `CANDIDATE_TEST_CONTENT (${candidateTestContents.length} file(s)):`,
452
- testContentBlock,
453
- '',
454
- contextBlock,
455
- ].join('\n');
456
- let parsed = null;
457
- try {
458
- const response = await provider.generateText(prompt, {
459
- maxTokens: Math.max(500, config.maxTokens),
460
- temperature: Math.max(0, Math.min(1, config.temperature)),
461
- timeout: 45000,
462
- systemPrompt: 'Return only valid JSON. Do not include markdown fences unless necessary.',
463
- });
464
- parsed = extractJson(response.text);
465
- }
466
- catch (error) {
467
- const message = error instanceof Error ? error.message : String(error);
468
- warnings.push(`AI mapping request failed (${provider.name}): ${message}`);
469
- return {
470
- enabled: true,
471
- used: false,
472
- provider: provider.name,
473
- mappedFlows: 0,
474
- matchedTests: 0,
475
- coverage: [],
476
- warnings,
477
- };
478
- }
479
- if (!parsed) {
480
- warnings.push(`AI mapping returned invalid JSON (${provider.name}).`);
481
- return {
482
- enabled: true,
483
- used: false,
484
- provider: provider.name,
485
- mappedFlows: 0,
486
- matchedTests: 0,
487
- coverage: [],
488
- warnings,
489
- };
490
- }
491
- const allowedFlowIds = new Set(prioritizedFlows.map((flow) => flow.id));
492
- const prioritizedFlowsById = new Map(prioritizedFlows.map((flow) => [flow.id, flow]));
493
- const mapped = new Map();
494
- const scenarioGaps = new Map();
495
- const matchedTests = new Set();
496
- for (const entry of parsed.mappings) {
497
- if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
498
- continue;
499
- }
500
- // Capture scenario suggestions for ALL flows up-front — before any early returns —
501
- // so unmapped flows (tests: []) still get their suggested scenarios in the gap report.
502
- if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
503
- const scenarios = entry.missingScenarios
504
- .filter((s) => typeof s === 'string' && s.trim().length > 0)
505
- .slice(0, 5);
506
- if (scenarios.length > 0) {
507
- scenarioGaps.set(entry.flowId, scenarios);
508
- }
509
- }
510
- const flow = prioritizedFlowsById.get(entry.flowId);
511
- const confidence = typeof entry.confidence === 'number' ? entry.confidence : undefined;
512
- const allowedTestsForFlow = candidateSelection.byFlow.get(entry.flowId);
513
- const valid = Array.from(new Set(entry.tests
514
- .map((testPath) => (0, utils_js_1.normalizePath)(testPath))
515
- .filter((testPath) => allowedTestsForFlow?.has(testPath))
516
- .filter((testPath) => (flow ? scoreTestPath(flow, testPath) > 0 : true)))).slice(0, 5);
517
- if (confidence !== undefined && confidence < 0.5) {
518
- warnings.push(`AI mapping rejected low-confidence result for ${entry.flowId} (${confidence}).`);
519
- continue;
520
- }
521
- if (valid.length === 0) {
522
- continue;
523
- }
524
- mapped.set(entry.flowId, valid);
525
- for (const testPath of valid) {
526
- matchedTests.add(testPath);
527
- }
528
- }
529
- // Post-AI exact-name fallback: for any flow still uncovered, search all test paths
530
- // for a file or directory whose name exactly matches the flow ID. This handles flows
531
- // whose keywords are all low-signal (e.g. view_user_group_modal) but whose test file
532
- // is named after the flow and is therefore unambiguous coverage evidence.
533
- const allNormalizedTests = tests.map((t) => (0, utils_js_1.normalizePath)(t.path)).filter(Boolean);
534
- for (const flow of prioritizedFlows) {
535
- if (mapped.has(flow.id)) {
536
- continue;
537
- }
538
- const exactMatch = allNormalizedTests.find((testPath) => {
539
- const segments = testPath.split('/');
540
- return segments.some((seg) => seg === flow.id || seg.replace(/\.spec\.[tj]sx?$/, '') === flow.id);
541
- });
542
- if (exactMatch) {
543
- mapped.set(flow.id, [exactMatch]);
544
- matchedTests.add(exactMatch);
545
- }
546
- }
547
- const coverage = buildCoverage(flows, mapped, scenarioGaps);
548
- if (mapped.size === 0) {
549
- warnings.push(`AI mapping returned no valid test mappings (${provider.name}).`);
550
- }
551
- return {
552
- enabled: true,
553
- used: mapped.size > 0,
554
- provider: provider.name,
555
- mappedFlows: mapped.size,
556
- matchedTests: matchedTests.size,
557
- coverage,
558
- warnings,
559
- };
560
- }
@@ -1,64 +0,0 @@
1
- import type { AgentConfig, AudienceRole } from './config.js';
2
- import { type BlastRadius, type FlagHit } from './flags.js';
3
- export type FlowPriority = 'P0' | 'P1' | 'P2';
4
- export interface FileAnalysis {
5
- relativePath: string;
6
- extension: string;
7
- exists: boolean;
8
- content: string | null;
9
- isUI: boolean;
10
- isScreen: boolean;
11
- isComponent: boolean;
12
- isState: boolean;
13
- isStyle: boolean;
14
- hasInteractions: boolean;
15
- keywords: string[];
16
- flowId: string;
17
- flowName: string;
18
- flowKind: 'screen' | 'flow';
19
- audience: AudienceRole[];
20
- flags: FlagHit[];
21
- subsystemRisk?: {
22
- rules: string[];
23
- scoreDelta: number;
24
- priorityFloor?: FlowPriority;
25
- reasons: string[];
26
- };
27
- }
28
- export interface FlowImpact {
29
- id: string;
30
- name: string;
31
- kind: 'screen' | 'flow';
32
- score: number;
33
- priority: FlowPriority;
34
- reasons: string[];
35
- keywords: string[];
36
- files: string[];
37
- audience?: AudienceRole[];
38
- flags?: FlagHit[];
39
- blastRadius?: BlastRadius;
40
- priorityFloor?: FlowPriority;
41
- subsystemRiskBoost?: number;
42
- subsystemRiskRules?: string[];
43
- existingTests?: string[];
44
- missingScenarios?: string[];
45
- }
46
- export interface ImpactAnalysisResult {
47
- files: FileAnalysis[];
48
- flows: FlowImpact[];
49
- warnings: string[];
50
- subsystemRisk: {
51
- source: 'map';
52
- enabled: boolean;
53
- mapPath: string;
54
- mapFound: boolean;
55
- rulesLoaded: number;
56
- filesMatched: number;
57
- ruleMatches: number;
58
- boostedFlows: number;
59
- };
60
- }
61
- export declare function analyzeFiles(appRoot: string, relativePaths: string[], config: AgentConfig): ImpactAnalysisResult;
62
- export declare function isTestFilePath(relativePath: string): boolean;
63
- export declare function scanRepositoryFlows(appRoot: string, limit?: number, patterns?: string[], exclude?: string[]): string[];
64
- //# sourceMappingURL=analysis.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/agent/analysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAE,YAAY,EAAa,MAAM,aAAa,CAAC;AACvE,OAAO,EAAqE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAC,MAAM,YAAY,CAAC;AAY9H,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,aAAa,CAAC,EAAE;QACZ,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,YAAY,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACL;AAED,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE;QACX,MAAM,EAAE,KAAK,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,OAAO,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AA6HD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,oBAAoB,CA+IhH;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCnH"}