@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.
@@ -3,6 +3,7 @@
3
3
  import { existsSync, mkdirSync, writeFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { getChangedFiles } from '../agent/git.js';
6
+ import { logger } from '../logger.js';
6
7
  import { preprocess } from './stage0_preprocess.js';
7
8
  import { runImpactStage } from './stage1_impact.js';
8
9
  import { runCoverageStage } from './stage2_coverage.js';
@@ -58,20 +59,25 @@ export async function runPipeline(config) {
58
59
  const reportPath = writeReport(config.testsRoot, emptyReport);
59
60
  return { report: emptyReport, reportPath, warnings: allWarnings };
60
61
  }
62
+ const timings = {};
61
63
  // Step 2: Preprocess — deterministic file classification + route family binding
64
+ const preprocessTimer = logger.timer('preprocess');
62
65
  const preprocessResult = preprocess(changedFiles, {
63
66
  appPath: config.appPath,
64
67
  testsRoot: config.testsRoot,
65
68
  routeFamilies: config.routeFamilies,
66
69
  apiSurface: config.apiSurface,
67
70
  });
71
+ timings.preprocess = preprocessTimer.end();
68
72
  allWarnings.push(...preprocessResult.warnings);
69
73
  let decisions = [];
70
74
  // Step 3: Impact stage — AI-powered flow identification per family
71
75
  if (stages.includes('impact')) {
76
+ const impactTimer = logger.timer('impact');
72
77
  const impactResult = await runImpactStage(preprocessResult.familyGroups, preprocessResult.manifest, preprocessResult.specIndex, preprocessResult.apiSurface, preprocessResult.context, config.impact || {});
73
78
  decisions = impactResult.decisions;
74
79
  allWarnings.push(...impactResult.warnings);
80
+ timings.impact = impactTimer.end();
75
81
  // Check cannot_determine ratio
76
82
  const cannotDetermineRatio = computeCannotDetermineRatio(decisions);
77
83
  if (cannotDetermineRatio > 0.3) {
@@ -80,18 +86,23 @@ export async function runPipeline(config) {
80
86
  }
81
87
  // Step 4: Coverage stage — AI-powered spec coverage evaluation
82
88
  if (stages.includes('coverage') && decisions.length > 0) {
89
+ const coverageTimer = logger.timer('coverage');
83
90
  const coverageResult = await runCoverageStage(decisions, preprocessResult.specIndex, preprocessResult.context, config.testsRoot, config.coverage || {});
84
91
  decisions = coverageResult.decisions;
92
+ timings.coverage = coverageTimer.end();
85
93
  allWarnings.push(...coverageResult.warnings);
86
94
  }
87
95
  // Step 5: Generation stage — AI-powered spec generation for create_spec / add_scenarios
88
96
  if (stages.includes('generation') && decisions.length > 0) {
97
+ const generationTimer = logger.timer('generation');
89
98
  const generationResult = await runGenerationStage(decisions, preprocessResult.apiSurface, config.testsRoot, config.generation || {});
90
99
  generatedSpecs = generationResult.generated;
100
+ timings.generation = generationTimer.end();
91
101
  allWarnings.push(...generationResult.warnings);
92
102
  }
93
103
  // Step 6: Heal stage — MCP-backed playwright-test-healer for failing/flaky specs
94
104
  if (stages.includes('heal')) {
105
+ const healTimer = logger.timer('heal');
95
106
  const healTargets = resolveHealTargets(config.testsRoot, {
96
107
  playwrightReportPath: config.playwrightReportPath,
97
108
  generatedSpecs,
@@ -103,6 +114,7 @@ export async function runPipeline(config) {
103
114
  else {
104
115
  allWarnings.push('Heal stage: no targets found (no failing specs in report, no generated specs).');
105
116
  }
117
+ timings.heal = healTimer.end();
106
118
  }
107
119
  // Build report
108
120
  const report = {
@@ -118,16 +130,18 @@ export async function runPipeline(config) {
118
130
  generationAgent: stages.includes('generation') ? (config.generation?.provider || 'auto') : undefined,
119
131
  },
120
132
  };
121
- const reportPath = writeReport(config.testsRoot, report, healResult);
133
+ const reportPath = writeReport(config.testsRoot, report, healResult, timings);
122
134
  return { report, reportPath, warnings: allWarnings, generated: generatedSpecs, healResult };
123
135
  }
124
- function writeReport(testsRoot, report, healResult) {
136
+ function writeReport(testsRoot, report, healResult, timings) {
125
137
  const outputDir = join(testsRoot, '.e2e-ai-agents');
126
138
  if (!existsSync(outputDir)) {
127
139
  mkdirSync(outputDir, { recursive: true });
128
140
  }
141
+ // Include timings in the JSON report if available
142
+ const reportWithTimings = timings ? { ...report, timings } : report;
129
143
  const jsonPath = join(outputDir, 'pipeline-report.json');
130
- writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
144
+ writeFileSync(jsonPath, JSON.stringify(reportWithTimings, null, 2), 'utf-8');
131
145
  const mdPath = join(outputDir, 'pipeline-report.md');
132
146
  writeFileSync(mdPath, renderMarkdown(report, healResult), 'utf-8');
133
147
  return jsonPath;
@@ -263,6 +263,8 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
263
263
  const enriched = [];
264
264
  let totalTokens = 0;
265
265
  let totalCost = 0;
266
+ let requestCount = 0;
267
+ let totalResponseMs = 0;
266
268
  const skipped = [];
267
269
  // Process in chunks of 4 families
268
270
  const chunkSize = 4;
@@ -295,15 +297,18 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
295
297
  prompt = prompt.slice(0, MAX_PROMPT_CHARS);
296
298
  }
297
299
  }
298
- let timer;
300
+ let timeoutTimer;
299
301
  try {
300
302
  const timeoutPromise = new Promise((_, reject) => {
301
- timer = setTimeout(() => reject(new Error('LLM request timed out')), LLM_TIMEOUT_MS);
303
+ timeoutTimer = setTimeout(() => reject(new Error('LLM request timed out')), LLM_TIMEOUT_MS);
302
304
  });
305
+ const reqStart = performance.now();
303
306
  const response = await Promise.race([
304
307
  provider.generateText(prompt, { maxTokens: 4096, temperature: 0.3 }),
305
308
  timeoutPromise,
306
309
  ]);
310
+ totalResponseMs += performance.now() - reqStart;
311
+ requestCount++;
307
312
  totalTokens += (response.usage?.inputTokens ?? 0) + (response.usage?.outputTokens ?? 0);
308
313
  totalCost += response.cost ?? 0;
309
314
  const entries = parseEnrichResponse(response.text);
@@ -324,8 +329,8 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
324
329
  enriched.push(...chunk);
325
330
  }
326
331
  finally {
327
- if (timer)
328
- clearTimeout(timer);
332
+ if (timeoutTimer)
333
+ clearTimeout(timeoutTimer);
329
334
  }
330
335
  }
331
336
  return {
@@ -333,5 +338,7 @@ export async function enrichFamilies(families, scanned, projectRoot, provider, b
333
338
  tokensUsed: totalTokens,
334
339
  costUSD: Math.round(totalCost * 100) / 100,
335
340
  skippedFamilies: skipped,
341
+ requestCount,
342
+ avgResponseMs: requestCount > 0 ? Math.round(totalResponseMs / requestCount) : 0,
336
343
  };
337
344
  }
@@ -513,15 +513,15 @@ export function discoverServerDerivedFamilies(serverRoot) {
513
513
  }
514
514
  }
515
515
  // Build families from grouped domains.
516
- // Only include server-only families that span 2 tiers (architecturally significant).
517
- const families = [];
516
+ // Multi-tier families (≥2 tiers) can be new families.
517
+ // Single-tier families can only merge into existing families.
518
+ const multiTierFamilies = [];
519
+ const singleTierFamilies = [];
518
520
  for (const [domain, paths] of familyPaths) {
519
521
  if (paths.size === 0)
520
522
  continue;
521
523
  const tierCount = familyTiers.get(domain)?.size ?? 0;
522
- if (tierCount < 2)
523
- continue; // Skip single-tier domains (likely infrastructure)
524
- families.push({
524
+ const family = {
525
525
  id: domain,
526
526
  routes: [`/${domain.replace(/_/g, '-')}`],
527
527
  webappPaths: [],
@@ -531,9 +531,15 @@ export function discoverServerDerivedFamilies(serverRoot) {
531
531
  tags: [],
532
532
  features: [],
533
533
  routesGuessed: true,
534
- });
534
+ };
535
+ if (tierCount >= 2) {
536
+ multiTierFamilies.push(family);
537
+ }
538
+ else {
539
+ singleTierFamilies.push(family);
540
+ }
535
541
  }
536
- return families;
542
+ return { multiTierFamilies, singleTierFamilies };
537
543
  }
538
544
  export function discoverTestDerivedFamilies(testsRoot) {
539
545
  const resolved = resolve(testsRoot);
@@ -620,7 +626,136 @@ export function discoverTestDerivedFamilies(testsRoot) {
620
626
  }
621
627
  return Array.from(familyMap.values());
622
628
  }
623
- export function scanProject(projectRoot, testsRoot, serverRoot) {
629
+ /**
630
+ * Discover test library paths (page objects, helpers) organized by feature.
631
+ * Walks well-known test lib directories and maps subdirectories and files to family IDs.
632
+ */
633
+ export function discoverTestLibPaths(testsRoot) {
634
+ const resolved = resolve(testsRoot);
635
+ const result = new Map();
636
+ const libDirs = [
637
+ 'lib/src/ui/components',
638
+ 'lib/src/ui/pages',
639
+ 'lib/src/server',
640
+ ];
641
+ for (const libDir of libDirs) {
642
+ const fullDir = join(resolved, libDir);
643
+ if (!existsSync(fullDir))
644
+ continue;
645
+ let entries;
646
+ try {
647
+ entries = readdirSync(fullDir);
648
+ }
649
+ catch {
650
+ continue;
651
+ }
652
+ for (const entry of entries) {
653
+ if (isSkipped(entry))
654
+ continue;
655
+ const fullPath = join(fullDir, entry);
656
+ try {
657
+ const stat = lstatSync(fullPath);
658
+ if (stat.isSymbolicLink())
659
+ continue;
660
+ if (stat.isDirectory()) {
661
+ // Subdirectory → family ID from dir name
662
+ const familyId = normalizeId(entry);
663
+ const relPath = relative(resolved, fullPath).replace(/\\/g, '/');
664
+ if (!result.has(familyId))
665
+ result.set(familyId, []);
666
+ result.get(familyId).push(`${relPath}/*`);
667
+ }
668
+ else if (stat.isFile()) {
669
+ // File → family ID from basename (e.g., channel.ts → channel)
670
+ const ext = entry.slice(entry.lastIndexOf('.'));
671
+ if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
672
+ continue;
673
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
674
+ const familyId = normalizeId(baseName);
675
+ if (familyId.length < 3)
676
+ continue;
677
+ const relPath = relative(resolved, fullPath).replace(/\\/g, '/');
678
+ if (!result.has(familyId))
679
+ result.set(familyId, []);
680
+ result.get(familyId).push(relPath);
681
+ }
682
+ }
683
+ catch {
684
+ continue;
685
+ }
686
+ }
687
+ }
688
+ return result;
689
+ }
690
+ /**
691
+ * Discover files in well-known directories (types, utils) whose basename
692
+ * maps directly to a family ID.
693
+ */
694
+ export function discoverNameMatchedPaths(appPath, gitRepoRoot) {
695
+ const result = new Map();
696
+ const resolvedApp = resolve(appPath);
697
+ const scanRoots = [
698
+ { root: join(resolvedApp, 'src/utils'), base: resolvedApp },
699
+ { root: join(resolvedApp, 'src/types'), base: resolvedApp },
700
+ ];
701
+ // Monorepo-aware: scan platform types and server model directories
702
+ if (gitRepoRoot) {
703
+ const resolvedGitRoot = resolve(gitRepoRoot);
704
+ const platformTypes = join(resolvedGitRoot, 'webapp/platform/types/src');
705
+ if (existsSync(platformTypes)) {
706
+ scanRoots.push({ root: platformTypes, base: resolvedGitRoot });
707
+ }
708
+ const platformClient = join(resolvedGitRoot, 'webapp/platform/client/src');
709
+ if (existsSync(platformClient)) {
710
+ scanRoots.push({ root: platformClient, base: resolvedGitRoot });
711
+ }
712
+ const serverModel = join(resolvedGitRoot, 'server/public/model');
713
+ if (existsSync(serverModel)) {
714
+ scanRoots.push({ root: serverModel, base: resolvedGitRoot });
715
+ }
716
+ }
717
+ for (const { root, base } of scanRoots) {
718
+ if (!existsSync(root))
719
+ continue;
720
+ let entries;
721
+ try {
722
+ entries = readdirSync(root);
723
+ }
724
+ catch {
725
+ continue;
726
+ }
727
+ for (const entry of entries) {
728
+ if (entry.startsWith('.'))
729
+ continue;
730
+ const ext = entry.slice(entry.lastIndexOf('.'));
731
+ if (!['.ts', '.tsx', '.js', '.jsx', '.go'].includes(ext))
732
+ continue;
733
+ // Skip Go test files
734
+ if (entry.endsWith('_test.go'))
735
+ continue;
736
+ const fullPath = join(root, entry);
737
+ try {
738
+ const stat = lstatSync(fullPath);
739
+ if (!stat.isFile() || stat.isSymbolicLink())
740
+ continue;
741
+ }
742
+ catch {
743
+ continue;
744
+ }
745
+ // Strip extension and normalize
746
+ const baseName = entry.slice(0, entry.lastIndexOf('.'));
747
+ const familyId = normalizeId(baseName);
748
+ if (familyId.length < 3)
749
+ continue;
750
+ const relPath = relative(base, fullPath).replace(/\\/g, '/');
751
+ if (!result.has(familyId))
752
+ result.set(familyId, []);
753
+ result.get(familyId).push(relPath);
754
+ }
755
+ }
756
+ return result;
757
+ }
758
+ export function scanProject(projectRoot, testsRoot, serverRoot, gitRepoRoot) {
624
759
  const resolved = resolve(projectRoot);
625
760
  const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolved;
626
761
  const sourceDirs = discoverSourceDirs(resolved);
@@ -683,9 +818,12 @@ export function scanProject(projectRoot, testsRoot, serverRoot) {
683
818
  // When a separate serverRoot is provided, discover families from Go source
684
819
  // filenames across the three-tier backend (api4, app, store).
685
820
  if (serverRoot) {
686
- const serverFamilies = discoverServerDerivedFamilies(resolve(serverRoot));
821
+ const { multiTierFamilies: serverMulti, singleTierFamilies: serverSingle } = discoverServerDerivedFamilies(resolve(serverRoot));
687
822
  const existingIds = new Set(families.map((f) => f.id));
688
- for (const sf of serverFamilies) {
823
+ // Merge ALL server families (multi + single tier) into existing families,
824
+ // but only add NEW families if they span ≥2 tiers.
825
+ const allServerFamilies = [...serverMulti, ...serverSingle];
826
+ for (const sf of allServerFamilies) {
689
827
  // Try exact match, then singular/plural variants
690
828
  let target = families.find((f) => f.id === sf.id);
691
829
  if (!target && !sf.id.endsWith('s')) {
@@ -702,13 +840,53 @@ export function scanProject(projectRoot, testsRoot, serverRoot) {
702
840
  }
703
841
  }
704
842
  }
705
- else {
706
- // New server-only family
843
+ else if (serverMulti.includes(sf)) {
844
+ // Only add new families if they span ≥2 tiers
707
845
  families.push(sf);
708
846
  existingIds.add(sf.id);
709
847
  }
710
848
  }
711
849
  }
850
+ // Merge test library paths (page objects, helpers) into existing families
851
+ if (testsRoot) {
852
+ const testLibPaths = discoverTestLibPaths(resolvedTestsRoot);
853
+ for (const [libFamilyId, patterns] of testLibPaths) {
854
+ let target = families.find((f) => f.id === libFamilyId);
855
+ if (!target && !libFamilyId.endsWith('s')) {
856
+ target = families.find((f) => f.id === libFamilyId + 's');
857
+ }
858
+ if (!target && libFamilyId.endsWith('s')) {
859
+ target = families.find((f) => f.id === libFamilyId.slice(0, -1));
860
+ }
861
+ if (target) {
862
+ for (const p of patterns) {
863
+ if (!target.webappPaths.includes(p)) {
864
+ target.webappPaths.push(p);
865
+ }
866
+ }
867
+ }
868
+ }
869
+ }
870
+ // Merge name-matched type/util files into existing families
871
+ {
872
+ const nameMatchedPaths = discoverNameMatchedPaths(resolved, gitRepoRoot);
873
+ for (const [nmFamilyId, paths] of nameMatchedPaths) {
874
+ let target = families.find((f) => f.id === nmFamilyId);
875
+ if (!target && !nmFamilyId.endsWith('s')) {
876
+ target = families.find((f) => f.id === nmFamilyId + 's');
877
+ }
878
+ if (!target && nmFamilyId.endsWith('s')) {
879
+ target = families.find((f) => f.id === nmFamilyId.slice(0, -1));
880
+ }
881
+ if (target) {
882
+ for (const p of paths) {
883
+ if (!target.webappPaths.includes(p)) {
884
+ target.webappPaths.push(p);
885
+ }
886
+ }
887
+ }
888
+ }
889
+ }
712
890
  const familyIds = new Set(families.map((f) => f.id));
713
891
  const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
714
892
  const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
@@ -3,6 +3,63 @@
3
3
  import { execFileSync } from 'child_process';
4
4
  import { resolve } from 'path';
5
5
  import { bindFilesToFamilies } from '../knowledge/route_families.js';
6
+ /**
7
+ * Glob-style patterns for infrastructure / cross-cutting files that will never
8
+ * belong to a single route family. Excluded from coverage calculations.
9
+ */
10
+ const INFRA_GLOBS = [
11
+ 'Makefile', 'go.mod', 'go.sum',
12
+ '*.lock',
13
+ '**/mocks/*', '**/storetest/*', '**/testlib/*',
14
+ '**/i18n/*',
15
+ '**/.github/*', '**/.ci/*', '**/scripts/*',
16
+ '**/docker-compose*',
17
+ '**/__fixtures__/*', '**/test_templates/*',
18
+ 'playwright.config.ts', 'global-setup.ts',
19
+ ];
20
+ /**
21
+ * Check if a file path matches any infrastructure glob pattern.
22
+ * Uses simple string matching — no external glob library needed.
23
+ */
24
+ export function isInfraFile(filePath) {
25
+ const normalized = filePath.replace(/\\/g, '/');
26
+ for (const pattern of INFRA_GLOBS) {
27
+ if (pattern.startsWith('**/')) {
28
+ // Match anywhere in the path
29
+ const suffix = pattern.slice(3);
30
+ if (suffix.endsWith('/*')) {
31
+ // Directory match: **/mocks/* → any segment named "mocks" with a child
32
+ const dirName = suffix.slice(0, -2);
33
+ if (normalized.includes(`/${dirName}/`) || normalized.startsWith(`${dirName}/`))
34
+ return true;
35
+ }
36
+ else if (suffix.endsWith('*')) {
37
+ // Prefix match: **/docker-compose* → file starting with docker-compose
38
+ const prefix = suffix.slice(0, -1);
39
+ const base = normalized.split('/').pop() || '';
40
+ if (base.startsWith(prefix))
41
+ return true;
42
+ }
43
+ else {
44
+ if (normalized.endsWith(`/${suffix}`) || normalized === suffix)
45
+ return true;
46
+ }
47
+ }
48
+ else if (pattern.startsWith('*.')) {
49
+ // Extension match: *.lock
50
+ const ext = pattern.slice(1);
51
+ if (normalized.endsWith(ext))
52
+ return true;
53
+ }
54
+ else {
55
+ // Exact basename match: Makefile, go.mod, go.sum
56
+ const base = normalized.split('/').pop() || '';
57
+ if (base === pattern)
58
+ return true;
59
+ }
60
+ }
61
+ return false;
62
+ }
6
63
  export function parseGitLog(log) {
7
64
  const commits = [];
8
65
  let current = null;
@@ -48,16 +105,56 @@ export function getCommitFiles(projectRoot, since) {
48
105
  }
49
106
  return parseGitLog(log);
50
107
  }
51
- export function validateCommit(manifest, files, hash, message) {
52
- // Filter out non-source files
108
+ /**
109
+ * For each file, try matching both the original path and any prefix-stripped
110
+ * variant against the manifest. Returns one FileBinding per original file.
111
+ */
112
+ function bindWithPrefixes(files, manifest, prefixes) {
113
+ if (prefixes.length === 0) {
114
+ return bindFilesToFamilies(files, manifest);
115
+ }
116
+ // Build candidate variants for each file
117
+ const variants = files.map((f) => {
118
+ const normalized = f.replace(/\\/g, '/');
119
+ const candidates = [normalized];
120
+ for (const prefix of prefixes) {
121
+ if (normalized.startsWith(prefix)) {
122
+ candidates.push(normalized.slice(prefix.length));
123
+ break;
124
+ }
125
+ }
126
+ return candidates;
127
+ });
128
+ // Bind all variants and merge results per original file
129
+ return files.map((f, i) => {
130
+ const normalized = f.replace(/\\/g, '/');
131
+ const allBindings = [];
132
+ const seen = new Set();
133
+ for (const variant of variants[i]) {
134
+ const [result] = bindFilesToFamilies([variant], manifest);
135
+ for (const b of result.bindings) {
136
+ const key = `${b.family}:${b.feature || ''}`;
137
+ if (!seen.has(key)) {
138
+ seen.add(key);
139
+ allBindings.push(b);
140
+ }
141
+ }
142
+ }
143
+ return { file: normalized, bindings: allBindings };
144
+ });
145
+ }
146
+ export function validateCommit(manifest, files, hash, message, pathPrefixes) {
147
+ // Filter out non-source files and infrastructure files
53
148
  const sourceFiles = files.filter((f) => {
54
149
  return !f.endsWith('.md') && !f.endsWith('.json') && !f.endsWith('.yml') && !f.endsWith('.yaml') &&
55
- !f.startsWith('.') && !f.includes('node_modules/');
150
+ !f.startsWith('.') && !f.includes('node_modules/') && !isInfraFile(f);
56
151
  });
57
152
  if (sourceFiles.length === 0) {
58
153
  return { hash, message, changedFiles: [], boundFiles: 0, unboundFiles: [], familiesHit: [] };
59
154
  }
60
- const bindings = bindFilesToFamilies(sourceFiles, manifest);
155
+ const bindings = pathPrefixes
156
+ ? bindWithPrefixes(sourceFiles, manifest, pathPrefixes)
157
+ : bindFilesToFamilies(sourceFiles, manifest);
61
158
  const bound = bindings.filter((b) => b.bindings.length > 0);
62
159
  const unbound = bindings.filter((b) => b.bindings.length === 0);
63
160
  const familiesHit = new Set();
@@ -1 +1 @@
1
- {"version":3,"file":"route_families.d.ts","sourceRoot":"","sources":["../../src/knowledge/route_families.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"route_families.d.ts","sourceRoot":"","sources":["../../src/knowledge/route_families.ts"],"names":[],"mappings":"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"}
@@ -185,6 +185,7 @@ function bindFilesToFamilies(changedFiles, manifest) {
185
185
  const featurePatterns = [
186
186
  ...(feature.webappPaths || []),
187
187
  ...(feature.serverPaths || []),
188
+ ...(feature.specDirs || []),
188
189
  ];
189
190
  if (featurePatterns.length > 0 && matchesAnyPattern(normalized, featurePatterns)) {
190
191
  featureBindings.push({ family: family.id, feature: feature.id });
@@ -199,6 +200,8 @@ function bindFilesToFamilies(changedFiles, manifest) {
199
200
  const familyPatterns = [
200
201
  ...(family.webappPaths || []),
201
202
  ...(family.serverPaths || []),
203
+ ...(family.specDirs || []),
204
+ ...(family.cypressSpecDirs || []),
202
205
  ];
203
206
  if (familyPatterns.length > 0 && matchesAnyPattern(normalized, familyPatterns)) {
204
207
  bindings.push({ family: family.id });
package/dist/logger.d.ts CHANGED
@@ -11,12 +11,21 @@ export declare enum LogLevel {
11
11
  }
12
12
  export declare class Logger {
13
13
  private level;
14
+ private jsonMode;
14
15
  constructor(minLevel?: LogLevel);
15
16
  error(message: string, context?: Record<string, unknown>): void;
16
17
  warn(message: string, context?: Record<string, unknown>): void;
17
18
  info(message: string, context?: Record<string, unknown>): void;
18
19
  debug(message: string, context?: Record<string, unknown>): void;
19
20
  setLevel(level: LogLevel): void;
21
+ setJsonMode(enabled: boolean): void;
22
+ /**
23
+ * Start a timer for measuring duration of an operation.
24
+ * Returns an object with `end()` that logs at DEBUG level and returns elapsed ms.
25
+ */
26
+ timer(label: string): {
27
+ end: () => number;
28
+ };
20
29
  private log;
21
30
  }
22
31
  export declare const logger: Logger;
@@ -1 +1 @@
1
- {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,oBAAY,QAAQ;IAChB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACZ;AAqCD,qBAAa,MAAM;IACf,OAAO,CAAC,KAAK,CAAW;gBAEZ,QAAQ,CAAC,EAAE,QAAQ;IAI/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM/D,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI/B,OAAO,CAAC,GAAG;CAYd;AAGD,eAAO,MAAM,MAAM,QAAe,CAAC"}
1
+ {"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,oBAAY,QAAQ;IAChB,KAAK,IAAI;IACT,IAAI,IAAI;IACR,IAAI,IAAI;IACR,KAAK,IAAI;CACZ;AAqCD,qBAAa,MAAM;IACf,OAAO,CAAC,KAAK,CAAW;IACxB,OAAO,CAAC,QAAQ,CAAU;gBAEd,QAAQ,CAAC,EAAE,QAAQ;IAK/B,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM/D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM9D,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM9D,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAM/D,QAAQ,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI;IAI/B,WAAW,CAAC,OAAO,EAAE,OAAO,GAAG,IAAI;IAInC;;;OAGG;IACH,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG;QAAC,GAAG,EAAE,MAAM,MAAM,CAAA;KAAC;IAWzC,OAAO,CAAC,GAAG;CAoBd;AAGD,eAAO,MAAM,MAAM,QAAe,CAAC"}
package/dist/logger.js CHANGED
@@ -51,6 +51,7 @@ function logLevelToString(level) {
51
51
  class Logger {
52
52
  constructor(minLevel) {
53
53
  this.level = minLevel ?? getLogLevelFromEnv();
54
+ this.jsonMode = process.env.LOG_FORMAT?.toLowerCase() === 'json';
54
55
  }
55
56
  error(message, context) {
56
57
  if (this.level >= LogLevel.ERROR) {
@@ -75,11 +76,37 @@ class Logger {
75
76
  setLevel(level) {
76
77
  this.level = level;
77
78
  }
79
+ setJsonMode(enabled) {
80
+ this.jsonMode = enabled;
81
+ }
82
+ /**
83
+ * Start a timer for measuring duration of an operation.
84
+ * Returns an object with `end()` that logs at DEBUG level and returns elapsed ms.
85
+ */
86
+ timer(label) {
87
+ const start = performance.now();
88
+ return {
89
+ end: () => {
90
+ const elapsed = Math.round(performance.now() - start);
91
+ this.debug(`${label} completed`, { durationMs: elapsed });
92
+ return elapsed;
93
+ },
94
+ };
95
+ }
78
96
  log(level, message, context) {
79
97
  const timestamp = new Date().toISOString();
80
98
  const levelStr = logLevelToString(level);
81
- const contextStr = context ? ` ${JSON.stringify(context)}` : '';
82
- const output = `[${timestamp}] [${levelStr}] ${message}${contextStr}`;
99
+ let output;
100
+ if (this.jsonMode) {
101
+ const entry = { ts: timestamp, level: levelStr, msg: message };
102
+ if (context)
103
+ entry.ctx = context;
104
+ output = JSON.stringify(entry);
105
+ }
106
+ else {
107
+ const contextStr = context ? ` ${JSON.stringify(context)}` : '';
108
+ output = `[${timestamp}] [${levelStr}] ${message}${contextStr}`;
109
+ }
83
110
  if (level <= LogLevel.WARN) {
84
111
  console.error(output);
85
112
  }
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/pipeline/orchestrator.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAmB,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAuD,KAAK,UAAU,EAAE,KAAK,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACxH,OAAO,EAAe,KAAK,kBAAkB,EAAoB,MAAM,gCAAgC,CAAC;AAExG,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,iEAAiE;IACjE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAqBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAgIjF"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/pipeline/orchestrator.ts"],"names":[],"mappings":"AAQA,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAmB,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAuD,KAAK,UAAU,EAAE,KAAK,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACxH,OAAO,EAAe,KAAK,kBAAkB,EAAoB,MAAM,gCAAgC,CAAC;AAExG,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,iEAAiE;IACjE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAqBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CA6IjF"}