Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
70762f1
fix(cpp): source JSON number digits from shortest round-trip, trim th…
Liam0205 Jul 27, 2026
95fa8f6
fix(java): format JSON numbers like Go instead of falling back to Jac…
Liam0205 Jul 27, 2026
6d07ef4
test(fixtures): cover the number-format regimes byte-exactly (#180)
Liam0205 Jul 27, 2026
2e2a1fa
docs(llmdoc): record the JSON number contract and what each gate can pin
Liam0205 Jul 27, 2026
19bc739
fix(java): keep non-finite doubles quoted so the response stays valid…
Liam0205 Jul 27, 2026
b66ce71
fix: stop mangling non-finite doubles in C++ and shorten Java subnormals
Liam0205 Jul 27, 2026
9af9298
perf(java): take Double.toString when it is already shortest
Liam0205 Jul 27, 2026
7f4edff
perf(java): let the fast path actually cover integer-valued doubles
Liam0205 Jul 27, 2026
fb4ba00
perf(java): count significant digits so sub-1 values hit the fast pat…
Liam0205 Jul 27, 2026
999ea7a
refactor(java): drop the fast path; shortestRoundTrip just searches
Liam0205 Jul 27, 2026
4029c04
docs(java): correct the formatFloatF subnormal claim and the cost figure
Liam0205 Jul 27, 2026
895d9cb
refactor: drop the unreachable positive-exponent pad; fix two doc claims
Liam0205 Jul 27, 2026
ba06bd9
fix(java): register the Go double serializer for primitives and array…
Liam0205 Jul 27, 2026
3044075
fix(java): format float32 with 32-bit shortest round-trip, like Go
Liam0205 Jul 27, 2026
ef51a87
docs: narrow the type-coverage claim to what is actually verified
Liam0205 Jul 27, 2026
92dcb91
test(java): give the float primitive assertion a value that discrimin…
Liam0205 Jul 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions fixtures/server_byte_exact/06_number_format_regimes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"config": {
"pipeline_config": {
"operators": {
"compute": {
"type_name": "transform_by_lua",
"lua_script": "function f()\n return item_score * 2\nend",
"function_for_item": "f",
"function_for_common": "",
"$metadata": {
"common_input": [],
"common_output": [],
"item_input": [
"item_score"
],
"item_output": [
"doubled"
]
}
}
}
},
"pipeline_group": {
"main": {
"pipeline": [
"compute"
]
}
},
"flow_contract": {
"common_input": [],
"item_input": [
"item_score"
],
"common_output": [],
"item_output": [
"item_score",
"doubled"
]
}
},
"request": {
"common": {},
"items": [
{
"item_score": 5e+19
},
{
"item_score": 5e+20
},
{
"item_score": 7.5e+20
},
{
"item_score": 5e-08
},
{
"item_score": 5e-07
},
{
"item_score": 5.000000000000001e+19
},
{
"item_score": 5000000000000000.0
},
{
"item_score": -5e+19
}
]
}
}
13 changes: 11 additions & 2 deletions llmdoc/architecture/dag-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -812,18 +812,27 @@ Pine-Java 注册全部内置算子(`AllOperators.java`),与 Pine-Go `pine-

### 跨运行时格式兼容(GoFormat)

`GoFormat.java` 提供静态方法复制 Go 标准库数值格式化行为:
`GoFormat.java` 提供静态方法复制 Go 标准库数值格式化行为。它有**四个**格式化入口,各自对应 Go 侧不同的函数、阈值不同、**不可互换**

- `sprint(Object)` — 等效 Go `fmt.Sprint`;nil → `"<nil>"`,magnitude < 1e6 的整数值 float → 无小数点(阈值 1e6 匹配 Go 切换科学计数法的边界)
- `formatFloatF(double)` — 等效 Go `strconv.FormatFloat(d, 'f', -1, 64)`
- `formatG(double)` — 等效 Go `fmt.Sprintf("%g", d)`;保留完整精度
- `formatJsonNumber(double)` — 等效 Go `encoding/json` 对 float64 的输出,即 `strconv.FormatFloat(d, 'e'|'f', -1, 64)`:`|x| < 1e-6` 或 `|x| >= 1e21` 走 `'e'`(科学计数),否则走 `'f'`(平铺小数);precision `-1` 表示最短往返表示。这条路径**只服务 JSON 序列化**,与上面三个入口没有调用关系
- magnitude ∈ [1e6, 1e7) 时 `formatG` 将科学计数法表示转换为定点表示,匹配 Go `%g` 在该区间的输出
- `sprint` 支持 `List<?>` 和数组类型,输出 `"[a b c]"` 空格分隔格式(匹配 Go `fmt.Sprint` 对 slice 的行为)
- `formatG` 将 `Infinity` / `-Infinity` 输出为 `"+Inf"` / `"-Inf"`(Go 惯例)
- `formatG` 对 magnitude ∈ [1e-4, 1e-3) 的小数通过 `BigDecimal.toPlainString()` 转换为定点表示
- `sprint`、`formatFloatF`、`formatG` 均保留 `-0.0` 的符号位(输出 `"-0"` 而非 `"0"`),通过 `Double.doubleToRawLongBits` 在各自的整数快捷路径前检测

消费者:`TransformResourceLookup`(key coerce)、`TransformRedisGet`(key 拼接)、`FilterCondition`(条件比较值格式化,替代旧的 `formatValue` 方法)、`ReorderShuffle`(salt 格式化,替代旧的 `formatFloatG` 方法)。第六轮 parity 审计中移除了 `FilterCondition.formatValue` 和 `ReorderShuffle.formatFloatG`,统一使用 `GoFormat` 作为跨算子格式化单一事实源。
前三个入口的消费者:`TransformResourceLookup`(key coerce)、`TransformRedisGet`(key 拼接)、`FilterCondition`(条件比较值格式化,替代旧的 `formatValue` 方法)、`ReorderShuffle`(salt 格式化,替代旧的 `formatFloatG` 方法)。第六轮 parity 审计中移除了 `FilterCondition.formatValue` 和 `ReorderShuffle.formatFloatG`,统一使用 `GoFormat` 作为跨算子格式化单一事实源。

`formatJsonNumber` 的消费链是**独立的一条**:`GoFormat.createGoCompatMapper()` 里注册的 Jackson `Double` 序列化器调用它,该 mapper 的消费者是 `RunCli`(CLI 输出)、`PineServer`(HTTP `/execute` 等响应体)与 `MetricsCollectorTest`(测试侧)。也就是说 `/execute` 响应里的数字字面量**一条都不经过 `sprint` / `formatFloatF` / `formatG`**。

这个分裂正是 issue #180 能长期存在的条件:读文档的人会以为 GoFormat 是格式化的单一事实源,而 JSON 数字实际走的是序列化器自带的一套 double 逻辑;讽刺之处在于 `formatFloatF` 对 1e20 本来就给出正确答案(内部有 `BigDecimal.toPlainString` 平铺),只是 JSON 路径从来没调它。修复后的契约:

- 序列化器对**所有** `Double` 值(含 `0` 与 `-0.0`)都走 `writeRawValue(formatJsonNumber(...))`,不再有任何一条分支落回 Jackson `writeNumber`
- 测试 `GoJsonNumberParityTest.formatJsonNumberMatchesTheSerializer` 钉住「序列化器不得持有格式化规则的第二份拷贝」
- 跨语言等价实现见 `pine-cpp/src/config/json.cpp`(`go_format_json_number`);两处实现共同依赖的实测事实见 `llmdoc/reference/number-formatting-parity.md`

### 资源管理

Expand Down
30 changes: 30 additions & 0 deletions llmdoc/guides/ci-quality-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,34 @@ Nightly diff-fuzz artifact 分歧定位顺序:(a) 下载 artifact,解压 `di

判据:本地复现走弯路超过 30 分钟时,条件反射式切"从末端逐算子截断"策略,不要继续深挖末端错误路径。

### 校验通道能钉住的属性(归一化 vs 字节级)

差分 fuzz 与 cross-validate 大部分通道在比对前做**归一化**,因此有整类属性对它们结构上不可见。新增契约时必须先问「哪条通道会红」,而不是「测试是否全绿」。issue #180(JSON 数字格式跨运行时分歧)暴露的通道能力如下。

**differential-fuzz:归一化抹掉 key 顺序与绝大多数数字字面量差异。** `scripts/differential-fuzz.py` 的 `normalize_json` 做 `json.loads` → `_normalize_value` → `json.dumps(sort_keys=True)`。`sort_keys=True` 使 key 顺序整个维度不可见(issue #183 因此从未被抓到);`_normalize_value` 只对 `float` 分支做 `round(v, 10)` 与小量级归零,`int` 分支原样穿过。

**#180 能被 fuzz 报出来靠的是 Python 的 int/float 类型分裂,不是设计出来的检出能力**:Go 输出 `100000000000000000000` 被 `json.loads` 解析成 `int`(原样穿过),Java 输出 `1.0E20` 解析成 `float` 再 re-dump 成 `1e+20`,两串才不相等。推论:**只有至少一侧输出整数形状字面量(无小数点无指数)时,数字格式分歧才可见**。实测的可见性分档:

| 分歧 | 归一化后可见 |
|------|------|
| Go `100000000000000000000` vs Java `1.0E20` | 可见 |
| Go `100000000000000020000` vs C++ `100000000000000016384` | 可见 |
| Go `9007199254740992` vs Java `9.007199254740992E15` | 可见 |
| Go `0.0000001` vs Java `1.0E-7` | 不可见 |
| Go `1e+21` vs Java `1.0E21` | 不可见 |
| C++ `1e-07` vs Go `1e-7` | 不可见 |
| 第 11 位起的精度差(`1.2345678901234567` vs `...68`) | 不可见(`round(v,10)` 抹掉) |

#180 实际有 15 个分歧,fuzz 结构上只能看见其中一部分。

**`scripts/cross-validate/09-raw-byte.sh` 标题写 "no normalization",实际有回落。** 字节比较失败后会用 `normalize_json` 再比一次,相等就打 `[W]` 警告并**计为 pass**(`09-raw-byte.sh:115-126`)。这是 key 顺序差异被有意容忍的地方,同时也意味着它不能钉住字节级数字格式。

**`scripts/cross-validate/14-byte-exact-execute.sh` 是唯一真字节通道**:curl 响应体直接 `==`,无任何回落。#180 给它补了 `fixtures/server_byte_exact/06_number_format_regimes.json`,覆盖 Go 各个格式化区间(输入 doubled 后分别落在 1e20 / 1e21 / 1.5e21 / 1e-7 / 1e-6 / 最短往返差异 / 1e16 / -1e20)。双向 mutation 验证过有牙:Java 序列化器改回 `writeNumber` → Go-vs-Java 变红;C++ 改回 `chars_format::fixed` → Go-vs-C++ 变红。

**原有的 `04_number_precision.json` 名字看起来正好覆盖数字精度,实际不可能抓到 #180**:输入 `100000 / 1000001 / 0.5`,×2 后全部落在 ±2^53 内的整数值区间——恰好是 Java 旧代码唯一处理对的区间。一个名叫 `number_precision` 却漏掉所有真正分歧量级的 gate,比没有 gate 更糟:它读起来像已覆盖。

**纪律:声称「字节级对等」的属性,必须有一条不做任何归一化的通道覆盖。** 归一化通道只能证明「语义等价」,不能证明字节等价。这与 `guides/cross-layer-validation.md` 的「fixture 比对器语义决定该层能钉住的属性」是同一条原则。当前字节通道覆盖面窄的待决策项记在 `llmdoc/memory/doc-gaps.md`。

### Daily sanitized-fuzz(ASan/TSan 深度诊断)

`.github/workflows/daily-sanitized-fuzz.yml` 每日 schedule 运行 pine-cpp 的 ASan+UBSan 与 TSan 两个 sanitizer-instrumented differential-fuzz pass,复用同一份 `scripts/differential-fuzz.py`:
Expand Down Expand Up @@ -341,6 +369,8 @@ Pine-Java 通过 Sonatype Central Portal 发布到 Maven Central(release profi
- Differential-fuzz 脚本:`scripts/differential-fuzz.py`、`scripts/differential-fuzz.sh`
- DAG differential-fuzz 脚本:`scripts/dag-differential-fuzz.py`
- Cross-validate section 列表:`scripts/cross-validate/`
- Cross-validate raw-byte(带归一化回落):`scripts/cross-validate/09-raw-byte.sh`
- Cross-validate 唯一真字节通道:`scripts/cross-validate/14-byte-exact-execute.sh`、`fixtures/server_byte_exact/`
- Cross-validate metrics-parity section:`scripts/cross-validate/13-metrics-parity.sh`
- Cross-validate pine-cpp 预构建:`scripts/cross-validate/_prebuild.sh`
- 跨引擎 benchmark:`scripts/cross-engine-bench.py`、`scripts/cross-engine-bench-cli.sh`、`scripts/bench-generate-fixtures.py`
Expand Down
Loading
Loading