step.12.diff.txt -- narrative retro for step.12 (U2.3: UoW ledger
operations)

Executed: 2026-07-20, same session as the skeleton (no separate
skeleton-then-execute split -- table_parser.py/table_emitter.py/
table_transitions.py/table_errors.py, the two new test files, the new
doc, the end_to_end.md corrections, and this step file were all built
together, matching step.7/step.9/step.10/step.11's rhythm). Companion
artifact: step.12.gdiff.txt (automated, scripts/step_gdiff).
Distinguishing feature of this step versus its predecessors: the
"ground yourself first" brief asked for THREE separate investigations
before any code (stale section number, real dropped-reason precedent,
real formatting-quirk risk) and all three turned up real, non-obvious
findings -- not one of them was a formality.

## Plan vs actual

```
+---+----------------------------------+------------------------------------------+
| # | Planned (brief)                  | Actual                                   |
+---+----------------------------------+------------------------------------------+
| 1 | Verify the U2.3 bullet's "§9-    | Confirmed stale, and WORSE than the      |
|   |   table model" reference against | brief's own framing: not just the U2.3   |
|   |   the current heading number     | bullet -- THREE other places in          |
|   |   (named as likely-stale)        | end_to_end.md's own §0/preamble (lines   |
|   |                                  | ~16/29/30/32) also said "§9 ledger"/     |
|   |                                  | "§10 maintenance rules", pointing at the |
|   |                                  | wrong sections (actual: §11/§12). All 4  |
|   |                                  | corrected in this same step. Also        |
|   |                                  | decided NOT to touch steps/step.10.txt/  |
|   |                                  | step.11.txt's own "§9-table" mentions -- |
|   |                                  | those are historical retro artifacts,    |
|   |                                  | correcting them would falsify the        |
|   |                                  | record of what was true when written.    |
| 2 | Investigate a real "dropped"-    | No real dropped row exists in EITHER     |
|   |   reason precedent (this repo    | repo's live ledger table -- checked by   |
|   |   or the b3u sibling) before     | live grep of both before designing       |
|   |   designing the transition API   | anything. Resolved by REASONING from a   |
|   |                                  | DIFFERENT real precedent instead: the    |
|   |                                  | live table already uses a parenthetical  |
|   |                                  | appended to Title for annotation (U1.5/  |
|   |                                  | U1.6, real rows, real content) --        |
|   |                                  | extended that convention to dropped-     |
|   |                                  | reason rather than inventing something   |
|   |                                  | unrelated to how the table already       |
|   |                                  | works. Named explicitly as a REASONED    |
|   |                                  | design decision, not a copied precedent, |
|   |                                  | because none existed to copy.            |
| 3 | (not explicitly named in the     | The new parser's strict per-cell-width   |
|   |   brief -- found while building  | check (structural parity with the        |
|   |   and testing the parser against | border's own widths) refused to parse    |
|   |   the real live table)           | the live table on its FIRST real run:    |
|   |                                  | two continuation rows (U3.9/U3.9a's      |
|   |                                  | "see §5..." lines) were each ONE         |
|   |                                  | character narrower than every other row  |
|   |                                  | -- a real, pre-existing hand-authoring   |
|   |                                  | slip in end_to_end.md, most likely from  |
|   |                                  | how "§" interacts with an editor's       |
|   |                                  | column counting. Fixed by hand (2        |
|   |                                  | single-space insertions) rather than     |
|   |                                  | loosening the parser's tolerance --      |
|   |                                  | loosening it would have hidden a real    |
|   |                                  | defect and broken the refuse-loudly      |
|   |                                  | discipline U2.1 already established for  |
|   |                                  | the other format.                        |
+---+----------------------------------+------------------------------------------+
```

Every other part of the plan held: the two-tier emitter design
(verbatim `raw` per row preferred, word-wrapped synthesis otherwise)
worked once the widths were derived correctly from the (now-fixed)
border; the transition/field-requirement rule set matched the §0/§12
reasoning on the first design pass, no rework needed after the
dropped-reason investigation landed; the split-row-is-`add_row()`
conclusion (no separate `split_row()`) was decided BEFORE writing any
code, by inspecting the real U3.9/U3.9a rows first and finding no
distinguishing field or relationship a dedicated mechanism would need
to carry.

## Gate evidence

```
+------+---------------------------------------------------------------------+
| G1   | emit_ledger_table_text(parse_ledger_table_text(text)) == text       |
|      | against the real live table (post-formatting-fix); AND              |
|      | replace_ledger_table_in_doc(doc, table) == doc against the FULL     |
|      | end_to_end.md, both zero-mutation. PASS live. (Failed BEFORE the    |
|      | 2-line formatting fix -- a real, reported gate failure, not         |
|      | papered over; see Plan-vs-actual #3.)                               |
| G2   | validate_row_invariants() over every real row in the live table:    |
|      | 0 violations, including the U0.1-U0.4 founding-row `step="--"`      |
|      | exception explicitly asserted NOT flagged. PASS live.               |
| G3   | Structural round-trip after transition() on a live row (U2.4,       |
|      | synthetic active promotion in the TEST, not the real ledger) and    |
|      | after add_row() with a title long enough to force wrapping --       |
|      | both re-parse to the same fields. PASS live, 2/2.                   |
| G4   | Transition legality matrix: 4 legal pairs succeed with the right    |
|      | field recorded; todo->done, active->todo, done->active,             |
|      | done->dropped, dropped->active, dropped->todo, and an unknown       |
|      | target status all raise IllegalTransitionError with message-        |
|      | content assertions (not bare pytest.raises). PASS live, 4/4 + 6/6.  |
| G5   | Missing/empty required fields (active w/o step, active w/ empty     |
|      | step, done w/o commit, dropped w/o reason) all raise                |
|      | MissingTransitionFieldError with message-content assertions. PASS   |
|      | live, 4/4.                                                           |
| G6   | add_row(): plain append; DuplicateUoWError on an existing id; the   |
|      | U3.9/U3.9a split pattern replayed with a lettered id in the same    |
|      | phase; UnknownUoWError on a bad after= and on an out-of-range       |
|      | phase=. PASS live, 5/5.                                              |
| G7   | record_commit(): backfills the Commit cell without touching status; |
|      | empty commit raises MissingTransitionFieldError. PASS live, 2/2.    |
| G8   | 9 synthetic malformed-input refusals (bad border char, inconsistent |
|      | border, non-blank continuation Status/step/Commit cells, wrong cell |
|      | width, empty phase block, no Ledger heading, unclosed fence) plus 1 |
|      | heading-number-independence case (a "## 3. Ledger" heading, not     |
|      | "## 11.", still locates and parses correctly -- the direct          |
|      | regression test for this step's own staleness finding). PASS live,  |
|      | 10/10.                                                               |
| G9   | Full suite: 205 passed/11 skipped (post-step.11 baseline, re-        |
|      | confirmed live before any edit) -> 247 passed/11 skipped after. +42  |
|      | (40 in the two new test files + 2 from the step-file parser/        |
|      | emitter's own self-inclusive corpus parametrization picking up       |
|      | step.12.txt at collect time -- the same growth pattern step.10/11    |
|      | documented for their own predecessors, not new tests written by     |
|      | this step). 0 skipped delta, 0 failures.                             |
| G10  | end_to_end.md: 4 stale section refs corrected, 2 formatting-bug     |
|      | rows fixed, §4 U2.3 bullet -> DONE with *Delivered:*, §11 ledger row |
|      | for U2.3 -> done/step.12/PENDING -- the LAST edit performed THROUGH  |
|      | the new API itself (parse_ledger_table_from_doc -> transition(x2)   |
|      | -> replace_ledger_table_in_doc -> write), verified via `git diff    |
|      | --stat` before committing.                                          |
| G11  | b3ubot porcelain clean confirmed before committing; retro pair via  |
|      | the SHARED v3_claude/scripts/step_gdiff (steps/step.12.gdiff.txt).  |
+------+---------------------------------------------------------------------+
```

## Findings for later steps

1. **A "verify this stale reference" instruction should be treated as
   "verify every instance of the pattern," not just the one named
   example.** The brief named the U2.3 bullet's "§9" specifically; a
   full grep of end_to_end.md for "§9"/"§10" found three MORE stale
   self-references nobody had flagged. Same lesson step.11 recorded
   about risk investigations (#1 in that step's own findings) applied
   here to staleness-checking instead of byte-format-checking -- the
   general pattern: a named example is a floor on what to check, not
   the whole check.
2. **A parser's own strict validation is a discovery tool, not just a
   gate.** The 1-character-narrow row bug was found because
   `table_parser.py`'s cell-width check was written strict FIRST (no
   tolerance for "close enough"), then pointed at real data -- it
   failed loudly on the very first attempt, which is exactly what
   surfaced a real, previously invisible defect in end_to_end.md
   itself (a plain-text file nobody would spot-check by eye at that
   granularity). Loosening the check to "make the parser work" would
   have thrown away that signal permanently.
3. **When no real precedent exists for a design question, say so
   explicitly and reason from the closest REAL analog instead of
   inventing from nothing or overclaiming a precedent.** The
   dropped-reason investigation found zero real dropped rows in either
   repo -- rather than either (a) guessing a convention with no
   grounding at all, or (b) writing it up as if U1.5/U1.6 WERE
   dropped-row precedent (they aren't), this step named the gap
   plainly and extended the REAL, differently-purposed U1.5/U1.6
   parenthetical convention on its own reasoned merits. The
   distinction between "here is real precedent" and "here is a
   reasoned decision because no precedent exists" is itself worth
   preserving in the record, not just the conclusion.

## Numbers

```
+---------------------------------------+------------------------+
| new files                              | 7 (app/ledger/         |
|                                         | table_errors.py+       |
|                                         | table_parser.py+       |
|                                         | table_emitter.py+      |
|                                         | table_transitions.py,  |
|                                         | tests/test_ledger_     |
|                                         | table_roundtrip.py+    |
|                                         | test_ledger_table_     |
|                                         | transitions.py,        |
|                                         | docs/ledger_table_     |
|                                         | schema.md, steps/      |
|                                         | step.12.txt)           |
| files edited                           | 1 (end_to_end.md: 4    |
|                                         | stale refs, 2          |
|                                         | formatting-bug rows,   |
|                                         | U2.3 bullet, U2.3      |
|                                         | ledger row)            |
| table parser+emitter+transitions LOC   | ~120 + ~100 + ~215 =   |
|                                         | ~435                   |
| new tests                              | 40 (17 round-trip + 23 |
|                                         | transitions) + 2 from  |
|                                         | the step-file corpus's |
|                                         | own self-inclusive     |
|                                         | growth (picking up     |
|                                         | step.12.txt)           |
| live ledger table round-trips clean    | byte-identical, 0      |
| (post formatting fix)                  | mutations              |
| real rows invariant-clean              | all (0 violations)     |
| formatting bugs found + fixed in       | 2 (both U3.9/U3.9a's   |
| end_to_end.md                          | "see §5..." lines)     |
| stale section references corrected     | 4 (§9->§11 x2,         |
|                                         | §10->§12 x2)           |
| suite before -> after                  | 205p/11s -> 247p/11s   |
| regressions                            | 0                       |
+---------------------------------------+------------------------+
```

U2.3 done -- closes P2's ledger-operations UoW: rows, status
transitions, and same-commit update discipline are now a tested API
over the SAME live table `end_to_end.md` itself carries, not a
parallel/simulated model of it. Two real, previously-invisible defects
in the live document were found and fixed as a direct side effect of
building a strict parser for it (a 1-character formatting slip in 2
rows, and 4 stale section cross-references) -- the same "a corpus
survey has to actually run the check" lesson step.10/11 established
for the step-file format, now confirmed for the ledger-table format
too. Only U2.4 (retro-pair machinery + the `b3ubot ledger` CLI) remains
before P2 closes and P3 (the orchestrator) can begin. Ledger updated in
the work commit per the §12 execution contract, performed literally
THROUGH this step's own new API; commit hash of that same commit
backfilled into both end_to_end.md §11 and this file's own
steps/step.12.txt §7 in the immediately following gdiff commit -- the
same 2-part resolution step.7/step.9/step.10/step.11 used.

End of retro.
