# Devlog · chunk 组装 y-sort 毁双栏读序 — 修复后 2203 NID 0.568→0.936

> 日期：2026-06-10 · 起因：排查"双栏论文首页是确定性天花板"的论断 · 结论：**论断错了一半——大头是我方 chunk 管线 bug,不是天花板**

## 诊断路径(值得复用的方法)

1. 怀疑首页两栏没切开:`-f chunks` 的段落 bbox 横跨全页宽(x 50–545)。
2. 模拟 XY-cut(Python 复刻 reading_order 逻辑):行级切分**其实成功**(页码被横切分离后栏沟 V=22.5 触发)。
3. 临时 diag(examples/diag.rs,已删)打出真实 Line/Block 序:**layout 输出的读序完全正确**(左栏整列→右栏整列)。全页宽 bbox 只是 chunk 按章节聚合的 union box,误导了第 1 步判断。
4. 词级 diff 定位:词袋 F1 0.950 而顺序敏感 NID 0.568,错位块 200–450 词≈一栏的量 → 顺序问题不是内容问题。
5. 逐页 NID:纯散文双栏页(2/3/4/6)只有 0.53–0.63,但行序又是对的 → 矛盾指向 layout 之后的环节。
6. 页 6 chunks 实锤:右栏标题 `5.2. Generalization`(y=216)被插到左栏 `5.1.`(y=219)后面 → **chunk.rs 把 blocks+tables 按 y_top 全页重排**(为了把表格插进正文流),左右栏 y 区间重叠,column-aware 读序被毁。评测恰走 `-f chunks`,markdown/text 无此 sort。

## 修复

[chunk.rs](../../crates/docparse-core/src/chunk.rs):去掉全页 y-sort,保持 layout 给的 block 序;表格按"水平重叠 + 上缘低于表格"找锚点**拼接插入**到所在栏的正确位置(自底向上处理,同锚点的表保持自上而下)。单列页上与旧行为等价。

## 结果(零回归:54 单测、clippy 零 warning、三件套、确定性)

| 同台 | NID 前→后 | 备注 |
|---|---|---|
| vs ODL LTR 12 份 | **0.722 → 0.761** | `2203` 0.568→**0.936**、`2206` 0.615→0.748 |
| vs Docling LTR 10 份 | **0.763 → 0.832** | |
| `normal_4pages` | 0.609→0.580 | 仅此一跌:CJK label-value 页,ODL 按行读、我方按栏读,旧 y-sort 碰巧接近 ODL;属已知 CJK 分歧,net 大正不回退 |

## 教训

- **"确定性天花板"的判定必须落到失败环节**:此前把 `2203`/`2206` 归为"复杂版面需神经模型",实际 layout 读序全对,坏在下游一行 sort。逐页 NID + 词袋/顺序分离这两个手段能快速区分"内容丢失/顺序错/参照错"。
- chunk/输出阶段**不得重排** layout 已定的读序——新增不变量,违反即回归。
- 剩余真天花板:CJK 版面(`skipped_*`/`normal_4pages`)、`2206` 首页残余(0.748)、`amt`(0.660)。
