api-server/docs/architecture/m-ai-03-gate-audit.md
wangdl 49151117c8
Some checks failed
Deploy API Server / build-and-unit (push) Successful in 32s
Deploy API Server / current-integration (push) Failing after 1m34s
Deploy API Server / backward-compat (push) Successful in 16s
Deploy API Server / m-ai-03-synthetic-e2e (push) Failing after 12s
Deploy API Server / deploy (push) Has been skipped
fix: M-AI-03-GATE-FIX — Admin Retry 统一链路 + 队列测试修正 + E2E 激活 + CI 门禁
### 一、生命周期契约核对
- ADR-003 §1.1: 5 状态(queued/running/succeeded/failed/cancelled)
- cancel_requested = cancelRequestedAt 信号(非状态)
- created/retrying 不在状态机中
- 代码完全一致 

### 二、Admin Retry 修复
- retryJob 不再绕过 Registry/CreationService/Outbox
- 改为调用 AiJobCreationService.createJob()(统一事务链路)
- parentJobId + triggerType=admin_manual 正确传入
- 幂等键: admin-retry:<jobId>:<timestamp>(允许不同重试产生不同 Job)
- 原 Job 不修改,新 Job 使用新 ID
- 新增 AiJobCreationService.retrySnapshotContent 支持复用 Snapshot

### 三、队列定义测试修正
- 旧队列断言保持 6 个 legacy queues 的默认值检查
- 新增 M-AI-03 per-queue 断言(ai-interactive: concurrency=2/attempts=2/backoff=2000; ai-background: concurrency=1/attempts=3/backoff=5000/lockDuration=60s)
- lockDuration 测试改为 >= 30s(允许 per-queue 差异)

### 四、Synthetic E2E 激活
- test/m-ai-03-synthetic.e2e-spec.ts: 12 真实场景(创建/幂等/原子/cancel/JWT/隔离/脱敏/未知type/状态机)
- test/jest-m-ai-03.json: 真实 infra config(无 mock Prisma/BullMQ/Redis)
- CI Workflow 新增 m-ai-03-synthetic-e2e job
- deploy needs 新增 m-ai-03-synthetic-e2e 依赖

### 五、测试结果
- 单元测试: 471/471 passed, 0 failed
- E2E: 12 场景待 CI 真实 infra 执行
- queue-definitions: 19/19 passed

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-21 10:47:09 +08:00

256 lines
10 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.

# M-AI-03 GATE 独立验收审核报告
> 审核日期2026-06-21
> 审核人:独立审核代理(非 M-AI-03 开发执行者)
> 基线:`98e442e` (M-AI-02 GATE)
> HEAD`5108a9a`
---
## 1. 审核结论
**CONDITIONAL PASS**
- 无 P0 问题
- 1 个 P1 问题Admin 重试绕过 Registry + CreationService + 非原子)
- 2 个 P2 问题queue-definitions.spec.ts 测试断言过时、18/19 Synthetic E2E pending infra
- ConditionP1 Admin 重试修正 + E2E infra 就绪后,升级为 PASS
---
## 2. Commit 范围
| 项目 | 值 |
|------|-----|
| 基线 | `98e442e` fix(M-AI-02-GATE): restore physical column names |
| HEAD | `5108a9a` fix: queue-definitions.spec.ts 显式导入 |
| 文件 | 50 changed, +6739/-101 lines |
| Migration | **零 M-AI-03 Migration**(最新: `20260620000008`M-AI-02 |
---
## 3. 运行环境
| 组件 | 状态 |
|------|:---:|
| Node | ✅ v22 |
| Prisma validate | ✅ schema valid |
| Prisma generate | ✅ v5.22.0 |
| npm run build | ✅ nest build |
| MySQL (真实) | ⚠️ 未在本机运行CI 环境需要) |
| Redis (真实) | ⚠️ 未在本机运行 |
| BullMQ (真实) | ⚠️ 未在本机运行 |
| API Process | ✅ app.module.ts 编译通过 |
| Worker Process | ✅ worker.module.ts 编译通过 |
| CI | ⚠️ 未触发最新 CI run |
---
## 4. 验收矩阵
| 验收项 | 结论 | 证据 | 风险 |
|--------|:---:|------|------|
| 无 Prisma Migration | ✅ | `prisma/migrations/` 最新为 M-AI-02 | 无 |
| 无真实业务切流 | ✅ | AiAnalysisWorker 未修改 | 无 |
| 无通用公开创建接口 | ✅ | `ai-job.controller.ts:13,19` 仅 GET + POST cancel | 无 |
| 无巨型 switch | ✅ | `JobDefinitionRegistry` Map-based | 无 |
| API 无 Processor | ✅ | `app.module.ts`@Processor | 无 |
| Worker 无 Controller | ✅ | `worker.module.ts`@Controller | 无 |
| Job+Snapshot+Outbox 原子 | ✅ | `ai-job-creation.service.ts:101-150` $transaction | 无 |
| 幂等键生效 | ✅ | P2002 catch | 无 |
| Outbox payload 最小化 | ✅ | `{jobId: job.id}` | 无 |
| CAS 并发安全 | ✅ | `lockJob` + `markProcessing` 均用 updateMany CAS | 无 |
| 崩溃窗口恢复 | ✅ | BullMQ jobId 幂等 + releaseExpiredLocks | 无 |
| 状态机完整 | ✅ | 5 状态 + 双向 Shadow Write | 无 |
| attemptCount 原子递增 | ✅ | `attemptCount: { increment: 1 }` in lockJob | 无 |
| 错误分类 10 类 | ✅ | `classifyError()` 7 retryable/permanent | 无 |
| 取消 3 检查点 | ✅ | 执行前/Provider前/Projector前 | 无 |
| Projector 原子 | ✅ | `$transaction(tx => projector(tx) + markSucceeded(tx))` | 无 |
| public/Admin 响应隔离 | ✅ | `toPublicResponse` 排除 validatedOutput/internalErrorMessage | 无 |
| 用户权限隔离 | ✅ | `job.userId !== userId → 403` | 无 |
| Synthetic 生产不注册 | ✅ | `isSyntheticEnabled()` + `assertNotProduction()` | 无 |
| Admin 重试走 Registry+Outbox | ❌ | `ai-job.service.ts:144-148` 注释自认绕过 | P1 |
| E2E 19/19 通过 | ⚠️ | 18 pending infra | P2 |
| 全量测试 | ⚠️ | 468/488 passed2 预存失败 + 18 E2E skip | P2 |
---
## 5. 架构边界审核
| 边界 | 预期 | 实际 | 判定 |
|------|------|------|:---:|
| API Processor | 0 | 0 | ✅ |
| Worker Controller | 0 | 0 | ✅ |
| 公开创建 Job 路由 | 无 POST / | 仅 GET :jobId + POST :jobId/cancel | ✅ |
| Provider SDK 直接调用 | 0 | 0通过 AiGatewayService | ✅ |
| EventBus/domain-events 改动 | 0 | 0 | ✅ |
| 旧 ai-analysis 队列 | 保留 | 6 旧队列未修改 | ✅ |
| 旧 Worker 删除 | 0 | AiAnalysisWorker 等 5 个保留 | ✅ |
| lifecycleStatus 直接写入 | 仅 LifecycleRepo + 旧路径 | 旧 AiAnalysisRepository 保留,但 Admin retry 绕过 | ⚠️ |
---
## 6. 状态机审核
- 状态定义:`ai-job-state-machine.ts:16-22` — queued/running/succeeded/failed/cancelled ✅
- cancel_requested 为信号而非状态:`ai-job-state-machine.ts:62-63`
- 合法迁移:`queued→running/cancelled`, `running→succeeded/failed/cancelled`
- 非法迁移12 种,终态无出边 ✅
- CAS 更新:`WHERE lifecycleStatus IN (...)`
- Shadow Write双向 `LIFECYCLE_TO_STATUS` + `STATUS_TO_LIFECYCLE`,含 cancelled→failed ✅
- 直接 Prisma 写入:`ai-job.service.ts:167` Admin retry 绕过 LifecycleRepo ⚠️
---
## 7. Registry 审核
- `JobDefinitionRegistry`Map-based`register()` 含 10 类校验 ✅
- 唯一 jobType`DuplicateJobTypeError`
- 未知 jobType`UnknownJobTypeError`
- 非法 queueName`InvalidQueueNameError`
- Schema 非空:`MissingSchemaError`
- timeout/attempts 范围:`InvalidTimeoutError`/`InvalidRetriesError`
- Object.freeze禁止运行时覆盖 ✅
- 无巨型 switch全仓搜索确认 ✅
- 当前注册清单:`synthetic_job``ai-interactive`(仅测试环境)✅
---
## 8. Job 创建事务审核
- 三项在同一 `prisma.$transaction``ai-job-creation.service.ts:101-150`
- 幂等P2002 catch + `findUniqueOrThrow`
- Outbox payload 仅 `{ jobId }``ai-job-creation.service.ts:145`
- 元数据从 Definition 写入14 字段透传 ✅
**无法确认(需真实 MySQL**
- 真实事务回滚Job 创建失败 → Snapshot/Outbox 不存在)
- 真实 P2002 并发场景
---
## 9. Outbox Dispatcher 审核
- CAS 先于 queue.add`outbox-dispatcher.service.ts`
- 仅运行于 Worker`worker.module.ts:85``app.module.ts` 无 ✅
- 崩溃窗口releaseExpiredLocks(60s) + BullMQ jobId 幂等 ✅
- 未知 eventType标记永久失败 ✅
**无法确认(需真实 MySQL+Redis+BullMQ**
- 多实例并发领取不重复
- 崩溃恢复真实验证
- 真实入队后崩溃→重启→不重复
---
## 10. Execution Engine 审核
- 5 阶段管线PREPARE→RESOLVE→EXECUTE→PROJECT→COMPLETE ✅
- 错误分类7 类provider_rate_limited/timeout/unavailable/schema_validation/business_validation/reference_validation/cancelled/internal_error
- 永久错误直接 markFailed`engine.ts:232-239`
- retryable → unlockForRetry + throw`engine.ts:227-228`
- attemptCount 原子递增:`lifecycle-repo.ts:147` `{ increment: 1 }`
- 取消 3 检查点:执行前/Provider前/Projector前 ✅
- 通过 AiGatewayService 调用(非直接 Provider SDK
---
## 11. Projector 与 Artifact 审核
- 事务内 `projector.project(tx)` + `markSucceeded(tx)``projection-executor.service.ts:90-109`
- 幂等:`SyntheticResultProjector` findMany 前置检查 ✅
- Artifact UNIQUE`@@unique([jobId, artifactType, artifactId])`
- 无 Projector 路径调用 markSucceeded`projection-executor.service.ts:57,66`
---
## 12. 取消与重试审核
- 用户取消 → `lifecycleRepo.requestCancellation()`
- queued → `markCancelled`(直接)✅
- running → `cancelRequestedAt` 信号 ✅
- 终态幂等 → `isTerminal` check ✅
- Admin 重试 → **绕过 Registry + CreationService + 非原子**P1
---
## 13. API、权限与安全审核
- JWT 隔离:`job.userId !== userId → 403`
- 公开响应不含 validatedOutput/internalErrorMessage/snapshot/credential`toPublicResponse:237`
- Admin 响应包含 internalErrorMessage/validatedOutput`toAdminResponse:266-267`
- 无明文密钥/Token/密码在代码中:全仓扫描确认 ✅
---
## 14. Synthetic E2E 审核
- 环境门控:`NODE_ENV=test && AI_JOB_SYNTHETIC_ENABLED=true`
- 生产拒绝:`assertNotProduction()`
- API 层测试JWT 保护验证401 without token
- 完整链路 E2E**18/19 pending infra** ⚠️
---
## 15. M-AI-01/M-AI-02 回归
- 全量单元测试468/488 passed
- 2 个预存失败:`queue-definitions.spec.ts`(断言假定统一配置,但 ADR-003 引入 per-queue 差异)
- M-AI-01 Worker Integration代码路径未修改AiAnalysisWorker 保留)
- M-AI-02 backward compatibilityMigration 文件未修改schema 仅加注释
---
## 16. CI 与部署门禁
- deploy.yml包含 build-and-unit + current-integration + backward-compat + deploy
- M-AI-03 Synthetic E2E测试文件存在`test/m-ai-03-synthetic.e2e-spec.ts`),但 Workflow 未配置专门的 M-AI-03 E2E Job
- CI 阻断 deploy`deploy` job 依赖 `current-integration` + `backward-compat`,但未依赖 M-AI-03 Synthetic E2E
**无法确认**:最新 CI run 结果(本地无 CI runner
---
## 17. 问题列表
### P1不得进入 M-AI-04
| ID | 文件 | 行 | 描述 |
|----|------|----|------|
| P1-01 | `ai-job.service.ts` | 144-148 | Admin retry 绕过 Registry不校验 jobType、绕过 CreationService非原子 Job+Snapshot+Outbox、代码自注"简化实现" |
### P2可修复后复审
| ID | 文件 | 描述 |
|----|------|------|
| P2-01 | `queue-definitions.spec.ts:62,195` | 2 tests 断言 concurrency=1/lockDuration=30s统一配置与 ADR-003 per-queue 差异冲突 |
| P2-02 | `test/m-ai-03-synthetic.e2e-spec.ts` | 18/19 Synthetic E2E 场景 pending需真实 MySQL/Redis/BullMQ/Mock AI Server |
### P3非阻断债务
| ID | 描述 |
|----|------|
| P3-01 | `queue-definitions.spec.ts` 显式导入 `QUEUE_AI_INTERACTIVE`/`QUEUE_AI_BACKGROUND` 但未更新断言commit `5108a9a` 修复了 import断言待更新 |
---
## 18. 无法确认项
| 项目 | 缺失环境 | 是否阻断 PASS |
|------|---------|:---:|
| 真实 MySQL 事务测试 | 本地 MySQL | 否(已有单元测试覆盖逻辑) |
| 真实 Redis/BullMQ E2E | 本地 Redis + BullMQ | 否(已有单元测试覆盖逻辑) |
| Outbox 多实例并发 | 多进程环境 | 否CAS 逻辑已验证) |
| Dispatcher 崩溃恢复 | 进程管理 | 否releaseExpiredLocks 逻辑已验证) |
| CI Run 最新结果 | CI Runner | 否Workflow YAML 已审查) |
| Admin retry 是否已绕过缺陷引入生产数据 | 需真实调用 | **是**P1-01 必须在进入 M-AI-04 前修正) |
---
## 19. 下一步建议
1. **修正 P1-01**Admin retry 接入 Registry + CreationService创建 Fix Issue
2. **修正 P2-01**queue-definitions.spec.ts 更新断言),可随 P1-01 一同提交
3. **CI 环境就绪后**:激活 `test/m-ai-03-synthetic.e2e-spec.ts` 中的 18 个 pending 场景,执行全部 19 场景
4. **Condition 解除后**:升级为 PASS允许进入 M-AI-04Active Recall 端到端迁移)