All verification complete. Final report: **Phase 119 final verification pass — all criteria verified, one stale pin fixed.** - Verified implementation of all 6 tasks: D1 component name-hit rule (`name_hit` flag, titles never matched, retired length tie-break), D2 `BOR_NAME_HIT_BONUS` (0.005 default, 0 = byte-identical kill switch, negative fails startup, selection-layer only, `eval_retrieval` `suggested:` line), D3 suggested-folder lines (after `SUGGEST_INTRO`, before first block), D4 cite-discipline `SUGGEST_INTRO` sentence (PERSONA/LOW/`TOOLS_SECTION` byte-pins intact), D5 `done.sources` = read docs only (frontend no-op on empty confirmed), D6 mock `repeat your folder map` echo + new suite + telemetry. - Battery (replica restored per skill, fingerprint docs=1000/chunks=8866 verified, `eval_retrieval --from-file tests/fixtures/retrieval_battery.txt` re-run): **GATE PASS** — gitea README #4 in suggested top-5, forgejo 5/5 (README #1), gateway README in top-5 (#4), qwen3.8-27b quadlets top-5, Mongolia HIGH/fts=5 unchanged. - New E2E in isolation: `4 passed` ×2 (deterministic). All 27 modified E2E suites in isolation: 26 green; **1 stale pin fixed** — `test_source_chip_quality.py` durable-record order pin pre-dated the D1 re-rank (`aliases` stem sub-component name-hits `ssh_aliases.txt`, deterministically lifting `backups.md` over `kubernetes.md`; probe-verified 0.016277 vs 0.016036, 4/4 stable) — re-pinned with the phase-119 rationale; suite green ×2. - Gates: `uv run pytest --cov=app --cov-report=term-missing` → **2547 passed, app coverage 99%** (>90%); `uv run ruff check .` → All checks passed; `uv run pyright` → 0 errors. - Completion criteria: 1 ✅ (battery, recorded), 2 ✅ (folder lines; block/LOW byte-identical pins green), 3 ✅ (read-only chips, zero-read chips nothing, related row + durable record untouched — unit+E2E agree), 4 ✅ (all green), 5 → commit/phase-move left to the harness per pass rules (nothing committed). - Deviations: battery output + real-model telemetry recorded in `.agents/reports/119_name_signal_read_chips/task06_battery_and_e2e.md` and `TOOL_CALLING_TESTING.md` §11 (task files in `complete/` are immutable to this pass); gateway canonical doc at #4 vs overview's #3 was already documented at task 06 (containment gate met). - Next pending phase: **none** — `todo/` holds only phase 119.
1349 lines
56 KiB
Python
1349 lines
56 KiB
Python
"""Unit: retriever — ordering and dedup, no context cap (A7 revised) (fake rows).
|
||
|
||
The SQL side of :func:`app.rag.retriever.retrieve` is exercised by the
|
||
chat integration tests against real Postgres; the pure mapping logic in
|
||
:func:`select_documents` is tested here with in-memory rows.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import uuid
|
||
from types import SimpleNamespace
|
||
|
||
import pytest
|
||
|
||
from app.config import Settings
|
||
from app.models import Document
|
||
from app.rag import retriever
|
||
from app.rag.retriever import (
|
||
TRUNCATION_MARKER,
|
||
RetrievedChunk,
|
||
select_documents,
|
||
select_documents_tiered,
|
||
select_related,
|
||
select_suggested,
|
||
)
|
||
|
||
|
||
def _doc(path: str, content: str, source: str = "Homelab", title: str | None = None) -> Document:
|
||
return Document(
|
||
id=uuid.uuid4(),
|
||
source=source,
|
||
path=path,
|
||
full_path=f"/tmp/{path}",
|
||
title=title or path,
|
||
content=content,
|
||
content_hash="0" * 64,
|
||
)
|
||
|
||
|
||
def _chunk(doc: Document, score: float, position: int = 0) -> RetrievedChunk:
|
||
return RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=position,
|
||
content=doc.content[:40],
|
||
score=score,
|
||
document=doc,
|
||
)
|
||
|
||
|
||
def test_ranks_by_best_chunk_score_not_first_hit() -> None:
|
||
"""A doc whose *later* chunk scores highest must still rank first."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
chunks = [
|
||
_chunk(a, 0.4, position=0), # a's weak chunk comes first
|
||
_chunk(b, 0.8),
|
||
_chunk(a, 0.9, position=2), # a's best chunk comes last
|
||
_chunk(c, 0.5),
|
||
]
|
||
docs = select_documents(chunks, n=3)
|
||
assert [d.path for d in docs] == ["a.md", "b.md", "c.md"]
|
||
|
||
|
||
def test_dedups_to_one_document_per_hit_set() -> None:
|
||
a = _doc("a.md", "A" * 50)
|
||
chunks = [_chunk(a, 0.2), _chunk(a, 0.7), _chunk(a, 0.5)]
|
||
docs = select_documents(chunks, n=2)
|
||
assert len(docs) == 1
|
||
assert docs[0] is a
|
||
|
||
|
||
def test_caps_at_n_documents() -> None:
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(4)]
|
||
chunks = [_chunk(d, 0.5 - 0.1 * i) for i, d in enumerate(docs_in)]
|
||
out = select_documents(chunks, n=2)
|
||
assert [d.path for d in out] == ["d0.md", "d1.md"]
|
||
|
||
|
||
def test_content_never_truncated_even_past_old_budget() -> None:
|
||
"""Whole documents, never truncated (A7 revised, owner permission 2026-08-24).
|
||
|
||
Two documents of 20 000 + 15 000 chars — 35 000 combined, well past
|
||
the old 24 000 context budget — come back with content
|
||
**byte-identical** to the originals, and the truncation marker is
|
||
absent from both.
|
||
"""
|
||
big = _doc("big.md", "B" * 20_000)
|
||
small = _doc("small.md", "S" * 15_000)
|
||
chunks = [_chunk(big, 0.9), _chunk(small, 0.6)]
|
||
out = select_documents(chunks, n=2)
|
||
assert [d.path for d in out] == ["big.md", "small.md"]
|
||
assert out[0].content == "B" * 20_000
|
||
assert out[1].content == "S" * 15_000
|
||
assert TRUNCATION_MARKER not in out[0].content
|
||
assert TRUNCATION_MARKER not in out[1].content
|
||
|
||
|
||
def test_empty_hits_yield_no_documents() -> None:
|
||
assert select_documents([], n=2) == []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 113 — the usefulness bar: cited vs related tiers
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _cos_chunk(doc: Document, score: float, cosine: float, position: int = 0) -> RetrievedChunk:
|
||
"""A candidate with *score* (fused rank key) and *cosine* (gate input) decoupled."""
|
||
return RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=position,
|
||
content=doc.content[:40],
|
||
score=score,
|
||
document=doc,
|
||
cosine=cosine,
|
||
)
|
||
|
||
|
||
def test_tiered_both_clear_floor_both_cited() -> None:
|
||
"""Both docs clear the bar → both cited, nothing related."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.9, 0.50), _cos_chunk(b, 0.8, 0.40)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["a.md", "b.md"]
|
||
assert related == []
|
||
|
||
|
||
def test_tiered_strong_plus_weak_one_cited_one_related() -> None:
|
||
"""The recurring incident shape: a strong 1st doc and a weak 2nd — the
|
||
weak doc loses its citation slot and lands in the related tier."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.9, 0.50), _cos_chunk(b, 0.8, 0.10)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["a.md"]
|
||
assert [d.path for d in related] == ["b.md"]
|
||
|
||
|
||
def test_tiered_both_weak_zero_cited_all_related() -> None:
|
||
"""Neither doc clears the bar → no citation slot at all (the bar
|
||
filters, it never backfills), the weak hits become the related tier."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.9, 0.20), _cos_chunk(b, 0.8, 0.15)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert cited == []
|
||
assert [d.path for d in related] == ["a.md", "b.md"] # rank order kept
|
||
|
||
|
||
def test_tiered_related_cap_respected() -> None:
|
||
"""The related tier is capped (related_max_docs) in rank order."""
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(3)]
|
||
chunks = [_cos_chunk(d, 0.9 - 0.1 * i, 0.10 - 0.02 * i) for i, d in enumerate(docs_in)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert cited == []
|
||
assert [d.path for d in related] == ["d0.md", "d1.md"]
|
||
|
||
|
||
def test_tiered_n_is_ceiling_not_quota() -> None:
|
||
"""A single strong doc yields ONE cited doc — the bar never pads the
|
||
cited tier up to ``n`` (LOCKED A2). And docs that clear the bar but
|
||
exceed the ceiling fall through to the related tier (the next docs in
|
||
rank order, never overlapping cited)."""
|
||
only = _doc("only.md", "O" * 50)
|
||
cited, related = select_documents_tiered(
|
||
[_cos_chunk(only, 0.9, 0.80)], n=2, floor=0.35, related_cap=2
|
||
)
|
||
assert [d.path for d in cited] == ["only.md"]
|
||
assert related == []
|
||
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(4)]
|
||
chunks = [_cos_chunk(d, 0.9 - 0.1 * i, 0.8 - 0.05 * i) for i, d in enumerate(docs_in)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["d0.md", "d1.md"] # the ceiling
|
||
assert [d.path for d in related] == ["d2.md", "d3.md"] # the next in rank order
|
||
|
||
|
||
def test_tiered_bar_skips_weak_rank_one() -> None:
|
||
"""A weak rank-1 doc does not consume a citation slot: the next-ranked
|
||
bar-clearing docs take it (the bar filters, it does not backfill)."""
|
||
w = _doc("w.md", "W" * 50)
|
||
s1 = _doc("s1.md", "1" * 50)
|
||
s2 = _doc("s2.md", "2" * 50)
|
||
chunks = [
|
||
_cos_chunk(w, 0.9, 0.20), # rank 1 — below the bar
|
||
_cos_chunk(s1, 0.8, 0.90),
|
||
_cos_chunk(s2, 0.7, 0.80),
|
||
]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.5, related_cap=2)
|
||
assert [d.path for d in cited] == ["s1.md", "s2.md"]
|
||
assert [d.path for d in related] == ["w.md"]
|
||
|
||
|
||
def test_tiered_tracks_best_chunk_cosine_across_a_docs_chunks() -> None:
|
||
"""The bar is on the doc's BEST hit-chunk cosine — a weak first chunk
|
||
(rank 1) does not sink a doc whose later chunk is vector-strong."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [
|
||
_cos_chunk(a, 0.9, 0.10, position=0), # a's weak chunk ranks first
|
||
_cos_chunk(a, 0.5, 0.90, position=2), # a's strong chunk
|
||
_cos_chunk(b, 0.4, 0.0), # lexical-only b
|
||
]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["a.md"]
|
||
assert [d.path for d in related] == ["b.md"]
|
||
|
||
|
||
def test_tiered_lexical_only_hit_goes_to_related_above_zero_floor() -> None:
|
||
"""A lexical-only doc (cosine 0.0 by construction) is vector-unsupported
|
||
by definition: above a zero floor it never earns a cited slot (LOCKED A2)."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
lexical_only = RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=0,
|
||
content="X" * 10,
|
||
score=0.9, # top fused rank (the FTS hit)
|
||
document=a,
|
||
cosine=0.0, # no vector rank — lexical-only
|
||
fts_hit=True,
|
||
)
|
||
chunks = [lexical_only, _cos_chunk(b, 0.8, 0.5)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["b.md"]
|
||
assert [d.path for d in related] == ["a.md"]
|
||
|
||
|
||
def test_tiered_floor_zero_is_no_bar() -> None:
|
||
"""A zero floor admits every scored document — lexical-only (cosine
|
||
0.0) and weak alike — so the bar can be disabled per deployment."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.9, 0.0), _cos_chunk(b, 0.8, 0.1)]
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.0, related_cap=2)
|
||
assert [d.path for d in cited] == ["a.md", "b.md"]
|
||
assert related == []
|
||
|
||
|
||
def test_tiered_empty_chunks_yield_empty_tiers() -> None:
|
||
assert select_documents_tiered([], n=2, floor=0.35, related_cap=2) == ([], [])
|
||
|
||
|
||
def test_select_documents_wrapper_is_legacy_tiering() -> None:
|
||
"""The wrapper (floor 0.0, cap 0) is the legacy "any score, top-N"
|
||
selection — byte-identical for the shapes the existing callers see:
|
||
rank order, dedupe, multi-chunk docs, ties, lexical-only hits."""
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(5)]
|
||
chunks = [
|
||
_chunk(docs_in[0], 0.5), # cosine 0.0 (lexical-only)
|
||
_cos_chunk(docs_in[0], 0.9, 0.4, position=1),
|
||
_chunk(docs_in[1], 0.8),
|
||
_cos_chunk(docs_in[1], 0.8, 0.2, position=1), # fused tie across docs
|
||
_cos_chunk(docs_in[2], 0.7, 0.0),
|
||
_cos_chunk(docs_in[3], 0.6, 0.1),
|
||
_chunk(docs_in[4], 0.1),
|
||
]
|
||
for n in (1, 2, 3, 10):
|
||
assert select_documents(chunks, n=n) == select_documents_tiered(
|
||
chunks, n, 0.0, 0
|
||
)[0]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 118 — select_suggested: the top-N "start here" tier, NO floor (A3)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_suggested_rank_order_by_first_seen_chunk() -> None:
|
||
"""The SAME stable walk as ``select_documents_tiered``: a document's
|
||
rank is fixed by its FIRST seen chunk in score-descending order — a
|
||
doc whose best chunk appears later in the input list still ranks
|
||
where that chunk falls."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
chunks = [
|
||
_chunk(a, 0.4, position=0), # a's weak chunk comes first
|
||
_chunk(b, 0.8),
|
||
_chunk(a, 0.9, position=2), # a's best chunk comes last
|
||
_chunk(c, 0.5),
|
||
]
|
||
out = select_suggested(chunks, n=5)
|
||
assert [d.path for d in out] == ["a.md", "b.md", "c.md"]
|
||
# The rows carry the full content byte-identical (A6: the content is
|
||
# what ``read`` serves later — never truncated).
|
||
assert out[0].content == "A" * 50
|
||
assert TRUNCATION_MARKER not in out[0].content
|
||
|
||
|
||
def test_suggested_dedupes_multiple_chunks_to_one_row() -> None:
|
||
"""Multiple hit chunks of one document collapse to a single row."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [
|
||
_chunk(a, 0.2),
|
||
_chunk(b, 0.7),
|
||
_chunk(a, 0.9, position=2),
|
||
_chunk(a, 0.5),
|
||
]
|
||
out = select_suggested(chunks, n=5)
|
||
assert [d.path for d in out] == ["a.md", "b.md"] # one row per document
|
||
assert out[0] is a
|
||
|
||
|
||
def test_suggested_caps_at_n_in_rank_order() -> None:
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(7)]
|
||
chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)]
|
||
out = select_suggested(chunks, n=3)
|
||
assert [d.path for d in out] == ["d0.md", "d1.md", "d2.md"]
|
||
|
||
|
||
def test_suggested_default_cap_is_the_settings_value(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
"""``n`` omitted → ``BOR_SUGGESTED_DOCS`` caps the walk — default 5
|
||
(LOCKED A3, the top-5 "start here" directive, TODO L3) — and the cap
|
||
is the setting's LIVE value, not a frozen constant."""
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(7)]
|
||
chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)]
|
||
settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue]
|
||
assert settings.suggested_docs == 5 # the production default
|
||
monkeypatch.setattr(retriever, "get_settings", lambda: settings)
|
||
assert [d.path for d in select_suggested(chunks)] == [
|
||
f"d{i}.md" for i in range(5)
|
||
]
|
||
small = Settings(_env_file=None, suggested_docs=2) # pyright: ignore[reportCallIssue]
|
||
monkeypatch.setattr(retriever, "get_settings", lambda: small)
|
||
assert [d.path for d in select_suggested(chunks)] == ["d0.md", "d1.md"]
|
||
|
||
|
||
def test_suggested_never_filters_on_cosine_floor() -> None:
|
||
"""NO floor (LOCKED A3): a lexical-only hit (cosine 0.0 by
|
||
construction) is a suggestion when it ranks — the contrast pin
|
||
against ``select_documents_tiered``'s floored cited tier on the SAME
|
||
input, which demotes it to the related tier."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
lexical_only = RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=0,
|
||
content="X" * 10,
|
||
score=0.9, # top fused rank (the FTS hit)
|
||
document=a,
|
||
cosine=0.0, # no vector rank — lexical-only
|
||
fts_hit=True,
|
||
)
|
||
chunks = [lexical_only, _cos_chunk(b, 0.8, 0.5)]
|
||
# Suggested: the floor never filters — a leads, b follows in rank order.
|
||
assert [d.path for d in select_suggested(chunks, n=5)] == ["a.md", "b.md"]
|
||
# Contrast: the same input through the phase-113 cited tier — the
|
||
# 0.35 usefulness bar demotes the lexical-only doc to related.
|
||
cited, related = select_documents_tiered(chunks, n=2, floor=0.35, related_cap=2)
|
||
assert [d.path for d in cited] == ["b.md"]
|
||
assert [d.path for d in related] == ["a.md"]
|
||
|
||
|
||
def test_suggested_tie_break_inherited_from_fused_order() -> None:
|
||
"""Equal fused scores keep the input (fused) order — the stable
|
||
score-only walk inherits ``fuse()``'s (-score, -cosine, path,
|
||
position) tie-break; the selector never re-sorts it away."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.7, 0.5), _cos_chunk(b, 0.7, 0.4)] # a wins on cosine
|
||
assert [d.path for d in select_suggested(chunks, n=5)] == ["a.md", "b.md"]
|
||
# A FULL tie (score AND cosine): the input position — ``fuse()``'s
|
||
# path/position tie-break already applied — decides. ``m.md`` sorts
|
||
# AFTER ``b2.md`` alphabetically, so any re-sort by path would flip
|
||
# the order; the fused input order must win.
|
||
m = _doc("m.md", "M" * 50)
|
||
b2 = _doc("b2.md", "B" * 50)
|
||
chunks = [_cos_chunk(m, 0.7, 0.4), _cos_chunk(b2, 0.7, 0.4)]
|
||
assert [d.path for d in select_suggested(chunks, n=5)] == ["m.md", "b2.md"]
|
||
|
||
|
||
def test_suggested_empty_chunks_yield_no_documents() -> None:
|
||
assert select_suggested([], n=5) == []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 118, task 05 — select_related: the rank-6+ tier after the suggested set
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_related_walk_order_after_excluded_set() -> None:
|
||
"""The SAME stable score-descending walk as ``select_suggested``:
|
||
a document's rank is fixed by its FIRST seen chunk; documents in
|
||
*excluded_ids* (the suggested set) are skipped and the rest come
|
||
back in rank order."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
d = _doc("d.md", "D" * 50)
|
||
chunks = [
|
||
_chunk(a, 0.4, position=0), # a's weak chunk comes first
|
||
_chunk(b, 0.8),
|
||
_chunk(a, 0.9, position=2), # a's best chunk last — a ranks first
|
||
_chunk(c, 0.5),
|
||
_chunk(d, 0.3),
|
||
]
|
||
out = select_related(chunks, {a.id, b.id}, cap=2)
|
||
assert [x.path for x in out] == ["c.md", "d.md"]
|
||
|
||
|
||
def test_related_skips_excluded_documents() -> None:
|
||
"""Every document in *excluded_ids* is skipped, even when it would
|
||
rank inside the cap — the suggested set never rides the related row."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
chunks = [_chunk(a, 0.9), _chunk(b, 0.8), _chunk(c, 0.5)]
|
||
out = select_related(chunks, {a.id, b.id}, cap=5)
|
||
assert out == [c]
|
||
# Every doc excluded → empty, even with room left in the cap.
|
||
assert select_related(chunks, {a.id, b.id, c.id}, cap=5) == []
|
||
|
||
|
||
def test_related_caps_at_cap_in_rank_order() -> None:
|
||
"""The phase-118 turn wiring on 9 docs: suggested = the top 5,
|
||
related = rank 6–7 (capped at 2), disjoint from the suggested set."""
|
||
docs_in = [_doc(f"d{i}.md", "X" * 20) for i in range(9)]
|
||
chunks = [_chunk(d, 0.9 - 0.1 * i) for i, d in enumerate(docs_in)]
|
||
suggested = select_suggested(chunks, n=5)
|
||
out = select_related(chunks, {d.id for d in suggested}, cap=2)
|
||
assert [x.path for x in out] == ["d5.md", "d6.md"] # rank 6–7, capped
|
||
suggested_paths = {d.path for d in suggested}
|
||
assert suggested_paths.isdisjoint({x.path for x in out})
|
||
|
||
|
||
def test_related_cap_zero_yields_empty() -> None:
|
||
"""cap=0 is the kill switch (related_max_docs=0): no related docs,
|
||
the pre-phase-113 visibility."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
chunks = [_chunk(a, 0.9), _chunk(b, 0.8), _chunk(c, 0.5)]
|
||
assert select_related(chunks, {a.id}, cap=0) == []
|
||
|
||
|
||
def test_related_never_filters_on_cosine_floor() -> None:
|
||
"""NO floor: a lexical-only (cosine 0.0) doc is related when it
|
||
ranks after the excluded set — the related tier is visibility, not
|
||
citation (phase 118 applies no cosine floor to it)."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
lexical_only = RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=0,
|
||
content="X" * 10,
|
||
score=0.9, # top fused rank (the FTS hit)
|
||
document=a,
|
||
cosine=0.0, # no vector rank — lexical-only
|
||
fts_hit=True,
|
||
)
|
||
chunks = [lexical_only, _cos_chunk(b, 0.8, 0.5)]
|
||
out = select_related(chunks, set(), cap=5)
|
||
assert [x.path for x in out] == ["a.md", "b.md"]
|
||
|
||
|
||
def test_related_dedupes_multiple_chunks_to_one_row() -> None:
|
||
"""Multiple hit chunks of one document collapse to a single row
|
||
(first-seen-chunk rank, dedupe by document.id — the shared walk)."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_chunk(a, 0.2), _chunk(b, 0.7), _chunk(a, 0.9, position=2)]
|
||
out = select_related(chunks, set(), cap=5)
|
||
assert [x.path for x in out] == ["a.md", "b.md"] # one row per document
|
||
assert out[0] is a
|
||
|
||
|
||
def test_related_tie_break_inherited_from_fused_order() -> None:
|
||
"""Equal fused scores keep the input (fused) order — the stable
|
||
score-only walk inherits ``fuse()``'s (-score, -cosine, path,
|
||
position) tie-break; the selector never re-sorts it away."""
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
chunks = [_cos_chunk(a, 0.7, 0.5), _cos_chunk(b, 0.7, 0.4)] # a wins on cosine
|
||
assert [x.path for x in select_related(chunks, set(), cap=5)] == ["a.md", "b.md"]
|
||
m = _doc("m.md", "M" * 50)
|
||
b2 = _doc("b2.md", "B" * 50)
|
||
chunks = [_cos_chunk(m, 0.7, 0.4), _cos_chunk(b2, 0.7, 0.4)] # full tie
|
||
assert [x.path for x in select_related(chunks, set(), cap=5)] == ["m.md", "b2.md"]
|
||
|
||
|
||
def test_related_empty_chunks_yield_no_documents() -> None:
|
||
assert select_related([], set(), cap=5) == []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Hybrid retrieval (A7): RRF fusion + lexical tsquery
|
||
# ---------------------------------------------------------------------------
|
||
|
||
from app.rag.retriever import fuse, lexical_tsquery # noqa: E402
|
||
|
||
|
||
def _rc(
|
||
doc_path: str, cosine: float = 0.0, fts_hit: bool = False, position: int = 0,
|
||
is_summary: bool = False,
|
||
) -> RetrievedChunk:
|
||
return RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=position,
|
||
content="x" * 20,
|
||
score=0.0,
|
||
document=_doc(doc_path, "x" * 20),
|
||
cosine=cosine,
|
||
fts_hit=fts_hit,
|
||
is_summary=is_summary,
|
||
)
|
||
|
||
|
||
def test_lexical_tsquery_tokens_lowercased_deduped_in_order() -> None:
|
||
assert lexical_tsquery("How did I Install GITLAB gitlab?") == "how | did | i | install | gitlab"
|
||
|
||
|
||
def test_lexical_tsquery_punctuation_and_umlauts_ignored() -> None:
|
||
assert lexical_tsquery("c3-r00t? -- what's up!") == "c3 | r00t | what | s | up"
|
||
|
||
|
||
def test_lexical_tsquery_pure_symbols_return_none() -> None:
|
||
assert lexical_tsquery("??? ???") is None
|
||
assert lexical_tsquery("") is None
|
||
|
||
|
||
def test_lexical_tsquery_stopwords_left_to_postgres() -> None:
|
||
# lexical_tsquery passes raw tokens through; Postgres's to_tsquery
|
||
# lexing drops the stopwords (verified against real PG in
|
||
# test_retrieve_empty_kb / integration tests).
|
||
assert lexical_tsquery("how do i") == "how | do | i"
|
||
|
||
|
||
def test_lexical_tsquery_dotted_tokens_kept_whole() -> None:
|
||
"""The 2026-09-05 incident: the default parser lexes dotted words
|
||
as ONE lexeme ("llama.cpp" → 'llama.cpp', "Qwen 3.8" → '3.8'), so
|
||
the query carries them whole — split tokens (llama | cpp) can never
|
||
match the document side."""
|
||
assert lexical_tsquery(
|
||
"What are the correct llama.cpp arguments for Qwen 3.8?"
|
||
) == "what | are | the | correct | llama.cpp | arguments | for | qwen | 3.8"
|
||
# The dash still splits (only dots group): ai | internal.network.
|
||
assert lexical_tsquery("how did I set up ai-internal.network?") == (
|
||
"how | did | i | set | up | ai | internal.network"
|
||
)
|
||
|
||
|
||
def test_fuse_combines_both_lists_for_double_hits() -> None:
|
||
v1 = _rc("a.md", cosine=0.9)
|
||
v2 = _rc("b.md", cosine=0.5)
|
||
l1 = _rc("a.md", cosine=0.1) # same chunk id -> matched in place
|
||
a_id = v1.chunk_id
|
||
l1.chunk_id = a_id
|
||
out = fuse([v1, v2], [l1], k=60)
|
||
by_id = {rc.chunk_id: rc for rc in out}
|
||
# a: 1/61 (vector rank 1) + 1/61 (lexical rank 1); b: 1/62 only.
|
||
assert by_id[a_id].score == pytest.approx(2 / 61)
|
||
assert by_id[a_id].fts_hit is True
|
||
assert by_id[v2.chunk_id].score == pytest.approx(1 / 62)
|
||
assert by_id[v2.chunk_id].fts_hit is False
|
||
assert [rc.chunk_id for rc in out] == [a_id, v2.chunk_id]
|
||
|
||
|
||
def test_fuse_lexical_only_chunks_enter_with_zero_cosine() -> None:
|
||
vector = [_rc("a.md", cosine=0.8)]
|
||
lexical = [_rc("b.md", cosine=0.0, fts_hit=True)]
|
||
out = fuse(vector, lexical, k=60)
|
||
assert len(out) == 2
|
||
b = next(rc for rc in out if rc.document.path == "b.md")
|
||
assert b.cosine == 0.0
|
||
assert b.fts_hit is True
|
||
# Still ranked by its (only) RRF term.
|
||
assert b.score == pytest.approx(1 / 61)
|
||
|
||
|
||
def test_fuse_orders_by_score_then_cosine_then_path() -> None:
|
||
# Two chunks share an RRF score (both rank 1 in different lists):
|
||
# the higher-cosine one must sort first.
|
||
hi = _rc("z.md", cosine=0.9)
|
||
lo = _rc("a.md", cosine=0.2)
|
||
out = fuse([hi], [lo], k=60)
|
||
assert [rc.document.path for rc in out] == ["z.md", "a.md"]
|
||
# Equal score AND cosine -> path order.
|
||
p1 = _rc("b.md", cosine=0.5)
|
||
p2 = _rc("a.md", cosine=0.5)
|
||
out = fuse([p1], [p2], k=60)
|
||
assert [rc.document.path for rc in out] == ["a.md", "b.md"]
|
||
# Equal score, cosine, path -> position order.
|
||
s1 = _rc("a.md", cosine=0.5, position=1)
|
||
s2 = _rc("a.md", cosine=0.5, position=0)
|
||
out = fuse([s1], [s2], k=60)
|
||
assert [rc.position for rc in out] == [0, 1]
|
||
|
||
|
||
def test_fuse_rejects_nonpositive_k() -> None:
|
||
with pytest.raises(ValueError):
|
||
fuse([], [], k=0)
|
||
with pytest.raises(ValueError):
|
||
fuse([], [], k=-1)
|
||
|
||
|
||
def test_fuse_empty_lists() -> None:
|
||
assert fuse([], [], k=60) == []
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 30: is_summary survives both candidate lists and the fusion
|
||
# ---------------------------------------------------------------------------
|
||
|
||
from app.models import Chunk # noqa: E402
|
||
from app.rag.retriever import ( # noqa: E402
|
||
NAME_HIT_LIMIT,
|
||
_lexical_candidates,
|
||
_name_hit_chunks,
|
||
_normalize_name,
|
||
_vector_candidates,
|
||
name_hit_tokens,
|
||
)
|
||
|
||
|
||
class _FakeResult:
|
||
"""Stands in for SQLAlchemy's RowMapping result (``.all()`` only)."""
|
||
|
||
def __init__(self, rows: list) -> None:
|
||
self._rows = rows
|
||
|
||
def all(self) -> list:
|
||
return self._rows
|
||
|
||
|
||
class _FakeSession:
|
||
"""Returns canned rows from ``execute`` without touching Postgres.
|
||
|
||
One list of rows (legacy form) is returned for EVERY call; several
|
||
lists (one per successive ``execute``) model a query sequence — the
|
||
name-hit lexical path (2026-09-05) issues the document-projection
|
||
query and, when hits exist, the LATERAL chunk query, BEFORE the FTS
|
||
query.
|
||
"""
|
||
|
||
def __init__(self, *rowsets: list) -> None:
|
||
if len(rowsets) == 1 and not (
|
||
rowsets[0] and isinstance(rowsets[0][0], list)
|
||
):
|
||
rowsets = (rowsets[0],) # the single-rowset legacy form
|
||
self._rowsets = rowsets
|
||
self._call = 0
|
||
self.statements: list = []
|
||
|
||
def execute(self, stmt, params: dict | None = None) -> _FakeResult:
|
||
self.statements.append((stmt, params))
|
||
rows = self._rowsets[min(self._call, len(self._rowsets) - 1)]
|
||
self._call += 1
|
||
return _FakeResult(rows)
|
||
|
||
|
||
def _chunk_row(is_summary: bool) -> Chunk:
|
||
doc = _doc("summary-src.yaml", "RAW_YAML_CONTENT")
|
||
return Chunk(
|
||
id=uuid.uuid4(),
|
||
document_id=doc.id,
|
||
position=-1, # the summary chunk's position (phase 30)
|
||
content="Summary text",
|
||
is_summary=is_summary,
|
||
)
|
||
|
||
|
||
def test_vector_candidates_carry_is_summary_flag() -> None:
|
||
"""The vector list copies ``Chunk.is_summary`` onto each candidate."""
|
||
doc = _doc("summary-src.yaml", "RAW_YAML_CONTENT")
|
||
summary = _chunk_row(is_summary=True)
|
||
ordinary = _chunk_row(is_summary=False)
|
||
ordinary.position = 0
|
||
ordinary.content = "ordinary content"
|
||
rows = [
|
||
(summary, 0.123456, doc),
|
||
(ordinary, 0.2, doc),
|
||
]
|
||
out = _vector_candidates(_FakeSession(rows), [0.0] * 768, limit=5) # pyright: ignore[reportArgumentType]
|
||
assert len(out) == 2
|
||
by_pos = {rc.position: rc for rc in out}
|
||
assert by_pos[-1].is_summary is True # the summary chunk (position −1)
|
||
assert by_pos[0].is_summary is False # ordinary content chunk
|
||
assert by_pos[-1].cosine == pytest.approx(0.876544) # 1 − distance, still rounded
|
||
|
||
|
||
def test_vector_candidates_default_is_summary_false_for_legacy_chunks() -> None:
|
||
"""Pre-phase-30 rows have ``is_summary=false`` — candidates stay False."""
|
||
doc = _doc("legacy.md", "LEGACY")
|
||
legacy = Chunk(
|
||
id=uuid.uuid4(),
|
||
document_id=doc.id,
|
||
position=0,
|
||
content="legacy content",
|
||
is_summary=False,
|
||
)
|
||
out = _vector_candidates(_FakeSession([(legacy, 0.5, doc)]), [0.0] * 768, limit=5) # pyright: ignore[reportArgumentType]
|
||
assert out[0].is_summary is False
|
||
|
||
|
||
def _lexical_row(is_summary: bool, doc_path: str) -> object:
|
||
"""One row of ``_LEXICAL_SQL`` (attribute access, as SQLAlchemy returns)."""
|
||
doc = _doc(doc_path, "DOC_BODY")
|
||
return SimpleNamespace(
|
||
chunk_id=uuid.uuid4(),
|
||
position=-1 if is_summary else 0,
|
||
content="summary chunk text" if is_summary else "content chunk text",
|
||
doc_id=doc.id,
|
||
source=doc.source,
|
||
path=doc.path,
|
||
full_path=doc.full_path,
|
||
title=doc.title,
|
||
doc_content=doc.content,
|
||
content_hash=doc.content_hash,
|
||
indexed_at=None,
|
||
created_at=None,
|
||
is_summary=is_summary,
|
||
rank=0.33,
|
||
)
|
||
|
||
|
||
def test_lexical_candidates_carry_is_summary_flag() -> None:
|
||
"""The lexical list reads ``c.is_summary`` from the raw row.
|
||
|
||
The question's tokens are name candidates (class-agnostic, phase
|
||
119), so the name-hit projection runs — but the (empty) catalog
|
||
yields no path match, the LATERAL fetch is skipped, and the FTS
|
||
rowset answers the second call: the list is the plain FTS rows,
|
||
every one ``name_hit=False``.
|
||
"""
|
||
rows = [_lexical_row(True, "summary-src.yaml"), _lexical_row(False, "other.md")]
|
||
out = _lexical_candidates(
|
||
_FakeSession([], rows), "how do i configure the thing", limit=10 # pyright: ignore[reportArgumentType]
|
||
)
|
||
assert len(out) == 2
|
||
by_path = {rc.document.path: rc for rc in out}
|
||
assert by_path["summary-src.yaml"].is_summary is True
|
||
assert by_path["summary-src.yaml"].position == -1
|
||
assert by_path["other.md"].is_summary is False
|
||
assert all(rc.fts_hit is True for rc in out)
|
||
assert all(rc.name_hit is False for rc in out) # ordinary FTS rows
|
||
|
||
|
||
def test_fuse_keeps_is_summary_on_double_hit() -> None:
|
||
"""A summary chunk in both lists keeps the flag after fusion."""
|
||
v1 = _rc("s.yaml", cosine=0.9, is_summary=True)
|
||
l1 = _rc("s.yaml", cosine=0.9, is_summary=True) # lexical copy of the same chunk
|
||
l1.chunk_id = v1.chunk_id
|
||
out = fuse([v1], [l1], k=60)
|
||
assert len(out) == 1
|
||
assert out[0].is_summary is True
|
||
assert out[0].fts_hit is True
|
||
assert out[0].score == pytest.approx(2 / 61)
|
||
|
||
|
||
def test_fuse_keeps_is_summary_on_lexical_only_hit() -> None:
|
||
"""A summary-only lexical hit (no vector rank) keeps the flag."""
|
||
out = fuse([], [_rc("s.yaml", is_summary=True)], k=60)
|
||
assert len(out) == 1
|
||
assert out[0].is_summary is True
|
||
assert out[0].fts_hit is True
|
||
assert out[0].cosine == 0.0
|
||
|
||
|
||
def test_fuse_default_is_summary_stays_false_for_legacy_chunks() -> None:
|
||
"""Neither list flagged ⇒ fusion never invents a summary flag."""
|
||
out = fuse([_rc("a.md", cosine=0.8)], [_rc("b.md")], k=60)
|
||
assert len(out) == 2
|
||
assert all(rc.is_summary is False for rc in out)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Name-hit lexical signal (the 2026-09-05 incident — the versioned-name
|
||
# case the default parser lexes incompatibly: "Qwen 3.8" → qwen/3/8 can
|
||
# never match a document's qwen3/8/27b tokens)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
INCIDENT_QUESTION = "What are the correct llama.cpp arguments for Qwen 3.8?"
|
||
|
||
|
||
def test_normalize_name() -> None:
|
||
assert _normalize_name("Qwen 3.8") == "qwen38"
|
||
assert _normalize_name("qwen3.8-27b-juggernaut-vulkan") == "qwen3827bjuggernautvulkan"
|
||
assert _normalize_name("Mixed CASE-99") == "mixedcase99"
|
||
assert _normalize_name("!!!") == ""
|
||
|
||
|
||
def test_name_hit_tokens_incident_question() -> None:
|
||
"""Phase 119 (LOCKED A2): the candidate list is CLASS-AGNOSTIC —
|
||
every normalized token of length >= 4 (dotted kept whole:
|
||
``llama.cpp`` → ``llamacpp``) plus the versioned join ``qwen38``.
|
||
The digit distinction moved to the match side (:func:`_name_hit_chunks`) —
|
||
prose precision now comes from the match class (a digitless token
|
||
must EQUAL a whole path component). The single digits ("3", "8")
|
||
and the bare "38" are < 4 chars; the digit-leading ``38show``
|
||
boundary artifact cannot survive (the join only fires on a purely
|
||
numeric SECOND token)."""
|
||
tokens = name_hit_tokens(INCIDENT_QUESTION)
|
||
assert tokens == ["what", "correct", "llamacpp", "arguments", "qwen", "qwen38"]
|
||
for absent in ("3", "8", "38", "38show", "server"):
|
||
assert absent not in tokens
|
||
|
||
|
||
def test_name_hit_tokens_digitless_question_yields_long_tokens() -> None:
|
||
"""A question with NO digit-bearing token still yields candidates
|
||
(every normalized token of length >= 4) — the 2026-09-16 fix:
|
||
product names without digits ("gitea", "gateway") must get a name
|
||
signal. Prose joins (``correctcaddy``) never count (the second
|
||
token is not purely numeric)."""
|
||
assert name_hit_tokens("what is the correct caddy config") == [
|
||
"what", "correct", "caddy", "config",
|
||
]
|
||
assert name_hit_tokens("a e i o u 3 8") == []
|
||
|
||
|
||
def test_name_hit_tokens_bare_digit_bearing_token() -> None:
|
||
"""A single written token that carries a digit (``1panel``) is a
|
||
name candidate on its own — no join needed — alongside the plain
|
||
prose tokens of the same question (class-agnostic list)."""
|
||
tokens = name_hit_tokens("what is my 1panel dashboard setup")
|
||
assert tokens == ["what", "1panel", "dashboard", "setup"]
|
||
|
||
|
||
def test_name_hit_tokens_versioned_join_and_short_tokens() -> None:
|
||
"""The versioned join survives the class-agnostic change ("Qwen
|
||
3.8" → ``qwen38``), and short tokens (< :data:`NAME_TOKEN_MIN_LEN`
|
||
normalized — the single digits, "3.8" → ``38``) never become
|
||
candidates, with or without a join."""
|
||
tokens = name_hit_tokens("help me with Qwen 3.8 please")
|
||
# The join is appended at its FIRST token's position (after "qwen").
|
||
assert tokens == ["help", "with", "qwen", "qwen38", "please"]
|
||
assert name_hit_tokens("3.8 8 16 9") == [] # 38 / 8 / 16 / 9 / 816 / 169 all < 4
|
||
# Word-after-version: the word itself is a candidate, but the
|
||
# digit-leading join artifact ("38show") cannot survive (the join
|
||
# only fires on a purely numeric SECOND token).
|
||
assert name_hit_tokens("3.8 show") == ["show"]
|
||
|
||
|
||
def _name_row(doc: Document) -> tuple:
|
||
"""One row of the name-hit document projection (catalog order)."""
|
||
return (doc.id, doc.source, doc.path, doc.title)
|
||
|
||
|
||
def _name_hit_lateral_row(doc: Document, is_summary: bool = False) -> SimpleNamespace:
|
||
"""One row of the name-hit LATERAL chunk query."""
|
||
return SimpleNamespace(
|
||
doc_id=doc.id,
|
||
source=doc.source,
|
||
path=doc.path,
|
||
full_path=doc.full_path,
|
||
title=doc.title,
|
||
doc_content=doc.content,
|
||
content_hash=doc.content_hash,
|
||
indexed_at=None,
|
||
created_at=None,
|
||
chunk_id=uuid.uuid4(),
|
||
position=-1 if is_summary else 0,
|
||
content="summary chunk" if is_summary else "content chunk",
|
||
is_summary=is_summary,
|
||
)
|
||
|
||
|
||
def test_name_hit_chunks_no_tokens_skips_all_queries() -> None:
|
||
"""A question with no name tokens (every normalized token < 4)
|
||
issues no queries at all."""
|
||
session = _FakeSession([]) # any call would surface a statement
|
||
assert _name_hit_chunks(session, "a e i o u 3 8") == [] # pyright: ignore[reportArgumentType]
|
||
assert session.statements == []
|
||
|
||
|
||
def test_name_hit_chunks_no_matching_doc_returns_empty() -> None:
|
||
"""Name tokens exist but no document name carries one: the
|
||
projection runs, the LATERAL fetch does not."""
|
||
doc = _doc("quadlets/other.container", "body")
|
||
name_rows = [_name_row(doc)]
|
||
session = _FakeSession(name_rows, [])
|
||
assert _name_hit_chunks(session, INCIDENT_QUESTION) == [] # pyright: ignore[reportArgumentType]
|
||
assert len(session.statements) == 1 # projection only — no LATERAL fetch
|
||
|
||
|
||
def test_name_hit_chunks_digitless_exact_part_stem_subcomponent() -> None:
|
||
"""A DIGITLESS token EQUALS a normalized path part (the ``gitea/``
|
||
folder), the file stem (``gitea.md``), or a stem sub-component
|
||
(``kubernetes_gitea``, ``gitea-values``, ``test-gateway`` — the
|
||
stem split on non-alphanumeric runs) — the 2026-09-16 product-name
|
||
signal (LOCKED A2)."""
|
||
question = "how do i set up gitea or the gateway" # tokens: [gitea, gateway]
|
||
docs = [
|
||
_doc("deploy/reeseapps/gitea/README.md", "body"), # path part
|
||
_doc("notes/gitea.md", "body"), # file stem
|
||
_doc("deploy/k8s/kubernetes_gitea.md", "body"), # sub-component
|
||
_doc("deploy/k8s/gitea-values.yaml", "body"), # sub-component
|
||
_doc("deploy/istio/test-gateway.yaml", "body"), # sub-component (gateway)
|
||
_doc("notes/gitlab.md", "body"), # NO component matches — excluded
|
||
]
|
||
name_rows = [_name_row(d) for d in docs]
|
||
session = _FakeSession(name_rows, [_name_hit_lateral_row(d) for d in docs[:5]])
|
||
out = _name_hit_chunks(session, question) # pyright: ignore[reportArgumentType]
|
||
# Five one-token hits, catalog order (source, path):
|
||
assert [rc.document.path for rc in out] == [
|
||
"deploy/istio/test-gateway.yaml",
|
||
"deploy/k8s/gitea-values.yaml",
|
||
"deploy/k8s/kubernetes_gitea.md",
|
||
"deploy/reeseapps/gitea/README.md",
|
||
"notes/gitea.md",
|
||
]
|
||
assert all(rc.name_hit is True for rc in out)
|
||
assert all(rc.fts_hit is True for rc in out) # the lexical signal
|
||
assert all(rc.cosine == 0.0 for rc in out) # no vector rank
|
||
|
||
|
||
def test_name_hit_chunks_digitless_title_never_matched() -> None:
|
||
"""The owner-verified failure mode of the naive relaxation: a doc
|
||
under a ``Deployments/`` folder titled "Deployments" does NOT hit
|
||
the common token ``deploy`` (the part normalizes to
|
||
``deployments`` ≠ ``deploy``), and a doc titled "Gitea" with no
|
||
gitea path component does NOT hit ``gitea`` — TITLES ARE NEVER
|
||
MATCHED (LOCKED A2)."""
|
||
question = "how do i deploy gitea" # tokens: [deploy, gitea]
|
||
docs = [
|
||
_doc("Deployments/reeseapps/README.md", "body", title="Deployments"),
|
||
_doc("notes/internal-notes.md", "body", title="Gitea"), # title only
|
||
]
|
||
session = _FakeSession([_name_row(d) for d in docs], [])
|
||
assert _name_hit_chunks(session, question) == [] # pyright: ignore[reportArgumentType]
|
||
assert len(session.statements) == 1 # projection only — no LATERAL fetch
|
||
|
||
|
||
def test_name_hit_chunks_digit_bearing_prefix_not_midword() -> None:
|
||
"""A DIGIT-BEARING token is a PREFIX of a normalized part or stem
|
||
(``qwen38`` → ``qwen3.8-27b-epic-vulkan.container``) — a stem that
|
||
merely CONTAINS the token mid-word (``xqwen38y…``) does NOT hit;
|
||
sub-components are in the exact-match class only (LOCKED A2)."""
|
||
question = "what are the arguments for qwen 3.8" # tokens: what, arguments, qwen, qwen38
|
||
hit = _doc("quadlets/qwen3.8-27b-epic-vulkan.container", "body")
|
||
miss = _doc("quadlets/xqwen38y-test.container", "body") # mid-word containment
|
||
name_rows = [_name_row(hit), _name_row(miss)]
|
||
session = _FakeSession(name_rows, [_name_hit_lateral_row(hit)])
|
||
out = _name_hit_chunks(session, question) # pyright: ignore[reportArgumentType]
|
||
assert [rc.document.path for rc in out] == [hit.path]
|
||
assert out[0].name_hit is True
|
||
|
||
|
||
def test_name_hit_chunks_ranked_by_count_then_catalog() -> None:
|
||
"""A question (``deploy`` + ``gitea`` + ``qwen`` + ``qwen38``):
|
||
the document whose path carries BOTH a digitless component and a
|
||
digit-bearing prefix (2 matched tokens) leads; the two
|
||
single-token documents tie on count and fall to CATALOG ORDER —
|
||
the old total-matched-length tie-break is RETIRED (it would have
|
||
put the 6-char ``qwen38`` hit, ``quadlets/…``, before the 5-char
|
||
``gitea`` hit, ``gitea/notes.md`` — the flip is pinned). The
|
||
"Deployments"-titled doc and the title-only "Gitea" doc never
|
||
appear (titles are never matched)."""
|
||
precision = _doc("Deployments/reeseapps/README.md", "body", title="Deployments")
|
||
gitea_notes = _doc("gitea/notes.md", "body", title="Internal notes")
|
||
both = _doc("gitea/qwen3.8-model.container", "body", title="The model quadlet")
|
||
q38 = _doc("quadlets/qwen3.8-27b-juggernaut-vulkan.container", "body", title="juggernaut")
|
||
title_only = _doc("notes/internal-notes.md", "body", title="Gitea")
|
||
name_rows = [_name_row(d) for d in (precision, gitea_notes, both, q38, title_only)]
|
||
question = "how do i deploy gitea with qwen 3.8"
|
||
lateral_rows = [
|
||
_name_hit_lateral_row(q38, is_summary=True), # LATERAL may return any order
|
||
_name_hit_lateral_row(both),
|
||
_name_hit_lateral_row(gitea_notes),
|
||
]
|
||
session = _FakeSession(name_rows, lateral_rows)
|
||
out = _name_hit_chunks(session, question) # pyright: ignore[reportArgumentType]
|
||
assert [rc.document.path for rc in out] == [
|
||
"gitea/qwen3.8-model.container", # 2 matched tokens (gitea + qwen38) — leads
|
||
"gitea/notes.md", # 1 token (gitea, 5 chars) — catalog order beats quadlets
|
||
"quadlets/qwen3.8-27b-juggernaut-vulkan.container", # 1 token (qwen38, 6 chars)
|
||
]
|
||
assert all(rc.name_hit is True for rc in out)
|
||
assert all(rc.fts_hit is True for rc in out) # the lexical signal
|
||
assert all(rc.cosine == 0.0 for rc in out) # no vector rank
|
||
assert all(rc.score == 0.0 for rc in out) # fuse fills the score
|
||
by_path = {rc.document.path: rc for rc in out}
|
||
# The representative chunk keeps its summary flag (the LATERAL
|
||
# choice: is_summary DESC, position ASC — chunk 0 otherwise).
|
||
assert by_path["quadlets/qwen3.8-27b-juggernaut-vulkan.container"].is_summary is True
|
||
assert by_path["quadlets/qwen3.8-27b-juggernaut-vulkan.container"].position == -1
|
||
assert by_path["gitea/notes.md"].is_summary is False
|
||
|
||
|
||
def test_name_hit_chunks_short_tokens_never_hit() -> None:
|
||
"""Short tokens (< 4 normalized — "3.8" → ``38``, the single
|
||
digits) are never candidates, so they can never hit, with or
|
||
without the versioned join."""
|
||
session = _FakeSession([]) # any call would surface a statement
|
||
assert _name_hit_chunks(session, "3.8 8 16 9") == [] # pyright: ignore[reportArgumentType]
|
||
assert session.statements == []
|
||
|
||
|
||
def test_name_hit_chunks_capped_at_limit() -> None:
|
||
"""Twelve tied name hits (one matched token each — the ``qwen38``
|
||
stem prefix) yield exactly ``NAME_HIT_LIMIT`` of them — catalog
|
||
order (the deterministic tie-break)."""
|
||
docs = [_doc(f"quadlets/qwen3.8-m{i:02d}.container", "body") for i in range(12)]
|
||
name_rows = [_name_row(d) for d in docs]
|
||
# Only the ten winners (catalog order — the deterministic tie-break
|
||
# of the twelve identical scores) reach the LATERAL fetch; the fake
|
||
# answers with exactly those rows.
|
||
lateral_rows = [_name_hit_lateral_row(d) for d in docs[:NAME_HIT_LIMIT]]
|
||
session = _FakeSession(name_rows, lateral_rows)
|
||
out = _name_hit_chunks(
|
||
session, "tell me about the qwen 3.8 models" # pyright: ignore[reportArgumentType]
|
||
)
|
||
assert len(out) == NAME_HIT_LIMIT
|
||
assert [rc.document.path for rc in out] == [
|
||
f"quadlets/qwen3.8-m{i:02d}.container" for i in range(10)
|
||
]
|
||
assert all(rc.name_hit is True for rc in out)
|
||
|
||
|
||
def test_lexical_candidates_name_hits_lead_and_dedupe_with_fts() -> None:
|
||
"""The full lexical list: name hits LEAD (their representative
|
||
chunks), the FTS rows follow, and an FTS row sharing the name hit's
|
||
chunk id appears exactly once (deduped)."""
|
||
q38 = _doc("quadlets/qwen3.8-27b-juggernaut-vulkan.container", "body")
|
||
other = _doc("quadlets/qwen38-other.container", "body") # 1 matched token
|
||
name_rows = [_name_row(q38), _name_row(other)]
|
||
q38_chunk = uuid.uuid4()
|
||
|
||
def _lateral(doc: Document) -> SimpleNamespace:
|
||
row = _name_hit_lateral_row(doc)
|
||
if doc is q38:
|
||
row.chunk_id = q38_chunk
|
||
return row
|
||
|
||
lateral_rows = [_lateral(q38), _lateral(other)]
|
||
fts_rows = [
|
||
# an FTS hit on the SAME chunk as the q38 name hit (deduped away)
|
||
SimpleNamespace(
|
||
chunk_id=q38_chunk, position=1, content="c", doc_id=q38.id,
|
||
source=q38.source, path=q38.path, full_path=q38.full_path,
|
||
title=q38.title, doc_content=q38.content, content_hash=q38.content_hash,
|
||
indexed_at=None, created_at=None, is_summary=False, rank=0.1,
|
||
),
|
||
# an FTS hit on a different chunk of the OTHER doc (kept)
|
||
_lexical_row(False, "quadlets/qwen38-other.container"),
|
||
]
|
||
session = _FakeSession(name_rows, lateral_rows, fts_rows)
|
||
out = _lexical_candidates(session, INCIDENT_QUESTION, limit=10) # pyright: ignore[reportArgumentType]
|
||
assert len(out) == 3 # q38 (once), other (name hit), other (FTS chunk)
|
||
# Both name hits tie on count (1) — catalog order: "qwen3." (ASCII 46)
|
||
# sorts before "qwen38" (ASCII 56).
|
||
assert out[0].document.path == "quadlets/qwen3.8-27b-juggernaut-vulkan.container"
|
||
assert out[1].document.path == "quadlets/qwen38-other.container"
|
||
assert out[0].chunk_id == q38_chunk # the name-hit representative row
|
||
assert {
|
||
rc.chunk_id for rc in out
|
||
} == {q38_chunk, fts_rows[1].chunk_id, lateral_rows[1].chunk_id}
|
||
assert all(rc.fts_hit is True for rc in out)
|
||
# Phase 119: the name-hit representative rows are flagged, the plain
|
||
# FTS row is not (the selection tier's bonus input, task 02).
|
||
assert out[0].name_hit is True
|
||
assert out[1].name_hit is True
|
||
assert out[2].name_hit is False
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 119, D1 — the name_hit flag through fusion
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def test_fuse_keeps_name_hit_on_lexical_only_hit() -> None:
|
||
"""A name-hit row with no vector rank keeps ``name_hit=True``
|
||
through the fusion (the ``replace()`` copy carries the field)."""
|
||
nh = _rc("gitea/README.md")
|
||
nh.name_hit = True
|
||
out = fuse([], [nh], k=60)
|
||
assert len(out) == 1
|
||
assert out[0].name_hit is True
|
||
assert out[0].fts_hit is True
|
||
assert out[0].cosine == 0.0
|
||
|
||
|
||
def test_fuse_or_s_name_hit_on_double_hit() -> None:
|
||
"""A vector row that is ALSO the name hit's representative chunk
|
||
(the RRF merge dedupes by chunk id) keeps ``name_hit=True`` — the
|
||
merge ORs the flag in, so the selection tier (task 02) still sees
|
||
the name hit on the fused list."""
|
||
v = _rc("gitea/README.md", cosine=0.9)
|
||
l1 = _rc("gitea/README.md", cosine=0.1) # the lexical copy of the same chunk
|
||
l1.chunk_id = v.chunk_id
|
||
l1.name_hit = True
|
||
out = fuse([v], [l1], k=60)
|
||
assert len(out) == 1
|
||
assert out[0].name_hit is True
|
||
assert out[0].fts_hit is True
|
||
assert out[0].score == pytest.approx(2 / 61)
|
||
|
||
|
||
def test_fuse_default_name_hit_stays_false_for_ordinary_rows() -> None:
|
||
"""Neither list flagged ⇒ fusion never invents a name-hit flag —
|
||
ordinary vector and FTS rows are ``name_hit=False``."""
|
||
out = fuse([_rc("a.md", cosine=0.8)], [_rc("b.md", fts_hit=True)], k=60)
|
||
assert len(out) == 2
|
||
assert all(rc.name_hit is False for rc in out)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Phase 119, D2 — the bounded name-hit bonus (LOCKED A3)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
from app.rag.retriever import _selection_order, weak_hit_titles # noqa: E402
|
||
|
||
|
||
def _bonus_rc(
|
||
doc: Document,
|
||
score: float,
|
||
cosine: float,
|
||
position: int = 0,
|
||
name_hit: bool = False,
|
||
) -> RetrievedChunk:
|
||
"""One fused-list candidate (name-hit rows follow the D1 lexical
|
||
convention: ``cosine=0.0``, ``fts_hit=True``)."""
|
||
return RetrievedChunk(
|
||
chunk_id=uuid.uuid4(),
|
||
position=position,
|
||
content=doc.content[:20],
|
||
score=score,
|
||
document=doc,
|
||
cosine=cosine,
|
||
fts_hit=cosine == 0.0,
|
||
name_hit=name_hit,
|
||
)
|
||
|
||
|
||
def _bonus_chunks() -> list[RetrievedChunk]:
|
||
"""A mixed FUSED list — already in the ``fuse()`` key order
|
||
(−score, −cosine, path, position) — with one name-hit document
|
||
(``gitea/README.md``, the D1 convention: cosine 0.0) and ordinary
|
||
vector/FTS documents: ``a.md`` carries two chunks, and ``b.md`` /
|
||
``c.md`` tie on the fused score (separated only by cosine).
|
||
|
||
The pre-phase (bonus-0) document order this list walks — the golden
|
||
the kill switch must reproduce — is a (0.0200) → gitea (0.0160) →
|
||
b (0.0150, cos 0.7) → c (0.0150, cos 0.6) → d (0.0100).
|
||
"""
|
||
gitea = _doc("gitea/README.md", "G" * 50)
|
||
a = _doc("a.md", "A" * 50)
|
||
b = _doc("b.md", "B" * 50)
|
||
c = _doc("c.md", "C" * 50)
|
||
d = _doc("d.md", "D" * 50)
|
||
return [
|
||
_bonus_rc(a, 0.0200, 0.9, 0),
|
||
_bonus_rc(gitea, 0.0160, 0.0, 0, name_hit=True),
|
||
_bonus_rc(b, 0.0150, 0.7, 0),
|
||
_bonus_rc(c, 0.0150, 0.6, 0),
|
||
_bonus_rc(a, 0.0120, 0.5, 1),
|
||
_bonus_rc(d, 0.0100, 0.1, 0),
|
||
]
|
||
|
||
|
||
#: The golden document order the OLD pre-phase loop (stable score-
|
||
#: descending walk, first-seen-chunk dedupe) produces over
|
||
#: :func:`_bonus_chunks` — pinned byte-identical by the kill switch.
|
||
GOLDEN_PRE_PHASE_ORDER = ["a.md", "gitea/README.md", "b.md", "c.md", "d.md"]
|
||
|
||
|
||
def test_selection_order_bonus_zero_is_the_pre_phase_golden_walk() -> None:
|
||
"""LOCKED A3 kill switch: ``bonus=0`` returns the EXACT pre-phase
|
||
document order of the old score-descending first-seen walk — the
|
||
golden list pinned from the old loop over the mixed fused list
|
||
(incl. the b/c fused-score tie resolved by the input order the
|
||
fusion produced — the walk never re-sorts it away)."""
|
||
out = _selection_order(_bonus_chunks(), 0.0)
|
||
assert [d.path for d, _eff, _cos, _idx in out] == GOLDEN_PRE_PHASE_ORDER
|
||
# The re-rank inputs are exposed and exact: effective == best fused
|
||
# score (no bonus), best cosine tracked across a doc's chunks (a: 0.9
|
||
# from its rank-1 chunk, not 0.5), first-seen index in the
|
||
# score-descending walk.
|
||
assert [eff for _d, eff, _cos, _idx in out] == [
|
||
0.0200, 0.0160, 0.0150, 0.0150, 0.0100
|
||
]
|
||
assert [cos for _d, _eff, cos, _idx in out] == [
|
||
pytest.approx(v) for v in (0.9, 0.0, 0.7, 0.6, 0.1)
|
||
]
|
||
assert [idx for _d, _eff, _cos, idx in out] == [0, 1, 2, 3, 5]
|
||
|
||
|
||
def test_selection_order_bonus_inert_without_name_hits() -> None:
|
||
"""No name-hit chunk present → the bonus cannot fire: the order is
|
||
IDENTICAL to the pre-phase walk even with the default bonus on
|
||
(LOCKED A3)."""
|
||
chunks = _bonus_chunks()
|
||
for rc in chunks:
|
||
rc.name_hit = False
|
||
out = _selection_order(chunks, 0.005)
|
||
assert [d.path for d, _eff, _cos, _idx in out] == GOLDEN_PRE_PHASE_ORDER
|
||
|
||
|
||
def test_selection_order_bonus_lifts_name_hit_doc_below_bonus_gap() -> None:
|
||
"""The name-hit doc's effective 0.016 + 0.005 = 0.021 EXCEEDS a's
|
||
0.020 — a gap of 0.004 < bonus 0.005 — so the bonus lifts it to
|
||
rank 1; the rest keep their fused order (the bonus re-ranks, it
|
||
does not inflate)."""
|
||
out = _selection_order(_bonus_chunks(), 0.005)
|
||
assert [d.path for d, _eff, _cos, _idx in out] == [
|
||
"gitea/README.md", "a.md", "b.md", "c.md", "d.md"
|
||
]
|
||
assert out[0][1] == pytest.approx(0.016 + 0.005)
|
||
|
||
|
||
def test_selection_order_bonus_does_not_lift_above_bonus_gap() -> None:
|
||
"""A gap LARGER than the bonus is not closed: the name-hit doc's
|
||
best 0.010 + 0.005 = 0.015 ties b/c on effective and LOSES to both
|
||
on the (−effective, −best_cosine) tie-break (its D1 cosine is 0.0) —
|
||
a keeps the lead (0.020). A second name-hit doc (``e.md``, also
|
||
0.010/cos 0.0) trails gitea on the ``document.path`` tie-break —
|
||
the full re-rank key pinned."""
|
||
chunks = _bonus_chunks()
|
||
chunks[1].score = 0.010 # the name-hit doc drops to a 0.010 best
|
||
e = _doc("e.md", "E" * 50)
|
||
chunks.insert(2, _bonus_rc(e, 0.010, 0.0, 0, name_hit=True))
|
||
out = _selection_order(chunks, 0.005)
|
||
assert [d.path for d, _eff, _cos, _idx in out] == [
|
||
"a.md", "b.md", "c.md", "e.md", "gitea/README.md", "d.md"
|
||
]
|
||
|
||
|
||
def test_selection_order_first_seen_breaks_equal_path_ties() -> None:
|
||
"""Two documents sharing a path across sources (``notes.md`` in two
|
||
sources) can tie on (effective, cosine, path) — the pre-bonus
|
||
first-seen rank decides (the last key element)."""
|
||
s1 = _doc("notes.md", "X" * 50, source="Src1")
|
||
s2 = _doc("notes.md", "Y" * 50, source="Src2")
|
||
chunks = [
|
||
_bonus_rc(s1, 0.016, 0.0, 0, name_hit=True),
|
||
_bonus_rc(s2, 0.016, 0.0, 0, name_hit=True),
|
||
]
|
||
out = _selection_order(chunks, 0.005)
|
||
assert [d.source for d, *_ in out] == ["Src1", "Src2"]
|
||
|
||
|
||
def test_selection_order_bonus_applied_once_per_document() -> None:
|
||
"""The bonus is per DOCUMENT — applied ONCE no matter how many of
|
||
the doc's chunks are name hits (3×bonus would push the name-hit doc
|
||
above the 0.030 leader; one bonus cannot)."""
|
||
gitea = _doc("gitea/README.md", "G" * 50)
|
||
a = _doc("a.md", "A" * 50)
|
||
chunks = [
|
||
_bonus_rc(a, 0.030, 0.8),
|
||
_bonus_rc(gitea, 0.016, 0.0, 0, name_hit=True),
|
||
_bonus_rc(gitea, 0.010, 0.0, 1, name_hit=True),
|
||
_bonus_rc(gitea, 0.008, 0.0, 2, name_hit=True),
|
||
]
|
||
out = _selection_order(chunks, 0.005)
|
||
assert [d.path for d, _eff, _cos, _idx in out] == ["a.md", "gitea/README.md"]
|
||
assert out[1][1] == pytest.approx(0.016 + 0.005) # best + ONE bonus
|
||
|
||
|
||
def test_selection_order_bonus_fires_when_name_hit_is_not_first_chunk() -> None:
|
||
"""The bonus fires on ANY name-hit chunk of the document — including
|
||
when the doc's first-seen (best) chunk is an ordinary vector row and
|
||
only a lower-ranked chunk is the D1 name-hit representative (a
|
||
document's name hit and its best chunk can be different chunks).
|
||
The bonus still lands on the doc's BEST fused score, and the doc's
|
||
best cosine stays tracked across ALL its chunks."""
|
||
gitea = _doc("gitea/README.md", "G" * 50)
|
||
a = _doc("a.md", "A" * 50)
|
||
chunks = [
|
||
_bonus_rc(a, 0.024, 0.8),
|
||
_bonus_rc(gitea, 0.020, 0.2, 0), # the doc's best — an ordinary chunk
|
||
_bonus_rc(gitea, 0.016, 0.55, 2, name_hit=True), # the name-hit rep
|
||
]
|
||
assert [d.path for d, *_ in _selection_order(chunks, 0.0)] == [
|
||
"a.md", "gitea/README.md"
|
||
]
|
||
out = _selection_order(chunks, 0.005)
|
||
assert [d.path for d, *_ in out] == ["gitea/README.md", "a.md"]
|
||
assert out[0][1] == pytest.approx(0.020 + 0.005) # bonus on the BEST score
|
||
# The doc's best cosine is tracked across ALL its chunks — the
|
||
# lower-ranked name-hit chunk (0.55) beats the first-seen chunk's
|
||
# 0.2 (the re-rank tie-break input).
|
||
assert out[0][2] == pytest.approx(0.55)
|
||
|
||
|
||
def test_suggested_bonus_default_from_settings_and_kill_switch(
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
"""The *bonus* parameter defaults to the LIVE ``BOR_NAME_HIT_BONUS``
|
||
setting (the ``n`` parameter's settings-read pattern — the default
|
||
0.005 is the production value, not a frozen constant); an explicit
|
||
``bonus=0`` and a settings kill switch both reproduce the pre-phase
|
||
golden walk (LOCKED A3)."""
|
||
chunks = _bonus_chunks()
|
||
settings = Settings(_env_file=None) # pyright: ignore[reportCallIssue]
|
||
assert settings.name_hit_bonus == 0.005 # the production default
|
||
monkeypatch.setattr(retriever, "get_settings", lambda: settings)
|
||
assert [d.path for d in select_suggested(chunks, n=5)] == [
|
||
"gitea/README.md", "a.md", "b.md", "c.md", "d.md"
|
||
]
|
||
# Explicit kill switch: the byte-identical pre-phase order.
|
||
assert [d.path for d in select_suggested(chunks, n=5, bonus=0.0)] == GOLDEN_PRE_PHASE_ORDER
|
||
# Settings kill switch (BOR_NAME_HIT_BONUS=0) — the same golden walk.
|
||
off = Settings(_env_file=None, name_hit_bonus=0.0) # pyright: ignore[reportCallIssue]
|
||
monkeypatch.setattr(retriever, "get_settings", lambda: off)
|
||
assert [d.path for d in select_suggested(chunks, n=5)] == GOLDEN_PRE_PHASE_ORDER
|
||
|
||
|
||
def test_related_skips_excluded_ids_under_the_bonus() -> None:
|
||
"""Exclusion is orthogonal to the bonus: excluded ids are skipped
|
||
exactly as before, on the bonus-adjusted walk — an excluded doc
|
||
never rides the related row even when the bonus would lift it to
|
||
the lead."""
|
||
chunks = _bonus_chunks()
|
||
b_id = chunks[2].document.id
|
||
d_id = chunks[5].document.id
|
||
out = select_related(chunks, {b_id, d_id}, cap=5, bonus=0.005)
|
||
assert [d.path for d in out] == ["gitea/README.md", "a.md", "c.md"]
|
||
# Kill switch: the same exclusions on the pre-phase walk.
|
||
out0 = select_related(chunks, {b_id, d_id}, cap=5, bonus=0.0)
|
||
assert [d.path for d in out0] == ["a.md", "gitea/README.md", "c.md"]
|
||
# The name-hit doc itself excluded → the lead goes to the next doc.
|
||
g_id = chunks[1].document.id
|
||
out2 = select_related(chunks, {g_id}, cap=5, bonus=0.005)
|
||
assert [d.path for d in out2][0] == "a.md"
|
||
|
||
|
||
def test_weak_hit_titles_bonus_adjusted_order() -> None:
|
||
"""Titles follow the bonus-adjusted selection walk (``_doc`` titles
|
||
equal paths here, so the title list mirrors the doc order); the
|
||
kill switch returns the pre-phase golden order."""
|
||
chunks = _bonus_chunks()
|
||
assert weak_hit_titles(chunks, bonus=0.005) == [
|
||
"gitea/README.md", "a.md", "b.md", "c.md", "d.md"
|
||
]
|
||
assert weak_hit_titles(chunks, bonus=0.0) == GOLDEN_PRE_PHASE_ORDER
|
||
|
||
|
||
def test_bonus_lives_in_the_selection_layer_only() -> None:
|
||
"""LOCKED A3: the bonus never touches the chunk objects —
|
||
``score``/``cosine``/``fts_hit`` (the A8 gate's inputs —
|
||
``query_log.top_score`` is the best fused chunk score, the same
|
||
values) are unchanged after every selection walk, even with the
|
||
bonus lifting a document."""
|
||
chunks = _bonus_chunks()
|
||
before = {
|
||
rc.chunk_id: (rc.score, rc.cosine, rc.fts_hit, rc.name_hit) for rc in chunks
|
||
}
|
||
select_suggested(chunks, n=5, bonus=0.005)
|
||
select_related(chunks, set(), cap=5, bonus=0.005)
|
||
weak_hit_titles(chunks, bonus=0.005)
|
||
after = {
|
||
rc.chunk_id: (rc.score, rc.cosine, rc.fts_hit, rc.name_hit) for rc in chunks
|
||
}
|
||
assert before == after
|