soul.demarkus.io:6309/demarkus-library/journal/2026-06-13.md/v3 draft reader meta

2026-06-13 — floor live + trail root-listing fix

Floor in production

R1+R2+floor rolled to the cluster (server 0.17.18, broker 0.1.36, library 0.1.5). The worlds bump needed a one-time orphan-delete recreate (root + world-a) — caused by the chart stamping churning version labels on the immutable volumeClaimTemplates; fixed at the chart layer (#184: VCT uses selectorLabels), so future server bumps are clean. Full rollout detail in journal 2026-06-12.

Trail root-listing bug (library PR #16 → v0.1.6)

Fritz hit {"message":"malformed trail"} on /t/u/~/world-a/d/ — the floor's world-node link (opens a world's stacks = root listing, path "/"). Root cause: the trail parser rejected any pane chunk with an empty value (if !ok || value == ""), but the world-root listing legitimately encodes as <world>/d/ (doc-kind, empty value → "/"). Predated the floor — every root-listing link was broken since R2; the round-trip test only covered a non-empty subdir (/plans/) and missed root.

Three CodeRabbit findings, all real, fixed in sequence:

  1. Root-listing rejected — drop blanket value=="", handle per kind (doc empty → "/", tag still non-empty).
  2. Two parsers diverged — parsePaneChunk (/t/ chunks) and paneAddrFromRoute (/w/ route hrefs in the trailize pass) both had the bug; fixed one, not the other. Extracted the shared semantics into paneAddrFromParts(world, kind, value) — single source of truth; the two wrappers keep only their input-specific framing (chunk-split+floor vs URL-parse+fragment). Divergence now structurally impossible.
  3. Tag slash check ran on the RAW value, so tags/foo%2Fbar slipped through then unescaped to foo/bar. Moved empty/slash checks AFTER PathUnescape (in the shared helper → fixed for both decoders at once).

Pattern worth keeping: two parsers for the same logical address = a divergence bug waiting to happen. Share the semantics, not the syntax framing. Deploy bump library 0.1.5 → 0.1.6 staged (deploy fix/library-0.1.6; clean Deployment roll, no recreate).

The last planned Reading Room phase, built and verified live (QUIC against soul.demarkus.io). Not committed — awaiting Fritz + CodeRabbit per PR.

Load-bearing decision (recorded in plans/reading-room.md §"R3 build decisions"): edge source = render-time observed-links map. rewriteLinks already resolves every link to a (world,path); capturing those edges is free and transport-symmetric (library-internal, no broker store → decision 16 holds). Rejected: broker graph store (broker-only), library-side crawl (over-built). Honest limit: only edges among docs rendered this pod's lifetime; hub-sourced topology (addendum 2) is the durable upgrade layered in front later. Backlinks/graph fill in as the room is read.

Architecture: domain.Ref{World,Path} + domain.Neighborhood. Edge store is an internal service.linkGraph (forward+reverse maps under a mutex), mirroring floorCache — no new port/adapter/wiring (NewReadingService signature unchanged). Inbound port gains RecordLinks / Backlinks / Neighborhood. rewriteLinks now returns (html, []Ref); rewriteHref returns (route, Ref, isDoc). Web records edges after rendering doc panes (resolution stays in web, store in core).

Pieces:

  • Hover previews: previewize wraps in-app doc anchors with preview-host/preview-card + htmx (mouseenter delay:300ms once → /w/:world/preview/* fragment). Pure htmx+CSS, no JS (ADR 0003). One component serves body links AND backlink entries. Snippet = first paragraph, served from ReadCached (budget-safe). mouseenter overrides click trigger so hx-boost nav still works.
  • Backlinks margin block ("referenced by") in shared doc-meta — trail URLs on the canvas, /w/ permalinks on the single-doc view. Empty ⇒ absent.
  • Graph pane: new kind <world>/g/<path> via paneAddrFromParts (shared, not duplicated). Route /w/:world/g/*. SSR SVG (graph.go) — center + backlinks (left, blue) + outbound (right, green), each node a plain continuing the trail. Canvas island stays unspent. Margin "graph" affordance opens it.

Docs (decision 14): docs/trail-format.md + in-universe mark://soul.demarkus.io/.well-known/library/trails.md (v3) both updated with the g kind — kept in sync.

Verified live (QUIC): index.md → 43 wrapped body links; preview fragment renders title/status/snippet/mark; reading index.md recorded index→architecture so architecture.md shows index.md under "referenced by" (trail-linked); graph pane SVG renders, nodes continue the trail; /w/g permalink uses /w/ routes. go vet + full suite green; gofmt clean.

Open / observed: broker-mode acceptance is architecturally guaranteed (edge source is library-internal, no broker-only path) but not run here (needs interactive OAuth via dev.env) — Fritz to smoke. Visual: a hub doc with ~40 outbound links crowds the graph's right arc (aggregation/cap is future polish, same family as the floor's satellite crowding note). Suggested PR slices: (A) edge source + backlinks, (B) previews, (C) graph pane + docs.

R3 merged (PR #17, v0.1.7)

Merged to main, v0.1.7 auto-cut, branch deleted. CodeRabbit comments addressed pre-merge: (1) pre-commit.sh now pins golangci-lint to CI's v2.9.0 (hook only checked existence — a mismatched local binary could pass while CI fails); (2) clarified the //nolint:gosec wording on the canvas content; (3) fixed staticcheck QF1002 in graph.go (tagged switch i). Gate (./pre-commit.sh) green. Reading Room is code-complete on main. Cluster still at library 0.1.6 — R3 deploy (ArgoCD bump to v0.1.7) is the next session's first task. pre-commit.sh is the standing phase/task gate.

trail
  1. soul.demarkus.io:6309 v3