gen-module-graph.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. /** Generate the paired shared-instance package graph from workspace peer dependencies. */
  2. import { existsSync, readFileSync, writeFileSync } from 'node:fs'
  3. import { resolve } from 'node:path'
  4. import {
  5. collectPackageGraph,
  6. escapeMermaidLabel as escLabel,
  7. graphNodeId as nodeId,
  8. type PackageGraphNode,
  9. } from './package-graph.ts'
  10. import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
  11. import { renderTranslationPairingRecord, translationPairPaths } from './translation-pairing-record.ts'
  12. const root = resolve(import.meta.dirname, '..')
  13. const SOURCE = 'docs/module-graph.md'
  14. const PATHS = translationPairPaths(SOURCE)
  15. type Pkg = PackageGraphNode
  16. type Locale = 'en' | 'zh'
  17. const GROUP_ORDER = [
  18. 'util',
  19. 'llm',
  20. 'core',
  21. 'goal',
  22. 'bash',
  23. 'fs',
  24. 'skill',
  25. 'compact',
  26. 'subagent',
  27. 'web',
  28. 'spill',
  29. 'timeout',
  30. 'todo',
  31. 'plan',
  32. 'cordis',
  33. 'hooks',
  34. 'session-persistence',
  35. 'session-query',
  36. 'session-title',
  37. 'support',
  38. 'acp',
  39. 'ui',
  40. ]
  41. function packageLink(pkg: Pkg): string {
  42. return `[\`${pkg.short}\`](../${pkg.rel})`
  43. }
  44. /**
  45. * Render one locale of the complete deterministic package graph.
  46. * @param pkgs - Dependency-first package nodes.
  47. * @param locale - Output document language.
  48. * @returns Complete generated Markdown.
  49. */
  50. export function renderModuleGraph(pkgs: readonly Pkg[], locale: Locale): string {
  51. const edges: string[] = []
  52. for (const pkg of pkgs) {
  53. for (const dependency of pkg.deps) edges.push(` ${nodeId('pkg', pkg.short)} --> ${nodeId('pkg', dependency)}`)
  54. }
  55. const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
  56. const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((left, right) => {
  57. const leftIndex = GROUP_ORDER.indexOf(left)
  58. const rightIndex = GROUP_ORDER.indexOf(right)
  59. const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex
  60. const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex
  61. return normalizedLeft - normalizedRight || left.localeCompare(right)
  62. })
  63. const groupBlocks: string[] = []
  64. for (const group of groups) {
  65. groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
  66. for (const pkg of pkgs.filter(candidate => candidate.group === group)
  67. .sort((left, right) => left.short.localeCompare(right.short))) {
  68. groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
  69. }
  70. groupBlocks.push(' end')
  71. }
  72. const rows = pkgs.map((pkg) => {
  73. const dependencies = pkg.deps.length > 0
  74. ? pkg.deps.map((dependency) => {
  75. const target = byShort.get(dependency)
  76. return target ? packageLink(target) : `\`${dependency}\``
  77. }).join(', ')
  78. : '—'
  79. return `| ${packageLink(pkg)} | \`${pkg.group}\` | ${dependencies} |`
  80. })
  81. const chinese = locale === 'zh'
  82. return [
  83. chinese
  84. ? '<!-- 由 scripts/gen-module-graph.ts 生成——请勿手工编辑。\n 运行 `pnpm run gen-module-graph` 重新生成。 -->'
  85. : '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.\n Run `pnpm run gen-module-graph` to regenerate. -->',
  86. '',
  87. chinese ? '# 共享实例依赖关系图' : '# Shared-instance dependency graph',
  88. '',
  89. ...(chinese ? ['[English](module-graph.md) | 中文', ''] : []),
  90. chinese
  91. ? '`@deepseek-ai/dsh-*` harness 包之间的 peer 依赖关系。peer 表示消费端需要提供共享实例,不包括普通运行时 dependency 或仅开发期关系。该图按 `packages/<group>/<pkg>` 层级分组;边 `a --> b` 表示包 `a` peer 依赖包 `b`。名称中的 `@deepseek-ai/dsh-` 前缀已移除。'
  92. : 'Peer dependencies among the `@deepseek-ai/dsh-*` harness packages. A peer means the consumer requires a shared instance; ordinary runtime dependencies and development-only relationships are not shown. The graph is grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` has package `b` as a peer. Names omit the `@deepseek-ai/dsh-` prefix.',
  93. '',
  94. '```mermaid',
  95. 'flowchart TD',
  96. ...groupBlocks,
  97. ...edges,
  98. '```',
  99. '',
  100. chinese ? '| 包 | 分组 | Peer 依赖 |' : '| Package | Group | Peer dependencies |',
  101. '| --- | --- | --- |',
  102. ...rows,
  103. '',
  104. ].join('\n')
  105. }
  106. /**
  107. * Compute both localized graph documents from the current workspace manifests.
  108. * @param scanRoot - Repository root containing packages and documentation.
  109. * @returns Repository-relative output paths and exact generated content.
  110. */
  111. export function computeModuleGraphOutputs(scanRoot: string = root): ReadonlyMap<string, string> {
  112. const packages = collectPackageGraph(scanRoot, GROUP_ORDER, 'gen-module-graph')
  113. return new Map([
  114. [PATHS.source, renderModuleGraph(packages, 'en')],
  115. [PATHS.zh, renderModuleGraph(packages, 'zh')],
  116. ])
  117. }
  118. /**
  119. * Write both graph documents and their recovery record.
  120. * @param scanRoot - Repository root containing packages and documentation.
  121. * @returns Repository-relative paths whose content changed.
  122. */
  123. export function writeModuleGraph(scanRoot: string = root): string[] {
  124. const outputs = computeModuleGraphOutputs(scanRoot)
  125. const changed: string[] = []
  126. for (const [path, content] of outputs) {
  127. const destination = resolve(scanRoot, path)
  128. if (existsSync(destination) && readFileSync(destination, 'utf8') === content) continue
  129. writeFileSync(destination, content)
  130. changed.push(path)
  131. }
  132. const source = Buffer.from(outputs.get(PATHS.source) ?? '')
  133. const zh = Buffer.from(outputs.get(PATHS.zh) ?? '')
  134. const record = renderTranslationPairingRecord(PATHS, {
  135. sourceHash: storeGitBlob(scanRoot, source),
  136. zhHash: storeGitBlob(scanRoot, zh),
  137. })
  138. const recordPath = resolve(scanRoot, PATHS.meta)
  139. if (!existsSync(recordPath) || readFileSync(recordPath, 'utf8') !== record) {
  140. writeFileSync(recordPath, record)
  141. changed.push(PATHS.meta)
  142. }
  143. return changed.sort()
  144. }
  145. /** CLI entry: regenerate by default, or verify all paired outputs with `--check`. @returns Nothing. */
  146. export function main(): void {
  147. const outputs = computeModuleGraphOutputs(root)
  148. const record = renderTranslationPairingRecord(PATHS, {
  149. sourceHash: gitBlobHash(Buffer.from(outputs.get(PATHS.source) ?? '')),
  150. zhHash: gitBlobHash(Buffer.from(outputs.get(PATHS.zh) ?? '')),
  151. })
  152. const expected = new Map([...outputs, [PATHS.meta, record]])
  153. if (process.argv.includes('--check')) {
  154. const stale = [...expected].filter(([path, content]) => (
  155. !existsSync(resolve(root, path)) || readFileSync(resolve(root, path), 'utf8') !== content
  156. )).map(([path]) => path)
  157. if (stale.length === 0) {
  158. console.log(`gen-module-graph: ${expected.size} artifact(s) are up to date.`)
  159. return
  160. }
  161. console.error(`gen-module-graph: stale — ${stale.join(', ')}. Run \`pnpm run gen-module-graph\` and commit the result.`)
  162. process.exitCode = 1
  163. return
  164. }
  165. const changed = writeModuleGraph(root)
  166. console.log(`gen-module-graph: ${expected.size} artifact(s) computed, ${String(changed.length)} written.`)
  167. }
  168. if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) main()