Widen uncovered-release candidate set to rotation ∪ recently played - #2159
Merged
Conversation
This was referenced Aug 15, 2026
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
2 times, most recently
from
August 15, 2026 01:33
90036ee to
f5ba32c
Compare
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
from
August 23, 2026 04:57
f5ba32c to
9450908
Compare
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
2 times, most recently
from
August 23, 2026 17:38
a8abe99 to
a829401
Compare
Member
Author
|
Rebased onto the updated Verified locally on the rebased tree: build, typecheck (plus the per-job Briefly closed and reopened while probing whether GitHub would let the stack be retargeted to |
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
from
August 23, 2026 18:00
a829401 to
a76a8c7
Compare
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
from
August 23, 2026 18:06
a76a8c7 to
e64d9a8
Compare
…BS#1877 amendment) The search handoff offered only active rotation (~300 releases), but most of what DJs actually play — and what listeners see — is not current rotation. The candidate set is now active rotation ∪ recently played linked albums, keeping the wire schema, exact-match ceiling, and ADR 0013 architecture unchanged. - New plays.ts: fetchRecentPlays(lookbackDays) windows flowsheet (entry_type='track', album_id linked, canonical pair via the artists join); fetchAllPlayedAlbums() sources the album_plays MV for the one-time --backfill drain (37,421 albums, play-count desc, deliberately no SQL LIMIT — a LIMIT would silently stall the drain against the handed-off anti-join). - orchestrate.ts: single mode-blind fetchPlayCandidates injection; rotation-first concat + first-wins dedup; one cap (UNCOVERED_MAX_RELEASES_PER_RUN) post-anti-join and pre-DRY_RUN, capped_out computed at the cap site; the capped list feeds render/write/publish/recordHandoffs alike; rotation-lane guards stay hard throws in steady state and demote to log + Sentry captureError under --backfill; zero-plays escalates without throwing; locked DRY_RUN report widened with backfill, recent_play_rows, candidate_rows, capped_out. - Dockerfile: CMD → ENTRYPOINT + empty CMD so docker-level --backfill passes through; this deliberately relocates the default OUTPUT_PATH to the WORKDIR (documented). - Backfill pacing: single-path whole-file publish + publish-once markers means at most one --backfill invocation per consumer cycle until research-data#16 walks the snapshot branch history; requirement relayed on that ticket (WXYC/research-data#16 (comment)). - Docs: README rewrite (modes, cap semantics, pacing, guards, locked report), ADR 0013 line-57 sizing (credential-conditional), env-vars, ops-cron-scheduling, package description, workspace CLAUDE.md row, stale header sweeps. - Tests: unit coverage for both arms, options parsing, cap/dedup/guard behavior (report matched on its unique line prefix, not a shared token); integration cases for both arms with sql.unsafe MV refresh and fixture-scoped scoped* helpers. Steady-state cost after PUBLISH + RESEARCH_DATA_WRITE_TOKEN are provisioned: ~75 new albums/week ≈ 325 searches/month. Measured: play aggregate ~174 ms unindexed; anti-join 13–25 ms at the full 37,421-id cardinality. Code-review fixes folded in: the backfill and pull-from-container recipes now bind-mount the output directory instead of promising a docker cp that --rm makes impossible (with the credential unprovisioned that file is the run's only artifact, so the old recipe silently lost the batch); the empty-snapshot publish skip from the parent commit is re-keyed onto the capped list, which is what actually gets rendered, written, and published; and plays.ts cites the migration that creates flowsheet_track_add_time_idx rather than a schema.ts line range that had already rotted onto an unrelated block comment. Renumbers this PR's references to the marker migration from 0146 to 0156, matching the schema PR beneath it: main landed its own 0146 (library-delete-denylist, BS#2112) while this stack sat open. Touched job.ts's header comment (resolved against this branch's widened prose, which is kept) and the job's CLAUDE.md registration row. The surviving "migration 0146" in jobs/library-etl/README.md is correct as-is — it refers to main's denylist migration, not this one.
jakebromberg
force-pushed
the
uncovered-release-list-widening
branch
2 times, most recently
from
August 23, 2026 18:16
1ce9be8 to
bf90695
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The search handoff offered only active rotation (~300 releases). WXYC is a freeform station: most of what a DJ actually enters into the flowsheet is not a current rotation release, so the releases most likely to reach a listener's feed are exactly the ones the search crawler never sees. The candidate set becomes active rotation ∪ recently played linked albums, keeping the wire schema, the exact-match ceiling, and ADR 0013's architecture unchanged.
plays.ts.fetchRecentPlays(lookbackDays)windowsflowsheet(entry_type='track',album_idlinked, canonical pair via theartistsjoin);fetchAllPlayedAlbums()sources thealbum_playsMV for the one-time--backfilldrain (37,421 albums, play-count desc, deliberately no SQLLIMIT— aLIMITwould silently stall the drain, since each run would re-select the identical top N and the anti-join would drop all of them as already-handed-off).orchestrate.ts. One mode-blindfetchPlayCandidatesinjection; rotation-first concat + first-wins dedup; one cap (UNCOVERED_MAX_RELEASES_PER_RUN) post-anti-join and pre-DRY_RUN, withcapped_outcomputed at the cap site asuncovered.length - capped.length. The capped list feedsrenderSnapshot,writeSnapshot,publish, andrecordHandoffsalike, so the file on disk, the published snapshot, and the marker rows describe an identical release set.captureErrorunder--backfill— a rotation-source regression must not abort a drain holding 37,421 valid play-arm candidates. Zero-plays escalates without throwing.CMD→ENTRYPOINT+ emptyCMDso docker-level--backfillpasses through instead of replacing the launcher. This deliberately relocates the defaultOUTPUT_PATHto the WORKDIR — which is the pathwriter.ts's docstring already claimed, so the conversion makes that docstring true instead of stale. Documented in the README anddocs/env-vars.md.Backfill pacing — the one coupling the wire schema can't express
Publish is a whole-file replace of a single fixed path, and markers are publish-once. Run
--backfillten times back to back and 4,000 releases are marked handed-off while only the final 400 exist at branch HEAD — the exact permanent-drop failure the publish-gated marker design exists to prevent, arriving through the front door.So the operator contract is at most one
--backfillinvocation per consumer cycle, documented in the README next to thedocker runrecipe. The constraint disappears entirely if research-data#16's consumer walks the snapshot branch's commit history rather than reading HEAD only; that requirement is relayed on that ticket.Measured, not extrapolated
That 39.7% replay rate is why the backlog gets a
--backfillmode instead of being left to the sliding window: ~60% of anything the window evicts is lost for months or indefinitely.Precondition — this is gating, not a follow-up
Marker writes are publish-gated, and
PUBLISH+RESEARCH_DATA_WRITE_TOKENare still unprovisioned. Until they land,recordHandoffswrites nothing and every weekly run re-offers the entire eligible set — post-widening that means ~2,356 albums offered every run withcapped_outpinned near 1,956 indefinitely, the opposite of "a safety valve normal operation never reaches."Steady-state sizing (~75 new albums/week ≈ 325 searches/month, inside Brave's free tier) and the ADR line-57 rewrite are both stated as conditional on that credential.
Stack
Last link of a three-PR chain, split out of #1879's original 3,137-line form:
uncovered_release_search_markers(migration 0146).Closes #1877.The split preserves the property that made landing this on the same branch the right call: nothing downstream consumes the snapshot yet (research-data#16 is unbuilt) and the publish credential is unprovisioned, so while #1879 is merged and this is not, the cron runs rotation-only, writes no markers, and publishes nothing. The invariant is that all three land before the credential is provisioned.
Out of scope
Free-text (unlinked) plays — ~43% of music plays have
album_id IS NULL, and resolving them would mean running a DJ-typed string through a matcher, reintroducing exactly the wrong-album risk ADR 0013's canonical-pair design avoids. Also: per-insert live search, long-tail dedup ranking (#1878), draining the full 37,421-album tail, and provisioning the publish credential itself.Test plan
plays.test.ts(new) — canonical-field mapping and row pass-through; asserts no SQL shape, per this suite's convention that SQL shape is pinned by the integration spec.job.test.ts(new) —uncoveredJobOptionsenv parses and--backfill.orchestrate.test.ts— concat + dedup precedence, cap truncation,capped_outnon-zero under DRY_RUN, the capped list reaching all four downstream sites, the non-throwing empty-plays path, rotation-guard demotion under--backfill.tests/integration/uncovered-release-list.spec.js— one case per arm, both fixture-scoped (scoped*helpers) since the suite runs--runInBandover sharedflowsheetstate; backfill case refreshes the MV viasql.unsafefirst.npm run typecheck,npm run lint(0 errors),npm run format:check,npm run test:unitnpm run ci:testmock— full Docker CI pipeline greenCloses #2157.