diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml index 1051ae0b32..d90edf5075 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-13-published-document-fragments.md -2026-08-13-published-document-fragments.md: 9837d4ec1335f8da9910b3d2c2167f4c6d3a1aab -2026-08-13-published-document-fragments.zh.md: f4c68ae01082ab53a251c9951906538f0f4ae770 +2026-08-13-published-document-fragments.md: 4146a592a97b5d8b4e1d3fbabd0f035074cc8001 +2026-08-13-published-document-fragments.zh.md: 6cf76dfa349bbd61d1dd51cb683b696e4c405435 diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md index 9837d4ec13..4146a592a9 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.md @@ -10,9 +10,9 @@ English | [中文](2026-08-13-published-document-fragments.zh.md) ## Decision -`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent or either the target page or requested id is missing. Unit tests cover absent output, clean URLs, `.html` aliases, same-page links, encoded ids, missing ids, missing routes, and external-link exclusion. +`docs:build` and its MPA variant run `verify-doc-site-fragments` after VitePress emits `website/.dist`. The verifier parses every emitted HTML page, resolves each internal fragment link against VitePress clean URLs, and fails when the output is absent, routes are ambiguous, an href is malformed, or either the target page or requested id is missing. Unit tests cover those failures plus clean URLs, `.html` aliases, same-page links, encoded and literal ids, and external-link exclusion. -Generated config, tool, and persistence catalogs emit explicit GitHub-compatible id aliases before punctuation-heavy headings. Authored translated pages add explicit language-neutral aliases when their localized VitePress heading id differs from the shared fragment used by the bilingual pair. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering. +Any fragment target heading whose GitHub id differs from its VitePress id carries an explicit GitHub-compatible alias. Authored English and translated pages place the alias before the heading; translated pages use the English id shared by the bilingual pair. Generated config, tool, and persistence catalogs emit the alias from their owning generator. Source Markdown validation remains independent and continues to reject links that do not resolve under repository rendering. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md index f4c68ae010..6cf76dfa34 100644 --- a/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md +++ b/.agents/notes/implemented/process/2026-08-13-published-document-fragments.zh.md @@ -10,9 +10,9 @@ Status: implemented ## Decision -`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖缺失构建产物、clean URL、`.html` 别名、同页链接、编码 id、缺失 id、缺失路由和外部链接排除。 +`docs:build` 及其 MPA 变体会在 VitePress 生成 `website/.dist` 后运行 `verify-doc-site-fragments`。该校验器解析每个生成的 HTML 页面,按照 VitePress clean URL 解析每个内部 fragment 链接,并在构建产物不存在、路由有歧义、href 格式错误、目标页面不存在或请求的 id 缺失时失败。单元测试覆盖这些失败,以及 clean URL、`.html` 别名、同页链接、编码和字面 id 与外部链接排除。 -生成的配置、工具和持久化目录会在包含较多标点的标题前生成与 GitHub 兼容的显式 id 别名。如果翻译页面的 VitePress 本地化标题 id 与双语对侧文件共享的 fragment 不同,编写者会添加语言无关的显式别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。 +任何 GitHub id 与 VitePress id 不同的 fragment 目标标题都会带有与 GitHub 兼容的显式别名。英文手写页面和翻译页面会在标题前添加别名;翻译页面使用双语对侧文件共享的英文 id。生成的配置、工具和持久化目录由所属生成器输出别名。源码 Markdown 校验保持独立,仍会拒绝在仓库渲染规则下无法解析的链接。 ## Alternatives considered diff --git a/.agents/skills/dsh-doc-site-sync/SKILL.md b/.agents/skills/dsh-doc-site-sync/SKILL.md index bc5b3f48dd..91dd42ae8a 100644 --- a/.agents/skills/dsh-doc-site-sync/SKILL.md +++ b/.agents/skills/dsh-doc-site-sync/SKILL.md @@ -49,7 +49,7 @@ Write normal repository-relative Markdown links in canonical docs. The projector - An image is the exception: its file is copied into the generated tree and referenced from there, so the site serves it regardless of repository visibility. It must be a regular file inside the repository. - External URLs, site-absolute URLs, email links, and fragment-only links remain unchanged. - A missing repository-relative target fails projection instead of silently producing a broken link. -- Cross-language and generated-document fragments use the English GitHub heading id as their canonical id. If a translated or generated heading emits another id, place an explicit `` immediately before it; add generated aliases in the owning generator. +- Cross-page fragments use the English GitHub heading id as their canonical id. If an authored heading emits a different VitePress id, place an explicit `` immediately before it; add generated aliases in the owning generator. Do not write website-specific routes into canonical Markdown just to satisfy VitePress. Use `sourceAliases` for directory-style repository links that should resolve to a mapped index page. @@ -69,6 +69,8 @@ Run the focused website gate before treating the mapping as valid: pnpm docs:check ``` +If Markdown link checks pass but the site build reports a missing fragment, follow the `verify-doc-site-fragments` source and target paths. Preserve the English GitHub id with an explicit alias in authored Markdown or in the owning generator. + Before committing a documentation-site change, run: ```sh diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 9673439fbc..27664fab5e 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -607,7 +607,7 @@ function docSyncLeafGates(options: { pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }), pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts', 'scripts/verify-doc-site-fragments.spec.ts'], { - label: 'documentation projection', + label: 'documentation site checks', }), // Keep the VitePress build itself in one gate because projection rewrites website/.generated. pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }), diff --git a/scripts/verify-doc-site-fragments.spec.ts b/scripts/verify-doc-site-fragments.spec.ts index 2d041a33d5..d06d46f53b 100644 --- a/scripts/verify-doc-site-fragments.spec.ts +++ b/scripts/verify-doc-site-fragments.spec.ts @@ -38,9 +38,29 @@ describe('inspectSiteFragments', () => { it('resolves clean, encoded, and same-page routes', () => { const root = fixture() - writeFileSync(join(root, 'guide/encoded.html'), '

Encoded

self') + writeFileSync( + join(root, 'guide/encoded.html'), + '

Encoded

Literal

encodedliteral', + ) - expect(inspectSiteFragments(root)).toEqual({ checked: 5, broken: [] }) + expect(inspectSiteFragments(root)).toEqual({ checked: 6, broken: [] }) + }) + + it('rejects ambiguous built routes', () => { + const root = fixture() + writeFileSync(join(root, 'guide.html'), '

Flat

') + writeFileSync(join(root, 'guide/index.html'), '

Index

') + + expect(() => inspectSiteFragments(root)).toThrow('share route "/guide"') + }) + + it('rejects malformed fragment hrefs', () => { + const root = fixture() + writeFileSync(join(root, 'guide/invalid.html'), 'invalid') + + expect(() => inspectSiteFragments(root)).toThrow( + 'guide/invalid.html has invalid fragment href "http://[invalid]#fragment"', + ) }) it('reports missing ids and missing built routes', () => { diff --git a/scripts/verify-doc-site-fragments.ts b/scripts/verify-doc-site-fragments.ts index d42cc752b0..c086dedccb 100644 --- a/scripts/verify-doc-site-fragments.ts +++ b/scripts/verify-doc-site-fragments.ts @@ -3,7 +3,8 @@ * VitePress use different heading-slug algorithms, so source-link validation * alone cannot prove that a published fragment exists. * - * Run with `tsx scripts/verify-doc-site-fragments.ts` after `docs:build`. + * This runs as part of `docs:build` and can also run directly after a build + * with `tsx scripts/verify-doc-site-fragments.ts`. */ import { globSync, readFileSync } from 'node:fs' @@ -61,7 +62,9 @@ function aliasesFor(page: BuiltPage): string[] { function decodedFragment(hash: string): string { try { return decodeURIComponent(hash.slice(1)) - } catch { + } catch (error) { + if (!(error instanceof URIError)) throw error + // URIError means malformed percent encoding; preserve the literal id for comparison. return hash.slice(1) } } @@ -90,7 +93,15 @@ export function inspectSiteFragments(distRoot: string): SiteFragmentReport { const byRoute = new Map() for (const page of pages) { - for (const alias of aliasesFor(page)) byRoute.set(alias, page) + for (const alias of aliasesFor(page)) { + const existing = byRoute.get(alias) + if (existing !== undefined && existing !== page) { + throw new Error( + `verify-doc-site-fragments: built pages ${existing.file} and ${page.file} share route ${JSON.stringify(alias)}.`, + ) + } + byRoute.set(alias, page) + } } const origin = 'https://dsh-docs.invalid' @@ -103,8 +114,11 @@ export function inspectSiteFragments(distRoot: string): SiteFragmentReport { let targetUrl: URL try { targetUrl = new URL(href, `${origin}${page.route}`) - } catch { - continue + } catch (error) { + throw new Error( + `verify-doc-site-fragments: ${page.file} has invalid fragment href ${JSON.stringify(href)}.`, + { cause: error }, + ) } if (targetUrl.origin !== origin || targetUrl.hash === '') continue const fragment = decodedFragment(targetUrl.hash)