OverviewFilesCommitsIssuesDocsReleasesPeers

favela-markdown commits / a95388c

spec: reconcile to the room as container
interaction.md §0.1 (one log -> rooms), §3.4 (URI authority is the room key; identity-root is the personal-room case), §4.5 (audience is structural, the computed meet dissolves). design.md §5.1 (visibility is a place; reach stays a field). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
a95388c9a9cdf234b059fbb444691bc41bf1d774
by plankatron <fleeky@gmail.com> · 2026-09-05T19:53:56.000Z

Changed files (2) +80 -54

spec/design.md +30 -13
……@@ -116,19 +116,36 @@ the ending
116116:::
117117```
118118
119### 5.1 `visibility` and `reach` are two fields, not one
120
121- **`visibility` — who.** AS2 actors and collections. Durable and semantic. It sets the *maximum
122 possible* reach: a followers-only post can never go to bsky, because atproto cannot express
123 addressing at all. Typed and unambiguous, because a parse error here is a privacy bug.
124- **`reach` — where.** Which networks we actually syndicate to, within what `visibility` permits.
125 Operational; a bad value means a post does not syndicate, which is visible and recoverable.
126
127Reach is derivable from visibility; visibility is not derivable from reach. Storing only networks
128throws away intent, and a network added years later would require touching every record.
129
130**Not named `audience`:** AS2 defines `audience` as relevance, not addressing, and Mastodon does not
131route on it. Our field maps down to `to`/`cc`, which ap-bridge already emits.
119### 5.1 `visibility` is a PLACE; `reach` is a field
120
121**Revised 2026-09-05** (`hyper-social/docs/SOCIAL-MODEL.md`). This section previously made both of
122them frontmatter fields. Only one of them should ever have been.
123
124- **`visibility` — who. Now structural.** You do not mark a post followers-only; you **write it in a
125 room whose members are your followers**. A public post goes in your public room, a DM in a
126 two-member room, a community post in that community's room. Membership is the audience, and it is
127 enforced by replication rather than honoured by convention. A field is a promise every projection
128 must remember; a room is architecture, not policy.
129- **`reach` — where. Stays a field.** Which networks a record syndicates to, within what the room
130 permits. A room cannot express this: "which networks" is operational, not audience. A bad value
131 means a post does not syndicate — visible and recoverable, unlike a privacy failure.
132
133The original asymmetry survives the change and is now clearer: **the room sets the ceiling, `reach`
134chooses within it.** A followers-room post can never go to bsky, because atproto cannot express
135addressing at all — that is no longer a check to remember but a fact about where the record lives.
136
137**Why the field version was wrong.** A typed `visibility` field was chosen over YAML because
138`audience: no` parsing as `false` is a privacy bug. That reasoning was right about the danger and
139wrong about the fix: the way to stop a parse error becoming a privacy bug is to not depend on parsing
140for privacy at all.
141
142**Not named `audience`:** retained for the record — AS2 defines `audience` as relevance, not
143addressing, and Mastodon does not route on it. Room membership maps down to `to`/`cc`, which
144ap-bridge already emits.
145
146⚠ The frontmatter example above still shows a `visibility` array. It is left in place because
147`record-vocabulary.md` §3/§5 have not yet been rewritten (hyper-social #57 B1–B3); that rewrite is
148now also a room-model rewrite.
132149
133150### 5.2 Prose
134151
spec/interaction.md +50 -41
……@@ -20,14 +20,21 @@ default, not by decision"* — still running in the half nobody specified.
2020
2121### 0.1 The frame
2222
23There is **one log of named records**, and everything else is a projection of it, derived on read
24and never replicated.
23**The ROOM is the container.** Membership is the audience; the log is the history
24(`hyper-social/docs/SOCIAL-MODEL.md`). A room is a multi-writer autobase, and everything else is a
25projection of its records, derived on read and never replicated.
2526
26- **The log** — the shared StateBase autobase: `name -> { $oid, size }`. Names are
27 `<collection>/<rkey>`. **Authorship lives here** and nowhere else.
27- **A room** — the unit of scope, audience and governance. `<roomKey>` addresses it.
28- **The log** — that room's StateBase autobase: `name -> { $oid, size }`, names `<collection>/<rkey>`.
29 **Authorship lives here** and nowhere else.
2830- **The bytes** — content-addressed files in hyper-overlay. A record's content.
29- **A projection** — a pure function from records to some other representation: a timeline, a
30 notification list, `app.bsky.feed.like`, an AS2 `Like`.
31- **A projection** — a pure function from a room's records to some other representation: a timeline,
32 a notification list, `app.bsky.feed.like`, an AS2 `Like`.
33
34⚠ **Revised 2026-09-05.** This section previously read *"there is one log of named records"* — a
35single per-identity log, which is atproto's shape inherited by default. A **personal feed is a room
36you own with one writer**, so the per-identity repo is the degenerate case of a room, not the general
37container. What follows in this document is written against the room.
3138
3239⛔ **A projection must never constrain the log.** ActivityPub never did — it derives on read and
3340holds its own key. atproto did: it dictated the encoding, the signing and the storage layout, and
……@@ -122,16 +129,26 @@ maintain. Ours are *derived* (§3.1); foreign ones are *asserted* when observed
122129**A DID rotation therefore adds an edge; it never rewrites a record.** This is the same mechanism
123130that fixes the mention problem in `record-vocabulary.md:166,208`, rather than a special case for it.
124131
125### 3.4 The native URI — `favela://<identity-root>/<collection>/<rkey>`
132### 3.4 The native URI — `favela://<roomKey>/<collection>/<rkey>`
126133
127134**Decision (user, 2026-09-05).** `at://` is atproto's and `identity.md` §5 has just removed the DID
128135from the author position so it can rotate, so a native scheme is required — one favela peer could
129136otherwise not name another's record without reintroducing the DID.
130137
131**The authority segment is the hex identity root** (`pgh/lib/identity.js:101 identityRootHex`), the
132hyperident/keet root that sits ABOVE both the DID and the device keys. Under the SLIP-48
133`accountIndex` model each user is a distinct derived key (`keet-identity-key/index.js:227`), so a
134user's identity root IS the global name of the `User` that `identity.md` §5 asks records to carry.
138**The authority segment is the ROOM key**, because the room is the container (§0.1) — a record's
139address must name where it lives, not who wrote it. Authorship is inside the log, and a record moved
140between rooms is a new authorship act, never the same record at a new address
141(`SOCIAL-MODEL.md` §9 #5).
142
143**A personal room's key is derivable from its owner's identity root** (`pgh/lib/identity.js:101
144identityRootHex`) — the hyperident/keet root that sits ABOVE both the DID and the device keys, and
145under the SLIP-48 `accountIndex` model each user is a distinct derived key
146(`keet-identity-key/index.js:227`). So `favela://<identity-root>/…` survives as the **personal-room
147case**, resolving to that room, while a community record names the community's room key.
148
149⚠ **Revised 2026-09-05.** An earlier version of this section made the identity root the authority
150segment in general. That was written while the per-identity repo was still assumed to be the
151container; it is now one case of one.
135152
136153**A key in the bytes; a name only on screen.** pgh's `NameRegistry`
137154(`pgh/lib/name-registry.js:74`) is the resolver for display and discovery — it already anchors
……@@ -254,39 +271,31 @@ projections is local-only, which unifies the two ways that is expressible today
254271
255272---
256273
257### 4.5 An edge's reach is bounded by its SUBJECT
258
259`maxReach` is a lattice (`library.md` §6.1). An edge sits at the **meet** of two constraints, not
260one:
261
262```
263reach(edge) = maxReach(edge.visibility, edge.verb) ∩ maxReach(subject.visibility)
264```
274### 4.5 An edge's audience is STRUCTURAL — it is the room it was written in
265275
266The second term is the one that is easy to forget and expensive to get wrong. **An edge discloses
267its subject.** Projecting a `like` on a followers-only post to bsky publishes two facts — that the
268post exists, and who engaged with it — even when the like itself was marked public. The subject's
269audience is therefore a ceiling on the edge, exactly as `visibility` is a ceiling on `reach` for a
270post (`design.md` §5.1).
276**Revised 2026-09-05.** This section previously computed an edge's reach as the meet of its own
277`maxReach` and its subject's. That machinery exists to solve a problem the room removes.
271278
272**When the subject's visibility cannot be determined, the meet is `{favela}`.** Fail-closed, the same
273direction as an unparseable frontmatter (`library.md` §6.2) and an unknown verb (§5.2).
279**An interaction is written in the room where its subject lives.** Three things follow, none of which
280has to be enforced:
274281
275Two cases behave differently and both are correct:
282- **It is visible to exactly the people who can see the subject.** Not a rule — the only place it
283 could have been written.
284- **You cannot interact with what you cannot see**, because there is no room to write in.
285- **Write access to a room IS interaction permission.** atproto's threadgate and postgate become room
286 roles, which keet already has (admin / moderator / peer / read-only).
276287
277- **A foreign subject** (a bsky post, a Mastodon status) is already public on the network it came
278 from, so the meet is unrestrictive. This is the common case and it costs nothing.
279- **A favela-native subject with restricted visibility** is where the rule bites, and it is the case
280 that does not exist yet — which is precisely why it must be written before it does.
288This is `COMMUNITY-EMERGENCE.md`'s privacy principle applied to authoring: **architecture, not
289policy.** A computed bound is a promise every projection must remember to honour and leaks the first
290time one forgets; a room membership is enforced by replication itself.
281291
282`reply` inherits rather than intersects: a reply to a restricted post takes its parent's
283`visibility`, not merely a bound on its reach. `record-vocabulary.md` §11 #5 calls this *"almost
284certainly inherit"* — it is, and the reason to state it explicitly rather than let it emerge is that
285the failure mode is publishing to the wrong people.
292`reach` **survives as a field**, because a room cannot express *which networks* a record syndicates
293to — that is operational, not audience (`design.md` §5.1). `maxReach` therefore narrows to one job:
294refusing a projection a network cannot express, and being total over verbs (§5.2).
286295
287⚠ **Limit, not solved here.** A subject can become *more* restricted after an edge on it was already
288projected. The edge cannot be recalled by the lattice; recalling it is the retraction problem, and
289that is per-bridge delivery bookkeeping (`identity.md` §6.3), not a property of the record.
296⚠ **Limit, not solved here.** A room's membership can shrink after an edge in it was already
297projected outward. Recall is per-bridge delivery bookkeeping (`identity.md` §6.3), not a property of
298the record.
290299
291300---
292301
……@@ -438,9 +447,9 @@ content one shape, and an inbound edge is the same idea applied to relationships
4384472. ~~**The native URI scheme**~~ **CLOSED 2026-09-05** — `favela://<identity-root>/…`, §3.4. The
439448 remaining work is the fail-closed minting rule (§3.4.1), not a decision.
4404493. **Inbox admission** (§6) — rate limiting, and how long an unverified claim is retained.
4414. ~~**Where an edge's `reach` defaults from.**~~ **CLOSED 2026-09-05** — the meet of the edge's own
442 maxReach and the subject's, fail-closed to `{favela}` when the subject is unknown; `reply`
443 inherits. §4.5. What remains open is *recall* after a subject is restricted later (§4.5 limit).
4504. ~~**Where an edge's `reach` defaults from.**~~ **CLOSED 2026-09-05, then RESOLVED STRUCTURALLY** —
451 an edge's audience is the room it was written in (§4.5); no computed bound is needed. `reach` (which
452 networks) remains a field. What remains open is *recall* after a room's membership shrinks.
4444535. **Multi-DID id purity.** `records.js:72` warns the AP id functions stop being pure *"the moment we
445454 front more than one DID"*, and favela DIDs are co-owned communities. Edges make this sharper,
446455 because an edge names a subject that may belong to another fronted DID.