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:
- Root-listing rejected — drop blanket
value=="", handle per kind (doc empty → "/", tag still non-empty). - Two parsers diverged —
parsePaneChunk(/t/ chunks) andpaneAddrFromRoute(/w/ route hrefs in the trailize pass) both had the bug; fixed one, not the other. Extracted the shared semantics intopaneAddrFromParts(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. - Tag slash check ran on the RAW value, so
tags/foo%2Fbarslipped through then unescaped tofoo/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).
R3 built — previews + backlinks + graph pane (branch feature/reading-room-r3)
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:
previewizewraps in-app doc anchors withpreview-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.
R3 deployed to the cluster (v0.1.7)
Deploy bump merged (demarkus-knowledge-system-deploy,
apps/demarkus-library/applicationset.yaml: chart targetRevision + image
tag 0.1.6 → 0.1.7). Verified v0.1.7 artifacts on ghcr before sync (image +
chart manifests both 200). ArgoCD: Synced / Healthy, rev 0.1.7; pod running
ghcr.io/latebit-io/demarkus-library:0.1.7. Broker/worlds unchanged (already ≥
0.1.36 / 0.17.18). Reading Room R1→R3 now fully live in prod. Broker-mode R3
smoke still pending (interactive OAuth) — parity is architectural (edge source
is library-internal).
Floor enrichment PR1 merged (v0.1.8)
Library hub-topology reader merged to main, v0.1.8 auto-cut, branch deleted.
Floor now reads <hub>/graph.md (mark_graph_export) ∪ R3 observed map, masked
by mark_worlds, rendering world-level edges + portal nodes; reader-only,
degrades to baseline when no hub. DEMARKUS_HUB config (default self in quic).
NOT yet deployed (cluster still 0.1.7) and NOT yet set in the cluster — edges
stay observed-only until (a) DEMARKUS_HUB=root is set in deployment.yaml and
(b) the federation agent publishes a graph export to the hub.
Next phase (Fritz approved): agent publish-graph — demarkus repo. Extend
demarkus-agent (client/cmd/demarkus-agent + client/internal/fedcrawl,
which today publishes hash indexes only via PublishToHubs) to also build the
link graph (client/graph/crawl.go exists) and publish a graph export to each
hub at /graph.md, behind a --publish-graph flag. Then deploy: bump agent
image + flip the flag, and set DEMARKUS_HUB=root + bump library to 0.1.8.
Agent publish-graph BUILT (demarkus repo, branch feature/agent-publish-graph)
The producer side of floor edges. demarkus repo, NOT committed.
client/internal/fedcrawl/crawl.go: the federation crawler now accumulates a
graph.Graph during its existing walk (recordEdges — every doc's outbound
mark:// links become edges; external links skipped), and exposes GraphExport
(reuses graphstore.New().Merge().Export() → the exact mark_graph_export
format the library parses) + PublishGraphToHubs (publishes one aggregated
/graph.md per hub — cross-server edges are the point). client/cmd/ demarkus-agent/main.go: --publish-graph flag on crawl + daemon. Test
TestCrawlerGraphExportAndPublish (cross-server edge + /graph.md publish).
Client module: build/vet/golangci-lint(v?)/test all green.
Deploy to light it all up (3 repos, in order):
- demarkus: merge agent change → new demarkus-agent chart/image version.
- deploy: bump
apps/demarkus-agentimage+chart; add--publish-graphto the agent args (it already runs--publish --per-server); the agent's root token already has/**write, so/graph.mdpublishes without new authz. - deploy: set
DEMARKUS_HUB=rootin the library values + bump library to 0.1.8 (the PR1 reader). Then the floor shows durable cross-world edges + portals. Until then library degrades to observed-map edges.
Known follow-up: host-form join — graph edges use whatever host form the link
author wrote (often portless, or KS name) vs mark_worlds URLs; worldEdges
degrades unmatched endpoints to portals. Tune host normalization when wiring
the cluster (library service/hub.go + floor.go host2name).
Gap: agent chart didn't render --publish-graph (follow-up demarkus PR)
PR #185 added the binary flag but NOT the helm chart's arg rendering, so a
deploy publishGraph: true would do nothing. Fixed on branch
feature/agent-chart-publish-graph: deploy/helm/demarkus-agent/templates/ deployment.yaml renders --publish-graph when .Values.publishGraph;
values.yaml adds publishGraph: false; 2 unittest cases added. helm lint +
helm unittest green (23 tests). Chart version is set by the release pipeline
(source Chart.yaml is stale 0.1.0), so no manual bump.
Remaining to light up floor edges (in order):
- ✓ client 0.12.42 — binary flag (#185).
- this chart PR → merge → new demarkus-agent chart version (verify on ghcr).
- deploy repo: bump
apps/demarkus-agentimage+chart to the new version + setpublishGraph: true; setDEMARKUS_HUB=rootin library values + bump library image/chart to 0.1.8. Then ArgoCD syncs → agent publishes /graph.md → floor renders durable cross-world edges + portals.