首页
/ Understand-Anything 的 .understandignore:让用户精确控制代码分析边界的可配置排除机制

Understand-Anything 的 .understandignore:让用户精确控制代码分析边界的可配置排除机制

2026-09-06 11:53:33作者:曹令琨Iris

本文基于 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.tsignore-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 的当前实现中,模式按以下顺序增量合并加载——后加载的条目可以通过 ! 取反覆盖先前条目:

  1. 硬编码默认模式DEFAULT_IGNORE_PATTERNS)——与 project-scanner 代理的排除规则对齐;
  2. 数据目录下的 .understandignore——即 .ua/.understandignore,若项目此前已存在旧的 .understand-anything/ 目录则沿用该位置(由 resolveUaDir 解析,见 persistence/index.ts);
  3. 项目根目录的 .understandignore——为了可见性提供的备选位置;
  4. CLI --exclude 传入的模式extraPatterns 参数)——最高优先级,位于最后加载。

需要说明的是,计划文档中最初只定义了前三层;CLI --exclude 这一层是后续增强,createIgnoreFilter 的第二参数 extraPatternsscan-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/
锁文件 *.lockpackage-lock.jsonyarn.lockpnpm-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__testtestsfixturestestdatadocsexamplesscriptsmigrations.storybookunittestsunittestintegrationtestsbenchbenchmarkbenchmarksbenchesspec——覆盖了 JS、Go(testdata)、Cargo(benches)、RSpec(spec)以及 .NET(PascalCase 的 Tests/UnitTests)等生态惯例;
  • 后缀列表 SUFFIX_DIR_GLOBStestsspecs 两个后缀,用来一次覆盖 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.rsfoo.rs 并排放置的大 workspace(源码注释中记录了 _test.rs 在分析预算中占绝大多命中);
  • Swift:只使用精确目录名的 glob——**/Tests/**/*.swift**/Specs/**/*.swift。这是一个由底层匹配器行为反推出来的设计:ignore 包默认 ignorecase: true,裸的 **/*Test.swift 会连 Contest.swiftLatest.swiftBacktest.swiftProtest.swift 等生产文件一起吞掉,且无法靠 [Tt] 字符类挽救(模式会被编译成带 i 标志的 RegExp)。因此文件后缀式规则被整体放弃,改用零误报的目录精确名。ignore-generator.test.ts 有一个非常严格的端到端用例:把生成的 Swift 模式真正喂给 ignore 匹配器,断言 Sources/App/Contest.swiftSources/Interests/InterestPicker.swift 等生产文件不被忽略,而 Tests/AppTests/AppTests.swiftModules/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 的注释):

  • combinedcreateIgnoreFilter(projectRoot, excludePatterns),即默认 + 用户模式 + CLI 模式;
  • defaultOnly:用一个确定不存在 .understandignore 的临时目录构建的纯默认过滤器;
  • 某文件被 combined 排除、但 defaultOnly 会保留它,才计入 filteredByIgnore

这样 filteredByIgnore 精确地度量「用户驱动的排除增量」,不把基线默认排除算进去;而 ! 取反重新纳入的文件因为不会出现在 combined 的排除集合中,自然也不会被计入。若项目根本没有任何用户模式文件,则跳过双过滤器,直接复用 combined,避免额外开销。扫描输出的 JSON 中会带回 filteredByIgnoretotalFiles 字段,project-scanner 原样透传到最终的 scan-result.json

Phase 0.5:生成、审核与暂停

用户侧的体验由 SKILL.md 中的 Phase 0.5 — Ignore Configuration 提供,它插在 Pre-flight(Phase 0)与 SCAN(Phase 1)之间:

  1. 检查 $UA_DIR/.understandignore 是否存在($UA_DIR.ua/,旧项目为 .understand-anything/);

  2. 若不存在,调用捆绑脚本生成 starter 文件:

    PLUGIN_ROOT="$PLUGIN_ROOT" node "<SKILL_DIR>/generate-ignore.mjs" "$PROJECT_ROOT"
    

    generate-ignore.mjs 本身很薄:委托 core 的 generateStarterIgnoreFile 生成内容,写入 resolveUaDir(projectRoot) 解析出的数据目录;若目标文件已存在则打印跳过提示并以 0 退出,保证「不覆盖已有文件」的契约。随后技能会提示用户:

    Generated $UA_DIR/.understandignore with 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.

    并等待用户确认后才继续

  3. 若已存在,只报告「Found ... Review it if needed, then confirm to continue.」,同样等待确认;

  4. 确认后进入 Phase 1 扫描。

扫描完成后,如果 filteredByIgnore > 0,技能会向用户报告:

Excluded {filteredByIgnore} files via .understandignore and/or --exclude rules.

这条「生成 → 暂停审核 → 确认 → 扫描 → 报告排除数」的闭环,就是计划文档中「pre-analysis review pause」的完整落地。

测试验证与运行方式

该机制的可验证性由三层保障:

  • core 单测pnpm --filter @understand-anything/core test -- --run 运行 ignore-filter.test.tsignore-generator.test.ts。测试在 os.tmpdir() 下创建临时目录模拟不同项目结构,覆盖默认模式内容、用户文件读取、注释/空行处理、! 取反、** 递归、双文件合并、CLI 优先级、多语言目录检测、.gitignore 去重等场景;
  • 扫描脚本测试test_scan_project.test.mjs 验证 scan-project.mjs 的枚举与过滤行为;
  • 端到端导出处createIgnoreFilterDEFAULT_IGNORE_PATTERNSgenerateStarterIgnoreFile 均通过 core/src/index.ts@understand-anything/core 导出,供 scan-project.mjsgenerate-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 中构建了一条清晰的分层链路:IgnoreFilterignore-filter.ts)以「默认 → 数据目录 → 根目录 → CLI」四层合并 gitignore 模式并提供 ! 取反;IgnoreGeneratorignore-generator.ts)以确定性代码从 .gitignore、目录结构与语言惯例出发生成全部注释化的建议;scan-project.mjs 用双过滤器对账精确统计 filteredByIgnore;Phase 0.5 则把「审核」交还给用户。从计划文档到当前实现,默认列表调整(移除 bin/)、数据目录从 .understand-anything/ 演进到 .ua/、以及按语言分组的测试模式库,都印证了同一原则:排除规则宁可少排,也绝不静默丢掉真实源码。

登录后查看全文
热门项目推荐
相关项目推荐