Files
Agent-System-Dev/docs/superpowers/specs/2026-10-16-document-visualization-read-model-contract.md
T
weichen d58d5bc1dd Review design docs and freeze cockpit contract decisions
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
2026-10-16 06:25:19 +00:00

8.9 KiB
Raw Blame History

文档驾驶舱聚合读取模型与接口契约 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}/sources
  • GET .../nodes/{kind}/{id}/history
  • GET .../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 检查;本契约不新增绕过入口。