6.9 KiB
title, status, created, updated, severity
| title | status | created | updated | severity |
|---|---|---|---|---|
| 项目 143 受控文档正文因规范化哈希漂移返回 404 | done | 2026-09-14 | 2026-09-14 | critical |
项目 143 受控文档正文因规范化哈希漂移返回 404
现象
已登录用户打开项目 143 后,页面无法加载 doc_proj143_intake 正文,并周期性出现:
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 调用链:
bootstrapRevisionIfNeeded从 DocGov 文件读出 Markdown;EnsureBootstrap/OpenTemplateVersion调用CanonicalizeMarkdown;- canonicalizer 会去掉每行尾随空格,包括 Markdown 有语义的双空格硬换行;
openTemplatePGLocked将 canonical SHA 写入 snapshot、blob 和document_records.content_sha256;- 该路径没有把 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
- 既有账本审计: 2026-09-13-document-revision-runtime-404.md
- 既有真实 UI 报告: ../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又覆盖真实小队生成、影响/风险报告、高风险确认与拒绝、多文档、上传账本、权限、并发冲突及容器重启后恢复。最终验收见报告。