Poietra/manim-lint
GitHub: Poietra/manim-lint
用 Rust 编写的 Manim 场景静态分析器,在渲染前检测正确性、渲染、性能和确定性等问题。
Stars: 0 | Forks: 0
# manim-lint
**为 [Manim Community](https://www.manim.community/) 场景提供静态分析 —— 在渲染前捕获确定的运行时错误、静默的错误渲染、性能乘数以及非确定性。**
English | [日本語](README.ja.md)
`manim-lint` 是一个独立的静态分析器,适用于 Manim Community **0.20**
项目,使用 Rust 编写。它解析你的 Python 源代码,并将其与
精选的、带有版本控制的 Manim 语义模型进行检查 —— 它**从不导入或
执行** Manim 或你的代码。它不进行 API 名称的模式匹配,而是运行
一个生命周期抽象解释器来模拟 `Scene.play` 实际所做的
事情(参数编译、自动添加、引入器/移除器、更新器),以及一个
知道哪些代码只运行一次、哪些代码每帧运行的
符号成本模型。
## 示例
`scenes/demo.py`:
```
from manim import *
class TrackerDemo(Scene):
def construct(self):
title = Text("Tracking x", font_size=0)
square = Square()
tracker = ValueTracker(0)
label = always_redraw(lambda: MathTex(f"x={tracker.get_value():.2f}"))
self.add(title, square, label)
self.play(square.shift(RIGHT))
square.add_updater(lambda m: m.rotate(0.05))
self.play(tracker.animate.set_value(8), run_time=8)
self.wait(0)
```
```
$ manim-lint check . --format concise
scenes/demo.py:6:46: MLR115 error `Text(font_size=0)` is not positive; text sizing requires font_size > 0
scenes/demo.py:9:39: MLP226 warning Each invocation constructs a `MathTex` and performs a cache-key lookup, and this f-string key varies per frame: every rendered frame can mint a distinct Text/TeX cache key and disk asset (`K_resource ≈ F`). Across the 1 play(s) where this callback provably executes it may create at least ~480 distinct keys.
scenes/demo.py:11:19: MLC102 error `square.shift(...)` mutates the mobject immediately and returns the mobject itself, not an Animation; use `.animate` (e.g. `square.animate.shift(...)`) inside `Scene.play()`.
scenes/demo.py:12:38: MLD301 warning Updater lambda applies `rotate` with a fixed step every frame but declares no `dt` parameter; the motion speed depends on the profile frame rate
scenes/demo.py:14:9: MLC112 warning This `wait()` renders a single frozen frame: nothing makes it dynamic, and the updater registered at line 9 reads frame-varying state without a `dt` parameter, so its visual change never renders during the wait. Pass `frozen_frame=False`, or declare a `dt` parameter on the updater.
scenes/demo.py:14:19: MLC104 error Use a positive `duration`: the literal `0` is non-positive and playing it aborts the render.
```
其中两个会导致渲染崩溃(`MLC102`,`MLC104`),一个会渲染出
不可见的标题(`MLR115`),一个会随着帧率静默改变速度
(`MLD301`),一个会冻结作者期望产生动画的等待
(`MLC112`),还有一个会在每个渲染帧启动外部 TeX 编译器以获取新的缓存
键(`MLP226`)—— 并且 linter 可以估算其
成本:在 60 FPS 下 `run_time=8` 证明至少会生成约 480 个不同的键。
## 它检查什么
规则分为四大类:
- **MLC — 生命周期/正确性。** 确定的运行时错误和生命周期
错误会导致渲染出错误的画面:传给
`Scene.play` 的非 Animation 参数(`MLC102`),在任何
路径上没有调用 `generate_target()` 的 `MoveToTarget`(`MLC107`),没有
`save_state()` 的 `Restore`(`MLC120`),在一次 play 中有两个
动画写入同一个 mobject 的同一个通道
(`MLC108`),被 `Scene.remove(child)` 撤销后又重新添加了存活的
父对象(`MLC115`)。
- **MLR — 渲染。** 代码可以渲染,但结果不是你想要的:Python
转义符破坏了非原生 `MathTex` 字面量中的 TeX 命令(`MLR103`),
导致 Manim 运行时精确搜索失败的资产路径
(`MLR104`),传递给纯 `Text` 的 Pango 标记
(`MLR124`),`Transform(mob, mob)`(`MLR113`)。
- **MLP — 性能。** 带有机器可读证据的成本乘数:
在 updater 或
`always_redraw` 中构造 `Text`/`MathTex`/`SVGMobject`(`MLP201`),每帧生成一个磁盘
资产的随帧变化的 TeX 缓存键
(`MLP226`),每帧不断增长的场景图
(`MLP204`),没有
`dissipating_time` 的 `TracedPath`(`MLP220`)。
- **MLD — 确定性/可移植性。** 在不同机器、
帧率或渲染器之间不同的渲染:没有 `dt` 缩放的
固定每帧步长(`MLD301`),帧回调中未设置种子的
全局随机性
(`MLD302`),在区分大小写的目标上的
仅大小写不同的资产路径不匹配(`MLD305`)。
### 语义深度,而非名称匹配
分析器的核心原则(DESIGN §1):绝不单独对 API 名称发出警告。
`FadeOut(mob)` 用于从未添加过的 mobject 是没问题的 —— play 的准备工作
会自动添加它,随后移除器会将其删除。因此流水线会首先
构建真实的事实:
- **生命周期抽象解释器**:函数内 CFG,过程间
助手摘要,带有 `super()` 分派的每个 Scene MRO 组合,
分配点身份,场景成员资格/顺序/updater 追踪,
以及 play 组语义;
- **符号成本模型**:热上下文传播(updater,
`always_redraw`,停止条件,插值覆盖)和仅从
字面持续时间推导出的帧计数区间 —— 上面的成本报告说
`duration 8 s -> frames ~480`,因为在 60 FPS 下 `run_time=8` 是可证明的,
否则打印 `unknown`,绝不是捏造的数字。
每个诊断都将**严重性**(`error`/`warning`/`info`)与
**置信度**(`certain`/`high`/`medium`/`low`)区分开来,并且依赖于状态的规则
只在确定的、全路径证据下才会触发。当一个值无法进行静态
解析时,它会降级为 `Unknown`,并且 linter 会保持**沉默,而不是
猜测** —— 这是一个刻意的设计立场,贯穿于每一条规则。
## 安装说明
对于已发布的版本,请选择以下任何一个入口点。PyPI 包
安装的是原生的 Rust 可执行文件;它不导入 Manim,安装后也
不需要 Python 运行时。
```
# Python tooling
uv tool install manim-lint
# or: pipx install manim-lint
# Rust tooling (builds from source; Rust 1.85+)
cargo install manim-lint --locked
```
适用于 Linux、macOS 和 Windows 的独立安装程序和带有校验和的压缩包
附在每个 GitHub Release 中:
```
# macOS / Linux
curl --proto '=https' --tlsv1.2 -LsSf \
https://github.com/Poietra/manim-lint/releases/latest/download/manim-lint-installer.sh | sh
```
在首次注册表发布之前,或者要安装当前检出的代码,请从
源码构建:
```
git clone https://github.com/Poietra/manim-lint.git
cd manim-lint
cargo install --path .
```
不需要安装 Python、Manim 或 LaTeX:分析器解析
源代码并参考带有版本控制的知识配置文件,从不导入或
执行 Manim 或其分析的代码。
## 快速入门
```
manim-lint check . # analyze; rich in a terminal, concise when piped
manim-lint check . --format rich # force source frames and colour
manim-lint check . --format concise # one line per diagnostic
manim-lint check scenes --format full # explanations + evidence
manim-lint check . --format json # schemas/diagnostics-v1.json
manim-lint check . --format sarif # SARIF 2.1.0
manim-lint check . --format github # GitHub Actions annotations
manim-lint explain MLC102 # full documentation for a rule
manim-lint rules # every rule ID, phase, and status
manim-lint config # resolved effective configuration
manim-lint cost scenes/demo.py # per-scene cost breakdown
manim-lint coverage . # what the analysis could not resolve
manim-lint static-facts . > facts.json # StaticFacts v0 semantic projection
manim-lint change-impact --before old --after new > impact.json
manim-lint source-bridge . --request patch.json > candidates.json
```
退出代码:`0` —— 没有报告的诊断达到 `fail-level`;`1` —— 至少
有一个达到;`2` —— 命令行、配置或内部错误。
### 输出格式
如果不使用 `--format`,`check` 会根据其写入的位置选择输出。
附加到终端时,它会打印 `rich` 格式:每个发现对应一个横幅,带有
下划线范围的违规
源代码行、解释以及摘要。
```
✖ MLC104 scene.py:10:42 ───────────────────────────────────────────────
Use a positive `run_time`: the literal `0` is non-positive and playing
it aborts the render.
8 │ group = AnimationGroup()
9 │ self.add(title, eq)
> 10 │ self.play(Write(title), run_time=0)
│ ^
11 │ self.wait()
ℹ Manim validates durations when a play executes, not when an animation
is constructed …
✖ 2 errors ⚠ 1 warning in 1 file
```
当重定向到文件或管道时,它会打印 `concise` 格式 —— 每个
诊断对应一行稳定的内容,没有转义序列 —— 这样脚本和 CI 就能保持
它们今天所解析的格式。传递 `--format` 可以在任一方向上覆盖此
选择。
颜色遵循 `--color auto|always|never`。`auto` 仅对终端使用样式,
`NO_COLOR`(任何值)会禁用样式,而 `--color always` 即使
在重定向时也会使用样式。只有 `rich` 才会带有样式。`COLUMNS` 设置横幅
和换行使用的宽度。
有用的 `check` 选项:`--select` / `--ignore`,`--min-confidence`,
`--fail-level`,`--profile`,`--renderer`,`--fps`,
`--resolution WIDTHxHEIGHT`,`--color`,`--statistics`,`--analysis-summary`(即
下方的覆盖率报告,在诊断后打印到 stderr;stdout
和退出代码不受影响),以及下文描述的 baseline/fix 选项。
`--no-cache` 强制进行完整的分析,而不读取、
写入或创建缓存状态。`--select` 还会缩小分析本身:不需要的事实层
(生命周期解释器,符号成本
模型)会被跳过,因此缩小选择范围比完整运行更快。
无论哪种方式,报告的诊断都是相同的 —— 取代
所选规则的规则仍会运行,因此缩小选择范围绝不会恢复被取代的
诊断。
`--format full` 会在
每个诊断下打印解释和机器可读的证据:
```
scenes/demo.py:9:39: MLP226 warning Each invocation constructs a `MathTex` and performs a cache-key lookup, ...
A frame-varying key defeats the `MathTex` cache: instead of one shaping/compile job reused every frame,
each frame pays construction plus a cache miss, and for TeX classes each distinct key also launches the
external TeX compiler and `dvisvgm`, leaving one disk asset per key. ...
evidence.distinct_resource_keys: {"lower":480,"upper":null}
evidence.execution: {"plays":[{"certainty":"proven","kind":"play","location":"scenes/demo.py:13:9"},{"certainty":"maybe","kind":"play","location":"scenes/demo.py:11:9"}],"unresolved_entries":false}
evidence.frames: {"lower":480,"upper":null}
evidence.invocation_context: "frame-callback"
evidence.multiplicity: ["frames"]
evidence.state_path: ["construct","always_redraw:9"]
applies to profiles: production
```
## 分析缓存
正常的 `check` 运行会在
`.manim-lint-cache/cache-v2.sqlite3` 处保留一个一次性的 SQLite 缓存。相同的第二次运行会验证
文件系统依赖项,并在不
启动前端的情况下重用整个项目的诊断 JSON。在编辑源代码后,缓存 v2 仍然会解析并索引
完整的项目,然后使用解析出的导入、调用、
基类和模块名
冲突将项目文件划分为弱依赖
组件。未更改的组件重用 JSON 方法摘要和过滤后的
诊断;只有更改后的组件重新运行摘要、Scene 生命周期
和成本分析。AST 和已分析的代码从不被序列化或执行。
键涵盖了分析器构建、解析出的语义配置、Manim
知识配置文件、完整的源代码布局以及相关的源代码字节。字面
资产候选者和区分大小写的目录遍历会按条目标记,
因此资产更改会使受影响的组件失效。SQLite WAL 数据库
支持并发冷写入,并保留 16 个最近使用的
项目快照以及 256 个组件快照。它从来不是正确性所必需的:
损坏会通过警告重建,其他失败会继续
进行分析。`--no-cache` 禁用所有缓存文件系统活动。`--fix`、
baselines 和 `--analysis-summary` 特意运行完整的分析,
因为它们在生成诊断后需要实时的源码或索引状态。
将 `.manim-lint-cache/` 添加到项目的忽略文件中。较旧的
`cache-v1.sqlite3` 文件不会被使用,可以删除。
冷运行使用有限的工作池并行化独立的摘要组件、Scene 生命周期运行
和规则。递归的摘要不动点和
前端/项目索引保持有序和顺序执行。输出会被收集并
稳定排序,测试证明在 1 个和 4 个工作进程下 JSON 字节完全相同。
## 配置
配置存在于 `pyproject.toml` 的 `[tool.manim-lint]` 中,通过
从检查的路径向上遍历来找到。渲染配置文件是
`[[tool.manim-lint.profile]]` 条目:
```
[tool.manim-lint]
manim-version = "0.20"
target-python = "3.11"
select = ["MLC", "MLR", "MLP", "MLD"]
ignore = []
min-confidence = "high"
fail-level = "warning"
default-profile = "production"
knowledge-profile = "upstream_0_20"
respect-manim-cfg = true
exclude = [".venv/**", "media/**"]
per-file-ignores = { "tests/fixtures/**" = ["MLP", "MLD"] }
[[tool.manim-lint.profile]]
name = "production"
renderer = "cairo"
platform = "linux"
pixel-width = 1920
pixel-height = 1080
frame-rate = 60
assets-dir = "."
allowed-fonts = ["Noto Sans", "Noto Sans CJK JP"]
```
优先级,从高到低:
```
CLI > selected profile > pyproject base > manim.cfg > builtin defaults
```
当启用 `respect-manim-cfg`(默认值)时,`manim.cfg` 会
在 pyproject 设置下提供 resolution/fps/renderer 的默认值。未知的键、
未知的规则选择器、重复的配置文件名和未知的配置文件
引用都是配置错误(退出代码 2)。`--profile all` 会分析每个
定义的配置文件,并合并具有相同证据的诊断,列出每个诊断受影响的
配置文件。
配置得到了诚实的验证(违反时退出代码 2):
- 声明的 `manim-version` 必须落在配置的
知识配置文件所支持的 Manim 范围内(例如 `upstream_0_20` 支持
`>=0.20,<0.21`);不存在时,不验证任何内容。
- `target-python` 必须是介于 3.6 和 3.12 之间的 `MAJOR.MINOR`。上限
是捆绑的解析器(rustpython-parser 0.4)
实现的 Python 语法;下限是
无法再保证语法控制的底线(更早的目标会被拒绝并退出代码 2,而
不是被静默地不予执行)。语法是固定的(没有
`feature_version` 锁定),因此解析本身永远不会改变;相反,对
AST、token 流和 f-string 文本的解析后控制会
将所有比目标更新的构造报告为 `MLC000`:
`async def` 之外的 `async`/`await` 语法 (3.7),`:=`,仅限位置参数的
`/`,以及 f-string 自记录的 `=` (3.8),带有 `as` 的宽松装饰器和带括号的上下文管理器
(3.9),`match` (3.10),下标中的
`except*` 和 PEP 646 `*` 解包 (3.11),`type`
别名,PEP 695 类型参数和 PEP 701 f-string 表达式
(3.12)。保证控制静默通过的文件可以被
目标自身的解析器解析。受控制的文件仍会被完全分析,并且
引入此类语法的 `--fix` 将被回滚。有关
完整的覆盖范围表,请参见 `manim-lint explain MLC000`。
- 帧率为零、负数或非有限,以及分辨率的
维度为零,无论它们来自哪里(`--fps` /
`--resolution`,配置文件,还是
`manim.cfg`),都会被拒绝。
- `stub-paths` 尚未实现;非空列表将被拒绝,
而不是被静默忽略。
`manim-lint config` 打印解析出的配置以及
`enforcement` 部分,说明哪些设置是强制执行的,哪些是
信息性的。
## 使用优化的 fork 配置文件
使用本地修补过的 Manim fork 渲染的项目(配置文件
`local_0_20_1_4d25c031`)可以告诉 manim-lint 这样做,并解锁
特定于 fork 的分析层:
```
[tool.manim-lint]
knowledge-profile = "local_0_20_1_4d25c031"
default-profile = "production"
[[tool.manim-lint.profile]]
name = "production"
renderer = "cairo"
platform = "linux"
cairo-fork-workers = 4
cairo-static-layers = true
```
这在上游配置文件提供的所有功能之上,还启用了:
- **`manim-lint cost` 中的“fork fast paths”部分**:每次 play,是否应用 fork-per-play Cairo 流水线(`cairo-fork-workers`),静态层
保留路径(`cairo-static-layers`),以及打包插值 ——
当它们没有应用时(例如由于 Scene updater),会给出确切的阻碍因素及其源代码范围,包括在第一次串行 play 之后渲染器范围的单调禁用链。该部分从不建议移除某个功能;它
解释渲染路径的后果。
- **`MLP214`**:标记在场景的第一次 play 之前串行构造的
四个或更多不同的 TeX 编译键,并引用 fork 的预编译
API(`MathTex.precompile`,`tex_to_svg_file_async`)。
- **`MLP217`**:标记在热回调中随帧变化的
`use_svg_cache=True` 键,这些键会导致 fork 声明的进程全局 SVG 缓存每帧
增长。
- **`MLP225`**(通过 `--select MLP225` 选择性启用):将成本报告的
fast-path 阻碍因素解释作为每次 play 的诊断输出。
在 `upstream_0_20` 下,以上所有内容都是无效的:成本报告不包含
fork 部分,并且这三个规则从不触发,即使被选中也是如此。
## 抑制
```
self.play(square.shift(RIGHT)) # manim-lint: ignore[MLC102] # same statement
# manim-lint: ignore[MLP201] # next statement
label = always_redraw(...)
# manim-lint: file-ignore[MLP] # whole file; must appear in the file header
```
抑制针对的是**整个语句**,而不是单行:行内尾部的
注释(或者紧接其上方的独立注释)涵盖了整个
语句,包括多行调用的续行,因此
固定在语句内任何位置的诊断都会被抑制。对于
复合语句(`def`,`for`,`if`,`with`,...),抑制
仅涵盖到其冒号为止的头部 —— 一个注释永远无法静默
整个代码套件。
内联抑制中未知的规则 ID **不会**抑制
任何内容;它将作为专门的警告被报告:
```
scene.py:8:41: MLC001 warning unknown rule ID in suppression: MLC999
```
对于整个目录,请使用 `pyproject.toml` 中的 `per-file-ignores`(参见
上文)。
## 逐步采用:baselines
在现有项目上采用 linter,而无需先修复所有问题:
```
manim-lint check . --write-baseline .manim-lint-baseline.json # record today's findings
manim-lint check . --baseline .manim-lint-baseline.json # report only new findings
```
Baseline 指纹(`schemas/baseline-v1.json`)**不包含行
号** —— 它们由规则 ID、相对路径、限定的场景名称
和周围的 token 哈希构建 —— 因此在文件的其他位置插入不相关的行不会
使条目失效。`scene` 字段记录了限定的
封闭 Scene 类(在任何场景之外为空),因此不同场景中的相同发现
会获得不同的指纹。写入的文件带有一个
`scene_attribution: "attributed"` 出处标记:它们为空的 `scene`
字面意思就是“在任何 Scene 之外”,并且完全匹配。在
场景归属之前写入的 Baselines(没有标记)仍然会被读取,只有在这些 Baselines 中
空的 `scene` 才会作为通配符匹配。损坏或 schema 错误的 baseline
文件会以清晰的消息退出,代码为 2。
## 自动修复
```
manim-lint check . --fix # apply SAFE fixes only
manim-lint check . --fix --unsafe-fixes # also apply UNSAFE fixes
```
安全和不安全的修复是严格分开的:单独使用 `--fix` 仅应用
保留行为的编辑(例如 `MLC127` 从一个 `add()`/`VGroup()` 调用中移除重复的子对象,`MLR104` 更正仅大小写不同的资产路径)。
不安全的修复可能会更改运行时语义(例如为了 `MLC102` 将
`play(mob.shift(...))` 重写为 `play(mob.animate.shift(...))`),并且
需要显式的额外标志。每个修复后的文件都会被重新解析以进行
验证;修复未通过重新解析的文件将被回滚。
```
$ manim-lint check . --fix
scene.py:8:40: MLC127 info Remove the duplicate `square` from this `VGroup(...)` call: Manim warns and ignores repeated children of a single add.
fixed 1 issue(s) in 1 file(s)
```
## 成本命令
`manim-lint cost` 打印每个场景的符号成本细分 —— 包含帧间隔的 play 列表,带有来源以及回调可证明执行的那些 play 的热上下文,每帧的构造,以及资源键
增长。未知持续时间打印为 unknown,从不是捏造的
数字:
```
$ manim-lint cost scenes/demo.py
profiles: production (cairo, 1920x1080, 60 fps)
scene scenes.demo.TrackerDemo (scenes/demo.py)
plays:
scenes/demo.py:11:9 play duration unknown -> frames per-frame
scenes/demo.py:13:9 play duration 8 s -> frames ~480
scenes/demo.py:14:9 wait duration 0 s -> frames ~0
hot contexts:
scenes/demo.py:9:31 entry always_redraw; path construct -> always_redraw:9; factors frames; proven execution plays: scenes/demo.py:13:9
scenes/demo.py:12:28 entry updater; path construct -> updater:12; factors frames; proven execution plays: scenes/demo.py:13:9
per-frame constructions:
scenes/demo.py:9:39 MathTex construction x at least ~480 invocations across 1 proven play(s)
resource-key growth:
scenes/demo.py:9:39 MathTex distinct cache keys: at least ~480 across 1 proven play(s) (f-string key varies per frame)
```
在本地 fork 知识配置文件下,报告会获得每个场景的
“fork fast paths”部分(见下文)。例如,在配置文件中有
`cairo-fork-workers = 4` 和 `cairo-static-layers = true` 时:
```
$ manim-lint cost scene.py
...
fork fast paths (profile production, knowledge local_0_20_1_4d25c031):
fork-per-play (cairo_fork_workers 4):
scene.py:9:9 play #1: no static blocker found (fork-eligible pending the runtime audit)
scene.py:10:9 play #2: no static blocker found (fork-eligible pending the runtime audit)
static layers (cairo_static_layers on):
scene.py:9:9 play #1: no static blocker found
scene.py:10:9 play #2: no static blocker found
packed interpolation:
scene.py:9:9 play #1: canonical per-member interpolation because the animation type FadeIn is outside the audited allowlist at scene.py:9:19 (blocker unsupported_animation_type); an updater-bearing mobject is in the scene family (updater registered here) at scene.py:8:9 (blocker updater_bearing_family)
scene.py:10:9 play #2: canonical per-member interpolation because an updater-bearing mobject is in the scene family (updater registered here) at scene.py:8:9 (blocker updater_bearing_family)
evidence: measured packed interpolation on the calibration machine, 300 members / 60 frames: 130.658 -> 33.004 ms/play, steady state 2.0761 -> 0.1890 ms/frame (docs/research/perf-evidence.md)
note: the features named above can be correct expression; this section explains the render-path consequence and never advises removing them
```
## 分析覆盖率
分析器的保守静默是正确的但不可见:一次干净的
运行不会告诉你是不存在问题,还是项目的一半
无法分析。`manim-lint coverage`(以及
`manim-lint check --analysis-summary`,它会将相同的报告打印到
stderr,而不触及 stdout 或退出代码)展示了分析**无法**解析的所有
内容:
对于一个包含来自无法解析模块的星号导入,逃离了项目树的相对导入,高于
`target-python = "3.9"` 的 `match` 语句,以及包装在未解析的助手调用中的 play 的文件:
```
$ manim-lint coverage .
analysis coverage (knowledge profile upstream_0_20, target-python 3.9)
scene.py
constructs above target-python (MLC000): 1
star imports from unresolved modules: 1
unresolved relative imports: 1
calls with no resolved target: 1 of 6 (mystery x1)
scene scene.Demo (scene.py)
plays with unknown duration: 1 of 2
.animate builders with unknown target: 0 of 0
project
files parsed: 1 of 1
calls resolved: 5 of 6
play durations known: 1 of 2
scene constructors resolved: 1 of 1
constructs above target-python (MLC000): 1
unresolved imports: 2 (1 star, 1 relative)
manim APIs not in the knowledge profile: 0
helper calls summarized, not inlined: 0
top unresolved calls: mystery x1
analysis confidence: 1/1 files parsed, 5/6 calls resolved, 1/2 play durations known, 1/1 scene constructors resolved (counts of analyzed facts, not estimates)
```
每个数字都是计算事实的计数;唯一的比率是简单的
`resolved / total` 计数对。`--format json` 将相同的数据作为
稳定的、机器可读的文档发出,包含顶层键
`knowledge_profile`,`target_python`,`files[]`(`path`,`parsed`,
`gated_constructs`,`unresolved_star_imports`,
`unresolved_relative_imports`,`calls`,`unresolved_calls`,
`unresolved_call_names`,`apis_not_in_profile`),`scenes[]`(`name`,
`path`,`constructor_state_unknown`,`plays`,
`plays_with_unknown_duration`,`builders`,
`builders_with_unknown_target`),以及 `project`(总数加上
`top_unresolved_call_names` 和 `helper_inline_fallbacks` —— 这些是指
内联回退到效果摘要的助手调用点,记录在
项目范围内,并在共享助手链的场景中去重,因此
计数仅出现在 `project` 上)。对于相同的输入,输出是确定性的
且字节稳定的。
## CI 集成
直接在 PR diff 上的 GitHub Actions 批注:
```
name: manim-lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install --path . --locked
working-directory: manim-lint # path to your manim-lint checkout
- run: manim-lint check . --format github
```
或者上传 SARIF,以便发现出现在 GitHub 代码扫描 UI 中:
```
- run: manim-lint check . --format sarif > manim-lint.sarif
continue-on-error: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: manim-lint.sarif
```
## 预执行准入检查
因为 `manim-lint` 从不导入或执行它读取的代码,所以
渲染用户提供场景的服务可以在花费 sandbox、
CPU 和 GPU 时间*之前*运行它 —— 拒绝在渲染时一定会失败的场景,并
标记成本模型预测会爆炸的场景。
不受信任的输入需要整个契约,而不仅仅是规则:
- **限制是被强制执行的,而不是假定的。** 超过 4 MiB 的源码、超过
96 的嵌套深度,或超过 64 的前缀运算符运行,会在
解析之前作为 `MLC000` 被拒绝,因此恶意输入无法耗尽堆栈(参见
[MLC000](docs/rules/MLC000.md))。其他一切都仍然是调用者的
预算:对进程施加你自己的运行时间和内存限制。
- **从观察模式开始。** 在真实渲染结果旁边记录发现,而不是
基于它们进行阻断;只有当规则的
预测通过实际渲染的检查后,才将其提升为阻断。
- **仅在 `certain` 时阻断。** 正确的源代码可能会故意带有错误严重性的
发现 —— 断言 `VGroup(3.0)` 会引发异常的测试就是
最典型的情况。在真实语料库上测量的发现位于
[docs/research/corpus-evidence.md](docs/research/corpus-evidence.md) 中。
```
# Observe: record everything, never fail the request.
manim-lint check "$SCENE_DIR" --format json --fail-level error > findings.json || true
# Block: refuse only what the analyzer is certain about.
manim-lint check "$SCENE_DIR" --format json \
--min-confidence certain --fail-level error
```
退出代码 1 表示已达到阈值;退出代码 2 表示使用或
配置错误,绝不应被视为被拒绝的场景。
## 规则目录
该目录包含四大类的 92 个规则 ID;**所有 92 个都已
实现**:
| 类别 | 已实现 | 已保留 |
| --- | --- | --- |
| MLC 生命周期/正确性 | 31 | 0 |
| MLR 渲染 | 27 | 0 |
| MLP 性能 | 27 | 0 |
| MLD 确定性/可移植性 | 7 | 0 |
一个已实现的规则是选择性的:`MLP225` 具有 `default_enabled: false` 并且
从不加入正常的 `check` 运行;只有在本地 fork 配置文件下精确的 `--select MLP225` 才会对其进行评估。
带有每个规则状态、严重性和置信度的完整索引位于
[docs/rules/README.md](docs/rules/README.md) 中;每个已实现的规则在那里都有一个
文档页面,也可以通过 `manim-lint explain ` 获得。
## 架构
```
Python sources
|
SourceManager ............ encoding (PEP 263), newlines, Unicode columns
|
knowledge profile ........ versioned Manim 0.20 semantics (no import, ever)
|
frontend ................. imports/aliases, project index, qualified call facts
|
semantic ................. lifecycle abstract interpreter -> LifecycleFacts
|
cost ..................... hot contexts, frame intervals -> CostFacts
|
rules .................... MLC / MLR / MLP / MLD over the fact layers
|
suppressions, supersedes, baseline
|
output ................... concise | full | json | sarif | github, fixes, cost report
```
[docs/architecture.md](docs/architecture.md) 引导新的贡献者
了解此流水线:每个事实层提供什么,它位于何处,
知识配置文件系统,以及一个诊断是如何进行端到端流转的。
[`DESIGN.md`](DESIGN.md) 是语义模型、规则目录
和每个公共契约的权威规范。JSON 输出遵循
[`schemas/diagnostics-v1.json`](schemas/diagnostics-v1.json);baselines
遵循 [`schemas/baseline-v1.json`](schemas/baseline-v1.json)。由
`manim-lint static-facts` 发出的 Poietra/fast-manim 语义桥接由
[`StaticFacts v0`](docs/rfcs/0001-static-facts-v0.md) 及其
[`JSON Schema`](schemas/static-facts-v0.json) 指定。它发布快照范围内的
Scene/object/play/animation/updater ID,编码感知的源锚点,
带有原因的未知项,渲染器风险,以及覆盖率边界,而不会
暴露分析器句柄。它报告阻碍因素,但从不授予跳过或
fork 渲染的权限。对于相同的输入,输出是确定性的
且字节稳定的。
独立于缓存的
[`SemanticDependencyGraph v0`](docs/rfcs/0002-semantic-dependency-graph-v0.md)
是用于缓存组件分区和保守
源代码更改影响的共享事实层。它保留锚定的 Unknown 边界,而不是
猜测动态依赖边缘。
[`ChangeImpact v0`](docs/rfcs/0003-change-impact-v0.md) 比较两个源
快照,并发出经过 schema 验证的、带有原因的 Scene/play/object 影响
候选对象,包括从目标树中删除的关系。
[`SourceBridge v0`](docs/rfcs/0004-source-bridge-v0.md) 生成受哈希保护的
本地补丁候选对象,根据内存中的重新分析对它们进行验证,并且
报告 `match | ambiguous | missing` 而不写入项目文件。
## 已知局限性
- **目标版本。** 按照发布,知识配置文件涵盖 Manim Community
**仅 0.20** 版本。其他版本尚无配置文件。
- **资产检查探测运行 lint 的机器。** `MLR104` 使用 Manim 自身的运行时搜索,在运行
lint 的机器上解析字面
资产路径。对于项目树之外的绝对路径,这是关于
lint 主机的证据,不一定是渲染主机的(例如,CI 对在其他地方渲染的存储库进行 lint);这些诊断带有
`environment_dependent: true` 作为证据。仅大小写不匹配的报告
仅针对区分大小写的目标平台(`linux`);当所有受影响的配置文件都针对
windows/macos 时,声明的渲染会按原样解析文件,并且
linter 保持沉默。
- **源代码编码。** PEP 263 声明通过 WHATWG 标签
加上 CPython 编解码器别名表(`latin-1`,`cp932`,`koi8_r` 等)进行解析。
linter 无法表示的罕见 Python 编解码器将通过明确的
`MLC000` “not supported by manim-lint”通知被跳过 —— 绝不意味着
目标 Python 无法解码该文件。
- **持续时间仅来自字面量。** 一个持续时间依赖于
Manim *默认值*的 play(`self.play(m.animate.shift(RIGHT))` 在任何地方都没有
`run_time`,`self.wait()`)被报告为 unknown —— 帧计数
使用每帧的措辞而不是数字(保守:缺失,从不
捏造)。字面的 play 级别的 `run_time` 精确地决定整个 play 的
持续时间 —— 包括 Scene 助手中的 play,每个调用点,
即使调用点向参数上的 `.animate` builder 传递不同(或未追踪)的 mobject —— 并且一个*非字面量*的 `run_time`
(或 `**kwargs` 展开)诚实地扩大了它所覆盖的构造函数字面量。
- **源自摘要的 play 是保守的。** 当助手内联回退到
效果摘要时(递归,一个无法解析的调用 —— 在覆盖率报告中计为
`helper calls summarized, not inlined`),助手的 play 仍然会
实例化,但作为具有开放重复次数的 `Maybe` 确定性记录:诸如 `MLC104` 之类的字面持续时间检查仍会在那里触发,
而每个依赖于调用者状态的判断都会保持降级状态。
- **`TracedPath` 在构造函数中注册的 updater 仅用于成本计算。**
`TracedPath` 在构造时在自身上注册的 updater 是为了成本目的而建模的(`MLP220` 范围,traced lambda 的热上下文条目
但它不是生命周期 updater 注册:在生命周期模型中,单独的 `TracedPath`
不会使默认的 `wait()` 动态化,并且
绑定的方法 `traced_point_func` 主体不会被分析为热上下文。
- **故意的保守静默。** 一些检测比
其目录描述的范围更窄,并保持沉默而不是猜测:`MLR106` 仅看到
字面形式的 NaN/inf,而不是通过 `float("nan")` 调用;`MLD301`
仅证明缺少 `dt` 参数的 updater 的 FPS 依赖性(
声明但未使用的 `dt` 不被标记);`MLC113`/`MLC124` 仅识别
它们记录的调用形状;`MLR102` 需要解释器证明
播放的裸 builder 的目标未更改;`MLR105` 验证已验证的
Pango 子集(允许裸 `&`);`MLD304` 仅实现
ThreeDScene 固定对象清理差异。`manim-lint explain `
说明了每个规则的确切范围。
- **尚未实现。** 针对渲染 baselines 的阈值校准;
每晚的渲染比较 CI。
## 开发
```
cargo fmt --check
cargo build
cargo test --all-features
cargo clippy --all-targets --all-features -- -D warnings
```
必须通过所有四个关卡。
知识配置文件维护:`sync_manim_knowledge` 二进制文件静态地
读取 Manim 检出,生成可审查的配置文件候选对象,并检查
发布的配置文件是否存在偏移(矛盾时退出代码 1)—— 参见
[src/knowledge/profiles/README.md](src/knowledge/profiles/README.md)。
来源被拆分:`upstream_0_20` 描述了**干净**的上游基础
提交 `4d25c031`(通过 `git archive` 读取,从不读取工作树),并且
`local_0_20_1_4d25c031` 覆盖层带有同属 fork 的工作
树在其之上添加的内容:
```
# working tree (fork) — informational against upstream
cargo run --features dev-tools --bin sync_manim_knowledge -- --manim-root ../manim --diff
# clean upstream base — must be contradiction-free
cargo run --features dev-tools --bin sync_manim_knowledge -- --manim-root ../manim --manim-ref 4d25c031 --diff
cargo test --test knowledge_drift -- --ignored # layer-9 drift gate (both)
```
### 发布质量关卡(DESIGN §11.4)
三个额外的关卡保护发布:
```
# Labeled corpus gate — runs automatically inside `cargo test`.
# tests/corpus/manifest-v1.json pins sha256 + exact expected diagnostics
# (true positives and false-positive guards) for every corpus case,
# including pinned real-Manim example_scenes snapshots and the
# adversarial review probes.
cargo test --test corpus_gate
# Benchmark gate — explicit, release build, quiet machine.
# Cold ≤ 2 s / warm hit ≤ 0.5 s / one-of-20 incremental ≤ 0.5 s /
# peak RSS < 300 MiB over the pinned 10k-LOC fixture
# (tests/corpus/benchmark_10kloc); thresholds assert only on the machine
# matching benchmarks/reference-machine.json, informational elsewhere.
# The gate proves cold is a miss, warm a validated hit, and incremental a partial hit.
# Three-run median on the reference machine (2026-07-20): cold 0.422 s,
# warm 0.006 s, incremental 0.171 s, peak RSS 246.5 MiB — all within budget.
cargo test --release --test benchmark_gate -- --ignored benchmark
# Knowledge drift gate — needs the sibling Manim checkout; in CI it runs
# on schedule/dispatch against a shallow clone of the pinned base commit.
cargo test --test knowledge_drift -- --ignored
```
语料库案例绝不会机械地重新记录:不匹配意味着
在
[CONTRIBUTING.md](CONTRIBUTING.md#corpus-labeling) 中的标记协议下重新裁定。
有关
仓库布局、添加规则的分步指南以及
每次更改必须保持的不变量,请参见
[CONTRIBUTING.md](CONTRIBUTING.md);有关流水线和
事实层概述,请参见
[docs/architecture.md](docs/architecture.md)。`DESIGN.md` 是权威的;对
公共契约的更改必须更新它、其 schema 测试以及规则文档
。
## License
[MIT](LICENSE)。
依赖许可证,以及在发布
预构建二进制文件之前值得了解的一个后果 —— Python 解析器引入了一个 LGPL-3.0-only 的大整数
crate,Rust 将其静态链接 —— 记录在
[THIRD-PARTY-LICENSES.md](THIRD-PARTY-LICENSES.md) 中。从源码
安装(`cargo install`)不受影响。
预构建的发布版本包含 LGPL/GPL 文本、精确锁定的源代码,以及
[重新链接说明](RELINKING.md),适用于所有发行格式。如果
缺少该材料,发布关卡将拒绝发布。
`manim-lint` 是一个独立的项目。Manim Community 未参与
也不认可它。
标签:Manim, Python, Rust, SOC Prime, 代码质量检查, 动画引擎, 可视化界面, 开发工具, 无后门, 网络流量审计, 逆向工具, 通知系统, 错误基检测, 静态代码分析