docs: mesh multiplayer protocol with dot diagrams

Adds docs/mesh.md and five graphviz diagrams covering the federation
layer: identity stack, epoch state machine, per-member secret envelope,
gossip wire contract, and operator decision tree. Pins the protocol
contract for the upcoming HTTP wire (mesh sync, mesh serve).

Makefile gets a 'docs' target with pattern rule so PNG renders are
incremental from .dot sources.
This commit is contained in:
russell@unturf.com 2026-04-28 12:54:36 -04:00
parent 1f5bf7318d
commit c7275b5618
No known key found for this signature in database
12 changed files with 510 additions and 1 deletions

View file

@ -30,7 +30,7 @@ SEARCH_Q ?= computer
ingest-xml-attached ingest-abstract \
ingest-grok ingest-grok-media \
ingest-self ingest-git ingest-hg \
verify search stats test clean clean-db clean-data help
verify search stats test docs clean clean-db clean-data help
all: bootstrap fetch-cur ingest-cur verify stats ## bootstrap → fetch cur → ingest cur → verify → stats
@ -282,6 +282,14 @@ stats: bootstrap ## counts: documents, chunks, edges, audit chain
test: bootstrap ## run pytest suite
$(VENV)/bin/pytest -q
DOT_SRCS := $(wildcard docs/diagrams/*.dot)
DOT_PNGS := $(DOT_SRCS:.dot=.png)
docs/diagrams/%.png: docs/diagrams/%.dot
dot -Tpng $< -o $@
docs: $(DOT_PNGS) ## render docs/diagrams/*.dot -> .png via graphviz
# Reproducible micro-benchmark over a fixed slice of cur. Lets you compare
# ETL throughput across configs and catches regressions on optimization
# work. Override BENCH_DOCS=N (default 5000).

View file

@ -0,0 +1,73 @@
// aborist/mesh — what flows between peers.
//
// Aborist is content-addressed: two peers ingesting the same dump
// compute bit-identical document_roots. Gossip exchanges identifiers,
// derivations, falsifications, and witness signatures. Bulk content
// only flows on cache miss, by Merkle proof of inclusion.
//
// HTTP wire (mesh sync, mesh serve) is the next milestone; this
// diagram fixes the protocol contract that wire will implement.
//
// Render: dot -Tpng docs/diagrams/mesh-data-flow.dot -o /tmp/x.png
digraph mesh_data_flow {
rankdir=LR;
bgcolor="white";
compound=true;
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
subgraph cluster_alice {
label="peer: alice (admin)";
style="rounded,filled";
fillcolor="#fff7d6";
a_docs [label="documents\n(surface + core)", fillcolor="#ffffff"];
a_deriv [label="derivations\n(surface -> core proof_blob)", fillcolor="#ffffff"];
a_prov [label="providence_cache\n(8-dim cache_key, audit_mode)", fillcolor="#ffffff"];
a_falsif [label="falsifications\n('this answer is wrong')", fillcolor="#ffffff"];
a_audit [label="audit_events\n(append-only chain)", fillcolor="#ffffff"];
}
subgraph cluster_bob {
label="peer: bob (member)";
style="rounded,filled";
fillcolor="#d6ecff";
b_docs [label="documents\n(surface + core)", fillcolor="#ffffff"];
b_deriv [label="derivations", fillcolor="#ffffff"];
b_prov [label="providence_cache", fillcolor="#ffffff"];
b_falsif [label="falsifications", fillcolor="#ffffff"];
b_audit [label="audit_events", fillcolor="#ffffff"];
}
subgraph cluster_wire {
label="gossip messages (Ed25519-signed, optional AEAD)";
style="rounded,dashed";
color="#666666";
m_root [label="ANNOUNCE_ROOT\ndocument_root, source_uri,\nchunking_version, schema_version", fillcolor="#e8ffe8"];
m_der [label="ANNOUNCE_DERIVATION\ncore_root <- surface_root(s),\ndistiller_id, proof_blob hash", fillcolor="#e8ffe8"];
m_prov [label="ANNOUNCE_PROVIDENCE\ncache_key, audit_mode,\nanswer_hash", fillcolor="#e8ffe8"];
m_fals [label="ANNOUNCE_FALSIFICATION\ncache_key, reason, signed by witness", fillcolor="#ffe0e0"];
m_pull [label="REQUEST_BODY\ndocument_root or chunk_root\n(only on local miss)", fillcolor="#fff7d6"];
m_resp [label="DELIVER_BODY\nbytes + Merkle proof of inclusion\nrecipient verifies before insert", fillcolor="#fff7d6"];
}
a_docs -> m_root [lhead=cluster_wire];
a_deriv -> m_der [lhead=cluster_wire];
a_prov -> m_prov [lhead=cluster_wire];
a_falsif -> m_fals [lhead=cluster_wire];
m_root -> b_docs [ltail=cluster_wire];
m_der -> b_deriv [ltail=cluster_wire];
m_prov -> b_prov [ltail=cluster_wire];
m_fals -> b_falsif [ltail=cluster_wire];
b_docs -> m_pull [label="miss on root", color="#cc6600"];
m_pull -> a_docs [color="#cc6600"];
a_docs -> m_resp [color="#cc6600"];
m_resp -> b_docs [label="verify proof,\nthen ingest", color="#cc6600"];
a_audit -> b_audit [label="audit chain merge\n(prev_event_hash gates inserts)", style=dashed, color="#0066cc", constraint=false];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

View file

@ -0,0 +1,38 @@
// aborist/mesh — epoch state machine.
//
// Every roster mutation bumps a counter. Old epochs persist forever
// (signatures stay verifiable). New gossip is encrypted under the
// post-rotation secret, so evicted members go opaque from epoch+1.
//
// Render: dot -Tpng docs/diagrams/mesh-epoch-lifecycle.dot -o /tmp/x.png
digraph mesh_epoch_lifecycle {
rankdir=LR;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
none [label="no mesh state\n(meta.mesh.enabled = 0)", fillcolor="#eeeeee"];
e0 [label="epoch 0\nfounder = sole admin\nreason: 'genesis'", fillcolor="#fff7d6"];
e1 [label="epoch N\nroster + envelope\nsigned by actor", fillcolor="#d6ffd6"];
eN [label="epoch N+1\nfresh secret\nfresh envelope", fillcolor="#d6ffd6"];
historic [label="every prior epoch\nstays on disk\n(roster + signatures verifiable)", fillcolor="#ffffff", shape=note];
none -> e0 [label="aborist mesh init --group <name>"];
e0 -> e1 [label="add | kick | rotate"];
e1 -> eN [label="add | kick | rotate"];
eN -> eN [label="rotate (no roster change)"];
e1 -> historic [style=dashed, arrowhead=none, color="#888888"];
eN -> historic [style=dashed, arrowhead=none, color="#888888"];
subgraph cluster_ops {
label="Operator commands";
style="rounded,dashed";
color="#666666";
node [fillcolor="#e8f0ff", shape=note];
op_add [label="aborist mesh add\n--member-id bob ...\nadmin only"];
op_kick [label="aborist mesh kick\n--member-id bob ...\nadmin only"];
op_rotate [label="aborist mesh rotate\nany current member"];
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

View file

@ -0,0 +1,50 @@
// aborist/mesh — group operator decision tree.
//
// When a group steward (admin) faces a roster question, this diagram
// shows which CLI verb solves it and what guarantee comes out the
// other side.
//
// Render: dot -Tpng docs/diagrams/mesh-group-decisions.dot -o /tmp/x.png
digraph mesh_group_decisions {
rankdir=TB;
bgcolor="white";
node [fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
start [label="group steward has a question", shape=ellipse, style="filled", fillcolor="#eeeeee"];
q_first [label="is mesh\ninitialized\nyet?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_role [label="is the question\nabout people\nor secrets?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_who [label="add, remove,\nor refresh?", shape=diamond, style="filled", fillcolor="#fff7d6"];
q_admin [label="are you\nadmin in\ncurrent epoch?", shape=diamond, style="filled", fillcolor="#fff7d6"];
a_init [label="aborist mesh init --group <name>\nthen mesh enable\n(creates epoch 0; founder = sole admin)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_add [label="aborist mesh add\n--member-id <id>\n--sign-pub <hex>\n--dh-pub <hex>\n[--role admin]", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_kick [label="aborist mesh kick\n--member-id <id>\n--reason '...'", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
a_rotate [label="aborist mesh rotate\n--reason '...'\n(any current member)", shape=box, style="rounded,filled", fillcolor="#d6ffd6"];
g_admin [label="permission denied\n(only admins of the\ncurrent epoch may add or kick)", shape=box, style="rounded,filled", fillcolor="#ffe0e0"];
g_add [label="new member can decrypt\nepoch+1 forward;\ncannot reach prior epochs", shape=note, style="filled", fillcolor="#ffffff"];
g_kick [label="kicked member's prior\nsignatures stay valid forever;\nopaque to gossip from epoch+1 on", shape=note, style="filled", fillcolor="#ffffff"];
g_rot [label="fresh secret on the same roster;\nuse on suspected secret leak\nor scheduled hygiene", shape=note, style="filled", fillcolor="#ffffff"];
start -> q_first;
q_first -> a_init [label="no"];
q_first -> q_role [label="yes"];
q_role -> q_who [label="people"];
q_role -> a_rotate [label="just rotate the secret"];
q_who -> q_admin [label="add or kick"];
q_who -> a_rotate [label="refresh secret only"];
q_admin -> a_add [label="yes (admin) -> add"];
q_admin -> a_kick [label="yes (admin) -> kick"];
q_admin -> g_admin [label="no"];
a_add -> g_add [arrowhead=none, style=dotted];
a_kick -> g_kick [arrowhead=none, style=dotted];
a_rotate -> g_rot [arrowhead=none, style=dotted];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View file

@ -0,0 +1,51 @@
// aborist/mesh — cryptographic identity stack per peer.
//
// Each peer carries two keypairs and one shared epoch secret.
// Keys are raw 32-byte forms; everything lives in the standard SQLite db.
//
// Render: dot -Tpng docs/diagrams/mesh-identity-stack.dot -o /tmp/x.png
digraph mesh_identity_stack {
rankdir=TB;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
subgraph cluster_peer {
label="One peer (e.g. alice)";
style="rounded,dashed";
color="#666666";
sign_priv [label="Ed25519 priv\n(sign_priv, 32B)", fillcolor="#ffe8a3"];
sign_pub [label="Ed25519 pub\n(sign_pub, 32B)\nshared with group", fillcolor="#fff7d6"];
dh_priv [label="X25519 priv\n(dh_priv, 32B)", fillcolor="#a3d8ff"];
dh_pub [label="X25519 pub\n(dh_pub, 32B)\nshared with group", fillcolor="#d6ecff"];
member_id [label="member_id\n8 hex chars", fillcolor="#eeeeee"];
sign_priv -> sign_pub [label="public_key()"];
dh_priv -> dh_pub [label="public_key()"];
}
subgraph cluster_group {
label="Per-epoch group state";
style="rounded,dashed";
color="#666666";
roster [label="mesh_roster\n(epoch, member_id, sign_pub, dh_pub, role)", fillcolor="#e8ffe8"];
epoch [label="mesh_epochs\n(epoch_id, secret_envelope, started_event_hash)", fillcolor="#e8ffe8"];
secret [label="symmetric epoch secret\n32 random bytes\nnever stored in clear", fillcolor="#ffd6d6"];
epoch -> secret [label="wrapped per member via ECDH"];
}
audit [label="audit_events\n(prev_event_hash, event_hash)\nappend-only Merkle chain", fillcolor="#ffffff", shape=note];
sign_priv -> audit [label="signs each\nmembership op", style=dashed];
sign_pub -> roster [label="enrolled at"];
dh_pub -> roster [label="enrolled at"];
dh_priv -> secret [label="ECDH-unwraps\nself's envelope entry", style=dashed, color="#0066cc"];
member_id -> roster [label="keys by"];
{rank=same; sign_priv; dh_priv;}
{rank=same; sign_pub; dh_pub;}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

View file

@ -0,0 +1,63 @@
// aborist/mesh — per-member secret envelope.
//
// Every epoch carries one fresh 32-byte symmetric secret. That single
// secret is wrapped N times, once per member, using ECDH between the
// rotator's X25519 priv and each member's X25519 pub. Each member
// unwraps only their own slot. Evicted members get no slot at all.
//
// Render: dot -Tpng docs/diagrams/mesh-secret-envelope.dot -o /tmp/x.png
digraph mesh_secret_envelope {
rankdir=LR;
bgcolor="white";
node [shape=box, style="rounded,filled", fontname="Helvetica"];
edge [fontname="Helvetica", fontsize=10];
secret [label="epoch_N_secret\n32 random bytes", fillcolor="#ffd6d6", shape=cylinder];
subgraph cluster_wrap {
label="rotate_epoch() — done by actor (admin)";
style="rounded,dashed";
color="#666666";
ecdh_a [label="ECDH(rotator_dh_priv,\n alice_dh_pub)\n -> shared_a (HKDF)", fillcolor="#e8f0ff"];
ecdh_b [label="ECDH(rotator_dh_priv,\n bob_dh_pub)\n -> shared_b (HKDF)", fillcolor="#e8f0ff"];
ecdh_c [label="ECDH(rotator_dh_priv,\n carol_dh_pub)\n -> shared_c (HKDF)", fillcolor="#e8f0ff"];
wrap_a [label="ChaCha20-Poly1305\n(shared_a, nonce_a, secret,\n aad=alice.member_id)"];
wrap_b [label="ChaCha20-Poly1305\n(shared_b, nonce_b, secret,\n aad=bob.member_id)"];
wrap_c [label="ChaCha20-Poly1305\n(shared_c, nonce_c, secret,\n aad=carol.member_id)"];
secret -> wrap_a;
secret -> wrap_b;
secret -> wrap_c;
ecdh_a -> wrap_a [label="key"];
ecdh_b -> wrap_b [label="key"];
ecdh_c -> wrap_c [label="key"];
}
envelope [label="mesh_epochs.secret_envelope (JSON)\n{\n alice: {nonce_b64, ct_b64},\n bob: {nonce_b64, ct_b64},\n carol: {nonce_b64, ct_b64}\n}", fillcolor="#fff7d6", shape=note];
wrap_a -> envelope [label="alice slot"];
wrap_b -> envelope [label="bob slot"];
wrap_c -> envelope [label="carol slot"];
subgraph cluster_unwrap {
label="Each peer unwraps only their own slot";
style="rounded,dashed";
color="#666666";
node [fillcolor="#d6ffd6"];
u_alice [label="alice:\nECDH(alice_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.alice"];
u_bob [label="bob:\nECDH(bob_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.bob"];
u_carol [label="carol:\nECDH(carol_dh_priv,\n rotator_dh_pub)\nthen AEAD-decrypt slot.carol"];
}
envelope -> u_alice;
envelope -> u_bob;
envelope -> u_carol;
evicted [label="evicted dave\nno slot in envelope\nValueError on unwrap", fillcolor="#ffe0e0", shape=octagon];
envelope -> evicted [label="dave NOT included", color="#cc0000", style=dashed];
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

226
docs/mesh.md Normal file
View file

@ -0,0 +1,226 @@
# Mesh — multiplayer aborist
Aborist on a single laptop is a content-addressed forest of documents.
Aborist across many laptops is a *gossip-able* forest. Two peers that
ingest the same Wikipedia dump compute bit-identical `document_root`s,
so any peer can verify another peer's claim by re-deriving the hash
locally. Mesh is wire-and-trust scaffolding wrapped around that fact.
This file documents how groups manage data across each other. It pairs
with diagrams in `docs/diagrams/`.
> Status: cryptographic foundation, state machine, and CLI ship today.
> HTTP wire (`mesh sync`, `mesh serve`) is the next milestone. Protocol
> contract below pins what wire will implement.
## Default off
No code path touches a network unless a peer flips `meta.mesh.enabled = 1`.
Lookups, ingests, distillations, and Q&A run identically with or without
mesh. Initialization is two commands and is reversible.
```
aborist mesh init --group myteam # mint Ed25519 + X25519 keys; create epoch 0
aborist mesh enable # flip gating flag on
aborist mesh status # always-safe inspection
aborist mesh disable # flag back off; keys + history stay on disk
```
## Identity stack
![identity stack](diagrams/mesh-identity-stack.png)
Each peer carries:
- one **Ed25519** keypair for signing membership ops and gossip envelopes
- one **X25519** keypair for ECDH key agreement (used to wrap epoch secrets)
- one **8-hex `member_id`** (random by default; pinnable via `--member-id`)
Group state, replicated across every peer that participates:
- `mesh_roster` — per-epoch (member_id, sign_pub, dh_pub, role) tuples
- `mesh_epochs` — per-epoch lifecycle (started_at, audit linkage, secret envelope)
- `audit_events` — append-only Merkle chain of every state-changing op
Public keys are shared. Private keys never leave a peer's SQLite file.
## Epochs — a counter that bumps on every roster change
![epoch lifecycle](diagrams/mesh-epoch-lifecycle.png)
`init_identity()` writes epoch 0 with the founder as sole admin.
Every subsequent `add`, `kick`, or `rotate` writes a new epoch row,
generates a fresh 32-byte symmetric secret, and wraps that secret to
every member of a *new* roster.
Old epochs are not deleted. Their rosters and audit events stay on
disk so that historical signatures remain verifiable by anyone, forever.
Cores never evict; mesh state never evicts either.
## Per-member secret envelope
![secret envelope](diagrams/mesh-secret-envelope.png)
A single epoch carries one symmetric secret. That one secret is wrapped
N times, once per member, by ECDH between a rotator's X25519 priv and
each member's X25519 pub, then sealed with ChaCha20-Poly1305 using
`member_id` as AAD.
Decryption is local: a peer derives the same shared secret by ECDH
between their own X25519 priv and a rotator's X25519 pub, then unwraps
their own slot. An evicted peer simply has no slot, so unwrap raises
`ValueError` — they are opaque to gossip from epoch+1 onward by design.
Code: `aborist/mesh/state.py::_wrap_secret_for_members` and
`unwrap_secret_for_self`.
## What flows between peers
![data flow](diagrams/mesh-data-flow.png)
Aborist is content-addressed, so identifiers are short and bodies are
optional. A typical sync round looks like:
1. **alice** signs and sends `ANNOUNCE_ROOT(document_root, source_uri,
chunking_version, schema_version)` for everything new since last
sync. Same for derivations, providence_cache rows, and falsifications.
2. **bob** verifies alice's signature against her epoch-N `sign_pub`,
then checks his local store. Anything he already has is dedup'd by
`document_root` and dropped on the floor.
3. On miss, bob sends `REQUEST_BODY(root)`. alice replies with
`DELIVER_BODY(bytes, merkle_proof)`. bob verifies the proof against
the announced root before inserting.
4. Audit chains merge by `prev_event_hash`. A gossip insert that does
not extend a chain consistently is rejected — mesh never silently
forks history.
The five gossip message types:
| message | meaning | encrypted? | signed? |
|---|---|---|---|
| `ANNOUNCE_ROOT` | "I have these document_roots" | optional | always |
| `ANNOUNCE_DERIVATION` | "core C derives from surfaces S₁..Sₙ via distiller D" | optional | always |
| `ANNOUNCE_PROVIDENCE` | "I cached an answer at this 8-dim cache_key with this audit_mode" | optional | always |
| `ANNOUNCE_FALSIFICATION` | "this cache_key is wrong (witness signature attached)" | optional | always |
| `REQUEST_BODY` / `DELIVER_BODY` | pull bytes on local miss; recipient verifies Merkle proof before insert | optional | always |
Soft hashes (embeddings, TF-IDF scores) never enter wire. Only hard
SHA-256 commitments cross the network. Per `CLAUDE.md`'s soft/hard hash
split.
## Group operations — operator decision tree
![operator decisions](diagrams/mesh-group-decisions.png)
A group has two roles: `admin` and `member`. Admins may add and kick.
Any current member may rotate the secret on the same roster. Self-kick
is rejected; an admin who wants out runs `mesh disable` instead.
### Add a member (admin only)
```
# bob mints his keys and shares pubs (out-of-band: signal, in person, signed file).
aborist mesh init --group myteam --member-id bob # bob's machine
# alice (admin) enrolls bob at her machine:
aborist mesh add --member-id bob \
--sign-pub <bob_sign_pub_hex> \
--dh-pub <bob_dh_pub_hex>
# epoch bumps. fresh secret wrapped to alice + bob.
```
Bob can decrypt epoch+1 forward. Bob *cannot* reach prior epochs — by
design, joining doesn't grant retroactive access.
### Kick a member (admin only)
```
aborist mesh kick --member-id dave --reason "left team 2026-04-28"
```
Epoch bumps. New secret wrapped to everyone *except* dave. Dave's prior
signatures stay verifiable forever (his roster row at older epochs is
preserved). Any AEAD-protected gossip from epoch+1 onward is opaque to
him.
Cannot leave a roster with no admins. Promote first, then kick.
### Rotate the secret (any member)
```
aborist mesh rotate --reason "scheduled monthly hygiene"
```
Same roster, fresh secret. Use on suspected secret leak when no
identifiable bad actor exists, or as periodic key hygiene.
### When to use which
| situation | command |
|---|---|
| starting a new federation | `mesh init` then `mesh enable` |
| onboarding a teammate | `mesh add` (admin) |
| someone left the team | `mesh kick` (admin) |
| rumor of secret compromise but everyone's keys still trusted | `mesh rotate` |
| stepping away yourself | `mesh disable` (own peer only) |
## Forward-secrecy guarantees in plain language
- **Joining is non-retroactive.** A new member cannot decrypt anything
from before they joined. Wrapping per-epoch makes this automatic.
- **Eviction is forward-only.** A kicked member keeps everything they
already had. They lose access to gossip from the next epoch on.
- **Past signatures remain verifiable forever.** Roster history is
immutable on disk, so anyone can audit who said what at what epoch
even after rotations.
- **Rotation costs O(N).** One ECDH + one AEAD per member per rotate.
Linear in roster size, not document count.
## Trust boundary, threat model
Mesh defends against:
- **Roster impersonation.** Every membership op is Ed25519-signed by an
admin and chained via `audit_events.event_hash = sha256(prev || body)`.
A peer cannot fake a `mesh_epoch_rotate` without the admin's signing key.
- **Gossip tampering.** Every announcement is signed; recipients verify
before insert. A peer that doesn't carry an epoch's secret cannot
produce a valid AEAD payload for that epoch.
- **Eviction bypass.** Evicted members have no slot in the post-rotation
envelope. They cannot derive the new secret from any prior secret.
- **Forking.** Audit chain `prev_event_hash` linkage rejects gossip
inserts that do not extend a chain consistently.
Mesh does not defend against:
- **Out-of-band key distribution.** A new member's pubs must reach the
enrolling admin via a trusted channel (signal, in person, signed file).
Mesh has no built-in introduction protocol.
- **Compromise of an admin's signing key.** With that key an attacker
can forge any membership op. Mitigation: rotate admins regularly,
keep admin count small.
- **Traffic analysis.** Wire layer ships TLS but does not pad or mix.
## Related code
- `aborist/mesh/__init__.py` — module entrypoint, public API
- `aborist/mesh/crypto.py` — Ed25519 + X25519 + ChaCha20-Poly1305 wrappers
- `aborist/mesh/state.py` — DB writes + audit chain hooks; epoch rotation
- `aborist/mesh/members.py` — `add_member`, `kick_member`, `scheduled_rotate`
- `aborist/cli.py` — `_cmd_mesh_*` argparse handlers
- `aborist/store.py` — `mesh_identity`, `mesh_roster`, `mesh_epochs` schema
## Related docs
- `README.md` — top-level overview, ingest flow, single-peer Q&A
- `CLAUDE.md` — schema invariants, conventions, audit chain rules
- `~/git/proxy.unturf.com/pkg/verified/merkle.go` — fox's Go merkle
reference. Aborist's Python port mirrors the conventions.
## Rendering the diagrams
```
make docs # renders docs/diagrams/*.dot to .png via graphviz
```
Or directly: `dot -Tpng docs/diagrams/<file>.dot -o /tmp/out.png`.