---
log: 031
title: "The corpus was one column behind"
date: 2026-08-23
task: "#94 (remap phase 2, widened from the setlist to the corpus)"
tags: [surface, tooling, migration, haskell, corpus]
shareable: true
---

## Cap (what & why)
PLN: *"so many tracks still having ^42 instead of ^41 for kicks its confusing."*
Remap phase 2 column-aligned the board so column N IS orbit N — but it was
scoped to the **fourteen** files of the OPAL set. Everything else still spoke
the layout from when gMask held CC 41, where an orbit's gate row started one
column late: d1 → ^42, d2 → ^43.

## Manœuvre (how)
`button-column-audit.py` found **169** such files and, decisively, **all of them
shift +1** — one layout generation, not a scatter of typos. That made it a
migration rather than a cleanup.

Ran the migration corpus-wide, which is a genuinely different test from running
it on a setlist, and it refused two batches rather than writing bad Haskell.
Both refusals were real bugs:

1. **The chain-step boundary was depth-blind.** A step ends at the next line
   starting with `$`/`#` — but a `$` inside an open paren is a *continuation*.
   Multi-line bodies truncated after one line, left depth 1, and the balance
   check refused the file. 10 of 169 were unmigratable. Also: the line
   mentioning `^NN` is often *not* the step start, so the fix walks back to the
   nearest enclosing step whose segment both balances and contains the hit.
2. **Commenting out a `$`-step can under-apply the step above it.** Found only
   by `silent-eval`: 4 files stopped compiling while 165 were fine.

       $ midiOff "^57" (mask "<[t f]!0 ...>")   -- a FUNCTION, still hungry
       $ stack [ ... ]                          -- ITS ARGUMENT

   Comment the `stack` and GHC says *"midiOff is applied to too few arguments"*
   and the whole do-block dies — all twelve orbits — which is far worse than the
   cross-orbit trigger the comment-out was avoiding. `#`-steps stay commentable;
   `$`-steps are now annotated and left live, with the FIXME saying so.

## Prise (findings / artifacts)
- `b5ad8b6` on `claude/button-column-migration` — 169 tracks + both tool fixes.
- Layout-shifted files **169 → 0**. Own-column button refs **35.7% → 78.6%**.
- pvlint **30 → 26 errors**; warnings 996 → 1041 (the +45 are annotated-but-live
  `$`-steps honestly reporting a cross-column reach).
- Non-compiling tracks **17 → 17, identical sets**. Silent orbits: the same 19,
  same tracks. `fix-button-roles`: 3 clean rewrites applied, then 0.
- `--plan` re-run: **0 moves.** Converged in ≤2 passes.

## Sel (the shareable learning)
**Diff the SETS, not the counts.** "17 before, 17 after" means nothing until you
know it is the *same* 17 — that comparison is the only thing that proved the
final tree introduced zero new breakage, and it's what caught my first attempt
breaking 4 files while the totals looked plausible.

Corollaries worth keeping:
- **Measure risk on the set; exercise a rewriter on the whole corpus.** The rare
  shapes only exist out there. Both bugs were invisible to 14 files.
- **A refusal that costs a batch is cheap; a rewrite that compiles and is wrong
  is not.** Both bugs were findable *because* the tool bailed instead of guessing.
- **Errors down, warnings up can be the honest outcome.** I'd rather 45 warnings
  saying "this still fires across columns" than silence.
- Line numbers move when you insert FIXMEs — strip `(line N)` before diffing
  verdicts, or everything looks changed.

## Hameçon (hook)
"A paren-balance checker saved me from writing broken Haskell into 169 files.
Then the type checker caught the bug the paren checker couldn't see — commenting
out one line had quietly starved the function above it."

## Sillage (what it unlocks)
`^41` is d1's kick gate everywhere now, so muscle memory transfers between
tracks. Both `migrate-columns.py` fixes benefit any future `.tidal` rewriter.
Remaining: 118 same-role button pairs waiting on gSel (#54), and 17
pre-existing non-compiling tracks nobody has triaged — see memory
`project_button_migration_debts`.
