Commit graph

2 commits

Author SHA1 Message Date
Aaron Bockelie
8e09a3871f fix(retrieval): stop the score labels lying, and name both blind spots
Three defects that each cause an agent to trust a signal further than it holds.
All were surfaced by agents working the same retrieval task and reported
independently, twice each.

Score labels asserted a judgement the number cannot support
interpretScore mapped TF-IDF onto fixed bands — "Good" at >=1.0, "Low" at >0 —
but TF-IDF has no absolute scale; it means different things per query and per
corpus. On a vault where most notes mention the query term the scores compress:
measured, the single best-answering note scored 1.42 ("Good") while five
load-bearing notes scored 0.36-0.45 and were all labelled "Low". An agent
pruning at "Low" would have dropped the entire answer except the anchor. It said
so itself: "the graph was trustworthy; the ranking was not."

The number was never the problem — the adjective was. Scores now render bare,
against this run's top hit ("0.36, 25% of top hit"), which makes the compression
visible instead of hiding it behind a word. Results also carry an explicit
caution not to prune on score.

The graph's blind spot was undocumented, and it is the mirror of search's
The tool descriptions warned that search misses notes written in different
vocabulary. They did not warn that traversal only reaches what someone actually
linked. Both failure modes are real and they are opposite: in the test corpus a
note documenting a fifth consumer of a dependency hub was causally coupled to it
and NOT linked from it, so no traversal from the anchor could reach it — the
shell/ripgrep arm found it by content scan, the graph arm missed it entirely.
Meanwhile that same shell arm silently under-retrieved by 8 files because the
corpus spells the subject two ways, and "a partial match set is indistinguishable
from a complete one".

So they are complements, not substitutes: scan broadly to catch the unlinked,
follow links to catch the differently-worded, trust neither alone. The graph
description now says this, and points at backlinks as the way to find what
depends on a note — which its own text does not know.

"Summary view" read as a truncation warning
Every response closed with "_Summary view. For all metadata fields, use raw:
true._". On tools whose job is faithful retrieval, that reads as a warning that
the content was abridged, and callers spent real attention deciding whether the
note body was a lossy rendering of the argument. Only metadata is abbreviated;
genuine truncation already announces itself (a Pagination section, an explicit
"N more" line). The footer now names what it actually elides.

Also: search-traverse is now described with its cost, not just its brevity — it
returns snippets per node and prunes on scoreThreshold, so it finds WHICH notes
matter but is not the whole argument. Two agents declined to use it because the
description sold the saving without the trade, and chose more calls over less
certainty.

427 tests passing.
2026-07-13 18:45:26 -05:00
Aaron Bockelie
fba27c77e1 fix(retrieval): make paths usable and steer scan-then-follow
Measured against two vaults with a paired A/B harness (plugin tools vs. a shell
with ripgrep, same question, isolated agents). On a graph-shaped question over a
densely linked vault — the plugin's home field — it LOST: 90.3k tokens vs 70.1k,
20 calls vs 16, 3 dead ends vs 2. Not because the graph failed. Because the
plugin's own output made the graph unusable, and the agent spent the difference
working around it.

Paths were not usable as identifiers
vault.search elided long paths to `first/.../last`, and vault.list printed only
the basename. Both destroy the one field an agent must have verbatim: the value
it feeds to the very next call. An agent that joined the listed directory to the
name it was given got a path that does not exist and a hard "File not found".

This is the mechanism behind the repeated-search behaviour we keep seeing.
Scan-then-follow requires handing a search hit to graph.neighbors; if the hit's
path is elided, that handoff is impossible, so the only move left that still
works is another search. The tool was training the behaviour. Paths are now
emitted verbatim everywhere, and formatPath — a helper whose sole purpose was to
damage them — is deleted.

vault.fragments ignored its own `path` parameter
`path` was read only as a fallback *query* string, never as a scope, so asking
for fragments from one note returned passages from several, top-ranked hit
first. An agent reasoning "these are the key passages in the note I named" would
attribute content to the wrong file. Now scoped, over-fetching candidates first
so scoping does not silently return nothing when other notes outrank the target.

graph.statistics threw on the vault-wide call
No sourcePath is returned for the vault-wide shape; the formatter read it
unconditionally, threw, and fell back to raw JSON behind a "Formatter error"
banner. Now formatted — and it reports link density, because that is the number
that tells the caller whether following links will beat searching.

Steering, since the affordance was invisible
Both experiment arms independently concluded the graph was the strongest signal
available and both nearly missed it: "graph.neighbors was the MVP and I nearly
didn't reach for it"; and from the ripgrep arm, "I had a graph and was forced to
walk it as a pile of bytes." Search results now name graph.neighbors with the
top hit's path filled in, and the vault/graph tool descriptions teach the
pattern: a couple of broad scans, then follow links — rather than many narrow
searches that re-run the same ranking.

vault.read printed the frontmatter twice
A truncated "## Frontmatter" summary above a body that already opens with the
raw block. Suppressed when the body carries it; retained for fragment reads,
which do not.

Fragment strategy `semantic` renamed to `structure`
It cuts on headings and paragraphs. It has never been embedding or vector
search, but "semantic" in a retrieval slot reads as exactly that to any model
trained on the last decade of the literature — so callers selected it expecting
matching this index does not do. `semantic` still works as a deprecated alias.
All four strategies are now described; previously the enum shipped with no gloss
at all and the agent chose blind.

Tests: 415 passing. New suites cover path fidelity (including the fabricated
path an agent would construct), fragment scoping, the vault-wide stats shape,
and the frontmatter duplication.
2026-07-13 18:29:29 -05:00