phase: 106_document_dates
Build and Push Containers / build-and-push-app (push) Successful in 4m35s
Build and Push Containers / build-and-push-db (push) Successful in 14s

Everything is verified green. Final report:

**Phase 106 — Document dates (final verification pass; all 10 tasks already complete)**

- Verified all phase artifacts: alembic `0020` (dev DB at `0020`), `doc_dates.py`, git `file_commit_dates`, importer `doc_dates_by_root`/`dates_updated`, both entry-point wirings, date APIs + tree `created_at`/`updated_at`, LLM surfaces (prompt block, `read` line 2, appended `ls` field), `apply_recency_boost` in `retrieve()`, UI columns/badge, admin editor, mock-LLM regex — all present and correct; no defects found, no fixes needed.
- `uv run pytest --cov=app --cov-report=term-missing` → **2299 passed, TOTAL 99%** (>90% ✓)
- `uv run pytest tests/e2e/test_document_dates.py -v --no-cov` → **6/6 passed** in isolation (DB up)
- 12 regression E2E suites (retrieval_quality, whole_document_context, agent_document_tools, ls_tree_drilldown, read_truncation_cap, kb_tree, kb_tree_nav, document_viewer, edit_summaries, import_documents, sync_button, hidden_folders_toggle, smoke) → **all green in isolation**
- `uv run ruff check .` → clean; `uv run pyright` → **0 errors, 0 warnings**

**Completion criteria:** 1) non-null `created_at` + 0020 upgrade/downgrade on dev DB ✓ (real-Alembic integration tests) 2) sync refresh/older/manual-persists/content-reset/no sources_meta bump ✓ 3) zip/tar mtime + future→today ✓ 4) LLM date surfaces + cross-check ✓ 5) UI Created/Updated/badge positions ✓ 6) admin editor set+revert round-trip ✓ 7) old-correct-beats-new-similar (defaults & boost-off) + near-tie + `BOR_RECENCY_BOOST=0` byte-identical ✓ 8) full gate ✓ 9) commit/phase-move — left to harness per instructions.

- **Notable:** recency default tuned 0.001 → **0.0007** (task 07 step 5 explicitly permits; measured margins recorded in `test_recency_boost.py` docstring).
- **Next pending phase:** none — `todo/` holds only this phase.
This commit is contained in:
2026-09-13 19:28:05 -04:00
parent cec819743d
commit ee3efb28c9
113 changed files with 8228 additions and 344 deletions
+85 -51
View File
@@ -37,6 +37,7 @@ import logging
import uuid
from collections.abc import AsyncGenerator, AsyncIterator, Sequence
from copy import deepcopy
from datetime import UTC, datetime
from typing import TYPE_CHECKING, Any, cast
import pytest
@@ -72,6 +73,13 @@ def _settings(**kwargs: Any) -> Settings:
return Settings(**kwargs) # pyright: ignore[reportCallIssue]
#: The fixture documents' fixed creation date (phase 106, D5): the
#: agent formats ``doc.created_at`` on the ``read`` result's second
#: line — the detached fixture rows carry it exactly as the NOT NULL
#: DB column guarantees it for real rows.
_FIXTURE_CREATED_AT = datetime(2024, 6, 15, 12, 0, 0, tzinfo=UTC)
def _doc(source: str, path: str, title: str = "Title", content: str = "CONTENT") -> Document:
return Document(
id=uuid.uuid4(),
@@ -81,6 +89,7 @@ def _doc(source: str, path: str, title: str = "Title", content: str = "CONTENT")
title=title,
content=content,
content_hash="0" * 64,
created_at=_FIXTURE_CREATED_AT,
)
@@ -438,7 +447,9 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
# lookup, no self-correction candidates (phase 70).
assert calls == [("Homelab", "aws-route53.md")]
# The follow-up request carries the assistant tool-call + tool result.
# The follow-up request carries the assistant tool-call + tool
# result (phase 106, D5: the ``date:`` second line rides every
# read — the first line is byte-identical).
msgs = llm.requests[1][0]
assert msgs[0] == {"role": "system", "content": "SYSTEM_PROMPT"}
assert msgs[1] == {"role": "user", "content": "QUESTION"}
@@ -468,7 +479,11 @@ def test_ls_then_read_then_answer(monkeypatch: pytest.MonkeyPatch) -> None:
assert msgs[5] == {
"role": "tool",
"tool_call_id": "call_2",
"content": "Document Homelab/aws-route53.md:\nR53-CONTENT", # full text, no cap
"content": (
"Document Homelab/aws-route53.md:\n"
"date: 2024-06-15\n"
"R53-CONTENT"
), # full text, no cap (date: the D5 second line)
}
@@ -641,14 +656,14 @@ def test_ls_source_scope_lists_root_folder(monkeypatch: pytest.MonkeyPatch) -> N
"""A registered source name (no ``/``): the source's ROOT folder —
subfolders (2-space-indented, path order, ``: {summary}`` only when
stored) + the root's own file lines in EXACTLY the
``source: X | path: Y | title: Z`` format — the pinned template,
counted."""
``source: X | path: Y | title: Z | date: YYYY-MM-DD`` format (the
phase-106 D5 appended date field) — the pinned template, counted."""
monkeypatch.setattr(
agent,
"ls_folder",
lambda db, source, folder: (
[("backups", 2, "Backup notes."), ("networking", 1, None)],
[("Homelab", "readme.md", "Readme")],
[("Homelab", "readme.md", "Readme", "2024-06-15")],
1,
),
)
@@ -665,7 +680,7 @@ def test_ls_source_scope_lists_root_folder(monkeypatch: pytest.MonkeyPatch) -> N
" backups/ — 2 documents: Backup notes.\n"
" networking/ — 1 documents\n"
"\n"
"source: Homelab | path: readme.md | title: Readme"
"source: Homelab | path: readme.md | title: Readme | date: 2024-06-15"
)
assert holder.tool_calls == 1
@@ -677,11 +692,11 @@ def test_ls_nested_folder_scope_lists_one_level_deeper(
lines, identity = ``source/folder`` (the same template as the
root), counted; the fetchers are the source-scoped ones."""
def _rows(db: Any, source: str) -> list[tuple[str, str]]:
def _rows(db: Any, source: str) -> list[tuple[str, str, str]]:
assert (source, db) == ("Homelab", None)
return [
("networking/lan.md", "LAN"),
("networking/vpn.md", "VPN"),
("networking/lan.md", "LAN", "2024-06-15"),
("networking/vpn.md", "VPN", "2024-06-15"),
]
monkeypatch.setattr(agent, "_source_document_rows", _rows)
@@ -702,8 +717,8 @@ def test_ls_nested_folder_scope_lists_one_level_deeper(
assert llm.requests[1][0][3]["content"] == (
"Homelab/networking — 2 documents, 0 folders:\n"
"\n"
"source: Homelab | path: networking/lan.md | title: LAN\n"
"source: Homelab | path: networking/vpn.md | title: VPN"
"source: Homelab | path: networking/lan.md | title: LAN | date: 2024-06-15\n"
"source: Homelab | path: networking/vpn.md | title: VPN | date: 2024-06-15"
)
assert holder.tool_calls == 1
@@ -812,9 +827,9 @@ def test_ls_unknown_top_level_folder_gets_not_a_folder_teaching(
agent,
"_source_document_rows",
lambda db, source: [
("backups/cron.md", "Cron"),
("containers/caddy.md", "Caddy"),
("networking/lan.md", "LAN"),
("backups/cron.md", "Cron", "2024-06-15"),
("containers/caddy.md", "Caddy", "2024-06-15"),
("networking/lan.md", "LAN", "2024-06-15"),
],
)
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
@@ -847,9 +862,9 @@ def test_ls_unknown_nested_folder_gets_not_a_folder_with_nested_parent(
agent,
"_source_document_rows",
lambda db, source: [
("networking/lan/a.md", "A"),
("networking/vpn/b.md", "B"),
("readme.md", "Readme"),
("networking/lan/a.md", "A", "2024-06-15"),
("networking/vpn/b.md", "B", "2024-06-15"),
("readme.md", "Readme", "2024-06-15"),
],
)
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
@@ -878,7 +893,10 @@ def test_ls_file_path_scope_gets_not_a_folder(monkeypatch: pytest.MonkeyPatch) -
monkeypatch.setattr(
agent,
"_source_document_rows",
lambda db, source: [("notes.md", "Notes"), ("a/b.md", "B")],
lambda db, source: [
("notes.md", "Notes", "2024-06-15"),
("a/b.md", "B", "2024-06-15"),
],
)
monkeypatch.setattr(agent, "_source_folder_summaries", lambda db, source: {})
monkeypatch.setattr(agent, "list_source_names", lambda db: ["S"])
@@ -937,9 +955,9 @@ def test_ls_folder_composes_the_fetchers(monkeypatch: pytest.MonkeyPatch) -> Non
monkeypatch surface)."""
seen: list[tuple[str, str, str]] = []
def _rows(db: Any, source: str) -> list[tuple[str, str]]:
def _rows(db: Any, source: str) -> list[tuple[str, str, str]]:
seen.append(("rows", source, ""))
return [("a/b.md", "B"), ("a.md", "A")]
return [("a/b.md", "B", "2024-06-15"), ("a.md", "A", "2024-06-15")]
def _summaries(db: Any, source: str) -> dict[str, str]:
seen.append(("summaries", source, ""))
@@ -949,7 +967,7 @@ def test_ls_folder_composes_the_fetchers(monkeypatch: pytest.MonkeyPatch) -> Non
monkeypatch.setattr(agent, "_source_folder_summaries", _summaries)
assert agent.ls_folder(cast("Session", object()), "S", "") == (
[("a", 1, "A stuff.")],
[("S", "a.md", "A")],
[("S", "a.md", "A", "2024-06-15")],
1,
)
assert seen == [("rows", "S", ""), ("summaries", "S", "")]
@@ -961,10 +979,10 @@ def test_group_folder_listing_subfolder_recursion_and_counts() -> None:
counts for BOTH ``a`` and ``a/b``), path order, the stored summary
attached or ``None``."""
rows = [
("a/b/c.md", "C"),
("a/b/d.md", "D"),
("a/e.md", "E"),
("f.md", "F"),
("a/b/c.md", "C", "2024-06-15"),
("a/b/d.md", "D", "2024-06-15"),
("a/e.md", "E", "2024-06-15"),
("f.md", "F", "2024-06-15"),
]
sub, files, total = agent.group_folder_listing(
"S", "", rows, {"a": "A subtree.", "a/b": "B subtree."}
@@ -974,7 +992,7 @@ def test_group_folder_listing_subfolder_recursion_and_counts() -> None:
# recursive subtree (a/e.md + a/b/c.md + a/b/d.md), the stored
# summary attached.
assert sub == [("a", 3, "A subtree.")]
assert files == [("S", "f.md", "F")]
assert files == [("S", "f.md", "F", "2024-06-15")]
assert total == 1
# One level down: a/b is a's direct subfolder with its own count.
sub2, _files2, _total2 = agent.group_folder_listing("S", "a", rows, {"a/b": "B subtree."})
@@ -987,13 +1005,13 @@ def test_group_folder_listing_nested_level_counts_and_membership() -> None:
file of ``a``) — membership is the folder_of rule, order is path
order."""
rows = [
("a/b/c.md", "C"),
("a/b/d.md", "D"),
("a/e.md", "E"),
("a/b/c.md", "C", "2024-06-15"),
("a/b/d.md", "D", "2024-06-15"),
("a/e.md", "E", "2024-06-15"),
]
sub, files, total = agent.group_folder_listing("S", "a", rows, {})
assert sub == [("a/b", 2, None)]
assert files == [("S", "a/e.md", "E")]
assert files == [("S", "a/e.md", "E", "2024-06-15")]
assert total == 1
@@ -1004,18 +1022,18 @@ def test_group_folder_listing_file_path_is_not_a_folder() -> None:
sharing a real folder's name counts for that folder, the existence
rule intact)."""
rows = [
("a.md", "A"), # a file at the root, and a folder name? NO —
("b/x.md", "X"), # nothing starts with "a.md/"
("a.md", "A", "2024-06-15"), # a file at the root, and a folder name? NO —
("b/x.md", "X", "2024-06-15"), # nothing starts with "a.md/"
]
sub, files, total = agent.group_folder_listing("S", "", rows, {})
assert sub == [("b", 1, None)] # "a.md" is NOT a subfolder
assert files == [("S", "a.md", "A")] # b/x.md is NOT a direct root file
assert files == [("S", "a.md", "A", "2024-06-15")] # b/x.md is NOT a direct root file
assert total == 1
# The path == folder arm: a doc named "a" under a real folder "a/".
rows2 = [("a", "FileA"), ("a/c.md", "C")]
rows2 = [("a", "FileA", "2024-06-15"), ("a/c.md", "C", "2024-06-15")]
sub2, files2, total2 = agent.group_folder_listing("S", "", rows2, {})
assert sub2 == [("a", 2, None)] # the file "a" counts for folder "a"
assert files2 == [("S", "a", "FileA")] # …and is a direct ROOT file
assert files2 == [("S", "a", "FileA", "2024-06-15")] # …and is a direct ROOT file
assert total2 == 1
@@ -1023,14 +1041,14 @@ def test_group_folder_listing_caps_files_at_fifty_keeps_the_total() -> None:
"""The cap: 51 direct files → 50 file lines + the PRE-cap total (51)
for the renderer's note; 50 files → 50 lines, no note material.
A 500-file folder costs 50 lines, never 500."""
rows51 = [(f"big/f{i:03d}.md", f"T{i}") for i in range(51)]
rows51 = [(f"big/f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(51)]
sub, files, total = agent.group_folder_listing("S", "big", rows51, {})
assert sub == []
assert total == 51
assert len(files) == 50
assert files[0] == ("S", "big/f000.md", "T0")
assert files[-1] == ("S", "big/f049.md", "T49")
rows50 = [(f"big/f{i:03d}.md", f"T{i}") for i in range(50)]
assert files[0] == ("S", "big/f000.md", "T0", "2024-06-15")
assert files[-1] == ("S", "big/f049.md", "T49", "2024-06-15")
rows50 = [(f"big/f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(50)]
_sub, files50, total50 = agent.group_folder_listing("S", "big", rows50, {})
assert total50 == 50 and len(files50) == 50
@@ -1056,7 +1074,7 @@ def test_render_folder_listing_root_template() -> None:
agent.render_folder_listing(
"Homelab",
[("backups", 2, "Backup notes."), ("networking", 1, None)],
[("Homelab", "readme.md", "Readme")],
[("Homelab", "readme.md", "Readme", "2024-06-15")],
1,
)
== "Homelab — 1 documents, 2 folders:\n"
@@ -1064,7 +1082,7 @@ def test_render_folder_listing_root_template() -> None:
" backups/ — 2 documents: Backup notes.\n"
" networking/ — 1 documents\n"
"\n"
"source: Homelab | path: readme.md | title: Readme"
"source: Homelab | path: readme.md | title: Readme | date: 2024-06-15"
)
@@ -1090,7 +1108,7 @@ def test_render_folder_listing_cap_note_only_past_fifty() -> None:
cap: 51 → 50 lines + the deterministic grep-pointer note (the
``…and 1 more…`` shape — unpluralized, the house pin); 50 → no
note."""
files51 = [("S", f"f{i:03d}.md", f"T{i}") for i in range(51)]
files51 = [("S", f"f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(51)]
capped = files51[:50]
rendered = agent.render_folder_listing("S/big", [], capped, 51)
lines = rendered.splitlines()
@@ -1100,9 +1118,11 @@ def test_render_folder_listing_cap_note_only_past_fifty() -> None:
"…and 1 more documents in this folder — use grep (pattern) to "
"find a specific one."
)
files50 = [("S", f"f{i:03d}.md", f"T{i}") for i in range(50)]
files50 = [("S", f"f{i:03d}.md", f"T{i}", "2024-06-15") for i in range(50)]
rendered50 = agent.render_folder_listing("S/big", [], files50, 50)
assert rendered50.splitlines()[-1] == "source: S | path: f049.md | title: T49"
assert rendered50.splitlines()[-1] == (
"source: S | path: f049.md | title: T49 | date: 2024-06-15"
)
assert "more documents" not in rendered50
@@ -1145,7 +1165,9 @@ def test_read_combined_path_resolves_and_returns_full_content(
assert holder.read_docs == [doc]
assert holder.tool_calls == 1
assert llm.requests[1][0][3]["content"] == (
"Document Homelab/active/container_caddy/caddy.md:\nCADDY-CONTENT"
"Document Homelab/active/container_caddy/caddy.md:\n"
"date: 2024-06-15\n"
"CADDY-CONTENT"
)
@@ -1533,7 +1555,9 @@ def test_reading_an_already_read_doc_is_deduped(monkeypatch: pytest.MonkeyPatch)
asyncio.run(_run(llm, holder, _settings()))
assert holder.read_docs == [doc] # appended exactly once
assert holder.tool_calls == 1 # the re-read counts nothing
assert llm.requests[1][0][3]["content"] == "Document S/a.md:\nA-CONTENT"
assert llm.requests[1][0][3]["content"] == (
"Document S/a.md:\ndate: 2024-06-15\nA-CONTENT"
)
assert llm.requests[2][0][5]["content"] == agent.ALREADY_IN_CONTEXT
# Rejected → the tools are still offered on the next request…
assert llm.requests[2][1] == AGENT_TOOLS
@@ -1558,8 +1582,11 @@ def test_read_exactly_at_cap_is_byte_identical_and_untruncated(
[StreamPiece("content", "ans")],
)
out = asyncio.run(_run(llm, holder, _settings(read_max_chars=cap)))
# Byte-identical to today's read result (no marker, no notice).
assert llm.requests[1][0][3]["content"] == "Document S/big.md:\n" + content
# The read result plus the phase-106 D5 date line (no marker, no
# notice).
assert llm.requests[1][0][3]["content"] == (
"Document S/big.md:\ndate: 2024-06-15\n" + content
)
assert TRUNCATION_MARKER not in llm.requests[1][0][3]["content"]
# No truncation recorded, none surfaced to the loop.
assert holder.read_truncations == []
@@ -1589,6 +1616,7 @@ def test_read_at_cap_plus_one_truncates_with_marker_and_notice(
asyncio.run(_run(llm, holder, _settings(read_max_chars=cap)))
expected = (
"Document S/big.md:\n"
"date: 2024-06-15\n"
+ content[:cap]
+ "\n"
+ TRUNCATION_MARKER
@@ -2247,7 +2275,9 @@ def test_grep_counts_but_never_adds_context(monkeypatch: pytest.MonkeyPatch) ->
assert holder.tool_calls == 2 # grep + read, both executed
assert holder.read_docs == [doc] # only the read added context (A5)
assert llm.requests[1][0][3]["content"] == "S/a.md:1: needle here"
assert llm.requests[2][0][5]["content"] == "Document S/a.md:\nneedle here"
assert llm.requests[2][0][5]["content"] == (
"Document S/a.md:\ndate: 2024-06-15\nneedle here"
)
# ---------- unlimited calls: re-lists and multi-reads (phase 45) ----------
@@ -2292,8 +2322,12 @@ def test_multi_read_executes_without_budgets(monkeypatch: pytest.MonkeyPatch) ->
asyncio.run(_run(llm, holder, _settings()))
assert holder.read_docs == [a, b] # both reads appended, in order
assert holder.tool_calls == 2
assert llm.requests[1][0][3]["content"] == "Document S/a.md:\nA-CONTENT"
assert llm.requests[2][0][5]["content"] == "Document S/b.md:\nB-CONTENT"
assert llm.requests[1][0][3]["content"] == (
"Document S/a.md:\ndate: 2024-06-15\nA-CONTENT"
)
assert llm.requests[2][0][5]["content"] == (
"Document S/b.md:\ndate: 2024-06-15\nB-CONTENT"
)
assert llm.requests[2][1] == AGENT_TOOLS # the second read was still offered