merge: fix/controlled-content-canonical-hash follow-up
This commit is contained in:
@@ -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`):
|
||||
|
||||
@@ -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
|
||||
|
||||
## 验收标准
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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<HydrateRevisionState>;
|
||||
},
|
||||
): Promise<DocumentAST | null> {
|
||||
if (isLegacyLocalMockDocId(docId) || !shouldLoadControlledContent({ ...doc, docId })) {
|
||||
return null;
|
||||
}
|
||||
const key = `${docId}::${controlledHydrateFingerprint(doc)}`;
|
||||
const existing = inflightHydrates.get(key);
|
||||
if (existing) return existing;
|
||||
|
||||
Reference in New Issue
Block a user