@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
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AAqJ5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsO1F"}
|
|
@@ -43,6 +43,7 @@ const readline = __importStar(require("readline"));
|
|
|
43
43
|
const config_js_1 = require("../../agent/config.js");
|
|
44
44
|
const route_families_js_1 = require("../../knowledge/route_families.js");
|
|
45
45
|
const provider_factory_js_1 = require("../../provider_factory.js");
|
|
46
|
+
const logger_js_1 = require("../../logger.js");
|
|
46
47
|
const scanner_js_1 = require("../../training/scanner.js");
|
|
47
48
|
const merger_js_1 = require("../../training/merger.js");
|
|
48
49
|
const enricher_js_1 = require("../../training/enricher.js");
|
|
@@ -176,40 +177,47 @@ function serializeManifest(manifest) {
|
|
|
176
177
|
}
|
|
177
178
|
async function runTrainCommand(args, autoConfig) {
|
|
178
179
|
const opts = resolveTrainOptions(args, autoConfig);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
const totalTimer = logger_js_1.logger.timer('train-total');
|
|
181
|
+
const timings = {};
|
|
182
|
+
// Configure observability from CLI flags
|
|
183
|
+
if (args.verbose)
|
|
184
|
+
logger_js_1.logger.setLevel(logger_js_1.LogLevel.DEBUG);
|
|
185
|
+
if (args.jsonOutput)
|
|
186
|
+
logger_js_1.logger.setJsonMode(true);
|
|
187
|
+
logger_js_1.logger.info('e2e-ai-agents train');
|
|
188
|
+
logger_js_1.logger.info('===================');
|
|
183
189
|
// ---------- Phase 1: Deterministic scan ----------
|
|
184
|
-
|
|
190
|
+
logger_js_1.logger.info('Scanning project structure...');
|
|
185
191
|
if (opts.serverRoot) {
|
|
186
|
-
|
|
192
|
+
logger_js_1.logger.info(`Server root: ${opts.serverRoot}`);
|
|
187
193
|
}
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
const scanTimer = logger_js_1.logger.timer('scan');
|
|
195
|
+
const scanResult = (0, scanner_js_1.scanProject)(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot, opts.gitRepoRoot);
|
|
196
|
+
timings.scan = scanTimer.end();
|
|
197
|
+
logger_js_1.logger.info(`Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
198
|
+
logger_js_1.logger.info(`Discovered ${scanResult.families.length} candidate families`);
|
|
191
199
|
if (scanResult.families.length === 0) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
console.log(' (tests/, e2e/, specs/) with matching names.');
|
|
200
|
+
logger_js_1.logger.info('No families discovered. Make sure your project has recognizable');
|
|
201
|
+
logger_js_1.logger.info('source directories (src/, server/, app/) and test directories');
|
|
202
|
+
logger_js_1.logger.info('(tests/, e2e/, specs/) with matching names.');
|
|
196
203
|
return;
|
|
197
204
|
}
|
|
198
205
|
// ---------- Phase 2: Merge with existing ----------
|
|
206
|
+
const mergeTimer = logger_js_1.logger.timer('merge');
|
|
199
207
|
const existing = (0, route_families_js_1.loadRouteFamilyManifest)(opts.testsRoot);
|
|
200
208
|
if (existing) {
|
|
201
|
-
|
|
209
|
+
logger_js_1.logger.info(`Found existing manifest with ${existing.families.length} families`);
|
|
202
210
|
}
|
|
203
211
|
let mergeResult = (0, merger_js_1.mergeFamilies)(existing, scanResult.families);
|
|
204
|
-
|
|
212
|
+
timings.merge = mergeTimer.end();
|
|
213
|
+
logger_js_1.logger.info(`Merge: ${mergeResult.summary}`);
|
|
205
214
|
// ---------- Phase 3: Stale detection ----------
|
|
206
215
|
if (mergeResult.manifest.families.length > 0) {
|
|
207
216
|
const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath, opts.testsRoot);
|
|
208
217
|
if (stale.length > 0) {
|
|
209
|
-
|
|
210
|
-
console.log(` Stale families detected (${stale.length}):`);
|
|
218
|
+
logger_js_1.logger.info(`Stale families detected (${stale.length}):`);
|
|
211
219
|
for (const id of stale) {
|
|
212
|
-
|
|
220
|
+
logger_js_1.logger.info(` ${id} — paths no longer exist`);
|
|
213
221
|
}
|
|
214
222
|
if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
|
|
215
223
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -219,7 +227,7 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
219
227
|
const staleSet = new Set(stale);
|
|
220
228
|
mergeResult.manifest.families = mergeResult.manifest.families.filter((f) => !staleSet.has(f.id));
|
|
221
229
|
mergeResult.staleFamilies = stale;
|
|
222
|
-
|
|
230
|
+
logger_js_1.logger.info(`Removed ${stale.length} stale families`);
|
|
223
231
|
}
|
|
224
232
|
}
|
|
225
233
|
finally {
|
|
@@ -229,29 +237,41 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
229
237
|
}
|
|
230
238
|
}
|
|
231
239
|
// ---------- Phase 4: LLM Enrichment ----------
|
|
240
|
+
let enrichTokens = 0;
|
|
241
|
+
let enrichCost = 0;
|
|
242
|
+
let enrichRequests = 0;
|
|
243
|
+
let enrichAvgResponseMs = 0;
|
|
232
244
|
if (opts.enrich) {
|
|
233
|
-
|
|
234
|
-
|
|
245
|
+
logger_js_1.logger.info('Enriching with LLM...');
|
|
246
|
+
const enrichTimer = logger_js_1.logger.timer('enrich');
|
|
235
247
|
try {
|
|
236
248
|
const provider = await provider_factory_js_1.LLMProviderFactory.createFromEnv();
|
|
237
249
|
const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
|
|
238
250
|
mergeResult.manifest.families = enrichResult.enrichedFamilies;
|
|
239
|
-
|
|
251
|
+
enrichTokens = enrichResult.tokensUsed;
|
|
252
|
+
enrichCost = enrichResult.costUSD;
|
|
253
|
+
enrichRequests = enrichResult.requestCount ?? 0;
|
|
254
|
+
enrichAvgResponseMs = enrichResult.avgResponseMs ?? 0;
|
|
255
|
+
logger_js_1.logger.info(`Enriched ${enrichResult.enrichedFamilies.length} families`, {
|
|
256
|
+
tokens: enrichResult.tokensUsed,
|
|
257
|
+
cost: enrichResult.costUSD,
|
|
258
|
+
requests: enrichRequests,
|
|
259
|
+
avgResponseMs: enrichAvgResponseMs,
|
|
260
|
+
});
|
|
240
261
|
if (enrichResult.skippedFamilies.length > 0) {
|
|
241
|
-
|
|
262
|
+
logger_js_1.logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
|
|
242
263
|
}
|
|
243
264
|
}
|
|
244
265
|
catch (error) {
|
|
245
|
-
|
|
246
|
-
|
|
266
|
+
logger_js_1.logger.warn(`LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
267
|
+
logger_js_1.logger.warn('Continuing with deterministic results. Use --no-enrich to skip LLM.');
|
|
247
268
|
}
|
|
269
|
+
timings.enrich = enrichTimer.end();
|
|
248
270
|
}
|
|
249
271
|
// ---------- Phase 5: Write manifest ----------
|
|
250
|
-
console.log('');
|
|
251
272
|
const json = serializeManifest(mergeResult.manifest);
|
|
252
273
|
if (opts.dryRun) {
|
|
253
|
-
|
|
254
|
-
console.log('');
|
|
274
|
+
logger_js_1.logger.info('Dry run — proposed manifest:');
|
|
255
275
|
console.log(json);
|
|
256
276
|
}
|
|
257
277
|
else {
|
|
@@ -262,28 +282,28 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
262
282
|
const tmpPath = `${opts.outputPath}.tmp`;
|
|
263
283
|
(0, fs_1.writeFileSync)(tmpPath, json, 'utf-8');
|
|
264
284
|
(0, fs_1.renameSync)(tmpPath, opts.outputPath);
|
|
265
|
-
|
|
266
|
-
|
|
285
|
+
logger_js_1.logger.info(`Wrote ${opts.outputPath}`);
|
|
286
|
+
logger_js_1.logger.info(`${mergeResult.manifest.families.length} families`);
|
|
267
287
|
}
|
|
268
288
|
// ---------- Phase 6: Report unmatched ----------
|
|
269
289
|
if (scanResult.unmatchedSourceDirs.length > 0 || scanResult.unmatchedTestDirs.length > 0) {
|
|
270
|
-
|
|
271
|
-
console.log(' Unmatched (review manually):');
|
|
290
|
+
logger_js_1.logger.info('Unmatched (review manually):');
|
|
272
291
|
for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
|
|
273
|
-
|
|
292
|
+
logger_js_1.logger.info(` source: ${dir.relativePath}`);
|
|
274
293
|
}
|
|
275
294
|
for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
|
|
276
|
-
|
|
295
|
+
logger_js_1.logger.info(` test: ${dir.relativePath}`);
|
|
277
296
|
}
|
|
278
297
|
if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
|
|
279
|
-
|
|
298
|
+
logger_js_1.logger.info(' ... and more');
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
// ---------- Phase 7: Validation (optional) ----------
|
|
302
|
+
let validationReport;
|
|
283
303
|
if (opts.validate) {
|
|
304
|
+
const validateTimer = logger_js_1.logger.timer('validate');
|
|
284
305
|
if (opts.pr) {
|
|
285
|
-
|
|
286
|
-
console.log(` Validating against PR #${opts.pr}...`);
|
|
306
|
+
logger_js_1.logger.info(`Validating against PR #${opts.pr}...`);
|
|
287
307
|
// Check for gh CLI
|
|
288
308
|
const { execFileSync } = await import('child_process');
|
|
289
309
|
try {
|
|
@@ -306,29 +326,57 @@ async function runTrainCommand(args, autoConfig) {
|
|
|
306
326
|
throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
|
|
307
327
|
}
|
|
308
328
|
if (prFiles.length === 0) {
|
|
309
|
-
|
|
329
|
+
logger_js_1.logger.info('No files found in PR.');
|
|
310
330
|
}
|
|
311
331
|
else {
|
|
312
332
|
const validation = (0, validator_js_1.validateCommit)(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
console.log((0, validator_js_1.formatValidationReport)(report));
|
|
333
|
+
validationReport = (0, validator_js_1.buildValidationReport)([validation], mergeResult.manifest);
|
|
334
|
+
logger_js_1.logger.info((0, validator_js_1.formatValidationReport)(validationReport));
|
|
316
335
|
}
|
|
317
336
|
}
|
|
318
337
|
else {
|
|
319
|
-
|
|
320
|
-
console.log(` Validating against git history (${opts.since})...`);
|
|
338
|
+
logger_js_1.logger.info(`Validating against git history (${opts.since})...`);
|
|
321
339
|
const commits = (0, validator_js_1.getCommitFiles)(opts.gitRepoRoot || opts.appPath, opts.since);
|
|
322
340
|
if (commits.length === 0) {
|
|
323
|
-
|
|
341
|
+
logger_js_1.logger.info('No commits found in range.');
|
|
324
342
|
}
|
|
325
343
|
else {
|
|
326
344
|
const validations = commits.map((c) => (0, validator_js_1.validateCommit)(mergeResult.manifest, c.files, c.hash, c.message));
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
console.log((0, validator_js_1.formatValidationReport)(report));
|
|
345
|
+
validationReport = (0, validator_js_1.buildValidationReport)(validations, mergeResult.manifest);
|
|
346
|
+
logger_js_1.logger.info((0, validator_js_1.formatValidationReport)(validationReport));
|
|
330
347
|
}
|
|
331
348
|
}
|
|
349
|
+
timings.validate = validateTimer.end();
|
|
350
|
+
}
|
|
351
|
+
timings.total = totalTimer.end();
|
|
352
|
+
// ---------- Write train report ----------
|
|
353
|
+
if (!opts.dryRun) {
|
|
354
|
+
const reportDir = (0, path_1.dirname)(opts.outputPath);
|
|
355
|
+
const trainReport = {
|
|
356
|
+
timestamp: new Date().toISOString(),
|
|
357
|
+
version: '1.7.0',
|
|
358
|
+
timings,
|
|
359
|
+
families: {
|
|
360
|
+
total: mergeResult.manifest.families.length,
|
|
361
|
+
new: mergeResult.newFamilies.length,
|
|
362
|
+
updated: mergeResult.updatedFamilies.length,
|
|
363
|
+
stale: mergeResult.staleFamilies.length,
|
|
364
|
+
},
|
|
365
|
+
coverage: validationReport ? {
|
|
366
|
+
percent: validationReport.coveragePercent,
|
|
367
|
+
boundFiles: validationReport.boundFiles,
|
|
368
|
+
totalFiles: validationReport.totalFiles,
|
|
369
|
+
} : undefined,
|
|
370
|
+
llm: opts.enrich ? {
|
|
371
|
+
tokensUsed: enrichTokens,
|
|
372
|
+
costUSD: enrichCost,
|
|
373
|
+
requests: enrichRequests,
|
|
374
|
+
avgResponseMs: enrichAvgResponseMs,
|
|
375
|
+
} : undefined,
|
|
376
|
+
};
|
|
377
|
+
const reportPath = (0, path_1.join)(reportDir, 'train-report.json');
|
|
378
|
+
(0, fs_1.writeFileSync)(reportPath, JSON.stringify(trainReport, null, 2) + '\n', 'utf-8');
|
|
379
|
+
logger_js_1.logger.debug('Wrote train report', { path: reportPath });
|
|
332
380
|
}
|
|
333
|
-
|
|
381
|
+
logger_js_1.logger.info('Done.');
|
|
334
382
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;
|
|
1
|
+
{"version":3,"file":"parse_args.d.ts","sourceRoot":"","sources":["../../src/cli/parse_args.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAU,UAAU,EAAC,MAAM,YAAY,CAAC;AAEpD,eAAO,MAAM,iBAAiB,UAA8D,CAAC;AAE7F,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAmBlF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,GAAG,SAAS,CAmBtE;AA8ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EpD"}
|
package/dist/cli/parse_args.js
CHANGED
|
@@ -71,6 +71,8 @@ const FLAGS = {
|
|
|
71
71
|
'--no-enrich': { key: 'trainEnrich', type: 'boolean-false' },
|
|
72
72
|
'--validate': { key: 'trainValidate', type: 'boolean' },
|
|
73
73
|
'--yes': { key: 'trainYes', type: 'boolean', aliases: ['-y'] },
|
|
74
|
+
'--verbose': { key: 'verbose', type: 'boolean', aliases: ['-v'] },
|
|
75
|
+
'--json': { key: 'jsonOutput', type: 'boolean' },
|
|
74
76
|
'--mattermost': { key: 'profile', type: 'boolean', transform: () => 'mattermost' },
|
|
75
77
|
// -- string flags --
|
|
76
78
|
'--config': { key: 'configPath', type: 'string' },
|
package/dist/cli/types.d.ts
CHANGED
package/dist/cli/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/cli/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -7,6 +7,7 @@ import * as readline from 'readline';
|
|
|
7
7
|
import { resolveConfig } from '../../agent/config.js';
|
|
8
8
|
import { loadRouteFamilyManifest } from '../../knowledge/route_families.js';
|
|
9
9
|
import { LLMProviderFactory } from '../../provider_factory.js';
|
|
10
|
+
import { logger, LogLevel } from '../../logger.js';
|
|
10
11
|
import { scanProject } from '../../training/scanner.js';
|
|
11
12
|
import { mergeFamilies, detectStaleFamilies } from '../../training/merger.js';
|
|
12
13
|
import { enrichFamilies } from '../../training/enricher.js';
|
|
@@ -140,40 +141,47 @@ function serializeManifest(manifest) {
|
|
|
140
141
|
}
|
|
141
142
|
export async function runTrainCommand(args, autoConfig) {
|
|
142
143
|
const opts = resolveTrainOptions(args, autoConfig);
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
const totalTimer = logger.timer('train-total');
|
|
145
|
+
const timings = {};
|
|
146
|
+
// Configure observability from CLI flags
|
|
147
|
+
if (args.verbose)
|
|
148
|
+
logger.setLevel(LogLevel.DEBUG);
|
|
149
|
+
if (args.jsonOutput)
|
|
150
|
+
logger.setJsonMode(true);
|
|
151
|
+
logger.info('e2e-ai-agents train');
|
|
152
|
+
logger.info('===================');
|
|
147
153
|
// ---------- Phase 1: Deterministic scan ----------
|
|
148
|
-
|
|
154
|
+
logger.info('Scanning project structure...');
|
|
149
155
|
if (opts.serverRoot) {
|
|
150
|
-
|
|
156
|
+
logger.info(`Server root: ${opts.serverRoot}`);
|
|
151
157
|
}
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const scanTimer = logger.timer('scan');
|
|
159
|
+
const scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot, opts.gitRepoRoot);
|
|
160
|
+
timings.scan = scanTimer.end();
|
|
161
|
+
logger.info(`Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
|
|
162
|
+
logger.info(`Discovered ${scanResult.families.length} candidate families`);
|
|
155
163
|
if (scanResult.families.length === 0) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
console.log(' (tests/, e2e/, specs/) with matching names.');
|
|
164
|
+
logger.info('No families discovered. Make sure your project has recognizable');
|
|
165
|
+
logger.info('source directories (src/, server/, app/) and test directories');
|
|
166
|
+
logger.info('(tests/, e2e/, specs/) with matching names.');
|
|
160
167
|
return;
|
|
161
168
|
}
|
|
162
169
|
// ---------- Phase 2: Merge with existing ----------
|
|
170
|
+
const mergeTimer = logger.timer('merge');
|
|
163
171
|
const existing = loadRouteFamilyManifest(opts.testsRoot);
|
|
164
172
|
if (existing) {
|
|
165
|
-
|
|
173
|
+
logger.info(`Found existing manifest with ${existing.families.length} families`);
|
|
166
174
|
}
|
|
167
175
|
let mergeResult = mergeFamilies(existing, scanResult.families);
|
|
168
|
-
|
|
176
|
+
timings.merge = mergeTimer.end();
|
|
177
|
+
logger.info(`Merge: ${mergeResult.summary}`);
|
|
169
178
|
// ---------- Phase 3: Stale detection ----------
|
|
170
179
|
if (mergeResult.manifest.families.length > 0) {
|
|
171
180
|
const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath, opts.testsRoot);
|
|
172
181
|
if (stale.length > 0) {
|
|
173
|
-
|
|
174
|
-
console.log(` Stale families detected (${stale.length}):`);
|
|
182
|
+
logger.info(`Stale families detected (${stale.length}):`);
|
|
175
183
|
for (const id of stale) {
|
|
176
|
-
|
|
184
|
+
logger.info(` ${id} — paths no longer exist`);
|
|
177
185
|
}
|
|
178
186
|
if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
|
|
179
187
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -183,7 +191,7 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
183
191
|
const staleSet = new Set(stale);
|
|
184
192
|
mergeResult.manifest.families = mergeResult.manifest.families.filter((f) => !staleSet.has(f.id));
|
|
185
193
|
mergeResult.staleFamilies = stale;
|
|
186
|
-
|
|
194
|
+
logger.info(`Removed ${stale.length} stale families`);
|
|
187
195
|
}
|
|
188
196
|
}
|
|
189
197
|
finally {
|
|
@@ -193,29 +201,41 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
// ---------- Phase 4: LLM Enrichment ----------
|
|
204
|
+
let enrichTokens = 0;
|
|
205
|
+
let enrichCost = 0;
|
|
206
|
+
let enrichRequests = 0;
|
|
207
|
+
let enrichAvgResponseMs = 0;
|
|
196
208
|
if (opts.enrich) {
|
|
197
|
-
|
|
198
|
-
|
|
209
|
+
logger.info('Enriching with LLM...');
|
|
210
|
+
const enrichTimer = logger.timer('enrich');
|
|
199
211
|
try {
|
|
200
212
|
const provider = await LLMProviderFactory.createFromEnv();
|
|
201
213
|
const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
|
|
202
214
|
mergeResult.manifest.families = enrichResult.enrichedFamilies;
|
|
203
|
-
|
|
215
|
+
enrichTokens = enrichResult.tokensUsed;
|
|
216
|
+
enrichCost = enrichResult.costUSD;
|
|
217
|
+
enrichRequests = enrichResult.requestCount ?? 0;
|
|
218
|
+
enrichAvgResponseMs = enrichResult.avgResponseMs ?? 0;
|
|
219
|
+
logger.info(`Enriched ${enrichResult.enrichedFamilies.length} families`, {
|
|
220
|
+
tokens: enrichResult.tokensUsed,
|
|
221
|
+
cost: enrichResult.costUSD,
|
|
222
|
+
requests: enrichRequests,
|
|
223
|
+
avgResponseMs: enrichAvgResponseMs,
|
|
224
|
+
});
|
|
204
225
|
if (enrichResult.skippedFamilies.length > 0) {
|
|
205
|
-
|
|
226
|
+
logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
|
|
206
227
|
}
|
|
207
228
|
}
|
|
208
229
|
catch (error) {
|
|
209
|
-
|
|
210
|
-
|
|
230
|
+
logger.warn(`LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
231
|
+
logger.warn('Continuing with deterministic results. Use --no-enrich to skip LLM.');
|
|
211
232
|
}
|
|
233
|
+
timings.enrich = enrichTimer.end();
|
|
212
234
|
}
|
|
213
235
|
// ---------- Phase 5: Write manifest ----------
|
|
214
|
-
console.log('');
|
|
215
236
|
const json = serializeManifest(mergeResult.manifest);
|
|
216
237
|
if (opts.dryRun) {
|
|
217
|
-
|
|
218
|
-
console.log('');
|
|
238
|
+
logger.info('Dry run — proposed manifest:');
|
|
219
239
|
console.log(json);
|
|
220
240
|
}
|
|
221
241
|
else {
|
|
@@ -226,28 +246,28 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
226
246
|
const tmpPath = `${opts.outputPath}.tmp`;
|
|
227
247
|
writeFileSync(tmpPath, json, 'utf-8');
|
|
228
248
|
renameSync(tmpPath, opts.outputPath);
|
|
229
|
-
|
|
230
|
-
|
|
249
|
+
logger.info(`Wrote ${opts.outputPath}`);
|
|
250
|
+
logger.info(`${mergeResult.manifest.families.length} families`);
|
|
231
251
|
}
|
|
232
252
|
// ---------- Phase 6: Report unmatched ----------
|
|
233
253
|
if (scanResult.unmatchedSourceDirs.length > 0 || scanResult.unmatchedTestDirs.length > 0) {
|
|
234
|
-
|
|
235
|
-
console.log(' Unmatched (review manually):');
|
|
254
|
+
logger.info('Unmatched (review manually):');
|
|
236
255
|
for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
|
|
237
|
-
|
|
256
|
+
logger.info(` source: ${dir.relativePath}`);
|
|
238
257
|
}
|
|
239
258
|
for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
|
|
240
|
-
|
|
259
|
+
logger.info(` test: ${dir.relativePath}`);
|
|
241
260
|
}
|
|
242
261
|
if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
|
|
243
|
-
|
|
262
|
+
logger.info(' ... and more');
|
|
244
263
|
}
|
|
245
264
|
}
|
|
246
265
|
// ---------- Phase 7: Validation (optional) ----------
|
|
266
|
+
let validationReport;
|
|
247
267
|
if (opts.validate) {
|
|
268
|
+
const validateTimer = logger.timer('validate');
|
|
248
269
|
if (opts.pr) {
|
|
249
|
-
|
|
250
|
-
console.log(` Validating against PR #${opts.pr}...`);
|
|
270
|
+
logger.info(`Validating against PR #${opts.pr}...`);
|
|
251
271
|
// Check for gh CLI
|
|
252
272
|
const { execFileSync } = await import('child_process');
|
|
253
273
|
try {
|
|
@@ -270,29 +290,57 @@ export async function runTrainCommand(args, autoConfig) {
|
|
|
270
290
|
throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
|
|
271
291
|
}
|
|
272
292
|
if (prFiles.length === 0) {
|
|
273
|
-
|
|
293
|
+
logger.info('No files found in PR.');
|
|
274
294
|
}
|
|
275
295
|
else {
|
|
276
296
|
const validation = validateCommit(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
console.log(formatValidationReport(report));
|
|
297
|
+
validationReport = buildValidationReport([validation], mergeResult.manifest);
|
|
298
|
+
logger.info(formatValidationReport(validationReport));
|
|
280
299
|
}
|
|
281
300
|
}
|
|
282
301
|
else {
|
|
283
|
-
|
|
284
|
-
console.log(` Validating against git history (${opts.since})...`);
|
|
302
|
+
logger.info(`Validating against git history (${opts.since})...`);
|
|
285
303
|
const commits = getCommitFiles(opts.gitRepoRoot || opts.appPath, opts.since);
|
|
286
304
|
if (commits.length === 0) {
|
|
287
|
-
|
|
305
|
+
logger.info('No commits found in range.');
|
|
288
306
|
}
|
|
289
307
|
else {
|
|
290
308
|
const validations = commits.map((c) => validateCommit(mergeResult.manifest, c.files, c.hash, c.message));
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
console.log(formatValidationReport(report));
|
|
309
|
+
validationReport = buildValidationReport(validations, mergeResult.manifest);
|
|
310
|
+
logger.info(formatValidationReport(validationReport));
|
|
294
311
|
}
|
|
295
312
|
}
|
|
313
|
+
timings.validate = validateTimer.end();
|
|
314
|
+
}
|
|
315
|
+
timings.total = totalTimer.end();
|
|
316
|
+
// ---------- Write train report ----------
|
|
317
|
+
if (!opts.dryRun) {
|
|
318
|
+
const reportDir = dirname(opts.outputPath);
|
|
319
|
+
const trainReport = {
|
|
320
|
+
timestamp: new Date().toISOString(),
|
|
321
|
+
version: '1.7.0',
|
|
322
|
+
timings,
|
|
323
|
+
families: {
|
|
324
|
+
total: mergeResult.manifest.families.length,
|
|
325
|
+
new: mergeResult.newFamilies.length,
|
|
326
|
+
updated: mergeResult.updatedFamilies.length,
|
|
327
|
+
stale: mergeResult.staleFamilies.length,
|
|
328
|
+
},
|
|
329
|
+
coverage: validationReport ? {
|
|
330
|
+
percent: validationReport.coveragePercent,
|
|
331
|
+
boundFiles: validationReport.boundFiles,
|
|
332
|
+
totalFiles: validationReport.totalFiles,
|
|
333
|
+
} : undefined,
|
|
334
|
+
llm: opts.enrich ? {
|
|
335
|
+
tokensUsed: enrichTokens,
|
|
336
|
+
costUSD: enrichCost,
|
|
337
|
+
requests: enrichRequests,
|
|
338
|
+
avgResponseMs: enrichAvgResponseMs,
|
|
339
|
+
} : undefined,
|
|
340
|
+
};
|
|
341
|
+
const reportPath = join(reportDir, 'train-report.json');
|
|
342
|
+
writeFileSync(reportPath, JSON.stringify(trainReport, null, 2) + '\n', 'utf-8');
|
|
343
|
+
logger.debug('Wrote train report', { path: reportPath });
|
|
296
344
|
}
|
|
297
|
-
|
|
345
|
+
logger.info('Done.');
|
|
298
346
|
}
|
|
@@ -65,6 +65,8 @@ const FLAGS = {
|
|
|
65
65
|
'--no-enrich': { key: 'trainEnrich', type: 'boolean-false' },
|
|
66
66
|
'--validate': { key: 'trainValidate', type: 'boolean' },
|
|
67
67
|
'--yes': { key: 'trainYes', type: 'boolean', aliases: ['-y'] },
|
|
68
|
+
'--verbose': { key: 'verbose', type: 'boolean', aliases: ['-v'] },
|
|
69
|
+
'--json': { key: 'jsonOutput', type: 'boolean' },
|
|
68
70
|
'--mattermost': { key: 'profile', type: 'boolean', transform: () => 'mattermost' },
|
|
69
71
|
// -- string flags --
|
|
70
72
|
'--config': { key: 'configPath', type: 'string' },
|
package/dist/esm/logger.js
CHANGED
|
@@ -48,6 +48,7 @@ function logLevelToString(level) {
|
|
|
48
48
|
export class Logger {
|
|
49
49
|
constructor(minLevel) {
|
|
50
50
|
this.level = minLevel ?? getLogLevelFromEnv();
|
|
51
|
+
this.jsonMode = process.env.LOG_FORMAT?.toLowerCase() === 'json';
|
|
51
52
|
}
|
|
52
53
|
error(message, context) {
|
|
53
54
|
if (this.level >= LogLevel.ERROR) {
|
|
@@ -72,11 +73,37 @@ export class Logger {
|
|
|
72
73
|
setLevel(level) {
|
|
73
74
|
this.level = level;
|
|
74
75
|
}
|
|
76
|
+
setJsonMode(enabled) {
|
|
77
|
+
this.jsonMode = enabled;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Start a timer for measuring duration of an operation.
|
|
81
|
+
* Returns an object with `end()` that logs at DEBUG level and returns elapsed ms.
|
|
82
|
+
*/
|
|
83
|
+
timer(label) {
|
|
84
|
+
const start = performance.now();
|
|
85
|
+
return {
|
|
86
|
+
end: () => {
|
|
87
|
+
const elapsed = Math.round(performance.now() - start);
|
|
88
|
+
this.debug(`${label} completed`, { durationMs: elapsed });
|
|
89
|
+
return elapsed;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
75
93
|
log(level, message, context) {
|
|
76
94
|
const timestamp = new Date().toISOString();
|
|
77
95
|
const levelStr = logLevelToString(level);
|
|
78
|
-
|
|
79
|
-
|
|
96
|
+
let output;
|
|
97
|
+
if (this.jsonMode) {
|
|
98
|
+
const entry = { ts: timestamp, level: levelStr, msg: message };
|
|
99
|
+
if (context)
|
|
100
|
+
entry.ctx = context;
|
|
101
|
+
output = JSON.stringify(entry);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const contextStr = context ? ` ${JSON.stringify(context)}` : '';
|
|
105
|
+
output = `[${timestamp}] [${levelStr}] ${message}${contextStr}`;
|
|
106
|
+
}
|
|
80
107
|
if (level <= LogLevel.WARN) {
|
|
81
108
|
console.error(output);
|
|
82
109
|
}
|