---
title: Generate games page views from a natural-language ask
type: task
status: completed
description: >-
  Add games_view as a third view kind on the data-view generation rail, so an ask in words sets the
  /games controls object, validated by the game-sample engine and shareable.
base_uri: user:task/league/generate-games-views-from-natural-language.md
created_at: '2026-10-01T15:45:05.974Z'
entity_id: aca60e04-922b-4bba-bac4-460fbf9cbb57
finished_at: '2026-10-24T23:06:55.492Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[user:task/league/reduce-data-view-generation-latency.md]]
started_at: '2026-10-24T17:14:22.188Z'
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-24T23:49:09.405Z'
visibility_analyzed_at: '2026-11-05T23:49:32.793Z'
---

# Implementation Plan: Generate games page views from a natural-language ask

## Overview

- Let a signed-in, generation-enabled reader type an ask on `/games` ("WR targets in losses since 2023, cut by trailing at halftime") and get the page set to answer it, the way data views and plays views already do.
- The agent emits the games page's controls object; league validates it against the game-sample engine's own gate and the page applies it, unsaved, exactly as a link would.
- Impact: the games engine becomes askable in words by a reader who does not know its field grammar, and a generated sample becomes shareable at `/s/<share_id>` with its timeline.
- Ordering: follows the /games progressive-reveal redesign (league `b73f2f95f`), which made the page state one controls object hydrated from the link, and the player name search (league `06fdb3c00`), which the agent's player tool reuses. Inherits the latency profile tracked in `user:task/league/reduce-data-view-generation-latency.md`.

## Background

- The generation rail already serves two view kinds, `data_view` and `plays_view`. League runs no model: it supplies a job row, a dispatch seam, CLI tools, a socket and a validator; the agent runs on base's managed rail. See `docs/data-views-system.md` section "Agentic view generation" (around lines 1920-2079 of that file).
- Plays is the template: its whole catalogue sits in the prompt (`libs-server/plays-view/generation/plays-view-generation-contract.mjs`), it has three tools (`scripts/plays-view-{validate-table-state,preview-view,emit}.mjs`), and it applies through `plays_views_actions.plays_view_changed` (`app/core/plays-view-generation/sagas.js`).
- Generic and reused as-is: job queue and drainer (`libs-server/data-views/generation/generation-job-queue.mjs`, `generation-drainer.mjs`), timeline backfill and live feed, spend limits and kill switch, failure contract and signals (keyed by view kind), shares (`api/routes/view-generation-shares.mjs`), history (`api/routes/view-generations.mjs`), the action and reducer factories (`app/core/view-generation/`), and the presentational `ViewGenerationControl` with `react-agent-timeline`.
- Per-kind registrations to extend: the `view_kind` CHECK on `data_view_generation_jobs` and `view_generation_shares` (`db/schema.postgres.sql`), `VIEW_KINDS` (`generation-job-queue.mjs`), `GENERATION_FRAME_PREFIXES` (`api/sockets/data-view-generation.mjs`), `DOMAIN_PROMPT_BLOCKS` (`libs-server/data-views/generation/base-session-client.mjs`), `GENERATION_EMISSION_DOMAINS` (`api/routes/data-views.mjs`), the OpenAPI enum in `api/routes/view-generations.mjs`, and the share page's branch (`app/views/pages/view-generation-share/view-generation-share.js`).
- The games page: `app/views/pages/games/games-page.js` holds one controls object in component state, hydrated from the link by `read_games_search_params`; `build_games_document` (`games-page-document.mjs`) maps controls to an engine document, which `resolve_game_sample_document` (`libs-shared/game-sample/game-sample-expression.mjs`) accepts or refuses with a string. There is no inverse from document to controls and no store action to apply a page state.
- `build_rows_expression` lives in `app/views/components/game-sample-filter/game-sample-filter-options.js`, a component-tree file the server cannot import cleanly.
- The agent CLI `scripts/query-game-sample.mjs` is a flags CLI, not the stdin-JSON tool contract (`agent-tool-runner.mjs`) the rail's tools use.

### Assumptions

- The sandbox role `league_data_view_reader` can read every table the engine reads: checked, grants exist for `nfl_games`, `nfl_plays`, `player`, `player_gamelogs`, `player_receiving_gamelogs` in `db/schema.postgres.sql`; UNCHECKED by execution — the preview tool step runs the engine under `NODE_ENV=sandbox` to observe it.
- The field catalogue fits whole in the prompt: checked, `GAME_SAMPLE_METRICS` has 65 entries composed with 5 perspectives and 5 windows, a grammar rather than a list.
- The emission route keeps the envelope key `table_state` for every kind: operator decision 2026-10-01, documented as "the view kind's page state" rather than renamed.
- One thread config profile serves every kind and a second is refused: per `docs/data-views-system.md`; games script paths are added to that one profile's system prompt.
- One entitlement (`users.data_view_generation_is_enabled`) and one shared budget cover every kind: per the same docs; games inherits both.
- The agent reads the contract from the league checkout tracking master while the API enforces only after a deploy: per the docs' deploy-order caveat; the contract lands before or with the API change.

## Design

- **Approach:** add `games_view` as a third view kind on the existing rail. Nothing forks; each per-kind registry gains one entry.
- **Generation contract:** the emission envelope is unchanged (`expressible`, `explanation`, `inexpressible_reason`, `view_name`, `table_state`). For `games_view`, `table_state` is the games controls object: `subject_kind`, `pids`, `teams`, `positions`, `season_from`, `season_to`, `season_types`, `filter_rows`, `cut_rows`, `metric_ids_by_grain`.
- **Validator (`resolve_generated_games_controls`)**, returning the rail's `{ok, errors[{code, path, message}], table_state}`:
  - structural schema check of the controls object (closed set of keys, subject kind enum, row shape);
  - `build_games_document`, where a null document (incomplete subjects) is an error, because an expressible answer must sample something;
  - `resolve_game_sample_document`, with its refusal string mapped to one error.
- **One shared source for the page and the server:** move the controls defaults, `has_games_subjects`, `describe_games_seasons`, `build_games_document` and `build_rows_expression` into `libs-shared/game-sample/`, so the page, the validator and the tools read the same code.
- **Shortfall check moves into the data-view validator.** `validate_emission`'s `shortfall_column_ids` coupling reads data-view columns and does not apply to games.
- **Agent tools (four):**
  - `validate_controls` — the validator above; errors are the answer.
  - `preview_sample` — runs the engine as the requester with a small member limit and returns n and quantiles per cut, catching a sample that validates and is empty.
  - `search_players` — name to pid, on `libs-server/search-players.mjs`.
  - `emit` — validates and delivers through `deliver_emission`.
  - One declaration (`GAMES_GENERATION_TOOLS`) read by both the scripts and the prompt, as plays does.
- **Prompt block:** the field grammar, team abbreviations, positions and subject kinds, byte-stable at the head of the prompt via `DOMAIN_PROMPT_BLOCKS.games_view`.
- **Server wiring:** schema CHECKs, `VIEW_KINDS`, emission domain, frame prefix `GAMES_VIEW_GENERATION`, a new `api/sockets/games_view.mjs` mirroring `plays_view.mjs`, the OpenAPI enum, failure codes registered in `generation-failure-contract.mjs`.
- **Client state:** none added. The link is the page state, so a generation result, a share and history all apply by navigating to `/games?${write_games_search_params(controls)}`.
- **Client generation:** `app/core/games-view-generation/` built from the shared factories; the saga navigates to a completed result's link when the pending generation id matches. An edit sends the current controls as `input_table_state`.
- **UI entry point:** the shared `ViewGenerationControl` sits above "Whose games" as the other way to answer the opening question, shown only to generation-enabled accounts; its timeline expands beneath it; the explanation shows as a caption over the answer.
- **Sharing:** the share page gains a `games_view` branch that dispatches the shared controls and renders `GamesPage`.
- **Naming check:** `games_view`, `GAMES_VIEW_GENERATION`, `user_games_views`, `resolve_generated_games_controls`, `GAMES_GENERATION_TOOLS`, `validate_controls`, `preview_sample`, `search_players` are full words, searchable, and follow the existing `plays_view` / `PLAYS_` spellings.
- **Knowledge Base Impact:**
  - `docs/data-views-system.md` agentic section: add `games_view`, its contract and tools, and the note that `table_state` names each kind's page state.
  - League feature documentation that lists generation-capable surfaces gains `/games`.
  - Task relation: `relates` to `user:task/league/reduce-data-view-generation-latency.md`.
  - No new guideline or tag.
- **Scratch:** none; every artifact is code, specs or docs.
- **Shape:** one bounded task, not an orchestrator.

## Notes

- The schema change, `yarn export:schema` and the code reading the new kind ship in one commit (league `CLAUDE.md`, `docs/guides/schema.md`).
- The thread config profile is generated in the user base by `cli/league/build-data-view-generation-profile.mjs` into `config/thread-config-profile/league-data-view-generation.json`; regenerate rather than hand-edit, and do not add a second profile.
- Deploy order: the contract and tool scripts reach master before the API change that enables `games_view`, so the agent never reads an older contract than the API enforces.
- End-to-end verification is post-deploy only: enqueue writes a job row (refused by `dev:smoke`'s read-only database) and the agent delivers to the production API, and ████████████'s container cannot run `dev:smoke` at all. The population check runs as real asks on production with an entitled account.
- Known gap: a reload or reconnect mid-run stops the page following that run, since games has no view list to catch up from; and no route lists an account's past games views (plays has `api/routes/plays.mjs` list/get).
- Revised 2026-10-24 against the redesign (league `ebed9c6a2`): the /games link now carries the whole controls object (`write_games_search_params` / `read_games_search_params`, round-trip specced), and controls gained `competitive_plays_only`. A generated result applies by navigating to its link, so the page needs no store slice, and a share opens `/games?...`.
- Revised 2026-10-24: the rail places every result on a saved-view row (`GENERATION_VIEW_TABLES` in `publish-generation-job-change.mjs`, enqueue's empty-row insert, `place-generation-result-on-view.mjs`, cancel's delete), so games gets a `user_games_views` table shaped like `user_plays_views`, holding the controls as `table_state`. That keeps queue, placement, cancel and publish unchanged and gives a run history.
- Generation stays signed-in and entitlement-gated, as for the other kinds; `POST /games/sample` stays public.
- Product gate: the operator holds the /games full release and its menu link until this view lands (2026-10-24, on ██████████████████████████████████████████).

## Plan

### Shared games controls

- [x] **Move the controls model into libs-shared** (league `fc8fff5ce`: `games-controls.mjs` and `game-sample-filter-rows.mjs`)
  - Files: `libs-shared/game-sample/games-controls.mjs` (new), `app/views/pages/games/games-page-document.mjs` (removed), `app/views/components/game-sample-filter/game-sample-filter-options.js`, `app/views/pages/games/games-page.js`, `test/app.games-page-document.spec.mjs` (renamed to `test/libs-shared.games-controls.spec.mjs`)
  - Purpose: one source for the page, the validator and the agent tools.
  - Changes: move `GAMES_*` constants, `GAMES_DEFAULT_CONTROLS`, `has_games_subjects`, `describe_games_seasons`, `build_games_document`, `read_games_search_params` and `build_rows_expression` with its helpers; repoint imports; leave no re-export shim.
  - Verify: `TZ=America/New_York NODE_ENV=test npx mocha --no-config --exit test/libs-shared.games-controls.spec.mjs` passes, and `grep -rn "games-page-document" app test` returns nothing against a control grep for `games-controls` that returns hits.

- [x] **Apply a page state through its link** (league `0924a8f94`)
  - Files: `app/views/pages/games/games-page.js`
  - Purpose: one path for a typed link, a generation result, a share and history.
  - Changes: a completed generation's controls navigate to `/games?${write_games_search_params(controls)}`; the page's existing link hydration does the rest.
  - Verify: the round-trip spec already holds every control; a `dev:smoke` walk applies a completed run.

### Games generation contract and tools

- [x] **Validator** (league `4e2f0cfba`, at `libs-server/games-view/generation/`)
  - File: `libs-server/game-sample/generation/resolve-generated-games-controls.mjs` (new)
  - Purpose: the server-side gate for a games emission.
  - Changes: structural schema check, `build_games_document` null as error `subjects_incomplete`, engine refusal mapped to `document_refused` with its message.
  - Verify: spec with a valid controls object, an unknown key, a null-document case and an engine-refused field id; each fails when its check is removed.

- [x] **Contract and prompt block** (league `4e2f0cfba`)
  - File: `libs-server/game-sample/generation/games-view-generation-contract.mjs` (new)
  - Purpose: the agent's instructions and vocabulary, and the single tool declaration.
  - Changes: `GAMES_GENERATION_TOOLS`, emission schema, `GAMES_AGENT_INSTRUCTIONS` carrying the field grammar from `game-sample-catalogue.mjs`, team abbreviations and positions.
  - Verify: spec that every catalogue metric id appears in the instructions and every declared script path exists.

- [x] **Four tool scripts** (league `4e2f0cfba`; `preview_sample` not yet run under the sandbox role, which needs the generation container)
  - Files: `scripts/games-view-validate-controls.mjs`, `scripts/games-view-preview-sample.mjs`, `scripts/games-view-search-players.mjs`, `scripts/games-view-emit.mjs` (new)
  - Purpose: the agent's stdin-JSON tools on `agent-tool-runner.mjs`.
  - Changes: each takes its `input_keys` from `GAMES_GENERATION_TOOLS`.
  - Verify: run `preview_sample` under `NODE_ENV=sandbox` against the sandbox credential and observe a non-empty sample for "all teams, 2025, regular" — this is the execution check for the sandbox-grant assumption; run each script with a malformed input and observe a non-zero exit.

### Rail registration

- [x] **Schema and server registries** (schema league `04fb3afc0`; registries league `44c478866`, 269 specs passing; shortfall attribution is per domain via `attributes_shortfall_to_columns`)
  - Files: `private/db/adhoc/<date>-allow-games-view-generation.sql` (new), `db/schema.postgres.sql`, `libs-server/data-views/generation/generation-job-queue.mjs`, `libs-server/data-views/generation/base-session-client.mjs`, `libs-server/data-views/generation/validate-emission.mjs`, `api/routes/data-views.mjs`, `api/routes/view-generations.mjs`, `libs-server/data-views/generation/generation-failure-contract.mjs`
  - Purpose: admit `games_view` end to end on the server.
  - Changes: extend both CHECKs; create `user_games_views` (columns as `user_plays_views`) and register it in `GENERATION_VIEW_TABLES`, `generated-view-records.mjs`, `generation-edit-inputs.mjs` and `GENERATED_TABLE_STATE_RULES` (complete a controls object over `GAMES_DEFAULT_CONTROLS`); `VIEW_KINDS`; `DOMAIN_PROMPT_BLOCKS.games_view`; `GENERATION_EMISSION_DOMAINS.games_view`; move the shortfall coupling into the data-view resolver; OpenAPI enum; games failure codes.
  - Verify: `yarn test:isolated` on the generation specs (enqueue, validate-emission, emission route, failure contract) with a new case per kind; re-export the schema immediately before commit and read its diff.

- [x] **Socket** (league `44c478866`, `api/sockets/games_view.mjs`)
  - Files: `api/sockets/games_view.mjs` (new), `api/sockets/data-view-generation.mjs`, socket registration in `server.mjs` or its socket index
  - Purpose: request and collect frames for games.
  - Changes: `GAMES_VIEW_GENERATION` prefix; handlers mirroring `plays_view.mjs`.
  - Verify: socket spec that a games request enqueues a `games_view` job and a collect of a `plays_view` job on the games socket is refused.

- [x] **Thread config profile** (user-base `45ea09bddc`)
  - Files (user base): `cli/league/build-data-view-generation-profile.mjs`, `config/thread-config-profile/league-data-view-generation.json`
  - Purpose: the agent can find the games scripts.
  - Changes: add the games script paths to the generated system prompt; regenerate.
  - Verify: diff of the regenerated profile shows only the added paths.

### Client

- [x] **Games generation module** (league `0924a8f94`: games-specific sagas apply a completed run by navigating to its link, since the shared sagas require a saved-view list; 163 specs passing)
  - Files: `app/core/games-view-generation/{actions,reducer,sagas,index}.js` (new), `app/core/reducers.js`, `app/core/sagas.js`
  - Purpose: submit, follow and apply a games generation.
  - Changes: factories with prefix `GAMES_VIEW_GENERATION`; the apply saga navigates to the result's link when the pending generation id matches.
  - Verify: saga spec that a completed update applies once, and a stale generation id does not apply.

- [x] **Entry point on the page** (league `0924a8f94`)
  - Files: `app/views/components/games-view-generation-control/index.js` (new), `app/views/pages/games/games-page.js`, `app/views/pages/games/games.styl`
  - Purpose: ask in words above "Whose games".
  - Changes: `connect()` wrapper over `ViewGenerationControl`; hidden without the entitlement; caption from the explanation.
  - Verify: `dev:smoke` walk with an enabled account: submit an ask, watch the timeline, observe the controls replaced and the sample loaded; with a disabled account the control is absent.

- [x] **Share page branch** (league `0924a8f94`)
  - File: `app/views/pages/view-generation-share/view-generation-share.js`
  - Purpose: open a shared games generation.
  - Changes: `games_view` links to `/games?${write_games_search_params(table_state)}`; a refusal renders the header only. Add the kind to `libs-shared/page-routes.mjs`.
  - Verify: `dev:smoke` walk opening a share link in a signed-out tab shows the same sample count as the owner's page.

### Knowledge Base Updates

- [x] **Generation docs** (league `09ae268a7`)
  - File: `docs/data-views-system.md`
  - Changes: add `games_view` to the agentic generation section (contract, tools, prompt block, apply path) and state that `table_state` names each kind's page state.
  - Verify: `grep -n "games_view" docs/data-views-system.md` returns the new section.

- [x] **Feature documentation** (user-base `2f2f658dcb`; league `e8bccd115`, `7dbad65ab`)
  - Changes: add `/games` generation wherever league documentation lists generation-capable surfaces; find them with `base search "view generation"` and `grep -rln "plays_view" docs text/league`.
  - Verify: each hit that lists plays views also lists games.

### Testing & Validation

- [x] **End-to-end population check** (2026-10-24, live rail, five of five completed)
  - Run five varied asks (one per subject kind, one with a cut, one inexpressible) under `dev:smoke` and record per ask: status, branch, validator errors before emit, sample n. Failure mode caught: a contract that validates but yields empty or wrong-grain samples.
  - Verify: four expressible asks complete with n greater than zero and the inexpressible one completes as a refusal.

- [x] **Deploy in contract-first order** (`029a03ded`, 2026-10-24; contract and tools reached master before the API registration)
  - Contract and scripts on master first, then `yarn deploy:all` with the operator's go.
  - Verify: one production ask completes and its share link opens signed out.
