fix(rectification): harden v9 runtime after adversarial review
This commit is contained in:
@@ -2788,3 +2788,30 @@
|
||||
- `hasRectificationSession` 只断言“存在任意校正 Session”,不区分 draft / collecting / candidate_ready / candidate_accepted / confirmed / closed,因此已完成会话始终被当作可继续,且没有服务端 Case 状态可被测试断言。
|
||||
- `sessions.find(sessionType === "birth_time_rectification")` 取客户端数组第一条,既不保证精确 sessionId,也不校验 Case 绑定;排序、缓存或刷新差异都会改变打开哪条记录。
|
||||
- 旧测试只覆盖“能找到一条校正 Session”的客户端行为,没有服务端 Case 状态机、没有“点击指定 Session 必须精确恢复”的契约、没有并发/双击/多标签幂等断言,也没有 legacy→V9 一次性回填的数据库级验证,因此这些缺陷在回归中被遗漏。
|
||||
|
||||
## BUG-163 | V9 红队审查:引擎适配器契约不匹配 + accept 重放幂等顺序错误
|
||||
|
||||
- 状态:resolved(已修复;真实 PostgreSQL 17 与真实 Python 引擎双重实证)
|
||||
- 首次发现:2026-08-11(红队审查 `60e2ce4f..724fb64c`)
|
||||
- 最近更新:2026-08-11
|
||||
- 影响面:V9 候选比较管线(compare-candidates / read-diagnostics)、候选采用重放幂等、确认重放返回字段
|
||||
- 现象:
|
||||
1. `rectification-compare-candidates` 在生产面对真实 Python 引擎必然失败:前端 `engine-client.ts` 假设 `/api/rectification/v5/score` 返回 `rank/tied_minute_count/representative_time/confidence/margin_percent/selection_allowed/confirmation_allowed`,实际引擎只返回 `candidate_scores:[{time,score,supporting_event_ids,conflicting_event_ids}]`(`scripts/rectification/api_service.py:score_candidates`,已用真实 HTTP 请求实证);同时 V9 evidence kind/domain(`education_start/career_entry/promotion/relationship_commitment/finance_gain` 等)直接透传给引擎,而引擎 `SCOREABLE_EVENT_KINDS` 只有 `education_milestone/relocation/relationship_start|change/career_change/finance_change/self_health_event(health_pressure)`,几乎全部 400。结果:所有候选比较必然 `engine_no_candidates` 或 `engine_http_error`,V9 无法产出任何候选。
|
||||
2. `accept_agentic_rectification_candidate_for_case` 的幂等重放分支位于 profile 基线校验**之后**;第一次 accept 写入 `active_birth_time` 后,基线快照与 profile 必然分歧,重放/双击/断线重试返回 `candidate_profile_changed` 而不是 `idempotent=true`(旧 `accept_agentic_rectification_candidate` 是先重放后校验,语义回归)。
|
||||
3. `confirm_agentic_rectification_birth_time` 幂等分支在 `v_result` 载入前引用 `v_result.id`,confirmed 重放响应的 `result_id` 恒为 null。
|
||||
- 触发条件:任何进入 compare-candidates 的真实运行;accept 成功后同一候选再次 accept;confirmed 后同一 confirm 重放。
|
||||
- 根因:
|
||||
- 新增 `engine-client.ts` 从未与真实引擎做契约测试(既有测试全部 mock `runV9CandidateScore`,未覆盖真实响应形状);V9 evidence 领域模型与引擎粗粒度评分词汇之间缺少 kind/domain 翻译层。
|
||||
- accept/confirm RPC 的重放语义被基线保护逻辑错误地前置/后置,未对齐旧实现的先重放后校验顺序。
|
||||
- 修复:
|
||||
1. `engine-client.ts` 对齐真实引擎契约:新增 `toEngineScoreableEvent` kind/domain 翻译(education_*→education_milestone、career_*→career_change、relationship_start/commitment→relationship_start、relationship_separation→relationship_change、relocation→relocation、finance_*→finance_change、self_health_event→health_pressure;family/other 留在账本但不再进引擎);`readCandidates` 由 `time+score` 按分数降序推导 rank、同分 tied_minute_count、相对支持度归一化;`representative_time`=top1;`selection_allowed`=有候选;`confirmation_allowed` 只来自引擎 `can_confirm_exact_minute`(真实引擎当前为 false,confirm 门诚实关闭);`margin_percent` 取自 diagnostics;confidence 由 margin+retention 推导;无 scorable 事件/无候选时 fail-closed(`no_scorable_evidence`/`engine_no_candidates`)。
|
||||
2. `20260813010000_agentic_rectification_v9_agent_api.sql`:accept 重放分支移到 profile 基线校验之前,重放分支校验 profile 与已采用时间一致(对齐旧语义);confirm 幂等分支补 `select id into v_result.id`,`result_id` 不再为 null。
|
||||
3. 移除 `rectification-v9-tools.ts` compare-candidates 中双分支同 throw 的死代码。
|
||||
- 验证(真实执行,非 mock):
|
||||
- 本机安装 PostgreSQL 17(brew),按 `deploy/postgres/001-bootstrap-roles.sh` 建角色,从空库全量应用 95 个迁移 ×2(second run 95 already applied,`--check` exit 0),含两个 V9 迁移。
|
||||
- 真实 SQL 行为:open homepage create/resume、requestId 幂等、session 精确恢复、跨用户 404、new 冲突、profile_incomplete、evidence quote grounding/idempotent/confirm/revision lineage、terminal 只读、accept→accepted + 重放 idempotent=true(修复后)、confirm gate=false blocked + gate=true 且 consent 原文匹配 → confirmed + 重放 idempotent + `result_id` 非空、指纹缓存复用、profile 变更 → needs_rebaseline + 候选 invalidated、legacy backfill(confirmed/candidate_accepted/superseded/abandoned 分布、results_mapped=3、重跑 cases_created=0、verify 0 conflict/0 orphan)、RLS service_role-only 均实证通过。
|
||||
- 真实 Python 引擎:`scripts/jyotish_api_server.py` 启动后,修复后 `engine-client` 直连 `/api/rectification/v5/score` + `/v5/diagnostics` 成功产出候选(rank/relative_support/tied)、family_event 被排除、`confirmation_allowed=false`、diagnostics 键完整映射。
|
||||
- 新增 `rectification-v9-engine-contract.test.ts`(9 项,含真实引擎响应形状 fixture)与 migration 顺序静态回归 1 项;v9 聚焦 33+108 全部通过;全量 `npm test` 1256(1232 pass / 18 Docker ENOENT 环境失败,与基线 6d7a9a97 同因,worktree 实证);`tsc --noEmit` 仅剩 6 个既有未触碰测试文件错误;ESLint 0 error;`next build` 通过;`git diff --check` 通过。
|
||||
- 防复发:引擎适配器必须有真实响应形状的契约测试;新增任何映射层必须对照 `scripts/rectification/contracts.py:SCOREABLE_EVENT_KINDS`;RPC 重放/幂等语义以“先重放后校验、重放校验已落库状态”为唯一实现顺序;迁移修改必须在真实 PostgreSQL 上从空库全量应用并重跑。
|
||||
- 相关记录:BUG-162、BUG-112
|
||||
- 修复版本:本地 staging 候选(未 push / deploy)
|
||||
|
||||
Reference in New Issue
Block a user