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
+465
View File
@@ -0,0 +1,465 @@
"""Unit: the phase-182 verbatim-panel stylesheet contract (task 06).
The panel DOM is built by ``verbatim.js`` (task 07) and its streaming
placeholder by ``markdown.js`` (task 06); this module pins the CSS contract
that makes the phase readable, in the ``test_frontend_cluster_css.py`` /
``test_markdown_inline_links_images.py`` house pattern (no browser — the
browser-level evidence is the phase's Playwright suite, task 08):
* the phase-182 block EXISTS as one clearly-labelled region of
``frontend/assets/styles.css`` (BEGIN/END markers) and every selector the
contract names is styled there — ``.verbatim``, ``.verbatim-head``,
``.verbatim-caption``, ``.verbatim-badge``, ``.verbatim-body``,
``.verbatim-lines``, ``.verbatim-pending``, ``.verbatim-state``,
``.verbatim-unresolved``, ``.code-block``, ``.code-block-copy``,
``.verbatim-copy``;
* the panel is a BOUNDED scroll panel (PLAN §7.1): ``max-height`` in the
``min(…, 70vh)`` form + ``overflow: auto``, with the head row STICKY
(``position: sticky; top: 0``) on an opaque background so the caption
never scrolls away and rows never show through it;
* the code surface never wraps (phase 116 locked A1, the phase-165 prose/code
split): ``white-space: pre`` + ``overflow-wrap: normal`` + ``tab-size: 2``,
and the line-number gutter is ``user-select: none`` so a Copy/select never
drags numbers into the clipboard;
* text + color, never color alone (WCAG 1.4.1): the badge is a TEXT badge
with ``text-transform: none``, the unresolved state hides the badge, and
the ``.copied`` state exists beside the text swap task 07 performs;
* the whole block is PALETTE-VARIABLES ONLY (the phase-92 zero-literal /
monochrome invariant): no hex/rgb/hsl literal anywhere, and every
``var()`` is a declared ``:root`` token — so a light theme keeps this code
surface readable and no new hue enters the system;
* the ≤640px block lifts the Copy controls to the 44px touch floor, and
nothing in the block animates (so ``prefers-reduced-motion`` has nothing
to still — the rule is written as a guard so a future animation cannot be
added without its still).
"""
from __future__ import annotations
import re
from pathlib import Path
import pytest
STYLES_CSS = Path(__file__).resolve().parents[2] / "frontend" / "assets" / "styles.css"
#: The phase-182 region markers (the block is one contiguous, labelled
#: region, so "no literals in the phase-182 block" is a parseable claim).
BEGIN = "/* ---------- Verbatim panels (phase 182, task 06) — BEGIN ----------"
END = "/* ---------- Verbatim panels (phase 182, task 06) — END ---------- */"
#: Every selector the task's contract names.
PANEL_SELECTORS = (
".verbatim",
".verbatim-head",
".verbatim-caption",
".verbatim-badge",
".verbatim-body",
".verbatim-lines",
".verbatim-pending",
".verbatim-state",
".verbatim-unresolved",
".code-block",
".code-block-copy",
".verbatim-copy",
)
def _css() -> str:
return STYLES_CSS.read_text(encoding="utf-8")
def _phase182_block(css: str) -> str:
"""The phase-182 region, markers included (comments intact — the block
carries the contract in prose too)."""
start = css.find(BEGIN)
assert start != -1, "styles.css must carry the labelled phase-182 block (BEGIN marker)"
end = css.find(END, start)
assert end != -1, "the phase-182 block must be closed by its END marker"
return css[start : end + len(END)]
def _strip_comments(css: str) -> str:
return re.sub(r"/\*[\s\S]*?\*/", "", css)
def _rule_bodies(block: str, selector: str) -> list[str]:
"""The bodies of every rule whose selector list contains ``selector``
EXACTLY (the phase-180 parse pattern, comment-stripped)."""
bodies: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", _strip_comments(block)):
selectors = [s.strip() for s in " ".join(match.group(1).split()).split(",")]
if selector in selectors:
bodies.append(match.group(2))
return bodies
def _body(block: str, selector: str) -> str:
bodies = _rule_bodies(block, selector)
assert bodies, f"styles.css must style {selector}"
return " ".join(bodies)
def _root_palette(css: str) -> set[str]:
"""Every variable name the ``:root`` block declares."""
root = re.search(r":root\s*\{", css)
assert root, "styles.css must define :root"
body = css[root.end() : css.find("}", root.end())]
palette = set(re.findall(r"(--[a-z0-9-]+)\s*:", body))
assert palette, "the :root palette must declare variables"
return palette
def _media_block(css: str, query: str) -> str:
"""The body of the ``@media (<query>)`` block — the house non-greedy
pattern (nested closing braces are indented, so the first column-0 ``}``
is the block's own end)."""
m = re.search("@media \\(" + re.escape(query) + r"\) \{([\s\S]*?)\n\}", css)
assert m, f"{query} media query missing"
return m.group(1)
_CSS = _css()
_BLOCK = _phase182_block(_CSS)
_ROOT = _root_palette(_CSS)
# ---------------------------------------------------------------------------
# the block and its selectors
# ---------------------------------------------------------------------------
def test_phase_182_block_is_labelled_and_contains_every_selector() -> None:
"""The block is ONE clearly-labelled region carrying every selector of
the contract (task 06 item 5) — a rule that drifted outside the region
would escape the palette-only and reduced-motion pins below."""
for selector in PANEL_SELECTORS:
assert _rule_bodies(_BLOCK, selector), f"the phase-182 block must style {selector}"
# …and the block says what it is.
assert "phase 182" in _BLOCK
assert "VERBATIM" in _BLOCK, "the block documents the badge it styles"
def test_panel_is_a_themed_surface_card() -> None:
"""``.verbatim`` is a block on the theme's own canvas: the bubble's
rhythm (a vertical margin), a --line hairline, --radius-sm corners, the
--surface fill, ``min-width: 0`` (the long-line escape hatch every
scroller here carries) and ``overflow: hidden`` so the rounded corners
clip the scrolled body."""
body = _body(_BLOCK, ".verbatim")
for prop in (
"display: block",
"min-width: 0",
"background: var(--surface)",
"border: 1px solid var(--line)",
"border-radius: var(--radius-sm)",
"overflow: hidden",
):
assert prop in body, f".verbatim must carry {prop}"
assert re.search(r"margin:\s0\.[0-9]+rem 0;", body), (
".verbatim sits in the bubble's rhythm (a rem margin, not a px one)"
)
def test_head_row_is_sticky_with_an_opaque_background() -> None:
"""PLAN §7.1's bounded-panel rule: the head row STICKS to the top of the
scroll panel, so the caption (``source/path · lines 12–40 of 210``) is
still on screen in a 40-line slice. OPAQUE ``--surface`` background — a
translucent head would let the bytes read through it (the phase-156
``.table-wrap table thead th`` idiom)."""
body = _body(_BLOCK, ".verbatim-head")
assert "position: sticky" in body, "the head row must be sticky"
assert "top: 0" in body, "the sticky offset is the panel top"
assert "background: var(--surface)" in body, "the sticky head must be opaque"
assert "z-index: 1" in body, "the sticky head paints above the scrolled body"
assert "display: flex" in body, "the head is a row (caption, badge, copy)"
def test_caption_is_mono_soft_and_clipped_the_house_way() -> None:
"""``.verbatim-caption`` uses the phase-156 ``.cell-clip`` ellipsis
convention (``min-width: 0`` + ``overflow: hidden`` +
``text-overflow: ellipsis`` + ``white-space: nowrap``) so a long
``source/path`` clips to ``…`` instead of breaking the head row — the
FULL text stays reachable through the control's ``aria-label``/``title``
(the clip is visual only). ``--ink-soft`` on ``--surface`` is 8.2:1
(AA), and the mono family matches every other path in the app."""
body = _body(_BLOCK, ".verbatim-caption")
for prop in (
"min-width: 0",
"overflow: hidden",
"text-overflow: ellipsis",
"white-space: nowrap",
"font-family: var(--mono)",
"color: var(--ink-soft)",
):
assert prop in body, f".verbatim-caption must carry {prop}"
# the ellipsis convention must not rely on a fixed max-width here: the
# caption is the flexible item of a wrapping flex row.
assert "flex: 1" in body, "the caption takes the row's flexible space"
def test_badge_is_text_with_no_uppercase_surprise() -> None:
"""The badge is the WORD ``VERBATIM`` in the --ok family (10.6:1 on its
own fill) with ``text-transform: none`` — an uppercase-forced word is
spelled out letter-by-letter by some screen readers, and an icon-only
badge would fail WCAG 1.4.1 (the claim must be TEXT)."""
body = _body(_BLOCK, ".verbatim-badge")
assert "text-transform: none" in body
assert "color: var(--ok-ink)" in body, "the badge is the --ok family"
assert "background: var(--ok-bg)" in body
assert "content:" not in body, "the badge text is DOM text, never a CSS ::before"
def test_body_is_a_bounded_scroll_panel_of_code() -> None:
"""``.verbatim-body`` is the bounded scroll panel: ``max-height`` in the
``min(…, 70vh)`` form (PLAN §7.1 — the panel never eats the page),
``overflow: auto`` both axes, the mono family, ``white-space: pre`` and
``tab-size: 2`` (the file's own indentation survives), and
``overflow-wrap: normal`` — CODE scrolls horizontally and prose never
does (phase 116 locked A1, the phase-165 split), which also cancels the
``.bubble``'s inherited ``overflow-wrap: anywhere``. ``min-width: 0``
keeps the long-line escape hatch."""
body = _body(_BLOCK, ".verbatim-body")
assert re.search(r"max-height:\s*min\(\s*[0-9.]+rem,\s*70vh\s*\)", body), (
".verbatim-body must cap with max-height: min(<rem>, 70vh)"
)
for prop in (
"overflow: auto",
"min-width: 0",
"font-family: var(--mono)",
"white-space: pre",
"tab-size: 2",
"overflow-wrap: normal",
):
assert prop in body, f".verbatim-body must carry {prop}"
assert "white-space: pre-wrap" not in body, "code never wraps (locked A1)"
def test_line_numbers_are_not_selectable() -> None:
"""The gutter is ``user-select: none`` (with the ``-webkit-`` prefix) on
BOTH the container and its items, right-aligned in ``--ink-soft``: a
reader who selects the panel — or a Copy path that reads the selection —
never drags line numbers into the clipboard. ``list-style: none`` means
the digits are TEXT, so the rule behaves identically in every engine."""
container = _body(_BLOCK, ".verbatim-lines")
for prop in (
"user-select: none",
"-webkit-user-select: none",
"text-align: right",
"color: var(--ink-soft)",
"list-style: none",
):
assert prop in container, f".verbatim-lines must carry {prop}"
items = _body(_BLOCK, ".verbatim-lines li")
assert "user-select: none" in items, "the <li> line numbers must be unselectable too"
def test_pending_state_is_muted_and_static() -> None:
"""The streaming state (``.verbatim-pending`` + ``.verbatim-state``, the
markdown.js placeholder) is MUTED — ``--ink-soft`` on ``--surface``,
8.2:1 — and it never announces itself with motion: the pending line is a
``role="status"`` live region, so the announcement IS the feedback."""
pending = _body(_BLOCK, ".verbatim-pending")
state = _body(_BLOCK, ".verbatim-state")
assert "color: var(--ink-soft)" in state, "the pending line is muted (not an answer)"
assert "background: var(--surface)" in pending
assert "animation" not in pending and "transition" not in pending
assert "animation" not in state and "transition" not in state
def test_unresolved_state_carries_no_badge() -> None:
"""The honest failure state: the --err boundary + --err-bg canvas with
--ink-soft text (7.9:1, AA), and CSS-side enforcement that a ``VERBATIM``
badge NEVER appears on an unresolved panel whatever DOM reaches it — the
badge belongs only to a server-resolved block (locked A5)."""
body = _body(_BLOCK, ".verbatim-unresolved")
assert "var(--err" in body, "the unresolved state is the --err family"
assert "color: var(--ink-soft)" in body
hide = _body(_BLOCK, ".verbatim-unresolved .verbatim-badge")
assert "display: none" in hide, "an unresolved panel never badges"
def test_copy_controls_are_pills_with_a_ring_and_an_ellipsis_label() -> None:
"""``.code-block-copy`` / ``.verbatim-copy``: the house pill (mono text,
--line hairline, --surface fill, ink-soft label), the label is the clip
target (``text-overflow: ellipsis`` on its span, the control's
``aria-label`` naming the full path), the ``:focus-visible`` ring is the
app's 3px --brand ring, and the ``.copied`` rule exists to SUPPORT the
text swap (``Copy`` → ``Copied``) with colour — never colour alone."""
both = _body(_BLOCK, ".code-block-copy") + " " + _body(_BLOCK, ".verbatim-copy")
for prop in (
"border: 1px solid var(--line)",
"background: var(--surface)",
"color: var(--ink-soft)",
"font-family: var(--mono)",
"cursor: pointer",
"min-height:",
):
assert prop in both, f"the copy controls must carry {prop}"
label = _body(_BLOCK, ".code-block-copy > span")
assert "text-overflow: ellipsis" in label, "a clipped copy label shows the hint"
for selector in (".code-block-copy:focus-visible", ".verbatim-copy:focus-visible"):
ring = _body(_BLOCK, selector)
assert "outline: 3px solid var(--brand)" in ring, f"{selector} needs the house ring"
assert "outline-offset: 2px" in ring
for selector in (".code-block-copy.copied", ".verbatim-copy.copied"):
copied = _body(_BLOCK, selector)
assert "var(--ok" in copied, f"{selector} must use the --ok family as SUPPORT"
def test_plain_code_block_wrapper_is_not_a_verbatim_panel() -> None:
"""``.code-block`` is the plain wrapper task 07 puts around an ordinary
answer ``<pre>``: a Copy button belongs to it, the ``VERBATIM`` badge
does NOT — so the wrapper rule carries no --ok fill and no panel chrome
(locked A5: verbatim-and-badged OR plainly the model's, never a hybrid)."""
body = _body(_BLOCK, ".code-block")
assert "position: relative" in body
assert "min-width: 0" in body
assert "--ok" not in body, "an ordinary code block never wears the verbatim badge"
assert not _rule_bodies(_BLOCK, ".code-block .verbatim-badge"), (
"no badge rule may exist inside the plain wrapper"
)
# ---------------------------------------------------------------------------
# the palette-only invariant (phase 92 / the :root-drift guard)
# ---------------------------------------------------------------------------
def test_phase_182_block_uses_palette_variables_only() -> None:
"""NO colour literal anywhere in the phase-182 block: every colour comes
from a declared ``:root`` token (no hex, no ``rgb()``/``rgba()``, no
``hsl()``, no named colour). A literal would be a second source of truth
for the built-in palette (the ``test_theming.py`` drift guard parses the
built-ins out of this file) and a new hue would break the monochrome
invariant — this is also what keeps a code surface readable when the
owner switches the app to a light or a monochrome theme."""
code = _strip_comments(_BLOCK)
refs = re.findall(r"var\((--[a-z0-9-]+)\)", code)
assert refs, "the phase-182 block must reference palette vars"
for ref in set(refs):
assert ref in _ROOT, f"{ref} is not a :root palette var"
assert not re.search(r"#[0-9a-fA-F]{3,8}\b", code), "no hex color literal in the block"
for fn in ("rgb(", "rgba(", "hsl(", "hsla("):
assert fn not in code, f"no {fn} color literal in the block"
# named colours are the other way a second source of truth sneaks in
for name in (": white", ": black", "white;", "black;", "white }", "black }"):
assert name not in code, f"no named colour in the block ({name.strip()})"
def test_phase_182_block_restates_no_global_focus_rule_wrongly() -> None:
"""The restatement of the global ``:focus-visible`` ring uses the SAME
values as the app-wide rule (3px --brand, 2px offset) — a different
ring here would be a second, drifting source of focus truth."""
global_rule = _css()[: _css().find("\n::selection")]
assert re.search(r":focus-visible \{\s*outline: 3px solid var\(--brand\);\s*"
r"outline-offset: 2px;", global_rule), (
"the app-wide :focus-visible rule is the baseline this block copies"
)
# ---------------------------------------------------------------------------
# mobile + reduced motion
# ---------------------------------------------------------------------------
def test_mobile_block_keeps_a_44px_control() -> None:
"""The ≤640px block lifts BOTH Copy controls to the 44px touch floor
(PLAN §7: the phone tap target) — the desktop pill is smaller, so the
rule must live in the media block, not only in the base rule."""
mobile = _media_block(_CSS, "max-width: 640px")
assert "Phase 182" in mobile, "the mobile block must name the phase it serves"
hit = re.search(r"\.code-block-copy, \.verbatim-copy \{([^}]*)\}", mobile)
assert hit, "the mobile block must raise both Copy controls together"
assert "min-height: 44px" in hit.group(1)
def test_nothing_in_the_block_animates_without_a_reduced_motion_still() -> None:
"""``prefers-reduced-motion`` is honored by anything that animates. The
phase-182 block animates NOTHING (the pending state is a live region, so
the text swap is the feedback), which this pins as a GUARD: if a future
edit adds an ``animation``/``transition``, the same selectors must appear
in a ``prefers-reduced-motion: reduce`` block that stills them."""
code = _strip_comments(_BLOCK)
animated: list[str] = []
for match in re.finditer(r"([^{}]+)\{([^{}]*)\}", code):
body = match.group(2)
if re.search(r"(^|[;\s])(animation|transition)\s*:", body):
animated.append(" ".join(match.group(1).split()))
if not animated:
assert "prefers-reduced-motion" in _BLOCK, (
"a static block must say so, so the next reader does not add "
"a pulse 'to match the app'"
)
return
reduced = "\n".join(
m.group(0)
for m in re.finditer(r"@media \(prefers-reduced-motion: reduce\)[\s\S]*?\n\}", _CSS)
)
for selectors in animated:
for selector in selectors.split(","):
assert selector.strip() in reduced, (
f"{selector} animates but is never stilled under "
"prefers-reduced-motion"
)
def test_the_panel_never_repaints_the_existing_code_and_bubble_rules() -> None:
"""The neighbours are untouched: ``.bubble pre`` (the ordinary answer
code block) and ``.doc-raw`` (the viewer's scroller) keep their
declarations, so the phase adds a SURFACE and does not restyle every
code block in the app."""
css = _CSS
pre = re.search(r"\.bubble pre \{([^}]*)\}", css)
assert pre, ".bubble pre must still exist"
for prop in ("background: var(--surface)", "border: 1px solid var(--line)", "overflow-x: auto"):
assert prop in pre.group(1), f".bubble pre must keep {prop}"
raw = re.search(r"\.doc-raw \{([^}]*)\}", css)
assert raw, ".doc-raw must still exist"
assert "white-space: pre" in raw.group(1)
def test_block_is_one_contiguous_region_of_the_sheet() -> None:
"""The BEGIN/END markers wrap ONE region (a phase that appends a stray
``.verbatim-*`` rule elsewhere escapes every pin in this module)."""
css = _CSS
assert css.count(BEGIN) == 1 and css.count(END) == 1
assert css.index(BEGIN) < css.index(END)
between = css[css.index(BEGIN) : css.index(END)]
# every verbatim selector lives inside it
for selector in PANEL_SELECTORS:
assert re.search(r"(?m)^" + re.escape(selector) + r"[^\n]*\{", between), (
f"{selector} must be declared inside the labelled phase-182 region"
)
outside = _strip_comments(css[: css.index(BEGIN)] + css[css.index(END) :])
for selector in PANEL_SELECTORS:
assert not re.search(
r"(?m)^" + re.escape(selector) + r"[^\n]*\{", outside
), f"{selector} must not be restyled outside the phase-182 region"
def test_selector_names_match_the_dom_the_renderer_and_task_07_build() -> None:
"""The CSS class names are the contract between the renderer's pending
template (``markdown.js``) and the panel builder (``verbatim.js``, task
07): the pending template's classes must be exactly the ones styled here,
so the streaming placeholder is never an unstyled div."""
js = (STYLES_CSS.parent / "markdown.js").read_text(encoding="utf-8")
for cls in ("verbatim", "verbatim-pending", "verbatim-caption", "verbatim-state"):
assert cls in js, f"markdown.js must emit .{cls}"
assert _rule_bodies(_BLOCK, "." + cls), f"styles.css must style .{cls}"
assert "verbatim-unresolved" in _BLOCK, (
"the unresolved class is part of THIS contract (task 07 renders it)"
)
@pytest.mark.parametrize("pair", [("--ink", "--surface"), ("--ink-soft", "--surface")])
def test_block_pairs_the_documented_text_tokens(pair: tuple[str, str]) -> None:
"""The block pairs its text with the canvas the :root comment documents
(``--ink`` and ``--ink-soft`` on ``--surface``) rather than inventing a
third ink — the AA duty is inherited from the palette, not restated."""
code = _strip_comments(_BLOCK)
assert re.search(r"color: var\(" + re.escape(pair[0]) + r"\)", code), (
f"the block must use {pair[0]}"
)
assert re.search(r"background: var\(" + re.escape(pair[1]) + r"\)", code), (
f"the block must sit on {pair[1]}"
)