|
| 1 | +# ADR 0002 — Split document, filing, and argument across three models |
| 2 | + |
| 3 | +Status: Proposed |
| 4 | +Date: 2026-08-19 |
| 5 | +Decision drivers: Travis Gilbert |
| 6 | +Implementation target: extracted RustyRed / Theorem line, not this |
| 7 | +public `0.9.1` graph-first snapshot |
| 8 | + |
| 9 | +The three-way split below is accepted as product direction. Status is |
| 10 | +Proposed because it is not implemented. |
| 11 | + |
| 12 | +This standalone repo (`RustyRed-Graph-Database`, current `0.9.1`) is |
| 13 | +drastically different from Theorem. It does not take agents yet. |
| 14 | +Extracting the real RustyRed and updating this repo is follow-up work. |
| 15 | +The snapshot here is not a useful backend or control plane for the |
| 16 | +public site. |
| 17 | + |
| 18 | +Do not implement document tables, collection membership, or a |
| 19 | +published-ref API against `0.9.1` in this repo as it stands. This ADR |
| 20 | +still records the split (document / relational filing / graph |
| 21 | +argument). That decision travels with the extract. It is not a claim |
| 22 | +that this snapshot is the place to build it. |
| 23 | + |
| 24 | +`public-site/` stays on-disk markdown until the extracted backend |
| 25 | +exists. The compile/projection story is |
| 26 | +`docs/plans/public-projection.md`. |
| 27 | + |
| 28 | +## Context |
| 29 | + |
| 30 | +RustyRed `0.9.1` is graph-first. The shipped store is a directed |
| 31 | +property graph with epistemic edges, Git-like version packs at |
| 32 | +`/graph/version/*`, BM25 full-text, and HNSW vector search. Canonical |
| 33 | +types live in `crates/rustyred-core/src/graph_store.rs`. There is no |
| 34 | +document envelope, no collection membership table, and no publish |
| 35 | +semantics for prose. |
| 36 | + |
| 37 | +Product work still needs a place to put writing. The editor opens a |
| 38 | +document. A public site lists essays. A field note becomes an essay. |
| 39 | +Those are filing and argument problems, not graph-label problems. |
| 40 | + |
| 41 | +The cheap encoding is to store Essay / FieldNote / Project as |
| 42 | +first-class types or as node labels that replace Document. That freezes |
| 43 | +genre into species. Changing a field note into an essay becomes a type |
| 44 | +migration instead of a membership change. Folders become edge walks. |
| 45 | +Presentation chrome (callout styling, hero colors, annotation offsets) |
| 46 | +lands in the same row as the body. |
| 47 | + |
| 48 | +The live editor already has a CRDT buffer |
| 49 | +(`/v1/tenants/{t}/sync/yjs/:doc_id`) that persists as `YjsDoc` graph |
| 50 | +nodes. Graph version packs already compile snapshots, move refs |
| 51 | +(default branch `main`), and checkout commits. Search already indexes |
| 52 | +designated node properties. markdown-theory already treats |
| 53 | +article / note / log as templates, not types. None of that is a |
| 54 | +document model. The missing piece is the split: what holds the |
| 55 | +document, what holds filing, what holds argument. |
| 56 | + |
| 57 | +The first consumer is the extracted thin public site |
| 58 | +(`public-site/` on travisgilbert.me). It stays on-disk markdown until |
| 59 | +the extracted backend exists. The snapshot in this repo is not that |
| 60 | +backend. |
| 61 | + |
| 62 | +## Decision |
| 63 | + |
| 64 | +RustyRed is multi-model. Split responsibilities. Do not collapse |
| 65 | +writing, filing, and argument into one graph label set. |
| 66 | + |
| 67 | +### 1. Document model holds the document |
| 68 | + |
| 69 | +A Document is the thing the editor opens. The envelope is: |
| 70 | + |
| 71 | +- `id` |
| 72 | +- `body` (markdown, v1) |
| 73 | +- optional `title` |
| 74 | +- a working ref and a published ref |
| 75 | + |
| 76 | +Publish is a ref move, like updating `main` through |
| 77 | +`/graph/version/ref`. It is not a boolean on the row. |
| 78 | + |
| 79 | +This is also why the same model works as an IDE backend: the editor |
| 80 | +buffer is the working tree; publish is the commit you ship. |
| 81 | + |
| 82 | +Existing version packs (`/graph/version/compile`, `/diff`, `/ref`, |
| 83 | +`/log`, `/checkout`, `/merge`) are the revision substrate. Do not |
| 84 | +invent a second versioning system for documents. |
| 85 | + |
| 86 | +The live Yjs room remains the collaborative working buffer. This ADR |
| 87 | +does not replace that transport. It defines the durable envelope the |
| 88 | +editor opens and the published ref a reader may serve. |
| 89 | + |
| 90 | +Markdown body is v1. Do not wait on a block model. |
| 91 | + |
| 92 | +### 2. Relational model holds filing |
| 93 | + |
| 94 | +Collections, genres, membership (document in collection), and slugs |
| 95 | +belong here. Slugs are unique per collection. |
| 96 | + |
| 97 | +"Essay", "field note", and "project" are not types and not labels that |
| 98 | +change the species of a document. They are metadata that connect |
| 99 | +documents: collection membership, or a row that says this document is |
| 100 | +filed as an essay. A field note can become an essay without changing |
| 101 | +type. |
| 102 | + |
| 103 | +The public site query is a join: published documents in the Essays |
| 104 | +collection. |
| 105 | + |
| 106 | +Folders are tables. Do not encode the folder tree as graph edges. |
| 107 | + |
| 108 | +This is the product direction for filing on the extracted line. It is |
| 109 | +not a statement that `0.9.1` already ships SQL tables, and it is not |
| 110 | +permission to add those tables to this snapshot. |
| 111 | + |
| 112 | +### 3. Graph model holds argument |
| 113 | + |
| 114 | +Related, cites, contradicts, and "this note became that essay" belong |
| 115 | +on the graph. Use the existing epistemic types (`cites`, `contradicts`, |
| 116 | +and the rest of `EpistemicType`) where they already fit. Add ordinary |
| 117 | +relationship edges for related / became when those are not epistemic |
| 118 | +claims. |
| 119 | + |
| 120 | +Do not use the graph as a folder tree. |
| 121 | + |
| 122 | +### Search and consumers |
| 123 | + |
| 124 | +BM25 (`/graph/fulltext/*`) and vector search (`/graph/vector/*`) |
| 125 | +already exist. Document body should be searchable through them once |
| 126 | +the envelope is stored — designate the body property; do not stand up |
| 127 | +a parallel index. |
| 128 | + |
| 129 | +Stay aligned with markdown-theory: article / note / log are templates, |
| 130 | +not storage types. |
| 131 | + |
| 132 | +`public-site/` stays on-disk markdown until the extracted backend |
| 133 | +exists. |
| 134 | + |
| 135 | +### Out of the document envelope |
| 136 | + |
| 137 | +Callout chrome, hero colors, and annotation offsets are views. They |
| 138 | +do not live on the document row. |
| 139 | + |
| 140 | +## Alternatives considered |
| 141 | + |
| 142 | +### Option A — Split document / relational / graph (chosen) |
| 143 | + |
| 144 | +- **Upside:** The editor, the public site, and argument traversal each |
| 145 | + hit the model that matches the query. Genre changes do not rewrite |
| 146 | + the document type. Publish reuses the version-pack ref machinery |
| 147 | + already shipped at `/graph/version/*`. Search reuses BM25 and HNSW. |
| 148 | + markdown-theory templates stay templates. |
| 149 | +- **Risk:** Three models to keep coherent. A consumer that wants "all |
| 150 | + essays" must join published refs to collection membership instead of |
| 151 | + filtering a label. Mitigated by making that join the documented |
| 152 | + public-site query. |
| 153 | +- **Validation:** A field note can be filed as an essay without a type |
| 154 | + change. Publish updates a ref, not a boolean. The public site lists |
| 155 | + published documents in a named collection. Graph queries for cites / |
| 156 | + contradicts / became do not double as folder walks. |
| 157 | + |
| 158 | +### Option B — Essay / FieldNote / Project as first-class types or replacing node labels |
| 159 | + |
| 160 | +- **Upside:** Fast to query "all essays" as `labels = ["Essay"]`. |
| 161 | + Matches a graph-only reading of the `0.9.1` snapshot. |
| 162 | +- **Rejected because:** Filing becomes species. A field note that |
| 163 | + becomes an essay is a type migration, not a membership change. |
| 164 | + Conflicts with markdown-theory's template-not-type stance. The editor |
| 165 | + would open a subtype instead of a Document. |
| 166 | + |
| 167 | +### Option C — Collections and folders as graph edges only |
| 168 | + |
| 169 | +- **Upside:** No relational surface. Everything is already in |
| 170 | + `GraphStore`. |
| 171 | +- **Rejected because:** Folder trees and unique-per-collection slugs |
| 172 | + are relational constraints. Encoding them as edges makes the public |
| 173 | + site query a traversal and makes uniqueness a convention. Folders |
| 174 | + are tables. The graph keeps argument. |
| 175 | + |
| 176 | +### Option D — Store presentation chrome on the document envelope |
| 177 | + |
| 178 | +- **Upside:** One row has everything a renderer needs: body, callout |
| 179 | + styling, hero colors, annotation offsets. |
| 180 | +- **Rejected because:** Those are views. They change without changing |
| 181 | + the document. Putting them in the envelope couples publish to |
| 182 | + presentation and bloats the thing the editor opens. |
| 183 | + |
| 184 | +### Option E — Wait for a block model |
| 185 | + |
| 186 | +- **Upside:** Structured body from day one. Callouts and annotations |
| 187 | + could be first-class spans. |
| 188 | +- **Rejected because:** v1 body is markdown. A block model can layer |
| 189 | + later without changing Document from "the thing the editor opens." |
| 190 | + Waiting blocks the public site and the editor for no filing benefit. |
| 191 | + |
| 192 | +### Option F — `published` boolean on the document row |
| 193 | + |
| 194 | +- **Upside:** Simple filter. No ref machinery. |
| 195 | +- **Rejected because:** It throws away the version packs already |
| 196 | + shipped. Working vs published is the same shape as working tree vs |
| 197 | + `main`. A boolean cannot name which compiled pack is live, cannot |
| 198 | + roll back by moving a ref, and does not match the IDE-backend |
| 199 | + analogy this model is built on. |
| 200 | + |
| 201 | +### Option G — A second document-specific versioning system |
| 202 | + |
| 203 | +- **Upside:** Document history could look like a CMS (drafts table, |
| 204 | + revision rows) without using graph packs. |
| 205 | +- **Rejected because:** `/graph/version/*` already compiles |
| 206 | + content-addressed packs, moves refs, logs, checkouts, and merges. |
| 207 | + A second history is drift. Documents use that substrate. |
| 208 | + |
| 209 | +## Consequences |
| 210 | + |
| 211 | +### Positive |
| 212 | + |
| 213 | +- The editor has one species to open: Document. |
| 214 | +- Filing can change without rewriting the document. |
| 215 | +- Publish is a ref move over the existing version-pack substrate. |
| 216 | +- Argument stays on the graph, next to `cites` / `contradicts`. |
| 217 | +- The public site has a stable query shape: published documents in a |
| 218 | + collection. |
| 219 | +- Search work is designation of body onto indexes that already exist. |
| 220 | + |
| 221 | +### Negative |
| 222 | + |
| 223 | +- This repo's `0.9.1` docs (`docs/technical/data-model.md` and the HTTP |
| 224 | + graph surface) remain the shipped snapshot. This ADR is not a build |
| 225 | + ticket against that tree. Readers must not treat Proposed as |
| 226 | + released here. |
| 227 | +- Implementing collection membership as graph edges on this snapshot |
| 228 | + would still contradict the split, even if it "works" on today's |
| 229 | + store. The fix is the extract, not a workaround in `0.9.1`. |
| 230 | +- Yjs persistence as `YjsDoc` nodes is a CRDT implementation detail, |
| 231 | + not the document envelope. Bridging buffer → working ref → |
| 232 | + published ref is follow-up work, not specified here as an API. |
| 233 | + |
| 234 | +### Operational |
| 235 | + |
| 236 | +- No engine, proto, or HTTP change in the PR that records this ADR. |
| 237 | +- Do not implement document tables, collection membership, or a |
| 238 | + published-ref API against `0.9.1` in this repo as it stands. |
| 239 | +- `public-site/` stays on-disk markdown until the extracted backend |
| 240 | + exists. |
| 241 | +- When implementation starts on the extracted / Theorem line, document |
| 242 | + body is designated into the existing BM25 and vector indexes rather |
| 243 | + than growing a third search path. |
| 244 | +- markdown-theory templates remain the presentation vocabulary for |
| 245 | + article / note / log. They do not become RustyRed storage types. |
| 246 | +- Technical reference for the live graph stays in |
| 247 | + `docs/technical/data-model.md`. This ADR is the product split, not a |
| 248 | + replacement graph schema. |
| 249 | + |
| 250 | +## Reversibility |
| 251 | + |
| 252 | +Fully reversible until implementation lands on the extracted line. To |
| 253 | +revert the decision: |
| 254 | + |
| 255 | +1. Mark this ADR Superseded and point at the replacement. |
| 256 | +2. Leave this `0.9.1` snapshot as it is: collaborative buffers as |
| 257 | + `YjsDoc` graph nodes, no document tables here. |
| 258 | + |
| 259 | +After implementation on the extract, reversal is a migration: |
| 260 | +documents and membership tables would fold back into graph records. |
| 261 | +The version-pack refs can stay; they predate this ADR. |
| 262 | + |
| 263 | +## Related |
| 264 | + |
| 265 | +- `docs/plans/public-projection.md` — travisgilbert.me as a Theorem |
| 266 | + compile target. Companion plan; not a second model split. |
| 267 | +- `docs/technical/data-model.md` — shipped `0.9.1` graph snapshot |
| 268 | + (nodes, edges, epistemic types, content addressing). Not the |
| 269 | + document envelope. |
| 270 | +- `docs/technical/http-api.md` — `/graph/version/*`, |
| 271 | + `/graph/fulltext/*`, `/graph/vector/*`. |
| 272 | +- `crates/rustyred-core/src/versioned_graph.rs` — |
| 273 | + `DEFAULT_GRAPH_BRANCH` (`main`), compile / ref / checkout / merge. |
| 274 | +- `crates/rustyred-core/src/graph_store.rs` — `NodeRecord`, |
| 275 | + `EdgeRecord`, `EpistemicType` (`cites`, `contradicts`, …). |
| 276 | +- `crates/rustyred-core/src/fulltext.rs` — BM25 designation keyed by |
| 277 | + `(label, property)`. |
| 278 | +- `crates/rustyred-server/src/yjs_sync.rs` — live CRDT buffer; persists |
| 279 | + as `YjsDoc` graph nodes. Transport, not the document model. |
| 280 | +- `crates/rustyred-server/src/router.rs` — version, fulltext, vector, |
| 281 | + and Yjs routes as they exist today. |
0 commit comments