@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,118 @@
|
|
|
1
|
+
import { readFile } from 'fs/promises';
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
interface RuleConfig {
|
|
6
|
+
enabled: boolean;
|
|
7
|
+
threshold?: number;
|
|
8
|
+
exclude?: (string | number)[];
|
|
9
|
+
severity?: string;
|
|
10
|
+
methodThreshold?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PrinciplesConfig {
|
|
14
|
+
rules: {
|
|
15
|
+
'clean-code': {
|
|
16
|
+
'long-function': RuleConfig;
|
|
17
|
+
'large-file': RuleConfig;
|
|
18
|
+
'god-class': RuleConfig;
|
|
19
|
+
'deep-nesting': RuleConfig;
|
|
20
|
+
'too-many-params': RuleConfig;
|
|
21
|
+
'magic-numbers': RuleConfig;
|
|
22
|
+
'missing-error-handling': RuleConfig;
|
|
23
|
+
'unused-imports': RuleConfig;
|
|
24
|
+
'code-duplication': RuleConfig;
|
|
25
|
+
'many-exports': RuleConfig;
|
|
26
|
+
};
|
|
27
|
+
'solid': {
|
|
28
|
+
'srp': RuleConfig;
|
|
29
|
+
'ocp': RuleConfig;
|
|
30
|
+
'lsp': RuleConfig;
|
|
31
|
+
'isp': RuleConfig;
|
|
32
|
+
'dip': RuleConfig;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
output?: {
|
|
36
|
+
format?: string;
|
|
37
|
+
'show-score'?: boolean;
|
|
38
|
+
colorize?: boolean;
|
|
39
|
+
};
|
|
40
|
+
performance?: {
|
|
41
|
+
mode?: string;
|
|
42
|
+
mediumProjectDefinition?: string;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function getDefaultConfig(): PrinciplesConfig {
|
|
47
|
+
return {
|
|
48
|
+
rules: {
|
|
49
|
+
'clean-code': {
|
|
50
|
+
'long-function': { enabled: true, threshold: 50, severity: 'warning' },
|
|
51
|
+
'large-file': { enabled: true, threshold: 650, severity: 'warning' },
|
|
52
|
+
'god-class': { enabled: true, threshold: 15, severity: 'warning' },
|
|
53
|
+
'deep-nesting': { enabled: true, threshold: 4, severity: 'warning' },
|
|
54
|
+
'too-many-params': { enabled: true, threshold: 7, severity: 'info' },
|
|
55
|
+
'magic-numbers': {
|
|
56
|
+
enabled: true,
|
|
57
|
+
exclude: [0, 1, -1, 2, 10, 100, 1000, 60, 24, 7, 30, 365, 256, 1024],
|
|
58
|
+
severity: 'info'
|
|
59
|
+
},
|
|
60
|
+
'missing-error-handling': { enabled: true, severity: 'warning' },
|
|
61
|
+
'unused-imports': { enabled: true, severity: 'info' },
|
|
62
|
+
'code-duplication': { enabled: true, threshold: 15, severity: 'warning' },
|
|
63
|
+
'many-exports': { enabled: true, threshold: 10, severity: 'warning' }
|
|
64
|
+
},
|
|
65
|
+
'solid': {
|
|
66
|
+
'srp': { enabled: true, methodThreshold: 15, severity: 'warning' },
|
|
67
|
+
'ocp': { enabled: true, severity: 'info' },
|
|
68
|
+
'lsp': { enabled: true, severity: 'info' },
|
|
69
|
+
'isp': { enabled: true, methodThreshold: 10, severity: 'info' },
|
|
70
|
+
'dip': {
|
|
71
|
+
enabled: true,
|
|
72
|
+
exclude: ['Date', 'Map', 'Set', 'Error', 'Array', 'Object', 'Promise'],
|
|
73
|
+
severity: 'warning'
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
output: {
|
|
78
|
+
format: 'console',
|
|
79
|
+
'show-score': true,
|
|
80
|
+
colorize: true
|
|
81
|
+
},
|
|
82
|
+
performance: {
|
|
83
|
+
mode: 'changed-files-only',
|
|
84
|
+
mediumProjectDefinition: '10000 lines / 500 files'
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function loadConfig(): Promise<PrinciplesConfig> {
|
|
90
|
+
const defaultConfig = getDefaultConfig();
|
|
91
|
+
const configPath = join(process.cwd(), '.principlesrc');
|
|
92
|
+
|
|
93
|
+
if (!existsSync(configPath)) {
|
|
94
|
+
return defaultConfig;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const content = await readFile(configPath, 'utf-8');
|
|
99
|
+
const userConfig = JSON.parse(content) as Partial<PrinciplesConfig>;
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...defaultConfig,
|
|
103
|
+
...userConfig,
|
|
104
|
+
rules: {
|
|
105
|
+
'clean-code': {
|
|
106
|
+
...defaultConfig.rules['clean-code'],
|
|
107
|
+
...userConfig.rules?.['clean-code']
|
|
108
|
+
},
|
|
109
|
+
'solid': {
|
|
110
|
+
...defaultConfig.rules['solid'],
|
|
111
|
+
...userConfig.rules?.['solid']
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
return defaultConfig;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { analyze, getAdapterForFile } from './analyzer';
|
|
2
|
+
import { formatConsole, formatJSON, formatSARIF } from './reporter';
|
|
3
|
+
import { loadConfig } from './config';
|
|
4
|
+
import { getAllPrincipleRules } from './rules';
|
|
5
|
+
|
|
6
|
+
interface CLIOptions {
|
|
7
|
+
files: string[];
|
|
8
|
+
format: 'console' | 'json' | 'sarif';
|
|
9
|
+
changedOnly: boolean;
|
|
10
|
+
showScore: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const VALID_FORMATS: readonly string[] = ['json', 'console', 'sarif'] as const;
|
|
14
|
+
|
|
15
|
+
export function parseArgs(args: string[]): CLIOptions {
|
|
16
|
+
const options: CLIOptions = {
|
|
17
|
+
files: [],
|
|
18
|
+
format: 'console',
|
|
19
|
+
changedOnly: false,
|
|
20
|
+
showScore: false
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
switch (args[i]) {
|
|
25
|
+
case '--files':
|
|
26
|
+
const next = args[++i];
|
|
27
|
+
if (next) options.files = next.split(' ').filter(f => f.trim());
|
|
28
|
+
break;
|
|
29
|
+
case '--format':
|
|
30
|
+
const fmt = args[++i];
|
|
31
|
+
if (fmt && VALID_FORMATS.includes(fmt)) options.format = fmt as 'json' | 'console' | 'sarif';
|
|
32
|
+
break;
|
|
33
|
+
case '--changed-only':
|
|
34
|
+
options.changedOnly = true;
|
|
35
|
+
break;
|
|
36
|
+
case '--show-score':
|
|
37
|
+
options.showScore = true;
|
|
38
|
+
break;
|
|
39
|
+
default:
|
|
40
|
+
if (!args[i].startsWith('--')) options.files.push(args[i]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return options;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getAllRules() {
|
|
48
|
+
return getAllPrincipleRules();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export { getAllRules };
|
|
52
|
+
|
|
53
|
+
export async function main(args: string[]): Promise<number> {
|
|
54
|
+
const options = parseArgs(args);
|
|
55
|
+
|
|
56
|
+
if (options.files.length === 0) {
|
|
57
|
+
console.error('Usage: principles-checker --files <file1> <file2> ... [--format console|json|sarif] [--changed-only]');
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await loadConfig();
|
|
62
|
+
const rules = getAllRules();
|
|
63
|
+
const result = await analyze(options.files, rules, getAdapterForFile);
|
|
64
|
+
|
|
65
|
+
const formatters: Record<string, (r: typeof result) => string> = {
|
|
66
|
+
json: formatJSON,
|
|
67
|
+
sarif: formatSARIF,
|
|
68
|
+
console: formatConsole,
|
|
69
|
+
};
|
|
70
|
+
console.log(formatters[options.format](result));
|
|
71
|
+
|
|
72
|
+
return result.summary.totalViolations > 0 ? 1 : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const args = process.argv.slice(2);
|
|
76
|
+
if (typeof require !== 'undefined' && require.main === module) {
|
|
77
|
+
main(args)
|
|
78
|
+
.then(exitCode => {
|
|
79
|
+
if (exitCode !== 0) {
|
|
80
|
+
process.exit(exitCode);
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
.catch(err => {
|
|
84
|
+
console.error('Analysis failed:', err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lint-baseline.ts — Lint baseline engine for pre-commit gate
|
|
3
|
+
*
|
|
4
|
+
* Extends the BaselineEntry system to support lint tool baselines
|
|
5
|
+
* (ESLint, ruff, golangci-lint, shellcheck).
|
|
6
|
+
*
|
|
7
|
+
* Used by:
|
|
8
|
+
* - pre-commit hook (Gate 1): diff current lint against baseline
|
|
9
|
+
* - xp-gate baseline CLI: create/show/reset/diff baselines
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { BaselineEntry } from './baseline';
|
|
13
|
+
|
|
14
|
+
// ── Parsing ───────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface ESLintFileResult {
|
|
17
|
+
filePath: string;
|
|
18
|
+
errorCount: number;
|
|
19
|
+
warningCount: number;
|
|
20
|
+
messages?: Array<{ ruleId?: string; severity?: number }>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RuffMessage {
|
|
24
|
+
kind?: string;
|
|
25
|
+
message?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RuffFileResult {
|
|
29
|
+
file: string;
|
|
30
|
+
messages?: RuffMessage[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface GolangciIssue {
|
|
34
|
+
file: string;
|
|
35
|
+
line?: number;
|
|
36
|
+
severity?: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface GolangciOutput {
|
|
41
|
+
Issues?: GolangciIssue[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ShellcheckResult {
|
|
45
|
+
file: string;
|
|
46
|
+
line?: number;
|
|
47
|
+
level?: string;
|
|
48
|
+
message?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parse ESLint JSON output into BaselineEntry format.
|
|
53
|
+
* ESLint -f json output: Array<{filePath, errorCount, warningCount, messages}>
|
|
54
|
+
*/
|
|
55
|
+
function parseESLint(parsed: ESLintFileResult[], files: string[]): Record<string, BaselineEntry> {
|
|
56
|
+
const result: Record<string, BaselineEntry> = {};
|
|
57
|
+
const fileSet = new Set(files);
|
|
58
|
+
|
|
59
|
+
for (const fileResult of parsed) {
|
|
60
|
+
// Try to find a match in the files list
|
|
61
|
+
const matchedFile = files.find(f =>
|
|
62
|
+
fileResult.filePath.endsWith(f) || f.endsWith(fileResult.filePath)
|
|
63
|
+
);
|
|
64
|
+
if (!matchedFile || !fileSet.has(matchedFile)) continue;
|
|
65
|
+
|
|
66
|
+
result[matchedFile] = {
|
|
67
|
+
eslint: {
|
|
68
|
+
warnings: fileResult.warningCount || 0,
|
|
69
|
+
errors: fileResult.errorCount || 0,
|
|
70
|
+
},
|
|
71
|
+
totalWarnings: fileResult.warningCount || 0,
|
|
72
|
+
lastAnalyzed: new Date().toISOString(),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse ruff JSON output into BaselineEntry format.
|
|
81
|
+
* ruff check --output-format json: Array<{file, noqa_count, cells, messages: [{kind, message}]}>
|
|
82
|
+
*/
|
|
83
|
+
function parseRuff(parsed: RuffFileResult[], files: string[]): Record<string, BaselineEntry> {
|
|
84
|
+
const result: Record<string, BaselineEntry> = {};
|
|
85
|
+
const fileSet = new Set(files);
|
|
86
|
+
|
|
87
|
+
for (const fileResult of parsed) {
|
|
88
|
+
if (!fileSet.has(fileResult.file)) continue;
|
|
89
|
+
const messages = fileResult.messages || [];
|
|
90
|
+
const warnings = messages.length;
|
|
91
|
+
|
|
92
|
+
result[fileResult.file] = {
|
|
93
|
+
ruff: { warnings, errors: 0 },
|
|
94
|
+
totalWarnings: warnings,
|
|
95
|
+
lastAnalyzed: new Date().toISOString(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse golangci-lint JSON output into BaselineEntry format.
|
|
104
|
+
* golangci-lint --out-format json: {Issues: [{file, line, severity, text}]}
|
|
105
|
+
*/
|
|
106
|
+
function parseGolangci(parsed: GolangciOutput, files: string[]): Record<string, BaselineEntry> {
|
|
107
|
+
const result: Record<string, BaselineEntry> = {};
|
|
108
|
+
const fileSet = new Set(files);
|
|
109
|
+
const warningsByFile: Record<string, { warnings: number; errors: number }> = {};
|
|
110
|
+
|
|
111
|
+
for (const issue of parsed.Issues || []) {
|
|
112
|
+
// Issues have absolute paths; match against files list
|
|
113
|
+
const matchedFile = files.find(f => issue.file.endsWith(f));
|
|
114
|
+
if (!matchedFile || !fileSet.has(matchedFile)) continue;
|
|
115
|
+
|
|
116
|
+
if (!warningsByFile[matchedFile]) {
|
|
117
|
+
warningsByFile[matchedFile] = { warnings: 0, errors: 0 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (issue.severity === 'error') {
|
|
121
|
+
warningsByFile[matchedFile].errors++;
|
|
122
|
+
} else {
|
|
123
|
+
warningsByFile[matchedFile].warnings++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [file, counts] of Object.entries(warningsByFile)) {
|
|
128
|
+
result[file] = {
|
|
129
|
+
golangci: counts,
|
|
130
|
+
totalWarnings: counts.warnings,
|
|
131
|
+
lastAnalyzed: new Date().toISOString(),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return result;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parse shellcheck JSON output into BaselineEntry format.
|
|
140
|
+
* shellcheck -f json: Array<{file, line, level, message}>
|
|
141
|
+
*/
|
|
142
|
+
function parseShellcheck(parsed: ShellcheckResult[], files: string[]): Record<string, BaselineEntry> {
|
|
143
|
+
const result: Record<string, BaselineEntry> = {};
|
|
144
|
+
const fileSet = new Set(files);
|
|
145
|
+
const warningsByFile: Record<string, { warnings: number; errors: number }> = {};
|
|
146
|
+
|
|
147
|
+
for (const item of parsed) {
|
|
148
|
+
const matchedFile = files.find(f => item.file.endsWith(f));
|
|
149
|
+
if (!matchedFile || !fileSet.has(matchedFile)) continue;
|
|
150
|
+
|
|
151
|
+
if (!warningsByFile[matchedFile]) {
|
|
152
|
+
warningsByFile[matchedFile] = { warnings: 0, errors: 0 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (item.level === 'error') {
|
|
156
|
+
warningsByFile[matchedFile].errors++;
|
|
157
|
+
} else {
|
|
158
|
+
warningsByFile[matchedFile].warnings++;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const [file, counts] of Object.entries(warningsByFile)) {
|
|
163
|
+
result[file] = {
|
|
164
|
+
shellcheck: counts,
|
|
165
|
+
totalWarnings: counts.warnings,
|
|
166
|
+
lastAnalyzed: new Date().toISOString(),
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse lint tool JSON output into BaselineEntry format.
|
|
175
|
+
* Supports: eslint, ruff, golangci, shellcheck
|
|
176
|
+
*/
|
|
177
|
+
export function parseLintOutput(
|
|
178
|
+
tool: string,
|
|
179
|
+
output: string,
|
|
180
|
+
files: string[],
|
|
181
|
+
): Record<string, BaselineEntry> {
|
|
182
|
+
if (!output || output === '[]' || output === '{}') return {};
|
|
183
|
+
|
|
184
|
+
let parsed: unknown;
|
|
185
|
+
try {
|
|
186
|
+
parsed = JSON.parse(output);
|
|
187
|
+
} catch {
|
|
188
|
+
return {};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
switch (tool) {
|
|
192
|
+
case 'eslint':
|
|
193
|
+
return parseESLint(parsed as ESLintFileResult[], files);
|
|
194
|
+
case 'ruff':
|
|
195
|
+
return parseRuff(parsed as RuffFileResult[], files);
|
|
196
|
+
case 'golangci':
|
|
197
|
+
return parseGolangci(parsed as GolangciOutput, files);
|
|
198
|
+
case 'shellcheck':
|
|
199
|
+
return parseShellcheck(parsed as ShellcheckResult[], files);
|
|
200
|
+
default:
|
|
201
|
+
return {};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ── Diffing ───────────────────────────────────────────────
|
|
206
|
+
|
|
207
|
+
export interface LintDiffFileResult {
|
|
208
|
+
file: string;
|
|
209
|
+
oldWarnings: number;
|
|
210
|
+
newWarnings: number;
|
|
211
|
+
warningsDelta: number;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export interface LintDiffResult {
|
|
215
|
+
totalWarningsDelta: number;
|
|
216
|
+
filesAdded: LintDiffFileResult[];
|
|
217
|
+
filesRemoved: LintDiffFileResult[];
|
|
218
|
+
filesIncreased: LintDiffFileResult[];
|
|
219
|
+
filesDecreased: LintDiffFileResult[];
|
|
220
|
+
filesUnchanged: LintDiffFileResult[];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Use numeric totalWarnings for diff computation, not entry.totalWarnings
|
|
224
|
+
function getEffectiveWarnings(entry: BaselineEntry): number {
|
|
225
|
+
return entry.totalWarnings;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Compare two baselines and produce a structured diff.
|
|
230
|
+
* Used by: xp-gate baseline diff, pre-commit gate comparison
|
|
231
|
+
*/
|
|
232
|
+
export function diffBaselines(
|
|
233
|
+
oldBaseline: Record<string, BaselineEntry>,
|
|
234
|
+
newBaseline: Record<string, BaselineEntry>,
|
|
235
|
+
): LintDiffResult {
|
|
236
|
+
const filesAdded: LintDiffFileResult[] = [];
|
|
237
|
+
const filesRemoved: LintDiffFileResult[] = [];
|
|
238
|
+
const filesIncreased: LintDiffFileResult[] = [];
|
|
239
|
+
const filesDecreased: LintDiffFileResult[] = [];
|
|
240
|
+
const filesUnchanged: LintDiffFileResult[] = [];
|
|
241
|
+
let totalWarningsDelta = 0;
|
|
242
|
+
|
|
243
|
+
const allFiles = new Set([...Object.keys(oldBaseline), ...Object.keys(newBaseline)]);
|
|
244
|
+
|
|
245
|
+
for (const file of allFiles) {
|
|
246
|
+
const oldEntry = oldBaseline[file];
|
|
247
|
+
const newEntry = newBaseline[file];
|
|
248
|
+
const oldW = oldEntry ? getEffectiveWarnings(oldEntry) : 0;
|
|
249
|
+
const newW = newEntry ? getEffectiveWarnings(newEntry) : 0;
|
|
250
|
+
const delta = newW - oldW;
|
|
251
|
+
|
|
252
|
+
totalWarningsDelta += delta;
|
|
253
|
+
|
|
254
|
+
const result: LintDiffFileResult = { file, oldWarnings: oldW, newWarnings: newW, warningsDelta: delta };
|
|
255
|
+
|
|
256
|
+
if (!oldEntry && newEntry) {
|
|
257
|
+
filesAdded.push(result);
|
|
258
|
+
} else if (oldEntry && !newEntry) {
|
|
259
|
+
filesRemoved.push(result);
|
|
260
|
+
} else if (delta > 0) {
|
|
261
|
+
filesIncreased.push(result);
|
|
262
|
+
} else if (delta < 0) {
|
|
263
|
+
filesDecreased.push(result);
|
|
264
|
+
} else {
|
|
265
|
+
filesUnchanged.push(result);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
totalWarningsDelta,
|
|
271
|
+
filesAdded,
|
|
272
|
+
filesRemoved,
|
|
273
|
+
filesIncreased,
|
|
274
|
+
filesDecreased,
|
|
275
|
+
filesUnchanged,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Delta (per-commit enforcement) ───────────────────────
|
|
280
|
+
|
|
281
|
+
export interface LintDeltaResult {
|
|
282
|
+
enforcement: 'PASS' | 'BLOCK';
|
|
283
|
+
newWarnings: number;
|
|
284
|
+
newErrors: number;
|
|
285
|
+
reduction: number;
|
|
286
|
+
message: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Compare current lint state against a baseline entry for a single file.
|
|
291
|
+
* Used by the pre-commit hook to determine if new lint errors were introduced.
|
|
292
|
+
*/
|
|
293
|
+
export function computeLintDelta(
|
|
294
|
+
baseline: BaselineEntry | null,
|
|
295
|
+
current: BaselineEntry,
|
|
296
|
+
): LintDeltaResult {
|
|
297
|
+
if (!baseline) {
|
|
298
|
+
return {
|
|
299
|
+
enforcement: 'PASS',
|
|
300
|
+
newWarnings: 0,
|
|
301
|
+
newErrors: 0,
|
|
302
|
+
reduction: 0,
|
|
303
|
+
message: 'New file — baseline created. No comparison needed.',
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const oldW = baseline.totalWarnings;
|
|
308
|
+
const oldE = (baseline.eslint?.errors || 0) +
|
|
309
|
+
(baseline.ruff?.errors || 0) +
|
|
310
|
+
(baseline.golangci?.errors || 0) +
|
|
311
|
+
(baseline.shellcheck?.errors || 0);
|
|
312
|
+
const newW = current.totalWarnings;
|
|
313
|
+
const newE = (current.eslint?.errors || 0) +
|
|
314
|
+
(current.ruff?.errors || 0) +
|
|
315
|
+
(current.golangci?.errors || 0) +
|
|
316
|
+
(current.shellcheck?.errors || 0);
|
|
317
|
+
|
|
318
|
+
if (newW > oldW) {
|
|
319
|
+
return {
|
|
320
|
+
enforcement: 'BLOCK',
|
|
321
|
+
newWarnings: newW - oldW,
|
|
322
|
+
newErrors: newE > oldE ? newE - oldE : 0,
|
|
323
|
+
reduction: 0,
|
|
324
|
+
message: `Lint debt increased by ${newW - oldW} warnings (${oldW} → ${newW}). Fix new errors before committing.`,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (newW < oldW) {
|
|
329
|
+
return {
|
|
330
|
+
enforcement: 'PASS',
|
|
331
|
+
newWarnings: 0,
|
|
332
|
+
newErrors: 0,
|
|
333
|
+
reduction: oldW - newW,
|
|
334
|
+
message: `Lint debt reduced by ${oldW - newW} warnings (${oldW} → ${newW}). Good job!`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
enforcement: 'PASS',
|
|
340
|
+
newWarnings: 0,
|
|
341
|
+
newErrors: 0,
|
|
342
|
+
reduction: 0,
|
|
343
|
+
message: 'No change in lint debt.',
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Formatting ────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Format baseline as a human-readable summary string.
|
|
351
|
+
*/
|
|
352
|
+
export function formatBaselineSummary(baseline: Record<string, BaselineEntry>): string {
|
|
353
|
+
const files = Object.keys(baseline);
|
|
354
|
+
if (files.length === 0) {
|
|
355
|
+
return 'No baseline data available. Run `xp-gate baseline create` to initialize.';
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const totalWarnings = files.reduce((sum, f) => sum + baseline[f].totalWarnings, 0);
|
|
359
|
+
|
|
360
|
+
// Count tool usage
|
|
361
|
+
let eslintFiles = 0;
|
|
362
|
+
let ruffFiles = 0;
|
|
363
|
+
let golangciFiles = 0;
|
|
364
|
+
let shellcheckFiles = 0;
|
|
365
|
+
|
|
366
|
+
for (const entry of Object.values(baseline)) {
|
|
367
|
+
if (entry.eslint) eslintFiles++;
|
|
368
|
+
if (entry.ruff) ruffFiles++;
|
|
369
|
+
if (entry.golangci) golangciFiles++;
|
|
370
|
+
if (entry.shellcheck) shellcheckFiles++;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const lines: string[] = [
|
|
374
|
+
`Lint Baseline Summary:`,
|
|
375
|
+
` Files tracked: ${files.length}`,
|
|
376
|
+
` Total warnings: ${totalWarnings}`,
|
|
377
|
+
];
|
|
378
|
+
|
|
379
|
+
if (eslintFiles > 0) lines.push(` ESLint: ${eslintFiles} files`);
|
|
380
|
+
if (ruffFiles > 0) lines.push(` Ruff: ${ruffFiles} files`);
|
|
381
|
+
if (golangciFiles > 0) lines.push(` golangci-lint: ${golangciFiles} files`);
|
|
382
|
+
if (shellcheckFiles > 0) lines.push(` ShellCheck: ${shellcheckFiles} files`);
|
|
383
|
+
|
|
384
|
+
return lines.join('\n');
|
|
385
|
+
}
|