feat(tokens): stale plugin-cache disk-cleanup finding, honestly labelled Dead config (v5.9 B3b)

The B3a cache filter already exposes discovery.staleCacheVersions; surface them
as a finding so the user knows the superseded plugin versions are safe to delete.

Honesty (Verifiseringsplikt): the finding loads on ZERO turns, so it must NOT
read as a per-turn token cost. TOK normally humanizes to "Wasted tokens"; a new
per-finding category override ('plugin-cache-hygiene' -> "Dead config") plus a
dedicated humanizer translation ("Old plugin versions are sitting on disk (safe
to delete) ... cost zero tokens per turn ... housekeeping, not a performance
problem") keep the prose accurate instead of the generic "using more space"
default. evidence carries the explicit "zero live-context impact" note.

- token-hotspots: Pattern H emits CA-TOK (low) from discovery.staleCacheVersions
  when stale versions exist (`--global`); silent otherwise.
- humanizer: CATEGORY_TO_IMPACT lets a finding's category override the
  scanner-default impact label (raw `category` field unchanged -> --json/--raw
  byte-stable). humanizer-data: honest static translation for the finding.
- Tests: finding fires/severity/category, lists stale keys + zero-impact note,
  silent when none; humanizer override -> Dead config; honest translation locked.
- Docs: tokens command render note (disk-cleanup, not a token problem) +
  --no-exclude-cache flag; README + CLAUDE.md rows (7 patterns, cache-aware).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 18:17:55 +02:00
commit ba9f82f952
8 changed files with 124 additions and 6 deletions

View file

@ -354,3 +354,44 @@ describe('TOK scanner — F7 severity recalibration (v5)', () => {
});
}
});
// ── Pattern H: stale plugin-cache versions (v5.9 B3) ──────────────────────
describe('TOK scanner — H stale plugin-cache versions (v5.9 B3)', () => {
const staleDiscovery = {
files: [],
staleCacheVersions: [
{ key: 'mkt/voyage/5.1.1', fileCount: 35, estimatedBytes: 416237 },
{ key: 'mkt/config-audit/5.6.0', fileCount: 149, estimatedBytes: 301330 },
],
};
it('emits a low-severity stale-cache finding when stale versions exist', async () => {
resetCounter();
const result = await withHermeticHome(() => scan('/tmp/x', staleDiscovery));
const f = result.findings.find(x => /stale plugin-cache/i.test(x.title || ''));
assert.ok(f, 'expected a stale plugin-cache finding');
assert.equal(f.severity, 'low');
assert.equal(f.category, 'plugin-cache-hygiene');
});
it('states zero live-context impact and lists the stale keys', async () => {
resetCounter();
const result = await withHermeticHome(() => scan('/tmp/x', staleDiscovery));
const f = result.findings.find(x => /stale plugin-cache/i.test(x.title || ''));
assert.match(f.evidence, /zero live-context impact/i);
assert.match(f.evidence, /mkt\/voyage\/5\.1\.1/);
assert.match(f.evidence, /mkt\/config-audit\/5\.6\.0/);
});
it('does NOT fire when there are no stale versions', async () => {
resetCounter();
const result = await withHermeticHome(() => scan('/tmp/x', { files: [], staleCacheVersions: [] }));
assert.ok(!result.findings.some(x => /stale plugin-cache/i.test(x.title || '')));
});
it('does NOT fire when discovery omits staleCacheVersions entirely', async () => {
resetCounter();
const result = await withHermeticHome(() => scan('/tmp/x', { files: [] }));
assert.ok(!result.findings.some(x => /stale plugin-cache/i.test(x.title || '')));
});
});