gen-module-graph.ts 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /**
  2. * Generate (and verify) the module dependency graph in docs/module-graph.md.
  3. *
  4. * The architectural shape of the harness lives implicitly in each package's
  5. * `peerDependencies` — the canonical runtime-dependency signal (devDeps mirror
  6. * these as `workspace:^` plus test-only extras, which would add noise). This
  7. * script reads every `packages/* /* /package.json`, keeps only the
  8. * `@deepseek-ai/dsh-*` peer edges (dropping the `cordis` peer), and renders a
  9. * GitHub-viewable Mermaid graph grouped by `packages/<group>/` plus a
  10. * dependency table.
  11. *
  12. * The file is fully generated — never hand-edit it. Output is deterministic
  13. * (packages and edges sorted) so a regenerate-and-diff freshness check is
  14. * stable.
  15. *
  16. * `tsx scripts/gen-module-graph.ts` → write docs/module-graph.md
  17. * `tsx scripts/gen-module-graph.ts --check` → exit 1 if the committed file
  18. * is stale (CI / pre-push gate)
  19. */
  20. import { resolve } from 'node:path'
  21. import { readFileSync, writeFileSync } from 'node:fs'
  22. import {
  23. collectPackageGraph,
  24. escapeMermaidLabel as escLabel,
  25. graphNodeId as nodeId,
  26. type PackageGraphNode,
  27. } from './package-graph.ts'
  28. const root = resolve(import.meta.dirname, '..')
  29. const OUT = 'docs/module-graph.md'
  30. type Pkg = PackageGraphNode
  31. const GROUP_ORDER = [
  32. 'util',
  33. 'llm',
  34. 'core',
  35. 'bash',
  36. 'fs',
  37. 'skill',
  38. 'compact',
  39. 'subagent',
  40. 'web',
  41. 'timeout',
  42. 'todo',
  43. 'cordis',
  44. 'hooks',
  45. 'session-persistence',
  46. 'session-query',
  47. 'support',
  48. 'ui',
  49. ]
  50. function packageLink(pkg: Pkg): string {
  51. return `[\`${pkg.short}\`](../${pkg.rel})`
  52. }
  53. /** Render the full docs/module-graph.md content (pure, deterministic). */
  54. function render(pkgs: Pkg[]): string {
  55. const edges: string[] = []
  56. for (const p of pkgs) {
  57. for (const d of p.deps) edges.push(` ${nodeId('pkg', p.short)} --> ${nodeId('pkg', d)}`)
  58. }
  59. const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
  60. const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => {
  61. const ia = GROUP_ORDER.indexOf(a)
  62. const ib = GROUP_ORDER.indexOf(b)
  63. const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia
  64. const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib
  65. return na - nb || a.localeCompare(b)
  66. })
  67. const groupBlocks: string[] = []
  68. for (const group of groups) {
  69. groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
  70. for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) {
  71. groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
  72. }
  73. groupBlocks.push(' end')
  74. }
  75. const rows = pkgs.map((p) => {
  76. const deps = p.deps.length ? p.deps.map((d) => {
  77. const dep = byShort.get(d)
  78. return dep ? packageLink(dep) : `\`${d}\``
  79. }).join(', ') : '—'
  80. return `| ${packageLink(p)} | \`${p.group}\` | ${deps} |`
  81. })
  82. return [
  83. '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
  84. ' Run `pnpm run gen-module-graph` to regenerate. -->',
  85. '',
  86. '# Module dependency graph',
  87. '',
  88. '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.',
  89. '',
  90. '```mermaid',
  91. 'flowchart TD',
  92. ...groupBlocks,
  93. ...edges,
  94. '```',
  95. '',
  96. '| Package | Group | Depends on |',
  97. '| --- | --- | --- |',
  98. ...rows,
  99. '',
  100. ].join('\n')
  101. }
  102. const content = render(collectPackageGraph(root, GROUP_ORDER, 'gen-module-graph'))
  103. if (process.argv.includes('--check')) {
  104. let committed: string | null = null
  105. try {
  106. committed = readFileSync(resolve(root, OUT), 'utf8')
  107. } catch {
  108. // Only an ENOENT (file not yet generated) is expected here; readFileSync of
  109. // a present-but-unreadable file is not a state this repo produces. Either
  110. // way the remedy is the same — regenerate — so we treat a read failure as
  111. // "stale" and fall through to the failure branch below.
  112. committed = null
  113. }
  114. if (committed === content) {
  115. console.log(`gen-module-graph: ${OUT} is up to date.`)
  116. process.exit(0)
  117. }
  118. console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`)
  119. process.exit(1)
  120. }
  121. writeFileSync(resolve(root, OUT), content)
  122. console.log(`gen-module-graph: wrote ${OUT}.`)