diff --git a/docs/README.md b/docs/README.md
index 50f4da7..fb8846e 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -49,7 +49,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d
| [29-dream-design.md](./todo/system-design/29-dream-design.md) | **dream(经验汇流)**— ✅ 已实现。定时整理所有 agent 在所有公司的记忆,提炼后跨公司沉淀(不泄漏公司信息)。四条取舍已拍板并实现:去敏独立判定、四落点分层写入(经验/`SKILL.md`/`soul.md`/`agent.md`,受管区块+三道闸门)、每日定时(串行+空转保护)、引用真伪自动查、REST+控制台入口(`/api/dream/*`、`/dream`)。`innovator-evolver` 真 CLI 跑通一次。**仍缺的**:「结论对不对」只能人抽查;「套进 org/work graph 来跑」评估后用户选了不做(作用域不匹配,见文内 §5.7) |
| [30-task-delegation-reliability-gaps.md](./todo/system-design/30-task-delegation-reliability-gaps.md) | **任务委派可靠性现状**— 📝 调研记录,非设计方案。DevLoop `runUnit`、Company 招聘装配、Graph 执行三条链路核对结论一致:都是「失败预算耗尽 → 停等人工」,没有「卡住了主动通知人」这一层;DevLoop 当前还默认走 mock,真实 CLI 成功率无实证数据 |
| [31-work-graph-fail-fallback-coverage.md](./todo/system-design/31-work-graph-fail-fallback-coverage.md) | work graph 兜底边覆盖不全 — ✅ 已完成(除入口节点外强制兜底边,`planner.json`/`executor-fanout.json` 补 6 条边),978/978 单测通过;2026-08-21 又用真实 LLM 起了一条新任务做端到端回归验证,`org-planner-sdd` 自判失败后自动打回 `org-planner-prd`(3 次,零人工介入),修复确认生效 |
-| [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录**。真实回归任务里 `org-coder-impl` 从未产出任何 HTML/CSS 文件,`org-coder-validator` 却给六个视觉/交互维度全打了 100 分——rubric 的 `evidence_hint` 明确要求打开真实页面检查,validator 显然没真查就评了分。只记现象与证据,根因(提示词缺前置检查?工具不可用?)留给下次 |
+| [34-validator-scored-without-real-deliverable.md](./todo/system-design/34-validator-scored-without-real-deliverable.md) | 📝 **调研记录,原始前提已订正**。最初以为 `org-coder-impl` 从未产出任何 HTML/CSS(`org-coder-validator` 却给六维度全打 100 分),后来发现真实网站其实存在于 `graph_files` 索引不覆盖的「项目根」目录(`data/companies/{cid}/projects/{pid}/`)——这半个发现已经顺手修了(控制台「产物」面板现在会展示项目根)。订正后仍未解决的窄问题:validator 有没有真的照 `evidence_hint` 做浏览器级检查,还是即使产物存在也没真查 |
| [33-needs-gate-role-filter-bug.md](./todo/system-design/33-needs-gate-role-filter-bug.md) | ✅ **已修复**。`executor.json`/`executor-fanout.json` 里 `produces:["log"]` 声明了一个 test/integration 类节点永远不会被索引到的 kind,导致 `org-coder-validator` 被 `missing_required_input` 反复误判 blocked;改成 `required:false`(对齐 `validator.json` 既有写法)。文档记录了一条**走过的弯路**(按 role 过滤 `inboundContracts` 会削弱 T-mt25ecbd 的产出闸门,已撤回),发现于 31 号文档的真实回归验证过程中,与兜底边问题无关 |
## todo/future/(暂缓,非当前重心)
diff --git a/docs/todo/system-design/34-validator-scored-without-real-deliverable.md b/docs/todo/system-design/34-validator-scored-without-real-deliverable.md
index 1ae0353..1ca3657 100644
--- a/docs/todo/system-design/34-validator-scored-without-real-deliverable.md
+++ b/docs/todo/system-design/34-validator-scored-without-real-deliverable.md
@@ -1,8 +1,10 @@
# 34-validator-scored-without-real-deliverable. coder-validator 在没有真实产物的情况下给了满分
> **日期**: 2026-08-22
-> **状态**: 📝 **调研记录,非设计方案**——只记录现象与证据,不现场深挖根因(用户明确选择
-> 先记文档,验收链路本身的调查留给下次)
+> **状态**: 📝 **调研记录,非设计方案,且 §一/§二的核心前提已被 §零 推翻大半**——
+> 原始判断("index.html 从未被写出来")是错的,真实网站文件其实存在,只是不在
+> `graph_files` 索引里。修订后的问题更窄:validator 有没有**真的**照 `evidence_hint`
+> 做浏览器级检查,还是即使产物存在也照样走了个形式。这一层仍未核实,留待下次。
> **触发**: `e2e-demo` 公司真实回归任务 `T-mt2r6fz0-125b`(goal `G-mt2r62ia-73af`,
> root run `run_0e9074815130`)——[31 号文档](./31-work-graph-fail-fallback-coverage.md)
> 兜底边修复 + [33 号文档](./33-needs-gate-role-filter-bug.md) 契约修复之后,把这条任务
@@ -10,6 +12,36 @@
---
+## 零、2026-08-22 补充:原始判断错了一半——真实网站文件其实存在
+
+写这份文档时只查了两处:`org-coder-impl`/`org-coder-test` 各自的节点私有目录
+(`runs/{rootRunId}/nodes/{nodeRunId}/`)和 `graph_files` 索引表,两处都没有
+HTML/CSS/JS,于是断言"首页从未被写出来"。
+
+后来在**同一次会话里**给控制台加"任务产物汇总"面板(用户反馈的另一个需求)时,
+读 `graph-engine.js` 的 W2 段落才发现:这个引擎给每个任务的代码分配了一个**跨节点
+共享的项目根**(`ensureProjectRoot`,复用 DevLoop 的 `path-resolver.allocateProjectRoot`,
+落在 `data/companies/{cid}/projects/{pid}/`),这个目录**不受 `graph_files`/
+`indexNodeOutputs` 覆盖**——后者只扫每个节点自己的私有产出目录。真去看
+`data/companies/e2e-demo/projects/p-v82tpc/` 才发现:
+
+```
+index.html 2.2KB 真实的 apple.com 风格单页站点(hero + 3 个卖点区块 + 平滑滚动导航)
+script.js 1.0KB
+styles.css 3.3KB
+tests/unit-tester.check.js
+workgraph-plan-T-mt2r6fz0-125b.md
+```
+
+内容核对过:3 个卖点区块的标题与顺序(多智能体协作 → 全链路自动化 → Skill 竞技场)
+与 `org-coder-impl` 自己 `_plan.md` 里写的验收对照表逐字对应,不是空壳/占位文件。
+
+**这次查漏的教训**:`graph_files` 索引 + 节点私有目录,只是这个系统里**两个**产出
+落点中的一个;第三个(项目根)当时完全没查。下面 §一/§二 保留原文作历史记录,
+但"index.html 从未被写出来"这句话是错的,请按本节订正读。
+
+## 一、(原始记录,前提有误,见 §零)
+
## 一、现象
任务最终状态:`completed`,`org-planner` / `org-coder` / `org-validator` 三个 org 节点
@@ -55,43 +87,46 @@ contract.json 跨节点契约快照
| `responsive_layout` | 「浏览器依次设置视口宽度 375/768/1440px 检查横向滚动条与重叠」 |
| `resource_integrity` | 「打开浏览器开发者工具 Console + Network 面板刷新页面检查」 |
-这五项全都以"打开浏览器 / 打开 `index.html`"为前提——而 `index.html` 从来没有被
-`org-coder-impl` 写出来过。`org-coder-validator` 不可能真的执行了这些检查步骤,
-却仍然给出了 100/100。唯一有证据支撑的是第六维度 `pipeline_auto_recovery`
-(权重 0.25,本次回归验证真正要看的那一维)——它的 `evidence_hint` 是"读
-`attempt-*.json` 搜索 `fail_reason=input_underspecified`",这条本身不依赖前端产物,
-`org-coder` 的 `remediationNote` 里也确实写了"检索本 run 下 nodes/\*/attempt-\*.json
-的 verdict/fail_reason 字段,未发现任何一条真实的判定"——这条像是真查过的。
+这五项全都以"打开浏览器 / 打开 `index.html`"为前提——**当时以为** `index.html`
+从没被写出来过,所以断言 validator 不可能真的执行了这些检查步骤。**这个前提错了**
+(见 §零):`index.html`/`styles.css`/`script.js` 其实都在 `projects/p-v82tpc/`
+下真实存在,内容也对得上任务要求。
-**结论**:`org-coder-validator` 至少在 5/6 维度上没有依据 `evidence_hint` 去核实
-产物是否存在就给了分,是一次**没有真实检查支撑的评分**(rubber-stamped),
-不是"检查过但判断错误"。
+**订正后的结论**:不能再说这是"没有产物却给了满分"——产物是有的。真正没验证过的
+是**更窄的一个问题**:`org-coder-validator` 是否真的照 `evidence_hint` 打开了这个
+页面、量了字号比例、切了三档视口、开了 Console/Network 面板检查,还是即使产物
+存在,它也只是读了读源码/自己的 `_plan.md` 就直接给了 100 分,没有真的做那些
+`evidence_hint` 点名要求的浏览器级验证动作。这个问题现在的证据**不足以下结论**——
+六个维度全 100 分本身既可能是"真查过、页面确实做得不错",也可能是"没查、乐观估计",
+两种情况从现有的 `attempt-*.json`/`remediationNote` 文本里区分不出来。
-## 三、影响面(未核实,留给下次)
+## 三、影响面(订正后,仍未核实,留给下次)
- 这不是 31/33 号文档改动引入的新问题——两次修复只是让流程**不再卡死**,
- 让这条原本会一直 blocked 的任务真的走到了 `completed`,才把这个一直存在的
- 验收缺口暴露出来。
-- 如果这是系统性行为(不是这一次运气差),意味着 `executor.json` 这类骨架的
- `validator` 节点在**缺少真实产物**时可能倾向于给出宽松评分,而不是判 fail
- 或至少低分——这会让"任务显示已完成"和"任务真的达成了原始需求"之间出现
- 静默的落差,且这个落差**在界面上完全看不出来**(`org-coder` 的
- `remediationNote` 里其实提到了"artifacts/ 下产物缺失"这类线索,但那是给
- 下一轮 agent 看的内部备注,不是给人看的告警)。
+ 让这条原本会一直 blocked 的任务真的走到了 `completed`,才有机会去看"最终产出
+ 在哪、验收是怎么做的"这件事。
+- 真正独立于本文档、且已经确认修复的一个衍生问题:`graph_files`/`taskFiles`
+ 索引**不覆盖项目根**,这意味着控制台原本设想的"任务级产物汇总"面板如果只读
+ `graph_files`,会永远看不到真实交付物(这正是 §零 发现问题的路径)。这一半
+ 已经在同一次改动里修了:`graph-engine.readProjectRootRecord` 现在随
+ `GET /api/graph-runs/:id` 一起吐出来,控制台的「产物」按钮会分别列"最终交付物 ·
+ 项目根"和"节点产出索引"两节,见 `web/src/components/graph-console/TaskFilesPanel.vue`。
+- 仍然悬而未决的是 validator **有没有真的做过视觉/交互检查**这件事本身——这需要
+ 证据链(比如强制要求 validator 在 `attempt-*.json` 里回填它量出的具体数字:
+ h1:tagline 字号比、三档视口下的滚动条状态等),而不是只回一个分数。
## 四、留待下次调查的问题(不在本次范围内动手)
- [ ] `coder-validator` 本体(`system/agents/unit-tester/agent.md` 或对应角色)的
- 提示词里,评分前有没有一步"先确认产物文件真的存在"的强制检查?如果没有,
- 这可能是这次跑分虚高的直接原因。
-- [ ] `coder-validator` 是否有可用的浏览器/文件读取工具去真的执行
- `evidence_hint` 里描述的那些检查(截图、开发者工具)?如果工具不可用,
- "满分"可能是它在无法验证时的默认基调,而不是主动撒谎。
-- [ ] 这是不是这次真实回归测试里 `org-coder`/`unit-tester` 两个新招聘本体
- (见 [33 号文档提交时的记录](./33-needs-gate-role-filter-bug.md))本身
- 还没配好角色提示词导致的一次性现象,还是所有 `executor.json` 骨架的
- validator 节点普遍如此?
-- [ ] 是否需要给这类"需要打开真实产物检查"的维度加一道**引擎级的前置闸门**
- (类似 T-mt25ecbd 的 produces 闸门):`evidence_hint` 里点名要读的文件
- 如果在产物索引里查不到实例,直接判该维度 0 分或整体 blocked,不依赖
- validator 自己主动去查。
+ 提示词里,评分前有没有一步强制要求"回填 evidence_hint 点名要量的具体数字",
+ 而不是只回一个分数?没有这一步的话,"真查过"与"没查"从产出上完全无法区分。
+- [ ] `coder-validator` 是否有可用的浏览器工具(截图、DOM 检查)去真的执行
+ `evidence_hint` 里描述的那些检查?如果连工具都没有,让它对着"评分要求打开浏览器"
+ 的 rubric 打分,本身就是设计上要求了它做不到的事。
+- [ ] 抽查其他任务(比如 `p-bkhylu`/`p-v1mpcl` 这两个已存在的项目根)的 validator
+ 评分记录,看这次的六个满分是个例还是常态。
+- [ ] 是否需要给"必须打开真实产物检查"的维度加一道**引擎级的前置闸门**:至少确保
+ 被检查的文件(如 `index.html`)在 `evidence_hint` 点名的路径下真实存在
+ (现在项目根已经可查,这道闸门补起来比 §零 之前容易),如果连文件都不存在
+ 就直接拦下不许打分;文件存在之后 validator 有没有真的看,还是只能靠 §四第
+ 一条的"强制回填具体数字"来间接约束。
diff --git a/server/routes/graph-run.js b/server/routes/graph-run.js
index 305193e..93d148f 100644
--- a/server/routes/graph-run.js
+++ b/server/routes/graph-run.js
@@ -368,6 +368,11 @@ router.get('/:id', h((req, res) => {
edges,
companyRoot: companyRootDir(state.run.company_id),
taskFiles: findFiles({ companyId: state.run.company_id, taskId: state.run.task_id }),
+ // 控制台「产物」面板要区分两类产出:graph_files 只索引各节点私有目录下的东西
+ // (规划笔记、日志、契约快照……),真实交付物(代码)写在这个任务共享的项目根里
+ // (见 graph-engine.js「W2 · 每个 root run 一个项目根」那段),两者不是同一份数据。
+ // readProjectRootRecord 按 root_run_id 查,任一层 run 传进去结果相同(同一棵树共享一个项目根)。
+ projectRoot: engine.readProjectRootRecord(state.run),
loopRecords: listLoopRecords(state.run.id),
childRuns: listChildRuns(state.run.id),
budget,
diff --git a/server/services/graph-engine.js b/server/services/graph-engine.js
index bb47e06..8db913c 100644
--- a/server/services/graph-engine.js
+++ b/server/services/graph-engine.js
@@ -4363,6 +4363,10 @@ module.exports = {
// ensureProjectRoot 的真实调用方是 advanceOne,diffRepoStatus 的是 applyBoundaryGuard;
// 导出只是让单测能直接验这两条判据,不是新开一圈公开面。
ensureProjectRoot,
+ // readProjectRootRecord:控制台「产物」面板要知道这个任务的真实交付物写在哪——
+ // graph_files 只索引每个节点自己私有目录下的产出,project-root.json 指向的才是
+ // 跨节点共享的项目根(真实代码落盘的地方,见本文件 W2 那段大注释)。
+ readProjectRootRecord,
diffRepoStatus,
// 真实调用方是 advanceOne(每次节点执行前后各一次)。导出 + 可注入的 after 快照,
// 是为了让「越界 → success 降级成 fail + fail_reason=boundary_escape」这条分支
diff --git a/server/tests/unit/graph-run-route-io.test.js b/server/tests/unit/graph-run-route-io.test.js
index e67b21d..ca16df0 100644
--- a/server/tests/unit/graph-run-route-io.test.js
+++ b/server/tests/unit/graph-run-route-io.test.js
@@ -144,4 +144,28 @@ describe('GET /api/graph-runs/:id — R-19 输入输出三字段', () => {
const r = await fetch(`${base}/api/graph-runs/run_nope`);
expect(r.status).toBe(404);
});
+
+ // 34 号文档:graph_files 只索引节点私有目录,真实交付物写在这个任务共享的
+ // 项目根(W2 ensureProjectRoot)——控制台「产物」面板要同时看到两处,缺一个字段
+ // 就是同一路「实现在、零调用方」再来一次。
+ it('projectRoot:没分配过就是 null(不假装有)', async () => {
+ const r = await fetch(`${base}/api/graph-runs/${runId}`);
+ const d = await r.json();
+ expect(d.projectRoot).toBeNull();
+ });
+
+ it('projectRoot:分配过之后原样透传(root_path/pid 与落库的记录一致)', async () => {
+ const engine = require('../../services/graph-engine');
+ const run = wdb.getRun(runId);
+ const rec = engine.ensureProjectRoot({ run, companyId, nodeRunId: 'rn-fake' });
+ expect(rec).toBeTruthy();
+
+ const r = await fetch(`${base}/api/graph-runs/${runId}`);
+ const d = await r.json();
+ expect(d.projectRoot).toBeTruthy();
+ expect(d.projectRoot.pid).toBe(rec.pid);
+ expect(d.projectRoot.root_path).toBe(rec.root_path);
+ // 幂等:再查一次不会重新分配一个不同的项目根
+ expect(engine.ensureProjectRoot({ run, companyId, nodeRunId: 'rn-fake-2' }).root_path).toBe(rec.root_path);
+ });
});
diff --git a/web/src/components/graph-console/GraphRunPane.vue b/web/src/components/graph-console/GraphRunPane.vue
index 97872b0..664e699 100644
--- a/web/src/components/graph-console/GraphRunPane.vue
+++ b/web/src/components/graph-console/GraphRunPane.vue
@@ -101,6 +101,10 @@ const budget = ref(null);
// 「没有产物」和「后端没告诉我有没有产物」必须分得开。
const taskFiles = ref(null);
const companyRoot = ref(null);
+// 这个任务真实交付物的落点(W2「每个 root run 一个项目根」)——与 taskFiles 是两类数据,
+// 不要合并:graph_files 只索引节点私有目录,projectRoot 才是代码真正写的地方。
+// 没有记录(节点还没跑到 ensureProjectRoot 那一步)时保持 null。
+const projectRoot = ref(null);
const events = ref([]); // SSE 事件缓冲,供详情面板的「运行日志」tab 消费
const selectedDefId = ref(null);
const loading = ref(false);
@@ -208,6 +212,7 @@ async function load() {
budget.value = j.budget || null;
taskFiles.value = Array.isArray(j.taskFiles) ? j.taskFiles : null;
companyRoot.value = j.companyRoot || null;
+ projectRoot.value = j.projectRoot || null;
mode.value = j.run?.execution_mode || 'manual';
push();
} catch (e) {
@@ -226,6 +231,7 @@ function push() {
childRuns: childRuns.value,
// R-19:右栏「输入输出」tab 的实例侧数据,与 nodes/edges 走同一条 load
taskFiles: taskFiles.value, companyRoot: companyRoot.value,
+ projectRoot: projectRoot.value,
});
}
diff --git a/web/src/components/graph-console/TaskFilesPanel.vue b/web/src/components/graph-console/TaskFilesPanel.vue
new file mode 100644
index 0000000..d157bd6
--- /dev/null
+++ b/web/src/components/graph-console/TaskFilesPanel.vue
@@ -0,0 +1,241 @@
+
+
+
+
+
+ 这个任务的代码写在这里(跨节点共享,与下面「节点产出索引」不是同一批文件)。
+ {{ projectRoot.root_path }} 读取文件树中… 读不出来:{{ projErr }} 项目根已分配,但目录里还没有文件。 跨这个任务下的全部 run/节点,只列当前有效版本;规划笔记/日志/契约快照这类,不是最终交付物。 产物索引未随响应下发(后端还没返回 这个任务下还没有任何节点产出并登记进索引的文件。最终交付物 · 项目根
+
+
+ 节点产出索引
+ taskFiles),这一块整体降级——不猜。
+
+