Skip to main content
Glama

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.md first 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 k has 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.

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 X then deps(X)" — no closure, no graph-theoretic constraint; no theory; and no per-note closed-pack cost, so {i : h({i}) ≤ B} does not arise

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 f assumes monotone; a conflict penalty is supermodular, so the (1−1/e) guarantee stops applying and the correct citation becomes the non-monotone knapsack bound. Its 14B reversal bounds this project

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 h({i}), no feasibility frontier, and no supermodularity result

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 h({i}) nor the present/correct/given it split; a reason to fetch it before submission, not a claim of difference

vendored facetpack / ACE-RAG (joelvarun/facetpack; nayanananto/Diagnostic-for-Budget-Constrained-Multi-Hop-RAG)

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 (epsilongraph/priorart.py, epsilongraph/aic.py)

no dependency structure. The objective is re-measured monotone and submodular on our corpus exactly as published, but three of f's five terms do not see the query, and neither implementation prices a dependency

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 rank_order wins every column

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 [!summary]/[!important]/[!warning] + every Relations block)

≈ ~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 stats

Then 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 trafilatura

Vault 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 connectivity

Progressive disclosure ladder

Level

Content

Size

L0

root note + layer indexes

~2k tokens

L1

epsilongraph skeleton — every rule and relation, no bodies

~29k tokens

L2

epsilongraph pack <query> — the selected full notes

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" --auto

graph 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.

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.md

Resource 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 ./inbox
  • the 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;

  • fetch prefers trafilatura, import prefers markitdown; when neither is installed the stdlib fallback runs and says which path it took;

  • fetched notes carry acquired: true provenance so the vault's epistemic ledger never mistakes external content for ours.

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 server

Merge 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-check

Two 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  0

held 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:

  1. 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.

  2. 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.

  3. RRF_K = 60 was 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.0000

Holdout 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_order beats every optimizer on hit@pack, rule_coverage and AiC. The cause is structural — three of f's five terms do not see the query. Full table and the fix in THEORY.md §2; run python3 evals/compare_packers.py.

The dependency-closure gap

Where this is going: RESEARCH.md — the plan and the decision points. The executable version is todo.json (15 tasks, each with a falsifiable done_when, all runnable locally: pure Python + qwen3.5:4b via 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 as h(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.tsv

It 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, unclosed

Closure 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

lstpsche/obsidian-mcp

Layer 0, raw vault I/O

built from source (v3.1.0, 29 MB, 30m51s); wired into integrations/claude/mcp.json.snippet

live MCP session: initialize, tools/list → 10 tools under the read profile, search_text "scaffold split" → Concepts/Data Splitting and Leakage, the same note epsilongraph returns

xhluca/bm25s

independent BM25

evals/compare_bm25s.py — the cross-check that found the RRF bug

runs from the clone with numpy alone; 64% top-10 agreement with epsilongraph, 2 queries only epsilongraph finds, 0 neither

nayanananto/Diagnostic-for-Budget-Constrained-Multi-Hop-RAG (ACE-RAG)

the published objective and the task-level metric

epsilongraph/priorart.py reimplements ace_rag/evidence_packer.py's objective; epsilongraph/aic.py ports ace_rag/context_quality.py verbatim

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 tests/test_closure.py

joelvarun/facetpack

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 _objective_value. Noted, not copied

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 tests

126 tests in eleven groups:

  • parity with the pre-existing validate_links.py on 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.py must 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 sends think: false (without it the model puts everything in thinking and every answer scores as wrong), that the answer is read from response, 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's report(), testable without a model. report() reads an acc dict of per-query 0/1 lists, which is exactly what the archived reader-eval-*.json holds, 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 / html2text adapters 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 --source gets its own index under graphs/ 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 test

Available Tools

10 tools
vault_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_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

    A3.5/5.0

    Scored across 10 tools

    Disambiguation4/5

    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.

    Naming Consistency5/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      B
      quality
      B
      maintenance
      Local-first agentic knowledge layer over Obsidian notes, enabling MCP-aware agents to search, retrieve, and compile knowledge with provenance and task contracts.
      37
      16 npm
      1
      MIT
    • A
      license
      Not graded
      quality
      D
      maintenance
      Local-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.
      1
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables 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.
      1
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      Turns 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.
      12
      MIT