@yasserkhanorg/e2e-agents 0.3.4 → 0.3.6
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.
- package/README.md +59 -1
- package/dist/agent/ai_flow_analysis.d.ts +12 -0
- package/dist/agent/ai_flow_analysis.d.ts.map +1 -0
- package/dist/agent/ai_flow_analysis.js +326 -0
- package/dist/agent/ai_mapping.d.ts +14 -0
- package/dist/agent/ai_mapping.d.ts.map +1 -0
- package/dist/agent/ai_mapping.js +374 -0
- package/dist/agent/config.d.ts +30 -0
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/config.js +159 -1
- package/dist/agent/flow_catalog.d.ts.map +1 -1
- package/dist/agent/flow_catalog.js +10 -1
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/pipeline.js +182 -52
- package/dist/agent/plan.d.ts +1 -0
- package/dist/agent/plan.d.ts.map +1 -1
- package/dist/agent/plan.js +13 -4
- package/dist/agent/report.d.ts +2 -2
- package/dist/agent/report.d.ts.map +1 -1
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +233 -14
- package/dist/agent/tests.d.ts +1 -1
- package/dist/agent/tests.d.ts.map +1 -1
- package/dist/cli.js +59 -2
- package/dist/esm/agent/ai_flow_analysis.js +323 -0
- package/dist/esm/agent/ai_mapping.js +371 -0
- package/dist/esm/agent/config.js +159 -1
- package/dist/esm/agent/flow_catalog.js +10 -1
- package/dist/esm/agent/pipeline.js +182 -52
- package/dist/esm/agent/plan.js +13 -4
- package/dist/esm/agent/runner.js +233 -14
- package/dist/esm/cli.js +59 -2
- package/dist/esm/provider_factory.js +7 -3
- package/dist/provider_factory.d.ts.map +1 -1
- package/dist/provider_factory.js +7 -3
- package/package.json +4 -1
- package/schemas/impact.schema.json +3 -3
package/README.md
CHANGED
|
@@ -134,6 +134,7 @@ Run AI-driven impact analysis or gap analysis on any frontend repo.
|
|
|
134
134
|
npx e2e-ai-agents impact --path /path/to/webapp
|
|
135
135
|
npx e2e-ai-agents gap --path /path/to/webapp
|
|
136
136
|
npx e2e-ai-agents plan --path /path/to/webapp
|
|
137
|
+
npx e2e-ai-agents suggest --path /path/to/webapp --mattermost
|
|
137
138
|
npx e2e-ai-agents generate --path /path/to/webapp --pipeline
|
|
138
139
|
npx e2e-ai-agents heal --path /path/to/webapp --traceability-report ./playwright-report.json
|
|
139
140
|
npx e2e-ai-agents suggest --path /path/to/webapp
|
|
@@ -144,6 +145,26 @@ npx e2e-ai-agents traceability-capture --path /path/to/webapp --traceability-rep
|
|
|
144
145
|
npx e2e-ai-agents traceability-ingest --path /path/to/webapp --traceability-input ./traceability-input.json
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
Local approval workflow (dev/QA + AI) with one review artifact:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# 1) Suggest and generate local review + pending approval JSON
|
|
152
|
+
node scripts/local-impact-workflow.js suggest --config ./e2e-ai-agents.config.json --since master
|
|
153
|
+
|
|
154
|
+
# 2) Approve or reject after review
|
|
155
|
+
node scripts/local-impact-workflow.js approve --config ./e2e-ai-agents.config.json --decision approve --note "QA approved"
|
|
156
|
+
|
|
157
|
+
# 3) Generate/heal only after approval
|
|
158
|
+
node scripts/local-impact-workflow.js generate --config ./e2e-ai-agents.config.json --since master --pipeline-dry-run
|
|
159
|
+
# Generates in MCP-only mode by default (AI generation/healing only).
|
|
160
|
+
# Optional: tune MCP timeout per call:
|
|
161
|
+
# node scripts/local-impact-workflow.js generate --config ./e2e-ai-agents.config.json --since master --pipeline-mcp-timeout-ms 120000
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
Generated local artifacts:
|
|
165
|
+
- `<tests-root>/.e2e-ai-agents/local-impact-review.md`
|
|
166
|
+
- `<tests-root>/.e2e-ai-agents/local-impact-approval.json`
|
|
167
|
+
|
|
147
168
|
If tests live outside the app root:
|
|
148
169
|
|
|
149
170
|
```bash
|
|
@@ -155,6 +176,7 @@ Optional config file `e2e-ai-agents.config.json` (JSON):
|
|
|
155
176
|
```json
|
|
156
177
|
{
|
|
157
178
|
"path": ".",
|
|
179
|
+
"profile": "default",
|
|
158
180
|
"testsRoot": ".",
|
|
159
181
|
"flowCatalogPath": ".e2e-ai-agents/flows.json",
|
|
160
182
|
"mode": "impact",
|
|
@@ -195,6 +217,31 @@ Optional config file `e2e-ai-agents.config.json` (JSON):
|
|
|
195
217
|
"enabled": false,
|
|
196
218
|
"mapPath": ".e2e-ai-agents/subsystem-risk-map.json",
|
|
197
219
|
"maxRulesPerFile": 4
|
|
220
|
+
},
|
|
221
|
+
"aiFlow": {
|
|
222
|
+
"enabled": true,
|
|
223
|
+
"strict": true,
|
|
224
|
+
"provider": "anthropic",
|
|
225
|
+
"contextFiles": [
|
|
226
|
+
"CLAUDE.OPTIONAL.md",
|
|
227
|
+
".claude/CLAUDE.OPTIONAL.md"
|
|
228
|
+
],
|
|
229
|
+
"maxFilesPerRequest": 220,
|
|
230
|
+
"maxFlowsPerRequest": 80,
|
|
231
|
+
"maxTokens": 4000,
|
|
232
|
+
"temperature": 0
|
|
233
|
+
},
|
|
234
|
+
"aiMapping": {
|
|
235
|
+
"enabled": false,
|
|
236
|
+
"provider": "anthropic",
|
|
237
|
+
"contextFiles": [
|
|
238
|
+
"CLAUDE.OPTIONAL.md",
|
|
239
|
+
".claude/CLAUDE.OPTIONAL.md"
|
|
240
|
+
],
|
|
241
|
+
"maxFlowsPerRequest": 30,
|
|
242
|
+
"maxCandidateTests": 400,
|
|
243
|
+
"maxTokens": 4000,
|
|
244
|
+
"temperature": 0
|
|
198
245
|
}
|
|
199
246
|
},
|
|
200
247
|
"pipeline": {
|
|
@@ -203,7 +250,10 @@ Optional config file `e2e-ai-agents.config.json` (JSON):
|
|
|
203
250
|
"outputDir": "specs/functional/ai-assisted",
|
|
204
251
|
"heal": true,
|
|
205
252
|
"mcp": false,
|
|
206
|
-
"mcpAllowFallback": false
|
|
253
|
+
"mcpAllowFallback": false,
|
|
254
|
+
"mcpOnly": false,
|
|
255
|
+
"mcpCommandTimeoutMs": 180000,
|
|
256
|
+
"mcpRetries": 1
|
|
207
257
|
},
|
|
208
258
|
"llm": { "provider": "anthropic", "fallback": "ollama" },
|
|
209
259
|
"policy": {
|
|
@@ -235,6 +285,8 @@ Notes:
|
|
|
235
285
|
- Impact mode expects a git diff; use `--since` or add `"impact": { "allowFallback": true }` to fall back to scanning.
|
|
236
286
|
- Impact analysis now uses static reverse dependency graph expansion (configurable via `impact.dependencyGraph`) to propagate changed-file impact, including alias imports via `aliasRoots` and `pathAliases`.
|
|
237
287
|
- Impact analysis can use coverage-style traceability manifests (`impact.traceability`) for file->test mapping with heuristic fallback for uncovered flows.
|
|
288
|
+
- Impact analysis can run AI-first flow mapping (`impact.aiFlow`) so impacted flows and priorities come from LLM reasoning rather than heuristic scoring.
|
|
289
|
+
- Impact analysis can use optional Anthropic-powered AI mapping (`impact.aiMapping`) to map impacted flows to existing tests when traceability is missing/low; context is loaded from optional markdown files such as `CLAUDE.OPTIONAL.md`.
|
|
238
290
|
- Impact analysis can apply subsystem-aware risk boosts and priority floors from a map (`impact.subsystemRisk`) to capture known high-blast-radius areas.
|
|
239
291
|
- Diffing is computed from `merge-base(<since>, HEAD)` when available, which is the standard PR-impact baseline.
|
|
240
292
|
- Reports are written under `testsRoot/.e2e-ai-agents/reports` (or app root if `testsRoot` is not set).
|
|
@@ -242,6 +294,8 @@ Notes:
|
|
|
242
294
|
- Selector/data-testid patches are only applied when `--apply` is passed.
|
|
243
295
|
- `plan` is a direct alias for `suggest`.
|
|
244
296
|
- `generate` is a direct alias for `approve-and-generate`.
|
|
297
|
+
- Mattermost-first strict mode is available with `--mattermost` (or `"profile": "mattermost"` in config).
|
|
298
|
+
- In Mattermost mode, heuristic-only test mapping is treated as insufficient evidence and recommendations are escalated to broad runs.
|
|
245
299
|
- `heal` targets flaky/failed specs from a Playwright JSON report (`--traceability-report`).
|
|
246
300
|
- `--apply` remains available as a legacy shortcut for direct `gap` execution.
|
|
247
301
|
- Use `--pipeline` to run the Playwright generation pipeline.
|
|
@@ -254,6 +308,8 @@ Notes:
|
|
|
254
308
|
- In MCP mode, fallback is strict by default: if official agent setup fails, generation stops instead of silently degrading.
|
|
255
309
|
- Use `--pipeline-mcp-allow-fallback` (or config `pipeline.mcpAllowFallback=true`) only when you explicitly want fallback generation.
|
|
256
310
|
- MCP prerequisites: Playwright config in `testsRoot` and Claude CLI installed/authenticated.
|
|
311
|
+
- Use `--pipeline-mcp-timeout-ms` (or config `pipeline.mcpCommandTimeoutMs`) to limit per-command MCP wait time and fail fast in strict mode.
|
|
312
|
+
- Use `--pipeline-mcp-retries` (or config `pipeline.mcpRetries`) to retry transient MCP failures while staying in AI-only mode.
|
|
257
313
|
- Official MCP outputs are validated against discovered local API surface (`pw.*`, `pw.testBrowser.*`, `channelsPage.*`) to block invented methods (for example `pw.mainClient.*`).
|
|
258
314
|
- If fallback is enabled and official MCP agent execution is unavailable, pipeline falls back to `e2e-test-gen` (if present) or package-native generation with warnings in report output.
|
|
259
315
|
- `impact/gap` pipeline output now includes `pipeline.mcp` (`requested`, `active`, `backend`) so MCP activation is explicit.
|
|
@@ -365,6 +421,8 @@ CI integration template:
|
|
|
365
421
|
- Subsystem risk map schema: [schemas/subsystem-risk-map.schema.json](schemas/subsystem-risk-map.schema.json)
|
|
366
422
|
- Subsystem risk map example: [examples/subsystem-risk-map.sample.json](examples/subsystem-risk-map.sample.json)
|
|
367
423
|
- End-to-end verification steps: [examples/verification/README.md](examples/verification/README.md)
|
|
424
|
+
- Impact checklist playbook: [examples/verification/IMPACT_ANALYSIS_CHECKLIST.md](examples/verification/IMPACT_ANALYSIS_CHECKLIST.md)
|
|
425
|
+
- Checklist validator command: `npm run impact:checklist -- --root <tests-root>`
|
|
368
426
|
|
|
369
427
|
Traceability manifest example (`.e2e-ai-agents/traceability.json`):
|
|
370
428
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { FileAnalysis, FlowImpact } from './analysis.js';
|
|
2
|
+
import type { AIFlowImpactConfig } from './config.js';
|
|
3
|
+
export interface AIFlowAnalysisResult {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
used: boolean;
|
|
6
|
+
provider: string;
|
|
7
|
+
flowCount: number;
|
|
8
|
+
warnings: string[];
|
|
9
|
+
flows: FlowImpact[];
|
|
10
|
+
}
|
|
11
|
+
export declare function mapAIFlowsFromFiles(appRoot: string, testsRoot: string, config: AIFlowImpactConfig, files: FileAnalysis[], changedFiles: string[]): Promise<AIFlowAnalysisResult>;
|
|
12
|
+
//# sourceMappingURL=ai_flow_analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai_flow_analysis.d.ts","sourceRoot":"","sources":["../../src/agent/ai_flow_analysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,YAAY,EAAE,UAAU,EAAe,MAAM,eAAe,CAAC;AAC1E,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,aAAa,CAAC;AAkBpD,MAAM,WAAW,oBAAoB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,UAAU,EAAE,CAAC;CACvB;AA2JD,wBAAsB,mBAAmB,CACrC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,YAAY,EAAE,EACrB,YAAY,EAAE,MAAM,EAAE,GACvB,OAAO,CAAC,oBAAoB,CAAC,CA+L/B"}
|
|
@@ -0,0 +1,326 @@
|
|
|
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.mapAIFlowsFromFiles = mapAIFlowsFromFiles;
|
|
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
|
+
function extractJson(text) {
|
|
11
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
12
|
+
const candidates = fenced ? [fenced[1], text] : [text];
|
|
13
|
+
for (const candidate of candidates) {
|
|
14
|
+
const start = candidate.indexOf('{');
|
|
15
|
+
const end = candidate.lastIndexOf('}');
|
|
16
|
+
if (start < 0 || end <= start) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const raw = candidate.slice(start, end + 1);
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(raw);
|
|
22
|
+
if (parsed && Array.isArray(parsed.flows)) {
|
|
23
|
+
return parsed;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Ignore parse failure and keep trying other candidates.
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function resolveContextFiles(appRoot, testsRoot, files) {
|
|
33
|
+
const resolved = [];
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const maxCharsPerFile = 12000;
|
|
36
|
+
const maxTotalChars = 32000;
|
|
37
|
+
let totalChars = 0;
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
const candidates = (0, path_1.isAbsolute)(file)
|
|
40
|
+
? [file]
|
|
41
|
+
: [(0, path_1.join)(testsRoot, file), (0, path_1.join)(appRoot, file)];
|
|
42
|
+
for (const candidate of candidates) {
|
|
43
|
+
const normalized = (0, utils_js_1.normalizePath)(candidate);
|
|
44
|
+
if (seen.has(normalized) || !(0, fs_1.existsSync)(candidate)) {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const content = (0, fs_1.readFileSync)(candidate, 'utf-8');
|
|
48
|
+
const trimmed = content.trim();
|
|
49
|
+
if (!trimmed) {
|
|
50
|
+
seen.add(normalized);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const remaining = Math.max(0, maxTotalChars - totalChars);
|
|
54
|
+
if (remaining <= 0) {
|
|
55
|
+
return resolved;
|
|
56
|
+
}
|
|
57
|
+
const clipped = trimmed.slice(0, Math.min(maxCharsPerFile, remaining));
|
|
58
|
+
resolved.push({ path: normalized, content: clipped });
|
|
59
|
+
seen.add(normalized);
|
|
60
|
+
totalChars += clipped.length;
|
|
61
|
+
break;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
function priorityFromEntry(entry) {
|
|
67
|
+
if (entry.priority === 'P0' || entry.priority === 'P1' || entry.priority === 'P2') {
|
|
68
|
+
return entry.priority;
|
|
69
|
+
}
|
|
70
|
+
const score = typeof entry.score === 'number' ? entry.score : 0;
|
|
71
|
+
if (score >= 8) {
|
|
72
|
+
return 'P0';
|
|
73
|
+
}
|
|
74
|
+
if (score >= 5) {
|
|
75
|
+
return 'P1';
|
|
76
|
+
}
|
|
77
|
+
return 'P2';
|
|
78
|
+
}
|
|
79
|
+
function normalizeFlowId(value) {
|
|
80
|
+
return (0, utils_js_1.normalizePath)(value)
|
|
81
|
+
.replace(/[^a-zA-Z0-9/_-]+/g, '_')
|
|
82
|
+
.replace(/\/{2,}/g, '/')
|
|
83
|
+
.replace(/^\/+/, '')
|
|
84
|
+
.replace(/\/+$/, '')
|
|
85
|
+
.trim();
|
|
86
|
+
}
|
|
87
|
+
function sanitizeReasons(reasons, fallback) {
|
|
88
|
+
if (!Array.isArray(reasons)) {
|
|
89
|
+
return [fallback];
|
|
90
|
+
}
|
|
91
|
+
const cleaned = reasons.filter((entry) => typeof entry === 'string').map((entry) => entry.trim()).filter(Boolean);
|
|
92
|
+
return cleaned.length > 0 ? cleaned : [fallback];
|
|
93
|
+
}
|
|
94
|
+
function sanitizeKeywords(keywords, fallbackTokens) {
|
|
95
|
+
if (!Array.isArray(keywords)) {
|
|
96
|
+
return (0, utils_js_1.uniqueTokens)(fallbackTokens).slice(0, 20);
|
|
97
|
+
}
|
|
98
|
+
const fromAI = keywords.filter((entry) => typeof entry === 'string').flatMap((entry) => (0, utils_js_1.tokenize)(entry));
|
|
99
|
+
return (0, utils_js_1.uniqueTokens)([...fromAI, ...fallbackTokens]).slice(0, 20);
|
|
100
|
+
}
|
|
101
|
+
function summarizeFiles(files, changedFileSet, maxFiles) {
|
|
102
|
+
const sorted = [...files].sort((a, b) => {
|
|
103
|
+
const aChanged = changedFileSet.has(a.relativePath) ? 1 : 0;
|
|
104
|
+
const bChanged = changedFileSet.has(b.relativePath) ? 1 : 0;
|
|
105
|
+
if (aChanged !== bChanged) {
|
|
106
|
+
return bChanged - aChanged;
|
|
107
|
+
}
|
|
108
|
+
const aSignals = (a.isUI ? 1 : 0) + (a.isScreen ? 1 : 0) + (a.isState ? 1 : 0) + (a.hasInteractions ? 1 : 0);
|
|
109
|
+
const bSignals = (b.isUI ? 1 : 0) + (b.isScreen ? 1 : 0) + (b.isState ? 1 : 0) + (b.hasInteractions ? 1 : 0);
|
|
110
|
+
if (aSignals !== bSignals) {
|
|
111
|
+
return bSignals - aSignals;
|
|
112
|
+
}
|
|
113
|
+
return a.relativePath.localeCompare(b.relativePath);
|
|
114
|
+
}).slice(0, Math.max(20, maxFiles));
|
|
115
|
+
return sorted.map((file) => ({
|
|
116
|
+
path: file.relativePath,
|
|
117
|
+
changed: changedFileSet.has(file.relativePath),
|
|
118
|
+
isUI: file.isUI,
|
|
119
|
+
isScreen: file.isScreen,
|
|
120
|
+
isComponent: file.isComponent,
|
|
121
|
+
isState: file.isState,
|
|
122
|
+
isStyle: file.isStyle,
|
|
123
|
+
hasInteractions: file.hasInteractions,
|
|
124
|
+
keywords: file.keywords.slice(0, 20),
|
|
125
|
+
audience: file.audience,
|
|
126
|
+
flags: file.flags?.map((flag) => ({ name: flag.name, source: flag.source, defaultState: flag.defaultState })),
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
129
|
+
function mergeFlow(existing, candidate) {
|
|
130
|
+
if (!existing) {
|
|
131
|
+
return candidate;
|
|
132
|
+
}
|
|
133
|
+
const priorityOrder = { P0: 0, P1: 1, P2: 2 };
|
|
134
|
+
const priority = priorityOrder[candidate.priority] < priorityOrder[existing.priority]
|
|
135
|
+
? candidate.priority
|
|
136
|
+
: existing.priority;
|
|
137
|
+
return {
|
|
138
|
+
...existing,
|
|
139
|
+
name: existing.name || candidate.name,
|
|
140
|
+
kind: existing.kind || candidate.kind,
|
|
141
|
+
score: Math.max(existing.score, candidate.score),
|
|
142
|
+
priority,
|
|
143
|
+
reasons: (0, utils_js_1.uniqueTokens)([...(existing.reasons || []), ...(candidate.reasons || [])]),
|
|
144
|
+
keywords: (0, utils_js_1.uniqueTokens)([...(existing.keywords || []), ...(candidate.keywords || [])]),
|
|
145
|
+
files: (0, utils_js_1.uniqueTokens)([...(existing.files || []), ...(candidate.files || [])]),
|
|
146
|
+
audience: (0, utils_js_1.uniqueTokens)([...(existing.audience || []), ...(candidate.audience || [])]),
|
|
147
|
+
flags: [...(existing.flags || []), ...(candidate.flags || [])],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function mapAIFlowsFromFiles(appRoot, testsRoot, config, files, changedFiles) {
|
|
151
|
+
const providerName = config.provider === 'auto' ? 'auto' : config.provider;
|
|
152
|
+
const warnings = [];
|
|
153
|
+
if (!config.enabled) {
|
|
154
|
+
return {
|
|
155
|
+
enabled: false,
|
|
156
|
+
used: false,
|
|
157
|
+
provider: providerName,
|
|
158
|
+
flowCount: 0,
|
|
159
|
+
warnings,
|
|
160
|
+
flows: [],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (files.length === 0) {
|
|
164
|
+
warnings.push('AI flow analysis skipped: no analyzable files were found.');
|
|
165
|
+
return {
|
|
166
|
+
enabled: true,
|
|
167
|
+
used: false,
|
|
168
|
+
provider: providerName,
|
|
169
|
+
flowCount: 0,
|
|
170
|
+
warnings,
|
|
171
|
+
flows: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const changedFileSet = new Set(changedFiles.map((entry) => (0, utils_js_1.normalizePath)(entry)));
|
|
175
|
+
const summarizedFiles = summarizeFiles(files, changedFileSet, config.maxFilesPerRequest);
|
|
176
|
+
const allowedFiles = new Set(files.map((entry) => entry.relativePath));
|
|
177
|
+
const fileByPath = new Map(files.map((entry) => [entry.relativePath, entry]));
|
|
178
|
+
const contextFiles = resolveContextFiles(appRoot, testsRoot, config.contextFiles || []);
|
|
179
|
+
const contextBlock = contextFiles.length > 0
|
|
180
|
+
? contextFiles.map((entry) => `### Context: ${entry.path}\n${entry.content}`).join('\n\n')
|
|
181
|
+
: 'No optional markdown context files were found.';
|
|
182
|
+
if (contextFiles.length === 0) {
|
|
183
|
+
warnings.push('AI flow analysis context files were not found; continuing without optional markdown context.');
|
|
184
|
+
}
|
|
185
|
+
let provider;
|
|
186
|
+
try {
|
|
187
|
+
provider = config.provider === 'auto'
|
|
188
|
+
? await provider_factory_js_1.LLMProviderFactory.createFromEnv()
|
|
189
|
+
: provider_factory_js_1.LLMProviderFactory.createFromString(config.provider);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
193
|
+
warnings.push(`AI flow analysis unavailable (${providerName}): ${message}`);
|
|
194
|
+
return {
|
|
195
|
+
enabled: true,
|
|
196
|
+
used: false,
|
|
197
|
+
provider: providerName,
|
|
198
|
+
flowCount: 0,
|
|
199
|
+
warnings,
|
|
200
|
+
flows: [],
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const prompt = [
|
|
204
|
+
'You are an expert frontend impact analyst for Mattermost.',
|
|
205
|
+
'Build impacted user flows from changed frontend files.',
|
|
206
|
+
'This must be flow-centric, not file-centric.',
|
|
207
|
+
'',
|
|
208
|
+
'Return strict JSON only with this exact shape:',
|
|
209
|
+
'{"flows":[{"id":"<flow_id>","name":"<name>","kind":"flow|screen","priority":"P0|P1|P2","score":10,"reasons":["..."],"keywords":["..."],"files":["relative/path.tsx"]}]}',
|
|
210
|
+
'',
|
|
211
|
+
'Rules:',
|
|
212
|
+
'- Use only file paths listed in FILES.',
|
|
213
|
+
'- Every flow must have at least one file.',
|
|
214
|
+
'- Keep IDs stable and lowercase with underscores when possible.',
|
|
215
|
+
'- Prioritize true user-impacting flows; avoid low-value internal buckets.',
|
|
216
|
+
'- Keep at most 6 file paths per flow.',
|
|
217
|
+
`- Keep at most ${Math.max(1, config.maxFlowsPerRequest)} flows.`,
|
|
218
|
+
'',
|
|
219
|
+
`CHANGED_FILES (${changedFileSet.size}):`,
|
|
220
|
+
JSON.stringify(Array.from(changedFileSet), null, 2),
|
|
221
|
+
'',
|
|
222
|
+
`FILES (${summarizedFiles.length}):`,
|
|
223
|
+
JSON.stringify(summarizedFiles, null, 2),
|
|
224
|
+
'',
|
|
225
|
+
contextBlock,
|
|
226
|
+
].join('\n');
|
|
227
|
+
let parsed = null;
|
|
228
|
+
try {
|
|
229
|
+
const response = await provider.generateText(prompt, {
|
|
230
|
+
maxTokens: Math.max(800, config.maxTokens),
|
|
231
|
+
temperature: Math.max(0, Math.min(1, config.temperature)),
|
|
232
|
+
systemPrompt: 'Return only valid JSON. Do not include markdown fences unless necessary.',
|
|
233
|
+
});
|
|
234
|
+
parsed = extractJson(response.text);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
238
|
+
warnings.push(`AI flow analysis request failed (${provider.name}): ${message}`);
|
|
239
|
+
return {
|
|
240
|
+
enabled: true,
|
|
241
|
+
used: false,
|
|
242
|
+
provider: provider.name,
|
|
243
|
+
flowCount: 0,
|
|
244
|
+
warnings,
|
|
245
|
+
flows: [],
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (!parsed) {
|
|
249
|
+
warnings.push(`AI flow analysis returned invalid JSON (${provider.name}).`);
|
|
250
|
+
return {
|
|
251
|
+
enabled: true,
|
|
252
|
+
used: false,
|
|
253
|
+
provider: provider.name,
|
|
254
|
+
flowCount: 0,
|
|
255
|
+
warnings,
|
|
256
|
+
flows: [],
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
const flowsById = new Map();
|
|
260
|
+
for (const entry of parsed.flows) {
|
|
261
|
+
if (!entry || !Array.isArray(entry.files)) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const validFiles = Array.from(new Set(entry.files
|
|
265
|
+
.filter((value) => typeof value === 'string')
|
|
266
|
+
.map((value) => (0, utils_js_1.normalizePath)(value))
|
|
267
|
+
.filter((value) => allowedFiles.has(value)))).slice(0, 6);
|
|
268
|
+
if (validFiles.length === 0) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const rawId = typeof entry.id === 'string' && entry.id.trim()
|
|
272
|
+
? entry.id
|
|
273
|
+
: (typeof entry.name === 'string' && entry.name.trim() ? entry.name : validFiles[0]);
|
|
274
|
+
const id = normalizeFlowId(rawId);
|
|
275
|
+
if (!id) {
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
const fallbackTokens = (0, utils_js_1.uniqueTokens)([
|
|
279
|
+
...(0, utils_js_1.tokenize)(id),
|
|
280
|
+
...(typeof entry.name === 'string' ? (0, utils_js_1.tokenize)(entry.name) : []),
|
|
281
|
+
...validFiles.flatMap((value) => (0, utils_js_1.tokenize)(value)),
|
|
282
|
+
]);
|
|
283
|
+
const linkedFiles = validFiles.map((path) => fileByPath.get(path)).filter(Boolean);
|
|
284
|
+
const audience = (0, utils_js_1.uniqueTokens)(linkedFiles.flatMap((file) => file.audience || []));
|
|
285
|
+
const flags = linkedFiles.flatMap((file) => file.flags || []);
|
|
286
|
+
const score = typeof entry.score === 'number' && Number.isFinite(entry.score)
|
|
287
|
+
? Math.max(1, Math.min(20, Math.round(entry.score)))
|
|
288
|
+
: Math.max(4, validFiles.length * 2);
|
|
289
|
+
const flow = {
|
|
290
|
+
id,
|
|
291
|
+
name: typeof entry.name === 'string' && entry.name.trim() ? entry.name.trim() : id.replace(/[_/.-]+/g, ' '),
|
|
292
|
+
kind: entry.kind === 'screen' ? 'screen' : 'flow',
|
|
293
|
+
score,
|
|
294
|
+
priority: priorityFromEntry(entry),
|
|
295
|
+
reasons: sanitizeReasons(entry.reasons, 'AI flow analysis identified impacted behavior'),
|
|
296
|
+
keywords: sanitizeKeywords(entry.keywords, fallbackTokens),
|
|
297
|
+
files: validFiles,
|
|
298
|
+
audience,
|
|
299
|
+
flags,
|
|
300
|
+
};
|
|
301
|
+
flowsById.set(id, mergeFlow(flowsById.get(id), flow));
|
|
302
|
+
if (flowsById.size >= Math.max(1, config.maxFlowsPerRequest)) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const flows = Array.from(flowsById.values());
|
|
307
|
+
if (flows.length === 0) {
|
|
308
|
+
warnings.push('AI flow analysis did not return any valid flows linked to changed files.');
|
|
309
|
+
return {
|
|
310
|
+
enabled: true,
|
|
311
|
+
used: false,
|
|
312
|
+
provider: provider.name,
|
|
313
|
+
flowCount: 0,
|
|
314
|
+
warnings,
|
|
315
|
+
flows: [],
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
enabled: true,
|
|
320
|
+
used: true,
|
|
321
|
+
provider: provider.name,
|
|
322
|
+
flowCount: flows.length,
|
|
323
|
+
warnings,
|
|
324
|
+
flows,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { FlowImpact } from './analysis.js';
|
|
2
|
+
import type { AIMappingImpactConfig } from './config.js';
|
|
3
|
+
import type { FlowCoverage, TestFile } from './tests.js';
|
|
4
|
+
export interface AIMappingResult {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
used: boolean;
|
|
7
|
+
provider: string;
|
|
8
|
+
mappedFlows: number;
|
|
9
|
+
matchedTests: number;
|
|
10
|
+
coverage: FlowCoverage[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function mapAITestsToFlows(appRoot: string, testsRoot: string, config: AIMappingImpactConfig, flows: FlowImpact[], tests: TestFile[]): Promise<AIMappingResult>;
|
|
14
|
+
//# sourceMappingURL=ai_mapping.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai_mapping.d.ts","sourceRoot":"","sources":["../../src/agent/ai_mapping.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,YAAY,CAAC;AA2BvD,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAoND,wBAAsB,iBAAiB,CACnC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,QAAQ,EAAE,GAClB,OAAO,CAAC,eAAe,CAAC,CA8L1B"}
|