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
|
|
@ -4,6 +4,19 @@
|
|||
*/
|
||||
|
||||
import { gradeFromPassRate, WEIGHTS } from './severity.mjs';
|
||||
import { humanizeFinding } from './humanizer.mjs';
|
||||
|
||||
/**
|
||||
* One-line plain-language context per overall grade. Used when a scorecard
|
||||
* is rendered with `options.humanized: true`.
|
||||
*/
|
||||
const GRADE_CONTEXT = {
|
||||
A: 'Healthy setup, only minor polish needed',
|
||||
B: 'Good shape — a few items to address',
|
||||
C: 'Some attention needed',
|
||||
D: 'Several issues — prioritize the urgent ones',
|
||||
F: 'Important issues need attention',
|
||||
};
|
||||
|
||||
// --- Tier weights for utilization calculation ---
|
||||
const TIER_WEIGHTS = { t1: 3, t2: 2, t3: 1, t4: 1 };
|
||||
|
|
@ -235,14 +248,21 @@ export function scoreByArea(scannerResults) {
|
|||
/**
|
||||
* Derive top 3 actions from GAP findings (T1 first, then T2).
|
||||
* @param {object[]} gapFindings
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.humanized=false] - When true, return humanized
|
||||
* recommendations (looked up via humanizer translations).
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function topActions(gapFindings) {
|
||||
export function topActions(gapFindings, options = {}) {
|
||||
const tierOrder = ['t1', 't2', 't3', 't4'];
|
||||
const sorted = [...gapFindings].sort(
|
||||
(a, b) => tierOrder.indexOf(a.category) - tierOrder.indexOf(b.category),
|
||||
);
|
||||
return sorted.slice(0, 3).map(f => f.recommendation);
|
||||
const top3 = sorted.slice(0, 3);
|
||||
if (options.humanized) {
|
||||
return top3.map(f => humanizeFinding(f).recommendation);
|
||||
}
|
||||
return top3.map(f => f.recommendation);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -307,22 +327,39 @@ export function generateScorecard(areaScores, utilization, maturity, segment, ac
|
|||
* Shows only the quality areas (currently 8) — no utilization, maturity, or segment.
|
||||
* @param {{ areas: Array<{ name: string, grade: string, score: number }>, overallGrade: string }} areaScores
|
||||
* @param {number} opportunityCount - Number of GAP findings (shown as opportunity count)
|
||||
* @param {object} [options]
|
||||
* @param {boolean} [options.humanized=false] - When true, render with plain-language
|
||||
* grade context and friendlier opportunity phrasing. When false (default),
|
||||
* render the v5.0.0 verbatim scorecard (backwards-compatible).
|
||||
* @returns {string}
|
||||
*/
|
||||
export function generateHealthScorecard(areaScores, opportunityCount) {
|
||||
export function generateHealthScorecard(areaScores, opportunityCount, options = {}) {
|
||||
const qualityAreas = areaScores.areas.filter(a => a.name !== 'Feature Coverage');
|
||||
const avgScore = qualityAreas.length > 0
|
||||
? Math.round(qualityAreas.reduce((s, a) => s + a.score, 0) / qualityAreas.length)
|
||||
: 0;
|
||||
|
||||
const humanized = options.humanized === true;
|
||||
|
||||
const lines = [];
|
||||
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
lines.push(' Config-Audit Health Score');
|
||||
lines.push(humanized ? ' Configuration health' : ' Config-Audit Health Score');
|
||||
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
lines.push('');
|
||||
lines.push(` Health: ${areaScores.overallGrade} (${avgScore}/100) ${qualityAreas.length} areas scanned`);
|
||||
|
||||
if (humanized) {
|
||||
const context = GRADE_CONTEXT[areaScores.overallGrade] || '';
|
||||
const headline = context
|
||||
? ` Health: ${areaScores.overallGrade} (${avgScore}/100) — ${context}`
|
||||
: ` Health: ${areaScores.overallGrade} (${avgScore}/100)`;
|
||||
lines.push(headline);
|
||||
lines.push(` ${qualityAreas.length} areas reviewed`);
|
||||
} else {
|
||||
lines.push(` Health: ${areaScores.overallGrade} (${avgScore}/100) ${qualityAreas.length} areas scanned`);
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
lines.push(' Area Scores');
|
||||
lines.push(humanized ? ' Area scores' : ' Area Scores');
|
||||
lines.push(' ───────────');
|
||||
|
||||
// Format areas in 2-column layout (quality areas only)
|
||||
|
|
@ -340,7 +377,12 @@ export function generateHealthScorecard(areaScores, opportunityCount) {
|
|||
|
||||
if (opportunityCount > 0) {
|
||||
lines.push('');
|
||||
lines.push(` ${opportunityCount} ${opportunityCount === 1 ? 'opportunity' : 'opportunities'} available — run /config-audit feature-gap for recommendations`);
|
||||
if (humanized) {
|
||||
const noun = opportunityCount === 1 ? 'way' : 'ways';
|
||||
lines.push(` ${opportunityCount} ${noun} you could get more out of Claude Code — see /config-audit feature-gap`);
|
||||
} else {
|
||||
lines.push(` ${opportunityCount} ${opportunityCount === 1 ? 'opportunity' : 'opportunities'} available — run /config-audit feature-gap for recommendations`);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push('');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue