The skill-listing check emits a third finding for an oversized skill BODY
(v5.11 B7, RAW title "Skill body is large (loads on demand when the skill
runs)"). The body is an ON-DEMAND cost — it loads only when the skill is
invoked, not the always-loaded listing Claude reads every turn. The scanner is
careful to distinguish the two (RAW title + comment + evidence note).
But the humanizer-data SKL.static map had no entry for this title, so it fell
through to SKL._default ("A skill is using more of the listing budget than it
should"). The humanized title therefore claimed a listing-budget cost and
directly contradicted the finding's own humanized evidence ("loads ON DEMAND
only ... NOT every turn like the always-loaded listing") — the same internal
contradiction class as M-BUG-15/M-BUG-14, and the same "new finding type added
without a matching humanizer entry" gap the scanner checklist warns about.
Found by finding-granularity premise-verification of the linkedin-posts scan
before feeding it to the analyze pipeline (the prior session's pass focused on
the GAP findings and did not catch the SKL fall-through).
- humanizer-data.mjs: add SKL.static entry for "Skill body is large (loads on
demand when the skill runs)" with on-demand-correct title ("A skill's body is
large (it loads only when that skill runs)"), description, and recommendation.
No listing-budget language; tier1/tier3 forbidden-word checks pass.
- RED-first tests at both layers: humanizer.test.mjs (humanizeFinding path:
title is not the listing-budget _default, conveys on-demand body) and
humanizer-data.test.mjs (static entry exists, on-demand-correct).
RAW envelope unaffected (humanizer bypassed for --raw/--json), frozen v5.0.0
snapshots untouched, default-output fixtures contain no oversized-body skill so
no snapshot regen. Suite 1357->1359/0 (+2).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
344 lines
15 KiB
JavaScript
344 lines
15 KiB
JavaScript
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {
|
||
humanizeFinding,
|
||
humanizeFindings,
|
||
humanizeEnvelope,
|
||
computeRelevanceContext,
|
||
} from '../../scanners/lib/humanizer.mjs';
|
||
|
||
// ─── helpers ────────────────────────────────────────────────────────────
|
||
|
||
function makeFinding(overrides = {}) {
|
||
return {
|
||
id: 'CA-CML-001',
|
||
scanner: 'CML',
|
||
severity: 'medium',
|
||
title: 'No CLAUDE.md found',
|
||
description: 'No CLAUDE.md file at the project root.',
|
||
file: '/Users/test/project/CLAUDE.md',
|
||
line: null,
|
||
evidence: 'evidence text',
|
||
category: 'config',
|
||
recommendation: 'Create one.',
|
||
autoFixable: false,
|
||
...overrides,
|
||
};
|
||
}
|
||
|
||
// ─── purity ─────────────────────────────────────────────────────────────
|
||
|
||
test('humanizeFinding does not mutate its input', () => {
|
||
const input = makeFinding();
|
||
const before = JSON.parse(JSON.stringify(input));
|
||
humanizeFinding(input);
|
||
assert.deepEqual(input, before, 'input was mutated');
|
||
});
|
||
|
||
test('humanizeFindings does not mutate its input array', () => {
|
||
const input = [makeFinding(), makeFinding({ id: 'CA-CML-002', title: 'Repeated content detected' })];
|
||
const before = JSON.parse(JSON.stringify(input));
|
||
humanizeFindings(input);
|
||
assert.deepEqual(input, before, 'input array was mutated');
|
||
});
|
||
|
||
test('humanizeEnvelope does not mutate its input', () => {
|
||
const env = {
|
||
target_path: '/tmp',
|
||
scanners: [
|
||
{ scanner: 'CML', status: 'ok', findings: [makeFinding()], counts: {} },
|
||
],
|
||
};
|
||
const before = JSON.parse(JSON.stringify(env));
|
||
humanizeEnvelope(env);
|
||
assert.deepEqual(env, before, 'envelope was mutated');
|
||
});
|
||
|
||
// ─── field preservation ────────────────────────────────────────────────
|
||
|
||
test('humanizeFinding preserves id, scanner, severity, file, line, evidence, category, autoFixable', () => {
|
||
const input = makeFinding({
|
||
id: 'CA-CML-042',
|
||
scanner: 'CML',
|
||
severity: 'high',
|
||
file: '/tmp/x.md',
|
||
line: 17,
|
||
evidence: 'specific snippet',
|
||
category: 'cml',
|
||
autoFixable: true,
|
||
});
|
||
const out = humanizeFinding(input);
|
||
assert.equal(out.id, 'CA-CML-042');
|
||
assert.equal(out.scanner, 'CML');
|
||
assert.equal(out.severity, 'high');
|
||
assert.equal(out.file, '/tmp/x.md');
|
||
assert.equal(out.line, 17);
|
||
assert.equal(out.evidence, 'specific snippet');
|
||
assert.equal(out.category, 'cml');
|
||
assert.equal(out.autoFixable, true);
|
||
});
|
||
|
||
test('humanizeFinding preserves optional details payload', () => {
|
||
const input = makeFinding();
|
||
input.details = { foo: 'bar', count: 7 };
|
||
const out = humanizeFinding(input);
|
||
assert.deepEqual(out.details, { foo: 'bar', count: 7 });
|
||
});
|
||
|
||
// ─── translation lookup ────────────────────────────────────────────────
|
||
|
||
test('humanizeFinding rewrites title for known static title', () => {
|
||
const input = makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' });
|
||
const out = humanizeFinding(input);
|
||
assert.notEqual(out.title, input.title, 'title should be translated');
|
||
assert.ok(out.title.toLowerCase().includes('instructions') || out.title.toLowerCase().includes('claude'),
|
||
`humanized title should mention instructions or claude, got: ${out.title}`);
|
||
});
|
||
|
||
test('humanizeFinding: stale plugin-cache title humanizes to honest disk-cleanup prose, not the generic TOK default (B3)', () => {
|
||
const input = makeFinding({
|
||
scanner: 'TOK',
|
||
title: 'Stale plugin-cache versions (disk cleanup, zero live-context impact)',
|
||
category: 'plugin-cache-hygiene',
|
||
});
|
||
const out = humanizeFinding(input);
|
||
assert.match(out.title, /disk/i, 'humanized title must name disk, not generic "space"');
|
||
assert.match(out.description, /zero tokens per turn|never loaded/i,
|
||
'humanized description must keep the zero-context-impact honesty');
|
||
assert.equal(out.userImpactCategory, 'Dead config');
|
||
});
|
||
|
||
test('humanizeFinding falls back to _default when title unknown', () => {
|
||
const input = makeFinding({ scanner: 'CML', title: 'Unrecognized brand-new finding title' });
|
||
const out = humanizeFinding(input);
|
||
assert.notEqual(out.title, input.title, '_default should kick in');
|
||
// CML _default mentions "instructions file"
|
||
assert.ok(/instructions file/i.test(out.title), `expected CML _default title, got: ${out.title}`);
|
||
});
|
||
|
||
test('humanizeFinding maps SKL oversized-body finding to an on-demand title, not the listing-budget _default', () => {
|
||
// RAW title emitted by skill-listing-scanner for body > threshold (v5.11 B7).
|
||
// It is an ON-DEMAND body cost, NOT the always-loaded listing budget — the
|
||
// scanner is careful to distinguish them, so the humanized title must not
|
||
// regress into "listing budget" language via the SKL _default (M-BUG-16).
|
||
const input = makeFinding({
|
||
scanner: 'SKL',
|
||
severity: 'low',
|
||
title: 'Skill body is large (loads on demand when the skill runs)',
|
||
description: 'Skill "repo-init" (user) has a body of about 6223 tokens (712 lines), over the ~5000-token guidance for a skill body.',
|
||
evidence: 'body_tokens~6223; threshold=5000 tok; skill="repo-init"; source=user - this is the skill BODY which loads ON DEMAND only when the skill is invoked - NOT every turn like the always-loaded listing.',
|
||
});
|
||
const out = humanizeFinding(input);
|
||
assert.doesNotMatch(out.title, /listing budget/i,
|
||
`body finding must not be framed as a listing-budget cost, got: ${out.title}`);
|
||
assert.notEqual(out.title, 'A skill is using more of the listing budget than it should',
|
||
'body finding must have its own mapping, not the SKL _default');
|
||
assert.match(out.title, /body|on demand|when (it|the skill) runs/i,
|
||
`humanized title must convey the on-demand body cost, got: ${out.title}`);
|
||
});
|
||
|
||
test('humanizeFinding passes through original strings when scanner prefix unknown', () => {
|
||
const input = makeFinding({ scanner: 'XXX', title: 'whatever' });
|
||
const out = humanizeFinding(input);
|
||
assert.equal(out.title, 'whatever');
|
||
assert.equal(out.description, input.description);
|
||
assert.equal(out.recommendation, input.recommendation);
|
||
});
|
||
|
||
test('humanizeFinding matches pattern entries (template-literal titles)', () => {
|
||
const input = makeFinding({
|
||
scanner: 'COL',
|
||
title: 'Skill name "okr-helper" used by multiple plugins',
|
||
});
|
||
const out = humanizeFinding(input);
|
||
assert.notEqual(out.title, input.title);
|
||
assert.ok(/two plugins|same name|multiple/i.test(out.title) ||
|
||
/two plugins|same name|multiple/i.test(out.description),
|
||
`expected pattern match for COL multiple-plugins case, got title: ${out.title}`);
|
||
});
|
||
|
||
// ─── userActionLanguage ────────────────────────────────────────────────
|
||
|
||
test('humanizeFinding maps severity=critical -> "Fix this now"', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'critical' }));
|
||
assert.equal(out.userActionLanguage, 'Fix this now');
|
||
});
|
||
|
||
test('humanizeFinding maps severity=high -> "Fix soon"', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'high' }));
|
||
assert.equal(out.userActionLanguage, 'Fix soon');
|
||
});
|
||
|
||
test('humanizeFinding maps severity=medium -> "Fix when convenient"', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'medium' }));
|
||
assert.equal(out.userActionLanguage, 'Fix when convenient');
|
||
});
|
||
|
||
test('humanizeFinding maps severity=low -> "Optional cleanup"', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'low' }));
|
||
assert.equal(out.userActionLanguage, 'Optional cleanup');
|
||
});
|
||
|
||
test('humanizeFinding maps severity=info -> "FYI"', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'info' }));
|
||
assert.equal(out.userActionLanguage, 'FYI');
|
||
});
|
||
|
||
test('humanizeFinding falls back to "FYI" for unknown severity', () => {
|
||
const out = humanizeFinding(makeFinding({ severity: 'mystery' }));
|
||
assert.equal(out.userActionLanguage, 'FYI');
|
||
});
|
||
|
||
// ─── userImpactCategory ────────────────────────────────────────────────
|
||
|
||
test('humanizeFinding sets category Configuration mistake for CML/SET/HKV/RUL/MCP/IMP/PLH/OST', () => {
|
||
for (const s of ['CML', 'SET', 'HKV', 'RUL', 'MCP', 'IMP', 'PLH', 'OST']) {
|
||
const out = humanizeFinding(makeFinding({ scanner: s }));
|
||
assert.equal(out.userImpactCategory, 'Configuration mistake', `${s} should map to Configuration mistake`);
|
||
}
|
||
});
|
||
|
||
test('humanizeFinding sets category Conflict for CNF/COL', () => {
|
||
for (const s of ['CNF', 'COL']) {
|
||
const out = humanizeFinding(makeFinding({ scanner: s }));
|
||
assert.equal(out.userImpactCategory, 'Conflict');
|
||
}
|
||
});
|
||
|
||
test('humanizeFinding sets category Wasted tokens for TOK/CPS/SKL', () => {
|
||
for (const s of ['TOK', 'CPS', 'SKL']) {
|
||
const out = humanizeFinding(makeFinding({ scanner: s }));
|
||
assert.equal(out.userImpactCategory, 'Wasted tokens');
|
||
}
|
||
});
|
||
|
||
test('humanizeFinding sets category Dead config for DIS', () => {
|
||
const out = humanizeFinding(makeFinding({ scanner: 'DIS' }));
|
||
assert.equal(out.userImpactCategory, 'Dead config');
|
||
});
|
||
|
||
test('humanizeFinding: per-finding category plugin-cache-hygiene overrides scanner default to Dead config (B3)', () => {
|
||
// TOK normally maps to "Wasted tokens", but a stale-cache finding loads on
|
||
// zero turns — its honest impact is "Dead config", not wasted per-turn tokens.
|
||
const out = humanizeFinding(makeFinding({ scanner: 'TOK', category: 'plugin-cache-hygiene' }));
|
||
assert.equal(out.userImpactCategory, 'Dead config');
|
||
assert.equal(out.category, 'plugin-cache-hygiene', 'raw category field preserved unchanged');
|
||
});
|
||
|
||
test('humanizeFinding sets category Missed opportunity for GAP', () => {
|
||
const out = humanizeFinding(makeFinding({ scanner: 'GAP', title: 'No CLAUDE.md file' }));
|
||
assert.equal(out.userImpactCategory, 'Missed opportunity');
|
||
});
|
||
|
||
test('humanizeFinding sets category Other for unknown scanner', () => {
|
||
const out = humanizeFinding(makeFinding({ scanner: 'XXX' }));
|
||
assert.equal(out.userImpactCategory, 'Other');
|
||
});
|
||
|
||
// ─── relevanceContext ──────────────────────────────────────────────────
|
||
|
||
test('computeRelevanceContext detects test-fixture paths', () => {
|
||
assert.equal(computeRelevanceContext('/repo/tests/fixtures/foo/CLAUDE.md'), 'test-fixture-no-impact');
|
||
assert.equal(computeRelevanceContext('/repo/test/fixtures/bar.json'), 'test-fixture-no-impact');
|
||
});
|
||
|
||
test('computeRelevanceContext detects local-only paths via .local. infix', () => {
|
||
assert.equal(computeRelevanceContext('/repo/.claude/settings.local.json'), 'affects-this-machine-only');
|
||
assert.equal(computeRelevanceContext('/repo/CLAUDE.local.md'), 'affects-this-machine-only');
|
||
});
|
||
|
||
test('computeRelevanceContext defaults to affects-everyone for normal paths', () => {
|
||
assert.equal(computeRelevanceContext('/repo/CLAUDE.md'), 'affects-everyone');
|
||
assert.equal(computeRelevanceContext('/repo/.claude/settings.json'), 'affects-everyone');
|
||
});
|
||
|
||
test('computeRelevanceContext defaults to affects-everyone for null/empty paths', () => {
|
||
assert.equal(computeRelevanceContext(null), 'affects-everyone');
|
||
assert.equal(computeRelevanceContext(undefined), 'affects-everyone');
|
||
assert.equal(computeRelevanceContext(''), 'affects-everyone');
|
||
});
|
||
|
||
test('humanizeFinding sets relevanceContext from file', () => {
|
||
const f = makeFinding({ file: '/repo/tests/fixtures/x.json' });
|
||
assert.equal(humanizeFinding(f).relevanceContext, 'test-fixture-no-impact');
|
||
|
||
const g = makeFinding({ file: '/repo/.claude/settings.local.json' });
|
||
assert.equal(humanizeFinding(g).relevanceContext, 'affects-this-machine-only');
|
||
|
||
const h = makeFinding({ file: '/repo/CLAUDE.md' });
|
||
assert.equal(humanizeFinding(h).relevanceContext, 'affects-everyone');
|
||
});
|
||
|
||
// ─── humanizeFindings & humanizeEnvelope ──────────────────────────────
|
||
|
||
test('humanizeFindings translates each finding in the array', () => {
|
||
const findings = [
|
||
makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' }),
|
||
makeFinding({ id: 'CA-CML-002', scanner: 'CML', title: 'Uses HTML comments' }),
|
||
];
|
||
const out = humanizeFindings(findings);
|
||
assert.equal(out.length, 2);
|
||
assert.notEqual(out[0].title, findings[0].title);
|
||
assert.notEqual(out[1].title, findings[1].title);
|
||
});
|
||
|
||
test('humanizeFindings returns input unchanged if not an array', () => {
|
||
assert.equal(humanizeFindings(null), null);
|
||
assert.equal(humanizeFindings(undefined), undefined);
|
||
});
|
||
|
||
test('humanizeEnvelope walks scanners[].findings and humanizes each', () => {
|
||
const env = {
|
||
target_path: '/tmp',
|
||
scanners: [
|
||
{
|
||
scanner: 'CML',
|
||
status: 'ok',
|
||
findings: [makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' })],
|
||
counts: {},
|
||
},
|
||
{
|
||
scanner: 'TOK',
|
||
status: 'ok',
|
||
findings: [makeFinding({
|
||
id: 'CA-TOK-001',
|
||
scanner: 'TOK',
|
||
severity: 'low',
|
||
title: 'Cache-breaking volatile content at top of CLAUDE.md',
|
||
file: '/tmp/CLAUDE.md',
|
||
})],
|
||
counts: {},
|
||
},
|
||
],
|
||
};
|
||
const out = humanizeEnvelope(env);
|
||
assert.equal(out.target_path, '/tmp');
|
||
assert.equal(out.scanners.length, 2);
|
||
assert.notEqual(out.scanners[0].findings[0].title, env.scanners[0].findings[0].title);
|
||
assert.notEqual(out.scanners[1].findings[0].title, env.scanners[1].findings[0].title);
|
||
assert.equal(out.scanners[1].findings[0].userImpactCategory, 'Wasted tokens');
|
||
});
|
||
|
||
test('humanizeEnvelope returns input unchanged if shape is wrong', () => {
|
||
assert.equal(humanizeEnvelope(null), null);
|
||
assert.equal(humanizeEnvelope({}).scanners, undefined); // unchanged object
|
||
assert.equal(humanizeEnvelope({ scanners: 'not-an-array' }).scanners, 'not-an-array');
|
||
});
|
||
|
||
// ─── new fields presence ───────────────────────────────────────────────
|
||
|
||
test('humanizeFinding always sets the three new fields', () => {
|
||
const out = humanizeFinding(makeFinding());
|
||
assert.equal(typeof out.userImpactCategory, 'string');
|
||
assert.equal(typeof out.userActionLanguage, 'string');
|
||
assert.equal(typeof out.relevanceContext, 'string');
|
||
});
|
||
|
||
// ─── ANSI-free guarantee ───────────────────────────────────────────────
|
||
|
||
test('humanized output contains no ANSI escape sequences', () => {
|
||
const out = humanizeFinding(makeFinding({ scanner: 'CML', title: 'No CLAUDE.md found' }));
|
||
const allText = `${out.title} ${out.description} ${out.recommendation}`;
|
||
// eslint-disable-next-line no-control-regex
|
||
assert.equal(/\[/.test(allText), false, 'ANSI escape detected in humanized output');
|
||
});
|