---
title: Add row_grain to the react-table table-state schema
type: task
status: completed
description: >-
  react-table's published table-state schema declares no row_grain property, though the component
  reads it, its CLAUDE.md documents it, and league's generation pipeline emits it. The schema cannot
  validate a state the system routinely produces.
base_uri: user:task/league/data-views/add-row-grain-to-the-react-table-state-schema.md
created_at: '2026-09-10T21:08:58.414Z'
entity_id: af687bf8-5e85-4de5-97c5-c888678f70fa
finished_at: '2026-09-10T21:14:36.501Z'
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-10T21:21:30.557Z'
visibility_analyzed_at: '2026-11-05T23:48:43.437Z'
---

## Problem

`schema/state/table-state.json` in `repository/active/react-table` lists `sort`, `columns`, `prefix_columns`, `where`, `q`, `row_axes`, `subjects`, `rank_aggregation`, `disable_scatter_plot`, `disable_column_controls`, `disable_multi_sort` and `scatter_plot_options`. It does not list `row_grain`.

`row_grain` is nonetheless a first-class part of the state:

- `repository/active/react-table/CLAUDE.md` documents it in the Table State Structure block and again under the row-grain control, which reads `table_state.row_grain[0]`
- league's data-view generation emits it — job `12470d29-f194-432f-b1fe-9ce1d4fd7bf2` carries `row_grain: ["player"]`
- league's query branch seeds it explicitly at `libs-server/data-views/derive-view-from-query-result.mjs:223`

So the published schema cannot validate a state the system routinely produces, and anything that validates against it either rejects a legitimate state or (with no `additionalProperties: false`) passes it while checking nothing about it.

## Context

Surfaced by the signal #129436 investigation into data-view generation. It is not the cause of that crash and not on its critical path — the crash was the absent `sort` key, covered by [[user:task/league/data-views/normalize-generated-table-state-before-it-reaches-the-client.md]].

It is worth doing in the same sweep because the generation resolver's missing `row_grain` validation ([[user:task/league/data-views/close-the-key-set-on-the-generated-data-view-resolver.md]]) wants a schema declaration to check against.

## Plan

- [x] Add `row_grain` to `schema/state/table-state.json` as an array of strings, with a description matching how the component reads it
- [x] Check whether any other documented state key is missing from the schema, rather than fixing only the one that was noticed
- [x] Confirm nothing in react-table or league validates against the schema in a way the addition would newly reject

## Outcome

Done in react-table, commits `3b2b5ec` and `2a43167`. Committed only — no package release, no deploy.

The enumeration turned up a second gap the framing above missed: the table state has TWO schema copies under one `$id`. The published `schema/state/table-state.json` lacked `row_grain`; the bundled `SCHEMAS['table-state']` in `src/validators/schema-resolver.mjs` — which is what the runtime validator actually compiles — had `row_grain` but lacked `q` and `subjects`. Both now carry the same property set, and `test/table-state-schema-parity.spec.mjs` asserts they stay equal.

A third defect surfaced while building the verification harness and was fixed in the same sweep: `schema/columns/column-definition.json` declared `"type": "function"` for the `getValue` member of `cellProps`, which is not a draft-07 type, so ajv refused that file and the three that `$ref` into it. The type is dropped (JSON Schema cannot express a callback; the member is never serialized) and `test/schema-files-compile.spec.mjs` now walks `schema/` asserting every file validates and compiles.

`yarn verify` green at head: 428 passing, 0 failing.

## Verification

Ran as specified, against the exact stored `table_state` from job `12470d29-f194-432f-b1fe-9ce1d4fd7bf2`, with the negative control the plan called for:

- before — `row_grain` undeclared, real state accepted, wrong-type `row_grain` (a bare string) **accepted**
- after — `row_grain` declared, real state accepted, wrong-type `row_grain` **rejected** with `/row_grain: must be array`

The two readings differ. The real state passes both ways, confirming it proves nothing alone. Nothing is newly rejected: no league file imports react-table's JSON schema, and league validates `row_grain` through its own `libs-server/validators.mjs:354` schema (array of strings, enum player|team, min 1 max 1), which is strictly narrower than the addition.
