step.12.txt -- b3ubot U2.3: UoW ledger operations -- the end_to_end.md
               ledger-TABLE parser/emitter/transition API (a SEPARATE
               format from U2.1/U2.2's step-file parser/emitter), plus
               a live section-number staleness correction and a real
               1-character formatting bug the new parser caught on its
               first real parse.

Step:     12 -- promote UoW **U2.3** (end_to_end.md §4, P2) to execution
                depth: `app/ledger/table_parser.py` (`LedgerRow`/
                `LedgerTable`, `parse_ledger_table_text`/`_from_doc`/
                `_file`), `app/ledger/table_emitter.py`
                (`emit_ledger_table_text`, `replace_ledger_table_in_doc`),
                `app/ledger/table_transitions.py` (`transition`,
                `record_commit`, `add_row`, `validate_row_invariants`),
                `app/ledger/table_errors.py`, and
                `docs/ledger_table_schema.md`.
Parent:   end_to_end.md §4 U2.3 bullet (original text, before this
                step's own correction): "UoW ledger operations -- the
                §9-table model: rows, status transitions (todo ->
                active -> done/dropped), same-commit update discipline
                encoded as an API. Done when: ledger ops round-trip and
                validate transitions." end_to_end.md §0 (status
                vocabulary, ground truth for the state machine) and §12
                "How to maintain this file" (monotonic numbering,
                never-delete-rows, same-commit discipline -- the prose
                this step turns into code). app/ledger/parser.py +
                emitter.py (U2.1/U2.2, step.10/11) for the closest
                in-repo template of the house style (dataclasses,
                dedicated errors module, refuse-loudly, two-tier
                round-trip guarantee) -- a DIFFERENT format (the §11
                TABLE, not a step file), same discipline.
UoW:      U2.3 -- "UoW ledger operations". Third and final UoW of P2
                before U2.4 (retro-pair machinery + the `b3ubot ledger`
                CLI, explicitly out of scope here). Depends on nothing
                from U2.1/U2.2 code-wise (this step parses a different
                format), but continues the same Track A sequencing
                (finish P2 before P3/U3.1).
Date:     2026-07-20  (drafted and executed in the same pass, at the
                operator's explicit request -- the step.7/step.9/
                step.10/step.11 single-pass rhythm).
Origin:   Operator directive 2026-07-20: draft AND execute step.12 for
                U2.3, in one pass. Explicit brief naming: (1) verify
                the end_to_end.md U2.3 bullet's "§9-table model"
                reference against the CURRENT actual heading number
                (named as likely-stale, mirroring step.10/11's "34->50"
                / staleness-correction precedent); (2) ground the
                state machine in §0's ALREADY-DEFINED status vocabulary
                (todo/active/done/dropped), not an invented one; (3)
                investigate where a "dropped" reason actually gets
                recorded given the table has no Reason column, using a
                REAL precedent if one exists in this repo or the b3u
                sibling, not a guess; (4) build the parser/emitter/
                transition API with the same house style as U2.1/U2.2;
                (5) be honest about what "same-commit update
                discipline encoded as an API" can and cannot
                mechanically enforce; (6) round-trip + transition
                tests as a regression suite; (7) dogfood this step's
                own ledger-row update through the new API if it fits
                naturally.

## (0) Status

EXECUTED in this pass. Not a skeleton-then-later-execute split -- the
table parser/emitter/transitions modules, the tests, the docs, 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.

## (1) Ground truth probed at drafting (outputs -> named writers)

    READ    end_to_end.md in full: §4 U2.3 bullet, §0 (status
            vocabulary + standing locks), §11 (the live ledger table
            itself, all ~56 lines of it), §12 ("How to maintain this
            file"). app/ledger/parser.py, emitter.py, errors.py
            (U2.1/U2.2) as the closest in-repo template.
            docs/step_file_schema.md for the doc-writing style to
            mirror in docs/ledger_table_schema.md.
    VERIFIED (the brief's first named check) the CURRENT actual
            section number of the Ledger heading: `grep -n "^## .*
            Ledger" end_to_end.md` -> "## 11. Ledger -- UoW status".
            "§9" was indeed stale -- and not just in the U2.3 bullet:
            THREE other places in end_to_end.md's own §0/preamble
            (lines ~16, ~29, ~32, before this step's edits) also said
            "§9 ledger" and "§10 maintenance rules"; the maintenance
            rules are actually at §12. Same root cause as step.10's
            "34 -> 50" corpus-count staleness: a live document's own
            self-references drift as it's edited, and nothing had
            re-checked these four in particular since they were
            written. All four corrected in end_to_end.md in this same
            step (see (2) below and steps/step.12.diff.txt). NOT
            touched: steps/step.10.txt and step.11.txt's own
            "§9-table" mentions in their out-of-scope sections -- those
            are historical retro artifacts (immutable committed
            record of what was true/said AT THE TIME), not live docs;
            rewriting them would falsify the historical record rather
            than correct a live staleness.
    INVESTIGATED  the ground-truth status vocabulary (§0, verbatim):
            `todo` / `active` (has a step.N) / `done` / `dropped` (with
            reason) -- used as-is, not reinvented. Checked the b3u
            sibling's OWN end_to_end.md for comparison (a live grep):
            b3u's vocabulary is actually DIFFERENT -- it has a FOURTH
            state, `blocked(<reason>)`, that b3ubot's own §0 does not
            (b3ubot: `git grep -n "dropped" end_to_end.md` -> only the
            vocabulary-definition line and the §12 maintenance-rule
            line, no real dropped ROW; b3u: same pattern, plus a
            `blocked` state neither repo's real table currently uses
            in an actual row). This module does NOT add a `blocked`
            state to b3ubot -- §0 doesn't have one, and inventing one
            unasked would be exactly the "don't invent a different one"
            the brief warned against.
    INVESTIGATED  the SPECIFIC question the brief named: where does a
            "dropped" reason actually get recorded, given the table
            has no Reason column? Checked for a real precedent FIRST
            (both repos, live grep) before designing: NONE EXISTS --
            neither this repo's nor b3u's live ledger table has ever
            had a real `dropped` row. Resolved by REASONING from the
            table's own real structure rather than inventing from
            nothing: the live table ALREADY uses a parenthetical
            appended to the Title cell for exactly this kind of
            annotation -- U1.5 ("Human-relay adapter (manual,
            decouples workflow from any AI tool)") and U1.6 ("Wire
            --provider human into ask (reconcile propose_ask())") are
            real, live precedent for that convention, even though
            neither is itself a dropped row. `transition(row,
            "dropped", reason=...)` follows it: appends
            " (dropped: <reason>)" to Title. Full reasoning in
            docs/ledger_table_schema.md and table_transitions.py's
            module docstring -- named as a DESIGNED, REASONED answer,
            not a copied precedent (because none existed to copy).
    READ    the live table's own structure closely (grep + a small
            standalone python probe against the real file, not just
            visual inspection) to derive: one byte-identical border
            string reused everywhere (confirmed: 1 distinct border
            across the whole table), 5 columns with widths
            (7, 38, 8, 9, 9) derived from it, phase blocks bounded by
            border lines, continuation rows (blank UoW cell) for
            wrapped titles -- confirmed via U1.5/U1.6 (2-line wraps)
            AND U3.9/U3.9a (4-line and 2-line wraps -- the corpus's
            own evidence that wrapping isn't bounded to 2 lines).
    FOUND (live, via the new parser's own strict per-cell-width check,
            on its FIRST real run against the live table): two
            continuation rows -- U3.9's "dependency; see §5)." line and
            U3.9a's "see §5 U3.9a)." line -- were each exactly ONE
            character narrower than every other row in the table (76
            chars instead of the table's uniform 77). A real,
            pre-existing hand-authoring slip in end_to_end.md itself,
            most likely from how the "§" glyph interacts with an
            editor's column counting when those two lines were typed.
            Confirmed via a standalone script scanning every row line's
            length against the border-derived total (77) before
            deciding how to react: exactly these 2 lines, nowhere else.
            Fixed by hand in end_to_end.md (2 single-space insertions,
            restoring column alignment) -- the parser's strict
            per-cell-width enforcement (`len(cell) != w` against the
            border's own widths) is what CAUGHT this; loosening it to
            tolerate the anomaly was considered and rejected (it would
            hide a real data defect, contradicting the refuse-loudly
            house style app/ledger/parser.py already established for
            the other format).
    CONFIRMED  `git log --oneline -25` for exact commit-message
            phrasing (step.7/9/10/11's two-commit shape is this step's
            template) before writing either commit.
    VERIFIED  (live, standalone, before writing the pytest suite) that
            the parser round-trips the real live table byte-identically
            with zero mutations (both the raw table text alone and via
            `replace_ledger_table_in_doc` against the FULL document),
            and that a transition/add_row-mutated table re-parses to
            the same structured fields (structural round-trip) --
            both proven before the regression-suite tests were written
            around them, matching U2.2's own verification order.

## (2) What U2.3 delivers

  2.1 **app/ledger/table_errors.py** (new): `LedgerTableError` (base),
      `MalformedLedgerTableError`, `UnknownUoWError`,
      `DuplicateUoWError`, `IllegalTransitionError`,
      `MissingTransitionFieldError` -- a SEPARATE hierarchy from
      `StepFileError` (U2.1), same shape (named subclasses, no generic
      ValueError/KeyError, messages name the concrete defect and the
      fix).
  2.2 **app/ledger/table_parser.py** (new): `LedgerRow` (uow/title/
      status/step/commit/phase/raw) and `LedgerTable` (col_widths/
      border/header_cells/blocks, `.rows` property, `.row(uow)`
      lookup) dataclasses; `parse_ledger_table_text` (the primitive),
      `parse_ledger_table_from_doc` (locates the "## <N>. Ledger ..."
      heading by REGEX, not a hardcoded number -- directly answering
      this step's own staleness finding), `parse_ledger_table_file`
      (disk convenience). Structural-shape-only validation (border
      consistency, cell widths, continuation-row blankness) -- no
      semantic (status-vocabulary) validation here, same philosophy as
      app/ledger/parser.py not validating section-title vocabulary
      (semantic validation is table_transitions.py's job).
  2.3 **app/ledger/table_emitter.py** (new): `emit_ledger_table_text`
      (two-tier: `row.raw` preferred when present, word-wrapped
      synthesis via `_wrap_title`/`textwrap` otherwise -- no
      hyphenation, a deliberate, named simplification versus the
      historical corpus's hand-hyphenated wraps), and
      `replace_ledger_table_in_doc` (splice a freshly emitted table
      back into a full end_to_end.md-shaped document, everything else
      byte-identical -- the practical form of "same-commit update
      discipline").
  2.4 **app/ledger/table_transitions.py** (new): `transition(row,
      new_status, **fields)` (the resolved rule set: todo->active
      requires step=, todo->dropped and active->dropped require
      reason=, active->done requires commit=; done/dropped both
      terminal), `record_commit(row, commit)` (PENDING-backfill without
      a status change, the step.7/9/10/11 pattern), `add_row(table,
      ..., after=None)` (monotonic-id-defensive; ALSO the split-row
      operation -- U3.9/U3.9a replayed as evidence that no separate
      `split_row()` is warranted, see the function's own docstring),
      `validate_row_invariants(row)` (retrospective consistency check,
      used by the regression suite over the live table; names the
      U0.1-U0.4 founding-row `step="--"` exception explicitly).
  2.5 **tests/test_ledger_table_roundtrip.py** (new, 17 tests): live-
      table structural sanity (phase count, header cells, unique UoW
      ids), byte-identical round-trip (raw table text AND the full-
      document `replace_ledger_table_in_doc` no-op), the U1.5 lossy-
      title-join documented limitation, the U3.9/U3.9a split precedent
      present and distinct, structural round-trip after a real
      `transition()` and after a real `add_row()` (including a long
      title that must wrap), synthesized-row column-width fitting, and
      9 refuse-loudly cases against synthetic malformed input
      (bad border char, inconsistent border, non-blank continuation
      cells, wrong cell width, empty phase block, missing heading,
      unclosed fence, heading-number-independent location).
  2.6 **tests/test_ledger_table_transitions.py** (new, 23 tests): every
      legal transition succeeds with the right field recorded (4
      cases), every illegal transition raises with message-content
      assertions -- not just "it raised" (todo->done skip, active->
      todo backward, done terminal x2, dropped terminal x2, unknown
      status), every missing/empty required field raises with
      message-content assertions (4 cases), `record_commit` (backfill
      + empty-raises), `add_row` (append, duplicate-raises, the
      U3.9/U3.9a split replay, unknown `after=`, bad phase), and the
      live-table invariant sweep (every real active/done row clean,
      the U0.1 founding-row `step="--"` exception explicitly asserted
      NOT flagged, plus synthetic violation-detection cases for all
      three invariant classes).
  2.7 **docs/ledger_table_schema.md** (new): the table's shape, the
      section-number-staleness finding, the real formatting-bug
      finding, the two-guarantee round-trip shape, the resolved
      transition rule set with justification, the "where a dropped
      reason is recorded" investigation and answer, the
      `record_commit()` vs `transition()` distinction, the split-row-
      is-`add_row()` reasoning, and the honest "what this API can and
      cannot enforce" scope statement.
  2.8 **end_to_end.md** same-commit edits: the four stale "§9 ledger"/
      "§10 maintenance rules" references corrected to §11/§12 (§0 and
      the preamble); the two 1-character-narrow row lines fixed (2
      single-space insertions, U3.9/U3.9a's "see §5..." continuation
      lines); §4 U2.3 bullet -> DONE (step.12) with a *Delivered:*
      line naming everything in 2.1-2.7 plus the two findings; §11
      ledger row for U2.3 itself -> done/step.12/PENDING -- performed
      THROUGH the new API (`parse_ledger_table_from_doc` ->
      `transition(row, "active", step="step.12")` ->
      `transition(row, "done", commit="PENDING")` ->
      `replace_ledger_table_in_doc` -> write back), a live
      demonstration of the API on a real mutation, not just the
      synthetic test fixtures (real dogfooding, per the operator's
      "if it fits naturally" framing -- it did).

EXPLICITLY OUT OF SCOPE (named here per the operator's brief, not
silently built):
  - Retro-pair/cdiff templating machinery, the `b3ubot ledger` CLI
    surface.                                                  -> U2.4
  - P3's orchestrator, the DAG-compilation step, anything past P2.
                                                                -> P3
  - Rewriting steps/step.10.txt / step.11.txt's own historical
    "§9-table" mentions -- those are retro artifacts, not live docs
    (see (1)).                                        -> not applicable
  - Adding a `blocked(<reason>)` state to b3ubot's own status
    vocabulary (b3u's has one, b3ubot's §0 does not) -- not asked for,
    would be inventing a different state machine than the one this
    step was told to ground itself in.                -> not applicable

## (3) Verification gates

    G1  The live end_to_end.md ledger table round-trips byte-
        identically with zero mutations: both the raw table text
        (`emit_ledger_table_text(parse_ledger_table_text(text)) ==
        text`) and the full document
        (`replace_ledger_table_in_doc(doc, table) == doc` when `table`
        was parsed straight from `doc`) -- verified live, PASS (after
        the 2-line formatting-bug fix in (1); the table did NOT parse
        clean before that fix, a real, reported finding, not
        papered over).
    G2  Every real row in the live table passes
        `validate_row_invariants` (0 violations across all rows,
        including the documented U0.1-U0.4 founding-row exception) --
        verified live, PASS.
    G3  Structural round-trip after a real mutation: `transition()` on
        a live row, and `add_row()` with a long wrapping title, both
        re-parse to the same structured fields via
        `parse_ledger_table_text(emit_ledger_table_text(table))` --
        verified live, PASS (2 dedicated tests plus the actual U2.3
        row dogfood update in 2.8, which is this gate exercised for
        real, not just in tests).
    G4  Every legal transition (todo->active, todo->dropped,
        active->done, active->dropped) succeeds and records the right
        field; every illegal transition (todo->done, active->todo,
        done->anything x2, dropped->anything x2, unknown status)
        raises `IllegalTransitionError` with a message asserted on
        content, not just presence -- verified live: 4/4 + 6/6 PASS.
    G5  Every required-field omission (active missing/empty step, done
        missing commit, dropped missing reason) raises
        `MissingTransitionFieldError` with message-content assertions
        -- verified live: 4/4 PASS.
    G6  `add_row()`: append succeeds, duplicate id raises
        `DuplicateUoWError`, the U3.9/U3.9a split pattern replays
        cleanly with a lettered id, unknown `after=` and out-of-range
        `phase=` both raise `UnknownUoWError` -- verified live: 5/5
        PASS.
    G7  `record_commit()`: backfills without changing status; empty
        commit raises `MissingTransitionFieldError` -- verified live:
        2/2 PASS.
    G8  9 refuse-loudly cases against synthetic malformed table text
        (bad border char, inconsistent border, non-blank continuation
        cells, wrong cell width, empty phase block, missing Ledger
        heading, unclosed fence) plus 1 heading-number-independence
        case (a "## 3. Ledger" heading, not "## 11.", still locates
        correctly) -- verified live: 10/10 PASS.
    G9  NO REGRESSION: full suite before this step: 205 passed/11
        skipped (post-step.11 baseline, re-confirmed live before any
        edit). After: 245 passed/11 skipped. Delta: +40 passed (17 in
        test_ledger_table_roundtrip.py + 23 in
        test_ledger_table_transitions.py), 0 skipped delta, 0
        failures.
    G10 end_to_end.md updated same commit as the work: the 4 stale
        section references, the 2 formatting-bug fixes, the §4 U2.3
        bullet -> DONE with *Delivered:* line, and the §11 ledger row
        -> done/step.12/PENDING (performed through the new API, 2.8) --
        confirmed present via `git diff --stat` before committing.
    G11 Hygiene: b3ubot porcelain clean after each commit; retro via
        the SHARED `v3_claude/scripts/step_gdiff` (not a b3ubot-local
        copy).

## (4) LOCKs

    C-12-A  b3ubot porcelain clean after each commit.
    C-12-B  SEPARATE ERROR/MODULE HIERARCHY FROM U2.1/U2.2: no class in
            table_errors.py subclasses or is subclassed by
            StepFileError; table_parser.py/table_emitter.py/
            table_transitions.py import nothing from parser.py/
            emitter.py (the two formats stay genuinely independent,
            per design.md §2.3's own "scope, precisely" note).
    C-12-C  NO SILENT SCOPE CREEP: U2.4 (retro-pair/CLI machinery) and
            P3 are named out of scope in (2) above, not silently
            begun.
    C-12-D  NO SEMANTIC VALIDATION IN THE PARSER: table_parser.py
            enforces structural shape only (border/cell-width/
            continuation-blankness); status-vocabulary legality lives
            exclusively in table_transitions.py -- mirrors
            app/ledger/parser.py not validating section-title
            vocabulary (U2.1's own established asymmetry, applied
            again here).
    C-12-E  DONE/DROPPED ARE TERMINAL BY CONSTRUCTION: verified by
            test (G4: done->anything and dropped->anything both raise,
            2+2 dedicated cases) and by reasoning tied directly to
            end_to_end.md §12's monotonic-numbering rule (named in
            table_transitions.py's module docstring and
            docs/ledger_table_schema.md, not asserted without
            justification).
    C-12-F  THE DROPPED-REASON CONVENTION IS A REASONED DESIGN
            DECISION, NOT A COPIED PRECEDENT: no real `dropped` row
            exists in either repo's live ledger (verified by live grep
            before designing, named explicitly in (1) and
            docs/ledger_table_schema.md) -- the parenthetical-in-Title
            convention is derived from the REAL U1.5/U1.6 precedent for
            a DIFFERENT kind of annotation, not fabricated from
            nothing and not claimed as an existing dropped-row
            pattern that doesn't exist.
    C-12-G  "BYTE-STABLE" IS NOT SILENTLY REDEFINED (inherited
            discipline from C-11-F): the live table achieves true
            byte-identity with zero mutations (G1); the WEAKER
            structural guarantee applies ONLY to rows touched by
            add_row/transition/record_commit, named loudly in
            table_emitter.py's own module docstring,
            docs/ledger_table_schema.md, and end_to_end.md's own
            *Delivered:* line.
    C-12-H  SAME-COMMIT DISCIPLINE IS SCOPED HONESTLY, NOT OVERCLAIMED:
            table_transitions.py/table_emitter.py's docstrings and
            docs/ledger_table_schema.md's own "What this API can and
            cannot enforce" section both state plainly that git
            atomicity cannot be enforced from inside a library call --
            only the two things this API DOES mechanically guarantee
            (done requires commit=; replace_ledger_table_in_doc makes
            "update ledger + rest of doc unchanged" one function call)
            are claimed as guarantees.
    C-12-I  A REAL FORMATTING BUG WAS FIXED, NOT MASKED: the parser's
            strict per-cell-width check was NOT loosened to tolerate
            the 2 one-character-narrow rows found in (1) -- the source
            document was fixed instead, and the finding is named in
            this file, docs/ledger_table_schema.md, and end_to_end.md's
            own *Delivered:* line.
    C-12-J  standing locks inherited: B-3 (no disclosure/no push); B-6
            (this step's gates are T0/T1 deterministic pytest
            assertions; no AI review tier involved). B-1/B-2/B-4/B-5
            are moot -- this step invokes no cppcc, no provider, no
            egress.

## (5) Open questions

    Q-12-A  Should `LedgerRow`/`LedgerTable` gain a `to_doc_text()` or
            similar single-call convenience that combines
            parse-from-file + mutate + write-back, rather than making
            every caller compose `parse_ledger_table_from_doc` +
            `transition`/`add_row` + `replace_ledger_table_in_doc` +
            `Path.write_text` by hand (as this step's own dogfood
            script in 2.8 did)? LEAN (built this way): no, not yet --
            U2.4's `b3ubot ledger` CLI surface is the natural home for
            that convenience (a CLI command IS that composed call),
            and building it here would be scope creep ahead of knowing
            U2.4's actual shape. Revisit if U2.4 finds the composition
            awkward enough to want a library-level helper underneath
            the CLI, not just a CLI wrapper.
    Q-12-B  Should `add_row()` also accept an explicit `title_raw`-
            style verbatim override, the way U2.1/U2.2's `StepFile`
            keeps raw alongside flattened fields? LEAN (built this
            way): no -- `add_row()` only ever produces a row with NO
            source bytes to be verbatim about (it is new content by
            definition), so there is nothing for a raw field to
            capture; `raw` on `LedgerRow` exists purely for the PARSE
            side (mirrors why StepFile.title_raw/header_raw are
            populated only by parse_step_text, never by hand-built
            StepFiles in U2.2's own precedent).
    Q-12-C  Should `validate_row_invariants` also check that a
            `dropped` row's title-appended reason is non-trivial
            (e.g. reject a reason of just whitespace or a single
            word)? LEAN (built this way): no -- `transition()` already
            requires a non-empty `reason=` at write time
            (`MissingTransitionFieldError` on empty), and
            `validate_row_invariants` is a RETROSPECTIVE structural
            check (does the annotation convention exist at all), not a
            content-quality judgment; over-validating prose quality
            here would be scope creep past what §12's prose actually
            asks for ("dropped with a reason", not "dropped with a
            GOOD reason").

## (6) Acceptance

    - [x] app/ledger/table_errors.py (2.1, C-12-B).
    - [x] app/ledger/table_parser.py: LedgerRow/LedgerTable,
          parse_ledger_table_text/_from_doc/_file (2.2, G1, G8, C-12-D).
    - [x] app/ledger/table_emitter.py: emit_ledger_table_text,
          replace_ledger_table_in_doc (2.3, G1, G3, C-12-G).
    - [x] app/ledger/table_transitions.py: transition, record_commit,
          add_row, validate_row_invariants (2.4, G2-G7, C-12-E,
          C-12-F, C-12-H).
    - [x] tests/test_ledger_table_roundtrip.py -- 17/17 PASS (2.5, G1,
          G3, G8).
    - [x] tests/test_ledger_table_transitions.py -- 23/23 PASS (2.6,
          G2, G4-G7).
    - [x] docs/ledger_table_schema.md (2.7).
    - [x] end_to_end.md: stale §9/§10 -> §11/§12 corrected (4 places);
          2 one-character-narrow rows fixed; §4 U2.3 bullet -> DONE
          with *Delivered:*; §11 ledger row for U2.3 -> done/step.12/
          PENDING, written THROUGH the new API (2.8, G10, C-12-I).
    - [x] Zero regressions: 205p/11s -> 245p/11s (G9).
    - [x] Retro pair: this file + step.12.diff.txt + step.12.gdiff.txt
          via the SHARED `v3_claude/scripts/step_gdiff` (G11).

## (7) Hash backfill

    WORK commit (table_parser/emitter/transitions/errors + tests + docs + end_to_end.md corrections + this file + ledger):  f8e0f9f
    GDIFF commit (step.12.gdiff.txt via scripts/step_gdiff):                                                                c99c904

End of step.
