Преглед изворни кода

Merge remote-tracking branch 'origin/split/session-persistence' into split/agent-factory

Tianyi Cui пре 2 месеци
родитељ
комит
16e7bbc7c6
7 измењених фајлова са 197 додато и 2 уклоњено
  1. 6 0
      .github/workflows/ci.yml
  2. 3 1
      AGENTS.md
  3. 4 1
      docs/development.md
  4. 53 0
      docs/module-graph.md
  5. 3 0
      lefthook.yml
  6. 2 0
      package.json
  7. 126 0
      scripts/gen-module-graph.ts

+ 6 - 0
.github/workflows/ci.yml

@@ -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
 

+ 3 - 1
AGENTS.md

@@ -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,

+ 4 - 1
docs/development.md

@@ -57,7 +57,7 @@ DEEPSEEK_BASE_URL=https://... # optional
 lefthook is configured in `lefthook.yml` as an early local checkpoint before review:
 
 - `pre-commit` runs staged-file ESLint fixes, `pnpm run typecheck`, and the vendor manifest guard.
-- `pre-push` runs `pnpm run test`, `pnpm run hygiene`, and `pnpm run doc-sync`.
+- `pre-push` runs `pnpm run test`, `pnpm run hygiene`, `pnpm run doc-sync`, and `pnpm run verify-module-graph`.
 
 The vendor manifest guard checks that changes under `vendor/*/src` are staged with the matching `vendor/README.md` manifest update. See `vendor/README.md` before editing vendored code.
 
@@ -72,6 +72,7 @@ The GitHub workflow runs these gates on each pull request:
 - `pnpm run typecheck`
 - `pnpm run lint`
 - `pnpm run doc-sync`
+- `pnpm run verify-module-graph`
 - `pnpm run test:coverage`
 - `pnpm run build`
 - `pnpm run knip && pnpm run publint`
@@ -93,6 +94,8 @@ pnpm run lint:fix       # eslint . --fix
 pnpm run doc-typecheck  # compile checked TypeScript snippets in Markdown docs
 pnpm run verify-event-taxonomy  # compare docs/architecture.md event names with source
 pnpm run doc-sync       # doc-typecheck plus event taxonomy verification
+pnpm run gen-module-graph     # regenerate docs/module-graph.md from package peerDeps
+pnpm run verify-module-graph  # fail if docs/module-graph.md is stale
 pnpm run build          # build declarations and JS bundles
 pnpm run hygiene        # knip, publint, and workspace constraints
 ```

+ 53 - 0
docs/module-graph.md

@@ -0,0 +1,53 @@
+<!-- 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
+  bash-local --> bash
+  llm-deepseek --> llm
+  llm-pi-ai --> llm
+  session --> llm
+  system-prompt --> llm
+  agent --> llm
+  agent --> session
+  session-persistence --> session
+  invariants --> agent
+  invariants --> llm
+  invariants --> session
+  session-persistence-jsonl --> session
+  session-persistence-jsonl --> session-persistence
+  tools --> agent
+  tools --> llm
+  tools --> system-prompt
+  agent-loop --> agent
+  agent-loop --> llm
+  agent-loop --> session
+  agent-loop --> session-persistence
+  agent-loop --> system-prompt
+  agent-loop --> tools
+  tool-bash --> agent
+  tool-bash --> bash
+  tool-bash --> llm
+  tool-bash --> tools
+```
+
+| Package | Depends on |
+| --- | --- |
+| `bash` | — |
+| `llm` | — |
+| `bash-local` | `bash` |
+| `llm-deepseek` | `llm` |
+| `llm-pi-ai` | `llm` |
+| `session` | `llm` |
+| `system-prompt` | `llm` |
+| `agent` | `llm`, `session` |
+| `session-persistence` | `session` |
+| `invariants` | `agent`, `llm`, `session` |
+| `session-persistence-jsonl` | `session`, `session-persistence` |
+| `tools` | `agent`, `llm`, `system-prompt` |
+| `agent-loop` | `agent`, `llm`, `session`, `session-persistence`, `system-prompt`, `tools` |
+| `tool-bash` | `agent`, `bash`, `llm`, `tools` |

+ 3 - 0
lefthook.yml

@@ -30,3 +30,6 @@ pre-push:
 
     - name: doc-sync
       run: pnpm run doc-sync
+
+    - name: module-graph freshness
+      run: pnpm run verify-module-graph

+ 2 - 0
package.json

@@ -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",

+ 126 - 0
scripts/gen-module-graph.ts

@@ -0,0 +1,126 @@
+/**
+ * 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}.`)