2026-06-14 — world-view zoom level
Continues 2026-06-13. Floor enrichment was shipped + verified live (library 0.1.10, broker 0.1.37, worlds 0.17.19, agent 0.12.43). Fritz picked the next phase: world-view zoom level (the deferred zoom level 2). Demo links in world-a/index.md kept (legitimate related-links footer; floor edges stay live).
World-view zoom BUILT (branch feature/world-view-zoom)
The floor one zoom in: a world's catalog as top-level directory clusters, each a labeled hub with its top-importance docs orbiting + a "+N more" aggregate that opens the dir's listing pane. SSR SVG (canvas island still unspent). Decision recorded in plans/reading-room.md §"World-view zoom level". Gate green (./pre-commit.sh). NOT committed (Fritz commits).
Load-bearing decision: world map = the floor kind u WITH a World set. No
new pane kind — bare u = universe floor (unchanged); <world>/u/ = that
world's map (the same component, one zoom in; ADR 0005 decision 4). Parallels
<world>/d/ and <world>/g/<path> (trailing slash, empty value).
paneAddrFromParts gains a u case; paneChunk emits u for World=="" and
<world>/u/ otherwise. Single source of truth holds — both decoders share it.
Reached by: the floor's world cluster node now zooms to <world>/u/
(was <world>/d/ stacks). Stacks stay one click away via the world map's dir
aggregates. Plus a margin "map" affordance (next to "graph") so the world view
is reachable from any doc, not only the floor.
Architecture (hexagonal):
domain.WorldMap{World, Clusters, Edges}+WorldCluster{Dir, Docs, More, ListPath}.service/worldmap.go:WorldMap/WorldMapCached(per-world cache mirroring floorCache; focused-live policy),worldClusters(group by top-level dir, root first then alpha; per-cluster top-N labeled, rest → More),topDir,intraWorldEdges(observed map ∪ hub graph, host→name joined, filtered to labeled nodes only — an edge can't point at an undrawn node). Catalog read is the page; world identity + edges are best-effort enrichment.- Inbound port grows
WorldMap/WorldMapCached. web/worldmap.go:WorldMapPage(permalink/w/:world/u),worldMapPaneView(trail-aware),worldMapSVG(grid of dir clusters, two-pass: positions → edges → nodes). Node hrefs continue the trail (doc →<world>/d/<path>, hub/aggregate →<world>/d/<dir>/listing).- Edges: the floor drops intra-world edges (worldEdges skips same-world); the world map is where they belong. Drawn only between two labeled nodes.
Tests: service (cluster grouping + aggregate, topDir, intraWorldEdges join/
filter, assemble+cache, best-effort enrichment); web (SVG nodes/edges/aggregate/
escape/empty, focused-live, permalink uses /w/ routes, error tombstone, trail
round-trip for bare u + <world>/u/). Floor test updated (world node →
<world>/u/).
Docs (decision 14): docs/trail-format.md updated with the <world>/u/ chunk.
The in-universe trails.md twin updates at DEPLOY time (it describes live
behavior) — same as last phase.
Files
- new: internal/core/service/worldmap.go (+_test), internal/adapter/inbound/ web/worldmap.go (+_test)
- edited: domain/document.go, core/port/port.go, core/service/reading.go; web/trail.go, trail_handlers.go, floor.go, reading_handlers.go, reading_routes.go, templates/content.html, templates/page.html; floor_test.go, trail_test.go, reading_handlers_test.go; docs/trail-format.md
Next (after Fritz + CodeRabbit)
- Commit + PR; address review; merge → library auto-cut (will be 0.1.11).
- Deploy: bump apps/demarkus-library applicationset.yaml targetRevision + image.tag to the new version. No broker/worlds/agent change (reader-only, reuses mark_worlds + lookup). DEMARKUS_HUB already = root.
- Update in-universe mark://soul.demarkus.io/.well-known/library/trails.md with
the
<world>/u/chunk (kept in sync with docs/trail-format.md). - Browser smoke at library.knowledge.demarkus.io: floor → click a world → world map renders (dir clusters + aggregates); a doc margin "map" link opens it; aggregate → listing pane.
World-view zoom MERGED + DEPLOYED (v0.1.11)
PR #21 merged to main, v0.1.11 auto-cut. One CodeRabbit comment addressed
pre-merge: reject a world-map trail chunk that carries a value
(<world>/u/junk → 400, was silently dropping it) + regression test.
Deploy: apps/demarkus-library targetRevision + image.tag 0.1.10 → 0.1.11
(image + chart verified on ghcr). ArgoCD Synced / Healthy, rev 0.1.11; pod
running 0.1.11. Reader-only — no broker/worlds/agent change. Routes wired live
(/w/root/u, /t/root/u/ → 302 login, not 404).
In-universe mark://soul.demarkus.io/.well-known/library/trails.md updated to
v4 with the <world>/u/ chunk (in sync with docs/trail-format.md, decision 14).
Remaining: human browser smoke at library.knowledge.demarkus.io — floor → click a world → map renders (dir clusters + "+N more" aggregates); doc margin "map" link opens it; aggregate → listing pane.
Phase 3 cataloging desk — edit slice BUILT (branch feature/cataloging-desk-edit)
Fritz: "we need some write option to complete the narrative." Built the first write slice — edit an existing document (read → edit with live preview → publish → re-read). The room could only read; this closes the loop. Plan: /plans/phase-3-cataloging-desk.md. Gate green. NOT committed (Fritz commits).
Decisions taken (per the plan's D1–D5):
- D1 htmx-pure editor: a
<textarea>whose input hx-posts to a server-rendered preview fragment (POST /w/:world/preview) — same renderer the reader sees, zero JS, server authoritative. CodeMirror island NOT spent. - D2 edit is a focused-pane MODE, not a trail chunk: a margin "edit"
affordance → dedicated editor page (
/w/:world/edit/<path>), hx-boost off so save is a real POST→303 back to the doc. Drafts never enter the shareable trail URL. - D3 metadata as form fields: title / tags / status-picker / importance map
to the
mark_publishmetadata object; body is markdown only. Status folds into thestatus:axis tag. Structurally impossible to write a---body fence (the bug R1 papered over). - D4 expected_version + conflict: the form carries the fetched version;
on_conflict: "fail"→domain.ErrConflict→ re-render with a reload banner, submitted text preserved (never silent loss). Structural-merge flow deferred to a later slice. - D5 authz: edit affordance + write gated on an authenticated session (POC posture); per-path write-collections deferred to Phase 2 coupling.
Architecture (hexagonal):
domain:PublishMeta,EditDraft,ErrConflict,ErrWriteUnsupported.- Outbound port
WorldGateway.Publish— broker adapter callsmark_publish(metadata object, on_conflict fail, parses new version, maps conflict/unauth); QUIC adapter returnsErrWriteUnsupported(read-only client, honest degradation); federated routes by world shape. - Inbound port:
EditDraft/Preview/Publish;service/edit.go(Publish re-reads live to refresh the cache — focused-live). - Web:
edit.go(EditForm / EditPreview / SaveEdit + assembleTags + error mapping), routes,templates/edit.html, edit CSS, "edit" margin affordance (paneVM + page VM, gated on authed).
Tests: service (EditDraft split, Publish re-read + conflict, Preview); broker (Publish args/metadata/version, conflict + no-bearer mapping); web (form prefill, save→303 + captured meta/tags-with-status, conflict 409 + banner + preserved body, preview fragment, affordance auth-gating).
Slice boundaries (honest): edit-existing only — no create-new doc yet; no mark_append affordance yet; conflict is reload-not-merge; QUIC writes unsupported (broker-mode only). Follow-up flagged: no CSRF token on the write POST — matches the existing logout form's posture; acceptable for POC, revisit for the public face (Phase 5).
Next: Fritz + CodeRabbit → merge (library 0.1.12) → deploy bump.
Phase 3 edit slice MERGED + DEPLOYED (v0.1.12)
PR #22 merged, v0.1.12 auto-cut. CodeRabbit comments addressed pre-merge — all
the same theme (don't swallow errors / map them honestly):
- web SaveEdit: malformed version → 400 (was silently 0 = create sentinel).
- service EditDraft: unreadable version metadata → propagate error (not 0).
- editErrorStatus: ErrNotFound → 404; default (unmapped) → 502 (was 200). Each with a regression test.
Deploy: apps/demarkus-library 0.1.11 → 0.1.12 (image + chart verified on
ghcr). ArgoCD Synced / Healthy, rev 0.1.12; pod on 0.1.12, ready. Routes
wired live: /w/root/edit/index.md → 302 (login), /w/root/preview → 405
(POST-only). Reader+writer now; no broker/worlds/agent change.
Remaining: first live write smoke in a browser (needs an authed session):
open a doc → edit → change text + metadata → save → confirm it round-trips and
the stored source has NO --- fence (metadata stayed out of band). Then decide
the next slice (create-new doc / mark_append affordance / conflict merge flow).
Phase 3 — create-new-document slice BUILT (branch feature/cataloging-desk-create)
Edit-slice smoke passed live (Fritz). Built the second write slice: create a
new document. Reuses the publish path with expected_version: 0 (the create
sentinel) — no new outbound port work. Gate green. NOT committed.
- Affordance: a margin "new" link beside "edit" (page + trail pane, auth-
gated),
?dir=<current folder>so a created doc lands beside the one being read.dirOf(path)derives the folder. - Create-mode editor: the same
edit.htmltemplate with aCreateflag — an editable path field (placeholder/notes/idea.md), no version field (0 implicit), action/w/:world/new, button "create", cancel → world map. - Handlers:
NewForm(GET /w/:world/new[?dir=]) renders create mode;CreateDoc(POST) validates the path, publishes at version 0, 303s to the new doc.normalizeNewPathrejects empty / root / directory (trailing slash) / empty-segment paths → 400 with guidance. A version-0 conflict means the path is taken → 409 "already exists at that path" (create-specific message viacreateErrorMessage). - Tests: create-mode form (path field, no version, "create" button), publish-at-0 + redirect + status:draft tag fold, directory-path 400, path-exists 409, affordance auth-gating + folder scoping, normalizeNewPath + dirOf unit tables.
Slice boundaries: create reuses edit's preview + metadata + conflict plumbing; no world-map/empty-world "new" entry yet (margin "new" needs a doc open — fine for now, follow-up for first-doc-in-empty-world); no mark_append affordance yet; conflict still reload-not-merge. Next: Fritz + CodeRabbit → merge (0.1.13) → deploy.
Phase 3 — create-new-document MERGED + DEPLOYED (v0.1.13)
PR #23 merged, v0.1.13 auto-cut. Deploy: apps/demarkus-library 0.1.12 →
0.1.13 (image + chart verified on ghcr). ArgoCD Synced / Healthy, rev
0.1.13; pod ready on 0.1.13; /w/root/new wired (302 → login). Reader-only
deploy footprint. Cataloging desk now does edit + create.
Remaining Phase 3 slices: mark_append affordance; world-map "new" entry (create first doc in an empty world); conflict merge-candidate flow; write-collections authz (Phase 2 coupling). Open: human browser create-smoke (read → new → set path → create → lands on new doc).
Phase 3 — append slice BUILT (branch feature/cataloging-desk-append)
Third write slice: append to a document — the lightweight "add to" for
journal/log docs. Confirmed the broker's mark_append takes url+body, with
expected_version optional (0 → auto-resolve via VERSIONS), so append needs no
version juggling: additive, low-conflict, metadata preserved. Gate green.
- Outbound port
WorldGateway.Append(world,path,body)— broker calls mark_append (url+body, omits expected_version); QUIC ErrWriteUnsupported; federated routes. InboundReadingService.Append→ append then re-read live (focused-live). - Web: margin "append" affordance (beside edit/new); the editor in
Appendmode = body-only (no metadata, no path, no version), action /w/:world/append/ , button "append", a note that it only adds to the end. Empty body → 400 ("Nothing to append"). Reuses the live preview. - Tests: broker (args omit expected_version, version parse, no-bearer); service (append→re-read, error propagation); web (body-only form, add+303, empty 400, auth-gating).
Gotcha (test): asserting absence of name="title" false-matched the CSS
selector .edit-meta input[name="title"] in ; tightened to match input
markup (name="title" value=). Pattern: substring asserts on rendered HTML can
catch CSS — match the element, not the attribute name.
Cataloging desk now: edit + create + append. Remaining Phase 3: world-map "new" for empty worlds; conflict merge-candidate flow; write-collections authz (Phase 2). Next: Fritz + CodeRabbit → merge (0.1.14) → deploy.