Decision record
ADR-0012: Records append, they do not rewrite
ADR-0012: A decision records what it assumed, a runbook records its runs, and both append rather than rewrite
Status: accepted · 2026-09-15
Context
Two kinds of page were losing their most useful information because they had nowhere to put it.
An ADR captured Context, Decision and Consequences at the moment of deciding. Six months on, when the decision bit, the only place to record that was the Consequences paragraph — which meant editing prose written before the outcome was known, and losing the record of what was believed at the time. ADR-0009 in this repository carries the 2026-09-10 lost-writes incident in its Context because that was the section that existed. Nothing recorded which assumption had broken, because assumptions were never written down as such. And nothing distinguished a decision that could be reverted with one commit from one with a migration behind it; both got the same three sections.
A runbook was a page with a runbook badge. Whether the procedure had ever
been followed, when, by whom, and where it stopped lived in nobody’s memory —
docs/runbooks/firestore-restore.md has a rehearsal-log table that has been
empty since it was written. The one property a runbook needs — that it works
— was unknowable from the page.
The dead ends between decisions — “spent two days on X because the library docs lied” — had no home cheap enough that anyone wrote them. Plans carry a progress log, but a plan is ceremony; a dead end is not.
Three shapes were weighed for the record. Structured fields on the page (Firestore) — queryable, but invisible in the Markdown an agent reads and an export carries. A separate log type per page — a second page to maintain per decision. Or an append-only section in the Markdown itself, written through a tool that adds one dated line and edits nothing.
Decision
Records are dated lines appended to a named section of the page, through a tool that never edits what is above them. Runs are additionally records under the page, because a run has structure a line cannot hold.
datedLineinpackages/editoris the one shape:- YYYY-MM-DD — text (see …) — by.appendUnderHeadingis the one operation. Three faces use it:add_outcomeon an ADR’s What happened,append_journalon the month’s Entries, and a run’s line under a runbook’s Runs. Each is a revision attributed to the caller; the section above is untouched.- An ADR declares its reversibility:
door: one-way | two-way, default two-way. The template gains Assumptions, Invalidation triggers, Blast radius, Alternatives rejected, Escape plan and What happened. A one-way door is refused acceptance while Assumptions, Invalidation triggers or Escape plan are empty — the same shape of refusal as completing a plan with open tasks, on both the web and MCP paths. A two-way door is three lines. - What happened and Entries are ledgers: the tools that write them
append; the guidance says never to
update_sectionthem. - The journal is
type: journal, one page per workspace per month, found by an externalRef Bladbase stamps (bladbase/journal:YYYY-MM) and created on first append, so two first appends in a new month resolve to one page. - A runbook’s steps are its sections in order; a section with a fenced
shell block is automated, one without is manual. Runs are records under
the page (
runs/{runId}), written only byrecord_run, and one line lands under Runs so the record survives export. Bladbase never executes a step; the CLI runs it in the operator’s shell and reports.
Assumptions
- HTML comments in a template are dropped by the Markdown round trip. That is relied on: the gated sections are seeded with comments so that an unfilled one reads as empty to the gate, and a filled one is a real answer.
- The Markdown round trip is stable enough that appending a line and
re-serialising the document changes nothing else.
set_taskhas depended on this since Phase 6.
Invalidation triggers
- Outcomes or journal entries appearing edited or reordered in revision diffs, which would mean the round trip is not stable for these sections.
- A one-way ADR accepted with an empty Escape plan, which would mean a status path the gate does not cover.
Blast radius
Contained in packages/editor/src/frontmatter.ts (the line, the section
helpers), packages/domain/src/entities.ts (door, the journal helpers, the
required sections), apps/web/src/lib/server/pages/content-use-cases.ts
(appendLine and its faces) and the MCP tools. Leaks into the ADR template
in packages/domain/src/guidance.ts, which every new workspace is seeded
with.
Alternatives rejected
- Structured fields for outcomes. Queryable, but an export would not carry them and an agent reading Markdown would not see them. The page is the record.
- A
changes-requested-style status for “drifting”. A status is one value; the ledger is many lines. (The same reasoning as ADR-0010.) - A per-page log page. Twice the pages to keep in step.
Consequences
- Every ADR written from now on says how reversible it is, and a one-way door cannot be accepted on hope.
- A milestone review has evidence to read: the What-happened lines beside the Assumptions they tested.
- A runbook page can say Never run, which is the honest state of most runbooks, and saying it is the point.
- Templates carry HTML comments a person never sees in the editor; the guidance page is where the section descriptions live for people.
Escape plan
The ledgers are ordinary Markdown sections; removing the tools leaves the
pages readable. door is one optional field; dropping the gate is one
condition in two status paths. Run records are one subcollection.