--- title: "项目 reviewer 未同步到受控文档职责" status: done created: 2026-09-13 updated: 2026-09-22 severity: critical --- # 项目 reviewer 未同步到受控文档职责 > **W3 UI-rerun 关闭(2026-09-22)**:`status=done`。真实 Playwright 账本 PASS:[`ui-acceptance-results.json`](../test-reports/assets/2026-09-22-docrev-ui-rerun/ui-acceptance-results.json)(main UI-00..16)+ [`ui-acceptance-results-post-restart.json`](../test-reports/assets/2026-09-22-docrev-ui-rerun/ui-acceptance-results-post-restart.json)(UI-15)。任务 tip 已合入本地 `dev`;详见 [short-term](../ops/short-term/2026-09-22-docrev-ui-rerun.md) 与 [测试报告](../test-reports/2026-09-22-docrev-ui-rerun-test-report.md)。 ## 现象 第17轮真实 UI(schema 30,`dev@639969f`):UI-00 至 UI-12 PASS。UI-13a 精确职责断言首次暴露产品缺陷: - UI-01 页面建项目把 USER_B 的职责下拉设为 `reviewer` - B 登录打开 `doc_proj6145_intake` 权限矩阵时,select `value` / badge `data-doc-role` 实际为 `viewer` - 失败:`reviewer B intake duty select must be reviewer: expected_duty:reviewer;got:viewer` 现场文档头仍写「编制人: 编制工程师 / 审核人: 审核负责人 / 批准人: 批准负责人」。 ## 根因分析 只读查本轮 `project 6145`(不暴露账号密码/token): | 层 | 证据 | 后果 | |---|---|---| | 立项 UI | `CreateProjectModal` 把 B 标成 `reviewer`;默认还勾选 seed `id=3` 李主管 / `id=4` 王总监 | 用户意图是 B=审核人 | | 成员持久化 | `toApiProjectRole` **禁止** author/reviewer/approver,一律映射 `member`;payload 无 document duty | B=`171` 落成 `project_role=member` | | `project_members` | `170` owner(A)、`171` member(B)、`3`/`4` member | 库表无法区分 B 是 reviewer | | 任务生成 | `bizdev.service`:`ReviewerID==0` → **写死 2** +「审核负责人」;`ApproverID==0` → **写死 3** | 与真实成员无关 | | `document_records` | intake / prelim_design 均为 `author_id=170`(恰为创建者 UserID)、`reviewer_id=2`、`approver_id=3` | 第二份文档继承了同一套假职责 | | 列表 API | 原样投影 `reviewer_id=2` | 前端拿到权威但错误的 id | | `resolveCurrentDocRole` | 受控文档按 id 匹配,fail-closed 为 viewer | B=`171` ≠ `2` → viewer;**不是前端提权,是服务端职责丢失** | `document_duty_assignments` 只挂官方工作流实例任务,聊天 `TRIGGER_TASK` 生成账本不走该表。 ## 严重度 `critical` 项目创建时明确指定的审核人在后续每份受控文档上变成查阅者;写路径虽因 fail-closed 未把 B 提成编制人,但审核职责与权限矩阵验收全部失真。 ## 关联整改 - Remediation: [../remediation/2026-09-13-document-revision-reviewer-duty-sync.md](../remediation/2026-09-13-document-revision-reviewer-duty-sync.md) - Short-term: [../ops/short-term/2026-09-13-document-revision-reviewer-duty-sync.md](../archive/ops/2026-09-13-document-revision-reviewer-duty-sync.md) - 分支: `fix/document-revision-reviewer-duty-sync` ## 证据 - 失败归档(只读,不改不删):`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/schema30-ui13a-reviewer-duty-sync-failure/` - `phase-state.json`:`controlledDocId=doc_proj6145_intake`,项目 `UI验收-超声微创系统-mu0bvnp8` - Playwright:`expected_duty:reviewer;got:viewer` ## 独立审查收紧(同一分支,暂不合入) 独立审查指出首轮 Heal「已是任意在任成员则保留」会让批准人串位:6145 类场景 `approver=3`(李主管也是成员)而 assigned=`4` 时不应保留 3。项目 `document_duty` 才是权威。 立项后成员增改/恢复若只发 `project_role`,`document_duty` 会静默失效。更新成员时若省略 `project_role` 且带 `document_duty`,默认 `member` 会把 owner 降级。 生成/列表路径若吞掉自愈错误,会带着 `reviewer_id=2` 假成功。 ## 最后一轮缺口(已在同分支修复) - 移出成员只改 `exited` 不同步文档;前端移出不刷新矩阵 - 空 assigned 若保留非成员占位,退出审核人会留在文档上 - reviewer→viewer 时人仍 active,generic heal 会把槽位留下,必须按更新前 duty 显式撤销 - `CreateOrGetDocument` 错误被 `_` 吞掉会假成功 ## 原子清槽约束(已在同分支修复) 职责撤销/转移不得在成员 upsert 已提交后再用服务层前后快照做非原子清理。权威路径是同一 DB 事务:`pg_advisory_xact_lock` → 读旧 duty / 冲突 holder → 成员更新 → 精确清 `reviewer_*` / `approver_*`。随后 heal 填新 holder。 ## Heal 与成员写竞态(已在同分支修复) `HealProjectDocuments` 若无项目锁、用 pool 先读后写,会出现:heal 读到 reviewer A → 成员事务持锁把 A 清槽并提交为 B → 旧 heal 把 A 写回。现已与成员写共用 `pg_advisory_xact_lock(project_id)`,解析与 `document_records` 更新在同一事务。 ## 开放问题 - 本轮不重跑真实 UI-13a;不得标 E2E 通过或任务 `done` - **6145 剩余风险**:迁移后 owner 会补 `author`;B(171)当时只落成 `member` 且 `document_duty=''`。服务端不能臆造 reviewer。须再次经成员 API 写入 `document_duty=reviewer` 后,自愈才会把文档从占位 `2` 对齐到 B。 - 隔离 PG 集成须用临时库 + `TEST_DATABASE_URL`,见 `scripts/run-isolated-duty-sync-pg-tests.sh`;禁止默认连 `agent_system_dev`