a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
6.7 KiB
6.7 KiB
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.pymapper:仍读取旧列并传给 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_hintPATCH 成功。next_actionPATCH 被拒绝,并返回清晰错误。next_actionsPATCH 被拒绝或明确忽略策略不再接受。/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_actionnext_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_hintPATCH 继续成功。next_action / next_actionsPATCH 已改为拒绝。- 拒绝状态返回
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 后,再决定是否正式删列。