gen-module-graph.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  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 plus a dependency table.
  10. *
  11. * The file is fully generated — never hand-edit it. Output is deterministic
  12. * (packages and edges sorted) so a regenerate-and-diff freshness check is
  13. * stable.
  14. *
  15. * `tsx scripts/gen-module-graph.ts` → write docs/module-graph.md
  16. * `tsx scripts/gen-module-graph.ts --check` → exit 1 if the committed file
  17. * is stale (CI / pre-push gate)
  18. */
  19. import { globSync, readFileSync, writeFileSync } from 'node:fs'
  20. import { resolve } from 'node:path'
  21. const root = resolve(import.meta.dirname, '..')
  22. const OUT = 'docs/module-graph.md'
  23. const SCOPE = '@deepseek-ai/dsh-'
  24. interface Pkg {
  25. /** Short name, `@deepseek-ai/dsh-` prefix stripped (e.g. `agent-loop`). */
  26. short: string
  27. /** Short names of this package's in-repo peer dependencies, sorted. */
  28. deps: string[]
  29. }
  30. /** Read every workspace package and its `@deepseek-ai/dsh-*` peer edges. */
  31. function collect(): Pkg[] {
  32. const pkgs: Pkg[] = []
  33. for (const rel of globSync('packages/*/package.json', { cwd: root })) {
  34. const json = JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as {
  35. name: string
  36. peerDependencies?: Record<string, string>
  37. }
  38. if (!json.name.startsWith(SCOPE)) continue
  39. const deps = Object.keys(json.peerDependencies ?? {})
  40. .filter(d => d.startsWith(SCOPE))
  41. .map(d => d.slice(SCOPE.length))
  42. .sort()
  43. pkgs.push({ short: json.name.slice(SCOPE.length), deps })
  44. }
  45. return pkgs.sort((a, b) => a.short.localeCompare(b.short))
  46. }
  47. /** Render the full docs/module-graph.md content (pure, deterministic). */
  48. function render(pkgs: Pkg[]): string {
  49. const edges: string[] = []
  50. for (const p of pkgs) {
  51. for (const d of p.deps) edges.push(` ${p.short} --> ${d}`)
  52. }
  53. const rows = pkgs.map(p => `| \`${p.short}\` | ${p.deps.length ? p.deps.map(d => `\`${d}\``).join(', ') : '—'} |`)
  54. return [
  55. '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
  56. ' Run `pnpm run gen-module-graph` to regenerate. -->',
  57. '',
  58. '# Module dependency graph',
  59. '',
  60. 'Inter-package dependencies among the `@deepseek-ai/dsh-*` harness packages, derived from each',
  61. 'package\'s `peerDependencies` (the canonical runtime-dependency signal). An edge `a --> b` means',
  62. 'package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.',
  63. '',
  64. '```mermaid',
  65. 'graph TD',
  66. ...edges,
  67. '```',
  68. '',
  69. '| Package | Depends on |',
  70. '| --- | --- |',
  71. ...rows,
  72. '',
  73. ].join('\n')
  74. }
  75. const content = render(collect())
  76. if (process.argv.includes('--check')) {
  77. let committed: string | null = null
  78. try {
  79. committed = readFileSync(resolve(root, OUT), 'utf8')
  80. } catch {
  81. // Only an ENOENT (file not yet generated) is expected here; readFileSync of
  82. // a present-but-unreadable file is not a state this repo produces. Either
  83. // way the remedy is the same — regenerate — so we treat a read failure as
  84. // "stale" and fall through to the failure branch below.
  85. committed = null
  86. }
  87. if (committed === content) {
  88. console.log(`gen-module-graph: ${OUT} is up to date.`)
  89. process.exit(0)
  90. }
  91. console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`)
  92. process.exit(1)
  93. }
  94. writeFileSync(resolve(root, OUT), content)
  95. console.log(`gen-module-graph: wrote ${OUT}.`)