diff --git a/.github/workflows/issue-gate.yml b/.github/workflows/issue-gate.yml index ab1faa7959..c36bd6086d 100644 --- a/.github/workflows/issue-gate.yml +++ b/.github/workflows/issue-gate.yml @@ -65,8 +65,36 @@ jobs: exit 0 fi - FOUND=$(echo "$PR_TITLE - $PR_BODY" | grep -oiE '(Closes?|Fixes?|Resolves?|Refs?|Updates?)\s*#[0-9]+' || true) + # A REFERENCE INSIDE A FENCE OR A QUOTE IS NOT A REFERENCE. + # + # The body is markdown. A pull request that shows `Closes #12` inside a + # fenced code block -- quoting someone else's commit, pasting a template + # -- satisfied this gate without referencing anything, and so did a line + # beginning with `>`, which is the body quoting a different pull request. + # Verified before the change: both forms passed. + # + # This repository already carries the same lesson about its own skill + # file, where a parser counted headings quoted inside code blocks. + # A quotation is not a claim. + # + # The TITLE is not markdown and is matched as written. + BODY_PROSE=$(printf '%s\n' "${PR_BODY:-}" | awk ' + /^[[:space:]]*```/ { fence = !fence; next } + fence { next } + /^[[:space:]]*>/ { next } + { print } + ') + + # `#0` IS NOT AN ISSUE. `#[0-9]+` matched it, and `gh issue view 0` + # answers "Could not resolve to an issue or pull request". A number that + # cannot name an issue is not traceability. The cheapest honest guard is + # to refuse a leading zero rather than to ask the API: a network call + # inside a REQUIRED context turns a GitHub hiccup into a merge block for + # the whole repository. + # + # Whether a reference names an issue that EXISTS is a heavier question, + # recorded rather than answered here. + FOUND=$(printf '%s\n%s\n' "$PR_TITLE" "$BODY_PROSE" | grep -oiE '(Closes?|Fixes?|Resolves?|Refs?|Updates?)\s*#[1-9][0-9]*' || true) if [ -n "$FOUND" ]; then echo "✅ Issue gate passed: $FOUND" diff --git a/docs/now/2026-09-06-four-required-context-defects-closed.md b/docs/now/2026-09-06-four-required-context-defects-closed.md new file mode 100644 index 0000000000..06838b89f2 --- /dev/null +++ b/docs/now/2026-09-06-four-required-context-defects-closed.md @@ -0,0 +1,22 @@ +# NOW -- Four required-context defects closed, the fifth priced (2026-09-06) + +## Four required-context defects closed (Refs #3388) + +- `Closes #0` passed `check-linked-issue`, and `gh issue view 0` answers "Could not + resolve to an issue". The number must not be zero now. +- A reference inside a fenced code block or a `>` quote counted as traceability. Fences + and quoted lines are stripped from the body first; the title is not markdown and is + matched as written. This repository already carries the same lesson about its own skill + file, where a parser counted headings quoted inside code blocks. +- `check` missed a rename: `--diff-filter=A` reports an R entry, so a docs/now path + whose content newly lands by rename never reached the shape reader. `--no-renames`. +- `validate` decoded with `errors="replace"`, turning invalid bytes into U+FFFD and + parsing the repaired text. Measured first: **0 of 2086** tracked files are non-UTF-8, so + the strictness costs nothing today and closes the hole anyway. +- Six controls on the issue-gate logic, including the one that matters: a real reference + AFTER a fence is still found, so the stripping does not swallow what follows it. +- The fifth is left: bare `Infinity` is accepted by CPython and refused by + `node -e JSON.parse`. Exactly one tracked file carries it -- my first count said 20, + and 19 of those have it inside strings. The file is GENERATED and READ by two tools, so + the encoding is a conformance-data decision; shipping the stricter parse without it + would leave a REQUIRED context red and block every merge. diff --git a/tools/check_json_parses.py b/tools/check_json_parses.py index fcf67f5ef7..f35b32a56e 100755 --- a/tools/check_json_parses.py +++ b/tools/check_json_parses.py @@ -66,7 +66,29 @@ def scan(root=ROOT): empty.append(rel) continue try: - json.loads(raw.decode("utf-8", "replace")) + # STRICT on both axes, because a lenient reader here is a gate that + # passes what its consumers refuse. + # + # `errors="replace"` turned any invalid byte into U+FFFD and then + # parsed the repaired text, so a file no strict reader accepts came + # back clean. Measured before the change: 0 of 2086 tracked files are + # non-UTF-8, so this costs nothing today and closes the hole anyway. + # + # NOT DONE HERE, and the reason is the point: `parse_constant` would + # also refuse bare `Infinity`, which CPython accepts and documents as + # "an extension to the JSON specification" while RFC 8259 has no such + # literal. `node -e JSON.parse(...)` rejects it, so this gate passes + # what another consumer refuses -- a real defect, confirmed. + # + # It is not fixed here because the blast radius is one tracked file, + # `conformance/vectors/gf16_conformance_v0.json`, whose six values are + # genuine IEEE754 infinities. That file is GENERATED by + # conformance/vectors/gen_all_formats.py and READ by + # tools/wp18_selftest_gate.py, so an encoding both sides agree on is a + # decision about conformance data, not about this reader. Shipping the + # stricter parse without it would leave a REQUIRED context red and + # block every merge in the repository. + json.loads(raw.decode("utf-8")) except Exception as e: bad.append((rel, str(e)[:90])) return empty, bad diff --git a/tools/check_now_entry_shape.py b/tools/check_now_entry_shape.py index f7bc5f229a..ab41f03a78 100644 --- a/tools/check_now_entry_shape.py +++ b/tools/check_now_entry_shape.py @@ -65,7 +65,12 @@ def added_now_entries(base, head): # `.md`, so the entry left the population SILENTLY and the gate reported that the # change adds none. Measured on a real Cyrillic filename. r = subprocess.run( - ["git", "diff", "-z", "--name-only", "--diff-filter=A", f"{base}...{head}", + # `--no-renames`, because rename detection decides this gate's population. + # git reports a rename as a single R entry, so `--diff-filter=A` misses it + # and a docs/now/ path whose content newly lands by rename never reaches + # the shape reader. With the flag git reports D(old) + A(new) and the new + # path is judged like any other. + ["git", "diff", "-z", "--name-only", "--no-renames", "--diff-filter=A", f"{base}...{head}", "--", "docs/now/"], capture_output=True, text=True,