fix(acr): posture --output-file humanizes findings in default mode (M-BUG-12)
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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01683eAqVecv9VZfQzL8CQ9h
This commit is contained in:
parent
1d63492617
commit
b58393099a
2 changed files with 51 additions and 2 deletions
|
|
@ -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`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(() => {});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue