feat(campaign): cross-repo prioritized backlog (v5.7 Fase 2 Block 4b)

buildBacklog(ledger) pure transform + read-only campaign-cli payload field
+ command rendering. The single machine-wide pick-list: per-repo (the ledger
tracks severity counts, not individual findings), severity-weighted
(SEVERITY_WEIGHTS c1000/h100/m10/l1), deterministic tie-break, excludes
implemented/pending/zero-finding repos.

No schema change, no new scanner -> scanner count stays 15, snapshot/backcompat
byte-stable. suite 1138->1150 (lib +9, campaign-cli +3). README badge 1091+->1150+.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-23 02:44:21 +02:00
commit 49833aded8
7 changed files with 263 additions and 10 deletions

View file

@ -10,6 +10,7 @@ import {
addRepo,
setRepoStatus,
rollUp,
buildBacklog,
saveLedger,
} from '../../scanners/lib/campaign-ledger.mjs';
@ -126,6 +127,39 @@ describe('campaign-cli — payload shape', () => {
});
});
describe('campaign-cli — backlog', () => {
it('emits a backlog matching the lib, excluding implemented/pending/zero-finding repos', async () => {
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
const { file, ledger } = await seedLedger(dir);
const out = JSON.parse(runCli(['--ledger-file', file]).stdout);
// seed: a=audited(with findings), b=implemented(excluded), c=pending(excluded)
assert.deepEqual(out.backlog, buildBacklog(ledger));
assert.equal(out.backlog.length, 1);
assert.equal(out.backlog[0].name, 'a');
assert.equal(out.backlog[0].rank, 1);
});
it('ranks multiple actionable repos by severity', async () => {
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
let l = createLedger({ now: NOW });
l = addRepo(l, { path: '/r/low', name: 'low' }, { now: NOW });
l = addRepo(l, { path: '/r/crit', name: 'crit' }, { now: NOW });
l = setRepoStatus(l, '/r/low', 'audited', { now: NOW, findingsBySeverity: { critical: 0, high: 0, medium: 0, low: 9 } });
l = setRepoStatus(l, '/r/crit', 'planned', { now: NOW, findingsBySeverity: { critical: 1, high: 0, medium: 0, low: 0 } });
const file = join(dir, 'campaign-ledger.json');
await saveLedger(file, l);
const out = JSON.parse(runCli(['--ledger-file', file]).stdout);
assert.deepEqual(out.backlog.map((i) => i.name), ['crit', 'low']);
});
it('uninitialized payload has an empty backlog', () => {
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));
const out = JSON.parse(runCli(['--ledger-file', join(dir, 'nope.json')]).stdout);
assert.deepEqual(out.backlog, []);
});
});
describe('campaign-cli — --output-file', () => {
it('writes JSON to the file and stays silent on stdout', async () => {
const dir = mkdtempSync(join(tmpdir(), 'camp-cli-'));