@yasserkhanorg/e2e-agents 0.5.16 → 0.6.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.
- package/dist/agent/pipeline.d.ts +1 -1
- package/dist/agent/pipeline.d.ts.map +1 -1
- package/dist/agent/plan.d.ts +0 -12
- package/dist/agent/plan.d.ts.map +1 -1
- package/dist/agent/plan.js +0 -365
- package/dist/agent/types.d.ts +42 -0
- package/dist/agent/types.d.ts.map +1 -0
- package/dist/agent/types.js +4 -0
- package/dist/api.d.ts +10 -14
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +29 -59
- package/dist/cli.js +41 -174
- package/dist/engine/impact_engine.d.ts +36 -0
- package/dist/engine/impact_engine.d.ts.map +1 -0
- package/dist/engine/impact_engine.js +196 -0
- package/dist/engine/plan_builder.d.ts +9 -0
- package/dist/engine/plan_builder.d.ts.map +1 -0
- package/dist/engine/plan_builder.js +329 -0
- package/dist/esm/agent/plan.js +1 -360
- package/dist/esm/agent/types.js +3 -0
- package/dist/esm/api.js +27 -56
- package/dist/esm/cli.js +40 -173
- package/dist/esm/engine/impact_engine.js +191 -0
- package/dist/esm/engine/plan_builder.js +323 -0
- package/dist/esm/index.js +6 -3
- package/dist/esm/knowledge/route_families.js +57 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -5
- package/dist/knowledge/route_families.d.ts +19 -0
- package/dist/knowledge/route_families.d.ts.map +1 -1
- package/dist/knowledge/route_families.js +60 -0
- package/package.json +1 -1
- package/dist/agent/ai_flow_analysis.d.ts +0 -13
- package/dist/agent/ai_flow_analysis.d.ts.map +0 -1
- package/dist/agent/ai_flow_analysis.js +0 -334
- package/dist/agent/ai_mapping.d.ts +0 -14
- package/dist/agent/ai_mapping.d.ts.map +0 -1
- package/dist/agent/ai_mapping.js +0 -560
- package/dist/agent/analysis.d.ts +0 -64
- package/dist/agent/analysis.d.ts.map +0 -1
- package/dist/agent/analysis.js +0 -292
- package/dist/agent/blast_radius.d.ts +0 -4
- package/dist/agent/blast_radius.d.ts.map +0 -1
- package/dist/agent/blast_radius.js +0 -37
- package/dist/agent/dependency_graph.d.ts +0 -14
- package/dist/agent/dependency_graph.d.ts.map +0 -1
- package/dist/agent/dependency_graph.js +0 -227
- package/dist/agent/flags.d.ts +0 -23
- package/dist/agent/flags.d.ts.map +0 -1
- package/dist/agent/flags.js +0 -171
- package/dist/agent/flow_catalog.d.ts +0 -25
- package/dist/agent/flow_catalog.d.ts.map +0 -1
- package/dist/agent/flow_catalog.js +0 -115
- package/dist/agent/flow_mapping.d.ts +0 -10
- package/dist/agent/flow_mapping.d.ts.map +0 -1
- package/dist/agent/flow_mapping.js +0 -84
- package/dist/agent/framework.d.ts +0 -13
- package/dist/agent/framework.d.ts.map +0 -1
- package/dist/agent/framework.js +0 -149
- package/dist/agent/gap_suggestions.d.ts +0 -14
- package/dist/agent/gap_suggestions.d.ts.map +0 -1
- package/dist/agent/gap_suggestions.js +0 -101
- package/dist/agent/generator.d.ts +0 -10
- package/dist/agent/generator.d.ts.map +0 -1
- package/dist/agent/generator.js +0 -115
- package/dist/agent/operational_insights.d.ts +0 -41
- package/dist/agent/operational_insights.d.ts.map +0 -1
- package/dist/agent/operational_insights.js +0 -127
- package/dist/agent/report.d.ts +0 -97
- package/dist/agent/report.d.ts.map +0 -1
- package/dist/agent/report.js +0 -159
- package/dist/agent/runner.d.ts +0 -7
- package/dist/agent/runner.d.ts.map +0 -1
- package/dist/agent/runner.js +0 -898
- package/dist/agent/selectors.d.ts +0 -10
- package/dist/agent/selectors.d.ts.map +0 -1
- package/dist/agent/selectors.js +0 -75
- package/dist/agent/subsystem_risk.d.ts +0 -23
- package/dist/agent/subsystem_risk.d.ts.map +0 -1
- package/dist/agent/subsystem_risk.js +0 -207
- package/dist/agent/tests.d.ts +0 -19
- package/dist/agent/tests.d.ts.map +0 -1
- package/dist/agent/tests.js +0 -116
- package/dist/agent/traceability.d.ts +0 -22
- package/dist/agent/traceability.d.ts.map +0 -1
- package/dist/agent/traceability.js +0 -183
- package/dist/esm/agent/ai_flow_analysis.js +0 -331
- package/dist/esm/agent/ai_mapping.js +0 -557
- package/dist/esm/agent/analysis.js +0 -287
- package/dist/esm/agent/blast_radius.js +0 -34
- package/dist/esm/agent/dependency_graph.js +0 -224
- package/dist/esm/agent/flags.js +0 -160
- package/dist/esm/agent/flow_catalog.js +0 -112
- package/dist/esm/agent/flow_mapping.js +0 -81
- package/dist/esm/agent/framework.js +0 -145
- package/dist/esm/agent/gap_suggestions.js +0 -98
- package/dist/esm/agent/generator.js +0 -112
- package/dist/esm/agent/operational_insights.js +0 -124
- package/dist/esm/agent/report.js +0 -156
- package/dist/esm/agent/runner.js +0 -894
- package/dist/esm/agent/selectors.js +0 -71
- package/dist/esm/agent/subsystem_risk.js +0 -204
- package/dist/esm/agent/tests.js +0 -111
- package/dist/esm/agent/traceability.js +0 -180
|
@@ -1,287 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { existsSync } from 'fs';
|
|
4
|
-
import { join } from 'path';
|
|
5
|
-
import { globSync } from 'glob';
|
|
6
|
-
import { extractFlagHits, inferAudienceFromPath, mergeFlags, normalizeRoles } from './flags.js';
|
|
7
|
-
import { baseNameWithoutExt, fileExtension, normalizePath, safeReadTextFile, titleCase, tokenize, uniqueTokens, } from './utils.js';
|
|
8
|
-
import { loadSubsystemRiskResolver } from './subsystem_risk.js';
|
|
9
|
-
const TEST_PATH_PATTERN = /(^|\/)__tests__(\/|$)|(^|\/)tests?(\/|$)|\.(spec|test)\.[a-z0-9]+$/i;
|
|
10
|
-
const SCREEN_DIRS = new Set(['pages', 'screens', 'views', 'routes']);
|
|
11
|
-
const FEATURE_DIRS = new Set(['features', 'modules', 'flows']);
|
|
12
|
-
const COMPONENT_DIRS = new Set(['components', 'widgets', 'ui']);
|
|
13
|
-
const STATE_DIRS = new Set(['state', 'store', 'stores', 'reducers', 'actions', 'context', 'hooks']);
|
|
14
|
-
const STYLE_EXTS = new Set(['css', 'scss', 'sass', 'less', 'styl']);
|
|
15
|
-
const UI_EXTS = new Set(['tsx', 'jsx']);
|
|
16
|
-
const CODE_EXTS = new Set(['ts', 'js', 'tsx', 'jsx']);
|
|
17
|
-
const INTERACTION_PATTERN = /(onClick|onSubmit|onChange|type=['"]submit['"]|role=['"]button['"]|aria-label=)/;
|
|
18
|
-
const PRIORITY_RANK = {
|
|
19
|
-
P0: 0,
|
|
20
|
-
P1: 1,
|
|
21
|
-
P2: 2,
|
|
22
|
-
};
|
|
23
|
-
function deriveFlowFromPath(relativePath) {
|
|
24
|
-
const segments = normalizePath(relativePath).split('/').filter(Boolean);
|
|
25
|
-
const baseName = baseNameWithoutExt(relativePath);
|
|
26
|
-
const base = baseName.toLowerCase() === 'index' && segments.length > 1 ? segments[segments.length - 2] : baseName;
|
|
27
|
-
for (let i = 0; i < segments.length; i += 1) {
|
|
28
|
-
const segment = segments[i].toLowerCase();
|
|
29
|
-
if (SCREEN_DIRS.has(segment)) {
|
|
30
|
-
const next = segments[i + 1] ? segments[i + 1] : base;
|
|
31
|
-
return { id: normalizePath(next), name: titleCase(next), kind: 'screen' };
|
|
32
|
-
}
|
|
33
|
-
if (FEATURE_DIRS.has(segment)) {
|
|
34
|
-
const next = segments[i + 1] ? segments[i + 1] : base;
|
|
35
|
-
return { id: normalizePath(next), name: titleCase(next), kind: 'flow' };
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return { id: normalizePath(base), name: titleCase(base), kind: 'flow' };
|
|
39
|
-
}
|
|
40
|
-
function extractKeywords(relativePath) {
|
|
41
|
-
const segments = normalizePath(relativePath).split('/').filter(Boolean);
|
|
42
|
-
const base = baseNameWithoutExt(relativePath);
|
|
43
|
-
const tokens = segments.flatMap((segment) => tokenize(segment));
|
|
44
|
-
tokens.push(...tokenize(base));
|
|
45
|
-
return uniqueTokens(tokens);
|
|
46
|
-
}
|
|
47
|
-
function detectInteractions(content) {
|
|
48
|
-
if (!content)
|
|
49
|
-
return false;
|
|
50
|
-
return INTERACTION_PATTERN.test(content);
|
|
51
|
-
}
|
|
52
|
-
function isScreenPath(relativePath) {
|
|
53
|
-
const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
|
|
54
|
-
if (segments.some((segment) => segment === 'selectors' || segment === 'reducers' || segment === 'actions')) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
return segments.some((segment) => SCREEN_DIRS.has(segment));
|
|
58
|
-
}
|
|
59
|
-
function isComponentPath(relativePath) {
|
|
60
|
-
const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
|
|
61
|
-
return segments.some((segment) => COMPONENT_DIRS.has(segment));
|
|
62
|
-
}
|
|
63
|
-
function isStatePath(relativePath) {
|
|
64
|
-
const segments = normalizePath(relativePath).split('/').map((segment) => segment.toLowerCase());
|
|
65
|
-
return segments.some((segment) => STATE_DIRS.has(segment));
|
|
66
|
-
}
|
|
67
|
-
function scoreFile(file, risk) {
|
|
68
|
-
let score = 1;
|
|
69
|
-
const reasons = [];
|
|
70
|
-
if (file.isScreen) {
|
|
71
|
-
score += 3;
|
|
72
|
-
reasons.push('Screen-level change');
|
|
73
|
-
}
|
|
74
|
-
if (file.isComponent) {
|
|
75
|
-
score += 2;
|
|
76
|
-
reasons.push('Shared component change');
|
|
77
|
-
}
|
|
78
|
-
if (file.isUI) {
|
|
79
|
-
score += 2;
|
|
80
|
-
reasons.push('UI logic change');
|
|
81
|
-
}
|
|
82
|
-
if (file.isState) {
|
|
83
|
-
score += 2;
|
|
84
|
-
reasons.push('State or data flow change');
|
|
85
|
-
}
|
|
86
|
-
if (file.isStyle) {
|
|
87
|
-
score += 1;
|
|
88
|
-
reasons.push('Visual styling change');
|
|
89
|
-
}
|
|
90
|
-
if (file.hasInteractions) {
|
|
91
|
-
score += 2;
|
|
92
|
-
reasons.push('Interactive element change');
|
|
93
|
-
}
|
|
94
|
-
const keywordHit = file.keywords.find((keyword) => risk.criticalKeywords.includes(keyword));
|
|
95
|
-
if (keywordHit) {
|
|
96
|
-
score += 2;
|
|
97
|
-
reasons.push(`Critical keyword: ${keywordHit}`);
|
|
98
|
-
}
|
|
99
|
-
if (file.subsystemRisk) {
|
|
100
|
-
const scoreDelta = file.subsystemRisk.scoreDelta;
|
|
101
|
-
if (scoreDelta !== 0) {
|
|
102
|
-
score += scoreDelta;
|
|
103
|
-
reasons.push(`Subsystem risk adjustment: ${scoreDelta > 0 ? '+' : ''}${scoreDelta}`);
|
|
104
|
-
}
|
|
105
|
-
reasons.push(...file.subsystemRisk.reasons);
|
|
106
|
-
}
|
|
107
|
-
return { score, reasons };
|
|
108
|
-
}
|
|
109
|
-
function mergePriorityFloor(current, candidate) {
|
|
110
|
-
if (!candidate) {
|
|
111
|
-
return current;
|
|
112
|
-
}
|
|
113
|
-
if (!current) {
|
|
114
|
-
return candidate;
|
|
115
|
-
}
|
|
116
|
-
return PRIORITY_RANK[candidate] < PRIORITY_RANK[current] ? candidate : current;
|
|
117
|
-
}
|
|
118
|
-
export function analyzeFiles(appRoot, relativePaths, config) {
|
|
119
|
-
const files = [];
|
|
120
|
-
const defaultAudience = config.audience.defaultRoles;
|
|
121
|
-
const defaultFlagState = config.flags.defaultState;
|
|
122
|
-
const warnings = [];
|
|
123
|
-
const subsystemRisk = loadSubsystemRiskResolver(config.impact.subsystemRisk);
|
|
124
|
-
warnings.push(...subsystemRisk.warnings);
|
|
125
|
-
let subsystemRiskMatchedFiles = 0;
|
|
126
|
-
let subsystemRiskRuleMatches = 0;
|
|
127
|
-
for (const relativePath of relativePaths) {
|
|
128
|
-
if (isTestFilePath(relativePath)) {
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
const fullPath = join(appRoot, relativePath);
|
|
132
|
-
const exists = existsSync(fullPath);
|
|
133
|
-
const extension = fileExtension(relativePath);
|
|
134
|
-
const content = exists && CODE_EXTS.has(extension) ? safeReadTextFile(fullPath) : null;
|
|
135
|
-
const { id, name, kind } = deriveFlowFromPath(relativePath);
|
|
136
|
-
const audience = inferAudienceFromPath(relativePath, config);
|
|
137
|
-
const flags = extractFlagHits(content, config);
|
|
138
|
-
const subsystemRiskMatches = subsystemRisk.matchFile(normalizePath(relativePath));
|
|
139
|
-
let subsystemRiskScoreDelta = 0;
|
|
140
|
-
let subsystemRiskPriorityFloor;
|
|
141
|
-
const subsystemRiskRules = uniqueTokens(subsystemRiskMatches.map((entry) => entry.ruleId));
|
|
142
|
-
const subsystemRiskReasons = uniqueTokens(subsystemRiskMatches.flatMap((entry) => entry.reasons));
|
|
143
|
-
if (subsystemRiskMatches.length > 0) {
|
|
144
|
-
subsystemRiskMatchedFiles += 1;
|
|
145
|
-
subsystemRiskRuleMatches += subsystemRiskMatches.length;
|
|
146
|
-
subsystemRiskScoreDelta = subsystemRiskMatches.reduce((acc, entry) => acc + entry.scoreDelta, 0);
|
|
147
|
-
for (const match of subsystemRiskMatches) {
|
|
148
|
-
subsystemRiskPriorityFloor = mergePriorityFloor(subsystemRiskPriorityFloor, match.priorityFloor);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
const subsystemKeywords = uniqueTokens(subsystemRiskMatches.flatMap((entry) => entry.keywords));
|
|
152
|
-
const analysis = {
|
|
153
|
-
relativePath: normalizePath(relativePath),
|
|
154
|
-
extension,
|
|
155
|
-
exists,
|
|
156
|
-
content,
|
|
157
|
-
isUI: UI_EXTS.has(extension),
|
|
158
|
-
isScreen: isScreenPath(relativePath),
|
|
159
|
-
isComponent: isComponentPath(relativePath),
|
|
160
|
-
isState: isStatePath(relativePath),
|
|
161
|
-
isStyle: STYLE_EXTS.has(extension),
|
|
162
|
-
hasInteractions: detectInteractions(content),
|
|
163
|
-
keywords: uniqueTokens([...extractKeywords(relativePath), ...subsystemKeywords]),
|
|
164
|
-
flowId: id,
|
|
165
|
-
flowName: name,
|
|
166
|
-
flowKind: kind,
|
|
167
|
-
audience,
|
|
168
|
-
flags,
|
|
169
|
-
subsystemRisk: subsystemRiskMatches.length > 0
|
|
170
|
-
? {
|
|
171
|
-
rules: subsystemRiskRules,
|
|
172
|
-
scoreDelta: subsystemRiskScoreDelta,
|
|
173
|
-
priorityFloor: subsystemRiskPriorityFloor,
|
|
174
|
-
reasons: subsystemRiskReasons,
|
|
175
|
-
}
|
|
176
|
-
: undefined,
|
|
177
|
-
};
|
|
178
|
-
files.push(analysis);
|
|
179
|
-
}
|
|
180
|
-
const flowMap = new Map();
|
|
181
|
-
for (const file of files) {
|
|
182
|
-
const { score, reasons } = scoreFile(file, config.risk);
|
|
183
|
-
const existing = flowMap.get(file.flowId);
|
|
184
|
-
if (!existing) {
|
|
185
|
-
flowMap.set(file.flowId, {
|
|
186
|
-
id: file.flowId,
|
|
187
|
-
name: file.flowName,
|
|
188
|
-
kind: file.flowKind,
|
|
189
|
-
score,
|
|
190
|
-
priority: 'P2',
|
|
191
|
-
reasons: [...reasons],
|
|
192
|
-
keywords: [...file.keywords],
|
|
193
|
-
files: [file.relativePath],
|
|
194
|
-
audience: file.audience,
|
|
195
|
-
flags: file.flags,
|
|
196
|
-
priorityFloor: file.subsystemRisk?.priorityFloor,
|
|
197
|
-
subsystemRiskBoost: file.subsystemRisk?.scoreDelta || 0,
|
|
198
|
-
subsystemRiskRules: file.subsystemRisk?.rules || [],
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
else {
|
|
202
|
-
existing.score += score;
|
|
203
|
-
existing.files.push(file.relativePath);
|
|
204
|
-
existing.reasons.push(...reasons);
|
|
205
|
-
existing.keywords.push(...file.keywords);
|
|
206
|
-
existing.audience = normalizeRoles([...(existing.audience || []), ...file.audience], defaultAudience);
|
|
207
|
-
existing.flags = mergeFlags([...(existing.flags || []), ...file.flags], defaultFlagState);
|
|
208
|
-
existing.priorityFloor = mergePriorityFloor(existing.priorityFloor, file.subsystemRisk?.priorityFloor);
|
|
209
|
-
existing.subsystemRiskBoost = (existing.subsystemRiskBoost || 0) + (file.subsystemRisk?.scoreDelta || 0);
|
|
210
|
-
existing.subsystemRiskRules = uniqueTokens([...(existing.subsystemRiskRules || []), ...(file.subsystemRisk?.rules || [])]);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
let boostedFlows = 0;
|
|
214
|
-
const flows = Array.from(flowMap.values()).map((flow) => {
|
|
215
|
-
const uniqueReason = uniqueTokens(flow.reasons);
|
|
216
|
-
const uniqueKeywords = uniqueTokens(flow.keywords);
|
|
217
|
-
const computedPriority = flow.score >= config.risk.p0Threshold
|
|
218
|
-
? 'P0'
|
|
219
|
-
: flow.score >= config.risk.p1Threshold
|
|
220
|
-
? 'P1'
|
|
221
|
-
: 'P2';
|
|
222
|
-
const priority = mergePriorityFloor(computedPriority, flow.priorityFloor) || computedPriority;
|
|
223
|
-
if ((flow.subsystemRiskBoost || 0) !== 0 || (flow.priorityFloor && flow.priorityFloor !== computedPriority)) {
|
|
224
|
-
boostedFlows += 1;
|
|
225
|
-
}
|
|
226
|
-
return {
|
|
227
|
-
...flow,
|
|
228
|
-
reasons: uniqueReason.map((reason) => reason),
|
|
229
|
-
keywords: uniqueKeywords,
|
|
230
|
-
priority,
|
|
231
|
-
};
|
|
232
|
-
});
|
|
233
|
-
return {
|
|
234
|
-
files,
|
|
235
|
-
flows,
|
|
236
|
-
warnings,
|
|
237
|
-
subsystemRisk: {
|
|
238
|
-
source: 'map',
|
|
239
|
-
enabled: subsystemRisk.info.enabled,
|
|
240
|
-
mapPath: subsystemRisk.info.mapPath,
|
|
241
|
-
mapFound: subsystemRisk.info.mapFound,
|
|
242
|
-
rulesLoaded: subsystemRisk.info.rulesLoaded,
|
|
243
|
-
filesMatched: subsystemRiskMatchedFiles,
|
|
244
|
-
ruleMatches: subsystemRiskRuleMatches,
|
|
245
|
-
boostedFlows,
|
|
246
|
-
},
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
export function isTestFilePath(relativePath) {
|
|
250
|
-
return TEST_PATH_PATTERN.test(normalizePath(relativePath));
|
|
251
|
-
}
|
|
252
|
-
export function scanRepositoryFlows(appRoot, limit = 250, patterns, exclude) {
|
|
253
|
-
const defaultPatterns = [
|
|
254
|
-
'**/pages/**/*.{tsx,jsx,ts,js}',
|
|
255
|
-
'**/screens/**/*.{tsx,jsx,ts,js}',
|
|
256
|
-
'**/views/**/*.{tsx,jsx,ts,js}',
|
|
257
|
-
'**/routes/**/*.{tsx,jsx,ts,js}',
|
|
258
|
-
];
|
|
259
|
-
const useDefaults = !(patterns && patterns.length > 0);
|
|
260
|
-
const activePatterns = useDefaults ? defaultPatterns : patterns;
|
|
261
|
-
const matches = new Set();
|
|
262
|
-
const ignorePatterns = [
|
|
263
|
-
'**/node_modules/**',
|
|
264
|
-
'**/.git/**',
|
|
265
|
-
'**/__tests__/**',
|
|
266
|
-
'**/tests/**',
|
|
267
|
-
...(useDefaults ? ['**/selectors/**', '**/reducers/**', '**/actions/**'] : []),
|
|
268
|
-
...(exclude || []),
|
|
269
|
-
];
|
|
270
|
-
for (const pattern of activePatterns) {
|
|
271
|
-
const files = globSync(pattern, {
|
|
272
|
-
cwd: appRoot,
|
|
273
|
-
ignore: ignorePatterns,
|
|
274
|
-
nodir: true,
|
|
275
|
-
});
|
|
276
|
-
for (const file of files) {
|
|
277
|
-
matches.add(normalizePath(file));
|
|
278
|
-
if (matches.size >= limit) {
|
|
279
|
-
break;
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
if (matches.size >= limit) {
|
|
283
|
-
break;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
return Array.from(matches);
|
|
287
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { computeBlastRadius, mergeFlags, normalizeRoles } from './flags.js';
|
|
4
|
-
export function applyBlastRadius(flows, files, config) {
|
|
5
|
-
if (flows.length === 0) {
|
|
6
|
-
return flows;
|
|
7
|
-
}
|
|
8
|
-
const fileMap = new Map();
|
|
9
|
-
for (const file of files) {
|
|
10
|
-
fileMap.set(file.relativePath, file);
|
|
11
|
-
}
|
|
12
|
-
return flows.map((flow) => {
|
|
13
|
-
const collectedFlags = [...(flow.flags || [])];
|
|
14
|
-
const collectedAudience = [...(flow.audience || [])];
|
|
15
|
-
for (const filePath of flow.files) {
|
|
16
|
-
const file = fileMap.get(filePath);
|
|
17
|
-
if (!file) {
|
|
18
|
-
continue;
|
|
19
|
-
}
|
|
20
|
-
collectedFlags.push(...file.flags);
|
|
21
|
-
collectedAudience.push(...file.audience);
|
|
22
|
-
}
|
|
23
|
-
const mergedFlags = mergeFlags(collectedFlags, config.flags.defaultState);
|
|
24
|
-
const mergedAudience = normalizeRoles(collectedAudience, config.audience.defaultRoles);
|
|
25
|
-
const blastRadius = computeBlastRadius(mergedAudience, mergedFlags, config);
|
|
26
|
-
return {
|
|
27
|
-
...flow,
|
|
28
|
-
audience: mergedAudience,
|
|
29
|
-
flags: mergedFlags,
|
|
30
|
-
blastRadius,
|
|
31
|
-
score: flow.score + blastRadius.scoreDelta,
|
|
32
|
-
};
|
|
33
|
-
});
|
|
34
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
-
// See LICENSE.txt for license information.
|
|
3
|
-
import { globSync } from 'glob';
|
|
4
|
-
import { dirname, join, normalize } from 'path';
|
|
5
|
-
import { normalizePath, safeReadTextFile } from './utils.js';
|
|
6
|
-
const IMPORT_REGEXES = [
|
|
7
|
-
/import\s+[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
8
|
-
/import\s+['"]([^'"]+)['"]/g,
|
|
9
|
-
/export\s+[\s\S]*?\s+from\s+['"]([^'"]+)['"]/g,
|
|
10
|
-
/require\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
11
|
-
/import\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
12
|
-
];
|
|
13
|
-
const RESOLVABLE_EXTENSIONS = ['ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs'];
|
|
14
|
-
function extractImportSpecifiers(content) {
|
|
15
|
-
const imports = [];
|
|
16
|
-
for (const regex of IMPORT_REGEXES) {
|
|
17
|
-
regex.lastIndex = 0;
|
|
18
|
-
let match = regex.exec(content);
|
|
19
|
-
while (match) {
|
|
20
|
-
const specifier = match[1];
|
|
21
|
-
if (specifier) {
|
|
22
|
-
imports.push(specifier);
|
|
23
|
-
}
|
|
24
|
-
match = regex.exec(content);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return Array.from(new Set(imports));
|
|
28
|
-
}
|
|
29
|
-
function listCandidateFiles(appRoot, cfg) {
|
|
30
|
-
const files = new Set();
|
|
31
|
-
for (const pattern of cfg.filePatterns) {
|
|
32
|
-
const matches = globSync(pattern, {
|
|
33
|
-
cwd: appRoot,
|
|
34
|
-
nodir: true,
|
|
35
|
-
ignore: cfg.excludePatterns,
|
|
36
|
-
});
|
|
37
|
-
for (const match of matches) {
|
|
38
|
-
files.add(normalizePath(match));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return Array.from(files);
|
|
42
|
-
}
|
|
43
|
-
function resolveWithCandidates(candidateBase, fileSet) {
|
|
44
|
-
if (fileSet.has(candidateBase)) {
|
|
45
|
-
return candidateBase;
|
|
46
|
-
}
|
|
47
|
-
for (const ext of RESOLVABLE_EXTENSIONS) {
|
|
48
|
-
const withExt = `${candidateBase}.${ext}`;
|
|
49
|
-
if (fileSet.has(withExt)) {
|
|
50
|
-
return withExt;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
for (const ext of RESOLVABLE_EXTENSIONS) {
|
|
54
|
-
const indexPath = `${candidateBase}/index.${ext}`;
|
|
55
|
-
if (fileSet.has(indexPath)) {
|
|
56
|
-
return indexPath;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return null;
|
|
60
|
-
}
|
|
61
|
-
function expandAliasTarget(pattern, target, specifier) {
|
|
62
|
-
const normalizedPattern = normalizePath(pattern);
|
|
63
|
-
const normalizedTarget = normalizePath(target);
|
|
64
|
-
const normalizedSpecifier = normalizePath(specifier);
|
|
65
|
-
if (normalizedPattern.endsWith('/*')) {
|
|
66
|
-
const prefix = normalizedPattern.slice(0, -2);
|
|
67
|
-
if (normalizedSpecifier === prefix || normalizedSpecifier.startsWith(`${prefix}/`)) {
|
|
68
|
-
const suffix = normalizedSpecifier === prefix ? '' : normalizedSpecifier.slice(prefix.length + 1);
|
|
69
|
-
if (normalizedTarget.endsWith('/*')) {
|
|
70
|
-
const targetPrefix = normalizedTarget.slice(0, -2);
|
|
71
|
-
return suffix ? normalizePath(`${targetPrefix}/${suffix}`) : normalizePath(targetPrefix);
|
|
72
|
-
}
|
|
73
|
-
return normalizePath(normalizedTarget);
|
|
74
|
-
}
|
|
75
|
-
return null;
|
|
76
|
-
}
|
|
77
|
-
if (normalizedPattern === normalizedSpecifier) {
|
|
78
|
-
if (normalizedTarget.endsWith('/*')) {
|
|
79
|
-
return normalizePath(normalizedTarget.slice(0, -2));
|
|
80
|
-
}
|
|
81
|
-
return normalizePath(normalizedTarget);
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
function resolvePathAliasImport(specifier, fileSet, cfg) {
|
|
86
|
-
for (const [pattern, targets] of Object.entries(cfg.pathAliases)) {
|
|
87
|
-
for (const target of targets) {
|
|
88
|
-
const aliasPath = expandAliasTarget(pattern, target, specifier);
|
|
89
|
-
if (!aliasPath) {
|
|
90
|
-
continue;
|
|
91
|
-
}
|
|
92
|
-
const resolved = resolveWithCandidates(aliasPath, fileSet);
|
|
93
|
-
if (resolved) {
|
|
94
|
-
return resolved;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
function resolveImport(fromFile, specifier, fileSet, cfg) {
|
|
101
|
-
if (specifier.startsWith('.')) {
|
|
102
|
-
const fromDir = dirname(fromFile);
|
|
103
|
-
const relativeCandidate = normalizePath(normalize(join(fromDir, specifier)));
|
|
104
|
-
return resolveWithCandidates(relativeCandidate, fileSet);
|
|
105
|
-
}
|
|
106
|
-
const aliasResolved = resolvePathAliasImport(specifier, fileSet, cfg);
|
|
107
|
-
if (aliasResolved) {
|
|
108
|
-
return aliasResolved;
|
|
109
|
-
}
|
|
110
|
-
for (const root of cfg.aliasRoots) {
|
|
111
|
-
const rootedCandidate = normalizePath(normalize(join(root, specifier)));
|
|
112
|
-
const resolved = resolveWithCandidates(rootedCandidate, fileSet);
|
|
113
|
-
if (resolved) {
|
|
114
|
-
return resolved;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const directCandidate = normalizePath(specifier);
|
|
118
|
-
return resolveWithCandidates(directCandidate, fileSet);
|
|
119
|
-
}
|
|
120
|
-
export function expandByDependencyGraph(appRoot, changedFiles, cfg) {
|
|
121
|
-
const warnings = [];
|
|
122
|
-
if (!cfg.enabled) {
|
|
123
|
-
return {
|
|
124
|
-
source: 'static-dependency-graph',
|
|
125
|
-
seedFiles: [],
|
|
126
|
-
impactedFiles: [],
|
|
127
|
-
expandedFiles: [],
|
|
128
|
-
analyzedFiles: 0,
|
|
129
|
-
analyzedEdges: 0,
|
|
130
|
-
maxDepth: 0,
|
|
131
|
-
truncated: false,
|
|
132
|
-
warnings,
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
const candidates = listCandidateFiles(appRoot, cfg);
|
|
136
|
-
const fileSet = new Set(candidates);
|
|
137
|
-
if (candidates.length === 0) {
|
|
138
|
-
warnings.push('Dependency graph found no candidate source files.');
|
|
139
|
-
}
|
|
140
|
-
const reverse = new Map();
|
|
141
|
-
let analyzedEdges = 0;
|
|
142
|
-
for (const file of candidates) {
|
|
143
|
-
const fullPath = join(appRoot, file);
|
|
144
|
-
const content = safeReadTextFile(fullPath);
|
|
145
|
-
if (!content) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
148
|
-
const imports = extractImportSpecifiers(content);
|
|
149
|
-
for (const specifier of imports) {
|
|
150
|
-
const resolved = resolveImport(file, specifier, fileSet, cfg);
|
|
151
|
-
if (!resolved) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
if (!reverse.has(resolved)) {
|
|
155
|
-
reverse.set(resolved, new Set());
|
|
156
|
-
}
|
|
157
|
-
reverse.get(resolved)?.add(file);
|
|
158
|
-
analyzedEdges += 1;
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
const seeds = Array.from(new Set(changedFiles
|
|
162
|
-
.map((file) => normalizePath(file))
|
|
163
|
-
.filter((file) => fileSet.has(file))));
|
|
164
|
-
if (seeds.length === 0) {
|
|
165
|
-
warnings.push('No changed files were found in dependency graph candidates.');
|
|
166
|
-
return {
|
|
167
|
-
source: 'static-dependency-graph',
|
|
168
|
-
seedFiles: [],
|
|
169
|
-
impactedFiles: [],
|
|
170
|
-
expandedFiles: [],
|
|
171
|
-
analyzedFiles: candidates.length,
|
|
172
|
-
analyzedEdges,
|
|
173
|
-
maxDepth: cfg.maxDepth,
|
|
174
|
-
truncated: false,
|
|
175
|
-
warnings,
|
|
176
|
-
};
|
|
177
|
-
}
|
|
178
|
-
const impacted = new Set(seeds);
|
|
179
|
-
const queue = seeds.map((file) => ({ file, depth: 0 }));
|
|
180
|
-
let truncated = false;
|
|
181
|
-
while (queue.length > 0) {
|
|
182
|
-
const next = queue.shift();
|
|
183
|
-
if (!next) {
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (next.depth >= cfg.maxDepth) {
|
|
187
|
-
continue;
|
|
188
|
-
}
|
|
189
|
-
const dependents = reverse.get(next.file);
|
|
190
|
-
if (!dependents) {
|
|
191
|
-
continue;
|
|
192
|
-
}
|
|
193
|
-
for (const dependent of dependents) {
|
|
194
|
-
if (impacted.has(dependent)) {
|
|
195
|
-
continue;
|
|
196
|
-
}
|
|
197
|
-
if (impacted.size - seeds.length >= cfg.maxExpandedFiles) {
|
|
198
|
-
truncated = true;
|
|
199
|
-
break;
|
|
200
|
-
}
|
|
201
|
-
impacted.add(dependent);
|
|
202
|
-
queue.push({ file: dependent, depth: next.depth + 1 });
|
|
203
|
-
}
|
|
204
|
-
if (truncated) {
|
|
205
|
-
break;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
const impactedFiles = Array.from(impacted);
|
|
209
|
-
const expandedFiles = impactedFiles.filter((file) => !seeds.includes(file));
|
|
210
|
-
if (truncated) {
|
|
211
|
-
warnings.push(`Dependency expansion was truncated at maxExpandedFiles=${cfg.maxExpandedFiles}.`);
|
|
212
|
-
}
|
|
213
|
-
return {
|
|
214
|
-
source: 'static-dependency-graph',
|
|
215
|
-
seedFiles: seeds,
|
|
216
|
-
impactedFiles,
|
|
217
|
-
expandedFiles,
|
|
218
|
-
analyzedFiles: candidates.length,
|
|
219
|
-
analyzedEdges,
|
|
220
|
-
maxDepth: cfg.maxDepth,
|
|
221
|
-
truncated,
|
|
222
|
-
warnings,
|
|
223
|
-
};
|
|
224
|
-
}
|