@yasserkhanorg/e2e-agents 1.8.5 → 1.10.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 (274) 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/api_surface.js +265 -34
  125. package/dist/esm/knowledge/cluster_utils.js +60 -0
  126. package/dist/esm/knowledge/failure_history.js +121 -0
  127. package/dist/esm/knowledge/kg_bridge.js +381 -0
  128. package/dist/esm/knowledge/kg_types.js +3 -0
  129. package/dist/esm/knowledge/route_families.js +119 -0
  130. package/dist/esm/mcp-server.js +2 -4
  131. package/dist/esm/metrics/prometheus.js +149 -0
  132. package/dist/esm/model_router.js +59 -0
  133. package/dist/esm/ollama_provider.js +1 -0
  134. package/dist/esm/openai_provider.js +1 -0
  135. package/dist/esm/pipeline/orchestrator.js +6 -12
  136. package/dist/esm/pipeline/stage0_preprocess.js +12 -19
  137. package/dist/esm/pipeline/stage1_impact.js +19 -3
  138. package/dist/esm/pipeline/stage2_coverage.js +29 -7
  139. package/dist/esm/pipeline/stage3_generation.js +21 -1
  140. package/dist/esm/progress.js +112 -0
  141. package/dist/esm/prompts/coverage.js +17 -24
  142. package/dist/esm/prompts/cross-impact.js +3 -21
  143. package/dist/esm/prompts/generation.js +201 -45
  144. package/dist/esm/prompts/generation_profile.js +147 -0
  145. package/dist/esm/prompts/heal.js +33 -15
  146. package/dist/esm/prompts/impact.js +3 -22
  147. package/dist/esm/prompts/json_extract.js +36 -0
  148. package/dist/esm/prompts/strategist.js +2 -20
  149. package/dist/esm/prompts/test-designer.js +6 -21
  150. package/dist/esm/provider_factory.js +6 -4
  151. package/dist/esm/reporters/junit.js +86 -0
  152. package/dist/esm/reporters/reporter.js +3 -0
  153. package/dist/esm/reporters/sarif.js +131 -0
  154. package/dist/esm/resilience/circuit_breaker.js +78 -0
  155. package/dist/esm/resilience/retry.js +56 -0
  156. package/dist/esm/sanitize.js +66 -0
  157. package/dist/esm/training/kg_scanner.js +115 -0
  158. package/dist/esm/training/scanner.js +27 -34
  159. package/dist/esm/validation/guardrails.js +5 -0
  160. package/dist/esm/version.js +33 -0
  161. package/dist/index.d.ts +21 -1
  162. package/dist/index.d.ts.map +1 -1
  163. package/dist/index.js +45 -1
  164. package/dist/knowledge/api_surface.d.ts +12 -0
  165. package/dist/knowledge/api_surface.d.ts.map +1 -1
  166. package/dist/knowledge/api_surface.js +268 -34
  167. package/dist/knowledge/cluster_utils.d.ts +28 -0
  168. package/dist/knowledge/cluster_utils.d.ts.map +1 -0
  169. package/dist/knowledge/cluster_utils.js +67 -0
  170. package/dist/knowledge/failure_history.d.ts +39 -0
  171. package/dist/knowledge/failure_history.d.ts.map +1 -0
  172. package/dist/knowledge/failure_history.js +128 -0
  173. package/dist/knowledge/kg_bridge.d.ts +31 -0
  174. package/dist/knowledge/kg_bridge.d.ts.map +1 -0
  175. package/dist/knowledge/kg_bridge.js +388 -0
  176. package/dist/knowledge/kg_types.d.ts +75 -0
  177. package/dist/knowledge/kg_types.d.ts.map +1 -0
  178. package/dist/knowledge/kg_types.js +4 -0
  179. package/dist/knowledge/route_families.d.ts +29 -0
  180. package/dist/knowledge/route_families.d.ts.map +1 -1
  181. package/dist/knowledge/route_families.js +122 -0
  182. package/dist/mcp-server.d.ts.map +1 -1
  183. package/dist/mcp-server.js +2 -4
  184. package/dist/metrics/prometheus.d.ts +37 -0
  185. package/dist/metrics/prometheus.d.ts.map +1 -0
  186. package/dist/metrics/prometheus.js +153 -0
  187. package/dist/model_router.d.ts +28 -0
  188. package/dist/model_router.d.ts.map +1 -0
  189. package/dist/model_router.js +63 -0
  190. package/dist/ollama_provider.d.ts.map +1 -1
  191. package/dist/ollama_provider.js +1 -0
  192. package/dist/openai_provider.d.ts.map +1 -1
  193. package/dist/openai_provider.js +1 -0
  194. package/dist/pipeline/orchestrator.d.ts +2 -0
  195. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  196. package/dist/pipeline/orchestrator.js +6 -12
  197. package/dist/pipeline/stage0_preprocess.d.ts.map +1 -1
  198. package/dist/pipeline/stage0_preprocess.js +11 -18
  199. package/dist/pipeline/stage1_impact.d.ts +1 -1
  200. package/dist/pipeline/stage1_impact.d.ts.map +1 -1
  201. package/dist/pipeline/stage1_impact.js +18 -2
  202. package/dist/pipeline/stage2_coverage.d.ts +2 -0
  203. package/dist/pipeline/stage2_coverage.d.ts.map +1 -1
  204. package/dist/pipeline/stage2_coverage.js +29 -7
  205. package/dist/pipeline/stage3_generation.d.ts +2 -0
  206. package/dist/pipeline/stage3_generation.d.ts.map +1 -1
  207. package/dist/pipeline/stage3_generation.js +21 -1
  208. package/dist/pipeline/stage4_heal.d.ts +2 -0
  209. package/dist/pipeline/stage4_heal.d.ts.map +1 -1
  210. package/dist/progress.d.ts +22 -0
  211. package/dist/progress.d.ts.map +1 -0
  212. package/dist/progress.js +116 -0
  213. package/dist/prompts/coverage.d.ts +2 -0
  214. package/dist/prompts/coverage.d.ts.map +1 -1
  215. package/dist/prompts/coverage.js +17 -24
  216. package/dist/prompts/cross-impact.d.ts +1 -0
  217. package/dist/prompts/cross-impact.d.ts.map +1 -1
  218. package/dist/prompts/cross-impact.js +3 -21
  219. package/dist/prompts/generation.d.ts +4 -2
  220. package/dist/prompts/generation.d.ts.map +1 -1
  221. package/dist/prompts/generation.js +201 -45
  222. package/dist/prompts/generation_profile.d.ts +29 -0
  223. package/dist/prompts/generation_profile.d.ts.map +1 -0
  224. package/dist/prompts/generation_profile.js +151 -0
  225. package/dist/prompts/heal.d.ts +3 -1
  226. package/dist/prompts/heal.d.ts.map +1 -1
  227. package/dist/prompts/heal.js +33 -15
  228. package/dist/prompts/impact.d.ts +1 -0
  229. package/dist/prompts/impact.d.ts.map +1 -1
  230. package/dist/prompts/impact.js +3 -22
  231. package/dist/prompts/json_extract.d.ts +14 -0
  232. package/dist/prompts/json_extract.d.ts.map +1 -0
  233. package/dist/prompts/json_extract.js +39 -0
  234. package/dist/prompts/strategist.d.ts.map +1 -1
  235. package/dist/prompts/strategist.js +2 -20
  236. package/dist/prompts/test-designer.d.ts +2 -0
  237. package/dist/prompts/test-designer.d.ts.map +1 -1
  238. package/dist/prompts/test-designer.js +6 -21
  239. package/dist/provider_factory.d.ts.map +1 -1
  240. package/dist/provider_factory.js +6 -4
  241. package/dist/reporters/junit.d.ts +6 -0
  242. package/dist/reporters/junit.d.ts.map +1 -0
  243. package/dist/reporters/junit.js +89 -0
  244. package/dist/reporters/reporter.d.ts +42 -0
  245. package/dist/reporters/reporter.d.ts.map +1 -0
  246. package/dist/reporters/reporter.js +4 -0
  247. package/dist/reporters/sarif.d.ts +7 -0
  248. package/dist/reporters/sarif.d.ts.map +1 -0
  249. package/dist/reporters/sarif.js +134 -0
  250. package/dist/resilience/circuit_breaker.d.ts +36 -0
  251. package/dist/resilience/circuit_breaker.d.ts.map +1 -0
  252. package/dist/resilience/circuit_breaker.js +82 -0
  253. package/dist/resilience/retry.d.ts +11 -0
  254. package/dist/resilience/retry.d.ts.map +1 -0
  255. package/dist/resilience/retry.js +59 -0
  256. package/dist/sanitize.d.ts +15 -0
  257. package/dist/sanitize.d.ts.map +1 -0
  258. package/dist/sanitize.js +71 -0
  259. package/dist/training/kg_scanner.d.ts +13 -0
  260. package/dist/training/kg_scanner.d.ts.map +1 -0
  261. package/dist/training/kg_scanner.js +118 -0
  262. package/dist/training/scanner.d.ts +7 -2
  263. package/dist/training/scanner.d.ts.map +1 -1
  264. package/dist/training/scanner.js +27 -34
  265. package/dist/validation/guardrails.d.ts +2 -0
  266. package/dist/validation/guardrails.d.ts.map +1 -1
  267. package/dist/validation/guardrails.js +5 -0
  268. package/dist/validation/output_schema.d.ts +3 -0
  269. package/dist/validation/output_schema.d.ts.map +1 -1
  270. package/dist/version.d.ts +6 -0
  271. package/dist/version.d.ts.map +1 -0
  272. package/dist/version.js +36 -0
  273. package/package.json +7 -2
  274. package/schemas/route-families.schema.json +31 -1
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
3
  // See LICENSE.txt for license information.
4
+ var __importDefault = (this && this.__importDefault) || function (mod) {
5
+ return (mod && mod.__esModule) ? mod : { "default": mod };
6
+ };
4
7
  Object.defineProperty(exports, "__esModule", { value: true });
5
8
  exports.buildApiSurface = buildApiSurface;
6
9
  exports.loadOrBuildApiSurface = loadOrBuildApiSurface;
@@ -9,39 +12,233 @@ exports.validateMethodCall = validateMethodCall;
9
12
  exports.formatApiSurfaceForPrompt = formatApiSurfaceForPrompt;
10
13
  const fs_1 = require("fs");
11
14
  const path_1 = require("path");
15
+ const typescript_1 = __importDefault(require("typescript"));
16
+ // ── TypeScript AST-based extraction ────────────────────────
17
+ function extractMethodsFromAST(sourceFile, checker) {
18
+ const surfaces = [];
19
+ typescript_1.default.forEachChild(sourceFile, (node) => {
20
+ if (!typescript_1.default.isClassDeclaration(node) || !node.name) {
21
+ return;
22
+ }
23
+ const className = node.name.text;
24
+ const methods = [];
25
+ const seen = new Set();
26
+ // Get base class name if extends
27
+ let extendsName;
28
+ if (node.heritageClauses) {
29
+ for (const clause of node.heritageClauses) {
30
+ if (clause.token === typescript_1.default.SyntaxKind.ExtendsKeyword && clause.types.length > 0) {
31
+ const expr = clause.types[0].expression;
32
+ if (typescript_1.default.isIdentifier(expr)) {
33
+ extendsName = expr.text;
34
+ }
35
+ }
36
+ }
37
+ }
38
+ for (const member of node.members) {
39
+ // Skip constructor
40
+ if (typescript_1.default.isConstructorDeclaration(member)) {
41
+ continue;
42
+ }
43
+ // Skip private/protected members
44
+ const modifiers = typescript_1.default.canHaveModifiers(member) ? typescript_1.default.getModifiers(member) : undefined;
45
+ if (modifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.PrivateKeyword || m.kind === typescript_1.default.SyntaxKind.ProtectedKeyword)) {
46
+ continue;
47
+ }
48
+ const name = member.name && typescript_1.default.isIdentifier(member.name) ? member.name.text : null;
49
+ if (!name || name.startsWith('_') || seen.has(name)) {
50
+ continue;
51
+ }
52
+ seen.add(name);
53
+ if (typescript_1.default.isMethodDeclaration(member)) {
54
+ const isAsync = modifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.AsyncKeyword) ?? false;
55
+ const params = extractParams(member.parameters, checker);
56
+ const returnType = extractReturnType(member, checker);
57
+ methods.push({
58
+ name,
59
+ kind: 'method',
60
+ async: isAsync ? true : undefined,
61
+ params: params.length > 0 ? params : undefined,
62
+ returnType: returnType || undefined,
63
+ });
64
+ }
65
+ else if (typescript_1.default.isGetAccessorDeclaration(member)) {
66
+ methods.push({ name, kind: 'getter' });
67
+ }
68
+ else if (typescript_1.default.isPropertyDeclaration(member)) {
69
+ // Check if it's an arrow function property (e.g., name = async () => {})
70
+ if (member.initializer && (typescript_1.default.isArrowFunction(member.initializer) || typescript_1.default.isFunctionExpression(member.initializer))) {
71
+ const fn = member.initializer;
72
+ const fnModifiers = typescript_1.default.canHaveModifiers(fn) ? typescript_1.default.getModifiers(fn) : undefined;
73
+ const isAsync = fnModifiers?.some((m) => m.kind === typescript_1.default.SyntaxKind.AsyncKeyword) ?? false;
74
+ const params = extractParams(fn.parameters, checker);
75
+ methods.push({
76
+ name,
77
+ kind: 'method',
78
+ async: isAsync ? true : undefined,
79
+ params: params.length > 0 ? params : undefined,
80
+ });
81
+ }
82
+ else {
83
+ methods.push({ name, kind: 'property' });
84
+ }
85
+ }
86
+ }
87
+ if (methods.length > 0) {
88
+ surfaces.push({
89
+ className,
90
+ file: sourceFile.fileName,
91
+ methods,
92
+ extends: extendsName,
93
+ });
94
+ }
95
+ });
96
+ return surfaces;
97
+ }
98
+ function extractParams(params, checker) {
99
+ return params.map((p) => {
100
+ const name = typescript_1.default.isIdentifier(p.name) ? p.name.text : p.name.getText();
101
+ const optional = p.questionToken !== undefined || p.initializer !== undefined;
102
+ let type;
103
+ if (p.type) {
104
+ type = p.type.getText();
105
+ }
106
+ else if (checker) {
107
+ try {
108
+ const symbol = checker.getSymbolAtLocation(p.name);
109
+ if (symbol) {
110
+ const t = checker.getTypeOfSymbolAtLocation(symbol, p);
111
+ type = checker.typeToString(t);
112
+ }
113
+ }
114
+ catch {
115
+ // Type inference failure is non-fatal
116
+ }
117
+ }
118
+ return { name, type, optional: optional || undefined };
119
+ });
120
+ }
121
+ function extractReturnType(method, checker) {
122
+ if (method.type) {
123
+ return method.type.getText();
124
+ }
125
+ if (checker) {
126
+ try {
127
+ const signature = checker.getSignatureFromDeclaration(method);
128
+ if (signature) {
129
+ const returnType = checker.getReturnTypeOfSignature(signature);
130
+ const typeStr = checker.typeToString(returnType);
131
+ // Skip overly verbose inferred types
132
+ if (typeStr.length < 100) {
133
+ return typeStr;
134
+ }
135
+ }
136
+ }
137
+ catch {
138
+ // Type inference failure is non-fatal
139
+ }
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Extract page objects using the TypeScript Compiler API.
145
+ * Falls back to regex if compilation fails.
146
+ */
147
+ function extractWithAST(files) {
148
+ if (files.length === 0) {
149
+ return [];
150
+ }
151
+ // Find the nearest tsconfig.json
152
+ const firstDir = (0, path_1.join)(files[0], '..');
153
+ const tsconfigPath = typescript_1.default.findConfigFile(firstDir, typescript_1.default.sys.fileExists, 'tsconfig.json');
154
+ let program;
155
+ if (tsconfigPath) {
156
+ const configFile = typescript_1.default.readConfigFile(tsconfigPath, typescript_1.default.sys.readFile);
157
+ const parsedConfig = typescript_1.default.parseJsonConfigFileContent(configFile.config, typescript_1.default.sys, (0, path_1.join)(tsconfigPath, '..'));
158
+ // Only compile the files we care about, using the config's compiler options
159
+ program = typescript_1.default.createProgram(files, parsedConfig.options);
160
+ }
161
+ else {
162
+ program = typescript_1.default.createProgram(files, {
163
+ target: typescript_1.default.ScriptTarget.ES2022,
164
+ module: typescript_1.default.ModuleKind.ESNext,
165
+ moduleResolution: typescript_1.default.ModuleResolutionKind.Bundler,
166
+ strict: true,
167
+ allowJs: false,
168
+ noEmit: true,
169
+ });
170
+ }
171
+ const checker = program.getTypeChecker();
172
+ const surfaces = [];
173
+ for (const filePath of files) {
174
+ const sourceFile = program.getSourceFile(filePath);
175
+ if (!sourceFile) {
176
+ continue;
177
+ }
178
+ surfaces.push(...extractMethodsFromAST(sourceFile, checker));
179
+ }
180
+ // Resolve inherited methods: if class extends another class in the catalog,
181
+ // merge parent methods that the child doesn't override
182
+ resolveInheritance(surfaces);
183
+ return surfaces;
184
+ }
185
+ /**
186
+ * Merge parent class methods into child classes that extend them.
187
+ */
188
+ function resolveInheritance(surfaces) {
189
+ const byName = new Map(surfaces.map((s) => [s.className, s]));
190
+ for (const surface of surfaces) {
191
+ if (!surface.extends) {
192
+ continue;
193
+ }
194
+ const parent = byName.get(surface.extends);
195
+ if (!parent) {
196
+ continue;
197
+ }
198
+ const childMethodNames = new Set(surface.methods.map((m) => m.name));
199
+ for (const parentMethod of parent.methods) {
200
+ if (!childMethodNames.has(parentMethod.name)) {
201
+ surface.methods.push({ ...parentMethod });
202
+ }
203
+ }
204
+ }
205
+ }
206
+ // ── Regex-based extraction (fallback) ──────────────────────
12
207
  const RESERVED_WORDS = new Set([
13
208
  'private', 'protected', 'static', 'abstract', 'override',
14
209
  'if', 'for', 'while', 'switch', 'return',
15
210
  'const', 'let', 'var', 'import', 'export',
16
211
  'class', 'type', 'interface', 'constructor',
17
212
  ]);
18
- function extractMethodsFromSource(content) {
213
+ function extractMethodsFromRegex(content) {
19
214
  const methods = [];
20
215
  const seen = new Set();
21
- // Match async method declarations: async methodName(
22
216
  const asyncMethodRe = /(?:async\s+)([a-zA-Z_]\w*)\s*\(/g;
23
217
  let match;
24
218
  while ((match = asyncMethodRe.exec(content)) !== null) {
25
219
  const name = match[1];
26
- if (RESERVED_WORDS.has(name)) {
27
- continue;
28
- }
29
- if (!seen.has(name)) {
220
+ if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
30
221
  seen.add(name);
31
- methods.push({ name, kind: 'method' });
222
+ methods.push({ name, kind: 'method', async: true });
32
223
  }
33
224
  }
34
- // Match non-async public method patterns
35
225
  const methodRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*(?:\(|=\s*(?:async\s*)?\()/gm;
36
226
  while ((match = methodRe.exec(content)) !== null) {
37
227
  const name = match[1];
38
- if (RESERVED_WORDS.has(name) || seen.has(name)) {
39
- continue;
228
+ if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
229
+ seen.add(name);
230
+ const isAsync = match[0].includes('async');
231
+ methods.push({ name, kind: 'method', async: isAsync ? true : undefined });
232
+ }
233
+ }
234
+ const arrowRe = /^\s+([a-zA-Z_]\w*)\s*=\s*(async\s+)?(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>/gm;
235
+ while ((match = arrowRe.exec(content)) !== null) {
236
+ const name = match[1];
237
+ if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
238
+ seen.add(name);
239
+ methods.push({ name, kind: 'method', async: match[2] ? true : undefined });
40
240
  }
41
- seen.add(name);
42
- methods.push({ name, kind: 'method' });
43
241
  }
44
- // Match getter patterns: get propertyName()
45
242
  const getterRe = /\bget\s+([a-zA-Z_]\w*)\s*\(\)/g;
46
243
  while ((match = getterRe.exec(content)) !== null) {
47
244
  const name = match[1];
@@ -50,15 +247,13 @@ function extractMethodsFromSource(content) {
50
247
  methods.push({ name, kind: 'getter' });
51
248
  }
52
249
  }
53
- // Match readonly property declarations
54
250
  const propRe = /^\s+(?:readonly\s+)?([a-zA-Z_]\w*)\s*[:=]/gm;
55
251
  while ((match = propRe.exec(content)) !== null) {
56
252
  const name = match[1];
57
- if (RESERVED_WORDS.has(name) || seen.has(name)) {
58
- continue;
253
+ if (!RESERVED_WORDS.has(name) && !seen.has(name)) {
254
+ seen.add(name);
255
+ methods.push({ name, kind: 'property' });
59
256
  }
60
- seen.add(name);
61
- methods.push({ name, kind: 'property' });
62
257
  }
63
258
  return methods;
64
259
  }
@@ -66,7 +261,27 @@ function extractClassName(content) {
66
261
  const match = content.match(/(?:export\s+)?class\s+(\w+)/);
67
262
  return match ? match[1] : null;
68
263
  }
69
- function scanDirectory(dir) {
264
+ // ── Directory scanning ─────────────────────────────────────
265
+ function collectTypeScriptFiles(dir) {
266
+ const files = [];
267
+ if (!(0, fs_1.existsSync)(dir)) {
268
+ return files;
269
+ }
270
+ const entries = (0, fs_1.readdirSync)(dir, { withFileTypes: true });
271
+ for (const entry of entries) {
272
+ const fullPath = (0, path_1.join)(dir, entry.name);
273
+ if (entry.isDirectory()) {
274
+ files.push(...collectTypeScriptFiles(fullPath));
275
+ continue;
276
+ }
277
+ const ext = (0, path_1.extname)(entry.name);
278
+ if (ext === '.ts' || ext === '.tsx') {
279
+ files.push(fullPath);
280
+ }
281
+ }
282
+ return files;
283
+ }
284
+ function scanDirectoryWithRegex(dir) {
70
285
  const surfaces = [];
71
286
  if (!(0, fs_1.existsSync)(dir)) {
72
287
  return surfaces;
@@ -75,29 +290,22 @@ function scanDirectory(dir) {
75
290
  for (const entry of entries) {
76
291
  const fullPath = (0, path_1.join)(dir, entry.name);
77
292
  if (entry.isDirectory()) {
78
- surfaces.push(...scanDirectory(fullPath));
293
+ surfaces.push(...scanDirectoryWithRegex(fullPath));
79
294
  continue;
80
295
  }
81
296
  const ext = (0, path_1.extname)(entry.name);
82
297
  if (ext !== '.ts' && ext !== '.tsx') {
83
298
  continue;
84
299
  }
85
- if (entry.name === 'index.ts' || entry.name === 'index.tsx') {
86
- continue;
87
- }
88
300
  try {
89
301
  const content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
90
302
  const className = extractClassName(content);
91
303
  if (!className) {
92
304
  continue;
93
305
  }
94
- const extractedMethods = extractMethodsFromSource(content);
306
+ const extractedMethods = extractMethodsFromRegex(content);
95
307
  if (extractedMethods.length > 0) {
96
- surfaces.push({
97
- className,
98
- file: fullPath,
99
- methods: extractedMethods,
100
- });
308
+ surfaces.push({ className, file: fullPath, methods: extractedMethods });
101
309
  }
102
310
  }
103
311
  catch {
@@ -106,6 +314,7 @@ function scanDirectory(dir) {
106
314
  }
107
315
  return surfaces;
108
316
  }
317
+ // ── Public API ──────────────────────────────────────────────
109
318
  function buildApiSurface(testsRoot, config) {
110
319
  const pageObjectsDir = config?.pageObjectsDir
111
320
  ? (0, path_1.join)(testsRoot, config.pageObjectsDir)
@@ -113,10 +322,30 @@ function buildApiSurface(testsRoot, config) {
113
322
  const componentsDir = config?.componentsDir
114
323
  ? (0, path_1.join)(testsRoot, config.componentsDir)
115
324
  : (0, path_1.join)(testsRoot, 'lib', 'src', 'ui', 'components');
116
- const pageObjects = [
117
- ...scanDirectory(pageObjectsDir),
118
- ...scanDirectory(componentsDir),
119
- ];
325
+ let pageObjects;
326
+ if (config?.useRegexFallback) {
327
+ pageObjects = [
328
+ ...scanDirectoryWithRegex(pageObjectsDir),
329
+ ...scanDirectoryWithRegex(componentsDir),
330
+ ];
331
+ }
332
+ else {
333
+ // Use TypeScript AST — full type info, inheritance, params
334
+ const allFiles = [
335
+ ...collectTypeScriptFiles(pageObjectsDir),
336
+ ...collectTypeScriptFiles(componentsDir),
337
+ ];
338
+ try {
339
+ pageObjects = extractWithAST(allFiles);
340
+ }
341
+ catch {
342
+ // Fall back to regex if AST extraction fails
343
+ pageObjects = [
344
+ ...scanDirectoryWithRegex(pageObjectsDir),
345
+ ...scanDirectoryWithRegex(componentsDir),
346
+ ];
347
+ }
348
+ }
120
349
  return {
121
350
  pageObjects,
122
351
  generatedAt: new Date().toISOString(),
@@ -175,7 +404,12 @@ function formatApiSurfaceForPrompt(catalog, classNames) {
175
404
  if (m.kind === 'getter') {
176
405
  return ` get ${m.name}()`;
177
406
  }
178
- return ` ${m.name}()`;
407
+ const prefix = m.async ? 'async ' : '';
408
+ const paramStr = m.params
409
+ ? m.params.map((p) => `${p.name}${p.optional ? '?' : ''}${p.type ? `: ${p.type}` : ''}`).join(', ')
410
+ : '';
411
+ const retStr = m.returnType ? `: ${m.returnType}` : '';
412
+ return ` ${prefix}${m.name}(${paramStr})${retStr}`;
179
413
  })
180
414
  .join('\n');
181
415
  sections.push(`${name}:\n${methodList}`);
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared cluster ID derivation for knowledge graph processing.
3
+ * Used by both kg_bridge.ts and kg_scanner.ts.
4
+ */
5
+ /** Default directories to skip when deriving cluster IDs from file paths. */
6
+ declare const DEFAULT_SKIP_DIRS: Set<string>;
7
+ /** Extended skip set that also excludes test directories. */
8
+ declare const SKIP_DIRS_WITH_TESTS: Set<string>;
9
+ /**
10
+ * Normalize a name to a snake_case cluster ID.
11
+ * Handles camelCase conversion, then strips non-alphanumeric characters.
12
+ */
13
+ export declare function normalizeToClusterId(name: string): string;
14
+ /**
15
+ * Derive a cluster ID from a node that has an optional filePath and name.
16
+ * Prefers file path grouping for consistency.
17
+ */
18
+ export declare function deriveClusterId(node: {
19
+ filePath?: string;
20
+ name: string;
21
+ }, skipDirs?: Set<string>): string | null;
22
+ /**
23
+ * Derive a cluster ID from a file path by finding the first meaningful
24
+ * directory segment after skipping common structural prefixes.
25
+ */
26
+ export declare function deriveClusterIdFromPath(filePath: string, skipDirs?: Set<string>): string | null;
27
+ export { DEFAULT_SKIP_DIRS, SKIP_DIRS_WITH_TESTS };
28
+ //# sourceMappingURL=cluster_utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cluster_utils.d.ts","sourceRoot":"","sources":["../../src/knowledge/cluster_utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,6EAA6E;AAC7E,QAAA,MAAM,iBAAiB,aAGrB,CAAC;AAEH,6DAA6D;AAC7D,QAAA,MAAM,oBAAoB,aAGxB,CAAC;AAEH;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMzD;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAC3B,IAAI,EAAE;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAC,EACvC,QAAQ,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,GACvB,MAAM,GAAG,IAAI,CAMf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,GAAE,GAAG,CAAC,MAAM,CAAqB,GAC1C,MAAM,GAAG,IAAI,CAef;AAED,OAAO,EAAC,iBAAiB,EAAE,oBAAoB,EAAC,CAAC"}
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.SKIP_DIRS_WITH_TESTS = exports.DEFAULT_SKIP_DIRS = void 0;
6
+ exports.normalizeToClusterId = normalizeToClusterId;
7
+ exports.deriveClusterId = deriveClusterId;
8
+ exports.deriveClusterIdFromPath = deriveClusterIdFromPath;
9
+ /**
10
+ * Shared cluster ID derivation for knowledge graph processing.
11
+ * Used by both kg_bridge.ts and kg_scanner.ts.
12
+ */
13
+ /** Default directories to skip when deriving cluster IDs from file paths. */
14
+ const DEFAULT_SKIP_DIRS = new Set([
15
+ 'src', 'app', 'lib', 'packages', 'server', 'api', 'pages',
16
+ 'components', 'features', 'modules',
17
+ ]);
18
+ exports.DEFAULT_SKIP_DIRS = DEFAULT_SKIP_DIRS;
19
+ /** Extended skip set that also excludes test directories. */
20
+ const SKIP_DIRS_WITH_TESTS = new Set([
21
+ ...DEFAULT_SKIP_DIRS,
22
+ 'test', 'tests', 'e2e', 'spec', 'specs',
23
+ ]);
24
+ exports.SKIP_DIRS_WITH_TESTS = SKIP_DIRS_WITH_TESTS;
25
+ /**
26
+ * Normalize a name to a snake_case cluster ID.
27
+ * Handles camelCase conversion, then strips non-alphanumeric characters.
28
+ */
29
+ function normalizeToClusterId(name) {
30
+ return name
31
+ .replace(/[A-Z]/g, (c, idx) => (idx > 0 ? `_${c.toLowerCase()}` : c.toLowerCase()))
32
+ .replace(/[^a-z0-9_]/g, '_')
33
+ .replace(/_+/g, '_')
34
+ .replace(/^_|_$/g, '');
35
+ }
36
+ /**
37
+ * Derive a cluster ID from a node that has an optional filePath and name.
38
+ * Prefers file path grouping for consistency.
39
+ */
40
+ function deriveClusterId(node, skipDirs) {
41
+ if (node.filePath) {
42
+ return deriveClusterIdFromPath(node.filePath, skipDirs);
43
+ }
44
+ const name = normalizeToClusterId(node.name);
45
+ return name && name.length > 1 ? name : null;
46
+ }
47
+ /**
48
+ * Derive a cluster ID from a file path by finding the first meaningful
49
+ * directory segment after skipping common structural prefixes.
50
+ */
51
+ function deriveClusterIdFromPath(filePath, skipDirs = DEFAULT_SKIP_DIRS) {
52
+ const parts = filePath.replace(/\\/g, '/').split('/').filter(Boolean);
53
+ for (const part of parts) {
54
+ if (skipDirs.has(part))
55
+ continue;
56
+ if (part.includes('.'))
57
+ continue; // skip files
58
+ const normalized = part.toLowerCase()
59
+ .replace(/[^a-z0-9_]/g, '_')
60
+ .replace(/_+/g, '_')
61
+ .replace(/^_|_$/g, '');
62
+ if (normalized && normalized.length > 1) {
63
+ return normalized;
64
+ }
65
+ }
66
+ return null;
67
+ }
@@ -0,0 +1,39 @@
1
+ export interface FailureCorrelation {
2
+ /** Source file that was changed */
3
+ changedFile: string;
4
+ /** Spec file that failed */
5
+ specFile: string;
6
+ /** Number of times this correlation has been observed */
7
+ count: number;
8
+ /** ISO timestamp of last observation */
9
+ lastSeen: string;
10
+ }
11
+ export interface FailureHistory {
12
+ correlations: FailureCorrelation[];
13
+ /** Total runs recorded */
14
+ totalRuns: number;
15
+ /** ISO timestamp of last update */
16
+ updatedAt: string;
17
+ }
18
+ export declare function loadFailureHistory(testsRoot: string): FailureHistory;
19
+ export declare function saveFailureHistory(testsRoot: string, history: FailureHistory): void;
20
+ /**
21
+ * Record that a set of changed files caused a set of spec failures.
22
+ * Call this after a test run where failures were observed.
23
+ */
24
+ export declare function recordFailures(history: FailureHistory, changedFiles: string[], failedSpecs: string[]): FailureHistory;
25
+ /**
26
+ * Get a confidence boost (0-20) for a file based on historical failure patterns.
27
+ * A file that historically causes test failures gets a higher confidence boost
28
+ * when detected as impacted, meaning the system is more confident it needs testing.
29
+ */
30
+ export declare function getConfidenceBoost(history: FailureHistory, changedFile: string): number;
31
+ /**
32
+ * Get the most likely failing specs for a set of changed files, based on history.
33
+ * Returns specs sorted by correlation strength (count * recency).
34
+ */
35
+ export declare function getPredictedFailures(history: FailureHistory, changedFiles: string[], limit?: number): Array<{
36
+ specFile: string;
37
+ score: number;
38
+ }>;
39
+ //# sourceMappingURL=failure_history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure_history.d.ts","sourceRoot":"","sources":["../../src/knowledge/failure_history.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,kBAAkB;IAC/B,mCAAmC;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC3B,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,0BAA0B;IAC1B,SAAS,EAAE,MAAM,CAAC;IAClB,mCAAmC;IACnC,SAAS,EAAE,MAAM,CAAC;CACrB;AAQD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,CAcpE;AAED,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,cAAc,GAAG,IAAI,CAYnF;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC1B,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EAAE,EACtB,WAAW,EAAE,MAAM,EAAE,GACtB,cAAc,CA8BhB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAevF;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAChC,OAAO,EAAE,cAAc,EACvB,YAAY,EAAE,MAAM,EAAE,EACtB,KAAK,SAAK,GACX,KAAK,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAC,CAAC,CAoB1C"}
@@ -0,0 +1,128 @@
1
+ "use strict";
2
+ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
+ // See LICENSE.txt for license information.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.loadFailureHistory = loadFailureHistory;
6
+ exports.saveFailureHistory = saveFailureHistory;
7
+ exports.recordFailures = recordFailures;
8
+ exports.getConfidenceBoost = getConfidenceBoost;
9
+ exports.getPredictedFailures = getPredictedFailures;
10
+ /**
11
+ * Tracks historical test failure correlations: which tests fail when certain files change.
12
+ * Used to boost confidence in impact analysis — if a file change historically breaks a test,
13
+ * future changes to that file should prioritize that test.
14
+ *
15
+ * Data is stored as a JSON file at .e2e-ai-agents/failure-history.json.
16
+ */
17
+ const fs_1 = require("fs");
18
+ const path_1 = require("path");
19
+ const DEFAULT_HISTORY = {
20
+ correlations: [],
21
+ totalRuns: 0,
22
+ updatedAt: new Date().toISOString(),
23
+ };
24
+ function loadFailureHistory(testsRoot) {
25
+ const historyPath = (0, path_1.join)(testsRoot, '.e2e-ai-agents', 'failure-history.json');
26
+ if (!(0, fs_1.existsSync)(historyPath)) {
27
+ return { ...DEFAULT_HISTORY };
28
+ }
29
+ try {
30
+ const raw = JSON.parse((0, fs_1.readFileSync)(historyPath, 'utf-8'));
31
+ if (!Array.isArray(raw.correlations)) {
32
+ return { ...DEFAULT_HISTORY };
33
+ }
34
+ return raw;
35
+ }
36
+ catch {
37
+ return { ...DEFAULT_HISTORY };
38
+ }
39
+ }
40
+ function saveFailureHistory(testsRoot, history) {
41
+ const historyPath = (0, path_1.join)(testsRoot, '.e2e-ai-agents', 'failure-history.json');
42
+ try {
43
+ const dir = (0, path_1.dirname)(historyPath);
44
+ if (!(0, fs_1.existsSync)(dir)) {
45
+ (0, fs_1.mkdirSync)(dir, { recursive: true });
46
+ }
47
+ history.updatedAt = new Date().toISOString();
48
+ (0, fs_1.writeFileSync)(historyPath, JSON.stringify(history, null, 2), 'utf-8');
49
+ }
50
+ catch {
51
+ // Non-fatal — history is advisory, not required
52
+ }
53
+ }
54
+ /**
55
+ * Record that a set of changed files caused a set of spec failures.
56
+ * Call this after a test run where failures were observed.
57
+ */
58
+ function recordFailures(history, changedFiles, failedSpecs) {
59
+ const now = new Date().toISOString();
60
+ const updated = { ...history, totalRuns: history.totalRuns + 1, correlations: [...history.correlations] };
61
+ for (const changedFile of changedFiles) {
62
+ for (const specFile of failedSpecs) {
63
+ const existing = updated.correlations.find((c) => c.changedFile === changedFile && c.specFile === specFile);
64
+ if (existing) {
65
+ existing.count++;
66
+ existing.lastSeen = now;
67
+ }
68
+ else {
69
+ updated.correlations.push({
70
+ changedFile,
71
+ specFile,
72
+ count: 1,
73
+ lastSeen: now,
74
+ });
75
+ }
76
+ }
77
+ }
78
+ // Prune stale correlations (not seen in 90 days)
79
+ const cutoff = new Date();
80
+ cutoff.setDate(cutoff.getDate() - 90);
81
+ const cutoffStr = cutoff.toISOString();
82
+ updated.correlations = updated.correlations.filter((c) => c.lastSeen >= cutoffStr);
83
+ return updated;
84
+ }
85
+ /**
86
+ * Get a confidence boost (0-20) for a file based on historical failure patterns.
87
+ * A file that historically causes test failures gets a higher confidence boost
88
+ * when detected as impacted, meaning the system is more confident it needs testing.
89
+ */
90
+ function getConfidenceBoost(history, changedFile) {
91
+ const correlations = history.correlations.filter((c) => c.changedFile === changedFile);
92
+ if (correlations.length === 0) {
93
+ return 0;
94
+ }
95
+ // More correlations and higher counts = more confidence
96
+ const totalCount = correlations.reduce((sum, c) => sum + c.count, 0);
97
+ const uniqueSpecs = correlations.length;
98
+ // Scale: 1 correlation = +5, 3+ = +10, 5+ with high counts = +15, max +20
99
+ if (totalCount >= 10 && uniqueSpecs >= 5)
100
+ return 20;
101
+ if (totalCount >= 5 && uniqueSpecs >= 3)
102
+ return 15;
103
+ if (totalCount >= 3)
104
+ return 10;
105
+ return 5;
106
+ }
107
+ /**
108
+ * Get the most likely failing specs for a set of changed files, based on history.
109
+ * Returns specs sorted by correlation strength (count * recency).
110
+ */
111
+ function getPredictedFailures(history, changedFiles, limit = 10) {
112
+ const specScores = new Map();
113
+ for (const changedFile of changedFiles) {
114
+ for (const c of history.correlations) {
115
+ if (c.changedFile !== changedFile)
116
+ continue;
117
+ // Score: count weighted by recency (days since last seen)
118
+ const daysSince = (Date.now() - new Date(c.lastSeen).getTime()) / (1000 * 60 * 60 * 24);
119
+ const recencyWeight = Math.max(0.1, 1 - daysSince / 90);
120
+ const score = c.count * recencyWeight;
121
+ specScores.set(c.specFile, (specScores.get(c.specFile) || 0) + score);
122
+ }
123
+ }
124
+ return Array.from(specScores.entries())
125
+ .map(([specFile, score]) => ({ specFile, score }))
126
+ .sort((a, b) => b.score - a.score)
127
+ .slice(0, limit);
128
+ }