Skip to content

fix(leaderboard): gate brief_inclusions on inscription finalization#307

Merged
biwasxyz merged 1 commit into
mainfrom
fix/leaderboard-inscription-gate
Mar 27, 2026
Merged

fix(leaderboard): gate brief_inclusions on inscription finalization#307
biwasxyz merged 1 commit into
mainfrom
fix/leaderboard-inscription-gate

Conversation

@biwasxyz
Copy link
Copy Markdown
Contributor

Summary

  • Inscription gate on brief_inclusions: The leaderboard brief_inclusions_30d subquery now joins brief_signalsbriefs and requires briefs.inscription_id IS NOT NULL. Signals only count toward the brief inclusion score after the daily brief has been inscribed on Bitcoin.

Problem

The leaderboard scoring formula counts brief_inclusions as soon as signals are compiled into a brief (~11pm PT), before the brief is inscribed on-chain (~11:30pm PT) and before payments are sent (~1-6am PT). During this window, a correspondent's score temporarily inflates. If a brief compilation fails or signals are dropped, the score would be inaccurate.

As noted in issue #298 comments, the brief_signals table is written at review time (before inscription), but earnings is correctly gated on payment time. This PR closes the gap for brief_inclusions.

Changes

File Change
src/objects/news-do.ts Join brief_signals bsbriefs br in the bi subquery of queryLeaderboard(), add AND br.inscription_id IS NOT NULL

What this does NOT change

Per the issue #298 discussion:

  • signal_count / days_active: These count raw signal submissions regardless of brief inclusion. They measure volume and participation, not editorial confirmation. Gating them on inscription would make the leaderboard appear frozen until 11pm PT daily, which is bad UX per the discussion.
  • streaks: Written at signal submission time. A confirmed_at column would be a larger migration best done alongside PR feat: per-beat streak tracking for multi-beat agents #273 (beat_streaks). This is a follow-up.
  • earnings: Already correctly gated on payment time — no change needed.

Test plan

  • npx tsc --noEmit passes (confirmed locally)
  • Deploy to staging, compile a brief WITHOUT inscribing — verify brief_inclusions_30d stays at 0
  • Inscribe the brief — verify brief_inclusions_30d reflects the included signals
  • Verify retracted_at IS NULL guard still works (retracted signals don't count even after inscription)

Closes #298

🤖 Generated with Claude Code

…zation

The leaderboard brief_inclusions_30d count now only includes signals
whose brief has been inscribed on Bitcoin (briefs.inscription_id IS NOT
NULL). Previously, signals scored for brief inclusion as soon as they
were compiled, before the daily inscription confirmed them on-chain.

This ensures the leaderboard reflects finalized editorial contributions
and prevents temporary score inflation during the compilation→inscription
window (~11pm-11:30pm PT each day).

Closes #298

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
agent-news b84ec60 Mar 27 2026, 10:46 AM

@github-actions
Copy link
Copy Markdown
Contributor

Preview deployed: https://agent-news-staging.hosting-962.workers.dev

This preview uses sample data — beats, signals, and streaks are seeded automatically.

Copy link
Copy Markdown
Contributor

@arc0btc arc0btc left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gates `brief_inclusions_30d` on inscription finalization — addresses the temporary score inflation window between brief compilation (~11pm PT) and on-chain inscription (~11:30pm PT).

What works well:

  • The join condition `bs.brief_date = br.date` is clean and semantically correct — matches signals to their brief by the date the brief was compiled
  • `retracted_at IS NULL` guard preserved correctly on the `bs` alias
  • The scope is deliberately tight: only `brief_inclusions` is changed; `signal_count`, `streaks`, and `earnings` are explicitly deferred with clear reasoning in the PR description
  • The PR description is thorough — explains why each untouched metric is intentionally left alone

[question] Performance: is `briefs.date` indexed? (`src/objects/news-do.ts`)
The added JOIN on `bs.brief_date = br.date` is fine when `briefs` is small (one row/day), but if the table grows over months/years of competition history, a missing index on `briefs.date` could slow the leaderboard query. Worth confirming or adding an index if it doesn't exist.

[question] 30-day window semantics
The filter remains on `bs.created_at` (signal submission time). That's probably correct — we want "signals submitted in the last 30 days that ended up in an inscribed brief." Just confirming this is intentional rather than having it be on `br.inscription_date` or similar.

[nit] The implicit INNER JOIN (was LEFT-like before via direct table reference) now silently drops `brief_signals` rows where `brief_date` has no matching `briefs.date`. This is the right behavior — orphaned signals shouldn't count — but worth a comment if this schema relationship isn't documented elsewhere.

Code quality notes:

  • Minimal change, well-scoped. No simplification opportunities — this is already the cleanest way to express the constraint.
  • No reuse concerns; this is a single-query modification.

Operational note: We file signals for the ordinals beat under the active $100K competition. This fix matters for competition integrity — Arc's own `brief_inclusions_30d` count would temporarily inflate during the compilation→inscription gap each night. Accurate leaderboard scores help all contributors track standing correctly. The gate is the right call.

@biwasxyz biwasxyz requested a review from arc0btc March 27, 2026 10:52
@biwasxyz
Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review @arc0btc. Answering your questions:

Performance: briefs.date is indexed — It's the PRIMARY KEY on the briefs table (schema.ts line 45: date TEXT PRIMARY KEY). SQLite uses the PK as the row ID lookup, so the JOIN is effectively a direct index scan. Even at years of history (365 rows/year), this is a sub-millisecond lookup.

30-day window on bs.created_at — Yes, intentional. The question is "which signals were submitted recently and made it into an inscribed brief?" Using bs.created_at (submission time) keeps the window semantics consistent with signal_count and days_active which also use submission time. If we filtered on inscription time instead, a signal submitted 31 days ago but inscribed 29 days ago would count for brief_inclusions but not signal_count — which would be confusing for correspondents tracking their scores.

INNER JOIN dropping orphaned rows — Good catch. Orphaned brief_signals rows (where brief_date doesn't match any briefs.date) would be silently excluded. This is correct — if there's no brief record, there's no inscription to gate on. In practice, brief_signals rows are always written alongside a briefs row during compilation, so orphans shouldn't exist. But the implicit behavior is worth noting.

Copy link
Copy Markdown
Contributor

@tfireubs-ui tfireubs-ui left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVED

The JOIN is correct: brief_signals.brief_date = briefs.date is the right FK relationship, and adding br.inscription_id IS NOT NULL precisely gates inclusion counting on on-chain inscription finalization.

Key things verified:

  • The subquery retains the existing 30-day window and retracted_at IS NULL filter — no regression there
  • The new JOIN doesn't inflate counts (it's a LEFT JOIN outer that filters to a strict subset)
  • Closing timing issue from issue #298: inclusions compiled at ~11pm PT but not yet inscribed will now correctly show 0 until ~11:30pm inscription confirms

One thing to keep in mind: if a brief's inscription is reverted or the inscription_id is later nulled for any reason, historical inclusion counts could drop unexpectedly. But that's an edge case and not a blocker given the current system's behavior.

Clean, minimal fix. The leaderboard now accurately reflects on-chain settled state.

@biwasxyz biwasxyz merged commit 4b69521 into main Mar 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Leaderboard should only update after daily inscription + payments finalize

3 participants