发布历史
本文档记录了每个版本的用户可见变更、实现修复与验证测试证据。条目按发布版本线分组,最新版本置于最前。单个提交请查阅 Git 提交历史,已发布的版本记录请查阅 GitHub Releases 页面。
请将各个条目视为对应版本的历史事实陈述。文档中保留了旧版配置名称、依赖项选择、默认值、压测规模与性能测量数据,以便读者在具体的历史语境中理解版本升级。有关当前 API,请参阅 React、Vue 与 Mantine 参考文档以及指南目录;若从 1.x 升级,请从迁移对照指南开始阅读。
验证测试数量属于对应候选版本报告的历史数据,并非针对本次文档修订重新执行的测试。同样,零缺陷的模糊测试(fuzz)或压测轮次仅能证明其受测输入族与配置下的表现;后续条目将说明在扩展这些输入族时发现的其他缺陷。
3.2.2 — 语言自动检测默认开启
Section titled “3.2.2 — 语言自动检测默认开启”本次补丁版本将 engine、core、React、Mantine 和 Vue 同步升级至 3.2.2,其中 engine、core、React 与 Vue 仅做版本对齐。@ai-markdown/code-language-detector 仍为 1.1.0,高亮插件仍为 1.0.2。
Mantine
Section titled “Mantine”codeBlock.autoDetectUnknownLanguage默认值改为true。 只要@ai-markdown/code-language-detector能够判定,未标注语言的代码块无需任何配置就会得到标签与语法高亮。检测器在证据不足时会放弃判断而不是猜测,因此无法判定的代码块仍按纯文本渲染,标签为“unknown”,显式声明的围栏语言也永远不会被覆盖。检测不需要额外依赖,在渲染期间同步执行(包括服务端渲染),典型代码块耗时约 0.3 毫秒。- 对现有应用是可见变化。 在
3.2.1中按纯文本渲染的未标注代码块,现在只要被检测器识别,就会显示语言标签页和语法高亮。在真实文件上,检测器约有 3% 的代码块会判到错误的语言家族(详见探测器 README)。如需保持原有行为,传入codeBlock={{ autoDetectUnknownLanguage: false }},或在defineMantineBehaviors中设置。
Mantine 的全部 102 项单元测试通过,React Storybook 套件(111 项)也全部通过。自 3.2.1 以来引擎源码没有改动,压测影响检查判定此区间无需压测。
3.2.1 — 代码语言探测器 1.1.0
Section titled “3.2.1 — 代码语言探测器 1.1.0”本次补丁版本将 engine、core、React、Mantine 和 Vue 同步升级至 3.2.1,其中 engine、core、React 与 Vue 仅做版本对齐,自 3.2.0 以来源码没有改动。@ai-markdown/code-language-detector 升级至 1.1.0,@ai-markdown/react-mantine 现在依赖 ^1.1.0。独立发布的高亮插件仍为 1.0.2。
代码语言探测器 1.1.0
Section titled “代码语言探测器 1.1.0”- 准确率改为在规则从未参照过的文件上测量。 我们手工挑选了 504 个真实文件(42 种语言各 12 个,来自宽松许可的仓库),结果表明
1.0.0给出的「跨语言家族误判 0%、loose precision 100%」只在调规则所用的语料上成立。README 现在优先报告留出集的数字:单次检测覆盖率 74.7%、loose precision 96.8%,流式检测中 2.4% 的文件出现过跨家族翻转,最终判定正确率 90.8%。与1.0.0在同一留出集上相比:覆盖率从 68.3% 提升至 74.7%,最终判定正确率从 83.6% 提升至 90.8%,出现翻转的文件从 457 个中的 13 个降至 11 个。 - 评分前清空 fence 内的代码。 满是 TypeScript 示例的 Markdown 指南会判为
markdown;Python 提示词模板中用 fence 包裹的 JSON 示例不再算作 JSON 证据。连续的///或//!文档注释会给 Markdown 扣分,以三引号 docstring 开头的片段不会被判为 Markdown。 - 打成平局的近亲语言也会得到判定。 当证据分不清 C 与 C++、JavaScript 与 TypeScript、CSS 与 SCSS、Less,但相对其他所有语言已能确定所属家族时,检测器以恰好
0.8的置信度给出最可能的成员。在1.0.0中返回language: null的片段,例如只含#include与static函数的 C 文件,或不含 SCSS、Less 语法的样式表,现在会返回c或css。置信度恰好为0.8表示确定了家族,但不确定具体成员。 - 跨家族误判修复。 以
<?php开头的片段排除 JavaScript、TypeScript、C# 与 Java;Dockerfile 的# syntax=指令成为决定性证据,大写的RUN行会给 Bash 扣分;Markdown front matter 能与 YAML 区分;Dart 的part of指令与factory构造函数、Ruby 的魔法注释都有了专属规则;Sorbet 的T::Array[...]不再被读作 Scala,import Select from "…"不再被读作 SQL,许可证文本里的retain也不再计为 Objective-C 证据。 - 覆盖率。 新增签名规则,覆盖 VB.NET 修饰符、MATLAB 单返回值函数、Julia 的
import Base: name与 export 块、Svelte 组件导入、C 的static函数与_t类型、CSS 后代选择器与声明行、tox 风格的 INI、任意包的 Java 导入,以及预处理.S文件中的 GNU 汇编指令。 - 公开 API 没有变化。
Mantine
Section titled “Mantine”codeBlock.autoDetectUnknownLanguage随探测器1.1.0更新:以前因 C 与 C++、JavaScript 与 TypeScript、CSS 与其预处理器打平而保持纯文本的未标注代码块,现在会得到标签与语法高亮。包内其他内容没有变化。
- 引擎压测影响检查会忽略 engine 与 highlight 清单文件及 lockfile 依赖图中的类型声明包(
@types/*),以及既不属于六条压测腿、也不是 fuzz 套件的 engine 或 highlight 单元测试。其余测试与构建工具链的变动仍要求完整压测。 - 当 macOS 对成员仍在退出的进程组返回
EPERM时,Storybook 进程管理器会继续等待,因此本地pnpm preflight可以完整跑完 Storybook 构建、站点与 dev 检查。 - Storybook 新增 Integrations / Mantine / Language Detection 页面,包含可编辑的探测器 Playground 与 fence 语言名映射表。
全部 3,091 项单元测试通过(另有 1 项 todo),React(111 项)与 Vue(42 项)Storybook 套件也全部通过。上文的探测器数据来自其 evidence harness 在钉住 commit 的留出集语料上的测量结果。
本次发布没有运行引擎压测。check:soak-impact 判定需要压测,仅仅是因为影响检查本身有改动(scripts/soak/impact.mjs 及其测试)。自 3.2.0 以来,engine、core、React、Vue 与高亮插件的源码均无改动,任何压测腿执行的内容都没有变化,维护者据此批准本次发布作为例外。
3.2.0 — 代码语言探测器与 Mantine 同步语言检测
Section titled “3.2.0 — 代码语言探测器与 Mantine 同步语言检测”本次次版本将 engine、core、React、Mantine 和 Vue 同步升级至 3.2.0,其中 engine、core、React 与 Vue 仅做版本对齐。独立发布的高亮插件仍为 1.0.2。新增独立版本包 @ai-markdown/code-language-detector,首个版本为 1.0.0。Mantine 改用该包做语言自动检测,并移除了为旧检测方式提供 highlight.js 的选项。
代码语言探测器
Section titled “代码语言探测器”- 新包
@ai-markdown/code-language-detector1.0.0。 为未标注语言的代码块做启发式语言检测,面向大模型的流式输出设计。它没有任何运行时依赖,同时提供 ESM 与 CJS 构建。证据不足时返回language: null而不是猜测;在流式模式下不会在语言家族之间来回切换。 - API。
CodeLanguage枚举包含 42 种语言,取值为 Shiki id(如CodeLanguage.Rust = 'rust'、CodeLanguage.VisualBasic = 'vb')。detectLanguage(code)返回{ language, confidence, candidates, evidence }。StreamingLanguageDetector在代码块流式输入期间调用update(accumulatedText),在围栏闭合时调用finalize(text),另有reset()与current。同时导出DetectionCache。toShikiLanguage与toHighlightJsLanguage把检测结果映射为高亮器使用的名称;highlight.js 映射会改写objective-c、vb、asm、jsx与tsx,并把 HTML、Vue 与 Svelte 映射为xml。normalizeCodeLanguage(name)把名称、扩展名或高亮器别名解析为CodeLanguage,不对应 42 种语言中的任何一种时返回null。详见包 README。 - 围栏语言的高亮器名称。
normalizeHighlightJsLanguage(name)与normalizeShikiLanguage(name)把模型在代码围栏上书写的语言名称映射为高亮器使用的名称,忽略大小写和首尾空白。属于 42 种语言之一的名称经normalizeCodeLanguage和转换函数解析(objc变为objectivec或objective-c,vue在 highlight.js 下变为xml)。一张小表转换 42 种语言之外、两个高亮器拼写不同的常见名称(txt变为plaintext或text,console变为shell或shellsession,makefile变为makefile或make)。其他名称只转为小写,其余原样返回(haskell、jsonc)。返回的是名称,并不保证对应的 grammar 已加载或已注册。
Mantine
Section titled “Mantine”- 检测改由新包完成。
codeBlock.autoDetectUnknownLanguage现在使用@ai-markdown/code-language-detector,该包是@ai-markdown/react-mantine的常规依赖,不再需要 highlight.js 实例。检测是同步的,在渲染期间执行(包括服务端渲染),因此检测出的标签页标题直接出现在 SSR 标记中,代码块不会先显示“unknown”再原位升级。 - 流式策略。 流式期间,证据充分时才为代码块标注语言,之后只有明显增长才会重新检测。检测器不会降低已有的置信度,也不会在中途切换到另一个语言家族。重新生成(新文本不是在旧文本之后追加,包括长度相同的替换)会让检测从头开始;
streaming结束时确定最终结果。检测器放弃判断的代码块保持纯文本,标签为“unknown”;检测器永远不会覆盖显式声明的围栏语言。旧的倍增检测计划、highlightAuto、加载函数缓存与重试以及缺少实例时的警告均已移除。 - 新增
codeBlock.languageFormat。 渲染器无法得知CodeHighlightAdapterProvider中使用的是哪个适配器,因此由该字段说明语言以哪套名称交给高亮器:MantineLanguageFormat.HighlightJs('highlight-js',默认值)或MantineLanguageFormat.Shiki('shiki')。它既作用于检测出的语言,也作用于显式声明的围栏语言:高亮器收到的是小写名称经normalizeHighlightJsLanguage或normalizeShikiLanguage映射后的结果,因此使用 highlight.js 时```objc按objectivec高亮、```txt按plaintext高亮,使用 Shiki 时```Makefile按make高亮。标签页标题保留围栏上书写的语言(转换为小写),或检测出的语言本身的名称,因此使用 highlight.js 时,检测出的 Vue 组件标签为vue,按xml高亮;只有代码块没有语言时,标题才显示“unknown”。JSON 美化格式化与 Mermaid 渲染仍依据小写名称判断。无法识别的值回退为默认值。该枚举从包根入口导出。 - 次版本中的破坏性变更。 移除了
codeBlock.highlightJs、MantineHighlightJsLike与MantineHighlightJsSource两个类型,以及preloadMantineCodeAssets的options参数。preloadMantineCodeAssets()现在不接受参数,返回Promise<void>,只预加载 mermaid。这项移除经维护者决定随3.2.0发布,没有等到主版本。 - 迁移方法。 从
codeBlock分组和defineMantineBehaviors调用中删除highlightJs。如果使用 Mantine 的 Shiki 适配器,设置languageFormat: MantineLanguageFormat.Shiki,该设置同样影响显式声明了围栏语言的代码块;默认值已与 highlight.js 适配器匹配。调用preloadMantineCodeAssets()时不再传参。删除对上述两个类型的导入。 highlight.js仍是可选 peer,只有 Mantine 的 highlight.js 适配器需要它;本包从不导入它。
- 独立版本包统一登记在
scripts/release-packages.mjs(remark-mark-highlight、code-language-detector)。统一版本标签会先发布这些包,再依次发布 engine、core、React、Mantine 与 Vue。 code-language-detector-vX.Y.Z是有效的包标签。首次发布code-language-detector-v1.0.0通过FIRST_PUBLISH_NPM_TOKEN引导完成;此后必须在 npm 上为新包配置 Trusted Publisher。
全部 3,067 项单元测试通过(另有 1 项 todo),React(109 项)与 Vue(42 项)Storybook 套件、打包消费者、document-lifetime 与包含 Firefox 和 WebKit 的 Vue 浏览器检查、公开 API 快照、包导出检查,以及文档站构建和链接检查也全部通过。在钉住 commit 的 GitHub 语料上,检测器的 evidence harness 测得:单次检测覆盖率 84.3%、严格 precision 97.1%;流式检测没有出现跨语言家族翻转,最终判定正确率 94%。
本次发布没有运行引擎压测。check:soak-impact 判定需要压测,原因是 3.1.0 压测之后有三类改动:两项 LaTeX 预处理器单元测试放宽了计时上限、高亮插件的开发依赖 @types/node 升级,以及 lockfile 中的开发工具链条目(Mantine、sass、yaml、Playwright)。分别从上次压测所测的提交和本次候选构建,engine 与高亮插件的产物逐字节相同,变动的依赖也都不是引擎的运行时依赖。维护者据此批准本次发布作为例外。
3.1.0 — 评审修复、任务列表增量解析与适配器对齐
Section titled “3.1.0 — 评审修复、任务列表增量解析与适配器对齐”本次次版本将 engine、core、React、Mantine 和 Vue 同步升级至 3.1.0。独立发布的高亮插件仍为 1.0.2。本版本修复了对五个包全面评审的全部发现,为增量解析器加入显式的语法能力声明,并使 Vue 适配器在文档协调上与 React 对齐。
- 任务列表不再钉住增量边界。
- [x]复选框此前一直作为未解析的[x]引用候选保留,直到出现[x]:定义,因此任务列表之后的每一帧都是全量解析。新增的容器与段落跟踪器在段落不可逆关闭后(已确认的空行、fence 或数学开启符、html 块、无歧义的同级项或子列表),且 setext 下划线、GFM 表格分隔行、定义列表回溯都无法再触及该段落时,按精确源位置认证复选框;模型之外的结构一律保留 taint。- [x] done后接 400 个段落分 60 帧流式输入,从 1,005 毫秒且无一帧拼接降到 45 毫秒、59 帧拼接。 - 发布压测发现的认证形状。 空标记行(
-后接[x] a)一律不认证;根级缩进代码块之后,标记行按 micromark 的打断模式解析,12. [x] done在那里是段落文本,后到的定义会把[x]变成链接。两种形状都已作为拼接等价与语法用例钉住。 - 显式语法能力。
AdvanceOptions与FreezeBoundaryOptions新增可选的gfmTaskListItems和mathFlow,两者都进入 checkpoint 配置和 deps key。gfmTaskListItems启用上述认证;mathFlow: true声明有 remark-math,false声明没有,省略时按两种语法的保守并集扫描($$区域保留 fence 阻断,同时其中的行仍会扫描引用与 html)。core 的PipelineFrameOptions透传这两个字段;React 与 Vue 适配器都声明为 true,因为内置链始终包含 remark-gfm 和 remark-math。边界 oracle 测试证明未声明配置的冻结边界不会超过任一声明配置。 - 线性扫描。 定义标签的快速探测改为手写扫描,替换了标签体可跨行的正则(40,000 行
[a从 5.7 秒降到 45 毫秒)。\(/\[分隔符转换显式查找闭合符(40,000 个未闭合开启符从 0.6–1.3 秒降到 4 毫秒以内)。 - 后缀货币。
5$、1,000.50$、US$、A$ 5和5 $ now视为货币而非行内数学。此前用US$充当游离$的测试夹具改用lone $。 - 拼接守卫匹配完整标签名。
<col-md-6>、<td-cell>之类的自定义元素不再每帧强制全量解析,<header>不再命中<head回溯守卫。新增测试把守卫正则与扫描器名单钉在一起。 - 每帧开销。 状态中保留的拼接前缀缓存消除了每帧对全部顶层块的遍历:64,000 个块时每个追加 token 从 28.8 毫秒降到 4.1 毫秒。
- 两处拼接分歧 由新的 fuzz 轴(制表符缩进、前缀碰撞标签名)发现:parse5 特殊元素之上的
</span>等普通闭合标签现在按 parse5 的规则丢弃且该区域不可冻结;正文以未闭合 fence、数学或 html 块结尾的冻结脚注定义在回放时保持与全量解析相同的页脚位置。 - 公开面。
sanitizeCrossChunkUrl标记弃用,改用resolveCrossChunkReference;seal-release 计数器与扫描器名单移出生产包;tsup 开启 treeshake(ESM 从 230 KB 降到 225 KB)。hasLatexTrigger的早退和未闭合<code>/<pre>的字面内容保护作为既定行为写入文档,未改动。
Core 与 React
Section titled “Core 与 React”- 吞入后续兄弟节点的原始 HTML 块在 32 位摘要相同后再比较精确的被吞源文本,哈希碰撞不会再返回旧内容。
- 从注册表解析出的跨块链接和图片现在与块内元素一样经过
customComponents。 documentScopeCache在缺少WeakRef或FinalizationRegistry时回退为强引用;兼容性一节记录了该要求。urlTransform={null}的语义记录为等同默认转换。
enginePlugins与sanitizeSchema在 prop 边界做深比较稳定化,父组件传入内联字面量不再重建插件链或重新解析。- 顶层节点带源偏移 key,与 React 的块计划一致;切换
documentId只解析一次,不再出现旧注册表配新前缀的帧。 node、streaming、metadata只传给声明了它们的组件;以.或^开头的属性键在 DOM sink 检查前直接拒绝。AIMarkdownDocuments新增preserveOrphanReferences(默认true),与 React 一样覆盖每个块自身的 prop。
Mantine
Section titled “Mantine”- 发布包以
'use client'开头,dist 断言脚本会检查它。 mermaid.initialize读回站点配置后只合并必需的键,宿主的主题、字体和sandbox级别得以保留;渲染错误以文本显示原因;超过配置maxTextSize的图在解析前即报错。codeBlock.mermaidIntervalMs(默认 300)在流式期间节流图表渲染。- 通过
useSyncExternalStore读取计算后的配色,auto配合深色系统时首个客户端帧即为深色。 highlight.js变为可选 peer:语言自动检测使用codeBlock.highlightJs传入的实例或加载函数,字面量导入已移除。启用autoDetectUnknownLanguage的使用者必须提供它,否则检测保持关闭并给出一次警告。
全部 2,768 项单元测试通过,同时通过拼接 fuzz、Storybook 套件、打包消费者、document-lifetime 与 Vue 浏览器检查,以及包含 Firefox 和 WebKit 的完整 preflight。每项引擎改动都经第二个 agent 独立复核,其复现用例已成为常驻测试(taskListTaint、taskListMathCapability、mathCapabilityOracle、tagNameBoundary、tabIndentAxis、tagNamePrefixAxis)。
使用全新种子 202609700 的发布配置引擎压测在提交 22b05e3 上通过全部六个分支和 84/84 个分片,耗时 10,874 秒。此前对同一候选的两轮压测起到了作用:种子 202609405(fuzz)和 202609711(方向电池)发现了上述两种认证形状,oracle 分支越过了按 3.0.x 语料校准的 hazard 全盲文档上限(8.22% 对 8%)。该上限已按文档规定的方法重新推导:在 3.1.0 语料上做 12 轮各 4,000 次扫描,测得 5.79% 到 8.14%,上限移至 12%,并为 oracle 增加了按生成器族归因的读数。放行发布前,已对照打标签的提交完成证据校验。
3.0.2 — 跨块引用与流式光标修复
Section titled “3.0.2 — 跨块引用与流式光标修复”本次补丁将 engine、core、React、Mantine 和 Vue 同步升级至 3.0.2。独立发布的高亮插件仍为 1.0.2。
- 数学公式附近的引用: React 和 Vue 扫描跨块链接、脚注定义时,现已采用与渲染一致的数学语法。数学块之后定义的链接可以正常解析,公式内部类似脚注定义的文本也不会再生成无目标的引用。引擎扫描器新增可选的
math参数;不传参数时保留原有语法,并继续支持自定义解析回调。 - React 脚注: 协调模式下的脚注标记现在遵循
urlTransform和自定义sup、a组件配置,包括使用全局编号的引用。 - Vue 流式光标: 光标定位现在正确扣除容器边框,覆盖从左到右、从右到左以及等比例缩放的布局。
扫描器契约文档及其中文版本已同步,补充了可选数学语法的用法。
验证包括全部 2,140 项单元测试、完整发布 CI,以及 Node 20、22、24 的打包消费者测试。使用全新种子 202609150 的发布配置引擎压测通过全部六个分支和 84/84 个分片,耗时 10,334 秒。放行 npm 发布前,已对照 v3.0.2 标签对应的提交完成证据校验。
3.0.1 — 渲染与流式正确性
Section titled “3.0.1 — 渲染与流式正确性”
该补丁版本将 engine、core、React、Mantine 与 Vue 相关包同步发布为 3.0.1。独立版本发布的高亮插件保持为 1.0.2。该版本修正了流式数学公式、脚注协调、React 延迟水合以及 Vue 响应式失效问题,同时保持既有的相关包入口点不变。
基于全新随机种子 202739110 的完整发布配置引擎压测顺利通过全部六个测试分支以及 84/84 分片,耗时 10,338 秒。在整个测试活动期间受测源码保持不变。全部 2,127 项单元测试均顺利通过,并完成了构建、类型与公开 API 检查;完整本地预检还覆盖了打包使用端测试、Storybook、文档生命周期测试以及 Chromium、Firefox 和 WebKit 环境下的 Vue 浏览器测试。发布测试证据在正式发布前对照最终版本化候选版本完成验证。
内置 LaTeX 预处理器、共享 remark 插件链、原生 HTML 步骤以及增量解析标识元组中的修正改变了受影响输入的渲染字节。
- 单独残留的单个
$(如quoted in US$ per unit)不再导致整篇文档后续的所有|均被转义为\vert{}。行内数学公式仅在单行内生效,因此已结束行上的未成对$保持为字面文本。未闭合定界符扫描现在具备类型感知能力:位于$$块内部的$x$属于正文内容而非闭合符,并且内部包含$x$且仍在流式传输中的块级数学公式会与其他未闭合块一样被正确截断。 - 行内的
$$(如It costs $$100 per month.)被限制在所在段落范围内,与 remark-math 的行为保持一致。它不再与后续块级数学公式的起始符错误配对,从而使后续块及其后所有内容得以保留;真正处于开启状态的尾部块仍会被截断。 - 词法分析器对 HTML 标签的匹配要求遵循 CommonMark 属性语法,且绝不跨越空行。正文中的
a<b不再遮蔽其后的数学公式,并且包含大量未闭合<b的文档可在多项式线性时间内完成扫描(8000 行扫描耗时从 315 ms 降低至 4 ms)。带引号的属性值中不得包含>,与既有规则保持一致。 - 超长单行上的货币符号转义耗时恢复为线性表现(包含 32k 个
$的 240 KB 单行:耗时从 2.7 秒下降至 11 毫秒),输出字节保持完全一致。 removeComments仅移除完全由注释构成的 HTML 节点:内部包含注释的<details>块、<!-- note --> visible text以及<div title="<!-- keep -->">均保留其正文内容与属性,因为 rehype-raw 会对嵌入的注释进行分词,而安全清洗器会负责丢弃它们。该插件绝不修改 HTML 节点的 value 值,因此节点位置信息对于拼接路径保持精确。仅包含注释的块仍会被正常移除。引擎不再依赖外部的remark-remove-comments。smartypants插件在中日韩(CJK)文本旁遇到引号时,会在 SmartyPants 执行前预先完成配对,从而使中文"引号"中文渲染为中文 “引号” 中文,中文'引号'中文渲染为中文‘引号’中文,而不再错误渲染为两个闭合引号。配对状态在块级作用域内跨行内标记维护,因此中文'*引号*'中文以及跨软换行闭合的引号均能正确配对。没有 CJK 邻居的引号仍由 SmartyPants 处理(it's、'90s、"quoted" text保持不变)。插件链顺序保持为removeComments、smartypants、pangu;此前曾尝试将 pangu 移至最前,虽然解决了双引号问题,但会将每个直引号独立填充空格(导致出现中文 ’ 引号 ’ 中文)。- 在列表项内部开启的
$$块在未闭合状态下不再吞噬列表之后的所有文档内容:此前- Item\n\n $$\n x\n\nAfter the list.会被错误截断为- Item。现在列表项会从起始符上方的行中读取(依据其标记与内容缩进),且列表项的结束标志着该数学块的结束,与 remark-math 行为一致;缩进 1 至 3 个空格且正文与闭合符未缩进的顶层代码块不受影响。增量预处理器会传递其已冻结的前缀,使两个入口点读取完全相同的行,且竖线转义流程拒绝进行跨越列表项末尾的配对。 - 阶段 A 会移除文档开头的所有字节顺序标记(BOM),而不再仅移除第一个。此前由于此处剥离了一个且 micromark 又丢弃了一个,导致
\uFEFF\uFEFF# Heading被渲染为标题且\uFEFF\uFEFF[x]: /url被解析为定义(两者的原始解析原本均应为普通段落),而三个 BOM 则会在正文中残留一个。对连续 BOM 进行规范化属于显式的预处理契约,而非针对多 BOM 输入的原始 micromark 等价性;出现在其他位置的 U+FEFF 保持为普通文本。定义标签扫描器在直接使用包含原始前导 BOM 的输入驱动时,现在采用与collectDefLabels完全等价的全量解析路径,而不再自行剥离一个 BOM 并让解析器剥离另一个。 - 嵌套深度超过引擎上限的原生 HTML 不再导致适配器子树崩溃。引擎的原生 HTML 处理步骤现在在语法树重新解析完成后立即使用迭代遍历测量元素嵌套深度,并在深度超过
RAW_HTML_MAX_DEPTH(256,设置为测得的最浅溢出深度的四分之一:Vue 在 Chromium 下挂载约 1,000 个嵌套<div>时溢出,Firefox 与 WebKit 更高;scripts/measure-raw-depth.mjs可复现此表格)时直接拒绝该数据帧,随后才让 sanitize、KaTeX、规划器或渲染器递归进入其中。作为安全防护网,该步骤自身的调用栈耗尽(在 V8、JavaScriptCore 与 Firefox 中通过错误名称与消息识别,如InternalError: too much recursion)也会以相同方式报告。两者均体现为EngineRawHtmlDepthError,这是@ai-markdown/engine中有意新增的公开错误类型,配套提供下文所述的可选嵌套引用元数据;守卫本身保持为内部逻辑。正常的嵌套(如 64 层的列表、引用块或 div)不受任何影响。共享流水线会话将该异常帧降级渲染为一个经过转义的纯文本段落,并在下一帧恢复正常解析。仅此特定错误会触发降级:由宿主传入的 remark/rehype 插件或处理器抛出的异常,以及非该栈溢出的任何RangeError,仍会像以往一样从parse中向外抛出。Vue 浏览器测试套件覆盖了 Chromium、Firefox 与 WebKit 环境下的降级帧与恢复过程。 - 在各帧之间切换定义列表插件会触发重新解析整篇文档,而不是将新尾部与在另一种语法配置下解析的旧树进行拼接:
defListEnabled现已纳入增量引擎的依赖项 key(边界扫描器的检查点此前已拒绝跨切换恢复,但保留的语法树此前未做处理)。 - 开发构建中会报告一项引擎不变式检查:片段自身同时也定义了跨片段幻影标签。幻影标签集合被排除在依赖项 key 之外的前提是幻影不会在本地被定义;该检查通过与边界扫描器确认的定义进行集合交集比对实现,在生产环境中不消耗任何计算资源。
- 原始待闭合辅助函数能够识别单一的文档前导 BOM,与 micromark 保持一致,从而避免为已完成的代码块错误添加多余的尾部闭合符。
- 竖线转义流程采用与未闭合定界符扫描相同的有序流式块跨度。开放流式块内同行的
$$…$$不再导致后续正文表格行在全量与增量预处理中产生不一致的转义结果。通过减少缩进闭合的列表内部数学公式遵循相同的规则。
对片段如何向共享注册表发布其脚注与链接定义以及如何组装聚合页脚进行了四处修正。这些改动仅影响在 <AIMarkdownDocuments> 下渲染的文档;单机独立渲染输出保持不变。
- 包含有效百分号转义的脚注标签(如
[^a%41])能够在聚合页脚中保留其正文内容。此前提取逻辑对页脚的<li id>锚点片段进行了解码(将a%41转为aA),而注册表以源码标识符为 key 存储定义,导致二者无法匹配而使页脚渲染出空列表项。现在双方统一使用编码后的片段footnoteSafeId(sourceIdentifier)。sourceIdFromFootnoteLiId仍提供解码功能以供流式光标进行 DOM 查询。 - 在脚注正文内部编写的链接定义或脚注定义现在能够正常贡献到注册表中。此前标签扫描器已提取了该标签,导致同级片段注入了该幻影标签却没有任何片段能够解析它。差异测试现在要求扫描器、全量收集器与贡献提取器在每个流式前缀上必须报告完全相同的标签集合。
- 仅从另一个脚注正文中引用的脚注会以独立编号出现在聚合页脚中(位于所有流式引用之后,按页脚顺序排列)。嵌套引用通过新增的可选字段
RefRecord.nestedIn进行贡献;它们参与globalNumber计算,但不参与getRefsForLabel或出现范围计算,因此不会产生指向不存在行内上标标记 ID 的无效反向链接。buildAggregateTree按全局编号对条目进行排序。引擎 API 快照增加了该可选字段。 - 脚注正文仅从合成的页脚中提取,通过共享的
isFootnoteSection断言函数识别(检查标签、属性且不具备源码位置),并按section > ol > li读取(每个 ID 取首项)。作者手动编写的原生<section data-footnotes>,或脚注定义内部被 HTML 解析提升到真实条目旁边的原生<li id="fn-…">,均不再会错误替换定义正文。
- 规划器复用 Markdown 节点时,在同一源码行上共享的多个图片均会完整保留渲染出的兄弟节点,避免了开发模式下的断言失败以及复用输出不完整的问题。
- 跨片段哈希链接在重新计算基准路径时,空的
#链接保持为空锚点片段。 - 在各自独立的
<Suspense>边界内包裹的跨片段协同组件(具有共享documentId的<AIMarkdownDocuments>),当某个边界在同级节点完成注册后才完成水合时,不再触发可恢复的“Hydration failed”水合失败错误:水合渲染阶段不读取任何注册表状态,因此与服务端的字面[^a]/[link][x]完全吻合,注册表在水合完成后立即按原逻辑进行解析。
- 注册表通知不再导致
AIMarkdownDocuments树中的每个片段都执行重新解析。对 N 个片段中的某一个片段进行单次追加仅会解析该片段一次;其他片段仅在其正在等待的标签出现时才进行解析,并仅在所展示的脚注编号或链接目标发生变化时才触发重渲染(实测:对 5 个片段中的最后一个片段进行追加此前会触发 6 次解析,现在仅为 1 次)。Vue 片段在每一帧上也无需再运行块规划器:适配器中没有任何逻辑需要读取其规划。独立的客户端挂载将其首帧解析次数从两次减少到一次:增量路径在 setup 时根据运行环境确定(与 React 一致),而无需等待挂载完成。 - Vue 在解析引用时采用无冲突的 URL/标题快照,在非安全上下文中无需
crypto.randomUUID即可正常运行,并避免将内部协调与节奏属性渲染到 DOM 上。光标尾部标记仅在光标实际存在时输出。
Mantine 与发布工具链
Section titled “Mantine 与发布工具链”- 按需加载语法的代码高亮器可在嵌套的 Mantine Provider 中正常使用。Mermaid 头部控件内置了行内 SVG 图标。
- 发布验证使用只读权限;仅发布任务本身获得写权限与 OIDC 权限。发布恢复流程会拒绝无法为目标发布标签生成溯源信息的 workflow 引用。
- 压测影响分析按任务对比工作流的 Node 版本,发布版本更新仅重写当前的安装指南,而不修改历史发布记录。
- 打包的基于 React 衍生的代码包含了其上游的 MIT 开源许可声明。
已知性能限制
Section titled “已知性能限制”超大型 GFM 表格保留了上游二次方复杂度的解析开销。任务列表复选框可能会保守地阻止增量前缀复用;渲染结果保持正确。详情请参阅流式输出与性能。
3.0.0 — 稳定版本与最终候选版本
Section titled “3.0.0 — 稳定版本与最终候选版本”
稳定统一版本发布将 @ai-markdown/engine、@ai-markdown/core、@ai-markdown/react、@ai-markdown/react-mantine 与 @ai-markdown/vue 同步发布为 3.0.0。应用程序直接安装 React 或 Vue;Mantine 集成仍仅限 React。独立的高亮插件保持为 1.0.2。安装说明与旧版作用域映射请参阅快速开始与包迁移指南。
公开的 core 与 engine 契约、React 根模块/插件、Mantine 与 Vue 类型声明均纳入 API 快照校验范围。公开契约自此稳定版本线起遵循语义化版本规范。稳定版 Mantine 声明了 ^3.0.0 的 React 适配器对等依赖;core 和 engine 保持为严格的统一版本发布依赖。ESM/CJS、开发/生产入口以及公开样式表均保留了 RC 版本的 API 形态。
Node 使用端要求 ^20.19.0 || >=22.12.0,与 CJS 依赖项加载 ESM 的运行时要求相符。打包使用端测试在声明的最低版本以及 Node 24 上均通过验证。Vue 功能验收覆盖 Chromium、Firefox 与 WebKit;强制 GC 生命周期检查仍仅在 Chromium 下运行。Nuxt、KeepAlive 与 Suspense 的深度集成暂未纳入宣传的 Vue 覆盖范围。
针对干净提交 “chore(release): prepare 3.0.0-rc.1 compatibility gates” 的全新 RC 引擎测试轮次在随机种子 202689100 下通过了全部六个测试分支与 84/84 分片,耗时 10,917 秒。随后的 ANSI 日志解析修复(“fix(soak): normalize terminal colors before checking verdicts”)使聚合器能够读取带颜色的 Vitest 判定结果,同时保留了对失败判定的拒绝机制以及所有结构化测试证据检查。维护者通过人工发布审查明确授权为纯解析器修复复用该祖先测试轮次的证据;保守的自动化证据覆盖检查未被绕过或误报为通过。发布门禁与记录请参阅 3.0 发布验收记录。
3.0.0-rc.1
Section titled “3.0.0-rc.1”五个相关包的统一发布版本线晋级为候选发布版(RC),其运行时 API 与 beta.2 保持一致。Node 支持范围修正为 ^20.19.0 || >=22.12.0:较早的 Node 20 版本无法从 CJS 入口加载 ESM 依赖项。独立的高亮插件在 1.0.2 中同步进行了元数据修正。
发布门禁现在包含 React、React 插件与 Mantine 类型声明快照,覆盖已声明 Node 最低版本及 Node 24 下的打包使用端测试,以及在 Firefox、WebKit 和 Chromium 下的 Vue 浏览器功能测试套件。自动生成的 core 贡献/聚合正确性测试获得了 30 秒超时时间,以容忍 CI 启动开销并保留其完整计算工作量。
RC 发布工作流通过了自动化验证与人工压测审查,并通过 OIDC 发布了全部六个包版本。注册表检查确认了预期的 dist-tags、tarball 哈希与溯源信息。全新下载的 npm 包顺利通过 ESM/CJS、React/Mantine/Vue SSR、CSS、类型声明以及 Vue 3.5.0 使用端检查。
查找与你的升级相关的版本线
Section titled “查找与你的升级相关的版本线”| 版本线 | 主要变更 | 当前参考指南 |
|---|---|---|
| 2.12–2.13 | 引用保真、保留源码结构的 JSON、等待插槽、代码展示合并、选择性通知 | 跨片段协调、Mantine README |
| 2.10–2.11 | 截止时间节奏控制与 LaTeX 软原子处理;私有标签溯源 | 平滑流式输出、内容预处理器 |
| 2.5–2.9 | 语法覆盖、扫描器正确性、参考实现与压测改进 | 架构设计全景、压测覆盖 |
| 2.4 | 全仓库正确性审查、SSR 与缓存修复、懒加载代码资产 | URL 策略、流式性能 |
| 2.0–2.3 | 平铺配置、光标与平滑呈现表面、引擎包拆分 | 迁移指南、光标 |
| 1.2–1.8 | 初始集成、跨片段协调、自定义扩展、增量解析推进 | 下文历史记录;具体实现请使用当前指南 |
补丁版本标题指明修复或维护项;次版本标题引入新功能或较广泛的兼容改动。确切的渲染字节与默认视觉数值不属于语义化版本的绝对承诺。
3.0.0 预发布版本 — 框架适配器与共享契约
Section titled “3.0.0 预发布版本 — 框架适配器与共享契约”
3.0.0-beta.2 — Vue 3.5 适配器与显式共享 API
Section titled “3.0.0-beta.2 — Vue 3.5 适配器与显式共享 API”该 Beta 版本引入了适用于 Vue 3.5 及更高版本的 @ai-markdown/vue。使用 @beta 标签安装。Engine、core、React、React Mantine 与 Vue 现已同步在 3.0.0-beta.2 发布版本中;独立版本发布的高亮插件保持在 1.0.1。React 继续面向 19.x。显式的 beta npm 标签针对每个统一发布包进行校验。若 npm 在 Vue 首次发布期间分配了 latest,根据维护者决定保留初始的 latest → 3.0.0-beta.2 映射;该例外不适用于后续预发布版本或其他包。
Vue 适配器包含 Markdown 渲染、经过安全清洗的跨片段引用与脚注、SSR/水合、自定义组件与插槽、带有文档轮流呈现的平滑流式输出以及流式光标。协调脚注标记遵循 URL 转换与自定义 sup/锚点渲染。浏览器验证覆盖定义变更、文档隔离、源替换、取消以及卸载清理。Nuxt、KeepAlive、Suspense 以及非 Chromium 浏览器的集成暂未进行验证。
Core 会话与规划器现在具有命名的公开契约,engine 根目录导出已显式化。公开的 isEnginePlugin 守卫取代了对内部插件阶段元数据的依赖。beta.1 的使用者应查阅 API 契约 并更新对已从公共根目录移除的辅助函数的导入。克隆渲染拥有的 URL 元数据可防止适配器修改与其他使用方共享的解析器或注册表树。
Core 拥有独立的构建/类型检查/测试门禁,包含 91 项测试,涵盖针对流水线失效、贡献重建、聚合所有权与协调器清理的固定种子状态序列。Vue Chromium 验证门禁包含 24 个文档生命周期用例和 288 次追加更新,验证订阅释放,并检查文档注册表与协调器在其 Provider 保持挂载时能否被正常垃圾回收。该检查曾成功排查出一项偶发的注册表残留缺陷。
发布压测现已改为基于改动影响触发。PR 检查展示累积候选版本是否需要引擎压测;具有引擎影响的发布在 npm 发布前需要本地测试证据以及维护者的 soak-approval 确认。Core 与适配器检查仍须独立完成。触发规则与测试证据验证详见压测覆盖。
候选版本 “fix: enforce release soak baseline and detect matrix runtime changes” 通过了全部九项 CI 检查以及全新的六阶段发布配置压测:84/84 分片,随机种子基准 202669090,干净的工作区状态,且 repositoryChanged: false。本地发布验证器对照候选版本接受了完整的测试证据。整个测试活动耗时约 2 小时 52 分钟。
3.0.0-beta.1 — ai-markdown 作用域与公共共享 Core
Section titled “3.0.0-beta.1 — ai-markdown 作用域与公共共享 Core”新作用域下的首个 Beta 版本将代码仓库迁移到 ai-markdown/ai-markdown,并在同一统一版本发布线上发布 @ai-markdown/engine、@ai-markdown/core、@ai-markdown/react 与 @ai-markdown/react-mantine。使用 @beta 安装框架包;独立的 @ai-markdown/remark-mark-highlight 插件保留其 1.x 版本。
旧版 React core 更名为 @ai-markdown/react。原先私有的 runtime 演进为新的共享 @ai-markdown/core,与 engine 一同作为外部精确版本依赖项。React 组件/Hook 名称、插件与排版子路径、Mantine 样式表行为、ESM/CJS 以及开发/生产入口均得以保留。React 对等依赖限定在经过验证的 19.x 主版本范围内。
公开的 core 导出显式列出。注册表注册与发布通过 RegistryController 暴露;贡献会话接受最小写入能力。平滑协调器返回记录的公开接口。内部注册表容器与场景测试装置不再作为公开 engine 导出。Beta 版本的适配器 API 在 3.0.0 稳定版之前可能继续演进。
Vue 保持为私有的生命周期/SSR 准备原型。完整的 Vue 渲染器与专用文档站点属于后续里程碑。新旧相关包路径映射详见迁移指南,相关包职责划分详见架构设计全景。
完整预检通过了 152 个测试文件 / 1,982 项测试,外加 Chromium 并发文档生命周期回归测试与外部打包使用端检查(ESM/CJS、开发条件、SSR、CSS/插件路径及 TypeScript 类型声明)。全部六个 GitHub CI 任务针对候选版本均顺利通过。其全新的六阶段发布压测通过了 84/84 分片,采用种子基准 202659090,repositoryChanged: false 且候选状态干净。运行 ID 为 new-scope-3.0.0-beta.1-20260908T223231Z-9dae734-1278142。
2.14.x — 最终 ai-react-markdown 发布版本线
Section titled “2.14.x — 最终 ai-react-markdown 发布版本线”
2.14.1 — 释放废弃文档作用域与共享准备决策
Section titled “2.14.1 — 释放废弃文档作用域与共享准备决策”- 修复在 React 注册前放弃渲染后,由长生命周期包装层保留文档注册表与平滑协调器的问题。弱引用作用域缓存保留了活跃使用方的身份,同时允许回收未使用的内存分配;显式的最终释放清理机制依然保留。
- 增加针对 Chromium 的真实回归测试,覆盖 StrictMode、Suspense/transition 中止、同文档兄弟节点标识、活跃所有权与卸载回收。该测试在本地预检、浏览器 CI 以及发布工作流中运行。
- 将幻影目标推导、处理器/正文提取策略以及贡献失效 key 提取到私有运行时中。两条 React 渲染路径均使用这些共享决策;公开的 React/Mantine 配置与类型保持兼容。
- 增加私有的 Vue 3 生命周期原型,验证双片段定义共享、响应式更新、文档切换、SSR 准备与清理。Vue 保持在旧版发布包之外;该实验不构成完整的 Vue 渲染器或公开的新作用域版本。
- 更新生命周期文档,记录共享的准备契约以及剩余的 DOM/水合工作。
验证结果:完整本地预检通过 152 个测试文件 / 1,982 项测试,外加显式的 Chromium 垃圾回收回归测试。公开的 .d.ts 与 .d.cts 入口类型声明与 2.14.0 保持逐字节完全一致。在干净的源码提交 “fix: release abandoned document scopes and share preparation for v2.14.1” 上,全新的六阶段发布压测通过了全部 84 个分片,采用随机种子基准 202649080;最终报告记录 repositoryChanged: false,聚合报告判定为 PASS。运行 ID 为 legacy-patch-2.14.1-20260908T114553Z-5da773e-920560。
2.14.0 — ai-markdown 迁移前的共享运行时提取
Section titled “2.14.0 — ai-markdown 迁移前的共享运行时提取”- 确立在
ai-react-markdown作用域下的最后一个计划内旧版发布。既有的 React/Mantine 导入、样式表路径与公开配置保持兼容;新的组织/作用域迁移随后独立展开。 - 将框架中立的流水线会话、块规划与指纹、已提交贡献发布、聚合脚注 HAST、源码尾部分类以及平滑展现协调逻辑提取到私有运行时工作区中。
- 将 React 组件、Context、生命周期 Hook、节点缓存以及 DOM 光标处理保留在现有的 core 适配器中。将运行时打包到 core 中;保持 engine 作为外部的精确版本依赖项,并在分发构建产物中拒绝私有运行时导入。
- 增加一个无界面的无头使用端,直接使用真实的生产与开发环境 ESM 及 CJS 入口进行测试,无需引入 UI 框架,并配合会话回退/重置、发布时机与聚合不可变性检查。
- 修正引擎对仅提供 ESM 默认导出的插件以及仅支持 import 的
remend入口的 CommonJS 处理逻辑。 - 在迁移指南中记录提取出的契约、规划的新包映射、剩余的第二框架验证以及专用文档站点范围。
验证结果:完整本地预检通过 149 个测试文件 / 1,974 项测试,包含浏览器 Stories、类型声明/相关包检查以及生产/开发运行时分发产物。所有六项 CI 检查均在已验证的源码提交上顺利通过。隔离的 tarball 使用端在未安装私有运行时的情况下通过了四个 ESM/CJS 服务端渲染对等用例;发布的入口声明(.d.ts 与 .d.cts)与 2.13.3 保持逐字节完全一致。基于全新种子的六阶段发布压测在 2 小时 52 分钟内通过了全部 84 个分片(提交 “test: configure runtime package test discovery”,种子基准 202629080)。
2.13.x — 减少流式重复计算
Section titled “2.13.x — 减少流式重复计算”
2.13.3 — 文档与源码实现保持对齐
Section titled “2.13.3 — 文档与源码实现保持对齐”- 重写开发者指南与相关包 README,更深入阐述渲染阶段、自定义扩展契约、默认值与实现边界。
- 提供完整的流式对话示例,覆盖增量 SSE 分帧、UTF-8 解码、请求取消、显式完成处理与错误处理。
- 厘清跨片段引用生命周期与排序、平滑流完成判定、URL 安全清洗、中日韩排版解析以及保留源码结构的 Mantine 代码展示。
- 修正过时的 API 示例、基准测试解读、发布历史陈述以及 Markdown 格式,同时完整保留历史测量数据与验证记录。
- 将独立高亮插件的更新 README 发布为
@ai-react-markdown/remark-mark-highlight@1.0.1。本版本无运行时源码改动。
验证结果:完整本地预检通过 147 个测试文件 / 1,963 项测试,包括浏览器 Stories、打包与类型检查。文档审查还检查了关键 TypeScript 示例、27 个 SSE/协议/路由用例以及 306 个本地链接与锚点。
2.13.2 — 流式修复与依赖项维护
Section titled “2.13.2 — 流式修复与依赖项维护”- 将可选的流式修复预处理器升级至 remend 1.3.1,包含 Safari 兼容性、强调/数学公式处理以及更快的不完整代码块扫描。
- 更新 CJK 插件,同时保留其纯解析入口点;更新 Mermaid 与 Mantine 集成依赖。
2.13.1 — 修正不确定块边界处的引用处理
Section titled “2.13.1 — 修正不确定块边界处的引用处理”- 保证当定义在混合表格、原生 HTML 与数学公式内容之后到达时,流式引用链接与脚注仍能正确解析。不确定块中符合定义特征的文本不再会过早冻结先前未解析的引用。
2.13.0 — 合并高亮更新与选择性引用更新
Section titled “2.13.0 — 合并高亮更新与选择性引用更新”- 使用
codeBlock.highlightIntervalMs(默认 50 ms,设为 0 禁用)合并常规流式代码展示更新;保留立即呈现的最终帧以及对最新源文本的复制能力。 - 通过标签订阅路由占位符通知,包括间接脚注重新编号与出现频次变动。
- 复用已保留的引用前缀规划,同时将完整文档上下文带入尾部规划。
- 分离冻结检查点状态、行语法与行转换,并分离拼接注入、坐标系、HTML 守卫与接缝对齐逻辑。保留现有入口点与转换逻辑主体。
2.12.x — 内容保真与减少流式重复计算
Section titled “2.12.x — 内容保真与减少流式重复计算”
2.12.0 — 引用保真、显式队列等待与流式效率
Section titled “2.12.0 — 引用保真、显式队列等待与流式效率”跨片段引用现在完整保留最终链接/图片的策略。 解析后的引用遵循最终元素的安全清洗标签、属性、协议与祖先限制。哈希目标在 URL 回调执行前使用文档前缀,带转义或字符引用的标签保留单独的查询标识符。全量、折叠与快捷引用均支持跨片段正常工作,而无需使用解码后的展示文本作为注册表 key。
代码展示与复制完整保留源码结构。 JSON 格式化保留数字词法单元、重复 key 与键名顺序,避免通过 JavaScript Number 进行往返解析导致数值失真。复制按钮复制原始代码文本。新增可选的 codeBlock.formatJson 与 codeBlock.expandNestedJson 标记,默认均为 true;可关闭嵌套展开以仅进行基础格式化,或关闭格式化以完全展示原始代码。包含嵌套标记、同级文本或额外属性的原生 pre/code 结构保持其正常渲染。非追加的代码替换操作(包括相同长度的代码替换)同样会重新启动语言自动检测。
预挂载的空片段可以显式等待输入到达。 从初次挂载起等待请求期间,在 AIMarkdownSmoothStream 上设置 smoothWaiting,或在 useDocumentSmoothStream 上设置 waiting。当流式开始或空结果完成时清除该标记。默认值仍为 false:非流式的空片段属于已完成的结果。完成状态依然具备粘滞性,documentIndex 不会重新排列平滑呈现轮次。
在不改变节奏法则的前提下减少重复计算。 注册表查询共享基于版本懒计算的索引;引用占位符按需选取其渲染所需的数值。Core 复用增量引擎保留的无上下文前缀规划,而引用与原生 HTML 区域保持保守规划。定义与 JSON 完整性扫描维护追加状态。Mermaid 对初始化、解析与渲染进行串行化处理,同时每个实例仅保留最新的待处理任务。平滑控制器通过游标使用其字素队列,而无需在每一帧复制剩余的积压数据。全局注册表通知、顶层规划遍历与常规代码高亮仍有进一步优化空间;本版本不声称实现了完全仅计算尾部的渲染路径。
验证结果:完整预检通过 143 个测试文件 / 1,905 项测试,包括 Chromium Stories。标准的全新随机种子六阶段发布压测顺利通过全部 84 个分片,运行期间源码无任何变动。
2.11.x — 公式内部的标签仍属于同一公式
Section titled “2.11.x — 公式内部的标签仍属于同一公式”
2.11.0 — 软原子机制与引擎自身标签的凭据校验
Section titled “2.11.0 — 软原子机制与引擎自身标签的凭据校验”此前 $$ a <br> b $$ 会被错误渲染为 <br> b $$。 这不仅发生在流式传输中,在已完成的静态文档中同样如此。任意行包含 <br> 的多行展示型公式块会同时丢失头部与尾部;表格单元格公式中的成对标签(如 | $a <b>x</b> b$ |)会将该行末尾的竖线重写为 \vert{},从而破坏整行表格。运行时不报任何错误。测试语料库自 2.10.1 以来一直将行内公式用例($x <br> y$)作为已知缺陷记录;而块级展示公式的问题是在编写修复计划时发现的。
其根本原因在于单一词法分析器使用单一字段处理了两种不同的工作。splitByProtectedRegions 将每一个受保护区域(围栏代码块、行内代码跨度、<code>…</code> 区域以及白名单中的每个 HTML 标签)标记为同一类对象,而转换链是在各保护区域之间相互隔离的文本片段上独立运行的。这种处理对代码是正确的:行内代码跨度既需要遮罩(mask)其字节(绝不对其重写),又需要作为被分析文本的定界符(delimiter)(代码跨度之前的 $ 不能与之后的 $ 配对)。但行内标签只需要前者。将其视为定界边界会导致 $$ a 与 b $$ 被切断为两段文本,每段均包含一个未闭合的 $$,随后流式保护逻辑从各自的定界符处分别执行了截断。
2.11.0 将这两项职责解耦。词法分析器的分段现在改为可辨识联合类型 —— text、code、literal、multilineTag 与 tag,processSlice 按三种方式进行分类:
| 分类 | 包含成员 | 对被分析文本的影响 |
|---|---|---|
| 硬边界(hard boundary) | 围栏代码、行内代码跨度、字面元素、跨越换行符的标签 | 保持不变:终止被分析文本。将代码作为可遮罩原子曾两次实测证明有误,保持作为硬边界 |
| 软原子(soft atom) | 所有其他单行内的标签 | 在文本中替换为单个私有 Unicode 码元,在插件链执行完毕后按顺序还原;标签自身的字节内容绝不被重写 |
| 元素作用域(element scope) | 在同一次运行的同一行上成对出现的软开启标签与闭合标签 | 内部文本作为独立的一次运行递归处理;处理完毕后的元素在外部视为单一原子,因此 <span>$</span>100 仍能隔离其内部的 $ |
作用域配对仅支持栈顶配对(不对 <b><i></b></i> 这类交叉错位进行修复),遵循共享的 \n / \r\n / \r 换行规则在行尾停止,并设定最大深度为 8,被抑制的开启标签保留在栈中以防止同名闭合标签错误闭合外层标签。遮罩码元是根据公开调用的完整输入进行选取的(绝非按片段选取),这是因为增量包装器会分别处理冻结候选区域与尾部,而无状态路径面对整篇文档,若仅在其并集上耗尽字符表会导致两个入口点产生分歧。当全部 6400 个私有码位均被占用,或还原过程检测到不变式被破坏时,调用会针对整篇源文本回退到旧版逻辑,且增量谱系在非追加重置之前保持在该模式下 —— 在同一次调用内保证每帧仍严格等价于 preprocessLaTeX(full)。
输出变更均已确认并批准。 包含标签的公式现在会带着内部标签完整传递给 KaTeX,并渲染为乱码数学符号(< 与 > 作为关系运算符)而不是被静默删除 —— 维护者的决策依据:显示乱码优于静默吞噬内容。对未闭合 $$ 的流式截断现在覆盖整块公式,而不再泄漏标签及其后的文本。紧随同行标签或代码跨度之后的 $$ 不再开启公式流(原本就不应开启)。同行美元符号配对、\[ … \] 转换以及未闭合尾部竖线转义现在均能穿透标签生效。单独的 \r 在截断扫描中被识别为换行符,与文件中的其他所有扫描器保持一致。$5 <b>and</b> $6 表现不变。对于既不包含标签也不包含代码边界的输入,输出保持逐字节完全一致。
上述最后一句是严格的自动化门禁,并非口头承诺:latexSoftAtoms.differential.test.ts 在共享测试装置、六个提交的语料库文档以及 4000 个带种子的模糊测试样本上,将旧版实现(在 processSlice 内部原样保留)与新版实现进行对比,将每一处差异分类到具名类别中,遇到任何未分类的变更或出现在“无标签/无边界”类别中的任何变动直接报错失败。模糊测试字符表补充了此前缺失的形态 —— 同行成对标签、公式内部标签、<span>$</span>100 用法、私有使用码元、CRLF 换行 —— 确保入口点等价性模糊测试无法通过虚无断言蒙混过关。专用的报告脚本(latexSoftAtoms.evidence.ts,60 × 400)记录各类别统计以及两个分支的非幂等率。
引擎的占位符标签不再能通过原始 HTML 进行伪造。 footnote-sup、cross-chunk-link 与 cross-chunk-image 在安全清洗 Schema 中被允许放行,以便跨片段处理器的输出能够传递给 React 占位符 —— 但这也导致手动编写的 <footnote-sup label="a"> 同样能到达占位符并携带 label,从而夺取页脚反向链接所指向的 fnref-a 锚点并污染块依赖集合。现在通过两层机制区分真实标签与伪造标签:
- 属性名称层:
hast-util-raw在不经过分词器的情况下将现有元素节点重新发射为解析器 Token,因此驼峰命名的engineProvenance能够得以保留,而手动编写的属性会被转为小写因而不可能产生该属性(针对已安装的 fork 进行了固定测试)。 - 属性值层:基于 Web Crypto 生成每实例唯一的 128 位凭据,由处理器打上标记,并在
rehypeRaw与rehypeSanitize之间的rehypeVerifyEngineTags中进行校验;真实的实例在通过校验后剥离该属性并放行,所有其他实例则像未经许可的元素一样被安全清洗器解构内容。该凭据绝不进入 DOM、缓存或日志。
在缺乏 Web Crypto 的环境下,该属性值具备唯一性但无保密性,名称校验层仍保持生效 —— 单机独立渲染不受影响,伪造的实例仍被解构,开发模式下输出单次诊断日志 —— 并且生产环境通过代码执行证明其无额外开销(assert-prod-diagnostic-inert.mjs),而非仅凭字符串缺席来推断,因为 core 的构建明确不做 treeshake。
保留公开调用签名。buildCoreRehypePlugins(schema, prefix) 默认返回不包含校验器的现有插件链;若在手动组装流水线中启用校验,需传入第三个参数 { provenance } 并且在 remarkRehypeOptions(CrossChunkHandlerOptions.provenance)中传入完全相同的字符串。官方内置的渲染器始终默认启用该校验。
2.10.x — 定界符不再删除后续内容
Section titled “2.10.x — 定界符不再删除后续内容”
2.10.1 — 价格符号不再导致页面后续内容被删除
Section titled “2.10.1 — 价格符号不再导致页面后续内容被删除”此前 The server costs $$100 per month. 会导致该句子之后的所有内容被全部移除。 这不仅发生在流式输出期间,在已完成的静态文档中同样如此。货币规则仅对单个 $ 执行转义,因此成对出现的 $$ 被误判为开启块级展示数学公式的定界符,随后用于隐藏未完成公式的流式保护逻辑从该位置一直截断到文件末尾。运行时无任何报错;文档是合法的 Markdown,但内容却与用户所写大相径庭。
保护机制本身是正确的并予以保留。错误之处在于其触发的时机与位置。remark-math 的 mathFlow 是叶子块:它仅在行的首个非空字符处开启,且缩进最多为三个空格。原先的实现在任意位置遇到未配对的 $$(包括行中)均执行截断,但在行中根本不可能吞噬后续块级内容,因此完全不需要此类保护。该函数自身的首句注释原本一直写着“在行首位置”。
边界逻辑对照 remark-math 进行了严格校准,通过追加标题和段落并观察它们是否落入数学公式节点内部来核实每种形态:
| 形态 | mathFlow 是否开启 | 当前是否截断 |
|---|---|---|
$$ 处于 0–3 个空格缩进 | 是 | 是 |
$$ 处于 4 个空格缩进,或紧随制表符 | 否(视为缩进代码块) | 否 |
$$ 处于行中任意其他位置 | 否 | 否 |
三项既有的测试用例发生变动,每一项在修改前均对照 remark-math 进行了核实,而不是单纯为了迎合新代码:这三处用例此前均固定了对行中定界符的过度截断行为,现在它们都能正常解析且不吞噬任何内容。
第二个修复解决了第一个修复引入的回归问题。 改变了截断的判定条件但未同步调整旁边的标记逻辑,导致 truncatedAtSeamStart 在不再被裁剪的尾部内容上被错误激活,进而导致增量预处理器修剪了一个无状态预处理器所保留的换行符 —— 造成两个入口点之间的逐字节等价性发生偏离,而这是该模块不能破坏的核心契约:
$a$\n $$ stateless $$a$$\n $$ incremental $$a$$ $$$a$\n\t$$ stateless $$a$$\n\t$$ incremental $$a$$\t$$在这一偏离存在期间,五阶段压测对于记录的测试轮次完全通过。该问题之所以浮出水面,是因为一项无关的设计任务在探索增量路径。门禁中此前缺乏针对两个预处理器入口点进行相互模糊比对的测试分支(fuzz 分支覆盖的是增量解析等价性),且触发该情况需要 freezeThreshold: 0 与缩进变体的组合,属于随机种子测试分支未覆盖的角落。此项已作为测试盲点记录。
针对这两项修复补充了十个双向的固定测试用例:既包含不再执行截断的形态,也包含仍必须严格执行截断的缩进形态。
公开相关包无其他改动。本版本的其余工作是一个全新的语料库相关包及其测试门禁:包含 Markdown、代码、Mermaid 与数学公式的四层测试材料 —— 源自 KaTeX 官方表格的 1139 个 KaTeX 标识符,全部 31 种通过语法校验的 Mermaid 图表类型,以及 75 个来自真实使用场景而非从源码反推的数学公式用例。
2.10.0 — 打字机控制器停止停顿分类
Section titled “2.10.0 — 打字机控制器停止停顿分类”一项影响所有宿主环境的行为变更:balanced 预设的最大展开延迟从内部的 1.2 秒调整为预设级的 2.5 秒承诺(maxLagMs,SmoothStreamPacingParams 上的新增可选字段;smooth 为 3.5 秒,responsive 为 0.8 秒)。延迟属于按需产生的开销 —— 细粒度的数据流不会接近该上限 —— 但此前容易发生停顿的粗粒度数据流现在最多可缓冲 1.3 秒并平稳播放完毕。若你的产品此前依赖旧的上限,可显式覆盖 maxLagMs。
平滑流式控制器的节奏法则进行了重构。旧版的自适应抖动缓冲区存在隐蔽缺陷:其指数移动平均(EMA)周期兼作停顿分类器,因此当数据块到达间隔超过约 1.8 秒时(例如开发服务器或反向代理将 SSE 数据流缓冲为 2 秒推送一次的大块),控制器完全无法建立节奏预估。每个大块在约 180 毫秒内倾泻完毕,随后屏幕陷入停滞直到下一个大块到达:该功能本为消除“卡顿打字机”现象而生,结果却亲自制造了卡顿。水位控制法则进一步加剧了这一问题:其稳态会积压缓冲区并在每个周期内释放固定微量内容,因此调高缓冲上限只增加了延迟而未增加平滑度。
替代方案是基于数据间隔分位数窗口的完成截止时间法则:保留每个数据块到达的时间间隔(不再进行停顿分类 —— 停顿只是被延迟上限消化掉的间隔),预期的投递间隔取近期窗口的高分位数,手头积压的所有文本以恒定速率展开,刚好在预计下一个数据块到达的瞬间展开完毕。连续两个较慢的时间间隔足以自适应切换到更粗粒度的输入;原本每 2.2 秒突发一次的数据流现在表现为落后实际接收约 1 秒的均匀打字机输出,而在过去它有一半的时间处于停滞状态。流结束时的排空速率根据速率连续性进行计算 —— 约为测得流吞吐量的两倍,并限制在 [drainMs, 3 × drainMs] 之间 —— 因此在最后一块中一次性推送全部剩余内容的服务端不会再导致消息以 15 倍速率骤然倾泻收尾。
emaTauMs 已被废弃且不再被读取。控制器还引入了防冻结的最低速率,只要存在积压内容就始终保持字素递增展开。
2.9.x — 枚举机制让位于规则推导
Section titled “2.9.x — 枚举机制让位于规则推导”
2.9.1 — 修复所依据的名称列表属于扫描器自身的内部假设
Section titled “2.9.1 — 修复所依据的名称列表属于扫描器自身的内部假设”2.9.0 的核心变更使用 parse5 针对零散闭合标签的规则取代了四名称污染机制:无匹配的开启元素,因此丢弃该词法单元并合并其两侧的文本。该规则本身是正确的。代码读取以应用该规则的是 openStack —— 扫描器对 parse5 开启元素栈的模型,而非 parse5 自身的栈 —— 而模型恰恰可能在依据规则设计的防护中仍依赖于错误的判断对象。
两者的栈在除 parse5 绝不压入栈底的名称之外均保持一致。有两个此类名称,由无关机制导致:<frame>,parse5 在 frameset 外部丢弃其起始标签;以及 <image>,parse5 将其重写为 img,从而生成 img 元素而绝不生成名为 image 的元素。在这两种情况下,扫描器均压入了栈,闭合标签匹配了自身维护的栈,执行出栈,且未触发任何防护警报 —— 而 parse5 丢弃了闭合标签并合并了文本。<frame>\n</frame>\n\n 将全部 18 字节完全冻结,标签之间的换行符成为活跃的根文本节点并在下一次追加时增长。这一缺陷自 2.6.0 很久以前就已发布,在 2.8.2 和 2.9.0 上逐字节完全一致。 未配对的 <frame> 幸免仅凭代码已记录的未闭合 <body> 的巧合 —— 栈上的虚假幽灵元素阻断了候选切分点 —— 这正是配对标签成为问题潜伏处的原因。
三个测试工具均声称“已覆盖”,每个工具的原因均不相同,这正是值得记录的部分。 2.9.0 自身的发布说明末尾称“frame 排除在外,因为 parse5 在 frameset 外部忽略它且完全不构建元素” —— 这是一种依据 frameset 针对名为 frame 的风险构建的防护,而在文档中放入 <frameset> 才能使边界安全。构造轴探测此前测量了两个名称,放入了 unmeasurable 分类中,并准确写出了机制;但没有任何代码读取该分类,无人问津的第三种答案等同于注释。而 census 字符表根据名称在扫描器自身列表中的归属进行划分,因此 frame 与 div 一起位于包含 39 个名称的等价类中,image 则不在任何列表中 —— 门禁根本无法触及两者。
污染机制现在依据四种原始名称作为其中一种机制的后果来触发:parse5 在该标签名称下不构建任何元素。其成员归属是在扫描器所区分的两种上下文中、针对包含 146 个名称的池进行实测得出,而非主观断言,断言两个方向以及上下文划分均为空;尾随不完整行开启标签列表由此派生而非转录,此前已落后了两个名称。代价:零 —— 在 6,060 个固定的测试语料库条目中没有任何一个条目向任一方向变动,且这两个名称均未在 fuzz 生成器中出现,因此过去的任何全绿测试记录均未失效。
未修复事项的记账清单首先被清空。 四个此前作为“已记录、未解决”的事项在重新审视后均未保留为未决债务。
扫描器的八个名称列表中有三个此前仍属于自我证明 —— 派生的 census 字符表根据名称在这些列表中的归属进行划分,因此扫描器错误未能区分的差异在用于捕获它的语料库中同样缺失,且 F19 正是存在于其中一个列表中。所有三个列表现在都配备了测量列表自身声称的适配器:作用域屏障阻断 parse5 的遍历,从而丢弃闭合标签并将标记留在包装器内部;外部内容确实能自闭合 <g/>;零散的表格组件将后续表格的单元格文本提升至其元素外部。三个列表在危险方向上均为空,且每个命名空间都携带防真空控制 —— 作用域探测经过三次错误构建才调整正确,且每一次错误构建都打印出了空的危险集合。
状态引导搜索曾指出三个抽象状态从未被触及,同时在其自身输出中表示无法区分字符表缺失与不可能状态。所有三个均为字符表缺失,且绝非仅存在于搜索中:片段 token 列表同时也是 CENSUS 的字符表,因此发布门禁此前从未构建过 GFM 表格,且 tableMaybeOpen —— 封条释放判定谓词的一个合取项 —— 在仓库中从未被任何测试触及。在添加了三个 token 后,声明的域在深度 3 下完全可达,不再包含死单元格。
拼接层的块末尾/内部字面量分类器此前失去了最后一个确定性测试杀手,因为该分支仅能通过首次冻结从文档中到达,而健全的封条会拒绝每个可能携带它的首次冻结 —— 每次封条修复都顺带移除了一个见证用例,针对修复后扫描器的三次采集均如实返回空集。守卫现已移至分类器的契约中,而非通往它的路径上。在每次采集后幸存的突变体现在会报红,经双向验证。测试明确声明其不声称该状态在实际中可达。
名称测试频带被发现存在门禁从未通过的第二种切分步长,由审查第一种步长时发现。通过该步长在该频带上耗费 2.7 倍开销 —— 约占 census 阶段的 8% —— 而在触及标签名称的频带上获得 2.8 倍的切分调度覆盖,F13、F19 和 F28 正是存在于该频带。
同一审查中还发现了另外两项改进,均在安全方向上且零成本。 parse5 携带一个仅有 </form> 能清除的 formElement 指针,因此隐式闭合的 form 会使其保持设置状态,后续的 <form> 会被忽略 —— 这是证明枚举解析器条件无法做到完备的长期证据。扫描器此前从未对其进行建模。使其保持隐蔽的是一个巧合指向该风险的无关建模选择,而针对该巧合失效的缓解措施是一条注释,要求后续修改者同步添加守卫:一项对未来变更的要求,无人负责且无测试检验。 该规则现在直接建模,测量成本为零。验证其承载能力经历了两次尝试 —— 出栈开启元素而不递减计数并不是隐式闭合标签建模,且它报告的边界与有效守卫完全相同。
快照门禁此前在应当比对多重集(multiset)的地方比对了集合(set),这是一个漏洞的两种表述:子集检查无法识别重复签名之间的交换,且完全无法识别新增项。现在两个方向均进行计数。明确说明这一点是因为免成本的收紧往往在缺乏证据的情况下被轻信 —— 各处的判定均未发生变动,未发现旧检查保持沉默而新检查报错的文档。这是一个预防性修补的漏洞,而非已发生见证的漏洞。
澄清已解决的事项
Section titled “澄清已解决的事项”本版本之后保留了四项记录,均非延期事项。列于此处以便后续读者不会将记账表误读为未偿还的债务:
annotation-xml被无条件视为 HTML 整合点,而规范视encoding而定。修复此问题会扩大边界 —— 当前的解读属于过度阻断,位于安全的一侧。这是一项在安全性与冻结率之间权衡的精度项,而非缺陷。- 退役的封条释放枚举机制多保留一个版本。它是刚刚获得覆盖率底线的包含关系触发器的有效组成部分;现在删除它会在刚刚测量之后立即撤掉安全网。
- 三项属于测量的局限,而非工作项:SVG
title确实属于作用域屏障,但适配器的 raw-text 守卫是在 HTML 命名空间中测量的;caption、td与th需要一个能响应作用域遍历的<table>祖先才能生效;拼接分类器的无位置兄弟节点用例不是合成构建的合法形态。删除这些条目不会消除限制,只会消除对它们的记录。
最后一组恰恰是工程纪律的核心,而非例外。F28 的发现正是因为探测器如实记录了 frame 与 image 为其无法决定的名称 —— 一个空的“无法决定”列表并不代表一个没有盲点的仓库,而代表一个不再发问的仓库。
附带进行了三项细微修正,均属于同一类问题。发布门禁此前悄然停止了全量 census 运行 —— 在修复另外两个运行 CI 值的旋钮的提交中,CENSUS_STRIDE 从 1 变为了 3,而三处注释依然将 full-stride 描述为门禁属性;现已恢复为 1。封条释放包含关系遍历此前断言开发构建触发器保持沉默,却从未报告其是否已应用,因此现在对其自身评估进行计数(在 6,060 次扫描中评估 862 次)并维持在底线之上。门禁脚本自身被发现无法在门禁运行的两台机器中的一台上执行 —— 第二台机器没有 zsh,这正是自 2.8.1 以来每次拆分的发布门禁都是从脚本的环境变量行手动拼装而非直接运行的真实原因。
2.9.0 — 曾五次泄漏的枚举机制被动态推导取代
Section titled “2.9.0 — 曾五次泄漏的枚举机制被动态推导取代”封条释放判定谓词决定一行是否锁定冻结前缀与尾部之间的接缝。它此前属于“不发出顶层节点的行”的枚举,其默认行为是未列出 ⇒ 释放。该默认行为在 18 个月内泄露了五次 —— 仅包含注释的行、整行的零散闭合标签、定义延续行、恢复的脚注体的惰性延续,最后是整行的 <!DOCTYPE html>、截断的 <!DOCTYPE、<html> 与 <head>。每次修复增加一个成员;没有任何一次改变产生下一个成员的形态。
现在改为推导得出。 分为三层,每层回答关于该行的一个问题,而非识别该行:此行是否开启一个块(micromark 自身的内容构造判定)、该块的类型是否在换行中可见,以及它是否存活以发出 parse5 节点。可恢复上下文门禁在此之前运行 —— 在脚注定义、链接定义或开启容器下方,仅可证明的中断在任意缩进下才允许释放。任何一层中的未知情况均进行保留(扣留),反转了导致泄漏的默认行为。迁移采用了差分触发器:推导规则与枚举规则在三个谱系上针对固定语料库并行计算,断言包含关系 推导 => 枚举,并对违反的象限进行硬报错。该象限在每个阶段均保持为空;其他分歧经过分诊,其中四处属于枚举规则自身的错误。
这四处正是核心发现,它们是在针对它们的搜寻无功而返之后被发现的。 该设计的第二轮对抗性审查去搜寻第五个成员,一无所获,并将该类别记录为已闭合。事实并非如此:<!DOCTYPE html> 及其三个同胞释放了接缝,而 parse5 没有为它们中的任何一个留下节点 —— doctype 在片段上下文中被丢弃,文档结构起始标签被丢弃且其属性合并到已存在的元素上。它们在今天表现为惰性,但仅因第二种机制的污染机制恰好覆盖了全部四者,这是巧合而非设计。该判定谓词在其存在的整个历史中在这四种行形态上都是错误的。通用教训记录在偏差记账表中:空成员搜寻并不代表闭合类别 —— 搜寻只是抽样形态,而该故障类别由默认行为定义,无论多少抽样都无法界定补集的边界。
顺带发布了三项延期修复,因为本次变更本就需要发布门禁。PI 的残留文本节点在追加下不再改变标识 —— 且它是由推导机制本身修复的,而非任何针对它的专门代码,这是证明这三项属于同一种病灶的最明确证据。依据四个名称(DOCUMENT_STRUCTURE_NAMES)设计的防御重新依据产生风险的规则构建:parse5 丢弃每个没有匹配开启元素的闭合标签,不变式是不变式在其在 EOF 处于两个构造之间留下的文本节点,在追加时增长 —— <area> 作为一个 void 起始标签属于该家族,因此该形态绝非仅是“零散闭合标签”。VOID_TAGS 增加了 basefont、bgsound 与 keygen,遗漏项现在被断言为空集而非注释;frame 排除在外,因为 parse5 在 frameset 外部忽略它且完全不构建元素。
代价:6,060 个固定语料库条目中有 3 个下移,无上移;在两个谱系上真实文档的冻结率保持逐字节不变。
第四项缺陷是在各旋钮最终连接的状态下运行门禁时发现的。 链接引用定义属于块级构造 —— micromark 在行内解析之前决定它,在此时反引号是普通字符且代码跨度尚未存在。扫描器清空行内代码跨度,使其行内提取不会作用于 micromark 视为代码的内容上,然后将同一被清空的行传递给定义判定。遮罩删除了内容,而删除的内容恰好使定义失效:[x]: /u 后接代码跨度注册了一个被 micromark 称为带有活跃快捷引用的段落的定义,因此扫描器冻结了一个在碰撞定义到达时首个文本节点发生分裂的文档。修复了三种形态,包括因标签自身被遮罩而以错误标识符注册的定义。语料库变动为零 —— 它对整个类别均属于盲区,因此回归测试框架永远无法发现它。这与另外三处属于相同的键控错误,只是出现在了新位置:对已被行内阶段重写的行提出了块级问题。 目前共有四处呈现这种形式,它们的共同点是防御与风险依据了不同的事物,而错误的键持续返回看似合理的结果。
它的浮现仅因本周上线的三个独立的接线变动 —— raw-frozen 属性、全部六种配置而非每文档一个哈希配置,以及片段频带在门禁规模下达到深度 4。审查发现发布门禁从未传递其自身昂贵旋钮中的两个;这就是它们背后的产物。
验证工具先行重构,它们正是发现大部分此类问题的功臣。 构造轴探测表在一个封闭操作符集合上测量 micromark 与 parse5 在构造终止符与其内容治理上的分歧,任何两套语法不一致的单元格必须指明声称它们一致的扫描器成员 —— 记录为散文的分歧曾两次未能保护任何内容,因此该表是一项强制义务而非文档。扫描器的八个名称列表中有四个现在对照测得的语法行为进行证伪,而非盲目信任;另外四个在读者遇到之处标记为自我证明。有界穷举 census 阶段增加了一项不依赖拼接生效的属性 —— 此前的属性因该原因对一整类缺陷存在结构性盲区 —— 外加全部六种配置和源自扫描器自身分类法的字符表。针对抽象扫描检查点签名的状态导向搜索用两个 token 就能触及表面枚举需要八个 token 才能构建的见证用例。raw 模式门禁去除了任何文档可在 24 字节内伪造的豁免:它此前依赖渲染后的包装器,因此作者书写的 <section data-footnotes> 会从等价性两端移除作者自身的内容,并在每种配置下静默植入的缺陷。
2.8.x — 延期裁剪收敛
Section titled “2.8.x — 延期裁剪收敛”
2.8.2 — 扩充后的测试语料库捕获的两项长期存在的流式缺陷
Section titled “2.8.2 — 扩充后的测试语料库捕获的两项长期存在的流式缺陷”v2.8.1 的记账留下了两笔债务。偿还它们发现了一个缺陷;随后发布门禁又发现了两处 —— 两者均属于真实的、用户可见的流式缺陷,直到并包括 2.8.1 的每个版本都带有这些缺陷。所有三项均已修复发布。
作用域屏障同样会丢弃 markdown 生成的闭合标签(F19)。 hast-util-raw 在重新解析 mdast 之前对其进行序列化,因此 > 变为真实的 <blockquote> 元素,# 变为 <h1>,*a* 变为 <em> —— 这些标签从未在源码中出现。当某个生成的闭合标签触发时,如果 HTML 作用域屏障(<table>、<marquee>、<object>、<applet>)仍处于开启状态,parse5 会通过扫描器为源码标签所记录的相同作用域遍历丢弃该闭合标签 —— 但由于被丢弃的标签从未存在于源码中,因此不会运行闭合遍历,原始流保持完美平衡。任何标签平衡工作都无法发现它。在真实增量路径上测量的流式后果:*<object>* 随后 </object> 随后尾部段落,在全量解析下将尾部渲染为嵌套在顶层 <em> 内部,而在增量下渲染为普通的同级 <p> —— 每个涉及的帧均产生分歧。扫描器现在从离开第 0 列 HTML 块(唯一周围没有生成元素的位置)保持屏障开启的确认行开始触发污染。成本:6,060 个固定条目中有 3 个下移,无上移;现实文档冻结率保持在 63.82% 不变。记账表行 F19。
原始文本块的存活时间可能超过支持它的分词器状态(F20)。 </script/> 为 parse5 闭合元素 —— 斜杠是它忽略的伪自闭合标记 —— 而 CommonMark 的 type-1 结束条件要求字面的 </script>,因此 micromark 的块继续运行。扫描器在该块开启时屏蔽原始构造扫描,前提是两套语法在闭合符之前的每个字节都属于内容达成一致;一旦发生脱节,屏蔽就会隐藏 parse5 真正起作用的标签,包括 F13 所要捕获的后续 <pre> 内部的虚假开启标记。该 pre 随后吞噬文档的其余部分。这是 F17 缺失的一半:其对块开启区域的豁免依赖于“它们的闭合被精确追踪”,这对 parse5 的闭合为真,对 micromark 的闭合为假 —— 而屏蔽询问的是 micromark。六个配置中有三个在重现用例上激活,其激活帧 100% 产生分歧。通过将屏蔽绑定到两套语法(mdType1RawText && !inRawTextTok)修复,无新增状态。记账表行 F20。
封条释放枚举的第四个成员被攻破(F18)。 对派生封条释放设计的对抗性审查构建了 F16 修复自身 indent >= 4 合取项仍允许通过的形态:跨越空行恢复的脚注体的惰性延续([^a]: note、空行、缩进 cont、随后在缩进 0 或 2 处的 lazy tail)不发出任何节点,却释放了封条。与 F19 和 F20 不同,此项属于模型层面的问题,且测量非真空地表明:11 个追加时序 × 6 个配置在每个时序上均激活增量路径,且无一发生分歧,因为 sanitize 恰好掩盖了差异。该修复基于“掩盖不是安全理由”的原则,而非基于事故。该子句现在属于中断逻辑,而非缩进测试:在可恢复脚注下方,仅有空行前导的 ≤3 缩进块起始能逃脱扣留 —— 任何其他非空行均保持封条,与缩进无关。通用教训记录在记账表中:以缩进为条件的“不发出节点”子句本质上是错误的。记账表行 F18;四个翻转/控制固定用例;基线移动零条目。
语料库学习其曾盲视的复合形态。 确认的五个 v2.8.1 战役缺陷此前使 6,000 条目固定语料库的变动恰好为零 —— 生成器从未组合这些成分。新增八个复合族(三个全新:preBogusOpener、sealReleasePiercer、rawTextRunOn;五个合并入拥有机制的族中),每个均具有测量的 REACH(生成形态对比单字节控制翻转扫描器的边界 —— 例如 <pre> 保持的 PI 开启 0 vs 22,遮罩 doctype 0 vs 10,残留下的包装定义标题 0 vs 39)以及测量的密度成本(未触及的族保持其约 88% 的采样;每个固定种子标记均清除 RUNS/60 底线)。seal-piercer 族区分了修复 F18 的树与未修复的树,新的 containerHeldRemnant 族固定了当前的根近似行为 —— 这些行是未来派生封条替换必须下移的行,且重新生成规则记录了它。拆分对比合并的标准、所有数字以及树归属记录在 GRAMMAR-COVERAGE 的复合族注释中。
派生封条释放设计本身(三层 L1×L2×L3 合取、未知即扣留默认行为、差分绊网迁移)经受住了两轮对抗性审查 —— 第一轮发现了 F18 —— 并定稿为设计;实现另行排期。
2.8.1 — 代码审查收效:两项流式缺陷、真实的记账核对以及消除虚无测试
Section titled “2.8.1 — 代码审查收效:两项流式缺陷、真实的记账核对以及消除虚无测试”全主线外部审查(v2.5.5..v2.8.0,对照实时 micromark/parse5 的差分 A/B 测量)发现了三个阻断项和验证体系中的一系列结构性弱点。发现的所有问题均在此修复发布;无延期事项。
两项真实的流式缺陷,均对以往的所有安全网隐形:
<pre>是 CommonMark type-1 名称,但不是 parse5 原始文本元素 —— parse5 在 DATA 状态下对其中内容进行分词,因此<pre>内部的<?x或</3真正开启了一个虚假注释,吞噬了</pre>的>,导致该元素吃掉文档的其余部分。P4a 虚拟开启门禁假设 type-1 意味着 raw text,并移除了此前纯凭巧合覆盖此问题的污染逻辑。type-1 成员现在携带 parse5 raw-ness(raw: boolean,仅对pre为 false),门禁读取该成员。在修复下固定语料库变动恰好为零字节 —— 该家族对语料库隐形,这也是 P4a 验收从未发现它的原因。偏差记账表行 F13。- 容器行使得被拒绝的 type-7 标签行不可判定:micromark 的 tagName 携带没有人建模过的
!parser.lazy例外,因此> quoted随后<x-y/>开启容器保持的 html 块(惰性延续),而> # h随后<x-y/>开启顶层多行块 —— 21 个容器前缀中有 16 个发生分歧,未被污染,且流式反例在单字符追加时重写了已冻结前缀。管道类的解答在此适用:粘滞的containerMaybeOpen标记(与tableMaybeOpen相同的五处重置位置)污染被拒绝的标签行而非做出判定。29 类测试集的预言机本身在此处存在盲区 —— 它只读取根子节点,因此容器持有的 html 处于隐形状态,四行被固定为错误的测得答案;它现在遍历整棵树。记账表行 F14。
测试记录修正。 v2.8.0 发布运行失败并非 node V8 漂移:在单一节点上测量,语料库指纹仅随 type-7 粘滞残留修复的生成器编辑而翻转。真实的规则 —— 对 fuzzGenerators.ts/pinnedCorpus.ts 的任何编辑在同一提交中重新生成 boundaryBaseline.json,增加归属 —— 现在写在测试工具头部、覆盖率文档和 CI 中,发布后重新生成所吸收的 12 处增加归属于它们实际对应的语料库组成变动。明确说明一个后果:v2.8.0 标签自身的树带有一个红色的 boundaryDiff(基线落后其语料库一次生成器编辑);HEAD 过去和现在都是绿色的。
验证机制不再能虚假通过。 植入的全量拼接崩溃过去能通过除一个门禁外的所有门禁:一致性遍历的参与底线在结构上不可达(memo-hit 帧算作参与;空尾部保证每文档 4 个探测),且 K=4 census 完全没有底线。现在:参与度仅计算非空尾部拼接,census 断言比率底线,assertStreamEquivalence 默认要求参与(poison-to-zero 固定用例显式退出 —— 遍历发现了两个死固定用例,其实时断言现在是污染本身),两个无法失败的 v2.8.0 回归固定用例(一个从不拼接;一个过度决定)获得了区分性替换。边界差分网的指纹是重新生成触发器,而非逃逸通道:每样本内容哈希使增加红线在语料库组成变动中保持装填 —— 正是通过该绕过方式,一次纯测试提交曾吸收了 645 处未归属的增加。(P) 同一性预言机首次设卡:压测阶段 5 在 ORACLE_RAW=1 下运行一致性遍历,并具有强制的豁免白名单,其谓词在对抗性审计(~50k 探测、6,880 个流式文档、零引擎分歧)中幸存,并因此进行了头部锚定 + 拒绝门禁 —— 真正的新分歧家族现在会失败,而非隐藏在 80%+ 的豁免区中。
P 侧的模型契约修复(均为过度阻断方向,均进行了翻转固定): definitelyInsideTable 现在遵循其自身的欠声明契约(截断的 <table 开启不再计入 —— parse5 丢弃了它们);封条释放谓词不再在仅为零散闭合标签的行上释放(parse5 为其不发出节点,因此后续文本仍可增长)。在 M 侧,单行中断表获知了其双行失败模式:裸列表标记不能中断开启的段落(惰性延续 —— 错误的 false 过去通过 setext 行符号翻转进入欠声明),且实时表格内部的 setext 残留行是表格行,而非下划线(它不再解除 tableMaybeOpen 的武装)。三头 × 25 类双行测试集对照实时 micromark 锚定了所有这些内容。
精度恢复,已归属: tableMaybeOpen 不再由 html 拥有的管道携带者装填(<!-- a|b --> 不可能是表格行) —— 提升的边界经过逐字节流式验证;未恢复的一个携带者(<![CDATA[a|b]]>)独立地被引用污染保持在 0,并作为此类被固定。
接口与整洁性: 4 个零使用者的 engine barrel 导出退回内部(在构建的 d.ts 上的测量接口 diff:恰好是这四个);fork 链锁定精确版本;documentIndex 加入根 README 的 props 表格;formElement 条目现在如实表述(闭合遍历守卫是全部缓解手段 —— 经突变验证 —— 且 schema 固定是漂移绊网,而非第二道守卫;sanitize 掩盖被审计反证为不可作为防御手段);release-highlights 自身的 2.8.0 章节跟进了其第三次压测修复。
验证:engine 1019 / core 455 在每次提交时保持全绿;boundaryDiff 全程保持绿色且所有变动均有归属;五阶段全新随机种子压测分布在两台机器上(fuzz + 扫描器在本地,direction + census + 新的 raw 门禁阶段 5 在较大 runner 上) —— 结果在发布时记录。
2.8.0 — 精确 Type 7 HTML 块处理、退役污染标记与偶然特性的规范化
Section titled “2.8.0 — 精确 Type 7 HTML 块处理、退役污染标记与偶然特性的规范化”2.7.0 带有依据推迟的三项内容,每项均作为其自身的门禁序列落地 —— 外加转换为带绊网命名守卫的“凭巧合安全”的事实类别。
精确 §4.6 type 7(规划中所说需要自身独立发布的切分 —— 正是本项)。isType7Line 逐状态转录了 micromark 的完整标签自动机,且一致性固定用例对照实时 remark-parse 运行,因此 micromark 升级会导致测试失败而非静默漂移:带引号的属性值可能包含 >(<span title="a>b"> 单独成行属于一个块 —— 退役的运行标志所要覆盖的漏洞),未带引号的值链式接 =(比规范书面语法更宽松 —— 经过测量,刻意匹配),<a b=/> 完整,闭合原始文本名称属于 type 7(</style> —— 覆盖率表格中旧的“段落作为闭合标签”记录是错误的,且仅在标志笼罩下无害),且 <style/> 派发越过原始名称检查。
中断条件是无人写下的另一半。 Type 7 “不能中断段落”实际上指的是 micromark 的 CONTENT 构造 —— 而旧的 prevLineWasText 门禁(“任何非空行”)在标题、主题分隔线、终止符行和围栏闭合后拒绝,所有这些在测量中均属于 type-7 开启。prevLineOpenContent 按行类别推导精确答案(对照 remark-parse 锚定的 29 类测试集);行模型无法解决的一个类别 —— 管道行:GFM 表格行(开启)对比带有管道的段落(不能开启),在多行之前由表头/定界符对决定 —— 转为装填一个粘滞标记(tableMaybeOpen):表格延续行完全不需要管道,因此该标记穿过每条内容行直到空行解除其武装,在其下方被拒绝的任何标签行均触发污染。此处的任何一种近似方向都不安全:欠声明成员会重新打开屏蔽漏洞,过度声明可能将真实的 type-1 块遮蔽在虚假运行后。
随着漏洞闭合,标志退役。 迁移 B 的保留行(屏蔽、带后盾的围栏/数学公式抑制、接缝设置/清除、截断开启的可恢复性)逐个提交迁移到成员中,每次变动使用引擎探测测试集归属到具体样本(benign-201 +94 = 真正属于代码跨度的反引号标签;hazard-518 +107 = 在空行处恢复的段落 <span 截断);mayBeRawToMicromark 被删除;歧义起始风险污染随着歧义本身退役 —— 在 34 个文档中 69 个固定边界提升,这正是该阶段所期望的冻结率恢复,602 个引擎探测,零缺陷。
P3b 带来了所有三个污染退役 —— 由发布压测确定了 F6 恢复的真实边界。 双重转义阶梯被精确追踪(double 蕴含 escaped;在 double 时 </script> 退回到 escaped,元素保持开启并被计数;--> 退出两个级别,经 parse5 验证)。这能换取多少恢复经过了两次测量决定:首次落地因拼接端反例被回滚(跨越元素吞噬了块间换行分隔符 —— 第 20 帧处额外的根 " ",红测重现并通过新的 rawTextRegionCrossesOut 守卫修复),而本版本自身的全新种子压测随后发现了其背后的扫描器端漏洞(三个方向测试集分片):多行纠缠必然跨越 micromark 块,且 sanitize 剥离跨越元素是一种擦除,其合并向后延伸穿过较早的边界 —— 属于 F9/F11 类别,因此在全文档范围污染,仅在开启行上解决的纠缠才能恢复。--!> 和 PI/CDATA 首个 > 的分歧做到了窗口精确:parse5 提前离开构造,micromark 块运行到其自身的终止符,两个闭合符之间的窗口仅在其能容纳 parse5 起作用的字节时才污染 —— 无标记窗口是 parse5 文本,在终止符处收敛,其残留归 blocker-6 接缝所有(floatingResidue 现在取两套语法注释状态的交集)。在此过程中,p5 虚假注释覆盖层与 md 类别 3/4/5 如实配对 —— 原始构造状态机中最后一个“一个字段服务两套语法”的不对称性 —— 且压测发现的配对推论也落地了:type-6/7 运行中的 2-5 开启不再夺取成员(运行的标识存活;parse5 一半位于覆盖层上),关闭了已删除运行标志此前一直在掩盖的虚拟数学公式漏洞。
formElement 偶发事实成为设计的守卫。 设计 §2.1 的枚举反例通过两个巧合保持潜伏:闭合标签遍历从未对隐式闭合标签建模(隐式闭合的 <form> 保持计数),且 form 在默认 sanitize 白名单中不存在。两者现在均成为具有绊网测试的具名守卫 —— 对隐式闭合标签建模或允许 form 会导致测试失败,明确指出必须与变动一起发布的内容。
发布压测再次发挥价值 —— 三次。 首次四阶段运行(种子 20282500 系列)在上述两个漏洞上失败了四个分片,重新运行捕获了第三个:GFM 表格延续行不需要管道,因此逐行管道测试在后一行欠污染了被拒绝的标签行 —— 这正是 tableMaybeOpen 变为粘滞的地方。所有三者在每提交门禁中在结构上隐形(边界差分语料库和 495 个单元测试从未包含这些形态;拼接守卫无法保护边界本身)。所有三者均已修复,完全重放失败的确切种子且顺利通过,门禁在发布前以全新种子重新运行。
验证:预检全绿;一致性预言机在 ORACLE_RUNS=800 下两次零缺陷;每次边界变动均经过引擎探测(跨归属批次 1000+ 探测,零缺陷);失败的压测种子重放通过;四阶段全新种子压测在扩大规模下(更大 runner,种子 20286000 系列)在带有全部三项压测修复的树上顺利通过。
2.7.x — 两种语法,两种模型
Section titled “2.7.x — 两种语法,两种模型”
2.7.0 — 沿缺陷多发的接缝拆分冻结扫描器
Section titled “2.7.0 — 沿缺陷多发的接缝拆分冻结扫描器”18 个提交,每个均由 2.6.0 工具把关(每步边界差分、每阶段一致性预言机、每次设计落地前的对抗性预言机审查)。这些工具所针对的重构:扫描器的单一行模型 —— 服务于 micromark 和 parse5 的同一组字段,每个偏差记账表行指出的根本原因 —— 成为两个按语法划分的模型,阻塞项作为它们之间的关系。
引用解析移出(referenceTaint.ts):阻断项 5 的定义/引用语法和逐行收集作为纯粹的代码搬移离开了 processConfirmedLine —— 在所有 6060 个固定边界上严格为零增量,这是第一个由测试工具而非争论判定的真实代码变动。
(P) 变为 P5Tok —— parse5 分词器宏观状态的一个划分({data, comment, rawText, script, bogus}),标签内属性位置作为一个独立的覆盖层,因为测量表明它与它们中的任何一个共存。三个审查阻断项在进入途中重塑了它,每个在采纳前均经过验证:raw-text 状态是 MASK(掩码),而非阻断项(设置时没有任何内容到达平衡 —— 因此模型相信但 parse5 不在的状态使候选切分点更有可能存活);标签位置不是分词器状态;行内锁存必须在开启时捕获。在旧模型会同时持有两个事实的地方,迁移污染该行而非静默选择。
外部内容子系统收敛为两个方向。 六符号精确模型(跳出列表、整合点、弹出)存在是为了精确说明 HTML 规则何时在 <svg>/<math> 内部恢复 —— 而在此做到精确正是 F1/F2/F5 家族。现在:自闭合标签仅针对外部根本身遵循,外部内容附近的原始文本元素起始触发污染 —— 审查测量得出任何一个精确答案都在一个方向上是已发布的缺陷(其阻断项:过度声明的分词器切换在 <svg><title><div></title></svg> 上将边界从 0 提升到 36,因为开放栈上未切换的 <div> 是唯一的保护)。量化代价:30 处边界减少,均发生在携带自闭合 svg 子节点的 12 个语料库文档中。
(M) 变为 MdBlock —— 针对 micromark 的开放流构造的一个联合(fence, math, html types 1–7),替代了原本的 11 个字段。type-7 成员通过刻意近似的 TYPE7_LINE_RE 进入(精确的 §4.6 type 7 保持剪裁),且一个运行标志按既定设计保留:mayBeRawToMicromark,作为该近似属性漏洞的保守掩盖 —— 审查测量到盲目迁移其使用者会导致四处边界上升,其中一处删除了相位污染后盾,且新的语料库家族(nonType6QuotedGt)现在恰好对该漏洞设防。在此过程中,虚拟原始构造开启符消失(<!--\n<?x 过去同时保持两个块开启;两套语法均称这些字节为文本) —— 8 处边界增加,每处均通过引擎探测测试集。
parse5 的注释状态获得其自身的字段。 --!> 为 parse5 闭合注释,对 CommonMark 则不然;该分歧现在是 p5Tok 和 mdBlock 之间的关系 —— 在开启处污染,通过测试固定,两个字段均无法在另一语法仍在内部时释放块。
一次退役尝试,经测量并在一次会议中回滚 —— 与发布的内容同样显著地记录:跟踪脚本双重转义阶梯以使边界在分歧窗口后恢复在扫描器层是健全的,但 micromark 切割两个原始块,而 parse5 跨它们构建一个元素,拼接的接缝合成在远端双重计算分隔符。流式反例在测试文件中固定;剩余的阻断项 7 污染保持不变,直到拼接对跨原始块元素建模。发布的是安全片段:--> 精确退出转义状态。
发布压测再次证明其价值。 合并结果上的首次四阶段运行发现了每提交门禁在结构上无法发现的回归:零散的 --> —— 对两套语法均为文本,在旧的逐字段 commentOpen = false 下为无操作 —— 触及了新联合中未受保护的成员清除,并释放了 type-1 块的 html{1}(上游的 </script/> 不是 CommonMark 的字面闭合符,因此该块必须运行到 EOF 并保持阻断)。折叠前扫描器保持 0 的边界变为 170;单字符追加重写了已冻结区域。两处注释闭合位置现在仅清除其自身成员,审计了每个其他清除位置均有分支保护,缩小后的文档固定了流等价性 —— 分工严格按文档保持:固定差分语料库和 958 个单元测试从未见过该形态;只有全新种子见到了。
验证:预检 1590 项测试;一致性预言机在每个阶段零缺陷;边界差分的四个植入突变仍被捕获;合并结果上包含修复的四阶段全新种子压测记录完全通过 —— census 阶段现在采用哈希分散(分片之间的实际运行时间差距为 ×1.5,对照预测每分片成本测量 r=0.980,降至 1.004)。
2.6.x — 改造前的测试工具准备
Section titled “2.6.x — 改造前的测试工具准备”
2.6.0 — 为冻结扫描器配备一致性参考实现、固定 Diff 语料库与不透明检查点
Section titled “2.6.0 — 为冻结扫描器配备一致性参考实现、固定 Diff 语料库与不透明检查点”本版本无运行时行为变更 —— 属于计划重构的前置和测试工具准备阶段(P0 + P1),该重构将把冻结扫描器的单一行模型拆分为两个按语法划分的模型。此处的所有内容都是为了让确实改变行为的阶段能够由机器而非争论来裁决。
FreezeScanCheckpoint 现在是不透明凭证(opaque token)。 检查点的 42 字段形态曾通过 dist/index.d.ts 发布 —— 可通过 computeFreezeBoundary 的签名和 IncrementalParseState.scanCheckpoint 访问 —— 因此重构删除的每个内部字段都会成为公开 2.x 类型的破坏性变更。公开类型现在仅携带结构化品牌键('~freezeScanCheckpoint' —— unique symbol 会在双重 .d.ts/.d.cts 入口之间名义不兼容);真实形态存在于 d.ts rollup 无法触及的包内部接口中。读取字段的唯一生产调用方(phantomSuffixCloser)现在调用狭窄的访问器 pendingFenceCloser,它将相位信任和仅限第 0 列的决策保留在扫描器模块内部。如果您从外部访问检查点:这些字段从未是 API,类型现在明确指出了这一点。
表 D 清偿了安全论据所依赖的文档债务。 冻结候选点仅位于确认的空行;其正当理由是“语义跨越空行的每个构造均已处理”。该枚举此前不存在于任何地方。GRAMMAR-COVERAGE.md 现在将其分三组列表 —— 空行位于其内部的块(围栏、数学公式、HTML 类型 1–5、阻断项 3 延续、parse5 侧跨越)、空行之后重新解析其之前内容的内容(定义列表反向认领、引用重定向、擦除合并、接缝),以及语法本身在空行处停止的构造 —— 每行指明覆盖机制。向链路添加语法扩展现在意味着首先添加行。
一致性预言机(新增,仅限测试)。 权威工具先流式传输一个文档,然后将该文档外加一个对抗性探测尾部通过真实引擎流式传输,并对照全新的全量解析进行深等价比较 —— 包含位置,绝不取决于拼接是否生效。探测测试集包含没有生成器发出的形态:隐式闭合 form 之后的 <form>、表格部件、与前缀实际使用的标签碰撞的定义。micromark 层跨度预言机将失败归因于 markdown 语法;差分 parse5 层同一性预言机保持 formElement 类潜在分歧可见(sanitize 掩盖元素差异 —— 且 sanitizeSchema 是公开属性,因此“被掩盖”并不等于“消除”)。
构建工具产生了三项比工具本身更有价值的测量发现:
- 安全契约是扫描器的边界加上拼接端守卫。 纯粹的“前缀和尾部独立解析并连接”的同一性对于拼接合法拒绝的尾部会失败 ——
<td>尾部在任何段落前缀后都会发生分歧,因为仅尾部的片段解析仍以 parse5 的“in template”模式启动,而全量解析早已弹出到“in body”。未意识到这一点的预言机要么谎报狼来了,要么静默掩盖 F8 家族。 - 一致性预言机可能过度声明。 首次遍历将其比对锚定在纯前缀上,并在压测规模下产生 ~3.7k 次一致报警 —— 全属噪音:扫描器在给定快照的每条确认行时授予边界(阻断项 3 和 4 根据超越边界的证据确定候选点),且引擎切分快照的解析,绝非纯前缀的解析。重新锚定后,相同的 2×2000 文档遍历报告零报警和零缺陷。坏预言机条目与缺陷以相同权重记录。
- 测试固件库包含零个链接引用定义。 在通过植入突变验证测试工具时测得:越过已解析的链接引用进行冻结 —— 安全条件的 (R) 维度 —— 几乎未被任何测试覆盖。三个专用文档现在按名称固定了它。
边界差分测试工具在每次测试调用时运行。 6060 个固定边界(17 个冻结固件文档 + 3 个引用解析文档 + 2000 个固定种子 fuzz 样本,每个在三种生产语法配置下)与提交的基线进行比对:任何边界增加都会失败,直到伴随指明修复欠阻断的记账条目刻意重新生成基线;减少则打印直方图并通过。该测试工具直到刻意让其失败才被信任 —— 四个植入突变(blankRun 差一错误、遗漏阻断项、遗漏污染、遗漏定义注册),全部捕获。
验证:预检 1581 项测试;两种子下 2×2000 文档的一致性遍历,零缺陷;四阶段全新种子压测记录通过。
2.5.x — 由你掌控的片段顺序
Section titled “2.5.x — 由你掌控的片段顺序”
2.5.5 — 顺序、模式与阶段:行扫描器无法计数的三个维度
Section titled “2.5.5 — 顺序、模式与阶段:行扫描器无法计数的三个维度”本版本同时修复了 <svg><template> 导致渲染崩溃的问题。 任何包含该原始 HTML 组合的 Markdown 都会导致 hast-util-from-parse5 抛出 TypeError: Cannot read properties of undefined (reading 'nodeName') —— 这与流式无关,全量解析同样会崩溃,上游 rehype-parse 用户受到同等影响。根本原因在于:转换器会读取每个名为 template 的元素的 .content 属性,但根据 HTML 规范,parse5 仅为 HTML 命名空间下的 template 挂载 template 内容;在 <svg>/<math> 内部,<template> 属于普通的外部元素,其 .content 为 undefined。由于上游已 18 个月未发布新版本,引擎现在依赖 GitHub ai-markdown 组织下的三层 fork 依赖链:@ai-markdown/rehype-raw → @ai-markdown/hast-util-raw → @ai-markdown/hast-util-from-parse5,其中外层两个包与上游仅相差一行别名配置,最内层包包含了单行守卫以及五个回归测试。fork 遵循上游版本规范(8.0.5 / 9.1.3 / 7.0.3 = 上游 + 修复),通过带来源证明的 npm 可信发布,并将在上游合并修复时废弃。引擎级回归测试(svgTemplateCrash.test.ts)固定了该行为,防止依赖变更静默还原依赖链导致生产事故。
七个已发布的欠阻断缺陷,均在此前已存在(每个均通过将修复文件还原对照 v2.5.4 验证),不是通过更多 fuzz 发现,而是通过一系列新工具发现:对抗性设计审查指出 parse5 侧模型缺少树构建;测量该声称产生了一个手写条件遗漏的反例;在全缀/尾部矩阵上扫描反例的同一性发现了首个 bug;每次修复的全新种子压测浮现了下一个。每一步均由新评判机制开启,而非更用力重跑旧机制。
作用域遍历(<div><table></div></table>,族:marquee/object/template/applet)。 HTML 规范通过向下遍历开放元素栈并在作用域屏障处停止来解析大多数闭合标签;如果首先未找到匹配,闭合标签是解析错误并被忽略 —— 元素保持开启。</div> 被丢弃因为 table 是屏障,</table> 仅弹出 table,div 吞噬文档剩余部分。扫描器计数 div 1-1、table 1-1,报告平衡并冻结。名称→计数袋无法看到这一点,因为开启与其闭合之间存在什么是袋丢弃的信息。扫描器现在保留有序 openStack 并通过作用域遍历解析闭合标签 —— 仅移除匹配元素,因为块名称穿透弹出,而格式化名称运行 adoption agency 代替,对前者建模会导致后者欠计数(实测:在收窄前将四个测试固件变为了新的欠阻断)。
模式捕获(a + 空行 + <title> + 空行 + *b* —— 十六字节)。 startTagInTemplate 将 script/style/title/noframes 直接路由到 “in head” 而不首先弹出 template 插入模式,不像其他所有起始标签那样弹出、压入 “in body” 并重新处理。因此当其中一个开启 raw-text 区域时,_switchToTextParsing 在全量解析中捕获 IN_BODY,在尾部独立解析中捕获 IN_TEMPLATE;首个零散闭合标签恢复捕获的模式,从该处起 </p> 在一侧合成空段落而在另一侧被忽略。在拼接层修复:当尾部前导 html 运行开启四者之一且捕获仍活跃并在运行内无诚实闭合符时,回退到全量解析。textarea/iframe/noembed/xmp 走默认分支,捕获收敛模式,经测量安全。修复本身需要一次迭代:判定“通用起始标签已弹出模式”需要知道哪些 <…> 是标记,而 <![CDATA[…]]> 内部的 <div> 不是 —— parse5 在首个 > 结束虚假注释,因此该问题是正则不可判定的,当开启符之前存在原始构造时,回退机制现在拒绝断定收敛。
相位分裂(三十字节:x <!D y + 换行 + <!DOCTYPE>)。 阻断项 7 污染了未在其自身行上闭合的段落内联 <!--;<?、<! + 字母和 <![CDATA[ 具有相同形态但无规则。两种失败模式叠加。micromark 的块扫描在下一行中断段落,因此位于那里的 doctype 从未到达扫描器的标签扫描,其文档结构污染从未触发。parse5 将整个跨行构造视为一个虚假注释 —— sanitize 阶段移除的节点,合并两侧文本,且合并向后延伸:在 fuzz 反例中合并的分隔符位于构造前 40 字节,在冻结区域内部。这就是为何补全采用文档级污染而非从此处开始污染:尝试过开启偏移污染,经测量未覆盖它。
空行相位分裂(六十三字节,且单个字符追加重写了已冻结区域)。 type-6 html 块在空行处结束;parse5 的 RAWTEXT 状态持续到字面闭合标签。空行之后每行同时生活在两套语法中 —— micromark 开启新块,其元素节点 hast-util-raw 直接压入树中,而相同字节对 parse5 属于 raw text,因此它们的闭合标签什么都不闭合。<iframe> + 空行 + *b*\n<div>…</div>\n</iframe> 使 div 保持开启吞噬文档,而扫描器抑制 rawTextOpen 下的每个标签,称其平衡。在空行处污染;type-1 块免除因为空行不结束它们 —— 在那里两套语法一致,这正是未闭合 <script> 仍能流式的原因。
第六个,来自扩展门禁本身(容器内的 <template>)。 <template> 块完全消失:hast-util-from-parse5 将其子节点挂在 .content 而非 .children 下,sanitize 阶段丢弃该元素 —— 因此没有任何内容到达输出。两侧有空行时该擦除不可见,这正是早期抽样记录 template 为无害时的情况。直接位于列表项或块引用行下,消失的块使容器吞噬后续段落,单字符追加通过惰性延续重写已冻结区域。<template> 起始标签现在全文档污染 —— 文档结构名称之外的第二种擦除类型,也是本周第三次“实测无害”的结论被触及更多布局的语料库所推翻。
第七个,由第六个自身的修复注释所预测。 段落内联 <!-- 污染此前依赖一个代理门禁(htmlFlowSinceBlank),任何 <letter 行首都会设置该门禁 —— 因此 <b>x</b> <!-- comment…(段落;b 不是 type-6 名称)抑制了自身的保护,200 字节中有 173 字节跨越擦除合并被冻结。F9 修复曾刻意保留了该注释污染,写道“语料库携带有若其失效即可捕获的形态” —— 一次扩展压测后,它确实失效了。门禁现在改为代理所近似的精确行首检查,污染像其同类一样全文档化。
三个回归文件(scopeBarrierEndTag, headRoutedCapture, rawConstructPhase),五个新生成器族,以及 GRAMMAR-COVERAGE.md 中的记账行 F7–F12 —— 包括更正了该文件自身的错误引理(曾断言错位嵌套元素“仅通过不平衡的 <b>/<i> 触及,而它们在标签平衡上本就会阻断”)。验证:预检 1547 项测试;记录战役的四阶段全新种子压测全绿;扩展门禁 —— 40 万拼接 fuzz 样本、60 万方向前缀、全穷举 K=4 census —— 在 12 个从未用过的种子上顺利通过。
2.5.4 — 外部内容模型错用计数集合而非语法栈
Section titled “2.5.4 — 外部内容模型错用计数集合而非语法栈”三个已发布的欠阻断缺陷,来自两个不相关的根本原因,均由完成 2.5.3 开启的语料库工作浮现。首先:fuzz 语料库此前从未包含 <svg> 或 <math>,因此 inForeignContent() 在 fuzz 下从未返回 true,扫描器的两个外部分支 —— honoursSelfClosing() 和 htmlRulesApply() —— 自编写以来从未在测试中执行。
增加语料库族用了一个分片就产生了一个缩减到 58 字节的反例:
<svg><b></b><textarea>x</textarea></svg>
[a]: /u
Term其中的 <b> 是一个跳出(breakout)起始标签。parse5 的“外部内容”插入模式在遇到跳出标签时将其外部根从栈中弹出并按 HTML 处理 —— 但扫描器将外部内容深度建模为 tagBalance.get('svg') > 0(名称→计数袋),袋无法表达其从未见过的弹出。它在运行的剩余部分持续报告“仍处于外部内容”,产生两个后果:
htmlRulesApply()持续称“外部规则”,因此<textarea>从未开启rawTextOpen—— 2.5.3 中添加的行内 raw-text 污染从未触发。上一版本的修复被陈旧模型而非新构造绕过。honoursSelfClosing()遵循了 parse5 忽略的标记。 弹出后,<svg><div></div><a/></svg>在 parse5 中使<a>真正保持开启,吞噬其后所有内容,而扫描器完全未计入开启元素。
popForeignRoots() 建立了弹出模型。它仅清除根节点,而规范将每个外部元素弹出到整合点 —— 刻意减少,因为保持外部子元素计数使 openTotal 大于或等于 parse5 栈深度,属于过度阻断的安全一侧。随之修复:HTML_INTEGRATION_POINTS 遗漏了 title,尽管 SVG title 是 HTML 整合点,因此 <svg><title> 内部的自闭合标签在 HTML 规则开启元素时被跳过。(annotation-xml 无条件保留在列表中;仅在其 encoding 为 text/html 时才合规,将其视为整合点始终属于过度阻断)。
不包含 <svg> 或 <math> 的文档逐位完全不受影响 —— 此处的每个分支均在 inForeignContent() 之后设卡,整个边界断言套件报告相同的数值。
首个修复存在一个值得指出的不完整性:它位于 applyTag 中,而 VOID 起始标签从未到达 applyTag —— 调用方跳过了它们。br, hr, img, embed 和 meta 均为跳出名称,因此 <svg><br><a/></svg> 仍遵循了标记。12 个方向测试集分片中有 10 个指出此问题。该形态是第二个已发布的欠阻断,不仅是未完成的补丁:2.5.3 完全没有弹出,因此在那里也是错误的 —— 部分修复未能覆盖它。弹出仅由标签名称决定,且规范在处理标签之前弹出,因此标签本身是否入栈无关紧要;它现在在跳过位置也执行。
第二个不相关家族来自同一语料库工作:parse5 的脚本数据转义状态。 在 <script> 内部,<!-- 进入“脚本数据转义”,嵌套的 <script 随后进入“双重转义” —— 其中 </script> 不再结束元素,仅退回到转义状态。CommonMark 没有此概念:type-1 块在首个持有字面闭合符的行结束。在:
<script><!--<script></script>之后,micromark 称块结束且随后的 $$ 开启流式数学,而 parse5 称脚本仍开启并吞噬之。这是第三个已发布的欠阻断,与外部内容无关 —— 来自同一周的语料库工作,而非同一缺陷。与外部内容不同,此问题无法通过建模消除 —— 扫描器必须选择一种语法而在另一种下出错 —— 因此它触发污染,这正是阻断项 7 存在的目的。12 个 fuzz 分片中的 7 个。
两条其他路径被扫描并确认无缺陷,各自因属于另一阻断项的原因而安全,这也是它们现在被固定而非盲目信任的原因:
- Foster parenting。 直接位于
<table>内部的文本被移出到表格前方,并与已存在的文本节点合并 —— 与 2.5.3 doctype 家族相同的追溯形态。它能维持安全是因为开启的<table>使openTotal大于零,因此在被提升的文本存在之前边界已停在合并目标之前。 - 内容中的
<template>。rehype-raw在带有<template>上下文的片段模式下解析,因此嵌套模板压入另一个插入模式;其子节点落入从未到达 hast 的内容片段中,且 sanitize schema 丢弃该元素。
本版本新增:packages/engine/src/components/incrementalParse/GRAMMAR-COVERAGE.md,逐条枚举了两套源语法 —— CommonMark 的七种 html 块类型及其起始/结束/中断条件,parse5 的标记吞噬分词器状态与元素名称及位置的交叉,以及擦除或移动节点的插入模式 —— 对照扫描器的应对措施以及语料库是否能触及它们。它还记录了偏差记账表和整个分析所依托的基础事实(片段模式、<template> 上下文、scriptingEnabled: false 以及提升未许可元素子节点的 sanitize schema)。
压测套件增加了第四阶段。collectDefLabels 谱系在关闭 mathFlow 和 referenceTaint 下运行扫描器,拥有发布脚本此前从未调用的 fuzz 套件;现在调用了。remarkInjectPhantomDefs 谱系已被覆盖 —— spliceFuzz 的第三属性驱动 runCrossChunk,在每一帧调用 phantomSuffixCloser。
fuzz 语料库扩展揭示了其自身脚手架的脆弱性。覆盖率仪表要求每个生成器家族至少被采样 RUNS/60 次,该底线不随家族数量扩展 —— 因此每个新家族稀释所有其他家族。将 raw-HTML 池从 38 扩充到 49 权重使跨 12 种子的失败率从 1/12 变为 4/12。默认样本数现在为 300 而非 120,在该设置下全部 12 个通过。良性家族的参与底线原为 0.3,收敛值为 0.29-0.32,现调整为 0.2,这仍然能确保崩溃不可漏过。
方法论说明:此处的每个偏差最初都在手写扫描的基础上两次被归类为无害:外部内容的 3140 种形态(“偏离但在下游被吸收”),以及脚本转义家族的进一步矩阵(“真实但稳定,因为流仅会追加”)。两种解读都是合理的,但都是错误的,且在每种情况下 fuzz 语料库在添加家族的一个分片内就推翻了它们 —— 通过将其与链接定义或 $$ 块交叉,这些手写矩阵均不包含的组合。证实偏差无害的扫描是比完全找不到偏差的扫描弱得多的证据,因为手写构建的形态来自产生该偏差的同一种认知理解。
2.5.3 — 堵塞模糊测试语料三处漏洞捕获的四类冻结扫描器欠阻断缺陷
Section titled “2.5.3 — 堵塞模糊测试语料三处漏洞捕获的四类冻结扫描器欠阻断缺陷”全部为修复,全部此前已存在,且以往守护此代码的语料库均无法触及。这项工作始于尝试添加两个生成器家族,最终发现了四个缺陷家族,因为语料库中的盲点与扫描器中的盲点源自相同的一组用例。
语料库漏洞首先被排查,每个均通过整个 784 项测试的引擎套件放行的突变得到确认:
- 完全不存在
<!+ 字母的形态。 生成器中的所有<!形式均为<!--、<!-、<! x >或<![CDATA[—— 注释、虚假注释、CDATA。声明开启符及其跨行传递无法触及。 - CDATA 仅有自闭合形式。 单一生成器为
<![CDATA[<div>data</div>]]> trailing prose,因此跨越行尾传递cdataOpen的分支从未执行。(OVERLAP_OPENERS中的<?x >确实跨行传递piOpen,这也是处理指令此前已被覆盖的原因。) - 原始文本元素仅作为块级运行出现,从未在段落内行内开启并跨越换行符。
这些形态随后暴露出的问题:
-
parse5 擦除文档结构 token,且该擦除具有追溯性。
<!DOCTYPE …>,<html>,<head>,<body>,<frameset>及其闭合标签被 “before html” / “in head” / “in body” 插入模式吸收并不向片段发出节点;两侧的文本节点合并为跨度始于构造之前的一个节点。由于行模型中的每个其他不变式都假设确认的行只影响自身及其后内容,因此后到达的<!DOCTYPE>重写了扫描器已冻结的 hast(围栏/doctype 接缝在索引 1 处将顶层文本节点从"\n"变为"\n\n")。原始重现包含三反引号行、空行、五反引号行、另一空行、
<!DOCTYPE>和e;显式表达这些行避免在发布说明剩余部分开启代码围栏。反直觉的是,未闭合的<body>纯属偶然此前是安全的 —— 它使openTotal保持在 1,因不相关的原因阻断了候选点 —— 因此仅有平衡的形式达到零并冻结跨越。这些名称现在在全文档范围污染;在 38,400 个形态上测量,围栏、缩进块或行内代码跨度内部的 doctype 仍像先前一样精确冻结。 -
带属性的闭合标签吞噬了真实的起始标签。 标签正则在首个
>结束匹配,因此</t <div a="">被匹配为“属性”是<div a=""的一个闭合标签;段落上下文规则跳过了整个跨度。micromark 则不然:它回溯无效闭合标签为字面文本并从其内部重新扫描,其中<div a="">是 parse5 开启的真实 html-text 标签。扫描现在回退越过标签名称而非跳过匹配。 -
Type-1 html 块(
<script,<pre,<style,<textarea)具有不同于其他所有 html 块的块边界,两半均未建模。 Type 1 在持有字面闭合符的行结束而非空行,但htmlFlowReal此前“粘滞于空行” —— 因此</script>之后的行仍被读取为真实的 html-flow 运行,其中 parse5 接受带属性的闭合标签,导致p <div> x </div a="b"> y的</div a="b">被计为真实闭合。micromark 在此处看到新的段落,该文本是字面文本且<div>保持开启;冻结跨越它使文档其余部分全部嵌套在该 div 内部。相反,空行并不结束 type 1 —— 仅有字面闭合符或 EOF 才能结束 —— 因此未终止的块(
<script></script >从不闭合:CommonMark 要求字面的</script>,空格使其变为文本)其原始内容被读取为标记。在该块内部,平衡扫描处于盲目状态,因为rawTextOpen抑制其标签,导致openTotal读取为 0,候选点看起来完全平衡。Type-6 用例作为对照且仍能冻结:type 6 确实运行到空行,因此相同的</div a="b">在那里确实是闭合。 -
行内 raw-text 跨度跨越了两套在其结束位置上不一致的语法。
title、noframes和iframe对 parse5 是 RAWTEXT/RCDATA,对 CommonMark 是 type-6 名称,parse5 将它们提升出文本流。在行内开启并跨越换行符,重写了其所在的段落(p<title>+ 换行 +</title>+ 空行:在任何追加时<p>的子节点在 0-8 处从 [p,"\n"] 变为在 0-19 处的 [p,"\n\n"])。更糟的是,</title a>对 micromark 是字面段落文本,但对 parse5 的分词器是有效的闭合标签,因此扫描器跨越它保持rawTextOpen并抑制了真实解析保持开启的<div>。作用范围经过测量而非假设 —— 40 个标签名称 × 7 种形态,随后 12 × 10 —— 严格限制在这三个名称,且仅限行内、仅限跨越换行符。
值得保留的一条方法论说明:首个 type-1 守卫询问“该运行是否始于本行”,读取 htmlFlowSinceBlank —— 任何 <tag 行首都会将其设置为过度近似。<embed 行(既非 type-6 名称亦非完整 type-7 行,因此对 micromark 是段落)开启了运行并将其背后的真实 type-1 块隐藏。Type 1 可以中断段落;真正关键的问题是真实的 html 块是否已开启。用 htmlFlowReal 替换代理修复了作为独立家族归档的另外两个反例。
普通文档冻结率不变:17 种现实形态 —— 正文、标题、列表、表格、数学公式、围栏、引用、脚注、原始 HTML、闭合的 <script>/<pre>、行内标签 —— 均报告完全相同的边界。成本仅落在真正包含裸块级 doctype、平衡 <head>/<body>、未终止 type-1 块或跨行行内 raw-text 跨度的文档上。
验证:每个修复形态均为确定性固定用例(三个文件中 77 个新回归断言,修复前报红);完整预检 1491 项测试;发布压测三阶段全绿;扩展运行在 12 个全新种子上的 400,000 拼接 fuzz 样本、12 个独立链上的 600,000 方向测试集前缀,以及 K=4 census 从步长 2 采样提升为全穷举 —— 36 个分片,零失败。采用全新种子而非相同链的更长运行是关键所在:此处的每个缺陷都是语料库已能生成但从未抽样到的形态。
2.5.2 — 两处手工维持一致的逻辑合而为一
Section titled “2.5.2 — 两处手工维持一致的逻辑合而为一”无行为变更,无新功能:针对仓库中演化为“记得同时更新两处”的代码进行梳理,并合并了两处此类代码。
- LaTeX 转换链曾存在两份拷贝。
preprocessLaTeX(无状态)与增量预处理器的逐切片函数各自以相同顺序写出相同的八个步骤 —— 后者自身的 JSDoc 将自身描述为“preprocessLaTeX的逐段流水线的精确副本”。两者之间的逐字节等价是核心要求,因为增量包装器冻结无状态输出的前缀,因此向一个添加而未向另一个添加属于正确性缺陷而非整洁性问题。preprocessLaTeX现在成为全字符串提前退出加上委托;提前退出是唯一真正不同的内容并保留在共享链之外,因为切片不能重新决定它(在触发无关切片中的\text{a_b}在完整字符串其他位置携带$时仍能转换)。 <li>尾部扫描曾存在两份拷贝,2.3.0 相关包拆分两侧各一份。两者都编码了关于mdast-util-to-hast的相同事实 ——state.wrap在<li>的块子节点之间穿插\n文本节点,因此有意义的尾部绝不仅仅是最后一个子节点 —— 且两者出于对称原因需要它:core 的聚合页脚在此处追加反向引用,engine 的正文提取器剥离反向引用并必须找到相同节点。每份拷贝记录了一半的原因。两者现在统一归入hastPredicates,拆分已将跨其共享的 hast 辅助工具置于该处。
验证:LaTeX 变动在 115 万样本上对照重构前实现保持逐字节等价(49 个风险片段的手工语料库加上全部 2401 个有序对、25 万片段序列、在定界符字符表上的 40 万原生字符字符串、25 万带 LaTeX 的正文),差分本身经过变异检查。两项变动:完整预检 1414 项测试,LaTeX 属性套件 10,000 次流,发布压测三阶段(5 万 fuzz / 2 万 direction / K=4 census)全绿。
2.5.1 — 恢复流式输出不再破坏 Emoji,单个 HTML 表格不再导致整篇文档禁用冻结
Section titled “2.5.1 — 恢复流式输出不再破坏 Emoji,单个 HTML 表格不再导致整篇文档禁用冻结”全部为修复。2.5.0 遗留的三个事项,每个在触及前均验证为真实缺陷 —— 七个候选中的两个被证实并非缺陷,作为记录保留而非进行修复。
-
恢复的平滑流暴露出破碎的字素簇。
finish(),snap()和完成的flush()无条件确认源码结束,这在当时是正确的:已完成的流应当向调用方交付拥有的每个字节。但工具调用之间的一轮结束随后继续,作为源码结束的偏移量不一定是更长字符串的簇边界 —— 而用于保护确认边界的hold从未看到这些边界。三条左侧上下文规则被破坏:由下一轮补全的孤立高位代理对("👩👧👦x"在 4 处恢复被切分为["\uDC67", "👦", "x"]并显现为破碎的片段)、在奇数位置恢复的区域指示符序列(GB12/GB13 从序列起始处开始配对国旗,因此"🇺🇸🇬🇧🇫🇷"在 2 处恢复将 🇸 和 🇬 误读为一个国旗),以及前导 ZWJ 或组合标记。不再枚举 UAX #29 的左侧上下文规则,恢复现在丢弃计划的积压并从
visibleEnd(唯一确定的偏移量)通过有界回溯窗口重新分段;字素切分是有限状态过程,因此始于簇中间的窗口在一两个簇内重新同步。区域指示符是窗口无法恢复的唯一规则,因此锚点遍历其整个序列。稳态流式零开销。 -
一个格式规范的 HTML 表格禁用了整篇文档的冻结。 表格外部零散的表格部件(
<td>,<tr>,<col>…)改变 parse5 构建后续每个表格的方式,因此 2.4.4 让扫描器从该标签起污染候选点。两处检查均未询问该部件是否真正零散 —— 因此<table><tr><td>a</td></tr></table>将边界从表格起置为 0(相同正文没有它时为 43),且在每个调度上增量帧为零且无法恢复:phasePoisonedAt只会向下移动。包装表格的<details>也会导致该问题。两处检查现在改为位置敏感的检查 —— 扫描器对照tagBalance(跨越放弃机制早已信任的开放元素模型),仅用于抑制污染,因此误数会保留旧的过度阻断行为;拼接层跨 raw 值携带表格深度,因为hast-util-raw将它们全部输入给一个 parse5。五个表格形态现在各自运行 142–183 个增量帧。 -
解析跨片段脚注导致后续每个脚注编号错误。 块缓存的脚注位次对
state.footnoteOrder建模,其位置是协同渲染烘焙到每个标记中作为localNumber的内容。虚拟标签从未进入 footnoteOrder,但位次计算统计了它们:[^A]由另一片段提供而[^B]为本地,B 烘焙的编号为 1,而 A 为虚拟标签且在 A 的定义到达后为 2 —— B 的位次两次均统计 A,保持为 1。相同的原始文本、相同的偏移量、相同的 key,因此缓存持续在编号为 2 的页脚下提供显示为 1 的上标。 -
性能: 被吞噬范围的污染测试从扫描优化为二分查找(在流式热路径上原本为 O(taint nodes × raw-HTML blocks) —— 在拥有 200 个定义和 120 个容器的文档上占
buildBlocks的 14%)。 -
经审查保留的事项,其理由现写入代码防止后续审查反复提交: mermaid 弹窗的同源 blob URL(到达 DOM 的脚本已持有页面的 origin —— 通过
innerHTML插入的内联on*处理程序不是惰性的,仅<script>是惰性的,因此弹窗并未增加 DOMPurify 绕过漏洞所不具备的风险;noopener注释不再声称解决此问题);==highlight==保持 CommonMark 未修补的侧翼规则,其中**和~~在全角标点旁配对而==则不然 ——==既非 CommonMark 亦非 GFM,其价值在于在 Obsidian 和 VitePress 中渲染一致,在此处放宽会破坏该一致性。apps/docs/content/guides/cjk-typography.md记录了该限制和三种有效形态。 -
实测未变动: 合并对 raw-HTML 容器子树的两次遍历。用单纯遍历替换一次遍历的成本与完全删除相同(0.199 ms 对比 0.203 ms),因此遍历是免费的,成本完全在于逐节点工作 —— 合并毫无节约。
验证:每个修复形态均为确定性固定用例,在 2.5.0 上报红;发布压测(5 万 fuzz / 2 万 direction / K=4 census)在最终引擎上全绿;完整预检 1414 项测试。
2.5.0 — 针对 2.4.5 的第二轮审查:五个 parse5 模型差异、LaTeX 盲转义与 documentIndex
Section titled “2.5.0 — 针对 2.4.5 的第二轮审查:五个 parse5 模型差异、LaTeX 盲转义与 documentIndex”唯一的新 API 是 documentIndex(最后一项);其他全部为修复。
针对 2.4.5 的第二轮七维度审查(project-review-2026-08-19-r2.md)—— 其 P1 部分,全部通过等价性预言机复现,外加修复的对抗性复查发现的问题。第一批,仅限引擎:
- P1 — LaTeX display
$$未遵循\$。LATEX_BLOCK_REGEX的展示分支没有转义守卫,而其他每个$读取器都有:Cost \$$x$ each. … | table | … $$将转义的$与远在下方的真实$$配对,并将表格的每个|重写为\vert{}(无状态输出错误,且增量包装器冻结了前缀,随后与全量运行不一致)。两个展示定界符现在严格使用探测器的词法:由偶数个反斜杠前导的$$才是定界符,其他均不重要(首次尝试了(?<![\\$])守卫,但在$$$$/\$$$上不一致 —— 被复查捕获;修复后 64.5 万个随机帧逐字节等价)。 - P1(2.4.5 跨行标签修复的回归)— 跨越换行的属性引号。 跨行标签模型在下一行首个
>处结束标签并在之前的任何引号处污染。parse5 跨换行对属性值进行分词:<hr title="+\n<p></div>使外部 div 保持开启(>是值字节),<div\n class="a">—— 最常见的折行属性形态 —— 在流的其余部分被污染(冻结文档的 0.4% 而非 96%)。扫描器现在跨行携带 parse5 的属性区域状态(outside/ after=/ unquoted / inside"or'):>仅在带引号值之外结束标签;在空行处仍开启的值触发污染。 - P1 — parse5 虚假注释。 在真实 html-flow 运行中,
<!(非<!--/ 声明 / CDATA)和</+ 非字母开启被吞噬到下一个>的虚假注释 —— 其内部的</div>是注释文本;扫描器此前将其计为闭合。 - P1 — type-6 列表中的 RAWTEXT / RCDATA 元素。
title,iframe,noframes,xmp,noembed,noscript对 micromark 是普通 html 块,对 parse5 是文本:<div>\n<title>\n</div>\n</title>在平衡中闭合了外部 div。扫描器现在保持 raw-text 元素开启直到其自身闭合标签,并忽略内部的其他每个标签和注释 token(plaintext永不结束:触发污染)。 - P1 — 孤立
\r对扫描器而言同样是换行符。 2.4.5 修复了拼接的行计数器;扫描器仍仅在\n上切分,因此a\r+ 围栏 /$$开启符位于一个段落行内,候选点落入开启块内部。行现在在\r\n、\r或\n处结束(作为最后一个字节的孤立\r未被确认)。 - 复查发现的既有缺陷: 非 void 元素上的自闭合标志在 HTML 内容中被 parse5 忽略(
<div/>、<title/>、<p/>开启 —— 扫描器跳过了它们并冻结穿过吞噬尾部的 div;对外部内容、其根及其在 HTML 整合点/跳出标签之下的所有内容建模);<div a=<(属性区域中的<)未计为开启(截断标签锚点现在是开启标签名称的最后一个<);标签自身行上带引号属性值内部的>结束了标签(</div a=">—— parse5 继续吞噬到闭合引号;标签行现在用相同的属性值状态机遍历,在该处未闭合的标签将其状态携带到下一行);<svg>/<math>内部的<title>等是外部元素(在那里没有 raw-text 切换,仅在整合点之下才有);noscript曾短暂建模为 raw text —— hast-util-raw 在scriptingEnabled: false下运行 parse5,其内容为普通 HTML(在发布前被批次审查捕获)。 - 既有缺陷 — 带属性的闭合标签对 micromark 而言不是闭合标签。 CommonMark 的 html-text(以及 type-7 flow)仅接受
</name+ 可选空白 +>,因此p <div> x </div a="b"> y是字面文本:parse5 从未看到闭合且<div>包裹其后所有内容,而扫描器统计了闭合并冻结跨越它。携带属性的闭合标签现在仅在真实 html-flow 运行内部应用(type 6 允许它们 —— 自身成行的</div a="b">是 html flow 因为div是 type-6 名称;</span a="b">则不是,属于段落文本)。 - fuzz 语料库:增加 dangling-open-quote / paired-quote / bogus-comment / raw-text-element / lone-CR 族与覆盖率仪表 —— 审查列出这四个盲点作为这些形态通过每次压测的原因。
审查的 P2/P3 项,第二批(无引擎解析层变更):
- 跨片段预检匹配了错误形式的标签。 micromark 在引用的
identifier中保留反斜杠([^a\*b]→a\*b;仅label被反转义)且注册表键由此构建 —— 但 PASS 0.5 子串预检首先对源码进行了解转义,因此包含转义标点字符的标签永久未命中,跨片段引用字面渲染。normalizeForMatch现改为normalizeId(2.4.5 的收窄保留了错误的前提)。 - LaTeX 处理: 货币转义奇偶性检查在每次匹配时重新扫描已处理的行 —— 在包含大量货币命中的行上为 O(line²)(8 KB 行:每帧 100 ms,现在 1 ms,增量裸
$计数器);增量尾部遍历在尾部以$$开头时运行两次未闭合$$扫描(流式展示块的稳态)。 - Sanitize schema: 引擎导出的
sanitizeSchema深度冻结(README 称其为只读单例;tagNames.push('script')过去会在进程范围拓宽安全清洗器 ——extendSanitizeSchema草案保持可变);raw-text 元素(style,title,textarea,noframes,noembed,xmp,iframe,plaintext)与其内容一同剥离而非解包(<style>a{b:c}</style>不再留下a{b:c}作为正文文本);注意事项说明添加标签即放行其全部语义。 - 协同脚注: 片段的首帧(尚无片段符号)显示已知全局编号而非空白(SSR 曾输出无锚点的页脚反向引用);聚合页脚的树仅由最后一个片段构建(每个片段此前在每个注册表版本上重建它 —— 在挂载时为 O(N²))。
- Mantine: mermaid 在
mermaidAPI.getConfig()报告非 strict 时重新断言securityLevel: 'strict',不仅在主题切换时(为click交互变为loose的宿主否则会将未净化的 SVG 输入我们的innerHTML);浅色卡片使用方案独立的主题变量(--mantine-color-black/-white),使带有colorScheme="light"的深色 Provider 保持其图标可见,并恢复了深色悬停规则;JSON 美化输出展开作为 JSON 文档的字符串值(工具调用转录形态)但不再将"true"/"123"/"null"重写为其他类型 —— 移除deep-parse-json改用小型局部展开器;mermaid“在新窗口打开”动作是真正的头部按钮且 SVG 容器不携带 ARIA role(role="button"和role="img"均会使 mermaid 自身的graphics-document语义和 accTitle/accDescr 变为纯展示性);定义列表使用逻辑属性(RTL 镜像对称)。 - 发布工作流: 预发布版本(
x.y.z-beta.1)发布在其 pre-id dist-tag(beta/rc/next)下而非占用latest;统一版本发布 tag 检查同时校验 core 对 engine 的精确workspace:*锁定,以及 engine 展开的remark-mark-highlight版本已存在于 npm 上。 - 引擎 API 说明:
lastRegionStart和DEF_LINE_START_RE(@internal,“仅供测试导出”)离开引擎包根导出 —— 它们曾可通过export *访问;仓库或文档中没有任何内容从入口使用它们。 - 文档 / 仓库整洁性:
custom-components.md说明在 Mantine 下覆盖pre同样意味着customComponents.code从不挂载于围栏块;泛型指南中的sanitizeSchema门禁锚点与门禁编号;#install徽章链接;design-tokens.md中的深色变量示例匹配发布值;SECURITY.md/apps/docs/content/guides/index.md要求精确版本;针对忽略 exports 映射的解析器的core/plugins/package.json存根;assert-dist-clean覆盖dist/plugins/*;.gitignore覆盖.impeccable/与调度器锁;pnpm bench运行 Vitest 微基准测试,benchmark.md说明哪些数值属于带日期的快照;collectDefLabels的仅测试辅助函数离开包根;段落截断的<td b仅在后续>确认其为标签后才污染;LaTeX 代码跨度搜索像扫描器一样识别 CR / CRLF 空行。
第三批 —— 审查归类为 P2 的缓存/流式缺陷:
-
未闭合 raw-HTML 容器脱离了逐块指纹。 在流式中途,rehype-raw 将后续同级节点重新归入未闭合
<details>/<div>中;容器自身的 mdast 节点是空html节点,因此派生出的每个污染事实均为空,块回退到无指纹缓存 key。容器前等长的编辑可能改变其内烘焙的脚注编号,而过期子树保持缓存(标记解析为 null 全局序号并消失)。容器现在从其实际吞噬的 HAST 子树读取污染指纹 —— 占位符标签与烘焙的
localOccurrence/localUrl、独立脚注标记 —— 这正是缓存的 ReactNode 所持有的内容。被吞噬的独立链接或图片引用渲染为普通<a href>/<img src>,子树扫描无法将其与行内链接区分,因此当源码中的任何引用或定义落入被吞噬范围内时,该块被额外标记为已污染 —— 否则位于容器之前的定义(在摘要哈希范围之外)可能在每个缓存 key 组件未变的情况下改变渲染的 href。 -
…并且它也吞噬了脚注部分。 容器内合成的
<section data-footnotes>不是顶层规划项,因此协同模式的“跳过本地页脚,由聚合页脚渲染”规则从未触发:文档展示两个<section data-footnotes>且liid 碰撞,标记的href有两个候选目标。协同渲染现在从该块子树中剥离被吞噬的部分;独立渲染保持其在全量解析放置的位置。 -
平滑流式:在代理对内部切分的帧暴露出部分字素簇。 孤立高位代理对自身是一个字素簇,因此其前面的簇被确认 —— 补全的代理对随后合并两者,使展现的前缀留在合并簇内部(在 UTF-16 索引 5 处切分的
👩👧👦x展现为👩;👍+ 肤色、区域指示符国旗对亦然)。模块契约声明此情况绝不发生,而仓库自身的流式模拟器按 UTF-16 索引切片,因此这是默认路径。当源码在悬空高位代理对处结束且前序可吸收补全字符时,两个簇现在保持暂定状态。 -
新增可选属性
documentIndex— 片段排序不再取决于挂载顺序。 跨片段状态(脚注编号、由哪个片段渲染聚合页脚)遵循片段在注册表中注册的顺序,此前该顺序即挂载顺序。虚拟化列表会打破这一点:滚动出视图的消息卸载,滚回时在保持挂载的片段之后重新注册 —— 导致脚注重新编号且页脚移动到文档中间(useId派生自位置,因此重新挂载的片段无法自行收回其槽位)。传入任何稳定的单片段序号,注册表便按其对片段排序(注意:该属性仅用于对同一逻辑文档内的片段引用与跨片段状态进行排序,并不控制多文档之间的前端展示顺序)。该属性为可选,省略时完全保持先前的行为;提供索引的片段排在未提供索引的片段之前,使部分灰度发布可预测降级。跨片段指南中的“规划中”说明现已成为正式记录的属性。
验证:每个修复形态均为确定性固定用例(在 2.4.5 上报红);发布压测(5 万拼接 fuzz / 2 万 direction / K=4 census,10,000 次流的 LaTeX 增量属性套件)在最终引擎上全绿;对修复进行了五次预言机检查。
2.4.x — 项目全面审查治理
Section titled “2.4.x — 项目全面审查治理”
2.4.5 — 全面审查 2.4.3:截断闭合标签清零标签计数、孤立 CR 行号与跨段落代码跨度
Section titled “2.4.5 — 全面审查 2.4.3:截断闭合标签清零标签计数、孤立 CR 行号与跨段落代码跨度”(没有 2.4.4——跳过了该版本号。)在 2.4.3 进行了六个维度的审查(engine 解析、engine 预处理器、core、mantine + 插件、安全、基建与文档);P1 问题在修复前均通过等价性预言机复现。包括 1 项 P1、5 项 P2、若干 P3,以及针对本批次变更进行对抗性审查发现的问题:
-
P1 — 行截断的闭合标签被就地计数。
para </style(行内无>)立即递减了平衡计数,而开启方向的截断自 2.4.0 以来一直处于挂起并在行尾恢复的状态。对 micromark 而言该文本是普通正文;对 parse5 而言<style>元素仍处于开启状态(RAWTEXT 等待>)——因此下一个空行被判定为平衡,边界切穿了仍处于开启状态的元素(拼接:尾部成为一个段落;全量解析:尾部作为 raw text 被吞噬——<textarea>亦然;容器标签恰好被随后的分隔符计数兜底捕获,代价是永久进入全量解析谱系)。现在截断的闭合标签绝不在出现处计数:在段落上下文中作为普通正文处理(下一行的块缩进
>属于引用块;补全标签所需的 4+ 空格延续格式不作建模——过度阻断);在 html-flow 运行中被挂起,仅当后续行带来>时应用(parse5 在此处完成闭合标签);在空行处直接丢弃而不应用——元素保持计数状态,过度阻断。模糊测试语料补充了截断闭合模式(proseTruncatedClose仪表)——此前仅包含截断开启模式,这也是 fuzz 与压测此前从未发现该问题的原因。 -
P1(由本批次对抗性审查发现,此前已存在)——截断标签之后的下一行在首个
>之前均属于属性垃圾内容。 在 html-flow 运行中,以未完成标签(<div、</div、<br——开启、闭合或空元素)结尾的行会使 parse5 的词法分析器停留在该标签内:下一行直到首个>为止的所有内容均属于该标签,因此此处看似真实的</div>实际上补全了该标签,并未闭合外层元素——扫描器此前将其计为闭合并冻结穿过了仍处于开启状态的<div>/<details>(<div>\n<div>\n</div\n</div>、<div>\n<br\n</div>;在 1 字符切片下可见,在较粗切片下因前一帧已吞噬触发兜底而掩盖)。扫描器现在跟踪“处于跨行标签内部”状态——仅当运行属于真正的 micromark html 块起始(类型 6 / 类型 1 / 不中断段落的类型 7;以
</i或<br开头的段落不在此列,且其下一行的<div>/<!--属于真正的块——预言机复测捕获了较宽松门禁导致的错误吞噬):后续无>的行完全不进行标签扫描;包含>的行补全挂起的闭合并仅扫描其后的文本;在>之前出现引号会导致污染(parse5 处于属性值内部——无法预知哪个>结束标签),缩进低于截断行同样会导致污染(列表项的 html 块可能在此处结束,hast-util-raw 重置词法分析器,下一行的<div>属于真正的块)。模糊测试语料增加
crossLineTagGarbage族。此外来自发布压测方向测试集(此前已存在):零散的表格组件标签(<td>、<tr>、<col>…)现在从该标签起污染扫描器的候选切分点——拼接层此前在冻结前缀中已对其进行兜底放弃(2.4.3),因此输出未受影响,但<td>前缀此前仍可能“冻结”一个 parse5 结构依赖于尾部的表格,导致每一帧都付出扫描 + 尾部解析 + 拼接尝试的开销,随后被兜底丢弃。 -
P2 — 孤立
\r属于换行符。 拼接层的行号重基线此前仅统计\n;micromark 将\r、\r\n与\n各计为一个换行符,因此冻结前缀中的每个孤立\r都会导致尾部的position.line少计 1(偏移量与结构正确——读取position.line的使用者获取到错误行号)。2.4.2 在扫描器中剥离了 CRLF 的\r;现在的计数器对三者均与 micromark 保持一致。 -
P2 — LaTeX 预处理器:代码跨度不能跨越空行。 不同段落中的两个孤立反引号此前被配对为同一个代码跨度,它们之间的所有
$…$均未被转换(渲染为字面美元符号)。闭合标记搜索现在在首个空行处停止——与 2.4.2 的代码围栏闭合修复形成行内对应。 -
P2 — 协同块缓存指纹纳入片段局部脚注状态。 缓存节点此前固化了引擎针对脚注标记的每个片段
localOccurrence/localNumber(在前序引用上的递增计数器);上游等长就地编辑([^x]→[^w])使后续块的原始文本、位置与注册表结果保持不变,过期的出现序号解析为 null 全局序号——标记消失且无法恢复。指纹现在携带每个引用标签的先验出现计数与首次出现位次(JSON 编码,fl:部分),在文档顺序下按每个引用节点在 mdast 上预计算一次——与引擎计数器保持一致(定义体在页脚渲染并跳过;先遇到的定义参与位次计算;将多个 hast 同级节点映射到单个 mdast 节点的范围回退块不会重复计数)。纯追加流式此前从未受此影响。 -
P2 — 增量 LaTeX 包装器:冻结失败进行退避,孤立反引号不再导致后续流式永久放弃冻结。 当冻结候选区永不静止时(前期的零散
$如US$使后续每个切片的定界符奇偶性保持奇数;孤立反引号将悬空运行风险锁定在整个活跃区域),每次追加都需要执行三次全文档处理——在流式传输 600 次的 42 KB 文档上达到无状态开销的 3.2× / 1.8×,恰好发生在优化本应适用的文档上。实施了三项改进且无 API 变更:失败的尝试现在等待活跃区域翻倍后再进行下一次尝试(失败尝试开销呈几何级数;延迟冻结不改变输出——每帧依然严格等价于preprocessLaTeX(full));反引号风险在空行处释放(代码跨度不能跨越空行——见上述预处理器修复——因此空行会平息其前的所有运行,其后的行再次成为安全切分点);尾部遍历跳过从未读取的静止探测。测量结果:零散
$降至 1.03×,孤立反引号降至 0.06×,纯净文本降至 0.06×。审查期间曾设计并实现更短的“回退切分”,后被放弃:静止性按片段划分(普通正文为单一片段),出现问题前的未冻结前缀无论如何均低于尝试阈值——仅退避机制即可达到无状态开销水平。测试:@internal的onAttempt钩子锁定对数级尝试上限与冻结推进;等价性测试套件在关闭退避(@internal backoff: false)下运行,以确保 1 字符切块持续覆盖每条切分规则。 -
P2 —
esbuild覆盖设置上限(>=0.28.1 <0.29),与文件中的其他覆盖规则一致——esbuild 为 0.x 版本,重新解析 lockfile 可能会将经过验证的 0.28 替换为未经测试的 0.29。 -
P3:
normalizeForMatch仅剥离反斜杠 + ASCII 标点转义(标签中的字面\曾导致跨片段预检遗漏);货币转义处理在每次匹配时不再将后续全部内容切分为行;标签已编号但在注册表中缺少自身出现记录的脚注标记——瞬态(后挂载的片段重复该标签)或永久(脚注定义体内部的引用:贡献收集跳过定义体)——现在展示全局编号(不带 id——无页脚反向引用指向它,且片段局部 id 会与另一片段的真实标记冲突)而不是完全不渲染;mantine 的pre覆盖支持字符串className;mermaid 的“查看 SVG”窗口使用noopener打开;mermaid 暗色头部图标使用静态深色变量(浅色 Provider 下显式设置colorScheme="dark"曾导致其不可见);发布工作流同时校验 mantine 的 core peer 范围随发布版本线前移;引擎 README(katex属于可选 peer;管线表格仅列出导出的符号)、SECURITY.md(受支持版本;urlTransform={null}不是逃逸通道——它表示默认行为)、eslint 配置指示。
刻意未作修改:工作流 action SHA 锁定与 packageManager 完整性哈希——供应链强化组在 2.4.0 中已明确按既定设计拒绝修改。
验证:每个修复的形态均为确定性锚定用例(在 2.4.3 上报红);发布压测(5 万次拼接 fuzz / 2 万次方向测试集 / K=4 遍历,加上 def-label 3 万次专项与 10,000 次流式的 LaTeX 增量属性测试套件)在最终引擎上顺利通过。
2.4.3 — 全面审查 2.4.2:注册表原生存储 URL、两处 parse5 树构建特异性与嵌套列表围栏
Section titled “2.4.3 — 全面审查 2.4.2:注册表原生存储 URL、两处 parse5 树构建特异性与嵌套列表围栏”对 2.4.2 进行了七个维度的审查(engine 解析与运行时层、core、mantine + remark 插件、安全清洗专项、基建与文档)。无 P0;四项 P1 全部复现并修复:
- 跨片段定义 URL 原生存入注册表。 贡献收集时的
urlTransform预处理此前将协议拦截的 URL 折叠为'',导致渲染时门禁无法区分是被拦截(属性缺失)还是合法的空路径(href="")——一经存入注册表就破坏了 2.4.2 的区分(在用户面前 DOM 从<a>跳变为<a href="">)——并且独立运行时仅执行一次的重写转换在此处执行了两次。渲染时的sanitizeCrossChunkUrl门禁现在成为唯一的执行点;extractContributions不再接受urlTransform选项(engine API)。 - 拼接 — parse5 树构建特异性。 在全量解析中,零散的
</br>变为<br>,零散的</p>变为空的<p>(HTML 的两处闭合标签合成特例);仅解析尾部时开启的模式会在首个元素起始标签前丢弃它们,导致增量帧丢失该元素(x\n\n</br>\n\ny,前置注释或 PI 后面亦然)。在任何表格外部零散出现的表格组件标签(<td>、<tr>、<col>…)会改变后续所有 GFM 表格的构建方式(单元格文本脱离原结构被提升至根节点,表格骨架丢失)——影响贯穿文档剩余部分。两者现在均回退到全量解析:尾部前导 html 块运行中的任何此类标签,以及冻结前缀中任何位置的表格组件标签。 - LaTeX 预处理器 — 支持任意缩进的代码围栏。 深入列表两层的围栏位于第 4 列;0–3 空格规则(从第 0 列而非列表容器起算)拒绝了该围栏,导致块内的
$被重写(~~~块受影响;反引号围栏仅凭巧合幸免)。开启标记支持在任意缩进下被接受;闭合标记深度超出开启标记不得超过 3 列(CommonMark 容器相对规则——第 0 列围栏内的 ` ```` 行属于正文内容,将其视为闭合会泄露块的正文)。缩进代码块依然不作建模(在缺乏容器模型的情况下无法与列表延续段落区分),并作为已知限制加以标注。 - 文档:
Registry.resolveLinkDef(label).url现已明确标注为原生 RAW 数据——跨片段与 URL 清洗指南中的用法在渲染前会先进行清洗,resolveLinkDef的 JSDoc 中亦作了说明。 - P2/P3: 原生 HTML 块缓存摘要对被吞噬范围的源码进行哈希(未闭合
<details>内部等长的单帧替换曾发生过期缓存命中);失败的highlight.js下载与 mermaid 一样支持重试;ci.yml声明permissions: contents: read且其覆盖注释与脚本保持一致;sanitizeCrossChunkUrl将显式的protocols: undefined视为与上游一致的无限制;snap()明确记录为不受尾部字素暂扣承诺保证;“默认 schema 未作为值导出”的说法限定于@ai-react-markdown/core(engine 以只读方式导出);记录了 mermaid 共享单例securityLevel前提。
验证:生成器族 treeQuirkArb + 遍历词法单元 </br>(2.4.2 拼接在新的锚定用例上失败),发布压测(5 万次拼接 fuzz / 2 万次方向测试集 / K=4 遍历)在最终引擎上顺利通过。
2.4.2 — 全面审查 2.4.1:两处隐性等价分支与字节一致性遗留缺陷
Section titled “2.4.2 — 全面审查 2.4.1:两处隐性等价分支与字节一致性遗留缺陷”对 2.4.1 整个仓库进行了五个维度的只读审查(带真实插件链 fuzz 的 engine 解析层、engine 流式层、core、mantine + 插件、基建与文档)。发布门禁全绿且两项 2.4.1 修复保持稳定;审查发现了破坏“拼接等价于全量解析”承诺的两处 P1 问题以及一系列细部缺陷。全部修复:
- P1 — Unicode 空白行被误作为普通空行处理。 冻结扫描器此前使用 JS
trim()判定空行;而 micromark 仅识别 U+0020 / U+0009。仅包含 U+3000(全角空格——CJK 输出中常见)或 NBSP 的行属于段落的惰性延续,导致切分边界落入未完成段落内部(拼接:两个<p>;全量解析:一个<p>)。围栏/数学公式闭合标记判定也存在同样的 JS 与 micromark 差异(```␠NBSP不是合法的闭合标记)。两者现在均采用纯 ASCII 判定。 - P1 — 跨越软换行的引用标签未被污染标记捕获。
see [foo\nbar] end此前从未匹配按行进行的括号扫描,段落被冻结,随后的[foo bar]: /url将已冻结的字面文本重新定向为链接。扫描器现在将段落未闭合的[跨行传递并污染合并后的标签(过度阻断安全;去除引用块延续标记>以便标签仍能解析)。 - 针对这些修复进行对抗性审查的后续跟进: 扫描器中剩余的所有 JS
trim()均改为纯 ASCII 判定(以 NBSP 结尾的定义余项此前会注册幽灵定义;段落内联<!--前的全角空格此前跳过了分歧污染;标签规范化现在与 micromark 逐字节一致),解析失败的行内链接([foo](bad url)——裸目标中包含空格)保持[foo]作为 micromark 所构建的实时快捷引用的污染标记,而不是作为普通链接被跳过。 - 拼接: 尾部闭合标签被 parse5 丢弃的 html 块(
</details>\n</details>)会留下带位置的纯空白残留,全量解析随后会将其合并到接缝分隔符中;切分处现在回退到全量解析,而不是进行错误的重建。rebaseTree容忍使用者插件半构建的position,避免在渲染路径中抛出异常。 - 拼接(来自发布压测,在 2.4.1 中已存在): parse5 直接丢弃的 html 块(零散的
</details>行)不产生节点,因此其周围的分隔符会合并为单一文本——尾部重建输出了两个文本节点,其后无位置信息的 KaTeX 块与被丢弃的块配对并被冻结了两次。两条路径均回退到全量解析。 - LaTeX 预处理器: 带尾随文本的围栏行(
``` not-a-closer)不再被视为闭合标记,info 字符串中包含反引号的反引号围栏不再被视为开启标记——旧规则此前导致整篇文档后续的开启/闭合相位颠倒。 - 字节一致性,协同模式与独立模式: 段落中单独存在的带链接图片(
[][ref])同样通过链接占位符解包;合法的空目标([x]: <>)保留href="",仅在协议拦截时移除属性(此前两者发生折叠);解析出的脚注标记与服务端分支一样输出data-footnote-ref="";块缓存指纹不会因 URL 或标题内部的|发生冲突;包含半构建 mdast 位置的块在未缓存状态下渲染而不是消失;已释放片段符号下的贡献收集被忽略,以便注册表正常清空。 - Mantine: 语言自动检测在非流式替换后重新排期(此前永久停留在 “unknown”);JSON 美化输出门禁要求括号平衡,避免几乎每个流式片段都重复解析美化后的 JSON;流式结束时
import('mermaid')失败会保留当前视图并在受控次数内重试下载,而不是显示永久渲染错误。 - 引擎 API 说明:
sanitizeCrossChunkUrl(engine 入口)现在返回string | null——协议拦截返回null(渲染时移除属性),合法空目标返回''。@ai-react-markdown/core精确锁定 engine 版本;直接导入 engine 的调用方会看到放宽的类型。(自 3.0 起已弃用:适配器改用resolveCrossChunkReference解析跨片段引用,它还会重定基 hash 链接并对最终元素做净化;sanitizeCrossChunkUrl在 3.x 内保留导出。) - 基建 / 文档: 统一版本发布的包 tag(
core-vX.Y.Z)被发布工作流拒绝(这会绕过统一版本发布检查);qs/brace-expansion覆盖设置获得策略规定的上限;四个相关包声明engines.node >=20;CRLF 行在扫描时去除\r,使所有行锚定规则判定结果与 LF 一致;修复版本脚本头部、RegistryInternal导出注释、remark-mark-highlight 一致性用例措辞,以及 core README 关于blockMemo输出等价性的说明(仅限独立模式)。流式游标动画名称派生自样式指纹(升至v2,避免混合版本的页面解析到其他版本的关键帧)。
刻意未作修改:snap()(内容替换)依然不触发 onSmoothDrained——这是既定文档契约;LaTeX 单字符 lookbehind 与货币转义逻辑保持原样(未构建出用户可见错误)。
验证:生成器优先——新模糊测试族(unicodeBlank、crossLineRef)与遍历词法单元使 2.4.1 扫描器在修复前自报两项 P1;每个修复形态均为确定性锚定用例(在 2.4.1 上报红);发布压测(5 万次拼接 fuzz / 2 万次方向测试集 / K=4 遍历)在最终引擎上顺利通过。
2.4.1 — 2.4.0 发布后审查:两处引擎回归与 SSR 字节一致性漏洞
Section titled “2.4.1 — 2.4.0 发布后审查:两处引擎回归与 SSR 字节一致性漏洞”对 2.4.0 的变更进行了第二轮对抗性审查(针对新引入结构的探测用例与小型 fuzz,所有失败用例均最小化并在 2.3.3 上重放,以区分回归问题与既有缺陷)。修复了两项回归与一系列既有漏洞:
- 回归 — 虚拟后缀闭合标记可能误开启代码块。 2.4.0 在追加协同哨兵定义前闭合了开启的围栏/数学公式块;对于在列表项内部开启、已被后续反缩进内容终止的围栏,或者被扫描器无法识别的 ≥4 空格闭合标记闭合的围栏,发出的闭合标记在哨兵行周围开启了新围栏(哨兵文本进入代码块,跨片段引用丢失——比被修复的问题更严重)。闭合标记现在仅针对可证明属于顶层的第 0 列开启标记发出;仲裁器测试工具断言每个闭合标记均对输出呈中性。
- 回归 — 截断标签恢复可能遗漏真实标签, 其
>位于代码跨度掩码内部(<div x="\”>b`:micromark 优先解析标签),并在计入虚拟开启时跳过了接缝残留检查。封堵了两处欠阻断;<位于所有掩码之外的真实标签即使其>` 位于掩码内部现在也会被正常计数。 - 修复的既有欠阻断: 被原始构造触及的行上的标签此前从未被扫描(终止符后的
?><details>,开启符前的<details> <?php);零散闭合标签后的纯空白浮动残留计为接缝残留;紧随已冻结元素或定义之后的零散闭合标签的丢弃标签残留此前被拼接重复(切分处现在仅当其属于该 html 块自身的文本时才冻结尾随字面量;以此类块开头的尾部合并其残留,以被丢弃<div开启标记开头的尾部回退到全量解析而不是猜测——后两者在形态纳入 fuzz 语料后由发布压测暴露);终止符提及守卫遗漏了[ __aimd_…]。 - 协同占位符在 2.4.0 声明中遗漏的四种情况下与独立模式逐字节一致——脚注 id 像 mdast-util-to-hast 一样进行百分比编码(
[^注]/[^a%b]锚点正常工作;聚合页脚使用相同编码),段落中单独存在的图片引用完成解包,URL 经过normalizeUri处理,被拦截的 URL 移除属性而不是渲染为href=""。其中两项自协同功能发布以来在客户端解析路径上同样存在错误。 - Mantine: 语言自动检测在片段流式途中被替换(重新生成)时重新启动,而不是保留前一个块的标签;懒加载
import('mermaid')/import('highlight.js')失败后支持重试,而不是永久缓存错误。 - 文档:
--aim-spacing-sm使用方、mantine 标签页标签小写化。
验证:发布压测(5 万次拼接 fuzz / 2 万次方向测试集 / K=4 遍历)在最终引擎上顺利通过;每个修复形态均为确定性锚定用例。
2.4.0 — 全仓库审查发现的 63 项问题集中修复
Section titled “2.4.0 — 全仓库审查发现的 63 项问题集中修复”对四个包(engine / core / mantine / remark-mark-highlight)、Storybook 测试套件、文档与发布管线进行了全面审查,共产生 66 项发现——无 P0,6 项 P1。本版本解决了其中的 63 项(两项供应链强化按既定设计拒绝修改;一项样式问题作为刻意的视觉设计选择予以保留)。按使用者可见表现分组:
增量解析(engine)— 正确性
-
三处冻结边界欠阻断,均发生在 fuzz 生成器无法触及的形态中。 行扫描器参考 micromark 实现了注释/PI 终止符建模,但遗漏了与开启符重叠的闭合符(
<!-->,<!--->,行首<?>),忽略了决定最终 HTML 的 parse5 与 CommonMark 产生分歧的场景(--!>仅对 parse5 闭合注释;<?…/<![CDATA[…虚假注释在首个>处结束),并将 micromark 拒绝其目标的链接定义([a]: /u(x,[a]: <u<v>)误注册为提前释放引用污染的幽灵定义。每一种都会导致边界冻结穿过真实开启的
<details>(吞噬类问题)或导致后续真实定义重新定向已冻结文本。重叠闭合符现在就地闭合;两种语法之间的分歧会污染相位(粘滞过度阻断,绝不猜测);目标校验运行 micromark 自身的语法。修复采用生成器优先方式:扩展 fuzz 族与穷举遍历字符集,使旧扫描器在 ~30 个样本内报错——进而暴露了另外两处既有欠阻断(<!-- c --> tail接缝残留;隐藏在已开启注释内部的相同重叠-->),并在对抗性审查中发现了两项跟进问题(目标中的反斜杠转义;重叠闭合符后的残留)。每个反例均为确定性锚定用例。
-
if x<y then正文不再导致后续流式永久禁用拼接。 行截断的<x此前被永久计为开启标签;段落行截断现在在段落空行处恢复,除非有>加以确认(html-flow 截断保持计数——parse5 确实会持续对其进行词法分析)。 -
拼接守卫:终止符提及守卫像 micromark 标签匹配一样进行大小写折叠;注入遍历使用过滤语义,在插件树位置混乱时回退而不是静默跳过定义。
-
验证: 发布压测(跨 12 个随机种子的 5 万次拼接 fuzz、2 万次方向测试集、在扩充的 27 词法单元字符表上跨 12 个分片的 K=4 穷举遍历)在最终引擎上顺利通过;fuzz 语料增加了重叠终止符、raw 文本块、无效目标与正文截断族,并配备覆盖率仪表。
跨片段协同
- 带填充 / 软换行的引用标签在协同模式下正常解析。
normalizeId现在委托给 micromark 的normalizeIdentifier(修剪 + 纯 ASCII 空白折叠);此前[ foo ]或在软换行处折断的标签使用' FOO '查询以'FOO'为键的定义并回退为字面文本——仅发生在协同模式下。 - 虚拟定义后缀在未闭合代码围栏中幸存。 以未闭合 ``` 或
$$块结尾的流式帧此前会吞噬追加的哨兵定义——表现为代码块内部出现哨兵文本,且片段内的每个跨片段引用在整块中回退为[text][label]。引擎现在仅在扫描器相位可信时,首先为开启块发出输出中性的闭合符(相同围栏字符/长度,开启标记缩进)。 - 协同服务端渲染保留其标记与链接。 在注册表为空时(服务端、客户端首帧),占位符现在回退到片段自身的独立事实——局部脚注编号、自身的链接/图片定义——因此包装片段的服务端输出与独立渲染逐字节一致(已锚定)且水合无不匹配;一旦片段完成注册,文档级编号与规范链接即接管。在此之前跨片段引用依然字面渲染(已记录在文档中)。
- 运行时属性变更不再在缓存后遗留过期输出:在运行时切换
preserveOrphanReferences会刷新合成页脚(策略变更时清空块缓存);旧版blockMemo: false路径同样应用孤儿保护,因此blockMemo在独立渲染中真正保持输出不变性;在运行时更换 sanitize schema / rehype 链会将脚注体重新发布到注册表(贡献指纹通过全同性比对解析输入链)。 <AIMarkdownSmoothStream smoothCoordination={null}>遵循全库 null 规则回退为默认值(true),而不是静默关闭轮候呈现。Registry.releaseSymbol忽略不平衡的多余释放,避免引用计数跌入零以下导致片段泄露。
流式行为
useSmoothStream().flush()在流式途中保持字素纪律:展现所有已确认内容,并严格按动画方式保留尾随字素(代理对半边或正在生成的 emoji ZWJ 序列绝不进入解析器);streaming={false}或下一次追加完成确认。- 标识翻转开发警告对间隔超过 10 秒的翻转重新计数,因此相隔数分钟的三次合理主题切换不再被提示为“在每次渲染中变化”。
clobberPrefix按文档 id 记忆化。
Mantine 相关包
mermaid与highlight.js实现按需加载。 ~1.5 MB 的 mermaid 模块此前在默认pre组件链上属于静态导入——每个使用者的主包都被迫携带它;现在仅在渲染首个图表时加载(源码视图覆盖加载期间),且完全不再导入根highlight.js入口(所有语言)——Mantine 自身的适配器已将未知语言降级为纯文本,自动检测在初次需要时加载完整构建。偏好预加载的应用可在启动时调用新的preloadMantineCodeAssets()(或自行导入模块)。- 自动检测按翻倍节奏运行(约 32 字符时初次推断,块大小每次翻倍时修正重跑,流式结束时最终判定),而不是在每个流式片段上对每种语言重新评分;JSON 美化在代码块看似完整时立即生效,而不是在未完成块的每个片段上运行。
- 围栏语言匹配不区分大小写(
```Mermaid正常渲染图表);弹窗拦截器返回 null 时撤销 SVG 对象 URL;图表类型标签使用 Mantine 浅色主题变量;重新断言与其他实例主题重新初始化产生竞态的渲染;codeBlock组中的显式undefined不再穿透默认值;代码文本拼接不人为插入空行。MermaidCode 流式契约现在拥有对照真实渲染器的浏览器级 QA 测试用例。
文档、测试与基建
- README/文档修正:包装器 API 表格补充
smoothTurnTaking、感知定义的游标行为、contentPreprocessors属于 WARN_ONLY、设计变量默认值与使用方、SmartyPants 替换、mantine 默认导出、包列表表格包含remark-mark-highlight,以及片段重新挂载的虚拟化注意事项(注册顺序决定编号与页脚位置)。 - 测试:CJK 回归测试共享统一定位用例与断言;CrossChunkStress 在 DOM 上断言其头部声明;remark-mark-highlight 锚定与 remark-gfm 的交互;两个 QA 测试用例驱动运行时属性翻转穿透真实 React commit。
- 发布管线:统一版本发布 tag 校验所有三个同步版本;发布任务像 CI 一样运行根类型检查;发布说明提取输出告警而不是静默回退;core 与 mantine 打包附带 LICENSE;所有四个 exports 映射共享统一结构并导出
./package.json;mantine 构建断言 dist 中无process.env。
2.3.x — 框架无关的 Markdown 引擎
Section titled “2.3.x — 框架无关的 Markdown 引擎”
2.3.3 — 脚注定义先于引用出现与空白 fontSize
Section titled “2.3.3 — 脚注定义先于引用出现与空白 fontSize”修复了两处边界缺陷:
首先,在默认开启 preserveOrphanReferences 的独立渲染中,出现在其首次引用上方的脚注定义会被注册两次:孤儿保护逻辑与 mdast-util-to-hast 的默认引用处理器通过两个不同的状态判断“是否已注册”(footnoteOrder 与 footnoteCounts),导致页脚输出重复的 <li> 且 DOM id 冲突,上标标记按数组长度而非位置编号。孤儿保护逻辑现在在推进顺序的同时将引用计数器初始化为 0——是零而非一,因此从未被引用的定义依然不发出悬空反向引用,纯孤儿输出保持逐字节一致。协同渲染(AIMarkdownDocuments)与增量拼接重放经证明完全不受影响;该修复附带独立模式回归测试集以及全三项发布压测(5 万次拼接 fuzz、2 万次方向测试集、K=4 穷举遍历)的顺利通过记录。
其次,fontSize=""——来自被清空的文本输入或未声明类型的调用方的空字符串——此前被原样传递给 --aim-font-size-root。在 CSS 中空自定义属性属于确定无效的值,导致整个 --aim-* 尺寸比例系统(以及基于其链接的 Mantine 包装器覆盖)静默塌陷为浏览器默认样式。现在空字符串与 null/undefined 一样解析为 rem 默认值,而 fontSize={0} 依然表示 0px。
2.3.2 — Mermaid 图表控件支持免指针键盘操作
Section titled “2.3.2 — Mermaid 图表控件支持免指针键盘操作”Mantine Mermaid 块的交互界面此前仅支持指针:渲染的图表点击时在新窗口打开但位于 Tab 键导航顺序之外,两个仅包含图标的控件(显示源码、复制)未暴露可访问名称。图表容器现在改为可通过 Enter 或 Space 激活且可聚焦的 role="button",两个控件均携带显式的 aria-label——键盘与屏幕阅读器用户获得与指针用户相同的交互路径。首个外部贡献,由 @alectimison-maker 提供。(#31)
2.3.1 — documentId 中的未配对代理对不再导致渲染崩溃
Section titled “2.3.1 — documentId 中的未配对代理对不再导致渲染崩溃”使用者传入的包含未配对 UTF-16 代理对的 documentId——常见于上游管线截断于 emoji 中间的字符串——此前使 clobber-prefix 派生过程同步抛出 URIError: URI malformed,在解析任何 markdown 之前就破坏了整个渲染。该故障取决于长度:超过 16 字符的 id 幸免仅因哈希路径的 TextEncoder 将孤立代理对有损折叠为 U+FFFD。现在任意长度的畸形 id 均路由至哈希路径,在其原始 UTF-16 码元上使用域隔离种子进行计算,因此能够正常渲染、保持确定性,且不同的损坏 id 保持不同的前缀——明确拒绝了有损映射方案,因为该方案会静默合并在不同位置被截断的 id。格式正常的 id 派生出的前缀与 2.3.0 逐字节完全一致。开发构建针对上游损坏记录单次单 id 警告,引擎现在导出 hasLoneSurrogate 作为检测语义的唯一权责主体。(#32)
2.3.0 — 引擎迁移至独立包;顺带根除两处长期隐匿的拼接接缝缺陷
Section titled “2.3.0 — 引擎迁移至独立包;顺带根除两处长期隐匿的拼接接缝缺陷”Markdown 引擎——增量解析、LaTeX 预处理、定义/脚注机制以及封闭的插件管线——现在归入 @ai-react-markdown/engine,这是一个在其依赖树中完全不包含 React 的框架无关包。@ai-react-markdown/core 使用该包并保留逐字节一致的公开接口:现有所有导入保持有效,没有任何命名变动,运行时输出直至解析树完全等价。这是非 React 适配器(Vue 相关包是直接推动力)以及未来嵌入式运行时使用的结构前提——引擎属于纯文本与语法树上的纯计算,在任何运行时路径上均无 DOM 访问与 Node 专属 API。
- 迁移内容:拼接/冻结增量引擎及其全部验证工具(fuzz 测试集、穷举遍历、仲裁器测试工具、压测脚本)、预处理器(LaTeX、remend)、带封闭插件目录的 remark/rehype 链装配、安全清洗 schema 机制、跨片段定义注册表,以及内置 react-markdown 的纯解析/转换阶段。React 部分——渲染、hooks、块缓存、平滑流式的 React 层、流式游标——保留在 core 中。Core 的运行时依赖列表从 30 个包骤降至 4 个。
- 版本机制:引擎与 core 及 mantine 保持统一版本发布同步,core 对其进行精确锁定(
2.3.0,非 caret 范围)——引擎内部 API 在 3.0.0 之前不承诺稳定性,范围依赖会导致未来引擎与旧版 core 之间发生漂移。请将引擎视为 core 的内部供应包;仅在开发框架适配器时才直接依赖它。 - 修复了两处既有引擎缺陷——通过验证拆分时运行的 170 万样本全新随机种子 fuzz 战役发现,均可在 2.2.1 上逐字节复现(可追溯至增量引擎初次引入时,其发现构成了迁移本身最有力的字节等价性证据):
- 处理指令开头的原始块可能吞噬后续文本。 parse5 在首个
>处结束<?…虚假注释,而 micromark 的 flow 构造一直运行至?>;包含内部>的 PI(<?instr <b> ?> after the pi)会留下无位置信息的文本残留,拼接切分可能将其遗弃——冻结侧丢弃了它,尾部重新解析无法重现它。切分处现在检测切分点之后的残留并在该帧回退到全量解析。 - 内部原始字面量可能丢失源码位置。 同一 html 块的两个元素之间带位置信息的文本(
</details>文本<embed/>,其中 sanitize 剥离了 embed)此前被传入建模块末尾字面量的合并路径中,脱落了尾随换行符并丢弃了位置——对块缓存缓存 key 构成威胁。分类器现在将内部字面量(原样保留,独立分隔符)与块末尾字面量(与接缝合并,位置丢弃,正如全量解析所为)区分开来,基于字面量的源码是否在其所属节点末尾结束进行判断。
- 处理指令开头的原始块可能吞噬后续文本。 parse5 在首个
- 验证:完整发布门禁(5 万次拼接 fuzz、2 万次方向测试集、跨 12 个分片的 K=4 遍历、3 万次定义扫描器测试、5000 次流式的 LaTeX 测试)加上对所有发现种子批次的 150 万样本重测——全部通过;反例被固化为约 50ms 的确定性测试;预检包含 70 个文件中的 1,153 项测试。两轮独立审查(架构审查 + 对抗性变异测试)签署通过,变异测试确认接缝修复的两部分均由各自的测试用例进行防护。
2.2.x — 文档级轮候呈现
Section titled “2.2.x — 文档级轮候呈现”
2.2.1 — 流式游标跟随脚注定义;轮候呈现细节打磨
Section titled “2.2.1 — 流式游标跟随脚注定义;轮候呈现细节打磨”两项使用者可见修复,无引擎变更:
- 流式游标现在跟随流式脚注定义进入页脚。 定义在重置至文档末尾的页脚中渲染,因此 DOM 尾部与源码尾部发生分歧——游标此前在引文页脚流式输出时闪烁在正文尾部(标准 LLM 结束模式)。渲染器现在根据其持有的解析树推导尾部类型(惰性延续行、块引用/列表/嵌套定义均由 micromark 自身的判定进行分类,虚拟注入被过滤)并标记游标读取的隐藏同 commit 标记:指示器按标签锚定在正确的页脚条目内部(页脚顺序为首次引用顺序而非源码顺序),跳过反向引用箭头,在正文恢复时返回正文,并对无法真实指向的尾部进行隐藏——流式链接引用定义(不渲染任何内容)或在跨片段协同下其页脚条目位于另一片段中的定义。包含逐字符引文的 Demo 演示完整实时行为。
- 轮候呈现: 源码结束后释放的空片段不再闪烁一帧游标(释放握手的强制节拍仅限定于非空积压),开发专用的阻塞标记警告判定移入单元测试覆盖的纯函数中。
2.2.0 — 多片段平滑流式协同汇聚为统一打字机效果
Section titled “2.2.0 — 多片段平滑流式协同汇聚为统一打字机效果”在 <AIMarkdownDocuments> 下共享 documentId 的平滑流式片段现在自动进行协同:挂载内容为空的片段等待所有前序片段完成(源码结束且呈现完全排空),随后通过常规排空法则播放其积压内容——即使源流并发传输,也呈现为一个打字机、一个游标。引擎、控制器、useSmoothStream 或注册表无变更;门禁属于上层的轻量协同层。
useDocumentSmoothStream:useSmoothStream加上门禁逻辑,为自定义包装器输出相同 props 结构的结果(<MantineAIMarkdown {...smooth} documentId={id} />)。缺少documentId或在包装器外部使用时逐字节降级为useSmoothStream。外层组件自动通过它进行路由。- 针对真实聊天 UI 构建的安全机制:非空挂载无需经过门禁直接穿透(水合、虚拟化滚动返回与流式中途重新挂载保持即时呈现——绝不白屏或重新播放);完成状态具有粘滞性(第 2 轮工具调用绝不重新隐藏后继者的可见文本);卸载释放后继者(虚拟化不会造成队列死锁);不同的
documentId属于独立队列;流式结束时未产出文本的片段依然正常移交轮候次序。 - 逃逸通道与可观测性:每个片段支持
smoothCoordination={false}(即使在门禁中途也立即释放——适用于脱离挂载顺序插入的片段),包装器支持全局smoothTurnTaking={false},并在队列阻塞在前序未将streaming置为 false 的片段之后时输出开发模式告警(基于心跳,因此正在发射 token 的慢速模型不会误触发)。 - 验证:实现前经过三轮设计审查,实现后经过两轮实现审查加最终审计;包含七个测试用例的浏览器收敛测试覆盖带单游标锁定的排序、释放时动画而非突变、粘滞完成重叠、虚拟化滚出/滚回、双文档独立性、卡死队列逃逸以及零通知空片段——最后一项经过反证验证(边沿驱动的完成回归会导致其报红)。完整预检:1,119 项测试。引擎未做任何修改,无需压测门禁。
2.1.x — 平滑流式输出
Section titled “2.1.x — 平滑流式输出”
2.1.0 — 自适应抖动缓冲区的打字机节奏;逐帧热路径实现全增量处理
Section titled “2.1.0 — 自适应抖动缓冲区的打字机节奏;逐帧热路径实现全增量处理”新特性界面与三方面性能攻坚。节奏法则提交上的 feat! 标记属于从未发布的界面的迭代——本版本与 2.0.3 相比无破坏性变更。
-
平滑流式:
<AIMarkdownSmoothStream>将突发 token 片段平滑呈现为稳定的逐字素打字机效果。分为三层——无框架依赖的createSmoothStreamController(无 React/DOM 依赖)、结果可展开至任意包装器的useSmoothStreamhook(<MantineAIMarkdown {...smooth} />),以及具备完整<AIMarkdown>属性并附加smoothPacing与onSmoothDrained两项属性的外层组件。控制器采用自适应抖动缓冲区设计:不规则采样 EMA 跟踪源端到达速率与突发间隔,目标缓冲区维持在约一次突发的量级(平滑处理的因果下限),并在流式结束后的drainMs内按标记的截止时间排空积压——快速模型不再存在固定的延迟窗口,慢速模型也不会被过度追赶导致停顿抽搐。停顿绝不计入节奏评估(工具调用间隔不属于节奏),任何参数中的 NaN/Infinity 会回退而不是破坏法则。调节界面提供三个预设——
'smooth'/'balanced'(默认) /'responsive'——沿用音频插件缓冲区命名惯例;数值级覆盖存在于控制器上,预设集合导出为SMOOTH_STREAM_PACING_PRESETS。 -
真实聊天 UI 中关键的契约细节:挂载与内容替换即时呈现(SSR 水合逐字节一致,虚拟化列表绝不重新播放动画);
finish支持多次重入以配合多轮工具调用流程;暂扣尾随字素确保代理对与 emoji ZWJ 序列绝不以半截状态进入解析器;外层组件在呈现排空之前保持下游streaming为 true(游标在动画途中不卸载),且onSmoothDrained在每轮流式中仅触发一次。经五轮审查强化——StrictMode 可恢复的销毁处理、积压形成时装填排空锁存,以及前馈锁定测试均先行编写测试用例。 -
协同模式定义扫描现在双向实现增量化(消除了旧版 Documents+smooth 隐患):快速路径探测需要完整的
]:定义特征,因此无序链接列表、任务列表与引用列表——AI 输出密集出现的形态——绝不脱离快速路径;当定义块真实流式输出时(引文页脚),冻结边界前缀缓存仅重新解析活跃尾部(在 12k 字符片段上从每次追加约 30ms 降至约 0.8ms)。computeFreezeBoundary增加扫描器语法配置项(mathFlow/referenceTaint退出选项),引擎默认行为逐字节一致;通过覆盖引擎风险语料的新 fuzz 测试套件与语法验证的固定反例完成验证。 -
内置 LaTeX 预处理器按渲染器实例感知追加:输出与无状态运行逐字节一致(包含早期退出特性),流式追加仅重新处理活跃尾部——在 15k 字符密集数学公式流中从每次追加约 2ms 降至约 20µs,即使退化的开放围栏流式也超越旧路径性能。无公开 API 变动;直接调用
preprocessLaTeX不受影响。剩余的每帧 O(全量前缀) 开销来自用户传入的contentPreprocessors——建议保持其轻量或内部感知追加(文档)。 -
验证:完整预检(1,097 项测试)加发布门禁压测——全新随机种子 5 万次拼接 fuzz、2 万次方向测试集、3 万次定义扫描器 fuzz、5,000 次流式的 LaTeX 增量 fuzz,以及完整的跨 12 分片 K=4 遍历——全部通过。新指南:平滑流式输出。
2.0.x — 扁平属性 API
Section titled “2.0.x — 扁平属性 API”
2.0.3 — 双重审查战役带来的属性 API 接口强化
Section titled “2.0.3 — 双重审查战役带来的属性 API 接口强化”无引擎变更——解析/拼接管线与 2.0.2 逐字节一致。所有修复均针对 v2 属性/上下文界面,由双人审查(实现 + 架构)结合对抗性复审发现:
- 全库 null 守卫完善:“显式传递
null视为缺省”的规则现在覆盖所有属性,而不仅限于引擎部分。此前序列化调用方传递Typography: null会导致渲染崩溃,在fontSize/streaming/variant/colorScheme上传递null会穿透默认值。TS 属性类型依然排除null——守卫属于运行时纵深防御。 - Mantine 不再阻断应用级
codeBlock通道:在缺少codeBlock属性时,<MantineAIMarkdown>此前输出空占位组,在内层优先合并规则下静默遮蔽外层AIMarkdownBehaviorsProvider的组。未传递属性时现在完全不贡献该组键名;文档中的包装器示例遵循相同模式。 - 密封运行时门禁同时检查两个键名:
enginePlugins安全清洗现在要求公开的'~sealed'标记以及内部阶段元数据,拒绝仅有阶段信息的结构仿冒体(两种情况均输出开发告警)。 - 规范治理:core README 增加带 2.x 保留策略的组键名注册表(core 不会在大版本内将已注册的组键名提升为 core 锁定键名);执行计划增加附录 B 记录每项实现偏离;新增契约测试锚定插件链顺序无关性与导出接口(v1 符号保持删除状态)。Storybook 标签与 JSDoc 不再提及 v1 时代的属性名。
2.0.2 — 增量解析第二轮强化:精确守卫替代粗粒度兜底
Section titled “2.0.2 — 增量解析第二轮强化:精确守卫替代粗粒度兜底”修复了七项引擎问题(六项在已发布的 2.0.1 中可达;均非 2.0.1 回归),通过四轮 30 万样本全新随机种子分片压测发现并验证——最后一轮全部通过。亮点:
- 冻结边界检测器阻断项 7:在形似 html-flow 行下方紧接的
$$数学公式或 ``` 围栏开启仅在顶层确定被吞噬——在容器内部它属于真正开启,导致跟踪器的开启/闭合相位永久颠倒。被抑制的开启现在污染后续冻结候选点(粘滞性,仅过度阻断)。相同机制覆盖段落内联且从未闭合的<!--(对 micromark 属于字面文本,但此前被扫描为注释内部——导致真实标记未被计数)以及围栏闭合后紧接的 4 空格缩进行(跨越空行合并的缩进代码)。 - 拼接层:接缝分隔符连续长度模型现在计入合并至原始尾随字面量的换行分隔符(正确计算而非回退);无位置内容文本必须直接位于其所属元素之后(任何前置文本均属于尾部自身的剥离构造残留——冻结会导致重复);以剥离构造(
…-->)结尾的已冻结 html 子节点放弃尾部重建(其内部空白无形中合并入接缝分隔符)。 - 净效益:移除了 2.0.1 中粗粒度的“切分点以无位置节点结尾则回退”逻辑;良性文档的增量覆盖率重回 2.0.1 之前的上限之上(对照 fuzz:良性文档从 0.4291 提升至 0.4395,风险用例从 0.3020 提升至 0.3466),全部七个反例均已锚定。
2.0.1 — 拼接层强化;第一方 ==mark== 插件;类型依赖修复
Section titled “2.0.1 — 拼接层强化;第一方 ==mark== 插件;类型依赖修复”- 引擎修复(全部早已存在——在已发布的 1.8.0 中可达,由扩大的分片压测发现;均非 2.0.0 回归):通过结构化回退到全量解析封堵了三类四项增量解析拼接分歧。合并换行分隔符的原始尾随字面量可能错误配对无位置信息的 KaTeX 跨度(导致接缝分隔符重复);以无位置节点结尾的冻结切分脱离了带位置包含关系的交叉校验(导致数学公式块错位);未闭合的行内
<details>使 parse5 提升根元素并吞噬后续兄弟节点,导致 mdast 清洁的冻结边界在 hast 上并不清洁(导致尾部内容重复)。反例已固化为测试用例;良性文档增量帧比例实测开销约 1 个百分点。通过完整三项压测加 30 万样本全新随机种子运行验证——全部通过。 - 原生 Node CJS
require()恢复工作——2.0.0 的已知问题已解决。无人维护的remark-mark-highlight依赖被替换为第一方@ai-react-markdown/remark-mark-highlight(独立版本发布,双 ESM/CJS 构建):与上游 0.1.1 输出字节兼容,通过替换前对照上游生成的 50 用例一致性语料库进行锚定。 - 发布的类型在严格解析器下正常解析:
@types/hast移入dependencies——发布的 d.ts 从hast导入,缺少该包时 pnpm/PnP 使用者会遇到 TS2307(或在skipLibCheck下静默推导为any)。
2.0.0 — 属性/配置 API v2:扁平属性、封闭引擎插件与五个上下文
Section titled “2.0.0 — 属性/配置 API v2:扁平属性、封闭引擎插件与五个上下文”重大变更。完全移除 1.x 的 config / defaultConfig 对象配置通道——不提供兼容层。每个移除的符号均有一对一的迁移目标,在迁移指南中提供了可运行的前后代码对比;引擎本身未做变动(生成的插件链逐字节等价,由独立镜像测试套件与全新完整压测保证)。
- 输入界面:18 个扁平属性对照内置默认值一次性解析——显式传入(
v != null)优于默认值,null视为缺省(RSC 序列化保护)。行为开关变更为blockMemo/incrementalParse/preserveOrphanReferences;注意缺省语义的翻转——省略incrementalParse现在表示内置默认值(开启),而在 1.x 中自定义defaultConfig省略该字段静默表示关闭。两个选择枚举折叠为enginePlugins,接受来自新子路径@ai-react-markdown/core/plugins的 core 密封插件对象(highlight、definitionList、smartypants、pangu、removeComments、defaultEnginePlugins);插件链位置由插件元数据决定,绝不由数组顺序决定。defineTheme/defineBehaviors/definePipeline将集成时片段打包为冻结展开对象,引用稳定性收敛到单一的表驱动防火墙(useStableRecord+AIMarkdownStabilityPolicy,导出供包装器复用)。 - 输出界面:渲染状态上下文及其调用方断言的
TConfig泛型(useAIMarkdownRenderState)被五个具有精细 hooks 的按系统上下文取代——useAIMarkdownDocument/Metadata/State/Theme/Behaviors——外加聚合的useAIMarkdown()。streaming状态翻转现在仅唤醒 State 订阅者(通过浏览器运行的 story 测试锚定)。包装器与应用通过堆叠在<AIMarkdown>外部的可叠加AIMarkdownBehaviorsProvider/AIMarkdownStateProvider进行扩展,core 键名受到三重锁定(类型层never、展开顺序、开发告警),在<AIMarkdown>外部滥用会严格按精细 hooks 的契约抛出异常。 - mantine:
codeBlock变更为扁平行为组属性,通过useMantineCodeBlockOptions()读取(单一的断言与默认值位置);defineMantineBehaviors为扩充后的工厂函数;渲染配置三件套(MantineAIMarkdownRenderConfig、defaultMantineAIMarkdownRenderConfig、useMantineAIMarkdownRenderState)被移除。泛型签名调整位置:AIMarkdownProps<TConfig, TMetadata>→AIMarkdownProps<TMetadata>。 - 引擎修复(在已发布的 1.8.0 中可达,由发布门禁压测发现):吞噬非标签行的 html-flow 运行会留下平衡的浮动残留,其 rehype-raw 接缝取决于后续内容——在定义与段落之间切换的尾部可能会重塑已冻结区域。冻结边界检测器增加阻断项 6(原始残留接缝):与此类运行相邻的候选切分点将被拒绝,直到后续内容锁定接缝为止,经对抗性审查强化(平息行上的多行 raw 构造;不产生 hast 输出的行不再释放)。仅过度阻断——确保输出正确性,而非改变输出形态。最终检测器通过完整三项压测(5 万次 fuzz、2 万次方向前缀、K=4 遍历 ×12 分片)。
- 文档:所有三个 README 与每个
docs/页面均已迁移;apps/docs/content/guides/migrating-to-v2.md提供完整的新旧映射;apps/docs/content/guides/typescript-generics.md简化为仅介绍元数据泛型;apps/docs/content/guides/extending-via-subpackage.md围绕行为组与可叠加 Provider 重写,与 mantine 实现保持一致。 - 已知问题(既有,自 1.8.0 保持不变):原生 Node CJS
require('@ai-react-markdown/core')会失败,原因是remark-mark-highlight依赖项未发布require条件;打包器与 ESM 使用者不受影响。计划推出第一方替代子包。
1.8.x — 默认启用增量解析
Section titled “1.8.x — 默认启用增量解析”
1.8.0 — incrementalParseEnabled 默认置为 true
Section titled “1.8.0 — incrementalParseEnabled 默认置为 true”config.incrementalParseEnabled从可选启用改为默认开启,并移除了 EXPERIMENTAL 标签。晋升条件由 1.6.1 验证战役及其压测记录(5 万次 fuzz 样本、2 万次方向测试集前缀、穷举 K=4 遍历——全部通过)达成:纯追加流式现在开箱即用冻结稳定文档前缀并仅重解析尾部,在基准负载下将逐帧解析/转换开销削减了 83–94%。- 渲染输出未发生变动。引擎契约——拼接树在结构与位置上与全量解析深等价——由拼接等价性测试套件逐帧保障,每个不安全帧依然静默回退到常规全量解析(非追加变更、尚无安全冻结边界、容器嵌套定义、SSR)。
- 退出机制:传递
config={{ incrementalParseEnabled: false }}。子包作者须知:该字段在AIMarkdownRenderConfig中仍为可选,省略该字段的自定义defaultConfig在引擎门禁处仍解析为false——需显式设置为true以匹配新的库默认值。 - 全文相关文档完成同步更新(README、
apps/docs/content/guides/streaming-and-performance.md、apps/docs/content/guides/benchmark.md);下文的历史记录保留原本默认false的当时记录。
1.7.x — 流式游标
Section titled “1.7.x — 流式游标”
1.7.0 — 内联流式游标
Section titled “1.7.0 — 内联流式游标”<AIMarkdown>新增streamingCursor插槽,并导出AIMarkdownStreamingCursor外壳:当streaming === true时,指示器直接渲染在最后一个流式字符之后,并在 token 停顿期间保持可见活动状态——即使用户未收到新内容,也能区分“仍在生成”与“卡死”。所有操作均在 DOM 层完成(白名单遍历到最后一个文本节点,对其最后一个字符进行 Range 测量——感知代理对——并通过绘制前的 MutationObserver 配合 ResizeObserver/fonts.ready保持命令式transform同步),因此内容字符串、解析管线与块缓存缓存完全不受影响:增量解析保持其追加门禁,通过专用浏览器回归测试用例进行锚定。此前记录的content + '▍'模式正是因此被废弃——它在每一帧静默强制执行全量解析。- 默认指示器为与当前行等高闪烁的原点,在静默 5 秒后淡入为双色微调环,并在 token 恢复时弹回——纯 CSS(仅使用 opacity/transform,无布局属性),感知
prefers-reduced-motion,标记aria-hidden,复制安全(绝不位于文本流内部),关键帧通过useInsertionEffect去重注入单一document.head标签。自定义视觉效果通过indicator契约接入({ width, height, lastMutationAt })。无法锚定的尾部(代码围栏、KaTeX 输出、SVG、原生 HTML、空元素——或首个 token 之前的空内容)在这些帧中隐藏游标;当文本尾部返回时重新显示。RTL 锚定在正确侧,补偿祖先transform: scale,SSR 仅输出惰性外壳(无水合跳动)。 - 合并前经三轮审查强化:修复了 RTL 重叠、排版
:last-child外边距修剪导致的布局突变(该规则现在作为 core 与 mantine 中独立的 Baseline-2023 安全规则集,使最后一个真实块保持无外边距,同时零高度游标外壳保持挂载),以及可能永久抑制停顿状态的毫秒以下 Chromium 定时器提前触发问题(通过偶发故障的 story 发现,通过带截止时间戳的复查重装填解决);实测浏览器极端测试(并发实例、变形尾部、1 毫秒流、途中重置)未发现其他缺陷。七个浏览器冒烟用例作为永久回归测试固件与 Node 环境套件一同发布。 - 文档:
apps/docs/content/guides/streaming-and-performance.md的“变体:流式游标”小节记录了该内置插槽(并提示了为何追加游标字符会破坏增量解析),端到端聊天示例同样采纳了该插槽。
1.6.x — 增量解析
Section titled “1.6.x — 增量解析”
1.6.1 — 增量解析验证测试与 CI 浏览器门禁
Section titled “1.6.1 — 增量解析验证测试与 CI 浏览器门禁”- 增量解析引擎(仍为
config.incrementalParseEnabled,仍默认false)经历了一次机器驱动的验证战役,发现并修复了 18 个真实的正确性缺陷——每一个仅在开启标志时可达,因此已发布的默认行为不变,开启标志的使用者获得了显著更为正确的输出。每个缺陷均以确定性测试用例(文档 + 流式时序)永久固化。分类:4 个检测器欠阻断(html-flow 延续行内的代码跨度掩码;列表打断段落或紧随闭合围栏/公式行时的过期延续判定;注释终止符或歧义非块标签后抑制真实$$开启的粘滞 flow 标志;$$$$开启长度为 4 的数学公式围栏),5 个畸形定义注册(html-flow 文本中形似定义的行、脚注定义错误串联、无目标的[label]:、目标后的垃圾文本、行尾未闭合的标题),1 个引用欠污染(形似定义的段落延续行丢弃其有效的[label]引用),以及 hast-util-raw 较少触及输出形态周围的 8 个拼接层分歧(根位置锚定、html 块尾随字面量位置生命周期、分词器丢弃的 raw 构造周围的接缝合并以及仅页脚尾部)。 - 背后的反证测试套件由基于统一仲裁器预言机的四层组成:基于属性的 fuzz 仲裁器(针对检测器已知近似偏置的 fast-check 生成器)、有界穷举遍历(在 24 个 markdown 热点字符集上的每个 ≤K 词法单元序列 × 每个 2 次切分时序——在界限内属于穷举而非抽样)、将检测器“仅会过度阻断”声明转化为轰炸属性的方向测试集,以及植入已知故障并断言仲裁器捕获的敏感度元测试套件(确保全绿压测具有实际意义)。附带记录了 Stryker 变异审计。该战役的压测记录:5 万次 fuzz 样本、2 万次方向测试集前缀以及完整的 K=4 遍历,在修复后全部通过。研究目录(
src/experiments/prefixFreeze/)记录了完整内容。 - CI 现在运行 Storybook 浏览器冒烟测试。 根 vitest 配置的
storybook项目在无头 Chromium 中渲染每个 story——这是唯一覆盖真实浏览器 DOM 的门禁,但pnpm -r test从未执行到它(该项目位于工作区根目录)。它获得了独立的 CI 矩阵任务并在发布前于 release 工作流中内联运行,填补了已知的门禁漏洞。 @ai-react-markdown/mantine的 mermaid 渲染器进行了保持行为的重构:其三个布尔状态折叠为单一的视图相位机,mermaid.initialize按主题缓存,未变更的(code, theme)对跳过冗余重新渲染——1.5.1 发布的流式/重新生成/主题翻转行为保持不变,并经过实测验证。- 维护工作:流式基准测试 story 统一定义比较维度并共享控制行与重放外壳;开发依赖安全通告(
qs、esbuild)通过范围覆盖解决(仅限开发链路,不向外发布);CI actions 迁移至 Node 24 目标。
1.6.0 — 用于流式传输的实验性前缀冻结解析
Section titled “1.6.0 — 用于流式传输的实验性前缀冻结解析”-
新增
config.incrementalParseEnabled(默认false,要求blockMemoEnabled):在纯追加流式传输期间,渲染器在验证安全的边界处冻结文档的稳定前缀,仅重新解析尾部,并将前一帧的 mdast/hast 与尾部进行拼接——在基准负载下将逐帧解析/转换开销削减至大致相当于尾部的份额(管线阶段耗时减少 83–94%——超过了测量研究中 70–89% 的仅解析预估;冻结边界覆盖真实 LLM 输出的约 73–87%)。块缓存缓存 key 基于位置,因此两项优化能够协同:冻结块持续命中缓存。 -
安全性经过反证而非假设:拼接等价性测试套件断言拼接树在插件排列目录与对抗性用例(松散列表、rehype-raw 吞噬容器、未闭合
$$数学公式、迟到引用/脚注定义、定义列表术语声明、Unicode 大小写折叠标签、CRLF)下每帧与全量解析深等价(包含位置)。Storybook play 测试额外锚定真实浏览器中开启与关闭标志流式的实时 DOM 等价性。 -
脚注支持拼接,而不是强迫每帧进行全量解析。脚注编号、页脚归属/顺序以及反向引用 id 属于 mdast-util-to-hast 内部的整文档状态,因此引擎在尾部头部重放前缀的脚注事件序列(按出现次数、文档顺序记录定义与引用)——尾部运行精确重建该状态,重新生成完整的文档页脚,并且页脚的位置通过双重规则重写回文档坐标(注入定义段按段处理,尾部原生节点进行常规位移)。带定义基准语料从“衡量回退开销”转变为 >50% 拼接帧;重度脚注的浏览器冒烟测试在 StrictMode 下锚定实时 DOM 等价性。
-
跨片段(
<AIMarkdownDocuments>)文档同样支持拼接。每个片段由注册表驱动的虚拟定义后缀作为始终属于尾部的输入传递给引擎:追加门禁与边界扫描仅关注片段自身的文本,因此虚拟变动(同级片段流式传输时标签的到达/离开)仅重新解析尾部。引用污染是正确性的后盾——虚拟定义绝不存在于片段自身文本中,因此虚拟解析的引用绝不进入冻结前缀。配套提供专用CrossChunkIncrementalComparestory 与协同浏览器冒烟测试。 -
Sanitize 剥离的前缀节点支持拼接(HTML 注释、
<?…?>虚假注释、<script>):其孤立的换行分隔符由分隔符序列对齐模型重新推导,而不再触发全量解析回退(此前冻结前缀包含此类节点时每帧均会触发回退)。 -
每一帧依然重新检查门禁链,在无法证明安全时静默回退到常规全量解析:非追加内容变更、尚无安全冻结边界、无法原样重新注入的容器嵌套定义,或超出对齐模型的 hast 布局。SSR 始终走全量路径。参见
apps/docs/content/guides/streaming-and-performance.md中的“增量解析(前缀冻结)”章节。 -
新增可选的
createRemendPreprocessor()——基于 Vercel Streamdown 零依赖remend引擎构建的流式尾部修复工具:未闭合的**粗体**/`代码`/~~删除线~~/链接在流式中途渲染为样式而非字面量。作为可选辅助函数暴露(实际打包依赖于包构建与使用者打包器),在规范文本上为无操作(最终帧完全一致),linkMode在 URL 清洗器下默认仅文本,数学公式补全永久关闭(内置 LaTeX 预处理器管理$)。与块缓存自由组合;在增量解析下,仅未闭合构造内部的帧会发生回退。参见apps/docs/content/guides/content-preprocessors.md。 -
开发阶段遥测增加
ai-markdown:stage:scan测量项(边界检测器),parse/transform在发生拼接的帧中仅报告尾部耗时。BlockMemoComparison基准 story 在块缓存一侧增加incremental开关。 -
发布前经多角度审查与打磨强化:修复了六个探测确认的检测器边界问题(缩进代码合并、形似定义的延续行、块引用嵌套定义、行中
$$、带反引号的围栏 info 字符串、html 块类型 3–5),每一个均转化为永久测试用例;v1 时代的脚注绕过逻辑升级为感知围栏/代码跨度(后在 v2 注入重放下完全移除);检测器增加检查点增量扫描(扫描阶段在 4× 下从 9 降至 4 ms,在 16× 下从 84 降至 13 ms),附带恢复与全新扫描等价性测试;插件链统一收敛(pluginChain.ts),并对实验记录进行了方向一致性锚定;apps/docs/content/guides/benchmark.md发布重新校准的真实浏览器数据(84%/94% 管线节省,boost p50 从 32 降至 7.5 ms——未受强化修改影响)。 -
v2 拼接功能在发布前经历相同流程:两轮对抗性审查封堵了两处正确性漏洞——注入文本自身引入了检测器未建模的延续上下文(尾随脚注定义体可能吞噬缩进尾部内容;定义列表
: desc可能通过压缩连接认领注入块),两者均通过在每次注入后追加哨兵终止符定义加以消除,外加提及门禁,以防字面书写哨兵标签的文档发生误解析——以及一处经确认的回归(以表格开头的文档静默从不拼接)。过程中的强化:注入方案被缓存并增量推进(消除了每帧 O(stream²) 遍历),引擎增加了异常防护网(帧中途异常不再遗留过期扫描检查点),SSR 渲染完全跳过引擎的种子扫描,且检测器检查点不再保留文档副本(每个挂载实例回收约 2–3× 文档大小的内存)。
-
Storybook 增加完整比较矩阵:
IncrementalParseCompare、BoostCompare(开启所有优化 vs 旧版)、进程隔离变体以及带实时冻结边界条的VerificationPlayground——每个同页比较均携带逐帧 DOM 等价性校验器(clobber 前缀已规范化)。 -
设计背后的测量研究以
packages/engine/src/experiments/prefixFreeze/形式发布(在 2.x 引擎拆分前位于packages/core)(消融阶梯 L0–L4 附带反证表——包括为何启发该特性的双空行规则对典型单空行 LLM 输出冻结率为 0%)。
1.5.x — Mantine 9
Section titled “1.5.x — Mantine 9”
1.5.1 — 流式鲁棒性:原生 HTML 吞噬防护与 Mermaid 生命周期
Section titled “1.5.1 — 流式鲁棒性:原生 HTML 吞噬防护与 Mermaid 生命周期”- Core 的块缓存缓存不再在流式文档包含未闭合原生 HTML 容器(
</details>到达前的<details>——但适用于任何容器标签)时冻结过期内容。rehype-raw 的 HTML 解析会将所有后续同级节点重新归入开启的容器中(包括合成脚注部分),而块在源码级的缓存标识保持逐字节一致;首次被吞噬的快照此前成为永久缓存命中,导致重复的脚注部分被困在容器内并在流式中途冻结后续内容。原生 HTML 块现在携带其渲染子树的结构摘要,因此缓存恰好在吞噬生效期间失效,并在闭合标签到达后于一帧内恢复。Markdown 原生块完全跳过摘要遍历——KaTeX 输出等大型确定性子树无新增每帧开销。 @ai-react-markdown/mantine的 mermaid 渲染器现在感知流式传输。当streaming为 true 时,截断代码上的解析失败保持静默:源码作为普通代码块展示,直到首个前缀解析成功,随后最新的正确 SVG 保持显示并在后续每次成功时刷新——流式中途不再闪烁“Mermaid Render Error”。流式结束时在最终代码上运行一次修正处理;其失败是唯一允许替换已渲染图表的情况。streaming的 false→true 边沿视为新生成(聊天“重新生成”复用相同组件实例)并重置预热状态,因此新流绝不展示上一轮生成的过期图表或错误选项卡。静态(非流式)渲染保持原本的保守规则:已渲染图表绝不被后续瞬态故障破坏。mermaid.render不再接收宿主元素:宿主在预热期间隐藏,且display: none会使 mermaid 的 getBBox 文本测量清零——一次性静态渲染曾产出约 16px 的 SVG。通过 mermaid 自身的 body 临时路径进行渲染将测量与容器可见性解耦;suppressErrorRendering此外防止在绘制阶段错误脱离parse时 mermaid 的临时节点在document.body中累积。- Storybook 增加
Mantine/MantineAIMarkdown → Streamingstory,逐 token 流式传输富含 mermaid 的文档;content作为控件提供,支持流式传输任意 markdown 进行直观观察。
1.5.0 — @ai-react-markdown/mantine 迁移至 Mantine 9
Section titled “1.5.0 — @ai-react-markdown/mantine 迁移至 Mantine 9”@ai-react-markdown/mantine现要求@mantine/core与@mantine/code-highlight^9.0.0作为 peer 依赖(针对 9.4.1 测试)。自身 API 无变更——相关包接口无损兼容 Mantine 9 的所有重大变更。使用者继承 Mantine 9 的默认值:md(8px)默认圆角、实心浅色变体颜色(Mantine 的v8CssVariablesResolver可恢复 8.x 外观),以及 React 19.2 底线要求。- 自行安装
@mantine/hooks的使用者注意:@mantine/core@9将其 hooks peer 锁定为完全匹配的版本——保持三个@mantine/*相关包版本一致。 - Core 行为未变。针对
@types/hast≥3.0.5 进行了一项内部类型适配:聚合脚注页脚的<li>编号在 hast 中存储为字符串(value: "3"代替value: 3)——渲染的 DOM 逐字节一致。 - Core 的可选
katexpeer 放宽为^0.16.0 || ^0.17.0。此前范围排除了 0.17(在 0.x semver 下^0.16.0无法匹配 0.17),导致 npm 用户在 katex 0.17 下遇到虚假的ERESOLVE冲突——尽管本库持续对照 0.17 进行测试。
1.4.x — 自定义接口强化
Section titled “1.4.x — 自定义接口强化”1.4 版本线开放了自定义接口(URL 清洗、文档命名空间、设计变量)并建立了防护屏障,以便使用者安全扩展。
1.4.9 — 双重开发/生产构建;根除流式重复解析
Section titled “1.4.9 — 双重开发/生产构建;根除流式重复解析”- 相关包现在在
development导出条件后提供独立的开发与生产构建,发布的文件完全不包含process.env读取——无打包器的使用者(import maps、原生<script type="module">、CDN ESM)不再因process is not defined崩溃。构建后断言确保 dist 永久无 env 读取。关于条件导出带来的 SSR 与 Jest 注意事项,请参阅 README 的“开发与生产构建”章节。 - 流式传输不再为每个 token 支付第二次全量 markdown 解析。独立片段完全跳过定义标签扫描;协同片段(
<AIMarkdownDocuments>下的documentId)将其置于追加感知扫描器之后,仅重新扫描自上一个空行以来的区域,且仅在行首[可能引入新定义时执行。标签集合在未变更时保持其对象标识,因此逐 token 重新注册与下游 memo 失效也随之停止。 urlTransform应用现在具备收敛性:原始 URL 在首次转换时保存,每次处理均基于原始值重新计算,因此记忆化(重新进入)的 hast 树不会被双重转换。树由调用方持有时跳过内部防御性树克隆——常见路径零分配。- 开发构建发出各阶段的
performance.measure条目(ai-markdown:stage:parse|transform|build|render),供在 DevTools Performance 面板中进行管线分析;生产构建完全编译移除门禁代码。
1.4.8 — 自动化 GitHub Releases
Section titled “1.4.8 — 自动化 GitHub Releases”- 推送
v*tag 现在同时发布匹配的 GitHub release,其发布说明提取自本文档对应版本的章节(回退为自动生成的说明)。npm 发布与 GitHub release 仅需一次 tag 推送并在单个 CI 步骤中完成。库运行时无变动。
1.4.7 — 开发模式诊断;支持来源证明的安全发布
Section titled “1.4.7 — 开发模式诊断;支持来源证明的安全发布”- 具有畸形百分比编码的脚注 id 现在记录开发模式警告,而不是静默降级(聚合页脚为该标签渲染空条目——此前无任何原因提示)。
- 首次通过 npm 可信发布(OIDC)发布:两个 tarball 均携带来源证明,将其与确切的源码 commit 及 CI 运行关联。管线中完全不存在 npm token。
@ai-react-markdown/mantine增加了 SSR 冒烟测试套件并在 CI 中设置类型检查门禁(无运行时变动)。
1.4.6 — 跨片段注册表要求显式传递 documentId
Section titled “1.4.6 — 跨片段注册表要求显式传递 documentId”- 跨片段协同现在仅在使用者显式传递
documentId时激活。此前省略的documentId在到达useDocumentRegistry前默认使用useId(),导致包装在<AIMarkdownDocuments>中的独立片段错误走上协同路径——且此类片段中零散的原生占位符标签可能开启一个从未被逐出的孤立注册表外壳(无配对的registerChunk)。 documentId现在在单一位置(渲染状态 Provider)解析,通过状态向useDocumentRegistry与占位符组件传递documentIdExplicit标志。AIMarkdownRenderState.documentIdExplicit为 1.x 兼容性设为可选。
1.4.5 — default 变体设计变量补齐
Section titled “1.4.5 — default 变体设计变量补齐”- default 变体中的间距、字号与标题变量现在读取
--aim-font-size-root。修改fontSize属性会缩放以根为基准的间距与排版尺寸——无需为尺寸一致的主题逐变量覆盖。 - 新增自定义变量:
--aim-font-weight-strong(由所有标题与<th>共享,默认700)与--aim-katex-font-size(默认为--aim-font-size-root,使数学公式无论父级上下文如何均保持组件根字号)。
完整变量列表请参阅设计变量。
1.4.4 — 封堵跨片段 URL XSS 漏洞;收敛公开接口
Section titled “1.4.4 — 封堵跨片段 URL XSS 漏洞;收敛公开接口”- 跨片段链接/图片引用现在在渲染时执行第二次逐属性安全清洗。此前一个片段中宽松的
urlTransform可能会将javascript:/data:URL 泄露到定义它们的另一片段中,绕过使用片段的策略。现在每个使用者独立应用自身门禁——跨片段边界的纵深防御。 - 通过
useDocumentRegistry暴露的Registry类型收敛为只读界面。修改方法(registerChunk、allocateSymbol、releaseSymbol等)不再属于公开类型,因此使用者代码无法意外破坏引用计数或编号不变式。 - 收敛
sanitizeSchema的公开类型——与只读Registry收敛相配合。
1.4.3 — urlTransform 与 sanitizeSchema 属性文档补齐
Section titled “1.4.3 — urlTransform 与 sanitizeSchema 属性文档补齐”- 针对两个属性提供详尽 JSDoc:引用稳定性要求、与
defaultUrlTransform的组合、协议方案名称的正则转义(/^web\+app:/i相比/^web+app:/i的静默泛化),以及非对称稳定性模型(函数引用同一性 vs 深等价数据)。
1.4.2 — 超长 documentId 自动哈希缩短
Section titled “1.4.2 — 超长 documentId 自动哈希缩短”- 超过 16 字符的
documentId值(UUID、nanoid)通过 MurmurHash3 → Base62 哈希缩短至 ≤6 字符,且仅作用于渲染的id="…"/href="#…"前缀内部。状态(state.documentId)与注册表索引使用原始值,因此深度链接与useDocumentRegistry(documentId)不受影响。纯粹的渲染 HTML 紧凑性收益。
1.4.1 — Mantine README 与相关包元数据
Section titled “1.4.1 — Mantine README 与相关包元数据”- 充实了 Mantine 相关包 README,涵盖代码高亮适配器设置、Mermaid 特性、配色方案集成以及完整的 Mantine 扩展配置表格。
1.4.0 — 开放 urlTransform 与 sanitizeSchema 属性
Section titled “1.4.0 — 开放 urlTransform 与 sanitizeSchema 属性”<AIMarkdown>暴露用于双门禁清洗模型的新属性。- 引入
extendSanitizeSchema((draft) => Schema | void)辅助函数——修改并返回的工厂函数,向调用方提供库默认值的深拷贝。库的不变式(跨片段标签白名单、KaTeX className 白名单、<mark>许可)自动保留。 - 库默认 schema 未作为值从
@ai-react-markdown/core导出,以防止浅拷贝展开隐患({ ...sanitizeSchema, … }会导致嵌套数组别名引用);engine 相关包以只读方式导出供渲染器使用。
双门禁模型与引用稳定性规则请参阅 URL 清洗与自定义协议。
1.3.x — 跨片段协同
Section titled “1.3.x — 跨片段协同”1.3 版本线引入了本库迄今为止最大的一项特性:分块 Markdown 文档的协同渲染。
1.3.0 — 跨片段引用
Section titled “1.3.0 — 跨片段引用”<AIMarkdownDocuments>包装器组件——按需启用,按documentId作用域划分。- 脚注、链接引用(
[label]: …)与图片引用支持跨片段协同。片段 B 中的脚注可以解析为片段 D 中的定义;编号为全文档级而非片段局部。 - 通过 React 上下文共享每个文档的
Registry。以符号为键的贡献带有引用计数与微任务延迟清理——无损兼容 React 19 严格模式(Strict Mode)的双重挂载语义而不丢失片段标识。 - 聚合脚注页脚在文档的最后一个片段处渲染一次;在流式传输期间随片段挂载/卸载自动重新排序。
- 通过
documentId属性实现每个文档的 id 命名空间(省略时通过useId()自动生成)——防止同一页面渲染多个<AIMarkdown>实例时脚注反向引用发生冲突。
完整模型请参阅跨片段协同。
1.3.0 — 块级缓存缓存
Section titled “1.3.0 — 块级缓存缓存”与跨片段协同在同一次次要版本发布中一同推出:
blockMemoEnabled配置字段(默认true)。渲染器将每个文档划分为逐块单元,并按源码标识(raw + occurrence + ctx + position)记忆化每个块的 React 子树。流式传输期间未变更的块完全跳过toJsxRuntime与 React 协调工作。- 输出与禁用路径逐字节一致——由覆盖每种插件排列的
byteEquivalence.test.tsx测试套件验证。 - 管线重构为三个独立阶段(解析、规划、渲染),使记忆化能够在阶段之间进行拦截,而无需修改上游
react-markdownAPI。
使用者一侧的引用稳定性规则请参阅流式传输与性能。
1.2.x — 流式安全性改进
Section titled “1.2.x — 流式安全性改进”1.2 版本线专注于提高渲染器对流式中途输入的鲁棒性(部分未闭合 LaTeX 块、瞬态 Mermaid 解析失败等)。
1.2 系列补丁亮点
Section titled “1.2 系列补丁亮点”- 流式中截断未闭合的
$$数学公式块,防止模型在 token 输出中途时mathFlow吞噬文档剩余部分。 - 转义未闭合 LaTeX 块内部的
|字符,使包含\frac{a|b}{c}的流式 token 不会破坏周围内容中的 GFM 表格解析。 - 强化 LaTeX 流式边界情况——货币
$5.99可靠保持为普通正文;括号定界符(\(…\),\[…\])即使仅有一半到达也能正确规范化。 - 修复快速重新渲染时的 Mermaid 竞态条件——并发重新渲染不再在相同图表 id 上发生冲突。
- 流式中途在瞬态解析失败时保留 Mermaid 最近一次成功的渲染。当模型输出部分图表时,先前完全渲染的图表保持可见,直到新图表有效为止;渲染输出绝不闪烁为源码回退,除非图表在完成时确实损坏。
- Mermaid
securityLevel从'loose'收紧至'strict'——防御恶意图表内容的纵深防御改进。 - 受保护的 HTML 注释容器,使正文内部的
<!-- inline comment -->不会意外开启原生 HTML 注入路径。 - 通过打包解决 Vite SSR + pnpm ESM 下 lodash-es 的解析失败——修复了一类此前在使用严格隔离安装器的使用者中出现的“开发环境正常、生产环境报错”问题。
1.2.0 — 首次公开发布
Section titled “1.2.0 — 首次公开发布”- 双相关包 monorepo:
@ai-react-markdown/core(React,独立于 UI 库)与@ai-react-markdown/mantine(Mantine UI 集成)。 - GFM(表格、删除线、任务列表、自动链接)。
- 基于 KaTeX 的 LaTeX 数学公式,配合智能预处理(货币 $、mhchem、括号定界符、竖线转义)。
- Emoji 短代码(
:smile:)。 - CJK 友好换行 + 可选 pangu 自动空格。
- 额外语法:
==高亮==、定义列表。 - SmartyPants 排版、HTML 注释移除。
- 感知流式的上下文。
- 自定义排版变体 + 配色方案。
- 自定义组件(按 HTML 元素覆盖)。
- 与渲染状态分离的元数据上下文。
- 用于扩展配置 + 元数据的 TypeScript 泛型。
- Mermaid 图表,支持深浅主题切换、源码切换、复制、新窗口打开。
- 基于
@mantine/code-highlight(highlight.js)的语法高亮。 - JSON 美化输出(深度解析嵌套的 JSON 编码字符串)。
- Mantine 配色方案自动检测。
1. 流式正确性贯穿解析器之外的多个层面
Section titled “1. 流式正确性贯穿解析器之外的多个层面”演进历史涵盖了输入规范化、边界扫描、语法树拼接、块标识、注册表状态与异步呈现。即使最终源码解析正确,也可能出现过期的引用或图表。这就是为什么除了最终输出断言之外,修复工作反复增加中间帧比较与生命周期测试。
1.3 的块缓存减少了重复的 React 元素构建;1.6–1.8 的增量工作减少了重复解析与转换。后续版本使其准入与失效规则更加精确。这些优化的设计原则是在无法确立安全性时进行回退,而不是承诺任意输入绝不出现崩溃或昂贵路径。
2. 自定义机制具备明确归属
Section titled “2. 自定义机制具备明确归属”排版、设计变量、元素渲染器、元数据、URL 策略与预处理器各自拥有独立的契约。1.4 系列暴露并记录了这些接口,而 2.0 使用扁平属性与狭窄上下文取代了共享配置对象。包装器拥有其行为组默认值;core 拥有其锁定行为与生命周期键名;引擎拥有受支持的语法。
这种职责分离解释了受支持的扩展路径及其限制。预处理器在解析前修改源码,自定义组件在安全清洗后修改呈现,私有 URL 方案必须通过两个门禁。三者都不是将任意 remark/rehype 插件注入经过验证的增量管线的通用替代方案。
3. 集成层通过组合 React 公开 API 运作
Section titled “3. 集成层通过组合 React 公开 API 运作”Mantine 围绕 core 提供排版、额外样式、pre 渲染器以及行为组。自 2.0 以来,可叠加 Provider 与扩充工厂函数取代了旧的 defaultConfig 机制。2.3 的引擎拆分并未要求 React 集成层转为引擎使用者:core 继续暴露其所需的属性、hooks 与类型。
非 React 适配器具有不同的权责与稳定性边界。它直接使用引擎,精确锁定版本,并负责渲染与生命周期的等价性。在 3.0.0 之前,引擎在 npm 上的公开可用性不应被理解为稳定的产品 API。
4. 验证测试随可达语法范围同步拓展
Section titled “4. 验证测试随可达语法范围同步拓展”反复全绿的测试可能遗漏生成器从未发出的语法形态。因此多个版本首先扩充语法语料库,在旧实现中证明故障,随后加入修复与永久回归测试。敏感度测试与防真空底线用例用于区分真正执行到的优化路径与通过的全量解析回退。
历史样本计数与变异测试结果应与原始配置关联保留。当前的覆盖率总览与运行脚本定义了当下的发布测试证据,而实验记录阐明了这些证据是如何形成的。
5. 兼容性遵循公开接口契约
Section titled “5. 兼容性遵循公开接口契约”公开属性与 hook 名称遵循 semver 契约。视觉默认值、精确 HTML 序列化结果、内部注册表细节以及 3.0 之前的引擎导出符号具有更窄的保证范围。在选择要在集成层中锁定的内容或在应用测试中进行断言时,请查阅稳定性说明。
既定暂缓实现的事项
Section titled “既定暂缓实现的事项”本节说明了当前的系统边界,取代了随着特性与 CI 逐步加入而已过期的早期规划记录。
- 开放的解析插件注入依然位于 core 公开 API 之外。 封闭目录允许选用边界行为经过验证的已知构造。新语法在加入该目录前需要对应的扫描器与预言机覆盖。
- core 依然不直接导出默认 schema。 使用
extendSanitizeSchema获取可变的深拷贝。引擎为其使用者导出深度冻结的单例,但通过浅拷贝共享嵌套数组不属于合规的自定义机制。 - 注册表变更保持内部私有。 公开使用者可以读取选择器并按全局或标签订阅。注册、符号分配、释放、编号以及贡献写入依然由渲染器掌控,以确保自定义组件无法绕过其不变式。
- 跨组件拆分的任意语法不进行协同。 支持跨片段脚注、链接引用与图片引用,包括处理后的脚注体。该特性不会拼接来自另一渲染器的半个代码围栏或半个段落。请累积传输增量或使用经过设计的逻辑 Markdown 片段。
- 浏览器基准测试预算不作为自动发布门禁。 CI 与发布工作流确实存在且徽章齐全。独立的浏览器基准工作流仅支持手动运行;其早期的自动收集计划已被撤回。在解读基准测试数据前请查阅 benchmark README。
如果某一系统边界阻碍了具体的集成需求,请在 issue 中描述源码形态、生命周期、预期输出与当前结果。这为调整契约提供了可供审查的基础,而无需将过时的历史暂缓实现视为永久的设计决策。