phase: 94_ls_tree_drilldown
Build and Push Containers / build-and-push-app (push) Successful in 1m45s
Build and Push Containers / build-and-push-db (push) Successful in 25s

All green. Verification complete.

**Phase 94 — `ls` drill-down tree: final verification pass (all 5 tasks were already complete; verified, nothing to fix)**

- Verified `ls` 3-level tree (`app/rag/agent.py`): `ls()` sources + summaries, `ls(source)`/`ls(source/folder)` drill-down, 50-line file cap + grep-pointer note, NOT-A-FOLDER teaching refusal
- Verified `folder_summaries` (migration 0017, model, `app/rag/folder_summaries.py` generator: `FOLDER_SUMMARY_MODE` marker, fail-soft per folder, ≥2-doc scope + prune) wired change-gated in both sync paths
- Verified 10-turn fixture battery verdict recorded in `TOOL_CALLING_TESTING.md` §9 (2026-09-11): turbo PASS 19/19 contract, 98.7 s (−12.5…−13.2 % vs baseline); lite PASS 18/18, 43.6 s (+7.7 %) — accuracy at/above baseline, gate met
- `uv run pytest --cov=app --cov-report=term-missing` → 1939 passed, 0 failed; TOTAL coverage **99 %** (folder_summaries.py 100 %)
- `uv run ruff check .` → clean; `uv run pyright` → 0 errors, 0 warnings
- E2E in isolation: `test_ls_tree_drilldown.py` 3 passed; `test_agent_document_tools` 4, `test_agent_unlimited_tools` 4, `test_harness_aligned_tools` 3, `test_search_tool` 3, `test_grep_regex_teaching` 2, `test_response_to_docs` 4 — all passed (read/grep contracts untouched)
- Dedicated folder-summary tests (fail-soft, prune, both sync paths, migration): 46 passed
- Completion criteria: all 6 met; working tree holds only phase-94 changes (commit left to harness per protocol)

**Next pending phase:** `95_read_truncation_cap`
This commit is contained in:
2026-09-11 00:59:35 -04:00
parent 9188be259b
commit d4943b4822
61 changed files with 6289 additions and 666 deletions
+61 -20
View File
@@ -23,6 +23,15 @@ and unchanged re-runs never bump (``sources_version=skipped``), and a
failed ``lite`` never rolls the bump back. The counter is pinned to the
migration-0010 seed (0) around every test by
:func:`_reset_sources_version`.
Phase 94 (task 02, line-extension house rule): the summary line now
ends with the folder-summary stats —
``folder_summaries=<generated>/<failed>/<pruned>`` when the gate fired
(this fixture's 2-doc source holds exactly ONE qualifying subtree: the
source root) or ``folder_summaries=skipped`` otherwise — so the line
pinned here gains that token, and a KB-changing run burns exactly ONE
extra ``lite`` call (the source-root folder summary, markdown files
never get a document summary).
"""
from __future__ import annotations
@@ -98,10 +107,10 @@ def src(tmp_path: Path) -> Path:
@pytest.fixture(autouse=True)
def _clean_kb(db: Session) -> Iterator[None]:
db.execute(text("TRUNCATE chunks, documents, kb_overview"))
db.execute(text("TRUNCATE chunks, documents, kb_overview, folder_summaries"))
db.commit()
yield
db.execute(text("TRUNCATE chunks, documents, kb_overview"))
db.execute(text("TRUNCATE chunks, documents, kb_overview, folder_summaries"))
db.commit()
@@ -149,16 +158,24 @@ def test_changed_import_writes_overview_row(
assert rc == 0
assert "added=2" in out
assert out.rstrip().endswith("overview=updated sources_version=1")
# Phase 94: the line gains the folder-stats token — the 2-doc source
# holds one qualifying subtree (the source root): 1 generated.
assert out.rstrip().endswith(
"overview=updated sources_version=1 folder_summaries=1/0/0"
)
assert _version(db) == 1 # phase 53: a changed import bumps exactly once
# Exactly one lite call — the overview itself (markdown files never
# get a summary, so nothing else may touch ``chat``).
assert len(llm.chat_calls) == 1
# Exactly two lite calls — the overview + the source-root folder
# summary (markdown files never get a document summary, so nothing
# else may touch ``chat``).
assert len(llm.chat_calls) == 2
by_role = {m["role"]: m["content"] for m in llm.chat_calls[0]}
assert "KB_OVERVIEW_MODE" in by_role["system"]
# One line per doc: source — path — title (no summary for markdown).
assert "MyDocs — alpha.md — Alpha" in by_role["user"]
assert "MyDocs — beta.md — Beta" in by_role["user"]
by_role = {m["role"]: m["content"] for m in llm.chat_calls[1]}
assert "FOLDER_SUMMARY_MODE" in by_role["system"]
assert by_role["user"].splitlines()[0] == "Folder: MyDocs"
# The model's outline lands in the single row.
row = _row(db)
assert row is not None
@@ -177,16 +194,20 @@ def test_unchanged_reimport_does_not_call_lite(
llm = FakeEmbedder()
rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys)
assert rc == 0
assert out.rstrip().endswith("overview=updated sources_version=1")
assert len(llm.chat_calls) == 1
assert out.rstrip().endswith(
"overview=updated sources_version=1 folder_summaries=1/0/0"
)
assert len(llm.chat_calls) == 2 # overview + source-root folder summary
assert _row(db) is not None
# Same hashes → no KB change → no lite call, previous outline kept.
rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys)
assert rc == 0
assert "unchanged=2" in out
assert out.rstrip().endswith("overview=skipped sources_version=skipped")
assert len(llm.chat_calls) == 1 # no new lite call
assert out.rstrip().endswith(
"overview=skipped sources_version=skipped folder_summaries=skipped"
)
assert len(llm.chat_calls) == 2 # no new lite call
row = _row(db)
assert row is not None and row.content == "Summary of MyDocs"
assert _version(db) == 1 # phase 53: an unchanged re-run never bumps
@@ -201,7 +222,9 @@ def test_lite_failure_is_fail_soft(
good = FakeEmbedder()
rc, out = _run_main(monkeypatch, good, ["--source", str(src)], capsys)
assert rc == 0
assert out.rstrip().endswith("overview=updated sources_version=1")
assert out.rstrip().endswith(
"overview=updated sources_version=1 folder_summaries=1/0/0"
)
previous = _row(db)
assert previous is not None
previous_content = previous.content
@@ -213,8 +236,12 @@ def test_lite_failure_is_fail_soft(
rc, out = _run_main(monkeypatch, bad, ["--source", str(src)], capsys)
assert rc == 0 # a failed outline must not fail the import
assert "updated=1" in out
assert out.rstrip().endswith("overview=failed sources_version=2")
assert len(bad.chat_calls) == 1 # the (failed) attempt was made
# Phase 94: the folder batch fails too (per-folder fail-soft) — the
# failed attempt counts into the stats, the previous row stays.
assert out.rstrip().endswith(
"overview=failed sources_version=2 folder_summaries=0/1/0"
)
assert len(bad.chat_calls) == 2 # the (failed) attempts were made
row = _row(db)
assert row is not None
assert row.content == previous_content # previous row untouched
@@ -232,8 +259,10 @@ def test_limit_run_skips_overview(
llm = FakeEmbedder()
rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys)
assert rc == 0
assert out.rstrip().endswith("overview=updated sources_version=1")
assert len(llm.chat_calls) == 1
assert out.rstrip().endswith(
"overview=updated sources_version=1 folder_summaries=1/0/0"
)
assert len(llm.chat_calls) == 2
# An incomplete walk must not rewrite the outline (mirrors the
# --prune-with---limit guard) — and must not advance the version.
@@ -241,8 +270,10 @@ def test_limit_run_skips_overview(
rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--limit", "1"], capsys)
assert rc == 0
assert "updated=1" in out
assert out.rstrip().endswith("overview=skipped sources_version=skipped")
assert len(llm.chat_calls) == 1 # --limit never burns a lite call
assert out.rstrip().endswith(
"overview=skipped sources_version=skipped folder_summaries=skipped"
)
assert len(llm.chat_calls) == 2 # --limit never burns a lite call
row = _row(db)
assert row is not None and row.content == "Summary of MyDocs"
assert _version(db) == 1 # phase 53: --limit debug runs never bump
@@ -262,7 +293,9 @@ def test_empty_source_without_row_creates_nothing(
assert rc == 0
assert "files=0" in out
assert out.rstrip().endswith("overview=skipped sources_version=skipped")
assert out.rstrip().endswith(
"overview=skipped sources_version=skipped folder_summaries=skipped"
)
assert llm.chat_calls == [] # no KB → no outline, no wasted model call
assert _row(db) is None # nothing created
assert _version(db) == 0 # nothing changed → nothing bumped
@@ -283,7 +316,9 @@ def test_prune_only_run_bumps_sources_version(
rc, out = _run_main(monkeypatch, llm, ["--source", str(src)], capsys)
assert rc == 0
assert "added=2" in out
assert out.rstrip().endswith("overview=updated sources_version=1")
assert out.rstrip().endswith(
"overview=updated sources_version=1 folder_summaries=1/0/0"
)
assert _version(db) == 1
# Delete one file; a --prune run drops exactly it: no add/update,
@@ -292,6 +327,12 @@ def test_prune_only_run_bumps_sources_version(
rc, out = _run_main(monkeypatch, llm, ["--source", str(src), "--prune"], capsys)
assert rc == 0
assert "pruned=1" in out
assert out.rstrip().endswith("overview=skipped sources_version=2")
# Phase 94: the folder gate is the overview's (added + updated > 0
# or empty table) — a prune-only re-walk with a populated table
# skips generation (the remaining 1-doc source stays summarized by
# its existing root row, which still describes it).
assert out.rstrip().endswith(
"overview=skipped sources_version=2 folder_summaries=skipped"
)
assert _version(db) == 2 # the prune-only change bumped exactly once
assert _row(db) is not None # the outline row is untouched