@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,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
- }