--- title: "项目 143 受控文档正文因规范化哈希漂移返回 404" status: done created: 2026-09-14 updated: 2026-09-14 severity: critical --- # 项目 143 受控文档正文因规范化哈希漂移返回 404 ## 现象 已登录用户打开项目 143 后,页面无法加载 `doc_proj143_intake` 正文,并周期性出现: ```text GET /api/v1/docgov/documents/doc_proj143_intake/content 404 自动拉取受控文档正文失败: fetch controlled document content failed: 404 ``` 本轮未操作用户浏览器,也未执行真实 PI 或页面验收;上述 UI 现象来自用户现场。诊断仅对当前 dev 源码、PostgreSQL、容器挂载文件和既有真实 UI 证据做只读核对。 ## 根因分析 ### R1 — 文件存在,404 实际是哈希完整性冲突(直接原因) 当前 dev 数据链如下: | 项 | 值 | |---|---| | project | `143`,active,workflow=`main` | | document | `doc_proj143_intake`,非模板、drafting | | work_dir | `/app/data/workspaces/bizdev/intake_1789270446856290128` | | output.md | 存在,38,581 bytes | | 文件原始 SHA-256 | `c97d749728bf5607a9117c29e45c0a165353f5bb8ba65532f21bda87232a988b` | | `document_records.content_sha256` | `7e3c4954de0bd225ccde0c8494784d4275e7f29a517510f6a286ec004c35d490` | | ledger head snapshot SHA-256 | 同上 `7e3c...d490`,38,571 bytes | `output.md` 有 5 行各保留 2 个 Markdown 尾随空格,共多 10 bytes。逐行去掉行尾空白后,文件 SHA-256 **精确等于**数据库记录和账本 head 的 `7e3c...d490`。这证明漂移由当前 canonicalizer 的行尾裁剪造成,而不是文件缺失;但 Markdown 的两个尾随空格表示硬换行,具有渲染语义,不能把两侧判为语义等价,也不能直接用账本 blob 覆盖原文件。 `internal/docgov/project_docs.go::ReadDocumentContent` 对文件原始字节计算 SHA,并与 `document_records.content_sha256` 比较;不等即报 `content sha256 mismatch`。`internal/server/project_doc_handlers.go::handleDocGovGetDocumentContent` 又把 `ReadDocumentContent` 的所有错误统一写成 HTTP 404,所以页面看到的是错误的 Not Found。 ### R2 — revision bootstrap 改了数据库哈希,没有同步规范化文件(制造漂移) 项目 143 的 `output.md` 于 2026-09-13 03:42 UTC 由真实小队落盘;账本 snapshot 66 于 10:32 UTC 才由旧文档首次 revision bootstrap 建立。 bootstrap 调用链: 1. `bootstrapRevisionIfNeeded` 从 DocGov 文件读出 Markdown; 2. `EnsureBootstrap` / `OpenTemplateVersion` 调用 `CanonicalizeMarkdown`; 3. canonicalizer 会去掉每行尾随空格,包括 Markdown 有语义的双空格硬换行; 4. `openTemplatePGLocked` 将 canonical SHA 写入 snapshot、blob 和 `document_records.content_sha256`; 5. 该路径没有把 canonical blob 重新投影到 `output.md`。 结果是数据库与账本指向 38,571-byte canonical 内容,兼容文件仍为 38,581-byte raw 内容。旧项目一旦被 bootstrap,后续正文读取稳定失败。 ### R3 — content 与 revision 并发 hydration 使首次行为存在竞态 `hydrateControlledDocumentContent` 用 `Promise.all` 同时请求 content 与 revision-state。对尚未建账本的旧文档: - content 若先读到旧 raw 哈希,可能首次成功; - revision-state 随后 bootstrap 并把记录改成 canonical 哈希; - 后续 content 立即或下一轮稳定 404。 既有真实 UI 资产已经记录过项目 143 的 `revision-state=200`、`content=404`;Round20 的 PASS 使用新项目 6159,不能覆盖项目 143 的旧数据迁移场景。 ### R4 — 5 秒 lease 同步无变化也制造新数组,放大为重复 404 正文 hydration effect 依赖整个 `deliverables`。lease 轮询每 5 秒执行 `prev.map(...)` 并始终返回新数组,即使每个文档的 lease 均未变化。React 因引用变化重跑 hydration effect,持续请求同一已知失败正文。 因此控制台中的多次 404 不是多份正文丢失,而是一个后端一致性错误被前端无效状态更新周期性放大。 ### R5 — 错误协议丢失可诊断信息 - 后端将记录不存在、文件不存在、文件为空、哈希冲突全部映射为 404。 - `fetchControlledDocumentContent` 只抛出 HTTP status,不解析结构化错误码、message 或 correlation id。 - 用户只能看到 `failed: 404`,无法区分可重试网络问题与必须修复的数据冲突。 ## 影响范围 - **已确认受影响**:dev 项目 143 / `doc_proj143_intake`。 - 对当前 dev 中可定位到现存 `output.md` 的 27 个非模板受控记录做只读比对:26 个 raw SHA 一致,只有项目 143 为 mismatch。 - 数据库另有大量测试记录使用临时路径或空 `work_dir`;属于测试数据隔离/清理问题,不据此扩大本缺陷的用户数据范围。 - 任何“旧文件含会被 canonicalizer 改写的内容、首次 bootstrap 只改数据库不投影文件”的文档都具有同类风险;不能仅手工修项目 143 后关闭代码缺陷。 ## 严重度 `critical` — 用户正文实际存在却无法在 UI 查看;编辑、overlay、版本比较等后续能力均被正文 hydration 阻断,且错误码误导为不存在。 ## 关联整改 - Remediation: [../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md) - 既有账本审计: [2026-09-13-document-revision-runtime-404.md](./2026-09-13-document-revision-runtime-404.md) - 既有真实 UI 报告: [../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md) ## 证据 - 源码:`internal/docrevision/canonicalize.go`、`internal/docrevision/pg_mutate.go`、`internal/docgov/project_docs.go`、`internal/server/project_doc_handlers.go` - 前端:`web/src/lib/controlledDocHydrate.ts`、`web/src/App.tsx` - 数据库:`document_records.id=87`、`document_versions.id=59`、`document_snapshots.id=66` - 历史 UI 资产:`.doc/test-reports/assets/2026-09-13-document-revision-runtime-ui/ui01-parent-directory-sync-failure/ui-acceptance-results.json` ## 关闭结果 - Cursor 已完成语义保持的 canonicalization、结构化错误、原子投影、定向 reconciler、legacy/mock 水合隔离和刷新缓存竞态修复;产品代码已合入并部署至 dev。 - 项目 143 已在保全原文件与旧 head 后以显式 `migration_repair` 校正账本,没有把裁剪硬换行的旧 head 静默覆盖回原文件。 - Antigravity 的 `clean-run3` 仅通过真实页面完成登录、正文打开、30 秒静置、人工编辑、三模式切换、硬刷新和退出重登;`doc_proj143_intake` 的 content/revision 目标 404 全程为 0。 - 新项目 `full-chain-final` 又覆盖真实小队生成、影响/风险报告、高风险确认与拒绝、多文档、上传账本、权限、并发冲突及容器重启后恢复。最终验收见[报告](../test-reports/2026-09-14-controlled-content-ui-acceptance.md)。