tootsies

a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.


Project maintained by mejasonmejason Hosted on GitHub Pages — Theme by mattgraham

Billboard alerts: when we fire, and what it looks like

The plan for the alert lanes on top of utils/billboard.py + the HITS forecast feed. Two jobs, deliberately separated:

  1. Parity — report what everyone has. The weekly chart, the #1, the top-10 debuts. Table stakes. If we don’t have this we’re not a chart account.
  2. Edge — the things the accounts copying each other structurally cannot post. This is where the sources research pays off, and it is most of this document.

Every timing claim below is measured, not assumed; the probes are in docs/BILLBOARD_SOURCES.md.


1. The race we cannot win — and shouldn’t enter

Measured Monday 2026-08-03 22:00 UTC:

Source Chart it was serving
@PopBase on X Aug 8, 2026 (posted ~5h earlier)
billboard.com/charts/hot-100/ Aug 1, 2026
GitHub JSON mirror Aug 1, 2026

The wire has the top 10 about a day before any feed does. Billboard hands its headline chart news to press/socials before the chart pages flip, so a poll-only lane is structurally last. No amount of polling fixes this — it is a publishing decision, not a latency problem.

So: do not build a lane whose job is to beat PopBase to “here’s the top 10.” We would lose every week, and lose visibly. The wire is the trigger; the feed is the truth.

What the copiers all share is that they post the top 10, on Sunday/Monday, after the chart exists. Every edge below is a different axis: earlier, deeper, or reconciled.


2. The lanes

Lane A — Chart drop (PARITY)

Because we’re second by design here, the post should not pretend to be breaking. It should carry what a repost can’t: full context per row (peak, weeks on, the streak).

PREVIEW (mock — illustrative, not model output):
  hot 100, week of aug 1.
  ella langley's 40th week on the chart and 16th at #1. taylor holds at 2.
  lil baby debuts at 18. BTS re-enters at 33.

Lane B — The called shot (EDGE: ~5 days early) ⭐

This is the highest-value lane and the reason the HITS source matters.

HITS Daily Double’s building chart projects first-week units for the chart that has not published yet. We can call the #1 — with a number — days before Billboard prints it, and then be publicly right.

This is validated on real data, both directions:

Retrospective (it works): HITS on Jul 25 projected GRACIE ABRAMS | DAUGHTER FROM HELL — NEW at #1, 123,975 units. The Billboard 200 dated Aug 1 — which is what billboard.com serves right now — printed #1, Daughter From Hell, DEBUT. Called correctly, days ahead.

Live and unresolved as of writing: HITS on Jul 31 projects CHARLI XCX | MUSIC, FASHION, FILM — NEW at #3, 74,403 units (52,027 pure album sales, 22,272 streaming). The Billboard 200 dated Aug 8 has not published. That is a callable shot right now, scoreable within ~48h — and the natural first live test of this lane.

PREVIEW (mock):
  charli xcx is tracking for a #3 debut. ~74k first week, 52k of it pure sales.
  chart doesn't print till friday. we'll see.

The honesty rule matters here: a projection is stated as a projection, and the lane is worthless if it hedges into meaninglessness or overclaims into being wrong. Fail CLOSED — no HITS doc, no called shot.

As built. A lane in cogs/music_desk.py, ranked second in _LANE_PRIORITY — behind the printed chart (a harder fact) and ahead of everything else, because it is the most perishable thing the desk carries: its whole value is the lead over Billboard’s print, and it is worth nothing once the chart lands.

Live dry run (2026-08-04, real Sanity pull + real compose + the real 0.6 gate), on the Charli XCX call above: 6/6 ship at 0.92, every number matching the source, no career or trajectory claims. The framing needed two rounds to get there, both caught by the dry run and invisible to unit tests:

  1. the first version produced wire copy — “…is tracking a #3 debut on the Billboard 200, per Hits Daily Double, with ~74.4K projected units, 52K of it pure sales” — four near-identical headlines with the trailing press-release tag that _CONTEXT already bans for Kalshi;
  2. the block and the framing were fighting: the block said “credit them by name” while the framing banned the trailing tag, so the model kept reaching for it.

Aligning both on “credit HITS inside the sentence, in your own words” fixed it: 0/6 used the banned tag afterwards. The self-gate could not have caught this — it grades grounding and accuracy, and the copy was accurate the whole time.

Lane C — Deep cuts (EDGE: depth)

Everyone posts the top 10. We parse all 100 (and all 200). Rows 11–100 are uncontested territory and they’re where the actual movement is.

Real examples off the Aug 1 Hot 100, none of which any top-10 post contains: a 31-spot climb to #26 (Gracie Abrams, “Hit The Wall”), a 25-spot climb to #17 (Shakira, the World Cup song), a re-entry at #33 (BTS), an 11-spot fall at #42 (Beyoncé).

The mid-chart MOVE was cut back (owner steer, 2026-08-11: “cut mid chart moves”). The depth edge is real for ENTRIES, and it was too wide for MOVES. Measured over eight real chart weeks (both charts, the live floors): 358 candidate move-cards, of which 226 (63%) sat outside the top 20 and were not entries — a 5-spot climb by Folklore at #98, ANTI at #73, a fall by Purple at #50. That is catalog drift, and nobody reads a card about it. Two things fed it: the notability score trades rank against move size, so a big enough climb bought its way in from anywhere on the page; and the watch pass floors a top-100 artist at 2, which is exactly the tier whose catalog drifts.

So RANK is now a gate ahead of the score (music_desk._within_rank_cap):

The rank caps sit AHEAD of the notability floors, so a big move deep in the chart can no longer buy a card. The env knobs are MUSIC_DESK_CHART_MOVE_MAX_RANK (20) and MUSIC_DESK_CHART_ENTRY_MAX_RANK (40).

“Wherever it lands” reaches the bottom half of the 200-row charts too (#2321 review). billboard.movements scans the top 100 by default, so a watched act’s debut at #101–200 on the Billboard 200 or Global 200 was never derived and the no-cap rule could not fire. A third derivation pass reads the WHOLE chart for ENTRIES only (watched acts, no notability floor), so the promise holds across the full chart. Entries only, so it never re-opens the mid-chart MOVE the cut removed.

The album bomb is a board now, not N cards (owner steer, 2026-08-11). When an album lands, every track enters the Hot 100 the same week and the movement lane carded each one. Three of the eight measured weeks did this: Ariana Grande 11 debut cards off petal, Future 16 off The Real Me, Olivia Rodrigo 10 — 37 near-identical cards from 3 releases. So a cluster of _MIN_CLUSTER_TITLES (3) debuts by one act becomes ONE board (chart_standings.album_drop_board, story album_drop). The act’s best-placed debut keeps its own card (owner call: the headline entry is the story, the board is the depth); every other title is folded out of the movement lane (_pick_chart_moves reads _folded_debuts). The board names the act’s own album debut as a SECOND stated fact — “the album X entered the Billboard 200 at #N the same week”, read off the album chart the desk already fetches — and never claims the listed songs are ON it (a tracklist source we do not have here; a feature single debuting the same week would make the claim false). When no album of the act debuted that week the board names no album at all (owner: “only if theres not album ofc”; prefer absent over invented). Grouping is on the LEAD credit, so an album’s guest features do not split the cluster back into cards. Live dry run (2026-08-11, real Aug 15 Hot 100 + real compose + the 0.6 gate): the Ariana cluster composed 3/3 at 0.92, every take grounding the album at #1 on the 200.

Lane D — Reconciliation (EDGE: nobody else has both numbers)

We hold three numbers for the same release that rarely agree:

The gaps between them are a story only someone holding all three can tell — and it’s inherently accountable, which is the tone that earns trust.

The running scorecard (owner ask 2026-09-16). “Has their predictions for previous weeks been accurate” is the reply every projection post draws on X, so the ledger now holds the whole record per row: HITS’ projected rank + units, Kalshi’s implied units at call time, the printed rank, Luminate’s settled count (settled_units, read off the settled Kalshi market when the call is scored) and the confirmed release date. utils/called_shot.py:scorecard grades it (rank verdicts; each forecaster’s median absolute miss on units over the rows Billboard published a count for; who was nearer) and scorecard_card lays it out for chart_cards.render_table_portrait — a COLUMN card (release / HITS / Kalshi / Billboard), because the board card’s one credit column could not hold three figures. The eyebrow names the comparison (“Projection vs Billboard”) and the one-line hero the measure (“Projection accuracy”), there is no caption, and a TOTALS row at the foot carries each forecaster’s median miss over the drawn rows (owner layout, 2026-09-17). No rank column, and only releases BOTH forecasters called are drawn: Kalshi calls no position and lists the big releases only, so a rank column and a HITS-only row are not comparisons (owner: “remove the rank column because Kalshi doesn’t predict that”; a HITS-only Pooh Shiesty row came off). The rank record stays in Scorecard for the post and the telemetry. The caption is scorecard_caption: plain wire style, naming the board and the two forecasters, and it never quotes a miss figure (owner steer 2026-09-17: “plain wire style, but cut the last sentence. We don’t want to make HITS or Kalshi look too bad”) — the card carries the numbers. The lane that posts this card (#3387) uses that function, not a compose. Billboard is the final score (owner, 2026-09-17): the outcome columns are what Billboard PUBLISHED — the position, and the first-week count in its chart story (LedgerRow.billboard_units). That count is not the Kalshi settlement. Measured 2026-09-17 on the first ten calls, the figure Kalshi settled on (Luminate’s total, settled_units) ran up to 8% above Billboard’s printed count — Rod Wave 106,778 against 99,000, KATSEYE 179,122 against 170,000 — so grading against the settlement flattered both forecasters. The gap is Billboard’s own chart rules (owner steer 2026-09-17: “Billboard filters. They have their own filtering rules”): the chart admits only what its policy allows — no album sold in a merchandise or ticket bundle (since Oct 9, 2020), no physical-plus-download reported as a digital sale, a pricing floor on what counts as a sale, and streams weighted at Billboard’s ratio (1,000 paid / 2,500 ad-supported per unit from the chart dated Jan 17, 2026). The wider trade total is what HITS’ building chart carries (its Activity column matched Kalshi’s Shaboozey settlement to the unit, 31,618) and what Kalshi settles on, so that number normally runs above Billboard’s. A forecaster aiming at the trade total will read “high” against Billboard even when it hit the total; the card grades against Billboard because that is the score the owner and the room use. The card grades against Billboard’s figure only; a release Billboard published no count for (it states units for the top 10 and the notable debuts) grades on the rank axis and draws no row. Until the lane reads the chart stories itself (#3387), the figures reach the script through --billboard-units, a hand-verified “album|artist” → units map. scripts/called_shot_scorecard.py renders it off the live ledger (--debug-query; --billboard-units supplies the published counts; --fill-settled backfills the pre-column rows’ settlement from Kalshi’s settled ladders) and hands the PNG to the owner; nothing posts it yet. Measured on the first 13 scored calls (charts dated Aug 15 – Sep 12, 2026), against Billboard’s published counts: 9 exact, 3 close, 1 miss on rank; median units miss Kalshi 7.5% (n=9) against HITS 7.9% (n=10), HITS nearer on 5 of 9. HITS ran UNDER Billboard’s count on 7 of 10 (it forecasts what survives Billboard’s filter); Kalshi ran over on 6 of 9. Every HITS figure on the card is the MIDWEEK document’s Activity value (verified against HITS’ Sanity dataset to the unit); HITS’ Friday building chart lands much closer (Rod Wave 100,176 against Billboard’s 99,000), so the card grades the earlier, softer call — the one Toots posted. This board compares the two publishers ON PURPOSE and is the one place that may: the 2026-09-16 steer (#3377, “never compare kalshi and hitsdd numbers”) is about a TAKE setting one forecast against the other; this grades each against what Luminate measured, and no model composes over it.

A score is recorded whenever it resolves, but posted only while the chart is news (the music_reconcile_post_max_age_days knob on /tune, default 10 days after the chart date): a call re-scored weeks late (the two EP verdicts the strip_ep_marker fix re-opened, or a queue drained after an outage) lands in the ledger and the telemetry, emits music_desk_scored with reason=stale_score, and composes nothing. A late score also stores NO settled count: _lookup_settled’s artist fallback is right for a live release and wrong once that week’s market has left the sweep.

PREVIEW (mock):
  called charli at 74k. luminate settled 81.2k, billboard printed 76k.
  the filter took 5k. projection was light but the debut spot was right.

As built. The desk’s THIRD-priority lane, behind the chart and the call it scores. Ranked there on purpose: a call decays (its value is the lead over the print), a SCORE stays true, so it yields to the call but outranks the slower lanes.

Live dry run (2026-08-04) against the real Aug 1 Billboard 200, on the real resolved call from this document — HITS projected Gracie Abrams’ Daughter From Hell at #1, and rank_of found it at #1. Then the same release scored as a win, a near-miss and a blown call: 12/12 ship, 0.75–0.92.

“Daughter from Hell” debuted at #1 on the Billboard 200, we had Gracie Abrams at #12, so we were 11 spots too low.

The dry run caught a real inversion that unit tests could not. A smaller chart number is a BETTER position, so a #3 call printing at #1 beat the call. With the direction in a trailing parenthetical, the model wrote “two rungs short” — a flat inversion — in 3 of 5 samples, and the self-gate caught only two of them. The direction now leads the phrase and carries the number with it (“it charted 2 rungs HIGHER than we called, so the call was too low”), and the framing bans “short”/”shy”/”below” outright. After: 12/12 ship, zero inversions.

Lane E — Radio early warning (EDGE: leading indicator)

HITS’ Mediabase add_chart / building_chart update multiple times daily (4 minutes stale at probe). Radio adds today become airplay chart movement in ~2 weeks and Hot 100 movement after that. It is the only genuinely leading signal in the stack.

This lane is the most speculative of the five — treat it as staging-first until the correlation is checked against a few weeks of real outcomes. Ship it dark.


As built. The desk’s LAST-priority lane, on the owner’s #1982 steer (“billboard updates are more important than radio updates”). Radio is a leading indicator — a song on the way up, not a result — so it is the softest claim the desk makes.

Two feeds, two stories: adds (stations picking a song up this week) and breakouts (spins climbing inside a format chart). Everything is computed per format via by_genre; the feeds concatenate ten independent charts each ranked 1–50, and flattening them makes ten different songs read “#1”.

Format whitelist (owner steers, 2026-08-06 and 2026-08-07). The room covers rap, R&B and pop radio only. radio.COVERED_FORMATS is the gate moves() applies. It began as a whitelist of six — Urban, Urban AC, Rhythmic (rap/R&B) and Top 40, Hot AC, AC (pop) — and was narrowed to three on 2026-08-07 (“should we just cut the ac charts”): Urban, Rhythmic, Top 40. radio.CUT_FORMATS records both cuts: Country, Rock, Active Rock, Alternative and AAA (adult album alternative — indie/rock/folk) on the 6th, then AC, Hot AC and Urban AC on the 7th. The genre registry carries eleven format names; “Rock” holds no rows on the live feed today but sits beside “Active Rock”, so both are named.

The AC cut was measured before it shipped. Over the previous 30 days Hot AC posted 6 stories and Urban AC 5, of 33 with a format recorded, while plain AC posted none. The dropped stories were the shape the owner had already named as too obscure — the live 2026-08-07 candidate set lost Michelle Branch/New Radicals, Olivia Dean, Cameron Whitcomb and Eric Benet, the same shape as the Hot AC Noah Kahan card called “way too unknown”. AC is also the smallest chart on the feed by a wide margin: 11,444 weekly spins against Top 40’s 156,062. At the spin floor shipping that day the cut removed nothing from the candidate set; at the 200 floor in #2117 it took 14 candidates to 10.

The main-vs-secondary split is gone with it (owner call: cut dead code). The lane carried a second format set (MAIN_FORMATS), a second rank cap (MUSIC_DESK_RADIO_OTHER_MAX_RANK, 15) and a tier term in the lane’s sort. The AC cut emptied the secondary half, so all three were removed rather than left dormant. One format set, one rank cap, one sort that reads position then size of move.

What that means for whoever widens COVERED_FORMATS next: a re-added format gets the SAME rank cap as Top 40 and sorts by position beside it. Stricter treatment for a smaller format has to be a deliberate new decision, not something the code still does on its own. test_one_format_set_and_one_rank_cap is the record of that. A whitelist rather than a blacklist because of the fail direction: a format the source ADDS or RENAMES stays out until someone covers it deliberately, where a blacklist would auto-ship stories from an unreviewed format and a rename would bring a cut format back on its own (prefer absent over invented). The gate applies to the STORY pick only: standing() (the per-song context join) still reports an uncovered format, because a song the desk already covers keeps its full radio picture. The 2026-08-04 dry-run samples below predate the cut, which is why they include a Country add and an Alternative breakout.

The source labels its two spin columns the wrong way round. The document’s plays_last_week holds this week’s play count and plays_this_week holds the previous week’s. Reading the labels literally inverted every number this lane produced: it called TYGA’s “2POPS” a Rhythmic breakout that tripled to 69 spins, in the week the song fell from 69 spins to 23. The row’s own percent_change field settles it — on the live 474-row feed, 435 rows match only the swapped reading, 21 match both (the counts are equal), 18 are new entries with no prior week, and zero match the literal reading.

The symptom was visible before the cause: 33 of 34 “breakouts” had falling ranks. After the correction that number is zero — spins and rank move together, which is what a real feed looks like. hits.parse_radio owns the swap, checks every row against its own percent_change, and drops the spin counts (keeping the rank) on any row that disagrees, so a future change at the source makes the lane go quiet instead of making it lie.

Both thresholds are measured, not guessed — and both repeat a mistake this epic already made once:

At min_notability=40, 25 of 101 derived moves clear — a handful of genuine stories a week. Provisional: one week of data. (Refined 2026-08-06, owner steer: fewer unknown-artist posts, more volume on names the room knows. The lane now floors artists in THREE recognition tiers — watched (top ~100, floor 20), known (top ~1000 on kworb’s two ranking pages, the original floor 40), and unknown (absent from the pages entirely, floor 80: a monster move or nothing) — and the per-slot cap is 2. The middle tier exists because score alone cannot separate a known mid-tier name from an unknown: measured live that day, Latto scored 75 (kworb rank ~609 — posts) while Magnus Ferrell scored 78 (absent from the top 1000 — cut). The known list costs no extra fetches; it is the same two pages the watch build already reads.)

A breakout must also LAND high (rd.lands_high, same day, second steer). The owner saw the live cards and called the deep landings noise — BTS +78% landing #41 on Top 40 (“especially for non main radios nobody cares about these”), Noah Kahan landing #31 on Hot AC (“way too unknown” for the room’s taste even though he is watched-tier globally). A percentage climb is not the story; the position it buys is. So a breakout on a MAIN must land top-30 (MUSIC_DESK_RADIO_MAX_RANK). It was two caps — top-30 on a main format, top-15 on a secondary one — until the 2026-08-07 AC cut left no secondary format to apply the stricter cap to. Calibrated against three live owner judgments: the building chart’s biggest surges land in the 20s–30s (Katseye +110% at #27, Beyoncé at #25 — keep), Stella Lefty at #21 Top 40 was “not too bad” (keep), the BTS #41 and Noah Kahan Hot-AC #31 cards were the complaint (cut). ADDS are exempt: a format picking a song up is the debut story, and the add chart’s rank is an add-count ordering, not a chart position.

The lane’s ORDER: main formats first, then chart position (owner steers, 2026-08-07: “can we give the most popular radio format the priority”, then “maybe use position as weight”). moves() ordered the lane by raw notability. Notability is format-relative AND growth-relative by construction, so it answered neither question the room asks first. On the 2026-08-07 feed it led with a Top 40 song at #27 while a #1 Urban breakout sat sixth, and at the 200-spin floor an Urban AC breakout at #30 sat SECOND, ahead of every Urban and Rhythmic story.

Two plain sort terms now, no computed score:

  1. Chart POSITION (rd.position) — a #1 leads a #27.
  2. Then notability, as the tie-break.

COVERED_FORMATS is the measured top three, not an assumption. Total spins on each format’s own building chart, 2026-08-07:

format weekly spins   format weekly spins
Top 40 156,062   Hot AC 49,068
Urban 71,328   Urban AC 20,736
Rhythmic 63,598   AC 11,444

The three covered formats are the three largest by a wide margin, so no table of format sizes exists to maintain: the whitelist already encodes it.

Two heavier designs were cut on the way here, and both are worth recording. A format-size WEIGHTING scaled notability by the square root of each format’s spin ratio; the per-slot cap is 2, and the weighted and unweighted orders picked the SAME top two at both spin floors, so the calibrated table bought nothing that posts. A format TIER then led the big three ahead of the rest; the AC cut emptied the other tier the same day, so the term could never fire again. The reasoning is kept; the constants are not.

Adds have no chart position. The add chart’s rank is an add-count ordering, not a chart position — the same reason lands_high exempts adds from the rank gate. Sorting the two together would rank a most-added song above a song actually sitting at #2, so position() returns a sentinel for an add and it sorts after the positioned rows in its tier. Prefer absent over invented, applied to ordering. (Adds are rare in practice: all 34 radio posts in the six days to 2026-08-07 were breakouts.)

Two effects, and only two. It decides which stories fill the per-slot cap, and it decides which format a song charting on several is told through — Beyoncé’s 2026-08-07 breakout reads as Urban #25, because Urban AC #9 is a better position on a secondary format and Rhythmic #28 is a worse position on a main one. It does not change what qualifies: every notability floor (_RADIO_*_MIN_NOTABILITY) and the min_notability filter read raw notability, so the ordering can reshuffle the queue but never silence anything. A format not on COVERED_FORMATS produces no move at all, so a rename at the source or a brand-new format chart still cannot reach the lane.

One post per song per chart week (same day, third steer: “i’ve seen 3 debut posts for donk beyonce”). The dedup key was FORMAT-scoped (“the same song on two charts is two facts”), so DONK’s arrival posted from Urban, Rhythmic AND Urban AC in one week. The key is now SONG-scoped (radio:<kind>:<artist-track>): the pick keeps the first format in the lane’s order, which is the covered format the song charts HIGHEST on, and folds the rest, in-slot and across the ~6-day window. KIND stays in the key on purpose — an add and a breakout for the same song are two facts, so a song can still get one of each in a week. A folded song gets one compose attempt per slot; a self-gate miss retries next slot. The old format-scoped keys — including the just-cut Country/rock formats’ — are still checked against history so a pre-change post cannot repeat while its entry ages out. The week backtest also exposed the “YE”/”Kanye West” alias gap costing a real story (his #1 Urban breakout read as an unknown artist), so artist_watch._ALIASES now bridges feed credits to kworb names — seeded by measured cost, one line per case (#2101).

Its own rollout stage (music_radio, default STAGING). It first shared music_called_shot because both lanes read HITS — which was wrong: that lane is in PRODUCTION, so a shared stage would have sent the desk’s newest and softest claim to the room and to X with no audition. A leading indicator read off a third-party feed, whose spin columns arrive labelled the wrong way round, is exactly the surface that earns a staging period.

Live dry run (2026-08-04, real Mediabase feeds + real compose + the 0.6 gate, after the column fix): 7/7 ship at 0.91–0.94, every line naming its format, crediting Mediabase inside the sentence, and leading with the song rather than the source.

"Window" by Foo Fighters jumped from #63 to #26 on Alternative radio this week, spins
surging 11 to 72 by Mediabase's count.

"Morning Dew (DONK)" by Beyoncé jumped from #47 to #26 on the Urban chart, with spins
surging from 46 to 247 this week, by Mediabase's count.

"Anytime Soon" lands as Country radio's most-added this week, 70 stations in by
Mediabase's count.

3. When we fire: thresholds

Movement.notability collapses rank × kind × move-size into one number so a lane thresholds on a single value. Measured on the real Hot 100 week of 2026-08-01 (reproduce with python -m scripts.dryrun_billboard):

Floor Alerts that week Use
0 45 the weekly report (Lane A) — everything that moved
5 6 Lane C default — a few real stories
10 4 tighter
20 2 only the big ones

Calibration points: a #1 debut scores 200, a top-10 debut ~22, a new peak at #3 42, a 25-spot climb to #17 13, a new peak at #32 4.

Always fires regardless of score: a #1 change, any top-10 debut. Never fires: a new peak outside the top 40 (true of nearly every climbing song), a sub-10-spot move, anything already posted this week (the existing post_dedup_history spine), and any week whose rows are flagged stale (no degraded source exists today; the guard stays).

“An artist’s first career #1” — a chart ROW cannot detect it, but the artist chart-history page can (#2587). A single chart row carries position, movement, peak and weeks, never career history, so asserting a first-#1 from a row is guessing — the compose fence still bans it (§2 status notes). What made it safe is a real SOURCE: the per-artist chart-history page publishes each entry’s PEAK, so music_news.peak_history_fact reads the whole catalogue and states “their first #1” (current position 1, no prior entry peaked #1) or “their highest-charting entry yet” (current position beats every prior peak) — deterministic, keyed on the CURRENT week’s position so it is fresh, suppressed on a missing peak or a same-titled re-release. Dry run (real page + real compose, both shipped models, n=5 each per claim): 20/20 stated the fact cleanly, no fabrication.

The career-ENTRY ordinal now HAS a source (#2154 follow-up). The newsroom shipped “JUNGLE’s first entry on the Billboard 200” on 2026-08-25 — their FOURTH (the screenshot showed four JUNGLE titles). The take invented the ordinal, and the career_ordinal gate could only DROP the post, not correct it. Billboard’s OWN per-artist chart-history page (/artist/<slug>/chart-history/<code>/) lists every title an act has placed on one chart, server-rendered like the weekly charts, so utils.billboard.artist_entries reads it and COUNTS the entries — EXACT, deterministic, no web judge (the same shape as milestone_qualifier.nth_release_qualifier). The newsroom hands that count in as one grounded fact (music_news.entry_ordinal_fact), so the take states “their fourth Billboard 200 entry” accurately AND the count grounds the ordinal gate, turning a drop into a correct ship. Dry run (real chart-history read + real compose, both shipped models, n=5 each): 10/10 stated “fourth”, zero drops. It is scoped to the entry COUNT on the two flagship charts (billboard200tlp, hot100hsi). The sibling PEAK claim (“first #1”, “highest-charting yet”) rides the same read (#2587, above) and takes precedence — one career clause ships, the stronger claim wins. Fail-open: a blocked / unknown-slug / not-yet-listed read yields no fact and the take ships on the bare chart fact, still accurate.


3b. Reconciling with the surfaces we already have

Billboard/HITS is the fourth music-numbers source in a room that already had three, so the real risk isn’t a missing feature — it’s the same fact posted twice from different sources. Mapped out explicitly:

Surface What it says about a chart move Collides?
music_news (newsroom) RELAYS the move off @billboardcharts / @chartdata Yes — directly
music_desk reveal/tracking Luminate UNITS behind the release (Kalshi-settled) Adjacent, same subject
music_desk projection Kalshi-IMPLIED first-week number, mid-week Overlaps HITS (Lane B)
market_alert / betting Price moves on a market the chart settles Adjacent, same subject

The one live collision, now fixed. The newsroom posts “X debuts at #1 on the Hot 100” relayed from the wire; the chart lane posts the same move off the first-party feed. Before this, WIRE_DEDUP_SURFACES covered the wire desks but not music_desk — the desk only ever posted market numbers, which rarely collided with a headline — and the desk read only its own history. So the two surfaces could restate each other with neither able to see it. The chart lane is what made that live, so music_desk now both appears in the set (the newsroom sees the desk’s posts) and reads it (the desk sees the newsroom’s). Both directions are needed; either alone leaves one surface blind. The Haiku topic-dedup is what actually catches it, since a relay and a first-party read of one move are worded differently by construction.

The adjacent-but-distinct ones are fine, and shouldn’t be merged. A Luminate units reveal (“140.5K first-week units”) and a chart position (“#1 on the Billboard 200”) are genuinely different facts about the same release — Billboard’s published number is filtered down from Luminate’s, which is the whole reason the desk can report a figure nobody else has. Suppressing one for the other would lose information. The 36h topic window plus the judge’s “genuine development vs restatement” call is the right tool here, not a hard rule.

Where the overlap was real — projections. DECIDED, then REVERSED: both post, each named. The desk’s projection lane posts a Kalshi-implied first-week number; the called-shot lane posts the HITS one for the same release in the same week.

The first decision was that HITS wins and Kalshi yields, on the reasoning that two estimates of one quantity read as the desk repeating itself. The owner reversed it, and was right to: they are not the same claim. HITS is the INDUSTRY’s own tracking estimate. Kalshi is what the CROWD will bet the number turns out to be. Said with the source named, the pair IS the story — the same “industry bet against crowd bet” framing the final-chart lane already uses.

It also deletes machinery. The suppression needed a coverage set and a “callable” rule shared with the posting lane, and the two drifted apart in production: the silencer read every projection row while the poster used only top-10 debuts, so 50 albums were silenced where exactly 1 was postable. There is nothing left to keep in step.

The richer “both numbers in one sentence” shape (“HITS says 74K, the market’s at 68K”) is Lane D, and it stays Lane D. It needs a matcher from a HITS album to a Kalshi event plus its own honesty rules about disagreeing sources, which is a bigger change than choosing which of two lanes speaks.

The newsroom’s chart gate is the other open piece — and the biggest. cogs/music_news.py:_live_chart_position verifies a wire chart claim against live rows, but charts_named() returns () for a Billboard claim (its comment says so outright: “A chart we can’t read (Billboard/ARIA) → web verify, as before”), so those fall through to a web search. That was correct when we had no Billboard feed. Now we do, and it is the cheapest remaining win in the whole epic: 30 of 118 claim lookups unresolved over 30 days, largely because Billboard claims were being checked against Spotify rows. Wiring bot.billboard into that gate is deliberately NOT in this PR — it changes how wire claims get verified and dropped, which is its own behavioural change with its own risk surface, and it deserves its own diff and dry run rather than riding along on a lane.

The wires name artists by @handle, and we now read the handle (#2010). @USRadioUpdater writes .@whamcbfw4's "Dead Fresh" enters the Top 20 on today's US Rhythmic Radio (Mediabase) rolling chart. The artist appears only as an X handle. The classifier had no way to know that @whamcbfw4 is Lil Baby, so the handle reached the card as the artist name. The bot posted a chart card titled “Dead Fresh - whamcbfw4”. Two of the five posts this account has ever shipped named the artist wrong the same way: the other read “IS IT LOVE - Tylla”, off @Tyllaaaaaaa, whose name is Tyla.

The tweet payload already carried the answer. X returns entities.user_mentions: [{"screen_name": "whamcbfw4", "name": "Lil Baby"}], and tweet_to_post discarded it. The provider now keeps that map (source_providers.MENTIONS_KEY) and the cog hands it to classify_music_news.

The map is EVIDENCE, not a substitution — and the live data is why. The first design replaced each @handle in the wire text with the account’s display name. A sweep of the 41 accounts the trusted music wires actually mention killed it. A display name is right often enough to matter (@whamcbfw4 = “Lil Baby”, @Tyllaaaaaaa = “Tyla”, @ellalangleymsic = “Ella Langley”) and wrong often enough to break cases that work today (@Drake = “Drizzy”, @tylerthecreator = “T”, @Latto = “BIG MAMA”, @MacMiller = “Mac”). Neither form is reliably the artist’s catalog name, but together they identify the account. So the classifier gets both and is told to trust neither alone. A dry run against the six real wires confirms it: the two broken cases now read “Dead Fresh - Lil Baby” and “IS IT LOVE - Tyla”, and the four working cases are unchanged.

The telemetry is the positive signal (music_news_recover, via=mention_name), because the negative one cannot be written honestly: for many acts the handle IS the name (@Madonna, @shakira), so “the artist looks like a handle” is usually correct and must never gate a post. Watch the rate instead. If it falls to zero while usradioupdater keeps posting, the mention map stopped arriving.

The glossary block is the whole fix, and the scar was measured out. A first pass also wrote a handle rule into the always-on subject rule: “these wires name the artist by @handle, NEVER write a bare @handle, and if you cannot tell who a handle is leave the artist out rather than guessing or copying the handle”. An A/B on the 7 live wires (n=3 per arm) scored the glossary ALONE identically on all 7, so that addition bought nothing and was removed. Its final sentence was worse than dead weight. The one case it was written for is a handle with no glossary entry, and there the model copied the handle anyway, 3/3, with the sentence present. A prompt line that promises behaviour the model does not deliver reads as a guardrail while protecting nothing. Per docs/PROMPT_OPTIMIZATION.md rule 6, a rule that does not measurably change the output is context cost and goes.

arm lil baby tyla drake latto
production (no glossary) Dead Fresh - WHAM IS IT LOVE - Tyllaaaaaaa - Drake GOMF - Latto ft. Glo
glossary + the extra rule Dead Fresh - Lil Baby IS IT LOVE - Tyla - Drake GOMF - Latto & GloRilla
glossary alone (shipped) Dead Fresh - Lil Baby IS IT LOVE - Tyla - Drake GOMF - Latto & GloRilla

4. Where this rides

Nothing here needs new infrastructure. Per CLAUDE.md’s cross-cutting spines:


5. Status

Piece State
Billboard chart read + parse + guards + loud blocked telemetry built (utils/billboard.py)
movements / diff_weeks / notability derivations built, calibrated on a real week
Dry run over every chart, and over the real lane compose built (scripts/dryrun_billboard.py, --compose)
Lanes A + C as a music-desk lane, with cards builtcogs/music_desk.py, the desk’s first-priority lane
Chart standing in the prediction/market context builtbillboard.chart_context → the desk’s compose
Being first (two-speed cache) builtexpected_chart_date + a ~20min re-read while a flip is due, + the read-time superseded check that makes it actually work
HITS Sanity client (Lane B/E source) builtutils/hits.py; robots question resolved, see BILLBOARD_SOURCES.md §5b
Lane B (called shot), with cards builtcogs/music_desk.py, the desk’s second-priority lane, own music_called_shot stage (ships STAGING)
Being first on the projection (publish-window cache) builthits.expected_tracking_end + superseded, the utils.billboard pattern
Lane D (reconciliation), with cards partial — the called_shots ledger + utils/called_shot.py score our call’s POSITION against the printed chart. The third number (settled Luminate units, via Kalshi) is NOT wired in yet, so the units chain the lane exists for – projected vs settled vs charted – is still missing
Lane E (radio), with cards builtutils/radio.py + the desk’s LAST-priority lane; Mediabase adds + spin breakouts, scored per FORMAT
Newsroom chart-gate reads Billboard for Billboard claims builtcharts_named resolves the two Billboard charts; live=False keeps them out of the staleness gate

How the built lanes ride the desk

Owner steer: “this should ride music desk, and have its dedicated prioritized lane.” Same call as the streaming-milestone lane (#1495, “it should be on music desk not its own”), so it is a lane in cogs/music_desk.py, not a cog.

Priority is positional. _sched_compose_units returns an ordered list the base delivers in order, and the chart lane is built first. On a week where the chart moved, the chart leads the slot; on a quiet week the lane returns [] and the desk behaves exactly as it did before. No new scheduler, no new kill switch, no new dedup table — it reuses music_desk_events under a bb:<chart>:<week>:<title> key.

Being first is a cache property, not a scheduler one. expected_chart_date knows which Saturday-dated chart should be current (verified against the live flip). Two things use it, and both are needed:

The first slot after a flip already has the new week, most of the week is one read per chart, and the extra reads are bounded to the flip window.

Why the read check exists, and why the TTL alone was not enough. DurableCache’s L1 tier is a plain LRU with no expiry: utils/lru.py stores no timestamps, and get() returns an L1 hit without checking any deadline. A short TTL therefore only shortens the life of the durable row — the in-process copy is served until the bot restarts. Shipped that way, the whole mechanism above was decorative: the first read of the day pinned last week’s chart in memory and the lane would never have seen the flip. Fixed by checking the chart DATE at read time, which is also the stronger question — it asks whether what we hold is the current week, not how long ago we asked. Regression test: test_a_superseded_chart_is_refetched_not_served_from_cache. The same L1 hole affects every other DurableCache consumer (see the tracking issue); this fix is Billboard-local on purpose, because changing the shared cache changes upstream call volume on metered APIs.

Same voice, same floor. The lane runs the identical compose_market_drop + music_desk_score path as every other desk story and the same 0.6 ship floor — it gets priority, not a lower bar. Live dry run: a #1 debut scored 0.95 and a new peak 0.92.

One fence the dry run forced. Given a #1 debut the model wrote “Gracie Abrams’ first chart-topper” — plausible, ungrounded, and not a number, so the number-only fence missed it. A chart row carries position, movement, peak and weeks and nothing else, so any career or record claim is invented by construction. The framing now forbids them explicitly: 0/6 samples carried one afterward, against a hit on the very first sample before.

What the HITS client makes possible now

projected_chart_date() is the piece that turns a projection into a scoreable call: a doc’s date is the tracking-week end (always a Thursday) and the Billboard chart it feeds is dated 9 days later. That rule was verified against a known outcome — the doc dated 2026-07-23 projected Gracie Abrams at #1 as a NEW entry, and the Billboard 200 dated 2026-08-01 printed exactly that — so “were we right” is now billboard.chart(week=proj.chart_date), a lookup rather than a judgement. It is also what makes Lane D (reconciliation) mechanical rather than manual.

Verified live at build time: the current building chart calls the chart dated 2026-08-08, billboard.chart("billboard200", week=2026-08-08) returns nothing (unpublished — so the call is live), and the top projected debut is CHARLI XCX | MUSIC, FASHION, FILM at #3, ~74.4K units (52K pure).

Two properties of the rendered block are deliberate and belong to the block rather than to a prompt, because a caller might not pass one: the “PROJECTED, not final” framing (a projection stated as a fact is the one way this lane actively misleads) and the HITS credit (an uncredited number is the one way it’s unfair to them).

Order of remaining work: the newsroom gate fix, then Lane B — the called shot, still the highest-value lane and now fully sourced.


6. The top-artist watch (owner steer, 2026-08-04)

The owner steer: “track the top artists: pull every source we have for them and push any update not already published, across billboard, hitsdd predictions, radio etc.” The trigger was measured, not felt: the milestone lane’s first 30 days produced TWO posts, both mid-tier names, zero superstars. Three structural causes (Axiom, 2026-08-04):

  1. The milestone detection pool was Kalshi’s market subjects. A superstar’s CATALOG is never a subject, so “X crossed 3 billion streams” could not fire for the biggest names at all.
  2. The top rungs are far apart. A 10B career step takes months; a riser crosses 100M → 250M → 500M in weeks. Early output is small artists by construction.
  3. The chart/radio notability floors are size-blind. A watched-tier artist’s real move below the general floor was dropped like anyone else’s.

The watch is not a new lane. utils/artist_watch.py builds the list (top MUSIC_DESK_WATCH_ARTISTS, default 100, by kworb DAILY streams, unioned with the top monthly-listener names — both free cached pages), and the desk:

Credit matching is segment-scoped, not containment. “Drake” must hit “Drake Featuring Yebba” and never “Drake Milligan” — artist_watch.fold_tokens normalizes every collab connector to one token and matches watched names only on segment boundaries. kworb._fold containment would misattribute on exactly the names the watch exists for.

Telemetry: one artist_watch event per sweep (count = crossings composed, detail{checked, tracks_windowed}); everything else rides the existing lane events.

A milestone break carries the artist’s cross-source standing (owner ask on the PR: “should we add anything from hitsdd, billboard here?”). After a crossing is detected — never per rung check — the compose context gains one labeled block (artist_watch.standing_block): their Billboard entries this week (count + best row), a live HITS projection (always marked PROJECTED), their radio picture per format, and their kworb Spotify ranks. Every source is fail-open and the block is “” when nothing carries them. The desk’s _CONTEXT already instructs the model to spend at most one clause on context, so the block adds facts, not instructions. Live dry run (2026-08-04, real chart + kworb data, simulated Taylor Swift 130B career crossing): 3/3 ship at 0.85–0.92, each take grounding its one clause in the block (“pulling 41.6M a day”, “holding the #2 spot globally”) instead of padding the bare number.

Watch v2 (owner steer on the PR: “every music source we have should be checked on these sweeps”). Two more halves, same discipline:

Coverage check (owner, on the v2 PR: “exits, entries, jumps – make sure they’re all covered”). The audit found and closed two jump holes; the full matrix for a watched artist:

Story shape Billboard Platform charts (kworb) Radio (Mediabase)
Entry (debut / re-entry) movements badges, watch floor 2 days==1 / NEW flag adds lane
Exit _exit_units two-page diff — (no prior-period page)
Jump watch pass at min_jump 5 (general lane keeps 10) ≥5 into top 10, OR a ≥15-spot surge into the top 50 breakouts, watch floor 20
New #1 movements (a #1 move always clears the floor) crown (a climb into #1) Lane E per format

The two closed holes: movements dropped a sub-10-spot jump/fall BEFORE any floor ran, so a watched artist’s #15 -> #8 climb was invisible even at watch floor 2 (the watched derivation now runs at min_jump 5, merged after the general pass so general behavior is byte-identical); and the platform jump rule (≥5 into the top 10) missed a 30-spot leap to #12 (the surge band, ≥15 into the top 50, catches it).

The watched-artist move is a BOARD now, not N cards (owner steer, 2026-08-11: “artist watch moves should just be a board for the artist and where all their singles are”). The platform-chart sweep cards ONE move at a time, so an act moving on several rows of one chart got several near-identical cards. Measured over 14 days of live posts: Ariana Grande 8 posts across 4 days, Pooh Shiesty 5 across 2 — two of those in the SAME MINUTE, for #8 and #9 on one chart. So an act with _MIN_WATCH_BOARD_MOVES (2) or more moves on ONE chart becomes a board (chart_boards.watch_artist_board, story watch_board): every one of their titles on that chart, where each sits, and the day’s move beside it. The folded stories leave the card path (music_desk._watch_boards), so the board REPLACES the cards rather than adding to them. One chart per board on purpose (owner call) — an Apple #8 and a Spotify #33 are not comparable positions, and a board mixing them would invite exactly that comparison. The board lists the act’s WHOLE presence on the chart, not only the rows that moved, because that is what its headline counts. It states no PERIOD for the move column: the rank-only charts’ move period is not established (#2019). Live dry run (2026-08-11, real Apple Songs page + real compose + the 0.6 gate): Morgan Wallen (13 songs) and Drake (13 songs) each composed 2/2 at 0.92–0.95, position-led, no invented period.

The watch board’s dedup key is the SET OF MOVING SONGS, not a title count (#2321 review). The first version keyed wb:<chart>:<artist>:<count> on the act’s total title count. A count stays put while WHICH songs move changes, so a fresh set of moves matched the prior board, the board was dropped as already-seen, and _watch_boards still folded its cards out — neither the board nor the cards posted, a silent blackout. The key is now the sorted set of moving song_keys (_watch_board_dedup_key), which follows the individual cards’ own per-song grain, so a new cast of movers is a new board while a stable standing does not repost on daily position wiggle.

A fresh board folds its cards only when it SHIPS (#2321 review). _watch_boards returns the fresh boards to compose plus the card keys of boards it found ALREADY SEEN. The caller folds a seen board’s cards straight away — that exact set of movers already posted inside the window, so re-carding them is the repeat the fold removes — but folds a fresh board’s cards only after _compose_board_unit returns a unit. A board that fails to compose (self-gate miss, error) therefore leaves its moves in picks for the card path, instead of suppressing both forms. The board and the card loops share bounded attempt budgets (MUSIC_DESK_WATCH_BOARD_ATTEMPTS, RADIO_ATTEMPTS, ALBUM_DROP_ATTEMPTS), so a slot whose boards all fail the gate cannot run one compose + score per group.

A shipped board stamps the card keys it replaced (#2321 review). The watch and radio boards pass their per-move card dedup keys to _compose_board_unit as extra_dedup_keys; _record_ship stamps them alongside the board’s own key on a successful post. Without this, a group that drops from two movers to one on a later fetch no longer forms a board, so the surviving mover — already shown on the board — would re-card inside the six-day window. The board also collapses mixes of one release by song_key before it counts, so two edits of one song are one row, not two.

The board grouping reads the WHOLE per-chart presence, and folds a real collaboration under its lead (#2322/#2323). Two follow-ups to the board fold:

The album fold defers to board-ship, like the watch and radio lanes (#2323). The album-drop board composes BEFORE the move lane now (_album_drop_units runs first and returns the debut row_keys it folded), so the move lane drops only the debuts whose board shipped or deferred; a board that fails the gate leaves its debuts to card. And a board is skipped when a NON-LEAD debut already carded (its bb: key is in seen) — the deploy-transition case where the cards shipped before the fold existed, so the board would otherwise repeat them (_cluster_already_carded; the lead is excluded because it cards on every normal week).

The same board treatment reaches RADIO (owner steer, 2026-08-11: “do the same for … all radio”). The Mediabase lane (Lane E) is separate from the platform-chart sweep, so it kept carding one move at a time. On the live feed that meant one song shipping twice — Beyoncé’s “Morning Dew” is an Urban breakout AND a Rhythmic add in one sweep, so it was two cards. The per-song-per-KIND dedup keeps a breakout and an add apart on purpose (“two facts”, #2101), so the repeat was by design. The board reconciles both steers: it keeps BOTH facts (each move is its own row) while drawing them on ONE card. An act with _MIN_RADIO_BOARD_MOVES (2) or more radio moves this slot — a song’s breakout + add, or two different songs — becomes a board (radio.artist_board / board_groups, story radio_board); the folded moves leave the card path. The dedup key is the set of moves (song + format + kind), not a count — the same collision the watch board’s key hit. Headline shapes by the cluster: one song on one format (“gaining on Top 40 with X”), one song across formats (“working X on 2 formats: Rhythmic, Urban”), or several songs (“N songs moving on radio”). Live dry run (2026-08-11, real Mediabase feeds): Beyoncé, Ariana Grande and Cardi B each folded a breakout+add pair into one board. Same stage (music_radio) and window as the cards, so a staged radio audition covers the board too.

Apple / Spotify / iTunes are already covered by the watch board above — the platform-chart sweep reads all of them (_WATCH_CHARTS), so a watched act’s cluster there folds without new code. The radio board closes the last surface the fold did not reach.

Two rules hold for all three fold boards (album-drop, watch, radio), from the #2321 review. First, the OFF switch keeps the cards: setting the board cap to 0 (MUSIC_DESK_MAX_ALBUM_DROPS / MAX_WATCH_BOARDS / MAX_RADIO_BOARDS) skips the fold, so the individual cards stay — a fold that ran with no board to replace them would post nothing. Second, a board that draws more than 10 rows says so: the card renderer caps at 10, so a headline naming a larger count (Future’s 16 debuts, Drake’s 13 Apple titles) adds “top 10 shown on the card”, the same disclosure the lookup boards make.

Deliberately not built: HITS holdover/climber projections for watched artists (a new claim type with its own honesty rules), and watch-widening the music_alert kworb crown/jump lanes (a different cog’s mechanics; the desk’s platform-chart sweep now covers the watched-artist half of that gap). Both remain follow-ups on the tracking issue.