Understand-Anything 大型仓库基准测试的“不支持结果”设计:区分不支持的分析能力与真实执行失败
本文围绕 Understand-Anything(下称 UA)仓库中一份基准测试改进实施计划展开:让大仓库基准测试(large-repo benchmark)能够把“解析器不支持该文件”“调用图能力缺失”这类能力性不支持,与“已选定能力后真正执行失败”区分开。读完本文,你将理解该方案如何通过复用 filesSkipped 与 call-graph skipped 计数、在不改动报告 Schema 1.0.0 的前提下实现“降级但不失败”的语义,并能结合源码定位到能力检测、结果映射、批次汇总与状态判定的完整调用链。
一、背景与目标:基准测试的“假失败”问题
UA 的 /understand 流程依赖一套确定性的辅助脚本(扫描、导入图、批次规划、Tree-sitter 结构提取)。当基准测试在超大、多语言 monorepo 上运行时,难免遇到注册表中没有对应解析器的文件(例如某些私有格式或脚本变体),或某个解析器只支持结构提取、不支持调用图提取。
问题在于:如果把“没有插件”“没有该能力”一律按失败处理,基准报告的状态就会被错误地拉低为 failed,使规模证据失去可比性;反之,如果把“解析器已声明能力却抛异常/返回非法值”也吞掉,则会掩盖真实的一致性(integrity)问题。
实施计划文档给出了明确的目标与架构决策:
Goal: Make the large-repository benchmark distinguish unsupported or unavailable analysis capabilities from real execution failures.
Architecture: Reuse the existing
filesSkippedand call-graphskippedpaths rather than changing report schema 1.0.0. Capability detection happens before interpreting parser results; only a selected, advertised capability that fails remains fatal.
翻译成设计原则就是:
- 能力检测先于结果解释——先看“这个文件有没有插件、插件声明了什么能力”,再决定如何解释后续结果;
- 不升级报告 Schema——复用现有
filesSkipped(结构侧)和 call-graphskipped计数,保持 JSON/Markdown 双报告与large-repo-report-1.0.0.schema.json的兼容性; - 只有一种情况保持致命——能力被选中并声明(advertised)之后,再抛异常或返回非法值,仍是
failed。
技术栈为:Node.js ESM、PluginRegistry、Vitest、JSON Schema 2020-12、pnpm。
二、全局约束(Global Constraints)
计划文档列出了六条硬性约束,它们是后续所有实现步骤的验收边界:
| 约束 | 说明 |
|---|---|
| 保持 Schema 不变 | large-repo-report-1.0.0.schema.json 与 JSON/Markdown 成对报告兼容性不被破坏 |
| 不支持文件可入账 | 结构分析不支持的文件必须被计入,产生 degraded 而非 failed,且退出码为 0 |
| 调用图能力缺失是跳过 | 可选的 call-graph 能力缺失时按 skipped 处理,不算失败 |
| 选中后的异常仍失败 | 能力被选中后的异常或非法值必须保持 failed |
| 不削弱完整性检查 | 路径、digest、隐私(脱敏)、清理、非法输出检查均不得放松 |
| 输出不落盘于被测仓库 | 基准产物不得写入被测仓库内部 |
这些约束在 docs/benchmarks/large-monorepo.md 中已有对应的公开语义描述(后文第四节会给出原文对照)。
三、源码级实现:能力感知的结果映射
3.1 核心入口 analyzeFileWithOutcomes()
结果映射逻辑被抽离为纯函数模块 extract-structure-result.mjs,与带 shebang 的 CLI 入口分离,保证单元测试不必导入可执行脚本。核心函数签名:
export function analyzeFileWithOutcomes(registry, file, content)
其返回值同时携带“数据”与“结果分类”(outcome):
{ analysis, callGraph, structureOutcome, callGraphOutcome }
// structureOutcome / callGraphOutcome ∈ 'succeeded' | 'failed' | 'skipped'
第一步:按扩展名/路径选择插件,插件不存在即双跳过。 见 extract-structure-result.mjs#L132-L146:
const wantsCallGraph =
file.fileCategory === 'code' || file.fileCategory === 'script';
const selectedPlugin = typeof registry.getPluginForFile === 'function'
? registry.getPluginForFile(file.path)
: registry;
if (selectedPlugin === null) {
return {
analysis: null,
callGraph: null,
structureOutcome: 'skipped',
callGraphOutcome: 'skipped',
};
}
这里的能力探测依赖 PluginRegistry.getPluginForFile(),它在注册表中按文件路径查找解析器,找不到时返回 null——这正是“已知不支持”的信号,与“找到了插件但调用炸了”严格区分开。
第二步:读取插件声明的能力。 见 extract-structure-result.mjs#L148-L151:
const supportsFullAnalysis =
typeof selectedPlugin?.analyzeFileFull === 'function';
const supportsSeparateCallGraph =
typeof selectedPlugin?.extractCallGraph === 'function';
解析器有两种形态:
- 组合式:提供
analyzeFileFull,一次返回{ structure, callGraph }; - 分离式:只提供
analyzeFile(结构)和/或extractCallGraph(调用图)。
3.2 三条分析路径与各自的失败边界
路径 A:组合式全量分析(插件声明了 analyzeFileFull 且文件需要调用图)。 见 extract-structure-result.mjs#L153-L180。此路径下 analyzeFileFull 抛异常直接记为双 failed(因为能力已声明,异常就是真失败);而结构有效性与调用图有效性独立校验——full.structure 非法只会让 structureOutcome 为 failed,不影响 callGraphOutcome,反之亦然。这保证了一个坏字段不会污染另一个维度的统计。
路径 B:分离式结构分析。 见 extract-structure-result.mjs#L182-L198:
let callGraphOutcome = wantsCallGraph && supportsSeparateCallGraph
? 'failed'
: 'skipped';
注意这个初始值的设计:只有当文件确实需要调用图(code/script 类目)且插件声明了 extractCallGraph 时,初始预期才是 failed;否则直接置为 skipped。也就是说,“插件没有这个能力”永远不会变成失败——这正是计划中“缺失可选调用图能力必须跳过而非失败”的落地。
路径 C:文件不需要调用图(非 code/script 类目)。 此时 wantsCallGraph 为 false,callGraphOutcome 恒为 skipped,即使插件声明了 analyzeFileFull 也不会去调用它(测试用例 uses separate structure analysis for non-code files even when full analysis is advertised 显式验证了 fullCalls === 0)。
3.3 CLI 侧汇总:filesSkipped 与 analysisOutcomes
extract-structure.mjs 的批处理主循环把映射结果汇入输出 JSON,见 extract-structure.mjs#L110-L124:
const { analysis, callGraph, structureOutcome, callGraphOutcome } =
analyzeFileWithOutcomes(registry, file, content);
if (structureOutcome === 'skipped') {
filesSkipped.push(file.path);
continue; // 不追加 result、不累加 outcome 计数
}
analysisOutcomes.structure[structureOutcome] += 1;
analysisOutcomes.callGraph[callGraphOutcome] += 1;
对应输出契约:
{
scriptCompleted: true,
filesAnalyzed: results.length,
filesSkipped, // 含读取失败与“无插件支持”两类
analysisOutcomes: {
structure: { succeeded: 0, failed: 0 },
callGraph: { succeeded: 0, failed: 0, skipped: 0 },
},
results: [...],
}
关键点:structureOutcome === 'skipped' 的文件只进 filesSkipped,不进 results、也不参与 outcome 计数。这样基准侧的“账目平衡”校验(后文说明)依然成立,而 filesSkipped 正是 Schema 中 degraded 状态的合法触发条件。
3.4 测试用例印证语义边界
test_extract_structure_outcomes.test.mjs 覆盖了计划要求的全部边界:
| 测试场景 | 期望结果 |
|---|---|
注册表 getPluginForFile() 返回 null |
结构、调用图均为 skipped(L333-L352) |
选中的解析器只有 analyzeFile(无 analyzeFileFull 也无 extractCallGraph) |
structureOutcome: 'succeeded'、callGraphOutcome: 'skipped'(L354-L382) |
已声明的 analyzeFileFull 抛异常/返回 null/undefined,且分离式调用成功 |
仍记 failed,不得被分离式成功“掩盖”(L428-L479) |
| 结构非法但调用图合法(及反向) | 两个维度独立计分(L481-L542) |
| 37 种畸形结构条目 + 非法调用图条目 | 分别判 failed,且不串扰另一维度(L585-L678) |
四、基准测试侧:从批次汇总到报告状态
4.1 完整性校验:账目必须平衡
基准运行器 scripts/lib/large-repo-benchmark.mjs 中的 summarizeStructureOutput()(L1257-L1358)对每批结构提取输出做严格核对。核心的平衡等式是:
structureSucceeded + structureFailed !== results.length ||
callGraphSucceeded + callGraphFailed + callGraphSkipped !== results.length
// → malformed = true
由于“skipped”的文件根本不进入 results(3.3 节),这两个等式恰好把 skipped 排除在“已分析”账目之外,而 skipped 计数通过 filesSkipped 单独入账。此外还核对:批次期望路径无缺失、无重复、无意外路径(missingStructurePaths / duplicateStructurePaths / unexpectedStructurePaths)。任何一条不成立都会把该批 complete 置为 false,最终升级为完整性失败——这满足“不削弱完整性检查”的约束。
4.2 状态判定:degraded、failed 与退出码
报告状态判定见 large-repo-benchmark.mjs#L1991-L2004:
if (hasFailedIntegrity(report.integrity)) {
report.status = 'failed';
exitCode = 1;
} else if (
report.warnings.length > 0 ||
report.integrity.filesSkipped > 0
) {
report.status = 'degraded';
exitCode = 0;
} else {
report.status = 'ok';
exitCode = 0;
}
即:只要完整性检查通过,filesSkipped > 0(不支持文件被正确入账)只会得到 degraded + 退出码 0;只有完整性失败(含“已声明能力却失败”导致的 structureFailed/callGraphFailed 批次不完整)才落入 failed + 退出码 1。退出码语义在 docs/benchmarks/large-monorepo.md 的状态表中公开:
| Exit code | Meaning | Report behavior |
|---|---|---|
0 |
完成,状态 ok 或 degraded |
JSON 与 Markdown 均写出 |
1 |
确定性阶段、完整性检查或临时产物清理失败 | 输出位置可写时写出部分报告 |
2 |
CLI 用法非法 | 不写报告 |
4.3 Schema 侧的配套约束
large-repo-report-1.0.0.schema.json 无需新增字段,仅靠既有分支即可表达新语义。其 degraded 分支(L144-L171)要求:报告必须至少有 1 条 warning,或 integrity.filesSkipped >= 1;ok 分支则要求 warnings 为空且 filesSkipped 恒为 0(L119-L135)。这与 3.3/4.2 节的实现严格互锁:不支持文件被计为 skipped → filesSkipped > 0 → degraded 合法、ok 非法。测试 test_large_repo_report_schema.test.mjs 会对 ok / 空 / degraded / 部分 failed 四类报告逐一编译并校验该 Schema,而 test_large_repo_benchmark.test.mjs 覆盖运行器行为(注意:运行器本身不做运行时 Schema 校验,Schema 验证发生在基准测试中,这是 large-monorepo.md 明确说明的)。
4.4 文档语义对照
实施计划 Task 1 的 Step 5 要求在基准文档中写明新语义,对应 large-monorepo.md 中已有段落(“Structure diagnostics” 一节):
Structural coverage counts successful structure analyses; files without a registered structural parser are accounted in
filesSkippedand produce a degraded report. Missing optional call-graph capability is also skipped. After a parser advertises a capability, an exception or invalid result remains an integrity failure instead of being reported as skipped work.
这句话与本节源码分析完全对应:无解析器 → filesSkipped + degraded;缺调用图能力 → skipped;声明后失败 → integrity failure。
五、计划文档中的实施步骤(可直接执行)
计划采用 TDD 流程(RED → GREEN),全部命令以 PowerShell 形式给出(计划面向 Windows 检出环境;其他平台等价替换为 npx vitest run ... 即可)。
Step 1:先加失败测试。 在 tests/skill/understand/test_extract_structure_outcomes.test.mjs 中新增两条:一条让注册表 getPluginForFile() 返回 null,断言双 skipped;一条让选中的解析器支持结构但没有 analyzeFileFull/extractCallGraph,断言 succeeded + skipped。
Step 2:验证 RED。 运行:
& .\node_modules\.bin\vitest.CMD run tests\skill\understand\test_extract_structure_outcomes.test.mjs
此时新测试应失败,因为旧版映射器会把无插件文件记为 failed。
Step 3:最小实现。 按第 3.1/3.3 节所示修改 analyzeFileWithOutcomes() 与 extract-structure.mjs 的汇总逻辑:已知不支持 → skipped 且计入 filesSkipped;需要调用图但插件未声明能力 → 调用图 skipped;保持既有计数使基准的形态校验(4.1 节平衡等式)继续成立。
Step 4:验证 GREEN。 同时跑三个测试文件:
& .\node_modules\.bin\vitest.CMD run tests\skill\understand\test_extract_structure_outcomes.test.mjs tests\benchmark\test_large_repo_report_schema.test.mjs tests\benchmark\test_large_repo_benchmark.test.mjs
期望:除已有文档化跳过外全部通过。
Step 5:文档化语义。 更新 docs/benchmarks/large-monorepo.md,写明“不支持的结构文件按 skipped 入账并产生 degraded;能力选中后的解析器失败仍是致命的”。
Step 6:全量验证。 串行跑根测试套件、构建 @understand-anything/core,然后对干净仓库运行基准(输出必须在被测工作树之外),用 Schema 1.0.0 校验 JSON,并确认 JSON/Markdown 成对报告的 pairId 一致。运行方式参考 large-monorepo.md 的官方命令:
corepack pnpm install --frozen-lockfile
corepack pnpm --filter @understand-anything/core build
corepack pnpm benchmark:large-repo /absolute/path/to/repository \
--label public-repository-name \
--output /absolute/path/to/benchmark-results/public-repository-name.json
Step 7:提交。 提交信息为:
fix(bench): distinguish unsupported analysis from failures
六、设计要点回顾
- 能力探测先行:
getPluginForFile()返回null或插件未声明对应方法,是唯一的“跳过”入口;此后的一切异常都按失败处理,避免了“用跳过掩盖真实故障”的滑坡。 - 零 Schema 升级:
skipped语义被拆到两个既有维度——结构侧进filesSkipped,调用图侧进callGraph.skipped计数——既有 1.0.0 Schema 的degraded分支天然覆盖,JSON/Markdown 成对报告与pairId机制不受影响。 - 账目恒等式是安全网:
succeeded + failed === results.length(结构)、succeeded + failed + skipped === results.length(调用图)使“被悄悄丢掉的文件”在批次校验阶段即暴露。 - 降级但不静默:
degraded+ 退出码 0 让 CI 可继续消费报告,同时warnings与filesSkipped字段保证读者能在比较性能前先检查 large-monorepo.md 所要求的warnings、integrity与 Markdown 摘要。
对维护者而言,这套语义的验证入口集中在三个测试文件:test_extract_structure_outcomes.test.mjs(映射层)、test_large_repo_report_schema.test.mjs(契约层)、test_large_repo_benchmark.test.mjs(运行层);如需深入解析器注册机制,可继续阅读 registry.ts 与 plugins/discovery.ts。
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 StartedRust0622
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