claude-git-hooks 2.66.1 → 2.67.3
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/CHANGELOG.md +96 -8
- package/README.md +34 -0
- package/lib/commands/analyze-diff.js +26 -23
- package/lib/commands/analyze.js +3 -1
- package/lib/commands/back-merge.js +39 -33
- package/lib/commands/bump-version.js +20 -15
- package/lib/commands/close-release.js +25 -19
- package/lib/commands/create-pr.js +30 -1
- package/lib/commands/create-release.js +19 -13
- package/lib/defaults.json +5 -0
- package/lib/hooks/pre-commit.js +112 -32
- package/lib/utils/analysis-engine.js +7 -3
- package/lib/utils/config-registry.js +1 -0
- package/lib/utils/library-resolver.js +50 -0
- package/lib/utils/prompt-builder.js +15 -0
- package/lib/utils/skill-registry/catalogue.js +74 -0
- package/lib/utils/skill-registry/feedback-writer.js +196 -0
- package/lib/utils/skill-registry/index.js +254 -0
- package/lib/utils/skill-registry/parser.js +311 -0
- package/lib/utils/skill-registry/resume.js +81 -0
- package/lib/utils/skill-registry/runner.js +265 -0
- package/lib/utils/version-manager.js +9 -3
- package/package.json +1 -2
- package/templates/CLAUDE_ANALYSIS_PROMPT.md +2 -1
- package/templates/config.advanced.example.json +25 -0
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: skill-registry/feedback-writer.js
|
|
3
|
+
* Purpose: Option E (bidirectional feedback loop) — take the AI analysis
|
|
4
|
+
* findings AFTER Claude returns them and, for each finding the
|
|
5
|
+
* AI couldn't classify against the catalogue, append a candidate
|
|
6
|
+
* [skill-gap] entry to the appropriate skill's skill-feedback.md.
|
|
7
|
+
*
|
|
8
|
+
* How it works (depends on Option E2's catalogue injection in prompt-builder):
|
|
9
|
+
* - Claude's analysis prompt now contains the rule catalogue.
|
|
10
|
+
* - For each finding, Claude populates `details[].rule` with the matching
|
|
11
|
+
* JBE-NNN / UIK-NNN ID — or leaves it empty if nothing matched.
|
|
12
|
+
* - We treat empty `rule` as "AI saw something real but couldn't tag it"
|
|
13
|
+
* — i.e. the registry has a gap. Those are valuable signal for the
|
|
14
|
+
* `automation-skills retro` loop.
|
|
15
|
+
*
|
|
16
|
+
* Per the maintainer's `feedback_skill_improvements_user_approval` rule,
|
|
17
|
+
* we never auto-merge into improvement-registry.md or promote candidates
|
|
18
|
+
* to rules. We write them to skill-feedback.md as candidates only; the
|
|
19
|
+
* user reviews and approves via `automation-skills retro` later.
|
|
20
|
+
*
|
|
21
|
+
* Dedup: we skip writing if an entry with the same (scope, file, message-hash)
|
|
22
|
+
* already exists in skill-feedback.md. Prevents the same recurring AI
|
|
23
|
+
* finding from spamming the doc on every commit.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
27
|
+
import { join, extname } from 'path';
|
|
28
|
+
import { createHash } from 'crypto';
|
|
29
|
+
import { SCOPE_BY_EXT } from './parser.js';
|
|
30
|
+
|
|
31
|
+
const SKILL_PATHS = {
|
|
32
|
+
backend: 'skills/backend/automation-standards-backend/docs/skill-feedback.md',
|
|
33
|
+
frontend: 'skills/frontend/automation-standards/docs/skill-feedback.md',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Process the AI's analysis details, write skill-gap candidates as needed.
|
|
38
|
+
*
|
|
39
|
+
* @param {object} aiResult - The `result` returned by analysis-engine.runAnalysis
|
|
40
|
+
* (shape: { issues, details: [{file, line, message, severity, rule, type}], ... })
|
|
41
|
+
* @param {object} context - { skillRoot, branch, repoName }
|
|
42
|
+
* @returns {{ written: number, skippedDuplicate: number, skippedClassified: number,
|
|
43
|
+
* candidatesByScope: { backend?: Array, frontend?: Array } }}
|
|
44
|
+
*/
|
|
45
|
+
export function writeSkillGapCandidates(aiResult, context) {
|
|
46
|
+
const { skillRoot, branch, repoName } = context;
|
|
47
|
+
if (!skillRoot) return emptyResult();
|
|
48
|
+
if (!aiResult || !Array.isArray(aiResult.details)) return emptyResult();
|
|
49
|
+
|
|
50
|
+
// Bucket unclassified details by scope.
|
|
51
|
+
const byScope = { backend: [], frontend: [] };
|
|
52
|
+
for (const d of aiResult.details) {
|
|
53
|
+
if (d.rule && /^[A-Z]+-\d{3}$/.test(d.rule)) continue; // already classified
|
|
54
|
+
if (!d.file || !d.message) continue; // can't dedup without these
|
|
55
|
+
const ext = extname(String(d.file)).toLowerCase();
|
|
56
|
+
const scope = SCOPE_BY_EXT[ext];
|
|
57
|
+
if (!scope) continue; // unknown scope, ignore
|
|
58
|
+
byScope[scope].push(d);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const date = today();
|
|
62
|
+
let written = 0;
|
|
63
|
+
let skippedDuplicate = 0;
|
|
64
|
+
let skippedClassified = 0;
|
|
65
|
+
const candidatesByScope = {};
|
|
66
|
+
|
|
67
|
+
// Count classified for the stats output.
|
|
68
|
+
for (const d of aiResult.details) {
|
|
69
|
+
if (d.rule && /^[A-Z]+-\d{3}$/.test(d.rule)) skippedClassified++;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const scope of ['backend', 'frontend']) {
|
|
73
|
+
const candidates = byScope[scope];
|
|
74
|
+
if (candidates.length === 0) continue;
|
|
75
|
+
|
|
76
|
+
const filePath = join(skillRoot, SKILL_PATHS[scope]);
|
|
77
|
+
if (!existsSync(filePath)) continue;
|
|
78
|
+
|
|
79
|
+
const existing = readFileSync(filePath, 'utf8');
|
|
80
|
+
const existingHashes = extractExistingHashes(existing);
|
|
81
|
+
|
|
82
|
+
const toWrite = [];
|
|
83
|
+
for (const detail of candidates) {
|
|
84
|
+
const fp = fingerprint(scope, detail);
|
|
85
|
+
if (existingHashes.has(fp)) {
|
|
86
|
+
skippedDuplicate++;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
toWrite.push({ detail, fingerprint: fp });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (toWrite.length === 0) continue;
|
|
93
|
+
|
|
94
|
+
const entry = composeEntry({
|
|
95
|
+
date, scope, repoName, branch, candidates: toWrite,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
appendToFeedback(filePath, entry);
|
|
99
|
+
written += toWrite.length;
|
|
100
|
+
candidatesByScope[scope] = toWrite.map((x) => x.detail);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { written, skippedDuplicate, skippedClassified, candidatesByScope };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Fingerprint a detail so repeat reports on the same file/message don't
|
|
108
|
+
* pile up entries across commits. (scope, file, hash(message-normalised))
|
|
109
|
+
*/
|
|
110
|
+
function fingerprint(scope, detail) {
|
|
111
|
+
const msg = String(detail.message || '').toLowerCase().replace(/\s+/g, ' ').trim();
|
|
112
|
+
const file = String(detail.file || '').replace(/\\/g, '/');
|
|
113
|
+
const hash = createHash('sha1').update(msg).digest('hex').slice(0, 12);
|
|
114
|
+
return `${scope}::${file}::${hash}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Walk the existing skill-feedback.md and collect fingerprints of entries
|
|
119
|
+
* already present (we embed the fingerprint as an HTML comment so the
|
|
120
|
+
* dedup is durable even after the file is hand-edited).
|
|
121
|
+
*/
|
|
122
|
+
function extractExistingHashes(content) {
|
|
123
|
+
const set = new Set();
|
|
124
|
+
const re = /<!--\s*skill-gap-fp:\s*([^\s>]+)\s*-->/g;
|
|
125
|
+
let m;
|
|
126
|
+
while ((m = re.exec(content)) !== null) {
|
|
127
|
+
set.add(m[1]);
|
|
128
|
+
}
|
|
129
|
+
return set;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Compose a single dated entry covering N candidates from the same scope
|
|
134
|
+
* + branch + commit. Each candidate becomes one bullet.
|
|
135
|
+
*/
|
|
136
|
+
function composeEntry({ date, scope, repoName, branch, candidates }) {
|
|
137
|
+
const title = '[skill-gap candidates from claude-git-hooks AI analysis]';
|
|
138
|
+
const ctx = [
|
|
139
|
+
repoName ? `**Repo:** ${repoName}` : null,
|
|
140
|
+
branch ? `**Branch:** ${branch}` : null,
|
|
141
|
+
].filter(Boolean).join('\n');
|
|
142
|
+
|
|
143
|
+
const bullets = candidates.map(({ detail, fingerprint: fp }) => {
|
|
144
|
+
const file = String(detail.file).replace(/\\/g, '/');
|
|
145
|
+
const line = detail.line ? `:${detail.line}` : '';
|
|
146
|
+
const sev = detail.severity ? ` (${detail.severity})` : '';
|
|
147
|
+
const type = detail.type ? ` [${detail.type}]` : '';
|
|
148
|
+
const msg = String(detail.message).replace(/\n/g, ' ').slice(0, 300);
|
|
149
|
+
return `- **[skill-gap]** ${file}${line}${type}${sev} — ${msg} <!-- skill-gap-fp: ${fp} -->`;
|
|
150
|
+
}).join('\n');
|
|
151
|
+
|
|
152
|
+
return `### ${date} — ${title} (${scope})
|
|
153
|
+
|
|
154
|
+
${ctx}
|
|
155
|
+
|
|
156
|
+
**Source:** Claude AI analysis flagged these findings, but none matched a JBE/UIK rule in the catalogue. They're recorded here for review during the next \`automation-skills retro\`. If a recurring pattern emerges, file a new registry entry.
|
|
157
|
+
|
|
158
|
+
**Candidates:**
|
|
159
|
+
|
|
160
|
+
${bullets}
|
|
161
|
+
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Append the entry to skill-feedback.md, after the "How to recognize a
|
|
167
|
+
* skill-caused issue" section and any existing dated entries, but before
|
|
168
|
+
* "How this compounds" (the footer).
|
|
169
|
+
*/
|
|
170
|
+
function appendToFeedback(filePath, entry) {
|
|
171
|
+
let content = readFileSync(filePath, 'utf8');
|
|
172
|
+
|
|
173
|
+
// Replace the placeholder "(none yet — …)" line if present; otherwise
|
|
174
|
+
// insert before "## How this compounds".
|
|
175
|
+
const placeholder = /_\(none yet — the first entry will be appended below\.\)_\n/;
|
|
176
|
+
if (placeholder.test(content)) {
|
|
177
|
+
content = content.replace(placeholder, `${entry.trimEnd() }\n`);
|
|
178
|
+
} else {
|
|
179
|
+
const footer = '## How this compounds';
|
|
180
|
+
const idx = content.indexOf(footer);
|
|
181
|
+
if (idx === -1) {
|
|
182
|
+
content = `${content.trimEnd() }\n\n${ entry}`;
|
|
183
|
+
} else {
|
|
184
|
+
content = `${content.slice(0, idx).trimEnd() }\n\n${ entry }---\n\n${ content.slice(idx)}`;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
writeFileSync(filePath, content, 'utf8');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function emptyResult() {
|
|
191
|
+
return { written: 0, skippedDuplicate: 0, skippedClassified: 0, candidatesByScope: {} };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function today() {
|
|
195
|
+
return new Date().toISOString().slice(0, 10);
|
|
196
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File: skill-registry/index.js
|
|
3
|
+
* Purpose: Public API (facade) for the mscope automation-skills integration.
|
|
4
|
+
*
|
|
5
|
+
* Integration entry points — consumers should only need these three:
|
|
6
|
+
* - runSkillChecks(...) — freshness + registry load + line-level checks
|
|
7
|
+
* + display + blockOn gating decision.
|
|
8
|
+
* - getCatalogueSection(...) — rendered rule catalogue for the analysis
|
|
9
|
+
* prompt (memoized registry load).
|
|
10
|
+
* - recordSkillGaps(...) — append unclassified AI findings to the
|
|
11
|
+
* skill repo's skill-feedback.md.
|
|
12
|
+
*
|
|
13
|
+
* Lower-level building blocks remain exported for tests and advanced use:
|
|
14
|
+
* - loadRegistry() — parse the JBE/UIK rule catalogue.
|
|
15
|
+
* - runRules(...) — line-level pre-commit checks against staged files.
|
|
16
|
+
* - displayFindings(...) — formatted console output.
|
|
17
|
+
* - checkFreshness() — non-blocking call to `automation-skills check`.
|
|
18
|
+
*
|
|
19
|
+
* Designed so the pre-commit hook can do:
|
|
20
|
+
*
|
|
21
|
+
* import * as skill from '../utils/skill-registry/index.js';
|
|
22
|
+
* const check = skill.runSkillChecks(stagedAbsPaths, { repoRoot, config });
|
|
23
|
+
* if (check.shouldBlock) process.exit(1); // exit stays in the hook
|
|
24
|
+
*
|
|
25
|
+
* Failure mode is always "silent skip" — if the skill repo isn't found, or
|
|
26
|
+
* the registry is malformed, the hook continues without these checks. The
|
|
27
|
+
* mscope skill is a value-add, not a hard dependency of this repo.
|
|
28
|
+
*
|
|
29
|
+
* No function in this module calls process.exit() — utils return decisions,
|
|
30
|
+
* the CLI/hook layer acts on them (repo convention).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export { resolveSkillRepoRoot, loadRegistry, SCOPE_BY_EXT } from './parser.js';
|
|
34
|
+
export { runRules } from './runner.js';
|
|
35
|
+
|
|
36
|
+
import { spawnSync } from 'child_process';
|
|
37
|
+
import { loadRegistry } from './parser.js';
|
|
38
|
+
import { runRules } from './runner.js';
|
|
39
|
+
import { renderCatalogue } from './catalogue.js';
|
|
40
|
+
import { writeSkillGapCandidates } from './feedback-writer.js';
|
|
41
|
+
import logger from '../logger.js';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Severity rank used by the blockOn gate. Lower = more severe.
|
|
45
|
+
* 'never' (or any unknown value) disables gating.
|
|
46
|
+
*/
|
|
47
|
+
const SEVERITY_RANK = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Process-lifetime cache of registry loads, keyed by the workingRepoRoot
|
|
51
|
+
* used for resolution. Lets runSkillChecks and getCatalogueSection share
|
|
52
|
+
* a single filesystem pass per commit.
|
|
53
|
+
*/
|
|
54
|
+
const registryCache = new Map();
|
|
55
|
+
|
|
56
|
+
function loadRegistryCached(workingRepoRoot) {
|
|
57
|
+
const key = workingRepoRoot || '';
|
|
58
|
+
if (!registryCache.has(key)) {
|
|
59
|
+
registryCache.set(key, loadRegistry(null, workingRepoRoot));
|
|
60
|
+
}
|
|
61
|
+
return registryCache.get(key);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Test hook — clears the memoized registry. Not for production use.
|
|
66
|
+
*/
|
|
67
|
+
export function _resetRegistryCache() {
|
|
68
|
+
registryCache.clear();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* One-call integration point for the pre-commit hook: freshness warning,
|
|
73
|
+
* registry load (memoized), rule run, console display, and the blockOn
|
|
74
|
+
* gating decision.
|
|
75
|
+
*
|
|
76
|
+
* Never calls process.exit — returns `shouldBlock` and lets the hook decide.
|
|
77
|
+
* Throws only on unexpected internal errors (callers wrap in try/catch).
|
|
78
|
+
*
|
|
79
|
+
* @param {string[]} absPaths - Absolute paths of staged files to check.
|
|
80
|
+
* @param {object} options
|
|
81
|
+
* @param {string} options.repoRoot - Working repo root (for sibling resolution + relative display).
|
|
82
|
+
* @param {object} options.config - Merged config; reads config.skillRegistry.blockOn.
|
|
83
|
+
* @returns {{ skillRoot: string|null, rules: Array, findings: Array,
|
|
84
|
+
* totalFindings: number, shouldBlock: boolean, triggeringCount: number }}
|
|
85
|
+
*/
|
|
86
|
+
export function runSkillChecks(absPaths, { repoRoot, config } = {}) {
|
|
87
|
+
const freshness = checkFreshness();
|
|
88
|
+
if (freshness.status === 'stale') {
|
|
89
|
+
logger.warning(`📚 mscope skill is behind: installed v${freshness.installed}, latest v${freshness.latest}. Run: automation-skills update`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const { rules, skillRoot } = loadRegistryCached(repoRoot);
|
|
93
|
+
if (!skillRoot || rules.length === 0) {
|
|
94
|
+
if (!skillRoot) {
|
|
95
|
+
logger.debug('skill-registry - runSkillChecks', 'Skill registry not found — skipping skill-driven checks');
|
|
96
|
+
}
|
|
97
|
+
return { skillRoot, rules: rules || [], findings: [], totalFindings: 0, shouldBlock: false, triggeringCount: 0 };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const result = runRules(rules, absPaths, { repoRoot });
|
|
101
|
+
displayFindings(result);
|
|
102
|
+
|
|
103
|
+
// Optional gating: blockOn = 'critical' | 'high' | 'medium' | 'low' | 'never'
|
|
104
|
+
// (default: 'never' — warn-only for incremental adoption).
|
|
105
|
+
const blockOn = config?.skillRegistry?.blockOn || 'never';
|
|
106
|
+
const threshold = SEVERITY_RANK[blockOn] ?? -1;
|
|
107
|
+
const triggering = threshold >= 0
|
|
108
|
+
? result.findings.filter((f) => (SEVERITY_RANK[f.severity] ?? 99) <= threshold)
|
|
109
|
+
: [];
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
skillRoot,
|
|
113
|
+
rules,
|
|
114
|
+
findings: result.findings,
|
|
115
|
+
totalFindings: result.totalFindings,
|
|
116
|
+
shouldBlock: triggering.length > 0,
|
|
117
|
+
triggeringCount: triggering.length,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Rendered `=== PLATFORM ANTIPATTERN CATALOGUE ===` body for injection into
|
|
123
|
+
* the Claude analysis prompt. Uses the memoized registry load, so calling
|
|
124
|
+
* this after runSkillChecks costs nothing.
|
|
125
|
+
*
|
|
126
|
+
* Never throws — the catalogue is value-add; on any failure returns null.
|
|
127
|
+
*
|
|
128
|
+
* @param {object} options
|
|
129
|
+
* @param {string} options.repoRoot - Working repo root for sibling resolution.
|
|
130
|
+
* @returns {string|null} Markdown catalogue, or null if unavailable/empty.
|
|
131
|
+
*/
|
|
132
|
+
export function getCatalogueSection({ repoRoot } = {}) {
|
|
133
|
+
try {
|
|
134
|
+
const { rules, skillRoot } = loadRegistryCached(repoRoot);
|
|
135
|
+
if (!skillRoot || rules.length === 0) return null;
|
|
136
|
+
return renderCatalogue(rules) || null;
|
|
137
|
+
} catch (err) {
|
|
138
|
+
logger.debug('skill-registry - getCatalogueSection', 'Catalogue unavailable', { error: err?.message });
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Append skill-gap candidates (AI findings with no catalogue rule ID) to the
|
|
145
|
+
* skill repo's skill-feedback.md. Thin wrapper over writeSkillGapCandidates;
|
|
146
|
+
* throws on write failure (caller decides how loud to be).
|
|
147
|
+
*
|
|
148
|
+
* @param {object} aiResult - Result from analysis-engine.runAnalysis.
|
|
149
|
+
* @param {object} context - { skillRoot, branch, repoName }
|
|
150
|
+
* @returns {{ written: number, skippedDuplicate: number, skippedClassified: number }}
|
|
151
|
+
*/
|
|
152
|
+
export function recordSkillGaps(aiResult, context) {
|
|
153
|
+
return writeSkillGapCandidates(aiResult, context);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Run `automation-skills check --json` to find out if the installed skill
|
|
158
|
+
* is behind the latest. Returns:
|
|
159
|
+
* { status: 'fresh' | 'stale' | 'unknown', installed, latest }
|
|
160
|
+
*
|
|
161
|
+
* Never throws; never blocks the hook. If the CLI isn't installed or the
|
|
162
|
+
* check fails, returns { status: 'unknown' }.
|
|
163
|
+
*/
|
|
164
|
+
export function checkFreshness({ timeoutMs = 3000 } = {}) {
|
|
165
|
+
try {
|
|
166
|
+
const res = spawnSync('automation-skills', ['check', '--json'], {
|
|
167
|
+
timeout: timeoutMs,
|
|
168
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
169
|
+
});
|
|
170
|
+
if (res.error || (res.status !== 0 && res.status !== 1)) {
|
|
171
|
+
return { status: 'unknown' };
|
|
172
|
+
}
|
|
173
|
+
const line = (res.stdout || '').toString().trim();
|
|
174
|
+
if (!line) return { status: 'unknown' };
|
|
175
|
+
const parsed = JSON.parse(line);
|
|
176
|
+
return {
|
|
177
|
+
status: parsed.status === 'stale' ? 'stale'
|
|
178
|
+
: parsed.status === 'fresh' ? 'fresh'
|
|
179
|
+
: parsed.status === 'ahead' ? 'ahead'
|
|
180
|
+
: 'unknown',
|
|
181
|
+
installed: parsed.installed,
|
|
182
|
+
latest: parsed.latest,
|
|
183
|
+
};
|
|
184
|
+
} catch {
|
|
185
|
+
return { status: 'unknown' };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Print a formatted summary of pre-commit findings (from runRules) to
|
|
191
|
+
* stdout. Returns nothing.
|
|
192
|
+
*
|
|
193
|
+
* Format intentionally mirrors the existing hook's `displayResults` style
|
|
194
|
+
* so it slots in naturally.
|
|
195
|
+
*
|
|
196
|
+
* 📚 mscope skill registry — 2 findings (1 high, 1 medium)
|
|
197
|
+
*
|
|
198
|
+
* [JBE-016] medium Wildcard import (`import x.*;`)
|
|
199
|
+
* file.java:12 import jakarta.persistence.*;
|
|
200
|
+
* → see automation-standards-backend/docs/improvement-registry.md#jbe-016
|
|
201
|
+
*/
|
|
202
|
+
export function displayFindings({ totalFindings, findings, bySeverity }) {
|
|
203
|
+
if (totalFindings === 0) {
|
|
204
|
+
logger.info('📚 mscope skill registry — no rule violations in staged files.');
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const sev = bySeverity || {};
|
|
209
|
+
const sevParts = [];
|
|
210
|
+
if (sev.critical) sevParts.push(`${sev.critical} 🔴 critical`);
|
|
211
|
+
if (sev.high) sevParts.push(`${sev.high} 🟠 high`);
|
|
212
|
+
if (sev.medium) sevParts.push(`${sev.medium} 🟡 medium`);
|
|
213
|
+
if (sev.low) sevParts.push(`${sev.low} 🟢 low`);
|
|
214
|
+
|
|
215
|
+
logger.info(`📚 mscope skill registry — ${totalFindings} finding${totalFindings === 1 ? '' : 's'}${
|
|
216
|
+
sevParts.length ? ` (${sevParts.join(', ')})` : ''}`);
|
|
217
|
+
logger.info('');
|
|
218
|
+
|
|
219
|
+
// Group findings by rule for compact output.
|
|
220
|
+
const byRule = {};
|
|
221
|
+
for (const f of findings) {
|
|
222
|
+
(byRule[f.ruleId] ||= { rule: f, occurrences: [] }).occurrences.push(f);
|
|
223
|
+
}
|
|
224
|
+
const rulesSorted = Object.keys(byRule).sort((a, b) => {
|
|
225
|
+
// High severity first, then by rule id.
|
|
226
|
+
const order = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
|
|
227
|
+
const sa = order[byRule[a].rule.severity] ?? 99;
|
|
228
|
+
const sb = order[byRule[b].rule.severity] ?? 99;
|
|
229
|
+
if (sa !== sb) return sa - sb;
|
|
230
|
+
return a.localeCompare(b);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
for (const ruleId of rulesSorted) {
|
|
234
|
+
const { rule, occurrences } = byRule[ruleId];
|
|
235
|
+
const sevTag = sevTagFor(rule.severity);
|
|
236
|
+
logger.info(` [${ruleId}] ${sevTag} ${rule.title}`);
|
|
237
|
+
for (const occ of occurrences.slice(0, 5)) {
|
|
238
|
+
logger.info(` ${occ.file}:${occ.line} ${occ.lineText.slice(0, 100)}`);
|
|
239
|
+
}
|
|
240
|
+
if (occurrences.length > 5) {
|
|
241
|
+
logger.info(` … and ${occurrences.length - 5} more occurrence${occurrences.length - 5 === 1 ? '' : 's'}`);
|
|
242
|
+
}
|
|
243
|
+
logger.info(` → see ${rule.scope}/improvement-registry.md → ${ruleId}`);
|
|
244
|
+
logger.info('');
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function sevTagFor(sev) {
|
|
249
|
+
if (sev === 'critical') return '🔴 critical';
|
|
250
|
+
if (sev === 'high') return '🟠 high';
|
|
251
|
+
if (sev === 'medium') return '🟡 medium';
|
|
252
|
+
if (sev === 'low') return '🟢 low';
|
|
253
|
+
return `⚪ ${ sev}`;
|
|
254
|
+
}
|