| … | … | @@ -110,63 +110,80 @@ verified. |
| 110 | 110 | |
| 111 | 111 | ### 3.1 The rkey pairing — the mechanism everything rests on |
| 112 | 112 | |
| 113 | | **A canonical record and its atproto projection share one rkey, differing only by collection.** |
| 113 | **Revised 2026-09-05.** The room is the container (`hyper-social/docs/SOCIAL-MODEL.md`), so the |
| 114 | canonical name is room-scoped. The pairing itself is unchanged and is still what everything rests on: |
| 115 | **one rkey, N names, each a pure function of the others.** |
| 114 | 116 | |
| 115 | 117 | ``` |
| 116 | | canonical at://<did>/net.favela.post/3msdnle65lk22 |
| 117 | | projection at://<did>/app.bsky.feed.post/3msdnle65lk22 |
| 118 | canonical favela://<roomKey>/net.favela.post/3msdnle65lk22 |
| 119 | projection at://<did>/app.bsky.feed.post/3msdnle65lk22 (public rooms only) |
| 118 | 120 | AP object https://ap.7rnx.net/ap/objects/3msdnle65lk22 |
| 119 | 121 | ``` |
| 120 | 122 | |
| 121 | | This is not a convenience. It resolves four problems at once: |
| 123 | It resolves four problems at once: |
| 122 | 124 | |
| 123 | | - **No mapping table.** Given the canonical, the projection is derivable, and vice versa. |
| 125 | - **No mapping table.** Given the canonical, each projection is derivable, and vice versa. |
| 124 | 126 | - **The AP object URI never changes.** ap-bridge keeps deriving from the rkey, so the 22 already- |
| 125 | | federated URIs keep resolving. The collection-in-path change discussed earlier is **not needed**. |
| 127 | federated URIs keep resolving. Unaffected by the room change — it never used the DID. |
| 126 | 128 | - **ap-bridge's pure-function id-map survives** — it never fronts a second collection. |
| 127 | 129 | - **Delete and edit cascade mechanically** by rkey. |
| 128 | 130 | |
| 131 | ⚠ The `at://` projection exists **only for public rooms**, because atproto cannot express addressing |
| 132 | (§5.2). A record in a followers or community room has a canonical name and an AP object and no |
| 133 | atproto name at all — that is not a gap, it is the ceiling of the target. |
| 134 | |
| 129 | 135 | ### 3.2 The core fields |
| 130 | 136 | |
| 131 | | The intersection of what atproto and AS2 both express, checked field by field against both specs: |
| 137 | **Revised 2026-09-05.** A record is a **file** — JSON frontmatter plus a markdown body |
| 138 | (`design.md` §5) — not a JSON object with a stored body. What follows is the frontmatter. |
| 132 | 139 | |
| 133 | 140 | ```json |
| 134 | 141 | { |
| 135 | | "$type": "net.favela.post", |
| 136 | 142 | "createdAt": "2026-08-05T13:38:23.294Z", |
| 137 | 143 | "langs": ["en"], |
| 138 | | |
| 139 | | "source": { "content": "hello **world**", "mediaType": "text/markdown" }, |
| 140 | | "text": "hello world", |
| 141 | | "facets": [ /* byte offsets into `text` */ ], |
| 142 | | |
| 143 | | "reply": { "root": {…}, "parent": {…} }, |
| 144 | | "embed": { /* images / quote / external */ }, |
| 145 | | |
| 146 | | "audience": ["https://www.w3.org/ns/activitystreams#Public"] |
| 144 | "reply": { "root": {…}, "parent": {…} }, |
| 145 | "reach": ["favela", "bsky", "fediverse"] |
| 147 | 146 | } |
| 148 | 147 | ``` |
| 149 | 148 | |
| 150 | | `audience` is the only genuinely new field. Everything else is what is already written today, |
| 151 | | because those parts of bsky's model are sound and reinventing them would be churn. |
| 149 | Everything the old version of this section listed as a stored field — `text`, `facets`, `source`, |
| 150 | `embed` — is **derived at projection time** from the body (§3.3). `$type` is carried by the |
| 151 | collection in the record's name, not repeated in the bytes. |
| 152 | |
| 153 | ⛔ **`audience` is gone, and it is not renamed — it is deleted.** Who can see a record is **the room |
| 154 | it was written in**; membership is the audience, enforced by replication rather than honoured by |
| 155 | convention (`design.md` §5.1). `reach` — which networks to syndicate to — remains, because a room |
| 156 | cannot express that. |
| 152 | 157 | |
| 153 | | ### 3.3 Two representation decisions, settled on recoverability |
| 158 | ### 3.3 Two representation decisions — one reversed |
| 154 | 159 | |
| 155 | | **Body: plain text + byte-offset facets is canonical; markdown is the source; HTML is derived.** |
| 160 | **Body: the MARKDOWN FILE is canonical; `text` + `facets` are DERIVED.** |
| 156 | 161 | |
| 157 | | Markdown → HTML is deterministic. HTML → byte-offset facets is not reliably recoverable. So the |
| 158 | | canonical machine-readable body is `text` + `facets`, and `source` carries what the human typed. |
| 159 | | This is not "atproto won" — it is the direction that loses nothing. |
| 162 | ⛔ **Reversed 2026-09-05.** This section previously said *"plain text + byte-offset facets is |
| 163 | canonical; markdown is the source"* — the pre-08-31 position, contradicting §0 ever since. §0 is |
| 164 | correct: the markdown document is the canonical record, and `text`/`facets` are computed by |
| 165 | `strip()` at projection time (`markdown-strip.md`), never stored. |
| 160 | 166 | |
| 161 | | **Threading: `{root, parent}` is canonical, `inReplyTo` is derived.** atproto's pair carries strictly |
| 162 | | more than AS2's single URI, and the projection down is trivial. |
| 167 | The old reasoning was that HTML → byte-offset facets is not reliably recoverable. True, and no longer |
| 168 | relevant: nothing recovers facets from HTML, because facets are produced from the **markdown parse**, |
| 169 | which yields exact source ranges (§3.4). Storing them was solving a problem that the parse removes. |
| 170 | |
| 171 | **Threading: `{root, parent}` is canonical, `inReplyTo` is derived.** Unchanged — atproto's pair |
| 172 | carries strictly more than AS2's single URI, and the projection down is trivial. Its markdown-native |
| 173 | spelling is still open (`design.md` §8 #2). |
| 163 | 174 | |
| 164 | 175 | ### 3.4 Markdown links *are* facets |
| 165 | 176 | |
| 166 | 177 | `[@alice](did:plc:xyz)` carries display text plus resolved target — exactly what a facet is. A |
| 167 | 178 | CommonMark parser yields exact source ranges, so facet offsets are **derived from the parse**, never |
| 168 | 179 | hand-maintained. This preserves the existing rule (favela-web 0.3.11) that a mention resolves to a |
| 169 | | DID or is omitted, never guessed. |
| 180 | target or is omitted, never guessed. |
| 181 | |
| 182 | ⛔ **OPEN — a mention embeds a DID in canonical bytes.** `[@alice](did:plc:xyz)` writes a *target's* |
| 183 | DID into the record, which is the same defect §8 and `identity.md` §5 forbid for the *author's* DID: |
| 184 | when alice rotates, every mention of her everywhere points at a dead identifier, and §8 forbids |
| 185 | rewriting them. The mention target should be a `favela://` name resolved to a DID at projection time |
| 186 | — the treatment the author already gets. Not settled here. |
| 170 | 187 | |
| 171 | 188 | --- |
| 172 | 189 | |
| … | … | @@ -240,24 +257,35 @@ The AP projection is **never** truncated; AS2 has no length limit. |
| 240 | 257 | |
| 241 | 258 | ## 5. Audience |
| 242 | 259 | |
| 243 | | ### 5.1 Shape |
| 260 | ### 5.1 The room is the audience; `reach` is the field |
| 261 | |
| 262 | **Revised 2026-09-05.** This section previously specified `audience` as a list of URIs in the record. |
| 263 | Half of that is now structural and half survives. |
| 244 | 264 | |
| 245 | | A list of URIs. `https://www.w3.org/ns/activitystreams#Public` means everyone. A community is its |
| 246 | | room key as a URI; a list is its `at://` URI. |
| 265 | - **Who — the room.** You do not list an audience; you write in a room whose members *are* the |
| 266 | audience. A public room, a followers room, a community room, a two-member DM room. Enforced by |
| 267 | replication rather than by every projection remembering to honour a field |
| 268 | (`hyper-social/docs/SOCIAL-MODEL.md` §4). |
| 269 | - **Where — `reach`.** Which networks to syndicate to, within what the room permits. Stays a field, |
| 270 | because a room cannot express it. |
| 247 | 271 | |
| 248 | | URIs rather than protocol names (`["bsky","fediverse"]`) because a URI names *who*, not *where*. |
| 249 | | Adding a protocol later adds no field — it is another audience that either includes a given |
| 250 | | recipient or does not. Protocol names as flags would need a new field per protocol forever, and |
| 251 | | would put knowledge of projections inside the record, which is the thing this design exists to |
| 252 | | avoid. |
| 272 | **What the old reasoning got right, and keep.** It argued for URIs over protocol names |
| 273 | (`["bsky","fediverse"]`) because *"a URI names who, not where"*, and because protocol flags would |
| 274 | put knowledge of projections inside the record. Both correct — and the room model is that argument |
| 275 | carried to its conclusion: **the strongest way to keep projection knowledge out of the record is for |
| 276 | "who" not to be in the record at all.** `reach` is explicitly the "where" half, and is named so. |
| 253 | 277 | |
| 254 | 278 | ### 5.2 What each projection does with it |
| 255 | 279 | |
| 256 | 280 | | projection | enforcement | |
| 257 | 281 | |---|---| |
| 258 | | | **P2P (favela-web and any other client)** | not a projection at all — it is the substrate (§5.4). It renders everything it holds, and `audience` never withholds anything from it. | |
| 259 | | | **ActivityPub** | **native.** `audience` maps onto `to`/`cc`. ap-bridge already emits these — hardcoded to Public. The change is to read the value instead of assuming it. | |
| 260 | | | **atproto / bsky** | **none.** atproto has no addressing. The only lever is whether the `app.bsky.feed.post` projection is written at all. | |
| 282 | | **P2P (favela-web and any other client)** | not a projection at all — it is the substrate (§5.4). It renders every room it is a member of. | |
| 283 | | **ActivityPub** | **native.** Room membership maps onto `to`/`cc`; a followers room onto the followers collection. ap-bridge already emits these, hardcoded to Public — the change is to derive them from the room. | |
| 284 | | **atproto / bsky** | **none.** atproto has no addressing. Only a **public room** can project at all; for any other room the `app.bsky.feed.post` projection is simply not written. | |
| 285 | |
| 286 | ⚠ The asymmetry is structural, not an implementation gap: AP maps well **because AP has addressing** |
| 287 | and atproto maps badly **because it has none**. That is the same reason ap-bridge is a fifth the size |
| 288 | of the atproto path. |
| 261 | 289 | |
| 262 | 290 | ### 5.3 The honesty requirement — non-negotiable |
| 263 | 291 | |
| … | … | @@ -274,6 +302,9 @@ remains "community, not secret". |
| 274 | 302 | |
| 275 | 303 | ### 5.4 ★ Local is the SUBSTRATE, not a scope — reach is N independent toggles |
| 276 | 304 | |
| 305 | ✅ **Survives the room change unchanged (checked 2026-09-05)** — and reads more naturally under it: |
| 306 | "local" is the room you wrote in, always on because writing *is* placing. |
| 307 | |
| 277 | 308 | **Every post is local.** The canonical record always exists on the P2P side; that is what authoring |
| 278 | 309 | one means. "Local" is therefore not a destination you opt into, and there is no way to opt out of |
| 279 | 310 | it. What the author actually chooses is **which projections to emit**. |