Why this session exists
Back in Session 1, docs/AUDIENCE.md named a third audience: future maintainers, including future us. They have been waiting patiently while we served analysts (README, Session 4) and on-call engineers (how-to, Session 5). Maintainers don't need steps; they need facts, decisions, and history. Those are three different doc types, and today we build one of each. Together they are the memory of the project - the part of the doc set that keeps paying off after everyone who built Pulse has moved on.
config.yaml, Pulse's first architecture decision record (ADR-001, the UTC decision), and a real CHANGELOG.md with an Unreleased section and a properly categorized 0.1.0 entry. Cards marked "Live" are what we do together; "Self-study" cards give you full source depth at your own pace.
Reference: describe the machine 7 min live
Diátaxis defines reference as technical description of the machinery: austere, neutral, factual, consulted while working rather than read end to end. Nobody curls up with a reference page. They arrive with one question ("what does alerts.min_daily_events actually do?"), grab the answer, and leave. Everything about how you write reference follows from that.
LiveReference discipline: facts only, no instructions3 min▶
Diátaxis is strict here, and the strictness is the value. Reference is austere and factual technical description. Its structure mirrors the structure of the thing it describes: the reference for config.yaml is organized like config.yaml, not like a lesson plan. And it never gives instructions.
- Describe, don't teach. "clean.py drops rows whose event type is not in this list" is reference. "You should keep this list small" is opinion; "to add a type, edit the list and rerun" is a how-to. Both leak into reference pages constantly, and both belong elsewhere.
- "How to change it" is a link, not a paragraph. When the maintainer reading about
alerts.min_daily_eventswants to tune it, the reference links to a how-to. Mixing instruction into description makes the page longer to scan and harder to trust - is this line a fact or a suggestion? - Examples are allowed; instruction is not. Diátaxis explicitly permits illustrative examples in reference ("a valid value looks like
page_view"). The line it draws is between showing what something is and telling the reader what to do.
The config page nobody trusted. A data platform team's settings page mixed defaults, war stories and advice: "we set this to 200 after the March incident, you probably want something similar." Readers couldn't tell current fact from historical opinion, so they read the source code instead - the page was worse than nothing. The rewrite kept only type, default, effect; the March story moved to an ADR. Lookups came back within a month.
LiveThe standard-pattern rule2 min▶
Diátaxis: reference should follow standard patterns, the way a dictionary gives every word the same treatment. For a config reference, that means every key gets the same fields, in the same order, every time:
- Type - what kind of value this is (path, integer, list of strings).
- Default - the value that ships, exactly as it appears in the file.
- Effect - what the pipeline does with it, in one factual sentence.
- Safe range - what values work, and what breaks outside them.
The payoff is mechanical: after the first entry, the reader's eyes know where the default lives on the page. They stop reading and start looking up - which is the entire job of a reference page. It also makes gaps visible: an entry missing its "safe range" line is obviously unfinished, in a way a free-prose paragraph never is.
Self-studyOrganizing large docs: when Pulse outgrows one page3 min read▶
Today Pulse's reference fits on one page. Google's Technical Writing Two covers what to do when it doesn't - when a doc grows enough that readers get lost inside it:
- Split by reader task, not by module. The signal to break one doc into several is that different readers come for different jobs. Outline first; a doc that resists outlining is usually two docs.
- Introductions declare coverage. Each resulting doc opens by stating what it covers, what it assumes, and what it deliberately excludes - the Session 1 declarations, applied at scale.
- Task-based headings, text under every heading. "Configuring alerts" beats "Alerts"; and never jump from a heading straight to a subheading - give the reader at least a sentence of orientation first.
- Progressive disclosure, simple to complex. Early sections must not depend on late ones. The common case comes first; the exotic override lives at the bottom, where only the reader who needs it will ever scroll.
Decisions and changes: the project's memory 8 min live
Reference tells the maintainer what the machine is. Two more docs tell them why it is that way and how it got here. Architecture decision records (Michael Nygard's 2011 template) capture the why; a changelog (keepachangelog.com) captures the when. Both exist because the alternative is archaeology: reading old commits and guessing at intent.
LiveThe ADR template, section by section4 min▶
Nygard's template has five short sections, written in full sentences - he describes an ADR as a conversation with a future developer, one or two pages long. Pulse has a perfect first candidate: clean.py silently converts every timestamp to UTC, a decision that once caused real pain and that no doc currently explains. Here is the template, with ADR-001 as the worked example:
| Section | What goes there | ADR-001: UTC everywhere |
|---|---|---|
| Title | Numbered, short noun phrase | "ADR 1: Normalize all timestamps to UTC in clean.py" |
| Context | The forces at play - technical, political, social. Neutral and factual: even people who disagreed should accept this section as fair. | Events arrive with mixed timezones; some timestamps are naive. A past incident double-converted times. The team itself spans timezones. |
| Decision | Active voice: "We will..." | "We will normalize every timestamp to UTC at the clean step. Naive timestamps are assumed UTC." |
| Status | Proposed, accepted, deprecated, or superseded (with a pointer to the successor) | Accepted |
| Consequences | ALL outcomes: positive, negative, and neutral | + downstream math is tz-safe · - local-time debugging needs conversion, naive non-UTC sources are silently mis-read · ~ dashboards must convert for display |
The consequences section is where most ADRs go wrong. If yours lists only benefits, it isn't a record of a decision - it's a press release. The negative consequences are precisely what the future developer needs: "naive non-UTC sources are silently mis-read" is the sentence that saves them a week of debugging.
LiveChangelog principles: written for humans3 min▶
Keep a Changelog (v1.1.0) starts from one principle: changelogs are for humans, not machines. A human is deciding whether to upgrade, what to test, what might break. Everything else follows:
- One entry per version, newest first, with an ISO 8601 date (2026-07-14, never 07/14/26 - a date format half the world reads backwards is not for humans).
- Group changes into six categories: Added, Changed, Deprecated, Removed, Fixed, Security. The categories do the reader's triage for them - Security and Removed get read first, every time.
- Keep an [Unreleased] section at the top. Merged-but-unshipped changes accumulate there, so cutting a release is a rename, not an afternoon of git spelunking.
- Say whether you follow SemVer. One line at the top; it tells readers how to interpret your version numbers.
And the anti-patterns, which keepachangelog calls out by name: dumping your commit log (commits are for machines; "refactor helpers, fix typo, wip" tells an upgrader nothing), ignoring deprecations (the reader's one chance to migrate calmly), regional date formats, and selective reporting - a changelog that hides the embarrassing changes trains readers to distrust all of it.
daily_metrics.csv is your upgrader: "Changed: value column now has 2 decimals" is the line that stops their Slack message before they send it.
Self-studyThe ADR lifecycle and where ADRs live2 min read▶
Conventions from adr.github.io and Nygard's original post, for when Pulse has more than one decision on record:
- ADRs live in the repo (conventionally
docs/adr/), versioned with the code they explain. A decision record on a wiki drifts; one in the repo travels with every clone. - Numbered sequentially, and numbers are never reused. ADR-001 is ADR-001 forever, even if the decision dies.
- Superseded, never deleted. When a decision is reversed, the old ADR's status becomes "superseded by ADR-00X" and it stays put. The trail of reversals is itself the documentation: it tells the future developer this problem is hard and has been solved twice already.
- Keep them 1-2 pages of full sentences. Bullet-point fragments lose the reasoning; the reasoning is the entire point.
Write the config reference ★ 8 min · build your own
Every key in pulse/config.yaml, documented in the standard pattern. This is the doc the plan promised maintainers in Session 1: "change any setting safely."
Create docs/reference-config.md. Open with one scope line: "Factual reference for every key in config.yaml. For changing values, see the how-to guides."
Document all six keys - the four paths, events.valid_types, and alerts.min_daily_events - using the same fields for each: type, default, what it does, what breaks if wrong. A table works; so do repeated blocks. Consistency matters more than the format.
Audit your own prose: hunt for "you should", "we recommend", "to change this". Each one is instruction leaking into description - cut it or turn it into a link.
ADR-001: why UTC everywhere ★ 8 min · everyone follows
The UTC conversion in clean.py is exactly the kind of decision that evaporates: invisible in the output, obvious to whoever wrote it, baffling to whoever inherits it. We write it down while the reasoning is still alive.
Create the directory docs/adr/ and the file ADR-001-utc-everywhere.md.
Write the five sections. Keep Context neutral - report the forces, don't argue the case. Put the argument's outcome in Decision, in active voice.
Stress-test Consequences: it must contain at least one negative and one neutral outcome. If you can't think of a negative consequence, you haven't understood the decision yet.
The decision that got re-litigated quarterly. A team without ADRs debated their event-time convention from scratch every time someone new joined - same arguments, same whiteboard, four times a year. After writing the decision down with its consequences, the debate became a two-minute link. New hires still disagreed sometimes; now they opened a superseding ADR instead of a meeting.
Start the changelog ★ 6 min · build your own
Pulse first produced metrics on 2026-07-06. That is version 0.1.0, and everything it shipped gets categorized properly - no commit-log dump, no vague "initial release" and nothing else.
Create CHANGELOG.md at the repo root, with the header lines stating the format and SemVer adherence.
Add the [Unreleased] section first - today's three docs go straight into it under Added.
Write the 0.1.0 entry, dated the day the pipeline first ran. Sort every item into one of the six categories; for a first release that's mostly Added, and that's fine.
Try it yourself - this week ◐ 30-45 min total
- Run the ★ Try it now from Part 1 on three settings of a real system you own. If the standard pattern feels tedious, good - tedium for the writer is scanability for the reader.
- Write one real ADR for your team: the decision everyone re-explains to new joiners. Use the five sections; get one negative consequence in writing.
- Look at your most-used project's changelog (or its absence). If it's a commit dump, rewrite the latest release's entry in the six categories - it usually takes ten minutes and reads ten times better.
- Read the two Self-study cards: organizing large docs, and the ADR lifecycle. The superseded-not-deleted rule is the one people get wrong first.
- Optional depth: Nygard's original 2011 post (cognitect.com), adr.github.io, and keepachangelog.com - all three are short, and this page covers their working content.
Official sources covered
This session teaches the working content of these sources. What stays with the originals: Diátaxis's full essay prose, Nygard's worked Rails example, and keepachangelog's FAQ.
Three questions before you go 🎯 ◐ 90 seconds
1 · Which line does NOT belong on the reference page for alerts.min_daily_events?
B is instruction - a how-to wearing a reference costume. Reference describes; the steps for changing a value live in a how-to guide, one link away.
2 · An ADR's Consequences section lists five benefits and nothing else. What's wrong?
Nygard is explicit: all consequences, positive, negative and neutral. The negatives ("naive non-UTC sources are silently mis-read") are exactly what the future developer needs most.
3 · Which of these is a changelog anti-pattern called out by keepachangelog?
Commit logs are for machines; changelogs are for humans deciding whether to upgrade. B and C are both required practice, not anti-patterns.