diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 3954627..aac4d86 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 @@ -28,4 +41,35 @@ 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 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" + - run: python3 harness/conformance/check.py "$RUNNER_TEMP/ref.json" 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/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..22b79c3 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 → 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 `
`, 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 `` 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 `` 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 `
` 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), `
` 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` |
 | 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..1233b97 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/` | 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
@@ -51,12 +61,24 @@ 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 316 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. 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 `` 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`
+(`.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..249d407 100644
--- a/harness/check-knowledge.py
+++ b/harness/check-knowledge.py
@@ -8,25 +8,35 @@
                  (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  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)
   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 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 
 and  (not at all when the lesson holds raw HTML that
+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.
 
-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 +46,51 @@
 import re
 import sys
 
+sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent))
+try:
+    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})")
+    _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]
 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
+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; 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),
+             (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 +118,386 @@ 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
-
+    return re.sub(r"\r\n?", "\n", text)
 
-def visible_ws(s: str) -> str:
-    """Spaces and tabs made visible so prose normalisation cannot collapse code whitespace."""
-    return s.replace(" ", "␣").replace("\t", "⇥")
 
+# --- 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 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 __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 is_mark(ln: str) -> bool:
-    return mark_kind(ln) is not None
+class Doc:
+    """A parsed Markdown file: tokens, top-level blocks, reference env, source lines."""
 
-
-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 __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
+            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:
+                # 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
+                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
+                            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
+
+
+def walk(n: Node):
+    yield n
+    for k in n.kids:
+        yield from walk(k)
+
+
+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 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":
+            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 == "style" for kind, _, name in segs):
+        return "".join(text).strip(WS)
+    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"):
+            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 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)
+
+
+def visible(n: Node) -> bool:
+    """Does this top-level block render anything? Only raw HTML can be invisible."""
+    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]:
+    """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:
+                out.append(doc.line(d).strip(" \t"))
+    return out
 
 
-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)
+# --- 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 == "")]
 
 
-BACKTICK_RUN_RE = re.compile(r"`+")
+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 all(kind == "hidden" for kind, _, _ in html_segments(c.content))
 
 
-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 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 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 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
+    (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:
+        if c.type == "html_inline":
+            raw_step(depth, c)
+            if not invisible_inline(c):
+                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 == "code_inline" and code_text:
+            out.append(c.content)
+        elif c.type in ("code_inline", "image"):
+            out.append(SEP)
+    return "".join(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)
+def lead_text(tokens) -> str:
+    """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":
+            out.append(c.content)
+        elif not invisible_inline(c):
+            break
     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)):
+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.
+    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
+        k += 1
+    if not lead.startswith("Source:") or lead[len("Source:"):].strip(" \t"):
+        return None
+    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]+$", 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]).replace(SPLIT, "").strip(WS):   # needs a visible title
+        return None
+    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
+
+
+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 = [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
+    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 = 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
+    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 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:
+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))  # 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 walk(t.children)):
             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()
-
-    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
-                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
-            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
-    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)
-
-    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
+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)")
+    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)
+        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 kids[1:]:
+        if block.kind != "bullet_list" or block.t.markup != "-":
             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)
-            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 = [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
+            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 = 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(has_value(doc, b) for b in fk[1:]))
             if key == "source":
-                lesson["source_raw"] = value
+                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:
+                    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 +505,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 +538,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 +566,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 +584,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 +595,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 +616,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 +666,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 = (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)
     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 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")
 
-# --- 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..a253458
--- /dev/null
+++ b/harness/conformance/cases.json
@@ -0,0 +1,357 @@
+[
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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"
+ },
+ {
+  "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
new file mode 100644
index 0000000..f4a3e3b
--- /dev/null
+++ b/harness/conformance/check.py
@@ -0,0 +1,75 @@
+#!/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 —
+# 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 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 "
+                       "only spaces, tabs and a line ending inside a tag. cmark and cmark-gfm (GitHub): text.",
+                       "<p><div\xa0class=x>\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><span\xa0a=b></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 <span\xa0a=b> y</p>\n"),
+}
+
+
+def canon(html: str) -> str:
+    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 = [], []
+for group in ("spec", "cases"):
+    for ex in ref[group]:
+        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:
+    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) - set(bad))   # a mis-pinned entry is reported above
+for a in sorted(allowed_hit):
+    print(f"  allowed {a}: {ALLOWED[a][0]}")
+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/html_cases.json b/harness/conformance/html_cases.json
new file mode 100644
index 0000000..596c603
--- /dev/null
+++ b/harness/conformance/html_cases.json
@@ -0,0 +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 -->"
+ },
+ {
+  "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"
+ },
+ {
+  "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
new file mode 100644
index 0000000..bc2f60a
--- /dev/null
+++ b/harness/conformance/html_oracle.py
@@ -0,0 +1,87 @@
+#!/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/>", "</>", "PUBLIC", "SYSTEM",
+         "<!DOCTYPE "]
+
+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/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..47a7821
--- /dev/null
+++ b/harness/knowledge_md.py
@@ -0,0 +1,199 @@
+"""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
+
+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
+
+
+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
+
+
+# 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. 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", 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 `=`; 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 = 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 + "*>"
+    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)
+
+
+# --- 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 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), 0                         # foreign: open svg/math elements
+    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))
+            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(
+                    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")
+
+
+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/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..79525a5
--- /dev/null
+++ b/harness/tests/README.md
@@ -0,0 +1,25 @@
+# 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. 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, 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:
+text inside it no longer counts as a field value, so round-29 cases put plain
+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
+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..b3357b5
--- /dev/null
+++ b/harness/tests/neg11.sh
@@ -0,0 +1,113 @@
+#!/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); 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"
+  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
+}
+
+# 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
+# (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"
+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 ] && 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))
+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..f3c612f
--- /dev/null
+++ b/harness/tests/neg12.sh
@@ -0,0 +1,78 @@
+#!/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); 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
+}
+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"
+  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() { # 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"
+  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
+}
+
+# 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..d4a9e85
--- /dev/null
+++ b/harness/tests/neg13.sh
@@ -0,0 +1,57 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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"; 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"; 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; }
+
+# 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"; 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
new file mode 100755
index 0000000..d9f4f3b
--- /dev/null
+++ b/harness/tests/neg14.sh
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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)"
+  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"; 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)); 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..a2c2377
--- /dev/null
+++ b/harness/tests/neg15.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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)"
+  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"; 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)); 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"; }
+# 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"; }
+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..0fc0875
--- /dev/null
+++ b/harness/tests/neg16.sh
@@ -0,0 +1,73 @@
+#!/usr/bin/env bash
+set -u
+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='
+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"; 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)"
+  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..fb981b2
--- /dev/null
+++ b/harness/tests/neg17.sh
@@ -0,0 +1,51 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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)"
+  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..bef95c6
--- /dev/null
+++ b/harness/tests/neg18.sh
@@ -0,0 +1,81 @@
+#!/usr/bin/env bash
+set -u
+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='
+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"; 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)"
+  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"; 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)); 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..6e0c4b1
--- /dev/null
+++ b/harness/tests/neg19.sh
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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)"
+  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"; 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)); 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..b2d4cd3
--- /dev/null
+++ b/harness/tests/neg20.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+set -u
+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='
+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"; 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)"
+  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"; 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)); 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..20a2a35
--- /dev/null
+++ b/harness/tests/neg21.sh
@@ -0,0 +1,65 @@
+#!/usr/bin/env bash
+set -u
+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
+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"; 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)"
+  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"; 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)); 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..cdae3e5
--- /dev/null
+++ b/harness/tests/neg22.sh
@@ -0,0 +1,133 @@
+#!/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); 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"; 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))
+'
+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..af8e0e1
--- /dev/null
+++ b/harness/tests/neg23.sh
@@ -0,0 +1,122 @@
+#!/usr/bin/env bash
+# Round-23 negative tests (Codex review of 320a5cc).
+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%"'
+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"; }
+# 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"; }
+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..97c355c
--- /dev/null
+++ b/harness/tests/neg24.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+# Round-24 negative tests (Codex review of e7edf49).
+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%"'
+
+# 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..c293374
--- /dev/null
+++ b/harness/tests/neg25.sh
@@ -0,0 +1,94 @@
+#!/usr/bin/env bash
+# Round-25 negative tests (Codex review of 0446523).
+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%"'
+
+# 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..c3ad7e2
--- /dev/null
+++ b/harness/tests/neg26.sh
@@ -0,0 +1,62 @@
+#!/usr/bin/env bash
+# Round-26 negative tests (Codex review of 03bb96e).
+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%"'
+
+# 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"; }
+# 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
+
+# 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..26f6dc3
--- /dev/null
+++ b/harness/tests/neg27.sh
@@ -0,0 +1,72 @@
+#!/usr/bin/env bash
+# Round-27 negative tests (Codex review of a0fa435).
+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%"'
+
+# 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"; }
+# 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)"
+rm -rf "$W"
+[ $bad -eq 0 ]
diff --git a/harness/tests/neg28.sh b/harness/tests/neg28.sh
new file mode 100644
index 0000000..3b538d5
--- /dev/null
+++ b/harness/tests/neg28.sh
@@ -0,0 +1,92 @@
+#!/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
+# 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%"'
+
+# 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/neg29.sh b/harness/tests/neg29.sh
new file mode 100644
index 0000000..aada89b
--- /dev/null
+++ b/harness/tests/neg29.sh
@@ -0,0 +1,113 @@
+#!/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.
+#    (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: 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: type <textarea>a  b</textarea>",'"$F"']'"$INS"; }
+# 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"; }
+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/harness/tests/neg30.sh b/harness/tests/neg30.sh
new file mode 100644
index 0000000..6a105d7
--- /dev/null
+++ b/harness/tests/neg30.sh
@@ -0,0 +1,88 @@
+#!/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"; }
+# 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
+
+# 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.
+#    (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: 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: 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: 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
+
+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/neg31.sh b/harness/tests/neg31.sh
new file mode 100644
index 0000000..9963f84
--- /dev/null
+++ b/harness/tests/neg31.sh
@@ -0,0 +1,120 @@
+#!/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"; }
+# 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"; }
+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"; }
+# 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"; }
+# 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"; }
+# 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
+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"; }
+# 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
+
+# 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"; }
+# 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"; }
+# 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"; }
+# 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
+
+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/neg32.sh b/harness/tests/neg32.sh
new file mode 100644
index 0000000..e1f521b
--- /dev/null
+++ b/harness/tests/neg32.sh
@@ -0,0 +1,97 @@
+#!/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
+
+# 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'
+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 ]
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 ]
diff --git a/harness/tests/neg34.sh b/harness/tests/neg34.sh
new file mode 100644
index 0000000..e473ac9
--- /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("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
+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 ]
diff --git a/harness/tests/neg35.sh b/harness/tests/neg35.sh
new file mode 100644
index 0000000..84858c7
--- /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("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>")
+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 ]
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 ]
diff --git a/harness/tests/run.sh b/harness/tests/run.sh
new file mode 100755
index 0000000..465bdec
--- /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]}")" || exit 1
+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..555d953
--- /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, 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.
+
+## 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).