7.8 KiB
7.8 KiB
Task 05 — Date API surface: reads, the admin date edit, and the tree's dates (D7/D8/D9)
Phase: 106_document_dates · Source: owner request 2026-09-13 — "This timestamp should be editable so users can correct for errors"; the catalog needs the file dates + folder last-updated (the UI in task 08 renders exactly what this task serves).
Objective
Serve the date everywhere the UI (task 08) and the viewer need it — GET /api/docs, GET /api/documents/content, and GET /api/docs/tree (files: created_at; folders/sources: the derived subtree-max updated_at, D9) — and add the admin-only PATCH /api/documents/date (set + revert, the phase-57 gate/idiom, D7).
Work
app/schemas.py:DocSummary(L228-235): addcreated_at: str(ISO-8601 — theindexed_atdocstring style: "verbatim from the row").DocContent(L343-358): addcreated_at: str(aftersummary, beforecontent— group the metadata).- NEW
DateUpdate(theSummaryUpdateshape, L361-373):source: str,path: str,date: str | None(docstring: an ISO dateYYYY-MM-DDor full ISO datetime; null/absent = the CLEAR — drop the manual flag, the stored date stands until the next sync refresh; a malformed non-null value 422s through Pydantic… correction:strpasses any string — the handler parses (step 3); the 422 comes from the handler, not the model, so the error detail can name the field). - NEW
DateResult:source: str,path: str,created_at: str,created_at_manual: bool(echoes the stored state — the viewer re-renders from it). KbTreeFile(L241-256): addcreated_at: str(verbatim from the catalogue row — theindexed_atfield's docstring pattern).KbTreeFolder(L259-290) andKbTreeSource(L293-320): addupdated_at: str | None(docstring: the subtree's MAX documentcreated_at— D9, derived, never stored;nullfor a 0-document source, thesummary: str | Noneshape).
app/api/docs.py:list_indexed_documents(L80-119): addDocument.created_atto the select AND thegroup_by(theindexed_attwin, L104/L107);DocSummary(..., created_at=row.created_at.isoformat()).get_document_content(L121-162):created_at=doc.created_at.isoformat()in theDocContent(L161 site).- NEW
PATCH /api/documents/date(route order: next toupdate_document_summary, L164-232 —require_admindependency, the phase-57 gate):Logic (DB-only — the@router.patch("/documents/date", response_model=DateResult) def update_document_date(payload: DateUpdate, db: Session = Depends(get_db), _admin: None = Depends(require_admin)) -> DateResult:/documents/contentrow-lookup rule, no filesystem, no LLM/embedding call — a date is never embedded, the phase-57 no-LLM contrast): look up the row by(source, path)→ none → 404{"detail": "document not found"}(row-lookup semantics, the traversal-string-is-not-a-row note).payload.datetruthy →parsed = datetime.fromisoformat(payload.date)(a bareYYYY-MM-DDand full ISO datetimes both parse;ValueError→ 422{"detail": "date must be an ISO date or datetime (e.g. 2024-06-15)"}) →doc.created_at = normalize_doc_date(parsed)(import fromapp.rag.doc_dates— D3: a manually set FUTURE date also folds to today, consistency with the sourced path) →doc.created_at_manual = True.payload.datefalsy (null/absent — the CLEAR) →doc.created_at_manual = Falseonly (the stored date stands; the next sync refreshes it — the API cannot re-read the source, D7).db.commit(); returnDateResultwith the storedcreated_at.isoformat()+ flag. - The tree (task-05 half of D8/D9):
TreeDocRow(L296-302) becomes the 6-tuple(source, path, title, chunks, indexed_at, created_at)(both ISO strings — the builder stays pure over plain types);_folder_counts/_level_children/_source_nodethread a 6th element through their tuple unpacks (the_-named slots gain the date) and_level_children/_source_nodecompute each folder/source'supdated_at: the MAX of the direct files'created_atand the children'supdated_atvalues (ISO-8601 strings compare correctly lexicographically — they're all the sameisoformat()shape; document that in the builder docstring) —Nonewhen the node has no documents at all (the 0-document registered source).KbTreeFolder(…, updated_at=…)/KbTreeFile(…, created_at=…)/KbTreeSource(…, updated_at=…)at their construction sites (L342-375, L467-490).list_kb_tree(L492-551): addDocument.created_atto the query's select + group_by (theindexed_attwin, L543-546) and thedoc_rowscomprehension. Thebuild_kb_treedocstring gains the D9 clause (updated_at = subtree max, derived, None for empty).
- Tests:
tests/unit/test_kb_tree_builder.py(extended — the pure builder): file nodes carrycreated_atverbatim; a nested fixture asserts each folder's + the source'supdated_at= the subtree max (a deeper file's date wins over a shallow sibling's); a 0-document registered source →updated_at is Noneand no children; the ls↔group_folder_listingcross-check tests (L210-280) still pass with the extended tuples (task 06 changes the agent side — until then the rows stay 6-tuples on BOTH sides only after task 06; for THIS task the cross-check compares file(path, title[, chunks, indexed_at])projections — read the current assertions and keep them green: the tree builder's file tuples are internal to the builder, the cross-check uses the builder's OUTPUT nodes, so it should pass unchanged — verify and pin).tests/integration/test_docs_api_dates.py(NEW — thetests/integration/test_docs_api.pyscaffolding: real app +dbfixture, an admin cookie where that suite gets one): seed two documents in a nested folder (distinctcreated_ats via direct row writes):GET /api/docs(admin) reportscreated_atper row (andindexed_atunchanged);GET /api/documents/contentcarriescreated_at;GET /api/docs/tree— the file node'screated_atverbatim, the parent folder's and the source'supdated_at= the max, a registered-but-empty source →updated_at: null;- the PATCH matrix — set
2020-01-02→ 200 + response echoes the stored ISO +created_at_manual: true+ a re-GET confirms; set a full ISO datetime → accepted; malformed"not-a-date"→ 422 (the detail names the field);date: null→ 200 +created_at_manual: false+ the stored date UNCHANGED; a future date"2999-01-01"→ storedcreated_atfolds to today (D3); unknown(source, path)→ 404document not found; anonymous → 403 (the gate).
- Run
uv run pytest tests/unit/test_kb_tree_builder.py tests/integration/test_docs_api_dates.py -q(DB up) — green.
Testing & Quality
- Unit: the pure builder's date threading (max computation, None-for-empty, verbatim file dates).
- Integration: the full API matrix (reads + PATCH set/malformed/clear/future/404/403) against real Postgres.
- Coverage: >90% on
app/(the new route + the builder branches covered — the validate.sh gate).
Completion Criteria
GET /api/docs,GET /api/documents/content, andGET /api/docs/treeservecreated_at(files) andupdated_at(folders/sources — subtree max,nullwhen empty, derived in the pure builder, D9)PATCH /api/documents/dateis admin-only, DB-only, no-LLM: set (ISO date or datetime, future folds to today,created_at_manual=true), clear (null → flag drops, date stands), 422 malformed, 404 unknown pair, 403 anonymous — the phase-57 split intact (viewer stays user-gated)tests/unit/test_kb_tree_builder.py+tests/integration/test_docs_api_dates.pypass; existing docs-API suites stay greenuv run ruff check . && uv run pyrightclean