Skip to content

Bug: auto-router depth="auto" returns triples-only ranking — the summary merge is a no-op #58

@raphasouthall

Description

@raphasouthall

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:

  1. Run an independent summary search (hybrid_search, dedup by note → per-note summary score) for the same query.
  2. Min-max normalise the triple scores and the summary scores to [0, 1] within each set.
  3. Merge by note_path: score = max(norm_triple, norm_summary) (or a weighted sum; default 0.5/0.5, tunable via config).
  4. Sort the merged list, truncate to top_k.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmcpMCP protocol

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions