# T-WIN-022:编码取整与契约参考实现对齐(编码差异收紧为 0) - **状态**: done - **依赖**: T-WIN-021(跨实现一致性向量) - **文档**: v1.6 §7.6.2 - **契约**: `websocket-keypoints 0.2.0` §2 ## 缺陷 `KeypointFrame.encode()` 用 GDScript 的 `roundi` 取整,而契约参考实现 (`opendance-web` 的 `keypoint-codec.ts`)与**生产发送方 Android**(Kotlin `Math.round`) 都用「半数朝 +∞」: | | `-2.5` | `2.5` | `-0.5` | |--|--|--|--| | JS `Math.round` / Kotlin `Math.round` | **-2** | 3 | 0 | | GDScript `roundi`(半数远离零) | **-3** | 3 | -1 | 实测差异:T-WIN-021 的 12 个向量里有 1 个用例(坐标含 `-0.5`)**33 字节不同**。 **影响**:本端是**接收方**,`encode()` 只用于自测与协议回归,因此对产品无影响 —— 但它让「跨实现一致性」这条断言只能是宽松口径(「差异仅限取整边界」), 而宽松口径在实践中会退化成「永远没人看的已知差异」。 ## 修复 ```gdscript ## 量化取整:与契约参考实现(JS Math.round)逐位一致 static func _round_like_js(value: float) -> int: return int(floor(value + 0.5)) ``` `encode()` 的 4 处取整全部改用它(坐标 ×3 + 置信度)。 夹紧顺序(先取整后 `clampi`)与参考实现的「先夹后取整」对任意实数等价 (`32767.4 → 32767`;`-32768.4 → -32768`),已在任务里逐值核对。 ## 验证 1. **单测**(`tests/test_keypoint_frame.gd`,+11 条断言): * `_round_like_js` 的 5 个取值(含 `-2.5 → -2`、`-0.5 → 0`); * 端到端 `-0.5/32767 → 0`(`roundi` 会给 -1,是有效的判别用例); * `test_reference_vectors_decode_and_encode_exactly`:直接读**锁定的参考向量** (`build/integration/frame-vectors.json`),逐值解码比对 + 用 `f32InputPoints` 重编码与 `f32Hex` **逐字节**比对 —— 这一步让 `verify.sh`(只跑源码,无需 pck) 也覆盖了「Web ↔ Windows」这条边。 2. **产物级**(`build/accept-frame-vectors.sh`,已收紧口径): 原先允许「编码差异仅限 `.5` 取整边界」,现在**硬性要求 0 项**。 实测(v0.2.4 发布包): ``` [frame-vectors] ✓ 解码结果与 Web 参考实现逐值一致(float32 精确相等) [frame-vectors] ✓ 本端 encode() 与参考字节完全一致 ✓ 解码差异 0 项(必须为 0) ✓ 编码器字节与参考实现完全一致(0 项差异) ``` `verify.sh`:**785 通过 / 0 失败**(原 774)。 ## 写测试时踩的坑 第一版断言写成「`2.5/32767` 编码为 3」,**失败**。原因不是编码错了,而是 `PackedVector3Array` 把该值存成 **float32**,`float32(2.5/32767) × 32767` 略小于 2.5, `floor(x+0.5)` 得 2 —— 与 JS 对同一 float32 输入的行为**完全一致**。 即:**断言必须基于实际存储精度**,或改用「规则本身 + 有效判别用例」 (`-0.5/32767`)而不是「理想有理数」。这与 T-WIN-021 的两处踩坑同源。