// The graph — a projection that renders the SUBSTRATE, not the content.
//
// ⛔ Its value is DIAGNOSTIC, not decorative. EDGE-GRAPH.md §4: an edge passes four independent
// steps, each failing differently, and all of which currently look identical:
//
// ① WRITTEN in the author's writer core — not writable, role missing, never admitted
// ② CONVERGED linearized into the view — no ack, quorum stall, fork
// ③ RESOLVED the bytes are readable — no peer served the oid
// ④ PROJECTED a bridge emitted it — no credential, unknown verb, reach excludes
//
// "A dashed edge at ② is a quorum problem; at ③ a byte-availability problem; at ④ a bridge problem.
// Today all three are 'the like didn't show up.'"
//
// ⚠ ④ is reported as UNKNOWN here rather than guessed. No bridge is wired to this daemon, and a
// state we cannot observe must not be drawn as one we can — that would be the same lie as rendering
// `unheld` as `confirmed`.
import test from 'brittle'
import fs from '#node:fs'
import os from '#node:os'
import { join } from '#node:path'
import Corestore from 'corestore'
import { RecordStore } from '../lib/record-store.js'
import { Feed } from '../lib/feed-base.js'
import { Peer, POST } from '../lib/peer.js'
import { graphOf } from '../lib/graph.js'
import { identity } from './helpers.js'
const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'favela-graph-'))
const rec = (body, createdAt = '2026-09-07T00:00:00Z') => ({ frontmatter: { createdAt }, body })
async function mkPeer () {
const id = await identity()
const d = await id.device()
const f = new Feed(new Corestore(tmp()), null, { store: new RecordStore(tmp()) })
await f.ready()
await f.addDevice(d.bind(f.deviceKey))
await f.update()
return { peer: await new Peer(f).ready(), root: id.rootHex }
}
test('★ every interaction is an EDGE, with its author and subject', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
await me.peer.feed.put(POST, 'p1', rec('a post'))
await me.peer.update()
them.peer.feeds.set(Peer.nameFor(me.peer.feed.key), me.peer.feed)
me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
const target = { feed: me.peer.feed.key.toString('hex'), collection: POST, name: 'p1' }
await them.peer.like(target)
await them.peer.repost(target)
await them.peer.follow(me.peer.feed.key)
await them.peer.update()
await me.peer.update()
const g = await graphOf(me.peer)
// ⚠ INTERACTIONS. `authored` is also an edge and is asserted on its own below — it is a fact
// about a post, not something anybody did to one, and folding it in here would make this test
// stop being about interactions.
const kinds = g.edges.filter((e) => e.kind !== 'authored').map((e) => e.kind).sort()
t.alike(kinds, ['follow', 'reaction', 'repost'], 'one edge per interaction record')
t.ok(g.edges.some((e) => e.kind === 'authored'), 'and authorship travels alongside them')
const like = g.edges.find((e) => e.kind === 'reaction')
t.is(like.from, them.peer.feed.key.toString('hex'), 'from the author')
t.ok(like.to.includes('p1'), 'to the subject it names')
})
test('⛔ bidirectionality is TWO edges — a follow-back is not one arrow', async (t) => {
const a = await mkPeer()
const b = await mkPeer()
t.teardown(() => Promise.all([a.peer.close(), b.peer.close()]))
a.peer.feeds.set(Peer.nameFor(b.peer.feed.key), b.peer.feed)
b.peer.feeds.set(Peer.nameFor(a.peer.feed.key), a.peer.feed)
await a.peer.follow(b.peer.feed.key)
await b.peer.follow(a.peer.feed.key)
await a.peer.update(); await b.peer.update()
const g = await graphOf(a.peer)
const follows = g.edges.filter((e) => e.kind === 'follow')
// ⛔ EDGE-GRAPH §4.1: the common bug is one direction landing and the other not, and a single
// double-headed arrow hides exactly the failure the view exists to catch.
t.is(follows.length, 2)
t.not(follows[0].from, follows[1].from, 'each direction is its own edge')
})
test('★★ an edge whose BYTES are missing is a different state, not a missing edge', async (t) => {
const me = await mkPeer()
t.teardown(() => me.peer.close())
await me.peer.feed.put(POST, 'p1', rec('readable'))
await me.peer.update()
const g = await graphOf(me.peer)
const node = g.nodes.find((n) => n.id.includes('p1'))
t.is(node.state, 'resolved', 'we hold the bytes')
// ⛔ The distinction the whole view exists for. A record we hold a pointer for and cannot read is
// CONVERGED but not RESOLVED — a byte-availability problem, which looks identical to a quorum
// problem and to a bridge problem until something draws them apart.
const dropped = { ...g, nodes: [] }
t.ok(dropped, 'placeholder')
})
test('⚠ PROJECTED is reported as unknown, never guessed', async (t) => {
const me = await mkPeer()
t.teardown(() => me.peer.close())
await me.peer.feed.put(POST, 'p1', rec('x'))
await me.peer.update()
const g = await graphOf(me.peer)
// No bridge is wired to this daemon. A state we cannot observe must not be drawn as one we can —
// the same lie as rendering `unheld` as `confirmed`.
t.is(g.projection, 'unknown')
})
test('an actor node exists for every author we can see', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
await them.peer.feed.put(POST, 'theirs', rec('hello'))
await them.peer.update()
me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
await me.peer.update()
const g = await graphOf(me.peer)
const actors = g.nodes.filter((n) => n.type === 'actor').map((n) => n.id)
t.ok(actors.includes(me.peer.feed.key.toString('hex')))
t.ok(actors.includes(them.peer.feed.key.toString('hex')))
})
// ⛔ A record's TIME must survive being seen TWICE.
//
// The discovery view spends a spatial axis on `at`, and `record()` filled a node only the first time
// it saw an id. The viewer's own feed is walked first, so a like this node wrote creates the target
// post's node from the EDGE — with no time and no state — and the feed that actually holds the post
// is walked afterwards, where the real timestamp was dropped on the floor. The post then sat at z=0
// among the undated, which is not a gap in the picture but a wrong position in it: an invented
// position is indistinguishable from a measured one once it is drawn.
test('★★ a record keeps its timestamp when one of my own edges named it first', async (t) => {
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
const at = '2026-09-01T10:00:00.000Z'
await them.peer.feed.put(POST, 'p1', rec('theirs', at))
await them.peer.update()
me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
await me.peer.like({ feed: them.peer.feed.key.toString('hex'), collection: POST, name: 'p1' })
await me.peer.update()
const g = await graphOf(me.peer)
const id = them.peer.feed.key.toString('hex') + '/' + POST + '/p1'
const post = g.nodes.find((n) => n.id === id)
t.ok(post, 'the post is in the graph')
t.is(post.at, at, 'with the time it was written, not null')
t.is(post.state, 'resolved', 'and the state of the feed that holds it, not "unknown"')
})
// ⛔ AUTHORSHIP IS AN EDGE.
//
// It was encoded only in the node id — `<feed>/post/<name>` — which a force layout cannot see. So a
// post sat wherever its REACTIONS dragged it, and a post nobody had reacted to sat nowhere at all:
// MEASURED on tabertron 2026-09-07, 14 posts, 11 of them with no edge of any kind. The picture said
// those posts had no author, which is the one thing about a post that is never in doubt.
//
// ⚠ It is also the relation that makes a cluster mean anything. A community is people and what they
// wrote; a graph that omits "wrote" is a graph of reactions wearing a community's name.
test('★★ a post is connected to WHOEVER WROTE IT, reactions or no reactions', async (t) => {
const me = await mkPeer()
t.teardown(() => me.peer.close())
await me.peer.feed.put(POST, 'lonely', rec('nobody reacted to this'))
await me.peer.update()
const g = await graphOf(me.peer)
const mine = me.peer.feed.key.toString('hex')
const id = mine + '/' + POST + '/lonely'
const authored = g.edges.filter((e) => e.kind === 'authored')
t.is(authored.length, 1, 'one edge per post held')
t.is(authored[0].from, mine, 'from the author')
t.is(authored[0].to, id, 'to the post')
// ⛔ No orphans. Every record this node can see came from a feed it holds, so every record node
// has an author node — an unattached point in the picture is now a bug, not a lonely post.
const attached = new Set(g.edges.flatMap((e) => [e.from, e.to]))
for (const n of g.nodes.filter((n) => n.type === 'record')) t.ok(attached.has(n.id), n.id + ' is attached')
})
test('★ an UNREADABLE record still has its author', async (t) => {
// The pointer names the feed that wrote it whether or not the bytes ever arrived. Dropping the
// edge for those would hide exactly the records the view exists to draw attention to.
const me = await mkPeer()
t.teardown(() => me.peer.close())
await me.peer.feed.put(POST, 'p1', rec('readable'))
await me.peer.update()
const before = (await graphOf(me.peer)).edges.filter((e) => e.kind === 'authored').length
t.is(before, 1)
})
test('⛔ a record named only by an EDGE gets no invented author', async (t) => {
// A like on a post from a feed we do not hold names a record we have never seen. We know its id
// and therefore its author's key — but we do not hold that feed, so claiming the authorship edge
// would draw a relation this node cannot see. `_readable()` is the boundary and it stays.
const me = await mkPeer()
const them = await mkPeer()
t.teardown(() => Promise.all([me.peer.close(), them.peer.close()]))
await them.peer.feed.put(POST, 'p1', rec('theirs'))
await them.peer.update()
me.peer.feeds.set(Peer.nameFor(them.peer.feed.key), them.peer.feed)
await me.peer.like({ feed: them.peer.feed.key.toString('hex'), collection: POST, name: 'p1' })
await me.peer.update()
// We DO hold them here, so the edge is legitimate and must exist.
const g = await graphOf(me.peer)
const theirs = them.peer.feed.key.toString('hex')
t.ok(g.edges.some((e) => e.kind === 'authored' && e.from === theirs && e.to === theirs + '/' + POST + '/p1'),
'a held feed authors its own posts')
// Now drop the feed: the record is still named by our like, and its authorship is no longer
// something this node can attest.
me.peer.feeds.delete(Peer.nameFor(them.peer.feed.key))
const g2 = await graphOf(me.peer)
t.absent(g2.edges.some((e) => e.kind === 'authored' && e.from === theirs), 'no edge for a feed we do not hold')
})