oh-my-muse/docs/agent-specs/2026-06-08-P1R7fContentSourceOwnerPropagation审阅版.md
zizi acda7a2d06 docs(p1r): 记录 Content 事件传播规格边界
保留 P1R-7f 审阅版、执行版与 review gate 结论,明确本阶段只覆盖 saveBlock source attribution active 链路,不推进 completed。
2026-06-08 23:17:25 +08:00

16 KiB
Raw Blame History

P1R7f Content Source Owner Propagation 审阅版

结论

推荐进入 P1R-7f但第一切片只覆盖 Content 用户保存 Block 后形成的 canonical source attribution 事实,不覆盖全部 Content 写命令、导入、解析、导出、planning、meta projection 或 completed approval。

推荐方案:

  1. 选择 ContentAppServiceImpl.saveBlock 成功后写入的 muse_content_block_source_attribution(source_status=active) 作为 Content 第一批 source fact。
  2. 新增 Content 本域 Events publish outbox / worker调用 EventsPublishApi,发布 Events 已声明的 notification/source_status_change
  3. 不把 Block 正文、sourceSnapshot 原始对象、auditReason、authorizationSnapshotId 或 lineage payload 放入 Events payload。
  4. 不修改 OpenAPI、scanner 或 coverage report不推进 Content / Events / P1R-7 completed。
flowchart LR
    Save["App saveBlock command"] --> Block["muse_content_block<br/>content + revision"]
    Save --> Attr["muse_content_block_source_attribution<br/>blockId + revision + active"]
    Attr --> Outbox["Content events publish outbox<br/>queued / running / retryable / published / dead_letter"]
    Outbox --> Worker["Content worker<br/>claim + retry + stale-claim guard"]
    Worker --> Api["EventsPublishApi<br/>events-api only"]
    Api --> Unified[("muse_unified_event<br/>notification / source_status_change")]
    Unified --> SSE["/app-api/muse/events<br/>work owner visible"]

本审阅版只冻结目标、非目标、取舍、风险和验收标准。执行版、实现和提交必须在本审阅版 fresh spec review + fresh quality / feasibility review 双 PASS 后再继续。

Review Gate

P1R-7f 审阅版已通过 fresh review

  • Harvey spec / scope reviewPASS无 P0/P1/P2已验证本审阅版严格停留在 Content saveBlock -> block source attribution -> Events notification 第一切片,没有把 completed、OpenAPI、coverage 或全部 Content 写命令混入。
  • Chandrasekhar quality / feasibility reviewPASS无 P0/P1/P2已验证 saveBlock -> muse_content_block_source_attribution(source_status=active) -> Content outbox -> EventsPublishApi -> muse_unified_event -> SSE 路径可落地,并确认 Content 不应误用 chunk,执行版必须加入防误用 chunk 的测试或静态 gate。

Halley quality reviewer 长时间超时未返回,已丢弃,不计入 review gate。

审阅版双 PASS 只允许进入执行版编写,不代表 P1R-7f 实现完成,也不代表 Content / Events / P1R-7 completed。执行版完成后仍必须派发 fresh execution spec review + fresh execution quality / feasibility review执行版双 PASS 前不得实现代码、提交、push 或推进 completed。

已验证事实

工作区与状态

  • 正确 worktree/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
  • 分支:dev/1.0.0
  • 当前 HEAD68cee5d test(p1r): 收口 Account 事件传播真实链路门禁
  • 当前分支与 origin/dev/1.0.0 对齐。
  • git pull --ff-only origin dev/1.0.0 返回 Already up to date.
  • 当前 coverage summary
    • completedOperations=100
    • needsVerificationOperations=133
    • incompleteOperations=0
    • genericPersistenceOperations=0
    • ssePlaceholderOperations=0
  • Content 51 operations 当前均为 dedicated / needs_verification
  • 受保护文件 staged / unstaged diff 为空:
    • docs/api-contracts/account/openapi.yaml
    • docs/api-contracts/market/openapi.yaml
    • docs/api-contracts/ai/openapi.yaml
    • docs/api-contracts/knowledge/openapi.yaml
    • docs/api-contracts/events/openapi.yaml
    • muse-cloud/scripts/p1r-audit-api-coverage.py
    • docs/superpowers/reports/p1r-api-coverage.json
    • docs/superpowers/reports/p1r-api-coverage.md

全局 P1R-7 边界

  • P1R-7a 到 P1R-7e 已分别覆盖 Events SSE、AI、Knowledge、Market、Account source owner propagation evidence。
  • P1R-7b / 7c / 7d / 7e 均只把对应真实链路推进到 needs_verification evidence不代表 Events / P1R-7 / 对应业务域 completed。
  • P1R-7 全局审阅版把 P1R-7f 定义为 Content canonical change / block saved / export task event propagation,状态口径仍是只推进 needs_verification 证据。
  • P1R-7 completed approval 预检明确:如果 Account / Content 仍是硬前置,则应继续 P1R-7e Account 与 P1R-7f Content而不是直接 completed。Account 已在 P1R-7e 收口并推送,因此 Content 是剩余 owner 切片。

Events 可复用合同

  • EventsPublishReqDTO 已包含 commandIdtenantIdownerUserIdsourceOwnersourceTypesourceIdsourceRevisioneventTyperesourceTyperesourceIdpayloadSummaryemittedAt
  • EventsPublishServiceImpl 先按 commandId 回放,再按 source tuple 回放。
  • docs/api-contracts/events/openapi.yaml 已声明 notification 事件,SSENotificationEvent.data.type 允许 source_status_change
  • EventsPublishServiceImplEventsStreamServiceImpl 均允许 notification subtype source_status_change
  • Events 当前没有 block_saved notification subtype本阶段不得新增 subtype 或修改 Events OpenAPI。
  • SSEChunkEvent 语义是 AI 生成文本片段,要求 content/sequenceNo。Content block saved 不应使用 chunk,否则会泄露正文或伪造 AI chunk 语义。

Content 当前事实模型

  • Content owner 位于 muse-module-content/muse-module-content-server
  • muse-module-content-server/pom.xml 当前没有依赖 muse-module-events-api,也没有依赖 muse-module-events-server
  • 最新 migration 当前是 V20__extend_account_events_publish_outbox.sql;若 P1R-7f 进入实现且 live 文件列表不变,下一 migration 应为 V21
  • ContentAppServiceImpl.saveBlock 在同一事务内:
    • ContentCommandService.reserveCommandcommandId 做幂等预占。
    • 通过 requireOwnedBlock(userId, workId, blockId) 校验作品 owner。
    • 校验 Block expectedRevision
    • 更新 muse_content_block.content_text / word_count / revision
    • 写入 muse_content_block_source_attribution,其中 workIdblockIdrevisionsourceTypesourceObjectIdsourceVersionauthorizationSnapshotIdsourceStatus=active
    • 调用 ContentAuditService.recordSucceededWithAudit 写入 command result snapshot 与 audit 摘要。
  • muse_content_block_source_attribution 有独立自增 id,并有唯一约束 (tenant_id, block_id, revision)
  • WorkDO.ownerUserId 是作品 ownersaveBlockuserId 经 owner guard 后与该 owner 一致。
  • BlockSaveReqVO 要求 commandIdcontentexpectedRevisionsourceSnapshot

推断

  • P1R-7f 第一切片应选择 saveBlock,因为它是 Content 自有 canonical change具备 owner、workId、blockId、revision、commandId、requestHash、source attribution 和审计链路。
  • muse_content_block_source_attributionmuse_content_block 更适合作为第一批 source fact它表达“该 Block revision 的来源状态 active”可以自然映射到 source_status_change,并避免把正文变化伪装成 AI chunk
  • 复用 notification/source_status_change 是当前不改 OpenAPI 下的最小可行方案;如果未来产品需要明确的 block_saved subtype应另起 Events OpenAPI 合同审批。
  • 不应把 Content 全部写命令放进 P1R-7f 第一切片。create_blockdelete_blocksplitBlockmergeBlocks、导入、解析、导出、planning、meta projection 都有不同语义和不同敏感 payload 边界,混入会扩大 blast radius。
  • muse_content_block_source_attribution 是 source fact不应直接兼任 publish outbox。它没有 claim lease、attempt、retry、published event、dead_letter、last_error 等发布补偿语义。

假设

  • P1R-7f 允许在 Content owner 内新增独立 Events publish outbox 表或等价状态表。
  • P1R-7f 允许 muse-module-content-server 新增对 muse-module-events-api 的直接依赖,但不允许依赖 muse-module-events-server
  • Content block save notification 的 SSE owner 是作品 ownerWorkDO.ownerUserId / 当前 app userId
  • source_status_change 可以表达 Content block source attribution 从用户保存命令进入 active 状态的用户可见摘要。

目标

  1. 证明 Content source owner 的真实 Block 保存事实可以通过 Content 本域 publish outbox / worker 发布到 Events。
  2. 让作品 owner 可以通过统一 SSE 看到自己的 Block 来源状态变更摘要。
  3. 建立 Content 与 Events 的单向依赖证据Content server 依赖 events-api不依赖 events-serverEvents server 不反向依赖 Content server。
  4. 为后续 Content create/delete/split/merge/import/export/planning/meta projection 等事件扩展保留清晰边界。

非目标

  1. 不把 Content 51 operations 从 needs_verification 推进到 completed
  2. 不把 Events streamEvents 或总 P1R-7 推进到 completed
  3. 不修改 Content / Events OpenAPI。
  4. 不修改 coverage scanner 或 coverage report。
  5. 不发布 Block 正文、完整 sourceSnapshot、lineage payload、license restriction、authorizationSnapshotId、auditReason、requestHash、command result snapshot 或错误堆栈。
  6. 不覆盖 create_blockdelete_blocksplitBlockmergeBlocksmergeBlockSuggestion、import/parse/export、planning、meta projection、admin risk action。
  7. 不让 Events server 查询 Content 表。
  8. 不把 SSE chunk 用作 Content block save 事件。

推荐方案

事件选择

第一批只允许:

Content source fact 触发入口 Events eventType notification type SSE owner
muse_content_block_source_attribution(source_status=active) ContentAppServiceImpl.saveBlock notification source_status_change WorkDO.ownerUserId

暂不选择:

候选事实 暂不选择原因
muse_content_block 正文变化 直接以正文变化为 SSE payload 有泄露风险;应通过 source attribution 摘要表达
create_block 创建类命令首次执行前 targetId 为空source tuple 和 replay 边界需要单独冻结
delete_block 删除前 source attribution 是 deleted snapshot用户可见语义与保存不同
splitBlock / mergeBlocks 一次命令可能影响多个 Block 和章节 revision第一切片不扩大结构编辑语义
import / parse / export task 异步任务状态和文件凭证边界不同,容易误暴露内部任务或下载信息
planning / meta projection 更接近 AI / Meta 协作投影,需单独定义用户可见摘要
admin risk action 属于治理动作,已有 Market governance 类似模式但 Content 侧需单独审阅

Payload allowlist

第一批 payload 只允许:

{
  "type": "source_status_change",
  "message": "内容来源状态已更新",
  "resourceRef": {
    "resourceType": "content_block",
    "resourceId": 8001
  },
  "timestamp": "2026-06-08T12:00:00"
}

执行版可以把 message 收窄为安全固定枚举,例如:

  • 内容来源状态已更新
  • Block 来源状态已更新

不得把以下字段放入 Events payload

  • Block 正文 contentText
  • 请求正文 content
  • sourceSnapshot 原始对象
  • sourceObjectId
  • authorizationSnapshotId
  • lineagePayload
  • licenseRestrictionSnapshot
  • auditReason
  • requestHash
  • resultSnapshot
  • 错误堆栈

Outbox 边界

推荐新增独立 Content Events publish outbox

  • source tablemuse_content_block_source_attribution
  • source tuplesourceOwner=contentsourceType=content_block_source_attributionsourceId=blockIdsourceRevision=blockRevision
  • commandId稳定短幂等键建议 content_evt:<sha256(tenantId|blockId|revision|source_status_change)>
  • 唯一约束:建议覆盖 (tenant_id, block_id, block_revision, notification_type),避免同一 Block revision 重复发布。
  • 状态机:queued/running/retryable/published/dead_letter
  • claim必须原子领取具备 lease / retry / attempt ownership。
  • 终态回写:必须绑定本次 claimed attempt避免 stale worker 覆盖新 claim。
  • worker 默认关闭,配置启用后才 claim。
  • invalid owner、invalid payload 或 missing attribution 必须 fail-closed不得发布给 owner 0 或其他用户。

依赖边界

  • Content server 可以依赖 muse-module-events-api
  • Content server 不得依赖 muse-module-events-server
  • Events server 不得依赖 Content server。
  • muse-server 作为装配层可以同时包含 Content server 与 Events server。

关键取舍

  • 选择 saveBlock,不选择全部 Content 写命令:第一切片需要最小真实链路,避免把结构编辑、任务状态和治理语义混在一起。
  • 选择 block source attribution不选择正文本身source attribution 是已落库、可审计、可脱敏的 source fact正文不应进入统一 SSE payload。
  • 选择 notification/source_status_change,不新增 block_saved:当前 Events 合同已声明该 subtype新增 subtype 必须另起 OpenAPI 审批。
  • 不使用 SSE chunkchunk 是 AI 生成文本片段语义Content 保存正文不应伪造为 AI chunk也不应发布正文片段。
  • 不复用 attribution 表作为 publish queuesource fact 与 publish compensation 是两个职责,混用会缺 claim、retry 和 dead_letter 证据。

影响范围

  • 直接影响:
    • Content saveBlock source attribution 发布链路。
    • Content server 依赖 events-api。
    • Content 本域 outbox / worker / migration / focused tests。
    • Events unified event focused E2E。
  • 间接影响:
    • P1R-7 completed approval 的 source owner evidence 更完整。
  • 不影响:
    • OpenAPI 合同。
    • coverage scanner / coverage report。
    • Content 51 operations completion status。
    • Content saveBlock 同步返回语义。
    • Import / parse / export / planning / meta projection / admin governance 业务逻辑。

风险与兼容性

  • 语义风险:source_status_change 原先主要用于 Knowledge执行版必须说明 Content 使用该 subtype 的语义边界,不能把它扩展成万能通知。
  • 事件重复风险:同一 (blockId, revision) retry 不能创建多条 unified event必须用 commandId 与 source tuple 双重幂等。
  • 敏感信息风险正文、sourceSnapshot 原文、authorizationSnapshotId、lineage、license restriction 和 auditReason 不得进入 payload。
  • owner 风险SSE owner 必须是作品 owner不能由 path 参数或 payload 自带 owner 推断。
  • 并发风险:saveBlock 已有 revision guardoutbox 创建必须与成功 attribution 写入同事务,且不能在 command replay 分支重复写 outbox。
  • 可达性风险:如果实现时发现 sourceAttributionMapper.insert 后无法可靠拿到 attribution id应以 (blockId, revision) 作为 source tuple不为拿 id 改动既有 mapper 行为。
  • 兼容性:新增 outbox 与 worker 默认关闭,不改变现有 Content API 同步返回语义。

验收标准

审阅版通过后,执行版必须包含:

  1. Content outbox DO / Mapper / Service / Worker / 配置 / V21 migration 的文件边界。
  2. ContentAppServiceImpl.saveBlock 在 Block 更新和 attribution 写入后同事务创建 outbox 的事务边界。
  3. payload exact allowlist 与敏感字段拒绝策略。
  4. worker success / duplicate / retryable / CommonResult error / rejected / blocked / dead_letter / stale claim tests。
  5. Content saveBlock source fact -> Content outbox -> worker -> EventsPublishApi -> muse_unified_event -> SSE visible focused E2E。
  6. dependency gateContent server 有 events-api、无 events-serverEvents server 无 AI / Knowledge / Market / Member / Content server。
  7. V21 SQL 静态 gate 与 Flyway _test
  8. P1R mixed gates 继续确认 AI / Knowledge / Market / Account source owner gates 不回退Content 仍为 dedicated / needs_verification
  9. coverage scanner 只在 /tmp 隔离副本运行,真实 worktree protected diff 为空。
  10. git diff --check 和 protected staged / unstaged diff gate。

待确认项

  1. 是否接受 P1R-7f 第一切片只覆盖 saveBlock,其他 Content 写命令进入后续切片。
  2. 是否接受在不改 OpenAPI 的前提下复用 notification/source_status_change 表达 Content block source attribution active。
  3. 如果 reviewer 认为 source_status_change 语义不适配 Content是否先停止 P1R-7f 实现,改走 Events OpenAPI subtype 变更审批。