OverviewFilesCommitsIssuesDocsReleasesPeers

favela-web commits / a3d469b

spec: §0 the governing decision — markdown becomes the record
The 2026-08-31 work (hyper-social #56 §3) was left uncommitted in the working tree: §0 the governing decision, §0.1 mutable + extensible, and the §4.1 dialect rewrite reconciled against markdown-strip.md §2.1's pinned six extensions. Committed here, where it was written, before the file moves to favela-markdown. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a3d469b752c4da7b7dc78f364ba8d3a074d9d98c
by plankatron <fleeky@gmail.com> · 2026-09-03T00:48:20.000Z

Changed files (1) +80 -8

spec/record-vocabulary.md +80 -8
……@@ -1,12 +1,56 @@
11# The record vocabulary — `net.favela.post`
22
33**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
5DAG-CBOR record. atproto stops being the canonical P2P path and becomes a projection like any other.
46**Date:** 2026-08-05
57**Scope:** hyper-proto-peer (authoring), atproto-store (storage), atproto-bridge + ap-bridge
68(projection), favela-web (compose + render).
79
810---
911
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
15peer layer on atproto's data model imported atproto's constraints — MST derivation, DAG-CBOR
16byte-exactness, lexicon field ceilings, blob CIDs sealed inside signed records — into a network
17that only ever wanted to *interface* with atproto. That machinery is genuinely useful, but it
18belongs 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 —
21is a lossy projection derived from it on read.**
22
23```
24records/net.favela.post/3msdnle65lk22.md canonical. what the author typed.
25records/app.bsky.feed.post/3msdnle65lk22.dagcbor derived projection (bridge builds the MST from it)
26https://ap.7rnx.net/ap/objects/3msdnle65lk22 derived projection
27```
28
29This is a smaller change than it sounds, because `atproto-store/lib/overlay-repo-store.js` already
30stores one file per record and *derives* MST/commit/CAR on read — "nothing atproto-specific is
31stored 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
34replication. **No MST, no DAG-CBOR, no byte-exact CIDs.** Those become the bridge's problem, where
35they belong. This is what makes a second implementation tractable.
36
37### 0.1 The two governing properties
38
39Everything 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
49stable identity, never the hash.** §3.1 already depends on this, and it is why the 22 already-
50federated AP object URIs keep resolving.
51
52---
53
1054## 1. The decision
1155
1256**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
145189extra fields would be that app's private state. Because the richest view is the *record*, any P2P
146190reader — including one nobody has written yet — sees the whole post.
147191
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
196The extension set is more expansive than any target network because it does not have to ask
197permission: 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
200does not know `:::spoiler` renders the text inside it. Falling through to literal text *is* the
201behaviour, not something this document has to invent. That is a property of the format, and it is
202why 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
205six 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
207spec change, because it changes the bytes of every record that uses it."* Fenced code is CommonMark
208core, not an extension. Markdown links are mentions — `[@alice](did:plc:xyz)` carries display text
209plus resolved target, so facets are *derived from the parse* when projecting (§3.4), never
210hand-maintained.
149211
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
214parsed out of loose text** — a spelling quirk must not be able to change who receives a post.
215Everything else may be casual.
154216
155217### 4.2 The strip — must be specified, not merely implemented
156218
……@@ -463,11 +525,21 @@ ignored, honestly and visibly.
463525Honest gaps. This design is not complete until these are closed.
464526(Editing was one of these; it is now settled in §10.5.)
465527
4661. **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.
5281. ~~**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
5331b. **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.**
4685392. **Embed/attachment mapping** across three representations (atproto blob refs, AS2 `attachment`,
469540 markdown image syntax) is not worked through.
4703. **The extension dialect** (§4.1) names candidates but does not pin a set.
5413. ~~**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).
4715434. **Community/list audience URIs** — the URI form for a favela community room is unspecified.
4725445. **`audience` semantics for replies** — does a reply inherit its parent's audience? Almost
473545 certainly yes, but the failure mode of getting it wrong is publishing to the wrong people.