> 预处理残渣有三种,而守卫只有两道——被剥成普通单词的那一种无人可见
一个把 LaTeX 论文剥成纯文本、再交给若干个模型读者去评审的工具, 对"剥坏了"这件事已经建了两道守卫,而且写明了为什么要分两道:
结论先行¶
一个把 LaTeX 论文剥成纯文本、再交给若干个模型读者去评审的工具, 对"剥坏了"这件事已经建了两道守卫,而且写明了为什么要分两道:
| 残渣形态 | 下游读者看到 | 守卫 |
|---|---|---|
① 残留的 \macro |
"我不知道这是什么" | 警告(stderr 列出前 12 个) |
② 语法空洞 from to、( ,、= , |
看不见 | 拒写(REFUSED_HOLES,退出码 3) |
分级的理由写在函数的 docstring 里,是我见过最好的一句:
一个空洞不是空的。读者不会把它留空——它会编两个数填进去, 然后把自己两个发明之间的矛盾报成论文最严重的缺陷, 而且是在那个以"内部一致性"为职责的角色里,于是排序规则给它背书。
但还有第三种,它既不是 \macro 也不是空洞,而是一个看着像作者写的普通单词。
我在当前版本(2026-09-25.1)上复现了:
源文件 caption: Latency under a \emph{sharp} load step; the knee is visible.
读者收到的: [FIGURE fig:knee: Latency under a ph sharp load step; the knee is visible.]
^^
两道守卫全程沉默:extraction_residue() 扫的是 \\([A-Za-z@]+),ph 没有反斜杠;
_HOLE 扫的是语法缺口,ph sharp 不缺任何东西。退出码 0,stderr 一个字都没有。
下游的 skim reader 把 ph 报成了"论文里有个没清掉的占位符"。
机理是一行正则:命名表按前缀匹配,不要求词边界。
_known = sorted(set(_FORMATTING) | set(_MATHSYM) | set(_MATHDROP), key=len, reverse=True)
t = re.sub(r"\x03(" + "|".join(...) + r")", ...) # ← 没有 \b
em 在表里、emph 不在,于是 \emph 命中 em、吐出 ph。
我扫了 31 个真实论文会用、但不在表里的命令,14 个被静默截断:
| 命令 | 命中的前缀 | 留在正文里的残渣 |
|---|---|---|
\emph |
\em |
ph |
\texttt / \textsc / \textsf / \textup |
\text |
tt / sc / sf / up |
\textnormal / \textsuperscript / \textsubscript |
\text |
normal / superscript / subscript |
\caption / \captionof |
\cap |
tion / tionof |
\newline |
\ne |
wline |
\num{12.7}(siunitx) |
\nu |
m 12.7 |
\todo |
\to |
do |
\includegraphics |
\in |
cludegraphics |
normal、superscript、do 是英文单词。它们不像残渣,像作者的句子。
为什么¶
残渣的严重性不取决于它删掉多少,取决于它对下一级是否可见。 这正是那两道守卫的分级标准, 而第三种恰好落在标准的盲区里——它比空洞更隐蔽,因为空洞还有个洞,它连洞都没有。
三种残渣引出的下游行为完全不同:
| 读者的反应 | 报告里的样子 | |
|---|---|---|
① \macro |
诚实地说不懂 | 噪声,可丢 |
| ② 空洞 | 编内容填上 | 把自己的发明报成论文的矛盾 |
| ③ 普通单词 | 当成作者写的 | 一条形态完整、对着论文的缺陷指控 |
第三种的产出最像真发现,所以最难丢。
更值得记的是它是怎么来的。那个词边界是故意去掉的,理由写在同一个函数里,相隔二十行:
已知名字优先消费,且不要求其后有词边界,因为
\noindent Limitations到这一步时空格已经没了、成了一个 token——老的查表漏掉它,吐出\noindentLimitations。
\noindentLimitations 是第一种残渣:丑、显眼、读者会说不懂。
为了消掉它而去掉词边界,把它换成了第三种:ph、tion、normal。
按这个函数自己写下的分级标准,这个交换的方向是错的。
还有一层:_known 是 _FORMATTING | _MATHSYM | _MATHDROP 三张表的并集。
text 之所以在表里,是因为数学模式下 \text{...} 该被丢掉——那是个数学模式的条目,
现在却成了整个 text* 文本族的前缀陷阱。把两个命名空间并进一张表,
表里最短的那个条目就成了另一个命名空间的陷阱。
判别法很简单,而且两种情况都能照顾到:前缀匹配只在余部以大写字母开头时才成立。
\noindentLimitations 的余部是 Limitations(大写,是正文的词);
误命中的余部一律是 ph/tt/sc/sf/tion(小写,是命令名的尾巴)。
怎么用¶
- 任何"剥取 → 交给另一个判断者"的管道,残渣计数器要按下游的可见性分级,不是按形态分级。 能被下游说出"我不懂"的,警告就够;下游会当成内容的,必须拒绝交付。
- 先列出第三种的清单。 方法很便宜:拿一个包含几十个真实命令的探针文件跑一遍, 逐个看输出里有没有不该出现的小写碎片。我用 31 个命令换到 14 个缺陷,耗时几分钟。
- 名字表按前缀匹配时,给余部加一个形态判据(首字母大写 / 非字母), 否则表里每一个短条目都是一个陷阱,而陷阱数量随表的增长而增长。
- 不要把两个命名空间并进同一张查找表,或者并了之后给条目带上它所属的模式。
- 下游报出"论文里有个占位符 / 有个没清掉的标记"时,先在交给它的那份文件里 grep 那个词。 这是消费侧的同一件事,见 八个审稿人都报了同一个我从没见过的缺陷——先怀疑他们读到的那份东西; 本篇是生产侧。清洗步骤的自检通则见 清洗步骤必须有一道"我是不是清过头了"的自检——它差点删掉一整本书, "一个标签盖了三种根因"的同形问题见 一个标签盖了三种根因,而三种的处置相反——"可能是幻觉"的三张脸。
出处¶
~/Tools/nonexpert-review(nonexpert_review.py 2026-09-25.1)。
守卫与分级理由在 extraction_residue() 及其调用处;前缀匹配在 reader_view() 末尾。
ph 的复现、31 个命令的扫描、以及 body 路径上 \textsf{A9} → sfA9 的同类命中,
都是我这次在当前版本上跑出来的。第三种残渣的下游后果有一条真实的现场回报(2026-10-12):
caption 里的 \emph 被读者报成"论文里的占位符"。