@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.
- package/dist/cli/commands/train.d.ts.map +1 -1
- package/dist/cli/commands/train.js +125 -50
- package/dist/cli/parse_args.d.ts.map +1 -1
- package/dist/cli/parse_args.js +3 -0
- package/dist/cli/types.d.ts +3 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/esm/cli/commands/train.js +125 -50
- package/dist/esm/cli/parse_args.js +3 -0
- package/dist/esm/logger.js +29 -2
- package/dist/esm/pipeline/orchestrator.js +17 -3
- package/dist/esm/training/enricher.js +82 -11
- package/dist/esm/training/merger.js +77 -10
- package/dist/esm/training/scanner.js +523 -2
- 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 +3 -1
- package/dist/training/enricher.d.ts.map +1 -1
- package/dist/training/enricher.js +82 -11
- package/dist/training/merger.d.ts +11 -1
- package/dist/training/merger.d.ts.map +1 -1
- package/dist/training/merger.js +77 -10
- package/dist/training/scanner.d.ts +28 -2
- package/dist/training/scanner.d.ts.map +1 -1
- package/dist/training/scanner.js +527 -2
- package/dist/training/types.d.ts +8 -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
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;
|
|
@@ -8,8 +8,10 @@ export interface EnrichedEntry {
|
|
|
8
8
|
routes?: string[];
|
|
9
9
|
pageObjects?: string[];
|
|
10
10
|
components?: string[];
|
|
11
|
+
webappPaths?: string[];
|
|
12
|
+
serverPaths?: string[];
|
|
11
13
|
}
|
|
12
14
|
export declare function validateEntries(parsed: unknown[]): EnrichedEntry[];
|
|
13
15
|
export declare function parseEnrichResponse(response: string): EnrichedEntry[];
|
|
14
|
-
export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number): Promise<EnrichmentResult>;
|
|
16
|
+
export declare function enrichFamilies(families: RouteFamily[], scanned: ScannedFamily[], projectRoot: string, provider: LLMProvider, budgetUSD: number, testsRoot?: string): Promise<EnrichmentResult>;
|
|
15
17
|
//# sourceMappingURL=enricher.d.ts.map
|
|
@@ -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;
|
|
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"}
|
|
@@ -64,9 +64,47 @@ function sampleFiles(dir, maxFiles) {
|
|
|
64
64
|
walk(dir);
|
|
65
65
|
return files;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
/**
|
|
68
|
+
* Build a shallow directory listing of the source tree (depth 2-3) so the LLM
|
|
69
|
+
* can suggest accurate webappPaths / serverPaths for test-derived families.
|
|
70
|
+
*/
|
|
71
|
+
function getSourceTreeListing(projectRoot, maxDepth = 3) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
function walk(dir, depth, prefix) {
|
|
74
|
+
if (depth > maxDepth || lines.length > 200)
|
|
75
|
+
return;
|
|
76
|
+
let entries;
|
|
77
|
+
try {
|
|
78
|
+
entries = (0, fs_1.readdirSync)(dir).sort();
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const dirs = entries.filter((e) => {
|
|
84
|
+
if (e.startsWith('.') || SKIP_DIRS.has(e))
|
|
85
|
+
return false;
|
|
86
|
+
try {
|
|
87
|
+
const stat = (0, fs_1.lstatSync)((0, path_1.join)(dir, e));
|
|
88
|
+
return !stat.isSymbolicLink() && stat.isDirectory();
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
for (const d of dirs) {
|
|
95
|
+
lines.push(`${prefix}${d}/`);
|
|
96
|
+
walk((0, path_1.join)(dir, d), depth + 1, prefix + ' ');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
walk((0, path_1.resolve)(projectRoot), 0, '');
|
|
100
|
+
return lines.join('\n');
|
|
101
|
+
}
|
|
102
|
+
function buildEnrichPrompt(families, projectRoot, testsRoot) {
|
|
68
103
|
const sections = [];
|
|
104
|
+
const hasTestOnlyFamilies = families.some((f) => f.webappPaths.length === 0 && f.serverPaths.length === 0);
|
|
105
|
+
const resolvedTestsRoot = testsRoot ? (0, path_1.resolve)(testsRoot) : (0, path_1.resolve)(projectRoot);
|
|
69
106
|
for (const family of families) {
|
|
107
|
+
const isTestOnly = family.webappPaths.length === 0 && family.serverPaths.length === 0;
|
|
70
108
|
const allDirs = [
|
|
71
109
|
...family.webappPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
72
110
|
...family.serverPaths.map((p) => p.replace(/\/?\*.*$/, '')),
|
|
@@ -80,10 +118,19 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
80
118
|
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
81
119
|
break;
|
|
82
120
|
}
|
|
121
|
+
// For test-only families, sample the test files themselves for richer context
|
|
122
|
+
if (isTestOnly) {
|
|
123
|
+
for (const specDir of family.specDirs) {
|
|
124
|
+
if (samples.length >= MAX_FILES_PER_FAMILY)
|
|
125
|
+
break;
|
|
126
|
+
const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
|
|
127
|
+
samples.push(...sampleFiles(fullDir, MAX_FILES_PER_FAMILY - samples.length));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
83
130
|
// Sample spec descriptions
|
|
84
131
|
const specSamples = [];
|
|
85
132
|
for (const specDir of family.specDirs) {
|
|
86
|
-
const fullDir = (0, path_1.join)(
|
|
133
|
+
const fullDir = (0, path_1.join)(resolvedTestsRoot, specDir);
|
|
87
134
|
const specFiles = sampleFiles(fullDir, 5);
|
|
88
135
|
for (const sf of specFiles) {
|
|
89
136
|
const matches = sf.content.match(/(?:test|it|describe)\s*\(\s*['"`]([^'"`]+)/g);
|
|
@@ -92,7 +139,7 @@ function buildEnrichPrompt(families, projectRoot) {
|
|
|
92
139
|
}
|
|
93
140
|
}
|
|
94
141
|
}
|
|
95
|
-
sections.push(`## Family: ${family.id}
|
|
142
|
+
sections.push(`## Family: ${family.id}${isTestOnly ? ' [TEST-ONLY — needs webappPaths/serverPaths]' : ''}
|
|
96
143
|
Routes (guessed): ${JSON.stringify(family.routes)}
|
|
97
144
|
Webapp paths: ${JSON.stringify(family.webappPaths)}
|
|
98
145
|
Server paths: ${JSON.stringify(family.serverPaths)}
|
|
@@ -107,6 +154,10 @@ Test descriptions:
|
|
|
107
154
|
${specSamples.length > 0 ? specSamples.map((d) => `- ${d}`).join('\n') : '(none found)'}
|
|
108
155
|
`);
|
|
109
156
|
}
|
|
157
|
+
// Include source tree listing when we have test-only families
|
|
158
|
+
const sourceTreeSection = hasTestOnlyFamilies
|
|
159
|
+
? `\n## Source Directory Structure\nUse this to suggest accurate webappPaths and serverPaths for test-only families:\n\`\`\`\n${getSourceTreeListing(projectRoot)}\n\`\`\`\n`
|
|
160
|
+
: '';
|
|
110
161
|
return `You are analyzing a codebase to enrich route-family definitions for an E2E test impact analysis tool.
|
|
111
162
|
|
|
112
163
|
For each family below, provide:
|
|
@@ -115,6 +166,8 @@ For each family below, provide:
|
|
|
115
166
|
3. **routes**: Improved URL patterns (e.g., "/{team}/channels/{channel}" instead of "/channels")
|
|
116
167
|
4. **pageObjects**: Array of page object class names found in the code
|
|
117
168
|
5. **components**: Array of UI component names relevant to this family
|
|
169
|
+
6. **webappPaths**: Array of glob patterns for frontend source directories (e.g., "src/components/drafts/**"). REQUIRED for families marked [TEST-ONLY].
|
|
170
|
+
7. **serverPaths**: Array of glob patterns for backend source directories. REQUIRED for families marked [TEST-ONLY].
|
|
118
171
|
|
|
119
172
|
Respond in JSON format:
|
|
120
173
|
\`\`\`json
|
|
@@ -125,11 +178,13 @@ Respond in JSON format:
|
|
|
125
178
|
"userFlows": ["Flow name 1", "Flow name 2"],
|
|
126
179
|
"routes": ["/improved/route/{param}"],
|
|
127
180
|
"pageObjects": ["PageName"],
|
|
128
|
-
"components": ["ComponentName"]
|
|
181
|
+
"components": ["ComponentName"],
|
|
182
|
+
"webappPaths": ["src/components/feature_name/**"],
|
|
183
|
+
"serverPaths": ["server/channels/api4/feature.go"]
|
|
129
184
|
}
|
|
130
185
|
]
|
|
131
186
|
\`\`\`
|
|
132
|
-
|
|
187
|
+
${sourceTreeSection}
|
|
133
188
|
${sections.join('\n---\n')}`;
|
|
134
189
|
}
|
|
135
190
|
function validateEntries(parsed) {
|
|
@@ -148,6 +203,8 @@ function validateEntries(parsed) {
|
|
|
148
203
|
userFlows: filterStrings(entry.userFlows, 500),
|
|
149
204
|
pageObjects: filterStrings(entry.pageObjects, 200),
|
|
150
205
|
components: filterStrings(entry.components, 200),
|
|
206
|
+
webappPaths: filterStrings(entry.webappPaths, 300),
|
|
207
|
+
serverPaths: filterStrings(entry.serverPaths, 300),
|
|
151
208
|
}));
|
|
152
209
|
}
|
|
153
210
|
function parseEnrichResponse(response) {
|
|
@@ -197,13 +254,22 @@ function applyEnrichment(family, enriched) {
|
|
|
197
254
|
if (enriched.components && (!family.components || family.components.length === 0)) {
|
|
198
255
|
result.components = enriched.components;
|
|
199
256
|
}
|
|
257
|
+
// Only fill source paths when the family has none (test-derived families)
|
|
258
|
+
if (enriched.webappPaths && (!family.webappPaths || family.webappPaths.length === 0)) {
|
|
259
|
+
result.webappPaths = enriched.webappPaths;
|
|
260
|
+
}
|
|
261
|
+
if (enriched.serverPaths && (!family.serverPaths || family.serverPaths.length === 0)) {
|
|
262
|
+
result.serverPaths = enriched.serverPaths;
|
|
263
|
+
}
|
|
200
264
|
return result;
|
|
201
265
|
}
|
|
202
|
-
async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD) {
|
|
266
|
+
async function enrichFamilies(families, scanned, projectRoot, provider, budgetUSD, testsRoot) {
|
|
203
267
|
const scannedMap = new Map(scanned.map((s) => [s.id, s]));
|
|
204
268
|
const enriched = [];
|
|
205
269
|
let totalTokens = 0;
|
|
206
270
|
let totalCost = 0;
|
|
271
|
+
let requestCount = 0;
|
|
272
|
+
let totalResponseMs = 0;
|
|
207
273
|
const skipped = [];
|
|
208
274
|
// Process in chunks of 4 families
|
|
209
275
|
const chunkSize = 4;
|
|
@@ -223,7 +289,7 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
|
|
|
223
289
|
enriched.push(...chunk);
|
|
224
290
|
continue;
|
|
225
291
|
}
|
|
226
|
-
let prompt = buildEnrichPrompt(scannedChunk, projectRoot);
|
|
292
|
+
let prompt = buildEnrichPrompt(scannedChunk, projectRoot, testsRoot);
|
|
227
293
|
if (prompt.length > MAX_PROMPT_CHARS) {
|
|
228
294
|
// Truncate at the last complete section boundary to avoid malformed input
|
|
229
295
|
const lastSectionEnd = prompt.lastIndexOf('\n---\n', MAX_PROMPT_CHARS);
|
|
@@ -236,15 +302,18 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
|
|
|
236
302
|
prompt = prompt.slice(0, MAX_PROMPT_CHARS);
|
|
237
303
|
}
|
|
238
304
|
}
|
|
239
|
-
let
|
|
305
|
+
let timeoutTimer;
|
|
240
306
|
try {
|
|
241
307
|
const timeoutPromise = new Promise((_, reject) => {
|
|
242
|
-
|
|
308
|
+
timeoutTimer = setTimeout(() => reject(new Error('LLM request timed out')), LLM_TIMEOUT_MS);
|
|
243
309
|
});
|
|
310
|
+
const reqStart = performance.now();
|
|
244
311
|
const response = await Promise.race([
|
|
245
312
|
provider.generateText(prompt, { maxTokens: 4096, temperature: 0.3 }),
|
|
246
313
|
timeoutPromise,
|
|
247
314
|
]);
|
|
315
|
+
totalResponseMs += performance.now() - reqStart;
|
|
316
|
+
requestCount++;
|
|
248
317
|
totalTokens += (response.usage?.inputTokens ?? 0) + (response.usage?.outputTokens ?? 0);
|
|
249
318
|
totalCost += response.cost ?? 0;
|
|
250
319
|
const entries = parseEnrichResponse(response.text);
|
|
@@ -265,8 +334,8 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
|
|
|
265
334
|
enriched.push(...chunk);
|
|
266
335
|
}
|
|
267
336
|
finally {
|
|
268
|
-
if (
|
|
269
|
-
clearTimeout(
|
|
337
|
+
if (timeoutTimer)
|
|
338
|
+
clearTimeout(timeoutTimer);
|
|
270
339
|
}
|
|
271
340
|
}
|
|
272
341
|
return {
|
|
@@ -274,5 +343,7 @@ async function enrichFamilies(families, scanned, projectRoot, provider, budgetUS
|
|
|
274
343
|
tokensUsed: totalTokens,
|
|
275
344
|
costUSD: Math.round(totalCost * 100) / 100,
|
|
276
345
|
skippedFamilies: skipped,
|
|
346
|
+
requestCount,
|
|
347
|
+
avgResponseMs: requestCount > 0 ? Math.round(totalResponseMs / requestCount) : 0,
|
|
277
348
|
};
|
|
278
349
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { RouteFamilyManifest } from '../knowledge/route_families.js';
|
|
2
2
|
import type { MergeResult, ScannedFamily } from './types.js';
|
|
3
3
|
export declare function mergeFamilies(existing: RouteFamilyManifest | null, scanned: ScannedFamily[]): MergeResult;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Detect families whose paths no longer exist on disk.
|
|
6
|
+
*
|
|
7
|
+
* Paths in the manifest may be relative to different roots:
|
|
8
|
+
* - webappPaths / serverPaths are typically relative to the repo root
|
|
9
|
+
* - specDirs may be relative to the tests root
|
|
10
|
+
*
|
|
11
|
+
* We try each pattern against all provided roots (and the git repo root
|
|
12
|
+
* if discoverable) to avoid false positives from path-prefix mismatches.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectStaleFamilies(manifest: RouteFamilyManifest, projectRoot: string, testsRoot?: string): string[];
|
|
5
15
|
//# sourceMappingURL=merger.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"merger.d.ts","sourceRoot":"","sources":["../../src/training/merger.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAc,mBAAmB,EAAC,MAAM,gCAAgC,CAAC;AAGrF,OAAO,KAAK,EAAC,WAAW,EAAE,aAAa,EAAC,MAAM,YAAY,CAAC;AAkF3D,wBAAgB,aAAa,CACzB,QAAQ,EAAE,mBAAmB,GAAG,IAAI,EACpC,OAAO,EAAE,aAAa,EAAE,GACzB,WAAW,CA+Cb;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAC/B,QAAQ,EAAE,mBAAmB,EAC7B,WAAW,EAAE,MAAM,EACnB,SAAS,CAAC,EAAE,MAAM,GACnB,MAAM,EAAE,CA6DV"}
|
package/dist/training/merger.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
5
|
exports.mergeFamilies = mergeFamilies;
|
|
6
6
|
exports.detectStaleFamilies = detectStaleFamilies;
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
7
8
|
const fs_1 = require("fs");
|
|
8
9
|
const path_1 = require("path");
|
|
9
10
|
const types_js_1 = require("./types.js");
|
|
@@ -71,6 +72,21 @@ function scannedToRouteFamily(scanned) {
|
|
|
71
72
|
}
|
|
72
73
|
return family;
|
|
73
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Try to find a matching family ID with singular/plural normalization.
|
|
77
|
+
* "team" matches "teams", "emoji" matches "emoji", etc.
|
|
78
|
+
*/
|
|
79
|
+
function findFuzzyMatch(id, idMap) {
|
|
80
|
+
if (idMap.has(id))
|
|
81
|
+
return id;
|
|
82
|
+
// Try adding 's'
|
|
83
|
+
if (!id.endsWith('s') && idMap.has(id + 's'))
|
|
84
|
+
return id + 's';
|
|
85
|
+
// Try removing 's'
|
|
86
|
+
if (id.endsWith('s') && idMap.has(id.slice(0, -1)))
|
|
87
|
+
return id.slice(0, -1);
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
74
90
|
function mergeFamilies(existing, scanned) {
|
|
75
91
|
const existingFamilies = existing?.families || [];
|
|
76
92
|
const existingMap = new Map(existingFamilies.map((f) => [f.id, f]));
|
|
@@ -78,9 +94,15 @@ function mergeFamilies(existing, scanned) {
|
|
|
78
94
|
const newFamilies = [];
|
|
79
95
|
const updatedFamilies = [];
|
|
80
96
|
const mergedFamilies = [];
|
|
81
|
-
// Process existing families
|
|
97
|
+
// Process existing families — match scanned by exact or fuzzy ID
|
|
82
98
|
for (const ef of existingFamilies) {
|
|
83
|
-
|
|
99
|
+
let sf = scannedMap.get(ef.id);
|
|
100
|
+
// Try singular/plural match if exact match failed
|
|
101
|
+
if (!sf) {
|
|
102
|
+
const fuzzyId = findFuzzyMatch(ef.id, scannedMap);
|
|
103
|
+
if (fuzzyId)
|
|
104
|
+
sf = scannedMap.get(fuzzyId);
|
|
105
|
+
}
|
|
84
106
|
if (sf) {
|
|
85
107
|
mergedFamilies.push(mergeFamily(ef, sf));
|
|
86
108
|
updatedFamilies.push(ef.id);
|
|
@@ -90,9 +112,10 @@ function mergeFamilies(existing, scanned) {
|
|
|
90
112
|
mergedFamilies.push({ ...ef });
|
|
91
113
|
}
|
|
92
114
|
}
|
|
93
|
-
// Add new families from scanner
|
|
115
|
+
// Add new families from scanner (if no existing family matched)
|
|
94
116
|
for (const sf of scanned) {
|
|
95
|
-
|
|
117
|
+
const matchedExisting = findFuzzyMatch(sf.id, existingMap);
|
|
118
|
+
if (!matchedExisting) {
|
|
96
119
|
mergedFamilies.push(scannedToRouteFamily(sf));
|
|
97
120
|
newFamilies.push(sf.id);
|
|
98
121
|
}
|
|
@@ -112,8 +135,33 @@ function mergeFamilies(existing, scanned) {
|
|
|
112
135
|
summary: parts.join(', '),
|
|
113
136
|
};
|
|
114
137
|
}
|
|
115
|
-
|
|
116
|
-
|
|
138
|
+
/**
|
|
139
|
+
* Detect families whose paths no longer exist on disk.
|
|
140
|
+
*
|
|
141
|
+
* Paths in the manifest may be relative to different roots:
|
|
142
|
+
* - webappPaths / serverPaths are typically relative to the repo root
|
|
143
|
+
* - specDirs may be relative to the tests root
|
|
144
|
+
*
|
|
145
|
+
* We try each pattern against all provided roots (and the git repo root
|
|
146
|
+
* if discoverable) to avoid false positives from path-prefix mismatches.
|
|
147
|
+
*/
|
|
148
|
+
function detectStaleFamilies(manifest, projectRoot, testsRoot) {
|
|
149
|
+
const roots = new Set([(0, path_1.resolve)(projectRoot)]);
|
|
150
|
+
if (testsRoot)
|
|
151
|
+
roots.add((0, path_1.resolve)(testsRoot));
|
|
152
|
+
// Also try to discover the git repo root — manifest paths may be repo-relative
|
|
153
|
+
try {
|
|
154
|
+
const gitRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
|
|
155
|
+
cwd: projectRoot,
|
|
156
|
+
encoding: 'utf-8',
|
|
157
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
158
|
+
}).trim();
|
|
159
|
+
if (gitRoot)
|
|
160
|
+
roots.add((0, path_1.resolve)(gitRoot));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Not a git repo or git not available — that's fine
|
|
164
|
+
}
|
|
117
165
|
const stale = [];
|
|
118
166
|
for (const family of manifest.families) {
|
|
119
167
|
const allPatterns = [
|
|
@@ -123,15 +171,34 @@ function detectStaleFamilies(manifest, projectRoot) {
|
|
|
123
171
|
];
|
|
124
172
|
if (allPatterns.length === 0)
|
|
125
173
|
continue;
|
|
126
|
-
// Check if any pattern resolves to existing files/dirs
|
|
174
|
+
// Check if any pattern resolves to existing files/dirs in any root
|
|
127
175
|
let hasAny = false;
|
|
128
176
|
for (const pattern of allPatterns) {
|
|
129
177
|
// Strip trailing glob (* or **) to get the directory
|
|
130
178
|
const dirPart = pattern.replace(/\/?\*.*$/, '');
|
|
131
|
-
if (dirPart
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
if (!dirPart)
|
|
180
|
+
continue;
|
|
181
|
+
// For file-level patterns like "server/channels/api4/draft*.go",
|
|
182
|
+
// dirPart is "server/channels/api4/draft" — check the parent dir instead
|
|
183
|
+
const isFileGlob = /\.\w+$/.test(pattern);
|
|
184
|
+
const pathsToCheck = [dirPart];
|
|
185
|
+
if (isFileGlob) {
|
|
186
|
+
const parentDir = dirPart.split('/').slice(0, -1).join('/');
|
|
187
|
+
if (parentDir)
|
|
188
|
+
pathsToCheck.push(parentDir);
|
|
189
|
+
}
|
|
190
|
+
for (const checkPath of pathsToCheck) {
|
|
191
|
+
for (const root of roots) {
|
|
192
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(root, checkPath))) {
|
|
193
|
+
hasAny = true;
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (hasAny)
|
|
198
|
+
break;
|
|
134
199
|
}
|
|
200
|
+
if (hasAny)
|
|
201
|
+
break;
|
|
135
202
|
}
|
|
136
203
|
if (!hasAny) {
|
|
137
204
|
stale.push(family.id);
|
|
@@ -1,5 +1,31 @@
|
|
|
1
|
-
import type { DiscoveredDir, ScanResult } from './types.js';
|
|
1
|
+
import type { DiscoveredDir, ScannedFamily, ScanResult } from './types.js';
|
|
2
2
|
export declare function discoverSourceDirs(projectRoot: string): DiscoveredDir[];
|
|
3
3
|
export declare function discoverTestDirs(projectRoot: string): DiscoveredDir[];
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Discover families by scanning server Go source files.
|
|
6
|
+
*
|
|
7
|
+
* The backend follows a three-tier pattern:
|
|
8
|
+
* api4/draft.go + app/draft.go + store/sqlstore/draft_store.go
|
|
9
|
+
*
|
|
10
|
+
* Related files are grouped under parent domains:
|
|
11
|
+
* channel.go, channel_bookmark.go, channel_category.go → "channel" family
|
|
12
|
+
*
|
|
13
|
+
* Each domain becomes a candidate family with precise serverPaths.
|
|
14
|
+
*/
|
|
15
|
+
export declare function discoverServerDerivedFamilies(serverRoot: string): {
|
|
16
|
+
multiTierFamilies: ScannedFamily[];
|
|
17
|
+
singleTierFamilies: ScannedFamily[];
|
|
18
|
+
};
|
|
19
|
+
export declare function discoverTestDerivedFamilies(testsRoot: string): ScannedFamily[];
|
|
20
|
+
/**
|
|
21
|
+
* Discover test library paths (page objects, helpers) organized by feature.
|
|
22
|
+
* Walks well-known test lib directories and maps subdirectories to family IDs.
|
|
23
|
+
*/
|
|
24
|
+
export declare function discoverTestLibPaths(testsRoot: string): Map<string, string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Discover files in well-known directories (types, utils) whose basename
|
|
27
|
+
* maps directly to a family ID.
|
|
28
|
+
*/
|
|
29
|
+
export declare function discoverNameMatchedPaths(appPath: string, gitRepoRoot?: string): Map<string, string[]>;
|
|
30
|
+
export declare function scanProject(projectRoot: string, testsRoot?: string, serverRoot?: string, gitRepoRoot?: string): ScanResult;
|
|
5
31
|
//# sourceMappingURL=scanner.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,
|
|
1
|
+
{"version":3,"file":"scanner.d.ts","sourceRoot":"","sources":["../../src/training/scanner.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,aAAa,EAAE,aAAa,EAAkB,UAAU,EAAC,MAAM,YAAY,CAAC;AAgJzF,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA+BvE;AAED,wBAAgB,gBAAgB,CAAC,WAAW,EAAE,MAAM,GAAG,aAAa,EAAE,CA6DrE;AAuLD;;;;;;;;;;GAUG;AACH,wBAAgB,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG;IAAC,iBAAiB,EAAE,aAAa,EAAE,CAAC;IAAC,kBAAkB,EAAE,aAAa,EAAE,CAAA;CAAC,CAgI3I;AAED,wBAAgB,2BAA2B,CAAC,SAAS,EAAE,MAAM,GAAG,aAAa,EAAE,CAiG9E;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAmC7E;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CACpC,OAAO,EAAE,MAAM,EACf,WAAW,CAAC,EAAE,MAAM,GACrB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAmDvB;AAED,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,UAAU,CA0L1H"}
|