artfusion/ccgrapher

GitHub: artfusion/ccgrapher

一个 agent 工作流 linter,通过分析步骤间的真实数据依赖,找出不必要的串行等待并生成可并发的编排代码和可视化图表。

Stars: 0 | Forks: 0

# ccgrapher [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/artfusion/ccgrapher/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](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 层 | | --- | --- | | 带有两条标记为 'carries no data' 的红色虚线的六层高大阶梯图 | 三个审阅者并排位于同一行的四层图 | 这三个审阅步骤互不需要。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(层)。节点总是位于其最深依赖项的下一行,因此任何两个 没有依赖关系的节点都会自动排列在同一行。菱形结构正是由此自然产生的: A four-layer diamond: split, five workers on one row, a checker, then a merge ## 六条 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 ``` Split view: YAML on the left, the laid-out graph in the middle, live lint findings underneath 编辑 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 层 | | --- | --- | | 九个 pull request 依次合并的十二层阶梯图 | 九个 pull request 中有六个并排位于同一行的五层图 | ## 出处 该设计来源于一份交接文档,该文档是在阅读 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代理, 云安全监控, 工作流编排, 并行计算, 自动化攻击, 静态分析