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:
@@ -39,6 +39,12 @@ phase-124 ``show_image`` clause through one mechanism, and every build
|
||||
the flags do not select stays byte-identical (the constant's own sha pin
|
||||
lives in :mod:`tests.unit.test_prompt_lock`).
|
||||
|
||||
And the phase-191 source names (task 05, D4): a ``<document>`` block can
|
||||
carry the owner's label row as the ADDITIVE ``name`` / ``label``
|
||||
attributes (order ``source`` → ``name?`` → ``label?`` → ``path`` →
|
||||
``title`` → ``date``, XML-escaped values), with no map / no entry for the
|
||||
source byte-identical to pre-phase (the kill pin).
|
||||
|
||||
And the phase-119 cite discipline (task 04, LOCKED A5): the intro's
|
||||
final sentence ("cite the document(s) you used, by path", phase 118)
|
||||
is REPLACED — the HIGH prompt carries the discipline sentence exactly
|
||||
@@ -65,6 +71,7 @@ from app.rag.prompts import (
|
||||
SHOW_TEXT_CLAUSE,
|
||||
SUGGEST_INTRO,
|
||||
TOOLS_SECTION,
|
||||
_attr,
|
||||
_base,
|
||||
_document_body,
|
||||
_extract_context_line,
|
||||
@@ -74,6 +81,7 @@ from app.rag.prompts import (
|
||||
build_steering_section,
|
||||
)
|
||||
from app.rag.retriever import TRUNCATION_MARKER
|
||||
from app.rag.source_labels import SourceLabelView
|
||||
|
||||
#: A small multi-line outline standing in for the lite-generated one.
|
||||
OVERVIEW = "- Homelab\n - Kubernetes (k3s)\n- Deployments\n - Borg backups"
|
||||
@@ -1944,3 +1952,204 @@ def test_high_prompt_pdf_image_doc_block_exact_shape() -> None:
|
||||
assert prompt.index(line) < prompt.index("A diagram of the cluster.")
|
||||
assert block_start < prompt.index(line) < prompt.index("</document>")
|
||||
assert "The second page of the guide" not in prompt
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 191 (D4, task 05): the ``<document>`` block learns the owner's name
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
#: The owner's display name for the fixture source (phase 191 D1 — an
|
||||
#: ALIAS beside the derived name, never a replacement for it).
|
||||
GIT_SERVER = "the git server"
|
||||
|
||||
#: The owner's one-line description of the same source.
|
||||
FORGE_LABEL = "self-hosted forge + Gitea"
|
||||
|
||||
|
||||
def _label_view(
|
||||
source: str = "Homelab",
|
||||
display_name: str | None = None,
|
||||
label: str | None = None,
|
||||
tags: tuple[str, ...] = (),
|
||||
) -> SourceLabelView:
|
||||
"""One label-row view (task 01's frozen dataclass) for the pins."""
|
||||
return SourceLabelView(
|
||||
source=source,
|
||||
display_name=display_name,
|
||||
label=label,
|
||||
tags=tags,
|
||||
)
|
||||
|
||||
|
||||
def _block_head(prompt: str) -> str:
|
||||
"""The opening tag of the FIRST ``<document>`` block (attribute
|
||||
order/escaping pins read it whole, so nothing is asserted loosely)."""
|
||||
start = prompt.index("<document ")
|
||||
return prompt[start : prompt.index(">", start) + 1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"names",
|
||||
[
|
||||
None,
|
||||
{},
|
||||
{"Other": _label_view("Other", display_name=GIT_SERVER, label=FORGE_LABEL)},
|
||||
],
|
||||
ids=["names=None", "names={}", "no-entry-for-the-source"],
|
||||
)
|
||||
def test_high_prompt_without_a_label_entry_is_byte_identical_to_pre_phase(
|
||||
names: dict[str, SourceLabelView] | None,
|
||||
) -> None:
|
||||
"""THE kill pin (phase 191 D4): no map, an empty map and a map with no
|
||||
entry for the document's source each build a prompt byte-identical to
|
||||
the pre-phase build — no ``name``, no ``label``, no stray space."""
|
||||
doc = _doc(
|
||||
"kubernetes.md",
|
||||
"Talos Linux on three nodes.",
|
||||
"Kubernetes Homelab Cluster",
|
||||
summary="A Talos Linux cluster on three nodes.",
|
||||
)
|
||||
pre = build_high_prompt([doc])
|
||||
assert build_high_prompt([doc], names=names) == pre
|
||||
assert ' name="' not in pre and ' label="' not in pre
|
||||
|
||||
|
||||
def test_no_label_block_keeps_the_exact_pre_phase_head() -> None:
|
||||
"""The block head for a label-less source, byte for byte (the shape the
|
||||
phase-106/118/125 pins rely on: ``source`` → ``path`` → ``title`` →
|
||||
``date``, single-spaced)."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes Homelab Cluster", summary="S.")
|
||||
assert _block_head(build_high_prompt([doc], names={})) == (
|
||||
'<document source="Homelab" path="kubernetes.md" '
|
||||
'title="Kubernetes Homelab Cluster" date="2024-06-15">'
|
||||
)
|
||||
|
||||
|
||||
def test_document_block_name_attribute_sits_immediately_after_source() -> None:
|
||||
"""A ``display_name``-only row: exactly `` name="…"`` added, directly
|
||||
after ``source="…"`` and before ``path`` (the attribute-order pin)."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
prompt = build_high_prompt([doc], names={"Homelab": _label_view(display_name=GIT_SERVER)})
|
||||
assert _block_head(prompt) == (
|
||||
'<document source="Homelab" name="the git server" path="kubernetes.md" '
|
||||
'title="Kubernetes" date="2024-06-15">'
|
||||
)
|
||||
assert ' label="' not in prompt
|
||||
|
||||
|
||||
def test_document_block_label_only_view_adds_exactly_the_label_attribute() -> None:
|
||||
"""A ``label``-only row gains ONE attribute — an unset field adds
|
||||
NOTHING (never an empty ``name=""``)."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
prompt = build_high_prompt([doc], names={"Homelab": _label_view(label=FORGE_LABEL)})
|
||||
assert _block_head(prompt) == (
|
||||
'<document source="Homelab" label="self-hosted forge + Gitea" '
|
||||
'path="kubernetes.md" title="Kubernetes" date="2024-06-15">'
|
||||
)
|
||||
assert ' name="' not in prompt
|
||||
|
||||
|
||||
def test_document_block_row_with_neither_field_is_byte_identical() -> None:
|
||||
"""A row that exists but carries neither name nor label (the owner
|
||||
saved only tags) leaves the block untouched — tags never render in the
|
||||
block (they are a retrieval input, D2/D3)."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
pre = build_high_prompt([doc])
|
||||
names = {"Homelab": _label_view(tags=("gitea", "forge"))}
|
||||
built = build_high_prompt([doc], names=names)
|
||||
assert built == pre
|
||||
assert "gitea" not in built
|
||||
|
||||
|
||||
def test_document_block_carries_both_attributes_in_the_locked_order() -> None:
|
||||
"""Both fields set → both attributes, in the fixed order
|
||||
``source`` → ``name`` → ``label`` → ``path`` → ``title`` → ``date``."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
names = {"Homelab": _label_view(display_name=GIT_SERVER, label=FORGE_LABEL)}
|
||||
head = _block_head(build_high_prompt([doc], names=names))
|
||||
order = ['source="', 'name="', 'label="', 'path="', 'title="', 'date="']
|
||||
positions = [head.index(attr) for attr in order]
|
||||
assert positions == sorted(positions)
|
||||
assert head == (
|
||||
'<document source="Homelab" name="the git server" '
|
||||
'label="self-hosted forge + Gitea" path="kubernetes.md" '
|
||||
'title="Kubernetes" date="2024-06-15">'
|
||||
)
|
||||
|
||||
|
||||
def test_document_block_name_and_label_are_xml_escaped() -> None:
|
||||
"""The escaping pin (task 05 work item 1): a ``"`` in owner text renders
|
||||
as ``"`` so the attribute cannot break; ``&``/``<`` fold too."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
names = {
|
||||
"Homelab": _label_view(
|
||||
display_name='Reese "forge" & <co>',
|
||||
label='say "hi"',
|
||||
)
|
||||
}
|
||||
prompt = build_high_prompt([doc], names=names)
|
||||
assert ' name="Reese "forge" & <co>"' in prompt
|
||||
assert ' label="say "hi""' in prompt
|
||||
# The unescaped form never reaches the wire: the attribute stays closed.
|
||||
assert ' name="Reese "forge"' not in prompt
|
||||
head = _block_head(prompt)
|
||||
assert head.count('"') % 2 == 0, "an unescaped quote would break the attribute"
|
||||
|
||||
|
||||
def test_attribute_escaping_helper_covers_the_xml_five() -> None:
|
||||
"""``_attr`` is the ONE escaper for the new attribute values: the XML
|
||||
specials go, the single quote stays (the delimiter is the double
|
||||
quote, and ``Reese's`` must stay readable to the model)."""
|
||||
assert _attr('a & b < c > d " e') == "a & b < c > d " e"
|
||||
assert _attr("Reese's forge") == "Reese's forge"
|
||||
|
||||
|
||||
def test_names_apply_per_document_source_and_never_touch_the_body() -> None:
|
||||
"""Only the blocks whose source has an entry gain the attribute; the
|
||||
summary body (and the other source's block) are untouched."""
|
||||
named = _doc("a.md", "CONTENT_A", "Title A", summary="SUMMARY A.")
|
||||
other = _doc("b.md", "CONTENT_B", "Title B", summary="SUMMARY B.")
|
||||
other.source = "Other"
|
||||
prompt = build_high_prompt(
|
||||
[named, other],
|
||||
names={"Homelab": _label_view(display_name=GIT_SERVER)},
|
||||
)
|
||||
assert 'source="Homelab" name="the git server" path="a.md"' in prompt
|
||||
assert 'source="Other" path="b.md"' in prompt
|
||||
assert prompt.count(GIT_SERVER) == 1, "the name rides the attribute, nothing else"
|
||||
assert "SUMMARY A." in prompt and "SUMMARY B." in prompt
|
||||
|
||||
|
||||
def test_names_on_an_empty_documents_build_change_nothing() -> None:
|
||||
"""No blocks → nothing to annotate: the honest "(no documents
|
||||
matched…)" build is byte-identical with a label row in hand."""
|
||||
names = {"Homelab": _label_view(display_name=GIT_SERVER, label=FORGE_LABEL)}
|
||||
assert build_high_prompt([], names=names) == build_high_prompt([])
|
||||
|
||||
|
||||
def test_names_compose_with_folder_lines_and_display_clauses() -> None:
|
||||
"""The additive attributes ride alongside the phase-119 folder lines and
|
||||
the phase-124/182 display clauses — the section order and the intro are
|
||||
untouched (the folder lines stay plain, no new markup)."""
|
||||
doc = _doc("kubernetes.md", "CONTENT", "Kubernetes", summary="S.")
|
||||
names = {"Homelab": _label_view(display_name=GIT_SERVER)}
|
||||
prompt = build_high_prompt(
|
||||
[doc],
|
||||
folder_lines=["docs/gitea.md — Gitea install"],
|
||||
show_image=True,
|
||||
show_text=True,
|
||||
names=names,
|
||||
)
|
||||
assert 'source="Homelab" name="the git server" path="kubernetes.md"' in prompt
|
||||
assert prompt.index(SUGGEST_INTRO) < prompt.index("docs/gitea.md — Gitea install")
|
||||
assert prompt.index("docs/gitea.md — Gitea install") < prompt.index("<document ")
|
||||
assert prompt.endswith(TOOLS_SECTION[: -len("</tools>")]
|
||||
+ SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>")
|
||||
|
||||
|
||||
def test_deflect_prompt_has_no_name_or_label_surface() -> None:
|
||||
"""D4 touches the suggested-tier block only: the deflected prompt is
|
||||
byte-identical (it carries no document blocks to annotate)."""
|
||||
prompt = build_deflect_prompt(["Kubernetes Homelab Cluster"])
|
||||
assert ' name="' not in prompt and ' label="' not in prompt
|
||||
assert GIT_SERVER not in prompt
|
||||
|
||||
Reference in New Issue
Block a user