@boyingliu01/xp-gate 0.8.8 → 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.
- package/bin/xp-gate.js +18 -0
- package/hooks/pre-commit +565 -120
- package/lib/arch.js +49 -0
- package/lib/check.js +50 -0
- package/lib/init.js +36 -0
- package/lib/principles.js +48 -0
- package/mock-policy/AGENTS.md +94 -0
- package/mock-policy/__tests__/config.test.ts +292 -0
- package/mock-policy/__tests__/integration.test.ts +600 -0
- package/mock-policy/__tests__/mock-decision-engine.test.ts +231 -0
- package/mock-policy/__tests__/scope-scanner.test.ts +535 -0
- package/mock-policy/config.ts +106 -0
- package/mock-policy/gate-m3.ts +233 -0
- package/mock-policy/mock-decision-engine.ts +155 -0
- package/mock-policy/schema.ts +22 -0
- package/mock-policy/scope-scanner.ts +288 -0
- package/mock-policy/types.ts +65 -0
- package/mutation/AGENTS.md +73 -0
- package/mutation/__tests__/detect-ai-test.test.ts +152 -0
- package/mutation/__tests__/gate-m.test.ts +194 -0
- package/mutation/detect-ai-test.ts +128 -0
- package/mutation/gate-m.ts +619 -0
- package/mutation/init-baseline.ts +192 -0
- package/mutation/stryker-types.ts +14 -0
- package/mutation/types.ts +70 -0
- package/mutation/update-baseline.ts +160 -0
- package/package.json +5 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +2 -2
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/claude-code/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/claude-code/skills/delphi-review/SKILL.md +12 -12
- package/plugins/claude-code/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/opencode/README.md +23 -7
- package/plugins/opencode/index.ts +36 -21
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/opencode/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/opencode/skills/delphi-review/SKILL.md +12 -12
- package/plugins/opencode/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +20 -0
- package/plugins/qoder/skills/delphi-review/AGENTS.md +7 -7
- package/plugins/qoder/skills/delphi-review/INSTALL.md +1 -1
- package/plugins/qoder/skills/delphi-review/SKILL.md +12 -12
- package/plugins/qoder/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +83 -36
- package/plugins/qoder/skills/sprint-flow/SKILL.md +51 -1
- package/plugins/qoder/skills/sprint-flow/references/force-levels.md +1 -1
- package/plugins/qoder/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/plugins/qoder/skills/test-driven-development/SKILL.md +371 -0
- package/plugins/qoder/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +111 -0
- package/principles/__tests__/analyzer.test.ts +343 -0
- package/principles/__tests__/baseline-storage.test.ts +187 -0
- package/principles/__tests__/baseline.test.ts +328 -0
- package/principles/__tests__/boy-scout-integration.test.ts +264 -0
- package/principles/__tests__/boy-scout.test.ts +646 -0
- package/principles/__tests__/config.test.ts +78 -0
- package/principles/__tests__/index.test.ts +81 -0
- package/principles/__tests__/lint-baseline.test.ts +314 -0
- package/principles/__tests__/reporter.test.ts +832 -0
- package/principles/__tests__/types.test.ts +101 -0
- package/principles/adapters/__tests__/base.test.ts +95 -0
- package/principles/adapters/__tests__/cpp.test.ts +290 -0
- package/principles/adapters/__tests__/dart.test.ts +84 -0
- package/principles/adapters/__tests__/go.test.ts +68 -0
- package/principles/adapters/__tests__/java.test.ts +68 -0
- package/principles/adapters/__tests__/kotlin.test.ts +76 -0
- package/principles/adapters/__tests__/objectivec.test.ts +453 -0
- package/principles/adapters/__tests__/python.test.ts +84 -0
- package/principles/adapters/__tests__/swift.test.ts +68 -0
- package/principles/adapters/__tests__/typescript.test.ts +116 -0
- package/principles/adapters/base.ts +124 -0
- package/principles/adapters/cpp.ts +115 -0
- package/principles/adapters/dart.ts +44 -0
- package/principles/adapters/go.ts +55 -0
- package/principles/adapters/java.ts +56 -0
- package/principles/adapters/kotlin.ts +54 -0
- package/principles/adapters/objectivec.ts +82 -0
- package/principles/adapters/python.ts +83 -0
- package/principles/adapters/swift.ts +40 -0
- package/principles/adapters/typescript.ts +66 -0
- package/principles/analyzer.ts +190 -0
- package/principles/baseline.ts +273 -0
- package/principles/boy-scout.ts +416 -0
- package/principles/config.ts +118 -0
- package/principles/index.ts +87 -0
- package/principles/lint-baseline.ts +385 -0
- package/principles/reporter.ts +188 -0
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +84 -0
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +74 -0
- package/principles/rules/__tests__/clean-code/god-class.test.ts +111 -0
- package/principles/rules/__tests__/clean-code/large-file.test.ts +73 -0
- package/principles/rules/__tests__/clean-code/long-function.test.ts +83 -0
- package/principles/rules/__tests__/clean-code/magic-numbers.test.ts +68 -0
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +118 -0
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +173 -0
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +60 -0
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +107 -0
- package/principles/rules/__tests__/index.test.ts +49 -0
- package/principles/rules/__tests__/solid/dip.test.ts +121 -0
- package/principles/rules/__tests__/solid/isp.test.ts +60 -0
- package/principles/rules/__tests__/solid/lsp.test.ts +184 -0
- package/principles/rules/__tests__/solid/ocp.test.ts +69 -0
- package/principles/rules/__tests__/solid/srp.test.ts +89 -0
- package/principles/rules/clean-code/code-duplication.ts +31 -0
- package/principles/rules/clean-code/deep-nesting.ts +38 -0
- package/principles/rules/clean-code/god-class.ts +54 -0
- package/principles/rules/clean-code/large-file.ts +34 -0
- package/principles/rules/clean-code/long-function.ts +47 -0
- package/principles/rules/clean-code/magic-numbers.ts +59 -0
- package/principles/rules/clean-code/many-exports.ts +40 -0
- package/principles/rules/clean-code/missing-error-handling.ts +33 -0
- package/principles/rules/clean-code/too-many-params.ts +35 -0
- package/principles/rules/clean-code/unused-imports.ts +33 -0
- package/principles/rules/index.ts +61 -0
- package/principles/rules/solid/dip.ts +47 -0
- package/principles/rules/solid/isp.ts +37 -0
- package/principles/rules/solid/lsp.ts +79 -0
- package/principles/rules/solid/ocp.ts +40 -0
- package/principles/rules/solid/srp.ts +44 -0
- package/principles/types.ts +27 -0
- package/skills/delphi-review/AGENTS.md +7 -7
- package/skills/delphi-review/INSTALL.md +1 -1
- package/skills/delphi-review/SKILL.md +12 -12
- package/skills/delphi-review/references/code-walkthrough.md +1 -1
- package/skills/sprint-flow/AGENTS.md +83 -36
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/force-levels.md +1 -1
- package/skills/sprint-flow/references/phase-3-review.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +3 -3
|
@@ -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
|
+
});
|