@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.
@@ -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,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
- 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,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
- 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');
284
305
  if (opts.pr) {
285
- console.log('');
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
- console.log(' No files found in PR.');
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
- const report = (0, validator_js_1.buildValidationReport)([validation], mergeResult.manifest);
314
- console.log('');
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
- console.log('');
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
- console.log(' No commits found in range.');
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
- const report = (0, validator_js_1.buildValidationReport)(validations, mergeResult.manifest);
328
- console.log('');
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
- console.log('');
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;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":"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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
- 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,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
- 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');
248
269
  if (opts.pr) {
249
- console.log('');
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
- console.log(' No files found in PR.');
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
- const report = buildValidationReport([validation], mergeResult.manifest);
278
- console.log('');
279
- console.log(formatValidationReport(report));
297
+ validationReport = buildValidationReport([validation], mergeResult.manifest);
298
+ logger.info(formatValidationReport(validationReport));
280
299
  }
281
300
  }
282
301
  else {
283
- console.log('');
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
- console.log(' No commits found in range.');
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
- const report = buildValidationReport(validations, mergeResult.manifest);
292
- console.log('');
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
- console.log('');
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' },
@@ -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
  }