# P1R7e Account Source Owner Propagation 执行版 ## 结论 P1R-7e 只执行 Account/Member source owner propagation 的第四 owner 切片:把 Account quota adjustment terminal fact 转成 Account 本域 Events publish outbox,再由 Account worker 调用 `EventsPublishApi` 写入 `muse_unified_event`,最终通过 `/app-api/muse/events` 对被调整账户用户 SSE 可见。 本执行版不允许把 Events / P1R-7 / Account 推进到 `completed`,不允许修改 OpenAPI、coverage scanner 或 coverage report,不允许把 security event、quota request、integration call 或 account audit 表复用为 Events publish outbox。 ```mermaid flowchart LR Command["adminCreateQuotaAdjustment
commandId + expected snapshot"] --> Quota["muse_member_quota
limit / used / revision"] Command --> Audit["muse_account_audit
admin operation audit"] Command --> Log["muse_member_entitlement_audit_log
one row per resourceType"] Log --> Outbox["muse_account_event_publish_outbox
one row per entitlement audit log"] Outbox --> Worker["Account publish worker
claim/retry/dead_letter"] Worker --> Api["EventsPublishApi
events-api only"] Api --> Unified["muse_unified_event
notification/quota_alert"] Unified --> Stream["/app-api/muse/events
account owner visible SSE"] ``` ## Review Gate P1R-7e 审阅版已通过 fresh review: - Gibbs spec compliance review:PASS,无 P0/P1/P2。 - Ampere quality / feasibility review:PASS,无 P0/P1/P2。 旧 Goodall feasibility review 对早期 security event 候选 FAIL。该反馈已验证有效,并已在审阅版中通过改选 `quota adjustment -> quota_alert` 关闭;旧 FAIL 不计入当前最终审阅版 PASS gate。 P1R-7e 执行版第三轮 fresh review 已通过: - Pauli execution spec compliance review:PASS,无 P0/P1/P2;已验证 scope 未扩大、protected staged/unstaged diff gate、V20 migration 编号前提、P1R mixed gate、Surefire XML 计数、隔离 coverage scanner 和 completed 边界。 - Hooke execution quality / feasibility review:PASS,无 P0/P1/P2;已验证 `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 同事务 audit log / outbox 接入点、Events `quota_alert` 合同、payload/resourceRef/sourceRevision/timestamp 合同、最终验证假绿口。 旧执行版 review 中,Pascal PASS / Schrodinger FAIL、Feynman PASS / Kant FAIL 均因后续执行版修订失效,不计入当前最终 PASS gate;其中有效反馈已经关闭到本执行版。本轮双 PASS 只允许进入 fresh implementer 实现,不代表 P1R-7e 实现完成,也不代表 Events / P1R-7 / Account completed。 非阻塞风险已在本执行版冻结: - 每条 `muse_member_entitlement_audit_log` 发布一条 `quota_alert`,不做 command 聚合。 - `resourceRef.resourceId` 固定为数值型 entitlement audit log id。 - `sourceRevision` 固定使用 Events 现有 `__none__`,不新增 audit log revision。 ## 边界 ### 本轮目标 - 在 Member(Account) owner 内新增独立 Events publish outbox。 - 只接入 `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 首次成功路径。 - 只发布 `muse_member_entitlement_audit_log(change_type=quota_adjustment)` 形成的用户可见 quota alert。 - 每条 entitlement audit log 对应一条 outbox 和一条 Events notification。 - 通过 Account worker 调用 `EventsPublishApi`。 - 用 focused tests、P1R mixed gates、Flyway `_test`、dependency gate、dependency tree 和隔离 coverage scanner 证明链路进入 `needs_verification` 证据层。 ### 本轮非目标 - 不发布 `muse_member_security_event`。 - 不发布 `muse_account_security_event_ack`。 - 不发布 `muse_account_quota_request`。 - 不发布 `muse_account_integration_call`。 - 不发布 `muse_member_usage_record`。 - 不发布 New-API binding、call attribution job、export task、download credential 或 file service 事件。 - 不发布 admin reason 原文、requestHash、before/after snapshot、operatorUserId、correlationId、external provider detail、error stack 或 credential。 - 不抽取 AI / Knowledge / Market / Account 共享 outbox 框架。 - 不为历史 entitlement audit log 做 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 为 `e8ed7d7 test(p1r): 收口 Market 事件传播真实链路门禁`。 - P1R-7b 已提交并 push:`3db5fbe` 与 `230152c`。 - P1R-7c 已提交并 push:`cef686b` 与 `e55618f`。 - P1R-7d 已提交并 push:`b36b153` 与 `e8ed7d7`。 - P1R-7e 审阅版路径为 `docs/agent-specs/2026-06-07-P1R7eAccountSourceOwnerPropagation审阅版.md`。 - P1R-7e 审阅版 fresh review 已双 PASS:Gibbs spec PASS,Ampere quality PASS。 - 当前 coverage 边界仍为 `completedOperations=100`、`needsVerificationOperations=133`、`incompleteOperations=0`、`genericPersistenceOperations=0`、`ssePlaceholderOperations=0`。 - Events `streamEvents` 当前是 `dedicated / needs_verification`。 - Account 33 operations 当前是 `dedicated / needs_verification`。 - 当前受保护文件 diff 为空。 - 当前最新 migration 是 `muse-cloud/sql/muse/V19__extend_market_events_publish_outbox.sql`;P1R-7e 下一 migration 编号应为 `V20`。若实现前 live 文件列表已出现更高版本,必须停止并修订本执行版的 migration 编号。 - `EventsPublishReqDTO` 必填 `commandId`、`tenantId`、`ownerUserId`、`sourceOwner`、`sourceType`、`sourceId`、`eventType`、`payloadSummary`、`emittedAt`。 - `EventsPublishServiceImpl` 当前先按 `commandId` 回放,再按 source tuple 回放。 - `EventsPublishServiceImpl` 当前已声明 `notification` event type。 - `EventsPublishServiceImpl` 当前已声明 `quota_alert` notification type。 - Events OpenAPI `SSENotificationEvent.data.resourceRef.resourceId` 为 int64。 - `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` 成功后写 `muse_member_entitlement_audit_log(changeType=quota_adjustment)`,每个 adjustment 一条 log。 - `MemberEntitlementAuditLogDO` 包含 `id`、`accountUserId`、`changeType`、`sourceOwner`、`sourceId`、`idempotencyKey`、`beforeValueSnapshot`、`deltaValueSnapshot`、`afterValueSnapshot`、`reasonCode`、`reasonMessage`、`operatorUserId`、`auditEventId`。 - `MemberEntitlementAuditLogMapper.selectPageByAccountUserId` 已把 quota ledger 固定为 `quota_adjustment`,并按 OpenAPI source owner allowlist 过滤。 - `muse-module-member-server/pom.xml` 当前没有 `muse-module-events-api` 依赖;实现只允许新增 events-api direct dependency,不得新增 events-server dependency。 - `MemberSecurityEventDO` 虽有 App 读侧和 ack,但当前高敏导出生产写入路径被 fail-closed 阻断,不是 P1R-7e 第一切片 source fact。 ## 推断 - P1R-7e 可复用 P1R-7b/P1R-7c/P1R-7d 的 outbox 状态机、worker claim、stale claim guard、dependency tree gate 和 Flyway `_test` 模式。 - Account 第一切片按 entitlement audit log 逐条发布,比按 command 聚合更稳:source fact 粒度、idempotencyKey 粒度和 resourceType 粒度一致。 - Account outbox 与 entitlement audit log 必须保持两张表 / 两套状态机,否则 Account ledger 查询事实与 Events publish 补偿状态会互相污染。 ## 假设 - P1R-7e 允许 Member server 新增 `muse-module-events-api` direct dependency。 - P1R-7e 允许新增 `muse_account_event_publish_outbox`。 - P1R-7e 第一批通知的 SSE owner 是 `MemberEntitlementAuditLogDO.accountUserId`,不是 admin operator。 - P1R-7e 第一批使用 Events 现有 `notification/quota_alert`,不新增 OpenAPI event type 或 notification type。 ## 设计合同 ### 事件合同 第一批只允许以下 Account source fact 进入 publish outbox: | Account source fact | changeType | Events eventType | notification type | SSE owner | message | |---|---|---|---|---|---| | `muse_member_entitlement_audit_log` | `quota_adjustment` | `notification` | `quota_alert` | `accountUserId` | `额度已调整` | 多 resourceType 同一 command 时,必须按 entitlement audit log 逐条发布,不允许同时做 command 聚合。 ### 幂等合同 - 每条 entitlement audit log 只生成一条 outbox。 - `commandId` 格式:`acct_evt:` + SHA-256(`tenantId|auditLogId|accountUserId|quota_alert`) 前 32 位 hex。 - `outboxId` 格式:`acct_out:` + SHA-256(`tenantId|auditLogId|accountUserId|quota_alert`) 前 32 位 hex。 - Events source tuple: - `sourceOwner = account` - `sourceType = account_quota_adjustment` - `sourceId = entitlementAuditLog.id` - `sourceRevision = __none__` - `eventType = notification` - 同一 entitlement audit log 重放必须得到同一 Events `eventId/sequenceNo`。 - 同一 command 下不同 resourceType 的 entitlement audit log 必须生成不同 commandId 和不同 source tuple。 - Command replay 策略:`adminCreateQuotaAdjustment` 命中 Account command replay 时不补历史 outbox;只有首次成功写入 entitlement audit log 的事务内创建 outbox。历史 backfill 若需要,必须另起任务。 ### Payload 合同 Internal outbox 允许保存以下字段: - `auditLogId` - `sourceCommandId` - `accountUserId` - `operatorUserId` - `resourceType` - `deltaAmount` - `sourceOwner` - `auditEventId` - `requestHash` - `sourceRevision` - `message` - `resourceRefType` - `resourceRefId` Events `payloadSummary` 只能发布: ```json { "type": "quota_alert", "message": "额度已调整", "resourceRef": { "resourceType": "account_quota", "resourceId": 8001 }, "timestamp": "2026-06-07T12:00:00" } ``` 约束: - `resourceRef.resourceId` 必须是数值型 Long / Integer 语义,值为 entitlement audit log id。 - DTO `EventsPublishReqDTO.resourceId` 是 String,可以传 `String.valueOf(auditLogId)`;payload `resourceRef.resourceId` 必须保持 Number,不能是 String。 - `message` 第一切片只允许 `额度已调整`。 - `timestamp` 使用 entitlement audit log 的 `createTime`,缺失时才回退 outbox `createTime`。 - 不得发布 `reasonMessage`、`reasonCode`、`beforeValueSnapshot`、`deltaValueSnapshot`、`afterValueSnapshot`、operatorUserId、correlationId、external provider detail、requestHash、error stack。 - `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、accountUserId、outboxId、auditLogId、attempt、targetStatus、errorCode。 - Worker 默认关闭:`muse.account.events.publish-worker.enabled=false`。 - 调度配置键: - `muse.account.events.publish-worker.initial-delay-ms` - `muse.account.events.publish-worker.fixed-delay-ms` - `muse.account.events.publish-worker.max-attempt` - `muse.account.events.publish-worker.claim-timeout-seconds` ### 失败路径合同 | 路径 | 结果 | |---|---| | 无 entitlement audit log | 不创建 outbox | | changeType 非 `quota_adjustment` | 不创建 outbox | | accountUserId null 或 <= 0 | 不创建 outbox,记录安全日志 | | audit log id null | 不创建 outbox,记录安全日志 | | payload 构造缺 required 字段 | 创建 outbox 前拒绝并记录安全日志,不创建 outbox,不发布,错误码固定为 `ACCOUNT_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 | | command replay | 不补历史 outbox,不重复发布 | ## 涉及文件 ### 生产代码 - Modify: `muse-cloud/muse-module-member/muse-module-member-server/pom.xml` - Modify: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountQuotaServiceImpl.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxService.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceImpl.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorker.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/application/account/AccountEventPayloads.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/dataobject/account/AccountEventPublishOutboxDO.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/dal/mysql/account/AccountEventPublishOutboxMapper.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/framework/config/MuseAccountEventsConfiguration.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/main/java/cn/iocoder/muse/module/member/framework/config/MuseAccountEventsProperties.java` - Create: `muse-cloud/sql/muse/V20__extend_account_events_publish_outbox.sql` ### 测试代码 - Modify: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountQuotaServiceTest.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceTest.java` - Create: `muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorkerTest.java` - Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishDependencyTest.java` - Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishEndToEndTest.java` - Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishFlywayMigrationIT.java` - Create: `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishMigrationSqlTest.java` ### 文档 - Modify: `docs/agent-specs/.agent` - Create: `docs/memorys/YYYY-MM-DD-P1R7eAccountSourceOwnerPropagation真实链路.md` ## 实施任务 ### Task 0:fresh 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 ``` - 若最新 migration 已不是 V19,停止并修订 V20 编号。 - 若 protected diff 非空,停止并汇报。 - 若远端有更新且无法 fast-forward,停止并汇报。 完成条件: - 记录 dirty baseline。 - 确认 protected diff 为空。 - 确认 V20 编号仍可用。 ### Task 1:依赖与配置骨架 目标: - Member server 只新增 `muse-module-events-api` direct dependency。 - 新增 Account Events worker 配置,默认关闭。 文件: - Modify: `muse-cloud/muse-module-member/muse-module-member-server/pom.xml` - Create: `MuseAccountEventsConfiguration.java` - Create: `MuseAccountEventsProperties.java` 要求: - 不新增 `muse-module-events-server`。 - 配置 prefix:`muse.account.events`。 - 默认: - `publish-worker.enabled=false` - `publish-worker.max-attempt=5` - `publish-worker.claim-timeout-seconds=60` - 配置类必须可被 Spring 扫描。 Focused tests: - 配置默认值。 - worker disabled 时 `dispatchOnce()` 不 claim。 ### Task 2:V20 outbox schema 目标: - 新增 `muse_account_event_publish_outbox`,只服务 Account -> Events publish compensation。 文件: - Create: `muse-cloud/sql/muse/V20__extend_account_events_publish_outbox.sql` - Create: `AccountEventPublishOutboxDO.java` - Create: `AccountEventPublishOutboxMapper.java` - Create: `P1rAccountEventsPublishMigrationSqlTest.java` 表字段必须包含: - `id` - `tenant_id` - `outbox_id` - `command_id` - `source_command_id` - `audit_log_id` - `account_user_id` - `operator_user_id` - `resource_type` - `delta_amount` - `source_owner` - `audit_event_id` - `request_hash` - `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, audit_log_id, notification_type)` Check 约束: - `event_type = 'notification'` - `notification_type = 'quota_alert'` - `publish_status in ('queued','running','retryable','published','dead_letter')` - `account_user_id > 0` - `audit_log_id > 0` - `attempt_count >= 0` - `max_attempt > 0` - `resource_ref_id > 0` 索引: - claim index:`publish_status, next_retry_at, claim_expires_at` - owner status index:`account_user_id, publish_status` - audit log index:`audit_log_id` Trigger: - `update_time` trigger。 Mapper 方法: - `insertIgnore` - `selectByTenantIdAndOutboxId` - `selectByTenantIdAndCommandId` - `selectByTenantIdAndAuditLogIdAndNotificationType` - `claimNextPublishOutbox` - `markPublished` - `markRetryable` - `markDeadLetter` SQL gate: - 断言字段、唯一约束、check 约束、索引、trigger。 - 断言非法 `event_type`、`notification_type`、`publish_status`、`account_user_id`、`audit_log_id`、`resource_ref_id` 在 Flyway `_test` 中被 DB 拒绝。 ### Task 3:Account outbox service 目标: - 在 Account quota adjustment 首次成功事务内,为每条 entitlement audit log 创建一条 outbox。 文件: - Create: `AccountEventPublishOutboxService.java` - Create: `AccountEventPublishOutboxServiceImpl.java` - Create: `AccountEventPayloads.java` - Modify: `AccountQuotaServiceImpl.java` - Test: `AccountEventPublishOutboxServiceTest.java` - Test: `AccountQuotaServiceTest.java` 接入点: - `AccountQuotaServiceImpl.adminCreateQuotaAdjustment` - 在每个 `MemberEntitlementAuditLogDO` insert 后调用 outbox service。 - command replay 返回时不得创建 outbox。 Outbox service 输入: - `MemberEntitlementAuditLogDO log` - source command id:必须优先由调用方传入原始 `reqVO.getCommandId()`;只有回查历史 log 且调用方无法传入时,才允许从 `log.idempotencyKey` 用 `lastIndexOf(':')` 切出 commandId,禁止用第一个冒号切分。 - request hash:允许 outbox 内部保存,不允许 Events payload 发布。 Payload 构造: ```json { "type": "quota_alert", "message": "额度已调整", "resourceRef": { "resourceType": "account_quota", "resourceId": 8001 }, "timestamp": "2026-06-07T12:00:00" } ``` Payload validator: - 顶层 exact keys:`type/message/resourceRef/timestamp`。 - `resourceRef` exact keys:`resourceType/resourceId`。 - `type=quota_alert`。 - `message` 只允许 `额度已调整`。 - `resourceType=account_quota`。 - `resourceId` 必须是 Number。 - 不允许 requestHash、reason、snapshot、operator、correlation、error 等字段。 错误码: - payload invalid 固定为 `ACCOUNT_EVENTS_PAYLOAD_INVALID`。 Focused tests: - 首次 successful quota adjustment 为每个 audit log 创建 outbox。 - command replay 不创建 outbox。 - invalid owner 不创建 outbox并记录安全日志。 - payload exact allowlist,敏感字段负断言。 - 多 resourceType 产生多条不同 outbox。 - configured maxAttempt 生效。 - timestamp 使用 audit log createTime,缺失时回退 outbox createTime。 ### Task 4:Account publish worker 目标: - Claim Account outbox,构造 `EventsPublishReqDTO`,调用 `EventsPublishApi`,按结果更新 outbox 状态。 文件: - Create: `AccountEventPublishWorker.java` - Test: `AccountEventPublishWorkerTest.java` Events request: - `commandId = outbox.commandId` - `tenantId = outbox.tenantId` - `ownerUserId = outbox.accountUserId` - `sourceOwner = account` - `sourceType = account_quota_adjustment` - `sourceId = String.valueOf(outbox.auditLogId)` - `sourceRevision = __none__` - `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。 - terminal update 使用 claimed attempt ownership。 - stale terminal update 返回 0 时记录诊断日志。 ### Task 5:P1R executable gates 目标: - 用 server 侧 gate 证明依赖方向、focused E2E、V20 migration 和既有 owner gate 不回退。 文件: - Create: `P1rAccountEventsPublishDependencyTest.java` - Create: `P1rAccountEventsPublishEndToEndTest.java` - Create: `P1rAccountEventsPublishFlywayMigrationIT.java` - Create: `P1rAccountEventsPublishMigrationSqlTest.java` Dependency test: - Member server 有 direct `muse-module-events-api`。 - Member server 无 `muse-module-events-server`。 - Events server 无 AI / Knowledge / Market / Member / Content server。 E2E focused test: - 构造 quota adjustment source fact。 - Account outbox service 创建 outbox。 - Worker 发布到 fake / in-memory Events publish API 或真实 Events service harness。 - `muse_unified_event` 中 owner 为 accountUserId。 - SSE visible query 对 account owner 可见,对其他 owner 不可见。 - payload 不含 requestHash、reason、snapshot、operator、correlation。 - 多 resourceType 产生多条独立通知。 - rejected -> outbox dead_letter,visible query 为空。 - stale claim terminal update 被忽略。 Flyway `_test`: - 使用真实 PostgreSQL `_test` 数据库。 - 跑到 V20。 - 验证 table / indexes / constraints / trigger。 - 验证非法 insert 被 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 ``` 期望:62/62 或 live reactor 全量 modules success。 #### 2. Account focused tests 先断言新增测试文件存在: ```bash cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0 && \ test -f muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishOutboxServiceTest.java && \ test -f muse-cloud/muse-module-member/muse-module-member-server/src/test/java/cn/iocoder/muse/module/member/application/account/AccountEventPublishWorkerTest.java && \ test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishMigrationSqlTest.java && \ test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishDependencyTest.java && \ test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishEndToEndTest.java && \ test -f muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAccountEventsPublishFlywayMigrationIT.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-member/muse-module-member-server -am \ -Dtest=AccountQuotaServiceTest,AccountEventPublishOutboxServiceTest,AccountEventPublishWorkerTest \ -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=P1rAccountEventsPublishMigrationSqlTest,P1rAccountEventsPublishDependencyTest,P1rAccountEventsPublishEndToEndTest,P1rAccountRealApiGateTest,P1rAccountRouteOwnershipTest,P1rMarketEventsPublishMigrationSqlTest,P1rMarketEventsPublishDependencyTest,P1rMarketEventsPublishEndToEndTest,P1rKnowledgeEventsPublishMigrationSqlTest,P1rKnowledgeEventsPublishDependencyTest,P1rKnowledgeEventsPublishEndToEndTest,P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \ -Dsurefire.failIfNoSpecifiedTests=false ``` 必须检查 surefire XML,新增 Account 三个 P1R gate 和既有 Account owner gate 测试类 `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.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=P1rAccountEventsPublishFlywayMigrationIT \ -Dflyway.postgresql.transactional.lock=false \ -Dp1r.flyway.locations=filesystem:sql/muse \ -Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/muse_p1r7e_account_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 = 20。 - target_schema_version = 20。 - `muse_account_event_publish_outbox` 存在。 - 目标测试 XML tests > 0,failures/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-member/muse-module-member-server -DskipTests dependency:tree > /tmp/p1r7e-member-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/p1r7e-events-server-dependency-tree.txt && \ rg "muse-module-events-api" /tmp/p1r7e-member-server-dependency-tree.txt && \ ! rg "muse-module-events-server" /tmp/p1r7e-member-server-dependency-tree.txt && \ ! rg "muse-module-(ai|knowledge|market|member|content)-server" /tmp/p1r7e-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/p1r7e-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`。 - Account 33 operations 仍为 `dedicated / needs_verification`。 #### 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 新增: - `docs/memorys/2026-06-07-P1R7eAccountSourceOwnerPropagation真实链路.md` 必须记录: - 目标和非目标。 - 关键实现文件。 - focused tests 数量。 - P1R mixed gates 数量。 - Flyway `_test` 结果。 - dependency tree gate。 - isolated coverage scanner 目录和 summary。 - protected diff。 - fresh implementation review 结果。 - 明确说明仍不代表 Events / P1R-7 / Account completed。 #### 9. Fresh implementation review 实现完成和最终验证后必须重新派发: - fresh spec / correctness review。 - fresh quality / data-integrity / testing review。 双 PASS 前不得提交、push 或推进 completed。 ## 完成条件 P1R-7e 实现完成条件: - Account quota adjustment terminal fact -> Account outbox -> worker -> `EventsPublishApi` -> `muse_unified_event` -> SSE owner-visible focused evidence 成立。 - Member server 只依赖 events-api,不依赖 events-server。 - Events server 不反向依赖 AI / Knowledge / Market / Member / Content server。 - V20 SQL 静态 gate 和 Flyway `_test` 通过。 - focused tests、P1R mixed gates、dependency tree、isolated coverage scanner、protected diff、`git diff --check` 均通过。 - fresh implementation spec review + quality review 双 PASS。 仍不得称为 completed: - Account 33 operations。 - Events `streamEvents`。 - 总 P1R-7。 - Market / Knowledge / AI 之外任何未单独批准的域。