oh-my-muse/docs/memorys/2026-06-08-P1R7fContentSourceOwnerPropagation真实链路.md
zizi f84c18fbfb test(p1r): 收口 Content 事件传播真实链路门禁
补齐 Content focused tests、P1R mixed gates、Flyway _test、dependency gate 与 evidence memory,证明 P1R-7f 只推进到 needs_verification,不代表 completed。
2026-06-08 23:17:54 +08:00

329 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:<sha256(tenantId|blockId|blockRevision|source_status_change) 前 32 位 hex>`
- `outboxId=content_out:<sha256(tenantId|blockId|blockRevision|source_status_change) 前 32 位 hex>`
- 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 062/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 029 tests passfailures/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 088 tests passfailures/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/sequenceNodifferent 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` testsfailures=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 passfailures/errors/skipped 均为 0。
关键输出:
```text
flyway_success=true
flyway_url=jdbc:postgresql://<host>/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 reviewPASS无 P0/P1/P2/P3 blocker。
- Dalton quality/data-integrity/testing reviewPASS无 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。