a discord bot for the tootsies server. ask, recap, discuss, ship features by typing.
The plan for the alert lanes on top of utils/billboard.py + the HITS forecast feed.
Two jobs, deliberately separated:
Every timing claim below is measured, not assumed; the probes are in
docs/BILLBOARD_SOURCES.md.
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.
billboard.chart() returns a week_of newer than the last one posted.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.
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.
hits-top-50 or midweek-20 doc, with a NEW entry in the top 5
(or a projected #1 change).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.
Callable = a projected DEBUT (last_week is None) inside the top 40. A climber
is real data but not a called shot; the claim we can be publicly right about is “this
lands at #N”.
The depth was 10, set on an assertion with no data behind it, and it cost a real call: The Strokes were projected #15 so the lane could not call them, and they printed at #13. The measurement it should have had — HITS’ 50 projected rows for the chart dated 2026-08-08, checked against that chart once it printed:
| projected band | n | mean error | within 5 |
|---|---|---|---|
| 1–10 | 10 | 0.2 | 10/10 |
| 11–20 | 9 | 1.1 | 9/9 |
| 21–30 | 9 | 0.4 | 9/9 |
| 31–40 | 10 | 0.8 | 10/10 |
| 41–50 | 9 | 1.9 | 9/9 |
HITS is as accurate at #40 as at #5 — the error does not grow with depth until the last band. The old cutoff was not protecting the lane from unreliable projections; it was discarding callable stories. 40 rather than 50 because 41–50 is where the error finally doubles and where the never-charted rows cluster. One chart week of data.
The watch band (#2007, a top artist’s projected debut) moves from 40 to 50 in the same change. It was set against a general band of 10 on a NEWSWORTHINESS argument — a top artist’s deep debut is a story where an unknown’s isn’t — and a general band of 40 would have left it inert. 41–50 is where the accuracy argument stops covering everyone, so that is where the newsworthiness one now earns its keep.
music_called_shot, default STAGING). It is the one desk
lane that makes a falsifiable public claim, so a mod auditions the calls in #bot-logs
before they reach the room without muting the rest of the desk. OFF skips the lane
entirely — no HITS read, no compose.projected=True so it wears the forecast treatment, credited to HITS, no market link.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:
_CONTEXT already
bans for Kalshi;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.
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.
movements(week, min_notability=...) on rows outside the top 10.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.
We hold three numbers for the same release that rarely agree:
utils/luminate.py, already integrated)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.
called_shots records what we actually
POSTED — the rank and units that went out in the post — written at ship, after the
self-gate. Re-reading HITS later would score HITS’ latest numbers instead, which is a
different and much more flattering claim, and HITS rolls its docs over weekly, so the
number we posted would simply be gone.rank_of requires title AND artist to match, so a same-titled release by another
act is never scored as ours.exact, close (within 2 rungs, either way), miss, uncharted. Not
charting at all is reported, not dropped — silently discarding the worst results is
precisely the self-flattery that makes a scoring lane worthless.music_called_shot stage as the call, so a room that only auditioned the
prediction is never handed the scorecard for a post it never saw.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.
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.
plays_this_week jump for a song not yet in the
Hot 100 top 40.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:
MIN_SPINS floor drops
133 of 474 rows.A noise floor is not an interest floor. MIN_SPINS shipped at 50, which kept the tail
out but left the lane publishing songs on 52, 55 and 57 spins — true, and dull. Because
the score is a percentage, 21 spins becoming 52 reads as +148% and outranks a song adding
700 real plays. Raised to 500 on 2026-08-07 (owner: “lets cut the radio posts in
2/3rds”). Measured against the live building chart (485 rows) at min_notability=20:
MIN_SPINS |
50 | 100 | 200 | 300 | 350 | 400 | 500 | 750 |
|---|---|---|---|---|---|---|---|---|
| candidates | 43 | 36 | 26 | 24 | 19 | 18 | 15 | 8 |
| cut | — | 16% | 40% | 44% | 56% | 58% | 65% | 81% |
500 is the two-thirds cut, and the lowest songs it keeps sit on 508–592 spins. Env-tunable
via MUSIC_RADIO_MIN_SPINS. standing() deliberately ignores the floor: a song’s radio
picture is CONTEXT about that song, not a story pick.
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:
rd.position) — a #1 leads a #27.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.
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 (billboard200→tlp, hot100→hsi). 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.
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 |
Nothing here needs new infrastructure. Per CLAUDE.md’s cross-cutting spines:
ScheduledPoster (master switch, mood, /menu calendar, slot
pacing, dedup, 429 retry) — not a new scheduler.post_dedup_history table — no new *_history table.experiments.py registry (staging = #bot-logs
audition) because they make falsifiable public claims. Lane A/C can go to production.min_notability, the adds threshold) are Tunables in
utils/tunables.py, so they’re tuned from /menu rather than by redeploy.| 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 | built — cogs/music_desk.py, the desk’s first-priority lane |
| Chart standing in the prediction/market context | built — billboard.chart_context → the desk’s compose |
| Being first (two-speed cache) | built — expected_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) | built — utils/hits.py; robots question resolved, see BILLBOARD_SOURCES.md §5b |
| Lane B (called shot), with cards | built — cogs/music_desk.py, the desk’s second-priority lane, own music_called_shot stage (ships STAGING) |
| Being first on the projection (publish-window cache) | built — hits.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 | built — utils/radio.py + the desk’s LAST-priority lane; Mediabase adds + spin breakouts, scored per FORMAT |
| Newsroom chart-gate reads Billboard for Billboard claims | built — charts_named resolves the two Billboard charts; live=False keeps them out of the staleness gate |
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:
superseded) — a cached chart is served only while it is the
current week. Once a flip is due, the copy we hold is out of date, so the client goes
and looks.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.
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.
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):
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:
_known_artists): the same two kworb
pages as the watch read to the top ~1000, UNIONED with the artists on the owner-kept
GENRE charts (rap / R&B-hip-hop / Latin, songs + albums — bb.KNOWN_GENRE_CHARTS,
this chart week and last). The union is the owner’s genre carve-out: a pop/urban name
can be known without being global-top-1000 (Larry June ranks past kworb’s pages yet
sat #5 on Top Rap Albums), while rock and country names cannot enter — their charts
are not in the tuple. Zero extra integrations (kworb pages already cached for the
watch; genre pages ride the durably-cached Billboard client), fail-open to “everyone
is known” when the kworb base misses (the genre extension deliberately does NOT run
on an empty base). Each lane floors in three tiers:
_EXIT_WATCHED_MAX_PEAK) – a deep catalog holdover ageing off is not
news even for a name the room knows (Aventura off the 200 at a #156 peak after 41
weeks was the live miss that added this floor).
Same compose paths, same cards, same dedup keys, same stages — a watched story can
never double-post against its general-lane twin._watch_milestone_units): career streams + monthly listeners off the ranking pages
(free), plus cumulative track totals for their songs on the Spotify Global Daily
chart (a bounded, hour-rotated window of MUSIC_DESK_WATCH_TRACK_FETCHES track-page
reads per slot, default 8). Same entity keys + songstats_milestone_state rows as
the Songstats path — one shared high-water rung per entity, so the two detectors
cannot double-fire. This sweep is the CAREER-streams authority and the Songstats
path now defers to it (_kworb_career_total, 2026-09-14). The two sources agree on
listeners (0.00% over 8 acts) and on track totals (~0.1%), and NOT on an artist’s
career total: measured over 14 top acts the gap ran 0.0%–4.3%, Songstats always
higher, with 2 of the 14 in the band where Songstats clears a rung the board does
not. A shared rung fed by both therefore broke a rung kworb did not support — the
Weeknd “100 billion career Spotify streams” card of 2026-09-14, whose own standing
strip read #4 with 97.9B. The Songstats artist total also restates downward, which a
high-water rung cannot represent. kworb’s total is a published sum of the artist’s
tracked songs, matches its per-artist songs page to the digit, and its 50B floor
lands near rank 18 of 1000, so reading it costs no coverage.
Seed-on-first-sight holds: the first sweep seeds every baseline silently.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:
Chart EXITS. movements reads one page, so a release that fell OFF the chart
was invisible — and “gone after one week” is a real story (the owner’s example: two
rap albums off the Billboard 200 after a single week). _exit_units diffs this
week’s page against LAST week’s dated page (bb.diff_weeks, still no stored
history) and posts an exit when it is a story on its own (a run of ≤ 2 weeks that
peaked top-40, by a RECOGNIZED artist — the 2026-08-06 unknown-artist cut) or the
artist is watched AND the release peaked top-40 (_EXIT_WATCHED_MAX_PEAK, added after
Aventura’s #156 / 41-week catalog roll-off posted only because the watched branch
carried no peak floor). The recognition gate is what forced two normalization fixes in
fold_tokens (“$” reads as “s”, “the” drops): on the live 2026-08-08 diff the gate
first cut “Rick Ro$$” (kworb: “Rick Ross”) and “Rolling Stones” (kworb: “The Rolling
Stones”) — the owner’s own example story, silenced by styling. Per-name aliases that
no normalization covers (“YE”/”Kanye West”) are #2101. Sorted shortest-run-first — measured on the
real 2026-08-08 diff, peak-first ranked a 291-week catalog roll-off above a one-week
top-15 flop. Own bbx: dedup namespace, 1/slot, OUT-figure card. Live dry run: the
real 50 Cent catalog exit and the real Motionless in White one-week flop both
composed 0.90–0.95, every position stated as last week’s.
The OUT hero is dialed down (owner steer 2026-08-26). The figure is the WORD “OUT”,
not a number, so at the stat-hero size the fit loop never shrank it and it filled the
frame — the Justin Bieber “Believe” exit off the 200 (a 77-week, #1-peak run aging off)
read as breaking news. exit_card_fields now stamps figure_max (_EXIT_FIGURE_MAX_PX,
112), which render_number_card takes as the fit-loop START size (new figure_max_size
param, threaded through build_number_card). The word lands as a firm label under the
art, not a monolith. Scoped to the exit only: a numeric MOVE card stamps no figure_max
and keeps the stat size, and the other word-heroes (“NO”, “Diamond”) are untouched.
The exits FOLD now (owner steer 2026-08-12: “fold them”). The single-card lane
produced the same failure the album bomb did: on 2026-08-12 four watched-artist Hot
100 exits — Olivia Rodrigo “Less”, Drake “I’m Spent”, Drake “Whisper My Name”,
Michael Jackson “Chicago” — posted as four OUT cards across four consecutive slots,
because the lane cards each departure on its own and rode the chart tag (rank 0), so
once the printed-chart debuts were used up the exit won every slot. So two or more
exits on ONE chart in one week fold into a single board
(chart_standings.exit_board, story chart_exit_board), the same fold #2321 applied
to album, watch and radio moves: ship → stamp the folded exits’ bbx: keys so a title
the board showed cannot re-card; gate-fail → fall back to the individual OUT cards
(_MIN_EXIT_CLUSTER, default 2; bbxb:<chart>:<week> board key). The board carries
peak and total run per row and NEVER a current rank — the single-exit fence
(billboard.exit_compose_inputs) carries over in exit_board_compose_inputs.
And the exit is no longer priority 0. Both the board and the single-card fallback
moved off the chart tag to their own chart_exit_board / chart_exit tags, ranked
with the whole-chart roundups (above standings, below every sharp single fact). An
exit is a real Billboard fact but the softest one, so it must not outrank a debut or a
called shot and fill every slot — the second half of the same owner steer (“why is the
most important thing at every slot an exit”).
chart_standings.artist_entries board
with a value column (catalog_values matches each entry to the streams + sales rows by
normalized title + LEAD act, so a Featuring credit still matches and a same-titled cover
never borrows a number), on its OWN desk lane (_artist_catalog_units, story
artist_catalog) rather than inside the standings lane. The move is the point: three
distinct standings boards (census, debut-class, this one) shared the standings lane’s
single per-slot cap and rotated behind each other, so the individual-artist board rarely
surfaced. chart_standings.boards no longer returns artist_entries; the census and
debut-class boards keep the standings lane. Fact-keyed dedup (re-posts only when the
act’s positions change, never on a streams tick), 2/slot so the Hot 100 and Billboard
200 boards both post the same slot, its own artist_catalog_compose_inputs that LETS
the take name the shown streams/sales but bans inventing a figure for a blank row. Live
dry run (real compose + the 0.6 gate): 6/6 ship at 0.90–0.92, each naming only the
numbers the block carries.
leader_entries), so a top act whose big
week sat below the leader got a lone move card instead of a board — the shipped case
was Pooh Shiesty placing four songs on the Hot 100 (one fresh debut at #42) carded as
one move. chart_standings.catalog_picks(week, known, also=…) now returns the leader
PLUS every WATCHED act the predicate selects, and artist_entries(..., pick=…) renders
each one. The move is still carded from _chart_units; the board is the depth behind it
(the album-drop lane’s card-plus-board split). This only ADDS boards — the leader is
always the first pick.
_ARTIST_CATALOG_DEBUT_MIN (2) + entries — the “for this debut” case — OR a deep
_ARTIST_CATALOG_DEEP_MIN (3) + catalog with no trigger. The debut trigger is
load-bearing on volume: the raw 2-entry-no-trigger floor was a ~40-board/week
firehose on the live 2026-08-22 charts (a deep catalog like Rihanna at #68/#78 is
not news, and album positions drift so the fact-keyed dedup still reposts it). The
two arms together measured ~18/week on the same charts, and both thresholds are
env-tunable (MUSIC_DESK_ARTIST_CATALOG_DEBUT_MIN / _DEEP_MIN).PLATFORM charts. _watch_chart_units sweeps every kworb chart the bot already
caches — Spotify US + Global daily, Global weekly, Apple Music songs + albums,
iTunes songs, US radio airplay — for a watched artist’s debut / new #1 / big jump
into the top (artist_watch.chart_stories, one duck-typed derivation over all four
row shapes). Own watch_chart story tag ranked below every Billboard/Luminate fact,
kc:<chart>:<title> dedup on a ~6-day window, 2/slot, platform-labeled card pill.
The framing makes the platform part of the fact — a bare “#4” reads as a Billboard
claim the row cannot support. Live dry run: 4/4 ship at 0.90–0.92, every take naming
its chart. Songstats stays off these sweeps on purpose: it is metered (~18/day)
and kworb mirrors the same Spotify counts free; the metered budget stays on the
per-story deep reads.
CROWN and TOP10 fire for ANY artist on the Spotify + Apple charts (owner steer
2026-08-07, “make sure our top 10 is indeed firing”; chart_stories(top_for_all=
True), the _TOP_ALL_CHARTS keys). The watchlist is the top ~100 by daily
streams, and it provably missed one: Temper City’s “Self Aware” climbed into the
Spotify Global Daily top 10 and no lane carried it — meanwhile the newsroom now
floors deep platform positions, so first-party top-of-chart coverage is what
replaces the wire relays. A new #1 or a top-10 entry is news whoever made it; the
other kinds (new_entry / jump) stay watchlist-only, because an unwatched act’s
mid-chart move is exactly the deep-chart chatter the floor cuts. iTunes and radio
keep the watchlist gate: the store chart’s top ten churns on fanbase pushes, and
the radio chart’s top is slow-moving format inventory.
The iTunes chart lists one song several times, and the parser now collapses that
(#2019). iTunes is a STORE chart. It ranks purchasable listings, and one song is
sold as several listings — an explicit edition, a clean edition, an album track and
a stand-alone single — so each listing charts on its own row. On 2026-08-05 “Ariana
Grande - petal” held #4, #12 and #27 at the same time, and the #12 row carried the
‘NEW’ marker. chart_stories reads rows one at a time, so it could not see that the
#12 ‘NEW’ row was the same song as the #4 row: it made a debut story, the compose
scored it 0.90, and the wire posted “petal debuts at #12 on the US iTunes Top Songs
chart” for a song already 8 places higher on that chart. The fix is in
kworb.parse_apple_chart, not in the lane — it now returns ONE row per (artist,
title), at the song’s best position, so every reader of the chart (this sweep, the
apple_debut alert lane, the newsroom’s chart-claim check) sees the song’s real
standing. Apple Music and Spotify chart one row per song, so only the store chart
needs it, but the collapse runs on all three rank-only charts. Golden:
test_apple_debuts_ignores_a_duplicate_listing.
The ‘NEW’ marker survives the collapse only if EVERY listing is new, and the dry run
is what caught that. The first fix kept the best-placed row and its own marker. Hours
later the live page held “hate that i made you love me” at #15 (NEW), #111 (NEW) and
#144 (-98) — best-row-wins keeps the #15 ‘NEW’ and calls it a debut, while the -98 row
proves the song was on the chart the day before. A song is new only when NO listing of
it is an established entry. Where the listings disagree the song keeps its best
POSITION and loses both claims: no debut, and a move of 0, because the page does not
tell us the song’s own move. Duplicates are also collapsed across the WHOLE page before
limit is applied — truncating first re-opens the hole, since the row that disproves a
debut can sit past the cut (that -98 row was #144, far outside the lane’s 100).
Dry run: python -m scripts.dryrun_kworb_charts. Reads every kworb chart live and
prints, side by side, kworb’s own column headers, what our parser maps them to, the
period/unit our code declares and whether it is MEASURED, the blob the watch lane would
build, and an audit of that blob for ungrounded period words. It exits non-zero on a
surviving duplicate, a chart that parses to nothing, or a period claim on an unmeasured
chart. Accepted duplicates live in _KNOWN_DUPES with the reason. Run it before
changing a chart parser, adding a chart, or wording a chart blob. --compose adds the
real model and the 0.6 gate.
It was not one bad row, and it was not one song. The same live page carried
three duplicated songs, and two of them put the ‘NEW’ marker on the duplicate:
petal at #4 / #12 (NEW) / #27, hate that i made you love me at #17 / #114 (NEW)
/ #127, and AH HA at #6 / #153. So a second false debut was already queued behind
the first. Read this as the normal state of a store chart, not an accident.
Every other chart the bot reads was checked live on 2026-08-05, and all are clean. Owner steer, after the first fix: check the other charts too.
| Source | Rows read | Duplicate rows |
|---|---|---|
| kworb Spotify US daily / Global daily / Global weekly | 199 / 200 / 200 | 0 / 0 / 0 |
| kworb Apple Music US songs / albums | 200 / 200 | 0 / 0 |
| kworb US iTunes Top Songs | 200 raw | 3 songs, 5 rows |
| kworb US radio airplay | 100 | 0 |
| kworb YouTube most-viewed, global + anglophone | 500 / 300 | 0 duplicate video ids |
| billboard.com, all 11 registered charts | 25–200 each | 0 on every one |
| kworb Spotify artists ranking | 1000 | 1 (see below) |
| kworb monthly-listeners ranking | 1000 | 0 |
The Spotify charts and the YouTube pages key their rows on a link id, so a duplicate cannot form there. Billboard curates one row per title. The store chart is the only one that ranks products rather than songs, which is exactly why it is the only one with this shape.
Near-duplicate EDITIONS are deliberately NOT collapsed. The same iTunes page carries “Cardi B - AH HA” at #6 beside “AH HA (Acapella)” at #140, and “Kimo Sounds
petal post came from the titles being
IDENTICAL, which is what let one song read as two. “AH HA (Acapella) debuts at #140”
names a different product in the sentence itself, so a reader is not misled. Exact
match is therefore the right width: it removes the false claim without inventing a
new one.The Spotify half had its own version of the same mistake: a weekly count stated as
a daily one (#2019). Owner steer: check Spotify too. Spotify cannot grow a duplicate
row — its rows key on a track link — but chart_stories hardcoded the words “plays
that day” onto whatever chart it was handed. The stream cell means a different span
per chart: the Spotify DAILY charts carry that day’s plays, the Spotify GLOBAL WEEKLY
carries that week’s. So the weekly blob told the model a week of plays was a day of
plays, and the model repeated it — “Hips Don’t Lie by Shakira jumps to No. 33 on
Spotify’s Global Weekly chart, up from #48, on 15M plays that day” (real compose,
2026-08-05, two samples in a row). The self-gate cannot catch this: the figure IS on
the row, and only the span is wrong. chart_stories now takes a period, and
_WATCH_CHARTS declares it per chart. Live compose after the fix: “pulling 15M
streams this week”, 0.91–0.92. Regression:
test_chart_stories_states_the_weekly_charts_span_not_the_daily_one.
The same question asked of the POSITION-MOVE column found a third one, in the alert
lane (#2019). Owner steer: does any other kworb chart not have the same semantics?
The P+ column also spans a different period per chart, and music_alert._chart_jump
told the model “This is a TODAY move (daily chart)” for every caller it served.
AppleSongRow.pos_change’s own docstring called that column
“the weekly move”. Both cannot be true and neither was measured._chart_jump now takes a move_frame, and a caller passes one only where the period
is measured. The Spotify caller keeps “today”. The Apple caller states no period and
tells the model not to invent one. Live compose on the exact Olivia Rodrigo row: 3/3
said “today” before, 0/3 after. Regression:
test_jump_blob_states_a_period_only_where_the_period_is_measured.
Open measurement. The rank-only pages publish no per-track history, so settling
the Apple/iTunes P+ period needs snapshots of the page 24h and 7d apart. Until
someone takes them, no caller may state a period for an Apple or iTunes move.
A known limit that this source cannot fix: “debut” on the weekly chart can mean a
catalog song. Carly Rae Jepsen’s “Call Me Maybe” (2012) entered the Global Weekly
chart at #176 with Wks=1, so the row reads as a fresh entry, and the lane would
narrate it as a debut. The row carries no release date, so nothing in the data can
separate “new song” from “old song new to this chart”. It has not shipped — the lane
only writes about watched artists, and she is outside the watchlist — but the shape
is there for any catalog act that is watched. Fixing it needs a release date from
another source, not a rule over these rows.
One latent duplicate sits outside this fix. The Spotify artists ranking lists
“Macklemore & Ryan Lewis” twice, at rank 476 (7.39B total streams) and rank 553
(6.36B) — two Spotify artist entities under one name. _watch_milestone_units would
build two candidates under the same artist: entity key with different totals, so
the milestone rungs could be crossed twice with two different numbers. It cannot
reach a post today: the lane only reads rows whose name is on the watchlist, and the
watchlist is the top 100 by DAILY streams, which this act is far outside (checked at
sizes 50 through 200 — absent from all of them). Fix it if the watchlist ever grows
deep enough to admit a duplicated act.
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:
lead_artist drops a feature
clause but leaves an ambiguous “&”/”with” connector whole, so “Cardi B” and “Cardi B &
GloRilla” grouped apart and neither cleared the two-move floor. _lead_act /
debut_clusters now take the lead act off a corroborated split (credit_parties
against the known-artist list), so the two group together — while a name like “Earth,
Wind & Fire” stays whole because no part of it charts alone.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.