@mycodemap/mycodemap 0.5.0 → 0.5.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.
Files changed (199) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +77 -9
  3. package/dist/cli/commands/analyze.d.ts +18 -0
  4. package/dist/cli/commands/analyze.d.ts.map +1 -1
  5. package/dist/cli/commands/analyze.js +239 -6
  6. package/dist/cli/commands/analyze.js.map +1 -1
  7. package/dist/cli/commands/check.d.ts +22 -0
  8. package/dist/cli/commands/check.d.ts.map +1 -0
  9. package/dist/cli/commands/check.js +168 -0
  10. package/dist/cli/commands/check.js.map +1 -0
  11. package/dist/cli/commands/ci.d.ts +25 -0
  12. package/dist/cli/commands/ci.d.ts.map +1 -1
  13. package/dist/cli/commands/ci.js +139 -36
  14. package/dist/cli/commands/ci.js.map +1 -1
  15. package/dist/cli/commands/complexity.d.ts.map +1 -1
  16. package/dist/cli/commands/complexity.js +6 -0
  17. package/dist/cli/commands/complexity.js.map +1 -1
  18. package/dist/cli/commands/design.d.ts +5 -0
  19. package/dist/cli/commands/design.d.ts.map +1 -1
  20. package/dist/cli/commands/design.js +6 -0
  21. package/dist/cli/commands/design.js.map +1 -1
  22. package/dist/cli/commands/generate.d.ts +1 -0
  23. package/dist/cli/commands/generate.d.ts.map +1 -1
  24. package/dist/cli/commands/generate.js +121 -8
  25. package/dist/cli/commands/generate.js.map +1 -1
  26. package/dist/cli/commands/history.d.ts +26 -0
  27. package/dist/cli/commands/history.d.ts.map +1 -0
  28. package/dist/cli/commands/history.js +92 -0
  29. package/dist/cli/commands/history.js.map +1 -0
  30. package/dist/cli/commands/mcp.d.ts +13 -0
  31. package/dist/cli/commands/mcp.d.ts.map +1 -0
  32. package/dist/cli/commands/mcp.js +108 -0
  33. package/dist/cli/commands/mcp.js.map +1 -0
  34. package/dist/cli/commands/workflow.d.ts.map +1 -1
  35. package/dist/cli/commands/workflow.js +22 -2
  36. package/dist/cli/commands/workflow.js.map +1 -1
  37. package/dist/cli/config-loader.d.ts.map +1 -1
  38. package/dist/cli/config-loader.js +3 -2
  39. package/dist/cli/config-loader.js.map +1 -1
  40. package/dist/cli/contract-checker.d.ts +33 -0
  41. package/dist/cli/contract-checker.d.ts.map +1 -0
  42. package/dist/cli/contract-checker.js +719 -0
  43. package/dist/cli/contract-checker.js.map +1 -0
  44. package/dist/cli/contract-diff-scope.d.ts +14 -0
  45. package/dist/cli/contract-diff-scope.d.ts.map +1 -0
  46. package/dist/cli/contract-diff-scope.js +127 -0
  47. package/dist/cli/contract-diff-scope.js.map +1 -0
  48. package/dist/cli/contract-gate-thresholds.d.ts +14 -0
  49. package/dist/cli/contract-gate-thresholds.d.ts.map +1 -0
  50. package/dist/cli/contract-gate-thresholds.js +19 -0
  51. package/dist/cli/contract-gate-thresholds.js.map +1 -0
  52. package/dist/cli/design-contract-loader.d.ts.map +1 -1
  53. package/dist/cli/design-contract-loader.js +355 -3
  54. package/dist/cli/design-contract-loader.js.map +1 -1
  55. package/dist/cli/design-scope-resolver.d.ts.map +1 -1
  56. package/dist/cli/design-scope-resolver.js +89 -41
  57. package/dist/cli/design-scope-resolver.js.map +1 -1
  58. package/dist/cli/index.js +18 -6
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/cli/paths.d.ts.map +1 -1
  61. package/dist/cli/paths.js +30 -7
  62. package/dist/cli/paths.js.map +1 -1
  63. package/dist/core/analyzer.d.ts.map +1 -1
  64. package/dist/core/analyzer.js +16 -0
  65. package/dist/core/analyzer.js.map +1 -1
  66. package/dist/domain/entities/CodeGraph.d.ts +5 -1
  67. package/dist/domain/entities/CodeGraph.d.ts.map +1 -1
  68. package/dist/domain/entities/CodeGraph.js +29 -12
  69. package/dist/domain/entities/CodeGraph.js.map +1 -1
  70. package/dist/domain/entities/Dependency.d.ts +8 -1
  71. package/dist/domain/entities/Dependency.d.ts.map +1 -1
  72. package/dist/domain/entities/Dependency.js +19 -4
  73. package/dist/domain/entities/Dependency.js.map +1 -1
  74. package/dist/domain/entities/Symbol.d.ts +2 -1
  75. package/dist/domain/entities/Symbol.d.ts.map +1 -1
  76. package/dist/domain/entities/Symbol.js +6 -3
  77. package/dist/domain/entities/Symbol.js.map +1 -1
  78. package/dist/infrastructure/storage/StorageFactory.d.ts +1 -0
  79. package/dist/infrastructure/storage/StorageFactory.d.ts.map +1 -1
  80. package/dist/infrastructure/storage/StorageFactory.js +7 -2
  81. package/dist/infrastructure/storage/StorageFactory.js.map +1 -1
  82. package/dist/infrastructure/storage/adapters/FileSystemStorage.d.ts +3 -1
  83. package/dist/infrastructure/storage/adapters/FileSystemStorage.d.ts.map +1 -1
  84. package/dist/infrastructure/storage/adapters/FileSystemStorage.js +10 -2
  85. package/dist/infrastructure/storage/adapters/FileSystemStorage.js.map +1 -1
  86. package/dist/infrastructure/storage/adapters/KuzuDBStorage.d.ts +3 -1
  87. package/dist/infrastructure/storage/adapters/KuzuDBStorage.d.ts.map +1 -1
  88. package/dist/infrastructure/storage/adapters/KuzuDBStorage.js +9 -1
  89. package/dist/infrastructure/storage/adapters/KuzuDBStorage.js.map +1 -1
  90. package/dist/infrastructure/storage/adapters/MemoryStorage.d.ts +3 -1
  91. package/dist/infrastructure/storage/adapters/MemoryStorage.d.ts.map +1 -1
  92. package/dist/infrastructure/storage/adapters/MemoryStorage.js +9 -1
  93. package/dist/infrastructure/storage/adapters/MemoryStorage.js.map +1 -1
  94. package/dist/infrastructure/storage/adapters/SQLiteStorage.d.ts +53 -0
  95. package/dist/infrastructure/storage/adapters/SQLiteStorage.d.ts.map +1 -0
  96. package/dist/infrastructure/storage/adapters/SQLiteStorage.js +879 -0
  97. package/dist/infrastructure/storage/adapters/SQLiteStorage.js.map +1 -0
  98. package/dist/infrastructure/storage/graph-helpers.d.ts +3 -1
  99. package/dist/infrastructure/storage/graph-helpers.d.ts.map +1 -1
  100. package/dist/infrastructure/storage/graph-helpers.js +90 -0
  101. package/dist/infrastructure/storage/graph-helpers.js.map +1 -1
  102. package/dist/infrastructure/storage/index.d.ts +1 -1
  103. package/dist/infrastructure/storage/index.d.ts.map +1 -1
  104. package/dist/infrastructure/storage/interfaces/StorageBase.d.ts +3 -1
  105. package/dist/infrastructure/storage/interfaces/StorageBase.d.ts.map +1 -1
  106. package/dist/infrastructure/storage/interfaces/StorageBase.js.map +1 -1
  107. package/dist/infrastructure/storage/sqlite/GovernanceGraphCache.d.ts +27 -0
  108. package/dist/infrastructure/storage/sqlite/GovernanceGraphCache.d.ts.map +1 -0
  109. package/dist/infrastructure/storage/sqlite/GovernanceGraphCache.js +246 -0
  110. package/dist/infrastructure/storage/sqlite/GovernanceGraphCache.js.map +1 -0
  111. package/dist/infrastructure/storage/sqlite/perf-thresholds.d.ts +25 -0
  112. package/dist/infrastructure/storage/sqlite/perf-thresholds.d.ts.map +1 -0
  113. package/dist/infrastructure/storage/sqlite/perf-thresholds.js +25 -0
  114. package/dist/infrastructure/storage/sqlite/perf-thresholds.js.map +1 -0
  115. package/dist/infrastructure/storage/sqlite/schema.d.ts +4 -0
  116. package/dist/infrastructure/storage/sqlite/schema.d.ts.map +1 -0
  117. package/dist/infrastructure/storage/sqlite/schema.js +111 -0
  118. package/dist/infrastructure/storage/sqlite/schema.js.map +1 -0
  119. package/dist/interface/types/design-check.d.ts +73 -0
  120. package/dist/interface/types/design-check.d.ts.map +1 -0
  121. package/dist/interface/types/design-check.js +4 -0
  122. package/dist/interface/types/design-check.js.map +1 -0
  123. package/dist/interface/types/design-contract.d.ts +56 -1
  124. package/dist/interface/types/design-contract.d.ts.map +1 -1
  125. package/dist/interface/types/history-risk.d.ts +90 -0
  126. package/dist/interface/types/history-risk.d.ts.map +1 -0
  127. package/dist/interface/types/history-risk.js +4 -0
  128. package/dist/interface/types/history-risk.js.map +1 -0
  129. package/dist/interface/types/index.d.ts +17 -2
  130. package/dist/interface/types/index.d.ts.map +1 -1
  131. package/dist/interface/types/storage.d.ts +28 -1
  132. package/dist/interface/types/storage.d.ts.map +1 -1
  133. package/dist/orchestrator/adapters/ast-grep-adapter.d.ts +10 -0
  134. package/dist/orchestrator/adapters/ast-grep-adapter.d.ts.map +1 -1
  135. package/dist/orchestrator/adapters/ast-grep-adapter.js +46 -17
  136. package/dist/orchestrator/adapters/ast-grep-adapter.js.map +1 -1
  137. package/dist/orchestrator/adapters/codemap-adapter.d.ts.map +1 -1
  138. package/dist/orchestrator/adapters/codemap-adapter.js +2 -22
  139. package/dist/orchestrator/adapters/codemap-adapter.js.map +1 -1
  140. package/dist/orchestrator/history-risk-service.d.ts +55 -0
  141. package/dist/orchestrator/history-risk-service.d.ts.map +1 -0
  142. package/dist/orchestrator/history-risk-service.js +680 -0
  143. package/dist/orchestrator/history-risk-service.js.map +1 -0
  144. package/dist/orchestrator/types.d.ts +19 -1
  145. package/dist/orchestrator/types.d.ts.map +1 -1
  146. package/dist/orchestrator/types.js +19 -0
  147. package/dist/orchestrator/types.js.map +1 -1
  148. package/dist/server/mcp/index.d.ts +4 -0
  149. package/dist/server/mcp/index.d.ts.map +1 -0
  150. package/dist/server/mcp/index.js +5 -0
  151. package/dist/server/mcp/index.js.map +1 -0
  152. package/dist/server/mcp/server.d.ts +17 -0
  153. package/dist/server/mcp/server.d.ts.map +1 -0
  154. package/dist/server/mcp/server.js +84 -0
  155. package/dist/server/mcp/server.js.map +1 -0
  156. package/dist/server/mcp/service.d.ts +22 -0
  157. package/dist/server/mcp/service.d.ts.map +1 -0
  158. package/dist/server/mcp/service.js +177 -0
  159. package/dist/server/mcp/service.js.map +1 -0
  160. package/dist/server/mcp/types.d.ts +56 -0
  161. package/dist/server/mcp/types.d.ts.map +1 -0
  162. package/dist/server/mcp/types.js +4 -0
  163. package/dist/server/mcp/types.js.map +1 -0
  164. package/docs/AI_ASSISTANT_SETUP.md +1 -1
  165. package/docs/SETUP_GUIDE.md +6 -6
  166. package/docs/ai-guide/COMMANDS.md +98 -4
  167. package/docs/ai-guide/INTEGRATION.md +137 -433
  168. package/docs/ai-guide/OUTPUT.md +476 -6
  169. package/docs/ai-guide/PATTERNS.md +41 -11
  170. package/docs/ai-guide/PROMPTS.md +11 -6
  171. package/docs/backlog.md +177 -0
  172. package/docs/eatdogfood-reports/2026-04-17-eatdogfood-agent-experience.md +231 -0
  173. package/docs/exec-plans/completed/2026-04-17-eatdogfood-codemap-cli.md +103 -0
  174. package/docs/ideation/2026-04-15-executable-architecture-constitution-ideation.md +102 -0
  175. package/docs/product-specs/DESIGN_CONTRACT_TEMPLATE.md +47 -0
  176. package/docs/product-specs/MVP3-ARCHITECTURE-COMPARISON.md +11 -10
  177. package/docs/product-specs/MVP3-ARCHITECTURE-REDESIGN-PRD.md +10 -10
  178. package/docs/product-specs/MVP3-ARCHITECTURE-REDESIGN-TECH-PRD.md +17 -12
  179. package/docs/rules/README.md +16 -11
  180. package/docs/rules/architecture-guardrails.md +24 -336
  181. package/docs/rules/code-quality-redlines.md +25 -311
  182. package/docs/rules/engineering-with-codex-openai.md +14 -1
  183. package/docs/rules/validation.md +90 -40
  184. package/mycodemap.config.schema.json +3 -3
  185. package/package.json +7 -2
  186. package/scripts/benchmark-governance-graph.mjs +132 -0
  187. package/scripts/calibrate-contract-gate.mjs +221 -0
  188. package/scripts/capability-report.py +255 -0
  189. package/scripts/qa-rule-control.sh +254 -0
  190. package/scripts/report-high-risk-files.mjs +395 -0
  191. package/scripts/rule-context.mjs +155 -0
  192. package/scripts/smoke-sqlite-impact.mjs +85 -0
  193. package/scripts/sync-analyze-docs.js +1 -0
  194. package/scripts/tests/test_capability_report.py +89 -0
  195. package/scripts/tests/test_rule_control_workflow.py +51 -0
  196. package/scripts/tests/test_validate_rules.py +81 -0
  197. package/scripts/validate-ai-docs.js +283 -1
  198. package/scripts/validate-docs.js +249 -42
  199. package/scripts/validate-rules.py +254 -0
@@ -0,0 +1,155 @@
1
+ #!/usr/bin/env node
2
+
3
+ import path from 'node:path';
4
+
5
+ const USAGE = 'Usage: node scripts/rule-context.mjs --files <path> [<path>...] --format json|prompt';
6
+ const DEFAULT_VERIFY_COMMANDS = ['python3 scripts/validate-rules.py code --report-only'];
7
+
8
+ const ROUTES = [
9
+ {
10
+ matches: filePath => filePath.endsWith('.test.ts'),
11
+ rules: ['docs/rules/testing.md'],
12
+ },
13
+ {
14
+ matches: filePath => /^src\/interface\//.test(filePath),
15
+ rules: ['docs/rules/architecture-guardrails.md'],
16
+ },
17
+ {
18
+ matches: filePath => /^src\/(?:cli|domain|server|infrastructure)\//.test(filePath),
19
+ rules: [
20
+ 'docs/rules/code-quality-redlines.md',
21
+ 'docs/rules/architecture-guardrails.md',
22
+ ],
23
+ },
24
+ {
25
+ matches: filePath =>
26
+ /^(?:docs\/|\.githooks\/|\.github\/workflows\/)/.test(filePath),
27
+ rules: [
28
+ 'docs/rules/validation.md',
29
+ 'docs/rules/engineering-with-codex-openai.md',
30
+ ],
31
+ },
32
+ ];
33
+
34
+ function normalizeFilePath(filePath) {
35
+ const resolved = path.isAbsolute(filePath)
36
+ ? path.relative(process.cwd(), filePath)
37
+ : filePath;
38
+
39
+ return resolved.replace(/\\/g, '/').replace(/^\.\//, '');
40
+ }
41
+
42
+ function parseArgs(argv) {
43
+ const files = [];
44
+ let format = 'json';
45
+
46
+ for (let index = 0; index < argv.length; index += 1) {
47
+ const current = argv[index];
48
+
49
+ if (current === '--files') {
50
+ index += 1;
51
+ while (index < argv.length && !argv[index].startsWith('--')) {
52
+ files.push(argv[index]);
53
+ index += 1;
54
+ }
55
+ index -= 1;
56
+ continue;
57
+ }
58
+
59
+ if (current === '--format') {
60
+ format = argv[index + 1] ?? '';
61
+ index += 1;
62
+ continue;
63
+ }
64
+
65
+ if (current === '--help' || current === '-h') {
66
+ console.log(USAGE);
67
+ process.exit(0);
68
+ }
69
+ }
70
+
71
+ if (files.length === 0 || (format !== 'json' && format !== 'prompt')) {
72
+ console.error(USAGE);
73
+ process.exit(1);
74
+ }
75
+
76
+ return {
77
+ files: files.map(normalizeFilePath),
78
+ format,
79
+ };
80
+ }
81
+
82
+ function inferScopedRules(files) {
83
+ const matchedRules = [];
84
+ const matchedFiles = [];
85
+
86
+ for (const filePath of files) {
87
+ const route = ROUTES.find(candidate => candidate.matches(filePath));
88
+ if (!route) {
89
+ continue;
90
+ }
91
+
92
+ matchedFiles.push(filePath);
93
+
94
+ for (const rulePath of route.rules) {
95
+ if (!matchedRules.includes(rulePath)) {
96
+ matchedRules.push(rulePath);
97
+ }
98
+
99
+ if (matchedRules.length === 2) {
100
+ return matchedRules.length === 0
101
+ ? { files, matchedFiles: [], matchedRules: [], verifyCommands: [] }
102
+ : {
103
+ files,
104
+ matchedFiles,
105
+ matchedRules,
106
+ verifyCommands: DEFAULT_VERIFY_COMMANDS,
107
+ };
108
+ }
109
+ }
110
+ }
111
+
112
+ return matchedRules.length === 0
113
+ ? { files, matchedFiles: [], matchedRules: [], verifyCommands: [] }
114
+ : {
115
+ files,
116
+ matchedFiles,
117
+ matchedRules,
118
+ verifyCommands: DEFAULT_VERIFY_COMMANDS,
119
+ };
120
+ }
121
+
122
+ function formatPrompt(result) {
123
+ if (result.matchedRules.length === 0) {
124
+ return 'No scoped rules inferred';
125
+ }
126
+
127
+ return [
128
+ 'Only inject matched rules:',
129
+ ...result.matchedRules.map(rulePath => `- ${rulePath}`),
130
+ '',
131
+ 'Verify after edits:',
132
+ ...result.verifyCommands.map(command => `- ${command}`),
133
+ ].join('\n');
134
+ }
135
+
136
+ const args = parseArgs(process.argv.slice(2));
137
+ const result = inferScopedRules(args.files);
138
+
139
+ if (args.format === 'prompt') {
140
+ console.log(formatPrompt(result));
141
+ process.exit(0);
142
+ }
143
+
144
+ console.log(
145
+ JSON.stringify(
146
+ {
147
+ files: result.files,
148
+ matchedFiles: result.matchedFiles,
149
+ matchedRules: result.matchedRules,
150
+ verifyCommands: result.verifyCommands,
151
+ },
152
+ null,
153
+ 2,
154
+ ),
155
+ );
@@ -0,0 +1,85 @@
1
+ import { mkdtempSync, rmSync } from 'node:fs';
2
+ import { tmpdir } from 'node:os';
3
+ import path from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+
6
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
+ const loadModule = async (relativePath) => import(pathToFileURL(path.join(repoRoot, relativePath)).href);
8
+ const { SQLiteStorage } = await loadModule('dist/infrastructure/storage/adapters/SQLiteStorage.js');
9
+ const { QueryHandler } = await loadModule('dist/server/handlers/QueryHandler.js');
10
+
11
+ function createImpactFixture() {
12
+ return {
13
+ project: {
14
+ id: 'proj-smoke',
15
+ name: 'sqlite-impact-smoke',
16
+ rootPath: '/fixture',
17
+ createdAt: new Date('2026-04-15T00:00:00Z'),
18
+ updatedAt: new Date('2026-04-15T00:00:00Z'),
19
+ },
20
+ modules: [
21
+ {
22
+ id: 'core',
23
+ projectId: 'proj-smoke',
24
+ path: 'src/core.ts',
25
+ language: 'ts',
26
+ stats: { lines: 10, codeLines: 8, commentLines: 1, blankLines: 1 },
27
+ },
28
+ {
29
+ id: 'service',
30
+ projectId: 'proj-smoke',
31
+ path: 'src/service.ts',
32
+ language: 'ts',
33
+ stats: { lines: 10, codeLines: 8, commentLines: 1, blankLines: 1 },
34
+ },
35
+ {
36
+ id: 'api',
37
+ projectId: 'proj-smoke',
38
+ path: 'src/api.ts',
39
+ language: 'ts',
40
+ stats: { lines: 10, codeLines: 8, commentLines: 1, blankLines: 1 },
41
+ },
42
+ ],
43
+ symbols: [],
44
+ dependencies: [
45
+ { id: 'dep-1', sourceId: 'service', targetId: 'core', type: 'import' },
46
+ { id: 'dep-2', sourceId: 'api', targetId: 'service', type: 'import' },
47
+ ],
48
+ };
49
+ }
50
+
51
+ const rootDir = mkdtempSync(path.join(tmpdir(), 'codemap-sqlite-impact-smoke-'));
52
+ const storage = new SQLiteStorage({ type: 'sqlite', databasePath: '.codemap/governance.sqlite' });
53
+
54
+ try {
55
+ await storage.initialize(rootDir);
56
+ await storage.saveCodeGraph(createImpactFixture());
57
+ const handler = new QueryHandler(storage);
58
+ const result = await handler.analyzeImpact({ moduleId: 'core', depth: 3 });
59
+ const stats = storage.getGovernanceGraphRuntimeStats();
60
+
61
+ if (result.rootModule !== 'core') {
62
+ throw new Error(`unexpected root module: ${result.rootModule}`);
63
+ }
64
+
65
+ if (result.totalAffected !== 2 || result.maxDepth !== 2) {
66
+ throw new Error(`unexpected impact result: ${JSON.stringify(result)}`);
67
+ }
68
+
69
+ if (stats.cacheMode !== 'memory-eager') {
70
+ throw new Error(`expected memory-eager smoke path, got ${stats.cacheMode} (${stats.warning ?? 'no warning'})`);
71
+ }
72
+
73
+ console.log(JSON.stringify({
74
+ ok: true,
75
+ smoke: 'sqlite-impact',
76
+ cacheMode: stats.cacheMode,
77
+ rootModule: result.rootModule,
78
+ affectedModules: result.affectedModules,
79
+ loadMs: stats.loadMs,
80
+ rssDeltaMb: stats.rssDeltaMb,
81
+ }, null, 2));
82
+ } finally {
83
+ await storage.close();
84
+ rmSync(rootDir, { recursive: true, force: true });
85
+ }
@@ -92,6 +92,7 @@ function renderCommandsAnalyzeIntentExamples() {
92
92
  title: '1. find - 查找符号 / 文本',
93
93
  commands: [
94
94
  'mycodemap analyze -i find -k "UnifiedResult"',
95
+ 'mycodemap analyze -i find -k "SourceLocation" --json --structured',
95
96
  'mycodemap analyze -i find -t "src/orchestrator" -k "IntentRouter" --topK 20',
96
97
  ],
97
98
  },
@@ -0,0 +1,89 @@
1
+ import contextlib
2
+ import io
3
+ import importlib.util
4
+ import json
5
+ import tempfile
6
+ import unittest
7
+ from pathlib import Path
8
+
9
+
10
+ def load_capability_report_module():
11
+ module_path = Path(__file__).resolve().parents[1] / "capability-report.py"
12
+ spec = importlib.util.spec_from_file_location("capability_report", module_path)
13
+ if spec is None or spec.loader is None:
14
+ raise RuntimeError(f"Unable to load module from {module_path}")
15
+ module = importlib.util.module_from_spec(spec)
16
+ spec.loader.exec_module(module)
17
+ return module
18
+
19
+
20
+ class CapabilityReportTests(unittest.TestCase):
21
+ @classmethod
22
+ def setUpClass(cls):
23
+ cls.module = load_capability_report_module()
24
+
25
+ def test_required_fail_status(self):
26
+ def failing_runner(command, cwd, timeout):
27
+ return {
28
+ "available": True,
29
+ "returncode": 1,
30
+ "stdout": "",
31
+ "stderr": "failed",
32
+ "duration_ms": 8,
33
+ }
34
+
35
+ item = self.module.build_item(
36
+ "python3",
37
+ "required",
38
+ ["python3", "--version"],
39
+ cwd=Path("."),
40
+ timeout=10,
41
+ runner=failing_runner,
42
+ )
43
+
44
+ self.assertEqual(item["status"], "REQUIRED_FAIL")
45
+
46
+ def test_optional_disabled_status(self):
47
+ def missing_runner(command, cwd, timeout):
48
+ return {
49
+ "available": False,
50
+ "returncode": None,
51
+ "stdout": "",
52
+ "stderr": "missing",
53
+ "duration_ms": 5,
54
+ }
55
+
56
+ item = self.module.build_item(
57
+ "validate_rules",
58
+ "optional",
59
+ ["python3", "scripts/validate-rules.py", "code", "--report-only"],
60
+ cwd=Path("."),
61
+ timeout=10,
62
+ runner=missing_runner,
63
+ )
64
+
65
+ self.assertEqual(item["status"], "OPTIONAL_DISABLED")
66
+
67
+ def test_output_file_written(self):
68
+ fake_report = {
69
+ "version": 1,
70
+ "generated_at": "2026-04-19T00:00:00Z",
71
+ "summary": {"required": {"passed": 1, "failed": 0, "total": 1}},
72
+ "items": [{"name": "python3", "kind": "required", "status": "PASS"}],
73
+ }
74
+
75
+ with tempfile.TemporaryDirectory() as temp_dir:
76
+ output_path = Path(temp_dir) / "capability-report.json"
77
+ with contextlib.redirect_stdout(io.StringIO()):
78
+ exit_code = self.module.main(["--output", str(output_path)], report_builder=lambda: fake_report)
79
+
80
+ self.assertEqual(exit_code, 0)
81
+ self.assertTrue(output_path.exists())
82
+
83
+ payload = json.loads(output_path.read_text(encoding="utf-8"))
84
+ self.assertIn("summary", payload)
85
+ self.assertIn("items", payload)
86
+
87
+
88
+ if __name__ == "__main__":
89
+ unittest.main()
@@ -0,0 +1,51 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+
9
+ ROOT = Path(__file__).resolve().parents[2]
10
+
11
+
12
+ class RuleControlWorkflowTests(unittest.TestCase):
13
+ def test_rule_context_is_scoped(self):
14
+ result = subprocess.run(
15
+ [
16
+ "node",
17
+ "scripts/rule-context.mjs",
18
+ "--files",
19
+ "src/cli/index.ts",
20
+ "--format",
21
+ "json",
22
+ ],
23
+ cwd=ROOT,
24
+ check=True,
25
+ capture_output=True,
26
+ text=True,
27
+ )
28
+
29
+ payload = json.loads(result.stdout)
30
+ self.assertIn("docs/rules/code-quality-redlines.md", payload["matchedRules"])
31
+ self.assertNotIn("docs/rules/testing.md", payload["matchedRules"])
32
+
33
+ def test_execute_workflows_include_rule_context(self):
34
+ workflow_paths = [
35
+ ROOT / ".codex/get-shit-done/workflows/execute-phase.md",
36
+ ROOT / ".claude/get-shit-done/workflows/execute-phase.md",
37
+ ]
38
+
39
+ for workflow_path in workflow_paths:
40
+ content = workflow_path.read_text(encoding="utf-8")
41
+ self.assertIn("rule-context.mjs --files", content)
42
+ self.assertIn("<rule_context>", content)
43
+
44
+ def test_ci_contains_rule_backstop(self):
45
+ content = (ROOT / ".github/workflows/ci-gateway.yml").read_text(encoding="utf-8")
46
+ self.assertIn("Rule validation backstop", content)
47
+ self.assertIn("python3 scripts/validate-rules.py code", content)
48
+
49
+
50
+ if __name__ == "__main__":
51
+ unittest.main()
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import sys
5
+ import unittest
6
+ from pathlib import Path
7
+
8
+
9
+ def load_validate_rules_module():
10
+ module_path = Path(__file__).resolve().parents[1] / "validate-rules.py"
11
+ spec = importlib.util.spec_from_file_location("validate_rules", module_path)
12
+ if spec is None or spec.loader is None:
13
+ raise RuntimeError(f"Failed to load module from {module_path}")
14
+
15
+ module = importlib.util.module_from_spec(spec)
16
+ sys.modules[spec.name] = module
17
+ spec.loader.exec_module(module)
18
+ return module
19
+
20
+
21
+ validate_rules = load_validate_rules_module()
22
+
23
+
24
+ def make_check(level: str, status: str):
25
+ return validate_rules.make_check(
26
+ name=f"{level.lower()}-{status}",
27
+ level=level,
28
+ command=["echo", status],
29
+ status=status,
30
+ output=status,
31
+ )
32
+
33
+
34
+ class ValidateRulesExitCodeTests(unittest.TestCase):
35
+ def test_exit_code_for_no_findings(self):
36
+ checks = [make_check("P0", "passed"), make_check("P1", "passed")]
37
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=False), 0)
38
+
39
+ def test_exit_code_for_p0(self):
40
+ checks = [make_check("P0", "failed"), make_check("P1", "passed")]
41
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=False), 1)
42
+
43
+ def test_exit_code_for_p1(self):
44
+ checks = [make_check("P0", "passed"), make_check("P1", "failed")]
45
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=False), 2)
46
+
47
+ def test_exit_code_for_p2(self):
48
+ checks = [make_check("P0", "passed"), make_check("P2", "failed")]
49
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=False), 3)
50
+
51
+ def test_exit_code_for_unavailable(self):
52
+ checks = [make_check("P0", "failed"), make_check("P1", "unavailable")]
53
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=False), 4)
54
+
55
+ def test_report_only_exits_zero(self):
56
+ checks = [make_check("P0", "failed"), make_check("P1", "unavailable")]
57
+ self.assertEqual(validate_rules.resolve_exit_code(checks, report_only=True), 0)
58
+
59
+ def test_report_status_values_are_runtime_only(self):
60
+ checks = [
61
+ make_check("P0", "passed"),
62
+ make_check("P1", "failed"),
63
+ make_check("P2", "unavailable"),
64
+ ]
65
+ report = validate_rules.build_report("all", checks, report_only=False)
66
+ self.assertEqual([check["level"] for check in report["checks"]], ["P0", "P1", "P2"])
67
+ self.assertSetEqual(
68
+ {check["status"] for check in report["checks"]},
69
+ {"passed", "failed", "unavailable"},
70
+ )
71
+
72
+ def test_arch_check_marks_missing_dist_as_unavailable(self):
73
+ missing_dist = Path("/tmp/validate-rules-missing-dist.js")
74
+ checks = validate_rules.execute_checks("arch", dist_cli_path=missing_dist)
75
+ self.assertEqual(len(checks), 1)
76
+ self.assertEqual(checks[0]["level"], "P0")
77
+ self.assertEqual(checks[0]["status"], "unavailable")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main()