gen-module-graph.ts 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /**
  2. * Generate `docs/module-graph.md` from in-repo `peerDependencies`, the canonical
  3. * runtime edges. The deterministic output groups packages by directory and
  4. * renders both Mermaid and a dependency table; `--check` verifies freshness.
  5. */
  6. import { resolve } from 'node:path'
  7. import { readFileSync, writeFileSync } from 'node:fs'
  8. import {
  9. collectPackageGraph,
  10. escapeMermaidLabel as escLabel,
  11. graphNodeId as nodeId,
  12. type PackageGraphNode,
  13. } from './package-graph.ts'
  14. const root = resolve(import.meta.dirname, '..')
  15. const OUT = 'docs/module-graph.md'
  16. type Pkg = PackageGraphNode
  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. /** Render the full docs/module-graph.md content (pure, deterministic). */
  45. function render(pkgs: Pkg[]): string {
  46. const edges: string[] = []
  47. for (const p of pkgs) {
  48. for (const d of p.deps) edges.push(` ${nodeId('pkg', p.short)} --> ${nodeId('pkg', d)}`)
  49. }
  50. const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
  51. const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
  52. const ia = GROUP_ORDER.indexOf(a)
  53. const ib = GROUP_ORDER.indexOf(b)
  54. const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
  55. const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
  56. return na - nb || a.localeCompare(b)
  57. })
  58. const groupBlocks: string[] = []
  59. for (const group of groups) {
  60. groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
  61. for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
  62. groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
  63. }
  64. groupBlocks.push(' end')
  65. }
  66. const rows = pkgs.map((p) => {
  67. const deps = p.deps.length ? p.deps.map((d) => {
  68. const dep = byShort.get(d)
  69. return dep ? packageLink(dep) : `\`${d}\``
  70. }).join(', ') : '—'
  71. return `| ${packageLink(p)} | \`${p.group}\` | ${deps} |`
  72. })
  73. return [
  74. '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
  75. ' Run `pnpm run gen-module-graph` to regenerate. -->',
  76. '',
  77. '# Module dependency graph',
  78. '',
  79. 'Inter-package dependencies among the `@deepseek-ai/dsh-*` harness packages, derived from each package\'s `peerDependencies` (the canonical runtime-dependency signal) and grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.',
  80. '',
  81. '```mermaid',
  82. 'flowchart TD',
  83. ...groupBlocks,
  84. ...edges,
  85. '```',
  86. '',
  87. '| Package | Group | Depends on |',
  88. '| --- | --- | --- |',
  89. ...rows,
  90. '',
  91. ].join('\n')
  92. }
  93. const content = render(collectPackageGraph(root, GROUP_ORDER, 'gen-module-graph'))
  94. if (process.argv.includes('--check')) {
  95. let committed: string | null = null
  96. try {
  97. committed = readFileSync(resolve(root, OUT), 'utf8')
  98. } catch {
  99. // A missing artifact is the expected read failure. Any read failure has the
  100. // same remedy here—regenerate—so it is reported as stale below.
  101. committed = null
  102. }
  103. if (committed === content) {
  104. console.log(`gen-module-graph: ${OUT} is up to date.`)
  105. process.exit(0)
  106. }
  107. console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`)
  108. process.exit(1)
  109. }
  110. writeFileSync(resolve(root, OUT), content)
  111. console.log(`gen-module-graph: wrote ${OUT}.`)