保留 P1R-7f 审阅版、执行版与 review gate 结论,明确本阶段只覆盖 saveBlock source attribution active 链路,不推进 completed。
16 KiB
16 KiB
P1R7f Content Source Owner Propagation 审阅版
结论
推荐进入 P1R-7f,但第一切片只覆盖 Content 用户保存 Block 后形成的 canonical source attribution 事实,不覆盖全部 Content 写命令、导入、解析、导出、planning、meta projection 或 completed approval。
推荐方案:
- 选择
ContentAppServiceImpl.saveBlock成功后写入的muse_content_block_source_attribution(source_status=active)作为 Content 第一批 source fact。 - 新增 Content 本域 Events publish outbox / worker,调用
EventsPublishApi,发布 Events 已声明的notification/source_status_change。 - 不把 Block 正文、sourceSnapshot 原始对象、auditReason、authorizationSnapshotId 或 lineage payload 放入 Events payload。
- 不修改 OpenAPI、scanner 或 coverage report;不推进 Content / Events / P1R-7 completed。
flowchart LR
Save["App saveBlock command"] --> Block["muse_content_block<br/>content + revision"]
Save --> Attr["muse_content_block_source_attribution<br/>blockId + revision + active"]
Attr --> Outbox["Content events publish outbox<br/>queued / running / retryable / published / dead_letter"]
Outbox --> Worker["Content worker<br/>claim + retry + stale-claim guard"]
Worker --> Api["EventsPublishApi<br/>events-api only"]
Api --> Unified[("muse_unified_event<br/>notification / source_status_change")]
Unified --> SSE["/app-api/muse/events<br/>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=100needsVerificationOperations=133incompleteOperations=0genericPersistenceOperations=0ssePlaceholderOperations=0
- Content 51 operations 当前均为
dedicated / needs_verification。 - 受保护文件 staged / unstaged diff 为空:
docs/api-contracts/account/openapi.yamldocs/api-contracts/market/openapi.yamldocs/api-contracts/ai/openapi.yamldocs/api-contracts/knowledge/openapi.yamldocs/api-contracts/events/openapi.yamlmuse-cloud/scripts/p1r-audit-api-coverage.pydocs/superpowers/reports/p1r-api-coverage.jsondocs/superpowers/reports/p1r-api-coverage.md
全局 P1R-7 边界
- P1R-7a 到 P1R-7e 已分别覆盖 Events SSE、AI、Knowledge、Market、Account source owner propagation evidence。
- P1R-7b / 7c / 7d / 7e 均只把对应真实链路推进到
needs_verificationevidence,不代表 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 subtypesource_status_change。- Events 当前没有
block_savednotification 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,并避免把正文变化伪装成 AIchunk。- 复用
notification/source_status_change是当前不改 OpenAPI 下的最小可行方案;如果未来产品需要明确的block_savedsubtype,应另起 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/ 当前 appuserId。 source_status_change可以表达 Content block source attribution 从用户保存命令进入 active 状态的用户可见摘要。
目标
- 证明 Content source owner 的真实 Block 保存事实可以通过 Content 本域 publish outbox / worker 发布到 Events。
- 让作品 owner 可以通过统一 SSE 看到自己的 Block 来源状态变更摘要。
- 建立 Content 与 Events 的单向依赖证据:Content server 依赖 events-api,不依赖 events-server;Events server 不反向依赖 Content server。
- 为后续 Content create/delete/split/merge/import/export/planning/meta projection 等事件扩展保留清晰边界。
非目标
- 不把 Content 51 operations 从
needs_verification推进到completed。 - 不把 Events
streamEvents或总 P1R-7 推进到completed。 - 不修改 Content / Events OpenAPI。
- 不修改 coverage scanner 或 coverage report。
- 不发布 Block 正文、完整 sourceSnapshot、lineage payload、license restriction、authorizationSnapshotId、auditReason、requestHash、command result snapshot 或错误堆栈。
- 不覆盖
create_block、delete_block、splitBlock、mergeBlocks、mergeBlockSuggestion、import/parse/export、planning、meta projection、admin risk action。 - 不让 Events server 查询 Content 表。
- 不把 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 只允许:
{
"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原始对象sourceObjectIdauthorizationSnapshotIdlineagePayloadlicenseRestrictionSnapshotauditReasonrequestHashresultSnapshot- 错误堆栈
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:<sha256(tenantId|blockId|revision|source_status_change)>。 - 唯一约束:建议覆盖
(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
saveBlocksource attribution 发布链路。 - Content server 依赖 events-api。
- Content 本域 outbox / worker / migration / focused tests。
- Events unified event focused E2E。
- Content
- 间接影响:
- 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 同步返回语义。
验收标准
审阅版通过后,执行版必须包含:
- Content outbox DO / Mapper / Service / Worker / 配置 / V21 migration 的文件边界。
ContentAppServiceImpl.saveBlock在 Block 更新和 attribution 写入后同事务创建 outbox 的事务边界。- payload exact allowlist 与敏感字段拒绝策略。
- worker success / duplicate / retryable / CommonResult error / rejected / blocked / dead_letter / stale claim tests。
- Content saveBlock source fact -> Content outbox -> worker -> EventsPublishApi ->
muse_unified_event-> SSE visible focused E2E。 - dependency gate:Content server 有 events-api、无 events-server;Events server 无 AI / Knowledge / Market / Member / Content server。
- V21 SQL 静态 gate 与 Flyway
_test。 - P1R mixed gates 继续确认 AI / Knowledge / Market / Account source owner gates 不回退,Content 仍为
dedicated / needs_verification。 - coverage scanner 只在
/tmp隔离副本运行,真实 worktree protected diff 为空。 git diff --check和 protected staged / unstaged diff gate。
待确认项
- 是否接受 P1R-7f 第一切片只覆盖
saveBlock,其他 Content 写命令进入后续切片。 - 是否接受在不改 OpenAPI 的前提下复用
notification/source_status_change表达 Content block source attribution active。 - 如果 reviewer 认为
source_status_change语义不适配 Content,是否先停止 P1R-7f 实现,改走 Events OpenAPI subtype 变更审批。