diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_hydrate.rs b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_hydrate.rs index f97731c67..02d09ebc2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_hydrate.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_protocol/planning_hydrate.rs @@ -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 都没有变化。 diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index c3a58bea7..b4c505bf1 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -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 审批决定失败路径与恢复期弹层门控