@vitronai/themis 0.1.0-beta.0 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/artifacts.js CHANGED
@@ -6,6 +6,9 @@ const LAST_RUN_FILE = 'last-run.json';
6
6
  const FAILED_TESTS_FILE = 'failed-tests.json';
7
7
  const RUN_DIFF_FILE = 'run-diff.json';
8
8
  const RUN_HISTORY_FILE = 'run-history.json';
9
+ const FIX_HANDOFF_FILE = 'fix-handoff.json';
10
+ const GENERATE_MAP_FILE = 'generate-map.json';
11
+ const GENERATE_BACKLOG_FILE = 'generate-backlog.json';
9
12
 
10
13
  function writeRunArtifacts(cwd, result) {
11
14
  const artifactDir = path.join(cwd, ARTIFACT_DIR);
@@ -19,7 +22,8 @@ function writeRunArtifacts(cwd, result) {
19
22
  lastRun: path.join(ARTIFACT_DIR, LAST_RUN_FILE),
20
23
  failedTests: path.join(ARTIFACT_DIR, FAILED_TESTS_FILE),
21
24
  runDiff: path.join(ARTIFACT_DIR, RUN_DIFF_FILE),
22
- runHistory: path.join(ARTIFACT_DIR, RUN_HISTORY_FILE)
25
+ runHistory: path.join(ARTIFACT_DIR, RUN_HISTORY_FILE),
26
+ fixHandoff: path.join(ARTIFACT_DIR, FIX_HANDOFF_FILE)
23
27
  };
24
28
 
25
29
  result.artifacts = {
@@ -79,13 +83,26 @@ function writeRunArtifacts(cwd, result) {
79
83
  });
80
84
  fs.writeFileSync(historyPath, `${stringifyArtifact(nextHistory)}\n`, 'utf8');
81
85
 
86
+ const fixHandoffPath = path.join(artifactDir, FIX_HANDOFF_FILE);
87
+ let fixHandoff = null;
88
+ if (failedTests.length > 0) {
89
+ fixHandoff = buildFixHandoffPayload(cwd, result, {
90
+ runId,
91
+ failedTests,
92
+ relativePaths
93
+ });
94
+ fs.writeFileSync(fixHandoffPath, `${stringifyArtifact(fixHandoff)}\n`, 'utf8');
95
+ }
96
+
82
97
  return {
83
98
  runPath,
84
99
  failuresPath,
85
100
  diffPath,
86
101
  historyPath,
102
+ fixHandoffPath,
87
103
  failuresPayload,
88
- comparison
104
+ comparison,
105
+ fixHandoff
89
106
  };
90
107
  }
91
108
 
@@ -193,6 +210,167 @@ function readJsonIfExists(filePath) {
193
210
  }
194
211
  }
195
212
 
213
+ function buildFixHandoffPayload(cwd, result, context) {
214
+ const generateMap = readJsonIfExists(path.join(cwd, ARTIFACT_DIR, GENERATE_MAP_FILE));
215
+ const generateBacklog = readJsonIfExists(path.join(cwd, ARTIFACT_DIR, GENERATE_BACKLOG_FILE));
216
+ const mapEntries = Array.isArray(generateMap && generateMap.entries) ? generateMap.entries : [];
217
+ const backlogItems = Array.isArray(generateBacklog && generateBacklog.items) ? generateBacklog.items : [];
218
+ const byGeneratedTest = new Map();
219
+
220
+ for (const entry of mapEntries) {
221
+ if (!entry || !entry.testFile) {
222
+ continue;
223
+ }
224
+ byGeneratedTest.set(path.resolve(cwd, entry.testFile), entry);
225
+ }
226
+
227
+ const groupedItems = new Map();
228
+ for (const failedTest of context.failedTests) {
229
+ const generatedEntry = byGeneratedTest.get(path.resolve(failedTest.file));
230
+ if (!generatedEntry) {
231
+ continue;
232
+ }
233
+
234
+ const reason = String(failedTest.message || '');
235
+ const category = classifyFixCategory(reason);
236
+ const backlogMatch = backlogItems.find((item) => item && item.sourceFile === generatedEntry.sourceFile);
237
+ const groupKey = `${generatedEntry.testFile}:${category}`;
238
+ if (groupedItems.has(groupKey)) {
239
+ const existing = groupedItems.get(groupKey);
240
+ existing.failureCount += 1;
241
+ existing.failedTests.push(failedTest.fullName);
242
+ continue;
243
+ }
244
+
245
+ groupedItems.set(groupKey, {
246
+ file: failedTest.file,
247
+ name: failedTest.name,
248
+ fullName: failedTest.fullName,
249
+ message: reason,
250
+ testFile: generatedEntry.testFile,
251
+ sourceFile: generatedEntry.sourceFile,
252
+ moduleKind: generatedEntry.moduleKind,
253
+ confidence: generatedEntry.confidence,
254
+ scenarios: Array.isArray(generatedEntry.scenarios) ? generatedEntry.scenarios.map((scenario) => scenario.kind) : [],
255
+ hintsFile: generatedEntry.hintsFile || (backlogMatch ? backlogMatch.hintsFile : null),
256
+ category,
257
+ failureCount: 1,
258
+ failedTests: [failedTest.fullName],
259
+ repairStrategy: resolveRepairStrategy(category, generatedEntry, backlogMatch),
260
+ candidateFiles: buildFixCandidateFiles(generatedEntry, backlogMatch),
261
+ suggestedAction: resolveFixAction(category, generatedEntry, backlogMatch),
262
+ suggestedCommand: resolveFixCommand(category, generatedEntry, backlogMatch),
263
+ autofixCommand: resolveAutofixCommand(category, generatedEntry, backlogMatch)
264
+ });
265
+ }
266
+
267
+ const items = [...groupedItems.values()];
268
+
269
+ const summary = {
270
+ totalFailures: Number(result.summary?.failed || 0),
271
+ generatedFailures: items.length,
272
+ staleSources: items.filter((item) => item.category === 'source-drift').length,
273
+ contractFailures: items.filter((item) => item.category === 'generated-contract-failure').length
274
+ };
275
+
276
+ return {
277
+ schema: 'themis.fix.handoff.v1',
278
+ runId: context.runId,
279
+ createdAt: new Date().toISOString(),
280
+ summary,
281
+ artifacts: {
282
+ failedTests: context.relativePaths.failedTests,
283
+ generateMap: path.join(ARTIFACT_DIR, GENERATE_MAP_FILE),
284
+ generateBacklog: path.join(ARTIFACT_DIR, GENERATE_BACKLOG_FILE),
285
+ fixHandoff: context.relativePaths.fixHandoff
286
+ },
287
+ items,
288
+ nextActions: buildFixNextActions(summary)
289
+ };
290
+ }
291
+
292
+ function classifyFixCategory(message) {
293
+ const lower = String(message || '').toLowerCase();
294
+ if (
295
+ lower.includes('generated from source file has changed since scan')
296
+ || (lower.includes('stale') && lower.includes('npx themis generate'))
297
+ ) {
298
+ return 'source-drift';
299
+ }
300
+ return 'generated-contract-failure';
301
+ }
302
+
303
+ function resolveFixAction(category, entry, backlogMatch) {
304
+ if (category === 'source-drift') {
305
+ return `Regenerate the generated test for ${entry.sourceFile}.`;
306
+ }
307
+ if (backlogMatch && backlogMatch.suggestedAction) {
308
+ return backlogMatch.suggestedAction;
309
+ }
310
+ return `Inspect the generated contract and supporting hints for ${entry.sourceFile}.`;
311
+ }
312
+
313
+ function resolveFixCommand(category, entry, backlogMatch) {
314
+ if (backlogMatch && backlogMatch.suggestedCommand) {
315
+ return backlogMatch.suggestedCommand;
316
+ }
317
+ if (entry && entry.sourceFile) {
318
+ return `npx themis generate ${entry.sourceFile} --update`;
319
+ }
320
+ return null;
321
+ }
322
+
323
+ function resolveAutofixCommand(category, entry, backlogMatch) {
324
+ if (backlogMatch && backlogMatch.suggestedCommand) {
325
+ return backlogMatch.suggestedCommand;
326
+ }
327
+ if (category === 'source-drift' && entry && entry.sourceFile) {
328
+ return `npx themis generate ${entry.sourceFile} --update && npx themis test --match ${JSON.stringify(path.basename(entry.testFile || entry.sourceFile))}`;
329
+ }
330
+ if (entry && entry.sourceFile) {
331
+ return `npx themis generate ${entry.sourceFile} --update`;
332
+ }
333
+ return null;
334
+ }
335
+
336
+ function resolveRepairStrategy(category, entry, backlogMatch) {
337
+ if (category === 'source-drift') {
338
+ return 'regenerate-source';
339
+ }
340
+ if (backlogMatch && backlogMatch.hintsFile) {
341
+ return 'tighten-hints';
342
+ }
343
+ return 'inspect-contract';
344
+ }
345
+
346
+ function buildFixCandidateFiles(entry, backlogMatch) {
347
+ const files = [];
348
+ if (entry && entry.sourceFile) {
349
+ files.push(entry.sourceFile);
350
+ }
351
+ if (entry && entry.testFile) {
352
+ files.push(entry.testFile);
353
+ }
354
+ if (entry && entry.hintsFile) {
355
+ files.push(entry.hintsFile);
356
+ } else if (backlogMatch && backlogMatch.hintsFile) {
357
+ files.push(backlogMatch.hintsFile);
358
+ }
359
+ return [...new Set(files)];
360
+ }
361
+
362
+ function buildFixNextActions(summary) {
363
+ const actions = [];
364
+ if (summary.generatedFailures > 0) {
365
+ actions.push('Review .themis/fix-handoff.json and start with source-drift items.');
366
+ actions.push('Regenerate narrow targets before rerunning the full suite.');
367
+ }
368
+ if (summary.generatedFailures === 0) {
369
+ actions.push('No generated-test repair work was detected in this run.');
370
+ }
371
+ return actions;
372
+ }
373
+
196
374
  function roundDuration(value) {
197
375
  return Math.round(Number(value || 0) * 100) / 100;
198
376
  }
package/src/cli.js CHANGED
@@ -1,8 +1,11 @@
1
+ const path = require('path');
1
2
  const { loadConfig } = require('./config');
2
3
  const { discoverTests } = require('./discovery');
3
4
  const { runTests } = require('./runner');
4
5
  const { printSpec, printJson, printAgent, printNext, writeHtmlReport } = require('./reporter');
5
6
  const { runInit } = require('./init');
7
+ const { runMigrate } = require('./migrate');
8
+ const { generateTestsFromSource, writeGenerateArtifacts } = require('./generate');
6
9
  const { writeRunArtifacts, readFailedTestsArtifact } = require('./artifacts');
7
10
  const { buildStabilityReport, hasStabilityBreaches } = require('./stability');
8
11
  const { verdictReveal } = require('./verdict');
@@ -22,14 +25,103 @@ async function main(argv) {
22
25
  return;
23
26
  }
24
27
 
28
+ if (command === 'generate' || command === 'scan') {
29
+ const flags = parseGenerateFlags(argv.slice(1));
30
+ if (!flags.json) {
31
+ printBanner('next');
32
+ }
33
+ const summary = generateTestsFromSource(cwd, {
34
+ targetDir: flags.targetDir || 'src',
35
+ outputDir: flags.outputDir,
36
+ force: Boolean(flags.force),
37
+ strict: Boolean(flags.strict),
38
+ writeHints: Boolean(flags.writeHints),
39
+ review: Boolean(flags.review),
40
+ update: Boolean(flags.update),
41
+ clean: Boolean(flags.clean),
42
+ changed: Boolean(flags.changed),
43
+ plan: Boolean(flags.plan),
44
+ failOnSkips: Boolean(flags.failOnSkips),
45
+ failOnConflicts: Boolean(flags.failOnConflicts),
46
+ requireConfidence: flags.requireConfidence || null,
47
+ scenario: flags.scenario || null,
48
+ minConfidence: flags.minConfidence || null,
49
+ files: flags.files || null,
50
+ matchSource: flags.matchSource || null,
51
+ matchExport: flags.matchExport || null,
52
+ include: flags.include || null,
53
+ exclude: flags.exclude || null
54
+ });
55
+ const { payload } = writeGenerateArtifacts(summary, cwd);
56
+ if (flags.json) {
57
+ console.log(JSON.stringify(payload));
58
+ if (summary.gates.failed) {
59
+ process.exitCode = 1;
60
+ }
61
+ return;
62
+ }
63
+ printGenerateSummary(summary, cwd);
64
+ if (summary.gates.failed) {
65
+ process.exitCode = 1;
66
+ }
67
+ return;
68
+ }
69
+
70
+ if (command === 'migrate') {
71
+ const migrateFlags = parseMigrateFlags(argv.slice(1));
72
+ const result = runMigrate(cwd, migrateFlags.source, migrateFlags);
73
+ console.log(`Themis migration scaffold created for ${result.source}.`);
74
+ console.log(`Config: ${formatCliPath(cwd, result.configPath)}`);
75
+ console.log(`Setup: ${formatCliPath(cwd, result.setupPath)}`);
76
+ console.log(`Compat: ${formatCliPath(cwd, result.compatPath)}`);
77
+ if (result.packageUpdated && result.packageJsonPath) {
78
+ console.log(`Scripts: updated ${formatCliPath(cwd, result.packageJsonPath)} with test:themis`);
79
+ }
80
+ if (result.rewriteImports) {
81
+ console.log(`Imports: rewrote ${result.rewrittenFiles.length} file(s) to local Themis compatibility imports.`);
82
+ }
83
+ console.log('Runtime compatibility is enabled for @jest/globals, vitest, and @testing-library/react imports.');
84
+ console.log('Next: run npx themis test or npm run test:themis');
85
+ return;
86
+ }
87
+
25
88
  if (command !== 'test') {
26
89
  printUsage();
27
90
  process.exitCode = 1;
28
91
  return;
29
92
  }
30
93
 
31
- const config = loadConfig(cwd);
32
94
  const flags = parseFlags(argv.slice(1));
95
+ const watchIsolation = flags.watch ? (flags.isolation || 'in-process') : flags.isolation;
96
+ const watchCache = flags.watch ? (flags.cache !== undefined ? flags.cache : watchIsolation === 'in-process') : flags.cache;
97
+ if (watchIsolation) {
98
+ validateIsolation(watchIsolation);
99
+ }
100
+ if (flags.watch) {
101
+ await runWatchMode({
102
+ cwd,
103
+ cliArgs: argv.slice(1),
104
+ inProcess: watchIsolation === 'in-process',
105
+ executeInProcess: async (cliArgs) => {
106
+ const rerunFlags = parseFlags(cliArgs);
107
+ rerunFlags.watch = false;
108
+ if (!rerunFlags.isolation && watchIsolation) {
109
+ rerunFlags.isolation = watchIsolation;
110
+ }
111
+ if (rerunFlags.cache === undefined && watchCache !== undefined) {
112
+ rerunFlags.cache = watchCache;
113
+ }
114
+ await executeTestRun(cwd, rerunFlags);
115
+ }
116
+ });
117
+ return;
118
+ }
119
+
120
+ await executeTestRun(cwd, flags);
121
+ }
122
+
123
+ async function executeTestRun(cwd, flags) {
124
+ const config = loadConfig(cwd);
33
125
 
34
126
  if (flags.match) {
35
127
  validateRegex(flags.match);
@@ -43,12 +135,8 @@ async function main(argv) {
43
135
  validateStabilityRuns(flags.stability);
44
136
  const environment = resolveEnvironment(flags, config);
45
137
  validateEnvironment(environment, flags.environment, config.environment);
46
- if (flags.watch) {
47
- await runWatchMode({
48
- cwd,
49
- cliArgs: argv.slice(1)
50
- });
51
- return;
138
+ if (flags.isolation) {
139
+ validateIsolation(flags.isolation);
52
140
  }
53
141
  printBanner(reporter);
54
142
  const maxWorkers = resolveWorkerCount(flags.workers, config.maxWorkers);
@@ -96,7 +184,8 @@ async function main(argv) {
96
184
  environment,
97
185
  setupFiles: config.setupFiles,
98
186
  tsconfigPath: config.tsconfigPath,
99
- updateSnapshots: Boolean(flags.updateSnapshots)
187
+ isolation: flags.isolation || 'worker',
188
+ cache: Boolean(flags.cache)
100
189
  });
101
190
  runResults.push(runResult);
102
191
  }
@@ -200,8 +289,7 @@ function parseFlags(args) {
200
289
  continue;
201
290
  }
202
291
  if (token === '-u' || token === '--update-snapshots') {
203
- flags.updateSnapshots = true;
204
- continue;
292
+ throw new Error('Snapshots have been removed from Themis. Replace -u/--update-snapshots with direct assertions or generated contract flows.');
205
293
  }
206
294
  if (token === '--reporter') {
207
295
  flags.reporter = requireFlagValue(args, i, '--reporter');
@@ -238,7 +326,146 @@ function parseFlags(args) {
238
326
  i += 1;
239
327
  continue;
240
328
  }
329
+ if (token === '--isolation') {
330
+ flags.isolation = requireFlagValue(args, i, '--isolation');
331
+ i += 1;
332
+ continue;
333
+ }
334
+ if (token === '--cache') {
335
+ flags.cache = true;
336
+ continue;
337
+ }
338
+ if (token.startsWith('-')) {
339
+ throw new Error(`Unsupported test option: ${token}`);
340
+ }
341
+ }
342
+ return flags;
343
+ }
344
+
345
+ function parseMigrateFlags(args) {
346
+ const flags = {
347
+ source: args[0],
348
+ rewriteImports: false
349
+ };
350
+
351
+ for (let i = 1; i < args.length; i += 1) {
352
+ const token = args[i];
353
+ if (token === '--rewrite-imports') {
354
+ flags.rewriteImports = true;
355
+ }
356
+ }
357
+
358
+ return flags;
359
+ }
360
+
361
+ function parseGenerateFlags(args) {
362
+ const flags = {};
363
+
364
+ for (let i = 0; i < args.length; i += 1) {
365
+ const token = args[i];
366
+ if (token === '--json') {
367
+ flags.json = true;
368
+ continue;
369
+ }
370
+ if (token === '--plan') {
371
+ flags.plan = true;
372
+ flags.review = true;
373
+ flags.json = true;
374
+ continue;
375
+ }
376
+ if (token === '--output') {
377
+ flags.outputDir = requireFlagValue(args, i, '--output');
378
+ i += 1;
379
+ continue;
380
+ }
381
+ if (token === '--write-hints') {
382
+ flags.writeHints = true;
383
+ continue;
384
+ }
385
+ if (token === '--files') {
386
+ flags.files = requireFlagValue(args, i, '--files');
387
+ i += 1;
388
+ continue;
389
+ }
390
+ if (token === '--match-source') {
391
+ flags.matchSource = requireFlagValue(args, i, '--match-source');
392
+ i += 1;
393
+ continue;
394
+ }
395
+ if (token === '--match-export') {
396
+ flags.matchExport = requireFlagValue(args, i, '--match-export');
397
+ i += 1;
398
+ continue;
399
+ }
400
+ if (token === '--scenario') {
401
+ flags.scenario = requireFlagValue(args, i, '--scenario');
402
+ i += 1;
403
+ continue;
404
+ }
405
+ if (token === '--require-confidence') {
406
+ flags.requireConfidence = requireFlagValue(args, i, '--require-confidence');
407
+ i += 1;
408
+ continue;
409
+ }
410
+ if (token === '--min-confidence') {
411
+ flags.minConfidence = requireFlagValue(args, i, '--min-confidence');
412
+ i += 1;
413
+ continue;
414
+ }
415
+ if (token === '--include') {
416
+ flags.include = requireFlagValue(args, i, '--include');
417
+ i += 1;
418
+ continue;
419
+ }
420
+ if (token === '--exclude') {
421
+ flags.exclude = requireFlagValue(args, i, '--exclude');
422
+ i += 1;
423
+ continue;
424
+ }
425
+ if (token === '--force') {
426
+ flags.force = true;
427
+ continue;
428
+ }
429
+ if (token === '--strict') {
430
+ flags.strict = true;
431
+ continue;
432
+ }
433
+ if (token === '--fail-on-skips') {
434
+ flags.failOnSkips = true;
435
+ continue;
436
+ }
437
+ if (token === '--fail-on-conflicts') {
438
+ flags.failOnConflicts = true;
439
+ continue;
440
+ }
441
+ if (token === '--review') {
442
+ flags.review = true;
443
+ continue;
444
+ }
445
+ if (token === '--update') {
446
+ flags.update = true;
447
+ continue;
448
+ }
449
+ if (token === '--clean') {
450
+ flags.clean = true;
451
+ continue;
452
+ }
453
+ if (token === '--changed') {
454
+ flags.changed = true;
455
+ continue;
456
+ }
457
+ if (token.startsWith('--')) {
458
+ throw new Error(
459
+ 'Unsupported generate option: ' + token +
460
+ '. Use --json, --plan, --output <dir>, --files <paths>, --match-source <regex>, --match-export <regex>, --scenario <name>, --min-confidence <level>, --require-confidence <level>, --include <regex>, --exclude <regex>, --force, --strict, --write-hints, --fail-on-skips, --fail-on-conflicts, --review, --update, --clean, or --changed.'
461
+ );
462
+ }
463
+ if (flags.targetDir) {
464
+ throw new Error(`Unexpected extra argument: ${token}`);
465
+ }
466
+ flags.targetDir = token;
241
467
  }
468
+
242
469
  return flags;
243
470
  }
244
471
 
@@ -299,6 +526,13 @@ function validateWorkerCount(flagValue, configValue) {
299
526
  throw new Error(`Invalid config maxWorkers value: ${String(configValue)}. Use a positive integer.`);
300
527
  }
301
528
 
529
+ function validateIsolation(value) {
530
+ if (value === 'worker' || value === 'in-process') {
531
+ return;
532
+ }
533
+ throw new Error(`Unsupported --isolation value: ${value}. Use one of: worker, in-process.`);
534
+ }
535
+
302
536
  function resolveWorkerCount(flagValue, configValue) {
303
537
  const sourceValue = flagValue !== undefined ? flagValue : configValue;
304
538
  return Number(sourceValue);
@@ -326,7 +560,152 @@ function printUsage() {
326
560
  console.log('Usage: themis <command> [options]');
327
561
  console.log('Commands:');
328
562
  console.log(' init Create themis.config.json and sample tests');
329
- console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [-w|--watch] [-u|--update-snapshots] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
563
+ console.log(' generate [path] Scan source files and generate Themis contract tests');
564
+ console.log(' Options: [--json] [--plan] [--output path] [--files a,b] [--match-source regex] [--match-export regex] [--scenario name] [--min-confidence level] [--require-confidence level] [--include regex] [--exclude regex] [--review] [--update] [--clean] [--changed] [--force] [--strict] [--write-hints] [--fail-on-skips] [--fail-on-conflicts]');
565
+ console.log(' scan [path] Alias for generate');
566
+ console.log(' migrate <jest|vitest> [--rewrite-imports] Scaffold an incremental migration bridge for existing suites');
567
+ console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [--isolation worker|in-process] [--cache] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
568
+ }
569
+
570
+ function printGenerateSummary(summary, cwd) {
571
+ const target = formatCliPath(cwd, summary.targetDir);
572
+ const output = formatCliPath(cwd, summary.outputDir);
573
+ console.log('THEMIS CODE SCAN COMPLETE');
574
+ console.log(` target: ${target}`);
575
+ console.log(` output: ${output}`);
576
+ console.log(` scanned: ${summary.scannedFiles.length}`);
577
+ console.log(` generated: ${summary.generatedFiles.length}`);
578
+ console.log(` created: ${summary.createdFiles.length}`);
579
+ console.log(` updated: ${summary.updatedFiles.length}`);
580
+ console.log(` unchanged: ${summary.unchangedFiles.length}`);
581
+ console.log(` removed: ${summary.removedFiles.length}`);
582
+ console.log(` skipped: ${summary.skippedFiles.length}`);
583
+ console.log(` conflicts: ${summary.conflictFiles.length}`);
584
+
585
+ if (summary.plan || summary.review || summary.update || summary.clean || summary.changed || summary.writeHints) {
586
+ console.log('');
587
+ console.log('Mode');
588
+ console.log(` plan: ${summary.plan ? 'yes' : 'no'}`);
589
+ console.log(` review: ${summary.review ? 'yes' : 'no'}`);
590
+ console.log(` update: ${summary.update ? 'yes' : 'no'}`);
591
+ console.log(` clean: ${summary.clean ? 'yes' : 'no'}`);
592
+ console.log(` changed: ${summary.changed ? 'yes' : 'no'}`);
593
+ console.log(` write-hints: ${summary.writeHints ? 'yes' : 'no'}`);
594
+ }
595
+
596
+ if (summary.filters.scenario || summary.filters.minConfidence) {
597
+ console.log('');
598
+ console.log('Steering');
599
+ console.log(` scenario: ${summary.filters.scenario || '(any)'}`);
600
+ console.log(` min-confidence: ${summary.filters.minConfidence || '(any)'}`);
601
+ }
602
+
603
+ if (summary.gates.failed || summary.gates.strict || summary.gates.failOnSkips || summary.gates.requireConfidence) {
604
+ console.log('');
605
+ console.log('Gates');
606
+ console.log(` strict: ${summary.gates.strict ? 'yes' : 'no'}`);
607
+ console.log(` fail-on-skips: ${summary.gates.failOnSkips ? 'yes' : 'no'}`);
608
+ console.log(` fail-on-conflicts: ${summary.gates.failOnConflicts ? 'yes' : 'no'}`);
609
+ console.log(` require-confidence: ${summary.gates.requireConfidence || '(none)'}`);
610
+ console.log(` status: ${summary.gates.failed ? 'failed' : 'passed'}`);
611
+ }
612
+
613
+ if (summary.generatedFiles.length > 0) {
614
+ console.log('');
615
+ console.log('Selected Generated Files');
616
+ for (const file of summary.generatedFiles.slice(0, 10)) {
617
+ console.log(` ${formatCliPath(cwd, file)}`);
618
+ }
619
+ if (summary.generatedFiles.length > 10) {
620
+ console.log(` ... ${summary.generatedFiles.length - 10} more`);
621
+ }
622
+ }
623
+
624
+ if (summary.removedFiles.length > 0) {
625
+ console.log('');
626
+ console.log('Removed Files');
627
+ for (const file of summary.removedFiles.slice(0, 10)) {
628
+ console.log(` ${formatCliPath(cwd, file)}`);
629
+ }
630
+ if (summary.removedFiles.length > 10) {
631
+ console.log(` ... ${summary.removedFiles.length - 10} more`);
632
+ }
633
+ }
634
+
635
+ if (summary.skippedFiles.length > 0) {
636
+ console.log('');
637
+ console.log('Skipped Files');
638
+ for (const entry of summary.skippedFiles.slice(0, 5)) {
639
+ console.log(` ${formatCliPath(cwd, entry.file)} (${entry.reason})`);
640
+ }
641
+ if (summary.skippedFiles.length > 5) {
642
+ console.log(` ... ${summary.skippedFiles.length - 5} more`);
643
+ }
644
+ }
645
+
646
+ if (summary.conflictFiles.length > 0) {
647
+ console.log('');
648
+ console.log('Conflicting Files');
649
+ for (const file of summary.conflictFiles.slice(0, 5)) {
650
+ console.log(` ${formatCliPath(cwd, file)}`);
651
+ }
652
+ if (summary.conflictFiles.length > 5) {
653
+ console.log(` ... ${summary.conflictFiles.length - 5} more`);
654
+ }
655
+ }
656
+
657
+ if (summary.hintFiles.created.length > 0 || summary.hintFiles.updated.length > 0 || summary.hintFiles.unchanged.length > 0) {
658
+ console.log('');
659
+ console.log('Hint Files');
660
+ console.log(` created: ${summary.hintFiles.created.length}`);
661
+ console.log(` updated: ${summary.hintFiles.updated.length}`);
662
+ console.log(` unchanged: ${summary.hintFiles.unchanged.length}`);
663
+ for (const file of [...summary.hintFiles.created, ...summary.hintFiles.updated].slice(0, 5)) {
664
+ console.log(` ${formatCliPath(cwd, file)}`);
665
+ }
666
+ }
667
+
668
+ if (summary.backlog.summary.total > 0) {
669
+ console.log('');
670
+ console.log('Backlog');
671
+ console.log(` total: ${summary.backlog.summary.total}`);
672
+ console.log(` errors: ${summary.backlog.summary.errors}`);
673
+ console.log(` warnings: ${summary.backlog.summary.warnings}`);
674
+ for (const item of summary.backlog.items.slice(0, 5)) {
675
+ console.log(` ${item.severity.toUpperCase()} ${formatCliPath(cwd, item.sourceFile)} (${item.reason})`);
676
+ }
677
+ if (summary.backlog.items.length > 5) {
678
+ console.log(` ... ${summary.backlog.items.length - 5} more`);
679
+ }
680
+ }
681
+
682
+ if (summary.gates.failed) {
683
+ console.log('');
684
+ console.log('Gate Failures');
685
+ for (const failure of summary.gates.failures) {
686
+ console.log(` ${failure.code}: ${failure.message}`);
687
+ }
688
+ }
689
+
690
+ console.log('');
691
+ console.log('Prompt');
692
+ console.log(` ${summary.prompt}`);
693
+
694
+ console.log('');
695
+ console.log('Artifacts');
696
+ console.log(` ${formatCliPath(cwd, summary.artifacts.generateResult)}`);
697
+ console.log(` ${formatCliPath(cwd, summary.artifacts.generateHandoff)}`);
698
+ console.log(` ${formatCliPath(cwd, summary.artifacts.generateBacklog)}`);
699
+ console.log(` ${formatCliPath(cwd, summary.artifacts.generateMap)}`);
700
+
701
+ console.log('');
702
+ console.log('Next Step');
703
+ console.log(` Run: ${summary.review ? summary.gates.failed ? 'resolve backlog items, rerun generate, then npx themis test' : 'npx themis generate ' + target + ' && npx themis test' : summary.gates.failed ? 'resolve backlog items, rerun generate, then npx themis test' : 'npx themis test'}`);
704
+ }
705
+
706
+ function formatCliPath(cwd, targetPath) {
707
+ const relative = path.relative(cwd, targetPath);
708
+ return relative && !relative.startsWith('..') ? relative.split(path.sep).join('/') : targetPath;
330
709
  }
331
710
 
332
711
  function printBanner(reporter) {