niceeval 0.6.1 → 0.6.2

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 (296) hide show
  1. package/dist/agents/types.d.ts +67 -5
  2. package/dist/context/types.d.ts +32 -12
  3. package/dist/i18n/en.d.ts +54 -0
  4. package/dist/i18n/zh-CN.d.ts +55 -1
  5. package/dist/o11y/types.d.ts +16 -2
  6. package/dist/report/aggregate.d.ts +5 -3
  7. package/dist/report/aggregate.js +32 -5
  8. package/dist/report/built-ins/experiment-comparison.d.ts +39 -1
  9. package/dist/report/built-ins/experiment-comparison.js +116 -10
  10. package/dist/report/built-ins/index.d.ts +1 -0
  11. package/dist/report/built-ins/index.js +1 -1
  12. package/dist/report/components.d.ts +8 -2
  13. package/dist/report/components.js +3 -3
  14. package/dist/report/compute.d.ts +11 -18
  15. package/dist/report/compute.js +54 -34
  16. package/dist/report/flag.d.ts +16 -1
  17. package/dist/report/flag.js +19 -1
  18. package/dist/report/format.d.ts +16 -8
  19. package/dist/report/format.js +27 -12
  20. package/dist/report/index.d.ts +4 -3
  21. package/dist/report/index.js +5 -4
  22. package/dist/report/locale.d.ts +11 -2
  23. package/dist/report/locale.js +23 -5
  24. package/dist/report/metrics.d.ts +13 -1
  25. package/dist/report/metrics.js +65 -14
  26. package/dist/report/primitives.d.ts +6 -0
  27. package/dist/report/react/AttemptList.d.ts +2 -2
  28. package/dist/report/react/AttemptList.js +5 -6
  29. package/dist/report/react/EvalList.d.ts +1 -1
  30. package/dist/report/react/EvalList.js +0 -0
  31. package/dist/report/react/ExperimentComparison.d.ts +8 -0
  32. package/dist/report/react/ExperimentComparison.js +11 -0
  33. package/dist/report/react/ExperimentList.d.ts +2 -1
  34. package/dist/report/react/ExperimentList.js +8 -10
  35. package/dist/report/react/MetricScatter.js +5 -11
  36. package/dist/report/react/chart-math.d.ts +23 -6
  37. package/dist/report/react/chart-math.js +71 -19
  38. package/dist/report/react/fixtures.d.ts +3 -3
  39. package/dist/report/react/fixtures.js +21 -14
  40. package/dist/report/report.d.ts +5 -1
  41. package/dist/report/report.js +6 -2
  42. package/dist/report/text/faces.d.ts +1 -1
  43. package/dist/report/text/faces.js +42 -41
  44. package/dist/report/text/table.js +36 -5
  45. package/dist/report/types.d.ts +39 -21
  46. package/dist/results/types.d.ts +11 -0
  47. package/dist/runner/feedback/sink.d.ts +110 -0
  48. package/dist/runner/types.d.ts +513 -22
  49. package/dist/sandbox/docker.d.ts +23 -2
  50. package/dist/sandbox/e2b.d.ts +15 -1
  51. package/dist/sandbox/errors.d.ts +30 -3
  52. package/dist/sandbox/io-retry.d.ts +17 -0
  53. package/dist/sandbox/registry.d.ts +2 -0
  54. package/dist/sandbox/resolve.d.ts +18 -5
  55. package/dist/sandbox/retry.d.ts +11 -1
  56. package/dist/sandbox/types.d.ts +39 -5
  57. package/dist/sandbox/vercel.d.ts +7 -1
  58. package/dist/scoring/coverage.d.ts +30 -0
  59. package/dist/scoring/display.d.ts +21 -0
  60. package/dist/scoring/display.js +120 -0
  61. package/dist/scoring/types.d.ts +103 -20
  62. package/dist/shared/aggregate.d.ts +1 -0
  63. package/dist/shared/aggregate.js +3 -3
  64. package/dist/shared/types.d.ts +28 -0
  65. package/dist/tty-line.d.ts +0 -4
  66. package/dist/util.d.ts +23 -0
  67. package/docs-site/zh/concepts/adapter.mdx +22 -4
  68. package/docs-site/zh/concepts/experiment.mdx +1 -1
  69. package/docs-site/zh/concepts/overview.mdx +6 -6
  70. package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
  71. package/docs-site/zh/guides/authoring.mdx +33 -0
  72. package/docs-site/zh/guides/ci-integration.mdx +23 -12
  73. package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
  74. package/docs-site/zh/guides/custom-reports.mdx +29 -34
  75. package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
  76. package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
  77. package/docs-site/zh/guides/debugging.mdx +210 -0
  78. package/docs-site/zh/guides/experiments.mdx +10 -3
  79. package/docs-site/zh/guides/official-adapters.mdx +26 -2
  80. package/docs-site/zh/guides/publish-report.mdx +30 -16
  81. package/docs-site/zh/guides/report-components.mdx +42 -30
  82. package/docs-site/zh/guides/reporters.mdx +2 -2
  83. package/docs-site/zh/guides/results-data.mdx +17 -9
  84. package/docs-site/zh/guides/runner.mdx +17 -7
  85. package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
  86. package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
  87. package/docs-site/zh/guides/scoring-guide.mdx +4 -4
  88. package/docs-site/zh/guides/viewing-results.mdx +79 -36
  89. package/docs-site/zh/guides/write-experiment.mdx +5 -3
  90. package/docs-site/zh/guides/write-send.mdx +17 -1
  91. package/docs-site/zh/index.mdx +1 -1
  92. package/docs-site/zh/reference/builtin-agents.mdx +27 -0
  93. package/docs-site/zh/reference/capabilities.mdx +2 -2
  94. package/docs-site/zh/reference/cli.mdx +33 -7
  95. package/docs-site/zh/reference/define-agent.mdx +57 -4
  96. package/docs-site/zh/reference/define-config.mdx +1 -1
  97. package/docs-site/zh/reference/define-eval.mdx +42 -9
  98. package/docs-site/zh/reference/expect.mdx +26 -1
  99. package/package.json +5 -1
  100. package/src/agents/ai-sdk-otel.test.ts +1 -0
  101. package/src/agents/ai-sdk.test.ts +3 -0
  102. package/src/agents/ai-sdk.ts +3 -0
  103. package/src/agents/bub-install-spec.test.ts +34 -0
  104. package/src/agents/bub-install-spec.ts +32 -0
  105. package/src/agents/bub.ts +31 -32
  106. package/src/agents/claude-code.test.ts +130 -9
  107. package/src/agents/claude-code.ts +76 -4
  108. package/src/agents/codex.test.ts +189 -40
  109. package/src/agents/codex.ts +155 -14
  110. package/src/agents/coding-cli-versions.test.ts +15 -0
  111. package/src/agents/coding-cli-versions.ts +3 -0
  112. package/src/agents/index.ts +11 -0
  113. package/src/agents/langgraph.test.ts +204 -0
  114. package/src/agents/langgraph.ts +495 -0
  115. package/src/agents/marketplace.ts +85 -0
  116. package/src/agents/native-config.test.ts +179 -0
  117. package/src/agents/native-config.ts +267 -0
  118. package/src/agents/openai-compat.test.ts +1 -0
  119. package/src/agents/openclaw.test.ts +31 -0
  120. package/src/agents/openclaw.ts +171 -0
  121. package/src/agents/plugin-config.test.ts +1 -0
  122. package/src/agents/sdk-streams.test.ts +79 -0
  123. package/src/agents/sdk-streams.ts +55 -10
  124. package/src/agents/skills.test.ts +1 -0
  125. package/src/agents/streaming.test.ts +3 -9
  126. package/src/agents/types.ts +68 -5
  127. package/src/agents/ui-message-stream.test.ts +3 -0
  128. package/src/cli.ts +411 -108
  129. package/src/context/context.test.ts +51 -12
  130. package/src/context/context.ts +161 -29
  131. package/src/context/session.test.ts +1 -0
  132. package/src/context/session.ts +114 -6
  133. package/src/context/types.ts +30 -12
  134. package/src/define.test.ts +13 -8
  135. package/src/define.ts +25 -4
  136. package/src/expect/index.ts +53 -23
  137. package/src/i18n/en.ts +64 -2
  138. package/src/i18n/zh-CN.ts +65 -3
  139. package/src/o11y/cost.test.ts +1 -0
  140. package/src/o11y/execution-tree.test.ts +1 -20
  141. package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
  142. package/src/o11y/otlp/parse.test.ts +1 -0
  143. package/src/o11y/otlp/turn-otel.test.ts +1 -0
  144. package/src/o11y/parsers/bub.test.ts +1 -0
  145. package/src/o11y/parsers/claude-code.test.ts +1 -34
  146. package/src/o11y/parsers/openclaw.test.ts +154 -0
  147. package/src/o11y/parsers/openclaw.ts +310 -0
  148. package/src/o11y/prices.json +746 -311
  149. package/src/o11y/tool-names.test.ts +1 -0
  150. package/src/o11y/types.ts +16 -2
  151. package/src/report/aggregate.ts +34 -5
  152. package/src/report/built-in-user-parity.test.tsx +110 -153
  153. package/src/report/built-ins/experiment-comparison.tsx +173 -13
  154. package/src/report/built-ins/index.ts +6 -1
  155. package/src/report/components.tsx +9 -3
  156. package/src/report/compute.ts +70 -40
  157. package/src/report/dual-render.test.tsx +194 -67
  158. package/src/report/flag.ts +30 -2
  159. package/src/report/format.ts +35 -11
  160. package/src/report/index.ts +22 -4
  161. package/src/report/locale.ts +25 -5
  162. package/src/report/metrics.ts +67 -14
  163. package/src/report/primitives.tsx +6 -0
  164. package/src/report/react/AttemptList.tsx +6 -31
  165. package/src/report/react/EvalList.tsx +0 -0
  166. package/src/report/react/ExperimentComparison.tsx +68 -0
  167. package/src/report/react/ExperimentList.tsx +15 -9
  168. package/src/report/react/MetricScatter.tsx +12 -14
  169. package/src/report/react/chart-math.test.ts +85 -0
  170. package/src/report/react/chart-math.ts +101 -22
  171. package/src/report/react/enhance.js +33 -1
  172. package/src/report/react/fixtures.ts +24 -17
  173. package/src/report/react/render.test.tsx +9 -64
  174. package/src/report/react/styles.css +73 -2
  175. package/src/report/report.test.ts +306 -98
  176. package/src/report/report.ts +6 -2
  177. package/src/report/text/faces.ts +47 -43
  178. package/src/report/text/table.ts +42 -5
  179. package/src/report/types.ts +41 -21
  180. package/src/results/annotated-source.test.ts +62 -9
  181. package/src/results/annotated-source.ts +64 -6
  182. package/src/results/attempt-evidence.test.ts +9 -7
  183. package/src/results/attempt-evidence.ts +15 -8
  184. package/src/results/attempt-source.ts +6 -3
  185. package/src/results/copy.ts +145 -55
  186. package/src/results/host-equivalence.test.ts +8 -6
  187. package/src/results/index.ts +2 -0
  188. package/src/results/locator.test.ts +1 -22
  189. package/src/results/open.ts +7 -1
  190. package/src/results/publish.ts +149 -0
  191. package/src/results/results.test.ts +85 -51
  192. package/src/results/truncate.ts +90 -0
  193. package/src/results/types.ts +7 -0
  194. package/src/results/writer.ts +31 -13
  195. package/src/runner/attempt.test.ts +138 -7
  196. package/src/runner/attempt.ts +603 -104
  197. package/src/runner/discover.test.ts +47 -0
  198. package/src/runner/discover.ts +36 -2
  199. package/src/runner/eval-source.test.ts +1 -27
  200. package/src/runner/feedback/agent.test.ts +504 -0
  201. package/src/runner/feedback/agent.ts +409 -0
  202. package/src/runner/feedback/ci.test.ts +562 -0
  203. package/src/runner/feedback/ci.ts +401 -0
  204. package/src/runner/feedback/coordinator.test.ts +317 -0
  205. package/src/runner/feedback/coordinator.ts +397 -0
  206. package/src/runner/feedback/failure.ts +40 -0
  207. package/src/runner/feedback/human.test.ts +616 -0
  208. package/src/runner/feedback/human.ts +535 -0
  209. package/src/runner/feedback/index.ts +66 -0
  210. package/src/runner/feedback/io.ts +78 -0
  211. package/src/runner/feedback/profile.test.ts +50 -0
  212. package/src/runner/feedback/profile.ts +58 -0
  213. package/src/runner/feedback/reducer.test.ts +395 -0
  214. package/src/runner/feedback/reducer.ts +260 -0
  215. package/src/runner/feedback/renderer.ts +82 -0
  216. package/src/runner/feedback/sink.ts +203 -0
  217. package/src/runner/feedback/testing.ts +106 -0
  218. package/src/runner/ledger.test.ts +230 -0
  219. package/src/runner/ledger.ts +329 -0
  220. package/src/runner/report.test.ts +128 -3
  221. package/src/runner/report.ts +33 -9
  222. package/src/runner/reporters/artifacts.ts +8 -2
  223. package/src/runner/reporters/braintrust.test.ts +8 -7
  224. package/src/runner/reporters/braintrust.ts +9 -2
  225. package/src/runner/reporters/index.ts +2 -2
  226. package/src/runner/reporters/json.test.ts +162 -0
  227. package/src/runner/reporters/json.ts +35 -8
  228. package/src/runner/reporters/shared.ts +1 -5
  229. package/src/runner/run.test.ts +760 -3
  230. package/src/runner/run.ts +242 -36
  231. package/src/runner/sandbox-prep.ts +3 -42
  232. package/src/runner/timing.ts +158 -0
  233. package/src/runner/types.ts +518 -22
  234. package/src/sandbox/checkpoint.test.ts +55 -0
  235. package/src/sandbox/checkpoint.ts +29 -8
  236. package/src/sandbox/cli-commands.ts +407 -0
  237. package/src/sandbox/docker.ts +115 -16
  238. package/src/sandbox/e2b-agent-template.test.ts +56 -0
  239. package/src/sandbox/e2b-agent-template.ts +94 -0
  240. package/src/sandbox/e2b.ts +74 -9
  241. package/src/sandbox/errors.ts +111 -4
  242. package/src/sandbox/index.ts +2 -0
  243. package/src/sandbox/io-retry.test.ts +58 -0
  244. package/src/sandbox/io-retry.ts +45 -0
  245. package/src/sandbox/keep-registry.test.ts +86 -0
  246. package/src/sandbox/keep-registry.ts +142 -0
  247. package/src/sandbox/keep.ts +178 -0
  248. package/src/sandbox/paths.test.ts +1 -0
  249. package/src/sandbox/paths.ts +19 -8
  250. package/src/sandbox/registry.ts +20 -3
  251. package/src/sandbox/resolve.ts +76 -11
  252. package/src/sandbox/retry.test.ts +70 -0
  253. package/src/sandbox/retry.ts +46 -4
  254. package/src/sandbox/types.ts +44 -6
  255. package/src/sandbox/vercel.ts +43 -20
  256. package/src/scoring/collector.ts +60 -17
  257. package/src/scoring/coverage.ts +95 -0
  258. package/src/scoring/diff.ts +81 -0
  259. package/src/scoring/display.test.ts +121 -0
  260. package/src/scoring/display.ts +133 -0
  261. package/src/scoring/evidence.test.ts +189 -0
  262. package/src/scoring/judge.test.ts +142 -0
  263. package/src/scoring/judge.ts +15 -18
  264. package/src/scoring/scoped.ts +217 -50
  265. package/src/scoring/types.ts +117 -20
  266. package/src/scoring/verdict.ts +16 -4
  267. package/src/shared/aggregate.ts +3 -2
  268. package/src/shared/types.ts +31 -0
  269. package/src/show/compose.ts +2 -2
  270. package/src/show/index.ts +21 -1
  271. package/src/show/render.ts +619 -104
  272. package/src/show/show.test.ts +235 -19
  273. package/src/tty-line.ts +8 -26
  274. package/src/util.test.ts +1 -0
  275. package/src/util.ts +41 -0
  276. package/src/view/app/components/AttemptModal.tsx +153 -2
  277. package/src/view/app/components/CodeView.tsx +32 -11
  278. package/src/view/app/components/CopyControls.tsx +2 -2
  279. package/src/view/app/i18n.ts +6 -0
  280. package/src/view/app/lib/attempt-route.test.ts +1 -0
  281. package/src/view/app/lib/verdict.ts +7 -9
  282. package/src/view/artifact-serving.test.ts +2 -1
  283. package/src/view/client-dist/app.css +1 -1
  284. package/src/view/client-dist/app.js +17 -17
  285. package/src/view/data.test.ts +1 -0
  286. package/src/view/data.ts +11 -1
  287. package/src/view/index.ts +11 -0
  288. package/src/view/server.ts +2 -0
  289. package/src/view/styles.css +3 -0
  290. package/src/view/view-report.test.ts +6 -5
  291. package/src/runner/reporters/console.ts +0 -70
  292. package/src/runner/reporters/live.test.ts +0 -56
  293. package/src/runner/reporters/live.ts +0 -247
  294. package/src/runner/reporters/quiet.test.ts +0 -66
  295. package/src/runner/reporters/quiet.ts +0 -49
  296. package/src/runner/reporters/table.ts +0 -277
@@ -3,6 +3,7 @@
3
3
 
4
4
  import type { Agent, AgentContext, AgentSession, InputFile, InputRequest, InputResponse, Sandbox, StreamEvent, Telemetry, TraceSpan, Turn, Usage } from "../types.ts";
5
5
  import type { AgentOtelChannel } from "../o11y/otlp/turn-otel.ts";
6
+ import { downgradeCoverage, resolveAgentCoverage, worstCoverage, type ResolvedCoverage } from "../scoring/coverage.ts";
6
7
  import { captureLoc } from "../source-loc.ts";
7
8
  import { t } from "../i18n/index.ts";
8
9
 
@@ -81,6 +82,10 @@ export class RunSession implements AgentSession {
81
82
  readonly events: StreamEvent[] = [];
82
83
  readonly pendingInputRequests: InputRequest[] = [];
83
84
  readonly usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, requests: 0 };
85
+ /** 本会话累计的证据覆盖(初值 = Agent 级默认,逐轮按 Turn.coverage 降级折叠)。 */
86
+ coverage!: ResolvedCoverage;
87
+ /** 本会话内的轮次计数(turn 时间树 / 展示标签 s<session>/t<turn> 用)。 */
88
+ turnCount = 0;
84
89
  }
85
90
 
86
91
  export interface SessionDeps {
@@ -91,6 +96,29 @@ export interface SessionDeps {
91
96
  flags: Record<string, unknown>;
92
97
  signal: AbortSignal;
93
98
  log(msg: string): void;
99
+ /** runner 绑定的作用域反馈(adapter ctx.progress/diagnostic);省略时 progress 退回 log。 */
100
+ feedback?: import("../types.ts").ScopedFeedback;
101
+ /** adapter send 在飞时通知 runner(errored 归因到嵌套的 `agent.run` 阶段用)。 */
102
+ onSendActive?: (active: boolean) => void;
103
+ /**
104
+ * 变更分类账的 send 窗口钩子(仅沙箱型 agent):`beforeSend` 在 adapter send 前落 eval 归因
105
+ * commit,`afterSend` 在返回后落 agent 归因 commit;label 是 `s<session>/t<turn>` 窗口标签。
106
+ * 提供钩子时 send 自动串行(同一 workdir 上重叠的 send 是写入竞争,窗口不重叠)。
107
+ */
108
+ ledgerHooks?: {
109
+ beforeSend(label: string): Promise<void>;
110
+ afterSend(label: string): Promise<void>;
111
+ };
112
+ /** 每轮 send 结束后回报墙钟包络(runner 挂成 eval.run 下的 turn 时间树节点)。 */
113
+ onTurn?: (info: {
114
+ sessionIndex: number;
115
+ turnIndex: number;
116
+ startedAt: number;
117
+ durationMs: number;
118
+ failed?: boolean;
119
+ traceId?: string;
120
+ traceAttribution?: "traceparent" | "window" | "none";
121
+ }) => void;
94
122
  /** 路径推导出的实验 id(经 send ctx 透给 adapter,见 AgentContext.experimentId)。 */
95
123
  experimentId?: string;
96
124
  /** tracing agent 的 OTLP 端点(经 send ctx 透给 adapter,用于注入导出 env)。 */
@@ -104,6 +132,10 @@ export class SessionManager {
104
132
  readonly allEvents: StreamEvent[] = [];
105
133
  readonly usage: Usage = { inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, requests: 0 };
106
134
  lastStatus: "completed" | "failed" | "waiting" = "completed";
135
+ /** Agent 级默认覆盖(全通道解析,未声明 = unknown)。 */
136
+ readonly agentCoverage: ResolvedCoverage;
137
+ /** attempt 级累计覆盖:各轮解析后覆盖的最差值,随每次 send 折叠。 */
138
+ coverage: ResolvedCoverage;
107
139
 
108
140
  /** 归属到本 attempt 的 span(逐轮攒;attempt 末尾连同 sweep 的迟到 span 一起挂 trace)。 */
109
141
  readonly otelSpans: TraceSpan[] = [];
@@ -116,18 +148,28 @@ export class SessionManager {
116
148
  private readonly sessions: RunSession[] = [];
117
149
  private turnCount = 0;
118
150
  private sessionCount = 0;
151
+ /** 沙箱型 send 的串行链(见 SessionDeps.ledgerHooks):窗口不重叠。 */
152
+ private sendChain: Promise<unknown> = Promise.resolve();
119
153
 
120
154
  constructor(private readonly deps: SessionDeps) {
155
+ this.agentCoverage = resolveAgentCoverage(deps.agent.coverage);
156
+ this.coverage = this.agentCoverage;
121
157
  this.primary = this.newSession();
122
158
  }
123
159
 
124
160
  newSession(): RunSession {
125
161
  const s = new RunSession();
126
162
  s.index = ++this.sessionCount;
163
+ s.coverage = this.agentCoverage;
127
164
  this.sessions.push(s);
128
165
  return s;
129
166
  }
130
167
 
168
+ /** 一轮的解析后覆盖:Agent 默认按 Turn.coverage 降级(只降不升)。 */
169
+ resolveTurnCoverage(turn: Turn): ResolvedCoverage {
170
+ return downgradeCoverage(this.agentCoverage, turn.coverage);
171
+ }
172
+
131
173
  async send(
132
174
  session: RunSession,
133
175
  text: string,
@@ -136,6 +178,23 @@ export class SessionManager {
136
178
  ): Promise<Turn> {
137
179
  // 抓住作者调 t.send / t.sendFile 那一行(view 把回复叠回这一行)。
138
180
  const loc = captureLoc();
181
+ if (this.deps.ledgerHooks) {
182
+ // 沙箱型 send 经串行链执行:同一 workdir 上重叠的 send 本身就是写入竞争,
183
+ // 合并窗口只会掩盖归因不确定性(见 docs/feature/sandbox/architecture.md)。
184
+ const run = this.sendChain.then(() => this.sendSerialized(session, text, loc, files, responses));
185
+ this.sendChain = run.catch(() => {});
186
+ return run;
187
+ }
188
+ return this.sendSerialized(session, text, loc, files, responses);
189
+ }
190
+
191
+ private async sendSerialized(
192
+ session: RunSession,
193
+ text: string,
194
+ loc: ReturnType<typeof captureLoc>,
195
+ files?: readonly InputFile[],
196
+ responses?: readonly InputResponse[],
197
+ ): Promise<Turn> {
139
198
  const ctx: AgentContext = {
140
199
  signal: this.deps.signal,
141
200
  model: this.deps.model,
@@ -145,6 +204,12 @@ export class SessionManager {
145
204
  sandbox: this.deps.sandbox,
146
205
  session,
147
206
  telemetry: this.deps.telemetry,
207
+ progress: (u) =>
208
+ this.deps.feedback
209
+ ? this.deps.feedback.progress(u)
210
+ : this.deps.log(u.current !== undefined && u.total !== undefined ? `${u.message} (${u.current}/${u.total})` : u.message),
211
+ diagnostic: (d) => this.deps.feedback?.diagnostic(d),
212
+ // log 是 progress({ message }) 的别名(见 AgentContext.log)。
148
213
  log: this.deps.log,
149
214
  };
150
215
 
@@ -162,9 +227,48 @@ export class SessionManager {
162
227
  this.allEvents.push(userEvent);
163
228
  session.events.push(userEvent);
164
229
  session.pendingInputRequests.length = 0;
165
- const turn = this.deps.otel
166
- ? await this.sendWithOtel(this.deps.otel, { text, files, responses }, ctx)
167
- : await this.deps.agent.send({ text, files, responses }, ctx);
230
+ const turnIndex = ++session.turnCount;
231
+ const windowLabel = `s${session.index}/t${turnIndex}`;
232
+ // send 进入前:workdir 有未记录变化(fixture / setup / runCommand 副作用)先落 eval 归因。
233
+ await this.deps.ledgerHooks?.beforeSend(windowLabel);
234
+ let turn: Turn;
235
+ let sentTraceId: string | undefined;
236
+ let sentAttribution: "traceparent" | "window" | "none" | undefined;
237
+ this.deps.onSendActive?.(true);
238
+ try {
239
+ if (this.deps.otel) {
240
+ const r = await this.sendWithOtel(this.deps.otel, { text, files, responses }, ctx);
241
+ turn = r.turn;
242
+ sentTraceId = r.traceId;
243
+ sentAttribution = r.attribution;
244
+ } else {
245
+ turn = await this.deps.agent.send({ text, files, responses }, ctx);
246
+ }
247
+ } catch (e) {
248
+ this.deps.onTurn?.({
249
+ sessionIndex: session.index,
250
+ turnIndex,
251
+ startedAt: t0,
252
+ durationMs: Date.now() - t0,
253
+ failed: true,
254
+ traceAttribution: sentAttribution,
255
+ });
256
+ throw e;
257
+ } finally {
258
+ this.deps.onSendActive?.(false);
259
+ // send 返回后:这个 send 窗口内的全部 workspace 变化落 agent 归因(HITL waiting 同样收窗:
260
+ // adapter 义务保证返回时 agent 侧进程已退出或进入不再写 workspace 的静止态)。
261
+ await this.deps.ledgerHooks?.afterSend(windowLabel).catch(() => {});
262
+ }
263
+ this.deps.onTurn?.({
264
+ sessionIndex: session.index,
265
+ turnIndex,
266
+ startedAt: t0,
267
+ durationMs: Date.now() - t0,
268
+ failed: turn.status === "failed" ? true : undefined,
269
+ traceId: sentTraceId,
270
+ traceAttribution: sentAttribution,
271
+ });
168
272
 
169
273
  this.allEvents.push(...turn.events);
170
274
  session.events.push(...turn.events);
@@ -177,6 +281,10 @@ export class SessionManager {
177
281
  accumulateUsage(this.usage, turn.usage);
178
282
  accumulateUsage(session.usage, turn.usage);
179
283
  }
284
+ // 证据覆盖:attempt / session 级聚合取各轮最差值(见 scoring/coverage.ts)。
285
+ const turnCoverage = this.resolveTurnCoverage(turn);
286
+ this.coverage = worstCoverage([this.coverage, turnCoverage]);
287
+ session.coverage = worstCoverage([session.coverage, turnCoverage]);
180
288
  session.lastStatus = turn.status;
181
289
  this.lastStatus = turn.status;
182
290
  const reply = lastAssistantText(turn.events);
@@ -200,7 +308,7 @@ export class SessionManager {
200
308
  otel: AgentOtelChannel,
201
309
  input: { text: string; files?: readonly InputFile[]; responses?: readonly InputResponse[] },
202
310
  ctx: AgentContext,
203
- ): Promise<Turn> {
311
+ ): Promise<{ turn: Turn; traceId: string; attribution: "traceparent" | "window" | "none" }> {
204
312
  const r = await otel.runTurn((headers) => {
205
313
  const turnCtx: AgentContext = ctx.telemetry
206
314
  ? { ...ctx, telemetry: { ...ctx.telemetry, headers } }
@@ -218,7 +326,7 @@ export class SessionManager {
218
326
  this.warnedNoSpans = true;
219
327
  this.deps.log(t("otel.noSpans"));
220
328
  }
221
- return r.result;
329
+ return { turn: r.result, traceId: r.traceId, attribution: r.spans.length === 0 ? "none" : r.attribution };
222
330
  }
223
331
  }
224
332
 
@@ -236,7 +344,7 @@ function failureReason(events: readonly StreamEvent[]): string | undefined {
236
344
  return undefined;
237
345
  }
238
346
 
239
- /** 单行截断,口径同 src/runner/reporters/table.ts 的 truncateOneLine(折叠空白 + 120 字符上限)。 */
347
+ /** 单行截断:折叠空白 + 120 字符上限。 */
240
348
  function truncateOneLine(s: string, width: number): string {
241
349
  const clean = s.replace(/\s+/g, " ").trim();
242
350
  if (clean.length <= width) return clean;
@@ -2,6 +2,7 @@
2
2
  // `t` 的形状按 Agent 能力组装(见 docs/architecture.md「能力决定形状」)。
3
3
 
4
4
  import type { InputRequest, StreamEvent, ToolCall, Usage } from "../o11y/types.ts";
5
+ import type { DiagnosticInput, ProgressUpdate } from "../shared/types.ts";
5
6
  import type { AssertionHandle, ValueAssertion } from "../scoring/types.ts";
6
7
  import type {
7
8
  CommandOptions,
@@ -11,6 +12,9 @@ import type {
11
12
  SourceFiles,
12
13
  } from "../sandbox/types.ts";
13
14
 
15
+ /** `t.send()` / `session.send()` 的入参:字符串,或带附件的结构化消息。 */
16
+ export type SendInput = string | { text: string; files?: readonly import("../agents/types.ts").InputFile[] };
17
+
14
18
  /** t.send() 返回的句柄:从事件流派生便利字段 + expectOk。 */
15
19
  export interface TurnHandle {
16
20
  /** 本轮的原始事件流(工具调用、消息增量等);下面的派生字段都算自它。 */
@@ -59,8 +63,8 @@ export interface TurnHandle {
59
63
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
60
64
  /** 断言本轮事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
61
65
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
62
- /** 用自定义谓词对本轮整段事件流断言;失败时用 label 说明原因。 */
63
- eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
66
+ /** 用自定义谓词对本轮整段事件流断言;label 必填、进断言标题(谓词不透明,解释责任在 label)。 */
67
+ eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
64
68
  /** 断言本轮 token 用量不超过 max。 */
65
69
  maxTokens(max: number): AssertionHandle;
66
70
  /** 断言本轮花费(USD)不超过 usd。 */
@@ -93,7 +97,11 @@ export interface DiffView {
93
97
 
94
98
  /** 工具匹配小语言。 */
95
99
  export interface ToolMatch {
96
- /** 只匹配入参包含这些键值的调用(浅层包含,非深度相等)。 */
100
+ /**
101
+ * 只匹配入参包含这些键值的调用:**深度部分匹配**——嵌套对象逐键下钻,数组按值比较;
102
+ * 值可以是 RegExp(对字符串字段测试,不命中时再对整个 input 的序列化串兜底测一次)
103
+ * 或谓词函数。不要求深度相等,多余的入参键不影响命中。
104
+ */
97
105
  input?: Record<string, unknown>;
98
106
  /** 精确匹配调用次数,省略则只要求「至少一次」。 */
99
107
  count?: number;
@@ -179,8 +187,8 @@ export interface SandboxHandle {
179
187
  * 只看最后一轮要用 `send()` 返回的 TurnHandle。
180
188
  */
181
189
  export interface SessionHandle {
182
- /** 在这个会话里发一条消息,返回该轮的 TurnHandle。 */
183
- send(text: string): Promise<TurnHandle>;
190
+ /** 在这个会话里发一条消息(字符串或结构化消息),返回该轮的 TurnHandle。 */
191
+ send(input: SendInput): Promise<TurnHandle>;
184
192
  /** 在这个会话里发一条带文件的消息,语义同 TestContext.sendFile。 */
185
193
  sendFile(path: string, text?: string): Promise<TurnHandle>;
186
194
  /** 取这个会话里等待中的 HITL 输入请求;拿不到就抛,语义同 TestContext.requireInputRequest。 */
@@ -223,8 +231,8 @@ export interface SessionHandle {
223
231
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
224
232
  /** 断言这个会话累计事件按给定类型顺序出现。 */
225
233
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
226
- /** 用自定义谓词对这个会话累计的整段事件流断言。 */
227
- eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
234
+ /** 用自定义谓词对这个会话累计的整段事件流断言;label 必填、进断言标题。 */
235
+ eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
228
236
  /** 断言这个会话累计 token 用量不超过 max。 */
229
237
  maxTokens(max: number): AssertionHandle;
230
238
  /** 断言这个会话累计花费(USD)不超过 usd。 */
@@ -241,8 +249,8 @@ export interface SessionHandle {
241
249
  */
242
250
  export interface TestContext {
243
251
  // 会话
244
- /** 向默认会话发一条消息,返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。 */
245
- send(text: string): Promise<TurnHandle>;
252
+ /** 向默认会话发一条消息(字符串或结构化消息),返回该轮的 TurnHandle。事件同时累加进默认会话的累计事件流,供下面的作用域断言使用。 */
253
+ send(input: SendInput): Promise<TurnHandle>;
246
254
  /** 发一条带文件(图片等多模态输入)的消息。`path` 相对项目根;读出后 base64 随 TurnInput.files 交给 adapter。 */
247
255
  sendFile(path: string, text?: string): Promise<TurnHandle>;
248
256
  /** 取默认会话里等待中的 HITL 输入请求;不传 filter 要求恰好一条,拿不到就抛。 */
@@ -276,7 +284,17 @@ export interface TestContext {
276
284
  readonly reasoningEffort?: string;
277
285
  /** 本次 attempt 生效的实验 flags(experiment.flags 的只读视图;实验条件,非命令行开关)。 */
278
286
  readonly flags: Readonly<Record<string, unknown>>;
279
- /** 打一行调试日志;有 live 进度回调时走该回调,否则落到 stderr,不出现在最终结果里。 */
287
+ /**
288
+ * 作用域反馈:报告 eval 自己执行的长步骤(上传 fixture、跑构建……)。短命状态,scope 固定
289
+ * 为 `eval.run`;只报告不断言(见 docs/feature/eval/library/context.md「向运行反馈长步骤」)。
290
+ */
291
+ progress(update: ProgressUpdate): void;
292
+ /**
293
+ * 作用域反馈:报告运行结束后仍需保留的问题(永久事件,落 attempt diagnostics)。
294
+ * 即使 level 为 "error" 也不自动改变 verdict——测试结论仍由断言决定。
295
+ */
296
+ diagnostic(input: DiagnosticInput): void;
297
+ /** `progress({ message: msg })` 的别名(调试日志),不出现在最终结果里。 */
280
298
  log(msg: string): void;
281
299
  /** 立即中止本 eval 并标记为 skipped(verdict / EvalResult.skipReason),reason 不能为空。 */
282
300
  skip(reason: string): never;
@@ -330,8 +348,8 @@ export interface TestContext {
330
348
  calledSubagent(name: string, match?: SubagentMatch): AssertionHandle;
331
349
  /** 断言默认会话累计事件按给定类型顺序出现(允许中间夹杂其它事件)。 */
332
350
  eventOrder(types: StreamEvent["type"][]): AssertionHandle;
333
- /** 用自定义谓词对默认会话累计的整段事件流断言;失败时用 label 说明原因。 */
334
- eventsSatisfy(predicate: (events: readonly StreamEvent[]) => boolean, label?: string): AssertionHandle;
351
+ /** 用自定义谓词对默认会话累计的整段事件流断言;label 必填、进断言标题。 */
352
+ eventsSatisfy(label: string, predicate: (events: readonly StreamEvent[]) => boolean): AssertionHandle;
335
353
 
336
354
  // 工作区 / 沙箱
337
355
  /** 受限沙箱视图:能执行命令 / 读写文件 / 看最终 diff,不能 stop 沙箱本身(见 SandboxHandle)。 */
@@ -1,13 +1,18 @@
1
+ // cases: docs/engineering/unit-tests/sandbox/cases.md
1
2
  // SandboxSpec 链式钩子(.setup()/.teardown())的构造期契约:不可变、多次追加按顺序累加。
2
3
  // 执行顺序(setup 正序 / teardown 逆序 / LIFO cleanup)是 runner 的事,见
3
4
  // test/e2e-sandbox-hooks.test.ts;这里只测 dockerSandbox()/vercelSandbox()/e2bSandbox()/
4
5
  // defineSandbox() 这四个工厂产出的 spec 对象本身的构造行为。
5
6
  import { describe, expect, it } from "vitest";
6
7
  import { dockerSandbox, e2bSandbox, vercelSandbox, defineSandbox } from "./define.ts";
7
- import type { AgentSetup, AgentTeardown } from "./types.ts";
8
+ // 从公开 subpath 取类型,守住下游共享 helper 的正式导入面。
9
+ import type { SandboxHook, SandboxHookContext } from "./sandbox/index.ts";
8
10
 
9
- const noopSetup: AgentSetup = () => {};
10
- const noopTeardown: AgentTeardown = () => {};
11
+ const noopSetup: SandboxHook = () => {};
12
+ const noopTeardown: SandboxHook = () => {};
13
+ // subpath 类型导出的守护由 typecheck 完成:这两个类型标注编译不过即失败,无需运行时断言。
14
+ const _acceptsPublicHookContext = (_ctx: SandboxHookContext): void => {};
15
+ void _acceptsPublicHookContext;
11
16
 
12
17
  describe("sandbox factories: .setup()/.teardown() chain", () => {
13
18
  it("dockerSandbox() starts with empty hook arrays", () => {
@@ -27,15 +32,15 @@ describe("sandbox factories: .setup()/.teardown() chain", () => {
27
32
  });
28
33
 
29
34
  it("multiple .setup() calls accumulate in append order", () => {
30
- const a: AgentSetup = () => {};
31
- const b: AgentSetup = () => {};
35
+ const a: SandboxHook = () => {};
36
+ const b: SandboxHook = () => {};
32
37
  const spec = dockerSandbox().setup(a).setup(b);
33
38
  expect(spec.setupHooks).toEqual([a, b]);
34
39
  });
35
40
 
36
41
  it("multiple .teardown() calls accumulate in append order (execution order is the runner's job)", () => {
37
- const x: AgentTeardown = () => {};
38
- const y: AgentTeardown = () => {};
42
+ const x: SandboxHook = () => {};
43
+ const y: SandboxHook = () => {};
39
44
  const spec = dockerSandbox().teardown(x).teardown(y);
40
45
  expect(spec.teardownHooks).toEqual([x, y]);
41
46
  });
@@ -72,7 +77,7 @@ describe("sandbox factories: .setup()/.teardown() chain", () => {
72
77
 
73
78
  it("each chain call produces an independent object (no shared mutable state)", () => {
74
79
  const base = dockerSandbox();
75
- const other: AgentSetup = () => {};
80
+ const other: SandboxHook = () => {};
76
81
  const branchA = base.setup(noopSetup);
77
82
  const branchB = base.setup(other);
78
83
  expect(branchA.setupHooks).toEqual([noopSetup]);
package/src/define.ts CHANGED
@@ -3,8 +3,6 @@
3
3
 
4
4
  import type {
5
5
  Agent,
6
- AgentSetup,
7
- AgentTeardown,
8
6
  Config,
9
7
  CustomSandboxSpec,
10
8
  DockerSandboxSpec,
@@ -13,6 +11,7 @@ import type {
13
11
  ExperimentDef,
14
12
  RemoteAgentDef,
15
13
  SandboxAgentDef,
14
+ SandboxHook,
16
15
  SandboxHooks,
17
16
  VercelSandboxSpec,
18
17
  } from "./types.ts";
@@ -24,6 +23,7 @@ export function defineSandboxAgent(def: SandboxAgentDef): Agent {
24
23
  return {
25
24
  name: def.name,
26
25
  kind: "sandbox",
26
+ coverage: def.coverage,
27
27
  setup: def.setup,
28
28
  tracing: def.tracing,
29
29
  spanMapper: def.spanMapper,
@@ -38,6 +38,7 @@ export function defineAgent(def: RemoteAgentDef): Agent {
38
38
  return {
39
39
  name: def.name,
40
40
  kind: "remote",
41
+ coverage: def.coverage,
41
42
  setup: def.setup,
42
43
  tracing: def.tracing,
43
44
  spanMapper: def.spanMapper,
@@ -63,9 +64,26 @@ export function defineExperiment(def: ExperimentDef): ExperimentDef {
63
64
  throw new Error(t("define.experimentIdRejected"));
64
65
  }
65
66
  if (!def.agent) throw new Error(t("define.experimentAgentRequired"));
67
+ // flags 必须可 JSON 序列化(进结果快照的 ExperimentRunInfo.flags):解析时即校验,
68
+ // 非 JSON 值(函数 / undefined / 循环引用 / bigint)直接报错,不等到落盘才炸。
69
+ if (def.flags !== undefined) {
70
+ for (const [key, value] of Object.entries(def.flags)) {
71
+ if (!isJsonValue(value)) {
72
+ throw new Error(t("define.experimentFlagNotJson", { key }));
73
+ }
74
+ }
75
+ }
66
76
  return def;
67
77
  }
68
78
 
79
+ function isJsonValue(v: unknown): boolean {
80
+ if (v === null || typeof v === "string" || typeof v === "boolean") return true;
81
+ if (typeof v === "number") return Number.isFinite(v);
82
+ if (Array.isArray(v)) return v.every(isJsonValue);
83
+ if (typeof v === "object") return Object.values(v as Record<string, unknown>).every(isJsonValue);
84
+ return false;
85
+ }
86
+
69
87
  /** 项目级配置。 */
70
88
  export function defineConfig(config: Config): Config {
71
89
  return config;
@@ -81,8 +99,8 @@ export function defineConfig(config: Config): Config {
81
99
 
82
100
  /** 链式追加中累积的钩子(setup 按追加顺序执行,teardown 执行时逆序,见 SandboxHooks)。 */
83
101
  interface HookState {
84
- readonly setupHooks: readonly AgentSetup[];
85
- readonly teardownHooks: readonly AgentTeardown[];
102
+ readonly setupHooks: readonly SandboxHook[];
103
+ readonly teardownHooks: readonly SandboxHook[];
86
104
  }
87
105
 
88
106
  /** 四个工厂共用:把当前钩子状态包成 `.setup()` / `.teardown()` 方法,调用即用新状态重新 build。 */
@@ -148,6 +166,8 @@ export function defineSandbox(def: {
148
166
  name: string;
149
167
  create: CustomSandboxSpec["create"];
150
168
  recommendedConcurrency?: number;
169
+ /** 可发布参数的投影(进结果快照);未实现时只落 provider 名。 */
170
+ publicConfig?: CustomSandboxSpec["publicConfig"];
151
171
  }): CustomSandboxSpec {
152
172
  if (!def.name) throw new Error(t("define.sandboxNameRequired"));
153
173
  if (typeof def.create !== "function") throw new Error(t("define.sandboxCreateRequired"));
@@ -155,6 +175,7 @@ export function defineSandbox(def: {
155
175
  provider: def.name,
156
176
  create: def.create,
157
177
  recommendedConcurrency: def.recommendedConcurrency,
178
+ publicConfig: def.publicConfig,
158
179
  setupHooks: state.setupHooks,
159
180
  teardownHooks: state.teardownHooks,
160
181
  ...hookMethods(state, build),
@@ -17,24 +17,31 @@ export interface MatchOptions {
17
17
  // ───────────────────────── 内部工厂 ─────────────────────────
18
18
 
19
19
  /**
20
- * 唯一的内部工厂。gate()/atLeast() 都基于它返回新的不可变实例,
21
- * 共享同一个 score(只换 severity / threshold)。
20
+ * 唯一的内部工厂。gate()/atLeast()/optional() 都基于它返回新的不可变实例,
21
+ * 共享同一个 score(只换 severity / threshold / isOptional)。
22
+ * `expected` 是期望条件的有界文本描述(如 `contains "Brooklyn"`),失败时进
23
+ * AssertionResult.expected 供 show/view 展示。
22
24
  */
23
25
  function createAssertion(
24
26
  name: string,
25
27
  severity: Severity,
26
28
  score: (value: unknown) => number | Promise<number>,
27
29
  threshold?: number,
30
+ opts?: { expected?: string; isOptional?: boolean },
28
31
  ): ValueAssertion {
29
32
  const self: ValueAssertion = {
30
33
  name,
31
34
  severity,
32
35
  threshold,
36
+ ...(opts?.isOptional ? { isOptional: true } : {}),
37
+ ...(opts?.expected !== undefined ? { expected: opts.expected } : {}),
33
38
  score,
34
39
  // 转成硬门槛(失败即整条 eval 不通过)。
35
- gate: (t?: number) => createAssertion(name, "gate", score, t),
40
+ gate: (t?: number) => createAssertion(name, "gate", score, t, opts),
36
41
  // 软阈值:默认不改变 verdict;--strict 下软阈值失败也会使 verdict=failed。
37
- atLeast: (t: number) => createAssertion(name, "soft", score, t),
42
+ atLeast: (t: number) => createAssertion(name, "soft", score, t, opts),
43
+ // 允许证据缺席:评不了只记 unavailable,不影响判定(与 severity 正交)。
44
+ optional: () => createAssertion(name, severity, score, threshold, { ...opts, isOptional: true }),
38
45
  };
39
46
  return Object.freeze(self);
40
47
  }
@@ -86,28 +93,44 @@ function toMatchTarget(value: unknown, opts?: MatchOptions): string {
86
93
  export function includes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
87
94
  const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
88
95
  const suffix = opts?.stripComments ? ", stripComments" : "";
89
- return createAssertion(`includes(${label}${suffix})`, "gate", (value) => {
90
- const s = toMatchTarget(value, opts);
91
- if (needle instanceof RegExp) return needle.test(s) ? 1 : 0;
92
- return s.includes(needle) ? 1 : 0;
93
- });
96
+ return createAssertion(
97
+ `includes(${label}${suffix})`,
98
+ "gate",
99
+ (value) => {
100
+ const s = toMatchTarget(value, opts);
101
+ if (needle instanceof RegExp) return needle.test(s) ? 1 : 0;
102
+ return s.includes(needle) ? 1 : 0;
103
+ },
104
+ undefined,
105
+ { expected: needle instanceof RegExp ? `matches ${label}` : `contains ${label}` },
106
+ );
94
107
  }
95
108
 
96
109
  /** includes 的取反:不含子串 / 不命中正则则 1,否则 0。默认硬门槛。opts.stripComments 时只看真实代码。 */
97
110
  export function excludes(needle: string | RegExp, opts?: MatchOptions): ValueAssertion {
98
111
  const label = needle instanceof RegExp ? needle.toString() : safeLabel(needle);
99
112
  const suffix = opts?.stripComments ? ", stripComments" : "";
100
- return createAssertion(`excludes(${label}${suffix})`, "gate", (value) => {
101
- const s = toMatchTarget(value, opts);
102
- if (needle instanceof RegExp) return needle.test(s) ? 0 : 1;
103
- return s.includes(needle) ? 0 : 1;
104
- });
113
+ return createAssertion(
114
+ `excludes(${label}${suffix})`,
115
+ "gate",
116
+ (value) => {
117
+ const s = toMatchTarget(value, opts);
118
+ if (needle instanceof RegExp) return needle.test(s) ? 0 : 1;
119
+ return s.includes(needle) ? 0 : 1;
120
+ },
121
+ undefined,
122
+ { expected: needle instanceof RegExp ? `does not match ${label}` : `does not contain ${label}` },
123
+ );
105
124
  }
106
125
 
107
126
  /** 深相等则 1,否则 0。默认硬门槛。 */
108
127
  export function equals(expected: unknown): ValueAssertion {
109
- return createAssertion(`equals(${safeLabel(expected)})`, "gate", (value) =>
110
- deepEqual(value, expected) ? 1 : 0,
128
+ return createAssertion(
129
+ `equals(${safeLabel(expected)})`,
130
+ "gate",
131
+ (value) => (deepEqual(value, expected) ? 1 : 0),
132
+ undefined,
133
+ { expected: safeLabel(expected) },
111
134
  );
112
135
  }
113
136
 
@@ -138,6 +161,7 @@ export function similarity(expected: string): ValueAssertion {
138
161
  return 1 - levenshtein(a, b) / maxLen;
139
162
  },
140
163
  0.6,
164
+ { expected: safeLabel(expected) },
141
165
  );
142
166
  }
143
167
 
@@ -150,27 +174,33 @@ export function satisfies(predicate: (v: unknown) => boolean, label?: string): V
150
174
  /** value 非 null / 非 undefined 则 1,否则 0。省掉 `x !== undefined` + isTrue 的样板。默认硬门槛。 */
151
175
  export function isDefined(label?: string): ValueAssertion {
152
176
  const name = label ? `isDefined(${label})` : "isDefined()";
153
- return createAssertion(name, "gate", (value) => (value != null ? 1 : 0));
177
+ return createAssertion(name, "gate", (value) => (value != null ? 1 : 0), undefined, { expected: "defined" });
154
178
  }
155
179
 
156
180
  /** value === true 则 1,否则 0。带 label 的布尔断言(fileExists 等检查用)。默认硬门槛。 */
157
181
  export function isTrue(label?: string): ValueAssertion {
158
182
  const name = label ? `isTrue(${label})` : "isTrue()";
159
- return createAssertion(name, "gate", (value) => (value === true ? 1 : 0));
183
+ return createAssertion(name, "gate", (value) => (value === true ? 1 : 0), undefined, { expected: "true" });
160
184
  }
161
185
 
162
186
  /** CommandResult.exitCode === 0 则 1,否则 0。默认硬门槛。 */
163
187
  export function commandSucceeded(): ValueAssertion {
164
- return createAssertion("commandSucceeded()", "gate", (value) => {
165
- if (value === null || typeof value !== "object") return 0;
166
- return (value as { exitCode?: unknown }).exitCode === 0 ? 1 : 0;
167
- });
188
+ return createAssertion(
189
+ "commandSucceeded()",
190
+ "gate",
191
+ (value) => {
192
+ if (value === null || typeof value !== "object") return 0;
193
+ return (value as { exitCode?: unknown }).exitCode === 0 ? 1 : 0;
194
+ },
195
+ undefined,
196
+ { expected: "exit 0" },
197
+ );
168
198
  }
169
199
 
170
200
  /** value === false 则 1,否则 0。带 label 的布尔断言。默认硬门槛。 */
171
201
  export function isFalse(label?: string): ValueAssertion {
172
202
  const name = label ? `isFalse(${label})` : "isFalse()";
173
- return createAssertion(name, "gate", (value) => (value === false ? 1 : 0));
203
+ return createAssertion(name, "gate", (value) => (value === false ? 1 : 0), undefined, { expected: "false" });
174
204
  }
175
205
 
176
206
  /**