Files
Agent-System/.doc/audit/2026-09-14-project143-controlled-content-404.md

6.9 KiB
Raw Permalink Blame History

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 调用链:

  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 阻断,且错误码误导为不存在。

关联整改

证据

  • 源码: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 又覆盖真实小队生成、影响/风险报告、高风险确认与拒绝、多文档、上传账本、权限、并发冲突及容器重启后恢复。最终验收见报告。