beads: show the stored rows behind an issue on its detail pane Every field on the detail pane is a reading of a row: the Issue struct names the columns bd surfaces and drops the rest, humanizeEvent turns two JSON blobs into a sentence, an Edge keeps an id and a type out of a dependency row. A reading that went wrong looks exactly like one that did not, so the detail modes now carry the rows they were built from — the issue's own row first, then the rows belonging to it in labels, dependencies, comments and events, in the order the tables were read and the columns came back in. custom_statuses is read for the lane and is deliberately absent: its rows describe the tracker's statuses, not this issue. Related tables are capped at RawMax rows and counted whole, so a busy issue's audit log cannot turn the pane into a second page. A raw view may not report an absent cell and an empty string as the same thing, which is the one place cell()'s flattening of a real NULL to "" is wrong. rowCells.isNull is now the single site that decides, shared by cell and the raw row, so the two can never disagree; RawCell carries Null beside Value and drops browse's "NULL" text, which would have put the collision straight back. The section renders as a collapsed <details> in the stream layout's Past Stand idiom — it is a check on the rendering, not the reason a reader came. Values are escaped and deliberately not run through beadLinks.Text: a linkifier rewrites what it is given, and a value that has been rewritten is no longer the value that is stored. A cell holding no value renders as a NULL chip, a cell storing those four characters as the text it stores, an empty one as (empty); a long value scrolls inside its own cell.
beads: the board names every clipped table it draws from The board raised its truncation flag for a clipped issues or dependencies table and for nothing else, so a clipped labels table took the pills off every card and the options out of the label filter in silence, and a clipped custom_statuses table could put a card in the wrong lane with the counts still reading as facts. The flag could not grow to hold that. It is paired with a count line — "the first N of M issues" — so it can only mean the reads that decide N and M; widening it would have made a bool that means four different things and a number that no longer follows from it. Data.Truncated and ShownOf are therefore byte-for-byte what they were (mcpsrv's list_issues reads them as they are), and the wider fact is a new field: Clipped, one entry per clipped table the board reads, in read order, each naming the table, the rows read against the rows that exist, and what the board lost by the rest. The banner follows. The count line now prints only when the issues read was actually short — "the first 4 of 4 issues" was a wrong sentence, not a warning — and every other clipped table gets its own line beneath it. The detail pane keeps its single sentence from 82d997d; a pane that never drew a lane has no use for a board's list. TestBoardTruncationIsUnchanged is now TestBoardTruncationFlagIsUnchanged: the flag is what it was, which is the claim that survives. Its "comments does not flip the board" subtest survives as "a clip in a table the board does not read" and now also asserts comments reaches neither the flag nor the list, plus a new subtest pinning that labels and custom_statuses are reported without touching the flag.
beads: stop flattening a stored NULL string
beads: report a clipped read from every projection
web: answer what is ready across every tracker
beads: add a one-column stream layout beside the board ?layout=stream renders the same filtered set, the same buckets and the same cards as one column of sections instead of four lanes side by side. It is a layout of the Beads view and not a fifth tab: filters, the ready toggle, the search box and ?issue= behave exactly as they do on the board. A column read top to bottom can afford one sort per section, because each section answers a different question: Rolling by started_at desc (what was picked up last is what is being worked on), Lined Up ready-first then priority then oldest, Stalled by blocker count (one blocker away is nearer than five), Past Stand by closed_at desc. started_at and closed_at join the card model as sort keys and are not displayed; a row missing one sorts last, because an unset timestamp is not a very old one. Past Stand opens collapsed in a <details> — the largest and least actionable section, closed without a line of JavaScript. The sections are derived from the finished lanes rather than bucketed again, so the section counts cannot drift from the marquee, and the lanes keep the board order the board renders. The Board/Stream toggle rebuilds the current query with layout replaced, via a new withQuery template func, so every active filter survives the switch.
beads: drop the Bead/Beads prefix from the moved types
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.