epsilongraph
Provides tools for working with an Obsidian vault as a typed knowledge graph and epistemic ledger, enabling queries about grounding, evidence, contradictions, blast radius, admitted gaps, and context packing for coding agents.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@epsilongraphif I change the scaffold split note, which notes become wrong?"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
EpsilonGraph
A typed knowledge graph + epistemic ledger over an Obsidian vault, exposed to coding agents (Claude Code and anything else that speaks MCP).
It answers questions that no RAG system answers:
What grounds the rule
scaffold split is the default— and has it been tested, in how many projects?If I change this note, which other notes become wrong (not merely stale)?
What does the vault itself admit it does not know?
Give me the 8k-token subset of this 160k-token vault that this task needs.
[!note] The mathematics is written down Read
THEORY.mdfirst if you want to know why any of this should be believed. It states what each sub-system is mathematically, and then says which claims are proved, which are measured, which are mine, and which are still guesses.The short version: blast radius is a semiring-provenance transitive closure (Green et al., PODS 2007), context packing is submodular maximization under a knapsack constraint (Sviridenko 2004; Amanatidis et al., TCS 1060, 2026), and RRF's
khas a derived bound where the practitioner literature has only folklore. Writing those three down found four real bugs that reading the code did not.Correction, 2026-10-12. A literature check found that the packing formulation is already published — arXiv:2607.00725 (Jul 2026) casts reader-context construction as budgeted monotone submodular maximization, with the task-level eval this repo lacks. That claim is withdrawn. The replacement candidate is a dependency-closure constraint under a budget, which is measurably outside that literature because closure terms are supermodular.
THEORY.md§3 and §5. What replaced it, per prior work, is the related-work table below — one row per work, each naming the quantity it leaves open.
[!note] The product view Evaluating this as a product rather than a research project — install, resource auto-acquisition, multi-vault, distribution, scale — is written up in
PRODUCT.md.
Related work: the scope map
What each prior work models, what it leaves open, and whether this project's
measured results contradict it, confirm it, or are orthogonal (touch a
different quantity). Every cell is drawn from RESEARCH.md §4 and §P8.1 and
THEORY.md §5; no citation or number appears here that is not already there.
prior work | what it models | what it leaves open | this project's result |
Hwang, Retrieval-Governed Context, Zenodo 2026-04-15 | scope gates, priority weights, conflict resolution and mandatory injection over a unified instructions+tools corpus; a governance layer | mandatory injection is "always include this", not "if | orthogonal. Confirms why this corpus makes the question non-trivial (its effect scales with metadata richness) but models no dependency; it supplies the F1 delta for removing a gate, not a price for carrying one |
arXiv:2607.00725, What Survives Into Context (Jul 2026) | reader-context construction as budgeted monotone submodular maximization, with a packer over relevance/coverage/representativeness/diversity and a task-level eval (answer-in-context) | the supermodularity of the closure terms — it has no dependency structure at all, so it never meets the fact that both natural closure terms are supermodular; also reports retrieval and answer quality separately, never answer-quality conditioned on presence | contradicts (one assumption) + orthogonal (the rest). Its |
arXiv:2603.03180, Type-Aware RAG with Dependency Closure (Mar 2026) | typed knowledge graph + minimal dependency closure over the graph, for compilability (missing declarations, type inconsistencies, so a solver can execute the model) | the budget — closure is satisfied fully, then the model is built, with no packing and no token cost | orthogonal. Same phrase, different purpose: its closure is a correctness precondition that is never paid for out of an answer's budget, so it has no |
DCTR, Dual-Constraint Subgraph Optimization for KG-RAG (AAAI 2026) | subgraph optimisation for KG-RAG, with dual constraints on the subgraph | the decomposition, and any closure-cost quantity. Read at title/abstract level only — this row is the weakest here and is marked as such | orthogonal (unverified). On the available text it reports neither |
vendored | cost-aware submodular context packing as a drop-in for top-k; ACE-RAG supplies the published objective and the answer-in-context metric, both ported ( | no dependency structure. The objective is re-measured monotone and submodular on our corpus exactly as published, but three of | confirms (the port) + contradicts (as evidence). The port is faithful (submodularity re-measured, metric matches on 8 cases incl. the adversarial one); but intrinsic objective value is not evidence of retrieval quality — on the task proxy the baseline |
The three quantities this project stands on — h({i}) / the feasibility
frontier, the supermodularity of the closure terms, and the
present/correct/given it decomposition — are each attributed above to
exactly one prior work that lacks them (RESEARCH.md §P8.1).
Related MCP server: Brainstem
Why this is not a RAG project
Measured on the live vault (/home/hugo/Documents/Obsidian, 103 notes):
Measure | Value |
Whole vault | 642,293 chars ≈ ~160k tokens |
Rules + graph skeleton (all | ≈ ~29k tokens = 18% |
Typed relation edges | 491 |
Retrieval collapse is documented at the 10K–100K document range. At 103 notes this is a long-context selection-and-ordering problem, not a recall problem. The gap the vault actually has is epistemic state — grounding, evidence, cross-project status, contradictions, admitted gaps, blast radius — and the vault already models all of it. epsilongraph supplies the missing interface.
See DESIGN.md for the full argument, the boundary conditions that
exclude GraphRAG/RAPTOR/semantic chunking at this size, and the explicit
not-doing list.
Install
No third-party dependencies. Python 3.11+ (stdlib sqlite3 with FTS5).
cd ~/codes/EpsilonGraph
python3 -m venv .venv && source .venv/bin/activate # a venv: Arch's PEP 668 blocks system pip
pip install -e . # gives you the `epsilongraph` console script
# or, without installing anything:
python3 -m epsilongraph statsThen one command fetches and verifies every external resource:
epsilongraph acquire status # what is missing, and the exact fix for each
epsilongraph acquire obsidian-mcp # checksummed prebuilt Layer 0 binary
epsilongraph acquire model # ollama server + qwen3.5:4b (pull)Optional adapters for URL/file acquisition (installed into the venv, never required):
pip install -e '.[acquire]' # trafilatura, markitdown, html2text
# or pin-by-pin:
.venv/bin/epsilongraph acquire repo install trafilaturaVault resolution order: --vault → $EPSILONGRAPH_VAULT → ~/.config/epsilongraph/config.json
→ Obsidian's own registry (obsidian.json, the vault marked "open": true) → cwd.
The index lives at ~/.cache/epsilongraph/index.sqlite — outside the vault, so it is
never committed and never syncs. The Layer 0 binary, the OSS clones, fetched
content, and the acquisition ledger live under ~/.cache/epsilongraph/ too.
Installed state (2026-10-11)
Both MCP servers are registered in ~/.claude.json at user scope and both
report ✓ Connected:
epsilongraph: python3 -m epsilongraph.mcp_server ✓
obsidian-mcp: /home/hugo/codes/EpsilonGraph/vendor/obsidian-mcp/target/release/... ✓The skill is installed at ~/.claude/skills/vault-retrieval/. The config was
backed up to ~/.claude.json.bak-kgraph-<timestamp> before the change, and the
three pre-existing servers (github, filesystem, MiniMax) are untouched.
obsidian-mcp points at the local release build rather than cargo install, so
vendor/ must stay. If you would rather not depend on vendor/, run
cargo install obsidian-mcp (~31 min) and change the command to obsidian-mcp.
LICENSE: MIT — added 2026-10-16 when the repo went public.
Use
epsilongraph index # build; ~0.05s for 103 notes
epsilongraph stats # layer / provenance breakdown
epsilongraph skeleton # the ~29k-token decision layer (L1)
epsilongraph pack "how should I lay out a new training project" --budget 8000
epsilongraph claim "scaffold split" # grounding + evidence + projects + contradictions
epsilongraph impact "Principles/Planning Before Execution" # blast radius
epsilongraph gaps # what the vault admits it does not know
epsilongraph search "docking power"
epsilongraph check # staleness + integrity
epsilongraph acquire status # every external resource, with its fix
epsilongraph graph report # derived-graph connectivityProgressive disclosure ladder
Level | Content | Size |
L0 | root note + layer indexes | ~2k tokens |
L1 |
| ~29k tokens |
L2 |
| budgeted |
L3 | the whole vault | ~160k tokens |
pack runs three passes over the budget: cores first, then key points and gaps,
then graph context. A tight budget drops detail, never candidates — and the
output states what it dropped.
Derived edges (--auto)
Every index now carries a second, derived graph next to the curated one
(graph.py): wikilink / backlink from the author's own links, cotag,
couples (co-citation), and lexical similarity. Two rules keep it honest:
derived edges live in their own table and are never consulted by
impact/load-bearing— a guessed edge must not be able to falsify a note;retrieval uses them only when asked:
search --auto,pack --auto,neighbors --auto, ranked below every lexical result (auto:*is glossed with its kind and weight so a reader can tell inference from curation).
epsilongraph graph build # rebuild the live index + derived graph
epsilongraph graph report # connectivity: components, orphans, kinds
epsilongraph search "packing" --auto # derived context, opt-in
epsilongraph neighbors "Flow Matching" --autograph build --source <any markdown tree> builds a graph over foreign markdown
(an acquired repo, a fetched inbox) in its own index under
~/.cache/epsilongraph/graphs/ — the live vault's index is never touched.
Missing-link suggestions (suggest at most, never auto-write)
graph suggest ranks unlinked note pairs by derived evidence (co-citation
first, lexical last), asks the local reader model for one of the eight typed
buckets, and writes a report — paste-ready lines for the note's
## Relations block. Nothing is ever written into the vault: the typed graph
stays hand-curated, which is the product's core claim about quality.
epsilongraph graph suggest --top 8 --out suggestions.mdResource auto-acquisition
Everything this project does not own — the Layer 0 Rust binary, the reader
model, the adapted OSS projects — is fetched, verified, and recorded by one
command. All of it stdlib-only, checksum-verified, and --dry-run-able.
epsilongraph acquire status # one screen: what is missing, and the fix
epsilongraph acquire obsidian-mcp # checksummed prebuilt binary -> ~/.cache/epsilongraph/bin/
epsilongraph acquire model # ollama + qwen3.5:4b (starts the server, pulls the model)
epsilongraph acquire repo trafilatura # pinned-commit clone -> ~/.cache/epsilongraph/repos/
epsilongraph acquire fetch --url https://example.com/doc --to ./inbox
epsilongraph acquire import --path paper.pdf --to ./inboxthe obsidian-mcp download verifies the release's own sha256 digest before the binary is made executable, and deletes on mismatch;
repos are pinned by immutable commit SHA and hash-locked in
~/.cache/epsilongraph/acquisitions.json— a later fetch that does not match the recorded hash is reported as tampering;fetchpreferstrafilatura,importprefersmarkitdown; when neither is installed the stdlib fallback runs and says which path it took;fetched notes carry
acquired: trueprovenance so the vault's epistemic ledger never mistakes external content forours.
MCP server
Dependency-free newline-delimited JSON-RPC 2.0 over stdio. Nine tools:
vault_pack vault_claim vault_impact vault_gaps vault_search
vault_neighbors vault_get vault_skeleton vault_stats
python3 -m epsilongraph.mcp_server --selftest # drives a real subprocess session
python3 -m epsilongraph.mcp_server # stdio serverMerge integrations/claude/mcp.json.snippet into ~/.claude.json, and append
integrations/claude/CLAUDE.md.snippet to your CLAUDE.md. The skill at
integrations/claude/skills/vault-retrieval/SKILL.md installs to
~/.claude/skills/vault-retrieval/.
Eval
python3 evals/run_eval.py # dev set (30)
python3 evals/run_eval.py --queries evals/queries-holdout.tsv # held-out (30)
python3 evals/tune_rrf.py # choose k, check on holdout
python3 evals/compare_pack.py # 3 packers, paired sign test
python3 evals/compare_packers.py # 8 packers x 2 objectives x AiC proxy
python3 evals/closure_gap.py # the §3 measurement, in one run
python3 evals/synth_corpus.py --out /tmp/epsilongraph-synth # generate an off-vault corpus
PYTHONPATH=vendor/bm25s python3 evals/compare_bm25s.py # second BM25, cross-checkTwo 30-query sets with expected note paths, R@1/3/5 + MRR, a four-config ablation, and a separate coverage metric for graph expansion.
dev (queries.tsv — where a parameter may be chosen):
config R@1 R@3 R@5 MRR misses
lexical 0.70 0.97 0.97 0.821 0
+priors 0.70 0.97 0.97 0.821 0
+graph 0.70 0.97 0.97 0.821 0
full 0.70 0.97 0.97 0.821 0held out (queries-holdout.tsv — where it is checked):
lexical 0.80 0.97 0.97 0.871 0
graph expansion: without graph 0/30 (0%) with graph 24/30 (80%)The eval has falsified three of the design's own claims, each recorded in the code rather than quietly dropped:
Structural priors do not help. They first measured as harmful (MRR 0.817 → 0.767); after the RRF fix they measure as exactly +0.000 on both sets. Off by default.
Graph expansion's MRR delta is exactly 0.000 — because expansion is kept out of the ranked list by construction, so MRR cannot move. The right metric is coverage: 0/30 → 24/30.
RRF_K = 60was wrong. See below.
Packing: rank order vs greedy vs Sviridenko
retrieve.pack used to fill the budget in rank order, optimising nothing in
particular. packopt.py writes the objective down (THEORY.md §2) and scores
all three algorithms on it. Untuned weights, budget 1800 tokens:
split algorithm mean f vs rank_order sign test
DEV rank_order 3.098 -- --
(30 q) greedy 3.576 +15.4% 20W/3L, p=0.0005
sviridenko 3.692 +19.2% 20W/0L, p=0.0000
HOLDOUT rank_order 3.167 -- --
(30 q) greedy 3.766 +18.9% 27W/1L, p=0.0000
sviridenko 3.813 +20.4% 28W/0L, p=0.0000Holdout improves more than dev, and nothing was tuned. rank_order lands
within 4% of best_single (3.167 vs 3.041 on holdout) — the old pack was, in
objective terms, "pick the single best note and stop".
⚠️ The +20.4% above is not evidence of anything. It was measured before any task-level proxy existed. With one (answer-in-context, ported from the paper that owns this formulation), the ordering reverses:
rank_orderbeats every optimizer onhit@pack,rule_coverageand AiC. The cause is structural — three off's five terms do not see the query. Full table and the fix inTHEORY.md§2; runpython3 evals/compare_packers.py.
The dependency-closure gap
Where this is going:
RESEARCH.md— the plan and the decision points. The executable version istodo.json(15 tasks, each with a falsifiabledone_when, all runnable locally: pure Python +qwen3.5:4bvia ollama).The gap below is the project's research problem. Short version: 10 of 103 notes have no dependency-closed pack at all under an 1800-token budget (
h({i})runs min 17 / median 816 / max 2495 tokens), while at 4000 every one of them does — a feasibility threshold, not a binary verdict. Reframed ash(S) = cost_full(S) + cost_skel(closure(S))it becomes a monotone submodular constraint (verified exactly), so the problem is a non-monotone submodular objective under a submodular knapsack — the corner neither half of the published literature covers.
The vault's own loading rule ("rules before procedures … you will follow the steps while violating the intent") is a constraint on what a pack must contain, and no packing objective expresses it. Measured on the 30 holdout queries: 24 packs contain the note that answers the query, and 0 of those 24 carry its dependencies.
The obvious fix is unavailable. Both natural closure terms — edges with both
endpoints inside the pack, and edges leaving it — are supermodular (worst
violation +2.0), so a closure-aware packer is outside the submodular-packing
literature that the 2026 papers, and this repo, live in. Three mechanisms were
built and all three fail: post-hoc repair reaches 100% closure at 0.00
hit@pack, closure-by-construction at 0.03, and a soft term is a no-op
(the budget is already spent). Details in THEORY.md §3; run
python3 evals/closure_gap.py.
Does it hold off-vault? (evals/synth_corpus.py)
Every number above comes from one corpus: the author's own vault, 103 notes, hand-typed relations. That is the project's weakest flank — a result that holds only on a graph you built by hand is a case study, and no second real vault has the typed-edge, two-tier structure the question needs. So the second corpus is generated, with the dependency graph, each note's closure size, and each query's target set by construction rather than observed.
python3 evals/synth_corpus.py --out /tmp/epsilongraph-synth --notes 120 --depth 3
python3 -m epsilongraph --vault /tmp/epsilongraph-synth index
EPSILONGRAPH_VAULT=/tmp/epsilongraph-synth EPSILONGRAPH_INDEX=/tmp/epsilongraph-synth.sqlite \
python3 evals/closure_gap.py --queries /tmp/epsilongraph-synth/_queries.tsvIt reproduces the vault's three structural facts — the gap (28/30 packs contain
the answer, 15/30 carry its dependencies), both closure terms supermodular at
+2.0, f itself exactly submodular — and it turns the vault's observed
slot-competition explanation into a controlled curve. --depth sets closure
size; the closure-requiring packer's hit@pack falls monotonically as it rises
while the unclosed packer stays flat:
closure median 2 5 6 12 <- --depth 2 3 4 6
amort+closed 1.00 0.90 0.93 0.63 hit@pack
amort+rel 1.00 1.00 1.00 0.90 hit@pack, unclosedClosure stays affordable at every depth — 100% of notes are closable at 1800 — and still costs retrieval. That separation, "affordable" against "free", is what the live vault could show but not vary. The generator prints a manifest so the packer's output can be checked against ground truth fixed before it ran.
Scope, stated so the table is not over-read: the synthetic notes carry planted lexical signal and a uniform-depth graph, where the real vault has neither. Only the shape of the tradeoff transfers across corpora, not absolute R@k.
The reader grid was re-run on this corpus at two closure depths (seed 7, 120
notes, --depth 2 the control and --depth 6 the treatment, qwen3.5:4b, n=30;
archived to evals/reader-eval-synth-{d2,d6}-2026-10-12.json). What transferred
and what did not — one row per headline number, with a verdict — is the table in
RESEARCH.md §P6.3. The short version: the depth-vs-hit@pack
curve and the slot-competition mechanism transfer; the levels do not, and the
closed-vs-unclosed present gap is invisible at depth 2 because a median closure
of 2 leaves nothing to carry.
What the second BM25 found
evals/compare_bm25s.py ranks the same 103 notes with xhluca/bm25s — an
independent BM25 on numpy sparse matrices, different tokenizer, different
scoring constants, no field weights. Two independent implementations over one
corpus is the only way to tell "the ranking is fine" from "my ranking agrees
with itself".
It agreed on 64% of top-10 sets and disagreed usefully: it exposed a query where the correct note is ranked #1 by the title retriever and #18 after fusion.
The cause was arithmetic, not taste. Score is 1/(k + rank). With k=60 and a
candidate depth of 50, every contribution lands in [1/110, 1/61] — under 2×
spread. So appearing in a second list (2×) beat being ranked first (1.8×).
Ubiquity outranked strength. k=60 is tuned for corpora of thousands of
documents; at 103 notes it is a flattening filter.
k=1 was chosen on dev and confirmed on holdout, where MRR is monotonic in k:
k | dev MRR | holdout MRR |
1 | 0.821 | 0.871 |
8 | 0.826 | 0.861 |
60 | 0.817 | 0.830 |
The split earned its keep: k=8 scored best on dev and worse on holdout — it
was fitted to the 30 dev queries and would have shipped as an improvement.
evals/tune_rrf.py flags that case as FITTED automatically.
With n=30, one query is 0.033 MRR. Differences below ~0.03 are a single query and are not findings.
Vendored
Four upstream projects, cloned to vendor/ and adapted. None is imported by
epsilongraph/ — the package stays dependency-free; they are measurement and
infrastructure, not runtime dependencies.
Project | Role | Adaptation | Verified |
Layer 0, raw vault I/O | built from source (v3.1.0, 29 MB, 30m51s); wired into | live MCP session: | |
independent BM25 |
| runs from the clone with numpy alone; 64% top-10 agreement with epsilongraph, 2 queries only epsilongraph finds, 0 neither | |
| the published objective and the task-level metric |
| objective re-measured monotone and submodular on our corpus (their published claim), so the port is faithful; the metric matches the original on 8 cases including the adversarial bag-of-words one — pinned by |
cost-aware submodular packing, a second independent implementation | read, not imported; its single-best guard was compared against ACE-RAG's | its guard compares a score against a sum of scores — two different quantities — where ACE-RAG's correctly calls its own |
The last two are the 2026-10-12 prior-art adaptation. Their ace_rag/ cannot run
here (it needs Qwen2.5 3B/7B/14B from HuggingFace, which this machine cannot
reach), so the two pieces that are reader-independent were extracted instead:
the objective and the metric. vendor/ is gitignored except ace-rag/ace_rag/,
which tests/test_closure.py imports for the parity check.
One vault-pollution note. obsidian-mcp keeps per-vault state at
{vault}/.obsidian-mcp. Setting OBSIDIAN_MCP_DATA moves the heavy cache (BM25
index, embeddings) to ~/.cache/obsidian-mcp/vaults/{slug}/, but the server
still creates a small vault-local .obsidian-mcp/ignore by design. It contains
no markdown. epsilongraph/parse.py skips the directory anyway — that is insurance
against the coupling, not a fix for a bug that happened.
Not vendored: AnswerDotAI/rerankers was the right second choice on paper —
cross-encoder reranking is the one technique in the "non-negotiable tier" this
project has not implemented, its core is dependency-free, and its FlashRank
backend is ONNX rather than torch. It was rejected because its models download
from HuggingFace, which this machine cannot reach (curl: (35) Recv failure: Connection reset by peer; PyPI is fine). A reranker that cannot load a model is
not a reranker. The design's no-dense conclusion is unchanged either way: at 103
notes with R@5 0.97 there is no measured headroom to win back.
Tests
python3 -m unittest discover -s tests126 tests in eleven groups:
parity with the pre-existing
validate_links.pyon the live vault — note set, link count, broken-link set, and per-note relation buckets must match exactly. Two parsers for one corpus is a drift generator; the agreement is tested, not asserted.ranking — pins the three properties that measurement found broken: that rank outweighs list membership, that a near-exact title match reaches the top 5, and that the held-out set is still held out.
closure — three kinds, kept separate on purpose. Parity:
epsilongraph/aic.pymust agree with the vendored original, and the ported ACE objective must be monotone and submodular on our corpus as published. Properties: both natural closure terms must stay supermodular — if one is ever found submodular,THEORY.md§3 collapses into §2 and the test says so. Invariants: the mechanisms return closed, within-budget sets, and the production packer still does not.docs consistency — the numbers the documents agree to repeat are recomputed from the repo and compared: the corpus size, the dependency-edge count, the test count, and the file layout. Five numbers were corrected in code and left stale in prose on 2026-10-12, each found by grepping by hand. This turns that into a failure. It deliberately allows a correction note to name the value it corrects — forbidding that would forbid the retraction itself, which is what the first version of the check did.
reader harness — the four claims of
evals/reader.py, testable without a model: that it is stdlib-only (the AST is parsed and the import set checked against a whitelist), that the request sendsthink: false(without it the model puts everything inthinkingand every answer scores as wrong), that the answer is read fromresponse, and that the cache key covers the whole request rather than the prompt alone.synthetic corpus — the artefact assertions of
evals/synth_corpus.py, the off-vault validation. Every other test in this repo assumes the live vault; this one asserts that a generated corpus actually has typed dependency edges, a non-degenerate closure distribution, a depth knob that moves it, and query targets that exist in it. It exists because the generator failed in exactly that way twice during construction — a bullet-prefixed Relations header and a bucket name that was a sentence — and both times it produced a corpus that indexed cleanly, looked plausible, and had zero typed edges. A closure measurement against such a corpus reports that closure costs nothing, and is believed.reader report — the confidence intervals and paired tests of
evals/reader_eval.py'sreport(), testable without a model.report()reads anaccdict of per-query 0/1 lists, which is exactly what the archivedreader-eval-*.jsonholds, so the offline path (--from-json) is exercised both against a hand-built dict and against the real archive. The assertions require the Wilson[k/n]count and the paired[n=...]marker to be present, because a p-value without its denominator is the failure this project keeps recording — a test that merely checks "a p appears" would pass on the report that drops the n.unit — gloss retention, gap-bullet splitting, the table-escaped-pipe contract.
acquire — platform→asset mapping for the Layer 0 download, checksum verification (mismatch ⇒ deleted, never kept), tarball extraction and ledgering, the stdlib HTML fallback, the one-screen status report, and the zero-runtime-dependency invariant (the optional
trafilatura/markitdown/html2textadapters may only ever be imported inside a function).graph — the derived-edge computation (exact edge sets, thresholds, co-citation, orphan agreement) on a fixture vault; that curated pairs are never offered as missing-link suggestions; that a foreign
--sourcegets its own index undergraphs/and never resolves the live vault's.
Layout
epsilongraph/
parse.py # frontmatter, callouts, glossed relations, gap bullets
config.py # vault resolution (5 steps), index location
index.py # SQLite + FTS5 build; provenance classification; staleness
retrieve.py # 4-retriever RRF, budgeted packing, skeleton
ledger.py # claim / impact / gaps — the epistemics
severity.py # blast radius as a semiring closure over a severity lattice
packopt.py # f as a set function; greedy / Sviridenko; closure machinery
priorart.py # the published objective (arXiv:2607.00725), ported
aic.py # answer-in-context, ported verbatim — the task-level proxy
cli.py # subcommands
acquire.py # resource auto-acquisition: Layer 0 binary, ollama model, OSS clones, URL/file fetch
graph.py # automatic KG construction: derived edges over any markdown tree, suggestions
mcp_server.py # stdio JSON-RPC, 9 tools, --selftest
evals/
queries.tsv # 30 dev queries
queries-holdout.tsv # 30 held-out queries (do not edit)
run_eval.py # R@k / MRR / ablation / graph coverage
tune_rrf.py # choose k on dev, check on holdout, flag FITTED rows
compare_pack.py # 3 packers, paired sign test -- where THEORY 2's p-values come from
compare_packers.py # 8 packers x 2 objectives x the AiC proxy (means, no test)
stats.py # the shared statistics: sign test, Wilson + bootstrap CIs, permutation
closure_gap.py # the dependency-closure measurement (§3)
closure_sweep.py # the depth x budget frontier (P1b)
synth_corpus.py # a second, generated corpus with a known dependency graph
config_manifest.py # one JSON: vault/index paths, budgets, SEED, reader model, deps
reader.py # the local reader: stdlib only, self-starting, cached
reader_eval.py # the task-level eval and its grader (P2)
compare_bm25s.py # second independent BM25 over the same corpus
reader-eval-*.json # the recorded runs, so a number in the docs is traceable
tests/ # parity + ranking + closure + reader + report + docs + unit + synthetic
integrations/ # Claude Code mcp.json / CLAUDE.md / skill
vendor/ # upstream clones — only ace-rag/ace_rag/ is committed
Makefile # make repro (offline evals -> results/), make repro-reader, make testAvailable Tools
10 toolsvault_claimA
The epistemic state of one rule or note: its grounding, whether it has been tested and in how many distinct projects, recorded disagreements, blast radius if changed, and what the note admits it does not know. Use before relying on a rule, and before changing one.
Inputs: note (required).
vault_gapsA
What the vault itself admits it does not know, gathered from the notes' own [!todo] and [!question] callouts. Read this BEFORE proposing a new direction: the corpus may already record the gap, or may already have closed it.
Inputs: limit, query.
vault_getB
Full markdown of one note. Use after pack/search to drill in.
Inputs: note (required).
vault_impactA
The blast radius of changing a note. By default this is the TRANSITIVE closure, not depth-1: depth-1 understates the falsified set by a median of 3.9x and up to 9.5x on the notes that matter. semantics='falsified' (default) answers 'what is now FALSE?' and follows only 'Governed by', because that is the one bucket asserting content conformance. semantics='needs_reread' also follows 'Read first', which asserts a reading order rather than conformance, and answers 'what must I re-examine?' -- a much larger set. Run before editing any framework note.
Inputs: note (required), deep, semantics.
vault_load_bearingB
Every note ranked by how much of the vault a change to it would falsify. On this vault the answer is bimodal: a handful of notes falsify 30-50% of the corpus and the large majority falsify nothing. Use this to decide where to be careful.
Inputs: limit, semantics.
vault_neighborsB
Typed neighbours of a note. The eight buckets are not equivalent: Governed by and Read first are constraints, Source and Evidence are grounding, Applied in is where it is executed, See also is the weakest.
Inputs: note (required), buckets.
vault_packA
Budgeted, provenance-annotated context pack for a task or question. Returns ranked notes with WHY each matched, whether it is grounded (and by what), what constrains it, and what it applies to. Use this first for any question about how to do something in this vault. Cheaper and more trustworthy than reading whole notes.
Inputs: query (required), k, full_body, budget_tokens, +1 more.
vault_searchC
Ranked notes for a query, with match explanation and provenance.
Inputs: query (required), k, layer, status, +1 more.
vault_skeletonA
Every rule and every typed relation in the vault, with no bodies (~29k tokens here, 18% of the corpus). Small enough to hold for a whole session, which is usually better than repeated searching.
Inputs: no named parameters.
vault_statsB
Index size, layer and provenance distribution, and freshness.
Inputs: no named parameters.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
TDQS
Scored across 10 tools
Most tools target clearly distinct operations (search, get, pack, neighbors, skeleton, stats, gaps). However, vault_load_bearing vs vault_impact both compute falsification/blast radius (global ranking vs per-note transitive closure), and vault_claim also surfaces blast radius, so an agent could conflate these three. The rich descriptions mostly resolve the overlap, but the boundaries are not as crisp as they could be.
Every tool uses the same vault_ prefix followed by a single descriptive noun (search, get, pack, neighbors, skeleton, stats, gaps, claim, impact, load_bearing). The convention is uniform snake_case throughout with no mixing of styles or verb forms.
Ten tools is well-scoped for a knowledge-vault query and analysis server, with each tool earning its place across retrieval (search, get, pack), structure (neighbors, skeleton), and epistemic analysis (claim, impact, load_bearing, gaps, stats). Nothing feels redundant or padding.
The read/analysis surface is comprehensive: retrieval, structure, provenance, epistemic state, and impact are all covered, plus a session-level skeleton and stats. The one notable gap is that descriptions repeatedly reference editing and changing notes, yet no mutation tool (edit/update) exists in the set, leaving a workflow dead-end if agents are meant to act on the vault.
Maintenance
Related MCP Connectors
Token-efficient MCP memory for Markdown vaults. Tiered search, GraphRAG, AI memories.
Notes and memory assistant: one Markdown vault that Claude, ChatGPT and any MCP client can share.
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
Agent-native MCP server over the public saagarpatel.dev corpus. Read-only, stateless.
Related MCP Servers
- AlicenseBqualityBmaintenanceLocal-first agentic knowledge layer over Obsidian notes, enabling MCP-aware agents to search, retrieve, and compile knowledge with provenance and task contracts.3716 npm1MIT
- AlicenseNot gradedqualityDmaintenanceLocal-first knowledge backend for AI agents that connects MCP hosts to an Obsidian-compatible vault with indexed retrieval, token-budgeted memory recall, and secure ingestion.1MIT
- AlicenseNot gradedqualityBmaintenanceEnables LLM clients to read, search, and write to a local Obsidian vault over MCP, with full-text search, vault analytics, health scoring, and policy-enforced file operations.1MIT
- AlicenseAqualityBmaintenanceTurns a plain-Markdown Obsidian vault into a second brain that any MCP-speaking coding agent can search, read, extend, and maintain, using swappable SQLite, Qdrant, or markdown memory backends with automatic re-indexing and wiki-link graph navigation. Exposes read, write, and index tools plus an operator CLI for health, linting, and graph export.12MIT