@yasserkhanorg/e2e-agents 0.3.3 → 0.3.5
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 +66 -3
- 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 +32 -0
- package/dist/agent/config.d.ts.map +1 -1
- package/dist/agent/config.js +187 -1
- package/dist/agent/flow_catalog.d.ts.map +1 -1
- package/dist/agent/flow_catalog.js +10 -1
- package/dist/agent/operational_insights.d.ts +1 -1
- package/dist/agent/operational_insights.d.ts.map +1 -1
- package/dist/agent/operational_insights.js +2 -1
- package/dist/agent/pipeline.d.ts +2 -0
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/pipeline.js +409 -68
- package/dist/agent/plan.d.ts +40 -0
- package/dist/agent/plan.d.ts.map +1 -1
- package/dist/agent/plan.js +159 -4
- package/dist/agent/report.d.ts +13 -2
- package/dist/agent/report.d.ts.map +1 -1
- package/dist/agent/report.js +9 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +246 -19
- package/dist/agent/tests.d.ts +1 -1
- package/dist/agent/tests.d.ts.map +1 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +1 -0
- package/dist/cli.js +97 -4
- 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 +187 -1
- package/dist/esm/agent/flow_catalog.js +10 -1
- package/dist/esm/agent/operational_insights.js +2 -1
- package/dist/esm/agent/pipeline.js +409 -68
- package/dist/esm/agent/plan.js +158 -5
- package/dist/esm/agent/report.js +9 -0
- package/dist/esm/agent/runner.js +246 -19
- package/dist/esm/api.js +2 -1
- package/dist/esm/cli.js +98 -5
- 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 +40 -3
- package/schemas/plan.schema.json +48 -0
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": {
|
|
@@ -212,7 +262,9 @@ Optional config file `e2e-ai-agents.config.json` (JSON):
|
|
|
212
262
|
"forceFullOnWarningsAtOrAbove": 2,
|
|
213
263
|
"forceFullOnP0WithGaps": true,
|
|
214
264
|
"forceFullOnRiskyFiles": true,
|
|
215
|
-
"riskyFilePatterns": ["**/auth/**", "**/permissions/**", "**/security/**", "**/*.sql"]
|
|
265
|
+
"riskyFilePatterns": ["**/auth/**", "**/permissions/**", "**/security/**", "**/*.sql"],
|
|
266
|
+
"enforcementMode": "advisory",
|
|
267
|
+
"blockOnActions": ["must-add-tests"]
|
|
216
268
|
},
|
|
217
269
|
"flags": { "defaultState": "on" },
|
|
218
270
|
"audience": { "defaultRoles": ["member"] },
|
|
@@ -233,6 +285,8 @@ Notes:
|
|
|
233
285
|
- Impact mode expects a git diff; use `--since` or add `"impact": { "allowFallback": true }` to fall back to scanning.
|
|
234
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`.
|
|
235
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`.
|
|
236
290
|
- Impact analysis can apply subsystem-aware risk boosts and priority floors from a map (`impact.subsystemRisk`) to capture known high-blast-radius areas.
|
|
237
291
|
- Diffing is computed from `merge-base(<since>, HEAD)` when available, which is the standard PR-impact baseline.
|
|
238
292
|
- Reports are written under `testsRoot/.e2e-ai-agents/reports` (or app root if `testsRoot` is not set).
|
|
@@ -240,6 +294,8 @@ Notes:
|
|
|
240
294
|
- Selector/data-testid patches are only applied when `--apply` is passed.
|
|
241
295
|
- `plan` is a direct alias for `suggest`.
|
|
242
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.
|
|
243
299
|
- `heal` targets flaky/failed specs from a Playwright JSON report (`--traceability-report`).
|
|
244
300
|
- `--apply` remains available as a legacy shortcut for direct `gap` execution.
|
|
245
301
|
- Use `--pipeline` to run the Playwright generation pipeline.
|
|
@@ -252,17 +308,22 @@ Notes:
|
|
|
252
308
|
- In MCP mode, fallback is strict by default: if official agent setup fails, generation stops instead of silently degrading.
|
|
253
309
|
- Use `--pipeline-mcp-allow-fallback` (or config `pipeline.mcpAllowFallback=true`) only when you explicitly want fallback generation.
|
|
254
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.
|
|
255
313
|
- Official MCP outputs are validated against discovered local API surface (`pw.*`, `pw.testBrowser.*`, `channelsPage.*`) to block invented methods (for example `pw.mainClient.*`).
|
|
256
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.
|
|
257
315
|
- `impact/gap` pipeline output now includes `pipeline.mcp` (`requested`, `active`, `backend`) so MCP activation is explicit.
|
|
258
316
|
- `suggest` writes `.e2e-ai-agents/plan.json` with `runSet` (`smoke|targeted|full`) and confidence.
|
|
259
317
|
- `suggest` also writes `.e2e-ai-agents/ci-summary.md` with CI status: `run-now`, `must-add-tests`, or `safe-to-merge`.
|
|
260
|
-
- CLI policy overrides: `--policy-min-confidence`, `--policy-safe-merge-confidence`, `--policy-force-full-on-warnings`, `--policy-risky-patterns`.
|
|
318
|
+
- CLI policy overrides: `--policy-min-confidence`, `--policy-safe-merge-confidence`, `--policy-force-full-on-warnings`, `--policy-risky-patterns`, `--policy-enforcement-mode`, `--policy-block-actions`.
|
|
261
319
|
- GitHub Actions output wiring: `--github-output $GITHUB_OUTPUT`.
|
|
262
320
|
- Optional merge gating: `--fail-on-must-add-tests` exits non-zero when uncovered P0/P1 gaps are detected. Leave this flag unset for advisory-only mode.
|
|
321
|
+
- `suggest` now appends run metrics to `.e2e-ai-agents/metrics.jsonl` and writes aggregated `.e2e-ai-agents/metrics-summary.json`.
|
|
263
322
|
- `impact/gap` now include actionable `testSuggestions` with linked source files and skeleton test code.
|
|
264
323
|
- `impact/gap` now include `impactModel` metadata (`flowMapping`, `testMapping`, `confidenceClass`, traceability stats, dependency graph stats).
|
|
324
|
+
- `impact/gap` now include `runMetadata` (run id/timestamps/duration/since ref) for auditability.
|
|
265
325
|
- `impact/gap` now include optional `impactModel.subsystemRisk` stats (map status, matched files/rules, boosted flows).
|
|
326
|
+
- `impact/gap` pipeline result rows now include failure taxonomy (`failureCategory`, `failureCode`) when generation/heal fails.
|
|
266
327
|
- `feedback` appends outcomes to `.e2e-ai-agents/feedback.json` and recomputes `.e2e-ai-agents/calibration.json`.
|
|
267
328
|
- `feedback` also computes intelligent flaky scores into `.e2e-ai-agents/flaky-tests.json`.
|
|
268
329
|
- `traceability-capture` converts Playwright JSON execution report + optional coverage map into `.e2e-ai-agents/traceability-input.json`.
|
|
@@ -360,6 +421,8 @@ CI integration template:
|
|
|
360
421
|
- Subsystem risk map schema: [schemas/subsystem-risk-map.schema.json](schemas/subsystem-risk-map.schema.json)
|
|
361
422
|
- Subsystem risk map example: [examples/subsystem-risk-map.sample.json](examples/subsystem-risk-map.sample.json)
|
|
362
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>`
|
|
363
426
|
|
|
364
427
|
Traceability manifest example (`.e2e-ai-agents/traceability.json`):
|
|
365
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"}
|