From f4af19d72443eeff15d97ea0431047de4bc1ed8f Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 13:45:34 +0800 Subject: [PATCH 1/3] perf(infra): shorten doc-sync critical path --- ...13-documentation-site-projection.i18n.yaml | 4 ++-- ...026-07-13-documentation-site-projection.md | 2 ++ ...-07-13-documentation-site-projection.zh.md | 2 ++ scripts/run-gates.spec.ts | 9 +++++++++ scripts/run-gates.ts | 18 +++++++++--------- website/.vitepress/config.ts | 19 +++++++++++++++++-- 6 files changed, 41 insertions(+), 13 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml index d849fd4bb1..aa4b87ee6a 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.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-07-13-documentation-site-projection.md -2026-07-13-documentation-site-projection.md: 10b8ac8761ef6cc522e752780324c11dfda23622 -2026-07-13-documentation-site-projection.zh.md: 4a03a47e6293b613822e034c710cba6ed7084008 +2026-07-13-documentation-site-projection.md: e8e3070164d9e90dea6406b545067d5fc221550a +2026-07-13-documentation-site-projection.zh.md: 50862ad79cf8b1099c6ef940a12d09098baa0524 diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md index 10b8ac8761..e8e3070164 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md @@ -26,6 +26,8 @@ The projector parses Markdown links without reserializing the document. A link t Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception. +The configured Markdown renderer keeps a build-local cache for non-Mermaid code fences, keyed by the exact content, info string, delimiter, and token attributes. The bilingual projection repeats 886 of its 1,748 code fences, so Shiki renders each distinct representation once. Mermaid fences bypass the cache because the plugin output includes a token-position id. The cache is discarded with the renderer after each build and preserves the emitted fence HTML. + Site publication remains separate from site construction. A dedicated GitHub Actions workflow runs the existing documentation gates, uploads `website/.dist` as a Pages artifact, and deploys only after the build succeeds. `actions/configure-pages` supplies the destination's base path to VitePress at build time, so the private Pages origin, a later public project path, and a custom domain do not require distinct checked-in configurations. Pages visibility remains a repository hosting setting rather than a workflow permission. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md index 4a03a47e62..50862ad79c 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md @@ -26,6 +26,8 @@ Status: implemented Mermaid 渲染权威图表。网站工作区显式声明 `vitepress-plugin-mermaid` 要求 Vite 预打包的 5 个包,因为 pnpm 的严格依赖隔离会使本地开发服务器无法使用这些传递依赖;Knip 将这种仅运行时使用记录为有意的依赖例外。 +配置后的 Markdown 渲染器会在单次构建期间缓存非 Mermaid 代码围栏,缓存键包含确切正文、info string、分隔符和 token 属性。双语投影的 1748 个代码围栏中有 886 个是重复内容,因此 Shiki 只渲染一次每种不同表示。Mermaid 围栏不会进入缓存,因为插件输出包含 token 位置 id。每次构建后,缓存会随渲染器一同丢弃,并且不会改变生成的围栏 HTML。 + 网站发布与网站构建保持分离。专用 GitHub Actions 工作流运行现有文档门禁,将 `website/.dist` 作为 Pages 产物上传,并只在构建成功后部署。`actions/configure-pages` 在构建时向 VitePress 提供目标位置的 base path,因此私有 Pages 源站、未来的公开项目路径和自定义域名不需要各自的检入配置。Pages 可见性仍是仓库托管设置,而不是工作流权限。 ## 考虑过的替代方案 diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index dce448b2e0..b83c845652 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -83,6 +83,15 @@ describe('gate graph validation', () => { expect(ids).toContain('public-repository-links') }) + it('schedules the longest documentation leaves before short checks', () => { + const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) + + expect(ids.slice(0, 10)).toEqual([ + 'doc-typecheck', 'docs-site-build', 'doc-graphs', 'markdown-links', 'type-equivalence', + 'cordis-catalog', 'mermaid', 'scoped-events', 'translation-pairing', 'markdown-wrap', + ]) + }) + it.each(['ci-primary', 'ci-static', 'check-all'] as const)( 'keeps the DSH package license policy in %s', (mode) => { diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 1f65fed97e..30d669ffcb 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -636,38 +636,38 @@ function docSyncLeafGates(options: { if (options.docTypecheckNeeds !== undefined) docTypecheckOptions.needs = options.docTypecheckNeeds if (options.docTypecheckEnv !== undefined) docTypecheckOptions.env = options.docTypecheckEnv return [ + // Stable FIFO starts the longest leaves first; only docs-site-build writes website/.generated. ...options.includeDocTypecheck === false ? [] : [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)], + pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }), + pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), + pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), + pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }), pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }), + pnpmScript('mermaid', 'verify-mermaid'), + pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), + pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }), + pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), pnpmScript('client-catalog', 'verify-client-catalog', { label: 'client catalog' }), pnpmScript('export-jsdoc', 'verify-export-jsdoc', { label: 'export jsdoc' }), pnpmScript('tool-catalog', 'verify-tool-catalog', { label: 'tool catalog' }), pnpmScript('config-catalog', 'verify-config-catalog', { label: 'config catalog' }), pnpmScript('persistence-catalog', 'verify-persistence-catalog', { label: 'persistence catalog' }), - pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }), - pnpmScript('scoped-events', 'verify-scoped-events', { label: 'scoped events' }), - pnpmScript('markdown-wrap', 'verify-md-wrap', { label: 'markdown wrap' }), - pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links' }), pnpmScript('public-repository-links', 'verify-public-repository-links', { label: 'public repository links' }), pnpmScript('doc-refs', 'verify-doc-refs', { label: 'doc refs' }), pnpmScript('package-paths', 'verify-package-paths', { label: 'package paths' }), pnpmScript('config-source-ownership', 'verify-config-source-ownership', { label: 'config source ownership' }), pnpmScript('package-readme-model-experience', 'verify-package-readme-model-experience', { label: 'package README model experience' }), - pnpmScript('mermaid', 'verify-mermaid'), pnpmScript('agent-note-classification', 'verify-agent-note-classification', { label: 'agent note classification' }), pnpmScript('agent-note-format', 'verify-agent-note-format', { label: 'agent note format' }), pnpmScript('archived-agent-notes', 'verify-archived-agent-notes', { label: 'archived agent notes' }), - pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence' }), pnpmScript('skill-invocation-metadata', 'verify-skill-invocation-metadata', { label: 'skill invocation metadata' }), pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }), - 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 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' }), pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }), ] } diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 86e22c7706..ec1b1a95c9 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -330,11 +330,26 @@ export default withMermaid({ config(md) { const renderText = md.renderer.rules.text const renderCode = md.renderer.rules.code_inline - if (renderText === undefined || renderCode === undefined) { - throw new Error('VitePress Markdown renderer is missing its text or inline-code rule.') + const renderFence = md.renderer.rules.fence + if (renderText === undefined || renderCode === undefined || renderFence === undefined) { + throw new Error('VitePress Markdown renderer is missing a required rendering rule.') } md.renderer.rules.text = (...args) => escapeVueInterpolation(renderText(...args)) md.renderer.rules.code_inline = (...args) => escapeVueInterpolation(renderCode(...args)) + const renderedFences = new Map() + md.renderer.rules.fence = (...args) => { + const [tokens, index] = args + const token = tokens[index] + if (token === undefined) throw new Error('VitePress code-fence renderer received no token.') + // Mermaid output embeds the token position, so only Shiki fences are reusable. + if (['mermaid', 'mmd'].includes(token.info.trim().split(/\s+/, 1)[0] ?? '')) return renderFence(...args) + const key = JSON.stringify([token.content, token.info, token.markup, token.attrs]) + const cached = renderedFences.get(key) + if (cached !== undefined) return cached + const html = renderFence(...args) + renderedFences.set(key, html) + return html + } }, }, mermaid: {}, From e850c076913cc9d69c9766161e6605c0d9f7cfe1 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 14:12:16 +0800 Subject: [PATCH 2/3] perf(infra): parallelize hygiene checks --- .../2026-07-06-parallel-pre-push-gates.i18n.yaml | 4 ++-- .../2026-07-06-parallel-pre-push-gates.md | 4 ++-- .../2026-07-06-parallel-pre-push-gates.zh.md | 4 ++-- package.json | 2 +- scripts/run-gates.spec.ts | 16 ++++++++++++++++ scripts/run-gates.ts | 13 +++++++++++-- 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml index 0f915e737d..03e84842c0 100644 --- a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.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-07-06-parallel-pre-push-gates.md -2026-07-06-parallel-pre-push-gates.md: 2ae08b8c87939085a0f8c7e0cb3ac69fb3ab8e91 -2026-07-06-parallel-pre-push-gates.zh.md: d31397966e7561cb7edcd9815a57b22a4a3ba8e1 +2026-07-06-parallel-pre-push-gates.md: 22d69478f0fe664b91c4ada2c5c97e7c61ee7deb +2026-07-06-parallel-pre-push-gates.zh.md: bed13531cb43545c42b7580f5144c993a6a59d90 diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md index 2ae08b8c87..22d69478f0 100644 --- a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.md @@ -20,11 +20,11 @@ The Node 24 consumer job is one ten-gate mode rather than a shell-owned process [scripts/publint-all.ts](../../../../scripts/publint-all.ts) discovers packages from `packages//` and runs `publint` with a worker pool sized from `availableParallelism()`. `DSH_PUBLINT_CONCURRENCY` can cap or raise the worker count for local machines and CI runners with different resource profiles. Results are buffered per package and printed in deterministic package order, so parallel execution does not scramble each package's log block. -The per-gate package scripts remain the vocabulary for ad hoc local runs. `hygiene` stays an aggregate `&&` chain, while `doc-sync` owns its member list in the scheduler ([doc-sync through the gate scheduler](../../archived/process/2026-07-21-doc-sync-through-gate-scheduler.md)). +The per-gate package scripts remain the vocabulary for ad hoc local runs. `hygiene` invokes a scheduler mode containing the same thirteen checks with the local four-worker cap, while `doc-sync` owns its member list in the scheduler ([doc-sync through the gate scheduler](../../archived/process/2026-07-21-doc-sync-through-gate-scheduler.md)). ## Verification -[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) rejects invalid graphs before the executor runs, pins pass-required and settle-only ordering, pins the consumer and native Windows inventories and their failure semantics, exercises signal termination through a real child process, and proves that streamed output is immediate and unbuffered. [scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) rejects a missing public export before downstream artifact consumers run. +[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) rejects invalid graphs before the executor runs, pins pass-required and settle-only ordering, pins the hygiene, consumer, and native Windows inventories and their failure semantics, exercises signal termination through a real child process, and proves that streamed output is immediate and unbuffered. [scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) rejects a missing public export before downstream artifact consumers run. ## Alternatives considered diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md index d31397966e..bed13531cb 100644 --- a/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-pre-push-gates.zh.md @@ -20,11 +20,11 @@ Node 24 消费方任务采用单个包含 10 道门禁的模式,而非由 shel [scripts/publint-all.ts](../../../../scripts/publint-all.ts) 从 `packages//` 发现包,并以根据 `availableParallelism()` 确定大小的 worker 池运行 `publint`。`DSH_PUBLINT_CONCURRENCY` 可以针对资源配置不同的本地机器和 CI runner 限制或提高 worker 数量。结果按包缓冲,并按确定性的包顺序打印,因此并行执行不会打乱各包的日志块。 -各门禁的包脚本仍是临时本地运行所用的命令入口。`hygiene` 继续作为聚合 `&&` 链,而 `doc-sync` 的成员列表由调度器管理([通过门禁调度器运行 doc-sync](../../archived/process/2026-07-21-doc-sync-through-gate-scheduler.md))。 +各门禁的包脚本仍是临时本地运行所用的命令入口。`hygiene` 调用包含相同十三道检查且限制为本地四个 worker 的调度器模式,而 `doc-sync` 的成员列表由调度器管理([通过门禁调度器运行 doc-sync](../../archived/process/2026-07-21-doc-sync-through-gate-scheduler.md))。 ## 验证 -[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) 在执行器运行前拒绝无效图,锁定必须通过与只等结算两种顺序,锁定消费方与原生 Windows 清单及其失败语义,通过真实子进程验证信号终止,并证明流式输出会立即显示且不被缓冲。[scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) 在下游产物消费方运行前拒绝缺失的公开导出。 +[scripts/run-gates.spec.ts](../../../../scripts/run-gates.spec.ts) 在执行器运行前拒绝无效图,锁定必须通过与只等结算两种顺序,锁定 hygiene、消费方与原生 Windows 清单及其失败语义,通过真实子进程验证信号终止,并证明流式输出会立即显示且不被缓冲。[scripts/publint-all.spec.ts](../../../../scripts/publint-all.spec.ts) 在下游产物消费方运行前拒绝缺失的公开导出。 ## 曾考虑的替代方案 diff --git a/package.json b/package.json index 3ec9a6c1c5..546cacbd8c 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "verify-module-graph": "tsx scripts/gen-module-graph.ts --check", "constraints": "tsx scripts/check-workspace-constraints.ts", "doc-sync": "tsx scripts/run-gates.ts doc-sync", - "hygiene": "pnpm run rescope-vendor:check && pnpm run knip && pnpm run publint && pnpm run constraints && pnpm run verify-dsh-package-licenses && pnpm run verify-package-invariants && pnpm run verify-built-package-invariants && pnpm run verify-cordis-config && pnpm run verify-node-next-types && pnpm run verify-optional-dependency-imports && pnpm run verify-runtime-closure && pnpm run verify-client-packages && pnpm run verify-vendored-links", + "hygiene": "tsx scripts/run-gates.ts hygiene", "publish:npm-baseline": "tsx scripts/publish-npm-baseline.ts", "release:dsh": "tsx scripts/release/bump.ts --family dsh", "release:vendor": "tsx scripts/release/bump.ts --family vendor", diff --git a/scripts/run-gates.spec.ts b/scripts/run-gates.spec.ts index b83c845652..2cfe266d08 100644 --- a/scripts/run-gates.spec.ts +++ b/scripts/run-gates.spec.ts @@ -69,6 +69,7 @@ describe('gate graph validation', () => { 'ci-windows-observational', 'node-compat', 'check-all', + 'hygiene', 'doc-sync', ] as const)('constructs and executes preflight for a valid non-empty %s graph', async (mode) => { const subject = withPnpmEntrypoint(() => gatesForMode(mode)) @@ -83,6 +84,21 @@ describe('gate graph validation', () => { expect(ids).toContain('public-repository-links') }) + it('keeps the hygiene aggregate aligned with the package script checks', () => { + const ids = withPnpmEntrypoint(() => gatesForMode('hygiene').map(subject => subject.id)) + + expect(ids).toEqual([ + 'rescope-vendor', 'knip', 'publint', 'constraints', 'dsh-package-licenses', + 'package-invariants', 'built-package-invariants', 'node-next-types', + 'optional-dependency-imports', 'client-packages', 'cordis-config', + 'runtime-closure', 'vendored-links', + ]) + expect(defaultConcurrency('hygiene', ids.length, 8)).toEqual({ + workers: 4, + source: '8 available CPU(s), hygiene cap 4', + }) + }) + it('schedules the longest documentation leaves before short checks', () => { const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id)) diff --git a/scripts/run-gates.ts b/scripts/run-gates.ts index 30d669ffcb..f0fe1d447a 100644 --- a/scripts/run-gates.ts +++ b/scripts/run-gates.ts @@ -32,6 +32,7 @@ export type Mode = | 'ci-windows-observational' | 'node-compat' | 'check-all' + | 'hygiene' | 'doc-sync' type GateResultStatus = 'passed' | 'failed' | 'skipped' @@ -122,11 +123,12 @@ function parseMode(raw: string | undefined): Mode { case 'ci-windows-observational': case 'node-compat': case 'check-all': + case 'hygiene': case 'doc-sync': return raw default: throw new Error( - `run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint-contracts-ready | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | doc-sync, got ${JSON.stringify(raw)}.`, + `run-gates: expected mode ci-primary | ci-linux-primary | ci-static | ci-lint-contracts-ready | ci-coverage | ci-snapshot | ci-artifacts | ci-consumers | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | check-all | hygiene | doc-sync, got ${JSON.stringify(raw)}.`, ) } } @@ -146,7 +148,7 @@ export function defaultConcurrency( if (selectedMode === 'ci-consumers') return { workers: total, source: 'ci-consumers gate count' } // Local modes cap workers: several doc gates each build a full ts.Program, // so an uncapped default on a large host trades wall clock for memory blowups. - const localCap = selectedMode === 'check-all' || selectedMode === 'doc-sync' + const localCap = selectedMode === 'check-all' || selectedMode === 'hygiene' || selectedMode === 'doc-sync' const modeLimit = localCap ? Math.min(4, available) : available return { workers: Math.min(total, modeLimit), @@ -248,6 +250,13 @@ export function gatesForMode(selected: Mode): Gate[] { }), pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }), ] + case 'hygiene': + return [ + ...hygieneLeafGates(), + pnpmScript('cordis-config', 'verify-cordis-config', { label: 'Cordis config' }), + pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }), + pnpmScript('vendored-links', 'verify-vendored-links', { label: 'vendored links' }), + ] case 'doc-sync': return docSyncLeafGates() } From f281933dde2bdee4cba649d9c0fe5ace5d0a5b33 Mon Sep 17 00:00:00 2001 From: _Kerman Date: Wed, 19 Aug 2026 14:27:56 +0800 Subject: [PATCH 3/3] fix doc-site fence cache safety --- ...2026-07-13-documentation-site-projection.i18n.yaml | 4 ++-- .../2026-07-13-documentation-site-projection.md | 2 +- .../2026-07-13-documentation-site-projection.zh.md | 2 +- website/.vitepress/config.ts | 11 +++++++---- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml index aa4b87ee6a..364bbd3bb6 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.i18n.yaml +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.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-07-13-documentation-site-projection.md -2026-07-13-documentation-site-projection.md: e8e3070164d9e90dea6406b545067d5fc221550a -2026-07-13-documentation-site-projection.zh.md: 50862ad79cf8b1099c6ef940a12d09098baa0524 +2026-07-13-documentation-site-projection.md: 46462fcf52dd3adfc10127acfd1917f0d1209d55 +2026-07-13-documentation-site-projection.zh.md: 48237c5702d71b6c4e83f8e2c6f14bd7fb879bb0 diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md index e8e3070164..46462fcf52 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.md @@ -26,7 +26,7 @@ The projector parses Markdown links without reserializing the document. A link t Mermaid renders the canonical diagrams. The website workspace explicitly declares the five packages that `vitepress-plugin-mermaid` asks Vite to prebundle because pnpm's strict dependency isolation otherwise makes those transitive packages unavailable to the local development server; Knip records this runtime-only use as an intentional dependency exception. -The configured Markdown renderer keeps a build-local cache for non-Mermaid code fences, keyed by the exact content, info string, delimiter, and token attributes. The bilingual projection repeats 886 of its 1,748 code fences, so Shiki renders each distinct representation once. Mermaid fences bypass the cache because the plugin output includes a token-position id. The cache is discarded with the renderer after each build and preserves the emitted fence HTML. +The configured Markdown renderer keeps a build-local cache for non-Mermaid, non-snippet code fences, keyed by the exact content, info string, delimiter, and token attributes. About half of the bilingual projection's code fences repeat, so Shiki renders each distinct representation once. Mermaid fences bypass the cache because the plugin output includes a token-position id, and VitePress snippets resolve their source files during rendering. The cache is enabled only for production builds, is discarded with the renderer after each build, and does not change the emitted fence HTML. Site publication remains separate from site construction. A dedicated GitHub Actions workflow runs the existing documentation gates, uploads `website/.dist` as a Pages artifact, and deploys only after the build succeeds. `actions/configure-pages` supplies the destination's base path to VitePress at build time, so the private Pages origin, a later public project path, and a custom domain do not require distinct checked-in configurations. Pages visibility remains a repository hosting setting rather than a workflow permission. diff --git a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md index 50862ad79c..48237c5702 100644 --- a/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md +++ b/.agents/notes/implemented/process/2026-07-13-documentation-site-projection.zh.md @@ -26,7 +26,7 @@ Status: implemented Mermaid 渲染权威图表。网站工作区显式声明 `vitepress-plugin-mermaid` 要求 Vite 预打包的 5 个包,因为 pnpm 的严格依赖隔离会使本地开发服务器无法使用这些传递依赖;Knip 将这种仅运行时使用记录为有意的依赖例外。 -配置后的 Markdown 渲染器会在单次构建期间缓存非 Mermaid 代码围栏,缓存键包含确切正文、info string、分隔符和 token 属性。双语投影的 1748 个代码围栏中有 886 个是重复内容,因此 Shiki 只渲染一次每种不同表示。Mermaid 围栏不会进入缓存,因为插件输出包含 token 位置 id。每次构建后,缓存会随渲染器一同丢弃,并且不会改变生成的围栏 HTML。 +配置后的 Markdown 渲染器会在单次构建期间缓存非 Mermaid、非 snippet 代码围栏,缓存键包含确切正文、info string、分隔符和 token 属性。双语投影中约半数代码围栏是重复内容,因此 Shiki 只渲染一次每种不同表示。Mermaid 围栏不会进入缓存,因为插件输出包含 token 位置 id;VitePress snippet 则在渲染期间解析源文件。缓存仅在生产构建中启用,每次构建后会随渲染器一同丢弃,并且不会改变生成的围栏 HTML。 网站发布与网站构建保持分离。专用 GitHub Actions 工作流运行现有文档门禁,将 `website/.dist` 作为 Pages 产物上传,并只在构建成功后部署。`actions/configure-pages` 在构建时向 VitePress 提供目标位置的 base path,因此私有 Pages 源站、未来的公开项目路径和自定义域名不需要各自的检入配置。Pages 可见性仍是仓库托管设置,而不是工作流权限。 diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index ec1b1a95c9..6798d1452a 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -331,9 +331,9 @@ export default withMermaid({ const renderText = md.renderer.rules.text const renderCode = md.renderer.rules.code_inline const renderFence = md.renderer.rules.fence - if (renderText === undefined || renderCode === undefined || renderFence === undefined) { - throw new Error('VitePress Markdown renderer is missing a required rendering rule.') - } + if (renderText === undefined) throw new Error('VitePress Markdown renderer is missing the text rendering rule.') + if (renderCode === undefined) throw new Error('VitePress Markdown renderer is missing the inline-code rendering rule.') + if (renderFence === undefined) throw new Error('VitePress Markdown renderer is missing the fence rendering rule.') md.renderer.rules.text = (...args) => escapeVueInterpolation(renderText(...args)) md.renderer.rules.code_inline = (...args) => escapeVueInterpolation(renderCode(...args)) const renderedFences = new Map() @@ -341,8 +341,11 @@ export default withMermaid({ const [tokens, index] = args const token = tokens[index] if (token === undefined) throw new Error('VitePress code-fence renderer received no token.') - // Mermaid output embeds the token position, so only Shiki fences are reusable. + // Mermaid output embeds the token position, and VitePress snippets resolve source files during rendering. if (['mermaid', 'mmd'].includes(token.info.trim().split(/\s+/, 1)[0] ?? '')) return renderFence(...args) + if (Reflect.get(token, 'src') !== undefined) return renderFence(...args) + // Keep the cache build-local; a dev renderer can survive many HMR updates. + if (process.env.NODE_ENV !== 'production') return renderFence(...args) const key = JSON.stringify([token.content, token.info, token.markup, token.attrs]) const cached = renderedFences.get(key) if (cached !== undefined) return cached