| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- /**
- * 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<string, string>
- }
- 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<string>()
- 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 [
- '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.',
- ' Run `pnpm run gen-module-graph` to regenerate. -->',
- '',
- '# 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}.`)
|