/** * Generate (and verify) the module dependency graph in docs/module-graph.md. * * The architectural shape of the harness lives implicitly in each package's * `peerDependencies` — the canonical runtime-dependency signal (devDeps mirror * these as `workspace:^` plus test-only extras, which would add noise). This * script reads every `packages/* /package.json`, keeps only the * `@deepseek-ai/dsh-*` peer edges (dropping the `cordis` peer), and renders a * GitHub-viewable Mermaid graph plus a dependency table. * * The file is fully generated — never hand-edit it. Output is deterministic * (packages and edges sorted) so a regenerate-and-diff freshness check is * stable. * * `tsx scripts/gen-module-graph.ts` → write docs/module-graph.md * `tsx scripts/gen-module-graph.ts --check` → exit 1 if the committed file * is stale (CI / pre-push gate) */ import { globSync, readFileSync, writeFileSync } from 'node:fs' import { resolve } from 'node:path' const root = resolve(import.meta.dirname, '..') const OUT = 'docs/module-graph.md' const SCOPE = '@deepseek-ai/dsh-' interface Pkg { /** Short name, `@deepseek-ai/dsh-` prefix stripped (e.g. `agent-loop`). */ short: string /** Short names of this package's in-repo peer dependencies, sorted. */ deps: string[] } /** Read every workspace package and its `@deepseek-ai/dsh-*` peer edges. */ function collect(): Pkg[] { const pkgs: Pkg[] = [] for (const rel of globSync('packages/*/package.json', { cwd: root })) { const json = JSON.parse(readFileSync(resolve(root, rel), 'utf8')) as { name: string peerDependencies?: Record } if (!json.name.startsWith(SCOPE)) continue const deps = Object.keys(json.peerDependencies ?? {}) .filter(d => d.startsWith(SCOPE)) .map(d => d.slice(SCOPE.length)) .sort() pkgs.push({ short: json.name.slice(SCOPE.length), deps }) } return topoSort(pkgs) } /** * Order packages low-level → high-level: a package appears only after every * package it depends on. Kahn-style layering with an alphabetical tiebreak * within each layer, so the output stays deterministic (the freshness check * compares whole-file). The graph is a DAG, so this always terminates; a cycle * would leave nodes unplaced and throw. */ function topoSort(pkgs: Pkg[]): Pkg[] { const remaining = new Map(pkgs.map(p => [p.short, p])) const placed = new Set() const out: Pkg[] = [] while (remaining.size > 0) { const ready = [...remaining.values()] .filter(p => p.deps.every(d => placed.has(d))) .sort((a, b) => a.short.localeCompare(b.short)) if (ready.length === 0) throw new Error(`gen-module-graph: dependency cycle among ${[...remaining.keys()].join(', ')}`) for (const p of ready) { out.push(p) placed.add(p.short) remaining.delete(p.short) } } return out } /** 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(` ${p.short} --> ${d}`) } const rows = pkgs.map(p => `| \`${p.short}\` | ${p.deps.length ? p.deps.map(d => `\`${d}\``).join(', ') : '—'} |`) 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). An edge `a --> b` means', 'package `a` depends on package `b`. Names have the `@deepseek-ai/dsh-` prefix stripped.', '', '```mermaid', 'graph TD', ...edges, '```', '', '| Package | Depends on |', '| --- | --- |', ...rows, '', ].join('\n') } const content = render(collect()) if (process.argv.includes('--check')) { let committed: string | null = null try { committed = readFileSync(resolve(root, OUT), 'utf8') } catch { // Only an ENOENT (file not yet generated) is expected here; readFileSync of // a present-but-unreadable file is not a state this repo produces. Either // way the remedy is the same — regenerate — so we treat a read failure as // "stale" and fall through to the failure branch 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}.`)