# 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 本域事实
muse_ai_task_event done/error"] --> Outbox["AI publish outbox/job
muse_ai_event_publish_outbox"] Outbox --> Worker["AI publish worker
claim + retry + dead_letter"] Worker --> Api["EventsPublishApi
只依赖 muse-module-events-api"] Api --> Store[("muse_unified_event
accepted/rejected/blocked")] Store --> Query["Events visible query
tenant + ownerUserId + sequence"] Query --> SSE["/app-api/muse/events
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` 明确本仓是设计文档 SSOT,Source 传播采用事件驱动 + 各模块自治,统一实时通信为 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 tests,Events 和 Market 仍不得写成 `completed`。 7. `docs/superpowers/plans/2026-06-05-P1R-7a-events-sse.md` 固定跨 owner 发布策略为 source owner 本域 outbox + worker 调用 Events API;Events 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:`,长度 `<= 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:`;禁止使用包含 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 1:Preflight 与依赖/现状冻结 执行角色: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 2:AI 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:` 或等价短 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 3:AI 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:` 或等价短 hash 格式,测试覆盖长度 `<= 128`,禁止长拼接格式作为推荐或默认实现。 8. `MuseAiRuntimeProjectionService` 在写 terminal task event 的同一事务内调用 outbox service。 9. `MuseJobServiceImpl.appendTaskCancellationEventIfAbsent` 在写 cancellation `error` terminal event 的同一事务内调用 outbox service;payload 只能包含 `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:` 或等价短 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 4:AI 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,写入 outbox,worker 调用 `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-api,Events 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。