Not sure where the discrepancy is coming from.
Also, I think we could make home simply render a table for each open "epic" rather than open designdocs, this woud mean we don't have to hit Github for the list of designdocs, and be more general / scalable for creating more "projects" that aren't necessarily keyed around a checked-in rigorous design doc artefact.
I would still like to see links to the designdocs easily from the open epics, this could be achieved by adding the designdoc link to a markdown field somewhere in the epic dataobject, and extracting it at render time. It would also be useful to have that link easily accessible on the board view:
https://nfold.dev/board/nf-758
But overall, reducing our rendering dependence on Github I hope also makes everything load faster.
None.
Spec: render the homepage task panel from open bd epics; link each epic to its design doc via a description pointer
Prescriptive card for this issue (spec round, 2026-08-21). Aedan's ask is the issue description above, verbatim — this comment turns it into an implementable card. Supersedes and absorbs nf-s8op (same core swap, from Aedan's 2026-08-20 chat: "can you just read the designdocs from the beads epic for them?"); nf-s8op is closed as duplicate pointing here.
The discrepancy, diagnosed
The two pages apply different filters to different sources:
-
/(fetch_design_task_panels,tools/web_frontend/server.py:317) walksdesign_docs/*.mdover the GitHub Contents API and keeps a doc only if it has (a) a parseable task table, (b) a**Beads:**pointer line (server.py:365-367), and (c) an epic that resolves and is not closed (server.py:368-370). Today that leaves 012 only: 005b's doc has no**Beads:**line (its epic nf-lbh was minted outsideoperationalize_design.py --apply— verified, `grep 'Beads:' design_docs/*.md` matches only 011 and 012), and 011's epic nf-tvc is closed. -/beads(fetch_bd_epics→_epics_by_id, server.py:406-435) renders every epic in the workspace, closed included (Bd.list_issuesdeliberately passes--all, server.py:205-212). Today: 005b + 011 + 012.The prescription dissolves the discrepancy rather than patching either filter (A4): both pages read the same epic objects from bd; the homepage shows the non-closed ones.
Prescription
All in
tools/web_frontend/server.pyunless noted.1. Swap the homepage panel's source. In
_serve_index(server.py:1521), replacefetch_design_task_panels(_recording_gh(gh.repo, gh), bd)with the epic fetch:fetch_bd_epics(bd)filtered to epics whosestatusis notclosed(the epic dict from_epics_by_idneedsstatusadded — one field). Render with the existingrender_bd_epics/_bd_epic_panel(server.py:805-816), not a new renderer (B10). Retitle the section "Open epics" (was "Design-doc task status"). This removes the section's entire GitHub cost —contents_dir+ the batchedcontents_filesGraphQL call — which nf-ijyi measured as the dashboard's dominant cost.2. Design-doc pointer on the epic. Convention: a line
**Design doc:** <url>in the epic's description (the markdown field Aedan named;bd list --jsonalready returnsdescription— verified live — so this costs zero extra bd calls). Extract at render time with a_design_doc_pointer(text)helper mirroring_beads_pointer(server.py:299-304) — the same pointer idea, inverted: doc→epic became epic→doc. Render it as a link in two places: -_bd_epic_panel's<h3>— aDoc ↗link styled like the existingproject-link"Board ↗" (server.py:794-795). The panel is shared by/and/beads, so both pages get it. -render_board_page's nav<h1>(server.py:1425-1443) — Aedan asked for it on the board view explicitly. No pointer → no link, silently (an epic without a design doc is a valid, expected object — that generality is the point of the ask).3. Stamp the pointer automatically for future epics (B7, C1 — a convention nobody has to remember).
_resolve_epicindesign_docs/tools/operationalize_design.py:327creates the epic on--apply; pass a body containing the**Design doc:**line (the GitHub blob URL of the doc being operationalized,https://github.com/cfsh/ninefold/blob/main/design_docs/<name>) vianflib.bd.Bd.create(body_file=...)(tools/nflib/bd.py:163 already takes it). Reuse path (already-stamped doc): leave the epic untouched — the migration below covers the live ones.4. One-off data migration (by hand or a tiny script in the PR, then discarded): both live epic descriptions are empty (verified), so wholesale
-dis safe: -bd update nf-lbh -d '**Design doc:** https://github.com/cfsh/ninefold/blob/main/design_docs/005b-affected-probe-selection.md'-bd update nf-758 -d '**Design doc:** https://github.com/cfsh/ninefold/blob/main/design_docs/012-jj-and-the-merge-pipeline.md'(nf-tvc is closed; stamping it is optional, harmless, and nice for the /beads archive view.)5. Delete the dead code, as its own mechanical PR (E3, B12). After the swap, the homepage's Contents-API path is unreachable:
fetch_design_task_panels,_recording_gh,render_design_task_panels,_design_task_panel,_beads_pointer,_row_bd_id, and thegithub_liveimports used only by it (parse_task_table,contents_dir/contents_filesusage). Check whatserver_test.py::StandaloneTests' allowlist andredeploy.py's copy list still need —github_livemay drop out ofserver.pyentirely (the/issue/<id>PR-reference path uses the localGh, notgithub_live). Do NOT fold in nf-sge6's wider github_live cleanup — that issue owns it.Not touched / adjacent
- nf-amkl (async homepage sections, in_progress) also edits
_serve_index— whoever lands second rebases; this change makes the epic section cheap enough that it may not need async at all, which is worth a comment on nf-amkl when this lands. -design_docs/tools/status.py(dev.py designs, the terminal view) keeps its doc-driven algorithm — it runs in a checkout and is out of scope here.Test plan
-
tools/web_frontend/server_test.py(unittest, the presubmit python lane):_design_doc_pointerextraction (present / absent / mid-description); homepage fetch returns only non-closed epics (FakeBd with an open + a closed epic);_bd_epic_panelrenders the doc link when the description has the pointer and no<a>when it doesn't;render_board_pagelikewise. -web_golden_test.py: extendtest_data/bd_fixture.jsonlwith a**Design doc:**-stamped epic, regenerate via./tools/web_frontend/generate_web_golden.py, review the diff before committing (the golden is expected to change — that's its job). -operationalize_design_test.py:--applyon a fresh doc creates the epic with the pointer in its description. -./presubmit.py— all lanes. - Live (E5): after redeploy,/shows 005b + 012 panels each with a working Doc link and no 011;/board/nf-758shows the doc link in the header. Screenshot or curl output in the PR body.PR shape
A stack of two (E3): PR1 behaviour — homepage swap + pointer extract/render + operationalize stamp + migration + tests. PR2 mechanical — dead-code deletion. Splitting PR1 further (render vs stamp) is the dev's call.
Open questions, with defaults
- Hide closed epics on
/beadstoo? Default: no —/beadsstays the full-workspace view (its--allis deliberate, server.py:206-211, so the closed column and the archive stay visible); the homepage is the "what's open" view. The remaining/vs/beadsdifference is then exactly "closed epics", by design, not by accident. - **Stamp the pointer into an existing epic on the--applyreuse path when missing? Default: no** — keep the tool's write surface minimal (B4); the migration covers the two live epics. - Homepage section link to the design_docs folder (nf-s8op's proposed static link)? Default: no — superseded; per-epic links are what Aedan asked for here.