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:
parent
ce7c42f517
commit
34669d596c
6 changed files with 115 additions and 9 deletions
|
|
@ -24,12 +24,6 @@ import { SEVERITY } from './lib/severity.mjs';
|
||||||
import { findImports, parseJson } from './lib/yaml-parser.mjs';
|
import { findImports, parseJson } from './lib/yaml-parser.mjs';
|
||||||
import { estimateTokens, readActiveConfig } from './lib/active-config-reader.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 SCANNER = 'TOK';
|
||||||
|
|
||||||
const VOLATILE_TOP_LINES = 30;
|
const VOLATILE_TOP_LINES = 30;
|
||||||
|
|
@ -179,8 +173,12 @@ function detectSonnetEra(discovery) {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the ranked hotspots array.
|
* 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 = [];
|
const ranked = [];
|
||||||
for (const f of discovery.files) {
|
for (const f of discovery.files) {
|
||||||
const kind = tokenKind(f.type);
|
const kind = tokenKind(f.type);
|
||||||
|
|
@ -195,6 +193,21 @@ async function buildHotspots(discovery, targetPath) {
|
||||||
estimated_tokens: tokens,
|
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);
|
ranked.sort((a, b) => b.estimated_tokens - a.estimated_tokens);
|
||||||
|
|
||||||
// If we have fewer than HOTSPOTS_MIN entries, pad with placeholder entries
|
// 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;
|
let filesScanned = 0;
|
||||||
const contentCache = new Map();
|
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 ──
|
// ── Pattern A: cache-breaking volatile top in CLAUDE.md ──
|
||||||
for (const f of discovery.files) {
|
for (const f of discovery.files) {
|
||||||
if (f.type !== 'claude-md') continue;
|
if (f.type !== 'claude-md') continue;
|
||||||
|
|
@ -350,16 +372,29 @@ export async function scan(targetPath, discovery) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Hotspots ranking ──
|
// ── 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;
|
let totalTokens = 0;
|
||||||
for (const f of discovery.files) {
|
for (const f of discovery.files) {
|
||||||
totalTokens += estimateTokens(f.size, tokenKind(f.type));
|
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);
|
const result = scannerResult(SCANNER, 'ok', findings, filesScanned, Date.now() - start);
|
||||||
result.hotspots = hotspots;
|
result.hotspots = hotspots;
|
||||||
result.total_estimated_tokens = totalTokens;
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json
vendored
Normal file
5
plugins/config-audit/tests/fixtures/tok-active-config/.claude-plugin/plugin.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"name": "tok-active-config",
|
||||||
|
"description": "Fixture plugin for TOK scanner active-config integration test",
|
||||||
|
"version": "0.0.1"
|
||||||
|
}
|
||||||
12
plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json
vendored
Normal file
12
plugins/config-audit/tests/fixtures/tok-active-config/.mcp.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"alpha": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["alpha-server"]
|
||||||
|
},
|
||||||
|
"beta": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["beta-server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md
vendored
Normal file
14
plugins/config-audit/tests/fixtures/tok-active-config/CLAUDE.md
vendored
Normal file
|
|
@ -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`.
|
||||||
11
plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md
vendored
Normal file
11
plugins/config-audit/tests/fixtures/tok-active-config/commands/sample.md
vendored
Normal file
|
|
@ -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.
|
||||||
|
|
@ -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', () => {
|
describe('TOK scanner — hotspots contract', () => {
|
||||||
let result;
|
let result;
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue