@boyingliu01/xp-gate 0.8.9 → 0.8.10

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 (132) hide show
  1. package/lib/init.js +36 -0
  2. package/mock-policy/AGENTS.md +94 -0
  3. package/mock-policy/__tests__/config.test.ts +292 -0
  4. package/mock-policy/__tests__/integration.test.ts +600 -0
  5. package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
  6. package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
  7. package/mock-policy/config.ts +106 -0
  8. package/mock-policy/gate-m3.ts +233 -0
  9. package/mock-policy/mock-decision-engine.ts +155 -0
  10. package/mock-policy/schema.ts +22 -0
  11. package/mock-policy/scope-scanner.ts +288 -0
  12. package/mock-policy/types.ts +65 -0
  13. package/mutation/AGENTS.md +73 -0
  14. package/mutation/__tests__/detect-ai-test.test.ts +152 -0
  15. package/mutation/__tests__/gate-m.test.ts +194 -0
  16. package/mutation/detect-ai-test.ts +128 -0
  17. package/mutation/gate-m.ts +619 -0
  18. package/mutation/init-baseline.ts +192 -0
  19. package/mutation/stryker-types.ts +14 -0
  20. package/mutation/types.ts +70 -0
  21. package/mutation/update-baseline.ts +160 -0
  22. package/package.json +5 -2
  23. package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
  24. package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
  25. package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
  26. package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
  27. package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
  28. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +7 -7
  29. package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
  30. package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
  31. package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
  32. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
  33. package/plugins/opencode/package.json +1 -1
  34. package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
  35. package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
  36. package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
  37. package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
  38. package/plugins/opencode/skills/sprint-flow/AGENTS.md +7 -7
  39. package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
  40. package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
  41. package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
  42. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
  43. package/plugins/qoder/plugin.json +1 -1
  44. package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
  45. package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
  46. package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
  47. package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
  48. package/plugins/qoder/skills/sprint-flow/AGENTS.md +7 -7
  49. package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
  50. package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
  51. package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
  52. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
  53. package/principles/AGENTS.md +111 -0
  54. package/principles/__tests__/analyzer.test.ts +343 -0
  55. package/principles/__tests__/baseline-storage.test.ts +187 -0
  56. package/principles/__tests__/baseline.test.ts +328 -0
  57. package/principles/__tests__/boy-scout-integration.test.ts +264 -0
  58. package/principles/__tests__/boy-scout.test.ts +646 -0
  59. package/principles/__tests__/config.test.ts +78 -0
  60. package/principles/__tests__/index.test.ts +81 -0
  61. package/principles/__tests__/lint-baseline.test.ts +314 -0
  62. package/principles/__tests__/reporter.test.ts +832 -0
  63. package/principles/__tests__/types.test.ts +101 -0
  64. package/principles/adapters/__tests__/base.test.ts +95 -0
  65. package/principles/adapters/__tests__/cpp.test.ts +290 -0
  66. package/principles/adapters/__tests__/dart.test.ts +84 -0
  67. package/principles/adapters/__tests__/go.test.ts +68 -0
  68. package/principles/adapters/__tests__/java.test.ts +68 -0
  69. package/principles/adapters/__tests__/kotlin.test.ts +76 -0
  70. package/principles/adapters/__tests__/objectivec.test.ts +453 -0
  71. package/principles/adapters/__tests__/python.test.ts +84 -0
  72. package/principles/adapters/__tests__/swift.test.ts +68 -0
  73. package/principles/adapters/__tests__/typescript.test.ts +116 -0
  74. package/principles/adapters/base.ts +124 -0
  75. package/principles/adapters/cpp.ts +115 -0
  76. package/principles/adapters/dart.ts +44 -0
  77. package/principles/adapters/go.ts +55 -0
  78. package/principles/adapters/java.ts +56 -0
  79. package/principles/adapters/kotlin.ts +54 -0
  80. package/principles/adapters/objectivec.ts +82 -0
  81. package/principles/adapters/python.ts +83 -0
  82. package/principles/adapters/swift.ts +40 -0
  83. package/principles/adapters/typescript.ts +66 -0
  84. package/principles/analyzer.ts +190 -0
  85. package/principles/baseline.ts +273 -0
  86. package/principles/boy-scout.ts +416 -0
  87. package/principles/config.ts +118 -0
  88. package/principles/index.ts +87 -0
  89. package/principles/lint-baseline.ts +385 -0
  90. package/principles/reporter.ts +188 -0
  91. package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
  92. package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
  93. package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
  94. package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
  95. package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
  96. package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
  97. package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
  98. package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
  99. package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
  100. package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
  101. package/principles/rules/__tests__/index.test.ts +49 -0
  102. package/principles/rules/__tests__/solid/dip.test.ts +121 -0
  103. package/principles/rules/__tests__/solid/isp.test.ts +60 -0
  104. package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
  105. package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
  106. package/principles/rules/__tests__/solid/srp.test.ts +89 -0
  107. package/principles/rules/clean-code/code-duplication.ts +31 -0
  108. package/principles/rules/clean-code/deep-nesting.ts +38 -0
  109. package/principles/rules/clean-code/god-class.ts +54 -0
  110. package/principles/rules/clean-code/large-file.ts +34 -0
  111. package/principles/rules/clean-code/long-function.ts +47 -0
  112. package/principles/rules/clean-code/magic-numbers.ts +59 -0
  113. package/principles/rules/clean-code/many-exports.ts +40 -0
  114. package/principles/rules/clean-code/missing-error-handling.ts +33 -0
  115. package/principles/rules/clean-code/too-many-params.ts +35 -0
  116. package/principles/rules/clean-code/unused-imports.ts +33 -0
  117. package/principles/rules/index.ts +61 -0
  118. package/principles/rules/solid/dip.ts +47 -0
  119. package/principles/rules/solid/isp.ts +37 -0
  120. package/principles/rules/solid/lsp.ts +79 -0
  121. package/principles/rules/solid/ocp.ts +40 -0
  122. package/principles/rules/solid/srp.ts +44 -0
  123. package/principles/types.ts +27 -0
  124. package/skills/delphi-review/AGENTS.md +7 -7
  125. package/skills/delphi-review/INSTALL.md +1 -1
  126. package/skills/delphi-review/SKILL.md +12 -12
  127. package/skills/delphi-review/references/code-walkthrough.md +1 -1
  128. package/skills/sprint-flow/AGENTS.md +7 -7
  129. package/skills/sprint-flow/SKILL.md +1 -1
  130. package/skills/sprint-flow/references/force-levels.md +1 -1
  131. package/skills/sprint-flow/references/phase-3-review.md +2 -2
  132. package/skills/test-specification-alignment/AGENTS.md +3 -3
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
- **Generated:** 2026-06-11
4
- **Commit:** c18f82b
3
+ **Generated:** 2026-06-14
4
+ **Commit:** e215a50
5
5
  **Branch:** main
6
- **Version:** 0.8.9.0
6
+ **Version:** 0.8.10.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
@@ -0,0 +1,111 @@
1
+ # PRINCIPLES CHECKER MODULE
2
+
3
+ **Generated:** 2026-06-14
4
+ **Commit:** e215a50
5
+ **Branch:** main
6
+ **Version:** 0.8.10.0
7
+
8
+ ## OVERVIEW
9
+ Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
10
+
11
+ ## STRUCTURE
12
+ ```
13
+ src/principles/
14
+ ├── adapters/ # 9 language adapters
15
+ │ ├── base.ts # Shared base class
16
+ │ ├── typescript.ts
17
+ │ ├── python.ts
18
+ │ ├── go.ts
19
+ │ ├── java.ts
20
+ │ ├── kotlin.ts
21
+ │ ├── dart.ts
22
+ │ ├── swift.ts
23
+ │ ├── cpp.ts # Regex-based C++ extraction (handles .cpp AND .c files)
24
+ │ └── objectivec.ts # Regex-based ObjC extraction
25
+ ├── rules/
26
+ │ ├── clean-code/ # 9 Clean Code rules
27
+ │ │ ├── long-function.ts (≤50 lines)
28
+ │ │ ├── large-file.ts
29
+ │ │ ├── god-class.ts
30
+ │ │ ├── deep-nesting.ts (≤4 levels)
31
+ │ │ ├── magic-numbers.ts
32
+ │ │ ├── missing-error-handling.ts
33
+ │ │ ├── too-many-params.ts (≤4 params)
34
+ │ │ ├── unused-imports.ts
35
+ │ │ └── code-duplication.ts
36
+ │ └── solid/ # 5 SOLID rules: srp, ocp, lsp, isp, dip
37
+ ├── analyzer.ts # Rule orchestration engine
38
+ ├── boy-scout.ts # Differential warning enforcement (Gate 6)
39
+ ├── baseline.ts # Warning history (.warnings-baseline.json)
40
+ ├── reporter.ts # Console / JSON / SARIF 2.1.0 output
41
+ ├── config.ts # .principlesrc loader
42
+ ├── index.ts # CLI entry — `getAllRules()` aggregates all 14
43
+ ├── types.ts # Type definitions
44
+ └── __tests__/ # Unit tests across adapters, rules, core
45
+ ```
46
+
47
+ ## WHERE TO LOOK
48
+ | Task | Location | Notes |
49
+ |------|----------|-------|
50
+ | Rule engine | analyzer.ts | Orchestrates 14 rules × 9 adapters |
51
+ | CLI entry | index.ts | `getAllRules()` returns the full rule set |
52
+ | Output | reporter.ts | Emits Console, JSON, or SARIF 2.1.0 |
53
+ | Thresholds | config.ts + .principlesrc | Defaults + project overrides |
54
+ | Boy Scout enforcement | boy-scout.ts | classifyFiles → calculateDelta → enforceBoyScoutRule |
55
+ | Baseline lifecycle | baseline.ts | loadBaseline, saveBaseline, initBaseline |
56
+ | Adapter for new language | adapters/base.ts + add `<lang>.ts` | Subclass base; export via index |
57
+ | C / C++ extraction | adapters/cpp.ts | Regex-based; handles both `.cpp` and `.c` |
58
+ | Tests | __tests__/ | Adapter, rule, and core tests |
59
+
60
+ ## CODE MAP
61
+ | Symbol | Type | Location | Role |
62
+ |--------|------|----------|------|
63
+ | getAllRules | Function | index.ts | Aggregates 9 clean-code + 5 SOLID rules |
64
+ | analyze | Function | analyzer.ts | Walks files × rules × adapters; collects violations |
65
+ | report | Function | reporter.ts | Picks Console / JSON / SARIF emitter |
66
+ | loadConfig | Function | config.ts | Reads `.principlesrc`; merges with built-in defaults |
67
+ | classifyFiles | Function | boy-scout.ts | new / modified / untouched bucket assignment |
68
+ | calculateDelta | Function | boy-scout.ts | Diff vs `.warnings-baseline.json` |
69
+ | enforceBoyScoutRule | Function | boy-scout.ts | Block when modified-file warnings increase |
70
+ | loadBaseline | Function | baseline.ts | Read `.warnings-baseline.json` |
71
+ | saveBaseline | Function | baseline.ts | Write updated baseline after successful commit |
72
+ | initBaseline | Function | baseline.ts | First-time auto-snapshot (≤5 warnings ⇒ must clear to zero) |
73
+
74
+ ## CONVENTIONS
75
+ - **Rule ID format**: `clean-code.long-function`, `solid.srp`. Stable, dot-namespaced.
76
+ - **Severity**: `error` (block), `warning` (block under Boy Scout if increased), `info` (log only).
77
+ - **SARIF 2.1.0** output includes rule descriptions + default level mapping for GitHub Code Scanning.
78
+ - **Boy Scout Rule**: auto-baseline on first touch. Modified files cannot increase warning count vs `.warnings-baseline.json`. New files: zero warnings required. Initial baseline ≤5 warnings must clear to zero on next modification.
79
+ - **Test annotations mandatory**: every test file declares `@test REQ-XXX`, `@intent ...`, `@covers AC-XXX` in JSDoc. Missing tags ⇒ test rejected.
80
+ - **Mock-first**: inline mocks only, no separate fixture files.
81
+ - **ast-grep preferred** for AST work; regex fallback only where ast-grep doesn't reach (cpp / objc).
82
+
83
+ ## ANTI-PATTERNS
84
+ - Do NOT use `as any`, `@ts-ignore`, or `@ts-expect-error` in rule implementations.
85
+ - Do NOT suppress violations via config for production code paths.
86
+ - Do NOT skip `ast-grep` installation — regex fallback is intentionally limited.
87
+ - Do NOT introduce a new rule without: (a) ID under `rules/<category>/`, (b) unit tests, (c) listing in `index.ts`, (d) SARIF rule description in `reporter.ts`.
88
+
89
+ ## UNIQUE STYLES
90
+ - **9 language adapters** here vs **13 shell adapters** in `githooks/adapters/` — divergence is intentional. Principles engine covers what can be parsed; shell adapters cover what can be linted/tested. Languages in shell-only set (shell, powershell, flutter, iac) don't have TS-side rule support.
91
+ - **C++ adapter handles C** via regex — no standalone `c.ts`.
92
+ - **SARIF 2.1.0** chosen for GitHub Code Scanning compatibility.
93
+
94
+ ## COMMANDS
95
+ ```bash
96
+ # Console output
97
+ npx tsx src/principles/index.ts --files "src/**/*.ts" --format console
98
+
99
+ # SARIF for GitHub Actions
100
+ npx tsx src/principles/index.ts --files "src/**/*.ts" --format sarif > results.sarif
101
+
102
+ # Custom config
103
+ npx tsx src/principles/index.ts --files "src/**/*.ts" --config .principlesrc
104
+ ```
105
+
106
+ ## NOTES
107
+ - Gate 4 of pre-commit (one of 10 numbered gates; see root `AGENTS.md` GATES section).
108
+ - Boy Scout enforcement runs separately as Gate 6 in `githooks/pre-commit`.
109
+ - Performance: ~95ms for 28 files; ~340ms estimated for 100 files. Memory: ~102 MB (Node.js baseline).
110
+ - `getAllRules()` is the stable extension point — register new rules there.
111
+
@@ -0,0 +1,343 @@
1
+ /**
2
+ * @test analyzer.ts - Rule orchestration engine
3
+ * @intent Verify analyzer runs rules and aggregates violations correctly
4
+ * @covers clean-code-solid-checker-design Section 3 (CORE ENGINE)
5
+ */
6
+
7
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
8
+ import { analyze } from '../analyzer';
9
+ import { Rule, Adapter, Severity } from '../types';
10
+
11
+ describe('analyzer.ts - Rule Orchestration Engine', () => {
12
+ let mockAdapter: Adapter;
13
+ let mockRule: Rule;
14
+
15
+ beforeEach(() => {
16
+ // Mock adapter
17
+ mockAdapter = {
18
+ detectLanguage: vi.fn().mockReturnValue('typescript'),
19
+ parseAST: vi.fn().mockReturnValue({}),
20
+ extractFunctions: vi.fn().mockReturnValue([
21
+ { name: 'testFunc', startLine: 1, length: 60 }
22
+ ]),
23
+ extractClasses: vi.fn().mockReturnValue([]),
24
+ extractExports: vi.fn().mockReturnValue([]),
25
+ countLines: vi.fn().mockReturnValue(100)
26
+ };
27
+
28
+ // Mock rule that detects violations
29
+ mockRule = {
30
+ id: 'clean-code.long-function',
31
+ name: 'Long Function Rule',
32
+ threshold: 50,
33
+ severity: 'warning' as Severity,
34
+ check: vi.fn().mockReturnValue([
35
+ {
36
+ file: 'test.ts',
37
+ line: 1,
38
+ ruleId: 'clean-code.long-function',
39
+ message: 'Function "testFunc" is too long: 60 lines (maximum: 50)',
40
+ severity: 'warning' as Severity
41
+ }
42
+ ])
43
+ };
44
+ });
45
+
46
+ describe('analyze function', () => {
47
+ it('should return AnalysisResult with violations array', async () => {
48
+ const files = ['test.ts'];
49
+ const rules = [mockRule];
50
+
51
+ const result = await analyze(files, rules, mockAdapter);
52
+
53
+ expect(result).toBeDefined();
54
+ expect(result.violations).toBeInstanceOf(Array);
55
+ expect(result.summary).toBeDefined();
56
+ });
57
+
58
+ it('should run each rule against each file', async () => {
59
+ const files = ['test.ts', 'test2.ts'];
60
+ const rules = [mockRule];
61
+
62
+ await analyze(files, rules, mockAdapter);
63
+
64
+ // Each file should be checked
65
+ expect(mockRule.check).toHaveBeenCalledTimes(2);
66
+ });
67
+
68
+ it('should aggregate violations from all rules', async () => {
69
+ const files = ['test.ts'];
70
+ const rules = [
71
+ mockRule,
72
+ {
73
+ id: 'clean-code.large-file',
74
+ name: 'Large File Rule',
75
+ threshold: 500,
76
+ severity: 'warning' as Severity,
77
+ check: vi.fn().mockReturnValue([
78
+ {
79
+ file: 'test.ts',
80
+ line: 1,
81
+ ruleId: 'clean-code.large-file',
82
+ message: 'File is too large: 600 lines',
83
+ severity: 'warning' as Severity
84
+ }
85
+ ])
86
+ } as Rule
87
+ ];
88
+
89
+ const result = await analyze(files, rules, mockAdapter);
90
+
91
+ expect(result.violations.length).toBe(2);
92
+ expect(result.violations[0].ruleId).toBe('clean-code.long-function');
93
+ expect(result.violations[1].ruleId).toBe('clean-code.large-file');
94
+ });
95
+
96
+ it('should filter rules by enabled config', async () => {
97
+ const files = ['test.ts'];
98
+
99
+ // Mock a disabled rule
100
+ const disabledRule = {
101
+ id: 'clean-code.disabled-rule',
102
+ name: 'Disabled Rule',
103
+ threshold: 10,
104
+ severity: 'info' as Severity,
105
+ check: vi.fn().mockReturnValue([])
106
+ };
107
+
108
+ await analyze(files, [mockRule, disabledRule], mockAdapter, {
109
+ enabledRules: ['clean-code.long-function']
110
+ });
111
+
112
+ // Only enabled rule should run
113
+ expect(mockRule.check).toHaveBeenCalled();
114
+ expect(disabledRule.check).not.toHaveBeenCalled();
115
+ });
116
+
117
+ it('should return empty violations if all rules pass', async () => {
118
+ const passingRule: Rule = {
119
+ id: 'clean-code.passing',
120
+ name: 'Passing Rule',
121
+ threshold: 50,
122
+ severity: 'warning' as Severity,
123
+ check: vi.fn().mockReturnValue([])
124
+ };
125
+
126
+ const files = ['clean.ts'];
127
+ const result = await analyze(files, [passingRule], mockAdapter);
128
+
129
+ expect(result.violations.length).toBe(0);
130
+ expect(result.summary.totalViolations).toBe(0);
131
+ });
132
+
133
+ it('should calculate summary statistics correctly', async () => {
134
+ const files = ['test.ts'];
135
+ const rules = [mockRule];
136
+
137
+ const result = await analyze(files, rules, mockAdapter);
138
+
139
+ expect(result.summary.totalViolations).toBe(1);
140
+ expect(result.summary.warningCount).toBe(1);
141
+ expect(result.summary.errorCount).toBe(0);
142
+ expect(result.summary.infoCount).toBe(0);
143
+ });
144
+
145
+ it('should handle multiple severity levels in summary', async () => {
146
+ const files = ['test.ts'];
147
+ const rules = [
148
+ mockRule,
149
+ {
150
+ id: 'test.error',
151
+ name: 'Error Rule',
152
+ threshold: 10,
153
+ severity: 'error' as Severity,
154
+ check: vi.fn().mockReturnValue([
155
+ {
156
+ file: 'test.ts',
157
+ line: 1,
158
+ ruleId: 'test.error',
159
+ message: 'Error violation',
160
+ severity: 'error' as Severity
161
+ }
162
+ ])
163
+ } as Rule,
164
+ {
165
+ id: 'test.info',
166
+ name: 'Info Rule',
167
+ threshold: 10,
168
+ severity: 'info' as Severity,
169
+ check: vi.fn().mockReturnValue([
170
+ {
171
+ file: 'test.ts',
172
+ line: 2,
173
+ ruleId: 'test.info',
174
+ message: 'Info violation',
175
+ severity: 'info' as Severity
176
+ }
177
+ ])
178
+ } as Rule
179
+ ];
180
+
181
+ const result = await analyze(files, rules, mockAdapter);
182
+
183
+ expect(result.summary.errorCount).toBe(1);
184
+ expect(result.summary.warningCount).toBe(1);
185
+ expect(result.summary.infoCount).toBe(1);
186
+ expect(result.summary.totalViolations).toBe(3);
187
+ });
188
+
189
+ it('should skip files that do not match adapter language', async () => {
190
+ const files = ['test.py', 'test.ts'];
191
+
192
+ const adapterFactory = (filePath: string): Adapter => {
193
+ const ext = filePath.split('.').pop()?.toLowerCase();
194
+ if (ext === 'ts' || ext === 'tsx') {
195
+ return mockAdapter;
196
+ }
197
+ return {
198
+ ...mockAdapter,
199
+ detectLanguage: () => 'unknown'
200
+ };
201
+ };
202
+
203
+ await analyze(files, [mockRule], adapterFactory);
204
+
205
+ expect(mockRule.check).toHaveBeenCalledTimes(1);
206
+ });
207
+
208
+ it('should handle rule check errors gracefully', async () => {
209
+ const errorRule: Rule = {
210
+ id: 'error.rule',
211
+ name: 'Error Rule',
212
+ threshold: 10,
213
+ severity: 'warning' as Severity,
214
+ check: vi.fn().mockImplementation(() => {
215
+ throw new Error('Rule check failed');
216
+ })
217
+ };
218
+
219
+ const files = ['test.ts'];
220
+
221
+ // Should not throw, should handle gracefully
222
+ const result = await analyze(files, [errorRule], mockAdapter);
223
+
224
+ expect(result.violations.length).toBe(0);
225
+ expect(result.errors).toBeDefined();
226
+ expect(result.errors.length).toBeGreaterThan(0);
227
+ });
228
+ });
229
+
230
+ describe('runRuleOnFile (via analyze) — verifies context-object parameter bundling', () => {
231
+ it('should correctly aggregate violations per file and per rule', async () => {
232
+ const files = ['a.ts', 'b.ts'];
233
+ const ruleA = {
234
+ id: 'test.rule-a',
235
+ name: 'Rule A',
236
+ threshold: 10,
237
+ severity: 'warning' as Severity,
238
+ check: vi.fn().mockImplementation((file: string) => {
239
+ if (file === 'a.ts') {
240
+ return [{ file: 'a.ts', line: 1, ruleId: 'test.rule-a', message: 'violation in a', severity: 'warning' as Severity }];
241
+ }
242
+ return [];
243
+ })
244
+ } as Rule;
245
+ const ruleB = {
246
+ id: 'test.rule-b',
247
+ name: 'Rule B',
248
+ threshold: 10,
249
+ severity: 'error' as Severity,
250
+ check: vi.fn().mockReturnValue([
251
+ { file: 'a.ts', line: 2, ruleId: 'test.rule-b', message: 'error in a', severity: 'error' as Severity },
252
+ { file: 'b.ts', line: 1, ruleId: 'test.rule-b', message: 'error in b', severity: 'error' as Severity },
253
+ ])
254
+ } as Rule;
255
+
256
+ const result = await analyze(files, [ruleA, ruleB], mockAdapter);
257
+
258
+ // fileResults — each file has the correct violations
259
+ // ruleA: 1 violation for a.ts via file-match, 0 for b.ts; ruleB: mockReturnValue returns both for any file
260
+ expect(result.fileResults['a.ts'].violations).toHaveLength(3);
261
+ expect(result.fileResults['a.ts'].ruleIds).toEqual(['test.rule-a', 'test.rule-b']);
262
+ // ruleB returns both violations on every call (mockReturnValue — no per-file filter)
263
+ expect(result.fileResults['b.ts'].violations).toHaveLength(2);
264
+ expect(result.fileResults['b.ts'].ruleIds).toEqual(['test.rule-b']);
265
+
266
+ // ruleResults — each rule tracked correctly
267
+ expect(result.ruleResults['test.rule-a'].violationCount).toBe(1);
268
+ expect(result.ruleResults['test.rule-a'].filesChecked).toBe(2);
269
+ // ruleB's violationCount accumulates across all files (2 per file × 2 files = 4)
270
+ expect(result.ruleResults['test.rule-b'].violationCount).toBe(4);
271
+ expect(result.ruleResults['test.rule-b'].filesChecked).toBe(2);
272
+
273
+ // total violations = 1 (ruleA) + 4 (ruleB) = 5
274
+ expect(result.summary.totalViolations).toBe(5);
275
+ });
276
+
277
+ it('should collect per-rule errors from runRuleOnFile into top-level errors array', async () => {
278
+ const files = ['a.ts', 'b.ts'];
279
+ const goodRule = {
280
+ id: 'test.good',
281
+ name: 'Good Rule',
282
+ threshold: 10,
283
+ severity: 'info' as Severity,
284
+ check: vi.fn().mockReturnValue([])
285
+ } as Rule;
286
+ const badRule = {
287
+ id: 'test.bad',
288
+ name: 'Bad Rule',
289
+ threshold: 10,
290
+ severity: 'error' as Severity,
291
+ check: vi.fn().mockImplementation((file: string) => {
292
+ if (file === 'b.ts') throw new Error('crash on b');
293
+ return [];
294
+ })
295
+ } as Rule;
296
+
297
+ const result = await analyze(files, [goodRule, badRule], mockAdapter);
298
+
299
+ // goodRule ran on both files
300
+ expect(goodRule.check).toHaveBeenCalledTimes(2);
301
+ // badRule crashed on b.ts — error captured, a.ts succeeded
302
+ expect(result.errors).toContain('Rule test.bad failed on b.ts: crash on b');
303
+ // goodRule results still intact
304
+ expect(result.ruleResults['test.good'].filesChecked).toBe(2);
305
+ // filesChecked counts attempted files (incremented before try/catch)
306
+ expect(result.ruleResults['test.bad'].filesChecked).toBe(2);
307
+ });
308
+ });
309
+
310
+ describe('AnalysisResult structure', () => {
311
+ it('should include file-by-file breakdown', async () => {
312
+ const files = ['test.ts'];
313
+ const rules = [mockRule];
314
+
315
+ const result = await analyze(files, rules, mockAdapter);
316
+
317
+ expect(result.fileResults).toBeDefined();
318
+ expect(result.fileResults['test.ts']).toBeDefined();
319
+ expect(result.fileResults['test.ts'].violations.length).toBe(1);
320
+ });
321
+
322
+ it('should include execution time', async () => {
323
+ const files = ['test.ts'];
324
+ const rules = [mockRule];
325
+
326
+ const result = await analyze(files, rules, mockAdapter);
327
+
328
+ expect(result.executionTimeMs).toBeDefined();
329
+ expect(result.executionTimeMs).toBeGreaterThanOrEqual(0);
330
+ });
331
+
332
+ it('should include rule-by-rule breakdown', async () => {
333
+ const files = ['test.ts'];
334
+ const rules = [mockRule];
335
+
336
+ const result = await analyze(files, rules, mockAdapter);
337
+
338
+ expect(result.ruleResults).toBeDefined();
339
+ expect(result.ruleResults['clean-code.long-function']).toBeDefined();
340
+ expect(result.ruleResults['clean-code.long-function'].violationCount).toBe(1);
341
+ });
342
+ });
343
+ });
@@ -0,0 +1,187 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import fs from 'fs/promises';
3
+ import { BaselineStorage, BaselineEntry, filterBaselineWarnings } from '../baseline';
4
+
5
+ describe('Baseline Storage', () => {
6
+ const mockBaselinePath = '.warnings-baseline.json';
7
+ let baselineStorage: BaselineStorage;
8
+
9
+ beforeEach(() => {
10
+ baselineStorage = new BaselineStorage();
11
+ });
12
+
13
+ afterEach(() => {
14
+ (fs.access as unknown as {mockRestore?: () => void}).mockRestore?.();
15
+ (fs.readFile as unknown as {mockRestore?: () => void}).mockRestore?.();
16
+ (fs.writeFile as unknown as {mockRestore?: () => void}).mockRestore?.();
17
+ vi.resetAllMocks();
18
+ });
19
+
20
+ it('loads baseline from json file when exists', async () => {
21
+ const mockBaseline = {
22
+ 'src/example.ts': {
23
+ eslint: { warnings: 5, errors: 0 },
24
+ totalWarnings: 5,
25
+ lastAnalyzed: new Date().toISOString()
26
+ }
27
+ };
28
+
29
+ vi.spyOn(fs, 'access').mockResolvedValue();
30
+ vi.spyOn(fs, 'readFile').mockResolvedValue(JSON.stringify(mockBaseline));
31
+
32
+ const result = await baselineStorage.load(mockBaselinePath);
33
+ expect(result).toEqual(mockBaseline);
34
+ });
35
+
36
+ it('returns empty baseline when file missing', async () => {
37
+ vi.spyOn(fs, 'access').mockRejectedValue(new Error('File does not exist'));
38
+
39
+ const result = await baselineStorage.load(mockBaselinePath);
40
+ expect(result).toEqual({});
41
+ });
42
+
43
+ it('saves updated baseline on pass', async () => {
44
+ const mockBaseline = {
45
+ 'src/example.ts': {
46
+ eslint: { warnings: 5, errors: 0 },
47
+ totalWarnings: 5,
48
+ lastAnalyzed: new Date().toISOString()
49
+ }
50
+ };
51
+
52
+ const writeFileSpy = vi.spyOn(fs, 'writeFile').mockResolvedValue();
53
+
54
+ await baselineStorage.save(mockBaselinePath, mockBaseline);
55
+
56
+ expect(writeFileSpy).toHaveBeenCalledWith(
57
+ mockBaselinePath,
58
+ JSON.stringify(mockBaseline, null, 2)
59
+ );
60
+ });
61
+
62
+ it('handles large baselines efficiently', async () => {
63
+ const largeBaseline: Record<string, BaselineEntry> = {};
64
+ for (let i = 0; i < 100; i++) {
65
+ largeBaseline[`src/component-${i}.ts`] = {
66
+ eslint: { warnings: 2, errors: 0 },
67
+ totalWarnings: 2,
68
+ lastAnalyzed: new Date().toISOString()
69
+ };
70
+ }
71
+
72
+ const spyOnWrite = vi.spyOn(fs, 'writeFile').mockResolvedValue();
73
+
74
+ await baselineStorage.save(mockBaselinePath, largeBaseline);
75
+ expect(spyOnWrite).toHaveBeenCalledWith(
76
+ mockBaselinePath,
77
+ expect.any(String)
78
+ );
79
+
80
+ const [calledWithPath, content] = spyOnWrite.mock.calls[0];
81
+ expect(calledWithPath).toBe(mockBaselinePath);
82
+ expect(JSON.parse(content as string)).toEqual(largeBaseline);
83
+ });
84
+
85
+ it('validates baseline with proper structure', () => {
86
+ const validBaseline = {
87
+ 'src/file1.ts': {
88
+ eslint: { warnings: 2, errors: 1 },
89
+ principles: { warnings: 0, errors: 1 },
90
+ ccn: { warnings: 0, max: 3 },
91
+ totalWarnings: 2,
92
+ lastAnalyzed: new Date().toISOString()
93
+ }
94
+ };
95
+
96
+ expect(() => baselineStorage.validate(validBaseline)).not.toThrow();
97
+ });
98
+
99
+ it('throws error for invalid baseline', () => {
100
+ const invalidBaseline = {
101
+ 'src/file1.ts': {
102
+ totalWarnings: -5,
103
+ lastAnalyzed: new Date().toISOString()
104
+ }
105
+ };
106
+
107
+ expect(() => baselineStorage.validate(invalidBaseline)).toThrow();
108
+ });
109
+
110
+ it('filters baseline to include only files with warnings', () => {
111
+ const baseline = {
112
+ 'src/clean1.ts': {
113
+ eslint: { warnings: 0, errors: 0 },
114
+ totalWarnings: 0,
115
+ lastAnalyzed: new Date().toISOString()
116
+ },
117
+ 'src/warning1.ts': {
118
+ eslint: { warnings: 5, errors: 0 },
119
+ totalWarnings: 5,
120
+ lastAnalyzed: new Date().toISOString()
121
+ },
122
+ 'src/clean2.ts': {
123
+ eslint: { warnings: 0, errors: 0 },
124
+ totalWarnings: 0,
125
+ lastAnalyzed: new Date().toISOString()
126
+ }
127
+ };
128
+
129
+ const filtered = filterBaselineWarnings(baseline, 1);
130
+ expect(Object.keys(filtered)).toHaveLength(1);
131
+ expect(filtered['src/warning1.ts']).toBeDefined();
132
+ expect(filtered['src/clean1.ts']).toBeUndefined();
133
+ expect(filtered['src/clean2.ts']).toBeUndefined();
134
+ });
135
+
136
+ it('gets summary statistics', () => {
137
+ const baseline = {
138
+ 'src/file1.ts': {
139
+ eslint: { warnings: 2, errors: 1 },
140
+ principles: { warnings: 1, errors: 0 },
141
+ totalWarnings: 3,
142
+ lastAnalyzed: new Date().toISOString()
143
+ },
144
+ 'src/file2.ts': {
145
+ eslint: { warnings: 4, errors: 0 },
146
+ principles: { warnings: 0, errors: 2 },
147
+ totalWarnings: 4,
148
+ lastAnalyzed: new Date().toISOString()
149
+ }
150
+ };
151
+
152
+ const stats = baselineStorage.getSummaryStatistics(baseline);
153
+
154
+ expect(stats.totalFiles).toBe(2);
155
+ expect(stats.totalWarnings).toBe(7);
156
+ expect(stats.averageWarningsPerFile).toBe(3.5);
157
+ expect(stats.eslint?.totalWarnings).toBe(6);
158
+ expect(stats.eslint?.totalErrors).toBe(1);
159
+ });
160
+
161
+ it('creates baseline from files with warning data', async () => {
162
+ const warningData = [
163
+ {
164
+ file: 'src/example1.ts',
165
+ counts: {
166
+ eslint: { warnings: 2, errors: 1 },
167
+ totalWarnings: 2
168
+ }
169
+ },
170
+ {
171
+ file: 'src/example2.ts',
172
+ counts: {
173
+ eslint: { warnings: 5, errors: 0 },
174
+ principles: { warnings: 1, errors: 0 },
175
+ totalWarnings: 6
176
+ }
177
+ }
178
+ ];
179
+
180
+ const result = await baselineStorage.createFromFiles(warningData);
181
+
182
+ expect(Object.keys(result)).toEqual(['src/example1.ts', 'src/example2.ts']);
183
+ expect(result['src/example1.ts'].totalWarnings).toBe(2);
184
+ expect(result['src/example2.ts'].totalWarnings).toBe(6);
185
+ expect(result['src/example1.ts'].eslint!.warnings).toBe(2);
186
+ });
187
+ });