Extend the campaign ledger with a machine-wide always-loaded token bill, the
pure data-model + aggregation half of B2. Deliberately does NOT do live I/O:
the cross-repo readActiveConfig sweep that populates real numbers is B2b.
Design — shared layer counted ONCE, structurally:
- Ledger root gets an optional `sharedGlobal` summary (the always-loaded layer
paid in every repo: global CLAUDE.md + agent listing + global MCP + unscoped
global rules + active plugins' always components), stored ONCE via the new
setSharedGlobal().
- Each repo entry gets an optional `tokens` summary (its PER-REPO delta only),
set via the new setRepoTokens(); addRepo now seeds `tokens: null` (mirrors
findingsBySeverity).
- rollUp() stays PURE and gains a `tokens` aggregate: machineWide = sharedGlobal
+ Σ(per-repo deltas), so the shared layer is counted exactly once by
construction — the structural guard against the historic double-count bug.
Plus a `byRepo` table ranked DESC by always-loaded cost ("most expensive
repos") with a deterministic name tie-break.
Summary shape mirrors manifest's summarizeByLoadPattern exactly
({always|onDemand|external|unknown: {tokens,count}}) so B2b wires in trivially.
Keeps rollUp pure (no filesystem I/O) → byte-stable, respects the THIN campaign
motor invariant. Chosen over the plan's literal "run readActiveConfig inside
rollUp" which would break both (operator-approved deviation).
- 8 new ledger tests incl. the counted-once regression guard + byRepo ranking.
- Updated addRepo shape test (+tokens:null) and campaign-cli EMPTY_ROLLUP.
- No frozen snapshot affected (campaign is v5.7, absent from v5.0.0 baselines).
Suite green: 1189 tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the durable ledger that sits ABOVE individual config-audit sessions for a
machine-wide audit campaign: repo list + per-repo lifecycle (pending → audited →
planned → implemented) + a machine-wide roll-up by status and severity.
scanners/lib/campaign-ledger.mjs — same hybrid split as knowledge-refresh:
- PURE transforms (createLedger / addRepo / setRepoStatus / rollUp) with `now`
injected (YYYY-MM-DD, never the clock) → deterministic + unit-testable.
- soft validateLedger (returns {valid,errors}, never throws) for loaded data;
transforms throw on programmer error (invalid status, unknown path).
- thin IO shell (defaultLedgerPath / loadLedger→null-on-ENOENT / saveLedger).
- persists to ~/.claude/config-audit/campaign-ledger.json — OUTSIDE the plugin
dir (next to sessions/) so it survives uninstall/reinstall/upgrade.
- schemaVersion stamped from the start → cheap Block 4 migration.
THIN scope (Block 3a, operator-approved): ledger + roll-up + persistence only —
no CLI/command/execution (Blocks 3b/3c/4). Internal plumbing, byte-stable until
consumed: no `export async function scan` + lives in lib/ → scanner count stays
15, no orchestrator wiring, SC-5 unchanged.
tests/lib/campaign-ledger.test.mjs — 28 tests (TDD, red→green). Full hermetic
suite 1091→1119 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>