Resumable state for the multi-session build described in _PLAN. First file created, last file touched. Read this before writing anything in this folder. July material below is preserved as labelled history; the current authority is the Aug. 2 10-team refresh.
_Progress — 2026 Fantasy Football Encyclopedia
Working rules
format: espn-ppr1.0-10tm-stdgoes in the frontmatter of every derived note, so a later settings correction has a greppable blast radius.- No number is authored anywhere but
Data/players.csv. Notes render it or cite it; they never re-type a rank. - Every player reference resolves to one of the twelve labels in Player archetypes. A bare name carries zero information for this reader.
- Every jargon term's first use links to Fantasy football glossary.
- Zero LaTeX in
Start Here/andDraft/. - Perishable notes carry
expires. The audit fails any perishable note whoseas_ofis more than 14 days older than the draft date. - Cap
## Open questionsat three bullets.
Current run — 10-team refresh (2026-08-02)
Locked facts. Ten teams, ESPN, full PPR with fractional yardage, snake draft, 1.01, 16 rounds, 1QB/2RB/2WR/1TE/1FLEX/1D-ST/1K, seven bench and one IR. No keepers: treat this as a pure 2026 redraft. The exact pick vector is 1, 20, 21, 40, 41, 60, 61, 80, 81, 100, 101, 120, 121, 140, 141, 160.
Current market/projection controls. The certified projection input is FFToday's complete 419-row July 30 base-plus-pagination stat-line snapshot. Current market evidence is FFC 10-team PPR: 4,121 drafts from Jul. 26–Aug. 2. It is an availability clock, not ESPN ADP. Both feeds were frozen, independently reviewed and promoted into the 237-row Data/players.csv; hashes and import audit live in Data/research/research-run-manifest.json.
Replacement model. build_board.py encodes the confirmed 10-team lines: RB24, WR26, TE10, QB10, K2 and D/ST2. The promoted baselines are RB 201.3, WR 203.6, TE 159.5 and QB 295.3 points; the model and shallow-league consequences are documented in League size and replacement level.
Dynamic research campaign. Run ff-10team-20260802T192309Z used all 12 worker slots, atomic receipt ownership and controller-only shared-ledger writes; see Data/research/research-run-manifest.json and Data/research/research-task-ledger.csv. All team/status lanes, current-source captures, projection/market sources, independent verification, 1.01 decision work and the 10-team turn strategy have completed. Receipts—not camp anecdotes—are the merge inputs.
Current integration state. The canonical data/status merge, 10-team board, tiers, availability model, simulation, decision pages, player dossiers and generated site were released and verified on the August 2 freeze. The next required work is the mandatory refresh within 48 hours of the still-unknown draft time.
Still unresolved. Confirm the exact late-August draft date/timezone and the source of the $150 difference between $900 in confirmed gross prizes and $750 in ordinary pool contributions. The intermediate D/ST points-allowed brackets and sack/interception/fumble-recovery values also remain unrecorded; do not silently treat ESPN defaults as confirmed.
Historical control — Pass 1 on July inputs (2026-07-25)
Everything below this heading is retained as an audit trail for the original 12-team dry run. Its dates, 1/24/25 turn vector, 12-team baselines and July ADP counts are historical findings, not instructions for the active build.
Pass 1 — dry run on July data (2026-07-25)
The whole build exists once, end to end, on 2026-07-25 inputs. This proves the pipeline. It is not the artifact Andrew drafts from — the perishable layer gets rebuilt in the 72 hours before the draft (_PLAN §10.2).
Ordering gates
| Gate | State |
|---|---|
A — _Progress.md before the first note |
done |
B — format: defined and stamped |
done |
C — Player archetypes before any prose names a player |
done |
D — players.csv before any board prose |
done |
| E — tiers and turn map derived, never hand-edited | done — both render from build_board.py |
| F — player notes last, only the pick-window set | done |
| G — vault integration and link check at the end | done |
Data layer
| Artifact | State |
|---|---|
Data/raw/fftoday-projections-2026-07-23.txt |
Projected stat lines, 243 players, retrieved 2026-07-25 |
Data/raw/ffcalc-ppr-adp-2026-07-25.txt |
PPR ADP, 150 players, 3,091 mocks 07-18 → 07-25 |
Data/players.csv |
243 rows. Generated once from the two raw pulls, hand-maintained from here on |
Data/build_board.py |
One file, stdlib only. load · vor · tiers · render. --check runs the self-check |
Data/check_links.py |
Copied from the basketball build, unmodified |
proj_points is not an imported PPR total. It is computed from published projected stat lines under this league's own scoring (_PLAN §1.1), which is the only way the fractional yardage and full-point reception are actually applied.
Decisions taken during the build
Kicker and D/ST replacement baselines are set at rank 3, not 12. These are the only two positions where the wire reliably offers a startable option every week, so "the 12th best season-long" is the wrong replacement. Baselining them at 12 put Houston's defence at board rank 28, which is nonsense. Every other position uses the _PLAN §1.2 derived starter count (RB 29, WR 31, TE 12, QB 12). The board's baseline table still prints the rank-12 comparison, because that is the number Value over replacement in fantasy football argues from.
D/ST projected points are modelled and labelled as such. No published ranking prices this league's active yards-allowed brackets (_PLAN §5.2a), so the bracket contribution is computed from projected points and yards allowed under a normal approximation, not read off a source. The board says so in a callout. The result — a 3.4 point-per-week gap between the best defence and a typical starter, against the ~1.1 that published rankings imply — is exactly the understatement §5.2a predicted, but it is a model output, not an observation.
Kicker points use a modelled field-goal distance mix. Projections give makes and attempts, not distances, and this league pays 3/4/5/6 by distance with −1 for a miss. The distance split is the league-wide attempt mix, so kicker totals are ordering, not forecasts.
The turn map excludes K and D/ST. They are taken in the last two rounds by rule, not because value says so.
ADP is a labelled proxy. ESPN's own ADP could not be extracted programmatically (_PLAN §10.5 #2). Every row carries adp_source. ESPN mocks are the substitute measurement, which is another reason mocks are on the critical path.
Independent check of _PLAN's asserted VOR table
_PLAN §5.2 #2 asserts per-week edges "as of 2026-07-25, verify before use". Recomputed from players.csv:
| Pos | _PLAN asserts | This build computes | Verdict |
|---|---|---|---|
| RB | 11.0 | 10.7 | holds |
| WR | 8.4 | 8.5 | holds |
| TE | 5.2 | 5.7 | holds |
| QB | 4.2 | 4.4 | holds |
| K | 1.4 | 1.8 | holds |
| D/ST | 1.1 | 3.4 | does not hold — see the D/ST decision above |
Findings that correct _PLAN.md
Each was found by a writer checking a plan assertion against players.csv rather than repeating it. Verified independently before being applied, per §7.
- "Week 6 second worst" is not what the data says. _PLAN §5.1 #7 calls week 11 the crunch (six teams off — confirmed) and week 6 second worst. In
players.csv, weeks 6, 7, 8, 10 and 13 are all tied at four teams; weeks 5, 9 and 14 have two; week 12 has none. Bye-week planning and The 2026 season calendar state the tie accurately and argue week 6 is the worst of the four-team weeks because it is the earliest, which follows from the build's own "early byes hurt more" rule rather than from a miscount. - The elite tight ends are not gone before pick 24. _PLAN §5.2 #4 says the elite TE tier goes "~20–25", which would lock the 1.01 out of it entirely. The board says 28.9 and 35.8 — both just past picks 24 and 25. The honest version, now in Drafting from the 1.01: the 1.01 gets exactly one shot at the tier, at 24/25, and taking it spends half of the best turn on the position with the smallest per-week edge. By 48/49 both are gone.
_PLAN.mdnever specifies this league's per-event D/ST values. §1.1 and §5.2a give the yards-allowed brackets, one points-allowed value (0 allowed = 5) and safety = 4, but no sack, interception or fumble-recovery value.build_board.pyassumes the ESPN defaults — sack 1, interception 2, fumble recovery 2, defensive or return touchdown 6 — and this is an unverified assumption, not a confirmed setting. Read the rest off ESPN before the draft.players.csvis structurally silent on availability. Every row carriesgames: 17and there is no injury field, so no projection in this build is discounted for missed time. Theriskcolumn is a projection-confidence flag, not a durability rating. Player notes say so rather than implying otherwise.- Two _PLAN claims about specific players could not be verified and were not asserted. The "Bo Nix finished top-8 in total quarterback points but roughly QB10 per game" claim has no source in this build, and no candidate's injury history could be checked. Both are flagged in the notes as unverified rather than silently converted to fact.
Verification passes — what they found
Three independent passes ran over the finished build, each with a different lens, each told to refute and to default to refuted when uncertain.
| Pass | Lens | Findings |
|---|---|---|
| Beginner comprehension (_PLAN §9.3 phase 7) | "would someone who has never watched an NFL game understand this?" over all of Start Here/ and Draft/ |
30 |
| Numbers against source | every quantitative claim recomputed from players.csv and the raw pulls |
9 |
| Settings consistency | every settings-dependent claim against _PLAN §1 and §5.2a | 4 |
Phase 7 is the pass the basketball build never ran, and it earned its place: its single largest finding was that Tiers by position rendered roughly 250 players as bare names with no archetype, which breaks the build's own binding rule for exactly the reader it is written for. That was a renderer defect, not a writing one, and it was fixed in build_board.py.
Defects found in build_board.py and fixed:
- The tier stacks named ~250 players with no archetype. Every entry now renders as
Jahmyr Gibbs — three-down bell cow (RB1, bye 6), and the note carries a key explaining thatRB1means first in this file, not a top-twelve claim. - The baseline table's second column was mislabelled. It claimed to compare against "a typical (12th) starter" while actually comparing against the same replacement rank as the first column. The board now prints two genuinely different columns — over replacement (RB 10.7, WR 8.5 per week) and over the 12th-best at the position (RB 6.3, WR 5.9) — and says which one answers which question.
- Kicker points were modelled but not flagged. Only D/ST carried the modelled-not-observed warning; kicker points rest on an assumed field-goal distance mix and are equally a model output. Both are now flagged together.
- The board's
Riskcolumn drove recommendations but was never defined. It now says what it encodes, that it is assigned mechanically, and — the important part — that it is not a durability or injury rating, because nothing in this build knows anything about injury history. - The
Δ vs ADPcolumn, the team codes and the tier-cut rule were all opaque to the target reader. The board now carries a plain-language column key.
Findings rejected as wrong (rejecting counts as much as applying, §7):
- "The −5 for 46 or more points allowed is unconfirmed." It is confirmed, in _PLAN §5.2a. The −12 worked example stands.
- "Safety = 4 is unconfirmed." Confirmed on the same line.
Contradictions between notes, reconciled to one canonical wording each: when to take a quarterback (three notes disagreed, and one of them is the note read on the clock); the quarterback-flatness claim (stated four different ways); whether handcuffs suit a convex payout; the length of the fantasy regular season; and whether week 6 is the second-worst bye week. Every reconciliation was decided from players.csv, not by picking a favourite.
The canonical quarterback rule, now stated in identical words in both Drafting from the 1.01 and Late-round quarterback:
No quarterback at 24/25 — that turn is the only place in this draft where two top-twelve receivers are jointly available, and the quarterback edge does not pay for giving that up. Picks 48/49 and 96/97 are the trap. 72/73 is the one live early window — Daniels (ADP 85.4) and Hurts (ADP 93.7) are still on the board there, at a reach of roughly twelve and twenty slots. Otherwise wait to 120/121, where Dart (ADP 121.3) lands almost exactly on the pick. Never spend 168, 169 or 192 on a second quarterback: those three are the kicker, the defence, and the IR stash.
Applied: 45 edits across 14 notes plus five renderer fixes. Two findings rejected. After the fix pass: 1,053 wikilinks checked, 0 broken, 0 wrong-path, 0 ambiguous; frontmatter and open-questions audit clean; build_board.py --check passes and re-rendering is byte-identical.
The corrections a pass like this cannot make are the ones worth naming: nothing in this build knows a player's injury history, age, contract or usage history, and no note asserts one. The risk column is a role-contest flag, not a durability rating. Superseded by Pass 1b below, which closes exactly this gap; the risk column still means what it says here.
Rejected
- 32 team desks with conserved touch budgets. _PLAN §5.3 already ruled this out; nothing found during the build argued to reinstate it.
- A second scoring format, a projection engine, and the pre-draft HTML wiki. _PLAN §4.1.
- Importing a published PPR points total as
proj_points. It would bake in someone else's scoring, and this league's kicker and defence scoring are both non-standard. - Baselining every position at the derived starter count. Correct for RB/WR/TE/QB; wrong for the two streamed positions.
Pass 1b — observed player context added (2026-07-25)
The gap Pass 1 closed with a caveat — nothing in this build knows a player's injury history, age, contract or usage history — is now closed with data, for all 203 skill players on the board (243 rows minus the 20 kickers and 20 defences, which have no player-level history to join).
What was added, and from where
Data/player_context.csv, 203 rows, one per skill player in players.csv. Every field observed, all retrieved 2026-07-25.
| Fields | Source |
|---|---|
age on the 2026-09-09 opener, years_exp, draft_year, draft_overall, draft_club, college |
nflverse roster_2026.csv + draft_picks.csv |
g_2023, g_2024, g_2025, games_played_3yr, games_missed_3yr, and 2025 usage — snap_pct, carries, targets, receptions, tgt_share, yardage |
nflverse stats_player_reg_2023/24/25 + snap_counts_2023/24/25 |
inj_weeks_out_3yr, inj_weeks_qd_3yr, inj_parts_3yr, missed_off_injury_report |
nflverse injuries_2023/24/25 — the official weekly report |
contract_value, contract_apy, contract_gtd, contract_free_agent |
OverTheCap live position pages. nflverse's own contracts release stops at 2022 and is unusable for anyone drafted since |
Two new scripts, stdlib only. Data/fetch_nflverse.sh pulls roughly 120MB of raw releases into a scratch directory that is not kept in the vault — only the derived CSV is. Data/build_context.py joins them.
Run order. This is the only ordering constraint in the build:
fetch_nflverse.sh → build_context.py → build_board.py
build_board.py fails closed if player_context.csv is missing, per §6's no-fail-open rule. Outputs: Age, Avail and Adj VOR columns on 2026 draft board, plus one new generated note, Availability and injury risk.
The join bug a validator caught
build_context.py carries a validator whose only job is to refute its own join: if a player's projection line calls for 40 or more catches, or 150 or more carries, and his joined 2025 season shows fewer than 10 receptions or 20 carries, the match is almost certainly a different human.
It fired on D.J. Moore — a big slot — matched to David Moore, an unrelated receiver who is not even in players.csv. The name normaliser was splitting "D.J." into two tokens, so "d j moore" reduced to a Moore-plus-initial that David Moore also satisfied. D.J. Moore had been carrying David Moore's three-year games played, which would have flowed straight into his avail and from there into his adjusted board rank. Fixed by joining runs of single letters before matching.
The point worth recording: the defect was found by a check written to fail, not by reading the output. Nothing in the finished CSV looked wrong.
The finding that contradicts _PLAN §10.5 risk 4
§10.5 risk 4 asserts that VOR does not price running-back injury risk "and RBs are the most injury-prone position — roughly 7 in 10 lead backs miss at least one game", then uses that to caveat the running-back-heavy top of the board. Measured over the 134 board players who have three full seasons of history:
| Pos | Players | Seasons missing 1+ game | Missing 4+ | Eight games or fewer |
|---|---|---|---|---|
| QB | 24 | 68% | 32% | 19% |
| RB | 41 | 65% | 29% | 8% |
| WR | 46 | 67% | 24% | 8% |
| TE | 23 | 70% | 32% | 6% |
The "7 in 10" rate is right in magnitude and wrong in attribution. Every position sits between 65% and 70%, and running back is the lowest of the four; backs also lose whole seasons less often than quarterbacks or tight ends. The caveat that survives is a shape claim, not a rate claim — running back carries the fatter left tail, so backs are not more likely to miss a game, they are more likely to miss a lot of them. Availability and injury risk states it that way, and The injury-inheritance asymmetry now says which half of its own claim this grounds and which half it does not.
One limit, stated on the board and in the note: this pool is the top ~200 by 2026 projection, so it is survivor-selected. A career ended by injury is not in it. The narrow claim — among the players you would actually draft, running-back availability is not measurably worse than receiver availability — holds. The wider claim that the position destroys careers faster is untested here and may well be true.
The third and fourth deliberately-labelled models
Pass 1 shipped two model outputs and labelled both: D/ST projected points, and the kicker field-goal distance mix. This pass adds two more. The running count is worth keeping, because a model that quietly stops being labelled is how a build starts lying.
avail— a player's three-year games-played rate shrunk toward his position's base rate by the weight of one full season (PRIOR_W = 17). Without the shrinkage a rookie at 17-of-17 scores a perfect 1.00 off a single season, which is the exact small-sample error Sample size in a 17-game season warns against everywhere else; that note now uses this as its worked example. Players with no NFL history carry the position base rate outright and are flaggedavail_basis = position base rate.Adj VOR— value over replacement recomputed after scaling each projection byavaildivided by the position's meanavail. The scaling is deliberately relative: a published projection already prices the average player's injury risk, so multiplying by raw availability would count it twice. An average-durability player does not move at all.
Everything else in player_context.csv is observed and is presented as such. Games played, injury-report weeks, age, draft position and contract terms are counted facts; avail and Adj VOR are marked as models on every surface they appear on.
One reading caution now baked into the notes: a missed game is not automatically an injury. games_missed_3yr counts absence of every kind, and missed_off_injury_report shows that most missed games in this file were not accompanied by a week-of listing on the official report — partly because a player placed on injured reserve drops off the weekly report entirely, and partly because suspensions, holdouts and inactive backups all read the same way in a games-played column. The two fields are kept separate rather than merged for this reason.
Pass 1b — the context layer and the website (2026-07-25)
Andrew asked for two things the first pass could not do: real injury, age, contract and usage history for every player, and the whole vault rendered as a website.
The context layer
Data/player_context.csv — 203 skill players, observed, joined from nflverse (2026 rosters, 2023–2025 season stat lines, 2023–2025 official weekly injury reports, snap counts) and OverTheCap's live contract pages. Run order, and the only ordering constraint in the build:
./fetch_nflverse.sh → build_context.py → build_board.py → simulate_season.py
build_board.py fails closed if the context file is missing. nflverse's own contracts release was unusable — it stops at year_signed 2022, so it has nothing for anyone drafted since — hence OverTheCap.
Three data defects, all found by checks rather than by reading
- A wrong-player join.
D.J. Moorematched David Moore, inheriting 27 missed games instead of 0. Caught by adding a validator that flags any joined row whose observed usage is impossible given the projection — a receiver projected for 71 catches cannot have caught 1 last season. It now runs on every build and currently reports zero. - A missing input silently becoming a zero — the exact failure
_PLAN.md§6 forbids. Five players had no stat line found for seasons they played, and the code scored that as seventeen missed games: Kenny Gainwell came out at 0.197 availability after three healthy seasons. Nickname forms now match, and a season with no stat line is excluded from the denominator and recorded inseasons_unmatchedrather than counted as missed. - Positional column assignment. Games played were written by right-aligning a list, so a player missing one season had his 2024 total printed under 2025. Now keyed by season.
The RB-injury finding, and how it changed under correction
The first version of this claimed running backs were more durable than receivers. After defects 1–3 were fixed the gap closed almost to nothing, and the honest version is narrower:
| Pos | Mean availability | Missing 1+ | Missing 4+ | Eight or fewer |
|---|---|---|---|---|
| QB | 0.806 | 67% | 29% | 16% |
| RB | 0.857 | 63% | 27% | 6% |
| WR | 0.861 | 67% | 24% | 8% |
| TE | 0.830 | 70% | 32% | 6% |
On none of the four measures is running back the worst position, and quarterback is worst on two. So _PLAN.md §10.5 risk 4 — "RBs are the most injury-prone position" — is not what the record shows, and the board's caveat has been rewritten. The pool is survivor-selected (players who mattered in 2026 and were in the league all three years), which is stated in the note.
The comparative sentences in that note are now derived from its own table, not asserted. A verification pass caught the previous version claiming running backs had "the lowest rate of near-lost seasons" three lines below a table showing they did not. Prose that restates a computed table will eventually contradict it; prose generated from the table cannot.
Availability is not injury
Avail counts a missed game whatever caused it, so suspensions, holdouts and healthy backups all land in it. The note now prints the players whose absences mostly never reached an official injury report — Rashee Rice's 23 missed games include only 2 weeks listed Out — so the number is never read as fragility when it is not.
Two new models, and one measurement
Adj VORscales each projection by the player's availability relative to his position's mean, then recomputes value over replacement. Relative on purpose: a published projection already prices the average player's risk, so scaling by raw availability would count it twice. This closes the caveat the first board had to concede.- When to start drafting for upside finally measures _PLAN.md's open question. The answer is that the question was posed wrong — see the note. Two versions of the experiment had to be thrown away first: one where "upside" and "value" produced identical rosters from round 7 on, so the sweep measured nothing, and one where the drafter paid for role upside the simulated season never delivered.
The website
~/Desktop/Code/fantasy-football-encyclopedia — 118 pages, python3 build.py. Same architecture as the fantasy basketball wiki (pre-generated, token CSS, paper/lamplight, client-side search), shifted to a pine-and-brass register. The Draft Room is built straight from the CSVs by the same arithmetic as build_board.py, so it cannot drift from the vault board. Player pages carry fact cards with the observed context data.
§9.4 — the post-draft decision, written down now
- The perishable research layer is abandoned at the draft. Board, tiers, ADP deltas, turn map, player notes, rookie note. A half-maintained board that still looks authoritative in Week 6 is worse than no board.
- The weekly checklist stays alive to the final whistle. Tuesday claims → Wednesday adds → Friday designations → Sunday inactives → start/sit by implied team total. One page: The fantasy football week and Start-sit — a standing rule, not a research project. The $25 deposit is refunded only for participation, so going quiet in Week 8 costs real money on top of the lost season.
Historical open items (superseded by current-run block)
- Exact draft date/timezone and league-playoff/payout details. These are no longer the only missing inputs; use the current-run blocker list above.
- Pass 2 — within 72 hours of the confirmed draft, refresh projections and market, regenerate
players.csv, rerunbuild_board.py, and rewrite Who to take at 1.01 and The 2026 rookies who matter first. - Mock drafts. Zero logged in the July control. Run at least five from 1.01, including one human room, on the actual ESPN-style 10-team board.
Open questions
- Where does the ceiling-over-floor tilt stop biting? The round-7 crossover is still asserted rather than measured; a simulation over
players.csvwould settle it and is the single highest-value addition left. - Does the weekly waiver reset change optimal draft strategy, not just in-season play? Unmeasured.
- Is the D/ST bracket model right? It is the one number in this build with no published check.