config-audit/scanners/token-hotspots-cli.mjs
Kjell Tore Guttormsen caea8aca23 fix(commands): stop answering questions the caller did not ask
Dogfooding `campaign` + `knowledge-refresh` against a throwaway ledger. Seven
defects, all found by running the commands as written and measuring, not by
reading them.

The headline pair only existed together. `knowledge-refresh` built
`STALE_AFTER="--stale-after 30"` and expanded it unquoted, trusting the shell to
split it in two. bash does; zsh — the macOS default, and what the Bash tool runs
here — does not. The CLI got one argv entry, matched no flag, and because it had
no unknown-flag branch, silently kept the 90-day default and reported "✓ All 14
register entries were re-verified within the last 90 days": a true-sounding
sentence about a threshold the user had just overridden. Fixing either half alone
leaves a silent wrong answer or a loud one; both are fixed, and a guard now
rejects any template that packs a flag and its value into one variable.

`knowledge-refresh` also read one register and wrote another: step 6 named an
unanchored `knowledge/best-practices.json` while the CLI reads
`${CLAUDE_PLUGIN_ROOT}/…`, which for an installed plugin is the cache. The
validation gate then ran the cached test against the cached register — green no
matter what was written. The two copies were byte-identical that day, which is
exactly why it was invisible.

`campaign` vouched for repos it could not read. `add /finnes/ikke` returned
`added` + exit 0; `refresh-tokens` then put the phantom in `swept[]` with a
0-token delta and left `skipped[]` empty, so the machine-wide bill claimed
coverage of three repos on a machine with two. Paths stay tracked — an unmounted
volume is a legitimate absence — but are reported as `addedUnverified`, and the
command names them.

Two class sweeps, both measured rather than assumed. `posture` was the single
scanner (1 of 14) whose fatal catch exited 1, which ux-rules defines as a normal
WARNING grade — a crash indistinguishable from a result. And all 13 payload
writers failed on a `--output-file` whose parent did not exist, which on a fresh
machine turned `campaign`'s first run into "the ledger may be corrupt"; they now
share `scanners/lib/write-output.mjs`.

Predicted breadth was too wide for the first time in five sessions: 6 of 8 CLIs
predicted to lack unknown-flag rejection, 4 measured. `drift` and `fix` already
reject them, via a construct the grep did not recognise — a grep matches an
implementation, the invariant is a behaviour. The sweep was rewritten to run each
CLI with a bogus flag and read the exit code.

Suite 1453 → 1469/0. Frozen snapshots untouched. `optimize-lens-cli` and
`token-hotspots-cli` share the unknown-flag defect and are deferred to the v5.14
argument-handling chunk with their positional-swallow arm; the count is recorded
in the guard rather than rounded down to zero.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012NHWjN8EnoxSqRvMTLK2NE
2026-08-01 21:26:39 +02:00

149 lines
4.8 KiB
JavaScript
Executable file

#!/usr/bin/env node
/**
* token-hotspots CLI — emit ranked token hotspots and prompt-cache pattern findings
* for a target repo path.
*
* Usage:
* node token-hotspots-cli.mjs [path] [--json] [--output-file <path>] [--global]
* [--with-telemetry-recipe] [--accurate-tokens]
*
* Exit codes: 0=ok, 3=unrecoverable error.
* Zero external dependencies.
*/
import { resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { readFile, stat } from 'node:fs/promises';
import { writeOutputFile } from './lib/write-output.mjs';
import { discoverConfigFiles } from './lib/file-discovery.mjs';
import { resetCounter } from './lib/output.mjs';
import { scan } from './token-hotspots.mjs';
import * as tokenizerApi from './lib/tokenizer-api.mjs';
import { humanizeFindings } from './lib/humanizer.mjs';
const __dirname = dirname(fileURLToPath(import.meta.url));
const TELEMETRY_RECIPE_PATH = resolve(__dirname, '..', 'knowledge', 'cache-telemetry-recipe.md');
const ACCURATE_TOKENS_SAMPLE_SIZE = 3;
async function calibrateAgainstApi(hotspots, apiKey) {
const sampled = hotspots.slice(0, ACCURATE_TOKENS_SAMPLE_SIZE);
let actualTokens = 0;
for (const hotspot of sampled) {
if (!hotspot?.path) continue;
let content;
try {
content = await readFile(hotspot.path, 'utf-8');
} catch {
continue;
}
const result = await tokenizerApi.callCountTokensApi(content, apiKey);
actualTokens += result.input_tokens;
}
return {
actual_tokens: actualTokens,
source: 'count_tokens_api',
sampled_hotspots: sampled.length,
};
}
async function main() {
const args = process.argv.slice(2);
let targetPath = '.';
let outputFile = null;
let jsonMode = false;
let rawMode = false;
let includeGlobal = false;
let withTelemetryRecipe = false;
let accurateTokens = false;
// Default ON for this live-cost scan: stale plugin-cache versions pollute the
// hotspot ranking with config that loads on zero turns. --no-exclude-cache
// restores the full walk. (B3)
let excludeCache = true;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--json') jsonMode = true;
else if (args[i] === '--raw') rawMode = true;
else if (args[i] === '--global') includeGlobal = true;
else if (args[i] === '--with-telemetry-recipe') withTelemetryRecipe = true;
else if (args[i] === '--accurate-tokens') accurateTokens = true;
else if (args[i] === '--exclude-cache') excludeCache = true;
else if (args[i] === '--no-exclude-cache') excludeCache = false;
else if (args[i] === '--output-file' && args[i + 1]) outputFile = args[++i];
else if (!args[i].startsWith('-')) targetPath = args[i];
}
const absPath = resolve(targetPath);
try {
const s = await stat(absPath);
if (!s.isDirectory()) {
process.stderr.write(`Error: ${absPath} is not a directory\n`);
process.exitCode = 3;
return;
}
} catch {
process.stderr.write(`Error: path does not exist: ${absPath}\n`);
process.exitCode = 3;
return;
}
resetCounter();
const discovery = await discoverConfigFiles(absPath, { includeGlobal, excludeCache });
const result = await scan(absPath, discovery);
const payload = {
scanner: result.scanner,
status: result.status,
files_scanned: result.files_scanned,
duration_ms: result.duration_ms,
total_estimated_tokens: result.total_estimated_tokens,
hotspots: result.hotspots,
findings: result.findings,
counts: result.counts,
};
if (withTelemetryRecipe) {
payload.telemetry_recipe_path = TELEMETRY_RECIPE_PATH;
}
if (accurateTokens) {
const apiKey = process.env.ANTHROPIC_API_KEY;
if (!apiKey || apiKey.length === 0) {
process.stderr.write('ANTHROPIC_API_KEY not set — skipping API calibration\n');
payload.calibration = { skipped: 'no-api-key' };
} else {
try {
payload.calibration = await calibrateAgainstApi(result.hotspots || [], apiKey);
} catch (err) {
// Error message is already key-masked by tokenizer-api.mjs.
process.stderr.write(`Calibration error: ${err.message}\n`);
payload.calibration = { skipped: 'api-error', error: err.message };
}
}
}
// Default mode humanizes payload.findings (NOT result.findings).
// --json and --raw bypass for v5.0.0 byte-equal output.
if (!jsonMode && !rawMode) {
payload.findings = humanizeFindings(payload.findings);
}
const json = JSON.stringify(payload, null, 2);
if (outputFile) {
await writeOutputFile(outputFile, json, 'utf-8');
}
if (jsonMode || rawMode || !outputFile) {
process.stdout.write(json + '\n');
}
}
const isDirectRun = process.argv[1] && resolve(process.argv[1]) === resolve(new URL(import.meta.url).pathname);
if (isDirectRun) {
main().catch(err => {
process.stderr.write(`Fatal: ${err.message}\n`);
process.exitCode = 3;
});
}