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
142 lines
4.7 KiB
JavaScript
142 lines
4.7 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
/**
|
|
* Config-Audit Posture Assessment CLI
|
|
* Runs all scanners + scoring in a single Node.js process.
|
|
* Usage: node posture.mjs <target-path> [--json] [--global] [--output-file path]
|
|
* Zero external dependencies.
|
|
*/
|
|
|
|
import { resolve } from 'node:path';
|
|
import { writeOutputFile } from './lib/write-output.mjs';
|
|
import { runAllScanners } from './scan-orchestrator.mjs';
|
|
import { humanizeEnvelope } from './lib/humanizer.mjs';
|
|
import {
|
|
calculateUtilization,
|
|
determineMaturityLevel,
|
|
determineSegment,
|
|
scoreByArea,
|
|
topActions,
|
|
generateScorecard,
|
|
generateHealthScorecard,
|
|
} from './lib/scoring.mjs';
|
|
|
|
/**
|
|
* Run posture assessment and return structured result.
|
|
* @param {string} targetPath
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.includeGlobal=false]
|
|
* @param {boolean} [opts.fullMachine=false] - Scan all known locations across the machine
|
|
* @returns {Promise<object>}
|
|
*/
|
|
export async function runPosture(targetPath, opts = {}) {
|
|
const envelope = await runAllScanners(targetPath, opts);
|
|
|
|
// Extract GAP scanner results
|
|
const gapScanner = envelope.scanners.find(s => s.scanner === 'GAP');
|
|
const gapFindings = gapScanner ? gapScanner.findings : [];
|
|
|
|
// Calculate scores
|
|
const utilization = calculateUtilization(gapFindings);
|
|
const maturity = determineMaturityLevel(gapFindings, { files: [] });
|
|
const segment = determineSegment(utilization.score);
|
|
const areaScores = scoreByArea(envelope.scanners);
|
|
const actions = topActions(gapFindings);
|
|
|
|
return {
|
|
utilization,
|
|
maturity,
|
|
segment,
|
|
areas: areaScores.areas,
|
|
overallGrade: areaScores.overallGrade,
|
|
topActions: actions,
|
|
opportunityCount: gapFindings.length,
|
|
scannerEnvelope: envelope,
|
|
};
|
|
}
|
|
|
|
// --- CLI entry point ---
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
let targetPath = '.';
|
|
let outputFile = null;
|
|
let jsonMode = false;
|
|
let rawMode = false;
|
|
let includeGlobal = false;
|
|
let fullMachine = false;
|
|
let contextWindow = null;
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
if (args[i] === '--output-file' && args[i + 1]) {
|
|
outputFile = args[++i];
|
|
} else if (args[i] === '--context-window' && args[i + 1]) {
|
|
contextWindow = args[++i];
|
|
} else if (args[i] === '--json') {
|
|
jsonMode = true;
|
|
} else if (args[i] === '--raw') {
|
|
rawMode = true;
|
|
} else if (args[i] === '--global') {
|
|
includeGlobal = true;
|
|
} else if (args[i] === '--full-machine') {
|
|
fullMachine = true;
|
|
} else if (args[i] === '--include-fixtures') {
|
|
// handled below
|
|
} else if (!args[i].startsWith('-')) {
|
|
targetPath = args[i];
|
|
}
|
|
}
|
|
|
|
const filterFixtures = !args.includes('--include-fixtures');
|
|
const humanizedProgress = !jsonMode && !rawMode;
|
|
const result = await runPosture(targetPath, {
|
|
includeGlobal,
|
|
fullMachine,
|
|
filterFixtures,
|
|
humanizedProgress,
|
|
contextWindow,
|
|
});
|
|
|
|
// stdout JSON path: --json and --raw both write the v5.0.0-shape result
|
|
// (byte-identical). Default mode writes nothing to stdout.
|
|
if (jsonMode || rawMode) {
|
|
const json = JSON.stringify(result, null, 2);
|
|
process.stdout.write(json + '\n');
|
|
}
|
|
|
|
// stderr scorecard path: --json suppresses; --raw renders v5.0.0 verbatim
|
|
// (humanized=false); default renders humanized scorecard.
|
|
if (!jsonMode) {
|
|
const scorecard = generateHealthScorecard(
|
|
{ areas: result.areas, overallGrade: result.overallGrade },
|
|
result.opportunityCount,
|
|
{ humanized: !rawMode },
|
|
);
|
|
process.stderr.write('\n' + scorecard + '\n');
|
|
}
|
|
|
|
if (outputFile) {
|
|
// Consumers (feature-gap.md, posture.md) read scannerEnvelope.scanners[].findings
|
|
// and group on humanizer fields. posture's result nests the envelope under
|
|
// `scannerEnvelope`, so humanize THAT (not `result`, which has no top-level
|
|
// `scanners` array — humanizeEnvelope would no-op). --json/--raw stay raw.
|
|
const fileEnv = (jsonMode || rawMode)
|
|
? result
|
|
: { ...result, scannerEnvelope: humanizeEnvelope(result.scannerEnvelope) };
|
|
const json = JSON.stringify(fileEnv, null, 2);
|
|
await writeOutputFile(outputFile, json, 'utf-8');
|
|
process.stderr.write(`\nResults written to ${outputFile}\n`);
|
|
}
|
|
}
|
|
|
|
// Only run CLI if invoked directly
|
|
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`);
|
|
// 3, not 1. Every command in this plugin is told that 0/1/2 are normal grades
|
|
// (PASS/WARNING/FAIL) and only 3 is a real error, so exiting 1 here made a crash
|
|
// indistinguishable from a WARNING — and the command went on to Read a payload file
|
|
// that was never written.
|
|
process.exitCode = 3;
|
|
});
|
|
}
|