# P1R7f Content Source Owner Propagation 真实链路 日期:2026-06-08 ## 结论 P1R-7f Content source owner propagation 主体实现、V21 SQL、Content focused tests、P1R mixed executable gates、V21 Flyway `_test`、dependency tree gate、full reactor build、隔离 coverage scanner 与 protected diff gate 已形成 `needs_verification` evidence。 当前仍不能写 `completed`。本轮证据只代表 Content `saveBlock` 首次成功 -> `muse_content_block_source_attribution(source_status=active)` -> Content publish outbox -> Content worker -> EventsPublishApi -> `muse_unified_event` -> `/app-api/muse/events` 对 owner 可见的链路推进到证据层,不代表 Events / P1R-7 / Content completed。 本轮未修改 OpenAPI、coverage scanner 或真实 coverage 报告。 ## 已验证事实 ### 工作区与保护文件 - 正确 worktree:`/Users/qingse/.config/superpowers/worktrees/oh-my-muse/dev-1.0.0`。 - 分支:`dev/1.0.0`。 - 起点 HEAD:`68cee5d test(p1r): 收口 Account 事件传播真实链路门禁`。 - 本轮执行过 `git pull --ff-only origin dev/1.0.0`,结果为 `Already up to date.`。 - 受保护文件最终 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` ### 本轮实现范围 - Content server 新增直接依赖 `muse-module-events-api`。 - 新增 Content 本域 publish outbox: - `ContentEventPublishOutboxDO` - `ContentEventPublishOutboxMapper` - `ContentEventPublishOutboxService` - `ContentEventPublishOutboxServiceImpl` - `ContentEventPayloads` - `ContentEventPublishWorker` - `MuseContentEventsProperties` - `MuseContentEventsConfiguration` - `ContentAppServiceImpl.saveBlock` 在首次成功写入 `muse_content_block_source_attribution(source_status=active)` 后,同事务创建 Content Events publish outbox。 - command replay 与 revision conflict 不创建 outbox。 - 新增 V21 migration:`muse-cloud/sql/muse/V21__extend_content_events_publish_outbox.sql`。 ## 合同边界 - 第一切片只覆盖 `ContentAppServiceImpl.saveBlock` 首次成功路径。 - 不覆盖 `create_block`、`delete_block`、`splitBlock`、`mergeBlocks`、`mergeBlockSuggestion`。 - 不覆盖 import、parse、export、planning、meta projection、admin risk action。 - Events source tuple: - `sourceOwner=content` - `sourceType=content_block_source_attribution` - `sourceId=blockId` - `sourceRevision=blockRevision` - `eventType=notification` - notification type 固定为 `source_status_change`。 - 幂等 key: - `commandId=content_evt:` - `outboxId=content_out:` - outbox 唯一约束覆盖 `(tenant_id, block_id, block_revision, notification_type)`。 - payload exact allowlist: - 顶层只允许 `type/message/resourceRef/timestamp`。 - `resourceRef` 只允许 `resourceType/resourceId`。 - `message` 固定为 `内容来源状态已更新`。 - `resourceRef.resourceId` 保持 Number/Long 语义,值为 block id。 - `EventsPublishReqDTO.resourceId` 仍按 Events DTO 合同使用 String。 - 禁止发布正文、sourceSnapshot 原文、sourceObjectId、authorizationSnapshotId、lineagePayload、licenseRestrictionSnapshot、auditReason、requestHash、resultSnapshot、错误堆栈。 - worker 默认配置关闭:`muse.content.events.publish-worker.enabled=false`。 - worker 只发布 `source_status=active`,非 active / invalid owner / invalid payload 均 fail-closed。 - 终态回写使用 `publish_status='running' AND attempt_count=#{claimedAttemptCount}` 防止过期 claim 覆盖新终态;终态更新 0 行会记录 stale claim 诊断日志。 ## 验证证据 ### Full reactor build 执行: ```bash JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o clean install -DskipTests -Dspring-boot.repackage.skip=true ``` 结果:exit 0,62/62 reactor modules success,包含 `BUILD SUCCESS`。 ### Content focused tests 执行: ```bash JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \ -pl muse-module-content/muse-module-content-server -am \ -Dtest=ContentAppServiceTest,ContentEventPublishOutboxServiceTest,ContentEventPublishWorkerTest \ -Dsurefire.failIfNoSpecifiedTests=false ``` 结果:exit 0,29 tests pass,failures/errors/skipped 均为 0。 Surefire XML: | 测试类 | tests | failures | errors | skipped | |---|---:|---:|---:|---:| | `ContentAppServiceTest` | 13 | 0 | 0 | 0 | | `ContentEventPublishOutboxServiceTest` | 6 | 0 | 0 | 0 | | `ContentEventPublishWorkerTest` | 10 | 0 | 0 | 0 | 覆盖点: - `saveBlock` 首次成功路径在 source attribution insert 后创建 outbox。 - command replay 不创建 outbox。 - revision conflict 不更新 block、不写 source attribution、不创建 outbox。 - outbox payload exact allowlist 与敏感字段负断言。 - configured `maxAttempt` 与 `claimTimeoutSeconds` 生效。 - timestamp 优先使用 source attribution `createTime`,缺失时回退 outbox `createTime`。 - worker disabled 不 claim。 - accepted / duplicate accepted -> published。 - rejected / blocked -> dead_letter。 - CommonResult error / exception -> retryable 或 retry exhausted dead_letter。 - invalid payload / invalid owner / 非 active source status -> dead_letter 且不调用 EventsPublishApi。 - terminal update 具备 claim attempt ownership guard。 - stale terminal update 记录诊断日志。 ### P1R mixed executable gates 执行: ```bash JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test \ -pl muse-server -am \ -Dtest=P1rContentEventsPublishMigrationSqlTest,P1rContentEventsPublishDependencyTest,P1rContentEventsPublishEndToEndTest,P1rContentRealApiGateTest,P1rAccountEventsPublishMigrationSqlTest,P1rAccountEventsPublishDependencyTest,P1rAccountEventsPublishEndToEndTest,P1rAccountRealApiGateTest,P1rAccountRouteOwnershipTest,P1rMarketEventsPublishMigrationSqlTest,P1rMarketEventsPublishDependencyTest,P1rMarketEventsPublishEndToEndTest,P1rKnowledgeEventsPublishMigrationSqlTest,P1rKnowledgeEventsPublishDependencyTest,P1rKnowledgeEventsPublishEndToEndTest,P1rAiEventsPublishMigrationSqlTest,P1rAiEventsPublishDependencyTest,P1rAiEventsPublishEndToEndTest,P1rApiCoverageReportTest,P1rEventsRealApiGateTest,P1rEventsRouteOwnershipTest,P1rAiRealApiGateTest,P1rAiRouteOwnershipTest \ -Dsurefire.failIfNoSpecifiedTests=false ``` 结果:exit 0,88 tests pass,failures/errors/skipped 均为 0。 Surefire XML: | 测试类 | tests | failures | errors | skipped | |---|---:|---:|---:|---:| | `P1rContentEventsPublishMigrationSqlTest` | 3 | 0 | 0 | 0 | | `P1rContentEventsPublishDependencyTest` | 2 | 0 | 0 | 0 | | `P1rContentEventsPublishEndToEndTest` | 5 | 0 | 0 | 0 | | `P1rContentRealApiGateTest` | 5 | 0 | 0 | 0 | | `P1rAccountEventsPublishMigrationSqlTest` | 3 | 0 | 0 | 0 | | `P1rAccountEventsPublishDependencyTest` | 2 | 0 | 0 | 0 | | `P1rAccountEventsPublishEndToEndTest` | 4 | 0 | 0 | 0 | | `P1rAccountRealApiGateTest` | 4 | 0 | 0 | 0 | | `P1rAccountRouteOwnershipTest` | 4 | 0 | 0 | 0 | | `P1rMarketEventsPublishMigrationSqlTest` | 3 | 0 | 0 | 0 | | `P1rMarketEventsPublishDependencyTest` | 2 | 0 | 0 | 0 | | `P1rMarketEventsPublishEndToEndTest` | 4 | 0 | 0 | 0 | | `P1rKnowledgeEventsPublishMigrationSqlTest` | 3 | 0 | 0 | 0 | | `P1rKnowledgeEventsPublishDependencyTest` | 2 | 0 | 0 | 0 | | `P1rKnowledgeEventsPublishEndToEndTest` | 5 | 0 | 0 | 0 | | `P1rAiEventsPublishMigrationSqlTest` | 4 | 0 | 0 | 0 | | `P1rAiEventsPublishDependencyTest` | 2 | 0 | 0 | 0 | | `P1rAiEventsPublishEndToEndTest` | 4 | 0 | 0 | 0 | | `P1rApiCoverageReportTest` | 5 | 0 | 0 | 0 | | `P1rEventsRealApiGateTest` | 5 | 0 | 0 | 0 | | `P1rEventsRouteOwnershipTest` | 4 | 0 | 0 | 0 | | `P1rAiRealApiGateTest` | 6 | 0 | 0 | 0 | | `P1rAiRouteOwnershipTest` | 7 | 0 | 0 | 0 | 覆盖点: - Content server 直接依赖 `muse-module-events-api`,不依赖 `muse-module-events-server`。 - Events server 不反向依赖 AI / Knowledge / Market / Member / Content server。 - V21 SQL 静态断言覆盖字段、唯一约束、check 约束、索引、trigger、BaseDO/TenantBaseDO 审计列、敏感列和外键禁用。 - focused E2E 使用真实 `EventsPublishServiceImpl`,只 mock / in-memory `UnifiedEventMapper`,避免 bare fake EventsPublishApi 假绿。 - Content notification 只对 owner 可见,非 owner 不可见。 - duplicate same block revision 复用同一 eventId/sequenceNo,different revision 产生不同 notification。 - rejected -> dead_letter 且 SSE visible query 为空。 - non-active source status -> dead_letter 且不调用 Events。 - stale claim terminal update attempt mismatch 不覆盖当前 running outbox。 - 同时回归 Account/Market/Knowledge/AI/Events 既有 P1R gates 与 coverage report gate。 ### Surefire XML 计数核对 clean build 后已重新运行 focused tests 与 P1R mixed executable gates,以保留最终 surefire XML。 结果:Content focused + P1R mixed 目标 XML 合计 `117` tests,failures=0、errors=0、skipped=0。 ### V21 Flyway `_test` 准备: ```bash source ~/.config/muse-repo/infra.env createdb -h "$MUSE_POSTGRES_HOST" -p "$MUSE_POSTGRES_PORT" -U "$MUSE_POSTGRES_USERNAME" muse_p1r7f_content_events_publish_test ``` 执行: ```bash source ~/.config/muse-repo/infra.env export P1R_FLYWAY_PASSWORD="$MUSE_POSTGRES_PASSWORD" JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o test -pl muse-server -am \ -Dtest=P1rContentEventsPublishFlywayMigrationIT \ -Dflyway.postgresql.transactional.lock=false \ -Dp1r.flyway.locations=filesystem:sql/muse \ -Dp1r.flyway.url="jdbc:postgresql://$MUSE_POSTGRES_HOST:$MUSE_POSTGRES_PORT/muse_p1r7f_content_events_publish_test" \ -Dp1r.flyway.user="$MUSE_POSTGRES_USERNAME" \ -Djava.net.useSystemProxies=false \ -DsocksProxyHost= \ -DsocksProxyPort= \ -Dhttp.proxyHost= \ -Dhttp.proxyPort= \ -Dhttps.proxyHost= \ -Dhttps.proxyPort= \ -Dsurefire.failIfNoSpecifiedTests=false ``` 结果:exit 0,`P1rContentEventsPublishFlywayMigrationIT` 4 tests pass,failures/errors/skipped 均为 0。 关键输出: ```text flyway_success=true flyway_url=jdbc:postgresql:///muse_p1r7f_content_events_publish_test migrations_executed=21 successful_migration_count=21 target_schema_version=21 flyway_latest=21:extend content events publish outbox v21_table=muse_content_event_publish_outbox v21_indexes=idx_muse_content_event_publish_outbox_claim,idx_muse_content_event_publish_outbox_owner_status,idx_muse_content_event_publish_outbox_block_revision v21_triggers=trg_muse_content_event_publish_outbox_update_time ``` Flyway IT 覆盖: - 只允许 `_test` 数据库。 - 拒绝 JDBC URL 携带 credential query。 - V1-V21 clean migrate。 - V21 表、索引、约束、trigger 存在。 - `event_type='chunk'`、`notification_type!='source_status_change'`、`source_status='deleted'`、非法 `publish_status`、非法 owner/block/resource/attempt 会被数据库拒绝。 调试留痕: - 首次 Flyway `_test` 失败于测试 helper 对 PostgreSQL 单值 check constraint 的判定过窄,真实约束为 `CHECK (((event_type)::text = 'notification'::text))`,生产 SQL 未改,修正的是测试判定函数。 - 第二次运行出现长等待,线程栈显示新建 JDBC 连接或 Flyway advisory lock 阶段等待;最终通过复用已建立 connection 做非法 insert 验收并在每次失败后 rollback,避免连接层不稳定干扰 schema 断言。 ### Dependency tree gate 执行: ```bash JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-content/muse-module-content-server -DskipTests dependency:tree > /tmp/p1r7f-content-server-dependency-tree.txt JAVA_HOME=$(/usr/libexec/java_home -v 21) mvn -o -pl muse-module-events/muse-module-events-server -DskipTests dependency:tree > /tmp/p1r7f-events-server-dependency-tree.txt rg "muse-module-events-api" /tmp/p1r7f-content-server-dependency-tree.txt ! rg "muse-module-events-server" /tmp/p1r7f-content-server-dependency-tree.txt ! rg "muse-module-(ai|knowledge|market|member|content)-server" /tmp/p1r7f-events-server-dependency-tree.txt ``` 结果: - Content server tree 命中 `muse-module-events-api`。 - Content server tree 未命中 `muse-module-events-server`。 - Events server tree 未命中 AI / Knowledge / Market / Member / Content server。 ### Coverage scanner 隔离副本 首次尝试在临时 venv 安装 PyYAML 失败,原因为 pip TLS 证书校验失败,不计入 scanner 失败。随后确认系统 Python 已有 PyYAML 6.0.3,复用同一 `/tmp` 隔离副本执行 scanner。 执行: ```bash tmpdir=$(mktemp -d /tmp/p1r7f-coverage-scan.XXXXXX) rsync -a --delete --exclude .git --exclude 'muse-cloud/**/target' ./ "$tmpdir"/ cd "$tmpdir" python3 muse-cloud/scripts/p1r-audit-api-coverage.py --check ``` 结果:exit 0,仅在 `/tmp/p1r7f-coverage-scan.N1DHvZ` 生成报告。 summary: ```text completedOperations=100 needsVerificationOperations=133 incompleteOperations=0 genericPersistenceOperations=0 ssePlaceholderOperations=0 ``` ### Protected diff 与 whitespace 执行: ```bash git diff --check git diff --name-only -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md git diff --cached --name-only -- docs/api-contracts/account/openapi.yaml docs/api-contracts/market/openapi.yaml docs/api-contracts/ai/openapi.yaml docs/api-contracts/knowledge/openapi.yaml docs/api-contracts/events/openapi.yaml muse-cloud/scripts/p1r-audit-api-coverage.py docs/superpowers/reports/p1r-api-coverage.json docs/superpowers/reports/p1r-api-coverage.md ``` 结果: - `git diff --check` exit 0。 - protected unstaged diff 输出为空。 - protected staged diff 输出为空。 ## 推断 - 当前实现与 P1R-7f 执行版第一切片一致:只把 Content `saveBlock` 产生的 active source attribution terminal fact 推送到 Events notification 链路,不试图把 Content 全部写命令一次性纳入 Events。 - V21 DB check、worker fail-closed、SQL gate、Flyway `_test` 与 E2E 共同降低了把 Content save 误发布为 `chunk` 或把 deleted source status 发布给 SSE 的风险。 ## 假设 - 本轮 focused E2E 使用 in-memory / mocked mapper 验证 `EventsPublishServiceImpl` 语义,仍不是完整生产运行时。 - 后续如果要推进 Content / Events / P1R-7 completed,仍需要单独 approval gate,不能复用本文件作为 completed 依据。 ## 后续动作 ## Fresh implementation review fresh implementation 双 review 已通过: - Hubble correctness/spec review:PASS,无 P0/P1/P2/P3 blocker。 - Dalton quality/data-integrity/testing review:PASS,无 P0/P1/P2/P3 blocker。 Hubble 复核了正确 worktree、protected diff、scope、`saveBlock` 接入点、outbox/worker source tuple、V21 防 `chunk/deleted`、真实 `EventsPublishServiceImpl` E2E、依赖边界与 `git diff --check`。 Dalton 复核了 V21 migration 的 NOT NULL / unique / check / index / trigger、无 FK/敏感列、事务 outbox、幂等复用、payload exact allowlist、worker 状态机、focused/P1R/Flyway 测试质量与 Surefire XML 防空跑,读取目标 XML 得到 121 tests 且 failures/errors/skipped 均为 0。 ## 后续动作 - 可按逻辑分组准备提交。 - 提交、push 或推进 completed 仍需用户明确批准。 - 本轮双 PASS 仍不代表 Events / P1R-7 / Content completed。