artfusion/ccgrapher
GitHub: artfusion/ccgrapher
一个 agent 工作流 linter,通过分析步骤间的真实数据依赖,找出不必要的串行等待并生成可并发的编排代码和可视化图表。
Stars: 0 | Forks: 0
# ccgrapher
[](https://github.com/artfusion/ccgrapher/actions/workflows/ci.yml)
[](LICENSE)
**你的 agent 总是在按部就班地做着那些本不需要排队等待的任务。** ccgrapher 能够找出那些
从不依赖任何前置任务的步骤,并让它们并发执行。
## 从这里开始:Claude Code 技能
如果你使用 Claude Code,只需一条命令即可体验完整功能:
```
git clone https://github.com/artfusion/ccgrapher.git
ln -s "$PWD/ccgrapher/skills/parallel-plan" ~/.claude/skills/parallel-plan
```
现在,在你的 agent 执行包含五个或更多步骤的计划之前,它会检查其中哪些步骤
确实存在相互依赖关系——并让那些相互独立的步骤并发执行,而不是按部就班地顺着
列表往下走。
```
wave 1 scope
wave 2 ×6 together fix_auth, rebuild_landing, new_site, pricing, byok, compare
wave 3 ×3 together add_tests, reframe_copy, cross_links
wave 4 review
wave 5 release
```
这是一个真实的记录:九个 pull request 一个接一个地被提交。其中六个
从未等待过任何东西。同样的工作量,只用了五波而非十二波。
它还能捕获那些与速度无关的失败——比如一个给自己工作打分的步骤,或者
一个在某个分支静默失败时仍报告成功的 fan-in。
**它为什么有效:**你必须声明每个步骤*读取*了什么,而不是在它之前发生了什么。如实
写下这些,就能暴露出那些从未在等待的步骤。下面的所有内容都是用来
检查这一声明的机制。
## 核心理念
只有当**真实数据沿着边传递**时,两个 agent 节点之间的边才真正存在。大多数工作流出于习惯
被写成了直链,因为这就是你脑海中步骤出现的顺序——所以大多数
边都是无效的。删掉它们,图就会变宽而不是变高:同样的工作现在所需的完成时间
取决于最慢的那一层,而不是所有步骤耗时的总和。
下面是同一个六节点工作流的前后对比。没有任何重写;只是将两条不携带
数据的边重新指向了实际提供审阅者所需内容的节点。
| 写成直链——6 层 | 它的实际结构——4 层 |
| --- | --- |
|
|
|
这三个审阅步骤互不需要。linter 会机械地找出这一点,而图片正是
你看到这一过程的直观体现。
## 快速开始
```
npx @ccgrapher/cli lint your-workflow.yaml
```
无需安装。或者从源码构建:
```
git clone https://github.com/artfusion/ccgrapher.git
cd ccgrapher && pnpm install && pnpm build
node apps/cli/dist/index.js lint examples/linear-chain.yaml
```
```
linear-chain — review this repo for bugs and doc rot
error FAKE_EDGE review_a -> review_b carries nothing — review_b does not wait on review_a
error FAKE_EDGE review_b -> lint_docs carries nothing — lint_docs does not wait on review_b
error MISSING_INPUT review_b requires 'repo' but no inbound edge carries it
error MISSING_INPUT lint_docs requires 'repo' but no inbound edge carries it
warn HIDDEN_EDGE review_a and review_b run concurrently and both write 'notes/findings.md'
— set worktree: true or serialise them (after repair)
Proposed repairs
repoint review_a -> review_b becomes setup -> review_b, carrying 'repo'
setup is the nearest node that supplies 'repo'
repoint review_b -> lint_docs becomes setup -> lint_docs, carrying 'repo'
setup is the nearest node that supplies 'repo'
Critical path: 6 layers -> 4 layers (2 fewer after repair)
5 findings (4 errors, 1 warning)
```
退出代码 `0` 表示一切正常,`1` 表示有错误,`2` 表示用法错误。
## 编写 spec
spec 就是一个 YAML 文件。包含两个必需的列表——`nodes` 和 `edges`——外加一个名称和一个可选的目标。
```
version: 1
name: market-scan
goal: "how do we compare to the top 3 competitors" # a caption, never a node
nodes:
- id: split
label: split the job
kind: split
in: { question: string }
out: { angle: string }
- id: worker_1
label: worker 1
kind: worker
model: cheap
in: { angle: string }
out: { claim: string, source: url }
edges:
- { from: split, to: worker_1, carries: [angle] }
```
### 节点
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `id` | string,**必需** | 唯一标识。用于 edges 和生成的代码。 |
| `label` | string,**必需** | 在方框中绘制的内容。一到四个词。 |
| `kind` | enum,**必需** | 见下文。决定形状和图标。 |
| `in` | map | 字段名 → 类型描述符。该节点消费的内容。 |
| `out` | map | 字段名 → 类型描述符。该节点生成的内容。 |
| `model` | `cheap` \| `strong` \| `null` | `null` 表示纯代码——无模型,不消耗 token。 |
| `writes` | string[] | 它接触的文件或 API。两个并发写入者就是一条隐藏边。 |
| `freshContext` | boolean | 在 verifier 上设置。worker 绝不能给自己的工作打分。 |
| `expects` | number | fan-in 守卫。预期到达多少个结果。 |
| `fanOut` | `{ over, cap? }` | 对每个项运行一次。保持为一个节点,绘制为带有 `×N` 徽章的堆栈。 |
| `worktree` | boolean | 每次运行提供独立空间,以防并行 worker 在磁盘上发生冲突。 |
**Kinds(类型)。** `split` 分发任务 · `worker` 执行一个单元 · `verifier` 检查他人的工作 ·
`reduce` 进行合并(通常是纯代码) · `synthesize` 编写最终答案 · `gate` 需要人工审批 ·
`goal` 可用于显式指定目标节点,尽管顶层的 `goal:` 字符串只是一个标题,永远不会被转换为节点。
**类型描述符**是自由文本——比如 `string`、`url`、`path`、`markdown`、`boolean`、`string[]`、
`YYYY-MM-DD`、`keep|drop`。linter 只会比较字段*名称*,但 codegen 会将描述符转换为
真正的 JSON Schema 和 TypeScript 类型,所以描述具体一些会有好处。
### 边
| 字段 | 含义 |
| --- | --- |
| `from`, `to` | Node id。 |
| `carries` | 沿着该边传递的字段名。**这是整个机制的核心。** |
当 `carries` 指定的字段同时存在于源的 `out` **以及**目标的
`in` 中时,这条边就是真实的。如果两者的交集没有任何存活内容,这条边就是一次
毫无意义的等待——这正是 `FAKE_EDGE` 所报告的问题。
### 层
你永远不需要声明 layers(层)。节点总是位于其最深依赖项的下一行,因此任何两个
没有依赖关系的节点都会自动排列在同一行。菱形结构正是由此自然产生的:
## 六条 lint 规则
| 规则 | 严重级别 | 触发条件 |
| --- | --- | --- |
| `FAKE_EDGE` | error | 没有任何携带的字段落入目标声明的 `in` 中。 |
| `MISSING_INPUT` | error | 非根节点声明了一个没有任何来源提供的输入。 |
| `HIDDEN_EDGE` | warn | 两个并发节点共享一个 `writes` 条目,且均未被隔离。 |
| `SELF_GRADING` | warn | `verifier` 未标记为 `freshContext`。 |
| `CONTEXT_COLLAPSE` | warn | 超过 30 个结果到达,却没有中间的 `reduce`。 |
| `SILENT_FAILURE` | warn | 真正的 fan-in 缺少 `expects` 守卫,或者守卫不正确。 |
关于这一点,有两件事不那么显而易见:
**Lint 会运行两次。** 有些问题在写好的图中是不可见的。在 `linear-chain` 中,两个
审阅者都写入了 `notes/findings.md`,但由于其中一个比另一个慢一个层级,所以它们看起来
从不并发。只有当虚假边被修复后,它们才会落到同一行,冲突才会
变为现实。因此,检查结果会带有 `phase: "raw" | "repaired"` 标签。
**修复是重新指向,而不是删除。** 删除一条虚假边会使其目标成为一个根节点,从而在
其真正的依赖项生成任何内容之前就开始运行。因此,修复建议始终是“重新指向提供
缺失字段的最近祖先”,只有当上游没有任何节点能提供该字段时,该边才会被
删除。
## 渲染
```
ccg render examples/diamond.yaml -o diagram.svg # hand-drawn SVG
ccg render examples/diamond.yaml -o diagram.mmd # Mermaid
ccg render examples/diamond.yaml -o diagram.excalidraw # Excalidraw scene
ccg render examples/linear-chain.yaml --fix -o after.svg # draw the repaired graph
```
格式取决于扩展名,或者可以通过传递 `-f svg|mermaid|excalidraw` 指定。
- **SVG** —— rough.js 描边,纸张纹理,内嵌手写字体,因此文件在任何地方都能
完全一致地渲染。虚假的边显示为红色虚线,并带有“carries no data”的标签。
`--no-grain` 去除纸张纹理(光栅化后体积会小得多);`--no-embed-font` 通过名称引用
字体,而不是将其内嵌。
- **Mermaid** —— 带有 `look: handDrawn` 的 `flowchart TD`。可以在 GitHub 和 Notion 上渲染。每种类型
都有易于区分的形状,并且边都带有它们所携带内容的标签。
- **Excalidraw** —— 一个你可以打开并手动微调的场景。箭头绑定在它们的方框上,
标签位于其容器内部,因此拖动节点时所有内容都会跟着移动。
每个渲染器都是确定性的:输入相同的 spec,就会输出字节完全相同的文件。
## 生成编排代码
```
ccg codegen examples/diamond.yaml -t claude-code # agent() / parallel()
ccg codegen examples/diamond.yaml -t plain-ts # typed functions + Promise.all
ccg codegen examples/diamond.yaml -t langgraph # StateGraph wiring
```
阶段直接来源于图的层级排名,因此生成脚本中的并发性与
图中显示的并发性完全一致——这两者不会产生偏差。
```
phase("Workers")
const [worker_1, worker_2, worker_3, worker_4, worker_5] = await parallel([
() => agent(`worker 1. Return claim (string), source (url), date (YYYY-MM-DD).
…`, { label: "worker_1", schema: WORKER_1_SCHEMA, model: "haiku" }),
…
])
phase("Checker")
const checkerInputs = [worker_1, worker_2, worker_3, worker_4, worker_5].filter(Boolean)
if (checkerInputs.length !== 5) {
log(`checker: expected 5 results, got ${checkerInputs.length}`)
}
// checker runs in a fresh context — it must not share one with the work it grades
const checker = await agent(…)
```
`fanOut` 会变成一个有上限的 `parallel` map,`worktree` 会变成 `isolation: "worktree"`,而 `expects`
会变成针对*已到达*结果的守卫——因为否则的话,死掉的上游节点就会蒙混过关,
导致 synthesis 步骤在部分数据上生成报告,却表现得好像数据完整一样。
传递 `--fix` 以基于修复后的图生成代码。如果在一个已知存在虚假边的 spec 上生成代码,会将
这些浪费的等待固化到你的 runtime 中,因此 CLI 会在你这么做时发出警告。
## 读取现有代码
另一个方向——我的工作流*实际*在做什么,而不是我本意想做什么?
```
ccg ingest orchestration.ts # reconstruct the spec
ccg ingest orchestration.ts --lint # audit the code directly
```
`ingest` 使用 ts-morph 从真实的 TypeScript 代码中还原节点、依赖项和并发性。这是
虚假边审计发现那些从未出现在任何人图中的浪费的地方。
双向转换是无损的:`spec → codegen → ingest → spec` 对于每一个测试用例
都能返回深度相等的 spec,这已由测试套件断言。TypeScript 会平铺为 `string` 的描述符(如 `url`、
`path`、`YYYY-MM-DD`)会保留在尾部的注释中,而类型系统无法表达的所有内容
—— kind、model 层级、fresh context、worktree、守卫 —— 都存在于每个函数上方的文档注释中。
两者都可以作为普通的文档来阅读。
## Web 画布
```
pnpm --filter @ccgrapher/web dev
```
编辑 YAML,图会在每次击键时重绘并重新进行 lint。**预览修复后的结构**可在不改动源码的情况下显示
折叠后的图;**应用修复**则会重写源码。它导入了与 CLI 相同的
`core`、`lint` 和 `layout` 包,因此这两者绝不会产生分歧。
## 关于该 skill 的更多信息
`skills/parallel-plan/` 会在计划包含约五个或更多步骤时触发,或者每当工作被
分发给子 agent 时触发。低于此数量时,这种繁琐的流程成本反而高于它节省的成本,而且一个在任何事情上都会触发的技能
最终会被忽视。`template.yaml` 是它复制的基础模板。
在阅读其输出时,有两件事值得注意:
- **波次(Waves)是上限,而不是指令。** 一波中包含六个步骤意味着*可能*会同时运行六个——但
速率限制、成本或共享文件等情况可能会有不同的要求。
- **层数的准确性仅取决于 `in:` 字段的诚实度。** 声明了一个不真实的依赖关系,
你就会得到一条直链。spec 就是论据;而工具的作用是检查它的一致性。
## 架构
| 包 | 功能 |
| --- | --- |
| [`core`](packages/core) | Zod schema、YAML 解析、循环检测、最长路径层级排名 |
| [`lint`](packages/lint) | 六条规则和两步修复流水线 |
| [`layout`](packages/layout) | dagre 包装器 → 节点定位与边路径规划 |
| [`render-svg`](packages/render-svg) | rough.js + 纸张纹理 + 内嵌字体 |
| [`render-mermaid`](packages/render-mermaid) | 带有 `look: handDrawn` 的 `flowchart TD` |
| [`render-excalidraw`](packages/render-excalidraw) | 带有绑定箭头的 Scene JSON |
| [`codegen`](packages/codegen) | Spec → Claude Code / 原生 TS / LangGraph |
| [`ingest`](packages/ingest) | ts-morph:编排代码 → spec |
| [`apps/cli`](apps/cli) | `ccg lint · render · codegen · ingest` |
| [`apps/web`](apps/web) | Next.js + React Flow 画布 |
**唯一的恒定法则。** `core` 计算*层级排名*;`layout` 计算*像素位置*。lint 报告中“6 层 → 4 层”的数值
来源于 `core`,绝不会来自 layout 库,因此图表和报告
不会产生悄然的分歧。dagre 以 `ranker: "longest-path"` 运行以保持匹配,并且有测试断言
两者在每个 fixture 上都保持一致。
`core` 的主入口是 bundler-safe 的;文件系统辅助程序位于 `@ccgrapher/core/node`。正是这一点
让浏览器画布能够运行完全相同的 linter。
## 开发说明
```
pnpm build # tsc -b across the workspace
pnpm test # 246 tests
pnpm test:watch
```
需 Node 22+。CI 会在 Node 22 和 24 上测试套件,并端到端地重新检查验收标准。
生成的代码是通过解析来验证的,而不是靠肉眼看:TypeScript 目标代码会在 `strict` 模式下通过真正的
`ts.createProgram` 诊断进行检验,而 Claude Code 目标代码会被解析为一个异步
函数体。Mermaid 输出则通过实际渲染来验证——在生成一些 `.mmd` 文件后
打开 `tools/mermaid-check.html`。`tools/rasterize.mjs` 会重新生成 README 中的图片。
请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) —— 添加一条 lint 规则只需四个步骤,这是你
能做出的最有用的贡献。
### 示例
`diamond`、`research-desk` 和 `route-auth-audit` 是干净的——每条边都携带真实数据,因此它们
在测试套件中也兼作阴性对照组。`linear-chain` 是故意被破坏的,作为
linter 的测试用例。添加 `self-grading` 和 `wide-fanin` 是因为最初版本中没有任何内容
测试了 `SELF_GRADING` 或 `CONTEXT_COLLAPSE`。
如果你想了解这为什么值得做,`release-session` 是必读的用例。它根本不是一个 agent
工作流——它代表了人类一周的工作:九个 pull request 一个接一个地提交,然后进行一次
发布。九个任务中有六个
从未在等待任何东西,而 CI 节点声明它期望得到九个
结果,但实际上只有八条边能到达它。一个看似完整的发布,却建立在一个从未
看到完整全貌的检查之上。
```
ccg lint examples/release-session.yaml
```
| 实际运行情况——12 层 | 它的真实面貌——5 层 |
| --- | --- |
|
|
|
## 出处
该设计来源于一份交接文档,该文档是在阅读 Anatoli Kopadze 的文章
[“图工程解析”](https://x.com/anatolikopadze/status/2080668775796314331)时编写的,其中涉及了
虚假边测试、菱形模式以及各种失败模式的来源。该文档被原封不动地保留为
[HANDOFF.md](HANDOFF.md);相关的聊天记录未公开。
根据其自身的 fixture 对该交接文档进行审查时,发现了九个缺陷——其中包括
按原定规则 `HIDDEN_EDGE` 永远不会触发,每个 fixture 的入口节点都会报告
虚假的 `MISSING_INPUT`,以及某个 fixture 的层数计算完全
错误。这些解决方案正是 linter 表现出当前这种行为的原因,并且它们被记录在 git
历史中:第一次提交完全是原始交付的内容,第二次提交则包含了在其基础上构建的所有内容。
文章中的图表仅通过 URL 引用,未被本地化打包。匹配手绘和橙色的
美学风格是可以的;但照搬别人的插图是不行的。
## 许可证
[Apache-2.0](LICENSE)。请参阅 [NOTICE](NOTICE)。
如果你打算重新分发输出内容,有一件事需要知道:`render-svg` 会将其生成的 SVG 中的
[Caveat](https://github.com/googlefonts/caveat) 字体(SIL OFL 1.1)内嵌进去,因此
每个生成的 SVG 都会在文件中自带该字体的署名。如果你想要一个
不包含字体数据的文件,请使用 `--no-embed-font` 渲染。完整细节请参阅 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。
|
|
这三个审阅步骤互不需要。linter 会机械地找出这一点,而图片正是
你看到这一过程的直观体现。
## 快速开始
```
npx @ccgrapher/cli lint your-workflow.yaml
```
无需安装。或者从源码构建:
```
git clone https://github.com/artfusion/ccgrapher.git
cd ccgrapher && pnpm install && pnpm build
node apps/cli/dist/index.js lint examples/linear-chain.yaml
```
```
linear-chain — review this repo for bugs and doc rot
error FAKE_EDGE review_a -> review_b carries nothing — review_b does not wait on review_a
error FAKE_EDGE review_b -> lint_docs carries nothing — lint_docs does not wait on review_b
error MISSING_INPUT review_b requires 'repo' but no inbound edge carries it
error MISSING_INPUT lint_docs requires 'repo' but no inbound edge carries it
warn HIDDEN_EDGE review_a and review_b run concurrently and both write 'notes/findings.md'
— set worktree: true or serialise them (after repair)
Proposed repairs
repoint review_a -> review_b becomes setup -> review_b, carrying 'repo'
setup is the nearest node that supplies 'repo'
repoint review_b -> lint_docs becomes setup -> lint_docs, carrying 'repo'
setup is the nearest node that supplies 'repo'
Critical path: 6 layers -> 4 layers (2 fewer after repair)
5 findings (4 errors, 1 warning)
```
退出代码 `0` 表示一切正常,`1` 表示有错误,`2` 表示用法错误。
## 编写 spec
spec 就是一个 YAML 文件。包含两个必需的列表——`nodes` 和 `edges`——外加一个名称和一个可选的目标。
```
version: 1
name: market-scan
goal: "how do we compare to the top 3 competitors" # a caption, never a node
nodes:
- id: split
label: split the job
kind: split
in: { question: string }
out: { angle: string }
- id: worker_1
label: worker 1
kind: worker
model: cheap
in: { angle: string }
out: { claim: string, source: url }
edges:
- { from: split, to: worker_1, carries: [angle] }
```
### 节点
| 字段 | 类型 | 含义 |
| --- | --- | --- |
| `id` | string,**必需** | 唯一标识。用于 edges 和生成的代码。 |
| `label` | string,**必需** | 在方框中绘制的内容。一到四个词。 |
| `kind` | enum,**必需** | 见下文。决定形状和图标。 |
| `in` | map | 字段名 → 类型描述符。该节点消费的内容。 |
| `out` | map | 字段名 → 类型描述符。该节点生成的内容。 |
| `model` | `cheap` \| `strong` \| `null` | `null` 表示纯代码——无模型,不消耗 token。 |
| `writes` | string[] | 它接触的文件或 API。两个并发写入者就是一条隐藏边。 |
| `freshContext` | boolean | 在 verifier 上设置。worker 绝不能给自己的工作打分。 |
| `expects` | number | fan-in 守卫。预期到达多少个结果。 |
| `fanOut` | `{ over, cap? }` | 对每个项运行一次。保持为一个节点,绘制为带有 `×N` 徽章的堆栈。 |
| `worktree` | boolean | 每次运行提供独立空间,以防并行 worker 在磁盘上发生冲突。 |
**Kinds(类型)。** `split` 分发任务 · `worker` 执行一个单元 · `verifier` 检查他人的工作 ·
`reduce` 进行合并(通常是纯代码) · `synthesize` 编写最终答案 · `gate` 需要人工审批 ·
`goal` 可用于显式指定目标节点,尽管顶层的 `goal:` 字符串只是一个标题,永远不会被转换为节点。
**类型描述符**是自由文本——比如 `string`、`url`、`path`、`markdown`、`boolean`、`string[]`、
`YYYY-MM-DD`、`keep|drop`。linter 只会比较字段*名称*,但 codegen 会将描述符转换为
真正的 JSON Schema 和 TypeScript 类型,所以描述具体一些会有好处。
### 边
| 字段 | 含义 |
| --- | --- |
| `from`, `to` | Node id。 |
| `carries` | 沿着该边传递的字段名。**这是整个机制的核心。** |
当 `carries` 指定的字段同时存在于源的 `out` **以及**目标的
`in` 中时,这条边就是真实的。如果两者的交集没有任何存活内容,这条边就是一次
毫无意义的等待——这正是 `FAKE_EDGE` 所报告的问题。
### 层
你永远不需要声明 layers(层)。节点总是位于其最深依赖项的下一行,因此任何两个
没有依赖关系的节点都会自动排列在同一行。菱形结构正是由此自然产生的:
## 六条 lint 规则
| 规则 | 严重级别 | 触发条件 |
| --- | --- | --- |
| `FAKE_EDGE` | error | 没有任何携带的字段落入目标声明的 `in` 中。 |
| `MISSING_INPUT` | error | 非根节点声明了一个没有任何来源提供的输入。 |
| `HIDDEN_EDGE` | warn | 两个并发节点共享一个 `writes` 条目,且均未被隔离。 |
| `SELF_GRADING` | warn | `verifier` 未标记为 `freshContext`。 |
| `CONTEXT_COLLAPSE` | warn | 超过 30 个结果到达,却没有中间的 `reduce`。 |
| `SILENT_FAILURE` | warn | 真正的 fan-in 缺少 `expects` 守卫,或者守卫不正确。 |
关于这一点,有两件事不那么显而易见:
**Lint 会运行两次。** 有些问题在写好的图中是不可见的。在 `linear-chain` 中,两个
审阅者都写入了 `notes/findings.md`,但由于其中一个比另一个慢一个层级,所以它们看起来
从不并发。只有当虚假边被修复后,它们才会落到同一行,冲突才会
变为现实。因此,检查结果会带有 `phase: "raw" | "repaired"` 标签。
**修复是重新指向,而不是删除。** 删除一条虚假边会使其目标成为一个根节点,从而在
其真正的依赖项生成任何内容之前就开始运行。因此,修复建议始终是“重新指向提供
缺失字段的最近祖先”,只有当上游没有任何节点能提供该字段时,该边才会被
删除。
## 渲染
```
ccg render examples/diamond.yaml -o diagram.svg # hand-drawn SVG
ccg render examples/diamond.yaml -o diagram.mmd # Mermaid
ccg render examples/diamond.yaml -o diagram.excalidraw # Excalidraw scene
ccg render examples/linear-chain.yaml --fix -o after.svg # draw the repaired graph
```
格式取决于扩展名,或者可以通过传递 `-f svg|mermaid|excalidraw` 指定。
- **SVG** —— rough.js 描边,纸张纹理,内嵌手写字体,因此文件在任何地方都能
完全一致地渲染。虚假的边显示为红色虚线,并带有“carries no data”的标签。
`--no-grain` 去除纸张纹理(光栅化后体积会小得多);`--no-embed-font` 通过名称引用
字体,而不是将其内嵌。
- **Mermaid** —— 带有 `look: handDrawn` 的 `flowchart TD`。可以在 GitHub 和 Notion 上渲染。每种类型
都有易于区分的形状,并且边都带有它们所携带内容的标签。
- **Excalidraw** —— 一个你可以打开并手动微调的场景。箭头绑定在它们的方框上,
标签位于其容器内部,因此拖动节点时所有内容都会跟着移动。
每个渲染器都是确定性的:输入相同的 spec,就会输出字节完全相同的文件。
## 生成编排代码
```
ccg codegen examples/diamond.yaml -t claude-code # agent() / parallel()
ccg codegen examples/diamond.yaml -t plain-ts # typed functions + Promise.all
ccg codegen examples/diamond.yaml -t langgraph # StateGraph wiring
```
阶段直接来源于图的层级排名,因此生成脚本中的并发性与
图中显示的并发性完全一致——这两者不会产生偏差。
```
phase("Workers")
const [worker_1, worker_2, worker_3, worker_4, worker_5] = await parallel([
() => agent(`worker 1. Return claim (string), source (url), date (YYYY-MM-DD).
…`, { label: "worker_1", schema: WORKER_1_SCHEMA, model: "haiku" }),
…
])
phase("Checker")
const checkerInputs = [worker_1, worker_2, worker_3, worker_4, worker_5].filter(Boolean)
if (checkerInputs.length !== 5) {
log(`checker: expected 5 results, got ${checkerInputs.length}`)
}
// checker runs in a fresh context — it must not share one with the work it grades
const checker = await agent(…)
```
`fanOut` 会变成一个有上限的 `parallel` map,`worktree` 会变成 `isolation: "worktree"`,而 `expects`
会变成针对*已到达*结果的守卫——因为否则的话,死掉的上游节点就会蒙混过关,
导致 synthesis 步骤在部分数据上生成报告,却表现得好像数据完整一样。
传递 `--fix` 以基于修复后的图生成代码。如果在一个已知存在虚假边的 spec 上生成代码,会将
这些浪费的等待固化到你的 runtime 中,因此 CLI 会在你这么做时发出警告。
## 读取现有代码
另一个方向——我的工作流*实际*在做什么,而不是我本意想做什么?
```
ccg ingest orchestration.ts # reconstruct the spec
ccg ingest orchestration.ts --lint # audit the code directly
```
`ingest` 使用 ts-morph 从真实的 TypeScript 代码中还原节点、依赖项和并发性。这是
虚假边审计发现那些从未出现在任何人图中的浪费的地方。
双向转换是无损的:`spec → codegen → ingest → spec` 对于每一个测试用例
都能返回深度相等的 spec,这已由测试套件断言。TypeScript 会平铺为 `string` 的描述符(如 `url`、
`path`、`YYYY-MM-DD`)会保留在尾部的注释中,而类型系统无法表达的所有内容
—— kind、model 层级、fresh context、worktree、守卫 —— 都存在于每个函数上方的文档注释中。
两者都可以作为普通的文档来阅读。
## Web 画布
```
pnpm --filter @ccgrapher/web dev
```
编辑 YAML,图会在每次击键时重绘并重新进行 lint。**预览修复后的结构**可在不改动源码的情况下显示
折叠后的图;**应用修复**则会重写源码。它导入了与 CLI 相同的
`core`、`lint` 和 `layout` 包,因此这两者绝不会产生分歧。
## 关于该 skill 的更多信息
`skills/parallel-plan/` 会在计划包含约五个或更多步骤时触发,或者每当工作被
分发给子 agent 时触发。低于此数量时,这种繁琐的流程成本反而高于它节省的成本,而且一个在任何事情上都会触发的技能
最终会被忽视。`template.yaml` 是它复制的基础模板。
在阅读其输出时,有两件事值得注意:
- **波次(Waves)是上限,而不是指令。** 一波中包含六个步骤意味着*可能*会同时运行六个——但
速率限制、成本或共享文件等情况可能会有不同的要求。
- **层数的准确性仅取决于 `in:` 字段的诚实度。** 声明了一个不真实的依赖关系,
你就会得到一条直链。spec 就是论据;而工具的作用是检查它的一致性。
## 架构
| 包 | 功能 |
| --- | --- |
| [`core`](packages/core) | Zod schema、YAML 解析、循环检测、最长路径层级排名 |
| [`lint`](packages/lint) | 六条规则和两步修复流水线 |
| [`layout`](packages/layout) | dagre 包装器 → 节点定位与边路径规划 |
| [`render-svg`](packages/render-svg) | rough.js + 纸张纹理 + 内嵌字体 |
| [`render-mermaid`](packages/render-mermaid) | 带有 `look: handDrawn` 的 `flowchart TD` |
| [`render-excalidraw`](packages/render-excalidraw) | 带有绑定箭头的 Scene JSON |
| [`codegen`](packages/codegen) | Spec → Claude Code / 原生 TS / LangGraph |
| [`ingest`](packages/ingest) | ts-morph:编排代码 → spec |
| [`apps/cli`](apps/cli) | `ccg lint · render · codegen · ingest` |
| [`apps/web`](apps/web) | Next.js + React Flow 画布 |
**唯一的恒定法则。** `core` 计算*层级排名*;`layout` 计算*像素位置*。lint 报告中“6 层 → 4 层”的数值
来源于 `core`,绝不会来自 layout 库,因此图表和报告
不会产生悄然的分歧。dagre 以 `ranker: "longest-path"` 运行以保持匹配,并且有测试断言
两者在每个 fixture 上都保持一致。
`core` 的主入口是 bundler-safe 的;文件系统辅助程序位于 `@ccgrapher/core/node`。正是这一点
让浏览器画布能够运行完全相同的 linter。
## 开发说明
```
pnpm build # tsc -b across the workspace
pnpm test # 246 tests
pnpm test:watch
```
需 Node 22+。CI 会在 Node 22 和 24 上测试套件,并端到端地重新检查验收标准。
生成的代码是通过解析来验证的,而不是靠肉眼看:TypeScript 目标代码会在 `strict` 模式下通过真正的
`ts.createProgram` 诊断进行检验,而 Claude Code 目标代码会被解析为一个异步
函数体。Mermaid 输出则通过实际渲染来验证——在生成一些 `.mmd` 文件后
打开 `tools/mermaid-check.html`。`tools/rasterize.mjs` 会重新生成 README 中的图片。
请参阅 [CONTRIBUTING.md](CONTRIBUTING.md) —— 添加一条 lint 规则只需四个步骤,这是你
能做出的最有用的贡献。
### 示例
`diamond`、`research-desk` 和 `route-auth-audit` 是干净的——每条边都携带真实数据,因此它们
在测试套件中也兼作阴性对照组。`linear-chain` 是故意被破坏的,作为
linter 的测试用例。添加 `self-grading` 和 `wide-fanin` 是因为最初版本中没有任何内容
测试了 `SELF_GRADING` 或 `CONTEXT_COLLAPSE`。
如果你想了解这为什么值得做,`release-session` 是必读的用例。它根本不是一个 agent
工作流——它代表了人类一周的工作:九个 pull request 一个接一个地提交,然后进行一次
发布。九个任务中有六个
从未在等待任何东西,而 CI 节点声明它期望得到九个
结果,但实际上只有八条边能到达它。一个看似完整的发布,却建立在一个从未
看到完整全貌的检查之上。
```
ccg lint examples/release-session.yaml
```
| 实际运行情况——12 层 | 它的真实面貌——5 层 |
| --- | --- |
|
|
|
## 出处
该设计来源于一份交接文档,该文档是在阅读 Anatoli Kopadze 的文章
[“图工程解析”](https://x.com/anatolikopadze/status/2080668775796314331)时编写的,其中涉及了
虚假边测试、菱形模式以及各种失败模式的来源。该文档被原封不动地保留为
[HANDOFF.md](HANDOFF.md);相关的聊天记录未公开。
根据其自身的 fixture 对该交接文档进行审查时,发现了九个缺陷——其中包括
按原定规则 `HIDDEN_EDGE` 永远不会触发,每个 fixture 的入口节点都会报告
虚假的 `MISSING_INPUT`,以及某个 fixture 的层数计算完全
错误。这些解决方案正是 linter 表现出当前这种行为的原因,并且它们被记录在 git
历史中:第一次提交完全是原始交付的内容,第二次提交则包含了在其基础上构建的所有内容。
文章中的图表仅通过 URL 引用,未被本地化打包。匹配手绘和橙色的
美学风格是可以的;但照搬别人的插图是不行的。
## 许可证
[Apache-2.0](LICENSE)。请参阅 [NOTICE](NOTICE)。
如果你打算重新分发输出内容,有一件事需要知道:`render-svg` 会将其生成的 SVG 中的
[Caveat](https://github.com/googlefonts/caveat) 字体(SIL OFL 1.1)内嵌进去,因此
每个生成的 SVG 都会在文件中自带该字体的署名。如果你想要一个
不包含字体数据的文件,请使用 `--no-embed-font` 渲染。完整细节请参阅 [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md)。标签:AI智能体, Claude Code, DAG依赖图, MITM代理, 云安全监控, 工作流编排, 并行计算, 自动化攻击, 静态分析