From 78d12a9ecaeb3da5c6b010f693bae02865e0427e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 27 Sep 2026 17:42:41 -0400 Subject: [PATCH 01/13] harness v2: rebuild check-knowledge.py on a spec-compliant parser; suites + conformance gate in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 27 review rounds on PR #1 were all Markdown-parsing edge cases in a hand-rolled parser. This replaces that parser with markdown-it-py (CommonMark 0.31.2 preset) and compares the two copies of each lesson by their rendered, visibility-normalised HTML. - harness/check-knowledge.py: 1,345 -> 551 lines; same check names, messages and CLI contract - harness/knowledge_md.py (37 lines): the one parser, with two patches that align markdown-it-py 4.0.0 with CommonMark 0.31.2: link labels capped at 999 characters; declaration blocks start at any ASCII letter and HTML-block patterns take only space/tab as whitespace (not NBSP) - harness/conformance/: differential gate. The parser must render all 652 spec examples and 36 edge cases like commonmark.js 0.31.2 (the reference); two reviewed exceptions where commonmark.js departs from the spec text on NBSP (cmark and cmark-gfm agree with the spec) - harness/tests/: the 199 negative-test cases from the PR #1 review, repo-relative, plus runner; four assertions corrected where the reference proves the old reading wrong (neg18 5, neg19 1, neg19 10, neg22 F1), two replacement cases added -> 201 cases - CI: dependency pin, suites job, conformance job - README: lesson feed documented as paused (retired 2026-09-27), harness layout, how to run; CONTRIBUTING: harness change rules, ledger open->merged convention - verified/2026-09-26_claude-surfaces_v3.md: proof repo renamed to cloud-launch-pad; v2 bannered - LEDGER: PR #1 rows flipped to merged (7946224), this PR's row Evidence (local): check-knowledge.py RESULT: PASS — 12 pass, 0 warn, 0 fail; run.sh TOTAL: PASS — 17 suites, 201 cases, 0 failing; conformance RESULT: PASS — 652/652 spec, 34/36 cases + 2 reviewed exceptions; mutation check: 6/6 deliberately broken checkers caught (1–70 cases red each); markdownlint-cli2 0.23.3: 0 issues. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK --- .github/workflows/lint.yml | 29 + CONTRIBUTING.md | 9 +- LEDGER.md | 5 +- README.md | 22 +- harness/check-knowledge.py | 1480 +++++---------------- harness/conformance/cases.json | 182 +++ harness/conformance/check.py | 60 + harness/conformance/reference.cjs | 12 + harness/knowledge_md.py | 52 + harness/requirements.txt | 1 + harness/tests/README.md | 19 + harness/tests/neg11.sh | 107 ++ harness/tests/neg12.sh | 74 ++ harness/tests/neg13.sh | 53 + harness/tests/neg14.sh | 95 ++ harness/tests/neg15.sh | 75 ++ harness/tests/neg16.sh | 69 + harness/tests/neg17.sh | 47 + harness/tests/neg18.sh | 77 ++ harness/tests/neg19.sh | 98 ++ harness/tests/neg20.sh | 68 + harness/tests/neg21.sh | 61 + harness/tests/neg22.sh | 129 ++ harness/tests/neg23.sh | 116 ++ harness/tests/neg24.sh | 96 ++ harness/tests/neg25.sh | 90 ++ harness/tests/neg26.sh | 56 + harness/tests/neg27.sh | 66 + harness/tests/run.sh | 19 + verified/2026-09-26_claude-surfaces_v2.md | 2 + verified/2026-09-26_claude-surfaces_v3.md | 45 + 31 files changed, 2172 insertions(+), 1142 deletions(-) create mode 100644 harness/conformance/cases.json create mode 100644 harness/conformance/check.py create mode 100644 harness/conformance/reference.cjs create mode 100644 harness/knowledge_md.py create mode 100644 harness/requirements.txt create mode 100644 harness/tests/README.md create mode 100755 harness/tests/neg11.sh create mode 100755 harness/tests/neg12.sh create mode 100755 harness/tests/neg13.sh create mode 100755 harness/tests/neg14.sh create mode 100755 harness/tests/neg15.sh create mode 100755 harness/tests/neg16.sh create mode 100755 harness/tests/neg17.sh create mode 100755 harness/tests/neg18.sh create mode 100755 harness/tests/neg19.sh create mode 100755 harness/tests/neg20.sh create mode 100755 harness/tests/neg21.sh create mode 100755 harness/tests/neg22.sh create mode 100755 harness/tests/neg23.sh create mode 100755 harness/tests/neg24.sh create mode 100755 harness/tests/neg25.sh create mode 100755 harness/tests/neg26.sh create mode 100755 harness/tests/neg27.sh create mode 100755 harness/tests/run.sh create mode 100644 verified/2026-09-26_claude-surfaces_v3.md diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3954627..0602b8e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,4 +28,33 @@ jobs: - uses: actions/setup-python@v5 with: python-version: "3.12" + - run: pip install -r harness/requirements.txt - run: python3 harness/check-knowledge.py + + suites: + name: negative-test suites + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install -r harness/requirements.txt + - run: bash harness/tests/run.sh + + conformance: + name: CommonMark conformance + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: pip install -r harness/requirements.txt + # The reference implementation and the official spec examples, pinned to CommonMark 0.31.2. + - run: npm install --no-save --no-audit --no-fund --prefix "$RUNNER_TEMP/ref" commonmark@0.31.2 commonmark-spec@0.31.2 + - run: node harness/conformance/reference.cjs "$RUNNER_TEMP/ref/node_modules" > "$RUNNER_TEMP/ref.json" + - run: python3 harness/conformance/check.py "$RUNNER_TEMP/ref.json" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index be1286c..b021063 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,12 +27,19 @@ verified fact sheets and fix things. These rules keep them from colliding. - Hand or session changes go on `claude/` and land through a pull request. Connector commits are the one exception. - The PR body says what changed, pastes the `check-knowledge.py` result, and - names the risk. + names the risk. A change under `harness/` also pastes `bash harness/tests/run.sh`; + a change to `harness/knowledge_md.py` or the markdown-it-py pin must keep the + conformance gate green (see the `conformance` job in `.github/workflows/lint.yml`). +- Found a Markdown shape the checker gets wrong? Render it with the reference + implementation (commonmark.js 0.31.2) first, then add it to + `harness/conformance/cases.json` and a failing case to `harness/tests/` before fixing. - Merge only on green CI. ## Ledger Every hand or session change adds one row to `LEDGER.md` (newest first). +A row written inside its own pull request says `open`; the next ledger change +flips it to `merged` or `closed` with the merge commit. Connector runs are not logged there; their trail is the git history of `knowledge.md`. diff --git a/LEDGER.md b/LEDGER.md index 088577a..81350bf 100644 --- a/LEDGER.md +++ b/LEDGER.md @@ -9,7 +9,8 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`. | Date | Session | Branch | PR | Outcome | Evidence | | --- | --- | --- | --- | --- | --- | -| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | open | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded | -| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | open | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) | +| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 551 lines + 37-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 36 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`) | +| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded | +| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) | | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` | | 2026-08-24 | Cowork session | `main` | — | direct | commits `27aa529`…`0763bf4`: repo bootstrap, first 5 lessons (`cfd032a`), README | diff --git a/README.md b/README.md index e23d824..b3aee5e 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,13 @@ Written mostly by a machine. The daily.dev MCP connector (Cloudflare Worker add verified fact sheets; humans fix typos. Run state (seen-post watermark, counters) lives in Cloudflare KV, not here. +> **Status (2026-09-27): the lesson feed is paused.** The scheduled run +> ("daily-dev-agentic · daily learn") never had the daily.dev connector +> attached, so from 2026-08-24 it exited early every day while reporting +> success; T retired it on 2026-09-27. The last lessons are from 2026-08-24. +> To restart: add the connector in claude.ai (Settings → Connectors → custom +> connector), enable it on that scheduled task, and switch the task back on. + ## Layout | Path | What it holds | Written by | @@ -23,6 +30,9 @@ counters) lives in Cloudflare KV, not here. | `lessons/YYYY-MM-DD.md` | The same lessons, one file per day | Connector | | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained | | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions | +| `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions | +| `harness/tests/` | 201 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions | +| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 36 edge cases like the reference implementation | Sessions | | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits | ## How a lesson is shaped @@ -51,12 +61,20 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad. ## Checking it ```bash +pip install -r harness/requirements.txt # markdown-it-py, pinned python3 harness/check-knowledge.py # PASS/FAIL lines python3 harness/check-knowledge.py --json # same, as one JSON object +bash harness/tests/run.sh # the 201 negative-test cases ``` -CI runs this plus markdownlint on every pull request and on every push to -`main` (`.github/workflows/lint.yml`). Run it locally before pushing a branch. +The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser +rather than pattern matching. Two copies of a lesson count as the same when they +render to the same visible HTML. CI runs the checker, the test suites, +markdownlint and a conformance gate against the reference implementation on +every pull request and every push to `main` (`.github/workflows/lint.yml`). +GitHub itself renders with an older spec (cmark-gfm, 0.29): in rare edge cases, +such as a lowercase `` line, GitHub shows text where 0.31.2 hides a +declaration. ## Hand edits diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py index b50fb41..60188de 100644 --- a/harness/check-knowledge.py +++ b/harness/check-knowledge.py @@ -14,19 +14,23 @@ paragraph, heading, rule, code or raw HTML — no day file carries it) lessons/ one regular (non-symlink) YYYY-MM-DD.md per day section in knowledge.md, headed `# Lessons — `, no orphans, - no `## ` headings of its own, and lesson content identical - (title, why, do, source — continuation lines included) to - that day's section in knowledge.md, in the same order + no H2 headings of its own, and every lesson RENDERING identically + to that day's section in knowledge.md, in the same order verified/ only regular files named YYYY-MM-DD_topic_vN.md (real dates) - whose first visible non-blank line is a real H1 with text (not indented 4+; - a code block or raw HTML before it is visible, a comment is not) - Text inside HTML comments, fenced code blocks or indented code blocks never - counts as content; a copy that turns prose into code (or back) is a difference. + whose first visible block is an ATX H1 with text (a code block or + raw HTML before it is visible, a comment or reference definition is not) + +How: Markdown is parsed by markdown-it-py (CommonMark 0.31.2 preset; CI pins it and +checks it against the reference implementation on all 652 spec examples), so block +structure is the parser's, not ours. Two copies of a lesson are the same when they +render to the same HTML once invisible markup (comments, processing instructions, +declarations, CDATA) is removed and whitespace is collapsed the way a browser does +outside
 and .
 
 Contract: one line per check (PASS|WARN|FAIL name evidence), final RESULT
 line, exit 0 only when nothing FAILed. `--json` prints one JSON object.
 
-Run:  python3 harness/check-knowledge.py [--json]
+Run:  pip install -r harness/requirements.txt && python3 harness/check-knowledge.py [--json]
 """
 from __future__ import annotations
 
@@ -36,195 +40,44 @@
 import re
 import sys
 
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
+try:
+    from knowledge_md import MD                       # markdown-it-py, CommonMark preset, conformance-patched
+except ImportError:                                   # the contract still holds: a FAIL line, a RESULT line, exit 1
+    print("FAIL harness:deps             markdown-it-py is not installed — run: pip install -r harness/requirements.txt")
+    print("----")
+    print("RESULT: FAIL — 0 pass, 0 warn, 1 fail")
+    sys.exit(1)
+
 ROOT = pathlib.Path(__file__).resolve().parents[1]
 RESULTS: list[dict] = []
 
 KNOWLEDGE_H1 = "# daily-dev-agentic knowledge — T agent"   # the connector depends on this exact line
-
-# CommonMark whitespace is space and tab only: a NBSP (U+00A0) or any other Unicode space is
-# CONTENT — `## 2026-08-24` is not the date heading, a NBSP-only line is not blank, `-x`
-# is not a list item. Structural checks therefore never use `\s`, `\S` or bare `.strip()`.
-DATE_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
-SECTION_RE = re.compile(r"^ {0,3}## ([0-9]{4}-[0-9]{2}-[0-9]{2})[ \t]*$")
-LESSON_RE = re.compile(r"^ {0,3}-([ \t]+)\*\*(.+?)\*\*[ \t]*$")     # bold lesson bullet: `-`, 1-4 columns of whitespace (checked after), **title**
-TOP_BULLET_RE = re.compile(r"^ {0,3}(?:[-*+]|[0-9]{1,9}[.)])(?:[ \t]+[^ \t]|[ \t]*$)")   # any top-level list item incl. an EMPTY one; ordered markers are 1-9 digits (CommonMark)
-FIELD_LABEL_RE = re.compile(r"^( *)-([ \t]+)(Why it matters here|Do|Source):(.*)$")   # label first; 1-4 columns after `-` (5+ = code), nesting checked by indent
-# Blocks that interrupt a paragraph (CommonMark): an ATX heading, a block quote, a thematic break.
-# A line like this at top level is never lazy continuation of the lesson above it.
-THEMATIC_BREAK = r"(?:(?:-[ \t]*){3,}|(?:\*[ \t]*){3,}|(?:_[ \t]*){3,})$"   # 3+ of the SAME character (`- * -` is not a break)
-INTERRUPT_RE = re.compile(r"^ {0,3}(?:#{1,6}(?:[ \t]|$)|>|" + THEMATIC_BREAK + ")")
-ATX_RE = re.compile(r"^ {0,3}#{1,6}(?:[ \t]|$)")                                   # any ATX heading
-_REF_TITLE = r"""(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\((?:[^()\\]|\\.)*\))"""
-REF_TITLE_RE = re.compile(r"^[ \t]*" + _REF_TITLE + r"[ \t]*$")   # a ref def's title alone on the next line
-_REF_LABEL_RE = re.compile(r"^ {0,3}\[((?:\\.|[^\[\]\\])+)\]:[ \t]*")   # `[label]:` — no unescaped brackets inside the label
-_REF_TITLE_TAIL_RE = re.compile(r"^(?:[ \t]+(?P" + _REF_TITLE + r"))?[ \t]*$")
-
-
-def indent_of(ln: str) -> int:
-    return len(ln) - len(ln.lstrip(" "))
-
-
-def blank(ln: str) -> bool:
-    """A blank line the CommonMark way: nothing but spaces and tabs (a NBSP-only line is a paragraph)."""
-    return ln.strip(" \t") == ""
-
-
-def scan_destination(text: str, i: int) -> int | None:
-    """Index just past a bare link destination starting at `text[i]`, or None when there is none.
-
-    CommonMark: no spaces or ASCII control characters, no unescaped `<` or `>`, and unescaped
-    parentheses must balance — `https://x.y/(oops` never closes, so it is not a destination.
-    """
-    depth = 0
-    while i < len(text):
-        c = text[i]
-        if c == " " or c == "\t" or ord(c) < 32 or c == "\x7f" or c in "<>":
-            break
-        if c == "\\" and i + 1 < len(text) and text[i + 1] in ASCII_PUNCT:
-            i += 2                                   # `\(` is destination text, not a delimiter
-            continue
-        if c == "(":
-            depth += 1
-        elif c == ")":
-            if depth == 0:
-                break
-            depth -= 1
-        i += 1
-    if i < len(text) and text[i] in "<>":
-        return None
-    return i if depth == 0 else None
-
-
-def link_ref_def(ln: str) -> tuple[str, str, str | None] | None:
-    """None unless `ln` is a COMPLETE link reference definition `[label]: destination ["title"]`;
-    otherwise (normalised label, destination, title) — title None when one may still follow on
-    the next line.
-    The label needs a non-whitespace character and no unescaped brackets; the destination is
-    `<…>` (no `<`, `>` inside) or a bare one with balanced parentheses; anything else after it
-    makes the line prose."""
-    lm = _REF_LABEL_RE.match(ln)
-    if not lm or not lm.group(1).strip(" \t") or len(lm.group(1)) > 999:   # CommonMark: at most 999 characters in a label
-        return None
-    i = lm.end()
-    if i < len(ln) and ln[i] == "<":
-        j = i + 1                                    # `<…>`: ends at the first UNESCAPED `>`; an unescaped `<` inside breaks it
-        while j < len(ln) and ln[j] not in "<>":
-            j += 2 if ln[j] == "\\" and j + 1 < len(ln) and ln[j + 1] in ASCII_PUNCT else 1
-        if j >= len(ln) or ln[j] != ">":
-            return None
-        end = j + 1
-    else:
-        end = scan_destination(ln, i)
-        if end is None or end == i:
-            return None
-    tm = _REF_TITLE_TAIL_RE.match(ln[end:])
-    if not tm:
-        return None
-    dest = ln[i + 1:end - 1] if ln[i] == "<" else ln[i:end]
-    return (ref_label(lm.group(1)), dest, tm.group("title"))
-
-
-def ref_label(label: str) -> str:
-    """A link label the way CommonMark matches it: case-folded, inner whitespace collapsed."""
-    return " ".join(label.casefold().split())
-
-
-def first_visible(lines: list[str]) -> str:
-    """The first line a reader sees as content: blank lines, comment blocks, link reference
-    definitions (and, for a definition that has no title yet, its title on the following
-    line — a quoted line after a definition that already carries one is a paragraph) are
-    skipped. A continuation line (the prose after a multi-line inline comment's `-->`)
-    counts as visible only when it carries text."""
-    k = 0
-    while k < len(lines):
-        ln = lines[k]
-        if ln.startswith(CONT_MARK):
-            tail = ln[len(CONT_MARK):]
-            if not blank(tail):
-                return tail
-            k += 1
-            continue
-        if blank(ln) or mark_kind(ln) == COMMENT_MARK:
-            k += 1
-            continue
-        if is_mark(ln):
-            return mark_token(ln)                    # a code block or raw HTML is visible content
-        rd = link_ref_def(ln)
-        if rd is not None:
-            k += 1
-            if rd[2] is None and k < len(lines) and REF_TITLE_RE.match(lines[k]):
-                k += 1
-            continue
-        return ln
-    return ""
-LINK_LABEL = r"\[(?:\\.|[^\]\\])+\]"                                          # allows escaped \] inside the label
-LINK_LABEL_RE = re.compile(r"\[((?:\\.|[^\[\]\\])+)\]")                          # every bracketed label candidate in prose
-LINK_OPEN_RE = re.compile(r"^" + LINK_LABEL + r"\(")                       # `[label](` — the destination is scanned by hand
-INLINE_TAG_RE = re.compile(   # a CommonMark inline open or closing tag (attributes included): raw HTML, not prose
-    r"<[A-Za-z][A-Za-z0-9-]*(?:[ \t]+[A-Za-z_:][A-Za-z0-9_.:-]*(?:[ \t]*=[ \t]*(?:[^ \t\"'=<>`]+|'[^']*'|\"[^\"]*\"))?)*[ \t]*/?>"
-    r"|</[A-Za-z][A-Za-z0-9-]*[ \t]*>")
-CONFIDENCE_RE = re.compile(r"(?<![A-Za-z0-9_])confidence ([0-9]{1,3})%(?![0-9%])")   # a standalone label: `overconfidence 80%` is not one
-ASCII_PUNCT = set(r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""")   # a backslash escapes exactly these (CommonMark)
-
-
-def parse_source(text: str) -> tuple[str, int] | None:
-    """(destination, confidence) for `[label](https://…) … confidence N%`, else None.
-
-    The destination is read the CommonMark way: no whitespace, and parentheses must
-    balance — `https://x.y/(broken)` never closes, so it is not a link at all.
-    """
-    text = text.strip(" \t")
-    if not text.startswith("["):
-        return None
-    j, bdepth = 1, 0                                 # the label: balanced, unescaped brackets are allowed inside
-    while j < len(text):
-        c = text[j]
-        if c == "\\" and j + 1 < len(text) and text[j + 1] in ASCII_PUNCT:
-            j += 2
-            continue
-        if c == "[":
-            bdepth += 1
-        elif c == "]":
-            if bdepth == 0:
-                break
-            bdepth -= 1
-        j += 1
-    if j >= len(text) or j == 1 or j + 1 >= len(text) or text[j + 1] != "(":
-        return None
-    i = scan_destination(text, j + 2)
-    if i is None or i >= len(text) or text[i] != ")":
-        return None                              # no whitespace, no unescaped < >, parentheses balance, `)` closes
-    dest = text[j + 2:i]
-    if not re.match(r"https?://[^ \t]+$", dest):
-        return None
-    cm = CONFIDENCE_RE.search(INLINE_TAG_RE.sub(" ", text[i + 1:]))   # `<br title="confidence 80%">` is not a visible label
-    return (dest, int(cm.group(1))) if cm else None
+SECTION_RE = re.compile(r"^ {0,3}## ([0-9]{4}-[0-9]{2}-[0-9]{2})[ \t]*$")   # exact: a NBSP or closing `##` is not the day heading
 DAY_H1_RE = re.compile(r"^# Lessons — ([0-9]{4}-[0-9]{2}-[0-9]{2})[ \t]*$")
+DATE_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}$")
 VERIFIED_NAME_RE = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}_[a-z0-9-]+_v[0-9]+\.md$")
-_H1_OPEN_RE = re.compile(r"^ {0,3}#(?:[ \t]+(.*))?$")   # a `#` heading line: ≤3 leading spaces (4 = code), space/tab or EOL after `#`
-
-
-def h1_text(ln: str) -> str | None:
-    """The content of an ATX H1, or None when the line is not one. CommonMark strips the optional
-    closing sequence (`#`s preceded by a space or tab, or making up the whole content): `# ##`
-    is an H1 with NO text; `# Title ##` is "Title"; `# \#` keeps its escaped hash."""
-    m = _H1_OPEN_RE.match(ln)
-    if not m:
-        return None
-    content = (m.group(1) or "").strip(" \t")
-    if re.fullmatch(r"#+", content):
-        return ""
-    return re.sub(r"[ \t]+#+$", "", content).strip(" \t")
-
-
-def is_real_h1(ln: str) -> bool:
-    return bool(h1_text(ln))
+LABELS = {"Why it matters here": "why", "Do": "do", "Source": "source"}
+FIELD_RE = re.compile(r"(Why it matters here|Do|Source):")
 FIELDS = ("why", "do", "source")
-H2_RE = re.compile(r"^ {0,3}##(?:[ \t]|$)")   # any ATX H2 (≤3-space indent), including a bare `##`; NBSP is not a separator
-bad_headings: list[str] = []   # filled by parse_lessons: H2s that are not valid day headings
-SETEXT_H2_RE = re.compile(r"^ {0,3}-+[ \t]*$")   # a dash underline directly under a top-level paragraph line makes it an H2
-misplaced: list[str] = []      # filled by parse_lessons: top-level bullets that sit under no `## YYYY-MM-DD`
-stray: list[str] = []          # filled by parse_lessons: visible top-level blocks inside a day section that are not lessons
+CONFIDENCE_RE = re.compile(r"(?<![A-Za-z0-9_])confidence ([0-9]{1,3})%(?![0-9%])")   # a standalone label only
+MARKER_RE = re.compile(r"^[ \t]*(?:[-*+]|[0-9]{1,9}[.)])[ \t]*")
+WS = " \t\n\r\f"                                    # HTML/CommonMark whitespace; a NBSP is content
+
+# Raw HTML a browser never displays. Each alternative also swallows an unterminated
+# opener to the end, because an unclosed comment hides everything after it.
+INVISIBLE_RE = re.compile(
+    r"<!--.*?(?:-->|\Z)|<\?.*?(?:\?>|\Z)|<!\[CDATA\[.*?(?:\]\]>|\Z)|<![A-Za-z][^>]*(?:>|\Z)", re.S)
+PROTECT_RE = re.compile(r"<pre\b.*?</pre>|<code\b.*?</code>", re.S | re.I)   # whitespace inside code is content
+BLOCK_TAG_RE = re.compile(
+    r"[ \t\n\r\f]*(</?(?:address|article|aside|blockquote|br|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|"
+    r"footer|form|h[1-6]|header|hgroup|hr|li|main|nav|ol|p|pre|search|section|summary|table|tbody|td|tfoot|th|thead|"
+    r"tr|ul)\b[^>]*>)[ \t\n\r\f]*", re.I)
+# Raw-HTML blocks that only end at a terminator (CommonMark types 1-5); the parser also ends them
+# when their container ends, but the OUTPUT then holds unterminated HTML that a browser reads past.
+HTML_ENDS = ((re.compile(r"<(script|pre|style|textarea)(?:[ \t>]|$)", re.I), None),
+             (re.compile(r"<!--"), "-->"), (re.compile(r"<\?"), "?>"),
+             (re.compile(r"<!\[CDATA\["), "]]>"), (re.compile(r"<![A-Za-z]"), ">"))
 
 
 def record(status: str, name: str, detail: str) -> None:
@@ -252,902 +105,255 @@ def valid_date(s: str) -> bool:
         return False
 
 
-def norm(s: str) -> str:
-    """Collapse runs of spaces/tabs in prose (a NBSP is content and stays), but keep inline code
-    verbatim (`printf 'a  b'` means two spaces)."""
-    spans: list[str] = []
-    parts: list[str] = []
-    i = 0
-    for a, b in code_spans(s):
-        parts.append(s[i:a])
-        spans.append(s[a:b])
-        parts.append(f"\x02{len(spans) - 1}\x02")
-        i = b
-    parts.append(s[i:])
-    out = " ".join(t for t in re.split(r"[ \t]+", "".join(parts).strip(" \t")) if t)
-    return re.sub(r"\x02(\d+)\x02", lambda m: spans[int(m.group(1))], out)
-
-
-HARD_BREAK = "⏎"   # joins two lines of a field when the first ends in a hard line break (2+ spaces or an odd backslash)
-PARA_BREAK = "¶"   # joins two paragraphs of the same field (a blank line between the field and its continuation)
-
-
-def ends_hard(ln: str) -> bool:
-    """True when a following line would render after a hard line break, not a soft one."""
-    if ln.endswith("  "):
-        return True
-    if not ln.endswith("\\"):
-        return False                                  # `text\ ` — the space, not the backslash, ends the line
-    return (len(ln) - len(ln.rstrip("\\"))) % 2 == 1
-
-
 def regular_file(p: pathlib.Path) -> bool:
-    """True only for a plain file: symlinks (live or dangling) and directories are rejected.
-
-    `is_file()` follows a symlink, so a link pointing outside the repo would be read
-    and checked as if it were repo content; `lstat()` looks at the entry itself.
-    """
+    """A plain file only: symlinks (live or dangling) and directories are rejected, because
+    `is_file()` follows a link and would check content from outside the repo."""
     try:
         return not p.is_symlink() and p.is_file()
     except OSError:
         return False
 
 
-def read_lines(p: pathlib.Path) -> list[str] | None:
-    """Lines of a UTF-8 text file (a leading BOM is encoding metadata, not content, and is
-    dropped), or None (with a FAIL recorded) when it is not valid UTF-8."""
+def read_text(p: pathlib.Path) -> str | None:
+    """A UTF-8 file's text with a leading BOM dropped and CR / CRLF made LF (CommonMark line
+    endings), or None with a FAIL recorded when it is not valid UTF-8."""
     try:
         text = p.read_text(encoding="utf-8-sig")
     except UnicodeDecodeError as exc:
         fail(f"utf8:{p.name}", f"not valid UTF-8 at byte {exc.start}: {exc.reason}")
         return None
-    # CommonMark line endings are CR, LF and CRLF only; U+2028, U+000C etc. stay inside a line.
-    lines = re.split(r"\r\n|\r|\n", text)
-    if lines and lines[-1] == "":
-        lines.pop()
-    return lines
-
-
-FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})(.*)$")
-FENCE_MARK = "\x00fence"       # left by sanitize() where a fenced block opened, at its indent: a block boundary, never text
-COMMENT_MARK = "\x00comment"   # same, where a NON-RENDERING HTML block opened: comment, `<?…?>`, `<!DECL>`, CDATA (types 2-5)
-CONT_MARK = "\x00cont"         # prefix: this line is paragraph continuation text (it began inside an inline comment)
-LIST_ITEM_RE = re.compile(r"^( *)([-*+]|[0-9]{1,9}[.)])([ \t]+)[^ \t]")   # marker + whitespace + content (for content-column tracking)
-THEMATIC_RE = re.compile(r"^ {0,3}" + THEMATIC_BREAK)                 # `---`, `- - -`, `***`: a thematic break outranks a list item
-_HTML6 = ("address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|"
-          "fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|"
-          "menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul")
-HTML_MARK = "\x00html"         # left by sanitize() where a RENDERING HTML block opened (types 1, 6, 7)
-MARKS = (FENCE_MARK, COMMENT_MARK, HTML_MARK)
-PAYLOAD_SEP = "\x01"          # a fence/HTML mark may carry the block's text after this: `<indent>MARK\x01line⏎line`
-BLOCK_SEP = "⏎"
-_HTML_TYPE1 = re.compile(r"^ {0,3}<(script|pre|style|textarea)(?:[ \t]|>|$)", re.IGNORECASE)   # tag whitespace is space/tab only
-_HTML_TYPE7 = re.compile(   # a complete open or close tag alone on the line (cannot interrupt a paragraph)
-    r"^ {0,3}(?:<[A-Za-z][A-Za-z0-9-]*(?:[ \t]+[A-Za-z_:][A-Za-z0-9_.:-]*(?:[ \t]*=[ \t]*(?:[^ \t\"'=<>`]+|'[^']*'|\"[^\"]*\"))?)*[ \t]*/?>"
-    r"|</[A-Za-z][A-Za-z0-9-]*[ \t]*>)[ \t]*$")
-
-
-def html_block_start(rel: str, in_paragraph: bool) -> tuple[str, str | None] | None:
-    """(mark, end) when `rel` (a line with its container indent stripped) opens a CommonMark
-    HTML block: `end` is the substring that closes it (on this or a later line, that line
-    included), or None for a block that ends at the next blank line. Type 7 never starts
-    inside a paragraph."""
-    m = _HTML_TYPE1.match(rel)
-    if m:
-        return HTML_MARK, "</" + m.group(1).lower() + ">"
-    # Types 2-5 (comment, processing instruction, declaration, CDATA) are raw HTML that a browser
-    # never displays: they leave COMMENT_MARK, the non-rendering mark. Types 1, 6 and 7 render.
-    if re.match(r"^ {0,3}<!--", rel):
-        return COMMENT_MARK, "-->"
-    if re.match(r"^ {0,3}<\?", rel):
-        return COMMENT_MARK, "?>"
-    if re.match(r"^ {0,3}<!\[CDATA\[", rel):
-        return COMMENT_MARK, "]]>"
-    if re.match(r"^ {0,3}<![A-Z]", rel):         # uppercase only (CommonMark type 4); `<!foo>` is text
-        return COMMENT_MARK, ">"
-    if re.match(r"^ {0,3}</?(?:" + _HTML6 + r")(?:[ \t]|/?>|$)", rel, re.IGNORECASE):
-        return HTML_MARK, None
-    if not in_paragraph and _HTML_TYPE7.match(rel):
-        return HTML_MARK, None
-    return None
-
-
-def has_inner_strong(s: str) -> bool:
-    """True when `s` (a would-be title, code spans masked) holds a live `**`: one whose run of
-    preceding backslashes is EVEN (`\\**` is an escaped backslash and then a real delimiter,
-    `\**` is an escaped star). Every position is tested, so `\***` — an escaped star followed
-    by a live `**` — is caught too. A live `**` inside the title means it is not ONE strong span."""
-    for a in range(len(s) - 1):
-        if s[a] == "*" and s[a + 1] == "*" and (a - len(s[:a].rstrip("\\"))) % 2 == 0:
-            return True
-    return False
-
-
-def interrupts_paragraph(ln: str, eff: int) -> bool:
-    """CommonMark: can `ln` (leading tabs expanded), read inside the list item whose content
-    column is `eff`, interrupt a paragraph? An ATX heading, a thematic break, a block quote,
-    a fence, an HTML block of types 1-6 and a non-empty list item (an ordered one only when it
-    starts at 1) can; anything indented 4+ columns past `eff` cannot — indented code never
-    interrupts a paragraph, it is lazy continuation text there."""
-    rel = ln[eff:]
-    if len(rel) - len(rel.lstrip(" ")) > 3:
-        return False
-    li = LIST_ITEM_RE.match(rel)
-    fm = FENCE_RE.match(rel)
-    return bool(THEMATIC_RE.match(rel) or ATX_RE.match(rel) or rel.lstrip(" ").startswith(">")
-                or (li and can_interrupt(li))
-                or (fm and not (fm.group(1)[0] == "`" and "`" in fm.group(2)))
-                or html_block_start(rel, True))
-unclosed: list[str] = []   # filled by sanitize: a fence or comment still open at end of file
-
-
-QUOTE_RE = re.compile(r"^ {0,3}> ?")
-
-
-def quote_depth(ln: str) -> tuple[int, str]:
-    """(number of leading block-quote markers, the text inside them) — CommonMark parses the
-    quoted text as blocks, so `> ## x` holds a real H2 and `> Foo` / `> ---` a Setext one."""
-    depth = 0
-    while True:
-        m = QUOTE_RE.match(ln)
-        if not m:
-            return depth, ln
-        depth += 1
-        ln = ln[m.end():]
-
-
-def unquote(ln: str) -> str:
-    return quote_depth(ln)[1]
-
-
-def is_h2(ln: str) -> bool:
-    """An ATX H2 on this line, inside block quotes or not."""
-    return bool(H2_RE.match(unquote(ln)))
-
-
-def mark_kind(ln: str) -> str | None:
-    """Which block mark this sanitized line is (its payload ignored), or None for ordinary text."""
-    head = ln.lstrip(" ").split(PAYLOAD_SEP, 1)[0]
-    return head if head in MARKS else None
-
-
-def visible_ws(s: str) -> str:
-    """Spaces and tabs made visible so prose normalisation cannot collapse code whitespace."""
-    return s.replace(" ", "␣").replace("\t", "⇥")
-
-
-def mark_token(ln: str) -> str:
-    """A comparison token for a fence/HTML mark: what readers see of the block, spaces made visible
-    so prose normalisation cannot collapse them (`⟨code⟩print(1)`, `⟨html⟩<div>x</div>`)."""
-    kind = mark_kind(ln)
-    payload = ln.split(PAYLOAD_SEP, 1)[1] if PAYLOAD_SEP in ln else ""
-    return ("⟨code⟩" if kind == FENCE_MARK else "⟨html⟩") + visible_ws(payload)
-
-
-def is_mark(ln: str) -> bool:
-    return mark_kind(ln) is not None
-
-
-def expand_lead(ln: str) -> str:
-    """Leading tabs expanded to spaces at 4-column tab stops (CommonMark), rest untouched."""
-    col = 0
-    i = 0
-    while i < len(ln) and ln[i] in " \t":
-        col = col + 1 if ln[i] == " " else col + 4 - col % 4
-        i += 1
-    return " " * col + ln[i:]
-
-
-def gap_cols(start: int, ws: str) -> int:
-    """Width in columns of whitespace `ws` beginning at column `start` (tabs to 4-column stops)."""
-    end = start
-    for ch in ws:
-        end = end + 1 if ch == " " else end + 4 - end % 4
-    return end - start
-
-
-def can_interrupt(li: re.Match) -> bool:
-    """CommonMark: a list item may interrupt a paragraph only if it is non-empty and, when
-    ordered, starts at 1 (`2. more` under a paragraph is lazy continuation text)."""
-    marker = li.group(2)
-    return not marker[0].isdigit() or int(marker[:-1]) == 1
-
-
-def content_col(li: re.Match) -> int:
-    """Content column of a list item (CommonMark): marker end, then the whitespace
-    after it expanded at 4-column tab stops; 5+ columns of it counts as 1 (the rest
-    is code inside the item)."""
-    col = len(li.group(1)) + len(li.group(2))
-    gap = gap_cols(col, li.group(3))
-    return col + (1 if gap >= 5 else gap)
-
+    return re.sub(r"\r\n?", "\n", text)
 
-BACKTICK_RUN_RE = re.compile(r"`+")
 
+# --- parse tree ------------------------------------------------------------------
+class Node:
+    """One block of the parse: its opening token `t`, child blocks `kids`, and the token
+    span [i, j] it covers (a leaf block spans one token)."""
+    __slots__ = ("t", "kind", "kids", "i", "j")
 
-def backtick_runs(ln: str) -> list[tuple[int, int]]:
-    """(start, end) of every backtick run that can delimit code. A backtick preceded by an ODD
-    number of backslashes is an escaped literal and drops out of its run; an even number
-    (`\\\\``) is an escaped backslash followed by a live backtick."""
-    runs: list[tuple[int, int]] = []
-    for m in BACKTICK_RUN_RE.finditer(ln):
-        a, b = m.start(), m.end()
-        if (a - len(ln[:a].rstrip("\\"))) % 2 == 1:
-            a += 1
-        if a < b:
-            runs.append((a, b))
-    return runs
+    def __init__(self, t, i: int) -> None:
+        self.t, self.i, self.j, self.kids = t, i, i, []
+        self.kind = t.type[:-5] if t.type.endswith("_open") else t.type
 
 
-def code_spans(s: str) -> list[tuple[int, int]]:
-    """Extents (delimiters included) of the code spans that open AND close within `s`, paired the
-    CommonMark way: a run pairs with the next run of the same length; a run with no partner is
-    literal text and scanning continues after it."""
-    runs = backtick_runs(s)
-    out: list[tuple[int, int]] = []
-    k = 0
-    while k < len(runs):
-        a, b = runs[k]
-        j = next((idx for idx in range(k + 1, len(runs)) if runs[idx][1] - runs[idx][0] == b - a), None)
-        if j is None:
-            k += 1
-            continue
-        out.append((a, runs[j][1]))
-        k = j + 1
-    return out
-
-
-def mask_spans(s: str) -> str:
-    """`s` with each closed code span replaced by `x`s of the same length (delimiters included)."""
-    out = list(s)
-    for a, b in code_spans(s):
-        out[a:b] = "x" * (b - a)
-    return "".join(out)
-
-
-def closer_ahead(run_len: int, rest: list[str], containers: list[int]) -> bool:
-    """True when a later line of the SAME paragraph (before a blank line or an interrupting block)
-    carries a backtick run of exactly `run_len` — the closer of a span opened on this line."""
-    for nxt in rest:
-        nxt = expand_lead(nxt)
-        if blank(nxt):
-            return False
-        indent = len(nxt) - len(nxt.lstrip(" "))
-        eff = next((c for c in reversed(containers) if c <= indent), 0)
-        if interrupts_paragraph(nxt, eff):
-            return False
-        if any(b - a == run_len for a, b in backtick_runs(nxt)):
-            return True
-    return False
-
-
-def mask_code(ln: str, open_run: int, rest: list[str] = (), containers: list[int] = ()) -> tuple[str, int]:
-    """Mask inline code with backticks for delimiter scanning; return (masked, open_run_after).
-
-    A backtick run with no same-length closer on its line opens a span that continues on the
-    following lines of the paragraph (CommonMark treats the newline as a space) — but only when
-    such a closer exists later in the paragraph (`rest`, judged against `containers`); with no
-    closer anywhere the run is literal text and scanning goes on after it. `open_run` is the
-    length of a run carried over from the previous line (0 = none).
-    """
-    runs = backtick_runs(ln)
-    out: list[str] = []
-    i = k = 0
-    if open_run:
-        j = next((idx for idx, (a, b) in enumerate(runs) if b - a == open_run), None)
-        if j is None:
-            return "`" * len(ln), open_run           # the whole line is still code
-        out.append("`" * runs[j][1])
-        i, k, open_run = runs[j][1], j + 1, 0
-    while k < len(runs):
-        a, b = runs[k]
-        j = next((idx for idx in range(k + 1, len(runs)) if runs[idx][1] - runs[idx][0] == b - a), None)
-        if j is None:
-            if closer_ahead(b - a, rest, containers):     # opens a span that closes on a later line
-                out.append(ln[i:a])
-                out.append("`" * (len(ln) - a))
-                return "".join(out), b - a
-            k += 1                                        # no closer anywhere: literal backticks
-            continue
-        out.append(ln[i:a])
-        out.append("`" * (runs[j][1] - a))
-        i, k = runs[j][1], j + 1
-    out.append(ln[i:])
-    return "".join(out), 0
-
-
-def closes_in_paragraph(rest: list[str], containers: list[int] = ()) -> bool:
-    """True when `-->` arrives on a later line of the SAME paragraph. CommonMark settles block
-    structure before inline HTML, so the paragraph ends at a blank line or at any line that
-    starts a block able to interrupt it — a heading, thematic break, block quote, fence, HTML
-    block, or a list item (a sibling `- Do:` included), judged against the deepest open list
-    item the line's indent satisfies. An opener whose closer lies beyond that is literal text."""
-    for nxt in rest:
-        nxt = expand_lead(nxt)
-        if blank(nxt):
-            return False
-        indent = len(nxt) - len(nxt.lstrip(" "))
-        eff = next((c for c in reversed(containers) if c <= indent), 0)
-        if interrupts_paragraph(nxt, eff):
-            return False
-        if "-->" in nxt:
-            return True
-    return False
-
-
-def strip_inline_comments(ln: str, scan: str, rest: list[str] = (), containers: list[int] = ()) -> str:
-    """Remove `<!-- … -->` spans from one prose line (`scan` is `ln` with code masked), carrying
-    an unterminated comment into the module-level `_in_comment` flag when — and only when — its
-    closer arrives later in the same paragraph (`rest` = the lines that follow, `containers` =
-    the content columns of the list items open at this line)."""
-    global _in_comment
-    buf: list[str] = []
-    i = 0
-    comment_here = False
-    while i < len(scan):
-        if _in_comment:
-            j = scan.find("-->", i)
-            if j < 0:
-                i = len(scan)
-            else:
-                _in_comment = False
-                i = j + 3
-        else:
-            j = scan.find("<!--", i)
-            while j >= 0 and (j - len(scan[:j].rstrip("\\"))) % 2 == 1:
-                j = scan.find("<!--", j + 1)         # `\<!--`: the `<` is escaped, so this is literal text
-            if j < 0:
-                buf.append(ln[i:])
-                i = len(scan)
-            elif "-->" not in scan[j + 4:] and not closes_in_paragraph(rest, containers):
-                buf.append(ln[i:])                   # opener with no closer in this paragraph: literal text
-                i = len(scan)
-            else:
-                buf.append(ln[i:j])
-                _in_comment = comment_here = True
-                i = j + 4
-    prose = "".join(buf)
-    # Spaces that sat before a comment which ran to the end of the line (`text  <!-- c -->`
-    # or a comment that continues onto the next line) were never line-ending spaces, so
-    # they must not read as a hard break once the comment is gone. Spaces AFTER `-->` stay.
-    if comment_here and (_in_comment or scan.endswith("-->")):
-        prose = prose.rstrip(" \t")                 # a NBSP there is content and stays
-    return prose
-
-
-_in_comment = False   # an inline `<!-- …` still open at the end of the previous prose line (sanitize state)
-
-
-def sanitize(lines: list[str]) -> list[str]:
-    """Return what a reader sees as prose: fenced code and HTML comments blanked.
-
-    Single pass, CommonMark-shaped: inside a fence, nothing is interpreted (a
-    `<!--` in code cannot open a comment) and the block closes only on a fence
-    of the same character at least as long as the opener with nothing but
-    spaces after it. Inside a comment, nothing is interpreted (a ``` in a
-    comment cannot open a fence) until `-->`. Fences are recognised relative
-    to the content column of the innermost open list item, so a code block
-    nested under a lesson field is code too. The opener line is replaced by
-    FENCE_MARK at the fence's indent so the parser still sees a block boundary
-    there (a fence closes an open list item just as a heading does), and an
-    HTML comment that starts a line (an HTML block) leaves COMMENT_MARK the
-    same way; an inline `<!-- … -->` mid-paragraph leaves nothing, it is just
-    raw inline HTML. The rest of each block is blank. Leading tabs are expanded
-    to spaces at 4-column stops. Line count is preserved.
-    """
-    out: list[str] = []
-    fence_char: str | None = None
-    fence_len = 0
-    global _in_comment
-    _in_comment = False          # an INLINE `<!-- …` (mid-paragraph) still open from a previous line
-    html_end: str | None = None  # inside an HTML block: the string that closes it, or None = closes at a blank line
-    in_html = False
-    in_paragraph = False         # the previous line was prose (a type-7 HTML block cannot start here)
-    open_run = 0                 # length of an inline-code backtick run still open from the previous line
-    containers: list[int] = []   # content columns of the open list items, innermost last
-    block_at = -1                # index in `out` of the open fence/HTML block's mark line
-    block_base = 0               # content column of the list item that block lives in
-    block_quote = 0              # block-quote depth the block was opened at (its lines carry that many `>`)
-    fence_indent = 0             # indentation of the opening fence inside its container (0-3): removed from content
-    block_text: list[str] = []   # the block's lines, container and quote markers removed (its payload)
-    unclosed.clear()
+class Doc:
+    """A parsed Markdown file: tokens, top-level blocks, reference env, source lines."""
 
-    def block_inner(ln: str) -> str | None:
-        """The text of `ln` inside the open block's container and block quotes, or None when the
-        line does not reach them (the block ends there)."""
-        if len(ln) - len(ln.lstrip(" ")) < block_base:
-            return None
-        inner = ln[block_base:]
-        for _ in range(block_quote):
-            qm = QUOTE_RE.match(inner)
-            if not qm:
-                return None
-            inner = inner[qm.end():]
-        return inner
-
-    def close_block(tail: str = "") -> None:
-        """Seal the open block's mark. A rendering block gets its text as payload; a non-rendering one
-        (comment, declaration, …) stays invisible unless raw text follows its terminator on the
-        closing line — CommonMark emits that line whole, so the tail is visible and the mark becomes
-        a rendering one carrying it."""
-        nonlocal block_at
-        if block_at >= 0:
-            if mark_kind(out[block_at]) == COMMENT_MARK:
-                if tail.strip(" \t"):
-                    out[block_at] = out[block_at].replace(COMMENT_MARK, HTML_MARK) + PAYLOAD_SEP + tail.strip(" \t")
-            else:
-                out[block_at] += PAYLOAD_SEP + BLOCK_SEP.join(block_text)
-        block_at = -1
-        block_text.clear()
-
-    for k, ln in enumerate(lines):
-        ln = expand_lead(ln)
-        indent = len(ln) - len(ln.lstrip(" "))
-        if blank(ln):
-            open_run = 0             # a code span cannot cross a blank line (the paragraph ends)
-            _in_comment = False      # nor can an inline comment: an unclosed `<!--` was literal text
-            in_paragraph = False
-        # A fence or HTML block inside a list item ends with that item (CommonMark: the block runs
-        # "until the end of the containing block"): a non-blank line shallower than the item's
-        # content column closes both, and is then read as an ordinary line.
-        inner = block_inner(ln) if (fence_char is not None or in_html) else None
-        if (fence_char is not None or in_html) and not blank(ln) and inner is None:
-            # The line leaves the item or the block quote, so CommonMark ends the block there too. Raw
-            # HTML that needed a terminator (`-->`, `</pre>`, …) is still unterminated in the OUTPUT,
-            # and a browser reads on past it — so it is reported like an unclosed block.
-            if in_html and html_end is not None:
-                unclosed.append("HTML comment never closes" if html_end == "-->" else f"HTML block never closes (no {html_end!r})")
-            fence_char = None
-            in_html = False
-            close_block()
-        if fence_char is not None:
-            m = FENCE_RE.match(inner) if inner is not None else None
-            if m and m.group(1)[0] == fence_char and len(m.group(1)) >= fence_len and m.group(2).strip(" \t") == "":   # NBSP etc. is content
-                fence_char = None
-                close_block()                    # the closing fence is not content
-            else:
-                text = inner if inner is not None else ""      # a blank line inside the fence
-                lead = len(text) - len(text.lstrip(" "))
-                block_text.append(text[min(lead, fence_indent):])   # CommonMark: up to the opener's indentation is removed
-            out.append("")
-            continue
-        if in_html:
-            # Types 1-5 end WITH the line holding their terminator; types 6-7 end at a blank
-            # line, which is itself not part of the block.
-            if html_end is None:
-                if blank(ln):
-                    in_html = False
-                    close_block()
-                else:
-                    block_text.append(inner)
-            else:
-                block_text.append(inner if inner is not None else "")
-                if inner is not None and html_end in inner.lower():   # `</PRE>` closes a `<pre>` block too
-                    in_html = False
-                    close_block(inner[inner.lower().index(html_end) + len(html_end):])
-            out.append("")
-            continue
-        cont = False
-        if _in_comment:
-            j = ln.find("-->")
-            if j < 0:
-                out.append(CONT_MARK)    # wholly inside the inline comment: no text, but the paragraph is still open
+    def __init__(self, text: str) -> None:
+        self.env: dict = {}
+        self.tokens = MD.parse(text, self.env)
+        self.lines = text.split("\n")
+        self.n_lines = text.count("\n") + (0 if text.endswith("\n") or not text else 1)
+        self.top: list[Node] = []
+        stack: list[Node] = []
+        for k, t in enumerate(self.tokens):
+            if t.nesting == -1:
+                stack.pop().j = k
                 continue
-            _in_comment = False
-            ln = ln[j + 3:]          # the rest of the line is prose again — but it did not BEGIN the
-            cont = True              # line, so it can never start a block: it stays in the paragraph
-        # Track which list items are still open: a non-blank line shallower than an
-        # item's content column closes that item (and everything nested in it).
-        lazy = False
-        new_item = False             # this line opened a list item: its content starts at `base` on this same line
-        if not blank(ln) and not cont:
-            # Judge "does this line start a block?" relative to the deepest container the
-            # line's indent still satisfies (a `<!--` at column 0 under a column-4 field is a
-            # top-level HTML block, not paragraph text). A lazy continuation (paragraph text
-            # that is none of the block starts) stays inside the open list item however
-            # shallow it is, so it must not pop the containers.
-            eff = next((c for c in reversed(containers) if c <= indent), 0)
-            lazy = in_paragraph and not interrupts_paragraph(ln, eff)
-            if not lazy:
-                open_run = 0         # a new block began: a backtick run left open in the previous paragraph was literal
-                while containers and indent < containers[-1]:
-                    containers.pop()
-                base = containers[-1] if containers else 0
-                li = LIST_ITEM_RE.match(ln[base:])   # a marker 4+ columns past the container is code, not a list item
-                if li and len(li.group(1)) <= 3 and not THEMATIC_RE.match(ln[base:]):
-                    containers.append(base + content_col(li))
-                    in_paragraph = False         # the item's content begins a new block: a type-7 tag there opens an HTML block
-                    new_item = True
-        base = containers[-1] if containers else 0
-        if cont:
-            scan, open_run = mask_code(ln, open_run, lines[k + 1:], containers)
-            out.append(CONT_MARK + strip_inline_comments(ln, scan, lines[k + 1:], containers))
-            in_paragraph = True
-            continue
-        # An HTML block (`<!--`, `<pre>`, `<div>`, … first on the line, relative to the open
-        # item) is a block like a fence: it leaves a boundary mark and its lines are raw HTML,
-        # never Markdown — a `## date` or `- Do:` inside it is not a heading or a field.
-        # Openers are read inside the container AND inside any block-quote markers (`> ```` opens a
-        # fence inside the quote); the block then lives at that quote depth.
-        in_container = indent >= base or new_item   # the opener line's content sits at `base` too (`- <sub>`, `- ````)
-        qd, qinner = quote_depth(ln[base:]) if in_container else (0, "")
-        hb = html_block_start(qinner, in_paragraph) if in_container else None
-        if hb:
-            mark, html_end = hb
-            out.append(" " * indent + mark)
-            open_run = 0
-            in_paragraph = False
-            rel = qinner.lstrip(" ")
-            if html_end is None:
-                in_html = True                       # until a blank line
-            elif html_end == ">":
-                in_html = ">" not in rel[2:]         # `<!X … >` may close on its own line
+            n = Node(t, k)
+            (stack[-1].kids if stack else self.top).append(n)
+            if t.nesting == 1:
+                stack.append(n)
+
+    def line(self, n: Node, offset: int = 0) -> str:
+        return self.lines[n.t.map[0] + offset] if n.t.map else ""
+
+    def html(self, n: Node) -> str:
+        """The block rendered with this file's reference definitions, as a reader sees it."""
+        return visible_html(MD.renderer.render(self.tokens[n.i:n.j + 1], MD.options, self.env))
+
+    def unclosed(self) -> list[str]:
+        out: list[str] = []
+        for t in self.tokens:
+            if t.type == "fence" and t.map[1] >= self.n_lines:
+                last = self.lines[t.map[1] - 1] if t.map[1] - 1 > t.map[0] else ""
+                closer = r"[ \t>]*" + re.escape(t.markup[0]) + "{%d,}[ \t]*" % len(t.markup)
+                if not re.fullmatch(closer, last):
+                    out.append(f"fenced code block opened with {t.markup} never closes")
+            elif t.type == "html_block":
+                c = t.content.lstrip(" ")
+                for opener, end in HTML_ENDS:
+                    m = opener.match(c)
+                    if m:
+                        end = end or f"</{m.group(1).lower()}>"
+                        if end not in c.lower()[2:]:
+                            out.append("HTML comment never closes" if end == "-->" else f"HTML block never closes (no {end!r})")
+                        break
+        return out
+
+
+def walk(n: Node):
+    yield n
+    for k in n.kids:
+        yield from walk(k)
+
+
+def collapse(s: str) -> str:
+    return BLOCK_TAG_RE.sub(r"\1", re.sub(r"[ \t\n\r\f]+", " ", s))
+
+
+def visible_html(html: str) -> str:
+    """Rendered HTML reduced to what a reader can tell apart: invisible markup removed,
+    whitespace collapsed outside <pre>/<code>, and dropped next to block boundaries."""
+    html = INVISIBLE_RE.sub("", html)
+    parts: list[str] = []
+    i = 0
+    for m in PROTECT_RE.finditer(html):
+        seg = collapse(html[i:m.start()])
+        parts.append(seg.rstrip(WS) if m.group(0)[:4].lower() == "<pre" else seg)
+        parts.append(m.group(0))
+        i = m.end()
+        if m.group(0)[:4].lower() == "<pre":
+            html = html[:i] + html[i:].lstrip(WS)
+    parts.append(collapse(html[i:]))
+    return "".join(parts).strip(WS)
+
+
+def visible(n: Node) -> bool:
+    """Does this top-level block render anything? Only raw HTML can be invisible."""
+    return n.kind != "html_block" or bool(INVISIBLE_RE.sub("", n.t.content).strip(WS))
+
+
+def h2s(doc: Doc, n: Node) -> list[str]:
+    """Every H2 inside `n` (block quotes and list items included), ATX or Setext."""
+    out = []
+    for d in walk(n):
+        if d.kind == "heading" and d.t.tag == "h2":
+            if d.t.markup in ("-", "="):
+                out.append(doc.line(d).strip(WS)[:60] + " / " + doc.line(d, d.t.map[1] - d.t.map[0] - 1).strip(WS)[:20])
             else:
-                in_html = html_end not in rel.lower()[len(html_end) - 1:]   # e.g. `<!-- x -->` closes on its own line
-            block_base, block_quote = base, qd
-            block_at = len(out) - 1
-            if mark == HTML_MARK:
-                block_text.append(qinner.lstrip(" "))   # the opener's 0-3 spaces of indentation are not content
-            if not in_html:                          # closed on its own line: any raw text after the terminator is visible
-                tail = ""
-                if html_end is not None:
-                    low = rel.lower()
-                    at = low.find(html_end, len(html_end) - 1 if html_end != ">" else 2)
-                    tail = rel[at + len(html_end):] if at >= 0 else ""
-                close_block(tail)
-            continue
-        m = FENCE_RE.match(qinner) if in_container else None
-        # CommonMark: a backtick fence cannot open when its info string contains a
-        # backtick (that line is an inline code span, not a fence); tilde fences may.
-        if m and not (m.group(1)[0] == "`" and "`" in m.group(2)):
-            fence_char, fence_len = m.group(1)[0], len(m.group(1))
-            fence_indent = len(qinner) - len(qinner.lstrip(" "))
-            open_run = 0
-            in_paragraph = False
-            out.append(" " * indent + FENCE_MARK)
-            block_at, block_base, block_quote = len(out) - 1, base, qd
-            block_text.append(m.group(2).strip(" \t"))   # what readers see of the opener: its info string
-            continue
-        # Inline code spans are opaque: a <!-- inside `…` is code, not a comment opener.
-        # Mask them (same length, harmless chars) for delimiter scanning; restore text after.
-        scan, open_run = mask_code(ln, open_run, lines[k + 1:], containers)
-        prose = strip_inline_comments(ln, scan, lines[k + 1:], containers)
-        # Prose that continues the paragraph keeps it open; otherwise the line opens one only if,
-        # read inside its container, it is neither a heading, a thematic break nor indented code.
-        rel = prose[base:]
-        in_paragraph = lazy or (not blank(prose) and len(rel) - len(rel.lstrip(" ")) <= 3
-                                and not ATX_RE.match(rel) and not THEMATIC_RE.match(rel))
-        out.append(prose)
-    if fence_char is not None:   # implicitly closed at end of file, but a hand editor forgot the closer
-        unclosed.append(f"fenced code block opened with {fence_char * fence_len} never closes")
-    if _in_comment or (in_html and html_end == "-->"):
-        unclosed.append("HTML comment never closes")
-    elif in_html and html_end is not None:
-        unclosed.append(f"HTML block never closes (no {html_end!r})")
-    close_block()                # a block still open at end of file keeps its text
+                out.append(doc.line(d).strip(" \t"))
     return out
 
 
-def parse_lessons(lines: list[str], *, presanitized: bool = False) -> tuple[dict[str, list[dict]], list[str]]:
-    """Return ({date: [lesson]}, [duplicate dates]) for a knowledge-style file.
-
-    A lesson is {"title", "why", "do", "source", "problems"} with the field
-    TEXT (normalized) so two copies can be compared, and a list of problems:
-    missing or empty fields, a confidence outside 0-100, or a top-level bullet
-    that is not a well-formed bold lesson (recorded, never dropped, so a
-    malformed lesson can't vanish from the count). A repeated `## date`
-    heading is recorded as a duplicate; its lessons join the first occurrence.
-    """
-    sections: dict[str, list[dict]] = {}
-    duplicates: list[str] = []
-    current: str | None = None
-    lesson: dict | None = None
-    bad_headings.clear()
-    misplaced.clear()
-    stray.clear()
-    if not presanitized:                 # a day file arrives already sanitized: a second pass would read block payloads as Markdown
-        lines = sanitize(lines)
+# --- lessons ---------------------------------------------------------------------
+def inline_tokens(inline) -> list:
+    """The paragraph's inline tokens without the empty text tokens the parser leaves around delimiters."""
+    return [c for c in inline.children if not (c.type == "text" and c.content == "")]
 
-    def new_lesson(title: str, malformed: bool) -> dict:
-        # col   content column of the lesson item; fcol  that of the open field item (None = none open)
-        # last  the open field's key; para  a paragraph is open (lazy continuation may follow);
-        # pcol  the content column of the container that paragraph lives in (a Setext underline
-        #       must sit there); tail  lesson-level prose after the fields — visible, so compared
-        return {"title": title, "why": "", "do": "", "source": "", "source_raw": "", "tail": "", "order": [], "refs": "",
-                "col": 2, "fcol": None, "last": None, "hard": False, "para": False, "pcol": 0, "pq": 0, "code": None,
-                "seen": {"why": False, "do": False, "source": False},
-                "problems": (["not-a-bold-lesson-bullet"] if malformed else [])}
 
-    def absorb(lesson: dict, ln: str, para: bool = False) -> None:
-        """Prose is gathered RAW here and normalised once the lesson is complete. It belongs to the
-        open field — or, before any field, to the title when it continues
-        the title's own paragraph (a shape problem) and to the lesson's tail when it is a separate
-        paragraph; after the fields, to the tail too. A hard line break on the previous line is
-        kept as HARD_BREAK, and a new paragraph inside the item as PARA_BREAK, so two copies
-        that render differently never compare equal."""
-        text = ln.strip(" \t")
-        if not text:
-            return
-        sep = f" {PARA_BREAK} " if para else (f" {HARD_BREAK} " if lesson["hard"] else " ")
-        if lesson["last"] is not None:
-            key = lesson["last"]
-            if key == "source":
-                lesson["source_raw"] = lesson["source_raw"] + sep + text
-            else:
-                lesson[key] = lesson[key] + sep + text
-        elif not any(lesson["seen"].values()) and not para:
-            # Same paragraph as the bold title: CommonMark renders this text OUTSIDE the closing `**`,
-            # so the title paragraph is no longer one strong span.
-            lesson["title"] = lesson["title"] + sep + text
-            if "not-a-bold-lesson-bullet" not in lesson["problems"] and "title paragraph continues past the bold span" not in lesson["problems"]:
-                lesson["problems"].append("title paragraph continues past the bold span")
-        else:
-            lesson["tail"] = lesson["tail"] + sep + text       # lesson-level prose outside title and fields: visible, so compared
-        lesson["hard"] = ends_hard(ln)
-
-    def open_para(lesson: dict, col: int, qdepth: int = 0) -> None:
-        lesson["para"], lesson["pcol"], lesson["pq"] = True, col, qdepth
-
-    def close_para(lesson: dict) -> None:
-        lesson["para"], lesson["hard"] = False, False
-
-    def flush_code(lesson: dict) -> None:
-        """An indented code block inside the item is visible: its lines (trailing blank ones dropped)
-        join the comparison as one ⟨code⟩ token, like a fence's payload."""
-        code = lesson["code"]
-        if code is not None:
-            while code and code[-1] == "":
-                code.pop()
-            absorb(lesson, "⟨code⟩" + visible_ws(BLOCK_SEP.join(code)), para=True)
-            lesson["code"] = None
-
-    top_para: str | None = None   # the previous line, when it was a top-level paragraph line (a Setext underline may follow)
-    quote_para: tuple[int, str] | None = None   # same, for a paragraph line inside a top-level block quote (depth, text)
-    ref_title_next = False        # the previous line was a reference definition without a title: its title may follow
-    pending_label = ""            # …and this is its label
-    defs: dict[str, tuple[str, str | None]] = {}   # link reference definitions seen in this file: label → (destination, title)
-    for idx, ln in enumerate(lines):
-        prev_blank = blank(ln)
-        prev_top_para, top_para = top_para, None
-        prev_quote_para, quote_para = quote_para, None
-        title_may_follow, ref_title_next = ref_title_next, False
-        if prev_blank:
-            if lesson is not None:
-                close_para(lesson)                # a blank line ends any paragraph (the list item stays open)
-                if lesson["code"] is not None:
-                    lesson["code"].append("")     # inside an indented code block a blank line is a blank code line
-            continue
-        # A dash underline right under a top-level paragraph line turns that line into an H2
-        # (Setext). It is never a day heading, so it is a bad heading that ends the section.
-        # The same holds inside a block quote when both lines sit at the same quote depth.
-        qd, qtext = quote_depth(ln)
-        if (prev_top_para is not None and SETEXT_H2_RE.match(ln)) or (
-                prev_quote_para is not None and qd == prev_quote_para[0] and SETEXT_H2_RE.match(qtext)):
-            para_text = prev_top_para if prev_top_para is not None else prev_quote_para[1]
-            bad_headings.append(para_text.strip()[:60] + " / " + ln.strip()[:20])
-            current = None
-            lesson = None
-            continue
-        if qd and not blank(qtext) and not INTERRUPT_RE.match(qtext) and not TOP_BULLET_RE.match(qtext) and not FENCE_RE.match(qtext) \
-                and (lesson is None or indent_of(ln) < lesson["col"]):
-            quote_para = (qd, qtext)              # a paragraph line inside a top-level block quote
-        if ln.startswith(CONT_MARK):              # paragraph text that began inside an inline comment
-            if lesson is not None and current is not None and lesson["para"]:
-                absorb(lesson, ln[len(CONT_MARK):])
-            continue
-        indent = len(ln) - len(ln.lstrip(" "))
-        # CommonMark: a line indented at or past the open lesson's content column is INSIDE that
-        # list item. Blocks there are read relative to the innermost item the line sits in: the
-        # open field's content column when the line reaches it, else the lesson's.
-        nested = lesson is not None and current is not None and indent >= lesson["col"]
-        is_code = False
-        if nested:
-            in_field = lesson["fcol"] is not None and indent >= lesson["fcol"]
-            eff = lesson["fcol"] if in_field else lesson["col"]
-            rel = ln[eff:]
-            rel_indent = len(rel) - len(rel.lstrip(" "))
-            is_code = rel_indent >= 4 and not lesson["para"]   # indented code (a lazy paragraph line may be deeper)
-        if lesson is not None and lesson["code"] is not None and not is_code:
-            flush_code(lesson)                    # the indented code block ended before this line
-        if nested:
-            # An ATX heading inside the item — even `  ## 2026-08-23`, or `  > ## x` inside a block
-            # quote — can never open a day section (and a heading interrupts any paragraph). An H2
-            # there is a problem; other levels are recorded as text so copies that differ in them differ.
-            if ATX_RE.match(unquote(rel)):
-                if not in_field and lesson["fcol"] is not None:
-                    lesson["last"] = lesson["fcol"] = None
-                if is_h2(rel):
-                    lesson["problems"].append(f"heading {ln.strip()[:30]!r} nested inside the lesson (indent {indent})")
-                else:
-                    absorb(lesson, ln, para=True)
-                close_para(lesson)
-                continue
-        m = SECTION_RE.match(ln)
-        if m:
-            current = m.group(1)
-            if current in sections:
-                duplicates.append(current)
-            else:
-                sections[current] = []
-            lesson = None
-            continue
-        if is_h2(ln):                             # an H2 that is not `## YYYY-MM-DD` (block-quoted ones included)
-            bad_headings.append(ln.strip(" \t"))    # keep a trailing NBSP visible in the report
-            current = None                        # nothing after it belongs to a day
-            lesson = None
-            continue
-        if current is None:
-            # Nothing visible belongs outside a day section except the file's H1 on its first line
-            # and link reference definitions (with a title on the next line when the definition has
-            # none): a bullet, a paragraph, a thematic break, a code block — readers see it, no day file has it.
-            if title_may_follow and REF_TITLE_RE.match(ln):
-                defs[pending_label] = (defs[pending_label][0], ln.strip(" \t"))
-                continue
-            rd = link_ref_def(ln)
-            if rd is not None:
-                defs.setdefault(rd[0], (rd[1], rd[2]))   # CommonMark: the first definition of a label wins
-                ref_title_next, pending_label = rd[2] is None, rd[0]
-                continue
-            if not is_mark(ln) and not (idx == 0 and is_real_h1(ln)):
-                misplaced.append(ln.strip()[:60])
-            if mark_kind(ln) in (FENCE_MARK, HTML_MARK):   # a comment renders nothing and is allowed metadata
-                misplaced.append("<code block or raw HTML>")
-            if not TOP_BULLET_RE.match(ln) and not INTERRUPT_RE.match(ln) and not is_mark(ln) and (indent <= 3 or prev_top_para is not None):
-                top_para = ln
-            continue
-        if not nested:
-            # A line shallower than the lesson's content column is a sibling or a block that closes the list.
-            if THEMATIC_RE.match(ln):             # `- - -` is a thematic break, never a list item — and a visible <hr>
-                stray.append(ln.strip()[:60])
-                lesson = None
-                continue
-            m = LESSON_RE.match(ln)
-            li = LIST_ITEM_RE.match(ln)
-            # `- ** **` / `- ** x**` / `- **x **` are not bold (a `**` next to whitespace cannot
-            # open or close); `- **a** b **c**` is two strong spans, not one title; 5+ columns
-            # after the marker make the title an indented code block, not bold.
-            if (m and li and m.group(2).strip() and not m.group(2)[0].isspace() and not m.group(2)[-1].isspace()
-                    and (len(m.group(2)) - len(m.group(2).rstrip("\\"))) % 2 == 0   # `\**` escapes the first closing star
-                    and m.group(2)[0] != "*"                                      # `***foo**` → `*` + strong
-                    and not (m.group(2).endswith("*") and (len(m.group(2)) - 1 - len(m.group(2)[:-1].rstrip("\\"))) % 2 == 0)   # `**foo***` → strong + `*`
-                    and not has_inner_strong(mask_spans(m.group(2)))                  # `**` inside a code span is opaque
-                    and gap_cols(indent + 1, m.group(1)) <= 4):
-                lesson = new_lesson(m.group(2), malformed=False)
-                lesson["col"] = content_col(li)
-                lesson["hard"] = ends_hard(ln)
-                open_para(lesson, lesson["col"])
-                sections[current].append(lesson)
-                continue
-            if TOP_BULLET_RE.match(ln) and not (lesson is not None and lesson["para"]
-                                                and (li is None or not can_interrupt(li))):
-                # (a `2.` item or an empty item directly under a paragraph cannot start a list: it
-                # is lazy continuation text and is handled below)
-                parts = ln.strip().split(None, 1)
-                lesson = new_lesson("MALFORMED: " + (parts[1].strip() if len(parts) > 1 else "(empty list item)"), malformed=True)
-                lesson["col"] = content_col(li) if li else indent + 2
-                lesson["hard"] = ends_hard(ln)
-                open_para(lesson, lesson["col"])
-                sections[current].append(lesson)
-                continue
-            if not is_mark(ln) and not INTERRUPT_RE.match(ln) and (lesson is None or not lesson["para"]) and (
-                    indent <= 3 or prev_top_para is not None):
-                top_para = ln                          # a top-level paragraph line: a `---` next would make it an H2
-            if lesson is not None and lesson["para"] and not INTERRUPT_RE.match(ln) and not is_mark(ln):
-                # Lazy continuation (CommonMark): a non-blank, non-list line directly
-                # under a paragraph — even at indent 0 — is still that paragraph's text.
-                absorb(lesson, ln)
-                continue
-            # A heading, thematic break, block quote, fenced code block, HTML block (`<div>`, …) or a
-            # paragraph after a blank line closes the list: fields that follow render as a NEW list,
-            # not this lesson's, so they must not reconnect. Unless it renders nothing (a comment block
-            # or a reference definition) it is visible content that no lesson — and no day file — holds.
-            lesson = None
-            if title_may_follow and REF_TITLE_RE.match(ln):
-                defs[pending_label] = (defs[pending_label][0], ln.strip(" \t"))
-                continue                          # the title of the reference definition on the line above
-            rd = link_ref_def(ln)
-            if rd is not None:
-                defs.setdefault(rd[0], (rd[1], rd[2]))
-                ref_title_next, pending_label = rd[2] is None, rd[0]
-                continue
-            if mark_kind(ln) != COMMENT_MARK:
-                stray.append(mark_token(ln)[:60] if is_mark(ln) else ln.strip()[:60])
-            continue
-        # ---- nested line: inside the open lesson item ----
-        if is_mark(ln):
-            # A fenced code block, comment block or HTML block inside the item ends the paragraph;
-            # sitting beside the field items (shallower than the open field's content) it closes that
-            # item. Code and raw HTML are visible, so their text joins the comparison as a token.
-            if not in_field and lesson["fcol"] is not None:
-                lesson["last"] = lesson["fcol"] = None
-            if mark_kind(ln) != COMMENT_MARK:
-                absorb(lesson, mark_token(ln), para=True)
-            close_para(lesson)
-            continue
-        # A dash underline directly under a paragraph inside the item, at that paragraph's
-        # container column, makes it a Setext H2 (CommonMark) — a heading hidden in a lesson.
-        if lesson["para"] and indent >= lesson["pcol"]:
-            uq, utext = quote_depth(ln[lesson["pcol"]:])
-            if uq == lesson["pq"] and SETEXT_H2_RE.match(utext):
-                lesson["problems"].append(f"Setext H2 (underline {ln.strip()[:12]!r}) nested inside the lesson")
-                close_para(lesson)
-                continue
-        if lesson["para"] and not interrupts_paragraph(ln, eff):
-            absorb(lesson, ln)                    # lazy continuation of the open paragraph, wherever the line sits
-            continue
-        if not in_field and lesson["fcol"] is not None:
-            lesson["last"] = lesson["fcol"] = None   # shallower than the open field's content: that item is closed
-        if rel_indent >= 4:                       # an indented code block inside the item: collected, flushed as one token
-            if lesson["code"] is None:
-                lesson["code"] = []
-            lesson["code"].append(rel[4:])        # CommonMark removes four columns of indentation
-            close_para(lesson)
+def parse_source(inline) -> int | None:
+    """Confidence N for a Source field rendering as `Source: <link to http(s)> … confidence N%`,
+    else None. Only visible text after the link counts — never link text, code or HTML attributes."""
+    all_ch = inline_tokens(inline)
+    ch = [c for c in all_ch if c.type != "softbreak"]
+    if not ch or ch[0].type != "text" or ch[0].content[len("Source:"):].strip(" \t"):
+        return None
+    if len(ch) < 2 or ch[1].type != "link_open" or ch[1].markup == "autolink":
+        return None
+    if not re.match(r"https?://[^ \t]+$", ch[1].attrGet("href") or ""):
+        return None
+    k = next((i for i in range(2, len(ch)) if ch[i].type == "link_close"), None)
+    if k is None:
+        return None
+    tail = "".join(c.content if c.type == "text" else " " for c in all_ch[all_ch.index(ch[k]) + 1:]
+                   if c.type in ("text", "softbreak", "hardbreak"))
+    m = CONFIDENCE_RE.search(tail)
+    return int(m.group(1)) if m else None
+
+
+def title_of(inline) -> tuple[str | None, bool]:
+    """(title, continues) when the paragraph opens with ONE `**strong**` span holding no other
+    strong span — `continues` when the paragraph goes on past it on later lines. (None, _)
+    when it is not a bold lesson title at all."""
+    ch = inline_tokens(inline)
+    if not ch or ch[0].type != "strong_open" or ch[0].markup != "**":
+        return None, False
+    depth, end = 0, None
+    for k, c in enumerate(ch):
+        if c.type == "strong_open":
+            depth += 1
+            if depth > 1:
+                return None, False
+        elif c.type == "strong_close":
+            depth -= 1
+            if depth == 0:
+                end = k
+                break
+    if end is None:
+        return None, False
+    title = "".join(c.content for c in ch[1:end] if c.type in ("text", "code_inline"))
+    rest = ch[end + 1:]
+    if rest and rest[0].type not in ("softbreak", "hardbreak"):
+        return None, False                        # `**a** b **c**`, `**foo***`: not one bold span
+    return (title if title.strip(WS) else None), bool(rest)
+
+
+def parse_lesson(doc: Doc, item: Node) -> dict:
+    """{title, problems, key, parts} for one top-level list item of a day section."""
+    lesson = {"problems": [], "key": doc.html(item), "parts": {}, "seen": {k: False for k in FIELDS}}
+    head = item.kids[0] if item.kids and item.kids[0].kind == "paragraph" else None
+    title, continues = title_of(head.kids[0].t) if head and item.t.markup == "-" else (None, False)
+    if title is None:
+        raw = MARKER_RE.sub("", doc.line(item), count=1).strip(WS)
+        lesson["title"] = "MALFORMED: " + (raw or "(empty list item)")
+        lesson["problems"].append("not-a-bold-lesson-bullet")
+        return lesson
+    lesson["title"] = title
+    lesson["parts"]["title"] = doc.html(head)
+    if continues:
+        lesson["problems"].append("title paragraph continues past the bold span")
+    order: list[str] = []
+    for block in item.kids[1:]:
+        if block.kind != "bullet_list" or block.t.markup != "-":
             continue
-        fm = FIELD_LABEL_RE.match(ln)
-        if fm and not in_field:
-            gap = gap_cols(indent + 1, fm.group(2))
-            if gap > 4:
-                lesson["problems"].append(f"field '{fm.group(3)}' has {gap} columns after `-`: its text renders as code, not a field")
-                lesson["last"], lesson["fcol"] = None, indent + 2
-                close_para(lesson)
-                continue
-            label, value = fm.group(3), fm.group(4)
-            key = {"Why it matters here": "why", "Do": "do", "Source": "source"}[label]
-            lesson["last"], lesson["fcol"] = key, indent + 1 + gap
-            lesson["order"].append(key)
-            open_para(lesson, lesson["fcol"])
-            lesson["hard"] = ends_hard(ln)
+        for field in block.kids:
+            fk = field.kids
+            m = FIELD_RE.match(fk[0].kids[0].t.content) if fk and fk[0].kind == "paragraph" else None
+            if not m:
+                continue                          # a sub-bullet that is not a field: compared, not counted
+            key = LABELS[m.group(1)]
+            order.append(key)
+            lesson["parts"][key] = doc.html(field)
             if lesson["seen"][key]:
                 lesson["problems"].append(f"duplicate {key}")
             lesson["seen"][key] = True
-            # The value may continue on the next line(s); emptiness and the source shape are
-            # judged once the whole field has been assembled (see the finalisation loop).
+            empty = not fk[0].kids[0].t.content[m.end():].strip(WS) and len(fk) == 1
             if key == "source":
-                lesson["source_raw"] = value
+                conf = None if empty else parse_source(fk[0].kids[0].t)
+                if conf is None:
+                    lesson["problems"].append("source line is not `[title](https://…) … confidence N%`")
+                elif conf > 100:
+                    lesson["problems"].append(f"confidence {conf}% out of 0-100")
+            elif empty:
+                lesson["problems"].append(f"missing or empty {key}")
+    lesson["parts"]["order"] = " ".join(order)
+    lesson["problems"] += [f"missing {k}" for k in FIELDS if not lesson["seen"][k]]
+    lesson["problems"] += [f"heading {h[:30]!r} nested inside the lesson" for h in h2s(doc, item)]
+    return lesson
+
+
+def parse_sections(doc: Doc, nodes: list[Node], day: str | None = None) -> dict:
+    """Walk top-level blocks. knowledge.md mode (day None): `## YYYY-MM-DD` opens a section,
+    anything visible outside one is misplaced. Day-file mode: everything is that day's."""
+    res = {"sections": {day: []} if day else {}, "dups": [], "bad": [], "misplaced": [], "stray": []}
+    current = day
+    for n in nodes:
+        if n.kind == "heading" and n.t.tag == "h2" and not day:
+            m = SECTION_RE.match(doc.line(n)) if n.t.markup == "##" else None
+            if m:
+                current = m.group(1)
+                if current in res["sections"]:
+                    res["dups"].append(current)
+                else:
+                    res["sections"][current] = []
             else:
-                lesson[key] = value
-            continue
-        if THEMATIC_RE.match(rel):                # a thematic break inside the item renders as <hr>: a visible block
-            absorb(lesson, "⟨hr⟩", para=True)
-            close_para(lesson)
+                res["bad"] += h2s(doc, n)
+                current = None                    # nothing after a bad heading belongs to a day
             continue
-        # Anything else opens a new paragraph inside the item: a list item that is not a lesson
-        # field (a sub-bullet, an ordered item, a `- Do:` nested inside another field), a block
-        # quote, or plain text after a blank line, a block or code. Its text is recorded.
-        li = LIST_ITEM_RE.match(rel)
-        absorb(lesson, ln, para=True)
-        open_para(lesson, eff + content_col(li) if li else eff, quote_depth(rel)[0])
-
-    if lesson is not None and lesson["code"] is not None:
-        flush_code(lesson)
-    for ls in sections.values():
-        for l in ls:
-            # Text was gathered raw; normalise each assembled paragraph ONCE, so a code span that opens
-            # on one line and closes on a later one is seen whole and keeps its inner spaces.
-            for k in ("title", "why", "do", "source_raw", "tail"):
-                l[k] = norm(l[k])
-            # Reference-style links resolve through the file's definitions, so a label used by the
-            # lesson carries its destination (and title) into the comparison; a label defined in one
-            # copy only, or defined differently, is a visible difference.
-            used = {ref_label(m.group(1)) for k in ("title", "why", "do", "source_raw", "tail")
-                    for m in LINK_LABEL_RE.finditer(l[k])}
-            l["refs"] = "; ".join(f"{lab}={defs[lab][0]} {defs[lab][1] or ''}".rstrip() for lab in sorted(used) if lab in defs)
-            if "not-a-bold-lesson-bullet" in l["problems"]:
-                continue
-            for k in FIELDS:
-                if not l["seen"][k]:
-                    l["problems"].append(f"missing {k}")
-                elif k != "source" and not l[k]:
-                    l["problems"].append(f"missing or empty {k}")
-            if l["seen"]["source"]:
-                parsed = parse_source(l["source_raw"])
-                if parsed is None:
-                    l["problems"].append("source line is not `[title](https://…) … confidence N%`")
-                elif not 0 <= parsed[1] <= 100:
-                    l["problems"].append(f"confidence {parsed[1]}% out of 0-100")
-                else:
-                    l["source"] = l["source_raw"]
-    return sections, duplicates
+        if not visible(n):
+            continue                              # a comment block renders nothing
+        quoted = h2s(doc, n) if n.kind == "blockquote" and not day else []
+        if n.kind in ("bullet_list", "ordered_list") and current:
+            res["sections"][current] += [parse_lesson(doc, item) for item in n.kids]
+        elif current is None and not (n.kind == "heading" and n.t.tag == "h1" and n.t.map[0] == 0):
+            res["misplaced"] += [doc.line(k).strip(WS)[:60] for k in (n.kids if n.kind.endswith("list") else [n])]
+        elif current:
+            res["stray"].append(doc.line(n).strip(WS)[:60] or n.kind)
+        if quoted:
+            res["bad"] += quoted
+            current = None
+    return res
 
 
 def shape_errors(sections: dict[str, list[dict]]) -> list[str]:
@@ -1155,14 +361,32 @@ def shape_errors(sections: dict[str, list[dict]]) -> list[str]:
     return [f"{d}#{i}: {p}" for d, ls in sections.items() for i, l in enumerate(ls, 1) for p in l["problems"]]
 
 
-KEY_FIELDS = ("title",) + FIELDS + ("tail", "order", "refs")   # what two copies of a lesson are compared on
-
-
-def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
-    return (l["title"], l["why"], l["do"], l["source"], l["tail"], " ".join(l["order"]), l["refs"])
-
-
-# --- knowledge.md ----------------------------------------------------------
+def compare(k_lessons: list[dict], d_lessons: list[dict]) -> str | None:
+    """None when both copies render identically lesson by lesson, else a diagnosis."""
+    if [l["key"] for l in k_lessons] == [l["key"] for l in d_lessons]:
+        return None
+    titles_k = {l["title"] for l in k_lessons}
+    titles_d = {l["title"] for l in d_lessons}
+    only_k = [l["title"][:40] for l in k_lessons if l["title"] not in titles_d]
+    only_d = [l["title"][:40] for l in d_lessons if l["title"] not in titles_k]
+    diff = []
+    for a, b in zip(k_lessons, d_lessons):
+        if a["key"] != b["key"] and a["title"] == b["title"]:
+            parts = [p for p in ("title", "why", "do", "source", "order") if a["parts"].get(p) != b["parts"].get(p)]
+            diff.append(a["title"][:30] + ": " + (",".join(parts) or "other content"))
+    detail = f"{len(d_lessons)} lesson(s) here vs {len(k_lessons)} in knowledge.md"
+    if diff:
+        detail += f"; same title, different content: {diff[:3]}"
+    if only_k:
+        detail += f"; only in knowledge.md: {only_k[:3]}"
+    if only_d:
+        detail += f"; only here: {only_d[:3]}"
+    if not diff and not only_k and not only_d:
+        detail += "; same set, different order" if len(d_lessons) == len(k_lessons) else "; a lesson repeats"
+    return detail
+
+
+# --- knowledge.md ------------------------------------------------------------------
 kpath = ROOT / "knowledge.md"
 sections: dict[str, list[dict]] = {}
 if kpath.is_symlink():
@@ -1170,27 +394,26 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
 elif not kpath.is_file():
     fail("knowledge.md", "missing")
 else:
-    lines = read_lines(kpath) or []
-    if lines and lines[0] == KNOWLEDGE_H1:            # exact: trailing whitespace would break the connector's anchor too
+    text = read_text(kpath) or ""
+    first_line = text.split("\n", 1)[0]
+    if first_line == KNOWLEDGE_H1:                   # exact: trailing whitespace would break the connector's anchor too
         ok("knowledge:h1", "exact connector heading present")
     else:
-        fail("knowledge:h1", f"first line must be {KNOWLEDGE_H1!r}, got {(lines[0] if lines else '')[:70]!r}")
-    sections, dup_dates = parse_lessons(lines)
-    k_unclosed = list(unclosed)
-    if k_unclosed:
-        fail("knowledge:unclosed", "; ".join(k_unclosed) + " — everything after it is hidden from readers")
-    k_bad_headings = list(bad_headings)
-    k_misplaced = list(misplaced)
-    k_stray = list(stray)
-    dates = list(sections)
-    if k_bad_headings:
-        fail("knowledge:headings", f"H2 headings that are not `## YYYY-MM-DD`: {k_bad_headings[:4]}")
+        fail("knowledge:h1", f"first line must be {KNOWLEDGE_H1!r}, got {first_line[:70]!r}")
+    kdoc = Doc(text)
+    kres = parse_sections(kdoc, kdoc.top)
+    sections = kres["sections"]
+    if kdoc.unclosed():
+        fail("knowledge:unclosed", "; ".join(kdoc.unclosed()) + " — everything after it is hidden from readers")
+    if kres["bad"]:
+        fail("knowledge:headings", f"H2 headings that are not `## YYYY-MM-DD`: {kres['bad'][:4]}")
     else:
         ok("knowledge:headings", "every H2 is a day heading")
-    if k_misplaced:
-        fail("knowledge:misplaced", f"{len(k_misplaced)} visible block(s) outside any `## YYYY-MM-DD` section: {k_misplaced[:3]}")
-    if k_stray:
-        fail("knowledge:stray", f"{len(k_stray)} visible block(s) inside a day section that are not lessons (no day file carries them): {k_stray[:3]}")
+    if kres["misplaced"]:
+        fail("knowledge:misplaced", f"{len(kres['misplaced'])} visible block(s) outside any `## YYYY-MM-DD` section: {kres['misplaced'][:3]}")
+    if kres["stray"]:
+        fail("knowledge:stray", f"{len(kres['stray'])} visible block(s) inside a day section that are not lessons (no day file carries them): {kres['stray'][:3]}")
+    dates = list(sections)
     if not dates:
         fail("knowledge:sections", "no `## YYYY-MM-DD` sections")
     else:
@@ -1199,8 +422,8 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
             ok("knowledge:order", "newest first")
         else:
             fail("knowledge:order", f"sections not newest-first: {dates}")
-        if dup_dates:
-            fail("knowledge:unique-dates", f"day section repeated: {sorted(set(dup_dates))}")
+        if kres["dups"]:
+            fail("knowledge:unique-dates", f"day section repeated: {sorted(set(kres['dups']))}")
         else:
             ok("knowledge:unique-dates", "no repeated day sections")
         impossible = [d for d in dates if not valid_date(d)]
@@ -1217,7 +440,7 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
     if empty:
         fail("knowledge:empty-days", f"day sections with no recognizable lessons: {empty}")
 
-# --- lessons/ --------------------------------------------------------------
+# --- lessons/ ------------------------------------------------------------------------
 ldir = ROOT / "lessons"
 if ldir.is_symlink():
     fail("lessons/", "is a symlink — the managed directory must be a real directory inside the repo")
@@ -1228,13 +451,13 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
         warn("lessons/", "directory missing (no day sections to cover)")
 else:
     entries = sorted(ldir.iterdir())                            # every entry, not only *.md
-    unreadable = [p.name for p in entries if not regular_file(p)]   # symlinks (live or dangling), directories
+    unreadable = [p.name for p in entries if not regular_file(p)]
     if unreadable:
         fail("lessons:readable", f"not regular files (symlinks and directories are rejected): {unreadable}")
     files = [p.name for p in entries if regular_file(p)]
-    stray = [f for f in files if not f.endswith(".md") or not DATE_RE.match(f[:-3]) or not valid_date(f[:-3])]
-    if stray:
-        fail("lessons:names", f"not a real YYYY-MM-DD.md: {stray}")
+    badnames = [f for f in files if not f.endswith(".md") or not DATE_RE.match(f[:-3]) or not valid_date(f[:-3])]
+    if badnames:
+        fail("lessons:names", f"not a real YYYY-MM-DD.md: {badnames}")
     else:
         ok("lessons:names", f"{len(files)} file(s), all dated")
     missing = [d for d in sections if f"{d}.md" not in files]
@@ -1249,67 +472,47 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
         p = ldir / f"{d}.md"
         if not regular_file(p):
             continue
-        raw = read_lines(p)
-        if raw is None:
+        text = read_text(p)
+        if text is None:
             continue
-        day_lines = sanitize(raw)
-        problems: list[str] = list(unclosed)
-        first = first_visible(day_lines)   # comments, code and reference definitions are skipped, not counted
-        hm = DAY_H1_RE.match(first)
+        ddoc = Doc(text)
+        problems = ddoc.unclosed()
+        shown = [n for n in ddoc.top if visible(n)]
+        head = shown[0] if shown else None
+        first = ddoc.line(head) if head else ""
+        hm = DAY_H1_RE.match(first) if head and head.kind == "heading" and head.t.tag == "h1" else None
         if not hm:
             problems.append(f"first visible line must be `# Lessons — {d}`, got {first[:40]!r}")
         elif hm.group(1) != d:
             problems.append(f"H1 date {hm.group(1)} does not match filename {d}")
-        stray_headings = [ln.strip() for ln in day_lines if is_h2(ln)]
-        if stray_headings:
-            problems.append(f"contains H2 heading(s) {stray_headings[:4]} — a per-day file has no `## ` headings")
-        # A per-day file carries no `## ` heading of its own: parse it as that day's section.
-        body = [ln for ln in day_lines if not is_h2(ln)]
-        if hm:
-            body.remove(first)                    # the day's own H1 (the first occurrence only)
-        day_sections, _ = parse_lessons([f"## {d}"] + body, presanitized=True)
-        if bad_headings:
-            problems.append(f"contains Setext H2 heading(s) {bad_headings[:4]} — a per-day file has no `## ` headings")
-        if stray:
-            problems.append(f"visible block(s) that are not lessons: {stray[:3]}")
-        day_lessons = day_sections.get(d, [])
+        headings = [h for n in ddoc.top for h in h2s(ddoc, n)]
+        if headings:
+            problems.append(f"contains H2 heading(s) {headings[:4]} — a per-day file has no `## ` headings")
+        body = [n for n in ddoc.top if n is not head or not hm]
+        body = [n for n in body if not (n.kind == "heading" and n.t.tag == "h2")]
+        dres = parse_sections(ddoc, body, day=d)
+        if dres["stray"]:
+            problems.append(f"visible block(s) that are not lessons: {dres['stray'][:3]}")
+        day_lessons = dres["sections"][d]
         bad_day = shape_errors({d: day_lessons})
         if bad_day:
             problems.append(f"shape: {bad_day[:4]}")
-        keys_k = [lesson_key(l) for l in sections[d]]
-        keys_d = [lesson_key(l) for l in day_lessons]
-        if keys_d != keys_k:
-            titles_k = {k[0] for k in keys_k}
-            titles_d = {k[0] for k in keys_d}
-            only_k = [k[0][:40] for k in keys_k if k[0] not in titles_d]   # title absent from the day file
-            only_d = [k[0][:40] for k in keys_d if k[0] not in titles_k]   # title absent from knowledge.md
-            diff_fields: list[str] = []
-            for a, b in zip(keys_k, keys_d):
-                if a != b and a[0] == b[0]:
-                    diff_fields.append(a[0][:30] + ": " + ",".join(f for f, x, y in zip(KEY_FIELDS, a, b) if x != y))
-            detail = f"{len(keys_d)} lesson(s) here vs {len(keys_k)} in knowledge.md"
-            if diff_fields:
-                detail += f"; same title, different content: {diff_fields[:3]}"
-            if only_k:
-                detail += f"; only in knowledge.md: {only_k[:3]}"
-            if only_d:
-                detail += f"; only here: {only_d[:3]}"
-            if not diff_fields and not only_k and not only_d:
-                detail += "; same set, different order" if len(keys_d) == len(keys_k) else "; a lesson repeats"
+        detail = compare(sections[d], day_lessons)
+        if detail:
             problems.append(detail)
         if problems:
             fail(f"lessons:{d}", "; ".join(problems))
         else:
             ok(f"lessons:{d}", f"{len(day_lessons)} lesson(s), identical content and order to knowledge.md")
 
-# --- verified/ -------------------------------------------------------------
+# --- verified/ -------------------------------------------------------------------------
 vdir = ROOT / "verified"
 if vdir.is_symlink():
     fail("verified/", "is a symlink — the managed directory must be a real directory inside the repo")
 elif not vdir.is_dir():
     warn("verified/", "directory missing")
 else:
-    ventries = sorted(vdir.iterdir())                           # every entry, not only *.md
+    ventries = sorted(vdir.iterdir())
     vunreadable = [p.name for p in ventries if not regular_file(p)]
     if vunreadable:
         fail("verified:readable", f"not regular files (symlinks and directories are rejected): {vunreadable}")
@@ -1319,20 +522,23 @@ def lesson_key(l: dict) -> tuple[str, str, str, str, str, str, str]:
         fail("verified:names", f"not a real YYYY-MM-DD_topic_vN.md: {badnames}")
     else:
         ok("verified:names", f"{len(vfiles)} sheet(s), all dated and versioned")
-    def first_nonblank(p: pathlib.Path) -> str | None:
-        """First line a reader sees: comment/declaration blocks are skipped; code and raw HTML count."""
-        lines = read_lines(p)
-        if lines is None:
-            return None
-        return first_visible(sanitize(lines))
-    firsts = {p.name: first_nonblank(p) for p in vfiles}
-    noh1 = [n for n, f in firsts.items() if f is not None and not is_real_h1(f)]
+    noh1 = []
+    for p in vfiles:
+        text = read_text(p)
+        if text is None:
+            continue
+        vdoc = Doc(text)
+        head = next((n for n in vdoc.top if visible(n)), None)
+        heading_text = ("".join(c.content for c in head.kids[0].t.children if c.type in ("text", "code_inline"))
+                        if head and head.kind == "heading" and head.t.tag == "h1" and head.t.markup == "#" else "")
+        if not heading_text.strip(WS):
+            noh1.append(p.name)
     if noh1:
-        fail("verified:h1", f"first visible non-blank line is not an H1 with text (indent ≤3, `# `; comments and declarations ignored, code and raw HTML count): {noh1}")
+        fail("verified:h1", f"first visible block is not an ATX H1 with text (comments and reference definitions ignored; code and raw HTML count): {noh1}")
     elif vfiles:
         ok("verified:h1", "every sheet starts with an H1")
 
-# --- summary ---------------------------------------------------------------
+# --- summary ---------------------------------------------------------------------------
 counts = {s: sum(r["status"] == s for r in RESULTS) for s in ("PASS", "WARN", "FAIL")}
 passed = counts["FAIL"] == 0
 if "--json" in sys.argv:
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
new file mode 100644
index 0000000..6d9f4b4
--- /dev/null
+++ b/harness/conformance/cases.json
@@ -0,0 +1,182 @@
+[
+ {
+  "id": "label-999",
+  "markdown": "[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: https://x.y\n\n[x][aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]\n",
+  "why": "labels up to 999 characters define a reference"
+ },
+ {
+  "id": "label-1000",
+  "markdown": "[aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa]: https://x.y\n\n# H1\n",
+  "why": "a 1000-character label is prose (patched in knowledge_md.py)"
+ },
+ {
+  "id": "angle-inside-destination",
+  "markdown": "[T](https://x.y/<bad>) confidence 80%\n",
+  "why": "< inside a bare destination is allowed"
+ },
+ {
+  "id": "pointy-unclosed",
+  "markdown": "[T](<https://x.y) confidence 80%\n",
+  "why": "an unclosed <destination is not a link"
+ },
+ {
+  "id": "escaped-gt-in-pointy",
+  "markdown": "[docs]: <https://example.com/a\\>b>\n\n[docs]\n",
+  "why": "escaped > inside <…> does not close it"
+ },
+ {
+  "id": "unbalanced-paren-def",
+  "markdown": "[docs]: https://example.com/(oops\n\n# H1\n",
+  "why": "unbalanced ( makes a definition prose"
+ },
+ {
+  "id": "def-title-then-quote",
+  "markdown": "[docs]: https://example.com \"first\"\n\"visible second title\"\n\n# H1\n",
+  "why": "a second quoted line is a paragraph"
+ },
+ {
+  "id": "def-two-line-title",
+  "markdown": "[docs]: https://example.com\n  \"the title\"\n\n[docs]\n",
+  "why": "a title on the next line belongs to the definition"
+ },
+ {
+  "id": "ordered-col0-after-list",
+  "markdown": "- a\n  - b\n2. c\n",
+  "why": "2. at column 0 after a list opens a new list"
+ },
+ {
+  "id": "ordered-inside-field",
+  "markdown": "- a\n  - b\n    2. c\n",
+  "why": "2. inside the item's paragraph is lazy text"
+ },
+ {
+  "id": "ordered-under-paragraph",
+  "markdown": "a\n2. b\n",
+  "why": "2. cannot interrupt a direct paragraph"
+ },
+ {
+  "id": "nbsp-after-heading",
+  "markdown": "## 2026-08-24 \n",
+  "why": "a NBSP is heading content"
+ },
+ {
+  "id": "nbsp-marker",
+  "markdown": "- x\n",
+  "why": "a NBSP after a marker is not a list item"
+ },
+ {
+  "id": "nbsp-only-line",
+  "markdown": "foo\n \nbar\n",
+  "why": "a NBSP-only line is not blank"
+ },
+ {
+  "id": "comment-tail",
+  "markdown": "<!-- hidden -->VISIBLE\n\n# H1\n",
+  "why": "text after a comment block's closer is emitted"
+ },
+ {
+  "id": "lowercase-decl",
+  "markdown": "<!foo> bar\n",
+  "why": "lowercase <!foo> does not start an HTML block"
+ },
+ {
+  "id": "uppercase-decl",
+  "markdown": "<!FOO>\n",
+  "why": "uppercase <!FOO> is a declaration block"
+ },
+ {
+  "id": "hgroup-under-title",
+  "markdown": "- **T**\n  <hgroup>\n  - Why it matters here: x\n",
+  "why": "hgroup is not a type-6 tag"
+ },
+ {
+  "id": "type7-first-in-item",
+  "markdown": "- **T**\n  - <sub>\n    - Why it matters here: x\n    - Do: y\n\n  - Source: z\n",
+  "why": "a complete tag as an item's first content opens a block"
+ },
+ {
+  "id": "quoted-fence",
+  "markdown": "- **T**\n  - Why it matters here: x\n\n  > ```\n  > <!-- alpha -->\n  > ```\n",
+  "why": "a fence inside a block quote"
+ },
+ {
+  "id": "fence-indent-3",
+  "markdown": "- a\n     ```\n        code\n     ```\n",
+  "why": "content keeps indentation beyond the opener's"
+ },
+ {
+  "id": "escaped-comment-opener",
+  "markdown": "x \\<!-- visible -->\n",
+  "why": "an escaped < is literal"
+ },
+ {
+  "id": "even-backslash-backtick",
+  "markdown": "- **a \\\\`**` b**\n",
+  "why": "\\\\ then ` opens a code span"
+ },
+ {
+  "id": "inner-strong-even-backslash",
+  "markdown": "- **alpha\\\\**beta**\n",
+  "why": "\\\\ leaves the inner ** live"
+ },
+ {
+  "id": "setext-in-quote",
+  "markdown": "> Foo\n> ---\n",
+  "why": "a Setext H2 inside a block quote"
+ },
+ {
+  "id": "h2-in-quote",
+  "markdown": "> ## Unexpected\n",
+  "why": "an ATX H2 inside a block quote"
+ },
+ {
+  "id": "cross-line-code-span",
+  "markdown": "- x `a  x\n  b` tail\n",
+  "why": "spaces inside a multi-line code span are kept"
+ },
+ {
+  "id": "empty-h1",
+  "markdown": "# ##\n",
+  "why": "a closing sequence alone leaves an empty H1"
+ },
+ {
+  "id": "escaped-hash-h1",
+  "markdown": "# \\#\n",
+  "why": "an escaped # is H1 text"
+ },
+ {
+  "id": "confidence-attr",
+  "markdown": "x <br title=\"confidence 80%\"> y\n",
+  "why": "attribute text is raw HTML, not prose"
+ },
+ {
+  "id": "nested-fence-no-closer",
+  "markdown": "- a\n  - b\n    ```\n    code\n  - c\n",
+  "why": "a fence ends with its list item"
+ },
+ {
+  "id": "unclosed-comment-in-item",
+  "markdown": "- a\n  - b\n    <!-- open\n  - c\n",
+  "why": "a comment block ends with its list item"
+ },
+ {
+  "id": "upper-pre-blank",
+  "markdown": "<PRE>\nraw\n\nstill raw\n</PRE>\nafter\n",
+  "why": "<PRE> is type 1 (case-insensitive) and survives a blank line"
+ },
+ {
+  "id": "nbsp-after-tag",
+  "markdown": "<div class=x>\ntext\n",
+  "why": "a NBSP after a tag name does not make an HTML block"
+ },
+ {
+  "id": "type7-nbsp",
+  "markdown": "<span a=b>\n",
+  "why": "a NBSP before an attribute does not make a complete tag"
+ },
+ {
+  "id": "lowercase-decl-after-field",
+  "markdown": "- **T**\n  - Do: y\n<!foo> extra\n",
+  "why": "a lowercase declaration interrupts and closes the list"
+ }
+]
\ No newline at end of file
diff --git a/harness/conformance/check.py b/harness/conformance/check.py
new file mode 100644
index 0000000..b1079f6
--- /dev/null
+++ b/harness/conformance/check.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python3
+"""check.py — differential conformance gate: the harness's Markdown parser (knowledge_md.MD)
+must render every CommonMark 0.31.2 spec example and every case in cases.json exactly like the
+reference implementation (commonmark.js 0.31.2). Output follows the harness contract.
+
+Usage: python3 harness/conformance/check.py ref.json   (ref.json from reference.cjs)
+"""
+import json
+import pathlib
+import sys
+
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
+from knowledge_md import MD  # noqa: E402
+
+import re  # noqa: E402
+
+# Renderer whitespace that no browser shows: the two libraries place a newline differently
+# right before some block-level tags (`<li>b<pre>` vs `<li>b\n<pre>`, an empty
+# `<blockquote>\n</blockquote>`). Both sides are compared with exactly that newline removed;
+# every other byte must match.
+BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|pre|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<!--)")
+# Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each must
+# still diverge (a stale entry fails the gate), so the list cannot silently outlive its reason.
+ALLOWED = {
+    "nbsp-after-tag": "commonmark.js matches HTML-block starts with JS `\\s`, which includes U+00A0; spec 0.31.2 "
+                      "section 4.6 allows only space, tab, end of line, `>` or `/>` after the tag name. cmark and "
+                      "cmark-gfm (GitHub) follow the spec: paragraph.",
+    "type7-nbsp": "same root cause: an attribute must be preceded by space, tab or a line ending (spec 6.6); "
+                  "cmark and cmark-gfm render a paragraph.",
+}
+
+
+def canon(html: str) -> str:
+    return BLOCK_NL.sub("", html)
+
+ref = json.load(open(sys.argv[1], encoding="utf-8"))
+bad, allowed_hit = [], []
+for group in ("spec", "cases"):
+    for ex in ref[group]:
+        if canon(MD.render(ex["markdown"])) != canon(ex["html"]):
+            (allowed_hit if ex["id"] in ALLOWED else bad).append(ex["id"])
+n_spec, n_cases = len(ref["spec"]), len(ref["cases"])
+def differing(group_is_spec: bool) -> int:
+    return sum(1 for x in bad + allowed_hit if x.startswith("spec#") == group_is_spec)
+
+
+print(f"{'PASS' if not bad else 'FAIL'} conformance:spec+cases   {n_spec - differing(True)}/{n_spec} spec examples identical, "
+      f"{n_cases - differing(False)}/{n_cases} edge cases identical, {len(allowed_hit)} reviewed exception(s) {sorted(allowed_hit)}")
+for b in bad:
+    ex = next(e for g in ("spec", "cases") for e in ref[g] if e["id"] == b)
+    print(f"  DIVERGES {b}: {ex.get('why', ex.get('section'))}\n    input {ex['markdown']!r}\n    ref   {ex['html']!r}\n    ours  {MD.render(ex['markdown'])!r}")
+stale = sorted(set(ALLOWED) - set(allowed_hit))
+for a in sorted(allowed_hit):
+    print(f"  allowed {a}: {ALLOWED[a]}")
+if stale:
+    print(f"FAIL conformance:allowlist      allowed divergence(s) no longer diverge — remove them: {stale}")
+print("----")
+ok = not bad and not stale
+print(f"RESULT: {'PASS' if ok else 'FAIL'} — {len(bad)} unexpected divergence(s)")
+sys.exit(0 if ok else 1)
diff --git a/harness/conformance/reference.cjs b/harness/conformance/reference.cjs
new file mode 100644
index 0000000..be57613
--- /dev/null
+++ b/harness/conformance/reference.cjs
@@ -0,0 +1,12 @@
+// Render the CommonMark 0.31.2 spec examples and harness/conformance/cases.json with the
+// reference implementation (commonmark.js 0.31.2). Usage: node reference.cjs <node_modules dir> > ref.json
+const path = require("path"), fs = require("fs");
+const nm = process.argv[2];
+const cm = require(path.join(nm, "commonmark"));
+const spec = require(path.join(nm, "commonmark-spec")).tests;
+const cases = JSON.parse(fs.readFileSync(path.join(__dirname, "cases.json"), "utf8"));
+const render = (s) => new cm.HtmlRenderer().render(new cm.Parser().parse(s));
+process.stdout.write(JSON.stringify({
+  spec: spec.map((e) => ({ id: `spec#${e.number}`, section: e.section, markdown: e.markdown, html: render(e.markdown) })),
+  cases: cases.map((c) => ({ id: c.id, why: c.why, markdown: c.markdown, html: render(c.markdown) })),
+}));
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
new file mode 100644
index 0000000..2e32269
--- /dev/null
+++ b/harness/knowledge_md.py
@@ -0,0 +1,52 @@
+"""knowledge_md.py — the one Markdown parser every harness script uses.
+
+markdown-it-py with the CommonMark preset, plus the patches that close the gaps between it and
+the CommonMark 0.31.2 reference implementation (commonmark.js) that this repo's checks depend
+on. harness/conformance/check.py proves the combination against the reference on all 652 spec
+examples and on cases.json; a new markdown-it-py release that drifts fails CI there first.
+"""
+from __future__ import annotations
+
+import re
+import sys
+
+import markdown_it.rules_block  # noqa: F401  (loads the html_block module patched below)
+from markdown_it import MarkdownIt
+from markdown_it.rules_block.reference import reference as _reference
+
+
+def _reference_999(state, start: int, end: int, silent: bool) -> bool:
+    """markdown-it-py 4.0.0 accepts link-reference labels of any length; CommonMark caps them at
+    999 characters (the reference implementation renders a longer one as a paragraph). Undo such
+    a definition so the line stays visible prose. Pinned by harness/conformance/cases.json (label-999, label-1000)."""
+    if silent:                                      # a probe only: nothing is recorded, nothing to undo
+        return _reference(state, start, end, silent)
+    before = dict(state.env.get("references", {}))
+    n_tokens = len(state.tokens)
+    if not _reference(state, start, end, silent):
+        return False
+    raw =state.src[state.bMarks[start] + state.tShift[start]:state.eMarks[state.line - 1]]
+    m = re.match(r"\[((?:\\.|[^\\\[\]])*)\]:", raw, re.S)
+    if m and len(m.group(1)) > 999:
+        state.env["references"] = before
+        del state.tokens[n_tokens:]
+        state.line = start
+        return False
+    return True
+
+
+def _patch_html_blocks() -> None:
+    """markdown-it-py 4.0.0 starts a type-4 (declaration) HTML block only for an UPPERCASE letter
+    after `<!` — CommonMark 0.31.2 takes any ASCII letter — and uses Python's `\\s`, which also
+    matches a NBSP, where the spec allows only space and tab. Rewrite those start patterns in
+    place (the rule reads the module table at call time). Pinned by cases.json
+    (lowercase-decl, nbsp-after-tag, type7-nbsp)."""
+    seqs = sys.modules["markdown_it.rules_block.html_block"].HTML_SEQUENCES
+    for k, (start, end, can_interrupt) in enumerate(seqs):
+        pattern = start.pattern.replace("<![A-Z]", "<![A-Za-z]").replace(r"\s", "[ \t]")
+        seqs[k] = (re.compile(pattern, start.flags), end, can_interrupt)
+
+
+_patch_html_blocks()
+MD = MarkdownIt("commonmark")
+MD.block.ruler.at("reference", _reference_999)
diff --git a/harness/requirements.txt b/harness/requirements.txt
new file mode 100644
index 0000000..d0a92d0
--- /dev/null
+++ b/harness/requirements.txt
@@ -0,0 +1 @@
+markdown-it-py==4.0.0
diff --git a/harness/tests/README.md b/harness/tests/README.md
new file mode 100644
index 0000000..9431162
--- /dev/null
+++ b/harness/tests/README.md
@@ -0,0 +1,19 @@
+# Negative-test suites
+
+Each `negNN.sh` copies the repository to a temporary directory, applies one
+mutation per case (usually to both `knowledge.md` and the day file, so the
+structural check, not the copy comparison, has to catch it), runs
+`harness/check-knowledge.py`, and asserts the verdict. `run` expects a FAIL
+containing a given string, `pos` expects PASS, and `stray_only` expects a
+`knowledge:stray` FAIL without the wrong diagnosis.
+
+The suite numbers follow the review rounds that produced them (round 11 to
+round 27 of the review on PR #1). Cases marked `CORRECTED 2026-09-27` encoded a
+CommonMark reading that the reference implementation (commonmark.js 0.31.2)
+proved wrong. Each correction keeps a replacement case for the intent that
+survives.
+
+```bash
+pip install -r harness/requirements.txt
+bash harness/tests/run.sh      # one RESULT line per suite, then TOTAL
+```
diff --git a/harness/tests/neg11.sh b/harness/tests/neg11.sh
new file mode 100755
index 0000000..1311e44
--- /dev/null
+++ b/harness/tests/neg11.sh
@@ -0,0 +1,107 @@
+#!/usr/bin/env bash
+# Round-11 negative tests: each case copies the repo, applies one mutation, and
+# expects the named check to FAIL (or the named behaviour). Prints PASS/FAIL per case.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+run() { # name expected-substring-in-output mutation-fn
+  local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
+  ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else
+    bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'
+  fi
+}
+
+# 1. backtick fence opener whose info string has a backtick must NOT open a fence
+#    -> the lesson after it stays visible; put a broken lesson after such a line so shape FAILs
+t1() { python3 - <<'EOF'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]
+s.insert(i+1,'```py `x`')            # not a fence per CommonMark
+s.insert(i+2,'- **Visible lesson with no fields**')
+open(p,'w').write('\n'.join(s))
+EOF
+}
+run "1 fence-info-backtick" "missing why" t1
+
+# 2. lazy continuation at indent 0 after a field is content -> copies differ
+t2() { python3 - <<'EOF'
+p='lessons/2026-08-24.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Do:')][0]
+s.insert(i+1,'extra lazy text only in the day file')
+open(p,'w').write('\n'.join(s))
+EOF
+}
+run "2 lazy-continuation" "same title, different content" t2
+
+# 3. tab after the list marker is a top-level item -> `-\tNot bold` is malformed
+t3() { python3 - <<'EOF'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]
+s.insert(i+1,'-\tTab-marked bullet that is not a bold lesson')
+open(p,'w').write('\n'.join(s))
+EOF
+}
+run "3 tab-after-marker" "not-a-bold-lesson-bullet" t3
+
+# 4a. live symlink in verified/ rejected
+t4a() { ln -s ../README.md verified/2026-01-01_link_v1.md; }
+run "4a verified-symlink" "verified:readable" t4a
+# 4b. live symlink in lessons/ rejected
+t4b() { ln -s ../README.md lessons/2026-01-01.md; }
+run "4b lessons-symlink" "lessons:readable" t4b
+
+# 5. whitespace-only bold title is malformed
+t5() { python3 - <<'EOF'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]
+s.insert(i+1,'- ** **')
+open(p,'w').write('\n'.join(s))
+EOF
+}
+run "5 whitespace-bold-title" "not-a-bold-lesson-bullet" t5
+
+# 6. verified sheet whose first line is an HTML comment then NO H1 -> must FAIL
+#    (previously the comment line itself was tested; now the visible first line is)
+t6() { printf '<!-- lead comment -->\nNot a heading\n' > verified/2026-01-02_nohead_v1.md; }
+run "6 verified-h1-sanitized" "verified:h1" t6
+
+# 7. a lesson before the first `## date` -> misplaced FAIL
+t7() { python3 - <<'EOF'
+p='knowledge.md'; s=open(p).read().split('\n')
+s.insert(1,'- **Lesson placed before any day heading**')
+open(p,'w').write('\n'.join(s))
+EOF
+}
+run "7 misplaced-lesson" "knowledge:misplaced" t7
+
+# 6-positive: a verified sheet that STARTS with a comment and then a real H1 must PASS
+n=$((n+1))
+rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
+printf '<!-- lead comment -->\n\n# Real heading\n\nbody\n' > "$W/r/verified/2026-01-03_lead_v1.md"
+out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+if [ $rc -eq 0 ]; then echo "PASS 6+ comment-then-H1 accepted"; else bad=$((bad+1)); echo "FAIL 6+ comment-then-H1 rejected:"; echo "$out" | sed 's/^/    /'; fi
+
+# 3-positive: `-\t**Title**` lesson with tab-marked fields in BOTH copies must PASS
+n=$((n+1))
+rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
+( cd "$W/r" && python3 - <<'EOF'
+add=['-\t**Tab lesson**','    -\tWhy it matters here: tabs','    -\tDo: nothing','    -\tSource: [T](https://x.y) — A · confidence 50%']   # content column is 4 after a tab
+for p,marker in (('knowledge.md','## '),('lessons/2026-08-24.md','# Lessons')):
+    s=open(p).read().split('\n'); i=[k for k,l in enumerate(s) if l.startswith(marker)][0]
+    s[i+1:i+1]=['']+add
+    open(p,'w').write('\n'.join(s))
+EOF
+)
+out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+if [ $rc -eq 0 ] && printf '%s' "$out" | grep -q "6 lesson(s)"; then echo "PASS 3+ tab-marked lesson counted (6 lessons)"; else bad=$((bad+1)); echo "FAIL 3+ tab lesson:"; echo "$out" | sed 's/^/    /'; fi
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg12.sh b/harness/tests/neg12.sh
new file mode 100755
index 0000000..cc65e4c
--- /dev/null
+++ b/harness/tests/neg12.sh
@@ -0,0 +1,74 @@
+#!/usr/bin/env bash
+# Round-12 negative tests. Each mutation is applied to BOTH copies (knowledge.md and the day file)
+# so the copy-comparison cannot be what catches it; the structural check must.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { # python snippet that mutates one file given as sys.argv[1]
+  for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done
+}
+run() { # name expected mutation-fn
+  local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
+  ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else
+    bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'
+  fi
+}
+pos() { # name mutation-fn : must stay PASS
+  local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
+  ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi
+}
+
+# helper: insert a lesson right after the first heading line (## date / # Lessons)
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+
+# 1. fence nested under a field at 4 absolute spaces hides Do/Source -> must FAIL missing do/source
+t1() { both 'LESSON=["- **Nested fence lesson**","  - Why it matters here: x","    ```","    - Do: hidden in code","    - Source: [T](https://x.y) — A · confidence 50%","    ```"]'"$INS"; }
+run "1 nested-fence-hides-fields" "missing do" t1
+
+# 1b. same, but fence nested at the lesson level (2 spaces) — already ≤3, must FAIL too (regression guard)
+t1b() { both 'LESSON=["- **Two-space fence lesson**","  ```","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%","  ```"]'"$INS"; }
+run "1b two-space-fence" "missing why" t1b
+
+# 1c. fence nested at 4 spaces that closes, followed by real fields -> must PASS
+t1c() { both 'LESSON=["- **Nested fence then fields**","  - Why it matters here: x","    ```","    code sample","    ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "1c nested-fence-then-fields" t1c
+
+# 2a. thematic break between fields closes the lesson -> later fields are stray bullets -> FAIL
+t2a() { both 'LESSON=["- **Split by hr**","  - Why it matters here: x","","---","","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "2a thematic-break-splits" "missing do" t2a
+
+# 2b. non-lazy paragraph after a blank line closes the lesson -> FAIL
+t2b() { both 'LESSON=["- **Split by paragraph**","  - Why it matters here: x","","A top-level paragraph.","","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "2b paragraph-splits" "missing do" t2b
+
+# 2c. H3 heading between fields closes the lesson -> FAIL
+t2c() { both 'LESSON=["- **Split by h3**","  - Why it matters here: x","### Sub","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "2c heading-splits" "missing do" t2c
+
+# 2d. lazy continuation (no blank line) must still NOT split -> PASS
+t2d() { both 'LESSON=["- **Lazy ok**","  - Why it matters here: x","lazy tail","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "2d lazy-continuation-keeps-lesson" t2d
+
+# 2e. blank line between fields (no block) must still NOT split -> PASS
+t2e() { both 'LESSON=["- **Blank ok**","  - Why it matters here: x","","  - Do: y","","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "2e blank-lines-keep-lesson" t2e
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg13.sh b/harness/tests/neg13.sh
new file mode 100755
index 0000000..3453d6e
--- /dev/null
+++ b/harness/tests/neg13.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+run() { local name="$1" expect="$2"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+
+# 1. top-level fence between Why and Do closes the list -> FAIL (same in both copies)
+t1() { both 'LESSON=["- **Split by fence**","  - Why it matters here: x","","```","code","```","","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "1 top-level-fence-splits" "missing do" t1
+# 1b. same without blank lines around the fence -> still FAIL (a fence interrupts a paragraph)
+t1b() { both 'LESSON=["- **Split by fence tight**","  - Why it matters here: x","```","code","```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "1b tight-fence-splits" "missing do" t1b
+# 2. fence nested INSIDE the lesson (2 spaces) between fields -> PASS (item continues)
+t2() { both 'LESSON=["- **Fence inside item**","  - Why it matters here: x","  ```","  code","  ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "2 fence-inside-item-keeps-lesson" t2
+# 3. fence nested under a field (4 spaces) then fields -> PASS
+t3() { both 'LESSON=["- **Fence under field**","  - Why it matters here: x","    ```","    code","    ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "3 fence-under-field-keeps-lesson" t3
+# 4. top-level fence AFTER a complete lesson, then another lesson -> PASS
+t4() { both 'LESSON=["- **Complete**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%","","```","code","```","","- **Second**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+stray_only "4 fence-between-lessons" "missing" t4 "7 lesson(s)"
+# 5. a fenced block under Why is VISIBLE to readers (rewritten 2026-09-27, round 23): a copy without it
+#    differs, and the raw sentinel must never surface in a diagnostic. Same block in both copies -> PASS.
+t5() { python3 -c 'LESSON=["- **Leak check**","  - Why it matters here: x","    ```","    secret","    ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Leak check**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS" lessons/2026-08-24.md; }
+run "5 fence-in-one-copy-differs" "same title, different content" t5
+t5b() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c 'LESSON=["- **Leak check**","  - Why it matters here: x","    ```","    secret","    ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS" "$p"; done; }
+pos "5b fence-in-both-copies-identical" t5b
+n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && t5 )
+if (cd "$W/r" && python3 harness/check-knowledge.py 2>&1) | grep -qaP '\x00'; then bad=$((bad+1)); echo "FAIL 5c raw sentinel leaked into a diagnostic"; else echo "PASS 5c no raw sentinel in diagnostics"; fi
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg14.sh b/harness/tests/neg14.sh
new file mode 100755
index 0000000..e3fd03f
--- /dev/null
+++ b/harness/tests/neg14.sh
@@ -0,0 +1,95 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+
+# 1. top-level HTML comment block between fields closes the list -> FAIL
+t1() { both 'LESSON=["- **Split by comment**","  - Why it matters here: x","<!-- note -->","  - Do: y","'"$F"'"]'"$INS"; }
+run "1 comment-block-splits" "missing do" t1
+# 1b. multi-line top-level comment -> FAIL
+t1b() { both 'LESSON=["- **Split by long comment**","  - Why it matters here: x","<!-- note","more -->","  - Do: y","'"$F"'"]'"$INS"; }
+run "1b multiline-comment-splits" "missing do" t1b
+# 1c. INLINE comment inside the Why text does not split -> PASS
+t1c() { both 'LESSON=["- **Inline comment**","  - Why it matters here: x <!-- c --> y","  - Do: y","'"$F"'"]'"$INS"; }
+pos "1c inline-comment-keeps-lesson" t1c
+# 1d. comment block nested inside the item (2 spaces) -> PASS
+t1d() { both 'LESSON=["- **Nested comment**","  - Why it matters here: x","  <!-- c -->","  - Do: y","'"$F"'"]'"$INS"; }
+pos "1d nested-comment-keeps-lesson" t1d
+
+# 2. `- - -` thematic break in a section is NOT a malformed lesson -> PASS
+t2() { both 'LESSON=["- **Complete**","  - Why it matters here: x","  - Do: y","'"$F"'","","- - -",""]'"$INS"; }
+stray_only "2 dash-space-hr-not-a-bullet" "not-a-bold-lesson-bullet" t2
+# 2b. `* * *` before the first section is NOT misplaced -> PASS
+t2b() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n'); s.insert(1,'* * *'); open(p,'w').write('\n'.join(s))
+PY
+}
+run "2b hr-before-first-section-is-misplaced-not-malformed" "knowledge:misplaced" t2b   # a visible rule outside any day section (round 21); never a malformed lesson (round 14)
+n=$((n+1)); prep t2b; if (cd "$W/r" && python3 harness/check-knowledge.py 2>&1) | grep -q "not-a-bold-lesson-bullet"; then bad=$((bad+1)); echo "FAIL 2b+ hr reported as malformed lesson"; else echo "PASS 2b+ hr not reported as a malformed lesson"; fi
+# 2c. but `- - -` between Why and Do still splits the lesson -> FAIL
+t2c() { both 'LESSON=["- **Split by dash hr**","  - Why it matters here: x","- - -","  - Do: y","'"$F"'"]'"$INS"; }
+run "2c dash-space-hr-splits" "missing do" t2c
+
+# 3. verified sheet: a fenced code block before the H1 RENDERS (<pre>), so the H1 is not the first visible
+#    content -> FAIL. Rewritten 2026-09-27 (round 24): the old PASS expectation treated code as invisible.
+t3() { printf '```\ncode\n```\n\n# Real heading\n' > verified/2026-01-04_fence_v1.md; }
+run "3 verified-fence-then-h1" "verified:h1" t3
+# 3b. verified sheet: fence then NO H1 -> FAIL
+t3b() { printf '```\ncode\n```\n\nNot a heading\n' > verified/2026-01-05_nofence_v1.md; }
+run "3b verified-fence-then-no-h1" "verified:h1" t3b
+
+# 4. `- **First** plain **second**` is two strong spans -> malformed
+t4() { both 'LESSON=["- **First** plain **second**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS"; }
+run "4 two-strong-spans" "not-a-bold-lesson-bullet" t4
+# 4b. escaped \*\* inside a title is fine -> PASS
+t4b() { both 'LESSON=["- **Use \\*\\*bold\\*\\* sparingly**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS"; }
+pos "4b escaped-stars-in-title" t4b
+
+# 5. `-\t**T**` has content column 4: 2-space fields are siblings -> FAIL
+t5() { both 'LESSON=["-\t**Tab lesson**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS"; }
+run "5 tab-marker-2-space-fields" "missing why" t5
+# 5b. same lesson with 4-space fields -> PASS
+t5b() { both 'LESSON=["-\t**Tab lesson**","    - Why it matters here: x","    - Do: y","    - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "5b tab-marker-4-space-fields" t5b
+# 5c. `-    **T**` (4 spaces after marker, content col 5): 2-space fields are siblings -> FAIL
+t5c() { both 'LESSON=["-    **Wide gap**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS"; }
+run "5c wide-gap-2-space-fields" "missing why" t5c
+# 5d. `-     **T**` (5 spaces after marker) is code inside the item, not a bold title -> malformed
+t5d() { both 'LESSON=["-     **Five spaces**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS"; }
+run "5d five-space-gap-is-code" "not-a-bold-lesson-bullet" t5d
+# 2d. `- * -` mixes characters: NOT a thematic break, so it is a malformed bullet -> FAIL
+t2d() { both 'LESSON=["- * -"]'"$INS"; }
+run "2d mixed-chars-not-hr" "not-a-bold-lesson-bullet" t2d
+
+# 6. lessons/ as a symlink to a real dir -> FAIL; same for verified/
+t6() { mv lessons real-lessons && ln -s real-lessons lessons; }
+run "6 lessons-dir-symlink" "lessons/" t6
+t6b() { mv verified real-verified && ln -s real-verified verified; }
+run "6b verified-dir-symlink" "verified/" t6b
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg15.sh b/harness/tests/neg15.sh
new file mode 100755
index 0000000..783ff29
--- /dev/null
+++ b/harness/tests/neg15.sh
@@ -0,0 +1,75 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+L3='  - Why it matters here: x","  - Do: y","'"$F"'"'
+
+# 1. `  ## 2026-08-23` nested inside a lesson item is NOT a day section; a lessons/2026-08-23.md is then an orphan -> FAIL
+t1() { printf '\n- **Complete**\n  - Why it matters here: x\n  - Do: y\n%s\n\n  ## 2026-08-23\n\n- **Nested day lesson**\n  - Why it matters here: x\n  - Do: y\n%s\n' "$F" "$F" >> knowledge.md
+       printf '\n- **Complete**\n  - Why it matters here: x\n  - Do: y\n%s\n' "$F" >> lessons/2026-08-24.md
+       printf '# Lessons — 2026-08-23\n\n- **Nested day lesson**\n  - Why it matters here: x\n  - Do: y\n%s\n' "$F" > lessons/2026-08-23.md; }
+run "1 nested-date-heading-not-a-section" "lessons:orphans" t1
+
+# 2. `<div>boundary</div>` at top level between Why and Do closes the list -> FAIL
+t2() { both 'LESSON=["- **Split by div**","  - Why it matters here: x","<div>boundary</div>","  - Do: y","'"$F"'"]'"$INS"; }
+run "2 html-block-splits" "missing do" t2
+# 2b. a type-7 tag (`<span>`) directly under the paragraph is lazy continuation -> PASS
+t2b() { both 'LESSON=["- **Span ok**","  - Why it matters here: x","<span>note</span>","  - Do: y","'"$F"'"]'"$INS"; }
+pos "2b type7-tag-is-lazy-continuation" t2b
+
+# 3. a 10-digit "ordered marker" is a paragraph, not a list item -> PASS
+t3() { both 'LESSON=["- **Complete**","'"$L3"',"","1234567890. This is an identifier",""]'"$INS"; }
+stray_only "3 ten-digit-number-is-paragraph" "not-a-bold-lesson-bullet" t3
+# 3b. a 9-digit one IS a list item (malformed lesson) -> FAIL
+t3b() { both 'LESSON=["123456789. still a list item"]'"$INS"; }
+run "3b nine-digit-marker-is-list" "not-a-bold-lesson-bullet" t3b
+
+# 4. `**` inside a code span in the title is opaque -> PASS
+t4() { both 'LESSON=["- **Use `**` as the delimiter.**","'"$L3"']'"$INS"; }
+pos "4 stars-in-title-code-span" t4
+# 4b. real inner ** still malformed -> FAIL
+t4b() { both 'LESSON=["- **a** b **c**","'"$L3"']'"$INS"; }
+run "4b real-inner-strong-still-malformed" "not-a-bold-lesson-bullet" t4b
+
+# 5. hard break (2 trailing spaces) in one copy only -> content differs -> FAIL
+t5() { python3 -c 'LESSON=["- **Hard**","  - Why it matters here: x","  - Do: y  ","    more","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Hard**","  - Why it matters here: x","  - Do: y","    more","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "5 hard-break-differs" "same title, different content" t5
+# 5b. hard break in both copies -> PASS
+t5b() { both 'LESSON=["- **Hard both**","  - Why it matters here: x","  - Do: y  ","    more","'"$F"'"]'"$INS"; }
+pos "5b hard-break-in-both" t5b
+# 5c. trailing spaces on the LAST line of a field are not a break -> copies identical -> PASS
+t5c() { python3 -c 'LESSON=["- **Tail**","  - Why it matters here: x","  - Do: y  ","'"$F"'"]'"$INS" knowledge.md
+        python3 -c 'LESSON=["- **Tail**","  - Why it matters here: x","  - Do: y","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+pos "5c trailing-spaces-without-continuation" t5c
+# 5d. backslash hard break in one copy only -> FAIL
+t5d() { python3 -c 'LESSON=["- **Slash**","  - Why it matters here: x","  - Do: y\\","    more","'"$F"'"]'"$INS" knowledge.md
+        python3 -c 'LESSON=["- **Slash**","  - Why it matters here: x","  - Do: y","    more","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "5d backslash-break-differs" "same title, different content" t5d
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg16.sh b/harness/tests/neg16.sh
new file mode 100755
index 0000000..94bac03
--- /dev/null
+++ b/harness/tests/neg16.sh
@@ -0,0 +1,69 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+W1='  - Why it matters here: x'
+
+# 1. knowledge.md as a symlink -> FAIL
+t1() { mv knowledge.md real.md && ln -s real.md knowledge.md; }
+run "1 knowledge-symlink" "knowledge.md" t1
+
+# 2. whitespace inside inline code differs -> FAIL
+t2() { python3 -c 'LESSON=["- **Code ws**","'"$W1"'","  - Do: run `printf '"'"'a b'"'"'`","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Code ws**","'"$W1"'","  - Do: run `printf '"'"'a  b'"'"'`","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "2 code-span-whitespace-differs" "same title, different content" t2
+# 2b. whitespace in PROSE still normalized -> PASS
+t2b() { python3 -c 'LESSON=["- **Prose ws**","'"$W1"'","  - Do: run  it   now","'"$F"'"]'"$INS" knowledge.md
+        python3 -c 'LESSON=["- **Prose ws**","'"$W1"'","  - Do: run it now","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+pos "2b prose-whitespace-still-normalized" t2b
+
+# 3. unbalanced paren in the link destination -> not a link -> FAIL
+t3() { both 'LESSON=["- **Bad link**","'"$W1"'","  - Do: y","  - Source: [T](https://x.y/(broken) — A · confidence 50%"]'"$INS"; }
+run "3 unbalanced-dest-paren" "source line is not" t3
+# 3b. balanced parens in the destination -> PASS
+t3b() { both 'LESSON=["- **Ok link**","'"$W1"'","  - Do: y","  - Source: [T](https://x.y/(ok)) — A · confidence 50%"]'"$INS"; }
+pos "3b balanced-dest-paren" t3b
+
+# 4. `- Do:` with its value on the continuation line -> PASS
+t4() { both 'LESSON=["- **Deferred**","'"$W1"'","  - Do:","    y","'"$F"'"]'"$INS"; }
+pos "4 value-on-continuation-line" t4
+# 4b. `- Do:` with nothing after -> still FAIL
+t4b() { both 'LESSON=["- **Empty do**","'"$W1"'","  - Do:","'"$F"'"]'"$INS"; }
+run "4b empty-do-still-fails" "missing or empty do" t4b
+# 4c. `- Source:` with the link on the continuation line -> PASS
+t4c() { both 'LESSON=["- **Deferred source**","'"$W1"'","  - Do: y","  - Source:","    [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "4c source-on-continuation-line" t4c
+
+# 5. verified sheet with a tab after `#` -> PASS
+t5() { printf '#\tTab heading\n\nbody\n' > verified/2026-01-06_tab_v1.md; }
+pos "5 tab-after-hash-is-h1" t5
+
+# 6. code span crossing a line boundary with `<!--` on the second line, in both copies -> PASS
+t6() { both 'LESSON=["- **Cross-line code**","  - Why it matters here: see `git log","    --oneline <!-- not a comment` here","  - Do: y","'"$F"'"]'"$INS"; }
+pos "6 code-span-across-lines" t6
+# 6b. but `<!--` at the START of the next line is an HTML block (blocks parse before inlines) -> comment opens -> FAIL
+t6b() { both 'LESSON=["- **Block wins**","  - Why it matters here: see `git log","    <!-- a real comment block` here","  - Do: y","'"$F"'"]'"$INS"; }
+run "6b html-block-start-beats-open-span" "never closes" t6b
+# 6c. the span closes on line 2 and a real `<!--` follows AFTER the closer -> comment opens -> FAIL
+t6c() { split 'LESSON=["- **After closer**","  - Why it matters here: see `git log","    --oneline` <!-- hidden","    still hidden --> visible","  - Do: y","'"$F"'"]'"$INS" \
+              'LESSON=["- **After closer**","  - Why it matters here: see `git log","    --oneline` visible","  - Do: y","'"$F"'"]'"$INS"; }
+pos "6c comment-after-closer" t6c   # a real comment after the closer is stripped → the copies match; a harness that kept the span open would see `<!--` as code and FAIL
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg17.sh b/harness/tests/neg17.sh
new file mode 100755
index 0000000..0b1acb3
--- /dev/null
+++ b/harness/tests/neg17.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+L3='  - Why it matters here: x","  - Do: y","'"$F"'"'
+
+# 1. closing ** preceded by whitespace cannot close -> not bold -> malformed
+t1() { both 'LESSON=["- **Not actually strong **","'"$L3"']'"$INS"; }
+run "1 trailing-space-before-closer" "not-a-bold-lesson-bullet" t1
+# 1b. opening ** followed by whitespace cannot open -> malformed
+t1b() { both 'LESSON=["- ** Not strong**","'"$L3"']'"$INS"; }
+run "1b space-after-opener" "not-a-bold-lesson-bullet" t1b
+
+# 2. verified sheet saved with a UTF-8 BOM -> H1 still recognised -> PASS
+t2() { printf '\xef\xbb\xbf# BOM heading\n\nbody\n' > verified/2026-01-07_bom_v1.md; }
+pos "2 bom-before-h1" t2
+
+# 3. two spaces before a stripped inline comment are NOT a hard break -> copies identical -> PASS
+t3() { python3 -c 'LESSON=["- **Comment spaces**","  - Why it matters here: text  <!-- note -->","    more","  - Do: y","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Comment spaces**","  - Why it matters here: text","    more","  - Do: y","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+pos "3 spaces-before-comment-not-a-break" t3
+# 3b. two spaces AFTER the comment are a real hard break -> copies differ -> FAIL
+t3b() { python3 -c 'LESSON=["- **Real break**","  - Why it matters here: text <!-- note -->  ","    more","  - Do: y","'"$F"'"]'"$INS" knowledge.md
+        python3 -c 'LESSON=["- **Real break**","  - Why it matters here: text","    more","  - Do: y","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "3b spaces-after-comment-are-a-break" "same title, different content" t3b
+
+# 4. backslash-escaped paren in the destination is text -> PASS
+t4() { both 'LESSON=["- **Escaped paren**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y/\\() — A · confidence 80%"]'"$INS"; }
+pos "4 escaped-paren-in-destination" t4
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg18.sh b/harness/tests/neg18.sh
new file mode 100755
index 0000000..503e370
--- /dev/null
+++ b/harness/tests/neg18.sh
@@ -0,0 +1,77 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+L3='  - Why it matters here: x","  - Do: y","'"$F"'"'
+
+# 1. escaped closing asterisk: `\**` is `\*` + `*`, no strong closer -> malformed
+t1() { both 'LESSON=["- **Broken closing escape\\**","'"$L3"']'"$INS"; }
+run "1 escaped-closing-star" "not-a-bold-lesson-bullet" t1
+# 1b. escaped backslash before the closer is fine -> PASS
+t1b() { both 'LESSON=["- **Ends with a backslash\\\\**","'"$L3"']'"$INS"; }
+pos "1b escaped-backslash-then-closer" t1b
+
+# 2. an escaped backtick does not open a code span, so the comment after it is real -> fields hidden -> FAIL
+t2() { split 'LESSON=["- **Escaped tick**","  - Why it matters here: visible \\` <!-- hidden","    hidden too --> tail","  - Do: y","'"$F"'"]'"$INS" \
+             'LESSON=["- **Escaped tick**","  - Why it matters here: visible \\` tail","  - Do: y","'"$F"'"]'"$INS"; }
+pos "2 escaped-backtick-not-a-span" t2   # the comment is real (closer inside the paragraph) → stripped → copies match; a span-opening `\`` would mask it and FAIL
+
+# 3. the whole dated content wrapped in a <pre> HTML block is not Markdown -> FAIL
+t3() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().rstrip('\n').split('\n'); s.insert(1,'<pre>'); s.append('</pre>'); open(p,'w').write('\n'.join(s)+'\n')
+p='lessons/2026-08-24.md'; s=open(p).read().rstrip('\n').split('\n'); s.insert(1,'<pre>'); s.append('</pre>'); open(p,'w').write('\n'.join(s)+'\n')
+PY
+}
+run "3 pre-block-hides-everything" "knowledge:sections" t3
+# 3b. a <div> block ends at the first blank line; lessons after it are visible -> PASS
+t3b() { both 'LESSON=["<div>","banner","</div>","","- **After div**","'"$L3"']'"$INS"; }
+stray_only "3b div-block-ends-at-blank-line" "FAIL knowledge:sections" t3b "6 lesson(s)"
+# 3c. <div> with the fields inside the block (no blank line) -> hidden -> FAIL
+t3c() { both 'LESSON=["- **Div swallows**","  - Why it matters here: x","<div>","  - Do: y","'"$F"'","</div>"]'"$INS"; }
+run "3c div-block-swallows-fields" "missing do" t3c
+
+# 4. `text\` is a hard break, `text\ ` is not -> copies differ -> FAIL
+t4() { python3 -c 'LESSON=["- **Slash space**","  - Why it matters here: x","  - Do: y\\","    more","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Slash space**","  - Why it matters here: x","  - Do: y\\ ","    more","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "4 backslash-space-not-a-break" "same title, different content" t4
+
+# 5. CORRECTED 2026-09-27 (harness v2): `<` / `>` INSIDE a bare destination are allowed — only a leading `<`
+#    changes the form. commonmark.js 0.31.2 renders [T](https://x.y/<bad>) as a link (href https://x.y/%3Cbad%3E),
+#    so the round-18 expectation ("not a link -> FAIL") was wrong -> PASS
+t5() { both 'LESSON=["- **Angle dest**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y/<bad>) — A · confidence 80%"]'"$INS"; }
+pos "5 angle-brackets-inside-destination-are-a-link" t5
+# 5b. the intent that survives: a destination OPENED with `<` and never closed is not a link (reference: literal text) -> FAIL
+t5b() { both 'LESSON=["- **Angle open**","  - Why it matters here: x","  - Do: y","  - Source: [T](<https://x.y) — A · confidence 80%"]'"$INS"; }
+run "5b unclosed-pointy-destination-is-text" "source line is not" t5b
+
+# 6. U+2028 inside one physical line does not make new lines -> fields are not real -> FAIL
+t6() { both 'LESSON=["- **Line sep**","  - Why it matters here: x
  - Do: y
  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+run "6 unicode-line-separator" "missing do" t6
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg19.sh b/harness/tests/neg19.sh
new file mode 100755
index 0000000..9a42e85
--- /dev/null
+++ b/harness/tests/neg19.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+L3='  - Why it matters here: x","  - Do: y","'"$F"'"'
+
+# 1. CORRECTED 2026-09-27 (harness v2): CommonMark 0.31.2 starts a declaration block at `<!` + ANY ASCII letter,
+#    so `<!foo>` is an invisible declaration and the H1 IS the first visible block (commonmark.js 0.31.2: raw
+#    `<!foo>` then <h1>) -> PASS. Note: GitHub renders with cmark-gfm (spec 0.29), where `<!foo>` is visible text;
+#    this harness targets 0.31.2 (see harness/conformance/check.py).
+t1() { printf '<!foo>\n# Heading\n\nbody\n' > verified/2026-01-08_decl_v1.md; }
+pos "1 lowercase-decl-is-a-declaration" t1
+# 1b. `<!DOCTYPE html>` IS a declaration block -> skipped -> PASS
+t1b() { printf '<!DOCTYPE html>\n\n# Heading\n\nbody\n' > verified/2026-01-09_doctype_v1.md; }
+pos "1b uppercase-decl-is-a-block" t1b
+
+# 2. `<PRE>` … `</PRE>` closes case-insensitively -> nothing hidden -> PASS
+t2() { both 'LESSON=["<PRE>","raw","</PRE>","","- **After PRE**","'"$L3"']'"$INS"; }
+stray_only "2 uppercase-terminator" "FAIL knowledge:sections" t2 "6 lesson(s)"
+
+# 3. `- **foo***` leaves a star outside the strong span -> malformed
+t3() { both 'LESSON=["- **foo***","'"$L3"']'"$INS"; }
+run "3 surplus-closing-star" "not-a-bold-lesson-bullet" t3
+# 3b. `- ***foo**` likewise -> malformed
+t3b() { both 'LESSON=["- ***foo**","'"$L3"']'"$INS"; }
+run "3b surplus-opening-star" "not-a-bold-lesson-bullet" t3b
+# 3c. an escaped star at the end of the title is fine -> PASS
+t3c() { both 'LESSON=["- **foo\\***","'"$L3"']'"$INS"; }
+pos "3c escaped-star-at-end" t3c
+
+# 4. balanced brackets inside the link label -> valid -> PASS
+t4() { both 'LESSON=["- **Nested label**","  - Why it matters here: x","  - Do: y","  - Source: [RFC [draft]](https://x.y) — A · confidence 80%"]'"$INS"; }
+pos "4 balanced-brackets-in-label" t4
+
+# 5. a lazy continuation at column 0 must not drop the list containers: a fence at 4 spaces after it is still a fence -> fields hidden -> FAIL
+t5() { both 'LESSON=["- **Lazy then fence**","  - Why it matters here: x","lazy","    ```","    - Do: hidden","    - Source: [T](https://x.y) — A · confidence 50%","    ```"]'"$INS"; }
+run "5 containers-survive-lazy-continuation" "missing do" t5
+
+# 6. a type-7 tag right after the H1 (no blank line) starts an HTML block that swallows the sections -> FAIL
+t6() { python3 - <<'PY'
+for p in ('knowledge.md','lessons/2026-08-24.md'):
+    s=open(p).read().split('\n'); assert s[1]=='' ; s[1]='<x>'; open(p,'w').write('\n'.join(s))
+PY
+}
+run "6 type7-after-heading-starts-block" "knowledge:sections" t6
+
+# 7. `##` + NBSP is a paragraph, not a heading -> PASS
+t7() { both 'LESSON=["- **Complete**","'"$L3"',"","## note",""]'"$INS"; }
+stray_only "7 nbsp-after-hashes-not-a-heading" "FAIL knowledge:headings" t7
+
+# 8. text after a multi-line inline comment's `-->` stays in the paragraph: `--> - Do: y` is not a field -> FAIL
+t8() { both 'LESSON=["- **Suffix**","  - Why it matters here: text <!--","-->   - Do: y","'"$F"'"]'"$INS"; }
+run "8 comment-suffix-stays-in-paragraph" "missing do" t8
+
+# 9. a link reference definition before the verified H1 renders nothing -> PASS
+t9() { printf '[docs]: https://example.com\n\n# Heading\n\nbody\n' > verified/2026-01-10_refdef_v1.md; }
+pos "9 link-ref-def-before-h1" t9
+
+# 10. CORRECTED 2026-09-27 (harness v2): `2. more` at column 0 after a lesson is NOT lazy continuation. The
+#     no-interrupt rule for `2.` applies only when a paragraph is the line's direct container; here the line
+#     leaves every list item first, so it opens a new top-level list (commonmark.js 0.31.2: `<ol start="2">`
+#     after the `</ul>`). The round-19 expectation (PASS) was wrong -> a malformed top-level item -> FAIL
+t10() { both 'LESSON=["- **Two dot**","'"$L3"',"2. additional source context"]'"$INS"; }
+run "10 ordered-marker-at-column-0-opens-a-list" "not-a-bold-lesson-bullet" t10
+# 10c. the intent that survives: `2. more` INSIDE the Source item (its content column) cannot interrupt that
+#      paragraph -> lazy continuation of Source (reference: "…confidence 50%\n2. more" in one <li>) -> PASS
+t10c() { both 'LESSON=["- **Two dot inside**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%","    2. additional source context"]'"$INS"; }
+pos "10c ordered-marker-inside-field-is-lazy" t10c
+# 10b. `1. more` in the same spot DOES start a list -> malformed bullet -> FAIL
+t10b() { both 'LESSON=["- **One dot**","'"$L3"',"1. starts a list"]'"$INS"; }
+run "10b ordered-marker-one-interrupts" "not-a-bold-lesson-bullet" t10b
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg20.sh b/harness/tests/neg20.sh
new file mode 100755
index 0000000..89506f8
--- /dev/null
+++ b/harness/tests/neg20.sh
@@ -0,0 +1,68 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+
+# 1. an inline `<!--` with no `-->` before the paragraph ends (blank line) is literal text -> fields stay visible -> PASS
+t1() { both 'LESSON=["- **Literal opener**","  - Why it matters here: text <!--","","  - Do: y","'"$F"'","","-->"]'"$INS"; }
+stray_only "1 unclosed-comment-ends-at-paragraph" "missing do" t1
+
+# 2. an unmatched backtick in Why must not carry into the next list item: the comment on the Do line is real -> Source hidden -> FAIL
+t2() { split 'LESSON=["- **Tick then comment**","  - Why it matters here: x `","  - Do: action <!-- hidden","    hidden too --> tail","'"$F"'"]'"$INS" \
+             'LESSON=["- **Tick then comment**","  - Why it matters here: x `","  - Do: action tail","'"$F"'"]'"$INS"; }
+pos "2 code-span-reset-at-block-start" t2   # the Do line starts a new item, so Why's open backtick is literal and the comment on Do is real → stripped → copies match
+
+# 3. a two-line reference definition (title on the next line) before the H1 -> PASS
+t3() { printf '[docs]: https://example.com\n  "Docs title"\n\n# Heading\n\nbody\n' > verified/2026-01-11_reftitle_v1.md; }
+pos "3 multiline-ref-def-before-h1" t3
+
+# 4. `<div<NBSP>x>` is not an HTML block (NBSP is not tag whitespace) -> visible text before the H1 -> FAIL
+t4() { printf '<div\xc2\xa0x>\n\n# Heading\n\nbody\n' > verified/2026-01-12_nbsp_v1.md; }
+run "4 nbsp-in-tag-is-text" "verified:h1" t4
+
+# 5. a non-ASCII digit in the version suffix -> bad name -> FAIL
+t5() { printf '# Heading\n' > "verified/2026-01-13_topic_v١.md"; }
+run "5 unicode-digit-version" "verified:names" t5
+
+# 6. 2-4 spaces after a nested field marker are valid -> PASS
+t6() { both 'LESSON=["- **Wide field gap**","  -  Why it matters here: x","  -   Do: y","  -    Source: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "6 field-marker-2-4-spaces" t6
+# 6b. 5+ spaces after the marker make the field an indented code block -> not a field -> FAIL
+t6b() { both 'LESSON=["- **Code field**","  - Why it matters here: x","  -     Do: y","'"$F"'"]'"$INS"; }
+run "6b field-marker-5-spaces-is-code" "missing do" t6b
+
+# 7. a blank line before the continuation makes a new paragraph in one copy only -> copies differ -> FAIL
+t7() { python3 -c 'LESSON=["- **Para break**","  - Why it matters here: x","  - Do: y","    more","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **Para break**","  - Why it matters here: x","  - Do: y","","    more","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "7 paragraph-break-differs" "same title, different content" t7
+# 7b. the same paragraph break in both copies -> PASS
+t7b() { both 'LESSON=["- **Para both**","  - Why it matters here: x","  - Do: y","","    more","'"$F"'"]'"$INS"; }
+pos "7b paragraph-break-in-both" t7b
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg21.sh b/harness/tests/neg21.sh
new file mode 100755
index 0000000..be6849a
--- /dev/null
+++ b/harness/tests/neg21.sh
@@ -0,0 +1,61 @@
+#!/usr/bin/env bash
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+# stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
+# `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
+stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
+     && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
+    echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1)); prep "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+F='  - Source: [T](https://x.y) — A · confidence 50%'
+L3='  - Why it matters here: x","  - Do: y","'"$F"'"'
+
+# 1. a malformed definition (unquoted trailing text) is a visible paragraph -> FAIL
+t1() { printf '[docs]: https://example.com extra junk\n\n# Heading\n\nbody\n' > verified/2026-01-14_badref_v1.md; }
+run "1 invalid-ref-def-is-text" "verified:h1" t1
+# 1b. a definition with a quoted title on the same line is valid -> PASS
+t1b() { printf '[docs]: https://example.com "Docs"\n\n# Heading\n\nbody\n' > verified/2026-01-15_okref_v1.md; }
+pos "1b ref-def-with-title" t1b
+
+# 2. a Setext H2 (paragraph + dashes) is a non-date H2 -> FAIL in knowledge.md and in the day file
+t2() { both 'LESSON=["- **Complete**","'"$L3"',"","Unexpected heading","------------------",""]'"$INS"; }
+run "2 setext-h2" "knowledge:headings" t2
+# 2b. dashes after a BLANK line are a thematic break, not a heading -> PASS
+t2b() { both 'LESSON=["- **Complete**","'"$L3"',"","---",""]'"$INS"; }
+stray_only "2b dashes-after-blank-are-a-break" "FAIL knowledge:headings" t2b
+
+# 3. a closing fence followed by NBSP does not close: that line is code, and the block ends with its list
+#    item (commonmark.js 0.31.2: Do/Source render as items). Rewritten 2026-09-27 (round 23) — the old
+#    "missing do" expectation was wrong. knowledge.md has the NBSP closer, the day copy a real one -> the
+#    code blocks differ -> FAIL; a harness that accepted the NBSP closer would see them equal.
+t3() { python3 -c 'LESSON=["- **NBSP closer**","  - Why it matters here: x","    ```","    code","    ```\u00a0","  - Do: y","'"$F"'"]'"$INS" knowledge.md
+       python3 -c 'LESSON=["- **NBSP closer**","  - Why it matters here: x","    ```","    code","    ```","  - Do: y","'"$F"'"]'"$INS" lessons/2026-08-24.md; }
+run "3 nbsp-after-closing-fence" "same title, different content" t3
+
+# 4. visible prose between the H1 and the first day section -> FAIL
+t4() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n'); s[1:1]=['','Some visible prose here.']; open(p,'w').write('\n'.join(s))
+PY
+}
+run "4 prose-before-first-section" "knowledge:misplaced" t4
+
+echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg22.sh b/harness/tests/neg22.sh
new file mode 100755
index 0000000..3d9adb3
--- /dev/null
+++ b/harness/tests/neg22.sh
@@ -0,0 +1,129 @@
+#!/usr/bin/env bash
+# Round-22 negative tests (Codex review of 5319f30). Mutations hit BOTH copies where the
+# structural check — not the copy comparison — must catch them.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+one()  { python3 -c "$1" knowledge.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+SRC_LINE='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# F1 (4115921652): lowercase `<!foo>` is NOT an HTML block — it is lazy continuation text, so a
+#    knowledge.md carrying it must differ from a day copy without it.
+t1() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Source:')][0]   # after the LAST field: nothing follows to trip on
+s.insert(i+1,'<!foo> extra visible context only in knowledge.md')
+open(p,'w').write('\n'.join(s))
+PY
+}
+# CORRECTED 2026-09-27 (harness v2): under CommonMark 0.31.2 `<!foo>` at column 0 starts a declaration block, so
+# this line closes the list and its visible tail becomes a top-level block no day file carries -> knowledge:stray.
+# The mutation is still caught; only the diagnosis changed (GitHub/0.29 reads it as lazy text -> a content diff).
+run "F1 lowercase-declaration-line-is-caught" "knowledge:stray" t1
+# F1+ uppercase `<!FOO>` at column 0 IS an HTML block (type 4): closes the list, later fields are stray → FAIL missing
+t1b() { both 'LESSON=["- **Decl lesson**","  - Why it matters here: x","<!FOO>","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+run "F1+ uppercase-declaration-is-block" "missing do" t1b
+
+# F2 (4115921656): a ref def that already has a title must not swallow the next quoted line
+t2() { printf '[docs]: https://example.com "first"\n"visible second title"\n\n# Real heading\n' > verified/2026-01-04_two-titles_v1.md; }
+run "F2 second-title-is-paragraph" "verified:h1" t2
+# F2+ a ref def WITHOUT a title followed by its title on the next line is still fine
+t2b() { printf '[docs]: https://example.com\n"the title"\n\n# Real heading\n' > verified/2026-01-05_split-title_v1.md; }
+pos "F2+ next-line-title-accepted" t2b
+
+# F3 (4115921662): 8 spaces after a blank = indented code inside the field; 4 spaces = a new paragraph.
+#    Different structures must not compare equal.
+t3() { python3 - <<'PY'
+for p,ind in (('knowledge.md',8),('lessons/2026-08-24.md',4)):
+    s=open(p).read().split('\n')
+    i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Do:')][0]
+    s[i+1:i+1]=['',' '*ind+'trailing text']
+    open(p,'w').write('\n'.join(s))
+PY
+}
+run "F3 indented-code-vs-paragraph" "same title, different content" t3
+# F3+ the same 4-space paragraph in both copies is fine
+t3b() { both 'LESSON=["- **Para lesson**","  - Why it matters here: x","  - Do: y","","    a second paragraph of Do",'"$SRC_LINE"']'"$INS"; }
+pos "F3+ same-paragraph-both-copies" t3b
+
+# F4 (4115921672): a Setext H2 nested in a field (blank, text, dashes at the field's content column)
+t4() { both 'LESSON=["- **Setext lesson**","  - Why it matters here: x","","    Nested heading","    --------------","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+run "F4 nested-setext-h2" "nested inside the lesson" t4
+# F4+ a Setext underline directly under a field label line makes THAT line an H2 too
+t4b() { both 'LESSON=["- **Setext label**","  - Why it matters here: x","    ---","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+run "F4+ setext-under-field-label" "nested inside the lesson" t4b
+
+# F5 (4115921661): `- **alpha\\**beta**` — even backslashes leave the inner ** active
+t5() { both 'LESSON=["- **alpha\\\\**beta**","  - Why it matters here: x","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+run "F5 inner-strong-after-even-backslashes" "not-a-bold-lesson-bullet" t5
+# F5+ `- **alpha\\*\\*beta**` (odd backslashes each) is one bold span → PASS
+t5b() { both 'LESSON=["- **alpha\\*\\*beta**","  - Why it matters here: x","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+pos "F5+ escaped-inner-stars-accepted" t5b
+
+# F6 (4115921665): trailing whitespace on the connector H1 is not the exact line
+t6() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n'); s[0]=s[0]+'  '; open(p,'w').write('\n'.join(s))
+PY
+}
+run "F6 h1-trailing-space" "knowledge:h1" t6
+
+# F7 (4115921669): `<!--` at the end of a field, next line a sibling field, `-->` later:
+#    the list item ends the paragraph, so the opener is literal and both copies are VALID → PASS
+t7() { both 'LESSON=["- **Opener lesson**","  - Why it matters here: x <!--","  - Do: y -->",'"$SRC_LINE"']'"$INS"; }
+pos "F7 opener-literal-across-list-item" t7
+# F7+ but a closer INSIDE the same paragraph (lazy line) still hides the text between
+t7b() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Do:')][0]
+s[i]=s[i]+' <!-- hidden'
+s.insert(i+1,'still hidden --> visible tail only in knowledge.md')
+open(p,'w').write('\n'.join(s))
+PY
+}
+run "F7+ closer-in-same-paragraph" "same title, different content" t7b
+
+# Adjacent (found during the audit): prose after a code block inside a field must count
+t8() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Do:')][0]
+s[i+1:i+1]=['    ```','    code','    ```','    prose after the fence, only in knowledge.md']
+open(p,'w').write('\n'.join(s))
+PY
+}
+run "A1 prose-after-nested-fence-counts" "same title, different content" t8
+# Adjacent: a lesson-level paragraph after the fields (indent between lesson col and field col) must count
+t9() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Source:')][0]
+s[i+1:i+1]=['','  a trailing lesson-level paragraph only in knowledge.md']
+open(p,'w').write('\n'.join(s))
+PY
+}
+run "A2 lesson-tail-paragraph-counts" "same title, different content" t9
+# Adjacent: a nested `## x` deeper than 3 spaces but ≤3 relative to the lesson's content column is still an H2
+t10() { both 'LESSON=["- **Deep heading**","  - Why it matters here: x","","    ## 2026-01-01","  - Do: y",'"$SRC_LINE"']'"$INS"; }
+run "A3 nested-h2-relative-indent" "nested inside the lesson" t10
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg23.sh b/harness/tests/neg23.sh
new file mode 100755
index 0000000..e86de04
--- /dev/null
+++ b/harness/tests/neg23.sh
@@ -0,0 +1,116 @@
+#!/usr/bin/env bash
+# Round-23 negative tests (Codex review of 320a5cc).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+NB=$(printf '\xc2\xa0')
+
+# G1 (4115994843): NBSP after the section date is heading content, not Markdown whitespace
+t1() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]; s[i]=s[i]+' '; open(p,'w').write('\n'.join(s))
+PY
+}
+run "G1 nbsp-after-section-date" "knowledge:headings" t1
+# G1+ NBSP after a day-file H1 is not the exact H1
+t1b() { python3 - <<'PY'
+p='lessons/2026-08-24.md'; s=open(p).read().split('\n'); s[0]=s[0]+' '; open(p,'w').write('\n'.join(s))
+PY
+}
+run "G1+ nbsp-after-day-h1" "first visible line must be" t1b
+# G1++ a NBSP-only line is a paragraph line (lazy continuation), not a blank: knowledge.md carries it, the day copy does not
+t1c() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.lstrip().startswith('- Do:')][0]; s.insert(i+1,' visible nbsp-led lazy line'); open(p,'w').write('\n'.join(s))
+PY
+}
+run "G1++ nbsp-line-is-text" "same title, different content" t1c
+
+# G2 (4115994845): a ref def whose bare destination has an unbalanced `(` is prose, not a definition
+t2() { printf '[docs]: https://example.com/(oops\n\n# Real heading\n' > verified/2026-01-06_unbalanced_v1.md; }
+run "G2 unbalanced-paren-destination" "verified:h1" t2
+# G2+ balanced parens and an escaped paren are fine
+t2b() { printf '[docs]: https://example.com/a_(b)\n[more]: https://example.com/\\(c "t"\n\n# Real heading\n' > verified/2026-01-07_balanced_v1.md; }
+pos "G2+ balanced-and-escaped-parens" t2b
+
+# G3 (4115994849): `\\` before a backtick escapes the backslash, so the backtick opens a code span whose ** is opaque → valid title
+t3() { both 'LESSON=["- **a \\\\`**` b**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "G3 even-backslashes-before-backtick" t3
+# G3+ a single backslash before the backtick escapes the backtick: the ** is live → malformed
+t3b() { both 'LESSON=["- **a \\`**` b**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "G3+ odd-backslash-before-backtick" "not-a-bold-lesson-bullet" t3b
+
+# G4 (4115994853): a raw HTML block nested in a lesson is visible — a copy without it differs
+t4() { split 'LESSON=["- **Html lesson**","  - Why it matters here: x","","    <div>visible only here</div>","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Html lesson**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "G4 nested-html-block-counts" "same title, different content" t4
+# G4+ same block in both copies → PASS
+t4b() { both 'LESSON=["- **Html lesson**","  - Why it matters here: x","","    <div>same in both</div>","  - Do: y",'"$F"']'"$INS"; }
+pos "G4+ nested-html-block-both" t4b
+# G4++ nested fenced code with different content differs too (readers see the code)
+t4c() { split 'LESSON=["- **Code lesson**","  - Why it matters here: x","    ```","    print(1)","    ```","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Code lesson**","  - Why it matters here: x","    ```","    print(2)","    ```","  - Do: y",'"$F"']'"$INS"; }
+run "G4++ nested-code-content-counts" "same title, different content" t4c
+
+# G5 (4115994859): `overconfidence 80%` is not a standalone confidence label
+t5() { both 'LESSON=["- **Conf lesson**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — overconfidence 80%"]'"$INS"; }
+run "G5 overconfidence-not-a-label" "source line is not" t5
+
+# G6 (4115994856): an H2 inside a block quote is still an H2
+t6() { both 'LESSON=["- **Quote lesson**","  - Why it matters here: x","  - Do: y",'"$F"',"","> ## Unexpected"]'"$INS"; }
+run "G6 h2-in-blockquote" "knowledge:headings" t6
+# G6+ nested inside the lesson too
+t6b() { both 'LESSON=["- **Quote nested**","  - Why it matters here: x","","  > ## Unexpected","  - Do: y",'"$F"']'"$INS"; }
+run "G6+ h2-in-blockquote-nested" "nested inside the lesson" t6b
+# G6++ day file only (knowledge.md clean): the per-day no-H2 check must see it
+t6c() { python3 - <<'PY'
+p='lessons/2026-08-24.md'; s=open(p).read().split('\n'); s.append(''); s.append('> > ## Deep quote'); open(p,'w').write('\n'.join(s))
+PY
+}
+run "G6++ h2-in-nested-quote-dayfile" "contains H2 heading" t6c
+
+# G6+++ Setext H2 inside a top-level block quote (`> Foo` / `> ---`)
+t6d() { both 'LESSON=["- **Quote setext**","  - Why it matters here: x","  - Do: y",'"$F"',"","> Foo","> ---"]'"$INS"; }
+run "G6+++ setext-in-blockquote" "knowledge:headings" t6d
+# G6++++ …and nested inside a lesson
+t6e() { both 'LESSON=["- **Quote setext nested**","  - Why it matters here: x","","  > Foo","  > ---","  - Do: y",'"$F"']'"$INS"; }
+run "G6++++ setext-in-blockquote-nested" "nested inside the lesson" t6e
+# G6 negative: a quoted paragraph followed by a DEEPER quoted dash line is not its underline → PASS
+t6f() { both 'LESSON=["- **Quote depth**","  - Why it matters here: x","","  > Foo","  > > ---","  - Do: y",'"$F"']'"$INS"; }
+pos "G6 quote-depth-mismatch-not-setext" t6f
+
+# Container end (found via G4): a nested fence with no closer ends with its list item — the sibling field survives
+t7() { both 'LESSON=["- **Implicit close**","  - Why it matters here: x","    ```","    code without a closer","  - Do: y",'"$F"']'"$INS"; }
+pos "C1 nested-fence-closed-by-container-end" t7
+# …but the same fence at TOP level (base 0) swallows everything to EOF → unclosed FAIL stands
+t7b() { both 'LESSON=["- **Top fence**","  - Why it matters here: x","```","  - Do: y",'"$F"']'"$INS"; }
+run "C1+ top-level-unclosed-fence" "never closes" t7b
+# G1 negatives: NBSP after a marker is not a list item (a lazy paragraph line instead)
+t8() { both 'LESSON=["- **Nbsp marker**","  - Why it matters here: x","  -'"$NB"'Do: y","  - Do: y",'"$F"']'"$INS"; }
+pos "G1+++ nbsp-after-marker-is-text" t8
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg24.sh b/harness/tests/neg24.sh
new file mode 100755
index 0000000..bfa66d1
--- /dev/null
+++ b/harness/tests/neg24.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+# Round-24 negative tests (Codex review of e7edf49).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# H1 (4116065528): a NBSP before an end-of-line inline comment is content; a copy with a plain space differs
+t1() { split 'LESSON=["- **Nbsp comment**","  - Why it matters here: x\u00a0<!-- c -->","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Nbsp comment**","  - Why it matters here: x <!-- c -->","  - Do: y",'"$F"']'"$INS"; }
+run "H1 nbsp-before-eol-comment" "same title, different content" t1
+
+# H2 (4116065538): a 1000-character label is not a reference definition (999 is)
+t2() { python3 -c 'open("verified/2026-01-08_biglabel_v1.md","w").write("[" + "a"*1000 + "]: https://x.y\n\n# Real heading\n")'; }
+run "H2 label-over-999-is-prose" "verified:h1" t2
+t2b() { python3 -c 'open("verified/2026-01-09_maxlabel_v1.md","w").write("[" + "a"*999 + "]: https://x.y\n\n# Real heading\n")'; }
+pos "H2+ label-of-999-accepted" t2b
+
+# H3 (4116065523): raw HTML before a verified sheet's H1 is visible content
+t3() { printf '<div>visible before heading</div>\n\n# Real heading\n' > verified/2026-01-10_htmlfirst_v1.md; }
+run "H3 raw-html-before-h1" "verified:h1" t3
+t3b() { printf '```\ncode first\n```\n\n# Real heading\n' > verified/2026-01-11_codefirst_v1.md; }
+run "H3+ fence-before-h1" "verified:h1" t3b
+t3c() { printf '<!-- comment -->\n\n# Real heading\n' > verified/2026-01-12_commentfirst_v1.md; }
+pos "H3++ comment-before-h1-still-fine" t3c
+
+# H4 (4116065549): `<hgroup>` is NOT a type-6 tag (commonmark.js 0.31.2 list has none); under a title it is
+#    lazy inline HTML that extends the title paragraph past the bold span → the shape check must flag it
+t4() { both 'LESSON=["- **Hgroup**","  <hgroup>","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "H4 hgroup-is-title-continuation" "title paragraph continues" t4
+
+# H5 (4116065545): `\<!-- visible -->` is literal text, not a comment
+#    knowledge.md: `x \<!-- visible -->` (literal); day copy: `x \` — a harness that wrongly strips the escaped
+#    opener leaves `x \` on both sides and PASSes; the correct reading differs
+t5() { split 'LESSON=["- **Escaped opener**","  - Why it matters here: x \\<!-- visible -->","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Escaped opener**","  - Why it matters here: x \\","  - Do: y",'"$F"']'"$INS"; }
+run "H5 escaped-comment-opener-is-text" "same title, different content" t5
+t5b() { split 'LESSON=["- **Escaped backslash opener**","  - Why it matters here: x \\\\<!-- hidden -->","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Escaped backslash opener**","  - Why it matters here: x \\\\","  - Do: y",'"$F"']'"$INS"; }
+pos "H5+ even-backslashes-then-real-comment" t5b
+
+# H6 (4116065532): a thematic break inside a lesson renders as <hr>; a copy without it differs
+t6() { split 'LESSON=["- **Hr lesson**","  - Why it matters here: x","","  ---","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Hr lesson**","  - Why it matters here: x","","  - Do: y",'"$F"']'"$INS"; }
+run "H6 nested-thematic-break-counts" "same title, different content" t6
+t6b() { both 'LESSON=["- **Hr lesson**","  - Why it matters here: x","","  ---","  - Do: y",'"$F"']'"$INS"; }
+pos "H6+ nested-thematic-break-both" t6b
+
+# H7 (4116065540): lazy text after the bold title is outside the strong span → shape problem
+t7() { both 'LESSON=["- **title**","  unbold text","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "H7 prose-after-bold-title" "title paragraph continues" t7
+t7b() { both 'LESSON=["- **title**","unbold lazy at column 0","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "H7+ lazy-prose-after-bold-title" "title paragraph continues" t7b
+
+# H8 (4116065555): `# ##` is an H1 with no content
+t8() { printf '# ##\n\nbody\n' > verified/2026-01-13_emptyh1_v1.md; }
+run "H8 empty-h1-closing-sequence" "verified:h1" t8
+t8b() { printf '# Title ##\n\nbody\n' > verified/2026-01-14_closedh1_v1.md; }
+pos "H8+ h1-with-closing-sequence-accepted" t8b
+t8c() { printf '# \\#\n\nbody\n' > verified/2026-01-15_escapedhash_v1.md; }
+pos "H8++ escaped-hash-is-content" t8c
+
+# H9 (4116065550): an escaped `>` inside an angle-bracket destination does not close it
+t9() { printf '[docs]: <https://example.com/a\\>b>\n\n# Real heading\n' > verified/2026-01-16_anglesc_v1.md; }
+pos "H9 escaped-gt-in-angle-destination" t9
+t9b() { printf '[docs]: <https://example.com/a>b>\n\n# Real heading\n' > verified/2026-01-17_angleunesc_v1.md; }
+run "H9+ unescaped-gt-then-text-is-prose" "verified:h1" t9b
+
+# H10 (4116065552): the day file must not be sanitized twice — a code payload holding `<!-- hidden -->` in BOTH copies is identical
+t10() { both 'LESSON=["- **Payload lesson**","  - Why it matters here: x","    ```","    <!-- hidden --> in code","    ```","  - Do: y",'"$F"']'"$INS"; }
+pos "H10 payload-not-resanitized" t10
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg25.sh b/harness/tests/neg25.sh
new file mode 100755
index 0000000..f2c4919
--- /dev/null
+++ b/harness/tests/neg25.sh
@@ -0,0 +1,90 @@
+#!/usr/bin/env bash
+# Round-25 negative tests (Codex review of 0446523).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# I1 (4116126983): a backtick run with no closer anywhere in the paragraph is literal; the comment after it is real
+t1() { split 'LESSON=["- **Loose tick**","  - Why it matters here: x ` open","    more <!-- hidden --> text","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Loose tick**","  - Why it matters here: x ` open","    more text","  - Do: y",'"$F"']'"$INS"; }
+pos "I1 unmatched-run-is-literal" t1
+# I1+ a run that DOES close on a later line keeps the comment-like text inside as code (visible)
+t1b() { split 'LESSON=["- **Closed tick**","  - Why it matters here: x `open","    <!-- shown --> close` text","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Closed tick**","  - Why it matters here: x `open","    close` text","  - Do: y",'"$F"']'"$INS"; }
+run "I1+ closer-ahead-keeps-span" "same title, different content" t1b
+
+# I2 (4116126973): content indentation is measured from the fence's own indentation
+t2() { split 'LESSON=["- **Fence indent**","  - Why it matters here: x","      ```","        code","      ```","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Fence indent**","  - Why it matters here: x","       ```","        code","       ```","  - Do: y",'"$F"']'"$INS"; }
+run "I2 fence-indent-changes-content" "same title, different content" t2
+t2b() { both 'LESSON=["- **Fence indent**","  - Why it matters here: x","      ```","        code","      ```","  - Do: y",'"$F"']'"$INS"; }
+pos "I2+ same-fence-indent-both" t2b
+
+# I3 (4116126977): indented code blocks inside a lesson are compared
+t3() { split 'LESSON=["- **Indented code**","  - Why it matters here: x","","        alpha","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Indented code**","  - Why it matters here: x","","        beta","  - Do: y",'"$F"']'"$INS"; }
+run "I3 indented-code-payload-differs" "same title, different content" t3
+t3b() { both 'LESSON=["- **Indented code**","  - Why it matters here: x","","        alpha","","        gamma","  - Do: y",'"$F"']'"$INS"; }
+pos "I3+ same-indented-code-both" t3b
+t3c() { split 'LESSON=["- **Indented code blank**","  - Why it matters here: x","","        alpha","","        gamma","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Indented code blank**","  - Why it matters here: x","","        alpha","        gamma","  - Do: y",'"$F"']'"$INS"; }
+run "I3++ blank-line-inside-code-counts" "same title, different content" t3c
+
+# I4 (4116126989): visible top-level content inside a day section that is not a lesson
+t4() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]; s[i+1:i+1]=['','Visible only in knowledge.']; open(p,'w').write('\n'.join(s))
+PY
+}
+run "I4 stray-paragraph-in-section" "knowledge:stray" t4
+t4b() { python3 - <<'PY'
+p='knowledge.md'; s=open(p).read().split('\n')
+i=[k for k,l in enumerate(s) if l.startswith('## ')][0]; s[i+1:i+1]=['','### Sub-heading']; open(p,'w').write('\n'.join(s))
+PY
+}
+run "I4+ stray-h3-in-section" "knowledge:stray" t4b
+t4c() { python3 - <<'PY'
+p='lessons/2026-08-24.md'; s=open(p).read().split('\n'); s[2:2]=['','A paragraph only in the day file.']; open(p,'w').write('\n'.join(s))
+PY
+}
+run "I4++ stray-paragraph-in-dayfile" "lessons:2026-08-24" t4c
+t4d() { both 'LESSON=["- **Comment ok**","  - Why it matters here: x","  - Do: y",'"$F"',"","<!-- a top-level comment between lessons renders nothing -->"]'"$INS"; }
+pos "I4+++ top-level-comment-not-stray" t4d
+
+# I5 (4116126996): field order is part of the comparison
+t5() { split 'LESSON=["- **Order**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Order**","  - Do: y","  - Why it matters here: x",'"$F"']'"$INS"; }
+run "I5 field-order-differs" "same title, different content" t5
+
+# I6 (4116127003): a fenced block inside a block quote inside a lesson is a fence (quote marker removed first)
+t6() { split 'LESSON=["- **Quoted fence**","  - Why it matters here: x","","  > ```","  > <!-- alpha -->","  > ```","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Quoted fence**","  - Why it matters here: x","","  > ```","  > <!-- beta -->","  > ```","  - Do: y",'"$F"']'"$INS"; }
+run "I6 quoted-fence-payload-differs" "same title, different content" t6
+t6b() { both 'LESSON=["- **Quoted fence**","  - Why it matters here: x","","  > ```","  > <!-- alpha -->","  > ```","  - Do: y",'"$F"']'"$INS"; }
+pos "I6+ same-quoted-fence-both" t6b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg26.sh b/harness/tests/neg26.sh
new file mode 100755
index 0000000..44e068c
--- /dev/null
+++ b/harness/tests/neg26.sh
@@ -0,0 +1,56 @@
+#!/usr/bin/env bash
+# Round-26 negative tests (Codex review of 03bb96e).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# J1 (4116180182): a code span that opens on one field line and closes on the next keeps its inner spaces
+t1() { split 'LESSON=["- **Span lines**","  - Why it matters here: `a  x","    b` tail","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Span lines**","  - Why it matters here: `a   x","    b` tail","  - Do: y",'"$F"']'"$INS"; }
+run "J1 cross-line-code-span-spaces-differ" "same title, different content" t1
+t1b() { both 'LESSON=["- **Span lines**","  - Why it matters here: `a  x","    b` tail","  - Do: y",'"$F"']'"$INS"; }
+pos "J1+ cross-line-code-span-same" t1b
+# J1++ prose (outside any span) still collapses: two vs three spaces in plain text compare equal
+t1c() { split 'LESSON=["- **Prose spaces**","  - Why it matters here: a  x","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Prose spaces**","  - Why it matters here: a   x","  - Do: y",'"$F"']'"$INS"; }
+pos "J1++ prose-spaces-still-collapse" t1c
+
+# J2 (4116180191): the opener's 0-3 spaces of indentation are not payload
+t2() { split 'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>same</div>","  - Do: y",'"$F"']'"$INS" \
+             'LESSON=["- **Html indent**","  - Why it matters here: x","","      <div>same</div>","  - Do: y",'"$F"']'"$INS"; }
+pos "J2 html-opener-indent-ignored" t2
+t2b() { split 'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>same</div>","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>other</div>","  - Do: y",'"$F"']'"$INS"; }
+run "J2+ html-content-still-compared" "same title, different content" t2b
+
+# J3 (4116180187): a two-line reference definition inside a day section renders nothing
+t3() { both 'LESSON=["- **Ref def**","  - Why it matters here: x","  - Do: y",'"$F"',"","[docs]: https://example.com","  \"the title\""]'"$INS"; }
+pos "J3 two-line-ref-def-in-section" t3
+t3b() { both 'LESSON=["- **Ref def**","  - Why it matters here: x","  - Do: y",'"$F"',"","[docs]: https://example.com \"t\"","  \"visible second\""]'"$INS"; }
+run "J3+ second-title-is-stray" "knowledge:stray" t3b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg27.sh b/harness/tests/neg27.sh
new file mode 100755
index 0000000..b60212e
--- /dev/null
+++ b/harness/tests/neg27.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+# Round-27 negative tests (Codex review of a0fa435).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d)
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# K1 (4116219332): text after a comment block's closer on the same line is visible raw HTML
+t1() { printf '<!-- hidden -->VISIBLE BEFORE H1\n\n# Real heading\n' > verified/2026-01-18_tail_v1.md; }
+run "K1 text-after-comment-closer-before-h1" "verified:h1" t1
+t1b() { printf '<!-- hidden\nstill hidden -->TAIL\n\n# Real heading\n' > verified/2026-01-19_tail2_v1.md; }
+run "K1+ multiline-comment-tail-before-h1" "verified:h1" t1b
+t1c() { split 'LESSON=["- **Tail lesson**","  - Why it matters here: x","","  <!-- c -->shown","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Tail lesson**","  - Why it matters here: x","","  <!-- c -->","  - Do: y",'"$F"']'"$INS"; }
+run "K1++ comment-tail-in-lesson-counts" "same title, different content" t1c
+t1d() { printf '<!-- hidden -->   \n\n# Real heading\n' > verified/2026-01-20_tailws_v1.md; }
+pos "K1+++ whitespace-after-closer-still-invisible" t1d
+
+# K2 (4116219333): a complete type-7 tag as the FIRST content of a child item opens a raw HTML block
+t2() { both 'LESSON=["- **Sub lesson**","  - <sub>","    - Why it matters here: x","    - Do: y","",'"$F"']'"$INS"; }
+run "K2 type7-first-in-item-swallows-fields" "missing why" t2
+# K2+ the same hole for a fence as the first content of an item: `- ```` opens a fence inside the item
+t2b() { split 'LESSON=["- **Fence first**","  - ```","    hidden one","    ```","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS" \
+              'LESSON=["- **Fence first**","  - ```","    hidden two","    ```","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "K2+ fence-first-in-item-is-a-fence" "same title, different content" t2b
+
+# K3 (4116219327): reference-style links resolve through definitions that must match between copies
+t3() { split 'LESSON=["- **Ref link**","  - Why it matters here: see [docs][ref]","  - Do: y",'"$F"',"","[ref]: https://knowledge.example"]'"$INS" \
+             'LESSON=["- **Ref link**","  - Why it matters here: see [docs][ref]","  - Do: y",'"$F"',"","[ref]: https://daily.example"]'"$INS"; }
+run "K3 ref-definition-differs" "same title, different content" t3
+t3b() { split 'LESSON=["- **Ref link**","  - Why it matters here: see [docs][ref]","  - Do: y",'"$F"',"","[ref]: https://knowledge.example"]'"$INS" \
+              'LESSON=["- **Ref link**","  - Why it matters here: see [docs][ref]","  - Do: y",'"$F"']'"$INS"; }
+run "K3+ ref-definition-missing-in-one-copy" "same title, different content" t3b
+t3c() { both 'LESSON=["- **Ref link**","  - Why it matters here: see [docs][REF]","  - Do: y",'"$F"',"","[ref]: https://knowledge.example \"t\""]'"$INS"; }
+pos "K3++ same-definition-case-insensitive" t3c
+
+# K4 (4116219329): a confidence hidden in an HTML attribute is not a visible label
+t4() { both 'LESSON=["- **Attr conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A <br title=\"confidence 80%\">"]'"$INS"; }
+run "K4 confidence-in-attribute" "source line is not" t4
+t4b() { both 'LESSON=["- **Tag then conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A <br> confidence 80%"]'"$INS"; }
+pos "K4+ visible-confidence-after-tag" t4b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/run.sh b/harness/tests/run.sh
new file mode 100755
index 0000000..63e595b
--- /dev/null
+++ b/harness/tests/run.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+# Run every negative-test suite in this directory against the checker in this repo.
+# Each suite copies the repo to a temp dir, applies one mutation per case, and asserts the checker's
+# verdict. Prints one RESULT line per suite and a TOTAL line; exits non-zero if any case fails.
+set -u
+cd "$(dirname "${BASH_SOURCE[0]}")"
+total=0; failed=0; suites=0
+for s in neg*.sh; do
+  out=$(bash "./$s" 2>&1); rc=$?
+  line=$(printf '%s\n' "$out" | grep -E '^RESULT' | tail -1)
+  n=$(printf '%s' "$line" | grep -oE '\(([0-9]+) cases\)' | grep -oE '[0-9]+'); f=$(printf '%s' "$line" | grep -oE '[0-9]+ fail' | grep -oE '[0-9]+')
+  printf '%-10s %s\n' "${s%.sh}" "${line:-RESULT: FAIL — suite did not report (rc=$rc)}"
+  printf '%s\n' "$out" | grep -E '^FAIL' | sed 's/^/    /'
+  total=$((total + ${n:-0})); failed=$((failed + ${f:-1})); suites=$((suites + 1))
+  [ "$rc" -ne 0 ] && [ "${f:-0}" -eq 0 ] && failed=$((failed + 1))
+done
+echo "----"
+echo "TOTAL: $([ $failed -eq 0 ] && echo PASS || echo FAIL) — $suites suites, $total cases, $failed failing"
+[ $failed -eq 0 ]
diff --git a/verified/2026-09-26_claude-surfaces_v2.md b/verified/2026-09-26_claude-surfaces_v2.md
index d222881..266cadc 100644
--- a/verified/2026-09-26_claude-surfaces_v2.md
+++ b/verified/2026-09-26_claude-surfaces_v2.md
@@ -1,5 +1,7 @@
 # Verified: Claude surfaces — cloud sessions from the chat/Cowork tab vs the Code tab (2026-09-26, corrected 2026-09-27)
 
+> **Superseded by [v3](2026-09-26_claude-surfaces_v3.md) (2026-09-27).** v3 records the rename of the proof repository to `cloud-launch-pad`; nothing else changed.
+
 Source: live tool runs in cloud session <https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK> — opened from the **chat/Cowork tab**, not the Code tab (T's correction, 2026-09-27; v1 of this sheet mislabelled it) — plus the current Claude Code docs (claude-code-on-the-web, routines, claude-projects, remote-control) and the Cowork scheduled-tasks help article. Not a daily.dev lesson — a session-verified fact sheet.
 
 ## Core finding
diff --git a/verified/2026-09-26_claude-surfaces_v3.md b/verified/2026-09-26_claude-surfaces_v3.md
new file mode 100644
index 0000000..30ba476
--- /dev/null
+++ b/verified/2026-09-26_claude-surfaces_v3.md
@@ -0,0 +1,45 @@
+# Verified: Claude surfaces — cloud sessions from the chat/Cowork tab vs the Code tab (2026-09-26, corrected and renamed 2026-09-27)
+
+Source: live tool runs in cloud session <https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK> — opened from the **chat/Cowork tab**, not the Code tab (T's correction, 2026-09-27; v1 of this sheet mislabelled it) — plus the current Claude Code docs (claude-code-on-the-web, routines, claude-projects, remote-control) and the Cowork scheduled-tasks help article. Not a daily.dev lesson — a session-verified fact sheet.
+
+## Core finding
+
+"Chat/Cowork is file-out, Code tab is repo-in/repo-out" is false. It is one cloud harness (Claude Code Remote) with many front doors: the chat/Cowork tab, claude.ai/code, mobile Code tab, desktop app "Cloud", `claude --cloud`, routines, Claude Tag. Pick the door by what the session must be handed at birth: GitHub push credential + PR tooling → cloud session; a folder on the PC → Cowork/desktop. Routines, connectors, sub-agents and artifacts travel with all of them.
+
+## Verified live (cloud session opened from the chat/Cowork tab)
+
+- `add_repo(push)` → shallow clone → signed commit → `git push`: native. Credentials never enter the VM; Anthropic's GitHub proxy injects them.
+- PR creation via `POST api.github.com/repos/<o>/<r>/pulls` works with the injected credential but REQUIRES `Content-Type: application/json` (proxy returns HTTP 415 without it). `gh` CLI is not installed in the cloud VM.
+- Opening a PR auto-subscribes the session (`subscription.created` ~1 s later) under a drive-to-green contract: CI red, merge conflicts and non-optional review findings are work now; never skip/disable a test to get green; re-request reviewers after pushing.
+- Scheduled tasks: create / list / delete from a cloud session work. Run-once tasks don't count against the daily routine cap. `send_later` re-arms same-session check-ins.
+- Parallel sub-agents run concurrently (two agents, overlapping timestamps).
+- A cloud session reaches the linked PC through the device bridge (get_device_info → win32 "my-damn-pc", desktop app 2.9939.2; local MCP servers Filesystem, memory, local-kb-search, sequential-thinking, Windows-MCP).
+- The Cowork-created scheduled task (2026-08-24) is listed from this session → one routine store.
+- Native push from the chat side: VERIFIED — every push, PR and scheduled-task call in this sheet came from a session opened in the chat/Cowork tab (tab per T's statement; the git/PR/task results are tool-verified). The Cowork session of 2026-08-24 had used a pushbot workaround (this repo, cfd032a; daily-dev-connector with CI); the workaround is not required.
+
+## From the docs (fetched 2026-09-26)
+
+- Remote Control connects claude.ai to a session running on your machine — a local feature viewed remotely. Teleport pulls a cloud session into a local terminal (clean git state, same repo checkout).
+- Routines: created from web, desktop app or CLI, all into one cloud account. Desktop "Local" routine = Desktop scheduled task on the machine. Cowork scheduled tasks "can't be tied to a folder on your computer".
+- Code-tab Projects (parallel-thread coordinator): public beta on Pro/Max, rolling out first to accounts with cloud-session history and no existing chat/Cowork projects → eligibility for this account UNVERIFIED.
+- Auto-fix PRs requires the Claude GitHub App installed on the repo.
+
+## Gotchas
+
+1. After a shallow `add_repo` clone, a pushed branch has no `origin/<branch>` ref (single-branch fetch refspec); the stop-hook falsely reports "unpushed commits / no remote branch". Fix: `git config --add remote.origin.fetch '+refs/heads/X:refs/remotes/origin/X' && git fetch origin X && git branch -u origin/X`.
+2. The PC's `memory` knowledge-graph MCP fails from the cloud side (outputSchema declares JSON-Schema draft-07; the cloud validator supports 2020-12 only) — read and write both blocked as of 2026-09-26.
+3. The daily-dev-agentic routine reports SUCCEEDED every day, yet this repo has no lesson commits after 2026-08-24: the run exits cleanly on the "connector not attached" branch of its prompt. Green run status ≠ task success.
+
+## Bots seen on the repos
+
+GitGuardian Security Checks · Kilo Code Review (glm-5.3) · Sourcery (no access on private repos; upsell comment only).
+
+## Correction trail
+
+- v1 (commit `ef0c3ac`) labelled the session "Code tab" because T tapped **Code** on the where-does-this-chat-live tile by accident. He corrected it on 2026-09-27; v1 is kept with a superseded banner, this v2 carries the fix. The core finding is unchanged and now stronger: the chat/Cowork-tab session did everything natively.
+
+- v3 (2026-09-27): the proof repository was renamed to `cloud-launch-pad` at T's request; only the Proof artifact line changed.
+
+## Proof artifact
+
+lookathim010-source/cloud-launch-pad (named Claude-Code-Android-App-FULL-ACCESS-ENV-Anthropic-cloud until 2026-09-27; GitHub redirects the old URL) — branch `claude-chat-capability-proof`, commit 9ee5237, PR #1 (merged by T).

From d2be01d5c4bdb5e83c75f69d7af914944b81b7a4 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 18:02:24 -0400
Subject: [PATCH 02/13] harness: round-28 review fixes (offsets after <pre>,
 invisible-only fields, comments in labels, --json without deps, split
 confidence word)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Five Codex findings on 78d12a9, each reproduced first (harness/tests/neg28.sh
failed 8 of 17 cases before this change, passes 17 of 17 after):

- visible_html() shortened the string that PROTECT_RE.finditer was walking,
  so after a <pre> every later code span was sliced at stale offsets and the
  text right after it dropped out of the comparison key (`y`A == `y`B).
  The whitespace after </pre> is now stripped from the next segment instead.
- A field whose only content is invisible (`Why it matters here: <!-- x -->`,
  or a comment block under `Do:`) counted as non-empty. Emptiness now comes
  from the visible text after the label and from visible blocks only.
- `Why<!-- note --> it matters here:` renders as the label but was not
  recognised. Labels now match the paragraph's leading plain text with
  invisible raw HTML skipped; code, images or other markup still break it.
- `--json` printed text lines when markdown-it-py was missing. The deps error
  now honours the mode, and a parser that fails to load for any reason
  (incompatible release) is reported the same way.
- confi`code`dence 80% was read as a confidence label because code spans were
  dropped from the text after the link. Visible non-text content (code,
  images, displayed raw HTML) now splits words; formatting delimiters and
  invisible markup stay zero-width.

Reference first: six new cases in harness/conformance/cases.json pin the
parses these tests rely on (commonmark.js 0.31.2 agrees on all six).

Also: shellcheck + pyflakes CI job (.shellcheckrc documents the two
exclusions; run.sh gets `cd … || exit 1`), `raw = state.src` spacing in
knowledge_md.py, and the LEDGER row's line counts corrected (the helper
module is 52 lines, not 37).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 .github/workflows/lint.yml     | 13 +++++
 .shellcheckrc                  |  5 ++
 LEDGER.md                      |  2 +-
 README.md                      |  6 +--
 harness/check-knowledge.py     | 94 +++++++++++++++++++++++++---------
 harness/conformance/cases.json | 32 +++++++++++-
 harness/knowledge_md.py        |  2 +-
 harness/tests/README.md        | 10 ++--
 harness/tests/neg28.sh         | 88 +++++++++++++++++++++++++++++++
 harness/tests/run.sh           |  2 +-
 10 files changed, 218 insertions(+), 36 deletions(-)
 create mode 100644 .shellcheckrc
 create mode 100644 harness/tests/neg28.sh

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 0602b8e..891ba76 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -20,6 +20,19 @@ jobs:
           node-version: 22
       - run: npx --yes markdownlint-cli2@0.23.3 "**/*.md" "#node_modules"
 
+  code:
+    name: shellcheck + pyflakes
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: "3.12"
+      # shellcheck is preinstalled on ubuntu-latest runners; .shellcheckrc holds the two reviewed exclusions
+      - run: shellcheck --version | head -2 && shellcheck -s bash harness/tests/*.sh
+      # pyflakes rules only (unused names, undefined names, bad imports); flake8 honors the `noqa` markers
+      - run: pip install flake8==7.3.0 && python3 -m flake8 --select=F harness
+
   knowledge:
     name: check-knowledge.py
     runs-on: ubuntu-latest
diff --git a/.shellcheckrc b/.shellcheckrc
new file mode 100644
index 0000000..ceed376
--- /dev/null
+++ b/.shellcheckrc
@@ -0,0 +1,5 @@
+# shellcheck settings for harness/tests/*.sh (CI runs shellcheck at its default severity).
+# SC2016: the suites pass Python and Markdown in single quotes on purpose; nothing there may expand.
+disable=SC2016
+# SC2001: `sed 's/^/    /'` indents every line of multi-line output; parameter expansion cannot anchor per line.
+disable=SC2001
diff --git a/LEDGER.md b/LEDGER.md
index 81350bf..14f4bae 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 551 lines + 37-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 36 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 597 lines + 52-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 42 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases, 218 total) and a shellcheck + pyflakes CI job |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index b3aee5e..1858c16 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 201 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 36 edge cases like the reference implementation | Sessions |
+| `harness/tests/` | 218 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 42 edge cases like the reference implementation | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 201 negative-test cases
+bash harness/tests/run.sh                   # the 218 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 60188de..9fd800c 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -43,10 +43,17 @@
 sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
 try:
     from knowledge_md import MD                       # markdown-it-py, CommonMark preset, conformance-patched
-except ImportError:                                   # the contract still holds: a FAIL line, a RESULT line, exit 1
-    print("FAIL harness:deps             markdown-it-py is not installed — run: pip install -r harness/requirements.txt")
-    print("----")
-    print("RESULT: FAIL — 0 pass, 0 warn, 1 fail")
+except Exception as exc:                              # missing, or a release whose internals the patches no longer fit
+    _why = ("markdown-it-py is not installed" if isinstance(exc, ImportError) and "markdown_it" in str(exc)
+            else f"markdown-it-py failed to load ({type(exc).__name__}: {exc})")
+    _detail = f"{_why} — run: pip install -r harness/requirements.txt"
+    if "--json" in sys.argv:                          # the contract holds in both modes: one JSON object, or lines
+        print(json.dumps({"ok": False, "pass": 0, "warn": 0, "fail": 1,
+                          "checks": [{"status": "FAIL", "name": "harness:deps", "detail": _detail}]}))
+    else:
+        print(f"FAIL harness:deps             {_detail}")
+        print("----")
+        print("RESULT: FAIL — 0 pass, 0 warn, 1 fail")
     sys.exit(1)
 
 ROOT = pathlib.Path(__file__).resolve().parents[1]
@@ -63,6 +70,7 @@
 CONFIDENCE_RE = re.compile(r"(?<![A-Za-z0-9_])confidence ([0-9]{1,3})%(?![0-9%])")   # a standalone label only
 MARKER_RE = re.compile(r"^[ \t]*(?:[-*+]|[0-9]{1,9}[.)])[ \t]*")
 WS = " \t\n\r\f"                                    # HTML/CommonMark whitespace; a NBSP is content
+SEP = "\ufffc"   # stands in for visible non-text inline content (code, image, displayed raw HTML): it splits words
 
 # Raw HTML a browser never displays. Each alternative also swallows an unterminated
 # opener to the end, because an unclosed comment hides everything after it.
@@ -197,15 +205,16 @@ def visible_html(html: str) -> str:
     whitespace collapsed outside <pre>/<code>, and dropped next to block boundaries."""
     html = INVISIBLE_RE.sub("", html)
     parts: list[str] = []
-    i = 0
-    for m in PROTECT_RE.finditer(html):
-        seg = collapse(html[i:m.start()])
-        parts.append(seg.rstrip(WS) if m.group(0)[:4].lower() == "<pre" else seg)
+    i, after_pre = 0, False
+    for m in PROTECT_RE.finditer(html):               # offsets index `html`, so it is never modified here
+        seg = html[i:m.start()]
+        seg = collapse(seg.lstrip(WS) if after_pre else seg)
+        after_pre = m.group(0)[:4].lower() == "<pre"
+        parts.append(seg.rstrip(WS) if after_pre else seg)
         parts.append(m.group(0))
         i = m.end()
-        if m.group(0)[:4].lower() == "<pre":
-            html = html[:i] + html[i:].lstrip(WS)
-    parts.append(collapse(html[i:]))
+    tail = html[i:]
+    parts.append(collapse(tail.lstrip(WS) if after_pre else tail))
     return "".join(parts).strip(WS)
 
 
@@ -232,23 +241,57 @@ def inline_tokens(inline) -> list:
     return [c for c in inline.children if not (c.type == "text" and c.content == "")]
 
 
+def invisible_inline(c) -> bool:
+    """An inline raw-HTML token a browser never displays (a comment, PI, declaration, CDATA)."""
+    return c.type == "html_inline" and not INVISIBLE_RE.sub("", c.content).strip(WS)
+
+
+def visible_text(tokens) -> str:
+    """The text a reader sees in a run of inline tokens. Formatting delimiters (emphasis, link
+    brackets) and invisible raw HTML are zero-width; line breaks read as a space; other visible
+    content (code, an image, raw HTML that displays) becomes SEP, so it splits the words around it."""
+    out = []
+    for c in tokens:
+        if c.type == "text":
+            out.append(c.content)
+        elif c.type in ("softbreak", "hardbreak"):
+            out.append(" ")
+        elif c.type in ("code_inline", "image") or (c.type == "html_inline" and not invisible_inline(c)):
+            out.append(SEP)
+    return "".join(out)
+
+
+def lead_text(tokens) -> str:
+    """The paragraph's leading plain text (invisible raw HTML skipped): where a field label must sit."""
+    out = []
+    for c in tokens:
+        if c.type == "text":
+            out.append(c.content)
+        elif not invisible_inline(c):
+            break
+    return "".join(out)
+
+
 def parse_source(inline) -> int | None:
     """Confidence N for a Source field rendering as `Source: <link to http(s)> … confidence N%`,
     else None. Only visible text after the link counts — never link text, code or HTML attributes."""
-    all_ch = inline_tokens(inline)
-    ch = [c for c in all_ch if c.type != "softbreak"]
-    if not ch or ch[0].type != "text" or ch[0].content[len("Source:"):].strip(" \t"):
+    toks = [c for c in inline_tokens(inline) if not invisible_inline(c)]
+    k, lead = 0, ""
+    while k < len(toks) and toks[k].type == "text":
+        lead += toks[k].content
+        k += 1
+    if not lead.startswith("Source:") or lead[len("Source:"):].strip(" \t"):
         return None
-    if len(ch) < 2 or ch[1].type != "link_open" or ch[1].markup == "autolink":
+    while k < len(toks) and toks[k].type == "softbreak":
+        k += 1
+    if k >= len(toks) or toks[k].type != "link_open" or toks[k].markup == "autolink":
         return None
-    if not re.match(r"https?://[^ \t]+$", ch[1].attrGet("href") or ""):
+    if not re.match(r"https?://[^ \t]+$", toks[k].attrGet("href") or ""):
         return None
-    k = next((i for i in range(2, len(ch)) if ch[i].type == "link_close"), None)
-    if k is None:
+    close = next((j for j in range(k + 1, len(toks)) if toks[j].type == "link_close"), None)
+    if close is None:
         return None
-    tail = "".join(c.content if c.type == "text" else " " for c in all_ch[all_ch.index(ch[k]) + 1:]
-                   if c.type in ("text", "softbreak", "hardbreak"))
-    m = CONFIDENCE_RE.search(tail)
+    m = CONFIDENCE_RE.search(visible_text(toks[close + 1:]))
     return int(m.group(1)) if m else None
 
 
@@ -299,7 +342,9 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
             continue
         for field in block.kids:
             fk = field.kids
-            m = FIELD_RE.match(fk[0].kids[0].t.content) if fk and fk[0].kind == "paragraph" else None
+            para = fk[0].kids[0].t if fk and fk[0].kind == "paragraph" else None
+            toks = inline_tokens(para) if para else []
+            m = FIELD_RE.match(lead_text(toks)) if para else None
             if not m:
                 continue                          # a sub-bullet that is not a field: compared, not counted
             key = LABELS[m.group(1)]
@@ -308,9 +353,10 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
             if lesson["seen"][key]:
                 lesson["problems"].append(f"duplicate {key}")
             lesson["seen"][key] = True
-            empty = not fk[0].kids[0].t.content[m.end():].strip(WS) and len(fk) == 1
+            # empty = nothing a reader sees after the label: not in the paragraph, not in a later block
+            empty = not visible_text(toks)[m.end():].strip(WS) and not any(visible(b) for b in fk[1:])
             if key == "source":
-                conf = None if empty else parse_source(fk[0].kids[0].t)
+                conf = None if empty else parse_source(para)
                 if conf is None:
                     lesson["problems"].append("source line is not `[title](https://…) … confidence N%`")
                 elif conf > 100:
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 6d9f4b4..3db8390 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -178,5 +178,35 @@
   "id": "lowercase-decl-after-field",
   "markdown": "- **T**\n  - Do: y\n<!foo> extra\n",
   "why": "a lowercase declaration interrupts and closes the list"
+ },
+ {
+  "id": "pre-then-code-span",
+  "markdown": "- **T**\n  - Why it matters here: x\n\n    ```\n    x\n    ```\n\n  - Do: `y`A\n",
+  "why": "a fence inside a field item, then an inline code span in the next field"
+ },
+ {
+  "id": "comment-only-field",
+  "markdown": "- **T**\n  - Why it matters here: <!-- hidden -->\n  - Do: y\n",
+  "why": "a field whose only content is an inline comment"
+ },
+ {
+  "id": "comment-block-only-field",
+  "markdown": "- **T**\n  - Do:\n\n    <!-- hidden block -->\n\n  - Source: s\n",
+  "why": "a field whose only content is a comment block"
+ },
+ {
+  "id": "comment-inside-label",
+  "markdown": "- **T**\n  - Why<!-- note --> it matters here: x\n",
+  "why": "an inline comment inside a field label"
+ },
+ {
+  "id": "code-splits-confidence",
+  "markdown": "Source: [T](https://x.y) — A · confi`code`dence 80%\n",
+  "why": "a code span inside the confidence word"
+ },
+ {
+  "id": "image-splits-confidence",
+  "markdown": "Source: [T](https://x.y) — A · confi![i](https://x.y/i.png)dence 80%\n",
+  "why": "an image inside the confidence word"
  }
-]
\ No newline at end of file
+]
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index 2e32269..63a0f49 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -25,7 +25,7 @@ def _reference_999(state, start: int, end: int, silent: bool) -> bool:
     n_tokens = len(state.tokens)
     if not _reference(state, start, end, silent):
         return False
-    raw =state.src[state.bMarks[start] + state.tShift[start]:state.eMarks[state.line - 1]]
+    raw = state.src[state.bMarks[start] + state.tShift[start]:state.eMarks[state.line - 1]]
     m = re.match(r"\[((?:\\.|[^\\\[\]])*)\]:", raw, re.S)
     if m and len(m.group(1)) > 999:
         state.env["references"] = before
diff --git a/harness/tests/README.md b/harness/tests/README.md
index 9431162..e5c4621 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -7,11 +7,11 @@ structural check, not the copy comparison, has to catch it), runs
 containing a given string, `pos` expects PASS, and `stray_only` expects a
 `knowledge:stray` FAIL without the wrong diagnosis.
 
-The suite numbers follow the review rounds that produced them (round 11 to
-round 27 of the review on PR #1). Cases marked `CORRECTED 2026-09-27` encoded a
-CommonMark reading that the reference implementation (commonmark.js 0.31.2)
-proved wrong. Each correction keeps a replacement case for the intent that
-survives.
+The suite numbers follow the review rounds that produced them (rounds 11 to
+27 of the review on PR #1, round 28 on PR #2). Cases marked
+`CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
+implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
+replacement case for the intent that survives.
 
 ```bash
 pip install -r harness/requirements.txt
diff --git a/harness/tests/neg28.sh b/harness/tests/neg28.sh
new file mode 100644
index 0000000..832b63b
--- /dev/null
+++ b/harness/tests/neg28.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# Round-28 negative tests (Codex review of 78d12a9, harness v2 on PR #2).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# L1 (4117111332): whitespace dropped after a <pre> must not shift the offsets of later code spans.
+#     The copies differ only in the character right after an inline code span that follows a code block.
+t1() { split 'LESSON=["- **Pre then code**","  - Why it matters here: x","","    ```","    x","    ```","","  - Do: `y`A",'"$F"']'"$INS" \
+             'LESSON=["- **Pre then code**","  - Why it matters here: x","","    ```","    x","    ```","","  - Do: `y`B",'"$F"']'"$INS"; }
+run "L1 text-after-code-after-pre-is-compared" "same title, different content" t1
+t1b() { split 'LESSON=["- **Pre code code**","  - Why it matters here: x","","    ```","    x","    ```","","  - Do: `y` then `z`A tail",'"$F"']'"$INS" \
+              'LESSON=["- **Pre code code**","  - Why it matters here: x","","    ```","    x","    ```","","  - Do: `y` then `z`B tail",'"$F"']'"$INS"; }
+run "L1+ two-code-spans-after-pre" "same title, different content" t1b
+t1c() { both 'LESSON=["- **Pre then code**","  - Why it matters here: x","","    ```","    x","    ```","","  - Do: `y`A",'"$F"']'"$INS"; }
+pos "L1++ identical-copies-still-pass" t1c
+
+# L2 (4117111335): a field whose only content is invisible is empty.
+t2() { both 'LESSON=["- **Hidden why**","  - Why it matters here: <!-- hidden -->","  - Do: y",'"$F"']'"$INS"; }
+run "L2 comment-only-why-is-empty" "missing or empty why" t2
+t2b() { both 'LESSON=["- **Hidden do**","  - Why it matters here: x","  - Do:","","    <!-- hidden block -->","",'"$F"']'"$INS"; }
+run "L2+ invisible-block-only-do-is-empty" "missing or empty do" t2b
+t2c() { both 'LESSON=["- **Code why**","  - Why it matters here: `x`","  - Do: <!-- c --> real",'"$F"']'"$INS"; }
+pos "L2++ code-or-text-beside-a-comment-is-content" t2c
+t2d() { both 'LESSON=["- **Hidden source**","  - Why it matters here: x","  - Do: y","  - Source: <!-- hidden -->"]'"$INS"; }
+run "L2+++ comment-only-source" "source line is not" t2d
+
+# L3 (4117111338): an invisible comment inside a field label does not hide the field.
+t3() { both 'LESSON=["- **Label comment**","  - Why<!-- note --> it matters here: x","  - Do<!-- n -->: y","  - Source<!-- n -->: [T](https://x.y) — A · confidence 50%"]'"$INS"; }
+pos "L3 comment-inside-labels-is-invisible" t3
+t3b() { both 'LESSON=["- **Label code**","  - Why `it` matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "L3+ code-inside-a-label-is-not-the-label" "missing why" t3b
+
+# L4 (4117111343): --json keeps its one-JSON-object contract when markdown-it-py is missing.
+n=$((n+1))
+rm -rf "$W/r" "$W/stub"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mkdir -p "$W/stub/markdown_it"
+echo 'raise ImportError("stub: markdown-it-py hidden by neg28 L4")' > "$W/stub/markdown_it/__init__.py"
+out=$(cd "$W/r" && PYTHONPATH="$W/stub" python3 harness/check-knowledge.py --json 2>&1); rc=$?
+if [ $rc -eq 1 ] && printf '%s' "$out" | python3 -c '
+import json, sys
+d = json.loads(sys.stdin.read())
+assert d["ok"] is False and d["fail"] == 1 and d["checks"][0]["name"] == "harness:deps", d
+'; then echo "PASS L4 json-mode-without-deps — one JSON object, ok=false, rc=1"
+else bad=$((bad+1)); echo "FAIL L4 json-mode-without-deps — rc=$rc, output:"; echo "$out" | sed 's/^/    /'; fi
+n=$((n+1))
+out=$(cd "$W/r" && PYTHONPATH="$W/stub" python3 harness/check-knowledge.py 2>&1); rc=$?
+if [ $rc -eq 1 ] && printf '%s\n' "$out" | grep -q '^FAIL harness:deps' && printf '%s\n' "$out" | grep -q '^RESULT: FAIL'; then
+  echo "PASS L4+ text-mode-without-deps — FAIL line + RESULT line, rc=1"
+else bad=$((bad+1)); echo "FAIL L4+ text-mode-without-deps — rc=$rc, output:"; echo "$out" | sed 's/^/    /'; fi
+
+# L5 (4117111346): visible non-text content splits words; only formatting and invisible markup are zero-width.
+t5() { both 'LESSON=["- **Split conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confi`code`dence 80%"]'"$INS"; }
+run "L5 code-span-splits-the-label" "source line is not" t5
+t5b() { both 'LESSON=["- **Img conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confi![i](https://x.y/i.png)dence 80%"]'"$INS"; }
+run "L5+ image-splits-the-label" "source line is not" t5b
+t5c() { both 'LESSON=["- **Tag conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confidence<br>80%"]'"$INS"; }
+run "L5++ raw-tag-splits-label-and-number" "source line is not" t5c
+t5d() { both 'LESSON=["- **Code conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · `confidence 80%`"]'"$INS"; }
+run "L5+++ label-inside-code-is-not-a-label" "source line is not" t5d
+t5e() { both 'LESSON=["- **Bold conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · **confidence 80%**"]'"$INS"; }
+pos "L5++++ formatting-is-zero-width" t5e
+t5f() { both 'LESSON=["- **Comment conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A · confi<!-- x -->dence 80%"]'"$INS"; }
+pos "L5+++++ comment-inside-label-is-invisible" t5f
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]
diff --git a/harness/tests/run.sh b/harness/tests/run.sh
index 63e595b..465bdec 100755
--- a/harness/tests/run.sh
+++ b/harness/tests/run.sh
@@ -3,7 +3,7 @@
 # Each suite copies the repo to a temp dir, applies one mutation per case, and asserts the checker's
 # verdict. Prints one RESULT line per suite and a TOTAL line; exits non-zero if any case fails.
 set -u
-cd "$(dirname "${BASH_SOURCE[0]}")"
+cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1
 total=0; failed=0; suites=0
 for s in neg*.sh; do
   out=$(bash "./$s" 2>&1); rc=$?

From 9eda1478622f7d4d25bbcc45f782b3688ab1e996 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 18:19:28 -0400
Subject: [PATCH 03/13] harness: round-29 review fixes (whitespace-keeping raw
 HTML, empty source-link text, silent mutation failures, `<?>` closing) +
 browser-accurate invisible markup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Second Codex review of 78d12a9; every finding reproduced first by
harness/tests/neg29.sh (13 of 20 cases failed on d2be01d, 20 of 20 pass now):

- <textarea>a  b</textarea> vs a   b compared equal. A lesson holding raw
  HTML that shows whitespace as written (textarea, xmp, listing, plaintext)
  or can (a style element or attribute) is now compared exactly; Chromium
  141 reports white-space pre / pre-wrap for all of these.
- Source: [](https://…) was accepted. The source link now needs visible link
  text (code or an image counts; whitespace or a comment does not).
- A `pos` case whose mutation failed ran against the untouched repo and
  passed. Every suite now applies mutations through mutate(): the mutation
  runs under set -e, and on any failure the copy's checker is replaced by a
  stub printing MUTATION DID NOT APPLY, so the case cannot pass. The full run
  after this change confirms all 218 earlier mutations were applying. Temp
  dirs are now removed on exit in every suite.
- `<?>` (and `<!-->`) as an HTML block was reported as never closing:
  unclosed() searched past the opener. CommonMark tests the whole line.
- verified/…_v3.md called itself "this v2".

Found while fixing `<?>`: the invisible-markup pattern ended PIs, CDATA and
declarations at `?>` / `]]>` and comments only at `-->`, while a browser
ends PIs/CDATA/declarations ("bogus comments") at the first `>`, comments
also at `--!>`, and treats `<!-->` / `<!--->` as complete. Text after those
points is visible (Chromium 141 textContent), so copies differing there
compared equal. INVISIBLE_RE now follows the tokenizer; neg29 N6 cases.

Reference first: eight new cases in harness/conformance/cases.json pin the
parses (commonmark.js 0.31.2 agrees on all eight; 652/652 spec examples).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                                 |   2 +-
 README.md                                 |   6 +-
 harness/check-knowledge.py                |  19 ++--
 harness/conformance/cases.json            |  40 ++++++++
 harness/tests/README.md                   |   6 +-
 harness/tests/neg11.sh                    |   8 +-
 harness/tests/neg12.sh                    |  10 +-
 harness/tests/neg13.sh                    |  14 ++-
 harness/tests/neg14.sh                    |  10 +-
 harness/tests/neg15.sh                    |  10 +-
 harness/tests/neg16.sh                    |   8 +-
 harness/tests/neg17.sh                    |   8 +-
 harness/tests/neg18.sh                    |  10 +-
 harness/tests/neg19.sh                    |  10 +-
 harness/tests/neg20.sh                    |  10 +-
 harness/tests/neg21.sh                    |  10 +-
 harness/tests/neg22.sh                    |  10 +-
 harness/tests/neg23.sh                    |  10 +-
 harness/tests/neg24.sh                    |  10 +-
 harness/tests/neg25.sh                    |  10 +-
 harness/tests/neg26.sh                    |  10 +-
 harness/tests/neg27.sh                    |  10 +-
 harness/tests/neg28.sh                    |   8 +-
 harness/tests/neg29.sh                    | 110 ++++++++++++++++++++++
 verified/2026-09-26_claude-surfaces_v3.md |   2 +-
 25 files changed, 296 insertions(+), 65 deletions(-)
 create mode 100644 harness/tests/neg29.sh

diff --git a/LEDGER.md b/LEDGER.md
index 14f4bae..ba3e3af 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 597 lines + 52-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 42 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases, 218 total) and a shellcheck + pyflakes CI job |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 604 lines + 52-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 50 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases, 238 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 1858c16..ff33673 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 218 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 42 edge cases like the reference implementation | Sessions |
+| `harness/tests/` | 238 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 50 edge cases like the reference implementation | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 218 negative-test cases
+bash harness/tests/run.sh                   # the 238 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 9fd800c..c5816e1 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -72,10 +72,15 @@
 WS = " \t\n\r\f"                                    # HTML/CommonMark whitespace; a NBSP is content
 SEP = "\ufffc"   # stands in for visible non-text inline content (code, image, displayed raw HTML): it splits words
 
-# Raw HTML a browser never displays. Each alternative also swallows an unterminated
-# opener to the end, because an unclosed comment hides everything after it.
-INVISIBLE_RE = re.compile(
-    r"<!--.*?(?:-->|\Z)|<\?.*?(?:\?>|\Z)|<!\[CDATA\[.*?(?:\]\]>|\Z)|<![A-Za-z][^>]*(?:>|\Z)", re.S)
+# Raw HTML a browser never displays, ended where a browser's HTML tokenizer ends it (checked in
+# Chromium 141): a comment at `-->` or `--!>` (`<!-->` and `<!--->` are complete, empty comments);
+# a processing instruction, declaration or CDATA section, all "bogus comments" in HTML content, at
+# the first `>`. An unterminated one hides everything to the end.
+INVISIBLE_RE = re.compile(r"<!--(?:-?>|.*?(?:--!?>|\Z))|<[!?][^>]*(?:>|\Z)", re.S)
+# Raw HTML that shows whitespace as written (textarea, xmp, listing, plaintext render as pre /
+# pre-wrap) or can make it so (a style element or attribute): a lesson holding any of it is
+# compared without whitespace normalization.
+WS_KEEP_RE = re.compile(r"<(?:textarea|xmp|listing|plaintext|style)\b|\bstyle\s*=", re.I)
 PROTECT_RE = re.compile(r"<pre\b.*?</pre>|<code\b.*?</code>", re.S | re.I)   # whitespace inside code is content
 BLOCK_TAG_RE = re.compile(
     r"[ \t\n\r\f]*(</?(?:address|article|aside|blockquote|br|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|"
@@ -184,7 +189,7 @@ def unclosed(self) -> list[str]:
                     m = opener.match(c)
                     if m:
                         end = end or f"</{m.group(1).lower()}>"
-                        if end not in c.lower()[2:]:
+                        if end not in c.lower():  # CommonMark tests the whole line: `<?>` and `<!-->` close at once
                             out.append("HTML comment never closes" if end == "-->" else f"HTML block never closes (no {end!r})")
                         break
         return out
@@ -204,6 +209,8 @@ def visible_html(html: str) -> str:
     """Rendered HTML reduced to what a reader can tell apart: invisible markup removed,
     whitespace collapsed outside <pre>/<code>, and dropped next to block boundaries."""
     html = INVISIBLE_RE.sub("", html)
+    if WS_KEEP_RE.search(html):                       # whitespace may be visible anywhere in it: compare exactly
+        return html.strip(WS)
     parts: list[str] = []
     i, after_pre = 0, False
     for m in PROTECT_RE.finditer(html):               # offsets index `html`, so it is never modified here
@@ -289,7 +296,7 @@ def parse_source(inline) -> int | None:
     if not re.match(r"https?://[^ \t]+$", toks[k].attrGet("href") or ""):
         return None
     close = next((j for j in range(k + 1, len(toks)) if toks[j].type == "link_close"), None)
-    if close is None:
+    if close is None or not visible_text(toks[k + 1:close]).strip(WS):   # the link needs a visible title
         return None
     m = CONFIDENCE_RE.search(visible_text(toks[close + 1:]))
     return int(m.group(1)) if m else None
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 3db8390..f882211 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -208,5 +208,45 @@
   "id": "image-splits-confidence",
   "markdown": "Source: [T](https://x.y) — A · confi![i](https://x.y/i.png)dence 80%\n",
   "why": "an image inside the confidence word"
+ },
+ {
+  "id": "empty-comment-inline",
+  "markdown": "y <!-->A\n",
+  "why": "`<!-->` is a complete inline comment (0.31.2); the text after it is prose"
+ },
+ {
+  "id": "empty-comment-block",
+  "markdown": "<!-->\ntext\n",
+  "why": "a type-2 HTML block that closes on its own line"
+ },
+ {
+  "id": "pi-block-minimal",
+  "markdown": "<?>\ntext\n",
+  "why": "a type-3 HTML block whose `?>` overlaps its opener closes on its own line"
+ },
+ {
+  "id": "cdata-with-gt-inline",
+  "markdown": "y <![CDATA[ a > b ]]>\n",
+  "why": "inline CDATA containing `>` is one raw HTML span"
+ },
+ {
+  "id": "comment-bang-close-inline",
+  "markdown": "y <!-- a --!>A -->\n",
+  "why": "CommonMark ends the inline comment at `-->`, past a `--!>`"
+ },
+ {
+  "id": "textarea-block",
+  "markdown": "<textarea>\na  b\n</textarea>\n",
+  "why": "a type-1 HTML block keeps its inner whitespace verbatim"
+ },
+ {
+  "id": "empty-link-label",
+  "markdown": "Source: [](https://x.y) — A\n",
+  "why": "an empty link text is still a link"
+ },
+ {
+  "id": "whitespace-link-label",
+  "markdown": "[ ](https://x.y)\n",
+  "why": "a whitespace-only link text is still a link"
  }
 ]
diff --git a/harness/tests/README.md b/harness/tests/README.md
index e5c4621..aa3f49d 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -5,10 +5,12 @@ mutation per case (usually to both `knowledge.md` and the day file, so the
 structural check, not the copy comparison, has to catch it), runs
 `harness/check-knowledge.py`, and asserts the verdict. `run` expects a FAIL
 containing a given string, `pos` expects PASS, and `stray_only` expects a
-`knowledge:stray` FAIL without the wrong diagnosis.
+`knowledge:stray` FAIL without the wrong diagnosis. A mutation runs under
+`set -e`; if any step of it fails, the copy's checker is replaced by a stub
+that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, round 28 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 and 29 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives.
diff --git a/harness/tests/neg11.sh b/harness/tests/neg11.sh
index 1311e44..1b36479 100755
--- a/harness/tests/neg11.sh
+++ b/harness/tests/neg11.sh
@@ -3,12 +3,16 @@
 # expects the named check to FAIL (or the named behaviour). Prints PASS/FAIL per case.
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 run() { # name expected-substring-in-output mutation-fn
   local name="$1" expect="$2"; n=$((n+1))
   rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
-  ( cd "$W/r" && "$3" )
+  mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
diff --git a/harness/tests/neg12.sh b/harness/tests/neg12.sh
index cc65e4c..f3c612f 100755
--- a/harness/tests/neg12.sh
+++ b/harness/tests/neg12.sh
@@ -3,7 +3,11 @@
 # so the copy-comparison cannot be what catches it; the structural check must.
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { # python snippet that mutates one file given as sys.argv[1]
   for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done
@@ -11,7 +15,7 @@ both() { # python snippet that mutates one file given as sys.argv[1]
 run() { # name expected mutation-fn
   local name="$1" expect="$2"; n=$((n+1))
   rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
-  ( cd "$W/r" && "$3" )
+  mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
@@ -22,7 +26,7 @@ run() { # name expected mutation-fn
 pos() { # name mutation-fn : must stay PASS
   local name="$1"; n=$((n+1))
   rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
-  ( cd "$W/r" && "$2" )
+  mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi
 }
diff --git a/harness/tests/neg13.sh b/harness/tests/neg13.sh
index 3453d6e..d4a9e85 100755
--- a/harness/tests/neg13.sh
+++ b/harness/tests/neg13.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,20 +13,20 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-run() { local name="$1" expect="$2"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+run() { local name="$1" expect="$2"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
     echo "PASS $name — stray fired, no '$absent'${present:+, saw '$present'}"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, wanted knowledge:stray without '$absent'${present:+ and with '$present'}:"; echo "$out" | sed 's/^/    /'; fi; }
-pos() { local name="$1"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+pos() { local name="$1"; n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 
@@ -48,6 +52,6 @@ t5() { python3 -c 'LESSON=["- **Leak check**","  - Why it matters here: x","
 run "5 fence-in-one-copy-differs" "same title, different content" t5
 t5b() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c 'LESSON=["- **Leak check**","  - Why it matters here: x","    ```","    secret","    ```","  - Do: y","  - Source: [T](https://x.y) — A · confidence 50%"]'"$INS" "$p"; done; }
 pos "5b fence-in-both-copies-identical" t5b
-n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && t5 )
+n=$((n+1)); rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate t5
 if (cd "$W/r" && python3 harness/check-knowledge.py 2>&1) | grep -qaP '\x00'; then bad=$((bad+1)); echo "FAIL 5c raw sentinel leaked into a diagnostic"; else echo "PASS 5c no raw sentinel in diagnostics"; fi
 echo "----"; echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"; rm -rf "$W"; [ $bad -eq 0 ]
diff --git a/harness/tests/neg14.sh b/harness/tests/neg14.sh
index e3fd03f..d9f4f3b 100755
--- a/harness/tests/neg14.sh
+++ b/harness/tests/neg14.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,7 +13,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -17,7 +21,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg15.sh b/harness/tests/neg15.sh
index 783ff29..e189a1e 100755
--- a/harness/tests/neg15.sh
+++ b/harness/tests/neg15.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,7 +13,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -17,7 +21,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg16.sh b/harness/tests/neg16.sh
index 94bac03..0fc0875 100755
--- a/harness/tests/neg16.sh
+++ b/harness/tests/neg16.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
@@ -10,7 +14,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
diff --git a/harness/tests/neg17.sh b/harness/tests/neg17.sh
index 0b1acb3..fb981b2 100755
--- a/harness/tests/neg17.sh
+++ b/harness/tests/neg17.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,7 +13,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
diff --git a/harness/tests/neg18.sh b/harness/tests/neg18.sh
index 503e370..bef95c6 100755
--- a/harness/tests/neg18.sh
+++ b/harness/tests/neg18.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
@@ -10,7 +14,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -18,7 +22,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg19.sh b/harness/tests/neg19.sh
index 9a42e85..6e0c4b1 100755
--- a/harness/tests/neg19.sh
+++ b/harness/tests/neg19.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,7 +13,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -17,7 +21,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg20.sh b/harness/tests/neg20.sh
index 89506f8..b2d4cd3 100755
--- a/harness/tests/neg20.sh
+++ b/harness/tests/neg20.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }   # different copies (round-22 rewrite)
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
@@ -10,7 +14,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -18,7 +22,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg21.sh b/harness/tests/neg21.sh
index be6849a..20a2a35 100755
--- a/harness/tests/neg21.sh
+++ b/harness/tests/neg21.sh
@@ -1,6 +1,10 @@
 #!/usr/bin/env bash
 set -u
-SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); n=0; bad=0
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"; W=$(mktemp -d); trap 'rm -rf "$W"' EXIT; n=0; bad=0
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 INS='
 import sys
@@ -9,7 +13,7 @@ i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lesso
 s[i+1:i+1]=[""]+LESSON
 open(p,"w").write("\n".join(s))
 '
-prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$1" ); }
+prep() { rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$1"; }
 run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-150)"
@@ -17,7 +21,7 @@ run() { local name="$1" expect="$2"; n=$((n+1)); prep "$3"
 # stray_only: the mutation plants visible top-level content inside a day section — since round 25 that is a
 # `knowledge:stray` FAIL — so the test asserts that stray fires AND that the WRONG diagnosis stays absent.
 stray_only() { local name="$1" absent="$2" present="${4:-}"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "knowledge:stray" && ! printf '%s' "$out" | grep -qF -- "$absent" \
      && { [ -z "$present" ] || printf '%s' "$out" | grep -qF -- "$present"; }; then
diff --git a/harness/tests/neg22.sh b/harness/tests/neg22.sh
index 3d9adb3..cdae3e5 100755
--- a/harness/tests/neg22.sh
+++ b/harness/tests/neg22.sh
@@ -3,18 +3,22 @@
 # structural check — not the copy comparison — must catch them.
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 one()  { python3 -c "$1" knowledge.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg23.sh b/harness/tests/neg23.sh
index e86de04..d095f51 100755
--- a/harness/tests/neg23.sh
+++ b/harness/tests/neg23.sh
@@ -2,18 +2,22 @@
 # Round-23 negative tests (Codex review of 320a5cc).
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg24.sh b/harness/tests/neg24.sh
index bfa66d1..97c355c 100755
--- a/harness/tests/neg24.sh
+++ b/harness/tests/neg24.sh
@@ -2,18 +2,22 @@
 # Round-24 negative tests (Codex review of e7edf49).
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg25.sh b/harness/tests/neg25.sh
index f2c4919..c293374 100755
--- a/harness/tests/neg25.sh
+++ b/harness/tests/neg25.sh
@@ -2,18 +2,22 @@
 # Round-25 negative tests (Codex review of 0446523).
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg26.sh b/harness/tests/neg26.sh
index 44e068c..713aede 100755
--- a/harness/tests/neg26.sh
+++ b/harness/tests/neg26.sh
@@ -2,18 +2,22 @@
 # Round-26 negative tests (Codex review of 03bb96e).
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg27.sh b/harness/tests/neg27.sh
index b60212e..836e949 100755
--- a/harness/tests/neg27.sh
+++ b/harness/tests/neg27.sh
@@ -2,18 +2,22 @@
 # Round-27 negative tests (Codex review of a0fa435).
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
-W=$(mktemp -d)
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg28.sh b/harness/tests/neg28.sh
index 832b63b..3b538d5 100644
--- a/harness/tests/neg28.sh
+++ b/harness/tests/neg28.sh
@@ -3,17 +3,21 @@
 set -u
 SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
 W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
 n=0; bad=0
 both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
 split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
 run() { local name="$1" expect="$2"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$3" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
     echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
   else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
 pos() { local name="$1"; n=$((n+1))
-  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; ( cd "$W/r" && "$2" )
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
   out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
   if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
 INS='
diff --git a/harness/tests/neg29.sh b/harness/tests/neg29.sh
new file mode 100644
index 0000000..fcfa94f
--- /dev/null
+++ b/harness/tests/neg29.sh
@@ -0,0 +1,110 @@
+#!/usr/bin/env bash
+# Round-29 negative tests (second Codex review of 78d12a9, harness v2 on PR #2).
+# Browser behaviour in N5/N6/N1 was checked in Chromium 141 (textContent / computed white-space).
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# N1 (4117161110): raw HTML that keeps whitespace visible (textarea, xmp, listing, a white-space style) is compared exactly
+t1() { split 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a  b</textarea>",'"$F"']'"$INS" \
+             'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a   b</textarea>",'"$F"']'"$INS"; }
+run "N1 textarea-whitespace-is-visible" "same title, different content" t1
+t1b() { split 'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: <span style=\"white-space:pre\">a  b</span>",'"$F"']'"$INS" \
+              'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: <span style=\"white-space:pre\">a b</span>",'"$F"']'"$INS"; }
+run "N1+ white-space-style-is-visible" "same title, different content" t1b
+t1c() { split 'LESSON=["- **Block textarea**","  - Why it matters here: x","  - Do: y","","    <textarea>","    a  b","    </textarea>","",'"$F"']'"$INS" \
+              'LESSON=["- **Block textarea**","  - Why it matters here: x","  - Do: y","","    <textarea>","    a   b","    </textarea>","",'"$F"']'"$INS"; }
+run "N1++ textarea-block-whitespace-is-visible" "same title, different content" t1c
+t1d() { both 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a  b</textarea>",'"$F"']'"$INS"; }
+pos "N1+++ identical-textarea-copies-pass" t1d
+
+# N3 (4117161124): a source link needs visible link text
+t3() { both 'LESSON=["- **Empty label**","  - Why it matters here: x","  - Do: y","  - Source: [](https://x.y) — A · confidence 80%"]'"$INS"; }
+run "N3 empty-link-label" "source line is not" t3
+t3b() { both 'LESSON=["- **Blank label**","  - Why it matters here: x","  - Do: y","  - Source: [ ](https://x.y) — A · confidence 80%"]'"$INS"; }
+run "N3+ whitespace-link-label" "source line is not" t3b
+t3c() { both 'LESSON=["- **Hidden label**","  - Why it matters here: x","  - Do: y","  - Source: [<!-- c -->](https://x.y) — A · confidence 80%"]'"$INS"; }
+run "N3++ invisible-link-label" "source line is not" t3c
+t3d() { both 'LESSON=["- **Code label**","  - Why it matters here: x","  - Do: y","  - Source: [`api`](https://x.y) — A · confidence 80%"]'"$INS"; }
+pos "N3+++ code-link-label-is-visible" t3d
+t3e() { both 'LESSON=["- **Image label**","  - Why it matters here: x","  - Do: y","  - Source: [![logo](https://x.y/l.png)](https://x.y) — A · confidence 80%"]'"$INS"; }
+pos "N3++++ image-link-label-is-visible" t3e
+
+# N4 (4117161130): a case whose mutation fails must be reported as a failure, never as PASS
+tbad() { python3 -c 'import sys; next(k for k in [] if k)' knowledge.md; }
+probe=$(pos "probe-pos" tbad 2>&1); n=$((n+1))
+if printf '%s' "$probe" | grep -q '^FAIL probe-pos'; then echo "PASS N4 pos-with-failed-mutation-is-reported"
+else bad=$((bad+1)); echo "FAIL N4 pos-with-failed-mutation-is-reported — got:"; echo "$probe" | sed 's/^/    /'; fi
+probe=$(run "probe-run" "MUTATION DID NOT APPLY" tbad 2>&1); n=$((n+1))
+if printf '%s' "$probe" | grep -q '^PASS probe-run'; then echo "PASS N4+ failed-mutation-names-itself"
+else bad=$((bad+1)); echo "FAIL N4+ failed-mutation-names-itself — got:"; echo "$probe" | sed 's/^/    /'; fi
+tbad2() { both 'raise SystemExit(1)'; }
+probe=$(pos "probe-both" tbad2 2>&1); n=$((n+1))
+if printf '%s' "$probe" | grep -q '^FAIL probe-both'; then echo "PASS N4++ first-of-two-mutations-failing-is-reported"
+else bad=$((bad+1)); echo "FAIL N4++ first-of-two-mutations-failing-is-reported — got:"; echo "$probe" | sed 's/^/    /'; fi
+
+# N5 (4117161134): a minimal processing instruction or empty comment closes on its own line
+t5() { both 'import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=["","<?>"]
+open(p,"w").write("\n".join(s))'; }
+pos "N5 minimal-processing-instruction-closes" t5
+t5b() { both 'import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=["","<!-->"]
+open(p,"w").write("\n".join(s))'; }
+pos "N5+ empty-comment-closes" t5b
+t5c() { both 'import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=["","<?"]
+open(p,"w").write("\n".join(s))'; }
+run "N5++ open-processing-instruction-never-closes" "HTML block never closes" t5c
+
+# N6 (found while fixing N5): invisible raw HTML ends where a browser ends it, not later
+t6() { split 'LESSON=["- **Abrupt comment**","  - Why it matters here: x","  - Do: y <!-->A",'"$F"']'"$INS" \
+             'LESSON=["- **Abrupt comment**","  - Why it matters here: x","  - Do: y <!-->B",'"$F"']'"$INS"; }
+run "N6 text-after-empty-comment-is-visible" "same title, different content" t6
+t6b() { split 'LESSON=["- **Minimal PI**","  - Why it matters here: x","  - Do: y <?>A",'"$F"']'"$INS" \
+              'LESSON=["- **Minimal PI**","  - Why it matters here: x","  - Do: y <?>B",'"$F"']'"$INS"; }
+run "N6+ text-after-minimal-pi-is-visible" "same title, different content" t6b
+t6c() { split 'LESSON=["- **CDATA gt**","  - Why it matters here: x","  - Do: y <![CDATA[ a > b ]]>",'"$F"']'"$INS" \
+              'LESSON=["- **CDATA gt**","  - Why it matters here: x","  - Do: y <![CDATA[ a > c ]]>",'"$F"']'"$INS"; }
+run "N6++ cdata-ends-at-first-gt" "same title, different content" t6c
+t6d() { split 'LESSON=["- **Bang close**","  - Why it matters here: x","  - Do: y <!-- a --!>A -->",'"$F"']'"$INS" \
+              'LESSON=["- **Bang close**","  - Why it matters here: x","  - Do: y <!-- a --!>B -->",'"$F"']'"$INS"; }
+run "N6+++ comment-ends-at-bang-close" "same title, different content" t6d
+t6e() { split 'LESSON=["- **Comment gt**","  - Why it matters here: x","  - Do: y <!-- a > b -->",'"$F"']'"$INS" \
+              'LESSON=["- **Comment gt**","  - Why it matters here: x","  - Do: y <!-- a > c -->",'"$F"']'"$INS"; }
+pos "N6++++ comment-text-stays-hidden" t6e
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]
diff --git a/verified/2026-09-26_claude-surfaces_v3.md b/verified/2026-09-26_claude-surfaces_v3.md
index 30ba476..555d953 100644
--- a/verified/2026-09-26_claude-surfaces_v3.md
+++ b/verified/2026-09-26_claude-surfaces_v3.md
@@ -36,7 +36,7 @@ GitGuardian Security Checks · Kilo Code Review (glm-5.3) · Sourcery (no access
 
 ## Correction trail
 
-- v1 (commit `ef0c3ac`) labelled the session "Code tab" because T tapped **Code** on the where-does-this-chat-live tile by accident. He corrected it on 2026-09-27; v1 is kept with a superseded banner, this v2 carries the fix. The core finding is unchanged and now stronger: the chat/Cowork-tab session did everything natively.
+- v1 (commit `ef0c3ac`) labelled the session "Code tab" because T tapped **Code** on the where-does-this-chat-live tile by accident. He corrected it on 2026-09-27; v1 is kept with a superseded banner, and v2 carried the fix. The core finding is unchanged and now stronger: the chat/Cowork-tab session did everything natively.
 
 - v3 (2026-09-27): the proof repository was renamed to `cloud-launch-pad` at T's request; only the Proof artifact line changed.
 

From 2fb93c2b709584f4b6f82ee619a805851bcd7cc5 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 18:44:52 -0400
Subject: [PATCH 04/13] =?UTF-8?q?harness:=20round-30=20review=20fixes=20?=
 =?UTF-8?q?=E2=80=94=20browser-faithful=20HTML=20tokenizer=20(html5lib-che?=
 =?UTF-8?q?cked)=20and=20raw=20HTML=20never=20counts=20as=20a=20value?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex review of d2be01d; reproduced first by harness/tests/neg30.sh (11 of 14
cases failed on 9eda147; 15 of 15 pass now):

- Text inside hidden raw HTML (`<span hidden>confidence 80%</span>`, a
  `<template>`, a raw `<div hidden>` block) satisfied Why / Do / Source and
  the confidence label. Raw HTML can hide what it wraps (hidden, template,
  CSS), so text inside a raw HTML element — or a raw HTML block — no longer
  counts as a field value, a source link title or a confidence label.
  Text beside raw HTML still counts (P1++++).
- Invisible markup around the title: a comment AFTER or INSIDE `**Title**`
  is now ignored (P2+, P2+++). A comment BEFORE it cannot be ignored: an item
  whose content starts with `<!--` opens an HTML block, so the title is raw
  text shown with its asterisks (commonmark.js 0.31.2 agrees; pinned as
  `comment-before-title`). P2 asserts that FAIL.
- `<input value="<!-- A -->">` vs `"<!-- B -->"`, comment-like text in a
  textarea, and whitespace or tag-like text inside attribute values were
  removed or normalized by regexes that did not know HTML context. The
  checker now tokenizes rendered HTML the way a browser does
  (knowledge_md.html_segments: WHATWG tag / attribute / raw-text / comment
  / bogus-comment states) and only removes what that tokenizer marks as
  never displayed; tags, attributes and raw text stay byte-exact, and
  whitespace is collapsed only in text segments.

Verification of the tokenizer: harness/conformance/html_oracle.py drives
html5lib's WHATWG tokenizer and compares token streams on a 28-case corpus
plus seeded fuzz — 100,000 fragments in CI, 500,000 locally, 0 divergences.
Chromium 141 (DOMParser) agrees on 18,452 of 18,452 fuzzed fragments that
removing what html_segments hides leaves the DOM unchanged apart from
comments. CI's conformance job now installs html5lib 1.1 and runs the oracle.

Round-29 cases N1/N1+/N1+++ now put plain text beside their raw HTML (under
the new rule a textarea-only Do: is empty). Six new reference cases in
cases.json (56 total, 652/652 spec examples).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 .github/workflows/lint.yml          |   4 +-
 LEDGER.md                           |   2 +-
 README.md                           |  16 ++--
 harness/check-knowledge.py          | 122 ++++++++++++++++------------
 harness/conformance/cases.json      |  30 +++++++
 harness/conformance/html_cases.json |  30 +++++++
 harness/conformance/html_oracle.py  |  86 ++++++++++++++++++++
 harness/knowledge_md.py             | 105 +++++++++++++++++++++++-
 harness/tests/README.md             |   6 +-
 harness/tests/neg29.sh              |  13 +--
 harness/tests/neg30.sh              |  79 ++++++++++++++++++
 11 files changed, 424 insertions(+), 69 deletions(-)
 create mode 100644 harness/conformance/html_cases.json
 create mode 100644 harness/conformance/html_oracle.py
 create mode 100644 harness/tests/neg30.sh

diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 891ba76..aac4d86 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -66,7 +66,9 @@ jobs:
       - uses: actions/setup-node@v4
         with:
           node-version: 22
-      - run: pip install -r harness/requirements.txt
+      - run: pip install -r harness/requirements.txt html5lib==1.1
+      # The HTML tokenizer the checker uses to find never-displayed markup, against html5lib (WHATWG).
+      - run: python3 harness/conformance/html_oracle.py
       # The reference implementation and the official spec examples, pinned to CommonMark 0.31.2.
       - run: npm install --no-save --no-audit --no-fund --prefix "$RUNNER_TEMP/ref" commonmark@0.31.2 commonmark-spec@0.31.2
       - run: node harness/conformance/reference.cjs "$RUNNER_TEMP/ref/node_modules" > "$RUNNER_TEMP/ref.json"
diff --git a/LEDGER.md b/LEDGER.md
index ba3e3af..361cde8 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 604 lines + 52-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 50 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases, 238 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 622 lines + 155-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 56 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (15 cases, 253 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index ff33673..78bceaf 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 238 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gate: the parser must render all 652 CommonMark spec examples and 50 edge cases like the reference implementation | Sessions |
+| `harness/tests/` | 253 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 56 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,14 +64,18 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 238 negative-test cases
+bash harness/tests/run.sh                   # the 253 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
 rather than pattern matching. Two copies of a lesson count as the same when they
-render to the same visible HTML. CI runs the checker, the test suites,
-markdownlint and a conformance gate against the reference implementation on
-every pull request and every push to `main` (`.github/workflows/lint.yml`).
+render to the same visible HTML. Raw HTML can hide what it wraps, so a Why, Do
+or Source value (or a confidence label) has to be Markdown text, code or an
+image, never text inside raw HTML. CI runs the checker, the test suites,
+markdownlint, shellcheck, pyflakes and two conformance gates (the Markdown
+parser against the reference implementation, the HTML tokenizer against
+html5lib) on every pull request and every push to `main`
+(`.github/workflows/lint.yml`).
 GitHub itself renders with an older spec (cmark-gfm, 0.29): in rare edge cases,
 such as a lowercase `<!foo>` line, GitHub shows text where 0.31.2 hides a
 declaration.
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index c5816e1..2665349 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -23,9 +23,13 @@
 How: Markdown is parsed by markdown-it-py (CommonMark 0.31.2 preset; CI pins it and
 checks it against the reference implementation on all 652 spec examples), so block
 structure is the parser's, not ours. Two copies of a lesson are the same when they
-render to the same HTML once invisible markup (comments, processing instructions,
-declarations, CDATA) is removed and whitespace is collapsed the way a browser does
-outside <pre> and <code>.
+render to the same HTML once markup a browser never displays (comments, processing
+instructions, declarations, CDATA) is removed — found by a browser-faithful HTML
+tokenizer that CI checks against html5lib — and text whitespace is collapsed the way a
+browser does outside <pre> and <code> (not at all when the lesson holds raw HTML that
+shows whitespace as written). Raw HTML can hide what it wraps, so text inside a raw
+HTML element, or a raw HTML block, never counts as a Why / Do / Source value, a source
+link title or a confidence label.
 
 Contract: one line per check (PASS|WARN|FAIL name evidence), final RESULT
 line, exit 0 only when nothing FAILed. `--json` prints one JSON object.
@@ -42,7 +46,7 @@
 
 sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
 try:
-    from knowledge_md import MD                       # markdown-it-py, CommonMark preset, conformance-patched
+    from knowledge_md import MD, VOID_TAGS, html_segments   # CommonMark parser + browser-faithful HTML tokenizer
 except Exception as exc:                              # missing, or a release whose internals the patches no longer fit
     _why = ("markdown-it-py is not installed" if isinstance(exc, ImportError) and "markdown_it" in str(exc)
             else f"markdown-it-py failed to load ({type(exc).__name__}: {exc})")
@@ -70,22 +74,16 @@
 CONFIDENCE_RE = re.compile(r"(?<![A-Za-z0-9_])confidence ([0-9]{1,3})%(?![0-9%])")   # a standalone label only
 MARKER_RE = re.compile(r"^[ \t]*(?:[-*+]|[0-9]{1,9}[.)])[ \t]*")
 WS = " \t\n\r\f"                                    # HTML/CommonMark whitespace; a NBSP is content
-SEP = "\ufffc"   # stands in for visible non-text inline content (code, image, displayed raw HTML): it splits words
-
-# Raw HTML a browser never displays, ended where a browser's HTML tokenizer ends it (checked in
-# Chromium 141): a comment at `-->` or `--!>` (`<!-->` and `<!--->` are complete, empty comments);
-# a processing instruction, declaration or CDATA section, all "bogus comments" in HTML content, at
-# the first `>`. An unterminated one hides everything to the end.
-INVISIBLE_RE = re.compile(r"<!--(?:-?>|.*?(?:--!?>|\Z))|<[!?][^>]*(?:>|\Z)", re.S)
+SEP = "\ufffc"    # visible non-text content (code, an image): splits words and counts as a value
+SPLIT = "\x00"    # a raw HTML tag, or anything inside a raw HTML element: splits words, never counts as a value
+BLOCK_TAGS = frozenset("address article aside blockquote br dd details dialog div dl dt fieldset figcaption figure "
+                       "footer form h1 h2 h3 h4 h5 h6 header hgroup hr li main nav ol p pre search section summary "
+                       "table tbody td tfoot th thead tr ul".split())      # whitespace next to these is never shown
 # Raw HTML that shows whitespace as written (textarea, xmp, listing, plaintext render as pre /
-# pre-wrap) or can make it so (a style element or attribute): a lesson holding any of it is
-# compared without whitespace normalization.
-WS_KEEP_RE = re.compile(r"<(?:textarea|xmp|listing|plaintext|style)\b|\bstyle\s*=", re.I)
-PROTECT_RE = re.compile(r"<pre\b.*?</pre>|<code\b.*?</code>", re.S | re.I)   # whitespace inside code is content
-BLOCK_TAG_RE = re.compile(
-    r"[ \t\n\r\f]*(</?(?:address|article|aside|blockquote|br|dd|details|dialog|div|dl|dt|fieldset|figcaption|figure|"
-    r"footer|form|h[1-6]|header|hgroup|hr|li|main|nav|ol|p|pre|search|section|summary|table|tbody|td|tfoot|th|thead|"
-    r"tr|ul)\b[^>]*>)[ \t\n\r\f]*", re.I)
+# pre-wrap; checked in Chromium 141) or can make it so (a style element or attribute): a lesson
+# holding any of it is compared without whitespace normalization.
+WS_KEEP_TAGS = frozenset({"textarea", "xmp", "listing", "plaintext", "style"})
+STYLE_ATTR_RE = re.compile(r"[\t\n\f\r \"'/]style[\t\n\f\r ]*=", re.I)
 # Raw-HTML blocks that only end at a terminator (CommonMark types 1-5); the parser also ends them
 # when their container ends, but the OUTPUT then holds unterminated HTML that a browser reads past.
 HTML_ENDS = ((re.compile(r"<(script|pre|style|textarea)(?:[ \t>]|$)", re.I), None),
@@ -201,33 +199,40 @@ def walk(n: Node):
         yield from walk(k)
 
 
-def collapse(s: str) -> str:
-    return BLOCK_TAG_RE.sub(r"\1", re.sub(r"[ \t\n\r\f]+", " ", s))
-
-
 def visible_html(html: str) -> str:
-    """Rendered HTML reduced to what a reader can tell apart: invisible markup removed,
-    whitespace collapsed outside <pre>/<code>, and dropped next to block boundaries."""
-    html = INVISIBLE_RE.sub("", html)
-    if WS_KEEP_RE.search(html):                       # whitespace may be visible anywhere in it: compare exactly
-        return html.strip(WS)
-    parts: list[str] = []
-    i, after_pre = 0, False
-    for m in PROTECT_RE.finditer(html):               # offsets index `html`, so it is never modified here
-        seg = html[i:m.start()]
-        seg = collapse(seg.lstrip(WS) if after_pre else seg)
-        after_pre = m.group(0)[:4].lower() == "<pre"
-        parts.append(seg.rstrip(WS) if after_pre else seg)
-        parts.append(m.group(0))
-        i = m.end()
-    tail = html[i:]
-    parts.append(collapse(tail.lstrip(WS) if after_pre else tail))
-    return "".join(parts).strip(WS)
+    """Rendered HTML reduced to what a reader can tell apart. Markup a browser never displays is
+    removed by a browser-faithful tokenizer (knowledge_md.html_segments), so attribute values and
+    raw text are never touched. Text whitespace is collapsed outside <pre>/<code> and dropped next
+    to block boundaries, unless something in the HTML shows whitespace as written: then only the
+    removal applies."""
+    segs: list[tuple[str, str, str]] = []
+    for seg in html_segments(html):
+        if seg[0] == "text" and segs and segs[-1][0] == "text":
+            segs[-1] = ("text", segs[-1][1] + seg[1], "")    # text that a removed comment split is one text
+        elif seg[0] != "hidden":
+            segs.append(seg)
+    text = [src.replace("<", "<") if kind == "text" else src for kind, src, _ in segs]
+    if any(kind == "start" and (name in WS_KEEP_TAGS or STYLE_ATTR_RE.search(src)) for kind, src, name in segs):
+        return "".join(text).strip(WS)
+    verbatim = 0
+    for k, (kind, src, name) in enumerate(segs):
+        if kind in ("start", "end") and name in ("pre", "code"):
+            verbatim = verbatim + 1 if kind == "start" else max(0, verbatim - 1)
+        if kind != "text" or verbatim:
+            continue
+        t = re.sub(r"[ \t\n\r\f]+", " ", text[k])
+        if k and segs[k - 1][0] in ("start", "end") and segs[k - 1][2] in BLOCK_TAGS:
+            t = t.lstrip(" ")
+        if k + 1 < len(segs) and segs[k + 1][0] in ("start", "end") and segs[k + 1][2] in BLOCK_TAGS:
+            t = t.rstrip(" ")
+        text[k] = t
+    return "".join(text).strip(WS)
 
 
 def visible(n: Node) -> bool:
     """Does this top-level block render anything? Only raw HTML can be invisible."""
-    return n.kind != "html_block" or bool(INVISIBLE_RE.sub("", n.t.content).strip(WS))
+    return n.kind != "html_block" or any(kind != "hidden" and (kind != "text" or src.strip(WS))
+                                         for kind, src, _ in html_segments(n.t.content))
 
 
 def h2s(doc: Doc, n: Node) -> list[str]:
@@ -250,20 +255,31 @@ def inline_tokens(inline) -> list:
 
 def invisible_inline(c) -> bool:
     """An inline raw-HTML token a browser never displays (a comment, PI, declaration, CDATA)."""
-    return c.type == "html_inline" and not INVISIBLE_RE.sub("", c.content).strip(WS)
+    return c.type == "html_inline" and all(kind == "hidden" for kind, _, _ in html_segments(c.content))
 
 
 def visible_text(tokens) -> str:
-    """The text a reader sees in a run of inline tokens. Formatting delimiters (emphasis, link
-    brackets) and invisible raw HTML are zero-width; line breaks read as a space; other visible
-    content (code, an image, raw HTML that displays) becomes SEP, so it splits the words around it."""
-    out = []
+    """The text a reader is sure to see in a run of inline tokens. Formatting delimiters and
+    never-displayed markup are zero-width; line breaks read as a space; code and images become SEP.
+    A raw HTML tag, and everything inside a raw HTML element (it may be `hidden`, a template, or
+    styled away), becomes SPLIT: it splits words but never counts as a value."""
+    out, depth = [], 0
     for c in tokens:
-        if c.type == "text":
+        if c.type == "html_inline":
+            for kind, _, name in html_segments(c.content):
+                if kind == "start" and name not in VOID_TAGS:
+                    depth += 1
+                elif kind == "end":
+                    depth = max(0, depth - 1)
+                if kind != "hidden":
+                    out.append(SPLIT)
+        elif depth:
+            out.append(SPLIT)
+        elif c.type == "text":
             out.append(c.content)
         elif c.type in ("softbreak", "hardbreak"):
             out.append(" ")
-        elif c.type in ("code_inline", "image") or (c.type == "html_inline" and not invisible_inline(c)):
+        elif c.type in ("code_inline", "image"):
             out.append(SEP)
     return "".join(out)
 
@@ -296,7 +312,7 @@ def parse_source(inline) -> int | None:
     if not re.match(r"https?://[^ \t]+$", toks[k].attrGet("href") or ""):
         return None
     close = next((j for j in range(k + 1, len(toks)) if toks[j].type == "link_close"), None)
-    if close is None or not visible_text(toks[k + 1:close]).strip(WS):   # the link needs a visible title
+    if close is None or not visible_text(toks[k + 1:close]).replace(SPLIT, "").strip(WS):   # needs a visible title
         return None
     m = CONFIDENCE_RE.search(visible_text(toks[close + 1:]))
     return int(m.group(1)) if m else None
@@ -306,7 +322,7 @@ def title_of(inline) -> tuple[str | None, bool]:
     """(title, continues) when the paragraph opens with ONE `**strong**` span holding no other
     strong span — `continues` when the paragraph goes on past it on later lines. (None, _)
     when it is not a bold lesson title at all."""
-    ch = inline_tokens(inline)
+    ch = [c for c in inline_tokens(inline) if not invisible_inline(c)]
     if not ch or ch[0].type != "strong_open" or ch[0].markup != "**":
         return None, False
     depth, end = 0, None
@@ -360,8 +376,10 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
             if lesson["seen"][key]:
                 lesson["problems"].append(f"duplicate {key}")
             lesson["seen"][key] = True
-            # empty = nothing a reader sees after the label: not in the paragraph, not in a later block
-            empty = not visible_text(toks)[m.end():].strip(WS) and not any(visible(b) for b in fk[1:])
+            # empty = nothing a reader is sure to see after the label: not in the paragraph, and no
+            # later Markdown block (raw HTML never counts as a value — it may be hidden)
+            empty = (not visible_text(toks)[m.end():].replace(SPLIT, "").strip(WS)
+                     and not any(b.kind != "html_block" for b in fk[1:]))
             if key == "source":
                 conf = None if empty else parse_source(para)
                 if conf is None:
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index f882211..1ae170a 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -248,5 +248,35 @@
   "id": "whitespace-link-label",
   "markdown": "[ ](https://x.y)\n",
   "why": "a whitespace-only link text is still a link"
+ },
+ {
+  "id": "comment-before-title",
+  "markdown": "- <!-- note -->**Commented title**\n  - Why it matters here: x\n",
+  "why": "item content starting with `<!--` is an HTML block: the title is raw text"
+ },
+ {
+  "id": "comment-after-title",
+  "markdown": "- **Commented title**<!-- c -->\n  - Why it matters here: x\n",
+  "why": "an inline comment after the bold title stays inline"
+ },
+ {
+  "id": "hidden-span-confidence",
+  "markdown": "Source: [T](https://x.y) — A <span hidden>confidence 80%</span>\n",
+  "why": "raw inline tags around the confidence label"
+ },
+ {
+  "id": "comment-in-attribute",
+  "markdown": "Do: <input value=\"<!-- A -->\">\n",
+  "why": "a double-quoted attribute value may hold comment-like text"
+ },
+ {
+  "id": "block-tag-in-attribute",
+  "markdown": "Do: <abbr title=\"a <p> b\">y</abbr>\n",
+  "why": "a double-quoted attribute value may hold tag-like text"
+ },
+ {
+  "id": "raw-div-field-value",
+  "markdown": "- Why it matters here:\n\n  <div hidden>x</div>\n",
+  "why": "a type-6 HTML block as a field's only content"
  }
 ]
diff --git a/harness/conformance/html_cases.json b/harness/conformance/html_cases.json
new file mode 100644
index 0000000..4cf2306
--- /dev/null
+++ b/harness/conformance/html_cases.json
@@ -0,0 +1,30 @@
+[
+ {"id": "comment", "html": "<p>a <!-- x --> b</p>"},
+ {"id": "empty-comment", "html": "<p>x <!-->A</p>"},
+ {"id": "empty-comment-dash", "html": "<p>x <!--->A</p>"},
+ {"id": "bang-close", "html": "<p><!-- a --!>A --></p>"},
+ {"id": "comment-gt", "html": "<p><!-- a > b -->C</p>"},
+ {"id": "pi-minimal", "html": "<p><?>A</p>"},
+ {"id": "pi-gt-inside", "html": "<p><?a > b ?>C</p>"},
+ {"id": "cdata-html", "html": "<p><![CDATA[ a > b ]]>C</p>"},
+ {"id": "declaration", "html": "<p><!foo bar>C</p>"},
+ {"id": "doctype", "html": "<!DOCTYPE html><p>x</p>"},
+ {"id": "empty-end-tag", "html": "a</>b"},
+ {"id": "bogus-end-tag", "html": "a</ x>b"},
+ {"id": "attr-comment", "html": "<input value=\"<!-- A -->\">"},
+ {"id": "attr-gt-quoted", "html": "<abbr title=\"a > b\">y</abbr>"},
+ {"id": "attr-gt-single", "html": "<abbr title='a > b'>y</abbr>"},
+ {"id": "attr-unquoted-quote", "html": "<a b=c\"d>e\"f</a>"},
+ {"id": "equals-first", "html": "<a =\"x>y\">z"},
+ {"id": "textarea-comment", "html": "<textarea><!-- A --></textarea>b"},
+ {"id": "textarea-tags", "html": "<textarea><p>x</p></textarea>"},
+ {"id": "textarea-end-delim", "html": "<textarea>a</textareax>b</textarea >c"},
+ {"id": "title-rcdata", "html": "<title><!-- t -->"},
+ {"id": "style-rawtext", "html": ""},
+ {"id": "xmp-rawtext", "html": "<!-- x -->"},
+ {"id": "plaintext", "html": "<!-- x --></plaintext>"},
+ {"id": "lone-lt", "html": "a < b <1 c"},
+ {"id": "lt-lt", "html": "<<!-- x -->a>"},
+ {"id": "trailing-end-open", "html": "a</"},
+ {"id": "rendered-lesson", "html": "<ul>\n<li><strong>T</strong>\n<ul>\n<li>Why it matters here: x <!-- c --></li>\n<li>Do: <code>y</code></li>\n</ul>\n</li>\n</ul>\n"}
+]
diff --git a/harness/conformance/html_oracle.py b/harness/conformance/html_oracle.py
new file mode 100644
index 0000000..568992e
--- /dev/null
+++ b/harness/conformance/html_oracle.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python3
+"""html_oracle.py — differential gate for knowledge_md.html_segments(): it must split HTML into
+tags, text and never-displayed markup exactly where html5lib (a WHATWG-conformant tokenizer)
+does, on the fixed corpus in html_cases.json and on seeded random fragments.
+
+The comparison drives html5lib's tokenizer the way its tree builder does (RCDATA / RAWTEXT /
+PLAINTEXT after the matching start tags) and compares what is left once never-displayed markup
+(comments, bogus comments, doctypes, `</>`) is removed: start and end tag names, and the text. Out of scope by design, documented
+in the README: `<script>` (its escape states only move boundaries inside content no browser
+displays), svg/math (CDATA there is kept verbatim, which is stricter), and a tag cut off by the end
+of the input (kept, because inside a fragment the browser reads on into what follows).
+
+Usage: python3 harness/conformance/html_oracle.py [N_FUZZ]   (default 100000; seed fixed)
+"""
+import json
+import pathlib
+import random
+import sys
+
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1]))
+from knowledge_md import _tag_end, html_segments  # noqa: E402
+
+from html5lib._tokenizer import HTMLTokenizer  # noqa: E402
+from html5lib.constants import tokenTypes as T  # noqa: E402
+
+RCDATA, RAWTEXT = {"textarea", "title"}, {"style", "xmp", "iframe", "noembed", "noframes", "noscript"}
+
+
+def reference(s: str) -> list:
+    tk, out = HTMLTokenizer(s), []
+    for tok in tk:
+        ty = tok["type"]
+        if ty == T["StartTag"]:
+            out.append(("start", tok["name"]))
+            tk.state = (tk.rcdataState if tok["name"] in RCDATA else tk.rawtextState if tok["name"] in RAWTEXT
+                        else tk.plaintextState if tok["name"] == "plaintext" else tk.state)
+        elif ty == T["EndTag"]:
+            out.append(("end", tok["name"]))
+        elif ty in (T["Characters"], T["SpaceCharacters"]):
+            out.append(("text", tok["data"]))
+        elif ty in (T["Comment"], T["Doctype"]):
+            out.append(("hidden",))
+    return merge(out)
+
+
+def ours(s: str) -> list:
+    segs = html_segments(s)
+    if segs and segs[-1][0] in ("start", "end") and _tag_end(segs[-1][1], 0) < 0:
+        segs = segs[:-1]                                   # cut off by the end: html5lib drops it (see above)
+    return merge([("text", src) if kind in ("text", "raw") else (kind, name) if kind in ("start", "end")
+                  else ("hidden",) for kind, src, name in segs])
+
+
+def merge(toks: list) -> list:
+    out: list = []
+    for t in toks:
+        if t[0] == "text" and out and out[-1][0] == "text":
+            out[-1] = ("text", out[-1][1] + t[1])
+        elif t[0] == "hidden":
+            continue                                       # what is never displayed only has to disappear
+        elif t != ("text", ""):
+            out.append(t)
+    return out
+
+
+ATOMS = ["<", ">", "!", "-", "--", "?", "/", "=", '"', "'", " ", "\n", "\t", "a", "b", "x", "p", "div", "span",
+         "input", "textarea", "title", "style", "xmp", "plaintext", "value", "<!--", "-->", "--!>", "<?", "<!",
+         "</", "[CDATA[", "]]>", "DOCTYPE", "<a ", "<p>", "</p>", "<textarea>", "</textarea>", "<style>",
+         "</style", "<title>", "</title>", "<!-->", "<!--->", "=\"", "='", "<b/>", "</>"]
+
+cases = json.loads((pathlib.Path(__file__).parent / "html_cases.json").read_text(encoding="utf-8"))
+rng = random.Random(20260927)
+n_fuzz = int(sys.argv[1]) if len(sys.argv) > 1 else 100000
+fuzz = ["".join(rng.choice(ATOMS) for _ in range(rng.randint(1, 24))) for _ in range(n_fuzz)]
+bad = []
+for label, s in [(c["id"], c["html"]) for c in cases] + [(f"fuzz#{k}", s) for k, s in enumerate(fuzz)]:
+    if ours(s) != reference(s):
+        bad.append((label, s))
+print(f"{'PASS' if not bad else 'FAIL'} conformance:html-tokens   {len(cases) - sum(not b[0].startswith('fuzz#') for b in bad)}/"
+      f"{len(cases)} corpus cases and {n_fuzz - sum(b[0].startswith('fuzz#') for b in bad)}/{n_fuzz} fuzzed fragments "
+      f"split exactly like html5lib")
+for label, s in bad[:10]:
+    print(f"  DIVERGES {label}: {s!r}\n    ref  {reference(s)}\n    ours {ours(s)}")
+print("----")
+print(f"RESULT: {'PASS' if not bad else 'FAIL'} — {len(bad)} divergence(s)")
+sys.exit(0 if not bad else 1)
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index 63a0f49..1107f72 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -1,9 +1,13 @@
-"""knowledge_md.py — the one Markdown parser every harness script uses.
+"""knowledge_md.py — the one Markdown parser and HTML tokenizer every harness script uses.
 
 markdown-it-py with the CommonMark preset, plus the patches that close the gaps between it and
 the CommonMark 0.31.2 reference implementation (commonmark.js) that this repo's checks depend
 on. harness/conformance/check.py proves the combination against the reference on all 652 spec
 examples and on cases.json; a new markdown-it-py release that drifts fails CI there first.
+
+html_segments() splits rendered HTML the way a browser's tokenizer does, so the checker can
+drop only what a browser never displays. harness/conformance/html_oracle.py checks it against
+html5lib (a WHATWG-conformant tokenizer) on a fixed corpus and on seeded fuzz.
 """
 from __future__ import annotations
 
@@ -50,3 +54,102 @@ def _patch_html_blocks() -> None:
 _patch_html_blocks()
 MD = MarkdownIt("commonmark")
 MD.block.ruler.at("reference", _reference_999)
+
+
+# --- raw HTML, tokenized the way a browser does ----------------------------------------------
+HTML_WS = "\t\n\f\r "
+RAWTEXT_TAGS = frozenset({"script", "style", "textarea", "title", "xmp", "iframe", "noembed", "noframes",
+                          "noscript"})                      # content is text to the matching end tag
+VOID_TAGS = frozenset({"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source",
+                       "track", "wbr"})                     # never have content, `/>` or not
+_NAME_RE = re.compile(r"</?([A-Za-z][^\t\n\f\r />]*)")
+_COMMENT_RE = re.compile(r"<!--(?:-?>|.*?(?:--!?>|\Z))", re.S)   # `<!-->`, `<!--->` are complete; `--!>` ends one too
+
+
+def _tag_end(s: str, i: int) -> int:
+    """Index just past the tag starting at s[i] (WHATWG attribute states: a quote only opens a
+    value right after `=`, and `>` anywhere else ends the tag), or -1 when it never ends."""
+    j, n, state = _NAME_RE.match(s, i).end(), len(s), "before"
+    while j < n:
+        c = s[j]
+        if state == "value":                                 # right after `=`
+            if c in HTML_WS:
+                j += 1
+                continue
+            if c in "\"'":
+                k = s.find(c, j + 1)
+                if k < 0:
+                    return -1
+                j, state = k + 1, "before"
+                continue
+            state = "unquoted"
+        if c == ">":
+            return j + 1
+        if state == "unquoted":
+            state = "before" if c in HTML_WS else "unquoted"
+        elif c == "=" and state in ("name", "after"):
+            state = "value"
+        elif c in HTML_WS:
+            state = "after" if state == "name" else state
+        elif c == "/":
+            state = "before"
+        else:
+            state = "name"                                   # a new attribute name (a leading `=` included)
+        j += 1
+    return -1
+
+
+def html_segments(html: str) -> list[tuple[str, str, str]]:
+    """[(kind, source, tag name)] with kind "text", "start", "end", "raw" (a raw-text element's
+    content, or CDATA once svg/math appears) or "hidden" (a comment, PI, declaration, CDATA or
+    `</>`: a browser never displays it). A tag cut off by the end of the input is kept, because in a
+    fragment the browser would read on into whatever follows."""
+    out: list[tuple[str, str, str]] = []
+    i, n, foreign = 0, len(html), False
+    while i < n:
+        j = html.find("<", i)
+        if j < 0:
+            out.append(("text", html[i:], ""))
+            break
+        if j > i:
+            out.append(("text", html[i:j], ""))
+        nm = _NAME_RE.match(html, j)
+        if html.startswith("<!--", j):
+            k = _COMMENT_RE.match(html, j).end()
+            out.append(("hidden", html[j:k], ""))
+        elif nm:
+            k, name = _tag_end(html, j), nm.group(1).lower()
+            k = n if k < 0 else k
+            end = html[j + 1] == "/"
+            out.append(("end" if end else "start", html[j:k], name))
+            foreign = foreign or (not end and name in ("svg", "math"))
+            if not end and (name in RAWTEXT_TAGS or name == "plaintext"):
+                close = None if name == "plaintext" else re.compile(
+                    r"</" + re.escape(name) + r"[\t\n\f\r />]", re.I).search(html, k)
+                stop = close.start() if close else n
+                if stop > k:
+                    out.append(("raw", html[k:stop], name))
+                k = stop
+        elif foreign and html.startswith("<![CDATA[", j):   # real CDATA in svg/math: kept verbatim
+            e = html.find("]]>", j)
+            k = n if e < 0 else e + 3
+            out.append(("raw", html[j:k], ""))
+        elif html.startswith("</>", j):
+            k = j + 3
+            out.append(("hidden", "</>", ""))
+        elif html[j + 1:j + 2] in ("!", "?") or (html[j + 1:j + 2] == "/" and j + 2 < n):
+            e = html.find(">", j)                            # a bogus comment: ends at the first `>`
+            k = n if e < 0 else e + 1
+            out.append(("hidden", html[j:k], ""))
+        else:
+            k = j + (2 if html.startswith("</", j) else 1)   # a lone `<`, or `</` at the very end
+            out.append(("text", html[j:k], ""))
+        i = k
+    return out
+
+
+def strip_hidden(html: str) -> str:
+    """The HTML without what a browser never displays, a literal `<` in text written as `&lt;`
+    so that removing a comment can never splice two pieces into a new tag."""
+    return "".join(src.replace("<", "&lt;") if kind == "text" else src
+                   for kind, src, _ in html_segments(html) if kind != "hidden")
diff --git a/harness/tests/README.md b/harness/tests/README.md
index aa3f49d..c14234e 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,10 +10,12 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 and 29 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 30 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
-replacement case for the intent that survives.
+replacement case for the intent that survives. Round 30 made raw HTML stricter:
+text inside it no longer counts as a field value, so round-29 cases put plain
+text beside their raw HTML.
 
 ```bash
 pip install -r harness/requirements.txt
diff --git a/harness/tests/neg29.sh b/harness/tests/neg29.sh
index fcfa94f..c243193 100644
--- a/harness/tests/neg29.sh
+++ b/harness/tests/neg29.sh
@@ -30,17 +30,18 @@ open(p,"w").write("\n".join(s))
 '
 F='"  - Source: [T](https://x.y) — A · confidence 50%"'
 
-# N1 (4117161110): raw HTML that keeps whitespace visible (textarea, xmp, listing, a white-space style) is compared exactly
-t1() { split 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a  b</textarea>",'"$F"']'"$INS" \
-             'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a   b</textarea>",'"$F"']'"$INS"; }
+# N1 (4117161110): raw HTML that keeps whitespace visible (textarea, xmp, listing, a white-space style) is compared exactly.
+#    (Each Do: carries plain text beside the raw HTML: since round 30, content inside raw HTML never counts as a value.)
+t1() { split 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: type <textarea>a  b</textarea>",'"$F"']'"$INS" \
+             'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: type <textarea>a   b</textarea>",'"$F"']'"$INS"; }
 run "N1 textarea-whitespace-is-visible" "same title, different content" t1
-t1b() { split 'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: <span style=\"white-space:pre\">a  b</span>",'"$F"']'"$INS" \
-              'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: <span style=\"white-space:pre\">a b</span>",'"$F"']'"$INS"; }
+t1b() { split 'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: type <span style=\"white-space:pre\">a  b</span>",'"$F"']'"$INS" \
+              'LESSON=["- **Styled**","  - Why it matters here: x","  - Do: type <span style=\"white-space:pre\">a b</span>",'"$F"']'"$INS"; }
 run "N1+ white-space-style-is-visible" "same title, different content" t1b
 t1c() { split 'LESSON=["- **Block textarea**","  - Why it matters here: x","  - Do: y","","    <textarea>","    a  b","    </textarea>","",'"$F"']'"$INS" \
               'LESSON=["- **Block textarea**","  - Why it matters here: x","  - Do: y","","    <textarea>","    a   b","    </textarea>","",'"$F"']'"$INS"; }
 run "N1++ textarea-block-whitespace-is-visible" "same title, different content" t1c
-t1d() { both 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: <textarea>a  b</textarea>",'"$F"']'"$INS"; }
+t1d() { both 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: type <textarea>a  b</textarea>",'"$F"']'"$INS"; }
 pos "N1+++ identical-textarea-copies-pass" t1d
 
 # N3 (4117161124): a source link needs visible link text
diff --git a/harness/tests/neg30.sh b/harness/tests/neg30.sh
new file mode 100644
index 0000000..ba74e5d
--- /dev/null
+++ b/harness/tests/neg30.sh
@@ -0,0 +1,79 @@
+#!/usr/bin/env bash
+# Round-30 negative tests (Codex review of d2be01d, harness v2 on PR #2).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# P1 (4117250121): text inside raw HTML can be hidden (hidden, template, CSS) — it never satisfies a required value
+t1() { both 'LESSON=["- **Hidden conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A <span hidden>confidence 80%</span>"]'"$INS"; }
+run "P1 confidence-inside-raw-html" "source line is not" t1
+t1b() { both 'LESSON=["- **Hidden why**","  - Why it matters here: <span hidden>x</span>","  - Do: y",'"$F"']'"$INS"; }
+run "P1+ why-only-inside-raw-html" "missing or empty why" t1b
+t1c() { both 'LESSON=["- **Template do**","  - Why it matters here: x","  - Do: <template>y</template>",'"$F"']'"$INS"; }
+run "P1++ do-only-inside-template" "missing or empty do" t1c
+t1d() { both 'LESSON=["- **Block why**","  - Why it matters here:","","    <div hidden>x</div>","","  - Do: y",'"$F"']'"$INS"; }
+run "P1+++ why-only-a-raw-html-block" "missing or empty why" t1d
+t1e() { both 'LESSON=["- **Kbd do**","  - Why it matters here: x","  - Do: press <kbd>Ctrl</kbd> twice",'"$F"']'"$INS"; }
+pos "P1++++ text-beside-raw-html-counts" t1e
+t1f() { both 'LESSON=["- **Hidden title**","  - Why it matters here: x","  - Do: y","  - Source: [<span hidden>T</span>](https://x.y) — A · confidence 80%"]'"$INS"; }
+run "P1+++++ link-title-only-inside-raw-html" "source line is not" t1f
+
+# P2 (4117250127): invisible markup after or inside the bold title is invisible.
+#    A comment BEFORE it cannot be: a list item whose content starts with `<!--` opens an HTML block, so
+#    `**Commented title**` is raw text shown with its asterisks (commonmark.js 0.31.2:
+#    `<li>\n<!-- note -->**Commented title**`; pinned as `comment-before-title` in cases.json) -> FAIL
+t2() { both 'LESSON=["- <!-- note -->**Commented title**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "P2 comment-before-title-is-an-html-block" "not-a-bold-lesson-bullet" t2
+t2d() { both 'LESSON=["- **Commented<!-- c --> title**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "P2+++ comment-inside-title" t2d
+t2b() { both 'LESSON=["- **Commented title**<!-- c -->","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "P2+ comment-after-title" t2b
+t2c() { both 'LESSON=["- <span>**Wrapped title**</span>","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "P2++ raw-html-around-title" "not-a-bold-lesson-bullet" t2c
+
+# P3 (4117250134): only real markup nodes are removed or normalized — never attribute values or raw text
+t3() { split 'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: <input value=\"<!-- A -->\">",'"$F"']'"$INS" \
+             'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: <input value=\"<!-- B -->\">",'"$F"']'"$INS"; }
+run "P3 comment-like-text-in-attribute-is-content" "same title, different content" t3
+t3b() { split 'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: <textarea><!-- A --></textarea> y",'"$F"']'"$INS" \
+              'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: <textarea><!-- B --></textarea> y",'"$F"']'"$INS"; }
+run "P3+ comment-like-text-in-textarea-is-content" "same title, different content" t3b
+t3c() { split 'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: <input value=\"a  b\"> y",'"$F"']'"$INS" \
+              'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: <input value=\"a b\"> y",'"$F"']'"$INS"; }
+run "P3++ attribute-whitespace-is-not-collapsed" "same title, different content" t3c
+t3d() { split 'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: <abbr title=\"a <p> b\">y</abbr>",'"$F"']'"$INS" \
+              'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: <abbr title=\"a <p>b\">y</abbr>",'"$F"']'"$INS"; }
+run "P3+++ block-tag-text-inside-attribute-is-content" "same title, different content" t3d
+t3e() { split 'LESSON=["- **Real comment**","  - Why it matters here: x","  - Do: y <!-- A --> z",'"$F"']'"$INS" \
+              'LESSON=["- **Real comment**","  - Why it matters here: x","  - Do: y <!-- B --> z",'"$F"']'"$INS"; }
+pos "P3++++ real-comments-stay-invisible" t3e
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From 9afc610a73f9f91745bafec889fc77fa09cdb9d5 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 19:12:28 -0400
Subject: [PATCH 05/13] harness/tests: kill mutant M13 (quotes ignored in tags)
 in the suites

The round-30 mutation run showed M13 surviving all 253 suite cases; only the
html5lib conformance gate caught it (3 corpus cases, 734 of 100,000 fuzzed
fragments). Two neg30 cases now pin it directly: a `>` inside a quoted
attribute value must not end the tag, so whitespace after it is not
collapsed (P3+++++) and a comment after it is not removed (P3++++++).
Both FAIL against the M13 mutant and PASS on the real checker.

The P3 cases also get plain text beside their raw HTML, so the copy
comparison alone decides them (under round 30, a raw-HTML-only Do: is empty).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md              |  2 +-
 README.md              |  4 ++--
 harness/tests/neg30.sh | 25 ++++++++++++++++---------
 3 files changed, 19 insertions(+), 12 deletions(-)

diff --git a/LEDGER.md b/LEDGER.md
index 361cde8..6c1246f 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 622 lines + 155-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 56 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (15 cases, 253 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 622 lines + 155-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 56 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases, 255 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 78bceaf..66d7c21 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 253 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/tests/` | 255 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
 | `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 56 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 253 negative-test cases
+bash harness/tests/run.sh                   # the 255 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/tests/neg30.sh b/harness/tests/neg30.sh
index ba74e5d..9cf2dd8 100644
--- a/harness/tests/neg30.sh
+++ b/harness/tests/neg30.sh
@@ -57,19 +57,26 @@ pos "P2+ comment-after-title" t2b
 t2c() { both 'LESSON=["- <span>**Wrapped title**</span>","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
 run "P2++ raw-html-around-title" "not-a-bold-lesson-bullet" t2c
 
-# P3 (4117250134): only real markup nodes are removed or normalized — never attribute values or raw text
-t3() { split 'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: <input value=\"<!-- A -->\">",'"$F"']'"$INS" \
-             'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: <input value=\"<!-- B -->\">",'"$F"']'"$INS"; }
+# P3 (4117250134): only real markup nodes are removed or normalized — never attribute values or raw text.
+#    (Each Do: carries plain text beside the raw HTML, so the copy comparison alone decides the case.)
+t3() { split 'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: type <input value=\"<!-- A -->\">",'"$F"']'"$INS" \
+             'LESSON=["- **Attr comment**","  - Why it matters here: x","  - Do: type <input value=\"<!-- B -->\">",'"$F"']'"$INS"; }
 run "P3 comment-like-text-in-attribute-is-content" "same title, different content" t3
-t3b() { split 'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: <textarea><!-- A --></textarea> y",'"$F"']'"$INS" \
-              'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: <textarea><!-- B --></textarea> y",'"$F"']'"$INS"; }
+t3b() { split 'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: type <textarea><!-- A --></textarea> y",'"$F"']'"$INS" \
+              'LESSON=["- **Textarea comment**","  - Why it matters here: x","  - Do: type <textarea><!-- B --></textarea> y",'"$F"']'"$INS"; }
 run "P3+ comment-like-text-in-textarea-is-content" "same title, different content" t3b
-t3c() { split 'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: <input value=\"a  b\"> y",'"$F"']'"$INS" \
-              'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: <input value=\"a b\"> y",'"$F"']'"$INS"; }
+t3c() { split 'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: type <input value=\"a  b\"> y",'"$F"']'"$INS" \
+              'LESSON=["- **Attr space**","  - Why it matters here: x","  - Do: type <input value=\"a b\"> y",'"$F"']'"$INS"; }
 run "P3++ attribute-whitespace-is-not-collapsed" "same title, different content" t3c
-t3d() { split 'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: <abbr title=\"a <p> b\">y</abbr>",'"$F"']'"$INS" \
-              'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: <abbr title=\"a <p>b\">y</abbr>",'"$F"']'"$INS"; }
+t3d() { split 'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: see <abbr title=\"a <p> b\">y</abbr>",'"$F"']'"$INS" \
+              'LESSON=["- **Tooltip**","  - Why it matters here: x","  - Do: see <abbr title=\"a <p>b\">y</abbr>",'"$F"']'"$INS"; }
 run "P3+++ block-tag-text-inside-attribute-is-content" "same title, different content" t3d
+t3f() { split 'LESSON=["- **Quoted gt**","  - Why it matters here: x","  - Do: see <abbr title=\"a > b  c\">y</abbr>",'"$F"']'"$INS" \
+              'LESSON=["- **Quoted gt**","  - Why it matters here: x","  - Do: see <abbr title=\"a > b c\">y</abbr>",'"$F"']'"$INS"; }
+run "P3+++++ gt-inside-quoted-value-does-not-end-the-tag" "same title, different content" t3f
+t3g() { split 'LESSON=["- **Quoted comment**","  - Why it matters here: x","  - Do: see <abbr title=\"a > <!-- A -->\">y</abbr>",'"$F"']'"$INS" \
+              'LESSON=["- **Quoted comment**","  - Why it matters here: x","  - Do: see <abbr title=\"a > <!-- B -->\">y</abbr>",'"$F"']'"$INS"; }
+run "P3++++++ comment-after-gt-inside-quoted-value-is-content" "same title, different content" t3g
 t3e() { split 'LESSON=["- **Real comment**","  - Why it matters here: x","  - Do: y <!-- A --> z",'"$F"']'"$INS" \
               'LESSON=["- **Real comment**","  - Why it matters here: x","  - Do: y <!-- B --> z",'"$F"']'"$INS"; }
 pos "P3++++ real-comments-stay-invisible" t3e

From b30cdafe593340daf07dfca4c4d2f4a141896cef Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 19:28:35 -0400
Subject: [PATCH 06/13] harness: round-31 review fixes (11 findings across the
 reviews of 9eda147, 2fb93c2, 9afc610)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Each reproduced first by harness/tests/neg31.sh (12 of 21 cases failed on
9afc610; 21 of 21 pass now); browser claims checked in Chromium 141.

- A type-1 block `<script data-end="</script>">` counted as closed because
  CommonMark's end marker sits in the attribute; the browser leaves the
  script open and swallows what follows. unclosed() now also asks the
  tokenizer (knowledge_md.html_open_at_end: cut-off tag, comment, bogus
  comment, open raw-text element) and FAILs if either view is still open.
- `[T](…)confidence 80%` passed: the link text's last visible character now
  takes part in the word boundary.
- `<span hidden>x</bogus>confidence 80%` passed: a raw end tag now closes an
  element only when it matches the innermost open one (Chromium ignores the
  stray `</bogus>`); a stray end tag never ends a hidden region early.
- `**<span hidden>Invisible</span>**` passed: the title is the visible text
  of the bold span, raw-HTML content excluded.
- `x <span> y</span>` vs `x<span> y</span>` (same rendering) FAILed: text
  whitespace now follows the CSS model across inline boundaries, with one
  canonical place for the surviving space; block boundaries unchanged.
- `Do:` followed only by an empty heading or a rule passed: a later block
  counts only if it shows Markdown text, code or an image (has_value()).
- svg/math: CDATA is raw only while an svg/math element is open (nesting is
  tracked), so after `</svg>` it is a hidden bogus comment again.
- Whitespace preservation is scoped to the element that asks for it
  (textarea, xmp, listing, plaintext, style attribute); only a <style>
  element, which can restyle anything, keeps a lesson exact everywhere.
- neg11 case 6+ wrote its fixture directly; it now goes through mutate()
  and also asserts the sheet was counted (FAILs if verified/ is missing).
- Not changed: the DOCTYPE finding. A `>` inside a quoted public identifier
  ENDS the DOCTYPE (WHATWG abrupt-doctype-public-identifier); html5lib and
  Chromium 141 both display `confidence 80%">`. neg31 Q9 pins it, and the
  oracle now fuzzes PUBLIC / SYSTEM doctypes (+2 corpus cases).

Reference cases: 5 more in cases.json (61; 652/652 spec examples) and 5 in
html_cases.json (33; 100,000 fuzzed fragments, 0 divergences).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                           |   2 +-
 README.md                           |   6 +-
 harness/check-knowledge.py          |  98 ++++++++++++-----
 harness/conformance/cases.json      |  25 +++++
 harness/conformance/html_cases.json | 160 +++++++++++++++++++++++-----
 harness/conformance/html_oracle.py  |   3 +-
 harness/knowledge_md.py             |  27 ++++-
 harness/tests/README.md             |   2 +-
 harness/tests/neg11.sh              |   6 +-
 harness/tests/neg31.sh              | 104 ++++++++++++++++++
 10 files changed, 366 insertions(+), 67 deletions(-)
 create mode 100644 harness/tests/neg31.sh

diff --git a/LEDGER.md b/LEDGER.md
index 6c1246f..f9de4d2 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 622 lines + 155-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 56 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases, 255 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 664 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 61 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases, 276 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 66d7c21..15de79f 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 255 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 56 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
+| `harness/tests/` | 276 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 61 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 255 negative-test cases
+bash harness/tests/run.sh                   # the 276 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 2665349..277390f 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -46,7 +46,7 @@
 
 sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
 try:
-    from knowledge_md import MD, VOID_TAGS, html_segments   # CommonMark parser + browser-faithful HTML tokenizer
+    from knowledge_md import MD, VOID_TAGS, html_open_at_end, html_segments   # CommonMark + browser-faithful HTML
 except Exception as exc:                              # missing, or a release whose internals the patches no longer fit
     _why = ("markdown-it-py is not installed" if isinstance(exc, ImportError) and "markdown_it" in str(exc)
             else f"markdown-it-py failed to load ({type(exc).__name__}: {exc})")
@@ -182,14 +182,17 @@ def unclosed(self) -> list[str]:
                 if not re.fullmatch(closer, last):
                     out.append(f"fenced code block opened with {t.markup} never closes")
             elif t.type == "html_block":
-                c = t.content.lstrip(" ")
-                for opener, end in HTML_ENDS:
+                c, msg = t.content.lstrip(" "), None
+                for opener, end in HTML_ENDS:             # CommonMark: the block swallows Markdown until its end marker
                     m = opener.match(c)
                     if m:
                         end = end or f"</{m.group(1).lower()}>"
                         if end not in c.lower():  # CommonMark tests the whole line: `<?>` and `<!-->` close at once
-                            out.append("HTML comment never closes" if end == "-->" else f"HTML block never closes (no {end!r})")
+                            msg = "comment" if end == "-->" else f"no {end!r}"
                         break
+                msg = msg or html_open_at_end(t.content)  # the browser: still inside a tag, comment or raw-text element
+                if msg:
+                    out.append("HTML comment never closes" if msg == "comment" else f"HTML block never closes ({msg})")
         return out
 
 
@@ -201,10 +204,12 @@ def walk(n: Node):
 
 def visible_html(html: str) -> str:
     """Rendered HTML reduced to what a reader can tell apart. Markup a browser never displays is
-    removed by a browser-faithful tokenizer (knowledge_md.html_segments), so attribute values and
-    raw text are never touched. Text whitespace is collapsed outside <pre>/<code> and dropped next
-    to block boundaries, unless something in the HTML shows whitespace as written: then only the
-    removal applies."""
+    removed by a browser-faithful tokenizer (knowledge_md.html_segments), so tags, attribute values
+    and raw text stay byte-exact. Text whitespace is collapsed the way CSS does in normal flow —
+    runs become one space, also across inline element boundaries, and none survives next to a block
+    boundary — except inside <pre>, <code> and elements that keep whitespace (textarea, xmp,
+    listing, plaintext, a style attribute). A <style> element can restyle anything, so a lesson
+    holding one is compared with whitespace exactly as written."""
     segs: list[tuple[str, str, str]] = []
     for seg in html_segments(html):
         if seg[0] == "text" and segs and segs[-1][0] == "text":
@@ -212,20 +217,37 @@ def visible_html(html: str) -> str:
         elif seg[0] != "hidden":
             segs.append(seg)
     text = [src.replace("<", "&lt;") if kind == "text" else src for kind, src, _ in segs]
-    if any(kind == "start" and (name in WS_KEEP_TAGS or STYLE_ATTR_RE.search(src)) for kind, src, name in segs):
+    if any(kind == "start" and name == "style" for kind, _, name in segs):
         return "".join(text).strip(WS)
-    verbatim = 0
+    keep: list[str] = []                                      # open elements whose whitespace is shown as written
+    last, space = None, True                                  # last collapsible text index; ends in (or at) a space
     for k, (kind, src, name) in enumerate(segs):
-        if kind in ("start", "end") and name in ("pre", "code"):
-            verbatim = verbatim + 1 if kind == "start" else max(0, verbatim - 1)
-        if kind != "text" or verbatim:
+        if kind in ("start", "end"):
+            if kind == "start" and name not in VOID_TAGS and (name in WS_KEEP_TAGS or name in ("pre", "code")
+                                                                or STYLE_ATTR_RE.search(src)):
+                keep.append(name)
+            elif kind == "end" and keep and keep[-1] == name:
+                keep.pop()
+            if name in BLOCK_TAGS:                            # a block boundary: no space survives on either side
+                if last is not None:
+                    text[last] = text[last].rstrip(" ")
+                last, space = None, True
+            elif kind == "start" and name in VOID_TAGS:       # an image or input is visible content
+                last, space = None, False
+            continue
+        if kind == "raw" or keep:
+            last, space = None, space and not src
             continue
         t = re.sub(r"[ \t\n\r\f]+", " ", text[k])
-        if k and segs[k - 1][0] in ("start", "end") and segs[k - 1][2] in BLOCK_TAGS:
-            t = t.lstrip(" ")
-        if k + 1 < len(segs) and segs[k + 1][0] in ("start", "end") and segs[k + 1][2] in BLOCK_TAGS:
-            t = t.rstrip(" ")
+        if space and t.startswith(" "):
+            t = t[1:]
+        elif t.startswith(" ") and last is not None:          # only inline tags since the last text: one canonical
+            text[last], t = text[last] + " ", t[1:]           # place for the space (`x<a> y` reads as `x <a>y`)
+        if t:
+            last, space = k, t.endswith(" ")
         text[k] = t
+    if last is not None:
+        text[last] = text[last].rstrip(" ")
     return "".join(text).strip(WS)
 
 
@@ -258,21 +280,29 @@ def invisible_inline(c) -> bool:
     return c.type == "html_inline" and all(kind == "hidden" for kind, _, _ in html_segments(c.content))
 
 
+def raw_step(stack: list[str], c) -> None:
+    """Follow raw HTML elements through an inline html token: a non-void start tag opens one; an end
+    tag closes it only when it matches the innermost open element (a stray end tag is ignored, so a
+    hidden region is never left early)."""
+    for kind, _, name in html_segments(c.content):
+        if kind == "start" and name not in VOID_TAGS:
+            stack.append(name)
+        elif kind == "end" and stack and stack[-1] == name:
+            stack.pop()
+
+
 def visible_text(tokens) -> str:
     """The text a reader is sure to see in a run of inline tokens. Formatting delimiters and
     never-displayed markup are zero-width; line breaks read as a space; code and images become SEP.
     A raw HTML tag, and everything inside a raw HTML element (it may be `hidden`, a template, or
     styled away), becomes SPLIT: it splits words but never counts as a value."""
-    out, depth = [], 0
+    out: list[str] = []
+    depth: list[str] = []
     for c in tokens:
         if c.type == "html_inline":
-            for kind, _, name in html_segments(c.content):
-                if kind == "start" and name not in VOID_TAGS:
-                    depth += 1
-                elif kind == "end":
-                    depth = max(0, depth - 1)
-                if kind != "hidden":
-                    out.append(SPLIT)
+            raw_step(depth, c)
+            if not invisible_inline(c):
+                out.append(SPLIT)
         elif depth:
             out.append(SPLIT)
         elif c.type == "text":
@@ -314,7 +344,8 @@ def parse_source(inline) -> int | None:
     close = next((j for j in range(k + 1, len(toks)) if toks[j].type == "link_close"), None)
     if close is None or not visible_text(toks[k + 1:close]).replace(SPLIT, "").strip(WS):   # needs a visible title
         return None
-    m = CONFIDENCE_RE.search(visible_text(toks[close + 1:]))
+    tail = visible_text(toks[k + 1:close])[-1:] + visible_text(toks[close + 1:])   # the link text's last character
+    m = CONFIDENCE_RE.search(tail, 1)                                              # takes part in the word boundary
     return int(m.group(1)) if m else None
 
 
@@ -338,13 +369,24 @@ def title_of(inline) -> tuple[str | None, bool]:
                 break
     if end is None:
         return None, False
-    title = "".join(c.content for c in ch[1:end] if c.type in ("text", "code_inline"))
+    title = visible_text(ch[1:end]).replace(SPLIT, "").replace(SEP, "")   # what a reader sees in the bold span
     rest = ch[end + 1:]
     if rest and rest[0].type not in ("softbreak", "hardbreak"):
         return None, False                        # `**a** b **c**`, `**foo***`: not one bold span
     return (title if title.strip(WS) else None), bool(rest)
 
 
+def has_value(doc: Doc, n: Node) -> bool:
+    """Does this block show a reader something: visible text, code or an image in Markdown (never
+    inside raw HTML, which may be hidden)? An empty heading or a rule does not."""
+    for t in doc.tokens[n.i:n.j + 1]:
+        if t.type == "inline" and visible_text(inline_tokens(t)).replace(SPLIT, "").strip(WS):
+            return True
+        if t.type in ("fence", "code_block") and t.content.strip(WS):
+            return True
+    return False
+
+
 def parse_lesson(doc: Doc, item: Node) -> dict:
     """{title, problems, key, parts} for one top-level list item of a day section."""
     lesson = {"problems": [], "key": doc.html(item), "parts": {}, "seen": {k: False for k in FIELDS}}
@@ -379,7 +421,7 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
             # empty = nothing a reader is sure to see after the label: not in the paragraph, and no
             # later Markdown block (raw HTML never counts as a value — it may be hidden)
             empty = (not visible_text(toks)[m.end():].replace(SPLIT, "").strip(WS)
-                     and not any(b.kind != "html_block" for b in fk[1:]))
+                     and not any(has_value(doc, b) for b in fk[1:]))
             if key == "source":
                 conf = None if empty else parse_source(para)
                 if conf is None:
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 1ae170a..30ba371 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -278,5 +278,30 @@
   "id": "raw-div-field-value",
   "markdown": "- Why it matters here:\n\n  <div hidden>x</div>\n",
   "why": "a type-6 HTML block as a field's only content"
+ },
+ {
+  "id": "script-block-quoted-end",
+  "markdown": "- Do: y\n\n  <script data-end=\"</script>\">\n\n- next\n",
+  "why": "CommonMark ends a type-1 block at `</script>` even inside an attribute"
+ },
+ {
+  "id": "empty-heading-in-field",
+  "markdown": "- Do:\n\n  ###\n",
+  "why": "an empty ATX heading inside a field item"
+ },
+ {
+  "id": "rule-in-field",
+  "markdown": "- Do:\n\n  ***\n",
+  "why": "a thematic break inside a field item"
+ },
+ {
+  "id": "label-glued-to-link",
+  "markdown": "Source: [T](https://x.y)confidence 80%\n",
+  "why": "text directly after a link"
+ },
+ {
+  "id": "link-text-leading-space",
+  "markdown": "Do: x[ y](https://x.y)\n",
+  "why": "a link whose text starts with a space"
  }
 ]
diff --git a/harness/conformance/html_cases.json b/harness/conformance/html_cases.json
index 4cf2306..596c603 100644
--- a/harness/conformance/html_cases.json
+++ b/harness/conformance/html_cases.json
@@ -1,30 +1,134 @@
 [
- {"id": "comment", "html": "<p>a <!-- x --> b</p>"},
- {"id": "empty-comment", "html": "<p>x <!-->A</p>"},
- {"id": "empty-comment-dash", "html": "<p>x <!--->A</p>"},
- {"id": "bang-close", "html": "<p><!-- a --!>A --></p>"},
- {"id": "comment-gt", "html": "<p><!-- a > b -->C</p>"},
- {"id": "pi-minimal", "html": "<p><?>A</p>"},
- {"id": "pi-gt-inside", "html": "<p><?a > b ?>C</p>"},
- {"id": "cdata-html", "html": "<p><![CDATA[ a > b ]]>C</p>"},
- {"id": "declaration", "html": "<p><!foo bar>C</p>"},
- {"id": "doctype", "html": "<!DOCTYPE html><p>x</p>"},
- {"id": "empty-end-tag", "html": "a</>b"},
- {"id": "bogus-end-tag", "html": "a</ x>b"},
- {"id": "attr-comment", "html": "<input value=\"<!-- A -->\">"},
- {"id": "attr-gt-quoted", "html": "<abbr title=\"a > b\">y</abbr>"},
- {"id": "attr-gt-single", "html": "<abbr title='a > b'>y</abbr>"},
- {"id": "attr-unquoted-quote", "html": "<a b=c\"d>e\"f</a>"},
- {"id": "equals-first", "html": "<a =\"x>y\">z"},
- {"id": "textarea-comment", "html": "<textarea><!-- A --></textarea>b"},
- {"id": "textarea-tags", "html": "<textarea><p>x</p></textarea>"},
- {"id": "textarea-end-delim", "html": "<textarea>a</textareax>b</textarea >c"},
- {"id": "title-rcdata", "html": "<title><!-- t --></title>"},
- {"id": "style-rawtext", "html": "<style>a > b { }</style><!-- c -->"},
- {"id": "xmp-rawtext", "html": "<xmp><!-- x --></xmp>"},
- {"id": "plaintext", "html": "<plaintext><!-- x --></plaintext>"},
- {"id": "lone-lt", "html": "a < b <1 c"},
- {"id": "lt-lt", "html": "<<!-- x -->a>"},
- {"id": "trailing-end-open", "html": "a</"},
- {"id": "rendered-lesson", "html": "<ul>\n<li><strong>T</strong>\n<ul>\n<li>Why it matters here: x <!-- c --></li>\n<li>Do: <code>y</code></li>\n</ul>\n</li>\n</ul>\n"}
+ {
+  "id": "comment",
+  "html": "<p>a <!-- x --> b</p>"
+ },
+ {
+  "id": "empty-comment",
+  "html": "<p>x <!-->A</p>"
+ },
+ {
+  "id": "empty-comment-dash",
+  "html": "<p>x <!--->A</p>"
+ },
+ {
+  "id": "bang-close",
+  "html": "<p><!-- a --!>A --></p>"
+ },
+ {
+  "id": "comment-gt",
+  "html": "<p><!-- a > b -->C</p>"
+ },
+ {
+  "id": "pi-minimal",
+  "html": "<p><?>A</p>"
+ },
+ {
+  "id": "pi-gt-inside",
+  "html": "<p><?a > b ?>C</p>"
+ },
+ {
+  "id": "cdata-html",
+  "html": "<p><![CDATA[ a > b ]]>C</p>"
+ },
+ {
+  "id": "declaration",
+  "html": "<p><!foo bar>C</p>"
+ },
+ {
+  "id": "doctype",
+  "html": "<!DOCTYPE html><p>x</p>"
+ },
+ {
+  "id": "empty-end-tag",
+  "html": "a</>b"
+ },
+ {
+  "id": "bogus-end-tag",
+  "html": "a</ x>b"
+ },
+ {
+  "id": "attr-comment",
+  "html": "<input value=\"<!-- A -->\">"
+ },
+ {
+  "id": "attr-gt-quoted",
+  "html": "<abbr title=\"a > b\">y</abbr>"
+ },
+ {
+  "id": "attr-gt-single",
+  "html": "<abbr title='a > b'>y</abbr>"
+ },
+ {
+  "id": "attr-unquoted-quote",
+  "html": "<a b=c\"d>e\"f</a>"
+ },
+ {
+  "id": "equals-first",
+  "html": "<a =\"x>y\">z"
+ },
+ {
+  "id": "textarea-comment",
+  "html": "<textarea><!-- A --></textarea>b"
+ },
+ {
+  "id": "textarea-tags",
+  "html": "<textarea><p>x</p></textarea>"
+ },
+ {
+  "id": "textarea-end-delim",
+  "html": "<textarea>a</textareax>b</textarea >c"
+ },
+ {
+  "id": "title-rcdata",
+  "html": "<title><!-- t --></title>"
+ },
+ {
+  "id": "style-rawtext",
+  "html": "<style>a > b { }</style><!-- c -->"
+ },
+ {
+  "id": "xmp-rawtext",
+  "html": "<xmp><!-- x --></xmp>"
+ },
+ {
+  "id": "plaintext",
+  "html": "<plaintext><!-- x --></plaintext>"
+ },
+ {
+  "id": "lone-lt",
+  "html": "a < b <1 c"
+ },
+ {
+  "id": "lt-lt",
+  "html": "<<!-- x -->a>"
+ },
+ {
+  "id": "trailing-end-open",
+  "html": "a</"
+ },
+ {
+  "id": "rendered-lesson",
+  "html": "<ul>\n<li><strong>T</strong>\n<ul>\n<li>Why it matters here: x <!-- c --></li>\n<li>Do: <code>y</code></li>\n</ul>\n</li>\n</ul>\n"
+ },
+ {
+  "id": "doctype-quoted-gt",
+  "html": "A <!DOCTYPE html PUBLIC \"x>confidence 80%\">B"
+ },
+ {
+  "id": "doctype-system",
+  "html": "<!doctype x SYSTEM 'a>b'>c"
+ },
+ {
+  "id": "svg-closed-then-cdata",
+  "html": "y <svg></svg><![CDATA[A]]>z"
+ },
+ {
+  "id": "script-quoted-end",
+  "html": "<textarea data-end=\"</textarea>\">x"
+ },
+ {
+  "id": "bogus-end-in-span",
+  "html": "x <span hidden>a</bogus>confidence 80%</span>"
+ }
 ]
diff --git a/harness/conformance/html_oracle.py b/harness/conformance/html_oracle.py
index 568992e..bc2f60a 100644
--- a/harness/conformance/html_oracle.py
+++ b/harness/conformance/html_oracle.py
@@ -66,7 +66,8 @@ def merge(toks: list) -> list:
 ATOMS = ["<", ">", "!", "-", "--", "?", "/", "=", '"', "'", " ", "\n", "\t", "a", "b", "x", "p", "div", "span",
          "input", "textarea", "title", "style", "xmp", "plaintext", "value", "<!--", "-->", "--!>", "<?", "<!",
          "</", "[CDATA[", "]]>", "DOCTYPE", "<a ", "<p>", "</p>", "<textarea>", "</textarea>", "<style>",
-         "</style", "<title>", "</title>", "<!-->", "<!--->", "=\"", "='", "<b/>", "</>"]
+         "</style", "<title>", "</title>", "<!-->", "<!--->", "=\"", "='", "<b/>", "</>", "PUBLIC", "SYSTEM",
+         "<!DOCTYPE "]
 
 cases = json.loads((pathlib.Path(__file__).parent / "html_cases.json").read_text(encoding="utf-8"))
 rng = random.Random(20260927)
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index 1107f72..271a2eb 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -101,11 +101,11 @@ def _tag_end(s: str, i: int) -> int:
 
 def html_segments(html: str) -> list[tuple[str, str, str]]:
     """[(kind, source, tag name)] with kind "text", "start", "end", "raw" (a raw-text element's
-    content, or CDATA once svg/math appears) or "hidden" (a comment, PI, declaration, CDATA or
+    content, or CDATA inside svg/math) or "hidden" (a comment, PI, declaration, CDATA or
     `</>`: a browser never displays it). A tag cut off by the end of the input is kept, because in a
     fragment the browser would read on into whatever follows."""
     out: list[tuple[str, str, str]] = []
-    i, n, foreign = 0, len(html), False
+    i, n, foreign = 0, len(html), 0                         # foreign: open svg/math elements
     while i < n:
         j = html.find("<", i)
         if j < 0:
@@ -122,7 +122,8 @@ def html_segments(html: str) -> list[tuple[str, str, str]]:
             k = n if k < 0 else k
             end = html[j + 1] == "/"
             out.append(("end" if end else "start", html[j:k], name))
-            foreign = foreign or (not end and name in ("svg", "math"))
+            if name in ("svg", "math") and not html[j:k].endswith("/>"):
+                foreign = max(0, foreign - 1) if end else foreign + 1
             if not end and (name in RAWTEXT_TAGS or name == "plaintext"):
                 close = None if name == "plaintext" else re.compile(
                     r"</" + re.escape(name) + r"[\t\n\f\r />]", re.I).search(html, k)
@@ -153,3 +154,23 @@ def strip_hidden(html: str) -> str:
     so that removing a comment can never splice two pieces into a new tag."""
     return "".join(src.replace("<", "&lt;") if kind == "text" else src
                    for kind, src, _ in html_segments(html) if kind != "hidden")
+
+
+def html_open_at_end(html: str) -> str | None:
+    """Why a browser would still be inside something when this HTML ends — and so read on into
+    whatever follows — or None: a cut-off tag, comment or bogus comment, or an open raw-text element."""
+    segs = html_segments(html)
+    if not segs:
+        return None
+    kind, src, name = segs[-1]
+    if kind in ("start", "end") and _tag_end(src, 0) < 0:
+        return "a tag is cut off"
+    if kind == "hidden" and src.startswith("<!--") and not (src.endswith(("-->", "--!>")) or src in ("<!-->", "<!--->")):
+        return "comment"
+    if kind == "hidden" and not src.endswith(">"):
+        return "no '>'"
+    if kind == "raw" and not name and not src.endswith("]]>"):
+        return "no ']]>'"
+    if (kind == "raw" or kind == "start") and (name in RAWTEXT_TAGS or name == "plaintext"):
+        return f"<{name}> is still open"
+    return None
diff --git a/harness/tests/README.md b/harness/tests/README.md
index c14234e..07a4ea9 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,7 +10,7 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 30 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 31 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
diff --git a/harness/tests/neg11.sh b/harness/tests/neg11.sh
index 1b36479..b3357b5 100755
--- a/harness/tests/neg11.sh
+++ b/harness/tests/neg11.sh
@@ -85,11 +85,13 @@ EOF
 run "7 misplaced-lesson" "knowledge:misplaced" t7
 
 # 6-positive: a verified sheet that STARTS with a comment and then a real H1 must PASS
+# (routed through mutate(), and the sheet must be counted, so a fixture change cannot turn this into a no-op)
 n=$((n+1))
 rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"
-printf '<!-- lead comment -->\n\n# Real heading\n\nbody\n' > "$W/r/verified/2026-01-03_lead_v1.md"
+t6p() { printf '<!-- lead comment -->\n\n# Real heading\n\nbody\n' > verified/2026-01-03_lead_v1.md; }
+mutate t6p
 out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
-if [ $rc -eq 0 ]; then echo "PASS 6+ comment-then-H1 accepted"; else bad=$((bad+1)); echo "FAIL 6+ comment-then-H1 rejected:"; echo "$out" | sed 's/^/    /'; fi
+if [ $rc -eq 0 ] && printf '%s' "$out" | grep -q "verified:names .* 4 sheet(s)"; then echo "PASS 6+ comment-then-H1 accepted"; else bad=$((bad+1)); echo "FAIL 6+ comment-then-H1 rejected (or the sheet was not written):"; echo "$out" | sed 's/^/    /'; fi
 
 # 3-positive: `-\t**Title**` lesson with tab-marked fields in BOTH copies must PASS
 n=$((n+1))
diff --git a/harness/tests/neg31.sh b/harness/tests/neg31.sh
new file mode 100644
index 0000000..ee2e007
--- /dev/null
+++ b/harness/tests/neg31.sh
@@ -0,0 +1,104 @@
+#!/usr/bin/env bash
+# Round-31 negative tests (Codex reviews of 9eda147, 2fb93c2 and 9afc610, harness v2 on PR #2).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# Q2 (4117305791): a quoted `</script>` in the start tag does not close the element; the browser swallows what follows
+t2() { both 'LESSON=["- **Script attr**","  - Why it matters here: x","  - Do: y","","    <script data-end=\"</script>\">","",'"$F"']'"$INS"; }
+run "Q2 quoted-end-marker-leaves-script-open" "HTML block never closes" t2
+t2b() { both 'LESSON=["- **Script ok**","  - Why it matters here: x","  - Do: y","","    <script>x</script>","",'"$F"']'"$INS"; }
+pos "Q2+ closed-script-block" t2b
+
+# Q3 (4117305795): the confidence label must stand alone after the link text too
+t3() { both 'LESSON=["- **Glued conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y)confidence 80%"]'"$INS"; }
+run "Q3 label-glued-to-link-text" "source line is not" t3
+t3b() { both 'LESSON=["- **Spaced conf**","  - Why it matters here: x","  - Do: y","  - Source: [T.](https://x.y)confidence 80%"]'"$INS"; }
+pos "Q3+ punctuation-before-label" t3b
+
+# Q4 (4117383769): an unrelated end tag does not close the hidden element (Chromium: `</bogus>` ignored)
+t4() { both 'LESSON=["- **Bogus end**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <span hidden>x</bogus>confidence 80%</span>"]'"$INS"; }
+run "Q4 unmatched-end-tag-keeps-hidden-region" "source line is not" t4
+t4b() { both 'LESSON=["- **Closed span**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <kbd>K</kbd> · confidence 80%"]'"$INS"; }
+pos "Q4+ label-after-a-closed-element" t4b
+
+# Q5 (4117383771): the title is what a reader sees in the bold span
+t5() { both 'LESSON=["- **<span hidden>Invisible</span>**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "Q5 hidden-title" "not-a-bold-lesson-bullet" t5
+t5b() { both 'LESSON=["- **Press <kbd>K</kbd>**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "Q5+ title-with-visible-text-beside-raw-html" t5b
+
+# Q6 (4117383777): whitespace collapses across inline element boundaries, as in a browser
+t6() { split 'LESSON=["- **Inline space**","  - Why it matters here: x","  - Do: x <span> y</span>",'"$F"']'"$INS" \
+             'LESSON=["- **Inline space**","  - Why it matters here: x","  - Do: x<span> y</span>",'"$F"']'"$INS"; }
+pos "Q6 space-across-raw-inline-boundary" t6
+t6b() { split 'LESSON=["- **Link space**","  - Why it matters here: x","  - Do: x [ y](https://x.y)",'"$F"']'"$INS" \
+              'LESSON=["- **Link space**","  - Why it matters here: x","  - Do: x[ y](https://x.y)",'"$F"']'"$INS"; }
+pos "Q6+ space-across-link-boundary" t6b
+t6c() { split 'LESSON=["- **Real space**","  - Why it matters here: x","  - Do: x [y](https://x.y)",'"$F"']'"$INS" \
+              'LESSON=["- **Real space**","  - Why it matters here: x","  - Do: x[y](https://x.y)",'"$F"']'"$INS"; }
+run "Q6++ a-missing-space-still-differs" "same title, different content" t6c
+
+# Q7 (4117383781): a later block only counts when it renders visible content
+t7() { both 'LESSON=["- **Empty heading**","  - Why it matters here: x","  - Do:","","    ###","",'"$F"']'"$INS"; }
+run "Q7 empty-heading-is-no-value" "missing or empty do" t7
+t7b() { both 'LESSON=["- **Rule only**","  - Why it matters here: x","  - Do:","","    ***","",'"$F"']'"$INS"; }
+run "Q7+ thematic-break-is-no-value" "missing or empty do" t7b
+t7c() { both 'LESSON=["- **Hidden para**","  - Why it matters here: x","  - Do:","","    <span hidden>y</span> <!-- c -->","",'"$F"']'"$INS"; }
+run "Q7++ raw-html-only-paragraph-is-no-value" "missing or empty do" t7c
+t7d() { both 'LESSON=["- **Code value**","  - Why it matters here: x","  - Do:","","    ```","    make","    ```","",'"$F"']'"$INS"; }
+pos "Q7+++ code-block-is-a-value" t7d
+
+# Q8 (4117383783): after </svg>, CDATA is an HTML bogus comment again (invisible)
+t8() { split 'LESSON=["- **After svg**","  - Why it matters here: x","  - Do: y <svg></svg><![CDATA[A]]>",'"$F"']'"$INS" \
+             'LESSON=["- **After svg**","  - Why it matters here: x","  - Do: y <svg></svg><![CDATA[B]]>",'"$F"']'"$INS"; }
+pos "Q8 cdata-after-closed-svg-is-hidden" t8
+t8b() { split 'LESSON=["- **In svg**","  - Why it matters here: x","  - Do: y <svg><![CDATA[A]]></svg>",'"$F"']'"$INS" \
+              'LESSON=["- **In svg**","  - Why it matters here: x","  - Do: y <svg><![CDATA[B]]></svg>",'"$F"']'"$INS"; }
+run "Q8+ cdata-inside-svg-is-compared" "same title, different content" t8b
+
+# Q9 (4117383785): a `>` inside a quoted DOCTYPE identifier ENDS the DOCTYPE (WHATWG abrupt-doctype-public-identifier;
+#    html5lib and Chromium 141 both show `confidence 80%">`), so the label is visible and the source is valid
+t9() { both 'LESSON=["- **Doctype conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <!DOCTYPE html PUBLIC \"x>confidence 80%\">"]'"$INS"; }
+pos "Q9 gt-in-quoted-doctype-identifier-ends-it" t9
+
+# Q10 (4117425621): preserved whitespace is scoped to the element that asks for it; a <style> element keeps it lesson-wide
+t10() { split 'LESSON=["- **Scoped**","  - Why it matters here: x","  - Do: a  b <textarea>x</textarea>",'"$F"']'"$INS" \
+              'LESSON=["- **Scoped**","  - Why it matters here: x","  - Do: a b <textarea>x</textarea>",'"$F"']'"$INS"; }
+pos "Q10 whitespace-outside-textarea-collapses" t10
+t10b() { split 'LESSON=["- **Styled scope**","  - Why it matters here: x","  - Do: a  b <span style=\"white-space:pre\">c</span>",'"$F"']'"$INS" \
+               'LESSON=["- **Styled scope**","  - Why it matters here: x","  - Do: a b <span style=\"white-space:pre\">c</span>",'"$F"']'"$INS"; }
+pos "Q10+ whitespace-outside-styled-element-collapses" t10b
+t10c() { split 'LESSON=["- **Sheet**","  - Why it matters here: x","  - Do: a  b <style>li{white-space:pre}</style>",'"$F"']'"$INS" \
+               'LESSON=["- **Sheet**","  - Why it matters here: x","  - Do: a b <style>li{white-space:pre}</style>",'"$F"']'"$INS"; }
+run "Q10++ a-style-element-keeps-whitespace-everywhere" "same title, different content" t10c
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From 37a18ebb0becc2b6666af528785ff5b1aeed8b8a Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 21:43:46 -0400
Subject: [PATCH 07/13] harness: ban raw HTML (other than comments) from
 lessons; round-32 fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

T's decision, 2026-09-27: every Codex round since 78d12a9 found new browser
edge cases in raw HTML inside lessons (3 to 11 per review; 7 more on
b30cdaf), while the real lesson file holds none. Rather than emulate a
browser case by case, lessons are now plain Markdown: raw_html_in() FAILs a
lesson holding any raw HTML other than `<!-- comments -->` — inline or as a
block, tags, declarations, PIs and CDATA alike ("raw HTML in a lesson (only
<!-- comments --> are allowed)"). HTML inside code, autolinks and comments
stay allowed (neg32 R0+++++++ to R0+++++++++).

This supersedes five of the seven b30cdaf findings (style attributes,
intervening end tags, raw HTML at a link's end, an open <div> swallowing the
fields, character references in attributes): each is now a FAIL (neg32
R2-R6). The other two are fixed:
- regression from b30cdaf: a title that is inline code (`**`Title`**`)
  lost its text and was rejected; titles keep code text again (R1, R1+).
- `</svg/>` is an end tag: the self-closing check now applies to start
  tags only, so the tokenizer leaves foreign content (R7).
Both reproduce on b30cdaf and pass now.

Expectation changes (marked CHANGED 2026-09-27 in the suites): 14 positive
cases carried raw HTML in a lesson and now expect the ban — 4 from PR #1
(neg15 2b, neg23 G4+, neg26 J2, neg27 K4+) and 10 from rounds 29-31.

22 suites, 294 cases, 0 failing; conformance 652/652 spec, 61 edge cases;
html5lib oracle 100,000 fragments, 0 divergences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                  |  2 +-
 README.md                  | 10 ++---
 harness/check-knowledge.py | 39 +++++++++++++----
 harness/knowledge_md.py    |  4 +-
 harness/tests/README.md    |  6 ++-
 harness/tests/neg15.sh     |  4 +-
 harness/tests/neg23.sh     |  4 +-
 harness/tests/neg26.sh     |  4 +-
 harness/tests/neg27.sh     |  4 +-
 harness/tests/neg29.sh     |  4 +-
 harness/tests/neg30.sh     |  4 +-
 harness/tests/neg31.sh     | 32 ++++++++++----
 harness/tests/neg32.sh     | 89 ++++++++++++++++++++++++++++++++++++++
 13 files changed, 173 insertions(+), 33 deletions(-)
 create mode 100644 harness/tests/neg32.sh

diff --git a/LEDGER.md b/LEDGER.md
index f9de4d2..654bea7 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 664 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 61 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases, 276 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 685 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 61 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases, 294 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 15de79f..2573e0b 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 276 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/tests/` | 294 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
 | `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 61 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
@@ -64,14 +64,14 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 276 negative-test cases
+bash harness/tests/run.sh                   # the 294 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
 rather than pattern matching. Two copies of a lesson count as the same when they
-render to the same visible HTML. Raw HTML can hide what it wraps, so a Why, Do
-or Source value (or a confidence label) has to be Markdown text, code or an
-image, never text inside raw HTML. CI runs the checker, the test suites,
+render to the same visible HTML. Lessons are plain Markdown: raw HTML can
+hide, restyle or swallow what a reader sees, so a lesson holding any raw HTML
+other than a `<!-- comment -->` fails the check. CI runs the checker, the test suites,
 markdownlint, shellcheck, pyflakes and two conformance gates (the Markdown
 parser against the reference implementation, the HTML tokenizer against
 html5lib) on every pull request and every push to `main`
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 277390f..be462cd 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -8,7 +8,8 @@
                  (real calendar dates, no repeats, no other H2s) newest first; every top-level
                  bullet in a section is a bold lesson with a non-empty
                  "Why it matters here:", a non-empty "Do:", and a "Source:"
-                 line carrying a link and a 0-100 confidence; a bullet
+                 line carrying a link and a 0-100 confidence, written in plain
+                 Markdown (raw HTML other than <!-- comments --> FAILs); a bullet
                  outside every day section is a FAIL (readers never see it), and so
                  is any other visible top-level block inside a day section (a
                  paragraph, heading, rule, code or raw HTML — no day file carries it)
@@ -27,9 +28,10 @@
 instructions, declarations, CDATA) is removed — found by a browser-faithful HTML
 tokenizer that CI checks against html5lib — and text whitespace is collapsed the way a
 browser does outside <pre> and <code> (not at all when the lesson holds raw HTML that
-shows whitespace as written). Raw HTML can hide what it wraps, so text inside a raw
-HTML element, or a raw HTML block, never counts as a Why / Do / Source value, a source
-link title or a confidence label.
+shows whitespace as written). Raw HTML can hide, restyle or swallow what a reader
+sees, so a lesson holding any raw HTML other than a comment FAILs outright; the
+visibility rules that remain (text inside raw HTML never counts as a value) only shape
+the extra diagnostics such a lesson gets.
 
 Contract: one line per check (PASS|WARN|FAIL name evidence), final RESULT
 line, exit 0 only when nothing FAILed. `--json` prints one JSON object.
@@ -291,11 +293,12 @@ def raw_step(stack: list[str], c) -> None:
             stack.pop()
 
 
-def visible_text(tokens) -> str:
+def visible_text(tokens, code_text: bool = False) -> str:
     """The text a reader is sure to see in a run of inline tokens. Formatting delimiters and
-    never-displayed markup are zero-width; line breaks read as a space; code and images become SEP.
-    A raw HTML tag, and everything inside a raw HTML element (it may be `hidden`, a template, or
-    styled away), becomes SPLIT: it splits words but never counts as a value."""
+    never-displayed markup are zero-width; line breaks read as a space; code and images become SEP
+    (code becomes its own text with code_text). A raw HTML tag, and everything inside a raw HTML
+    element (it may be `hidden`, a template, or styled away), becomes SPLIT: it splits words but
+    never counts as a value."""
     out: list[str] = []
     depth: list[str] = []
     for c in tokens:
@@ -309,6 +312,8 @@ def visible_text(tokens) -> str:
             out.append(c.content)
         elif c.type in ("softbreak", "hardbreak"):
             out.append(" ")
+        elif c.type == "code_inline" and code_text:
+            out.append(c.content)
         elif c.type in ("code_inline", "image"):
             out.append(SEP)
     return "".join(out)
@@ -369,7 +374,7 @@ def title_of(inline) -> tuple[str | None, bool]:
                 break
     if end is None:
         return None, False
-    title = visible_text(ch[1:end]).replace(SPLIT, "").replace(SEP, "")   # what a reader sees in the bold span
+    title = visible_text(ch[1:end], code_text=True).replace(SPLIT, "").replace(SEP, "")   # what a reader sees
     rest = ch[end + 1:]
     if rest and rest[0].type not in ("softbreak", "hardbreak"):
         return None, False                        # `**a** b **c**`, `**foo***`: not one bold span
@@ -387,9 +392,25 @@ def has_value(doc: Doc, n: Node) -> bool:
     return False
 
 
+def raw_html_in(doc: Doc, n: Node) -> bool:
+    """Does this block hold raw HTML other than `<!-- comments -->`? Lessons are plain Markdown:
+    raw HTML can hide, restyle or swallow what a reader sees, so only comments are allowed."""
+    def only_comments(html: str) -> bool:
+        return all((kind == "hidden" and src.startswith("<!--")) or (kind == "text" and not src.strip(WS))
+                   for kind, src, _ in html_segments(html))
+    for t in doc.tokens[n.i:n.j + 1]:
+        if t.type == "html_block" and not only_comments(t.content):
+            return True
+        if t.type == "inline" and any(c.type == "html_inline" and not only_comments(c.content) for c in t.children):
+            return True
+    return False
+
+
 def parse_lesson(doc: Doc, item: Node) -> dict:
     """{title, problems, key, parts} for one top-level list item of a day section."""
     lesson = {"problems": [], "key": doc.html(item), "parts": {}, "seen": {k: False for k in FIELDS}}
+    if raw_html_in(doc, item):
+        lesson["problems"].append("raw HTML in a lesson (only <!-- comments --> are allowed)")
     head = item.kids[0] if item.kids and item.kids[0].kind == "paragraph" else None
     title, continues = title_of(head.kids[0].t) if head and item.t.markup == "-" else (None, False)
     if title is None:
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index 271a2eb..4312103 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -122,8 +122,8 @@ def html_segments(html: str) -> list[tuple[str, str, str]]:
             k = n if k < 0 else k
             end = html[j + 1] == "/"
             out.append(("end" if end else "start", html[j:k], name))
-            if name in ("svg", "math") and not html[j:k].endswith("/>"):
-                foreign = max(0, foreign - 1) if end else foreign + 1
+            if name in ("svg", "math"):                     # `/>` self-closes a foreign START tag only
+                foreign = max(0, foreign - 1) if end else foreign + (not html[j:k].endswith("/>"))
             if not end and (name in RAWTEXT_TAGS or name == "plaintext"):
                 close = None if name == "plaintext" else re.compile(
                     r"</" + re.escape(name) + r"[\t\n\f\r />]", re.I).search(html, k)
diff --git a/harness/tests/README.md b/harness/tests/README.md
index 07a4ea9..dcc5ee0 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,12 +10,14 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 31 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 32 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
 text inside it no longer counts as a field value, so round-29 cases put plain
-text beside their raw HTML.
+text beside their raw HTML. Round 32 bans raw HTML other than comments from
+lessons (T's decision, 2026-09-27): cases marked `CHANGED 2026-09-27` used to
+PASS with raw HTML in a lesson and now expect `raw HTML in a lesson`.
 
 ```bash
 pip install -r harness/requirements.txt
diff --git a/harness/tests/neg15.sh b/harness/tests/neg15.sh
index e189a1e..a2c2377 100755
--- a/harness/tests/neg15.sh
+++ b/harness/tests/neg15.sh
@@ -44,7 +44,9 @@ t2() { both 'LESSON=["- **Split by div**","  - Why it matters here: x","<div>bou
 run "2 html-block-splits" "missing do" t2
 # 2b. a type-7 tag (`<span>`) directly under the paragraph is lazy continuation -> PASS
 t2b() { both 'LESSON=["- **Span ok**","  - Why it matters here: x","<span>note</span>","  - Do: y","'"$F"'"]'"$INS"; }
-pos "2b type7-tag-is-lazy-continuation" t2b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the parse this case pinned no longer decides the verdict.
+run "2b type7-tag-is-lazy-continuation -> raw-html-now-fails" "raw HTML in a lesson" t2b
 
 # 3. a 10-digit "ordered marker" is a paragraph, not a list item -> PASS
 t3() { both 'LESSON=["- **Complete**","'"$L3"',"","1234567890. This is an identifier",""]'"$INS"; }
diff --git a/harness/tests/neg23.sh b/harness/tests/neg23.sh
index d095f51..af8e0e1 100755
--- a/harness/tests/neg23.sh
+++ b/harness/tests/neg23.sh
@@ -71,7 +71,9 @@ t4() { split 'LESSON=["- **Html lesson**","  - Why it matters here: x","","    <
 run "G4 nested-html-block-counts" "same title, different content" t4
 # G4+ same block in both copies → PASS
 t4b() { both 'LESSON=["- **Html lesson**","  - Why it matters here: x","","    <div>same in both</div>","  - Do: y",'"$F"']'"$INS"; }
-pos "G4+ nested-html-block-both" t4b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the parse this case pinned no longer decides the verdict.
+run "G4+ nested-html-block-both -> raw-html-now-fails" "raw HTML in a lesson" t4b
 # G4++ nested fenced code with different content differs too (readers see the code)
 t4c() { split 'LESSON=["- **Code lesson**","  - Why it matters here: x","    ```","    print(1)","    ```","  - Do: y",'"$F"']'"$INS" \
               'LESSON=["- **Code lesson**","  - Why it matters here: x","    ```","    print(2)","    ```","  - Do: y",'"$F"']'"$INS"; }
diff --git a/harness/tests/neg26.sh b/harness/tests/neg26.sh
index 713aede..c3ad7e2 100755
--- a/harness/tests/neg26.sh
+++ b/harness/tests/neg26.sh
@@ -43,7 +43,9 @@ pos "J1++ prose-spaces-still-collapse" t1c
 # J2 (4116180191): the opener's 0-3 spaces of indentation are not payload
 t2() { split 'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>same</div>","  - Do: y",'"$F"']'"$INS" \
              'LESSON=["- **Html indent**","  - Why it matters here: x","","      <div>same</div>","  - Do: y",'"$F"']'"$INS"; }
-pos "J2 html-opener-indent-ignored" t2
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the parse this case pinned no longer decides the verdict.
+run "J2 html-opener-indent-ignored -> raw-html-now-fails" "raw HTML in a lesson" t2
 t2b() { split 'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>same</div>","  - Do: y",'"$F"']'"$INS" \
               'LESSON=["- **Html indent**","  - Why it matters here: x","","    <div>other</div>","  - Do: y",'"$F"']'"$INS"; }
 run "J2+ html-content-still-compared" "same title, different content" t2b
diff --git a/harness/tests/neg27.sh b/harness/tests/neg27.sh
index 836e949..26f6dc3 100755
--- a/harness/tests/neg27.sh
+++ b/harness/tests/neg27.sh
@@ -62,7 +62,9 @@ pos "K3++ same-definition-case-insensitive" t3c
 t4() { both 'LESSON=["- **Attr conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A <br title=\"confidence 80%\">"]'"$INS"; }
 run "K4 confidence-in-attribute" "source line is not" t4
 t4b() { both 'LESSON=["- **Tag then conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) — A <br> confidence 80%"]'"$INS"; }
-pos "K4+ visible-confidence-after-tag" t4b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the parse this case pinned no longer decides the verdict.
+run "K4+ visible-confidence-after-tag -> raw-html-now-fails" "raw HTML in a lesson" t4b
 
 echo "----"
 echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
diff --git a/harness/tests/neg29.sh b/harness/tests/neg29.sh
index c243193..aada89b 100644
--- a/harness/tests/neg29.sh
+++ b/harness/tests/neg29.sh
@@ -42,7 +42,9 @@ t1c() { split 'LESSON=["- **Block textarea**","  - Why it matters here: x","  -
               'LESSON=["- **Block textarea**","  - Why it matters here: x","  - Do: y","","    <textarea>","    a   b","    </textarea>","",'"$F"']'"$INS"; }
 run "N1++ textarea-block-whitespace-is-visible" "same title, different content" t1c
 t1d() { both 'LESSON=["- **Textarea**","  - Why it matters here: x","  - Do: type <textarea>a  b</textarea>",'"$F"']'"$INS"; }
-pos "N1+++ identical-textarea-copies-pass" t1d
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "N1+++ identical-textarea-copies-pass -> raw-html-now-fails" "raw HTML in a lesson" t1d
 
 # N3 (4117161124): a source link needs visible link text
 t3() { both 'LESSON=["- **Empty label**","  - Why it matters here: x","  - Do: y","  - Source: [](https://x.y) — A · confidence 80%"]'"$INS"; }
diff --git a/harness/tests/neg30.sh b/harness/tests/neg30.sh
index 9cf2dd8..6a105d7 100644
--- a/harness/tests/neg30.sh
+++ b/harness/tests/neg30.sh
@@ -40,7 +40,9 @@ run "P1++ do-only-inside-template" "missing or empty do" t1c
 t1d() { both 'LESSON=["- **Block why**","  - Why it matters here:","","    <div hidden>x</div>","","  - Do: y",'"$F"']'"$INS"; }
 run "P1+++ why-only-a-raw-html-block" "missing or empty why" t1d
 t1e() { both 'LESSON=["- **Kbd do**","  - Why it matters here: x","  - Do: press <kbd>Ctrl</kbd> twice",'"$F"']'"$INS"; }
-pos "P1++++ text-beside-raw-html-counts" t1e
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "P1++++ text-beside-raw-html-counts -> raw-html-now-fails" "raw HTML in a lesson" t1e
 t1f() { both 'LESSON=["- **Hidden title**","  - Why it matters here: x","  - Do: y","  - Source: [<span hidden>T</span>](https://x.y) — A · confidence 80%"]'"$INS"; }
 run "P1+++++ link-title-only-inside-raw-html" "source line is not" t1f
 
diff --git a/harness/tests/neg31.sh b/harness/tests/neg31.sh
index ee2e007..9963f84 100644
--- a/harness/tests/neg31.sh
+++ b/harness/tests/neg31.sh
@@ -34,7 +34,9 @@ F='"  - Source: [T](https://x.y) — A · confidence 50%"'
 t2() { both 'LESSON=["- **Script attr**","  - Why it matters here: x","  - Do: y","","    <script data-end=\"</script>\">","",'"$F"']'"$INS"; }
 run "Q2 quoted-end-marker-leaves-script-open" "HTML block never closes" t2
 t2b() { both 'LESSON=["- **Script ok**","  - Why it matters here: x","  - Do: y","","    <script>x</script>","",'"$F"']'"$INS"; }
-pos "Q2+ closed-script-block" t2b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q2+ closed-script-block -> raw-html-now-fails" "raw HTML in a lesson" t2b
 
 # Q3 (4117305795): the confidence label must stand alone after the link text too
 t3() { both 'LESSON=["- **Glued conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y)confidence 80%"]'"$INS"; }
@@ -46,18 +48,24 @@ pos "Q3+ punctuation-before-label" t3b
 t4() { both 'LESSON=["- **Bogus end**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <span hidden>x</bogus>confidence 80%</span>"]'"$INS"; }
 run "Q4 unmatched-end-tag-keeps-hidden-region" "source line is not" t4
 t4b() { both 'LESSON=["- **Closed span**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <kbd>K</kbd> · confidence 80%"]'"$INS"; }
-pos "Q4+ label-after-a-closed-element" t4b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q4+ label-after-a-closed-element -> raw-html-now-fails" "raw HTML in a lesson" t4b
 
 # Q5 (4117383771): the title is what a reader sees in the bold span
 t5() { both 'LESSON=["- **<span hidden>Invisible</span>**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
 run "Q5 hidden-title" "not-a-bold-lesson-bullet" t5
 t5b() { both 'LESSON=["- **Press <kbd>K</kbd>**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
-pos "Q5+ title-with-visible-text-beside-raw-html" t5b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q5+ title-with-visible-text-beside-raw-html -> raw-html-now-fails" "raw HTML in a lesson" t5b
 
 # Q6 (4117383777): whitespace collapses across inline element boundaries, as in a browser
 t6() { split 'LESSON=["- **Inline space**","  - Why it matters here: x","  - Do: x <span> y</span>",'"$F"']'"$INS" \
              'LESSON=["- **Inline space**","  - Why it matters here: x","  - Do: x<span> y</span>",'"$F"']'"$INS"; }
-pos "Q6 space-across-raw-inline-boundary" t6
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q6 space-across-raw-inline-boundary -> raw-html-now-fails" "raw HTML in a lesson" t6
 t6b() { split 'LESSON=["- **Link space**","  - Why it matters here: x","  - Do: x [ y](https://x.y)",'"$F"']'"$INS" \
               'LESSON=["- **Link space**","  - Why it matters here: x","  - Do: x[ y](https://x.y)",'"$F"']'"$INS"; }
 pos "Q6+ space-across-link-boundary" t6b
@@ -78,7 +86,9 @@ pos "Q7+++ code-block-is-a-value" t7d
 # Q8 (4117383783): after </svg>, CDATA is an HTML bogus comment again (invisible)
 t8() { split 'LESSON=["- **After svg**","  - Why it matters here: x","  - Do: y <svg></svg><![CDATA[A]]>",'"$F"']'"$INS" \
              'LESSON=["- **After svg**","  - Why it matters here: x","  - Do: y <svg></svg><![CDATA[B]]>",'"$F"']'"$INS"; }
-pos "Q8 cdata-after-closed-svg-is-hidden" t8
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q8 cdata-after-closed-svg-is-hidden -> raw-html-now-fails" "raw HTML in a lesson" t8
 t8b() { split 'LESSON=["- **In svg**","  - Why it matters here: x","  - Do: y <svg><![CDATA[A]]></svg>",'"$F"']'"$INS" \
               'LESSON=["- **In svg**","  - Why it matters here: x","  - Do: y <svg><![CDATA[B]]></svg>",'"$F"']'"$INS"; }
 run "Q8+ cdata-inside-svg-is-compared" "same title, different content" t8b
@@ -86,15 +96,21 @@ run "Q8+ cdata-inside-svg-is-compared" "same title, different content" t8b
 # Q9 (4117383785): a `>` inside a quoted DOCTYPE identifier ENDS the DOCTYPE (WHATWG abrupt-doctype-public-identifier;
 #    html5lib and Chromium 141 both show `confidence 80%">`), so the label is visible and the source is valid
 t9() { both 'LESSON=["- **Doctype conf**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <!DOCTYPE html PUBLIC \"x>confidence 80%\">"]'"$INS"; }
-pos "Q9 gt-in-quoted-doctype-identifier-ends-it" t9
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q9 gt-in-quoted-doctype-identifier-ends-it -> raw-html-now-fails" "raw HTML in a lesson" t9
 
 # Q10 (4117425621): preserved whitespace is scoped to the element that asks for it; a <style> element keeps it lesson-wide
 t10() { split 'LESSON=["- **Scoped**","  - Why it matters here: x","  - Do: a  b <textarea>x</textarea>",'"$F"']'"$INS" \
               'LESSON=["- **Scoped**","  - Why it matters here: x","  - Do: a b <textarea>x</textarea>",'"$F"']'"$INS"; }
-pos "Q10 whitespace-outside-textarea-collapses" t10
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q10 whitespace-outside-textarea-collapses -> raw-html-now-fails" "raw HTML in a lesson" t10
 t10b() { split 'LESSON=["- **Styled scope**","  - Why it matters here: x","  - Do: a  b <span style=\"white-space:pre\">c</span>",'"$F"']'"$INS" \
                'LESSON=["- **Styled scope**","  - Why it matters here: x","  - Do: a b <span style=\"white-space:pre\">c</span>",'"$F"']'"$INS"; }
-pos "Q10+ whitespace-outside-styled-element-collapses" t10b
+# CHANGED 2026-09-27 (raw-HTML ban, T's decision on PR #2): this lesson carries raw HTML other than a
+# comment, so it now FAILs whatever the HTML does; the browser behaviour it pinned no longer decides the verdict.
+run "Q10+ whitespace-outside-styled-element-collapses -> raw-html-now-fails" "raw HTML in a lesson" t10b
 t10c() { split 'LESSON=["- **Sheet**","  - Why it matters here: x","  - Do: a  b <style>li{white-space:pre}</style>",'"$F"']'"$INS" \
                'LESSON=["- **Sheet**","  - Why it matters here: x","  - Do: a b <style>li{white-space:pre}</style>",'"$F"']'"$INS"; }
 run "Q10++ a-style-element-keeps-whitespace-everywhere" "same title, different content" t10c
diff --git a/harness/tests/neg32.sh b/harness/tests/neg32.sh
new file mode 100644
index 0000000..11dd2a3
--- /dev/null
+++ b/harness/tests/neg32.sh
@@ -0,0 +1,89 @@
+#!/usr/bin/env bash
+# Round-32 negative tests (Codex review of b30cdaf) and the raw-HTML ban (T's decision, 2026-09-27):
+# lessons are plain Markdown; any raw HTML other than a <!-- comment --> FAILs the lesson.
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+R='raw HTML in a lesson'
+# R0: the ban itself — every kind of raw HTML except comments, inline and as a block
+t0() { both 'LESSON=["- **Inline tag**","  - Why it matters here: x","  - Do: press <kbd>K</kbd>",'"$F"']'"$INS"; }
+run "R0 inline-tag" "$R" t0
+t0b() { both 'LESSON=["- **Block tag**","  - Why it matters here: x","  - Do: y","","    <div>z</div>","",'"$F"']'"$INS"; }
+run "R0+ html-block" "$R" t0b
+t0c() { both 'LESSON=["- **Void tag**","  - Why it matters here: x","  - Do: y<br>z",'"$F"']'"$INS"; }
+run "R0++ void-tag" "$R" t0c
+t0d() { both 'LESSON=["- **Declaration**","  - Why it matters here: x","  - Do: y <!DOCTYPE html>",'"$F"']'"$INS"; }
+run "R0+++ declaration" "$R" t0d
+t0e() { both 'LESSON=["- **PI**","  - Why it matters here: x","  - Do: y <?php x ?>",'"$F"']'"$INS"; }
+run "R0++++ processing-instruction" "$R" t0e
+t0f() { both 'LESSON=["- **CDATA**","  - Why it matters here: x","  - Do: y <![CDATA[ z ]]>",'"$F"']'"$INS"; }
+run "R0+++++ cdata" "$R" t0f
+t0g() { both 'LESSON=["- **Tag in title <b>B</b>**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "R0++++++ tag-in-title" "$R" t0g
+t0h() { both 'LESSON=["- **Comments ok**<!-- a -->","  - Why it matters here: x <!-- b -->","  - Do: y","","    <!-- c -->","",'"$F"']'"$INS"; }
+pos "R0+++++++ comments-inline-and-block-are-allowed" t0h
+t0i() { both 'LESSON=["- **Code ok**","  - Why it matters here: `<span>` in code is text","  - Do: y","","    ```html","    <div>z</div>","    ```","",'"$F"']'"$INS"; }
+pos "R0++++++++ html-inside-code-is-not-raw-html" t0i
+t0j() { both 'LESSON=["- **Autolink ok**","  - Why it matters here: see <https://x.y>","  - Do: y",'"$F"']'"$INS"; }
+pos "R0+++++++++ autolink-is-not-raw-html" t0j
+
+# R1 (4117489564): a title that is inline code is a valid bold title (regression from b30cdaf)
+t1() { both 'LESSON=["- **`Title`**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "R1 code-title" t1
+t1b() { both 'LESSON=["- **Use `make` first**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "R1+ title-with-code" t1b
+
+# R2-R6 (4117489575, 4117489572, 4117489569, 4117489581, 4117489588): raw-HTML semantics in lessons — superseded by the ban
+t2() { split 'LESSON=["- **Style color**","  - Why it matters here: x","  - Do: <span style=\"color:red\">a  b</span>",'"$F"']'"$INS" \
+             'LESSON=["- **Style color**","  - Why it matters here: x","  - Do: <span style=\"color:red\">a b</span>",'"$F"']'"$INS"; }
+run "R2 style-attribute" "$R" t2
+t3() { both 'LESSON=["- **Pop**","  - Why it matters here: x","  - Do: y","  - Source: [T](https://x.y) <span hidden><b>x</span>confidence 80%"]'"$INS"; }
+run "R3 intervening-element" "$R" t3
+t4() { both 'LESSON=["- **Kbd link**","  - Why it matters here: x","  - Do: y","  - Source: [T<kbd>K</kbd>](https://x.y)confidence 80%"]'"$INS"; }
+run "R4 raw-html-at-link-end" "$R" t4
+t5() { both 'LESSON=["- **Hidden div**","","  <div hidden>","","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "R5 open-div-before-fields" "$R" t5
+t6() { split 'LESSON=["- **Char ref**","  - Why it matters here: x","  - Do: <input value=\"A\">",'"$F"']'"$INS" \
+             'LESSON=["- **Char ref**","  - Why it matters here: x","  - Do: <input value=\"&#65;\">",'"$F"']'"$INS"; }
+run "R6 character-reference-in-attribute" "$R" t6
+
+# R7 (4117489584): `</svg/>` is an end tag; the tokenizer leaves foreign content (CDATA is a bogus comment again)
+n=$((n+1))
+if python3 - "$SRC/harness" <<'PY'
+import sys; sys.path.insert(0, sys.argv[1])
+from knowledge_md import html_segments
+segs = html_segments("<svg></svg/><![CDATA[A]]>z")
+assert segs[-2][0] == "hidden" and segs[-1] == ("text", "z", ""), segs
+PY
+then echo "PASS R7 slash-marked-end-tag-leaves-svg"; else bad=$((bad+1)); echo "FAIL R7 slash-marked-end-tag-leaves-svg"; fi
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From 9b395d41c74081d48b789119c0424ea905fa9e57 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 22:19:30 -0400
Subject: [PATCH 08/13] =?UTF-8?q?harness:=20Source=20links=20must=20be=20w?=
 =?UTF-8?q?ritten=20inline,=20[title](https://=E2=80=A6)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex review of 37a18eb: `Source: [Title][src]` with `[src]: https://…`
elsewhere was accepted, although the documented shape is `[title](https://…)`
and the v1 checker required `](`. parse_source() now re-reads the Source
paragraph with an empty reference environment (MD.parseInline(content, {})),
so a full, collapsed or shortcut reference link is plain text there and the
field FAILs with `source line is not …`. Reference links elsewhere in a
lesson are unaffected.

neg32 R8 / R8+ reproduce it on 37a18eb (both passed there) and FAIL now;
R8++ keeps a reference link in a Why field passing. cases.json pins the
parse (`undefined-reference-is-text`; 62 cases, 652/652 spec examples).
22 suites, 297 cases, 0 failing. Mutation check on 37a18eb: 22 of 22 caught.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                      | 2 +-
 README.md                      | 6 +++---
 harness/check-knowledge.py     | 6 ++++--
 harness/conformance/cases.json | 5 +++++
 harness/tests/neg32.sh         | 8 ++++++++
 5 files changed, 21 insertions(+), 6 deletions(-)

diff --git a/LEDGER.md b/LEDGER.md
index 654bea7..0326208 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 685 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 61 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases, 294 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 687 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 62 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 2573e0b..e4d1243 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 294 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 61 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
+| `harness/tests/` | 297 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 62 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 294 negative-test cases
+bash harness/tests/run.sh                   # the 297 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index be462cd..58a6f14 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -332,8 +332,10 @@ def lead_text(tokens) -> str:
 
 def parse_source(inline) -> int | None:
     """Confidence N for a Source field rendering as `Source: <link to http(s)> … confidence N%`,
-    else None. Only visible text after the link counts — never link text, code or HTML attributes."""
-    toks = [c for c in inline_tokens(inline) if not invisible_inline(c)]
+    else None. Only visible text after the link counts — never link text, code or HTML attributes.
+    The link must be written inline, `[title](https://…)`: the paragraph is re-read without the
+    file's reference definitions, so a `[title][ref]` link does not count."""
+    toks = [c for c in inline_tokens(MD.parseInline(inline.content, {})[0]) if not invisible_inline(c)]
     k, lead = 0, ""
     while k < len(toks) and toks[k].type == "text":
         lead += toks[k].content
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 30ba371..d3c87f0 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -303,5 +303,10 @@
   "id": "link-text-leading-space",
   "markdown": "Do: x[ y](https://x.y)\n",
   "why": "a link whose text starts with a space"
+ },
+ {
+  "id": "undefined-reference-is-text",
+  "markdown": "Source: [Title][src] — A · confidence 80%\n",
+  "why": "without its definition a reference link is plain text (how the checker re-reads a Source line)"
  }
 ]
diff --git a/harness/tests/neg32.sh b/harness/tests/neg32.sh
index 11dd2a3..e1f521b 100644
--- a/harness/tests/neg32.sh
+++ b/harness/tests/neg32.sh
@@ -74,6 +74,14 @@ t6() { split 'LESSON=["- **Char ref**","  - Why it matters here: x","  - Do: <in
              'LESSON=["- **Char ref**","  - Why it matters here: x","  - Do: <input value=\"&#65;\">",'"$F"']'"$INS"; }
 run "R6 character-reference-in-attribute" "$R" t6
 
+# R8 (4117888333): the source link must be inline `[title](https://…)` — a reference-style link does not count
+t8() { both 'LESSON=["- **Ref source**","  - Why it matters here: x","  - Do: y","  - Source: [Title][src] — A · confidence 80%","","[src]: https://x.y"]'"$INS"; }
+run "R8 full-reference-source-link" "source line is not" t8
+t8b() { both 'LESSON=["- **Shortcut source**","  - Why it matters here: x","  - Do: y","  - Source: [src] — A · confidence 80%","","[src]: https://x.y"]'"$INS"; }
+run "R8+ shortcut-reference-source-link" "source line is not" t8b
+t8c() { both 'LESSON=["- **Ref in why**","  - Why it matters here: see [docs][src]","  - Do: y",'"$F"',"","[src]: https://x.y"]'"$INS"; }
+pos "R8++ reference-links-elsewhere-still-fine" t8c
+
 # R7 (4117489584): `</svg/>` is an end tag; the tokenizer leaves foreign content (CDATA is a bogus comment again)
 n=$((n+1))
 if python3 - "$SRC/harness" <<'PY'

From 418720018b848ab07abbda253e348d3490fddb67 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 22:32:34 -0400
Subject: [PATCH 09/13] harness: round-33 review fixes; conformance exceptions
 pinned; inline tags follow the spec on NBSP

Codex review of 9b395d4; reproduced first by harness/tests/neg33.sh (3 of 7
cases failed on 9b395d4; 7 of 7 pass now):

- `[T`K`](https://x.y)confidence 80%` passed: the link text's last
  character now includes inline-code text, so code glues to the next word
  just as text does (S1; S1+ with a space still passes).
- `# <span hidden>Invisible</span>` counted as a verified sheet's H1 text:
  the H1 text now uses the same visibility rules as lesson titles (S3; code
  and text beside raw HTML still count, S3+ / S3++).
- `- <!-- note -->` alone on the item's first line, then `**Title**`: the
  comment block ends before the title (commonmark.js agrees, pinned as
  `comment-block-then-title`), so invisible blocks are skipped when finding
  the title and the fields (S4; S4+ keeps a plain title failing).
- harness/conformance/check.py allowlisted divergences by id only. Each
  exception now pins our exact output, which must match and must still differ
  from commonmark.js; a wrong pin FAILs the gate (checked with a tampered pin).

Pinning exposed one more gap: markdown-it-py 4.0.0 let Python's `\s` (NBSP
included) separate a raw INLINE tag's parts, so `<span\xa0a=b>` became raw
HTML where spec 0.31.2 section 6.6 and cmark-gfm (GitHub) make it text.
knowledge_md._patch_html_inline() rebuilds the inline tag pattern with
spaces, tabs and a line ending only. Our three NBSP cases now render
byte-for-byte like cmark-gfm 0.29.0.gfm.13 (new case `nbsp-in-inline-tag`).

23 suites, 304 cases, 0 failing; 652/652 spec examples, 64 edge cases;
html5lib oracle 100,000 fragments, 0 divergences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                      |  2 +-
 README.md                      |  6 ++--
 harness/check-knowledge.py     |  9 +++---
 harness/conformance/cases.json | 10 +++++++
 harness/conformance/check.py   | 27 ++++++++++-------
 harness/knowledge_md.py        | 18 +++++++++++
 harness/tests/README.md        |  2 +-
 harness/tests/neg33.sh         | 55 ++++++++++++++++++++++++++++++++++
 8 files changed, 110 insertions(+), 19 deletions(-)
 create mode 100644 harness/tests/neg33.sh

diff --git a/LEDGER.md b/LEDGER.md
index 0326208..7983c0c 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 687 lines + 176-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 62 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 688 lines + 194-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 64 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases, 304 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index e4d1243..06e1004 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 297 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 62 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
+| `harness/tests/` | 304 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 64 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 297 negative-test cases
+bash harness/tests/run.sh                   # the 304 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 58a6f14..705edcd 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -351,7 +351,7 @@ def parse_source(inline) -> int | None:
     close = next((j for j in range(k + 1, len(toks)) if toks[j].type == "link_close"), None)
     if close is None or not visible_text(toks[k + 1:close]).replace(SPLIT, "").strip(WS):   # needs a visible title
         return None
-    tail = visible_text(toks[k + 1:close])[-1:] + visible_text(toks[close + 1:])   # the link text's last character
+    tail = visible_text(toks[k + 1:close], code_text=True)[-1:] + visible_text(toks[close + 1:])   # link text's last char
     m = CONFIDENCE_RE.search(tail, 1)                                              # takes part in the word boundary
     return int(m.group(1)) if m else None
 
@@ -413,7 +413,8 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
     lesson = {"problems": [], "key": doc.html(item), "parts": {}, "seen": {k: False for k in FIELDS}}
     if raw_html_in(doc, item):
         lesson["problems"].append("raw HTML in a lesson (only <!-- comments --> are allowed)")
-    head = item.kids[0] if item.kids and item.kids[0].kind == "paragraph" else None
+    kids = [k for k in item.kids if visible(k)]            # a comment-only block before the title is invisible
+    head = kids[0] if kids and kids[0].kind == "paragraph" else None
     title, continues = title_of(head.kids[0].t) if head and item.t.markup == "-" else (None, False)
     if title is None:
         raw = MARKER_RE.sub("", doc.line(item), count=1).strip(WS)
@@ -425,7 +426,7 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
     if continues:
         lesson["problems"].append("title paragraph continues past the bold span")
     order: list[str] = []
-    for block in item.kids[1:]:
+    for block in kids[1:]:
         if block.kind != "bullet_list" or block.t.markup != "-":
             continue
         for field in block.kids:
@@ -665,7 +666,7 @@ def compare(k_lessons: list[dict], d_lessons: list[dict]) -> str | None:
             continue
         vdoc = Doc(text)
         head = next((n for n in vdoc.top if visible(n)), None)
-        heading_text = ("".join(c.content for c in head.kids[0].t.children if c.type in ("text", "code_inline"))
+        heading_text = (visible_text(inline_tokens(head.kids[0].t), code_text=True).replace(SPLIT, "").replace(SEP, "")
                         if head and head.kind == "heading" and head.t.tag == "h1" and head.t.markup == "#" else "")
         if not heading_text.strip(WS):
             noh1.append(p.name)
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index d3c87f0..9f133a8 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -308,5 +308,15 @@
   "id": "undefined-reference-is-text",
   "markdown": "Source: [Title][src] — A · confidence 80%\n",
   "why": "without its definition a reference link is plain text (how the checker re-reads a Source line)"
+ },
+ {
+  "id": "nbsp-in-inline-tag",
+  "markdown": "x <span a=b> y\n",
+  "why": "a NBSP before an attribute does not make an inline raw tag"
+ },
+ {
+  "id": "comment-block-then-title",
+  "markdown": "- <!-- note -->\n  **Own-line comment**\n  - Why it matters here: x\n",
+  "why": "a comment alone on the item's first line ends its HTML block; the next line is a Markdown paragraph"
  }
 ]
diff --git a/harness/conformance/check.py b/harness/conformance/check.py
index b1079f6..7fdf35f 100644
--- a/harness/conformance/check.py
+++ b/harness/conformance/check.py
@@ -19,14 +19,18 @@
 # `<blockquote>\n</blockquote>`). Both sides are compared with exactly that newline removed;
 # every other byte must match.
 BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|pre|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<!--)")
-# Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each must
-# still diverge (a stale entry fails the gate), so the list cannot silently outlive its reason.
+# Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each entry
+# pins OUR exact output (cmark-gfm 0.29.0.gfm.13's output, checked 2026-09-27) and must still differ
+# from the reference, so an entry can neither hide a regression nor outlive its reason.
 ALLOWED = {
-    "nbsp-after-tag": "commonmark.js matches HTML-block starts with JS `\\s`, which includes U+00A0; spec 0.31.2 "
-                      "section 4.6 allows only space, tab, end of line, `>` or `/>` after the tag name. cmark and "
-                      "cmark-gfm (GitHub) follow the spec: paragraph.",
-    "type7-nbsp": "same root cause: an attribute must be preceded by space, tab or a line ending (spec 6.6); "
-                  "cmark and cmark-gfm render a paragraph.",
+    "nbsp-after-tag": ("commonmark.js matches HTML-block starts with JS `\\s`, which includes U+00A0; spec 0.31.2 "
+                       "section 4.6 allows only space, tab, end of line, `>` or `/>` after the tag name, and 6.6 "
+                       "only spaces, tabs and a line ending inside a tag. cmark and cmark-gfm (GitHub): text.",
+                       "<p>&lt;div\xa0class=x&gt;\ntext</p>\n"),
+    "type7-nbsp": ("same root cause: an attribute must be preceded by space, tab or a line ending (spec 6.6); "
+                   "cmark and cmark-gfm render the tag as text.", "<p>&lt;span\xa0a=b&gt;</p>\n"),
+    "nbsp-in-inline-tag": ("same root cause inline: commonmark.js takes `<span` + U+00A0 + `a=b>` as raw HTML; spec 6.6 and "
+                           "cmark-gfm make it text.", "<p>x &lt;span\xa0a=b&gt; y</p>\n"),
 }
 
 
@@ -37,7 +41,10 @@ def canon(html: str) -> str:
 bad, allowed_hit = [], []
 for group in ("spec", "cases"):
     for ex in ref[group]:
-        if canon(MD.render(ex["markdown"])) != canon(ex["html"]):
+        ours = canon(MD.render(ex["markdown"]))
+        if ex["id"] in ALLOWED and ours != canon(ALLOWED[ex["id"]][1]):
+            bad.append(ex["id"])                           # an exception must render exactly as pinned
+        elif ours != canon(ex["html"]):
             (allowed_hit if ex["id"] in ALLOWED else bad).append(ex["id"])
 n_spec, n_cases = len(ref["spec"]), len(ref["cases"])
 def differing(group_is_spec: bool) -> int:
@@ -49,9 +56,9 @@ def differing(group_is_spec: bool) -> int:
 for b in bad:
     ex = next(e for g in ("spec", "cases") for e in ref[g] if e["id"] == b)
     print(f"  DIVERGES {b}: {ex.get('why', ex.get('section'))}\n    input {ex['markdown']!r}\n    ref   {ex['html']!r}\n    ours  {MD.render(ex['markdown'])!r}")
-stale = sorted(set(ALLOWED) - set(allowed_hit))
+stale = sorted(set(ALLOWED) - set(allowed_hit) - set(bad))   # a mis-pinned entry is reported above
 for a in sorted(allowed_hit):
-    print(f"  allowed {a}: {ALLOWED[a]}")
+    print(f"  allowed {a}: {ALLOWED[a][0]}")
 if stale:
     print(f"FAIL conformance:allowlist      allowed divergence(s) no longer diverge — remove them: {stale}")
 print("----")
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index 4312103..d624626 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -14,7 +14,9 @@
 import re
 import sys
 
+import markdown_it.common.html_re  # noqa: F401  (the inline tag pattern rebuilt below)
 import markdown_it.rules_block  # noqa: F401  (loads the html_block module patched below)
+import markdown_it.rules_inline.html_inline  # noqa: F401  (whose tag pattern is replaced below)
 from markdown_it import MarkdownIt
 from markdown_it.rules_block.reference import reference as _reference
 
@@ -51,7 +53,23 @@ def _patch_html_blocks() -> None:
         seqs[k] = (re.compile(pattern, start.flags), end, can_interrupt)
 
 
+def _patch_html_inline() -> None:
+    """markdown-it-py 4.0.0 lets Python's `\\s` (which matches a NBSP) separate a raw inline tag's
+    name, attributes and `=`; CommonMark 0.31.2 allows only spaces, tabs and a line ending, so
+    `<span\\xa0a=b>` is literal text (cmark and cmark-gfm agree). Rebuild the inline tag pattern
+    with that whitespace; comments, PIs, declarations and CDATA are unchanged. Pinned by cases.json
+    (nbsp-in-inline-tag)."""
+    h = sys.modules["markdown_it.common.html_re"]
+    ws = "[ \\t\\n]"
+    attribute = "(?:" + ws + "+" + h.attr_name + "(?:" + ws + "*=" + ws + "*" + h.attr_value + ")?)"
+    open_tag = "<[A-Za-z][A-Za-z0-9\\-]*" + attribute + "*" + ws + "*\\/?>"
+    close_tag = "<\\/[A-Za-z][A-Za-z0-9\\-]*" + ws + "*>"
+    sys.modules["markdown_it.rules_inline.html_inline"].HTML_TAG_RE = re.compile(
+        "^(?:" + "|".join((open_tag, close_tag, h.comment, h.processing, h.declaration, h.cdata)) + ")")
+
+
 _patch_html_blocks()
+_patch_html_inline()
 MD = MarkdownIt("commonmark")
 MD.block.ruler.at("reference", _reference_999)
 
diff --git a/harness/tests/README.md b/harness/tests/README.md
index dcc5ee0..8653c13 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,7 +10,7 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 32 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 33 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
diff --git a/harness/tests/neg33.sh b/harness/tests/neg33.sh
new file mode 100644
index 0000000..aa4357c
--- /dev/null
+++ b/harness/tests/neg33.sh
@@ -0,0 +1,55 @@
+#!/usr/bin/env bash
+# Round-33 negative tests (Codex review of 9b395d4).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# S1 (4118023210): a link title ending in inline code still glues to a following word
+t1() { both 'LESSON=["- **Code glue**","  - Why it matters here: x","  - Do: y","  - Source: [T`K`](https://x.y)confidence 80%"]'"$INS"; }
+run "S1 code-at-link-end-glues-the-label" "source line is not" t1
+t1b() { both 'LESSON=["- **Code spaced**","  - Why it matters here: x","  - Do: y","  - Source: [T`K`](https://x.y) confidence 80%"]'"$INS"; }
+pos "S1+ code-at-link-end-then-space" t1b
+
+# S3 (4118023220): a verified sheet's H1 needs visible text (raw HTML is still allowed in verified sheets)
+t3() { printf '# <span hidden>Invisible</span>\n\nbody\n' > verified/2026-01-22_hidden-h1_v1.md; }
+run "S3 hidden-text-h1" "verified:h1" t3
+t3b() { printf '# `code` title\n\nbody\n' > verified/2026-01-23_code-h1_v1.md; }
+pos "S3+ code-h1-is-visible" t3b
+t3c() { printf '# Title <span hidden>x</span>\n\nbody\n' > verified/2026-01-24_mixed-h1_v1.md; }
+pos "S3++ visible-text-beside-raw-html" t3c
+
+# S4 (4118023229): a comment in its own first block before the title is invisible; the title is still Markdown
+t4() { both 'LESSON=["- <!-- note -->","  **Own-line comment**","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "S4 comment-block-before-title" t4
+t4b() { both 'LESSON=["- <!-- note -->","  plain title","  - Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+run "S4+ comment-block-then-plain-title" "not-a-bold-lesson-bullet" t4b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From 868313f4e42769a632474ded7a2a79cc83b29e68 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 22:47:42 -0400
Subject: [PATCH 10/13] =?UTF-8?q?harness:=20round-34=20=E2=80=94=20keep=20?=
 =?UTF-8?q?<pre>=20newlines=20in=20the=20conformance=20gate;=20ASCII=20whi?=
 =?UTF-8?q?tespace=20in=20tag=20patches?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex review of 4187200:
- harness/conformance/check.py canonicalized the newline before `</pre>`,
  which is visible content, so a parser dropping it would still match.
  Newlines are now only canonicalized outside <pre> (the one needed case,
  before `<pre>`, stays). neg34 T4 checks `<pre>x\n</pre>` vs `<pre>x</pre>`
  differ; it fails against 4187200's gate and passes now.
- `# <span>Visible</span>` in a verified sheet stays a FAIL, by design: the
  H1's text must be Markdown text or code, the same rule as lesson titles
  (raw HTML may hide what it wraps). The diagnostic now says so; neg34 T1.

Kilo review of 4187200 (first run with credits):
- "critical" lead_text/visible_text offset mismatch: does not reproduce.
  lead_text() stops at the first token visible_text() would not render as
  plain text, so it is always a prefix; neg34 T3 asserts it on 10 inputs,
  with and without code text. Documented in lead_text()'s docstring.
- "warning" IndexError on an empty `# ` heading: does not reproduce; the
  heading always has an inline child. neg34 T2 / T2+ pin a clean FAIL.
- suggestion on CR/FF in the inline tag pattern: CR never reaches inline
  parsing (normalized to LF), but checking FF against the references showed
  a real gap of mine: since 78d12a9 the block-start patch replaced `\s`
  with `[ \t]`, and 4187200's inline patch with `[ \t\n]`, while cmark
  (spacechar), cmark-gfm and commonmark.js all accept a form feed or
  vertical tab there. Both patches now use ASCII whitespace
  [ \t\n\r\f\v]; NBSP stays excluded (spec + cmark). 5 new conformance
  cases (ff/vt inline, after a block tag, type 7) diverged on 4187200 and
  are identical to commonmark.js now.
- suggestion/nitpick comment wording (foreign end tags, the whitespace
  allowance in only_comments): reworded.

24 suites, 309 cases, 0 failing; 652/652 spec examples, 69 edge cases
(3 pinned exceptions); html5lib oracle 100,000 fragments, 0 divergences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                      |  2 +-
 README.md                      |  6 +--
 harness/check-knowledge.py     | 10 +++--
 harness/conformance/cases.json | 25 ++++++++++++
 harness/conformance/check.py   | 10 +++--
 harness/knowledge_md.py        | 25 +++++++-----
 harness/tests/README.md        |  2 +-
 harness/tests/neg34.sh         | 72 ++++++++++++++++++++++++++++++++++
 8 files changed, 129 insertions(+), 23 deletions(-)
 create mode 100644 harness/tests/neg34.sh

diff --git a/LEDGER.md b/LEDGER.md
index 7983c0c..aa790f2 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 688 lines + 194-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 64 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases, 304 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 690 lines + 199-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 69 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases); 10th push answers Codex + Kilo on 4187200 (newlines inside `<pre>` kept in the conformance canonicalizer; raw-HTML-only H1 text stays a FAIL by the same rule as lessons; Kilo's index and crash reports disproved by T2/T3), round-34 suite (5 cases, 309 total); the HTML-tag whitespace patches now take ASCII whitespace (form feed / vertical tab were wrongly rejected since 78d12a9; 5 new conformance cases) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 06e1004..c3c8736 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 304 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 64 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
+| `harness/tests/` | 309 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 69 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 304 negative-test cases
+bash harness/tests/run.sh                   # the 309 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 705edcd..023057e 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -320,7 +320,9 @@ def visible_text(tokens, code_text: bool = False) -> str:
 
 
 def lead_text(tokens) -> str:
-    """The paragraph's leading plain text (invisible raw HTML skipped): where a field label must sit."""
+    """The paragraph's leading plain text (invisible raw HTML skipped): where a field label must sit.
+    It stops at the first token visible_text() would not render as plain text, so it is always a
+    prefix of visible_text(tokens) and a match offset in it is valid there too (neg34 T3 checks)."""
     out = []
     for c in tokens:
         if c.type == "text":
@@ -398,8 +400,8 @@ def raw_html_in(doc: Doc, n: Node) -> bool:
     """Does this block hold raw HTML other than `<!-- comments -->`? Lessons are plain Markdown:
     raw HTML can hide, restyle or swallow what a reader sees, so only comments are allowed."""
     def only_comments(html: str) -> bool:
-        return all((kind == "hidden" and src.startswith("<!--")) or (kind == "text" and not src.strip(WS))
-                   for kind, src, _ in html_segments(html))
+        return all((kind == "hidden" and src.startswith("<!--")) or (kind == "text" and not src.strip(WS))  # blank
+                   for kind, src, _ in html_segments(html))                 # text: the padding of a comment block
     for t in doc.tokens[n.i:n.j + 1]:
         if t.type == "html_block" and not only_comments(t.content):
             return True
@@ -671,7 +673,7 @@ def compare(k_lessons: list[dict], d_lessons: list[dict]) -> str | None:
         if not heading_text.strip(WS):
             noh1.append(p.name)
     if noh1:
-        fail("verified:h1", f"first visible block is not an ATX H1 with text (comments and reference definitions ignored; code and raw HTML count): {noh1}")
+        fail("verified:h1", f"first visible block is not an ATX H1 with text (comments and reference definitions ignored; code and raw HTML count as blocks; the H1's text must be Markdown text or code, not text inside raw HTML): {noh1}")
     elif vfiles:
         ok("verified:h1", "every sheet starts with an H1")
 
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 9f133a8..736f41d 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -318,5 +318,30 @@
   "id": "comment-block-then-title",
   "markdown": "- <!-- note -->\n  **Own-line comment**\n  - Why it matters here: x\n",
   "why": "a comment alone on the item's first line ends its HTML block; the next line is a Markdown paragraph"
+ },
+ {
+  "id": "ff-in-inline-tag",
+  "markdown": "x <span\fa=b> y\n",
+  "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
+ },
+ {
+  "id": "vt-in-inline-tag",
+  "markdown": "x <span\u000ba=b> y\n",
+  "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
+ },
+ {
+  "id": "ff-after-block-tag",
+  "markdown": "<div\fclass=x>\ntext\n",
+  "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
+ },
+ {
+  "id": "ff-type7-tag",
+  "markdown": "<span\fa=b>\n",
+  "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
+ },
+ {
+  "id": "vt-after-block-tag",
+  "markdown": "<div\u000bx>\ntext\n",
+  "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
  }
 ]
diff --git a/harness/conformance/check.py b/harness/conformance/check.py
index 7fdf35f..ac7e5cf 100644
--- a/harness/conformance/check.py
+++ b/harness/conformance/check.py
@@ -16,9 +16,10 @@
 
 # Renderer whitespace that no browser shows: the two libraries place a newline differently
 # right before some block-level tags (`<li>b<pre>` vs `<li>b\n<pre>`, an empty
-# `<blockquote>\n</blockquote>`). Both sides are compared with exactly that newline removed;
-# every other byte must match.
-BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|pre|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<!--)")
+# `<blockquote>\n</blockquote>`). Both sides are compared with exactly that newline removed —
+# never inside <pre>, where a newline is visible; every other byte must match.
+BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<pre[\s>])|\n(?=<!--)")
+PRE = re.compile(r"<pre[\s>].*?(?=</pre>)", re.S)   # a newline inside <pre> is visible: never canonicalized
 # Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each entry
 # pins OUR exact output (cmark-gfm 0.29.0.gfm.13's output, checked 2026-09-27) and must still differ
 # from the reference, so an entry can neither hide a regression nor outlive its reason.
@@ -35,7 +36,8 @@
 
 
 def canon(html: str) -> str:
-    return BLOCK_NL.sub("", html)
+    inside = [m.span() for m in PRE.finditer(html)]
+    return BLOCK_NL.sub(lambda m: m.group(0) if any(a < m.start() < b for a, b in inside) else "", html)
 
 ref = json.load(open(sys.argv[1], encoding="utf-8"))
 bad, allowed_hit = [], []
diff --git a/harness/knowledge_md.py b/harness/knowledge_md.py
index d624626..47a7821 100644
--- a/harness/knowledge_md.py
+++ b/harness/knowledge_md.py
@@ -41,26 +41,31 @@ def _reference_999(state, start: int, end: int, silent: bool) -> bool:
     return True
 
 
+# ASCII whitespace: what cmark's `spacechar` and commonmark.js accept inside and after a tag name.
+# Python's `\\s` also matches a NBSP and other Unicode spaces, which neither reference accepts.
+ASCII_WS = "[ \\t\\n\\r\\f\\v]"
+
+
 def _patch_html_blocks() -> None:
     """markdown-it-py 4.0.0 starts a type-4 (declaration) HTML block only for an UPPERCASE letter
     after `<!` — CommonMark 0.31.2 takes any ASCII letter — and uses Python's `\\s`, which also
-    matches a NBSP, where the spec allows only space and tab. Rewrite those start patterns in
-    place (the rule reads the module table at call time). Pinned by cases.json
-    (lowercase-decl, nbsp-after-tag, type7-nbsp)."""
+    matches a NBSP. Rewrite those start patterns in place with ASCII whitespace (the rule reads the
+    module table at call time). Pinned by cases.json (lowercase-decl, nbsp-after-tag, type7-nbsp,
+    ff-after-block-tag, ff-type7-tag, vt-after-block-tag)."""
     seqs = sys.modules["markdown_it.rules_block.html_block"].HTML_SEQUENCES
     for k, (start, end, can_interrupt) in enumerate(seqs):
-        pattern = start.pattern.replace("<![A-Z]", "<![A-Za-z]").replace(r"\s", "[ \t]")
+        pattern = start.pattern.replace("<![A-Z]", "<![A-Za-z]").replace(r"\s", ASCII_WS)
         seqs[k] = (re.compile(pattern, start.flags), end, can_interrupt)
 
 
 def _patch_html_inline() -> None:
     """markdown-it-py 4.0.0 lets Python's `\\s` (which matches a NBSP) separate a raw inline tag's
-    name, attributes and `=`; CommonMark 0.31.2 allows only spaces, tabs and a line ending, so
-    `<span\\xa0a=b>` is literal text (cmark and cmark-gfm agree). Rebuild the inline tag pattern
-    with that whitespace; comments, PIs, declarations and CDATA are unchanged. Pinned by cases.json
-    (nbsp-in-inline-tag)."""
+    name, attributes and `=`; cmark and cmark-gfm take only ASCII whitespace there, so
+    `<span\\xa0a=b>` is literal text. Rebuild the inline tag pattern with ASCII whitespace; comments,
+    PIs, declarations and CDATA are unchanged. Pinned by cases.json (nbsp-in-inline-tag,
+    ff-in-inline-tag, vt-in-inline-tag)."""
     h = sys.modules["markdown_it.common.html_re"]
-    ws = "[ \\t\\n]"
+    ws = ASCII_WS
     attribute = "(?:" + ws + "+" + h.attr_name + "(?:" + ws + "*=" + ws + "*" + h.attr_value + ")?)"
     open_tag = "<[A-Za-z][A-Za-z0-9\\-]*" + attribute + "*" + ws + "*\\/?>"
     close_tag = "<\\/[A-Za-z][A-Za-z0-9\\-]*" + ws + "*>"
@@ -140,7 +145,7 @@ def html_segments(html: str) -> list[tuple[str, str, str]]:
             k = n if k < 0 else k
             end = html[j + 1] == "/"
             out.append(("end" if end else "start", html[j:k], name))
-            if name in ("svg", "math"):                     # `/>` self-closes a foreign START tag only
+            if name in ("svg", "math"):  # `/>` self-closes a foreign start tag; `</svg/>` is just an end tag
                 foreign = max(0, foreign - 1) if end else foreign + (not html[j:k].endswith("/>"))
             if not end and (name in RAWTEXT_TAGS or name == "plaintext"):
                 close = None if name == "plaintext" else re.compile(
diff --git a/harness/tests/README.md b/harness/tests/README.md
index 8653c13..0ea5b81 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,7 +10,7 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 33 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 34 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
diff --git a/harness/tests/neg34.sh b/harness/tests/neg34.sh
new file mode 100644
index 0000000..cc0dd41
--- /dev/null
+++ b/harness/tests/neg34.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Round-34 negative tests (Codex and Kilo reviews of 4187200).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+
+# T1 (Codex 4118069781): a verified sheet's H1 text must be Markdown text or code — text inside raw HTML never counts
+t1() { printf '# <span>Visible</span>\n\nbody\n' > verified/2026-01-25_span-h1_v1.md; }
+run "T1 raw-html-only-h1-text" "verified:h1" t1
+
+# T2 (Kilo 4118066660): an empty ATX heading is a FAIL, never a crash
+t2() { printf '# \n\nbody\n' > verified/2026-01-26_empty-h1_v1.md; }
+run "T2 empty-h1-fails-cleanly" "verified:h1" t2
+t2b() { printf '#\n\nbody\n' > verified/2026-01-27_bare-h1_v1.md; }
+run "T2+ bare-hash-h1-fails-cleanly" "verified:h1" t2b
+
+# T3 (Kilo 4118066651): lead_text() is always a prefix of visible_text(), so the label's match offset is valid in both
+n=$((n+1))
+if python3 - "$SRC/harness" <<'PY'
+import sys, pathlib
+h = pathlib.Path(sys.argv[1]); sys.path.insert(0, str(h))
+src = (h / "check-knowledge.py").read_text()
+g = {"__file__": str(h / "check-knowledge.py"), "__name__": "ck"}
+exec(compile(src[:src.index("# --- knowledge.md")], "ck", "exec"), g)
+MD, inline_tokens, lead_text, visible_text = g["MD"], g["inline_tokens"], g["lead_text"], g["visible_text"]
+samples = ["Why it matters here: <span>x</span>", "Why it matters here: `x`", "Why<!-- n --> it matters here: x",
+           "Do: ![i](https://x.y/i.png) z", "Do:\nnext line", "Do: **b** <!-- c --> <kbd>k</kbd>", "Source: [T](https://x.y) x",
+           "Why it matters here: <!-- c --><!-- d -->", "Do: a\\\nb", "Do: &amp; &#65; <br> c"]
+for s in samples:
+    toks = inline_tokens(MD.parseInline(s, {})[0])
+    for c in (False, True):
+        assert visible_text(toks, code_text=c).startswith(lead_text(toks)), (s, c)
+PY
+then echo "PASS T3 lead-text-is-a-prefix-of-visible-text"; else bad=$((bad+1)); echo "FAIL T3 lead-text-is-a-prefix-of-visible-text"; fi
+
+# T4 (Codex 4118069784): the conformance canonicalizer never touches a newline inside <pre>
+n=$((n+1))
+if python3 - "$SRC/harness/conformance/check.py" <<'PY'
+import re, sys
+src = open(sys.argv[1]).read()
+ns = {"re": re}
+exec(src[src.index("BLOCK_NL = "):src.index("# Reviewed cases")], ns)
+exec(src[src.index("def canon"):src.index("ref = json.load")], ns)
+c = ns["canon"]
+assert c("<pre>x\n</pre>") != c("<pre>x</pre>")                       # the newline before </pre> is visible
+assert c("<pre><code>x\n</code></pre>\n") != c("<pre><code>x</code></pre>\n")
+assert c("<pre>x\n<!-- c --></pre>") != c("<pre>x<!-- c --></pre>")
+assert c("<li>a\n<pre>x</pre>") == c("<li>a<pre>x</pre>")
+PY
+then echo "PASS T4 newline-inside-pre-is-compared"; else bad=$((bad+1)); echo "FAIL T4 newline-inside-pre-is-compared"; fi
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From caeed3660ca4e98b3027b8e566fd3ae70c767cc6 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 23:00:41 -0400
Subject: [PATCH 11/13] =?UTF-8?q?harness:=20round-35=20=E2=80=94=20fence?=
 =?UTF-8?q?=20closure=20from=20the=20parse;=20<pre>=20protected=20in=20any?=
 =?UTF-8?q?=20case?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex review of 868313f; both reproduced first by harness/tests/neg35.sh
(2 of 3 cases failed on 868313f; 3 of 3 pass now):

- unclosed() judged a fence reaching the end of the file closed if its last
  line looked like a closer with ANY indentation. A would-be closer indented
  4+ columns past its container is content, so the fence never closes and
  swallows what follows. The check now reads the parser's own verdict: a
  closed fence spans opener + content + closer lines, an open one only
  opener + content. U2 (closer 6 columns deep) FAILs with `never closes`;
  U2+ (3 columns) still closes.
- harness/conformance/check.py matched `<pre>` case-sensitively, so a raw
  `<PRE>` region was not protected from newline canonicalization. The
  pattern is case-insensitive now (U1).

25 suites, 312 cases, 0 failing; 652/652 spec examples, 69 edge cases;
html5lib oracle 100,000 fragments, 0 divergences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                    |  2 +-
 README.md                    |  4 +--
 harness/check-knowledge.py   |  7 +++--
 harness/conformance/check.py |  2 +-
 harness/tests/README.md      |  2 +-
 harness/tests/neg35.sh       | 53 ++++++++++++++++++++++++++++++++++++
 6 files changed, 62 insertions(+), 8 deletions(-)
 create mode 100644 harness/tests/neg35.sh

diff --git a/LEDGER.md b/LEDGER.md
index aa790f2..880e6dc 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 690 lines + 199-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 69 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases); 10th push answers Codex + Kilo on 4187200 (newlines inside `<pre>` kept in the conformance canonicalizer; raw-HTML-only H1 text stays a FAIL by the same rule as lessons; Kilo's index and crash reports disproved by T2/T3), round-34 suite (5 cases, 309 total); the HTML-tag whitespace patches now take ASCII whitespace (form feed / vertical tab were wrongly rejected since 78d12a9; 5 new conformance cases) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 691 lines + 199-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 69 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases); 10th push answers Codex + Kilo on 4187200 (newlines inside `<pre>` kept in the conformance canonicalizer; raw-HTML-only H1 text stays a FAIL by the same rule as lessons; Kilo's index and crash reports disproved by T2/T3), round-34 suite (5 cases, 309 total); the HTML-tag whitespace patches now take ASCII whitespace (form feed / vertical tab were wrongly rejected since 78d12a9; 5 new conformance cases); 11th push: fence closure read from the parse (an over-indented would-be closer no longer counts), `<pre>` protected in any case, round-35 suite (3 cases, 312 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index c3c8736..6801e3c 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 309 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/tests/` | 312 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
 | `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 69 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 309 negative-test cases
+bash harness/tests/run.sh                   # the 312 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index 023057e..ef4d5b8 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -179,9 +179,10 @@ def unclosed(self) -> list[str]:
         out: list[str] = []
         for t in self.tokens:
             if t.type == "fence" and t.map[1] >= self.n_lines:
-                last = self.lines[t.map[1] - 1] if t.map[1] - 1 > t.map[0] else ""
-                closer = r"[ \t>]*" + re.escape(t.markup[0]) + "{%d,}[ \t]*" % len(t.markup)
-                if not re.fullmatch(closer, last):
+                # the parser's own verdict: a closed fence spans opener + content + closer lines, an open
+                # one only opener + content (a would-be closer indented too far, or in the wrong
+                # container, is content)
+                if (t.map[1] - t.map[0]) - 1 - len(t.content.splitlines()) < 1:
                     out.append(f"fenced code block opened with {t.markup} never closes")
             elif t.type == "html_block":
                 c, msg = t.content.lstrip(" "), None
diff --git a/harness/conformance/check.py b/harness/conformance/check.py
index ac7e5cf..d292601 100644
--- a/harness/conformance/check.py
+++ b/harness/conformance/check.py
@@ -19,7 +19,7 @@
 # `<blockquote>\n</blockquote>`). Both sides are compared with exactly that newline removed —
 # never inside <pre>, where a newline is visible; every other byte must match.
 BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<pre[\s>])|\n(?=<!--)")
-PRE = re.compile(r"<pre[\s>].*?(?=</pre>)", re.S)   # a newline inside <pre> is visible: never canonicalized
+PRE = re.compile(r"<pre[\s>].*?(?=</pre>)", re.S | re.I)   # a newline inside <pre> is visible: never canonicalized
 # Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each entry
 # pins OUR exact output (cmark-gfm 0.29.0.gfm.13's output, checked 2026-09-27) and must still differ
 # from the reference, so an entry can neither hide a regression nor outlive its reason.
diff --git a/harness/tests/README.md b/harness/tests/README.md
index 0ea5b81..b786623 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,7 +10,7 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 34 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 35 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
diff --git a/harness/tests/neg35.sh b/harness/tests/neg35.sh
new file mode 100644
index 0000000..495debc
--- /dev/null
+++ b/harness/tests/neg35.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+# Round-35 negative tests (Codex review of 868313f).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+
+# U1 (4118171927): the conformance canonicalizer protects <pre> in any letter case
+n=$((n+1))
+if python3 - "$SRC/harness/conformance/check.py" <<'PY'
+import re, sys
+src = open(sys.argv[1]).read()
+ns = {"re": re}
+exec(src[src.index("BLOCK_NL = "):src.index("# Reviewed cases")], ns)
+exec(src[src.index("def canon"):src.index("ref = json.load")], ns)
+c = ns["canon"]
+assert c("<PRE>x\n<!-- c --></PRE>") != c("<PRE>x<!-- c --></PRE>")
+assert c("<Pre>x\n</pRe>") != c("<Pre>x</pRe>")
+PY
+then echo "PASS U1 uppercase-pre-is-protected"; else bad=$((bad+1)); echo "FAIL U1 uppercase-pre-is-protected"; fi
+
+# U2 (4118171930): a would-be closer indented 4+ columns past its container is content, so the fence never closes
+t2() { both 'import sys
+p=sys.argv[1]; s=open(p).read().rstrip("\n").split("\n")
+s += ["", "- **Deep fence**", "  - Why it matters here: x", "  - Do: y", "  - Source: [T](https://x.y) — A · confidence 50%", "", "    ```", "    code", "          ```", ""]
+open(p,"w").write("\n".join(s))'; }
+run "U2 over-indented-closer-leaves-fence-open" "never closes" t2
+t2b() { both 'import sys
+p=sys.argv[1]; s=open(p).read().rstrip("\n").split("\n")
+s += ["", "- **Near fence**", "  - Why it matters here: x", "  - Do: y", "  - Source: [T](https://x.y) — A · confidence 50%", "", "    ```", "    code", "       ```", ""]
+open(p,"w").write("\n".join(s))'; }
+pos "U2+ closer-within-three-columns-closes" t2b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From 6e8f79e21d41b80d7b7664bbf4e23819f8059760 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 23:13:48 -0400
Subject: [PATCH 12/13] =?UTF-8?q?harness:=20round-36=20=E2=80=94=20comment?=
 =?UTF-8?q?=20blocks=20before=20a=20field=20label;=20raw=20HTML=20inside?=
 =?UTF-8?q?=20image=20descriptions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Codex review of caeed36; reproduced first by harness/tests/neg36.sh (3 of 4
cases failed on caeed36; 4 of 4 pass now):

- A field item starting with a comment-only block (`- <!-- note -->`, then
  `Why it matters here: x` on the next line) was ignored and the lesson
  reported the field missing. Comment-only blocks are skipped when reading a
  field, as they already were before the title (V1; V1+ keeps an empty
  field failing). commonmark.js agrees on the parse (`comment-block-then-field`).
- `![<b>x</b>](…)`: raw HTML nested in an image description escaped the
  raw-HTML ban because only top-level inline tokens were inspected. The ban
  now walks nested tokens (V2; V2+ keeps a comment there allowed).

The new reference case `html-in-image-description` showed the three
implementations render that alt text differently (commonmark.js raw,
cmark-gfm escaped, markdown-it text only; the spec only recommends the
plain string). It is a pinned, reviewed exception: the parse is identical,
and the ban means no verdict depends on it.

26 suites, 316 cases, 0 failing; 652/652 spec examples, 71 edge cases
(4 pinned exceptions); html5lib oracle 100,000 fragments, 0 divergences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 LEDGER.md                      |  2 +-
 README.md                      |  6 ++---
 harness/check-knowledge.py     |  8 ++++--
 harness/conformance/cases.json | 10 ++++++++
 harness/conformance/check.py   | 12 ++++++---
 harness/tests/README.md        |  2 +-
 harness/tests/neg36.sh         | 47 ++++++++++++++++++++++++++++++++++
 7 files changed, 77 insertions(+), 10 deletions(-)
 create mode 100644 harness/tests/neg36.sh

diff --git a/LEDGER.md b/LEDGER.md
index 880e6dc..22b79c3 100644
--- a/LEDGER.md
+++ b/LEDGER.md
@@ -9,7 +9,7 @@ Columns: **Date** is UTC. **Session** is the claude.ai/code link or `hand`.
 
 | Date | Session | Branch | PR | Outcome | Evidence |
 | --- | --- | --- | --- | --- | --- |
-| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 691 lines + 199-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 69 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases); 10th push answers Codex + Kilo on 4187200 (newlines inside `<pre>` kept in the conformance canonicalizer; raw-HTML-only H1 text stays a FAIL by the same rule as lessons; Kilo's index and crash reports disproved by T2/T3), round-34 suite (5 cases, 309 total); the HTML-tag whitespace patches now take ASCII whitespace (form feed / vertical tab were wrongly rejected since 78d12a9; 5 new conformance cases); 11th push: fence closure read from the parse (an over-indented would-be closer no longer counts), `<pre>` protected in any case, round-35 suite (3 cases, 312 total) |
+| 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/harness-v2` | #2 | open | harness v2: `check-knowledge.py` rebuilt on markdown-it-py (1,345 → 695 lines + 199-line `knowledge_md.py`), 201 negative-test cases moved into `harness/tests/` and CI, CommonMark conformance gate (652/652 spec examples, 71 edge cases vs commonmark.js 0.31.2), 4 old test assertions corrected with reference evidence, `verified/…_v3.md` for the launch-pad rename, lesson feed documented as paused, PR #1 rows flipped to merged (merge `7946224`); 2nd push fixes 5 Codex findings on `78d12a9` (offsets after `<pre>`, invisible-only fields, comments in labels, `--json` without deps, code splitting the confidence word) with round-28 suite (17 cases) and a shellcheck + pyflakes CI job; 3rd push fixes 5 more (whitespace-keeping raw HTML, empty source-link text, mutations that fail silently, `<?>` closing, v3 sheet wording) plus invisible-markup ends matched to Chromium, with round-29 suite (20 cases); 4th push answers 3 more (hidden raw-HTML subtrees, comments around the title, comment-like text in attributes / raw text) by replacing the regex with a browser-faithful HTML tokenizer checked against html5lib (100,000 seeded fuzz fragments in CI) and a rule that raw HTML never counts as a field value, round-30 suite (17 cases); 6th push answers 11 more across three reviews (quoted `</script>` closure via the tokenizer, link-glued confidence, stray end tags, hidden titles, whitespace across inline boundaries, empty later blocks, svg nesting, scoped whitespace preservation, neg11 routed through `mutate()`; the DOCTYPE claim disproved with html5lib + Chromium), round-31 suite (21 cases); 7th push: the review loop on raw-HTML semantics ended by T's decision to ban raw HTML other than comments from lessons (14 positive cases, 4 of them from PR #1, now expect the ban), plus the inline-code-title regression and `</svg/>` fixed, round-32 suite (18 cases); 8th push: Source links must be inline `[title](https://…)` again (a reference-style link was accepted), round-32 suite 21 cases, 297 total; 22 of 22 mutants caught on 37a18eb; 9th push fixes 4 more (inline code at the link boundary, hidden verified H1s, a comment block before the title, allowlisted conformance outputs pinned) and brings raw inline tags in line with the spec on NBSP, round-33 suite (7 cases); 10th push answers Codex + Kilo on 4187200 (newlines inside `<pre>` kept in the conformance canonicalizer; raw-HTML-only H1 text stays a FAIL by the same rule as lessons; Kilo's index and crash reports disproved by T2/T3), round-34 suite (5 cases, 309 total); the HTML-tag whitespace patches now take ASCII whitespace (form feed / vertical tab were wrongly rejected since 78d12a9; 5 new conformance cases); 11th push: fence closure read from the parse (an over-indented would-be closer no longer counts), `<pre>` protected in any case, round-35 suite (3 cases); 12th push: comment blocks before a field label skipped, raw HTML inside image descriptions caught by the ban, round-36 suite (4 cases, 316 total) |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | correction: `verified/2026-09-26_claude-surfaces_v2.md` (session ran in the chat/Cowork tab, not the Code tab — T's correction 2026-09-27); v1 banner-marked superseded |
 | 2026-09-27 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `claude/face-lift-v1` | #1 | merged | README restructure, CONTRIBUTING, ledger, `harness/check-knowledge.py` (RESULT: PASS — 12 pass, 0 warn, 0 fail after review fixes), lint CI, description + topics set via `gh` on the linked PC; follow-up pushes fix 3 Sourcery + 129 Codex findings, 1 pushed back with evidence (27 rounds; rounds 22-25 also rewrote 14 earlier suite cases whose vehicle or CommonMark reading no longer held, each checked against commonmark.js 0.31.2 or by mutation) |
 | 2026-09-26 | [session_01T7N2VsxDrRZPwWM7zdFnuK](https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK) | `main` | — | direct | commit `ef0c3ac`: `verified/2026-09-26_claude-surfaces_v1.md` |
diff --git a/README.md b/README.md
index 6801e3c..1233b97 100644
--- a/README.md
+++ b/README.md
@@ -31,8 +31,8 @@ counters) lives in Cloudflare KV, not here.
 | `verified/YYYY-MM-DD_topic_vN.md` | Fact sheets verified live in a Claude session, not daily.dev lessons. Revisions bump `vN` | Claude sessions, hand-maintained |
 | `harness/check-knowledge.py` | PASS/FAIL check that the files above still have the shape readers rely on | Run by CI and by sessions |
 | `harness/knowledge_md.py` | The one Markdown parser the checks use: markdown-it-py (CommonMark 0.31.2) plus the patches that align it with the reference | Sessions |
-| `harness/tests/` | 312 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
-| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 69 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
+| `harness/tests/` | 316 negative-test cases: each copies the repo, breaks one thing, and asserts the checker's verdict | Sessions |
+| `harness/conformance/` | Differential gates: the parser must render all 652 CommonMark spec examples and 71 edge cases like the reference implementation, and the HTML tokenizer must split HTML like html5lib | Sessions |
 | `LEDGER.md` | One row per hand-made or session-made change (connector runs are not logged here) | Sessions and hand edits |
 
 ## How a lesson is shaped
@@ -64,7 +64,7 @@ Zero lessons on a quiet day is a valid result; the loop is told never to pad.
 pip install -r harness/requirements.txt     # markdown-it-py, pinned
 python3 harness/check-knowledge.py          # PASS/FAIL lines
 python3 harness/check-knowledge.py --json   # same, as one JSON object
-bash harness/tests/run.sh                   # the 312 negative-test cases
+bash harness/tests/run.sh                   # the 316 negative-test cases
 ```
 
 The checker reads Markdown the way CommonMark 0.31.2 does, using a real parser
diff --git a/harness/check-knowledge.py b/harness/check-knowledge.py
index ef4d5b8..249d407 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -403,10 +403,14 @@ def raw_html_in(doc: Doc, n: Node) -> bool:
     def only_comments(html: str) -> bool:
         return all((kind == "hidden" and src.startswith("<!--")) or (kind == "text" and not src.strip(WS))  # blank
                    for kind, src, _ in html_segments(html))                 # text: the padding of a comment block
+    def walk(tokens):                                      # image descriptions nest their own tokens
+        for c in tokens or []:
+            yield c
+            yield from walk(c.children)
     for t in doc.tokens[n.i:n.j + 1]:
         if t.type == "html_block" and not only_comments(t.content):
             return True
-        if t.type == "inline" and any(c.type == "html_inline" and not only_comments(c.content) for c in t.children):
+        if t.type == "inline" and any(c.type == "html_inline" and not only_comments(c.content) for c in walk(t.children)):
             return True
     return False
 
@@ -433,7 +437,7 @@ def parse_lesson(doc: Doc, item: Node) -> dict:
         if block.kind != "bullet_list" or block.t.markup != "-":
             continue
         for field in block.kids:
-            fk = field.kids
+            fk = [k for k in field.kids if visible(k)]   # a comment-only block before the label is invisible
             para = fk[0].kids[0].t if fk and fk[0].kind == "paragraph" else None
             toks = inline_tokens(para) if para else []
             m = FIELD_RE.match(lead_text(toks)) if para else None
diff --git a/harness/conformance/cases.json b/harness/conformance/cases.json
index 736f41d..a253458 100644
--- a/harness/conformance/cases.json
+++ b/harness/conformance/cases.json
@@ -343,5 +343,15 @@
   "id": "vt-after-block-tag",
   "markdown": "<div\u000bx>\ntext\n",
   "why": "a form feed or vertical tab separates tag parts (cmark spacechar, commonmark.js \\s); only a NBSP does not"
+ },
+ {
+  "id": "comment-block-then-field",
+  "markdown": "- **T**\n  - <!-- note -->\n    Why it matters here: x\n",
+  "why": "a comment alone on a field item's first line ends its HTML block; the label paragraph follows"
+ },
+ {
+  "id": "html-in-image-description",
+  "markdown": "Do: ![<b>x</b>](https://x.y/i.png) y\n",
+  "why": "raw HTML inside an image description is dropped from the alt text"
  }
 ]
diff --git a/harness/conformance/check.py b/harness/conformance/check.py
index d292601..f4a3e3b 100644
--- a/harness/conformance/check.py
+++ b/harness/conformance/check.py
@@ -20,9 +20,10 @@
 # never inside <pre>, where a newline is visible; every other byte must match.
 BLOCK_NL = re.compile(r"\n(?=</?(?:blockquote|ul|ol|li|p|h[1-6]|hr|div|table)[\s/>])|\n(?=<pre[\s>])|\n(?=<!--)")
 PRE = re.compile(r"<pre[\s>].*?(?=</pre>)", re.S | re.I)   # a newline inside <pre> is visible: never canonicalized
-# Reviewed cases where the REFERENCE departs from the spec text; we follow the spec. Each entry
-# pins OUR exact output (cmark-gfm 0.29.0.gfm.13's output, checked 2026-09-27) and must still differ
-# from the reference, so an entry can neither hide a regression nor outlive its reason.
+# Reviewed divergences: where the REFERENCE departs from the spec text (we follow the spec, and
+# cmark-gfm 0.29.0.gfm.13, checked 2026-09-27), or where the spec leaves rendering open. Each entry
+# pins OUR exact output and must still differ from the reference, so an entry can neither hide a
+# regression nor outlive its reason.
 ALLOWED = {
     "nbsp-after-tag": ("commonmark.js matches HTML-block starts with JS `\\s`, which includes U+00A0; spec 0.31.2 "
                        "section 4.6 allows only space, tab, end of line, `>` or `/>` after the tag name, and 6.6 "
@@ -30,6 +31,11 @@
                        "<p>&lt;div\xa0class=x&gt;\ntext</p>\n"),
     "type7-nbsp": ("same root cause: an attribute must be preceded by space, tab or a line ending (spec 6.6); "
                    "cmark and cmark-gfm render the tag as text.", "<p>&lt;span\xa0a=b&gt;</p>\n"),
+    "html-in-image-description": ("the spec only recommends the description's plain string content for `alt` "
+                                  "(6.4) and implementations differ: commonmark.js writes `<b>x</b>` raw, cmark-gfm "
+                                  "escapes it, markdown-it keeps the text `x`. Raw HTML is banned in lessons, so no "
+                                  "verdict depends on it; the parse (an image) is the same.",
+                                  '<p>Do: <img src="https://x.y/i.png" alt="x" /> y</p>\n'),
     "nbsp-in-inline-tag": ("same root cause inline: commonmark.js takes `<span` + U+00A0 + `a=b>` as raw HTML; spec 6.6 and "
                            "cmark-gfm make it text.", "<p>x &lt;span\xa0a=b&gt; y</p>\n"),
 }
diff --git a/harness/tests/README.md b/harness/tests/README.md
index b786623..79525a5 100644
--- a/harness/tests/README.md
+++ b/harness/tests/README.md
@@ -10,7 +10,7 @@ containing a given string, `pos` expects PASS, and `stray_only` expects a
 that prints `MUTATION DID NOT APPLY`, so no case can pass on an unchanged repo.
 
 The suite numbers follow the review rounds that produced them (rounds 11 to
-27 of the review on PR #1, rounds 28 to 35 on PR #2). Cases marked
+27 of the review on PR #1, rounds 28 to 36 on PR #2). Cases marked
 `CORRECTED 2026-09-27` encoded a CommonMark reading that the reference
 implementation (commonmark.js 0.31.2) proved wrong. Each correction keeps a
 replacement case for the intent that survives. Round 30 made raw HTML stricter:
diff --git a/harness/tests/neg36.sh b/harness/tests/neg36.sh
new file mode 100644
index 0000000..48e685d
--- /dev/null
+++ b/harness/tests/neg36.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Round-36 negative tests (Codex review of caeed36).
+# Browser behaviour here is checked by harness/conformance/html_oracle.py (html5lib) and in Chromium 141.
+set -u
+SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+W=$(mktemp -d); trap 'rm -rf "$W"' EXIT
+# Apply one case's mutation inside the copy. If any command in it fails, the copy's checker is replaced by a
+# stub that names the failure, so a case can never pass (or fail as expected) on an unmutated repo.
+mutate() { ( set -e; cd "$W/r"; "$1" ); local rc=$?
+  [ "$rc" -eq 0 ] || printf 'print("MUTATION DID NOT APPLY: %s (rc=%s)"); raise SystemExit(3)\n' "$1" "$rc" > "$W/r/harness/check-knowledge.py"; }
+n=0; bad=0
+both() { for p in knowledge.md lessons/2026-08-24.md; do python3 -c "$1" "$p"; done; }
+split() { python3 -c "$1" knowledge.md; python3 -c "$2" lessons/2026-08-24.md; }
+run() { local name="$1" expect="$2"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$3"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -ne 0 ] && printf '%s' "$out" | grep -qF -- "$expect"; then
+    echo "PASS $name — FAIL fired: $(printf '%s' "$out" | grep -F -- "$expect" | head -1 | cut -c1-160)"
+  else bad=$((bad+1)); echo "FAIL $name — rc=$rc, expected '$expect' in:"; echo "$out" | sed 's/^/    /'; fi; }
+pos() { local name="$1"; n=$((n+1))
+  rm -rf "$W/r"; cp -r "$SRC" "$W/r"; rm -rf "$W/r/.git"; mutate "$2"
+  out=$(cd "$W/r" && python3 harness/check-knowledge.py 2>&1); rc=$?
+  if [ $rc -eq 0 ]; then echo "PASS $name — still PASS"; else bad=$((bad+1)); echo "FAIL $name — unexpected FAIL:"; echo "$out" | sed 's/^/    /'; fi; }
+INS='
+import sys
+p=sys.argv[1]; s=open(p).read().split("\n")
+i=next(k for k,l in enumerate(s) if l.startswith("## ") or l.startswith("# Lessons"))
+s[i+1:i+1]=[""]+LESSON
+open(p,"w").write("\n".join(s))
+'
+F='"  - Source: [T](https://x.y) — A · confidence 50%"'
+
+# V1 (4118216332): a comment block at the start of a field item is invisible; the field paragraph after it counts
+t1() { both 'LESSON=["- **Field comment**","  - <!-- note -->","    Why it matters here: x","  - Do: y",'"$F"']'"$INS"; }
+pos "V1 comment-block-before-field-paragraph" t1
+t1b() { both 'LESSON=["- **Field comment empty**","  - <!-- note -->","    Why it matters here:","  - Do: y",'"$F"']'"$INS"; }
+run "V1+ comment-block-then-empty-field" "missing or empty why" t1b
+
+# V2 (4118216337): raw HTML nested in an image description is still raw HTML in a lesson
+t2() { both 'LESSON=["- **Image html**","  - Why it matters here: x","  - Do: ![<b>x</b>](https://x.y/i.png) y",'"$F"']'"$INS"; }
+run "V2 raw-html-inside-image-description" "raw HTML in a lesson" t2
+t2b() { both 'LESSON=["- **Image comment**","  - Why it matters here: x","  - Do: ![a <!-- c --> b](https://x.y/i.png) y",'"$F"']'"$INS"; }
+pos "V2+ comment-inside-image-description" t2b
+
+echo "----"
+echo "RESULT: $([ $bad -eq 0 ] && echo PASS || echo FAIL) — $((n-bad)) pass, $bad fail ($n cases)"
+[ $bad -eq 0 ]

From cab91cc8e6f0fb5abf421ea146bf4896d32aeef6 Mon Sep 17 00:00:00 2001
From: Claude <noreply@anthropic.com>
Date: Sun, 27 Sep 2026 23:18:21 -0400
Subject: [PATCH 13/13] harness/tests: anchor the conformance-gate probes on
 code, not a comment (CI fix)

6e8f79e reworded the comment above ALLOWED in harness/conformance/check.py;
neg34 T4 and neg35 U1 sliced that file's source up to the old comment text,
so both failed with a ValueError and the "negative-test suites" CI job went
red on 6e8f79e. The probes now slice up to `ALLOWED = {`, a code anchor.
Reproduced locally (26 suites, 316 cases, 2 failing on 6e8f79e) and fixed
(0 failing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T7N2VsxDrRZPwWM7zdFnuK
---
 harness/tests/neg34.sh | 2 +-
 harness/tests/neg35.sh | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/harness/tests/neg34.sh b/harness/tests/neg34.sh
index cc0dd41..e473ac9 100644
--- a/harness/tests/neg34.sh
+++ b/harness/tests/neg34.sh
@@ -57,7 +57,7 @@ if python3 - "$SRC/harness/conformance/check.py" <<'PY'
 import re, sys
 src = open(sys.argv[1]).read()
 ns = {"re": re}
-exec(src[src.index("BLOCK_NL = "):src.index("# Reviewed cases")], ns)
+exec(src[src.index("BLOCK_NL = "):src.index("ALLOWED = {")], ns)   # code anchors, not comments
 exec(src[src.index("def canon"):src.index("ref = json.load")], ns)
 c = ns["canon"]
 assert c("<pre>x\n</pre>") != c("<pre>x</pre>")                       # the newline before </pre> is visible
diff --git a/harness/tests/neg35.sh b/harness/tests/neg35.sh
index 495debc..84858c7 100644
--- a/harness/tests/neg35.sh
+++ b/harness/tests/neg35.sh
@@ -28,7 +28,7 @@ if python3 - "$SRC/harness/conformance/check.py" <<'PY'
 import re, sys
 src = open(sys.argv[1]).read()
 ns = {"re": re}
-exec(src[src.index("BLOCK_NL = "):src.index("# Reviewed cases")], ns)
+exec(src[src.index("BLOCK_NL = "):src.index("ALLOWED = {")], ns)   # code anchors, not comments
 exec(src[src.index("def canon"):src.index("ref = json.load")], ns)
 c = ns["canon"]
 assert c("<PRE>x\n<!-- c --></PRE>") != c("<PRE>x<!-- c --></PRE>")