From 34669d596c1b7592456635b175b81c74fd578ed2 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 06:27:34 +0200 Subject: [PATCH] feat(config-audit): TOK consumes readActiveConfig (v5 F1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removes the v4 'void readActiveConfig' placeholder and wires the active-config snapshot into the TOK scanner. Per-turn behavior changes: - Each enabled MCP server becomes its own hotspot entry (richer than the parent .mcp.json file alone) - total_estimated_tokens now includes MCP server cost - result.activeConfig exposes a small summary (claudeMdEstimatedTokens, mcpServerCount, pluginCount, skillCount) Failures of readActiveConfig are non-fatal — the scanner falls back to the discovery-only path used in v4. Tests: +3 cases on the new tok-active-config fixture (.mcp.json with 2 servers, CLAUDE.md, plugin skeleton). --- .../config-audit/scanners/token-hotspots.mjs | 53 +++++++++++++++---- .../.claude-plugin/plugin.json | 5 ++ .../fixtures/tok-active-config/.mcp.json | 12 +++++ .../fixtures/tok-active-config/CLAUDE.md | 14 +++++ .../tok-active-config/commands/sample.md | 11 ++++ .../tests/scanners/token-hotspots.test.mjs | 29 ++++++++++ 6 files changed, 115 insertions(+), 9 deletions(-) create mode 100644 plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json create mode 100644 plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json create mode 100644 plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md create mode 100644 plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md diff --git a/plugins/config-audit/scanners/token-hotspots.mjs b/plugins/config-audit/scanners/token-hotspots.mjs index 9e8f241..f88ccef 100644 --- a/plugins/config-audit/scanners/token-hotspots.mjs +++ b/plugins/config-audit/scanners/token-hotspots.mjs @@ -24,12 +24,6 @@ import { SEVERITY } from './lib/severity.mjs'; import { findImports, parseJson } from './lib/yaml-parser.mjs'; import { estimateTokens, readActiveConfig } from './lib/active-config-reader.mjs'; -// readActiveConfig is exposed here for future integration when the TOK scanner -// expands to cross-cascade hotspot ranking (plugins, skills, MCP). Today the -// scanner uses the per-file discovery shape so it stays test-isolated and does -// not pull in the user's real ~/.claude/ state. -void readActiveConfig; - const SCANNER = 'TOK'; const VOLATILE_TOP_LINES = 30; @@ -179,8 +173,12 @@ function detectSonnetEra(discovery) { /** * Build the ranked hotspots array. + * + * v5 F1: when activeConfig is available, expand each MCP server into its own + * hotspot entry (richer signal than the parent .mcp.json file). Discovery + * files remain the primary source for CLAUDE.md / settings / skills. */ -async function buildHotspots(discovery, targetPath) { +async function buildHotspots(discovery, targetPath, activeConfig) { const ranked = []; for (const f of discovery.files) { const kind = tokenKind(f.type); @@ -195,6 +193,21 @@ async function buildHotspots(discovery, targetPath) { estimated_tokens: tokens, }); } + // Per-MCP-server entries from activeConfig (each ~500+ tokens at runtime, + // not represented by the parent .mcp.json file size alone). + if (activeConfig && Array.isArray(activeConfig.mcpServers)) { + for (const m of activeConfig.mcpServers) { + if (!m || !m.enabled) continue; + ranked.push({ + absPath: m.source || `mcp:${m.name}`, + relPath: `mcp:${m.name} (${m.source})`, + type: 'mcp-server', + scope: m.source, + size: 0, + estimated_tokens: m.estimatedTokens || 0, + }); + } + } ranked.sort((a, b) => b.estimated_tokens - a.estimated_tokens); // If we have fewer than HOTSPOTS_MIN entries, pad with placeholder entries @@ -259,6 +272,15 @@ export async function scan(targetPath, discovery) { let filesScanned = 0; const contentCache = new Map(); + // v5 F1: pull active-config snapshot once. Failures are non-fatal — the + // scanner falls back to the discovery-only path used in v4. + let activeConfig = null; + try { + activeConfig = await readActiveConfig(targetPath, {}); + } catch { + activeConfig = null; + } + // ── Pattern A: cache-breaking volatile top in CLAUDE.md ── for (const f of discovery.files) { if (f.type !== 'claude-md') continue; @@ -350,16 +372,29 @@ export async function scan(targetPath, discovery) { } // ── Hotspots ranking ── - const hotspots = await buildHotspots(discovery, targetPath); + const hotspots = await buildHotspots(discovery, targetPath, activeConfig); - // ── Total estimated tokens (sum of every discovered source) ── + // ── Total estimated tokens (sum of every discovered source + activeConfig MCP) ── let totalTokens = 0; for (const f of discovery.files) { totalTokens += estimateTokens(f.size, tokenKind(f.type)); } + if (activeConfig && Array.isArray(activeConfig.mcpServers)) { + for (const m of activeConfig.mcpServers) { + if (m && m.enabled) totalTokens += m.estimatedTokens || 0; + } + } const result = scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start); result.hotspots = hotspots; result.total_estimated_tokens = totalTokens; + if (activeConfig) { + result.activeConfig = { + claudeMdEstimatedTokens: activeConfig.claudeMd?.estimatedTokens ?? 0, + mcpServerCount: activeConfig.mcpServers?.length ?? 0, + pluginCount: activeConfig.plugins?.length ?? 0, + skillCount: activeConfig.skills?.length ?? 0, + }; + } return result; } diff --git a/plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json b/plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json new file mode 100644 index 0000000..27b4be8 --- /dev/null +++ b/plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json @@ -0,0 +1,5 @@ +{ + "name": "tok-active-config", + "description": "Fixture plugin for TOK scanner active-config integration test", + "version": "0.0.1" +} diff --git a/plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json b/plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json new file mode 100644 index 0000000..3402c87 --- /dev/null +++ b/plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json @@ -0,0 +1,12 @@ +{ + "mcpServers": { + "alpha": { + "command": "npx", + "args": ["alpha-server"] + }, + "beta": { + "command": "npx", + "args": ["beta-server"] + } + } +} diff --git a/plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md b/plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md new file mode 100644 index 0000000..53fea54 --- /dev/null +++ b/plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md @@ -0,0 +1,14 @@ +# Tok Active-Config Fixture + +A small Claude Code-shaped project used by the TOK scanner integration test. + +## Purpose + +Verify that the TOK scanner consumes `readActiveConfig` output: MCP servers +appear in hotspots and the CLAUDE.md cascade contributes a non-zero token +estimate when active-config integration is wired up (v5 F1). + +## Notes + +This file is intentionally larger than a one-liner so the cascade contributes +visible tokens to `activeConfig.claudeMd.estimatedTokens`. diff --git a/plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md b/plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md new file mode 100644 index 0000000..6cb2814 --- /dev/null +++ b/plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md @@ -0,0 +1,11 @@ +--- +name: sample +description: Sample command in the tok-active-config fixture +model: sonnet +--- + +# /sample + +A trivial command body so the file has both frontmatter and content. The TOK +scanner ranks command sources by their estimated tokens; this is bigger than +zero, smaller than CLAUDE.md. diff --git a/plugins/config-audit/tests/scanners/token-hotspots.test.mjs b/plugins/config-audit/tests/scanners/token-hotspots.test.mjs index fd0d976..1a71edb 100644 --- a/plugins/config-audit/tests/scanners/token-hotspots.test.mjs +++ b/plugins/config-audit/tests/scanners/token-hotspots.test.mjs @@ -112,6 +112,35 @@ describe('TOK scanner — marketplace scale ordering', () => { }); }); +describe('TOK scanner — readActiveConfig integration (v5 F1)', () => { + let result; + beforeEach(async () => { + result = await runScanner('tok-active-config'); + }); + + it('exposes activeConfig summary on the result (proves readActiveConfig was called)', () => { + assert.ok(result.activeConfig, 'expected result.activeConfig to be set'); + assert.equal(typeof result.activeConfig.claudeMdEstimatedTokens, 'number'); + assert.ok(result.activeConfig.claudeMdEstimatedTokens > 0, + `expected claudeMd cascade > 0 tokens, got ${result.activeConfig.claudeMdEstimatedTokens}`); + }); + + it('hotspots include at least one MCP-source entry', () => { + const hasMcp = result.hotspots.some(h => /mcp/i.test(h.source)); + assert.ok(hasMcp, + `expected hotspots to include an MCP source; got: ${result.hotspots.map(h => h.source).join(', ')}`); + }); + + it('total_estimated_tokens exceeds the minimal sonnet-era baseline', async () => { + // sonnet-era has no .mcp.json — the activeConfig MCP entries from this + // fixture should push its total above sonnet-era's even when both fixtures + // share the user's ambient cascade/plugin state. + const baseline = await runScanner('opus-47/sonnet-era'); + assert.ok(result.total_estimated_tokens > baseline.total_estimated_tokens, + `expected ${result.total_estimated_tokens} > ${baseline.total_estimated_tokens}`); + }); +}); + describe('TOK scanner — hotspots contract', () => { let result; beforeEach(async () => {