phase: 191_source_name_labels_tags
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:
+76
-2
@@ -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 (``"``), 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, {'"': """})
|
||||
|
||||
|
||||
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>"
|
||||
|
||||
Reference in New Issue
Block a user