feat(eval): filesense ablation arm, deep fixture, and event payloads (#411) - #412
Conversation
The 2026-07-12 ablation cannot say anything about filesense: its two arms (`full`/`ablation`) toggle SDD and the guard, and leave filesense enabled in both. Recomputing the committed JSONL confirms it — `filesense_navigated` fires on 11/27 tasks in *each* arm. This adds the three things needed to actually measure it. **A third arm.** `--arm no-filesense` differs from `full` in exactly one field. Wiring it surfaced a fourth instance of this repo's "declared but never wired" defect class (cf. #386/#400/#403): `filesense.enabled` is live config in core (`planner-skills.ts:134` reads it) but `RunFrontAgentTaskOptions` had no `filesense` field, so the harness's option was silently dropped — the arm would have degraded to "both arms on", exactly as #386 voided July's guard arm. `run.ts` now threads it at both assembly sites, with tests pinning both the passthrough and the omitted-case fallback. **Event payloads.** The harness stored only event counts, which is why the 2026-07-31 report could not classify its own `validation_failed` events. It now also stores the payloads of `filesense_navigated` (intent, paths, entries, truncated, candidates), `validation_failed` (which checks actually failed — the only way to separate real interceptions from tool errors) and `rollback_failed`, plus per-task `filesenseEntries` / `filesenseTruncated`. **A fixture where the budget gate actually closes.** The flat fixture is 14 source files deep 2; measured, filesense scans 17 of the repo's 18 entries and never truncates — one `ls -R` is the whole project, so navigation and a plain command are indistinguishable by construction, and any result is a fact about the fixture, not the capability. `fixture-deep` is feature-sliced: 236 source files / 188 directories / depth 5, with `format.ts`, `Button.tsx` and `useToggle.ts` each duplicated across 24 features so filenames alone cannot locate the target. Measured under the same budget: 180 entries then truncated, out of 424. Its 12 tasks deliberately omit the `files` pre-seed the flat tasks use — pre-seeding hands the agent the path and erases the thing under test. The 228 noise modules are generated by `fixture-deep/generate.mjs` (deterministic) and gitignored; only the 19 hand-written anchor files are committed. `report-filesense.mjs` reports the full-vs-off comparison, and separates the triggered subset — filesense fires on ~40% of tasks by design (`trigger-policy.ts` skips known single-file work), so a whole-set average dilutes the effect. It also states the read discipline: same-arm "used vs not used" comparisons are confounded by task type; only the cross-arm same-task comparison is a valid control. No pass-rate claim is made here — this commit is the instrument, not a result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 请求修改
决策摘要: run.ts 的接线修复本身正确、覆盖完整、对既有调用方零影响,可以合并;但本 PR 的交付物是「测量仪器」,而仪器里有三处会产出「看起来像证据的空结果」——validation_failed / rollback_failed 在本仓库根本没有 emit 点、report 把任务集硬编码成 tasks.json、JSONL 输出文件名不含 fixture 导致两套夹具会静默混进同一份报告。这正是本 PR 立论要消除的失效模式,修复成本都很低。
级联分析
- 变更符号:
RunFrontAgentTaskOptions.filesense(新增可选字段)、runFrontAgentTask/planFrontAgentTask的AgentConfig组装、run-eval.mjs的 arm/fixture 选择与事件采集。 - 受影响流程: 运行时 API →
AgentConfig.filesense→planner-skills.ts的phase.filesense-navigate注入;benchmarks 评测编排与报告生成。 - 变更集外调用方: 已用文本检索穷举
filesense: (resolved|options),全仓仅三处——run.ts:187、run.ts:458(本 PR 均已改),以及mcp-server.ts:552;后者是frontagent_status的诊断输出,不是AgentConfig组装点,不受影响。RuntimeConfigInput用的是扁平的filesenseEnabled等字段,与新字段无命名冲突。新字段可选且缺省回落resolved.filesense,现有调用方行为不变。 - 置信度: medium(text search + 直接读取源码,无代码图谱证据;已确认受影响组装点在本变更集中全部更新)
问题发现
-
[高]
validation_failed与rollback_failed在本仓库没有 emit 点,新增的载荷采集永远为空- 证据: 全仓检索
rollback_failed只命中benchmarks/eval/run-eval.mjs一处——它不在packages/core/src/types.ts:775-797的AgentEvent联合类型里,event.snapshotId/event.error的形状是凭空假定的。检索validation_failed只命中 5 个文件(types.ts 的类型定义、desktop reducer、benchmarks),没有任何emit站点;benchmarks/results/2026-07-12-sdd-ablation.md:39-40已把这一点记为缺陷 3,其后续项 3「在执行器校验路径上补validation_failed事件」至今未落地。 - 受影响调用方/流程: 每条 JSONL 记录里
eventDetails.validationFailed/rollbackFailed恒为[]。下一份报告读到空数组时无法区分「guard 一次都没拦」与「事件压根没接线」——与 PR 描述「now also stored: which checks failed,the only way to separate a real interception from a tool error」不符。PR 正文称 2026-07-31 遥测报告有两次validation_failed事件,但在本 checkout 上该事件不可能被runFrontAgentTask发出,两者需要对齐。 - 最小可行修复: 删除
rollback_failed分支(或改采集确实存在的rollback_started/rollback_completed);在validation_failed分支上写明「该事件当前无 emit 点,见 2026-07-12 §缺陷3 / 后续项3」,并在report-filesense.mjs里把「0 条」显式标注为「未接线」而非「零拦截」。
- 证据: 全仓检索
-
[高] 报告器把任务集硬编码成
tasks.json,对 deep 夹具的产物会自述错误的方法- 证据:
report-filesense.mjs:58输出冻结的 N 条任务(benchmarks/eval/tasks.json),而本 PR 的主用例是--fixture deep(走tasks-deep.json、fixture-deep)。报告器没有任何 fixture 概念,> 口径段还断言「任务集、夹具全部相同」,却无从校验。 - 受影响调用方/流程: 按 PR 推荐路径跑完 deep 两臂后生成的 Markdown 会声称用的是 flat 任务集——一份自述失真的报告,正是 #410 撤回结论的同一类问题。
- 最小可行修复: 在 JSONL 记录里加
fixture/taskSet字段(run-eval.mjs:190附近),报告器从记录读取并断言两臂一致,不一致时直接报错退出。
- 证据:
-
[中] 输出文件名只含 arm,不含 fixture,flat 与 deep 的结果会静默合并
- 证据:
run-eval.mjs:26的OUT = out/${ARM}.jsonl未随新增的--fixture(run-eval.mjs:39-42)变化;断点续跑的去重只按taskId(run-eval.mjs:84-89),而 deep 任务 id 带deep-前缀与 flat 不冲突,因此先跑 flat 再跑 deep 会追加进同一文件而不报错。report-filesense.mjs:24的commonIds会把两套夹具的任务并成一个「冻结任务集」。 - 受影响调用方/流程: 跨夹具的通过率被混合平均,
filesenseEntries/filesenseTruncated汇总同样被污染。 - 最小可行修复:
OUT改为${ARM}${FIXTURE_KIND === 'deep' ? '-deep' : ''}.jsonl,并在记录里补fixture: FIXTURE_KIND。
- 证据:
-
[中] deep 夹具的前置检查漏掉
node_modules,且没有锁文件- 证据:
run-eval.mjs:43-45只校验生成树是否存在;setupWorkspace(run-eval.mjs:71)无条件symlinkSync(join(FIXTURE, 'node_modules'), …)。flat 夹具提交了benchmarks/eval/fixture/pnpm-lock.yaml,fixture-deep/只有package.json,没有锁文件。 - 受影响调用方/流程: 未安装依赖时会建出悬空软链,
checks.mjs:24-32的typecheck与cmd全部返回非 0,整臂输出一串 FAIL 记录;而唯一的中止启发式是「零成功 LLM 调用」(run-eval.mjs:206),LLM 正常时不会触发——又一次「失败被记成数据」。 - 最小可行修复: 前置检查追加
existsSync(join(FIXTURE, 'node_modules')),并提交fixture-deep的锁文件以对齐 flat 夹具。
- 证据:
-
[中]
fixture-deep/sdd.yaml是 flat 夹具的逐字拷贝,约束指向不存在的目录- 证据: 深夹具是 feature-sliced(
src/features/src/entities/src/shared/src/app),但 sdd.yaml 的directory_structure仍是src/components/src/hooks/src/utils,module_boundaries仍是src/components/* → src/api/*,project.name仍是eval-fixture——与benchmarks/eval/fixture/sdd.yaml第 23-40 行完全一致。这些路径在深夹具中一个都不存在。 - 受影响调用方/流程: 两臂共用同一份 sdd.yaml,filesense 的对照仍然成立;但 deep 轮名义上「SDD 开」,SDD 却对真实目录树无任何约束,而
deep-create-shared-truncate/deep-create-checkout-hook/deep-create-entity-guard三条任务恰恰在考「遵循既有分层约定」——即directory_structure本该编码的东西。 - 最小可行修复: 按 feature-sliced 布局重写
directory_structure与module_boundaries,并更新project.name/description。
- 证据: 深夹具是 feature-sliced(
-
[中]
deep-query-format-convention的验收无法区分两臂- 证据:
tasks-deep.json:32唯一的 check 是result_contains: "checkout",而任务文本本身就是「checkout 这个 feature 自己的 format 函数……」。任何复述问题的回答都会命中。 - 受影响调用方/流程: 这条任务是专为「24 份同名
format.ts」设计的定位测试,却对两臂都白送一分,直接稀释本次消融要测的效应。 - 最小可行修复: 改为断言可区分的内容,如返回串形态
checkout:配合toFixed,或路径features/checkout/lib/format\.ts。
- 证据:
-
[中]
options.filesense是整体替换而非合并,会丢掉环境变量解析出的兄弟字段- 证据:
FilesenseConfig(packages/core/src/types.ts:165-178)全部字段可选;run.ts:187/run.ts:458用options.filesense ?? resolved.filesense。调用方传{ enabled: true }时,config.ts:341-361从FRONTAGENT_FILESENSE_MAX_ENTRIES等解析出的output/writeMode/maxEntries/maxBytes/timeoutMs会一并被丢弃。 - 受影响调用方/流程: 本 PR 自己的
no-filesense臂(run-eval.mjs:60)不受影响(enabled: false后其余字段无意义),但这与run.test.ts:423注释「省略时不得变成 undefined:那会连环境变量解析出来的配置一起丢掉」担心的是同一类丢失,只是发生在部分覆盖而非省略时。 - 最小可行修复: 改为
{ ...resolved.filesense, ...options.filesense },并补一条「部分覆盖保留其余已解析字段」的测试;若坚持整体替换语义,在 JSDoc 里写明这一点。
- 证据:
-
[低] README 未同步新增的臂与夹具,checklist 勾了「已更新文档」
- 证据:
README.md:1301-1312仍只写两臂 +report.mjs;scripts/tests/workflow-rules.test.mjs:560-562把这段 README 钉为评测入口的文档面(该测试只断言既有两行存在,因此不会失败)。新增的--arm no-filesense、--fixture deep、必须先跑generate.mjs这一前置步骤、report-filesense.mjs都无处可查。 - 最小可行修复: 在同一节补三条命令与 generate 前置步骤。
- 证据:
-
[低] 生成器导入了未使用的
rmSync,且没有清理步骤,"确定性" 只在首次生成成立- 证据:
generate.mjs:23导入rmSync但全文未用;生成前不清理旧产物。改动FEATURES/ENTITIES后重跑会留下孤儿文件,条目数与截断行为随之漂移,而这两个数正是本夹具存在的理由。biome.json:61把**/benchmarks排除在 lint 之外,所以pnpm quality:precommit不会报这个未使用导入。 - 最小可行修复: 删除未使用导入;或在写入前只清理生成子树,注意不要误删
features/{checkout,cart,shipping}/lib下已提交的锚点文件。
- 证据:
行级发现
- [packages/runtime-node/src/run.ts:187]
options.filesense ?? resolved.filesense是整体替换:调用方传部分对象时会丢掉 env 解析出的output/writeMode/maxEntries/maxBytes/timeoutMs;建议改为{ ...resolved.filesense, ...options.filesense }(458 行同)。 - [benchmarks/eval/run-eval.mjs:116]
validation_failed在全仓没有 emit 站点(2026-07-12 报告 §缺陷3 的后续项 3 未落地),该数组恒为空;请在此注明未接线,否则空数组会被读成「零拦截」。 - [benchmarks/eval/run-eval.mjs:126]
rollback_failed不在AgentEvent(types.ts:775-797)里,全仓也无发出方,snapshotId/error形状是假定的——删除此分支,或改采集确实存在的rollback_started/rollback_completed。 - [benchmarks/eval/run-eval.mjs:42] 记录里没有
fixture/taskSet字段,而输出文件名只含 arm,flat 与 deep 的结果会被追加进同一个${ARM}.jsonl;请把 fixture 写进文件名并落进记录。 - [benchmarks/eval/run-eval.mjs:43] 前置检查只验生成树;缺
node_modules时 71 行会建出悬空软链,整臂 check 全 FAIL 却不中止——请一并校验existsSync(join(FIXTURE, 'node_modules'))。 - [benchmarks/eval/report-filesense.mjs:58] 任务集硬编码为
benchmarks/eval/tasks.json,用于 deep 产物时报告会自述错误方法;改为从记录的fixture/taskSet字段读取,并在两臂不一致时报错退出。 - [benchmarks/eval/tasks-deep.json:32] 唯一 check
result_contains: "checkout"与任务文本重合,两臂必过、无法区分同名format.ts的定位能力;改断言checkout:返回串形态或features/checkout/lib/format\.ts路径。 - [benchmarks/eval/fixture-deep/sdd.yaml:24]
src/components/src/hooks/src/utils在 feature-sliced 深夹具里都不存在,SDD 对真实目录树不构成任何约束;请按src/features|entities|shared|app重写并更新project.name。 - [benchmarks/eval/fixture-deep/generate.mjs:23]
rmSync导入后未使用且无清理步骤,改动 FEATURES/ENTITIES 后重跑会留孤儿文件、条目数漂移;删除导入或只清理生成子树(勿删已提交锚点)。
Karpathy 评审
- 假设: 三处隐藏假设未被验证——
validation_failed/rollback_failed会被发出(实际没有发出方)、评测输出目录里只会有一套夹具的数据(文件名不区分)、深夹具的依赖已安装(无锁文件、无检查)。run.ts侧的假设是干净的:新字段可选、缺省回落,run.test.ts:400-425两个方向都钉住了。 - 简洁性:
run.ts改动是本类缺陷(#386/#400/#403)已有的最小形状,没有引入新抽象;--fixture flat|deep用两个三元表达式选路径,没有过度配置。report-filesense.mjs与report.mjs有约 40 行加载/统计 helper 重复,但两者叙事确实不同,现阶段不值得抽公共模块——不作为阻塞项。 - 结构质量: 没有发现文件膨胀、错误分层、薄 wrapper 或共享路径特例。
collectEventDetail是一个内聚的纯函数,位置合适;问题在于它有两个分支对应的事件不存在,属正确性而非结构问题。 - 变更范围: 与既定目标一致,无无关重构与格式噪音。生产侧只有
run.ts的 +14/-2,其余全在benchmarks/。 - 验证:
run.ts的验证强度与其风险匹配(双向测试 + 两次真实跑)。但评测工具本身几乎没有验证:夹具前置条件、事件接线、报告自述口径都无自检,而这些恰恰是本 PR 要交付的东西。
缺失覆盖
options.filesense部分覆盖(如只传{ enabled: true })时其余已解析字段应保留的测试——当前两条测试只覆盖「全量覆盖」与「完全省略」。report-filesense.mjs对两臂fixture/taskSet不一致时应拒绝出报告的断言。- 深夹具的可再生性检查:重跑
generate.mjs后条目数/截断结果应与 PR 表格中的 180 / truncated=true 一致(当前无清理步骤,无法保证)。 --fixture deep路径的一次冒烟(--tasks smoke,共 4 条 smoke 任务)作为 PR 内可复现证据;PR 声明「no run is reported here」是合理的克制,但仪器自检与结果宣称是两回事。
| rag: resolved.rag, | ||
| filesense: resolved.filesense, | ||
| // 调用方显式给了就用调用方的(消融臂靠这条),否则沿用解析出来的默认配置 | ||
| filesense: options.filesense ?? resolved.filesense, |
There was a problem hiding this comment.
options.filesense ?? resolved.filesense 是整体替换:调用方传部分对象时会丢掉 env 解析出的 output/writeMode/maxEntries/maxBytes/timeoutMs;建议改为 { ...resolved.filesense, ...options.filesense }(458 行同)。
| }); | ||
| return; | ||
| } | ||
| if (event.type === 'validation_failed') { |
There was a problem hiding this comment.
validation_failed 在全仓没有 emit 站点(2026-07-12 报告 §缺陷3 的后续项 3 未落地),该数组恒为空;请在此注明未接线,否则空数组会被读成「零拦截」。
| }); | ||
| return; | ||
| } | ||
| if (event.type === 'rollback_failed') { |
There was a problem hiding this comment.
rollback_failed 不在 AgentEvent(types.ts:775-797)里,全仓也无发出方,snapshotId/error 形状是假定的——删除此分支,或改采集确实存在的 rollback_started/rollback_completed。
| const FIXTURE_KIND = args.fixture ?? 'flat'; | ||
| if (!['flat', 'deep'].includes(FIXTURE_KIND)) throw new Error('--fixture flat|deep'); | ||
| const FIXTURE = join(HERE, FIXTURE_KIND === 'deep' ? 'fixture-deep' : 'fixture'); | ||
| const TASKS_FILE = join(HERE, FIXTURE_KIND === 'deep' ? 'tasks-deep.json' : 'tasks.json'); |
There was a problem hiding this comment.
记录里没有 fixture/taskSet 字段,而输出文件名只含 arm,flat 与 deep 的结果会被追加进同一个 ${ARM}.jsonl;请把 fixture 写进文件名并落进记录。
| if (!['flat', 'deep'].includes(FIXTURE_KIND)) throw new Error('--fixture flat|deep'); | ||
| const FIXTURE = join(HERE, FIXTURE_KIND === 'deep' ? 'fixture-deep' : 'fixture'); | ||
| const TASKS_FILE = join(HERE, FIXTURE_KIND === 'deep' ? 'tasks-deep.json' : 'tasks.json'); | ||
| if (FIXTURE_KIND === 'deep' && !existsSync(join(FIXTURE, 'src', 'features', 'catalog'))) { |
There was a problem hiding this comment.
前置检查只验生成树;缺 node_modules 时 71 行会建出悬空软链,整臂 check 全 FAIL 却不中止——请一并校验 existsSync(join(FIXTURE, 'node_modules'))。
|
|
||
| ## 方法 | ||
|
|
||
| - **任务集**:冻结的 ${commonIds.length} 条任务(\`benchmarks/eval/tasks.json\`)。 |
There was a problem hiding this comment.
任务集硬编码为 benchmarks/eval/tasks.json,用于 deep 产物时报告会自述错误方法;改为从记录的 fixture/taskSet 字段读取,并在两臂不一致时报错退出。
| "smoke": false, | ||
| "task": "checkout 这个 feature 自己的 format 函数返回什么格式的字符串?注意项目里有多个同名 format。", | ||
| "type": "query", | ||
| "checks": [{ "kind": "result_contains", "pattern": "checkout" }] |
There was a problem hiding this comment.
唯一 check result_contains: "checkout" 与任务文本重合,两臂必过、无法区分同名 format.ts 的定位能力;改断言 checkout: 返回串形态或 features/checkout/lib/format\.ts 路径。
|
|
||
| # 目录结构约束 | ||
| directory_structure: | ||
| src/components: |
There was a problem hiding this comment.
src/components/src/hooks/src/utils 在 feature-sliced 深夹具里都不存在,SDD 对真实目录树不构成任何约束;请按 src/features|entities|shared|app 重写并更新 project.name。
| * 用法:node benchmarks/eval/fixture-deep/generate.mjs | ||
| */ | ||
|
|
||
| import { mkdirSync, rmSync, writeFileSync } from 'node:fs'; |
There was a problem hiding this comment.
rmSync 导入后未使用且无清理步骤,改动 FEATURES/ENTITIES 后重跑会留孤儿文件、条目数漂移;删除导入或只清理生成子树(勿删已提交锚点)。
…esults repo-guard's review on #412: the deliverable is a measuring instrument, and it had three ways to emit results that look like findings but are artifacts. **Provenance is now recorded and checked.** Records carry `fixture` and `taskSet`, output files are `<arm>-deep.jsonl` for the deep fixture, and `report-filesense.mjs` reads both and refuses to emit a report if the two arms disagree. Previously the filename carried only the arm, resume-dedup keyed only on `taskId`, and deep task ids do not collide with flat ones — so running both fixtures appended into one file and the report averaged across them while claiming, in its own methodology section, to have used `tasks.json`. **Missing dependencies fail fast.** Without `node_modules`, `setupWorkspace` built a dangling symlink and every `typecheck`/`cmd` check returned non-zero; the only abort heuristic is "zero successful LLM calls", which does not fire when the model is fine. An entire arm of FAIL records would have been recorded as data. A lockfile is committed to match the flat fixture. **The unwired events are labelled.** `validation_failed` and `rollback_failed` have no emit site until #402 lands, so those arrays are necessarily empty on develop — an empty array means "not wired", not "zero interceptions", which is the exact misreading this PR exists to prevent. Rollback collection widened to `rollback_started`/`completed`/`failed` so "undone" and "intercepted but still on disk" are distinguishable. **`options.filesense` now merges instead of replacing.** Whole-object replacement dropped every env-resolved sibling field when a caller overrode one field — the same loss the omitted-case test already guarded against, just via a different door. Regression test included. **The deep fixture's SDD config was a verbatim copy of the flat one**, so its `directory_structure` and `module_boundaries` named `src/components|hooks|utils` — none of which exist here. The deep round would have been nominally "SDD on" with SDD constraining nothing, while three of its tasks test exactly the layering convention that config is supposed to encode. Rewritten for the feature-sliced layout with one-way boundaries. Also: `deep-query-format-convention`'s only assertion echoed its own prompt and would have passed in both arms; the generator now cleans its own subtree (while preserving the committed anchors) instead of leaving orphans that drift the entry count; README documents the arm, the fixture flag, and the generator prerequisite, with a contract test; counts corrected to 242 files / 188 dirs / 430 entries after the planted-bug anchors were added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 评论
决策摘要: 生产侧改动(run.ts 一个可选字段 + 逐字段合并)正确、向后兼容且有测试覆盖,可以合并;但评测仪器本身仍有几处会让「跑出来的数」失真的缺口——最关键的是对照臂 full 的 filesense 状态没有被钉死,仍取自环境变量,跑任何一轮并发布数字之前应先补上。
级联分析
- 变更符号:
RunFrontAgentTaskOptions(新增可选filesense?: FilesenseConfig)、runFrontAgentTask与planFrontAgentTask内的两处AgentConfig组装(run.ts:189/run.ts:461)。 - 受影响流程: 运行时 → core
AgentConfig.filesense→planner-skills.ts:134(filesense?.enabled !== false决定是否注入phase.filesense-navigate);评测编排器run-eval.mjs的三臂配置。 - 变更集外调用方: 全仓 grep
runFrontAgentTask|planFrontAgentTask命中 18 个文件(cli/headless、vscode、desktop、mcp-server、benchmarks)。新字段是可选的,缺省走{ ...resolved.filesense },与改动前的resolved.filesense等价,无调用方破坏。mcp-server.ts:552的filesense: resolved.filesense是frontagent_status的报告载荷、不是第三处AgentConfig组装点,PR 所称「两处组装点」属实;MCP 参数也够不到新字段,status 输出仍准确。(text search + 直接阅读源码,未跑测试) - 置信度: medium
问题发现
-
[中] 对照臂
full的 filesense 状态是环境态、不是钉死的配置- 证据:
run-eval.mjs:57的full: { sddPath: 'sdd.yaml' }未声明 filesense,而resolveRuntimeConfig会读FRONTAGENT_FILESENSE_ENABLED(packages/runtime-node/src/config.ts:342-345);新臂run-eval.mjs:69却把enabled:false钉死。两臂一个钉死、一个随环境。 - 受影响调用方/流程: 两臂对比的全部结论。操作者环境里存在该变量(或将来默认值翻转)时,会得到「两臂都关」的空结果,与 #386 让 guard 臂失效、#411 让本臂失效属于同一类失真——正是本 PR 立论要消灭的那类。
- 最小可行修复:
full: { sddPath: 'sdd.yaml', filesense: { enabled: true } }。这正是本 PR 新增的 seam 才做得到的事。
- 证据:
-
[中] deep 的 bugfix 任务可以靠改测试文件通过
- 证据:
tasks-deep.json三条 bugfix 的唯一实质验收是npx vitest run <该测试文件>退出 0;deep 任务集刻意不给files预置(PR 正文说明),任务描述也不点名源文件;fixture-deep/sdd.yaml的protected_files只保护package.json/tsconfig.json。改测试与改源码在验收上不可区分。平坦任务集靠files: ["src/utils/formatDate.ts"]+ 任务里直接点名源文件形成事实锚定,deep 把这层锚定去掉了。 - 受影响调用方/流程:
report-filesense.mjs的通过率与「仅触发子集」两个口径;两臂不一定对称受影响(导航被关的一臂更可能走捷径),会直接偏向结论方向。 - 最小可行修复: 落盘时记录工作区内
*.test.ts是否与夹具副本一致(一个布尔字段即可),让报告能剔除这类任务;或在checks.mjs增加file_unchangedcheck kind。平坦任务集同样受益,改在checks.mjs是正确的层。
- 证据:
-
[低] 报告器对「开臂零导航」只有散文警告,没有拒绝
- 证据:
report-filesense.mjs:22-33对夹具/任务集不一致是硬拒绝(process.exit(1)),但triggered.length === 0和truncTasks == 0只在正文里写了提醒,仍会照常输出主结果表和「全量差值 X 个百分点」这样的结论式表述。 - 受影响调用方/流程: 报告产物会被提交进
benchmarks/results/当证据。 - 最小可行修复:
triggered计算之后(第 60 行附近)比照 provenance 守卫,零触发时拒绝出报告,或在文首强制打上作废横幅。
- 证据:
-
[低] 事件载荷映射跨包耦合,既无类型检查也无测试
- 证据:
run-eval.mjs:114-122以.mjs直接读event.entries/event.truncated/event.candidateCount,对端是 TS 联合类型packages/core/src/types.ts:775-784。当前字段名完全对得上(已逐字段核对),今天不是缺陷;但 core 一旦改名,filesenseEntries静默变 0、filesenseTruncated静默变 false,报告照样出「累计扫描条目 0」——恰是上一个提交 "stop the instrument from producing evidence-shaped empty results" 要根治的形态。 - 最小可行修复: 把
collectEventDetail导出并加一条最小单测钉住字段映射,或在entries非 number 时显式告警而不是记 0。
- 证据:
-
[低] README 的 deep 流程缺少夹具依赖安装步骤,而本 PR 新加的守卫会硬失败
- 证据:
run-eval.mjs:48-50新增node_modules缺失即 throw(这个守卫本身是对的);README:1319-1325 只写了generate.mjs,没有安装步骤,仓库任何地方也没有文档化过夹具装依赖。照 README 逐条执行的读者会停在第二条命令。该守卫不区分夹具,平坦夹具那段(README:1305-1310)同样受影响。 - 最小可行修复: 在代码块里补一行安装命令,并把它加进
workflow-rules.test.mjs:569那条新断言——理由与该测试注释里给generate.mjs的理由完全相同。
- 证据:
-
[低] 生成器的清理逻辑做不到注释所声称的事
- 证据:
generate.mjs:95-106只遍历当前的FEATURES/ENTITIES做rmSync,而注释说清理是为了「改动 FEATURES / ENTITIES 后重跑会留下孤儿文件,条目数与截断行为随之漂移」。从列表里删掉一个条目时,它的目录会永久残留,条目数照样漂移。 - 最小可行修复: 改为
readdirSync(SRC/features)/entities实际枚举,删除不在ANCHORED_FEATURES里的目录。
- 证据:
-
[低] 已提交的计数与 PR 描述互相不一致
- 证据:
fixture-deep/sdd.yaml:9写「236 源文件 / 188 目录」,run-eval.mjs:33-35写「242 个源文件 …… 全仓 430 条」,PR 正文表格写「repo total 424」。按生成器推算:24 features × 8 + 12 entities × 3 = 228 生成文件 + 14 个已提交 src 文件 = 242 文件,188 目录(我按生成器逐层数过,188 无误),合计 430 条;236/424 这一组恰好少了 6 个已提交的*.test.ts——而真实navigate扫描是会数上这 6 个的。另外 PR 正文与 issue #411 都把修复描述成options.filesense ?? resolved.filesense,实际合入的是逐字段合并{ ...resolved.filesense, ...options.filesense }(run.ts:189/461,并由run.test.ts:417-431钉住)。合并语义更好,但关键骨架变更的 GitNexus 影响摘要描述的是另一份代码。 - 最小可行修复: 两处注释统一成扫描器口径(文件+目录)的同一个数;PR 正文与 #411 的 Fix 段改为「逐字段合并」。
- 证据:
行级发现
- [benchmarks/eval/run-eval.mjs:69] 新臂把
enabled:false钉死,但对照臂full未钉enabled:true,仍取自FRONTAGENT_FILESENSE_ENABLED;给full也加上filesense: { enabled: true },让两臂都由显式配置定义。 - [benchmarks/eval/tasks-deep.json:144] 该 bugfix 的唯一实质验收是跑这个测试文件,agent 改测试即可通过;补一个「测试文件未被修改」的落盘标记或
file_unchangedcheck。 - [benchmarks/eval/run-eval.mjs:117]
event.entries等字段与 core 的AgentEvent联合类型无类型约束也无测试,core 改名会静默变成 0/false;非 number 时应显式告警而不是计 0。 - [benchmarks/eval/report-filesense.mjs:60]
triggered为 0 时应比照上面的夹具 provenance 守卫拒绝出报告(或强制作废横幅),而不是继续输出「全量差值」结论表。 - [README.md:1321] 这段流程缺少夹具依赖安装步骤,而
run-eval.mjs新增的node_modules守卫会在下一条命令直接抛错;补一行安装命令并加进 workflow-rules 断言。 - [benchmarks/eval/fixture-deep/generate.mjs:102] 清理只遍历当前
FEATURES,从列表里删除的条目会留下孤儿目录、让条目数漂移;改为枚举磁盘上实际存在的目录并跳过锚点。 - [benchmarks/eval/fixture-deep/sdd.yaml:9] 「236 源文件」与
run-eval.mjs:33-35的「242 源文件 / 全仓 430 条」口径不一致(差值恰为 6 个已提交测试文件);统一到扫描器实际会数到的口径。
Karpathy 评审
- 假设: 对照臂假设操作者环境是干净的(见发现 1);报告器假设文件名与臂一一对应(
OUT由ARM生成,实际成立);deep 任务假设生成树是最新的(已由run-eval.mjs:42-44的存在性守卫兜住,但不校验新鲜度)。 - 简洁性:
run.ts的改动是最小外科手术式的——一个可选字段 + 两处组装点合并,且合并优于 PR 描述里的??(不会丢掉环境变量解析出的兄弟字段)。collectEventDetail是一个单一分发函数,没有引入框架化抽象。 - 结构质量: 无退化。
report-filesense.mjs:1-56与report.mjs:1-34的 loader 与 6 个统计一行函数近乎逐字重复(约 30 行),但都是稳定的小工具、且 PR 对「为何要独立报告器」给出了可信理由,不构成阻塞项;若之后再加第三个报告器,值得抽一个report-lib.mjs。run.ts两处组装点连同 6 行注释一起复制——这是既有的组装重复,不是本 PR 引入。 - 变更范围: 与既定目标一致。夹具、任务集、报告器、README、workflow 测试都服务于「让这一臂可测量」这一件事;没有夹带无关重构。
- 验证: 生产侧 seam 三条测试覆盖了「透传 / 部分覆盖合并 / 省略不丢失」三个方向,方向完整。夹具的埋雷可信:我核对了三个模块的实现与断言,
computeTotal2 条、mergeLines1 条、estimateEta1 条失败,其余 11 条通过——与 PR 声称的 4 失败 / 11 通过一致。评测侧无自动化验证(见下)。
缺失覆盖
- 「本臂与
full只差一个字段」是本臂的定义性质,却只靠人工阅读保证;scripts/tests/里加一条对ARM_OPTIONS的断言即可长期钉住(顺带能挡住发现 1 那类不对称)。 report-filesense.mjs的 provenance 守卫是全 PR 唯一防止「拿一套夹具的方法学描述另一套产物」的逻辑,本身没有任何测试;它是纯函数式脚本,用两个临时 JSONL 就能测。collectEventDetail的字段映射未被任何测试钉住(发现 4)。- 无端到端回归证明
--arm no-filesense确实产出filesense_navigated: 0——PR 正文给了人工跑的数字,但该证据不在仓库里、也不可重跑校验;这一条属于评测流程的固有限制,记录在此供人工判断。
| // filesense 单独消融:SDD 与 guard 与 full 臂完全相同,唯一变量是导航能力。 | ||
| // 这是回答「filesense 有没有用」的臂——full/ablation 两臂 filesense 都开着, | ||
| // 它们之间的差异说明不了 filesense 的任何事情。 | ||
| 'no-filesense': { sddPath: 'sdd.yaml', filesense: { enabled: false } }, |
There was a problem hiding this comment.
新臂把 enabled:false 钉死,但对照臂 full 未钉 enabled:true,仍取自 FRONTAGENT_FILESENSE_ENABLED;给 full 也加上 filesense: { enabled: true },让两臂都由显式配置定义。
| "type": "modify", | ||
| "checks": [ | ||
| { | ||
| "kind": "cmd", |
There was a problem hiding this comment.
该 bugfix 的唯一实质验收是跑这个测试文件,agent 改测试即可通过;补一个「测试文件未被修改」的落盘标记或 file_unchanged check。
| details.filesense.push({ | ||
| intent: event.intent ?? null, | ||
| paths: event.paths ?? [], | ||
| entries: event.entries, |
There was a problem hiding this comment.
event.entries 等字段与 core 的 AgentEvent 联合类型无类型约束也无测试,core 改名会静默变成 0/false;非 number 时应显式告警而不是计 0。
|
|
||
| // 触发率是本报告的第一读数:filesense 只在触发的任务上可能起作用, | ||
| // 不触发的任务被算进平均值只会把效应稀释成噪音。 | ||
| const triggered = navTasks(arms.full); |
There was a problem hiding this comment.
triggered 为 0 时应比照上面的夹具 provenance 守卫拒绝出报告(或强制作废横幅),而不是继续输出「全量差值」结论表。
|
|
||
| ```bash | ||
| # One-time: materialise the deep fixture's generated module tree | ||
| node benchmarks/eval/fixture-deep/generate.mjs |
There was a problem hiding this comment.
这段流程缺少夹具依赖安装步骤,而 run-eval.mjs 新增的 node_modules 守卫会在下一条命令直接抛错;补一行安装命令并加进 workflow-rules 断言。
| } | ||
| continue; | ||
| } | ||
| rmSync(join(SRC, 'features', feature), { recursive: true, force: true }); |
There was a problem hiding this comment.
清理只遍历当前 FEATURES,从列表里删除的条目会留下孤儿目录、让条目数漂移;改为枚举磁盘上实际存在的目录并跳过锚点。
| project: | ||
| name: "eval-fixture-deep" | ||
| type: "react-spa" | ||
| description: "filesense 消融评测的冻结深夹具工程(feature-sliced,236 源文件 / 188 目录)" |
There was a problem hiding this comment.
「236 源文件」与 run-eval.mjs:33-35 的「242 源文件 / 全仓 430 条」口径不一致(差值恰为 6 个已提交测试文件);统一到扫描器实际会数到的口径。
… editing the test Third repo-guard round on #412. **The control arm was not pinned.** `no-filesense` hard-set `enabled: false` while `full` declared nothing and inherited `FRONTAGENT_FILESENSE_ENABLED` from the operator's environment. One arm pinned, one ambient: on a machine with that variable set, both arms run with filesense off and the comparison returns a clean-looking null — the same failure shape as #386 voiding the guard arm and #411 voiding this one. `full` now pins `enabled: true`, which is only expressible because of the seam this PR adds. **The deep bugfix tasks could be passed by editing the test.** Their only substantive check is `vitest run <that test file>`, and the deep task set deliberately omits the `files` pre-seed and does not name the source file — pre-seeding hands over the answer. So "fix the source" and "edit the test" were indistinguishable at acceptance, and the two arms would not take that shortcut symmetrically: the arm without navigation is likelier to, which biases the result in a specific direction. New `file_unchanged` check kind in `checks.mjs` (diffing against the fixture original), applied to all three. Verified end-to-end. **The report now refuses zero-trigger runs.** Provenance mismatch was already a hard exit, but a run where filesense never fired still printed the main table and a "difference: X pp" conclusion — and these reports get committed as evidence. **A field-shape mismatch now throws instead of counting zero.** `run-eval.mjs` reads core's `AgentEvent` fields from `.mjs`, with no type checking. A rename in core would have silently produced `filesenseEntries: 0` and a report claiming "total entries scanned: 0" — the exact evidence-shaped-empty-result failure the previous commit set out to remove. Also: the generator's cleanup enumerates directories on disk rather than the current `FEATURES`/`ENTITIES` lists, so removing an entry no longer leaves an orphan that drifts the entry count; README documents the fixture dependency install that the new `node_modules` guard requires (with a contract assertion); and the 236/424 counts are corrected to the scanner's own 242/430. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 请求修改
决策摘要: 生产侧改动(run.ts 一个可选字段 + 逐字段合并)正确且有测试覆盖,可以合并;但评测 harness 里两处缺陷正属于本 PR 声称要根治的「空结果被当成证据」——事件形状守卫被 Agent.emit 的 try/catch 静默吞掉,且 ablation 臂未钉死 filesense,修掉这两处即可合入。
级联分析
- 变更符号:
RunFrontAgentTaskOptions.filesense(新增可选字段)、runFrontAgentTask/planFrontAgentTask的AgentConfig组装(packages/runtime-node/src/run.ts:187、:459)、runChecks(checks, {…, fixtureRoot})(benchmarks/eval/checks.mjs:10)、run-eval.mjs的ARM_OPTIONS/OUT命名 / JSONL 记录形状。 - 受影响流程: 运行时任务装配 → core
planner-skills.ts读filesense.enabled;评测编排 → checks →report-filesense.mjs/report.mjs。 - 变更集外调用方:
runFrontAgentTask的 15 个调用点(cli/headless、desktop、vscode、mcp-server、旧 benchmark)均未传filesense,取到{...resolved.filesense}—— 值等同、仅对象身份变化,无调用方依赖身份(text search)。runChecks唯一调用方是run-eval.mjs,已同步传fixtureRoot(text search)。filesense_navigated载荷字段与packages/core/src/types.ts:776-783逐字段吻合;rollback_failed确实尚未进入AgentEvent联合类型,注释与现状一致(graph 不可用,读源码确认)。 - 置信度: medium(无代码图谱,结论来自 diff review + 只读源码核对;关联 issue #411/#386/#400 上下文完整)
问题发现
-
[高]
filesense_navigated形状守卫被静默吞掉,仍会产出「静默为 0」的假数据- 证据:
collectEventDetail在entries非数字时throw(run-eval.mjs:120-125),但该回调经run.ts:253-256注册进agent.addEventListener,而Agent.emit对 listener 异常只做try { listener(event) } catch { this.debugError(...) }(packages/core/src/agent/agent.ts:304-311)。评测跑在非 debug +filterConsole: true下,异常完全不可见。 - 受影响调用方/流程: 抛出发生在
events[e.type]自增之后,于是 JSONL 会记下filesense_navigated: N却带着空eventDetails.filesense→filesenseEntries: 0、filesenseTruncated: false;report-filesense.mjs的零触发守卫用的是events.filesense_navigated计数(report-filesense.mjs:52,60,65),因此照常出报告,正文印出「累计扫描条目 0 / 预算闸触发 0」。这正是注释里写的「宁可吵,不可静默」要防的失真,守卫目前只是给了虚假的安全感。 - 最小可行修复: 不要在 listener 里抛。把形状不符记成
details.shapeErrors.push(...),在每条任务落盘后检查——非空即console.error+process.exit(2)(与「零成功 LLM 调用」中止同一处理);同时在report-filesense.mjs增加一条守卫:events.filesense_navigated > 0但eventDetails.filesense为空时拒绝出报告。
- 证据:
-
[中]
ablation臂未钉死 filesense,full却钉死了 —— 新引入的两臂环境不对称- 证据:
run-eval.mjs:57-59的注释明确写「对照臂也必须把 filesense 钉死」,full拿到filesense: { enabled: true }(:60),但ablation(:61-68)没有该字段,仍取FRONTAGENT_FILESENSE_ENABLED(packages/runtime-node/src/config.ts:341-345)。 - 受影响调用方/流程:
report.mjs的 full vs ablation 对比。本 PR 之前两臂同取环境变量、至少是对称的;改动后操作者环境里存在FRONTAGENT_FILESENSE_ENABLED=0就会得到「full 开、ablation 关」,SDD 消融的差值里混进 filesense 变量。按 PR 自己的论证(#386 废掉 guard 臂),这是同一类失真。 - 最小可行修复: 给
ablation补上filesense: { enabled: true },或抽一个const PINNED = { filesense: { enabled: true } }基底展开进三个臂,让「唯一变量」这条前提在配置里可见。
- 证据:
-
[低] 实现语义与 PR 描述 / issue #411 / GitNexus 影响摘要不一致
- 证据: 三处文本都写
options.filesense ?? resolved.filesense、并称「与 #389 给 hallucinationGuard 用的形状相同」,实际代码是逐字段合并{ ...resolved.filesense, ...options.filesense }(run.ts:189、:461),与hallucinationGuard: options.hallucinationGuard(run.ts:184,直接透传、无 fallback)并不同形。 - 受影响调用方/流程: 合并语义本身更好(保住
FRONTAGENT_FILESENSE_MAX_ENTRIES等兄弟字段,run.test.ts 已 pin),但关键骨架变更的影响摘要描述了不存在的代码,后续评审与 #411 的读者会按??语义推理。另有一个未 pin 的边界:调用方显式传{ maxEntries: undefined }会把已解析值覆盖成undefined,??语义下不会。 - 最小可行修复: 更新 PR 描述与 #411 正文为「逐字段合并」,并在
run.test.ts补一条「显式 undefined 字段」的期望(保留覆盖或忽略,二选一并写死)。
- 证据: 三处文本都写
-
[低] 两个 reporter 复制了同一组汇总 helper
- 证据:
report-filesense.mjs:11-20,46-51,103-115的load/rate/avg/sum/pct/count与失败清单循环,与report.mjs:14-34,103-117基本同形。 - 受影响调用方/流程: 口径漂移风险——某一侧改了
avg的取整或pass判定,两份提交进benchmarks/results/的报告会用不同口径描述同一批 JSONL。 - 最小可行修复: 抽
benchmarks/eval/report-lib.mjs存这几个纯函数,两个 reporter 各自只保留叙事。非阻塞,可后续 PR 处理。
- 证据:
-
[低] 夹具规模数字在三处不一致
- 证据: PR 正文写「236 source files / repo total 424」,
run-eval.mjs:33-35与fixture-deep/sdd.yaml:9写「242 源文件 / 430 条目」。按生成器算:24×8 + 12×3 = 228 噪音文件 + 14 个已提交 src 文件 = 242;242 + 188 目录 = 430。两组数差 6,恰好等于 6 个.test.ts,疑似统计口径不同(含/不含测试文件)。 - 受影响调用方/流程: 这些数字是「预算闸真的会关」这条方法学主张的证据,会被引用进 results 文档。
- 最小可行修复: 统一到一处口径,并注明是否计入
.test.ts。
- 证据: PR 正文写「236 source files / repo total 424」,
行级发现
- [benchmarks/eval/run-eval.mjs:121] 这个
throw在agent.addEventListener的回调里,被Agent.emit的 try/catch 吞掉(packages/core/src/agent/agent.ts:304-311),非 debug 下无任何输出;结果仍是filesenseEntries: 0的静默假数据。改为把形状错误记进details,任务落盘后统一process.exit(2)。 - [benchmarks/eval/run-eval.mjs:60]
full臂钉死了 filesense,但紧邻的ablation臂没有,仍随FRONTAGENT_FILESENSE_ENABLED变动;按本行上方注释自己的理由,给ablation补filesense: { enabled: true }(或抽公共基底展开进三个臂)。 - [benchmarks/eval/report-filesense.mjs:60] 触发判定只看
events.filesense_navigated计数,与filesenseEntries(来自eventDetails)不是同一数据源;补一条守卫:计数 > 0 而eventDetails.filesense为空时拒绝出报告,否则会印出「累计扫描条目 0」当结论。 - [packages/runtime-node/src/run.ts:189] 实际是逐字段合并,而 PR 描述 / issue #411 / GitNexus 摘要都写的是
options.filesense ?? resolved.filesense;请更新文本,并补一条测试钉住调用方显式传undefined字段时的预期行为。
Karpathy 评审
- 假设: 「抛异常就能吵醒操作者」这条假设未经验证,实际被 emit 的 try/catch 否定(发现 1)。「两臂唯一变量是 filesense」在 full/no-filesense 之间成立,但被引入到 full/ablation 之间时不成立(发现 2)。
rollback_failed/validation_failed依赖未合入的 #402,注释已如实标注,符合预期。 - 简洁性: 合并语义、arm/fixture 参数化、
file_unchanged都是与问题规模匹配的最小手段,未见猜测性抽象。两个 reporter 的 helper 复制是唯一的多余重复(发现 4)。 - 结构质量: 无文件跨界膨胀(
run-eval.mjs249 行、report-filesense.mjs174 行),新逻辑都落在自己拥有的层;fixtureRoot作为显式参数传入runChecks而非藏进全局,边界干净。生成器只清理磁盘上实际存在的目录(而非遍历常量列表)是正确的选择。 - 变更范围: 与 #411 的目标一致,无夹带重构。README +
workflow-rules.test.mjs的文档断言与运行时新增的硬失败(缺node_modules即拒跑)配套,属于必要范围。已核对 flat 夹具确实有package.json/pnpm-lock.yaml,README 新增的 install 命令可执行。 - 验证:
run.ts三条测试方向完整(覆盖、部分合并、省略回退),且toEqual({ enabled: false })在normalizeFilesense*默认返回undefined的前提下成立。深夹具的「4 失败 / 11 通过」我按埋点逐条复算与 PR 声明一致(computeTotal 2 + mergeLines 1 + estimateEta 1),三个 bug 均可被Math.ceil/ 累加 / 乘数量修复,任务可解。
缺失覆盖
- 事件形状不符时 harness 行为的回归:目前无任何测试或运行时断言能捕捉「计数 > 0 但载荷为空」这一状态(发现 1 的直接后果)。
checks.mjs的file_unchanged没有任何单测,而它现在是 bugfix 任务的通过/失败闸门:建议补 node:test 覆盖未改动、已改动、文件缺失、fixtureRoot未提供四种分支。- 平坦任务集
tasks.json的 bugfix 任务仍没有file_unchanged,与tasks-deep.json验收口径不对称;若两套结果曾被并列引用,需在报告里说明差异(PR 已论证平坦任务预置了files,风险较低,但差异本身未记录)。 - 无测试钉住
filesense覆盖对象里显式undefined字段的合并结果(发现 3)。
| // core 一旦改名,entries 会静默变成 0、报告照样出「累计扫描条目 0」—— | ||
| // 正是本 harness 要根治的「空结果被当成证据」。宁可吵,不可静默。 | ||
| if (typeof event.entries !== 'number') { | ||
| throw new Error( |
There was a problem hiding this comment.
这个 throw 在 agent.addEventListener 的回调里,被 Agent.emit 的 try/catch 吞掉(packages/core/src/agent/agent.ts:304-311),非 debug 下无任何输出;结果仍是 filesenseEntries: 0 的静默假数据。改为把形状错误记进 details,任务落盘后统一 process.exit(2)。
| // 对照臂也必须把 filesense 钉死。不写的话它取自 FRONTAGENT_FILESENSE_ENABLED—— | ||
| // 一臂钉死、一臂随环境,操作者环境里存在该变量就会得到「两臂都关」的空结果, | ||
| // 与 #386 废掉 guard 臂、#411 废掉本臂是同一类失真。 | ||
| full: { sddPath: 'sdd.yaml', filesense: { enabled: true } }, |
There was a problem hiding this comment.
full 臂钉死了 filesense,但紧邻的 ablation 臂没有,仍随 FRONTAGENT_FILESENSE_ENABLED 变动;按本行上方注释自己的理由,给 ablation 补 filesense: { enabled: true }(或抽公共基底展开进三个臂)。
|
|
||
| // 触发率是本报告的第一读数:filesense 只在触发的任务上可能起作用, | ||
| // 不触发的任务被算进平均值只会把效应稀释成噪音。 | ||
| const triggered = navTasks(arms.full); |
There was a problem hiding this comment.
触发判定只看 events.filesense_navigated 计数,与 filesenseEntries(来自 eventDetails)不是同一数据源;补一条守卫:计数 > 0 而 eventDetails.filesense 为空时拒绝出报告,否则会印出「累计扫描条目 0」当结论。
| filesense: resolved.filesense, | ||
| // 合并而非整体替换:调用方常常只覆盖一个字段(消融臂只给 enabled: false), | ||
| // 整体替换会把 FRONTAGENT_FILESENSE_MAX_ENTRIES 等解析出来的兄弟字段一起丢掉。 | ||
| filesense: { ...resolved.filesense, ...options.filesense }, |
There was a problem hiding this comment.
实际是逐字段合并,而 PR 描述 / issue #411 / GitNexus 摘要都写的是 options.filesense ?? resolved.filesense;请更新文本,并补一条测试钉住调用方显式传 undefined 字段时的预期行为。
… was unpinned Fourth repo-guard round on #412 — and the first finding is the same failure mode this PR exists to remove, committed by the fix for it. **The guard threw, and `Agent.emit` swallows it.** `agent.ts:307-315` wraps every listener call in try/catch and reports at debug level, so a `throw` from `collectEventDetail` would have vanished and the run would have carried on producing the silently-zero data the guard was added to prevent. Shape mismatches are now accumulated and the arm aborts after the record is written, alongside the existing "zero successful LLM calls" abort. **The `ablation` arm was not pinned either.** The last round pinned `full`, but `ablation` still inherited `FRONTAGENT_FILESENSE_ENABLED`. With three arms, one of them ambient means arm-to-arm differences are no longer attributable to the one thing being ablated. Also: the generator now measures the tree it produced and prints it in the scanner's own units, instead of prose repeating a hardcoded number that drifts whenever `FEATURES`/`ENTITIES` change — which is what put three different figures in three places. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 files from the deep-fixture work (PR #412) were swept into this branch by a `git add benchmarks/` while they sat untracked in the worktree. This PR is docs/data only — the report, its raw JSONL, the probe, and the README qualifier with its contract test. Nothing here references fixture-deep, and a committed `node_modules` symlink pointed at a gitignored path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 高
处理建议: 请求修改
决策摘要: ** 评测侧(deep 夹具、事件载荷、report-filesense)质量高且自洽,但 run.ts 新增的 filesense?: FilesenseConfig 与同一个 options 对象上已经接通的 filesenseEnabled / filesenseMaxEntries 等六个扁平字段完全重叠——消融臂本可零production改动实现,合并前需要先决定保留哪一套公共配置面。
级联分析
- 变更符号: 原始模型未提供结构化级联字段。
- 受影响流程: 原始模型未提供结构化级联字段。
- 变更集外调用方: unknown
- 置信度: degraded
问题发现
- [高]
filesense是第二套重叠的公共配置面;#411 的「消融臂无法存在」前提不成立- 证据:
RunFrontAgentTaskOptions extends RuntimeConfigInput(run.ts:43),而RuntimeConfigInput已声明filesenseEnabled?: boolean | string(config.ts:68)等六个字段;resolveRuntimeConfig优先读input.filesenseEnabled,再回落到环境变量(config.ts:342-345)。即runFrontAgentTask({ filesenseEnabled: false })早就能让planner-skills.ts:134跳过注入。该扁平面还已暴露到 MCP 工具 schema(mcp-server.ts:268,373)并有测试(config.test.ts:134、mcp-server.test.ts:92,150)。本 PR 自己的合并测试正是用filesenseMaxEntries: 42作为「已解析配置」——说明扁平面可用且作者在用。 - 受影响调用方/流程:
RunFrontAgentTaskOptions是 runtime-node 导出的公共契约,加进去就很难再删。合并后同一件事有两个入口、优先级未定义(嵌套覆盖扁平),下一个「同一 defect class 第五例」会从这里长出来。 - 最小可行修复: 二选一并说清楚。(a) 删掉
filesense?: FilesenseConfig,ARM_OPTIONS改用filesenseEnabled: false/filesenseEnabled: true——production diff 归零,消融臂照样成立,#411 需相应改写为「文档缺失」而非「死配置」;(b) 若确要保留嵌套面,补一条测试钉住{ filesenseEnabled: true, filesense: { enabled: false } }的优先级,并在 JSDoc 里写明嵌套优先于扁平。
- 证据:
- [中] 事件契约守卫会把被污染的记录先落盘再中止,断点续跑时静默复活
- 证据:
run-eval.mjs:245先appendFileSync(OUT, record),run-eval.mjs:249才检查harnessFailures并exit(3)。此时filesenseEntries已按?? 0汇总为 0 写入 JSONL。 - 受影响调用方/流程: 续跑去重只按
taskId(run-eval.mjs:98-103),修好后重跑会跳过这条,report-filesense 照常把它计入「累计扫描条目」。这正是该守卫注释里要根治的「空结果被当成证据」,只是换了条路径。 - 最小可行修复: 把
harnessFailures.length > 0的判断提到appendFileSync之前,失败时不写记录直接exit(3)。
- 证据:
- [中] report-filesense 只守「开臂零触发」,不守「关臂非零触发」
- 证据:
report-filesense.mjs:60,65对arms.full有零触发拒绝出报告的守卫,但全文没有任何一处断言navTasks(arms.off).length === 0;分类表的「该类触发导航数」列也只统计f(开臂)。 - 受影响调用方/流程:
filesense_navigate工具在agent.ts:232-253是无条件注册的,filesense.enabled只关掉 planner 技能注入(planner-skills.ts:134)。PR 用一条任务人工核对了关臂为 0,但报告器本身不会在后续每轮复核这个不变量——而「关臂其实没关」正是 #386/#411 的失效形态,且判定所需数据(events.filesense_navigated)已经在 JSONL 里。 - 最小可行修复: 在零触发守卫旁加对称检查,关臂出现任何
filesense_navigated即console.error+exit(1),或至少在方法学章节印出关臂触发数。
- 证据:
- [低] 同一对象字面量内
hallucinationGuard整体替换、filesense逐字段合并- 证据:
run.ts:184与run.ts:189相邻两行语义不同;run.test.ts现有用例还专门钉住hallucinationGuard省略时为undefined。 - 受影响调用方/流程: 无行为风险,但两个消融开关的覆盖语义不一致,后续加第三个开关时无既定规则可循。
- 最小可行修复: 在
filesense那条注释里补一句说明为何两者语义不同(guard 无扁平面、无 env 兄弟字段可丢),避免读者误以为是疏漏。
- 证据:
行级发现
- [packages/runtime-node/src/run.ts:69] 该字段与
RuntimeConfigInput.filesenseEnabled/filesenseMaxEntries/filesenseOutput/filesenseWriteMode/filesenseMaxBytes/filesenseTimeoutMs(config.ts:68-73,已在config.ts:342-360接通并经 MCP schema 暴露)功能完全重叠;建议删除本字段并让消融臂改用filesenseEnabled: false,或保留但在此处写明嵌套优先于扁平的优先级规则。 - [packages/runtime-node/src/run.ts:189] 合并语义正确,但未定义
options.filesense.enabled与options.filesenseEnabled冲突时的优先级;若保留嵌套面,请补一条同时传两者的测试把「嵌套胜出」钉死。 - [benchmarks/eval/run-eval.mjs:74] 此处用新增的嵌套入口;改成
{ sddPath: 'sdd.yaml', filesenseEnabled: false }可让本 PR 的 production diff 归零,消融臂效果等价。 - [benchmarks/eval/run-eval.mjs:249] 守卫在
appendFileSync(第 245 行)之后才触发,被污染的记录已落盘;续跑按 taskId 去重会跳过它,报告仍会把filesenseEntries: 0计入。请把这段前移到写盘之前。 - [benchmarks/eval/report-filesense.mjs:60] 只对开臂做了零触发守卫;请一并断言关臂
navTasks(arms.off).length === 0,否则「关臂其实没关」这类失真仍需人工发现。
Karpathy 评审
- 假设: 模型输出需要归一化为固定 Markdown 契约。
- 简洁性: 已提取 summary、finding、evidence 与 fix;原始 prose 不再附在评论中,避免占用下游解析与代理上下文。
- 变更范围: 原始模型未提供结构化范围字段。
- 验证: 需要查看 CI、测试或人工 CR 证据补强合并信心。
缺失覆盖
- 输出未命中 Repo Guard Markdown 契约;建议补充真实模型质量评估覆盖。
| * (`planner-skills.ts` 真读),但从运行时 API 根本够不着—— | ||
| * filesense 消融臂会像 #386 废掉 guard 臂那样,静默退化成「两臂都开着」。 | ||
| */ | ||
| filesense?: FilesenseConfig; |
There was a problem hiding this comment.
该字段与 RuntimeConfigInput.filesenseEnabled/filesenseMaxEntries/filesenseOutput/filesenseWriteMode/filesenseMaxBytes/filesenseTimeoutMs(config.ts:68-73,已在 config.ts:342-360 接通并经 MCP schema 暴露)功能完全重叠;建议删除本字段并让消融臂改用 filesenseEnabled: false,或保留但在此处写明嵌套优先于扁平的优先级规则。
| filesense: resolved.filesense, | ||
| // 合并而非整体替换:调用方常常只覆盖一个字段(消融臂只给 enabled: false), | ||
| // 整体替换会把 FRONTAGENT_FILESENSE_MAX_ENTRIES 等解析出来的兄弟字段一起丢掉。 | ||
| filesense: { ...resolved.filesense, ...options.filesense }, |
There was a problem hiding this comment.
合并语义正确,但未定义 options.filesense.enabled 与 options.filesenseEnabled 冲突时的优先级;若保留嵌套面,请补一条同时传两者的测试把「嵌套胜出」钉死。
| // filesense 单独消融:SDD 与 guard 与 full 臂完全相同,唯一变量是导航能力。 | ||
| // 这是回答「filesense 有没有用」的臂——full/ablation 两臂 filesense 都开着, | ||
| // 它们之间的差异说明不了 filesense 的任何事情。 | ||
| 'no-filesense': { sddPath: 'sdd.yaml', filesense: { enabled: false } }, |
There was a problem hiding this comment.
此处用新增的嵌套入口;改成 { sddPath: 'sdd.yaml', filesenseEnabled: false } 可让本 PR 的 production diff 归零,消融臂效果等价。
| console.log( | ||
| `[${ARM}] ${task.id}: ${pass ? 'PASS' : 'FAIL'} (${record.elapsedMs}ms, ${llmCalls} calls, ${llmFailures} failures, events=${JSON.stringify(events)})`, | ||
| ); | ||
| if (harnessFailures.length > 0) { |
There was a problem hiding this comment.
守卫在 appendFileSync(第 245 行)之后才触发,被污染的记录已落盘;续跑按 taskId 去重会跳过它,报告仍会把 filesenseEntries: 0 计入。请把这段前移到写盘之前。
|
|
||
| // 触发率是本报告的第一读数:filesense 只在触发的任务上可能起作用, | ||
| // 不触发的任务被算进平均值只会把效应稀释成噪音。 | ||
| const triggered = navTasks(arms.full); |
There was a problem hiding this comment.
只对开臂做了零触发守卫;请一并断言关臂 navTasks(arms.off).length === 0,否则「关臂其实没关」这类失真仍需人工发现。
…s already worked Fifth repo-guard round on #412, and this one invalidates the premise I filed issue #411 on. `RuntimeConfigInput` already exposes `filesenseEnabled`, `filesenseOutput`, `filesenseWriteMode`, `filesenseMaxEntries`, `filesenseMaxBytes` and `filesenseTimeoutMs`, and `RunFrontAgentTaskOptions extends RuntimeConfigInput`. `resolveRuntimeConfig` wires all six. Verified directly against a build with my change reverted: resolveRuntimeConfig({ filesenseEnabled: false }, '/tmp') -> filesense = { enabled: false } and end-to-end on the deep fixture with `run.ts` at develop's version: full filesense_navigated = 1 no-filesense filesense_navigated = 0 So the ablation arm needed **zero production change**. The `filesense?: FilesenseConfig` field I added was a second, overlapping public config surface for settings that already had one, and #411's claim that the arm "cannot exist" was wrong. `run.ts` and `run.test.ts` are reverted to develop; the harness uses `filesenseEnabled`. I reached for a new seam without checking whether the existing one reached the same place — the same failure of verification the rest of this series keeps finding in the code. Also from this round: - The event-contract guard aborted *after* appending the record. Resume-dedup keys on `taskId`, so a re-run after the fix would have **skipped** the poisoned row and the report would have counted its zeroes — the guard's own failure mode by another route. It now aborts before the write. - `report-filesense.mjs` guards the off arm too. It refused a zero-trigger *on* arm but never checked that the *off* arm is actually zero; a non-zero there means filesense was never disabled, which is equally fatal in the opposite direction. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
229 files from PR #412's deep fixture were swept in by `git add benchmarks/` while they sat untracked in the worktree. They came back a second time because the first removal only untracked them — the files stayed on disk, and this branch carries no .gitignore for that path, so the next `git add benchmarks/` re-staged them. This PR is docs/data only: the report, its raw JSONL, the probe, and the README qualifier with its contract test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 请求修改
决策摘要: 代码本身可合并——我逐层验证了 --arm no-filesense 在当前 develop 上确实生效,无需任何 runtime 改动;但 PR 正文、Impact Scope 与 GitNexus 影响摘要仍在描述一个已被最后一次 revert 撤掉、且前提本就不成立的 run.ts 修改,而 #411 已 CLOSED 并宣称了一个仓库里不存在的修复。这份记录一旦合入,就是这个 PR 自己反对的那类"看起来有证据的空结论",修正成本只是文字与 issue 状态。
级联分析
- 变更符号:
runChecks(新增fixtureRoot参数与file_unchanged分支)、run-eval.mjs 的ARM_OPTIONS/ JSONL record 形状(新增fixture/taskSet/eventDetails/filesenseEntries/filesenseTruncated)、新增report-filesense.mjs、新增 deep 夹具与任务集。 - 受影响流程:
run-eval.mjs→runFrontAgentTask(filesenseEnabled扁平字段)→resolveRuntimeConfig(config.ts:341-345)→run.ts:176filesense: resolved.filesense→planner-skills.ts:134filesense?.enabled !== false。链路完整,no-filesense臂确实不会注入phase.filesense-navigate。 - 变更集外调用方:
runChecks仅被run-eval.mjs:214调用(text search,无其他调用点),签名扩展为可选参数、旧调用不存在,无破坏。report.mjs(2026-07-12 报告器)读full.jsonl/ablation.jsonl,deep 产物落*-deep.jsonl,两者不互相污染。AgentEvent.filesense_navigated的字段(types.ts:775-784:entries/truncated/candidateCount/warnings)与 harness 的读取一致。夹具的.gitignore不影响导航——filesense 用的是.filesignore(mcp-filesense/src/types.ts:146),生成树对扫描器可见,430/180 的实测口径成立。 - 置信度: medium(无代码图谱,结论来自直接读取
run.ts/config.ts/planner-skills.ts/types.ts与 grep;未实际执行两臂评测)。
问题发现
-
[高] PR 记录与实际 diff 不符:宣称的
run.ts关键骨架变更不在本次变更中- 证据: 26 个变更文件中没有
packages/runtime-node/**;仓库当前run.ts:176与run.ts:446仍是filesense: resolved.filesense,不存在options.filesense ?? resolved.filesense,run.test.ts也没有 issue 里承诺的两条测试。而 PR 的 Impact Scope、GitNexus 影响摘要("Critical skeleton changes: yes")、以及 checklist 里"已为关键骨架变更跑pnpm quality:local"都在描述这个不存在的变更。分支最后一次提交revert(runtime): drop the redundant filesense option已说明原因:RunFrontAgentTaskOptions extends RuntimeConfigInput(run.ts:42),后者声明了filesenseEnabled(config.ts:68)并在config.ts:343解析——扁平字段本来就能到达 core,#411 的前提("caller 传入的 filesense 被静默丢弃")对这条路径不成立。 - 受影响调用方/流程: 合并记录 / issue #411(状态 CLOSED,正文给出的
filesense: { ...resolved.filesense, ...options.filesense }修复在树里不存在)。后续读者按 issue 去run.ts找不到该代码,很可能"重新修一遍",把整对象替换或合并语义再引入一次;同时"第四例 declared-but-never-wired"这个说法会带着 #386/#400/#403 一起进入 benchmark 叙事而无对应证据。 - 最小可行修复: 把 Impact Scope / GitNexus 摘要改为"仅 benchmarks + README + scripts/tests,无生产代码变更",删除
quality:local一项或注明不适用;把"Fixes #411"改为说明 #411 已被本分支自证为误报,并在 #411 上补一条纠正(扁平filesenseEnabled一直有效,最初被丢弃的是嵌套filesense对象),或将其重开为"嵌套filesense选项缺失"的准确描述。表格里 full=1 / no-filesense=0 的实测无需改动——那个观测仍然成立。
- 证据: 26 个变更文件中没有
-
[中] 只有
filesense.enabled被钉死,预算字段仍随操作者环境浮动,而报告把预算截断当结论印出来- 证据:
ARM_OPTIONS(run-eval.mjs:60/65/74)只固定filesenseEnabled;output/writeMode/maxEntries/maxBytes/timeoutMs仍取自FRONTAGENT_FILESENSE_*(config.ts:346-361),且planner-skills.ts:147-151中filesense?.maxEntries ?? decision.maxEntries让配置值压过 trigger-policy 的默认预算。JSONL 记录了fixture/taskSet做溯源,却没有记录生效的 filesense 配置。 - 受影响调用方/流程:
report-filesense.mjs的"预算闸被触发(truncated)的任务数"与"累计扫描条目"——这两个数正是 deep 夹具存在的理由。操作者环境里存在FRONTAGENT_FILESENSE_MAX_ENTRIES=1000时,两臂对称地不截断,报告会照常印出truncated = 0,读者按正文的读数纪律会把它理解为"夹具太小",而真实原因是环境变量。这与本 PR 亲自给enabled加钉死注释所防的是同一类失真。 - 最小可行修复: 在
ARM_OPTIONS里一并钉死filesenseMaxEntries/filesenseMaxBytes/filesenseOutput/filesenseWriteMode/filesenseTimeoutMs;或把resolved.filesense随 record 落盘,并在report-filesense.mjs的 provenance 守卫旁加一条"两臂预算配置一致且为已知值"的断言。
- 证据:
-
[低] 关臂并非"filesense 不可用",而是"不注入导航步骤"
- 证据:
agent.ts:247无条件把filesense_navigate注册进 file 工具映射;enabled: false只影响planner-skills.ts:133的注入判定。我已确认 LLM 计划 prompt / schema 未暴露 filesense 动作(llm/prompts.ts、llm/schemas.ts中无匹配),因此实际泄漏概率很低。 - 受影响调用方/流程:
report-filesense.mjs对关臂任一次触发直接exit 1拒绝出报告——一旦发生,代价是丢弃一整轮已跑完的两臂评测。 - 最小可行修复: 保留守卫但降级为"剔除泄漏任务并在报告中显式列出",或在正文把口径写成"关闭注入式导航"而非"关闭 filesense"。
- 证据:
行级发现
- [benchmarks/eval/run-eval.mjs:60] 与
filesenseEnabled同理,filesenseMaxEntries/filesenseMaxBytes/filesenseOutput/filesenseWriteMode/filesenseTimeoutMs也应在三臂里钉死(或落盘进 record 供报告断言),否则报告的"预算闸被触发数"随操作者环境静默改变。 - [benchmarks/eval/run-eval.mjs:128] 注释说"由主循环在写完本条记录后中止本臂",但 245-253 行是先中止、不写记录(且那才是正确行为)。改成"在写盘前中止",避免下次有人按注释把
process.exit挪到appendFileSync之后。 - [benchmarks/eval/run-eval.mjs:225]
TASKS_FILE.split('/').pop()在非 POSIX 分隔符下会把整条绝对路径写进taskSet,报告随后拼成benchmarks/eval/<绝对路径>;改用node:path的basename(TASKS_FILE)(该文件已从node:path导入其他函数)。
Karpathy 评审
- 假设: 最大的隐藏假设不在代码里而在正文——"#411 是真 bug 且已在本 PR 修复"。实际链路
filesenseEnabled → resolveRuntimeConfig → resolved.filesense → planner-skills:134一直通畅,本分支自己 revert 时也承认了这点。次要假设:deep 夹具的 430/180 口径依赖FEATURES/ENTITIES长度,任何改动都会让 run-eval.mjs:33-35 与 sdd.yaml 描述里的数字漂移,而没有任何检查会因此失败(generate.mjs 只console.log实测值)。 - 简洁性: 与问题规模匹配。
file_unchanged是最小可行的"防改测试"验收;report-filesense.mjs与report.mjs分开而不是加 mode 分支,是正确的取舍(两者叙事不同,合并只会造出模式 flag)。事件载荷只截存必要字段并做长度截断,没有过度通用化。 - 结构质量: 没有退化。新增逻辑全部落在新文件或既有 switch 的新 case 上;
collectEventDetail从主循环抽出为纯函数,主循环没有变长成难扫描的块;无薄 wrapper、无重复的既有 helper、无文件跨越可扫描边界。 - 变更范围: 与目标一致,无无关重构与格式噪音。唯一可讨论的是 flat 夹具的
pnpm --dir ... install文档行——但它是 runner 新增硬失败的直接后果,属于必要范围。 - 验证: 三条 harness 守卫(provenance / 零触发 / 关臂泄漏)与
file_unchanged是本 PR 最关键的新逻辑,却没有任何自动化测试;新增的scripts/tests用例只断言 README 文案。埋 bug 的 4 失败 / 11 通过与 estimateEta 的Math.ceil语义我按 diff 复核一致(floor(1000/500)+1=3仅被"整数倍"那条用例捕获,600km 那条在修复前后都通过)。
缺失覆盖
checks.mjs的file_unchanged:无单测。fixtureRoot传错或缺失时它返回ok:false,会把全部 deep bugfix 任务判为 FAIL——一个静默把整臂推向失败的路径,值得一条与 README 文案检查同级的测试。report-filesense.mjs的三条守卫(fixture/taskSet 不一致、开臂零触发、关臂泄漏):都是"防止空结果被当证据"的承重逻辑,均无测试。用几行合成 JSONL 即可覆盖退出码与退出原因。- deep 夹具规模(242 文件 / 188 目录 / 430 条目)与截断阈值:目前只写在注释、
sdd.yaml描述与 README 散文里。generate.mjs已经实测并打印,建议让它在实测值与文档值不符时非零退出,否则调整FEATURES/ENTITIES会让这个夹具的存在理由静默失效。 --fixture deep与--arm ablation的组合未被文档或校验覆盖:deep 夹具没有sdd.disabled.yaml(这正是该臂期望的"文件不存在"),但也没有任何地方说明这个组合是否受支持。
| // 对照臂也必须把 filesense 钉死。不写的话它取自 FRONTAGENT_FILESENSE_ENABLED—— | ||
| // 一臂钉死、一臂随环境,操作者环境里存在该变量就会得到「两臂都关」的空结果, | ||
| // 与 #386 废掉 guard 臂、#411 废掉本臂是同一类失真。 | ||
| full: { sddPath: 'sdd.yaml', filesenseEnabled: true }, |
There was a problem hiding this comment.
与 filesenseEnabled 同理,filesenseMaxEntries / filesenseMaxBytes / filesenseOutput / filesenseWriteMode / filesenseTimeoutMs 也应在三臂里钉死(或落盘进 record 供报告断言),否则报告的"预算闸被触发数"随操作者环境静默改变。
| if (typeof event.entries !== 'number') { | ||
| // 不能 throw:`Agent.emit` 对监听器异常是 try/catch + debug 级日志 | ||
| // (`agent.ts:307-315`),抛出去只会被静默吞掉——正是本 harness 要防的那种 | ||
| // 「守卫看起来加了、实际没生效」。改为记账,由主循环在写完本条记录后中止本臂。 |
There was a problem hiding this comment.
注释说"由主循环在写完本条记录后中止本臂",但 245-253 行是先中止、不写记录(且那才是正确行为)。改成"在写盘前中止",避免下次有人按注释把 process.exit 挪到 appendFileSync 之后。
| // 报告器据此断言两臂用的是同一套夹具与任务集——否则会拿 flat 的方法学 | ||
| // 去描述 deep 的产物,正是 #410 撤回结论的同一类失真。 | ||
| fixture: FIXTURE_KIND, | ||
| taskSet: TASKS_FILE.split('/').pop(), |
There was a problem hiding this comment.
TASKS_FILE.split('/').pop() 在非 POSIX 分隔符下会把整条绝对路径写进 taskSet,报告随后拼成 benchmarks/eval/<绝对路径>;改用 node:path 的 basename(TASKS_FILE)(该文件已从 node:path 导入其他函数)。
Sixth repo-guard round on #412. Only `filesenseEnabled` was pinned; `output` / `writeMode` / `maxEntries` / `maxBytes` / `timeoutMs` still came from `FRONTAGENT_FILESENSE_*`. That matters more than it looks: `planner-skills.ts` lets a configured value **override** trigger-policy's default budget, and "was the budget truncated" is one of the conclusions this report prints. An operator with `FRONTAGENT_FILESENSE_MAX_ENTRIES` set would have published a fact about their machine. All five are pinned now, the effective config is written into every JSONL record, and `report-filesense.mjs` refuses to emit if the two arms disagree — the same shape as the existing fixture/task-set provenance guard. Verified end-to-end: both arms carry identical budgets, differ only in `enabled`, and the arm switch still reads 1 vs 0. Also corrected the framing: the off arm does not make filesense *unavailable*, it stops the planner *injecting* the navigate step. The tool stays in the registry; the plan prompt and schema expose no filesense action, so the model cannot reach it — which is what the off-arm zero-trigger guard verifies. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They belong to PR #412. Swept in by `git add` while sitting untracked in a shared worktree; this is the third time, so the generated tree is now kept out of the worktree entirely rather than re-untracked after the fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They belong to PR #412. Swept in by `git add` while sitting untracked in a shared worktree; this is the third time, so the generated tree is now kept out of the worktree entirely rather than re-untracked after the fact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 请求修改
决策摘要: 无生产代码变更、no-filesense 臂的构造有效性经查证成立、#411 的回退判断正确;但 FILESENSE_PINNED 用一组常量顶替了 trigger-policy 的按 intent 预算,而报告把「预算闸被触发」当结论印出,合入后会产出一份描述合成预算而非产品行为的证据文件——先修这一项再合。
级联分析
- 变更符号:
runChecks(checks, {workspace, resultText, fixtureRoot})(新增第 3 个字段)、run-eval.mjs的--arm/--fixtureCLI 契约、JSONL 记录 schema(新增fixture/taskSet/filesenseConfig/eventDetails/filesenseEntries/filesenseTruncated)、新增 check kindfile_unchanged。 - 受影响流程:
benchmarks/eval/评测编排 → JSONL 产出 →report-filesense.mjs;README 文档块 →scripts/tests/workflow-rules.test.mjs断言。 - 变更集外调用方:
runChecks仅run-eval.mjs:233一处调用,签名向后兼容(fixtureRoot可选,缺失时file_unchanged返回 ok:false)(text search)。.github/**无任何run-eval引用,新增的node_modules硬失败仅影响开发者手动流程 (text search)。report.mjs(2026-07-12 那轮)读full.jsonl/ablation.jsonl,deep 产物落-deep.jsonl,互不干扰。 - 置信度: medium —— 无代码图谱,结论由 diff + 全仓文本检索得出;
packages/**未被触碰,跨包耦合只有run-eval.mjs运行时读取 core 的AgentEvent字段,我已逐字段核对packages/core/src/types.ts:776-784与packages/shared/src/types/validation.ts:9-13,当前名称全部对得上。
问题发现
-
[高] 钉死的预算顶替了 trigger-policy 的按 intent 预算,而截断数是报告的结论行
- 证据:
run-eval.mjs:64-70三臂统一钉死filesenseMaxEntries: 300/filesenseMaxBytes: 131072/filesenseTimeoutMs: 3000;packages/core/src/skills/planner-skills.ts:147-149是filesense?.maxEntries ?? decision.maxEntries,即配置值替换而非兜底策略预算。packages/core/src/filesense/trigger-policy.ts的预算是按 intent 分配的:validate_freshness120/48KB/1500ms(:153-156)、query 的understand_structure250/96KB(:164-165)、prepare_create180/64KB(:188-189)、prepare_refactor500/160KB(:201-202)。钉死后全部塌成 300/128KB/3000ms ——prepare_refactor被收紧 500→300、160KB→128KB,prepare_create被放宽 180→300,validate_freshness被放宽 120→300 且超时翻倍。 - 受影响调用方/流程:
report-filesense.mjs把「预算闸被触发(truncated)的任务数」作为独立结论行印出,并在「读数纪律」里据此解释夹具规模是否足够。该数字现在描述的是产品从不使用的扁平预算:deep 夹具 430 条目下,两条 refactor 任务会被харness 制造出策略本不会产生的截断,而 create 任务的截断则被抑制。PR 描述里的实测表(prepare_create、maxEntries 180 → 扫 180、truncated true)与run-eval.mjs:33-35的「实测同样预算下扫 180 条即截断」,说的正是随后被这段配置覆盖掉的那个预算。 - 最小可行修复:
filesenseOutput/filesenseWriteMode保留钉死(这两项是忠实的 no-op:config.ts:144-154未设时返回undefined,planner-skills.ts:150-151的兜底恰好就是'summary'/'cache');把三个数值预算从FILESENSE_PINNED移除,改为在 harness 启动时删除或断言未设FRONTAGENT_FILESENSE_MAX_ENTRIES/_MAX_BYTES/_TIMEOUT_MS。这样同时拿到臂间确定性与产品真实的按 intent 预算。eventDetails.filesense[].intent已经记了 intent,报告可据此把截断归因到具体 intent。若扁平化是刻意选择,就在报告的「读数纪律」里写明预算已被压平为 300、不再反映出厂策略,并停止把截断呈现为夹具属性。
- 证据:
-
[中]
filesenseConfig记的是请求值不是生效值,报告的预算守卫是同义反复- 证据:
run-eval.mjs:247-250用传入时的同一组ARM_OPTIONS/FILESENSE_PINNED常量构造该字段,全程没有回读resolveRuntimeConfig实际产出的FilesenseConfig。report-filesense.mjs的预算一致性守卫比较的正是这些自证字段。 - 受影响调用方/流程: 这恰好是本系列在追的缺陷类(#411 / #386 / #400 / #403:「参数在一层被接受,从未到达读它的那一层」)—— 真发生时 JSONL 仍会照常宣称钉死的预算。该守卫实际只能捕获「两臂 JSONL 来自不同版本 harness」这一种情形。PR 描述的「The effective config is written into every record」高于实际。注意
enabled是被独立验证过的:报告的关臂零触发守卫构成真实证据;预算没有对应证据。 - 最小可行修复: 字段更名为
filesenseConfigRequested,并在报告里注明预算一致性是同进程同常量的构造性结论、非遥测验证;或让 core 在filesense_navigated载荷里回显生效的 navigate 参数,再据此断言。
- 证据:
-
[中] 形状守卫只覆盖一个事件的一个字段,其余新增载荷仍会静默降级
- 证据:
run-eval.mjs:144-151只校验filesense_navigated.entries是数字。同一函数还读event.truncated、event.candidateCount(:157-158)与event.result?.results/event.result?.blockedBy(:169-172),全部无守卫。我已核对这些名称当前与types.ts:776-784、validation.ts:9-13一致。 - 受影响调用方/流程:
truncated是报告头条截断行的唯一输入。core 一旦改名,每条记录truncated: undefined→filesenseTruncated: false→ 报告印出「预算闸被触发 0」,正是:141-143注释声称要根治的「空结果被当成证据」,只是换了个字段。 - 最小可行修复: 把同一条守卫扩展到
typeof event.truncated === 'boolean'与typeof event.candidateCount === 'number',复用已有的harnessFailures中止路径。
- 证据:
-
[低] 手写 basename
- 证据:
run-eval.mjs:244用TASKS_FILE.split('/').pop(),而node:path的join/resolve/dirname已在 :5 导入。 - 受影响调用方/流程: 非 POSIX 分隔符下会记入绝对路径,
report-filesense.mjs随后对fixture/taskSet拼串做split('/')解构会取错分段。 - 最小可行修复: 改用
basename(TASKS_FILE)。
- 证据:
行级发现
- [benchmarks/eval/run-eval.mjs:67] 这个 300 会经
planner-skills.ts:147顶替trigger-policy的按 intent 预算(prepare_create180、prepare_refactor500、validate_freshness120),使报告的「预算闸被触发」描述一套产品不使用的扁平预算;改为清除/断言FRONTAGENT_FILESENSE_MAX_ENTRIES等环境变量并让本字段保持未设,即可同时拿到臂间确定性与真实策略预算。 - [benchmarks/eval/run-eval.mjs:157]
truncated与下一行的candidateCount未纳入上面那条形状守卫,改名后会静默变成false/undefined并让报告印出「预算闸被触发 0」;按entries的写法补两条typeof校验进harnessFailures。 - [benchmarks/eval/run-eval.mjs:247] 这里写的是请求值而非
resolveRuntimeConfig的生效值,报告据此做的预算一致性校验对「配置在中途被丢弃」(#411 那一类)完全不敏感;更名为filesenseConfigRequested并在报告中降格该守卫的表述,或让 core 回显生效参数后再断言。 - [benchmarks/eval/run-eval.mjs:244] 手写
split('/').pop()在非 POSIX 分隔符下会记入绝对路径并破坏报告端的split('/')解构;node:path已导入,改用basename(TASKS_FILE)。
Karpathy 评审
- 假设: PR 对自身假设的处理是本次评审见到的正面样本——#411 被自查证伪并连同
run.ts/run.test.ts一起回退,我核对packages/runtime-node/src/config.ts:68-73与:341-361确认RuntimeConfigInput确实已暴露六个扁平字段,回退判断成立,diff 中确无packages/**文件。关臂的构造有效性也成立:packages/core/src/llm/(schemas.ts/prompts.ts/plan-generation.ts)全无 filesense 字样,模型无法自行选中filesense_navigate,注入是planner-skills.ts:133-134这一条路径且受enabled !== false控制 (text search)。剩下的未言明假设只有一条:预算钉死等价于「排除操作者环境」,实际上它同时排除了产品自己的预算策略(问题 1)。 - 简洁性: 三臂共享
FILESENSE_PINNED展开、collectEventDetail按事件类型早返回、报告的守卫逐条独立且各带失败原因,没有猜测性抽象。file_unchanged是最小可行的新增 check kind,而不是一套通用快照框架。 - 结构质量: 无退化。
checks.mjs新增一个 case(+22 行,全文件 60 行);run-eval.mjs增至 291 行,远未越过可扫描边界;新增文件各自职责单一,report-filesense.mjs与report.mjs的分工在文件头写明了。夹具生成器把「实测规模」而非硬编码数字打印出来,避免了文档漂移。 - 变更范围: 与既定目标一致,无夹带重构。README 顺带补上平坦夹具的
pnpm --dir benchmarks/eval/fixture install,这是新增的node_modules硬失败带来的必要文档修正,不是范围蔓延。 - 验证: 夹具算术可复核并成立——24 features × 8 + 12 entities × 3 + 14 个已提交锚点 = 242 文件,188 目录,430 条目,与
generate.mjs/sdd.yaml/ README /run-eval.mjs:33-35的说法一致。三处埋 bug 逐条核对无误:computeTotal丢弃quantity(2 条失败)、mergeLines用赋值而非累加(1 条)、estimateEta的floor(d/500)+1在整数倍处多一天(1 条),合计 4 失败 / 11 通过,与 PR 声明相符,且每条都存在有效修法(eta 用Math.ceil,distanceKm <= 0早返回已护住 0)。.gitignore的否定链正确(每一层父目录先取消忽略再忽略其内容)。缺口在于报告自身的四条拒绝路径无任何测试(见下)。
缺失覆盖
report-filesense.mjs的四条拒绝路径(两臂夹具/任务集不一致、预算配置不一致、开臂零触发、关臂有泄漏)是这份报告全部的完整性保障,目前没有任何测试;scripts/tests/workflow-rules.test.mjs只断言 README 字符串。建议对load之后的判定逻辑抽出纯函数并补最小单测,覆盖四条process.exit(1)。- 新增 check kind
file_unchanged无测试:至少覆盖缺fixtureRoot、原件或工作区文件缺失、内容一致、内容被改四种返回。 - 修问题 1 后,需要一条断言「未向
runFrontAgentTask传入数值预算时,filesense_navigated的扫描量随 intent 变化」的证据,否则无法确认策略预算确实生效。 - README 中新钉进
workflow-rules.test.mjs:583的pnpm --dir benchmarks/eval/fixture-deep install本身未被执行验证;fixture-deep 与既有平坦夹具一样位于pnpm-workspace.yaml(packages/*/apps/*)之外,沿用既有实践,属残余风险而非发现。
| const FILESENSE_PINNED = { | ||
| filesenseOutput: 'summary', | ||
| filesenseWriteMode: 'cache', | ||
| filesenseMaxEntries: 300, |
There was a problem hiding this comment.
这个 300 会经 planner-skills.ts:147 顶替 trigger-policy 的按 intent 预算(prepare_create 180、prepare_refactor 500、validate_freshness 120),使报告的「预算闸被触发」描述一套产品不使用的扁平预算;改为清除/断言 FRONTAGENT_FILESENSE_MAX_ENTRIES 等环境变量并让本字段保持未设,即可同时拿到臂间确定性与真实策略预算。
| paths: event.paths ?? [], | ||
| entries: event.entries, | ||
| elapsedMs: event.elapsedMs, | ||
| truncated: event.truncated, |
There was a problem hiding this comment.
truncated 与下一行的 candidateCount 未纳入上面那条形状守卫,改名后会静默变成 false/undefined 并让报告印出「预算闸被触发 0」;按 entries 的写法补两条 typeof 校验进 harnessFailures。
| taskSet: TASKS_FILE.split('/').pop(), | ||
| // 生效的 filesense 配置随记录落盘:没有它,「预算被截断」这个结论 | ||
| // 无法与产生它的预算值对应,报告也就无从校验两臂是否同一套预算。 | ||
| filesenseConfig: { |
There was a problem hiding this comment.
这里写的是请求值而非 resolveRuntimeConfig 的生效值,报告据此做的预算一致性校验对「配置在中途被丢弃」(#411 那一类)完全不敏感;更名为 filesenseConfigRequested 并在报告中降格该守卫的表述,或让 core 回显生效参数后再断言。
| // 报告器据此断言两臂用的是同一套夹具与任务集——否则会拿 flat 的方法学 | ||
| // 去描述 deep 的产物,正是 #410 撤回结论的同一类失真。 | ||
| fixture: FIXTURE_KIND, | ||
| taskSet: TASKS_FILE.split('/').pop(), |
There was a problem hiding this comment.
手写 split('/').pop() 在非 POSIX 分隔符下会记入绝对路径并破坏报告端的 split('/') 解构;node:path 已导入,改用 basename(TASKS_FILE)。
…licy's per-intent values Seventh repo-guard round on #412, and a consequence of my own previous round. Pinning `maxEntries` / `maxBytes` / `timeoutMs` to make the arms deterministic **flattened the product's per-intent budgets**. `planner-skills.ts` uses `filesense?.maxEntries ?? decision.maxEntries` — configuration *replaces* the policy value rather than backing it up. trigger-policy allocates by intent: `validate_freshness` 120, `prepare_create` 180, `understand_structure` 250, `prepare_refactor` 500. Pinning 300 **tightened** `prepare_refactor` and loosened `validate_freshness` while doubling its timeout — and "was the budget truncated" is a conclusion line of this report. I would have been measuring a budget the product never uses. Determinism now comes from asserting the three environment variables are unset, which the harness refuses to start without. The arms stay reproducible and the per-intent budgets stay real; `eventDetails.filesense[].intent` is already recorded, so truncation can be attributed to the intent that caused it. `output` / `writeMode` stay pinned — those are faithful no-ops: unset they resolve to `undefined`, and `planner-skills.ts` falls back to exactly `'summary'` / `'cache'`. **`filesenseConfig` renamed to `filesenseConfigRequested`.** It read back the same in-process constants it was passing in, so the report's consistency check was tautological. The name and the report now say so; a genuine check needs core to echo the effective navigate parameters in the event payload. **The shape guard covered one field of one event.** Checking only `entries` left `truncated` degrading to `false` and `candidateCount` to `0` — the same silent degradation, one field over. All four collected fields are checked now. Verified end-to-end: the navigate step runs under `understand_structure`'s own budget again, and the arm switch still reads 1 vs 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…swept in Fourth occurrence. Untracking after the fact does not work: the generated tree stays in the shared worktree, this branch carries no ignore rule for it, and the next `git add benchmarks/` re-stages all 229 files. The ignore rule is the fix — #412 exempts the 20 files it genuinely commits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 低
处理建议: 评论
决策摘要: 可以合并——变更全部位于 benchmarks/eval/,无生产代码,消融臂的机制已按仓库实际接口核实为真;四个非阻塞问题都关于「空数据会被误读成零事件」,建议合入前后修掉其中前两条注释/守卫级问题。
级联分析
- 变更符号:
runChecks(新增可选fixtureRoot与file_unchangedcheck kind)、run-eval.mjsCLI 契约(新增--fixture、第三臂no-filesense、输出文件名后缀)、JSONL 记录 schema(新增fixture/taskSet/filesenseConfigRequested/eventDetails/filesenseEntries/filesenseTruncated)、新增report-filesense.mjs、新增夹具与任务集。 - 受影响流程: 评测编排 → 检查执行 → JSONL 落盘 → 报告生成;README 与
scripts/tests/workflow-rules.test.mjs的文档断言。 - 变更集外调用方:
runChecks全仓仅run-eval.mjs一处调用,已同步更新 (text search)。benchmarks/eval/report.mjs仍读full.jsonl/ablation.jsonl,deep 跑产出-deep后缀、记录字段为纯新增,故不受影响 (已直接读report.mjs:1-34确认)。packages/**与apps/**无改动,PR 描述里「run.ts 变更已回滚」与仓库现状一致:packages/runtime-node/src/run.ts:42的RunFrontAgentTaskOptions extends RuntimeConfigInput,config.ts:68-73已暴露filesenseEnabled等扁平字段,planner-skills.ts:134读filesense?.enabled !== false——即消融臂无需新 seam,#411 关为 invalid 的判断成立。 - 置信度: medium(无代码图谱;结论来自直接阅读被调用文件 + 全仓文本搜索,
packages/**零改动使下游执行流不需评估)
问题发现
-
[中]
rollback_started/rollback_completed在 develop 上同样没有 emit 站点,注释声称「一定会到」与仓库事实不符- 证据: 全仓搜索这两个事件名,只命中
packages/core/src/types.ts:794-795(联合类型声明)与apps/desktop/src/state/executionReducer.ts:286,289(消费方),packages/**源码中没有任何 emit 站点;validation_failed同样只有types.ts:793一处声明。而run-eval.mjs:206的注释写的是「rollback_started/rollback_completed是既有事件,一定会到」,只把rollback_failed标为未接线。PR 描述里的清单同样只列了validation_failed与rollback_failed。 - 受影响调用方/流程: 每条记录的
eventDetails.rollback在 develop 上恒为[]。后续读 JSONL 写报告的人按相邻注释会把它读成「没有发生回滚」,而不是「事件未接线」——正是本 PR 为validation_failed在run-eval.mjs:193-195明确防住的那类误读,也是 #410 撤回分类的同一根因。 - 最小可行修复: 把
run-eval.mjs:206-207的注释改为「三者在 develop 上均无 emit 站点,空数组含义是未接线」,与 193 行同一措辞;并同步 PR 描述里的未接线清单。
- 证据: 全仓搜索这两个事件名,只命中
-
[中]
validation_failed载荷提取没有形状守卫,与同一函数内 filesense 分支的标准不对称- 证据:
run-eval.mjs:170-181对filesense_navigated的四个字段逐个断言类型并记入harnessFailures(主循环据此在写盘前中止),理由写得很清楚是「宁可吵,不可静默」;但run-eval.mjs:199-202对validation_failed用的是(event.result?.results ?? [])/(event.result?.blockedBy ?? [])纯可选链。当前形状确实匹配packages/shared/src/types/validation.ts:9-14(pass/results[].pass|type|severity|message/blockedBy?),所以今天是对的。 - 受影响调用方/流程: 一旦
ValidationResult改名或嵌套层级变化,failedChecks会静默变成[],报告照样能出「零拦截」。而 #402 落地后的第一轮跑正是这个字段最有说服力、也最经不起静默降级的时刻。 - 最小可行修复: 在
validation_failed分支加同样的记账——typeof event.result?.pass !== 'boolean'或!Array.isArray(event.result?.results)时 push 进harnessFailures。
- 证据:
-
[低] 报告器对「平坦夹具」不设闸,而 PR 自己论证平坦夹具上的对比按构造不可解释
- 证据:
report-filesense.mjs:47只在FIXTURE_KIND === 'unknown'时拒绝出报告。用--fixture flat跑出的full.jsonl/no-filesense.jsonl会通过全部守卫(两臂夹具一致、开臂有触发、关臂零触发),正常输出一份含「全量差值 X 个百分点」的报告;而 PR 描述与run-eval.mjs:30-32都论证该夹具上「预算闸从不关闸、导航与ls -R不可区分」。 - 受影响调用方/流程: 报告产物是要提交进
benchmarks/results/当证据的。正文虽然会印出夹具名、「读数纪律」也提示了 truncated=0 的外推限制,但结论行仍是一个看起来可引用的百分点差值。 - 最小可行修复: 在
FIXTURE_KIND !== 'deep'且truncTasks(arms.full).length === 0时,与零触发守卫同级拒绝出报告(或至少在标题下方插入一条不可省略的横幅),复用已有的process.exit(1)路径。
- 证据:
-
[低]
cleanGeneratedTree对锚点 feature 只清 4 个硬编码子目录,遗漏了它自己生成的lib/format.ts与index.ts- 证据:
generate.mjs:104对锚点 feature 只rmSync['api','model','hooks','ui'],但生成循环对每个 feature(含 checkout/cart/shipping)都会写features/<f>/lib/format.ts(generate.mjs:138-142)和features/<f>/index.ts。这与该函数上方generate.mjs:92-96自述的不变量(「不清会留下孤儿文件,条目数与截断行为随之漂移——而这两个数正是这个夹具存在的理由」)不一致。 - 受影响调用方/流程: 生成器结构调整后,3 个锚点 feature 下会残留约 5 个孤儿文件,条目总数漂移;而条目数与是否截断正是报告的结论行与 README 中引用的实测数字。
- 最小可行修复: 锚点分支改为「删除该目录下除已提交锚点文件外的一切」,或把豁免清单与
.gitignore的白名单收敛到同一处常量,避免两份手写清单继续分叉。
- 证据:
行级发现
- [benchmarks/eval/run-eval.mjs:206]
rollback_started/rollback_completed在 develop 源码中同样没有 emit 站点(仅types.ts:794-795声明 + desktop reducer 消费),注释断言「一定会到」会让读者把恒空的eventDetails.rollback读成「零回滚」;改为与 193 行同一措辞,声明三者当前均未接线。 - [benchmarks/eval/run-eval.mjs:199] 这里用可选链兜底提取
ValidationResult,缺少 filesense 分支那样的形状断言;result.results一旦改名会静默产出failedChecks: []。加一条!Array.isArray(event.result?.results)→harnessFailures.push(...)即可与同函数标准对齐。 - [benchmarks/eval/report-filesense.mjs:47] 守卫只拦
unknown夹具;flat夹具的对比按本 PR 自己的论证不可解释,却能正常出一份带百分点结论的报告。建议在FIXTURE_KIND !== 'deep'且零截断时按同一路径拒绝出报告。 - [benchmarks/eval/fixture-deep/generate.mjs:104] 硬编码的
['api','model','hooks','ui']漏掉了本文件同样会写入锚点 feature 的lib/format.ts与index.ts,与 92-96 行自述的「不留孤儿、条目数不漂移」不符;改为保留白名单式清理。
Karpathy 评审
- 假设: 关键假设已被验证而非声称——
filesenseEnabled确实经RuntimeConfigInput→resolved.filesense→planner-skills.ts:134生效,filesenseOutput/WriteMode钉死为'summary'/'cache'与planner-skills.ts:150-151的兜底一致,属忠实 no-op;数值预算改为「断言环境变量未设」而非钉死,与trigger-policy.ts的按 intent 分档(120/180/250/300/500)一致。剩余未验证假设是「关臂模型确实点不到已注册的filesense_navigate工具」,PR 用关臂零触发守卫把它转成了可证伪项,处理得当。 - 简洁性: 无投机抽象。
file_unchanged是最小新增 check kind,fixtureRoot为可选参数、老调用不受影响。两个报告器复制了 6 个一行 helper(rate/avg/sum/pct/count),叙事确实不同,量级不足以要求抽公共模块。 - 结构质量: 未见退化。
run-eval.mjs增至 322 行、report-filesense.mjs204 行,均在可扫描范围;没有共享路径的 ad-hoc 特例,--fixture分支集中在文件头部的三个常量上。-deep输出后缀避免了两套夹具静默混入同一 JSONL,是真正删除了一类误用而非搬运复杂度。 - 变更范围: 与既定目标一致。回滚
run.ts/run.test.ts后 diff 不含生产代码;README 与 workflow 测试的新增断言与新命令一一对应(--arm no-filesense、--fixture deep、generate.mjs、report-filesense.mjs、pnpm --dir ... install均已在 README 1313-1331 出现)。给平坦夹具补的pnpm --dir benchmarks/eval/fixture install是新增硬失败守卫的必要配套,不算范围蔓延。 - 验证: 计划外的三处已核实:
biome.json:61忽略benchmarks,故新增夹具文件不进 lint;pnpm test是turbo test,只覆盖 workspace(pnpm-workspace.yaml仅packages/*/apps/*),fixture-deep 的 4 条埋雷失败测试不会进 CI;平坦夹具已有同类埋雷(fixture/src/utils/parseQuery.ts缺decodeURIComponent),提交失败测试属既有惯例。PR 自报的4 failures / 11 passing与三个埋雷模块的断言逐条对得上(computeTotal 2、mergeLines 1、estimateEta 1;总计 15 条)。
缺失覆盖
checks.mjs的file_unchanged没有任何自动化测试:全仓benchmarks/eval/下无脚本级测试,scripts/tests/也只有 README 断言。至少应覆盖三条分支——基准与工作区一致、被改动、以及fixtureRoot缺失时返回ok:false(后者目前只有它自己会静默把任务判失败)。report-filesense.mjs的四道完整性闸(两臂夹具/任务集不一致、预算不一致、开臂零触发、关臂泄漏)是这份证据的把关点,目前只能靠手工触发验证;建议补一组喂假 JSONL 的脚本级用例,断言各自exit 1。run-eval.mjs的事件契约中止路径(harnessFailures→ 不写盘 →exit 3)同样无覆盖;这条路径的价值恰恰在于「平时不跑」,一旦回归会以静默写入污染记录的形式表现出来。
| }); | ||
| return; | ||
| } | ||
| // `rollback_started` / `rollback_completed` 是既有事件,一定会到; |
There was a problem hiding this comment.
rollback_started / rollback_completed 在 develop 源码中同样没有 emit 站点(仅 types.ts:794-795 声明 + desktop reducer 消费),注释断言「一定会到」会让读者把恒空的 eventDetails.rollback 读成「零回滚」;改为与 193 行同一措辞,声明三者当前均未接线。
| if (event.type === 'validation_failed') { | ||
| details.validationFailed.push({ | ||
| // 判失败的检查项:区分「真·内容拦截」与「纯工具失败」的唯一依据 | ||
| failedChecks: (event.result?.results ?? []) |
There was a problem hiding this comment.
这里用可选链兜底提取 ValidationResult,缺少 filesense 分支那样的形状断言;result.results 一旦改名会静默产出 failedChecks: []。加一条 !Array.isArray(event.result?.results) → harnessFailures.push(...) 即可与同函数标准对齐。
| console.error('JSONL 缺少 filesenseConfigRequested 字段——来自旧版 harness,无法确认预算口径,拒绝出报告'); | ||
| process.exit(1); | ||
| } | ||
| if (FIXTURE_KIND === 'unknown') { |
There was a problem hiding this comment.
守卫只拦 unknown 夹具;flat 夹具的对比按本 PR 自己的论证不可解释,却能正常出一份带百分点结论的报告。建议在 FIXTURE_KIND !== 'deep' 且零截断时按同一路径拒绝出报告。
| if (!entry.isDirectory()) continue; | ||
| if (anchored.has(entry.name)) { | ||
| // 锚点目录只清生成的子目录,保留 lib/ 下已提交的文件 | ||
| for (const sub of ['api', 'model', 'hooks', 'ui']) { |
There was a problem hiding this comment.
硬编码的 ['api','model','hooks','ui'] 漏掉了本文件同样会写入锚点 feature 的 lib/format.ts 与 index.ts,与 92-96 行自述的「不留孤儿、条目数不漂移」不符;改为保留白名单式清理。
Conflict in run-eval.mjs's onEvent: #414 added per-stage counting, this branch added payload collection. Both are needed — the stage counter without payloads cannot tell a real interception from a tool error, and payloads without the stage counter leave the aggregate unusable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
代码评审报告: feat(eval): filesense ablation arm, deep fixture, and event payloads (#411)
风险等级: 中
处理建议: 请求修改
决策摘要: 无生产代码风险,工具设计扎实且自证性强;但生成的报告正文有一句可验证为假的溯源声明(report-filesense.mjs:109),而「预算闸会关上」这条核心方法学结论是在 trigger-policy 从不发出的 depth 2 + maxEntries 180 组合下实测的——两处都是文本/度量口径修正,改完即可合。
级联分析
- 变更符号:
runChecks(新增fixtureRoot参数与file_unchanged分支)、run-eval.mjs的ARM_OPTIONS/collectEventDetail/ JSONL record schema;新增report-filesense.mjs、fixture-deep/*、tasks-deep.json。 - 受影响流程: 仅
benchmarks/eval/评测编排;packages/**、apps/**无改动。已直接确认packages/runtime-node/src/run.ts:176,446仍为 develop 版本(filesense: resolved.filesense,无嵌套filesense选项),PR 所述的回退是真实的。 - 变更集外调用方:
runChecks全仓仅run-eval.mjs:269一处调用,新参数为可选、向后兼容。report.mjs读full.jsonl/ablation.jsonl,deep 产物落-deep.jsonl,无覆盖。fixture-deep/package.json不在pnpm-workspace.yaml(仅packages/*、apps/*)内,嵌套 lockfile 与既有fixture/同构,不污染根安装。 - 跨包契约核对(逐字段直读,非图谱遍历):
filesense_navigated的intent/paths/entries/elapsedMs/truncated/candidateCount/warnings与packages/core/src/types.ts:796-805完全一致;validation_failed.result.{results,blockedBy}与packages/shared/src/types/validation.ts:9-14一致;rollback_failed确实尚未进入联合类型,PR 的说明属实。filesenseEnabled/Output/WriteMode确实存在于RuntimeConfigInput(config.ts:68-73),planner-skills.ts:156的filesense?.enabled !== false就是关臂的真实开关,output ?? 'summary'/writeMode ?? 'cache'(:172-173、:149)证实钉死这两项是忠实 no-op。 - 置信度: medium(结论来自直接读文件而非代码图谱;受影响面已完整枚举且很小)
问题发现
-
[高] 报告正文声称的校验范围与 harness 实际记录的内容不符
- 证据:
report-filesense.mjs:109写入产物:"SDD、guard、RAG、模型、任务集、夹具、以及 filesense 的全部预算参数都相同且被显式钉死(已从 JSONL 校验)"。但 JSONL record(run-eval.mjs:273-306)只含fixture/taskSet/filesenseConfigRequested{enabled,output,writeMode},不含 SDD、guard、RAG、模型任一字段;而数值预算恰恰是run-eval.mjs:56-75明确刻意不钉的("数值预算刻意不钉"),仅靠启动时的环境变量断言(:79-89),该断言结果同样未落进 JSONL。 - 受影响调用方/流程: 该字符串是要提交进
benchmarks/results/当证据的产物首屏。读者会据此认为臂间同质性已由数据验证,而实际上是构造性假设。这正是 #410 撤回结论的同一类失真,只是这次出现在用来防止它的工具里。 - 最小可行修复: 把该句拆成两句——"夹具、任务集、filesense 的
enabled/output/writeMode已从 JSONL 校验;SDD、guard、RAG、模型与数值预算由 harness 单进程常量保证,未落盘校验,数值预算来自 trigger-policy 按 intent 分档且启动时断言相关环境变量未设"。
- 证据:
-
[中] 「深夹具让预算闸关上」是在 trigger-policy 不会发出的 depth/预算组合下实测的
- 证据: PR 正文与
run-eval.mjs:30-36、README 均以 "src, depth 2, maxEntries 180 → 180 条截断" 作为深夹具存在的理由。但trigger-policy.ts:184-190的prepare_create是 depth 1 / maxEntries 180;depth 2 只出现在understand_structure(250)、locate(300)、prepare_refactor(500)。(depth 2, 180) 不是任何 intent 会产生的组合。按engine.ts:730+walkDirectories:252-287的 depth 语义(depth N 仍列举第 N 层目录的子项)估算本夹具:从srcdepth 1 ≈ 43 条,depth 2 ≈ 219 条。于是prepare_create(depth 1/180)不截断,locate(depth 2/300)不截断,understand_structure(depth 2/250)只有在任务文案恰好含英文字面量src(trigger-policy.ts:113-133的 FOCUS_DIRS 匹配,12 条任务中仅deep-query-structure命中)时才可能越过 250。 - 受影响调用方/流程: 报告的结论行「预算闸被触发(truncated)的任务数」很可能是 0–1,而报告自己的读数纪律会把 0 解释为"夹具太小",与 README 的"deep 夹具会截断"直接冲突。夹具确实显著更大、同名文件干扰也确实成立,受影响的只是截断这一条主张。
- 最小可行修复: 用每个 intent 实际会发出的 (paths, depth, maxEntries) 重跑一次探针,把表格换成按 intent 的真实值;若真实预算下均不截断,就把 README 与代码注释里的理由从"预算闸会关闸"改为"同名文件使文件名不足以定位",不要保留未被产品配置支持的截断承诺。
- 证据: PR 正文与
-
[中] 事件形状守卫挡不住它自己描述的那种静默降级
- 证据:
run-eval.mjs:160-181的注释声称"core 一旦改名,entries 会静默变成 0、报告照样出「累计扫描条目 0」——宁可吵,不可静默"。但事件载荷在到达监听器前先经packages/core/src/context/helpers.ts:56-63的normalizeFilesenseNavigation归一化:entries非 number 时补0,elapsedMs补0,truncated用=== true强制成 boolean,candidates缺失时为[]→candidateCount为0。所以只要漂移发生在 MCP 返回载荷这一侧(最可能的一侧),类型检查全部通过,报告照样印出「累计扫描条目 0 / 预算闸触发 0」。守卫真正能捕获的只有AgentEvent联合类型自身改名。 - 受影响调用方/流程: 报告没有零条目守卫(只有零触发、关臂泄漏两个守卫),零条目会被读数纪律解释成"夹具太小",与真实原因(载荷通路断了)无法区分。
- 最小可行修复: 把注释改为准确描述守卫覆盖面(只覆盖事件联合类型漂移),并在报告侧补一条守卫:开臂有触发但
sum(filesenseEntries) === 0时拒绝出报告——这才是对应本条风险的检查点。
- 证据:
-
[低] 报告未断言每臂的
enabled与arm字段- 证据:
report-filesense.mjs:36-38的一致性比较显式剔除了enabled,而记录里的arm字段(run-eval.mjs:276)从未被读取。臂身份完全依赖文件名与"开臂必有触发 / 关臂必须零触发"两个间接守卫。 - 受影响调用方/流程: 文件被重命名或错拷时,只有当触发行为恰好异常才会被发现;在这个以溯源守卫为卖点的文件里属于可补的缺口。
- 最小可行修复: 在既有守卫旁加两行——
fullAll全部filesenseConfigRequested.enabled === true && arm === 'full',offAll全部=== false && arm === 'no-filesense',否则拒绝出报告。
- 证据:
行级发现
- [benchmarks/eval/report-filesense.mjs:109] 该句把 SDD/guard/RAG/模型与"filesense 全部预算参数"都算作"已从 JSONL 校验",但 JSONL 只记录
fixture/taskSet/filesenseConfigRequested{enabled,output,writeMode},数值预算按run-eval.mjs:56-75是刻意不钉的;拆成"已校验项"与"构造性保证项"两句。 - [benchmarks/eval/run-eval.mjs:35] "实测同样预算下扫 180 条即截断"对应的是 (depth 2, maxEntries 180),而
trigger-policy.ts:184-190的prepare_create是 depth 1/180,depth 2 的档位是 250/300/500;请按各 intent 真实发出的组合重测,或改写这条理由。 - [benchmarks/eval/run-eval.mjs:176] 该类型检查无法捕获注释所述的静默归零:
context/helpers.ts:56-63已把entries/elapsedMs归一为0、truncated归一为false、candidateCount归一为0;请缩小注释声称的覆盖面,并把零条目守卫加到报告侧。 - [benchmarks/eval/report-filesense.mjs:37] 一致性比较剔除
enabled后,全程没有任何地方断言开臂enabled===true、关臂enabled===false,记录里的arm字段也从未被读;补两行断言即可闭合臂身份。
Karpathy 评审
- 假设: 最关键的隐藏假设是"深夹具会让预算闸关闸"(发现 2)。次要假设是"事件形状守卫能挡住静默降级"(发现 3)。
filesenseConfigRequested是请求值而非生效值这一点,作者已在run-eval.mjs:281-283和报告注释里主动标注,处理得当。关臂"不注入"而非"不可用"的口径差异也已显式说明并配了经验守卫。 - 简洁性: 与问题规模匹配。
file_unchanged是 deep 任务集刻意不预置files后新出现的真实需求(对照tasks.json:443-445的 flat bugfix 任务确实点名了源文件),不是投机抽象。FILESENSE_PINNED只钉两个经核实的 no-op、用环境变量断言替代钉死数值预算,是比"全钉死"更克制也更正确的选择。generate.mjs的清理逻辑按磁盘实际目录枚举而非按列表枚举,避免了删列表项后的孤儿残留,考虑周到。 - 结构质量: 报告器与
report.mjs分立而非加叙事开关,是正确的边界选择。collectEventDetail作为独立函数、harnessFailures记账而非抛出(因Agent.emit会吞掉监听器异常)都合理。未见薄 wrapper、重复 helper 或文件膨胀问题。 - 变更范围: 严格限定在
benchmarks/eval/加一条 README 与配套 workflow 测试;错误的run.ts改动已回退并已核实。新增测试与 README 改动一一对应(--arm no-filesense、--fixture deep、generate.mjs、report-filesense.mjs、pnpm --dir ... install五条断言均在 README 中存在)。无范围蔓延。 - 验证: 夹具规模数字可独立复算并全部吻合——24 features × 8 + 12 entities × 3 = 228 生成文件,加 14 个提交源文件 = 242;目录 144+36+2+3+2 = 187,
dirs+1= 188;条目 242+187+1 = 430。三处埋 bug 均可解且与"4 失败 / 11 通过"精确对应(computeTotal 2 失败、mergeLines 1、estimateEta 1;总 15 个用例)。这部分验证强度很高。缺口在发现 2、3 指出的两条未被实测支撑的主张。
缺失覆盖
checks.mjs的file_unchanged无任何测试:fixtureRoot缺失、原件/副本任一不存在、内容相同/不同四条分支目前只靠人工阅读保证,而它是三条 bugfix 任务能否被"改测试"绕过的唯一屏障。report-filesense.mjs的四条拒绝出报告守卫(夹具不一致、预算不一致、开臂零触发、关臂泄漏)是这个文件的承重逻辑,全部未被测试覆盖;仓库已有scripts/tests/的 node:test 先例,补一个针对合成 JSONL 的用例成本很低。- 若采纳发现 2 的重测建议,建议把"各 intent 真实预算下的扫描条目/是否截断"作为一次性探针结果记进 PR 或 README,使"深夹具的必要性"这条主张与其余数字一样可复算。
| console.log(`# FrontAgent 消融评测:filesense 目录导航对一次通过率的影响 | ||
|
|
||
| > **口径**:两臂唯一的差异是**是否注入 filesense 导航步骤**(\`filesenseEnabled\`)。 | ||
| > SDD、guard、RAG、模型、任务集、夹具、以及 filesense 的全部预算参数都相同且被显式钉死(已从 JSONL 校验)。 |
There was a problem hiding this comment.
该句把 SDD/guard/RAG/模型与"filesense 全部预算参数"都算作"已从 JSONL 校验",但 JSONL 只记录 fixture/taskSet/filesenseConfigRequested{enabled,output,writeMode},数值预算按 run-eval.mjs:56-75 是刻意不钉的;拆成"已校验项"与"构造性保证项"两句。
| * 而全仓一共 18 条),一次 `ls -R` 就能塞进上下文——按构造测不出按需导航的价值。 | ||
| * - `deep`:feature-sliced,242 个源文件 / 188 个目录 / 最深 5 层,且 `format.ts`、 | ||
| * `Button.tsx`、`useToggle.ts` 各有 24 份散落在不同 feature 下。实测同样预算下 | ||
| * 扫 180 条即截断(全仓 430 条),定位必须靠语义而非文件名。 |
There was a problem hiding this comment.
"实测同样预算下扫 180 条即截断"对应的是 (depth 2, maxEntries 180),而 trigger-policy.ts:184-190 的 prepare_create 是 depth 1/180,depth 2 的档位是 250/300/500;请按各 intent 真实发出的组合重测,或改写这条理由。
| ['truncated', 'boolean'], | ||
| ['candidateCount', 'number'], | ||
| ]) { | ||
| if (typeof event[field] !== expected) { |
There was a problem hiding this comment.
该类型检查无法捕获注释所述的静默归零:context/helpers.ts:56-63 已把 entries/elapsedMs 归一为 0、truncated 归一为 false、candidateCount 归一为 0;请缩小注释声称的覆盖面,并把零条目守卫加到报告侧。
| // 同进程同一组常量的回读,不是遥测到的生效值。数值预算不在其中——那些来自 | ||
| // trigger-policy 的按 intent 分档,harness 在启动时断言相关环境变量未设。 | ||
| const budgets = [...fullAll, ...offAll].map((r) => | ||
| JSON.stringify({ ...(r.filesenseConfigRequested ?? {}), enabled: undefined }), |
There was a problem hiding this comment.
一致性比较剔除 enabled 后,全程没有任何地方断言开臂 enabled===true、关臂 enabled===false,记录里的 arm 字段也从未被读;补两行断言即可闭合臂身份。
Linked Issue Or Context
Summary
The 2026-07-12 ablation cannot say anything about filesense. Its two arms toggle SDD and the guard; filesense is enabled in both. Recomputing the committed JSONL confirms it:
filesense_navigatedfires on 11/27 tasks in each arm, identically.This PR adds the instrument needed to measure it. It makes no pass-rate claim — no run is reported here.
There is no production change. Everything is under
benchmarks/eval/.A correction: #411 was my error
Mid-way through this work I saw
--arm no-filesensestill emittingfilesense_navigated: 1, concluded thatrunFrontAgentTaskwas dropping the option, filed #411 as a fourth instance of the "declared but never wired" class (#386 / #400 / #403), and added a nestedfilesense?: FilesenseConfigfield torun.ts.That was wrong.
RuntimeConfigInputalready exposesfilesenseEnabled,filesenseOutput,filesenseWriteMode,filesenseMaxEntries,filesenseMaxBytesandfilesenseTimeoutMs, andRunFrontAgentTaskOptions extends RuntimeConfigInput. Verified against a build with my change reverted:and end-to-end on the deep fixture with
run.tsat develop's version:filesense_navigatedfullno-filesenseI had passed a nested object where the API takes flat fields. The
run.tsandrun.test.tschanges are reverted and #411 is closed as invalid with the same explanation. I reached for a new seam without checking whether the existing one already reached the same place — the same failure of verification this series keeps finding in the code, this time in my own work.What this PR actually contains
1. A third arm
--arm no-filesensediffers fromfullin exactly one field. Strictly it stops the planner injecting the navigate step rather than making filesense unavailable — the tool stays registered, but the plan prompt and schema expose no filesense action, so the model cannot reach it. The report's off-arm guard verifies that empirically.The whole config surface is pinned in all three arms, not just
enabled.planner-skills.tslets a configured budget override trigger-policy's default, and "was the budget truncated" is one of the report's conclusions — leaving those toFRONTAGENT_FILESENSE_*would publish a fact about the operator's machine. The effective config is written into every record and the report refuses to run if the arms disagree.2. Event payloads in the JSONL
The harness stored only event counts — which is exactly why the 2026-07-31 report could not classify its own two
validation_failedevents and had to withdraw the classification.Now also stored:
filesense_navigatedpayloads (intent / paths / entries / truncated / candidateCount),validation_failedpayloads (which checks failed),rollback_started|completed|failed, plus per-taskfilesenseEntriesandfilesenseTruncated. A shape mismatch against core's event union aborts the arm before the record is written — writing it first would let resume-dedup skip the poisoned row on a re-run and fix the bad data in place.Note
validation_failedandrollback_failedhave no emit site until #402 lands, so those arrays are empty on develop. An empty array means not wired, not zero interceptions; this is stated at the collection site.3. A fixture where the budget gate actually closes
Measured under filesense's own
prepare_createbudget (src, depth 2, maxEntries 180):fixture(flat)fixture-deepOn the flat fixture filesense supplies essentially the entire repository — one
ls -Ris the whole project, so budgeted navigation and a plain command are indistinguishable by construction, and any result there is a fact about the fixture, not the capability.fixture-deepis feature-sliced: 242 files / 188 directories / 430 entries at depth 5, withformat.ts,Button.tsxanduseToggle.tseach duplicated across 24 features so filenames alone cannot identify the target. Its 12 tasks deliberately omit thefilespre-seed the flat tasks use — pre-seeding hands the agent the exact path and erases the thing under test.The 228 noise modules come from a deterministic generator (which measures and reports the tree it produced, rather than repeating a hardcoded number) and are gitignored; only the 20 committed files are in the diff. Three carry planted bugs, guarded by a new
file_unchangedcheck kind — without it a bugfix task could be passed by editing the test, and the two arms would not take that shortcut symmetrically.4.
report-filesense.mjsReports full-vs-off, separating the triggered subset — filesense fires on ~40% of tasks by design (
trigger-policy.tsskips known single-file work), so a whole-set average dilutes the effect. It refuses to emit a report when the arms disagree on fixture, task set, or budget; when the on arm never triggered; or when the off arm triggered at all.It states the read discipline explicitly: same-arm "used vs not used" is confounded by task type (create tasks always trigger and are the hardest); only the cross-arm same-task comparison is a valid control.
Impact Scope
benchmarks/eval/only — harness, new fixture, new task set, new check kind, new report generator.GitNexus Impact Summary
packages/runtime-node/src/run.tschange was reverted.packages/**orapps/**is modified, so there are no downstream execution flows to assess.benchmarks/eval/run-eval.mjsreads core'sAgentEventshape at runtime; that coupling is unchecked by the type system and is now guarded explicitly.pnpm quality:precommitexit 0.Verification
pnpm quality:precommit— exit 0.fixture-deepagainst develop'srun.ts; the arm switch and the pinned budgets are confirmed from recorded telemetry, not by inspection.fixture-deep:tsc --noEmitclean;vitest run→ 4 failures, all in the three planted-bug modules, 11 passing.mcp-filesense'snavigate.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