feat(config-audit): cross-plugin collision scanner COL (v5 N6) [skip-docs]

New COL scanner detects skill-name collisions across plugins and
between user-level skills (~/.claude/skills/) and plugin-bundled
skills. Skill identity is the directory basename — matches how
enumerateSkills resolves names.

Detection rules (per docs/v5-namespace-research.md, confidence: medium):
- Plugin-vs-plugin same skill name → severity low (CA-COL-001)
- User-vs-plugin same skill name → severity medium (CA-COL-001)
- Plugin-vs-built-in collisions: out of scope for v5.0.0 (insufficient
  verification — recorded for v5.0.1 follow-up).

Findings carry details.namespaces array with {source, name, path} for
every conflicting source — supports per-collision reporting downstream.

output.mjs: finding() helper now passes through optional `details`
field (scanner-specific structured payload).

scoring.mjs: COL → "Plugin Hygiene" (new area, 10 total). Posture test
updated from 9 → 10 area scores.

.gitignore: docs/v5-namespace-research.md is local-only (Step 22a
research output, gitignored per plan).

Fixture collision-plugins/fake-home/ has user skill `review` colliding
with plugin-a + plugin-b's `review` (medium severity), plus plugin-c's
unique `summarize` (no collision).

[skip-docs] reason: v5 plan fences off README/CLAUDE.md badge updates
to Session 5; Forgejo pre-commit-docs-gate hook requires this tag.

Tests: 617 → 625 (+8).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 07:46:15 +02:00
commit cd25c1e934
14 changed files with 307 additions and 3 deletions

View file

@ -26,12 +26,13 @@ export function resetCounter() {
* @param {string} [opts.category] - quality category
* @param {string} [opts.recommendation] - suggested fix
* @param {boolean} [opts.autoFixable] - can be auto-fixed
* @param {object} [opts.details] - structured details (scanner-specific shape)
* @returns {object}
*/
export function finding(opts) {
findingCounter++;
const id = `CA-${opts.scanner}-${String(findingCounter).padStart(3, '0')}`;
return {
const result = {
id,
scanner: opts.scanner,
severity: opts.severity,
@ -44,6 +45,10 @@ export function finding(opts) {
recommendation: opts.recommendation || null,
autoFixable: opts.autoFixable || false,
};
if (opts.details && typeof opts.details === 'object') {
result.details = opts.details;
}
return result;
}
/**