phase: 182_verbatim_show_text
Build and Push Containers / build-and-push-app (push) Successful in 1m46s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 182 — final verification pass (all 8 tasks already complete; this was verification + docs + one defect fix).**

- Verified: `app/rag/verbatim.py` (locator grammar, `splitlines()` slice, line-boundary cap, `resolve_answer_blocks`), `SHOW_TEXT_TOOL` + `shown_texts`, `SHOW_TEXT_CLAUSE`, `done.answer_text` (9 fields, key dropped when empty) + `ChatMessageRecord.answer_text` (≤50, 600 000-char cap counted in the 8 M budget), `verbatim.js` + 4 `{verbatim:true}` renderers, `Containerfile` minify step, `.env.example` 3 vars.
- `uv run python -c "…answer_text, show_text_max_chars, show_text_max_blocks"` → `True 12000 5`; each negative refuses startup naming its field; `0` accepted as inert ✅
- `bash .agents/validate.sh` → **validation OK**: 4320 unit+integration tests green, `TOTAL 6849 17 → 99%` (gate app/ >90% OK), `ruff check .` "All checks passed!", `pyright` 0 errors (re-run after my edits, 08:28).
- E2E, each in isolation `--no-cov`: `test_verbatim_show_text.py` 13 passed (panel bytes == `documents.content`, server caption, clipboard == server string, `/document.html?source=…&path=…`, honest unresolved state, reload+share, `show_image` regression, flag-off app) ✅; green: `test_answer_images` `test_harness_aligned_tools` `test_agent_document_tools` `test_markdown_tables` `test_markdown_inline_links_images` `test_svg_document_preview` `test_smoke` `test_document_viewer` `test_markdown_blockquotes` `test_chat_persistence` `test_thinking_timeline` `test_chat_rag` `test_settings_mobile_value_width`.
- **Defect found & fixed (pre-existing, not phase 182):** `tests/e2e/test_admin_settings_panel.py::test_admin_sees_groups_and_source_badges` failed — its whole-page `assert "reese" not in page.content()` tripped on the shell's onboarding chips, which follow the newest saved chats in the one shared E2E DB (a leftover `reeselink.json` question from a tool suite). Scoped the strict sweep to `.settings-shell` and kept a whole-page `reese:reese` credential needle → 12 passed with the leftovers still present.
- Docs: README fixed 2 inconsistencies — Quick Start omitted the **required** `BOR_ADMIN_PASSWORD`/`BOR_SESSION_SECRET` (the app fail-loud refuses boot) and the broken anchor `#chat--` → `#chat-`; full re-read of README (1816 lines) + AGENTS confirmed the phase-182 feature/settings/log/UI-contract text matches the code, and every `.env.example` var + every `Settings` field is documented.
- Notable: kept `resolve_answer_blocks` synchronous on the SSE terminal path (bounded indexed SELECTs, matching the existing `query_log` precedent and the phase's documented fence-lookup bound); no live-infra changes made.
- Next pending phase: none — `todo/` holds only 182 (its 8 task files are in `complete/`; the harness commits + moves `00_phase.md`).
This commit is contained in:
2026-10-15 08:33:12 -04:00
parent 814a400e6d
commit b34b8ca672
104 changed files with 10852 additions and 203 deletions
+175
View File
@@ -30,6 +30,15 @@ discipline rules stay byte-identical): the teaching refusals in
:mod:`app.rag.agent` re-state the same contract; the ``<tools>``
marker keying (HIGH only) is unchanged.
And the phase-182 ``show_text`` clause (task 04, LOCKED A1–A3/A6): the
verbatim-display teaching appended INSIDE ``<tools>`` when
``BOR_ANSWER_TEXT`` is on — the user copies from what is DISPLAYED, never
from the model's retype, and the inline ```bor <source/path> [range]```
fence places a server-resolved block. The clause composes with the
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-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
@@ -40,6 +49,7 @@ the LOW prompt and ``PERSONA`` / ``TOOLS_SECTION`` stay byte-identical
from __future__ import annotations
import inspect
import re
import uuid
from datetime import UTC, datetime
@@ -52,6 +62,7 @@ from app.rag.prompts import (
PERSONA,
SEARCH_DISCIPLINE_RULE,
SHOW_IMAGE_CLAUSE,
SHOW_TEXT_CLAUSE,
SUGGEST_INTRO,
TOOLS_SECTION,
_base,
@@ -1281,6 +1292,170 @@ def test_show_image_clause_is_last_for_every_high_shape() -> None:
assert SHOW_IMAGE_CLAUSE not in off # the toggle-off twin carries none of it
# ---------- phase 182 (task 04, LOCKED A1–A3/A6): the show_text clause ----------
#
# The clause is the fix for the retype bug: nothing in the pre-phase prompt
# ever said the user copies from what is DISPLAYED, so the model retyped
# files from context and every retype differed from the stored bytes by a
# character. Pinned three ways: the locked sentence order (the behaviour
# the E2E and the persona depend on), the byte-identity of every build the
# flag does NOT select, and the composition of the two display clauses
# through the one "section minus its final </tools>" mechanism.
#: The clause's sentence anchors, in the locked order (task 04's work
#: list): display contract → when to call → do-NOT-retype → the ```bor```
#: placement syntax → the numbers belong to the server → the failure rule
#: → the image pointer.
SHOW_TEXT_ANCHORS: tuple[str, ...] = (
"displays part of a document to the user EXACTLY as stored",
"the user copies from what is displayed, not from your prose",
"Call it whenever the user wants to SEE, COPY or RUN a file",
"a quadlet, a config, a script, a unit file",
"do NOT repeat the displayed lines in your answer",
"may differ from the file by a character, and the user cannot tell",
"write a locator line on its own: three backticks followed by `bor`",
"the combined `source/path`, and optionally the line range (` 12-40`)",
"Leave its body EMPTY — the app replaces it with the exact stored text",
"Never type the file's content inside a `bor` block",
"Never guess line numbers — use the numbers `grep` and `read` reported",
"OMIT the range and display the whole document",
"the numbers you display are the server's, never yours",
"If `show_text` refuses, say what you could not display",
"do not reconstruct it from memory",
"For an image use `show_image` instead",
)
def test_show_text_clause_carries_every_locked_rule_in_order() -> None:
"""Every sentence of the contract is present, in the locked order, and
the clause is a clause — one paragraph, no ``<tools>`` markup of its
own (the ``SHOW_IMAGE_CLAUSE`` precedent)."""
positions = [SHOW_TEXT_CLAUSE.index(anchor) for anchor in SHOW_TEXT_ANCHORS]
assert positions == sorted(positions), SHOW_TEXT_ANCHORS
assert "\n" not in SHOW_TEXT_CLAUSE
assert SHOW_TEXT_CLAUSE.startswith("`show_text`")
assert SHOW_TEXT_CLAUSE.endswith("instead.")
assert "<tools>" not in SHOW_TEXT_CLAUSE and "</tools>" not in SHOW_TEXT_CLAUSE
# The fence language is named as a token, never as a stray substring.
assert re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", SHOW_TEXT_CLAUSE)
def test_high_prompt_default_stays_byte_identical_without_show_text() -> None:
"""Completion pin: ``show_text`` defaults to ``False`` — the default
build and the explicit ``show_text=False`` are byte-identical to each
other and end with the untouched ``TOOLS_SECTION``, with no trace of
the tool or the fence language (the sha pins in
:mod:`tests.unit.test_prompt_lock` pass UNMODIFIED)."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
plain = build_high_prompt([doc])
assert build_high_prompt([doc], show_text=False) == plain
assert plain.endswith(TOOLS_SECTION)
assert SHOW_TEXT_CLAUSE not in plain
assert "show_text" not in plain
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", plain)
empty = build_high_prompt([])
assert build_high_prompt([], show_text=False) == empty
assert empty.endswith(TOOLS_SECTION)
assert SHOW_TEXT_CLAUSE not in empty
def test_high_prompt_show_text_clause_lands_inside_tools_after_the_copy() -> None:
"""``show_text=True`` builds EXACTLY the toggle-off prompt with its
final ``</tools>`` replaced by the clause (on its own line, after the
existing three-tool copy) + the closing tag — the three-tool copy is
byte-identical, ``TOOLS_SECTION`` is unedited, and the clause is the
last thing the model reads."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
off = build_high_prompt([doc])
on = build_high_prompt([doc], show_text=True)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
clause_start = on.index(SHOW_TEXT_CLAUSE)
assert on[:clause_start] == off[: -len("</tools>")]
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
assert "Answer as soon as you have what you need." in on[:clause_start]
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
assert on.index("Answer as soon as you have what you need.") < clause_start
assert clause_start < on.index("</tools>")
assert on.count(SHOW_TEXT_CLAUSE) == 1
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
def test_high_prompt_display_clauses_compose_independently() -> None:
"""The two display flags are independent (LOCKED A1): ``show_image``
only reproduces the phase-124 bytes exactly, ``show_text`` only is the
three-tool copy plus the text clause, both puts the image clause
FIRST and the text clause last, and neither is byte-identical to
pre-phase — no combination leaks the other's clause."""
doc = _doc("kubernetes.md", "Talos Linux on three nodes.", "Kubernetes Homelab Cluster")
neither = build_high_prompt([doc])
head = neither[: -len("</tools>")]
image = build_high_prompt([doc], show_image=True)
text = build_high_prompt([doc], show_text=True)
both = build_high_prompt([doc], show_image=True, show_text=True)
assert neither.endswith(TOOLS_SECTION)
assert image == head + SHOW_IMAGE_CLAUSE + "\n</tools>"
assert text == head + SHOW_TEXT_CLAUSE + "\n</tools>"
assert both == head + SHOW_IMAGE_CLAUSE + "\n\n" + SHOW_TEXT_CLAUSE + "\n</tools>"
assert SHOW_TEXT_CLAUSE not in image and SHOW_IMAGE_CLAUSE not in text
assert both.index(SHOW_IMAGE_CLAUSE) < both.index(SHOW_TEXT_CLAUSE)
for on in (image, text, both):
assert on[: on.index("<tools>")] == neither[: on.index("<tools>")]
assert on.count("<tools>") == 1 and on.count("</tools>") == 1
def test_high_prompt_empty_documents_show_text_clause_still_in_tools() -> None:
"""The "(no documents matched…)" body + ``show_text=True``: the clause
still lands inside ``<tools>`` — the grounded turn may still ``ls`` /
``grep`` its way to a file and display it."""
off = build_high_prompt([])
on = build_high_prompt([], show_text=True)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
assert "(no documents matched — do not invent specifics)" in on
assert on.index("<tools>") < on.index(SHOW_TEXT_CLAUSE) < on.index("</tools>")
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
def test_show_text_clause_is_last_for_every_high_shape() -> None:
"""Steering + KB overview + folder lines + documents all present: the
section order is UNCHANGED (``<relevance>`` → ``<knowledge_base>`` →
``<tuning>`` → ``<documents>`` → ``<tools>``) and the text clause is
still the last thing inside ``<tools>`` before the closing tag."""
doc = _doc("deploy/k8s.md", "CONTENT_SENTINEL", "K8s Deploy", summary="K8s deploy notes.")
lines = ["Homelab/deploy/: k8s.md (1 doc)"]
off = build_high_prompt([doc], notes=["be concise"], kb_overview=OVERVIEW, folder_lines=lines)
on = build_high_prompt(
[doc],
notes=["be concise"],
kb_overview=OVERVIEW,
folder_lines=lines,
show_text=True,
)
assert on == off[: -len("</tools>")] + SHOW_TEXT_CLAUSE + "\n</tools>"
clause_start = on.index(SHOW_TEXT_CLAUSE)
assert on[on.index("<tools>") : clause_start] == TOOLS_SECTION[: -len("</tools>")]
for section in ("<relevance>", "<knowledge_base>", "<tuning>", "<documents>"):
assert on.index(section) < on.index("<tools>")
assert on.index("K8s deploy notes.") < on.index("<tools>")
assert on.endswith(SHOW_TEXT_CLAUSE + "\n</tools>")
assert SHOW_TEXT_CLAUSE not in off
def test_show_text_clause_rides_only_the_high_prompt() -> None:
"""Neither the tool name nor the fence language ever reaches the
deflection prompt (phase 182's D9: a deflected turn has no tools, and
resolving a locator there would leak KB text through a turn that just
disclaimed it) — and ``build_deflect_prompt`` takes no such flag."""
assert "show_text" not in inspect.signature(build_deflect_prompt).parameters
for prompt in (
build_deflect_prompt(["T1", "T2"]),
build_deflect_prompt(["T1"], notes=["be concise"], kb_overview=OVERVIEW),
build_deflect_prompt([]),
):
assert SHOW_TEXT_CLAUSE not in prompt
assert "show_text" not in prompt
assert not re.search(r"(?<![A-Za-z0-9_])bor(?![A-Za-z0-9_])", prompt)
# ---------- phase 125 (task 05, LOCKED A4): the image context line ----------
# An enriched image doc (tasks 03/04: non-NULL ``image_context`` in the
# "Referenced in …" block shape) leads its ``<document>`` suggestion