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
+105 -7
View File
@@ -66,6 +66,20 @@ backstop). The ``DEFLECT_MODE`` marker and everything else in the
prompt stay put — the E2E mock LLM keys on the marker's *presence*,
not the wording, so that contract is unchanged.
Display-tool clauses (phase 124 ``show_image``; phase 182 ``show_text``):
the **HIGH** prompt's ``<tools>`` section can carry two extra clauses
before its closing tag, each appended only when its own setting is on —
:data:`SHOW_IMAGE_CLAUSE` when ``BOR_ANSWER_IMAGES`` (default OFF) and
:data:`SHOW_TEXT_CLAUSE` when ``BOR_ANSWER_TEXT`` (default ON). The text
clause teaches the two things the phase-182 incident needed said: the
user copies from what is DISPLAYED (so never retype a file), and the
inline `` ```bor <source/path> [range] ``` `` locator fence whose EMPTY
body the app replaces with the server-resolved bytes. Both clauses
compose through one mechanism (section minus its final tag + the present
clauses + the tag), so a build with neither clause is byte-identical to
pre-phase and a ``show_image``-only build reproduces the phase-124 bytes
exactly; the LOW prompt carries neither.
Disclosed-answer contract (phase 112, task 03; owner decision
2026-09-14, roadmap confirmation — option (iii) of the three the
2026-09-15 interactive deflection test raised): that test found the
@@ -388,6 +402,61 @@ SHOW_IMAGE_CLAUSE: str = (
"in text either way."
)
#: The ``show_text`` contract clause (phase 182, task 04, LOCKED A1–A3/A6) —
#: appended INSIDE the ``<tools>`` section of the HIGH prompt, after the
#: existing three-tool copy (and after :data:`SHOW_IMAGE_CLAUSE` when that
#: rides too), ONLY when :func:`build_high_prompt` is called with
#: ``show_text=True`` (the ``BOR_ANSWER_TEXT`` master switch, via
#: ``app.api.chat``'s grounded plan — the only call site that passes it).
#: It exists because the incident this phase fixes is a PROMPT gap, not a
#: tool gap: nothing ever told the model that the user copies from what is
#: DISPLAYED, so it retyped files out of context and every retype differed
#: from the stored bytes by a character. What each sentence pins, in order:
#: (1) the display contract — the tool shows the document EXACTLY as stored
#: and the copy source is the display, not the prose; (2) when to call it —
#: the user wants to SEE, COPY or RUN a file (the owner's quadlet / config /
#: script shapes) rather than read content out of the answer; (3) the do-
#: NOT-retype rule with its reason (anything the model types may differ by
#: one character and the user cannot tell — the phase's reason for
#: existing); (4) the placement syntax — the inline `` ```bor <source/path>
#: [range] ``` `` locator fence with an EMPTY body (D1/D7: a fence is a
#: resolution request, the app supplies the text, and D3 keeps the model's
#: body out of the DOM); (5) the honesty-about-numbers rule — line numbers
#: come from ``grep``/``read``, and when unsure the range is OMITTED
#: because the displayed extent is the server's (A2); (6) the failure rule
#: — refuse out loud, never reconstruct from memory (the persona's radical
#: honesty, D3's posture on the model side); (7) the image pointer
#: (LOCKED A6: images belong to ``show_image``, never silently converted).
#: ``TOOLS_SECTION`` itself stays BYTE-IDENTICAL (the sha pins in
#: :mod:`tests.unit.test_prompt_lock` hold); a ``show_text=False`` build
#: (the default — every pre-phase call site) is byte-identical to the
#: pre-task output, and a ``show_image=True, show_text=False`` build
#: reproduces the phase-124 bytes EXACTLY. The ``<tools>`` marker the E2E
#: mock keys on is untouched either way — both clauses ride on its
#: presence, not its wording. The LOW/deflection prompt never carries the
#: clause: a deflected turn has no tools, and phase 182's D9 forbids
#: resolving a locator on a turn that just disclaimed the knowledge base.
SHOW_TEXT_CLAUSE: str = (
"`show_text` 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 — instead of writing the content into "
"your answer yourself. After calling it, do NOT repeat the displayed "
"lines in your answer — describe them, explain them, point at them. "
"Anything you type yourself may differ from the file by a character, "
"and the user cannot tell. To put a displayed block exactly where you "
"want it in your answer, write a locator line on its own: three "
"backticks followed by `bor`, a space, 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. When you are not sure which lines, 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 history_to_messages(
history: Sequence[HistoryTurn],
@@ -762,6 +831,7 @@ def build_high_prompt(
kb_overview: str | None = None,
folder_lines: Sequence[str] = (),
show_image: bool = False,
show_text: bool = False,
) -> str:
"""Grounded turn: locked persona (+ steering, + KB overview) + the
top-ranked documents' SUMMARY blocks + the ``<tools>`` instructions
@@ -771,7 +841,8 @@ def build_high_prompt(
seeds SUMMARIES, never full texts — full text enters the context
only through the capped ``read`` tool; phase 119, LOCKED A4: the
suggested-folder context lines, below; phase 124, task 03, LOCKED
A5: the optional :data:`SHOW_IMAGE_CLAUSE`, below).
A5: the optional :data:`SHOW_IMAGE_CLAUSE`, below; phase 182, task 04,
LOCKED A1–A3: the optional :data:`SHOW_TEXT_CLAUSE`, below).
Section order: ``<relevance>`` → ``<knowledge_base>`` → ``<tuning>``
→ ``<documents>`` → ``<tools>``; empty steering/overview omit their
@@ -796,6 +867,22 @@ def build_high_prompt(
never carries the clause — a deflected turn has no tools and never
shows an image.
*show_text* (phase 182, task 04, LOCKED A1–A3): when true (the caller
passes the ``BOR_ANSWER_TEXT`` master switch — the same single grounded
call site), the :data:`SHOW_TEXT_CLAUSE` rides INSIDE the same
``<tools>`` section, AFTER :data:`SHOW_IMAGE_CLAUSE` when that is
present too and always before the closing tag. The two flags compose
independently through one mechanism (the built section is
``TOOLS_SECTION`` minus its final ``</tools>`` + the present clauses,
blank-line separated, + the closing tag): ``show_image`` only
reproduces the phase-124 bytes exactly, ``show_text`` only is the
three-tool copy plus the text clause, both is the image clause then
the text clause, neither is byte-identical to pre-phase. The default
``False`` keeps every pre-existing call byte-identical. The deflection
prompt NEVER carries it — a deflected turn has no tools, and phase
182's D9 forbids resolving a ``bor`` locator on a turn whose answer
just disclaimed the knowledge base.
When at least one block is present, the section leads with the
start-here :data:`SUGGEST_INTRO` line (before the first block — the
phase-15 ``_STEERING_INTRO`` / phase-31 ``_KB_INTRO`` precedent): the
@@ -861,13 +948,24 @@ def build_high_prompt(
for part in (build_kb_section(kb_overview or ""), build_steering_section(notes or [])):
if part:
prompt += "\n" + part
# Phase 124 (task 03, LOCKED A5) / phase 182 (task 04, LOCKED A1): the
# display-tool clauses ride inside <tools> — TOOLS_SECTION's text minus
# its final </tools>, the clauses offered for this build (each on its
# own line, blank-line separated, after the existing three-tool copy),
# then the closing tag. TOOLS_SECTION itself is unedited, the
# no-clause path keeps the exact pre-task concatenation, and a
# show_image-only build reproduces the phase-124 bytes exactly.
clauses: list[str] = []
if show_image:
# Phase 124 (task 03, LOCKED A5): the clause rides inside
# <tools> — TOOLS_SECTION's text minus its final </tools>, the
# clause (on its own line, after the existing three-tool copy),
# then the closing tag. TOOLS_SECTION itself is unedited and the
# show_image=False path keeps the exact pre-task concatenation.
tools_section = TOOLS_SECTION[: -len("</tools>")] + SHOW_IMAGE_CLAUSE + "\n</tools>"
clauses.append(SHOW_IMAGE_CLAUSE)
if show_text:
clauses.append(SHOW_TEXT_CLAUSE)
if clauses:
tools_section = (
TOOLS_SECTION[: -len("</tools>")]
+ "\n\n".join(clauses)
+ "\n</tools>"
)
else:
tools_section = TOOLS_SECTION
return prompt + "\n<documents>\n" + body + "\n</documents>\n" + tools_section