Summary
tiered_search(..., depth="auto") advertises a triples+summaries merge but never performs one. When triple coverage clears a confidence gate, it attaches summary text to the notes already ranked by triple score and returns. It never runs an independent summary or chunk search, never normalises scores, and never re-ranks. The returned note ordering is byte-identical to depth="triples".
auto is the default depth for the vault_search MCP tool and neurostack tiered, so every default query with decent triple coverage silently gets triples-only ranking — not the blended ranking the depth name implies.
Location
src/neurostack/search.py, tiered_search() auto branch (currently lines ~1434–1467).
Current behaviour
# Auto mode: start cheap, escalate if needed
triples = search_triples(query, top_k=top_k * 3, ...)
result["triples"] = [...]
triple_notes = {t.note_path for t in triples}
triple_confidence = max((t.score for t in triples), default=0.0)
if len(triple_notes) >= 2 and triple_confidence > 0.4:
# "Add summaries for the top-scoring note paths"
top_notes = list(dict.fromkeys(t.note_path for t in triples))[:top_k]
for np_ in top_notes:
summary_row = conn.execute(
"SELECT s.summary_text, n.title FROM summaries s "
"JOIN notes n ON n.path = s.note_path WHERE s.note_path = ?",
(np_,),
).fetchone()
if summary_row:
result["summaries"].append({...})
result["depth_used"] = "auto:triples+summaries"
return result
The gate is len(triple_notes) >= 2 and triple_confidence > 0.4, where triple_confidence = max(triple scores) and a triple score is 0.3*fts + 0.7*cosine (roughly [0, 1]). Once any query clears 0.4, the summaries branch runs a per-note SELECT summary_text keyed on note paths already ordered by triple rank. There is no rescoring. result["summaries"] comes back in triple order, and the function returns before the chunk-search fallback.
Why #41 did not cover this
#41 added the link-section penalty inside hybrid_search (the depth="full" path). The auto branch returns before it ever reaches hybrid_search whenever the gate fires, so the #41 fix never applies to default-depth queries.
Impact
- Default-depth retrieval degrades to triples-only ranking whenever triples are present.
- Notes with zero triples (an extraction gap) are invisible to the auto path even when their summary or chunks are the best match for the query.
depth_used: "auto:triples+summaries" is misleading: no summary ranking happened.
Repro
On a populated index:
neurostack tiered "QUERY" --depth auto > auto.json
neurostack tiered "QUERY" --depth triples > triples.json
# top-k note ordering is identical
neurostack tiered "QUERY" --depth summaries # different — often better — ordering
Observed historically on a real query ("NBSE disaster recovery"): auto and triples returned an identical top-5 that surfaced none of the actual DR notes, while summaries ranked the correct note first.
Proposed fix
When the gate fires, perform a real merge instead of an attach:
- Run an independent summary search (
hybrid_search, dedup by note → per-note summary score) for the same query.
- Min-max normalise the triple scores and the summary scores to [0, 1] within each set.
- Merge by
note_path: score = max(norm_triple, norm_summary) (or a weighted sum; default 0.5/0.5, tunable via config).
- Sort the merged list, truncate to
top_k.
- Keep the current dict-of-lists response shape so callers don't break;
triples and summaries lists reflect the unified order. Optionally add a merged_ranking list.
Validation
- Unit test: auto ordering differs from triples-only when a note with a high summary score and low triple score exists.
- Unit test: a zero-triple note with a strong summary match can surface in auto.
- Regression: the gate-not-fired path still falls back to full chunk search.
Summary
tiered_search(..., depth="auto")advertises atriples+summariesmerge but never performs one. When triple coverage clears a confidence gate, it attaches summary text to the notes already ranked by triple score and returns. It never runs an independent summary or chunk search, never normalises scores, and never re-ranks. The returned note ordering is byte-identical todepth="triples".autois the default depth for thevault_searchMCP tool andneurostack tiered, so every default query with decent triple coverage silently gets triples-only ranking — not the blended ranking the depth name implies.Location
src/neurostack/search.py,tiered_search()auto branch (currently lines ~1434–1467).Current behaviour
The gate is
len(triple_notes) >= 2 and triple_confidence > 0.4, wheretriple_confidence = max(triple scores)and a triple score is0.3*fts + 0.7*cosine(roughly [0, 1]). Once any query clears 0.4, the summaries branch runs a per-noteSELECT summary_textkeyed on note paths already ordered by triple rank. There is no rescoring.result["summaries"]comes back in triple order, and the function returns before the chunk-search fallback.Why #41 did not cover this
#41 added the link-section penalty inside
hybrid_search(thedepth="full"path). The auto branch returns before it ever reacheshybrid_searchwhenever the gate fires, so the #41 fix never applies to default-depth queries.Impact
depth_used: "auto:triples+summaries"is misleading: no summary ranking happened.Repro
On a populated index:
Observed historically on a real query (
"NBSE disaster recovery"):autoandtriplesreturned an identical top-5 that surfaced none of the actual DR notes, whilesummariesranked the correct note first.Proposed fix
When the gate fires, perform a real merge instead of an attach:
hybrid_search, dedup by note → per-note summary score) for the same query.note_path:score = max(norm_triple, norm_summary)(or a weighted sum; default 0.5/0.5, tunable via config).top_k.triplesandsummarieslists reflect the unified order. Optionally add amerged_rankinglist.Validation