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
+72 -7
View File
@@ -235,6 +235,10 @@ class DocSummary(BaseModel):
path: str
title: str
chunks: int
#: The document's creation date (phase 106, D8) — ISO-8601, verbatim
#: from the row (the ``indexed_at`` style); the RAG view's file table
#: renders it as the ``Created`` column (before ``Indexed``).
created_at: str
indexed_at: str
@@ -251,7 +255,8 @@ class KbTreeFile(BaseModel):
JSON shape is the contract). ``path`` is SOURCE-RELATIVE (the RAG
view prefixes the source in its breadcrumb); ``title`` /
``chunks`` (content + ``is_summary`` chunks — the same count
``GET /api/docs`` returns) / ``indexed_at`` (ISO-8601) are verbatim
``GET /api/docs`` returns) / ``created_at`` (phase 106, D8 — the
document's creation date) / ``indexed_at`` (ISO-8601) are verbatim
from the catalogue row the endpoint reads.
"""
@@ -259,6 +264,10 @@ class KbTreeFile(BaseModel):
path: str
title: str
chunks: int = Field(ge=0)
#: The document's creation date (phase 106, D8) — ISO-8601, verbatim
#: from the catalogue row; the RAG view's file table renders it as
#: the ``Created`` column (before ``Indexed``).
created_at: str
indexed_at: str
@@ -269,11 +278,14 @@ class KbTreeFolder(BaseModel):
node IS the root); ``documents`` is the recursive subtree count
(the phase-94 ``ls`` count rule: every path equal to the folder or
starting with ``folder + "/"`` — the file sharing a folder's name
counts); ``summary`` is the stored ``folder_summaries`` row (AI or
manual — any row) or null; ``children`` are the direct subfolders
(path order) followed by the direct files (catalog order) — the
recursive union (Pydantic v2 resolves it with
``from __future__ import annotations``).
counts); ``updated_at`` (phase 106, D9) is the subtree's MAX
document ``created_at`` — DERIVED in the pure tree builder as it
recurses, never stored (``null`` for a node with no documents at
all — the ``summary: str | None`` shape); ``summary`` is the stored
``folder_summaries`` row (AI or manual — any row) or null;
``children`` are the direct subfolders (path order) followed by the
direct files (catalog order) — the recursive union (Pydantic v2
resolves it with ``from __future__ import annotations``).
``summary_pending`` (phase 98, D3 — ONE concept): true iff this
folder's recursive count ≥ ``MIN_DOCS_PER_FOLDER`` (2) AND it has
@@ -287,6 +299,10 @@ class KbTreeFolder(BaseModel):
kind: Literal["folder"] = "folder"
path: str
documents: int = Field(ge=0)
#: The subtree's MAX document ``created_at`` (phase 106, D9 —
#: derived in the pure builder, never stored); ISO-8601, ``null``
#: for a node with no documents at all.
updated_at: str | None = None
summary: str | None = None
summary_pending: bool = False
children: list[KbTreeFolder | KbTreeFile] = Field(default_factory=list)
@@ -299,7 +315,10 @@ class KbTreeSource(BaseModel):
always present, a registered 0-document source lists with
``documents: 0`` and no children), then the indexed-only sources
(alphabetical) — the phase-97 superset rule. ``documents`` is the
source's whole recursive count; ``summary`` is the stored
source's whole recursive count; ``updated_at`` (phase 106, D9) is
the source's subtree MAX document ``created_at`` — DERIVED in the
pure tree builder, never stored (``null`` for a 0-document source —
the ``summary: str | None`` shape); ``summary`` is the stored
``(source, "")`` source-root row or null; ``children`` are the
source's direct subfolders + direct files (same shape as a folder
node's).
@@ -316,6 +335,10 @@ class KbTreeSource(BaseModel):
name: str
documents: int = Field(ge=0)
#: The source's subtree MAX document ``created_at`` (phase 106,
#: D9 — derived in the pure builder, never stored); ISO-8601,
#: ``null`` for a 0-document source.
updated_at: str | None = None
summary: str | None = None
summary_pending: bool = False
children: list[KbTreeFolder | KbTreeFile] = Field(default_factory=list)
@@ -350,6 +373,10 @@ class DocContent(BaseModel):
#: markdown documents, pre-phase-30 rows, and the fail-soft path where
#: summary generation failed but the document was still indexed.
summary: str | None = None
#: The document's creation date (phase 106, D8) — ISO-8601, verbatim
#: from the row; the viewer's top meta row renders the ``Created``
#: badge from it (before the ``Indexed`` badge).
created_at: str
content: str
indexed_at: str
chunks: int
@@ -389,6 +416,44 @@ class SummaryResult(BaseModel):
chunks: int
class DateUpdate(BaseModel):
"""``PATCH /api/documents/date`` body (phase 106, task 05, D7).
``source`` / ``path`` name the indexed document (the same pair the
public ``GET /api/documents/content`` looks up); ``date`` is the
owner's corrected creation date — an ISO date (``YYYY-MM-DD``) or a
full ISO datetime. **Null/absent is the CLEAR** (the "revert to
sync" operation, D7): the ``created_at_manual`` flag is dropped and
the stored date stands until the next sync refreshes it (the API is
DB-only — it cannot re-read the source). Unconstrained
``str | None`` on purpose: a MALFORMED non-null value 422s in the
handler (``datetime.fromisoformat``), so the error detail can name
the field; an unknown pair must 404 as "document not found"
(row-lookup semantics), exactly like the public content endpoint.
"""
source: str
path: str
date: str | None = None
class DateResult(BaseModel):
"""``PATCH /api/documents/date`` response (phase 106, task 05, D7).
Echoes the STORED state after the change: ``created_at`` is the
stored ISO-8601 value (a set stores the normalized parse — a
manually set FUTURE date folds to today, D3; a clear leaves the
stored date standing) and ``created_at_manual`` the flag (true
after a set, false after a clear). The viewer re-renders its
Created badge from exactly this echo — no second fetch.
"""
source: str
path: str
created_at: str
created_at_manual: bool
class FolderSummaryUpdate(BaseModel):
"""``PATCH /api/folders/summary`` body (phase 97, task 03).