@yasserkhanorg/e2e-agents 1.6.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.
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +96 -48
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +2 -0
- package/dist/cli/types.d.ts +2 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/esm/cli/commands/train.js +96 -48
- package/dist/esm/cli/parse_args.js +2 -0
- package/dist/esm/logger.js +29 -2
- package/dist/esm/pipeline/orchestrator.js +17 -3
- package/dist/esm/training/enricher.js +11 -4
- package/dist/esm/training/scanner.js +167 -12
- package/dist/esm/training/validator.js +58 -2
- package/dist/logger.d.ts +9 -0
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +29 -2
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +17 -3
- package/dist/training/enricher.d.ts.map +1 -1
- package/dist/training/enricher.js +11 -4
- package/dist/training/scanner.d.ts +15 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +169 -12
- package/dist/training/types.d.ts +4 -0
- package/dist/training/types.d.ts.map +1 -1
- package/dist/training/validator.d.ts +5 -0
- package/dist/training/validator.d.ts.map +1 -1
- package/dist/training/validator.js +59 -2
- package/package.json +1 -1
|
@@ -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(
|
|
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
|
|
300
|
+
let timeoutTimer;
|
|
299
301
|
try {
|
|
300
302
|
const timeoutPromise = new Promise((_, reject) => {
|
|
301
|
-
|
|
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 (
|
|
328
|
-
clearTimeout(
|
|
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
|
-
//
|
|
517
|
-
|
|
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
|
-
|
|
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
|
|
542
|
+
return { multiTierFamilies, singleTierFamilies };
|
|
537
543
|
}
|
|
538
544
|
export function discoverTestDerivedFamilies(testsRoot) {
|
|
539
545
|
const resolved = resolve(testsRoot);
|
|
@@ -620,7 +626,113 @@ export function discoverTestDerivedFamilies(testsRoot) {
|
|
|
620
626
|
}
|
|
621
627
|
return Array.from(familyMap.values());
|
|
622
628
|
}
|
|
623
|
-
|
|
629
|
+
/**
|
|
630
|
+
* Discover test library paths (page objects, helpers) organized by feature.
|
|
631
|
+
* Walks well-known test lib directories and maps subdirectories 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() || !stat.isDirectory())
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
continue;
|
|
663
|
+
}
|
|
664
|
+
const familyId = normalizeId(entry);
|
|
665
|
+
const relPath = relative(resolved, fullPath).replace(/\\/g, '/');
|
|
666
|
+
const pattern = `${relPath}/*`;
|
|
667
|
+
if (!result.has(familyId))
|
|
668
|
+
result.set(familyId, []);
|
|
669
|
+
result.get(familyId).push(pattern);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return result;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* Discover files in well-known directories (types, utils) whose basename
|
|
676
|
+
* maps directly to a family ID.
|
|
677
|
+
*/
|
|
678
|
+
export function discoverNameMatchedPaths(appPath, gitRepoRoot) {
|
|
679
|
+
const result = new Map();
|
|
680
|
+
const resolvedApp = resolve(appPath);
|
|
681
|
+
const scanRoots = [
|
|
682
|
+
{ root: join(resolvedApp, 'src/utils'), base: resolvedApp },
|
|
683
|
+
{ root: join(resolvedApp, 'src/types'), base: resolvedApp },
|
|
684
|
+
];
|
|
685
|
+
// Monorepo-aware: scan platform types directory
|
|
686
|
+
if (gitRepoRoot) {
|
|
687
|
+
const resolvedGitRoot = resolve(gitRepoRoot);
|
|
688
|
+
const platformTypes = join(resolvedGitRoot, 'webapp/platform/types/src');
|
|
689
|
+
if (existsSync(platformTypes)) {
|
|
690
|
+
scanRoots.push({ root: platformTypes, base: resolvedGitRoot });
|
|
691
|
+
}
|
|
692
|
+
const platformClient = join(resolvedGitRoot, 'webapp/platform/client/src');
|
|
693
|
+
if (existsSync(platformClient)) {
|
|
694
|
+
scanRoots.push({ root: platformClient, base: resolvedGitRoot });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (const { root, base } of scanRoots) {
|
|
698
|
+
if (!existsSync(root))
|
|
699
|
+
continue;
|
|
700
|
+
let entries;
|
|
701
|
+
try {
|
|
702
|
+
entries = readdirSync(root);
|
|
703
|
+
}
|
|
704
|
+
catch {
|
|
705
|
+
continue;
|
|
706
|
+
}
|
|
707
|
+
for (const entry of entries) {
|
|
708
|
+
if (entry.startsWith('.'))
|
|
709
|
+
continue;
|
|
710
|
+
const ext = entry.slice(entry.lastIndexOf('.'));
|
|
711
|
+
if (!['.ts', '.tsx', '.js', '.jsx'].includes(ext))
|
|
712
|
+
continue;
|
|
713
|
+
const fullPath = join(root, entry);
|
|
714
|
+
try {
|
|
715
|
+
const stat = lstatSync(fullPath);
|
|
716
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
// Strip extension and normalize
|
|
723
|
+
const baseName = entry.slice(0, entry.lastIndexOf('.'));
|
|
724
|
+
const familyId = normalizeId(baseName);
|
|
725
|
+
if (familyId.length < 3)
|
|
726
|
+
continue;
|
|
727
|
+
const relPath = relative(base, fullPath).replace(/\\/g, '/');
|
|
728
|
+
if (!result.has(familyId))
|
|
729
|
+
result.set(familyId, []);
|
|
730
|
+
result.get(familyId).push(relPath);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return result;
|
|
734
|
+
}
|
|
735
|
+
export function scanProject(projectRoot, testsRoot, serverRoot, gitRepoRoot) {
|
|
624
736
|
const resolved = resolve(projectRoot);
|
|
625
737
|
const resolvedTestsRoot = testsRoot ? resolve(testsRoot) : resolved;
|
|
626
738
|
const sourceDirs = discoverSourceDirs(resolved);
|
|
@@ -683,9 +795,12 @@ export function scanProject(projectRoot, testsRoot, serverRoot) {
|
|
|
683
795
|
// When a separate serverRoot is provided, discover families from Go source
|
|
684
796
|
// filenames across the three-tier backend (api4, app, store).
|
|
685
797
|
if (serverRoot) {
|
|
686
|
-
const
|
|
798
|
+
const { multiTierFamilies: serverMulti, singleTierFamilies: serverSingle } = discoverServerDerivedFamilies(resolve(serverRoot));
|
|
687
799
|
const existingIds = new Set(families.map((f) => f.id));
|
|
688
|
-
|
|
800
|
+
// Merge ALL server families (multi + single tier) into existing families,
|
|
801
|
+
// but only add NEW families if they span ≥2 tiers.
|
|
802
|
+
const allServerFamilies = [...serverMulti, ...serverSingle];
|
|
803
|
+
for (const sf of allServerFamilies) {
|
|
689
804
|
// Try exact match, then singular/plural variants
|
|
690
805
|
let target = families.find((f) => f.id === sf.id);
|
|
691
806
|
if (!target && !sf.id.endsWith('s')) {
|
|
@@ -702,13 +817,53 @@ export function scanProject(projectRoot, testsRoot, serverRoot) {
|
|
|
702
817
|
}
|
|
703
818
|
}
|
|
704
819
|
}
|
|
705
|
-
else {
|
|
706
|
-
//
|
|
820
|
+
else if (serverMulti.includes(sf)) {
|
|
821
|
+
// Only add new families if they span ≥2 tiers
|
|
707
822
|
families.push(sf);
|
|
708
823
|
existingIds.add(sf.id);
|
|
709
824
|
}
|
|
710
825
|
}
|
|
711
826
|
}
|
|
827
|
+
// Merge test library paths (page objects, helpers) into existing families
|
|
828
|
+
if (testsRoot) {
|
|
829
|
+
const testLibPaths = discoverTestLibPaths(resolvedTestsRoot);
|
|
830
|
+
for (const [libFamilyId, patterns] of testLibPaths) {
|
|
831
|
+
let target = families.find((f) => f.id === libFamilyId);
|
|
832
|
+
if (!target && !libFamilyId.endsWith('s')) {
|
|
833
|
+
target = families.find((f) => f.id === libFamilyId + 's');
|
|
834
|
+
}
|
|
835
|
+
if (!target && libFamilyId.endsWith('s')) {
|
|
836
|
+
target = families.find((f) => f.id === libFamilyId.slice(0, -1));
|
|
837
|
+
}
|
|
838
|
+
if (target) {
|
|
839
|
+
for (const p of patterns) {
|
|
840
|
+
if (!target.webappPaths.includes(p)) {
|
|
841
|
+
target.webappPaths.push(p);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Merge name-matched type/util files into existing families
|
|
848
|
+
{
|
|
849
|
+
const nameMatchedPaths = discoverNameMatchedPaths(resolved, gitRepoRoot);
|
|
850
|
+
for (const [nmFamilyId, paths] of nameMatchedPaths) {
|
|
851
|
+
let target = families.find((f) => f.id === nmFamilyId);
|
|
852
|
+
if (!target && !nmFamilyId.endsWith('s')) {
|
|
853
|
+
target = families.find((f) => f.id === nmFamilyId + 's');
|
|
854
|
+
}
|
|
855
|
+
if (!target && nmFamilyId.endsWith('s')) {
|
|
856
|
+
target = families.find((f) => f.id === nmFamilyId.slice(0, -1));
|
|
857
|
+
}
|
|
858
|
+
if (target) {
|
|
859
|
+
for (const p of paths) {
|
|
860
|
+
if (!target.webappPaths.includes(p)) {
|
|
861
|
+
target.webappPaths.push(p);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
712
867
|
const familyIds = new Set(families.map((f) => f.id));
|
|
713
868
|
const unmatchedSourceDirs = sourceDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
714
869
|
const unmatchedTestDirs = testDirs.filter((d) => !familyIds.has(normalizeId(d.familyHint)));
|
|
@@ -3,6 +3,62 @@
|
|
|
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/*', '**/scripts/*',
|
|
16
|
+
'**/docker-compose*',
|
|
17
|
+
'**/__fixtures__/*', '**/test_templates/*',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Check if a file path matches any infrastructure glob pattern.
|
|
21
|
+
* Uses simple string matching — no external glob library needed.
|
|
22
|
+
*/
|
|
23
|
+
export function isInfraFile(filePath) {
|
|
24
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
25
|
+
for (const pattern of INFRA_GLOBS) {
|
|
26
|
+
if (pattern.startsWith('**/')) {
|
|
27
|
+
// Match anywhere in the path
|
|
28
|
+
const suffix = pattern.slice(3);
|
|
29
|
+
if (suffix.endsWith('/*')) {
|
|
30
|
+
// Directory match: **/mocks/* → any segment named "mocks" with a child
|
|
31
|
+
const dirName = suffix.slice(0, -2);
|
|
32
|
+
if (normalized.includes(`/${dirName}/`) || normalized.startsWith(`${dirName}/`))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
else if (suffix.endsWith('*')) {
|
|
36
|
+
// Prefix match: **/docker-compose* → file starting with docker-compose
|
|
37
|
+
const prefix = suffix.slice(0, -1);
|
|
38
|
+
const base = normalized.split('/').pop() || '';
|
|
39
|
+
if (base.startsWith(prefix))
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (normalized.endsWith(`/${suffix}`) || normalized === suffix)
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (pattern.startsWith('*.')) {
|
|
48
|
+
// Extension match: *.lock
|
|
49
|
+
const ext = pattern.slice(1);
|
|
50
|
+
if (normalized.endsWith(ext))
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
// Exact basename match: Makefile, go.mod, go.sum
|
|
55
|
+
const base = normalized.split('/').pop() || '';
|
|
56
|
+
if (base === pattern)
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
6
62
|
export function parseGitLog(log) {
|
|
7
63
|
const commits = [];
|
|
8
64
|
let current = null;
|
|
@@ -49,10 +105,10 @@ export function getCommitFiles(projectRoot, since) {
|
|
|
49
105
|
return parseGitLog(log);
|
|
50
106
|
}
|
|
51
107
|
export function validateCommit(manifest, files, hash, message) {
|
|
52
|
-
// Filter out non-source files
|
|
108
|
+
// Filter out non-source files and infrastructure files
|
|
53
109
|
const sourceFiles = files.filter((f) => {
|
|
54
110
|
return !f.endsWith('.md') && !f.endsWith('.json') && !f.endsWith('.yml') && !f.endsWith('.yaml') &&
|
|
55
|
-
!f.startsWith('.') && !f.includes('node_modules/');
|
|
111
|
+
!f.startsWith('.') && !f.includes('node_modules/') && !isInfraFile(f);
|
|
56
112
|
});
|
|
57
113
|
if (sourceFiles.length === 0) {
|
|
58
114
|
return { hash, message, changedFiles: [], boundFiles: 0, unboundFiles: [], familiesHit: [] };
|
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;
|
package/dist/logger.d.ts.map
CHANGED
|
@@ -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;
|
|
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
|
-
|
|
82
|
-
|
|
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,
|
|
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"}
|
|
@@ -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(
|
|
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,
|
|
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
|
|
305
|
+
let timeoutTimer;
|
|
304
306
|
try {
|
|
305
307
|
const timeoutPromise = new Promise((_, reject) => {
|
|
306
|
-
|
|
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 (
|
|
333
|
-
clearTimeout(
|
|
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
|
}
|