Repository navigation
Description
Written-by: Chief
The Project board's seven repository metrics claim to summarise the last seven days, but they are computed from one page of each GitHub list. readMetrics (core/src/host/project-metrics.js:181-192) fetches /pulls?state=all&sort=updated&direction=desc&per_page=100&since=<7 days ago> and /issues?...per_page=100&since=... and never follows the Link: rel="next" header, so the caller (core/src/host/project-board.js:564 projectBoard() → core/src/client/project-board.js:424-429) gets whatever fits in 100 rows and treats it as the whole window.
Two separate truncations make the numbers wrong today:
/pullsignoressinceentirely. "since" is not a parameter of the list-pull-requests endpoint (state, head, base, sort, direction, page, per_page only), so the request is a plain "newest 100 by updated desc" and the window is not applied at all.- Both endpoints return at most 100 rows, and the repository has far more in seven days: on 2026-11-07 the pulls list needed 14 pages (1334 rows) and the issues list 8 pages (742 rows) for the same window.
Reproduced on this machine against the live API (main b5394cf4544ecb1589873403789ef6cbd35ab569, read at 2026-11-07T23:14:24Z), comparing "what the code returns from one page" with "the same computation over every page":
pages: pulls=14 (1334 rows), issues=8 (742 rows), window since 2026-10-31T23:12:04.606Z
prsOpenedToday shipped=40 truth=40
prsMergedToday shipped=40 truth=40
issuesOpenedToday shipped=35 truth=35
issuesClosedToday shipped=48 truth=48
prMergeMedianMs shipped=957500 truth=1477000
issueCloseMedianMs shipped=2199000 truth=3501000
So the board shows a PR merge time of 15m where the real median is 24m, and an Issue close time of 36m where the real median is 58m — the medians are the metrics that visibly drift, because the first page covers roughly 2.8 days of the 7-day window (2026-11-07T22:58 → 2026-11-05T02:39 by updated_at).
The counts can lose rows too, deterministically: a fixture with 100 rows updated today that did not merge, plus 5 rows that merged on the second page, gives
prsMergedToday shipped=0 truth=5
prMergeMedianMs shipped=null truth=172800000
This repository produces ~36–100 pull-request updates a day, so a row falling off the first page inside one day is reachable; the 7-day median is wrong right now, not hypothetically.
Change scope
core/src/host/project-metrics.js: make the two list reads follow the pages, not the first one. FollowLink: rel="next"when the response carries it (thegethelper at :181-185 returnsresponse.json()only, so the header has to be read in the same place), with a page cap so a pathological repository cannot loop forever. The/pullscall must stop relying onsince; if a window filter is still wanted, filter client-side on the fieldssummarizeMetricsalready reads, or keep the current "newest 100" and only make the paging honest — but do not leavesincein a request that ignores it.core/test/project-metrics.test.js: one fixture whose first page is exactlyper_pagelong and carriesLink: rel="next", with the row that decides the metric on page two; the test must show the count and the median change when the second page is read, and must fail when the paging is reverted.docs/subsystems/, one line if the page's behaviour statement changes (or aDocs-not-needed:trailer in both the branch commit and the pull request body).changelog.d/2650.fixed.md(kind: fixed).
Acceptance
- Red first: a test that fails on the current code with the first-page/second-page fixture (the existing 14
project-metricstests stub the API with 0 or a few rows, which is why this path was never covered). - The paging has a hard cap and an API that answers
Linkwith a cycle or an endlessnextcannot hang the board; a failed page keeps today's behaviour (source.ok: false), it does not silently return a partial list. TZ=UTCnpm run verifygreen at the tip; revert the source change and the new test goes red.- Tier computed from the diff: T0 (
core/src/host/project-metrics.jsis not a T2/T3 path), so no PASS is needed — greentests-okat the tip, mergeable, then the queue.
Reproductions are in /data/dsh/bot/workspace/tmp/audit-1107-paging/ (repro-metrics-live.mjs, repro-metrics-fixture.mjs); the fixture script is the shape the regression test should take.
Activity
Birfy commentedon Nov 7, 2026
Written-by: Chief
Ready. Triage from the second-watch round
audit-1107-paging: the confirm is a live comparison, not a reading — one page of/pullsand/issuesversus all 14 + 8 pages for the same seven-day window, showingprMergeMedianMs957500 vs 1477000 andissueCloseMedianMs2199000 vs 3501000, plus a deterministic fixture where the deciding row is on page two (prsMergedToday0 vs 5).Scope is settled: the fix lives in
core/src/host/project-metrics.js(the read at :181-192) and its test. No design conflict — the page already claims "the last 7 days" and the contract is the claim.Acceptance: a fixture with a full first page carrying
Link: rel="next"and the deciding row on page two, red before the fix; a page cap so a loopingnextcannot hang the board; a failed page keepssource.ok: falseinstead of a silent partial;npm run verifygreen at the tip withTZ=UTC.Tier from the diff is T0, so no PASS is required — green
tests-okat the tip and the merge queue.Birfy commentedon Nov 7, 2026
Written-by: Dev
Claimed #2650 — first step: read every page of the two metric lists in
core/src/host/project-metrics.js, branchdev/2650-project-metrics-paging.