soul.demarkus.io:6309/demarkus-library/plans/phase-3-cataloging-desk.md/v1 draft reader meta

Phase 3 — Cataloging Desk (plan)

Status: draft — recorded 2026-06-14 (Fritz + Opus). Important. Not yet scheduled, but flagged high among the forward phases. This is the detailed plan behind the roadmap's "Phase 3 cataloging desk" bullet.

One-line

Add the write side: create and edit documents in markdown source from the Reading Room — a projection of the broker's write protocol (mark_publish / mark_append), the same way reads project the read protocol (ADR 0005 decision 11). Markdown in, rendered out. No WYSIWYG, no rich text, no comments.

Why (the gap)

The room is read-only today. The library WorldGateway port has read verbs only (Fetch/List/Versions/Lookup/Worlds); there is no write path through the hexagon. The write protocol already exists at the broker — this phase is the room's surface onto it, so a human edits a document the same way an agent does, and both pass through the same authz + write gate.

What the broker already gives us (verified 2026-06-14)

  • Writes use "a long-lived per-world token the broker provisions on first write" (broker MCPConfig doc). The write token's op is hardcoded to ["publish"] (#163) and its Paths are world-config globs.
  • mark_publish carries metadata OUT OF BAND — title, tags, importance in a metadata object; the body is pure markdown. A body that opens with a --- frontmatter fence is stored as literal content (the recurring publisher bug). expected_version is required (0 to create); on_conflict defaults to a structural-merge candidate the caller re-verifies.
  • mark_append appends to a body and CANNOT carry metadata — tags/importance stay frozen at the doc's last publish; a materially new subject means re-publish with extended tags.
  • A write-time gate enforces tags/importance at the system's chosen severity (warn / block / ask). The desk should satisfy it on the first submit.

Design decisions (recommendations — settle before building)

D1. Editor surface — RECOMMEND htmx-pure textarea + live server preview first

The room is htmx-hard, no client state (ADR 0003). An editor is the natural place to break that, so be deliberate:

  • Recommended: a plain <textarea> whose input hx-posts to a server-rendered preview fragment (debounced) — the same markdown→HTML pipeline the reader already uses, so WYSIWYG-grade fidelity with ZERO new JS and the server as the single source of truth. True to ADR 0003.
  • Escalation (documented, not first): a CodeMirror island for syntax highlighting / large docs — a bounded JS island like mermaid/KaTeX, client state confined to the edit buffer, server still authoritative on submit. Spend this island only if the textarea proves inadequate.

The roadmap's "CodeMirror island" phrasing is the escalation, not the default.

D2. Editing is a focused-pane MODE, not a trail chunk — RECOMMEND

A trail URL is a shareable reading context; an unsaved draft is client state that has no place in a shared, reconstructable URL. So:

  • Editing is an action on the focused document pane (an edit mode), reached from a margin "edit" affordance — NOT a new pane kind in the trail codec.
  • On save, the room re-reads the doc live (focused-live policy) and the trail is unchanged. A fresh-create flow lands the new doc as the focused pane.
  • This keeps the trail format (decision 14) clean — agents still only mint/parse reading trails, never draft state.

D3. Metadata as separate form fields — RECOMMEND (and it's load-bearing)

The single most important UX rule, given the frontmatter-fence bug history: title / tags / importance are dedicated form inputs that map to the mark_publish metadata object. The body field is markdown only. The desk must make it structurally impossible to hand-author a --- fence into the body:

  • Title → metadata.title (or the body H1 — pick one and be consistent).
  • Tags → a tag input → metadata.tags; the status axis (status:draft|wip| accepted|archived) is a tag, surfaced as a status picker that writes status:<v> so the badge round-trips.
  • Importance → a 0–1 control → metadata.importance.
  • Satisfy the write gate before submit (client-side hint + server is the authority).

D4. Create vs edit vs append + optimistic concurrency — RECOMMEND

  • Edit fetches the current doc (body + version), the editor carries the version/etag, and submit sends expected_version. On conflict, surface the broker's merge candidate for the human to resolve, then re-submit at the returned publish-at-version — never blind-overwrite (the silent-loss trap the merge flow exists to prevent).
  • Create is expected_version: 0; the new path lands as the focused pane.
  • Append (mark_append) is a lighter affordance for adding to a log/journal doc; remind that it can't change tags, so a new-subject append nudges a re-publish.

D5. Write authorization — DEPENDS ON / COORDINATE WITH Phase 2

Reads are tokenless; today who may write is only bounded by the world-level AllowConfig (SSO gate) + the write token's path globs. Surfacing editing to every SSO-authed reader is probably too open. The "who can edit which paths" question is the write-side twin of Phase 2's read collections — design them together (reuse the AllowConfig-per-glob shape). At minimum, the room must only show the edit affordance where the identity can actually write, and degrade gracefully (broker rejects → the desk says "not permitted to write here," not a generic error).

Library work (the bulk)

  1. Outbound port: WorldGateway grows write verbs — Publish(ctx, world, path, body, meta, expectedVersion) and Append(ctx, world, path, body) — implemented by the broker MCP adapter (calls mark_publish/mark_append with the reader's bearer; the broker maps to the per-world write token). QUIC adapter implements them too (direct token) or returns not-supported.
  2. Inbound port / service: an Edit/Create/Append use case + a Preview render (reuse the existing renderer). Service owns validation shaping; web owns the form.
  3. Web: edit-mode template (textarea + metadata fields + live preview target), POST handlers (/w/:world/edit/*, /w/:world/new, append), conflict UI, margin "edit" affordance on doc panes (only when writable). Invalidate the rendered-document cache for the written (world,path,version) on success so the re-read shows fresh content.
  4. Agent symmetry (decision 11/12): every desk affordance ⇄ the mark_* write channel; the protocol escape already shows mark:// + raw source.

Broker work (small, if any)

  • Likely none for the basic flow — mark_publish/mark_append + first-write token provisioning already exist. IF D5 lands write-collections, that's broker config + enforcement (shared with Phase 2).
  • Confirm the write gate severity for the cluster (warn vs block) so the desk's client-side hinting matches.

Acceptance (both transports — ADR 0005 decision 16)

  • Broker mode: an authorized identity edits a doc — preview matches the read render; save publishes with metadata out of band (no --- in the stored body); status picker round-trips the badge; a stale edit hits the merge flow, not silent loss; an unauthorized write shows a clear "not permitted," and the edit affordance is hidden where the identity can't write.
  • QUIC mode: with a write token configured, the same flow works directly; without one, the desk is read-only (affordance absent) — honest degradation.

Scope / non-goals

  • Markdown source editing only — no WYSIWYG, no rich-text, no comments/reactions.
  • No media upload (the universe is markdown-native, no hosted media).
  • Write authz beyond "affordance gated on writability" is Phase 2-coupled (D5).
  • Bulk/CLI authoring stays the agent's job (the desk is the human projection).

Build order

  1. Settle D1–D5 with Fritz (esp. D1 surface and D5 authz coupling).
  2. Outbound write verbs + broker adapter (smallest end-to-end: create a doc).
  3. Service use cases + live preview fragment.
  4. Edit-mode UI: textarea + metadata fields + preview + status picker.
  5. Conflict/merge flow + cache invalidation on write.
  6. Edit affordance gating (writability) + not-permitted UX.
  7. Acceptance both transports; update the authoring contract (/.well-known/library/authoring.md) if the desk changes any conventions.

Dependencies / risks

  • First write path through the library — the read-only invariant that has simplified everything (cacheable, idempotent) ends here; treat write idempotency, cache invalidation, and conflict handling as first-class.
  • Frontmatter-fence trap (D3): the desk must make in-band metadata structurally impossible, or it will reproduce the exact bug R1 was built to paper over.
  • Authz coupling (D5): don't ship an open write surface — coordinate with Phase 2 so read-collections and write-permissions share one model.
  • Needs the broker repo only if D5 write-collections are in scope; the basic desk is library-only.
trail
  1. soul.demarkus.io:6309 v1