@yasserkhanorg/e2e-agents 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"train.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/train.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AA6H5C,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA6K1F"}
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"}
@@ -36,12 +36,14 @@ var __importStar = (this && this.__importStar) || (function () {
36
36
  })();
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
38
  exports.runTrainCommand = runTrainCommand;
39
+ const child_process_1 = require("child_process");
39
40
  const fs_1 = require("fs");
40
41
  const path_1 = require("path");
41
42
  const readline = __importStar(require("readline"));
42
43
  const config_js_1 = require("../../agent/config.js");
43
44
  const route_families_js_1 = require("../../knowledge/route_families.js");
44
45
  const provider_factory_js_1 = require("../../provider_factory.js");
46
+ const logger_js_1 = require("../../logger.js");
45
47
  const scanner_js_1 = require("../../training/scanner.js");
46
48
  const merger_js_1 = require("../../training/merger.js");
47
49
  const enricher_js_1 = require("../../training/enricher.js");
@@ -111,9 +113,32 @@ function resolveTrainOptions(args, autoConfig) {
111
113
  if (!inApp && !inTests) {
112
114
  throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
113
115
  }
116
+ // Discover git repo root for monorepo-aware scanning and validation
117
+ let gitRepoRoot;
118
+ try {
119
+ gitRepoRoot = (0, child_process_1.execFileSync)('git', ['rev-parse', '--show-toplevel'], {
120
+ cwd: resolvedAppPath,
121
+ encoding: 'utf-8',
122
+ stdio: ['pipe', 'pipe', 'pipe'],
123
+ }).trim();
124
+ }
125
+ catch {
126
+ // Not a git repo or git not available
127
+ }
128
+ // Resolve serverRoot: explicit flag, or auto-detect from git repo root
129
+ let serverRoot = args.serverPath;
130
+ if (!serverRoot && gitRepoRoot) {
131
+ const serverDir = (0, path_1.join)(gitRepoRoot, 'server');
132
+ if ((0, fs_1.existsSync)(serverDir)) {
133
+ serverRoot = serverDir;
134
+ }
135
+ }
136
+ const resolvedServerRoot = serverRoot ? (0, path_1.resolve)(serverRoot) : undefined;
114
137
  return {
115
138
  appPath: resolvedAppPath,
116
139
  testsRoot: resolvedTestsRoot,
140
+ serverRoot: resolvedServerRoot,
141
+ gitRepoRoot: gitRepoRoot ? (0, path_1.resolve)(gitRepoRoot) : undefined,
117
142
  enrich: args.trainEnrich !== false,
118
143
  validate: args.trainValidate || false,
119
144
  since,
@@ -152,37 +177,47 @@ function serializeManifest(manifest) {
152
177
  }
153
178
  async function runTrainCommand(args, autoConfig) {
154
179
  const opts = resolveTrainOptions(args, autoConfig);
155
- console.log('');
156
- console.log(' e2e-ai-agents train');
157
- console.log(' ===================');
158
- 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('===================');
159
189
  // ---------- Phase 1: Deterministic scan ----------
160
- console.log(' Scanning project structure...');
161
- const scanResult = (0, scanner_js_1.scanProject)(opts.appPath);
162
- console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
163
- console.log(` Discovered ${scanResult.families.length} candidate families`);
190
+ logger_js_1.logger.info('Scanning project structure...');
191
+ if (opts.serverRoot) {
192
+ logger_js_1.logger.info(`Server root: ${opts.serverRoot}`);
193
+ }
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`);
164
199
  if (scanResult.families.length === 0) {
165
- console.log('');
166
- console.log(' No families discovered. Make sure your project has recognizable');
167
- console.log(' source directories (src/, server/, app/) and test directories');
168
- 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.');
169
203
  return;
170
204
  }
171
205
  // ---------- Phase 2: Merge with existing ----------
206
+ const mergeTimer = logger_js_1.logger.timer('merge');
172
207
  const existing = (0, route_families_js_1.loadRouteFamilyManifest)(opts.testsRoot);
173
208
  if (existing) {
174
- console.log(` Found existing manifest with ${existing.families.length} families`);
209
+ logger_js_1.logger.info(`Found existing manifest with ${existing.families.length} families`);
175
210
  }
176
211
  let mergeResult = (0, merger_js_1.mergeFamilies)(existing, scanResult.families);
177
- console.log(` Merge: ${mergeResult.summary}`);
212
+ timings.merge = mergeTimer.end();
213
+ logger_js_1.logger.info(`Merge: ${mergeResult.summary}`);
178
214
  // ---------- Phase 3: Stale detection ----------
179
215
  if (mergeResult.manifest.families.length > 0) {
180
- const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath);
216
+ const stale = (0, merger_js_1.detectStaleFamilies)(mergeResult.manifest, opts.appPath, opts.testsRoot);
181
217
  if (stale.length > 0) {
182
- console.log('');
183
- console.log(` Stale families detected (${stale.length}):`);
218
+ logger_js_1.logger.info(`Stale families detected (${stale.length}):`);
184
219
  for (const id of stale) {
185
- console.log(` ${id} — paths no longer exist`);
220
+ logger_js_1.logger.info(` ${id} — paths no longer exist`);
186
221
  }
187
222
  if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
188
223
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -192,7 +227,7 @@ async function runTrainCommand(args, autoConfig) {
192
227
  const staleSet = new Set(stale);
193
228
  mergeResult.manifest.families = mergeResult.manifest.families.filter((f) => !staleSet.has(f.id));
194
229
  mergeResult.staleFamilies = stale;
195
- console.log(` Removed ${stale.length} stale families`);
230
+ logger_js_1.logger.info(`Removed ${stale.length} stale families`);
196
231
  }
197
232
  }
198
233
  finally {
@@ -202,29 +237,41 @@ async function runTrainCommand(args, autoConfig) {
202
237
  }
203
238
  }
204
239
  // ---------- Phase 4: LLM Enrichment ----------
240
+ let enrichTokens = 0;
241
+ let enrichCost = 0;
242
+ let enrichRequests = 0;
243
+ let enrichAvgResponseMs = 0;
205
244
  if (opts.enrich) {
206
- console.log('');
207
- console.log(' Enriching with LLM...');
245
+ logger_js_1.logger.info('Enriching with LLM...');
246
+ const enrichTimer = logger_js_1.logger.timer('enrich');
208
247
  try {
209
248
  const provider = await provider_factory_js_1.LLMProviderFactory.createFromEnv();
210
- const enrichResult = await (0, enricher_js_1.enrichFamilies)(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
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);
211
250
  mergeResult.manifest.families = enrichResult.enrichedFamilies;
212
- 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
+ });
213
261
  if (enrichResult.skippedFamilies.length > 0) {
214
- console.log(` Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
262
+ logger_js_1.logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
215
263
  }
216
264
  }
217
265
  catch (error) {
218
- console.warn(` LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
219
- 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.');
220
268
  }
269
+ timings.enrich = enrichTimer.end();
221
270
  }
222
271
  // ---------- Phase 5: Write manifest ----------
223
- console.log('');
224
272
  const json = serializeManifest(mergeResult.manifest);
225
273
  if (opts.dryRun) {
226
- console.log(' Dry run — proposed manifest:');
227
- console.log('');
274
+ logger_js_1.logger.info('Dry run — proposed manifest:');
228
275
  console.log(json);
229
276
  }
230
277
  else {
@@ -235,28 +282,28 @@ async function runTrainCommand(args, autoConfig) {
235
282
  const tmpPath = `${opts.outputPath}.tmp`;
236
283
  (0, fs_1.writeFileSync)(tmpPath, json, 'utf-8');
237
284
  (0, fs_1.renameSync)(tmpPath, opts.outputPath);
238
- console.log(` Wrote ${opts.outputPath}`);
239
- 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`);
240
287
  }
241
288
  // ---------- Phase 6: Report unmatched ----------
242
289
  if (scanResult.unmatchedSourceDirs.length > 0 || scanResult.unmatchedTestDirs.length > 0) {
243
- console.log('');
244
- console.log(' Unmatched (review manually):');
290
+ logger_js_1.logger.info('Unmatched (review manually):');
245
291
  for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
246
- console.log(` source: ${dir.relativePath}`);
292
+ logger_js_1.logger.info(` source: ${dir.relativePath}`);
247
293
  }
248
294
  for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
249
- console.log(` test: ${dir.relativePath}`);
295
+ logger_js_1.logger.info(` test: ${dir.relativePath}`);
250
296
  }
251
297
  if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
252
- console.log(' ... and more');
298
+ logger_js_1.logger.info(' ... and more');
253
299
  }
254
300
  }
255
301
  // ---------- Phase 7: Validation (optional) ----------
302
+ let validationReport;
256
303
  if (opts.validate) {
304
+ const validateTimer = logger_js_1.logger.timer('validate');
257
305
  if (opts.pr) {
258
- console.log('');
259
- console.log(` Validating against PR #${opts.pr}...`);
306
+ logger_js_1.logger.info(`Validating against PR #${opts.pr}...`);
260
307
  // Check for gh CLI
261
308
  const { execFileSync } = await import('child_process');
262
309
  try {
@@ -279,29 +326,57 @@ async function runTrainCommand(args, autoConfig) {
279
326
  throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
280
327
  }
281
328
  if (prFiles.length === 0) {
282
- console.log(' No files found in PR.');
329
+ logger_js_1.logger.info('No files found in PR.');
283
330
  }
284
331
  else {
285
332
  const validation = (0, validator_js_1.validateCommit)(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
286
- const report = (0, validator_js_1.buildValidationReport)([validation], mergeResult.manifest);
287
- console.log('');
288
- 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));
289
335
  }
290
336
  }
291
337
  else {
292
- console.log('');
293
- console.log(` Validating against git history (${opts.since})...`);
294
- const commits = (0, validator_js_1.getCommitFiles)(opts.appPath, opts.since);
338
+ logger_js_1.logger.info(`Validating against git history (${opts.since})...`);
339
+ const commits = (0, validator_js_1.getCommitFiles)(opts.gitRepoRoot || opts.appPath, opts.since);
295
340
  if (commits.length === 0) {
296
- console.log(' No commits found in range.');
341
+ logger_js_1.logger.info('No commits found in range.');
297
342
  }
298
343
  else {
299
344
  const validations = commits.map((c) => (0, validator_js_1.validateCommit)(mergeResult.manifest, c.files, c.hash, c.message));
300
- const report = (0, validator_js_1.buildValidationReport)(validations, mergeResult.manifest);
301
- console.log('');
302
- 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));
303
347
  }
304
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 });
305
380
  }
306
- console.log('');
381
+ logger_js_1.logger.info('Done.');
307
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;AA2ID,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' },
@@ -101,6 +103,7 @@ const FLAGS = {
101
103
  '--heal-report': { key: 'analyzeHealReport', type: 'string' },
102
104
  '--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
103
105
  '--output': { key: 'trainOutput', type: 'string' },
106
+ '--server-path': { key: 'serverPath', type: 'string' },
104
107
  // -- number flags (with isFinite guard) --
105
108
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
106
109
  '--time': { key: 'timeLimitMinutes', type: 'number' },
@@ -71,5 +71,8 @@ export interface ParsedArgs {
71
71
  trainPr?: number;
72
72
  trainOutput?: string;
73
73
  trainYes?: boolean;
74
+ serverPath?: string;
75
+ verbose?: boolean;
76
+ jsonOutput?: boolean;
74
77
  }
75
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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"}
@@ -1,11 +1,13 @@
1
1
  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2
2
  // See LICENSE.txt for license information.
3
+ import { execFileSync } from 'child_process';
3
4
  import { existsSync, mkdirSync, renameSync, writeFileSync } from 'fs';
4
5
  import { dirname, join, resolve } from 'path';
5
6
  import * as readline from 'readline';
6
7
  import { resolveConfig } from '../../agent/config.js';
7
8
  import { loadRouteFamilyManifest } from '../../knowledge/route_families.js';
8
9
  import { LLMProviderFactory } from '../../provider_factory.js';
10
+ import { logger, LogLevel } from '../../logger.js';
9
11
  import { scanProject } from '../../training/scanner.js';
10
12
  import { mergeFamilies, detectStaleFamilies } from '../../training/merger.js';
11
13
  import { enrichFamilies } from '../../training/enricher.js';
@@ -75,9 +77,32 @@ function resolveTrainOptions(args, autoConfig) {
75
77
  if (!inApp && !inTests) {
76
78
  throw new TrainError(`Output path must be within the project root or tests root: ${resolvedOutputPath}`);
77
79
  }
80
+ // Discover git repo root for monorepo-aware scanning and validation
81
+ let gitRepoRoot;
82
+ try {
83
+ gitRepoRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
84
+ cwd: resolvedAppPath,
85
+ encoding: 'utf-8',
86
+ stdio: ['pipe', 'pipe', 'pipe'],
87
+ }).trim();
88
+ }
89
+ catch {
90
+ // Not a git repo or git not available
91
+ }
92
+ // Resolve serverRoot: explicit flag, or auto-detect from git repo root
93
+ let serverRoot = args.serverPath;
94
+ if (!serverRoot && gitRepoRoot) {
95
+ const serverDir = join(gitRepoRoot, 'server');
96
+ if (existsSync(serverDir)) {
97
+ serverRoot = serverDir;
98
+ }
99
+ }
100
+ const resolvedServerRoot = serverRoot ? resolve(serverRoot) : undefined;
78
101
  return {
79
102
  appPath: resolvedAppPath,
80
103
  testsRoot: resolvedTestsRoot,
104
+ serverRoot: resolvedServerRoot,
105
+ gitRepoRoot: gitRepoRoot ? resolve(gitRepoRoot) : undefined,
81
106
  enrich: args.trainEnrich !== false,
82
107
  validate: args.trainValidate || false,
83
108
  since,
@@ -116,37 +141,47 @@ function serializeManifest(manifest) {
116
141
  }
117
142
  export async function runTrainCommand(args, autoConfig) {
118
143
  const opts = resolveTrainOptions(args, autoConfig);
119
- console.log('');
120
- console.log(' e2e-ai-agents train');
121
- console.log(' ===================');
122
- 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('===================');
123
153
  // ---------- Phase 1: Deterministic scan ----------
124
- console.log(' Scanning project structure...');
125
- const scanResult = scanProject(opts.appPath);
126
- console.log(` Found ${scanResult.stats.totalSourceFiles} source files, ${scanResult.stats.totalTestFiles} test files`);
127
- console.log(` Discovered ${scanResult.families.length} candidate families`);
154
+ logger.info('Scanning project structure...');
155
+ if (opts.serverRoot) {
156
+ logger.info(`Server root: ${opts.serverRoot}`);
157
+ }
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`);
128
163
  if (scanResult.families.length === 0) {
129
- console.log('');
130
- console.log(' No families discovered. Make sure your project has recognizable');
131
- console.log(' source directories (src/, server/, app/) and test directories');
132
- 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.');
133
167
  return;
134
168
  }
135
169
  // ---------- Phase 2: Merge with existing ----------
170
+ const mergeTimer = logger.timer('merge');
136
171
  const existing = loadRouteFamilyManifest(opts.testsRoot);
137
172
  if (existing) {
138
- console.log(` Found existing manifest with ${existing.families.length} families`);
173
+ logger.info(`Found existing manifest with ${existing.families.length} families`);
139
174
  }
140
175
  let mergeResult = mergeFamilies(existing, scanResult.families);
141
- console.log(` Merge: ${mergeResult.summary}`);
176
+ timings.merge = mergeTimer.end();
177
+ logger.info(`Merge: ${mergeResult.summary}`);
142
178
  // ---------- Phase 3: Stale detection ----------
143
179
  if (mergeResult.manifest.families.length > 0) {
144
- const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath);
180
+ const stale = detectStaleFamilies(mergeResult.manifest, opts.appPath, opts.testsRoot);
145
181
  if (stale.length > 0) {
146
- console.log('');
147
- console.log(` Stale families detected (${stale.length}):`);
182
+ logger.info(`Stale families detected (${stale.length}):`);
148
183
  for (const id of stale) {
149
- console.log(` ${id} — paths no longer exist`);
184
+ logger.info(` ${id} — paths no longer exist`);
150
185
  }
151
186
  if (!opts.yes && !opts.dryRun && process.stdin.isTTY) {
152
187
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
@@ -156,7 +191,7 @@ export async function runTrainCommand(args, autoConfig) {
156
191
  const staleSet = new Set(stale);
157
192
  mergeResult.manifest.families = mergeResult.manifest.families.filter((f) => !staleSet.has(f.id));
158
193
  mergeResult.staleFamilies = stale;
159
- console.log(` Removed ${stale.length} stale families`);
194
+ logger.info(`Removed ${stale.length} stale families`);
160
195
  }
161
196
  }
162
197
  finally {
@@ -166,29 +201,41 @@ export async function runTrainCommand(args, autoConfig) {
166
201
  }
167
202
  }
168
203
  // ---------- Phase 4: LLM Enrichment ----------
204
+ let enrichTokens = 0;
205
+ let enrichCost = 0;
206
+ let enrichRequests = 0;
207
+ let enrichAvgResponseMs = 0;
169
208
  if (opts.enrich) {
170
- console.log('');
171
- console.log(' Enriching with LLM...');
209
+ logger.info('Enriching with LLM...');
210
+ const enrichTimer = logger.timer('enrich');
172
211
  try {
173
212
  const provider = await LLMProviderFactory.createFromEnv();
174
- const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD);
213
+ const enrichResult = await enrichFamilies(mergeResult.manifest.families, scanResult.families, opts.appPath, provider, opts.budgetUSD, opts.testsRoot !== opts.appPath ? opts.testsRoot : undefined);
175
214
  mergeResult.manifest.families = enrichResult.enrichedFamilies;
176
- 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
+ });
177
225
  if (enrichResult.skippedFamilies.length > 0) {
178
- console.log(` Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
226
+ logger.info(`Skipped ${enrichResult.skippedFamilies.length} families (budget limit)`);
179
227
  }
180
228
  }
181
229
  catch (error) {
182
- console.warn(` LLM enrichment failed: ${error instanceof Error ? error.message : String(error)}`);
183
- 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.');
184
232
  }
233
+ timings.enrich = enrichTimer.end();
185
234
  }
186
235
  // ---------- Phase 5: Write manifest ----------
187
- console.log('');
188
236
  const json = serializeManifest(mergeResult.manifest);
189
237
  if (opts.dryRun) {
190
- console.log(' Dry run — proposed manifest:');
191
- console.log('');
238
+ logger.info('Dry run — proposed manifest:');
192
239
  console.log(json);
193
240
  }
194
241
  else {
@@ -199,28 +246,28 @@ export async function runTrainCommand(args, autoConfig) {
199
246
  const tmpPath = `${opts.outputPath}.tmp`;
200
247
  writeFileSync(tmpPath, json, 'utf-8');
201
248
  renameSync(tmpPath, opts.outputPath);
202
- console.log(` Wrote ${opts.outputPath}`);
203
- console.log(` ${mergeResult.manifest.families.length} families`);
249
+ logger.info(`Wrote ${opts.outputPath}`);
250
+ logger.info(`${mergeResult.manifest.families.length} families`);
204
251
  }
205
252
  // ---------- Phase 6: Report unmatched ----------
206
253
  if (scanResult.unmatchedSourceDirs.length > 0 || scanResult.unmatchedTestDirs.length > 0) {
207
- console.log('');
208
- console.log(' Unmatched (review manually):');
254
+ logger.info('Unmatched (review manually):');
209
255
  for (const dir of scanResult.unmatchedSourceDirs.slice(0, 10)) {
210
- console.log(` source: ${dir.relativePath}`);
256
+ logger.info(` source: ${dir.relativePath}`);
211
257
  }
212
258
  for (const dir of scanResult.unmatchedTestDirs.slice(0, 10)) {
213
- console.log(` test: ${dir.relativePath}`);
259
+ logger.info(` test: ${dir.relativePath}`);
214
260
  }
215
261
  if (scanResult.unmatchedSourceDirs.length + scanResult.unmatchedTestDirs.length > 20) {
216
- console.log(' ... and more');
262
+ logger.info(' ... and more');
217
263
  }
218
264
  }
219
265
  // ---------- Phase 7: Validation (optional) ----------
266
+ let validationReport;
220
267
  if (opts.validate) {
268
+ const validateTimer = logger.timer('validate');
221
269
  if (opts.pr) {
222
- console.log('');
223
- console.log(` Validating against PR #${opts.pr}...`);
270
+ logger.info(`Validating against PR #${opts.pr}...`);
224
271
  // Check for gh CLI
225
272
  const { execFileSync } = await import('child_process');
226
273
  try {
@@ -243,29 +290,57 @@ export async function runTrainCommand(args, autoConfig) {
243
290
  throw new TrainError(`Error fetching PR #${opts.pr}: ${error instanceof Error ? error.message : String(error)}`);
244
291
  }
245
292
  if (prFiles.length === 0) {
246
- console.log(' No files found in PR.');
293
+ logger.info('No files found in PR.');
247
294
  }
248
295
  else {
249
296
  const validation = validateCommit(mergeResult.manifest, prFiles, `PR#${opts.pr}`, `PR #${opts.pr}`);
250
- const report = buildValidationReport([validation], mergeResult.manifest);
251
- console.log('');
252
- console.log(formatValidationReport(report));
297
+ validationReport = buildValidationReport([validation], mergeResult.manifest);
298
+ logger.info(formatValidationReport(validationReport));
253
299
  }
254
300
  }
255
301
  else {
256
- console.log('');
257
- console.log(` Validating against git history (${opts.since})...`);
258
- const commits = getCommitFiles(opts.appPath, opts.since);
302
+ logger.info(`Validating against git history (${opts.since})...`);
303
+ const commits = getCommitFiles(opts.gitRepoRoot || opts.appPath, opts.since);
259
304
  if (commits.length === 0) {
260
- console.log(' No commits found in range.');
305
+ logger.info('No commits found in range.');
261
306
  }
262
307
  else {
263
308
  const validations = commits.map((c) => validateCommit(mergeResult.manifest, c.files, c.hash, c.message));
264
- const report = buildValidationReport(validations, mergeResult.manifest);
265
- console.log('');
266
- console.log(formatValidationReport(report));
309
+ validationReport = buildValidationReport(validations, mergeResult.manifest);
310
+ logger.info(formatValidationReport(validationReport));
267
311
  }
268
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 });
269
344
  }
270
- console.log('');
345
+ logger.info('Done.');
271
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' },
@@ -95,6 +97,7 @@ const FLAGS = {
95
97
  '--heal-report': { key: 'analyzeHealReport', type: 'string' },
96
98
  '--flow-catalog': { key: 'flowCatalogPath', type: 'string' },
97
99
  '--output': { key: 'trainOutput', type: 'string' },
100
+ '--server-path': { key: 'serverPath', type: 'string' },
98
101
  // -- number flags (with isFinite guard) --
99
102
  '--pipeline-scenarios': { key: 'pipelineScenarios', type: 'number' },
100
103
  '--time': { key: 'timeLimitMinutes', type: 'number' },