Nothing else on the memory roadmap should be built until a change can be shown
to help. Right now we judge by feel, and the benchmark literature is full of
systems that felt better and measured worse. This is the missing gate.
WHAT IT MEASURES, AND WHY IT BOOTS A REAL SOUL
The subject is Will's designed retrieval — spreading activation over the
weighted directed graph, four-factor multiplicative scoring — not a proxy for
it. A Python re-implementation would measure my reading of the design, so the
harness compiles the actual soul.el amalgam from a git ref and asks it over
HTTP on /api/neuron/recall, exactly as the MCP wrapper and the app do.
BUILT ON WHAT WAS ALREADY HERE, NOT AROUND IT
docs/research/graphrag_eval/{collect,score}.py — per-query relevant-id
scoring and fixed-denominator precision@5 (kept verbatim: an empty result
should be punished like a page of junk).
docs/research-archive/p0-prototypes/eval_pinned_40q_20260715.py — the pinned
ground truth + --check winnability gate, so every run judges alike.
scripts/verify-soul-contract.sh — the isolation recipe, including the
non-obvious SOUL_ISE_URL pin without which an "isolated" soul silently
syncs the operator's live brain.
gen-soul-amalgam.sh + .gitea/workflows/ci.yaml — the build recipe and flags.
New here: ids rather than regexes as ground truth, an associative category
derived from real edges, a superseded category scored on ranking, a
machine-checked zero-lexical-overlap guarantee on paraphrases, paired
significance testing, and measurement of the real compiled soul rather than an
offline replica of one leg of it.
THE GOLD SET IS AUDITABLE, NOT VIBES
38 queries over the real 78,768-node corpus, each carrying a `derivation`
string, each re-validated by `build_gold_set.py --check`. exact_rare is mined
(document frequency 1). phrase is mined (verbatim scan; >25 matches rejected as
too diffuse). paraphrase is hand-selected then PROVEN to share zero content
words with its target — a leak fails the build, so the category cannot decay
into lexical matching. associative is derived from real hub edges with
lexically-reachable siblings dropped. nonsense is verified absent. superseded
pairs are kept only when both sides survive as distinct nodes.
HONEST ABOUT NOISE
Minimum detectable swing on 38 queries is 6: if every changed query moves the
same way, p = 2*0.5^n first clears 0.05 at n=6. Run-to-run drift is measured,
not assumed — activation is a stateful read, and it shows: main is fully
deterministic across 3 runs, the candidate drifts by 1 query. compare.py
reports "no measurable difference" for anything inside max(6, drift+1).
FIRST VERDICT — feat/recall-through-activation
hit@5 34.3% -> 22.9%, phrase 85.7% -> 28.6%, latency p50 2.81x. Five discordant
pairs, all five against the candidate, none for it; McNemar exact p = 0.0625,
so by the stated rule this is one query short of significant and is reported as
such rather than as a win for main. The latency regression is deterministic and
not in any noise band.
The benefit the branch was written for is absent: associative recall is 0/6 on
BOTH builds. Probed directly, the traversal returns the lexical seed at rank 8
and none of its 12 hub siblings. Two measured corpus facts explain it — only
4,060 of 78,768 nodes (5.2%) carry any edge, and no node has an embedding, so
the fourth factor of the four-factor product has nothing to compute from. The
mechanism runs; the corpus lacks the structure it needs.
SAFETY
Throwaway port, throwaway HOME, disposable per-run copy of the corpus; live
ports refused by name. Every soul started is killed AND confirmed dead by pid
probe, with the confirmation written into the results file; run_comparison.sh
sweeps for strays and exits non-zero if any survive. Nothing under ~/.neuron,
/Applications/Neuron*, or ~/neuron-dev-stack is read, written, or restarted.
Rung: E2E-VERIFIED — 6 full runs (3 per config) against the real compiled
binaries on the real corpus; numbers above are measured, not projected.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.8 KiB
Retrieval eval harness
Measures Neuron's memory retrieval so a change can be shown to help before it is believed to help. Nothing else on the memory roadmap should ship without a run through this.
tools/retrieval-eval/run_comparison.sh --baseline main --candidate <branch>
That builds a soul from each ref, boots each in isolation on a fixed corpus, runs the gold set three times per ref, and prints a table plus a verdict that refuses to call a difference real if it is inside the noise band.
What was reused
This is not a new idea, it is the missing third of an existing one.
| Prior work | What it gave | What was missing |
|---|---|---|
docs/research/graphrag_eval/ (collect.py, score.py, 2026-06-08) |
The three-retriever comparison that produced the numbers everyone quotes: substring 1.7% P@5, graph 21.7%, BM25 55%. Per-query relevant-id scoring, fixed-denominator precision@5, unique-relevant analysis. | 13 hand-written queries, judged by an LLM after the fact; measured the live soul on the live engram. |
docs/research-archive/p0-prototypes/eval_pinned_40q_20260715.py |
The pinned-query discipline: ground truth committed as regexes so every run judges alike, plus a --check winnability gate. 40 queries in 5 bands including a deliberate paraphrase-hard band. |
Scored offline replicas of substring/BM25 — it never ran the real retrieval path. |
docs/research-archive/p0-prototypes/stage0_eval_20260714.py |
The hit@5 metric and the substring/BM25 reference implementations. |
Same: offline only. |
scripts/verify-soul-contract.sh |
The isolation recipe, verbatim: throwaway port, throwaway HOME, SOUL_ENGRAM_PATH, and the non-obvious SOUL_ISE_URL pin that stops an "isolated" soul silently syncing the operator's live brain. |
It is a contract gate, not a measurement. |
_engine-liveness-91/gen-soul-amalgam.sh + .gitea/workflows/ci.yaml |
The build recipe (elc --target=c with every .elh on the import chain removed) and CI's exact compile flags. |
— |
Reused directly: the isolation recipe, the build recipe, fixed-denominator precision@5, the pinned-ground-truth and winnability ideas. New here: ids rather than regexes as ground truth, an associative category derived from real graph edges, a superseded/contradicted category scored on ranking, a machine-checked zero-lexical-overlap guarantee on paraphrases, paired significance testing, and — the point — measurement against the real compiled soul rather than an offline replica of one leg of it.
Design fit
The thing under measurement is Will's designed retrieval: spreading activation
over the weighted directed graph, four-factor multiplicative scoring (parent
strength x edge weight x target salience x query/target cosine). A Python
re-implementation would measure my reading of the design. So the harness
compiles the actual soul.el amalgam and asks it over HTTP on
/api/neuron/recall, exactly as the MCP wrapper and the app do.
Files
| File | Does |
|---|---|
build_gold_set.py |
Derives and validates the gold set from the corpus. --check re-validates and exits non-zero if a query became unwinnable or a paraphrase leaked a word. |
gold_set.json |
38 queries. Every one carries a derivation string. |
run_eval.py |
Boots one soul in isolation, runs the gold set, writes metrics. Kills and confirms dead its child; records the confirmation in the results file. |
compare.py |
Paired diff of two result files with McNemar's exact test and a stated noise floor. |
build-soul.sh |
Compiles a soul binary from a plain source tree. |
run_comparison.sh |
All of the above, end to end, from two git refs. |
The gold set — 38 queries
Built from the real corpus (snapshot-pre-repair-20260806.json, 78,768 nodes /
14,214 edges) so it reflects one person's accumulating memory, not document QA.
| Category | n | Expected answer derived by |
|---|---|---|
exact_rare |
6 | Mined. Tokens with document frequency 1 across all 78,768 nodes, whose single containing node is a 300–6000 char Memory/Knowledge/Belief. That node is the only possible answer. Re-verified every build. |
phrase |
7 | Mined. Case-insensitive verbatim scan; the matching set is the answer key. Phrases matching >25 nodes are rejected as too diffuse. |
paraphrase |
13 | Hand-selected, machine-checked. Target locked by id; the build then proves that zero content words of the query appear anywhere in the target's label, content, or tags. A leak fails the build — the category cannot quietly decay into lexical matching. |
associative |
6 | Derived from edges. Query built from one value node's distinctive vocabulary; expected answers are its siblings on the Self - Values (grounded) hub. Siblings sharing any query word are dropped, so the only route from query to answer is seed -> hub -> sibling. |
nonsense |
3 | Control. Verified that no token occurs anywhere in the corpus. Correct behaviour is to return nothing. |
superseded |
3 | Derived. Correction/stale pairs located by regex scan, kept only when both sides resolve to different surviving nodes. Scored on ranking: the correction must be returned and rank above the stale node. |
Metrics
hit@5, recall@5, recall@10, precision@5 (fixed denominator 5, so an
empty result is punished like a page of junk), MRR@10, and wall-clock latency
per query (p50/p95/max). Output is a table plus a machine-readable JSON per run
so runs can be diffed.
Honesty about noise
- Minimum detectable swing on this 38-query set: 6 queries. If every query
that changes changes the same way,
p = 2 x 0.5^n, which first drops under 0.05 at n=6. Any net change smaller than that is inside the noise band andcompare.pysays so in those words. - Run-to-run drift is measured, not assumed. Activation is a stateful read
by design (traversal reinforces what it touches), so identical inputs need not
give identical outputs. Observed:
main0 queries of drift across 3 runs (fully deterministic); the activation branch 1 query. - The noise floor used for the verdict is
max(6, observed_drift + 1). - This gold set is underpowered for small effects. A genuine 3-query improvement would not clear the bar. Growing the set is the fix; until then, a small positive delta means "not shown", not "no effect".
First result: main vs feat/recall-through-activation
Corpus and gold set identical, three runs each, fresh corpus copy per run.
| main | recall-through-activation | delta | |
|---|---|---|---|
| hit@5 | 34.3% | 22.9% | -11.4pp |
| recall@5 | 26.9% | 19.1% | -7.9pp |
| recall@10 | 33.3% | 24.3% | -9.1pp |
| precision@5 | 12.0% | 7.4% | -4.6pp |
| MRR@10 | 0.294 | 0.242 | -0.053 |
| latency p50 | 1140 ms | 3209 ms | 2.81x |
| latency p95 | 1584 ms | 4852 ms | 3.06x |
| nonsense clean | 2/3 | 2/3 | — |
| superseded outranks | 1/3 | 0/3 | -1 |
By category (hit@5):
| category | main | activation |
|---|---|---|
| exact_rare | 100% | 100% |
| phrase | 85.7% | 28.6% |
| paraphrase | 0% | 0% |
| associative | 0% | 0% |
| superseded | 0% | 0% |
Verdict: directionally worse, one query short of significant. 5 discordant pairs, all 5 against the candidate, 0 for it. McNemar exact p = 0.0625 — under the stated rule that is inside the noise band, so the harness reports "no measurable difference" on accuracy and the honest summary is "5 for 5 the wrong way, needs a 6th or a larger gold set to call".
Latency is a different story: 2.8x at p50 is deterministic and far outside any noise band. That regression is real.
The result the branch was written for did not appear. Its stated purpose was to
recover sibling nodes one hub-hop away — the associative category — and that
category is 0/6 on both builds. Probing directly: for the query
Marines hernia sepsis medical ward, the activation build returns the lexical
seed node itself at rank 8, and none of its 12 hub siblings anywhere in the top
10. The traversal is running; it is not reaching siblings.
Two corpus facts likely explain it, and both are measurable rather than speculative:
- The graph is nearly edgeless. Only 4,060 of 78,768 nodes (5.2%) carry any edge at all — 14,214 edges total, 0.18 per node. Spreading activation over a graph with no edges is an expensive way to do lexical matching, which is roughly what the numbers show.
- No embeddings. No node in this snapshot has an embedding field, so the fourth factor of the four-factor product — query/target cosine similarity — has nothing to compute from, and the semantic seeding pass is inert.
That is the harness earning its keep on its first job: the change would have felt like progress (it is the designed mechanism, and it does run) and measures as a regression on phrase queries plus a 2.8x latency cost, with its intended benefit unrealised because the corpus lacks the structure it needs.