web: the milestones page says when its rollup is partial MilestoneView has carried the clip since ebbd30f and list_milestones has reported it since a419499; milestones.html rendered nothing, so the page was the last surface presenting a partial rollup as whole-tracker arithmetic. Every number on it is a rollup — "1/4 done", "1995 of 2000 issues carry no milestone label" — and arithmetic over the rows that were read renders exactly like arithmetic over the tracker. The notice is the detail pane's, word for word and class for class: it is the same fact about the same read, and a second phrasing for it would be a second style. It sits above the rollup and outside the milestones/no-milestones branch, because "No milestones." over a clipped read is the reading most easily mistaken for a fact about the tracker. A milestone can lose members here rather than merely undercount them — membership comes from labels — so the fixture makes that concrete: m1 has five members, one of them past the cap, and the page counts four while saying why.
beads: extract the projection out of web/ web/beads.go held the one reading of the beads schema — the table fingerprint, the lane bucketing, the ready rule, the status categories, the transitive dependency walk, the event humanizer, the filter model and the milestone rollup — where a second consumer could not reach it. The MCP surface and the cross-database ready page both need it. Move it to a new root package beads/ that depends on browse/ and the standard library only: rows in, view model out, no net/http, no html/template, no core. The BrowseSession seam is declared consumer-side there and names the one method the projections call, so web's larger BrowseSession satisfies it structurally and a session passes straight through. web/beads.go and web/milestones.go keep only their View adapters — slug, label, template, Applies, and the hand-off of ref and query. The templates are unchanged: the moved types keep their names and their display methods, so every dot still resolves. Pure move plus the beadsMax -> beads.Max export rename. The projection tests move with the code and become testify; the render tests stay in web/ unchanged. Same 63 tests pass before and after.
feat(web/beads): hierarchy in the milestone view Arrange each milestone as a shallow hierarchy instead of a flat list. - Put the milestone-typed issue(s) on top as the milestone's heads. - Nest tasks under their epics via parent-child dependency edges, with a done/total rollup on the epic row; membership stays label-based. - Keep remaining members as a loose tail; blocks edges are ignored. - Read the dependencies table optionally, degrading to a flat list. - Extend fixtures and real-data logging to cover the hierarchy.
feat(web/beads): promote milestones to their own view/tab Move the milestone rollup off the board and into a dedicated "Milestones" tab (a registered View, companion to Beads via the same fingerprint). The tab is richer than the old board panel: per-milestone progress bar, done/open/in-progress counts, and the issue list under each milestone (open-work first, closed struck through), each linking to its beads detail. Issues with no milestone label are summarized in a footnote. A beads DB with no milestone labels still gets the tab, showing an empty-state hint. Removes the board's inline milestone panel and the collectMilestones/BeadMilestone helpers it used; the grouping now lives in the milestones view.