From b58393099a29edddb141a4e42dd2c4a990831434 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Tue, 30 Jun 2026 09:10:07 +0200 Subject: [PATCH] fix(acr): posture --output-file humanizes findings in default mode (M-BUG-12) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feature-gap.md (Step 3-4) and posture.md (Step 3-4) read findings from `posture.mjs --output-file` and group on the humanizer fields (userActionLanguage / userImpactCategory / relevanceContext). But posture.mjs only humanized the stderr scorecard — its --output-file JSON wrote the raw v5.0.0-shape `result`, so every finding's humanizer fields were `undefined`. Both commands silently degraded to the raw tier-fallback: v5.1.0 plain-language output was dead for feature-gap and for posture's finding-level grouping. Re-derived on tests/fixtures/marketplace-medium: 17 GAP findings, all three humanizer fields undefined in the default --output-file JSON. Fix (posture-CLI-local, surgical): humanize the output-file payload in default mode, mirroring scan-orchestrator.mjs:277 — but posture nests the scanner envelope under `result.scannerEnvelope` (its `result` has no top-level `scanners` array), so humanizeEnvelope is applied to `result.scannerEnvelope`, not `result` (the latter would no-op). --json / --raw stay raw, so the explicit-v5.0.0-shape contract and snapshot byte-compat are preserved. TDD: red-first test in posture-humanizer.test.mjs default-mode block asserts GAP findings in the output file carry userActionLanguage/userImpactCategory; a --raw --output-file guard asserts the raw shape is unchanged. Suite 1350/0 (+2). Frozen v5.0.0 + SC-5/6/7 + default-output snapshots byte-stable: --json/--raw bypass the humanizer (their snapshot tests use those flags), and the default --output-file JSON is not snapshot-pinned. Committed, not released — batches with M-BUG-11 in a later hardening release. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h --- scanners/posture.mjs | 10 +++++- tests/scanners/posture-humanizer.test.mjs | 43 ++++++++++++++++++++++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/scanners/posture.mjs b/scanners/posture.mjs index db12564..16a89f0 100644 --- a/scanners/posture.mjs +++ b/scanners/posture.mjs @@ -10,6 +10,7 @@ import { resolve } from 'node:path'; import { writeFile } from 'node:fs/promises'; import { runAllScanners } from './scan-orchestrator.mjs'; +import { humanizeEnvelope } from './lib/humanizer.mjs'; import { calculateUtilization, determineMaturityLevel, @@ -114,7 +115,14 @@ async function main() { } if (outputFile) { - const json = JSON.stringify(result, null, 2); + // 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 writeFile(outputFile, json, 'utf-8'); process.stderr.write(`\nResults written to ${outputFile}\n`); } diff --git a/tests/scanners/posture-humanizer.test.mjs b/tests/scanners/posture-humanizer.test.mjs index fc297f9..22c99d4 100644 --- a/tests/scanners/posture-humanizer.test.mjs +++ b/tests/scanners/posture-humanizer.test.mjs @@ -1,6 +1,7 @@ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; -import { resolve, dirname } from 'node:path'; +import { resolve, dirname, join } from 'node:path'; +import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { execFile } from 'node:child_process'; import { promisify } from 'node:util'; @@ -141,4 +142,44 @@ describe('posture humanizer wiring (Step 6)', () => { 'humanized stderr must differ from v5.0.0 verbatim stderr'); }); }); + + // M-BUG-12: feature-gap.md/posture.md read findings from posture.mjs --output-file + // and group on humanizer fields (userActionLanguage etc.). Default-mode output-file + // must therefore humanize findings inside the nested scannerEnvelope; --raw stays raw. + describe('default mode --output-file (M-BUG-12: humanized findings)', () => { + it('writes humanized GAP findings (userActionLanguage defined) to the output file', async () => { + const tmp = join(tmpdir(), `ca-posture-outfile-${process.pid}.json`); + try { + await runPosture(['--output-file', tmp]); + const env = JSON.parse(await readFile(tmp, 'utf-8')); + const gap = env.scannerEnvelope.scanners.find(s => s.scanner === 'GAP'); + assert.ok(gap, 'GAP scanner must be present in the output file'); + assert.ok(gap.findings.length > 0, 'fixture must yield at least one GAP finding'); + for (const f of gap.findings) { + assert.notEqual(f.userActionLanguage, undefined, + `${f.id}: default-mode output-file findings must carry userActionLanguage`); + assert.notEqual(f.userImpactCategory, undefined, + `${f.id}: default-mode output-file findings must carry userImpactCategory`); + } + } finally { + await unlink(tmp).catch(() => {}); + } + }); + + it('--raw --output-file keeps v5.0.0 raw finding shape (no humanizer fields)', async () => { + const tmp = join(tmpdir(), `ca-posture-outfile-raw-${process.pid}.json`); + try { + await runPosture(['--raw', '--output-file', tmp]); + const env = JSON.parse(await readFile(tmp, 'utf-8')); + for (const s of env.scannerEnvelope.scanners) { + for (const f of s.findings) { + assert.equal(f.userActionLanguage, undefined, + `${f.id}: --raw output-file must not carry userActionLanguage`); + } + } + } finally { + await unlink(tmp).catch(() => {}); + } + }); + }); });