diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 734e0022..b6d279c8 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -1662,3 +1662,17 @@ - 防复发:模型调用成功、Schema 成功和问题被接受必须分开观测;Opportunity utility 不得把历史关键词与“未覆盖领域”组合成伪装的固定轮询。 - 相关记录:BUG-086、BUG-090、BUG-091 - 修复版本:`birth-time-rectification-v6` / `rectification-agent-v6-1` + +## BUG-093 | 首次候选评分因跨语言计算规格哈希不一致而失败 + +- 状态:resolved +- 首次发现:2026-07-29 +- 最近更新:2026-07-29 +- 影响面:V5 Candidate Engine 首次达到三事件评分门后的 Feature Snapshot 原子持久化 +- 用户现象:第三件可评分事件已经保存在 Turn,但 Worker 最终显示“这次比较没有完成,回答已经保留,请再试一次”,Case 恢复上一问题且没有写入新事件。 +- 根因:服务器创建 Case 时由 TypeScript 对整数时区 `8` 计算规格哈希;Python 请求归一化把它变成浮点数 `8.0`,而 Python JSON 序列化保留 `.0`。两个运行时对语义相同的规格得到不同哈希,完成事务因此拒绝 Feature Snapshot 并抛出 `rectification_v5_feature_snapshot_mismatch`。 +- 修复:Python 生成跨服务 Calculation Spec 时将整数值的经纬度和时区规范化为整数,使其 JSON 数字表示与 TypeScript `JSON.stringify` 一致;评分算法和候选矩阵不变。 +- 验证:新增已知 TypeScript 哈希向量测试,修复前稳定失败、修复后通过;staging Case `e2d3e1d2-efb0-461d-9914-f890bc2b8569` 的 PostgreSQL 日志确认原始异常,使用同一规格重放确认 Python Feature Snapshot 哈希恢复为 Case 哈希。 +- 防复发:跨语言持久化指纹必须使用已知向量验证 JSON 数字规范化,不能只在各自语言内断言自洽。 +- 相关记录:BUG-087、BUG-092 +- 修复版本:`rectification-v5-matrix-scoring-1`(仅修复输入规范化,算法版本不变) diff --git a/scripts/rectification/scoring_service.py b/scripts/rectification/scoring_service.py index 6f119573..84a2816f 100644 --- a/scripts/rectification/scoring_service.py +++ b/scripts/rectification/scoring_service.py @@ -150,11 +150,16 @@ def score_from_matrix(request: RectificationRequest, built: dict[str, Any]) -> l def calculation_spec(request: RectificationRequest) -> dict[str, Any]: + def json_number(value: float) -> int | float: + return int(value) if value.is_integer() else value + return { "version": INPUT_CONTRACT_VERSION, "birthDate": request["birth_date"], "candidateRange": {"start": request["start_time"], "end": request["end_time"]}, - "latitude": request["lat"], "longitude": request["lon"], "timezoneOffsetHours": request["tz"], + "latitude": json_number(request["lat"]), + "longitude": json_number(request["lon"]), + "timezoneOffsetHours": json_number(request["tz"]), "ayanamsa": "lahiri", "nodeMode": "mean", "minuteStep": 1, } diff --git a/tests/test_rectification_v5_services.py b/tests/test_rectification_v5_services.py index 76bd1224..60364f0e 100644 --- a/tests/test_rectification_v5_services.py +++ b/tests/test_rectification_v5_services.py @@ -6,7 +6,13 @@ from unittest.mock import patch from scripts.rectification.api_service import diagnostics, score_candidates from scripts.rectification.contracts import normalize_rectification_request -from scripts.rectification.scoring_service import build_event_contribution_matrix, sample_event_dates, score_from_matrix +from scripts.rectification.scoring_service import ( + build_event_contribution_matrix, + calculation_spec, + sample_event_dates, + score_from_matrix, + sha256, +) from scripts.jyotish_api_server import ( API_COMMAND_MAP, TECHNIQUE_EXAMPLE_ENDPOINTS, @@ -38,6 +44,13 @@ def request(*, precision: str = "month", event_kind: str = "education_milestone" class RectificationV5ServicesTest(unittest.TestCase): + def test_calculation_spec_hash_matches_typescript_for_integral_timezone(self): + normalized = normalize_rectification_request(request(), today=date(2026, 7, 28)) + self.assertEqual( + sha256(calculation_spec(normalized)), + "f05fe0f56ef9ba2b18ec3c6c54f1649f06f1ae5a926491a5c5f676d718d92865", + ) + def test_shared_validator_rejects_family_and_non_self_health_scoring(self): with self.assertRaisesRegex(ValueError, "domain is not scoreable"): normalize_rectification_request(request(domain="family", event_kind="family_bereavement"), today=date(2026, 7, 28))