保留 P1R-7f 审阅版、执行版与 review gate 结论,明确本阶段只覆盖 saveBlock source attribution active 链路,不推进 completed。
41 KiB
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。
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 review:PASS,无 P0/P1/P2;已验证本审阅版严格停留在 Content
saveBlock -> block source attribution -> Events notification第一切片,没有把 completed、OpenAPI、coverage 或全部 Content 写命令混入。 - Chandrasekhar quality / feasibility review:PASS,无 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 review(Hypatia)FAIL,两个 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 reviewer(Arendt)在首轮文档修订前长时间未返回,关闭动作被会话中断;该旧 review 视为 stale / discarded,即使后续异步返回也不计入 gate。
P1R-7f 执行版第二轮 fresh review 已通过:
- Huygens execution spec compliance review:PASS,无 P0/P1/P2/P3;已验证 scope、非目标、protected 边界、completed 边界、依赖边界、Chandrasekhar P3、Hypatia P1 修订、V21 / worker / E2E / Flyway / P1R mixed /
/tmpscanner / protected diff gate 均已闭合。 - Raman execution quality / feasibility review:PASS,无 P0/P1/P2/P3;已验证
saveBlock事务接入点、command replay 早退、BlockSourceAttributionDO字段、Eventsnotification/source_status_change合同、chunk不适配 Content save、V21source_status=active约束、payload exact allowlist、focused tests、真实EventsPublishServiceImplE2E 与防假绿门禁均可落地。
本轮双 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.yamldocs/api-contracts/market/openapi.yamldocs/api-contracts/ai/openapi.yamldocs/api-contracts/knowledge/openapi.yamldocs/api-contracts/events/openapi.yamlmuse-cloud/scripts/p1r-audit-api-coverage.pydocs/superpowers/reports/p1r-api-coverage.jsondocs/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 已双 PASS:Harvey spec PASS,Chandrasekhar 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当前已声明notificationevent type。EventsPublishServiceImpl当前已声明source_status_changenotification 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-apidirect dependency。 - P1R-7f 允许新增
muse_content_event_publish_outbox。 - P1R-7f 第一批通知的 SSE owner 是作品 owner,即
WorkDO.ownerUserId/ 当前saveBlockuserId。 - 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 = contentsourceType = content_block_source_attributionsourceId = blockIdsourceRevision = blockRevisioneventType = 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 允许保存以下字段:
sourceAttributionIdsourceCommandIdownerUserIdworkIdblockIdblockRevisionsourceStatuseventTypenotificationTyperesourceRefTyperesourceRefIdpayloadSummary
Events payloadSummary 只能发布:
{
"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);payloadresourceRef.resourceId必须保持 Number,不能是 String。 message第一切片只允许内容来源状态已更新。timestamp使用 source attributioncreateTime,缺失时才回退 outboxcreateTime。- 不得发布
contentText、请求content、sourceSnapshot原文、sourceObjectId、authorizationSnapshotId、lineagePayload、licenseRestrictionSnapshot、auditReason、requestHash、resultSnapshot、错误堆栈。 payloadSummary必须是 exact allowlist:顶层只能包含type、message、resourceRef、timestamp;resourceRef只能包含resourceType、resourceId。
状态机合同
Outbox 本域状态只允许:
queuedrunningretryablepublisheddead_letter
状态流转:
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-msmuse.content.events.publish-worker.fixed-delay-msmuse.content.events.publish-worker.max-attemptmuse.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真实链路.mdonly after implementation evidence is available
实施任务
Task 0:fresh implementer preflight
在 worktree 根执行:
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-apidirect 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=falsepublish-worker.max-attempt=5publish-worker.claim-timeout-seconds=60
- 配置类必须可被 Spring 扫描。
- 关键配置、状态转换和 worker 禁用分支必须写中文注释,说明 WHY。
Focused tests:
- 配置默认值。
- worker disabled 时
dispatchOnce()不 claim。
Task 2:V21 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
表字段必须包含:
idtenant_idoutbox_idcommand_idsource_command_idsource_attribution_idowner_user_idwork_idblock_idblock_revisionsource_statussource_revisionevent_typenotification_typeresource_ref_typeresource_ref_idpayload_summarypublish_statusattempt_countmax_attemptnext_retry_atclaimed_atclaim_expires_atlast_error_codelast_error_messagepublished_event_idpublished_sequence_nocreatorupdatercreate_timeupdate_timedeleted
唯一约束:
(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 > 0block_id > 0block_revision > 0resource_ref_id > 0attempt_count >= 0max_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_timetrigger。
Mapper 方法:
insertIgnoreselectByTenantIdAndOutboxIdselectByTenantIdAndCommandIdselectByTenantIdAndBlockIdAndBlockRevisionAndNotificationTypeclaimNextPublishOutboxmarkPublishedmarkRetryablemarkDeadLetter
SQL gate:
- 断言字段、唯一约束、check 约束、索引、trigger。
- 断言
event_typecheck 是单值notification,不能包含chunk或 OR 放宽。 - 断言
notification_typecheck 是单值source_status_change。 - 断言
source_statuscheck 是单值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 3:Content 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 输入:
userIdworkIdblockIdblockRevisionsourceCommandId = reqVO.getCommandId()BlockSourceAttributionDO sourceAttribution
Payload 构造:
{
"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。 resourceRefexact 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 4:Content publish worker
目标:
- Claim Content outbox,构造
EventsPublishReqDTO,调用EventsPublishApi,按结果更新 outbox 状态。
文件:
- Create:
ContentEventPublishWorker.java - Test:
ContentEventPublishWorkerTest.java
Events request:
commandId = outbox.commandIdtenantId = outbox.tenantIdownerUserId = outbox.ownerUserIdsourceOwner = contentsourceType = content_block_source_attributionsourceId = String.valueOf(outbox.blockId)sourceRevision = String.valueOf(outbox.blockRevision)eventType = notificationresourceType = outbox.resourceRefTyperesourceId = String.valueOf(outbox.resourceRefId)payloadSummary = parsed exact allowlist payloademittedAt = 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 5:P1R 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:
- 构造
saveBlocksource fact。 - Content outbox service 创建 outbox。
- Worker 成功路径必须调用真实
EventsPublishServiceImpl;只允许使用 in-memory / mockedUnifiedEventMapper承接 unified event 行,禁止 bare fakeEventsPublishApi直接返回 accepted 作为成功证据。 muse_unified_event中 owner 为作品 owner。- SSE visible query 对作品 owner 可见,对其他 owner 不可见。
- unified event
eventType=notification,payloadtype=source_status_change。 - 负断言没有
chunkevent。 - payload 不含正文、sourceSnapshot、authorizationSnapshotId、lineage、license restriction、auditReason、requestHash、resultSnapshot、错误堆栈。
- 同一 block revision duplicate replay 返回同一 eventId / sequenceNo。
- 同一 block 不同 revision 产生不同 notification。
- rejected / blocked -> outbox dead_letter,visible 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
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
先断言新增测试文件存在:
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
运行:
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
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:
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
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 > 0,failures/errors/skipped 均为 0。
5. Dependency tree gate
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:
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
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。