| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- /** Generate the paired shared-instance package graph from workspace peer dependencies. */
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
- import { resolve } from 'node:path'
- import {
- collectPackageGraph,
- escapeMermaidLabel as escLabel,
- graphNodeId as nodeId,
- type PackageGraphNode,
- } from './package-graph.ts'
- import { gitBlobHash, storeGitBlob } from './translation-pairing-git.ts'
- import { renderTranslationPairingRecord, translationPairPaths } from './translation-pairing-record.ts'
- const root = resolve(import.meta.dirname, '..')
- const SOURCE = 'docs/module-graph.md'
- const PATHS = translationPairPaths(SOURCE)
- type Pkg = PackageGraphNode
- type Locale = 'en' | 'zh'
- 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',
- 'acp',
- 'ui',
- ]
- function packageLink(pkg: Pkg): string {
- return `[\`${pkg.short}\`](../${pkg.rel})`
- }
- /**
- * Render one locale of the complete deterministic package graph.
- * @param pkgs - Dependency-first package nodes.
- * @param locale - Output document language.
- * @returns Complete generated Markdown.
- */
- export function renderModuleGraph(pkgs: readonly Pkg[], locale: Locale): string {
- const edges: string[] = []
- for (const pkg of pkgs) {
- for (const dependency of pkg.deps) edges.push(` ${nodeId('pkg', pkg.short)} --> ${nodeId('pkg', dependency)}`)
- }
- const byShort = new Map(pkgs.map(pkg => [pkg.short, pkg]))
- const groups = [...new Set(pkgs.map(pkg => pkg.group))].sort((left, right) => {
- const leftIndex = GROUP_ORDER.indexOf(left)
- const rightIndex = GROUP_ORDER.indexOf(right)
- const normalizedLeft = leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex
- const normalizedRight = rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex
- return normalizedLeft - normalizedRight || left.localeCompare(right)
- })
- const groupBlocks: string[] = []
- for (const group of groups) {
- groupBlocks.push(` subgraph ${nodeId('group', group)}["packages/${escLabel(group)}"]`)
- for (const pkg of pkgs.filter(candidate => candidate.group === group)
- .sort((left, right) => left.short.localeCompare(right.short))) {
- groupBlocks.push(` ${nodeId('pkg', pkg.short)}["${escLabel(pkg.short)}"]`)
- }
- groupBlocks.push(' end')
- }
- const rows = pkgs.map((pkg) => {
- const dependencies = pkg.deps.length > 0
- ? pkg.deps.map((dependency) => {
- const target = byShort.get(dependency)
- return target ? packageLink(target) : `\`${dependency}\``
- }).join(', ')
- : '—'
- return `| ${packageLink(pkg)} | \`${pkg.group}\` | ${dependencies} |`
- })
- const chinese = locale === 'zh'
- return [
- chinese
- ? '<!-- 由 scripts/gen-module-graph.ts 生成——请勿手工编辑。\n 运行 `pnpm run gen-module-graph` 重新生成。 -->'
- : '<!-- Generated by scripts/gen-module-graph.ts — do not edit by hand.\n Run `pnpm run gen-module-graph` to regenerate. -->',
- '',
- chinese ? '# 共享实例依赖关系图' : '# Shared-instance dependency graph',
- '',
- ...(chinese ? ['[English](module-graph.md) | 中文', ''] : []),
- chinese
- ? '`@deepseek-ai/dsh-*` harness 包之间的 peer 依赖关系。peer 表示消费端需要提供共享实例,不包括普通运行时 dependency 或仅开发期关系。该图按 `packages/<group>/<pkg>` 层级分组;边 `a --> b` 表示包 `a` peer 依赖包 `b`。名称中的 `@deepseek-ai/dsh-` 前缀已移除。'
- : 'Peer dependencies among the `@deepseek-ai/dsh-*` harness packages. A peer means the consumer requires a shared instance; ordinary runtime dependencies and development-only relationships are not shown. The graph is grouped by the `packages/<group>/<pkg>` hierarchy. An edge `a --> b` means package `a` has package `b` as a peer. Names omit the `@deepseek-ai/dsh-` prefix.',
- '',
- '```mermaid',
- 'flowchart TD',
- ...groupBlocks,
- ...edges,
- '```',
- '',
- chinese ? '| 包 | 分组 | Peer 依赖 |' : '| Package | Group | Peer dependencies |',
- '| --- | --- | --- |',
- ...rows,
- '',
- ].join('\n')
- }
- /**
- * Compute both localized graph documents from the current workspace manifests.
- * @param scanRoot - Repository root containing packages and documentation.
- * @returns Repository-relative output paths and exact generated content.
- */
- export function computeModuleGraphOutputs(scanRoot: string = root): ReadonlyMap<string, string> {
- const packages = collectPackageGraph(scanRoot, GROUP_ORDER, 'gen-module-graph')
- return new Map([
- [PATHS.source, renderModuleGraph(packages, 'en')],
- [PATHS.zh, renderModuleGraph(packages, 'zh')],
- ])
- }
- /**
- * Write both graph documents and their recovery record.
- * @param scanRoot - Repository root containing packages and documentation.
- * @returns Repository-relative paths whose content changed.
- */
- export function writeModuleGraph(scanRoot: string = root): string[] {
- const outputs = computeModuleGraphOutputs(scanRoot)
- const changed: string[] = []
- for (const [path, content] of outputs) {
- const destination = resolve(scanRoot, path)
- if (existsSync(destination) && readFileSync(destination, 'utf8') === content) continue
- writeFileSync(destination, content)
- changed.push(path)
- }
- const source = Buffer.from(outputs.get(PATHS.source) ?? '')
- const zh = Buffer.from(outputs.get(PATHS.zh) ?? '')
- const record = renderTranslationPairingRecord(PATHS, {
- sourceHash: storeGitBlob(scanRoot, source),
- zhHash: storeGitBlob(scanRoot, zh),
- })
- const recordPath = resolve(scanRoot, PATHS.meta)
- if (!existsSync(recordPath) || readFileSync(recordPath, 'utf8') !== record) {
- writeFileSync(recordPath, record)
- changed.push(PATHS.meta)
- }
- return changed.sort()
- }
- /** CLI entry: regenerate by default, or verify all paired outputs with `--check`. @returns Nothing. */
- export function main(): void {
- const outputs = computeModuleGraphOutputs(root)
- const record = renderTranslationPairingRecord(PATHS, {
- sourceHash: gitBlobHash(Buffer.from(outputs.get(PATHS.source) ?? '')),
- zhHash: gitBlobHash(Buffer.from(outputs.get(PATHS.zh) ?? '')),
- })
- const expected = new Map([...outputs, [PATHS.meta, record]])
- if (process.argv.includes('--check')) {
- const stale = [...expected].filter(([path, content]) => (
- !existsSync(resolve(root, path)) || readFileSync(resolve(root, path), 'utf8') !== content
- )).map(([path]) => path)
- if (stale.length === 0) {
- console.log(`gen-module-graph: ${expected.size} artifact(s) are up to date.`)
- return
- }
- console.error(`gen-module-graph: stale — ${stale.join(', ')}. Run \`pnpm run gen-module-graph\` and commit the result.`)
- process.exitCode = 1
- return
- }
- const changed = writeModuleGraph(root)
- console.log(`gen-module-graph: ${expected.size} artifact(s) computed, ${String(changed.length)} written.`)
- }
- if (process.argv[1] !== undefined && import.meta.filename === resolve(process.argv[1])) main()
|