Why: user approved the four design documents and authorized implementation (/goal); the plan's section-7 tradeoffs gate the I1 public DTO and had to be frozen before any implementation unit starts. A read-only audit of the RDM backend (Agent-System-RDM main 5df66106) supplied the source facts. Evidence: audit findings recorded in rollout section 7 (tree is doc->section only, no confidence field, no persisted conflict entity, per-project event seq complete); review fixes folded into the read-model contract (statReasons, structuralGap, AssertionSummary); precheck receipt READY imported via task-resume. Impact: unlocks I1 implementation in the RDM repo; design deliverable ready for PR/CI/merge; no business code, pins, database or deployment touched. Refs: .task/feature-document-visualization-plan.md
8.9 KiB
文档驾驶舱聚合读取模型与接口契约 r1
状态:待评审;只定义读取契约,不授权实现或迁移。
1. 契约定位
本契约把 RDM 已有树、正文、来源、关系和事件事实投影成一个面向 UI 的只读模型。它不替代 RDM 领域实体,也不允许前端根据多个旧接口自行拼装出第二套事实。所有接口都必须带项目上下文并在服务端完成授权。
2. 公共约定
type NodeKind = 'document' | 'section' | 'subsection' | 'paragraph'
type NodeRef = { kind: NodeKind; id: string; version?: string }
type ViewMode = 'panorama' | 'reader'
type Density = 'compact' | 'full'
type SourceKind = 'conversation' | 'file' | 'web' | 'tool' | 'agent' | 'human' | 'derived'
type ContentState = 'complete' | 'partial' | 'empty' | 'inaccessible' | 'unavailable'
type VerificationState = 'verified' | 'mixed' | 'unverified' | 'disputed' | 'not_applicable'
interface SnapshotMeta {
schemaVersion: 'document-cockpit.v1'
projectId: string
snapshotRevision: string
asOfSeq: number
generatedAt: string
expiresAt?: string
truncated: boolean
truncationReasons: string[]
}
interface TreePage {
meta: SnapshotMeta
items: TreeNode[]
nextCursor?: string
hasMore: boolean
}
projectId 是服务端解析后的项目定位,不是授权令牌。客户端提交的节点 ID、来源 ID 和 cursor 必须在同一项目范围重新解析。
3. 读取资源
3.1 项目概览
GET /api/rdm/projects/{projectId}/document-cockpit/overview
返回项目统计、最近变化和顶层文档节点。统计字段必须区分“确实为 0”和“无法计算”:
interface CockpitOverview {
meta: SnapshotMeta
project: { title: string; visibility: 'member' | 'restricted' }
stats: {
documentCount: number | null
completion: { known: number; total: number; state: 'known' | 'partial' | 'unavailable' }
emptyNodeCount: number | null
unverifiedSourceCount: number | null
conflictCount: number | null
pendingChangeCount: number | null
updatedAt?: string
// 任一统计为 null 时,必须在本表给出该字段名的 unavailableReason
// (如 forbidden_source / projection_lag / upstream_error),见 §6。
statReasons: Record<string, string>
}
documents: TreeNode[]
recentChanges: ChangeSummary[]
}
3.2 树分页
GET /api/rdm/projects/{projectId}/document-cockpit/tree?parent=&cursor=&limit=&filter=
返回 TreePage { meta, items: TreeNode[], nextCursor?, hasMore }。服务端排序稳定为“文档顺序 / 章节顺序 / 最近更新时间”,不得按数据库内部 ID 作为用户可见排序。filter 是服务端解析后的来源、状态、负责人、风险和时间条件,未知过滤值拒绝而不是忽略。
interface TreeNode {
ref: NodeRef
title: string
path: string[]
parent?: NodeRef
childCount: number
state: {
content: ContentState
verification: VerificationState
sourceCount: number
conflictCount: number
impactCount: number
missingChildCount: number
// 现有层级不足以表达契约层级时为 true(v1:文档树只有 document -> section,
// 章节即叶节点;subsection/paragraph 枚举值保留给后续层级扩展)。
structuralGap: boolean
}
lastChangedAt?: string
lastChangedBy?: ActorSummary
visibility: 'full' | 'redacted' | 'metadata_only'
}
3.3 节点详情
GET /api/rdm/projects/{projectId}/document-cockpit/nodes/{kind}/{id}?version=
返回正文和面板摘要。正文必须区分 contentState 与 body 缺失;被权限过滤不能返回空字符串冒充空内容。
interface NodeDetail {
meta: SnapshotMeta
node: TreeNode
body?: ContentBlock[]
parent?: NodeRef
children: TreeNode[]
sources: SourceSummaryPage
history: HistorySummaryPage
impacts: ImpactPage
conflicts: ConflictPage
availableActions: string[]
}
3.4 面板分页
GET .../nodes/{kind}/{id}/sourcesGET .../nodes/{kind}/{id}/historyGET .../nodes/{kind}/{id}/impacts?depth=1&cursor=GET .../nodes/{kind}/{id}/conflicts
每个分页资源返回同一 SnapshotMeta、items、nextCursor、hasMore、truncated 和 redactions。关系查询默认 depth=1、带服务端上限;超过上限只能返回明确的 truncated=true。
4. 内容与来源字段
interface SourceSummary {
ref: { kind: SourceKind; id: string }
label: string
excerpt?: string
locator?: { conversationId?: string; filePath?: string; url?: string; toolName?: string }
capturedAt?: string
verifiedAt?: string
verifiedBy?: ActorSummary
confidence: 'high' | 'medium' | 'low' | 'unknown'
verification: VerificationState
visibility: 'full' | 'redacted' | 'metadata_only'
redactedReason?: 'private' | 'forbidden' | 'expired' | 'not_loaded'
}
来源摘要不是来源原文缓存。excerpt 只有在当前用户拥有对应字段披露权限时返回;否则只返回标签、类型、时间和 redactedReason。
5. 变化与冲突
interface ChangeSummary {
node: NodeRef
title: string
changedAt: string
actor?: ActorSummary
kind: 'created' | 'content_changed' | 'source_added' | 'verification_changed' | 'conflict_opened' | 'conflict_resolved'
summary: string
version?: string
}
interface AssertionSummary {
node: NodeRef
claim: string
sourceRef?: { kind: SourceKind; id: string }
visibility: 'full' | 'redacted' | 'metadata_only'
redactedReason?: 'private' | 'forbidden' | 'expired' | 'not_loaded'
}
interface ConflictSummary {
id: string
node: NodeRef
state: 'open' | 'acknowledged' | 'resolved' | 'superseded'
left: AssertionSummary
right: AssertionSummary
detectedAt: string
resolvedAt?: string
resolutionRef?: string
}
v1 冲突投影(冻结于实施计划 §7.3):领域无持久化冲突实体,适配器只转换现有碰撞检测视图与 supersedes 裁决边;acknowledged 首期不产生,一侧受限时对应 AssertionSummary 降为 redacted 并给出 redactedReason。
历史查询和导出只能使用已提交批次结束序号。读服务不得把正在写入的中间事件投影为“最近变化”。
6. 筛选、排序和缓存
- 筛选表达式必须序列化为稳定哈希
filterHash,并回显在响应中,防止旧筛选响应覆盖当前结果。 ETag/If-None-Match可用于只读缓存,但缓存键必须包含用户可见范围、项目、筛选、版本和字段披露策略。- 允许短时缓存统计和树页;正文、来源和权限变化后必须以事件或 TTL 失效。缓存命中返回
generatedAt和expiresAt。 - 任何
null统计都必须伴随unavailableReason,例如forbidden_source、projection_lag或upstream_error。
7. 错误契约
| 场景 | HTTP/状态 | UI 处理 |
|---|---|---|
| 项目不存在或不可见 | 404(不泄露存在性) | 显示项目不可访问,不重试节点请求 |
| 节点属于其他项目 | 404 | 丢弃本地选择并刷新项目快照 |
| 权限允许元数据但禁止正文 | 200 + metadata_only |
显示标题/状态,正文和原文标为受限 |
| 快照过期 | 200 + expired=true 或 409 |
保留旧内容,显示刷新入口,不静默当当前事实 |
| cursor/过滤器非法 | 400 | 显示可理解的筛选错误,保留上一次有效筛选 |
| 上游读取失败 | 503/领域错误码 | 显示局部不可用和重试;不清空其他成功面板 |
| 关系超限 | 200 + truncated=true |
显示已截断及继续展开的明确上限 |
8. 事件与前端状态
读模型可以通过现有项目事件/SSE 通知失效,事件只携带受影响 projectId、nodeRef、seq 和 reason。前端维护一个异步所有者:
contextKey = projectId + filterHash + selectedNodeRef + viewMode。- 每次请求拥有
requestGeneration;响应不匹配当前 generation 或 contextKey 时丢弃。 - 每个事件流按
seq严格递增应用;旧事件丢弃,缺口触发只读重拉而不是猜测中间状态。 - 任何视图只读取同一快照缓存,不自行维护正文、统计或权限的第二份副本。
9. 版本兼容
响应顶层带 schemaVersion。新增字段向后兼容;状态枚举扩展必须先让旧客户端落入 unknown 安全分支。删除字段或改变字段语义必须提升版本并经过组合验证。契约初版不要求迁移旧 RDM 存储,读取服务通过适配器把既有 rdm.inspect.tree/object/relations/events/history 事实映射到新 DTO。
10. 安全验收
- 同一节点 ID 换项目提交时不得返回另一项目数据。
- 仅有项目管理权而无正文/来源职责时,不得获得被保护原文。
metadata_only、redacted和inaccessible三种状态在接口和 UI 中可区分。- 历史序号位于未提交批次时不得读取半个操作。
- 导出、下载和关系展开沿用相同 scope/ACL 检查;本契约不新增绕过入口。