Skip to content

fix(gen-zh-hant): rewrite /zh-Hans/ links to marketing pages and clusters - #266

Merged
os-tesla merged 1 commit into
mainfrom
claude/sharp-mendel-1087cg-issue-265
Oct 16, 2026
Merged

os-tesla merged 1 commit into
mainfrom
claude/sharp-mendel-1087cg-issue-265

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #265

What changed

scripts/gen-zh-hant.mjs zhHantTargetBuilds now answers true for a single-segment target (/zh-Hans/SLUG/) when the slug is a marketing page or a cluster. Before this change, every single segment outside STATIC_ROUTES answered false. The generated zh-Hant post therefore kept the /zh-Hans/ link, content-lint's cross-locale rule (#179) refused it because /zh-Hant/SLUG/ IS built, and pnpm build exited 1.

Both families come from one route, src/pages/[lang]/[cluster].astro. For every locale it emits every slug in CLUSTERS plus every slug getMarketingPages(locale) returns. For zh-Hant that means every page authored in en, zh-Hans (derived by s2t) or zh-Hant. So the predicate reads:

  • clusters: CLUSTERS imported from src/lib/clusters.ts. This is the same module, and the same Node type-stripping import, that content-lint's readClusterSlugs uses.
  • pages: content/pages/{zh-Hant,zh-Hans,en}/SLUG.ts, read by filename the way content-lint's readLocaleTree reads them, through the HANT_SOURCE_LOCALES list the glossary branch already uses. The page modules cannot be imported under plain Node, because they import other modules without an extension.

An unknown slug still answers false, stays unrewritten and is still reported. #142's rule is unchanged: rewrite only where the Traditional target is provably built, and only on link targets.

Before and after:

// before (origin/main 45d5ce9): a single segment builds only if it is a static route
if (segments.length === 1 && STATIC_ROUTES.has(segments[0])) return true;
// ... blog, glossary ...
return false;

// after
function zhHantTargetBuilds(rest, slugBuilds = singleSegmentBuilds) {
  // ...
  if (segments.length === 1) return STATIC_ROUTES.has(segments[0]) || slugBuilds(segments[0]);

function singleSegmentBuilds(slug) {
  if (CLUSTER_SLUGS.has(slug)) return true;
  return HANT_SOURCE_LOCALES.some((locale) =>
    existsSync(path.join(PAGES, locale, `${slug}.ts`))
  );
}

slugBuilds is a parameter only so the pinned fixtures can run the real function off a stub. I added two link-rewrite fixtures, and they run inside every pnpm dev and pnpm build:

  • [t](/zh-Hans/known/) becomes [t](/zh-Hant/known/) (positive).
  • [t](/zh-Hans/ghost/) is left as it is (negative).

The header's "WHERE NO CHECK IS NEEDED" bullets now describe the new behaviour.

Sensitivity leg (scratch fixture, never committed)

A temporary paragraph went into content/blog/ai-wrote-your-app-dare-to-merge/index.zh-Hans.mdx through ablation-replace. That tool requires the anchor to hit, and it proves the restore by blob hash and an empty git diff HEAD. A trap then restored both the zh-Hans and zh-Hant files from HEAD.

Leg A (positive). The paragraph links [本体页](/zh-Hans/ontology/), [对比页](/zh-Hans/compare/#faq) and [AI 原生应用平台](/zh-Hans/ai-native-app-platform/) (a cluster).

before, 45d5ce9 after, 8374487
generated zh-Hant line [本體頁](/zh-Hans/ontology/)、[對比頁](/zh-Hans/compare/#faq) 與 [AI 原生應用平臺](/zh-Hans/ai-native-app-platform/) [本體頁](/zh-Hant/ontology/)、[對比頁](/zh-Hant/compare/#faq) 與 [AI 原生應用平臺](/zh-Hant/ai-native-app-platform/)
generator exit 0, ⚠ zh-Hant: left 3 /zh-Hans/ link(s) as-is — no provable Traditional build exit 0, no skip warning
pnpm content:lint exit 1, ✗ content lint failed (3 blocking issues): three Cross-locale internal link … IS built errors (ontology, compare#faq, ai-native-app-platform) exit 0, ✓ content lint passed (347 files, 46 glossary terms checked)

Leg B (negative). The paragraph links [不存在](/zh-Hans/no-such-page-265/). The result is the same before and after: the generated line keeps /zh-Hans/no-such-page-265/, and the generator reports left 1 /zh-Hans/ link(s) as-is.

The cluster link fails in exactly the same way as the marketing-page links, so it is the same defect and it is fixed here too.

Ablation (are the fixtures sensitive?)

With the fix committed, ablation-replace replaced return STATIC_ROUTES.has(segments[0]) || slugBuilds(segments[0]); with return STATIC_ROUTES.has(segments[0]);. The tool reported ok mutation landed: anchor 1 -> 0, blob b496693887d8 -> b50235dea050. node scripts/gen-zh-hant.mjs then exited 1:

✗ gen-zh-hant: fixture failed
  [link-rewrite]
  in:       "[t](/zh-Hans/known/)"
  expected: "[t](/zh-Hant/known/)"
  actual:   "[t](/zh-Hans/known/)"

It went red, as predicted. Only the positive case failed; the negative case is correct either way. Restore: ok restored: blob == HEAD (b496693887d8) and git diff HEAD is empty.

Provably built, checked against dist/

After pnpm build, I checked every slug the new predicate can answer true for: 17 pages and 5 clusters, 22 in all. Each has a dist/zh-Hant/SLUG/index.html, so none is missing. dist/zh-Hant/no-such-page-265/ is absent.

Gates (HEAD 8374487)

gate exit verdict line
pnpm content:lint 0 ✓ content lint passed (347 files, 46 glossary terms checked)
pnpm content:lint --published 0 ✓ content lint passed (347 files, 46 glossary terms checked)
pnpm check (shared lock) 0 VERDICT command-exit 0; Result (160 files): 0 errors, 0 warnings, 0 hints
pnpm build (shared lock) 0 VERDICT command-exit 0; ✓ zh-Hant: generated 47, kept 0 hand-maintained, 7 with body links pointed at /zh-Hant/; [build] Complete!; ✓ content lint --dist links passed (98464 internal links in 912 built pages resolve to built files)
pnpm seo:smoke 0 SEO smoke test passed (902 HTML pages checked; 586 sitemap URLs against 316 noindexed pages; …)

git status --porcelain is empty after the build, so no generated zh-Hant file moved. That is expected: no zh-Hans post on main links a page or a cluster yet.

Acceptance notes


Generated by Claude Code

…ters

zhHantTargetBuilds answered false for every single-segment target outside
STATIC_ROUTES, so a zh-Hans body link to /zh-Hans/ontology/ (a marketing page)
or /zh-Hans/ai-native-app-platform/ (a cluster) survived into the generated
zh-Hant file. content-lint's cross-locale rule then refused that file, because
/zh-Hant/<slug>/ is built for both, and `pnpm build` exited 1.

A single segment now builds when the slug is in CLUSTERS (imported from
src/lib/clusters.ts, as content-lint does) or names a page in
content/pages/{zh-Hant,zh-Hans,en}/ (read by filename, as content-lint does,
mirroring the glossary branch). Unknown slugs stay unrewritten and reported.
Two pinned link fixtures run the real predicate off a stub slug registry.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GeNDQH7E5S2rX8ajVAPxv
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT — repo:www.objectos.ai · seat 2 (session_017GeNDQH7E5S2rX8ajVAPxv), 2026-10-16T08:40Z, on head 8374487.

Check Result
Shape Draft PR to main. The first line is Fixes #265, the only closing keyword in the body. Assignee os-tesla.
File surface 1 file, scripts/gen-zh-hant.mjs (+36/−7). src/lib/clusters.ts is imported read-only, unchanged. No governed path.
CI on head Build and deploy: completed/success on 8374487.
Rule kept (#142) Rewrite only where the Traditional target is provably built. Single segments now prove it two ways: the slug is in CLUSTERS, or content/pages/{zh-Hant,zh-Hans,en}/SLUG.ts exists. Anything else is still reported and left as is.
Widening accepted Clusters were added because the dev measured the same failure on /zh-Hans/ai-native-app-platform/, and every cluster is emitted for every locale from the same route. The dev's cross-check after the build found all 22 slugs the predicate can accept present under dist/zh-Hant/, with 0 missing.
Sensitivity leg, reproduced by the seat A scratch paragraph went into ai-wrote-your-app-dare-to-merge/index.zh-Hans.mdx with links to /zh-Hans/ontology/, /zh-Hans/compare/ and /zh-Hans/no-such-page-seat2/. With this PR's generator the zh-Hant line came out as [本體頁](/zh-Hant/ontology/)、[對比頁](/zh-Hant/compare/) 與 [不存在](/zh-Hans/no-such-page-seat2/), with left 1 reported. With main's generator on the same input the line was unchanged and left 3 was reported. The scratch edit was restored with git checkout HEAD --; git diff HEAD is empty.
Fixtures One positive (/zh-Hans/known/ → /zh-Hant/known/) and one negative (ghost left as is). They run the real predicate with only the slug registry stubbed, so the dev's ablation, which removed slugBuilds, turned the positive red.

Accepted note: the registry drops a page file whose slug differs from its filename, and this predicate reads filenames. All 34 page files match today, and content lint --dist links would catch a resulting dead link at build time. Noted, not filed.

Next: PR #264 (/zh-Hans/compare/) and PR #263 (/zh-Hans/ontology/) can now restore their zh-Hans links before their slots.

Landing: ready, then squash by the seat.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants