@vitronai/themis 0.1.15 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +26 -10
- package/docs/agents-adoption.md +68 -0
- package/docs/api.md +3 -1
- package/docs/migration.md +9 -5
- package/docs/roadmap.md +1 -1
- package/docs/schemas/migration-report.v1.json +122 -0
- package/package.json +8 -2
- package/scripts/claude-hook.js +153 -0
- package/src/cli.js +79 -6
- package/src/init.js +122 -4
- package/src/migrate.js +127 -5
- package/templates/CLAUDE.themis.md +43 -0
- package/templates/claude-commands/themis-fix.md +14 -0
- package/templates/claude-commands/themis-generate.md +14 -0
- package/templates/claude-commands/themis-migrate.md +18 -0
- package/templates/claude-commands/themis-test.md +12 -0
- package/templates/claude-skill/SKILL.md +94 -0
- package/templates/cursorrules.themis.md +28 -0
- package/themis.ai.json +16 -0
package/src/cli.js
CHANGED
|
@@ -24,13 +24,54 @@ async function main(argv) {
|
|
|
24
24
|
const initFlags = parseInitFlags(argv.slice(1));
|
|
25
25
|
const initResult = runInit(cwd, initFlags);
|
|
26
26
|
console.log('Themis initialized. Next: npx themis generate <source-root> && npx themis test');
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
if (initResult.autoDetected) {
|
|
28
|
+
const detected = [];
|
|
29
|
+
if (initResult.agents) detected.push('AGENTS.md');
|
|
30
|
+
if (initResult.claudeCode) detected.push('Claude Code');
|
|
31
|
+
if (initResult.cursor) detected.push('Cursor');
|
|
32
|
+
if (detected.length > 0) {
|
|
33
|
+
console.log(`Auto-detected: ${detected.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (initFlags.agents || initResult.agents) {
|
|
37
|
+
const agents = initResult.agents;
|
|
38
|
+
if (agents && agents.created) {
|
|
39
|
+
console.log(`Agents: created ${formatCliPath(cwd, agents.path)} from the Themis downstream template.`);
|
|
30
40
|
} else {
|
|
31
41
|
console.log('Agents: skipped AGENTS.md scaffold because one already exists.');
|
|
32
42
|
}
|
|
33
43
|
}
|
|
44
|
+
if (initResult.cursor) {
|
|
45
|
+
const cur = initResult.cursor;
|
|
46
|
+
if (cur.created) {
|
|
47
|
+
console.log(`Cursor: created ${formatCliPath(cwd, cur.path)} from the Themis Cursor template.`);
|
|
48
|
+
} else if (cur.appended) {
|
|
49
|
+
console.log(`Cursor: appended Themis section to ${formatCliPath(cwd, cur.path)}.`);
|
|
50
|
+
} else {
|
|
51
|
+
console.log(`Cursor: skipped .cursorrules update (already mentions @vitronai/themis).`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (initResult.claudeCode) {
|
|
55
|
+
const cc = initResult.claudeCode;
|
|
56
|
+
if (cc.claudeMd.created) {
|
|
57
|
+
console.log(`Claude Code: created ${formatCliPath(cwd, cc.claudeMd.path)} from the Themis Claude template.`);
|
|
58
|
+
} else if (cc.claudeMd.appended) {
|
|
59
|
+
console.log(`Claude Code: appended Themis section to ${formatCliPath(cwd, cc.claudeMd.path)}.`);
|
|
60
|
+
} else {
|
|
61
|
+
console.log(`Claude Code: skipped CLAUDE.md update (already mentions @vitronai/themis).`);
|
|
62
|
+
}
|
|
63
|
+
if (cc.skill.created) {
|
|
64
|
+
console.log(`Claude Code: installed skill at ${formatCliPath(cwd, cc.skill.path)}.`);
|
|
65
|
+
} else {
|
|
66
|
+
console.log(`Claude Code: skipped skill (${formatCliPath(cwd, cc.skill.path)} already exists).`);
|
|
67
|
+
}
|
|
68
|
+
if (cc.commands.written.length > 0) {
|
|
69
|
+
console.log(`Claude Code: installed ${cc.commands.written.length} slash command(s) under ${formatCliPath(cwd, cc.commands.dir)}.`);
|
|
70
|
+
}
|
|
71
|
+
if (cc.commands.skipped.length > 0) {
|
|
72
|
+
console.log(`Claude Code: skipped ${cc.commands.skipped.length} existing slash command file(s).`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
34
75
|
return;
|
|
35
76
|
}
|
|
36
77
|
|
|
@@ -95,6 +136,17 @@ async function main(argv) {
|
|
|
95
136
|
if (result.convertedFiles && result.convertedFiles.length > 0) {
|
|
96
137
|
console.log(`Codemods: converted ${result.convertedFiles.length} file(s) to Themis-native patterns.`);
|
|
97
138
|
}
|
|
139
|
+
if (result.assist) {
|
|
140
|
+
console.log(`Assistant: analyzed ${result.assistSummary.analyzedFiles} migrated file(s).`);
|
|
141
|
+
if (result.assistSummary.findings.length > 0) {
|
|
142
|
+
console.log(
|
|
143
|
+
`Assistant: flagged ${result.assistSummary.findings.length} manual follow-up item(s) across ${result.assistSummary.unresolvedFiles.length} file(s).`
|
|
144
|
+
);
|
|
145
|
+
} else {
|
|
146
|
+
console.log('Assistant: no unsupported Jest/Vitest-only patterns detected in migrated files.');
|
|
147
|
+
}
|
|
148
|
+
console.log(`Report: ${formatCliPath(cwd, result.reportPath)}`);
|
|
149
|
+
}
|
|
98
150
|
console.log('Runtime compatibility is enabled for @jest/globals, vitest, and @testing-library/react imports.');
|
|
99
151
|
console.log('Next: run npx themis test or npm run test:themis');
|
|
100
152
|
return;
|
|
@@ -500,7 +552,8 @@ function parseMigrateFlags(args) {
|
|
|
500
552
|
const flags = {
|
|
501
553
|
source: args[0],
|
|
502
554
|
rewriteImports: false,
|
|
503
|
-
convert: false
|
|
555
|
+
convert: false,
|
|
556
|
+
assist: false
|
|
504
557
|
};
|
|
505
558
|
|
|
506
559
|
for (let i = 1; i < args.length; i += 1) {
|
|
@@ -511,6 +564,16 @@ function parseMigrateFlags(args) {
|
|
|
511
564
|
}
|
|
512
565
|
if (token === '--convert') {
|
|
513
566
|
flags.convert = true;
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (token === '--assist') {
|
|
570
|
+
flags.assist = true;
|
|
571
|
+
flags.rewriteImports = true;
|
|
572
|
+
flags.convert = true;
|
|
573
|
+
continue;
|
|
574
|
+
}
|
|
575
|
+
if (token.startsWith('-')) {
|
|
576
|
+
throw new Error(`Unsupported migrate option: ${token}`);
|
|
514
577
|
}
|
|
515
578
|
}
|
|
516
579
|
|
|
@@ -519,7 +582,9 @@ function parseMigrateFlags(args) {
|
|
|
519
582
|
|
|
520
583
|
function parseInitFlags(args) {
|
|
521
584
|
const flags = {
|
|
522
|
-
agents: false
|
|
585
|
+
agents: false,
|
|
586
|
+
claudeCode: false,
|
|
587
|
+
cursor: false
|
|
523
588
|
};
|
|
524
589
|
|
|
525
590
|
for (let i = 0; i < args.length; i += 1) {
|
|
@@ -528,6 +593,14 @@ function parseInitFlags(args) {
|
|
|
528
593
|
flags.agents = true;
|
|
529
594
|
continue;
|
|
530
595
|
}
|
|
596
|
+
if (token === '--claude-code' || token === '--claude') {
|
|
597
|
+
flags.claudeCode = true;
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (token === '--cursor') {
|
|
601
|
+
flags.cursor = true;
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
531
604
|
if (token.startsWith('-')) {
|
|
532
605
|
throw new Error(`Unsupported init option: ${token}`);
|
|
533
606
|
}
|
|
@@ -741,7 +814,7 @@ function printUsage() {
|
|
|
741
814
|
console.log(' generate [path] Scan source files and generate Themis contract tests');
|
|
742
815
|
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]');
|
|
743
816
|
console.log(' scan [path] Alias for generate');
|
|
744
|
-
console.log(' migrate <jest|vitest> [--rewrite-imports] [--convert] Scaffold an incremental migration bridge for existing suites');
|
|
817
|
+
console.log(' migrate <jest|vitest> [--rewrite-imports] [--convert] [--assist] Scaffold an incremental migration bridge for existing suites');
|
|
745
818
|
console.log(' test [--json] [--agent] [--next] [--reporter spec|next|json|agent|html] [--workers N] [--stability N] [--environment node|jsdom] [--isolation worker|in-process] [--cache] [--update-contracts] [--fix] [-w|--watch] [--html-output path] [--match regex] [--rerun-failed] [--no-memes] [--lexicon classic|themis]');
|
|
746
819
|
}
|
|
747
820
|
|
package/src/init.js
CHANGED
|
@@ -3,17 +3,36 @@ const path = require('path');
|
|
|
3
3
|
const { initConfig } = require('./config');
|
|
4
4
|
const { ensureGitignoreEntries } = require('./gitignore');
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
|
|
7
|
+
const AGENTS_TEMPLATE_PATH = path.join(TEMPLATES_DIR, 'AGENTS.themis.md');
|
|
8
|
+
const CLAUDE_TEMPLATE_PATH = path.join(TEMPLATES_DIR, 'CLAUDE.themis.md');
|
|
9
|
+
const CLAUDE_SKILL_TEMPLATE_PATH = path.join(TEMPLATES_DIR, 'claude-skill', 'SKILL.md');
|
|
10
|
+
const CLAUDE_COMMANDS_TEMPLATE_DIR = path.join(TEMPLATES_DIR, 'claude-commands');
|
|
11
|
+
const CURSOR_TEMPLATE_PATH = path.join(TEMPLATES_DIR, 'cursorrules.themis.md');
|
|
7
12
|
|
|
8
13
|
function runInit(cwd, options = {}) {
|
|
9
14
|
initConfig(cwd);
|
|
10
15
|
ensureGitignoreEntries(cwd, ['.themis/', '__themis__/reports/', '__themis__/shims/']);
|
|
11
16
|
|
|
12
|
-
|
|
13
|
-
|
|
17
|
+
const hasExplicitFlags = options.agents || options.claudeCode || options.cursor;
|
|
18
|
+
const detected = hasExplicitFlags ? options : detectAgents(cwd, options);
|
|
19
|
+
|
|
20
|
+
const result = {};
|
|
21
|
+
|
|
22
|
+
if (detected.agents) {
|
|
23
|
+
result.agents = ensureAgentsTemplate(cwd);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (detected.claudeCode) {
|
|
27
|
+
result.claudeCode = ensureClaudeCodeAssets(cwd);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (detected.cursor) {
|
|
31
|
+
result.cursor = writeCursorRules(cwd);
|
|
14
32
|
}
|
|
15
33
|
|
|
16
|
-
|
|
34
|
+
result.autoDetected = !hasExplicitFlags;
|
|
35
|
+
return result;
|
|
17
36
|
}
|
|
18
37
|
|
|
19
38
|
function ensureAgentsTemplate(cwd) {
|
|
@@ -33,6 +52,105 @@ function ensureAgentsTemplate(cwd) {
|
|
|
33
52
|
};
|
|
34
53
|
}
|
|
35
54
|
|
|
55
|
+
function ensureClaudeCodeAssets(cwd) {
|
|
56
|
+
const result = {
|
|
57
|
+
claudeMd: writeClaudeMd(cwd),
|
|
58
|
+
skill: writeClaudeSkill(cwd),
|
|
59
|
+
commands: writeClaudeCommands(cwd)
|
|
60
|
+
};
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeClaudeMd(cwd) {
|
|
65
|
+
const targetPath = path.join(cwd, 'CLAUDE.md');
|
|
66
|
+
const source = fs.readFileSync(CLAUDE_TEMPLATE_PATH, 'utf8');
|
|
67
|
+
|
|
68
|
+
if (!fs.existsSync(targetPath)) {
|
|
69
|
+
fs.writeFileSync(targetPath, source, 'utf8');
|
|
70
|
+
return { path: targetPath, created: true, appended: false };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
74
|
+
if (existing.includes('@vitronai/themis')) {
|
|
75
|
+
return { path: targetPath, created: false, appended: false };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
79
|
+
fs.writeFileSync(targetPath, existing + separator + source, 'utf8');
|
|
80
|
+
return { path: targetPath, created: false, appended: true };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeClaudeSkill(cwd) {
|
|
84
|
+
const targetDir = path.join(cwd, '.claude', 'skills', 'themis');
|
|
85
|
+
const targetPath = path.join(targetDir, 'SKILL.md');
|
|
86
|
+
if (fs.existsSync(targetPath)) {
|
|
87
|
+
return { path: targetPath, created: false };
|
|
88
|
+
}
|
|
89
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
90
|
+
const source = fs.readFileSync(CLAUDE_SKILL_TEMPLATE_PATH, 'utf8');
|
|
91
|
+
fs.writeFileSync(targetPath, source, 'utf8');
|
|
92
|
+
return { path: targetPath, created: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function writeClaudeCommands(cwd) {
|
|
96
|
+
const targetDir = path.join(cwd, '.claude', 'commands');
|
|
97
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
98
|
+
|
|
99
|
+
const entries = fs.readdirSync(CLAUDE_COMMANDS_TEMPLATE_DIR);
|
|
100
|
+
const written = [];
|
|
101
|
+
const skipped = [];
|
|
102
|
+
|
|
103
|
+
for (const entry of entries) {
|
|
104
|
+
if (!entry.endsWith('.md')) continue;
|
|
105
|
+
const sourcePath = path.join(CLAUDE_COMMANDS_TEMPLATE_DIR, entry);
|
|
106
|
+
const targetPath = path.join(targetDir, entry);
|
|
107
|
+
if (fs.existsSync(targetPath)) {
|
|
108
|
+
skipped.push(targetPath);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const source = fs.readFileSync(sourcePath, 'utf8');
|
|
112
|
+
fs.writeFileSync(targetPath, source, 'utf8');
|
|
113
|
+
written.push(targetPath);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return { dir: targetDir, written, skipped };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function detectAgents(cwd) {
|
|
120
|
+
const flags = { agents: true, claudeCode: false, cursor: false };
|
|
121
|
+
|
|
122
|
+
// Claude Code: .claude/ dir or CLAUDE.md exists
|
|
123
|
+
if (fs.existsSync(path.join(cwd, '.claude')) || fs.existsSync(path.join(cwd, 'CLAUDE.md'))) {
|
|
124
|
+
flags.claudeCode = true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Cursor: .cursorrules or .cursor/ dir exists
|
|
128
|
+
if (fs.existsSync(path.join(cwd, '.cursorrules')) || fs.existsSync(path.join(cwd, '.cursor'))) {
|
|
129
|
+
flags.cursor = true;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return flags;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function writeCursorRules(cwd) {
|
|
136
|
+
const targetPath = path.join(cwd, '.cursorrules');
|
|
137
|
+
const source = fs.readFileSync(CURSOR_TEMPLATE_PATH, 'utf8');
|
|
138
|
+
|
|
139
|
+
if (!fs.existsSync(targetPath)) {
|
|
140
|
+
fs.writeFileSync(targetPath, source, 'utf8');
|
|
141
|
+
return { path: targetPath, created: true, appended: false };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const existing = fs.readFileSync(targetPath, 'utf8');
|
|
145
|
+
if (existing.includes('@vitronai/themis')) {
|
|
146
|
+
return { path: targetPath, created: false, appended: false };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const separator = existing.endsWith('\n') ? '\n' : '\n\n';
|
|
150
|
+
fs.writeFileSync(targetPath, existing + separator + source, 'utf8');
|
|
151
|
+
return { path: targetPath, created: false, appended: true };
|
|
152
|
+
}
|
|
153
|
+
|
|
36
154
|
module.exports = {
|
|
37
155
|
runInit
|
|
38
156
|
};
|
package/src/migrate.js
CHANGED
|
@@ -10,6 +10,40 @@ const THEMIS_COMPAT_FILE = 'themis.compat.js';
|
|
|
10
10
|
const MIGRATION_REPORT_FILE = ARTIFACT_RELATIVE_PATHS.migrationReport;
|
|
11
11
|
const SCANNABLE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
|
|
12
12
|
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', '.themis']);
|
|
13
|
+
const MIGRATION_ASSIST_PATTERNS = Object.freeze([
|
|
14
|
+
{
|
|
15
|
+
id: 'remaining-framework-import',
|
|
16
|
+
category: 'remaining-framework-import',
|
|
17
|
+
severity: 'warning',
|
|
18
|
+
pattern: /(?:import\s+[^;]*?from\s+['"](?:@jest\/globals|vitest|@testing-library\/react)['"]|import\s*\(\s*['"](?:@jest\/globals|vitest|@testing-library\/react)['"]\s*\)|require\(\s*['"](?:@jest\/globals|vitest|@testing-library\/react)['"]\s*\))/,
|
|
19
|
+
message: 'Framework-specific imports are still present after migration scaffolding.',
|
|
20
|
+
suggestion: 'Rewrite or remove remaining framework imports so the suite only depends on Themis-compatible entry points.'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'unsupported-helper',
|
|
24
|
+
category: 'unsupported-helper',
|
|
25
|
+
severity: 'warning',
|
|
26
|
+
pattern: /\b(?:jest|vi)\.(?:mocked|doMock|dontMock|setMock|requireActual|requireMock|createMockFromModule|isolateModules|isolateModulesAsync|unstable_mockModule|importActual|importMock)\s*\(/,
|
|
27
|
+
message: 'Unsupported Jest/Vitest helper detected.',
|
|
28
|
+
suggestion: 'Replace the helper with an explicit Themis mock, fixture, or project-local test utility before relying on the migrated suite.'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'async-matcher-chain',
|
|
32
|
+
category: 'async-matcher-chain',
|
|
33
|
+
severity: 'warning',
|
|
34
|
+
pattern: /\.\s*(?:resolves|rejects)\b/,
|
|
35
|
+
message: 'Promise matcher chains remain in the migrated file.',
|
|
36
|
+
suggestion: 'Rewrite promise assertions to explicit await-based checks so they run under Themis without framework-specific matcher chaining.'
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'focused-alias',
|
|
40
|
+
category: 'focused-alias',
|
|
41
|
+
severity: 'warning',
|
|
42
|
+
pattern: /\b(?:fit|xit|fdescribe|xdescribe)\s*\(/,
|
|
43
|
+
message: 'Focused or excluded Jest/Vitest aliases remain in the migrated file.',
|
|
44
|
+
suggestion: 'Replace focused aliases with Themis-supported describe/test forms before running the migrated suite broadly.'
|
|
45
|
+
}
|
|
46
|
+
]);
|
|
13
47
|
|
|
14
48
|
function runMigrate(cwd, framework, options = {}) {
|
|
15
49
|
const source = String(framework || '').trim().toLowerCase();
|
|
@@ -66,7 +100,14 @@ function runMigrate(cwd, framework, options = {}) {
|
|
|
66
100
|
const conversionSummary = options.convert
|
|
67
101
|
? convertMigrationFiles(projectRoot, scan.matches)
|
|
68
102
|
: { convertedFiles: [], convertedAssertions: 0, removedImports: 0 };
|
|
69
|
-
const
|
|
103
|
+
const assistSummary = analyzeMigrationAssist(projectRoot, scan.matches, {
|
|
104
|
+
enabled: Boolean(options.assist)
|
|
105
|
+
});
|
|
106
|
+
const report = buildMigrationReport(projectRoot, source, scan.matches, rewriteSummary, conversionSummary, assistSummary, {
|
|
107
|
+
rewriteImports: Boolean(options.rewriteImports),
|
|
108
|
+
convert: Boolean(options.convert),
|
|
109
|
+
assist: Boolean(options.assist)
|
|
110
|
+
});
|
|
70
111
|
fs.mkdirSync(path.dirname(reportPath), { recursive: true });
|
|
71
112
|
fs.writeFileSync(reportPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
72
113
|
|
|
@@ -84,7 +125,9 @@ function runMigrate(cwd, framework, options = {}) {
|
|
|
84
125
|
rewriteImports: Boolean(options.rewriteImports),
|
|
85
126
|
rewrittenFiles: rewriteSummary.rewrittenFiles,
|
|
86
127
|
convert: Boolean(options.convert),
|
|
87
|
-
convertedFiles: conversionSummary.convertedFiles
|
|
128
|
+
convertedFiles: conversionSummary.convertedFiles,
|
|
129
|
+
assist: Boolean(options.assist),
|
|
130
|
+
assistSummary
|
|
88
131
|
};
|
|
89
132
|
}
|
|
90
133
|
|
|
@@ -170,12 +213,20 @@ function buildMigrationReport(
|
|
|
170
213
|
source,
|
|
171
214
|
matches,
|
|
172
215
|
rewriteSummary = { rewrittenFiles: [], rewrittenImports: 0 },
|
|
173
|
-
conversionSummary = { convertedFiles: [], convertedAssertions: 0, removedImports: 0 }
|
|
216
|
+
conversionSummary = { convertedFiles: [], convertedAssertions: 0, removedImports: 0 },
|
|
217
|
+
assistSummary = buildEmptyAssistSummary(false),
|
|
218
|
+
mode = { rewriteImports: false, convert: false, assist: false }
|
|
174
219
|
) {
|
|
220
|
+
const effectiveAssistSummary = normalizeAssistSummary(assistSummary, Boolean(mode.assist));
|
|
175
221
|
return {
|
|
176
222
|
schema: 'themis.migration.report.v1',
|
|
177
223
|
source,
|
|
178
224
|
createdAt: new Date().toISOString(),
|
|
225
|
+
mode: {
|
|
226
|
+
rewriteImports: Boolean(mode.rewriteImports),
|
|
227
|
+
convert: Boolean(mode.convert),
|
|
228
|
+
assist: Boolean(mode.assist)
|
|
229
|
+
},
|
|
179
230
|
summary: {
|
|
180
231
|
matchedFiles: matches.length,
|
|
181
232
|
jestGlobals: matches.filter((entry) => entry.imports.includes('@jest/globals')).length,
|
|
@@ -185,11 +236,18 @@ function buildMigrationReport(
|
|
|
185
236
|
rewrittenImports: Number(rewriteSummary.rewrittenImports || 0),
|
|
186
237
|
convertedFiles: Array.isArray(conversionSummary.convertedFiles) ? conversionSummary.convertedFiles.length : 0,
|
|
187
238
|
convertedAssertions: Number(conversionSummary.convertedAssertions || 0),
|
|
188
|
-
removedImports: Number(conversionSummary.removedImports || 0)
|
|
239
|
+
removedImports: Number(conversionSummary.removedImports || 0),
|
|
240
|
+
assistedFiles: Number(effectiveAssistSummary.analyzedFiles || 0),
|
|
241
|
+
unresolvedFiles: Array.isArray(effectiveAssistSummary.unresolvedFiles) ? effectiveAssistSummary.unresolvedFiles.length : 0,
|
|
242
|
+
findings: Array.isArray(effectiveAssistSummary.findings) ? effectiveAssistSummary.findings.length : 0,
|
|
243
|
+
unsupportedPatterns: Number(effectiveAssistSummary.unsupportedPatterns || 0)
|
|
189
244
|
},
|
|
190
245
|
files: matches,
|
|
191
246
|
nextActions: [
|
|
192
247
|
'Run npx themis test to execute migrated suites under the Themis runtime.',
|
|
248
|
+
...(effectiveAssistSummary.findings.length > 0
|
|
249
|
+
? ['Resolve assistant findings in the migration report before relying on the migrated suite in CI.']
|
|
250
|
+
: []),
|
|
193
251
|
'Replace any unsupported Jest/Vitest-only helpers with Themis built-ins or project setup utilities.',
|
|
194
252
|
'Use npx themis generate src for source-driven unit-layer coverage alongside migrated suites.'
|
|
195
253
|
],
|
|
@@ -198,7 +256,71 @@ function buildMigrationReport(
|
|
|
198
256
|
: [],
|
|
199
257
|
conversions: Array.isArray(conversionSummary.convertedFiles)
|
|
200
258
|
? conversionSummary.convertedFiles
|
|
201
|
-
: []
|
|
259
|
+
: [],
|
|
260
|
+
assistant: effectiveAssistSummary
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function analyzeMigrationAssist(projectRoot, matches, options = {}) {
|
|
265
|
+
const enabled = Boolean(options.enabled);
|
|
266
|
+
if (!enabled) {
|
|
267
|
+
return buildEmptyAssistSummary(false);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const findings = [];
|
|
271
|
+
const unresolvedFiles = new Set();
|
|
272
|
+
|
|
273
|
+
for (const match of matches) {
|
|
274
|
+
const absoluteFile = path.join(projectRoot, match.file);
|
|
275
|
+
const sourceText = fs.readFileSync(absoluteFile, 'utf8');
|
|
276
|
+
|
|
277
|
+
for (const definition of MIGRATION_ASSIST_PATTERNS) {
|
|
278
|
+
if (!definition.pattern.test(sourceText)) {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
unresolvedFiles.add(match.file);
|
|
282
|
+
findings.push({
|
|
283
|
+
file: match.file,
|
|
284
|
+
category: definition.category,
|
|
285
|
+
severity: definition.severity,
|
|
286
|
+
pattern: definition.id,
|
|
287
|
+
message: definition.message,
|
|
288
|
+
suggestion: definition.suggestion
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return normalizeAssistSummary(
|
|
294
|
+
{
|
|
295
|
+
enabled: true,
|
|
296
|
+
analyzedFiles: matches.length,
|
|
297
|
+
findings,
|
|
298
|
+
unresolvedFiles: Array.from(unresolvedFiles).sort(),
|
|
299
|
+
unsupportedPatterns: findings.length
|
|
300
|
+
},
|
|
301
|
+
true
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function buildEmptyAssistSummary(enabled) {
|
|
306
|
+
return {
|
|
307
|
+
enabled: Boolean(enabled),
|
|
308
|
+
analyzedFiles: 0,
|
|
309
|
+
findings: [],
|
|
310
|
+
unresolvedFiles: [],
|
|
311
|
+
unsupportedPatterns: 0
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function normalizeAssistSummary(summary, enabled) {
|
|
316
|
+
const findings = Array.isArray(summary && summary.findings) ? summary.findings : [];
|
|
317
|
+
const unresolvedFiles = Array.isArray(summary && summary.unresolvedFiles) ? summary.unresolvedFiles : [];
|
|
318
|
+
return {
|
|
319
|
+
enabled: Boolean(enabled),
|
|
320
|
+
analyzedFiles: Number((summary && summary.analyzedFiles) || 0),
|
|
321
|
+
findings,
|
|
322
|
+
unresolvedFiles,
|
|
323
|
+
unsupportedPatterns: Number((summary && summary.unsupportedPatterns) || findings.length)
|
|
202
324
|
};
|
|
203
325
|
}
|
|
204
326
|
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Testing With Themis
|
|
2
|
+
|
|
3
|
+
This repository uses [`@vitronai/themis`](https://www.npmjs.com/package/@vitronai/themis) as its unit test framework. Themis is a drop-in alternative to Jest and Vitest, designed for AI coding agents like Claude Code: deterministic reruns, structured failure output, and machine-readable repair hints.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
- Install: `npm install -D @vitronai/themis`
|
|
8
|
+
- Initialize: `npx themis init`
|
|
9
|
+
- Generate tests for a source tree: `npx themis generate src` (or `app` for Next App Router)
|
|
10
|
+
- Run tests: `npx themis test`
|
|
11
|
+
- Run tests and emit agent-readable output: `npx themis test --reporter agent`
|
|
12
|
+
- Re-run only failed tests: `npx themis test --rerun-failed`
|
|
13
|
+
- Migrate from Jest: `npx themis migrate jest` then `--rewrite-imports` then `--convert`
|
|
14
|
+
- Migrate from Vitest: `npx themis migrate vitest` then `--rewrite-imports` then `--convert`
|
|
15
|
+
|
|
16
|
+
## When You Are Asked To Add Or Fix Tests
|
|
17
|
+
|
|
18
|
+
1. If the repo has no tests yet, run `npx themis generate <source-root>` first. Generated tests land under `__themis__/tests` as `.generated.test.ts` (TS/TSX) or `.generated.test.js` (JS/JSX). Use them as a starting point — do not delete or rewrite them wholesale.
|
|
19
|
+
2. Prefer `intent(...)` for behavior and workflow tests. Prefer `test(...)` for low-level unit checks. Use the four-phase shape: `context` → `run` → `verify` → `cleanup`.
|
|
20
|
+
3. Run `npx themis test --reporter agent` and read the JSON output. Failure clusters and repair hints are structured — use them to drive your fix loop instead of re-reading raw stack traces.
|
|
21
|
+
4. After fixing, re-run only the failing tests with `npx themis test --rerun-failed` before running the full suite.
|
|
22
|
+
|
|
23
|
+
## When You Are Asked To Migrate From Jest Or Vitest
|
|
24
|
+
|
|
25
|
+
1. Run `npx themis migrate jest` (or `vitest`) — this scaffolds compatibility, no rewrites yet.
|
|
26
|
+
2. Run `npx themis migrate jest --rewrite-imports` to point imports at `themis.compat.js`.
|
|
27
|
+
3. Run `npx themis migrate jest --convert` to apply codemods toward native Themis style.
|
|
28
|
+
4. Run `npx themis migrate jest --assist` to get a structured findings report of files that still need manual follow-up. The report path is printed at the end of the command — read it before guessing what to fix.
|
|
29
|
+
5. Run `npx themis test` after each step. Migration is incremental on purpose; do not try to convert the whole suite in one pass.
|
|
30
|
+
|
|
31
|
+
## Things To Avoid
|
|
32
|
+
|
|
33
|
+
- Do not claim Themis is "not a unit test framework" — it is one.
|
|
34
|
+
- Do not create `tests/*.cjs` or `tests/*.setup.*` files just to load `.css`, `.png`, `.jpg`, `.svg`, or font/media imports. Themis handles these natively.
|
|
35
|
+
- Do not commit `.themis/`, `__themis__/reports/`, or `__themis__/shims/` — they are gitignored framework output.
|
|
36
|
+
- Do not rewrite generated tests under `__themis__/tests` by hand unless the user asks; treat them as Themis-managed.
|
|
37
|
+
- Do not reach for snapshot tests as the default — prefer deterministic assertions and `captureContract(...)` for contract-style checks.
|
|
38
|
+
|
|
39
|
+
## Reference
|
|
40
|
+
|
|
41
|
+
- API reference: [`node_modules/@vitronai/themis/docs/api.md`](node_modules/@vitronai/themis/docs/api.md)
|
|
42
|
+
- Adoption guide: [`node_modules/@vitronai/themis/docs/agents-adoption.md`](node_modules/@vitronai/themis/docs/agents-adoption.md)
|
|
43
|
+
- Machine-readable manifest: [`node_modules/@vitronai/themis/themis.ai.json`](node_modules/@vitronai/themis/themis.ai.json)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Fix failing Themis tests using the agent reporter's repair hints
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Read the most recent Themis run output and fix the failures.
|
|
6
|
+
|
|
7
|
+
1. Run `npx themis test --reporter agent` and capture the JSON output.
|
|
8
|
+
2. Group the failures by `failures[].cluster`. Fixes within a cluster usually share a root cause — address the cluster, not each failure independently.
|
|
9
|
+
3. For each cluster, read `failures[].repairHints` first. They are structured suggestions you can act on directly.
|
|
10
|
+
4. Apply the smallest fix that addresses the root cause. Do not refactor surrounding code.
|
|
11
|
+
5. Re-run with `npx themis test --rerun-failed` to confirm the cluster is fixed.
|
|
12
|
+
6. Repeat for the next cluster. Only run the full suite once `--rerun-failed` is green.
|
|
13
|
+
|
|
14
|
+
If the user passed specific test names or files, scope the work to those: $ARGUMENTS
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Generate Themis tests for a source tree
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Generate Themis tests for the given source root. If the user did not specify one, default to `src` (or `app` if this is a Next.js App Router project — check for `app/layout.tsx` or `app/page.tsx`).
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx themis generate $ARGUMENTS
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Generated tests land under `__themis__/tests`. After generation:
|
|
12
|
+
1. Read the summary the command prints — it tells you how many files were generated, skipped, and why.
|
|
13
|
+
2. Run `npx themis test --reporter agent` to verify the generated suite passes.
|
|
14
|
+
3. If any generated tests are wrong, do not delete them — extend or correct them in place.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Migrate this repo from Jest or Vitest to Themis
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Migrate this repository from Jest or Vitest to Themis. If the user did not say which, detect it: check `package.json` devDependencies for `jest` or `vitest`.
|
|
6
|
+
|
|
7
|
+
Run the four steps in order, and run `npx themis test` between each step to confirm the suite is still green:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx themis migrate <jest|vitest> # 1. scaffold compatibility
|
|
11
|
+
npx themis migrate <jest|vitest> --rewrite-imports # 2. rewrite imports
|
|
12
|
+
npx themis migrate <jest|vitest> --convert # 3. codemod to native style
|
|
13
|
+
npx themis migrate <jest|vitest> --assist # 4. emit structured findings
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
After step 4, read the findings report (its path is printed at the end of the command) and walk through any items it flags for manual follow-up. Do not guess at fixes — the report tells you what needs human attention and why.
|
|
17
|
+
|
|
18
|
+
If the user passed extra arguments, forward them: $ARGUMENTS
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Run the Themis test suite with agent-readable output
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
Run `npx themis test --reporter agent` and read the JSON output. If there are failures:
|
|
6
|
+
|
|
7
|
+
1. Group them by `failures[].cluster` — fixes for the same cluster usually share a root cause.
|
|
8
|
+
2. For each cluster, read `failures[].repairHints` before reading the raw stack trace.
|
|
9
|
+
3. Apply the smallest fix that addresses the root cause, then re-run with `npx themis test --rerun-failed` to verify.
|
|
10
|
+
4. Only run the full suite again once `--rerun-failed` is green.
|
|
11
|
+
|
|
12
|
+
If the user passed extra arguments, forward them to `npx themis test`: $ARGUMENTS
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: themis
|
|
3
|
+
description: Use this skill when the user asks to write, generate, run, fix, or migrate unit tests in a Node.js or TypeScript repository that has @vitronai/themis installed (or when the user explicitly mentions Themis). Covers test authoring with intent(...) and test(...), running the suite via `npx themis test`, reading agent-readable failure output, and incremental migration from Jest or Vitest.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Themis
|
|
7
|
+
|
|
8
|
+
This repo uses [`@vitronai/themis`](https://www.npmjs.com/package/@vitronai/themis) as its unit test framework. It is a drop-in alternative to Jest and Vitest, designed for AI coding agents: deterministic execution, structured failure output with repair hints, and a one-command migration path.
|
|
9
|
+
|
|
10
|
+
## How To Run Tests
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx themis test # full suite, human-readable output
|
|
14
|
+
npx themis test --reporter agent # JSON output with failure clusters and repair hints
|
|
15
|
+
npx themis test --rerun-failed # only re-run tests that failed last run
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**When you are in an edit-test-fix loop, always use `--reporter agent`.** The JSON output gives you:
|
|
19
|
+
- `failures[].cluster` — failures grouped by likely common cause
|
|
20
|
+
- `failures[].repairHints` — structured suggestions you can act on directly
|
|
21
|
+
- `failures[].sourceFile`, `lineNumber`, `expected`, `actual` — already parsed, no need to re-parse stack traces
|
|
22
|
+
|
|
23
|
+
After fixing, prefer `--rerun-failed` over a full re-run.
|
|
24
|
+
|
|
25
|
+
## How To Write Tests
|
|
26
|
+
|
|
27
|
+
Themis has two primary forms:
|
|
28
|
+
|
|
29
|
+
**`intent(...)`** for behavior and workflow tests. Use the four-phase shape:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
intent('user can sign in', ({ context, run, verify, cleanup }) => {
|
|
33
|
+
context('a valid user', (ctx) => {
|
|
34
|
+
ctx.user = { email: 'a@b.com', password: 'pw' };
|
|
35
|
+
});
|
|
36
|
+
run('the user submits credentials', (ctx) => {
|
|
37
|
+
ctx.result = signIn(ctx.user);
|
|
38
|
+
});
|
|
39
|
+
verify('authentication succeeds', (ctx) => {
|
|
40
|
+
expect(ctx.result.ok).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
cleanup('remove test state', (ctx) => {
|
|
43
|
+
delete ctx.user;
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**`test(...)`** for low-level unit checks (pure functions, single assertions).
|
|
49
|
+
|
|
50
|
+
Rules of thumb:
|
|
51
|
+
- Each phase has one job. Do not assert in `context` or `run`. Do not mutate state in `verify`.
|
|
52
|
+
- Prefer `expect(...)` style assertions. They work the same as Jest/Vitest.
|
|
53
|
+
- Prefer deterministic assertions over snapshots. For contract-style coverage use `captureContract(...)`.
|
|
54
|
+
|
|
55
|
+
## How To Generate Tests From Source
|
|
56
|
+
|
|
57
|
+
If the user asks you to add tests for a module or directory and the repo already has Themis installed:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx themis generate src # conventional source tree
|
|
61
|
+
npx themis generate app # Next.js App Router
|
|
62
|
+
npx themis generate src/auth # narrower target
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Generated tests land under `__themis__/tests` as `.generated.test.ts` (TS/TSX sources) or `.generated.test.js` (JS/JSX sources). Treat them as Themis-managed — extend rather than rewrite.
|
|
66
|
+
|
|
67
|
+
## How To Migrate From Jest Or Vitest
|
|
68
|
+
|
|
69
|
+
Migration is incremental on purpose. Run the steps in order and `npx themis test` between each:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
npx themis migrate jest # 1. scaffold compatibility, no code changes
|
|
73
|
+
npx themis migrate jest --rewrite-imports # 2. point imports at themis.compat.js
|
|
74
|
+
npx themis migrate jest --convert # 3. apply codemods to native Themis style
|
|
75
|
+
npx themis migrate jest --assist # 4. emit structured findings JSON for manual follow-ups
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Same flags work for `vitest`. **Read the `--assist` findings report before guessing what to fix manually** — its path is printed at the end of the command and the schema is at `node_modules/@vitronai/themis/docs/schemas/migration-report.v1.json`.
|
|
79
|
+
|
|
80
|
+
## Things To Avoid
|
|
81
|
+
|
|
82
|
+
- Do not claim Themis is "not a unit test framework". It is one.
|
|
83
|
+
- Do not create `tests/*.cjs` or `tests/*.setup.*` files to shim `.css`, `.png`, `.jpg`, `.svg`, or font imports. Themis handles those natively.
|
|
84
|
+
- Do not commit `.themis/`, `__themis__/reports/`, or `__themis__/shims/` — they are gitignored framework output.
|
|
85
|
+
- Do not rewrite generated tests under `__themis__/tests` by hand unless the user asks.
|
|
86
|
+
- Do not reach for `setupFiles` unless you genuinely need a real harness bootstrap.
|
|
87
|
+
- Do not try to migrate the whole Jest/Vitest suite in one pass. Use the four migrate steps in order.
|
|
88
|
+
|
|
89
|
+
## Reference Files In This Repo
|
|
90
|
+
|
|
91
|
+
- API reference: `node_modules/@vitronai/themis/docs/api.md`
|
|
92
|
+
- Adoption guide: `node_modules/@vitronai/themis/docs/agents-adoption.md`
|
|
93
|
+
- Machine-readable manifest: `node_modules/@vitronai/themis/themis.ai.json`
|
|
94
|
+
- Migration report schema: `node_modules/@vitronai/themis/docs/schemas/migration-report.v1.json`
|