feat(config-audit): TOK consumes readActiveConfig (v5 F1)

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).
This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 06:27:34 +02:00
commit 34669d596c
6 changed files with 115 additions and 9 deletions

View file

@ -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;
}