fix(dashboard): render LaTeX-native math delimiters via KaTeX - #7806
fix(dashboard): render LaTeX-native math delimiters via KaTeX#7806jeeshofone wants to merge 1 commit into
Conversation
02225e0 to
3c7ee2a
Compare
UX Review (Fable 5, fork) — ✅ PASSUX-level review of The diff adds no new user-visible control, label, or string — it routes already-emitted UX-Verdict: PASS No new controls or copy — existing raw-LaTeX breakage now renders through the already-shipped KaTeX pipeline, with prose/code false-positive shapes each regression-tested. [UX-REVIEWED] 1bcd277 |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound structural approach; the verbatim-tag pairing grammar is duplicated, not shared, so the two passes can disagree about what is "shown as source." WatchThe plugin hand-rolls its own tag grammar ( Suggestions
[DESIGN-REVIEWED] 1bcd277 |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All verification is done. The base has no existing First-Principles-Verdict: CONCERNS The plugin re-spells the renderer's existing tag-pairing ( Not justified as shipped
What this change shipsInventory (7 items) — 5 justifiedIntent: make LaTeX-native
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] 1bcd277 |
GPT 5.6 Review (fork) — 🔴 changes requested (blocking)Reviewed 2 of 2 blocking finding(s) are security-class and were withheld from adjudication, so the blocking verdict stands. BLOCKING -- website/src/utils/remarkLatexDelimiters.ts:378 -- Inline math removes surrounding soft breaks
BLOCKING -- website/src/utils/remarkLatexDelimiters.ts:292 -- Quoted
[BLOCK-MERGE] 1bcd277 Adjudication (Opus 4.8) — is blocking on each finding proportionate?I've read the prompt, the findings, and traced both fenced findings against the new file's source in the diff (the file is PR-added, so it exists only in the patch). F1 (remarkLatexDelimiters.ts:378) — F2 (remarkLatexDelimiters.ts:292) — [ADJUDICATION] 1bcd277 total=0 uphold=0 downgrade=0 🏷️ Fenced finding(s) machine-flagged as likely edge caseThe security fence keeps these findings blocking regardless of adjudication; the only clearance path is a human override recorded by a repository writer, who must independently verify a rationale before recording it — it is machine-authored, and a wrong override on a security-class finding ships exactly the class the fence exists to stop. (This lane's comment deliberately carries no override command.)
|
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsI've traced both candidates against the actual code. Candidate 1 ( Candidate 2 (mid-line No new grounded findings surfaced while falsifying these. No findings. [OPUS-REVIEWED] 1bcd277 |
3c7ee2a to
5f51fca
Compare
Review round 1 — GPT blockings addressed in
|
5f51fca to
b290b4d
Compare
Review round 2 — both lanes' blockings addressed in
|
63c1a76 to
e5777f9
Compare
|
@jeeshofone Thanks for staying with this one. Here is where the audit leaves it. Nothing on What it needs to land:
Posted from the 2026-09-08 open-PR relationship audit (read-only, one auditor per PR); reply here if any of this is wrong. |
e5777f9 to
0dbbbb8
Compare
|
@bolichen97 — apologies for the two-day gap on this one; that was a monitoring failure on my side, now fixed. All four items, on head 2. Rebased onto current main ( 3. Ride-along reverted. 4. Description refreshed. Tests section states the 18 cases the diff ships; the guards list now includes link destinations, reference-link definitions, indented code, and escaped-backslash parity. 1. Arbitration — picked: the parser-derived route, as a remark plugin (round-6 option 2), with one correction to my own round-6 framing. I objected then that a parser mask adds an ~18s parse on the pathological input. I measured it properly this time: remark's parse IS super-linear on this PR's |
0dbbbb8 to
993b56c
Compare
Review disposition (head
|
993b56c to
2ea24f6
Compare
Review disposition (head
|
2ea24f6 to
9d203f4
Compare
Review disposition (head
|
Models emit \[ ... \] (display) and \( ... \) (inline) — LaTeX's own delimiters — but remark-math only tokenizes dollar math, and the renderer deliberately runs singleDollarTextMath: false (the currency guard), so valid math displayed as raw source text (kirodotdev#7803). normalizeMathDelimiters rewrites both forms to $$ before remark-math, skipping fenced code blocks and inline code spans, leaving unmatched openers alone, and preserving string length exactly (2 chars -> 2 chars) so sourcePos coordinates stay valid. Fixes kirodotdev#7803
9d203f4 to
1bcd277
Compare
Review disposition (head
|
Problem / Motivation
Assistant responses containing valid LaTeX display math (
\[ ... \]) or inline math (\( ... \)) render as raw source text in the dashboard chat instead of formatted equations (#7803).Why it matters
These are the delimiters LaTeX itself uses, and exactly what models tend to emit. Any technical or mathematical conversation degrades to unreadable markup, even though the dashboard already ships a full KaTeX pipeline.
What changed (motivation → approach → change)
Symptom: valid math shows as raw text. Root cause: the markdown pipeline runs
remark-mathwithsingleDollarTextMath: false— a deliberate guard so currency strings ($9.99 … $19.95) don't get parsed as one giant math span — which leaves$$…$$as the only recognized math form. remark-math never tokenizes\[…\]/\(…\), so they fall through as text. KaTeX itself (rehype-katex+ stylesheet) is fully wired; the gap is purely delimiter recognition.Change: a remark transform,
remarkLatexDelimiters(website/src/utils/remarkLatexDelimiters.ts), registered inREMARK_PLUGINSright afterremark-math. It emits remark-math's own mdast nodes (inlineMath/math, identicaldata.hName/hProperties/hChildren) from eligible text nodes only, so rehype-katex renders them exactly like$$math. Because remark has already consumed CommonMark escapes when it builds a text node, the transform reads the node's RAW source slice through its position (so\[is still visible) and re-applies escape + character-reference decoding to the prose it hands back. Display math found inside a paragraph splits the paragraph so themathblock is valid flow content.What is protected, and why it is structural rather than enumerated:
code,inlineCode,html,link,linkReference,image,imageReference,definition, footnotes and frontmatter are other node types and are never visited. So fenced code (including inside blockquotes), indented code, inline code spans, link destinations, reference-link definitions and raw HTML attributes are untouched by construction — not by a scanner that has to recognise each one in source. Eligibility rules, each with a regression test:\\(is an escaped backslash followed by a plain paren;\\\(is an escaped backslash then a real opener).\( … \)needs its closer in the same text node.\[ … \]needs whitespace-shaped delimiters that own their line ends within the node (\[ x \],\[\n…\n\]); a hugging escaped bracket (\[a\],\[REDACTED: …\],see \[ x \] here— the Jira/ADF converter's shapes) stays a literal escape.(is link/image syntax and never converts.](junk input stays linear — the timeout-guarded tests are kept.This is the shape arbitrated in round 6: review rounds 1–6 each surfaced one more non-prose context a source scanner had to enumerate (ADF brackets, link destinations, definitions, escape parity, indented code, and finally raw HTML attributes), a list that can never be shown complete by inspection. The plugin costs no extra parse — it runs inside the parse the renderer already performs — and follows the file's existing "read it off remark's own parse" pattern (
AUTOLINK_PARSER,remarkAutolinkRules).Alternatives considered: enabling
singleDollarTextMath(reintroduces the currency-crash class the guard exists for); a standalone pre-parse mask (measured: the parse itself is super-linear on this PR's own](junk input, ~6.9s at 100kB — acceptable inside the render parse that already pays it, not as an additional pass).Tests
Twenty render-level cases in
MarkdownRenderer.test.tsx(144/144 in the file; 547/547 across the 35 renderer test files): display and inline math render through KaTeX (.katex-display/.katex); literal\[survives inside fenced code, blockquoted fenced code, indented code and inline code; unmatched openers are left alone; ADF/Jira hugging and mid-sentence escaped brackets stay literal; whitespace-padded display converts; link destinations and reference-link definitions keep their escaped parens; escape parity (\\(literal,\\\(converts); the two linear-time guards against pathological unmatched-opener and](junk input; a raw HTMLhrefwith escaped parens survives intact while prose math beside it converts (the round-7 finding — fails on the scanner head); a paragraph is split around display math with no<pre>under a<p>; character references in prose next to converted math decode correctly.Note: the new describe block is appended at the end of the test file — inserting it mid-file surfaced a pre-existing order-sensitivity in a neighboring test (reproducible at base with only test insertion, no source change); left for a separate report rather than folded into this fix.
Manual verification
N/A — unit coverage exercises the full render path (ReactMarkdown → remark-math → rehype-katex) through the real component in jsdom, including the exact payload from the issue.
Related Issues
Fixes #7803
Pattern harvest
Rule candidate: review-prompt
Pattern: "renderer feature enabled for only one delimiter dialect — check what producers actually emit (LLMs emit LaTeX-native
\[ \]/\( \), not$$) before concluding a rendering feature works"Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement