# ADR-0012: 活动智能作业结算统一——execute 深接缝与 DB 权威

**状态**: 已接受
**日期**: 2026-08-24
**决策者**: 架构团队
**相关**: ADR-0004（跨 run 聚合口径）、ADR-0006（拒绝通用条件写模块）、ADR-0011（终态纪律）、CONTEXT.md（智能分析、周期对比）

## Context

智能分析与周期对比两个 executor 各自手写了一遍完整的作业结算序列：幂等建行 → `claim_queued` 认领 → `upsert(generating)` → 领域工作 → 异常截断落 failed → completed 落账 → session 关闭。两遍编排约 200 行，逐句相似又逐处微妙不同（失败行带不带 baseline、截断长度、校验时机），任何一处结算语义的修改都要在两处同步，漂移风险随每次改动累积。

同时，自动触发的跳过守卫（正式线 + 分析模型可解析）在 `campaign_runner._auto_start_analysis` 与 executor 的自动对比链各写了一遍；分析触发的校验（活动终态 + 模型配置）在路由与分析 executor 各写了一遍——而周期对比已有 `validate_comparison_request` 共享入口的先例。

## Decision

1. **DB 行是作业权威，`TaskRegistry` 只保存进程句柄。** 排队/执行/成败一切状态以 `CampaignAnalysisDB` / `CampaignPeriodComparisonDB` 行为准；registry 只负责当前进程内的任务句柄、幂等启动与关停。重启后凭 DB 行恢复（`recover_campaign_intelligence_jobs`），不凭 registry。

2. **结算顺序：先认领再 generating。** `claim_queued` 的条件更新是耐久幂等权威——竞争者可以观察到同一 queued 行，但只有一个能把它推到 generating；抢不到的一方静默退出，绝不重复跑领域工作。`execute` 内部的固定次序：建行（ensure_queued）→ 认领 → 校验 → generating → work → 结算。

3. **`execute(job_kind, campaign_id, …)` 是结算的唯一实现。** 幂等建行、认领、异常归一（error 截断 500 字符一处）、failed/completed 落账全部收进 `intelligence_jobs.execute`；分析与对比退化为三个小 adapter：`ensure_queued`（建行/静默放弃）、`validate`（认领后校验，失败抛携带落账字段的 `JobValidationError`）、`work_fn`（纯领域工作，返回结果 + 附加落账字段）。删除任一侧 adapter 的编排序列与截断复制。

4. **校验共享入口对齐对比先例。** 新增 `validate_analysis_request`（活动终态 → 模型），路由捕获映射 400、executor 捕获落 failed 行，两处措辞与顺序不再漂移；与既有 `validate_comparison_request` 同构。自动触发跳过守卫收敛为 `auto_intelligence_eligible` 单一判断点（活动完成自动分析 + 分析完成自动对比共用）。

5. **截断与恢复上限语义。** work 异常落账统一截断 500 字符；重启恢复 queued 行上限 3 次（`MAX_QUEUED_RECOVERY_ATTEMPTS`），超限置 failed 并注明原因——与 ADR-0011 终态纪律一致：自愈有上限，超限收敛到终态且可见。

6. **投影归位。** `load_comparison_view`（GET /comparison 响应形状）移回 `campaign_read_model`；`comparison.py` 只留领域工作（指纹、基线解析、机械 diff、校验、叙述）。零调用的 `build_comparison_payload` 删除。

### app.py 关停顺序现状（暂不动）

关停依序：`scheduler_runtime` → `campaign_runtime` → `run_registry` → 智能作业 → `judge_registry`。顺序有意（上游先停，避免停掉的调度再派生新任务），现状有 lifespan 测试覆盖智能评估侧；在出现顺序相关的真实故障前不归一为通用机制（与 ADR-0006 拒绝通用化的立场一致）。

### Considered Options（已拒绝）

- **把结算序列抽成通用 async-job 框架**：两个 adapter 的校验时机、落账字段、静默放弃语义各有领域差异，通用框架的接口会比两个小 adapter 更复杂（浅模块）。等出现第三、第四种耐久作业再议。
- **CAS 共享原语收编认领逻辑**：本轮决策不收敛（三处条件写语义有差异），见 v3 重构计划「刻意不做」。

## Consequences

- 结算语义修改只需动 `execute` 一处；characterization 测试（认领竞争、重复触发、截断、恢复上限）锁定契约。
- executor 的校验面变宽：非终态活动直接调 executor 会落 failed 行（此前会继续跑）——与路由校验对齐后的刻意收敛。
- 失败行的模型缺失措辞统一为路由侧完整文案（含「或为该活动指定分析模型」引导）。
