## U12 part 8 — deletes the lossy `normalizeColumn`, and ratchets it
shut
Independent of the #2525 → #2528 → #2530 stack; touches only
`@fusion/core` exports.
This closes **one of the two `@deprecated (workflowColumns, U12)`
markers** the unit was named for.
### The hazard
`normalizeColumn` coerced an arbitrary value to a **legacy** column,
rewriting every workflow-defined custom id to `triage`. Silent data loss
for any project whose workflow declares a column outside the six
built-ins — and it sat one line away from `normalizeColumnId`, which
sanitises structurally and passes real ids through.
The dashboard picked the wrong one for its entire task-ingest path until
that was diagnosed; `useTasks.ts` and `routes-trait-rekey.test.ts` still
carry the notes from that fix. So this is not a hypothetical footgun —
it already fired once, on the surface where it mattered most.
Deleted rather than left deprecated because it has **zero callers
anywhere in the workspace**. It was pure exported hazard: a lossy
coercion next to its safe twin, waiting to be picked again.
### The ratchet is the point
`no-lossy-column-coercion-export.test.ts` bans the **behaviour, not the
identifier**: it walks every exported single-argument function whose
name mentions "column" and fails if one maps a valid custom id onto a
different legacy id. Re-adding `normalizeColumn` under any name trips
it.
Verified by actually reintroducing the function — **two of the three
cases fail, including the name-agnostic one**. That last detail is what
stops it being a guard that checks nothing.
Coverage stated plainly: deleting an unused export has no behaviour to
revert-check. The compile is the proof it had no callers; the ratchet is
the proof it cannot return.
---
## Answering the standing question: does anything still read the raw
`workflowColumns` flag?
**Yes. Exactly two sites, and both are U2b's.** I am not able to close
this out, and here is the complete list rather than a summary:
```
packages/core/src/store.ts:38,43 ← the definition
packages/core/src/task-store/moves.ts:9,363 ← `useWorkflow`
packages/core/src/task-store/workflow-task-create-ops.ts:11,351 ← move-policy preflight
```
That is the whole list in production code. Everything else that greps is
a comment, a test that writes the flag deliberately to exercise the dead
path, or the unrelated `workflowColumns.*` i18n namespace for the
Columns editor panel.
**Why I have not deleted the settings key.** It cannot go while those
two read it — the key is what they read. And the two are not separable
from each other: `workflow-task-create-ops.ts:351` computes the
`movePolicyPreflight` that `moves.ts` consumes and validates, and
un-gating the preflight alone would start evaluating workflow move
policies (with their plugin-gate side effects) while the branch that
consumes the result stays off. That is a behaviour change with no
consumer, which is worse than either state.
**Status of the blocker.** U2b has not landed. `main` at `919f68f9b`
still has both reads; the program's merged history goes `#2466 → #2467 →
#2468 (characterisation only) → #2469 → #2479 → #2500 → #2512 → #2513`,
with no convergence PR. PR #2468 was Phase A2 **steps 1–2 only** — the
differential characterisation — and the convergence that deletes one of
the two move paths was never merged.
So the honest state of the unit: everything U12 owns is done except the
two reads that U2b owns, and the settings key that cannot be deleted
until they are gone. If you want me to take U2b itself, say so — I have
the inventory and the divergence list, and I would want the current U2b
worker stood down from `moves.ts` first.
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>