phase: 162_search_recall_small_sources
**Phase 162 — final verification pass (all tasks already complete; verified + docs pass)** - Verified: `_fold` (NFKD+casefold, stdlib) inside `_normalize_name` — `BESTÅ`→`besta` question side, `BÅsta.md`→`basta` path side; A2 match classes/ranking/caps/bonus untouched; FTS/cosine/A8 untouched. - Verified: `grep_document` folds both sides via the imported `_fold` (no second copy); regex-teaching lines byte-identical; 3 copy sites updated. - Verified: `regenerate_overview` appends the deterministic `Sources: <name> (<k> doc|docs)` tail (happy path only; zero-doc + `LLMError` rows byte-identical; `build_overview_prompt`/`load_kb_overview`/`build_kb_section` untouched). - Verified: `TOOLS_SECTION` grep sentence updated + D4 search-discipline sentence present **verbatim**, immediately after the grep sentence; `PERSONA`/LOW pins byte-identical (`test_prompt_lock.py`). - Measured evidence (in `.agents/reports/162_…/task04_prefix_evidence_and_battery.md` + re-measured every run by `test_under_rank_is_measured_not_assumed`): pre-fix tokens `['build','best']` → IKEA doc rank 6 (outside top-5); post-fix `['build','besta']` → suggested #1, cosine unchanged 0.382. - `uv run pytest tests/e2e/test_search_recall_small_sources.py -v --no-cov` → **7 passed** (isolated); neighbours `test_kb_overview{,_freshness} test_name_signal_read_chips test_grep_regex_teaching test_harness_aligned_tools test_agent_document_tools test_tool_call_compact` → **24 passed**. - `uv run pytest --cov=app --cov-report=term-missing` → **3756 passed**, app coverage **99%** (>90%). - `uv run ruff check .` → All checks passed; `uv run pyright` → **0 errors**. - Completion criteria: fold both ways ✅ · `Sources:` tail ✅ · TOOLS_SECTION + D4 verbatim + re-cut pins ✅ · E2E green/battery recorded/full suite/coverage/lint ✅ · docs pass ✅ · commit + dir move = harness. - Battery: 5 phase-119 questions byte-identical to the phase-119 record (GATE PASS); the 2 Besta questions **skipped loudly** — verified the 2026-09-05 replica dump has zero `ikea` matches (`grep -c -i ikea data/bor_live_kb_replica.dump.sql` = 0); rank gate stays with the owner's live KB. - Docs fixed: README — overview-tail doc + `BOR_KB_OVERVIEW_MAX_CHARS` tail-cut constraint, the phase-162 search-discipline paragraph, per-phase (not per-story) E2E wording, new "Retrieval regression battery" section; AGENTS.md — rule 4 per-phase wording + battery command in Quick reference. (The overview's expected "AGENTS.md §6 tool table" does not exist in this repo — the grep contract line lives in README's tool table, already updated.) - Deviations (recorded, not assumed): `_fold` adds `casefold` (required for the pinned `ß → ss`); `①` NFKD-decomposes to `1` (the overview's parenthetical was wrong) and is excluded by `NAME_TOKEN_MIN_LEN`; pre-fix evidence lives in the reports dir because task files are read-only for this pass. - Owner follow-up A6 (runtime, not code): add the Tuning-tab steering note "IKEA's BESTÅ assembly PDF is a live source — search it as `BESTÅ` or `AA-1272068`". - Next pending phase: none — 162 is the only entry in `todo/` (harness moves it to `complete/`).
This commit is contained in:
+144
-6
@@ -5,7 +5,11 @@ per-document user lines (source/path/title/first summary line), and the
|
||||
input cap with the shared ``[…truncated…]`` marker. The loader and
|
||||
regenerator tests run against the local compose Postgres (preferred —
|
||||
real single-row upsert), skipping with clear instructions when the stack
|
||||
is not up — same pattern as ``test_importer.py``.
|
||||
is not up — same pattern as ``test_importer.py``. The phase-162 section
|
||||
pins the deterministic ``Sources: …`` inventory tail (LOCKED A2): the
|
||||
format/order/counts, the 1-doc survival, the happy-path stored content,
|
||||
the byte-identical zero-doc / ``LLMError`` paths, and the byte-identical
|
||||
model input.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,7 +19,7 @@ import uuid
|
||||
from datetime import UTC, datetime, timedelta
|
||||
|
||||
import pytest
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy import select, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.config import Settings, get_settings
|
||||
@@ -25,6 +29,7 @@ from app.rag.overview import (
|
||||
KB_OVERVIEW_MODE,
|
||||
OVERVIEW_INSTRUCTION,
|
||||
SYSTEM_PROMPT,
|
||||
_source_inventory,
|
||||
build_overview_prompt,
|
||||
load_kb_overview,
|
||||
regenerate_overview,
|
||||
@@ -302,11 +307,13 @@ def test_regenerate_happy_path_creates_single_row_id_1(
|
||||
row = db.get(KbOverview, 1)
|
||||
assert row is not None, "the single row must be upserted"
|
||||
assert row.id == 1
|
||||
assert row.content == REPLY
|
||||
# Phase 162 (LOCKED A2): the stored content carries the
|
||||
# deterministic source-inventory tail after the model text.
|
||||
assert row.content == REPLY + "\nSources: Homelab (2 docs)"
|
||||
assert row.updated_at is not None
|
||||
age = datetime.now(UTC) - row.updated_at
|
||||
assert age.total_seconds() < 300, "updated_at must be a fresh UTC timestamp"
|
||||
assert f"overview: regenerated docs=2 chars={len(REPLY)}" in caplog.text
|
||||
assert f"overview: regenerated docs=2 chars={len(row.content)}" in caplog.text
|
||||
finally:
|
||||
_truncate_documents(db)
|
||||
|
||||
@@ -324,7 +331,7 @@ def test_regenerate_updates_existing_row_in_place(db: Session, clean_overview) -
|
||||
assert count == 1, "still exactly one row after the re-generation"
|
||||
row = db.get(KbOverview, 1)
|
||||
assert row is not None
|
||||
assert row.content == REPLY # replaced, not appended
|
||||
assert row.content == REPLY + "\nSources: Homelab (1 doc)" # replaced, not appended
|
||||
assert row.updated_at is not None and row.updated_at > past
|
||||
finally:
|
||||
_truncate_documents(db)
|
||||
@@ -511,4 +518,135 @@ def test_regenerate_opens_own_session_when_none(db: Session, clean_overview) ->
|
||||
|
||||
assert ok is True
|
||||
row = db.get(KbOverview, 1) # fresh lookup via the test session
|
||||
assert row is not None and row.content == REPLY
|
||||
assert row is not None and row.content == REPLY + "\nSources: Homelab (1 doc)"
|
||||
|
||||
|
||||
# ---------- phase 162: the deterministic source inventory (LOCKED A2) -------
|
||||
|
||||
|
||||
def test_source_inventory_format_counts_and_catalog_order() -> None:
|
||||
"""Exact format, singular/plural, and catalog order: one entry per
|
||||
DISTINCT source — ``<name> (<k> doc|docs)`` joined with ``, ``
|
||||
behind ``Sources: ``, ordered ``source, path`` (the scrambled
|
||||
input rows prove the catalog order is the OUTPUT order, not the
|
||||
recency-first order of the rows the generator reads)."""
|
||||
rows = [
|
||||
("b", "x.md", "X", None),
|
||||
("a", "y.md", "Y", None),
|
||||
("c", "p1.md", "P1", None),
|
||||
("b", "z.md", "Z", None),
|
||||
("c", "p2.md", "P2", None),
|
||||
("c", "p3.md", "P3", None),
|
||||
]
|
||||
assert _source_inventory(rows) == "Sources: a (1 doc), b (2 docs), c (3 docs)"
|
||||
|
||||
|
||||
def test_source_inventory_keeps_a_one_doc_source_visible() -> None:
|
||||
"""The 2026-10-07 ``ikea_instructions`` case: a 1-doc source among
|
||||
many appears with ``(1 doc)`` — no source is invisible to
|
||||
``<knowledge_base>``, whatever the ``lite`` outline says."""
|
||||
rows = [
|
||||
("gufo", "a.md", "A", None),
|
||||
(
|
||||
"ikea_instructions",
|
||||
"besta-frame-white__AA-1272068-11-100.md",
|
||||
"Assembly Instructions",
|
||||
None,
|
||||
),
|
||||
("gufo", "b.md", "B", None),
|
||||
("players_handbook", "core.md", "Core", None),
|
||||
]
|
||||
assert _source_inventory(rows) == (
|
||||
"Sources: gufo (2 docs), ikea_instructions (1 doc), "
|
||||
"players_handbook (1 doc)"
|
||||
)
|
||||
|
||||
|
||||
def test_regenerate_appends_the_inventory_tail_to_the_stored_content(
|
||||
db: Session, clean_overview, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
"""Happy path, exact pin on the fake's fixed output: the stored row
|
||||
= model text (trailing newlines rstripped) + ``\\n`` + the inventory
|
||||
line; the log's chars = the FULL stored content, tail included."""
|
||||
_truncate_documents(db)
|
||||
try:
|
||||
_add_doc(db, "players_handbook", "rpg.md", "RPG")
|
||||
_add_doc(db, "Homelab", "b.md", "B")
|
||||
_add_doc(db, "Homelab", "a.md", "A")
|
||||
llm = _FakeLLM(reply=REPLY + "\n\n")
|
||||
expected = REPLY + "\nSources: Homelab (2 docs), players_handbook (1 doc)"
|
||||
with caplog.at_level(logging.INFO, logger="app.rag.overview"):
|
||||
ok = asyncio.run(regenerate_overview(llm, db))
|
||||
assert ok is True
|
||||
row = db.get(KbOverview, 1)
|
||||
assert row is not None
|
||||
assert row.content == expected # no blank line before the tail
|
||||
assert f"overview: regenerated docs=3 chars={len(expected)}" in caplog.text
|
||||
finally:
|
||||
_truncate_documents(db)
|
||||
|
||||
|
||||
def test_regenerate_model_input_never_gains_the_inventory_tail(
|
||||
db: Session, clean_overview
|
||||
) -> None:
|
||||
"""The model input is byte-identical to the pre-phase prompt: the
|
||||
user message the fake received equals ``build_overview_prompt`` for
|
||||
the same catalogue rows — the tail is never sent to ``lite``."""
|
||||
_truncate_documents(db)
|
||||
try:
|
||||
_add_doc(db, "Homelab", "a.md", "A", "A summary.\nSource: Homelab/a.md")
|
||||
_add_doc(db, "Deployments", "b.yaml", "B")
|
||||
llm = _FakeLLM()
|
||||
assert asyncio.run(regenerate_overview(llm, db)) is True
|
||||
rows = db.execute(
|
||||
select(Document.source, Document.path, Document.title, Document.summary)
|
||||
.order_by(Document.created_at.desc(), Document.source, Document.path)
|
||||
).all()
|
||||
finally:
|
||||
_truncate_documents(db)
|
||||
|
||||
user = next(m["content"] for m in llm.messages if m["role"] == "user")
|
||||
_, expected_user = build_overview_prompt([tuple(r) for r in rows])
|
||||
assert user == expected_user
|
||||
assert "Sources:" not in user
|
||||
|
||||
|
||||
def test_regenerate_zero_docs_keeps_tailless_pre_phase_row(
|
||||
db: Session, clean_overview
|
||||
) -> None:
|
||||
"""Zero-doc early return: byte-identical pre-phase behavior — the
|
||||
pre-phase (tail-less) row survives untouched, no tail is invented."""
|
||||
db.execute(text("DELETE FROM kb_overview"))
|
||||
pre_phase_row = "- Old outline\n- from before phase 162"
|
||||
db.add(KbOverview(id=1, content=pre_phase_row))
|
||||
db.commit()
|
||||
_truncate_documents(db)
|
||||
llm = _FakeLLM()
|
||||
ok = asyncio.run(regenerate_overview(llm, db))
|
||||
assert ok is False
|
||||
assert llm.calls == 0
|
||||
row = db.get(KbOverview, 1)
|
||||
assert row is not None and row.content == pre_phase_row
|
||||
assert "Sources:" not in row.content
|
||||
|
||||
|
||||
def test_regenerate_llm_error_preserves_an_existing_inventory_tail(
|
||||
db: Session, clean_overview
|
||||
) -> None:
|
||||
"""The tail is CONTENT: a previous row that already carried a
|
||||
``Sources: …`` tail survives an ``LLMError`` byte-identically —
|
||||
including the tail (an old outline is better than none)."""
|
||||
db.execute(text("DELETE FROM kb_overview"))
|
||||
previous = "- Old outline\nSources: Homelab (3 docs)"
|
||||
db.add(KbOverview(id=1, content=previous))
|
||||
db.commit()
|
||||
_truncate_documents(db)
|
||||
try:
|
||||
_add_doc(db, "Homelab", "a.md", "A")
|
||||
llm = _FakeLLM(fail=LLMError("simulated lite-model failure"))
|
||||
ok = asyncio.run(regenerate_overview(llm, db))
|
||||
assert ok is False
|
||||
row = db.get(KbOverview, 1)
|
||||
assert row is not None and row.content == previous
|
||||
finally:
|
||||
_truncate_documents(db)
|
||||
|
||||
Reference in New Issue
Block a user