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

173 lines
6.7 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.
# 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` 展示下一步动作。
## 不建议立即做的事
本轮不建议直接执行数据库删列:
```sql
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 后,再决定是否正式删列。