@yasserkhanorg/e2e-agents 1.6.0 → 1.7.1

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.
@@ -6,6 +6,7 @@ exports.runPipeline = runPipeline;
6
6
  const fs_1 = require("fs");
7
7
  const path_1 = require("path");
8
8
  const git_js_1 = require("../agent/git.js");
9
+ const logger_js_1 = require("../logger.js");
9
10
  const stage0_preprocess_js_1 = require("./stage0_preprocess.js");
10
11
  const stage1_impact_js_1 = require("./stage1_impact.js");
11
12
  const stage2_coverage_js_1 = require("./stage2_coverage.js");
@@ -61,20 +62,25 @@ async function runPipeline(config) {
61
62
  const reportPath = writeReport(config.testsRoot, emptyReport);
62
63
  return { report: emptyReport, reportPath, warnings: allWarnings };
63
64
  }
65
+ const timings = {};
64
66
  // Step 2: Preprocess — deterministic file classification + route family binding
67
+ const preprocessTimer = logger_js_1.logger.timer('preprocess');
65
68
  const preprocessResult = (0, stage0_preprocess_js_1.preprocess)(changedFiles, {
66
69
  appPath: config.appPath,
67
70
  testsRoot: config.testsRoot,
68
71
  routeFamilies: config.routeFamilies,
69
72
  apiSurface: config.apiSurface,
70
73
  });
74
+ timings.preprocess = preprocessTimer.end();
71
75
  allWarnings.push(...preprocessResult.warnings);
72
76
  let decisions = [];
73
77
  // Step 3: Impact stage — AI-powered flow identification per family
74
78
  if (stages.includes('impact')) {
79
+ const impactTimer = logger_js_1.logger.timer('impact');
75
80
  const impactResult = await (0, stage1_impact_js_1.runImpactStage)(preprocessResult.familyGroups, preprocessResult.manifest, preprocessResult.specIndex, preprocessResult.apiSurface, preprocessResult.context, config.impact || {});
76
81
  decisions = impactResult.decisions;
77
82
  allWarnings.push(...impactResult.warnings);
83
+ timings.impact = impactTimer.end();
78
84
  // Check cannot_determine ratio
79
85
  const cannotDetermineRatio = (0, guardrails_js_1.computeCannotDetermineRatio)(decisions);
80
86
  if (cannotDetermineRatio > 0.3) {
@@ -83,18 +89,23 @@ async function runPipeline(config) {
83
89
  }
84
90
  // Step 4: Coverage stage — AI-powered spec coverage evaluation
85
91
  if (stages.includes('coverage') && decisions.length > 0) {
92
+ const coverageTimer = logger_js_1.logger.timer('coverage');
86
93
  const coverageResult = await (0, stage2_coverage_js_1.runCoverageStage)(decisions, preprocessResult.specIndex, preprocessResult.context, config.testsRoot, config.coverage || {});
87
94
  decisions = coverageResult.decisions;
95
+ timings.coverage = coverageTimer.end();
88
96
  allWarnings.push(...coverageResult.warnings);
89
97
  }
90
98
  // Step 5: Generation stage — AI-powered spec generation for create_spec / add_scenarios
91
99
  if (stages.includes('generation') && decisions.length > 0) {
100
+ const generationTimer = logger_js_1.logger.timer('generation');
92
101
  const generationResult = await (0, stage3_generation_js_1.runGenerationStage)(decisions, preprocessResult.apiSurface, config.testsRoot, config.generation || {});
93
102
  generatedSpecs = generationResult.generated;
103
+ timings.generation = generationTimer.end();
94
104
  allWarnings.push(...generationResult.warnings);
95
105
  }
96
106
  // Step 6: Heal stage — MCP-backed playwright-test-healer for failing/flaky specs
97
107
  if (stages.includes('heal')) {
108
+ const healTimer = logger_js_1.logger.timer('heal');
98
109
  const healTargets = (0, stage4_heal_js_1.resolveHealTargets)(config.testsRoot, {
99
110
  playwrightReportPath: config.playwrightReportPath,
100
111
  generatedSpecs,
@@ -106,6 +117,7 @@ async function runPipeline(config) {
106
117
  else {
107
118
  allWarnings.push('Heal stage: no targets found (no failing specs in report, no generated specs).');
108
119
  }
120
+ timings.heal = healTimer.end();
109
121
  }
110
122
  // Build report
111
123
  const report = {
@@ -121,16 +133,18 @@ async function runPipeline(config) {
121
133
  generationAgent: stages.includes('generation') ? (config.generation?.provider || 'auto') : undefined,
122
134
  },
123
135
  };
124
- const reportPath = writeReport(config.testsRoot, report, healResult);
136
+ const reportPath = writeReport(config.testsRoot, report, healResult, timings);
125
137
  return { report, reportPath, warnings: allWarnings, generated: generatedSpecs, healResult };
126
138
  }
127
- function writeReport(testsRoot, report, healResult) {
139
+ function writeReport(testsRoot, report, healResult, timings) {
128
140
  const outputDir = (0, path_1.join)(testsRoot, '.e2e-ai-agents');
129
141
  if (!(0, fs_1.existsSync)(outputDir)) {
130
142
  (0, fs_1.mkdirSync)(outputDir, { recursive: true });
131
143
  }
144
+ // Include timings in the JSON report if available
145
+ const reportWithTimings = timings ? { ...report, timings } : report;
132
146
  const jsonPath = (0, path_1.join)(outputDir, 'pipeline-report.json');
133
- (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
147
+ (0, fs_1.writeFileSync)(jsonPath, JSON.stringify(reportWithTimings, null, 2), 'utf-8');
134
148
  const mdPath = (0, path_1.join)(outputDir, 'pipeline-report.md');
135
149
  (0, fs_1.writeFileSync)(mdPath, renderMarkdown(report, healResult), 'utf-8');
136
150
  return jsonPath;
@@ -1 +1 @@
1
- {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkLhE,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAmBlE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBrE;AAkCD,wBAAsB,cAAc,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,aAAa,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAgF3B"}
1
+ {"version":3,"file":"enricher.d.ts","sourceRoot":"","sources":["../../src/training/enricher.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,0BAA0B,CAAC;AAC1D,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,gCAAgC,CAAC;AAGhE,OAAO,KAAK,EAAC,gBAAgB,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkLhE,MAAM,WAAW,aAAa;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,aAAa,EAAE,CAmBlE;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,MAAM,GAAG,aAAa,EAAE,CAwBrE;AAkCD,wBAAsB,cAAc,CAChC,QAAQ,EAAE,WAAW,EAAE,EACvB,OAAO,EAAE,aAAa,EAAE,EACxB,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,WAAW,EACrB,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,GACnB,OAAO,CAAC,gBAAgB,CAAC,CAuF3B"}
@@ -268,6 +268,8 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
268
268
  const enriched = [];
269
269
  let totalTokens = 0;
270
270
  let totalCost = 0;
271
+ let requestCount = 0;
272
+ let totalResponseMs = 0;
271
273
  const skipped = [];
272
274
  // Process in chunks of 4 families
273
275
  const chunkSize = 4;
@@ -300,15 +302,18 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
300
302
  prompt = prompt.slice(0, MAX_PROMPT_CHARS);
301
303
  }
302
304
  }
303
- let timer;
305
+ let timeoutTimer;
304
306
  try {
305
307
  const timeoutPromise = new Promise((_, reject) => {
306
- timer = setTimeout(() => reject(new Error('LLM request timed out')), LLM_TIMEOUT_MS);
308
+ timeoutTimer = setTimeout(() => reject(new Error('LLM request timed out')), LLM_TIMEOUT_MS);
307
309
  });
310
+ const reqStart = performance.now();
308
311
  const response = await Promise.race([
309
312
  provider.generateText(prompt, { maxTokens: 4096, temperature: 0.3 }),
310
313
  timeoutPromise,
311
314
  ]);
315
+ totalResponseMs += performance.now() - reqStart;
316
+ requestCount++;
312
317
  totalTokens += (response.usage?.inputTokens ?? 0) + (response.usage?.outputTokens ?? 0);
313
318
  totalCost += response.cost ?? 0;
314
319
  const entries = parseEnrichResponse(response.text);
@@ -329,8 +334,8 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
329
334
  enriched.push(...chunk);
330
335
  }
331
336
  finally {
332
- if (timer)
333
- clearTimeout(timer);
337
+ if (timeoutTimer)
338
+ clearTimeout(timeoutTimer);
334
339
  }
335
340
  }
336
341
  return {
@@ -338,5 +343,7 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
338
343
  tokensUsed: totalTokens,
339
344
  costUSD: Math.round(totalCost * 100) / 100,
340
345
  skippedFamilies: skipped,
346
+ requestCount,
347
+ avgResponseMs: requestCount > 0 ? Math.round(totalResponseMs / requestCount) : 0,
341
348
  };
342
349
  }
@@ -12,7 +12,20 @@ export declare function discoverTestDirs(projectRoot: string): DiscoveredDir[];
12
12
  *
13
13
  * Each domain becomes a candidate family with precise serverPaths.
14
14
  */
15
- export declare function discoverServerDerivedFamilies(serverRoot: string): ScannedFamily[];
15
+ export declare function discoverServerDerivedFamilies(serverRoot: string): {
16
+ multiTierFamilies: ScannedFamily[];
17
+ singleTierFamilies: ScannedFamily[];
18
+ };
16
19
  export declare function discoverTestDerivedFamilies(testsRoot: string): ScannedFamily[];
17
- export declare function scanProject(projectRoot: string, testsRoot?: string, serverRoot?: string): ScanResult;
20
+ /**
21
+ * Discover test library paths (page objects, helpers) organized by feature.
22
+ * Walks well-known test lib directories and maps subdirectories and files to family IDs.
23
+ */
24
+ export declare function discoverTestLibPaths(testsRoot: string): Map<string, string[]>;
25
+ /**
26
+ * Discover files in well-known directories (types, utils) whose basename
27
+ * maps directly to a family ID.
28
+ */
29
+ export declare function discoverNameMatchedPaths(appPath: string, gitRepoRoot?: string): Map<string, string[]>;
30
+ export declare function scanProject(projectRoot: string, testsRoot?: string, serverRoot?: string, gitRepoRoot?: string): ScanResult;
18
31
  //# sourceMappingURL=scanner.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAkB,UAAU,EAAC,MAAM,YAAY,CAAC;AAgJzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AAuLD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,aAAa,EAAE,CA0HjF;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAiG9E;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CA6IpG"}
1
+ {"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAkB,UAAU,EAAC,MAAM,YAAY,CAAC;AAgJzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AAuLD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG;IAAC,iBAAiB,EAAE,aAAa,EAAE,CAAC;IAAC,kBAAkB,EAAE,aAAa,EAAE,CAAA;CAAC,CAgI3I;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAiG9E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CA8C7E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACpC,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACrB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAyDvB;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CA0L1H"}
@@ -6,6 +6,8 @@ exports.discoverSourceDirs = discoverSourceDirs;
6
6
  exports.discoverTestDirs = discoverTestDirs;
7
7
  exports.discoverServerDerivedFamilies = discoverServerDerivedFamilies;
8
8
  exports.discoverTestDerivedFamilies = discoverTestDerivedFamilies;
9
+ exports.discoverTestLibPaths = discoverTestLibPaths;
10
+ exports.discoverNameMatchedPaths = discoverNameMatchedPaths;
9
11
  exports.scanProject = scanProject;
10
12
  const fs_1 = require("fs");
11
13
  const path_1 = require("path");
@@ -520,15 +522,15 @@ function discoverServerDerivedFamilies(serverRoot) {
520
522
  }
521
523
  }
522
524
  // Build families from grouped domains.
523
- // Only include server-only families that span 2 tiers (architecturally significant).
524
- const families = [];
525
+ // Multi-tier families (≥2 tiers) can be new families.
526
+ // Single-tier families can only merge into existing families.
527
+ const multiTierFamilies = [];
528
+ const singleTierFamilies = [];
525
529
  for (const [domain, paths] of familyPaths) {
526
530
  if (paths.size === 0)
527
531
  continue;
528
532
  const tierCount = familyTiers.get(domain)?.size ?? 0;
529
- if (tierCount < 2)
530
- continue; // Skip single-tier domains (likely infrastructure)
531
- families.push({
533
+ const family = {
532
534
  id: domain,
533
535
  routes: [`/${domain.replace(/_/g, '-')}`],
534
536
  webappPaths: [],
@@ -538,9 +540,15 @@ function discoverServerDerivedFamilies(serverRoot) {
538
540
  tags: [],
539
541
  features: [],
540
542
  routesGuessed: true,
541
- });
543
+ };
544
+ if (tierCount >= 2) {
545
+ multiTierFamilies.push(family);
546
+ }
547
+ else {
548
+ singleTierFamilies.push(family);
549
+ }
542
550
  }
543
- return families;
551
+ return { multiTierFamilies, singleTierFamilies };
544
552
  }
545
553
  function discoverTestDerivedFamilies(testsRoot) {
546
554
  const resolved = (0, path_1.resolve)(testsRoot);
@@ -627,7 +635,136 @@ function discoverTestDerivedFamilies(testsRoot) {
627
635
  }
628
636
  return Array.from(familyMap.values());
629
637
  }
630
- function scanProject(projectRoot, testsRoot, serverRoot) {
638
+ /**
639
+ * Discover test library paths (page objects, helpers) organized by feature.
640
+ * Walks well-known test lib directories and maps subdirectories and files to family IDs.
641
+ */
642
+ function discoverTestLibPaths(testsRoot) {
643
+ const resolved = (0, path_1.resolve)(testsRoot);
644
+ const result = new Map();
645
+ const libDirs = [
646
+ 'lib/src/ui/components',
647
+ 'lib/src/ui/pages',
648
+ 'lib/src/server',
649
+ ];
650
+ for (const libDir of libDirs) {
651
+ const fullDir = (0, path_1.join)(resolved, libDir);
652
+ if (!(0, fs_1.existsSync)(fullDir))
653
+ continue;
654
+ let entries;
655
+ try {
656
+ entries = (0, fs_1.readdirSync)(fullDir);
657
+ }
658
+ catch {
659
+ continue;
660
+ }
661
+ for (const entry of entries) {
662
+ if (isSkipped(entry))
663
+ continue;
664
+ const fullPath = (0, path_1.join)(fullDir, entry);
665
+ try {
666
+ const stat = (0, fs_1.lstatSync)(fullPath);
667
+ if (stat.isSymbolicLink())
668
+ continue;
669
+ if (stat.isDirectory()) {
670
+ // Subdirectory → family ID from dir name
671
+ const familyId = normalizeId(entry);
672
+ const relPath = (0, path_1.relative)(resolved, fullPath).replace(/\\/g, '/');
673
+ if (!result.has(familyId))
674
+ result.set(familyId, []);
675
+ result.get(familyId).push(`${relPath}/*`);
676
+ }
677
+ else if (stat.isFile()) {
678
+ // File → family ID from basename (e.g., channel.ts → channel)
679
+ const ext = entry.slice(entry.lastIndexOf('.'));
680
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
681
+ continue;
682
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
683
+ const familyId = normalizeId(baseName);
684
+ if (familyId.length < 3)
685
+ continue;
686
+ const relPath = (0, path_1.relative)(resolved, fullPath).replace(/\\/g, '/');
687
+ if (!result.has(familyId))
688
+ result.set(familyId, []);
689
+ result.get(familyId).push(relPath);
690
+ }
691
+ }
692
+ catch {
693
+ continue;
694
+ }
695
+ }
696
+ }
697
+ return result;
698
+ }
699
+ /**
700
+ * Discover files in well-known directories (types, utils) whose basename
701
+ * maps directly to a family ID.
702
+ */
703
+ function discoverNameMatchedPaths(appPath, gitRepoRoot) {
704
+ const result = new Map();
705
+ const resolvedApp = (0, path_1.resolve)(appPath);
706
+ const scanRoots = [
707
+ { root: (0, path_1.join)(resolvedApp, 'src/utils'), base: resolvedApp },
708
+ { root: (0, path_1.join)(resolvedApp, 'src/types'), base: resolvedApp },
709
+ ];
710
+ // Monorepo-aware: scan platform types and server model directories
711
+ if (gitRepoRoot) {
712
+ const resolvedGitRoot = (0, path_1.resolve)(gitRepoRoot);
713
+ const platformTypes = (0, path_1.join)(resolvedGitRoot, 'webapp/platform/types/src');
714
+ if ((0, fs_1.existsSync)(platformTypes)) {
715
+ scanRoots.push({ root: platformTypes, base: resolvedGitRoot });
716
+ }
717
+ const platformClient = (0, path_1.join)(resolvedGitRoot, 'webapp/platform/client/src');
718
+ if ((0, fs_1.existsSync)(platformClient)) {
719
+ scanRoots.push({ root: platformClient, base: resolvedGitRoot });
720
+ }
721
+ const serverModel = (0, path_1.join)(resolvedGitRoot, 'server/public/model');
722
+ if ((0, fs_1.existsSync)(serverModel)) {
723
+ scanRoots.push({ root: serverModel, base: resolvedGitRoot });
724
+ }
725
+ }
726
+ for (const { root, base } of scanRoots) {
727
+ if (!(0, fs_1.existsSync)(root))
728
+ continue;
729
+ let entries;
730
+ try {
731
+ entries = (0, fs_1.readdirSync)(root);
732
+ }
733
+ catch {
734
+ continue;
735
+ }
736
+ for (const entry of entries) {
737
+ if (entry.startsWith('.'))
738
+ continue;
739
+ const ext = entry.slice(entry.lastIndexOf('.'));
740
+ if (!['.ts', '.tsx', '.js', '.jsx', '.go'].includes(ext))
741
+ continue;
742
+ // Skip Go test files
743
+ if (entry.endsWith('_test.go'))
744
+ continue;
745
+ const fullPath = (0, path_1.join)(root, entry);
746
+ try {
747
+ const stat = (0, fs_1.lstatSync)(fullPath);
748
+ if (!stat.isFile() || stat.isSymbolicLink())
749
+ continue;
750
+ }
751
+ catch {
752
+ continue;
753
+ }
754
+ // Strip extension and normalize
755
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
756
+ const familyId = normalizeId(baseName);
757
+ if (familyId.length < 3)
758
+ continue;
759
+ const relPath = (0, path_1.relative)(base, fullPath).replace(/\\/g, '/');
760
+ if (!result.has(familyId))
761
+ result.set(familyId, []);
762
+ result.get(familyId).push(relPath);
763
+ }
764
+ }
765
+ return result;
766
+ }
767
+ function scanProject(projectRoot, testsRoot, serverRoot, gitRepoRoot) {
631
768
  const resolved = (0, path_1.resolve)(projectRoot);
632
769
  const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : resolved;
633
770
  const sourceDirs = discoverSourceDirs(resolved);
@@ -690,9 +827,12 @@ function scanProject(projectRoot, testsRoot, serverRoot) {
690
827
  // When a separate serverRoot is provided, discover families from Go source
691
828
  // filenames across the three-tier backend (api4, app, store).
692
829
  if (serverRoot) {
693
- const serverFamilies = discoverServerDerivedFamilies((0, path_1.resolve)(serverRoot));
830
+ const { multiTierFamilies: serverMulti, singleTierFamilies: serverSingle } = discoverServerDerivedFamilies((0, path_1.resolve)(serverRoot));
694
831
  const existingIds = new Set(families.map((f) => f.id));
695
- for (const sf of serverFamilies) {
832
+ // Merge ALL server families (multi + single tier) into existing families,
833
+ // but only add NEW families if they span ≥2 tiers.
834
+ const allServerFamilies = [...serverMulti, ...serverSingle];
835
+ for (const sf of allServerFamilies) {
696
836
  // Try exact match, then singular/plural variants
697
837
  let target = families.find((f) => f.id === sf.id);
698
838
  if (!target && !sf.id.endsWith('s')) {
@@ -709,13 +849,53 @@ function scanProject(projectRoot, testsRoot, serverRoot) {
709
849
  }
710
850
  }
711
851
  }
712
- else {
713
- // New server-only family
852
+ else if (serverMulti.includes(sf)) {
853
+ // Only add new families if they span ≥2 tiers
714
854
  families.push(sf);
715
855
  existingIds.add(sf.id);
716
856
  }
717
857
  }
718
858
  }
859
+ // Merge test library paths (page objects, helpers) into existing families
860
+ if (testsRoot) {
861
+ const testLibPaths = discoverTestLibPaths(resolvedTestsRoot);
862
+ for (const [libFamilyId, patterns] of testLibPaths) {
863
+ let target = families.find((f) => f.id === libFamilyId);
864
+ if (!target && !libFamilyId.endsWith('s')) {
865
+ target = families.find((f) => f.id === libFamilyId + 's');
866
+ }
867
+ if (!target && libFamilyId.endsWith('s')) {
868
+ target = families.find((f) => f.id === libFamilyId.slice(0, -1));
869
+ }
870
+ if (target) {
871
+ for (const p of patterns) {
872
+ if (!target.webappPaths.includes(p)) {
873
+ target.webappPaths.push(p);
874
+ }
875
+ }
876
+ }
877
+ }
878
+ }
879
+ // Merge name-matched type/util files into existing families
880
+ {
881
+ const nameMatchedPaths = discoverNameMatchedPaths(resolved, gitRepoRoot);
882
+ for (const [nmFamilyId, paths] of nameMatchedPaths) {
883
+ let target = families.find((f) => f.id === nmFamilyId);
884
+ if (!target && !nmFamilyId.endsWith('s')) {
885
+ target = families.find((f) => f.id === nmFamilyId + 's');
886
+ }
887
+ if (!target && nmFamilyId.endsWith('s')) {
888
+ target = families.find((f) => f.id === nmFamilyId.slice(0, -1));
889
+ }
890
+ if (target) {
891
+ for (const p of paths) {
892
+ if (!target.webappPaths.includes(p)) {
893
+ target.webappPaths.push(p);
894
+ }
895
+ }
896
+ }
897
+ }
898
+ }
719
899
  const familyIds = new Set(families.map((f) => f.id));
720
900
  const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
721
901
  const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
@@ -47,6 +47,10 @@ export interface EnrichmentResult {
47
47
  tokensUsed: number;
48
48
  costUSD: number;
49
49
  skippedFamilies: string[];
50
+ /** Number of LLM requests made */
51
+ requestCount?: number;
52
+ /** Average response time per LLM request in ms */
53
+ avgResponseMs?: number;
50
54
  }
51
55
  /** A single commit's validation result */
52
56
  export interface CommitValidation {
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/training/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/training/types.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,10 @@
1
1
  import type { RouteFamilyManifest } from '../knowledge/route_families.js';
2
2
  import type { CommitValidation, ValidationReport } from './types.js';
3
+ /**
4
+ * Check if a file path matches any infrastructure glob pattern.
5
+ * Uses simple string matching — no external glob library needed.
6
+ */
7
+ export declare function isInfraFile(filePath: string): boolean;
3
8
  export declare function parseGitLog(log: string): Array<{
4
9
  hash: string;
5
10
  message: string;
@@ -10,7 +15,7 @@ export declare function getCommitFiles(projectRoot: string, since: string): Arra
10
15
  message: string;
11
16
  files: string[];
12
17
  }>;
13
- export declare function validateCommit(manifest: RouteFamilyManifest, files: string[], hash: string, message: string): CommitValidation;
18
+ export declare function validateCommit(manifest: RouteFamilyManifest, files: string[], hash: string, message: string, pathPrefixes?: string[]): CommitValidation;
14
19
  export declare function buildValidationReport(commits: CommitValidation[], manifest: RouteFamilyManifest): ValidationReport;
15
20
  export declare function formatValidationReport(report: ValidationReport): string;
16
21
  //# sourceMappingURL=validator.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/training/validator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAExE,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,YAAY,CAAC;AAEnE,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CA6BhG;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CAgB1H;AAED,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,GAChB,gBAAgB,CA6BlB;AAED,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,QAAQ,EAAE,mBAAmB,GAC9B,gBAAgB,CAkDlB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAgCvE"}
1
+ {"version":3,"file":"validator.d.ts","sourceRoot":"","sources":["../../src/training/validator.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAExE,OAAO,KAAK,EAAC,gBAAgB,EAAE,gBAAgB,EAAC,MAAM,YAAY,CAAC;AAiBnE;;;GAGG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CA6BrD;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CA6BhG;AAED,wBAAgB,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAC,CAAC,CAgB1H;AA+CD,wBAAgB,cAAc,CAC1B,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,YAAY,CAAC,EAAE,MAAM,EAAE,GACxB,gBAAgB,CA+BlB;AAED,wBAAgB,qBAAqB,CACjC,OAAO,EAAE,gBAAgB,EAAE,EAC3B,QAAQ,EAAE,mBAAmB,GAC9B,gBAAgB,CAkDlB;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,gBAAgB,GAAG,MAAM,CAgCvE"}
@@ -2,6 +2,7 @@
2
2
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
3
3
  // See LICENSE.txt for license information.
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.isInfraFile = isInfraFile;
5
6
  exports.parseGitLog = parseGitLog;
6
7
  exports.getCommitFiles = getCommitFiles;
7
8
  exports.validateCommit = validateCommit;
@@ -10,6 +11,63 @@ exports.formatValidationReport = formatValidationReport;
10
11
  const child_process_1 = require("child_process");
11
12
  const path_1 = require("path");
12
13
  const route_families_js_1 = require("../knowledge/route_families.js");
14
+ /**
15
+ * Glob-style patterns for infrastructure / cross-cutting files that will never
16
+ * belong to a single route family. Excluded from coverage calculations.
17
+ */
18
+ const INFRA_GLOBS = [
19
+ 'Makefile', 'go.mod', 'go.sum',
20
+ '*.lock',
21
+ '**/mocks/*', '**/storetest/*', '**/testlib/*',
22
+ '**/i18n/*',
23
+ '**/.github/*', '**/.ci/*', '**/scripts/*',
24
+ '**/docker-compose*',
25
+ '**/__fixtures__/*', '**/test_templates/*',
26
+ 'playwright.config.ts', 'global-setup.ts',
27
+ ];
28
+ /**
29
+ * Check if a file path matches any infrastructure glob pattern.
30
+ * Uses simple string matching — no external glob library needed.
31
+ */
32
+ function isInfraFile(filePath) {
33
+ const normalized = filePath.replace(/\\/g, '/');
34
+ for (const pattern of INFRA_GLOBS) {
35
+ if (pattern.startsWith('**/')) {
36
+ // Match anywhere in the path
37
+ const suffix = pattern.slice(3);
38
+ if (suffix.endsWith('/*')) {
39
+ // Directory match: **/mocks/* → any segment named "mocks" with a child
40
+ const dirName = suffix.slice(0, -2);
41
+ if (normalized.includes(`/${dirName}/`) || normalized.startsWith(`${dirName}/`))
42
+ return true;
43
+ }
44
+ else if (suffix.endsWith('*')) {
45
+ // Prefix match: **/docker-compose* → file starting with docker-compose
46
+ const prefix = suffix.slice(0, -1);
47
+ const base = normalized.split('/').pop() || '';
48
+ if (base.startsWith(prefix))
49
+ return true;
50
+ }
51
+ else {
52
+ if (normalized.endsWith(`/${suffix}`) || normalized === suffix)
53
+ return true;
54
+ }
55
+ }
56
+ else if (pattern.startsWith('*.')) {
57
+ // Extension match: *.lock
58
+ const ext = pattern.slice(1);
59
+ if (normalized.endsWith(ext))
60
+ return true;
61
+ }
62
+ else {
63
+ // Exact basename match: Makefile, go.mod, go.sum
64
+ const base = normalized.split('/').pop() || '';
65
+ if (base === pattern)
66
+ return true;
67
+ }
68
+ }
69
+ return false;
70
+ }
13
71
  function parseGitLog(log) {
14
72
  const commits = [];
15
73
  let current = null;
@@ -55,16 +113,56 @@ function getCommitFiles(projectRoot, since) {
55
113
  }
56
114
  return parseGitLog(log);
57
115
  }
58
- function validateCommit(manifest, files, hash, message) {
59
- // Filter out non-source files
116
+ /**
117
+ * For each file, try matching both the original path and any prefix-stripped
118
+ * variant against the manifest. Returns one FileBinding per original file.
119
+ */
120
+ function bindWithPrefixes(files, manifest, prefixes) {
121
+ if (prefixes.length === 0) {
122
+ return (0, route_families_js_1.bindFilesToFamilies)(files, manifest);
123
+ }
124
+ // Build candidate variants for each file
125
+ const variants = files.map((f) => {
126
+ const normalized = f.replace(/\\/g, '/');
127
+ const candidates = [normalized];
128
+ for (const prefix of prefixes) {
129
+ if (normalized.startsWith(prefix)) {
130
+ candidates.push(normalized.slice(prefix.length));
131
+ break;
132
+ }
133
+ }
134
+ return candidates;
135
+ });
136
+ // Bind all variants and merge results per original file
137
+ return files.map((f, i) => {
138
+ const normalized = f.replace(/\\/g, '/');
139
+ const allBindings = [];
140
+ const seen = new Set();
141
+ for (const variant of variants[i]) {
142
+ const [result] = (0, route_families_js_1.bindFilesToFamilies)([variant], manifest);
143
+ for (const b of result.bindings) {
144
+ const key = `${b.family}:${b.feature || ''}`;
145
+ if (!seen.has(key)) {
146
+ seen.add(key);
147
+ allBindings.push(b);
148
+ }
149
+ }
150
+ }
151
+ return { file: normalized, bindings: allBindings };
152
+ });
153
+ }
154
+ function validateCommit(manifest, files, hash, message, pathPrefixes) {
155
+ // Filter out non-source files and infrastructure files
60
156
  const sourceFiles = files.filter((f) => {
61
157
  return !f.endsWith('.md') && !f.endsWith('.json') && !f.endsWith('.yml') && !f.endsWith('.yaml') &&
62
- !f.startsWith('.') && !f.includes('node_modules/');
158
+ !f.startsWith('.') && !f.includes('node_modules/') && !isInfraFile(f);
63
159
  });
64
160
  if (sourceFiles.length === 0) {
65
161
  return { hash, message, changedFiles: [], boundFiles: 0, unboundFiles: [], familiesHit: [] };
66
162
  }
67
- const bindings = (0, route_families_js_1.bindFilesToFamilies)(sourceFiles, manifest);
163
+ const bindings = pathPrefixes
164
+ ? bindWithPrefixes(sourceFiles, manifest, pathPrefixes)
165
+ : (0, route_families_js_1.bindFilesToFamilies)(sourceFiles, manifest);
68
166
  const bound = bindings.filter((b) => b.bindings.length > 0);
69
167
  const unbound = bindings.filter((b) => b.bindings.length === 0);
70
168
  const familiesHit = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "1.6.0",
3
+ "version": "1.7.1",
4
4
  "description": "AI-powered E2E test impact analysis, generation, and healing. Analyzes code changes to identify affected Playwright tests, detects coverage gaps, and generates or repairs specs using pluggable LLM providers (Claude, OpenAI, Ollama). Includes MCP server, traceability, and CI/CD integration.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",