Skip to content

Ledger

Each objective of a campaign has a ledger: one JSON file under the manifest’s ledger directory (default .architecture-campaigns/), at <campaign>/<objective>.json. It is the baseline’s cousin and not the baseline — same fingerprint, different file, different semantics. A baseline records violations a repository is carrying; a ledger records debt an objective is paying down, per sector, and the file is where the burn-down lives.

{
"version": 2,
"campaign": "billing-ddd",
"objective": "no-raw-queries-outside-repository",
"created": "2026-09-22T21:56:54.000Z",
"sectors": {
"billing": {
"entered": "2026-09-22T21:56:54.000Z",
"initial": 212,
"cleared": 187,
"closed": 0,
"lastCleared": "2026-10-01T09:12:03.000Z",
"holdouts": ["InvoiceService.ts#reconcile#3bfd86f4"]
}
},
"concessions": [
{
"sector": "billing",
"at": "2026-09-28T14:02:11.000Z",
"by": "someone@example.com",
"delta": 2,
"reason": "hotfix for invoice rounding; BILL-412",
"holdouts": ["InvoiceService.ts#void#9a1c02ef", "InvoiceService.ts#void#c31d7f08"]
}
]
}

Holdouts are file or file#subject, written relative to the sector’s root — the marker’s folder, the matched glob, the file’s own folder, or the repository for the implicit sector and the legacy — deduplicated and sorted, one per line, so a pull request that fixes three of them is a three-line deletion a reviewer can read. A sector’s own holdout (a sector term that fails) is ~. Every counter and the holdout list are per sector inside one file per objective, so two pull requests paying down the same objective in billing and orders edit different regions of the file and merge cleanly; finishing a campaign is deleting its folder.

The count is not stored; it is holdouts.length, per sector, and check verifies that

holdouts.length === initial + Σ concessions.delta − cleared − closed

initial is the count the day the sector entered the objective’s window; cleared the holdouts that stopped firing and were cleared since; closed the holdouts still firing when the sector left the window — not progress, and never counted as such. A re-baseline concession ({ from, to }) counts as to − from. A holdout added by hand, with no concession, fails the build — with no git history in the loop. That is what keeps the file honest: every number in it is derivable from the others, and the only way a count rises is through a record of why.

Terminal window
architecture objectives clear [<campaign>[/<objective>]] # reconcile with the code
architecture objectives concede <campaign>[/<objective>] --reason "<why>" # record why a count may rise

clear reconciles the ledger with the code wherever that is not a regression. A holdout that no longer fires leaves, and cleared rises by one; a match holdout whose hash moved under a still-present declaration — a syntax match edited, a report diagnostic reworded — is rewritten in place and counts as neither; lastCleared moves only when something left. A sector newly inside an objective’s window is recorded with its initial, which is how a first ledger is written; a sector past a window has what still fires closed; a sector the code no longer births is closed the same way. A defined phase changed with a concession has its sectors re-baselined, each recording { from, to } with the phase concession’s reason. Unrecorded growth is left where it is: clear never adds a holdout. Alongside the ledgers it writes each sector’s record and the campaign’s plan, below. Until a sector in a window has an entry, check fails and names this command.

concede is the only verb that adds holdouts, and it always appends a concession: the sector, the holdouts, a reason, a timestamp, and an author — --by, else git’s user.email, else GIT_AUTHOR_EMAIL, and refused without one. A hit the ledger does not carry is unrecorded growth, and check fails on it with the objective’s how:

campaign billing-ddd: 1 new hit the ledger does not carry. Fix them, or record why the count may rise:
no-raw-queries-outside-repository · billing · InvoiceService.ts#refund#4b02a9e1
src/billing/InvoiceService.ts: Move the call behind the port.
architecture objectives concede billing-ddd --reason "<why>"

--holdouts a,b concedes a subset and leaves the rest failing, for a pull request that legitimately adds one hit while another is an accident; --sector <name> narrows to one sector. campaigns clear and campaigns concede are accepted as the same verbs.

What derivation cannot say about a sector lives in its own file, <campaign>/sectors/<sector>.json, written by clear, campaigns attest and campaigns note, and never edited by hand:

{
"version": 1,
"campaign": "billing-ddd",
"sector": "billing",
"reached": "cutover",
"since": "2026-10-02T00:00:00.000Z",
"attested": [
{
"phase": "backfilled",
"reason": "backfill ran against production",
"evidence": "https://ci/runs/4812",
"at": "2026-10-01T15:10:00.000Z",
"by": "someone@example.com"
}
],
"notes": [
{
"at": "2026-10-04T09:00:00.000Z",
"by": "someone@example.com",
"phase": "aggregates",
"text": "refunds read invoices but never write them"
}
]
}

reached is the furthest phase the sector has ever been derived at; it only advances, and it exists for one rule — a window the sector has passed never reopens for it. since is when it last advanced, and with the latest attestation and note it is the sector’s own clock: the campaign’s stall reads the later of an objective’s lastCleared and that, so a sector standing in an attested or open phase is stalled only when nothing has been said about it either. Notes are capped — the last twenty, each at most five hundred characters — and the nudge shows only those left at the sector’s current phase, marked as data, because free text shown to the next agent is an instruction channel whether or not anyone meant it as one.

<campaign>/plan.json is what clear last saw of the campaign’s phases — each phase’s id, a digest of its definition, whether it was defined, and how many concessions it carried. check compares the manifest against it: an open phase that gained criteria, or a new phase, is plan refined and free; a defined phase whose definition changed, or was removed, is plan changed and fails check until the phase carries a new concessions entry. Both are reported apart in conformance, so the one a reviewer must read every time stays readable.

The ledger is designed to be read in a diff. A line deleted from a sector’s holdouts and its cleared going up by one is progress. A line added must come with a new object under concessions naming it, with a reason the reviewer agrees with; a line added without one does not pass check, so it will not be in a green pull request. closed going up is a sector moving past a window, not paying anything down. A sector appearing under sectors with a large initial is code that just declared itself — a folder that grew a marker — and the legacy line in the conformance report shrank by the same amount; the report never collapses the two. A { from, to } concession is a plan change landing, and the phase’s own concessions entry in the manifest is where its reason is.

Because the files are per campaign and the records name their author, CODEOWNERS on the ledger directory puts the campaign’s owner on every change to its counts:

.architecture-campaigns/billing-ddd/ @dataquail/billing

A campaign may state staleAfter. One with holdouts left whose objectives’ lastCleared and sectors’ clocks are all older than that is stalled — a notice in check, and the first thing conformance prints in its campaigns section, since a migration nobody is working on is the failure the whole apparatus exists to make visible. A campaign with no staleAfter never stalls.

A campaign at zero everywhere is complete. What happens next is its onComplete: keep leaves it in the manifest as a guard against recurrence — a new hit is unrecorded growth like any other — and remove makes check fail until the campaign and its ledgers are deleted, so a finished migration does not linger as a rule about nothing.

ARCHITECTURE_NOW (an ISO date, or epoch milliseconds) pins the clock every timestamp and stall is judged by, for a CI that replays a day or a test that needs a stall to have happened.

Every change to a ledger, a record or the plan is a clear, concede, attest, note or a manifest edit — so architecture campaigns history [<campaign>] walks those files’ git history and renders the series: one row per commit, the holdouts per objective as the ledgers stood after it, and whether the manifest changed in it. It is a step function of human actions, not the code’s own trend; the code’s trend is the residue vectors, which CI emits per run through conformance --json.