Repository navigation
Project board's Task completion median is always —: the metric waits for a status and a stamp the store never writes #1884
Description
Implementation: FlyBot (ours) — PR follows within 24h. Do not pick up without a pickup-ok comment.
Written-by: FlyBot Lead
Defect
The Project board's Task completion median (taskDoneMedianMs, the —/number under "Task completion") is always — on a real deployment, because doneTaskDurations waits for a status and a stamp that no host path ever produces.
core/src/host/project-metrics.js:56-70(on33e9c6d5) keeps a task only whentask.status === 'done'andNumber.isFinite(task.completedAt).- A card finishes through
transition(task, 'accepted')(core/src/host/tasks.js:269-279), which writes onlystatus,updatedAtand an entry onactors({ status, at });TRANSITIONSends ataccepted/cancelled/failed, so'done'is never a record status. completedAtis read inproject-metrics.jsand written nowhere in the repository — a card that reached the board's Done column has no such field.core/src/shared/task-board.js:8-19mapsaccepted/cancelled/failedto thedonecolumn; a literal'done'status is not in that table and would paint asnot-started.docs/subsystems/core-host.md:793promises fields the host never writes (reads createdAt / completedAt from state.tasks).
Existing tests (core/test/project-metrics.test.js:119-121, core/test/project-metrics-scope.test.js:43,46) hand-write status: 'done' plus completedAt, which is exactly the false-green shape: the fixtures assert a record the store cannot produce.
Reproduction (clean 33e9c6d5, sources unchanged, only core/test/project-metrics.test.js taken from the fix)
A card in the shape the store really writes — status: 'accepted', createdAt 10h ago, actors: [open@-10h, claimed@-6h, accepted@-1h], no completedAt — must contribute 9h (32400000). It contributes nothing:
$ TZ=UTC node --test test/project-metrics.test.js
not ok 10 - the task median reads the store stamps only
null !== 7200000
not ok 13 - a card that finished the way the host finishes cards is a done duration
null !== 32400000
# tests 14 / # pass 12 / # fail 2
The two null guards pass on the base by design — nothing is counted at all today, so they only start guarding the accepted-only rule once the first line turns green.
Expected behaviour
One rule, read from the board's own column table — no second status table, no invented state, no back-filled completedAt:
- a card counts when
columnOf(task) === 'done'andtask.status === 'accepted';cancelledandfailedare in the done column but never completed, so they are skipped (the card's caption is "created to done"); - its duration is
createdAt→ theatof the last terminal entry onactors, falling back tocompletedAt(finite) and thenupdatedAtfor records without actors; - the seven-day window still compares that end instant; an empty window or a board whose only done-column cards were cancelled/failed answers
null(the client's—), never0; - docs line
core-host.md:793states the real source.
Scope
core/src/host/project-metrics.jsand its mirrorpatches/ds-bot/files/src/host/project-metrics.js- the red-first cases themselves, inside
core/test/project-metrics.test.js(and its mirror) - fixture corrections in
core/test/project-metrics.test.jsandcore/test/project-metrics-scope.test.js(thestatus: 'done'fixtures above, changed to the status the host writes) plus their mirrors docs/subsystems/core-host.md:793, one changelog fragment;generate+checkthree lines stay green
No client change, no shared/task-board.js change, no state-machine change.
Dedupe
search/issues on live main: taskDoneMedianMs → only #1880 and #1848 (both closed; they scope the same metric to one board), completedAt → 0 hits. This defect is a different one: the metric waits for a status and a stamp the store never writes.
Activity