fix(eval): actually capture exploration steps, across all their path shapes - #424
Conversation
…shapes The localization metric added in #423 recorded zeroes: the capture block never made it into the file (an earlier patch asserted out and I fixed only the aggregation half), and the shape I had written for it was wrong anyway. Exploration steps carry their target under different keys depending on the action — measured, not assumed: `read_file` / `list_directory` use `path`, `search_code` uses `filePattern` for glob discovery or `directory`. Keying on `path` alone would have dropped every `search_code` step, which is the main way query tasks explore now that #419 lets them plan at all. Glob patterns are counted separately (`globExplored`) rather than as off-target. A pattern does not name a directory, so scoring `**/*Route*.tsx` as a miss would penalise the glob-then-narrow workflow the plan prompt explicitly teaches. Verified on `deep-query-route-source`: ``` explored: 2 [search_code "*route*.ts", read_file "src/app/routes.ts"] glob: 1 offTarget: 0 (the file read is under the declared target dir src/app) ``` Two steps, one to narrow and one to read, landing on target. Before #419 this task planned no exploration at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: fix(eval): actually capture exploration steps, across all their path shapes
风险等级: 高
处理建议: 请求修改
决策摘要: ** 修复方向正确且明显优于现状,但捕获条件仍会静默漏掉一整类 search_code 步骤(只带 query/pattern 无路径字段的内容搜索),而 harness 又没有对 step 事件做形状断言——这正是本 PR 要根治的「空结果被当成证据」;改动很小,建议补齐后合入。
级联分析
- 变更符号: 原始模型未提供结构化级联字段。
- 受影响流程: 原始模型未提供结构化级联字段。
- 变更集外调用方: unknown
- 置信度: degraded
问题发现
- [高] 只带
query/pattern的search_code步骤仍被整条丢弃,且无声- 证据:
search_code的 schema 只有query/pattern/filePattern/globOnly/maxResults/contextLines(packages/mcp-file/src/tools/search-code.ts:181-217),filePattern有默认值、可以不传。规则回退路径明确发出无路径参数的步骤:params: { query: task.description, maxResults: 20 }(packages/core/src/planner.ts:532,另见:568)。此时where为undefined,步骤既不进explored也不报错。 - 受影响调用方/流程:
exploredCount是report-filesense.mjs:169「平均探索步数/任务」的唯一来源。一个大量用内容搜索定位的任务会显示成「探索步数低、脱靶 0」,读起来像导航很准,实际是没采到——与 #423 的失败模式同类,只是换了个参数形状。 - 最小可行修复: 探索类动作无论是否带路径字段都入账,用
path: null表示「无路径归属」,并在offTargetExplored/globExplored的 filter 里显式排除null(两处都已按字符串取值,改动约 3 行)。
- 证据:
- [高] 新增的跨包字段读取没有接入
harnessFailures契约断言- 证据: 本文件对
filesense_navigated的每个字段都做typeof断言并记账中止本臂(run-eval.mjs:170-181),理由写在:160-169:「core 一旦改名,entries 会静默变成 0……宁可吵,不可静默」。新捕获块用event.step?.action与event.step?.params ?? {}(:217-218),step改名或params变形只会让explored恒为空数组,报告照常出「探索 0 步、脱靶 0」。 - 受影响调用方/流程: 同上,直接污染
report-filesense.mjs的主指标;report-filesense.mjs现有的旧版 harness 守卫(:43-50)只检查字段存在性,exploredCount: 0是合法值,拦不住。 - 最小可行修复: 在两类 step 事件上断言
typeof event.step?.action === 'string'与event.step?.params为对象,不符则harnessFailures.push(...),与 filesense 分支同构。
- 证据: 本文件对
- [中]
globExplored写进了 JSONL,但报告里没有任何位置呈现它- 证据:
report-filesense.mjs:167-171的定位精度表只有exploredCount与offTargetExplored;正文:162-165声称统计的是「探索类步骤落在 targetDirs 之外的次数」,未提 glob 排除。改动后exploredCount含 glob 步、offTargetExplored不含,两个数字的口径不再一致,读者无法从报告还原分母。 - 受影响调用方/流程:
benchmarks/results/下要作为证据提交的消融报告。 - 最小可行修复: 在同一张表加一行
globExplored,并在:162-165的说明里补一句「glob 模式不计入脱靶」。
- 证据:
- [中] 注释块插在 rollback 注释与其
if之间,注释与代码错位- 证据:
run-eval.mjs:206-209是 rollback 分支的说明(「三者一起记……」),其if在:230-234;新块:210-228插在两者中间,自上而下读时 rollback 的理由会被当成 step 捕获块的说明。同时新块结束后没有return;,与本文件其他分支「判定即 return」的分发风格不一致(:191、:204)。 - 受影响调用方/流程: 仅可读性,但本文件的注释就是它的规格说明。
- 最小可行修复: 把新块整体移到
:205之后、:206之前,或移到 rollback 分支之后;并补return;。
- 证据:
- [低]
touched采集超出本 PR 声明范围,且当前无任何消费者- 证据:
:224-226采集create_file/apply_patch,:251初始化;全仓benchmarks/**/*.mjs搜索touched只有这两处定义,无读取方。PR 描述与标题只讲 exploration 捕获,未提写入捕获。 - 受影响调用方/流程: 无行为影响,仅进
eventDetails原始载荷。 - 最小可行修复: 要么在描述里说明它是后续写入定位指标的原始数据,要么拆出本 PR。
- 证据:
行级发现
- [benchmarks/eval/run-eval.mjs:219] params.directory
在read_file/list_directory/search_code三个工具的 schema 里都不存在(search-code.ts:181-217、list-directory.ts:11-15),是恒不命中的分支;真正的缺口是无路径字段的search_code(planner.ts:532就发这种步骤)会让where为 undefined 而被整条丢弃。建议改为「探索类动作一律入账,path: null` 表示无路径归属」,并在下游 filter 里排除 null。 - [benchmarks/eval/run-eval.mjs:217] 这里跨包读
event.step.action/event.step.params但没有像filesense_navigated分支(:170-181)那样断言形状;core 改名后explored会静默恒空、报告照出「探索 0 步」。建议对action为 string、params为对象各加一条harnessFailures.push。 - [benchmarks/eval/run-eval.mjs:214] 注释称「search_code 用
filePattern(glob 发现)或directory」并标注为实测,但search_code没有directory参数;请按实际 schema 修正这句,否则后来者会据此推断错误的事件形状。 - [benchmarks/eval/run-eval.mjs:210] 这段注释插在
:206-209的 rollback 说明与其:230的if之间,导致 rollback 的理由读起来像在解释本块;建议整块移到 rollback 分支之前或之后,并在块尾补return;以保持本文件「判定即 return」的分发风格。 - [benchmarks/eval/run-eval.mjs:225] touched` 只写不读:全仓无消费者,且写入捕获不在本 PR 声明范围内;请在描述中说明其用途,或拆出本 PR。
- [benchmarks/eval/run-eval.mjs:346] 新增的
globExplored没有进入report-filesense.mjs:167-171的定位精度表,而exploredCount(含 glob)与offTargetExplored(不含 glob)口径已不一致;建议同步加一行并在正文说明中补一句 glob 排除规则。 - [report-filesense.mjs:63] 64
的avg/sum把offTargetExplored: null(未声明targetDirs` 的任务)当作 0 参与平均,会稀释均值。属既有问题、非本 PR 引入,但本 PR 让该指标首次真正有值,值得一并确认口径。
Karpathy 评审
- 假设: 模型输出需要归一化为固定 Markdown 契约。
- 简洁性: 已提取 summary、finding、evidence 与 fix;原始 prose 不再附在评论中,避免占用下游解析与代理上下文。
- 变更范围: 原始模型未提供结构化范围字段。
- 验证: 需要查看 CI、测试或人工 CR 证据补强合并信心。
缺失覆盖
- 输出未命中 Repo Guard Markdown 契约;建议补充真实模型质量评估覆盖。
| if (event.type === 'step_completed' || event.type === 'step_failed') { | ||
| const action = event.step?.action; | ||
| const params = event.step?.params ?? {}; | ||
| const where = params.path ?? params.directory ?? params.filePattern; |
There was a problem hiding this comment.
params.directory在read_file/list_directory/search_code 三个工具的 schema 里都不存在(search-code.ts:181-217、list-directory.ts:11-15),是恒不命中的分支;真正的缺口是无路径字段的 search_code(planner.ts:532就发这种步骤)会让where 为 undefined 而被整条丢弃。建议改为「探索类动作一律入账,path: null` 表示无路径归属」,并在下游 filter 里排除 null。
| // search_code 用 `filePattern`(glob 发现)或 `directory`。只认 `path` | ||
| // 会把 search_code 全漏掉,而那正是 #419 修好后 query 任务的主要探索手段。 | ||
| if (event.type === 'step_completed' || event.type === 'step_failed') { | ||
| const action = event.step?.action; |
There was a problem hiding this comment.
这里跨包读 event.step.action / event.step.params 但没有像 filesense_navigated 分支(:170-181)那样断言形状;core 改名后 explored 会静默恒空、报告照出「探索 0 步」。建议对 action 为 string、params 为对象各加一条 harnessFailures.push。
| // search_code 慢慢摸出来,结果一样、代价不同,而代价才是导航能力的直接体现。 | ||
| // | ||
| // 路径字段因动作而异(实测):read_file / list_directory 用 `path`, | ||
| // search_code 用 `filePattern`(glob 发现)或 `directory`。只认 `path` |
There was a problem hiding this comment.
注释称「search_code 用 filePattern(glob 发现)或 directory」并标注为实测,但 search_code 没有 directory 参数;请按实际 schema 修正这句,否则后来者会据此推断错误的事件形状。
| // `rollback_failed` 随 #402 才进入 AgentEvent 联合类型,在它合入前不会出现。 | ||
| // 三者一起记,才能判定「撤销成功」与「拦到了但没撤销掉」——只看 started | ||
| // 分不出这两种,而后者意味着坏文件还在工作区里。 | ||
| // 定位精度:通过率对「找得准不准」不敏感——两臂都能靠 list_directory / |
There was a problem hiding this comment.
这段注释插在 :206-209 的 rollback 说明与其 :230 的 if 之间,导致 rollback 的理由读起来像在解释本块;建议整块移到 rollback 分支之前或之后,并在块尾补 return; 以保持本文件「判定即 return」的分发风格。
| details.explored.push({ action, path: String(where) }); | ||
| } | ||
| if (['create_file', 'apply_patch'].includes(action)) { | ||
| details.touched.push({ action, path: String(where) }); |
There was a problem hiding this comment.
touched` 只写不读:全仓无消费者,且写入捕获不在本 PR 声明范围内;请在描述中说明其用途,或拆出本 PR。
| (e) => !e.path.includes('*') && !task.targetDirs.some((d) => e.path.startsWith(d)), | ||
| ).length | ||
| : null, | ||
| globExplored: eventDetails.explored.filter((e) => e.path.includes('*')).length, |
There was a problem hiding this comment.
新增的 globExplored 没有进入 report-filesense.mjs:167-171 的定位精度表,而 exploredCount(含 glob)与 offTargetExplored(不含 glob)口径已不一致;建议同步加一行并在正文说明中补一句 glob 排除规则。
Follow-up to #423. The localization metric it added recorded zeroes — the capture block never landed in the file, and the event shape I wrote for it was wrong.
Measured, not assumed:
read_file/list_directorycarry the target underpath;search_codeusesfilePattern(glob discovery) ordirectory. Keying onpathalone dropped everysearch_codestep — the main way query tasks explore now that #419 lets them plan at all.Glob patterns count separately (
globExplored) instead of as off-target: a pattern names no directory, and scoring**/*Route*.tsxas a miss would penalise the glob-then-narrow workflow the plan prompt teaches.Verified on
deep-query-route-source:Two steps, one to narrow and one to read, landing on target. Before #419 this task planned no exploration at all.
Impact Scope
benchmarks/eval/run-eval.mjsonly. No production code.GitNexus Impact Summary
benchmarks/matches no critical contract rule.packages/**orapps/**change.pnpm quality:precommitexit 0, plus the measured run above.Verification
pnpm quality:precommit— exit 0.Checklist
pnpm quality:precommit, or explained why it could not run.pnpm quality:localfor critical skeleton changes, or explained why it could not run — there are none.🤖 Generated with Claude Code