Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,27 @@ import { createQueenPool } from '../../lib/db/queen-pool'
import { logger } from '../../lib/logger'
import { leaderboard } from '../services/queen-leaderboard'

/** The window, in days. A month covers the swarm's memory of its own turns. */
const DEFAULT_DAYS = 30
const MAX_DAYS = 90
/**
* ALL TIME BY DEFAULT. `?days=N` narrows it.
*
* This answered for thirty days until 2026-09-23, which was the wrong default
* for a record of who carried the swarm: a lender whose lanes worked hard last
* month and rested this one read as having done nothing, and the board quietly
* shrank as time passed rather than growing with the work.
*/
const MAX_DAYS = 3650

export function createQueenPublicLeaderboardRoute() {
return new Hono().get('/', async (c) => {
const asked = Number(c.req.query('days'))
const days = Number.isFinite(asked)
? Math.min(MAX_DAYS, Math.max(1, Math.trunc(asked)))
: DEFAULT_DAYS
const raw = c.req.query('days')
const asked = Number(raw)
// No `days` at all means the whole record; a `days` that is not a number is
// a mistake in the request rather than a request for everything, so it is
// treated as absent only when it was absent.
const days =
raw === undefined || !Number.isFinite(asked)
? null
: Math.min(MAX_DAYS, Math.max(1, Math.trunc(asked)))
const url = process.env.DATABASE_URL
if (!url) return c.json({ error: 'No database configured' }, 503)
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,20 +148,35 @@ export function rank(
* the archive is where the turns before it live; leaving it out counted the
* last attempt of each issue and called it the swarm's whole history.
*/
export async function keyWork(pool: Pool, days = 30): Promise<KeyWork[]> {
export async function keyWork(
pool: Pool,
days: number | null = null,
): Promise<KeyWork[]> {
// ALL TIME IS THE DEFAULT, and `days` narrows it.
//
// A thirty-day window was the first shape of this and it was the wrong one:
// the board is a record of who carried the swarm, and a record that forgets
// last month tells a lender their work stopped counting. It also shrinks on
// its own - a lane that worked hard in August and stopped simply vanished,
// which reads as "did nothing" rather than "did this, earlier".
//
// `null` means no time predicate at all rather than a very large number of
// days, so the query has nothing to be off-by-one about, and the archive's
// own rows decide how far back the answer goes.
const windowed = days !== null
const { rows } = await pool.query(
`WITH turns AS (
SELECT key_index, review_state, dispatched_at, finished_at
FROM queen_dispatch
WHERE dispatched_at > now() - ($1::integer * interval '1 day')
${windowed ? "WHERE dispatched_at > now() - ($1::integer * interval '1 day')" : ''}
UNION ALL
SELECT (snapshot->>'key_index')::integer,
snapshot->>'review_state',
(snapshot->>'dispatched_at')::timestamptz,
(snapshot->>'finished_at')::timestamptz
FROM queen_dispatch_history
WHERE archived_at > now() - ($1::integer * interval '1 day')
AND snapshot->>'key_index' ~ '^[0-9]+$'
WHERE snapshot->>'key_index' ~ '^[0-9]+$'
${windowed ? "AND archived_at > now() - ($1::integer * interval '1 day')" : ''}
)
SELECT key_index,
count(*) FILTER (WHERE review_state = 'accept') AS accepted,
Expand All @@ -175,7 +190,7 @@ export async function keyWork(pool: Pool, days = 30): Promise<KeyWork[]> {
WHERE key_index IS NOT NULL
GROUP BY key_index
ORDER BY key_index`,
[days],
windowed ? [days] : [],
)
return rows.map((row) => ({
keyIndex: Number(row.key_index),
Expand All @@ -186,14 +201,18 @@ export async function keyWork(pool: Pool, days = 30): Promise<KeyWork[]> {
}

export interface Leaderboard {
days: number
/** The window in days, or null for the whole record. */
days: number | null
measuredAt: string
/** What one accepted issue and one bee-hour are worth, so the page can say so. */
scoring: { acceptedXp: number; hourXp: number }
contributors: Contributor[]
}

export async function leaderboard(pool: Pool, days = 30): Promise<Leaderboard> {
export async function leaderboard(
pool: Pool,
days: number | null = null,
): Promise<Leaderboard> {
const work = await keyWork(pool, days)
return {
days,
Expand Down
41 changes: 41 additions & 0 deletions trios/agent-server/apps/server/tests/api/queen-leaderboard.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { describe, expect, it } from 'bun:test'
import type { Pool } from 'pg'

import {
ACCEPTED_XP,
githubLoginOf,
HOUR_XP,
type KeyWork,
keyWork,
parseOwners,
rank,
xpFor,
Expand Down Expand Up @@ -112,6 +115,44 @@ describe('the score', () => {
expect(row.claimed).toBe(true)
})

/**
* The window is the one branch here that SQL decides rather than TypeScript,
* so it is pinned by what the query actually says. A fake pool is enough:
* the question is whether a time predicate is written and whether a parameter
* is passed, and both are visible without a database.
*/
describe('the window', () => {
const spy = () => {
const seen: { text: string; params: unknown[] } = { text: '', params: [] }
const pool = {
query: (text: string, params: unknown[]) => {
seen.text = text
seen.params = params
return Promise.resolve({ rows: [] })
},
} as unknown as Pool
return { pool, seen }
}

it('asks for the whole record by default, with no time predicate at all', async () => {
const { pool, seen } = spy()
await keyWork(pool)
expect(seen.text).not.toContain('interval')
expect(seen.params).toEqual([])
// The archive still keeps its own guard, which is not about time.
expect(seen.text).toContain("snapshot->>'key_index' ~ ")
})

it('narrows both halves when days is given, and passes it once', async () => {
const { pool, seen } = spy()
await keyWork(pool, 30)
expect(seen.params).toEqual([30])
// Both the live table and the archive, or the answer is half-windowed.
expect(seen.text).toContain('dispatched_at > now()')
expect(seen.text).toContain('archived_at > now()')
})
})

it('gives an unclaimed lane no handle at all', () => {
const [row] = rank([lane(7, 1, 1, 0)], {})
expect(row.name).toBe('key #7')
Expand Down
Loading