---
title: Consolidate column_index onto one shared derivation
type: task
status: planned
description: >-
  column_index is recomputed in six places under three algorithms across league and react-table,
  with no test asserting they agree. Collapse them onto one exported derivation, and fold in the
  adjacent column-entry schema and naming hygiene.
base_uri: user:task/league/data-views/consolidate-column-index-derivation.md
created_at: '2026-07-31T21:49:45.269Z'
entity_id: ac42c5ed-1db0-48e8-a493-d7fdfe398ab3
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - follows [[█████████████████████-█████████-██████-█████-███████████-███████████]]
  - relates [[█████████████████████-████████████-██████████-██████████]]
  - blocked_by [[█████████████████████-█████████-██████-█████-███████████-███████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-07-31T21:56:03.387Z'
visibility_analyzed_at: '2026-11-05T23:48:45.525Z'
---

# Consolidate `column_index` onto one derivation

`column_index` is recomputed in **six independent places under three different algorithms**, across two repositories, with three more pure consumers trusting whichever producer fed them. All six must agree byte-for-byte or the client reads `undefined` — and there is no test anywhere asserting they do.

This is the organizing defect behind [[█████████████████████-█████████-██████-█████-███████████-███████████]]. That task is the point fix; this one removes the class.

Audited 2026-07-31 during [[█████████████████████-████████████-██████████-██████████]].

## The contract

`column_index` is an **occurrence ordinal among entries sharing a `column_id`**, not an absolute position. The wire format is `<column_id_or_name>_<column_index>`: the server bakes it into the SQL alias, and the client reconstructs it to read the response row back.

## The six producers

**Algorithm A — filter, then findIndex.**

1. `get_column_index`, league `libs-server/get-data-view-results.mjs:591-599`. Three call sites: `find_sort_column` (`:623`), the grouping loop in `get_grouped_clauses_by_table` (`:1483`), and the output-aggregator dispatch loop (`:1951`). The two loops build input as `[...prefix_columns, ...columns]` (`:656`, `:1912`) — **`prefix_columns` participate in the server denominator.**
2. League `app/views/pages/data-views/index.js:75-80` — a hand-rolled duplicate over `table_state.columns` only, **without** `prefix_columns`.

**Algorithm B — sorted findIndex over rendered columns.**

3. `react-table/src/table-header/table-header.js:112-123`, sorting `getAllLeafColumns()` by `columnDef.index`. A second derivation at `:124-136` then walks `table_state.columns` using the first as a counter target.
4. `react-table/src/table-cell/table-cell.js:24-35` — **byte-identical** to the table-header derivation, in its own `useMemo`. Verified identical, not merely similar.

**Algorithm C — counter while walking.**

5. `react-table/src/table-menu/table-menu.js`, `build_export_headers` (`:27-91`, counter loop `:51-79`).
6. Same file, `handle_copy_to_clipboard`'s independent counter loop (`:342-358`).

Both increment a per-`column_id` counter while walking `table_state.columns` in **array order**, assuming array order matches definition `.index` order. They are two separate reimplementations, not shared code.

**Pure consumers** (read a `column_index` prop, never derive): league `app/views/pages/data-views/data-views.js`, league `app/core/data-views-fields/game-table-fields.js`, and `table-menu.js`'s `get_cell_value` fallback (`:197-223`, fallback at `:211`).

## The wider alias surface

The emitted alias is not a single site either. `get_select_string` emits the fallback alias at `libs-server/data-views/select-string.mjs:272`, but **12 files under `libs-server/data-views-column-definitions/` hand-roll the same `` `${name}_${column_index}` `` template inside their own `main_select` — 26 occurrences.** Any consolidation must sweep all 12; changing `select-string.mjs` alone silently covers only the fallback path.

This is also why `column_index` cannot simply be replaced by a stable `instance_id`: the emitted key is ordinal-derived in 27 places. Replacing the ordinal is a bigger project than unifying its derivation, and this task is the latter.

## Scope

**In scope.** One shared derivation with one definition of the denominator, consumed by every producer above.

- The natural home is `react-table`, exported alongside `validate_table_state`, so league's client and league's server can both import it. League's server already imports from the package.
- Whatever the unified denominator is, decide `prefix_columns` explicitly — today the server includes them and league's own client duplicate does not.
- Delete the byte-identical duplicate in `table-cell.js` and the two counter loops in `table-menu.js` outright rather than aligning them.

**Out of scope.** Replacing the ordinal with a stable instance identity. That touches all 27 alias-emission sites and every cached result; it is a separate project, and this task should make it cheaper rather than attempt it.

**Ordering.** Land [[█████████████████████-█████████-██████-█████-███████████-███████████]] first. It establishes what the correct denominator is; this task then makes one implementation of it authoritative.

**Release coupling.** `parcels` pins `react-table` to a git commit SHA rather than a version range, so any react-table change here needs a deliberate pin bump in parcels before it propagates.

## Adjacent hygiene folded in

Small, in the same files, and each one a place a future change fails silently:

- **`react-table/schema/state/column-reference.json` has no `additionalProperties` key at all.** Arbitrary keys on a column entry validate silently. Add `additionalProperties: false`, and check existing saved views validate against it before shipping.
- **`react-table/src/table/table.js:400-419` — `set_column_hidden_by_index` does not hide, it removes** (`columns.splice(index, 1)`). Rename it to say so. Its sibling `set_column_hidden_by_id` (`table-column-controls.js:296-309`) is likewise a `filter`.
- **Document the two distinct `hidden` concepts.** Column *definitions* carry `hidden` meaning "hidden by default in the catalog" (`schema/columns/column-definition.json:91-93`, consumed at `column-param-select-filter-with-overrides.js:69`). There is no instance-level `hidden` at all — confirmed; `column-reference.json`'s object variant has only `column_id` and `params`. Write that down in `CLAUDE.md` so a future change does not invent a second meaning for the name.

**Deliberately NOT folded in:** the missing `$$strict` on league's `table_state_schema` and `params_with_output_schema` (`libs-server/validators.mjs:238-246`, `:113`). That is the live SQL-injection surface owned by [[█████████████████████-██████████████-████-████-█████-████████████]]. Do not touch it here.

## Plan

- [ ] **Pin the denominator**
  - Decide, and write down in `libs-server/data-views/ABOUT.md`: does the ordinal count `prefix_columns`, and does it count entries whose `column_id` has no definition? Both differ across producers today.
  - Verify: the answer is stated once, in one place, and every later step cites it.

- [ ] **Write the shared derivation**
  - File: `repository/active/react-table/src/derive-column-index.mjs` (new), exported from `index.js`
  - One function taking the entry list and returning ordinals, matching the pinned denominator.
  - Verify: unit tests covering same-`column_id` runs, `prefix_columns`, a missing definition, and an empty list.

- [ ] **Replace the four react-table producers**
  - Files: `src/table-header/table-header.js:112-136`, `src/table-cell/table-cell.js:24-35`, `src/table-menu/table-menu.js:51-79` and `:342-358`
  - Delete the duplicated derivations; call the shared one.
  - Verify: existing react-table tests pass; export headers and TSV copy still produce identical output for a fixture view.

- [ ] **Replace league's two producers**
  - Files: `libs-server/get-data-view-results.mjs:591-599`, `app/views/pages/data-views/index.js:75-80`
  - Have `get_column_index` delegate to the shared derivation, and delete the hand-rolled client duplicate.
  - Verify: `yarn test:local` — in particular the data-view golden-SQL fixtures, which embed `_${column_index}` in expected SQL and will catch any ordinal shift.

- [ ] **Audit the 12 hand-rolled alias sites**
  - Files: `libs-server/data-views-column-definitions/` (12 files, 26 occurrences)
  - These consume `column_index` rather than derive it, so they should need no change — confirm that, and note any that recompute rather than consume.
  - Verify: grep shows every occurrence reads a passed-in `column_index`.

- [ ] **Assert cross-side agreement in a test**
  - Extend the test added by the mismatch task so it now asserts a single shared implementation is used, not two agreeing ones.
  - Verify: `yarn test:local`

- [ ] **Apply the adjacent hygiene**
  - Files: `react-table/schema/state/column-reference.json`, `react-table/src/table/table.js`, `react-table/src/table-column-controls/table-column-controls.js`, `react-table/CLAUDE.md`
  - Add `additionalProperties: false`; rename the two misnamed hide-that-removes functions; document the two `hidden` concepts.
  - Verify: existing saved views validate against the tightened schema before it ships.

- [ ] **Bump the parcels pin**
  - File: `repository/active/parcels/package.json`
  - Move the `react-table` commit SHA forward, or record why parcels can stay behind.
  - Verify: parcels test suite passes against the new pin.
