phase: 191_source_name_labels_tags
Build and Push Containers / build-and-push-app (push) Successful in 7m24s
Build and Push Containers / build-and-push-db (push) Successful in 36s

All gates re-verified against a stable tree. Report:

**Verified (all 6 tasks in `complete/`; this was the final pass)**
- `SourceLabel`+alembic `0030` (dev DB `alembic current` = `0030 (head)`, table present, 0 rows); admin-only `GET/PUT/DELETE /api/source-labels` (`require_admin` router-wide, `resolve_source_name`, partial-update caps/404s); Sources-page "Names, labels and tags" card (read-only derived-name `<code>`, per-row `saveInFlight`, 404 note that survives the settle, chips reuse `.git-source-hidden-count`); `question_name_hits` D2 rule; ONE `all_labels` read per turn in `chat.py` feeding both the `source_name_hit` flag and the prompt surfaces; `source_name_hit_bonus=0.005` in `_selection_order`; ` name=`/` label=` in `build_high_prompt` + `build_overview_prompt(names=…)`.
- **Completion criteria:** E2E `tests/e2e/test_source_name_labels.py` ✅ 7 passed (arms a–e: editor all kinds, control/named question, rename-never-breaks-identity, 404 note, reset) · full suite ✅ · coverage ✅ · README/AGENTS ✅ · phase dir in `complete/` ✅ · commit left to the harness.
- **Gates:** `uv run pytest --cov=app --cov-report=term-missing` → **exit 0, TOTAL 7160 stmts / 16 missed = 99%** (>90%) · `uv run pytest tests/e2e/test_source_name_labels.py -v --no-cov` → **7 passed, exit 0** · regression E2E isolated, exit 0: `test_document_viewer`, `test_admin_auth`, `test_api_tokens`, `test_git_sources_admin`, `test_kb_tree`, `test_local_directory_sources` · `uv run ruff check .` → All checks passed · `uv run pyright` → 0 errors.
- **Battery (task 04 criterion):** `uv run python -m scripts.eval_retrieval --from-file tests/fixtures/retrieval_battery.txt` at default vs `BOR_SOURCE_NAME_HIT_BONUS=0` → **`diff` empty (line-for-line identical)**, matching the recorded arms (dev KB is empty, so every question deflects; the observable boost is unit/E2E-pinned).
- **Documentation:** README — fixed the `/git-sources.html` rows (nav label is **Sources**) and cross-linked the phase-191 card from that section; full audit otherwise clean: all 86 `BOR_*` settings documented with defaults matching `app/config.py`, 0 broken file/dir references and 0 broken anchors in README+AGENTS, AGENTS rule 17 re-checked line-by-line against the code.
- **Notable / deviations:** (1) a **concurrent pipeline process** (`phased-execution/progress.mjs` for `191_…_00_phase`) was mutating this same working tree mid-pass — it rewrote `tests/e2e/test_source_name_labels.py` (11→7 tests), `frontend/assets/git-sources.js`, `tests/unit/test_source_labels_editor_ui.py` and `AGENTS.md` at 05:27; every gate above was (re-)run after those edits with the tree no source file newer than the green full run. (2) The task-06 report/phase text claims regression suites `test_chat_token_gate.py`, `test_git_sources_page.py`, `test_rag_browser.py` are green — **those files have never existed in git history** (those runs exit 4); I ran the real equivalents listed above instead (phase files are not mine to edit). (3) **No live changes**: the VM DB already has `source_labels` (0 rows) but the deployed app predates this uncommitted code, so deploying `/api/source-labels` to the live service is the owner's post-commit step, not part of this phase's criteria.
- **Next pending phase:** none — `191_source_name_labels_tags` is the only entry in `.agents/phases/todo/`.
This commit is contained in:
2026-10-19 06:13:32 -04:00
parent c6ffa050a6
commit 4f20c256d7
70 changed files with 7378 additions and 50 deletions
+93
View File
@@ -312,6 +312,99 @@
quoted cosine/lexical margins, so editing a note there moves the numbers in
the module docstring and the pins together.
17. **A source label is an alias, never an identity (phase 191):** the
`source_labels` row (alembic `0030`, PK = the DERIVED source name
exactly as `Document.source` carries it) holds the owner's
`display_name` / `label` / `tags`, and only
`app/api/source_labels.py` writes it — `GET /api/source-labels` (the
catalogue: the `effective_sources` rows of BOTH kinds — or the
`BOR_GIT_SOURCES` names while the table is empty — plus every indexed
document source no row derives, de-duplicated and sorted, ONE
`all_labels` read for the whole list), `PUT` (partial update: omitted
keeps, explicit `null` clears, `tags: []` clears) and `DELETE` (the
reset); `require_admin` router-wide, and every name resolution goes
through `app.rag.source_removal.resolve_source_name` — never
re-derive the basename here or the row keys nothing. A `PUT` for a
name that is neither a registry row's derived name nor an indexed
document source is 404 (no orphan label rows) and a validation
failure writes NOTHING. NEVER rename, resolve or join on the display
name: `Document.source`, the `source/path` read/grep/`show_text`
locators, the citations, the KB tree and the verbatim caption stay on
the derived name, so a rename cannot invalidate a saved chat, a
shared page or a locator — and the name reaches the model only
through the prompt/retrieval layers (tasks 04/05), never by replacing
the identity in a chip, a table or a caption. Pinned by
`tests/integration/test_source_labels_api.py` (the 403 gate for
anonymous AND token users, the git+local+upload union sorted and
de-duplicated at three SELECTs with no `documents.content` read, the
partial-update shape, every cap as the framework's 422 with the row
byte-identical, both 404 arms, the `ValueError` → 422 funnel, and the
"a rename writes nothing but the label row" arm).
The Sources-page editor (task 03) is the owner-visible half of the SAME
contract and must stay that way: its row list comes from
`GET /api/source-labels` (the catalogue union — never a re-derived
basename, never a join on the display name), the row's identity cell is a
read-only `<code>` of the DERIVED name (an input there WOULD be the
alias-contract bug), Save/Reset `PUT`/`DELETE` the per-source path and
re-read the ROW from that response (no second GET), the save state is
scoped PER ROW (`saveInFlight` — one request per row, the `finally` always
settles it, the 404 note names the arm, a collapse discards the draft) —
and a FAILED save's note SURVIVES that settle: `updateLabelRowUI` returns
before the dirty hint whenever the line carries `.is-error`, so
"That source no longer exists — reload the page." cannot be painted over
by "unsaved changes" in the tick that wrote it (the defect the task-06
story suite walked into); only the next edit (`clearLabelError`) or a
collapse clears a note, never the settle path),
and the Tags token input's chips reuse the phase-89 tag rule
(`.source-label-tag` sits in the `.git-source-hidden-count` rule group — no
bespoke tag-input CSS). Pinned by
`tests/unit/test_source_labels_editor_ui.py` and, through the real UI over
one git + one local + one uploaded source, by
`tests/e2e/test_source_name_labels.py` (task 06: the editor saves and
re-populates all three kinds, a question that USES the name moves that
source's document into the seeded `<documents>` tier with
`name`/`label` on the wire while the same row with a name-free question
moves nothing, the vanished-source 404 note is visible, Reset reverts the
tier, and the alias never appears in a locator, a nearby-doc link, the KB
tree or the viewer); the chat source chips, the RAG
source tree and the verbatim caption keep the derived name untouched.
The selection boost (task 04) is the retrieval half of the SAME
contract: ONE `all_labels` read per turn at the
retrieval boundary (`app/api/chat.py`, after `retrieve`, before the
first selection call) flags `RetrievedChunk.source_name_hit` on the
chunks whose `document.source` is a D2 hit (the pure
`question_name_hits` rule applied per view — a loop over the few
dozen chunks, never a per-chunk query; that ONE result is also what
task 05's prompt surfaces consume, so a turn issues one label query
and the standalone `question_name_hit_sources` wrapper never runs in
one), and `_selection_order` adds
`source_name_hit_bonus` (default 0.005; `0` = the byte-identical
kill switch; a negative value refuses startup) as a SECOND,
independent bonus — once per DOCUMENT, at most two bonuses stacked
with the phase-119 `name_hit_bonus` (bounded 0.010 on the fused
scale), SELECTION layer only (`score`/`cosine`/`fts_hit`/
`query_log.top_score` and the per-turn log line untouched). Pinned
by `tests/unit/test_retriever.py` (the kill-switch byte-identity,
the bounded two-bonus application, the `fuse` OR-merge, the three
selection helpers pulling the knob from settings).
The prompt surfacing (task 05) is the model-facing half of the SAME
read: `build_high_prompt(names=…)` gives the suggested `<document>`
block the ADDITIVE ` name="…"` (immediately after `source="…"`) and
` label="…"` attributes — the order is locked
(`source` → `name?` → `label?` → `path` → `title` → `date`), the two
values escape through the ONE `_attr` helper, and the identity
attributes keep their pre-phase unescaped bytes; `regenerate_overview`
derives the KB-overview INPUT lines' name map from its own ONE
`all_labels` read (`build_overview_prompt(names=…)` swaps the raw
source name for the display name in the lite model's input ONLY — the
stored outline and its deterministic `Sources: …` tail stay derived
name). An unset field adds NOTHING (never `name=""`), tags never
render, the LOW prompt never carries a name, and no map / no entry is
byte-identical to pre-phase on BOTH builders (the kill pins in
`tests/unit/test_prompts.py` and `tests/unit/test_overview.py`, which
also marker-pin the one-read-per-surface contract, as
`tests/unit/test_chat_gate.py` does on the chat call site).
## Quick reference
```bash
podman compose up -d db # start Postgres 17 + pgvector