@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.
- package/lib/init.js +36 -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 +7 -7
- 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/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 +7 -7
- 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 +1 -1
- 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 +7 -7
- 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-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 +7 -7
- 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,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test config.ts - Configuration loader
|
|
3
|
+
* @intent Verify config loads thresholds correctly from .principlesrc
|
|
4
|
+
* @covers clean-code-solid-checker-design Section 11
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect } from 'vitest';
|
|
8
|
+
import { loadConfig, getDefaultConfig } from '../config';
|
|
9
|
+
|
|
10
|
+
describe('config.ts - Configuration Loader', () => {
|
|
11
|
+
describe('getDefaultConfig', () => {
|
|
12
|
+
it('should return default thresholds matching design spec', () => {
|
|
13
|
+
const config = getDefaultConfig();
|
|
14
|
+
|
|
15
|
+
// Clean code thresholds from design Section 6
|
|
16
|
+
expect(config.rules['clean-code']['long-function'].threshold).toBe(50);
|
|
17
|
+
expect(config.rules['clean-code']['large-file'].threshold).toBe(650);
|
|
18
|
+
expect(config.rules['clean-code']['god-class'].threshold).toBe(15);
|
|
19
|
+
expect(config.rules['clean-code']['deep-nesting'].threshold).toBe(4);
|
|
20
|
+
expect(config.rules['clean-code']['too-many-params'].threshold).toBe(7);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should include magic numbers exclusion list', () => {
|
|
24
|
+
const config = getDefaultConfig();
|
|
25
|
+
const magicNumbersExclude = config.rules['clean-code']['magic-numbers'].exclude;
|
|
26
|
+
|
|
27
|
+
expect(magicNumbersExclude).toContain(0);
|
|
28
|
+
expect(magicNumbersExclude).toContain(1);
|
|
29
|
+
expect(magicNumbersExclude).toContain(-1);
|
|
30
|
+
expect(magicNumbersExclude).toContain(2);
|
|
31
|
+
expect(magicNumbersExclude).toContain(10);
|
|
32
|
+
expect(magicNumbersExclude).toContain(100);
|
|
33
|
+
expect(magicNumbersExclude).toContain(1000);
|
|
34
|
+
expect(magicNumbersExclude).toContain(60);
|
|
35
|
+
expect(magicNumbersExclude).toContain(24);
|
|
36
|
+
expect(magicNumbersExclude).toContain(7);
|
|
37
|
+
expect(magicNumbersExclude).toContain(30);
|
|
38
|
+
expect(magicNumbersExclude).toContain(365);
|
|
39
|
+
expect(magicNumbersExclude).toContain(256);
|
|
40
|
+
expect(magicNumbersExclude).toContain(1024);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should include SOLID thresholds from design Section 7', () => {
|
|
44
|
+
const config = getDefaultConfig();
|
|
45
|
+
|
|
46
|
+
expect(config.rules['solid']['srp'].methodThreshold).toBe(15);
|
|
47
|
+
expect(config.rules['solid']['isp'].methodThreshold).toBe(10);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should include DIP exclusion list', () => {
|
|
51
|
+
const config = getDefaultConfig();
|
|
52
|
+
const dipExclude = config.rules['solid']['dip'].exclude;
|
|
53
|
+
|
|
54
|
+
expect(dipExclude).toContain('Date');
|
|
55
|
+
expect(dipExclude).toContain('Map');
|
|
56
|
+
expect(dipExclude).toContain('Set');
|
|
57
|
+
expect(dipExclude).toContain('Error');
|
|
58
|
+
expect(dipExclude).toContain('Array');
|
|
59
|
+
expect(dipExclude).toContain('Object');
|
|
60
|
+
expect(dipExclude).toContain('Promise');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('loadConfig', () => {
|
|
65
|
+
it('should load .principlesrc from project root', async () => {
|
|
66
|
+
// This will fail until config.ts exists
|
|
67
|
+
const config = await loadConfig();
|
|
68
|
+
expect(config).toBeDefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should override defaults with project config', async () => {
|
|
72
|
+
// If .principlesrc has custom threshold, it should override
|
|
73
|
+
const config = await loadConfig();
|
|
74
|
+
// Actual test depends on .principlesrc content
|
|
75
|
+
expect(config.rules).toBeDefined();
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test index.ts - CLI entry point
|
|
3
|
+
* @intent Verify CLI parses arguments and runs analysis correctly
|
|
4
|
+
* @covers clean-code-solid-checker-design Section 3 (Integration)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
8
|
+
import { main, parseArgs } from '../index';
|
|
9
|
+
|
|
10
|
+
describe('index.ts - CLI Entry Point', () => {
|
|
11
|
+
describe('parseArgs', () => {
|
|
12
|
+
it('should parse --files argument', () => {
|
|
13
|
+
const args = ['--files', 'test.ts test2.ts'];
|
|
14
|
+
const options = parseArgs(args);
|
|
15
|
+
|
|
16
|
+
expect(options.files).toEqual(['test.ts', 'test2.ts']);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should parse --format argument', () => {
|
|
20
|
+
const args = ['--files', 'test.ts', '--format', 'json'];
|
|
21
|
+
const options = parseArgs(args);
|
|
22
|
+
|
|
23
|
+
expect(options.format).toBe('json');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should parse --changed-only flag', () => {
|
|
27
|
+
const args = ['--files', 'test.ts', '--changed-only'];
|
|
28
|
+
const options = parseArgs(args);
|
|
29
|
+
|
|
30
|
+
expect(options.changedOnly).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should parse --show-score flag', () => {
|
|
34
|
+
const args = ['--files', 'test.ts', '--show-score'];
|
|
35
|
+
const options = parseArgs(args);
|
|
36
|
+
|
|
37
|
+
expect(options.showScore).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should default format to console', () => {
|
|
41
|
+
const args = ['--files', 'test.ts'];
|
|
42
|
+
const options = parseArgs(args);
|
|
43
|
+
|
|
44
|
+
expect(options.format).toBe('console');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should handle positional file arguments', () => {
|
|
48
|
+
const args = ['test.ts', 'test2.ts'];
|
|
49
|
+
const options = parseArgs(args);
|
|
50
|
+
|
|
51
|
+
expect(options.files).toEqual(['test.ts', 'test2.ts']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('main', () => {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
58
|
+
vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should return 1 when no files provided', async () => {
|
|
62
|
+
const result = await main([]);
|
|
63
|
+
|
|
64
|
+
expect(result).toBe(1);
|
|
65
|
+
expect(console.error).toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should run analysis on provided files', async () => {
|
|
69
|
+
const result = await main(['--files', 'src/principles/types.ts']);
|
|
70
|
+
|
|
71
|
+
expect(result).toBeDefined();
|
|
72
|
+
expect(console.log).toHaveBeenCalled();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should return 0 when no violations', async () => {
|
|
76
|
+
const result = await main(['--files', 'src/principles/types.ts', '--format', 'json']);
|
|
77
|
+
|
|
78
|
+
expect(result).toBe(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-152 Baseline-based lint checking
|
|
3
|
+
* @intent Lint baseline engine: parse tool output, diff baselines, manage baseline lifecycle
|
|
4
|
+
* @covers AC-152
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import {
|
|
8
|
+
parseLintOutput,
|
|
9
|
+
diffBaselines,
|
|
10
|
+
formatBaselineSummary,
|
|
11
|
+
computeLintDelta,
|
|
12
|
+
} from '../lint-baseline';
|
|
13
|
+
|
|
14
|
+
describe('parseLintOutput', () => {
|
|
15
|
+
it('parses ESLint JSON output into BaselineEntry format', () => {
|
|
16
|
+
const eslintJson = JSON.stringify([
|
|
17
|
+
{
|
|
18
|
+
filePath: '/project/src/app.ts',
|
|
19
|
+
errorCount: 2,
|
|
20
|
+
warningCount: 3,
|
|
21
|
+
messages: [
|
|
22
|
+
{ ruleId: 'no-unused-vars', severity: 2 },
|
|
23
|
+
{ ruleId: 'no-console', severity: 1 },
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
filePath: '/project/src/utils.ts',
|
|
28
|
+
errorCount: 0,
|
|
29
|
+
warningCount: 1,
|
|
30
|
+
messages: [{ ruleId: 'no-var', severity: 1 }],
|
|
31
|
+
},
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const result = parseLintOutput('eslint', eslintJson, ['src/app.ts', 'src/utils.ts']);
|
|
35
|
+
|
|
36
|
+
expect(result['src/app.ts']).toBeDefined();
|
|
37
|
+
expect(result['src/app.ts'].eslint).toEqual({ warnings: 3, errors: 2 });
|
|
38
|
+
expect(result['src/app.ts'].totalWarnings).toBe(3);
|
|
39
|
+
expect(result['src/utils.ts'].eslint).toEqual({ warnings: 1, errors: 0 });
|
|
40
|
+
expect(result['src/utils.ts'].totalWarnings).toBe(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('parses ruff JSON output into BaselineEntry format', () => {
|
|
44
|
+
const ruffJson = JSON.stringify([
|
|
45
|
+
{
|
|
46
|
+
file: 'src/app.py',
|
|
47
|
+
noqa_count: 0,
|
|
48
|
+
cells: [],
|
|
49
|
+
messages: [
|
|
50
|
+
{ kind: 'E501', message: 'Line too long' },
|
|
51
|
+
{ kind: 'F401', message: 'Unused import' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
const result = parseLintOutput('ruff', ruffJson, ['src/app.py']);
|
|
57
|
+
|
|
58
|
+
expect(result['src/app.py']).toBeDefined();
|
|
59
|
+
expect(result['src/app.py'].ruff).toEqual({ warnings: 2, errors: 0 });
|
|
60
|
+
expect(result['src/app.py'].totalWarnings).toBe(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('parses golangci-lint JSON output into BaselineEntry format', () => {
|
|
64
|
+
const golangciJson = JSON.stringify({
|
|
65
|
+
Issues: [
|
|
66
|
+
{ file: 'src/main.go', line: 10, severity: 'warning', text: 'unused variable' },
|
|
67
|
+
{ file: 'src/main.go', line: 20, severity: 'error', text: 'missing error check' },
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const result = parseLintOutput('golangci', golangciJson, ['src/main.go']);
|
|
72
|
+
|
|
73
|
+
expect(result['src/main.go']).toBeDefined();
|
|
74
|
+
expect(result['src/main.go'].golangci).toEqual({ warnings: 1, errors: 1 });
|
|
75
|
+
expect(result['src/main.go'].totalWarnings).toBe(1);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('parses shellcheck JSON output into BaselineEntry format', () => {
|
|
79
|
+
const shellcheckJson = JSON.stringify([
|
|
80
|
+
{
|
|
81
|
+
file: 'deploy.sh',
|
|
82
|
+
line: 5,
|
|
83
|
+
level: 'warning',
|
|
84
|
+
message: 'Double quote to prevent globbing',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
file: 'deploy.sh',
|
|
88
|
+
line: 12,
|
|
89
|
+
level: 'error',
|
|
90
|
+
message: 'Missing shebang',
|
|
91
|
+
},
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const result = parseLintOutput('shellcheck', shellcheckJson, ['deploy.sh']);
|
|
95
|
+
|
|
96
|
+
expect(result['deploy.sh']).toBeDefined();
|
|
97
|
+
expect(result['deploy.sh'].shellcheck).toEqual({ warnings: 1, errors: 1 });
|
|
98
|
+
expect(result['deploy.sh'].totalWarnings).toBe(1);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('returns empty baseline for empty tool output', () => {
|
|
102
|
+
expect(parseLintOutput('eslint', '[]', ['a.ts'])).toEqual({});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('ignores files not in the target file list', () => {
|
|
106
|
+
const eslintJson = JSON.stringify([
|
|
107
|
+
{
|
|
108
|
+
filePath: '/project/src/ignored.ts',
|
|
109
|
+
errorCount: 5,
|
|
110
|
+
warningCount: 0,
|
|
111
|
+
messages: [{ ruleId: 'no-console', severity: 2 }],
|
|
112
|
+
},
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
const result = parseLintOutput('eslint', eslintJson, ['src/target.ts']);
|
|
116
|
+
expect(result).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe('diffBaselines', () => {
|
|
121
|
+
it('detects increased lint debt', () => {
|
|
122
|
+
const oldBaseline = {
|
|
123
|
+
'src/app.ts': {
|
|
124
|
+
eslint: { warnings: 2, errors: 0 },
|
|
125
|
+
totalWarnings: 2,
|
|
126
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
const newBaseline = {
|
|
130
|
+
'src/app.ts': {
|
|
131
|
+
eslint: { warnings: 5, errors: 1 },
|
|
132
|
+
totalWarnings: 5,
|
|
133
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const diff = diffBaselines(oldBaseline, newBaseline);
|
|
138
|
+
expect(diff.totalWarningsDelta).toBe(3);
|
|
139
|
+
expect(diff.filesIncreased).toHaveLength(1);
|
|
140
|
+
expect(diff.filesIncreased[0].file).toBe('src/app.ts');
|
|
141
|
+
expect(diff.filesIncreased[0].warningsDelta).toBe(3);
|
|
142
|
+
expect(diff.filesDecreased).toHaveLength(0);
|
|
143
|
+
expect(diff.filesUnchanged).toHaveLength(0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('detects decreased lint debt', () => {
|
|
147
|
+
const oldBaseline = {
|
|
148
|
+
'src/app.ts': {
|
|
149
|
+
eslint: { warnings: 5, errors: 0 },
|
|
150
|
+
totalWarnings: 5,
|
|
151
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const newBaseline = {
|
|
155
|
+
'src/app.ts': {
|
|
156
|
+
eslint: { warnings: 2, errors: 0 },
|
|
157
|
+
totalWarnings: 2,
|
|
158
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const diff = diffBaselines(oldBaseline, newBaseline);
|
|
163
|
+
expect(diff.totalWarningsDelta).toBe(-3);
|
|
164
|
+
expect(diff.filesDecreased).toHaveLength(1);
|
|
165
|
+
expect(diff.filesDecreased[0].warningsDelta).toBe(-3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('detects new files added to baseline', () => {
|
|
169
|
+
const oldBaseline = {};
|
|
170
|
+
const newBaseline = {
|
|
171
|
+
'src/new.ts': {
|
|
172
|
+
eslint: { warnings: 1, errors: 0 },
|
|
173
|
+
totalWarnings: 1,
|
|
174
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const diff = diffBaselines(oldBaseline, newBaseline);
|
|
179
|
+
expect(diff.filesAdded).toHaveLength(1);
|
|
180
|
+
expect(diff.filesAdded[0].file).toBe('src/new.ts');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('detects files removed from baseline (warnings cleared)', () => {
|
|
184
|
+
const oldBaseline = {
|
|
185
|
+
'src/gone.ts': {
|
|
186
|
+
eslint: { warnings: 3, errors: 0 },
|
|
187
|
+
totalWarnings: 3,
|
|
188
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const newBaseline = {};
|
|
192
|
+
|
|
193
|
+
const diff = diffBaselines(oldBaseline, newBaseline);
|
|
194
|
+
expect(diff.filesRemoved).toHaveLength(1);
|
|
195
|
+
expect(diff.filesRemoved[0].file).toBe('src/gone.ts');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('reports unchanged files correctly', () => {
|
|
199
|
+
const baseline = {
|
|
200
|
+
'src/stable.ts': {
|
|
201
|
+
eslint: { warnings: 2, errors: 0 },
|
|
202
|
+
totalWarnings: 2,
|
|
203
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
const diff = diffBaselines(baseline, { ...baseline });
|
|
208
|
+
expect(diff.filesUnchanged).toHaveLength(1);
|
|
209
|
+
expect(diff.totalWarningsDelta).toBe(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('computes diff across multiple lint tools', () => {
|
|
213
|
+
const oldBaseline = {
|
|
214
|
+
'src/app.py': {
|
|
215
|
+
ruff: { warnings: 5, errors: 2 },
|
|
216
|
+
totalWarnings: 5,
|
|
217
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
218
|
+
},
|
|
219
|
+
'src/main.go': {
|
|
220
|
+
golangci: { warnings: 3, errors: 0 },
|
|
221
|
+
totalWarnings: 3,
|
|
222
|
+
lastAnalyzed: '2026-01-01T00:00:00Z',
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
const newBaseline = {
|
|
226
|
+
'src/app.py': {
|
|
227
|
+
ruff: { warnings: 2, errors: 1 },
|
|
228
|
+
totalWarnings: 2,
|
|
229
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
230
|
+
},
|
|
231
|
+
'src/main.go': {
|
|
232
|
+
golangci: { warnings: 3, errors: 0 },
|
|
233
|
+
totalWarnings: 3,
|
|
234
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const diff = diffBaselines(oldBaseline, newBaseline);
|
|
239
|
+
expect(diff.filesDecreased).toHaveLength(1);
|
|
240
|
+
expect(diff.filesDecreased[0].file).toBe('src/app.py');
|
|
241
|
+
expect(diff.filesUnchanged).toHaveLength(1);
|
|
242
|
+
expect(diff.totalWarningsDelta).toBe(-3);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('computeLintDelta', () => {
|
|
247
|
+
it('returns BLOCK if new warnings > baseline warnings', () => {
|
|
248
|
+
const baseline = { eslint: { warnings: 2, errors: 0 }, totalWarnings: 2, lastAnalyzed: '' };
|
|
249
|
+
const current = { eslint: { warnings: 5, errors: 0 }, totalWarnings: 5, lastAnalyzed: '' };
|
|
250
|
+
const result = computeLintDelta(baseline, current);
|
|
251
|
+
expect(result.enforcement).toBe('BLOCK');
|
|
252
|
+
expect(result.newWarnings).toBe(3);
|
|
253
|
+
expect(result.newErrors).toBe(0);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('returns PASS if current warnings <= baseline warnings', () => {
|
|
257
|
+
const baseline = { eslint: { warnings: 5, errors: 1 }, totalWarnings: 5, lastAnalyzed: '' };
|
|
258
|
+
const current = { eslint: { warnings: 3, errors: 0 }, totalWarnings: 3, lastAnalyzed: '' };
|
|
259
|
+
const result = computeLintDelta(baseline, current);
|
|
260
|
+
expect(result.enforcement).toBe('PASS');
|
|
261
|
+
expect(result.newWarnings).toBe(0);
|
|
262
|
+
expect(result.reduction).toBe(2);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('returns PASS with reduction message when debt decreases', () => {
|
|
266
|
+
const baseline = { eslint: { warnings: 10, errors: 0 }, totalWarnings: 10, lastAnalyzed: '' };
|
|
267
|
+
const current = { eslint: { warnings: 4, errors: 0 }, totalWarnings: 4, lastAnalyzed: '' };
|
|
268
|
+
const result = computeLintDelta(baseline, current);
|
|
269
|
+
expect(result.enforcement).toBe('PASS');
|
|
270
|
+
expect(result.reduction).toBe(6);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('handles no baseline (first scan) - always PASS', () => {
|
|
274
|
+
const current = { eslint: { warnings: 10, errors: 2 }, totalWarnings: 10, lastAnalyzed: '' };
|
|
275
|
+
const result = computeLintDelta(null, current);
|
|
276
|
+
expect(result.enforcement).toBe('PASS');
|
|
277
|
+
expect(result.message).toContain('baseline created');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('handles empty tool fields gracefully', () => {
|
|
281
|
+
const baseline = { eslint: { warnings: 0, errors: 0 }, totalWarnings: 0, lastAnalyzed: '' };
|
|
282
|
+
const current = { eslint: { warnings: 0, errors: 0 }, totalWarnings: 0, lastAnalyzed: '' };
|
|
283
|
+
const result = computeLintDelta(baseline, current);
|
|
284
|
+
expect(result.enforcement).toBe('PASS');
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('formatBaselineSummary', () => {
|
|
289
|
+
it('returns summary string with file count and warning totals', () => {
|
|
290
|
+
const baseline = {
|
|
291
|
+
'src/a.ts': {
|
|
292
|
+
eslint: { warnings: 3, errors: 1 },
|
|
293
|
+
totalWarnings: 3,
|
|
294
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
295
|
+
},
|
|
296
|
+
'src/b.py': {
|
|
297
|
+
ruff: { warnings: 5, errors: 0 },
|
|
298
|
+
totalWarnings: 5,
|
|
299
|
+
lastAnalyzed: '2026-06-01T00:00:00Z',
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const summary = formatBaselineSummary(baseline);
|
|
304
|
+
expect(summary).toContain('2');
|
|
305
|
+
expect(summary).toContain('8');
|
|
306
|
+
expect(summary).toContain('ESLint');
|
|
307
|
+
expect(summary).toContain('Ruff');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('returns empty message for empty baseline', () => {
|
|
311
|
+
const summary = formatBaselineSummary({});
|
|
312
|
+
expect(summary).toContain('No baseline');
|
|
313
|
+
});
|
|
314
|
+
});
|