Understand-Anything 的 .understandignore:让用户精确控制代码分析边界的可配置排除机制
本文基于 Understand-Anything 仓库中的实施计划 2026-04-10-understandignore-impl.md,完整讲解 .understandignore 用户可配置文件排除机制的设计与落地:包括 IgnoreFilter 的分层模式加载、IgnoreGenerator 的 starter 文件自动生成、project-scanner 扫描流程中的 filteredByIgnore 统计,以及 /understand 技能 Phase 0.5 的人工审核暂停点。读完你可以掌握该机制的完整源码实现,并能在自己的项目中正确编写 .understandignore 规则来缩减分析范围、控制 token 消耗。
设计目标与总体架构
.understandignore 是 Understand-Anything 为「分析范围控制」引入的配置文件,采用 .gitignore 语法,让用户无需修改任何硬编码默认值就能把与代码理解无关的文件(vendor 代码、生成产物、测试夹具等)排除出知识图谱分析。该计划的核心目标可以归纳为:
- 允许用户通过
.understandignore排除文件或目录; - 复用
.gitignore语法,零学习成本; - 保留硬编码默认模式作为内建基线,
.understandignore在其上「增量叠加」; - 支持
!取反,强制包含被默认规则排除的文件(如!dist/); - 首次运行时用确定性代码(而非 LLM)自动生成一份注释掉的建议文件;
- 在分析开始前暂停,让用户审核 ignore 文件再继续。
对应的非目标也很明确:不替代 .gitignore(本机制仅服务于分析)、不支持目录级(per-directory)的 .understandignore、不提供图形化编辑界面。完整的设计规格见 2026-04-10-understandignore-design.md。
从源码结构看,整个机制落在四个位置:
| 文件 | 职责 |
|---|---|
| ignore-filter.ts | 解析 .understandignore、与默认模式合并、过滤路径 |
| ignore-generator.ts | 扫描项目结构,生成 starter ignore 文件内容 |
| generate-ignore.mjs | Phase 0.5 调用的 CLI 封装,把 starter 内容写入数据目录 |
| scan-project.mjs | 项目扫描脚本,应用过滤并统计 filteredByIgnore |
测试侧则对应 ignore-filter.test.ts 与 ignore-generator.test.ts 两个 Vitest 测试文件。
IgnoreFilter:分层加载的核心过滤模块
IgnoreFilter 是过滤逻辑的单一事实来源,基于 npm 的 ignore 包(.gitignore 兼容匹配器)实现。其公开 API 非常精简:
export interface IgnoreFilter {
/** 返回 true 表示该相对路径应被排除在分析之外 */
isIgnored(relativePath: string): boolean;
}
export function createIgnoreFilter(projectRoot: string): IgnoreFilter;
在 ignore-filter.ts 的当前实现中,模式按以下顺序增量合并加载——后加载的条目可以通过 ! 取反覆盖先前条目:
- 硬编码默认模式(
DEFAULT_IGNORE_PATTERNS)——与project-scanner代理的排除规则对齐; - 数据目录下的
.understandignore——即.ua/.understandignore,若项目此前已存在旧的.understand-anything/目录则沿用该位置(由resolveUaDir解析,见 persistence/index.ts); - 项目根目录的
.understandignore——为了可见性提供的备选位置; - CLI
--exclude传入的模式(extraPatterns参数)——最高优先级,位于最后加载。
需要说明的是,计划文档中最初只定义了前三层;CLI --exclude 这一层是后续增强,createIgnoreFilter 的第二参数 extraPatterns 与 scan-project.mjs 的 --exclude 标志共同支撑了这一能力(见 SKILL.md 中对 --exclude "tests/*,docs/*" 的说明)。
硬编码默认模式全表
DEFAULT_IGNORE_PATTERNS 定义了「无论如何都会排除」的基线,按类别分组如下(完整实现见 ignore-filter.ts):
| 类别 | 模式 |
|---|---|
| 依赖目录 | node_modules/、.git/、vendor/、venv/、.venv/、__pycache__/ |
| 构建产物 | dist/、build/、out/、coverage/、.next/、.cache/、.turbo/、target/、obj/ |
| 锁文件 | *.lock、package-lock.json、yarn.lock、pnpm-lock.yaml |
| 二进制/资源 | *.png、*.jpg、*.jpeg、*.gif、*.svg、*.ico、*.woff、*.woff2、*.ttf、*.eot、*.mp3、*.mp4、*.pdf、*.zip、*.tar、*.gz |
| 生成文件 | *.min.js、*.min.css、*.map、*.generated.* |
| IDE/编辑器 | .idea/、.vscode/ |
| 杂项 | LICENSE、.gitignore、.editorconfig、.prettierrc、.eslintrc*、*.log |
一个值得注意的演进:计划文档最初把 bin/ 和 obj/ 都列为默认排除(为 .NET 项目考虑),但当前代码的默认列表中只保留了 obj/,移除了 bin/。从 ignore-filter.test.ts 中的断言 expect(DEFAULT_IGNORE_PATTERNS).not.toContain("bin/") 及其注释「used by Node/Ruby CLI launchers」可以看出,这是因为 bin/ 目录在 Node 与 Ruby 项目中常存放真实的 CLI 启动脚本,误排除会丢掉有效源码——这个取舍体现了「宁可多分析,不可静默丢源码」的设计原则。
匹配语义
由于底层是 ignore 包,.understandignore 支持完整的 .gitignore 语义:
#注释行与空行被忽略;- 末尾
/表示仅匹配目录(如docs/); **/递归匹配(如**/snapshots/命中任意深度的 snapshots 目录);!取反可覆盖更早加载的默认规则,例如写!dist/可以把dist/下的文件强制纳入分析。
ignore-filter.test.ts 对以上语义都有对应断言,例如 !dist/ 用例验证 filter.isIgnored("dist/index.js") 返回 false,**/snapshots/ 用例验证 src/components/snapshots/Button.snap 被命中;CLI 模式的高优先级也有专门用例:当 .understandignore 写了 !docs/ 而 CLI 又传 --exclude "docs/" 时,CLI 模式最后加载,docs/README.md 仍会被排除。
IgnoreGenerator:starter 文件的自动生成
generateStarterIgnoreFile(projectRoot) 是首次运行时的「起点生成器」,它的行为约束是:
- 确定性代码:只扫描项目根目录,不依赖 LLM;
- 返回字符串:只负责生成内容,写盘由调用方完成;
- 所有建议均为注释行(
#前缀),用户必须手动去掉注释才会生效——这保证了生成器永远不会悄悄改变用户仓库的分析行为。
ignore-generator.ts 的当前实现比计划中的初版丰富得多,生成内容分为三个板块:
板块一:来自 .gitignore 的模式(去重后)
生成器会解析项目根目录的 .gitignore,提取其中的有效模式(跳过注释与空行),并用 isCoveredByDefaults() 与硬编码默认值去重(比较前会归一化尾部斜杠,所以 .gitignore 里写 dist 与默认的 dist/ 会被判定为重复)。只有未被默认值覆盖的模式才会以「# --- From .gitignore (uncomment to exclude) ---」板块输出。如果 .gitignore 不存在、或所有模式都已被默认覆盖,整个板块省略。
板块二:检测到的目录
detectDirectories() 对 projectRoot 的直接子目录做两种匹配(均为大小写不敏感,且输出保留磁盘上的真实大小写):
- 精确名列表
EXACT_DIR_NAMES:__tests__、test、tests、fixtures、testdata、docs、examples、scripts、migrations、.storybook、unittests、unittest、integrationtests、bench、benchmark、benchmarks、benches、spec——覆盖了 JS、Go(testdata)、Cargo(benches)、RSpec(spec)以及 .NET(PascalCase 的Tests/UnitTests)等生态惯例; - 后缀列表
SUFFIX_DIR_GLOBS:tests、specs两个后缀,用来一次覆盖 C# 的Foo.Tests/Foo.UnitTests项目后缀与 Xcode 的MyAppTests/MyAppUITests目标目录约定。
后缀匹配是刻意「不锚定」的,也就是说生产目录 Contests/ 也会命中并出现在建议中。源码注释解释了为什么这是安全的:这个列表只针对磁盘上真实存在的目录运行,且结果以目录真实名称输出(# Contests/),用户可以看见并选择不取消注释;而一条推测性的 **/*Tests/** 通配则完全没有这种「可见、可拒绝」的信号。ignore-generator.test.ts 中「surfaces a production *tests dir under its real name」用例专门验证了这一点。
板块三:按语言分组的测试文件模式
TEST_PATTERN_GROUPS 按 JS/TS、C#/.NET、Java/Kotlin、Go、C++、Python、Rust、Ruby、Swift 九个组输出注释建议,每组针对该生态的测试组织习惯:
- JS/TS:
*.test.*、*.spec.*、*.snap; - Python:
**/test_*.py、**/*_test.py(tensorflow 风格)、**/tests.py(Django 单文件约定)、**/conftest.py; - Rust:
**/tests.rs、**/test_*.rs、**/*_test.rs、**/bench_*.rs、**/*_bench.rs——针对 polkadot-sdk 这类把foo_test.rs与foo.rs并排放置的大 workspace(源码注释中记录了_test.rs在分析预算中占绝大多命中); - Swift:只使用精确目录名的 glob——
**/Tests/**/*.swift与**/Specs/**/*.swift。这是一个由底层匹配器行为反推出来的设计:ignore包默认ignorecase: true,裸的**/*Test.swift会连Contest.swift、Latest.swift、Backtest.swift、Protest.swift等生产文件一起吞掉,且无法靠[Tt]字符类挽救(模式会被编译成带i标志的 RegExp)。因此文件后缀式规则被整体放弃,改用零误报的目录精确名。ignore-generator.test.ts 有一个非常严格的端到端用例:把生成的 Swift 模式真正喂给ignore匹配器,断言Sources/App/Contest.swift、Sources/Interests/InterestPicker.swift等生产文件不被忽略,而Tests/AppTests/AppTests.swift、Modules/Feature/Tests/A.swift等测试路径必须被忽略。
文件头部的 HEADER 注释则向用户解释文件用途、语法、以及内建默认值:
# .understandignore — patterns for files/dirs to exclude from analysis
# Syntax: same as .gitignore (globs, # comments, ! negation, trailing / for dirs)
# Lines below are suggestions — uncomment to activate.
# Use ! prefix to force-include something excluded by defaults.
与 project-scanner 的集成:filteredByIgnore 的精确统计
过滤真正生效的位置是 /understand 技能的扫描阶段。project-scanner.md 定义了 Phase 1 的三步编排,其中文件枚举、.understandignore 过滤、语言检测、行计数全部由捆绑脚本 scan-project.mjs 确定性完成,LLM 只负责读取 README 与清单文件来合成 name/description/frameworks 等叙述字段。调用方式:
node $PLUGIN_ROOT/skills/understand/scan-project.mjs \
"$PROJECT_ROOT" \
"$UA_DIR/tmp/ua-scan-files.json" \
--exclude "tests/*,docs/*" # 可选:CLI 追加排除
scan-project.mjs 内部的文件发现优先使用 git ls-files -z -co --exclude-standard(尊重仓库的 .gitignore,且 -z 保证非 ASCII 路径不丢失),非 git 目录则退化为递归遍历。过滤阶段的关键设计是双过滤器对账(见 scan-project.mjs 的注释):
combined:createIgnoreFilter(projectRoot, excludePatterns),即默认 + 用户模式 + CLI 模式;defaultOnly:用一个确定不存在.understandignore的临时目录构建的纯默认过滤器;- 某文件被
combined排除、但defaultOnly会保留它,才计入filteredByIgnore。
这样 filteredByIgnore 精确地度量「用户驱动的排除增量」,不把基线默认排除算进去;而 ! 取反重新纳入的文件因为不会出现在 combined 的排除集合中,自然也不会被计入。若项目根本没有任何用户模式文件,则跳过双过滤器,直接复用 combined,避免额外开销。扫描输出的 JSON 中会带回 filteredByIgnore 与 totalFiles 字段,project-scanner 原样透传到最终的 scan-result.json。
Phase 0.5:生成、审核与暂停
用户侧的体验由 SKILL.md 中的 Phase 0.5 — Ignore Configuration 提供,它插在 Pre-flight(Phase 0)与 SCAN(Phase 1)之间:
-
检查
$UA_DIR/.understandignore是否存在($UA_DIR即.ua/,旧项目为.understand-anything/); -
若不存在,调用捆绑脚本生成 starter 文件:
PLUGIN_ROOT="$PLUGIN_ROOT" node "<SKILL_DIR>/generate-ignore.mjs" "$PROJECT_ROOT"generate-ignore.mjs 本身很薄:委托 core 的
generateStarterIgnoreFile生成内容,写入resolveUaDir(projectRoot)解析出的数据目录;若目标文件已存在则打印跳过提示并以 0 退出,保证「不覆盖已有文件」的契约。随后技能会提示用户:Generated
$UA_DIR/.understandignorewith suggested exclusions based on your project structure. Please review it and uncomment any patterns you'd like to exclude from analysis. When ready, confirm to continue.并等待用户确认后才继续;
-
若已存在,只报告「Found ... Review it if needed, then confirm to continue.」,同样等待确认;
-
确认后进入 Phase 1 扫描。
扫描完成后,如果 filteredByIgnore > 0,技能会向用户报告:
Excluded {filteredByIgnore} files via
.understandignoreand/or--excluderules.
这条「生成 → 暂停审核 → 确认 → 扫描 → 报告排除数」的闭环,就是计划文档中「pre-analysis review pause」的完整落地。
测试验证与运行方式
该机制的可验证性由三层保障:
- core 单测:
pnpm --filter @understand-anything/core test -- --run运行 ignore-filter.test.ts 与 ignore-generator.test.ts。测试在os.tmpdir()下创建临时目录模拟不同项目结构,覆盖默认模式内容、用户文件读取、注释/空行处理、!取反、**递归、双文件合并、CLI 优先级、多语言目录检测、.gitignore去重等场景; - 扫描脚本测试:test_scan_project.test.mjs 验证
scan-project.mjs的枚举与过滤行为; - 端到端导出处:
createIgnoreFilter、DEFAULT_IGNORE_PATTERNS、generateStarterIgnoreFile均通过 core/src/index.ts 从@understand-anything/core导出,供scan-project.mjs与generate-ignore.mjs以「workspace 包优先、插件缓存 dist 兜底」的方式加载。
实战速查:编写你的 .understandignore
结合上述实现,一份典型的 .understandignore(放在项目根目录或 .ua/ 下)可以这样写:
# 排除测试与文档
__tests__/
fixtures/
docs/
# 排除 Go 测试文件
**/*_test.go
# 把默认排除的 dist/ 强制纳入分析
!dist/
使用要点:
- 语法与
.gitignore完全一致:glob、#注释、!取反、目录的尾部/、**/递归; - 建议行生成时是注释状态,取消注释才生效;
!dist/这类取反只在用户文件/CLI 层生效,且受「后加载覆盖先加载」的规则约束(CLI--exclude优先级最高);- 新增的
--exclude模式需要--full重新扫描才会体现(见 SKILL.md 中的说明); - 匹配默认大小写不敏感,编写 Swift/Java 等后缀相关模式时优先采用「精确目录名」形式以避免误伤生产文件。
小结
.understandignore 机制在 Understand-Anything 中构建了一条清晰的分层链路:IgnoreFilter(ignore-filter.ts)以「默认 → 数据目录 → 根目录 → CLI」四层合并 gitignore 模式并提供 ! 取反;IgnoreGenerator(ignore-generator.ts)以确定性代码从 .gitignore、目录结构与语言惯例出发生成全部注释化的建议;scan-project.mjs 用双过滤器对账精确统计 filteredByIgnore;Phase 0.5 则把「审核」交还给用户。从计划文档到当前实现,默认列表调整(移除 bin/)、数据目录从 .understand-anything/ 演进到 .ua/、以及按语言分组的测试模式库,都印证了同一原则:排除规则宁可少排,也绝不静默丢掉真实源码。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00