OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-social commits / 6c3df03

docs/social-model: the pyramid may be partial, and native vocabulary from keet
6.6.4: the mip model assumes an encoder at authoring time and the handset is the hard case, so the manifest states which levels EXIST; deriving is not authoring; the degenerate case is one level, which is today's behaviour. 3.5: keet's vocabulary keeps identity/position/causality/wall-time apart, and assigns position INSIDE apply - which is why it cannot collide and our client-minted rkeys can. Records where keet's vocabulary runs out for us. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
6c3df03999a359a9c62c34461cc10734707bde68
by plankatron <fleeky@gmail.com> · 2026-09-05T22:29:16.000Z

Changed files (1) +85 -0

docs/SOCIAL-MODEL.md +85 -0
……@@ -57,6 +57,67 @@ Any model that does not use these is paying Hypercore's costs without taking its
5757⛔ **A projection must never constrain the log.** That rule is the whole lesson of §1, and it is
5858checkable: *if adding a network changes the record, it was done wrong.*
5959
60## 3.5 Vocabulary — taken from keet, extended where we need more
61
62Our specs inherited atproto's words (`rkey`, `at://`, `repo`) along with its shapes. keet's
63vocabulary is the native one and it is **better than ours in a specific way**: it keeps four things
64apart that we had collapsed into one.
65
66`keet-core/lib/core/contract/chat.js:48-56`, a message record:
67
68```js
69{
70 id: { thread, index }, // POSITION in the view — stable across edits
71 oplog: { key, … }, // IDENTITY of the append that created it (key = device key)
72 clock: …, // logical clock — causality
73 timestamp: …, // wall time — deliberately NOT identity
74 status: { isEdited, isPinned, isDeleted }
75}
76```
77
78★ **`index` is assigned INSIDE `apply()`** — `index: p.count`, read from the thread's counter during
79deterministic replay, not minted by the client. It therefore cannot collide, because linearisation
80assigns it. **Our rkeys collide precisely because we mint them client-side from a clock.** That is
81the single most useful thing in this audit.
82
83And an edit rewrites the row at the same `{thread, index}`, so **position is the identity that
84survives an edit**; `oplog` records provenance. A mutable record wants the first, not the second.
85
86| we said | native | note |
87|---|---|---|
88| `rkey` | **position**, assigned in apply | atproto's word; ours collides because it is a client-side clock reading |
89| — | **`oplog`** — `{key, seq}` | provenance: which writer's append created this. We had no word for it |
90| `collection` | **collection** | ⚠ *not* atproto-specific — hyperdb uses it (`@keet/chat`), with `@keet/chat-by-message-id` as the index convention. This one survives |
91| `repo` | **room** (the autobase); `roomKey` = `base.key` | |
92| `cid` | **`oid`** (hyper-overlay blake2b) | two different hashes, both real (§6.6) |
93| `createdAt` | **`timestamp`**, and never identity | |
94
95### 3.5.1 Where keet's vocabulary runs out
96
97It is expressive enough for a chat room and **not** for us, in four places — all because keet never
98leaves keet:
99
1001. **No cross-room reference.** `parent` and `replyTo` name messages *within* the room. We must name
101 records in other rooms (a like on a foreign post, an `alias`). A qualified reference is
102 `<roomKey> + <position>`; keet needs no such thing and has no word for it.
1032. **No projection or loss vocabulary.** `reach`, `loss[]`, `level`, `variant` have no keet analogue.
1043. **No content-addressed body.** keet messages live inline in the db; ours are files with an `oid`
105 and a level manifest.
1064. **Collections are a closed set** in keet's contract. Ours must carry foreign namespaces
107 (`app.bsky.feed.post`), so the collection space is open, not an enum.
108
109### 3.5.2 The cost of assign-in-apply, stated
110
111A client does **not know its own record's id until the op linearises.** keet absorbs this by
112rendering optimistically from the tip. For us it means a permalink is not available at compose time,
113and the atproto projection's rkey is only determined once the view advances — which is fine, because
114the signer already materialises from the linearised view, but it must be designed for rather than
115discovered.
116
117⚠ atproto sorts rkeys lexicographically, so a decimal position sorts wrong. The projection needs a
118sortable encoding (zero-padded or base32), which is a projection-time concern and not a property of
119the native id.
120
60121## 4. The central move — visibility is a PLACE, not a field
61122
62123**You do not mark a post followers-only. You write it in a room whose members are your followers.**
……@@ -199,6 +260,30 @@ substrate; what is missing is generalising it to images and naming it as the mod
1992603. **Text has no pyramid.** `strip()` is lossy but not a ladder. This model covers media and identity
200261 (§6.6.3), not prose.
201262
263### 6.6.4 ⛔ The model assumes an ENCODER at authoring time — and the handset is the hard case
264
265This is the constraint that decides whether the pyramid is clean or partial.
266
267hypernote generates its ladder with **ffmpeg via `bare-subprocess`, on a server**
268(`lib/media/video-processor.js`). A handset posting a photo would have to produce four levels at post
269time: CPU, battery, and an image encoder that may not exist in Bare on that device. **Video is
270already solved — HLS *is* the manifest. Images are not, and the phone is where most posts come
271from.**
272
273So the pyramid must be allowed to be **partial**, and that has three consequences:
274
2751. **The manifest states which levels EXIST, never which levels *should*.** A consumer asking for a
276 level that was never generated must get a clear absence, not a 404 that looks like a fetch failure.
2772. **A level may be derived later** — by a bridge, or by a well-resourced peer that holds the base.
278 That makes §6.6.1's "who derived it" load-bearing rather than a nicety: the manifest records the
279 deriver per level, so an author's bytes and a bridge's re-encode are never confused.
2803. **Deriving is not authoring.** A peer adding a level does not become a co-author of the record;
281 it has produced a derived artifact and says so. Same rule as §8, applied to bytes.
282
283⚠ **The degenerate case is one level**, which is exactly today's behaviour — so this model has no
284worse a floor than what we ship now, and improves monotonically as encoders become available. That
285is the property that makes it safe to adopt before the image encoder question is settled.
286
202287⛔ **Who derived a level still matters.** §8's *"never re-encode a record you did not author"* cost two
203288of wj62's posts. Levels generated at authoring are the author's; a level a bridge generated later is
204289derived, and the manifest must say so rather than presenting it as authored bytes.