Files
ai-contract-platform/project-docs/V2-phase3-next-action-cleanup-migration-review-2026-07-02.md
T
yuqei a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
release: freeze V2 release candidate
2026-07-12 15:32:24 +08:00

6.7 KiB
Raw Blame History

AI 合同平台 V2 Phase 3 next_action cleanup migration 设计评审

评审背景

action_hint 已成为人工跟进提示的唯一推荐写入字段。

旧字段当前状态:

  • contracts.next_action:数据库旧展示缓存列,生产写路径已停写,读响应由 ContractActionContext 派生。
  • contracts.next_actions:数据库旧展示缓存列,生产写路径已停写,读响应由 ContractActionContext 派生。
  • ContractUpdateRequest.next_action:旧 PATCH 写入 alias,已标记 deprecated。
  • ContractUpdateRequest.next_actions:旧 PATCH 输入字段,已标记 deprecated,写路径忽略。

30 分钟 smoke 观察窗口已通过:

  • baseline_total=1。
  • final_total=1。
  • observed_delta=0。
  • relevant_new_count=0。
  • DEPRECATED_ALIAS_OBSERVATION_PASS。

这说明观察窗口内没有新增旧 alias 调用,可以进入 cleanup migration 设计评审。

当前依赖面

仍依赖旧列存在的模块:

  • SQLAlchemy model:ContractModel.next_action / next_actions。
  • 搜索索引读路径:用旧列作为 derive_contract_display(...) 的兼容输入。
  • workflow_helpers.py mapper:仍读取旧列并传给 display derivation。
  • display_cache_governance.py:仍把旧动作列作为 retained compatibility fields。
  • schema_check.py:display_cache_cleanup_prestate 仍要求旧动作列存在,作为删列前状态冻结。
  • seed/demo/in-memory repository:仍包含旧展示字段,用于兼容测试和 demo 响应。

对外兼容面:

  • ContractListItem.next_action 响应字段继续保留。
  • ContractDetail.next_actions 响应字段继续保留。
  • 前端仍读取响应 next_action / next_actions 展示下一步动作。

不建议立即做的事

本轮不建议直接执行数据库删列:

ALTER TABLE contracts DROP COLUMN next_action;
ALTER TABLE contracts DROP COLUMN next_actions;

原因:

  • 后端模型、mapper、搜索、schema-check 仍有旧列读取点。
  • 当前 schema-check 的 cleanup prestate 明确要求旧列存在;直接删列会把 prestate check 变成 error。
  • 对外响应字段仍叫 next_action / next_actions,需要先保证所有响应都完全不依赖旧列。
  • 若直接删列,会把“停止旧输入”和“删除旧缓存列”两个风险混在一片迁移里,回滚面过大。

建议拆分顺序

Slice A:移除旧写入 alias

目标:

  • 不再接受 ContractUpdateRequest.next_action 作为写入 alias。
  • 不再接受 ContractUpdateRequest.next_actions 输入。
  • API 响应字段继续保留,由派生层输出。
  • 数据库旧列继续保留。

建议实现:

  • 将 ContractUpdateRequest.next_action / next_actions 从“accepted deprecated input”改为“rejected deprecated input”。
  • 若请求体出现旧字段,返回 422 或 400,错误文案指向 action_hint。
  • 保留 deprecated_alias_observability,但把 compatibility_status 从 accepted 扩展为 rejected。
  • metrics 继续记录旧客户端尝试,方便发现未升级调用方。
  • OpenAPI 中移除或标记为 removed/deprecated rejected,根据前端 SDK 生成策略决定。

验收:

  • action_hint PATCH 成功。
  • next_action PATCH 被拒绝,并返回清晰错误。
  • next_actions PATCH 被拒绝或明确忽略策略不再接受。
  • /api/v1/metrics/deprecated-aliases 能看到 rejected 记录。
  • 前端 typecheck 通过。
  • 观察窗口内无真实旧调用。

Slice B:停止旧列读取

目标:

  • 所有 response/search/display 都不再读取 contracts.next_action / next_actions。
  • derive_contract_display(...) 只依赖状态、审批、审查、履约、条款修复、action_hint 等结构化上下文。

建议实现:

  • contract_detail_from_model / contract_list_item_from_model 不再把旧列传入 display derivation。
  • search document 生成不再读取旧列。
  • display cache governance 不再比较或展示旧动作列。
  • schema-check 增加新的 “legacy_action_columns_unused” 只读检查。

验收:

  • 创建、列表、详情、审批、审查、条款修复、统一搜索全部通过。
  • schema-check 显示旧动作列不再参与读路径。
  • 旧列即使有历史值,也不会影响响应和搜索。

Slice C:cleanup migration 删列

目标:

  • 从 contracts 表移除旧列:
    • next_action
    • next_actions

前置条件:

  • Slice A/B 已完成并通过 smoke。
  • 至少一个观察窗口内旧 alias 使用为 0。
  • schema-check 已从 “legacy columns retained” 切到 “legacy columns removable”。
  • 已有回滚方案:重新 add column + 默认空值,不恢复旧缓存语义。

验收:

  • Alembic head 正常。
  • /api/v1/runtime/schema-check 正常。
  • 合同列表/详情仍返回 next_action / next_actions 响应字段。
  • 数据库表结构不再包含 contracts.next_action / contracts.next_actions。

Slice A 执行结果

Slice A 已完成:

  • action_hint PATCH 继续成功。
  • next_action / next_actions PATCH 已改为拒绝。
  • 拒绝状态返回 422。
  • 观测记录保留,compatibility_status=rejected。
  • API 响应字段 next_action / next_actions 继续保留,由派生层输出。
  • 数据库结构未改动,contracts.next_action / contracts.next_actions 继续保留。
  • 远端 smoke schema-check 通过。

Slice B 执行结果

Slice B 已完成并通过 smoke:

  • 合同列表/详情 mapper 不再读取 contracts.next_action / next_actions。
  • 统一搜索索引生成不再读取 contracts.next_action / next_actions。
  • display cache governance 不再读取旧动作列值,只继续检查旧列存在性。
  • schema-check 文案已更新为旧动作列 retained but unused by read paths。
  • 新增回归证明旧列脏值不会出现在详情响应和搜索文本中。
  • 远端 smoke schema-check 通过,unused_compat_fields=next_action,next_actions。
  • 远端手工污染旧列后,合同详情和搜索文档均未出现旧 marker。
  • 远端 3 秒观察窗口通过,relevant_new_count=0。

本轮建议结论

Slice A 已完成,Slice B 已完成并通过 smoke。

暂不进入 Slice C:数据库删列。

理由:

  • 30 分钟观察窗口已证明当前 smoke 没有新增旧 alias 调用。
  • 旧输入已拒绝。
  • 读路径已停止读取旧列。
  • schema-check 仍要求旧列存在,作为删列前冻结状态。
  • 删列需要单独迁移、回滚方案和远端 smoke。

下一步建议

下一步最稳的是进入 Slice C 设计:

  • 设计 Alembic 删列迁移和回滚。
  • 将 schema-check 从 “legacy columns retained” 过渡到 “legacy columns removable / removed”。
  • 跑完整 Docker smoke 后,再决定是否正式删列。