Skip to content

Commit f2400a4

Browse files
njhensleymchmarny
andauthored
fix(docs): fail-closed MDX check for bare '<' + MDX-safe content (NVIDIA#2127)
Signed-off-by: Nathan Hensley <nhensley@nvidia.com> Co-authored-by: Mark Chmarny <mchmarny@users.noreply.github.com>
1 parent 1dd58ed commit f2400a4

6 files changed

Lines changed: 241 additions & 9 deletions

File tree

docs/contributor/api-server.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ and serializes the `ErrorResponse` shape (`code`, `message`, `details`,
122122

123123
Critical rule, enforced at this single chokepoint:
124124

125-
> Embed `Cause.Error()` in `details["error"]` **only when status < 500**.
125+
> Embed `Cause.Error()` in `details["error"]` **only when `status < 500`**.
126126
> 4xx errors typically carry validator feedback the client needs;
127127
> 5xx errors carry internal paths, kubeconfig contents, or upstream
128128
> service hostnames that must not leak.

docs/integrator/aks-gpu-setup.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,8 +308,8 @@ check-nvidia-smi) for training and inference, and the training performance
308308
phase passes the calibrated NCCL all-reduce gate at 157.29 GB/s (16 GiB
309309
message size across 2 nodes, gate >= 150). The Dynamo inference counterpart
310310
(`h100-aks-ubuntu-inference-dynamo`) passes `inference-perf` at
311-
148,004 tok/s throughput (gate >= 50,000) with 579.70 ms TTFT p99
312-
(gate <= 2,000) — Qwen/Qwen3-8B at 256 concurrency per GPU via
311+
148,004 tok/s throughput (gate `>= 50,000`) with 579.70 ms TTFT p99
312+
(gate `<= 2,000`) — Qwen/Qwen3-8B at 256 concurrency per GPU via
313313
dynamo-router on a single ND96isr node. Other SKUs and node images have
314314
not been exercised — run `aicr validate` after deployment and report gaps.
315315

docs/user/component-catalog.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Not every component appears in every recipe. The recipe engine selects component
7070

7171
Production GPU leaf recipes (H100, GB200, RTX Pro 6000 on EKS / AKS / GKE / OKE / LKE) enable the NFD Topology Updater. It publishes per-node `NodeResourceTopology` CRDs that describe NUMA zones, GPU-to-NUMA affinity, and NIC-to-NUMA affinity. Runtime consumers (NUMA-aware schedulers, debugging via `kubectl get noderesourcetopologies`) can read these CRDs without further configuration.
7272

73-
The Topology Updater requires the kubelet `podResources` gRPC socket. The `KubeletPodResources` feature gate has been on by default since Kubernetes 1.15 (Beta) and reached GA in Kubernetes 1.28; AICR's recipe constraints on the affected leaves require K8s ≥ 1.30 or higher, so this is satisfied in practice. Recipes targeting Kubernetes < 1.15 must enable the feature gate explicitly. Kind / KWOK simulated clusters do not run a real kubelet and therefore leave the Topology Updater disabled — kind-based recipes will not see `NodeResourceTopology` CRDs.
73+
The Topology Updater requires the kubelet `podResources` gRPC socket. The `KubeletPodResources` feature gate has been on by default since Kubernetes 1.15 (Beta) and reached GA in Kubernetes 1.28; AICR's recipe constraints on the affected leaves require K8s ≥ 1.30 or higher, so this is satisfied in practice. Recipes targeting Kubernetes `< 1.15` must enable the feature gate explicitly. Kind / KWOK simulated clusters do not run a real kubelet and therefore leave the Topology Updater disabled — kind-based recipes will not see `NodeResourceTopology` CRDs.
7474

7575
See the upstream [Topology Updater docs](https://kubernetes-sigs.github.io/node-feature-discovery/stable/usage/nfd-topology-updater.html) for runtime consumer examples.
7676

docs/user/validation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,7 @@ exits 8 by default, and is informational only under `--fail-on-error=false`.)
530530
| **B** | `inference-perf` is selected but `dynamo-platform` is not in recipe `componentRefs` | `skipped - dynamo-platform not in recipe components` |
531531
| **C** | `dynamo-platform` is declared but the `DynamoGraphDeployment` CRD is not installed on the cluster (operator not deployed yet) | `skipped - DynamoGraphDeployment CRD not installed on cluster (dynamo-platform component declared but operator not deployed yet)` |
532532

533-
Guards fire before any cluster mutation, so skips are cheap (typically < 10 s).
533+
Guards fire before any cluster mutation, so skips are cheap (typically `< 10 s`).
534534

535535
## Configured GPU allocation policy
536536

tools/check-docs-mdx

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,27 @@
2424
# 5. Bare angle-bracket placeholders (<word>) outside fenced and inline code
2525
# — MDX interprets them as JSX component tags and fails on unexpected
2626
# content that follows.
27+
# 6. Bare '<' NOT followed by a valid JSX name-start (fail-closed allowlist)
28+
# — a '<' before anything other than a letter, '/', '!', or '>' (e.g.
29+
# '<=', '< 5', '<3', '<-', or a trailing '<') is a parse error. Unlike
30+
# checks 4 and 5, which match a '<' that IS followed by a name-start,
31+
# this rule fails closed on every other '<'-follow, so future hazards in
32+
# that family are caught by default rather than needing a new pattern.
33+
#
34+
# Code handling (applies to all checks): fenced code blocks in BOTH backtick
35+
# (```) and tilde (~~~) styles are skipped, following CommonMark fence rules —
36+
# a fence opens on a run of >= 3 of either character and closes only on a later
37+
# line whose leading run is the SAME character and at least as long. Inline
38+
# code spans are stripped before scanning; a span opens on a run of N backticks
39+
# and closes at the next run of exactly N backticks, so spans of any
40+
# backtick-run length are honored (not just single-backtick spans).
41+
#
42+
# NOTE: this checker remains an APPROXIMATION of the MDX parser, not a
43+
# reimplementation of it. It can still drift on hazard classes outside the
44+
# '<'-follow family covered above (e.g. malformed JSX attributes, unbalanced
45+
# expression braces spanning lines, or acorn-level syntax errors). A green run
46+
# is NOT parser-level validation — it means none of the known hazard patterns
47+
# matched, not that Fern's MDX parser will accept the file.
2748

2849
set -euo pipefail
2950

@@ -55,14 +76,21 @@ for dir in "${SEARCH_DIRS[@]}"; do
5576
echo "MDX: non-self-closing void element: ${file}:${match}"
5677
ERRORS=$((ERRORS + 1))
5778
done < <(awk '
58-
/^```/ { in_fence = !in_fence; next }
79+
match($0, /^(`{3,}|~{3,})/) {
80+
m = substr($0, RSTART, RLENGTH); ch = substr(m, 1, 1); len = length(m)
81+
if (!in_fence) { in_fence = 1; fch = ch; flen = len; next }
82+
else if (ch == fch && len >= flen) { in_fence = 0; next }
83+
# else: fence-like line inside a fence of a different char/shorter run
84+
# is content -> fall through to the in_fence guard below.
85+
}
5986
in_fence { next }
6087
{ print NR": "$0 }
6188
' "$file" | grep -E "<(${VOID_ELEMENTS})([[:space:]][^>]*)?" | grep -Ev "<(${VOID_ELEMENTS})([[:space:]][^>]*)?[[:space:]]*/>" || true)
6289
done < <(find "${dir}" -type f \( -name '*.md' -o -name '*.mdx' \) "${EXCLUDE_ARGS[@]}" 2>/dev/null)
6390
done
6491

65-
# --- Checks 2, 3, 4, 5: bare braces, HTML comments, autolinks, bare tags ---
92+
# --- Checks 2, 3, 4, 5, 6: bare braces, HTML comments, autolinks, bare tags,
93+
# and bare '<' not starting a valid tag ---
6694
# All evaluated outside fenced code blocks; inline code spans are stripped
6795
# before the line is scanned so legitimate uses inside backticks don't trip.
6896
for dir in "${SEARCH_DIRS[@]}"; do
@@ -72,7 +100,13 @@ for dir in "${SEARCH_DIRS[@]}"; do
72100
# `set -e` does not abort the whole scan at the first offending file
73101
# (otherwise violations in later files are silently skipped).
74102
if ! awk '
75-
/^```/ { in_fence = !in_fence; next }
103+
match($0, /^(`{3,}|~{3,})/) {
104+
m = substr($0, RSTART, RLENGTH); ch = substr(m, 1, 1); len = length(m)
105+
if (!in_fence) { in_fence = 1; fch = ch; flen = len; next }
106+
else if (ch == fch && len >= flen) { in_fence = 0; next }
107+
# else: fence-like line inside a fence of a different char/shorter run
108+
# is content -> fall through to the in_fence guard below.
109+
}
76110
in_fence { next }
77111
{
78112
line = $0
@@ -81,7 +115,37 @@ for dir in "${SEARCH_DIRS[@]}"; do
81115
# below (or any later check). Doing this before the comment scan
82116
# prevents a backticked, unterminated `{/*` from opening a bogus
83117
# multi-line region that swallows real hazards on following lines.
84-
gsub(/`[^`]*`/, "", line)
118+
#
119+
# CommonMark inline code spans open with a run of N backticks and close
120+
# at the next run of EXACTLY N backticks; the text between (shorter runs
121+
# included) is code. Strip each such span for ANY N — this subsumes the
122+
# single-backtick (N=1) case. A run with no matching closing run of
123+
# equal length is left as literal text (not stripped).
124+
stripped = ""
125+
tail = line
126+
while (match(tail, /`+/)) {
127+
n = RLENGTH
128+
stripped = stripped substr(tail, 1, RSTART - 1)
129+
openrun = substr(tail, RSTART, RLENGTH)
130+
after = substr(tail, RSTART + RLENGTH)
131+
pos = 1
132+
closed = 0
133+
while (match(substr(after, pos), /`+/)) {
134+
abspos = pos + RSTART - 1
135+
if (RLENGTH == n) {
136+
tail = substr(after, abspos + RLENGTH)
137+
closed = 1
138+
break
139+
}
140+
pos = abspos + RLENGTH
141+
}
142+
if (!closed) {
143+
stripped = stripped openrun after
144+
tail = ""
145+
break
146+
}
147+
}
148+
line = stripped tail
85149
# MDX comments {/* ... */} are valid (and used for license headers
86150
# and the auto-generated splice markers in recipe-health.md). Treat
87151
# them like code: continue an open multi-line comment, strip
@@ -132,6 +196,20 @@ for dir in "${SEARCH_DIRS[@]}"; do
132196
printf "MDX: bare <word> tag outside code fence: %s:%d: %s\n", FILENAME, NR, $0
133197
errors++
134198
}
199+
# Check 6 (fail-closed allowlist): a bare `<` that is NOT followed by a
200+
# valid JSX name-start. MDX only accepts `<` before a letter (element),
201+
# `/` (closing tag), `!` (declaration/comment, caught by check 3), or `>`
202+
# (fragment); anything else — `<=`, `< 5`, `<3`, `<-`, or a trailing `<`
203+
# at end of line — is a parse error (e.g. `(gate <= 2,000)` yields
204+
# "Unexpected character = (U+003D) before name"). This complements
205+
# checks 4 and 5 rather than replacing them: they cover the cases where
206+
# `<` IS followed by a name-start (autolinks, placeholders), while this
207+
# covers the cases where it is not. Inverting to an allowlist means any
208+
# future `<`-follow hazard fails closed by default.
209+
line ~ /<([^A-Za-z!\/>]|$)/ {
210+
printf "MDX: bare < not starting a valid tag: %s:%d: %s\n", FILENAME, NR, $0
211+
errors++
212+
}
135213
END { exit (errors > 0) ? 1 : 0 }
136214
' "$file" 2>/dev/null; then
137215
ERRORS=$((ERRORS + 1))

tools/check-docs-mdx_test.sh

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
# Unit harness for tools/check-docs-mdx check 6 — the fail-closed allowlist
17+
# rule for a bare '<' not followed by a valid JSX name-start.
18+
# Run directly: bash tools/check-docs-mdx_test.sh
19+
# Wired into CI via `make test` (test-shell target, runs tools/*_test.sh).
20+
#
21+
# Hermetic: builds fixture .md files in a temp dir and runs the checker against
22+
# them, so no docs/ content is read and nothing on disk is mutated. The
23+
# fixtures pin the regression from issue #2050 (Fern's MDX parser rejects
24+
# '(gate <= 2,000)' with "Unexpected character = (U+003D) before name", which
25+
# the denylist-era checker reported as OK) and guard against false positives
26+
# when the same token is safely wrapped in inline or fenced code.
27+
set -uo pipefail
28+
29+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
30+
CHECK="${SCRIPT_DIR}/check-docs-mdx"
31+
32+
TMPDIR_TEST="$(mktemp -d)"
33+
trap 'rm -rf "${TMPDIR_TEST}"' EXIT
34+
35+
fails=0
36+
pass() { echo "PASS: $1"; }
37+
fail() { echo "FAIL: $1$2"; fails=$((fails + 1)); }
38+
39+
# run <dir>: capture combined stdout+stderr into $OUT and exit code into $RC.
40+
OUT=""
41+
RC=0
42+
run() {
43+
OUT="$("${CHECK}" "$1" 2>&1)"
44+
RC=$?
45+
}
46+
47+
check_rc_nonzero() { # <name>
48+
if [[ "${RC}" != "0" ]]; then pass "$1"; else fail "$1" "want nonzero rc, got 0"; fi
49+
}
50+
check_rc_zero() { # <name>
51+
if [[ "${RC}" == "0" ]]; then pass "$1"; else fail "$1" "want rc=0, got ${RC}"; fi
52+
}
53+
check_contains() { # <name> <needle>
54+
if [[ "${OUT}" == *"$2"* ]]; then pass "$1"; else fail "$1" "expected to contain: $2"; fi
55+
}
56+
check_absent() { # <name> <needle>
57+
if [[ "${OUT}" != *"$2"* ]]; then pass "$1"; else fail "$1" "expected NOT to contain: $2"; fi
58+
}
59+
60+
# --- Fixture 1: the #2050 regression — a bare '<= ' outside any code span. ---
61+
# The checker MUST fail closed here. If check 6 is removed, this token is not
62+
# a void element (check 1), autolink (check 4), or <name-start> tag (check 5),
63+
# so nothing else flags it and this assertion fails — proving the rule is what
64+
# catches it.
65+
DIR_HAZARD="${TMPDIR_TEST}/hazard"
66+
mkdir -p "${DIR_HAZARD}"
67+
cat >"${DIR_HAZARD}/bare-lt.md" <<'MD'
68+
# Bare less-than-or-equal hazard
69+
70+
The TTFT p99 stays low (gate <= 2,000) under the calibrated inference gate.
71+
MD
72+
73+
run "${DIR_HAZARD}"
74+
check_rc_nonzero "bare-lt-exits-nonzero"
75+
check_contains "bare-lt-reported" "MDX: bare < not starting a valid tag"
76+
check_contains "bare-lt-line-cited" "bare-lt.md:3:"
77+
78+
# --- Fixture 2: the SAME token, but safely wrapped. No false positive. ---
79+
# Inline backtick span and fenced code block both hide the '<=' from every
80+
# check, so a clean fixture built only from wrapped hazards must pass.
81+
DIR_SAFE="${TMPDIR_TEST}/safe"
82+
mkdir -p "${DIR_SAFE}"
83+
cat >"${DIR_SAFE}/wrapped-lt.md" <<'MD'
84+
# Wrapped less-than-or-equal is safe
85+
86+
The TTFT p99 stays low (gate `<= 2,000`) under the calibrated inference gate.
87+
88+
```text
89+
inference-perf TTFT p99 gate <= 2,000 ms
90+
```
91+
92+
A valid element like <br /> and a closing </div> must also stay clean.
93+
MD
94+
95+
run "${DIR_SAFE}"
96+
check_rc_zero "wrapped-lt-exits-zero"
97+
check_absent "wrapped-lt-no-violation" "bare < not starting a valid tag"
98+
99+
# --- Fixture 3: '<= ' inside a tilde (~~~) fenced code block. No false pos. ---
100+
# CommonMark honors ~~~ fences as code; the checker must skip their contents
101+
# just like ``` fences, so the hazard token stays hidden.
102+
DIR_TILDE="${TMPDIR_TEST}/tilde"
103+
mkdir -p "${DIR_TILDE}"
104+
cat >"${DIR_TILDE}/tilde-fence.md" <<'MD'
105+
# Tilde fence hides the hazard
106+
107+
~~~
108+
inference-perf TTFT p99 gate <= 2,000 ms
109+
~~~
110+
MD
111+
112+
run "${DIR_TILDE}"
113+
check_rc_zero "tilde-fence-exits-zero"
114+
check_absent "tilde-fence-no-violation" "bare < not starting a valid tag"
115+
116+
# --- Fixture 4: '<= ' inside a double-backtick (``…``) span. No false pos. ---
117+
# CommonMark closes an N-backtick span at the next run of exactly N backticks;
118+
# the checker strips spans of any run length, so the '<=' inside is code.
119+
DIR_DBT="${TMPDIR_TEST}/dbt"
120+
mkdir -p "${DIR_DBT}"
121+
cat >"${DIR_DBT}/double-backtick.md" <<'MD'
122+
# Double-backtick span hides the hazard
123+
124+
Use ``(gate <= 2,000)`` to express the inference gate inline.
125+
MD
126+
127+
run "${DIR_DBT}"
128+
check_rc_zero "double-backtick-exits-zero"
129+
check_absent "double-backtick-no-violation" "bare < not starting a valid tag"
130+
131+
# --- Fixture 5: a triple-backtick fence that CONTAINS a lone double-backtick. ---
132+
# The fence-length rule requires the closing run to be the SAME char and at
133+
# least as long as the opener, so an inner ``` shorter run (or the lone ``)
134+
# must NOT close the fence early and expose the hazard on a later line.
135+
DIR_LEN="${TMPDIR_TEST}/fencelen"
136+
mkdir -p "${DIR_LEN}"
137+
cat >"${DIR_LEN}/fence-length.md" <<'MD'
138+
# Fence-length rule keeps the block open
139+
140+
```
141+
here is a lone `` double backtick inside the block
142+
inference-perf TTFT p99 gate <= 2,000 ms
143+
```
144+
MD
145+
146+
run "${DIR_LEN}"
147+
check_rc_zero "fence-length-exits-zero"
148+
check_absent "fence-length-no-violation" "bare < not starting a valid tag"
149+
150+
if (( fails > 0 )); then
151+
echo "${fails} test(s) failed"
152+
exit 1
153+
fi
154+
echo "All check-docs-mdx tests passed"

0 commit comments

Comments
 (0)