niceeval 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +21 -12
  2. package/README.zh.md +8 -9
  3. package/docs-site/.mintignore +13 -0
  4. package/docs-site/AGENTS.md +46 -0
  5. package/docs-site/concepts/adapter.mdx +287 -0
  6. package/docs-site/concepts/assert.mdx +243 -0
  7. package/docs-site/concepts/drive.mdx +118 -0
  8. package/docs-site/concepts/evals.mdx +108 -0
  9. package/docs-site/concepts/experiment.mdx +37 -0
  10. package/docs-site/concepts/hitl.mdx +83 -0
  11. package/docs-site/concepts/judge.mdx +129 -0
  12. package/docs-site/concepts/overview.mdx +98 -0
  13. package/docs-site/concepts/tier.mdx +42 -0
  14. package/docs-site/docs-ref/00-index.md +23 -0
  15. package/docs-site/docs-ref/01-page-types.md +43 -0
  16. package/docs-site/docs-ref/03-style-rules.md +45 -0
  17. package/docs-site/docs-ref/04-code-examples.md +37 -0
  18. package/docs-site/docs-ref/05-dx-failure-paths.md +45 -0
  19. package/docs-site/docs-ref/06-checklists.md +53 -0
  20. package/docs-site/docs.json +256 -0
  21. package/docs-site/example/ai-agent-application.mdx +148 -0
  22. package/docs-site/example/claude-code-codex-plugin.mdx +162 -0
  23. package/docs-site/example/claude-code-codex-skill.mdx +147 -0
  24. package/docs-site/example/showcase.mdx +39 -0
  25. package/docs-site/example/tier1-ai-sdk-v7.mdx +136 -0
  26. package/docs-site/example/tier1-claude-sdk.mdx +119 -0
  27. package/docs-site/example/tier1-codex-sdk.mdx +111 -0
  28. package/docs-site/example/tier1-langgraph.mdx +119 -0
  29. package/docs-site/example/tier1-pi-sdk.mdx +119 -0
  30. package/docs-site/favicon.svg +5 -0
  31. package/docs-site/github-diff.css +135 -0
  32. package/docs-site/github-diff.js +11 -0
  33. package/docs-site/guides/authoring.mdx +129 -0
  34. package/docs-site/guides/ci-integration.mdx +97 -0
  35. package/docs-site/guides/connect-otel.mdx +209 -0
  36. package/docs-site/guides/connect-your-agent.mdx +221 -0
  37. package/docs-site/guides/dataset-fanout.mdx +98 -0
  38. package/docs-site/guides/experiments.mdx +71 -0
  39. package/docs-site/guides/fixtures.mdx +147 -0
  40. package/docs-site/guides/reporters.mdx +113 -0
  41. package/docs-site/guides/runner.mdx +79 -0
  42. package/docs-site/guides/sandbox-agent.mdx +138 -0
  43. package/docs-site/guides/sandbox-backends.mdx +64 -0
  44. package/docs-site/guides/scoring-guide.mdx +92 -0
  45. package/docs-site/guides/viewing-results.mdx +195 -0
  46. package/docs-site/guides/write-experiment.mdx +81 -0
  47. package/docs-site/guides/write-send.mdx +347 -0
  48. package/docs-site/images/adapter-tiers-zh.svg +60 -0
  49. package/docs-site/images/agent-turn-roundtrip-en.svg +62 -0
  50. package/docs-site/images/agent-turn-roundtrip-zh.svg +77 -0
  51. package/docs-site/images/hitl-handshake-zh.svg +89 -0
  52. package/docs-site/images/logo.svg +6 -0
  53. package/docs-site/index.mdx +181 -0
  54. package/docs-site/introduction.mdx +141 -0
  55. package/docs-site/quickstart.mdx +136 -0
  56. package/docs-site/reference/builtin-agents.mdx +161 -0
  57. package/docs-site/reference/capabilities.mdx +76 -0
  58. package/docs-site/reference/cli.mdx +120 -0
  59. package/docs-site/reference/define-agent.mdx +168 -0
  60. package/docs-site/reference/define-config.mdx +41 -0
  61. package/docs-site/reference/define-eval.mdx +160 -0
  62. package/docs-site/reference/events.mdx +131 -0
  63. package/docs-site/reference/expect.mdx +112 -0
  64. package/docs-site/tracker.js +8 -0
  65. package/docs-site/zh/concepts/adapter.mdx +286 -0
  66. package/docs-site/zh/concepts/assert.mdx +243 -0
  67. package/docs-site/zh/concepts/drive.mdx +118 -0
  68. package/docs-site/zh/concepts/evals.mdx +124 -0
  69. package/docs-site/zh/concepts/experiment.mdx +37 -0
  70. package/docs-site/zh/concepts/hitl.mdx +83 -0
  71. package/docs-site/zh/concepts/judge.mdx +130 -0
  72. package/docs-site/zh/concepts/overview.mdx +109 -0
  73. package/docs-site/zh/concepts/tier.mdx +44 -0
  74. package/docs-site/zh/example/ai-agent-application.mdx +152 -0
  75. package/docs-site/zh/example/claude-code-codex-plugin.mdx +163 -0
  76. package/docs-site/zh/example/claude-code-codex-skill.mdx +147 -0
  77. package/docs-site/zh/example/showcase.mdx +39 -0
  78. package/docs-site/zh/example/tier1-ai-sdk-v7.mdx +140 -0
  79. package/docs-site/zh/example/tier1-claude-sdk.mdx +122 -0
  80. package/docs-site/zh/example/tier1-codex-sdk.mdx +117 -0
  81. package/docs-site/zh/example/tier1-langgraph.mdx +123 -0
  82. package/docs-site/zh/example/tier1-pi-sdk.mdx +122 -0
  83. package/docs-site/zh/guides/agent-feedback-loop.mdx +79 -0
  84. package/docs-site/zh/guides/authoring.mdx +129 -0
  85. package/docs-site/zh/guides/ci-integration.mdx +97 -0
  86. package/docs-site/zh/guides/connect-otel.mdx +208 -0
  87. package/docs-site/zh/guides/connect-your-agent.mdx +226 -0
  88. package/docs-site/zh/guides/dataset-fanout.mdx +83 -0
  89. package/docs-site/zh/guides/experiments.mdx +72 -0
  90. package/docs-site/zh/guides/fixtures.mdx +147 -0
  91. package/docs-site/zh/guides/official-adapters.mdx +132 -0
  92. package/docs-site/zh/guides/reporters.mdx +113 -0
  93. package/docs-site/zh/guides/runner.mdx +82 -0
  94. package/docs-site/zh/guides/sandbox-agent.mdx +139 -0
  95. package/docs-site/zh/guides/sandbox-backends.mdx +75 -0
  96. package/docs-site/zh/guides/scoring-guide.mdx +92 -0
  97. package/docs-site/zh/guides/viewing-results.mdx +195 -0
  98. package/docs-site/zh/guides/write-experiment.mdx +101 -0
  99. package/docs-site/zh/guides/write-send.mdx +353 -0
  100. package/docs-site/zh/index.mdx +180 -0
  101. package/docs-site/zh/introduction.mdx +140 -0
  102. package/docs-site/zh/quickstart.mdx +137 -0
  103. package/docs-site/zh/reference/builtin-agents.mdx +364 -0
  104. package/docs-site/zh/reference/capabilities.mdx +76 -0
  105. package/docs-site/zh/reference/cli.mdx +140 -0
  106. package/docs-site/zh/reference/define-agent.mdx +461 -0
  107. package/docs-site/zh/reference/define-config.mdx +107 -0
  108. package/docs-site/zh/reference/define-eval.mdx +674 -0
  109. package/docs-site/zh/reference/events.mdx +252 -0
  110. package/docs-site/zh/reference/expect.mdx +208 -0
  111. package/package.json +3 -2
  112. package/src/agents/bub.ts +1 -1
  113. package/src/agents/claude-code.ts +8 -5
  114. package/src/agents/codex.ts +9 -5
  115. package/src/agents/sdk-streams.test.ts +4 -4
  116. package/src/agents/sdk-streams.ts +14 -9
  117. package/src/agents/types.ts +40 -1
  118. package/src/agents/ui-message-stream.ts +1 -0
  119. package/src/cli.ts +40 -10
  120. package/src/context/types.ts +140 -0
  121. package/src/expect/index.ts +15 -8
  122. package/src/i18n/en.ts +20 -6
  123. package/src/i18n/zh-CN.ts +20 -6
  124. package/src/o11y/parsers/bub.test.ts +71 -0
  125. package/src/o11y/parsers/bub.ts +5 -0
  126. package/src/o11y/types.ts +27 -2
  127. package/src/runner/attempt.ts +2 -1
  128. package/src/runner/reporters/artifacts.ts +11 -3
  129. package/src/runner/reporters/live.ts +45 -8
  130. package/src/runner/run.test.ts +50 -0
  131. package/src/runner/run.ts +135 -41
  132. package/src/runner/types.ts +55 -2
  133. package/src/sandbox/types.ts +18 -0
  134. package/src/scoring/types.ts +5 -0
  135. package/src/shared/types.ts +3 -0
  136. package/src/util.test.ts +26 -1
  137. package/src/util.ts +16 -0
  138. package/src/view/app/App.tsx +2 -2
  139. package/src/view/app/components/AttemptModal.tsx +2 -0
  140. package/src/view/app/components/CopyControls.tsx +87 -28
  141. package/src/view/app/i18n.ts +3 -3
  142. package/src/view/client-dist/app.css +1 -1
  143. package/src/view/client-dist/app.js +18 -18
  144. package/docs/README.md +0 -120
  145. package/docs/adapters/README.md +0 -60
  146. package/docs/adapters/authoring.md +0 -179
  147. package/docs/adapters/coding-agent-skills-plugins.md +0 -324
  148. package/docs/adapters/collection.md +0 -128
  149. package/docs/adapters/contract.md +0 -264
  150. package/docs/adapters/reference/agent-eval.md +0 -215
  151. package/docs/adapters/reference/agent-loop-apis.md +0 -69
  152. package/docs/adapters/reference/claude-code-otel-telemetry.md +0 -100
  153. package/docs/adapters/reference/eve-protocol.md +0 -127
  154. package/docs/adapters/reference/otel-genai.md +0 -107
  155. package/docs/adapters/reference/otel-instrumentation.md +0 -65
  156. package/docs/adapters/targets.md +0 -99
  157. package/docs/architecture.md +0 -140
  158. package/docs/assertions.md +0 -387
  159. package/docs/capabilities-by-construction.md +0 -48
  160. package/docs/cli.md +0 -173
  161. package/docs/concepts.md +0 -106
  162. package/docs/e2e-ci.md +0 -332
  163. package/docs/eval-authoring.md +0 -319
  164. package/docs/experiments.md +0 -157
  165. package/docs/getting-started.md +0 -224
  166. package/docs/multi-agent.md +0 -145
  167. package/docs/observability.md +0 -337
  168. package/docs/origin-integration.md +0 -195
  169. package/docs/references.md +0 -35
  170. package/docs/reports.md +0 -551
  171. package/docs/results-format.md +0 -228
  172. package/docs/results-lib.md +0 -191
  173. package/docs/runner.md +0 -104
  174. package/docs/sandbox.md +0 -276
  175. package/docs/scoring.md +0 -119
  176. package/docs/source-map.md +0 -126
  177. package/docs/tier-sync.md +0 -193
  178. package/docs/view.md +0 -194
  179. package/docs/vision.md +0 -88
@@ -143,10 +143,23 @@ export interface AgentSession {
143
143
  }
144
144
 
145
145
  export interface AgentContext {
146
+ /**
147
+ * 软取消信号:合并了 attempt 超时、run 级中断(用户 Ctrl+C)与 eval 自身的中断请求
148
+ * (见 src/runner/attempt.ts)。adapter 可以选择性检查它(或直接传给 `fetch`)以提前
149
+ * 优雅退出,但这不是唯一的硬边界——即便 adapter 完全忽略它,运行器也会用
150
+ * `Effect.timeoutTo` 兜底强制收尾(停沙箱容器)。
151
+ */
146
152
  readonly signal: AbortSignal;
153
+ /** 本次 attempt 用的模型名,透传自 experiment 的 `model` 字段。sandbox 型 agent 通常在 setup 里用它写配置,remote 型通常在 send 里用它选模型。 */
147
154
  readonly model?: string;
148
155
  /** 模型推理努力程度;归属同 model——实验决定,省略时不覆盖 agent 原生默认。 */
149
156
  readonly reasoningEffort?: string;
157
+ /**
158
+ * experiment 的 `flags` 字段原样透传,内容和结构完全由 experiment 作者自定义
159
+ * (如 `{ webResearch: true }`、`{ systemPrompt: "..." }`)。adapter 按自己的约定
160
+ * 读取其中的字段;框架本身不解释、不校验它的内容。与 CLI 解析出的同名 `flags`
161
+ * (跑法层面的 --timeout/--budget 等)是两个不相关的概念。
162
+ */
150
163
  readonly flags: Readonly<Record<string, unknown>>;
151
164
  /** 仅沙箱型 agent 有(运行器按 --sandbox 备好)。 */
152
165
  readonly sandbox: Sandbox;
@@ -160,6 +173,12 @@ export interface AgentContext {
160
173
  * (defineConfig({ telemetry: { port } }))固定的,不从这里传。
161
174
  */
162
175
  readonly telemetry?: Telemetry;
176
+ /**
177
+ * 写一行进运行器自身的进度日志(落 stderr,或调用方传入的 `onProgress` 回调)。
178
+ * 超时失败时,最近若干行会并入结果的 error 信息,方便定位卡在哪一步。
179
+ * 与 `Sandbox.appendLog` 是两回事:那个是把一行写进容器自己的原生日志
180
+ * (`docker logs` 能看到),这里只是运行器的观测通道。
181
+ */
163
182
  log(msg: string): void;
164
183
  }
165
184
 
@@ -198,23 +217,43 @@ export interface Agent {
198
217
  teardown?: AgentTeardown;
199
218
  }
200
219
 
220
+ /** `defineSandboxAgent()` 的入参形状(见 src/define.ts)——`kind: "sandbox"` 由 define 固定填入,不由用户声明。 */
201
221
  export interface SandboxAgentDef {
222
+ /** agent 的显示名/标识,原样进入 `Agent.name`——不是注册表查找 key,只用于展示、结果归属与去重指纹。 */
202
223
  name: string;
203
- /** 每个沙箱一次:装 CLI、写 config.toml / 鉴权配置。 */
224
+ /**
225
+ * 每个沙箱一次(不是每轮一次):装 CLI、写 config.toml / 鉴权配置(model/base/auth 等
226
+ * 本轮内不变的东西)。运行器在沙箱备好(上传/基线/eval.setup 之后)、第一次 send 前
227
+ * 调用一次。可返回一个 cleanup 闭包,与 `teardown` 一起在 finally 里跑。
228
+ */
204
229
  setup?: AgentSetup;
205
230
  /** OTLP 导出配置:沙箱里怎么让 CLI 把 trace 发到 endpoint(env / 配置文件),从 setup 拆出。 */
206
231
  tracing?: AgentTracing;
232
+ /** 原生 span → canonical 的薄 mapper;省略走通用 heuristic。只影响瀑布图。 */
207
233
  spanMapper?: SpanMapper;
208
234
  /** 每轮一次:跑 prompt(fresh / resume)+ 解析成 events。 */
209
235
  send(input: TurnInput, ctx: AgentContext): Promise<Turn>;
236
+ /** 沙箱销毁前的清理(与 setup 返回的 cleanup 一起、都在 finally 里跑一次)。 */
210
237
  teardown?: AgentTeardown;
211
238
  }
212
239
 
240
+ /** `defineAgent()` 的入参形状(见 src/define.ts)——`kind: "remote"` 由 define 固定填入,不由用户声明。 */
213
241
  export interface RemoteAgentDef {
242
+ /** agent 的显示名/标识,原样进入 `Agent.name`——不是注册表查找 key,只用于展示、结果归属与去重指纹。 */
214
243
  name: string;
244
+ /**
245
+ * 每个 attempt 一次(remote agent 没有真实沙箱,运行器会传入一个仅含 `workdir`/`sandboxId`/
246
+ * `otlpHost`/`stop` 等元信息的 stub `Sandbox`,其余方法调用即抛错——不要在这里调用
247
+ * 文件/命令类沙箱方法)。常用于建立连接、鉴权等一次性准备。可返回一个 cleanup 闭包,
248
+ * 与 `teardown` 都在 finally 里跑。
249
+ */
215
250
  setup?: AgentSetup;
251
+ /** OTLP 导出配置:远程被测对象怎么把 trace 发到 endpoint(env-based 注入 / file-based 配置)。 */
216
252
  tracing?: AgentTracing;
253
+ /** 原生 span → canonical 的薄 mapper;省略走通用 heuristic。只影响瀑布图。 */
217
254
  spanMapper?: SpanMapper;
255
+ /** 每轮一次:把一轮 prompt 发给远程被测对象(HTTP/SDK 等),解析响应成 events。 */
218
256
  send(input: TurnInput, ctx: AgentContext): Promise<Turn>;
257
+ /** 运行结束前的清理(与 setup 返回的 cleanup 一起、都在 finally 里跑一次)。 */
219
258
  teardown?: AgentTeardown;
220
259
  }
@@ -216,6 +216,7 @@ export interface UiMessageStreamAgentOptions {
216
216
  settleMs?: number;
217
217
  /** 应用有 OTel 时的端点投递方式(拿瀑布图);事件流不依赖它。 */
218
218
  tracing?: AgentTracing;
219
+ /** 应用有 OTel 时的 span 归一函数(拿瀑布图);事件流不依赖它。 */
219
220
  spanMapper?: SpanMapper;
220
221
  }
221
222
 
package/src/cli.ts CHANGED
@@ -11,7 +11,9 @@ import { pathToFileURL } from "node:url";
11
11
  import { parseArgs as nodeParseArgs } from "node:util";
12
12
  import { discoverEvals, discoverExperiments, makeFilter } from "./runner/discover.ts";
13
13
  import { runEvals, type AgentRun } from "./runner/run.ts";
14
+ import { runWho } from "./runner/types.ts";
14
15
  import { stopAllSandboxes, liveSandboxCount } from "./sandbox/registry.ts";
16
+ import { evalLevelStats } from "./shared/outcome.ts";
15
17
  import { sandboxRecommendedConcurrency } from "./sandbox/resolve.ts";
16
18
  import { Console as ConsoleReporter } from "./runner/reporters/console.ts";
17
19
  import { JUnit } from "./runner/reporters/json.ts";
@@ -19,7 +21,7 @@ import { Live as LiveReporter, type LiveRow } from "./runner/reporters/live.ts";
19
21
  import { Artifacts as ArtifactsReporter } from "./runner/reporters/artifacts.ts";
20
22
  import { buildView, startViewServer, loadLatestResultsPerEval, IncompatibleResultsError } from "./view/index.ts";
21
23
  import { t } from "./i18n/index.ts";
22
- import { formatThrown } from "./util.ts";
24
+ import { formatThrown, upsertManagedBlock } from "./util.ts";
23
25
  import type { Config, DiscoveredExperiment, Reporter } from "./types.ts";
24
26
 
25
27
  /** `niceeval view <summary.json>` 指向版本不同的报告时:打印 npx 提示后退出,不抛堆栈。 */
@@ -182,6 +184,19 @@ async function loadConfig(cwd: string): Promise<Config> {
182
184
  return mod.default;
183
185
  }
184
186
 
187
+ // AGENTS.md 托管区块:告诉在这个项目里干活的 coding agent「niceeval 不在你的训练数据里,
188
+ // 先读随包文档,跑完读结构化结果」。init 时写入/刷新;标记之外的用户内容永不触碰。
189
+ const AGENT_RULES_BEGIN = "<!-- BEGIN:niceeval-agent-rules -->";
190
+ const AGENT_RULES_END = "<!-- END:niceeval-agent-rules -->";
191
+ const AGENT_RULES_CONTENT = [
192
+ "# niceeval is NOT in your training data",
193
+ "",
194
+ "Its APIs and conventions may differ from anything you have seen. Read the relevant",
195
+ "guide in `node_modules/niceeval/docs-site/` (English at the top level, Chinese under",
196
+ "`zh/`) before writing any eval, experiment, adapter, or niceeval config. After a run,",
197
+ "read the `summary.json` path the CLI prints for structured results.",
198
+ ].join("\n");
199
+
185
200
  async function initProject(cwd: string): Promise<void> {
186
201
  await mkdir(join(cwd, "evals"), { recursive: true });
187
202
  const configPath = join(cwd, "niceeval.config.ts");
@@ -199,6 +214,10 @@ async function initProject(cwd: string): Promise<void> {
199
214
  "utf-8",
200
215
  );
201
216
  }
217
+ const agentsPath = join(cwd, "AGENTS.md");
218
+ const existing = existsSync(agentsPath) ? await readFile(agentsPath, "utf-8") : "";
219
+ const next = upsertManagedBlock(existing, AGENT_RULES_BEGIN, AGENT_RULES_END, AGENT_RULES_CONTENT);
220
+ if (next !== existing) await writeFile(agentsPath, next, "utf-8");
202
221
  }
203
222
 
204
223
  async function openBrowser(url: string): Promise<boolean> {
@@ -289,7 +308,6 @@ async function main(): Promise<void> {
289
308
  }
290
309
 
291
310
  const agentRuns: AgentRun[] = [];
292
- let expMaxConcurrency: number | undefined;
293
311
 
294
312
  if (command === "exp") {
295
313
  if (flags.agent || flags.model) {
@@ -324,10 +342,11 @@ async function main(): Promise<void> {
324
342
  evalFilter: evalsFilterFromExperiment(exp.evals, extraPatterns),
325
343
  experimentId: exp.id,
326
344
  strict: flags.strict,
345
+ // 实验级并发上限:随 AgentRun 进调度器按实验单独限流(runner 两级信号量),
346
+ // 不再取所有选中实验的最小值钳全局——那会让一个串行实验拖慢整批基线。
347
+ maxConcurrency: exp.maxConcurrency,
327
348
  });
328
349
  }
329
- const vals = selected.map((e) => e.maxConcurrency).filter((v): v is number => v !== undefined);
330
- if (vals.length > 0) expMaxConcurrency = Math.min(...vals);
331
350
  } else {
332
351
  // 裸 run / `niceeval <eval>` 不再执行。运行配置必须来自 experiments/,
333
352
  // 这样 agent/model/flags/runs/budget 与结果聚合都有可签入的身份。
@@ -372,9 +391,9 @@ async function main(): Promise<void> {
372
391
  // TTY 模式:用 live display 替换 Console reporter,把 attempt log 路由到状态表行尾
373
392
  const liveRows: LiveRow[] = [];
374
393
  for (const agentRun of agentRuns) {
375
- const who = agentRun.model
376
- ? `${agentRun.agent.name}/${agentRun.model}`
377
- : agentRun.agent.name;
394
+ // who 必须与 attempt.ts 的进度上报同源(runWho):曾用 agent/model,同 agent 同 model
395
+ // 的实验变体(xxx 与 xxx--agents-md)会被折叠成一行,0/2 看起来像同一 eval 跑两次。
396
+ const who = runWho(agentRun);
378
397
  const matched = evals.filter((e) => agentRun.evalFilter(e.id));
379
398
  for (const evalDef of matched) {
380
399
  liveRows.push({ evalId: evalDef.id, who, total: agentRun.runs });
@@ -388,7 +407,8 @@ async function main(): Promise<void> {
388
407
  reporters.push(ConsoleReporter());
389
408
  }
390
409
  }
391
- reporters.push(ArtifactsReporter());
410
+ const artifacts = ArtifactsReporter();
411
+ reporters.push(artifacts);
392
412
  if (flags.junit) reporters.push(JUnit(flags.junit));
393
413
  reporters.push(...(config.reporters ?? []));
394
414
 
@@ -445,7 +465,6 @@ async function main(): Promise<void> {
445
465
  maxConcurrency:
446
466
  flags.maxConcurrency ??
447
467
  envNumber("NICEEVAL_MAX_CONCURRENCY") ??
448
- expMaxConcurrency ??
449
468
  config.maxConcurrency ??
450
469
  sandboxDefaultConcurrency,
451
470
  signal: ctrl.signal,
@@ -457,7 +476,18 @@ async function main(): Promise<void> {
457
476
  // 跑顺利时登记表已空,是 no-op。
458
477
  await stopAllSandboxes();
459
478
 
460
- const failedExit = summary.failed > 0 || summary.errored > 0;
479
+ // agent 反馈闭环的入口:跑完直接给出结构化结果路径,agent summary.json 与各
480
+ // attempt 的工件(events/trace/diff),不必解析人类向的流式输出。--quiet 下也输出。
481
+ if (artifacts.outputDir()) {
482
+ process.stdout.write(t("cli.resultsPath", { path: join(artifacts.outputDir(), "summary.json") }));
483
+ }
484
+
485
+ // 退出码按 eval 级判决,不按 attempt:summary.failed/errored 统计的是每次 attempt,
486
+ // 被 runs+earlyExit 重试吸收的失败(先挂一次、后来过了)不该把进程判红——否则
487
+ // 「runs 吸收单次抖动」在 CI 退出码这层永远不成立。折叠口径与报表/view 共用
488
+ // foldEvalOutcome(任一轮通过 → 该 eval 通过),粒度 experimentId|eval id。
489
+ const stats = evalLevelStats(summary.results, (r) => `${r.experimentId ?? ""}|${r.id}`);
490
+ const failedExit = stats.failed > 0 || stats.errored > 0;
461
491
  process.exit(failedExit ? 1 : 0);
462
492
  }
463
493
 
@@ -13,34 +13,59 @@ import type {
13
13
 
14
14
  /** t.send() 返回的句柄:从事件流派生便利字段 + expectOk。 */
15
15
  export interface TurnHandle {
16
+ /** 本轮的原始事件流(工具调用、消息增量等);下面的派生字段都算自它。 */
16
17
  readonly events: StreamEvent[];
18
+ /** 本轮内被调用的工具列表,从 events 派生。 */
17
19
  readonly toolCalls: readonly ToolCall[];
20
+ /** 本轮结束状态:"completed" 正常结束、"failed" 出错、"waiting" 卡在 HITL 输入请求上。 */
18
21
  readonly status: "completed" | "failed" | "waiting";
22
+ /** 本轮助手最终文本回复(events 里的消息增量拼接结果)。 */
19
23
  readonly message: string;
24
+ /** adapter 附带的结构化输出(如有),供 outputEquals / outputMatches 比对。 */
20
25
  readonly data?: unknown;
26
+ /** 本轮 token 用量与估算成本(仅上报了 usage 的 agent 才有)。 */
21
27
  readonly usage?: Usage;
22
28
  /** 上一轮若 failed 则抛(中止后续)。 */
23
29
  expectOk(): TurnHandle;
30
+ /** 断言 data 与给定值深度相等。 */
24
31
  outputEquals(value: unknown): AssertionHandle;
32
+ /** 断言 data 满足给定 schema(如 zod schema)。 */
25
33
  outputMatches(schema: unknown): AssertionHandle;
26
34
  /** 断言本轮助手回复包含 token(仅限本轮事件流,不跨轮)。 */
27
35
  messageIncludes(token: string | RegExp): AssertionHandle;
36
+ /** 断言本轮 status 为 "completed"。 */
28
37
  succeeded(): AssertionHandle;
38
+ /** 断言本轮卡在 HITL 输入请求上(status 为 "waiting")。 */
29
39
  parked(): AssertionHandle;
40
+ /** 断言本轮调用过指定名字的工具;`match` 可进一步约束入参 / 次数 / 状态。 */
30
41
  calledTool(name: string, match?: ToolMatch): AssertionHandle;
42
+ /** 断言本轮未调用指定工具(或未按 match 条件调用)。 */
31
43
  notCalledTool(name: string, match?: ToolMatch): AssertionHandle;
44
+ /** 断言本轮工具调用按给定顺序出现(允许中间夹杂其它调用)。 */
32
45
  toolOrder(names: string[]): AssertionHandle;
46
+ /** 断言本轮未调用任何工具。 */
33
47
  usedNoTools(): AssertionHandle;
48
+ /** 断言本轮工具调用总数不超过 max。 */
34
49
  maxToolCalls(max: number): AssertionHandle;
50
+ /** 断言本轮加载过指定 skill。 */
35
51
  loadedSkill(skill: string): AssertionHandle;
52
+ /** 断言本轮没有失败的工具调用 / 命令。 */
36
53
  noFailedActions(): AssertionHandle;
54
+ /** 断言本轮出现过指定类型的事件;`opts.count` 可约束出现次数。 */
37
55
  event(type: StreamEvent["type"], opts?: { count?: number }): AssertionHandle;
56
+ /** 断言本轮未出现指定类型的事件。 */
38
57
  notEvent(type: StreamEvent["type"]): AssertionHandle;
58
+ /** 断言本轮调用过指定名字的子 agent;`match` 可约束次数 / 状态 / remoteUrl。 */
39
59
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
60
+ /** 断言本轮事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
40
61
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
62
+ /** 用自定义谓词对本轮整段事件流断言;失败时用 label 说明原因。 */
41
63
  eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
64
+ /** 断言本轮 token 用量不超过 max。 */
42
65
  maxTokens(max: number): AssertionHandle;
66
+ /** 断言本轮花费(USD)不超过 usd。 */
43
67
  maxCost(usd: number): AssertionHandle;
68
+ /** 本轮可用的 judge 命名空间(t.judge.autoevals.*)。 */
44
69
  readonly judge: JudgeNamespace;
45
70
  }
46
71
 
@@ -56,31 +81,43 @@ export interface JudgeNamespace {
56
81
  autoevals: AutoevalsNamespace;
57
82
  }
58
83
 
84
+ /** 最终 diff 的只读视图(t.sandbox.diff);内容在 test() 跑完、finalize 前才落定。 */
59
85
  export interface DiffView {
86
+ /** 取某个生成 / 修改文件的最终内容;文件不在 diff 里则 undefined。 */
60
87
  get(path: string): string | undefined;
88
+ /** 整个 diff 是否为空(既没有生成/修改的文件,也没有删除的文件)。 */
61
89
  isEmpty(): boolean;
90
+ /** 正则是否命中 diff 里任意文件的路径或内容。 */
62
91
  matches(re: RegExp): boolean;
63
92
  }
64
93
 
65
94
  /** 工具匹配小语言。 */
66
95
  export interface ToolMatch {
96
+ /** 只匹配入参包含这些键值的调用(浅层包含,非深度相等)。 */
67
97
  input?: Record<string, unknown>;
98
+ /** 精确匹配调用次数,省略则只要求「至少一次」。 */
68
99
  count?: number;
100
+ /** 只匹配处于该状态的调用(如 HITL 场景下的 rejected)。 */
69
101
  status?: "completed" | "failed" | "rejected";
70
102
  }
71
103
 
104
+ /** calledSubagent 的匹配小语言,语义同 ToolMatch。 */
72
105
  export interface SubagentMatch {
106
+ /** 精确匹配调用次数,省略则只要求「至少一次」。 */
73
107
  count?: number;
74
108
  status?: "completed" | "failed";
109
+ /** 只匹配指向该远程地址的子 agent 调用。 */
75
110
  remoteUrl?: string | RegExp;
76
111
  }
77
112
 
113
+ /** requireInputRequest 的过滤条件;多个字段之间是 AND 关系。 */
78
114
  export interface InputRequestFilter {
79
115
  id?: string | RegExp;
80
116
  prompt?: string | RegExp;
81
117
  display?: string | RegExp;
82
118
  action?: string | RegExp;
83
119
  input?: Record<string, unknown>;
120
+ /** 请求的可选项 id 集合必须与此完全一致(顺序无关)。 */
84
121
  optionIds?: readonly string[];
85
122
  }
86
123
 
@@ -97,53 +134,104 @@ export interface RespondAnswer {
97
134
 
98
135
  /** eval 作者可见的受限沙箱视图:能执行命令 / 文件 IO / 读最终 diff,但不能 stop。 */
99
136
  export interface SandboxHandle {
137
+ /** 沙箱内的工作目录绝对路径。 */
100
138
  readonly workdir: string;
139
+ /** 在沙箱里执行一条命令(argv 形式,不经 shell)。装系统依赖等需要 root 时传 `{ root: true }`。 */
101
140
  runCommand(cmd: string, args?: string[], opts?: CommandOptions): Promise<CommandResult>;
141
+ /** 在沙箱里执行一段 shell 脚本(经 shell 解释,支持管道 / 重定向)。 */
102
142
  runShell(script: string, opts?: CommandOptions): Promise<CommandResult>;
143
+ /** 读沙箱内某文件此刻的文本内容(实时读,不是 diff 快照)。 */
103
144
  readFile(path: string): Promise<string>;
145
+ /** 沙箱内某路径此刻是否存在。 */
104
146
  fileExists(path: string): Promise<boolean>;
147
+ /** 按选项批量读回沙箱里的源文件,供比对 / judge 用材料。 */
105
148
  readSourceFiles(opts?: ReadSourceFilesOptions): Promise<SourceFiles>;
149
+ /** 把一组内容写进沙箱(路径相对 targetDir,默认 workdir)。 */
106
150
  writeFiles(files: Record<string, string>, targetDir?: string): Promise<void>;
151
+ /** 把一批内存中的文件上传进沙箱。 */
107
152
  uploadFiles(files: SandboxFile[], targetDir?: string): Promise<void>;
153
+ /** 把本地目录整体上传进沙箱;`opts.ignore` 排除指定路径。 */
108
154
  uploadDirectory(localDir: string, targetDir?: string, opts?: { ignore?: string[] }): Promise<void>;
155
+ /** 从沙箱下载某文件内容。 */
109
156
  downloadFile(path: string): Promise<Buffer>;
157
+ /** 把一段内容上传成沙箱里的单个文件。 */
110
158
  uploadFile(path: string, content: Buffer): Promise<void>;
159
+ /** 沙箱后端分配的实例 id,用于排查 / 关联日志。 */
111
160
  readonly sandboxId: string;
161
+ /** 相对 git 基线的最终 diff 视图(test() 跑完、finalize 前才落定)。 */
112
162
  readonly diff: DiffView;
163
+ /** 取沙箱内某文件的最终内容,占位延迟到 finalize 才真正读取;只能配合 t.check 使用。 */
113
164
  file(path: string): string;
165
+ /** 断言 diff 里某路径发生了变化(新增或修改)。 */
114
166
  fileChanged(path: string): AssertionHandle;
167
+ /** 断言 diff 里某路径被删除。 */
115
168
  fileDeleted(path: string): AssertionHandle;
169
+ /** 断言 diff 的路径与内容都不匹配给定正则(常用来否定式检查不该出现的改动)。 */
116
170
  notInDiff(re: RegExp): AssertionHandle;
171
+ /** 断言沙箱里执行过的命令都没有失败退出。 */
117
172
  noFailedShellCommands(): AssertionHandle;
118
173
  }
119
174
 
175
+ /**
176
+ * `t.newSession()` 返回的独立会话句柄:另开一路对话,发送与断言都与默认会话及其它
177
+ * `newSession()` 会话相互隔离(各自的 events / usage 互不叠加)。下面的断言方法都按
178
+ * 「这个会话到目前为止的累计事件流」评估(跨该会话所有轮次),不是仅最后一轮——
179
+ * 只看最后一轮要用 `send()` 返回的 TurnHandle。
180
+ */
120
181
  export interface SessionHandle {
182
+ /** 在这个会话里发一条消息,返回该轮的 TurnHandle。 */
121
183
  send(text: string): Promise<TurnHandle>;
184
+ /** 在这个会话里发一条带文件的消息,语义同 TestContext.sendFile。 */
122
185
  sendFile(path: string, text?: string): Promise<TurnHandle>;
186
+ /** 取这个会话里等待中的 HITL 输入请求;拿不到就抛,语义同 TestContext.requireInputRequest。 */
123
187
  requireInputRequest(filter?: InputRequestFilter): InputRequest;
188
+ /** 回答这个会话里等待中的输入请求,返回续接的 TurnHandle。 */
124
189
  respond(...responses: (string | RespondAnswer)[]): Promise<TurnHandle>;
190
+ /** 用同一个 optionId 批量回答这个会话里全部等待中的输入请求。 */
125
191
  respondAll(optionId: string): Promise<TurnHandle>;
192
+ /** 这个会话最近一轮的助手回复文本。 */
126
193
  readonly reply: string;
194
+ /** adapter 侧的会话 id(有会话概念的 agent 才有)。 */
127
195
  readonly sessionId: string | undefined;
196
+ /** 这个会话累计的事件流(跨该会话内所有轮次,不含其它会话)。 */
128
197
  readonly events: readonly StreamEvent[];
198
+ /** 断言这个会话累计状态为 "completed"(跨全部轮次)。 */
129
199
  succeeded(): AssertionHandle;
200
+ /** 断言这个会话当前卡在 HITL 输入请求上。 */
130
201
  parked(): AssertionHandle;
202
+ /** 断言这个会话累计的助手回复包含 token(跨全部轮次)。 */
131
203
  messageIncludes(token: string | RegExp): AssertionHandle;
204
+ /** 断言这个会话累计调用过指定名字的工具;match 可约束入参 / 次数 / 状态。 */
132
205
  calledTool(name: string, match?: ToolMatch): AssertionHandle;
206
+ /** 断言这个会话累计未调用指定工具(或未按 match 条件调用)。 */
133
207
  notCalledTool(name: string, match?: ToolMatch): AssertionHandle;
208
+ /** 断言这个会话累计工具调用按给定顺序出现。 */
134
209
  toolOrder(names: string[]): AssertionHandle;
210
+ /** 断言这个会话至今未调用任何工具。 */
135
211
  usedNoTools(): AssertionHandle;
212
+ /** 断言这个会话累计工具调用总数不超过 max。 */
136
213
  maxToolCalls(max: number): AssertionHandle;
214
+ /** 断言这个会话累计加载过指定 skill。 */
137
215
  loadedSkill(skill: string): AssertionHandle;
216
+ /** 断言这个会话累计没有失败的工具调用 / 命令。 */
138
217
  noFailedActions(): AssertionHandle;
218
+ /** 断言这个会话累计出现过指定类型的事件;opts.count 可约束次数。 */
139
219
  event(type: StreamEvent["type"], opts?: { count?: number }): AssertionHandle;
220
+ /** 断言这个会话累计未出现指定类型的事件。 */
140
221
  notEvent(type: StreamEvent["type"]): AssertionHandle;
222
+ /** 断言这个会话累计调用过指定名字的子 agent;match 可约束次数 / 状态 / remoteUrl。 */
141
223
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
224
+ /** 断言这个会话累计事件按给定类型顺序出现。 */
142
225
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
226
+ /** 用自定义谓词对这个会话累计的整段事件流断言。 */
143
227
  eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
228
+ /** 断言这个会话累计 token 用量不超过 max。 */
144
229
  maxTokens(max: number): AssertionHandle;
230
+ /** 断言这个会话累计花费(USD)不超过 usd。 */
145
231
  maxCost(usd: number): AssertionHandle;
232
+ /** 这个会话累计的 token 用量与估算成本。 */
146
233
  readonly usage: Usage;
234
+ /** 这个会话可用的 judge 命名空间。 */
147
235
  readonly judge: JudgeNamespace;
148
236
  }
149
237
 
@@ -153,27 +241,59 @@ export interface SessionHandle {
153
241
  */
154
242
  export interface TestContext {
155
243
  // 会话
244
+ /** 向默认会话发一条消息,返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。 */
156
245
  send(text: string): Promise<TurnHandle>;
157
246
  /** 发一条带文件(图片等多模态输入)的消息。`path` 相对项目根;读出后 base64 随 TurnInput.files 交给 adapter。 */
158
247
  sendFile(path: string, text?: string): Promise<TurnHandle>;
248
+ /** 取默认会话里等待中的 HITL 输入请求;不传 filter 要求恰好一条,拿不到就抛。 */
159
249
  requireInputRequest(filter?: InputRequestFilter): InputRequest;
250
+ /**
251
+ * 回答默认会话里等待中的输入请求,返回续接的 TurnHandle。字符串形式按顺序对应各请求;
252
+ * 多个请求并停、需要指名回答哪一条时用 RespondAnswer 对象形式(见其类型注释)。
253
+ */
160
254
  respond(...responses: (string | RespondAnswer)[]): Promise<TurnHandle>;
255
+ /** 用同一个 optionId 批量回答默认会话里全部等待中的输入请求。 */
161
256
  respondAll(optionId: string): Promise<TurnHandle>;
257
+ /** 默认会话最近一轮的助手回复文本。 */
162
258
  readonly reply: string;
259
+ /** adapter 侧的默认会话 id(有会话概念的 agent 才有)。 */
163
260
  readonly sessionId: string | undefined;
261
+ /** 默认会话累计的事件流(跨该会话所有轮次)。 */
164
262
  readonly events: readonly StreamEvent[];
263
+ /**
264
+ * 另开一路独立会话,返回它的 SessionHandle——不是 void。多会话隔离的关键入口:
265
+ * 新会话的发送与断言都与默认会话及其它 newSession() 会话相互隔离,常用于「多用户
266
+ * 并行对话」「一条主线 + 一条旁支」这类场景。
267
+ */
165
268
  newSession(): SessionHandle;
166
269
 
167
270
  // 运行上下文
271
+ /** 本次 attempt 的中止信号;超时 / 早停 / 用户 Ctrl-C 时触发,传给 adapter 的长耗时调用做取消。 */
168
272
  readonly signal: AbortSignal;
273
+ /** 本次 attempt 使用的模型名(由 experiment/CLI flag 决定);省略即 agent 原生默认,不代表「无模型」。 */
169
274
  readonly model?: string;
275
+ /** 本次 attempt 的推理努力程度(如 "low"/"medium"/"high",取值由 adapter/模型决定)。 */
170
276
  readonly reasoningEffort?: string;
277
+ /** 本次 attempt 生效的 flags(experiment.flags 与 CLI flag 合并后的只读视图)。 */
171
278
  readonly flags: Readonly<Record<string, unknown>>;
279
+ /** 打一行调试日志;有 live 进度回调时走该回调,否则落到 stderr,不出现在最终结果里。 */
172
280
  log(msg: string): void;
281
+ /** 立即中止本 eval 并标记为 skipped(outcome / EvalResult.skipReason),reason 不能为空。 */
173
282
  skip(reason: string): never;
174
283
 
175
284
  // 值级断言
285
+ /**
286
+ * 对任意值跑一个 ValueAssertion,返回可链 `.gate()` / `.atLeast()` 的 AssertionHandle。
287
+ * 打分延迟到 eval 结束后统一 finalize,调用本身同步、不抛错——不通过只是记一条失败断言,
288
+ * 不会中止后续代码。要「不满足就立即中止 eval」用 require。
289
+ */
176
290
  check(value: unknown, assertion: ValueAssertion): AssertionHandle;
291
+ /**
292
+ * 对任意值跑一个 ValueAssertion,立即(await 时)求值;不满足就抛错中止整个 eval 剩余步骤
293
+ * (仍会把这条断言计入报告,不影响已记录的其它断言)。跟 check 的区别:check 只记录、
294
+ * 从不抛错,打分留到最后统一算;require 当场判定、失败即中止,适合「前置条件不满足,
295
+ * 后面写了也没意义」的场景。
296
+ */
177
297
  require(value: unknown, assertion: ValueAssertion): Promise<unknown>;
178
298
  /**
179
299
  * 把一组断言归到一个有标题的分组下(对照 vitest 的 test('title', ...))。纯组织/报告用,
@@ -182,30 +302,50 @@ export interface TestContext {
182
302
  group<T>(title: string, fn: () => Promise<T> | T): Promise<T>;
183
303
 
184
304
  // 作用域断言(工具 / 会话)
305
+ /** 断言默认会话累计状态为 "completed"(跨该会话所有轮次,不止最后一轮)。 */
185
306
  succeeded(): AssertionHandle;
307
+ /** 断言默认会话当前卡在 HITL 输入请求上。 */
186
308
  parked(): AssertionHandle;
309
+ /** 断言默认会话累计的助手回复包含 token(跨该会话所有轮次,不止最后一轮)。 */
187
310
  messageIncludes(token: string | RegExp): AssertionHandle;
311
+ /** 断言默认会话累计调用过指定名字的工具;match 可约束入参 / 次数 / 状态。 */
188
312
  calledTool(name: string, match?: ToolMatch): AssertionHandle;
313
+ /** 断言默认会话累计未调用指定工具(或未按 match 条件调用)。 */
189
314
  notCalledTool(name: string, match?: ToolMatch): AssertionHandle;
315
+ /** 断言默认会话累计工具调用按给定顺序出现(允许中间夹杂其它调用)。 */
190
316
  toolOrder(names: string[]): AssertionHandle;
317
+ /** 断言默认会话至今未调用任何工具。 */
191
318
  usedNoTools(): AssertionHandle;
319
+ /** 断言默认会话累计工具调用总数不超过 max。 */
192
320
  maxToolCalls(max: number): AssertionHandle;
321
+ /** 断言默认会话累计加载过指定 skill。 */
193
322
  loadedSkill(skill: string): AssertionHandle;
323
+ /** 断言默认会话累计没有失败的工具调用 / 命令。 */
194
324
  noFailedActions(): AssertionHandle;
325
+ /** 断言默认会话累计出现过指定类型的事件;opts.count 可约束出现次数。 */
195
326
  event(type: StreamEvent["type"], opts?: { count?: number }): AssertionHandle;
327
+ /** 断言默认会话累计未出现指定类型的事件。 */
196
328
  notEvent(type: StreamEvent["type"]): AssertionHandle;
329
+ /** 断言默认会话累计调用过指定名字的子 agent;match 可约束次数 / 状态 / remoteUrl。 */
197
330
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
331
+ /** 断言默认会话累计事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
198
332
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
333
+ /** 用自定义谓词对默认会话累计的整段事件流断言;失败时用 label 说明原因。 */
199
334
  eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
200
335
 
201
336
  // 工作区 / 沙箱
337
+ /** 受限沙箱视图:能执行命令 / 读写文件 / 看最终 diff,不能 stop 沙箱本身(见 SandboxHandle)。 */
202
338
  readonly sandbox: SandboxHandle;
203
339
 
204
340
  // 效率 / 成本
341
+ /** 默认会话累计的 token 用量与估算成本。 */
205
342
  readonly usage: Usage;
343
+ /** 断言默认会话累计 token 用量不超过 max。 */
206
344
  maxTokens(max: number): AssertionHandle;
345
+ /** 断言默认会话累计花费(USD)不超过 usd。 */
207
346
  maxCost(usd: number): AssertionHandle;
208
347
 
209
348
  // judge
349
+ /** 可用的 judge 命名空间(t.judge.autoevals.*)。 */
210
350
  readonly judge: JudgeNamespace;
211
351
  }
@@ -112,8 +112,9 @@ export function equals(expected: unknown): ValueAssertion {
112
112
  }
113
113
 
114
114
  /**
115
- * 用 schema 校验 value。优先 Standard Schema(schema['~standard'].validate),
116
- * 否则退化到 zod 风格的 .safeParse / .parse。校验通过 1,否则 0;任何异常 → 0。
115
+ * 用 schema 校验 value——不是正则匹配,是 Standard Schema / zod 风格的结构校验。
116
+ * 优先 Standard Schema(schema['~standard'].validate),否则退化到 zod 风格的
117
+ * .safeParse / .parse。校验通过 1,否则 0;任何异常 → 0。默认硬门槛。
117
118
  */
118
119
  export function matches(schema: unknown): ValueAssertion {
119
120
  return createAssertion("matches(schema)", "gate", async (value) =>
@@ -121,7 +122,10 @@ export function matches(schema: unknown): ValueAssertion {
121
122
  );
122
123
  }
123
124
 
124
- /** 归一化 Levenshtein 相似度 [0,1]。默认软分,阈值 0.6。 */
125
+ /**
126
+ * 纯字符串编辑距离,不是语义相似度——归一化 Levenshtein 距离 [0,1](1 - 编辑距离 / 较长串长度),
127
+ * 不理解含义,同义改写 / 语序调整会被判低分。默认软分,阈值 0.6。
128
+ */
125
129
  export function similarity(expected: string): ValueAssertion {
126
130
  return createAssertion(
127
131
  `similarity(${safeLabel(expected)})`,
@@ -143,19 +147,19 @@ export function satisfies(predicate: (v: unknown) => boolean, label?: string): V
143
147
  return createAssertion(name, "gate", (value) => (predicate(value) ? 1 : 0));
144
148
  }
145
149
 
146
- /** value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。 */
150
+ /** value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。默认硬门槛。 */
147
151
  export function isDefined(label?: string): ValueAssertion {
148
152
  const name = label ? `isDefined(${label})` : "isDefined()";
149
153
  return createAssertion(name, "gate", (value) => (value != null ? 1 : 0));
150
154
  }
151
155
 
152
- /** value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用) */
156
+ /** value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用)。默认硬门槛。 */
153
157
  export function isTrue(label?: string): ValueAssertion {
154
158
  const name = label ? `isTrue(${label})` : "isTrue()";
155
159
  return createAssertion(name, "gate", (value) => (value === true ? 1 : 0));
156
160
  }
157
161
 
158
- /** CommandResult.exitCode === 0 则通过。 */
162
+ /** CommandResult.exitCode === 0 1,否则 0。默认硬门槛。 */
159
163
  export function commandSucceeded(): ValueAssertion {
160
164
  return createAssertion("commandSucceeded()", "gate", (value) => {
161
165
  if (value === null || typeof value !== "object") return 0;
@@ -163,13 +167,16 @@ export function commandSucceeded(): ValueAssertion {
163
167
  });
164
168
  }
165
169
 
166
- /** value === false 则 1,否则 0。带 label 的布尔断言。 */
170
+ /** value === false 则 1,否则 0。带 label 的布尔断言。默认硬门槛。 */
167
171
  export function isFalse(label?: string): ValueAssertion {
168
172
  const name = label ? `isFalse(${label})` : "isFalse()";
169
173
  return createAssertion(name, "gate", (value) => (value === false ? 1 : 0));
170
174
  }
171
175
 
172
- /** 自定义断言工厂:直接给名字 / 严重级 / 阈值 / score。severity 省略默认 gate。 */
176
+ /**
177
+ * 自定义断言工厂:直接给名字 / 严重级 / 阈值 / score,一次调用即返回可用的 ValueAssertion——
178
+ * 不像 gate()/atLeast() 那样需要二段链式调用来定级。severity 省略默认 gate。
179
+ */
173
180
  export function makeAssertion(spec: {
174
181
  name: string;
175
182
  severity?: Severity;