@mmnto/cli 1.6.1 → 1.6.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.
Files changed (66) hide show
  1. package/dist/commands/check.js +2 -2
  2. package/dist/commands/check.test.js +9 -8
  3. package/dist/commands/check.test.js.map +1 -1
  4. package/dist/commands/config-drift.test.js +5 -4
  5. package/dist/commands/config-drift.test.js.map +1 -1
  6. package/dist/commands/config.d.ts +4 -0
  7. package/dist/commands/config.d.ts.map +1 -0
  8. package/dist/commands/config.js +39 -0
  9. package/dist/commands/config.js.map +1 -0
  10. package/dist/commands/config.test.d.ts +2 -0
  11. package/dist/commands/config.test.d.ts.map +1 -0
  12. package/dist/commands/config.test.js +117 -0
  13. package/dist/commands/config.test.js.map +1 -0
  14. package/dist/commands/docs.d.ts +1 -1
  15. package/dist/commands/docs.js +2 -2
  16. package/dist/commands/eject.d.ts.map +1 -1
  17. package/dist/commands/eject.js +1 -1
  18. package/dist/commands/eject.js.map +1 -1
  19. package/dist/commands/exemption.d.ts +7 -0
  20. package/dist/commands/exemption.d.ts.map +1 -0
  21. package/dist/commands/exemption.js +166 -0
  22. package/dist/commands/exemption.js.map +1 -0
  23. package/dist/commands/exemption.test.d.ts +2 -0
  24. package/dist/commands/exemption.test.d.ts.map +1 -0
  25. package/dist/commands/exemption.test.js +258 -0
  26. package/dist/commands/exemption.test.js.map +1 -0
  27. package/dist/commands/init-templates.d.ts +4 -4
  28. package/dist/commands/init-templates.js +5 -5
  29. package/dist/commands/init.d.ts.map +1 -1
  30. package/dist/commands/init.js +5 -3
  31. package/dist/commands/init.js.map +1 -1
  32. package/dist/commands/install-hooks.d.ts +3 -3
  33. package/dist/commands/install-hooks.d.ts.map +1 -1
  34. package/dist/commands/install-hooks.js +55 -56
  35. package/dist/commands/install-hooks.js.map +1 -1
  36. package/dist/commands/install-hooks.test.js +134 -77
  37. package/dist/commands/install-hooks.test.js.map +1 -1
  38. package/dist/commands/lesson.d.ts +3 -0
  39. package/dist/commands/lesson.d.ts.map +1 -0
  40. package/dist/commands/lesson.js +90 -0
  41. package/dist/commands/lesson.js.map +1 -0
  42. package/dist/commands/lesson.test.d.ts +2 -0
  43. package/dist/commands/lesson.test.d.ts.map +1 -0
  44. package/dist/commands/lesson.test.js +187 -0
  45. package/dist/commands/lesson.test.js.map +1 -0
  46. package/dist/commands/lint.d.ts.map +1 -1
  47. package/dist/commands/lint.js +52 -0
  48. package/dist/commands/lint.js.map +1 -1
  49. package/dist/commands/review-alias.test.d.ts +2 -0
  50. package/dist/commands/review-alias.test.d.ts.map +1 -0
  51. package/dist/commands/review-alias.test.js +49 -0
  52. package/dist/commands/review-alias.test.js.map +1 -0
  53. package/dist/commands/rule.d.ts +4 -0
  54. package/dist/commands/rule.d.ts.map +1 -0
  55. package/dist/commands/rule.js +154 -0
  56. package/dist/commands/rule.js.map +1 -0
  57. package/dist/commands/rule.test.d.ts +2 -0
  58. package/dist/commands/rule.test.d.ts.map +1 -0
  59. package/dist/commands/rule.test.js +315 -0
  60. package/dist/commands/rule.test.js.map +1 -0
  61. package/dist/commands/shield.js +5 -5
  62. package/dist/commands/spec-templates.d.ts +1 -1
  63. package/dist/commands/spec-templates.js +1 -1
  64. package/dist/index.js +226 -37
  65. package/dist/index.js.map +1 -1
  66. package/package.json +2 -2
@@ -0,0 +1,49 @@
1
+ import { execSync } from 'node:child_process';
2
+ import * as path from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+ /**
5
+ * Integration tests for the `review` command registration and
6
+ * the deprecated `shield` alias. Uses the built CLI dist output.
7
+ */
8
+ describe('review command alias', () => {
9
+ const distEntry = path.resolve(process.cwd(), 'dist/index.js');
10
+ const cli = (args) => {
11
+ try {
12
+ // totem-context: test helper — args are hardcoded test strings, not user input
13
+ const stdout = execSync(`node "${distEntry}" ${args}`, {
14
+ cwd: process.cwd(),
15
+ encoding: 'utf-8',
16
+ timeout: 10_000,
17
+ env: { ...process.env, NODE_NO_WARNINGS: '1' },
18
+ });
19
+ return { stdout, stderr: '' };
20
+ }
21
+ catch (err) {
22
+ const e = err;
23
+ return { stdout: e.stdout ?? '', stderr: e.stderr ?? '' };
24
+ }
25
+ };
26
+ it('registers review as a visible command in --help', () => {
27
+ const { stdout } = cli('--help');
28
+ expect(stdout).toContain('review');
29
+ });
30
+ it('does NOT show shield as a visible command in --help', () => {
31
+ const { stdout } = cli('--help');
32
+ // shield should be hidden — it must not appear as a top-level command line
33
+ const lines = stdout.split('\n').filter((l) => /^\s+shield\s/.test(l));
34
+ expect(lines).toHaveLength(0);
35
+ });
36
+ it('shield subcommand help describes it as deprecated alias', () => {
37
+ const { stdout } = cli('shield --help');
38
+ expect(stdout).toContain('Deprecated alias');
39
+ expect(stdout).toContain('totem review');
40
+ });
41
+ it('shield alias emits deprecation warning to stderr when invoked', () => {
42
+ // Running `shield` without proper git context will fail, but the
43
+ // deprecation warning is emitted before the action logic runs.
44
+ const { stderr } = cli('shield');
45
+ expect(stderr).toContain("'totem shield' is deprecated");
46
+ expect(stderr).toContain('totem review');
47
+ });
48
+ });
49
+ //# sourceMappingURL=review-alias.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"review-alias.test.js","sourceRoot":"","sources":["../../src/commands/review-alias.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAE9C;;;GAGG;AACH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,eAAe,CAAC,CAAC;IAE/D,MAAM,GAAG,GAAG,CAAC,IAAY,EAAsC,EAAE;QAC/D,IAAI,CAAC;YACH,+EAA+E;YAC/E,MAAM,MAAM,GAAG,QAAQ,CAAC,SAAS,SAAS,KAAK,IAAI,EAAE,EAAE;gBACrD,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;gBAClB,QAAQ,EAAE,OAAO;gBACjB,OAAO,EAAE,MAAM;gBACf,GAAG,EAAE,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,gBAAgB,EAAE,GAAG,EAAE;aAC/C,CAAC,CAAC;YACH,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;QAChC,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACtB,MAAM,CAAC,GAAG,GAA2C,CAAC;YACtD,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,MAAM,IAAI,EAAE,EAAE,CAAC;QAC5D,CAAC;IACH,CAAC,CAAC;IAEF,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QACzD,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,2EAA2E;QAC3E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACvE,MAAM,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yDAAyD,EAAE,GAAG,EAAE;QACjE,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC;QACxC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,kBAAkB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,+DAA+D,EAAE,GAAG,EAAE;QACvE,iEAAiE;QACjE,+DAA+D;QAC/D,MAAM,EAAE,MAAM,EAAE,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC,CAAC;QACzD,MAAM,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,cAAc,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function ruleListCommand(): Promise<void>;
2
+ export declare function ruleInspectCommand(id: string): Promise<void>;
3
+ export declare function ruleTestCommand(id: string): Promise<void>;
4
+ //# sourceMappingURL=rule.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.d.ts","sourceRoot":"","sources":["../../src/commands/rule.ts"],"names":[],"mappings":"AAkEA,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAmCrD;AAED,wBAAsB,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA0ClE;AAED,wBAAsB,eAAe,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyD/D"}
@@ -0,0 +1,154 @@
1
+ const TAG = 'Rule';
2
+ // ─── Helpers ───────────────────────────────────────────
3
+ function truncate(text, max) {
4
+ return text.length > max ? text.slice(0, max - 1) + '\u2026' : text;
5
+ }
6
+ /**
7
+ * Resolve the rules path and load rules, with a user-friendly error
8
+ * when no compiled-rules.json exists.
9
+ */
10
+ async function loadRulesOrExit() {
11
+ const path = await import('node:path');
12
+ const { loadCompiledRules } = await import('@mmnto/totem');
13
+ const { loadConfig, resolveConfigPath } = await import('../utils.js');
14
+ const cwd = process.cwd();
15
+ const configPath = resolveConfigPath(cwd);
16
+ const config = await loadConfig(configPath);
17
+ const totemDir = path.join(cwd, config.totemDir);
18
+ const rulesPath = path.join(totemDir, 'compiled-rules.json');
19
+ const rules = loadCompiledRules(rulesPath);
20
+ return { rules, totemDir, cwd };
21
+ }
22
+ /**
23
+ * Find rules matching a hash prefix. Returns the match(es) and handles
24
+ * ambiguous / not-found cases with user output.
25
+ */
26
+ function resolveRuleByPrefix(rules, id, log, bold) {
27
+ const lower = id.toLowerCase();
28
+ const matches = rules.filter((r) => r.lessonHash.toLowerCase().startsWith(lower));
29
+ if (matches.length === 0) {
30
+ log.error('Totem Error', `No rule found matching '${id}'`);
31
+ return null;
32
+ }
33
+ if (matches.length > 1) {
34
+ log.warn(TAG, `Ambiguous prefix '${id}' matches ${matches.length} rules:`);
35
+ for (const m of matches) {
36
+ log.info(TAG, ` ${bold(m.lessonHash)} \u2014 ${m.lessonHeading}`);
37
+ }
38
+ log.dim(TAG, 'Provide more characters to disambiguate.');
39
+ return null;
40
+ }
41
+ return matches[0];
42
+ }
43
+ // ─── Subcommands ───────────────────────────────────────
44
+ export async function ruleListCommand() {
45
+ const { log, dim, bold } = await import('../ui.js');
46
+ const { rules } = await loadRulesOrExit();
47
+ if (rules.length === 0) {
48
+ log.error('Totem Error', 'No compiled rules found. Run `totem compile` first.');
49
+ return;
50
+ }
51
+ // Table header
52
+ const hashW = 10;
53
+ const engineW = 10;
54
+ const sevW = 9;
55
+ const globW = 7;
56
+ const headingW = 50;
57
+ console.error(dim(` ${'HASH'.padEnd(hashW)}${'ENGINE'.padEnd(engineW)}${'SEVERITY'.padEnd(sevW)}${'GLOBS'.padEnd(globW)}HEADING`));
58
+ console.error(dim(' ' + '\u2500'.repeat(hashW + engineW + sevW + globW + headingW)));
59
+ for (const rule of rules) {
60
+ const hash = rule.lessonHash.slice(0, 8).padEnd(hashW);
61
+ const engine = (rule.engine ?? 'regex').padEnd(engineW);
62
+ const severity = (rule.severity ?? 'warning').padEnd(sevW);
63
+ const globs = String(rule.fileGlobs?.length ?? 0).padEnd(globW);
64
+ const heading = truncate(rule.lessonHeading, headingW);
65
+ console.error(` ${hash}${engine}${severity}${globs}${heading}`);
66
+ }
67
+ console.error('');
68
+ log.info(TAG, `${bold(String(rules.length))} rule(s) total`);
69
+ }
70
+ export async function ruleInspectCommand(id) {
71
+ const { log, bold, dim } = await import('../ui.js');
72
+ const { rules } = await loadRulesOrExit();
73
+ if (rules.length === 0) {
74
+ log.error('Totem Error', 'No compiled rules found. Run `totem compile` first.');
75
+ return;
76
+ }
77
+ const rule = resolveRuleByPrefix(rules, id, log, bold);
78
+ if (!rule)
79
+ return;
80
+ console.error('');
81
+ log.info(TAG, `${bold('Hash:')} ${rule.lessonHash}`);
82
+ log.info(TAG, `${bold('Heading:')} ${rule.lessonHeading}`);
83
+ log.info(TAG, `${bold('Engine:')} ${rule.engine}`);
84
+ log.info(TAG, `${bold('Severity:')} ${rule.severity ?? 'warning'}`);
85
+ log.info(TAG, `${bold('Message:')} ${rule.message}`);
86
+ if (rule.pattern) {
87
+ log.info(TAG, `${bold('Pattern:')} ${dim(rule.pattern)}`);
88
+ }
89
+ if (rule.astQuery) {
90
+ log.info(TAG, `${bold('AST Query:')} ${dim(rule.astQuery)}`);
91
+ }
92
+ if (rule.astGrepPattern) {
93
+ const display = typeof rule.astGrepPattern === 'string'
94
+ ? rule.astGrepPattern
95
+ : JSON.stringify(rule.astGrepPattern);
96
+ log.info(TAG, `${bold('AST Grep:')} ${dim(display)}`);
97
+ }
98
+ if (rule.fileGlobs && rule.fileGlobs.length > 0) {
99
+ log.info(TAG, `${bold('File Globs:')} ${rule.fileGlobs.join(', ')}`);
100
+ }
101
+ if (rule.compiledAt) {
102
+ log.info(TAG, `${bold('Compiled:')} ${rule.compiledAt}`);
103
+ }
104
+ if (rule.createdAt) {
105
+ log.info(TAG, `${bold('Created:')} ${rule.createdAt}`);
106
+ }
107
+ console.error('');
108
+ }
109
+ export async function ruleTestCommand(id) {
110
+ const { log, bold, errorColor, success: successColor } = await import('../ui.js');
111
+ const { hashLesson, readAllLessons, extractRuleExamples, verifyRuleExamples, formatExampleFailure, } = await import('@mmnto/totem');
112
+ const { rules, totemDir } = await loadRulesOrExit();
113
+ if (rules.length === 0) {
114
+ log.error('Totem Error', 'No compiled rules found. Run `totem compile` first.');
115
+ return;
116
+ }
117
+ const rule = resolveRuleByPrefix(rules, id, log, bold);
118
+ if (!rule)
119
+ return;
120
+ // Find source lesson — search all lessons for one whose hash matches the rule's lessonHash
121
+ const lessons = readAllLessons(totemDir);
122
+ const lesson = lessons.find((l) => hashLesson(l.heading, l.body) === rule.lessonHash);
123
+ if (!lesson) {
124
+ log.warn(TAG, `Source lesson for rule ${bold(rule.lessonHash)} not found in .totem/lessons/`);
125
+ log.dim(TAG, 'The lesson may have been removed or the hash may have changed.');
126
+ return;
127
+ }
128
+ // Check for Example Hit/Miss
129
+ const examples = extractRuleExamples(lesson.body);
130
+ if (!examples) {
131
+ log.warn(TAG, 'No Example Hit/Miss found in lesson for this rule. Add examples to test.');
132
+ return;
133
+ }
134
+ // Run verification using the same logic as compile
135
+ const result = verifyRuleExamples(rule, lesson.body);
136
+ if (!result) {
137
+ // verifyRuleExamples returns null for non-regex engines with no examples
138
+ log.warn(TAG, `Engine '${rule.engine}' does not support inline example testing.`);
139
+ return;
140
+ }
141
+ console.error('');
142
+ if (result.passed) {
143
+ const label = successColor(bold('PASS'));
144
+ log.info(TAG, `${label} \u2014 ${rule.lessonHeading}`);
145
+ log.dim(TAG, `${examples.hits.length} hit(s), ${examples.misses.length} miss(es) verified`);
146
+ }
147
+ else {
148
+ const label = errorColor(bold('FAIL'));
149
+ log.info(TAG, `${label} \u2014 ${rule.lessonHeading}`);
150
+ log.warn(TAG, formatExampleFailure(result));
151
+ }
152
+ console.error('');
153
+ }
154
+ //# sourceMappingURL=rule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.js","sourceRoot":"","sources":["../../src/commands/rule.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=rule.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.test.d.ts","sourceRoot":"","sources":["../../src/commands/rule.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,315 @@
1
+ import * as fs from 'node:fs';
2
+ import * as os from 'node:os';
3
+ import * as path from 'node:path';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { cleanTmpDir } from '../test-utils.js';
6
+ // ─── Mock utils to bypass real config loading ───────────
7
+ vi.mock('../utils.js', async () => {
8
+ const actual = await vi.importActual('../utils.js');
9
+ return {
10
+ ...actual,
11
+ resolveConfigPath: (cwd) => path.join(cwd, 'totem.config.ts'),
12
+ loadConfig: async () => ({
13
+ targets: [],
14
+ totemDir: '.totem',
15
+ ignorePatterns: [],
16
+ }),
17
+ };
18
+ });
19
+ // ─── Helpers ────────────────────────────────────────────
20
+ /** Strip ANSI escape codes for assertion matching. */
21
+ function stripAnsi(str) {
22
+ return str.replace(/\x1b\[[0-9;]*m/g, ''); // totem-context: ANSI regex — not user input
23
+ }
24
+ function makeTmpDir() {
25
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'totem-rule-'));
26
+ }
27
+ /** Write a minimal compiled-rules.json with the given rules. */
28
+ function writeRules(totemDir, rules) {
29
+ const rulesPath = path.join(totemDir, 'compiled-rules.json');
30
+ fs.writeFileSync(rulesPath, JSON.stringify({ version: 1, rules }), 'utf-8');
31
+ }
32
+ /** Scaffold a .totem directory with config, lessons dir, and optional rules. */
33
+ function scaffold(cwd, rules) {
34
+ const totemDir = path.join(cwd, '.totem');
35
+ const lessonsDir = path.join(totemDir, 'lessons');
36
+ fs.mkdirSync(lessonsDir, { recursive: true });
37
+ fs.writeFileSync(path.join(cwd, 'totem.config.ts'), 'export default {};', 'utf-8');
38
+ if (rules) {
39
+ writeRules(totemDir, rules);
40
+ }
41
+ return { totemDir, lessonsDir };
42
+ }
43
+ /**
44
+ * Compute the hash a rule would get after parseLessonsFile processes it.
45
+ * parseLessonsFile strips the **Tags:** line from the body before hashing,
46
+ * so we replicate that here to get a matching hash.
47
+ */
48
+ async function computeLessonHash(heading, bodyAfterTags) {
49
+ const { hashLesson } = await import('@mmnto/totem');
50
+ return hashLesson(heading, bodyAfterTags);
51
+ }
52
+ const SAMPLE_RULES = [
53
+ {
54
+ lessonHash: 'abcd1234abcd1234',
55
+ lessonHeading: 'Always use strict equality',
56
+ pattern: '===?\\s',
57
+ message: 'Use === instead of ==',
58
+ engine: 'regex',
59
+ severity: 'error',
60
+ compiledAt: '2026-01-01T00:00:00.000Z',
61
+ createdAt: '2025-12-01T00:00:00.000Z',
62
+ fileGlobs: ['**/*.ts', '**/*.js'],
63
+ },
64
+ {
65
+ lessonHash: 'efgh5678efgh5678',
66
+ lessonHeading: 'Avoid console.log in production code that ships to users',
67
+ pattern: 'console\\.log',
68
+ message: 'Remove console.log statements',
69
+ engine: 'regex',
70
+ severity: 'warning',
71
+ compiledAt: '2026-01-02T00:00:00.000Z',
72
+ fileGlobs: ['**/*.ts'],
73
+ },
74
+ {
75
+ lessonHash: 'abcd9999abcd9999',
76
+ lessonHeading: 'No var declarations',
77
+ pattern: '\\bvar\\b',
78
+ message: 'Use const or let instead of var',
79
+ engine: 'regex',
80
+ severity: 'warning',
81
+ compiledAt: '2026-01-03T00:00:00.000Z',
82
+ },
83
+ ];
84
+ // ─── Tests ──────────────────────────────────────────────
85
+ describe('rule list', () => {
86
+ let tmpDir;
87
+ let originalCwd;
88
+ beforeEach(() => {
89
+ tmpDir = makeTmpDir();
90
+ originalCwd = process.cwd();
91
+ process.chdir(tmpDir);
92
+ });
93
+ afterEach(() => {
94
+ process.chdir(originalCwd);
95
+ cleanTmpDir(tmpDir);
96
+ vi.restoreAllMocks();
97
+ });
98
+ it('outputs correct count and table format', async () => {
99
+ scaffold(tmpDir, SAMPLE_RULES);
100
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
101
+ const { ruleListCommand } = await import('./rule.js');
102
+ await ruleListCommand();
103
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
104
+ expect(output).toContain('3 rule(s) total');
105
+ expect(output).toContain('abcd1234');
106
+ expect(output).toContain('efgh5678');
107
+ expect(output).toContain('regex');
108
+ expect(output).toContain('error');
109
+ expect(output).toContain('warning');
110
+ });
111
+ it('truncates long headings', async () => {
112
+ scaffold(tmpDir, [
113
+ {
114
+ ...SAMPLE_RULES[0],
115
+ lessonHeading: 'This is a very long heading that exceeds fifty characters and should be truncated properly',
116
+ },
117
+ ]);
118
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
119
+ const { ruleListCommand } = await import('./rule.js');
120
+ await ruleListCommand();
121
+ const output = consoleSpy.mock.calls.map((c) => String(c[0])).join('\n');
122
+ // Should be truncated with ellipsis
123
+ expect(output).toContain('\u2026');
124
+ // Should NOT contain the full heading
125
+ expect(output).not.toContain('truncated properly');
126
+ });
127
+ it('shows error when no compiled-rules.json exists', async () => {
128
+ scaffold(tmpDir); // no rules
129
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
130
+ const { ruleListCommand } = await import('./rule.js');
131
+ await ruleListCommand();
132
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
133
+ expect(output).toContain('No compiled rules found');
134
+ expect(output).toContain('totem compile');
135
+ });
136
+ });
137
+ describe('rule inspect', () => {
138
+ let tmpDir;
139
+ let originalCwd;
140
+ beforeEach(() => {
141
+ tmpDir = makeTmpDir();
142
+ originalCwd = process.cwd();
143
+ process.chdir(tmpDir);
144
+ });
145
+ afterEach(() => {
146
+ process.chdir(originalCwd);
147
+ cleanTmpDir(tmpDir);
148
+ vi.restoreAllMocks();
149
+ });
150
+ it('finds rule by full hash', async () => {
151
+ scaffold(tmpDir, SAMPLE_RULES);
152
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
153
+ const { ruleInspectCommand } = await import('./rule.js');
154
+ await ruleInspectCommand('abcd1234abcd1234');
155
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
156
+ expect(output).toContain('abcd1234abcd1234');
157
+ expect(output).toContain('Always use strict equality');
158
+ expect(output).toContain('regex');
159
+ expect(output).toContain('error');
160
+ expect(output).toContain('**/*.ts');
161
+ });
162
+ it('finds rule by prefix', async () => {
163
+ scaffold(tmpDir, SAMPLE_RULES);
164
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
165
+ const { ruleInspectCommand } = await import('./rule.js');
166
+ await ruleInspectCommand('efgh');
167
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
168
+ expect(output).toContain('efgh5678efgh5678');
169
+ expect(output).toContain('console.log');
170
+ });
171
+ it('errors on no match', async () => {
172
+ scaffold(tmpDir, SAMPLE_RULES);
173
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
174
+ const { ruleInspectCommand } = await import('./rule.js');
175
+ await ruleInspectCommand('zzzz');
176
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
177
+ expect(output).toContain("No rule found matching 'zzzz'");
178
+ });
179
+ it('reports ambiguous prefix', async () => {
180
+ scaffold(tmpDir, SAMPLE_RULES);
181
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
182
+ // 'abcd' matches both abcd1234... and abcd9999...
183
+ const { ruleInspectCommand } = await import('./rule.js');
184
+ await ruleInspectCommand('abcd');
185
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
186
+ expect(output).toContain('Ambiguous prefix');
187
+ expect(output).toContain('abcd1234abcd1234');
188
+ expect(output).toContain('abcd9999abcd9999');
189
+ });
190
+ });
191
+ describe('rule test', () => {
192
+ let tmpDir;
193
+ let originalCwd;
194
+ beforeEach(() => {
195
+ tmpDir = makeTmpDir();
196
+ originalCwd = process.cwd();
197
+ process.chdir(tmpDir);
198
+ });
199
+ afterEach(() => {
200
+ process.chdir(originalCwd);
201
+ cleanTmpDir(tmpDir);
202
+ vi.restoreAllMocks();
203
+ });
204
+ it('passes when examples match correctly', async () => {
205
+ const heading = 'Catch console log usage';
206
+ // Body as parseLessonsFile would produce it (tags stripped)
207
+ const bodyAfterTags = [
208
+ 'Do not use console.log in production.',
209
+ '',
210
+ '**Pattern:** `console\\.log`',
211
+ '**Example Hit:** `console.log("debug")`',
212
+ '**Example Miss:** `logger.info("debug")`',
213
+ ].join('\n');
214
+ const realHash = await computeLessonHash(heading, bodyAfterTags);
215
+ const rule = {
216
+ lessonHash: realHash,
217
+ lessonHeading: heading,
218
+ pattern: 'console\\.log',
219
+ message: 'Remove console.log statements',
220
+ engine: 'regex',
221
+ severity: 'error',
222
+ compiledAt: '2026-01-01T00:00:00.000Z',
223
+ };
224
+ const { lessonsDir } = scaffold(tmpDir, [rule]);
225
+ // Write the lesson file — parseLessonsFile will extract heading and body
226
+ const lessonContent = [
227
+ `## Lesson \u2014 ${heading}`,
228
+ '',
229
+ '**Tags:** best-practice',
230
+ '',
231
+ bodyAfterTags,
232
+ '',
233
+ ].join('\n');
234
+ fs.writeFileSync(path.join(lessonsDir, 'lesson-test.md'), lessonContent, 'utf-8');
235
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
236
+ const { ruleTestCommand } = await import('./rule.js');
237
+ await ruleTestCommand(realHash);
238
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
239
+ expect(output).toContain('PASS');
240
+ });
241
+ it('reports failure when examples do not match', async () => {
242
+ const heading = 'Catch console log';
243
+ const bodyAfterTags = [
244
+ 'Remove console.log statements.',
245
+ '',
246
+ '**Pattern:** `console\\.log`',
247
+ '**Example Hit:** `console.log("test")`',
248
+ '**Example Miss:** `logger.info("test")`',
249
+ ].join('\n');
250
+ const realHash = await computeLessonHash(heading, bodyAfterTags);
251
+ // Deliberately wrong pattern so the example hit will NOT match
252
+ const rule = {
253
+ lessonHash: realHash,
254
+ lessonHeading: heading,
255
+ pattern: 'NOMATCH_PATTERN_XYZZY',
256
+ message: 'Remove console.log',
257
+ engine: 'regex',
258
+ severity: 'warning',
259
+ compiledAt: '2026-01-01T00:00:00.000Z',
260
+ };
261
+ const { lessonsDir } = scaffold(tmpDir, [rule]);
262
+ const lessonContent = [
263
+ `## Lesson \u2014 ${heading}`,
264
+ '',
265
+ '**Tags:** lint',
266
+ '',
267
+ bodyAfterTags,
268
+ '',
269
+ ].join('\n');
270
+ fs.writeFileSync(path.join(lessonsDir, 'lesson-test.md'), lessonContent, 'utf-8');
271
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
272
+ const { ruleTestCommand } = await import('./rule.js');
273
+ await ruleTestCommand(realHash);
274
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
275
+ expect(output).toContain('FAIL');
276
+ });
277
+ it('reports when no examples exist in lesson', async () => {
278
+ const heading = 'No examples lesson';
279
+ const bodyAfterTags = 'This lesson has no Example Hit/Miss lines.';
280
+ const realHash = await computeLessonHash(heading, bodyAfterTags);
281
+ const rule = {
282
+ lessonHash: realHash,
283
+ lessonHeading: heading,
284
+ pattern: 'something',
285
+ message: 'Some rule',
286
+ engine: 'regex',
287
+ severity: 'warning',
288
+ compiledAt: '2026-01-01T00:00:00.000Z',
289
+ };
290
+ const { lessonsDir } = scaffold(tmpDir, [rule]);
291
+ const lessonContent = [
292
+ `## Lesson \u2014 ${heading}`,
293
+ '',
294
+ '**Tags:** misc',
295
+ '',
296
+ bodyAfterTags,
297
+ '',
298
+ ].join('\n');
299
+ fs.writeFileSync(path.join(lessonsDir, 'lesson-test.md'), lessonContent, 'utf-8');
300
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
301
+ const { ruleTestCommand } = await import('./rule.js');
302
+ await ruleTestCommand(realHash);
303
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
304
+ expect(output).toContain('No Example Hit/Miss');
305
+ });
306
+ it('shows error when no compiled-rules.json exists', async () => {
307
+ scaffold(tmpDir); // no rules
308
+ const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
309
+ const { ruleTestCommand } = await import('./rule.js');
310
+ await ruleTestCommand('abcd');
311
+ const output = stripAnsi(consoleSpy.mock.calls.map((c) => String(c[0])).join('\n'));
312
+ expect(output).toContain('No compiled rules found');
313
+ });
314
+ });
315
+ //# sourceMappingURL=rule.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule.test.js","sourceRoot":"","sources":["../../src/commands/rule.test.ts"],"names":[],"mappings":"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"}
@@ -475,7 +475,7 @@ async function handleVerdictResult(content, diff, options, config, cwd, configRo
475
475
  if (options.learn) {
476
476
  await learnFromVerdict(JSON.stringify(structured, null, 2), diff, options, config, cwd, configRoot);
477
477
  }
478
- throw new TotemError('SHIELD_FAILED', `Shield ${modeLabel} review failed: ${verdict.reason}`, 'Fix the issues identified in the review above, then re-run `totem shield`.');
478
+ throw new TotemError('SHIELD_FAILED', `Shield ${modeLabel} review failed: ${verdict.reason}`, 'Fix the issues identified in the review above, then re-run `totem review`.');
479
479
  }
480
480
  return;
481
481
  }
@@ -506,7 +506,7 @@ async function handleVerdictResult(content, diff, options, config, cwd, configRo
506
506
  else {
507
507
  if (options.learn)
508
508
  await learnFromVerdict(content, diff, options, config, cwd, configRoot);
509
- throw new TotemError('SHIELD_FAILED', `Shield ${modeLabel} review failed: ${verdict.reason || 'no reason given'}`, 'Fix the issues identified in the review above, then re-run `totem shield`.');
509
+ throw new TotemError('SHIELD_FAILED', `Shield ${modeLabel} review failed: ${verdict.reason || 'no reason given'}`, 'Fix the issues identified in the review above, then re-run `totem review`.');
510
510
  }
511
511
  }
512
512
  else {
@@ -581,16 +581,16 @@ export async function shieldCommand(options) {
581
581
  const { classifyChangedFiles } = await import('./shield-classify.js');
582
582
  const { extractShieldContextAnnotations, extractShieldHints } = await import('./shield-hints.js');
583
583
  if (options.mode && options.mode !== 'standard' && options.mode !== 'structural') {
584
- throw new TotemConfigError(`Invalid --mode "${options.mode}". Use "standard" or "structural".`, 'Check `totem shield --help` for valid options.', 'CONFIG_INVALID');
584
+ throw new TotemConfigError(`Invalid --mode "${options.mode}". Use "standard" or "structural".`, 'Check `totem review --help` for valid options.', 'CONFIG_INVALID');
585
585
  }
586
586
  if (options.override !== undefined && options.override.length < 10) {
587
587
  throw new TotemConfigError(`--override reason must be at least 10 characters (got ${options.override.length}).`, 'Provide a meaningful justification, e.g., --override "False positive: onWarn param visible at line 273"', 'CONFIG_INVALID');
588
588
  }
589
589
  const cwd = process.cwd();
590
- // Silently upgrade the pre-push hook if it lacks shield auto-refresh (#1045)
590
+ // Silently upgrade the pre-push hook if it lacks review auto-refresh (#1045)
591
591
  const { upgradePrePushHookIfNeeded } = await import('./install-hooks.js');
592
592
  if (upgradePrePushHookIfNeeded(cwd)) {
593
- log.dim(TAG, 'Upgraded pre-push hook with shield auto-refresh');
593
+ log.dim(TAG, 'Upgraded pre-push hook with review auto-refresh');
594
594
  }
595
595
  const configPath = resolveConfigPath(cwd);
596
596
  const configRoot = path.dirname(configPath);
@@ -1,3 +1,3 @@
1
- export declare const SYSTEM_PROMPT = "# Spec System Prompt \u2014 Pre-Work Briefing\n\n## Identity & Role\nYou are a Staff-Level Software Architect. You do not write the implementation code yourself; your job is to guide developers. You design system interactions, define data contracts, identify architectural traps, and ensure the proposed plan aligns with existing project patterns.\n\n## Core Mission\nProduce a structured, highly technical pre-work briefing for a task before implementation begins, drawing heavily on provided Totem knowledge to ensure architectural consistency.\n\n## Critical Rules\n- **No Implementation Generation:** Do not write the final code. Provide architectural guidance, sequence logic, and structural plans.\n- **Define Contracts:** Explicitly define data contracts (e.g., Zod schemas, DB migrations, API interfaces) needed for the feature.\n- **Pessimistic Edge Cases:** Actively search for edge cases the issue description failed to mention (e.g., race conditions, missing indexes).\n- **Grounded Reality:** File paths must reference actual files from the context provided. When multiple approaches exist, list trade-offs with a firm recommendation.\n- **Lessons Are Law:** If RELEVANT LESSONS are provided, treat each lesson as a hard architectural constraint. Your plan MUST account for every relevant lesson. Call out which lessons influenced your approach in the Architectural Context section.\n\n## Output Format\nRespond with ONLY the sections below. No preamble, no closing remarks.\n\n### Problem Statement\n[1-2 sentences restating the issue in concrete implementation terms. What exactly needs to change?]\n\n### Architectural Context\n[Relevant sessions, PRs, decisions, or past traps from the provided Totem knowledge. If nothing relevant, say \"None found in provided context.\"]\n\n### Files to Examine\n[Ordered list of files the developer should read before starting. Most critical first. Format: `path/to/file.ts` \u2014 reason to examine]\n\n### Technical Approach & Contracts\n[Recommended implementation approach. Include concrete steps, sequence logic, and required data contract changes (e.g., schemas, types). If multiple valid approaches exist, list trade-offs with a clear recommendation.]\n\n### Edge Cases & Traps\n[Things the issue description missed. Include race conditions, existing patterns that MUST be followed, and potential architectural regressions.]\n\n### Implementation Tasks\n[Break the work into discrete, ordered checkbox tasks. Each task should be completable in 5-15 minutes. Format each as `- [ ] **Task N: Title**` followed by indented steps.\n\nFor each task:\n- Name the files to modify and the test files to update\n- If a retrieved Totem lesson applies to THIS SPECIFIC TASK, inject it inline as:\n > TOTEM INVARIANT ([lesson heading]): [one-line constraint summary]\n Place the invariant directly above the step it constrains, not in a separate section.\n- If the task introduces behavior that could regress, add a TDD directive:\n > TEST DIRECTIVE: Before implementing, write a failing test named `[descriptive test name]` that proves the regression is caught.\n The test name must be specific (e.g., `rejects empty catch blocks`), not generic (e.g., `works correctly`).\n- Each task ends with: write test (or update existing) \u2192 verify fails \u2192 implement \u2192 verify passes \u2192 lint\n\nRED FLAGS \u2014 if any of these occur, STOP and fix before proceeding:\n- Never move to the next task until the current task's tests pass AND lint is clean.\n- Never accept \"close enough\" on a failing test. Fix it or rewrite the approach.\n- Never skip the test step. No untested code advances to the next task.\n- Never write code before writing the failing test (TDD is mandatory, not advisory).]\n\n### Execution Flow (structural constraint)\n```dot\ndigraph workflow {\n spec -> write_test -> verify_fails -> implement -> verify_passes -> lint -> next_task\n verify_fails -> implement [label=\"RED only\"]\n verify_passes -> lint [label=\"GREEN required\"]\n lint -> next_task [label=\"0 violations\"]\n lint -> implement [label=\"violations found \u2014 fix first\"]\n}\n```\n\n### Verification (MANDATORY \u2014 do not skip)\nEvery implementation MUST end with these steps:\n1. `totem lint` \u2014 deterministic rule check (zero LLM, ~2s). Fixes any violations.\n2. `totem shield` \u2014 AI-powered architectural review (~18s). Addresses any critical findings.\n3. If using MCP, call `verify_execution` to confirm compliance before declaring the task done.\n\n### Test Plan\n[Specific test scenarios needed to prove the feature works and edge cases are handled. Reference existing test file patterns when applicable.]\n";
1
+ export declare const SYSTEM_PROMPT = "# Spec System Prompt \u2014 Pre-Work Briefing\n\n## Identity & Role\nYou are a Staff-Level Software Architect. You do not write the implementation code yourself; your job is to guide developers. You design system interactions, define data contracts, identify architectural traps, and ensure the proposed plan aligns with existing project patterns.\n\n## Core Mission\nProduce a structured, highly technical pre-work briefing for a task before implementation begins, drawing heavily on provided Totem knowledge to ensure architectural consistency.\n\n## Critical Rules\n- **No Implementation Generation:** Do not write the final code. Provide architectural guidance, sequence logic, and structural plans.\n- **Define Contracts:** Explicitly define data contracts (e.g., Zod schemas, DB migrations, API interfaces) needed for the feature.\n- **Pessimistic Edge Cases:** Actively search for edge cases the issue description failed to mention (e.g., race conditions, missing indexes).\n- **Grounded Reality:** File paths must reference actual files from the context provided. When multiple approaches exist, list trade-offs with a firm recommendation.\n- **Lessons Are Law:** If RELEVANT LESSONS are provided, treat each lesson as a hard architectural constraint. Your plan MUST account for every relevant lesson. Call out which lessons influenced your approach in the Architectural Context section.\n\n## Output Format\nRespond with ONLY the sections below. No preamble, no closing remarks.\n\n### Problem Statement\n[1-2 sentences restating the issue in concrete implementation terms. What exactly needs to change?]\n\n### Architectural Context\n[Relevant sessions, PRs, decisions, or past traps from the provided Totem knowledge. If nothing relevant, say \"None found in provided context.\"]\n\n### Files to Examine\n[Ordered list of files the developer should read before starting. Most critical first. Format: `path/to/file.ts` \u2014 reason to examine]\n\n### Technical Approach & Contracts\n[Recommended implementation approach. Include concrete steps, sequence logic, and required data contract changes (e.g., schemas, types). If multiple valid approaches exist, list trade-offs with a clear recommendation.]\n\n### Edge Cases & Traps\n[Things the issue description missed. Include race conditions, existing patterns that MUST be followed, and potential architectural regressions.]\n\n### Implementation Tasks\n[Break the work into discrete, ordered checkbox tasks. Each task should be completable in 5-15 minutes. Format each as `- [ ] **Task N: Title**` followed by indented steps.\n\nFor each task:\n- Name the files to modify and the test files to update\n- If a retrieved Totem lesson applies to THIS SPECIFIC TASK, inject it inline as:\n > TOTEM INVARIANT ([lesson heading]): [one-line constraint summary]\n Place the invariant directly above the step it constrains, not in a separate section.\n- If the task introduces behavior that could regress, add a TDD directive:\n > TEST DIRECTIVE: Before implementing, write a failing test named `[descriptive test name]` that proves the regression is caught.\n The test name must be specific (e.g., `rejects empty catch blocks`), not generic (e.g., `works correctly`).\n- Each task ends with: write test (or update existing) \u2192 verify fails \u2192 implement \u2192 verify passes \u2192 lint\n\nRED FLAGS \u2014 if any of these occur, STOP and fix before proceeding:\n- Never move to the next task until the current task's tests pass AND lint is clean.\n- Never accept \"close enough\" on a failing test. Fix it or rewrite the approach.\n- Never skip the test step. No untested code advances to the next task.\n- Never write code before writing the failing test (TDD is mandatory, not advisory).]\n\n### Execution Flow (structural constraint)\n```dot\ndigraph workflow {\n spec -> write_test -> verify_fails -> implement -> verify_passes -> lint -> next_task\n verify_fails -> implement [label=\"RED only\"]\n verify_passes -> lint [label=\"GREEN required\"]\n lint -> next_task [label=\"0 violations\"]\n lint -> implement [label=\"violations found \u2014 fix first\"]\n}\n```\n\n### Verification (MANDATORY \u2014 do not skip)\nEvery implementation MUST end with these steps:\n1. `totem lint` \u2014 deterministic rule check (zero LLM, ~2s). Fixes any violations.\n2. `totem review` \u2014 AI-powered architectural review (~18s). Addresses any critical findings.\n3. If using MCP, call `verify_execution` to confirm compliance before declaring the task done.\n\n### Test Plan\n[Specific test scenarios needed to prove the feature works and edge cases are handled. Reference existing test file patterns when applicable.]\n";
2
2
  export { SYSTEM_PROMPT as SPEC_SYSTEM_PROMPT };
3
3
  //# sourceMappingURL=spec-templates.d.ts.map