mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
feat: generate module dependency graph with freshness gate
Add scripts/gen-module-graph.ts, which derives the inter-package dependency graph from each package's @deepseek-ai/dsh-* peerDependencies and renders docs/module-graph.md (a GitHub-native Mermaid graph plus a dependency table). Output is deterministic so a regenerate-and-diff check is stable. Wire a freshness gate the same way doc-sync is wired (ADR 0007: hooks and CI run the same package.json scripts): verify-module-graph runs in pre-push (lefthook) and as a CI step. It fails if the committed file drifts from what the generator would produce.
This commit is contained in:
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -51,6 +51,12 @@ jobs:
|
||||
- name: Doc-sync gates (doc code blocks + event taxonomy)
|
||||
run: pnpm run doc-sync
|
||||
|
||||
# Module-graph freshness: regenerate docs/module-graph.md from the
|
||||
# packages' peerDependencies and fail if it differs from the committed
|
||||
# file. Only reads source package.json — no build needed.
|
||||
- name: Module-graph freshness
|
||||
run: pnpm run verify-module-graph
|
||||
|
||||
- name: Tests with coverage gate (per-file 100%)
|
||||
run: pnpm run test:coverage
|
||||
|
||||
|
||||
@@ -37,7 +37,9 @@ examples/ Runnable demos (not workspaces). echo-agent = mock model + echo
|
||||
tool + stdio UI + JSONL persistence, wired via cordis.yml.
|
||||
coding-agent = the real thing: DeepSeek V4 + bash tools
|
||||
(pnpm run demo:coding, needs DEEPSEEK_API_KEY).
|
||||
docs/ architecture.md — the design doc. adr/ — decision records (the
|
||||
docs/ architecture.md — the design doc. module-graph.md — generated
|
||||
inter-package dependency graph (Mermaid; `pnpm run gen-module-graph`).
|
||||
adr/ — decision records (the
|
||||
why behind vendoring, event-sourcing, the schema DSL, …).
|
||||
rfc/ — proposals for substantial future work.
|
||||
cookbook/ — step-by-step guides: adding a package, a tool,
|
||||
|
||||
49
docs/module-graph.md
Normal file
49
docs/module-graph.md
Normal file
@@ -0,0 +1,49 @@
|
||||
<!-- 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
|
||||
agent --> llm
|
||||
agent --> session
|
||||
agent-loop --> agent
|
||||
agent-loop --> llm
|
||||
agent-loop --> session
|
||||
agent-loop --> system-prompt
|
||||
agent-loop --> tools
|
||||
bash-local --> bash
|
||||
invariants --> agent
|
||||
invariants --> llm
|
||||
invariants --> session
|
||||
llm-deepseek --> llm
|
||||
llm-pi-ai --> llm
|
||||
session --> llm
|
||||
system-prompt --> llm
|
||||
tool-bash --> agent
|
||||
tool-bash --> bash
|
||||
tool-bash --> llm
|
||||
tool-bash --> tools
|
||||
tools --> agent
|
||||
tools --> llm
|
||||
tools --> system-prompt
|
||||
```
|
||||
|
||||
| Package | Depends on |
|
||||
| --- | --- |
|
||||
| `agent` | `llm`, `session` |
|
||||
| `agent-loop` | `agent`, `llm`, `session`, `system-prompt`, `tools` |
|
||||
| `bash` | — |
|
||||
| `bash-local` | `bash` |
|
||||
| `invariants` | `agent`, `llm`, `session` |
|
||||
| `llm` | — |
|
||||
| `llm-deepseek` | `llm` |
|
||||
| `llm-pi-ai` | `llm` |
|
||||
| `session` | `llm` |
|
||||
| `system-prompt` | `llm` |
|
||||
| `tool-bash` | `agent`, `bash`, `llm`, `tools` |
|
||||
| `tools` | `agent`, `llm`, `system-prompt` |
|
||||
@@ -30,3 +30,6 @@ pre-push:
|
||||
|
||||
- name: doc-sync
|
||||
run: pnpm run doc-sync
|
||||
|
||||
- name: module-graph freshness
|
||||
run: pnpm run verify-module-graph
|
||||
|
||||
@@ -23,6 +23,8 @@
|
||||
"publint": "tsx scripts/publint-all.ts",
|
||||
"doc-typecheck": "tsx scripts/doc-typecheck.ts",
|
||||
"verify-event-taxonomy": "tsx scripts/verify-event-taxonomy.ts",
|
||||
"gen-module-graph": "tsx scripts/gen-module-graph.ts",
|
||||
"verify-module-graph": "tsx scripts/gen-module-graph.ts --check",
|
||||
"constraints": "tsx scripts/check-workspace-constraints.ts",
|
||||
"doc-sync": "pnpm run doc-typecheck && pnpm run verify-event-taxonomy",
|
||||
"hygiene": "pnpm run knip && pnpm run publint && pnpm run constraints",
|
||||
|
||||
103
scripts/gen-module-graph.ts
Normal file
103
scripts/gen-module-graph.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 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 pkgs.sort((a, b) => a.short.localeCompare(b.short))
|
||||
}
|
||||
|
||||
/** 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}.`)
|
||||
Reference in New Issue
Block a user