A collaborative coding-agent orchestrator for atproto radl.app

design spike for tiered doc viewer #146

targeting main from radial/impl-a3842e16e635
Merged opened by and assigned to claudebot.disnetdev.com
targeting main from radial/impl-a3842e16e635

Tiered doc viewer — design spike #

A reading surface over the records a project already holds, in three tiers, plus the derivation that tells a page its sources have moved on and an in-app edit flow for members. The spike's central claim is that almost all of this already existed and needed composing rather than a documents subsystem: no lexicon change, no new record type, no fold rule, and the golden scenario is untouched.

Provenance #

Branch radial/impl-a3842e16e635
Commit 2b60f430efa264b6cbc535661489dc434e067050
Base main @ 68f2981
Pull request not opened by this turn (tangled has no CLI for it); Radial opens it from the pushed branch

What landed #

core — two read-time derivations (packages/core/src/timeline.ts). sourceDrift(index, unit) is the second drift signal beside staleness(): for the current version's driving request, each basedOn ref that resolves to an artifact whose chain now has a newer head, with how far behind the pinned version is. sourceGoals(index, unit) resolves a roadmap page's goal refs to live GoalViews. Both sit where staleness() and mergesSince() sit — pure functions over the index, outside the fold (design §8: staleness is annotation, not automation). materialize() is unchanged.

Two rules inside sourceDrift are deliberate: a ref resolves by URI, not uri#cid, because the store keeps the earliest CID it saw and so an in-place edit of a source is an edit annotation (design §7) rather than a superseding version; and a ref naming the unit's own versions is skipped, because an edit request pins the version it revises — that is what attaches it to the chain — and a page that drifted from itself would badge every second version.

The two tiers are registry entries. user-doc and roadmap-doc, scope: project, durability: living, with briefs written for an agent (a brief is the turn prompt the day somebody asks an agent to refresh a page). They inherit versioning, reviews, threads, bundle inclusion and the CLI unchanged.

ui — src/lib/docs.ts is the only module that knows the two names: tier partitioning, the two drift signals as separate badges, the roadmap's derived sections, the type create drafts behind "Set up docs", the two argument vectors editing writes, resume detection, and the route helpers. src/routes/p/[project]/docs/ plus DocReader.svelte and DocEditor.svelte are the surface; a Docs item joins System in the rail.

The reader draws prose first, then the provenance that makes a page checkable (each pinned source, at the version it was read at, with a link to read the newer one), then — on a roadmap page — road-ahead and road-travelled sections derived from live goal state, so closing a goal moves it between them on the next sync with nobody editing the page.

Editing is two ordinary commands (request create, artifact post) through write.ts, so a private space gets doc editing through the substituted writer with no branch anywhere in the new code.

Fixture. The two registry entries and three pages on radial-ng, one written against an architecture version since superseded twice, so the drift signal is on screen and not only in a test.

Three things worth knowing #

  1. The registry entry is load-bearing, and this is stronger than the plan assumed. materialize() drops an artifactRequest whose type no registry entry names — it cannot tell which scope it anchors in — and the artifact answering it goes with it. So before the two entries exist, a doc written into the space folds into nothing at all; it does not fall back to another tier. The empty state is therefore an admin-gated setup card, and registering the type later folds every such record back in with nothing rewritten. Pinned by a test, and docs/adr-tiered-doc-viewer.md §3.1.
  2. An edit request always names its own author as assignee. An unassigned open request is claimable by any operator's daemon; dispatch.ts refuses one assigned to a person, so a half-written page is answered by nobody. The same rule makes the two-write save recoverable: a failure between the writes leaves an open self-assigned request the page offers to finish rather than writing a second one and forking the chain.
  3. A new version re-pins its sources to their current heads. That is the only thing that clears source drift, and carrying stale refs forward would leave a page badged as drifted right after somebody updated it. A ref the fold cannot resolve is carried through untouched — it is the author's stated provenance.

Checks #

pnpm lint, pnpm typecheck and pnpm test are green from a clean pnpm install (core 426, daemon 596, sidecar 134, ingest 50, atproto 35, ui 96 node tests + 895 vitest across 67 files, 0 failures). pnpm codegen produces no diff — no lexicon changed, by construction. New coverage: packages/core/test/source-drift.test.mjs (13 cases, including permutation stability over a new docsScenario() fixture and "no record type this protocol did not already have"), packages/ui/src/lib/docs.test.ts (18), DocEditor.svelte.test.ts (3 mounted, real CodeMirror).

Existing assertions that moved are all fixture counts (the project now holds nine system units rather than six, and the registry two more types); each was updated in place with the reason.

Remaining risks and deliberate omissions #

  • The System page still lists the doc tiers. It is the work-tracking list of every project-scoped record and Docs is the reading surface over the same ones; not deduplicated on purpose, but it is the one call here a reviewer might want made the other way.
  • space create does not seed the two types. The plan chose an in-app affordance so existing spaces get them too; seeding new spaces as well is a one-line follow-up if wanted.
  • Auto-review is left off for both types, so a member's typo fix does not dispatch a review turn. Enabling it later is a setting, not a change.
  • Concurrent edits fork the chain and are answered with a sentence, not a merge (headMoved) — the same latest-wins trade labelGoal accepts.
  • Out of scope, as the plan set it: standalone dev./docs./roadmap. sites, userinput-request footnotes, and any automated doc-update trigger (design §8's generation-causing surface, which needs an iteration bound first).
  • The spike's decisions are recorded in-repo as docs/adr-tiered-doc-viewer.md; recording them as an adr artifact in the space is still worth doing and is a separate request.

Radial artifact

Commits 3

Edit continues the open doc request instead of forking it
Fix resumed doc source selection
Tiered doc viewer: docs tiers as registry types over the existing fold