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
+76 -2
View File
@@ -100,16 +100,32 @@ owner decision). The deterministic lever is the honesty gate itself
(A8 revised 2026-09-14: an FTS hit flips HIGH only when
``best_cosine >= lexical_support_floor``, so the misleading docs are
no longer injected — see :mod:`app.api.chat`).
Source names, labels and tags (phase 191, D4): a ``<document>`` block can
carry the owner's label row for its source as two ADDITIVE attributes —
``name="…"`` (the source's ``display_name``) immediately after
``source="…"``, then ``label="…"`` (the one-line description) — so the
model learns the name→docs mapping ("the git server" is a source with
these documents). The attribute order is fixed
(``source`` → ``name?`` → ``label?`` → ``path`` → ``title`` → ``date``),
an unset field adds NOTHING, and no *names* map / no entry for the
source leaves the block byte-identical to pre-phase (the alias contract,
D1: the identity attributes stay the DERIVED name — a display name never
replaces ``source`` or ``path``). Only the two new values are XML-escaped
(they are free-form owner text); the identity attributes keep their
pre-phase unescaped bytes.
"""
from __future__ import annotations
import re
from collections.abc import Sequence
from collections.abc import Mapping, Sequence
from typing import Any
from xml.sax.saxutils import escape
from app.config import Settings, get_settings
from app.models import Document
from app.rag.retriever import TRUNCATION_MARKER
from app.rag.source_labels import SourceLabelView
from app.schemas import HistoryTurn
#: PLAN §6 verbatim (line wrapping included); ``{relevance}`` is filled by
@@ -769,6 +785,47 @@ def _extract_context_line(image_context: str) -> str | None:
)
def _attr(value: str) -> str:
"""XML-escape one attribute value (phase 191, D4) — ``&``/``<``/``>``
and the double quote (``&quot;``), the delimiter the block uses.
Applied ONLY to the two additive label attributes (``name`` /
``label``): they hold free-form OWNER text where a ``"`` is realistic
and must not break the attribute. The pre-existing identity
attributes (``source`` / ``path`` / ``title`` / ``date``) deliberately
keep their unescaped pre-phase bytes — escaping them here would move
every label-less block, which the phase's byte-identity criterion
forbids.
"""
return escape(value, {'"': "&quot;"})
def _source_label_attrs(
doc: Document, names: Mapping[str, SourceLabelView] | None
) -> str:
"""The additive `` name="…"`` / `` label="…"`` fragment of one
``<document>`` block (phase 191, D4) — the empty string unless the
source has a label row with that field set.
``None`` / an empty map / no entry for ``doc.source`` / a row with
neither field set all render ``""`` — the block is then byte-identical
to pre-phase, and a label-only row adds exactly the one ``label``
attribute (an unset field adds NOTHING, it is never an empty
attribute).
"""
if not names:
return ""
view = names.get(doc.source)
if view is None:
return ""
attrs = ""
if view.display_name:
attrs += f' name="{_attr(view.display_name)}"'
if view.label:
attrs += f' label="{_attr(view.label)}"'
return attrs
def _image_context_line(doc: Document) -> str | None:
"""The phase-125 context line for one suggested document (task 05,
LOCKED A4) — ``None`` in the common case (EVERY text doc, and every
@@ -832,6 +889,7 @@ def build_high_prompt(
folder_lines: Sequence[str] = (),
show_image: bool = False,
show_text: bool = False,
names: Mapping[str, SourceLabelView] | None = None,
) -> str:
"""Grounded turn: locked persona (+ steering, + KB overview) + the
top-ranked documents' SUMMARY blocks + the ``<tools>`` instructions
@@ -917,6 +975,21 @@ def build_high_prompt(
(:func:`_document_body`; the NULL/whitespace-summary preview
fallback is LOCKED A5; an enriched image doc's summary leads with
the phase-125 one-line context line, LOCKED A4).
*names* (phase 191, D4): the source-name → :class:`SourceLabelView`
map of the owner's label rows (the SAME single ``all_labels`` read
that feeds the task-04 selection boost — one query per turn). When a
doc's source has a row, the block head gains `` name="{display_name}"``
immediately after ``source="…"`` and `` label="{label}"`` after that
(the fixed order ``source`` → ``name?`` → ``label?`` → ``path`` →
``title`` → ``date``); the two values are XML-escaped
(:func:`_attr`), the identity attributes are not. An unset field adds
NOTHING, so a label-only row gains exactly the one attribute, and
``None`` / ``{}`` / no entry for the source keeps the block
byte-identical to pre-phase (the default, and the kill pin in
:mod:`tests.unit.test_prompts`). This is surfacing only — the alias
contract (D1) keeps ``source`` / ``path`` as the document identity
the tools, citations and locators use.
"""
# Phase 106 (D5): every document the model sees carries its
# creation date — the block's ``date`` attribute (the row's
@@ -926,7 +999,8 @@ def build_high_prompt(
# stays out of the prompt (the capped ``read`` tool is the only
# full-text path).
blocks = [
f'<document source="{doc.source}" path="{doc.path}" title="{doc.title}" '
f'<document source="{doc.source}"{_source_label_attrs(doc, names)}'
f' path="{doc.path}" title="{doc.title}" '
f'date="{doc.created_at:%Y-%m-%d}">\n'
f"{_document_body(doc)}\n"
"</document>"