---
title: Normalize generated table_state to a complete state before it reaches the client
type: task
status: completed
description: >-
  The data-view generation path applies the agent's emitted table_state verbatim, so a state
  omitting an optional key (sort, where, prefix_columns) reaches react-table as-is. This produced
  signal 129436. Normalize at the apply seam.
base_uri: user:task/league/data-views/normalize-generated-table-state-before-it-reaches-the-client.md
created_at: '2026-09-10T20:40:32.998Z'
entity_id: 580bafe1-54e9-42ad-ab07-c8c141162f55
finished_at: '2026-09-10T22:08:40.750Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: High
public_read: true
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-10T22:08:40.750Z'
visibility_analyzed_at: '2026-11-05T23:48:55.572Z'
---

## Problem

Signal #129436 (TypeError, `Cannot read properties of undefined (reading 'find')`, 2026-09-10 19:26:43 UTC) was produced by data-view generation job `12470d29-f194-432f-b1fe-9ce1d4fd7bf2` (user 134, completed 2026-09-10T19:26:39.752Z — 3.2 seconds before the crash). Its emitted `table_state` carries exactly `columns, prefix_columns, row_axes, row_grain, where` and no `sort`.

Nothing on the generation path completes the state:

- `libs-server/data-views/generation/resolve-generated-table-state.mjs:691` — returns `{ ok, errors, table_state }` with `table_state` byte-identical to what the agent emitted. Every key check is `if (entries === undefined) continue`.
- `libs-server/data-views/generation/generation-job-queue.mjs:460` — `complete_generation_job` stores the envelope with `JSON.stringify(result)`, unmodified.
- `app/core/data-view-generation/sagas.js:121` — `handle_generation_update` spreads `payload.result.table_state` onto the view through `data_view_changed`, replacing the view's whole table_state.

The react-table fix (commit `4079531`, pinned at league `a5fe36df5`) stopped the crash. It did not repair the state: the view on the page still has no `sort`.

The contrast that proves the seam is missing is the short-link path, which does build a complete state — `app/views/pages/short-url-resolver/short-url-resolver.js:78-82` composes from `app/core/data-views/parse-table-state-from-url.mjs:136-151`, which always returns `columns`, `prefix_columns`, `where` and `sort` as arrays.

## Blast radius, measured

Against league_production, scoped to `view_kind = 'data_view'` — the earlier figure of "6 of 273" was measured across BOTH view kinds and overstated this defect, since the other 68 jobs are `plays_view` rows gated by a different resolver:

- 205 completed data-view jobs carry a `table_state`. 2 omit `sort`, 7 omit `where`, 1 omits `prefix_columns`, 1 omits `columns`.
- The 2 sort-less ones are exactly the crash run `12470d29-f194-432f-b1fe-9ce1d4fd7bf2` and the columnless `9c663f0b-7d13-4552-a7b5-9cc972e8cd33`. The other 4 sort-less results in the raw 273 are `plays_view`, where an omitted `sort` is the plays resolver's question, not this one's.
- 0 of 206 rows in `user_data_views` lack `sort`, so nothing malformed has persisted to a saved view. The crash-run view was never saved.

The same conflation is a trap on the key-set side, and it is worth recording because it nearly fired: `group_by` appears in 37 completed job results and would look like a key the closed set must refuse — but all 37 are `plays_view`. Across the 205 data-view rows the keys are exactly the six the set closes around, and `row_grain` is only ever `['player']` or `['team']`.

The reason it recurred rather than being a one-shot is browser persistence: `app/core/data-views/sagas.js:606` (`persist_table_state_to_browser`) snapshots the live table_state, and `app/core/data-views/sagas.js:675` restores it on reload. `libs-shared/data-view-storage/storage.mjs:78-86` (`sanitize_table_state`) preserves the omission. So the sort-less state survived reloads until the user changed the view.

`is_valid_table_state` (`libs-shared/data-view-storage/validate.mjs:5-10`) requires only that `columns` and `prefix_columns` be arrays and one be non-empty, so it is not the gate that would have caught this — nor should it be, since it also guards the save path.

## The shape is already in the repo

Only the REGISTRY branch is defective. The query branch already produces a complete state, and says why:

```
libs-server/data-views/derive-view-from-query-result.mjs:215-224
  table_state_seed: { columns, prefix_columns, sort: [], where: [], row_axes: [], row_grain: [] }
```

Its comment — "sort, where, offset and limit are deliberately empty: the moment this view is saved the user owns them" — is the argument for doing the same on the registry branch. This is not a design to invent; it is a seed to mirror.

## Why the obvious seam does not work

Normalizing inside `resolve_generated_table_state` alone reaches nobody. `libs-server/data-views/generation/validate-emission.mjs:286` returns `{ ok, branch, errors }` and DISCARDS `resolved.table_state`, and the emit route at `api/routes/data-views.mjs:2041-2043` builds `result` from the raw `emission` (the query branch is the only one that overlays anything, via `query_delivery`).

So the change spans three files or it does nothing:

1. `resolve-generated-table-state.mjs` — return a seeded state rather than the input
2. `validate-emission.mjs:286` — surface `resolved.table_state` instead of dropping it
3. `data-views.mjs:2041` — build the stored `result` from the resolved state on the registry branch, as it already does for the query branch

The read-side-normalization precedent is [[user:task/league/guard-column-param-select-filter-against-scalar-param-value.md]].

## Plan

- [ ] Write the red-before-green spec below and confirm it fails on today's tree
- [ ] Thread the resolved state through the three files above so the stored `result` carries a complete `table_state`
- [ ] Decide and apply the disposition for the six already-stored malformed job results — see below
- [ ] Re-run the production measurement and confirm newly completed registry jobs emit complete states

## The stored results — 12 rows, not six

"Six" was the count of sort-less results across BOTH view kinds and was never the backfill set. Scoped to `view_kind = 'data_view'` and to ANY missing key, 12 of the 205 stored results hold an incomplete `table_state`:

| missing | rows |
| --- | --- |
| `where` only | 5 |
| `row_axes` only | 4 |
| `row_axes` + `where` | 1 |
| `sort` only | 1 — the crash run `12470d29` |
| `columns`, `prefix_columns`, `row_axes`, `sort`, `where` | 1 — the columnless `9c663f0b` |

That changes what this is. It is not two anomalies; it is a steady low-rate incompleteness across the whole generation history, dominated by omitted `where` and `row_axes` rather than by the `sort` that produced the crash. The sort-less case was the one that happened to reach an unguarded read.

Urgency is nonetheless low, and lower than when this task was written: the apply seam added in `5d274f4b5` completes an incomplete stored state on the way in, so re-collecting any of the 12 no longer applies a malformed state. Backfill is cleanup, not a fix.

Recommendation stands — backfill so no stored state is malformed, rather than leaving a permanent read-side shim to outlive its reason. It is a production write and needs the operator's go-ahead.

## Verification

Red-before-green: a spec feeding `handle_generation_update` a `result.table_state` of `{ columns: [...], prefix_columns: [...], where: [...] }` and asserting the view's table_state carries `sort: []`. Confirm it fails on today's tree.

Pair it with a server-side spec asserting that a registry emission omitting `sort` produces a STORED result carrying `sort: []`, since that is the half the client spec cannot see.

## Related

[[user:task/league/data-views/add-row-grain-to-the-react-table-state-schema.md]] covers the schema key this state carries and the schema does not declare.
