diff --git a/src/visual_report.py b/src/visual_report.py
index d62362e69..b826c0b7b 100644
--- a/src/visual_report.py
+++ b/src/visual_report.py
@@ -107,6 +107,13 @@ def _extract_headings(md_text: str) -> List[Dict[str, str]]:
headings = []
seen_slugs: Dict[str, int] = {}
+ # Strip fenced code blocks before scanning for "## ..." lines: a heading-
+ # looking comment inside ``` / ~~~ is NOT rendered as an
by the
+ # markdown renderer, so counting it here desynced the TOC anchor ids
+ # (built by zipping these headings against the rendered /), making
+ # every later TOC link point at the wrong section.
+ md_text = re.sub(r'(?ms)^[ \t]*(`{3,}|~{3,})[^\n]*\n.*?^[ \t]*\1[ \t]*$', '', md_text)
+
def _plain_heading_text(text: str) -> str:
text = text.strip().rstrip("#").strip()
text = re.sub(r'!\[([^\]]*)\]\([^)]+\)', r'\1', text)
diff --git a/tests/test_visual_report_toc_code_fence.py b/tests/test_visual_report_toc_code_fence.py
new file mode 100644
index 000000000..617ea4a6a
--- /dev/null
+++ b/tests/test_visual_report_toc_code_fence.py
@@ -0,0 +1,28 @@
+"""TOC heading extraction must ignore headings inside code fences.
+
+A "## ..." comment inside a ``` or ~~~ block is not rendered as an , but
+_extract_headings counted it, so _apply_heading_ids (which zips TOC headings
+against rendered / by position) gave later sections the wrong anchor
+id and the trailing TOC link went dead.
+"""
+import pytest
+
+pytest.importorskip("bs4")
+
+from src.visual_report import _extract_headings
+
+
+def test_backtick_fenced_heading_is_ignored():
+ md = "## Intro\n\n```bash\n## not a heading\n```\n\n## Conclusion"
+ assert [h["text"] for h in _extract_headings(md)] == ["Intro", "Conclusion"]
+
+
+def test_tilde_fenced_heading_is_ignored():
+ md = "## A\n\n~~~\n## fake\n~~~\n\n## B"
+ assert [h["text"] for h in _extract_headings(md)] == ["A", "B"]
+
+
+def test_normal_headings_unaffected():
+ md = "## One\n\nsome text\n\n### Two"
+ out = [(h["level"], h["text"]) for h in _extract_headings(md)]
+ assert out == [(2, "One"), (3, "Two")]