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 -------------------------------------------------------------------------