From 248f772ce2ff619cd5527efb8bcc821bb5972ff7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 03:49:25 +0000 Subject: [PATCH] Fix markdown rendering issues and add lint:md to CI (#73) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN.md had Unicode subscripts/superscripts in code blocks and tables that don't render reliably in monospace fonts. These slipped through because lint-md.mjs only checked inside $$...$$ and ```mermaid blocks, and the check script didn't include lint:md at all. Fixes: - Replace Unicode subscripts/arrows/operators in pseudocode block with ASCII equivalents (v₀ -> v[0], ← -> <-, ≠ -> !=, etc.) - Replace Unicode subscripts in key-layout table with inline LaTeX - Replace Q² with Q2 in table header - Fix same class of issues in RESULTS.md code blocks - Expand lint-md.mjs with Rule 5: scan fenced code blocks for Unicode subscripts, superscripts, math arrows, and math operators - Add lint:md to the check script so CI catches markdown issues https://claude.ai/code/session_017b5kMJ5gECv63yLCHJ3SXP --- DESIGN.md | 8 ++-- RESULTS.md | 6 +-- package.json | 2 +- scripts/lint-md.mjs | 100 ++++++++++++++++++++++++++++++++++++++------ 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index fe87b44..8509a4b 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -496,9 +496,9 @@ Given a quantized vector $V = (v_0, v_1, \ldots, v_{n-1}) \in \{0,1,2,3\}^n$, **run-reduction** produces a transition sequence by a single left-to-right pass: ``` -R ← (v₀) +R <- (v[0]) for i in 1..n-1: - if vᵢ ≠ vᵢ₋₁: append vᵢ to R + if v[i] != v[i-1]: append v[i] to R ``` The result $R = (r_0, r_1, \ldots, r_{k-1})$ is the sequence of distinct consecutive @@ -572,7 +572,7 @@ distinction is resolved by the Lee-distance re-ranking step. | b63–62 | b61–60 | b59–58 | b57–56 | b55–54 | b53–52 | … | b5–4 | b3–2 | b1–0 | |:------:|:------:|:------:|:------:|:------:|:------:|:-:|:----:|:----:|:----:| -| r₀ | r₁ | r₂ | r₃ | r₄ | r₅ | … | r₂₉ | r₃₀ | r₃₁ (LSB) | +| $r_0$ | $r_1$ | $r_2$ | $r_3$ | $r_4$ | $r_5$ | … | $r_{29}$ | $r_{30}$ | $r_{31}$ (LSB) | --- @@ -734,7 +734,7 @@ $$S(x) - 1 = \frac{4x}{1 - 3x} = \underbrace{4x}_{S_1} \cdot \underbrace{\frac{1 The first factor $S_1 = 4x$ records the first symbol $r_0$ (4 choices, selecting the block file). The Geode $G = 1/(1-3x) = 1 + 3x + 9x^2 + \cdots$ counts all possible continuations — the tail of the key after the first symbol is fixed. -| Level | Paper | Q² transition key | General quantization | +| Level | Paper | Q2 transition key | General quantization | |:-----:|:------|:------------------|:--------------------| | Full structure | $S$ | All transition sequences | All codewords | | First level | $S_1$ | $r_0$ (first symbol → block file) | Coarse quantization cell | diff --git a/RESULTS.md b/RESULTS.md index 80e1773..00a6eff 100644 --- a/RESULTS.md +++ b/RESULTS.md @@ -29,7 +29,7 @@ This produces components drawn from Uniform[-1, 1]. After L2 normalisation of a E[‖u‖²] = n/3 = 128/3, so each normalised component follows approximately: ``` -v_i ≈ u_i / √(n/3) → Uniform[−√(3/n), √(3/n)] ≈ Uniform[−0.153, 0.153] +v_i ~ u_i / sqrt(n/3) -> Uniform[-sqrt(3/n), sqrt(3/n)] ~ Uniform[-0.153, 0.153] ``` The quantisation threshold is τ* = Φ⁻¹(¾)/√n ≈ 0.6745/√128 ≈ 0.0596, which @@ -49,7 +49,7 @@ producing systematically skewed symbol probabilities: With 500 trials × 128 dimensions = 64,000 total symbols, the predicted χ² is: ``` -χ² = 4 × (3520² / 16000) ≈ 3098 +chi2 = 4 * (3520^2 / 16000) ~ 3098 ``` This matches the observed 3127.86 to within rounding of the approximated marginal @@ -74,7 +74,7 @@ The benchmark was corrected to generate pre-normalisation components from N(0, 1 using Box-Muller, matching the Gaussian assumption under which τ* was derived: ```ts -// Box-Muller: pairs of uniform samples → standard normal pairs +// Box-Muller: pairs of uniform samples -> standard normal pairs for (let i = 0; i < n; i += 2) { const u1 = Math.random(), u2 = Math.random(); const r = Math.sqrt(-2 * Math.log(u1)); diff --git a/package.json b/package.json index d6bf492..9271190 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "lint": "eslint --no-inline-config --max-warnings 0 --fix src test --ext .js,.ts,.html,.yml,.yaml && eslint --no-inline-config --max-warnings 0 src test --ext .js,.ts,.html,.yml,.yaml", "lint:css": "stylelint --max-warnings 0 --allow-empty-input --fix \"**/*.{css,html}\" && stylelint --max-warnings 0 --allow-empty-input \"**/*.{css,html}\"", "lint:md": "bun scripts/lint-md.mjs", - "check": "bun run lint && bun run typecheck", + "check": "bun run lint && bun run lint:md && bun run typecheck", "prebuild": "bun run check && bun run lint:css", "build:wat": "wat2wasm src/q2.wat -o src/q2.wasm && bun run embed-wat", "embed-wat": "bun ./scripts/embed-wat.mjs", diff --git a/scripts/lint-md.mjs b/scripts/lint-md.mjs index 2a9a9a4..e884e63 100644 --- a/scripts/lint-md.mjs +++ b/scripts/lint-md.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env bun /** * lint-md.mjs — Lints Markdown files for encoding issues that break GitHub - * rendering of KaTeX math and Mermaid diagrams. + * rendering of KaTeX math, Mermaid diagrams, code blocks, and tables. * * Checks performed: * 1. Emoji characters (U+1F000+) inside LaTeX $...$ or $$...$$ blocks — @@ -13,6 +13,12 @@ * support and silently corrupt Mermaid output. * 4. Unicode MINUS SIGN (U+2212) anywhere in Mermaid blocks — diagram * labels should use ASCII hyphen-minus. + * 5. Unicode subscript/superscript digits (U+2070–U+209F), modifier + * letters (U+1D00–U+1D9F), mathematical arrows (U+2190–U+21FF), and + * mathematical operators (U+2200–U+22FF) inside fenced code blocks — + * monospace fonts often lack these glyphs. + * 6. (Emoji in prose is intentionally allowed — only emoji inside LaTeX + * math or Mermaid blocks is flagged, as it breaks rendering.) * * Usage: * bun scripts/lint-md.mjs [file.md ...] # lint specific files @@ -31,17 +37,6 @@ const root = join(__dirname, '..'); // ── helpers ────────────────────────────────────────────────────────────────── -/** Split content on $$ boundaries; odd-indexed parts are display math. */ -function displayMathBlocks(content) { - const parts = content.split('$$'); - const blocks = []; - for (let i = 1; i < parts.length; i += 2) { - const start = parts.slice(0, i).join('$$').length + 2; // byte offset approx - blocks.push({ text: parts[i], partIndex: i }); - } - return blocks; -} - /** Extract the content and approximate line number of each ```mermaid block. */ function mermaidBlocks(lines) { const blocks = []; @@ -63,6 +58,30 @@ function mermaidBlocks(lines) { return blocks; } +/** Extract the content and line numbers of non-Mermaid fenced code blocks. */ +function fencedCodeBlocks(lines) { + const blocks = []; + let inside = false; + let isMermaid = false; + let buf = []; + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (/^(`{3,}|~{3,})/.test(trimmed)) { + if (!inside) { + inside = true; + isMermaid = trimmed === '```mermaid'; + buf = []; + } else { + if (!isMermaid) blocks.push(buf); + inside = false; + } + } else if (inside) { + buf.push({ text: lines[i], lineNo: i + 1 }); + } + } + return blocks; +} + /** * Compute 1-based line number of a character offset within content. * Used to report line numbers for math-block violations. @@ -116,7 +135,7 @@ function checkDisplayMath(content, filePath) { } /** - * Rule 2 & 3: scan Mermaid blocks for disallowed Unicode. + * Rule 3 & 4: scan Mermaid blocks for disallowed Unicode. */ function checkMermaid(content, filePath) { const lines = content.split('\n'); @@ -163,6 +182,60 @@ function checkMermaid(content, filePath) { return violations; } +/** + * Rule 5: scan fenced code blocks for Unicode characters that do not render + * reliably in monospace fonts. Catches subscripts, superscripts, modifier + * letters, mathematical arrows, and mathematical operators. + */ +function checkCodeBlocks(content, filePath) { + const lines = content.split('\n'); + const violations = []; + + for (const block of fencedCodeBlocks(lines)) { + for (const { text, lineNo } of block) { + for (let j = 0; j < text.length; ) { + const cp = text.codePointAt(j); + const advance = cp > 0xffff ? 2 : 1; + + // Unicode subscript/superscript digits & letters: + // U+2070–U+209F (superscripts and subscripts) + // U+1D00–U+1D9F (phonetic/modifier letters used as subscripts) + if ( + (cp >= 0x2070 && cp <= 0x209f) || + (cp >= 0x1d00 && cp <= 0x1d9f) + ) { + violations.push({ + file: filePath, line: lineNo, + message: `Unicode subscript/superscript U+${cp.toString(16).toUpperCase()} ('${String.fromCodePoint(cp)}') in code block — use plain ASCII instead`, + }); + } + + // Mathematical arrows (U+2190–U+21FF): ← → ↑ ↓ etc. + if (cp >= 0x2190 && cp <= 0x21ff) { + violations.push({ + file: filePath, line: lineNo, + message: `Unicode arrow U+${cp.toString(16).toUpperCase()} ('${String.fromCodePoint(cp)}') in code block — use ASCII equivalent instead`, + }); + } + + // Mathematical operators (U+2200–U+22FF): ≠ ≤ ≥ etc. + if (cp >= 0x2200 && cp <= 0x22ff) { + violations.push({ + file: filePath, line: lineNo, + message: `Unicode math operator U+${cp.toString(16).toUpperCase()} ('${String.fromCodePoint(cp)}') in code block — use ASCII equivalent instead`, + }); + } + + j += advance; + } + } + } + return violations; +} + +// Emoji in prose is intentionally allowed (only emoji inside LaTeX math or +// Mermaid blocks is flagged by the block-specific rules above). + // ── main ────────────────────────────────────────────────────────────────────── const args = process.argv.slice(2); @@ -187,6 +260,7 @@ for (const filePath of files) { const violations = [ ...checkDisplayMath(content, filePath), ...checkMermaid(content, filePath), + ...checkCodeBlocks(content, filePath), ]; for (const { file, line, message } of violations) {