---
title: Close the key set on the generated data-view resolver
type: task
status: completed
description: >-
  The data-view generation resolver checks no key it does not recognize and never inspects row_grain
  at all, so a foreign or fabricated key rides through to the client. The plays resolver closes its
  schema; this one does not.
base_uri: user:task/league/data-views/close-the-key-set-on-the-generated-data-view-resolver.md
created_at: '2026-09-10T20:41:19.994Z'
entity_id: 741f7a6b-340f-469e-adbf-4ec79e460876
finished_at: '2026-09-10T21:21:29.579Z'
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:29.579Z'
visibility_analyzed_at: '2026-11-05T23:48:44.835Z'
---

## Problem

`libs-server/data-views/generation/resolve-generated-table-state.mjs` never enumerates the keys of the emitted `table_state`. There is no `unknown_table_state_key` code in `RESOLVER_ERROR_CODES` (lines 76-84) and no `Object.keys(table_state)` loop anywhere in the file — verified by grep returning zero matches for both.

Its plays sibling does close the schema: `libs-server/plays-view/generation/resolve-generated-plays-table-state.mjs` declares `PLAYS_TABLE_STATE_KEYS` and rejects anything outside it, with the explicit argument that a foreign key is not a harmless extra but evidence the model answered in the wrong domain. The comment there names the data-view resolver as the module it is the sibling of, so the asymmetry looks like an omission rather than a decision.

`row_grain` is the sharpest instance. The data-view resolver contains no occurrence of the string at all, so a generated `row_grain` is neither validated against the grain vocabulary nor rejected — it passes through untouched to the client and on to the query builder. The crash-run job `12470d29-f194-432f-b1fe-9ce1d4fd7bf2` emitted `row_grain: ["player"]`, which happened to be correct; nothing at the gate would have caught a fabricated one.

## Scope note

This is the lower-severity sibling of two other findings from the same trace and should be sequenced after them:

- [[user:task/league/data-views/normalize-generated-table-state-before-it-reaches-the-client.md]]
- [[user:task/league/data-views/require-columns-in-the-generated-data-view-resolver.md]]

Deciding the closed key set is the prerequisite for both, since normalization needs to know which keys a complete state has.

## Plan

- [ ] Enumerate the keys a data-view `table_state` may carry, from `schema/state/table-state.json` in react-table plus `row_grain`
- [ ] Add `unknown_table_state_key` and the key loop, mirroring the plays resolver
- [ ] Validate `row_grain` against the grain vocabulary the query builder enforces, so the gate's verdict and the executor's agree
- [ ] Check the 273 completed production job results for keys the new set would refuse, before shipping

## Verification

Red-before-green: feed the resolver a state carrying `group_by` (a plays-only key) and assert `ok === false`. It returns `ok: true` today. Pair it with an unperturbed valid state and require the two verdicts to differ.
