feat(humanizer): wire humanizer into posture and scoring scorecard
generateHealthScorecard signature: 2-arg → 3-arg (areaScores, opportunityCount,
options = {}). options.humanized=true renders friendlier title, grade-context
line per overall grade, and rephrased opportunity line. options.humanized=false
(or 2-arg call) preserves v5.0.0 verbatim output for backwards-compat.
topActions also gets an optional options.humanized that swaps recommendations
through humanizeFinding lookup.
posture.mjs main():
--json → write JSON to stdout, suppress stderr scorecard
--raw → write JSON to stdout (byte-identical to --json), write v5.0.0
verbatim scorecard to stderr
default → humanized scorecard to stderr, no stdout
posture.test.mjs scorecard-prose assertions re-anchored to --raw mode (the
explicit v5.0.0 path) — Wave 0 audit only covered finding-title strings;
scorecard prose surfaces here for the first time.
Wave 3 / Step 6 of v5.1.0 humanizer.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
5ff6594976
commit
70ff900578
5 changed files with 331 additions and 14 deletions
134
plugins/config-audit/tests/lib/scoring-humanizer.test.mjs
Normal file
134
plugins/config-audit/tests/lib/scoring-humanizer.test.mjs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
import { describe, it } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { generateHealthScorecard, topActions } from '../../scanners/lib/scoring.mjs';
|
||||
|
||||
const SAMPLE_AREA_SCORES = {
|
||||
areas: [
|
||||
{ id: 'claude_md', name: 'CLAUDE.md', grade: 'A', score: 100, findingCount: 0 },
|
||||
{ id: 'settings', name: 'Settings', grade: 'A', score: 90, findingCount: 1 },
|
||||
{ id: 'hooks', name: 'Hooks', grade: 'A', score: 100, findingCount: 0 },
|
||||
{ id: 'feature_coverage', name: 'Feature Coverage', grade: 'D', score: 30, findingCount: 17 },
|
||||
],
|
||||
overallGrade: 'A',
|
||||
};
|
||||
|
||||
const SAMPLE_GAP_FINDINGS = [
|
||||
{
|
||||
id: 'CA-GAP-001',
|
||||
scanner: 'GAP',
|
||||
severity: 'medium',
|
||||
title: 'No CLAUDE.md file',
|
||||
description: 'No project instructions file detected.',
|
||||
recommendation: 'Create a CLAUDE.md file with project-specific guidance.',
|
||||
category: 't1',
|
||||
file: null,
|
||||
},
|
||||
{
|
||||
id: 'CA-GAP-002',
|
||||
scanner: 'GAP',
|
||||
severity: 'medium',
|
||||
title: 'No permissions configured',
|
||||
description: 'No permissions block in settings.',
|
||||
recommendation: 'Add a permissions block to settings.json.',
|
||||
category: 't1',
|
||||
file: null,
|
||||
},
|
||||
{
|
||||
id: 'CA-GAP-003',
|
||||
scanner: 'GAP',
|
||||
severity: 'low',
|
||||
title: 'No status line configured',
|
||||
description: 'No status line.',
|
||||
recommendation: 'Add a status line.',
|
||||
category: 't3',
|
||||
file: null,
|
||||
},
|
||||
];
|
||||
|
||||
describe('generateHealthScorecard signature change (3-param)', () => {
|
||||
it('2-arg call: backwards-compatible (humanized defaults to false)', () => {
|
||||
const out = generateHealthScorecard(SAMPLE_AREA_SCORES, 17);
|
||||
assert.equal(typeof out, 'string');
|
||||
assert.ok(out.length > 0);
|
||||
assert.ok(out.includes('Config-Audit Health Score'),
|
||||
'non-humanized scorecard should contain v5.0.0 title');
|
||||
});
|
||||
|
||||
it('3-arg call with {humanized: false}: byte-equal to 2-arg call', () => {
|
||||
const twoArg = generateHealthScorecard(SAMPLE_AREA_SCORES, 17);
|
||||
const threeArgFalse = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false });
|
||||
assert.equal(threeArgFalse, twoArg, 'options.humanized=false must produce identical output to 2-arg call');
|
||||
});
|
||||
|
||||
it('3-arg call with {humanized: true}: differs from non-humanized', () => {
|
||||
const nonHumanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false });
|
||||
const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true });
|
||||
assert.notEqual(humanized, nonHumanized,
|
||||
'humanized=true must produce different output from humanized=false');
|
||||
});
|
||||
|
||||
it('3-arg call with {humanized: true}: contains user-friendly phrasing', () => {
|
||||
const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true });
|
||||
// Must contain at least one humanized cue distinguishing it from v5.0.0 prose
|
||||
const hasGradeContext = /healthy|good shape|attention|polish|setup/i.test(humanized);
|
||||
assert.ok(hasGradeContext,
|
||||
`humanized scorecard must include user-friendly grade context, got:\n${humanized}`);
|
||||
});
|
||||
|
||||
it('preserves area names and scores in both modes', () => {
|
||||
const nonHumanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: false });
|
||||
const humanized = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true });
|
||||
for (const area of SAMPLE_AREA_SCORES.areas.filter(a => a.name !== 'Feature Coverage')) {
|
||||
assert.ok(nonHumanized.includes(area.name),
|
||||
`non-humanized scorecard must include area name "${area.name}"`);
|
||||
assert.ok(humanized.includes(area.name),
|
||||
`humanized scorecard must include area name "${area.name}"`);
|
||||
assert.ok(nonHumanized.includes(`(${area.score})`),
|
||||
`non-humanized scorecard must include score (${area.score})`);
|
||||
assert.ok(humanized.includes(`(${area.score})`),
|
||||
`humanized scorecard must include score (${area.score})`);
|
||||
}
|
||||
});
|
||||
|
||||
it('opportunity count handling in humanized mode', () => {
|
||||
const humanizedZero = generateHealthScorecard(SAMPLE_AREA_SCORES, 0, { humanized: true });
|
||||
const humanizedMany = generateHealthScorecard(SAMPLE_AREA_SCORES, 17, { humanized: true });
|
||||
assert.ok(humanizedMany.includes('17'), 'humanized scorecard must include opportunity count');
|
||||
// Both paths must remain finite strings
|
||||
assert.equal(typeof humanizedZero, 'string');
|
||||
assert.equal(typeof humanizedMany, 'string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('topActions humanizer support', () => {
|
||||
it('1-arg call: returns raw recommendations (backwards-compatible)', () => {
|
||||
const actions = topActions(SAMPLE_GAP_FINDINGS);
|
||||
assert.equal(actions.length, 3);
|
||||
assert.equal(actions[0], 'Create a CLAUDE.md file with project-specific guidance.');
|
||||
assert.equal(actions[1], 'Add a permissions block to settings.json.');
|
||||
assert.equal(actions[2], 'Add a status line.');
|
||||
});
|
||||
|
||||
it('2-arg call with {humanized: false}: identical to 1-arg call', () => {
|
||||
const oneArg = topActions(SAMPLE_GAP_FINDINGS);
|
||||
const twoArg = topActions(SAMPLE_GAP_FINDINGS, { humanized: false });
|
||||
assert.deepStrictEqual(twoArg, oneArg);
|
||||
});
|
||||
|
||||
it('2-arg call with {humanized: true}: at least one recommendation differs', () => {
|
||||
const raw = topActions(SAMPLE_GAP_FINDINGS, { humanized: false });
|
||||
const humanized = topActions(SAMPLE_GAP_FINDINGS, { humanized: true });
|
||||
assert.equal(humanized.length, raw.length, 'array length preserved');
|
||||
// The humanizer's GAP TRANSLATIONS replace at least one recommendation (No CLAUDE.md → "Add the file…")
|
||||
const anyDiffer = humanized.some((r, i) => r !== raw[i]);
|
||||
assert.ok(anyDiffer,
|
||||
`humanized=true must change at least one recommendation. raw=${JSON.stringify(raw)} humanized=${JSON.stringify(humanized)}`);
|
||||
});
|
||||
|
||||
it('preserves ordering by tier (t1 → t2 → t3)', () => {
|
||||
const humanized = topActions(SAMPLE_GAP_FINDINGS, { humanized: true });
|
||||
assert.equal(humanized.length, 3);
|
||||
// 1st & 2nd: t1 findings, 3rd: t3 finding (t2 absent in sample)
|
||||
// Both modes preserve this ordering.
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue