| … | … | @@ -1,71 +1,95 @@ |
| 1 | 1 | # favela-peer |
| 2 | 2 | |
| 3 | | The favela daemon. Feeds, rooms and namespaces of markdown records over Hypercore. |
| 3 | **A social feed you actually hold.** Posts are markdown files in an append-only log that lives on |
| 4 | your machine. There is no server, no account, and nobody to ask. |
| 4 | 5 | |
| 5 | | **A clean break from atproto and ActivityPub.** Bridges come after the MVP, and they are |
| 6 | | *downstream* — they read this, this never reads them. |
| 6 | This is the daemon. It keeps your data, talks to other peers, and answers questions over a unix |
| 7 | socket. You read it through `favela-surface` in a browser, or the `favela` CLI below. |
| 7 | 8 | |
| 8 | | Runs under Bare: `bare index.js`. pgh installs it and the launcher runs it; there is no `pear run` |
| 9 | | path and never was. Storage is `~/.favela` and the control socket `~/.favela/favela.sock` |
| 10 | | (`FAVELA_STORAGE` / `FAVELA_SOCK` override both). `bin/favela.js` is the CLI that speaks to it. |
| 9 | ## Running |
| 11 | 10 | |
| 12 | | ## The boundary |
| 11 | ``` |
| 12 | bare index.js # or the pgh-installed launcher, favela-peer |
| 13 | ``` |
| 13 | 14 | |
| 14 | | ⛔ **This package must not import `atproto-*` or `ap-*`, and must not encode DAG-CBOR, build an MST, |
| 15 | | mint a DID or sign a commit.** That is the acceptance criterion, not an aspiration |
| 16 | | (`hyper-social` #61 §10). `test/boundary.test.js` enforces it over `lib/` and the dependency list, |
| 17 | | and it passes. |
| 15 | Storage is `~/.favela`, socket `~/.favela/favela.sock` — `FAVELA_STORAGE` and `FAVELA_SOCK` move |
| 16 | them. Set `FAVELA_DEBUG=1` for JSON logs. Only one daemon may hold a socket; a second refuses to |
| 17 | start rather than leaving the first unreachable. |
| 18 | 18 | |
| 19 | | Dependencies of substance: `favela-markdown` (the codec — a library with no storage and no |
| 20 | | network), `favela-identity`, `hyper-identity2`, `hyper-overlay`, Hypercore/corestore/Hyperswarm, |
| 21 | | `autobee`, `keet-identity-key`. |
| 19 | ## Using it |
| 22 | 20 | |
| 23 | | ## The primitives |
| 21 | `favela` is the CLI. Run it bare for the full list; these are the ones you need first: |
| 24 | 22 | |
| 25 | | - **feed** (`lib/feed.js`, `lib/feed-base.js`) — one identity's log. Its roster is DEVICE-FLAT: |
| 26 | | the writers are that identity's devices. You have exactly one. |
| 27 | | - **room** (`lib/room.js`) — many MEMBERS, each with devices, and membership is the audience. |
| 28 | | Two levels, so every authorisation question has to be asked at the right one — a role belongs to |
| 29 | | a person, not to a laptop. The vocabulary is keet's (admin / moderator / peer / read-only); the |
| 30 | | engine is **autobee**, not autobase, because autobase's `floor(N/2)+1` quorum freezes at N=2 and |
| 31 | | a person's two devices make N=2 the ordinary posture. A DM is a room with one other member. |
| 32 | | - **namespace** (`lib/namespace.js`) — human names that resolve to feeds. Not an authority, a |
| 33 | | SUBSCRIPTION: two people on different namespaces resolve the same string to different feeds, and |
| 34 | | that is correct. Resolution order is handle → self-label → key. |
| 23 | ``` |
| 24 | favela whoami # this node's feed key |
| 25 | favela link # attest this device from your identity vault |
| 26 | favela claim alice # take a handle — you get e.g. alice0 |
| 27 | favela post "hello" # write a post |
| 28 | favela follow <feedKey> # record a follow AND start holding the feed |
| 29 | favela timeline # posts from every feed you hold |
| 30 | favela invite # a one-time token to sign a browser in |
| 31 | ``` |
| 35 | 32 | |
| 36 | | ⚠ The earlier model here named a *document* as the second primitive and said a room was never one. |
| 37 | | That was superseded: the room is the container, membership is the audience, and no separate |
| 38 | | document type was ever built. |
| 33 | It is a testing surface rather than the product surface — `favela-surface` is what a person uses — |
| 34 | but everything the daemon can do is reachable from it, and it is the fastest way to see whether |
| 35 | something works. |
| 39 | 36 | |
| 40 | | ## The record |
| 37 | ## The ideas you need |
| 41 | 38 | |
| 42 | | A markdown file: JSON frontmatter plus a body. Mutable, edited in place. Bytes are content-addressed |
| 43 | | in hyper-overlay; the log holds a pointer. Because it is mutable, **the rkey is the stable identity, |
| 44 | | never the hash** — see `favela-markdown`. |
| 39 | **Following is a record, not a subscription.** To follow someone you write a record in *your own* |
| 40 | feed naming theirs. Nothing is written to theirs. They need not be online, need not consent, and |
| 41 | need never know. Likes and replies work the same way. This is why a feed's writer set never grows |
| 42 | past its owner's devices no matter how popular it is — and why counts are per-observer rather than |
| 43 | summed. Your "12 likes" and mine are both honest and may differ. |
| 45 | 44 | |
| 46 | | ## Interactions |
| 45 | **The timeline is local, and it is a projection.** There is no relay and no aggregator. Your |
| 46 | timeline is computed from the feeds this node happens to be holding, so two people with identical |
| 47 | follow lists can legitimately see different timelines. A feed you follow but have not replicated |
| 48 | contributes nothing — which is correct, and is why `favela missing` exists. A gap you can name is a |
| 49 | different thing from a gap that is silently empty. |
| 47 | 50 | |
| 48 | | A like or a reply is a record in **your own** feed naming a target. Nobody becomes a writer in |
| 49 | | someone else's feed to interact with it, so a feed's writer set never grows beyond its owner's |
| 50 | | devices — and counts are per-observer, never summed. |
| 51 | **A post is a markdown file** — JSON frontmatter, then a body. It is mutable and edited in place, |
| 52 | so the content hash changes on every edit and **the name is the stable identity, never the hash**. |
| 53 | The bytes live content-addressed in hyper-overlay; the log holds a pointer. See `favela-markdown`. |
| 51 | 54 | |
| 52 | | The timeline is likewise LOCAL: a pure function of the feeds this node happens to hold. There is no |
| 53 | | relay and no aggregator, so two peers legitimately compute different timelines from the same follow |
| 54 | | list. A feed you follow but have not replicated contributes nothing — which is why `missing()` |
| 55 | | exists: a gap you can name is a different thing from a gap that is silently empty. |
| 55 | ## The three primitives |
| 56 | 56 | |
| 57 | | ## Status |
| 57 | **Feed** — one identity's log. Its writers are that identity's devices, and its roster is flat: |
| 58 | the only question is whether a writer is on it. You have exactly one. |
| 59 | |
| 60 | **Room** — many *members*, each with their own devices, and membership is the audience. Two levels |
| 61 | rather than one, which matters more than it sounds: a role belongs to a person, not to a laptop. |
| 62 | The vocabulary is keet's — admin, moderator, peer, read-only — because there was no reason to |
| 63 | invent another. A DM is a room with one other member in it. |
| 58 | 64 | |
| 59 | | **Working.** 0.0.48, installed and running on tabertron under pgh alongside `favela-surface` (the |
| 60 | | renderer) and `favela-web`. |
| 65 | The engine underneath is **autobee**, not autobase. Autobase needs `floor(N/2)+1` indexers, so at |
| 66 | N=2 one member going offline freezes the view while it keeps growing, and it cannot heal by |
| 67 | dropping to one. A person's two devices make N=2 the ordinary case, not an edge case. Weights |
| 68 | retire that whole class of problem. |
| 69 | |
| 70 | **Namespace** — human names that resolve to feeds. It is a *subscription*, not an authority: two |
| 71 | people subscribed to different namespaces resolve the same string to different feeds, and that is |
| 72 | correct rather than a conflict. We give up global uniqueness and get, in exchange, a name that is |
| 73 | always qualified by a namespace you can point at. Resolution runs handle → self-label → key, and |
| 74 | the key is always shown. |
| 75 | |
| 76 | ## The boundary |
| 77 | |
| 78 | ⛔ **No `atproto-*` or `ap-*` imports. No DAG-CBOR, no MST, no DID, no signed commit.** |
| 79 | |
| 80 | This is a clean break from atproto and ActivityPub. Bridges come *after* the MVP and they are |
| 81 | downstream — they read this, this never reads them. The moment the daemon imports the projection, |
| 82 | the dependency has inverted and the recentring has failed. |
| 83 | |
| 84 | That is an acceptance criterion rather than an aspiration (`hyper-social` #61 §10), so |
| 85 | `test/boundary.test.js` enforces it over every file in `lib/` and over the dependency list. |
| 86 | |
| 87 | ## Status |
| 61 | 88 | |
| 62 | | - `lib/` is 22 modules / ~6.3k lines; `lib/rpc.js` answers **92 methods** over the UDS. |
| 63 | | - `npm test` — **342/342 tests, 813 asserts pass** (brittle-bare, ~35s, run 2026-09-09). |
| 64 | | - Covered by tests: feeds, rooms, namespaces, names/handles, device pairing and attestation, |
| 65 | | browser sessions, files and avatars, the mirror, the follow graph, search, notifications, |
| 66 | | overlay seeding and replication. |
| 89 | Working, and in daily use on tabertron alongside `favela-surface` and `favela-web`. |
| 67 | 90 | |
| 68 | | ⚠ **10 commits are unpushed.** The forge repo `favela-peer` is at 0.0.38 (`eb3a41d`); local master |
| 69 | | is 0.0.48. `git push pear master`. |
| 91 | `npm test` runs 342 tests over feeds, rooms, namespaces, handles, device pairing and attestation, |
| 92 | browser sessions, files and avatars, the mirror, the follow graph, search, notifications and |
| 93 | replication. All passing. |
| 70 | 94 | |
| 71 | | See `hyper-social` #61 §10 for the build order and §11.2 for what would falsify it. |
| 95 | Build order and the two-peer acceptance tests are in `hyper-social` #61 §10 and §11.2. |