OverviewFilesCommitsIssuesDocsReleasesPeers

favela-peer / lib / graph.js

138 lines · javascript ref:
// The graph — a PROJECTION that renders the substrate rather than the content.
//
// ⛔ Diagnostic, not decorative. EDGE-GRAPH.md §4: an edge passes four independent steps, each of
// which fails differently and all of which currently look identical from outside:
//
//   ① WRITTEN     the pointer is in the author's writer core
//   ② CONVERGED   it linearized into the view
//   ③ RESOLVED    the bytes are locally available
//   ④ PROJECTED   a bridge emitted it
//
// "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.'" Every one of those was a real hour of this
// project's life.
//
// ⛔ No storage, no replication, no new state — the same class of thing as a timeline. It is a pure
// function of what this node already holds.

import { Peer, POST, REPLY, REACTION, REPOST, FOLLOW } from './peer.js'

const KINDS = [[REACTION, 'reaction'], [REPOST, 'repost'], [REPLY, 'reply']]

/**
 * Nodes and edges for everything this peer can see.
 *
 * ⚠ Only what it HOLDS. A graph of the network would be a different and impossible thing; this is a
 * graph of one node's view, which is the only honest one and the only useful one for diagnosis.
 */
export async function graphOf (peer) {
  const nodes = new Map()
  const edges = []
  const mine = peer.feed.key.toString('hex')

  const actor = (id) => {
    if (!nodes.has(id)) nodes.set(id, { id, type: 'actor', state: 'resolved' })
  }
  // ⚠ `at` travels because the discovery view spends a spatial axis on TIME — a cluster forming
  // reads as a widening cone, which is not expressible in 2D without animation. It is the author's
  // claim, not a fact the log agrees on, exactly as `timeline()` documents.
  const record = (id, state, at) => {
    const prev = nodes.get(id)
    if (!prev) { nodes.set(id, { id, type: 'record', state, at: at || null }); return }
    // ⛔ LATER, BETTER information wins — the first sighting is not the authoritative one. A record
    // named by an edge is known only by id: no time, no state. The feed that actually HOLDS it is
    // walked afterwards whenever it sorts second, which for anything this node reacted to is
    // always, since its own feed is walked first. Skipping that second sighting left the post
    // undated at z=0 and marked 'unknown' — not a gap in the picture but a wrong position in it.
    if (prev.at == null && at) prev.at = at
    if (prev.state === 'unknown' && state !== 'unknown') prev.state = state
  }
  const rid = (t) => t.feed + '/' + t.collection + '/' + t.name

  /**
   * ⛔ AUTHORSHIP IS AN EDGE, not just a prefix on an id.
   *
   * It used to live only inside `<feed>/post/<name>`, which a force layout cannot see. So a post sat
   * wherever its REACTIONS dragged it — beside whoever liked it rather than whoever wrote it — and a
   * post nobody had reacted to sat nowhere: measured on tabertron 2026-09-07, 14 posts and 11 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.
   *
   * ⛔ Only for a feed this node HOLDS. A like on a stranger's post names a record whose id reveals
   * its author's key — and asserting the edge from that would draw a relation this node cannot see.
   * `_readable()` is the boundary, and every call here is inside that walk.
   */
  const seenAuthorship = new Set()
  const authored = (author, id, at) => {
    if (seenAuthorship.has(id)) return
    seenAuthorship.add(id)
    edges.push({ kind: 'authored', from: author, to: id, value: null, at, state: 'converged' })
  }

  for (const [author, feed] of peer._readable()) {
    actor(author)

    // ③ RESOLVED vs ② CONVERGED. `unresolved()` is exactly "we hold the pointer and cannot read the
    // bytes" — the state that used to be indistinguishable from the record not existing.
    for (const row of await feed.unresolved()) {
      record(author + '/' + row.collection + '/' + row.name, 'converged', null)
      // ⚠ The pointer names the feed that wrote it whether or not the bytes ever arrived. Dropping
      // the edge here would strand exactly the records the view exists to draw attention to.
      authored(author, author + '/' + row.collection + '/' + row.name, null)
    }
    for (const row of await feed.list(POST)) {
      const fm = (row.record && row.record.frontmatter) || {}
      record(author + '/' + POST + '/' + row.name, 'resolved', fm.createdAt || null)
      authored(author, author + '/' + POST + '/' + row.name, fm.createdAt || null)
    }

    for (const [collection, kind] of KINDS) {
      for (const row of await feed.list(collection)) {
        const fm = (row.record && row.record.frontmatter) || {}
        const t = Peer.normalizeTarget(fm.target)
        if (!t) continue
        record(rid(t), 'unknown', null)
        edges.push({
          kind,
          from: author,
          to: rid(t),
          value: fm.value || null,
          at: fm.createdAt || null,
          // ⛔ The edge's own state, which is not the same as its subject's. An edge we hold is
          // written and converged BY DEFINITION — we are reading it out of the view. What we cannot
          // see from here is whether it reached anybody else, and the view must not imply it did.
          state: 'converged'
        })
      }
    }

    // A follow names an actor, not a record. ⛔ Two follows are two edges — the common bug is one
    // direction landing and the other not, and a double-headed arrow hides exactly that.
    for (const row of await feed.list(FOLLOW)) {
      actor(row.name)
      edges.push({
        kind: 'follow',
        from: author,
        to: row.name,
        value: null,
        at: (row.record && row.record.frontmatter && row.record.frontmatter.createdAt) || null,
        state: 'converged'
      })
    }
  }

  return {
    me: mine,
    nodes: [...nodes.values()],
    edges,
    // ⚠ UNKNOWN, not "no". No bridge is wired to this daemon, and a state we cannot observe must
    // not be drawn as one we can — the same lie as rendering `unheld` as `confirmed`.
    projection: 'unknown'
  }
}

export default { graphOf }