/** * Generate `docs/module-graph.md` from in-repo `peerDependencies`, the canonical * runtime edges. The deterministic output groups packages by directory and * renders both Mermaid and a dependency table; `--check` verifies freshness. */ import { resolve } from 'node:path' import { readFileSync, writeFileSync } from 'node:fs' import { collectPackageGraph, escapeMermaidLabel as escLabel, graphNodeId as nodeId, type PackageGraphNode, } from './package-graph.ts' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/module-graph.md' type Pkg = PackageGraphNode const GROUP_ORDER = [ 'util', 'llm', 'core', 'goal', 'bash', 'fs', 'skill', 'compact', 'subagent', 'web', 'spill', 'timeout', 'todo', 'plan', 'cordis', 'hooks', 'session-persistence', 'session-query', 'session-title', 'support', 'ui', ] function packageLink(pkg: Pkg): string { return `[\`${pkg.short}\`](../${pkg.rel})` } /** Render the full docs/module-graph.md content (pure, deterministic). */ function render(pkgs: Pkg[]): string { const edges: string[] = [] for (const p of pkgs) { for (const d of p.deps) edges.push(` ${nodeId('pkg', p.short)} --> ${nodeId('pkg', d)}`) } const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg])) const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((a, b) => { const ia = GROUP_ORDER.indexOf(a) const ib = GROUP_ORDER.indexOf(b) const na = ia === -1 ? Number.MAX_SAFE_INTEGER : ia const nb = ib === -1 ? Number.MAX_SAFE_INTEGER : ib return na - nb || a.localeCompare(b) }) const groupBlocks: string[] = [] for (const group of groups) { groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`) for (const pkg of pkgs.filter(p => p.group === group).sort((a, b) => a.short.localeCompare(b.short))) { groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`) } groupBlocks.push(' end') } const rows = pkgs.map((p) => { const deps = p.deps.length ? p.deps.map((d) => { const dep = byShort.get(d) return dep ? packageLink(dep) : `\`${d}\`` }).join(', ') : '—' return `| ${packageLink(p)} | \`${p.group}\` | ${deps} |` }) return [ '', '', '# Module dependency graph', '', '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//` hierarchy. An edge `a --> b` means package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.', '', '```mermaid', 'flowchart TD', ...groupBlocks, ...edges, '```', '', '| Package | Group | Depends on |', '| --- | --- | --- |', ...rows, '', ].join('\n') } const content = render(collectPackageGraph(root, GROUP_ORDER, 'gen-module-graph')) if (process.argv.includes('--check')) { let committed: string | null = null try { committed = readFileSync(resolve(root, OUT), 'utf8') } catch { // A missing artifact is the expected read failure. Any read failure has the // same remedy here—regenerate—so it is reported as stale below. committed = null } if (committed === content) { console.log(`gen-module-graph: ${OUT} is up to date.`) process.exit(0) } console.error(`gen-module-graph: ${OUT} is stale. Run \`pnpm run gen-module-graph\` and commit ${OUT}.`) process.exit(1) } writeFileSync(resolve(root, OUT), content) console.log(`gen-module-graph: wrote ${OUT}.`)