@yasserkhanorg/e2e-agents 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,10 @@
4
4
  Object.defineProperty(exports, "__esModule", { value: true });
5
5
  exports.discoverSourceDirs = discoverSourceDirs;
6
6
  exports.discoverTestDirs = discoverTestDirs;
7
+ exports.discoverServerDerivedFamilies = discoverServerDerivedFamilies;
8
+ exports.discoverTestDerivedFamilies = discoverTestDerivedFamilies;
9
+ exports.discoverTestLibPaths = discoverTestLibPaths;
10
+ exports.discoverNameMatchedPaths = discoverNameMatchedPaths;
7
11
  exports.scanProject = scanProject;
8
12
  const fs_1 = require("fs");
9
13
  const path_1 = require("path");
@@ -20,6 +24,50 @@ const SKIP_DIRS = new Set([
20
24
  ]);
21
25
  const TEST_EXTENSIONS = ['.spec.ts', '.test.ts', '.spec.js', '.test.js', '.spec.tsx', '.test.tsx'];
22
26
  const GO_TEST_SUFFIX = '_test.go';
27
+ /**
28
+ * Test category directories that organize tests but aren't feature families.
29
+ * Test-only families matching these names are excluded.
30
+ */
31
+ const TEST_CATEGORY_DIRS = new Set([
32
+ 'specs', 'spec', 'accessibility', 'visual', 'smoke', 'regression',
33
+ 'integration', 'functional', 'unit', 'e2e', 'performance', 'load',
34
+ ]);
35
+ /**
36
+ * Structural directories that are code-organization concerns, not feature families.
37
+ * Discovered source dirs matching these names are excluded from family creation.
38
+ */
39
+ const STRUCTURAL_DIRS = new Set([
40
+ 'actions', 'client', 'components', 'hooks', 'i18n', 'packages',
41
+ 'reducers', 'selectors', 'store', 'stores', 'tests', 'types',
42
+ 'utils', 'helpers', 'lib', 'common', 'shared', 'constants',
43
+ 'config', 'styles', 'sass', 'css', 'assets', 'images', 'fonts',
44
+ 'middleware', 'contexts', 'providers', 'layouts', 'templates',
45
+ ]);
46
+ /**
47
+ * Server Go files that are infrastructure / cross-cutting concerns,
48
+ * not feature-specific domains. Matched after stripping _local/_store suffixes.
49
+ */
50
+ const SERVER_INFRA_FILES = new Set([
51
+ 'api', 'apitestlib', 'context', 'helpers', 'params', 'swagger',
52
+ 'app', 'server', 'enterprise', 'product_service', 'security_update_check',
53
+ 'store', 'adapters', 'errors', 'integrity', 'migrate', 'doc',
54
+ 'main', 'init', 'cluster_discovery', 'web_conn', 'web_broadcast_hooks',
55
+ 'manualtesting', 'testlib', 'router', 'handler', 'opentracing',
56
+ 'platform', 'focalboard', 'playbooks', 'client4', 'model',
57
+ 'manifest', 'permission', 'log', 'utils',
58
+ ]);
59
+ /**
60
+ * Server tier directories to scan for Go domain files.
61
+ * Each tier represents a layer of the backend architecture.
62
+ */
63
+ const SERVER_TIERS = [
64
+ 'channels/api4',
65
+ 'channels/app',
66
+ 'channels/store/sqlstore',
67
+ 'channels/web',
68
+ 'channels/wsapi',
69
+ 'public/model',
70
+ ];
23
71
  /** Type-safe includes check for readonly arrays */
24
72
  const includes = (arr, v) => arr.includes(v);
25
73
  function isSkipped(name) {
@@ -319,10 +367,385 @@ function detectFeatures(familyId, group, projectRoot) {
319
367
  }
320
368
  return features;
321
369
  }
322
- function scanProject(projectRoot) {
370
+ /**
371
+ * Discover families by walking the test directory tree at depth ≥ 2.
372
+ *
373
+ * This is the primary family discovery mechanism for projects where source
374
+ * code is organized by code type (components/, actions/) but tests are
375
+ * organized by feature (channels/drafts/, channels/search/).
376
+ *
377
+ * Each leaf test directory (containing spec files) at meaningful depth ≥ 2
378
+ * becomes a candidate family. Top-level feature dirs (depth 1) are already
379
+ * discovered by the standard `discoverTestDirs` + `groupByFamily` pipeline.
380
+ */
381
+ /**
382
+ * Normalize a Go filename into a family domain identifier.
383
+ * Strips _local, _store, trailing 's' (plurals), and normalizes casing.
384
+ */
385
+ function normalizeServerDomain(baseName) {
386
+ let name = baseName;
387
+ // Strip common suffixes
388
+ name = name.replace(/_local$/, '');
389
+ name = name.replace(/_store$/, '');
390
+ // Skip very short names (e.g., single-letter files)
391
+ if (name.length < 3)
392
+ return null;
393
+ return normalizeId(name);
394
+ }
395
+ /**
396
+ * Given a domain name like "channel_bookmark", find its parent domain
397
+ * if a shorter prefix exists in the set (e.g., "channel").
398
+ * This groups related server files under a single family.
399
+ */
400
+ function findParentDomain(name, allDomains) {
401
+ const parts = name.split('_');
402
+ // Try progressively shorter prefixes
403
+ for (let i = parts.length - 1; i >= 1; i--) {
404
+ const candidate = parts.slice(0, i).join('_');
405
+ if (allDomains.has(candidate) && candidate !== name) {
406
+ return candidate;
407
+ }
408
+ }
409
+ return name;
410
+ }
411
+ /**
412
+ * Discover families by scanning server Go source files.
413
+ *
414
+ * The backend follows a three-tier pattern:
415
+ * api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
416
+ *
417
+ * Related files are grouped under parent domains:
418
+ * channel.go, channel_bookmark.go, channel_category.go → "channel" family
419
+ *
420
+ * Each domain becomes a candidate family with precise serverPaths.
421
+ */
422
+ function discoverServerDerivedFamilies(serverRoot) {
423
+ const resolved = (0, path_1.resolve)(serverRoot);
424
+ // First pass: collect all raw domain names across tiers
425
+ const allRawDomains = new Set();
426
+ // domain → tier → Set<file basenames>
427
+ const domainTierFiles = new Map();
428
+ function collectGoFile(entry, tierRelPath) {
429
+ if (!entry.endsWith('.go') || entry.endsWith('_test.go') || entry.startsWith('.'))
430
+ return;
431
+ const baseName = entry.replace('.go', '');
432
+ const domain = normalizeServerDomain(baseName);
433
+ if (!domain || SERVER_INFRA_FILES.has(domain))
434
+ return;
435
+ allRawDomains.add(domain);
436
+ if (!domainTierFiles.has(domain))
437
+ domainTierFiles.set(domain, new Map());
438
+ const tierMap = domainTierFiles.get(domain);
439
+ if (!tierMap.has(tierRelPath))
440
+ tierMap.set(tierRelPath, new Set());
441
+ tierMap.get(tierRelPath).add(baseName);
442
+ }
443
+ for (const tier of SERVER_TIERS) {
444
+ const tierPath = (0, path_1.join)(resolved, tier);
445
+ if (!(0, fs_1.existsSync)(tierPath))
446
+ continue;
447
+ let entries;
448
+ try {
449
+ entries = (0, fs_1.readdirSync)(tierPath);
450
+ }
451
+ catch {
452
+ continue;
453
+ }
454
+ for (const entry of entries) {
455
+ collectGoFile(entry, tier);
456
+ // Also check subdirectories (e.g., app/slashcommands/, app/users/)
457
+ const subPath = (0, path_1.join)(tierPath, entry);
458
+ try {
459
+ const stat = (0, fs_1.lstatSync)(subPath);
460
+ if (stat.isDirectory() && !isSkipped(entry)) {
461
+ const subEntries = (0, fs_1.readdirSync)(subPath);
462
+ for (const subEntry of subEntries) {
463
+ collectGoFile(subEntry, `${tier}/${entry}`);
464
+ }
465
+ }
466
+ }
467
+ catch { /* skip */ }
468
+ }
469
+ }
470
+ // Scan job directories — each subdirectory is a job type
471
+ const jobsPath = (0, path_1.join)(resolved, 'channels/jobs');
472
+ if ((0, fs_1.existsSync)(jobsPath)) {
473
+ try {
474
+ for (const entry of (0, fs_1.readdirSync)(jobsPath)) {
475
+ const jobPath = (0, path_1.join)(jobsPath, entry);
476
+ try {
477
+ if (!(0, fs_1.lstatSync)(jobPath).isDirectory() || isSkipped(entry))
478
+ continue;
479
+ const domain = normalizeId(entry);
480
+ if (SERVER_INFRA_FILES.has(domain))
481
+ continue;
482
+ allRawDomains.add(domain);
483
+ const jobFiles = (0, fs_1.readdirSync)(jobPath);
484
+ for (const jf of jobFiles) {
485
+ if (jf.endsWith('.go') && !jf.endsWith('_test.go')) {
486
+ if (!domainTierFiles.has(domain))
487
+ domainTierFiles.set(domain, new Map());
488
+ const tierMap = domainTierFiles.get(domain);
489
+ const tierKey = `channels/jobs/${entry}`;
490
+ if (!tierMap.has(tierKey))
491
+ tierMap.set(tierKey, new Set());
492
+ tierMap.get(tierKey).add(jf.replace('.go', ''));
493
+ }
494
+ }
495
+ }
496
+ catch { /* skip */ }
497
+ }
498
+ }
499
+ catch { /* skip */ }
500
+ }
501
+ // Second pass: group child domains under parents
502
+ // e.g., channel_bookmark → channel, post_priority → post
503
+ // Track which top-level tiers each family touches for significance filtering.
504
+ const familyPaths = new Map();
505
+ const familyTiers = new Map();
506
+ for (const [domain, tierMap] of domainTierFiles) {
507
+ const parentDomain = findParentDomain(domain, allRawDomains);
508
+ if (!familyPaths.has(parentDomain))
509
+ familyPaths.set(parentDomain, new Set());
510
+ if (!familyTiers.has(parentDomain))
511
+ familyTiers.set(parentDomain, new Set());
512
+ const paths = familyPaths.get(parentDomain);
513
+ const tiers = familyTiers.get(parentDomain);
514
+ for (const [tierRelPath, fileNames] of tierMap) {
515
+ // Track the top-level tier (e.g., "channels/api4" from "channels/api4/slashcommands")
516
+ const topTier = tierRelPath.split('/').slice(0, 2).join('/');
517
+ tiers.add(topTier);
518
+ for (const baseName of fileNames) {
519
+ // Use directory-level glob to capture the file and related variants
520
+ paths.add(`server/${tierRelPath}/${baseName}*.go`);
521
+ }
522
+ }
523
+ }
524
+ // Build families from grouped domains.
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 = [];
529
+ for (const [domain, paths] of familyPaths) {
530
+ if (paths.size === 0)
531
+ continue;
532
+ const tierCount = familyTiers.get(domain)?.size ?? 0;
533
+ const family = {
534
+ id: domain,
535
+ routes: [`/${domain.replace(/_/g, '-')}`],
536
+ webappPaths: [],
537
+ serverPaths: Array.from(paths),
538
+ specDirs: [],
539
+ cypressSpecDirs: [],
540
+ tags: [],
541
+ features: [],
542
+ routesGuessed: true,
543
+ };
544
+ if (tierCount >= 2) {
545
+ multiTierFamilies.push(family);
546
+ }
547
+ else {
548
+ singleTierFamilies.push(family);
549
+ }
550
+ }
551
+ return { multiTierFamilies, singleTierFamilies };
552
+ }
553
+ function discoverTestDerivedFamilies(testsRoot) {
554
+ const resolved = (0, path_1.resolve)(testsRoot);
555
+ const candidates = [];
556
+ function walk(dir, depth) {
557
+ if (depth > 8)
558
+ return;
559
+ let entries;
560
+ try {
561
+ entries = (0, fs_1.readdirSync)(dir);
562
+ }
563
+ catch {
564
+ return;
565
+ }
566
+ const hasSpecs = entries.some((e) => TEST_EXTENSIONS.some((ext) => e.endsWith(ext)) || e.endsWith(GO_TEST_SUFFIX));
567
+ const subdirs = entries.filter((e) => {
568
+ if (isSkipped(e))
569
+ return false;
570
+ try {
571
+ const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir, e));
572
+ return !stat.isSymbolicLink() && stat.isDirectory();
573
+ }
574
+ catch {
575
+ return false;
576
+ }
577
+ });
578
+ const relPath = (0, path_1.relative)(resolved, dir).replace(/\\/g, '/');
579
+ const parts = relPath.split('/').filter(Boolean);
580
+ const meaningful = parts.filter((p) => !TEST_CATEGORY_DIRS.has(normalizeId(p)) && !isSkipped(p));
581
+ // Depth-2+ meaningful dirs with spec files → candidate families
582
+ if (meaningful.length >= 2 && hasSpecs) {
583
+ const leafId = normalizeId(meaningful[meaningful.length - 1]);
584
+ const parentId = normalizeId(meaningful[meaningful.length - 2]);
585
+ if (!STRUCTURAL_DIRS.has(leafId) && !TEST_CATEGORY_DIRS.has(leafId)) {
586
+ candidates.push({ dir, relPath, leafId, parentId });
587
+ }
588
+ }
589
+ for (const sub of subdirs) {
590
+ walk((0, path_1.join)(dir, sub), depth + 1);
591
+ }
592
+ }
593
+ // Walk from standard test roots
594
+ const testRoots = ['tests', 'test', 'e2e-tests', 'e2e', 'specs', 'spec'];
595
+ for (const root of testRoots) {
596
+ const rootPath = (0, path_1.join)(resolved, root);
597
+ if ((0, fs_1.existsSync)(rootPath)) {
598
+ walk(rootPath, 0);
599
+ }
600
+ }
601
+ // Detect leaf-name collisions across parents
602
+ const idCount = new Map();
603
+ for (const c of candidates) {
604
+ idCount.set(c.leafId, (idCount.get(c.leafId) || 0) + 1);
605
+ }
606
+ // Build families — prefix with parent when names collide
607
+ const familyMap = new Map();
608
+ for (const c of candidates) {
609
+ let familyId = c.leafId;
610
+ if ((idCount.get(c.leafId) || 0) > 1 && c.parentId) {
611
+ familyId = `${c.parentId}_${c.leafId}`;
612
+ }
613
+ if (!familyMap.has(familyId)) {
614
+ const specFiles = getSpecFiles(c.dir);
615
+ familyMap.set(familyId, {
616
+ id: familyId,
617
+ routes: [`/${familyId.replace(/_/g, '-')}`],
618
+ webappPaths: [],
619
+ serverPaths: [],
620
+ specDirs: [c.relPath + '/'],
621
+ cypressSpecDirs: [],
622
+ tags: extractTags(specFiles),
623
+ features: [],
624
+ routesGuessed: true,
625
+ });
626
+ }
627
+ else {
628
+ const existing = familyMap.get(familyId);
629
+ const specDir = c.relPath + '/';
630
+ if (!existing.specDirs.includes(specDir)) {
631
+ existing.specDirs.push(specDir);
632
+ existing.tags = [...new Set([...existing.tags, ...extractTags(getSpecFiles(c.dir))])];
633
+ }
634
+ }
635
+ }
636
+ return Array.from(familyMap.values());
637
+ }
638
+ /**
639
+ * Discover test library paths (page objects, helpers) organized by feature.
640
+ * Walks well-known test lib directories and maps subdirectories 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() || !stat.isDirectory())
668
+ continue;
669
+ }
670
+ catch {
671
+ continue;
672
+ }
673
+ const familyId = normalizeId(entry);
674
+ const relPath = (0, path_1.relative)(resolved, fullPath).replace(/\\/g, '/');
675
+ const pattern = `${relPath}/*`;
676
+ if (!result.has(familyId))
677
+ result.set(familyId, []);
678
+ result.get(familyId).push(pattern);
679
+ }
680
+ }
681
+ return result;
682
+ }
683
+ /**
684
+ * Discover files in well-known directories (types, utils) whose basename
685
+ * maps directly to a family ID.
686
+ */
687
+ function discoverNameMatchedPaths(appPath, gitRepoRoot) {
688
+ const result = new Map();
689
+ const resolvedApp = (0, path_1.resolve)(appPath);
690
+ const scanRoots = [
691
+ { root: (0, path_1.join)(resolvedApp, 'src/utils'), base: resolvedApp },
692
+ { root: (0, path_1.join)(resolvedApp, 'src/types'), base: resolvedApp },
693
+ ];
694
+ // Monorepo-aware: scan platform types directory
695
+ if (gitRepoRoot) {
696
+ const resolvedGitRoot = (0, path_1.resolve)(gitRepoRoot);
697
+ const platformTypes = (0, path_1.join)(resolvedGitRoot, 'webapp/platform/types/src');
698
+ if ((0, fs_1.existsSync)(platformTypes)) {
699
+ scanRoots.push({ root: platformTypes, base: resolvedGitRoot });
700
+ }
701
+ const platformClient = (0, path_1.join)(resolvedGitRoot, 'webapp/platform/client/src');
702
+ if ((0, fs_1.existsSync)(platformClient)) {
703
+ scanRoots.push({ root: platformClient, base: resolvedGitRoot });
704
+ }
705
+ }
706
+ for (const { root, base } of scanRoots) {
707
+ if (!(0, fs_1.existsSync)(root))
708
+ continue;
709
+ let entries;
710
+ try {
711
+ entries = (0, fs_1.readdirSync)(root);
712
+ }
713
+ catch {
714
+ continue;
715
+ }
716
+ for (const entry of entries) {
717
+ if (entry.startsWith('.'))
718
+ continue;
719
+ const ext = entry.slice(entry.lastIndexOf('.'));
720
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
721
+ continue;
722
+ const fullPath = (0, path_1.join)(root, entry);
723
+ try {
724
+ const stat = (0, fs_1.lstatSync)(fullPath);
725
+ if (!stat.isFile() || stat.isSymbolicLink())
726
+ continue;
727
+ }
728
+ catch {
729
+ continue;
730
+ }
731
+ // Strip extension and normalize
732
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
733
+ const familyId = normalizeId(baseName);
734
+ if (familyId.length < 3)
735
+ continue;
736
+ const relPath = (0, path_1.relative)(base, fullPath).replace(/\\/g, '/');
737
+ if (!result.has(familyId))
738
+ result.set(familyId, []);
739
+ result.get(familyId).push(relPath);
740
+ }
741
+ }
742
+ return result;
743
+ }
744
+ function scanProject(projectRoot, testsRoot, serverRoot, gitRepoRoot) {
323
745
  const resolved = (0, path_1.resolve)(projectRoot);
746
+ const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : resolved;
324
747
  const sourceDirs = discoverSourceDirs(resolved);
325
- const testDirs = discoverTestDirs(resolved);
748
+ const testDirs = discoverTestDirs(resolvedTestsRoot);
326
749
  const allDirs = [...sourceDirs, ...testDirs];
327
750
  const groups = groupByFamily(allDirs);
328
751
  const families = [];
@@ -331,6 +754,13 @@ function scanProject(projectRoot) {
331
754
  const hasTests = group.test.length > 0 || group.cypress.length > 0;
332
755
  if (!hasSrc && !hasTests)
333
756
  continue;
757
+ // Skip structural directories that are code-organization, not features.
758
+ // Only skip if they have source dirs but no corresponding test dirs.
759
+ if (STRUCTURAL_DIRS.has(familyId) && !hasTests)
760
+ continue;
761
+ // Skip test-only families that match broad test categories (not feature families).
762
+ if (!hasSrc && hasTests && TEST_CATEGORY_DIRS.has(familyId))
763
+ continue;
334
764
  const allSpecFiles = [];
335
765
  for (const td of [...group.test, ...group.cypress]) {
336
766
  allSpecFiles.push(...getSpecFiles(td.path));
@@ -348,6 +778,101 @@ function scanProject(projectRoot) {
348
778
  routesGuessed: true,
349
779
  });
350
780
  }
781
+ // When a separate testsRoot is provided, discover families from test
782
+ // directory structure. Projects with feature-organized tests but
783
+ // code-type-organized source benefit from this.
784
+ if (testsRoot) {
785
+ const testFamilies = discoverTestDerivedFamilies(resolvedTestsRoot);
786
+ const existingIds = new Set(families.map((f) => f.id));
787
+ for (const tf of testFamilies) {
788
+ if (existingIds.has(tf.id)) {
789
+ // Merge specDirs into existing family
790
+ const existing = families.find((f) => f.id === tf.id);
791
+ for (const sd of tf.specDirs) {
792
+ if (!existing.specDirs.includes(sd)) {
793
+ existing.specDirs.push(sd);
794
+ }
795
+ }
796
+ existing.tags = [...new Set([...existing.tags, ...tf.tags])];
797
+ }
798
+ else {
799
+ families.push(tf);
800
+ existingIds.add(tf.id);
801
+ }
802
+ }
803
+ }
804
+ // When a separate serverRoot is provided, discover families from Go source
805
+ // filenames across the three-tier backend (api4, app, store).
806
+ if (serverRoot) {
807
+ const { multiTierFamilies: serverMulti, singleTierFamilies: serverSingle } = discoverServerDerivedFamilies((0, path_1.resolve)(serverRoot));
808
+ const existingIds = new Set(families.map((f) => f.id));
809
+ // Merge ALL server families (multi + single tier) into existing families,
810
+ // but only add NEW families if they span ≥2 tiers.
811
+ const allServerFamilies = [...serverMulti, ...serverSingle];
812
+ for (const sf of allServerFamilies) {
813
+ // Try exact match, then singular/plural variants
814
+ let target = families.find((f) => f.id === sf.id);
815
+ if (!target && !sf.id.endsWith('s')) {
816
+ target = families.find((f) => f.id === sf.id + 's');
817
+ }
818
+ if (!target && sf.id.endsWith('s')) {
819
+ target = families.find((f) => f.id === sf.id.slice(0, -1));
820
+ }
821
+ if (target) {
822
+ // Merge serverPaths into existing family
823
+ for (const sp of sf.serverPaths) {
824
+ if (!target.serverPaths.includes(sp)) {
825
+ target.serverPaths.push(sp);
826
+ }
827
+ }
828
+ }
829
+ else if (serverMulti.includes(sf)) {
830
+ // Only add new families if they span ≥2 tiers
831
+ families.push(sf);
832
+ existingIds.add(sf.id);
833
+ }
834
+ }
835
+ }
836
+ // Merge test library paths (page objects, helpers) into existing families
837
+ if (testsRoot) {
838
+ const testLibPaths = discoverTestLibPaths(resolvedTestsRoot);
839
+ for (const [libFamilyId, patterns] of testLibPaths) {
840
+ let target = families.find((f) => f.id === libFamilyId);
841
+ if (!target && !libFamilyId.endsWith('s')) {
842
+ target = families.find((f) => f.id === libFamilyId + 's');
843
+ }
844
+ if (!target && libFamilyId.endsWith('s')) {
845
+ target = families.find((f) => f.id === libFamilyId.slice(0, -1));
846
+ }
847
+ if (target) {
848
+ for (const p of patterns) {
849
+ if (!target.webappPaths.includes(p)) {
850
+ target.webappPaths.push(p);
851
+ }
852
+ }
853
+ }
854
+ }
855
+ }
856
+ // Merge name-matched type/util files into existing families
857
+ {
858
+ const nameMatchedPaths = discoverNameMatchedPaths(resolved, gitRepoRoot);
859
+ for (const [nmFamilyId, paths] of nameMatchedPaths) {
860
+ let target = families.find((f) => f.id === nmFamilyId);
861
+ if (!target && !nmFamilyId.endsWith('s')) {
862
+ target = families.find((f) => f.id === nmFamilyId + 's');
863
+ }
864
+ if (!target && nmFamilyId.endsWith('s')) {
865
+ target = families.find((f) => f.id === nmFamilyId.slice(0, -1));
866
+ }
867
+ if (target) {
868
+ for (const p of paths) {
869
+ if (!target.webappPaths.includes(p)) {
870
+ target.webappPaths.push(p);
871
+ }
872
+ }
873
+ }
874
+ }
875
+ }
351
876
  const familyIds = new Set(families.map((f) => f.id));
352
877
  const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
353
878
  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 {
@@ -87,6 +91,10 @@ export interface TrainOptions {
87
91
  appPath: string;
88
92
  /** Path to tests root (may differ from appPath) */
89
93
  testsRoot: string;
94
+ /** Path to server/backend root (may differ from appPath) */
95
+ serverRoot?: string;
96
+ /** Git repo root for monorepo-aware validation */
97
+ gitRepoRoot?: string;
90
98
  /** Enable LLM enrichment (default: true) */
91
99
  enrich: boolean;
92
100
  /** Run validation against git history */
@@ -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;
@@ -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;AAgBnE;;;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;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"}