---
title: Correct the ███ ████ ████ ██████'s player accounting
type: task
status: completed
description: >-
  Build the game path engine measurement foundation: an attributed snap stream shared by simulated
  and real plays, one derivation of every registry player quantity calibrated against gamelogs, and
  an as-of forward coverage backtest, then fix goal-line, quarterback, backup and fumble accounting
  and limit the proposer to priced quantities.
base_uri: user:task/league/correct-nfl-game-path-player-accounting.md
created_at: '2026-10-29T22:17:20.315Z'
entity_id: 7ac25f97-0270-42da-8d6f-fc5c73af190e
finished_at: '2026-10-30T00:42:25.788Z'
observations:
  - ████████████████████████████████████████████████
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[██████████████████████-███-████-████-█████████]]
  - blocks [[█████████████████████-███-████-█████-████-█████████-████████]]
started_at: '2026-10-29T23:28:05.694Z'
tags:
  - user:tag/league-xo-football.md
  - user:tag/nfl-betting.md
updated_at: '2026-10-30T00:42:25.788Z'
visibility_analyzed_at: '2026-11-05T23:48:39.198Z'
---

# Implementation Plan: Correct the ███ ████ ████ ██████'s player accounting

## Overview

- **Build the measurement foundation the engine lacks.** It has three parts:
  - **One attributed play stream:** a snap row that records who carried, was targeted and passed. It is the same shape for simulated snaps and real plays.
  - **One quantity derivation:** turns those rows into every registry player quantity, split into rushing, receiving and passing. It is calibrated against the gamelogs the grader settles on.
  - **An as-of forward backtest:** scores what a kickoff forecast would have said against what happened.
- **Fix the defects that fall out of it.**
  - Player yards are capped at the goal line.
  - Quarterbacks and available backups are counted.
  - A lost fumble keeps its yards.
  - The proposer emits the player quantities filed rows price, under registry names, and refuses runs that lack them.
- **Out of scope:** what the engine draws does not change. The operator's 2026-10-29 direction is to draw from the most relevant population, through archetype roles and profiles, and to retire slot fill. That belongs to [[█████████████████████-███-████-█████-████-█████████-████████]], blocked by this task.
- **Impact.** Profilers get player curves they can read. Outside sims and settled actuals compare like for like. The engine stops showing tracked players with more yards than their whole offense: in week 8, GB's tracked players total 379.6 yards a path against 372.1 for the team. Everything stays shadow-only; nothing reaches a price or a filed row.
- **Ordering.**
  - It follows the branch-row proposer (league `a0384e5f5`, done).
  - Its player curves are not cited until this lands (handoff from `███████████████████████████████████`).
  - It precedes the proposer's week-9 verify and the archetype task, whose gate is this task's forward coverage.
  - It touches nothing under the NFL kit freeze (bulletin #1317): the engine and proposer sit outside the frozen files.

## Background

### Where the defects come from (code)

- **Uncapped player yards.** `update_lineup` runs before `apply` (`libs-shared/nfl-game-path-simulation/simulate-game-path.mjs:1314-1317`) and tallies the drawn `play.yards_gained` (`:806`). The state caps the gain at the goal line only later, in `game-path-state.mjs:331-332`.
- **Only 8 players counted.** The tracked set is the top 8 RB/FB/WR/TE by pool snaps (`build-game-path-lineup-table.mjs:28,156-160`; the participation loader reads those positions only, `libs-server/nfl-game-path-simulation/load-game-path-participation.mjs:9,43`).
  - Quarterbacks are never counted.
  - An available backup's touches are counted for nobody.
  - `player_measures` holds `{nfl_team, snap_count, touch_count, touch_yards}` only (`simulate-game-path.mjs:813-823,1369`).
- **Touch definition.** A touch is the carrier on a run or the target on a completed pass (`build-game-path-lineup-table.mjs:71-90`).
  - A scramble's yards go to nobody.
  - Interceptions are folded into `is_turnover` (`derive-game-path-plays.mjs:140`).
  - The pool already selects `is_interception` (`build-game-path-pool.mjs:82`). `is_qb_scramble` is not selected.
- **Slot fill is not recorded.** A drawn play whose carrier or target cannot play keeps its yardage. Its touch goes to a tracked player of the same group (`simulate-game-path.mjs:747-761`; `build-game-path-lineup-table.mjs:273-303`). The snap row carries no touch, passer or source pid (`simulate-game-path.mjs:378-395`), so no reader can tell who got the ball.
- **Uses of the tracked set beyond the tally.** These stay on the tracked set:
  - drive load;
  - lineup acceptance reweighting;
  - slot-fill candidates;
  - on-field draw;
  - review injury exits;
  - `data_snapshot.lineup`;
  - the coverage report's actual-player set.
- **Consumers of `player_measures`, confirmed by grep:**
  - the proposer (`derive-game-path-branch-rows.mjs:326-442`; `scripts/propose-game-path-branch-rows.mjs:76`);
  - coverage (`summarize-game-path-coverage.mjs:17-18,106-118`; `scripts/report-nfl-game-path-coverage.mjs:43-63,126-134`);
  - two specs.

  The replay gate, validate script, explorer, sync, review and checkpoint graph do not read it. The explorer does read snaps, and it labels a filled snap with the real play's carrier (`read-game-path-explorer.mjs:783-805`).
- **No schema version.** A run is "whole" on file presence alone (`game-path-run-store.mjs:46-75`). The proposer defaults missing fields to 0, so an old run would print zero curves instead of refusing.
- **Registry names.** Every player quantity is a `whole` curve in `text/nfl/nfl-betting/reference/projection-quantity-registry.md:164-195`:
  - `player_targets`, `player_receptions`, `player_receiving_yards`;
  - `player_rush_attempts` (counts kneels and scrambles), `player_rushing_yards`;
  - `player_pass_attempts`, `player_pass_completions`, `player_passing_yards`;
  - `player_pass_touchdowns`, `player_rushing_touchdowns`, `player_receiving_touchdowns`, `player_touchdowns`;
  - `player_interceptions_thrown`, `player_sacks_taken`, `player_rushing_receiving_yards`.

  `registry_curve_kinds` (`cli/nfl/belief_curve.py:151`) returns the curve kind of every registry quantity. A name outside it has no curve kind, so the plan asserts every emitted name is in it.

### What the run data shows (observed 2026-10-29)

- **Runs measured.** Week-8 kickoff forecasts for 24 offenses: CAR @ GB `e1bcd510` at 2,000 paths, plus 11 no-record runs at 500 paths that were not kept.
  - BAL @ BUF refused every path: BAL has an empty offense pool, the known regime limit.
  - LV @ NYJ refused before the run: LV has no defensive play caller on record.
  - Measured by joining each run's `snaps` to its `pool` on the outcome source play, per offense.
- **Changed-room exposure is common.** 11 of 24 offenses draw 63-97% of their snaps from pre-2026 games: KC, CIN, DEN, LA, GB, BUF, CHI, JAX, NE, DAL and IND.
  - For KC, GB, DEN, LA and CIN, 53-76% of touches belong to players outside the tracked set, 24-36 a path.
  - Offenses on a new caller (CAR, DET, TB, SEA, PIT) draw 0% pre-2026 and 2-18% untracked.
- **Slot fill pushes tracked totals to the team total.** In every high-exposure offense, tracked players' engine touch yards come within about 2% of the whole offense's capped yards, or above it:
  - KC 393.6 against 391.6;
  - GB 379.6 against 372.1;
  - CIN 371.6 against 348.8.

  In stable offenses, tracked players sit 5-25% below the team, which is the share the untracked backups hold.
- **Goal-line overcount is small but everywhere.** 3-17 yards a path per offense, median about 8. Slot fill is the large term: GB 214 yards a path, CAR 49.
- **Full-player tally reconciles exactly.** All-player own capped yards plus scrambles, minus sacks and other non-touch snaps, equal team capped yards on every path. The residual is 0 on CAR @ GB.
- **A current-room draw does not shrink every player.** GB's 2026 plays (7 games, 422 plays) give Watson 85.0 and Golden 78.7 yards a game, about where the engine has them now.
  - Jacobs would fall sharply: he appears in one 2026 game.
  - The gap to the outside sims (Stokastic Watson 61, Golden 51) is therefore partly real 2026 production, not only slot fill.
- **Settled-game coverage hides the issue.** Weeks 6-7, 24 games, 1,000 paths: `player_touch_yards` actuals fall 10.5% below p10 and 10.7% above p90. A settled game tracks the players who actually played, so its slot-fill exposure is small. Forward runs carry the defect, and forward player coverage is unmeasured.

### Rulings and guidelines that bind the room question

- **No ruling on slot fill or room admission.** Slot fill was built by an agent (league `2d9832aa2`). No operator ruling covers it, or whether a departed player's past touch may supply a current player's outcome.
- **The guideline's rule.** `guideline/nfl/betting/bet-the-game-read-against-the-market.md:56`: "A game under another caller, passer or room enters only for a mechanism that does not depend on what changed." Line 57: "History never fills a gap… the band widens, or the row refuses."
- **Precedent.** `guideline/nfl/betting/build-a-game-sample.md:81`: the caller's games "set the tendency, meaning pass rate, pace and dropback rate by score state. The current room's games under the interim caller set the shares."
- **Vacated shares.** `guideline/nfl/assess-player-change-impact.md:50` forbids projecting a vacated share by arithmetic. `price-a-player-line.md:47` says marginal volume carries the departing player's efficiency. Slot fill keeps the efficiency and reassigns the share.
- **Build-task ruling, 2026-10-27.** New regimes are "strict unless a profile files a named-mechanism extension covering call and outcome together". The call-and-outcome pairing at Level 1 (caller plus starting QB) is design text, not a dated ruling.

### Assumptions

- Building snap rows and deriving quantities adds no random draws, so points and team measures at a fixed seed stay byte-identical. Unchecked; the Baseline fingerprint on a final game is compared after the change.
- `passer_pid` is set on every pass record, and `ball_carrier_pid` on every run and scramble. Unchecked on all pools; checked on CAR @ GB, where the reconciliation residual is 0.
- Snap rows whose outcome came from `own_event_pool` (516 on CAR @ GB) carry no source play. They need the touch pid written by the simulator, not joined after the fact. Checked: those rows have a null `outcome_source_esbid`.
- Rule-drawn kneels carry no record and no pid, so the simulator must credit them to the starting quarterback to match `player_rush_attempts`. Checked: the registry row (`:168`) counts kneels, and the simulator receives no quarterback today (grep).
- No run outside `~/datasets` and the served roots needs the old `player_measures` shape. Runs are re-derivable by re-running the forecast, so none is unique data. Checked: only 85 local run dirs exist, and every one regenerates from its recorded inputs.
- The coverage report is read-only. Checked by reading it: selects, in-memory simulation and log output only.

## Design

### Scope

- **What this task does.** It builds the measurement foundation and changes nothing about what the engine draws. The foundation is:
  - an attributed play stream;
  - one quantity derivation over it;
  - an as-of forward backtest that judges any draw change.
- **What it leaves.** Slot fill keeps running until [[█████████████████████-███-████-█████-████-█████████-████████]], blocked by this task, replaces the draw with profiles and archetype roles. Every primitive here outlives that redesign, so nothing is transitional.
- **Operator rulings, 2026-10-29:**
  - one shared derivation for simulated paths and actuals, calibrated against gamelogs;
  - an as-of backtest as the gate;
  - the proposer limited to what filed rows price;
  - a player's yards from his own plays only (carried by the archetype task).

### Attributed play stream (the snap row)

- **One shape for both sides.** A snap row is the one event shape for simulated snaps and for real plays rebuilt from `nfl_plays` records.
- **Columns added to `snaps`,** named exactly as in `nfl_plays`:
  - `ball_carrier_pid`: the runner on a run, scramble or kneel, and the receiver on a completed pass;
  - `target_pid`: the targeted receiver on any pass attempt;
  - `passer_pid`: the passer on a dropback, sack and spike included;
  - `is_completion`, `is_sack`, `is_interception`, `is_qb_scramble`, `is_qb_kneel`, `is_qb_spike`, `is_fumble_lost`.
- **`box_score_yards`,** the one coined column, holds the yards the box score credits the play: the drawn record's credited yards (`rush_yards` on a run or scramble, `pass_yards` on a completion) plus the yardage kernel's delta (`simulate-game-path.mjs:1186`), capped at the goal line.
  - It is not the state's applied gain. That gain is `net_yards`, read off the next snap (`derive-game-path-plays.mjs:231-233`), which folds in penalty yardage: 95 passes and 103 runs in 2026 stood with a penalty and differed from the credited yards by 11.8 and 9.2 yards on average.
  - On an offensive touchdown it is the full gain to the line.
  - On a lost fumble, or a defensive touchdown after a catch or run, it keeps the credited yards, although the state applies none (`simulate-game-path.mjs:1144-1157`).
- **Who fills the pids.**
  - The simulator writes them for every snap, including slot-filled and `own_event_pool` snaps. A filled player is read off the filled record, which already carries him in `ball_carrier_pid` or `target_pid` (`simulate-game-path.mjs:758-760`).
  - A rule-drawn kneel credits the offense's starting quarterback. The simulator receives only its tables, and nothing in them names a quarterback (grep of `simulate-game-path.mjs` and `build-game-path-draw-tables.mjs`). So `build_game_path_context` puts each offense's `starting_quarterback_pid` into its tables.
  - Only `is_qb_scramble` and `is_qb_spike` need adding to the pool select. `is_qb_kneel`, `is_fumble_lost` and `is_interception` are already selected (`build-game-path-pool.mjs:78,82,83`). The record carries all of them.
- **Settled games** get the same rows from their real play records through one builder, `build-settled-game-path-snaps.mjs` beside `derive-game-path-plays.mjs`. Its `box_score_yards` is the credited `rush_yards` or `pass_yards`.

### Quantity derivation

- **One pure function,** `derive-game-path-quantities.mjs` in `libs-shared/nfl-game-path-simulation/`.
- **Input:** a stream of snap rows. **Output:** per-player registry quantities, minus the `player_` prefix:
  - `rush_attempts`, `rushing_yards`, `rushing_touchdowns`, `longest_rush`;
  - `targets`, `receptions`, `receiving_yards`, `receiving_touchdowns`, `longest_reception`;
  - `pass_attempts`, `pass_completions`, `passing_yards`, `pass_touchdowns`, `interceptions_thrown`, `sacks_taken`.
- **Counting rules match the gamelog columns the registry settles on:**
  - **Scrambles** key on `is_qb_scramble`, never on `call_class`. A scramble is `play_type` RUSH with `is_qb_dropback` set, so `classify_call` makes it a pass with no passer (`derive-game-path-plays.mjs:55`; 394 such rows in 2026). It is a rush attempt and rushing yards for the carrier, and not a pass attempt.
  - **Kneels** are rush attempts with their yards.
  - **Spikes** are pass attempts (39 rows in 2026).
  - **Sacks** are a sack taken for the passer. They are neither a pass attempt nor passing yards.
  - **Interceptions** are a pass attempt and an interception thrown.
  - **Two-point tries are excluded:** `TRY_TWO_POINT` snaps (`simulate-game-path.mjs:436`) and `is_two_point_conversion_attempt` plays.
  - **Touchdowns** come from the row's scoring flag. A defensive touchdown credits the offense's carrier or target with the box-score yards and no touchdown.
- **Callers.** Each is confirmed by a symbol grep in the plan:
  - the simulator at path end, into `path_summaries.player_measures`. Snap rows are built for every path whether or not `record_snaps` is set, kept in a per-path array (about 170 rows), derived and dropped. `from_game_state` runs and the coverage report store no snaps, so this is the only way they get player quantities.
  - the coverage report, for simulated and actual quantities alike;
  - the proposer, which reads summaries.
- **What it removes from the simulator.** The tally in `update_lineup` (`simulate-game-path.mjs:796-826`), and `touch_count`, `touch_yards` and `snap_count` in the measures. The drive load at `:811` keeps reading the uncapped `play.yards_gained`, so the long-touch threshold and the draw do not move. On-field snaps stay in lineup state.
- **No team-yards invariant.** Player and team yards would both come from `box_score_yards`, so the check could not fail. The calibration against gamelogs is the check.

### Calibration against gamelogs

- **What it checks.** A check runs the derivation over every settled 2026 regular-season game's real plays and compares each pid's quantities with:
  - `player_gamelogs`, filtered to `source in ('play-stats','gameday-roster')` and `is_active`, per [[███████████████████████████████-████████████-███████████]];
  - `player_passing_gamelogs.passing_sacks` for `sacks_taken`, the registry's source.
- **Measure.** The exact-match share per quantity, over pid-games where either side is nonzero. Counting all rows would let a wrong rule pass: crediting two-point plays still matched targets at 0.9819 and pass attempts at 0.9814 over all rows, but pass attempts fell to 0.8395 on the nonzero rows.
- **Threshold.** Every quantity at least 0.99. The correct rules matched at 0.991-1.000 over 2,097 pid-games in 107 games.
- **Failure mode it catches:** definition drift between the engine and the grader.
- **Where it lives:** `scripts/validate-nfl-game-path-simulation.mjs`, beside the replay gate.

### As-of forward backtest

- **Today a final game uses hindsight in four places.** Forward mode has to undo each:
  - availability switches to "this game" (`build-game-path-context.mjs:244-246`);
  - coach seats switch to "this game" (`build-game-path-pool.mjs:165-169`);
  - the starting quarterback becomes the game's own (`libs-server/game-sample/resolve-game-regime.mjs:158-163`);
  - wind comes from the game's own record (`build-game-path-context.mjs:84`).
- **The `as_of_kickoff` option.** It threads through those four sites:
  - availability from the last three admitted games;
  - seats and quarterback as projected before kickoff;
  - wind unknown, so kicks key on the roof alone, as the engine already does for unknown wind.

  The default keeps today's behaviour for every other caller of `resolve-game-regime.mjs`, a shared game-sample module.
- **Forward mode.** `scripts/report-nfl-game-path-coverage.mjs` gains `--mode forward|settled`, defaulting to `settled`. Forward mode builds each game with `as_of_kickoff`.
- **What it scores.** Both modes score the same families from the same derivation:
  - team points, total and margin;
  - player rushing yards, receiving yards, receptions, passing yards and touchdowns.

  Each family reports the PIT mean and the share outside p10-p90, overall and per offense.
- **The gate.** Forward coverage is what a draw change, starting with the archetype task, must improve.

### Proposer

- **Rows.** `derive-game-path-branch-rows.mjs` emits per-branch player rows for exactly the quantities the pricing kit's tail families settle on (`cli/nfl/pricing-kit.py:971-981`):
  - `player_rushing_yards`, `player_receiving_yards`, `player_rushing_receiving_yards`;
  - `player_receptions`, `player_touchdowns` (quarterbacks included);
  - `player_longest_reception`, `player_longest_rush`;
  - `player_passing_yards`, `player_pass_touchdowns`.
- **Floor.** A player qualifies for a quantity when his mean opportunities of that kind reach a floor: rush attempts, targets or pass attempts. This replaces the 2-touch rule.
- **Availability.** Rows are emitted only for pids available to the run. A touch slot fill could not move, because no tracked player of the group was available (`build-game-path-lineup-table.mjs:278-296`), stays with the unavailable player. The proposer prints the share of such touches rather than giving him a curve.
- **Refusal.** The script refuses a run whose summaries lack the derived quantities.
- **Not citable yet.** Player curves are not cited before this task lands (handoff from `███████████████████████████████████`).

### Explorer

- **Player table.** Each node, and the whole run, shows every player with touches on its paths: the median and p10-p90 of rushing yards, receiving yards, receptions, passing yards and touchdowns, from `player_measures`. Forecasts and `from_game_state` runs both store those.
- **Simulated receiver.** A forecast node's list of drawn real plays shows, for each, who received the touch in the simulation, from the snap rows' carrier and target, so slot fill shows where it happens.

### Old runs

- None are converted. Every run regenerates from its recorded inputs, so none is unique data.
- Readers that need the new columns refuse runs without them.
- The proposer is exercised on a week-9 forecast. CAR @ GB is final by implementation time, so a re-run of it would no longer be a kickoff forecast.

### Assumptions added by the design

- `player_gamelogs.passing_yards` is gross of sacks, and `rushing_yards` includes kneels and scrambles. Checked by the reviewer's calibration query: the correct rules match at 0.991-1.000.
- The registry's longest reception and longest rush read `nfl_plays` as the league settles them (registry tail-event section). Unchecked; the derivation spec compares `longest_reception` with the single-play maximum of credited `pass_yards` per target, and the first implementation step reads that section.
- `game-sample` and the engine are outside the NFL kit freeze (bulletin #1317 names kit files and three schemas). Unchecked for `resolve-game-regime.mjs`; the forward step re-reads the bulletin before editing.
- Forward mode over weeks 3-8 (about 80 games at 1,000 paths) runs in about 10 minutes against production. Estimated from 24 games in about 3 minutes; one run at a time.
- The new snap columns add roughly 15-25% to the snaps file. Unmeasured until the first run.

### File organization

- **`libs-shared/nfl-game-path-simulation/`:**
  - new: `derive-game-path-quantities.mjs`, `build-settled-game-path-snaps.mjs`;
  - changed: `simulate-game-path.mjs` (snap rows for every path, pids and flags, `box_score_yards`, kneel credit, derivation call, tally removed), `derive-game-path-plays.mjs` (scramble and spike flags), `build-game-path-lineup-table.mjs` (touch helpers reduced to the load's needs), `derive-game-path-branch-rows.mjs`, `summarize-game-path-coverage.mjs`.
- **`libs-server/nfl-game-path-simulation/`:** `build-game-path-pool.mjs` (select `is_qb_scramble` and `is_qb_spike`; `as_of_kickoff` seats), `build-game-path-context.mjs` (`starting_quarterback_pid` in tables; `as_of_kickoff` availability and wind), `read-game-path-explorer.mjs`.
- **`libs-server/game-sample/`:** `resolve-game-regime.mjs` (`as_of_kickoff` quarterback).
- **`scripts/`:** `report-nfl-game-path-coverage.mjs` (forward mode, shared derivation), `validate-nfl-game-path-simulation.mjs` (calibration check), `propose-game-path-branch-rows.mjs` (refusal, availability).
- **Specs:**
  - new: a derivation spec;
  - changed: simulate, lineup, branch rows, coverage, run store, the explorer API spec, and the game-sample regime spec if one exists.
- **Footprint:** no new dependency, configuration or database column. The naming ratchet scans only `db/schema.postgres.sql`.

### Naming check

The coined identifiers were checked against [[user:guideline/write/write-software.md]] and [[user:guideline/naming-conventions.md]]:

- **Snap columns copy `nfl_plays` exactly.** `ball_carrier_pid`, `target_pid`, `passer_pid`, `is_completion`, `is_sack`, `is_interception`, `is_qb_scramble`, `is_qb_kneel`, `is_qb_spike` and `is_fumble_lost` match columns there (checked: `information_schema.columns` for `nfl_plays`), so one grep finds both the real and the simulated field.
- **`box_score_yards` is the one coined column.** It is in full words, and it names the property that separates it from `yards_gained`, the drawn gain: what the box score credits.
- **Quantity keys are the registry quantity names minus `player_`** (`rush_attempts`, `passing_yards` and the rest), so a key and its registry row share a token.
- **File names stand alone.**
  - `derive-game-path-quantities.mjs` uses the registry's own word, "quantity".
  - `build-settled-game-path-snaps.mjs` names its output, a settled game's snap rows. A first draft, `build-game-path-play-events`, was rejected because it would blur with the existing `path_events` table.
- **Context fields.** `starting_quarterback_pid` reuses the seat name the regime resolver already prints. `as_of_kickoff` names what the option restricts the inputs to.
- **Closed sets name the distinguishing property.** The coverage report's `--mode` takes `forward` or `settled`, which is what the forecast knew. The proposer floor constant is `GAME_PATH_PLAYER_ROW_MINIMUM_OPPORTUNITIES`, replacing `GAME_PATH_PLAYER_ROW_MINIMUM_TOUCHES`.

### Knowledge Base Impact

- **`███████████████████████████████████`.** Update:
  - "Layers": the play stream and quantity derivation;
  - "Runs and schema": snap columns and player quantities;
  - "Entry points": proposer quantities and forward coverage mode;
  - "Gates": calibration and forward coverage;
  - "Known limits": slot fill remains until the archetype task.
- **Build task.** Supersede the three 2026-10-29 observations in place when this lands. The proposer item stays open for its week-9 verify.
- **Archetype task.** Carries the profile and room-composition framing (written 2026-10-29).
- **Nothing else.** No ABOUT.md, tag or guideline change. Scratch: none. Orchestrator: not warranted.

## Notes

- **The draw redesign lives on [[█████████████████████-███-████-█████-████-█████████-████████]].** The operator's 2026-10-29 direction is quoted there, along with the accepted profile and archetype framing. This task only builds what that redesign is judged by.
- **Current-season samples, measured on the week-8 forecasts.** These matter to the redesign, not to this task.
  - Every offense has 5-7 games and 302-497 run and pass plays in 2026.
  - Each has 2-7 players with 30 or more touch opportunities.
  - Today's pools reach 12,082 plays (KC).
- **Run cost.** A forecast takes about 20 s a game. Forward coverage over weeks 3-8 takes about 10 minutes. One engine job at a time against production.
- **League CI.** Master is red and standing (bulletin #1332). Check the touched specs in isolation before reading a CI failure as this change's.
- **Sibling sessions.** `███████████████████████████████████` handed the engine and proposer files to this plan with no open edits (2026-10-29).

## Plan

Worktree: /Users/trashman/worktrees/league/feat/correct-nfl-game-path-player-accounting (removed after merge)

Paths are relative to `repository/active/league` unless written in full. "Engine" means `libs-shared/nfl-game-path-simulation/`. Run production scripts with `TZ=America/New_York NODE_ENV=production`, and at most one engine job against production at a time.

### Baseline

- [x] **Capture the before-state the gates compare against**
  - Purpose: prove the draw is unchanged. The fingerprint uses a FINAL game, whose inputs no longer move, so a later mismatch can only come from code.
  - Changes: record each output as one observation on this task, with its command:
    - `node scripts/validate-nfl-game-path-simulation.mjs --seed 7`, then `--seed 11`: the replay points gap and signed gap;
    - a seed-7, 2,000-path, no-record forecast of TB @ CAR, week 7, FINAL: `NFL_GAME_PATH_SIMULATION_ROOT=/tmp/gp-baseline node scripts/run-nfl-game-path-simulation.mjs --esbid 2026102503 --seed 7 --no-record`;
    - its fingerprint: `duckdb -c "select md5(string_agg(path_number || ':' || home_points || ':' || away_points || ':' || team_measures::varchar, '|' order by path_number)) from '/tmp/gp-baseline/2026102503/*/path_summaries.parquet'"`.
  - Verify:
    - the observation holds both commands and the fingerprint;
    - running the forecast and fingerprint a second time, before any code change, gives the same value. This is the control that the fingerprint is deterministic.

### Attributed play stream

- [x] **Write the attribution on every simulated snap**
  - Files: `libs-server/nfl-game-path-simulation/build-game-path-pool.mjs`, `libs-server/nfl-game-path-simulation/build-game-path-context.mjs`, engine `derive-game-path-plays.mjs`, engine `simulate-game-path.mjs`
  - Purpose: one event shape that records who got the ball, after any slot fill, with the yards the box score credits.
  - Changes:
    - select `is_qb_scramble` and `is_qb_spike` in the pool, and carry them onto the record beside the existing kneel, fumble and interception flags;
    - put each offense's `starting_quarterback_pid` into its tables in `build_game_path_context`;
    - build a snap row for every snap of every path, whether or not `record_snaps` is set. It carries the `nfl_plays`-named pid columns and flags, plus `box_score_yards` as the Design defines it: credited yards plus the kernel delta, capped, with the full gain on an offensive touchdown and the credited yards kept on a giveaway;
    - read a filled player off the filled record;
    - credit a rule-drawn kneel to the offense's `starting_quarterback_pid`;
    - keep rows in the snaps file only when `record_snaps` is set.
  - As built:
    - Snap rows also carry `is_touchdown` and `touchdown_nfl_team` (the `nfl_plays` names), the scoring flag the derivation reads.
    - The attribution and the box-score rule are pure exports of `build-settled-game-path-snaps.mjs` (`game_path_snap_attribution_of`, `game_path_box_score_yards_of`), shared by the simulator and the settled builder.
    - The per-case rules are asserted in the quantities spec. The simulate spec asserts every run and pass snap credits a player, `box_score_yards` never passes the goal line, and each path's `player_measures` equals the derivation over its own snaps.
    - TB @ CAR seed-7 fingerprint after the change: 6f2de1c9eb01b59ecfb178cb44e41f36, equal to the Baseline. 0 unattributed run or pass snaps.
  - Verify:
    - a new case in `test/libs-shared.nfl-game-path-simulate.spec.mjs` covers a slot-filled snap, a kneel, a scramble, a spike, a gain capped into a touchdown, a lost fumble and a run that stood with a penalty, where `box_score_yards` equals the credited yards and not `net_yards`. It passes under `yarn test:isolated`;
    - `test/libs-server.nfl-game-path-run-store.spec.mjs` with the new columns in its fixture reads them back unchanged;
    - on a no-record forecast, the count of run and pass snaps (spikes, kneels and two-point tries excluded) with a null `ball_carrier_pid` and a null `passer_pid` is 0;
    - the Baseline fingerprint is unchanged.

- [x] **Build snap rows for a settled game from its real plays**
  - File: engine `build-settled-game-path-snaps.mjs` (new)
  - Purpose: actuals pass through the same event shape as paths.
  - Changes: map each derived play record to a snap row with the same columns. `box_score_yards` is the credited `rush_yards` or `pass_yards`.
  - Verify: `test/libs-shared.nfl-game-path-quantities.spec.mjs` (new) builds rows from a fixture game holding a scramble, a kneel, a spike, a sack, an interception, a lost fumble, a defensive touchdown after a catch and a two-point try, and asserts every column.

### Quantity derivation

- [x] **Derive every registry quantity from snap rows**
  - File: engine `derive-game-path-quantities.mjs` (new)
  - Purpose: one definition of every player quantity, for paths and actuals alike.
  - Changes: export `derive_game_path_quantities`, returning per pid `nfl_team` and the Design's quantities under the Design's counting rules.
  - Verify: the derivation spec asserts each counting rule on the fixture game, including the two-point exclusion and scramble keyed on `is_qb_scramble`. `longest_reception` and `longest_rush` are checked against the single-play maximum of credited yards per player, read against the registry's tail-event section before writing the case.

- [x] **Replace the simulator's tally with the derivation**
  - Files: engine `simulate-game-path.mjs`, `build-game-path-lineup-table.mjs`
  - Purpose: remove the parallel definition.
  - Changes:
    - drop the touch tally from `update_lineup` (`:796-826`). The load update at `:811` keeps reading the uncapped `play.yards_gained`;
    - build `player_measures` at path end (`:1369`) from `derive_game_path_quantities` over the path's snap rows, then drop the rows;
    - remove `touch_count`, `touch_yards` and `snap_count` from the measures;
    - cut the lineup table's touch helpers to what the load needs.
  - Verify:
    - the Baseline fingerprint is unchanged;
    - `git grep -nE "touch_yards|touch_count"` returns no hit outside the lineup load;
    - the lineup spec passes;
    - a `from_game_state` run's summaries carry the derived quantities.

### Calibration against gamelogs

- [x] **Check the derivation against the grader's columns**
  - File: `scripts/validate-nfl-game-path-simulation.mjs`
  - Purpose: catch drift between how the engine counts and how the grader settles.
  - Changes:
    - for every settled 2026 regular-season game, build snap rows from its plays and derive quantities;
    - compare each pid's values with `player_gamelogs`, filtered to `source in ('play-stats','gameday-roster')` and `is_active`. The columns are `rushing_attempts`, `rushing_yards`, `targets`, `receptions`, `receiving_yards`, `passing_attempts`, `passing_completions`, `passing_yards`, the three touchdown columns and `passing_interceptions`. Compare `sacks_taken` with `player_passing_gamelogs.passing_sacks`;
    - print the exact-match share per quantity over pid-games where either side is nonzero, list the mismatching pid-games, and exit 1 when any share is under 0.99.
  - Verify:
    - the run prints a share for every quantity, over more than 100 games;
    - a control that counts two-point plays as pass attempts drops `pass_attempts` under 0.99 and exits 1;
    - a control that counts sacks as attempts does the same.
  - As built (`libs-server/nfl-game-path-simulation/calibrate-game-path-quantities.mjs`, `--calibration_season`, default the current season):
    - 2026, 107 settled games: PASS. Every quantity matches at 0.9946 or better; 9 of 13 match exactly.
    - Two definition fixes it forced:
      - The settled builder no longer drops plays whose down did not advance; the gamelogs count them. Rush and pass attempts went from 0.953 and 0.963 to 1.000.
      - A completion lateraled after the catch credits the receiver his `receiving_yards` and the taker the rest and the touchdown, through new `lateral_pid` and `lateral_yards` snap columns. Receiving yards went from 0.988 to 1.000.
    - Controls: counting two-point tries gave pass attempts 0.9218 and FAIL; counting sacks as attempts gave 0.2377 and FAIL.

### Forward and settled coverage

- [x] **Score coverage on the derived quantities**
  - Files: engine `summarize-game-path-coverage.mjs`, `scripts/report-nfl-game-path-coverage.mjs`
  - Purpose: compare simulated and actual quantities through one derivation, for every player.
  - Changes:
    - the player families become `rushing_yards`, `receiving_yards`, `receptions`, `passing_yards` and `touchdowns`, read over every pid;
    - actuals come from `build-settled-game-path-snaps` plus the derivation, replacing `actual_player_measures` (`:42-63`).
  - Verify:
    - the coverage spec passes with the new families;
    - `report-nfl-game-path-coverage.mjs --week 6 7` prints every family, with at least one player contributing from every offense with an admitted regime.
  - As built: each family also reports a mean PIT (ties split). A player's family is scored only where he had a part, actual or simulated.
  - Settled weeks 6-7, 24 games, 1,000 paths, seed 7: player rushing yards PIT 0.577 (above p90 0.239), receiving yards 0.568 (above 0.240), receptions 0.567 (above 0.227), passing yards 0.503, touchdowns 0.506. Actuals above p90 come mostly from players the engine does not track, whose touches slot fill moves to the tracked eight: the bias the archetype task removes. LV games refused (no defensive play caller on record).

- [x] **Add the as-of-kickoff inputs and the forward mode**
  - Files: `libs-server/nfl-game-path-simulation/build-game-path-context.mjs`, `libs-server/nfl-game-path-simulation/build-game-path-pool.mjs`, `libs-server/game-sample/resolve-game-regime.mjs`, `scripts/report-nfl-game-path-coverage.mjs`
  - Purpose: measure what a kickoff forecast would have said, the gate for any draw change.
  - Changes:
    - first, re-read bulletin #1317's scope and confirm `game-sample` is outside it; if it is inside, hold this step and say so;
    - add `as_of_kickoff` through:
      - availability (`build-game-path-context.mjs:244-246`): last three admitted games;
      - seats (`build-game-path-pool.mjs:165-169`): projected, not this game's;
      - starting quarterback (`resolve-game-regime.mjs:158-163`): projected;
      - wind (`build-game-path-context.mjs:84`): unknown, roof only.

      The default leaves every existing caller unchanged;
    - `--mode forward|settled` on the coverage report, defaulting to `settled`.
  - Verify:
    - in forward mode over weeks 3-8, every game prints availability source "last three admitted games", and no seat or quarterback source reads "this game";
    - `git grep -n resolve_game_regime` lists every caller, and each still passes its existing spec;
    - the run completes and prints each family's PIT mean and share outside p10-p90, overall and per offense.
  - As built: bulletin #1317 names kit files and three schemas only, so `game-sample` was edited. The forward head coach comes from the team's latest game before kickoff, because the settled game's own coaches row (labelled "projected for this game") is filled after it was played. Each game prints its input sources. Over weeks 3-8 forward, all 70 games read "last three admitted games" and prior-week seats, and no source reads "this game".

- [x] **Record the forward baseline of the slot-fill draw**
  - Purpose: the number the archetype task must beat.
  - Changes: add one observation on `████████████████████████████████████████████████████████████` with the forward-mode command and its per-family results.
  - Verify: the observation is committed (`git log -1 -- ███████████████████████████████████████████████████████`).

### Proposer

- [x] **Emit the priced player quantities for available players, and refuse old runs**
  - Files: engine `derive-game-path-branch-rows.mjs`, `scripts/propose-game-path-branch-rows.mjs`
  - Purpose: give profilers the curves filed rows price, under registry names, only for players who can play.
  - Changes:
    - per-branch rows for the nine quantities the pricing kit's tail families settle on (Design § Proposer), quarterbacks included in `player_touchdowns`;
    - qualification by mean opportunities (`GAME_PATH_PLAYER_ROW_MINIMUM_OPPORTUNITIES`);
    - rows only for pids available to the run, plus a printed share of touches credited to unavailable players;
    - refusal of a run whose summaries lack the derived quantities.
  - Verify:
    - the branch-rows spec covers the refusal on a fixture with old measures, and an unavailable pid that gets no row;
    - every emitted quantity key is in `registry_curve_kinds` (`/Users/trashman/user-base/cli/nfl/belief_curve.py:151`) and equals the set of realized quantities in `TAIL_FAMILIES` (`/Users/trashman/user-base/cli/nfl/pricing-kit.py:971-981`).
  - As built:
    - Each offense's `available_pids` (the available backs, receivers and tight ends, plus the starting quarterback) is recorded in the run's `data_snapshot.lineup[team].availability`. The proposer refuses a run without it, or without derived quantities.
    - Verified: the emitted set equals the tail families' nine quantities, and none is missing from the registry's curve kinds. The branch-rows spec passes, covering the availability filter, the unavailable-touch share and the refusal.

### Explorer

- [x] **Show player production in the explorer**
  - Files: `libs-server/nfl-game-path-simulation/read-game-path-explorer.mjs`, `api/routes/game-paths.mjs`, the forecast and review pages under `app/views/pages/game-path-tree/`, `test/api.game-paths.spec.mjs`
  - Purpose: let a profiler read what each player does in a branch, and see slot fill where it happens. Operator, 2026-10-29: "we should continue to develop and improve the explorer".
  - Changes:
    - a player table on each node and on the whole run: every player with touches on the member paths, with the median and p10-p90 of rushing yards, receiving yards, receptions, passing yards and touchdowns, read from `player_measures`;
    - on a forecast node's source-play list, who received each drawn real play's touch in the simulation, read from the snap rows' carrier and target.
  - Note: the step this replaces assumed the explorer labels a snap with its source play's carrier. It does not: it shows no per-snap carrier, and its source-play list shows real plays as they happened, which is correct.
  - Verify:
    - the explorer API spec asserts a node's player table and a slot-filled play's simulated receiver;
    - the deployed page shows both on the week-9 forecast.
  - As built: league `35760da12`. On production after the deploy, DEN @ CAR `b48455d1` shows a 22-player run table (Young 299 passing yards, 214-390). One node's drawn real play shows its touch spread over McMillan, Coker, Metchie and Tremayne, which is slot fill in view.

### Gates and forecast

- [x] **Hold the engine gates**
  - Verify:
    - `node scripts/validate-nfl-game-path-simulation.mjs --seed 7` and `--seed 11` exit 0, with replay points gaps equal to the Baseline and the calibration check passing;
    - every touched or new spec passes under `yarn test:isolated`. The known single-spec `afterAll` hold-out failure is ignored per `docs/guides/test.md:93`.
  - As built: league `11da7dd3c`. Seeds 7 and 11 exit 0 with points gaps 1.40 (signed -0.70) and 1.38 (signed -0.25), equal to the Baseline; admission audit 36 admissions, 0 violations; calibration PASS. 421 specs across the 39 game-path and game-sample spec files pass in isolation.

- [x] **Exercise the proposer on a week-9 kickoff forecast**
  - Purpose: produce a recorded forecast the proposer reads, while the game is still unplayed.
  - Changes:
    - run `node scripts/run-nfl-game-path-simulation.mjs --esbid <a week-9 esbid>` (recorded), then `node scripts/sync-nfl-game-path-runs.mjs --source ~/datasets/nfl-game-path-simulation`;
    - run the proposer on the new run id.
  - Verify:
    - sync exits 0 with no backlog;
    - the proposer prints the nine player quantities per branch, and the unavailable-touch share.
  - As built: DEN @ CAR (2026110801), run `b48455d1-f07e-467b-aacb-933d3abefb19` from league `11da7dd3c`, 2,000 paths, no refusal. Sync backlog 0 of 87 on both targets. The proposer emits all nine quantities; touches credited to unavailable players 0.002.

- [x] **Ship the league change**
  - Purpose: the explorer API change reaches production. The scripts run locally.
  - Changes: commit in coherent units per `docs/guides/ship.md`, and let ████████ push. League master CI is red and standing (bulletin #1332), so confirm the touched specs pass in isolation before reading a CI failure as this change's.
  - Verify: `git log` shows the commits, and the deployed explorer shows a filled snap's player on the week-9 run.
  - As built: merged as league `11da7dd3c` and pushed by ████████. Deployed by `DEPLOY_ACK_RANGE=15 yarn deploy:all` from a clean worktree at `efbbfccd9`, with the operator approving the whole 15-commit range, including 9 commits from other sessions. No auction was live. Both hosts read `efbbfccd9`, the sha the bundle was built from.

### Knowledge Base Updates

- [x] **Update the engine doc**
  - Entity: `████████████████████████████████████████`
  - Changes:
    - "Layers": the snap stream and quantity derivation;
    - "Runs and schema": snap columns, `box_score_yards` and player quantities;
    - "Entry points": proposer quantities and `--mode forward`;
    - "Gates": calibration and forward coverage;
    - "Known limits": slot fill remains until the archetype task.
  - Verify: `grep -nE "touch_yards|touch_count" ███████████████████████████████████` returns nothing.
  - Done in user-base `24bab78eb3`.

- [x] **Supersede the build task's 2026-10-29 findings**
  - Entity: `██████████████████████████████████████████████`
  - Changes: replace the goal-line, slot-fill and measures observations in place (`base entity observe-remove --exact ... --replace ...`) with one line each, saying what landed and where.
  - Verify: `base entity get ██████████████████████████████████████████████` shows the replaced lines and no duplicate.
  - Done by a sibling session before this step ran: user-base `b008f26b72` removed the three observations and related the build task to this task and the archetype task.
