Skip to content

[finding] nothing pins the generated sitemap — the seven-locales-for-every-page defect was green on every gate #175

Closed

Description

@hotlong

Found while implementing #169.

On the pre-#169 tree, apps/docs/app/sitemap.ts emitted 3892 entries covering 574 distinct URLs — 3318 exact duplicates — against 335 real MDX source files. Every gate was green the entire time. Measured on both trees through the same pipeline:

tree entries distinct URLs type-check build test
pre-#169 3892 574 pass pass pass
post-#169 346 346 pass pass pass

Nothing in the repo asserts anything about the generated sitemap, so the defect was undetectable short of reading the XML by hand.

The machinery already exists: tools/ci-scripts/run-self-tests.mjs runs --self-test on each .github/scripts/*.mjs, CI runs it as pnpm turbo run test, and turbo.json already names apps/docs/lib/i18n.ts as an input to that task — so a locale-surface gate is already wired to re-run when the locale list changes.

Suggested gate: count .mdx files under content/docs per locale and assert the built sitemap's per-locale entry counts match. On the #169 tree that is 79 / 62 / 39 / 38 / 39 / 39 / 39 = 335 docs entries, plus 11 for the root and the two legal pages. Drift in either direction is a real defect: too many means untranslated URLs are being advertised again, too few means a shipped translation is invisible to crawlers.

One design note worth pricing before writing it: the check needs the built sitemap.xml, so it is a post-build step rather than a standalone zero-dependency self-test like check-node-floor.mjs. The alternative — importing sitemap.ts directly — pulls in the whole MDX collection and the @/ alias, which is why the measurement above was taken off the build artifact.

Out of #169's declared file surface, hence a separate card.


Generated by Claude Code

Activity

  1. added theissue type on Aug 26, 2026
  2. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Graded: promoted to pm:queue / Task. finding removed — grading is what that label waits for.

    Promoted rather than held, and the case got stronger after this card was written. The same missing-language-argument defect has now been found and fixed in four places, every one of them green on every gate for its entire lifetime: sitemap.ts (#169), llms.txt (#170), llms-full.txt (#177), and the OG card (#185). On #177 that meant 4.6 MB of seven-locale text served under a robots.txt that had just been opened to crawlers.

    Four instances is no longer a pattern worth noting. It is a gate that should exist.

    The card already prices the one real design constraint — the check needs the built output, so it is a post-build step rather than a standalone --self-test, because importing sitemap.ts pulls in the whole MDX collection and the @/ alias. That is the decision the implementer inherits rather than rediscovers.

    Scope note for dispatch: #184 is blocked on this card and carries the assertion shape for the two llms endpoints, already measured. Whatever harness this card settles on should be able to carry those two — build the harness with that in mind, but do not implement #184's assertions here.


    Generated by Claude Code

  3. self-assigned this
    on Aug 26, 2026
  4. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 6
    Session: session_01G5zjYc2BoFV2NjKBBapC7C
    Branch: claude/issue-175-pin-generated-locale-surface
    Worktree: objectos-issue-175
    Domain: n/a (objectos carries no domain:* lanes)
    File surface: a new gate under .github/scripts/ or tools/ci-scripts/, plus whatever wiring it needs (turbo.json, root package.json, .github/workflows/ci.yml). No apps/docs/** source file — this card asserts behaviour, it does not change it. (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus — PM judgement.
    Clause-②: no
    Serial constraints cleared: #173 and #178 are dispatched alongside this one. #173 changes where sitemap.ts gets the privacy/terms locale set from, not what it is (en + zh-Hans either way), so a gate asserting counts is unaffected by which lands first. #178 changes llms.txt links from relative to absolute, which does not touch the per-locale composition this gate asserts. Neither touches the gate surface.

    Read coupling worth naming: if this gate asserts the exact text of a llms.txt link line, #178 will turn it red for a correct reason. Assert composition (which pages, which locales), not link formatting.


    Generated by Claude Code

  5. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Dev claim (os-dev, this card).

    Session: session_01G5zjYc2BoFV2NjKBBapC7C
    Branch: claude/issue-175-pin-generated-locale-surface
    Worktree: /home/user/objectos-issue-175, off origin/main at 6653744
    PR: #193 (draft)

    File surface actually touched, within the declared bounds:

    • .github/scripts/check-locale-surface.mjs (new)
    • .github/workflows/ci.yml
    • tools/ci-scripts/run-self-tests.mjs
    • package.json

    No apps/docs source file. turbo.json was in the allowed surface but needed no change — its test inputs already name $TURBO_ROOT$/.github/scripts/**, so the new self-test re-runs when the script changes, and adding content/docs/** there would declare an input the self-test does not read.


    Generated by Claude Code


    Generated by Claude Code

  6. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
      "issue": 175,
      "status": "done",
      "branch": "claude/issue-175-pin-generated-locale-surface",
      "pr": "https://github.com/objectstack-ai/objectos/pull/193",
      "premise_still_valid": true,
      "head_sha": "fe6abaf",
      "files_changed": [
        ".github/scripts/check-locale-surface.mjs (new, +689)",
        ".github/workflows/ci.yml (+21)",
        "tools/ci-scripts/run-self-tests.mjs (+15/-2)",
        "package.json (+3/-1)"
      ],
      "gates": [
        {
          "gate": "node .github/scripts/check-locale-surface.mjs --self-test",
          "exit": 0,
          "verdict_line": "self-test: 7 case(s) over 6 rule(s) - every rule demonstrated able to fail, on fixtures read through the real readers"
        },
        {
          "gate": "pnpm turbo run test (runs tools/ci-scripts/run-self-tests.mjs)",
          "exit": 0,
          "verdict_line": "3 self-test(s) passed / Tasks: 1 successful, 1 total",
          "note": "was 2 self-tests before this PR; the runner's unlisted-script guard makes the wiring self-enforcing"
        },
        {
          "gate": "pnpm turbo run type-check --continue",
          "exit": 0,
          "verdict_line": "Tasks: 1 successful, 1 total"
        },
        {
          "gate": "pnpm turbo run build --filter=@objectos/docs",
          "exit": 0,
          "verdict_line": "Tasks: 1 successful, 1 total"
        },
        {
          "gate": "node .github/scripts/check-locale-surface.mjs (the new gate, good tree)",
          "exit": 0,
          "verdict_line": "every advertised URL has a source file, and every source file is advertised",
          "measured": "sitemap.xml advertised 346, distinct 346, expected 346, unexpected 0, missing 0, duplicated 0"
        },
        {
          "gate": "node .github/scripts/check-locale-surface.mjs (reconstructed pre-#169 defect)",
          "exit": 1,
          "verdict_line": "locale surface: 781 finding(s)",
          "measured": "sitemap.xml advertised 3892, distinct 574, expected 346, unexpected 228, missing 0, duplicated 553"
        }
      ],
      "summary": "New gate .github/scripts/check-locale-surface.mjs asserts the locale composition of the built sitemap.xml against an oracle derived every run from content/docs/ and apps/docs/lib/i18n.ts - no hardcoded counts, nothing read from app code. Six rules, each with a red fixture: unexpected-url, missing-url, duplicate-url, artifact-missing, artifact-empty, translation-orphan. Wired as a post-build CI step (deliberately not a turbo task: turbo would hash it against inputs excluding apps/docs/.next/, and a cached-green locale gate is the failure this card exists to end), with its --self-test registered in run-self-tests.mjs so pnpm turbo run test keeps proving the rules can go red. ARTIFACTS is the extension point #184 adds the two llms endpoints to; #184's assertions are deliberately not implemented here.",
      "tests": "Oracle validated against the real artifact before any code was written: derived per-locale docs entries en 79, zh-Hans 62, ja 39, de 38, es 39, fr 39, ko 39 = 335, plus 7 root + 2 privacy + 2 terms = 346, matching the built sitemap exactly with zero set difference in either direction - the card's measured values, derived not declared. REVERSE VERIFICATION, run as one foreground script with a restore trap, all under the shared verify lock. Leg 1: apps/docs/app/sitemap.ts checked out verbatim at ee74379, mutation proven on disk by git hash-object == 079b27b96c92ef934367e1d04a6dded3bb67e66a; it does not compile against today's tree - 'app/sitemap.ts(25,23): error TS2554: Expected 2 arguments, but got 1.' / 'Exit status 2' - because #169 made languageAlternates' locales argument required, and restoring lib/seo.ts to ee74379 too was not an option (translatedLocales did not exist there and the OG route plus 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), iterate i18n.languages), mutation proven on disk by blob hash ae4b5d9adcf90f308d478271b298930421672f3b != HEAD blob and by grep counts of injected text (getPages() with no argument: 1) and removed text (translatedLocales(: 0, STATIC_PAGE_LOCALES: 0); rebuilt with --force; artifact re-derived to loc count 3892 over 574 distinct - the issue body's numbers exactly (556 logical paths x 7 = 3892; 79 docs + 3 static, x 7 = 574). Gate exit 1 with 781 findings naming what is wrong: 228 unexpected-url ('https://docs.objectos.ai/de/docs/architecture is advertised but the content tree has no source file for it - an untranslated page is being advertised as a translation') and 553 duplicate-url ('https://docs.objectos.ai/docs/architecture advertised 7 times'). RESTORE proven by git checkout HEAD -- (absolute path), git hash-object back to 197084a227ec741fe275594bf9cb5dbe92b86870, and git diff HEAD returning 0 lines; rebuilt to loc count 346; gate exit 0. Final tree clean, sitemap source byte-identical to origin/main. Final union re-run at the final commit fe6abaf after all edits: type-check and test both 'Tasks: 1 successful, 1 total', gate exit 0.",
      "deviations": [
        "The card's verification recipe says to check out sitemap.ts at ee74379 and rebuild. That exact file cannot build against today's tree - it calls languageAlternates with one argument and #169 made the second required (TS2554), and next.config.mjs sets typescript.ignoreBuildErrors: false. Reported as leg 1 with its compiler output rather than worked around, then the DEFECT (not the file) was reconstructed against today's signature for leg 2. The reconstruction is provably the same emission: 3892 entries over 574 distinct URLs, matching the issue body.",
        "turbo.json is in the declared file surface but was not changed. Its test inputs already name $TURBO_ROOT$/.github/scripts/**, so the new self-test re-runs when the script changes; adding content/docs/** would declare an input the self-test does not read, and the real gate runs outside turbo on purpose."
      ],
      "open_questions": [],
      "out_of_scope_findings": []
    }

    Generated by Claude Code


    Generated by Claude Code

  7. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — PM review of record. PR #193 at fe6abaf.

    item verdict
    PR shape draft ✓, base main at 6653744, Fixes #175 ✓
    File surface new gate + its wiring only; no apps/docs/** source file, as required ✓
    CI build and Node floor both completed/success ✓
    Path-surface fork .github/** and tools/ — not governed surface (.claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/adr/**) ✓
    Self-test registered in run-self-tests.mjs; pnpm turbo run test went from 2 self-tests to 3 ✓

    My verification recipe was wrong, and it was reported rather than worked around

    I said: check out sitemap.ts at ee74379, rebuild, watch the gate go red. That file cannot build against today's tree — it calls languageAlternates with one argument and #169 made the second required, with typescript.ignoreBuildErrors: false. TS2554, exit 2, no build, no measurement.

    The easy paths were to quietly skip the reverse verification, or to loosen something until it compiled. Instead: leg 1 reported with the compiler's own output, then the defect — not the file — reconstructed against today's signature, with the mutation proven on disk by blob hash and by grep counts of both injected and removed text. The rebuilt artifact came back at 3892 entries over 574 distinct URLs: the issue body's numbers exactly, arrived at independently.

    Gate then exits 1 with 781 findings that name what is wrong — 228 unexpected-url, 553 duplicate-url, each naming the URL. Restore proven by blob hash back to 197084a and an empty git diff HEAD, rebuild back to 346, gate green.

    That is a gate demonstrated able to fail on the real defect it exists to catch, not on a fixture that resembles it.

    Design decisions worth keeping

    The oracle reads no app code. Stated in the header as the reason: "The gate must be able to go red when app code is wrong, which it cannot do if app code is where it gets its expectations." The same principle drives re-declaring SITE_URL instead of importing it — "a check that shares a constant with the thing it checks cannot catch that constant being wrong" — with the mismatch designed to surface loudly as every URL landing in both unexpected-url and missing-url.

    The reader deliberately does not de-duplicate. 3318 exact duplicates would otherwise hide behind a correct distinct count. The duplicate-url rule only exists because of that choice.

    Not a turbo task, on purpose. Turbo would hash it against the ci-scripts package's inputs, which do not include apps/docs/.next/ — so a locale regression would replay a cached green. That is the same shared-cache hazard two devs hit independently on this backlog, anticipated rather than suffered.

    shell: bash on the piped step. The default bash -e {0} has no pipefail, so node … | tee takes tee's status and a gate exiting 1 passes the job silently. I checked the provenance: this is PR #74's finding, measured there against a real gate script and fixed the same way. Inheriting an existing adjudication rather than rediscovering it, and the comment says so.

    artifact-empty as its own rule. A format change that leaves the reader matching nothing must fail rather than pass over zero measurements. That is the failure mode that would have made this entire gate worthless.

    One tension to name, for #173

    The privacy/terms locale set is a declared constant in the gate rather than derived — correctly, since those pages are not in content/docs/ at all and the adjudication put the oracle there. But #173 is in flight making sitemap.ts derive that same set from the route modules.

    After both land: translating the privacy policy makes sitemap.ts advertise a third locale automatically, and the gate goes red naming the page until its constant is updated. That is the right outcome, not a defect — the gate has to hold an expectation independent of the thing it checks, or it cannot catch that thing being wrong. But it means translating a legal page is a two-file action by design, and whoever does it should find that out from a red gate naming the page rather than from surprise. Flagging it to #173's dev.

    Deviations — accepted

    turbo.json untouched despite being in the declared surface, with a real reason (its test inputs already name .github/scripts/**; adding content/docs/** would declare an input the self-test does not read). The recipe correction above. The privacy/terms constant, reasoned above.

    Proceeding to ready and merge. #184 is the increment on ARTIFACTS and stays blocked until this lands.


    Generated by Claude Code

  8. added a commit that references this issue on Aug 26, 2026
    d6355de
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions