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
+175
View File
@@ -0,0 +1,175 @@
"""Unit: the D6 recency boost on the fused score (phase 106, task 07).
Pure-function pins for
:func:`app.rag.retriever.apply_recency_boost` — the decay magnitude
(zero age → full weight, one half-life → ``weight/e``, ten half-lives
→ negligible), the future-date clamp, the ``weight=0`` kill switch
(byte-identical scores AND order), the tie-breaks (a raw-score tie
breaks toward the newer document; the ``(path, position)`` key still
applies when scores AND cosines AND ages are equal), and the
no-mutation contract (the ``fuse`` convention). Fake rows, no DB —
the real-Postgres fine-line battery (the owner's scenario) lives in
``tests/integration/test_recency_boost.py``.
"""
from __future__ import annotations
import math
import uuid
from datetime import UTC, datetime, timedelta
import pytest
from app.config import get_settings
from app.models import Document
from app.rag.retriever import RetrievedChunk, apply_recency_boost
#: A fixed "now" — the pins must not depend on the wall clock.
NOW = datetime(2026, 9, 13, 12, 0, tzinfo=UTC)
def _doc(path: str, created_at: datetime, source: str = "Homelab") -> Document:
return Document(
id=uuid.uuid4(),
source=source,
path=path,
full_path=f"/tmp/{path}",
title=path,
content="x",
content_hash="0" * 64,
indexed_at=created_at,
created_at=created_at,
)
def _chunk(
doc: Document, score: float, position: int = 0, cosine: float = 0.9
) -> RetrievedChunk:
return RetrievedChunk(
chunk_id=uuid.uuid4(),
position=position,
content="x",
score=score,
document=doc,
cosine=cosine,
)
def test_zero_age_gets_the_full_weight_exactly() -> None:
"""Age 0 → ``+weight`` with no float drift (``exp(0) == 1.0``)."""
c = _chunk(_doc("a.md", NOW), 0.032787)
out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365)
assert out[0].score == 0.032787 + 0.0007
def test_one_half_life_of_age_decays_to_weight_over_e() -> None:
"""Age = half-life → ``+weight·e⁻¹`` (±1e-9)."""
c = _chunk(_doc("a.md", NOW - timedelta(days=365)), 0.032787)
out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365)
assert out[0].score == pytest.approx(
0.032787 + 0.0007 * math.exp(-1.0), abs=1e-9
)
def test_ten_half_lives_of_age_is_negligible() -> None:
"""Age 10× the half-life → ``+weight·e⁻¹⁰`` < ``weight·1e-3`` — the
boost has faded to nothing (recency is an age signal, not a binary)."""
c = _chunk(_doc("a.md", NOW - timedelta(days=3650)), 0.032787)
out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365)
assert out[0].score - 0.032787 < 0.0007 * 1e-3
def test_future_created_at_clamps_to_zero_age() -> None:
"""A future ``created_at`` clamps to age 0 — the full weight (the D3
today-folding consistency: a future-sourced doc reads as brand-new)."""
c = _chunk(_doc("a.md", NOW + timedelta(days=30)), 0.032787)
out = apply_recency_boost([c], now=NOW, weight=0.0007, half_life_days=365)
assert out[0].score == 0.032787 + 0.0007
def test_weight_zero_is_byte_identical_scores_and_order() -> None:
"""The kill switch: ``weight=0`` leaves every score untouched and the
order byte-identical for an already-fused (already 4-key-sorted)
input."""
a0 = _chunk(_doc("a.md", NOW - timedelta(days=10)), 0.03, cosine=0.9, position=0)
b0 = _chunk(_doc("b.md", NOW - timedelta(days=20)), 0.03, cosine=0.8, position=0)
a1 = _chunk(_doc("a.md", NOW - timedelta(days=10)), 0.02, cosine=0.95, position=0)
c1 = _chunk(_doc("c.md", NOW - timedelta(days=30)), 0.02, cosine=0.5, position=1)
chunks = [a0, b0, a1, c1] # already sorted by the 4-key order
out = apply_recency_boost(chunks, now=NOW, weight=0.0, half_life_days=365)
assert [
(rc.score, rc.cosine, rc.document.path, rc.position) for rc in out
] == [
(rc.score, rc.cosine, rc.document.path, rc.position) for rc in chunks
]
def test_raw_score_tie_breaks_toward_the_newer_document() -> None:
"""An EXACT raw-score + cosine tie: the boost moves only the newer
document (the older one's boost has decayed to ~0), so the newer
rank rises above it — and with ``weight=0`` the pre-phase
(path-ordered) ranking stands."""
old = _chunk(_doc("a-old.md", NOW - timedelta(days=2500)), 0.016129)
new = _chunk(_doc("b-new.md", NOW), 0.016129)
out = apply_recency_boost([old, new], now=NOW, weight=0.0007, half_life_days=365)
assert [rc.document.path for rc in out] == ["b-new.md", "a-old.md"]
out_off = apply_recency_boost([old, new], now=NOW, weight=0.0, half_life_days=365)
assert [rc.document.path for rc in out_off] == ["a-old.md", "b-new.md"]
def test_path_position_tiebreak_when_scores_cosines_and_ages_equal() -> None:
"""When the boosted scores AND cosines are equal (same age → same
boost), the EXISTING ``(path, position)`` tie-break still decides —
first by path, then by position within one path."""
a = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=1)
b = _chunk(_doc("b.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=0)
out = apply_recency_boost([b, a], now=NOW, weight=0.001, half_life_days=365)
assert [rc.document.path for rc in out] == ["a.md", "b.md"]
# Same path, different positions (same doc, same age, same score):
p1 = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=1)
p0 = _chunk(_doc("a.md", NOW - timedelta(days=100)), 0.02, cosine=0.5, position=0)
out2 = apply_recency_boost([p1, p0], now=NOW, weight=0.001, half_life_days=365)
assert [rc.position for rc in out2] == [0, 1]
def test_defaults_come_from_settings_when_omitted() -> None:
"""Omitted *weight* / *half_life_days* fall back to the settings
(``recency_boost`` / ``recency_half_life_days``) — the explicit
settings values must reproduce the default call exactly."""
chunks = [
_chunk(_doc("a.md", NOW - timedelta(days=30)), 0.02),
_chunk(_doc("b.md", NOW - timedelta(days=700)), 0.02),
]
s = get_settings()
out_default = apply_recency_boost(chunks, now=NOW)
out_explicit = apply_recency_boost(
chunks,
now=NOW,
weight=s.recency_boost,
half_life_days=s.recency_half_life_days,
)
assert [rc.score for rc in out_default] == [rc.score for rc in out_explicit]
def test_inputs_are_never_mutated() -> None:
"""The ``fuse`` convention: the input list and its scores are
untouched — every returned chunk is a fresh ``replace()`` copy."""
chunks = [
_chunk(_doc("a.md", NOW - timedelta(days=30)), 0.02),
_chunk(_doc("b.md", NOW), 0.03),
]
original_scores = [rc.score for rc in chunks]
original_order = [rc.chunk_id for rc in chunks]
out = apply_recency_boost(chunks, now=NOW, weight=0.001, half_life_days=365)
assert [rc.score for rc in chunks] == original_scores
assert [rc.chunk_id for rc in chunks] == original_order
assert out is not chunks
assert all(o is not i for o, i in zip(out, chunks, strict=True))
def test_non_positive_half_life_fails_loud() -> None:
"""A ``half_life_days <= 0`` argument would divide the exponent by
zero — the settings validator guards startup, the function guards
direct calls (the ``fuse`` ``k <= 0`` pattern)."""
c = _chunk(_doc("a.md", NOW), 0.02)
with pytest.raises(ValueError, match="half_life_days must be > 0"):
apply_recency_boost([c], now=NOW, weight=0.001, half_life_days=0)