---
title: Build the agent hit-rate query CLI
type: task
status: completed
description: >-
  A read-only script that answers quick player split questions (k of N, Wilson interval, per-season
  split, game list) over the hit-rate sample registry and evaluator, adding raw weather and opponent
  man-coverage fields and extracting the route's query sequence into a shared function.
base_uri: user:task/league/build-agent-hit-rate-query-cli.md
created_at: '2026-09-27T06:20:18.215Z'
entity_id: b802ae8a-e3f9-4a14-853f-ccfdefbd03b9
finished_at: '2026-09-27T06:56:11.283Z'
observations:
  - ████████████████████████████████████████████████
owner_identity_uri: █████████████████████████
priority: Medium
public_read: true
relations:
  - relates [[██████████████████████-███-████-██████-██████-████████████]]
  - relates [[█████████████████████-███-████-████-███████-██████-█████████]]
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-09-27T06:56:12.271Z'
visibility_analyzed_at: '2026-11-05T23:48:26.169Z'
---

# Implementation Plan: Build the Agent Hit-Rate Query CLI

## Overview

- Give agent sessions a read-only command that answers a split question such as "█████, 2023 onward, Burrow primary passer, route share 80 or more, wind under 15: 2+ TDs in k of N" in one call. It prints k/N, a Wilson interval, a per-season split with a concentration flag, the unknown count per filter, and the game list.
- It reuses the hit-rate sample registry and evaluator behind the Betting Markets panel, so the two answer the same question the same way. There is no parallel filter engine.
- It encodes the traps one week-3 session hit by hand. Weather thresholds are explicit and echoed. A bare percentage is never printed. Single-season concentration is flagged. Multi-TD outcomes carry the league P(2+) against P(1+) calibration.

## Background

- The registry is `libs-shared/prop-hit-rate-sample-fields.mjs` and the evaluator is `libs-server/prop-hit-rate-sample-filter.mjs`. Both are pure. The loaders and grader in `libs-server/prop-hit-rate-sample.mjs` only read.
- The resolve, load, evaluate and grade sequence lives only inline in `POST /players/:pid/hit-rate` (`api/routes/players.mjs`), so a second caller would have to copy it.
- Already fields: season scope, primary passer, starting QB, route share, snap share, usage counts, home, indoor, grass, rest, primetime, division, game script, lines and results. Missing: raw wind, temperature and roof (only the fixed 40F / 15 mph `is_clear_weather` exists), and opponent coverage man share.
- Teammate active/inactive is owned by the live sibling lane ████████████████████████████████████████████ and is consumed here, not built.
- `nfl_plays.coverage_class` is populated from 2018, on about 87 percent of REG dropbacks (`is_qb_dropback`); man share runs from 0.35 in 2018 to 0.27 in 2025.
- **The handed-off route-share trap does not apply to this tool.** For WRs on 70 percent or more of snaps, `player_receiving_gamelogs.route_share` over snap share is 0.99 to 1.01 in every season 2021-2026. The roughly one-fifth overstatement is a property of `view_nfl_player_route_participation`, which the registry does not read.
- League multi-TD calibration (2019-2025 REG, 20+ snaps): a WR's own 2+/1+ ratio barely persists. Among WRs scoring in 40 percent or more of games, a prior-three-season ratio of 0.15 was followed by 0.232 and a ratio of 0.30 by 0.239. Poisson from P(1+) overstates P(2+).
- No Wilson interval helper exists in the repo.

## Design

- **Extract `query_player_hit_rate`** into `libs-server/player-hit-rate-query.mjs`. It returns the route's current response body, or `{ refusal }` for a predicate refusal. The route keeps the fastest-validator gate and the 400 mapping. It is a pure move, and the existing route spec passing unchanged is the proof.
- **Registry fields:**
  - `wind_speed_mph` and `temperature_fahrenheit` are NUMBER with `named_category` NULL ("Indoors") for dome or closed roofs. An outdoor or open-roof game with no reading is `SAMPLE_FIELD_UNKNOWN`.
  - `roof` is SELECT over the four enum values.
  - `opponent_man_coverage_share` is a fraction: the opponent defense's MAN plays over classified dropbacks in that game. It comes from a new `nfl_plays_coverage` source, and a game with no classified dropback is unknown.
- **`libs-shared/wilson-interval.mjs`:** `wilson_interval({ hits, total, z = 1.96 })`.
- **`libs-server/multi-touchdown-calibration.mjs`:** one aggregate query giving per-position P(1+) buckets and their P(2+). The CLI attaches it to any two-plus or three-plus touchdown outcome.
- **`scripts/query-player-hit-rate.mjs`:**
  - Arguments: yargs with `.strict()`. Resolve the player by name or pid and refuse an ambiguous name. Take the outcome as `--market_type`, `--line` and `--side`, or the `--stat` / `--at_least` shorthand. Named filters map to predicates, plus a generic `--where field:op:value` and `--list_fields`.
  - Games the player sat out are excluded, the same as the stored rates since league 1e2250ad2 (2026-09-28); there is no flag to include them.
  - Output: the echoed document, k/N, the rate, the interval, the per-season split with the rate excluding the top season when one season holds half or more of the hits, filter coverage and the games. `--json` is available.
  - The argument-to-document builder is a pure exported function.
  - Read-only by construction. A spec asserts the script's import graph never reaches `prop-market-hit-rates.mjs`.
- **Out of scope:** play-grain splits (targets per route against man coverage), where the "current QB's starts only" trap bites. That is a follow-up task.
- **Knowledge Base Impact:**
  - Record the route-share correction on the receiving-gamelogs query-trap guideline.
  - Add a "quick player split" route to the league database routing guideline and to `docs/guides/scripts.md`.
  - Document the new fields in the registry header.
  - No tag, no ABOUT.md change, no scratch slug, no orchestrator.
- Work runs in a league worktree, because a sibling lane edits the registry in the shared tree, and merges locally per the merge-worktree workflow.

## Plan

All groups are merged to league master at `d2fb083fa`. The branch and its worktrees are retired.

### Shared Query Function

- [x] **Extract `query_player_hit_rate` from the route**
  - Files: `libs-server/player-hit-rate-query.mjs` (new), `api/routes/players.mjs`
  - Verify: the hit-rate route spec passes unchanged. Negative control: break the season-path branch in the new module and confirm route cases go red.
  - Landed: 73 route cases passing; the control failed 15. The module also owns the fastest-validator schema.

### Registry Fields

- [x] **Add raw weather fields: `wind_speed_mph`, `temperature_fahrenheit`**
  - File: `libs-shared/prop-hit-rate-sample-fields.mjs`
  - Verify: registry contract spec passes. New filter-spec cases cover indoor (named NULL), outdoor with a value, and open roof with no reading (unknown).
  - Landed: `roof` was dropped because `is_indoor` covers it and the builder has no picker for an unsourced select. A comparison against the named "Indoors" NULL is always false, so a predicate gained `include_null: true` ("this comparison, or the named state"), refused on non-named fields and beside IS NULL. Controlled: ignoring it turned exactly the two new cases red.
- [x] **Add `opponent_man_coverage_share` and its `nfl_plays_coverage` source**
  - Files: `libs-shared/prop-hit-rate-sample-fields.mjs`, `libs-server/prop-hit-rate-sample.mjs`
  - Verify: a DB-backed sources spec seeds plays with known MAN/ZONE counts and asserts the share. A game with no classified dropback reads unknown.
  - Landed: MAN over classified coverage on live dropbacks, keyed on the opponent as `defense_nfl_team`. Controlled by dropping the live-play filter. Marked `is_in_game`.

### Statistics Helpers

- [x] **Add `wilson_interval`**
  - File: `libs-shared/wilson-interval.mjs`, spec `test/libs-shared.wilson-interval.spec.mjs`
  - Verify: 8 of 35 gives about 0.12 to 0.39. The 0 and N edges stay inside [0, 1].
- [x] **Add the multi-touchdown calibration**
  - File: `libs-server/multi-touchdown-calibration.mjs`
  - Verify: the pure bucketing function is spec-tested on a fixture. The production readout reproduces WR P(2+) of about 0.075 at P(1+) of about 0.45.
  - Landed: the fixture caught a floating-point edge (3/10 divided by 0.1 floors to 2). The player's P(1+) is read over his REG games with 20+ snaps, the calibration's own population.

### CLI

- [x] **Write `scripts/query-player-hit-rate.mjs` with an exported document builder**
  - Spec: `test/scripts.query-player-hit-rate.spec.mjs` covers the builder (each named flag maps to its predicate, weather flags echo thresholds, and inactive games are excluded by default) and the read-only import guard.
  - Verify against production: █████, 2023-2026 REG, primary passer Burrow, route share 80 or more, `GAME_PLAYER_TWO_PLUS_TOUCHDOWNS` reproduces the hand query's 8 of 35. The wind and temperature variants move it as the hand queries did.
  - Landed: 8 of 36, 8 of 33 with wind under 15, and 7 of 22 with wind under 10 and above 40F. The hand queries gave 8 of 35, 8 of 34 and 7 of 24. The differences are outdoor games with no weather reading, which the report names. Read-only guard controlled by the precompute reaching the writer. A pre-merge review found two bugs, now fixed: the concentration flag fired on any two-season sample, and `--touchdowns` ignored `--side`.
- [x] **Wire the teammate flags to the sibling lane's field once it lands**
  - Landed: `--with` maps to `active_teammate_pids IN` (every listed pid active), and `--without` to NOT IN (none active).

### Documentation

- [x] **Update `docs/guides/scripts.md` and the registry header**
- [x] **Update the league routing and receiving-gamelogs query-trap guidelines**
