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
+285 -9
View File
@@ -66,6 +66,42 @@
* everyone exactly as phase 36 — the public viewer stays
* byte-for-byte identical (no button, no wiring, no admin-only
* network call) until the admin gate resolves true.
*
* Phase 106 (task 08, D8): the .doc-meta badge row gains the Created
* badge BEFORE the Indexed one (the owner's verbatim position — the
* date at the top of a clicked document): `Created <locale date>` via
* fmtDate(doc.created_at), the full ISO timestamp on the badge's
* title (the titleEl ellipsis-precision idiom). ONE shared core —
* the modal and /document.html both render it (no per-surface copy);
* the date arrives in the same content payload (task 05 added
* created_at to GET /api/documents/content). The date EDITOR (the
* admin inline edit + PATCH /api/documents/date) is the task-09
* section below.
*
* Phase 106 (task 09, D7): the badge row gains the ADMIN-ONLY date
* editor (the phase-57 wireSummaryEdit idiom — the same
* docAdminReady() gate on the module-cached whoami promise, so no
* second request per page): an "Edit date" text button after the
* Created badge (task 08's insertion point) swaps in-place for an
* inline editor — a native type=date input + Save / Cancel + the
* muted "Revert to sync" clear affordance (the phase-57 "clear =
* explicit" contrast) + a role=status live line and a role=alert
* error line. Save PATCHes /api/documents/date with { source, path,
* date } and the badge re-renders from the RESPONSE's created_at
* (never the input's optimistic value); "Revert to sync" sends
* date: null (the D7 CLEAR — the manual flag drops, the stored date
* stands until the next sync). An EMPTY date input can't clear (Save
* disables itself — the explicit Revert is the only clear path, no
* accidental wipes). §7.4 never-stale: the controls disable
* immediately on submit (one PATCH at a time); a failure (non-2xx
* or network) lands the server detail (or the canned retry copy) in
* the role=alert line, reverts the input to the stored date, and
* re-enables — the UI never claims a state the server didn't save.
* A non-admin / token holder / failed whoami keeps the byte-for-byte
* task-08 badge row (no button, no wiring, no admin-only network
* call — the phase-57 split). ONE shared core — the modal and
* /document.html both get it (document-modal.js imports
* renderDocument from this module — no per-surface copy).
*/
import { bindSharedHeaderControls, fetchIsAdmin, initSharedHeader } from "./header.js";
@@ -94,23 +130,29 @@ function contentUrl(s, p) {
return "/api/documents/content?source=" + encodeURIComponent(s) + "&path=" + encodeURIComponent(p);
}
function metaBadge(cls, text) {
function metaBadge(cls, text, title) {
const el = document.createElement("span");
el.className = cls;
el.textContent = text;
// Phase 106 (task 08): optional hover precision — the meta row may
// clip (nowrap), so the exact value stays reachable on the badge's
// title (the titleEl ellipsis-precision idiom). Omitted → the
// pre-phase two-argument shape (the other badges are unchanged).
if (title !== undefined) el.setAttribute("title", title);
return el;
}
/* ---------- shared renderer (phase 26, task 02) ----------
* Populates the three elements every render surface provides: a title,
* a .doc-meta badge row (source · format · mono path · indexed ·
* chunks), and a content container — an optional .doc-summary section
* first (phase 36: only when doc.summary is non-empty — markdown docs
* and fail-soft rows carry none, so they render exactly as before),
* then .doc-md for md/markdown (the shared escape-first renderer),
* <pre class="doc-raw"> otherwise. The XSS contract: innerHTML only
* through renderMarkdown; every document-derived string (summary text
* included) is a text node. */
* a .doc-meta badge row (source · format · mono path · created ·
* indexed · chunks — phase 106 (task 08, D8) added created BEFORE
* indexed), and a content container — an optional .doc-summary
* section first (phase 36: only when doc.summary is non-empty —
* markdown docs and fail-soft rows carry none, so they render exactly
* as before), then .doc-md for md/markdown (the shared escape-first
* renderer), <pre class="doc-raw"> otherwise. The XSS contract:
* innerHTML only through renderMarkdown; every document-derived
* string (summary text included) is a text node. */
export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
titleEl.textContent = doc.title;
// Phase 34 task 04: the titlebar title ellipsizes — the full title
@@ -124,10 +166,28 @@ export function renderDocument(doc, { titleEl, metaEl, contentEl }) {
metaBadge("doc-source-badge", doc.source),
metaBadge("format-badge", doc.format),
pathCode,
// Phase 106 (task 08, D8): the Created badge BEFORE the Indexed
// one (the owner's verbatim position — the date at the top of a
// clicked document). textContent = the locale date (fmtDate — the
// Indexed idiom), and the badge's title = the FULL ISO timestamp
// (the titleEl ellipsis-precision idiom — the row clips, the exact
// value stays reachable).
metaBadge("doc-created", `Created ${fmtDate(doc.created_at)}`, doc.created_at),
metaBadge("doc-indexed", `Indexed ${fmtDate(doc.indexed_at)}`),
metaBadge("doc-chunks", `${doc.chunks} chunk${doc.chunks === 1 ? "" : "s"}`),
);
// Phase 106 (task 09, D7): the ADMIN-ONLY date editor — the
// module-cached whoami promise (docAdminReady — the SAME single
// request per page the summary edit shares) gates the wiring, so a
// non-admin / token holder / failed whoami keeps EXACTLY the task-08
// badge row: no button, no wiring, no admin-only network call (the
// phase-57 split). The button lands after the Created badge (task
// 08's insertion point); the editor itself lives in wireDateEdit.
void docAdminReady().then((admin) => {
if (admin) wireDateEdit(metaEl, doc);
});
contentEl.replaceChildren();
// Phase 36: the summary panel — labeled section ABOVE the original
// content, on BOTH surfaces (page + modal) through this one core.
@@ -308,6 +368,222 @@ function wireSummaryEdit(section, doc) {
cancelBtn.addEventListener("click", () => closeEditor(""));
}
/* ---------- date editing (phase 106, task 09 — D7, admin-only) ----------
* The .doc-meta badge row (task 08's Created badge) is the ONE place
* the stored creation date is edited (page + modal through the
* shared renderDocument core — no per-surface copy). Only an admin
* (docAdminReady) ever gets the affordance; a non-admin / token
* holder / failed whoami keeps exactly the task-08 badge row (no
* button, no wiring, no admin-only network call — the phase-57
* split). Save PATCHes /api/documents/date with { source, path,
* date } — a non-empty date sets it (the server normalizes it, D3,
* and stores the manual flag, D1); the explicit "Revert to sync"
* affordance sends date: null (the D7 CLEAR — the manual flag drops
* and the stored date stands until the next sync refreshes it).
* The badge re-renders from the RESPONSE's created_at — the UI shows
* exactly what the server stored, never the input's optimistic
* value. §7.4 never-stale: the controls disable IMMEDIATELY on
* Save/Revert (no double-submit); a failure (non-2xx or network)
* lands the server detail (or the canned retry copy) in the
* role=alert line, reverts the input to the stored date, and
* re-enables — the UI never claims a state the server didn't save.
* An EMPTY date input can't clear: Save disables itself on an empty
* input (the explicit Revert is the only clear path — no accidental
* wipes). */
/* The server detail of a non-2xx response (the git-sources.js
* apiDetail shape — the phase-89 error-line idiom: a string detail
* or the 422 array's first msg), with the neutral fallback for a
* non-JSON body. */
async function apiDetail(res, fallback) {
try {
const data = await res.json();
if (Array.isArray(data.detail) && data.detail[0] && data.detail[0].msg) {
return String(data.detail[0].msg);
}
if (typeof data.detail === "string" && data.detail) return data.detail;
} catch {
/* non-JSON error body */
}
return fallback;
}
/* The date editor on one rendered .doc-meta badge row: the "Edit
* date" text button after the Created badge (admin-only — the public
* row keeps task 08's shape). Edit swaps the button for the inline
* editor in place — a native <input type="date"> (value, never
* innerHTML — XSS contract) prefilled with the stored date's UTC
* date part, Save / Cancel, the muted "Revert to sync" clear, and
* the role=status / role=alert live lines. One PATCH at a time (the
* controls disable before the fetch and re-enable in the finally —
* every outcome, never stale, PLAN §7.4). */
function wireDateEdit(metaEl, doc) {
const createdBadge = metaEl.querySelector(".doc-created");
if (!createdBadge) return;
/* Edit affordance (admin-only — the public badge row keeps exactly
* task 08's shape: no button, no wiring). setAttribute, never
* innerHTML (the document-derived pair is user-storable). */
const editBtn = document.createElement("button");
editBtn.type = "button";
editBtn.className = "doc-date-edit";
editBtn.textContent = "Edit date";
editBtn.setAttribute(
"aria-label",
`Edit creation date: ${doc.source}/${doc.path}`,
);
createdBadge.insertAdjacentElement("afterend", editBtn);
/* Editor parts (built once; the input is rebuilt on every open so
* it always starts from the CURRENT stored date). */
let input = null;
const box = document.createElement("span");
box.className = "doc-date-editor";
const saveBtn = document.createElement("button");
saveBtn.type = "button";
saveBtn.className = "doc-date-save";
saveBtn.textContent = "Save";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
cancelBtn.className = "doc-date-cancel";
cancelBtn.textContent = "Cancel";
const revertBtn = document.createElement("button");
revertBtn.type = "button";
revertBtn.className = "doc-date-revert";
revertBtn.textContent = "Revert to sync";
const status = document.createElement("span");
status.className = "doc-date-status";
status.setAttribute("role", "status");
status.setAttribute("aria-live", "polite");
const errorLine = document.createElement("span");
errorLine.className = "doc-date-error";
errorLine.setAttribute("role", "alert");
errorLine.setAttribute("aria-live", "assertive");
/* The stored date as a native date-input value: the UTC date part
* of the stored ISO timestamp (D3 stores UTC timestamptz). */
function storedValue() {
return new Date(doc.created_at).toISOString().slice(0, 10);
}
/* One PATCH at a time (PLAN §7.4): the controls disable
* IMMEDIATELY on submit; Save ADDITIONALLY disables on an EMPTY
* input (the explicit Revert is the only clear path — no
* accidental wipes). */
function setControlsLocked(locked) {
if (input) input.disabled = locked;
saveBtn.disabled = locked || input.value === "";
cancelBtn.disabled = locked;
revertBtn.disabled = locked;
}
/* Back to the display state: the badge row + the Edit button (the
* task-08 shape restored) and focus back on the opener. Idempotent
* (a stale success-beat timer may call it after a Cancel — the
* re-insert is a same-position move and the box is already out).
* The last announcement stays readable a short beat because the
* caller defers the call (the phase-57 "confirmation stays
* readable" precedent). */
function closeEditor() {
editBtn.hidden = false;
createdBadge.insertAdjacentElement("afterend", editBtn);
box.remove();
editBtn.focus();
}
async function saveDate(dateValue) {
setControlsLocked(true); // one PATCH at a time (never stale)
errorLine.textContent = "";
try {
const r = await fetch("/api/documents/date", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
source: doc.source,
path: doc.path,
date: dateValue,
}),
});
if (!r.ok) {
// The server detail into the role=alert line (the canned
// retry copy on a non-JSON body); the input reverts to the
// stored date; the controls re-enable in the finally. The
// editor stays open — the user's attempt is visible.
errorLine.textContent = await apiDetail(
r,
"Couldn't save the date — try again.",
);
input.value = storedValue();
return;
}
const res = await r.json();
// Response-driven: the badge shows exactly what the server
// stored (never the input's optimistic value), and the doc
// object syncs so a re-open prefills the CURRENT date.
doc.created_at = res.created_at;
doc.created_at_manual = res.created_at_manual;
createdBadge.textContent = `Created ${fmtDate(res.created_at)}`;
createdBadge.setAttribute("title", res.created_at);
// The confirmation lands AFTER the badge update (the phase-89
// last-announce order), then the row collapses back to the
// task-08 shape a short beat later (the announcement stays
// readable — the status line leaves with the box).
status.textContent =
dateValue === null
? "Reverted to sync-managed date."
: `Date saved for ${doc.source}/${doc.path}.`;
setTimeout(closeEditor, 2000);
} catch {
// Network failure: the canned retry copy (the phase-55
// neutral shape), the input reverts to the stored date, the
// editor stays open.
errorLine.textContent = "Couldn't save the date — try again.";
input.value = storedValue();
} finally {
setControlsLocked(false);
}
}
function openEditor() {
input = document.createElement("input");
input.type = "date";
input.className = "doc-date-input";
input.setAttribute("aria-label", "Document creation date");
input.value = storedValue(); // value, never innerHTML
input.addEventListener("input", () => {
// An EMPTY input can't clear: Save disables (the explicit
// Revert below is the only clear path — no accidental wipes).
// The locked state owns the controls while a PATCH is in
// flight (the input is disabled then, so this can't race it).
if (!input.disabled) saveBtn.disabled = input.value === "";
});
status.textContent = "";
errorLine.textContent = "";
box.replaceChildren(input, saveBtn, cancelBtn, revertBtn, status, errorLine);
editBtn.hidden = true;
createdBadge.insertAdjacentElement("afterend", box);
setControlsLocked(false);
input.focus();
}
editBtn.addEventListener("click", openEditor);
saveBtn.addEventListener("click", () => {
// Save is only ever enabled with a NON-empty input (the empty
// state disables it) — the clear path is the explicit Revert.
void saveDate(input.value);
});
cancelBtn.addEventListener("click", () => {
// Cancel: back to the display state, no PATCH (the stored value
// is untouched — the badge was never mutated).
status.textContent = "";
errorLine.textContent = "";
closeEditor();
});
revertBtn.addEventListener("click", () => {
void saveDate(null); // the D7 CLEAR: { source, path, date: null }
});
}
/* ---------- /document.html page (phases 10/13/19) ----------
* Phase 26: viewer-page-specific — see the import-safety note in the
* header. The guard is #doc-title: it exists only on this page, so the