learn-tech-writing-with-phoebe / Session 6 of 8
Learn Tech Writing with Phoebe · Session 6 of 8

Reference, ADRs, changelogs: milestone 2

Tutorials and how-tos serve the reader who is doing something right now. Today's three docs serve a quieter audience: the maintainer who arrives in a year and needs to know what every setting does, why the code is the way it is, and what changed since they last looked. This is the session where Pulse gets a memory.

🟠 Deeper Practitioners Data & AI teams 45 min live + self-study depth
0-3 · Welcome 3-18 · Concepts (live cards) 18-40 · Build-along on Pulse 40-45 · Q&A
Part 0

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.

Live - presented in session Self-study - read after class ★ Try it now exercise Sources: Diátaxis + Nygard + keepachangelog
★ What you walk out with today Milestone 2, in three files: a complete reference page for 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.
Part 1 · covers Diátaxis "Reference" + Google TW Two "Organizing large docs"

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.

paths.raw Typepath Defaultpulse/data/raw Effectwhere ingest.py reads Safe rangeany readable dir events.valid_types Typelist of strings Default4 event types Effectclean.py drops the rest Safe rangenon-empty list alerts.min_daily_events Typeinteger Default50 EffectLOW below this count Safe range>= 1 Different keys, identical shape. The reader learns the pattern once, then never reads again. Same shape for every entry = readers stop reading and start looking up.
🔍 Click to zoom - the reference-page pattern: one standard shape, repeated for every config key
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_events wants 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.
Real world

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.

★ Try it nowTake one setting from a system you own (an env var, a config key, a dbt var) and write its entry in the standard pattern: Key: Type: Default: Effect (one factual sentence): Safe range / what breaks if wrong: If "effect" came out as advice ("you should..."), rewrite it as description ("the pipeline does...").
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.
Part 2 · covers Nygard ADR template + keepachangelog 1.1.0

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.

[Unreleased] what's merged but not shipped - always at the top Added reference page, ADR-001 [0.2.0] - 2026-07-14 Added LOW alert on quiet days Changed value column now 2 decimals [0.1.0] - 2026-07-06 Added ingest, clean, metrics pipeline Fixed double timezone conversion Newest first · one entry per version · ISO 8601 dates · changes grouped by type, never a commit dump. A changelog is written for humans deciding whether to upgrade, not for machines.
🔍 Click to zoom - a keepachangelog-style timeline: releases newest-first, changes grouped by category
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:

SectionWhat goes thereADR-001: UTC everywhere
TitleNumbered, short noun phrase"ADR 1: Normalize all timestamps to UTC in clean.py"
ContextThe 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.
DecisionActive voice: "We will...""We will normalize every timestamp to UTC at the clean step. Naive timestamps are assumed UTC."
StatusProposed, accepted, deprecated, or superseded (with a pointer to the successor)Accepted
ConsequencesALL 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.

Data team tip Pipelines have consumers even when they don't have "users". The analyst whose dashboard reads 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.
Build-along 1 of 3

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.

docs/reference-config.md skeleton# config.yaml reference Factual reference for every key in config.yaml. For changing values, see docs/how-to-diagnose-low-day.md and the how-to guides. | Key | Type | Default | What it does | What breaks if wrong | |-----|------|---------|--------------|----------------------| | paths.raw | path | pulse/data/raw | where ingest.py reads incoming CSVs | ingest fails: dir not found | | paths.staged | path | pulse/data/staged | where ingest.py writes, clean.py reads | steps can't hand off | | paths.cleaned | path | pulse/data/cleaned | where clean.py writes, metrics.py reads | metrics reads stale data | | paths.metrics | path | pulse/data/metrics | where daily_metrics.csv lands | consumers can't find output | | events.valid_types | list of strings | page_view, signup, purchase, api_call | clean.py keeps only these types | valid rows silently dropped | | alerts.min_daily_events | integer | 50 | days below this get alert = LOW | 0 never alerts; too high always alerts |
Writing tip The "what breaks if wrong" column is reference gold and almost nobody writes it. It is still description - a fact about the system's behavior - but it answers the exact question a maintainer has at the moment they're staring at a bad value.
Build-along 2 of 3

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.

docs/adr/ADR-001-utc-everywhere.md# ADR 1: Normalize all timestamps to UTC in clean.py ## Status Accepted ## Context Pulse ingests event CSVs from several sources. Timestamps arrive in mixed timezones, and some rows carry naive timestamps with no offset information at all. Before this decision, each script handled timezones its own way; one past incident double-converted times and shifted a day's purchases into the wrong day's metrics. The team itself works across timezones, so "local time" is ambiguous even in conversation. ## Decision We will normalize every timestamp to UTC at the clean step (clean.py), before any metric is computed. Naive timestamps are assumed to already be UTC and are labeled as such rather than guessed. ## Consequences Positive: all downstream date math and day-bucketing is timezone-safe; metrics.py and every future consumer can trust one convention. Negative: debugging against local wall-clock time requires a mental or scripted conversion. Sources that emit naive timestamps in a non-UTC zone are silently mis-read; the assumption is documented here but not enforced in code. Neutral: dashboards and reports that present local time must convert at display time; this is their responsibility, not the pipeline's.
Real world

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.

Build-along 3 of 3

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.

CHANGELOG.md# Changelog All notable changes to Pulse are documented in this file. The format is based on Keep a Changelog 1.1.0, and this project adheres to Semantic Versioning. ## [Unreleased] ### Added - docs/reference-config.md: full config.yaml reference - docs/adr/ADR-001-utc-everywhere.md: timestamp normalization decision - CHANGELOG.md (this file) ## [0.1.0] - 2026-07-06 ### Added - Pipeline scripts: ingest.py, clean.py, metrics.py (fixed run order) - LOW alert flag on days below alerts.min_daily_events (default 50) - config.yaml with paths, valid event types, and alert threshold - Doc set: docs/AUDIENCE.md, docs/PLAN.md, README.md, docs/how-to-diagnose-low-day.md
Writing tip Notice what the Unreleased section just did: documenting docs. Doc changes are notable changes - the maintainer who upgrades wants to know the reference page exists now. This habit also feeds Session 7, where doc changes and code changes travel in the same pull request.
Homework

Try it yourself - this week ◐ 30-45 min total

Source material

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.

✓Diátaxis · ReferencePart 1: austere factual description, standard patterns, structure mirrors the product, no instruction
✓Nygard · Documenting Architecture Decisions (2011)Part 2 + Demo 2: the five-section template, worked on the UTC decision
✓keepachangelog.com · v1.1.0Part 2 + Demo 3: principles, six categories, Unreleased section, anti-patterns
✓Google TW Two · Organizing large documentsSelf-study card: splitting, introductions, task headings, progressive disclosure
✓adr.github.io · ADR conventionsSelf-study card: in-repo docs/adr/, sequential numbers never reused, superseded not deleted
Check yourself

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.

Session 6 cheat sheet · pin this

Reference disciplineAustere, factual, consulted not read. Structure mirrors the code. Examples yes, instructions never.
Standard patternEvery config key gets the same fields: type · default · effect · safe range. Same shape = lookup, not reading.
Link, don't teach"How to change it" is a link to a how-to, not a paragraph in the reference.
ADR five sectionsTitle (numbered noun phrase) · Context (neutral forces) · Decision ("We will...") · Status · Consequences.
All consequencesPositive, negative AND neutral. Benefits-only is a press release, not a record.
ADR lifecycleLive in docs/adr/, numbered forever, superseded never deleted. 1-2 pages of full sentences.
Changelog rulesFor humans · newest first · ISO dates · six categories (Added/Changed/Deprecated/Removed/Fixed/Security) · keep [Unreleased].
Pulse statusReference ✓ · ADR-001 ✓ · CHANGELOG ✓ - milestone 2 done. Session 7 wires docs into the same PRs as code.