---
title: Express the conditional line row axis to data view generation
type: task
status: completed
description: >-
  Let a generated data view use the line row axis by deciding how a conditional axis is expressed to
  a model that has not yet chosen the market_type param the condition turns on, then landing and
  measuring it. Three denial sites; the params-blind catalog is the hard one.
base_uri: user:task/league/data-views/express-conditional-line-row-axis-to-generation.md
created_at: '2026-09-09T01:54:29.138Z'
entity_id: bdb8e834-e494-4825-b5f2-0f8dbe1f1318
finished_at: '2026-09-09T05:30:55.344Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
started_at: '2026-09-09T02:23:10.983Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/software-task.md
updated_at: '2026-09-09T05:30:55.344Z'
visibility_analyzed_at: '2026-11-06T00:17:45.396Z'
---

## Goal

Let a generated data view use the `line` row axis, by deciding how a CONDITIONAL axis is expressed to a model that has not yet chosen the `market_type` param the condition turns on — then landing it and measuring it through the benchmark A/B.

## Why

`line` is a real shipped feature that generation cannot reach. Two commits built it (`bbb359fca`, `b26b83cb3`), the `player_year_week_line` identity declares `row_axes: ['year', 'week', 'line']`, and production carries 13 saved views using it since 2026-09-01. A betting alt-ladder view is composable by hand and unreachable by the agent.

## The three denial sites

Fixing only the first two produces a refusal, because the third contradicts them:

1. `libs-server/data-views/generation/generate-data-view.mjs:223` — the prompt says "the only two values are year and week", and tells the agent to check `describe_column` rather than assume.
2. `libs-server/data-views/generation/resolve-generated-table-state.mjs:82` — `ROW_AXES = ['year', 'week']`, and anything else is rejected as `unknown_row_axis`. Its own comment says adding an axis is a one-line edit here.
3. **The catalog reports `line` on ZERO columns, and this is the hard one.** `derive_supported_row_axes_from_source(column_definition)` takes a definition and NO params — it reads `source.supports_row_axes` or the identity's static grain. No server column definition declares `line` anywhere (verified by grep). Meanwhile the client's `game_prop_row_axes(params)` IS a function of params, returning `['year','week']` for a standard market and adding `line` only for a ladder one. So with the prompt and resolver fixed, `describe_column` would still tell the agent `line` is unavailable on exactly the columns that support it, and the prompt defers to the tool.

## Operator ruling, 2026-09-08

**Resolve per intended params**, with the negative control landed FIRST. `describe_column` accepts the params the agent means to use and answers exactly, mirroring what react-table already does per column instance. The agent asks twice: what market types exist, then what axes that choice allows.

Note the shared-signature constraint: `derive_supported_row_axes_from_source` is also called by the query engine at `libs-server/get-data-view-results.mjs:825` and `:1588`, so a signature change reaches the engine, not just the catalog.

## Three silent failure modes the contract must fail LOUD on

Each of these currently produces a view that looks like it worked:

1. **`line` on a standard market** — the server refuses, and per the client comment the refusal renders as one generic banner with the server's message dropped, so the user sees no cause.
2. **Two ladder columns naming different market types** — `validate-line-axis-columns.mjs` refuses, because axis rows key on the raw line value and "199.5 passing yards" would sit beside "199.5 rushing yards" as one bet. Same generic banner, same dropped message.
3. **A stale market type constant** — post-rename, a saved view naming an old constant returns zero rows silently rather than failing.

## Plan

1. **Done** — the negative control is landed (`8645b3e1c`): `qb-passing-yards-prop-lines-2024-week-1`, the suite's first betting market instruction.
2. **Done** — the params-aware resolution landed across the three denial sites (`d07168fe8` + `88874d8bd`, deployed on every host; design and file-by-file steps in the Progress sections below, gates 322 green).
3. **Done** — the LADDER instruction is landed (`bf0cc892b`): `qb-passing-yards-prop-ladder-2024-week-1`, both `INSTRUCTIONS` SQL and `build_reference_table_states`. `--check` clean, `--probe` 15/15 on 2026-09-09 data. The reference executes through the query engine (axis shipped 2026-09-01), so probate does not depend on the generation resolver change.
4. **Done** — the A/B ran and resolved as MEASURED-not-a-true-A/B (a before-arm is unobtainable on the self-updating generation mount; see Progress — session `███████████████████████████████`).

## Progress — 2026-09-09 (session `███████████████████████████████████████████`)

### Design, validated against production data

The params-aware resolution mirrors the client's `game_prop_row_axes(params)` exactly: a player-game-prop column carries `['year','week']` unconditionally, plus `line` when its `market_type` is a ladder alt member. The shared-signature constraint is handled by making the params argument GATE the change: `derive_supported_row_axes_from_source(column_definition, params)` returns the grain-derived answer when `params === undefined` (the engine call sites at `get-data-view-results.mjs:825` and `:1588`, and the catalog build) and the per-instance truth only when params are passed (describe_column). Engine behaviour is therefore byte-identical; nothing at the two engine sites needs to change.

Empirical facts the implementation should trust (measured 2026-09-09):

- The engine's tie-break on tied line values is `player.pid asc` — the emitted query ends `order by 5 DESC NULLS LAST, "player"."pid" asc`. A line sort on the ladder instruction ties (11 QBs at 349.5), so the oracle must replicate it (`order by measure desc, pid asc`), and it does, row-for-row.
- The oracle discriminates: market_type (standard passing tops near 275.5 vs ALT 349.5), source_id (FanDuel 349.5 Baker/Brock/C.J./Dak/Jared vs DraftKings 339.5 Stroud/Stafford/Mahomes/Tua/Goff), the line split itself (259 rows split vs ≤33 pooled; the pooled view renders 199.5, so it fails both `min_rows: 100` and the leaders), and week scope.
- The stale-market_type refusal already fails loud: the resolver enumerates `market_type` values (103), so a post-rename constant lands as `invalid_param_value`. The two line-axis refusals (line on a standard market, two ladder quantities) need NEW resolver checks.

### The remaining code change, in order

1. `derive-supported-row-axes.mjs`: params-gated branch for `is_player_game_prop`. `params === undefined` → grain answer (`[]`, unchanged); else `['year','week','line']` for a ladder `market_type` (read the first array element), else `['year','week']`. Import `is_ladder_market_type` from `line-axis-sources.mjs`.
2. `describe-column.mjs`: accept `params`, recompute `supports_row_axes` through the shared function on the RAW definition (import `data_view_column_definitions`), defaulting `params` to `{}` so betting columns report `['year','week']` even uninstantiated. Update the JSDoc's "Which time splits" comment.
3. `scripts/data-view-describe-column.mjs`: add `'params'` to `input_keys` and pass it through.
4. `resolve-generated-table-state.mjs`: add `'line'` to `ROW_AXES` (travels into `generated_table_state_schema.row_axes` enum automatically). Add line-axis validation when `row_axes` includes `'line'`: requires `week`; at least one ladder source (`resolve_line_axis_sources` non-empty) so the rung domain exists; and the same-quantity rule via `validate_line_axis_columns` (pass the raw definitions registry as `defs`). Assign one new error code, e.g. `line_axis_constraint`.
5. `generate-data-view.mjs` AGENT_INSTRUCTIONS: replace "the only two values are year and week" with the line conditional (line is available only on a player-game betting column whose `market_type` is a ladder alt market; it requires `week`; every ladder column shares one `market_type`); add `params` to the `describe_column` tool spelling in `# Tool invocation`.
6. Specs to update/add: `data-views.generation-resolver-row-axes.spec.mjs` (bare `['line']` is now rejected for a constraint reason, so `accepts every real row axis` needs a ladder-context special case; add positive ladder acceptance and the three refusals), `data-view-agent-tools.spec.mjs` (describe_column params-aware `supports_row_axes`), and direct tests that `derive_supported_row_axes_from_source(definition)` stays unchanged while the params form adds `line`.
7. Offline gates: `yarn test:isolated` on `test/data-view*generation*.spec.mjs test/data-view-agent-tools.spec.mjs`; prettier/lint on touched files.

### The A/B, after the code change

Post a bulletin claiming the arm window before every arm; read the deployed sha at BOTH ends. Before-arm runs on current synced code (production generation refuses `line`, so the ladder target should FAIL):

```bash
NODE_ENV=production node scripts/data-view-benchmark-run.mjs \
  --view-kind data_view --repeat 1 \
  --select qb-passing-yards-prop-ladder-2024-week-1 \
  --select qb-passing-yards-prop-lines-2024-week-1 \
  --select qb-passing-yards-2023 \
  --out scratch/league/data-view-generation-review/ladder-before.jsonl
```

Then commit B (the code change), sync, confirm ████████████'s league checkout carries the commit, deploy (`yarn deploy:all`), run the IDENTICAL selection as `ladder-after.jsonl`, and:

```bash
NODE_ENV=production node scripts/data-view-benchmark-diff.mjs \
  scratch/league/data-view-generation-review/ladder-before.jsonl \
  scratch/league/data-view-generation-review/ladder-after.jsonl \
  --target qb-passing-yards-prop-ladder-2024-week-1
```

Read the WARN lines, not just the verdict. The negative control must stay pooled (a split appearing on it is the regression the suite could not previously see). On SHIP record the iteration table row on the ██████████████████████████████ task — that file is owned by a sibling session, so message it (`base thread message repl-4105da6b`) rather than editing it.

## Progress — 2026-09-09 (session `██████████████████████████`)

The params-aware code change is IMPLEMENTED across the five files, per the step list above. Edits are UNCOMMITTED (session cut short by context + a broken hook dispatcher). Files:

1. `libs-server/data-views/derive-supported-row-axes.mjs` — params-gated branch for `is_player_game_prop`: `params === undefined` → grain answer unchanged; else `['year','week','line']` for a ladder market_type (first array element; `is_ladder_market_type` imported from `line-axis-sources.mjs`), else `['year','week']`.
2. `libs-server/data-views/generation/describe-column.mjs` — accepts `params` (default `{}`), recomputes `supports_row_axes` through `derive_supported_row_axes_from_source(data_view_column_definitions[column_id], params)`.
3. `scripts/data-view-describe-column.mjs` — `input_keys` now `['column_id','param_keys','params']`, passes `input.params` through.
4. `libs-server/data-views/generation/resolve-generated-table-state.mjs` — `ROW_AXES` = `['year','week','line']` (schema enum follows automatically); added `line_axis_constraint` error code; validation when `row_axes` has `'line'`: requires `week`, >=1 ladder source (resolve_line_axis_sources non-empty), same-quantity via validate_line_axis_columns (defs = raw registry); resolver signature takes `column_definitions` defaulting to the raw registry.
5. `libs-server/data-views/generation/generate-data-view.mjs` — AGENT_INSTRUCTIONS row-axis rule rewritten to carry the line conditional; `params` added to the describe_column tool spelling.

IMPORTANT — SHARED FILE COLLISION (state 2026-09-09 wrap): `resolve-generated-table-state.mjs` and `generate-data-view.mjs` carry BOTH this change's hunks and a sibling's uncommitted union of door checks from the review-improve rail (`career_scope_split_by_its_own_axis`, `year_offset_without_year_axis`, `requires_row_axes`/`agent_note` in the catalog build, a year_offset AGENT_INSTRUCTIONS bullet) — live sibling owner is repl-a7db5eab, who is partial-staging their hunks so working-tree changes on both lanes stay put. This session checkpointed its three pure files (derive-supported-row-axes.mjs, describe-column.mjs, scripts/data-view-describe-column.mjs) at d07168fe8; the two shared files remain MIXED and uncommitted. Commit ONLY your own hunks — `git diff` in the shared files shows both lanes, so a whole-file `git commit -- <share>` absorbs a sibling until their parts land. The describe-column parse failure repl-a7db5eab flagged is fixed (incoming param renamed to column_params).

NOT DONE, next-session order: specs (special-case `line` in `accepts every real row axis`; add positive ladder acceptance + the three refusals — no ladder column, two ladder quantities, line without week — to `test/data-views.generation-resolver-row-axes.spec.mjs`; add params-aware `supports_row_axes` cases + grep for any `input_keys` pin to `test/data-view-agent-tools.spec.mjs`); offline gates `yarn test:isolated test/data-view*generation*.spec.mjs test/data-view-agent-tools.spec.mjs` + prettier/lint; commit the change on its own commit (resolver per the collision note); sync (cli/sync), confirm ████████████'s league checkout carries the commit (████████████ is the generation container mount per config/league-topology.json; it self-updates hourly at :40 via `~/bin/update-league-checkout`), deploy `yarn deploy:all` (docs/guides/ship.md — the shared tree is dirty with sibling work, so a throwaway worktree may be required); then the A/B per the task body (bulletin before EVERY arm, deployed sha both ends; before-arm currently unrun — `NODE_ENV=production node scripts/data-view-benchmark-run.mjs --view-kind data_view --repeat 1 --select qb-passing-yards-prop-ladder-2024-week-1 --select qb-passing-yards-prop-lines-2024-week-1 --select qb-passing-yards-2023 --out scratch/league/data-view-generation-review/ladder-before.jsonl`, after-arm identical to ladder-after.jsonl, then `data-view-benchmark-diff.mjs ladder-before.jsonl ladder-after.jsonl --target qb-passing-yards-prop-ladder-2024-week-1`). Negative control (prop-lines) must stay pooled. On SHIP, message `repl-4105da6b` for the iteration-table row (never edit that file).

Ladder-context facts the specs can rely on (verified this session): player game prop columns declare `source: { grain: 'player' }` and `is_player_game_prop: true` (so the params-less catalog answer is `[]`; `data-views.row-axes-declaration-contract.spec.mjs` is the client picker and unaffected); raw ids `player_game_prop_line_from_betting_markets`, `player_game_prop_american_odds_from_betting_markets` etc.; `market_type` values include the ALT members (`player_game_prop_types` spreads `player_game_alt_prop_types`), so `['GAME_PLAYER_ALT_PASSING_YARDS']` passes the resolver's value check.

## Progress — 2026-09-09 (session `██████████████████████████`)

The code change is FULLY COMMITTED, in two commits, and the offline gates are green.

- Specs updated and green: `yarn test:isolated test/data-view*generation*.spec.mjs test/data-view-agent-tools.spec.mjs` → 322 passing, 0 failing. Row-axes spec: bare `['line']` rejected for `line_axis_constraint` (not `unknown_row_axis`), positive ladder acceptance, the three refusals (no ladder column; two ladder quantities; line without week), and direct `derive_supported_row_axes_from_source` param-gate tests. Agent-tools spec: params-aware `supports_row_axes` cases (function + script level). No spec pins describe_column `input_keys` to `['column_id','param_keys']`. Prettier + ESLint clean on all touched files.
- The change lands across two commits because the shared files' hunk mix (this lane + the career_scope rail) made one clean commit impossible:
  - `d07168fe8` (checkpoint, pre-existing): `derive-supported-row-axes.mjs`, `describe-column.mjs`, `scripts/data-view-describe-column.mjs`.
  - `88874d8bd` (this session, explicit pathspecs): `resolve-generated-table-state.mjs` (`ROW_AXES` gains `'line'`; `line_axis_constraint` gate requiring week + a ladder column + one shared market_type; resolver takes `column_definitions`), `generate-data-view.mjs` (line-conditional row-axes rule; `params` in the describe_column tool spelling), plus the two specs. Used the sanctioned `GIT_GUARD_SKIP_PRETTIER` bypass: HEAD's committed career_scope region is already prettier-dirty, and reformatting it would fold another lane's code in.
- The career_scope rail is committed separately as `42fb2a4fb` by its owner (review-improve rail). Note the brief's attribution was wrong: `repl-4105da6b` is the DEPLOY chain, not the review loop. Live review-rail sessions to coordinate with for the iteration-table row: `████████████████████████████████`, `████████████████████████████████`. The shared tree stays high-flux around `resolve-generated-table-state.mjs`; re-verify `git status` before touching it.

NOT DONE, next-session order, in the task body's A/B sequence: bulletin then BEFORE-ARM (unrun) on current deployed code (production refuses `line`, so the ladder target should FAIL) → `cli/sync` → confirm ████████████'s league checkout carries `88874d8bd` → deploy `yarn deploy:all` per `docs/guides/ship.md` → bulletin + AFTER-ARM (identical selection) → `data-view-benchmark-diff` → verdict per the loop workflow. Negative control `qb-passing-yards-prop-lines-2024-week-1` must stay POOLED. On SHIP, message the review-rail owner for the iteration-table row (never edit that file).

## Progress — 2026-09-09 (session `███████████████████████████████`)

The before-arm RAN, and it proves the A/B's premise had already collapsed: the arm measured the AFTER state, not a refusal, so this lane ends as MEASURED-not-a-true-A/B, not SHIP-by-diff.

Weapons-grade timing detail, all verified by shas read at BOTH ends of the arm:

- The lane commit had already auto-reached origin. `████████` pushes on a ~1-minute cycle, so by the time this successor session sat down, origin already carried `88874d8bd`; the recorded "let it reach origin" step was already spent. ████████████'s generation checkout (the container mount) self-updates hourly at :40, and at 04:40:04 it fast-forwarded `6859ba37e -> e9092e36d`, which contains the lane commit.
- The before-arm dispatched its three instructions at 04:39:44 / 04:41:39 / 04:45:46. The ladder target (04:45:46) therefore ran on the already-fixed generation checkout. Its raw row in the JSONL confirms it: `row_axes: ['week','line']`, `correct: true`, returned 100 rows matching the FanDuel ALT oracle leaders. That is the change answering correctly, captured in a file named `ladder-before.jsonl`.
- The API hosts also advanced under the arm: `league` and the second API host read `96e325853` at arm start and `e9092e36d` (carries `88874d8bd`) at arm end — a sibling deploy landed mid-window. So the change is already live on production generation AND the API, and there is no deploy left for this lane to run.
- Result table (all three on the fixed rail): `qb-passing-yards-2023` correct 8 turns / 4,897 out; `qb-passing-yards-prop-lines-2024-week-1` (negative control) correct 18 / 13,049, STILL POOLED, returned 10 rows — the regression the suite previously could not see does NOT appear; `qb-passing-yards-prop-ladder-2024-week-1` (target) correct 26 / 21,364, returned 100 rows.

Disposition, per [[user:workflow/league/review-data-view-generation-run.md]] Phase 3 and the 2026-09-09 precedent row in its own iteration table: a true before-arm is NOT obtainable for this change because the generation rail reads `AGENT_INSTRUCTIONS` from the container-mounted checkout, which already carries the fix on every host — manufacturing a "before" means reverting that checkout or minting a second profile, which the loop has already refused. So the comparator cannot run a valid pair and a diff here would be NEED-MORE-DATA for the wrong reason (both "arms" are the same deployed code). The honest record is: MEASURED — on the deployed, already-fixed production rail, the ladder target generates a line-axis view that matches the oracle, the negative control stays pooled, and the year control stays correct. No REVERT is warranted: nothing regressed and the target is correct. The refusal->acceptance transition itself is pinned by the offline resolver specs and the suite's ladder + negative-control assertions, which is the durable artifact.

## Key locations

- League repo: `repository/active/league` (PUBLIC — no private detail in entities there)
- Generation: `libs-server/data-views/generation/{generate-data-view,resolve-generated-table-state,describe-column,build-data-view-generation-catalog}.mjs`
- Axis machinery: `libs-server/data-views/{derive-supported-row-axes,line-axis-sources,validate-line-axis-columns}.mjs`
- Client counterpart: `app/core/data-views-fields/betting-market-table-fields.js`
- Bench: `scripts/data-view-benchmark-{run,ground-truth,diff}.mjs`; suite `test/data-view-benchmark/instructions.json`
- Read before any deploy: `docs/guides/ship.md`
