# AI 合同平台 V2 Phase 3 UI 缓存字段 cleanup 设计评审 ## 当前基线 本设计继续对齐《AI 合同平台 V2 下一阶段开发优化方案》的 Phase 3 schema 硬化方向,目标是把 `contracts` 表中的旧 UI 缓存字段从“历史兼容缓存”逐步治理到“可安全停写、可选删列”的状态。 截至本记录: - 远端 smoke PostgreSQL:`192.168.3.90` - Contract API:`http://192.168.3.90:18100` - Alembic actual/head:`20260630_0021` - `display_cache_consistency.status`:`ok` - `checked_rows`:12010 - `mismatch_count`:0 - 回滚 snapshot:`/opt/apps/ai-contract-platform-v2-smoke/backups/display-cache/display-cache-backfill-candidate-2026-06-30T17-37-24.json` - 本地备份:`project-docs/qa-artifacts/display-cache-governance/2026-06-30T17-37-24/display-cache-backfill-candidate-2026-06-30T17-37-24.json` 这说明当前 smoke 环境已经处于 cleanup migration 前的稳定状态:旧列仍保留,但值已与派生规则一致。 ## 字段分层判断 目标字段: - `contracts.status_label` - `contracts.status_tone` - `contracts.next_action` - `contracts.next_actions` 分层结论: 1. `status_label/status_tone` 是纯展示字段,可以完全由 `contracts.status` 派生。 2. `next_action/next_actions` 不是纯展示字段,当前混合了三类含义: - 状态默认动作,例如待提交、待审批、已审批。 - 结构化上下文动作,例如审查任务、审批实例、条款修复、履约任务。 - 少量人工/业务上下文提示,例如合同编辑页仍允许编辑 `next_action`。 因此不建议直接创建一次性 migration 删除四列。更稳的路径是先把 `status_label/status_tone` 和 `next_action/next_actions` 拆开治理。 ## 当前仍在写旧列的位置 ### 合同主写路径 - `persistence/contract_core.py` - 创建合同时仍从 `ContractCreateData` 写入 `status_label/status_tone/next_action/next_actions`。 - 更新合同时仍允许更新 `next_action/next_actions`。 ### 状态流/审计/时间线 - `persistence/audit.py` - `transition_contract` 与 `transition_contract_in_session` 仍将状态流参数写回四个旧列。 - `sqlalchemy_transition_orchestration.py` - 状态流仍传递四个展示字段。 - `sqlalchemy_approval_orchestration.py` - 审批提交、重提、通过、退回仍提供展示字段。 ### 审查/文件/条款修复 - `sqlalchemy_review_orchestration.py` - 审查开始、完成、失败后仍写 `status_label/status_tone/next_action/next_actions`。 - `sqlalchemy_file_orchestration.py` - 文件上传后仍写 `next_action/next_actions`。 - `sqlalchemy_clause_repair_orchestration.py` - 条款修复后仍写 `next_action/next_actions`。 ### 支撑路径 - `sqlalchemy_support_orchestration.py` - 归档动作仍写展示字段。 - `seed_data.py` - demo/seed 数据仍包含旧字段。 - `in_memory_repository.py` - 测试/demo 内存仓储仍维护旧字段。 ### 不属于本轮删除目标的字段 - `review_tasks.status_label`:这是审查任务自己的状态标签,不是 `contracts` 表 UI 缓存列。 - `contract_search_documents.status_label`:这是搜索索引/搜索展示冗余字段,后续可以单独治理,但不应与 `contracts` 表删列混在同一刀。 - OpenAPI 和前端 TypeScript 响应字段:仍应保留兼容,因为前端合同列表、详情、搜索、dashboard 仍依赖这些响应字段。 ## 当前读路径状态 读路径已经基本完成派生化: - 合同列表 mapper 使用 `derive_contract_display`。 - 合同详情 mapper 使用 `derive_contract_display`。 - 搜索文档同步/重建使用 `derive_contract_display`。 - `ContractActionContext` 已纳入审查、审批、条款修复、履约任务等上下文。 - `runtime/schema-check` 已复用同一套 display cache governance 逻辑做只读一致性检查。 这意味着 API 响应可以继续返回 `status_label/status_tone/next_action/next_actions`,但来源应逐步变为 serializer/domain 派生,而不是依赖数据库旧列。 ## 推荐 cleanup 顺序 ### Slice 3.5.1:停写前设计锁定 目标: - 保留数据库旧列。 - 保留 API/OpenAPI/前端字段。 - 明确 `next_action/next_actions` 中人工提示的归属。 建议动作: 1. 审计前端合同编辑页的 `next_action` 输入是否仍是产品需求。 2. 如果仍需要人工提示,新增事实字段或结构: - 方案 A:`contracts.action_hint` 文本字段。 - 方案 B:`contract_action_overrides` JSON/子表。 - 方案 C:完全移除人工编辑入口,只保留结构化派生动作。 3. 不在此片写 migration。 验收: - 形成明确决策:`next_action` 人工提示是保留、迁移还是废弃。 - `schema-check` 仍为 `ok`。 ### Slice 3.5.2:停止写 `status_label/status_tone` 目标: - `contracts.status_label/status_tone` 不再由业务编排手写。 - 仍保留列,用于兼容、回滚和 schema-check。 建议动作: 1. 创建集中 helper,例如 `apply_contract_display_cache(row, action_context=None)` 或 `sync_contract_display_cache(row)`。 2. 对状态流写路径,改为只写事实字段 `status`,展示字段由 helper/派生层统一刷新。 3. 对 create/seed/in-memory 路径,改为由 `status` 派生默认展示字段。 4. 增加测试:制造错误旧值后,读路径仍返回派生值;写路径不会再写手写 status label/tone。 验收: - `schema-check.display_cache_consistency` 保持 `ok`。 - 合同列表、详情、workspace、搜索抽样通过。 - OpenAPI/前端响应字段不变。 ### Slice 3.5.3:拆分 `next_action/next_actions` 语义 目标: - 将 `next_action/next_actions` 从“旧 UI 缓存列”转成真正的 Action Context 派生。 - 如果保留人工提示,迁移到新事实字段或覆盖结构。 建议动作: 1. 将合同编辑页的 `next_action` 输入改名/改语义,避免继续把它当展示缓存。 2. 将文件上传、审查任务、条款修复等写路径改成写结构化事实,而不是写 `next_action` 文案。 3. 扩展 `ContractActionContext`,覆盖合同版本/草稿编辑状态。 4. 搜索文档继续保留派生后的展示文案,但来源来自 domain/serializer。 验收: - 写路径不再直接设置 `contracts.next_action/next_actions`,或仅通过兼容 helper 写入派生结果。 - 旧列一致性检查仍为 `ok`。 - 前端主要动作按钮展示不退化。 ### Slice 3.5.4:cleanup migration 设计评审 只有满足以下条件后才进入删列 migration: - API 响应字段完全由 mapper/serializer 派生。 - 后端业务写路径不再依赖四个旧列。 - 前端不再提交旧 UI 缓存字段作为业务输入。 - 搜索索引重建可以完全从事实字段和派生规则生成展示文案。 - smoke 环境连续多轮 `display_cache_consistency` 为 `ok`。 - 已保留可回滚 snapshot,且 rollback 演练流程可执行。 删列顺序建议: 1. 第一片只删除或弃用 `status_label/status_tone`,因为它们最纯粹。 2. 第二片再处理 `next_action/next_actions`,前提是人工提示语义已经迁出。 3. 每片 migration 后都复跑 Docker smoke、schema-check、合同列表、详情、搜索、workspace 抽样。 ## 不建议的做法 - 不建议一次性删除四个旧列。 - 不建议为了“数据库更干净”破坏 OpenAPI/前端兼容字段。 - 不建议在 `next_action` 人工提示归属未决时删除 `next_action/next_actions`。 - 不建议把 `contract_search_documents.status_label` 和 `contracts.status_label` 放在同一个 migration 里治理。 ## 当前建议 2026-07-01 已完成完整上下文口径的 display cache prestate 重新固化: - 旧列 `status_label/status_tone/next_action/next_actions` 仍保留。 - `display_cache_governance` 已按完整 `ContractActionContext` 判断一致性。 - smoke 环境已执行可回滚 backfill,`display_cache_consistency.mismatch_count=0`。 - `display_cache_cleanup_prestate.status=ok`。 - 详细记录见 `project-docs/V2-phase3-display-cache-context-prestate-2026-07-01.md`。 下一步最稳仍不是直接写删列 migration,而是继续停写旧 `contracts.next_action/next_actions` 的生产写路径: 1. 让文件上传、审查任务、条款修复、审批流、归档等路径只写结构化事实。 2. API 响应继续由 `derive_contract_display(...)` 和完整 `ContractActionContext` 派生 `next_action/next_actions`。 3. 旧列只作为兼容缓存和回滚面保留,继续由 schema-check 保护。 4. 连续多轮 smoke 保持 `display_cache_consistency=ok` 后,再进入 Slice 3.5.4 cleanup migration 设计评审。 这个顺序能避免把“UI 文案缓存治理”和“合同下一步动作业务模型”混成一次高风险 schema 大改,同时保留对旧前端/API 调用方的兼容面。 ## 2026-07-01 停写旧动作缓存列进展 已完成第一片停写: - 生产 SQLAlchemy 写路径不再刷新 `contracts.next_action / contracts.next_actions`。 - 旧动作列仍保留,用于兼容、回滚和后续 cleanup migration 前保护。 - `display_cache_consistency` 调整为只检查 `status_label/status_tone` 与派生状态展示一致。 - `next_action/next_actions` 的用户可见值继续由 `derive_contract_display(...)` 和完整 `ContractActionContext` 派生。 - 详细记录见 `project-docs/V2-phase3-stop-legacy-next-action-writes-2026-07-01.md`。 下一步应先部署 smoke 验证,而不是直接删列。