# P1R7f Content Source Owner Propagation 审阅版 ## 结论 推荐进入 P1R-7f,但第一切片只覆盖 Content 用户保存 Block 后形成的 canonical source attribution 事实,不覆盖全部 Content 写命令、导入、解析、导出、planning、meta projection 或 completed approval。 推荐方案: 1. 选择 `ContentAppServiceImpl.saveBlock` 成功后写入的 `muse_content_block_source_attribution(source_status=active)` 作为 Content 第一批 source fact。 2. 新增 Content 本域 Events publish outbox / worker,调用 `EventsPublishApi`,发布 Events 已声明的 `notification/source_status_change`。 3. 不把 Block 正文、sourceSnapshot 原始对象、auditReason、authorizationSnapshotId 或 lineage payload 放入 Events payload。 4. 不修改 OpenAPI、scanner 或 coverage report;不推进 Content / Events / P1R-7 completed。 ```mermaid flowchart LR Save["App saveBlock command"] --> Block["muse_content_block
content + revision"] Save --> Attr["muse_content_block_source_attribution
blockId + revision + active"] Attr --> Outbox["Content events publish outbox
queued / running / retryable / published / dead_letter"] Outbox --> Worker["Content worker
claim + retry + stale-claim guard"] Worker --> Api["EventsPublishApi
events-api only"] Api --> Unified[("muse_unified_event
notification / source_status_change")] Unified --> SSE["/app-api/muse/events
work owner visible"] ``` 本审阅版只冻结目标、非目标、取舍、风险和验收标准。执行版、实现和提交必须在本审阅版 fresh spec review + fresh quality / feasibility review 双 PASS 后再继续。 ## Review Gate P1R-7f 审阅版已通过 fresh review: - Harvey spec / scope review:PASS,无 P0/P1/P2;已验证本审阅版严格停留在 Content `saveBlock -> block source attribution -> Events notification` 第一切片,没有把 completed、OpenAPI、coverage 或全部 Content 写命令混入。 - Chandrasekhar quality / feasibility review:PASS,无 P0/P1/P2;已验证 `saveBlock -> muse_content_block_source_attribution(source_status=active) -> Content outbox -> EventsPublishApi -> muse_unified_event -> SSE` 路径可落地,并确认 Content 不应误用 `chunk`,执行版必须加入防误用 `chunk` 的测试或静态 gate。 Halley quality reviewer 长时间超时未返回,已丢弃,不计入 review gate。 审阅版双 PASS 只允许进入执行版编写,不代表 P1R-7f 实现完成,也不代表 Content / Events / P1R-7 completed。执行版完成后仍必须派发 fresh execution spec review + fresh execution quality / feasibility review;执行版双 PASS 前不得实现代码、提交、push 或推进 completed。 ## 已验证事实 ### 工作区与状态 - 正确 worktree:`/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0`。 - 分支:`dev/1.0.0`。 - 当前 HEAD:`68cee5d test(p1r): 收口 Account 事件传播真实链路门禁`。 - 当前分支与 `origin/dev/1.0.0` 对齐。 - `git pull --ff-only origin dev/1.0.0` 返回 `Already up to date.`。 - 当前 coverage summary: - `completedOperations=100` - `needsVerificationOperations=133` - `incompleteOperations=0` - `genericPersistenceOperations=0` - `ssePlaceholderOperations=0` - Content 51 operations 当前均为 `dedicated / needs_verification`。 - 受保护文件 staged / unstaged diff 为空: - `docs/api-contracts/account/openapi.yaml` - `docs/api-contracts/market/openapi.yaml` - `docs/api-contracts/ai/openapi.yaml` - `docs/api-contracts/knowledge/openapi.yaml` - `docs/api-contracts/events/openapi.yaml` - `muse-cloud/scripts/p1r-audit-api-coverage.py` - `docs/superpowers/reports/p1r-api-coverage.json` - `docs/superpowers/reports/p1r-api-coverage.md` ### 全局 P1R-7 边界 - P1R-7a 到 P1R-7e 已分别覆盖 Events SSE、AI、Knowledge、Market、Account source owner propagation evidence。 - P1R-7b / 7c / 7d / 7e 均只把对应真实链路推进到 `needs_verification` evidence,不代表 Events / P1R-7 / 对应业务域 completed。 - P1R-7 全局审阅版把 P1R-7f 定义为 `Content canonical change / block saved / export task event propagation`,状态口径仍是只推进 `needs_verification` 证据。 - P1R-7 completed approval 预检明确:如果 Account / Content 仍是硬前置,则应继续 P1R-7e Account 与 P1R-7f Content,而不是直接 completed。Account 已在 P1R-7e 收口并推送,因此 Content 是剩余 owner 切片。 ### Events 可复用合同 - `EventsPublishReqDTO` 已包含 `commandId`、`tenantId`、`ownerUserId`、`sourceOwner`、`sourceType`、`sourceId`、`sourceRevision`、`eventType`、`resourceType`、`resourceId`、`payloadSummary`、`emittedAt`。 - `EventsPublishServiceImpl` 先按 `commandId` 回放,再按 source tuple 回放。 - `docs/api-contracts/events/openapi.yaml` 已声明 `notification` 事件,`SSENotificationEvent.data.type` 允许 `source_status_change`。 - `EventsPublishServiceImpl` 与 `EventsStreamServiceImpl` 均允许 notification subtype `source_status_change`。 - Events 当前没有 `block_saved` notification subtype;本阶段不得新增 subtype 或修改 Events OpenAPI。 - `SSEChunkEvent` 语义是 AI 生成文本片段,要求 `content/sequenceNo`。Content block saved 不应使用 `chunk`,否则会泄露正文或伪造 AI chunk 语义。 ### Content 当前事实模型 - Content owner 位于 `muse-module-content/muse-module-content-server`。 - `muse-module-content-server/pom.xml` 当前没有依赖 `muse-module-events-api`,也没有依赖 `muse-module-events-server`。 - 最新 migration 当前是 `V20__extend_account_events_publish_outbox.sql`;若 P1R-7f 进入实现且 live 文件列表不变,下一 migration 应为 `V21`。 - `ContentAppServiceImpl.saveBlock` 在同一事务内: - 用 `ContentCommandService.reserveCommand` 对 `commandId` 做幂等预占。 - 通过 `requireOwnedBlock(userId, workId, blockId)` 校验作品 owner。 - 校验 Block `expectedRevision`。 - 更新 `muse_content_block.content_text / word_count / revision`。 - 写入 `muse_content_block_source_attribution`,其中 `workId`、`blockId`、`revision`、`sourceType`、`sourceObjectId`、`sourceVersion`、`authorizationSnapshotId`、`sourceStatus=active`。 - 调用 `ContentAuditService.recordSucceededWithAudit` 写入 command result snapshot 与 audit 摘要。 - `muse_content_block_source_attribution` 有独立自增 `id`,并有唯一约束 `(tenant_id, block_id, revision)`。 - `WorkDO.ownerUserId` 是作品 owner;`saveBlock` 的 `userId` 经 owner guard 后与该 owner 一致。 - `BlockSaveReqVO` 要求 `commandId`、`content`、`expectedRevision` 和 `sourceSnapshot`。 ## 推断 - P1R-7f 第一切片应选择 `saveBlock`,因为它是 Content 自有 canonical change,具备 owner、workId、blockId、revision、commandId、requestHash、source attribution 和审计链路。 - `muse_content_block_source_attribution` 比 `muse_content_block` 更适合作为第一批 source fact:它表达“该 Block revision 的来源状态 active”,可以自然映射到 `source_status_change`,并避免把正文变化伪装成 AI `chunk`。 - 复用 `notification/source_status_change` 是当前不改 OpenAPI 下的最小可行方案;如果未来产品需要明确的 `block_saved` subtype,应另起 Events OpenAPI 合同审批。 - 不应把 Content 全部写命令放进 P1R-7f 第一切片。`create_block`、`delete_block`、`splitBlock`、`mergeBlocks`、导入、解析、导出、planning、meta projection 都有不同语义和不同敏感 payload 边界,混入会扩大 blast radius。 - `muse_content_block_source_attribution` 是 source fact,不应直接兼任 publish outbox。它没有 claim lease、attempt、retry、published event、dead_letter、last_error 等发布补偿语义。 ## 假设 - P1R-7f 允许在 Content owner 内新增独立 Events publish outbox 表或等价状态表。 - P1R-7f 允许 `muse-module-content-server` 新增对 `muse-module-events-api` 的直接依赖,但不允许依赖 `muse-module-events-server`。 - Content block save notification 的 SSE owner 是作品 owner,即 `WorkDO.ownerUserId` / 当前 app `userId`。 - `source_status_change` 可以表达 Content block source attribution 从用户保存命令进入 active 状态的用户可见摘要。 ## 目标 1. 证明 Content source owner 的真实 Block 保存事实可以通过 Content 本域 publish outbox / worker 发布到 Events。 2. 让作品 owner 可以通过统一 SSE 看到自己的 Block 来源状态变更摘要。 3. 建立 Content 与 Events 的单向依赖证据:Content server 依赖 events-api,不依赖 events-server;Events server 不反向依赖 Content server。 4. 为后续 Content create/delete/split/merge/import/export/planning/meta projection 等事件扩展保留清晰边界。 ## 非目标 1. 不把 Content 51 operations 从 `needs_verification` 推进到 `completed`。 2. 不把 Events `streamEvents` 或总 P1R-7 推进到 `completed`。 3. 不修改 Content / Events OpenAPI。 4. 不修改 coverage scanner 或 coverage report。 5. 不发布 Block 正文、完整 sourceSnapshot、lineage payload、license restriction、authorizationSnapshotId、auditReason、requestHash、command result snapshot 或错误堆栈。 6. 不覆盖 `create_block`、`delete_block`、`splitBlock`、`mergeBlocks`、`mergeBlockSuggestion`、import/parse/export、planning、meta projection、admin risk action。 7. 不让 Events server 查询 Content 表。 8. 不把 SSE `chunk` 用作 Content block save 事件。 ## 推荐方案 ### 事件选择 第一批只允许: | Content source fact | 触发入口 | Events eventType | notification type | SSE owner | |---|---|---|---|---| | `muse_content_block_source_attribution(source_status=active)` | `ContentAppServiceImpl.saveBlock` | `notification` | `source_status_change` | `WorkDO.ownerUserId` | 暂不选择: | 候选事实 | 暂不选择原因 | |---|---| | `muse_content_block` 正文变化 | 直接以正文变化为 SSE payload 有泄露风险;应通过 source attribution 摘要表达 | | `create_block` | 创建类命令首次执行前 targetId 为空,source tuple 和 replay 边界需要单独冻结 | | `delete_block` | 删除前 source attribution 是 deleted snapshot,用户可见语义与保存不同 | | `splitBlock` / `mergeBlocks` | 一次命令可能影响多个 Block 和章节 revision,第一切片不扩大结构编辑语义 | | import / parse / export task | 异步任务状态和文件凭证边界不同,容易误暴露内部任务或下载信息 | | planning / meta projection | 更接近 AI / Meta 协作投影,需单独定义用户可见摘要 | | admin risk action | 属于治理动作,已有 Market governance 类似模式但 Content 侧需单独审阅 | ### Payload allowlist 第一批 payload 只允许: ```json { "type": "source_status_change", "message": "内容来源状态已更新", "resourceRef": { "resourceType": "content_block", "resourceId": 8001 }, "timestamp": "2026-06-08T12:00:00" } ``` 执行版可以把 message 收窄为安全固定枚举,例如: - `内容来源状态已更新` - `Block 来源状态已更新` 不得把以下字段放入 Events payload: - Block 正文 `contentText` - 请求正文 `content` - `sourceSnapshot` 原始对象 - `sourceObjectId` - `authorizationSnapshotId` - `lineagePayload` - `licenseRestrictionSnapshot` - `auditReason` - `requestHash` - `resultSnapshot` - 错误堆栈 ### Outbox 边界 推荐新增独立 Content Events publish outbox: - source table:`muse_content_block_source_attribution`。 - source tuple:`sourceOwner=content`,`sourceType=content_block_source_attribution`,`sourceId=blockId`,`sourceRevision=blockRevision`。 - commandId:稳定短幂等键,建议 `content_evt:`。 - 唯一约束:建议覆盖 `(tenant_id, block_id, block_revision, notification_type)`,避免同一 Block revision 重复发布。 - 状态机:`queued/running/retryable/published/dead_letter`。 - claim:必须原子领取,具备 lease / retry / attempt ownership。 - 终态回写:必须绑定本次 claimed attempt,避免 stale worker 覆盖新 claim。 - worker 默认关闭,配置启用后才 claim。 - invalid owner、invalid payload 或 missing attribution 必须 fail-closed,不得发布给 owner `0` 或其他用户。 ### 依赖边界 - Content server 可以依赖 `muse-module-events-api`。 - Content server 不得依赖 `muse-module-events-server`。 - Events server 不得依赖 Content server。 - `muse-server` 作为装配层可以同时包含 Content server 与 Events server。 ## 关键取舍 - 选择 `saveBlock`,不选择全部 Content 写命令:第一切片需要最小真实链路,避免把结构编辑、任务状态和治理语义混在一起。 - 选择 block source attribution,不选择正文本身:source attribution 是已落库、可审计、可脱敏的 source fact;正文不应进入统一 SSE payload。 - 选择 `notification/source_status_change`,不新增 `block_saved`:当前 Events 合同已声明该 subtype;新增 subtype 必须另起 OpenAPI 审批。 - 不使用 SSE `chunk`:`chunk` 是 AI 生成文本片段语义,Content 保存正文不应伪造为 AI chunk,也不应发布正文片段。 - 不复用 attribution 表作为 publish queue:source fact 与 publish compensation 是两个职责,混用会缺 claim、retry 和 dead_letter 证据。 ## 影响范围 - 直接影响: - Content `saveBlock` source attribution 发布链路。 - Content server 依赖 events-api。 - Content 本域 outbox / worker / migration / focused tests。 - Events unified event focused E2E。 - 间接影响: - P1R-7 completed approval 的 source owner evidence 更完整。 - 不影响: - OpenAPI 合同。 - coverage scanner / coverage report。 - Content 51 operations completion status。 - Content `saveBlock` 同步返回语义。 - Import / parse / export / planning / meta projection / admin governance 业务逻辑。 ## 风险与兼容性 - 语义风险:`source_status_change` 原先主要用于 Knowledge;执行版必须说明 Content 使用该 subtype 的语义边界,不能把它扩展成万能通知。 - 事件重复风险:同一 `(blockId, revision)` retry 不能创建多条 unified event;必须用 commandId 与 source tuple 双重幂等。 - 敏感信息风险:正文、sourceSnapshot 原文、authorizationSnapshotId、lineage、license restriction 和 auditReason 不得进入 payload。 - owner 风险:SSE owner 必须是作品 owner,不能由 path 参数或 payload 自带 owner 推断。 - 并发风险:`saveBlock` 已有 revision guard;outbox 创建必须与成功 attribution 写入同事务,且不能在 command replay 分支重复写 outbox。 - 可达性风险:如果实现时发现 `sourceAttributionMapper.insert` 后无法可靠拿到 attribution id,应以 `(blockId, revision)` 作为 source tuple,不为拿 id 改动既有 mapper 行为。 - 兼容性:新增 outbox 与 worker 默认关闭,不改变现有 Content API 同步返回语义。 ## 验收标准 审阅版通过后,执行版必须包含: 1. Content outbox DO / Mapper / Service / Worker / 配置 / V21 migration 的文件边界。 2. `ContentAppServiceImpl.saveBlock` 在 Block 更新和 attribution 写入后同事务创建 outbox 的事务边界。 3. payload exact allowlist 与敏感字段拒绝策略。 4. worker success / duplicate / retryable / CommonResult error / rejected / blocked / dead_letter / stale claim tests。 5. Content saveBlock source fact -> Content outbox -> worker -> EventsPublishApi -> `muse_unified_event` -> SSE visible focused E2E。 6. dependency gate:Content server 有 events-api、无 events-server;Events server 无 AI / Knowledge / Market / Member / Content server。 7. V21 SQL 静态 gate 与 Flyway `_test`。 8. P1R mixed gates 继续确认 AI / Knowledge / Market / Account source owner gates 不回退,Content 仍为 `dedicated / needs_verification`。 9. coverage scanner 只在 `/tmp` 隔离副本运行,真实 worktree protected diff 为空。 10. `git diff --check` 和 protected staged / unstaged diff gate。 ## 待确认项 1. 是否接受 P1R-7f 第一切片只覆盖 `saveBlock`,其他 Content 写命令进入后续切片。 2. 是否接受在不改 OpenAPI 的前提下复用 `notification/source_status_change` 表达 Content block source attribution active。 3. 如果 reviewer 认为 `source_status_change` 语义不适配 Content,是否先停止 P1R-7f 实现,改走 Events OpenAPI subtype 变更审批。