| … | … | @@ -1,12 +1,56 @@ |
| 1 | 1 | # The record vocabulary — `net.favela.post` |
| 2 | 2 | |
| 3 | 3 | **Status:** design, not implemented. Nothing here has been built. |
| 4 | **Revised 2026-08-31** (§0, §4.1) — the canonical record becomes a *markdown file*, not a |
| 5 | DAG-CBOR record. atproto stops being the canonical P2P path and becomes a projection like any other. |
| 4 | 6 | **Date:** 2026-08-05 |
| 5 | 7 | **Scope:** hyper-proto-peer (authoring), atproto-store (storage), atproto-bridge + ap-bridge |
| 6 | 8 | (projection), favela-web (compose + render). |
| 7 | 9 | |
| 8 | 10 | --- |
| 9 | 11 | |
| 12 | ## 0. The governing decision (2026-08-31) |
| 13 | |
| 14 | **atproto was never meant to be the canonical P2P path. It became one by default.** Building the |
| 15 | peer layer on atproto's data model imported atproto's constraints — MST derivation, DAG-CBOR |
| 16 | byte-exactness, lexicon field ceilings, blob CIDs sealed inside signed records — into a network |
| 17 | that only ever wanted to *interface* with atproto. That machinery is genuinely useful, but it |
| 18 | belongs at the bridge/relay layer, not underneath every full-mode peer. |
| 19 | |
| 20 | **So: the canonical record is a markdown document. Every network — atproto and ActivityPub alike — |
| 21 | is a lossy projection derived from it on read.** |
| 22 | |
| 23 | ``` |
| 24 | records/net.favela.post/3msdnle65lk22.md canonical. what the author typed. |
| 25 | records/app.bsky.feed.post/3msdnle65lk22.dagcbor derived projection (bridge builds the MST from it) |
| 26 | https://ap.7rnx.net/ap/objects/3msdnle65lk22 derived projection |
| 27 | ``` |
| 28 | |
| 29 | This is a smaller change than it sounds, because `atproto-store/lib/overlay-repo-store.js` already |
| 30 | stores one file per record and *derives* MST/commit/CAR on read — "nothing atproto-specific is |
| 31 | stored beyond the record bytes + a tiny head.json". Only the encoding of the record file changes. |
| 32 | |
| 33 | **What it buys.** A full-mode peer needs: parse markdown, one file per record, hyper-overlay |
| 34 | replication. **No MST, no DAG-CBOR, no byte-exact CIDs.** Those become the bridge's problem, where |
| 35 | they belong. This is what makes a second implementation tractable. |
| 36 | |
| 37 | ### 0.1 The two governing properties |
| 38 | |
| 39 | Everything else is negotiable; these are not. |
| 40 | |
| 41 | - **Mutable.** A post is a file that can be rewritten. `OverlayRepoStore` is already |
| 42 | overwrite-in-place on edit and unlink on delete, so on-disk size is O(current records), not |
| 43 | O(operations-ever). |
| 44 | - **Extensible.** New extensions can be added at any time without migrating old records, *because |
| 45 | projections are derived on read*. Old posts stay valid; new projections simply carry more. This is |
| 46 | the opposite of a lexicon change, which is a migration. |
| 47 | |
| 48 | **Consequence:** mutable content means the content hash changes on every edit, so **the rkey is the |
| 49 | stable identity, never the hash.** §3.1 already depends on this, and it is why the 22 already- |
| 50 | federated AP object URIs keep resolving. |
| 51 | |
| 52 | --- |
| 53 | |
| 10 | 54 | ## 1. The decision |
| 11 | 55 | |
| 12 | 56 | **The canonical stored record moves from `app.bsky.feed.post` to `net.favela.post`. bsky and |
| … | … | @@ -145,12 +189,30 @@ This is also what makes the format worth having. If the richest view were a sing |
| 145 | 189 | extra fields would be that app's private state. Because the richest view is the *record*, any P2P |
| 146 | 190 | reader — including one nobody has written yet — sees the whole post. |
| 147 | 191 | |
| 148 | | ### 4.1 Dialect |
| 192 | ### 4.1 Dialect — CommonMark, and extensions that carry the rest |
| 193 | |
| 194 | **CommonMark is the base. Extensions are markdown syntax carrying whatever plain markdown cannot.** |
| 195 | |
| 196 | The extension set is more expansive than any target network because it does not have to ask |
| 197 | permission: the canonical record keeps everything, and each bridge takes only what it can carry. |
| 198 | |
| 199 | **Unknown extensions need no special rule — markdown already degrades on its own.** A reader that |
| 200 | does not know `:::spoiler` renders the text inside it. Falling through to literal text *is* the |
| 201 | behaviour, not something this document has to invent. That is a property of the format, and it is |
| 202 | why the extension set can grow without coordination. |
| 203 | |
| 204 | **The dialect is already pinned, in `markdown-strip.md` §2.1: CommonMark 0.31.2 plus exactly |
| 205 | six extensions** — strikethrough, GFM tables, footnotes, `[[wiki-links]]`, block directives |
| 206 | (`:::name`), and hashtags. *"An implementation MUST NOT enable further extensions. A new one is a |
| 207 | spec change, because it changes the bytes of every record that uses it."* Fenced code is CommonMark |
| 208 | core, not an extension. Markdown links are mentions — `[@alice](did:plc:xyz)` carries display text |
| 209 | plus resolved target, so facets are *derived from the parse* when projecting (§3.4), never |
| 210 | hand-maintained. |
| 149 | 211 | |
| 150 | | - **favela renders:** CommonMark + a *named, pinned* extension set — tables, fenced code, |
| 151 | | footnotes, `[[wiki-links]]` (already used by hypernote), `:::spoiler` directives. |
| 152 | | - **The strip is pinned separately** (§4.2) because its output is what bsky sees and what a CID |
| 153 | | commits to. The renderer may be generous; the strip may not. |
| 212 | **The one exception: addressing.** Getting a spoiler wrong makes a post render plainly. Getting |
| 213 | "who can see this" wrong sends it to the wrong people. So `audience` is a **typed field, never |
| 214 | parsed out of loose text** — a spelling quirk must not be able to change who receives a post. |
| 215 | Everything else may be casual. |
| 154 | 216 | |
| 155 | 217 | ### 4.2 The strip — must be specified, not merely implemented |
| 156 | 218 | |
| … | … | @@ -463,11 +525,21 @@ ignored, honestly and visibly. |
| 463 | 525 | Honest gaps. This design is not complete until these are closed. |
| 464 | 526 | (Editing was one of these; it is now settled in §10.5.) |
| 465 | 527 | |
| 466 | | 1. **The strip specification (§4.2)** is described but not written. It must be a byte-exact |
| 467 | | specification with test vectors, or two implementations diverge. |
| 528 | 1. ~~**The strip specification (§4.2)** is described but not written.~~ **CLOSED** — |
| 529 | `spec/markdown-strip.md` (220 lines) and `spec/vectors/markdown-strip.json` (36 vectors) were |
| 530 | written afterwards. `test/strip-vectors.test.mjs` validates the vectors themselves; no stripper |
| 531 | exists yet. |
| 532 | |
| 533 | 1b. **What signs a markdown record.** With the canonical record no longer a DAG-CBOR entry in a |
| 534 | signed atproto commit chain (§0), authorship and integrity need their own mechanism — a detached |
| 535 | per-record signature, or a native append-only log of `{rkey, contentHash, sig}` that peers |
| 536 | replicate. Mutability is not an obstacle: the signature covers the current bytes and is |
| 537 | re-issued on edit, exactly as atproto re-signs a commit on edit. **This is the next thing to |
| 538 | settle; everything in §9 (read path) hangs off it.** |
| 468 | 539 | 2. **Embed/attachment mapping** across three representations (atproto blob refs, AS2 `attachment`, |
| 469 | 540 | markdown image syntax) is not worked through. |
| 470 | | 3. **The extension dialect** (§4.1) names candidates but does not pin a set. |
| 541 | 3. ~~**The extension dialect** (§4.1) names candidates but does not pin a set.~~ **CLOSED** — |
| 542 | pinned in `markdown-strip.md` §2.1 (CommonMark 0.31.2 + six named extensions). |
| 471 | 543 | 4. **Community/list audience URIs** — the URI form for a favela community room is unspecified. |
| 472 | 544 | 5. **`audience` semantics for replies** — does a reply inherit its parent's audience? Almost |
| 473 | 545 | certainly yes, but the failure mode of getting it wrong is publishing to the wrong people. |