# 文档引用维护指南

本页描述 0.7.0 的文档维护功能，包括通配符、覆盖要求、Setext 标题和只读复核预览。

`check` 直接读取 Markdown 和来源文件。引用来源相对当前文档的目录解析；`--root` 定义整个工作区的边界，而不是改变每条来源的相对目录。

以 `/` 开头的本地来源相对 `--root` 解析，例如 `/src/file.mbt`；不表示操作系统绝对路径。相对来源继续从当前文档目录解析。两种路径都拒绝实际越界的目录穿越与符号链接。

## 项目配置：本地与 CI 用同一套规则

首次接入时，选择要长期维护的文档范围：

```sh
moon run cmd/main init docs README.md --root /path/to/repository
```

检查通过后，`init` 生成 `tracecite.json` 和 `.tracecite-docs.json`。已有同名配置或基线时会停止；来源不匹配、没有可检查引用或存在待复核项时不会创建配置和基线。新项目默认严格、离线检查。

将这两个文件提交到项目。之后，本地和 CI 都使用：

```sh
moon run cmd/main check --root /path/to/repository
```

独立 CLI 对应 `tracecite check`；从项目根目录运行时可省略 `--root`。本仓库的规则见 [tracecite.json](../tracecite.json)：

```json source=../tracecite.json
{
  "version": 1,
  "paths": [
    "README.mbt.md",
    "docs/maintenance.md",
    "docs/publishing.md",
    "docs/acceptance.md",
    "CHANGELOG.md",
    "THIRD_PARTY_NOTICES.md",
    "examples/maintenance/README.md",
    "examples/review/README.md",
    "docs/field-validation.md",
    "docs/maintenance-trials.md",
    "ROADMAP.md",
    "examples/compatibility/README.md",
    "docs/json-report.md",
    "examples/library/README.md",
    "examples/anchors/README.md",
    "examples/link-parsing/README.md",
    "examples/quote-policy/README.md",
    "examples/html-anchors/README.md"
  ],
  "exclude": [],
  "baseline": ".tracecite-docs.json",
  "strict": true,
  "online": false,
  "coverage": {
    "max_skipped_web": 64,
    "max_unbound_snippets": 57
  }
}
```

| 字段 | 省略时的值 | 含义 |
| --- | --- | --- |
| `version` | 必填 | 当前为 `1`，未知版本和字段会报错 |
| `paths` | `["."]` | 文件、目录或通配符的数组，不能为空 |
| `exclude` | `[]` | 排除文件、目录或通配符；字面目录包含其子路径 |
| `baseline` | 无 | 可选的 `.json` 基线路径；`null` 表示不比较基线 |
| `strict` | `true` | 待复核项导致失败 |
| `online` | `false` | 是否访问网页来源 |
| `coverage` | 不设置上限 | `max_skipped_web` / `max_unbound_snippets` 指定允许的跳过数量 |

配置里的路径相对 `--root`，不相对配置文件所在目录；只接受根目录内的相对路径。即使 Action 的进程在工具目录运行，也读取调用方仓库的配置。配置文件上限为 64 KiB。

参数覆盖规则：指定文件或目录时替换配置中的 `paths`；`--exclude` 追加排除项；`--baseline` 覆盖基线；`--online` / `--offline`、`--strict` / `--no-strict` 覆盖对应开关。`--max-skipped-web` / `--max-unbound-snippets` 覆盖对应数量上限。输出格式由 `--json` 或 `--github` 控制；`--review` 增加按来源汇总，`--preview-baseline` 增加只读候选与变化清单。

`--config rules/project.json` 选择其他配置，明确指定的文件不存在时会报错；`init --config rules/project.json --snapshot rules/baseline.json` 可定制首次创建位置，父目录需已存在。`--no-config` 跳过自动加载，也保留旧版未配置的 CLI 默认值：离线、非严格、扫描当前根目录。

GitHub Action 不填写输入时复用调用方配置；没有配置时保留严格、离线检查的原有默认行为。`report:` 兼容入口跳过项目配置。

## 通配符与覆盖要求

```json
{
  "version": 1,
  "paths": ["README.md", "docs/**/*.md"],
  "exclude": ["docs/drafts/**", "docs/**/*generated*.md"],
  "baseline": ".tracecite-docs.json",
  "coverage": {"max_skipped_web": 0, "max_unbound_snippets": 0}
}
```

`*` 匹配同一目录段中的零个或多个字符，`?` 匹配一个 Unicode 字符；完整段 `**`
匹配零层或多层目录，所以 `docs/**/*.md` 同时包含 docs/guide.md 与 docs/a/guide.md。
匹配区分大小写，`[]`、`{}` 按字面处理。`**` 不能嵌入其他字符，通配符路径不能含 `..`。
模式相对工作区根目录。排除规则先于文件选择；重叠范围不会重复计算同一文件。

每个通配符目标在排除后未匹配 Markdown 时返回退出码 1，即使其他目标有匹配也不静默忽略。
扫描按目录名称排序，跳过已有默认构建/依赖目录和递归扫描中的符号链接；显式目标仍核对真实路径在根目录内。
配置中的模式保留到后续检查，新文档会自动纳入。命令行模式必须加引号，避免 shell 先展开成固定文件列表。

覆盖上限必须是非负整数；省略或 `null` 表示不上限。设为 0 可以要求不跳过网页、不遗漏未绑定的代码块；
示例中的离线网页将触发 SKIPPED_WEB_LIMIT_EXCEEDED，修复方式是启用 `--online`、调整范围或显式调整预算。
未绑定代码块超过预算触发 UNBOUND_SNIPPET_LIMIT_EXCEEDED。超限在 `--no-strict` 下也失败，并阻止快照更新与候选生成。
覆盖要求可只填写其中一项；旧配置不设置上限，行为保持兼容。示例代码围栏也会计入未绑定数量。

## 本地代码或配置片段

代码围栏的语言之后加 `source=路径`。路径含空格时用双引号包住整个属性值。下面的代码块直接核对项目真实的 [模块配置](../moon.mod)：

```text source=../moon.mod
import {
  "moonbitlang/async@0.22.1",
  "bobzhang/html_parser@0.2.0",
}
```

片段按完整行查找，保留大小写、缩进与引号。只统一 CRLF/LF；不执行代码，不根据语义推测它属于哪个文件。整个来源文件中的其他片段改变，不会使仍然存在的这段代码失败。

需要限定来源范围时，可以在路径后面加行选择器或命名区域：

````markdown
```toml source=../config.toml#L2-L4
timeout = 20
retries = 3
enabled = true
```

```toml source=../config.toml#region:defaults
timeout = 20
```
````

命名区域的来源文件示例：

```text
# ANCHOR: defaults
timeout = 20
# ANCHOR_END: defaults
```

使用范围选择器时，基线会记录选中区域；区域中的其他内容改变也会要求复核。普通片段不带选择器时，记录匹配片段本身。

## 链接与标题

行内链接、引用式链接与图片链接都会提取本地目标；同一行 HTML 标签的 `href`、`src` 属性也会提取，例如 README 的 SVG 图片。展示属性中的引号和 Markdown 示例不当作引文或链接。文件和目录可以被检查；Markdown 标题支持 ATX 和 Setext 标题，自动生成小写锚点，重复标题依次加 `-1`、`-2`，并避开已有锚点。支持空格路径的百分号编码，以及 `<含空格的路径>` 写法。

从 0.7.2 起，引用式链接采用第一条有效定义；后续同名定义不能覆盖它。标签使用 Unicode 15.0.0 默认大小写折叠并压缩 Markdown 空白，保留标点前的空格，不进行重音规范化；STRASSE 可匹配 Straße，而 A . 与 A. 不同。

链接标题与地址分开解析，标题中的括号不影响目的地；支持制表符分隔标题、角括号地址以及地址中的 ASCII 标点转义。非法标题或定义不会被标为已验证。这些行为从 0.7.2 起提供，正反命令见[解析回归样例](../examples/link-parsing/README.md)。

Setext 标题使用紧随段落的 `===` 或 `---` 下划线，可包含多行标题；软换行按空格参与锚点生成，与 ATX 共用重复锚点规则。代码围栏、缩进代码、注释、HTML 块和列表标记不生成 Setext 锚点。选中的区域包含标题及下划线，到下一个同级或更高层级标题前结束。

ATX 标题支持 `#` 后的空格或制表符，以及可选的结尾 `#`；标题中的行内链接使用展示文字生成锚点，HTML 排版标签不计入标题文字。HTML 注释中的标题不会生成锚点，也不会截断正在引用的正文区域。

行内代码支持一个或多个反引号，开闭反引号数量必须一致。代码里的链接、双引号和 HTML 注释标记都是示例内容；未闭合的反引号作为普通文字处理，不会遮掉后面的真实链接。

实际入口是 [README 的快速开始](../README.mbt.md#快速开始)。自引用也可以使用 [本节](#链接与标题)。

```markdown
[安装](guide.md#安装)
[源码](../config.toml#L2-L4)
[配置][defaults]

[defaults]: ../config.toml
```

脚注定义 `[^id]: 正文` 不作为链接定义；脚注正文中的普通链接仍会检查。标题按 Unicode 15.0.0 的默认规则转换为小写，再移除 P* 标点；保留 ASCII 连字符与下划线、文字和 emoji。支持带重音的拉丁字母、希腊语、西里尔字母及补充平面字母；Sigma 根据上下文生成 σ 或词尾 ς，İ 展开为 i 加组合点。转换不按特定语言环境处理，不做 Unicode 规范化或重音折叠；链接应使用实际生成的锚点，仍区分大小写。

可直接运行的 [12 条多语言标题链接样例](../examples/anchors/README.md)包含重复标题和 Setext。Unicode 15.0.0 之后新增的大小写映射、完整 Markdown 行内排版对上下文的影响和站点特有规则仍需人工确认。

目前不解析自定义 HTML ID 或站点特有锚点规则；这些情况要改用支持的来源范围或人工核查。目录扫描默认跳过 Git、MoonBit 构建目录、依赖目录和 Python 缓存，递归扫描不跟随符号链接；直接指定的文档符号链接会解析到根目录内的真实文件。

## 逐字引文

本地文件和网页都能作为逐字引文来源。行内引文使用双引号，后面的来源链接位于同一行、同一句中。来源也可以是引用式链接：

```markdown
“The timeout is twenty seconds.” ([说明](source.md#settings))
"The timeout is twenty seconds." ([说明][settings])

[settings]: source.md#settings
```

无法在同一句表达来源时，使用相邻的显式引用块：

```markdown
> 原文：“The timeout is twenty seconds.”
> 来源：[说明](source.md#settings)
```

网页检查需要 `--online`。这会独立访问来源，验证可见文本中的引文；普通网页链接只检查可访问性，不检查它支持了什么主张，也不验证网页 URL 的标题锚点。引文忽略排版空白和引号字形差异；明确的省略号允许有序的较长原文片段。

## 无来源引号策略（0.8.0）

以下选项从 0.8.0 起提供；0.7.2 及更早的 CLI、库和 Action 标签不支持这些配置字段。升级时同时更新本地安装版本与 CI 标签。
默认 unassociated_quotes 为 review，保留无来源引号的人工复核。作者确认项目中的这些完整引号可不作引文检查后，可显式选择 skip，并设置未检查预算：

```json
{
  "version": 1,
  "paths": ["docs/**/*.md"],
  "strict": true,
  "unassociated_quotes": "skip",
  "coverage": {"max_skipped_quotes": 2}
}
```

本地与 Action 均读取同一配置。CLI 参数优先于配置，init 会持久化所选策略和预算：

```sh
moon run cmd/main init docs --root /path/to/project --unassociated-quotes skip --max-skipped-quotes 2
moon run cmd/main check --root /path/to/project
moon run cmd/main check --root /path/to/project --unassociated-quotes review
```

只有 UNASSOCIATED_QUOTE 对应的完整引号会被转为 UNASSOCIATED_QUOTE_SKIPPED。普通输出与 JSON 保留文档行；JSON 条目为 skipped/not_checked，并单独统计 skipped_quotes。
它们不增加 checked_count，也不计入专用于网页的 skipped_count。工具没有判断这些文字是否确实属于术语；skip 是项目作者对检查范围的明确决定。
未闭合引号、无效来源链接、显式 Quote/Source 块、有来源的逐字引文、代码片段、断链和越界检查都继续执行。

max_skipped_quotes 缺省或 null 表示不限数量，推荐显式设置预算，防止后来新增的无来源引号未被注意。超过预算时报告 SKIPPED_QUOTE_LIMIT_EXCEEDED；即使 --no-strict 也失败，不能生成候选或写入基线。
只有跳过项而没有任何可独立核验的引用时也失败；跳过项不进入引用快照，因此基线比较不会追踪它们的文本变化。
计数按解析器识别出的完整引号片段计算；同一行两个片段计为两项，不代表自然语言中的两条论断。跨行引文需改用支持的显式引文格式。
[八步正反样例](../examples/quote-policy/README.md)验证默认复核、显式跳过、覆盖超限、init 持久化及来源变化。

MoonBit 迁移：0.8.0 为公开 DocumentProjectConfig 增加 unassociated_quotes 字符串字段，为 DocumentCoveragePolicy 增加 max_skipped_quotes 可选整数。
通过 parse_document_project_config 读取旧配置无需修改；直接构造上述结构时补入 unassociated_quotes 为 review、max_skipped_quotes 为 None 即可保持原行为。
parse_document 仍返回全部解析警告，配置策略由 CLI 应用；库调用方不会因为读取 skip 配置而自动失去原始诊断。旧版本会拒绝新字段，降级前应删除它们并复核恢复后的检查范围。

## 独立 HTML ID（0.8.0）

支持同一行闭合、内容为空且单独占一行的 a 元素，通过 id 提供稳定目标。属性值可用单引号、双引号或无引号；ID 大小写和 Unicode 保留，不按标题规则转小写。
锚点后隔空行紧接标题时，观察该标题及子章节；后面先有正文时，观察到下一标题。两种情况都止于下一个受支持的独立锚点。
完整规则、实体限制、重复 ID 顺序及正反命令见 [独立 HTML 锚点样例](../examples/html-anchors/README.md)。

这是有限语法支持，不是通用 HTML 解析。跨行标签、其他元素的 ID、name 属性及站点专用目标规则仍不在支持范围。
代码、注释中的标签不产生目标；缺失目标继续失败。稳定 ID 可以避免标题改名造成断链，但正文或标题变化仍会触发基线复核。

## 快照与复核

```sh
moon run cmd/main check --snapshot .tracecite-docs.json
moon run cmd/main check
```

快照是工具生成的 JSON 文件，可以随仓库提交。引用身份由相对文档路径、来源、类型与文档片段组成，不包含文档行号。移动段落不会误报为新增引用；修改文档引用、增加引用，则需要复核并重新保存基线。

本地选择器记录被选中的内容；无选择器的代码块记录匹配片段。网页引文记录匹配位置前后各至多 100 个字符的归一化上下文，避免把整页导航和远处内容的改动都当作引用变化。普通无锚点链接仅记录存在性或可访问性，不做整份文件、整张图片或整页网页的变化监控。

基线比较发现变化时返回退出码 2，展示之前观察到的内容和当前内容。先人工决定文档是否需要修改；确认后，用 `--snapshot` 命令保存新基线。它忽略配置中的旧基线，仍独立检查引用与来源是否匹配。显式同时提供 `--baseline` 时仍会比较旧基线；发现变化或待复核项会拒绝写入。工具不会把失败检查自动接受成新基线。

0.8.0 将来源变化的展示片段定位到首个差异附近，保留最多 100 个字符的相同前后文，每侧展示不超过 1600 个 Unicode 字符。省略处显示省略号；较长或多处变化可能无法全部展示，应检查完整来源。快照、JSON 预览 changes 的 previous/current 和候选 snapshot 保留完整观察值，终端预览使用精简片段。实际使用动机与可重放记录见 [持续维护试用](maintenance-trials.md)。

来源片段在来源中出现多次时，无范围的逐字引文以首次匹配位置提取上下文；需要精确限制本地引用位置时应指定标题、行范围或命名区域。已有基线中的引用被从文档删除后，该引用不再检查；下一次保存基线会去掉它。比较文档子集只读取相关引用；保存快照会替换整个快照文件，应使用准备维护的完整文档范围。

## 集中复核与基线预览

```sh
moon run cmd/main check --review
moon run cmd/main check --preview-baseline --json
```

`--review` 将非成功、非跳过项按实际来源文件或 URL 分组，保留每个文档位置、诊断码、文档片段和前后来源内容。
不同相对路径指向同一文件时会归并。JSON 中的 review 数组包含 source 和 locations；普通 items 字段继续保留。
缺失值按现有 JSON 约定省略，不应假定所有可选字段都存在。

`--preview-baseline` 仍加载配置基线并检查来源，返回 baseline_preview：

| 字段 | 含义 |
| --- | --- |
| `can_update` | 当前来源匹配、覆盖要求、待复核解析诊断与非零检查范围均满足候选生成条件 |
| `scanned_documents` | 本次实际扫描的文档，便于复核范围 |
| `added_count` / `changed_count` / `removed_count` | 按引用身份去重后的新增、来源变化、删除数量 |
| `changes` | 每条变化的 document/source/kind/excerpt、change、previous/current |
| `snapshot` | 完整候选快照；检查不完整时省略，changes 为空，不把失败误标为删除 |

引用片段编辑表现为旧身份删除、新身份增加；段落行号移动不会产生变化。
来源变化和新增引用仍保留 check 的退出码，不因预览模式绕过失败；can_update 可以为 true，但退出码仍为 2。
这表示来源当前匹配且候选可生成，仍需要人工接受变化。

预览不会写入配置、基线或临时文件，不能与 init 或 `--snapshot` 同时使用。
快照更新替换整个文件；缩小扫描范围会在预览中列出范围外旧条目的删除，务必核对 scanned_documents 和 removed_count。
确认后以同一范围和覆盖规则执行 `--snapshot`，然后再次 check；两次运行之间来源变化时必须重新复核。
可运行闭环见 [14 步演示](../examples/review/README.md)。

## 输出与退出码

每次输出检查文件数、实际检查引用数、失败数、待复核数，以及跳过的网页和未绑定来源的代码块数量。`LOCAL_OK` 仅针对实际检查的本地引用。

| 退出码 | 含义 |
| --- | --- |
| 0 | 实际检查的引用通过；严格模式下没有待复核项 |
| 1 | 参数、读取输入或写入快照失败 |
| 2 | 引用不匹配、来源不可用、来源改变、零条可检查引用；严格模式下也包含待复核项 |

`--json` 输出结构化结果；`--github` 输出 GitHub 文件行号诊断。代码块没有 `source=` 会计入未检查数量，不会被执行；引文没有明确来源会报告 `UNASSOCIATED_QUOTE`。不存在的来源、失效锚点、不匹配片段分别用不同错误码。配置解析和文件读写错误返回 1，不会伪装成检查通过。

常用命令：

```sh
moon run cmd/main check docs README.md --strict
moon run cmd/main check docs --exclude docs/drafts --json
moon run cmd/main check docs --root /path/to/repository --github --strict
```

## 依赖与原有接口

本地检查只读取仓库内的文件；不需要 Agent、模型 API、Python、Node、数据库或后台服务。源码方式需要 MoonBit，独立 native 二进制无需 MoonBit 运行时安装。

网页回源复用原有 HTTP/HTML 实现，访问限制见 [HTTP 回源行为](contract.md#http-回源行为)。JSONL 与原来的 Agent 校验入口保留兼容，见 [原有输入约定](contract.md)；它们是可选接入方式。

0.7.0 的 check/init --json 报告增加 schema_version、tool_version 与逐项 category，原有字段保留。
完整字段、退出码及未知值处理见 [JSON 接入契约](json-report.md)；复用配置的 CI 模板见 [库与 CI 样例](../examples/library/README.md)。
