plans/ held 58 documents and exactly one said whether it was open. The rest
mixed finished work, reviews of shipped work, parked specs and genuinely
pending ones, with nothing distinguishing them, so "how many plans are in
the queue" had no answer short of reading all 58.
Now `grep -H '^Status:' plans/*.md` is the answer:
50 done 3 in progress 2 planned 2 reference 1 parked
Statuses were derived rather than guessed: CLAUDE.md's own built list and
"What's NOT built yet" section, plus checking the subject exists in the
code. A review of work that shipped counts as done -- it records what was
found, it is not a request for anything. `reference` separates the two docs
that are conventions rather than work items (admin-design-standards,
admin-work-framework), which otherwise read as permanently-open plans.
The vocabulary is deliberately five words. A larger one invites "mostly
done" and "blocked-ish", which is how the directory became unreadable.
test_plans_declare_status.py keeps it from rotting: a new plan without a
marker fails, as does an unknown status, one buried below the eighth line,
or an open status with no reason -- "planned" alone is the state that rots,
since nobody can tell later whether it waits on a decision, a dependency,
or just nobody's turn.
Also updates the sweep plan with what landed and what did not, including
that #9 was not a defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
4.4 KiB
Review: verdict-bar chart breaks on its first live update
Status: done -- review of shipped work
Scope: just this one defect, introduced in 906c9e6
("fix(admin): replace stretched verdict doughnut with partition bar and
split history into per-metric mini charts"), in
admin/frontend/index.html. Not a review of the rest of that commit (the
history mini-charts split was checked separately and is sound).
Verdict: real regression, invisible on first load, breaks on every subsequent poll
906c9e6 replaced the Verdict Mix doughnut with a horizontal "partition
bar" — one Chart.js bar dataset holding all N verdict counts, rendered as
a single row split into colored segments. That design depends on the chart
always having exactly one category on its index axis, with all N values
living inside that one dataset's data array.
Chart creation gets this right. renderChart's verdict-bar branch
(index.html:844-857) ignores whatever labels it was called with and
hardcodes a single-element array:
return new Chart(canvas, { type: 'bar', data: { labels: [''], datasets }, options: opts });
But updateChart — the path taken on every render after the first,
since renderVerdict calls it whenever verdictChart already exists
(index.html:543) — does not:
function updateChart(name, labels, values, type, colors) { // index.html:898
if (name !== 'verdict' || !verdictChart) return;
verdictChart.data.labels = labels; // index.html:900
verdictChart.data.datasets[0].data = values;
verdictChart.data.datasets[0].backgroundColor = colors;
verdictChart.update();
}
labels here is Object.keys(mix) (index.html:527) — e.g.
['pass', 'fail'], one entry per verdict category, not the single-element
array the chart was created with. Line 900 overwrites data.labels with
that real array.
For a stacked bar chart on indexAxis: 'y' with a single dataset,
Chart.js uses data.labels.length to decide how many category rows to
draw. stacked: true (index.html:854-855) only merges multiple
datasets that share an index position — it does nothing to merge
multiple values within one dataset onto a single row. So the moment
data.labels goes from [''] to ['pass', 'fail'], the same one dataset
that used to render as one bar with two colored segments instead renders
as two separate bars, one per label. The "partition bar" concept only
holds together as long as labels stays a single blank entry, which is
exactly the invariant updateChart breaks.
Why this passed the round of QA that caught the last verdict-chart bug
The previous regression (verdictChart never being assigned, causing
"Canvas is already in use") was caught by explicitly waiting 36+ seconds to
span a REFRESH_MS poll cycle
(plans/.omo/evidence/task-8-admin-visual-fixes-v3-verdict.md). That
discipline wasn't applied to 906c9e6 — it's a pure frontend diff with no
Playwright run, no screenshot, and no unit test attached
(git show 906c9e6 --stat touches only admin/frontend/index.html). A
single fresh-load screenshot would show a correct-looking single bar, since
first render always goes through renderChart, not updateChart — the
bug only shows up starting from the second render, i.e. the first 30-second
poll or SSE-triggered refresh after page load. Same blind spot as the last
bug, in a new function.
Fix
updateChart is only ever called for 'verdict' (the name !== 'verdict'
guard at the top makes it single-purpose), so there's no other caller
relying on the data.labels assignment. Drop it:
function updateChart(name, labels, values, type, colors) {
if (name !== 'verdict' || !verdictChart) return;
verdictChart.data.datasets[0].data = values;
verdictChart.data.datasets[0].backgroundColor = colors;
verdictChart.update();
}
labels becomes an unused parameter at that point; either drop it from the
signature and its one call site (index.html:543), or leave it for
signature symmetry with renderChart — cosmetic, doesn't affect
correctness either way.
Verification recommendation
A screenshot alone won't catch this class of bug twice. Confirm the fix the
same way the last one was confirmed: load /admin/, capture the verdict
bar on first render, wait past one REFRESH_MS cycle (30s+), capture again,
and diff — the bar should still be one row with the same segment count
before and after, not N separate rows.