ci: fail the build when an llms body carries a numeric character reference or a malformed link target - #285
Merged
Merged
Conversation
…rence or a malformed link target #197's fix is a fumadocs-core patch pinned to one exact version, and nothing asserted its outcome. The locale-surface gate now scans the built `llms-full.txt` body and every per-page `llms.mdx` body for HTML numeric character references and for markdown link targets that do not close with a literal `)`, runs a live negative control on the #197 shape before every scan, and checks the `llms.mdx` body set against the content-tree oracle so a directory that was never written cannot read as clean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…were built Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
hotlong
marked this pull request as ready for review
September 24, 2026 02:04
This was referenced Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #282
Notation.
AMPstands for one literal ampersand. This tracker's body sanitizer decodes HTML numeric character references, including inside code fences, soAMP#x2A;below means the six characters ampersand, hash, x, 2, A, semicolon. The gate's own output uses the same convention, because it lands in a markdown step summary.What changed
.github/scripts/check-locale-surface.mjsnow asserts #197's outcome on the built bytes of both consumers ofpage.data.getText('processed'):apps/docs/.next/server/app/llms-full.txt.bodyand every.bodyfile underllms.mdx/.numeric-character-referencemdast-util-to-markdown's encoder emits)malformed-link-target](opener whose target does not close with a literal)before whitespacellms-page-body-missingllms.mdxbody. The content-tree oracle this gate already builds says which bodies must exist (79 today), so a half-written directory cannot read as cleanno-link-targetsartifact-missing(existing rule)llms.mdxdirectory was never built.github/workflows/ci.yml: no new step. The existingLocale surfacestep already runs this script right afterpnpm turbo run buildand beforePackage the Worker…. Its comment block gains one paragraph saying it now guards #197. The size-gate comment block is untouched.Why a rule in this script and not a new one
.github/scripts/check-*.mjswith a--self-testmode must be registered intools/ci-scripts/run-self-tests.mjs, because that runner fails on a self-test it does not list. That file is outside the declared surface (.github/scripts/andci.yml).content/docs/. That is what turns "79 bodies were read" into a checked claim, not a count someone happened to print.llms-full.txt.body) at the right point in the job, and it already has the fixture,RULES-coverage and per-artifact machinery.llms bodies are markdown, not HTML), with a per-consumer table and a line naming thefumadocs-coreversion thatapps/docsresolves beside the version the patch is pinned to. A red on a bump therefore names its cause, even though the step is calledLocale surface.Negative control: both shapes, and the log says which ran
[AI Builder](/docs/build/ai-builderAMP#x29; — …) goes through the sameencodingFindingspath once per consumer. Both rules must fire for both consumers, ornegative-control-passedfails the run. The green log line on this branch reads: "Live negative control: the [finding] llms-full.txt emits HTML numeric entities into a plain-text file, and two of them break the markdown link they sit in #197 shape (a link target whose)is encoded asAMP#x29;) was fed through this scan for both consumers and firednumeric-character-referenceandmalformed-link-targetfor each — the scan below can go red, so its result is a measurement."c22c02ewith no.next), the gate exits 1 withartifact-missing×4 and printsNOT MEASURED — not builtrows for both consumers.--self-test(run in CI bypnpm turbo run test) grows to 28 cases over 14 rules. It adds one red case per new rule and consumer, a green case for ampersands and parentheses in URLs, exact-count arithmetic for the scan, and a check that each of the four (consumer × rule) pairs has a fixture that turns it red. It also blinds the scan to show the live control can fire.Measurements: the card's red and green states, on real builds
Built with
NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force(as in CI), read off.next. An independent scratch counter gives the same figures as the gate.fumadocs-core/ patchd725081, before #197's pinAMP#x2A;×58,AMP#x60;×5,AMP#x29;×4)cb0c146(this branch's base; the branch changes no build input)a69ef4d, built as-iscb0c146+ #239's version change, pin deletedThe
d725081figures match the card exactly, including the byte count PR #281 recorded (695176). #239's head is a pre-#281 tree, which is why it has 650 targets. That count matches the decision comment #281 quotes.The PM's mechanism assumption about the pin was partly wrong
The claim was: on a version change the patch silently stops applying and
pnpm installstill exits 0. Measured with pnpm 10.28.2 and #239's change applied tocb0c146:pnpm install --no-frozen-lockfileand--lockfile-onlyboth exit 1 withERR_PNPM_UNUSED_PATCH The following patches were not used: fumadocs-core@16.8.12. A bump is loud wherever the lockfile is regenerated.pnpm install --frozen-lockfilewith exit 0 and no warning. The frozen install does not check for an unused patch.The gate is still needed. The inaccurate paragraph in the patch header is filed as #284 (under
patches/, outside this surface).Ablation: the live control fails the real gate
On the committed tree, one mutation at a time, each confirmed on disk by
grep -cof the anchor (1 → 0) and of the injected text (0 → 1), with a trap restore fromHEAD. The restore was proven by blob hash equal to theHEADblob and an emptygit diff HEAD, not by an exit code:--self-testNUMERIC_REFERENCE→/(?!)/gLive negative control: FAILED — llms-full.txt:numeric-character-reference, llms.mdx:numeric-character-reference did not fire on the #197 shape.if (false)malformed-link-targetGates run on
76dca92(the head of this PR)Each exit code was captured before any pipe. The quoted line is the gate's own verdict.
node .github/scripts/check-locale-surface.mjs --self-test: exit 0,✓ self-test: 28 case(s) over 14 rule(s) and 3 artifact(s) — …node .github/scripts/check-locale-surface.mjson thecb0c146build: exit 0,✓ every advertised URL has a source file … and neither llms consumer carries a numeric character reference or a malformed link targetd725081build: exit 1,✗ locale surface: 35 finding(s)pnpm turbo run test --force: exit 0,✓ 7 self-test(s) passednode scripts/pm/check-half-states.mjs --self-test: exit 0,✓ check-half-states self-test: 1551 cases pass.node apps/docs/scripts/gen-zh-hant.mjs --check: exit 0,✓ zh-Hant: 73 generated file(s) match …node .github/scripts/check-node-floor.mjs --self-test: exit 0ci.ymlparses as YAML; no control bytes in either changed file.Not run locally: the Worker packaging, the size weigh-in and the preview smoke check. This PR changes no build input, and CI runs all three.
Scope limits, stated in the script header
[a](url "t")) would read as malformed. None exist today. Supporting one is a deliberate change to the rule.Not touched
apps/docs/,content/docs/,patches/, rootpackage.json,pnpm-lock.yaml,tools/ci-scripts/, and the size-gate comment block inci.yml. #239 is not pushed to, commented on or merged.🤖 Generated with Claude Code
https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
Generated by Claude Code
Generated by Claude Code