oh-my-muse/docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation执行版.md

1142 lines
81 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.

# P1R7b Source Owner Propagation 执行版
## 结论
P1R-7b 是 `docs/agent-specs/2026-06-06-P1R7SourceOwnerPropagation全局执行版.md` 下的 AI terminal event 第一切片子计划。本文件只修订计划,不实现代码,不运行测试,不提交,不推送,不推进 Events / P1R-7 / Market completed approval。
当前 AI 草稿实现只能作为“修订沿用候选”,不能作为已通过、已完成或可继续实现的事实。必须先让本子计划通过 fresh spec review + fresh quality / feasibility review 双 PASS才允许处理当前 AI 草稿实现;双 PASS 前不得继续 AI 草稿测试、提交、推送或把任何 dirty baseline 清理成“完成状态”。
执行主线固定为:
```mermaid
flowchart LR
Fact["AI owner 本域事实<br/>muse_ai_task_event done/error"] --> Outbox["AI publish outbox/job<br/>muse_ai_event_publish_outbox"]
Outbox --> Worker["AI publish worker<br/>claim + retry + dead_letter"]
Worker --> Api["EventsPublishApi<br/>只依赖 muse-module-events-api"]
Api --> Store[("muse_unified_event<br/>accepted/rejected/blocked")]
Store --> Query["Events visible query<br/>tenant + ownerUserId + sequence"]
Query --> SSE["/app-api/muse/events<br/>SSE 可见"]
```
执行版完成后仍只是计划;必须经过 fresh spec review + fresh quality review 双 PASS才允许进入实现。实现完成后也只推进 source propagation evidence / needs_verification不自动把 Events、P1R-7 或 Market 32 operations 标为 `completed`
本轮计划修订的硬边界:
1. 只允许修改本文件和 `docs/agent-specs/.agent`
2. 不修改任何 Java / SQL / test / `pom.xml` 文件。
3. 不修改 OpenAPI、scanner、coverage JSON/Markdown。
4. 不清理、不回退、不废弃当前 dirty baseline任何废弃草稿动作都必须等用户明确批准。
5. 不引入独立 source 模块,也不把 shared publish 库作为当前 P1R-7b 必做项。
## 目标与范围边界
目标:
1. 在全局 P1R7 Source Owner Propagation 执行版约束下,以 AI task terminal event 为第一切片,证明真实 owner 事实可以传播到统一 Events 投影。
2. 只发布用户可见 terminal `done` / `error` 摘要事件payload 严格匹配 `docs/api-contracts/events/openapi.yaml` 现有 schema。
3. 新增 AI 本域 publish outbox/job异步 worker 调用 `EventsPublishApi`
4. 建立可验证链路AI owner 本域事实 -> AI publish outbox/job -> AI worker -> `EventsPublishApi` -> `muse_unified_event` -> `/app-api/muse/events` SSE 可见。
5. 形成 TDD、focused tests、migration tests、dependency gate、coverage gate、review gate 的可执行实施计划。
范围边界:
1. 只在 `/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0` 工作。
2. 当前分支必须保持 `dev/1.0.0`
3. 当前 dirty baseline 已包含 AI 草稿代码、迁移、测试和 `docs/agent-specs/` 文档草稿;本子计划只记录处置策略,不清理、回退或改写 dirty/untracked baseline。
4. 不在 `/Users/qingse/Sync/local-git/oh-my-muse` 写任何文件。
5. 不修改受保护文件:
- `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`
## 非目标
1. 不实现代码;本文件只定义执行计划。
2. 不修改任何 OpenAPI 合同。
3. 不修改 scanner 或 coverage JSON/Markdown 来达成 coverage。
4. 不把 Events、P1R-7、Market 32 operations 标为 `completed`
5. 不复用 AI runtime job 作为 Events publish 队列。
6. 不复用 Market projection outbox。
7. 不同时改造 Knowledge、Market、Member(Account) 的 source owner propagation。
8. 不让 Events server 反向依赖 AI / Knowledge / Market / Member / Content server。
9. 不让统一 SSE Controller 扫描 AI 业务表。
10. 不处理 Knowledge / Market / Account / Content 的 propagation 实现。
11. 不新增独立 source 模块,不把 shared publish 库设为本切片必做前置。
12. 不把 `DuplicateKeyException` 捕获作为正常幂等路径。
## 已验证事实
1. `git status --short` 在正确 worktree 显示 AI P1R-7b 草稿代码、迁移、测试和 `docs/agent-specs/` 文档草稿;这些 dirty/untracked 文件尚未通过本子计划 fresh review也尚未运行本子计划验证。
2. `git branch --show-current` 输出 `dev/1.0.0`
3. `git log -1 --oneline` 输出 `fa29753 test(p1r): 收口 Events SSE 门禁`
4. `AGENTS.md` 只引用 `@CLAUDE.md``CLAUDE.md` 明确本仓是设计文档 SSOTSource 传播采用事件驱动 + 各模块自治,统一实时通信为 SSE。
5. `docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation审阅版.md` 推荐 P1R-7b 只选 AI task terminal event 第一切片,`done` / `error` 为最小事件类型,不进入 completed approval。
6. `docs/memorys/2026-06-05-P1R7aEventsSSE真实API收口.md` 记录 P1R-7a 未接入 source owner propagation未运行 source owner publish testsEvents 和 Market 仍不得写成 `completed`
7. `docs/superpowers/plans/2026-06-05-P1R-7a-events-sse.md` 固定跨 owner 发布策略为 source owner 本域 outbox + worker 调用 Events APIEvents server 不反向拉取 source owner 数据。
8. Coverage JSON 当前 `summary`total 233、completed 100、needsVerification 133、incomplete 0、genericPersistence 0、ssePlaceholder 0。
9. Coverage operation 明细显示 `events streamEvents GET /app-api/muse/events = dedicated / needs_verification / P1R-7 End-to-End Acceptance`
10. Coverage operation 明细显示 Market 32 个 operation 的 completionStatus 均为 `needs_verification`
11. `EventsPublishApi.publish(EventsPublishReqDTO)` 已存在于 `muse-cloud/muse-module-events/muse-module-events-api/src/main/java/cn/iocoder/muse/module/events/api/publish/EventsPublishApi.java`
12. `EventsPublishReqDTO` 字段包含 `commandId``tenantId``ownerUserId``sourceOwner``sourceType``sourceId``sourceRevision``eventType``resourceType``resourceId``payloadSummary``emittedAt`
13. `EventsPublishServiceImpl` 已实现 commandId 优先、source tuple 次级的幂等回查,非法 payload 写入 `rejected` 并不进入可见 SSE 查询。
14. `UnifiedEventMapper.selectVisibleEventsForOwner` 只查询 `publish_status = accepted`、同租户、同 ownerUserId、未删除、`sequenceNo` 大于 cursor 的事件。
15. `EventsStreamServiceImpl``done` event 投影为 `taskId``suggestionId` 和可选 `summary`;将 `error` event 投影为 `code``message`、可选 `detail`、可选 `retryable`
16. `docs/api-contracts/events/openapi.yaml` 已声明 `SSEDoneEvent` 必填 `taskId``suggestionId`,可选 `summary``SSEErrorEvent` 必填 `code``message`,可选 `detail``retryable`
17. `muse_ai_task_event` 已由 V12 创建,字段包含 `task_id``sequence_no``event_type``task_status``owner_user_id``actor_user_id``job_id``correlation_id``payload_summary``error_code``error_message``emitted_at`,并有 terminal 唯一约束 `uk_muse_ai_task_event_terminal`
18. `MuseAiTaskEventMapper.selectTerminalByTaskId` 已限定 terminal event 为 `done` / `error`
19. `MuseAiRuntimeProjectionService.applyRuntimeResponse` 成功终态写入 `done` task event非可重试失败终态写入 `error` task event可重试失败只让 job 回到 `queued`,不写 terminal event。
20. `MuseJobServiceImpl.appendTaskCancellationEventIfAbsent` 在 cancel 路径写入 `eventType = error``taskStatus = cancelled``errorCode = AI_TASK_CANCELLED``errorMessage = AI task cancelled`,并且代码注释明确取消原因不能进入 SSE payload。
21. `MuseAiRuntimeJobDispatcher` 采用 `@Scheduled(initialDelayString=..., fixedDelayString=...)` 调用 `dispatchOnce()` 的最小 worker 生命周期风格。
22. `MuseSourceEventServiceImpl.retrySourceEvent` 只提交 `source_event_retry` job并有注释说明不能把来源传播伪造成完成。
23. 当前 dirty `muse-module-ai-server/pom.xml` 已出现 `muse-module-events-api` 依赖;它仍只是 AI 草稿的一部分,必须在实现阶段重新通过 dependency tree 证明只依赖 events-api、不依赖 events-server。
24. `BaseDO` 暴露 `createTime/updateTime/creator/updater/deleted``TenantBaseDO` 继承 `BaseDO` 并新增 `tenantId`;项目 PostgreSQL DDL 约定字段名为 `create_time/update_time`
25. `muse-cloud/sql/muse/V1__init_content_schema.sql``update_updated_at_column()` 更新的是 `NEW.update_time`
26. 当前 `muse-cloud/sql/muse` 文件列表按数字排序最新已包含未提交草稿 `V17__extend_ai_events_publish_outbox.sql`;它不是已通过 migration 证据,后续必须在 P1R-7b 实现阶段决定修订沿用该 V17或经用户明确批准后废弃重做。
27. 当前草稿的 `MuseAiEventPublishOutboxServiceImpl` 存在捕获 `DuplicateKeyException` 作为 outbox 创建幂等回查路径的实现痕迹;这违反全局执行版“`ON CONFLICT DO NOTHING` / `insertIgnore` + 回查”的幂等合同,必须修订。
28. 当前受保护文件定向 `git status --short` 无输出:
- `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`
## 当前 dirty 文件处置表
结论:以下 dirty baseline 不是完成证据。P1R-7b 子计划双 review PASS 前,所有 AI 草稿代码、迁移和测试都只能停留在候选或待修订状态;任何废弃、清理或回退都必须等用户明确批准。
| path | 处置策略 | 理由 | 是否需要用户批准 |
|---|---|---|---|
| `muse-cloud/muse-module-ai/muse-module-ai-server/pom.xml` | 修订沿用候选 | 可能需要新增 `muse-module-events-api` 依赖,但必须重新证明只依赖 api、不依赖 server。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionService.java` | 修订沿用候选 | 可能承接 terminal fact 同事务创建 outbox但必须重新验证事务边界、payload allowlist 和非 terminal 不发布。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceImpl.java` | 修订沿用候选 | cancellation terminal `error` 可作为用户可见摘要候选,但必须保证取消原因不进入 SSE payload。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionServiceTest.java` | 修订沿用候选 | 可作为 terminal fact/outbox 绑定测试素材,但必须补本全局合同断言。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceTest.java` | 修订沿用候选 | 可作为 cancellation `error` 安全 payload 测试素材,但不能替代 fresh review。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxService.java` | 修订沿用候选 | AI 本域 publish outbox service 职责符合方向,但接口必须复核是否只覆盖 `done/error` terminal 摘要。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceImpl.java` | 必须修订 | 当前草稿捕获 `DuplicateKeyException` 作为正常幂等路径P1R-7b 必须改为 `ON CONFLICT DO NOTHING` / `insertIgnore` + 回查,避免 PostgreSQL 同事务异常导致 terminal fact 回滚风险。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/dataobject/muse/MuseAiEventPublishOutboxDO.java` | 修订沿用候选 | DO 可承接 AI outbox 表,但字段必须与全局 outbox 合同、BaseDO/TenantBaseDO 字段约定和 V17 migration 一致。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapper.java` | 必须修订 | mapper 必须提供 `insertIgnore` 或等价 `ON CONFLICT DO NOTHING`,并用原子 claim 支持 queued/retryable/stale running。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceTest.java` | 必须修订 | 现有测试如断言 `DuplicateKeyException` 幂等路径,必须改为 `insertIgnore` 返回 0 + 回查;否则会固化错误实现。 | 否;废弃或回退需要 |
| `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapperTest.java` | 修订沿用候选 | 可承接 mapper claim / insertIgnore SQL 断言,但必须覆盖全局 outbox 状态机。 | 否;废弃或回退需要 |
| `muse-cloud/sql/muse/V17__extend_ai_events_publish_outbox.sql` | 修订沿用候选 | V17 版本号需在实现前重新 live 查证;表结构必须对齐 outbox 合同、claim 索引和 update_time trigger。 | 否;废弃或回退需要 |
| `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishMigrationSqlTest.java` | 修订沿用候选 | 可作为 migration SQL 文本门禁,但必须补 V17/字段/索引/trigger/独立测试库保护断言。 | 否;废弃或回退需要 |
| `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishFlywayMigrationIT.java` | 修订沿用候选 | 可作为 Flyway `_test` 验证素材,但必须证明只跑独立 `_test` 库且 surefire XML 有真实 test count。 | 否;废弃或回退需要 |
| `docs/agent-specs/*` | 保持不动 | 本轮只允许修订本执行版和 `.agent`;其他审阅版/全局执行版/草稿不能被顺手改写。 | 否;废弃或删除需要 |
废弃规则:
1. 上表任何代码、迁移、测试草稿如果后续选择“废弃重做”,必须先向用户说明废弃范围、原因、替代方案和回滚影响,并取得明确批准。
2. 未获批准前,不得删除 untracked 文件,不得 `git checkout --` 回退已修改文件,不得用清理 dirty baseline 的方式制造“计划已收口”的假象。
## 推断
1. AI 是 P1R-7b 的最小 owner 切片,因为它已有 `muse_ai_task_event` 终态事实、ownerUserId、tenantId、sequence/replay 基础和 focused tests。
2. 需要新增清晰职责的 AI publish outbox/job因为现有 AI runtime job 的职责是调用 New-API runtime不是统一 Events 发布补偿。
3. `done` payload 不能直接复用 `MuseAiRuntimeProjectionService.donePayload`,因为现有 payload 缺少 Events OpenAPI 必填的 `taskId`,需要由 outbox mapper/service 从 task event + task/suggestion 事实组合生成 allowlist payload。
4. `error` payload 可以从 `MuseAiTaskEventDO.errorCode/errorMessage/payloadSummary.retryable` 归一化生成,但必须先做敏感词和字段 allowlist 过滤。
5. 如果 worker 与 terminal fact 写入不在同一事务内补 outbox存在终态事实已落库但未发布的不可追踪缺口因此 outbox 写入必须和 terminal fact 绑定在同一事务边界内。
## 假设
1. P1R-7b 允许在 AI owner 内新增 `muse_ai_event_publish_outbox` 或等价职责表。
2. 实现阶段可以只在 AI 模块新增 `muse-module-events-api` 编译依赖,不需要引入 events-server。
3. 实现阶段可以新增 focused integration test 或 mapper-level test 来证明 `muse_ai_event_publish_outbox` migration、claim、retry、dead_letter 和 SSE 可见链路。
4. P1R `_test` PostgreSQL 验证仍使用项目既有 `~/.config/muse-repo/infra.env` 方式,但运行前必须确认目标库是独立测试库。
## 全局架构主线
1. AI owner 只写本域事实和本域 outbox不直接写 `muse_unified_event`
2. AI worker 只调用 `EventsPublishApi`,不依赖 Events server 实现类。
3. Events owner 只接收发布 DTO、做幂等、payload 校验、脱敏、落库和 SSE 可见性过滤。
4. SSE 查询只读 `muse_unified_event`,不反查 AI 表。
5. replay 只能重放 AI 本域 outbox 记录;禁止 operator 或补偿任务绕过 source owner 直接向 Events 注入伪造 payload。
## 继承全局合同
结论P1R-7b 不重新发明合同,只把全局 P1R7 Source Owner Propagation 合同投影到 AI terminal event 第一切片。若本文件与全局执行版冲突,以全局执行版为准;后续实现前必须先修订本文件并重新 review。
### 依赖合同
1. `muse-module-ai-server` 只允许新增或保留 `muse-module-events-api` 依赖。
2. `muse-module-ai-server` 禁止依赖 `muse-module-events-server`
3. `muse-module-events-server` 禁止依赖 AI / Knowledge / Market / Member / Content server。
4. `muse-server` 可以装配 AI server 与 Events server但不能把装配关系写成业务反向依赖。
5. P1R-7b 实现前和实现后都必须运行 dependency tree 验证;验证命令只作为框架写入本计划,本轮不运行。
### 发布 envelope 合同
1. `commandId` 必须稳定、短、幂等,推荐 `ai_evt:<sha256-32>`,长度 `<= 128`
2. `tenantId``ownerUserId``sourceOwner=ai``sourceType=ai_task_event``sourceId``sourceRevision``eventType``resourceType/resourceId``payloadSummary``emittedAt` 都必须在 outbox 中可追溯。
3. `eventType` 只允许使用 Events OpenAPI 既有 `done` / `error`,不得新增 AI 私有事件类型绕过合同。
4. `sourceRevision` 使用 terminal event `sequence_no` 字符串;为空时不得创建可发布 outbox。
5. `emittedAt` 使用 AI terminal fact 发生时间,不使用 worker 发布时间替代事实时间。
### 用户可见事件判定
1. P1R-7b 只发布用户可见 terminal `done/error` 摘要。
2. 可重试 runtime failure、worker heartbeat、projection rebuild、source retry job、内部调度状态不进入统一 SSE。
3. cancellation 只能作为 `error` 摘要发布,且只包含 `AI_TASK_CANCELLED``AI task cancelled``retryable=false`,不得包含用户提交的取消原因或内部审计备注。
4. payload 需要完整 prompt、provider raw、正文、授权详情、风控原始证据才能解释时必须 fail-closed不得发布到 SSE。
5. 本切片不处理 Knowledge / Market / Account / Content 的用户可见事件。
### Outbox 合同
1. AI outbox 必须支持 `queued/running/retryable/published/dead_letter` 或全局执行版认可的等价状态。
2. claim 必须使用 `FOR UPDATE SKIP LOCKED` 或等价原子领取机制,并覆盖 queued、到期 retryable、stale running。
3. claim 时递增 `attempt_count`;失败处理不得二次递增 attempt。
4. 必须有 `max_attempt``next_retry_at``claimed_at``claim_expires_at`、claim 索引、固定退避策略。
5. 幂等插入必须使用 `ON CONFLICT DO NOTHING` / `insertIgnore` + 回查;禁止捕获 `DuplicateKeyException` 作为正常幂等路径。
6. `dead_letter` 不自动 replay只有确认是临时故障的记录才能由用户批准后重置为 `retryable`
7. 状态转换必须有中文日志,包含 owner、tenantId、ownerUserId、source tuple、outboxId、attempt、状态转换和安全错误摘要。
### Payload 合同
1. `done` payload 只允许 `taskId``suggestionId`、可选安全短 `summary`
2. `error` payload 只允许 `code``message`、可选安全 `detail`、可选 `retryable`
3. `taskId``suggestionId` 必须满足 Events OpenAPI int64 语义;不满足时 fail-closed 到 `dead_letter`,不得调用 `EventsPublishApi`
4. 禁止 prompt、userInstruction、provider request/response raw body、token、apiKey、authorization、bearer、secret、runtimePermissionEnvelope、source private context、full output、contentSnapshot 正文进入 payload。
5. payload invalid、Events rejected、Events blocked 都不得进入 SSE 可见查询。
## 涉及模块与文件路径
### 必须只读核对
- `AGENTS.md`
- `CLAUDE.md`
- `docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation审阅版.md`
- `docs/memorys/2026-06-05-P1R7aEventsSSE真实API收口.md`
- `docs/superpowers/plans/2026-06-05-P1R-7a-events-sse.md`
- `docs/api-contracts/events/openapi.yaml`
- `docs/superpowers/reports/p1r-api-coverage.json`
- `docs/superpowers/reports/p1r-api-coverage.md`
### 实现阶段允许修改
说明:本节的“新增/修改”描述 P1R-7b clean baseline 目标写集;如果当前 dirty baseline 已经存在同名草稿文件,后续 implementer 必须先按“当前 dirty 文件处置表”判断修订沿用或用户批准后废弃重做,不能因为文件已存在就跳过 TDD、验证或 fresh review。
- `muse-cloud/sql/muse/V17__extend_ai_events_publish_outbox.sql`,当前 dirty baseline 已存在该草稿;实现阶段若选择修订沿用,必须重新证明版本号、字段、索引和 Flyway `_test` 全部合规;若选择废弃重做,必须先获得用户明确批准。
- `muse-cloud/muse-module-ai/muse-module-ai-server/pom.xml`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/dataobject/muse/MuseAiEventPublishOutboxDO.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapper.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxService.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceImpl.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishWorker.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionService.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceImpl.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/framework/ai/config/MuseAiProperties.java`,如果实现阶段复用项目集中配置承载 worker 开关;否则必须在实现报告写明等价配置承载点。
### 实现阶段允许新增或修改测试
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapperTest.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceTest.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishWorkerTest.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionServiceTest.java`
- `muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceTest.java`
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishMigrationSqlTest.java`
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishFlywayMigrationIT.java`
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishDependencyTest.java`
- `muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishEndToEndTest.java`
### 必须保持不修改
- `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`
## 数据流与依赖方向
```mermaid
sequenceDiagram
participant Runtime as MuseAiRuntimeProjectionService
participant TaskEvent as muse_ai_task_event
participant Outbox as muse_ai_event_publish_outbox
participant Worker as MuseAiEventPublishWorker
participant Api as EventsPublishApi
participant Unified as muse_unified_event
participant SSE as /app-api/muse/events
Runtime->>TaskEvent: 同事务写 done/error terminal fact
Runtime->>Outbox: 同事务写 queued publish job
Worker->>Outbox: SKIP LOCKED claim queued/retryable
Worker->>Api: publish(EventsPublishReqDTO)
Api->>Unified: 幂等落库 accepted/rejected/blocked
Api-->>Worker: eventId/sequenceNo/status/duplicate
Worker->>Outbox: published 或 dead_letter 或 retryable
SSE->>Unified: 只读 accepted + tenant + ownerUserId + sequence
```
依赖方向:
1. `muse-module-ai-server` 可以新增 `muse-module-events-api` 依赖。
2. `muse-module-ai-server` 禁止依赖 `muse-module-events-server`
3. `muse-module-events-server` 禁止依赖 AI / Knowledge / Market / Member / Content server。
4. `muse-server` 继续装配 AI server 和 Events server。
## 数据契约与 payload allowlist
统一发布 DTO
| 字段 | AI outbox 来源 | 规则 |
|---|---|---|
| `commandId` | outbox.command_id | 必填,冻结为短格式或 hash 格式,推荐 `ai_evt:<sha256-32>`;禁止使用包含 tenantId/taskId/sourceRevision/eventType 的长拼接格式作为推荐格式,测试必须覆盖长度 `<= 128` |
| `tenantId` | outbox.tenant_id | 必填 |
| `ownerUserId` | task event owner_user_id | 必填 |
| `sourceOwner` | 常量 `ai` | 必填 |
| `sourceType` | 常量 `ai_task_event` | 必填 |
| `sourceId` | task event task_id | 必填,字符串形式 |
| `sourceRevision` | task event sequence_no | 必填,字符串形式;为空时不允许生成 outbox |
| `eventType` | task event event_type | 只允许 `done` / `error` |
| `resourceType` | 常量 `ai_task` | 必填或可空,但写入 outbox 时保持一致 |
| `resourceId` | task event task_id | 字符串形式 |
| `payloadSummary` | allowlist mapper 生成 | 只允许下方字段 |
| `emittedAt` | task event emitted_at | 必填 |
全局 payload 规则:
1. `muse_ai_task_event.task_id``VARCHAR`,但 SSE `done` payload 的 `taskId` 必须是 int64 语义数字P1R-7b 最小规则要求 `done``error` terminal event 在进入 Events publish 前都先校验 taskId 可解析为 int64。
2. 非数字 taskId 必须 fail-closed 到 AI outbox `dead_letter``last_error_code = AI_EVENTS_TASK_ID_NOT_NUMERIC`,不得调用 `EventsPublishApi`,不得发布 `done``error` 到 SSE 可见事件。
3. `command_id` 必须由稳定 source tuple 计算短 hash例如对 `tenantId|taskId|sourceRevision|eventType` 做 sha256 后取 32 位十六进制摘要并加 `ai_evt:` 前缀;测试必须证明生成结果长度 `<= 128`
`done` payload allowlist
```json
{
"taskId": 3001,
"suggestionId": 2001,
"summary": "AI task completed"
}
```
规则:
1. `taskId` 必须是 int64 语义数字。
2. `suggestionId` 必须是 int64 语义数字;如果 task 没有 suggestionId则不得发布 `done`outbox 写入或保持 `dead_letter``last_error_code = AI_EVENTS_DONE_PAYLOAD_INCOMPLETE`,并且不得进入 SSE 可见事件。
3. `summary` 可选,必须是安全短文本;不得来自 provider raw body 或完整输出正文。
`error` payload allowlist
```json
{
"code": "AI_NEW_API_UNAVAILABLE",
"message": "AI task failed",
"detail": "runtime unavailable",
"retryable": false
}
```
规则:
1. `code` 必须来自 AI 本域归一化错误码,缺失时使用 `AI_TASK_FAILED`
2. `message` 必须是安全错误摘要,不能包含 provider 原始响应、prompt、token、授权头或私有上下文。
3. `detail` 可选,只允许开发安全摘要;如包含敏感关键词则丢弃。
4. `retryable` 可选terminal error 对应非可重试失败时为 `false`
5. cancellation terminal event 是 `MuseJobServiceImpl.appendTaskCancellationEventIfAbsent` 写出的合同内 `error` 事件payload allowlist 只能包含 `code = AI_TASK_CANCELLED``message = AI task cancelled``retryable = false`;不得包含 `detail`,不得携带 admin/app 用户提交的取消原因。
禁止字段:
1. prompt / userInstruction 原文。
2. provider raw body / provider request body / provider response body。
3. token / apiKey / authorization / bearer / secret。
4. runtimePermissionEnvelope 原文。
5. source private context / full output / contentSnapshot 正文。
6. 未在 `docs/api-contracts/events/openapi.yaml` 声明的扩展字段。
## 迁移与表设计计划
表名:`muse_ai_event_publish_outbox`
当前 dirty baseline 已存在未提交 `V17__extend_ai_events_publish_outbox.sql` 草稿。实现前必须重新 live 查证 `muse-cloud/sql/muse` 版本号:如果 V17 草稿仍存在,只能修订沿用或经用户明确批准后废弃重做;如果上游已新增后续版本,必须先停下报告冲突,并按用户确认选择下一 migration 号。
字段计划:
| 字段 | 类型 | 约束 |
|---|---|---|
| `id` | `BIGINT GENERATED ALWAYS AS IDENTITY` | 主键 |
| `tenant_id` | `BIGINT` | `NOT NULL` |
| `owner_user_id` | `BIGINT` | `NOT NULL` |
| `task_id` | `VARCHAR(128)` | `NOT NULL` |
| `source_event_id` | `BIGINT` | `NOT NULL`,引用 `muse_ai_task_event.id` 这个 AI task event 本域自增主键 |
| `source_revision` | `VARCHAR(80)` | `NOT NULL`,使用 task event `sequence_no` 字符串 |
| `event_type` | `VARCHAR(32)` | `NOT NULL CHECK IN ('done','error')` |
| `payload_summary` | `JSONB` | `NOT NULL DEFAULT '{}'::jsonb` |
| `command_id` | `VARCHAR(128)` | `NOT NULL` |
| `publish_status` | `VARCHAR(32)` | `NOT NULL CHECK IN ('queued','running','published','retryable','dead_letter')` |
| `attempt_count` | `INT` | `NOT NULL DEFAULT 0` |
| `claimed_at` | `TIMESTAMP` | 可空worker claim 时写入当前时间 |
| `claim_expires_at` | `TIMESTAMP` | 可空worker claim 时写入 `now + lease`,用于 crash-after-claim 后回收 stale running |
| `next_retry_at` | `TIMESTAMP` | 可空 |
| `last_error_code` | `VARCHAR(64)` | 可空 |
| `last_error_message` | `TEXT` | 可空,必须是安全摘要 |
| `published_event_id` | `VARCHAR(128)` | 可空 |
| `published_sequence_no` | `BIGINT` | 可空 |
| `create_time` | `TIMESTAMP` | `NOT NULL DEFAULT CURRENT_TIMESTAMP`,对齐 `BaseDO.createTime` / 项目 DDL 字段约定 |
| `update_time` | `TIMESTAMP` | `NOT NULL DEFAULT CURRENT_TIMESTAMP`,对齐 `BaseDO.updateTime` / 项目 DDL 字段约定 |
| `creator` | `VARCHAR(64)` | `NOT NULL DEFAULT ''`,如项目基类需要 |
| `updater` | `VARCHAR(64)` | `NOT NULL DEFAULT ''`,如项目基类需要 |
| `deleted` | `BOOLEAN` | `NOT NULL DEFAULT FALSE` |
约束与索引:
1. `uk_muse_ai_event_publish_outbox_command``UNIQUE (tenant_id, command_id)`
2. `uk_muse_ai_event_publish_outbox_source``UNIQUE (tenant_id, task_id, source_revision, event_type)`
3. `idx_muse_ai_event_publish_outbox_claim``(publish_status, claim_expires_at, next_retry_at, create_time, id)`,过滤 `deleted = FALSE`;必须支持 claim 查询同时覆盖 `queued`、到期 `retryable`、以及 `running AND claim_expires_at < now` 的 stale running。
4. `idx_muse_ai_event_publish_outbox_owner_status``(tenant_id, owner_user_id, publish_status, create_time)`
5. `trg_muse_ai_event_publish_outbox_update_time`:复用 `update_updated_at_column()`,触发器必须更新 `update_time`
## 状态机
```mermaid
stateDiagram-v2
[*] --> queued: terminal fact 同事务创建 outbox
queued --> running: claim, attempt_count + 1, 写 lease
retryable --> running: next_retry_at 到期 claim, attempt_count + 1, 写 lease
running --> running: claim_expires_at < now stale reclaim, attempt_count + 1, 刷新 lease
running --> published: Events accepted 或 duplicate
running --> dead_letter: Events rejected/blocked 或 payload 不合规
running --> retryable: Events temporary failure 或调用异常
running --> dead_letter: stale reclaim 后 attempt_count 超过 max attempt
retryable --> dead_letter: claim 或失败处理发现 attempt_count 超过 max attempt
published --> [*]
dead_letter --> [*]
```
状态含义:
1. `queued`:待发布,`attempt_count = 0``claimed_at` / `claim_expires_at` 为空。
2. `running`:被 worker 原子领取,必须写 `claimed_at = now``claim_expires_at = now + lease`,并有中文日志记录 outbox id、tenantId、taskId、eventType、attempt、claimExpiresAt。
3. `published`Events `CommonResult` 成功且 `EventsPublishRespDTO.publishStatus = accepted`,或 `publishStatus = accepted``duplicate = true`;必须记录 `published_event_id``published_sequence_no`
4. `retryable`:临时失败,设置 `next_retry_at`,保留安全错误码和摘要。
5. `dead_letter`payload 不合规、Events 返回 rejected/blocked、或超过最大重试次数不得进入 SSE 可见事件。
注意:
1. AI outbox 本地 `publish_status` 不存在 `blocked`Events 侧 `accepted/rejected/blocked` 是统一事件投影层状态,不是 AI outbox 本地状态。
2. worker 不能只看 `CommonResult` 成功/失败;必须读取 `EventsPublishRespDTO.publishStatus` 并按 `accepted/duplicate/rejected/blocked` 分流。`CommonResult` 成功但 `publishStatus` 为空或未知时,必须 fail-closed 写 `retryable``dead_letter`,不得伪造成 `published`
3. claim 查询必须覆盖三类可领取记录:`queued``retryable AND next_retry_at <= now``running AND claim_expires_at < now`。未到期的 `running` 不得被领取。
4. attempt 语义冻结为 claim 次数:每次从 `queued`、到期 `retryable` 或 stale `running` 被 claim 时统一 `attempt_count + 1`;失败处理不得再次递增 attempt避免一次 publish 失败被计算两次。
5. stale running reclaim 采用 `running -> running` 最小语义:重领时刷新 `claimed_at/claim_expires_at` 并递增 attempt如果递增后超过 `max_attempt`,必须进入 `dead_letter`,错误码 `AI_EVENTS_PUBLISH_RETRY_EXHAUSTED`,避免 crash-after-claim 无限循环。
## 幂等策略
1. AI outbox 创建幂等:同一 `(tenant_id, task_id, source_revision, event_type)` 只能有一条 outbox。
2. command 幂等:同一 `(tenant_id, command_id)` 只能有一条 outbox。
3. Events publish 幂等:同一 `commandId` 或同一 source tuple 重放必须得到相同 `eventId / sequenceNo`
4. worker 幂等worker 重试只读取 outbox 记录,不重新读取不稳定上下文构造不同 payload。
5. terminal fact 幂等:`MuseAiTaskEventMapper.selectTerminalByTaskId` 和 DB terminal 唯一约束继续阻止同一 task 多个 terminal event。
6. replay 幂等:人工 replay 只允许把 `dead_letter` 中确认为临时故障的记录重置为 `retryable``AI_EVENTS_DONE_PAYLOAD_INCOMPLETE` 等 payload 不合规记录不得直接 replay。
## 失败、重试与 dead_letter
临时失败:
1. `EventsPublishApi` 调用抛出网络、超时、RPC unavailable 等异常。
2. `CommonResult` 表示临时不可用或无响应。
3. worker 处理时发生非 payload/contract 类异常。
处理方式:
1. `attempt_count` 已在 claim 时递增,失败处理不得再次 `+ 1`
2. 当前 `attempt_count` 未达到上限时写 `retryable``next_retry_at = now + backoff`,并清空或保留 lease 字段时必须有一致测试;推荐清空 `claimed_at/claim_expires_at`,让下一次 claim 只由 `next_retry_at` 控制。
3. backoff 建议最小固定序列 `1s, 2s, 4s, 8s, 16s`,超出使用 16s实现代码必须用中文注释说明这是 P1R-7b 最小退避,不引入全局任务框架。
4. 当前 `attempt_count` 达到或超过上限写 `dead_letter`,错误码 `AI_EVENTS_PUBLISH_RETRY_EXHAUSTED`
stale running
1. worker crash-after-claim 后,记录会停留在 `running`,直到 `claim_expires_at < now`
2. stale running 由后续 claim 原子重领,状态保持 `running -> running`,刷新 `claimed_at/claim_expires_at`,并按 claim 次数递增 `attempt_count`
3. stale running 重领前或重领后发现 `attempt_count` 超过 `max_attempt`,必须写 `dead_letter`,不得无限重领。
4. `claim_expires_at` 为空的 `running` 属于不合规历史状态;实现阶段必须 fail-closed 到 `retryable``dead_letter`,并用测试固定规则,不得被永久卡住。
永久失败:
1. payload 缺少 OpenAPI 必填字段。
2. payload 包含 prompt、provider raw body、token、authorization、bearer、secret 等敏感内容。
3. `EventsPublishApi` 返回 `rejected``blocked`
处理方式:
1. 立即写 `dead_letter`
2. 不重试。
3. 记录安全日志和审计摘要。
4. 不向 SSE 伪造事件。
## Worker 生命周期与停止策略
1. `MuseAiEventPublishWorker` 采用现有 `MuseAiRuntimeJobDispatcher` 的最小生命周期风格:`@Scheduled(initialDelayString = "${muse.ai.events.publish-worker.initial-delay-ms:1000}", fixedDelayString = "${muse.ai.events.publish-worker.fixed-delay-ms:1000}")` 只调用 `dispatchOnce()`
2. 新增配置开关 `muse.ai.events.publish-worker.enabled`,默认 `true`;实现阶段如项目配置类已有更适合的布尔默认约定,必须保持默认启用或在实现报告写明偏差原因。
3. `enabled=false` 时 scheduled 入口和手动入口都不得 claim outbox`enabled=true` 时 scheduled 入口调用 `dispatchOnce()` 并按 claim 结果处理一批或一条记录。
4. `dispatchOnce()` 必须保留可测试的同步方法,测试不依赖真实时间、不等待 scheduler。
5. 回滚或应急停止方式:关闭 `muse.ai.events.publish-worker.enabled`,或在更高风险回滚中移除 worker bean 装配。已经 claim 为 `running` 的记录不做人工改写;重新启用 worker 后由 `claim_expires_at` lease 回收 stale running并按 retry/dead_letter 规则继续处理。
## 日志、审计与可观测性
实现阶段必须补中文日志和注释:
1. 创建 outbox记录 `tenantId``ownerUserId``taskId``eventType``sourceRevision``commandId`,不记录 payload 正文。
2. worker claim记录 outbox id、attempt、状态转换、claimedAt、claimExpiresAt以及是否 stale running reclaim。
3. publish 成功:记录 outbox id、`publishedEventId``publishedSequenceNo``duplicate`
4. publish rejected/blocked记录 outbox id、错误码、安全摘要。
5. temporary failure记录 outbox id、错误类型、nextRetryAt。
6. dead_letter记录 outbox id、最终错误码、attemptCount。
7. payload sanitizer 或 allowlist 拒绝:必须有安全日志,不能输出原始敏感值。
审计要求:
1. 如 AI 现有 `MuseAiAuditService` 适用于内部业务审计,则记录 `operationId = aiEventsPublish` 或实现阶段冻结的等价 operation id。
2. 审计 request/response summary 只能包含 outbox id、taskId、eventType、publishStatus、publishedEventId、publishedSequenceNo、errorCode。
3. 审计不阻塞主业务事实落库;审计失败不得伪造成 publish 成功。
## TDD 与 review gate 总规则
P1R-7b 子计划 gate
1. 本文件必须先完成 fresh spec review。
2. 本文件必须再完成 fresh quality / feasibility review。
3. 双 PASS 前不得继续当前 AI 草稿实现,不得运行当前草稿测试,不得提交或推送。
4. 若任一 review FAIL只修订本文件和必要 `.agent` 状态说明;不得通过改代码、改 OpenAPI、改 scanner、改 coverage 绕过 review。
每个实现 Task 必须按以下顺序执行:
1. fresh subagent implementer 领取单个 Task。
2. 先写失败测试或门禁。
3. 运行 focused test确认因目标缺口失败。
4. 写最小实现。
5. 运行 focused tests确认通过。
6. 运行该 Task 指定验证命令。
7. fresh spec review。
8. fresh quality review。
9. 双 PASS 后才允许进入下一 Task。
任何 Task 若出现 FAIL
1. implementer 只修当前 Task 范围内问题。
2. 修复后重新跑 focused tests。
3. 重新进行 fresh spec review + fresh quality review。
4. 不得用 OpenAPI/scanner/coverage 状态改动绕过失败。
## Task 0子计划 fresh review gate
执行角色fresh spec reviewer + fresh quality / feasibility reviewer。
文件范围:
- 允许修改:`docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation执行版.md`
- 允许修改:`docs/agent-specs/.agent`
- 保持不动:任何 Java / SQL / test / `pom.xml` / OpenAPI / scanner / coverage 文件。
步骤:
1. 只读核对全局审阅版和全局执行版已把 P1R-7b 定义为 AI terminal event 第一切片。
2. 只读核对本文件是否继承依赖合同、发布 envelope 合同、用户可见事件判定、outbox 合同和 payload 合同。
3. 只读核对当前 dirty 文件处置表是否覆盖 AI 草稿代码、迁移、测试和 `docs/agent-specs/*`
4. 只读核对 `DuplicateKeyException` 幂等路径是否被列为必须修订点。
5. 只读核对本文件是否明确双 review PASS 前不得继续 AI 草稿实现。
验证命令框架,本轮计划修订不运行:
```bash
git status --short --branch
git status --short -- 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
rg -n "P1R-7b|AI terminal|fresh spec review|fresh quality|DuplicateKeyException|insertIgnore|ON CONFLICT|completed" docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation执行版.md
```
完成条件:
1. Fresh spec review PASS。
2. Fresh quality / feasibility review PASS。
3. protected files 定向状态无改动。
4. 输出下一步是“由 fresh implementer 处理 Task 1/Task 2”不是直接继续当前 AI 草稿。
## Task 1Preflight 与依赖/现状冻结
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 只读:`AGENTS.md`
- 只读:`CLAUDE.md`
- 只读:`docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation审阅版.md`
- 只读:`docs/memorys/2026-06-05-P1R7aEventsSSE真实API收口.md`
- 只读:`docs/superpowers/plans/2026-06-05-P1R-7a-events-sse.md`
- 只读:`docs/superpowers/reports/p1r-api-coverage.json`
- 只读:`muse-cloud/muse-module-events/**`
- 只读:`muse-cloud/muse-module-ai/**`
- 只读:`muse-cloud/sql/muse/**`
步骤:
1. 记录 `pwd``git status --short --branch``git branch --show-current``git log -1 --oneline`
2. 定向检查受保护文件和 coverage 报告无改动。
3. 读取 coverage summary 和 Events/Market operation 明细。
4. 检查 `muse-module-ai-server/pom.xml` 当前 dirty 草稿中的 `muse-module-events-api` 依赖,并确认没有 `muse-module-events-server` 依赖。
5. 检查 `muse-cloud/sql/muse` 当前最新 migration 版本,区分已推送基线和当前未提交 V17 草稿。
6. 检查 Events API、Events publish service、UnifiedEvent mapper、Events stream service 的现状。
7. 检查 AI task event、runtime projection、runtime dispatcher、job mapper、source event retry 的现状。
验证命令:
```bash
pwd
git status --short --branch
git branch --show-current
git log -1 --oneline
git diff --name-only -- 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
jq '.summary' docs/superpowers/reports/p1r-api-coverage.json
jq -r '.operations[] | select(.domain=="events") | [.domain,.operationId,.method,.path,.implementationStatus,.completionStatus,.targetStage] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
jq -r '[.operations[] | select(.domain=="market") | (.implementationStatus + "/" + .completionStatus)] | group_by(.)[] | [(.[0]), length] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
rg -n "muse-module-events-api|muse-module-events-server" muse-cloud/muse-module-ai/muse-module-ai-server/pom.xml muse-cloud/muse-server/pom.xml muse-cloud/muse-module-events/muse-module-events-server/pom.xml
find muse-cloud/sql/muse -maxdepth 1 -type f -name 'V*__*.sql' \
| sed -E 's#.*/V([0-9]+)__.*#\1&#' \
| sort -n \
| tail -n 10
```
完成条件:
1. 工作区、分支、dirty baseline 与本执行版约束一致。
2. 受保护文件和 coverage 报告无改动。
3. AI 当前 `muse-module-events-api` / `muse-module-events-server` 依赖状态有 live 证据。
4. 最新 migration 版本有 live 证据,且必须按 `V` 后数字排序;如果当前 V17 草稿仍存在,必须把它列为“修订沿用候选”,不能误写成已通过 migration。
5. spec review PASS + quality review PASS。
## Task 2AI publish outbox/job schema
执行角色fresh subagent implementer + fresh spec review + fresh quality review。Task 2 是第一个可写代码/迁移/测试的实现 Task必须等本子计划 Task 0 双 review PASS 后,由 fresh implementer 重新领取;当前会话不得直接继续 AI 草稿实现。
文件范围:
- 修订或新增:`muse-cloud/sql/muse/V17__extend_ai_events_publish_outbox.sql`。当前 dirty baseline 已存在 V17 草稿Task 2 implementer 必须先判断它是修订沿用还是在用户批准后废弃重做,不能把已存在文件当成已通过事实。
- 修订或新增:`muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishMigrationSqlTest.java`。当前 dirty baseline 已存在草稿Task 2 必须让测试针对当前 V17 真实缺口失败,再修订到通过。
- 修订或新增:`muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishFlywayMigrationIT.java`。当前 dirty baseline 已存在草稿Task 2 必须重新证明独立 `_test` 库保护和真实 test count。
测试范围:
1. SQL 文本测试必须断言表名、字段、唯一约束、claim 索引、状态 CHECK、event_type CHECK、`update_time` trigger`source_event_id` 字段类型唯一冻结为 `BIGINT NOT NULL`
2. SQL 文本测试必须断言 outbox schema 能支持后续 claim`claimed_at TIMESTAMP``claim_expires_at TIMESTAMP` 两个 lease 字段存在,`publish_status``next_retry_at``attempt_count` 字段存在,且 claim 索引包含 `publish_status, claim_expires_at, next_retry_at, create_time, id`
3. SQL 文本测试必须断言 outbox 表使用项目约定字段 `creator/create_time/updater/update_time/deleted`,并通过断言 `BaseDO.createTime/updateTime``TenantBaseDO extends BaseDO` 或等价反射/文本证据锁定项目基类约定。
4. SQL 文本测试必须断言 `update_updated_at_column()` 更新 `update_time`,并断言 outbox migration 不使用非项目约定的时间字段名。
5. SQL 文本测试必须断言 `command_id VARCHAR(128)`,并配套 service/mapper 测试冻结 `ai_evt:<sha256-32>` 或等价短 hash 格式,生成长度 `<= 128`
6. SQL 文本测试只断言 schema 层 claim 支撑能力:`publish_status` 状态 CHECK、`attempt_count` 非负约束、lease 字段和 retry 字段可空性、claim 索引字段顺序;不得要求断言具体 mapper claim SQL 或 mapper 注解。
7. Flyway `_test` 必须在独立测试库执行,不能 clean 开发库或真实库。
8. migration 必须和 V12/V16 共存,不修改既有表语义。
9. 如果沿用当前 V17 草稿,必须先修订所有不符合全局 outbox 合同的字段、索引、状态和幂等语义;不能因为文件已存在就跳过 TDD 失败测试和 fresh review。
TDD 步骤:
1. 先写或修订 `P1rAiEventsPublishMigrationSqlTest`,让它针对当前 V17 草稿的真实缺口失败;如果 V17 草稿被用户批准废弃后不存在,则测试应因 migration 未定义失败。
2. 运行 focused test确认失败原因来自 schema 合同缺口,而不是测试未运行。
3. 修订或新增 migration包含本执行版字段、lease 字段、约束、索引和 trigger。
4. 运行 SQL 文本测试通过。
5. 新增或扩展 Flyway IT证明所有 migrations 在独立 `_test` 库可执行。
6. 运行 Flyway `_test`
验证命令:
```bash
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-server -am -Dtest=P1rAiEventsPublishMigrationSqlTest test
set -a
source ~/.config/muse-repo/infra.env
set +a
export P1R7B_TEST_DB="${P1R7B_TEST_DB:-muse_p1r7b_events_publish_test}"
case "$P1R7B_TEST_DB" in *_test) ;; *) echo "P1R7B_TEST_DB must end with _test"; exit 1;; esac
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD"
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" timeout 240 mvn test \
-pl muse-server \
-Dtest=P1rAiEventsPublishFlywayMigrationIT \
-Dflyway.postgresql.transactional.lock=false \
-Djava.net.useSystemProxies=false \
-DsocksProxyHost= \
-DsocksProxyPort= \
-DsocksNonProxyHosts="127.0.0.1|localhost|100.64.*|100.*" \
-Dhttp.proxyHost= \
-Dhttps.proxyHost= \
-Dp1r.flyway.url="jdbc:postgresql://${MUSE_POSTGRES_HOST}:${MUSE_POSTGRES_PORT}/${P1R7B_TEST_DB}" \
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
-Dp1r.flyway.locations="filesystem:sql/muse"
```
完成条件:
1. migration 版本号来自 Task 1 live 查证。
2. 表字段覆盖本执行版迁移计划要求;`source_event_id` 必须是 `BIGINT NOT NULL`,引用 `muse_ai_task_event.id`,如果实现阶段发现 DO/mapper 名称不同,也必须先 live 查证后保持 Long id 语义。
3. 唯一约束覆盖 commandId 和 task/source/event tuple。
4. SQL 文本测试通过,且 Task 2 完成证据只来自 migration SQL 与 migration tests不依赖 Task 4 才新增或修改的 mapper 文件。
5. 独立 `_test` Flyway IT 通过,并断言目标 version、description、已执行 migration count 随 Task 1 live 下一版本号更新;保留 `_test` 库名保护,非 `_test` 库必须 fail-fast。
6. SQL 文本测试和 Flyway IT 都必须证明目标测试类实际运行:`target/surefire-reports/TEST-...P1rAiEventsPublishMigrationSqlTest.xml``TEST-...P1rAiEventsPublishFlywayMigrationIT.xml` 存在,且各自 test count > 0。
7. spec review PASS + quality review PASS。
## Task 3AI DO / Mapper / Service 与 terminal fact 同事务写 outbox
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/pom.xml`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/dataobject/muse/MuseAiEventPublishOutboxDO.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapper.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxService.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceImpl.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionService.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceImpl.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapperTest.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceTest.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiRuntimeProjectionServiceTest.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseJobServiceTest.java`
测试范围:
1. DO 字段映射覆盖 migration 字段。
2. Mapper 能按 commandId 和 source tuple 查重。
3. Service 只为 `done` / `error` terminal event 创建 outbox`cancelled` task status 只能通过 `eventType = error` 发布,不新增 cancelled event type。
4. Service 对 `done` 缺少 suggestionId 的事件写入或保持 `dead_letter`,记录 `last_error_code = AI_EVENTS_DONE_PAYLOAD_INCOMPLETE`,并证明不会发布到 SSE 可见事件。
5. Service 对非数字 taskId 必须 fail-closed 写入或保持 `dead_letter``last_error_code = AI_EVENTS_TASK_ID_NOT_NUMERIC`,并证明不会调用 `EventsPublishApi`,不会发布 `done``error` 到 SSE 可见事件。
6. Service 生成 payload allowlist不包含 prompt/provider raw body/token/auth/private context/full output。
7. Service 生成 `command_id` 必须使用 `ai_evt:<sha256-32>` 或等价短 hash 格式,测试覆盖长度 `<= 128`,禁止长拼接格式作为推荐或默认实现。
8. `MuseAiRuntimeProjectionService` 在写 terminal task event 的同一事务内调用 outbox service。
9. `MuseJobServiceImpl.appendTaskCancellationEventIfAbsent` 在写 cancellation `error` terminal event 的同一事务内调用 outbox servicepayload 只能包含 `code=AI_TASK_CANCELLED``message=AI task cancelled``retryable=false`,不能携带用户取消原因。
10. 可重试 failure 不写 terminal event也不写 Events publish outbox。
11. outbox 创建幂等必须通过 mapper `insertIgnore` / SQL `ON CONFLICT DO NOTHING` 返回插入行数,再按 commandId/source tuple 回查;测试必须禁止把 `DuplicateKeyException` 作为正常分支。
TDD 步骤:
1. 针对当前 dirty DO/Mapper/Service/Test 草稿写或修失败测试,失败点必须来自真实合同缺口,而不是“类不存在”。
2. 优先写 duplicate 幂等失败测试:当前草稿如果仍捕获 `DuplicateKeyException` 作为正常路径,测试必须失败;目标路径必须是 `insertIgnore` / `ON CONFLICT DO NOTHING` 返回 0 + commandId/source tuple 回查。
3. 写 projection 失败测试,断言 terminal done/error 后必须创建 outbox如果当前草稿已有调用点测试必须核对事务边界、sourceEventId、payload 和幂等语义。
4.`MuseJobServiceTest` 失败测试,断言 cancel admin/app 触发 `appendTaskCancellationEventIfAbsent` 后必须同事务创建 outbox且 cancellation payload 不包含 request reason。
5. 写 payload 安全失败测试,构造包含敏感字段的 payload断言 outbox payload 不包含敏感值。
6. 写非数字 taskId 失败测试,断言 taskId 为 `abc`、UUID 或带前缀字符串时 fail-closed 到 `dead_letter`,不会产生 SSE 可见事件。
7. 写 commandId 长度测试,断言 `ai_evt:<sha256-32>` 或等价短 hash 格式长度 `<= 128`,且不使用长拼接格式。
8. 运行 focused tests确认失败原因来自上述真实缺口且测试类实际运行。
9. 修订 DO/Mapper/Service 最小实现,先实现 `insertIgnore` / `ON CONFLICT DO NOTHING` + 回查,再接 terminal fact 调用点。
10. 复核 `muse-module-ai-server/pom.xml` 当前 dirty 草稿中的 `muse-module-events-api` 依赖;保留或修订时只允许 api不允许 server。
11.`MuseAiRuntimeProjectionService.appendTaskEvent` 写入 terminal event 后调用 outbox service关键事务和安全 allowlist 必须有中文注释。
12.`MuseJobServiceImpl.appendTaskCancellationEventIfAbsent` 写入 cancellation `error` terminal event 后调用 outbox service中文注释必须说明取消原因不能进入统一 Events payload。
13. 运行 focused tests。
验证命令:
```bash
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server -am \
-Dtest=MuseAiEventPublishOutboxMapperTest,MuseAiEventPublishOutboxServiceTest,MuseAiRuntimeProjectionServiceTest,MuseJobServiceTest test
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server -am dependency:tree \
-Dincludes=cn.iocoder.cloud:muse-module-events-api
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server -am dependency:tree \
| rg 'muse-module-events-server' && exit 1 || true
```
完成条件:
1. `muse-module-ai-server` 只新增 `muse-module-events-api` 依赖。
2. runtime done/error terminal fact 与 outbox 写入在同一事务链路内。
3. cancellation error terminal fact 与 outbox 写入在同一事务链路内,且 cancellation payload 只包含 `AI_TASK_CANCELLED``AI task cancelled``retryable=false` 三个安全事实。
4. payload allowlist 测试覆盖 done/error/cancellation、非数字 taskId fail-closed、commandId 短 hash 长度 `<= 128` 和敏感字段排除。
5. `MuseAiEventPublishOutboxServiceImpl` 不捕获 `DuplicateKeyException` 作为正常幂等路径;对应测试证明 duplicate 走 `insertIgnore=0` + 回查。
6. focused tests 通过,并必须证明 `MuseAiEventPublishOutboxMapperTest``MuseAiEventPublishOutboxServiceTest``MuseAiRuntimeProjectionServiceTest``MuseJobServiceTest` 实际运行:对应 surefire reports XML 存在且 test count > 0。
7. dependency tree 证明 AI 不依赖 events-server。
8. spec review PASS + quality review PASS。
## Task 4AI worker / dispatcher 发布链路
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishWorker.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/framework/ai/config/MuseAiProperties.java`,如果实现阶段选择用集中配置承载 `muse.ai.events.publish-worker.enabled`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapper.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/main/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishOutboxServiceImpl.java`
- 新增:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/application/muse/MuseAiEventPublishWorkerTest.java`
- 修改:`muse-cloud/muse-module-ai/muse-module-ai-server/src/test/java/cn/iocoder/muse/module/ai/dal/mysql/muse/MuseAiEventPublishOutboxMapperTest.java`
测试范围:
1. Mapper claim 使用单条 SQL 原子 `UPDATE ... WHERE id=(SELECT ... FOR UPDATE SKIP LOCKED) RETURNING *` 或等价机制。
2. claim 只领取 `queued`、到期 `retryable`、以及 `running AND claim_expires_at < now` 的 stale running不领取未到期 `running``published``dead_letter`
3. claim 时必须原子写 `publish_status = running``claimed_at = now``claim_expires_at = now + lease``attempt_count = attempt_count + 1`
4. stale running reclaim 必须证明 `running -> running`、刷新 lease、递增 attempt递增后超过 max attempt 时进入 `dead_letter`,错误码 `AI_EVENTS_PUBLISH_RETRY_EXHAUSTED`
5. worker 的 `@Scheduled(initialDelayString=..., fixedDelayString=...)` 入口只调用 `dispatchOnce()`
6. `muse.ai.events.publish-worker.enabled=false` 时 worker 不 claim`enabled=true` 时 scheduled 入口调用 `dispatchOnce()`
7. worker 对 `CommonResult` 成功且 `publishStatus=accepted` 标记 `published`,写入 `published_event_id/published_sequence_no`
8. worker 对 `publishStatus=accepted``duplicate=true` 标记 `published`,复用返回 eventId/sequenceNo。
9. worker 对 `publishStatus=rejected/blocked` 标记 `dead_letter`
10. worker 对 `publishStatus` 为空或未知 fail-closed不得标记 `published`
11. worker 对 CommonResult 失败、调用异常或临时不可用标记 `retryable` 并设置 `next_retry_at`,失败处理不得再次递增 `attempt_count`
12. worker 达到最大 attempt 后标记 `dead_letter`
13. worker 不把 payload 或敏感错误原文写入日志/审计。
TDD 步骤:
1. 写 worker 失败测试mock `EventsPublishApi` 返回 `publishStatus=accepted/accepted+duplicate/rejected/blocked/unknown`、CommonResult 失败和抛异常。
2. 写生命周期失败测试,断言 `enabled=false` 不 claim`enabled=true` 的 scheduled 入口调用 `dispatchOnce()`
3. 写 mapper claim SQL 反射测试,断言 SQL 包含 `FOR UPDATE SKIP LOCKED``claimed_at``claim_expires_at``attempt_count + 1``queued`、到期 `retryable`、stale `running``next_retry_at` 条件。
4. 写 stale running 测试,断言未到期 running 不领取,`claim_expires_at < now` 的 running 可 reclaim且超过 max attempt 进入 `dead_letter`
5. 运行 focused tests确认失败。
6. 实现 mapper claim、worker `dispatchOnce()``@Scheduled` scheduled 入口、配置开关、状态转换、lease 和日志。
7. 实现固定 backoff、claim lease 和最大 attempt中文注释说明 P1R-7b 最小策略。
8. 运行 focused tests。
验证命令:
```bash
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server -am \
-Dtest=MuseAiEventPublishWorkerTest,MuseAiEventPublishOutboxMapperTest test
```
完成条件:
1. worker 能处理 accepted、duplicate、rejected、blocked、temporary failure、retry exhausted。
2. worker 能证明 `enabled=false` 不 claim`enabled=true``dispatchOnce()``@Scheduled(initialDelayString=..., fixedDelayString=...)` 入口存在且只负责触发。
3. worker 能证明不只看 `CommonResult` 成功失败,而是按 `EventsPublishRespDTO.publishStatus``accepted/duplicate/rejected/blocked` 分流。
4. claim 具备并发安全语义。
5. 日志不泄露 payload 原文或敏感错误。
6. focused tests 通过,并必须证明 `MuseAiEventPublishWorkerTest``MuseAiEventPublishOutboxMapperTest` 实际运行:对应 surefire reports XML 存在且 test count > 0。
7. spec review PASS + quality review PASS。
## Task 5端到端 focused evidence 与 SSE 可见性
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 新增:`muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishEndToEndTest.java`
- 必要时修改AI outbox service / worker test fixture不修改 OpenAPI、scanner 或 coverage。
测试范围:
1. 构造 AI terminal `done` fact写入 outboxworker 调用 `EventsPublishApi`Events 写入 `muse_unified_event` accepted。
2. 查询 `EventsPublishService.listVisibleEvents``EventsStreamService.streamEvents`,证明当前 tenant + ownerUserId 可见。
3. 构造 AI terminal `error` fact证明 error payload 可见且不泄露敏感 detail。
4. 构造重复 publish证明 Events 返回同一 `eventId/sequenceNo`outbox 仍为 `published`
5. 构造 rejected payload证明 outbox dead_letter 且 SSE 不可见。
TDD 步骤:
1. 先写 E2E focused test断言当前链路缺少 AI outbox/worker 时失败。
2. 接入 Task 3/4 实现后的最小 fixture。
3. 运行 E2E focused test。
4. 若 E2E 需要 Spring 装配,优先复用现有 P1R gate 测试风格;不要引入新测试框架。
验证命令:
```bash
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-server -am \
-Dtest=P1rAiEventsPublishEndToEndTest test
```
完成条件:
1. AI done/error 两类 terminal event 均有 source owner -> Events -> SSE 可见证据。
2. duplicate/rejected 安全路径有 focused evidence。
3. 不需要修改 OpenAPI/scanner/coverage。
4. 必须证明 `P1rAiEventsPublishEndToEndTest` 实际运行:对应 surefire reports XML 存在且 test count > 0。
5. spec review PASS + quality review PASS。
## Task 6依赖方向、coverage gate 与迁移 gate
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 新增:`muse-cloud/muse-server/src/test/java/cn/iocoder/muse/server/framework/api/P1rAiEventsPublishDependencyTest.java`
- 运行并前后检查 diff`muse-cloud/scripts/p1r-audit-api-coverage.py`
- Task 6 scanner 必须在临时隔离副本中运行;当前 worktree 的 coverage JSON/Markdown 与 scanner 脚本仍是受保护文件,运行前后 diff 必须为空。
- 检查:受保护文件和 coverage 报告状态
测试范围:
1. AI server 依赖 `muse-module-events-api`
2. AI server 不依赖 `muse-module-events-server`
3. Events server 不依赖 AI / Knowledge / Market / Member / Content server。
4. `P1rApiCoverageReportTest``P1rEventsRealApiGateTest``P1rEventsRouteOwnershipTest``P1rAiRealApiGateTest``P1rAiRouteOwnershipTest` 仍通过。
5. Coverage scanner `--check` 必须真实运行,但只能在临时隔离副本写报告;当前 worktree 运行前后受保护文件 diff 必须为空。不得通过修改 scanner/coverage 达成状态。
TDD 步骤:
1. 写或修订 dependency test断言 live 依赖方向AI server 可以有 `muse-module-events-api`,不得有 `muse-module-events-server`Events server 不得依赖 AI / Knowledge / Market / Member / Content server。
2. 运行 dependency test确认它针对当前 dirty `pom.xml` 真实依赖状态做机械断言;不得再以缺失态作为预期失败前提。
3. 运行 P1R focused gates。
4.`rsync` 或等价方式复制当前 worktree 到 `/tmp` 临时隔离目录,排除 `.git``target``node_modules` 等构建产物;在副本中运行 coverage scanner `--check`,记录副本中的 scanner exit code、summary、Events/Market 状态,并检查当前 worktree 受保护文件 diff 仍为空。
5. 运行 Flyway SQL/Flyway IT。
验证命令:
```bash
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-server -am \
-Dtest=P1rAiEventsPublishDependencyTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \
test
git -C .. diff -- 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
cd ..
tmpdir=$(mktemp -d /tmp/p1r7b-coverage-scan.XXXXXX)
rsync -a --delete \
--exclude .git \
--exclude 'muse-cloud/**/target' \
--exclude 'node_modules' \
./ "$tmpdir"/
cd "$tmpdir/muse-cloud"
python3 scripts/p1r-audit-api-coverage.py --check
cd "$tmpdir"
jq '.summary' docs/superpowers/reports/p1r-api-coverage.json
jq -r '.operations[] | select(.domain=="events") | [.domain,.operationId,.implementationStatus,.completionStatus] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
jq -r '[.operations[] | select(.domain=="market") | (.implementationStatus + "/" + .completionStatus)] | group_by(.)[] | [(.[0]), length] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
git -C .. diff -- 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
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server -am dependency:tree \
| rg 'muse-module-events|muse-module-(ai|knowledge|market|member|content)-server'
```
完成条件:
1. 依赖方向 gate 通过。
2. P1R focused gates 通过。
3. scanner `--check` 在临时隔离副本中 exit 0副本报告 summary 符合 P1R-7b 边界,当前 worktree 运行前后受保护文件和 coverage 报告 diff 为空。
4. Coverage 状态不得被推进到 completed。
5. Market 32 operations 保持 `dedicated / needs_verification`
6. 必须证明 `P1rAiEventsPublishDependencyTest` 以及本命令列出的 P1R gate 测试类实际运行:对应 surefire reports XML 存在且 test count > 0。
7. spec review PASS + quality review PASS。
## Task 7最终验证、文档留痕与交接
执行角色fresh subagent implementer + fresh spec review + fresh quality review。
文件范围:
- 新增:`docs/memorys/2026-06-06-P1R7bSourceOwnerPropagation真实链路计划.md` 或实现收口时由用户确认的 10 到 20 字任务描述文件名。
- 修改:必要的实现报告或计划状态文档;不得修改 coverage completed 状态。
测试范围:
1. AI module focused tests。
2. Events module focused tests。
3. P1R gate tests。
4. Flyway `_test`
5. dependency tree。
6. coverage scanner。
7. reactor build。
8. protected files status。
最终验证命令:
```bash
git status --short --branch
git diff --name-only -- 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
cd muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o clean install \
-DskipTests \
-Dspring-boot.repackage.skip=true \
-pl muse-server -am
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-module-ai/muse-module-ai-server,muse-module-events/muse-module-events-server -am \
-Dtest=MuseAiEventPublishOutboxMapperTest,MuseAiEventPublishOutboxServiceTest,MuseAiEventPublishWorkerTest,MuseAiRuntimeProjectionServiceTest,EventsPublishServiceTest,EventsStreamServiceTest test
JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -pl muse-server -am \
-Dtest=P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \
test
set -a
source ~/.config/muse-repo/infra.env
set +a
export P1R7B_TEST_DB="${P1R7B_TEST_DB:-muse_p1r7b_events_publish_test}"
case "$P1R7B_TEST_DB" in *_test) ;; *) echo "P1R7B_TEST_DB must end with _test"; exit 1;; esac
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD"
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" timeout 240 mvn test \
-pl muse-server \
-Dtest=P1rAiEventsPublishFlywayMigrationIT \
-Dflyway.postgresql.transactional.lock=false \
-Djava.net.useSystemProxies=false \
-DsocksProxyHost= \
-DsocksProxyPort= \
-DsocksNonProxyHosts="127.0.0.1|localhost|100.64.*|100.*" \
-Dhttp.proxyHost= \
-Dhttps.proxyHost= \
-Dp1r.flyway.url="jdbc:postgresql://${MUSE_POSTGRES_HOST}:${MUSE_POSTGRES_PORT}/${P1R7B_TEST_DB}" \
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
-Dp1r.flyway.locations="filesystem:sql/muse"
git -C .. diff --name-only -- 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
cd ..
tmpdir=$(mktemp -d /tmp/p1r7b-final-coverage-scan.XXXXXX)
rsync -a --delete \
--exclude .git \
--exclude 'muse-cloud/**/target' \
--exclude 'node_modules' \
./ "$tmpdir"/
cd "$tmpdir/muse-cloud"
python3 scripts/p1r-audit-api-coverage.py --check
cd "$tmpdir"
jq '.summary' docs/superpowers/reports/p1r-api-coverage.json
jq -r '.operations[] | select(.domain=="events") | [.domain,.operationId,.implementationStatus,.completionStatus] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
jq -r '[.operations[] | select(.domain=="market") | (.implementationStatus + "/" + .completionStatus)] | group_by(.)[] | [(.[0]), length] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
git -C .. diff --name-only -- 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
```
完成条件:
1. 所有 focused tests 和 gates 有命令、exit code、关键输出。
2. 必须证明新增目标测试类和混合 gate 中列出的目标测试类实际运行:对应 surefire reports XML 存在且 test count > 0不能仅凭 Maven exit 0 判断。
3. 必须证明真实 Flyway `_test` IT `P1rAiEventsPublishFlywayMigrationIT` 实际运行:`target/surefire-reports/TEST-...P1rAiEventsPublishFlywayMigrationIT.xml` 存在且 test count > 0命令必须使用独立 `_test` 库、`flyway.postgresql.transactional.lock=false``p1r.flyway.locations=filesystem:sql/muse`
4. source propagation evidence 明确推进到 needs_verification 证据层。
5. Events / P1R-7 / Market 不被标记 completed。
6. 受保护文件无改动。
7. git status 报告必须区分本任务实现、本任务文档留痕和已记录且用户未批准清理的既有 dirty baseline不得为满足最终状态而清理、回退或删除既有 dirty baseline。
8. spec review PASS + quality review PASS。
## P1R-7b 验证命令框架
结论:以下命令是后续 fresh implementer 和 reviewer 的验证框架;本轮文档修订不运行这些命令,不把任何命令结果写成已通过事实。
### Protected status
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
git status --short --branch
git status --short -- 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
```
### Focused Maven tests
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -o clean install \
-DskipTests \
-Dspring-boot.repackage.skip=true \
-pl muse-server -am
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-module-ai/muse-module-ai-server -am \
-Dtest=MuseAiEventPublishOutboxMapperTest,MuseAiEventPublishOutboxServiceTest,MuseAiEventPublishWorkerTest,MuseAiRuntimeProjectionServiceTest,MuseJobServiceTest \
-Dsurefire.failIfNoSpecifiedTests=false test
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-server -am \
-Dtest=P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \
-Dsurefire.failIfNoSpecifiedTests=false test
```
### Dependency tree
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-module-ai/muse-module-ai-server -am dependency:tree \
-Dincludes=cn.iocoder.cloud:muse-module-events-api
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-module-ai/muse-module-ai-server -am dependency:tree \
| rg 'muse-module-events-server' && exit 1 || true
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-module-events/muse-module-events-server -am dependency:tree \
| rg 'muse-module-(ai|knowledge|market|member|content)-server' && exit 1 || true
```
### Migration SQL test
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" mvn -pl muse-server -am \
-Dtest=P1rAiEventsPublishMigrationSqlTest \
-Dsurefire.failIfNoSpecifiedTests=false test
```
### Flyway `_test` 独立库
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0/muse-cloud
set -a
source ~/.config/muse-repo/infra.env
set +a
export P1R7B_TEST_DB="${P1R7B_TEST_DB:-muse_p1r7b_events_publish_test}"
case "$P1R7B_TEST_DB" in *_test) ;; *) echo "P1R7B_TEST_DB must end with _test"; exit 1;; esac
export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD"
JAVA_HOME=$(/usr/libexec/java_home -v 21) PATH="$JAVA_HOME/bin:$PATH" timeout 240 mvn test \
-pl muse-server \
-Dtest=P1rAiEventsPublishFlywayMigrationIT \
-Dflyway.postgresql.transactional.lock=false \
-Djava.net.useSystemProxies=false \
-DsocksProxyHost= \
-DsocksProxyPort= \
-DsocksNonProxyHosts="127.0.0.1|localhost|100.64.*|100.*" \
-Dhttp.proxyHost= \
-Dhttps.proxyHost= \
-Dp1r.flyway.url="jdbc:postgresql://${MUSE_POSTGRES_HOST}:${MUSE_POSTGRES_PORT}/${P1R7B_TEST_DB}" \
-Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \
-Dp1r.flyway.locations="filesystem:sql/muse"
```
### Coverage 状态只读核查
```bash
cd /Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0
jq '.summary' docs/superpowers/reports/p1r-api-coverage.json
jq -r '.operations[] | select(.domain=="events") | [.domain,.operationId,.method,.path,.implementationStatus,.completionStatus,.targetStage] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
jq -r '[.operations[] | select(.domain=="market") | (.implementationStatus + "/" + .completionStatus)] | group_by(.)[] | [(.[0]), length] | @tsv' docs/superpowers/reports/p1r-api-coverage.json
git diff -- docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md muse-cloud/scripts/p1r-audit-api-coverage.py
```
## Review Gate
每个 Task 的 review 必须记录:
1. implementer agent 名称或 ID。
2. spec reviewer 名称或 ID、结果、关键意见。
3. quality reviewer 名称或 ID、结果、关键意见。
4. FAIL 修复摘要和 re-review 结果。
5. 本 Task 的 focused test 命令与结果。
6. 本 Task 是否触碰受保护文件。
双 PASS 定义:
1. spec review PASS确认范围、契约、依赖方向、状态机、测试计划符合本执行版。
2. quality review PASS确认实现最小、可维护、错误路径完整、日志/审计安全、测试有实际断言。
3. 任一 FAIL 均不得进入下一 Task。
## 完成条件
执行版计划完成条件:
1. 本文件存在于 `docs/agent-specs/2026-06-06-P1R7bSourceOwnerPropagation执行版.md`
2. 包含目标、边界、全局架构、非目标、事实/推断/假设、文件路径、数据流、契约、迁移、状态机、幂等、失败重试、日志审计、测试、验证命令、review gate、完成条件、回滚策略。
3. 每个实现 Task 都包含文件范围、测试范围、验证命令和完成条件。
4. 不含不可执行空洞表述。
5. 受保护文件无改动。
6. 本轮计划修订差异只允许落在本执行版和必要 `.agent` 状态说明;整体 `git status` 可以继续包含“当前 dirty 文件处置表”列出的既有 AI 草稿 baseline禁止为满足计划完成条件而清理、回退或删除这些 baseline。
实现阶段完成条件:
1. AI done/error terminal event 能通过本域 outbox/worker 发布到 Events。
2. `muse_unified_event` 有 accepted 记录,且 SSE 可见查询能读到当前 tenant + ownerUserId 的事件。
3. duplicate/rejected/temporary failure/retry/dead_letter 均有测试。
4. payload allowlist 和敏感信息拒绝均有测试。
5. 依赖方向 gate 证明 AI 只依赖 events-apiEvents server 不反向依赖 source owner server。
6. Coverage 不被改口径Market 32 operations 保持 `dedicated / needs_verification`
7. 所有新增测试类、混合 gate 目标测试类和 `P1rAiEventsPublishFlywayMigrationIT` 都必须有 surefire XML 与 test count > 0 证据,禁止用空测试命令或未匹配测试类的 Maven 成功输出冒充通过。
## 回滚策略
计划阶段回滚:
1. 仅删除本执行版文档和 `docs/agent-specs/.agent` 即可回滚本轮计划产物。
2. 不触碰审阅版文档和其他 dirty baseline。
实现阶段代码回滚:
1. 回滚 AI server 对 `muse-module-events-api` 的依赖。
2. 回滚 AI outbox DO/Mapper/Service/Worker 与相关测试。
3. 回滚 `MuseAiRuntimeProjectionService` 写 outbox 的调用点。
4. 保留或回滚 migration 必须按数据库发布状态决策:如果 migration 未发布,可随代码一起回滚;如果 migration 已发布,不删除表,改为停止 worker 和保留空表,避免破坏已部署数据库。
5. 停止 worker 后AI terminal fact 仍保留,业务行为回到 P1R-7a 后的状态;统一 Events 不再接收新的 AI terminal event。
6. 因故停止 worker 期间已经 claim 为 `running` 的记录不得人工批量改状态;重新启用后由 `claim_expires_at` lease 识别 stale running 并进入 reclaim/retry/dead_letter 规则。
## 计划自检清单
1. 本计划没有要求修改 OpenAPI 或 scanner。
2. 本计划没有要求修改 coverage JSON/Markdown 来达成 coverage。
3. 本计划没有把 Events、P1R-7 或 Market 标为 `completed`
4. 本计划没有复用 AI runtime job 或 Market projection outbox。
5. 本计划明确了 AI publish outbox/job、worker、EventsPublishApi、`muse_unified_event`、SSE 可见的真实链路。
6. 本计划每个实现 Task 都要求 fresh subagent implementer + fresh spec review + fresh quality review。
7. 本计划要求双 PASS 前不得进入下一 Task。
8. 本计划把执行版完成和实现完成分开:执行版完成仍只是计划;实现完成也只推进 source propagation evidence / needs_verification。