---
title: 'Docs Alongside Code'
source: 'https://academia.sh/en/courses/technical-writing/docs-alongside-code'
course: 'Technical Writing and Documentation'
language: en
updated: '2026-08-17T18:10:49+00:00'
license: 'CC BY-SA 4.0'
---

# Docs Alongside Code

Versioning documentation in the same repository as code leaves twelve versions of stale debt at 300/672; bringing it into review as well drops that to 43/672; on two surfaces review does not see, 15 claims stay silently stale.

Every measurement up to this point took the document **as it was the moment it was
written.** The four types, their surface mixes, heading layout, and whether examples ran —
all of it was measured at a single moment, version zero. At that moment a document is a
text, and every one of the text's claims is true.

A document does not stay at that moment. What it describes keeps changing, and if the
document does not change with it, the **claim** and reality drift apart. This lesson's
question is not how the document is written, it is where it stands: whether the
documentation lives next to the code or somewhere separate; whether it enters the same
record as the change, or gets gone over on a separate calendar. We will build three
operating regimes and track the same fifty-six claims through twelve versions in all
three.

## Three Operating Regimes

**Separate calendar.** The documentation lives outside the code; it has its own
repository, its own publishing schedule. No one opens it together with a change. At fixed
intervals — every five versions, in our setup — someone sits down and goes over the whole
document. At that pass every claim is refreshed, then the document is left alone again.

**Versioned alongside code.** The documentation is inside the source tree and enters the
same record as the change itself. This is the core of the **docs as code** approach: the
documentation is not a separate entity, it is a file of the repository. The author, while
changing the code, can also touch the documentation in the same record. They do not have
to.

**Versioned alongside code and brought into review.** The change is read by someone else,
and the reader also sees the documentation. Because the documentation's lines stand on the
same screen as the code's lines, the sentence "you renamed this field, the reference still
shows the old name" becomes possible to construct.

All three regimes spend the same writing effort. What differs between them is **when the
documentation gets looked at**, not how well it is written.

The third regime has two conditions, and both are structural. First, the documentation
file has to stand **inside** the change: a documentation fix filed as a separate record
does not appear on the same screen as the code change it fixes. Second, a missing
documentation update has to be counted by the reviewer as a **defect class**: of two people
looking at the same list, one reads an untouched file as a gap, the other does not. The
measurement assumes the second condition holds; where it does not, the third regime
collapses into the second and the table's gap disappears.

## The Surface Review Sees

Review's own axis was established in the Code Review and Team Process course: review sees
only in the axis it looks at and only as far as the chunk it reads. That discussion is not
repeated here. What we need is only this boundary: **the reviewer reads what is visible
inside the change.**

This boundary splits the four surfaces in two. **Signature** and **flow** are visible
inside the change: when a field's type changes or the order of steps changes, the lines
producing that change are inside the record, and the reviewer reads both side by side.
**Name** and **concept** are invisible: something's externally given name changes on the
product side, a reasoning changes in a discussion; neither may leave a trace in the source
line. When the reviewer reads the old name in the documentation, they have no reason to
suspect it is wrong.

Where the documentation stands inside the change is visible the same way:

```text
# example change listing, not executed

change: unit field added to the measurement interface
  source/metrics.py                 +14  -3
  source/metrics_validate.py         +6  -0
  docs/reference/metrics.md          +9  -4
  docs/tutorial/first-metric.md       0   0
```

The fourth line is what this lesson cares about. The reference is updated, the tutorial is
untouched. The reviewer sees this list and can notice the tutorial went untouched; but to
notice, they need to know **which claim** of the tutorial is affected by this change. What
they can know is signature and flow; what they cannot is name and concept.

## The Documentation's Version

Putting the documentation next to the code has a second consequence independent of
review: the documentation enters the repository's versioning scheme. A documentation
change made on a branch belongs to that branch; when a version tag is placed, that tag
points at the documentation too. The criterion the Introduction to Version Control course
established applies directly here — if the record was written to the object database, it
can be brought back. The documentation's state at version three can be read from the
version-three tag.

Documentation run on a separate calendar does not carry this property. It has a single
state: **the current one.** A reader using version three reads a text describing version
six and cannot measure the gap. This is a different kind of wrongness from the staleness
we measure: the claim is true for today's code, false for the code the reader has in
hand. Our measurement does not **count** this, because the oracle knows every version
individually; in a real setup, which version the reader is on is not known, and this
uncertainty cannot be closed by the documentation.

The versioning decision also has a direct cost. If the documentation moves forward on the
same branch as the code, a fix reaches a published version only by being carried over to
that version's branch; on a separate calendar, a fix is instantly visible to everyone.
The docs-as-code approach **binds correctness to the version** and, in exchange,
multiplies the publishing work. Our setup does not measure this cost; the only thing it
measures is whether a claim stays true.

## The Measurement's Assumptions

- **DO1** — Fifty-six claims, four types, and four surfaces are taken from the shared
  setup. The oracle is the setup itself: because we wrote it, we know which claim is
  bound to which surface and at which version it goes stale.
- **DO2** — Surface frequencies do not change: signature changes once every two versions,
  flow every three, name every five, concept every twelve. A claim goes **stale** the
  moment the surface it is bound to changes.
- **DO3** — Seventeen of the fifty-six claims are embedded in a runnable example. An
  embedded claim makes noise when it goes stale: the example fails. A claim that is not
  embedded makes no sound when it goes stale; this is **silent staleness**.
- **DO4** — In the separate-calendar regime, the document is gone over at versions five
  and ten, and at that pass **every** claim is refreshed. In the versions between, no one
  looks at the document.
- **DO5** — In the versioned-alongside-code regime, only embedded claims are repaired:
  staleness announces itself because the example fails, and it gets fixed in the same
  record.
- **DO6** — In the reviewed regime, claims bound to the **signature and flow** surface are
  repaired in addition to embedded claims; those on the **name and concept** surface are
  not repaired, because they are not visible inside the change.
- **DO7** — The measure's unit is the **stale version-claim** count: at the end of every
  version, the claims standing stale are counted and summed across twelve versions. The
  upper bound is fifty-six times twelve, that is, 672.
- **DO8** — Repair happens within the version the claim goes stale. The difference between
  regimes is not the speed of repair, it is **which claim never gets touched at all.**

## Measurement

```python
"""The effect of bringing documentation into review on staleness.

Part 1 - three operating regimes, stale version-count over twelve versions.
Part 2 - surfaces' embeddedness and visibility-in-review distribution.
"""
TYPES = {
    "tutorial":    {"flow": 9, "signature": 3, "name": 2, "concept": 1},
    "how-to":      {"flow": 6, "signature": 5, "name": 2, "concept": 1},
    "explanation": {"flow": 1, "signature": 1, "name": 1, "concept": 9},
    "reference":   {"flow": 0, "signature": 13, "name": 2, "concept": 0},
}
FREQUENCY = {"signature": 2, "flow": 3, "name": 5, "concept": 12}
EMBEDDED = {"tutorial": 0.60, "how-to": 0.70, "explanation": 0.10,
            "reference": 0.00}
SEEN = ("signature", "flow")   # surfaces code review notices
OVERHAUL = (5, 10)             # versions when the separate-calendar doc is overhauled
LAST = 12


def claims():
    entries, no = [], 0
    for kind, mix in TYPES.items():
        embedded_target = 0
        for surface, count in mix.items():
            for _ in range(count):
                no += 1
                embedded_target += EMBEDDED[kind]
                embedded = embedded_target >= 1 and surface != "concept"
                if embedded:
                    embedded_target -= 1
                entries.append({"no": no, "type": kind, "surface": surface,
                                 "embedded": embedded})
    return entries


def changed(version):
    return {s for s, f in FREQUENCY.items() if version % f == 0}


def repaired(regime, claim, version):
    if regime == "separate calendar":
        return version in OVERHAUL
    if regime == "versioned together":
        return claim["embedded"]
    return claim["embedded"] or claim["surface"] in SEEN


REGIMES = ("separate calendar", "versioned together", "versioned + reviewed")


def run(regime, entries):
    true = {i["no"]: True for i in entries}
    debt = 0
    for version in range(1, LAST + 1):
        d = changed(version)
        for i in entries:
            if i["surface"] in d:
                true[i["no"]] = False
            if not true[i["no"]] and repaired(regime, i, version):
                true[i["no"]] = True
        debt += sum(1 for i in entries if not true[i["no"]])
    stale = [i for i in entries if not true[i["no"]]]
    silent = [i for i in stale if not i["embedded"]]
    return debt, len(entries) - len(stale), len(stale), len(silent)


L = claims()
print(f"claims {len(L)}, embedded in example {sum(1 for i in L if i['embedded'])}, "
      f"surface review sees {sum(1 for i in L if i['surface'] in SEEN)}")
print()
print(f"{'regime':>20s} {'stale version-claims':>21s} {'true':>6s} {'stale':>6s} "
      f"{'silent':>7s}")
for regime in REGIMES:
    debt, true, stale, silent = run(regime, L)
    print(f"{regime:>20s} {f'{debt}/{len(L) * LAST}':>21s} {true:6d} {stale:6d} "
          f"{silent:7d}")
print()
print(f"{'surface':>10s} {'claims':>6s} {'embedded':>8s} {'visible in review':>19s}")
for y in FREQUENCY:
    g = [i for i in L if i["surface"] == y]
    print(f"{y:>10s} {len(g):6d} {sum(1 for i in g if i['embedded']):8d} "
          f"{'yes' if y in SEEN else 'no':>19s}")
```

```
claims 56, embedded in example 17, surface review sees 38

              regime  stale version-claims   true  stale  silent
   separate calendar               299/672      7     49      35
  versioned together               300/672     17     39      39
versioned + reviewed                43/672     41     15      15

   surface claims embedded   visible in review
 signature     22        5                 yes
      flow     16        9                 yes
      name      7        3                  no
   concept     11        0                  no
```

The right three columns give the state at the end of the twelfth version: how many claims
stayed true, how many are stale, how many of the stale ones are silent.

## Same Debt, Different Distribution

The first two rows' totals are nearly identical: **299** and **300**. A document run on a
separate calendar and a document versioned alongside code but not brought into review
spend almost the same amount of time wrong across twelve versions. Moving it next to the
code alone produced no gain.

The totals are equal; the distributions are entirely different. In the separate-calendar
regime, no claim is permanently stale — the passes at versions five and ten reset
everything — but between two passes, **every kind of claim** stands stale. Only **7**
claims stay true at version twelve, and these are the ones whose surface has not changed
since the last pass: the name claims refreshed at version ten, because name changes once
every five versions and its next change is at version fifteen. The number seven is the
full count of claims bound to the name surface.

This also explains where the separate-calendar regime's debt comes from. **If the
overhaul interval is longer than the fastest surface's frequency, that surface is already
stale between two passes.** A five-version interval does nothing against a signature that
changes every two versions: the twenty-two signature claims fall wrong again at the
second version after every pass and stay that way until the next pass. Shortening the
interval lowers the debt but grows the reading work linearly; every pass means
fifty-six claims verified by hand.

In the versioned-alongside-code regime, the table flips: **17** claims are always true and
**39** claims are stale at version twelve. Seventeen is familiar — it is exactly the count
of embedded claims. Thirty-nine is familiar too: **all** of the stale ones are silent.
This regime repairs the one that makes noise immediately and never touches the one that
does not. The documentation is next to the code, it passes before the eye at every
change, and thirty-nine claims stay wrong for years.

**Versioning alongside code is a relocation decision, not a maintenance decision.**
Moving a file's location places no obligation on anyone to read it.

## What Review Saves and What It Cannot

The third row drops the debt from **300** to **43** — sevenfold. The gap comes from a
single thing: the reviewer also reads the documentation claims bound to the surface the
change touches. The lower table gives the scope: **38** claims are bound to the two
surfaces review sees, more than two-thirds of the fifty-six.

**15** claims remain, and all of them are silent in the table. These are claims bound to
the name and concept surface that are not embedded in an example: **7 − 3 = 4** on the
name surface, **11** on the concept surface. The embedded count being zero in the concept
column is a rule of the setup — a reasoning cannot be embedded in a runnable example.

These fifteen claims produce no warning in any of the three regimes. The example runs,
review passes, no one complains. At version twelve the document carries **41** true
claims and looks quite good to the reader; none of the **15** wrong sentences it carries
shows itself. The course's central problem shows up exactly here, and the next two
lessons build on top of it: **the visible portion of staleness and the existing portion
of staleness are not the same thing.**

The regimes' cost is not equal either. Separate calendar demands two full overhauls; all
fifty-six claims are verified by hand twice. The reviewed regime demands no extra pass at
all, because verification is folded into work already being done — reading the change.
The gap between them is the decision of whether documentation maintenance is a **separate
job** or **a field of an existing job.**

## What the Measure Does Not Say

The third regime's **43** is a lower bound, and an optimistic one. Assumption DO6 says
review catches every claim it can see; the Code Review and Team Process course's result
showed this is not true — review sees only as far as the chunk it reads even on the axis
it looks at, and it does miss defects. Documentation lines are the most readily skipped
part of that chunk, because they do not block the build from working. In a real setup,
the third row's debt sits somewhere above **43** and below the second row's **300**.

Two numbers are unaffected by this optimism. The **15** claims on the name and concept
surface can never be caught in this regime no matter how careful review is: they are not
visible inside the change, so there is no place to look. The **17** embedded claims are
equally certain in the opposite direction: they need no review at all, because the example
announces itself the moment it fails to run. The choice of regime only changes the fate
of the **24** claims in between.

## Summary

- Documentation can run in three regimes: reviewed on a separate calendar, versioned
  alongside code, or versioned alongside code and brought into review.
- Separate calendar produces **299/672**, versioned-alongside-code produces **300/672**
  stale version-claims; equal debt, entirely different distribution.
- Versioned-alongside-code leaves **17** true claims at version twelve, and **39** of the
  **39** stale claims are silent; relocation alone is not maintenance.
- Review drops the debt to **43/672** and raises true claims to **41**, because it sees
  the **38** claims on the signature and flow surface.
- The remaining **15** claims are on the name and concept surface, invisible to review,
  and all silent; this number is the starting point of the next lessons.

## Next Step

Review works because it is **a person reading the claim**, and a reading person is
expensive. There is a cheaper path for one of the surfaces: instead of writing the claim
by hand, **generate** it from the source it is bound to. A generated claim changes along
with its source, and its going stale becomes unthinkable. The next lesson separates
production approaches by class name and measures which surface generation actually saves,
and which two surfaces it never saves at all.
