---
title: 'Code Review and Team Process'
source: 'https://academia.sh/en/courses/code-review'
language: en
updated: '2026-08-17T04:42:47+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Reviews a change request along the meaning, implementation, test, and documentation axes'
    - 'Writes constructive and actionable feedback'
    - 'Interprets the measurable health indicators of the review process'
---

# Code Review and Team Process

A review sees only along the axis it looks and only as far as it reads; the course measures every configuration by defects found, defects missed, and the class no axis looks for.

## Review Axis

Purpose of review, the contract of a public interface and backward compatibility, correctness and edge cases, the meaningfulness and fragility of tests, a change's reflection in documentation, and handing the style discussion off to a tool.

### Purpose of Code Review

Source: https://academia.sh/en/courses/code-review/purpose-of-code-review

On the same change, three reviewers on the same axis find 6 defects, three reviewers on separate axes find 15, a single reviewer on five axes finds 22; five reviewers on five axes also find 22, and two unwritten-requirement defects escape all four of the four configurations.

The Advanced Git course worked with a single unit across twelve lessons: the number of
objects an operation touched. Rewriting touched 5640 objects, binary search finished in
eleven steps, a hook placed a barrier early or late. The closing lesson left a question
those numbers could not answer: **the tool changes history, a human makes the decision.**
Knowing a transformation's cost does not say whether that transformation should happen.

This course looks at where the decision gets made, and the unit measured changes. There,
what was counted was **what the tool touched**; here, what is counted is **where a human
looks** — and what is left outside that. The question itself changes too: not how many
people read a change, but what that reading finds and what it fails to find.

#### The Three Jobs of Review

**Code review** is a change being read by someone who did not write it, before it is
merged, and that reading being put on record. It has three jobs, and the three are not
the same thing.

The first is **finding defects**: seeing something incorrect in a submitted change
before it is merged. The second is **spreading knowledge**: preventing a change from
being known only to its author; the reader becomes a second owner of that code. The
third is **design alignment**: checking whether the change stays pointed the same
direction as the rest of the repository.

What this course measures is the first, because it is the one that can be counted. But
the three are not independent of each other: the **review axis** concept established in
the next section applies to all three at once. Who looks at which axis determines the
defect found, the knowledge spread, and the level at which alignment happens, all at
once.

The mechanics of a change request — proposal, discussion, the merge cycle — were
established in the Branching and Collaboration course, and the review axis was
explicitly left for here; it is not repeated. Reading with a security eye is the subject
of the Secure Coding course, and security is not on this course's axis list. The
organizational discussion of team structure and decision rights belongs to the Process,
Team, and Delivery course. What is left here is only review's own numbers.

#### Two Limits: Axis and Attention

A reviewer is not unlimited. They work with two limits, and every measurement in this
course is built on these two.

**Axis:** a reading does not look at everything at once. The eye looking at the
interface searches for the signature, the naming, and the contract; the eye looking at
the implementation searches for loop boundaries and edge cases; the eye looking at the
test checks whether the assertion actually verifies something. Two eyes passing over the
same line see two separate defects, because they are searching for two separate things.
This course defines five axes: **interface, implementation, test, documentation,
style.**

**Attention:** a reviewer reads a limited amount of text in one round. The unit of
reading is called a **chunk**, and a chunk is fifty lines. The number of chunks actually
read in one round is fixed; if the change is larger than that, the rest goes unread. In
this lesson's measurement, the change is exactly the size of the attention budget, so
**no defect in this lesson goes unread.** Every defect that is missed will have a single
cause: an axis not looked at.

#### What the Five Axes Search For

The axes are not abstract; each asks a specific question and writes its finding in a
specific form. The notes below are the product of five readings looking at the same
change from five separate axes.

```text
# taught review-comment example, not executed

interface       Second parameter of the public function has become required;
                code calling with the previous signature no longer compiles.
                Giving a default value would preserve the contract.
implementation  Loop reads the last element when the list is empty; an early
                return is needed for empty input.
test            Test's only assertion is that the call does not throw; there
                is no assertion verifying the return value.
documentation   The behavior the function's description states has changed;
                the comment line describes the old behavior.
style           Indentation is used at two different widths in this file.
```

This block is an example and is not run; this lesson's numbers come only from the
measurement block. The notes share something in common: **all five point at a specific
spot in the change and say what would close it.** None of them mentions a person. This
is a boundary kept throughout the course — review reads the submitted change, not its
author.

Their second shared trait is that none can substitute for another. The interface note
cannot be written without looking at the signature; the eye writing the style note is
not searching for a loop boundary. Two defects sitting in the same line range of the
same file require separate readings, because they belong to separate axes. The
measurement's table is the numeric counterpart of this independence.

Spreading knowledge and design alignment carry the same dependency. Someone who reads a
change only along the style axis does not become that code's second owner; they learn
the indentation, not the behavior. Likewise, design alignment can only be seen from the
interface and implementation axes. Axis choice therefore determines all three of
review's jobs at once.

#### The Defect of What Was Never Written

Outside the five axes there is a sixth defect class, and it is the setup's most
important part: **unwritten requirement**. This is not a wrong line inside the change;
it is what **should have existed but was never written at all.**

For an interface defect, the signature is looked at; for a test defect, the assertion is
looked at. For an unwritten requirement there is no place to look, because that
requirement does not exist as text inside the change. Review looks at **what was
submitted**; it cannot see the absence of what was never submitted. This is not an
attention problem either: that class is not on the axis list.

#### The Measurement's Setup

The measurement is built on one change with **24 defects** scattered through it. Each
defect belongs to one of six classes. The oracle is the setup itself: because we placed
the defects ourselves, which chunk and which class each one belongs to is known. Four
configurations look at the same change, and for each one, **found** and **missed** are
written together.

- **RA1** — The change is 600 lines, meaning 12 chunks; attention is also 12 chunks. The
  entire change is read, and no chunk goes unread throughout this lesson.
- **RA2** — 24 defects are distributed across six classes by the setup; the class list
  and the distribution are the course's constant and are not changed in any lesson.
- **RA3** — A reviewer sees only the defects on their own axes. A defect outside the
  axis is not found even if the line has been passed over.
- **RA4** — Multiple reviewers' findings are a union: if two people find the same
  defect, it is counted once. The difference between configurations is therefore not
  **the number of people, it is the set of axes covered.**
- **RA5** — Two reviewers looking at the same axis find the same set. The setup includes
  no finding share where the second person completes the first; this is a deliberate
  simplification and ties the measurement's result to a single thing: axis diversity.
- **RA6** — The sixth class is on no axis's search list. This is not a gap, it is a
  definition.
- **RA7** — The measurement has no round, wait, or feedback-writing; those are the next
  topics' variables. Here, a single reading in a single round is counted.
- **RA8** — In a 24-defect set, the smallest measurable difference is **1/24**, that is,
  **0.042**. A difference smaller than this cannot be defended with this set.

#### Measurement

```python
"""Purpose of code review: reviewer count or axis count.

Part 1 - the change and the defects' class distribution.
Part 2 - four configurations: found, missed, missed that no axis looks for.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"      # not searched by any axis
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12                           # chunks actually read in one round
CHUNK = 50                               # lines


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    """A reviewer: sees only in their own axis and only the chunks they read."""
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


FULL = set(AXES)
ASSIGNMENTS = {
    "one reviewer, all axes": [FULL],
    "three reviewers, same axis": [{"implementation"}] * 3,
    "three reviewers, separate axes": [{"interface"}, {"implementation"},
                                        {"test", "documentation"}],
    "five reviewers, five axes": [{a} for a in AXES],
}

d = change(600)
distribution = {s: sum(1 for k in d["defects"] if k["class"] == s) for s in CLASSES}
print(f"change {d['lines']} lines, {d['chunks']} chunks, "
      f"defects {len(d['defects'])}, attention {ATTENTION} chunks")
print("class distribution: " + ", ".join(f"{s} {n}" for s, n in distribution.items()))
print()
print(f"{'assignment':<32s} {'axes':>5s} {'reviewers':>9s} {'found':>7s} "
      f"{'missed':>6s} {'unwritten missed':>16s}")
for name, assignment in ASSIGNMENTS.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    unwritten = sum(1 for k in missed if k["class"] == UNWRITTEN)
    axes = len(set().union(*assignment))
    print(f"{name:<32s} {axes:5d} {len(assignment):9d} {len(b):7d} "
          f"{len(missed):6d} {unwritten:16d}")
```

```
change 600 lines, 12 chunks, defects 24, attention 12 chunks
class distribution: interface 3, implementation 6, test 3, documentation 3, style 7, unwritten requirement 2

assignment                        axes reviewers   found missed unwritten missed
one reviewer, all axes               5         1      22      2                2
three reviewers, same axis           1         3       6     18                2
three reviewers, separate axes       4         3      15      9                2
five reviewers, five axes            5         5      22      2                2
```

#### The Number Does Not Track Reviewer Count

Two of the table's columns sit side by side, and the **found** column tracks only one of
them.

**Reviewer count is 1, 3, 3, 5**; defects found are **22, 6, 15, 22**. There is no
regular relationship between them: one person finds 22, three find 6, five find 22
again.

**Axis count is 5, 1, 4, 5**; defects found are **22, 6, 15, 22**. This column explains
what was found, row by row. Five axes give 22, four give 15, one gives 6. This is where
the course's measure comes from: **a review's number is not how many people are
looking, it is how many axes are being looked at.**

The second and fourth rows say this on their own. **Three reviewers on the same axis
find 6** defects; all three look at the implementation and there are 6 defects in the
implementation class. The second and third people add not a single defect to the first,
because they are searching for the same thing. In contrast, **a single reviewer looking
at five axes finds 22**, and **five reviewers spread across five axes also find 22**.
There is a **0** difference between five people and one. This is the second claim:
**adding reviewers does not find defects; adding axes does.**

The third row gives the value in between. Three reviewers cover four axes — one on the
interface, one on the implementation, one on both test and documentation — and **15**
defects come out. Of the 9 missed defects, **7** are in the style class; no one is
looking at that axis. This is the clearest evidence that it is adding axes, not adding
people, that works: the same three people find 15 instead of 6 once the axes are spread
out. The difference is **9** defects, more than twenty times the set's resolution.

#### The Distribution Is Itself a Result

The second line of the output is information that comes before the measurement, but it
deserves to be read on its own: **style 7, implementation 6, interface 3, test 3,
documentation 3, unwritten requirement 2.**

The most crowded class is style, holding **7** of the 24 defects, or **0.292** of them.
This number explains why the most-noted thing in a review is so often style: a style
defect is both more numerous and requires the least information to see. Next comes
**implementation**, with **6** defects, and this class is the opposite — seeing it
requires understanding what the code does.

The remaining three axes — interface, test, documentation — each carry **3** defects,
totaling **9**. All three together are fewer than style and implementation combined; yet
all three are classes that require a separate reading, and none is found as a
byproduct of another.

The distribution's practical consequence is the subject of later lessons. If a review
spends its attention on the crowded but cheap class, the share left for the expensive
classes shrinks; the total found does not show this, but the per-class breakdown does.
This is why this course writes **found and missed** together in every measurement, and
why the missed defects' class is counted separately.

#### Two Defects Missed in All Four

The last column gives the same value in all four rows: **2**.

These two defects belong to the unwritten-requirement class, and they are missed no
matter what the configuration is. Whether a single reviewer looks at five axes or five
reviewers spread across five axes, they are missed. The missed count drops to **2** and
stops there — because no place to look was ever created for the remaining two defects.

This gives the **fourth reading**: **no axis sees the unwritten.** Adding an axis raises
the found count up to 22; it does not raise it to 24. Putting more people into the
review, spreading more axes, or having the same change read a second time will not find
these two defects. They can only be caught somewhere outside review — where the
requirement gets written.

Where the two defects can be caught is known, and it is before review: where the
requirement is written, where the acceptance criterion is set, and where the change's
scope is decided. A single sentence placed there can eliminate a defect review cannot
find with any axis. This is why this course's numbers say not only how good a review is,
but also **what it can never see.**

This is not a defect of review, it is the definition of its limit. A review saying "I
found nothing" does not mean the change is correct; it means **nothing was found in
what was submitted, on the axes looked at.** This sentence has three conditions, and all
three stand as separate columns in the table.

#### Summary

- Review has three jobs — finding defects, spreading knowledge, design alignment — and
  the thing that determines all three at once is who looks at **which axis**.
- A reviewer works with two limits: **axis** limits them to seeing only the class they
  search for, **attention** limits the number of chunks they read in one round. In this
  lesson the change is exactly the size of the attention budget, so every missed
  defect's cause is the axis alone.
- On the same change, three reviewers on the same axis find **6**, three reviewers on
  separate axes find **15**, a single reviewer on five axes finds **22**; five reviewers
  on five axes also find **22**. **Adding reviewers does not find defects, adding axes
  does.**
- Two **unwritten requirement** defects are missed in all four of the four
  configurations; that class is on no axis list, and review cannot see the absence of
  what was never submitted.
- The class distribution is not even: style **7**, implementation **6**, interface,
  test, and documentation **3** each. The most crowded class is the one that requires
  the least information to see.
- The found count tops out at **22/24** in the best case; the remaining **2** defects
  are not review's problem, they are the problem of where the requirement gets written.

#### Next Step

The table's single-axis row gave 6 defects, and that axis was implementation. Each of
the five axes carries its own share, and the shares are separate from one another: what
one axis finds, no other axis finds. The next lesson measures this separateness on the
first axis, the **interface** axis: how does review notice a change that breaks a public
interface's contract, how many axes can see that finding, and how many defects are lost
if someone looking at a different axis replaces the reviewer looking at the interface.

### Meaning of Interface

Source: https://academia.sh/en/courses/code-review/meaning-of-interface

The sets the five axes find are disjoint and the sum of pairwise intersections is 0; found drops from 22 to 19 when the interface axis is closed, and ten reviewers without an interface axis find 3 fewer defects than a single reviewer covering all five axes.

The previous lesson showed that a five-axis reading finds **22** of the 24 defects and a
single-axis reading finds **6**. The difference came from axis count; but the table
assumed something and did not verify it: that the axes' **shares are disjoint from one
another.** If the five axes sum to 22, that means no defect is being counted by two axes
at once.

This lesson measures that assumption, and it does the measuring on the first axis, the
**interface** axis. Its question is this: is an interface defect really found only by
the eye looking at the interface, or does it become visible from another axis too if
enough people read carefully enough?

#### The Contract of a Public Interface

A piece of code's **public interface** is the set of externally callable names and their
behaviors: function signatures, type fields, return values, error forms, and the names
themselves. This set is called the **interface contract**, because the calling side
writes code trusting it.

Breaking the contract does not have to be a compile error. Versioning strategies and the
classification of breaking versus non-breaking changes were established in the Web API
Design course's Errors, Versions, and Documentation topic; they are not repeated here.
The only thing this lesson measures is **how it is noticed in review**: which axis's eye
sees the trace, and which does not.

#### Five Ways to Break It, and Their Trace in Review

**Adding a required parameter.** The trace is in the signature itself and is read inside
the change. Calling code does not compile; the defect is also caught at compile time
after merge. Review's contribution here is not finding it, it is finding it **early**.

**Changing a parameter's meaning.** The signature stays the same — same name, same type
— but the expected value is different; a duration field's unit has moved from seconds to
milliseconds. The only thing visible in the change is a multiplier in the body; there is
no trace in the signature.

**Changing the return value's behavior.** Returning an empty value instead of an empty
list, a default instead of an error, unordered instead of ordered. The trace is in the
body; the calling side compiles and silently runs wrong.

**Changing the class of thrown error.** Catch blocks no longer match; the error escapes
uncaught and the calling side does not learn this at compile time.

**Removing or renaming a name.** This is the most visible form and is read directly
inside the change; who uses the removed name, however, is outside the change.

What the middle three forms share is this lesson's starting point: **the compiler is
silent, the tests pass, and review remains as the only defense** — because the tests too
were updated in the same change to match the new behavior.

#### The Trace Is Inside the Change, Its Consequence Is Outside

The distinguishing trait of an interface defect in review is this: **its trace is
small, its consequence sits outside the change.** What is visible in the submitted
change is a single line — a signature. The code that line breaks is not inside the
change; it is in other files, other repositories, or calls not yet written.

This explains why the interface axis requires a separate reading. The eye looking at the
implementation reads the function's body, and the body is correct; the eye looking at
the test reads the tests, and the tests pass, because the tests were also updated to
match the new signature. Seeing the trace requires comparing the signature against its
**previous form**, and the only reading that does this is the one from the interface
axis.

```text
# taught review-comment example, not executed

subject   signature of the public function
before    parse(text)
after     parse(text, mode)

note      Second parameter added without a default; a call made with the
          previous signature is no longer valid. This is a change that
          cannot be published without touching calling code. Two paths
          exist: give the mode parameter a default that reproduces the
          previous behavior, or keep the previous signature and offer the
          new behavior under a separate name.
```

The note points at a spot in the change, names the broken condition, and proposes two
closing paths. This block is an example and is not run; the lesson's numbers come only
from the measurement block.

#### Three Sources Available to Review

A reading working on the interface axis can look at three sources, and their scope
differs.

**The change itself** is the added and removed lines. The contract's new form is here,
and its previous form sits in the removed lines; comparing these two forms is the
interface axis's actual job.

**The change's title and description** carry the author's intent. The difference
between "added a mode parameter" and "added a mode parameter as required" determines
which question review asks. If this source is missing, the reading is forced to
interpret the signature on its own.

**The rest of the repository** contains calling code but is not part of the change. The
interface defect's consequence is here, and review is not required to look at this
source; doing so is a separate decision and generates a separate reading request.

The measurement does not model these three sources separately; if the interface axis is
open, the defect is found, if closed, it is not. This simplicity in the model exists to
keep what is measured in a single variable: **which axis is being looked at.**

#### The Measurement's Assumptions

- **RA9** — The shared setup is unchanged: 600 lines, 12 chunks, 24 defects, attention
  12 chunks. The entire change is read; there is no unread chunk in this lesson either.
- **RA10** — A defect belongs to a single class. Two classes of defect can be found on
  the same line, but these are two separate defects and are counted separately.
- **RA11** — The **intersection** of two axes is the set of defect numbers both find.
  Because classes exclude one another, this intersection is expected to come out empty;
  the measurement does not assume this, it counts it.
- **RA12** — The "only this axis" column is the number of defects an axis finds that
  none of the other four finds.
- **RA13** — In panels without an interface reviewer, no reviewer carries the interface
  axis; the remaining four axes are distributed freely and the number of people can be
  increased.
- **RA14** — In the "ten reviewers, no interface" panel, each of the four axes gets two
  people, plus two more people who carry all four together; ten readings are done in
  total.
- **RA15** — The measurement is still single-round. Finding an interface defect later,
  once calling code breaks, is outside this table and is not counted as a review
  finding.

#### Measurement

```python
"""Interface axis: does another axis find what one axis finds.

Part 1 - five single-axis readings; intersection and unique-to-this-axis.
Part 2 - configurations with and without the interface axis.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


FULL = set(AXES)
d = change(600)
single = {a: review(d, {a}) for a in AXES}

print(f"{'axis':<15s} {'found':>7s} {'also in other four':>19s} "
      f"{'only this axis':>15s}")
for a in AXES:
    other = set().union(*(single[o] for o in AXES if o != a))
    print(f"{a:<15s} {len(single[a]):7d} {len(single[a] & other):19d} "
          f"{len(single[a] - other):15d}")
union = set().union(*single.values())
intersection = sum(len(single[a] & single[b])
                    for a in AXES for b in AXES if a < b)
print(f"union of five single readings {len(union)}, "
      f"sum of pairwise intersections {intersection}")

print()
FOUR = FULL - {"interface"}
PANELS = {
    "one reviewer, five axes": [FULL],
    "one reviewer, no interface": [FOUR],
    "three reviewers, interface included": [{"interface"}, {"implementation"},
                                             {"test", "documentation"}],
    "three reviewers, interface replaced": [{"implementation"}, {"implementation"},
                                             {"test", "documentation"}],
    "ten reviewers, no interface": [{a} for a in sorted(FOUR)] * 2 + [FOUR] * 2,
}
print(f"{'panel':<36s} {'reviewers':>9s} {'found':>7s} {'missed':>6s} "
      f"{'interface missed':>17s} {'unwritten missed':>16s}")
for name, assignment in PANELS.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    iface = sum(1 for k in missed if k["class"] == "interface")
    unwritten = sum(1 for k in missed if k["class"] == UNWRITTEN)
    print(f"{name:<36s} {len(assignment):9d} {len(b):7d} {len(missed):6d} "
          f"{iface:17d} {unwritten:16d}")
```

```
axis              found  also in other four  only this axis
interface             3                   0               3
implementation        6                   0               6
test                  3                   0               3
documentation         3                   0               3
style                 7                   0               7
union of five single readings 22, sum of pairwise intersections 0

panel                                reviewers   found missed  interface missed unwritten missed
one reviewer, five axes                      1      22      2                 0                2
one reviewer, no interface                   1      19      5                 3                2
three reviewers, interface included          3      15      9                 0                2
three reviewers, interface replaced          3      12     12                 3                2
ten reviewers, no interface                 10      19      5                 3                2
```

#### The Shares Are Disjoint

The top table's middle column gives **0** in all five rows, and the sum of pairwise
intersections is also **0**. None of the five axes finds even a single defect another
axis finds.

The column on the right is the mirror image of this: **all** of the defects each axis
finds appear only in that axis — **3** in interface, **6** in implementation, **3** in
test, **3** in documentation, **7** in style. Their total is **22**, equal to the union
of the five single readings. So an axis's contribution does not depend on which axes are
already open.

Being disjoint does not mean two defects cannot sit in the same file or the same line
range; it means the **classes** are separate. A signature defect and a style defect can
both stand on the same line of the same function, and the setup counts these as two
separate defects. Two eyes reading the same text find separate things because they are
searching for separate things.

The practical counterpart is exact: **the number of defects lost when an axis is closed
equals the number of defects in that axis's class.** There is no leftover that
compensates for a closed axis. If the interface axis is closed, three defects are gone;
no amount of reading added to other axes brings those three back. The bottom table tests
this sentence.

#### Once the Interface Axis Is Closed

The bottom table's first two rows are the measurement's core. Same single reviewer, same
change, the only difference is whether the interface axis is open: found drops from
**22** to **19**, missed climbs from **2** to **5**, and the difference is exactly **3**
interface defects. The loss is exactly the size of the class.

The third and fourth rows repeat the same measurement on a three-person panel. With the
interface axis open, three reviewers find **15**; when a second implementation reading
replaces the person on the interface axis, that drops to **12**. Reviewer count did not
change, reading count did not change — the second implementation reading is even extra
effort. The result is **3** fewer defects. The return on looking at the same axis twice
is **0**, and this measurement makes that cost visible.

The last row is the lesson's sharpest result. **Ten reviewers** work without an interface
axis: two people on each of the four axes, plus two more people reading all four
together. Found is **19**. A **single** reviewer covering five axes finds **22**. A
ten-person panel finds **3** fewer defects than a one-person panel — and the difference
comes from the single axis none of the ten looked at.

The fourth row's configuration is not made up; when a defect is missed in a review, the
first decision made is often **adding one more reviewer**. If the added person is not
told which axis to look at, they look at the most familiar one — implementation. The
table gives the numeric result of that decision: the panel grew, reading count went up,
found dropped from **15** to **12**. The drop is not because the new person broke
something; it is because the reading they replaced was on **a different axis**.

The difference is **3/24**, that is, **0.125**; given the set's resolution is **1/24 =
0.042**, this is three times that resolution and sits comfortably inside the measurement
band. The number's smallness can be misleading: each of the three missed defects is a
contract break that breaks code outside the change, and it becomes visible only where
review could not find it — once the calling side breaks.

There is a cheap way to open the axis that does not show up in the table: the interface
axis can be opened without rereading the entire change. The lines carrying public
declarations are a small part of the change, and a reading that looks at them first sees
the interface class's three defects before the others. Opening an axis is not always
adding a person to the panel; sometimes it is only changing the reading order. The setup
does not measure this distinction — an axis is either open or closed — but what the
table says holds for both: **there is no number of people that compensates for a closed
axis.**

#### The Unwritten Side of the Contract

The interface axis compares the **written** form of the contract. The signature, the
return type, and the error class are text, and text can be read. The contract also has
an unwritten side: behaviors the calling side relies on but that are stated nowhere —
whether repeating the call is safe, whether the returned list's order is stable, whether
the same input always gets the same response.

When these behaviors break, the interface axis finds no trace, because there is no
previous text to compare against. Its counterpart in the setup is direct: such a defect
falls not into the interface class but into the sixth class — **unwritten
requirement** — and keeps being missed even with the interface axis open. This is why
the bottom table's last column gives **2** in all five rows.

The distinction comes down to a single question: **is what broke written down
somewhere?** If it is written, it is the interface axis's job and is found once the axis
is opened. If it is not written, it is no axis's job, and it gets closed outside review,
where the contract gets written.

#### Summary

- A public interface is the set of externally callable names and their behaviors; its
  contract can break in five separate ways, and three of them run wrong without breaking
  calling code.
- An interface defect's trace inside the change is a single line, and its consequence
  sits outside the change; this is why it requires comparing the signature against its
  previous form.
- The sets the five axes find are disjoint: the sum of pairwise intersections is **0**,
  everything each axis finds appears only in that axis, and the union of the five is
  **22**.
- The defect lost when an axis is closed equals the size of that class: closing the
  interface axis drops found from **22** to **19** and raises missed from **2** to
  **5**.
- When a second implementation reading replaces the person on the interface axis, a
  three-person panel finds **12** instead of **15**; a **ten**-reviewer panel without an
  interface axis stays under the **22** a single reviewer covering five axes finds, at
  **19**.
- When the unwritten side of the contract breaks, the interface axis has no text to
  compare; this defect falls into the sixth class and is missed even with the axis open.

#### Next Step

The largest single share in the table belongs to the **implementation** axis: **6**
defects, **0.250** of the 24. The interface axis's finding came from comparing the
signature; the implementation axis's finding has no previous form to compare against —
what is looked at is the gap between what the code should do and what it does. The next
lesson measures this axis: as axes are opened one at a time, what curve does the found
count grow along, where does the implementation axis's own share sit on that curve, and
how much does being the single biggest finder make it, on its own, sufficient.

### Meaning of Implementation

Source: https://academia.sh/en/courses/code-review/meaning-of-implementation

An axis's marginal contribution is independent of which axes are already open: implementation adds 6 defects in both orders, and whether one, three, or twelve people look at the same axis does not change that 6.

The previous lesson measured that the sets the five axes find are disjoint: the sum of
pairwise intersections is **0**, and everything each axis finds appears only in that
axis. A result follows directly from this but has not yet been tested — if the shares
are disjoint, an axis's contribution must be **independent of which axes are already
open.**

This lesson tests that result, and it tests it on the axis holding the largest single
share, the **implementation** axis. The measurement asks two questions: does the curve
change when axes are opened in different orders, and does more people looking at the
same axis grow that axis's share?

#### What the Implementation Axis Asks

The **implementation** axis searches for the gap between what the code **should do**
and what it **does**. This is where it separates from the interface axis: on the
interface there were two texts to compare — the signature's previous and later form. On
implementation, one side of the comparison is not text. On one side sits what the body
actually does; on the other, what the reader thinks that body should do.

The axis splits into three sub-questions.

**Correctness.** Does the body do what its name and description say. This question
produces the most visible defects, because the code itself is the evidence: a wrong
comparison direction, an inverted condition, the wrong variable returned.

**Edge case.** Is the body correct outside the usual input too. Empty collection,
single-element collection, the boundary value itself, division by zero, overflow,
concurrent access. The edge-case concept was established in the Software Quality and
Testing curriculum's testing lessons; its review side is measured here.

**Complexity.** The body's branch count, nesting depth, and the responsibility it
gathers in one place. Naming, function length, and complexity metrics themselves were
established in the Clean Code course and are not repeated here. In this lesson these are
not a **rule list**; they are a **review finding** — something review sees, notes, and
asks to be closed. The distinction matters: a rule list applies to a change, a finding
points at a specific spot in it.

```text
# taught review-comment example, not executed

correctness    The last element is left out when computing the total; the
               upper bound should be included. Expected value for the same
               input is 15, returned is 10.

edge case      The first element is read when the list is empty. An early
               return for empty input, or explicit handling of the empty
               value, is needed.

complexity     This body has four nested conditions and the third never
               runs outside the first two. Pulling the condition into a
               separate helper function reduces branching to two.
```

The three notes share a common form: **where, what, under what condition.** All three
point at a spot in the change and name the closing condition. This block is an example
and is not run; the lesson's numbers come only from the measurement block.

#### Where Correctness's Oracle Lives

The interface axis had two texts to compare; the implementation axis has no such
counterpart. The reader has to derive what the code **should do** from somewhere, and
that place may not be inside the change.

Three sources are available. **The change's description** describes intent; when written
briefly, the reader tries to infer intent from the body itself, and that is circular —
whether the body is correct cannot be settled by looking at the body again. **Tests**
give the expected behavior by example; but if the tests were written in the same change,
they may share the same misunderstanding as the body. **Documentation** gives behavior
in words, and if it was not updated along with the change, it describes the old
behavior.

That all three sources can be flawed is clear, and this is no coincidence: in the same
change, all three come from the same hand. This is why the implementation axis is
expensive — the reader has to hold an independent judgment of what the code should do.

This has no counterpart in the measurement, and that absence is deliberate. The setup
counts every axis at equal cost: if the axis is open, its class is found. In a real
review, the implementation axis is the most expensive per defect and the style axis the
cheapest. The table counts defects found, not attention spent; **the fact that the two
numbers are separate stays open until this course's last lesson.**

#### Does Contribution Depend on Order

An axis's **marginal contribution** is how much the found count rises when that axis is
opened. If shares are disjoint, this rise should always equal the size of the class, and
the order of opening should change nothing.

This needs to be tested, because intuition says the opposite. In a real review, axes
really are opened in sequence: the reading starts somewhere and the reader's eye shifts
as it goes. Intuition says the axis opened first will collect the "easy" defects and
less will be left for the ones after. The measurement tests this intuition with two
separate orders: one starting from implementation, the other from style.

#### The Measurement's Assumptions

- **RA16** — The shared setup is unchanged: 600 lines, 12 chunks, 24 defects, attention
  12 chunks. There is no unread chunk.
- **RA17** — The curve is a single reviewer's axis set growing step by step. The same
  change is reread at each step; the reading budget is not exhausted by the step count.
- **RA18** — Marginal contribution is that step's cumulative found minus the previous
  step's.
- **RA19** — The two orders contain the same five axes, and only their opening order
  differs; order B is the reverse of order A.
- **RA20** — In the second part, reviewers looking at the same axis are identical to one
  another; the setup includes no share where a second reading catches what the first
  missed. This simplification is deliberate and ties the measurement's result to a
  single variable.
- **RA21** — The "reads" column is the number of reviews performed; one person reading
  twice and two people reading once each are not distinguished in this setup.
- **RA22** — The ratio is found divided by 24. The set's resolution is **1/24 =
  0.042**.

#### Measurement

```python
"""Implementation axis: the axis-adding curve and adding people to the same axis.

Part 1 - axes opened in two separate orders: cumulative found and marginal.
Part 2 - implementation axis open, closed, and with many readings.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def curve(d, order):
    """Axes are opened one at a time; cumulative and marginal at each step."""
    open_axes, rows, previous = set(), [], 0
    for a in order:
        open_axes.add(a)
        n = len(review(d, open_axes))
        rows.append((a, n, n - previous))
        previous = n
    return rows


FULL = set(AXES)
d = change(600)
A = curve(d, ("implementation", "interface", "test", "documentation", "style"))
B = curve(d, ("style", "documentation", "test", "interface", "implementation"))

print(f"{'step':>4s} {'order A':<15s} {'total':>6s} {'marginal':>8s}  "
      f"{'order B':<15s} {'total':>6s} {'marginal':>8s}")
for i, (a, b) in enumerate(zip(A, B), start=1):
    print(f"{i:4d} {a[0]:<15s} {a[1]:6d} {a[2]:8d}  "
          f"{b[0]:<15s} {b[1]:6d} {b[2]:8d}")

print()
FOUR = FULL - {"implementation"}
PANELS = {
    "one reviewer, implementation": [{"implementation"}],
    "three reviewers, implementation": [{"implementation"}] * 3,
    "twelve reviewers, implementation": [{"implementation"}] * 12,
    "one reviewer, four axes": [FOUR],
    "one reviewer, five axes": [FULL],
}
print(f"{'panel':<33s} {'reads':>5s} {'found':>7s} {'missed':>6s} "
      f"{'implementation missed':>22s} {'ratio':>5s}")
for name, assignment in PANELS.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    im = sum(1 for k in missed if k["class"] == "implementation")
    print(f"{name:<33s} {len(assignment):5d} {len(b):7d} {len(missed):6d} "
          f"{im:22d} {len(b) / 24:5.3f}")
```

```
step order A          total marginal  order B          total marginal
   1 implementation       6        6  style                7        7
   2 interface            9        3  documentation       10        3
   3 test                12        3  test                13        3
   4 documentation       15        3  interface           16        3
   5 style               22        7  implementation      22        6

panel                             reads   found missed  implementation missed ratio
one reviewer, implementation          1       6     18                      0 0.250
three reviewers, implementation       3       6     18                      0 0.250
twelve reviewers, implementation     12       6     18                      0 0.250
one reviewer, four axes               1      16      8                      6 0.667
one reviewer, five axes               1      22      2                      0 0.917
```

#### The Curve Is Independent of Order

The top table's two halves open the same five axes, one starting from implementation,
the other from style, and both curves end at **22**.

The marginal columns are the measurement's real result. Implementation is the **first**
axis opened in order A and contributes **6**; in order B it is the **last** axis opened
and still contributes **6**. Style adds **7** first in B, and **7** last in A. Interface,
test, and documentation add **3** in both orders.

An axis's contribution does not depend on how many axes were opened before it. The
intuition that "easy defects get collected first, less is left for later" turns out
wrong in this setup, and the reason was measured in the previous lesson: because shares
are disjoint, there is no shared pool being drawn down. Every axis brings its own class,
and that class cannot already have been collected by any other axis.

Independence has a limit too, and it is a known one. In this lesson attention is not
exhausted, because the change is exactly the size of the attention budget. The only case
where reading order would matter is when the reading gets cut off before finishing:
then, whichever axis was left for later directly becomes **an axis never looked at at
all.** There is no such cutoff in this topic's measurements.

This has a direct use. A panel's found count can be computed in advance — without
knowing who looked first or which file the reading started from — as **the sum of the
class sizes of the open axes.** Order is not a plan variable; **coverage** is the only
variable that is.

#### Adding People to the Same Axis

The bottom table's first three rows give a single number three times: **6**.

One reviewer looks at implementation and finds **6**. Three reviewers look at the same
axis, still **6**. Twelve reviewers look at the same axis, still **6**. Reading count
grew from one to twelve, and defects found grew by **0**. The ratio sits at **0.250** in
all three rows.

This is the sharpest form of the course's second claim: **adding reviewers does not
find defects.** "Does not" here is not a figure of speech, it is the measured number —
eleven extra readings return zero. The setup achieves this with a simplification: a
second reading on the same axis sees what the first one saw. In a real review, a second
reading may add a small extra share, but that share's size never matches what
**opening a new axis** contributes, because the second reading by definition searches
within the same class.

This does not mean the second reading serves no purpose; it means it serves a purpose
**other than finding.** The three jobs from the first lesson separate here: a second
reading on the same axis does not find defects but **spreads knowledge** — it becomes
that code's second reader, and the number of people able to look at the implementation
axis on later changes grows. A newcomer reading the same axis an experienced person
looks at falls into the same class; it is a learning decision.

The distinction sharpens here: **if a second reading is a finding decision, it is
measurable and its return is 0; if it is a knowledge-spreading decision, this table does
not measure it.** When the two are swapped for one another, the result comes out wrong.
The "let one more person look" decision made after a defect is missed is, by what the
number says, uncompensated on the finding side. The decision that does pay off is
opening an axis that was not being looked at.

The last two rows give the other end of the same measurement. When the implementation
axis is closed, a single reviewer covering the remaining four axes finds **16** defects,
and **6** of the **8** missed are in the implementation class. When the axis is reopened,
found climbs to **22**: the ratio from **0.667** to **0.917**. Opening a single axis
returns **6** defects; eleven extra readings return **0**.

#### The Separate Weight of the Three Sub-Questions

The measurement gives the implementation class as a single number — **6** — and does
not separate its three sub-questions. This is a setup decision; but the separation has a
practical consequence that falls outside the table.

A **correctness** defect has a proof: an input that produces the wrong result can be
shown. The finding is not open to debate, and it closes with a single fix.

An **edge case** defect's proof is also an input, but whether that input actually
occurs can be debated. The finding therefore sometimes closes not with a fix but with a
decision: recording that the case is out of scope.

A **complexity** defect has no input that can be shown. The finding points not at
today's error but at the cost of later changes, and closing it requires a shared
criterion. The criterion itself is the Clean Code course's subject; what is left for
review is tying the finding to a specific spot in the change and writing down what
counts as closed.

All three are counted in the same class because all three are found by the same axis's
reading. Which round they close in is a separate variable, and it does not enter this
topic's measurements.

#### Summary

- The implementation axis searches for the gap between what the code should do and what
  it does, and splits into three sub-questions: correctness, edge case, complexity.
- Complexity here is not a rule list but a **review finding**: it points at a specific
  spot in the change and names the closing condition.
- An axis's marginal contribution is independent of opening order: implementation gives
  **6** in both orders, style gives **7** in both orders, the remaining three axes add
  **3** each, and both curves end at **22**.
- A panel's found count is the sum of the class sizes of its open axes; reading order is
  not a plan variable.
- One, three, and twelve readings on the same axis all find **6** defects; the ratio
  stays fixed at **0.250**. Eleven extra readings return **0**, a single new axis
  returns **6** defects.
- A second reading on the same axis pays off not in finding but in spreading knowledge;
  when the two are swapped for each other, the "let one more person look" decision goes
  uncompensated on the finding side.

#### Next Step

Two axes have been measured so far, and in both, the reason a defect was missed was the
same: no one was looking at that axis. But there can be a second reason for a miss —
the text the defect sits in was never read at all. There was no such escape in this
lesson, because the change was exactly the size of the attention budget. The next
lesson separates these two on the **test** axis: do the defects the test itself carries
sit inside chunks that were read, how many times do reviewers reading the same chunk
from other axes pass over that defect, and why do they not see it despite passing over
it.

### Reviewing Tests

Source: https://academia.sh/en/courses/code-review/reviewing-tests

All three test defects sit in chunks that were read, and reviewers giving findings on other axes are present in those chunks; missed-in-unread-chunk is 0 on all five axes, and found drops from 22 to 19 once the test axis is closed.

The previous lesson assumed that in this setup a defect is missed for a single reason:
no one looking at that axis. But there can be a second reason for a miss — the text
the defect sits in was never read at all. The two reasons give the same result and show
up in the same column of the table; yet the response called for is not the same.

This lesson measures that distinction, and it measures it on the **test** axis. The test
axis is not chosen at random: tests sit in the same files as the change's body, or in
files adjacent to them, meaning they pass in front of the eyes of reviewers looking at
other axes too. Its question is this: can a defect, sitting inside text that has been
read, be missed in front of the eyes of multiple people who read that very text?

#### The Test Itself Is Also Reviewed

Test levels, the test pyramid, coverage metrics, and test-writing techniques were
established in the Software Quality and Testing curriculum and are not repeated here.
This lesson's subject is not how to write a test, but **what is found when the test
itself is looked at in review.**

The distinction sits here. A change's tests occupy two separate roles. In the first
role, a test is **evidence**: it shows the body is correct, and the reading looking at
the implementation axis relies on it. In the second role, a test is a **review object**:
it is itself written code and carries its own defects.

When the second role is skipped, the first collapses. A reading that decides on the
body's correctness by looking at whether the tests pass, without checking what the
tests actually verify, has looked at nothing at all. The test axis is exactly this
check.

#### Three Defects a Test Can Carry

**Coverage.** The new path the change introduces has not been tested. Tests exist, they
pass, and their count may even have grown; but no test touches the changed behavior
itself. This defect's trace visible in review is the absence of an assertion
corresponding to a branch in the changed body.

**Meaningfulness.** The test runs the new path but verifies nothing. A test that checks
only that the call does not throw passes no matter what the body returns. The same
class also includes tests whose expected value is computed with the same expression the
body uses: an assertion producing the expected value from the body's own expression is
confirmed even if the body is wrong.

**Fragility.** The test is bound not to behavior but to an implementation detail: call
order, an internal structure's field name, a log line's exact text. Such a test breaks
without the behavior changing, and when it breaks, what needs fixing is not the body,
it is the test.

The three are not equally visible in review. A coverage defect is a countable gap,
surfacing once the body's branches and the tests' state are placed side by side. A
meaningfulness defect requires reading the test's text and escapes a reading that looks
at test **count**, because a test verifying nothing is still a test and still counts.
Fragility is the hardest of the three: the test passes today, and its cost is paid in a
later, unrelated change that it stops for no real reason.

```text
# taught review-comment example, not executed

coverage       Change introduces two new branches; tests call only the
               first branch. A case needs to be added for the second
               branch.

meaningfulness Test's only assertion is that the call does not throw. This
               test passes no matter what value the body returns; the
               returned value needs to be verified.

fragility      Test compares the exact text of a log line. The test will
               break when the log format changes even if behavior stays
               the same; the comparison can be tied to the outcome
               instead.
```

All three notes point at a spot in the test and say what needs to be added for it to
close. This block is an example and is not run; the lesson's numbers come only from the
measurement block.

#### The Test Comes From the Same Hand as the Change

There is a structural reason the test axis requires a separate reading, and it ties back
to the previous lesson's question: the reading looking at the implementation axis had to
derive what the body should do from somewhere, and one available source was the tests.

The problem is that tests are part of the same change. The body and the test are written
at the same time, with the same understanding. If the body rests on a misunderstanding,
the test records the same misunderstanding and confirms the body. In that case, the test
passing is not evidence, it is an echo.

The consequence for review is direct: **the test's correctness has to be checked
independently of the body's correctness.** The test axis is the only reading that does
this, and what it looks at is not whether the test passes but **what the test claims**.
An assertion repeating the same expression as the body, never calling one of the body's
branches, or comparing no result at all — these are seen by reading the test, not by
running it.

The measurement does not model this dependency — defects in the test class are
independent of the ones in the implementation class. The model's simplicity favors the
measurement here: had the dependency been added, the cost of closing the test axis would
have come out larger than what shows in the table.

#### Missing Inside Text That Was Read

In the setup, finding a defect depends on two conditions: the defect's class has to be
on the reviewer's axis, and the chunk the defect sits in has to be among the chunks the
reviewer read. If either condition fails, the defect is missed.

At this topic's scale, the second condition always holds: the change is 12 chunks and
attention is also 12 chunks. So in this lesson **no defect is missed because its text
was unread.** The measurement counts this instead of assuming it, and splits the missed
into two: missed inside a read chunk, and missed inside an unread chunk.

The second column coming out zero shows that the entirety of the first column is
**axis miss**. This is a direct answer to the lesson's question: the missed defect does
not sit somewhere no one looked, it sits somewhere everyone read.

#### The Measurement's Assumptions

- **RA23** — The shared setup is unchanged: 600 lines, 12 chunks, 24 defects, attention
  12 chunks.
- **RA24** — A chunk is 50 lines and the unit of reading is the chunk; a reviewer who
  reads a chunk is considered to have seen all of it.
- **RA25** — The "other class in the same chunk" column gives the class names of
  defects found in that chunk that belong to a non-test axis.
- **RA26** — The "other axis flagging in chunk" count is the number of non-test axes
  that have a defect of their own class in that chunk. A reviewer on that axis has, by
  the setup's certainty, made a note in that chunk — the chunk has been read.
- **RA27** — A defect missed-and-read is one whose chunk was read but whose class is
  not on the reviewer's axis. A defect missed-and-unread is one whose chunk was never
  read.
- **RA28** — Every row in the second part is a single single-axis reading; the missed
  columns are relative to that one reading.
- **RA29** — Whether the test passes or fails is not in the measurement; what is
  measured is the defect the test carries **as seen in review**, not the result it gives
  when run.
- **RA30** — Resolution at the chunk level is **1/12 = 0.083**, at the defect level
  **1/24 = 0.042**.

#### Measurement

```python
"""Reviewing tests: defects missed inside chunks that were read.

Part 1 - the chunks test defects sit in and the other classes in that chunk.
Part 2 - for each single-axis reading, the split between missed-and-read and missed-and-unread.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


FULL = set(AXES)
d = change(600)
test_defects = [k for k in d["defects"] if k["class"] == "test"]

print(f"{'defect':>6s} {'chunk':>5s} {'other class in same chunk':<28s} "
      f"{'other axis flagging in chunk':>30s}")
for k in test_defects:
    neighbors = sorted({o["class"] for o in d["defects"]
                        if o["chunk"] == k["chunk"] and o["class"] in AXES
                        and o["class"] != "test"})
    print(f"{k['no']:6d} {k['chunk']:5d} {', '.join(neighbors):<28s} "
          f"{len(neighbors):30d}")
occupied = {k["chunk"] for k in d["defects"]}
mixed = sum(1 for o in range(d["chunks"])
            if len({k["class"] for k in d["defects"] if k["chunk"] == o}) > 1)
print(f"{d['chunks']} chunks, {len(occupied)} have at least one defect; "
      f"{mixed} chunks have defects from more than one class")

print()
print(f"{'axis':<15s} {'chunks read':>11s} {'found':>7s} "
      f"{'missed, read':>13s} {'missed, unread':>15s}")
for a in AXES:
    b = review(d, {a})
    read = set(range(min(ATTENTION, d["chunks"])))
    missed = [k for k in d["defects"] if k["no"] not in b]
    m_read = sum(1 for k in missed if k["chunk"] in read)
    m_unread = sum(1 for k in missed if k["chunk"] not in read)
    print(f"{a:<15s} {len(read):11d} {len(b):7d} {m_read:13d} {m_unread:15d}")

print()
FOUR = FULL - {"test"}
for name, axes in (("five axes", FULL), ("four axes, no test", FOUR)):
    b = review(d, axes)
    missed = [k for k in d["defects"] if k["no"] not in b]
    tm = sum(1 for k in missed if k["class"] == "test")
    print(f"{name:<20s} found {len(b):2d}  missed {len(missed):2d}  "
          f"test missed {tm}  unwritten missed "
          f"{sum(1 for k in missed if k['class'] == UNWRITTEN)}")
```

```
defect chunk other class in same chunk      other axis flagging in chunk
     3     9 implementation                                            1
     7     1 interface, style                                          2
    19     7 interface                                                 1
12 chunks, 11 have at least one defect; 7 chunks have defects from more than one class

axis            chunks read   found  missed, read  missed, unread
interface                12       3            21               0
implementation           12       6            18               0
test                     12       3            21               0
documentation            12       3            21               0
style                    12       7            17               0

five axes            found 22  missed  2  test missed 0  unwritten missed 2
four axes, no test   found 19  missed  5  test missed 3  unwritten missed 2
```

#### Same Chunk, Separate Axis

The top table shows the three test defects one by one, and none of the three stands
alone.

**Defect number 3** is in the ninth chunk, and that same chunk has an implementation
defect. **Defect number 7** is in the first chunk, where both an interface and a style
defect sit. **Defect number 19** is in the seventh chunk, next to an interface defect.

The column on the right gives what this means: **1, 2, 1.** These are the number of
axes that **found** a defect of their own class in that chunk. That is, in the first
chunk, two separate reviewers made a note; both read those same fifty lines; and both
missed the test defect inside that same fifty lines. The same holds once each for the
ninth and seventh chunks.

The three defects sit in three separate chunks; at the chunk level this is **3/12**,
that is, **0.250**. The chunks holding test defects correspond to a quarter of the
change, and none of these chunks went unread. Given the smallest measurable difference
at the chunk level is **1/12 = 0.083**, this share sits comfortably inside the
measurement band.

The bottom row gives the setup's density: **11 of 12 chunks** have at least one defect,
and **7 chunks** have defects from more than one class. The change is not crammed into a
narrow spot; the classes sit mixed together. In nearly every chunk a reviewer reads, the
chance is high that something they did not find sits next to something they did.

#### The Single Reason for a Miss

The middle table turns this lesson's distinction into a number. In all five rows,
**chunks read is 12** and **missed, unread is 0**.

The zero column says this: at this scale, no defect is missed because its text was
unread. Every missed defect — **21** in interface, **18** in implementation, **21** in
test, **21** in documentation, **17** in style — sits inside a chunk that was read.
Each has a single cause: that class is not on the reviewer's axis.

This names review's most deceptive trait. A reviewer can say they read the **entirety**
of the change, and they are telling the truth; the reading really was done in full. But
"I read all of it" does not mean "I looked at all of it." Reading belongs to coverage,
looking belongs to an axis, and the table shows the two are separate.

This has a consequence for review's record. When a review closes by saying "I found
nothing," the information left behind is incomplete: which axes were looked at is not
written down. Someone looking at the same change later cannot know which classes a
closed review covered, and by the table, this is as decisive as the list of defects
found.

What the record needs to carry is therefore not only the list of findings but **the list
of axes looked at**. A note such as "read on interface and implementation, not on test
and documentation" does not change the found count; it tells in advance which classes
the missed defects will be in. This note is about the change and records review's scope,
not the reviewer's success.

The bottom two rows give the size of the loss. A reading covering five axes finds
**22**; once the test axis is closed, **19**. Missed climbs from **2** to **5**, and the
difference is exactly **3** test defects. The test axis's share, in keeping with the
rule measured in the previous lesson, equals the size of its class.

The practical weight of these three defects being missed is, in one sense, heavier than
the others. A missed test defect does not stay only itself; it also breaks the test's
**evidence** role. A test suite with a coverage gap gives a false assurance to later
readings working on the implementation axis — the tests are passing, but what they are
passing is not the changed behavior.

#### Summary

- A test carries two roles in a change: **evidence** for the body's correctness, and a
  review **object** in its own right. When the second role is skipped, the first is left
  unsupported.
- The defects a test can carry are three kinds: **coverage** (the new path is
  untested), **meaningfulness** (the assertion verifies nothing), **fragility** (the
  test is bound to an implementation detail).
- The three test defects sit in chunks 9, 1, and 7, and other axes gave findings in
  those chunks — **1, 2, 1** respectively; the reviewers who read those chunks passed
  over the test defect.
- All five axes give **missed-in-unread-chunk 0**: at this scale, every missed defect's
  single cause is an axis not looked at. "I read all of it" does not mean "I looked at
  all of it."
- Once the test axis is closed, found drops from **22** to **19** and missed climbs
  from **2** to **5**; the three missed defects also weaken the test's evidence role.
- Because a test is written at the same time and with the same understanding as the
  body, a passing test is not independent evidence; the test axis looks at what the test
  claims, not whether it passes.

#### Next Step

Three axes have been measured so far, and all three had code as their object: signature,
body, test. The fourth axis's object sits outside the code — the change's reflection in
**documentation**. When this axis is closed too, the loss is expected to equal the size
of its class; but the documentation axis has a distinct side. A written document being
wrong and a behavior never having been documented at all are not the same thing, and the
two fall into separate classes in the setup. The next lesson closes the five axes one at
a time to measure the loss, and separates what the documentation axis can find from what
it cannot.

### Documentation Review

Source: https://academia.sh/en/courses/code-review/documentation-review

Closing the five axes one at a time always loses the size of the closed class — 3, 6, 3, 3, and 7 defects; closing the documentation axis drops found from 22 to 19, and two unwritten-requirement defects sitting in the same chunk keep being missed even with the axis open.

The previous three lessons measured three axes, and all three had code as their object:
signature, body, test. The fourth axis's object sits outside the code. When a change
alters a behavior, the text **describing** that behavior has to change too; if it does
not, what is left is a text that contradicts what the code says.

This lesson measures that axis, and it widens the measurement by one step: the five
axes are closed one at a time, and each closure's loss is placed side by side. Then the
documentation axis's own particular limit is measured — the difference between a
written document being wrong and a behavior never having been written down at all.

#### The Change's Reflection in Documentation

The kinds of text the documentation axis looks at are separate, and not all of them are
found in the same change. Comment lines next to the body, the public interface's
reference text, the repository's introduction file and setup instructions, sample code
snippets — each rests on a behavior, and becomes invalid once that behavior changes.

The distinction between document types, reader definitions, and writing discipline are
the subject of the Technical Writing and Documentation course and are not repeated here.
The only thing this lesson measures is **whether documentation is looked at in review,
and what is missed when it is not.**

The documentation axis's distinguishing trait is that its finding is discovered through
a contradiction. The other axes look at a single text: interface looks at the signature,
implementation at the body, test at the assertion. The documentation axis compares
**two texts** — what the code does and what the text says it does. This is why a
documentation defect cannot be found by looking only at the documentation file; reading
the documentation correctly requires having read the code too.

This need to compare makes the documentation axis look expensive, though it can be made
cheaper. What the code does has already been read on the interface and implementation
axes; the documentation axis lays the second text on top of that same reading. Carrying
two axes together in the same reading is the practical counterpart of the
additivity measured in the third lesson — axes do not eat into one another's share and
can be run together.

#### Three Findings of the Documentation Axis

**Stale text.** The behavior has changed, the sentence describing it has not. The text
is grammatically flawless and was once true; today it is wrong. Of the three, this is
the one most often missed, because the text itself is **invisible** inside the change —
since it did not change, it does not appear in the change.

**Missing text.** The change introduces a new public name or a new configuration
option, but no text describing it was added. This finding's trace is inside the change:
the added name is visible, the description that should have been added alongside it is
not.

**Contradicting text.** The same behavior is described in two separate places, and one
was updated. Whichever the reader looks at, they believe it; only someone reading both
at once sees that they contradict.

```text
# taught review-comment example, not executed

stale          Function's description line says "returns empty list"; the
               body now returns an empty value. Description needs to be
               updated to match the new behavior.

missing        New configuration option was added, no counterpart in the
               setup instructions. Option needs to be added to the
               instructions along with its default value.

contradicting  Example in the introduction file uses the old call form,
               the reference text describes the new form. One of the two
               texts needs to be updated to match the other.
```

This block is an example and is not run; the lesson's numbers come only from the
measurement block.

#### Why the Documentation Axis Closes

Of the five axes, documentation is the one that closes most often. The reasons are
outside the setup, but they change how the table should be read.

**Documentation is left for last.** The change's body and tests are ready,
documentation "will be written after merge." In this case review has not closed the
axis, it has postponed it; for the measurement, the two are the same, because no
documentation defect is found in that round.

**A documentation finding is not treated as a blocker.** An implementation finding
stops a merge; a documentation finding often does not — the finding is written and the
change merges anyway. The table does not carry this distinction: a defect found is
found, whether it closes is the next topics' variable.

**Documentation sits outside the change's scope.** The comment line next to the body is
inside the change; the introduction file, the setup instructions, or the reference text
may sit somewhere else, even in a different repository. Text outside the scope never
reaches review at all, and no finding is produced even with the axis open.

All three are manageable decisions, and the number the table gives is the price of
those decisions: closing the documentation axis means **3** defects are missed that
round. What is useful is not the size of the number but knowing in advance **which
class** the missed defects will belong to.

#### The Measurement's Assumptions

- **RA31** — The shared setup is unchanged: 600 lines, 12 chunks, 24 defects, attention
  12 chunks. There is no unread chunk; every missed defect's cause is an axis not
  looked at.
- **RA32** — In the first part, every row is a single reviewer's reading, and the only
  difference between rows is the closed axis. The first row closes no axis.
- **RA33** — The "loss" column is the found count with all five axes open minus that
  row's found count.
- **RA34** — The class breakdown of missed is the count, by class, of defects not found
  in that row, written in descending order.
- **RA35** — A documentation defect is a **written** text in the change contradicting
  the code. The setup keeps not where this text sits, but which chunk it falls into.
- **RA36** — An unwritten-requirement defect is the defect of what was never written at
  all; falling into a chunk does not make it visible — the chunk only marks where the
  defect belongs.
- **RA37** — When the documentation axis is open, only the documentation class is
  found; the sixth class is on no axis's search list, and this measurement does not
  change that.
- **RA38** — Resolution at the defect level is **1/24 = 0.042**, at the chunk level
  **1/12 = 0.083**.

#### Measurement

```python
"""Documentation review: what escapes when an axis is closed.

Part 1 - the five axes closed one at a time; found, missed, and the class breakdown of missed.
Part 2 - the split between a written document's defect and a never-written requirement.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


FULL = set(AXES)
d = change(600)

print(f"{'closed axis':<16s} {'open':>4s} {'found':>7s} {'missed':>6s} "
      f"{'loss':>5s} {'missed class breakdown':<40s}")
full = review(d, FULL)
for a in ("-",) + AXES:
    open_axes = FULL if a == "-" else FULL - {a}
    b = review(d, open_axes)
    missed = [k for k in d["defects"] if k["no"] not in b]
    breakdown = {}
    for k in missed:
        breakdown[k["class"]] = breakdown.get(k["class"], 0) + 1
    text = ", ".join(f"{s} {n}" for s, n in
                      sorted(breakdown.items(), key=lambda x: -x[1]))
    print(f"{a:<16s} {len(open_axes):4d} {len(b):7d} {len(missed):6d} "
          f"{len(full) - len(b):5d} {text:<40s}")

print()
documentation = [k for k in d["defects"] if k["class"] == "documentation"]
unwritten = [k for k in d["defects"] if k["class"] == UNWRITTEN]
print(f"{'class':<20s} {'defects':>7s} {'chunks':<14s} {'found by documentation axis':>28s}")
print(f"{'documentation':<20s} {len(documentation):7d} "
      f"{str(sorted(k['chunk'] for k in documentation)):<14s} {'yes':>28s}")
print(f"{UNWRITTEN:<20s} {len(unwritten):7d} "
      f"{str(sorted(k['chunk'] for k in unwritten)):<14s} {'no':>28s}")
for name, open_axes in (("documentation open", FULL), ("documentation closed",
                                                        FULL - {"documentation"})):
    b = review(d, open_axes)
    missed = [k for k in d["defects"] if k["no"] not in b]
    print(f"{name:<22s} documentation missed "
          f"{sum(1 for k in missed if k['class'] == 'documentation')}, "
          f"unwritten missed "
          f"{sum(1 for k in missed if k['class'] == UNWRITTEN)}")
```

```
closed axis      open   found missed  loss missed class breakdown                  
-                   5      22      2     0 unwritten requirement 2                 
interface           4      19      5     3 interface 3, unwritten requirement 2    
implementation      4      16      8     6 implementation 6, unwritten requirement 2
test                4      19      5     3 test 3, unwritten requirement 2         
documentation       4      19      5     3 documentation 3, unwritten requirement 2
style               4      15      9     7 style 7, unwritten requirement 2        

class                defects chunks          found by documentation axis
documentation              3 [2, 2, 3]                               yes
unwritten requirement       2 [2, 10]                                  no
documentation open     documentation missed 0, unwritten missed 2
documentation closed   documentation missed 3, unwritten missed 2
```

#### Closing the Five Axes One at a Time

The top table is this topic's closing table: same change, same single reviewer, the
only difference is which axis is closed.

The **loss** column confirms a single rule in all five rows: **3, 6, 3, 3, 7.** These
numbers are the class distribution itself. The number of defects lost when an axis is
closed equals the number of defects in that axis's class — neither more nor less. The
third lesson's order-independence measurement showed this going forward; this table
shows it going backward.

The size of the loss varies threefold from axis to axis. The most expensive closure is
the **style** axis: **7** defects, found drops to **15**. Next comes
**implementation**: **6** defects, found **16**. The interface, test, and documentation
axes each cost **3** defects, and all three leave found sitting at **19**.

The documentation axis looks like one of the cheapest closures in this table, and that
appearance is the lesson's warning. The table counts **how many defects are missed**; it
does not count **when the cost of a missed defect is paid.** An implementation defect
is paid once, when run. A documentation defect is paid every time it is read, and every
reader gets the same wrong information. Two numbers land in the same column but are paid
at two separate times. This distinction is outside the setup and cannot be extracted
from the table; the table only gives found and missed counts.

One row of the table overlaps exactly with the first lesson's table. When the style
axis is closed, found is **15**; in the first lesson, the "three reviewers, separate
axes" configuration also found **15**, and that panel's covered axes were interface,
implementation, test, and documentation — the four axes other than style. Two separate
measurements, two separate paths, arrive at the same number. This is not a coincidence,
it is the result of additivity: whatever number of people the same set of axes is spread
across, it finds the same set.

The table's practical use is a price list. An axis closing is often not a decision
explicitly made; it happens on its own, from no one being assigned to that axis or that
axis's finding not being treated as a blocker. The table gives that decision's price in
advance, and the prices are not equal: closing the style axis costs **7**, implementation
**6**, the others **3** each. The choice between "open one more axis" and "add one more
person" sits inside the same measurement too: the second decision's return was measured
at **0** in the third lesson, the first's is between **3 and 7** here.

The breakdown column on the right carries the same two words in every row: **unwritten
requirement 2.** It stands there in all six rows; no axis removes it.

#### A Written Document and an Unwritten Requirement

The bottom table places these two classes side by side and shows where the difference
is **not**.

The **documentation** class's three defects sit in chunks **2, 2, and 3**. The
**unwritten requirement** class's two defects sit in chunks **2 and 10**. The two
classes' defects meet in the **same chunk**: chunk two holds both two documentation
defects and one unwritten-requirement defect. Same fifty lines, same reading, same
reviewer.

When the documentation axis is open, **documentation missed is 0**; the two
documentation defects in the second chunk are found. The unwritten-requirement defect in
that same chunk is not found, and **unwritten missed stays at 2**. When the
documentation axis is closed, documentation missed climbs to **3**, and unwritten missed
is still **2**.

The difference is therefore not a difference of **location**. The two defects sit in the
same chunk and the reading passes over both. The difference is **whether there is a text
to look at.** In the documentation defect, a written sentence exists and can be compared
against the code; in the unwritten-requirement defect, no sentence to compare ever got
written.

This is the documentation axis's most confused trait. When it is open, a review saying
"documentation was looked at too" is correct — but what was looked at is **what was
written**. An unwritten requirement is outside the documentation axis too, because the
documentation axis, in the end, also compares two texts, and if one of the two texts
does not exist, no comparison can be made. All five axes stay inside the same
definition: **review looks at what was written.**

This gives the documentation axis's boundary case. If a behavior was never documented at
all, is this a documentation defect or an unwritten requirement? The distinction looks
at whether the text was **expected**. If the change introduces a new public name, that
name's documentation is expected, and its absence falls into the documentation class —
this is exactly the second finding type, and the documentation axis finds it. In
contrast, a case the change never addresses at all produces no text the documentation
axis would expect, and stays in the sixth class.

The boundary's practical counterpart is a single sentence: **the documentation axis asks
whether what the change introduces is documented; it does not ask what it failed to
introduce.** The thing that asks the second question is not review, it is the decision
that sets the change's scope.

#### Summary

- The documentation axis compares not one text but **two**: what the code does and what
  the text says it does. Its finding is a contradiction and cannot be found by reading
  the documentation alone.
- There are three finding types: **stale text** (invisible in the change because it did
  not change), **missing text** (the added name has no counterpart), **contradicting
  text** (two places describe two separate behaviors).
- The loss from closing an axis equals its class size: **3, 6, 3, 3, 7.** The most
  expensive closure is the style axis, dropping found to **15**.
- Closing the documentation axis drops found from **22** to **19**. The table counts
  missed defects, not when the cost is paid: a documentation defect is repaid every time
  it is read.
- Documentation defects sit in chunks **2, 2, 3**, unwritten-requirement defects in
  chunks **2 and 10**; both are present in the second chunk. With the documentation axis
  open, documentation missed is **0**, unwritten missed stays **2**.
- The difference is not location but the presence of a text: a documentation defect has
  a sentence to compare, an unwritten requirement does not.

#### Next Step

The table's most expensive row is the style axis: closing it misses **7** defects and
found drops to **15**. This is the five axes' most crowded class and, at the same time,
the one requiring the least information to see. The two traits together open a separate
question: does this axis require a human's reading at all. The next lesson measures the
style axis handed off to a tool — how the total found changes, what the class
distribution of the human's finding becomes, and how many reviewers it takes to produce
the same result.

### Code Style and Automation

Source: https://academia.sh/en/courses/code-review/code-style-and-automation

Once the style axis is handed to a tool, the human's finding drops from 22 to 15 and its style share from 0.318 to 0.000; the total stays the same if all five axes were already covered, and climbs from 15 to 22 in a three-reviewer panel.

The previous lesson's table gave its most expensive closure as the style axis: closing
it missed **7** defects and found dropped to **15**. The same axis was also the most
crowded class in the first lesson's class distribution, where a note was made — that
this class requires the least information to see.

The two traits together set this axis apart from the others and open a question: **does
this axis require a human's reading at all.** The lesson measures that question. The
style axis is handed to a tool, and three things are counted: what the total found
becomes, how much the human's own finding shrinks, and which classes that finding is
made up of.

#### What the Style Axis Searches For

The style axis looks at the code's **appearance**: indentation width and consistency,
line length, whitespace placement, bracket position, import order, the case convention
of names, trailing whitespace, end-of-file newline.

There is something missing from this list, and the distinction sits inside the course:
**choosing a name** is not the style axis's job. Whether a name is meaningful is a
judgment and is read on the implementation axis; which case convention that name is
written in is a rule and is read on the style axis. Naming itself is the subject of the
Clean Code course and is not repeated here.

The part that enters the measurement is one sentence: **what determines a style
defect's correctness is not a judgment, it is a writable rule.** A rule that can be
written can be applied, and a rule that can be applied can be handed off. None of the
other four axes carries this trait: whether a signature breaks a contract, whether a
body is correct, whether an assertion is meaningful, and whether a text contradicts the
code all require judgment, not a rule.

#### The Decision Itself and Applying the Decision

A style discussion is two separate jobs, and confusing them is what burns the most
rounds.

**The first job is the decision itself:** which indentation width, which line length,
which ordering. This is given once and written into a text. The decision is not
inherently right or wrong on its own; its value lies in consistency.

**The second job is applying the decision:** checking, on every change, whether the
rule was actually applied. This job is redone on every change and involves no judgment;
the rule's text is compared against the code's text.

What gets handed off is the second job. A **formatter** applies the rule to the code; a
**linter** reports where it was not applied. Both are a tool class and make no decision
— they carry out a rule already given to them. How these tools are wired in is the
subject of the Continuous Integration and Delivery course, and how they are set up in
frontend projects is the subject of the Frontend Quality course; neither is repeated
here. The only thing measured here is **what is left in review after the handoff.**

Skipping this distinction shows a single symptom in practice: the same style issue gets
re-argued from change to change. What is argued is, on the surface, a line of code; in
reality, it is a decision that was never written down.

Writing the rule therefore comes before setting up the tool, and is independent of it. A
written rule ends the argument even without a tool: the finding is no longer a
preference, it is a reference to a text, and the tool only adds execution on top of
that. The reverse order does not work — installing a tool in a repository with no rule
hands the decision over to the tool's settings, and the argument moves to the
configuration and continues there.

```text
# taught review-comment example, not executed

before handoff
  style   Indentation is used at two different widths in this file.
  style   Line length is over the limit in four places.
  style   Import order differs from the rest of the file.

after handoff
  (no review note on the style axis; the rule is now written into the tool)
  interface  Second parameter of the public function has become required.
  test       Test's only assertion is that the call does not throw.
```

This block is an example and is not run; the lesson's numbers come only from the
measurement block.

#### The Tool's Two Differences

The tool entering the measurement differs from a reviewer at two points.

**It is single-axis.** It looks only at the style class and finds nothing from the
other four classes. The rule is the one axis that can be written; the tool has no
counterpart on the axes that require judgment.

**It has no attention.** A reviewer reads a limited number of chunks in one round; the
tool reads the entire change, and that limit does not apply to it. At this lesson's
scale the two limits coincide — the change is 12 chunks, attention is also 12 chunks —
so the difference does not show up in the numbers. The condition of the difference is
still known, though, and it is outside this topic.

#### The Measurement's Assumptions

- **RA39** — The shared setup is unchanged: 600 lines, 12 chunks, 24 defects, attention
  12 chunks. The class list and distribution do not change.
- **RA40** — The tool finds only defects in the style class, and finds all of them; the
  chunk limit does not apply to it.
- **RA41** — The tool produces no false finding. The setup does not include the tool
  counting something outside the rule as a defect; the measurement is therefore in the
  tool's favor, and this is stated explicitly.
- **RA42** — In the "without tool" column the human can carry all five axes; in the
  "with tool" column no reviewer carries the style axis.
- **RA43** — Reviewer count is held the same in both regimes; the handoff adds no one
  to the panel and removes no one.
- **RA44** — The tool does not count as a reading and does not enter the reviewer
  column; its finding sits in a separate column.
- **RA45** — The cost of the handoff — writing the rule, setting up the tool, wiring the
  pipeline — is not in the measurement. The only thing measured is found and missed
  defects.
- **RA46** — The ratio is the style defect count in the human's finding divided by the
  human's total finding. Resolution at the defect level is **1/24 = 0.042**.

#### Measurement

```python
"""Code style and automation: what changes when the style axis is handed to a tool.

Part 1 - the same panels without and with the tool; the human's find and the total found.
Part 2 - the class distribution of the human's finding in the two regimes.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50


def rng(seed):
    state = seed % 2147483646 + 1

    def draw(n):
        nonlocal state
        state = (state * 48271) % 2147483647
        return state % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def formatter(d):
    """The tool class: only looks at the style axis, no attention limit."""
    return {k["no"] for k in d["defects"] if k["class"] == "style"}


FULL = set(AXES)
HUMAN = FULL - {"style"}
d = change(600)
tool = formatter(d)

PANELS = {
    "one reviewer": ([FULL], [HUMAN]),
    "three reviewers, separate axes": ([{"interface"}, {"implementation"},
                                        {"test", "documentation"}],
                                       [{"interface"}, {"implementation"},
                                        {"test", "documentation"}]),
    "three reviewers, same axis": ([{"implementation"}] * 3,
                                    [{"implementation"}] * 3),
    "five reviewers, five axes": ([{a} for a in AXES],
                                   [{a} for a in sorted(HUMAN)] + [HUMAN]),
}
print(f"{'panel':<32s} {'without tool found':>18s} {'with tool: human':>16s} "
      f"{'tool':>4s} {'total':>6s} {'missed':>6s}")
for name, (a, b) in PANELS.items():
    plain = panel(d, a)
    human = panel(d, b)
    total = human | tool
    missed = [k for k in d["defects"] if k["no"] not in total]
    print(f"{name:<32s} {len(plain):18d} {len(human):16d} {len(tool):4d} "
          f"{len(total):6d} {len(missed):6d}")

print()
print(f"{'regime':<20s} {'human found':>12s} "
      f"{'style share':>11s} {'ratio':>5s}")
for name, axes in (("style with human", FULL), ("style with tool", HUMAN)):
    b = review(d, axes)
    style = sum(1 for k in d["defects"]
                if k["no"] in b and k["class"] == "style")
    print(f"{name:<20s} {len(b):12d} {style:11d} {style / len(b):5.3f}")
remaining = sorted({k["class"] for k in d["defects"]
                    if k["no"] in review(d, HUMAN)})
print("classes of human's finding while style is with tool: " + ", ".join(remaining))
total = review(d, HUMAN) | tool
missed = [k for k in d["defects"] if k["no"] not in total]
print(f"human plus tool: found {len(total)}, missed {len(missed)}, "
      f"no axis searches for "
      f"{sum(1 for k in missed if k['class'] == UNWRITTEN)}")
```

```
panel                            without tool found with tool: human tool  total missed
one reviewer                                     22               15    7     22      2
three reviewers, separate axes                   15               15    7     22      2
three reviewers, same axis                        6                6    7     13     11
five reviewers, five axes                        22               15    7     22      2

regime                human found style share ratio
style with human               22           7 0.318
style with tool                15           0 0.000
classes of human's finding while style is with tool: documentation, implementation, interface, test
human plus tool: found 22, missed 2, no axis searches for 2
```

#### When the Tool Changes the Total

The top table's first and last rows say the same thing: **22 and 22.** If all five axes
were already covered by humans, the tool's contribution to the total found is **0**.
The tool does not look anywhere no one was already looking.

The second row shows the reverse. Three reviewers covered four axes — interface,
implementation, test, documentation — and found **15** without the tool. Once the style
axis is handed to the tool, the total climbs to **22**. No one was added to the panel or
removed; the one axis that was not covered got covered, at a cost of **7** defects. **A
three-person panel got matched to a five-person panel's number.**

The third row repeats the same rule on the narrowest panel. Three reviewers were looking
at the same axis and finding **6**; the tool raises this to **13**. Missed is still
**11**, and **9** of that comes from the three axes no human is looking at. The tool
closes one axis; it does not close the others.

The pattern comes together in one sentence: **the tool's contribution equals the size
of the axis the humans did not cover.** This is the axis side of the course's second
claim — the tool behaves like a person too: what grows the number is not its presence,
it is that the axis it covers had not been covered before.

**When** the tool's contribution happens does not show in the table but changes the
review. The tool runs the moment the change is submitted, and the seven defects it finds
are closed before review even begins: no note gets written, no reply is expected. Even
in rows where the total stays the same, the review's **subject matter** shrinks, and the
part that shrinks is exactly the part that required no judgment.

The last column carries the sixth class in all four rows. In the best case with the
tool, missed is **2**, and both are in the unwritten-requirement class.
**Automation does not raise the ceiling of found above 22.** The tool looks at what was
written, just like review; what was never written, it too cannot see.

#### Where Attention Shifts

The bottom table looks at the same handoff from the human's side, and the numbers here
change even in the rows where the total stayed fixed.

With style on the human, a single reviewer's finding is **22** defects and **7** of that
is in the style class; the ratio is **0.318**. With style on the tool, the human's
finding drops to **15** and the style share is **0**; the ratio is **0.000**. The
human's finding is left with four classes: interface, documentation, implementation,
test.

What changes is not the total, it is **the content of the review.** Before the handoff,
about a third of a review's findings are rule defects; after, it is zero. Every
remaining finding comes from an axis requiring judgment, and what gets discussed in the
review text shrinks in the same proportion: applying the rule is no longer up for
discussion, because the text deciding it was written once and its application is now
carried out on its own.

There is an honest limit here that needs to be stated. At this scale, the freed reading
has **no** numeric return: the change is 12 chunks, attention is also 12 chunks, so the
human's reading was not being exhausted to begin with. Redirecting the attention freed
from the style axis to another axis changes no number, because the other axes were
already being read in full. For the freed attention's counterpart to show, a single
condition is required: **what there is to read has to be larger than the attention
budget.** No such case has been built in this setup, and it is not this topic's
variable.

#### Two Assumptions Built in the Tool's Favor

The measurement holds the tool stronger than it might be in reality at two points, and
both affect how the result should be read.

**The tool finds the entire style class.** A formatter applies only the rules written
for it; a style defect outside the rule set — a file layout that differs from the rest
of the repository, an inconsistent abbreviation style — is invisible to it and stays
with the human. The measurement has no such remainder; if it did, the "style share
0.000" row would have come out above zero.

**The tool produces no false finding.** When the rule set does not match the rest of
the repository, the tool also reports spots that are not defects. These findings do not
enter review, but they cost time and require the rule to be rewritten, and this cost is
not included.

Both assumptions are set deliberately. The purpose is to show that even **in the tool's
best case**, the total stays at **22**: what sets the ceiling is not the tool's quality,
it is the axis list itself.

#### Summary

- The style axis looks at the code's appearance and separates from the other four at
  one point: what determines its correctness is not a judgment but a **writable rule**.
  Choosing a name is not on this axis; the case convention of a name is.
- A style discussion is two jobs: **the decision itself** is given once and written into
  a text, **applying the decision** is repeated on every change. What gets handed to the
  tool is the second.
- The tool's contribution to total found equals the axis humans did not cover: **0** if
  five axes are already covered, **15** to **22** on a three-reviewer panel, **6** to
  **13** on three reviewers on the same axis.
- Automation does not raise the ceiling: missed in the tool's best case is **2**, and
  both are in the unwritten-requirement class. The tool too looks only at what was
  written.
- The handoff changes the review's content even where the total stays fixed: the
  human's finding goes from **22** to **15**, its style share from **0.318** to
  **0.000**, leaving only the four axes requiring judgment.
- At this scale, the freed attention's numeric return is **0**, because the change is
  not larger than the attention budget.

#### Next Step

This topic's six lessons moved a single variable: **the axis looked at.** Raising axis
count pushed found from **6** to **15** and then to **22**; closing an axis cost exactly
the size of its class; adding people changed nothing. All these measurements shared one
silent assumption, and this lesson said it out loud: **the change was always exactly the
size of the attention budget**, meaning what there was to read
could be read.

The next topic lifts that assumption. The axis looked at is held fixed, and this time
**what there is to look at** grows. The question is this: do the defects axis diversity
earns stay in place once the change stops being fully readable, or does the found ratio
collapse independent of the axes?

## Review Culture

The limits of a reviewable change, feedback language that speaks to the change rather than the person, resolving a design argument, and reading wait time and round count.

### Change Size

Source: https://academia.sh/en/courses/code-review/change-size

Attention is fixed at 12 chunks; the same 24 defects are found 22 times at 100 lines, 18 times at 900 lines, 2 times at 2400 lines, and 36 of 2400 lines' chunks go unread.

The previous lesson handed the style axis off to a tool and measured attention shifting
to another axis. The pattern built across the topic was this: adding axes raised the
number of defects found. A single reviewer looking at five axes found what five reviewers
found; what grew the number was not who looked, but which axis was looked at.

This lesson's question comes from the opposite direction. Let the axis count stay fixed
and let **what is being looked at grow**. Same reviewer, same five axes, same defects —
only the change is 2400 lines instead of 100. With axis diversity left entirely intact,
what happens to the number found?

#### Attention Is a Budget

The shared fixture's reviewer works with two limits. The first is **axis**: they see only
the defects on the axes they look at. The second is **attention**: the number of chunks
actually read in one review round is limited. A chunk is 50 lines, attention is 12
chunks. This means roughly 600 lines are actually read in one sitting.

Attention being a budget is the fixture's most important assumption, and it agrees with
observation. A reviewer does not read a change at the same intensity start to finish;
first chunks are read carefully, later ones are scanned faster and faster, and past a
point reading gives way to approval. The fixture hardens this decline into a binary: the
first 12 chunks are read, the rest are not. The hardening does not exaggerate the number,
since chunks that stop getting read in reality are not completely blind either — but it
makes the pattern visible.

The first consequence of this is arithmetic. If the change is under 600 lines, every
chunk is read and the attention limit never engages; the only thing limiting is the axis
list. Once the change passes 600 lines, **every added chunk goes into the unread set.**
Attention does not grow; the change does.

#### Same Defects, Four Sizes

The constraint that makes the measurement meaningful is this: all four sizes carry the
**same 24 defects**. The fixture does not grow the defect count along with the change's
size. This is deliberate. If the defect count also grew, a falling ratio would not be
surprising; held constant, the ratio's drop has exactly one source — the unread chunk.

The class distribution is also the same at all four sizes: interface 3, implementation 6,
test 3, documentation 3, style 7, and **unwritten requirement 2**. The sixth class is the
class no axis searches for: the defect of what was never written. Review looks inside the
submitted change; it cannot see the absence of what was never submitted. This lesson
shows those two defects separately in the missed column, because they keep going missed
regardless of size and are not the cause of the drop.

The only thing that changes is the line count. Line count sets the chunk count, and chunk
count sets the area defects are spread over. Defects are scattered across this area;
since attention stays fixed, as the scattering area grows, an increasing share of defects
falls into unread chunks.

The measurement's assumptions:

- **RC1** — All four sizes are generated from the same shared fixture: 24 defects, the
  same class distribution, the same oracle. Since we placed the defects ourselves, which
  chunk each one sits in is known.
- **RC2** — The only thing that changes is the line count. The axis assignment is fixed:
  one reviewer, all five axes. Attention is fixed: 12 chunks.
- **RC3** — Reading starts at the first chunk and stops once attention runs out. A defect
  in an unread chunk goes unseen no matter which axis its class is on.
- **RC4** — In the second measurement, `review` takes a starting chunk; this changes
  **where** the attention limit is placed, not the size of attention itself. Every window
  still reads 12 chunks.
- **RC5** — Splitting does not redistribute defects. Same 2400 lines, same 48 chunks,
  same 24 defects; only the reading happens in four separate windows.
- **RC6** — The set's resolution is **1/24**, that is, **0.042**, at the defect level;
  **1/12** at the chunk level. A smaller difference cannot be defended with this set.

#### Measurement

```python
"""Change size: same 24 defects, four sizes, fixed attention.

Part 1 - unread chunks stay fixed as size grows, found ratio falls.
Part 2 - what changes when the same 2400 lines are read in four windows.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12       # chunks actually read in one round
CHUNK = 50            # lines
SCALES = (100, 300, 900, 2400)


def rng(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def change(lines, defect_count=24, seed=SEED):
    r, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[r(6)],
                         "chunk": r(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, start=0, attention=ATTENTION):
    """A reviewer starts at the given chunk and reads up to attention chunks."""
    read = set(range(start, min(start + attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


FULL = set(AXES)
d0 = change(600)
distribution = {s: sum(1 for k in d0["defects"] if k["class"] == s) for s in CLASSES}
print("class distribution: " + ", ".join(f"{s} {n}" for s, n in distribution.items()))
print()
print(f"{'lines':>6s} {'chunks':>6s} {'read':>6s} {'unread':>7s} "
      f"{'found':>6s} {'missed':>6s} {'unwritten missed':>17s} {'ratio':>6s}")
for lines in SCALES:
    d = change(lines)
    b = review(d, FULL)
    missed = [k for k in d["defects"] if k["no"] not in b]
    unw = sum(1 for k in missed if k["class"] == UNWRITTEN)
    print(f"{lines:6d} {d['chunks']:6d} {min(ATTENTION, d['chunks']):6d} "
          f"{max(0, d['chunks'] - ATTENTION):7d} {len(b):6d} {len(missed):6d} "
          f"{unw:17d} {len(b) / len(d['defects']):6.3f}")

print()
big = change(2400)
total = set()
print(f"{'pass':>5s} {'chunk range':>12s} {'found':>6s}")
for i, start in enumerate(range(0, big["chunks"], ATTENTION)):
    b = review(big, FULL, start)
    total |= b
    print(f"{i + 1:5d} {f'{start}-{start + ATTENTION - 1}':>12s} {len(b):6d}")
missed = [k for k in big["defects"] if k["no"] not in total]
print(f"four-pass total: found {len(total)}, missed {len(missed)}, "
      f"ratio {len(total) / 24:.3f}")
print("missed classes: " + ", ".join(sorted({k["class"] for k in missed})))
```

```
class distribution: interface 3, implementation 6, test 3, documentation 3, style 7, unwritten requirement 2

 lines chunks   read  unread  found missed  unwritten missed  ratio
   100      2      2       0     22      2                 2  0.917
   300      6      6       0     22      2                 2  0.917
   900     18     12       6     18      6                 2  0.750
  2400     48     12      36      2     22                 2  0.083

 pass  chunk range  found
    1         0-11      2
    2        12-23     12
    3        24-35      2
    4        36-47      6
four-pass total: found 22, missed 2, ratio 0.917
missed classes: unwritten requirement
```

#### Where the Ratio Collapses

The top table's first two rows are boring, and they should be. At 100 lines there are 2
chunks, at 300 there are 6; both are under attention, and unread chunks are **0** in
both. Found is **22**, missed is **2**, and both of the missed are **unwritten
requirement**. That is, as long as the change stays under attention, size has no effect
at all; the only thing missed is already the class no axis searches for.

In the third row, the limit engages. 900 lines is 18 chunks, 12 are read, **6 chunks go
unread**. Found drops to **18**, the ratio becomes **0.750**. Of the **6** missed, **2**
are unwritten requirement, and the remaining **4** are defects that were readable but
were not read. By class, these sit on axes the reviewer is looking at — implementation
and style. They are on the reviewer's list, they are in the oracle, only not in front of
their eyes.

The fourth row is the collapse. 2400 lines is **48 chunks** and attention is still 12;
**36 chunks go entirely unread.** Found drops to **2**, the ratio becomes **0.083**.
Defect count did not change, class distribution did not change, the reviewer's axis list
did not change. Only the change grew.

The class list in the missed column also widens in this row. At 900 lines, the missed
were spread across two axes; at 2400, the **22** missed spread across all six classes at
once. The loss from closing off an axis is confined to that axis and it is known where it
looks; the loss from an unread chunk is **independent of axis** and where it falls is
unknown. The two losses are not the same: the first is a **decision**, the second is an
**accident**.

The difference between the two rows fits one sentence: the ratio tracks read chunks over
total chunks. At 900 lines, 12/18 is read and the ratio falls to 0.750; at 2400, 12/48 is
read and it falls to 0.083. Falling below **12/48 = 0.250** itself is because defects are
not spread evenly across chunks: the first 12 chunks hold only **3** defects, and one is
unwritten requirement. In a large change, where the reviewer happens to stop becomes a
gamble.

Whether the differences sit inside the measurement's band also needs checking. The drop
from 22 to 18 is a difference of **4/24 = 0.167**; since the set's resolution is 0.042,
this is four steps above it and defensible. The drop from 18 to 2 is **16/24 = 0.667**
and needs no argument. In contrast, the difference between 100 and 300 lines is **0** —
the set has nothing to say, and saying it has nothing to say is itself a result.

#### Where a Large Change Comes From

The numbers show a limit, but they do not show why it gets crossed. No one writes a
2400-line change in one sitting; that size **accumulates**. There are three common
sources of accumulation, and all three are about **flow**, not writing.

The first is **branch lifetime**. The distinction built in the Branching and
Collaboration course turns into a number here: a short-lived branch produces a small
change, a long-lived branch grows a little every day and by review time is not a single
purpose but a week's total. The second is **mixed classes**: renaming, moving files, and
format fixes arrive in the same change as a behavior change. The third is **generated
files**: a dependency lock file, a compiled output, or a regenerable table growing the
line count while carrying nothing inside it a reviewer can decide on.

All three share the same consequence, and it is the measurement's fourth row: attention
stays the same, chunk count grows. The remedy for all three is given not during review
but **before** it — shortening branch lifetime, separating classes, keeping generated
files out of review. Once 48 chunks land on the review table, only one thing can still be
done: send it back.

#### Splitting Does Not Shrink Lines, It Grows Read Chunks

The bottom table reads the same 2400 lines in four windows. The change was not split,
defects were not rescattered, attention was not grown — reading was done four times, each
time through a different 12-chunk window. The result: **2 + 12 + 2 + 6 = 22** found,
**2** missed, ratio **0.917**.

This is the **same** ratio as the 100-line change. What determines the number found is
not the change's line count but the number of chunks read. When 2400 lines are read in
four windows, all 48 of the 48 chunks get read and none go unread. The **2** missed
defects are still the unwritten requirement class, and splitting does not touch them: no
window shows what was never written.

The imbalance between windows is also instructive. The first window yields only **2**,
the second **12** defects. The window read in a single pass is the poorest window in
defect terms. A reviewer reading the change in one round has no way of knowing this;
which chunks are dense is learned only after reading all of them.

Splitting's cost does not show in the number, but it is real: four windows mean four
separate readings. This cost gets paid later in this topic, as wait time and review round
count. What needs recording here is that the cost **exists** and is **predictable** — the
price of finding 22 defects is splitting the reading into four.

#### The Limit of a Reviewable Change

The measurement gives a practical limit: a change is reviewable when it fits into as many
chunks as the reviewer's attention. With the fixture's numbers, this is roughly 600
lines. The number itself is the fixture's assumption and varies team to team; what does
not vary is that a **limit exists**, and that crossing it makes the ratio drop
**silently**.

**Silently** is the key word here. An unread chunk produces no feedback. A review of a
2400-line change also gets approved, it also closes, and someone looking from outside
cannot tell the two reviews apart. **Approval is not proof of having been read.** The
measurement's **22** missed defects also entered the code right after an approved review.

The way to keep within the limit is to split the change, but splitting cannot be
arbitrary. Each piece must be understandable on its own and correct on its own; otherwise
the reviewer reads the piece, does not understand what it does, and waits for the next
piece — another name for deferring the reading. The workable splitting criteria are:
**single purpose** (a piece does one thing), **standalone correctness** (the system does
not break if the piece merges alone), and **separating moved from changed** (renaming,
moving files, and format fixes stay out of a behavior change).

The last criterion pays off the most. If a format fix grows a change by 300 lines, those
300 lines are 6 chunks, and 6 chunks of attention have been stolen from the behavior
change. The previous lesson handed the style axis off to a tool; this lesson shows the
same handoff's second payoff — the axis handed off removes not only the discussion but
the **chunk** from review too.

There are also changes that cannot be split. If a single interface's name is changing and
appears across two thousand lines, breaking it into pieces leaves every piece broken on
its own — the first criterion collides with the second. What can be done here is not
splitting but **declaring a reading order**: the author writes which chunks of the change
need a decision and which repeat the same pattern. The bottom table shows why this
works — two of the four windows yield 2 defects, one yields 12, one yields 6. Marked
chunks would keep the reviewer's first 12 chunks from being a gamble.

This declaration is not an accountability tool and should not be used as one. What it
measures is not the person's attention but the **change's readability**: the same 48
chunks give a different result once their order is declared. The next two lessons carry
this distinction forward — what gets measured, every time, is a property of the change
and the process, not the person.

#### Summary

- Attention is a budget: the number of chunks read in one review round is fixed (12
  chunks, 600 lines, in the fixture). If the change is larger than that, every added
  chunk goes into the unread set.
- Same 24 defects, same axis list: at 100 and 300 lines, **22** are found (**0.917**); at
  900, **18** (**0.750**); at 2400, **2** (**0.083**). Defect count did not change, only
  the change grew.
- 2400 lines is **48 chunks** and **36 chunks** go entirely unread; the 12 chunks read
  are the poorest window in defect terms, which is why the ratio falls even below 12/48.
- When the same 2400 lines are read in four windows, found rises to **22**, ratio to
  **0.917**. What determines this is not line count but the number of chunks read.
- The **2** missed defects stay the same at every size and in every window: **unwritten
  requirement**. Neither shrinking the size nor splitting shows what was never written.
- Approval is not proof of having been read; an unread chunk produces no feedback, and
  two reviews look the same from outside.

#### Next Step

When a change is a reviewable size, the reviewer finds 22 defects. But a found defect is not a
fixed defect: the gap between them is closed by a **review comment**, and what the comment says
determines how many review rounds it takes to close the defect. The next lesson measures the same
22 findings written three ways — a note stating what should change and why, a note that only says
what was seen, and a request carrying no closing criterion — and which note form forces a change to
close with the defect still open.

### Feedback Language

Source: https://academia.sh/en/courses/code-review/feedback-language

The same 22 findings close in 2 review rounds when written actionably, in 4 rounds when no closing criterion is written; in a change merged at the third round, the criterionless style leaves 22 of 22 notes open.

The previous lesson measured the limit of a reviewable change: because attention stayed
fixed at 12 chunks, the same 24 defects were found 22 times at 100 lines and 2 times at
2400. When a change is a readable size, the reviewer finds nearly everything they can.

But a found defect is not a fixed defect. One thing alone closes the gap between them:
the **review comment**. This lesson's question is not the comment's politeness but its
**function** — when the same finding is written two separate ways, how many review rounds
does it take for the defect to leave the code, and which writing leaves it there.

#### The Subject of a Note

This course's boundary takes its most concrete form here: **the subject of a review
comment is the change.** The rule is not an etiquette rule; it is a rule of function, and
its justification can be measured.

The test is mechanical. The comment's sentence is looked at and one question is asked:
**what is its subject?** If the subject is a code element — a function, a parameter, a
condition, a test, a documentation line — the comment stands as written. If the subject
is a role, the comment is rewritten and its subject replaced with the code element it is
actually talking about. No content is lost; the sentence now points at the thing that can
be fixed.

The justification is this: a note's **closing criterion** is found in the code. A note in
the form "this condition divides by zero on empty input" closes once that condition
changes, and its closing is visible to everyone. A sentence whose subject is a role has
no such criterion — what change closes it is not written, so it cannot close and it
circulates through review rounds. The measurement will count the cost of this
circulating.

The same boundary applies to the measurement itself. What is counted in this lesson is
not who wrote how many notes but **how many review rounds a given writing takes**. The
subject measured is, again, the change.

#### Three Ways to Write a Note

The same finding can be written three ways, and all three are about the change. The
difference is what the note hands the reader.

```text
# taught example note, not executed, not a measurement

observation
  average() misbehaves when given an empty list.

criterionless request
  the empty-list case should be handled more robustly.

actionable
  average() divides the total by the length when given an empty list, and
  raises a division-by-zero error. It should either return 0.0 in this case
  or raise a defined error. Whichever is chosen, the matching case must be
  added to the test file; the note closes when that test passes.
```

**Observation** says what was seen and stops there. It is correct, it points to the spot,
but it does not write what is being asked for; the author has to ask a question and the
reviewer has to answer it. This is one exchange and costs one review round.

**Criterionless request** asks for a change but does not say when the request is met.
"More robust" is not a criterion: the author sends a fix, the reviewer sees it is not
what they meant, the note reopens. This is two exchanges.

**Actionable** note carries three things at once: **what was seen**, **what should
change**, **what closes it**. The third is the most-skipped and the highest-paying. When
the closing criterion is written inside the note, whether the fix is correct needs no
discussion.

```text
# taught example note, not executed, not a measurement

observation
  the new parameter has been added in the middle of the signature.

actionable
  because the threshold parameter was added in the middle of the signature,
  calling code that relies on position silently shifts. The parameter
  should move to the end of the signature and be given a default value, so
  existing calls keep working unchanged. The note closes when the
  signature is at the end and has a default.
```

The second example shows that being actionable is a matter of **information**, not
style. An actionable note is longer because it says **more**: the two pieces observation
does not carry, the requested change and the closing criterion, are written there.
Brevity is not a virtue here; missing information comes back in the next review round.

#### A Note Written as a Question

Some notes are written as questions, and this form is not good or bad on its own. What
decides is whether the question **names the situation**.

A question that names the situation is actionable: if which input, which code element,
and which result is expected are written, the author can close the question with either a
fix or a one-sentence justification. A question that does not name the situation is in
the same class as observation — it adds one exchange, because which situation is being
discussed has to be pinned down first.

The question form's real function lies elsewhere: the note itself might rest on a
**wrong assumption**. When such a note is written as a question, one answer corrects the
assumption and it costs one exchange; when the same assumption is written as a firm
request, a wrong fix gets sent first and it costs two exchanges. The measurement shows
this directly: the criterionless-request regime's two extra rounds come from the
misunderstanding only being noticed after the fix arrives. When something is unknown,
writing that it is unknown saves a round.

The measurement's assumptions:

- **RC7** — The configuration is fixed: one reviewer, all five axes, 600 lines, attention
  12 chunks. Found is 22, missed is 2, and both missed are **unwritten requirement**.
  Note style does not change these numbers; what is measured is how the found defects
  close.
- **RC8** — A note is written for every found defect. The note's style sets the number
  of exchanges needed for the defect to leave the code: actionable **2**, observation
  **3**, criterionless request **4**.
- **RC9** — One review round consumes one exchange of every currently open note at once.
  Notes are handled together, not one at a time; this is why the **slowest note** sets
  the review round count.
- **RC10** — The time a review round adds to wait is **6 units**, independent of style.
  **Note-round** counts one unit for every review round a note stays open; it is the
  count of total load.
- **RC11** — In the mixed regime, every note's style is chosen with `rng`; it is a sample
  of how notes end up distributed when no rule is enforced.
- **RC12** — In the second measurement, the change merges at the third review round.
  Defects whose notes are still open at that point stay in the code and add to the 2
  defects never found.

#### Measurement

```python
"""Feedback language: same 22 findings, four note styles.

Part 1 - how many review rounds each style closes in, wait and note-round carried.
Part 2 - which notes stay open if the change merges at the third round.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50
ROUND_WAIT = 6        # units of wait one review round adds
# Exchanges needed to close a note.
STYLES = {"actionable": 2, "observation": 3, "criterionless request": 4}


def rng(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def change(lines, defect_count=24, seed=SEED):
    r, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[r(6)],
                         "chunk": r(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def cycle(needed, cap=6, wait_per_round=ROUND_WAIT):
    """Every review round consumes one exchange of every open note."""
    open_, rnd, wait, note_round = list(needed), 0, 0, 0
    while open_ and rnd < cap:
        rnd += 1
        wait += wait_per_round
        note_round += len(open_)
        open_ = [g - 1 for g in open_ if g > 1]
    return rnd, wait, note_round, len(open_)


FULL = set(AXES)
d = change(600)
found = sorted(review(d, FULL))
missed = [k for k in d["defects"] if k["no"] not in set(found)]
r = rng(SEED)
mixed = [list(STYLES)[r(3)] for _ in found]
REGIMES = {name: [name] * len(found) for name in STYLES}
REGIMES["mixed"] = mixed

print(f"found {len(found)}, missed {len(missed)}, its unwritten class "
      f"{sum(1 for k in missed if k['class'] == UNWRITTEN)}")
print("mixed regime distribution: " +
      ", ".join(f"{a} {mixed.count(a)}" for a in STYLES))
print()
print(f"{'note style':<22s} {'round':>5s} {'wait':>6s} {'note-round':>10s} "
      f"{'unclosed':>8s} {'left in code':>12s}")
for name, notes in REGIMES.items():
    rnd, wait, nr, open_ = cycle(STYLES[n] for n in notes)
    print(f"{name:<22s} {rnd:5d} {wait:6d} {nr:10d} {open_:8d} "
          f"{open_ + len(missed):12d}")

print()
print("if the change merges at the third review round")
print(f"{'note style':<22s} {'round':>5s} {'wait':>6s} {'closed':>6s} "
      f"{'unclosed':>8s} {'left in code':>12s}")
for name, notes in REGIMES.items():
    rnd, wait, nr, open_ = cycle((STYLES[n] for n in notes), cap=3)
    print(f"{name:<22s} {rnd:5d} {wait:6d} {len(notes) - open_:6d} "
          f"{open_:8d} {open_ + len(missed):12d}")
```

```
found 22, missed 2, its unwritten class 2
mixed regime distribution: actionable 2, observation 11, criterionless request 9

note style             round   wait note-round unclosed left in code
actionable                 2     12         44        0            2
observation                3     18         66        0            2
criterionless request      4     24         88        0            2
mixed                      4     24         73        0            2

if the change merges at the third review round
note style             round   wait closed unclosed left in code
actionable                 2     12     22        0            2
observation                3     18     22        0            2
criterionless request      3     18      0       22           24
mixed                      3     18     13        9           11
```

#### Where the Round Comes From

The top table's first columns give the cost of the same 22 findings. In the actionable
style, the process closes in **2** review rounds and **12** units of wait. Observation is
**3** rounds / **18** units, criterionless request **4** rounds / **24** units. Found
never changes: every regime has 22 findings, 2 missed, both missed unwritten requirement.
**Note style does not change what is found; it changes when the found leaves.**

What delays the closing is written into the number itself. Observation adds one exchange
because the missing information — what is being asked for — gets asked and answered in
the next round. Criterionless request adds two exchanges because the missing information
only surfaces once a wrong fix arrives. **Every piece of missing information asks for
itself back in a review round.**

The note-round column shows the load better: **44**, **66**, **88**. Criterionless
writing produces exactly twice the load of actionable writing, and all of it is carrying
the same 22 findings. There is no difference in the code; the difference is only in what
the notes say.

The mixed regime exposes how the review round count is actually set. The distribution is
2 actionable, 11 observation, 9 criterionless request; total load is **73** note-rounds,
roughly the average of the three regimes. Review round count, in contrast, is **4** — the
slowest regime's count. **In notes handled together, the slowest note sets the round
count, not the average.** Even if thirteen of twenty-two notes are written flawlessly,
the remaining nine still carry the process to four rounds.

#### Merged Before the Notes Close

The bottom table adds a real constraint: a change does not stay open forever. Merged at
the third review round, every regime pays the same wait but delivers different things.

Actionable writing had already finished at the second round; the limit does not touch it.
Observation writing closes exactly at the third round. In the criterionless-request
regime, closed is **0**, unclosed is **22**, and defects left in the code are **24** —
all of them. This is the table's harshest row: the review **found** 22 of 24 defects and
**delivered none**. A process measured by found count alone would call this review a
success.

The mixed regime gives a more realistic result: **13** closed, **9** unclosed, **11**
left in the code. Of the eleven, **2** are the class no axis searches for — unwritten
requirement, never something review could see. The remaining **9** were seen, written,
and failed to close because of **how they were written**. The difference between the two
losses matters: the first is review's limit, the second is a loss review **produced
itself** and could have eliminated entirely.

The set's resolution is **1/24 = 0.042** at 24 defects. A **9**-defect difference is
twenty times that, and sits comfortably inside the measurement band. So does the drop in
closed count from 22 to 13.

The bottom table's wait column is also notable: observation, criterionless request, and
mixed all spend **18** units. Three processes hitting the limit consume the same time;
the only thing that differs is how many defects actually left the code by the end of
it — **22**, **0**, and **13**, respectively. **A reading that looks only at wait cannot
tell these three regimes apart.**

#### Habits That Make a Note Actionable

Four habits follow directly from the measurement, and all four add one piece of
information into the note.

**Write the closing criterion.** A note should say which observable state it closes on: a
test passing, a signature reaching a specific form, a documentation line being added. The
criterionless-request regime's two extra rounds come from here.

**Separate the request from the observation.** Observation points to the spot, request
gives direction. When both are not in the same note, whatever is missing gets asked in
the next review round.

**Mark what is not binding.** Not every note is a blocker; some notes are suggestions
that can merge unfixed. Stating this in the note itself keeps non-binding notes from
adding to the review round count — the mixed regime's rise to four rounds came exactly
from a few slow notes.

**Write the same finding once.** In a change where one pattern repeats in ten places,
writing a separate note for every repeat grows the note count but not the information;
all ten notes wait for the same exchange. Writing the pattern once and stating its
scope — which files, which elements — gives the same closing criterion in a single note.

What the four habits share is that all of them make the note **more informed**. The
measurement had one source of slowness: information absent from the note being asked for
in the next review round. Politeness shows up in none of this table's columns; what shows
up is where the information stands.

#### Summary

- A review comment's subject is the change; the test is mechanical — if the subject is
  not a code element, the note is rewritten so the code element it is discussing becomes
  the subject.
- The justification is measurable: a note's **closing criterion** is found in the code. A
  note with no criterion does not say what closes it and circulates through review
  rounds.
- The same 22 findings cost **2** rounds / **12** wait / **44** note-rounds when written
  actionably, **3 / 18 / 66** as observation, **4 / 24 / 88** as criterionless request.
  Found and missed never change: 22 and 2, both missed unwritten requirement.
- Because notes are handled together, the review round count is set by the **slowest
  note**, not the average: in the mixed regime, even with 13 of 22 notes written fast,
  the process still takes 4 rounds.
- Merged at the third review round, criterionless writing closes **0** notes and leaves
  **24** defects in the code; mixed writing closes **13**, leaving **11** in the code —
  **2** of those are the never-found class, **9** are found but unclosed.

#### Next Step

The measurement assumed one thing: given enough exchanges, a note closes. This is not
always true. In some notes what is missing is not information but **agreement** — the
author and the reviewer defend different designs for the same code element, and adding
more exchanges does not bring the two sides closer. The next lesson measures how many
review rounds such a finding is carried for, and what four separate resolution rules do
to wait, closed notes, and defects left in the code.

### Disagreement Resolution

Source: https://academia.sh/en/courses/code-review/disagreement-resolution

When six of twenty-two findings turn into a design disagreement, having no rule produces 6 review rounds and 36 units of wait and leaves 8 defects in the code; tying the decision to a criterion takes 3 rounds and 18 units and leaves only 2.

The previous lesson measured how a review comment is written and found a single source:
information absent from the note got asked back in the next review round. Actionable
writing closed in 2 review rounds, writing with no closing criterion in 4. The
measurement's assumption was this — given enough exchanges, a note closes.

This assumption is not true for every note. In some findings what is missing is not
information but a **decision**: two designs are defended for the same code element, both
work, both pass their tests. In a note like this, adding exchanges does not move it
closer to closing; the same two justifications get rewritten every review round. This
lesson's question is: how many review rounds does an unresolved finding get carried for,
and what does the rule that resolves it add to wait.

#### Disagreement Versus Missing Information

Two notes look alike but behave differently, and the distinction can be made
mechanically. The test is one question: **does either side hold the information that
would close the note?**

If it does, the note is a **missing-information** note. The author knows what should
happen on empty input, or the reviewer knows why the signature broke; the information
crosses over in one exchange and the note closes. The previous lesson's entire
measurement was in this class.

If it does not, the note is a **design disagreement**. What is missing is information
nobody has: which design is better cannot be settled with the criteria on hand. In a note
like this, an exchange carries no information, only repeats justifications. The
measurement will count the cost of this repeating.

Which axes disagreement shows up on is not random either. Once the style axis is handed
off to a tool, no argument remains; whether a test covers a case or a documentation line
exists is settled by observation. Disagreement mainly shows up on the **interface** and
**implementation** axes, because these two axes have more than one defensible solution.
The measurement draws disagreements only from these two classes.

Organizational decision-making arrangements, authority distribution, and team structure
are not this lesson's subject; those were established in the Process, Team, and Delivery
course. What is measured here is review's own numbers: review round, wait, and defects
left in the code.

#### Four Resolution Rules

The measurement runs the same 22 findings through four rules. The rules substitute for
one another; a team picks one ahead of time and applies it once disagreement shows up.

**No rule.** Disagreement is re-argued every review round. Since the argument has no
closing mechanism, the change fills the most review rounds it can stay open for and the
disagreements stay open.

**Tied to criterion.** The argument is taken to a written criterion: a rule in the
interface contract, a measured number, whether a test passes. The criterion is
independent of either side's preference, and the decision follows from it.

**Owner decides.** If code ownership is defined, the owner gives the decision for a
disagreement a criterion cannot resolve. The decision arrives in one exchange, but the
owner is not inside the review; bringing them into the process charges extra wait.

**Separate work item.** The disagreement is pulled out of this change and moved to its
own work item. The change closes immediately; the design argument continues, and the
defect is not fixed in this change.

```text
# taught example note, not executed, not a measurement

disagreement
  Two designs are defended for the same function: collecting the options
  into a single dictionary parameter, or leaving them as separate named
  parameters. Both writings work, and both writings pass their tests.

tied to a criterion
  The decision criterion is the backward-compatibility rule in the
  interface contract: existing calls must keep working unchanged when a
  new option is added. The dictionary parameter satisfies this rule;
  separate parameters change the signature on every addition. The note
  closes when the dictionary parameter is chosen and written to the docs.
```

The example shows what tying to a criterion does: what ends the argument is not either
side being persuaded, it is **where the decision gets made changing**. The decision now
comes from the contract, not the note, and the note gains a closing criterion that passes
the previous lesson's test.

The measurement's assumptions:

- **RC13** — The configuration is fixed: one reviewer, five axes, 600 lines, attention
  12 chunks. Found is 22, missed is 2, and both missed are **unwritten requirement**. The
  resolution rule does not change these numbers.
- **RC14** — Disagreement can arise only among findings in the interface and
  implementation classes. Which of the **9** notes found in these classes turn into
  disagreement is chosen with `rng`.
- **RC15** — A non-disagreement note is written actionably and closes in **2**
  exchanges. The previous lesson's best regime is taken as the baseline in this lesson.
- **RC16** — With no rule, disagreement never closes: it is re-argued every review
  round. Tied to criterion and owner decides close it in **3** exchanges; separate work
  item closes it in **2** exchanges but does not remove the defect from the code.
- **RC17** — One review round adds **6 units** to wait. Owner decides additionally adds
  **6 units** of owner wait: the owner is outside the review and has to enter the queue.
- **RC18** — A change can stay open for at most **6** review rounds. Defects of notes
  still open at that limit stay in the code.
- **RC19** — The set's resolution is **1/24 = 0.042** at 24 defects; wait is measured
  per unit, and a difference smaller than one review round cannot be defended with this
  set.

#### Measurement

```python
"""Disagreement resolution: same 22 findings, four resolution rules.

Part 1 - which findings turn into disagreement.
Part 2 - how many review rounds, how much wait, what each rule leaves in the code.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50
ROUND_WAIT = 6        # units of wait one review round adds
CAP = 6                # most review rounds a change can stay open
CONTESTABLE = ("interface", "implementation")   # classes disagreement can occur in
ACTIONABLE = 2          # exchanges to close a non-disagreement note
# exchanges is None if disagreement is re-argued every round and never closes.
RULES = {
    "no rule": {"exchanges": None, "owner": 0, "defers": False},
    "tied to criterion": {"exchanges": 3, "owner": 0, "defers": False},
    "owner decides": {"exchanges": 3, "owner": ROUND_WAIT, "defers": False},
    "separate work item": {"exchanges": 2, "owner": 0, "defers": True},
}


def rng(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def change(lines, defect_count=24, seed=SEED):
    r, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[r(6)],
                         "chunk": r(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def cycle(needed, cap=CAP, wait_per_round=ROUND_WAIT):
    """Every review round consumes one exchange of every open note."""
    open_, rnd, wait, note_round = list(needed), 0, 0, 0
    while open_ and rnd < cap:
        rnd += 1
        wait += wait_per_round
        note_round += len(open_)
        open_ = [g - 1 for g in open_ if g > 1]
    return rnd, wait, note_round, len(open_)


FULL = set(AXES)
d = change(600)
found = [k for k in d["defects"] if k["no"] in review(d, FULL)]
missed = [k for k in d["defects"] if k not in found]
r = rng(SEED)
for k in found:
    k["disagreement"] = k["class"] in CONTESTABLE and r(2) == 0
dis = [k for k in found if k["disagreement"]]

print(f"found {len(found)}, missed {len(missed)}, its unwritten class "
      f"{sum(1 for k in missed if k['class'] == UNWRITTEN)}")
print(f"found in contestable class "
      f"{sum(1 for k in found if k['class'] in CONTESTABLE)}"
      f", turned to disagreement {len(dis)}")
print("disagreement classes: " +
      ", ".join(f"{s} {sum(1 for k in dis if k['class'] == s)}"
                for s in CONTESTABLE))
print()
print(f"{'rule':<20s} {'round':>5s} {'wait':>7s} {'note-round':>10s} "
      f"{'disagreement note-round':>24s} {'closed':>6s} {'deferred':>8s} "
      f"{'left in code':>13s}")
for name, rule in RULES.items():
    needed = [rule["exchanges"] or CAP + 1 if k["disagreement"]
              else ACTIONABLE for k in found]
    rnd, wait, nr, open_ = cycle(needed)
    dis_nr = sum(min(g, rnd) for g, k in zip(needed, found)
                 if k["disagreement"])
    deferred = len(dis) if rule["defers"] else 0
    closed = len(found) - open_ - deferred
    print(f"{name:<20s} {rnd:5d} {wait + rule['owner']:7d} {nr:10d} "
          f"{dis_nr:24d} {closed:6d} {deferred:8d} "
          f"{open_ + deferred + len(missed):13d}")
```

```
found 22, missed 2, its unwritten class 2
found in contestable class 9, turned to disagreement 6
disagreement classes: interface 1, implementation 5

rule                 round    wait note-round  disagreement note-round closed deferred  left in code
no rule                  6      36         68                       36     16        0             8
tied to criterion        3      18         50                       18     22        0             2
owner decides            3      24         50                       18     22        0             2
separate work item       2      12         44                       12     16        6             8
```

#### What an Unresolved Finding Carries

Of the twenty-two findings, **9** are in the contestable class, and **6** of those turn
into disagreement: 1 on interface, 5 on implementation. The remaining **16** notes are
missing-information notes and, in the previous lesson's baseline regime, close in 2
exchanges.

With no rule, the table collapses. The process climbs to **6** review rounds and **36**
units of wait; in the end, **16** notes close, **6** disagreements stay open, and defects
left in the code are **8**. Of the eight, **2** are the class no axis searches for —
unwritten requirement, a defect review could never see. The remaining **6** were seen,
written, argued, and stayed in the code because they were never **resolved**.

The disagreement note-round column gives where the cost comes from in a single number:
**36**. Each of the six disagreements stayed open across all six review rounds; 6 × 6 =
36. Of the total load's **68**, **36** — more than half — is repetition that produced
nothing. All the missing-information notes had already closed by the second round; the
four rounds from the third to the sixth were spent on the six disagreements alone.

This pair of numbers is familiar. The shared fixture's worst axis configuration also
produced **6** review rounds / **36** wait. Two different causes, the same bill: there, a
narrow axis list kept defects from being seen in the first round; here, seen defects
cannot be resolved. In both cases, what lengthens the review round count is **the amount
of work that does not close in the first round**.

This match also carries a warning about reading metrics. Looked at from outside, two
processes show the same two numbers; which one comes from a narrow axis list and which
from an unresolved argument is not written in the table itself. The same bill requires
two different fixes.

#### Two Ways to Resolve

Tied to criterion is the most balanced row: **3** review rounds, **18** units of wait,
**22** closed, and **2** left in the code. Both are unwritten requirement — the class
review can never reach. This is the one rule that delivers everything review is able to
find.

Its cost is negative relative to no rule: it falls from **36** units to **18**. A
resolution rule does not raise wait, it **lowers** it. This is the measurement's most
practical conclusion — disagreement resolution is not an extra process, it is what
removes the process's most expensive line item.

Owner decides gives the same result: **22** closed, **2** left in the code. The
difference is in the wait column: **24** units, **6** more than tied to criterion. The
gap is exactly one review round, and its cause is structural — the owner is not inside
the review and has to be brought in. If this rule is reserved for disagreements with no
criterion or with conflicting criteria, the extra 6 units get paid only where they are
actually needed.

#### Which Criterion Binds

Tying to a criterion works because of a quality of the criterion: it is **independent of
either side's preference** and it is **written in advance**. If either condition is
missing, the argument turns into an argument about the criterion, and the table's **18**
units do not come back.

Binding criteria are well known. The **interface contract** closes backward-compatibility
questions; what the contract says is written before the argument, not during it. A
**measured number** closes performance disagreements: which of two designs is faster is
not a preference, it is a measurement result. Whether a **test passes** closes behavior
disagreements. A rule written in the documentation — naming convention, layer boundary,
dependency direction — closes structural disagreements.

Non-binding criteria are also well known, and what they share is being produced **at the
moment of the argument**: personal habit, unmeasurable adjectives like "cleaner" or "more
readable," and the observation that it was done differently elsewhere. Clean code
principles were established in the Clean Code course as a list of rules there; for them
to close a disagreement, that list has to be **written and binding** on the team. If it
is not written, it is not a criterion, it is a second preference.

A practical conclusion follows: a criterion is written **before** the argument, not
during it. The measurement's **36**-unit bill is the bill for a missing criterion — and
that gap cannot be closed from inside the review.

#### Deferral Is a Decision

Separate work item is the table's fastest row: **2** review rounds, **12** units of
wait, total load **44** note-rounds. These numbers are not a coincidence; a process where
disagreements never get argued gives the same numbers as the previous lesson's
actionable regime.

The price paid is in the two right columns: **6** deferred and **8** defects left in the
code. Deferred notes are not lost; they stand in their own work items — but once that
change merges, the code carries those **6** defects on it. In terms of left-in-code
count, deferral and no rule give the **same** result: 8 in both. The difference is that
deferral does this with **24** fewer units of wait and leaves a record.

This is why deferral is not an escape but a **decision** — but only when the record is
actually kept. Deferral done without opening a work item lands in the same place as no
rule, in the measurement: 6 defects in the code, no record anywhere. Deferral's limit
follows from this too: the disagreement itself can be deferred, but if the defect
underneath the disagreement is a **bug**, it cannot be. Which form a design takes can
wait; dividing by zero on empty input cannot.

#### Summary

- A design disagreement is separated from a missing-information note mechanically: if
  either side holds the information that would close it, the note is missing-information;
  if neither does, it is disagreement.
- Disagreement shows up on the interface and implementation axes; in the measurement,
  **6** of the **9** findings in the contestable class turn into disagreement.
- With no rule, the process costs **6** review rounds / **36** units of wait, **16**
  notes close, and **8** defects stay in the code. Of the total load's **68** note-rounds,
  **36** are nothing but the six disagreements repeating.
- Tied to criterion closes all **22** notes in **3** rounds / **18** units; the **2** left
  in the code are the unwritten requirement class no axis searches for. Owner decides
  gives the same result but costs **24** units, since the owner is outside the review.
- Separate work item is the fastest row (**2** rounds / **12** units) but leaves **8**
  defects in the code — the same number as no rule, with far less wait and a record.

#### Next Step

Three lessons produced the same two numbers for three separate reasons: review round and
wait. Change size set the chunks read, note style set the closing speed, the resolution
rule set whether it closes at all. The next lesson takes these two numbers directly as
**metrics**: what axis assignment itself does to review round and wait, how the health of
the review queue is read, and why these metrics are not reported per person.

### Review Metrics

Source: https://academia.sh/en/courses/code-review/review-metrics

Three reviewers on the same axis produce 6 review rounds, 36 units of wait, and 3 remaining defects; one reviewer on five axes gives 2 rounds, 12 units, and 0 remaining, and the same gap turns into 2 versus 15 rounds of staying open in the review queue.

Three lessons arrived at the same two numbers. Change size set the chunks read and what
was found; note style set how fast the found closed; the resolution rule set whether it
closed at all. In all three, the result was a **review round** count and an amount of
**wait**.

This lesson takes those two numbers directly as **metrics** and adds a third: what
review does not to a single change but to the **review queue**. What is asked is this —
what does each of these three numbers measure, which decision does it support, and what
does it hide when read alone.

#### Three Metrics

**Wait time** is the span from the moment a change enters review to the moment it
closes. In the shared fixture, every review round adds 6 units to wait; wait is a direct
function of review round count.

**Review round count** is how many times a change goes back and forth before it closes.
It has one source, and the previous three lessons showed it three separate ways: **work
that does not close in the first round**. A defect not seen in the first round, a
closing criterion not written, and an argument not resolved all land in the same place.

**Review queue health** is a metric not of a single change but of all changes open at the
same time. A team's review capacity is limited: only a certain number of changes can be
reviewed at once. As every change takes more review rounds, capacity stays tied up longer
and changes coming in behind it wait. Queue health shows whether this backlog is forming.

#### Why Metrics Are Not Reported Per Person

These three metrics are the process's metrics, and they are not reported per person,
because what produces every row of the table is not people's effort but the **axis
assignment** — three reviewers producing 6 review rounds happens even though all three do
their job flawlessly.

The measurement's second row proves this. There are three reviewers, all three find
every defect on the axis they are responsible for, and the result is **6** of 24
defects. If a per-person number were produced, all three reviewers would look weak; yet
the only thing that needs to change is that all three are looking at the same axis. When
a metric is tied to the wrong subject, the fix goes to the wrong place too.

A second justification comes from the previous lesson. When note style changed, the
number of defects found never changed — it stayed fixed at **22**. That is, note count
and defects-found count are independent of each other. In a system that reports note
count per person, what grows is note count, not defects found.

The measurement's assumptions:

- **RC20** — The shared fixture does not change: 600 lines, 12 chunks, 24 defects, fixed
  class distribution. The only thing that changes is the **axis assignment**.
- **RC21** — Four assignments are measured: one reviewer on all axes; three reviewers on
  the same axis; three reviewers on separate axes; five reviewers on five axes. Every
  reviewer's attention is **12** chunks.
- **RC22** — Found defects are fixed in every review round; the remaining defect carries
  to the next round, and each round clears half of what was found. A change can stay
  open for at most **6** review rounds.
- **RC23** — One review round adds **6 units** to wait, independent of assignment.
- **RC24** — In the review queue, one change enters per round; **12** changes enter in
  total. The team's capacity is **3** simultaneous reviews.
- **RC25** — All changes in the queue are reviewed with the same assignment; this is why
  every one of them takes the same review round count.
- **RC26** — The queue measurement is tracked across **60** rounds; this span is enough
  for all changes to close in all four of the four assignments.
- **RC27** — The set's resolution is **1/24 = 0.042** at 24 defects; wait and review
  round are counted per unit.

#### Measurement

```python
"""Review metrics: found, review round, wait, and queue health.

Part 1 - what four axis assignments find and miss.
Part 2 - the same assignments' review round, wait, and remaining defects.
Part 3 - what the same rounds do to the review queue.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION = 12
CHUNK = 50
CAPACITY = 3          # changes reviewable at the same time
INCOMING = 12          # changes entering the queue during the measurement


def rng(seed):
    d = seed % 2147483646 + 1

    def r(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return r


def change(lines, defect_count=24, seed=SEED):
    r, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[r(6)],
                         "chunk": r(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def round_cycle(found, defect_count, wait_per_round=6):
    """Each round fixes what is found; the remainder carries to the next round."""
    remaining, rnd, wait = defect_count - len(found), 1, wait_per_round
    while remaining > 0 and rnd < 6:
        rnd += 1
        wait += wait_per_round
        remaining -= max(1, len(found) // 2)
    return rnd, wait, max(0, remaining)


def queue(rnd, incoming=INCOMING, capacity=CAPACITY, duration=60):
    """One change enters per round; at most `capacity` are reviewed at once."""
    pending, working, done, snapshot = [], {}, [], 0
    for t in range(1, duration + 1):
        if t <= incoming:
            pending.append(t)
        for no in [n for n, b in working.items() if b == t]:
            done.append((no, t))
            del working[no]
        while pending and len(working) < capacity:
            working[pending.pop(0)] = t + rnd
        if t == incoming:
            snapshot = len(pending) + len(working)
    return snapshot, max(b - g for g, b in done), max(b for _, b in done)


FULL = set(AXES)
ASSIGNMENTS = {
    "one reviewer, all axes": [FULL],
    "three reviewers, same axis": [{"implementation"}] * 3,
    "three reviewers, separate axes": [{"interface"}, {"implementation"},
                                        {"test", "documentation"}],
    "five reviewers, five axes": [{a} for a in AXES],
}
d = change(600)

print(f"{'assignment':<32s} {'reviewers':>9s} {'found':>7s} {'missed':>6s} "
      f"{'unwritten missed':>17s}")
for name, assignment in ASSIGNMENTS.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    unw = sum(1 for k in missed if k["class"] == UNWRITTEN)
    print(f"{name:<32s} {len(assignment):9d} {len(b):7d} {len(missed):6d} {unw:17d}")

print()
print(f"{'assignment':<32s} {'round':>5s} {'wait':>6s} {'remaining':>10s}")
for name, assignment in ASSIGNMENTS.items():
    rnd, wait, remaining = round_cycle(panel(d, assignment), len(d["defects"]))
    print(f"{name:<32s} {rnd:5d} {wait:6d} {remaining:10d}")

print()
print(f"review queue: one change enters per round, {INCOMING} changes, "
      f"capacity {CAPACITY}")
print(f"{'assignment':<32s} {'round':>5s} {'open at round 12':>17s} "
      f"{'longest open':>13s} {'last close':>11s}")
for name, assignment in ASSIGNMENTS.items():
    rnd = round_cycle(panel(d, assignment), len(d["defects"]))[0]
    snapshot, longest, last = queue(rnd)
    print(f"{name:<32s} {rnd:5d} {snapshot:17d} {longest:13d} {last:11d}")
```

```
assignment                       reviewers   found missed  unwritten missed
one reviewer, all axes                   1      22      2                 2
three reviewers, same axis               3       6     18                 2
three reviewers, separate axes           3      15      9                 2
five reviewers, five axes                5      22      2                 2

assignment                       round   wait  remaining
one reviewer, all axes               2     12          0
three reviewers, same axis           6     36          3
three reviewers, separate axes       3     18          0
five reviewers, five axes            2     12          0

review queue: one change enters per round, 12 changes, capacity 3
assignment                       round  open at round 12  longest open  last close
one reviewer, all axes               2                 2             2          14
three reviewers, same axis           6                 9            15          27
three reviewers, separate axes       3                 3             3          15
five reviewers, five axes            2                 2             2          14
```

#### What Lengthens the Round

Reading the first two tables side by side gives the topic's conclusion. When three
reviewers work the same axis, **6** defects are found, the process costs **6** review
rounds and **36** units of wait, and **3** defects still remain in the end. When one
reviewer looks at five axes, **22** defects are found, the process costs **2** review
rounds and **12** units, remaining is **0**.

The comparison matters because the second row has **three times** as many reviewers.
Three people produce six times the wait and end up unable to deliver three defects; one
person does the same job with a third of the wait, and flawlessly. **What lengthens the
review queue is not reviewer count, it is what goes unseen in the first round.**

The link is straightforward arithmetic. If little is found in the first round, a lot of
defects remain; the remaining defects clear in later rounds at only half the speed of
what was found; so finding little means both more review rounds and defects left at the
limit. When three reviewers spread across separate axes, **15** are found and the process
closes at **3** rounds / **18** units: same three people, only the axis spread changed.

The rightmost missed column is **2** in every row. All four of the four configurations
miss the two unwritten requirement defects. This number does not fall by adding
reviewers or by adding axes — no axis searches for what was never written. In reading the
metrics, this row serves as a **floor**: **2** of 24 defects sit where review can never
reach, and a process's target is **22**. A reading that does not know the floor calls
every review incomplete and sets an unreachable number as the goal; a metric's ceiling is
the ceiling of the process it measures.

#### The Health of the Queue

The third table shows what the same gap does outside a single change. The team is the
same team, capacity is **3** simultaneous reviews, one change enters per round.

When review round count is **2**, the queue is healthy: at round twelve only **2**
changes are open, the longest open stays **2** rounds, and the last change closes at
round **14**. When review round count is **3**, the system sits exactly at the limit —
capacity 3, service time 3 — and the open count settles at **3**.

When review round count is **6**, the queue breaks down. At round twelve, **9** changes
are open; the longest open climbs from **2** rounds to **15**; the last change closes at
round **27**. On a single change the gap was 2 versus 6; in the queue it is **2 versus
15**. Once capacity runs out, delay does not just add — it **compounds**.

A simple rule of queue health follows from this: **the queue grows once review round
count exceeds capacity.** This has nothing to do with how many people are assigned to
review, and everything to do with how many rounds a review takes to close — and the
previous three lessons each showed, one by one, where that number comes from.

The arithmetic of the backlog is simple too. With capacity 3 and service time 6, the
system can close at most **0.5** changes per round, while **1** change arrives per
round. On top of that, the first closing cannot happen before round **7**; of the 12
changes arrived by round twelve, only **3** have closed and **9** are open. Stopping the
inflow does not erase this backlog right away: the last-arriving change reaches its turn
at round **21** and closes at round **27**. **The queue does not recover at the speed it
broke down.**

#### What the Three Metrics Hide Alone

No metric is read alone, because each one can take the same value for two separate
reasons.

**Short wait** can be a good sign or the sign of a change that was never read. In the
change size lesson, a 2400-line review also got approved in a single round; defects
found was **2**. Read alone, wait makes this review look exemplary.

**Few review rounds** comes either from finding a lot in the first round or from
finding nothing at all. What separates the two is the second table's last column:
remaining defects. In the disagreement lesson, the deferral rule gave **2** rounds /
**12** units — the same as the table's best row — but left **8** defects in the code.

**A healthy queue** comes either from reviews that close fast or from changes that never
enter the queue at all. The remedy for all three is the same: metrics are read
**together with found and missed defects**. This is why every table in this topic has
carried both sides.

#### Which Metric Supports Which Decision

A metric's value is only as good as the decision it supports. The three metrics look at
three separate decisions and do not substitute for one another.

**Review round count** supports the axis-assignment decision. The second table gives
this directly: the same three people produce **6** rounds on the same axis, **3** rounds
on separate axes. When round count rises, the question to ask is not "who is slow," it is
**which axis is going unlooked-at**.

**Wait time** supports the change-size decision. Wait is six times the review round
count, and what sets review round count is what gets found in the first round; what most
reduces the first round's find is the **unread chunk**. Lengthening wait is, most often,
the sign of a change sent to review that does not fit inside attention.

**Queue health** supports the capacity decision. The third table shows the backlog
starting exactly when service time exceeds capacity. The decision here runs two ways:
either review round count is lowered, or simultaneous review capacity is raised. The two
do not cost the same — the first is paid with axis spread and change size, the second
with time taken from other work.

Read together, the table reduces to a single sentence: **every fix that raises what is
found also fixes wait and the queue.** The measurement has no row that contradicts this;
the two configurations that find the most are also the ones with the least wait and the
healthiest queue.

#### Summary

- The three metrics are the process's metrics: wait time, review round count, and review
  queue health. They are not reported per person, because what produces their values is
  the axis assignment — three reviewers producing 6 review rounds happens even though all
  three do their job flawlessly.
- Three reviewers on the same axis find **6** defects, cost **6** rounds / **36** units,
  and leave **3** remaining; one reviewer on five axes finds **22**, costs **2** rounds /
  **12** units, leaves **0**. Three times the reviewers, three times the wait.
- What lengthens the review queue is not reviewer count but **what goes unseen in the
  first round**: finding little produces both more review rounds and defects left at the
  limit.
- In a queue with capacity 3, the longest open is **2** rounds when review round count is
  2, and **15** rounds when it is 6; open changes at round twelve rise from **2** to
  **9**. Once capacity is exceeded, delay compounds.
- All four of the four configurations miss the **2** unwritten requirement defects; this
  is the floor metrics are read against — the most review can ever reach is **22** of 24.
- No metric is read alone: short wait, few review rounds, and a healthy queue can each
  take the same value in both the best and the worst case; what separates them is
  defects found and missed.

#### Next Step

In this topic, review always stayed around a single change: a change arrives, is read,
notes are written, it closes. The third table took the first step outside that circle and
showed changes waiting on one another. But what if the change itself is not a starting
point but an **element inside a longer chain**? The next topic treats review as one stop
on that chain: where the element stands before it reaches review and after it closes, and
what share of the total time review holds.

## Team Flow

The work item's life cycle, writing expectations for outside contribution, code ownership and required reviewers, the repository platform's class-level capabilities, and what a release note and a tag mean to the reader.

### Issue Tracking and Boards

Source: https://academia.sh/en/courses/code-review/issue-tracking-and-boards

The wait a work item spends between statuses is 19 units outside review; the review round varies between 12 and 36 depending on the assignment and its share climbs from 0.387 to 0.655; putting three remaining defects back on the board takes the total from 31 to 105.

Up to this point, review was measured around a single change: how many axes were looked at,
how many chunks were read, how many rounds it went through. The change stood alone in these
measurements — it came from nowhere and went nowhere. In a team, though, a change is not
just a change; **it is one item inside a flow.** It is born as a proposal, enters a queue,
someone takes it on, it is written, reviewed, merged, and closed.

In this chain, review is only one link. This lesson's question is: **how long does the link
itself take, and what is its share of the whole chain?** To know the value of shortening a
review round, it first has to be known what the round stands inside of. If the rest of the
chain is longer than review, cutting the review round in half changes little in the total.

#### The Work Item and Its Statuses

A **work item** is the smallest unit of work a team tracks: a bug report, a request, a
task. An item has a single identity, a single owner, and at any moment a single
**status**. A **board** is the surface these items are laid out on according to their
status; every column is a status, and an item moves left to right.

A board is not a record tool, it is a **visibility** tool. The same information could stand
in a list too; what a board adds is that how many items have piled up in which status can
be read at a glance. A column swelling says that exit from that status is slower than entry
into it.

```text
# example board dump — not executed

proposal      queued        in progress   in review     merged
──────────    ──────────    ──────────    ──────────    ──────────
#412          #398          #377          #361          #350
#411          #395          #374          #359
              #391                        #356
              #388                        #352
                                          #349
```

The status set itself is a design decision, and it has a single criterion: **a status
makes sense when its exit depends on someone else's action.** `in review` is a status
because there the item is waiting not on the author's action but the reviewer's. A column
whose exit is always in the same person's hands is not a status, it is that person's
personal list; it takes up room on the board and reports no wait. The same criterion
limits column count too: a new status is added only if there is a new handoff point.

In this dump, the `in review` column carries five items, `in progress` two. The number
itself is not a defect; the defect is the column's **emptying speed** falling below its
filling speed. If five items have piled up there, the **review queue** is lengthening, and
a lengthening queue writes wait onto every one of them.

#### Wait Stands Not in a Status but in a Transition

This is the point where a board readily misleads. Not all of the time an item spends
standing in the `in review` column is review; part of it is **waiting for the reviewer to
look at the item.** What has to be measured is not how long the item stands in one column,
but how long it takes to **move** from one column to the next.

This distinction defines the measurement. There are five transitions in the chain, and
four of them are outside review: a proposal enters the queue, someone from the queue takes
it on, writing finishes and the item goes out to review, review finishes and the item
closes. The fifth transition — from `in review` status to `merged` status — is review
itself and it is **not fixed**: it depends on how many rounds it goes through.

**Wait time** is counted here without a unit. Not hours or days, but a comparable unit;
the goal is not to read the absolute duration but the ratio between links to each other.
These metrics are metrics **of the process**: how long an item waits is the result not of
the person looking at that item, but of the shape of the chain.

#### Column Length and Wait Are Not the Same Thing

The five items standing in the board's `in review` column are an **accumulation**; the
wait measured at the transition is a **rate**. The two do not give the same number. A
five-item column means one unit of wait if the exit rate is five items per unit; it means
five units of wait if the exit rate is one item per unit. Someone looking at the column
cannot tell the two states apart, because a board shows accumulation and does not show
rate.

The direct consequence of this is: **shortening the column does not shorten the wait.**
Moving two of the five items to another column lowers the accumulation and relieves the
board; it does not touch how long the transition takes. The only thing that shortens the
wait is speeding up the transition itself, and in the review link this means lowering the
round count.

One more distinction is needed. A single work item can carry more than one change; then
the review link runs more than once per item, and the chain's total grows accordingly.
This measurement counts **one** change per item and gives its numbers under that
assumption. If an item is split into three changes, the transitions outside review are
paid once, the review link three times — its share grows in that proportion. This says
that splitting items is not free: splitting makes review easier but raises review's share
of the chain.

#### Review's Place in the Chain

What determines the length of the review link? The course's fifth reading already said
this: **what lengthens the round and the wait is what could not be seen in the first
round.** What is found in a round gets fixed; what is not found carries over to the next
round, and every round writes one more wait onto the chain. If axis diversity is low, the
first round finds little, round count rises, the link lengthens.

The chain's total is therefore fed from two sources. One is the **fixed** part — the
proposal, queue, writing, and closing transitions; these are independent of how review is
done. The other is the **variable** part, and it is the direct result of axis assignment.
The measurement counts these two separately and gives their ratio.

There is one more defect: the **remaining defect** once the rounds are over. A remaining
defect does not mean review is finished and the item is closed; the closed item later gets
**reopened** because of that defect and enters the chain a second time. This second pass
does not start with the proposal — the item has already been proposed — but it repeats
everything starting from the queue.

The measurement's assumptions:

- **TF1** — The shared setup's 600-line change and 24 defects are used as they are; the
  class distribution and the oracle are the course's constant.
- **TF2** — The wait of the four non-review transitions is fixed and independent of the
  assignment: **5**, **9**, **2**, and **3** units respectively. These values represent
  the chain's shape, not a team's measurement.
- **TF3** — The review link's wait comes from the shared setup's round cycle: every round
  writes **6 units** and round count is the result of the defect count found in the first
  round.
- **TF4** — A found defect is considered fixed in that round; a remaining defect carries
  over to the next round. Six rounds is the upper bound, and an assignment resting on that
  bound leaves a defect remaining.
- **TF5** — A remaining defect reopens the item **once** after it closes. The second pass
  starts from the `queued` status; the proposal transition is not repeated.
- **TF6** — The metric is reported per item, not per person. No number in the chain can be
  read as a role's performance; all of it is the result of the chain's shape.
- **TF7** — The set's resolution at 24 defects is **1/24**; because the wait unit is **6**
  per round, the smallest measurable difference in the chain is **6** units.

#### Measurement

```python
"""Work item chain: wait between statuses and review's share.

Part 1 - found, missed, and review wait in four assignments.
Part 2 - the chain's total, review's share, and the cost of reopening.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION, CHUNK = 12, 50
# Transitions outside review: wait read off the board.
OUTSIDE = (("proposal", "queued", 5), ("queued", "in progress", 9),
           ("in progress", "in review", 2), ("merged", "closed", 3))


def rng(seed):
    d = seed % 2147483646 + 1

    def draw(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def review_rounds(found, defect_count, wait_per_round=6):
    remaining, rounds, wait = defect_count - len(found), 1, wait_per_round
    while remaining > 0 and rounds < 6:
        rounds += 1
        wait += wait_per_round
        remaining -= max(1, len(found) // 2)
    return rounds, wait, max(0, remaining)


FULL = set(AXES)
ASSIGNMENTS = {
    "single reviewer, all axes": [FULL],
    "three reviewers, same axis": [{"implementation"}] * 3,
    "three reviewers, separate axes": [{"interface"}, {"implementation"},
                                        {"test", "documentation"}],
    "five reviewers, five axes": [{a} for a in AXES],
}
OUTSIDE_TOTAL = sum(b for _, _, b in OUTSIDE)
d = change(600)

print(f"work item 1 | change {d['lines']} lines, {d['chunks']} chunks, "
      f"defects {len(d['defects'])} | wait outside review {OUTSIDE_TOTAL}")
print("transitions outside review: "
      + ", ".join(f"{a}→{b} {n}" for a, b, n in OUTSIDE))
print()
print(f"{'assignment':<32s} {'found':>7s} {'missed':>5s} {'unwritten':>10s} "
      f"{'rounds':>3s} {'review':>8s} {'remaining':>5s}")
for name, assignment in ASSIGNMENTS.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    unwritten = sum(1 for k in missed if k["class"] == UNWRITTEN)
    rounds, wait, remaining = review_rounds(b, len(d["defects"]))
    print(f"{name:<32s} {len(b):7d} {len(missed):5d} {unwritten:10d} "
          f"{rounds:3d} {wait:8d} {remaining:5d}")

print()
print(f"{'assignment':<32s} {'chain':>6s} {'review share':>13s} "
      f"{'reopening':>14s} {'total':>6s}")
for name, assignment in ASSIGNMENTS.items():
    b = panel(d, assignment)
    rounds, wait, remaining = review_rounds(b, len(d["defects"]))
    chain = OUTSIDE_TOTAL + wait
    # A remaining defect puts the item back on the board: a second round starting from queued.
    reopened = 0 if remaining == 0 else chain - OUTSIDE[0][2]
    print(f"{name:<32s} {chain:6d} {wait / chain:13.3f} "
          f"{reopened:14d} {chain + reopened:6d}")
```

```
work item 1 | change 600 lines, 12 chunks, defects 24 | wait outside review 19
transitions outside review: proposal→queued 5, queued→in progress 9, in progress→in review 2, merged→closed 3

assignment                         found missed  unwritten rounds   review remaining
single reviewer, all axes             22     2          2   2       12     0
three reviewers, same axis             6    18          2   6       36     3
three reviewers, separate axes        15     9          2   3       18     0
five reviewers, five axes             22     2          2   2       12     0

assignment                        chain  review share      reopening  total
single reviewer, all axes            31         0.387              0     31
three reviewers, same axis           55         0.655             50    105
three reviewers, separate axes       37         0.486              0     37
five reviewers, five axes            31         0.387              0     31
```

#### Reading the Chain

The wait outside review is **19** units, and it is the same in all four rows. The only
thing that changes is the review link, and it swings between **12** and **36**. The
chain's total accordingly swings between **31** and **55**: the worst assignment produces
a chain **77%** longer than the best one.

Review's share is in the right column. In the best assignment, **0.387** of the chain is
review; that is, roughly two-thirds of the item's wait happens outside review. In the
worst assignment, the share climbs to **0.655** — review is now more than half the chain.
The gap between these two numbers gives a result: **review is not a fixed-cost link, it is
the chain's most volatile link.**

A practical ordering follows from this. The `queued→in progress` transition is the
longest non-review link at **9** units, and in the good assignment it alone is about
three-quarters of the review link. In the chain's best state, the place to shorten is not
review, it is the queue. But in the bad assignment, review is **36** units — four times
the queue — and there, shortening the queue contributes little to the total. **Which link
to shorten depends on which assignment it is**, and this is told not by the column length
on the board but by the transition's wait.

The table on the left also gives found and missed for the same rows, and the two should be
read together. The **three reviewers, same axis** row finds **6** and misses **18**; the
row with the longest chain is also the row that finds the least. **Single reviewer, all
axes** finds **22**, misses **2**, and has the shortest chain. So a long chain does not buy
a better review — **the source of the length is exactly what could not be found.**

Among the missed, every row has **2** **unwritten requirement** defects, and this number
does not change across the four rows. Neither the chain's shape, nor the assignment's
diversity, nor round count finds those two defects. Issue tracking is not the remedy for
this class; however well an item is tracked on a board, the absence of what was never
submitted is invisible to review.

#### Reopening

The second table's last two columns give the chain's real cost. In the **three reviewers,
same axis** assignment, **3** defects still stand after six rounds. The item is closed;
the defects are not. A remaining defect eventually surfaces somewhere and the item is
**reopened**.

Reopening adds **50** units to the chain: every transition besides the proposal runs one
more time. The total climbs from **55** to **105**. The same change closes in **31** units
in the best assignment; the gap here is **74** units, more than three times as much.

How this number is read matters. Reopening is not a delay, it is a **deferred wait**: the
wait not paid on the chain's first pass is paid with interest on the second pass. Someone
looking at the board sees no problem on the first pass — the item is closed, the column is
empty, the numbers are clean. The problem appears on the board when the item comes back,
and at that moment it is no longer associated with the original review's measurement.

This is why a closed-item count alone is not enough to make a board readable. **The number
of reopened items has to stand as its own column or its own count;** otherwise the
chain's most expensive behavior is invisible anywhere. A closing count reports two
different processes as the same number for as long as it does not separate what closed on
the first pass from what closed on the second.

#### Summary

- A **work item** is the smallest unit of work in a flow; a **board** lays items out by
  status, and wait accumulates not in the statuses but in the **transitions** between
  them.
- The chain's wait outside review is fixed at **19** units; the review link varies between
  **12** and **36** depending on the assignment, carrying the chain's total from **31**
  to **55**.
- Review's share of the chain is between **0.387** and **0.655**; review is not a
  fixed-cost link, it is the chain's most volatile link, and which link to shorten
  depends on the assignment.
- A long chain does not buy a better review: the **36**-unit link finds **6** defects and
  misses **18**, the **12**-unit link finds **22** and misses **2**.
- The **3** defects remaining after six rounds reopen the item and add **50** more units
  to the chain; the total climbs to **105**. A closing count hides this second pass unless
  it is counted separately.
- In all four of the four assignments, **2** **unwritten requirement** defects are missed;
  issue tracking does not find this class.

#### Next Step

This measurement assumed the item's author knew what to submit: the change entering the
chain was a change written while trying to meet the team's expectations. So where does the
author learn those expectations from? Someone inside the team has learned them across
rounds; someone coming from outside has not. An unwritten expectation comes back as a
finding in review's first round, and every finding writes one more round. The next lesson
measures the written form of these expectations: which classes does a **contribution
guide** close before review, how many rounds does closing each class cut, and is it really
the guide that is the one mechanism able to touch a class no axis searches for?

### Contribution Guidelines

Source: https://academia.sh/en/courses/code-review/contribution-guidelines

A written expectation works before review: in a narrow review, as the guide's scope grows, rounds drop from 6 to 5, 4, 3, and 2; in a wide review, rounds stay fixed at 2; and a guide requiring a requirement statement drops the missed count in the 24-defect set to 0 for the first time.

The previous lesson measured the work item's chain and showed that the review link's
length comes from round count. What lengthens the round is what could not be seen in the
first round; so is there a kind of finding that does not need to be seen in the first
round at all? There is: **if the author already knows, there is no finding at all.**

Someone working inside a team has already learned most expectations in earlier rounds.
Someone coming from outside has not, and relearns the same expectation in their own
rounds. A **contribution guide** is the written text that carries this learning to before
the round. This lesson's question is not what to write in the guide — it is **how many
rounds every written expectation cuts**, and whether there is a place the guide can never
touch at all.

#### Where an Expectation Is Learned

An expectation can be learned from three places. **From the guide:** the author reads it
before submitting the change and takes heed. **From a round:** the reviewer writes the
finding, the author fixes it, the change is resubmitted. **From nowhere:** the expectation
occurs to no one and the change merges as it is.

The three do not cost the same. An expectation learned from the guide spends zero rounds;
because the finding is never born, it never enters the review queue at all. An expectation
learned from a round spends at least one round, and every round writes wait onto the
chain. An expectation never learned spends no round but stays as a defect.

The guide's measurable effect is therefore sought in **round count**, not in the number of
defects found. The guide does not open review's eye; it lowers the defects coming into
review. This distinction is the mirror image of the course's second claim: adding an axis
grows **what is found**, the guide shrinks **what comes in**.

The third path — the expectation never learned — has a counterpart in this measurement,
and it is the **unwritten requirement** class. An expectation no one wrote, no one asked,
appears on no axis and is spoken of in no round. The guide's most interesting question is
right here: can a written text touch a class that is never spoken of at all?

#### What Can Be Written in a Guide

The guide is not a style text, it is a list of **acceptance conditions**. Every item names
an expectation that, when unmet, gives birth to a review round. A well-written item states
something the author can verify on their own before submitting.

```text
# example contribution guide excerpt — not executed

## Before submitting
- Style: the formatting setting at the repository root must be
  applied; a manual style fix is submitted as a separate change.
- Test: every changed public behavior must have at least one
  verifying test; a test that only repeats the call does not count.
- Documentation: if a public interface changed, the relevant
  document is updated in the same change.
- Requirement statement: the requirement the change meets is
  written in the description field with the work item number and
  a single sentence; if there is an unmet requirement, that is
  also written.

## Review
- A change meets a single requirement.
- A change submitted with an empty description field is not taken
  into review.
```

How items are written determines measurability. "Tests should be meaningful" is not an
item; the author cannot check it before submitting and the reviewer cannot derive a
finding from it. "Every changed public behavior must have at least one verifying test" is
a checkable condition: even if perspective changes, its answer is yes or no. The same
difference shows up in the second section too — "a change submitted with an empty
description field is not taken into review" is a gate and it is enforceable; "descriptions
should be adequate" is a wish.

The first three of the four items are readily recognized: style, test, and documentation
are three of the five review looks at. The fourth item is different, and it is the
measurement's real question. The **requirement statement** asks the author to write what
the change is supposed to do. Review looks at what was written; the guide widens the
surface review can look at by forcing the author **to write the requirement itself.**

The measurement's assumptions:

- **TF8** — The shared setup's 600-line change and 24 defects are used as they are; the
  class distribution and the oracle are the course's constant. `change` is not modified.
- **TF9** — The guide works **before** review: defects in its scope are resolved by the
  author before submitting and never enter review at all. The guide is not an axis; it
  does not find defects, it prevents them from being born.
- **TF10** — The guide's scope is a class list. Four scopes are tried, and each adds one
  class on top of the previous one; the fifth adds the **unwritten requirement** class.
- **TF11** — **All** defects of every class in scope are resolved. This is an upper
  bound; a partially followed guide's effect stays below these numbers.
- **TF12** — Review runs under two assignments: a single reviewer looking at five axes,
  and three reviewers looking at the same axis. Attention is fixed at **12 chunks**, and
  600 lines is 12 chunks; no chunk goes unread.
- **TF13** — The round cycle comes from the shared setup and writes **6 units** of wait
  per round. The round's input is no longer 24, it is the defect count that passes
  through the guide.
- **TF14** — The set's resolution at 24 defects is **1/24**; the smallest measurable
  difference at the round scale is **1 round**, that is, **6 units** of wait.

#### Measurement

```python
"""Contribution guide: the review round a written expectation closes.

The guide works before review: defects in its scope are resolved by the
author before submitting. The change and the class list do not change.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION, CHUNK = 12, 50


def rng(seed):
    d = seed % 2147483646 + 1

    def draw(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def past_guide(d, scope):
    """Defects in the guide's scope are resolved before submitting."""
    return [k for k in d["defects"] if k["class"] not in scope]


def review(defects, chunks, axes, attention=ATTENTION):
    read = set(range(min(attention, chunks)))
    return {k["no"] for k in defects
            if k["class"] in axes and k["chunk"] in read}


def panel(defects, chunks, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(defects, chunks, axes, attention)
    return found


def review_rounds(found, defect_count, wait_per_round=6):
    remaining, rounds, wait = defect_count - len(found), 1, wait_per_round
    while remaining > 0 and rounds < 6:
        rounds += 1
        wait += wait_per_round
        remaining -= max(1, len(found) // 2)
    return rounds, wait, max(0, remaining)


FULL = set(AXES)
GUIDES = {
    "no guide": set(),
    "style written": {"style"},
    "style, test": {"style", "test"},
    "style, test, documentation": {"style", "test", "documentation"},
    "three + requirement statement": {"style", "test", "documentation", UNWRITTEN},
}
ASSIGNMENTS = {"single reviewer, all axes": [FULL],
               "three reviewers, same axis": [{"implementation"}] * 3}
d = change(600)
distribution = {s: sum(1 for k in d["defects"] if k["class"] == s) for s in CLASSES}
print("class distribution: " + ", ".join(f"{s} {n}" for s, n in distribution.items()))

for assignment_name, assignment in ASSIGNMENTS.items():
    print()
    print(assignment_name)
    print(f"{'guide scope':<28s} {'resolved':>8s} {'entering':>8s} "
          f"{'found':>7s} {'missed':>5s} {'unwritten':>10s} {'rounds':>3s} "
          f"{'wait':>7s} {'remaining':>5s}")
    for name, scope in GUIDES.items():
        entering = past_guide(d, scope)
        b = panel(entering, d["chunks"], assignment)
        missed = [k for k in entering if k["no"] not in b]
        unwritten = sum(1 for k in missed if k["class"] == UNWRITTEN)
        rounds, wait, remaining = review_rounds(b, len(entering))
        print(f"{name:<28s} {24 - len(entering):8d} {len(entering):8d} {len(b):7d} "
              f"{len(missed):5d} {unwritten:10d} {rounds:3d} {wait:7d} {remaining:5d}")
```

```
class distribution: interface 3, implementation 6, test 3, documentation 3, style 7, unwritten requirement 2

single reviewer, all axes
guide scope                  resolved entering   found missed  unwritten rounds    wait remaining
no guide                            0       24      22     2          2   2      12     0
style written                       7       17      15     2          2   2      12     0
style, test                        10       14      12     2          2   2      12     0
style, test, documentation         13       11       9     2          2   2      12     0
three + requirement statement       15        9       9     0          0   1       6     0

three reviewers, same axis
guide scope                  resolved entering   found missed  unwritten rounds    wait remaining
no guide                            0       24       6    18          2   6      36     3
style written                       7       17       6    11          2   5      30     0
style, test                        10       14       6     8          2   4      24     0
style, test, documentation         13       11       6     5          2   3      18     0
three + requirement statement       15        9       6     3          0   2      12     0
```

#### Reading the Rounds

The two tables measure the same guides under two separate reviews, and the results are
polar opposites.

**In the narrow review** — three reviewers looking at the same axis — the guide cuts one
round for every class added: **6, 5, 4, 3, 2**. Wait drops from **36** to **12**; two-thirds
of it goes away. The found column, though, stays at **6** in all five rows. The guide did
not get even one more defect found; it only reduced what the rounds had to consume and
ended the loop earlier.

**In the wide review** — a single reviewer looking at all five axes — the first four rows
are the same at **2 rounds / 12 wait**. Even as scope climbs to three classes, round count
does not budge. The reason is on the table's left: in this review, missed is already fixed
at **2** and the loop finishes on the second round; there is no round left to cut. Here the
guide reduces not the round but **the work inside the round** — entering drops from **24**
to **11**, found from **22** to **9**.

The wide review's fifth row breaks this fixedness: round drops to **1**, wait to **6**.
This is the only configuration in the course that closes in one round. The reason reads
from the table — missed is **0**. The loop only moves to a second round if a defect
remains; if none remains, the first round is the approval and the work is done.

A two-sentence conclusion follows from this. **A contribution guide buys rounds where
review is narrow; it reduces work where review is wide.** Both gains are real but they are
not the same thing, and looking at the same number will not show it: the first is read in
the wait column, the second in the entering column.

The gap between the first row's **6 rounds / 36 wait / 3 remaining** and the second row's
**5 rounds / 30 wait / 0 remaining** is also worth noting. Writing a single class into the
guide did not just cut one round; **it also zeroed the remaining defect.** Because the loop
finished before hitting the six-round limit, no unclosed defect was left. The previous
lesson's **50**-unit reopening cost vanishes in this row.

#### The Guide Reaching the Unwritten

The fifth row shows something else for the first time across the course. The **unwritten
requirement** class has stood with **2** missed in every configuration up to now; here,
**0**. The total missed also drops to **0** in the wide review — this is the only row in
the 24-defect set where missed is zeroed.

The reason for this is not that an axis finally looks at that class. The axis list did not
change; no reviewer is looking anywhere new. What changes is **the defect's class.** When
a requirement statement is required, the author writes into the text what the change is
supposed to do; what was unwritten becomes written, and now it stands on a surface review
can look at.

This is the course's fourth reading's only real counterexample, and it does not refute the
reading, it defines its limit. **No axis sees the unwritten** is true; the remedy is not
changing the axis, it is **making the unwritten get written.** A mechanism standing outside
the review process — a guide item — turns a class review cannot see into a class review
can see.

Note the measure's resolution: two defects are **2/24** in the 24-item set. This is twice
the set's smallest unit and is defensible. A one-defect difference would not be defensible
with this set.

#### The Moment the Guide Is Read

The measurement gives an upper bound. **TF11** assumes all defects of every class in scope
are resolved; in reality the compliance rate is below this, and the gains in the table
shrink in the same proportion. If half the style defects were resolved, resolved would be
**3** or **4**, not **7**, and the narrow review's round might stay at **6** instead of
**5**. The guide's value depends less on the content of the text than on **how much of it
is followed.**

What determines the compliance rate is where the guide is read. A text standing at the
repository root, read once, is far from the moment of submitting; when that moment comes,
the item is not remembered. The same item appearing as a checklist on the submission
surface makes the text get read again at every submission. The two carry the same sentence
and do not give the same result.

The second determinant is the item's verifiability. An item the author cannot check on
their own gets skipped; a skipped item does not just lose its own gain, it also weakens
the reading habit for the whole list. This is why lengthening the guide can work in the
opposite direction: in a forty-item list, the share of items followed is lower than in a
four-item list.

The measurement gives a threshold for this. The smallest effect an item shows in the
table is **1 round**, that is, **6 units** of wait. An item that changes neither round nor
entering count in any scope takes up room in the guide and has no measurable payoff.

#### What the Guide Cannot Close

The fifth row drops missed to **3** in the narrow review too, but does not zero it. The
three remaining defects stand in axes other than `implementation` and are outside the
guide's scope: the **interface** class was closed by no guide item in either table.

There is a reason for this. A guide can write expectations the author **can verify on
their own.** Whether a style setting was applied, whether a test exists, whether
documentation was updated — all three can be checked alone before submitting. Whether an
interface decision is correct can only be judged by someone else; it has no form that can
be written into a guide, because it requires not an acceptance condition but **judgment.**

The boundary is therefore sharp: **the guide closes checkable expectations; axes requiring
judgment stay in review.** Padding the guide does not cross this boundary, it only produces
an unread text, and an unread item cuts no round; this row's value, invisible in the table,
is always **0 rounds.**

#### Summary

- A **contribution guide** carries the expectation to before the round: defects in its
  scope never enter review at all. The guide is not an axis; it does not grow what is
  found, it shrinks what comes in.
- In a narrow review, as guide scope grows, round drops as **6, 5, 4, 3, 2** and wait
  falls from **36** to **12**; found stays at **6** in all five rows.
- In a wide review, round is fixed at **2** for the first four rows; there the guide
  reduces not round but work — entering drops from **24** to **11**, found from **22** to
  **9**.
- Writing a single class drops the remaining defect from **3** to **0** in the narrow
  review and eliminates the previous lesson's **50**-unit reopening cost.
- A guide requiring a **requirement statement** drops the **unwritten requirement** missed
  count from **2** to **0** — because the class moves onto a surface review can see, not
  because a new axis was added.
- The guide closes only expectations the author can verify alone; axes requiring
  judgment, like **interface**, stay in review under every scope.

#### Next Step

The guide writes not who does what but **what will be met**. Once the change is
submitted, one more question is born: **who will look?** In some repositories this
question is open; in some it is answered by a rule bound to the file path — a directory
has an owner, and every change touching that directory waits on the owner's approval. The
next lesson measures this rule: do **code ownership** and **required reviewer** grow or
narrow axis diversity? Does raising the required reviewer count to three grow what is
found — or does the course's second claim show up here in reverse?

### Ownership and Approval Rules

Source: https://academia.sh/en/courses/code-review/ownership-and-approval-rules

Raising the required reviewer count to one, two, and three leaves found fixed at 6, rounds at 6, and wait at 36; a single open approval alone finds 22 on the same change, and the course's second claim shows up here in reverse — ownership is not a choice of person, it is a choice of axis.

The previous lesson measured the contribution guide: a written expectation lowers the
defects entering review and buys rounds in a narrow review. The guide writes what will be
met. Once the change is submitted, another question is born and the guide does not answer
it: **who will look?**

In some repositories this question is open; in some it is answered by a rule bound to the
file path. A directory has an **owner**, and every change touching that directory waits on
the owner's approval. This lesson's question is not whether the rule is fair — it is **how
many axes the rule opens** and what the number of axes opened does to defects found.

#### Code Ownership Is a Path Rule

**Code ownership** is a mapping that binds sections of the repository to roles. The rule
text lists paths and roles; when a change is submitted, the paths it touches are looked
at and the corresponding roles are added as **required reviewers**.

```text
# example ownership rule — not executed

# path prefix         required reviewer role
/                      maintenance
/core/                 core-owner
/core/protocol/        protocol-owner
/docs/                 docs-owner
```

Three properties are immediately visible. The rule looks at the **path**, not the
change's content: a typo fix under `/core/` also calls the core owner. The rule applies
the **most specific match**: a change under `/core/protocol/` calls the protocol owner.
And the rule is a **gate**: the change does not merge without the owner's approval.

The rule looking at the path works in two directions. On one hand it is automatic and not
forgotten: no one thinks "who should look at this," the mapping runs on its own. On the
other, the rule **knows the path, not the change.** Of two changes touching the same file,
one might carry an interface decision, the other might add a log line; the rule calls the
same owner for both. The measurement tests exactly this second direction: because the
owner's view is independent of the change, the axis set the rule opens is also independent
of the change.

The need the rule meets is clear. Not everyone knows every section of the repository; the
role that best knows a section's history, its reasoning, and its pitfalls is the one that
best evaluates a change touching it. This relationship between team structure and code
structure was discussed on its institutional side in the Process, Team, and Delivery
course; it is not repeated here. The only thing measured in this lesson is review's own
numbers.

#### The Approval Rule: How Many Approvals and From Whom

An ownership mapping says who gets called; an **approval rule** says how many approvals
are required. The two settings are independent of each other and produce four
combinations: no ownership and a single approval; ownership and a single owner's approval;
ownership and more than one owner's approval; ownership with an open approval added
alongside the owner's approval.

All of these settings are expressed **in number of people**, and the course's measure is
not number of people. A single required reviewer carries one axis; two required reviewers
do not have to carry two axes. Because an ownership rule is written by path, owners of the
same path most often carry **the same viewpoint**: they know the section's implementation.
Raising the approval count in this case adds people, not axes.

The rule has a third setting too, and it stays outside the measurement: **does the
author's own approval count?** When an owner writes to their own section, the rule shows
them as both author and required reviewer. Approval in this case adds no view — the only
view already looking at the change is the one that wrote it. In the measure's language:
self-approval adds **zero axes**. The rule excluding this does not change a row in the
table, but it protects the table's validity; if not excluded, every row would show the
approval count in the first column higher than it really is.

The measurement tests exactly this point. The course's second claim said **adding a
reviewer does not find defects; adding an axis does.** An ownership rule is the cleanest
example of a mechanism that adds reviewers without adding an axis; the claim shows up here
**in reverse.**

The measurement's assumptions:

- **TF15** — The shared setup's 600-line change and 24 defects are used as they are; the
  class distribution and the oracle are the course's constant.
- **TF16** — The axis the owner looks at is **implementation**. Because ownership is
  assigned by path and the path's owner is the role that knows the section's
  implementation, this is the default view.
- **TF17** — More than one owner of the same path carries **the same axis**. This is not
  a claim about a person, it is the result of the rule: the rule chooses the path, the
  path chooses the view.
- **TF18** — An open approval is a review with no axis assignment restriction and it
  looks at all five axes. The number of axes a rule opens is the union of the axes in the
  assignment.
- **TF19** — Attention is fixed at **12 chunks**, and 600 lines is 12 chunks; no chunk
  goes unread. The only thing that changes in this measurement is the axis assignment.
- **TF20** — The round cycle comes from the shared setup and writes **6 units** of wait
  per round. Approval count does not directly change the round; the round is determined
  by the defect count found in the first round.
- **TF21** — The set's resolution at 24 defects is **1/24**; the smallest measurable
  difference at the axis scale is one axis, and axes are not equal in size.

#### Measurement

```python
"""Code ownership and required reviewers: the approval rule's axis diversity.

Part 1 - six approval rules; required reviewer count against axes looked at.
Part 2 - defects standing in axes no owner looks at.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION, CHUNK = 12, 50


def rng(seed):
    d = seed % 2147483646 + 1

    def draw(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def review_rounds(found, defect_count, wait_per_round=6):
    remaining, rounds, wait = defect_count - len(found), 1, wait_per_round
    while remaining > 0 and rounds < 6:
        rounds += 1
        wait += wait_per_round
        remaining -= max(1, len(found) // 2)
    return rounds, wait, max(0, remaining)


FULL = set(AXES)
OWNER = {"implementation"}          # the single axis the owner looks at
RULES = {
    "no ownership, one approval": [FULL],
    "one required reviewer": [OWNER],
    "two required reviewers": [OWNER] * 2,
    "three required reviewers": [OWNER] * 3,
    "required reviewer + open approval": [OWNER, FULL],
    "three owners, separate axes": [{"interface"}, OWNER, {"test", "documentation"}],
}
d = change(600)
distribution = {s: sum(1 for k in d["defects"] if k["class"] == s) for s in CLASSES}
print("class distribution: " + ", ".join(f"{s} {n}" for s, n in distribution.items()))
print()
print(f"{'approval rule':<34s} {'appr':>4s} {'axes':>5s} {'found':>7s} "
      f"{'missed':>5s} {'unwritten':>10s} {'rounds':>3s} {'wait':>7s} "
      f"{'remaining':>5s}")
for name, assignment in RULES.items():
    b = panel(d, assignment)
    missed = [k for k in d["defects"] if k["no"] not in b]
    unwritten = sum(1 for k in missed if k["class"] == UNWRITTEN)
    rounds, wait, remaining = review_rounds(b, len(d["defects"]))
    axes = set().union(*assignment)
    print(f"{name:<34s} {len(assignment):4d} {len(axes):5d} {len(b):7d} "
          f"{len(missed):5d} {unwritten:10d} {rounds:3d} {wait:7d} {remaining:5d}")

print()
print(f"{'approval rule':<34s} {'axes not covered':>16s} {'defects standing there':>17s}")
for name, assignment in RULES.items():
    axes = set().union(*assignment)
    uncovered = [a for a in AXES if a not in axes]
    standing = sum(1 for k in d["defects"] if k["class"] in uncovered)
    print(f"{name:<34s} {len(uncovered):16d} {standing:17d}")
```

```
class distribution: interface 3, implementation 6, test 3, documentation 3, style 7, unwritten requirement 2

approval rule                      appr  axes   found missed  unwritten rounds    wait remaining
no ownership, one approval            1     5      22     2          2   2      12     0
one required reviewer                 1     1       6    18          2   6      36     3
two required reviewers                2     1       6    18          2   6      36     3
three required reviewers              3     1       6    18          2   6      36     3
required reviewer + open approval     2     5      22     2          2   2      12     0
three owners, separate axes           3     4      15     9          2   3      18     0

approval rule                      axes not covered defects standing there
no ownership, one approval                        0                 0
one required reviewer                             4                16
two required reviewers                            4                16
three required reviewers                          4                16
required reviewer + open approval                 0                 0
three owners, separate axes                       1                 7
```

#### The Second Claim in Reverse

The table's second, third, and fourth rows are identical. Approval count rises as **1**,
**2**, **3**; found stands at **6**, missed at **18**, rounds at **6**, wait at **36**, and
remaining at **3**. Triple the approvals does not get even one more defect found.

The course's second claim said, going forward, "adding an axis finds." Here the claim
reads in reverse: **adding a reviewer without adding an axis finds nothing.** Adding
people through an ownership rule is not free of cost either — waiting on three people's
approval produces a real wait; even though it does not show up in round count in the
measurement, the item collects three separate signatures in sequence.

The first row gives the comparison. A review with no ownership rule, single-approval, and
no axis restriction finds **22**, misses **2**, and closes in **2 rounds / 12 wait** —
more than **three times** what the three required owners found, at a third of the wait.
Approval count drops to one and the result improves; because what changed is not the
count, it is the **axis count**: from **1** to **5**.

The fifth row shows the combination that rescues the rule. The required owner stays, and
an approval with no axis restriction is added alongside: found **22**, missed **2**,
rounds **2**, wait **12**. The ownership rule's value is preserved — the role that knows
the section still looks — and the axis gap closes. The rule becomes this: **a required
reviewer is a guarantee of one axis, not a guarantee of coverage.**

The fifth row's approval count is **2**, more than the second row's **1**; despite this,
rounds drop from **6** to **2**, wait from **36** to **12**. Adding an approval shortened
the chain this time — because the added approval brought a new axis. The same operation
working in both directions shows why person count has no place in the measure: approval
count alone is neither a good nor a bad sign, it cannot be read without the axis column
next to it.

The sixth row produces the same result a different way. Three owners, because they own
three separate sections, look at three separate axes: found **15**, rounds **3**, wait
**18**. Three approvals are still three approvals, but the axes are **4** instead of
**1**, and the number changes accordingly. **An ownership rule's quality is not in how
many owners it calls, it is in how many separate axes the owners it calls represent.**

#### The Gap in Ownership

The second table reads the same rows from a different question: how many defects stand in
axes the rule never covers at all?

In single-axis ownership rules, **4** axes are open and **16** defects stand there. These
are not a subset of the missed; **16** of the missed **18** come from here, the remaining
two are the **unwritten requirement** class. So in narrow ownership, nearly all of the
missed stand in axes **no one is responsible for.**

How this number is read matters. The **16** defects are not defects looked at and not
seen; they are defects in axes never looked at at all. The gap between the two determines
where the process should touch: for a defect looked at and not seen, attention or change
size is discussed; for an axis never looked at, **the rule itself** is changed. Applying
the same fix to two different problems solves neither.

In the sixth row, which looks at three separate axes, the open axis drops to **1** and the
defects standing there to **7**. The one open axis is **style**, and there are **7**
defects there — the most crowded class in the set. This does not say that adding an owner
for the style axis to the ownership rule would give the single biggest individual gain;
the crowded class is the class that can be closed the cheapest, and closing it does not
need a person. That discussion belongs to the review axis topic's own lesson.

The last column's fixedness underlines this again: in all six of the six rules, **2**
**unwritten requirement** defects are missed. However broadly the ownership rule is
written, no one can be the owner of what was never written.

#### The Unmeasured Side of the Gate

The measurement deliberately leaves one thing out, and this limit has to be written:
**the required reviewer's availability.** The round cycle writes a fixed wait onto every
round and does not look at how many people that wait is collected from. In reality a gate
binds the queue to a single role; if that role is busy, the item stands in the
`in review` column and the chain's most volatile link lengthens further.

This effect does not show up in the table, but it changes how the table should be read.
The second, third, and fourth rows are identical in terms of found; **they are not in
terms of wait.** A rule collecting three signatures in sequence takes longer than a rule
producing the same result with one signature, and the measurement does not record this
gap. So the table's **36** units, for the rule with three required reviewers, is a
**lower bound.**

The second unmeasured side is that the rule's scope grows with file count. A broad path
prefix binds every change to the same role; that role becomes the gate for the entire
queue. It is invisible in the measurement, because the measurement looks at a single work
item. It is visible in the chain, because the number of items waiting in the same role's
queue swells the `queued` column.

The third side appears in the rule's **absence**. A repository with no ownership rule
gets the first row — **22** found — but only if someone who actually looks at that change
alone steps forward. The rule does not guarantee axis diversity; it guarantees that a look
will exist. The measurement counts the look that exists, not the one that does not.

#### Summary

- **Code ownership** is a mapping that binds a file path to a role; matching roles become
  **required reviewers** and their approval works like a gate.
- Raising required reviewer count from **1** to **3** leaves found fixed at **6**, rounds
  at **6**, and wait at **36**: adding a reviewer without adding an axis finds nothing.
- A **single** approval with no axis restriction finds **22** on the same change and
  closes in **2 rounds / 12 wait**; the gap comes not from person count but from axis
  count climbing from **1** to **5**.
- Adding an open approval alongside the required owner gives both: found **22**, open
  axes **0**. Ownership is a guarantee of one axis, not a guarantee of coverage.
- In narrow ownership, **4** axes stay open and **16** defects stand there; these are not
  defects looked at and missed, they are defects never looked at, and their remedy is the
  rule itself.
- In all six of the six rules, **2** **unwritten requirement** defects are missed; no one
  can be assigned as the owner of what was never written.

#### Next Step

Up to this point, the rules of the flow were built with text: the guide was a text,
ownership was a mapping text. There is also a surface these rules run on — the
capabilities where the work item is recorded, the queue is seen, discussion attaches to
the change, merged work gets published, and the published becomes consumable. The next
lesson takes up these capabilities **at the class level** and asks each one a single
question: **which transition's wait does it shorten?** A capability with no answer is a
capability with no place in the chain; and whether any of them touches the number of
defects found will show up in the same measurement.

### Repository Platform Capabilities

Source: https://academia.sh/en/courses/code-review/repository-platform-capabilities

The five-capability chain drops from 45 units to 21, but found stays at 15, missed at 9, and unwritten missed at 2 in every row; the individual gains range from 2 to 7 and only discussion's gain grows with round count — 4, 6, 12.

The previous two lessons built the flow's rules with text: the contribution guide was a
text, ownership was a mapping text. There is also a surface these rules run on — the
capabilities where the work item is recorded, the queue is seen, discussion attaches to
the change, merged work gets published, and the published becomes consumable.

These capabilities are taken up not as products but as **classes**: issue tracking,
board, discussion, release publishing, package registry. Which hosting surface they stand
on is not this lesson's subject, and the same classes will still exist five years from
now. This lesson has a single question: **which transition of the chain does each
capability shorten, and by how much?**

#### Five Capability Classes

**Issue tracking** is where a proposal gets recorded. When a proposal does not turn into a
record, it stands somewhere as a spoken thing, and its entering the queue depends on
someone remembering it. Recording binds the proposal to the chain's first transition.

**Board** is the surface where work items are laid out by status. As measured in the
previous lesson, it shows accumulation, not rate; its contribution is speeding up the
**taking on** of the next item in the queue. If no one sees what is in the queue, taking
something from the queue depends on someone asking.

**Discussion** is where a finding and the counter-view **attach** to the change. If a
finding stands in a conversation outside the change, the next round has to reconstruct it:
which line, which reasoning, which counter-proposal. Attached discussion removes this
reconstruction.

**Release publishing** is the capability that binds merged work to a point and announces
it outward. **Package registry** is where published work becomes **pullable** as a
dependency. The two touch separate transitions, and both are after review.

What the five share is that each keeps a **record**. The record's fields determine what
the capability can do in the chain.

```text
# example work item record — not executed

id          : #377
status      : in review
class       : interface
requirement : outbound-call timeout must be configurable
linked proposal : #361
change      : payment-timeout
discussion  : 4 notes, 2 attached to a line
release     : —
```

The `status` field gives the item's column on the board, the `change` field says which
change meets this item, the `discussion` field says where the findings attach, and the
`release` field says whether the work has been published. If a field is empty, that link
of the chain cannot be measured: in a repository where the `release` field is never
filled, the `merged→closed` transition's duration is not known.

#### Capabilities Linked to Each Other

What makes the measurement possible is capabilities being **linked to each other**, not
standing alone. The chain's wait is measured at transitions, and measuring a transition
requires both ends to be bound to the same identity: which work item a proposal belongs
to, which change a work item belongs to, which discussion a change belongs to, which
release the merged work belongs to.

When the link breaks, the loss is not only traceability. A finding standing in a
conversation detached from the change requires that finding to be reconstructed in the
next round, and in the measurement this is exactly discussion's gain — the round
duration dropping from **6** to **4** is the repair cost of a broken link.

The same logic works at the chain's end too. Which work items a release note covers comes
from the link between the release and the items. Without the link, the note is written by
hand, and a hand-written note is a list no one can verify. This lesson measures the link's
effect on **wait**; the note's effect on **the reader** is the next lesson's subject.

#### Every Capability Touches One Transition

The measurement's design rests on a single rule: **a capability touches only one
transition.** If a capability is said to help in more than one place, the measurement
loses its meaning; where a gain comes from can no longer be separated. The mapping is
this: issue tracking touches `proposal→queued`, board touches `queued→in progress`,
release publishing touches `merged→closed`, package registry touches
`closed→available`. Discussion touches none of these; it touches **round duration**.

The last transition is added in this lesson. The previous lesson's chain ended at the work
item's closing; package registry touches one step beyond that, the work becoming
**consumable** by someone else. This step is not the work item's status and does not show
up on the board, but it is where the chain's reader is waiting.

#### Round Count vs. Round Duration

Discussion standing apart makes a distinction necessary. **The number of review rounds**
and **the duration of one round** are not the same thing. What determines round count is
what is found in the first round, and this has been the result of axis assignment since
the start of the course. What determines a round's duration is how intact a finding
carries from one round to the next.

The practical consequence of this distinction is: **no platform capability lowers round
count.** Round count is determined by what review sees, and a surface does not change
seeing. The capabilities touch wait, and discussion also touches wait — only its
multiplier is different, because it changes the wait written per round and gets
multiplied by round count.

The measurement's assumptions:

- **TF22** — The shared setup's 600-line change and 24 defects are used as they are; the
  class distribution and the oracle are the course's constant.
- **TF23** — The chain is the previous lesson's transitions, with **8** units added at the
  end for `closed→available`. The other four transitions are **5**, **9**, **2**, and
  **3** units.
- **TF24** — Every capability lowers **a single transition's** wait to a new value: issue
  tracking **1**, board **4**, release publishing **1**, package registry **1**. The
  values represent the state where the capability exists and is used.
- **TF25** — Discussion touches not a single transition but **wait per round**, lowering
  it from **6** to **4**. The reasoning is that attached discussion removes the
  reconstruction that would otherwise happen between rounds.
- **TF26** — The main measurement's assignment is `three reviewers, separate axes`. The
  second measurement tests discussion separately across four assignments; nothing else
  changes.
- **TF27** — The capabilities do not touch defects found. A surface neither adds an axis
  nor grows attention; this assumption is tested along with the measurement's result and
  confirmed in the table.
- **TF28** — The set's resolution at 24 defects is **1/24**; the smallest measurable
  difference in the chain is **1 unit** of wait.

#### Measurement

```python
"""Repository platform capabilities: the single transition each capability touches.

Part 1 - five capabilities one by one and together; does found or missed change.
Part 2 - the gain of the round-duration capability across four assignments.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
ATTENTION, CHUNK = 12, 50
# Chain: the work item's transitions, plus the final step reaching the consumer.
CHAIN = {"proposal→queued": 5, "queued→in progress": 9, "in progress→in review": 2,
         "merged→closed": 3, "closed→available": 8}
ROUND_WAIT = 6
# Each capability touches a single transition; discussion touches round duration.
CAPABILITIES = {
    "issue tracking": ("proposal→queued", 1),
    "board": ("queued→in progress", 4),
    "discussion": ("round duration", 4),
    "release publishing": ("merged→closed", 1),
    "package registry": ("closed→available", 1),
}


def rng(seed):
    d = seed % 2147483646 + 1

    def draw(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


def review(d, axes, attention=ATTENTION):
    read = set(range(min(attention, d["chunks"])))
    return {k["no"] for k in d["defects"]
            if k["class"] in axes and k["chunk"] in read}


def panel(d, assignments, attention=ATTENTION):
    found = set()
    for axes in assignments:
        found |= review(d, axes, attention)
    return found


def review_rounds(found, defect_count, wait_per_round=ROUND_WAIT):
    remaining, rounds, wait = defect_count - len(found), 1, wait_per_round
    while remaining > 0 and rounds < 6:
        rounds += 1
        wait += wait_per_round
        remaining -= max(1, len(found) // 2)
    return rounds, wait, max(0, remaining)


def chain_total(d, assignment, enabled):
    """Chain's total wait with the enabled capabilities' settings."""
    transitions = dict(CHAIN)
    round_duration = ROUND_WAIT
    for name in enabled:
        step, value = CAPABILITIES[name]
        if step == "round duration":
            round_duration = value
        else:
            transitions[step] = value
    b = panel(d, assignment)
    rounds, wait, remaining = review_rounds(b, len(d["defects"]), round_duration)
    return sum(transitions.values()) + wait, rounds, wait, b, remaining


FULL = set(AXES)
ASSIGNMENTS = {
    "single reviewer, all axes": [FULL],
    "three reviewers, same axis": [{"implementation"}] * 3,
    "three reviewers, separate axes": [{"interface"}, {"implementation"},
                                        {"test", "documentation"}],
    "five reviewers, five axes": [{a} for a in AXES],
}
d = change(600)
MAIN = ASSIGNMENTS["three reviewers, separate axes"]
baseline, t0, b0, found0, _ = chain_total(d, MAIN, [])
print(f"assignment: three reviewers, separate axes | chain {baseline} | rounds {t0} | "
      f"review {b0} | found {len(found0)} | missed {24 - len(found0)}")
print()
print(f"{'capability':<20s} {'transition touched':<23s} {'rounds':>3s} "
      f"{'chain':>6s} {'shortened':>7s} {'found':>7s} {'missed':>5s} "
      f"{'unwritten':>10s}")
for name in CAPABILITIES:
    z, rounds, _, b, _ = chain_total(d, MAIN, [name])
    unwritten = sum(1 for k in d["defects"]
                     if k["no"] not in b and k["class"] == UNWRITTEN)
    print(f"{name:<20s} {CAPABILITIES[name][0]:<23s} {rounds:3d} {z:6d} "
          f"{baseline - z:7d} {len(b):7d} {24 - len(b):5d} {unwritten:10d}")
z, rounds, _, b, _ = chain_total(d, MAIN, list(CAPABILITIES))
unwritten = sum(1 for k in d["defects"]
                 if k["no"] not in b and k["class"] == UNWRITTEN)
print(f"{'all five together':<20s} {'—':<23s} {rounds:3d} {z:6d} {baseline - z:7d} "
      f"{len(b):7d} {24 - len(b):5d} {unwritten:10d}")

print()
print(f"{'assignment':<32s} {'rounds':>3s} {'no discussion':>13s} "
      f"{'with discussion':>16s} {'shortened':>7s}")
for name, assignment in ASSIGNMENTS.items():
    without, rounds, _, _, _ = chain_total(d, assignment, [])
    with_disc, _, _, _, _ = chain_total(d, assignment, ["discussion"])
    print(f"{name:<32s} {rounds:3d} {without:13d} {with_disc:16d} {without - with_disc:7d}")
```

```
assignment: three reviewers, separate axes | chain 45 | rounds 3 | review 18 | found 15 | missed 9

capability           transition touched      rounds  chain shortened   found missed  unwritten
issue tracking       proposal→queued           3     41       4      15     9          2
board                queued→in progress        3     40       5      15     9          2
discussion           round duration            3     39       6      15     9          2
release publishing   merged→closed             3     43       2      15     9          2
package registry     closed→available          3     38       7      15     9          2
all five together    —                         3     21      24      15     9          2

assignment                       rounds no discussion  with discussion shortened
single reviewer, all axes          2            39               35       4
three reviewers, same axis         6            63               51      12
three reviewers, separate axes     3            45               39       6
five reviewers, five axes          2            39               35       4
```

#### Reading What Is Shortened

The chain's baseline is **45** units. Opened one by one, the capabilities' gain ranges
between **2** and **7**, and the ordering is: package registry **7**, discussion **6**,
board **5**, issue tracking **4**, release publishing **2**.

What determines the ordering is not the capability's importance, it is the **starting
length** of the transition it touches. Package registry shortens the most because
`closed→available` was the longest transition — **8** units. Release publishing shortens
the least because the transition it touches was already **3** units and there is no room
to shorten there. This says a capability's value **depends on its place in the chain**:
the same capability sits in a different order in a different chain.

Opened all five together, the chain drops from **45** to **21**; the gain is **24**
units, that is, **53%** of the chain. The sum of the individual gains is also **24**, and
this is not a coincidence — because each capability touches a separate transition, the
gains do not mix and can be added. If two capabilities touched the same transition, the
total would be smaller than the sum of the individual gains.

The `rounds` column is **3** in all six of six rows. No capability lowers the round. While
the chain's total is cut in half, round count does not budge at all: **shortening wait and
shortening rounds are separate jobs**, and these surfaces do only the first.

#### Discussion's Exception

The second table tests discussion separately across four assignments and shows something
that does not show up in a single table: discussion's gain is **not fixed.** In the
two-round assignments it is **4** units, in the three-round one **6**, in the six-round
one **12**.

The reason is in its definition. Discussion lowers wait per round from **6** to **4**;
the gain is **2** units per round and gets multiplied by round count. The other four
capabilities touch the chain's **fixed** part and their gains are independent of the
assignment — whatever the assignment is, package registry shortens by **7** units.

An ordering rule follows from this. **In a chain where review is good, discussion's gain
is one of the smallest capabilities; in a chain where review is bad, it is the largest.**
In the six-round row, discussion shortens by **12** units, more than package registry's
**7**. So a capability's ordering changes depending on the state of the team's review, and
a fixed priority list cannot be written.

The same row carries a warning too. The six-round assignment's chain with discussion is
**51** units; it is still longer than the two-round assignment's without-discussion
**39**. **No capability closes a bad axis assignment.** A surface does not make review
see what it does not see.

#### The Untouched Columns

The table's right three columns are the same in all six rows: found **15**, missed **9**,
unwritten missed **2**. **TF27** assumed this and the measurement confirms it.

This fixedness is the lesson's real finding. Five capabilities cut the chain in half and
**do not get even one more defect found.** The source of finding defects has been the
same since the start of the course: how many axes were looked at and how many chunks were
read. A surface neither adds an axis nor grows attention; it speeds up the item's moving
from one place to another.

The unwritten missed column says this in the sharpest way. The **unwritten requirement**
class still misses **2** even when the issue tracking capability is on — and that is
exactly the capability that records the proposal. Recording puts the proposal into the
chain; it does not record the requirement that was never recorded. The **requirement
statement** measured in an earlier lesson was the mechanism able to touch that class, and
it was not a surface, it was a **guide item**.

This distinction gives a selection rule: a capability is valuable if it shortens a
transition in the chain, and the amount it shortens is measurable. If a capability claims
to raise defects found, that claim can only be true by changing axis count or chunks
read; no capability in this table does that.

#### Summary

- Five capabilities are taken up at the class level — issue tracking, board, discussion,
  release publishing, package registry — and each touches **a single transition** in the
  chain.
- The chain drops from **45** units to **21** when all five are opened together; the
  gain is **24** units and equals the sum of the individual gains, because the
  transitions do not overlap.
- Individual gains range between **2** and **7**, and the transition's starting length
  determines the ordering: package registry **7**, release publishing **2**.
- Round stays at **3** in all six of six rows; no capability lowers round count. Only
  discussion touches the round's **duration**, and its gain grows with round count:
  **4**, **6**, **12**.
- Found **15**, missed **9**, and unwritten missed **2** are unchanged in every row:
  capabilities shorten wait, they do not get defects found.
- The six-round assignment's chain with discussion is **51** units, still longer than the
  two-round assignment's without-discussion **39**; no capability closes a bad axis
  assignment.

#### Next Step

One transition was left at the chain's end and appeared in the measurement as only one
number: merged work gets bound to a point and announced outward. That announcement is
itself a text and it has a reader — someone who never appeared through this whole course.
The next lesson measures that text: what does the **release note** tell the reader? How
many questions does the gap between a list of changes and a list of the **meaning** of
changes answer, and how many of the reader's questions find no answer in any note form?

### Release Publishing Workflow

Source: https://academia.sh/en/courses/code-review/release-publishing-workflow

Twenty-four work items and six reader questions decide 144 decisions: the raw change list answers 22 of them (0.153), the classified list 44, the meaning list 88, and with tag and release asset together 132 (0.917); all 12 of the 12 unanswered decisions are also from the unwritten requirement class.

The previous lesson saw the chain's final transition as a single number: merged work gets
bound to a point and announced outward. That announcement is itself a text and it has a
reader — someone who never appeared through this whole course. Review knows the author and
the reviewer; **the release note's reader has seen neither the change nor the
discussion.**

This lesson's question is not how the note gets produced. The question is: **what does the
note tell the reader?** How many questions does the gap between a list of changes and a
list of the **meaning** of changes answer, and how many of the reader's questions find no
answer in any form?

#### What the Reader Asks

Someone reading a release note is not curious about the release; they are curious about
**their own situation.** They ask six questions, and every question's answer is found in
the note only if a specific field was written.

The `did it change` question's answer is in the **title** — this is the minimal field
that says something changed. The `which area` question wants a **class**: where the
change touched. The `does it affect me` question wants an **impact**; the `what should I
do` question wants an **action**. The `after which point` question's answer is a
**boundary**, and a **tag** gives it. The `how do I get it` question's answer is an
**asset**, and a **release asset** gives it.

How a tag is placed and what it marks was established in the Introduction to Version
Control course; which pipeline a release passes through to get published is the subject
of the Continuous Integration and Delivery course. Neither is repeated here. In this
lesson, the tag and the **release asset** are counted only as **fields**: two fields
answering two of the reader's questions.

#### Two Lists

The same release can be written in two formats, and both are produced from the same
information.

```text
# example release note — two formats, not executed

## change list
- #377 timeout setting on the payment call
- #381 retry counter fix
- #388 log format updated

## meaning list
### Applications making outbound calls
- The timeout duration is now configurable; the default did not
  change. To do: if no setting is given, behavior is the same, no
  action needed.
- Retry count is two instead of three. To do: call paths expecting
  three attempts should be reviewed.
### Internal changes
- Log format and style fixes; no effect on the reader's side.
```

The difference is not in length. The first list says **what was done**, the second says
**what happened to the reader**. In the first list, all three lines carry the same
weight; in the second, the change that does not concern the reader is folded into a
single line, and the two items the reader actually needs to look at are separated out.
The measurement counts these two differences separately: **question answered** and
**line scanned**.

The measurement's assumptions:

- **TF29** — The release's work items are taken from the shared setup's set: **24**
  items, and the class distribution is the course's fixed distribution. `change` and the
  class list are not modified.
- **TF30** — The item in the **unwritten requirement** class was never merged; it cannot
  enter any note format. The number of items entering the note is therefore **22**.
- **TF31** — The reader has six questions and every question's answer stands in a single
  field. If the field is written, the question is answered; if not, it stays unanswered.
  There is no wrong answer in this measurement.
- **TF32** — Four note formats are tried, and each covers the previous one's fields:
  title; title and class; meaning list; the meaning list with tag and release asset
  added.
- **TF33** — The reader-facing classes are **interface**, **implementation**, and
  **documentation**; test and style items do not change the reader's situation. If the
  class field is not written, the reader cannot tell them apart and scans every line
  entering the note.
- **TF34** — The set's resolution is **1/24** at 24 items, **1/144** at the decision
  scale; one item's one question is the smallest unit measurable in this measurement.

#### Measurement

```python
"""Release note: the gap between a change list and a meaning list.

Part 1 - four note formats and the reader's six questions.
Part 2 - the line the reader scans against the line that is useful.
"""
SEED = 20260815
AXES = ("interface", "implementation", "test", "documentation", "style")
UNWRITTEN = "unwritten requirement"
CLASSES = AXES + (UNWRITTEN,)
CHUNK = 50
# Reader's question -> the field carrying its answer.
QUESTIONS = {"did it change": "title", "which area": "class",
             "does it affect me": "impact", "what should I do": "action",
             "after which point": "boundary", "how do I get it": "asset"}
FORMATS = {
    "raw change list": ("title",),
    "classified list": ("title", "class"),
    "meaning list": ("title", "class", "impact", "action"),
    "meaning + tag + asset": ("title", "class", "impact", "action",
                              "boundary", "asset"),
}
# Reader-facing class: the release note's reader searches for these.
READER_FACING = {"interface", "implementation", "documentation"}


def rng(seed):
    d = seed % 2147483646 + 1

    def draw(n):
        nonlocal d
        d = (d * 48271) % 2147483647
        return d % n
    return draw


def change(lines, defect_count=24, seed=SEED):
    draw, defects = rng(seed), []
    chunk_count = max(1, lines // CHUNK)
    for i in range(defect_count):
        defects.append({"no": i + 1, "class": CLASSES[draw(6)],
                         "chunk": draw(chunk_count)})
    return {"lines": lines, "chunks": chunk_count, "defects": defects}


d = change(600)
# The release's work items: the class distribution is the course's fixed distribution.
ITEMS = [{"no": k["no"], "class": k["class"]} for k in d["defects"]]
# The unwritten class was never merged; it cannot enter any note format.
IN_NOTE = [o for o in ITEMS if o["class"] != UNWRITTEN]
distribution = {s: sum(1 for o in ITEMS if o["class"] == s) for s in CLASSES}
print("release: " + ", ".join(f"{s} {n}" for s, n in distribution.items()))
print(f"work items {len(ITEMS)} | in note {len(IN_NOTE)} | reader questions "
      f"{len(QUESTIONS)} | decisions {len(ITEMS) * len(QUESTIONS)}")
print()
print(f"{'note format':<24s} {'fields':>6s} {'answered':>8s} {'unanswered':>10s} "
      f"{'ratio':>6s} {'from unwritten':>14s}")
total = len(ITEMS) * len(QUESTIONS)
for name, fields in FORMATS.items():
    answered = sum(1 for o in IN_NOTE for s in QUESTIONS if QUESTIONS[s] in fields)
    unwritten = (len(ITEMS) - len(IN_NOTE)) * len(QUESTIONS)
    print(f"{name:<24s} {len(fields):6d} {answered:8d} {total - answered:10d} "
          f"{answered / total:6.3f} {unwritten:14d}")

print()
print(f"{'note format':<24s} {'scanned lines':>13s} {'useful':>6s} {'ratio':>6s}")
for name, fields in FORMATS.items():
    # No class field: the reader scans every line; with it, only the reader-facing group.
    scanned = (len(IN_NOTE) if "class" not in fields
               else sum(1 for o in IN_NOTE if o["class"] in READER_FACING))
    useful = sum(1 for o in IN_NOTE if o["class"] in READER_FACING)
    print(f"{name:<24s} {scanned:13d} {useful:6d} {useful / scanned:6.3f}")
```

```
release: interface 3, implementation 6, test 3, documentation 3, style 7, unwritten requirement 2
work items 24 | in note 22 | reader questions 6 | decisions 144

note format              fields answered unanswered  ratio from unwritten
raw change list               1       22        122  0.153             12
classified list               2       44        100  0.306             12
meaning list                  4       88         56  0.611             12
meaning + tag + asset         6      132         12  0.917             12

note format              scanned lines useful  ratio
raw change list                     22     12  0.545
classified list                     12     12  1.000
meaning list                        12     12  1.000
meaning + tag + asset               12     12  1.000
```

#### Reading the Gap

**22** of a hundred forty-four decisions are answered in the raw change list — a ratio of
**0.153**. The list is complete, no work item is skipped, and it does not answer five of
the reader's six questions. Completeness and answering are not the same thing.

Adding the class field raises answered to **44**, the meaning list to **88**. The real
jump is in the third row: the **impact** and **action** fields alone carry **44**
decisions, roughly a third of the set. What these two fields share is that neither can be
**derived** from the merged work. Title and class can be extracted from the merged work;
impact and action exist only if someone writes them.

In the fourth row, the ratio climbs to **0.917**. This number is familiar in the course:
in a 100-line change, the ratio of defects found was also **0.917**. This is not a
coincidence — in both cases what is left out is exactly **2/24**, and those two items
belong to the same class.

The table below gives the second gap. In the raw list, the reader scans **22** lines and
**12** of them are useful: ratio **0.545**. Close to half the reader's reading is wasted.
Once the class field is added, scanned drops to **12** and the ratio becomes **1.000**.
What stands out is that this gain is secured at the **second** row: grouping cuts the
reader's scanning in half before impact and action are even written. **The cheapest
improvement is not rewriting the note, it is classifying it.**

#### Summary

- The release note's reader has seen neither the change nor the discussion; they have six
  questions, and every question's answer stands in a single field written in the note.
- The raw change list answers **22** of a hundred forty-four decisions (**0.153**), the
  classified list **44**, the meaning list **88**, and with tag and release asset
  together **132** (**0.917**).
- The **impact** and **action** fields alone carry **44** decisions, and neither can be
  derived from the merged work; they exist only if someone writes them.
- The reader scans **22** lines in the raw list and uses **12** (**0.545**); the class
  field drops scanned to **12** and makes the ratio **1.000** — this gain is secured
  without writing impact or action.
- All **12** of the **12** unanswered decisions also belong to the **unwritten
  requirement** class; unmerged work shows up in no note format.

#### Course Wrap-Up

The course ran on a single measure: a review's number is **not how many people are
looking, it is how many axes are being looked at and how many chunks are read.** Fifteen
lessons applied this measure to separate places.

| lesson | measured | found or missed |
|---|---|---|
| Purpose of Code Review | what axis diversity finds | three reviewers on the same axis find 6, on separate axes 15, a single reviewer on five axes 22, five reviewers again 22; 2 unwritten-requirement defects are missed in all four |
| Meaning of Interface | the cost of closing one axis | the five axes' sets are disjoint, the sum of pairwise intersections is 0; closing interface drops found from 22 to 19, and ten reviewers without interface stay at 19 |
| Meaning of Implementation | an axis's marginal contribution | implementation adds 6 in both orders, style adds 7, the remaining three add 3 each; 1, 3, and 12 readings on the same axis stay at 6, ratio 0.250 |
| Reviewing Tests | the defect the test itself carries | three test defects sit in chunks 9, 1, and 7, and those chunks already had 1, 2, 1 findings from other axes; missed in an unread chunk is 0 on all five axes, and closing the test axis drops found from 22 to 19 |
| Documentation Review | what is missed when the documentation axis is closed | the five axes' individual closing cost is 3, 6, 3, 3, and 7; closing documentation drops found from 22 to 19, documentation missed is 0 while the axis is open, and unwritten missed stays at 2 |
| Code Style and Automation | attention shifting once the style axis is handed to a tool | human findings drop from 22 to 15, style's share from 0.318 to 0.000; the tool's contribution equals the axis left uncovered — 0 if five axes are covered, 15 to 22 with three reviewers, and missed stays at 2 in the best tooled case |
| Change Size | size against fixed attention | 22 (0.917) at 100 and 300 lines, 18 (0.750) at 900, 2 (0.083) at 2400; 36 of 48 chunks go entirely unread |
| Feedback Language | the round the same finding's wording produces | actionable 2 rounds / 12 wait / 44 note-rounds, observation 3 / 18 / 66, criterion-free 4 / 24 / 88; found stays at 22 and missed at 2 |
| Disagreement Resolution | the round an unresolved finding carries | rule absence: 6 rounds / 36 wait and 8 defects in the code; binding to a criterion: 3 rounds / 18 units and only 2 in the code |
| Review Metrics | wait, round, and queue health | three reviewers on the same axis: 6 rounds / 36 wait / 3 remaining, single reviewer on five axes: 2 / 12 / 0; longest open time in the queue is 2 rounds versus 15 |
| Issue Tracking and Boards | chain wait and review's share | wait outside review fixed at 19, the link between 12 and 36, share between 0.387 and 0.655; 3 remaining defects take the chain from 55 to 105 |
| Contribution Guidelines | the round a written expectation closes | in a narrow review, round is 6, 5, 4, 3, 2 and wait drops from 36 to 12; a requirement statement drops unwritten-requirement missed from 2 to 0 |
| Ownership and Approval Rules | the effect of required reviewers on axis diversity | as approvals climb from 1 to 3, found stays at 6, rounds at 6, wait at 36; a single approval with no axis restriction finds 22, and in narrow ownership 4 axes stay open with 16 defects standing there |
| Repository Platform Capabilities | the transition each capability shortens | the chain drops from 45 to 21, gain 24; found 15, missed 9, and unwritten missed 2 stay fixed in every row; discussion's gain is 4, 6, 12 |
| Release Publishing Workflow | the question the note answers the reader | 22 (0.153), 44, 88, and 132 (0.917) of 144 decisions; all 12 of the 12 unanswered are unwritten requirement |

Three readings come out of the table. **First:** what grows the number is not the person,
it is the axis, and this was reconfirmed in every topic — a third person looking at the
same axis adds **0**, a new axis adds between **3** and **7** defects. **Second:** what
determines round and wait is what could not be seen in the first round; the guide,
ownership rule, and platform capabilities touch wait, not what is found. **Third:** the
same two defects are missed across all fifteen rows. Adding an axis, shrinking size,
cutting the round, writing a rule, opening a surface — none of them reaches them. The one
mechanism that reaches them was the guide item requiring the author **to write** the
requirement, and it was not a review tool.

The third reading draws the course's boundary. **Review looks at what was submitted.**
Everything inside a submitted change — its interface, implementation, test,
documentation, style — can be seen with enough axes and a small enough size. The absence
of what was never submitted is not review's subject; only **writing it** closes that gap.

The next course — Technical Writing and Documentation — takes up exactly this work: the
separation of document types from each other, the separate structures of tutorial and
reference, the repository's own documents, the layout of an interface reference, and
writing release notes with the reader's meaning in view. This lesson's final measurement
leaves a number there: how many of the six questions a note answers depended not on who
wrote the note but on **which fields were written.** The distinction has to be set from
the start: **review looks at what was written; writing what was not written is a
separate job.** The measure this course leaves behind is the unit that will be used there
to count how many questions every document written counts as answering.

