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:
parent
ee0c762151
commit
49833aded8
7 changed files with 263 additions and 10 deletions
|
|
@ -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-'));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue