Repository navigation
ci: pin the generated sitemap's locale composition against the content tree - #193
Merged
Merged
Conversation
…t tree The same missing-language-argument defect has been found and fixed in four generated artifacts -- sitemap.ts (#169), llms.txt (#170), llms-full.txt (#177) and the OG card (#185) -- and every one was green on every gate for its entire lifetime. On the pre-#169 tree sitemap.xml advertised 3892 entries over 574 distinct URLs, 3318 of them exact duplicates, against 335 real MDX files, with type-check, build and test all passing. Nothing in the repo asserted anything about a generated artifact's locale composition, so the defect was undetectable short of reading the XML by hand. check-locale-surface.mjs derives the expected URL set every run from content/docs/ and apps/docs/lib/i18n.ts, then compares it to the built sitemap.xml. Both directions fail: an advertised URL with no source file is an untranslated page being advertised as a translation, and a source file with no advertised URL is a shipped translation invisible to crawlers. Duplicates are counted rather than collapsed, because de-duplicating on read would have hidden all 3318 of them behind a correct distinct count. Nothing is hardcoded that moves on a content PR: a gate needing hand-editing on every translation would be deleted within a month, and the edit that updates the number is the edit that would hide a regression. It reads the build output because importing sitemap.ts pulls in the whole MDX collection and the @/ alias. That makes it a post-build CI step rather than a turbo task -- turbo would hash it against inputs that exclude apps/docs/.next/, and a locale gate replaying a cached green is the failure it exists to end. Its --self-test needs no build and runs under pnpm turbo run test alongside the other two, so its rules stay provably able to go red. ARTIFACTS is the extension point #184 adds the two llms endpoints to. Part of #175 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G5zjYc2BoFV2NjKBBapC7C
hotlong
marked this pull request as ready for review
August 26, 2026 01:23
This was referenced Aug 26, 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 #175
Nothing in this repo could tell you that a generated artifact's locale composition was wrong. This adds the gate that can.
Why
The same missing-language-argument defect has now been found and repaired in four generated artifacts —
sitemap.ts(#169),llms.txt(#170),llms-full.txt(#177), the OG card (#185) — and every one of them was green on every gate for its entire lifetime. On the pre-#169 treesitemap.xmladvertised 3892 entries over 574 distinct URLs, 3318 of them exact duplicates, against 335 real MDX source files. Type-check passed, build passed, test passed. On #177 the same shape meant 4.6 MB of seven-locale text served under arobots.txtthat had just been opened to crawlers.The common root cause is an API that takes an optional language argument and quietly returns every locale's page set when you omit it. Nothing about that is a type error and nothing about it is a build error. It is only visible as a count.
What the gate asserts
.github/scripts/check-locale-surface.mjsderives the expected URL set — every run — from the two authorities the repo already has, then compares it to the builtsitemap.xml:content/docs/**for the pages and which locales really have each one (a translation is a locale-suffixed sibling; its absence means the page falls back to English, and a fallback URL must not be advertised as a translation);apps/docs/lib/i18n.tsfor the locale list and default locale — AGENTS.md names it the authority,check-translation-output.mjsalready reads it the same way, andturbo.jsonalready names it an input to thetesttask.Nothing is hardcoded that moves on a content PR. A gate that pins 346 by hand needs hand-editing on every translation and gets deleted within a month — and worse, the edit that updates the number is the same edit that would hide a regression.
Both directions fail, per the card:
unexpected-urlmissing-urlduplicate-urlartifact-missingartifact-emptytranslation-orphanDuplicates are counted rather than collapsed on read, because de-duplicating would have hidden all 3318 of the pre-#169 tree's behind a correct distinct count.
Composition, not formatting — as adjudicated on the card. The gate reads the
locvalues and compares URL sets. #178's relative-to-absolutellms.txtlink change does not touch it, and #173 changes wheresitemap.tsreads the privacy/terms locale set from, not what it is.Shape and wiring
It reads the build output because importing
sitemap.tspulls in the whole MDX collection and the@/alias — the constraint the card priced, inherited rather than rediscovered. So:ci.ymlinvokes the gate as its own step afterpnpm turbo run build, withshell: bashso theteepipe cannot swallow a non-zero exit (the same trap thenode-floorjob documents).ci-scriptspackage's inputs, which do not includeapps/docs/.next/— and a locale gate replaying a cached green is the exact failure this card exists to end.run-self-tests.mjsgains it inSELF_TESTED, sopnpm turbo run testkeeps running its--self-test. That mode needs no build: it drives the same collect/evaluate pipeline over fixture trees in a temp directory, so the readers are exercised too.turbo.jsonneeded no change. Itstestinputs already name$TURBO_ROOT$/.github/scripts/**, so the self-test re-runs when this script changes. Addingcontent/docs/**there would declare an input the self-test does not read.package.jsongainscheck:locale-surfacefor running it locally after a build.ARTIFACTSis the whole extension point. #184 addsllms.txtandllms-full.txtthere with their own reader and expectation; the harness needs no other change. Per the card's scope note, this PR ships only the sitemap and #184 stays open.Verification
The gate shown red on a real defect, not just green on a good tree.
Leg 1 —
apps/docs/app/sitemap.tschecked out verbatim atee74379(blob079b27b9, proven on disk bygit hash-object). It does not compile against today's tree:That is #169's own signature change working as designed —
languageAlternates(path, locales)made the argument required precisely so a call site could not silently get all seven again. Restoringlib/seo.tstoee74379too was not an option:translatedLocalesdid not exist there, and the OG route and the docs page both import it now.Leg 2 — the same defect reconstructed against today's signature (
source.getPages()with no argument,languageAlternates(path, i18n.languages), iteratei18n.languages), which is behaviourally identical toee74379. Mutation proven on disk by blob hash and by grep of the injected and removed text, then rebuilt:— the issue body's numbers exactly (556 logical paths x 7 locales = 3892; 79 docs + 3 static, x 7 = 574). The gate on that tree, exit 1:
Restore proven by blob hash back to
197084a2and bygit diff HEADreturning 0 lines, then rebuilt —loc count: 346, gate exit 0.Green run, at final commit
fe6abaf:Per-locale counts match the card's measured values exactly, derived rather than declared.
pnpm turbo run type-check --continueandpnpm turbo run testatfe6abaf: both1 successful, 1 total; the test task reports3 self-test(s) passed. The new self-test: 7 cases over 6 rules, every rule demonstrated able to fail, plus a per-locale tally assertion and three cases proving an unreadablei18n.tsis rejected rather than guessed around.Scope
Four files, all within the declared surface. No
apps/docssource file — this card asserts behaviour, it does not change it. The working tree is clean and the sitemap source is byte-identical toorigin/main.Generated by Claude Code