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 @@ -44,15 +44,32 @@ import type { Pool } from 'pg'
export const ACCEPTED_XP = 100
/** An hour of a bee's turn, on this key. */
export const HOUR_XP = 10
/**
* AN ACCEPTED ISSUE WHOSE BOUNDARY NAMED A `.t27` FILE, ON TOP OF ACCEPTED_XP.
*
* Owner's word, 2026-09-23: the specs themselves are the main thing this score
* should measure. That is not a preference, it is law L0 - everything below the
* interface becomes `.t27` - and a score that paid the same for a shell script
* and for the spec replacing it would reward every direction equally while the
* constitution names one.
*
* So spec work pays three times what other accepted work pays, and the board
* says which of the two it counted rather than hiding the weight in a total.
*/
export const SPEC_XP = 200

export interface KeyWork {
keyIndex: number
accepted: number
/** Of those accepted, the ones whose boundary named a `.t27` file. */
specs: number
finished: number
hours: number
}

export interface Contributor {
/** Accepted issues whose boundary named a `.t27` file: the game's own goal. */
specs?: number
/** The operator's name for the lender, or `key #N` when nobody claimed it. */
name: string
/** Whether a person claimed this lane in TRIOS_KEY_OWNERS. */
Expand Down Expand Up @@ -83,8 +100,16 @@ export function parseOwners(raw: string | undefined): Record<number, string> {
return owners
}

export function xpFor(work: Pick<KeyWork, 'accepted' | 'hours'>): number {
return Math.round(work.accepted * ACCEPTED_XP + work.hours * HOUR_XP)
export function xpFor(
work: Pick<KeyWork, 'accepted' | 'hours'> & { specs?: number },
): number {
// Spec work is accepted work too, so it earns ACCEPTED_XP and SPEC_XP on top
// rather than instead: the bonus says "and this one moved the goal", it does
// not pretend the rest was not work.
const specs = Math.min(work.specs ?? 0, work.accepted)
return Math.round(
work.accepted * ACCEPTED_XP + specs * SPEC_XP + work.hours * HOUR_XP,
)
}

/**
Expand Down Expand Up @@ -122,12 +147,14 @@ export function rank(
...(claimed ? { github: githubLoginOf(name) } : {}),
keys: [],
accepted: 0,
specs: 0,
finished: 0,
hours: 0,
xp: 0,
}
into.keys.push(lane.keyIndex)
into.accepted += lane.accepted
into.specs = (into.specs ?? 0) + (lane.specs ?? 0)
into.finished += lane.finished
into.hours = Math.round((into.hours + lane.hours) * 10) / 10
byName.set(name, into)
Expand Down Expand Up @@ -166,20 +193,33 @@ export async function keyWork(
const windowed = days !== null
const { rows } = await pool.query(
`WITH turns AS (
SELECT key_index, review_state, dispatched_at, finished_at
SELECT key_index, review_state, dispatched_at, finished_at, owned_paths
FROM queen_dispatch
${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
(snapshot->>'finished_at')::timestamptz,
coalesce(snapshot->'owned_paths', '[]'::jsonb)
FROM queen_dispatch_history
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,
-- The game's own goal, counted from what the issue said it would
-- touch: a boundary naming a .t27 file is spec work. The board
-- shows this beside the total rather than folding it in silently.
-- (No backticks in here: this is inside a template literal, and one
-- of them ends the string and turns the rest into a tagged call.)
count(*) FILTER (
WHERE review_state = 'accept'
AND EXISTS (
SELECT 1 FROM jsonb_array_elements_text(owned_paths) AS p(path)
WHERE path LIKE '%.t27'
)
) AS specs,
count(*) FILTER (WHERE finished_at IS NOT NULL) AS finished,
coalesce(
sum(extract(epoch FROM (finished_at - dispatched_at)))
Expand All @@ -195,6 +235,7 @@ export async function keyWork(
return rows.map((row) => ({
keyIndex: Number(row.key_index),
accepted: Number(row.accepted ?? 0),
specs: Number(row.specs ?? 0),
finished: Number(row.finished ?? 0),
hours: Math.round(Number(row.hours ?? 0) * 10) / 10,
}))
Expand All @@ -205,7 +246,7 @@ export interface Leaderboard {
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 }
scoring: { acceptedXp: number; specXp: number; hourXp: number }
contributors: Contributor[]
}

Expand All @@ -217,7 +258,7 @@ export async function leaderboard(
return {
days,
measuredAt: new Date().toISOString(),
scoring: { acceptedXp: ACCEPTED_XP, hourXp: HOUR_XP },
scoring: { acceptedXp: ACCEPTED_XP, specXp: SPEC_XP, hourXp: HOUR_XP },
contributors: rank(work, parseOwners(process.env.TRIOS_KEY_OWNERS)),
}
}
38 changes: 38 additions & 0 deletions trios/agent-server/apps/server/tests/api/queen-leaderboard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
keyWork,
parseOwners,
rank,
SPEC_XP,
xpFor,
} from '../../src/api/services/queen-leaderboard'

Expand All @@ -22,9 +23,14 @@ const lane = (
accepted: number,
finished: number,
hours: number,
// Of the accepted turns, the ones whose boundary named a `.t27` file. The
// default is none, so every case written before spec work was scored still
// says exactly what it said.
specs = 0,
): KeyWork => ({
keyIndex,
accepted,
specs,
finished,
hours,
})
Expand Down Expand Up @@ -79,6 +85,38 @@ describe('the score', () => {
expect(ranked.map((r) => r.name)).toEqual(['Zoe', 'Ann', 'Ada', 'Bob'])
})

/**
* Law L0 says the stack below the interface becomes `.t27`, and this is where
* the score says so. A spec pays ACCEPTED_XP and SPEC_XP on top rather than
* instead: the bonus says "and this one moved the goal", it does not pretend
* the rest was not work.
*/
describe('spec work, which is the goal of the game', () => {
it('pays the bonus on top of the accepted work, not instead of it', () => {
const plain = rank([lane(0, 2, 2, 0, 0)], { 0: 'A' })[0]
const spec = rank([lane(0, 2, 2, 0, 2)], { 0: 'A' })[0]
expect(plain.xp).toBe(2 * ACCEPTED_XP)
expect(spec.xp).toBe(2 * ACCEPTED_XP + 2 * SPEC_XP)
expect(spec.specs).toBe(2)
})

it('outranks the same amount of work that moved no spec', () => {
const ranked = rank([lane(0, 3, 3, 0, 0), lane(1, 3, 3, 0, 3)], {
0: 'ported nothing',
1: 'ported specs',
})
expect(ranked[0].name).toBe('ported specs')
})

it('cannot count more specs than accepted turns, whatever a row claims', () => {
// A number that outran its own denominator would be a score nobody can
// check against the issues it came from.
expect(xpFor({ accepted: 1, hours: 0, specs: 9 })).toBe(
ACCEPTED_XP + SPEC_XP,
)
})
})

it('reads a GitHub login out of an @name, and carries it to the row', () => {
expect(githubLoginOf('@alex')).toBe('alex')
expect(githubLoginOf('@torvalds')).toBe('torvalds')
Expand Down
Loading