FR

Prediction Timelines

The life cycle of a call, made structural: a home page split by kickoff, a two-hour delay so no match vanishes mid-play, and a revision history with a deadline-lock fallback that guarantees every fixture kicks off with a prediction attached.

Part of the Paulopus case study. A prediction has a life cycle: it appears before a match, stays open and revisable while the fixture is still ahead, then hardens into a verdict once the result is in. Most of Paulopus's product surface exists to make that arc legible — to show, at a glance, which calls are still live and which have already been graded. This focus is about the timeline machinery: the tab split, the delayed move to history, the revision model behind a single pronostic, and the fallbacks that keep a call from ever going missing.

Splitting the home page by kickoff

The first home page carried a Timeline/Directory pair, which conflated two very different states — matches you can still argue about and matches already settled. On 2026-06-13 the homepage was restructured into three tabs, partitioning by the one fact that actually governs a prediction's status: has kickoff passed?

The split runs through a splitByKickoff() partition feeding a shared MatchList component, so both the live and the settled views render from the same row primitive.

TabContentsPrediction state
Upcomingfixtures ahead of kickofflive, revisable
Historymatches past the move thresholdfrozen, graded
Teamsthe 48 competing sidesreference

The result was a home page where the prediction's status is structural, not a badge you have to read — a call is either in the tab where you can still watch it move, or in the one where the verdict is fixed. (Home tabs split: Upcoming / History / Teams, PR#3.)

Delaying the move to history until kickoff + 2h

The obvious rule — move a match to History the instant kickoff passes — has a sharp edge: a match that has just kicked off is neither upcoming nor settled. Snapping it to History at minute zero would make in-progress fixtures vanish from the live view and reappear beside fully-graded ones, with no result to show.

So the upcoming-to-history transition was delayed to kickoff + 2 hours, roughly a full match plus stoppage and halftime, so a fixture stays in Upcoming for its entire duration and only crosses over once it is genuinely done.

kickoff ─────────────▶ kickoff + 2h ─────────────▶
  │  match in progress │   result settled
  └── stays in Upcoming ┘   moves to History

The payoff is that no match ever jumps tabs while people are watching it. The same batch of work also fixed a regression that had briefly dropped the predicted score from match rows — the prediction has to stay visible for the row to mean anything. (Pronostic shown as a comment on match rows, PR#7 / PR#11 / PR#12.)

The pronostic as a comment beside the verdict

A prediction and its outcome are two moments, and the row has to hold both without looking like a scoreboard. The chosen treatment renders the pronostic as a chat-style comment sitting beside a verdict octopus — the prediction framed as something the octopus said, not a stat printed in a cell.

Which text shows depends on where the match sits in its timeline:

Match stateComment surfaced
Before resultthe locked pronostic statement
After resultthe debrief's pronostic appreciation

Before kickoff the row speaks the prediction; after the whistle it speaks the reflection on it, the octopus grading its own earlier call. The verdict octopus beside the comment carries one of four result moods (introduced in the same era's mascot refresh), so the row reads as a small dialogue between a claim and its outcome rather than a data grid. (Pronostic shown as a comment on match rows, PR#7.)

Making the pronostic revisable up to kickoff

The earliest model treated a prediction as a write-once lock: one immutable statement, committed inside a narrow window, never touched again. That is honest but brittle — a call made a day out can't absorb a confirmed lineup or a fresh injury report.

On 2026-06-13 the immutable lock was replaced with a drafts[] revision history driven by a single pronosticTrigger() function, so a fixture accumulates a sequence of dated calls up to kickoff instead of one frozen one.

// pronosticTrigger() — three occasions to (re)write the call
type PronosticTrigger =
  | "opening"          // first look at the fixture
  | "brief-update"     // a new detailed brief landed
  | "lineup-confirmed" // starting XIs are in

// each match: drafts[] — the pronostic revision history
// latest draft = the live call; ingest appends only on real change

Two integrity rules keep the history meaningful rather than noisy: ingest is append-only on real change (an identical re-run adds nothing), and writes are pre-kickoff only (once the match starts, the last draft is the final word). The latest entry in drafts[] is always the live call. (Regenerable pronostic drafts replace write-once locks, PR#13.)

Guaranteeing a call exists before kickoff

Revisability introduces a failure mode: the primary lock window is deliberately narrow — T-2h to T-5min — and the writer is a scheduled routine, not a service watching the clock. If no routine run happens to fall inside that window, a match could reach kickoff with no committed pronostic at all.

The fix was a deadline-lock fallback. When the narrow primary window is missed and kickoff is close — a wider T-6h to T-5min band — the fallback promotes the match's latest early-read draft to the lock verbatim, so an out-of-window run still yields a pronostic.

WindowRangeBehavior
Primary lockT-2h .. T-5minwrite the pronostic normally
Deadline fallbackT-6h .. T-5minpromote latest early-read draft to the lock

The invariant it buys: every match that kicks off has a call attached to it. No fixture reaches History with an empty prediction because the routine's cadence happened not to line up with the tight window. (Deadline-lock fallback for missed lock windows, PR#8.)

Freezing the timeline when the tournament ended

The whole Upcoming/History apparatus assumes there is a future to predict. Once the World Cup finished, there was none — every fixture was played, every call graded. On 2026-07-20 the home page dropped the Upcoming tab and grid entirely and changed the default tab from upcoming to history, so the site opens on the settled record rather than an empty live view.

That the freeze required almost no scoring change is a property of the derivation model: there is no stored karma ledger. lib/karma.deriveLedger(matches) recomputes the ledger on every read from each match's drafts[] (latest = the live call) and its result, with lib/scorer.ts scoring on the played score (extraTime ?? regulation) and the real winner:

// verdict + points, assigned deterministically at read time
Perfect  +1.5   // exact played score
Inspired +1
Missed    0
RedCard  -1

Because nothing is frozen at write time, retiring the live view is purely a presentation change — the timeline stops moving, and the History tab becomes the whole story. (Home page defaults to History; Upcoming tab hidden, PR#24; derivation per dataModel in architecture-summary.json.)

Prediction Timelines
  • slugprediction-timelines-0
  • contentPart of the [Paulopus](/plant/paulopus#execution) case study. A prediction has a life cycle: it appears before a match, stays open and revisable while the fixture is still ahead, then hardens into a verdict once the result is in. Most of Paulopus's product surface exists to make that arc legible — to show, at a glance, which calls are still live and which have already been graded. This focus is about the timeline machinery: the tab split, the delayed move to history, the revision model behind a single pronostic, and the fallbacks that keep a call from ever going missing. ## Splitting the home page by kickoff The first home page carried a Timeline/Directory pair, which conflated two very different states — matches you can still argue about and matches already settled. On 2026-06-13 the homepage was restructured into three tabs, partitioning by the one fact that actually governs a prediction's status: has kickoff passed? The split runs through a `splitByKickoff()` partition feeding a shared `MatchList` component, so both the live and the settled views render from the same row primitive. | Tab | Contents | Prediction state | | --- | --- | --- | | Upcoming | fixtures ahead of kickoff | live, revisable | | History | matches past the move threshold | frozen, graded | | Teams | the 48 competing sides | reference | The result was a home page where the prediction's status is structural, not a badge you have to read — a call is either in the tab where you can still watch it move, or in the one where the verdict is fixed. (`Home tabs split: Upcoming / History / Teams`, PR#3.) ## Delaying the move to history until kickoff + 2h The obvious rule — move a match to History the instant kickoff passes — has a sharp edge: a match that has just kicked off is neither upcoming nor settled. Snapping it to History at minute zero would make in-progress fixtures vanish from the live view and reappear beside fully-graded ones, with no result to show. So the upcoming-to-history transition was delayed to **kickoff + 2 hours**, roughly a full match plus stoppage and halftime, so a fixture stays in Upcoming for its entire duration and only crosses over once it is genuinely done. ```text kickoff ─────────────▶ kickoff + 2h ─────────────▶ │ match in progress │ result settled └── stays in Upcoming ┘ moves to History ``` The payoff is that no match ever jumps tabs while people are watching it. The same batch of work also fixed a regression that had briefly dropped the predicted score from match rows — the prediction has to stay visible for the row to mean anything. (`Pronostic shown as a comment on match rows`, PR#7 / PR#11 / PR#12.) ## The pronostic as a comment beside the verdict A prediction and its outcome are two moments, and the row has to hold both without looking like a scoreboard. The chosen treatment renders the pronostic as a chat-style comment sitting beside a verdict octopus — the prediction framed as something the octopus *said*, not a stat printed in a cell. Which text shows depends on where the match sits in its timeline: | Match state | Comment surfaced | | --- | --- | | Before result | the locked pronostic statement | | After result | the debrief's pronostic appreciation | Before kickoff the row speaks the prediction; after the whistle it speaks the reflection on it, the octopus grading its own earlier call. The verdict octopus beside the comment carries one of four result moods (introduced in the same era's mascot refresh), so the row reads as a small dialogue between a claim and its outcome rather than a data grid. (`Pronostic shown as a comment on match rows`, PR#7.) ## Making the pronostic revisable up to kickoff The earliest model treated a prediction as a write-once lock: one immutable statement, committed inside a narrow window, never touched again. That is honest but brittle — a call made a day out can't absorb a confirmed lineup or a fresh injury report. On 2026-06-13 the immutable lock was replaced with a `drafts[]` revision history driven by a single `pronosticTrigger()` function, so a fixture accumulates a sequence of dated calls up to kickoff instead of one frozen one. ```ts // pronosticTrigger() — three occasions to (re)write the call type PronosticTrigger = | "opening" // first look at the fixture | "brief-update" // a new detailed brief landed | "lineup-confirmed" // starting XIs are in // each match: drafts[] — the pronostic revision history // latest draft = the live call; ingest appends only on real change ``` Two integrity rules keep the history meaningful rather than noisy: ingest is **append-only on real change** (an identical re-run adds nothing), and writes are **pre-kickoff only** (once the match starts, the last draft is the final word). The latest entry in `drafts[]` is always the live call. (`Regenerable pronostic drafts replace write-once locks`, PR#13.) ## Guaranteeing a call exists before kickoff Revisability introduces a failure mode: the primary lock window is deliberately narrow — **T-2h to T-5min** — and the writer is a scheduled routine, not a service watching the clock. If no routine run happens to fall inside that window, a match could reach kickoff with no committed pronostic at all. The fix was a deadline-lock fallback. When the narrow primary window is missed and kickoff is close — a wider **T-6h to T-5min** band — the fallback promotes the match's latest early-read draft to the lock verbatim, so an out-of-window run still yields a pronostic. | Window | Range | Behavior | | --- | --- | --- | | Primary lock | T-2h .. T-5min | write the pronostic normally | | Deadline fallback | T-6h .. T-5min | promote latest early-read draft to the lock | The invariant it buys: every match that kicks off has a call attached to it. No fixture reaches History with an empty prediction because the routine's cadence happened not to line up with the tight window. (`Deadline-lock fallback for missed lock windows`, PR#8.) ## Freezing the timeline when the tournament ended The whole Upcoming/History apparatus assumes there is a future to predict. Once the World Cup finished, there was none — every fixture was played, every call graded. On 2026-07-20 the home page dropped the Upcoming tab and grid entirely and changed the default tab from `upcoming` to `history`, so the site opens on the settled record rather than an empty live view. That the freeze required almost no scoring change is a property of the derivation model: there is no stored karma ledger. `lib/karma.deriveLedger(matches)` recomputes the ledger on every read from each match's `drafts[]` (latest = the live call) and its `result`, with `lib/scorer.ts` scoring on the played score (`extraTime ?? regulation`) and the real winner: ```ts // verdict + points, assigned deterministically at read time Perfect +1.5 // exact played score Inspired +1 Missed 0 RedCard -1 ``` Because nothing is frozen at write time, retiring the live view is purely a presentation change — the timeline stops moving, and the History tab becomes the whole story. (`Home page defaults to History; Upcoming tab hidden`, PR#24; derivation per `dataModel` in architecture-summary.json.)
  • date2026-07-24
  • descriptionThe life cycle of a call, made structural: a home page split by kickoff, a two-hour delay so no match vanishes mid-play, and a revision history with a deadline-lock fallback that guarantees every fixture kicks off with a prediction attached.
  • namePrediction Timelines
  • typearticle
  • statepublished