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

261 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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。
```mermaid
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`
- 当前 HEAD`68cee5d 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` 已包含 `commandId``tenantId``ownerUserId``sourceOwner``sourceType``sourceId``sourceRevision``eventType``resourceType``resourceId``payloadSummary``emittedAt`
- `EventsPublishServiceImpl` 先按 `commandId` 回放,再按 source tuple 回放。
- `docs/api-contracts/events/openapi.yaml` 已声明 `notification` 事件,`SSENotificationEvent.data.type` 允许 `source_status_change`
- `EventsPublishServiceImpl``EventsStreamServiceImpl` 均允许 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.reserveCommand``commandId` 做幂等预占。
- 通过 `requireOwnedBlock(userId, workId, blockId)` 校验作品 owner。
- 校验 Block `expectedRevision`
- 更新 `muse_content_block.content_text / word_count / revision`
- 写入 `muse_content_block_source_attribution`,其中 `workId``blockId``revision``sourceType``sourceObjectId``sourceVersion``authorizationSnapshotId``sourceStatus=active`
- 调用 `ContentAuditService.recordSucceededWithAudit` 写入 command result snapshot 与 audit 摘要。
- `muse_content_block_source_attribution` 有独立自增 `id`,并有唯一约束 `(tenant_id, block_id, revision)`
- `WorkDO.ownerUserId` 是作品 owner`saveBlock``userId` 经 owner guard 后与该 owner 一致。
- `BlockSaveReqVO` 要求 `commandId``content``expectedRevision``sourceSnapshot`
## 推断
- P1R-7f 第一切片应选择 `saveBlock`,因为它是 Content 自有 canonical change具备 owner、workId、blockId、revision、commandId、requestHash、source attribution 和审计链路。
- `muse_content_block_source_attribution``muse_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_block``delete_block``splitBlock``mergeBlocks`、导入、解析、导出、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 是作品 owner`WorkDO.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_block``delete_block``splitBlock``mergeBlocks``mergeBlockSuggestion`、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 只允许:
```json
{
"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 table`muse_content_block_source_attribution`
- source tuple`sourceOwner=content``sourceType=content_block_source_attribution``sourceId=blockId``sourceRevision=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 `chunk``chunk` 是 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 变更审批。