From 68c1bdba462e36bfb8dd5136ea62168e43a50edb Mon Sep 17 00:00:00 2001 From: Jesse_Chen Date: Sun, 26 Jul 2026 14:05:03 +0800 Subject: [PATCH] fix: accept diagnostic rectification gates --- docs/BUG_HISTORY.md | 15 +++++++++++++++ .../src/lib/birth-time-journey-adapters.ts | 9 +++++++-- .../tests/birth-time-journey-adapters.test.ts | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 3993d75a..56ed733f 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1429,3 +1429,18 @@ - 相关记录:BUG-018、BUG-073 - 复发自:BUG-018 - 修复版本:待提交(本地可测) + +## BUG-077 | Python 诊断门状态导致历史事件评分误报 503 + +- 状态:resolved +- 首次发现:2026-07-26 +- 最近更新:2026-07-26 +- 影响面:生时校正历史事件评分、Python 技术合同到 Web 候选结果的适配边界 +- 用户现象:用户提交第一条可评分历史事件后,请求返回 503,日志显示 `stage=score_events error=ZodError`,开放叙事无法继续收敛。 +- 触发条件:Python `technique_contract.gates` 返回诊断专用状态 `diagnostic_fail`。 +- 根因:Python 引擎合法使用 `diagnostic_fail` 表示诊断未通过但不构成技术异常;Web wire schema 只接受 `pass | fail | blocked | not_evaluated`,因此在业务判断前拒绝整份候选结果。 +- 修复:Web wire schema 接受 `diagnostic_fail`,并在共享适配边界将其规范化为内部既有的 `fail`;不扩大持久化合同,不修改 Python 引擎。 +- 验证:适配器回归覆盖含 `diagnostic_fail` 的技术合同并断言内部状态为 `fail`;聚焦测试、目标 ESLint 与 `git diff --check` 通过。 +- 防复发:跨语言 wire schema 必须覆盖 Python 真源可返回的枚举;诊断状态在适配边界归一化,内部领域模型继续保持最小稳定集合。 +- 相关记录:BUG-067、BUG-075 +- 修复版本:本次修复提交 diff --git a/frontend/src/lib/birth-time-journey-adapters.ts b/frontend/src/lib/birth-time-journey-adapters.ts index 252f1d8e..9697ef09 100644 --- a/frontend/src/lib/birth-time-journey-adapters.ts +++ b/frontend/src/lib/birth-time-journey-adapters.ts @@ -141,7 +141,7 @@ const candidateResultApiSchema = z.object({ confirmation_allowed: z.boolean().optional(), decision: z.enum(["continue_rectification", "confirm_minute"]).optional(), gates: z.record(z.string(), z.object({ - status: z.enum(["pass", "fail", "blocked", "not_evaluated"]), + status: z.enum(["pass", "fail", "diagnostic_fail", "blocked", "not_evaluated"]), reason: z.string(), }).strict()).optional(), }).optional(), @@ -299,7 +299,12 @@ function adaptCandidateResult(parsed: z.infer): canonicalInputHash: parsed.technique_contract.canonical_input_hash, confirmationAllowed: parsed.technique_contract.confirmation_allowed, decision: parsed.technique_contract.decision, - gates: parsed.technique_contract.gates, + gates: parsed.technique_contract.gates + ? Object.fromEntries(Object.entries(parsed.technique_contract.gates).map(([name, gate]) => [ + name, + { ...gate, status: gate.status === "diagnostic_fail" ? "fail" : gate.status }, + ])) + : undefined, } } : {}), }); } diff --git a/frontend/tests/birth-time-journey-adapters.test.ts b/frontend/tests/birth-time-journey-adapters.test.ts index 9bd7c1ad..ad16a7fb 100644 --- a/frontend/tests/birth-time-journey-adapters.test.ts +++ b/frontend/tests/birth-time-journey-adapters.test.ts @@ -249,12 +249,30 @@ test("rectification adapter normalizes an event-scored candidate result", () => legacy_server_metadata: { source: "existing-engine" }, }], algorithm_version: "birth-time-event-scoring-v1", + technique_contract: { + calculation_status: "evaluated", + used_divisional_charts: ["D10"], + used_arudha: [], + dasha_tracks: ["vimshottari"], + missing_layers: [], + auxiliary_layers: [], + hard_blockers: ["neighbor_stability"], + confirmation_allowed: false, + decision: "continue_rectification", + gates: { + neighbor_stability: { + status: "diagnostic_fail", + reason: "diagnostic_only_unique_lead_at_plus_minus_1_2_5_minutes", + }, + }, + }, }); assert.equal(result.resultId, "1d8ee348-61a3-433d-8907-ff6d281b9992"); assert.equal(result.winningSegment?.representativeTime, "14:24"); assert.equal(result.canApply, false, "an old or incomplete engine receipt cannot open minute confirmation"); assert.deepEqual(result.evidence[0]?.ruleIds, ["vim_md_domain_house"]); + assert.equal(result.techniqueReceipt?.gates?.neighbor_stability?.status, "fail"); assert.equal("legacy_server_metadata" in (result.evidence[0] ?? {}), false); });