---
title: Gate data-view URL param renames and cover the client URL parser
type: task
status: completed
description: >-
  Shipped the preventive half of the splits-to-row_axes URL defect: a production-URL param gate
  whose accepted-key sets are imported rather than grepped, a declared legacy-alias registry, and
  first spec coverage for the client URL parser.
base_uri: user:task/league/data-views/gate-data-view-url-param-renames.md
created_at: '2026-08-05T20:18:23.238Z'
entity_id: 667890c4-705f-4faa-a557-6e402d879b18
finished_at: '2026-08-05T20:32:21.675Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
relations:
  - follows [[user:guideline/write/write-software.md]]
  - follows [[user:guideline/write/write-javascript.md]]
  - relates [[user:task/league/data-views/fix-week-sort-unreachable-and-legacy-splits-param.md]]
  - relates [[█████████████████████-████████████-██████-██-███-███████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-08-05T21:21:24.962Z'
visibility_analyzed_at: '2026-11-05T23:48:53.123Z'
---

# The data-view URL surface has no rename gate and no spec coverage

## Problem

`splits` was renamed to `row_axes` in June 2026. The rename shipped a compat rule on the versioned localStorage snapshot path (`v2_to_v3` in `libs-shared/data-view-storage/migrations.mjs`) — but a URL query string carries no version field and never enters that chain. So production short URLs silently rendered at the wrong grain for six weeks, and those that also sorted on the lost axis produced unexecutable SQL.

Measured over all five affected routes: **289 of 423** `splits`-carrying URLs had a non-empty value, and **10** sorted on a lost axis. (The figures this task was written with — 188 and 3 — covered `/data-views` alone; see Notes.)

Nothing caught it, and nothing would have:

- `check-saved-view-param-coverage.mjs` checks saved views' **column** params. `splits` is a top-level `table_state` key carried in the URL. Different surface.
- `check-data-view-sql-validity.mjs` never exercises `sort` and reported `GATE OK` on the tree where the originally-found three URLs failed `EXPLAIN`.
- `app/core/data-views/parse-table-state-from-url.mjs` — the file holding the entire blast radius — has **zero** spec coverage.

Fixed reactively in `924dfe328`. The gate and the coverage are what stop the next one.

## Plan

- [x] **Coordinate with the gate-domain owner first**
  - Messaged the `███████████████████████████████████` session (thread `repl-27d4d032`) with the proposed boundary before building. No reply received; built as a sibling script and touched none of their files. The boundary holds on inspection — theirs takes source-tree consumers against `db/schema.postgres.sql`, mine takes production `urls` rows against `SHARE_LINK_URL_SCHEMA`. Different input, different oracle, different remedy.

- [x] **Gate top-level `table_state` key renames against production URLs**
  - `db/adhoc/check-data-view-url-param-coverage.mjs`. Compares the query-string keys of the production `urls` table against `SHARE_LINK_URL_SCHEMA` (both `table_state` and `view`) plus `LEGACY_URL_PARAM_ALIASES`. Exit 0 clean / 1 findings / 2 tooling error, with `--json` and `--verbose`.
  - The accepted-key sets are **imported, not grepped**. That is the structural fix for the failure that blinded the model gate: a presence-grep cannot distinguish a consumer from prose about one, so comment-stripping is a patch where an imported oracle removes the failure mode outright.
  - Negative control run both directions: deleting the `splits` alias reports it over 423 URLs at exit 1; restoring returns exit 0. A sampled hash was confirmed to resolve to a real row via `convert_from(url_hash,'UTF8')`.
  - Population is **863 URLs, not 682**. `/leagues/:lid/players-table` is a `<Navigate>` redirect preserving `location.search` (`app/views/routes.js`), so its 159 URLs reach the same parser; `/plays` does too. A check scoped to `/data-views` would have missed them.

- [x] **Cover the client URL parser**
  - `test/data-views.parse-table-state-from-url.spec.mjs`, 18 tests. Legacy `splits`, present `row_axes`, both keys, explicitly-empty `row_axes`, malformed JSON, non-array values, the boolean/object params, view fields, and two registry-invariant tests.
  - **No alias harness was needed.** The module imports only `#`-prefixed specifiers and bare `react-table/...` paths, and both packages declare the matching `imports` field — it loads under plain Node today. The plan's premise was wrong; see the drift observation.
  - The legacy spelling is now the declared `LEGACY_URL_PARAM_ALIASES` registry rather than a bespoke function per key, applied by re-running the schema parser over a params copy so a legacy value is parsed by the same typed logic as a current one.
  - Writing the spec surfaced a live defect the pre-existing code shared in one direction: the schema parser's array branch falls back only for absent or unparseable input, so a well-formed non-array `row_axes` flowed through as an object. Now guarded for the current and aliased key alike.

- [x] **Resolve the "redundant" splits migration** (plan premise refuted, nothing deleted)
  - The item asked to delete one of two homes. Neither is redundant: `v2_to_v3` is the **only** reach to a snapshot stored at version 2, and `STORAGE_SCHEMA_VERSION` was 2 in production between `e1cf78e71` and `cabf950de`, so those snapshots exist and skip both earlier steps. The saved-view rule catches snapshots entering at version 0 or 1 and separately serves the versionless saved-view read path. `LEGACY_URL_PARAM_ALIASES` covers short URLs.
  - The item also mis-stated the location: the `924dfe328` rule landed in `data-views-saved-view-migration.mjs`, which runs as `v1_to_v2`, not `v0_to_v1`.
  - Deleting either would silently drop a real cohort's row axes, so the resolution is a comment in `migrations.mjs` pinning why all three exist — the mistaken reading has now been written down once and will recur otherwise.

## Shipped

- `ccc0a3d81` — alias registry, gate script, parser spec, non-array `row_axes` guard.
- `95f822604` — `migrations.mjs` comment pinning the three homes; gate wired into the CLAUDE.md rename gate list.
- `38b3d2ffa` — gate header: named acceptance procedure, explicit non-CI status, no-suppression-mechanism note, per the gate-domain owner's conventions.

Suite 3258 passing (3240 baseline + 18 new) in a worktree at HEAD carrying only these files, since the shared tree held a sibling's uncommitted scoring-format work. Spec proven red 3/18 with the alias registry emptied. Gate is deliberately NOT in CI — it reads production.

**Deployed** 2026-08-05 in `ed672ea7b` (`yarn deploy:all`, run by the `███████████████████████████████████` session), bundle `main.cd932cca.js`.

Verified after the deploy, in both halves:

- **Shipped.** The minified alias literal `splits:"row_axes"` is present in two deployed chunks. The pattern is discriminating rather than vacuous: `row_axes` alone appears in five chunks, and `git log -S` confirms the literal was introduced only by `ccc0a3d81`, so it could not have matched the pre-deploy bundle.
- **Behaviorally correct on real data.** All **423** production legacy URLs replayed through the deployed source (tree clean at `ed672ea7b`): 289 recover their axes exactly, 134 are correctly empty, 0 lost, 0 non-array. Note this proves the refactor **preserved** the reactive fix's behavior — it does not discriminate `ccc0a3d81` from `924dfe328`, which already shipped the recovery. No-regression is the correct claim.

## Notes

- **Blast radius was under-measured.** The original figures (188 of 682 non-empty, 3 sorting on a lost axis) covered `/data-views` alone. Over the full five-route population the true figures are **289 non-empty `splits` of 423, and 10 sorting on a lost axis** — the axis-sort cohort is more than 3x what was recorded. 134 carry `splits=[]`, 22 carry `row_axes`, 0 carry both. Saved views were clean (0 of 186).
- A SQL `LIKE '%splits=%5B%5D%'` filter does **not** isolate empty-`splits` URLs: the `%` in URL-encoding collides with `LIKE` wildcards, so the pattern degrades to "contains 5B then 5D" and matched all 423. Classify URL query strings in code, not in `LIKE`.
- Follow-up not taken (no operator ask): 130 of 231 `app/core/` files import the `@core`/`@components` webpack aliases and remain un-importable by mocha. An alias harness would unblock them and the silent `mapDispatchToProps` defect class, but it was not needed for this task and is its own piece of work.
