From 297c4c173cdde373a4634770e180ed9d7587f872 Mon Sep 17 00:00:00 2001 From: wangdl Date: Mon, 22 Jun 2026 19:44:39 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20M-AI-06-GATE=20audit=20=E2=80=94=20PASS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All 14 completion criteria verified. 482 tests, 0 failures. P0: 0, P1: 0, P2: 1 (non-blocking), P3: 3 (non-scope). Co-Authored-By: Claude --- docs/architecture/m-ai-06-gate-audit.md | 306 ++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100644 docs/architecture/m-ai-06-gate-audit.md diff --git a/docs/architecture/m-ai-06-gate-audit.md b/docs/architecture/m-ai-06-gate-audit.md new file mode 100644 index 0000000..ed739dc --- /dev/null +++ b/docs/architecture/m-ai-06-gate-audit.md @@ -0,0 +1,306 @@ +# M-AI-06-GATE:Feynman ReviewCard 子 Job 迁移验收 + +> 审核日期:2026-06-22 +> 对应里程碑:M-AI-06 Feynman ReviewCard 子 Job 与可靠生成 +> 审核范围:Issue #312 ~ #319 + +--- + +## 一、完成标准逐条验证 + +### 1. Parent Projector 原子创建 Child Job、Snapshot、Outbox ✅ + +**证据**:`feynman-projector.ts:248-268` + +```ts +const childJob = await this.creationService.createInTransaction(tx, { + userId: job.userId, + jobType: 'review_card_generation', + triggerType: 'parent_job', + ... + retrySnapshotContent: rcSnapshot as unknown as Record, +}); +``` + +- `createInTransaction(tx, input)` 使用 Parent Projector 的同一 `tx` +- Child Job + Snapshot + Outbox 全部在同一个 Prisma 事务中 +- `ai-job-creation.service.ts:128-169`:`createInTransaction` 不自行开启事务 + +**测试**:`feynman-projector.spec.ts` — "child_job 模式:在同一事务中创建 Child Job + Snapshot" + +--- + +### 2. 一个 Parent 最多一个 Child Job ✅ + +**证据**: + +- `feynman-projector.ts:262`:`idempotencyKey: 'review-card:feynman:${job.id}'` +- `schema.prisma:636`:`@@unique([userId, jobType, idempotencyKey])` +- `ai-job-creation.service.ts:143-155`:check-before-create 幂等 + +**测试**:`feynman-projector.spec.ts` — "入口幂等:已有 Artifact → 不重复创建 Child Job" + +--- + +### 3. Child Definition 正确注册 ✅ + +**证据**:`review-card-generation-job-definition.ts` + +| 字段 | 值 | 验证 | +|------|---|------| +| jobType | `review_card_generation` | 格式 `/^[a-z][a-z0-9_]{1,63}$/` ✅ | +| queueName | `ai-background` | 在 `ALLOWED_QUEUE_NAMES` ✅ | +| modelTier | `cheap` | 与 Legacy 一致 ✅ | +| modelName | `deepseek-v4-flash` | 与 `model-router.ts:20` 一致 ✅ | +| promptKey | `review-card-generation` | 与 Legacy 一致 ✅ | +| promptVersion | `1.0.0` | 与 `prompt-template.service.ts:46-49` 一致 ✅ | +| projectorKey | `review_card_generation_projector` | 已注册 ✅ | +| domain | `generation` | 区分于 `analysis` ✅ | + +**测试**:`review-card-generation-registration.service.spec.ts` — "Registry 注册成功" + "重复注册失败" + 13 字段验证 + +--- + +### 4. Child Executor 只通过 AiGateway ✅ + +**证据**:`review-card-generation-executor.ts:39` + +```ts +constructor(private readonly aiGateway: AiGatewayService) {} +``` + +- 无 PrismaService 注入 +- 无 Provider SDK 导入 +- `execute()` 仅调用 `this.aiGateway.generate()` + +**测试**:`review-card-generation-executor.spec.ts` — "不写数据库(无 Prisma 依赖)" + +--- + +### 5. 保持旧 ReviewCard Prompt 和 cheap tier ✅ + +**证据**:`review-card-generation-executor.ts:83-94` + +```ts +const response = await this.aiGateway.generate({ + feature: 'review-card-generation', + tier: s.modelTier, // = 'cheap' from Definition + promptKey: s.promptKey, // = 'review-card-generation' + promptVersion: s.promptVersion, // = '1.0.0' + outputSchema: ReviewCardGenerationResultSchema, + ... +}); +``` + +- userMessage 格式与 `review-card-generation.workflow.ts:18-28` 一致 +- 全部通过 Snapshot 读取(来自 Definition) + +**测试**:测试验证 "feature/promptKey/tier 与旧链路兼容" + +--- + +### 6. Child Projector 原子创建 ReviewCard 和 Artifact ✅ + +**证据**:`review-card-generation-projector.ts:77-98` + +- ReviewCard 创建与 Legacy `review.service.ts:82-94` 字段一致 +- Artifact 幂等:`upsertArtifact(tx, ...)` + P2002 catch +- 在 `ProjectionExecutor.$transaction` 内执行(`projection-executor.service.ts:91`) +- markSucceeded 与 Projector 同事务 + +**测试**:16 个测试覆盖正常投影、入口幂等、失败回滚、Artifact P2002、Parent 隔离 + +--- + +### 7. 重复消费不重复卡片 ✅(三层防护) + +| 层级 | 机制 | 代码 | +|------|------|------| +| Engine CAS | `lockJob` queued→running | `ai-job-lifecycle.repository.ts:130-167` | +| Projector 入口 | 已有 Artifact → 返回 | `review-card-generation-projector.ts:50-64` | +| Artifact DB | P2002 catch | `review-card-generation-projector.ts:116-130` | + +**测试**:入口幂等 + P2002 + 并发安全(3 个测试) + +--- + +### 8. Parent 成功不依赖 Child 成功 ✅ + +**证据**: + +- `feynman-projector.ts:208`:Child Job 创建在 Parent 事务内,但 Child 执行在事务外(Worker 异步) +- `feynman-projector.ts:217`:`return artifacts` — Parent Projector 在 Child Job 创建后即返回 +- Engine 在 Projector 返回后调用 `markSucceeded(jobId, tx)` — 同一事务提交 +- Child Job 执行失败时:`review_card_generation` 的 `lifecycleStatus = failed`,但不影响 Parent + +**设计约束**:契约 §3.3 — "Child Projector 不得修改 Parent Job 的任何字段" + +--- + +### 9. Child 失败可独立重试 ✅ + +**证据**: + +- `review-card-generation-job-definition.ts:43`:`maxRetries: 3` +- BullMQ 自动重试(Definition.execution.maxRetries) +- `AiJobService.retryJob()` (`ai-job.service.ts:142-193`) — Admin 手动重试 +- `idempotencyKey = review-card:feynman:` — 重试创建幂等 + +--- + +### 10. Legacy Event 与 Child Job 不双执行 ✅ + +**证据**:`feynman-projector.ts:235-262` + +```ts +if (mode === 'child_job') { + await this.createReviewCardChildJob(tx, job, validatedOutput, snapshot, artifacts); +} else { + // legacy_event: 不创建 Child Job + this.logger.log(`legacy_event mode: ...`); +} +``` + +- `child_job` 模式:不发布 `ai.analysis.completed`(无 EventBus 调用) +- `legacy_event` 模式:不创建 Child Job(无 `createInTransaction` 调用) +- FeatureFlag 控制互斥 + +--- + +### 11. Feature Flag 可快速回滚 ✅ + +**证据**:`feynman-projector.ts:237-248` + +```ts +const enabled = await this.featureFlag.isEnabled(FLAG_REVIEW_CARD_MODE, job.userId); +mode = enabled ? 'child_job' : 'legacy_event'; +// Flag 查询失败 → 安全回退到 legacy_event +``` + +- Flag 名:`FEYNMAN_REVIEW_CARD_MODE` +- 默认值:`legacy_event`(flag disabled 或不存在) +- 白名单支持(`FeatureFlagService.isEnabled(name, userId)`) +- 已在 E2E 设置中验证:切换 flag → 新请求走旧路径,已有 Child Job 继续运行 + +--- + +### 12. Parent/Child 状态可查询 ✅ + +**证据**: + +- `feynman-projector.ts:253-259`:创建 `ReviewCardChildJob` Artifact(`artifactType: 'ReviewCardChildJob'`, `artifactId: childJob.id`) +- `schema.prisma:618-619`:`parentJob` + `children` 自引用关系 +- `schema.prisma:627`:`@@index([parentJobId])` — 支持通过 parentJobId 查询所有子 Job +- Admin 查询:`ai-job.service.ts:245` — 返回 `parentJobId` +- `ProjectionExecutor` 日志:`projection complete: job=... projector=... artifacts=N` + +--- + +### 13. 真实 Feynman → Child Job → ReviewCard 链路通过 ✅ + +**E2E**:`test/m-ai-06-review-card-child.e2e-spec.ts`(361 行,15 场景中 6 个 API+DB 可验证场景) + +**单元等效覆盖**:9 个 Worker 场景由 Projector/Executor/Validator/CreationService 单元测试覆盖 + +**总测试数**:482 passed, 0 failed + +--- + +### 14. M-AI-01 至 M-AI-05 回归通过 ✅ + +``` +Test Suites: 22 passed, 22 total +Tests: 18 skipped, 482 passed, 500 total +``` + +- M-AI-04 Active Recall 测试:全部通过 +- M-AI-05 Feynman 测试:全部通过 +- Engine 测试:全部通过 +- CreationService 测试:全部通过 + +--- + +### 15. M-AI-06-GATE 为 PASS ✅ + +全部 14 条完成标准验证通过。 + +--- + +## 二、问题分级复查 + +### P0 检查(全部通过) + +| 检查项 | 状态 | +|--------|------| +| 跨用户卡片 | ✅ 无 — Child Snapshot 继承 userId,Projector 写入时继承,不跨用户 | +| 敏感信息泄漏 | ✅ 无 — Snapshot Builder 禁止 JWT/API Key/Credential,测试验证 | +| 重复 ReviewCard | ✅ 三层防护 — Engine CAS + Artifact 入口 + P2002 catch | +| Parent 数据丢失 | ✅ 原子事务 — Child 创建与 Parent Projector 同 tx | +| 同一 Parent 双生成 | ✅ idempotencyKey + @@unique 约束 | +| 无法回滚 | ✅ FeatureFlag `legacy_event` 即时生效 | + +### P1 检查(全部通过) + +| 检查项 | 状态 | +|--------|------| +| Parent 成功但 Child Job 未创建 | ✅ 同事务 — 失败则整体回滚 | +| 事务外丢失窗口 | ✅ `createInTransaction` 使用传入 tx,无独立事务 | +| Child Worker 无法执行 | ✅ Engine 已注册 `review_card_generation` 分支 | +| Child Projector 非原子 | ✅ `ProjectionExecutor.$transaction` 包裹 | +| 幂等失效 | ✅ check-before-create + P2002 | +| child_job 与 Legacy Event 同时执行 | ✅ FeatureFlag 互斥 | +| Child 失败修改 Parent | ✅ 代码无 Parent 修改路径 | +| 核心 E2E 假通过 | ✅ Fail-Closed — infra 不可用时 hard fail | + +### P2(不阻塞) + +- ReviewCard 表无 `@@unique([childJobId, ordinal])` 约束 → 依赖入口幂等 + Engine CAS(当前足够,可后续加固) +- Admin 父子展示仅通过 Artifact 关联 → 可通过 `children` 自引用增强 + +### P3(不阻塞) + +- 压力测试 +- Worker SIGKILL 全场景 +- 删除 Legacy Subscriber + +--- + +## 三、文件清单 + +| 文件 | Issue | 类型 | 行数 | +|------|-------|------|------| +| `docs/architecture/m-ai-06-review-card-child-job-contract.md` | 01 | 契约 | 676 | +| `src/modules/ai-job/ai-job-creation.service.ts` | 02 | 修改 | +95/-62 | +| `src/modules/ai-job/ai-job-creation.service.spec.ts` | 02 | 修改 | +215 | +| `src/modules/ai-job/review-card-generation-job-definition.ts` | 03 | 新增 | 79 | +| `src/modules/ai-job/review-card-generation-snapshot-builder.ts` | 03 | 新增 | 189 | +| `src/modules/ai-job/review-card-generation-registration.service.ts` | 03 | 新增 | 42 | +| `src/modules/ai-job/review-card-generation-registration.service.spec.ts` | 03 | 新增 | 261 | +| `src/modules/ai-job/review-card-generation-executor.ts` | 04 | 新增 | 103 | +| `src/modules/ai-job/review-card-generation-validator.ts` | 04 | 新增 | 170 | +| `src/modules/ai-job/review-card-generation-executor.spec.ts` | 04 | 新增 | 350 | +| `src/modules/ai-job/review-card-generation-projector.ts` | 05 | 新增 | 133 | +| `src/modules/ai-job/review-card-generation-projector.spec.ts` | 05 | 新增 | 285 | +| `src/modules/ai-job/ai-job-execution-engine.ts` | 05 | 修改 | +29 | +| `src/modules/ai-job/ai-job.module.ts` | 03-05 | 修改 | +14 | +| `src/modules/ai-job/feynman-projector.ts` | 06 | 修改 | +132/-18 | +| `src/modules/ai-job/feynman-projector.spec.ts` | 06 | 修改 | +165 | +| `test/m-ai-06-review-card-child.e2e-spec.ts` | 07 | 新增 | 361 | +| `.gitea/workflows/deploy.yml` | CI | 修改 | +1/-1 | +| **合计** | | **10 新增 + 8 修改** | **+2984** | + +--- + +## 四、最终结论 + +### M-AI-06-GATE:PASS + +| 决定 | 结论 | +|------|------| +| 允许进入下一里程碑(M-AI-07 AI Quiz 迁移) | **是** | +| 允许生产白名单 `child_job` | **是** | +| 允许删除 Legacy Subscriber | **否**(待生产验证后) | + +### 依据 + +全部 14 条完成标准通过验证,482 个测试 0 失败,CI 集成测试通道已配置,Feature Flag 支持即时回滚。P0/P1 无问题。P2 项(ReviewCard 唯一约束加固)不阻塞上线,可在后续里程碑中增强。