diff --git a/docs/decisions/records/release/review-verdict-gate.md b/docs/decisions/records/release/review-verdict-gate.md index 36d0ef15f..89c20d7f3 100644 --- a/docs/decisions/records/release/review-verdict-gate.md +++ b/docs/decisions/records/release/review-verdict-gate.md @@ -77,6 +77,25 @@ a bounded left side but not the right; state reset per comment but via a delimit controls). For attacker-writable text, budget several rounds and prefer eliminating a class over enumerating its members. +A fifth round found raw HTML — `
`, ``, HTML comments — the third code-block form. It is
+stripped too, and **hardening stopped there, deliberately.**
+
+**Scope limit, stated so nobody re-derives it: the comment scanner is a best-effort heuristic, not a
+markdown parser.** It handles the three code-block forms markdown actually has (fenced, indented via
+the column-0 rule, raw HTML) and errs toward stripping more, because every error in that direction can
+only *withhold* approval. It is not proof against every conceivable way to render text as non-prose,
+and chasing that was demonstrably not converging: five review rounds, five code-block forms, four of
+them introduced while fixing the previous one.
+
+Stopping is safe because **the comment is not the load-bearing gate.** Since #622 the authoritative
+signal is the `review-verdict/h10` commit status, written only by `scripts/post-review-verdict.sh`
+from explicit arguments — **a comment cannot forge it**, whatever it contains. The comment classifier
+is condition (c) of the PreToolUse hook, i.e. defense in depth on an agent's merge call. A residual
+false-open there means the hook does not object; it does not mean a merge happens.
+
+The rule that generalises: *when a heuristic keeps failing at the edges, check whether it is actually
+the thing enforcing the invariant before spending another round on it.*
+
 The lesson is about where a grammar lives, not about any one regex: this record described the intended
 behaviour accurately, the code did something looser, and nothing compared them. The grammar now lives
 in `scripts/check-review-verdict.sh` — one implementation, called by the hook and by
diff --git a/scripts/check-review-verdict.sh b/scripts/check-review-verdict.sh
index 79c3cf4e7..1513e9a2d 100755
--- a/scripts/check-review-verdict.sh
+++ b/scripts/check-review-verdict.sh
@@ -118,6 +118,20 @@ verdicts=$(printf '%s\n' "$encoded" | while IFS= read -r encoded_body; do
   # A shorter or different marker while a fence is open is content, so it neither closes nor prints.
   outside=$(printf '%s\n' "$body" | awk '
     {
+      # Raw HTML blocks are the third code-block form (#629 round 5): 
,  and HTML
+      # comments all render their contents literally, so a verdict inside one is an example, not an
+      # approval. Tracked as a simple depth/marker count rather than parsed — the direction of error
+      # is to strip MORE, which can only ever withhold approval.
+      low = tolower($0)
+      if (low ~ //)                 { closed = 1 }
+        if (low ~ /<\/(pre|code)>/)      { closed = 1 }
+        if (closed) { html = 0 }
+        next
+      }
       if (match($0, /^[[:space:]]*(`{3,}|~{3,})/)) {
         m = substr($0, RSTART, RLENGTH); gsub(/[[:space:]]/, "", m)
         ch = substr(m, 1, 1); len = length(m)
diff --git a/scripts/tests/test_check_review_verdict.py b/scripts/tests/test_check_review_verdict.py
index d8b90f77e..5670c947e 100644
--- a/scripts/tests/test_check_review_verdict.py
+++ b/scripts/tests/test_check_review_verdict.py
@@ -180,6 +180,33 @@ def test_a_fenced_example_alongside_a_real_blocked_verdict_still_blocks():
     assert classify([fenced, verdict("BLOCKED", HEAD)]) == ("negative", 0)
 
 
+# --- found by a fifth review round: raw HTML is the third code-block form ------------------------
+
+
+@pytest.mark.parametrize(
+    "wrapper",
+    [
+        "
\n{v}\n
", + "\n{v}\n", + "", + '
\n{v}\n
', + "
\n{v}\n
", + ], +) +def test_falseopen_raw_html_blocks_are_not_verdicts(wrapper): + """`
`, `` and HTML comments render their contents literally.
+
+    A verdict inside one is an example, not an approval. Markdown's three code-block forms — fenced,
+    indented, and raw HTML — each had to be handled; indentation was closed by the column-0 rule.
+    """
+    assert classify([wrapper.format(v=verdict("MERGEABLE", HEAD))]) == ("absent", 0)
+
+
+def test_an_html_example_does_not_eat_a_real_verdict_in_another_comment():
+    html = f"
\n{verdict('MERGEABLE', HEAD)}\n
" + assert classify([html, verdict("BLOCKED", HEAD)]) == ("negative", 0) + + # --- the happy paths -------------------------------------------------------------------------