@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/CHANGELOG.md +16 -1
- package/README.md +173 -11
- package/docs/api.md +272 -4
- package/docs/schemas/agent-result.v1.json +7 -4
- package/docs/schemas/fix-handoff.v1.json +105 -0
- package/docs/schemas/generate-backlog.v1.json +117 -0
- package/docs/schemas/generate-handoff.v1.json +191 -0
- package/docs/schemas/generate-map.v1.json +95 -0
- package/docs/schemas/generate-result.v1.json +341 -0
- package/docs/vscode-extension.md +6 -0
- package/docs/why-themis.md +1 -1
- package/globals.d.ts +31 -1
- package/index.d.ts +405 -2
- package/index.js +14 -0
- package/package.json +1 -1
- package/src/artifacts.js +180 -2
- package/src/assets/exampleThemisReport.png +0 -0
- package/src/cli.js +390 -11
- package/src/config.js +21 -3
- package/src/discovery.js +32 -3
- package/src/expect.js +33 -4
- package/src/generate.js +5313 -0
- package/src/migrate.js +280 -0
- package/src/module-loader.js +22 -3
- package/src/reporter.js +4 -3
- package/src/runner.js +74 -13
- package/src/runtime.js +175 -66
- package/src/test-utils.js +607 -1
- package/src/watch.js +9 -0
- package/src/worker.js +1 -2
- package/src/snapshots.js +0 -90
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
|
}
|
|
Binary file
|
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.
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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) {
|