a3bb78e06b
Security Quality Gate / npm audit, Bandit, pip-audit, detect-secrets (pull_request) Failing after 16m35s
173 lines
6.7 KiB
Markdown
173 lines
6.7 KiB
Markdown
# 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 后,再决定是否正式删列。
|