phase: 138_markdown_blockquotes
Build and Push Containers / build-and-push-app (push) Successful in 2m36s
Build and Push Containers / build-and-push-db (push) Successful in 11s

**Phase 138 final verification pass — all green, no defects found.**

- Verified: blockquote pass in `frontend/assets/markdown.js` (step 2b, between escape+inline and paragraphs), guard extended to `|blockquote`, docstring updated; `.md-quote` CSS (left border + padding + `--ink-soft`, no fill, palette vars only, contrast comments, user-bubble override); unit pins + node behavior; dedicated E2E (chat / viewer / restore); mock `QUOTE_TRIGGER`/`QUOTE_ANSWER`; fixture `tests/fixtures/quote_docs/`. Thinking block confirmed on the same shared renderer (app.js L2025/L3095).
- `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` → 3 passed (isolation)
- `uv run pytest --cov=app --cov-report=term-missing` → 3268 passed, 9 skipped; TOTAL **99%** (>90%)
- `uv run ruff check . && uv run pyright` → All checks passed; 0 errors
- Criteria: quotes render in chat/viewer/restore ✅ (E2E) + thinking ✅ (shared renderer + unscoped AA CSS); `>>` collapses, fence/table `>` untouched ✅ (unit+e2e); non-quote byte-identical ✅ (exact fixture; phase-44/renderer suites pass **unedited**, confirmed by empty `git diff --stat`)
- No deviations; commit + phase-dir move left to the harness per pipeline rules (working tree clean of stray changes — only phase-138 diffs present)
- Next pending phase: **139_read_range_tool_lines**
This commit is contained in:
2026-09-30 10:49:40 -04:00
parent 0db6ab5df0
commit 1a657ce4ca
22 changed files with 1230 additions and 1 deletions
@@ -1,45 +0,0 @@
# Phase 138 — Markdown blockquotes render: `>` quotes become styled `<blockquote>`s
**Source:** `TODO.md` L3 — "Markdown formatted ">" quotes aren't rendering"
**Story:** n/a (TODO-derived phase, Protocol B append).
**Context:** `frontend/assets/markdown.js` — the shared escape-first renderer (loaded by all three pages: `index.html` chat, `document.html` viewer, `shared.html`): step 1 fence protection (`\u0000CODEn\u0000`), step 1b table protection (`\u0000TABLEn\u0000`, phase 44), step 2 `inline(escapeHtml(text))` (the inline chain: code → strong → em → h3/h4 → ul → ol), step 3 the paragraph pass (split on `\n{2,}`; the block-element guard `/^<(h\d|ul|ol|pre|li)/`), step 4 restore. Today a `>` line is escaped to `&gt;` and renders as literal text in a `<p>` (verified: `renderMarkdown("> quoted")` → `<p>&gt; quoted</p>`). `frontend/assets/styles.css` — the `:root` palette (`--ink-soft` 8.2:1 on `--surface`, `--line`, the `--brand-*` family; the `:root`-drift unit test parses the built-ins from this file — no second copy) and the chat-bubble / viewer typography. `tests/unit/test_markdown_tables.py` — the frontend source-pin + node-behavior unit pattern (`HAVE_NODE` skip); `tests/e2e/test_markdown_tables.py` — the phase-44 E2E pattern (mock flow with table output + the document viewer).
## Objective
`>`-prefixed lines in any rendered markdown — a chat answer, the thinking block, a saved/shared chat (restore), the document viewer — render as a semantic `<blockquote class="md-quote">` with theme-safe styling; every non-quote input renders byte-identical to today (the phase-44 table suite and the renderer-dependent unit suites stay green unedited).
## Dependencies
- `137_pdf_image_context` (complete) — pipeline order only; the code dependencies are the shared renderer + `styles.css` on `main`.
## Design (shared by all tasks — the executor reads this, not the chat)
- **The pass (task 01):** a new line-block pass in `markdown.js` BETWEEN step 2 (`inline(escapeHtml(text))`) and step 3 (paragraphs) — at that point the text is escaped (a quote line starts with `&gt;`) and already inline-transformed (bold/emphasis/code work inside quotes for free — the pass never re-transforms). Group the maximal run of consecutive lines matching `^\s*&gt;+ ?` into ONE `<blockquote class="md-quote">…</blockquote>`: strip the leading `&gt;` run plus ONE optional space from each member (nested `>>` / `>>>` collapses to one level — ASSUMPTION A1), join members with `<br>` (the paragraph convention). A BLANK line terminates the group (a re-quoted line starts a new `<blockquote>` — simple, predictable). The step-3 guard extends to `/^<(h\d|ul|ol|pre|li|blockquote)/` so the paragraph pass leaves the block alone (no `<p>` wrap).
- **Precedence is untouched:** fences protect first (step 1 — a `>` inside a code block never reaches the pass), tables protect second (step 1b — cell content is escaped inside the `<td>` markup and restored in step 4, AFTER the pass). No other pass changes.
- **In-quote structure (documented limit):** a markdown STRUCTURE inside a quote (a `- list` line, a `# heading` line) renders as plain text inside the blockquote — the inline chain runs before the pass, so its line-start transforms never see the post-strip text. Inline (code/strong/em) still works. Acceptable — the scope is quote rendering (owner L3).
- **The CSS (task 02):** `.md-quote` in `styles.css` — theme-safe from the EXISTING `:root` palette only: left border + horizontal padding + vertical margin + a muted text color carrying a ≥4.5:1 contrast comment on the bubble surface (the house comment convention, cf. `--ink-soft`'s `/* 8.2:1 on --surface */`). ONE rule set serves all three contexts (chat `.bubble`, `.thinking-text`, the document viewer) — a scoped override only if the E2E proves a clash.
- **Byte-identity (task 03):** any input without a quote line renders byte-identical to the pre-phase renderer — pinned by the node behavior check and by the unedited `test_markdown_tables.py` / `test_thinking_scroll.py` / `test_document_viewer.py` suites.
- **E2E (task 03):** dedicated `tests/e2e/test_markdown_blockquotes.py`, run in isolation (AGENTS.md rules 4/9), mock-only.
## Tasks
1. `01_blockquote_pass.md` — the blockquote pass in `markdown.js` + the paragraph-guard extension.
2. `02_blockquote_css.md` — the `.md-quote` styling in `styles.css` (theme-safe, WCAG AA contrast).
3. `03_unit_and_e2e.md` — unit source-pins + node behavior + the dedicated Playwright E2E.
## Testing & Quality
- Unit: `tests/unit/test_markdown_blockquotes.py` (new — the phase-44 pattern): source pins (pass order fences → tables → escape+inline → **blockquotes** → paragraphs → restore; the guard regex includes `blockquote`; the exact `<blockquote class="md-quote">` marker; the `.md-quote` CSS rule references palette vars only) + node behavior (single-line quote, multi-line group, nested collapse, XSS quote inert, fence-wins, a table cell containing `>` untouched, non-quote input byte-identical).
- E2E: `tests/e2e/test_markdown_blockquotes.py` (new), in isolation.
- Coverage: **>90%** on `app/` (the gate runs the full suite — this phase adds no `app/` code, so the suite staying green IS the gate).
## Completion Criteria
- [ ] `>` quotes render as a styled `<blockquote class="md-quote">` in the chat answer, the thinking block, saved/shared chats (restore), and the document viewer
- [ ] nested `>>` collapses to one level; a `>` inside a code fence or a table cell is untouched
- [ ] non-quote input renders byte-identical (the phase-44 / renderer-dependent suites pass unedited)
- [ ] `uv run pytest` green; `uv run pytest --cov=app --cov-report=term-missing` TOTAL >90%; `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` green in isolation; `uv run ruff check . && uv run pyright` clean
- [ ] one `--no-gpg-sign` Conventional Commit; phase dir moved to `.agents/phases/complete/` by the pipeline gate
## Locked decisions
- **A1 — nested quotes (`>>`, `>>>`) collapse to a single `<blockquote>` level (owner-confirmed 2026-09-30, roadmap confirmation).**
- **A2 — the quote style is a left border + padding + muted text color (NO background fill), built from the existing `:root` palette only (owner-confirmed 2026-09-30, roadmap confirmation).**
## Commit
```bash
git add frontend/ tests/ .agents/phases/ && git commit --no-gpg-sign -m "feat(ui): render markdown blockquotes in chat, viewer, and shared pages"
```
@@ -1,22 +0,0 @@
# Task 01 — The blockquote pass in the shared renderer
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
**Story:** n/a
## Objective
`renderMarkdown` turns runs of `>`-prefixed lines into `<blockquote class="md-quote">` elements — after the escape + inline step, before the paragraph pass — with fences and tables keeping their precedence and non-quote input byte-identical.
## Work
1. `frontend/assets/markdown.js` — new pass between the step-2 `text = inline(escapeHtml(text))` and the step-3 paragraph `.map(...)`: split the text on `\n`; group the maximal run of consecutive lines matching `^\s*&gt;+ ?` (the escaped quote marker); strip the `&gt;` run + one optional space from each member (nested `>>`/`>>>` collapses to one level — ASSUMPTION A1); join members with `<br>`; emit `<blockquote class="md-quote">…</blockquote>`. A blank line terminates the group.
2. `frontend/assets/markdown.js` — extend the step-3 block-element guard from `/^<(h\d|ul|ol|pre|li)/` to `/^<(h\d|ul|ol|pre|li|blockquote)/` so the paragraph pass leaves the block alone (no `<p>` wrap).
3. `frontend/assets/markdown.js` — update the module docstring: pass order is now fences → tables → escape+inline → **blockquotes** → paragraphs → restore; document the group rule (blank line terminates; nested collapses) and the documented limit (a list/heading line INSIDE a quote renders as plain text — the inline chain ran first; inline code/strong/em work).
- ASSUMPTION: a `>` inside a code fence is already protected by the step-1 placeholder (no handling needed); a `>` in a table cell is escaped inside the `<td>` markup and restored in step 4 — both are pinned in task 03's behavior checks.
## Testing & Quality
- Unit: covered by task 03's file (the source pin + the node behavior listed there) — the frontend house pattern is one unit file per feature.
- Coverage: n/a (frontend) — the suite + the pins are the gate.
## Completion Criteria
- [ ] node repro: `renderMarkdown("> a\n> b")` → `<blockquote class="md-quote">a<br>b</blockquote>`; `renderMarkdown("> quoted")` no longer emits a literal `&gt;`
- [ ] a non-quote input (plain paragraph, list, table, fence) renders byte-identical to the pre-task renderer
- [ ] `uv run pytest` green (the renderer-dependent unit suites unedited); `uv run ruff check .` clean
@@ -1,21 +0,0 @@
# Task 02 — `.md-quote` styling (theme-safe, WCAG AA)
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
**Story:** n/a
## Objective
The blockquote is visually distinct and readable in all three rendering contexts (chat bubble, thinking block, document viewer) with ONE rule set, using only the existing `:root` palette.
## Work
1. `frontend/assets/styles.css` — add the `.md-quote` rule next to the other markdown element styles (the phase-44 `.md-table` / `.md-table-wrap` block is the anchor): `border-left: 3px solid <palette var>` (a visible-but-restrained border — pick from the existing `:root` palette, the executor's contrast judgment), `padding: 0.25rem 0.75rem`, `margin: 0.5rem 0`, `color: <muted palette var>` with a ≥4.5:1 contrast comment on the bubble surface per the house convention (`--ink-soft`'s `/* 8.2:1 on --surface */` qualifies — verify against the actual bubble background and document it).
2. `frontend/assets/styles.css` — no background fill (ASSUMPTION A2); verify the rule renders acceptably in the chat `.bubble`, the `.thinking-text` block, and the document-viewer content region — add a scoped override only if the task-03 E2E proves a clash.
- ASSUMPTION: the built-in dark palette is the single source of truth — the `:root`-drift unit test parses built-ins from this file, so no hex literal outside `:root` and no second copy.
## Testing & Quality
- Unit: `tests/unit/test_markdown_blockquotes.py` (task 03) source-pins the `.md-quote` rule's presence and its palette-var-only references (the phase-44 CSS-pin pattern).
- Coverage: n/a (frontend).
## Completion Criteria
- [ ] the `.md-quote` rule exists in `styles.css`, references only `:root` palette vars, and carries a ≥4.5:1 contrast comment
- [ ] the quote is visually distinct in chat + viewer (the E2E screenshot, task 03)
- [ ] the theme no-op (B4) is unaffected — the rule uses existing vars; no new `ui_settings` column
@@ -1,24 +0,0 @@
# Task 03 — Unit pins, node behavior, dedicated E2E
**Phase:** `138_markdown_blockquotes` · **Source:** `TODO.md:3` — "Markdown formatted ">" quotes aren't rendering"
**Story:** n/a
## Objective
Pin the renderer contract at the source level, prove the browser behavior, and ship the dedicated Playwright file, run in isolation (AGENTS.md rules 4/9).
## Work
1. `tests/unit/test_markdown_blockquotes.py` (new) — the phase-44 pattern (source pins + a `HAVE_NODE`-skipped node behavior):
- Source pins: the blockquote pass runs after `inline(escapeHtml(text))` and before the paragraph split; the paragraph guard includes `blockquote`; the exact `<blockquote class="md-quote">` marker string; the `.md-quote` CSS rule references palette vars only (the task-02 pin).
- Node behavior: single-line quote → one blockquote; a 3-line group → one blockquote with `<br>` joins; `>> nested` collapses to one level; `> <script>alert(1)</script>` renders inert (no live tag in the output); a `>` inside a ``` fence stays fence content; a table cell containing `> x` renders as table-cell text, not a blockquote; non-quote input (a paragraph + list + table) renders byte-identical to the pre-phase renderer (fixture string checked into the test, per the phase-44 shape).
2. `tests/e2e/test_markdown_blockquotes.py` (new) — dedicated, run in isolation (`--no-cov`, DB up), mock-only, following `tests/e2e/test_markdown_tables.py`'s structure:
- (a) the chat: a mock flow whose answer contains `>` quotes (the phase-44 mock-flow pattern — a new user-message marker, a deterministic canned answer) → assert the DOM: the `.bubble` contains a `blockquote.md-quote` with the quote text and no literal `&gt;` text at the block's start;
- (b) the viewer: a KB fixture with a `.md` document containing a quote block → open the document → assert the viewer content renders the `blockquote.md-quote`;
- (c) the restore: save the chat (or reload `/?chat=<id>`) → the restored bubble keeps the blockquote (the restore path runs the same renderer — a cheap assertion if the suite structure allows it).
## Testing & Quality
- Unit: as above. E2E: as above. Coverage: **>90%** on `app/` (the gate runs the full suite; frontend changes add no `app/` code — the suite must stay green).
## Completion Criteria
- [ ] `uv run pytest tests/unit/test_markdown_blockquotes.py -v` green (node available → the behavior check runs)
- [ ] `uv run pytest tests/e2e/test_markdown_blockquotes.py -v --no-cov` green in isolation
- [ ] `uv run pytest` green, coverage TOTAL >90%, `uv run ruff check . && uv run pyright` clean