返回 AICAP-180
B18 · Day 176OSS 收口 + 英文 + 全局 SOTA 复核

抽独立公开 repo 工程化

B18 是整个 AICAP-180 的收口批次。回顾整条曲线:

阶段: B18 · OSS 收口 + 英文 + 全局 SOTA 复核(Day 171-180) 标签: #monorepo-extraction #vitest #github-actions #deterministic-tests

今日导引(由浅入深)

B18 是整个 AICAP-180 的收口批次。回顾整条曲线:

  • B1→B17 把「评测严谨性」(κ harness、partial-credit、bootstrap CI、A/B、outcome 仪表盘)一砖一瓦造进了主仓 monorepo;
  • 但这些资产至今只活在一个私有大仓里——对外不可链接、不可独立验证,这正是 B18「OSS 收口」要解决的;
  • 今天要做的就是把其中最有外部价值的闭包——agent-evals——从 monorepo 切出来成为独立公开 repo,配上自己的 CI;
  • 目标是让「423 测试绿」里属于评测的那一部分变成一个 GitHub 上独立可点开、可 fork、可 npm test 复跑的工件。

承上启下:昨天(Day 175)写的是这个包的英文 README.en.md 草稿与 κ-harness 数据流图,今天是把文字承诺兑现成可运行的 repo;明天(Day 177)则转向真实 OSS PR 实现。今日最小可判定产出:一个新 repo,CI 跑 npm test 绿,passing 测试数 ≥ 18。

1. 机理精读

monorepo → 独立 repo 的本质是依赖闭包的切割。

  • 一个文件能否被「拎出去」,取决于它的传递依赖(transitive imports)是否构成一个不引回主仓其余部分的最小闭包。
  • 如果 cohensKappa.ts 只 import 了纯标准库 / 同目录的 stats.ts,那它就是可切的。
  • 如果它 import 了 ../aml/typology 或某个带 React/Vite 的模块,就会把整棵依赖树拖出来,闭包不再「最小」。
  • 识别最小闭包是这次工程化的第一步,也是最关键的一步——下面第 2 节就是逐文件画这张依赖图。

为什么 agent-evals 是天然的切割候选?

  • B1-B17 在设计这批评测代码时就刻意走了「纯 TS + 确定性 + RNG 可注入」的路线(见 stats.ts 头注:Pure + deterministic (RNG injectable) — no API key)。
  • 这意味着它们没有网络、没有时钟、没有真实模型 key 依赖——这正是可独立分发的前提。
  • 反例:一个需要 DEEPSEEK_API_KEY 才能跑测试的包,在别人(或自己的公开 CI)的环境里是跑不绿的,等于不可分发。

独立包的最小骨架。 seed 明确列出 agent-evals 独立包需自带:

  • cohensKappa.ts(judge-human 一致性)+ tasks.ts(任务集);
  • labels schema(标注数据的 JSON schema)——定义 agent-evals/labels.json 里手标数据的形状(哪个 taskId、人类给的 label、判官给的 label),是 κ 计算的输入契约;
  • 各自的 tsconfig / vitest 配置 + CI。
  • 一句话:切出去的不只是源码,还有一整套「让它自洽运行」的脚手架。

关键权衡:纯确定性 vs 真实信号。

  • 切出去的包能在 CI 里跑绿,恰恰因为它包含真模型回合——tasks.ts 里每个任务的 codeCheck 是确定性正则/JSON 断言,不需要 LLM。
  • 代价是:CI 绿只证明「评测脚手架本身正确」,不证明「某个模型在这套任务上表现如何」。
  • 后者要 pnpm eval:agent(需 key)才产出,那部分绝不能进 CI,否则 CI 不可重放。
  • 这与本仓 dsdb-lab「浏览器内确定性教学装置」是同一条工程哲学:把可确定性测试的部分和需要外部资源的部分严格分层。

与相邻概念的边界。

  • 今天做的是「打包/分发工程」,不是「写新评测逻辑」——逻辑早在 B1-B17 写完且已测。
  • 也不是「发布 npm 包」(那是另一步,涉及版本号 / registry / 语义化版本)。
  • 今天的判定边界就一条:新 repo 的 npm test 在 GitHub Actions 上绿,且 passing ≥ 18。

2. 代码走读:可抽出的纯 TS 闭包

seed 点名 stats.tssummarizeLatency p50/p95/p99)、abCompare.tsdashboard.ts 均纯 TS 可测,构成可抽出闭包。逐一走读其真实符号与依赖边界(已 Read 三个文件):

  • src/agent/eval/stats.ts — 零外部 import。导出 mean / sd / percentile / pairedBootstrap(返回 PairedResult{deltaMean, ci95, n, iters})/ requiredNForDelta(正态近似的功效分析 n = ((z_{α/2}+z_β)·sd/|δ|)²zAlpha=1.959963985zBeta=0.841621234)/ summarizeLatency(返回 {n,p50,p95,p99,mean})。无依赖 → 闭包根节点,第一个该拷的文件。
  • src/agent/eval/abCompare.ts — 只 import { pairedBootstrap } from './stats'。导出 abCompare(a, b),内部 passMapperTask[].judge.label==='pass' 折成 0/1,按 taskId 对齐(ids = [...ma.keys()].filter(id => mb.has(id)).sort()),再 pairedBootstrapdeltaMean/ci95,并统计 wins/losses/ties依赖只回到 stats.ts,仍在闭包内。
  • src/agent/eval/cohensKappa.ts — 零外部 import(自带 percentile)。导出 cohensKappa(a, b)(返回 {kappa, po, pe},退化边界 pe===1po===1?1:0)与 cohensKappaWithCI(百分位 bootstrap,B=2000,RNG 可注入)。头注直接写明用途:calibrate an LLM-judge against human gold labels,且 small N => wide CI — the reason P1 wants N>=50闭包内。
  • src/agent/eval/dashboard.ts — 零外部 import。导出 buildDashboard(reports) 聚合成 DashboardSnapshot{runs, totalTasks, nsm, leaves},NSM=task autonomous completion,叶子含 partial-credit / code-check pass / unknown rate / cost(USD/run) / judge-human kappa。注释强调「fs reader lives in scripts/build-dashboard.ts so this stays bundler-safe」——纯函数与 IO 已分层,切包时 fs 部分留主仓即可。
  • src/agent/eval/tasks.tsimport type { EvalTask } from './agentEval'(仅类型导入)。导出 EVAL_TASKS[...AML_DETECT, ...AML_RESTRAINT, ...AML_COMPLIANCE, ...AGENT_CORE],约 30 个任务)与 tasksByCategory()。每个 codeCheck 是确定性断言。注意闭包边界:要么把 agentEval.tsEvalTask 类型一并拷入,要么在独立包里就地定义该接口——这是切割时唯一需要处理的类型耦合。

走读结论与依赖图:

  • stats.ts —— 闭包根(零依赖)。
  • abCompare.tsstats.tspairedBootstrap)。
  • cohensKappa.ts —— 自包含(自带 percentile)。
  • dashboard.ts —— 自包含(纯聚合,fs reader 留在 scripts/build-dashboard.ts)。
  • tasks.tsagentEval.tsimport type)—— 唯一需手动处理的耦合。
  • 结论:五文件构成一个干净的纯函数闭包,切割成本只有「就地定义 EvalTask 类型」一处。

3. 今日实战

  1. 新建独立 repo(如 agent-evals),git init,配 package.json"test": "vitest run")。
  2. 画依赖图:从 stats.ts(闭包根,零依赖)出发,确认 abCompare → statscohensKappa(自包含)、dashboard(自包含)、tasks → agentEval(仅类型) 的边,标出唯一需处理的耦合点。
  3. 拷入纯 TS 闭包:src/agent/eval/ 下的 stats.ts / abCompare.ts / cohensKappa.ts / dashboard.ts / tasks.ts连同它们各自的 *.test.ts
  4. 处理 tasks.tsEvalTask 类型:把 agentEval.ts 里的 EvalTask 接口就地拷成一个最小 types.ts(只留打包需要的字段),避免拖入整个 agentEval 运行时。
  5. 加 labels schema:定义 labels.json 的 JSON schema(taskId / humanLabel / judgeLabel),供 cohensKappa 消费。
  6. tsconfig.json + vitest.config.ts(strict、ESM、node 环境)。
  7. .github/workflows/ci.ymlnpm ci && npm test不注入任何模型 key,确保 CI 纯确定性可重放。
  8. push 到 GitHub,确认 Actions 绿,统计 passing 测试数,与主仓 423 测试中归属 eval 的子集对齐。

4. 今日实测 / 产出

  • 新 repo CI 绿 + 测试通过数外部动作 + 待 CI(里程碑要求 ≥18 passing)。seed 状态为「外部动作 + 待 CI → 将产出新 repo CI 绿 + 测试通过数」,未完成,不升级为已完成
  • 本仓侧已有(已落地、可立即引用的硬资产):stats.tssummarizeLatency(p50/p95/p99)、abCompare.tsdashboard.ts 均纯 TS 可测,构成可抽出闭包;主仓 423 测试绿。
  • Block 里程碑参照:agent-evals 抽独立公开 repo = 待 CI(目标 ≥18 tests 绿,纯 TS 闭包已就绪:cohensKappa.ts / tasks.ts / stats.ts / abCompare.ts)。

5. 常见误区 / 陷阱

  • 把需 key 的测试塞进 CI:一旦 CI 跑真模型回合,结果随机、随网络波动,CI 不再可重放、PR 红绿失去意义。坚持「纯确定性单测进 CI,真模型 eval 留本地/手动」。
  • 闭包没切干净:拷 tasks.ts 时漏处理 EvalTask 类型导入,编译期就会把 agentEval.ts(及其传递依赖)拖进来,独立包失去「最小」属性。先画依赖图再动手。
  • README 数字与 CI 脱节:README 里引用的 423 / V4-Pro 89.7% 等数字属于主仓,独立 repo 的 CI 只覆盖评测脚手架本身。
    • 不要在独立 repo 里声称「423 tests 绿」,那是主仓数;独立 repo 只能声称它自己的 passing 数(≥18 目标)。
  • 把抽包当成「已交付」:seed 明确这是外部动作 + 待 CI,没合 CI 绿之前不算里程碑达成。
  • 拷源码忘拷测试:只拷 stats.ts 不拷 stats.test.ts,独立 repo 的 CI 就成了空跑——passing 数上不去,也证明不了脚手架正确。

6. 学习资源(每条带 YYYY-MM)

  • Vitest 官方文档(migration / config / CI 集成),2026-01 当周版本需复验。
  • GitHub Actions for Node.js(actions/setup-node + npm ci)官方指南,2026。
  • TypeScript Project References / monorepo 拆分实践(tsconfig references),TS 官方 handbook,2026。
  • 本仓 monorepo 结构 + vitest 测试体系(seed 依据)。
  • 本仓 src/agent/eval/stats.ts / abCompare.ts / cohensKappa.ts / dashboard.ts / tasks.ts(待抽出的纯 TS 闭包,本日已走读)。
  • Anthropic「Demystifying evals」(2026-01)——评测脚手架可复现性的方法论背书(被 B17/Day 175 引用,本日承接其「可独立验证」精神)。

SOTA检查 (2026-06 更新)

  • vitest + GitHub Actions 为 2026 TS 项目标准 CI 栈,仍 SOTA。vitest 当周版本号需在 npm 复验。
  • 当前主流栈:vitest(测试 runner)+ tsc --noEmit(类型门)+ GitHub Actions(CI)+ Conventional Commits / changesets(版本管理),仍是 2026 TS 库的默认组合。
  • 过时黑名单
    • 避免引入需真模型 key 的测试进 CI(会使 CI 不可重放)——保持纯确定性单测,与本仓 dsdb-lab「浏览器内确定性教学装置」同理;
    • 避免用已进入维护模式的旧测试框架配置(如纯 mocha+chai 手搓 ESM)做新库脚手架。
  • 下次复查点
    • 抽包后 CI 首次跑绿时,复核 passing 数是否 ≥18;
    • npm 上 vitest / @vitest/* 当周版本号;
    • GitHub Actions runner 镜像 node 版本。

衔接

  • 昨天:Day 175 — 英文 writeup 体例(problem→repro→fix→eval,写出 agent-evals README.en.md + κ-harness 数据流图)
  • 今天:把 agent-evals 纯 TS 闭包切成独立公开 repo,配确定性 CI,目标 ≥18 passing
  • 明天:Day 177 — 真实 OSS PR 实现(针对 D174 选定 issue 设计 fix + 单测,push fork branch)