diff --git a/README.md b/README.md index 6a32082..12a777f 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ ![Commands](https://img.shields.io/badge/commands-18-green) ![Agents](https://img.shields.io/badge/agents-6-orange) ![Hooks](https://img.shields.io/badge/hooks-4-red) -![Tests](https://img.shields.io/badge/tests-949+-brightgreen) +![Tests](https://img.shields.io/badge/tests-950+-brightgreen) ![License](https://img.shields.io/badge/license-MIT-lightgrey) A Claude Code plugin that checks configuration health, suggests context-aware improvements, and auto-fixes issues — `CLAUDE.md`, `settings.json`, hooks, rules, MCP servers, `@imports`, and plugins. 13 deterministic scanners across 10 quality areas, context-aware feature recommendations, auto-fix with backup/rollback, a prompt-cache-aware Token Hotspots scanner with optional API-calibrated `--accurate-tokens` mode, plus cache-prefix stability, dead-tool, and cross-plugin collision detection. Zero external dependencies. diff --git a/scanners/plugin-health-scanner.mjs b/scanners/plugin-health-scanner.mjs index 8bc6d06..1bffbfc 100644 --- a/scanners/plugin-health-scanner.mjs +++ b/scanners/plugin-health-scanner.mjs @@ -19,6 +19,19 @@ const SCANNER = 'PLH'; const REQUIRED_PLUGIN_JSON_FIELDS = ['name', 'description', 'version']; const RECOMMENDED_CLAUDE_MD_SECTIONS = ['commands', 'agents', 'hooks']; + +// A CLAUDE.md need only document the component types the plugin actually ships. Mirrors the +// optional-frontmatter rule: do not demand docs for commands/agents/hooks that do not exist. +async function pluginShipsComponent(pluginDir, section) { + if (section === 'hooks') { + try { await readFile(join(pluginDir, 'hooks', 'hooks.json'), 'utf-8'); return true; } + catch { return false; } + } + try { + const entries = await readdir(join(pluginDir, section)); + return entries.some(f => f.endsWith('.md')); + } catch { return false; } +} // Keys as they appear after yaml-parser normalizeKey (hyphens → underscores). // Field requirements are pinned to the primary docs, NOT to "every field a plugin could set": // - Commands/skills (code.claude.com/docs slash-commands): "All fields are optional. Only @@ -302,6 +315,9 @@ async function scanSinglePlugin(pluginDir) { const lower = content.toLowerCase(); for (const section of RECOMMENDED_CLAUDE_MD_SECTIONS) { + // Only require a section for a component the plugin actually ships (mirrors the + // optional-frontmatter rule — no docs demanded for absent commands/agents/hooks). + if (!(await pluginShipsComponent(pluginDir, section))) continue; // Look for markdown table header or section header const hasSection = lower.includes(`## ${section}`) || lower.includes(`| ${section}`) || diff --git a/tests/fixtures/plugin-section-coverage/.claude-plugin/plugin.json b/tests/fixtures/plugin-section-coverage/.claude-plugin/plugin.json new file mode 100644 index 0000000..989af7a --- /dev/null +++ b/tests/fixtures/plugin-section-coverage/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "section-coverage", + "description": "Fixture: ships one command, no agents or hooks", + "version": "1.0.0" +} diff --git a/tests/fixtures/plugin-section-coverage/CLAUDE.md b/tests/fixtures/plugin-section-coverage/CLAUDE.md new file mode 100644 index 0000000..64c96fd --- /dev/null +++ b/tests/fixtures/plugin-section-coverage/CLAUDE.md @@ -0,0 +1,7 @@ +# Section Coverage Fixture + +## Overview + +This plugin ships one command and no agents or hooks. Its CLAUDE.md intentionally omits the +Commands section, so the missing-section check should fire for `commands` (a component it has) +but NOT for `agents` or `hooks` (components it does not have). diff --git a/tests/fixtures/plugin-section-coverage/commands/foo.md b/tests/fixtures/plugin-section-coverage/commands/foo.md new file mode 100644 index 0000000..bc37b15 --- /dev/null +++ b/tests/fixtures/plugin-section-coverage/commands/foo.md @@ -0,0 +1,7 @@ +--- +description: A demo command for the section-coverage fixture +--- + +# Foo + +Demo command body. diff --git a/tests/scanners/plugin-health-scanner.test.mjs b/tests/scanners/plugin-health-scanner.test.mjs index b509193..e9fe9ab 100644 --- a/tests/scanners/plugin-health-scanner.test.mjs +++ b/tests/scanners/plugin-health-scanner.test.mjs @@ -13,6 +13,7 @@ const DUP_NAME = resolve(FIXTURES, 'duplicate-plugin-name'); const DUP_CMD = resolve(FIXTURES, 'duplicate-command-name'); const SHADOW = resolve(FIXTURES, 'plugin-shadow-folder'); const SKILLS_ARR = resolve(FIXTURES, 'plugin-skills-array'); +const SECTION_COV = resolve(FIXTURES, 'plugin-section-coverage'); describe('discoverPlugins', () => { it('discovers a single plugin when pointed at plugin dir', async () => { @@ -72,6 +73,25 @@ describe('scan on valid test-plugin', () => { }); }); +describe('CLAUDE.md section findings track present components', () => { + it('flags a missing section only for components the plugin actually ships', async () => { + resetCounter(); + const result = await scan(SECTION_COV); + // section-coverage ships commands/ but no agents/ or hooks, and its CLAUDE.md omits the + // Commands section. Per the component-aware rule: flag the present component's missing + // section, never require docs for absent components. + const sectionFindings = result.findings.filter(f => + f.scanner === 'PLH' && /missing.{0,40}section/i.test(f.title || '') + ); + assert.ok(sectionFindings.some(f => /command/i.test(f.title)), + 'missing Commands section must be flagged when commands/ exists'); + assert.ok(!sectionFindings.some(f => /agent/i.test(f.title)), + 'agents section must not be flagged (no agents/)'); + assert.ok(!sectionFindings.some(f => /hook/i.test(f.title)), + 'hooks section must not be flagged (no hooks)'); + }); +}); + describe('scan on broken-plugin', () => { it('detects missing plugin.json fields', async () => { resetCounter();