The Reading Room — design notes (draft)
Reading-first frontend for the universe library. Desktop only. Markdown-native, no hosted media, agent-maintained content, navigation by tag and link rather than search. Status: draft — under discussion 2026-06-12 (Fritz + Fable). Will be locked as ADR 0005 once the open decisions below settle.
Thesis
Most knowledge management feels exhausting because every click is a context amputation. Back/forward is a one-dimensional history: a single cursor on a line. Reading knowledge is not linear — it branches. You are on RFC A, it references B and C, and you want to return to A's argument with B still in view. A linear history cannot hold a branch, so the reader rebuilds context in their head on every pop. That reconstruction is the tax.
The Reading Room makes context spatial instead of temporal. The trail is laid out on screen; the reader never reconstructs where they were — they look left.
Governing principle: never make the reader hold in their head what the layout could hold for them. Panes hold the path. The margin holds the document's trust signals and the author's asides. The body holds only the argument.
Influences
- Andy Matuschak's working notes — stacked panes; a link opens a column to the right rather than navigating away.
- Tufte CSS — ~60ch reading column, wide working margin, sidenotes not footnotes, off-white/off-black, booktabs tables.
- Gwern "Sidenotes In Web Design" — margin-note rationale + implementation survey.
- adoc-studio Tufte layout — CSS-variable two-part layout (~650px body, ~250px margin).
- matt.roam.garden — hover link previews, backlinks, sliding layered notes.
- Maggie Appleton's garden — legible note maturity; status as first-class signal.
Pane mechanics
The invariant: the stack is always a single path, root → focus. Not a browsing history — the reasoning chain currently in view. Every rule defends this.
Link resolution (core rule). Click a link in pane N:
- Truncate the stack to N (discard everything right of it).
- Append the target as pane N+1.
- Exception: target already in the stack → don't duplicate; scroll it into view and focus it (handles circular references for free).
Truncation is the counterintuitive half: at A→B→C, returning to B to follow a different link abandons C — it's off the path, so it doesn't survive. Keeping it would turn the stack back into temporal history. C is one click away in B.
Active-link highlighting. Each pane marks which outbound link is open to its right. The stack is self-describing — the highlighted links ARE the breadcrumb, spatial and in-context.
Overflow. Panes are ~950px (60ch + margin); 1.5–2 full panes per screen. Overflow is the normal state, not an edge case. Never evict:
- Left panes collapse to a thin vertical spine (title + status badge).
- Focused pane + immediate parent render full; older panes are spines.
- Clicking a spine expands + focuses it — navigation, not a new branch; no truncation.
- New panes open at the right; canvas auto-scrolls to them.
- Only the focused pane shows its full margin; parents render body-only. The margin is an attribute of attention. (This is also the width math: 950 focused + ~700 body-only parent = 1650 fits 1920.)
Locked decisions:
- Canvas scrolls to reveal panes; panes do NOT resize to fit (resize produces unreadable columns past three hops). Shapes the layout engine.
- External (non-demarkus) links leave the graph — browser navigation, never a pane. The stack stays pure to the knowledge graph.
overscroll-behavior-x: containon the canvas — macOS two-finger swipe at the scroll edge must not trigger history-back mid-trail.
Trail sharing: the URL encodes the whole stack (ordered doc ids + focus), so sharing a link shares the reasoning chain, reconstructed exactly.
The margin
Two content kinds with opposite behaviors — don't mix:
Position-anchored (flows with text): sidenotes — author/agent written, anchored to a body point, vertically aligned to the referent. The Tufte core.
Document-level (pinned, position: sticky at margin top):
- Status — the trust signal. Unmissable badge; draft vs accepted look different at a glance (possibly muted body for drafts). The thing enterprise KM never does.
- Tags — clickable, the lateral-nav exit.
- Provenance — last touched, by which agent, when. Answers "can I trust this is current" for agent-written content.
- Backlinks ("referenced by") — the link graph made visible, each with a hover preview card (title + opening lines). The same preview component serves outbound body links — build once, use both directions.
Discipline: an empty margin is correct. Status, tags, provenance, backlinks, and the sidenotes the author actually wrote. Nothing else earns the space. A stuffed margin is a sidebar in costume.
What NOT to build
- No search box. Navigation is tags, links, backlinks. (Capability survives:
a tag click is
mark_lookupwith a tag filter rendered as a tag page — same backend, right affordance.) - No persistent sidebar file tree.
- No comments/reactions/collab cruft. Reading room, not workspace.
- No rich-text editing. Markdown in, rendered out (Phase 3 cataloging desk is markdown-source editing, not WYSIWYG).
Visual baseline
Body ~650px / 60ch · margin ~250px · off-white/off-black, color reserved for signal (status) · booktabs tables (three horizontal rules, no verticals) · mermaid + KaTeX (already shipped, v0.1.2 islands).
Feasibility (htmx-hard, ADR 0003) — assessed 2026-06-12, VIABLE
The unlock: the trail is server state encoded in the URL (/t/...), so
ADR 0003 rule 3 (no client state) holds. The server renders every link in
pane N already carrying its post-click trail URL (prefix-to-N + target) —
truncation, dedup, spine-expand, and active-link highlight all cost zero
client logic; the link IS the state transition. Browser back/forward becomes
temporal history OVER spatial states for free.
- Panes/spines/focus/pinned margin: server-rendered + CSS only.
- Hover previews: pure htmx (
hx-trigger="mouseenter delay:300ms once"→ server fragment). - Sidenotes: standard footnote syntax in markdown; goldmark AST transform inlines each note at its reference as a margin span; Tufte CSS floats it. Degrades to footnotes. Agent-writable.
- Only new JS in the entire design: ~5 lines of scroll-focused-pane-into-view
after htmx settle (htmx
show:is vertical-biased). Rides islands.js.
Hard requirement discovered: rendered-HTML cache. Naive full-canvas re-render = N world fetches per click; a 6-pane trail against the broker's 30/min budget = ~2 clicks/min then 429s. The design therefore REQUIRES a server-side rendered-HTML cache keyed (world, path, version) — a trail click fetches only the NEW document; the rest re-renders from cache (active-link highlight is a cheap post-process). demarkus versioning gives invalidation. With it, a click costs one read — same as today.
Status/maturity channel decision: body frontmatter is what we STRIP;
demarkus carries metadata out of band. Maturity rides metadata tags as an
axis (status:draft, status:accepted) — matches knowledge-system policy
axes, write-gate enforceable. Provenance comes from response metadata
(modified/version/agent) we already receive.
Backlinks dependency: the one margin item without a data source — library gateway has no backlinks verb; broker graph store is ephemeral (per-pod, crawl-populated). Phase behind the rest.
Phasing
- R1 — the Tufte pane (single doc): column + margin, pinned metadata block (status badge, tags, provenance), sidenote transform, booktabs styling, remove search box, add tag pages. Ships alone — the look + trust model.
- R2 — the trail engine: stack URL scheme, canvas, truncate/dedup/focus, spines, active-link highlight, rendered-HTML cache. Layout-shaping decisions lock here.
- R3 — previews + backlinks: hover cards both directions; backlinks when the data source lands.
- Universe view (mark_worlds floor) = the natural trail root: pane zero.
Open decisions (to settle before ADR 0005)
- Confirm killing the nav search box in R1 (capability survives as tag pages).
- Status-axis tag vocabulary (
status:draft|wip|accepted|archived?) and whether the write gate should require the axis. - Trail URL shape: path segments vs query param; stack depth cap (~10?).
- R1-before-R2 phasing vs straight to the trail engine.
- Backlinks data source: wait for broker graph persistence, or library-side crawl of the home world?
One-line synthesis
A Tufte-styled reading column with a working margin (sidenotes, backlinks, provenance), stacked panes for trail preservation, status as trust signal, lateral movement by tag. No search, no tree, no media. Everything in service of reading and trusting, nothing in service of the CMS. Most KM is the inverse.
Decisions settled 2026-06-12 (→ ADR 0005)
All five open decisions resolved; ADR 0005 locks the invariants. Key syntheses from the hash-out:
- Catalog-as-pane (decision 1): no global search box, but the card catalog joins the trail as a pane — tag clicks open it, and a filter input INSIDE the catalog pane keeps lookup easy without a bar to stare at. Search results live on the path.
- Frontmatter rendered friendly (decision 2): parse the stripped fence
and render keys into the margin's document-properties block. Metadata
channel stays authoritative; status badge reads metadata tag axis first,
frontmatter
status:fallback, absent ⇒ draft. - Graph-as-pane (decision 5, Fritz's "add into context, not break out"): click/hotkey in the margin opens the document's graph neighborhood AS A PANE; clicking a node continues the trail. Walking the graph IS building a trail — one model. The universe view is the same component at maximum zoom: the whole-universe graph as pane zero / the floor.
- Universe view needs research before building — what a special universe-scale graph view looks like (rendering approach, zoom levels, what aggregates at scale). Likely exercises ADR 0003's pre-agreed canvas island. Run research alongside R1/R2.
Agent symmetry (added 2026-06-12, ADR 0005 amendment — decisions 11–15)
Fritz: "we can't forget about the agent aspect." The room is human-facing; the universe is agent-maintained. Principles now locked in ADR 0005:
- Projection, never source (11): no HTML-only information, ever. Every affordance ⇄ an MCP-readable/writable channel.
- Escape to protocol (12): canonical mark:// + raw source in every pane's margin.
- Authoring contract published in-universe (13): style doc agents fetch; defines footnote→sidenote, status axis, alerts/mermaid/KaTeX, tag conventions. Ships with R1.
- Trail format documented + constructible (14): agents mint trails (hand humans their reasoning chain as a URL that reconstructs on screen) and parse them (load a human's trail as context). The trail is the serialized reading-context object both species share. Docs ship with R2.
- Librarian consumes the trail (15): Phase 4 librarian joins the canvas as a pane with the current stack as its context — the URL is the context spec.
The sleeper feature is 14: agent→human reasoning handoff as a trail URL.
R2 build decisions (2026-06-12, locked before building — Fable)
R1 shipped (PR #13, merged). These settle the two layout-shaping designs ADR 0005 leaves to R2.
Trail URL format (decision 1 + 14 concretized)
/t/ + one pane chunk per pane, separated by a literal ~ segment;
focus as a query param:
/t/<world>/d/<path>~<world>/tags/<tag>~<world>/d/<path>?focus=1
└────── pane 0 ──────┘ └──── pane 1 ───┘ └── pane 2 ──┘
- A pane chunk is exactly the tail of the pane's standalone
/w/route (world +d/<path>|tags/<tag>) — one address language everywhere; agents mint trails by concatenating route tails they already know. ~is a reserved path segment (never a world, never a path segment).- Raw
/inside chunks — no %2F anywhere (ingress-proof). Echo route:/t/*with library-side chunk splitting. ?focus=<0-based index>, omitted ⇒ last pane. Out of range ⇒ clamped.- Depth cap 10: an append that would exceed drops pane 0 (truncate-left, documented, deterministic).
- Click semantics (server-rendered into every link): link in pane N →
truncate to panes 0..N, append target, focus it; target already on the
path → same stack, focus jumps to it (dedup); spine/header click → same
stack,
focus=i. External links untouched (leave the graph via browser). /w/routes stay as-is: the single-pane permalink + fragment renderer (and the projection escape's stable address)./renders the default doc as a one-pane trail.
Cache policy (decision 9 concretized): focused-live, parents-cached
Key insight: the freshness that matters is where attention is. Every trail
render fetches the focused pane live (and refreshes its cache entry);
all other panes are read-through from the rendered-document cache
(kind-prefixed key d:/tags: + world + path). Consequences:
- Every click costs exactly one world read — append (new doc), spine expand, dedup focus, back/forward: all 1 read. Broker budget (~30/min) supports ~30 clicks/min/reader regardless of trail depth.
- Unfocused panes can be stale only in the sense that they show what the reader was already looking at — re-focusing refreshes them.
- Cold shared trail (agent-minted link): N reads once, then 1/click.
- Cache stores the rendered
domain.Document(markdown→HTML, margin data), NOT trail-ized links: per-request post-process rewrites pane hrefs to their post-click trail URLs + marks the active link (the cheap pass ADR 0005 anticipated). LRU, in-memory, per-pod (sessions are already per-pod; chart stays replicaCount 1).
Universe-view research (2026-06-13, Fable — recorded before building)
The universe view = the graph pane at maximum zoom = pane zero / the floor (ADR 0005 decision 4). Research findings; design to validate with Fritz before R3/universe build.
Data sources (decision 11 audit: every layer is MCP-derivable)
| layer | source | cost | availability |
|---|---|---|---|
| worlds | mark_worlds (to build — broker tool over authorizedWorlds, same data /me/install already serves on REST) |
1 call | broker only; quic-mode universe IS the home world (decision 16 degradation) |
| nodes per world | mark_lookup scope /, high limit — path, title, importance, tags |
1 call/world, cacheable | both transports |
| tree shape | mark_list (dirs as clusters) |
1 call/dir | both |
| edges | broker graph store (mark_graph crawl → mark_backlinks/mark_graph_export) — ephemeral, per-pod, crawl-populated by design (broker is a wire-shape adapter; persistent bucket store parked) |
crawl cost, store may be empty | broker; quic worlds have no store — library-side crawl or none |
Key insight: the catalog is the cheap universe. Importance gives node weight, tags give clustering, path gives hierarchy — one lookup per world, no link crawl. Edges are an enrichment, not the skeleton: the floor degrades gracefully to an importance-weighted cluster map when the graph store is cold. This resolves the "backlinks/graph must not assume the broker store" clause of decision 16.
Rendering approach — SSR SVG first, canvas island only if scale demands
ADR 0003 pre-agrees a canvas island as a concession, but research says we may not need to spend it:
- Scale reality: a knowledge system is a handful of worlds × O(10²–10³) catalogued docs. After aggregation (below) the floor renders tens of nodes, a world view low hundreds. That is comfortably server-rendered SVG territory — zero new JS, cacheable in the DocumentCache like any pane, degrades to markup, and stays projection-pure (the same data an agent gets from mark_worlds + mark_lookup).
- Layout must be deterministic server-side (no client force simulation): radial/orbit layout — worlds as anchors, docs orbiting their world clustered by top-level directory (or tag), radius ∝ rank, node size ∝ importance, status as stroke color (the trust signal again). Deterministic layout also means stable across renders → diffable, cacheable, shareable.
- Hover = title tooltip (SVG , free); click = the node's pane appended to the trail (plain wrapping SVG node — htmx-hard holds).
- The canvas island stays the documented escalation path: trigger = a world whose post-aggregation node count makes SVG DOM weight a real problem (~thousands of visible nodes), or a future force/physics want. Until a deployment hits that, SVG ships.
Zoom/aggregation levels (one component, zoom = which level renders)
- Floor (pane zero): worlds as large nodes (size = catalog count, label + world badge); each world shows its top-importance docs (importance ≥ 0.8 — hubs/ADRs per the tagging convention) as satellite nodes. Everything else aggregates into per-world "+N docs" rings.
- World view: one world's catalog — clusters by top-level dir, labeled nodes for the top ~50 by importance, aggregate bubbles ("plans/ +12") that click into the listing pane (the stacks — already a pane kind!). Edges drawn only if the graph store has them.
- Neighborhood (R3 graph pane): one doc + direct links/backlinks — the store-backed (or on-demand mark_graph depth-1) view.
Zoom transitions are clicks: aggregate bubble → listing pane or deeper graph pane — every transition continues the trail (decision 4), no client zoom state (ADR 0003).
Pane address
New pane kind in the trail vocabulary: <world>/g/<path> (neighborhood,
R3) and the floor as g at universe scope — exact chunk shape to lock
when building (must stay agent-constructible; trails doc gets updated,
decision 14). Floor in quic mode = world view of the home world.
Build order proposed
mark_worldsbroker tool (demarkus repo) — small, unblocks the floor.- Floor + world view (SSR SVG over mark_worlds + lookup) as a pane — could ship before R3's neighborhood pane since it doesn't need edges.
- R3 neighborhood pane + backlinks margin block when the edge-source decision lands (broker store vs library-side crawl).
Addendum: the non-brokered universe (Fritz + Fable, 2026-06-13)
The two postures define "universe" differently, and the floor should honor that rather than fake symmetry:
- Brokered = intensional. Config is the authoritative world list;
mark_worldsenumerates it. Worlds appear on the floor whether or not anything links to them. - Non-brokered = extensional. No registry exists; the universe is the
home world plus whatever it links to. Discovery is traversal: external
mark:// links are the only map. (
mark_graphalready records external links without following them — cross-host edges are exactly this data.)
Quic-mode floor design: home-world cluster at the center (catalog-based, identical to the brokered per-world view), externally-linked demarkus hosts as small portal nodes at the rim — rendered only when edge data exists, absent otherwise. No links observed ⇒ the floor is just the world; that degradation is honest, not a gap. No mark_worlds analogue needed — nothing to enumerate.
Cheap edge source for portals (no crawl): the library parses every link in every document it renders (rewriteLinks). Accumulating observed cross-world targets as a side effect of normal reading yields an observed-links map for free — per-pod and ephemeral, same posture as the broker's graph store. Candidate for the floor build.