phase: 180_markdown_inline_links_images
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) - ``→`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:
@@ -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 ``&`` 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"
|
||||
"\n"
|
||||
"\n"
|
||||
"An unsafe reference stays plain text: [oops](javascript:alert(1)).\n"
|
||||
"\n"
|
||||
")\n"
|
||||
"\n"
|
||||
"["
|
||||
"](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
|
||||
|
||||
Reference in New Issue
Block a user