@yasserkhanorg/e2e-agents 1.8.5 → 1.9.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.
Files changed (256) hide show
  1. package/README.md +95 -8
  2. package/dist/adapters/cypress.d.ts +10 -0
  3. package/dist/adapters/cypress.d.ts.map +1 -0
  4. package/dist/adapters/cypress.js +86 -0
  5. package/dist/adapters/framework_adapter.d.ts +41 -0
  6. package/dist/adapters/framework_adapter.d.ts.map +1 -0
  7. package/dist/adapters/framework_adapter.js +152 -0
  8. package/dist/adapters/playwright.d.ts +10 -0
  9. package/dist/adapters/playwright.d.ts.map +1 -0
  10. package/dist/adapters/playwright.js +86 -0
  11. package/dist/adapters/pytest.d.ts +10 -0
  12. package/dist/adapters/pytest.d.ts.map +1 -0
  13. package/dist/adapters/pytest.js +96 -0
  14. package/dist/adapters/supertest.d.ts +12 -0
  15. package/dist/adapters/supertest.d.ts.map +1 -0
  16. package/dist/adapters/supertest.js +85 -0
  17. package/dist/agent/config.d.ts +1 -1
  18. package/dist/agent/config.d.ts.map +1 -1
  19. package/dist/agent/git.d.ts +1 -0
  20. package/dist/agent/git.d.ts.map +1 -1
  21. package/dist/agent/git.js +3 -0
  22. package/dist/agentic/fix_loop.d.ts.map +1 -1
  23. package/dist/agentic/fix_loop.js +5 -4
  24. package/dist/agentic/runner.d.ts +2 -0
  25. package/dist/agentic/runner.d.ts.map +1 -1
  26. package/dist/agentic/runner.js +15 -12
  27. package/dist/agents/cross-impact.d.ts.map +1 -1
  28. package/dist/agents/cross-impact.js +6 -1
  29. package/dist/agents/executor.d.ts.map +1 -1
  30. package/dist/agents/executor.js +6 -1
  31. package/dist/agents/strategist.d.ts.map +1 -1
  32. package/dist/agents/strategist.js +6 -1
  33. package/dist/agents/test-designer.d.ts.map +1 -1
  34. package/dist/agents/test-designer.js +6 -1
  35. package/dist/anthropic_provider.d.ts.map +1 -1
  36. package/dist/anthropic_provider.js +1 -0
  37. package/dist/base_provider.d.ts +56 -0
  38. package/dist/base_provider.d.ts.map +1 -1
  39. package/dist/base_provider.js +123 -1
  40. package/dist/budget_ledger.d.ts +28 -0
  41. package/dist/budget_ledger.d.ts.map +1 -0
  42. package/dist/budget_ledger.js +62 -0
  43. package/dist/cache/cached_provider.d.ts +45 -0
  44. package/dist/cache/cached_provider.d.ts.map +1 -0
  45. package/dist/cache/cached_provider.js +88 -0
  46. package/dist/cache/response_cache.d.ts +79 -0
  47. package/dist/cache/response_cache.d.ts.map +1 -0
  48. package/dist/cache/response_cache.js +177 -0
  49. package/dist/cli/commands/bootstrap.d.ts +3 -0
  50. package/dist/cli/commands/bootstrap.d.ts.map +1 -0
  51. package/dist/cli/commands/bootstrap.js +109 -0
  52. package/dist/cli/commands/cost_report.d.ts +3 -0
  53. package/dist/cli/commands/cost_report.d.ts.map +1 -0
  54. package/dist/cli/commands/cost_report.js +115 -0
  55. package/dist/cli/commands/crew.d.ts.map +1 -1
  56. package/dist/cli/commands/crew.js +118 -1
  57. package/dist/cli/commands/gate.d.ts +3 -0
  58. package/dist/cli/commands/gate.d.ts.map +1 -0
  59. package/dist/cli/commands/gate.js +86 -0
  60. package/dist/cli/commands/init.d.ts.map +1 -1
  61. package/dist/cli/commands/init.js +7 -62
  62. package/dist/cli/commands/train.d.ts.map +1 -1
  63. package/dist/cli/commands/train.js +16 -21
  64. package/dist/cli/defaults.d.ts +35 -0
  65. package/dist/cli/defaults.d.ts.map +1 -0
  66. package/dist/cli/defaults.js +125 -0
  67. package/dist/cli/errors.d.ts +27 -0
  68. package/dist/cli/errors.d.ts.map +1 -0
  69. package/dist/cli/errors.js +57 -0
  70. package/dist/cli/parse_args.d.ts.map +1 -1
  71. package/dist/cli/parse_args.js +24 -2
  72. package/dist/cli/types.d.ts +7 -1
  73. package/dist/cli/types.d.ts.map +1 -1
  74. package/dist/cli.js +47 -2
  75. package/dist/crew/context.d.ts +15 -0
  76. package/dist/crew/context.d.ts.map +1 -1
  77. package/dist/crew/orchestrator.d.ts +14 -0
  78. package/dist/crew/orchestrator.d.ts.map +1 -1
  79. package/dist/crew/orchestrator.js +162 -4
  80. package/dist/crew/protocol.d.ts +13 -0
  81. package/dist/crew/protocol.d.ts.map +1 -1
  82. package/dist/crew/provider.d.ts +15 -1
  83. package/dist/crew/provider.d.ts.map +1 -1
  84. package/dist/crew/provider.js +24 -4
  85. package/dist/custom_provider.d.ts.map +1 -1
  86. package/dist/custom_provider.js +1 -0
  87. package/dist/engine/diff_loader.d.ts.map +1 -1
  88. package/dist/engine/diff_loader.js +3 -14
  89. package/dist/engine/impact_engine.d.ts.map +1 -1
  90. package/dist/engine/impact_engine.js +9 -23
  91. package/dist/esm/adapters/cypress.js +49 -0
  92. package/dist/esm/adapters/framework_adapter.js +114 -0
  93. package/dist/esm/adapters/playwright.js +49 -0
  94. package/dist/esm/adapters/pytest.js +59 -0
  95. package/dist/esm/adapters/supertest.js +48 -0
  96. package/dist/esm/agent/git.js +3 -1
  97. package/dist/esm/agentic/fix_loop.js +5 -4
  98. package/dist/esm/agentic/runner.js +15 -12
  99. package/dist/esm/agents/cross-impact.js +6 -1
  100. package/dist/esm/agents/executor.js +6 -1
  101. package/dist/esm/agents/strategist.js +6 -1
  102. package/dist/esm/agents/test-designer.js +6 -1
  103. package/dist/esm/anthropic_provider.js +1 -0
  104. package/dist/esm/base_provider.js +121 -0
  105. package/dist/esm/budget_ledger.js +58 -0
  106. package/dist/esm/cache/cached_provider.js +82 -0
  107. package/dist/esm/cache/response_cache.js +140 -0
  108. package/dist/esm/cli/commands/bootstrap.js +106 -0
  109. package/dist/esm/cli/commands/cost_report.js +112 -0
  110. package/dist/esm/cli/commands/crew.js +118 -1
  111. package/dist/esm/cli/commands/gate.js +83 -0
  112. package/dist/esm/cli/commands/init.js +3 -58
  113. package/dist/esm/cli/commands/train.js +16 -21
  114. package/dist/esm/cli/defaults.js +118 -0
  115. package/dist/esm/cli/errors.js +52 -0
  116. package/dist/esm/cli/parse_args.js +24 -2
  117. package/dist/esm/cli.js +47 -2
  118. package/dist/esm/crew/orchestrator.js +162 -4
  119. package/dist/esm/crew/provider.js +24 -4
  120. package/dist/esm/custom_provider.js +1 -0
  121. package/dist/esm/engine/diff_loader.js +1 -12
  122. package/dist/esm/engine/impact_engine.js +9 -23
  123. package/dist/esm/index.js +21 -0
  124. package/dist/esm/knowledge/cluster_utils.js +60 -0
  125. package/dist/esm/knowledge/kg_bridge.js +381 -0
  126. package/dist/esm/knowledge/kg_types.js +3 -0
  127. package/dist/esm/knowledge/route_families.js +89 -0
  128. package/dist/esm/mcp-server.js +2 -4
  129. package/dist/esm/metrics/prometheus.js +149 -0
  130. package/dist/esm/model_router.js +59 -0
  131. package/dist/esm/ollama_provider.js +1 -0
  132. package/dist/esm/openai_provider.js +1 -0
  133. package/dist/esm/pipeline/orchestrator.js +6 -12
  134. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  135. package/dist/esm/pipeline/stage2_coverage.js +1 -0
  136. package/dist/esm/pipeline/stage3_generation.js +1 -0
  137. package/dist/esm/progress.js +112 -0
  138. package/dist/esm/prompts/coverage.js +7 -24
  139. package/dist/esm/prompts/cross-impact.js +3 -21
  140. package/dist/esm/prompts/generation.js +158 -36
  141. package/dist/esm/prompts/generation_profile.js +147 -0
  142. package/dist/esm/prompts/heal.js +33 -15
  143. package/dist/esm/prompts/impact.js +3 -22
  144. package/dist/esm/prompts/json_extract.js +36 -0
  145. package/dist/esm/prompts/strategist.js +2 -20
  146. package/dist/esm/prompts/test-designer.js +6 -21
  147. package/dist/esm/provider_factory.js +6 -4
  148. package/dist/esm/reporters/junit.js +86 -0
  149. package/dist/esm/reporters/reporter.js +3 -0
  150. package/dist/esm/reporters/sarif.js +131 -0
  151. package/dist/esm/resilience/circuit_breaker.js +78 -0
  152. package/dist/esm/resilience/retry.js +56 -0
  153. package/dist/esm/sanitize.js +66 -0
  154. package/dist/esm/training/kg_scanner.js +115 -0
  155. package/dist/esm/training/scanner.js +27 -34
  156. package/dist/esm/version.js +33 -0
  157. package/dist/index.d.ts +21 -1
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/index.js +45 -1
  160. package/dist/knowledge/cluster_utils.d.ts +28 -0
  161. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  162. package/dist/knowledge/cluster_utils.js +67 -0
  163. package/dist/knowledge/kg_bridge.d.ts +31 -0
  164. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  165. package/dist/knowledge/kg_bridge.js +388 -0
  166. package/dist/knowledge/kg_types.d.ts +75 -0
  167. package/dist/knowledge/kg_types.d.ts.map +1 -0
  168. package/dist/knowledge/kg_types.js +4 -0
  169. package/dist/knowledge/route_families.d.ts +18 -0
  170. package/dist/knowledge/route_families.d.ts.map +1 -1
  171. package/dist/knowledge/route_families.js +91 -0
  172. package/dist/mcp-server.d.ts.map +1 -1
  173. package/dist/mcp-server.js +2 -4
  174. package/dist/metrics/prometheus.d.ts +37 -0
  175. package/dist/metrics/prometheus.d.ts.map +1 -0
  176. package/dist/metrics/prometheus.js +153 -0
  177. package/dist/model_router.d.ts +28 -0
  178. package/dist/model_router.d.ts.map +1 -0
  179. package/dist/model_router.js +63 -0
  180. package/dist/ollama_provider.d.ts.map +1 -1
  181. package/dist/ollama_provider.js +1 -0
  182. package/dist/openai_provider.d.ts.map +1 -1
  183. package/dist/openai_provider.js +1 -0
  184. package/dist/pipeline/orchestrator.d.ts +2 -0
  185. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  186. package/dist/pipeline/orchestrator.js +6 -12
  187. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  188. package/dist/pipeline/stage0_preprocess.js +11 -18
  189. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  190. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  191. package/dist/pipeline/stage2_coverage.js +1 -0
  192. package/dist/pipeline/stage3_generation.d.ts +2 -0
  193. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  194. package/dist/pipeline/stage3_generation.js +1 -0
  195. package/dist/pipeline/stage4_heal.d.ts +2 -0
  196. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  197. package/dist/progress.d.ts +22 -0
  198. package/dist/progress.d.ts.map +1 -0
  199. package/dist/progress.js +116 -0
  200. package/dist/prompts/coverage.d.ts +2 -0
  201. package/dist/prompts/coverage.d.ts.map +1 -1
  202. package/dist/prompts/coverage.js +7 -24
  203. package/dist/prompts/cross-impact.d.ts +1 -0
  204. package/dist/prompts/cross-impact.d.ts.map +1 -1
  205. package/dist/prompts/cross-impact.js +3 -21
  206. package/dist/prompts/generation.d.ts +3 -1
  207. package/dist/prompts/generation.d.ts.map +1 -1
  208. package/dist/prompts/generation.js +158 -36
  209. package/dist/prompts/generation_profile.d.ts +29 -0
  210. package/dist/prompts/generation_profile.d.ts.map +1 -0
  211. package/dist/prompts/generation_profile.js +151 -0
  212. package/dist/prompts/heal.d.ts +3 -1
  213. package/dist/prompts/heal.d.ts.map +1 -1
  214. package/dist/prompts/heal.js +33 -15
  215. package/dist/prompts/impact.d.ts +1 -0
  216. package/dist/prompts/impact.d.ts.map +1 -1
  217. package/dist/prompts/impact.js +3 -22
  218. package/dist/prompts/json_extract.d.ts +14 -0
  219. package/dist/prompts/json_extract.d.ts.map +1 -0
  220. package/dist/prompts/json_extract.js +39 -0
  221. package/dist/prompts/strategist.d.ts.map +1 -1
  222. package/dist/prompts/strategist.js +2 -20
  223. package/dist/prompts/test-designer.d.ts +2 -0
  224. package/dist/prompts/test-designer.d.ts.map +1 -1
  225. package/dist/prompts/test-designer.js +6 -21
  226. package/dist/provider_factory.d.ts.map +1 -1
  227. package/dist/provider_factory.js +6 -4
  228. package/dist/reporters/junit.d.ts +6 -0
  229. package/dist/reporters/junit.d.ts.map +1 -0
  230. package/dist/reporters/junit.js +89 -0
  231. package/dist/reporters/reporter.d.ts +42 -0
  232. package/dist/reporters/reporter.d.ts.map +1 -0
  233. package/dist/reporters/reporter.js +4 -0
  234. package/dist/reporters/sarif.d.ts +7 -0
  235. package/dist/reporters/sarif.d.ts.map +1 -0
  236. package/dist/reporters/sarif.js +134 -0
  237. package/dist/resilience/circuit_breaker.d.ts +36 -0
  238. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  239. package/dist/resilience/circuit_breaker.js +82 -0
  240. package/dist/resilience/retry.d.ts +11 -0
  241. package/dist/resilience/retry.d.ts.map +1 -0
  242. package/dist/resilience/retry.js +59 -0
  243. package/dist/sanitize.d.ts +15 -0
  244. package/dist/sanitize.d.ts.map +1 -0
  245. package/dist/sanitize.js +71 -0
  246. package/dist/training/kg_scanner.d.ts +13 -0
  247. package/dist/training/kg_scanner.d.ts.map +1 -0
  248. package/dist/training/kg_scanner.js +118 -0
  249. package/dist/training/scanner.d.ts +7 -2
  250. package/dist/training/scanner.d.ts.map +1 -1
  251. package/dist/training/scanner.js +27 -34
  252. package/dist/version.d.ts +6 -0
  253. package/dist/version.d.ts.map +1 -0
  254. package/dist/version.js +36 -0
  255. package/package.json +7 -2
  256. package/schemas/route-families.schema.json +31 -1
@@ -65,6 +65,7 @@ export class CustomProvider extends BaseProvider {
65
65
  };
66
66
  }
67
67
  async generateText(prompt, options) {
68
+ this.checkBudget();
68
69
  const startTime = Date.now();
69
70
  try {
70
71
  if (prompt.length > 10 * 1024 * 1024) {
@@ -1,20 +1,9 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
- import { spawnSync } from 'child_process';
3
+ import { runGitRaw } from '../agent/git.js';
4
4
  const MAX_DIFF_CHARS = 8000;
5
5
  const MAX_TOTAL_CHARS = 60000;
6
6
  const TRUNCATION_NOTICE = '\n... (diff truncated)';
7
- function runGitRaw(args, cwd) {
8
- const result = spawnSync('git', args, {
9
- cwd,
10
- encoding: 'utf-8',
11
- timeout: 30000,
12
- });
13
- if (result.error || result.status !== 0) {
14
- return null;
15
- }
16
- return result.stdout;
17
- }
18
7
  /**
19
8
  * Loads git diffs for the given changed files relative to the given since ref.
20
9
  * Uses `git merge-base` to find the accurate base ref first.
@@ -2,7 +2,8 @@
2
2
  // See LICENSE.txt for license information.
3
3
  import { existsSync, readdirSync, readFileSync } from 'fs';
4
4
  import { join } from 'path';
5
- import { loadRouteFamilyManifest, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
5
+ import { loadRouteFamilyManifest, buildHeuristicFamilies, bindFilesToFamilies, getSpecDirsForBinding, getCypressSpecDirsForBinding, getPriorityForBinding, getUserFlowsForBinding, } from '../knowledge/route_families.js';
6
+ import { isTestFile } from '../agent/git.js';
6
7
  function scanDirForSpecs(baseDir, specDir, extension) {
7
8
  const fullDir = join(baseDir, specDir);
8
9
  if (!existsSync(fullDir)) {
@@ -125,7 +126,8 @@ function groupBindings(fileBindings) {
125
126
  const key = binding.feature || binding.family;
126
127
  const existing = groups.get(key);
127
128
  if (existing) {
128
- if (!existing.files.includes(fb.file)) {
129
+ if (!existing._seen.has(fb.file)) {
130
+ existing._seen.add(fb.file);
129
131
  existing.files.push(fb.file);
130
132
  }
131
133
  }
@@ -134,23 +136,13 @@ function groupBindings(fileBindings) {
134
136
  familyId: binding.family,
135
137
  featureId: binding.feature,
136
138
  files: [fb.file],
139
+ _seen: new Set([fb.file]),
137
140
  });
138
141
  }
139
142
  }
140
143
  }
141
144
  return groups;
142
145
  }
143
- /** Filter out test files that should not be treated as application changes. */
144
- function isTestFile(file) {
145
- const normalized = file.replace(/\\/g, '/');
146
- return /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(normalized) ||
147
- /\.snap$/.test(normalized) ||
148
- /_test\.go$/.test(normalized) ||
149
- normalized.includes('__tests__/') ||
150
- normalized.includes('__snapshots__/') ||
151
- normalized.includes('/tests/') ||
152
- normalized.includes('/test/');
153
- }
154
146
  /** Classify filtered test files by type for downstream decision-making. */
155
147
  function classifyPrTestFiles(allFiles, sourceFiles) {
156
148
  const sourceSet = new Set(sourceFiles);
@@ -180,17 +172,11 @@ export function analyzeImpact(changedFiles, options) {
180
172
  const allOriginalFiles = [...new Set([...changedFiles, ...preFilteredTests])];
181
173
  changedFiles = changedFiles.filter((f) => !isTestFile(f));
182
174
  const prIncludedTestFiles = classifyPrTestFiles(allOriginalFiles, changedFiles);
183
- // Load manifest
184
- const manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
175
+ // Load manifest, fall back to heuristic families if not found
176
+ let manifest = loadRouteFamilyManifest(testsRoot, routeFamilies);
185
177
  if (!manifest) {
186
- return {
187
- changedFiles,
188
- expandedFiles: options.expandedFiles || [],
189
- impactedFeatures: [],
190
- unboundFiles: [...changedFiles],
191
- warnings: ['Route family manifest not found. All files are unbound.'],
192
- prIncludedTestFiles,
193
- };
178
+ manifest = buildHeuristicFamilies(changedFiles, testsRoot);
179
+ warnings.push('Route family manifest not found. Using directory-based heuristics (lower accuracy).', 'Tip: Run `e2e-ai-agents train` to generate a proper manifest.');
194
180
  }
195
181
  // Combine original + expanded files
196
182
  const allFiles = [...new Set([...changedFiles, ...(options.expandedFiles || [])])];
package/dist/esm/index.js CHANGED
@@ -44,8 +44,29 @@ export { StrategistAgent } from './agents/strategist.js';
44
44
  export { TestDesignerAgent } from './agents/test-designer.js';
45
45
  export { CrossImpactAgent } from './agents/cross-impact.js';
46
46
  export { RegressionAdvisorAgent } from './agents/regression-advisor.js';
47
+ // Base provider (for extending with custom providers)
48
+ export { BaseProvider, BudgetExceededError } from './base_provider.js';
49
+ // Budget tracking
50
+ export { BudgetLedger } from './budget_ledger.js';
51
+ // Model routing
52
+ export { ModelRouter } from './model_router.js';
53
+ // Resilience
54
+ export { withRetry } from './resilience/retry.js';
55
+ export { CircuitBreaker } from './resilience/circuit_breaker.js';
56
+ // Metrics
57
+ export { PrometheusMetrics } from './metrics/prometheus.js';
58
+ // Secret scanning
59
+ export { sanitizeSecrets, containsSecrets, sanitizeObject } from './sanitize.js';
60
+ // CLI errors
61
+ export { CliError, classifyError, EXIT_CODES } from './cli/errors.js';
47
62
  // Training (route-families bootstrap and maintenance)
48
63
  export { scanProject } from './training/scanner.js';
49
64
  export { mergeFamilies, detectStaleFamilies } from './training/merger.js';
50
65
  export { enrichFamilies } from './training/enricher.js';
51
66
  export { getCommitFiles, validateCommit, buildValidationReport, formatValidationReport } from './training/validator.js';
67
+ export { loadKnowledgeGraph, classifyProjectType, transformKGToFamilies, loadDiffOverlay } from './knowledge/kg_bridge.js';
68
+ export { scanFromKnowledgeGraph } from './training/kg_scanner.js';
69
+ export { resolveGenerationProfile, isMattermostProfile } from './prompts/generation_profile.js';
70
+ export { detectFramework, detectTestMode } from './adapters/framework_adapter.js';
71
+ // Route families (additional)
72
+ export { serializeManifest } from './knowledge/route_families.js';
@@ -0,0 +1,60 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Shared cluster ID derivation for knowledge graph processing.
5
+ * Used by both kg_bridge.ts and kg_scanner.ts.
6
+ */
7
+ /** Default directories to skip when deriving cluster IDs from file paths. */
8
+ const DEFAULT_SKIP_DIRS = new Set([
9
+ 'src', 'app', 'lib', 'packages', 'server', 'api', 'pages',
10
+ 'components', 'features', 'modules',
11
+ ]);
12
+ /** Extended skip set that also excludes test directories. */
13
+ const SKIP_DIRS_WITH_TESTS = new Set([
14
+ ...DEFAULT_SKIP_DIRS,
15
+ 'test', 'tests', 'e2e', 'spec', 'specs',
16
+ ]);
17
+ /**
18
+ * Normalize a name to a snake_case cluster ID.
19
+ * Handles camelCase conversion, then strips non-alphanumeric characters.
20
+ */
21
+ export function normalizeToClusterId(name) {
22
+ return name
23
+ .replace(/[A-Z]/g, (c, idx) => (idx > 0 ? `_${c.toLowerCase()}` : c.toLowerCase()))
24
+ .replace(/[^a-z0-9_]/g, '_')
25
+ .replace(/_+/g, '_')
26
+ .replace(/^_|_$/g, '');
27
+ }
28
+ /**
29
+ * Derive a cluster ID from a node that has an optional filePath and name.
30
+ * Prefers file path grouping for consistency.
31
+ */
32
+ export function deriveClusterId(node, skipDirs) {
33
+ if (node.filePath) {
34
+ return deriveClusterIdFromPath(node.filePath, skipDirs);
35
+ }
36
+ const name = normalizeToClusterId(node.name);
37
+ return name && name.length > 1 ? name : null;
38
+ }
39
+ /**
40
+ * Derive a cluster ID from a file path by finding the first meaningful
41
+ * directory segment after skipping common structural prefixes.
42
+ */
43
+ export function deriveClusterIdFromPath(filePath, skipDirs = DEFAULT_SKIP_DIRS) {
44
+ const parts = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
45
+ for (const part of parts) {
46
+ if (skipDirs.has(part))
47
+ continue;
48
+ if (part.includes('.'))
49
+ continue; // skip files
50
+ const normalized = part.toLowerCase()
51
+ .replace(/[^a-z0-9_]/g, '_')
52
+ .replace(/_+/g, '_')
53
+ .replace(/^_|_$/g, '');
54
+ if (normalized && normalized.length > 1) {
55
+ return normalized;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ export { DEFAULT_SKIP_DIRS, SKIP_DIRS_WITH_TESTS };
@@ -0,0 +1,381 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ /**
4
+ * Bridge between Understand-Anything's knowledge graph and e2e-agents' route families.
5
+ * Transforms KG nodes/edges into RouteFamilyManifest so existing pipeline works unchanged.
6
+ */
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ import { logger } from '../logger.js';
10
+ import { deriveClusterId, deriveClusterIdFromPath } from './cluster_utils.js';
11
+ const UA_DIR = '.understand-anything';
12
+ const KG_FILE = 'knowledge-graph.json';
13
+ const DIFF_FILE = 'diff-overlay.json';
14
+ const FRONTEND_FRAMEWORKS = new Set([
15
+ 'react', 'vue', 'angular', 'svelte', 'next', 'nextjs', 'next.js',
16
+ 'nuxt', 'nuxtjs', 'gatsby', 'remix', 'astro', 'solid', 'solidjs',
17
+ 'preact', 'lit', 'stencil', 'qwik',
18
+ ]);
19
+ const BACKEND_FRAMEWORKS = new Set([
20
+ 'express', 'fastify', 'koa', 'hapi', 'nest', 'nestjs',
21
+ 'django', 'flask', 'fastapi', 'rails', 'spring', 'gin', 'echo', 'fiber',
22
+ 'actix', 'axum', 'rocket', 'phoenix', 'laravel',
23
+ ]);
24
+ /**
25
+ * Loads the knowledge graph from .understand-anything/knowledge-graph.json
26
+ * or from a custom path when provided.
27
+ */
28
+ export function loadKnowledgeGraph(projectRoot, customPath) {
29
+ const kgPath = customPath || join(projectRoot, UA_DIR, KG_FILE);
30
+ if (!existsSync(kgPath)) {
31
+ return null;
32
+ }
33
+ try {
34
+ const raw = JSON.parse(readFileSync(kgPath, 'utf-8'));
35
+ if (!raw.nodes || !Array.isArray(raw.nodes) || !raw.edges || !Array.isArray(raw.edges)) {
36
+ logger.warn('Knowledge graph missing required nodes/edges arrays');
37
+ return null;
38
+ }
39
+ if (!raw.project) {
40
+ logger.warn('Knowledge graph missing project metadata');
41
+ return null;
42
+ }
43
+ // Field-level validation: filter out invalid nodes rather than rejecting the whole graph
44
+ const MAX_STRING_LEN = 1000;
45
+ const validNodes = raw.nodes.filter((node) => {
46
+ if (typeof node.id !== 'string' || typeof node.name !== 'string') {
47
+ logger.warn(`Dropping KG node with missing/invalid id or name: ${JSON.stringify(node).slice(0, 200)}`);
48
+ return false;
49
+ }
50
+ if (node.filePath !== undefined) {
51
+ if (typeof node.filePath !== 'string') {
52
+ logger.warn(`Dropping KG node "${node.id}": filePath is not a string`);
53
+ return false;
54
+ }
55
+ if (node.filePath.startsWith('/')) {
56
+ logger.warn(`Dropping KG node "${node.id}": absolute filePath rejected`);
57
+ return false;
58
+ }
59
+ if (node.filePath.includes('..')) {
60
+ logger.warn(`Dropping KG node "${node.id}": path traversal in filePath rejected`);
61
+ return false;
62
+ }
63
+ if (node.filePath.includes('\0')) {
64
+ logger.warn(`Dropping KG node "${node.id}": null byte in filePath rejected`);
65
+ return false;
66
+ }
67
+ }
68
+ return true;
69
+ });
70
+ // Truncate excessively long strings
71
+ for (const node of validNodes) {
72
+ if (node.name.length > MAX_STRING_LEN) {
73
+ node.name = node.name.slice(0, MAX_STRING_LEN);
74
+ }
75
+ if (node.description && node.description.length > MAX_STRING_LEN) {
76
+ node.description = node.description.slice(0, MAX_STRING_LEN);
77
+ }
78
+ }
79
+ raw.nodes = validNodes;
80
+ return raw;
81
+ }
82
+ catch (error) {
83
+ logger.warn(`Failed to load knowledge graph: ${error instanceof Error ? error.message : String(error)}`);
84
+ return null;
85
+ }
86
+ }
87
+ /**
88
+ * Classifies project type based on KG framework metadata.
89
+ */
90
+ export function classifyProjectType(kg) {
91
+ const frameworks = kg.project.frameworks.map((f) => f.toLowerCase());
92
+ const hasFrontend = frameworks.some((f) => FRONTEND_FRAMEWORKS.has(f));
93
+ const hasBackend = frameworks.some((f) => BACKEND_FRAMEWORKS.has(f));
94
+ if (hasFrontend && hasBackend)
95
+ return 'fullstack';
96
+ if (hasBackend)
97
+ return 'backend';
98
+ return 'frontend';
99
+ }
100
+ /**
101
+ * Core bridge: transforms KG into a RouteFamilyManifest.
102
+ *
103
+ * Strategy:
104
+ * - Frontend: cluster UI-layer nodes into families by module/component groups
105
+ * - Backend: cluster API-layer nodes, follow calls edges into Service→Data layers
106
+ * - Priority: P0 = high fan-in nodes, P1 = moderate, P2 = leaf/utility
107
+ * - userFlows: derived from KG tour steps referencing family nodes
108
+ */
109
+ export function transformKGToFamilies(kg) {
110
+ const projectType = classifyProjectType(kg);
111
+ const nodeMap = new Map(kg.nodes.map((n) => [n.id, n]));
112
+ const edgesByTarget = groupEdges(kg.edges, 'target');
113
+ // Build clusters based on project type
114
+ const clusters = buildClusters(kg, projectType, nodeMap);
115
+ // Transform clusters into route families
116
+ const families = [];
117
+ for (const [clusterId, nodeIds] of clusters) {
118
+ const nodes = nodeIds.map((id) => nodeMap.get(id)).filter((n) => n !== undefined);
119
+ if (nodes.length === 0)
120
+ continue;
121
+ const family = buildFamilyFromCluster(clusterId, nodes, projectType, nodeMap, edgesByTarget);
122
+ if (family) {
123
+ families.push(family);
124
+ }
125
+ }
126
+ // Derive userFlows from KG tour steps
127
+ if (kg.tour && kg.tour.length > 0) {
128
+ assignTourFlows(families, kg.tour, nodeMap);
129
+ }
130
+ // Sort by priority (P0 first) then alphabetically
131
+ families.sort((a, b) => {
132
+ const pOrder = { P0: 0, P1: 1, P2: 2 };
133
+ const pa = pOrder[a.priority || 'P2'];
134
+ const pb = pOrder[b.priority || 'P2'];
135
+ if (pa !== pb)
136
+ return pa - pb;
137
+ return a.id.localeCompare(b.id);
138
+ });
139
+ return {
140
+ families,
141
+ source: 'knowledge-graph',
142
+ };
143
+ }
144
+ /**
145
+ * Loads the diff overlay from .understand-anything/diff-overlay.json
146
+ */
147
+ export function loadDiffOverlay(projectRoot) {
148
+ const overlayPath = join(projectRoot, UA_DIR, DIFF_FILE);
149
+ if (!existsSync(overlayPath)) {
150
+ return null;
151
+ }
152
+ try {
153
+ const raw = JSON.parse(readFileSync(overlayPath, 'utf-8'));
154
+ if (!raw.changes || !Array.isArray(raw.changes)) {
155
+ return null;
156
+ }
157
+ return raw;
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ /**
164
+ * Maps diff overlay changes to file paths using KG node resolution.
165
+ */
166
+ export function diffOverlayToChangedFiles(overlay, kg) {
167
+ const nodeMap = new Map(kg.nodes.map((n) => [n.id, n]));
168
+ const files = new Set();
169
+ for (const change of overlay.changes) {
170
+ // Use filePath from change if available
171
+ if (change.filePath) {
172
+ files.add(change.filePath);
173
+ continue;
174
+ }
175
+ // Fall back to KG node's filePath
176
+ const node = nodeMap.get(change.nodeId);
177
+ if (node?.filePath) {
178
+ files.add(node.filePath);
179
+ }
180
+ }
181
+ return [...files];
182
+ }
183
+ // ---------------------------------------------------------------------------
184
+ // Internal helpers
185
+ // ---------------------------------------------------------------------------
186
+ function groupEdges(edges, key) {
187
+ const map = new Map();
188
+ for (const edge of edges) {
189
+ const val = edge[key];
190
+ if (!map.has(val))
191
+ map.set(val, []);
192
+ map.get(val).push(edge);
193
+ }
194
+ return map;
195
+ }
196
+ /**
197
+ * Build clusters of related nodes to become families.
198
+ * Frontend: group by route/page/component modules
199
+ * Backend: group by API endpoint handlers + their service dependencies
200
+ */
201
+ function buildClusters(kg, projectType, nodeMap) {
202
+ const clusters = new Map();
203
+ // Strategy 1: Use KG layers to find anchor nodes
204
+ const layerMap = new Map();
205
+ if (kg.layers) {
206
+ for (const layer of kg.layers) {
207
+ layerMap.set(layer.name, new Set(layer.nodeIds));
208
+ }
209
+ }
210
+ // Strategy 2: Group by module/component/route nodes
211
+ const anchorKinds = projectType === 'backend'
212
+ ? new Set(['module', 'route', 'class', 'function'])
213
+ : new Set(['component', 'route', 'module', 'class']);
214
+ const anchorLayers = projectType === 'backend'
215
+ ? new Set(['api', 'service'])
216
+ : new Set(['ui']);
217
+ for (const node of kg.nodes) {
218
+ // Skip test/infra nodes as cluster anchors
219
+ if (node.layer === 'test' || node.layer === 'infra')
220
+ continue;
221
+ const isAnchorKind = anchorKinds.has(node.kind);
222
+ const isAnchorLayer = !node.layer || anchorLayers.has(node.layer);
223
+ if (!isAnchorKind || !isAnchorLayer)
224
+ continue;
225
+ // Derive cluster ID from node path or name
226
+ const clusterId = deriveClusterId(node);
227
+ if (!clusterId)
228
+ continue;
229
+ if (!clusters.has(clusterId)) {
230
+ clusters.set(clusterId, []);
231
+ }
232
+ clusters.get(clusterId).push(node.id);
233
+ }
234
+ // If no clusters found, fall back to file-based grouping
235
+ if (clusters.size === 0) {
236
+ for (const node of kg.nodes) {
237
+ if (!node.filePath || node.layer === 'test' || node.layer === 'infra')
238
+ continue;
239
+ const clusterId = deriveClusterIdFromPath(node.filePath);
240
+ if (!clusterId)
241
+ continue;
242
+ if (!clusters.has(clusterId)) {
243
+ clusters.set(clusterId, []);
244
+ }
245
+ clusters.get(clusterId).push(node.id);
246
+ }
247
+ }
248
+ return clusters;
249
+ }
250
+ // deriveClusterId and deriveClusterIdFromPath imported from cluster_utils.ts
251
+ function computePriority(nodes) {
252
+ const maxFanIn = Math.max(...nodes.map((n) => n.fanIn || 0));
253
+ if (maxFanIn >= 10)
254
+ return 'P0';
255
+ if (maxFanIn >= 4)
256
+ return 'P1';
257
+ return 'P2';
258
+ }
259
+ function buildFamilyFromCluster(clusterId, nodes, projectType, nodeMap, edgesByTarget) {
260
+ const webappPaths = new Set();
261
+ const serverPaths = new Set();
262
+ const routes = [];
263
+ const apiEndpoints = [];
264
+ for (const node of nodes) {
265
+ if (!node.filePath)
266
+ continue;
267
+ const layer = node.layer;
268
+ if (layer === 'api' || layer === 'service' || layer === 'data') {
269
+ serverPaths.add(`${node.filePath}*`);
270
+ }
271
+ else if (layer === 'ui') {
272
+ webappPaths.add(`${node.filePath}*`);
273
+ }
274
+ else {
275
+ // Auto-assign based on project type
276
+ if (projectType === 'backend') {
277
+ serverPaths.add(`${node.filePath}*`);
278
+ }
279
+ else {
280
+ webappPaths.add(`${node.filePath}*`);
281
+ }
282
+ }
283
+ // Extract routes from route nodes
284
+ if (node.kind === 'route') {
285
+ const routePath = node.metadata?.path;
286
+ if (routePath) {
287
+ routes.push(routePath);
288
+ // Extract API endpoint info
289
+ const method = node.metadata?.method || 'GET';
290
+ apiEndpoints.push({ method: method.toUpperCase(), path: routePath, description: node.description });
291
+ }
292
+ else {
293
+ routes.push(`/${clusterId}`);
294
+ }
295
+ }
296
+ }
297
+ // If no routes extracted, generate a default
298
+ if (routes.length === 0) {
299
+ routes.push(`/${clusterId}`);
300
+ }
301
+ // Collect related test file paths by following 'tests' edges
302
+ const specDirs = new Set();
303
+ for (const node of nodes) {
304
+ const testEdges = (edgesByTarget.get(node.id) || []).filter((e) => e.type === 'tests');
305
+ for (const edge of testEdges) {
306
+ const testNode = nodeMap.get(edge.source);
307
+ if (testNode?.filePath) {
308
+ // Use directory of test file
309
+ const dir = testNode.filePath.replace(/\\/g, '/').split('/').slice(0, -1).join('/');
310
+ if (dir)
311
+ specDirs.add(`${dir}/*`);
312
+ }
313
+ }
314
+ }
315
+ const priority = computePriority(nodes);
316
+ // Determine test type based on content
317
+ let testType;
318
+ if (webappPaths.size > 0 && serverPaths.size > 0) {
319
+ testType = 'both';
320
+ }
321
+ else if (serverPaths.size > 0 && apiEndpoints.length > 0) {
322
+ testType = 'api';
323
+ }
324
+ else if (webappPaths.size > 0) {
325
+ testType = 'ui';
326
+ }
327
+ const family = {
328
+ id: clusterId,
329
+ routes: [...new Set(routes)],
330
+ priority,
331
+ };
332
+ if (webappPaths.size > 0)
333
+ family.webappPaths = [...webappPaths];
334
+ if (serverPaths.size > 0)
335
+ family.serverPaths = [...serverPaths];
336
+ if (specDirs.size > 0)
337
+ family.specDirs = [...specDirs];
338
+ if (apiEndpoints.length > 0)
339
+ family.apiEndpoints = apiEndpoints;
340
+ if (testType)
341
+ family.testType = testType;
342
+ return family;
343
+ }
344
+ function assignTourFlows(families, tour, nodeMap) {
345
+ if (!tour)
346
+ return;
347
+ // Build a map of nodeId → family for quick lookup
348
+ const nodeToFamily = new Map();
349
+ for (const family of families) {
350
+ // Resolve family nodes from paths
351
+ for (const [nodeId, node] of nodeMap) {
352
+ if (!node.filePath)
353
+ continue;
354
+ const matchesPaths = [
355
+ ...(family.webappPaths || []),
356
+ ...(family.serverPaths || []),
357
+ ];
358
+ for (const pattern of matchesPaths) {
359
+ const prefix = pattern.replace(/\*$/, '');
360
+ if (node.filePath.startsWith(prefix)) {
361
+ nodeToFamily.set(nodeId, family);
362
+ break;
363
+ }
364
+ }
365
+ }
366
+ }
367
+ // Assign tour steps as user flows
368
+ for (const step of tour.sort((a, b) => a.order - b.order)) {
369
+ for (const nodeId of step.nodeIds) {
370
+ const family = nodeToFamily.get(nodeId);
371
+ if (family) {
372
+ if (!family.userFlows)
373
+ family.userFlows = [];
374
+ const flowDesc = `${step.title}: ${step.description}`;
375
+ if (!family.userFlows.includes(flowDesc)) {
376
+ family.userFlows.push(flowDesc);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
@@ -0,0 +1,3 @@
1
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
+ // See LICENSE.txt for license information.
3
+ export {};
@@ -30,6 +30,17 @@ export function matchesGlob(filePath, pattern) {
30
30
  export function matchesAnyPattern(filePath, patterns) {
31
31
  return patterns.some((pattern) => matchesGlob(filePath, pattern));
32
32
  }
33
+ function validateApiEndpoint(ep) {
34
+ if (!ep || typeof ep !== 'object')
35
+ return null;
36
+ const obj = ep;
37
+ if (typeof obj.method !== 'string' || typeof obj.path !== 'string')
38
+ return null;
39
+ const result = { method: obj.method, path: obj.path };
40
+ if (typeof obj.description === 'string')
41
+ result.description = obj.description;
42
+ return result;
43
+ }
33
44
  function validateFamily(family) {
34
45
  if (!family || typeof family !== 'object') {
35
46
  return null;
@@ -38,6 +49,10 @@ function validateFamily(family) {
38
49
  if (typeof obj.id !== 'string' || !obj.id.trim()) {
39
50
  return null;
40
51
  }
52
+ // When testType is 'api', routes may contain API paths like "GET /api/users"
53
+ const testType = (obj.testType === 'ui' || obj.testType === 'api' || obj.testType === 'both')
54
+ ? obj.testType
55
+ : undefined;
41
56
  if (!Array.isArray(obj.routes) || obj.routes.length === 0) {
42
57
  return null;
43
58
  }
@@ -81,6 +96,16 @@ function validateFamily(family) {
81
96
  .map((f) => validateFeature(f))
82
97
  .filter((f) => f !== null);
83
98
  }
99
+ if (Array.isArray(obj.apiEndpoints)) {
100
+ const endpoints = obj.apiEndpoints
101
+ .map((ep) => validateApiEndpoint(ep))
102
+ .filter((ep) => ep !== null);
103
+ if (endpoints.length > 0)
104
+ result.apiEndpoints = endpoints;
105
+ }
106
+ if (testType) {
107
+ result.testType = testType;
108
+ }
84
109
  return result;
85
110
  }
86
111
  function validateFeature(feature) {
@@ -272,3 +297,67 @@ export function getRoutesForBinding(manifest, binding) {
272
297
  export function clearManifestCache() {
273
298
  manifestCache.clear();
274
299
  }
300
+ /**
301
+ * Build heuristic route families from changed files when no manifest exists.
302
+ * Groups files by their top-level directory to create rough family groupings.
303
+ * Results are lower confidence but allow analysis to proceed without training.
304
+ */
305
+ export function buildHeuristicFamilies(changedFiles, testsRoot) {
306
+ const dirGroups = new Map();
307
+ for (const file of changedFiles) {
308
+ const normalized = file.replace(/\\/g, '/');
309
+ const parts = normalized.split('/');
310
+ // Use the first meaningful directory segment as the family ID
311
+ // Skip common prefixes like 'src/', 'app/', 'lib/'
312
+ const skipDirs = new Set(['src', 'app', 'lib', 'packages', 'components']);
313
+ let familyDir = parts[0] || 'root';
314
+ if (skipDirs.has(familyDir) && parts.length > 1) {
315
+ familyDir = parts[1];
316
+ }
317
+ // Normalize to a clean family name
318
+ familyDir = familyDir.replace(/\.[^.]+$/, ''); // strip file extensions for single files
319
+ if (!dirGroups.has(familyDir)) {
320
+ dirGroups.set(familyDir, []);
321
+ }
322
+ dirGroups.get(familyDir).push(normalized);
323
+ }
324
+ const families = [];
325
+ for (const [dir, files] of dirGroups) {
326
+ families.push({
327
+ id: dir,
328
+ routes: [`/${dir}`],
329
+ webappPaths: files.map((f) => `${f}*`),
330
+ });
331
+ }
332
+ logger.info(`Built ${families.length} heuristic families from ${changedFiles.length} changed files (no route-families.json found)`);
333
+ logger.info('Tip: Run `e2e-ai-agents train` to generate a proper route-families manifest for better accuracy.');
334
+ return {
335
+ families,
336
+ source: 'heuristic',
337
+ };
338
+ }
339
+ /**
340
+ * Serialize a RouteFamilyManifest to clean JSON, stripping empty optional fields.
341
+ */
342
+ export function serializeManifest(manifest) {
343
+ const output = {
344
+ families: manifest.families.map((f) => {
345
+ const cleaned = { ...f };
346
+ const optionalArrays = [
347
+ 'pageObjects', 'components', 'webappPaths', 'serverPaths',
348
+ 'specDirs', 'cypressSpecDirs', 'tags', 'userFlows', 'features', 'apiEndpoints',
349
+ ];
350
+ for (const key of optionalArrays) {
351
+ if (!cleaned[key] || (Array.isArray(cleaned[key]) && cleaned[key].length === 0)) {
352
+ delete cleaned[key];
353
+ }
354
+ }
355
+ if (!cleaned.priority)
356
+ delete cleaned.priority;
357
+ if (!cleaned.testType)
358
+ delete cleaned.testType;
359
+ return cleaned;
360
+ }),
361
+ };
362
+ return JSON.stringify(output, null, 2) + '\n';
363
+ }