补齐 Content focused tests、P1R mixed gates、Flyway _test、dependency gate 与 evidence memory,证明 P1R-7f 只推进到 needs_verification,不代表 completed。
329 lines
16 KiB
Markdown
329 lines
16 KiB
Markdown
# 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 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://<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 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。
|