@yasserkhanorg/e2e-agents 1.6.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAaA,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,CAqL1F"}
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,CAsP1F"}
@@ -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
- console.log('');
180
- console.log(' e2e-ai-agents train');
181
- console.log(' ===================');
182
- console.log('');
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
- console.log(' Scanning project structure...');
190
+ logger_js_1.logger.info('Scanning project structure...');
185
191
  if (opts.serverRoot) {
186
- console.log(` Server root: ${opts.serverRoot}`);
192
+ logger_js_1.logger.info(`Server root: ${opts.serverRoot}`);
187
193
  }
188
- const scanResult = (0, scanner_js_1.scanProject)(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
189
- console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
190
- console.log(` Discovered ${scanResult.families.length} candidate families`);
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
- console.log('');
193
- console.log(' No families discovered. Make sure your project has recognizable');
194
- console.log(' source directories (src/, server/, app/) and test directories');
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
- console.log(` Found existing manifest with ${existing.families.length} families`);
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
- console.log(` Merge: ${mergeResult.summary}`);
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
- console.log('');
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
- console.log(` ${id} — paths no longer exist`);
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
- console.log(` Removed ${stale.length} stale families`);
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
- console.log('');
234
- console.log(' Enriching with LLM...');
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
- console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
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
- console.log(` Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
262
+ logger_js_1.logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
242
263
  }
243
264
  }
244
265
  catch (error) {
245
- console.warn(` LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
246
- console.warn(' Continuing with deterministic results. Use --no-enrich to skip LLM.');
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
- console.log(' Dry run — proposed manifest:');
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,42 @@ 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
- console.log(` Wrote ${opts.outputPath}`);
266
- console.log(` ${mergeResult.manifest.families.length} families`);
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
- console.log('');
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
- console.log(` source: ${dir.relativePath}`);
292
+ logger_js_1.logger.info(` source: ${dir.relativePath}`);
274
293
  }
275
294
  for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
276
- console.log(` test: ${dir.relativePath}`);
295
+ logger_js_1.logger.info(` test: ${dir.relativePath}`);
277
296
  }
278
297
  if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
279
- console.log(' ... and more');
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');
305
+ // Build path prefixes for monorepo-aware path normalization.
306
+ // Git log returns repo-root-relative paths, but manifest globs are
307
+ // relative to appPath, testsRoot, or serverRoot.
308
+ const pathPrefixes = [];
309
+ if (opts.gitRepoRoot) {
310
+ const { relative: relPath } = await import('path');
311
+ for (const root of [opts.appPath, opts.testsRoot, opts.serverRoot].filter(Boolean)) {
312
+ const rel = relPath(opts.gitRepoRoot, root).replace(/\\/g, '/');
313
+ if (rel && !rel.startsWith('..') && rel !== '.') {
314
+ pathPrefixes.push(rel.endsWith('/') ? rel : rel + '/');
315
+ }
316
+ }
317
+ }
318
+ logger_js_1.logger.debug('Validation path prefixes', { prefixes: pathPrefixes });
284
319
  if (opts.pr) {
285
- console.log('');
286
- console.log(` Validating against PR #${opts.pr}...`);
320
+ logger_js_1.logger.info(`Validating against PR #${opts.pr}...`);
287
321
  // Check for gh CLI
288
322
  const { execFileSync } = await import('child_process');
289
323
  try {
@@ -306,29 +340,57 @@ async function runTrainCommand(args, autoConfig) {
306
340
  throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
307
341
  }
308
342
  if (prFiles.length === 0) {
309
- console.log(' No files found in PR.');
343
+ logger_js_1.logger.info('No files found in PR.');
310
344
  }
311
345
  else {
312
- const validation = (0, validator_js_1.validateCommit)(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
313
- const report = (0, validator_js_1.buildValidationReport)([validation], mergeResult.manifest);
314
- console.log('');
315
- console.log((0, validator_js_1.formatValidationReport)(report));
346
+ const validation = (0, validator_js_1.validateCommit)(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`, pathPrefixes);
347
+ validationReport = (0, validator_js_1.buildValidationReport)([validation], mergeResult.manifest);
348
+ logger_js_1.logger.info((0, validator_js_1.formatValidationReport)(validationReport));
316
349
  }
317
350
  }
318
351
  else {
319
- console.log('');
320
- console.log(` Validating against git history (${opts.since})...`);
352
+ logger_js_1.logger.info(`Validating against git history (${opts.since})...`);
321
353
  const commits = (0, validator_js_1.getCommitFiles)(opts.gitRepoRoot || opts.appPath, opts.since);
322
354
  if (commits.length === 0) {
323
- console.log(' No commits found in range.');
355
+ logger_js_1.logger.info('No commits found in range.');
324
356
  }
325
357
  else {
326
- const validations = commits.map((c) => (0, validator_js_1.validateCommit)(mergeResult.manifest, c.files, c.hash, c.message));
327
- const report = (0, validator_js_1.buildValidationReport)(validations, mergeResult.manifest);
328
- console.log('');
329
- console.log((0, validator_js_1.formatValidationReport)(report));
358
+ const validations = commits.map((c) => (0, validator_js_1.validateCommit)(mergeResult.manifest, c.files, c.hash, c.message, pathPrefixes));
359
+ validationReport = (0, validator_js_1.buildValidationReport)(validations, mergeResult.manifest);
360
+ logger_js_1.logger.info((0, validator_js_1.formatValidationReport)(validationReport));
330
361
  }
331
362
  }
363
+ timings.validate = validateTimer.end();
364
+ }
365
+ timings.total = totalTimer.end();
366
+ // ---------- Write train report ----------
367
+ if (!opts.dryRun) {
368
+ const reportDir = (0, path_1.dirname)(opts.outputPath);
369
+ const trainReport = {
370
+ timestamp: new Date().toISOString(),
371
+ version: '1.7.0',
372
+ timings,
373
+ families: {
374
+ total: mergeResult.manifest.families.length,
375
+ new: mergeResult.newFamilies.length,
376
+ updated: mergeResult.updatedFamilies.length,
377
+ stale: mergeResult.staleFamilies.length,
378
+ },
379
+ coverage: validationReport ? {
380
+ percent: validationReport.coveragePercent,
381
+ boundFiles: validationReport.boundFiles,
382
+ totalFiles: validationReport.totalFiles,
383
+ } : undefined,
384
+ llm: opts.enrich ? {
385
+ tokensUsed: enrichTokens,
386
+ costUSD: enrichCost,
387
+ requests: enrichRequests,
388
+ avgResponseMs: enrichAvgResponseMs,
389
+ } : undefined,
390
+ };
391
+ const reportPath = (0, path_1.join)(reportDir, 'train-report.json');
392
+ (0, fs_1.writeFileSync)(reportPath, JSON.stringify(trainReport, null, 2) + '\n', 'utf-8');
393
+ logger_js_1.logger.debug('Wrote train report', { path: reportPath });
332
394
  }
333
- console.log('');
395
+ logger_js_1.logger.info('Done.');
334
396
  }
@@ -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;AA4ID,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,CA4EpD"}
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"}
@@ -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' },
@@ -72,5 +72,7 @@ export interface ParsedArgs {
72
72
  trainOutput?: string;
73
73
  trainYes?: boolean;
74
74
  serverPath?: string;
75
+ verbose?: boolean;
76
+ jsonOutput?: boolean;
75
77
  }
76
78
  //# sourceMappingURL=types.d.ts.map
@@ -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":"AAGA,OAAO,KAAK,EAAC,eAAe,EAAE,aAAa,EAAC,MAAM,oBAAoB,CAAC;AAEvE,MAAM,MAAM,OAAO,GACf,MAAM,GACJ,QAAQ,GACR,MAAM,GACN,MAAM,GACN,SAAS,GACT,UAAU,GACV,0BAA0B,GAC1B,UAAU,GACV,sBAAsB,GACtB,qBAAqB,GACrB,SAAS,GACT,YAAY,GACZ,OAAO,CAAC;AAEd,MAAM,WAAW,UAAU;IACvB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;IAC/D,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,qBAAqB,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,OAAO,CAAC;IACtD,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,GAAG,gBAAgB,GAAG,eAAe,CAAC,CAAC;IAC3E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,4BAA4B,CAAC,EAAE,MAAM,CAAC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,2BAA2B,CAAC,EAAE,MAAM,CAAC;IACrC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAG3B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IAGpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,OAAO,CAAC;CACxB"}
@@ -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
- console.log('');
144
- console.log(' e2e-ai-agents train');
145
- console.log(' ===================');
146
- console.log('');
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
- console.log(' Scanning project structure...');
154
+ logger.info('Scanning project structure...');
149
155
  if (opts.serverRoot) {
150
- console.log(` Server root: ${opts.serverRoot}`);
156
+ logger.info(`Server root: ${opts.serverRoot}`);
151
157
  }
152
- const scanResult = scanProject(opts.appPath, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined, opts.serverRoot);
153
- console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
154
- console.log(` Discovered ${scanResult.families.length} candidate families`);
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
- console.log('');
157
- console.log(' No families discovered. Make sure your project has recognizable');
158
- console.log(' source directories (src/, server/, app/) and test directories');
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
- console.log(` Found existing manifest with ${existing.families.length} families`);
173
+ logger.info(`Found existing manifest with ${existing.families.length} families`);
166
174
  }
167
175
  let mergeResult = mergeFamilies(existing, scanResult.families);
168
- console.log(` Merge: ${mergeResult.summary}`);
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
- console.log('');
174
- console.log(` Stale families detected (${stale.length}):`);
182
+ logger.info(`Stale families detected (${stale.length}):`);
175
183
  for (const id of stale) {
176
- console.log(` ${id} — paths no longer exist`);
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
- console.log(` Removed ${stale.length} stale families`);
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
- console.log('');
198
- console.log(' Enriching with LLM...');
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
- console.log(` Enriched ${enrichResult.enrichedFamilies.length} families (${enrichResult.tokensUsed} tokens, ~$${enrichResult.costUSD})`);
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
- console.log(` Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
226
+ logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
206
227
  }
207
228
  }
208
229
  catch (error) {
209
- console.warn(` LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
210
- console.warn(' Continuing with deterministic results. Use --no-enrich to skip LLM.');
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
- console.log(' Dry run — proposed manifest:');
218
- console.log('');
238
+ logger.info('Dry run — proposed manifest:');
219
239
  console.log(json);
220
240
  }
221
241
  else {
@@ -226,28 +246,42 @@ 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
- console.log(` Wrote ${opts.outputPath}`);
230
- console.log(` ${mergeResult.manifest.families.length} families`);
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
- console.log('');
235
- console.log(' Unmatched (review manually):');
254
+ logger.info('Unmatched (review manually):');
236
255
  for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
237
- console.log(` source: ${dir.relativePath}`);
256
+ logger.info(` source: ${dir.relativePath}`);
238
257
  }
239
258
  for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
240
- console.log(` test: ${dir.relativePath}`);
259
+ logger.info(` test: ${dir.relativePath}`);
241
260
  }
242
261
  if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
243
- console.log(' ... and more');
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');
269
+ // Build path prefixes for monorepo-aware path normalization.
270
+ // Git log returns repo-root-relative paths, but manifest globs are
271
+ // relative to appPath, testsRoot, or serverRoot.
272
+ const pathPrefixes = [];
273
+ if (opts.gitRepoRoot) {
274
+ const { relative: relPath } = await import('path');
275
+ for (const root of [opts.appPath, opts.testsRoot, opts.serverRoot].filter(Boolean)) {
276
+ const rel = relPath(opts.gitRepoRoot, root).replace(/\\/g, '/');
277
+ if (rel && !rel.startsWith('..') && rel !== '.') {
278
+ pathPrefixes.push(rel.endsWith('/') ? rel : rel + '/');
279
+ }
280
+ }
281
+ }
282
+ logger.debug('Validation path prefixes', { prefixes: pathPrefixes });
248
283
  if (opts.pr) {
249
- console.log('');
250
- console.log(` Validating against PR #${opts.pr}...`);
284
+ logger.info(`Validating against PR #${opts.pr}...`);
251
285
  // Check for gh CLI
252
286
  const { execFileSync } = await import('child_process');
253
287
  try {
@@ -270,29 +304,57 @@ export async function runTrainCommand(args, autoConfig) {
270
304
  throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
271
305
  }
272
306
  if (prFiles.length === 0) {
273
- console.log(' No files found in PR.');
307
+ logger.info('No files found in PR.');
274
308
  }
275
309
  else {
276
- const validation = validateCommit(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
277
- const report = buildValidationReport([validation], mergeResult.manifest);
278
- console.log('');
279
- console.log(formatValidationReport(report));
310
+ const validation = validateCommit(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`, pathPrefixes);
311
+ validationReport = buildValidationReport([validation], mergeResult.manifest);
312
+ logger.info(formatValidationReport(validationReport));
280
313
  }
281
314
  }
282
315
  else {
283
- console.log('');
284
- console.log(` Validating against git history (${opts.since})...`);
316
+ logger.info(`Validating against git history (${opts.since})...`);
285
317
  const commits = getCommitFiles(opts.gitRepoRoot || opts.appPath, opts.since);
286
318
  if (commits.length === 0) {
287
- console.log(' No commits found in range.');
319
+ logger.info('No commits found in range.');
288
320
  }
289
321
  else {
290
- const validations = commits.map((c) => validateCommit(mergeResult.manifest, c.files, c.hash, c.message));
291
- const report = buildValidationReport(validations, mergeResult.manifest);
292
- console.log('');
293
- console.log(formatValidationReport(report));
322
+ const validations = commits.map((c) => validateCommit(mergeResult.manifest, c.files, c.hash, c.message, pathPrefixes));
323
+ validationReport = buildValidationReport(validations, mergeResult.manifest);
324
+ logger.info(formatValidationReport(validationReport));
294
325
  }
295
326
  }
327
+ timings.validate = validateTimer.end();
328
+ }
329
+ timings.total = totalTimer.end();
330
+ // ---------- Write train report ----------
331
+ if (!opts.dryRun) {
332
+ const reportDir = dirname(opts.outputPath);
333
+ const trainReport = {
334
+ timestamp: new Date().toISOString(),
335
+ version: '1.7.0',
336
+ timings,
337
+ families: {
338
+ total: mergeResult.manifest.families.length,
339
+ new: mergeResult.newFamilies.length,
340
+ updated: mergeResult.updatedFamilies.length,
341
+ stale: mergeResult.staleFamilies.length,
342
+ },
343
+ coverage: validationReport ? {
344
+ percent: validationReport.coveragePercent,
345
+ boundFiles: validationReport.boundFiles,
346
+ totalFiles: validationReport.totalFiles,
347
+ } : undefined,
348
+ llm: opts.enrich ? {
349
+ tokensUsed: enrichTokens,
350
+ costUSD: enrichCost,
351
+ requests: enrichRequests,
352
+ avgResponseMs: enrichAvgResponseMs,
353
+ } : undefined,
354
+ };
355
+ const reportPath = join(reportDir, 'train-report.json');
356
+ writeFileSync(reportPath, JSON.stringify(trainReport, null, 2) + '\n', 'utf-8');
357
+ logger.debug('Wrote train report', { path: reportPath });
296
358
  }
297
- console.log('');
359
+ logger.info('Done.');
298
360
  }
@@ -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' },
@@ -171,6 +171,7 @@ export function bindFilesToFamilies(changedFiles, manifest) {
171
171
  const featurePatterns = [
172
172
  ...(feature.webappPaths || []),
173
173
  ...(feature.serverPaths || []),
174
+ ...(feature.specDirs || []),
174
175
  ];
175
176
  if (featurePatterns.length > 0 && matchesAnyPattern(normalized, featurePatterns)) {
176
177
  featureBindings.push({ family: family.id, feature: feature.id });
@@ -185,6 +186,8 @@ export function bindFilesToFamilies(changedFiles, manifest) {
185
186
  const familyPatterns = [
186
187
  ...(family.webappPaths || []),
187
188
  ...(family.serverPaths || []),
189
+ ...(family.specDirs || []),
190
+ ...(family.cypressSpecDirs || []),
188
191
  ];
189
192
  if (familyPatterns.length > 0 && matchesAnyPattern(normalized, familyPatterns)) {
190
193
  bindings.push({ family: family.id });
@@ -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
- const contextStr = context ? ` ${JSON.stringify(context)}` : '';
79
- const output = `[${timestamp}] [${levelStr}] ${message}${contextStr}`;
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
  }