bd-graph
A temporal knowledge graph over your beadsissue tracker — built by coding agents, queried over MCP, no LLM API keys required.
Your bd store accumulates decisions, invariants, supersessions, and provenance —but keyword search can't answer shape questions: which rules currently governthis component, and since when? Was that memory superseded? Which lastingdecisions came out of that epic? bd-graph derives a Neo4j graph from your bdcorpus (issues, memories, ADRs) that answers exactly those, and exposes it tocoding agents through a read-only MCP server.
The temporal layer is the point: edges carry valid_from / invalid_at, andSUPERSEDES / INVALIDATES edges record when guidance flipped — so agentsretrieve the current rule instead of a stale one.
The graph is a derived, disposable index. bd stays the source of truth.Rebuild it any time with one command; never write project state to it.
How it works
bd store ──export──▶ corpus/ ──┬─▶ load_p1.py (deterministic: structure + regex)
docs/adr ──copy───▶ └─▶ load_p2.py (semantic: agent-extracted JSON)
│
Neo4j ◀───────────┘
▲
mcp-neo4j-cypher (read-only MCP) ◀── your coding agents
Phase 1 — deterministic (no LLM). Issue dependency trees (CHILD_OF,DEPENDS_ON, DISCOVERED_FROM), ADR→issue tracking links, ADR supersessionstatus lines, and regex cross-mentions between issues/ADRs/memories/PRs. Thisalone is ~90% of the edges.
Phase 2 — semantic (agents, not APIs). Your coding agents (Claude Codesubagents, Codex, whatever you run) extract Component and Rule entities andGOVERNS / ESTABLISHES / SUPERSEDES / INVALIDATES edges with dates, usingthe prompt template in prompts/extraction.md. Because the extractor is youragent harness, this costs subscription tokens — no OPENAI_API_KEY, noembeddings provider, nothing.
Precision pass. A second wave of agents adversarially verifies everyextracted edge against its source (prompts/verification.md); apply_verdicts.pydeletes refuted edges and tags unverifiable ones confidence: "unsupported".In practice verifiers kill 5–15% of edges — mostly fabricated dates andmisattributed components.
Data model
| Node | Key | From |
|---|---|---|
Issue |
id |
every bd issue incl. closed (title, status, dates, close_reason, summary) |
Memory |
key |
bd memories --json |
ADR |
id |
NNNN-slug.md files (title, status, date) |
PR |
number |
regex over all text |
Component, Rule |
name |
Phase-2 extraction |
Meta |
— | freshness stamp: generated_at, corpus counts |
Phase-1 edges: CHILD_OF, DEPENDS_ON, DISCOVERED_FROM, TRACKED_IN,SUPERSEDES (ADR status lines), MENTIONS, REFERENCES_PR.Phase-2 edges: ABOUT, ESTABLISHES, GOVERNS, CONSTRAINS, SUPERSEDES,INVALIDATES, PART_OF — each with fact, valid_from, invalid_at,confidence, source.
Quickstart
Requirements: Python ≥3.11, Docker, a beads store (tested against bd 1.1.0);pipx install mcp-neo4j-cypher for the MCP server (no pipx? python3 -m pip install --user pipx first).
git clone <this repo> && cd bd-graph
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
docker run -d --name bd-graph-neo4j --restart unless-stopped \
-p 127.0.0.1:7474:7474 -p 127.0.0.1:7687:7687 \
-v bd-graph-data:/data -e NEO4J_AUTH=neo4j/<pick-a-password> neo4j:5-community
cp config.example.toml config.toml # edit: id_prefix, password, repo_root, vocab
# repo_root IS REQUIRED for the first run —
# it's what lets regenerate.sh export your corpus
mkdir -p corpus extracted
./regenerate.sh # exports corpus, builds Phase 1
.venv/bin/python test_graph.py # structural + recall assertions
Then wire the MCP server into your agent tool (Claude Code shown):
claude mcp add bd-graph --scope local -- mcp-neo4j-cypher \
--db-url bolt://localhost:7687 --username neo4j --password <password> \
--database neo4j --transport stdio --read-only
For Phase 2, dispatch extraction agents with prompts/extraction.md, verifywith prompts/verification.md, then:
.venv/bin/python apply_verdicts.py && ./regenerate.sh
Try it without a bd store
demo/ contains a synthetic corpus (5 issues, 3 memories, 2 ADRs, a sampleextraction) — setup commands in demo/config.demo.toml. The demo exercisesevery edge type including a supersession chain.
Example queries
// pre-flight: what binds this component, and since when
MATCH (r:Rule)-[g:GOVERNS|CONSTRAINS]->(c:Component {name:'Api'})
OPTIONAL MATCH (src)-[e:ESTABLISHES]->(r)
RETURN r.name, g.fact, e.valid_from, coalesce(src.id, src.key);
// staleness chain: what superseded what, in date order
MATCH (a)-[s:SUPERSEDES|INVALIDATES]->(b)
RETURN coalesce(a.id,a.key,a.name), type(s), s.valid_from,
coalesce(b.id,b.key,b.name) ORDER BY s.valid_from;
// epic rollup: lasting decisions that came out of an epic
MATCH (adr:ADR)-[:TRACKED_IN]->(:Issue)-[:CHILD_OF*1..2]->(:Issue {id:'demo-ep1'})
MATCH (adr)-[:ESTABLISHES]->(r:Rule) RETURN adr.id, r.name;
// issue provenance: where it came from, what it left behind
MATCH (i:Issue {id:'demo-bug7'})
OPTIONAL MATCH (i)-[:DISCOVERED_FROM]->(p)
OPTIONAL MATCH (i)-[:REFERENCES_PR]->(pr)
RETURN p.id, collect(pr.number);
Keeping it honest
Freshness is queryable in-band:
MATCH (m:Meta) RETURN m.generated_at, m.issues. Tell your agents to check it at first use per session../doctor.shnames the failing layer (container / bolt / data /staleness) with the exact fix command; exit 0/2/1 = healthy/stale/broken.--fullchains both test suites.test_graph.pyasserts corpus↔graph parity, dependency-edge parity,no dupes/self-loops, ISO dates, all corpus supersession lines present — plusany project-specific[[probes]]you define in config.test_mcp.pydrives the MCP server over raw stdio JSON-RPC and provesthe write path is closed.Suggested agent-instruction snippet (CLAUDE.md / AGENTS.md):
Use the
bd-graphMCP for shape questions over the tracker (what governs acomponent, supersession chains, epic rollups, provenance). It is a derivedindex — on any mismatch trust bd. CheckMeta.generated_atat first use;on any problem rundoctor.shand say explicitly that you're falling backto the bd CLI. Never fail silently.
Design notes & limitations
- Recall is deliberately partial (~8 edges/doc): the graph is a map, not asubstitute for reading the document it points to.
- ADR parsing assumes the common
# ADR-NNNN: title+- **Status:** / - **Date:** / - **Tracked in:**header block; adjustADR_METAinload_p1.pyforother formats. Duplicate ADR numbers merge into one node — treat that as alint error in your repo. - Extraction quality depends on your
[vocab]component list; keep it shortand canonical. - Inspired by the Graphiti/Neo4j temporal-graph approach(getzep/graphiti); this project tradesGraphiti's embedding-based hybrid search for zero API keys andagent-native extraction. If you have API keys and want semantic search,Graphiti is the heavier-duty option.
Security
Everything is local-only by design: Neo4j binds to 127.0.0.1, the MCP serveris registered --read-only, and config.toml (credentials, project vocab) isgitignored. Your corpus (corpus/, extracted/) contains your project'sinternal knowledge — both are gitignored; never commit or publish them.Note that mcp-neo4j-cypher takes the password as a CLI argument (visible inps on the local machine) — one more reason the password must be alocal-only throwaway, never a reused credential.
License
MIT