diff --git a/.doc/audit/2026-09-15-mock-deliverables-pollute-real-projects.md b/.doc/audit/2026-09-15-mock-deliverables-pollute-real-projects.md new file mode 100644 index 00000000..9cfb1ee6 --- /dev/null +++ b/.doc/audit/2026-09-15-mock-deliverables-pollute-real-projects.md @@ -0,0 +1,85 @@ +--- +title: "前端 mock 交付物污染真实项目:幻影受控文档伪造终审已生效态与法定职责" +status: done +created: 2026-09-15 +updated: 2026-09-15 +severity: critical +--- + +# 前端 mock 交付物污染真实项目:幻影受控文档伪造终审已生效态与法定职责 + +## 现象 + +用户在 **test 环境**(127.0.0.1:8081)新建测试项目「测试项目」(test 库 `projects.id=1`,唯一项目),工作台立即显示 11 份「受控文档」,其中包括与该项目毫无关系的: + +- 《4K超高清荧光内窥镜系统立项与可行性报告》 +- 《微创外科内窥镜全球竞品技术与临床深度调研报告》 +- 神经外科双频超声吸引(NeuroPulse)系列 +- CUSA 超声手术吸引系统系列 + +这些文档: + +1. 显示**伪造的生命周期状态**(`reviewing` / `approved`——`DeckDrawer.tsx` 将 `approved` 渲染为「**终审已生效**」),即用户所称「生效区」出现从未经过任何 docgov 生命周期流转的"已生效"文档; +2. 带有**伪造的法定职责人**(author 张工 / reviewer 李主管 / approver 王总监——均非真实用户); +3. 带有伪造版本号(V1.0)、伪造更新时间、伪造正文全文; +4. 在项目成员视角按真实受控文档参与 RBAC 可见性过滤与工作流槽位合并,**与真实服务端文档混排**。 + +而 test 库 `document_records` 为 **0 行**——服务端受控文档台账完全为空,这 11 份文档**全部来自前端硬编码 mock 数据**。 + +## 根因分析 + +### 1. mock 交付物硬编码绑定 projectId 1/2/3/5201/6001–6010 + +`web/src/App.tsx:267` `INITIAL_DELIVERABLES` 硬编码 7 份 mock 交付物 `projectId: 1`(doc-001~004、doc-shared-001、doc-private-001/002);`P2_INITIAL`(projectId 2)、`CUSA_INITIAL_DELIVERABLES`(projectId 3)、`NEUROPULSE`(5201)、`TEN_PROJECTS`(6001+)同理。`ALL_INITIAL_DELIVERABLES` 聚合后作为 `loadDeliverables()` 的默认值(App.tsx:621),并经 `saveDeliverables` **写入 localStorage 持久化**。 + +### 2. 项目过滤按数字 ID 匹配,新建项目与 mock 项目 ID 撞车 + +`App.tsx:1014`:`currentProjectDeliverables = deliverables.filter(doc => doc.projectId === currentProjectId)`。 + +- test 库为全新库,用户新建的第一个项目得到 `id=1` → 与 mock 的 `projectId: 1` **精确撞车**,7 份幻影文档全部"归属"该项目; +- 新建项目时 `createProjectTemplateDeliverables`(App.tsx:3844)再克隆 7 份模板骨架(`doc-p1-*`,`lifecycleState: 'template'`,合规)→ 7 mock + 4~7 模板/私有,构成用户看到的 11 份; +- **dev 库同样中招**:`agent_system_dev.projects` 存在真实项目 id 1–8(智能微创内窥镜/测试内窥镜系统等),id 1/2/3 的真实项目同样被 mock 文档污染。 + +### 3. mock 正文与 mock 用户/权限无环境门控,已进生产构建产物 + +- `mockDocuments.ts` 无任何 `import.meta.env` 门控;`App.tsx` 中 `MOCK_USERS`(含伪造 `projectPermissions: {doc-001: approver...}`)作为 `loadUsers()` 默认值与 `currentUser` 初始身份兜底(App.tsx:610/617); +- **构建产物已验证污染**:`web/dist/assets/index-DogUjae5.js` 中 grep 命中「4K超高清荧光」字符串——dev/test/main 三环境共用同一前端构建,main 生产环境同样受影响。 + +### 4. 既有防线只挡"正文请求",不挡"清单展示" + +团队已知悉 legacy mock ID(`revisionToolbarMount.ts:24 isLegacyLocalMockDocId`:`doc-\d{3}` / `doc-shared-*` / `doc-private-*` / `doc-cusa-*`),并禁止它们发起 content/revision 请求(`controlledDocHydrate.ts:shouldLoadControlledContent` 排除)。但这只防住了「幻影文档冒充服务端正文」,**没有防住幻影文档出现在交付物清单、生命周期徽章(终审已生效)、职责栏与项目文档统计中**——用户看到的正是后者。 + +### 5. 服务端权威清单未做前端对账 + +`GET /api/v1/projects/{id}/documents`(`docgov/project_docs.go`,`WHERE project_id=$1 AND task_key<>''`,服务端已正确按项目隔离)返回空数组时,`mergeWorkflowDocumentsFromServer`(`deliverableSync.ts:41`)**直接 `return prev`,不删除本地多余项**——本地幻影文档无任何下架机制。 + +## 严重度 + +`critical`: + +1. **治理完整性破坏**:空项目展示「终审已生效」受控文档 + 三级法定职责人,在医疗器械合规语境下(ISO 13485 / 21 CFR Part 11 电子记录可靠性)构成"记录失实"; +2. **跨环境广泛影响**:dev 真实项目 id 1–8、test 新建项目、main 生产同构建产物全部受影响; +3. **权限视图失真**:伪造职责人/权限映射混入 RBAC 过滤链,用户对"谁可审批什么"产生错误认知; +4. 既有防线(isLegacyLocalMockDocId)证明团队已两次踩坑(本审计为同域第三类症状),说明 mock 数据保留策略系统性不足。 + +## 关联整改 + +- Remediation: [2026-09-15-mock-deliverables-pollution-remediation.md](../remediation/2026-09-15-mock-deliverables-pollution-remediation.md) + +## 证据 + +- test 库(`shuimu-system-pg-test`,127.0.0.1:5434):`projects` 仅 id=1「测试项目」(通用医疗器械);`document_records` 0 行;全库 87 张业务表除 projects/users/auth_sessions/chat_sessions/project_workflows 外全部为空 +- dev 库(`shuimu-context-pg-dev`,127.0.0.1:5433):真实项目 id 1–8(智能微创内窥镜 / 测试内窥镜系统)→ 与 mock projectId 1/2/3 撞车 +- `web/src/App.tsx:267-498`(INITIAL_DELIVERABLES/P2/CUSA/ALL_INITIAL_DELIVERABLES)、`621`(loadDeliverables)、`1014`(projectId 过滤)、`3844`(新建克隆模板) +- `web/src/data/mockDocuments.ts:32-505`(mock 文档全文与伪造 lifecycleState/职责人)、`web/src/data/seeds/neuropulseSeed.json`(projectId 5201)、`tenProjectsSeed.json`(6001–6010) +- `web/dist/assets/index-DogUjae5.js`:grep 命中「4K超高清荧光」(生产构建含 mock 数据) +- `web/src/features/documentRevision/revisionToolbarMount.ts:24-32`(isLegacyLocalMockDocId 既有防线) +- `web/src/lib/deliverableSync.ts:41-45`(serverDocs 空时不回收本地项) +- `web/src/components/doc/DeckDrawer.tsx:87-103`(approved → 「终审已生效」渲染) +- `web/src/api/projectDocuments.ts:46-58`(服务端清单 API,服务端隔离正确) + +## 开放问题 + +- mock 数据的保留意图(demo 演示模式?)需要在整改方案中决策:彻底移除 vs 环境门控(`import.meta.env.MODE` / 构建期注入 `VITE_ENABLE_DEMO_SEEDS`) +- localStorage 中已被 `saveDeliverables` 持久化的 mock 项,需要一次性迁移清理(对齐既有 `dropRecordKeys`/ControlledInvalidationPlan 机制) +- main 环境是否已有真实用户看过幻影文档(`document_access_audit` 为 0 行,无服务端读取痕迹,风险主要在前端展示层) diff --git a/.doc/audit/2026-09-16-demo-seed-pollution-systemic.md b/.doc/audit/2026-09-16-demo-seed-pollution-systemic.md index 56e4d585..022c2579 100644 --- a/.doc/audit/2026-09-16-demo-seed-pollution-systemic.md +++ b/.doc/audit/2026-09-16-demo-seed-pollution-systemic.md @@ -8,6 +8,10 @@ owner: Cursor-Auto # Demo / 测试数据污染系统性审计 +## 前序 + +本审计在 [2026-09-15 mock 交付物污染真实项目](./2026-09-15-mock-deliverables-pollute-real-projects.md)(及对应 [整改](../remediation/2026-09-15-mock-deliverables-pollution-remediation.md))之上收敛为全栈系统性排查:前端门控、goose 演示项目/会话、stub API。 + ## 背景 空项目展示「终审已生效」受控文档 + 伪造三级职责人。初判怀疑 docgov 隔离失败;实锤为前端 mock/seed 与 DB goose 演示种子污染。 diff --git a/.doc/ops/short-term/2026-09-16-demo-seed-pollution.md b/.doc/ops/short-term/2026-09-16-demo-seed-pollution.md index 8280e405..37eda845 100644 --- a/.doc/ops/short-term/2026-09-16-demo-seed-pollution.md +++ b/.doc/ops/short-term/2026-09-16-demo-seed-pollution.md @@ -38,10 +38,14 @@ mutex: web - [x] `check-prod-bundle.sh` PASS - [x] deliverableSync / demoSeedIds / ResolveBackend 单测 PASS -- [ ] 合入后共享环境 migrate + 人工空项目抽检 +- [x] `cd web && npm test` 全绿(审核 P0) +- [x] 00046 含 chat_sessions retire + ListChatSessions 过滤(审核 P1) +- [ ] 合入后共享环境 migrate + 人工空项目/会话抽检 ## 相关链接 - Audit: [.doc/audit/2026-09-16-demo-seed-pollution-systemic.md](../audit/2026-09-16-demo-seed-pollution-systemic.md) +- 前序 Audit: [.doc/audit/2026-09-15-mock-deliverables-pollute-real-projects.md](../audit/2026-09-15-mock-deliverables-pollute-real-projects.md) - Remediation: [.doc/remediation/2026-09-16-demo-seed-pollution-remediation.md](../remediation/2026-09-16-demo-seed-pollution-remediation.md) - Test report: [.doc/test-reports/2026-09-16-demo-seed-pollution-test-report.md](../test-reports/2026-09-16-demo-seed-pollution-test-report.md) +- 挑拣:`a91a031` `SEED_DEMO_USERS=false`(自 `fix/clean-test-env-baseline`) diff --git a/.doc/remediation/2026-09-15-mock-deliverables-pollution-remediation.md b/.doc/remediation/2026-09-15-mock-deliverables-pollution-remediation.md new file mode 100644 index 00000000..c22c7b21 --- /dev/null +++ b/.doc/remediation/2026-09-15-mock-deliverables-pollution-remediation.md @@ -0,0 +1,47 @@ +--- +title: "mock 交付物污染真实项目整改方案(demo 数据环境门控 + 服务端清单对账回收)" +status: planned +created: 2026-09-15 +updated: 2026-09-15 +--- + +# mock 交付物污染真实项目整改方案(demo 数据环境门控 + 服务端清单对账回收) + +## 背景 + +[Audit 2026-09-15](../audit/2026-09-15-mock-deliverables-pollute-real-projects.md):前端硬编码 mock 交付物(projectId 1/2/3/5201/6001+)与真实项目数字 ID 撞车,空项目展示「终审已生效」幻影受控文档与伪造法定职责人;mock 数据无环境门控且已进入生产构建产物;服务端权威清单为空时前端不回收本地多余项。 + +## 目标 + +1. 任何真实(服务端注册)项目的工作台**只显示**服务端 `document_records` + 该项目新建模板克隆项,永不出现 mock 幻影文档; +2. mock/demo 数据仅在显式开启的 demo 构建中生效(构建期注入,默认关闭); +3. 生产构建产物中不含 mock 文档正文与 mock 用户权限; +4. localStorage 已持久化的 mock 项在升级后一次性回收; +5. 前端对服务端清单做**双向对账**:服务端为权威,本地多余受控项下架(复用 ControlledInvalidationPlan/dropRecordKeys 机制)。 + +## 分波次计划 + +| 波次 | 内容 | 验收 | +|------|------|------| +| Wave 0 止血(低风险) | 1) `mergeWorkflowDocumentsFromServer` 增加对账回收:`category=workflow` 且 `isLegacyLocalMockDocId(docId)` 的本地项,在服务端清单返回(含空数组)时一律 drop(走 `dropRecordKeys` + 受控失效计划既有管道);2) 新建项目克隆模板的 docId 命名空间(`doc-p{projectId}-*`)与 mock 命名空间隔离校验测试 | test 环境新建项目后清单只剩 `doc-p1-*` 模板项;单测覆盖空清单回收 | +| Wave 1 demo 门控 | 1) 所有 demo 种子(mockDocuments/neuropulse/tenProjects/MOCK_USERS/MOCK 权限)收口到 `web/src/data/seeds/` 单一入口,导出统一 `DEMO_SEEDS`;2) 入口处 `if (!import.meta.env.VITE_ENABLE_DEMO_SEEDS) return EMPTY`(构建期常量,生产默认 false);3) `npm run build` 产物 grep 验证不含「4K超高清荧光」等特征串(`VITE_ENABLE_DEMO_SEEDS= npm run build`);4) demo 构建单独脚本 `build:demo` | 产物 grep 断言进 CI/前端测试;demo 模式显式 opt-in | +| Wave 2 存量清理 | 1) `loadDeliverables` 加载时一次性迁移:剔除命中 `isLegacyLocalMockDocId` 的持久化项(版本化 storage key bump);2) `loadUsers`/`loadProjects` 同理剔除 mock 用户/mock 项目(保留真实同步数据);3) 迁移写入 `storage.ts` 版本号,幂等可重放 | 升级后 localStorage 无 mock 残留;重复刷新幂等 | +| Wave 3 UI 语义防线 | 1) `DeckDrawer`/`DocGovActionBar` 对非服务端注册文档(无 ledger 事实)禁用生命周期徽章/治理按钮,显示「本地示例(不入账本)」占位标签;2) `visibleDeliverables` 统计口径排除 mock ID(防污染项目文档计数) | 空项目不再出现「终审已生效」徽章;组件测试 | +| Wave 4 验收与归档 | 1) test 环境真实新建项目全链路(新建→清单→刷新→重启容器)回归;2) dev 环境 id 1–3 项目抽检;3) `go vet` / `go test -short` / `cd web && npm test && npm run build` 全绿;4) test-report 归档 + gap-matrix 行 | 报告归档 `.doc/test-reports/`;gap-matrix 更新 | + +## 验收标准 + +- [ ] test 新建项目:交付物清单 0 幻影文档,仅 `doc-p{id}-*` 模板项(lifecycleState=template) +- [ ] dev 项目 id 1/2/3:无 doc-001/doc-cusa-* 等幻影项 +- [ ] 生产构建产物 grep 无 mock 特征串;demo 构建仅显式开启时含种子 +- [ ] localStorage 升级迁移后无 mock 残留且幂等 +- [ ] 服务端清单对账:服务端删除/为空时本地对应受控项下架 +- [ ] 幻影文档(若 demo 模式保留)不带「终审已生效」徽章与治理操作 + +## 相关链接 + +- Audit: [../audit/2026-09-15-mock-deliverables-pollute-real-projects.md](../audit/2026-09-15-mock-deliverables-pollute-real-projects.md) +- Short-term: [../ops/short-term/2026-09-16-clean-test-env-baseline.md](../ops/short-term/2026-09-16-clean-test-env-baseline.md) +- Test Report: [../test-reports/2026-09-16-clean-test-env-baseline-test-report.md](../test-reports/2026-09-16-clean-test-env-baseline-test-report.md) +- gap-matrix 行: 待补(发布 blocker:治理记录失实 / 跨环境影响) +- 既有同域防线: `web/src/features/documentRevision/revisionToolbarMount.ts`(isLegacyLocalMockDocId)、`web/src/lib/controlledDocHydrate.ts`(shouldLoadControlledContent)、`web/src/lib/deliverableSync.ts`(mergeWorkflowDocumentsFromServer) diff --git a/.doc/remediation/2026-09-16-demo-seed-pollution-remediation.md b/.doc/remediation/2026-09-16-demo-seed-pollution-remediation.md index 3285369f..ef94d43f 100644 --- a/.doc/remediation/2026-09-16-demo-seed-pollution-remediation.md +++ b/.doc/remediation/2026-09-16-demo-seed-pollution-remediation.md @@ -22,7 +22,7 @@ mutex: web | 1 | `VITE_ENABLE_DEMO_SEEDS` + `@demo-seeds` alias + `check-prod-bundle.sh` | done | | 2 | `purgeDemoSeedsFromLocalStorage` 幂等迁移 | done | | 3 | DeckDrawer「本地示例」徽章;职责人默认值清空 | done | -| 4 | goose `00046` retire 6001–6010 + list 过滤 `demo_retired` | done | +| 4 | goose `00046` retire 6001–6010 projects **+ chat_sessions** + list/`ListChatSessions` 过滤 `demo_retired` | done | | 5 | `ResolveBackendInEnv` main/prod 拒 stub | done | | 6 | 审计/整改/报告/gap-matrix | done | @@ -47,3 +47,9 @@ mutex: web ```bash VITE_ENABLE_DEMO_SEEDS=true npm run dev ``` + +## Follow-up(P2,不挡合入) + +- stub 旁路:`resolveTaskBackend` 双重失败返回 `""` 时 squad 回落 `cfg.AgentBackend`;建议 main/prod 启动 fail-fast 校验 `AGENT_BACKEND≠stub` +- `purgeDemoSeedsFromLocalStorage` 对 projectId 1/2/3 无 serverIds 守卫,可能清掉 test 环境真实本地会话缓存;长期改为按 `sess-neuropulse-*` / `sess-wangjing-*` 前缀或注释权衡 +- 姊妹分支 `fix/clean-test-env-baseline` 重叠前端清理已放弃;已挑拣 `a91a031`(`SEED_DEMO_USERS=false`)与 2026-09-15 审计回链 diff --git a/.doc/remediation/gap-matrix.md b/.doc/remediation/gap-matrix.md index dd9b44a4..d853f85a 100644 --- a/.doc/remediation/gap-matrix.md +++ b/.doc/remediation/gap-matrix.md @@ -41,7 +41,7 @@ | DB-USERS-SEQ | 种子 users_id_seq 冷启动冲突 | PASS | `00025_...-sync-seed-sequences.sql`;dev/test setval | | DATA-LIFECYCLE-DEV | completed 实例对应 active 项目 | PASS | dev dry-run 47 → fix SQL → 0 | | DATA-TEST-WF | test 污染 active 工作流定义 | PASS | `retire-nonofficial-workflow-definitions.sql`;test UPDATE 5 | -| DEMO-DB-SEED | goose 00011/00012 演示项目污染列表 | **PARTIAL** | `00046_...-retire-demo-ten-projects.sql` + list 过滤 `demo_retired`;合入后 migrate 待槽位冒烟 | [audit](../audit/2026-09-16-demo-seed-pollution-systemic.md) | +| DEMO-DB-SEED | goose 00011/00012 演示项目污染列表 | **PARTIAL** | `00046` retire projects **+ chat_sessions**;`ListProjects*` / `ListChatSessions` 过滤 `demo_retired`;合入后 migrate 待槽位冒烟 | [audit](../audit/2026-09-16-demo-seed-pollution-systemic.md);前序 [2026-09-15](../audit/2026-09-15-mock-deliverables-pollute-real-projects.md) | | DEMO-STUB-API | main/prod 仍可请求 stub Agent | **PASS** | `ResolveBackendInEnv`;`TestResolveBackendInEnv_RejectsStubInMain` | | DEMO-WEB-SEEDS | 前端 mock/seed 进生产包并撞 projectId | **PASS** | `@demo-seeds` 默认关闭;`check-prod-bundle.sh` PASS;[report](../test-reports/2026-09-16-demo-seed-pollution-test-report.md) | | TEST-ADVISOR-BRIEF | 多意图 AdvisorBrief 丢失 | PASS | `action_planner.go` enrich;`action_planner_test.go` | diff --git a/.doc/test-reports/2026-09-16-demo-seed-pollution-test-report.md b/.doc/test-reports/2026-09-16-demo-seed-pollution-test-report.md index 6f4d8b97..d24600f1 100644 --- a/.doc/test-reports/2026-09-16-demo-seed-pollution-test-report.md +++ b/.doc/test-reports/2026-09-16-demo-seed-pollution-test-report.md @@ -31,12 +31,12 @@ - **判定**: PASS - **耗时**: ~11s(含 rebuild) -### 用例 2:deliverableSync 空清单回收 +### 用例 2:deliverableSync 空清单回收 + demoSeedIds(node:test) -- **输入**: `npx vitest run src/components/chat/deliverableSync.test.ts src/data/demoSeedIds.test.ts` -- **实际输出**: `Test Files 2 passed (2) / Tests 8 passed (8)` +- **输入**: `cd web && npm test`(仓库门禁;含 `demoSeedIds.test.ts` 经 tsx/node:test) +- **实际输出**: `tests 374 / pass 374` + vitest `Test Files 3 passed / Tests 32 passed` - **判定**: PASS -- **耗时**: ~105ms +- **耗时**: ~1.8s ### 用例 3:stub 环境门控 @@ -44,11 +44,18 @@ - **实际输出**: `ok chat-agent-system/internal/taskrun` - **判定**: PASS(含 `TestResolveBackendInEnv_RejectsStubInMain`) +### 用例 4:demo 会话列表过滤 + +- **输入**: `go test ./internal/ctxmgr/ -count=1 -run ListChatSessions_SkipsDemoRetired` +- **实际输出**: `ok` +- **判定**: PASS + ## 六、 自动化测试套件汇总 (Test Suite Execution) ```text -web: vitest deliverableSync + demoSeedIds → 8 PASS -go: ./internal/taskrun → PASS +cd web && npm test → 374 PASS (tsx) + 32 PASS (vitest) +go test ./internal/taskrun → PASS +go test ./internal/ctxmgr -run ListChatSessions_SkipsDemoRetired → PASS scripts/check-prod-bundle.sh → PASS ``` @@ -56,6 +63,6 @@ scripts/check-prod-bundle.sh → PASS ## 七、 测试结论与后续优化建议 (Conclusion & Next Steps) -- 代码层 Wave 0–5 已落地,可作为合并候选。 -- 后续:占集成槽位 migrate `00046`;dev/test 人工新建空项目确认交付物区无伪造 approved。 +- 代码层 Wave 0–5 + 审核 P0/P1(npm test、00046 会话退休、`SEED_DEMO_USERS` 挑拣)已落地,可作为合并候选。 +- 后续:占集成槽位 migrate `00046`;dev/test 人工新建空项目确认交付物区无伪造 approved;抽查 user 1/2/3 会话历史无 `sess-wangjing-*`。 - Demo 评审启用:`VITE_ENABLE_DEMO_SEEDS=true`。