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 { resolve } from 'node:path';
|
||||||
import { writeFile } from 'node:fs/promises';
|
import { writeFile } from 'node:fs/promises';
|
||||||
import { runAllScanners } from './scan-orchestrator.mjs';
|
import { runAllScanners } from './scan-orchestrator.mjs';
|
||||||
|
import { humanizeEnvelope } from './lib/humanizer.mjs';
|
||||||
import {
|
import {
|
||||||
calculateUtilization,
|
calculateUtilization,
|
||||||
determineMaturityLevel,
|
determineMaturityLevel,
|
||||||
|
|
@ -114,7 +115,14 @@ async function main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outputFile) {
|
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');
|
await writeFile(outputFile, json, 'utf-8');
|
||||||
process.stderr.write(`\nResults written to ${outputFile}\n`);
|
process.stderr.write(`\nResults written to ${outputFile}\n`);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { describe, it } from 'node:test';
|
import { describe, it } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
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 { fileURLToPath } from 'node:url';
|
||||||
import { execFile } from 'node:child_process';
|
import { execFile } from 'node:child_process';
|
||||||
import { promisify } from 'node:util';
|
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');
|
'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