Skip to content

Commit 80281f8

Browse files
docs(adr): document model split (ADR 0002)
* docs(adr): record document model split (ADR 0002) Proposed split of document envelope, relational filing, and graph argument. No engine or API changes. Co-authored-by: Travis Gilbert <Travis-Gilbert@users.noreply.github.com> * docs(adr): scope 0002 to the Theorem extract, not 0.9.1 The three-way split still stands. Do not implement it against this public snapshot; public-site stays on-disk markdown until the extracted backend exists. Co-authored-by: Travis Gilbert <Travis-Gilbert@users.noreply.github.com> * docs: add public projection plan as ADR 0002 companion travisgilbert.me is a Theorem compile target, not a CMS. Do not implement against 0.9.1; public-site stays on-disk markdown until the extract exists. Co-authored-by: Travis Gilbert <Travis-Gilbert@users.noreply.github.com> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Travis Gilbert <Travis-Gilbert@users.noreply.github.com>
1 parent 6a4555d commit 80281f8

3 files changed

Lines changed: 469 additions & 0 deletions

File tree

docs/adr/0002-document-model.md

Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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.

docs/adr/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ are marked as such and link to the replacement.
1010
| # | Title | Status |
1111
|---|---|---|
1212
| [0001](0001-vendored-proto-for-railway-build.md) | Vendor `rustyred.proto` for hermetic Docker / Railway builds | Accepted |
13+
| [0002](0002-document-model.md) | Split document, filing, and argument across three models | Proposed (Theorem extract, not 0.9.1) |

0 commit comments

Comments
 (0)