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

823 lines
41 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 source owner propagation 的第五 owner 切片:把 `ContentAppServiceImpl.saveBlock` 首次成功写入的 `muse_content_block_source_attribution(source_status=active)` 转成 Content 本域 Events publish outbox再由 Content worker 调用 `EventsPublishApi` 写入 `muse_unified_event`,最终通过 `/app-api/muse/events` 对作品 owner SSE 可见。
本执行版不允许把 Events / P1R-7 / Content 推进到 `completed`,不允许修改 OpenAPI、coverage scanner 或 coverage report不允许把 Content 保存正文伪造成 SSE `chunk`
```mermaid
flowchart LR
Save["saveBlock<br/>commandId + expectedRevision"] --> Block["muse_content_block<br/>content_text + revision"]
Save --> Attr["muse_content_block_source_attribution<br/>source_status=active"]
Attr --> Outbox["muse_content_event_publish_outbox<br/>one row per block revision"]
Outbox --> Worker["Content publish worker<br/>claim/retry/dead_letter"]
Worker --> Api["EventsPublishApi<br/>events-api only"]
Api --> Unified["muse_unified_event<br/>notification/source_status_change"]
Unified --> Stream["/app-api/muse/events<br/>work owner visible SSE"]
```
## 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`
Halley quality reviewer 长时间超时未返回,已丢弃,不计入 review gate。
Chandrasekhar P3 已冻结为本执行版硬 gate
- Content worker / outbox 只能发布 `eventType=notification``notificationType=source_status_change`
- 必须有测试或静态 gate 防止 Content 误用 `chunk`
P1R-7f 执行版首轮 quality / feasibility reviewHypatiaFAIL两个 P1 已验证有效:
- `source_status=active` 合同只停留在 service 描述V21 schema、Flyway `_test` 与 worker gate 没有兜住非 active outbox由于 `deleteBlock` 真实会写 `sourceStatus=deleted`,该风险实际可达。
- E2E wording 允许 bare fake `EventsPublishApi` 返回 accepted不能证明真实 `EventsPublishServiceImpl -> muse_unified_event -> visible query` 链路。
执行版已修订V21 check 约束、SQL gate、Flyway `_test`、worker fail-closed 与 focused tests 均补 `source_status = 'active'`E2E 成功路径收紧为必须使用真实 `EventsPublishServiceImpl`,只允许使用 in-memory / mocked `UnifiedEventMapper`,禁止 bare fake `EventsPublishApi` 作为成功证据。
旧 spec reviewerArendt在首轮文档修订前长时间未返回关闭动作被会话中断该旧 review 视为 stale / discarded即使后续异步返回也不计入 gate。
P1R-7f 执行版第二轮 fresh review 已通过:
- Huygens execution spec compliance reviewPASS无 P0/P1/P2/P3已验证 scope、非目标、protected 边界、completed 边界、依赖边界、Chandrasekhar P3、Hypatia P1 修订、V21 / worker / E2E / Flyway / P1R mixed / `/tmp` scanner / protected diff gate 均已闭合。
- Raman execution quality / feasibility reviewPASS无 P0/P1/P2/P3已验证 `saveBlock` 事务接入点、command replay 早退、`BlockSourceAttributionDO` 字段、Events `notification/source_status_change` 合同、`chunk` 不适配 Content save、V21 `source_status=active` 约束、payload exact allowlist、focused tests、真实 `EventsPublishServiceImpl` E2E 与防假绿门禁均可落地。
本轮双 PASS 只允许进入 fresh implementer 实现,不代表 P1R-7f 实现完成,也不代表 Events / P1R-7 / Content completed。实现后必须重新进行 fresh implementation spec / correctness review + fresh implementation quality / data-integrity / testing review双 PASS 前不得提交、push 或推进 completed。
## 边界
### 本轮目标
- 在 Content owner 内新增独立 Events publish outbox。
- 只接入 `ContentAppServiceImpl.saveBlock` 首次成功路径。
- 只发布 `muse_content_block_source_attribution(source_status=active)` 形成的用户可见 source status 摘要。
- 通过 Content worker 调用 `EventsPublishApi`
- 用 focused tests、P1R mixed gates、Flyway `_test`、dependency gate、dependency tree 和隔离 coverage scanner 证明链路进入 `needs_verification` 证据层。
### 本轮非目标
- 不覆盖 `createBlock``deleteBlock``splitBlock``mergeBlocks``mergeBlockSuggestion`
- 不覆盖 import、parse、export、planning、meta projection、admin risk action。
- 不发布 Block 正文、请求正文、完整 `sourceSnapshot``sourceObjectId``authorizationSnapshotId``lineagePayload``licenseRestrictionSnapshot``auditReason``requestHash``resultSnapshot` 或错误堆栈。
- 不发布 `chunk``done``quality_check``error` 事件。
- 不新增 Events notification subtype不新增 `block_saved`
- 不抽取 AI / Knowledge / Market / Account / Content 共享 outbox 框架。
- 不为历史 block source attribution 做 backfill。
- 不修改 `docs/api-contracts/*/openapi.yaml`
- 不修改 `muse-cloud/scripts/p1r-audit-api-coverage.py`
- 不修改 `docs/superpowers/reports/p1r-api-coverage.json``.md`
### 受保护文件
实现代理不得修改以下文件:
- `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`
## 已验证事实
- 正确 worktree 是 `/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0`
- 当前分支是 `dev/1.0.0`,远端跟踪是 `origin/dev/1.0.0`
- `git pull --ff-only origin dev/1.0.0` 返回 `Already up to date.`
- 当前 HEAD 为 `68cee5d test(p1r): 收口 Account 事件传播真实链路门禁`
- P1R-7e 已提交并 push`0f49802``0e10caf``68cee5d`
- P1R-7f 审阅版路径为 `docs/agent-specs/2026-06-08-P1R7fContentSourceOwnerPropagation审阅版.md`
- P1R-7f 审阅版 fresh review 已双 PASSHarvey spec PASSChandrasekhar quality PASS。
- 当前 coverage 边界仍为 `completedOperations=100``needsVerificationOperations=133``incompleteOperations=0``genericPersistenceOperations=0``ssePlaceholderOperations=0`
- Events `streamEvents` 当前是 `dedicated / needs_verification`
- Content 51 operations 当前均为 `dedicated / needs_verification`
- 当前受保护文件 staged / unstaged diff 为空。
- 当前最新 migration 是 `muse-cloud/sql/muse/V20__extend_account_events_publish_outbox.sql`P1R-7f 下一 migration 编号应为 `V21`。若实现前 live 文件列表已出现更高版本,必须停止并修订本执行版的 migration 编号。
- `muse-module-content-server/pom.xml` 当前没有 `muse-module-events-api` 依赖,也没有 `muse-module-events-server` 依赖。
- `muse-module-events-server/pom.xml` 当前不依赖 Content server。
- `ContentAppServiceImpl.saveBlock` 当前在同一事务内完成 command 预占、owner guard、revision 校验、Block 更新、source attribution 插入和 command audit。
- `saveBlock` 命中 command replay 时直接回放 `ContentCommandDO.resultSnapshot`,不会更新 Block也不会插入 `BlockSourceAttributionDO`
- `BlockSourceAttributionDO` 包含 `id``workId``blockId``revision``sourceType``sourceObjectId``sourceVersion``lineagePayload``authorizationSnapshotId``sourceStatus``licenseRestrictionSnapshot`
- `muse_content_block_source_attribution` 已有唯一约束 `(tenant_id, block_id, revision)`
- `EventsPublishReqDTO` 必填 `commandId``tenantId``ownerUserId``sourceOwner``sourceType``sourceId``eventType``payloadSummary``emittedAt`
- `EventsPublishServiceImpl` 当前先按 `commandId` 回放,再按 source tuple 回放。
- `EventsPublishServiceImpl` 当前已声明 `notification` event type。
- `EventsPublishServiceImpl` 当前已声明 `source_status_change` notification type。
- `EventsPublishServiceImpl` 当前把 `chunk` 识别为需要 `content/sequenceNo` 的 AI 文本片段语义Content saveBlock 不能使用 `chunk`
- `UnifiedEventMapper.selectVisibleEventsForOwner` 当前按 `tenantId``ownerUserId``publishStatus=accepted``deleted=false``visibleFrom<=now``sequenceNo>afterSequenceNo` 查询 SSE 可见事件。
## 推断
- P1R-7f 可复用 P1R-7b/P1R-7c/P1R-7d/P1R-7e 的 outbox 状态机、worker claim、stale claim guard、dependency tree gate、Surefire XML 防假绿和 Flyway `_test` 模式。
- `muse_content_block_source_attribution``muse_content_block` 更适合作为第一批 source fact因为它表达“该 Block revision 的来源状态 active”可脱敏映射为 `source_status_change`
- Content outbox 与 block source attribution 必须保持两张表 / 两套状态机,否则 source fact 与 publish 补偿状态会互相污染。
- `source_status_change` 在 Content 中只能表达“Block 来源状态已更新”,不能扩展成正文保存、任务完成或万能通知。
## 假设
- P1R-7f 允许 Content server 新增 `muse-module-events-api` direct dependency。
- P1R-7f 允许新增 `muse_content_event_publish_outbox`
- P1R-7f 第一批通知的 SSE owner 是作品 owner`WorkDO.ownerUserId` / 当前 `saveBlock` userId。
- P1R-7f 第一批使用 Events 现有 `notification/source_status_change`,不新增 OpenAPI event type 或 notification type。
## 设计合同
### 事件合同
第一批只允许以下 Content source fact 进入 publish outbox
| Content source fact | sourceStatus | Events eventType | notification type | SSE owner | message |
|---|---|---|---|---|---|
| `muse_content_block_source_attribution` | `active` | `notification` | `source_status_change` | `WorkDO.ownerUserId` | `内容来源状态已更新` |
强制约束:
- Content outbox service 必须二次校验 `sourceStatus=active`
- Content worker 构造的 `EventsPublishReqDTO.eventType` 必须固定为 `notification`
- Content worker 构造的 payload `type` 必须固定为 `source_status_change`
- Content 相关生产代码和 P1R gate 不得把 Content saveBlock 发布为 `chunk`
- V21 SQL、worker tests、E2E tests 至少各有一处证明 Content 只发布 `notification/source_status_change`,并负断言 `chunk`
### 幂等合同
- 每个 `(tenantId, blockId, blockRevision, source_status_change)` 只生成一条 outbox。
- `commandId` 格式:`content_evt:` + SHA-256(`tenantId|blockId|blockRevision|source_status_change`) 前 32 位 hex。
- `outboxId` 格式:`content_out:` + SHA-256(`tenantId|blockId|blockRevision|source_status_change`) 前 32 位 hex。
- Events source tuple
- `sourceOwner = content`
- `sourceType = content_block_source_attribution`
- `sourceId = blockId`
- `sourceRevision = blockRevision`
- `eventType = notification`
- 同一 Block revision 重放必须得到同一 Events `eventId/sequenceNo`
- 同一 Block 的不同 revision 必须生成不同 commandId 和不同 source tuple。
- Command replay 策略:`saveBlock` 命中 Content command replay 时不补历史 outbox只有首次成功写入 `BlockSourceAttributionDO(sourceStatus=active)` 的事务内创建 outbox。历史 backfill 若需要,必须另起任务。
### Payload 合同
Internal outbox 允许保存以下字段:
- `sourceAttributionId`
- `sourceCommandId`
- `ownerUserId`
- `workId`
- `blockId`
- `blockRevision`
- `sourceStatus`
- `eventType`
- `notificationType`
- `resourceRefType`
- `resourceRefId`
- `payloadSummary`
Events `payloadSummary` 只能发布:
```json
{
"type": "source_status_change",
"message": "内容来源状态已更新",
"resourceRef": {
"resourceType": "content_block",
"resourceId": 8001
},
"timestamp": "2026-06-08T12:00:00"
}
```
约束:
- `resourceRef.resourceId` 必须是数值型 Long / Integer 语义,值为 `blockId`
- DTO `EventsPublishReqDTO.resourceId` 是 String可以传 `String.valueOf(blockId)`payload `resourceRef.resourceId` 必须保持 Number不能是 String。
- `message` 第一切片只允许 `内容来源状态已更新`
- `timestamp` 使用 source attribution `createTime`,缺失时才回退 outbox `createTime`
- 不得发布 `contentText`、请求 `content``sourceSnapshot` 原文、`sourceObjectId``authorizationSnapshotId``lineagePayload``licenseRestrictionSnapshot``auditReason``requestHash``resultSnapshot`、错误堆栈。
- `payloadSummary` 必须是 exact allowlist顶层只能包含 `type``message``resourceRef``timestamp``resourceRef` 只能包含 `resourceType``resourceId`
### 状态机合同
Outbox 本域状态只允许:
- `queued`
- `running`
- `retryable`
- `published`
- `dead_letter`
状态流转:
```mermaid
stateDiagram-v2
[*] --> queued
queued --> running: claim
retryable --> running: retry due
running --> running: expired lease reclaim
running --> published: Events accepted
running --> dead_letter: Events rejected/blocked
running --> retryable: temporary failure and attempts remain
running --> dead_letter: retry exhausted
```
Worker claim 要求:
- 使用 PostgreSQL `FOR UPDATE SKIP LOCKED` 或等价 `UPDATE ... RETURNING` 原子领取。
- 调度线程无请求租户上下文时必须先忽略租户拦截器领取,再使用 outbox 行内 `tenantId` 恢复租户上下文发布。
- Claim 阶段唯一递增 `attemptCount`
- 临时失败只释放 claim、设置 `nextRetryAt` 和安全错误摘要,不二次递增 attempt count。
- `markPublished` / `markRetryable` / `markDeadLetter` 必须绑定 `publish_status='running' AND attempt_count=#{claimedAttemptCount}`
- terminal update 返回 0 时必须记录 stale claim 诊断日志,包含 tenantId、ownerUserId、outboxId、blockId、blockRevision、attempt、targetStatus、errorCode。
- Worker 默认关闭:`muse.content.events.publish-worker.enabled=false`
- 调度配置键:
- `muse.content.events.publish-worker.initial-delay-ms`
- `muse.content.events.publish-worker.fixed-delay-ms`
- `muse.content.events.publish-worker.max-attempt`
- `muse.content.events.publish-worker.claim-timeout-seconds`
### 失败路径合同
| 路径 | 结果 |
|---|---|
| command replay | 不补历史 outbox不重复发布 |
| revision guard 失败 | 不更新 Block不创建 source attribution不创建 outbox |
| owner guard 失败 | 不更新 Block不创建 source attribution不创建 outbox |
| source attribution 缺失 | 不创建 outbox记录安全日志 |
| sourceStatus 非 `active` | 不创建 outbox记录安全日志 |
| ownerUserId null 或 <= 0 | 不创建 outbox记录安全日志 |
| blockId null 或 <= 0 | 不创建 outbox记录安全日志 |
| blockRevision null 或 <= 0 | 不创建 outbox记录安全日志 |
| payload 构造缺 required 字段 | 创建 outbox 前拒绝并记录安全日志,不创建 outbox不发布错误码固定为 `CONTENT_EVENTS_PAYLOAD_INVALID` |
| Events accepted | outbox -> published回写 eventId / sequenceNo |
| Events duplicate accepted | outbox -> published回写既有 eventId / sequenceNo |
| Events rejected / blocked | outbox -> dead_letter |
| CommonResult error / exception | attempt 未耗尽 -> retryable耗尽 -> dead_letter |
| worker 构造出 `chunk` | 测试必须失败;生产实现不得出现该路径 |
### 依赖合同
允许:
- `muse-module-content-server` 依赖 `muse-module-events-api`
- `muse-server` 同时装配 Content server 与 Events server。
禁止:
- `muse-module-content-server` 依赖 `muse-module-events-server`
- `muse-module-events-server` 依赖 `muse-module-content-server`
- Events server 依赖 AI / Knowledge / Market / Member / Content server。
- Events stream controller 查询 Content 业务表。
## 涉及文件
### 生产代码
- Modify: `muse-cloud/muse-module-content/muse-module-content-server/pom.xml`
- Modify: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/application/ContentAppServiceImpl.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/application/ContentEventPublishOutboxService.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/application/ContentEventPublishOutboxServiceImpl.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/application/ContentEventPublishWorker.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/application/ContentEventPayloads.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/dal/dataobject/ContentEventPublishOutboxDO.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/dal/mysql/ContentEventPublishOutboxMapper.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/framework/config/MuseContentEventsConfiguration.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/main/java/cn/iocoder/muse/module/content/framework/config/MuseContentEventsProperties.java`
- Create: `muse-cloud/sql/muse/V21__extend_content_events_publish_outbox.sql`
### 测试代码
- Modify: `muse-cloud/muse-module-content/muse-module-content-server/src/test/java/cn/iocoder/muse/module/content/application/ContentAppServiceTest.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/test/java/cn/iocoder/muse/module/content/application/ContentEventPublishOutboxServiceTest.java`
- Create: `muse-cloud/muse-module-content/muse-module-content-server/src/test/java/cn/iocoder/muse/module/content/application/ContentEventPublishWorkerTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishDependencyTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishEndToEndTest.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishFlywayMigrationIT.java`
- Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishMigrationSqlTest.java`
### 文档
- Modify: `docs/agent-specs/.agent`
- Create: `docs/memorys/YYYY-MM-DD-P1R7fContentSourceOwnerPropagation真实链路.md` only after implementation evidence is available
## 实施任务
### Task 0fresh implementer preflight
在 worktree 根执行:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
git status --short --branch
git log --oneline -5
git pull --ff-only origin dev/1.0.0
git diff --quiet -- 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
git diff --cached --quiet -- 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
ls muse-cloud/sql/muse/V*__*.sql | sort -V | tail
```
完成条件:
- 记录 dirty baseline。
- 确认 protected staged / unstaged diff 为空。
- 确认 V21 编号仍可用;若最新 migration 已不是 V20停止并修订本执行版。
- 只读核对 Content server 当前没有 events-api dependency。
- 只读核对 Events server 仍不依赖 Content server。
### Task 1依赖与配置骨架
目标:
- Content server 只新增 `muse-module-events-api` direct dependency。
- 新增 Content Events worker 配置,默认关闭。
文件:
- Modify: `muse-cloud/muse-module-content/muse-module-content-server/pom.xml`
- Create: `MuseContentEventsConfiguration.java`
- Create: `MuseContentEventsProperties.java`
要求:
- 不新增 `muse-module-events-server`
- 配置 prefix`muse.content.events`
- 默认:
- `publish-worker.enabled=false`
- `publish-worker.max-attempt=5`
- `publish-worker.claim-timeout-seconds=60`
- 配置类必须可被 Spring 扫描。
- 关键配置、状态转换和 worker 禁用分支必须写中文注释,说明 WHY。
Focused tests
- 配置默认值。
- worker disabled 时 `dispatchOnce()` 不 claim。
### Task 2V21 outbox schema
目标:
- 新增 `muse_content_event_publish_outbox`,只服务 Content -> Events publish compensation。
文件:
- Create: `muse-cloud/sql/muse/V21__extend_content_events_publish_outbox.sql`
- Create: `ContentEventPublishOutboxDO.java`
- Create: `ContentEventPublishOutboxMapper.java`
- Create: `P1rContentEventsPublishMigrationSqlTest.java`
表字段必须包含:
- `id`
- `tenant_id`
- `outbox_id`
- `command_id`
- `source_command_id`
- `source_attribution_id`
- `owner_user_id`
- `work_id`
- `block_id`
- `block_revision`
- `source_status`
- `source_revision`
- `event_type`
- `notification_type`
- `resource_ref_type`
- `resource_ref_id`
- `payload_summary`
- `publish_status`
- `attempt_count`
- `max_attempt`
- `next_retry_at`
- `claimed_at`
- `claim_expires_at`
- `last_error_code`
- `last_error_message`
- `published_event_id`
- `published_sequence_no`
- `creator`
- `updater`
- `create_time`
- `update_time`
- `deleted`
唯一约束:
- `(tenant_id, outbox_id)`
- `(tenant_id, command_id)`
- `(tenant_id, block_id, block_revision, notification_type)`
Check 约束:
- `event_type = 'notification'`
- `notification_type = 'source_status_change'`
- `source_status = 'active'`
- `publish_status in ('queued','running','retryable','published','dead_letter')`
- `owner_user_id > 0`
- `block_id > 0`
- `block_revision > 0`
- `resource_ref_id > 0`
- `attempt_count >= 0`
- `max_attempt > 0`
索引:
- claim index`publish_status, next_retry_at, claim_expires_at`
- owner status index`owner_user_id, publish_status`
- block revision index`block_id, block_revision`
Trigger
- `update_time` trigger。
Mapper 方法:
- `insertIgnore`
- `selectByTenantIdAndOutboxId`
- `selectByTenantIdAndCommandId`
- `selectByTenantIdAndBlockIdAndBlockRevisionAndNotificationType`
- `claimNextPublishOutbox`
- `markPublished`
- `markRetryable`
- `markDeadLetter`
SQL gate
- 断言字段、唯一约束、check 约束、索引、trigger。
- 断言 `event_type` check 是单值 `notification`,不能包含 `chunk` 或 OR 放宽。
- 断言 `notification_type` check 是单值 `source_status_change`
- 断言 `source_status` check 是单值 `active`,不能包含 `deleted` 或 OR 放宽。
- 断言非法 `event_type='chunk'`、非法 `notification_type`、非法 `source_status='deleted'`、非法 `publish_status`、非法 `owner_user_id`、非法 `block_id`、非法 `block_revision`、非法 `resource_ref_id` 在 Flyway `_test` 中被 DB 拒绝。
### Task 3Content outbox service
目标:
-`saveBlock` 首次成功事务内,为 `BlockSourceAttributionDO(sourceStatus=active)` 创建一条 outbox。
文件:
- Create: `ContentEventPublishOutboxService.java`
- Create: `ContentEventPublishOutboxServiceImpl.java`
- Create: `ContentEventPayloads.java`
- Modify: `ContentAppServiceImpl.java`
- Test: `ContentEventPublishOutboxServiceTest.java`
- Test: `ContentAppServiceTest.java`
接入点:
- `ContentAppServiceImpl.saveBlock`
-`sourceAttributionMapper.insert(...)` 后调用 outbox service。
- command replay 返回时不得创建 outbox。
- revision guard / owner guard 失败时不得创建 outbox。
Outbox service 输入:
- `userId`
- `workId`
- `blockId`
- `blockRevision`
- `sourceCommandId = reqVO.getCommandId()`
- `BlockSourceAttributionDO sourceAttribution`
Payload 构造:
```json
{
"type": "source_status_change",
"message": "内容来源状态已更新",
"resourceRef": {
"resourceType": "content_block",
"resourceId": 8001
},
"timestamp": "2026-06-08T12:00:00"
}
```
Payload validator
- 顶层 exact keys`type/message/resourceRef/timestamp`
- `resourceRef` exact keys`resourceType/resourceId`
- `type=source_status_change`
- `message` 只允许 `内容来源状态已更新`
- `resourceType=content_block`
- `resourceId` 必须是 Number。
- 不允许 `contentText``content``sourceSnapshot``sourceObjectId``authorizationSnapshotId``lineagePayload``licenseRestrictionSnapshot``auditReason``requestHash``resultSnapshot``stackTrace`
错误码:
- payload invalid 固定为 `CONTENT_EVENTS_PAYLOAD_INVALID`
Focused tests
- successful saveBlock 首次执行在 source attribution 插入后创建 outbox。
- command replay 不创建 outbox。
- revision conflict 不创建 outbox。
- owner mismatch 不创建 outbox。
- duplicate `(blockId, revision, source_status_change)` 不创建第二条 outbox。
- invalid owner 不创建 outbox 并记录安全日志。
- sourceStatus 非 active 不创建 outbox。
- payload exact allowlist敏感字段负断言。
- `eventType` 固定 `notification``notificationType` 固定 `source_status_change`
- 负断言 Content outbox service 不生成 `chunk`
- configured maxAttempt 生效。
- timestamp 使用 source attribution createTime缺失时回退 outbox createTime。
### Task 4Content publish worker
目标:
- Claim Content outbox构造 `EventsPublishReqDTO`,调用 `EventsPublishApi`,按结果更新 outbox 状态。
文件:
- Create: `ContentEventPublishWorker.java`
- Test: `ContentEventPublishWorkerTest.java`
Events request
- `commandId = outbox.commandId`
- `tenantId = outbox.tenantId`
- `ownerUserId = outbox.ownerUserId`
- `sourceOwner = content`
- `sourceType = content_block_source_attribution`
- `sourceId = String.valueOf(outbox.blockId)`
- `sourceRevision = String.valueOf(outbox.blockRevision)`
- `eventType = notification`
- `resourceType = outbox.resourceRefType`
- `resourceId = String.valueOf(outbox.resourceRefId)`
- `payloadSummary = parsed exact allowlist payload`
- `emittedAt = outbox.createTime`
状态处理:
- accepted -> `published`
- duplicate accepted -> `published`
- rejected / blocked -> `dead_letter`
- CommonResult error -> `retryable` 或耗尽后 `dead_letter`
- exception -> `retryable` 或耗尽后 `dead_letter`
- payload invalid -> `dead_letter`,不调用 Events
- unknown status -> `dead_letter`
Focused tests
- disabled 不 claim。
- enabled claim 使用配置 claim timeout。
- accepted 回写 eventId / sequenceNo。
- duplicate accepted 回写既有 eventId / sequenceNo。
- rejected / blocked dead_letter。
- CommonResult error 非最后一次 retryable最后一次 dead_letter。
- exception 非最后一次 retryable最后一次 dead_letter。
- invalid payload dead_letter 且不调用 Events。
- owner invalid dead_letter 且不调用 Events。
- sourceStatus 非 active dead_letter 且不调用 Events。
- terminal update 使用 claimed attempt ownership。
- stale terminal update 返回 0 时记录诊断日志。
- worker 发出的 `EventsPublishReqDTO.eventType` 必须是 `notification`
- worker payload `type` 必须是 `source_status_change`
- 负断言 worker 不调用 `chunk`,且 payload 不包含正文。
### Task 5P1R executable gates
目标:
- 用 server 侧 gate 证明依赖方向、focused E2E、V21 migration、anti-chunk 合同和既有 owner gate 不回退。
文件:
- Create: `P1rContentEventsPublishDependencyTest.java`
- Create: `P1rContentEventsPublishEndToEndTest.java`
- Create: `P1rContentEventsPublishFlywayMigrationIT.java`
- Create: `P1rContentEventsPublishMigrationSqlTest.java`
Dependency test
- Content server 有 direct `muse-module-events-api`
- Content server 无 `muse-module-events-server`
- Events server 无 AI / Knowledge / Market / Member / Content server。
E2E focused test
- 构造 `saveBlock` source fact。
- Content outbox service 创建 outbox。
- Worker 成功路径必须调用真实 `EventsPublishServiceImpl`;只允许使用 in-memory / mocked `UnifiedEventMapper` 承接 unified event 行,禁止 bare fake `EventsPublishApi` 直接返回 accepted 作为成功证据。
- `muse_unified_event` 中 owner 为作品 owner。
- SSE visible query 对作品 owner 可见,对其他 owner 不可见。
- unified event `eventType=notification`payload `type=source_status_change`
- 负断言没有 `chunk` event。
- payload 不含正文、sourceSnapshot、authorizationSnapshotId、lineage、license restriction、auditReason、requestHash、resultSnapshot、错误堆栈。
- 同一 block revision duplicate replay 返回同一 eventId / sequenceNo。
- 同一 block 不同 revision 产生不同 notification。
- rejected / blocked -> outbox dead_lettervisible query 为空。
- sourceStatus 非 active -> outbox dead_letter 或创建前拒绝visible query 为空,且不调用 Events。
- stale claim terminal update 被忽略。
Flyway `_test`
- 使用真实 PostgreSQL `_test` 数据库。
- 跑到 V21。
- 验证 table / indexes / constraints / trigger。
- 验证非法 insert 被 DB 拒绝。
- 验证 `event_type='chunk'` 被 DB 拒绝。
- 验证 `source_status='deleted'` 被 DB 拒绝。
### Task 6最终验证、review 和留痕
最终验证顺序必须先 build再 tests避免 `clean` 清理 surefire XML。
#### 1. Full reactor build
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o clean install -DskipTests -Dspring-boot.repackage.skip=true
```
期望live reactor 全量 modules success。
#### 2. Content focused tests
先断言新增测试文件存在:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
test -f muse-cloud/muse-module-content/muse-module-content-server/src/test/java/cn/iocoder/muse/module/content/application/ContentEventPublishOutboxServiceTest.java && \
test -f muse-cloud/muse-module-content/muse-module-content-server/src/test/java/cn/iocoder/muse/module/content/application/ContentEventPublishWorkerTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishMigrationSqlTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishDependencyTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishEndToEndTest.java && \
test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rContentEventsPublishFlywayMigrationIT.java
```
运行:
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \
-pl muse-module-content/muse-module-content-server -am \
-Dtest=ContentAppServiceTest,ContentEventPublishOutboxServiceTest,ContentEventPublishWorkerTest \
-Dsurefire.failIfNoSpecifiedTests=false
```
必须检查 surefire XML目标测试类 `tests>0` 且 failures/errors/skipped 均为 0。
#### 3. P1R mixed executable gates
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \
-pl muse-server -am \
-Dtest=P1rContentEventsPublishMigrationSqlTest,P1rContentEventsPublishDependencyTest,P1rContentEventsPublishEndToEndTest,P1rContentRealApiGateTest,P1rAccountEventsPublishMigrationSqlTest,P1rAccountEventsPublishDependencyTest,P1rAccountEventsPublishEndToEndTest,P1rAccountRealApiGateTest,P1rAccountRouteOwnershipTest,P1rMarketEventsPublishMigrationSqlTest,P1rMarketEventsPublishDependencyTest,P1rMarketEventsPublishEndToEndTest,P1rKnowledgeEventsPublishMigrationSqlTest,P1rKnowledgeEventsPublishDependencyTest,P1rKnowledgeEventsPublishEndToEndTest,P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \
-Dsurefire.failIfNoSpecifiedTests=false
```
必须检查 surefire XML新增 Content 三个 P1R gate、既有 Content real API gate、Account/Market/Knowledge/AI/Events 目标测试类 `tests>0` 且 failures/errors/skipped 均为 0
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
python3 - <<'PY'
from pathlib import Path
import xml.etree.ElementTree as ET
targets = [
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rContentEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rContentEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rContentEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rContentRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAccountRouteOwnershipTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rMarketEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rKnowledgeEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishMigrationSqlTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishDependencyTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiEventsPublishEndToEndTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rApiCoverageReportTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rEventsRouteOwnershipTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRealApiGateTest.xml",
"muse-cloud/muse-server/target/surefire-reports/TEST-cn.iocoder.muse.server.framework.api.P1rAiRouteOwnershipTest.xml",
]
for target in targets:
path = Path(target)
if not path.exists():
raise SystemExit(f"missing surefire xml: {target}")
root = ET.parse(path).getroot()
tests = int(root.attrib.get("tests", "0"))
failures = int(root.attrib.get("failures", "0"))
errors = int(root.attrib.get("errors", "0"))
skipped = int(root.attrib.get("skipped", "0"))
if tests <= 0 or failures or errors or skipped:
raise SystemExit(f"bad surefire result: {target} tests={tests} failures={failures} errors={errors} skipped={skipped}")
PY
```
#### 4. Flyway `_test`
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
. ~/.config/muse-repo/infra.env && \
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD" && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test -pl muse-server -am \
-Dtest=P1rContentEventsPublishFlywayMigrationIT \
-Dflyway.postgresql.transactional.lock=false \
-Dp1r.flyway.locations=filesystem:sql/muse \
-Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/muse_p1r7f_content_events_publish_test" \
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
-Djava.net.useSystemProxies=false \
-DsocksProxyHost= \
-DsocksProxyPort= \
-Dhttp.proxyHost= \
-Dhttp.proxyPort= \
-Dhttps.proxyHost= \
-Dhttps.proxyPort= \
-Dsurefire.failIfNoSpecifiedTests=false
```
期望:
- migrations_executed = 21。
- target_schema_version = 21。
- `muse_content_event_publish_outbox` 存在。
- `event_type='chunk'` insert 被拒绝。
- `source_status='deleted'` insert 被拒绝。
- 目标测试 XML tests > 0failures/errors/skipped 均为 0。
#### 5. Dependency tree gate
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-content/muse-module-content-server -DskipTests dependency:tree > /tmp/p1r7f-content-server-dependency-tree.txt && \
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-events/muse-module-events-server -DskipTests dependency:tree > /tmp/p1r7f-events-server-dependency-tree.txt && \
rg "muse-module-events-api" /tmp/p1r7f-content-server-dependency-tree.txt && \
! rg "muse-module-events-server" /tmp/p1r7f-content-server-dependency-tree.txt && \
! rg "muse-module-(ai|knowledge|market|member|content)-server" /tmp/p1r7f-events-server-dependency-tree.txt
```
#### 6. Isolated coverage scanner
只允许在 `/tmp` 隔离副本运行 scanner
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
tmpdir=$(mktemp -d /tmp/p1r7f-coverage-scan.XXXXXX) && \
rsync -a --delete --exclude .git --exclude 'muse-cloud/**/target' ./ "$tmpdir"/ && \
cd "$tmpdir" && \
python3 -m venv .venv && \
. .venv/bin/activate && \
python -m pip install --quiet PyYAML && \
python muse-cloud/scripts/p1r-audit-api-coverage.py --check
```
期望:
- scanner exit 0。
- completed=100。
- needsVerification=133。
- incomplete=0。
- genericPersistence=0。
- ssePlaceholder=0。
- Events `streamEvents=dedicated / needs_verification`
- Content 51 operations 仍为 `dedicated / needs_verification`
- AI / Knowledge / Market / Account source owner gates 不回退。
#### 7. Protected diff and whitespace
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \
git diff --check && \
git diff --quiet -- 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 && \
git diff --cached --quiet -- 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
```
#### 8. Memory
实现 evidence 可用后新增:
- `docs/memorys/2026-06-08-P1R7fContentSourceOwnerPropagation真实链路.md`
必须记录:
- 目标和非目标。
- 关键实现文件。
- focused tests 数量。
- P1R mixed gates 数量。
- Flyway `_test` 结果。
- dependency tree gate。
- isolated coverage scanner 目录和 summary。
- protected diff。
- fresh implementation review 结果。
- 明确说明仍不代表 Events / P1R-7 / Content completed。
## 回滚策略
- 配置层回滚:保持或恢复 `muse.content.events.publish-worker.enabled=false`worker 不再 claim 新 outbox。
- 代码层回滚:回退 Content outbox service 注入点与 worker/config/pom 改动;不触碰 OpenAPI/scanner/coverage report。
- 数据层回滚V21 只新增 outbox 表、索引、约束和 trigger若未发布生产数据可按迁移策略 drop 新表;若已有数据,先停 worker再导出 outbox 审计数据后按 DBA 审批处理。
- 状态层回滚:不得把 rollback 描述成 completed 撤销P1R coverage 状态仍保持 `needs_verification`