# 组卷台 · 排版这块动手前必读

> 2026-08-26 定版。版式规则是陆老师拍的，四个尺寸常数是**在 WPS 里量出来的**。
> 改之前先看完这页，尤其是「不要做什么」。

## 版式规则（陆老师定的，别自己发挥）

- 页面按栏等分，**一页 2 / 3 / 4 栏**三档 + 一个「不排版·只印题面」，老师自己选
- **一栏一题**；只有**短算式**两道并排成一行、共占一栏
- 混排（一页里既有应用题又有计算题）时**仍然一栏一题**
- 题面比一栏高的题（带图、超长）占**整数个**栏，空白也是别人的整数倍
- **不加题号** —— 交付的是可编辑 Word，老师增删题后重编号是负担。视觉锚点用灰竖条 `▍`
- 多小问**紧挨着排**，不管几问都算一栏
- **同一页内每栏的书写空白一样高**（占多栏的题是整数倍）

## 四个尺寸常数 —— 不许凭印象改

`export_lib.py` 顶部，全部是 **2026-08-26 在 WPS 里实测**的：

| 常数 | 值 | 怎么量出来的 |
|---|---|---|
| `LINE_H` | 0.60 | 同页两栏 y 距 155px、页宽 415px=21cm → 栏距 7.84cm，减掉给的空白 6.67 和题间距 0.6 |
| `LINE_BASE` | 0.0 | 两行题的行内 y 距 12px = 0.60cm，与上面互相印证 |
| `CHARS_PER_LINE` | 38 | 反推：填空题权重 75.4 实测 2 行 → ≥37.7；应用题权重 44.8 实测 2 行 → <44.8 |
| `BODY_H_CM` | 22.3 | 正文首行顶到正文底量得 22.9，**再减一行当容错** |

⚠ **这四个数量了四轮才对。** 前三轮全是隔着别的引擎猜的，`LINE_H` 一度写成 1.30
（那是 LibreOffice 的行高，WPS 的近一半），每行估大 0.53cm，一页三栏就凭空多算
1.6cm，误差全砸在最后一栏 —— 用户看到的「最后一栏太大 / 空行不一致」就是这么来的。

⚠ `BODY_H_CM` **必须往小取**。末栏现在也画空白，Σ 超过真实可用高就会把这页
最后一题挤到下一页（实测：一道题的行数估少 2 行就发生）。取小只是页底多空几毫米。

## 三条实现上的硬要求

1. **分页靠硬分页符，不靠「空白把页撑满」**
   撑页靠估高，估高一偏大就多挤一题上来（3 栏冒出第 4 题），偏小就把后面的栏推走。
   现在每满 bands 栏插一个 `w:br type=page`，分页段设 `w:line=1 exact`（零高，
   否则它自己占一行、把末栏空白顶到下一页）。

2. **同页空白均分，末栏也要画**
   `unit = (BODY_H − Σ题面高 − Σ题间距) / Σ栏数`，每题 `blank = unit × 它占的栏数`。
   末栏若不画、留给「页底剩多少算多少」，BODY_H 的估算误差就全砸在末栏上。
   **末页例外：不摊满**（末页常只剩一两栏，摊满会给出 15cm 高的空白）。

3. **版式决策只有 `plan_bands()` 一处**
   `_build_md`（导出）和 `/api/layout`（网页预览）都从它取。以前两边各写一套，
   后端改题型分类后就漂了 —— 老师预览看到的和下载拿到的不是一回事。

## 怎么验

```bash
# 20 例回归：造卷 + XML 硬校验（分页/栏数/空白一致性，与渲染引擎无关）
~/.venvs/pdf-tools/bin/python <scratchpad>/regress20/build.py
```

成品与结果留在 `_排版回归_20260826/`（20 份 docx + `_校验结果.txt`）。判据是 **20/20**。

### ⚠ 不要用 LibreOffice 渲的图做视觉验收

两个引擎的尺子差近一倍（单行题面 WPS 0.60 / LO 1.12，一行放的字数也不同）。
拿 WPS 的常数生成、用 LO 渲图，**20 份会全部每页少排一栏、页数翻倍**，
规整成 `[3,1,3,1,…]`。2026-08-26 派了 89 个 agent 看这批图，报出 53 条缺陷、
复核驳回 51 条，剩 2 条也是这个引擎差异的产物 —— **白跑 3.5 小时、530 万 token**。

视觉验收目前只能在 WPS 里人工翻。WPS 没有可用的命令行/AppleScript 接口。

### 写校验器时最容易犯的三个错（都犯过）

- **把「栏」当成「题」**：带图题占 2 栏，按题数算会把正确的用例判成失败
- **单题页无法反推栏数**：「各题空白除以同页最小值」在只有一道题时恒等于 1，
  占 3 栏的超长题也算成 1 栏。这种页要报「未覆盖」，不是「失败」
- **排不满未必是错**：下一题占的栏比这页剩下的多时只能推到下一页（图题不能劈开），
  这时空白按实际栏数均分、整页照样填满。只有「下一题明明塞得下却没塞」才是 bug

## 别的坑

- **三个文件要同步**：`组卷系统/export_lib.py`、`成果/胡小群题库/export_lib.py`
  是同一份，改完 `cp` 过去；`组卷台.html` 的正主是 `成果/胡小群题库/gen_组卷台.py`，
  **别直接改 `组卷系统/组卷台.html`**（09-09 起它只是 0.46 MB 的壳，数据在同目录 `zj_assets/`，也是生成器写的）
- **改完必须重启 8848**：Python 模块缓存过，后端跑了 8 天、改完代码导出的还是旧版
- **WPS 会把「整段只有一个公式」的段落强制居中**，无视 `w:jc=left`。
  破法是段首加零宽空格 `﻿`（`_anti_center`）
- **计算题并排用制表位，不用表格** —— 陆老师明确说过怕表格、表格难编辑
- **`「计算：」前缀只删纯冗余的两类**（`计算：`/`算一算：`）。
  「用竖式计算」「用递等式计算」「列式计算」「直接写出得数」「用简便方法计算」
  都是解题要求，删了题就不成立

## 已知未做

- **混排时计算题一道都配不上对**：规则是「相邻两道短算式并排」，被别的题隔开就断。
  实测纯计算卷落单 0%、混排卷落单 **100%**，右半行全空。
  不动题序的解法是给老师一个「按题型归类」按钮，陆老师尚未拍板。
- **选项行数偶尔少估一行**（9 个实测样本错 1 个），那一栏空白小 0.4cm。
  要分毫不差只能上无边框表格，陆老师说过怕表格，没动。
- 组卷台单文件 **353MB**（2026-09-05 并入全国中考真题后），无头 Chrome 实测「打开到 DATA 可用」**3.8s**
  —— 和并库前的 4.1s 基本持平（时间主要花在网络与解析，base64 图不阻塞首屏）。

## AI 出题这块（2026-08-28 四件套）

- **判等器**：`verify_gen.parse_num` 已认 百分号/带分数/\frac/$壳/度数（18 例自测在 git 外，改动前先跑 `python verify_gen.py` 自检）。裂项类分数求和题豁免分母白名单（看题干形态，不是放宽数值）。
- **seed 契约**：`/api/similar` `by=gen` 时带 `seed` 字段 → 同 seed 必出同一批题（响应回传 seed）。等价 A/B 卷、错题同构重练靠它。DeepSeek 路径无此保证（API 不可复现）。
- **变式组**：`by=variants` 六策略各一道（换数字/扩数位/逆向求解/加干扰/换情境/加一步，GSM-Plus 范式）。⚠ deepseek-v4-flash 会漏掉 vtype 字段——已按位置兜底回填，改策略列表时注意顺序即语义。
- **模板工厂**：`自动出题/模板工厂.py`，DeepSeek 写生成器模块→沙箱验收→自修复 3 轮→落 `gen_候选/`。
  ⚠ **产物必须人工审核后才能挪入 gen/**：实测第一轮就出过「build/check 互验通过、render 却在题面多印绝对值符号」的语义错——harness 管不到渲染层，人工门不可省。
- 业界调研两份在 `自动出题/`：`AI数学出题保正确_业界调研.md`（学术）、`GitHub出题方案调研_20260828.md`（67 仓库）。

## AI 组卷（2026-09-02，「说一句话出一份卷」）

- **架构定死**：后端 `/api/compose` 只做「翻译+校验」，**选题在前端**（57k 题在 `DATA` 内存里）。
  服务端没有也不许有第二份题库。前端 `preNarrow`（正则）缩范围后把专题目录（≤40KB）随请求带上。
- **LLM 编不出假专题**：spec 里的 cd 经服务端三级修正（精确→子串→difflib）后必在目录内；
  「AI 的理解」那句话由**校验后的 spec** 拼出，不用 LLM 自述。`validate_spec` 是纯函数，8 例自测在案。
- **题源纪律**：题库真题优先，缺口只用确定性生成器补（`/api/similar by=gen`，带 seed），
  **绝不用 DeepSeek 现编题顶缺口**（陆老师定）；中考真题 `allow_gen=false`（生成器命中率 0%）。
  生成器按 cd 补题命中率实测：胡小群 48% / 举一反三 43% / 动脑筋 28% / 一课一练 18%，缺口如实显示。
- **归一层**：题型 `tnorm`（8 类）与难度 `diffOf`（易中难，胡小群用 d、其余用栏目 s 当粗代理）各只有一处实现；
  举一反三/动脑筋无题型标注，题型约束跳过并明示。
- **持久化**：AI/生成器题的题体随 `snap()` 存进 localStorage（`o.gen`），`applySnap` 先回灌 `BY` 再过滤——
  否则刷新即丢。AI 题 id 带时间戳（`regAi` 统一入口），老写法 `'AI-'+simSeq` 刷新重号会撞车。
- **改前端 = 改 `成果/胡小群题库/gen_组卷台.py` → 重跑生成 → cp → 重启 8848**，版本握手 `BUILD`/`NEED_BUILD` 同步（现为 2026-09-02.1）。
- E2E：`scratchpad/e2e_compose.py`（Playwright，16 项断言），机器高负载时构建要放后台（踩过：前台 10 分钟超时被杀）。

### AI 组卷提示词调优（2026-09-02 第二轮）
- DeepSeek 7/31 更新 V4-Flash-0731，代码用 `deepseek-v4-flash` 别名自动跟新版，无需改动。
- compose 的 temperature 已从 0.2 → 0（降低同句抖动；DeepSeek temp 0 也非严格确定，但实测刁钻句 3/3 稳定）。
- **提示词踩坑**：知识点规则若写成「先数关键词出现次数」这类字面判定，模型会整体滑进
  字面匹配模式，丢掉意译能力（「分数应用题」→找不到，而不是→「量率问题」）。
  正确写法是「语义优先 + 找齐所有相关专题 + 合着说的要拆」，并放**具体例子**
  （分数应用题→量率问题、加减巧算→加法简算+减法简算）。例子比抽象规则管用得多。
- 教材优先级写成编号列表且「第一条命中就定」：奥数→举一反三/动脑筋（绝不选胡小群）、
  中考→真题、课本→一课一练、初中→培优（即使胡小群也有初中册）、其余小学→胡小群。
- 回归口径：scratchpad/test_v4.py 12 句（含刁钻句×3 重复），改 COMPOSE_PROMPT 后必须 12/12。

### 初中代数生成器批量上线（2026-09-02）
- 模板工厂批产 17 个初中代数题型并全部人工审核收编（清单见 自动出题/CLAUDE.md）。
- 胡小群生成器覆盖 48%→**61%**（初中 22%→47%；小学 76% 不变，缺的是图形/逻辑，主动不建）。
- **审核教训（工厂稿常见病，下批必查）**：假洗牌（shuffle完又按原标签重排→答案恒C/D）、
  写死参数（变体全库只有1-2道题）、干扰项也是正确答案（多解坏题）、题面把答案印出来、
  坏LaTeX（$%s\%s$ 拼出 sin\A）、约束无解的死变体（验收测不出：别的变体会把批次填满）、
  --from-bank 母题跑偏（题库关键词抓到别的题型，宁可手写 --seed）。
- 体检.py 路由断言更新 2 条（None→新模块）：断言比被校验对象老时要跟上现实，别削足适履。

## 线上部署（2026-09-02 起）
- difrobot.cn 已迁**新机 115.159.51.38**（旧机不再对外），组卷台走 git 仓库 **zaojuan-deploy**，
  服务器 /opt/zaojuan-deploy 为 clone。02.1 已上线，验收全过（version/similar+seed/layout/export_docx/paper→COS 302）。
- nginx 白名单：version|topic|page|source|similar|similar_batch|export_docx|report|**compose|layout|paper**；
  openword 故意不放行（本地桌面功能）。xkw 线上无凭证=false，属预期。
- ⚠ 部署侧「7月加工脚本」对 _topic_args 的返回结构敏感（02.1 多返回 role/label 曾致静默半应用，
  deploy 仓库 bdfb165 已修）——**本地改 _topic_args 返回结构时要通知部署侧**。
- ⚠ 服务器与本机共用同一个 DeepSeek key/余额。
- 交接文档：组卷系统/交接_网站部署_20260902.md（02.1）、_20260903_02.2.md、_20260905_05.1.md、_20260906_05.2.md

### 05.2 上线（2026-09-06，zaojuan-deploy 6f586ec）后部署侧提的两条硬约束
- **单文件逼近 Gitee 100MB 上限**：部署侧把 76,077 处内嵌 base64 图抽成 69,766 个唯一文件放 COS 后，
  html 仍有 **95.3MB（gz 20MB，线上首屏 48s）**。下一版**必须**做分片懒加载压回去
  （方案在 `~/.claude/plans/adaptive-churning-falcon.md`），否则进不了仓库。
- **构建必须保持图字节稳定**：本版所有 PNG 被重新编码（像素一致、字节变了），部署侧 6.9 万张**全部重传**。
  根因是构建期每次都跑 `quantize+optimize` 重编码（`gen_组卷台.py fig_b64` / `zujuan_exam._fig_b64`），
  任何 Pillow 版本、参数（maxdim/colors）变化都会让全库字节漂。
  **规矩**：① 不要动这两个函数的参数；② 下一版加「源文件 sha256+参数 → 编码结果」的构建缓存
  （`_fig_cache/`），同源图跨版本字节一致，部署侧只需增量传新图。

## 知识点跨教材选题（KP，2026-09-03 上线，build 2026-09-02.2）

- **数据**：全库 57,299 题按胡小群知识图谱 733 KP 自动标注，`k` 字段覆盖 1.7%→**46.5%**（26,670 题）。
  标注器在 `知识点标注/`：`dump_cands.py`（本地 bge 召回 top10，带年级先验，零费用）→ sonnet 子智能体批量裁决
  （Workflow，60 题/批，`labels_claude/{tb}/batch_*.json`）→ `merge_claude_labels.py`（候选越界剔除 + **阈值 0.6** + id→名）
  → `labels/{tb}.jsonl` → `gen_组卷台.py` 构建期合入（同时清培优「知识点N」伪标、把胡小群 955 金标别名归一到 KP 正名）。
- **质量口径**：金标 955 题实测 sonnet 默认档 @0.6 = 80% 给标注 / 87.5% 命中；DeepSeek 只有 67%/80%；
  sonnet 低推理档只省 13% token 却掉 6 点，**别用低档**。一课一练覆盖仅 33% 是词表口径（奥数体系无校内认数/认图 KP），不是模型问题。
- **前端**：`ensureIdx` 同时建 `KIDX[kp名]=[id…]`；`#fk` 知识点筛选框对所有教材常显；
  AI 组卷 spec.groups 新增 `kps` 组（跨教材取池，年级差≤1 优先、不够放宽到 ≤2，中考题只配初中需求）；
  cpGo 把 KIDX 词表连**年级范围**一起发给后端（`{n,g}`）。
- **后端**：`/api/compose` 新增 `kp`（精确/模糊）与 `kp_like`（子串展开，同一主题多个细分 KP 一次拿全）两种 group；
  提示词规则：老师说「跨教材/不限教材/按知识点」才走 kp 轴，否则 cd 轴不变。回归 `test_compose_回归.py` 现 14 句。
- **重跑/续标**：`labels_claude` 已有的批次不重跑；新增题库只需 `dump_cands.py` 出新批 → 起 Workflow → `harvest_journal.py` 收割 → merge → 重建。
- ⚠ 订阅额度：sonnet 默认档 ≈ 2,000 token/题（agent 不写文件版），一个 5 小时窗口 ≈ 380 批/2.3 万题；开跑前先算总量。

## ⚠⚠ 分片架构（2026-09-09，build 2026-09-09.2 起）—— 动前端/部署前必读

`组卷台.html` 从 499 MB 变成 **0.46 MB 的壳**，题库和配图全在 `组卷系统/zj_assets/`（生成器 `gen_组卷台.py` 直接写到这里）：

| 文件 | 内容 | 加载方式 | 体积 |
|---|---|---|---|
| `data/NN_k.js` | `ZJ.data("教材",[题…])`，按教材切、单文件 ≤12 MB | html 里 `<script src>` **同步**顺序加载，主脚本开始时 `DATA` 已齐 | 14 个 101 MB（gz≈20） |
| `figm.js` | `ZJ.figm({key:[w,h,分片id]})` 配图元数据 | 同上，同步 | 8.6 MB |
| `fig/sNNNN.js` | `ZJ.fig("sNNNN",{key:dataURI})` 按题序 ≈1.2 MB 一片 | **懒加载**：渲染出 `<img data-fig=key>` 占位，MutationObserver 按片拉、回填 src | 301 个 370 MB |
| `vendor/*.js` | MathJax / docx | 外链 `<script src>` | 3.1 MB |

- URL 全带 `?v=BUILD` 破缓存，目录不带版本号；后端对 `/zj_assets/` 给 `immutable` 长缓存，html 仍 no-cache。生成器会**删掉** data/ fig/ 里这版没再写的旧片。
- **为什么不做 DATA 懒加载**：筛选/知识点索引/AI 组卷选题/篮子恢复全假设 `DATA` 全在内存，同步 `<script>` 加载零改造；首屏本机 0.7 s、传输 118 MB（线上 gzip 后 ≈25 MB）。要更快下一步才做按教材懒加载。
- **配图消费者规矩**：渲染用 `figTag(p)` / `inlImg`（自动占位+回填）；要**真字节**的地方（docx 导出、学科网传图、预览/打印/.doc）先 `await ensureFigs(ids)`，再 `await figData(key)`——`FIG[k].d` 可能是 dataURI 也可能是部署侧换成的 **COS URL**，`figData` 会 fetch 转 base64。
  ⚠ 别再直接读 `FIG[d.f].d` 当字节用；`figSrc()` 只在 `buildPaper` 之后的绘制里合法。
- **asset:// 正则现在是六处**（渲染 inlImg / stripTex / md 导出 / docx 切段 / docx 取图 / `figKeysOf`），字符集 `[A-Za-z0-9_\-]`，改一处改六处。
- 启动自检：`ZJ._files < ZJ._want`（分片没加载齐）会直接在页顶挂红横幅，不会静默少题。
- **分片自动重试（2026-09-09 晚，html 壳热修，BUILD 仍 09.4）**：线上首开实测一次链路瞬断把 6 个分片在同一毫秒截断（网站窗口查 nginx：返回字节 < gz 大小，服务端无异常，只有我这个 IP），截断的脚本是 SyntaxError 不触发 onerror。
  做法：每个分片 `<script>` 后跟 `ZJ._mark(文件名)` 记计数，计数没涨的就是坏片 → 同步 XHR 带 `&r=1/2` 重拉，尾巴校验 `]);`/`});` 后 `eval`，两次仍败才挂横幅并点名文件。
  E2E：`_pdf_batch/tools/e2e_retry.py`（Playwright route 首次截断两片→自愈 15/15；永久截断→横幅点名、每片 3 次请求）。分片文件字节不变，只有 html 变，部署只需重推壳。
- **部署侧怎么办**（见 交接_网站部署_20260909_09.4.md）：data/figm/vendor 随 html 进仓库（最大单文件 12.6 MB，Gitee 100 MB 上限彻底解掉）；fig/ 370 MB **不进 git**——
  要么把 `fig/*.js` 里的 dataURI 批量换成 COS URL 后进仓库（每片缩到几十 KB），要么 fig/ 整个放 COS 并在 html 最前注入 `window.ZJ_ASSET_BASE='https://…/'`。
- E2E：`scratchpad/e2e_shard.py` 13 项（分片齐全 / 启动不拉 fig / 四教材图回填 need==data / 预览真图 / 本地 docx 导出 / 分片断网有「图加载失败」标记不崩 / 零 JS 错误）。
- 源文件下载补齐（录入窗口 09-09）：`_专题拆分/_组卷台映射.json` 重建（AMC8/新思路/一课一练8上/胡小群 L7-L9/初中新册全有映射）；
  中考类改 `/api/paper/files?pid=` 列多份 + `/api/paper?pid=&i=`；前端 `updateSrcBar` 中考分支有几份渲染几行。
  ⚠ 线上此前从来没有 源题文件/ 和 数学学习系统/data，源文件下载一直是空的。**2026-09-09 已 rsync 上生产机**（陆老师同意，14.8 GB，加进 .gitignore e73b5ad），
  仅 restart 05.2 后端后 AMC8/胡小群等教材类源文件条已有货（`_paper_index` 是进程内缓存，文件到位必须重启）；多份源件/新思路等 09.2 全量。

- **部署侧使用日志（2026-09-10，网站窗口 zaojuan-deploy 8c48727）**：加工时自动往壳里注 `ui_tracker.js`（select/按钮/加入/回车/打开离开 → POST /api/track）、后端补丁记 `/api/*` 到 usage.jsonl；对我们的**唯一要求**：壳保留最后一个 `</body>`（现在有 2 处，第 1 处在 doWord 模板串里，别再增减），页面里**不要自己定义 `window.__difTrack`**，也别自己包 fetch。周报 `deploy/zaojuan_usage_report.py --flow 老师名`。

## 上海校本卷第二批（2026-09-09，build 2026-09-09.1）+ 全量复核

- 数据：`drafts/batch_school_20260909`（manifest 2026.09.09.3，上海名校 6-8 年级合集 六上~八下），`zujuan_school.py` 改成 `BATCHES` 双批次（各自 assets）。
  两批合计 **1,472 卷 / 36,309 题**（本批 +1,146 卷 / +28,278 题）；年级轴 六上 301 / 六下 180 / 七上 168 / 七下 172 / 八上 314 / 八下 333 / 九上 4；学年组 2018-2025。
  全库 **159,821 题 / 10 教材 / 本地 html 499 MB**（DATA 105.5 MB、FIG 389 MB）。
- 复核改动：① `ingest_som_bank` 的 grade/edition **去掉默认值**（缺字段直接 SystemExit，不再静默塞进「三年级上·普通版」）；
  ② `preNarrow` 年级写法互认（「六上」⇄「六年级上」，gkey 正则带上下册），此前老师说「六上」会把一课一练/新思路的「六年级上·…」轴筛掉；
  ③ 提示词汇编册触发条件收紧（只有 汇编/必刷/压轴 或 期中期末+专题名 才选汇编册，裸「八上期末」走校本卷），此前两条规则互相打架。
  分类汇编 `SRC_DROP` 复核：1,415 扫 / 剔 1,126 / 误剔 0，保留标签全是三模/四模/校级模拟。
- ⚠ **线上体积**：部署侧抽图后 html ≈ DATA + 3.3 MB 壳 ≈ **109 MB，已超 Gitee 100 MB**（05.2 时 95.3）。
  单独外置校本卷配图**无效**（部署侧本就把全部 base64 图抽到 COS），卡线的是题面文本。下一次上线前必须做 DATA 按教材分片（plans/adaptive-churning-falcon.md）。
- 回归 32 句；本机首屏 11 s。⚠ **回归每句一次 DeepSeek 调用，请求体带全量目录+知识点表（≈40-90 KB）**，2026-09-09 一天连跑 6 轮把共享余额烧到 402
  （线上 AI 组卷同时失效）。改提示词后**只跑一轮**，定位单句用 `CASES` 切片，别整套反复跑。

## 并入《新思路辅导与训练·数学》第三版 四册（2026-09-08，新教材「新思路」，build 2026-09-08.2）

- 数据：`成果/新思路题库/<年级><上下>第三版/一课一练_结构化题目.json`（SoM 管线同一课一练，字段同构，id 前缀 XSL六上-…），
  六上 605 / 六下 662 / 七上 1,065 / 七下 899 / 八上 1,152 / 八下 960（09-08 晚加，build 08.4）= **5,343 题，六册齐**。
- **gen 已参数化**：`ingest_som_bank(books, tb, lv, lib_dir, route, use_year)` 一个函数喂 `YKL_BOOKS`（一课一练）和 `XSL_BOOKS`（新思路）两张表，
  别再复制 loop。新思路 `use_year=False`（year 字段是版次备注，gg=「六年级上·第三版」）。
- 后端页图路由 `/原书页/xsl/<xsl6a…xsl8b>/pNNN.png` → `成果/新思路题库/<册>/pages/`（`_serve_source_page` 里 XSLD 字典，走 `_send_png`）。
- 前端：`diffOf` 新思路走一课一练那条（基础→易、拓展→难）；`preNarrow` 加「新思路」；提示词第 4 条加「提到新思路→新思路」；回归第 30 句。
- ⚠ 六上 PDF 页序≠书页序（p027-038 是书 43-54），页图按 pNNN 取不受影响，「上一页/下一页」翻页会跳章，属已知。
- 课次节点含综合块原文（「阶段训练1」「第15章 本章复习题」「期末测试卷 A卷」），lesson_key S1/R15/期末A。
- **答案**：六上/六下/七上的答案在正文 PDF 末尾（book.json `answer_pages`，切成 `00.全册参考答案·教师版.pdf`）；七下/八上/八下正文不含答案，答案是源目录里**单独一份**「…答案….pdf」（20/31/36 页），`split_topics.do_xsl` 2026-09-09 起会找同目录含「答案」的 PDF 硬链进来。六册「下载全册答案」现全有。

## 并入《一课一练 八年级上·普通版 2025秋（配上海新教材）》（2026-09-08，build 2026-09-08.1）

- 数据：`成果/一课一练题库/八年级上普通2025秋/一课一练_结构化题目.json`，SoM 管线定稿 1,178 题 / 58 课次 / 图 274，坏图链 0（定稿备份 过程存档/8s_20260908/）。
- 接法照手册第 ⑫ 步：`gen_组卷台.py` `YKL_BOOKS` 加 `('八年级上普通2025秋','一课一练_结构化题目.json','8s',7,157)`（录入窗口加）；
  后端 `_serve_source_page` 路由 regex 与 `YKLD` 加 `'8s'`（组卷台窗口加）；前后端版本同升。
- ⚠ 一课一练每加一册都是**两处**：YKL_BOOKS（前端数据）+ YKLD/regex（后端页图）。少了后端那处「对照原书」按钮 404，页面不报错。

## 并入《中考·分类汇编》册（2026-09-07，初中培优讲义第 12 册）+ 三处通用修复

- **数据**：数学资料里两套「5 年中考真题分类汇编」学生版 docx（2021-2025 版 17 专题 + 2020-2024 版 12 专题），v2 讲义管线；
  题首保留来源标签「（2025·上海奉贤·三模）」；**SRC_DROP**：标签是 中考正卷/一模/二模 的一律不上架（库里已按整卷收全，剔 1,126 题），
  上架 289 题 / 29 节，节点 4 元第 4 位 = 版本组。k 空。
- **AI 组卷**：提示词「中考分类汇编 + 专题名」→ 培优该册；第 3 条中考规则明写「汇编说法不算」。
- **修复①（preNarrow 通用）**：gg 不是「年级/年份」形态的轴（全国批=省份、培优「中考·分类汇编」）不再被 `hasG` 筛掉——教材命中就整轴带上。
  此前老师说「全国 2024 年中考」全国批会被上海批挤出候选（05.1 起的已知限制，今修）。
- **修复②（optgroup 合并）**：`#fg` change 里分组改成按分组名合并、按首次出现排序，不要求节点连续。
  分类汇编两个版本的专题交错排时，相邻合并会出 25 个碎 optgroup。
- **修复③（提示词顺序）**：校本卷规则从 5b 提到第 5 条、培优降为第 6 条——「第一条命中就定」下「八上月考」会先被「初中内容→培优」吃掉；
  另加「全国批老师说了年份必须 cd_like 年份，不许退成 all:true」。
- **回归脚本升级**：CASES 可带第 3 元 = 理解句必须含的教材/册名，缺了记问题（此前只查 unresolved，教材选错也算过）。现 **29 句 0 问题**。
- ⚠ **磁盘事故**：2026-09-07 重建写 html 时根分区满（剩 <400MB），`open().write()` 半途 OSError，**两份 组卷台.html 都成了 0 字节**、
  本地 8848 直接白屏；13 个 `组卷台.html.bak_*` 共 2.2GB 是主因。规矩：重建前 `df -h /` 剩余 < 2GB 不要跑；备份别再堆在项目目录。

## 并入《上海校本卷》（2026-09-07，新教材，中考类）

- **数据**：`数学学习系统/data/drafts/batch_school_20260907/`（manifest 2026.09.07.6），各校月考/期中/期末**整卷**：
  326 卷 / 8,031 题（八上 310 卷为主，六上~九上少量；同题面去重 -816、小问不单独成卡 -2975），配图 2,533 张（WMF 救回 280）。
  适配器 `zujuan_school.py`（照 `zujuan_exam` 写，公共函数 import 那边的）。
- **轴**：`tb=上海校本卷`，`gg=年级学期`（八上/七下…，与培优册名一致，前端年级识别认它），`cd=「2025学年·南洋模范中学·期中」`
  （学年·学校·考次，同级撞名补 #N），节点 4 元第 4 位=学年 → optgroup，`s/src=考次`，`t` 归成 选择/填空/解答/其他，`d=0`，`k` 空。
- **它是中考类教材**：前后端 `EXAM_TBS` 都加了「上海校本卷」（题型可信、按卷组织、不出类似题、不进批量、allow_gen=false）；
  `FIG_PFX` 加 `xb/`；`preNarrow` 加 校本/月考/期中/期末/模拟卷/押题/学校名 一条（期中/期末也命中培优汇编册，两边都进候选让 LLM 分）。
- **AI 组卷**：提示词 5b 条「学校名/整卷/模拟卷/押题/月考 → 校本卷；八年级期中期末提专题/必刷 → 汇编册；只说八上期末 → 校本卷」；
  轴说明 cd_like 期中/期末/月考/学校/学年，只说年级 → all:true。回归 25-27 句，现 **27 句 0 问题**。
- **原卷按钮**：`_paper_index` 加扫 `drafts/batch_school_*`，源文件在 `源题文件/中考一模二模真题/校本卷20260907/`；实测 333/333 可解析（8 场视觉转录卷的源 PDF 已拷进 校本卷20260907_vision/ 并改成相对路径）。
- ⚠ 构建时间：并入校本卷后 `gen_组卷台.py` 一次 **>10 分钟**（WMF 转换），前台跑会被工具超时杀掉且日志空（stdout 块缓冲）；
  **要 `python -u` 放后台跑**，看日志「生成:」行和 EXIT 码再发布。
- 验收（真浏览器）：教材下拉 9 套；校本卷 gg 7 个；八上 学年组 8 个 2018-2025（2025学年 154 卷最多；「2025--2026学年」双连字符曾被取成尾年 2026，数据侧已修）；张江集团 2026 月考节 5 题全中文、
  MathJax 73 零错误、图 6/6、题面含答案 0、无出类似题按钮（中考类）；`/api/paper?pid=xb-…` 200 docx。全库 **124,733 题 / 9 教材**。

## 并入《八年级期中期末汇编》两册（2026-09-07，初中培优讲义）

- **口径**：拆成**两册**挂培优——`八上·期末汇编`（排 八上 之后，11 节 395 题，一组「期末真题汇编（新教材）」）、
  `八下·期中汇编`（排 八下 之后，7 节 447 题，三组 模拟卷→必刷题→专题）。不叫「八年级·期中期末汇编」：
  前端预筛/年级识别认「八上/八下」开头，合成一册老师说「八上期末」时会被筛掉。
- 数据同九上新教材（v2 管线、units.json 18 条 g8f01-11/g8m01-07、解析版只供下载、k 留空、MTEF 直解 99.9%）。
- ⚠ 填空线落进 MTEF 对象会变成裸 `_` 下标语法（MathJax 报 Missing open brace），数据侧改成 `\_`；本批 1 处。渲染层暂无通用处理。
- 3 份区级期末整卷 PDF（黄浦/杨浦/市北）**未入库**，它们不是汇编，待单独定口径。
- **AI 组卷**：提示词「八年级 + 期中/期末/真题/必刷/压轴/模拟卷/汇编 → 汇编册；只说讲义内容 → 老册」；回归 22-24 句。现 **24 句 0 问题**。
- 验收（真浏览器）：专题01 实数 60 题 / 必刷62题压轴 62 题，全中文、MathJax 204/468 零错误、露源码 0、图 12/12 与 102/102、题面含答案 0、零 JS 错误。
  ⚠ 抽检正则里别把半角 `-` 当异常字符：题面来源标「(24-25八上·黄浦期末)」自带连字符，会全卡命中。全库 116,702 题。

## 并入《九上·新教材》讲义（2026-09-07，初中培优讲义新增一册）

- **口径**：不另立教材，挂 `初中培优讲义 / 九上·新教材`（册序在 九上 之后、九下 之前，`zujuan_v2.BOOK_ORDER`）。
  2026 沪教版五四制新教材第 27 章二次函数（5 考点培优 + 7 新课 + 1 单元测试）+ 第 28 章相似三角形（4 新课，卷商仍在更新，后补走同一册追加）。
  库里原有「九上」是**老教材**（24 相似/25 三角比/26 二次函数），章号内容都不同，不能混册。
- **数据**：v2 讲义管线，units.json 17 条（key nt27x/nk0x/nt28x/nu271，带 `chapter`；解析版 role=教师版 只供下载按钮，题面只从 stem+选项+小问拼），513 题 / 17 节 **4 元节点**（第 4 位=章名 → optgroup），公式全走 MTEF 源码（10,821 对象 99.2% 直解 + 第二解码器补 80，双解码盲比 99.0%）。
  新册 `k` 一律留空（组装器把小节头切坏，真名不可靠），等 KP 标注管线补。
- ⚠ 考点培优里大量解答题只有【详解】没【答案】行，v2 组装器以【答案】为锚点会把它们整段判成概念段（考点03 只切出 7 题）；数据侧给缺行的题补了一行**空**【答案】（不造内容）后 453→513 题。
- **AI 组卷**：提示词规则「老师只说九上 → 默认九上·新教材；说旧教材/老教材才选九上」；回归第 20/21 句覆盖两种说法。
- 验收（真浏览器）：27.1 节 25 卡 / 考点01 节 42 卡，全中文、MathJax 187/237 个零错误、露源码 0、图 21/21 与 22/22、题面含【答案】【详解】0、零 JS 错误。全库 115,860 题。

## 上海中考真题补录 76 卷（2026-09-07，build 2026-09-07.1）

- 数据：`batch_exams_final` 666 卷 / 22,329 题行（manifest **2026.09.07.11**：.3 退金山2020一模乱码卷、.4 修 `^{^\circ}` 84 处、.5 补单字母向量箭头 25 题、.6 把 165 个「文字型 WMF」（的/是/在/为/标点）回填成文字 299 题、.7 补 2 处向量与 2 处「的」、剥 5 题图注垃圾与 1 题页脚、2 题填空线里印答案还原成空线），新增 2012/2013 全年区卷、历年三模、2001/2002/2006 中考正卷（2004 资料没有、2005 是含答案版被拒、2003 无题号被拒）；组卷台内上海批 10,867→**12,714 题**，全库 115,347。.8-.11：三份三模 docx + 金山2026一模重提并入（病根：卷首「考生注意」1．2．3．4．被剥答案器当题号去重）、2003 中考补号并入 27 题、2012/2013 公式图 2,107 张双读转 LaTeX 落地 1,076（几何图 707 张保持原图、230 张分歧仍是图），验收时这两年公式应渲成 MathJax、几何图仍 `<img>`。
- 新卷种 **mock_3（三模）**：`zujuan_exam.TYPE_NAME/TYPE_ORDER` 已加，年内排在二模后；原先 8 份记成 mock_2 的三模卷已改字段（id 不动）。
  前端 `preNarrow` 与后端提示词的考次词表都加了「三模」，回归第 19 句「2025 年上海三模填空题 6 道」→ 须含「三模」的 6 节。
- 区名「浦东新」只在节名显示层映射成「浦东」（`zujuan_exam.DIST_SHOW`），数据层/分组键/id 不动；2014-2026 共 32 节全年份一起变，同年内 cd 零撞名。
- 2012/2013 的 .doc 公式全是 WMF 图（无 MTEF），已栅格化；验收时这两年 MathJax 容器为 0 是正常的，看 `<img>` 数与 `im` 字段相等即可（实测 26/26、32/32）。
- 6 卷未进库（守恒律/切题）：2003 中考、2023/2024 杨浦三模、2024 闵行三模、2026 金山一模、2014 学鼎一模；清单在 `数学学习系统/docs/补录_上海卷_20260906.md`。
- 回归口径现为 **19 句 0 问题**。已知（非本版引入）：「全国各省 2024 年」落到单省 all 而非 cd_like 2024；「近几年 AMC8」只取一个年份 —— spec 的 gg 是单值，跨年份需要多 gg 组，待做。

## 并入《全国中考真题》（2026-09-05，build 2026-09-05.1）

- **数据**：`数学学习系统/data/approved/cn_exams_2026.09.03/`（31 省 2229 卷 / 56,594 题，
  已审核转正，每卷一条 review_record）。适配器 `zujuan_cn.py`（照 `zujuan_exam.py` 写，
  公共函数直接 import 那边的，别复制第二份）。
- **轴**：`gg=省份`（不是年份 —— 全国 31 省 × 18 年，按年分组每年 120+ 张卷挤在一个下拉里
  没法选），`cd=「2019·苏州」`，`s/src=年份`，节点第 4 位=年份 → 前端渲成 optgroup。
- **去重只做一层**（题面哈希）。正式库的场次清单已按 (省,年,卷种,市) 唯一化，不再套上海批
  那套 0.85 整卷指纹（那是为「同一场录了两遍」设计的，这里没有，硬套只会误删）。
  实测另有 **21 张卷整卷被吸收** —— 福建/云南/新疆省统一命题收在多个市目录里、江苏2009 四市同卷，
  题一道不少，只是不重复露面；清单在 `_全国中考并库报告.json`，**不许无声消失**。
- ⚠ **资产 key 前缀**：上海批是 `ex/`、全国批是 `cn/`。前端原来三处渲染点把 `'ex/'+aid` **写死**，
  照抄会让全国卷 5.5 万张内联图全部渲不出来、而题面看着还是完整的（公式那格直接空掉）——
  典型静默错题。现已统一走 `figByAsset(aid)`（`FIG_PFX` 数组），**新增中考类教材只改这个数组**。
- ⚠ 同理，前端/后端凡是判「这是中考卷吗」的地方（题型可信、按卷组织、不出类似题、不进批量、
  allow_gen=false）都已改走 `isExamTb()` / `EXAM_TBS`，别再写死教材名。
- **AI 组卷新增 `{"all":true,"n":N}` 组**（整个年级轴全取）：全国批的 cd 是「2019·苏州」，
  老师说「江苏中考真题选 10 道」时既无专题名也无年份可填，原来必然 unresolved。
  ⚠ **只认显式 `all:true`** —— 空组仍按 unresolved 报出来，否则 LLM 交个 `{}` 就等于「全库随便挑」。
- **已知未做**：全国批**没有知识点(KP)标注**，也没有专题名。老师按知识点点名（「二次函数」「圆」）
  这个教材给不了；且 gg 轴是单个省，「全国各省…」这类跨省诉求只能落到某一个省。
- **并库后必跑**（缺一步就等于没验）：
  1. `python3 gen_catalog_full.py` 刷新目录快照，不刷则回归永远测不到新教材
  2. `~/.venvs/pdf-tools/bin/python3 verify_import_cn.py --base <旧题数>`
     （id 唯一 / 题数增量 / 内联图可解析 / 零答案 / node --check）
  3. 重启 8848 → `test_compose_回归.py`（现 16 句）
  4. **无头 Chrome 真开一次**（node --check 查不出运行时错）：
     `chrome-headless-shell --remote-debugging-port=9222` + CDP `Runtime.evaluate`，
     取 `DATA.length` / `#ftb` 选项 / 拿一道带图题跑 `renderStem` 数 `<img>` 个数。
     ⚠ **别用 `--dump-dom`**：它要把整个 DOM 序列化成一个 JS 字符串，353MB 直接撑爆 V8
       字符串上限（`FATAL: Empty MaybeLocal`），看起来像页面崩了，其实是检查手段的锅。
- 2026-09-05 实测：112,875 题 / 7 教材 / FIG 75,847 张，带图题 renderStem 出 2 个 `<img>`，零运行时异常。

## 并入《AMC8真题》（2026-09-05，与 AMC8考情分析 surface 配合）

- **数据**：`AMC8考情分析/题库/`（2000–2025 共 25 套 / 625 题，零答案，主源 AoPS Wiki）。
  适配器 `zujuan_amc8.py`，图片编码复用 `zujuan_exam._fig_b64`。**只读 questions，
  `amc8_answers.jsonl` 不进读取路径。**
- **轴**（2026-09-05 晚陆老师拍板，build 2026-09-05.2）：`gg=年份`**逐年**（2025年…2000年倒序，
  共 25 个；**没有 2021年**，那年 AMC8 因疫情停办，不是丢数据）、`cd=「2023年 AMC8」`一年一节、
  节点 **3 元不带 optgroup**、`cs=年份倒序`。选完年份默认「全部节/专题」就是整卷，和中考批「一卷一节」同款。
  （组卷台侧最初定的是年代段 3 组，理由是一年只有 1 节；陆老师认为老师找 AMC8 就是按年找，已否决。）
- **题面单语**：`q` 有中译用中译、没有的 81 题用英文；英文原题另存 `qe`，本版不显示不导出。
- **难度 `d` 按题号映射 2/3/4**（1-8/9-16/17-25），对上 `diffOf` 的胡小群刻度。
  ⚠ 这是按 MAA 题序递增惯例**推断**的，不是原卷声明；AMC8 官方从不发布逐题难度。
- `EXAM_TBS` **不含 AMC8**（它不是中考卷，语义上允许生成器补题）。

### ⚠⚠ 资产 id 字符集：五处正则必须一起改

中考两批的资产 id 是 `sha256-<hex>`（无下划线），AMC8 是 `AMC8_2000_04_0`（**有下划线**）。
前端五处解析 `asset://` 的正则原来写的是 `[A-Za-z0-9\-]+`，于是 AMC8 **209 道带图题
一张图都没渲染出来**——图不换、题面里直接露出 markdown 源码，**页面不报错**。
现已全部改成 `[A-Za-z0-9_\-]+`，位置：`stripTex`（纯文本导出）、`inlImg`（渲染）、
md 导出、docx 切段、docx 取图。**新增教材若 id 形态不同，先查这五处。**

### 这次的验收教训（比 bug 本身更值钱）

1. **静态检查全绿 ≠ 没问题**：`node --check`、题数守恒、id 唯一、零答案全过，
   图却一张没渲染。**带图的新教材必须在真浏览器里数 `<img>`**（CDP `Runtime.evaluate` 里
   跑 `renderStem` 数 img，见下方命令），这是唯一能证明渲染链通了的信号。
2. **我的校验器犯了和前端一模一样的错，而且更阴**：正则同样漏下划线 → 一条都没匹配到 →
   却输出「0 道题带内联图 ✓」**空跑当通过**。`verify_import_cn.py` 已加**覆盖度自检**：
   有 `im` 却一条 `asset://` 都没匹配到，直接判失败，不许空跑打勾。
3. **校验器里别手抄常量**：前缀表原来在校验器里手写 `('ex/','cn/')`，加 `am/` 后没同步，
   把 228 张好图报成「解析不出」。现改成**从页面的 `FIG_PFX` 直接读**。

```bash
# 带图新教材的必跑验收（缺这一步等于没验）
chrome-headless-shell --headless --no-sandbox --remote-debugging-port=9222 about:blank &
~/.venvs/latexocr/bin/python3 /tmp/cdpcheck.py   # DevTools 端点在 http://[::1]:9222/json
# 断言：每套教材抽 60 道带图题，renderStem 出的 <img> 数 ≥ im 字段长度
```

## 中考类源文件「转 PDF 后网页预览」（2026-09-09，build 2026-09-09.4）

陆老师定的：**不要「用 WPS 打开」按钮**（老师电脑未必有 WPS），要像胡小群那样**转成 PDF 在网页里预览**；转换**不许用 LibreOffice（公式/根号会失真），必须用 WPS 本体，由我 computer use 批量驱动**。

- 机制：教材类沿用 `/api/topic`（预渲页图 → pdftoppm 现渲）；中考类新加 `GET /api/paper/preview?pid=&i=`，只对 PDF，`topic_source.render_pdf_pages` 用 **pdftoppm（poppler）** 渲页图、缓存在 `.cache_pages/`，输出同款深色页图 HTML（顶部「⬇ 下载 PDF」）。
  `/api/paper/files` 多返回 `groups`：同主名的 docx/pdf 并成一行 `{label,role,docx:i,pdf:i}`（-1=没有）。前端 `updateSrcBar` 中考分支每组一行「📄 预览 | ⬇ Word | ⬇ PDF」，没 PDF 显示「（无 PDF，暂不能预览）」。
- **WPS 批量转 PDF 流程**（WPS Office 12.1 Mac，文件 → 输出为PDF → 多文件输出，输出位置=源文件夹，每份 ≈1 秒，公式/根号/向量/几何图/表格保真，试转 27 份逐页看过）：
  1. 建清单：`_pdf_batch/manifest.json`（`{n,name,src,chunk}`），把 doc/docx **硬链接**成纯数字名 `NNNNN.doc(x)` 放进 `chunk_NN/`（≤400 份一块；中文长路径会让 WPS 文件对话框粘贴出错，纯数字名 + 一块一个目录最稳）。
  2. 驱动：`_pdf_batch/tools/run_chunk.sh <块目录>`（前提：WPS「输出为PDF」多文件窗口开着且列表为空）→ `run_all.sh` 串块跑。点击用 `tools/click`（`click.swift` 编译，CGEvent 真实鼠标事件，逻辑坐标；`mcp__computer-use` 的点击会被「程序坞」命中判定拦掉，截图可用），键盘用 AppleScript keystroke（中文路径走 pbcopy+cmd+v）。
     坐标（1512×982 逻辑屏、WPS 全屏时）：添加文件 688,506（**空列表时居中**，非空时在右上 932,163）；地址栏 1020,66；首行 621,134；打开 1272,651；「已添加到队列」提示的「是」 983,477；开始输出 1167,694；清空列表 834,163；确定 983,477。
     卡住检测：45 s 无产出再点一次开始输出，270 s 无产出截图退出。
  3. 归位：`_pdf_batch/place_pdfs.py`（按 manifest copy 到源文件旁同主名 .pdf；源旁已有卷商 PDF **不覆盖**；块目录留作核对）→ **重启 8848**（`_paper_index` 进程内缓存）。
  第一批 4,315/4,315 零失败（放回 2,538、源旁已有 1,777）；补批 `_pdf_batch2/` 1,741/1,741 零失败（索引里没 PDF 的兄弟文件：解析版 .doc、校本卷「原卷版」等）。新增 PDF 共 **4,279 份 4.2 GB**。终态：全库中考类 4,232 卷 / 6,408 组，**缺 PDF 0 组**。
  ⚠ 踩过：`_VER_RE` 去后缀不忽略大小写，源里 3 份 `.DOC` 大写后缀配不上同主名 .pdf（组里显示无 PDF），已加 `re.I`。
- 踩过：`pkill -f soffice` 子串把 `wpsoffice` 一起杀了（要杀 LibreOffice 用 `pkill -f "soffice.bin"` 或按 pid）；台前调度（Stage Manager）的全屏 Dock 窗口会让 computer-use 判「落在程序坞上」，已按陆老师意思关掉（`defaults write com.apple.WindowManager GloballyEnabled -bool false; killall Dock`，要开回来改 true）；WPS 自绘对话框在辅助功能树里只有侧栏，AppleScript `click at` 无效，只能真鼠标事件；空列表与非空列表「添加文件」位置不同，第一版在旧文件上重按「开始输出」触发 27 次「是否替换」。
- **卷商广告页眉/页脚**（陆老师 09-09 让处理）：扫描 6,056 份转出 PDF 前两页，1,972 份带卷商广告（cs5311742016 / mlxt2022 / DEM2008·优尖升 / 真学子 / 2496342225 五家）。
  `_pdf_batch/strip_ads.py`（PyMuPDF 真删文字）：只删**页眉/页脚带（上下 12%）里含卷商关键词的整个文本块**，正文里「微信收付款」这类题面不碰；每份先验页数不变+pdftotext 无残留才覆盖；
  只覆盖我们自己转出的（块目录那份 + 源旁 **md5 相同**的那份）。⚠ 判「是不是我们放的」不能按大小：卷商 PDF 多半也是 WPS 导出同一 docx，1,357 份大小撞车只差元数据。
  实跑：处理 1,902 / 跳过 70（只有 Zxxk 之类学科网标签，不算广告）/ 失败 0；抽 40 份复核残留 0、页数不变、源旁副本 md5 一致。
  卷商原件：源旁 182 份是卷商自带 PDF，其中 175 份带广告——陆老师 09-09 16:30 拍板「要」覆盖：原件备份到 `_pdf_batch/vendor_orig_backup/`（按源目录相对路径），再用清过的转出版覆盖，页数差全部 ≤2，复核残留 0（`vendor_orig_overwrite_report.json`）。
  再全扫源目录 8,384 份 PDF 又找出 382 份卷商原件带广告（没有 doc 兄弟、或 doc 本身无广告）→ `strip_ads_orig.py` 同套逻辑原地涂（先备份到 vendor_orig_backup/，`STRIP_DRY=1` 演练），382/382 成功 0 失败。备份共 557 份。⬇ Word 下载的 docx 里广告仍在（源文件，不动）。
- 部署：服务器要有 **poppler（pdftoppm）**；新增的 .pdf 在 `源题文件/中考一模二模真题/` 各源文件旁，要 **rsync 上生产机**（只同步 `*.pdf`，按 mtime > 09-09 05:03 挑）；线上 nginx `/api/` 白名单需覆盖 `paper/preview`、`paper/files`。
- 验收：`tools/e2e_srcbar.py`（上海中考 2024 / 校本卷 / 云南 三类源文件条渲出「📄 预览 ⬇ Word ⬇ PDF」，预览 HTTP 200 且页图 >0，零 JS 错误）+ `tools/e2e_shard.py` 13/13。

## 教材类「对照原书」切片的页范围规矩（2026-09-09 陆老师发现新思路讲义页被切掉后全库排查）

`split_topics.py` 一课一练/新思路两支原来都是「一讲的页范围 = 该讲题目所在页的 min~max」，没题的页就丢。全库七套教材逐一核过：

| 教材 | 切法 | 会不会丢页 | 处理 |
|---|---|---|---|
| 新思路 | 题目页 min~max | **会**：每讲开头 1-2 页讲义（要点归纳/疑难分析/例题）没题，六册 259 讲丢 162 讲的第一页 | 起点 = 上一讲末题页 + 1，首讲用 book.json `question_pages[0]`；262 份重切 |
| 一课一练 | 题目页 min~max | **不会**。看着像空隙的 11 处，录入窗口逐页核过全是别的课次键（「概念部分」「7 复习」等，页序夹在中间）的页，题库没漏 | 不动。⚠ 我曾按 3 页目测判成「上一课尾页」改了「空隙归上一课」并重切 876 份，被录入窗口驳回后已撤回（教训：相邻判定按课次首页排会把夹在中间的别的键看成空隙，**先按键查归属再动切片**） |
| 动脑筋 / 举一反三 | `原书页/page_ranges.json`（官方拆分版硬链） | 不会：573 对相邻专题空页 0、重叠 0 | 无 |
| 胡小群讲义 / 课后 | md 锚点 / 「第 N 节」文本锚点，整讲 | 不会 | 无 |
| 初中培优讲义 | 一专题一 docx | 不会 | 无 |
| AMC8 | 一年一卷整份 | 不会 | 无 |

- 两讲之间「没人认领的页」先查是不是别的课次键的页（一课一练全是），确实没人要的再看图定归属（新思路归后一讲）。
- `cut()` 见 dst 已存在就跳过 → 改页范围后要先把 `_专题拆分/<教材>/` 挪开再跑（本次挪成 `新思路.bak-0909-讲义页前`；一课一练已还原，弃用的重切件在 `一课一练.regen-0909-已弃`，两个都可删）。跑完必跑 `build_precut_map.py`（分片壳里 COURSE 锚已改成 `const DATA=`）+ 重启 8848。
- 验收：`/api/topic?tb=新思路&g=七年级上·第三版&cd=11.2(三) 乘法公式(1)` 「共 4 页」且首页是标题页；一课一练 四年级下 期中练习 仍「共 1 页」（p145）。

## 并入《袋鼠数学 Level A / B 历年真题》（2026-09-10 接入位已备好，等录入窗口「定稿」再重建）

- 数据：`成果/袋鼠题库/Level{A,B}/一课一练_结构化题目.json`（SoM 库，字段同一课一练，另加 year / lesson=「2024年 Level A」/ lesson_key=年份 / sec=3分题|4分题|5分题 / stem_en / src_pdf / src_page），`pages/pNNN.png` 是合并 PDF 页图。A 340 题 2012–2026，B 480 题 2005–2026。
- 接法（照 AMC8）：`ingest_som_bank(MK_BOOKS,'袋鼠真题','MK','袋鼠题库','mk', gg_fn=年份+'年', node_key='lesson', d_fn=3/4/5分→2/3/4, keep_en=True)`；
  年份轴倒序；**节键必须用 lesson**（A、B 同年份的 lesson_key 都是年份，按它会并成一节）；`qe` 存英文原题不显示不导出；难度是按分档推的，前端 `hasDiff`/`diffOf` 已加 袋鼠真题，口径同 AMC8 要注明。
- `ingest_som_bank` 本次加了 `gg_fn / node_key / d_fn / keep_en` 四个可选参数，页止给 None 时按 `pages/` 实际张数算；老调用（一课一练/新思路）行为不变。
- 后端：页图路由 `/原书页/mk/(mka|mkb)/pNNN.png`；提示词第 2 条「袋鼠/Kangaroo/Level A/B → 袋鼠真题」+ 轴说明；前端 `preNarrow` 加 `/袋鼠|kangaroo|Level\s*[AB]\b/i`。
- ✅ 2026-09-10 00:50 并库完成（build 2026-09-10.1）：全库 161,344 题 / 11 教材；37 节；d=330/330/160；E2E `tools/e2e_mk.py` 7/7、分片 13/13、回归 34/34（新增两句）。
  ⚠ 提示词踩坑：「袋鼠 A 卷压轴题」首轮把同年 A/B 两节都取了，加了「出现 A 卷/B 卷/Level A/B 字样必给 cd_like」才对——同年份多节的教材，级别词必须落到 cd_like。
  ⚠ 分片数从 14 变 15（`ZJ._want` 16）、fig 312 片、清理旧片 4：10.1 上线是**全量部署**（data/figm/fig 都变），不是只推壳。
