Live Draft Tool — runbook
LIVE DRAFT — operator runbook (NSBA4)
Two tools: an ingestion pipeline (scripts/refresh.py) to keep the board
current, and the live draft engine (scripts/draft_live.py) you drive on
draft day. Everything uses the project venv: .venv/bin/python.
Strategy encoded: docs/findings/40_draft_strategy_v3.md on top of the joint
model in docs/findings/43_joint_hierarchical_model.md.
A. Where to drop new data
| What | Drop it here | Notes |
|---|---|---|
| New / updated combine results | data/raw/combine/nsba4_2026_combine_INPROGRESS.xlsx |
Overwrite this exact file with the latest export. parse_combine.py reads this filename for the nsba4 season. |
| New intro pastes | intros*.txt at repo root, or data/raw/discord/intros*.txt |
refresh.py re-runs A39, which auto-flags any handle it hasn't interpreted yet. 2-step flow below. |
Ingesting new intros (2 steps). Turning free-form Discord prose into the
structured fields (normalized grade, category tags, sandbag/fade flags + the
exact quote) needs judgment — it is not regex-able — so A39 does NOT blindly
auto-parse the .txt. Instead:
1. Drop the paste as intros*.txt (repo root) or data/raw/discord/intros*.txt,
then run refresh.py. A39 scans the raw text and prints
[A39] NEEDS INTERPRETATION: <handle> for every post it hasn't structured
yet (staff posters a.new.rag / auride0 are ignored).
2. Interpret the flagged posts (Claude does this from the raw text) into one
JSON object per line — same keys as A39's intros rows
(handle, name, grade, cats, sandbag, sandbag_q, fade, fade_q, notes) —
appended to data/raw/discord/intros_extra.jsonl. Re-run refresh.py;
A39 merges those rows (deduped by handle) and the warning clears.
The original ~45 hand-curated rows stay inline in A39 untouched; intros_extra.jsonl
is the clean append path for everything new. (For combine-driven board updates this
whole step is optional — the value board is driven by combine + game tape, not the
intro self-reports; intros feed eligibility, buy-low context, and the no-combine
watchlist.)
New combine-only players caveat. reconcile_identities_v2.py merges
identities across existing sources but does not mint brand-new
canonical_ids for players who appear only in a new combine drop and are not
already in canonical_players.csv. If a never-before-seen player takes the
nsba4 combine, they may need a manual canonical row before they appear on the
board. Watch the "unmatched" lines in the refresh summary.
B. Refresh the board
Run the whole chain (parse combine → GRM ability → master → intros → reconcile → worklist → re-fit joint model):
.venv/bin/python scripts/refresh.py
Full run is ~1 minute and idempotent (same inputs → identical board). It
backs up the old board to data/processed/joint_value_board.PREV.csv and prints
the top movers vs that backup. It never touches data/draft_state.json.
Flags:
- --dry-run — print the chain + current summary without executing anything.
- --combine-only — skip the intros parse (still reconciles + re-fits).
- --intros-only — skip the combine parse (still reconciles + re-fits).
- --no-backup — don't snapshot the old board (then no movers diff).
What to check after a refresh:
1. The *** N identity matches need David's confirmation *** block — these come
from data/processed/reconcile_worklist.csv. Confirm/resolve the high-signal
rows (sorted by importance) so the right game tape attaches to the right
player.
2. unmatched handles / unmatched to canonical_id — intro posters or combine
takers we couldn't tie to a canonical player.
3. The top movers — sanity-check that new combine/tape moved the board the
way you expect.
Outputs that feed the live tool:
data/processed/joint_value_board.csv (score/floor/ceiling) and
data/processed/joint_ability_posterior.csv (per player×subject theta + 80% CI).
C. Draft day — the live tool
The slot is unknown until ~06-06, so set it at init. Defaults are 14 teams, 6 rounds, snake.
CLI flow
.venv/bin/python scripts/draft_live.py init --slot 9 # [--teams 14 --rounds 6]
.venv/bin/python scripts/draft_live.py recommend --n 5 # who to take at your next pick
.venv/bin/python scripts/draft_live.py me "Rohan Garg" # record YOUR pick
.venv/bin/python scripts/draft_live.py pick "Kian" # record an opponent's pick (auto-assigns to team on the clock)
.venv/bin/python scripts/draft_live.py pick "Vishnu" --team 5 # force a team slot if needed
.venv/bin/python scripts/draft_live.py state # roster, coverage, whose turn, your next pick #
.venv/bin/python scripts/draft_live.py board --subject ess # remaining pool by theta in a subject
.venv/bin/python scripts/draft_live.py board # remaining pool by YOUR marginal value
.venv/bin/python scripts/draft_live.py undo # undo the last pick
.venv/bin/python scripts/draft_live.py ineligible "Some Name" # remove from draftable pool
.venv/bin/python scripts/draft_live.py captain "Some Name" # remove (pre-assigned captain)
State persists to data/draft_state.json; the tool is stateful across commands.
"Just tell Claude" flow
The engine functions all return structured dicts, so you can drive it
conversationally — the orchestrator translates and calls:
- "I'm slot 9" → init_draft(slot=9)
- "Team 3 took Kian" → record_pick("Kian") (auto-assigns to the team on the clock)
- "I'll take Rohan" → record_my_pick("Rohan")
- "Who should I take?" → recommend(n=5)
- "Where do we stand?" → state_summary()
- "Show me the best ESS guys left" → board(subject="ess")
- "Undo that" → undo_last()
Name resolution is fuzzy across canonical names, intro names, and Discord
handles. If a name is ambiguous, the tool returns the candidate list instead of
guessing — pick the canonical_id (e.g. P0346) or a more specific name.
D. What the recommendations mean
Each rec shows: name · fills · mv · proj [floor–ceiling] · slide.
- fills — what the player adds to YOUR roster: an open/thin subject
(e.g.
ess),BPA(best available, rounds 1–2), ordepth:bio(a 2nd strong body in your best subject — only once every subject is covered). - mv (marginal value) — the ranking driver. Rounds 1–2 it's ~85% raw projected value (BPA — the bias doesn't bite early, v3 §6.1). Rounds 3+ it shifts toward the player's theta in YOUR open/thin subjects and down-weights subjects you've already covered (coverage-marginal, v3 §2). A small depth bonus is added for your best subject once coverage is cleared (D14 playoff insurance). No CS reach bonus is ever added (v3 §1 / D8 refuted).
- proj [floor–ceiling] — the joint model's projected value with its honest 80% interval. Combine-only players have ~1.7× wider bands — respect the uncertainty (finding 43).
- slide / VONA — opponent model: the field drafts in raw-combine order
(F8, ρ≈0.7).
won't last= within your picks-until-next window by combine standing, so take them now or lose them.safe= will likely still be there next time. Players with no combine row are invisible to the field and slide — the combine-absence steal (v3 §5).
E. Known caveats (verify before relying)
- Slot, eligibility, captains unconfirmed until ~06-06 (D-level open
questions). Set
--slotat init; mark known captains / ineligibles withcaptain/ineligibleso they leave the draftable pool. Until confirmed, the default pool = nsba4 combine-takers ∪ intro posters minus known staff (Anurag/a.new.rag, Mingle/auride0). - The model is combine-grade out-of-sample. Per finding 43's LOSO test, the joint score is not a proven predictive upgrade over raw combine once you remove a season's own games. Its real value is the per-subject vectors and the uncertainty bands — use them for mid-board re-ordering and specialist identification, not as a round-1 oracle. Lean on per-subject fit + floor/ ceiling, not the point score.
- Name ambiguity is real (e.g. two distinct "Aryan" canonical rows
P0113/P0114 from imperfect reconciliation). The tool surfaces the candidates
rather than guessing — confirm the
canonical_idwhen prompted. - Theta coverage thresholds (
THETA_COVERED=0.6,THETA_THIN=0.0) are sensible defaults from the pool distribution, not calibrated truth; adjust indraft_live.pyif coverage calls look off on draft day.