A new season means canonical identities have to be born, not just copied
When a new NFL season rolls around, Statpro has to create a fresh set of canonical identity rows for every team, player, and game. You can't just copy last season's identities over because rosters change, team slugs get disambiguated, and the schedule is entirely new. Today's work shipped the 2026 canonical identity bootstrap and then spent the rest of the night fixing the cascade of edge cases that fell out of it.
The bootstrap: one transaction, one receipt
The bootstrap creates canonical team crosswalks, player profiles (with slug disambiguation when two players share a name), and game rows, all inside a single serializable transaction. The nice part is that the whole thing produces a hash-signed receipt so you can tell whether a re-run would produce the exact same state or actually change something:
// The receipt tells you what happened and whether re-running
// is safe. disposition: "planned" | "created" | "exact_replay"
type BootstrapReceipt = {
kind: "nfl-primary-v2-canonical-bootstrap";
version: "nfl-primary-v2-canonical-bootstrap-v1";
season: number;
counts: Record;
catalogCounts: { seasons: 3; teams: 32; players: number; games: number };
bootstrapHash: string;
disposition: "planned" | "created" | "exact_replay";
};
If disposition comes back exact_replay, the bootstrap detected that the target state already matches what it would create, and it touched nothing. That's what makes the migration replay-safe: you can run it forward at future migration heads without double-inserting, and the receipt is your audit trail.
The transaction itself runs at serializable isolation with a short lock timeout, so if two bootstrap attempts collide, one backs off instead of deadlocking the production database:
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET LOCAL lock_timeout = '5s';
SET LOCAL statement_timeout = '5min';
SET LOCAL idle_in_transaction_session_timeout = '30s';
The AZ problem
My favorite bug from the cascade: the Arizona Cardinals' source team code is AZ but the canonical code everyone expects is ARI. The bootstrap had a one-line fix for it, but it's the kind of thing that silently produces broken crosswalks if you assume source codes and canonical codes are the same string:
function canonicalTeamCode(sourceCode: string): string {
return sourceCode === "AZ" ? "ARI" : sourceCode;
}
Small, obvious in hindsight, and exactly the sort of identity-mapping edge case that a new season surfaces. Last season's bootstrap worked because the AZ Cardinals already existed; a fresh bootstrap against the source catalog exposed the mismatch.
Owner envelope fixes
After the bootstrap landed, five follow-up commits fixed the NFL owner envelope: replay permission at future heads, date readback, evidence count, evidence clock, and forecast evidence ordering. Each was a small correctness fix in how owner-level facts (the verified identity facts a published artifact depends on) survive a migration replay. The pattern across all of them: the owner envelope's evidence has to be ordered and counted consistently, or a replay produces a different hash and the receipt's exact_replay check fails when it shouldn't.
The takeaway for anyone building a canonical-identity system: make your migrations produce a hash of what they did, and make re-runs detect when they'd produce identical state. You'll catch a whole class of "did I already run this?" bugs for free, and your audit trail writes itself.