phase: 130_chat_answer_clusters
Build and Push Containers / build-and-push-app (push) Successful in 2m27s
Build and Push Containers / build-and-push-db (push) Successful in 12s

**Phase 130 — final verification pass: 100% green, complete.**

- **Verified:** chips-only row (no chip for image docs) → images-only row (`.msg-images`) → related row → `.msg-actions` last; `getActionsRow` used by all 3 buttons + stopped/failed notes; `markLastRetryable` save-re-append hack gone; in-place chip fallback on image-load failure; CSS right-align with per-button auto margins removed — implementation matches the design in every detail.
- **Completion criteria:** (1) mixed citation renders chips→images→actions-last, DOM-asserted ✅ (2) all 3 buttons in one right-aligned `.msg-actions`; non-admin sees Retry-only; restore path identical ✅ (3) 404-bytes figure → plain chip, unit-pinned ✅ (4) E2E isolated ✅ (5) no behavior change (modal affordances, related tier, ARIA names) ✅
- **Tests/lint/coverage:** `uv run pytest tests/e2e/test_chat_answer_clusters.py -v --no-cov` → 4 passed; `uv run pytest --cov=app --cov-report=term-missing` → 3061 passed, **99%** app coverage; the 4 E2E files this phase modified (test_answer_images, test_image_documents, test_pdf_support, test_responsive_polish) → 22 passed in isolation; `uv run ruff check .` clean, `uv run pyright` → 0 errors.
- **Docs:** README.md — verified consistent in full (page table vs routes, env-var coverage vs `.env.example`, phase-130 cluster wording already in tree); no changes needed. AGENTS.md — fixed one pre-existing inconsistency: rule 5 still cited the retired "centered 46rem chat column"; now states the 72rem container contract (phase 100).
- **Decisions:** PLAN.md §7.1 still says 46rem, but it's owner-managed (phase 100 records the supersession pending owner sign-off) and out of my editable scope — AGENTS.md now cross-references the supersession.
- **Next pending phase:** `131_read_line_ranges` (todo/).
This commit is contained in:
2026-09-28 07:49:37 -04:00
parent 2d0f608246
commit c88c65641a
46 changed files with 3266 additions and 351 deletions
+75 -57
View File
@@ -19,16 +19,18 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
turn, the shape that makes the chip assertion meaningful). The
brain bubble shows the ANSWER FIGURE (``.answer-image`` — ``<img>``
from the bytes route, ``loading="lazy"``, below the text and above
the chip row per A4, alt/caption settling to the summary), the
answer TEXT is still streamed in full (the tail echo of the
the source clusters per A4, alt/caption settling to the summary),
the answer TEXT is still streamed in full (the tail echo of the
``<documents>`` seed proves the description was in the prompt), the
source-chip row carries the image doc's chip + its phase-122
figure (the AUTO-CITE proof — the mock made zero ``read`` calls,
asserted on the mock's request capture), and the "Nearby docs" row
does not repeat the doc. The wire pin: the first model request
offered FOUR tools (the three base + ``show_image``) and the turn
executed exactly one ``show_image`` — the saved brain record
persists the ``answer_image`` ref.
sources block carries the image doc's citation as the compact
figure in the images row (``.msg-meta.msg-images`` — phase 130
task 01: NO chip for an image doc, the figure IS the citation;
the AUTO-CITE proof — the mock made zero ``read`` calls, asserted
on the mock's request capture), and the "Nearby docs" row does not
repeat the doc. The wire pin: the first model request offered
FOUR tools (the three base + ``show_image``) and the turn executed
exactly one ``show_image`` — the saved brain record persists the
``answer_image`` ref.
- **Restore + share** — a second feature turn, then a page RELOAD:
the restored brain bubble shows the figure again (the
``answer_image`` persistence through the ``bor.chat.v1`` restore
@@ -36,11 +38,12 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
fresh anonymous context) renders the figure on the shared page.
- **Bug-fix-only flow** — the ``which diagram is in your notes``
trigger (the mock answers straight from the seed with ZERO tool
calls — no ``show_image``, no ``read``): the source-chip row STILL
carries the image doc's chip + figure (the bug fix on the pure
zero-read path — the assertion the owner's report describes), the
answer text quotes the seeded description's tail, and NO answer
figure renders (the model never called the tool).
calls — no ``show_image``, no ``read``): the sources block STILL
carries the image doc's citation figure (no chip — phase 130;
the bug fix on the pure zero-read path — the assertion the owner's
report describes), the answer text quotes the seeded
description's tail, and NO answer figure renders (the model never
called the tool).
* **App B (default off)** — ``BOR_IMAGES=true``, ``BOR_ANSWER_IMAGES``
pinned to the code default ``false`` (EXPLICITLY — process env ranks
above an operator's local gitignored ``.env``, so the default-off
@@ -51,9 +54,10 @@ mock LLM and the shared e2e Postgres (truncate/re-import per app):
``show_image`` among the offered tools (the request capture carries
exactly the THREE base tools), the brain bubble has NO answer
figure, and the saved brain record carries NO ``answer_image`` key
(the byte-identical-off contract). The bug-fix trigger STILL chips
the image doc (the chips fix is NOT gated by ``BOR_ANSWER_IMAGES`` —
it belongs to the ``BOR_IMAGES``-indexed docs, per the phase design).
(the byte-identical-off contract). The bug-fix trigger STILL cites
the image doc (the figure, no chip — phase 130; the auto-cite fix is
NOT gated by ``BOR_ANSWER_IMAGES`` — it belongs to the
``BOR_IMAGES``-indexed docs, per the phase design).
DB isolation: the suites share one Postgres; ``saved_chats`` survives
the truncation (the house pattern), so every question text here is
@@ -593,15 +597,18 @@ def _assert_answer_figure(
def _assert_image_chip(
page: Page, seeded: dict[str, str], *, last_brain: str = ".msg.brain"
) -> None:
"""The citation chip the AUTO-CITE earns (phase 124 task 05): the
image doc's text chip + its phase-122 compact figure (``<img>``
from the bytes route, alt settling to the summary — the admin
surface; the shared page's chip is plain text and asserted
separately)."""
"""The AUTO-CITE (phase 124 task 05) in the phase-130 cluster
shape (task 01, TODO L6): an image doc gets NO chip — its figure
IS the citation — the compact figure (``<img>`` from the bytes
route, alt settling to the summary — the admin surface; the
shared page's plain-text chip is asserted separately) sits in the
images row (``.msg-meta.msg-images``)."""
chip = page.locator(f"{last_brain} .source-chip")
expect(chip).to_have_count(1)
expect(chip).to_have_text(f"{SOURCE_NAME}/{DOC_PATH}")
fig = page.locator(f"{last_brain} .source-image")
expect(chip).to_have_count(0)
row = page.locator(f"{last_brain} .msg-meta.msg-images")
expect(row).to_have_count(1)
expect(row).to_have_attribute("aria-label", "Source images")
fig = row.locator(".source-image")
expect(fig).to_have_count(1)
img = fig.locator(".source-image-img")
expect(img).to_be_visible(timeout=15_000)
@@ -612,7 +619,7 @@ def _assert_image_chip(
def _body_child_classes(page: Page) -> list[str]:
"""The LAST brain bubble's ``.msg-body`` direct children's class
names, in DOM order — the A4 position pin (bubble < answer-image
< msg-meta)."""
< the first .msg-meta-family source row)."""
return page.evaluate(
"""() => {
const body = [...document.querySelectorAll(".msg.brain")].pop()
@@ -635,15 +642,18 @@ def test_feature_flow_answer_figure_chips_and_zero_reads(
a ``show_image`` call (NO ``read`` — the zero-read turn). Asserts:
(1) the brain bubble's ``.msg-body`` carries ``.answer-image`` with
an ``<img>`` from the bytes route (200, image Content-Type) and
``loading="lazy"``; (2) the figure sits BEFORE the ``.msg-meta``
source-chip row (A4); (3) the answer TEXT is non-empty and quotes
the seed's tail (the "summaries still provided" contract + the
echo proving the seed was in the prompt); (4) the source-chip row
carries the image doc's chip + its figure (the AUTO-CITE proof —
the mock made ZERO ``read`` calls: only ``show_image`` — asserted
on the wire); (5) the "Nearby docs" row does NOT repeat the doc.
Plus the persistence pin: the saved brain record carries the
``answer_image`` ref (the task-04 frame field, round-tripped)."""
``loading="lazy"``; (2) the figure sits BELOW the answer text and
ABOVE the source clusters (A4 — the image-only KB renders the
images row, ``.msg-meta.msg-images`` — phase 130 task 01: no chip
row); (3) the answer TEXT is non-empty and quotes the seed's tail
(the "summaries still provided" contract + the echo proving the
seed was in the prompt); (4) the images row carries the image
doc's figure — no chip (phase 130: the figure IS the citation) —
the AUTO-CITE proof (the mock made ZERO ``read`` calls: only
``show_image`` — asserted on the wire); (5) the "Nearby docs" row
does NOT repeat the doc. Plus the persistence pin: the saved
brain record carries the ``answer_image`` ref (the task-04 frame
field, round-tripped)."""
login(page, app_url, next="/")
expect(page.locator("#kb-banner")).to_be_hidden()
@@ -664,20 +674,25 @@ def test_feature_flow_answer_figure_chips_and_zero_reads(
# summary (the fetchContentSummary pattern settles both).
_assert_answer_figure(page, app_url, seeded, caption=seeded["description"])
# (2) A4 position: below the answer text, above the chip row.
# (2) A4 position: below the answer text, above the source
# clusters — the image-only KB renders the images row
# (.msg-meta.msg-images, phase 130 task 01: no chip row).
classes = _body_child_classes(page)
assert "bubble" in classes, f"no answer bubble in .msg-body: {classes}"
assert "answer-image" in classes, f"no .answer-image in .msg-body: {classes}"
assert "msg-meta" in classes, f"no chip row in .msg-body: {classes}"
meta_idx = next(
(i for i, c in enumerate(classes) if c.startswith("msg-meta")), None
)
assert meta_idx is not None, f"no source row in .msg-body: {classes}"
assert (
classes.index("bubble")
< classes.index("answer-image")
< classes.index("msg-meta")
), f"A4 order violated (bubble < answer-image < msg-meta): {classes}"
< meta_idx
), f"A4 order violated (bubble < answer-image < source row): {classes}"
# (4) The chip row carries the image doc's chip + its figure —
# the AUTO-CITE (the model never read it; the seed IS its
# entire content, so suggested == read for it).
# (4) The images row carries the image doc's figure (no chip —
# phase 130) — the AUTO-CITE (the model never read it; the
# seed IS its entire content, so suggested == read for it).
_assert_image_chip(page, seeded)
# (5) The "Nearby docs" row does NOT repeat the image doc — the
@@ -817,8 +832,8 @@ def test_reload_restores_figure_and_shared_page_shows_it(
# ---------------------------------------------------------------------------
# 3. App A — the bug-fix-only flow: ZERO tool calls, the chip STILL
# appears (the pure zero-read path — the owner's report)
# 3. App A — the bug-fix-only flow: ZERO tool calls, the citation
# STILL renders (the pure zero-read path — the owner's report)
# ---------------------------------------------------------------------------
@@ -828,11 +843,12 @@ def test_bugfix_only_flow_zero_tool_calls_still_chips(
"""The ``which diagram is in your notes`` trigger: the mock
answers straight from the seed with ZERO tool calls (no
``show_image`` — the clause's flow is not triggered — and no
``read``): the source-chip row STILL carries the image doc's chip
+ figure (the bug fix on the pure zero-read path — the assertion
the owner's report describes), the answer text quotes the seeded
description's tail (the description WAS in the prompt), and NO
answer figure renders (the model never called the tool)."""
``read``): the sources block STILL carries the image doc's
citation figure (no chip — phase 130; the bug fix on the pure
zero-read path — the assertion the owner's report describes), the
answer text quotes the seeded description's tail (the description
WAS in the prompt), and NO answer figure renders (the model never
called the tool)."""
login(page, app_url, next="/")
expect(page.locator("#kb-banner")).to_be_hidden()
@@ -860,8 +876,9 @@ def test_bugfix_only_flow_zero_tool_calls_still_chips(
expect(page.locator(".msg.brain .tool-calls-disclosure")).to_have_count(0)
# NO answer figure (the model never called show_image) — but the
# chip row STILL carries the image doc's chip + figure (the auto-
# cite: suggested == read for an image doc, no read performed).
# sources block STILL carries the image doc's citation figure
# (no chip — phase 130; the auto-cite: suggested == read for an
# image doc, no read performed).
expect(page.locator(".msg.brain .answer-image")).to_have_count(0)
_assert_image_chip(page, seeded)
@@ -936,8 +953,8 @@ def test_toggle_off_flow_no_tool_no_answer_image_key(
# ---------------------------------------------------------------------------
# 5. App B (default off) — the bug-fix trigger STILL chips the image
# doc (the chips fix belongs to BOR_IMAGES, not BOR_ANSWER_IMAGES)
# 5. App B (default off) — the bug-fix trigger STILL cites the image
# doc (the auto-cite fix belongs to BOR_IMAGES, not BOR_ANSWER_IMAGES)
# ---------------------------------------------------------------------------
@@ -946,10 +963,11 @@ def test_toggle_off_bugfix_trigger_still_chips(
) -> None:
"""The bug-fix trigger on the default-OFF app (the clause is
absent — the trigger does NOT require it): ZERO tool calls, the
answer quotes the seeded description's tail, and the source-chip
row STILL carries the image doc's chip + figure — the chips fix
is NOT gated by ``BOR_ANSWER_IMAGES`` (it belongs to the
``BOR_IMAGES``-indexed docs, per the phase design)."""
answer quotes the seeded description's tail, and the sources
block STILL carries the image doc's citation figure (no chip —
phase 130) — the auto-cite fix is NOT gated by
``BOR_ANSWER_IMAGES`` (it belongs to the ``BOR_IMAGES``-indexed
docs, per the phase design)."""
login(page, default_app_url, next="/")
expect(page.locator("#kb-banner")).to_be_hidden()