在hydrate调用点写明投影修复先于身份校验的前提
Project CI / Repository checks (pull_request) Successful in 1m1s
Project CI / Frontend tests (pull_request) Successful in 2m57s
Project CI / Backend tests (pull_request) Successful in 3m58s
Project CI / Native shell tests (pull_request) Successful in 14m46s

裁决为不修,并把改动前提与非重入锁陷阱留在代码旁

第 18.3 节要求身份校验(第 3 步)先于 session 前滚(第 5 步)与 index/pending
重建(第 6 步),而 hydrate 的实际顺序相反。本次裁决为不修,理由与前提写进注释:

现在没有后果,是因为那道比对恒过——App 的全部 init/import 调用点都传同一个常量
seedManifest.projectId(local-project-draft),本机每个项目的 manifest 写的都是它,
projectId 分辨不出任意两个项目。该常量不属本工作包管辖。

触发后的后果也已复核为可忽略:命令仍正确返回 PLAN_PROJECT_ID_MISMATCH,前端拿不
到错数据;报错前写入的 pending/session/index 均为幂等或可重建投影,usage fold 有
按 fact 比对的幂等守卫,session.previous.json 是改名而非删除且只在 primary 缺失时
发生(即本来就该做的恢复)。

一旦 projectId 改成每项目唯一,这道校验才真正开始工作,届时顺序必须一起改,否则
hydrate 会先对一棵属于别的项目的 planning 树做完投影修复才发现认错人。注释里连同
陷阱一并写明:不能把 reconcile 直接挪到 hydrate 取锁之后——它自取项目锁,而
.agent/project.lock 是 create_new(true) 非重入的,调用方持锁再进去会死等满重试预算
然后失败;可行路径是锁外 projectId 预检,或把 reconcile 拆成薄壳 + _at_locked 由
hydrate 在自己的锁内调用(同型拆法见 observe_agent_runtime_agent_delegate)。

把提醒放在代码旁而不是只留在 decision-log,是因为真正会改 projectId 方案的人不在
本领域,不会来读这份文档。

cargo fmt --check、check:encoding 通过;纯注释与文档,无行为改动。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-18 12:45:46 +00:00
parent d44f930d1f
commit 4687807dc1
2 changed files with 20 additions and 1 deletions
@@ -424,6 +424,25 @@ pub(crate) fn hydrate_game_creator_plan_gdd_state_at(
));
}
// 这里的顺序与方案 §18.3 的固定顺序相反,是刻意保留的现状,改动前先读完这段。
//
// §18.3 要求身份校验(第 3 步)先于 session 前滚(第 5 步)与 index/pending 重建
// (第 6 步)。本函数反过来:先 `reconcile`、再前滚 session、再重建 index,最后才在
// `build_state_view_locked` 里比对 GDD/session 的 projectId 与 manifest。
//
// 现在没有后果,是因为那道比对恒过——App 的全部 init/import 调用点都传同一个常量
// `seedManifest.projectId``local-project-draft`),本机每个项目的 manifest 里写的
// 都是它,所以 projectId 分辨不出任意两个项目。该常量不属本模块管辖,此处不改。
//
// **一旦 projectId 改成每项目唯一,这道校验才真正开始工作,本顺序就必须一起改**:
// 否则 hydrate 会先对一棵属于别的项目的 planning 树做完投影修复,才发现认错了人。
//
// 改的时候有个坑:**不能把 `reconcile` 直接挪到 hydrate 取锁之后**。它自己要取项目
// 锁,而 `.agent/project.lock` 是 `create_new(true)` 的非重入文件锁,调用方已持锁再
// 进去会死等满重试预算然后失败。可行的两条路是:在 `reconcile` 之前加一次锁外的
// projectId 预检;或把 `reconcile` 拆成取锁薄壳 + `_at_locked` 核心,由 hydrate 在
// 自己那把锁内调用(仓库已有同型拆法,见 `observe_agent_runtime_agent_delegate`)。
//
// 直接传播:`PlanningStorageError` 的 Display 已是 `"{code}: {detail}"`
// 再用同一个 code 把 `to_string()` 当 detail 重包一次,只会渲染出
// `CODE: CODE: detail`code 与 detail 都没有变化。
@@ -17,7 +17,7 @@
- **测试陷阱(值得记)**:写「占住项目锁」的 fixture 时必须给锁 JSON 填**真实** `createdAt`。失效锁回收的年龄判定读的是该 JSON 字段而**不是**文件 mtime`project_write_lock_age_seconds`),填 0 会让锁显得约 1.7e9 秒老、越过 600 秒阈值被当场回收删除,hydrate 反而成功。第一版 fixture 正是这样自证失败的。
- **验证**Rust `planning_`**155 passed / 0 failed**(原 154 + 本次 1 条);`appSurface.test.ts` **383 passed / 0 failed**(378 原有 + 5 条新增);三条新回归均经变异验证,逆转对应修复即变红。`cargo fmt --check``agc:typecheck`、ESLint `--max-warnings 0``check:encoding``git diff --check` 通过。
- **撤回一条此前的审查发现**:曾判定 hydrate 读 manifest 缺符号链接判定(因其走裸 `root.join` 而非 `resolve_local_project_path`)。复核后**不成立**`read_manifest` 自身在 `metadata.file_type().is_symlink()` 处即拒(`manifest.rs`),防护在另一层;`.agent` 目录本身为符号链接的残差也无窗口,紧随其后的 `resolve_planning_path` 同样逐段判定。未据此改动代码。
- **仍未修**:① hydrate 在校验 GDD/session 的 projectId 与 manifest 一致之前已执行落盘投影修复,违反第 18.3 节固定顺序(修它须注意 `reconcile` 自取项目锁、`.agent/project.lock` 不可重入,不能把检查直接挪到 hydrate 取锁之后);② design 组展示名仍有 `agentPresentation.ts``groupConfigs``view/project-development/index.tsx``summarizeAgent` 两处硬编码「策划 Agent」,注意该两处与 `taskGroupLabels` **命名体系不同**(「X Agent」对「X组」),直接替换会连带改掉另外五个分组名,需先定命名口径。
- **仍未修**:① hydrate 在校验 GDD/session 的 projectId 与 manifest 一致之前已执行落盘投影修复,违反第 18.3 节固定顺序。**已裁决为不修**:它唯一有后果的前提是 projectId 变成每项目唯一,而该常量方案不属本工作包管辖;单独为一个不受控的假设改动权威读取路径不划算。触发后的实际后果也已复核为可忽略——命令仍正确返回 `PLAN_PROJECT_ID_MISMATCH`,写入的 pending/session/index 均为幂等或可重建投影,`session.previous.json` 是改名而非删除且只在 primary 缺失时发生。裁决与改动前提(含「不能把 `reconcile` 直接挪到 hydrate 取锁之后」这个非重入锁陷阱)已作为注释写在 `planning_hydrate.rs` 调用点旁,使提醒与会坏掉的代码同处,而不是只留在本文档里;② design 组展示名仍有 `agentPresentation.ts``groupConfigs``view/project-development/index.tsx``summarizeAgent` 两处硬编码「策划 Agent」,注意该两处与 `taskGroupLabels` **命名体系不同**(「X Agent」对「X组」),直接替换会连带改掉另外五个分组名,需先定命名口径。
- **新记一条既有问题(非 M1D 引入)**`seedManifest.projectId` 是常量 `local-project-draft`App 的 5 个 init/import 调用点全传它,因此**本机所有项目 projectId 相同**。第 18.3 节第 1 步依赖的「manifest 与 projectId 校验」因此分辨不出任意两个项目——把 A 项目的 `.agent/planning/**` 整体拷入 B 项目仍会通过。该门当前近乎恒真,须单独立项处置。
## 2026-08-18 M1D 审查修复:GDD 审批决定失败路径与恢复期弹层门控