| … | … | @@ -57,6 +57,67 @@ Any model that does not use these is paying Hypercore's costs without taking its |
| 57 | 57 | ⛔ **A projection must never constrain the log.** That rule is the whole lesson of §1, and it is |
| 58 | 58 | checkable: *if adding a network changes the record, it was done wrong.* |
| 59 | 59 | |
| 60 | ## 3.5 Vocabulary — taken from keet, extended where we need more |
| 61 | |
| 62 | Our specs inherited atproto's words (`rkey`, `at://`, `repo`) along with its shapes. keet's |
| 63 | vocabulary is the native one and it is **better than ours in a specific way**: it keeps four things |
| 64 | apart 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 |
| 79 | deterministic replay, not minted by the client. It therefore cannot collide, because linearisation |
| 80 | assigns it. **Our rkeys collide precisely because we mint them client-side from a clock.** That is |
| 81 | the single most useful thing in this audit. |
| 82 | |
| 83 | And an edit rewrites the row at the same `{thread, index}`, so **position is the identity that |
| 84 | survives 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 | |
| 97 | It is expressive enough for a chat room and **not** for us, in four places — all because keet never |
| 98 | leaves keet: |
| 99 | |
| 100 | 1. **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. |
| 103 | 2. **No projection or loss vocabulary.** `reach`, `loss[]`, `level`, `variant` have no keet analogue. |
| 104 | 3. **No content-addressed body.** keet messages live inline in the db; ours are files with an `oid` |
| 105 | and a level manifest. |
| 106 | 4. **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 | |
| 111 | A client does **not know its own record's id until the op linearises.** keet absorbs this by |
| 112 | rendering optimistically from the tip. For us it means a permalink is not available at compose time, |
| 113 | and the atproto projection's rkey is only determined once the view advances — which is fine, because |
| 114 | the signer already materialises from the linearised view, but it must be designed for rather than |
| 115 | discovered. |
| 116 | |
| 117 | ⚠ atproto sorts rkeys lexicographically, so a decimal position sorts wrong. The projection needs a |
| 118 | sortable encoding (zero-padded or base32), which is a projection-time concern and not a property of |
| 119 | the native id. |
| 120 | |
| 60 | 121 | ## 4. The central move — visibility is a PLACE, not a field |
| 61 | 122 | |
| 62 | 123 | **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 |
| 199 | 260 | 3. **Text has no pyramid.** `strip()` is lossy but not a ladder. This model covers media and identity |
| 200 | 261 | (§6.6.3), not prose. |
| 201 | 262 | |
| 263 | ### 6.6.4 ⛔ The model assumes an ENCODER at authoring time — and the handset is the hard case |
| 264 | |
| 265 | This is the constraint that decides whether the pyramid is clean or partial. |
| 266 | |
| 267 | hypernote 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 |
| 269 | time: CPU, battery, and an image encoder that may not exist in Bare on that device. **Video is |
| 270 | already solved — HLS *is* the manifest. Images are not, and the phone is where most posts come |
| 271 | from.** |
| 272 | |
| 273 | So the pyramid must be allowed to be **partial**, and that has three consequences: |
| 274 | |
| 275 | 1. **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. |
| 277 | 2. **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. |
| 280 | 3. **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 |
| 284 | worse a floor than what we ship now, and improves monotonically as encoders become available. That |
| 285 | is the property that makes it safe to adopt before the image encoder question is settled. |
| 286 | |
| 202 | 287 | ⛔ **Who derived a level still matters.** §8's *"never re-encode a record you did not author"* cost two |
| 203 | 288 | of wj62's posts. Levels generated at authoring are the author's; a level a bridge generated later is |
| 204 | 289 | derived, and the manifest must say so rather than presenting it as authored bytes. |