# ADR — The tiered doc viewer is two registry types and a read-time derivation, not a documents subsystem *Status: accepted, as a design spike. Decided while implementing "design spike for tiered doc viewer". Everything asserted below is covered by tests in this repository — `packages/core/test/source-drift.test.mjs` (the derivations and the fold's untouchedness), `packages/ui/src/lib/docs.test.ts` (tiers, the two drift signals, the argument vectors), and `packages/ui/src/lib/components/DocEditor.svelte.test.ts` (the mounted edit flow).* --- ## 1. Context The ask was a "view mode" of Radial with three tiers of project documentation: **dev docs** (the raw project artifacts — ADRs, architecture, conventions), **user docs** (product documentation covering the features, linked back to the artifacts they came from so a page can be told it has gone stale), and **roadmap docs** (user-facing, linked back to goals). Members should be able to edit them in the app. Almost every mechanism this needs already exists. Project-scoped artifact types collect under `ProjectView.currentSystemArtifacts` (design §8); the artifact-type registry is data rather than code (design §4); every artifact chains through `prev`, carries reviews pinned per version, and can anchor a thread; `artifactRequest.basedOn` is an array of strongrefs that may pin any record by `uri#cid`; `radial artifact post` is a first-class human write and the browser runs the identical `runCli()`. What did not exist: a reader-oriented surface (the System page is a work-tracking list), the two doc tiers, a derivation that notices a page's SOURCES have moved on, an in-app editing flow for artifacts, and any resolution of a doc's goal references to live goal state. ## 2. Decision **Both editable tiers are entries in the artifact-type registry.** `user-doc` and `roadmap-doc` are `scope: project, durability: living` — no lexicon change, no fold rule, no new record type. They inherit versioning, reviews, threads, turn-bundle inclusion and the CLI unchanged. **Linkage is `basedOn` on the driving request, never parsed prose.** A user doc's request pins the system artifacts it documents; a roadmap doc's pins goals. Anything mechanical reads the typed refs; `at://` mentions inside a body stay presentation-only chips. **Staleness is two annotations, side by side and never summed.** `staleness()` already answers "the CODE moved" — merges under the project since this version landed. `sourceDrift()` (`core/src/timeline.ts`) answers "the SOURCES moved": 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 it is. Both are read-time derivations over records the fold already holds (design §8: staleness is annotation, not automation) — nothing is blocked by either, and nothing updates itself. **Editing writes a request assigned to its own author, then posts the artifact.** Two ordinary commands through the one write path. ## 3. Consequences, and the three things worth knowing ### 3.1 The registry entry is load-bearing, not a grouping The plan this spike was built from assumed a space with no doc types registered would show its doc records under the dev tier. It would not: `materialize()` drops an `artifactRequest` whose `type` no registry entry names — it cannot tell which scope the request anchors in — and the artifact answering it goes with it. So before the types exist, a page written into the space folds into **nothing at all**. That is why the viewer's empty state for tiers 2 and 3 is a *setup card* rather than an invitation to write the first page, and why "Set up docs" is admin-gated: only an admin's registry record counts. Registering the type later folds every such record back in, with nothing rewritten and no migration — which is the one property that makes the ordering forgiving. ### 3.2 The self-assignment on an edit request is a safety rule, not a nicety An **unassigned** open request is claimable by any operator's daemon (`dispatch.ts`), which is the whole point of open requests. A member half-way through writing a page must therefore never leave one: `editRequestArgs` always sends `--assignee `, and `dispatch.ts` refuses a request whose assignee is not an active agent member, so a human-assigned request is dispatched by nobody, anywhere. The same rule makes the two-write save recoverable rather than dangerous. A failure between `request create` and `artifact post` leaves an open, self-assigned request that no daemon will answer; the docs page detects it (`resumableEdit`) and offers to finish it, because writing a second request for the same edit would fork the page's history over a dropped connection. Auto-review is deliberately left off for both types: a member fixing a typo must not dispatch a review turn. Turning it on later is a supported setting, not a code change. ### 3.3 Re-pinning the sources IS the update A new version pins its predecessor as `prev` and re-pins every source at the CURRENT head of its chain (`repinnedSources`). Carrying the old refs forward would leave a page badged as drifted immediately after somebody had gone and updated it, which teaches readers to ignore the badge. A ref the fold cannot resolve — a goal, or a record from a repo this observer has not read — is carried through untouched: it is the author's stated provenance, and dropping it because this observer cannot see it would quietly rewrite what the page is about. `sourceDrift()` resolves a ref by URI rather than by `uri#cid`, and excludes any ref naming the unit's own versions. The first is because the store keeps the earliest CID it saw, so an in-place edit of a source is an edit annotation (design §7) and not a superseding version. The second is because an edit request names the version it revises — that is what attaches it to the chain — and a page that had drifted away from itself would badge every second version. ### 3.4 Concurrent edits fork, and the answer is a sentence rather than a merge Two members editing one page in the same ingestion window both post `prev = v3`. The fold resolves a deterministic head and loses nothing — both records are in the version rail — but one edit stops being what a reader sees. The form re-reads the head it was opened on and says so (`headMoved`). It does not refuse the save, and Radial does not grow a merge: this is the same latest-wins trade `labelGoal` already accepts. ## 4. What this deliberately does not do - **No standalone docs sites.** The thread that prompted this imagined `dev.`/`docs.`/`roadmap.` subdomains with userinput-request footnotes. The model here supports that — `basedOn` provenance is exactly the hook — but the spike delivers the view mode inside the existing SPA. - **No automated doc updates.** A drifted page is annotated and never regenerated. That is design §8's third surface, and it is generation-causing: it needs an iteration bound before it exists. - **No second write path.** Editing runs `runCli()` through `write.ts` like everything else, so a private space gets doc editing through the substituted writer with no branch in any file. - **No change to the System page.** It stays the work-tracking list of every project-scoped record, doc tiers included. Docs is the reading surface over the same records, not a replacement for it.