Commit 39213ab2ce8268aabe54a1fa75a082161e11885e
1 parent
ebfcc437
graphlemon: auto-commit uncommitted changes for e30e6617-203e-4941-b85f-131e4ae9…
…be04-main-checkout-drift
Showing
1 changed file
with
148 additions
and
0 deletions
docs/build-report.md
0 → 100644
| 1 | +# graphlemon 构建报告 | |
| 2 | + | |
| 3 | +**六个 Phase,45 个 Unit,一套真正跑起来的"熄灯"软件工厂。** | |
| 4 | + | |
| 5 | +graphlemon 是一个 Level 5 自主研发系统:固定的 Planner → Coder → Validator Org Graph,驱动每个任务 | |
| 6 | +动态生成的 Work Graph,完全跑在 Claude Code 自己的 `claude -p` 后端之上——能写代码、能抓自己的回归, | |
| 7 | +到本次会话结束时,还能在无人读 diff 的情况下重新部署自己正在运行的进程。 | |
| 8 | + | |
| 9 | +- **状态**:PH0~PH5 全部完成,45 / 45 个 Unit | |
| 10 | +- **测试**:552 个测试全绿 | |
| 11 | +- **Git**:`fc94ae8`(初始基线)→ `59e96d3`(U-043,当前 tip) | |
| 12 | +- **数据模型**:17 张 sqlite 表 | |
| 13 | +- **Unit 分类**:Risk Gate 18 / Value Path 17 / Enabler 10 | |
| 14 | +- **过程中"停下来问用户"的关键决策**:3 次 | |
| 15 | + | |
| 16 | +--- | |
| 17 | + | |
| 18 | +## Phase 链路 | |
| 19 | + | |
| 20 | +``` | |
| 21 | +PH0(9 units,Enabler)→ PH1(6 units,MVP 闭环)→ PH2(7 units,质量闭环) | |
| 22 | + → PH3(10 units,控制台)→ PH4(3 units,全量回归收口)→ PH5(10 units,自举能力) | |
| 23 | +``` | |
| 24 | + | |
| 25 | +--- | |
| 26 | + | |
| 27 | +## PH0:脚手架与第一个高风险假设 | |
| 28 | + | |
| 29 | +**Unit**:U-001 ~ U-008、U-004.5 | |
| 30 | + | |
| 31 | +15 张核心表的数据模型、Hermes 人设迁移到 `.claude/agents/*.md`、一套受 `RUN_REAL_AGENT_E2E` 环境变量 | |
| 32 | +门控的真实 API 测试 harness,以及本地后端/前端的骨架——后面一切工作的地基。 | |
| 33 | + | |
| 34 | +> **假设验证——不是"干净通过"**:A-2 假设在这里被真正验证:Planner/Validator 的人设几乎可以原样迁移, | |
| 35 | +> 但 Coder 的 `SOUL.md` 不行——里面硬编码了一个真实的飞书 Base API token、整段"认领"机制强绑定 | |
| 36 | +> `lark-cli`、还指示自己再跑一次 `claude --dangerously-skip-permissions`,三处都必须删除或重写才能安全 | |
| 37 | +> 运行。 | |
| 38 | + | |
| 39 | +--- | |
| 40 | + | |
| 41 | +## PH1:第一条真正跑通的闭环 | |
| 42 | + | |
| 43 | +**Unit**:U-009 ~ U-014 | |
| 44 | + | |
| 45 | +用"实现贪吃蛇游戏"这个目标真实跑通全链路:Planner 产出 BRD→PRD→SDD→Benchmark,Coder 实现并自测, | |
| 46 | +Validator 复核并给出根因归属——架构文档 §11 验收标准第 1-3 条,是真跑出来的,不是模拟的。 | |
| 47 | + | |
| 48 | +> **假设确认**:A-1 假设在这里被确认——一次真实花费 $0.46 的 spike 证明了 Claude Code 的动态工作流原语 | |
| 49 | +> (`agent()`/`parallel()`/`pipeline()`)可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果。 | |
| 50 | + | |
| 51 | +> **平台层怪癖(实测发现)**:`--json-schema` 一旦和 `--agent` 同时传,会被静默忽略——模型退化成把结构化 | |
| 52 | +> 内容塞进自由文本里的一个 ```` ```json ```` 围栏代码块,而不是填充 `structured_output`。官方文档没有 | |
| 53 | +> 说明,是靠真实调用发现的。 | |
| 54 | + | |
| 55 | +--- | |
| 56 | + | |
| 57 | +## PH2:教会它从自己的错误里恢复 | |
| 58 | + | |
| 59 | +**Unit**:U-015 ~ U-021 | |
| 60 | + | |
| 61 | +三视角根因判定、Org-level Loop 的 `fast_lane`/`full_replan` 路由、双层预算护栏、Claude plan 额度耗尽的 | |
| 62 | +等待重试机制、Graph Engine 断点恢复。人为植入一个 bug,从一端进去,从另一端出来时已经是一次真实的修复、 | |
| 63 | +重新验证、任务关闭。 | |
| 64 | + | |
| 65 | +--- | |
| 66 | + | |
| 67 | +## PH3:一个不需要构建步骤的控制台 | |
| 68 | + | |
| 69 | +**Unit**:U-022 ~ U-030、U-024.5 | |
| 70 | + | |
| 71 | +项目创建、目标输入、实时仪表盘、任务看板与详情页、DAG 图可视化、实时日志查看器、Artifact/Skill 浏览器、 | |
| 72 | +配置页——纯原生 HTML/CSS/JS,配一个手写的 WebSocket 服务端,因为"纯 stdlib、无第三方依赖、无构建步骤" | |
| 73 | +这条约束对 graphlemon 自己的控制台同样适用。 | |
| 74 | + | |
| 75 | +> **真实 bug**:HTTP 服务端各请求线程和 WebSocket 轮询线程共享同一个 sqlite 连接时,如果关闭连接没有 | |
| 76 | +> 持有和读写操作同一把锁,会**直接把解释器段错误崩溃**——"关闭"看起来像一次性操作,但对同一个连接对象 | |
| 77 | +> 而言并不天然安全。修复后连续压测多次全部稳定。 | |
| 78 | + | |
| 79 | +> **后期补漏**:一次收口审计中发现,WebSocket 重连后任务看板和任务详情页从未真正重新拉取过状态快照—— | |
| 80 | +> 只有仪表盘做出反应,而且也只是切换了一个提示条。现在三处重连后都会真正刷新,符合 REQ-12 自己的边界 | |
| 81 | +> 用例要求。 | |
| 82 | + | |
| 83 | +--- | |
| 84 | + | |
| 85 | +## PH4:两轮审计,十个真实缺口 | |
| 86 | + | |
| 87 | +**Unit**:U-031 ~ U-033 | |
| 88 | + | |
| 89 | +把 06 节 REQ-01~REQ-14 的全部验收用例,逐条对照实际测试覆盖重新核对了两遍——而不是相信"单测全绿"就 | |
| 90 | +等于"验收要求真的被覆盖了"。 | |
| 91 | + | |
| 92 | +> **结构性缺口**:Work Graph 的三个核心控制器(规划、执行、单节点运行)各自都做过完整单测,但**从来没有 | |
| 93 | +> 在生产代码里被真正串联调用过**——唯一验证过的闭环(PH1 的贪吃蛇场景)悄悄绕开了它们,走了一条更简单 | |
| 94 | +> 的并行路径。补上这层编排后,真实 API 调用又暴露出两个新 bug(一个 manifest 字段名不匹配、一个缺失的 | |
| 95 | +> 工作目录),都是只有真实调用才会暴露的问题。 | |
| 96 | + | |
| 97 | +> **第二轮审计**:PH5 开工前对验收用例的又一次逐条复核,发现了 5 个新的真实缺口:一处遗漏的中间状态写入、 | |
| 98 | +> 一个从未真正测过的调度器场景、三条"节点被标记 blocked 但从未告知父任务"的失败路径、一个原本完全不存在 | |
| 99 | +> 的 PRD 一致性检查、外加一处偏弱的测试断言。其中一次修复(新增的一致性检查调用)当场抓到模型把 | |
| 100 | +> `consistent` 字段返回成 JSON **字符串** `"false"`,而 Python 的真值判断把它当成了 `True`。 | |
| 101 | + | |
| 102 | +> **假设正式收口**:A-2 假设在这里正式收口,结论仍是"部分成立"——PH0 之后又有四个不同 Unit 里真实调用 | |
| 103 | +> 过迁移后的人设,没有再暴露新的偏差。但结论不会因此升级为"confirmed":那次重写已经发生了,后面跑得再 | |
| 104 | +> 干净也不能把它抹掉。 | |
| 105 | + | |
| 106 | +--- | |
| 107 | + | |
| 108 | +## PH5:教会它重新部署自己 | |
| 109 | + | |
| 110 | +**Unit**:U-034 ~ U-043 | |
| 111 | + | |
| 112 | +一整套蓝绿自部署机制:在独立的 git worktree 里拉起候选实例、跑三项 cutover 前健康检查、切换生产流量、 | |
| 113 | +在监控窗口内采样四类信号、必要时回滚——包括"回滚本身也失败"这条兜底路径。 | |
| 114 | + | |
| 115 | +> **SDD 内部冲突**:设计文档和正式规范对"到底什么条件会触发一次自部署"有分歧——设计文档说"self-target | |
| 116 | +> 改动通过验收即可",规范原文却把它挂在一条已存在的 `core_engine` bug 记录上——而一次干净的自我改进任务 | |
| 117 | +> 根本不会产生这条记录。没有挑一种理解悄悄往下走,而是直接问了用户。 | |
| 118 | + | |
| 119 | +> **真实 bug**:用 `kill(pid, 0)` 做存活检测分不清"真的在跑"和"已经退出但从未被父进程回收"——一个僵尸 | |
| 120 | +> 进程能让这个检测正常通过。一个真实(未 mock)的、真的等待进程消失的测试因此一直挂起,直到补上正确的 | |
| 121 | +> 进程回收逻辑才修好。 | |
| 122 | + | |
| 123 | +> **其他发现**:缺一个跨重启追踪候选实例 checkout 目录的字段、后端服务完全没有命令行入口、参数表里唯独 | |
| 124 | +> 缺一个固定的生产端口、也没有 `/api/health` 端点给健康检查调用。 | |
| 125 | + | |
| 126 | +--- | |
| 127 | + | |
| 128 | +## ⚠️ 尚未真实执行的一项 | |
| 129 | + | |
| 130 | +U-042 的验收测试——对本仓库执行一次真实的自举 cutover,真实调用 `claude`、真实替换正在运行的进程——已经 | |
| 131 | +建好并且在 mock 下全部通过,但被一个刻意设置得更严格、和项目里其他真实测试用的环境变量**不同**的开关 | |
| 132 | +(`RUN_REAL_SELF_HOSTING_E2E=1`)挡住,因为这是整个代码库里爆炸半径最大的一个动作:它能替换掉正在运行 | |
| 133 | +的 graphlemon 进程本身,还会在这个真实仓库上执行 git revert。 | |
| 134 | + | |
| 135 | +**这项测试从未被真正执行过。** 是否要真的跑一次,是需要用户决定的事,不是构建过程自己能替你做的决定。 | |
| 136 | + | |
| 137 | +--- | |
| 138 | + | |
| 139 | +## 假设台账 | |
| 140 | + | |
| 141 | +| ID | 假设 | 结论 | | |
| 142 | +|---|---|---| | |
| 143 | +| A-1 | Claude Code 的动态工作流原语可以通过 `claude -p` 无交互调用,并返回可解析的结构化结果 | **confirmed** | | |
| 144 | +| A-2 | 本地 Hermes 人设文件(`SOUL.md`/`AGENTS.md`)可以原样迁移到 `.claude/agents/*.md`,无需大改 | **部分成立**(已收口) | | |
| 145 | + | |
| 146 | +--- | |
| 147 | + | |
| 148 | +*graphlemon · 纯 stdlib Python 实现,无任何第三方依赖 · 552 个测试 · 17 张表 · `fc94ae8` → `59e96d3`* | ... | ... |