phase: 180_markdown_inline_links_images
Build and Push Containers / build-and-push-app (push) Successful in 1m51s
Build and Push Containers / build-and-push-db (push) Successful in 15s

All verification complete. Final state summary:

**Report — phase 180 final verification pass**
- Verified (all already implemented in working tree): `inline()` chain = code→strong→em→linked-image→image→link→headings→lists in `frontend/assets/markdown.js`; `allowLinkUrl`/`allowImageUrl` allowlists (A5), shared `mdAnchorOpen` (target/rel only for absolute http(s)); `####`→`<h5>`, `#####`/`######`→`<h6>`, `#`/`##`→`<h3>`/`###`→`<h4>` unchanged; `.md-link`/`.md-img`/h5-h6 scale in `styles.css` (palette vars only). All surfaces (chat, thinking, restore, viewer, modal) route through the one renderer (pinned in `app.js`/`document.js`).
- Completion criteria:
  - `[text](url)`→`a.md-link`, `javascript:`/`data:` inert — ✅ (node probe + unit + E2E)
  - `![alt](src)`→`img.md-img loading="lazy"` with `alt`; bad src inert — ✅
  - `####`/`#####`/`######` ladder; shallow rungs unchanged — ✅
  - Fenced link/image/`####` stay code; cell-level link renders in `.md-table` — ✅ (node probe)
  - Byte-identity fixture + phase-44/138 suites pass unedited (git-verified untouched; 6 + 3 E2E pass in isolation) — ✅
  - `uv run pytest --cov=app --cov-report=term-missing`: **4198 passed, TOTAL 99%**; `uv run pytest tests/e2e/test_markdown_inline_links_images.py -v --no-cov`: **4 passed**; `uv run ruff check .`: clean; `uv run pyright`: **0 errors** — ✅
- Docs: full README/AGENTS.md review vs the whole project — env-example↔README table complete, page routes match `app/main.py`, AGENTS quick-ref artifacts exist. Fixed 1 inconsistency: README understated the image allowlist (impl per task 02 accepts first segments ≤2 chars, `^.{0,2}/`, not only `./`/`../`); phase-180 paragraphs otherwise accurate.
- Notable: image allowlist `.{0,2}/` is broader than the overview's `^\.{1,2}/` but is what task 02 explicitly specifies and the docstring documents — not a deviation from the tasks; SVG sizing deliberately deferred to phase 181.
- Next pending phase: `181_svg_document_preview`.
This commit is contained in:
2026-10-14 16:03:36 -04:00
parent 3d82a72b6f
commit 6880bd0866
39 changed files with 2952 additions and 1 deletions
+80
View File
@@ -528,6 +528,21 @@ Implements just enough of the aipi surface:
marker, same reasoning as ``SUMMARY_MODE``), so a marker question
always gets the quote answer; the E2E asks it against an on-topic
fixture (HIGH gate) and asserts non-deflection.
- user message containing ``show me inline markdown`` (phase 180,
markdown links / images / the deep heading ladder, TODO.md L3-L5) ->
the fixed inline answer (``INLINE_ANSWER``): an absolute link with a
``&`` query string, a ``/``-rooted link, a ``mailto:`` link and a
``#fragment`` link, an inert ``javascript:`` link probe, a
``data:image/svg+xml`` image, an inert ``javascript:`` image probe, a
LINKED image (locked A6), a ``####`` and a ``#####`` heading, a pipe
table carrying a link in a cell, and a plain closing line —
byte-stable, so the story E2E can assert the rendered
``<a class="md-link">`` / ``<img class="md-img">`` / ``<h5>`` /
``<h6>`` shapes, the inert probes, the cell-level transform and the
restore path's re-render. Checked BEFORE the ``DEFLECT_MODE`` branch
(like ``TABLE_TRIGGER`` — the marker lives in the user message), so a
marker question always gets the inline answer; the E2E asks it against
an on-topic fixture (HIGH gate) and asserts non-deflection.
- user message containing ``echo my history``
(``HISTORY_TRIGGER``, phase 74, TODO L4 — chat history with prior
thinking reaches the LLM) -> the deterministic HISTORY ECHO, derived
@@ -1491,6 +1506,60 @@ QUOTE_ANSWER = (
"And the quote ends here — a plain closing line."
)
#: Phase 180 (markdown links / images / the deep heading ladder, TODO.md
#: L3-L5): a user message containing this substring (case-insensitive)
#: gets the fixed inline answer (``INLINE_ANSWER`` below) — the renderer's
#: whole phase-180 contract in one byte-stable answer (see the module
#: docstring). The two ``data:image/svg+xml;base64,`` payloads are tiny
#: valid SVGs (no spaces, no parentheses — the URL capture is ``[^)\s]+``)
#: so the CSP's existing ``img-src 'self' data:`` paints them. Existing
#: E2E questions do not contain the phrase, so every other suite is
#: unaffected.
INLINE_TRIGGER = "show me inline markdown"
#: The fixed inline answer (phase 180) — byte-stable on purpose: the story
#: E2E asserts the rendered ``<a class="md-link">`` shapes (the absolute
#: link with its ``target``/``rel`` pair and its ``&amp;`` kept verbatim,
#: the relative / ``mailto:`` / ``#fragment`` links without them), the two
#: inert probes (no live anchor, no live image, the reference still
#: VISIBLE), the ``<img class="md-img">`` with its alt and lazy hint, the
#: linked image wrapped with no stray bracket (locked A6), the ``<h5>`` /
#: ``<h6>`` rungs (locked A4), the link inside the ``.md-table`` cell, and
#: the plain closing line (the stream-completion marker — it lands last).
INLINE_ANSWER = (
"Here's the shape, inline:\n"
"\n"
"[the absolute link](https://example.com/a?x=1&y=2) opens the runbook.\n"
"\n"
"[the relative link](/sources) opens the catalog, "
"[the mail link](mailto:reese@example.com) writes to the owner and "
"[the anchor link](#homelab-inline-markdown-notes) stays on this page.\n"
"\n"
"![topology](data:image/svg+xml;base64,"
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0OCAzMiI+"
"PHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjMyIiBmaWxsPSIjMzg2YjdhIi8+"
"PGNpcmNsZSBjeD0iMjQiY3k9IjE2IiByPSI4IiBmaWxsPSIjZTVlN2ViIi8+PC9zdmc+)\n"
"\n"
"An unsafe reference stays plain text: [oops](javascript:alert(1)).\n"
"\n"
"![bad](javascript:alert(1))\n"
"\n"
"[![linked topology](data:image/svg+xml;base64,"
"PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+"
"PHJlY3Qgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiBmaWxsPSIjN2EzODY4Ii8+PC9zdmc+)"
"](https://example.com/runbook)\n"
"\n"
"#### Deep header\n"
"\n"
"##### Deeper\n"
"\n"
"| Note | Link |\n"
"|---|---|\n"
"| topology | [cell link](https://example.com) |\n"
"\n"
"And the inline markdown ends here — a plain closing line."
)
# ---------------------------------------------------------------------------
# Phase 67 (LLM retry, TODO.md L3): deterministic failure injection
# ---------------------------------------------------------------------------
@@ -4085,6 +4154,17 @@ def compose_answer(body: dict[str, Any]) -> str:
# the marker lives in the user message, so a marker question
# always gets the quote answer whatever the gate says.
answer = QUOTE_ANSWER
elif INLINE_TRIGGER in user.lower():
# Markdown links, inline images and the deep heading ladder
# (phase 180, TODO.md L3-L5): the story E2E's deterministic
# inline answer — the allowlisted link/image shapes, the two
# inert ``javascript:`` probes, the linked image (locked A6),
# the ``####``/``#####`` rungs (locked A4) and the table-cell
# link, in one byte-stable stream. Checked BEFORE the
# DEFLECT_MODE branch, like TABLE_TRIGGER: the marker lives in
# the user message, so a marker question always gets the inline
# answer whatever the gate says.
answer = INLINE_ANSWER
elif HISTORY_TRIGGER in user.lower():
# Phase 74 (TODO L4, chat history): the deterministic history
# echo — proves on the wire that the client's prior turns (and