From f693e1c4746d48c4985566aa2ad11ca61cbfc9a0 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 14 Sep 2026 02:56:35 +0000 Subject: [PATCH] fix(web): stop hydrating local legacy mock documents Login-seeded doc-001 is not server-backed; align shouldLoad with revisionToolbarMountMode so content and revision-state are never requested. Co-authored-by: Cursor --- ...09-14-controlled-content-canonical-hash.md | 3 + ...09-14-controlled-content-canonical-hash.md | 1 + .doc/status/FEATURE-MATRIX.md | 2 +- web/src/lib/controlledDocHydrate.test.ts | 124 ++++++++++++++++++ web/src/lib/controlledDocHydrate.ts | 15 ++- 5 files changed, 142 insertions(+), 3 deletions(-) diff --git a/.doc/ops/handoffs/2026-09-14-controlled-content-canonical-hash.md b/.doc/ops/handoffs/2026-09-14-controlled-content-canonical-hash.md index a8072d19..a4b9862b 100644 --- a/.doc/ops/handoffs/2026-09-14-controlled-content-canonical-hash.md +++ b/.doc/ops/handoffs/2026-09-14-controlled-content-canonical-hash.md @@ -24,6 +24,7 @@ status: done - follow-up:apply 同时备份原文件与**旧 head 正文**(`head.md`)及 metadata;唯一目录 + O_EXCL;原子写 rename 后打开父目录 Sync(EINVAL/ENOTSUP/ENOSYS 忽略) - 终审:`--live --environment dev` 必须同时识别 DSN `:5433` 与库名 `agent_system_dev`,且 `--host-data-root` 为 `data/dev`;空 dsnEnv/pathEnv 不放行;stored work_dir 必须在 `--stored-data-root` 下并映射,禁止沿用其他绝对路径 - 工程门禁(worktree):`go vet`、`go test -short`、`web npm test`、`web npm run build` PASS +- UI follow-up:`shouldLoadControlledContent` / `hydrateControlledDocumentContent` 与 `revisionToolbarMountMode` 的 server-backed 资格对齐;`doc-001` 等本地 legacy/mock 不再请求 content 与 revision-state。`doc_proj*` 仍 hydrate;已注册 `doc-free`/freeform 与 template preview 不走受控 content 请求 ## 未完成 @@ -51,6 +52,7 @@ status: done | `internal/fsutil/atomic.go` | rename 后打开父目录 Sync;O_EXCL;不支持的 dir sync 忽略 | | `internal/docrevision/reconcile.go` | 唯一备份目录;回报 output/head/meta 三条路径 | | `cmd/document-content-reconcile/safety.go` | live 正向确认 dev DSN 与 `data/dev` | +| `web/src/lib/controlledDocHydrate.ts` | 本地 legacy/mock 不 hydrate;与 toolbar 资格一致 | | `internal/docgov/engine.go` | Mark 预加载/幂等补偿 | | `cmd/document-content-reconcile/` | 默认拒绝共享;live 硬门闩 + 路径映射 | | `internal/server/project_doc_handlers.go` | drift 返回 `DOCUMENT_CONTENT_PROJECTION_DRIFT` | @@ -63,6 +65,7 @@ go test -short ./... go test ./internal/docrevision/ ./internal/docgov/ ./internal/server/ ./internal/db/ ./cmd/document-content-reconcile/ \ -run 'TestCanonicalize|TestClassify|TestReconcile|TestCommitMigrationRepair|TestMapStored|TestInspectProjection|TestMigration032|TestReadDocumentContent_Typed|TestWriteControlledContentError|TestServeLedgerHead|TestValidateCLI' cd web && npm test && npm run build +# 重点:controlledDocHydrate 拒绝 doc-001 / template / registered freeform 发 content+revision ``` 隔离 PG(禁止 5433/5434/5435 与 `agent_system_dev|test|main`): diff --git a/.doc/ops/short-term/2026-09-14-controlled-content-canonical-hash.md b/.doc/ops/short-term/2026-09-14-controlled-content-canonical-hash.md index d2df74f1..bcb7468b 100644 --- a/.doc/ops/short-term/2026-09-14-controlled-content-canonical-hash.md +++ b/.doc/ops/short-term/2026-09-14-controlled-content-canonical-hash.md @@ -40,6 +40,7 @@ mutex: docgov 6. [x] 合入前审查 blockers:00032 / 修订号 / live CLI / HashUpdater 预加载 / 读路径 drift 冲突 / 隔离 PG 状态 7. [x] follow-up:旧 head 正文备份、备份不可覆盖、原子写目录 fsync;单提交后 done 8. [x] 终审 3 项:live 正向确认 :5433+agent_system_dev+data/dev;备份三件套+唯一目录/O_EXCL+全路径;rename 后目录 Sync 兼容处理 +9. [x] UI clean run:doc-001 等本地 legacy 不发 content/revision;hydrate 资格对齐 toolbar server-backed ## 验收标准 diff --git a/.doc/status/FEATURE-MATRIX.md b/.doc/status/FEATURE-MATRIX.md index e4d4428c..b323cc7b 100644 --- a/.doc/status/FEATURE-MATRIX.md +++ b/.doc/status/FEATURE-MATRIX.md @@ -21,7 +21,7 @@ | 工作流治理 | 三正式工作流 + 阶段门禁 | done | [remediation/three-workflows](../remediation/2026-09-12-three-workflows-strict-governance-remediation.md);[test 2026-09-12](../test-reports/2026-09-12-three-workflows-governance-remediation-test-report.md) | Phase2 设计器 partial | `workflow` | | 纯软件项目自举 | 软件项目创建、Bootstrap 基线、Delivery 迭代与受控工程交接 | planned | [详细设计](../system-design/2026-09-14-software-project-bootstrap-workflow.md);[现状调研](../ops/research/2026-09-14-software-project-bootstrap-current-state.md);[验收计划](../ops/checklists/2026-09-14-software-project-bootstrap-acceptance.md) | 当前仅完成方案;目录/模板/执行器/创建向导/真实 PI UI 均未实施。受控代码执行单列后续 Epic | `workflow-design` | | IAM / 鉴权 | 项目级 API 鉴权 + RBAC | partial | [audit/authorization](../audit/authorization-matrix-v2.md);[刷新恢复专项审计](../audit/2026-09-13-chat-history-refresh-disappears.md) | 历史读取端点补统一会话 owner + 阶段授权;旧 API 410 标注(BUG-11 partial) | `authz` | -| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | partial | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[项目 143 正文 404 审计](../audit/2026-09-14-project143-controlled-content-404.md);[整改](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md);[handoff](../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md) | 生命周期/租约保留。W1–W6 + 审查修补(00032、live CLI、drift 冲突)工程门禁已在 `fix/controlled-content-canonical-hash` 通过。隔离 PG apply、合 dev、143 apply、真实 UI 未做,不得标 done / PASS | `docgov` | +| 文档治理 | 法定文档生命周期 + 租约 + 受控正文读取 | partial | [test docgov](../test-reports/2026-09-08-docgov-lifecycle-and-claiming-framework-test-report.md);[项目 143 正文 404 审计](../audit/2026-09-14-project143-controlled-content-404.md);[整改](../remediation/2026-09-14-controlled-content-canonical-hash-remediation.md);[handoff](../ops/handoffs/2026-09-14-controlled-content-canonical-hash.md) | 生命周期/租约保留。W1–W6 + 审查修补 + UI hydrate 资格(本地 doc-001 不发 content/revision)工程门禁已在 `fix/controlled-content-canonical-hash` 通过。隔离 PG apply、合 dev、143 apply、真实 UI 未做,不得标 done / PASS | `docgov` | | 文档版本与修改认知 | 智能体版本边界、人工修订标记、任意版本比较、变更报告与负责人通知 | partial | [管理测试](../test-reports/2026-09-13-document-revision-management-test-report.md);[运行时 UI Round20](../test-reports/2026-09-14-document-revision-runtime-ui-test-report.md);[运行时审计](../audit/2026-09-13-document-revision-runtime-404.md);[ADR](../adr/2026-09-13-document-version-boundary.md) | **运行时 UI PASS**(6159 / V3/r2)。仍 `partial`:change report=`fallback`(真实 diff+lineage,非完整 PI 叙事)。系统级 `CHAT-INTENT` 见 gap-matrix | `docgov` | | 前端工作台 | 三栏 + 块文档 + SSE | partial | [test workbench](../test-reports/2026-09-08-three-column-workbench-and-block-document-test-report.md);[现场专项审计](../audit/2026-09-13-conversation-intent-runtime-state.md) | 错误卡片/任务进度/文档 busy 状态刷新恢复与人工抽检待完成 | `web` | | PI Runner / 配置链 | pi-agent + MiniMax 真实链路 | done | [test piagent](../test-reports/2026-09-13-codex-audit-p0-remediation-test-report.md);[container-env](../test-reports/2026-09-13-container-env-configuration-test-report.md) | — | `pi-agent` | diff --git a/web/src/lib/controlledDocHydrate.test.ts b/web/src/lib/controlledDocHydrate.test.ts index 7321f223..7cfd3bb2 100644 --- a/web/src/lib/controlledDocHydrate.test.ts +++ b/web/src/lib/controlledDocHydrate.test.ts @@ -7,6 +7,10 @@ import { resolveCanonicalDocId, shouldLoadControlledContent, } from './controlledDocHydrate'; +import { + revisionToolbarMountMode, + shouldMountDocumentRevisionToolbar, +} from '../features/documentRevision/revisionToolbarMount'; import { resolvePatchEtag } from './revisionEtag'; import { createEditorHydrationGate } from './editorHydrationGate'; import type { DeliverableItem } from '../types/workbench'; @@ -37,6 +41,126 @@ describe('controlledDocHydrate', () => { assert.equal(shouldLoadControlledContent({ ...base, isTemplateOnly: true }), false); }); + it('shouldLoadControlledContent rejects local legacy/mock ids like doc-001', () => { + const local = { ...base, docId: 'doc-001', projectId: 1, taskKey: 'feasibility_reg' }; + assert.equal(shouldLoadControlledContent(local), false); + assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-002' }), false); + assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-shared-1' }), false); + assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-private-01' }), false); + assert.equal(shouldLoadControlledContent({ ...base, docId: 'doc-cusa-feasibility-reg' }), false); + }); + + it('shouldLoadControlledContent keeps canonical, freeform-off, and template preview rules', () => { + assert.equal(shouldLoadControlledContent(base), true); + assert.equal( + shouldLoadControlledContent({ + ...base, + docId: 'doc-p143-intake', + isTemplateOnly: true, + taskKey: 'intake', + }), + false, + ); + assert.equal( + shouldLoadControlledContent({ + ...base, + docId: 'doc-free-1789263145197-2', + governanceMode: 'freeform', + taskKey: 'freeform_doc', + isFixedWorkflow: false, + }), + false, + ); + assert.equal( + shouldLoadControlledContent({ + ...base, + docId: 'doc-upload-1789263145197-0', + sourceType: 'uploaded_file', + }), + false, + ); + }); + + it('hydrateControlledDocumentContent never requests content/revision for doc-001', async () => { + let contentCalls = 0; + let revisionCalls = 0; + const ast = await hydrateControlledDocumentContent( + 'doc-001', + { ...base, docId: 'doc-001' }, + { + fetchContent: async () => { + contentCalls += 1; + throw new Error('must not fetch content for local mock'); + }, + fetchRevisionState: async () => { + revisionCalls += 1; + throw new Error('must not fetch revision-state for local mock'); + }, + }, + ); + assert.equal(ast, null); + assert.equal(contentCalls, 0); + assert.equal(revisionCalls, 0); + }); + + it('hydrateControlledDocumentContent never requests for template preview or registered freeform', async () => { + const forbidden = { + fetchContent: async () => { + throw new Error('must not fetch content'); + }, + fetchRevisionState: async () => { + throw new Error('must not fetch revision-state'); + }, + }; + const template = await hydrateControlledDocumentContent( + 'doc-p143-intake', + { ...base, docId: 'doc-p143-intake', isTemplateOnly: true, taskKey: 'intake' }, + forbidden, + ); + const freeform = await hydrateControlledDocumentContent( + 'doc-free-1789263145197-2', + { + ...base, + docId: 'doc-free-1789263145197-2', + governanceMode: 'freeform', + taskKey: 'freeform_doc', + isFixedWorkflow: false, + }, + forbidden, + ); + assert.equal(template, null); + assert.equal(freeform, null); + }); + + it('shouldLoadControlledContent stays inside toolbar server-backed eligibility', () => { + const samples: DeliverableItem[] = [ + base, + { ...base, docId: 'doc-001', projectId: 1, taskKey: 'feasibility_reg' }, + { ...base, docId: 'doc-p143-intake', isTemplateOnly: true, taskKey: 'intake' }, + { + ...base, + docId: 'doc-free-1789263145197-2', + governanceMode: 'freeform', + taskKey: 'freeform_doc', + isFixedWorkflow: false, + }, + { ...base, docId: 'doc-upload-1789263145197-0', sourceType: 'uploaded_file' }, + ]; + for (const doc of samples) { + const load = shouldLoadControlledContent(doc); + const mount = shouldMountDocumentRevisionToolbar(doc); + if (load) { + assert.equal(mount, true, `${doc.docId} loaded without toolbar eligibility`); + assert.equal(revisionToolbarMountMode(doc), 'toolbar'); + } + if (doc.docId === 'doc-001') { + assert.equal(load, false); + assert.equal(mount, false); + assert.equal(revisionToolbarMountMode(doc), 'local_hint'); + } + } + }); + it('resolveCanonicalDocId maps doc-p56-intake to doc_proj56_intake', () => { assert.equal(resolveCanonicalDocId('doc-p56-intake', [base]), 'doc_proj56_intake'); }); diff --git a/web/src/lib/controlledDocHydrate.ts b/web/src/lib/controlledDocHydrate.ts index fead442d..857647d7 100644 --- a/web/src/lib/controlledDocHydrate.ts +++ b/web/src/lib/controlledDocHydrate.ts @@ -2,6 +2,10 @@ import { fetchControlledDocumentContent } from '../api/projectDocuments'; import { getRevisionState } from '../api/documentRevision'; import { markdownToBlockAST } from '../engine/markdownToBlock'; import { applyHeadBlockMap } from '../features/documentRevision/overlayMatch'; +import { + isLegacyLocalMockDocId, + shouldMountDocumentRevisionToolbar, +} from '../features/documentRevision/revisionToolbarMount'; import type { RevisionState } from '../features/documentRevision/types'; import type { DocumentAST } from '../types/block'; import type { DeliverableItem } from '../types/workbench'; @@ -20,9 +24,13 @@ export function isControlledWorkflowDeliverable(doc: DeliverableItem): boolean { ); } -/** 服务端已有正文、应拉取 output.md 而非展示本地模板 */ +/** 服务端已有正文、应拉取 output.md 而非展示本地模板。 + * 资格必须与 revisionToolbarMountMode 的 server-backed 挂载一致: + * doc-001 等本地 legacy/mock 与未注册 orphan 绝不发 content/revision。 */ export function shouldLoadControlledContent(doc: DeliverableItem): boolean { - return isControlledWorkflowDeliverable(doc) && !doc.isTemplateOnly; + if (!isControlledWorkflowDeliverable(doc) || doc.isTemplateOnly) return false; + if (isLegacyLocalMockDocId(doc.docId)) return false; + return shouldMountDocumentRevisionToolbar(doc); } export function buildControlledLoadErrorSkeleton(doc: DeliverableItem): DocumentAST { @@ -125,6 +133,9 @@ export async function hydrateControlledDocumentContent( fetchRevisionState?: (id: string) => Promise; }, ): Promise { + if (isLegacyLocalMockDocId(docId) || !shouldLoadControlledContent({ ...doc, docId })) { + return null; + } const key = `${docId}::${controlledHydrateFingerprint(doc)}`; const existing = inflightHydrates.get(key); if (existing) return existing;