niceeval 0.6.0 → 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.
- package/dist/agents/types.d.ts +72 -6
- package/dist/context/types.d.ts +32 -12
- package/dist/i18n/en.d.ts +54 -0
- package/dist/i18n/zh-CN.d.ts +55 -1
- package/dist/o11y/types.d.ts +16 -2
- package/dist/report/aggregate.d.ts +5 -3
- package/dist/report/aggregate.js +32 -5
- package/dist/report/built-ins/experiment-comparison.d.ts +39 -0
- package/dist/report/built-ins/experiment-comparison.js +119 -0
- package/dist/report/built-ins/index.d.ts +2 -1
- package/dist/report/built-ins/index.js +2 -2
- package/dist/report/components.d.ts +10 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +68 -66
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -14
- package/dist/report/format.js +28 -30
- package/dist/report/index.d.ts +5 -4
- package/dist/report/index.js +6 -5
- package/dist/report/locale.d.ts +23 -3
- package/dist/report/locale.js +47 -6
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +66 -15
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +4 -4
- package/dist/report/react/AttemptList.js +8 -10
- package/dist/report/react/EvalList.d.ts +1 -1
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentComparison.d.ts +8 -0
- package/dist/report/react/ExperimentComparison.js +11 -0
- package/dist/report/react/ExperimentList.d.ts +4 -2
- package/dist/report/react/ExperimentList.js +57 -7
- package/dist/report/react/MetricScatter.js +6 -16
- package/dist/report/react/chart-math.d.ts +23 -6
- package/dist/report/react/chart-math.js +71 -19
- package/dist/report/react/fixtures.d.ts +3 -3
- package/dist/report/react/fixtures.js +30 -18
- package/dist/report/react/format.d.ts +1 -1
- package/dist/report/react/format.js +1 -1
- package/dist/report/react/index.d.ts +1 -1
- package/dist/report/report.d.ts +5 -1
- package/dist/report/report.js +6 -2
- package/dist/report/text/faces.d.ts +1 -1
- package/dist/report/text/faces.js +100 -61
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +40 -34
- package/dist/results/types.d.ts +11 -0
- package/dist/runner/feedback/sink.d.ts +110 -0
- package/dist/runner/types.d.ts +513 -22
- package/dist/sandbox/docker.d.ts +23 -2
- package/dist/sandbox/e2b.d.ts +15 -1
- package/dist/sandbox/errors.d.ts +30 -3
- package/dist/sandbox/io-retry.d.ts +17 -0
- package/dist/sandbox/registry.d.ts +2 -0
- package/dist/sandbox/resolve.d.ts +18 -5
- package/dist/sandbox/retry.d.ts +11 -1
- package/dist/sandbox/types.d.ts +39 -5
- package/dist/sandbox/vercel.d.ts +7 -1
- package/dist/scoring/coverage.d.ts +30 -0
- package/dist/scoring/display.d.ts +21 -0
- package/dist/scoring/display.js +120 -0
- package/dist/scoring/types.d.ts +103 -20
- package/dist/shared/aggregate.d.ts +1 -0
- package/dist/shared/aggregate.js +3 -3
- package/dist/shared/types.d.ts +28 -0
- package/dist/tty-line.d.ts +0 -4
- package/dist/util.d.ts +23 -0
- package/docs-site/zh/concepts/adapter.mdx +24 -6
- package/docs-site/zh/concepts/assert.mdx +11 -10
- package/docs-site/zh/concepts/evals.mdx +7 -6
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +7 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +35 -31
- package/docs-site/zh/guides/authoring.mdx +33 -0
- package/docs-site/zh/guides/ci-integration.mdx +23 -12
- package/docs-site/zh/guides/connect-your-agent.mdx +29 -3
- package/docs-site/zh/guides/custom-reports.mdx +29 -34
- package/docs-site/zh/guides/dataset-fanout.mdx +25 -3
- package/docs-site/zh/guides/debug-sandbox.mdx +57 -0
- package/docs-site/zh/guides/debugging.mdx +210 -0
- package/docs-site/zh/guides/experiments.mdx +10 -3
- package/docs-site/zh/guides/fixtures.mdx +3 -1
- package/docs-site/zh/guides/official-adapters.mdx +27 -3
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +49 -37
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +42 -8
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +57 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +258 -10
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +85 -41
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +19 -2
- package/docs-site/zh/index.mdx +1 -1
- package/docs-site/zh/reference/builtin-agents.mdx +27 -0
- package/docs-site/zh/reference/capabilities.mdx +2 -2
- package/docs-site/zh/reference/cli.mdx +35 -9
- package/docs-site/zh/reference/define-agent.mdx +60 -5
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +42 -9
- package/docs-site/zh/reference/events.mdx +2 -2
- package/docs-site/zh/reference/expect.mdx +36 -6
- package/package.json +5 -1
- package/src/agents/ai-sdk-otel.test.ts +1 -0
- package/src/agents/ai-sdk.test.ts +3 -0
- package/src/agents/ai-sdk.ts +3 -0
- package/src/agents/bub-install-spec.test.ts +34 -0
- package/src/agents/bub-install-spec.ts +32 -0
- package/src/agents/bub.ts +31 -32
- package/src/agents/claude-code.test.ts +130 -9
- package/src/agents/claude-code.ts +76 -4
- package/src/agents/codex.test.ts +189 -40
- package/src/agents/codex.ts +155 -14
- package/src/agents/coding-cli-versions.test.ts +15 -0
- package/src/agents/coding-cli-versions.ts +3 -0
- package/src/agents/index.ts +11 -0
- package/src/agents/langgraph.test.ts +204 -0
- package/src/agents/langgraph.ts +495 -0
- package/src/agents/marketplace.ts +85 -0
- package/src/agents/native-config.test.ts +179 -0
- package/src/agents/native-config.ts +267 -0
- package/src/agents/openai-compat.test.ts +1 -0
- package/src/agents/openclaw.test.ts +31 -0
- package/src/agents/openclaw.ts +171 -0
- package/src/agents/plugin-config.test.ts +1 -0
- package/src/agents/sdk-streams.test.ts +79 -0
- package/src/agents/sdk-streams.ts +55 -10
- package/src/agents/skills.test.ts +1 -0
- package/src/agents/streaming.test.ts +3 -9
- package/src/agents/types.ts +73 -6
- package/src/agents/ui-message-stream.test.ts +3 -0
- package/src/cli.ts +411 -108
- package/src/context/context.test.ts +51 -12
- package/src/context/context.ts +161 -29
- package/src/context/session.test.ts +1 -0
- package/src/context/session.ts +114 -6
- package/src/context/types.ts +30 -12
- package/src/define.test.ts +13 -8
- package/src/define.ts +25 -4
- package/src/expect/index.ts +53 -23
- package/src/i18n/en.ts +65 -4
- package/src/i18n/zh-CN.ts +66 -4
- package/src/o11y/cost.test.ts +1 -0
- package/src/o11y/execution-tree.test.ts +1 -20
- package/src/o11y/otlp/mappers/claude-code.test.ts +1 -0
- package/src/o11y/otlp/parse.test.ts +1 -0
- package/src/o11y/otlp/turn-otel.test.ts +1 -0
- package/src/o11y/parsers/bub.test.ts +1 -0
- package/src/o11y/parsers/claude-code.test.ts +1 -34
- package/src/o11y/parsers/openclaw.test.ts +154 -0
- package/src/o11y/parsers/openclaw.ts +310 -0
- package/src/o11y/prices.json +746 -311
- package/src/o11y/tool-names.test.ts +1 -0
- package/src/o11y/types.ts +16 -2
- package/src/report/aggregate.ts +34 -5
- package/src/report/built-in-user-parity.test.tsx +127 -173
- package/src/report/built-ins/experiment-comparison.tsx +179 -0
- package/src/report/built-ins/index.ts +7 -2
- package/src/report/components.tsx +11 -3
- package/src/report/compute.ts +80 -74
- package/src/report/dual-render.test.tsx +222 -91
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +36 -27
- package/src/report/index.ts +23 -6
- package/src/report/locale.ts +49 -6
- package/src/report/metrics.ts +68 -15
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +9 -36
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +173 -55
- package/src/report/react/MetricScatter.tsx +13 -25
- package/src/report/react/chart-math.test.ts +85 -0
- package/src/report/react/chart-math.ts +101 -22
- package/src/report/react/enhance.js +72 -1
- package/src/report/react/fixtures.ts +34 -21
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +0 -1
- package/src/report/react/render.test.tsx +30 -69
- package/src/report/react/styles.css +112 -14
- package/src/report/report.test.ts +308 -105
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +111 -67
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +42 -34
- package/src/results/annotated-source.test.ts +62 -9
- package/src/results/annotated-source.ts +64 -6
- package/src/results/attempt-evidence.test.ts +9 -7
- package/src/results/attempt-evidence.ts +15 -8
- package/src/results/attempt-source.ts +6 -3
- package/src/results/copy.ts +145 -55
- package/src/results/host-equivalence.test.ts +11 -9
- package/src/results/index.ts +2 -0
- package/src/results/locator.test.ts +1 -22
- package/src/results/open.ts +7 -1
- package/src/results/publish.ts +149 -0
- package/src/results/results.test.ts +85 -51
- package/src/results/truncate.ts +90 -0
- package/src/results/types.ts +7 -0
- package/src/results/writer.ts +31 -13
- package/src/runner/attempt.test.ts +138 -7
- package/src/runner/attempt.ts +603 -104
- package/src/runner/discover.test.ts +47 -0
- package/src/runner/discover.ts +36 -2
- package/src/runner/eval-source.test.ts +1 -27
- package/src/runner/feedback/agent.test.ts +504 -0
- package/src/runner/feedback/agent.ts +409 -0
- package/src/runner/feedback/ci.test.ts +562 -0
- package/src/runner/feedback/ci.ts +401 -0
- package/src/runner/feedback/coordinator.test.ts +317 -0
- package/src/runner/feedback/coordinator.ts +397 -0
- package/src/runner/feedback/failure.ts +40 -0
- package/src/runner/feedback/human.test.ts +616 -0
- package/src/runner/feedback/human.ts +535 -0
- package/src/runner/feedback/index.ts +66 -0
- package/src/runner/feedback/io.ts +78 -0
- package/src/runner/feedback/profile.test.ts +50 -0
- package/src/runner/feedback/profile.ts +58 -0
- package/src/runner/feedback/reducer.test.ts +395 -0
- package/src/runner/feedback/reducer.ts +260 -0
- package/src/runner/feedback/renderer.ts +82 -0
- package/src/runner/feedback/sink.ts +203 -0
- package/src/runner/feedback/testing.ts +106 -0
- package/src/runner/ledger.test.ts +230 -0
- package/src/runner/ledger.ts +329 -0
- package/src/runner/report.test.ts +128 -3
- package/src/runner/report.ts +33 -9
- package/src/runner/reporters/artifacts.ts +8 -2
- package/src/runner/reporters/braintrust.test.ts +8 -7
- package/src/runner/reporters/braintrust.ts +9 -2
- package/src/runner/reporters/index.ts +2 -2
- package/src/runner/reporters/json.test.ts +162 -0
- package/src/runner/reporters/json.ts +35 -8
- package/src/runner/reporters/shared.ts +1 -5
- package/src/runner/run.test.ts +760 -3
- package/src/runner/run.ts +242 -36
- package/src/runner/sandbox-prep.ts +3 -42
- package/src/runner/timing.ts +158 -0
- package/src/runner/types.ts +518 -22
- package/src/sandbox/checkpoint.test.ts +55 -0
- package/src/sandbox/checkpoint.ts +29 -8
- package/src/sandbox/cli-commands.ts +407 -0
- package/src/sandbox/docker.ts +115 -16
- package/src/sandbox/e2b-agent-template.test.ts +56 -0
- package/src/sandbox/e2b-agent-template.ts +94 -0
- package/src/sandbox/e2b.ts +74 -9
- package/src/sandbox/errors.ts +111 -4
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/io-retry.test.ts +58 -0
- package/src/sandbox/io-retry.ts +45 -0
- package/src/sandbox/keep-registry.test.ts +86 -0
- package/src/sandbox/keep-registry.ts +142 -0
- package/src/sandbox/keep.ts +178 -0
- package/src/sandbox/paths.test.ts +1 -0
- package/src/sandbox/paths.ts +19 -8
- package/src/sandbox/registry.ts +20 -3
- package/src/sandbox/resolve.ts +76 -11
- package/src/sandbox/retry.test.ts +70 -0
- package/src/sandbox/retry.ts +46 -4
- package/src/sandbox/types.ts +44 -6
- package/src/sandbox/vercel.ts +43 -20
- package/src/scoring/collector.ts +60 -17
- package/src/scoring/coverage.ts +95 -0
- package/src/scoring/diff.ts +81 -0
- package/src/scoring/display.test.ts +121 -0
- package/src/scoring/display.ts +133 -0
- package/src/scoring/evidence.test.ts +189 -0
- package/src/scoring/judge.test.ts +142 -0
- package/src/scoring/judge.ts +15 -18
- package/src/scoring/scoped.ts +217 -50
- package/src/scoring/types.ts +117 -20
- package/src/scoring/verdict.ts +16 -4
- package/src/shared/aggregate.ts +3 -2
- package/src/shared/types.ts +31 -0
- package/src/show/compose.ts +2 -2
- package/src/show/index.ts +29 -16
- package/src/show/render.ts +626 -308
- package/src/show/show.test.ts +251 -36
- package/src/tty-line.ts +8 -26
- package/src/util.test.ts +1 -0
- package/src/util.ts +41 -0
- package/src/view/app/components/AttemptModal.tsx +153 -2
- package/src/view/app/components/CodeView.tsx +32 -11
- package/src/view/app/components/CopyControls.tsx +2 -2
- package/src/view/app/i18n.ts +6 -0
- package/src/view/app/lib/attempt-route.test.ts +1 -0
- package/src/view/app/lib/verdict.ts +7 -9
- package/src/view/artifact-serving.test.ts +2 -1
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +17 -17
- package/src/view/data.test.ts +2 -1
- package/src/view/data.ts +17 -7
- package/src/view/index.ts +12 -1
- package/src/view/server.ts +2 -0
- package/src/view/shared/types.ts +1 -1
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +11 -10
- package/dist/o11y/execution-tree.d.ts +0 -103
- package/dist/o11y/otlp/select.d.ts +0 -22
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +0 -1
- package/dist/report/built-ins/cost-pass-rate-comparison.js +0 -17
- package/dist/results/annotated-source.d.ts +0 -61
- package/dist/results/attempt-evidence.d.ts +0 -69
- package/dist/results/attempt-source.d.ts +0 -15
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +0 -23
- package/src/runner/reporters/console.ts +0 -70
- package/src/runner/reporters/live.test.ts +0 -56
- package/src/runner/reporters/live.ts +0 -247
- package/src/runner/reporters/quiet.test.ts +0 -66
- package/src/runner/reporters/quiet.ts +0 -49
- package/src/runner/reporters/table.ts +0 -277
package/src/report/report.ts
CHANGED
|
@@ -44,10 +44,14 @@ export function defineReport(
|
|
|
44
44
|
return { build, [REPORT_DEFINITION]: true };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/**
|
|
47
|
+
/**
|
|
48
|
+
* 宿主装载报告文件时用:默认导出是不是 ReportDefinition。defineReport 的产物是普通对象;
|
|
49
|
+
* 「组件兼报告」(把 defineReport 产物 Object.assign 到双面组件上)是挂了 build 面的
|
|
50
|
+
* 可调用函数,同样算数——判据只看 build 面与标记,不看宿主形态。
|
|
51
|
+
*/
|
|
48
52
|
export function isReportDefinition(value: unknown): value is ReportDefinition {
|
|
49
53
|
return (
|
|
50
|
-
typeof value === "object" &&
|
|
54
|
+
(typeof value === "object" || typeof value === "function") &&
|
|
51
55
|
value !== null &&
|
|
52
56
|
(value as Partial<ReportDefinition>)[REPORT_DEFINITION] === true &&
|
|
53
57
|
typeof (value as Partial<ReportDefinition>).build === "function"
|
package/src/report/text/faces.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
AttemptListItem,
|
|
10
10
|
DeltaData,
|
|
11
11
|
EvalListItem,
|
|
12
|
-
ExperimentListEvalRow,
|
|
13
12
|
ExperimentListItem,
|
|
14
13
|
GroupSummaryData,
|
|
15
14
|
LineData,
|
|
@@ -24,7 +23,7 @@ import type { TextContext } from "../tree.ts";
|
|
|
24
23
|
import type { TableColumn, TableRow } from "../primitives.tsx";
|
|
25
24
|
import {
|
|
26
25
|
attemptItemReason,
|
|
27
|
-
|
|
26
|
+
experimentDisplayName,
|
|
28
27
|
formatDurationMs,
|
|
29
28
|
formatMetricValue,
|
|
30
29
|
formatPlainNumber,
|
|
@@ -32,7 +31,7 @@ import {
|
|
|
32
31
|
verdictMark,
|
|
33
32
|
} from "../format.ts";
|
|
34
33
|
import { countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
35
|
-
import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.ts";
|
|
34
|
+
import { indentBlock, padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.ts";
|
|
36
35
|
import { renderTableText } from "./table.ts";
|
|
37
36
|
import { renderCharPlot, renderCoordinateTable, type PlotPoint } from "./plot.ts";
|
|
38
37
|
|
|
@@ -325,11 +324,6 @@ export function scatterText(data: ScatterData, ctx: TextContext): string {
|
|
|
325
324
|
if (drawable.length === 0) {
|
|
326
325
|
return [localeText(locale, "scatter.noData", axes), ...footnotes].join("\n");
|
|
327
326
|
}
|
|
328
|
-
// 恰好 1 个可画点:成本 × 通过率的比较至少要两个实验,单点不成图。
|
|
329
|
-
if (drawable.length === 1) {
|
|
330
|
-
return [localeText(locale, "scatter.needTwo", axes), ...footnotes].join("\n");
|
|
331
|
-
}
|
|
332
|
-
|
|
333
327
|
// 点太密排不下时降级为坐标表,不硬挤
|
|
334
328
|
if (drawable.length > POINT_MARKS.length || ctx.width < 44) {
|
|
335
329
|
const table = renderCoordinateTable(
|
|
@@ -451,51 +445,118 @@ export function deltaText(data: DeltaData, ctx: TextContext): string {
|
|
|
451
445
|
|
|
452
446
|
// ───────────────────────── 实体列表(ExperimentList / EvalList / AttemptList)─────────────────────────
|
|
453
447
|
//
|
|
454
|
-
// 三面共用的紧凑标记:`locator
|
|
455
|
-
// 中间不留空格)——docs-site/zh/guides/report-components.mdx「终端输出形成反馈闭环」定的形态。
|
|
448
|
+
// 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
|
|
456
449
|
// ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
|
|
457
450
|
// niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
|
|
458
|
-
//
|
|
451
|
+
// 比较列表都不内联完整断言或命令模板。
|
|
459
452
|
|
|
460
|
-
function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"]
|
|
461
|
-
return `${item.locator}${verdictMark(item.verdict)}
|
|
453
|
+
function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
|
|
454
|
+
return `${item.locator}${verdictMark(item.verdict)}`;
|
|
462
455
|
}
|
|
463
456
|
|
|
464
457
|
// ── ExperimentList ──
|
|
465
458
|
|
|
466
|
-
function
|
|
467
|
-
const
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
459
|
+
function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
460
|
+
const locale = ctx.locale;
|
|
461
|
+
const compact = ctx.width < 100;
|
|
462
|
+
const columns: TableColumn[] = [
|
|
463
|
+
{ key: "experiment", header: compact && locale === "en" ? "Exp." : localeText(locale, "experimentList.experiment") },
|
|
464
|
+
{ key: "model", header: localeText(locale, "table.model") },
|
|
465
|
+
{ key: "agent", header: localeText(locale, "table.agent") },
|
|
466
|
+
{ key: "duration", header: compact && locale === "en" ? "Avg" : localeText(locale, "experimentList.avgDuration"), align: "right" },
|
|
467
|
+
{ key: "passRate", header: compact && locale === "en" ? "Pass" : localeText(locale, "experimentList.passRate"), align: "right" },
|
|
468
|
+
{ key: "result", header: localeText(locale, "experimentList.result") },
|
|
469
|
+
{ key: "tokens", header: localeText(locale, "experimentList.tokens"), align: "right" },
|
|
470
|
+
{ key: "cost", header: compact && locale === "en" ? "Cost" : localeText(locale, "experimentList.estimatedCost"), align: "right" },
|
|
471
|
+
];
|
|
472
|
+
const rows: TableRow[] = items.map((item) => ({
|
|
473
|
+
key: item.experimentId,
|
|
474
|
+
cells: {
|
|
475
|
+
experiment: experimentDisplayName(item.experimentId, relativeTo),
|
|
476
|
+
model: item.model ?? localeText(locale, "experimentList.defaultModel"),
|
|
477
|
+
agent: item.agent,
|
|
478
|
+
duration: cellText(item.duration),
|
|
479
|
+
passRate: cellText(item.passRate),
|
|
480
|
+
result: verdictTallyText(item.verdicts, locale),
|
|
481
|
+
tokens: cellText(item.tokens),
|
|
482
|
+
cost: cellText(item.cost),
|
|
483
|
+
},
|
|
484
|
+
}));
|
|
485
|
+
const metadata = items.flatMap((item) =>
|
|
486
|
+
wrapDisplay(
|
|
487
|
+
`${experimentDisplayName(item.experimentId, relativeTo)}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
|
|
488
|
+
Math.max(8, ctx.width - 2),
|
|
489
|
+
).map((line) => ` ${line}`),
|
|
490
|
+
);
|
|
491
|
+
return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
|
|
475
492
|
}
|
|
476
493
|
|
|
477
|
-
|
|
494
|
+
function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relativeTo?: string): string {
|
|
478
495
|
const locale = ctx.locale;
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
496
|
+
const columns: TableColumn[] = [
|
|
497
|
+
{ key: "status", header: localeText(locale, "experimentList.status") },
|
|
498
|
+
{ key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
|
|
499
|
+
// Result 是可扫读的失败预览,不是证据面:两行放不下的以 … 收口,完整值走 locator 下钻。
|
|
500
|
+
{ key: "result", header: localeText(locale, "experimentList.result"), maxLines: 2 },
|
|
501
|
+
{ key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
|
|
502
|
+
{ key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
|
|
503
|
+
];
|
|
504
|
+
// Result 的字符预算 ≈ 两行 × 它能分到的列宽(总宽减其它列的自然宽与列距)。这里只做
|
|
505
|
+
// 优先级让位(标题先截、received 最后截)的粗预算;精确的按宽度收口由列的 maxLines 兜底。
|
|
506
|
+
const statusWidth = Math.max(
|
|
507
|
+
stringWidth(localeText(locale, "experimentList.status")),
|
|
508
|
+
...item.evalRows.map((row) => stringWidth(`${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`)),
|
|
509
|
+
);
|
|
510
|
+
const entityWidth = Math.max(
|
|
511
|
+
stringWidth(localeText(locale, "experimentList.evalAttempt")),
|
|
512
|
+
...item.evalRows.flatMap((row) => [stringWidth(row.evalId), ...row.attempts.map((a) => stringWidth(a.locator) + 3)]),
|
|
513
|
+
);
|
|
514
|
+
const fixedWidth = statusWidth + entityWidth + 8 /* duration */ + 6 /* cost */ + 3 * 4; /* 4 段列距 */
|
|
515
|
+
const resultBudget = Math.max(24, (ctx.width - fixedWidth) * 2);
|
|
516
|
+
const rows: TableRow[] = item.evalRows.flatMap((row) => {
|
|
517
|
+
const parent: TableRow = {
|
|
518
|
+
key: row.evalId,
|
|
519
|
+
cells: {
|
|
520
|
+
status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
|
|
521
|
+
entity: row.evalId,
|
|
522
|
+
result: "",
|
|
523
|
+
duration: localeText(locale, "entityList.average", { value: cellText(row.duration) }),
|
|
524
|
+
cost: localeText(locale, "entityList.average", { value: cellText(row.cost) }),
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
|
|
528
|
+
key: attempt.locator,
|
|
529
|
+
cells: {
|
|
530
|
+
status: ` ${verdictMark(attempt.verdict)}`,
|
|
531
|
+
entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
|
|
532
|
+
result: attemptItemReason(attempt, resultBudget) ?? MISSING_MARK,
|
|
533
|
+
duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
|
|
534
|
+
cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
|
|
535
|
+
},
|
|
536
|
+
}));
|
|
537
|
+
return [parent, ...attempts];
|
|
491
538
|
});
|
|
492
|
-
|
|
539
|
+
const flags = item.flags && Object.keys(item.flags).length > 0
|
|
540
|
+
? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
|
|
541
|
+
: undefined;
|
|
542
|
+
return [experimentDisplayName(item.experimentId, relativeTo), flags, renderTableText({ columns, rows, locale }, ctx)]
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.join("\n");
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
export function experimentListText(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
548
|
+
if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
|
|
549
|
+
return [
|
|
550
|
+
experimentSummaryTable(items, ctx, relativeTo),
|
|
551
|
+
...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
|
|
552
|
+
].join("\n\n");
|
|
493
553
|
}
|
|
494
554
|
|
|
495
555
|
// ── EvalList ──
|
|
496
556
|
|
|
497
|
-
function evalListAttemptLine(item: AttemptListItem): string {
|
|
498
|
-
|
|
557
|
+
function evalListAttemptLine(item: AttemptListItem, ctx: TextContext): string {
|
|
558
|
+
// 行式列表同守「Result 最多两行」:预算 = 两行终端宽,超出由 fit 投影按优先级让位。
|
|
559
|
+
const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - stringWidth(locatorBadge(item)) - 6));
|
|
499
560
|
return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
|
|
500
561
|
}
|
|
501
562
|
|
|
@@ -507,10 +568,14 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
|
507
568
|
const summary = [
|
|
508
569
|
localeText(locale, "attemptList.score", { score: cellText(item.score) }),
|
|
509
570
|
localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
|
|
510
|
-
|
|
511
|
-
|
|
571
|
+
localeText(locale, "entityList.average", {
|
|
572
|
+
value: item.duration.value === null ? missingText(locale) : formatDurationMs(item.duration.value),
|
|
573
|
+
}),
|
|
574
|
+
localeText(locale, "entityList.average", {
|
|
575
|
+
value: item.cost.value === null ? missingText(locale) : formatUSD(item.cost.value),
|
|
576
|
+
}),
|
|
512
577
|
].join(" · ");
|
|
513
|
-
const attemptLines = item.attempts.map(evalListAttemptLine);
|
|
578
|
+
const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
|
|
514
579
|
return [identity, ` ${summary}`, ...attemptLines].join("\n");
|
|
515
580
|
});
|
|
516
581
|
return blocks.join("\n\n");
|
|
@@ -518,46 +583,25 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
|
518
583
|
|
|
519
584
|
// ── AttemptList ──
|
|
520
585
|
|
|
521
|
-
/**
|
|
522
|
-
|
|
523
|
-
function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: ReportLocale): string {
|
|
586
|
+
/** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions 走 locator 下钻。 */
|
|
587
|
+
function attemptListItemText(item: AttemptListItem, ctx: TextContext): string {
|
|
524
588
|
const head = [
|
|
525
589
|
`${verdictMark(item.verdict)} ${item.locator}`,
|
|
526
590
|
item.evalId,
|
|
527
591
|
item.experimentId,
|
|
528
592
|
formatDurationMs(item.durationMs),
|
|
529
593
|
...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
|
|
530
|
-
...(capabilityBadge(item.capabilities) ? [capabilityBadge(item.capabilities)] : []),
|
|
531
594
|
].join(" · ");
|
|
532
595
|
const lines = [head];
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
for (const assertion of item.assertions) {
|
|
537
|
-
const scoreText =
|
|
538
|
-
assertion.threshold !== undefined
|
|
539
|
-
? `${formatPlainNumber(assertion.score)}/${formatPlainNumber(assertion.threshold)}`
|
|
540
|
-
: formatPlainNumber(assertion.score);
|
|
541
|
-
lines.push(
|
|
542
|
-
` ${assertion.severity} ${assertion.name} · ${localeText(locale, `verdict.${assertion.passed ? "passed" : "failed"}`)}${assertion.severity === "soft" ? ` ${scoreText}` : ""}`,
|
|
543
|
-
);
|
|
544
|
-
if (assertion.detail) lines.push(indentBlock(wrapDisplay(assertion.detail, ctx.width - 4).join("\n"), " "));
|
|
545
|
-
if (assertion.evidence) {
|
|
546
|
-
const limit = Math.max(240, ctx.width * 5);
|
|
547
|
-
const evidence =
|
|
548
|
-
assertion.evidence.length <= limit
|
|
549
|
-
? assertion.evidence
|
|
550
|
-
: `${assertion.evidence.slice(0, limit)}… (${assertion.evidence.length - limit} more chars; open ${item.locator} for full evidence)`;
|
|
551
|
-
lines.push(indentBlock(wrapDisplay(evidence, ctx.width - 6).join("\n"), " "));
|
|
552
|
-
}
|
|
553
|
-
}
|
|
596
|
+
const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - 4));
|
|
597
|
+
if (reason) lines.push(` ${reason}`);
|
|
554
598
|
return lines.join("\n");
|
|
555
599
|
}
|
|
556
600
|
|
|
557
601
|
export function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string {
|
|
558
602
|
const locale = ctx.locale;
|
|
559
603
|
if (items.length === 0) return localeText(locale, "attemptList.empty");
|
|
560
|
-
const blocks = items.map((item) => attemptListItemText(item, ctx
|
|
604
|
+
const blocks = items.map((item) => attemptListItemText(item, ctx));
|
|
561
605
|
const remaining = (total ?? items.length) - items.length;
|
|
562
606
|
if (remaining > 0) blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));
|
|
563
607
|
return blocks.join("\n\n");
|
package/src/report/text/table.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import type { TableProps, TableRow } from "../primitives.tsx";
|
|
10
10
|
import type { TextContext } from "../tree.ts";
|
|
11
11
|
import { countText, localeText } from "../locale.ts";
|
|
12
|
-
import { renderAlignedRows, stringWidth, wrapDisplay, type ColumnAlign } from "./layout.ts";
|
|
12
|
+
import { charDisplayWidth, renderAlignedRows, stringWidth, wrapDisplay, type ColumnAlign } from "./layout.ts";
|
|
13
13
|
|
|
14
14
|
const MISSING_MARK = "—";
|
|
15
15
|
/** 列间距,与 renderAlignedRows 的 join(" ") 一致。 */
|
|
@@ -56,12 +56,43 @@ function fitWidths(
|
|
|
56
56
|
return { widths, hidden };
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
/**
|
|
60
|
-
function
|
|
59
|
+
/** 按显示宽度截到 width(不够放 `…` 时原样返回)。 */
|
|
60
|
+
function truncateDisplay(text: string, width: number): string {
|
|
61
|
+
if (stringWidth(text) <= width) return text;
|
|
62
|
+
let out = "";
|
|
63
|
+
let used = 0;
|
|
64
|
+
for (const ch of text) {
|
|
65
|
+
const w = charDisplayWidth(ch.codePointAt(0)!);
|
|
66
|
+
if (used + w > width) break;
|
|
67
|
+
out += ch;
|
|
68
|
+
used += w;
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 列的 maxLines 收口:超出的行丢弃,最后一行以 `…` 如实标注被收口。 */
|
|
74
|
+
function clampCellLines(lines: string[], maxLines: number | undefined, width: number): string[] {
|
|
75
|
+
if (maxLines === undefined || lines.length <= maxLines) return lines;
|
|
76
|
+
const kept = lines.slice(0, Math.max(1, maxLines));
|
|
77
|
+
const last = kept[kept.length - 1]!;
|
|
78
|
+
kept[kept.length - 1] = `${truncateDisplay(last, Math.max(1, width - 1))}…`;
|
|
79
|
+
return kept;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** 逻辑行 → 物理行:每格折到自己的列宽,列带 maxLines 就收口,行高取最高的那格,矮格补空串。 */
|
|
83
|
+
function toPhysicalRows(
|
|
84
|
+
cells: readonly string[],
|
|
85
|
+
widths: readonly number[],
|
|
86
|
+
maxLines: readonly (number | undefined)[],
|
|
87
|
+
): string[][] {
|
|
61
88
|
const wrapped = cells.map((cell, c) =>
|
|
62
89
|
// 放得下就原样保留:wrapDisplay 会把连续空格折成一个,而 DeltaTable 的
|
|
63
90
|
// "50% → 62% +12pp" 这类格子内含固定间距 —— 不折行时一个字节都不能动。
|
|
64
|
-
|
|
91
|
+
clampCellLines(
|
|
92
|
+
!cell.includes("\n") && stringWidth(cell) <= widths[c] ? [cell] : wrapDisplay(cell, widths[c]),
|
|
93
|
+
maxLines[c],
|
|
94
|
+
widths[c]!,
|
|
95
|
+
),
|
|
65
96
|
);
|
|
66
97
|
const height = Math.max(...wrapped.map((lines) => lines.length), 1);
|
|
67
98
|
const out: string[][] = [];
|
|
@@ -92,7 +123,13 @@ export function renderTableText(props: TableProps, ctx: TextContext): string {
|
|
|
92
123
|
const natural = header.map((_, c) => Math.max(...matrix.map((row) => stringWidth(row[c] ?? ""))));
|
|
93
124
|
const { widths, hidden } = fitWidths(natural, align, ctx.width);
|
|
94
125
|
|
|
95
|
-
const
|
|
126
|
+
const maxLines: (number | undefined)[] = props.columns.map((column) => column.maxLines);
|
|
127
|
+
if (hasLocator) maxLines.push(undefined);
|
|
128
|
+
// 表头不参与 maxLines 收口:表头是自己写的短词,收口只服务数据格。
|
|
129
|
+
const physical = [
|
|
130
|
+
...toPhysicalRows(header.slice(0, widths.length), widths, widths.map(() => undefined)),
|
|
131
|
+
...body.flatMap((row) => toPhysicalRows(row.slice(0, widths.length), widths, maxLines)),
|
|
132
|
+
];
|
|
96
133
|
const table = renderAlignedRows(physical, align);
|
|
97
134
|
return hidden > 0 ? `${table}\n${countText(locale, "table.columnsHidden", hidden)}` : table;
|
|
98
135
|
}
|
package/src/report/types.ts
CHANGED
|
@@ -4,12 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AttemptHandle, SelectionWarning } from "../results/types.ts";
|
|
6
6
|
import type { AttemptLocator } from "../results/locator.ts";
|
|
7
|
-
import type {
|
|
8
|
-
import type { AssertionResult, Verdict } from "../types.ts";
|
|
7
|
+
import type { AssertionResult, AttemptError, DiagnosticRecord, Verdict } from "../types.ts";
|
|
9
8
|
import type { LocalizedLabel, ReportLocale } from "./locale.ts";
|
|
10
9
|
|
|
11
10
|
export type { SelectionWarning };
|
|
12
|
-
export type { AttemptLocator
|
|
11
|
+
export type { AttemptLocator };
|
|
13
12
|
export type { LocalizedLabel, ReportLocale };
|
|
14
13
|
|
|
15
14
|
// ───────────────────────── 指标与聚合 ─────────────────────────
|
|
@@ -32,7 +31,7 @@ export interface MetricAggregate {
|
|
|
32
31
|
* 指标:纯函数,吃一个 AttemptHandle 吐一个值(null = 此 attempt 测不了这个指标,
|
|
33
32
|
* 不进聚合;0 = 测了结果是零,照常进),外加名字、两级聚合方式和渲染提示。
|
|
34
33
|
* 内置指标与自定义指标是同一个类型,没有特权。name 走字面量泛型:列键锚在指标
|
|
35
|
-
* 对象上(`row.cells[
|
|
34
|
+
* 对象上(`row.cells[taskPassRate.name]`),拼错列名编译不过。
|
|
36
35
|
*/
|
|
37
36
|
export interface Metric<Name extends string = string> {
|
|
38
37
|
/** MetricColumn.key 与列头的来源;同一次计算里重名是错误。 */
|
|
@@ -87,8 +86,24 @@ export interface FlagRef {
|
|
|
87
86
|
readonly unit?: string;
|
|
88
87
|
}
|
|
89
88
|
|
|
90
|
-
/**
|
|
91
|
-
|
|
89
|
+
/**
|
|
90
|
+
* config() 的产物:把顶层运行配置(快照 `ExperimentRunInfo` 投影的字段全集,外加桥接到
|
|
91
|
+
* 快照顶层权威字段的 `model` / `agent` 两个键)当维度或轴,槽位用法与 {@link FlagRef} 一致。
|
|
92
|
+
* 未投影的值不猜:分组如实归「(unset)」,作轴不画点、注脚报数。
|
|
93
|
+
*/
|
|
94
|
+
export interface ConfigRef {
|
|
95
|
+
readonly kind: "config";
|
|
96
|
+
readonly name: string;
|
|
97
|
+
/** 组标签 / 轴标签;函数形态把投影值折成组名。 */
|
|
98
|
+
readonly label?: string | ((value: string | number | boolean) => string);
|
|
99
|
+
readonly unit?: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** MetricLine 的 x 轴输入:experiment 声明的 flag,或顶层运行配置(config())。 */
|
|
103
|
+
export type AxisInput = FlagRef | ConfigRef;
|
|
104
|
+
|
|
105
|
+
/** 维度槽的输入:内置/自定义维度、experiment 声明的 flag,或顶层运行配置(config())。 */
|
|
106
|
+
export type DimensionInput = Dimension | FlagRef | ConfigRef;
|
|
92
107
|
|
|
93
108
|
// ───────────────────────── 计算产物(组件 data props)─────────────────────────
|
|
94
109
|
|
|
@@ -266,7 +281,7 @@ export interface GroupSummaryData {
|
|
|
266
281
|
/**
|
|
267
282
|
* 组的通过率:eval 级折叠计票的 `passed / (passed + failed + errored)`(`skipped` 不进
|
|
268
283
|
* 分母)——这是旧 `GroupSelector` 卡片的口径,不是 `OverviewData.totals.passRate` 那种
|
|
269
|
-
* `computeCell` 两级聚合(两者服务不同问题:「这组题多少算过」vs
|
|
284
|
+
* `computeCell` 两级聚合(两者服务不同问题:「这组题多少算过」vs「每次运行成功多少」)。
|
|
270
285
|
* 分母为 0(组内没有任何已跑的 eval)时 `value` 为 `null`,不编 0%。
|
|
271
286
|
*/
|
|
272
287
|
passRate: MetricCell;
|
|
@@ -290,10 +305,11 @@ export interface OverviewData {
|
|
|
290
305
|
errored: number;
|
|
291
306
|
skipped: number;
|
|
292
307
|
/**
|
|
293
|
-
*
|
|
294
|
-
* columns: [
|
|
308
|
+
* 默认成功率的唯一官方口径:`computeCell(endToEndPassRate, items)`,与 `MetricTable.data(...,
|
|
309
|
+
* columns: [endToEndPassRate])` 同一台两级聚合引擎(题内折叠 perEval、跨题折叠 across,默认都是
|
|
295
310
|
* mean)——一道题内多个 attempt 部分通过,贡献的是小数份额而不是二元票。`samples`/`total`
|
|
296
|
-
* 是两级聚合口径下的 attempt 计数(`total` 含 skipped
|
|
311
|
+
* 是两级聚合口径下的 attempt 计数(`total` 含 skipped 与 errored——endToEndPassRate 对
|
|
312
|
+
* errored 记 0、只对 skipped 记 null,`samples` 因此只不含 skipped),不等于上面四个
|
|
297
313
|
* verdict 计票的任何一个之和。
|
|
298
314
|
*/
|
|
299
315
|
passRate: MetricCell;
|
|
@@ -336,10 +352,11 @@ export interface DeltaData<K extends string = string> {
|
|
|
336
352
|
// `EvalListItem.attempts` 的元素,报告作者可以直接把这些嵌套数组喂给 `<AttemptList items={...} />`。
|
|
337
353
|
|
|
338
354
|
/**
|
|
339
|
-
* `AttemptList` 一项 = 一个 Attempt
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
*
|
|
355
|
+
* `AttemptList` 一项 = 一个 Attempt:身份、判定、断言、结构化 error、diagnostics、耗时、
|
|
356
|
+
* 成本和 locator。`ExperimentList` / `EvalList` 的下钻数组复用同一个类型,不是各自的精简版。
|
|
357
|
+
* 渲染面只显示一条主失败断言摘要或 error 的一层摘要;完整 assertions、cause / stack 与
|
|
358
|
+
* diagnostics 属于 locator 下钻详情,不塞进比较列表,但随数据携带 —— `AttemptList.data` 的 `redact`
|
|
359
|
+
* 钩子覆盖它们的自由文本(见 docs/feature/reports/library.md「AttemptList」)。
|
|
343
360
|
*/
|
|
344
361
|
export interface AttemptListItem {
|
|
345
362
|
evalId: string;
|
|
@@ -347,33 +364,25 @@ export interface AttemptListItem {
|
|
|
347
364
|
attempt: number;
|
|
348
365
|
agent: string;
|
|
349
366
|
verdict: Verdict;
|
|
350
|
-
error
|
|
367
|
+
/** 结构化执行错误(与 `EvalResult.error` 同构):列表只显示 `message` 一层摘要。 */
|
|
368
|
+
error?: AttemptError;
|
|
369
|
+
/** 本 attempt 的有界诊断(teardown / cleanup 失败等,与 verdict 独立);属于下钻详情。 */
|
|
370
|
+
diagnostics?: DiagnosticRecord[];
|
|
351
371
|
assertions: AssertionResult[];
|
|
352
372
|
durationMs: number;
|
|
353
373
|
costUSD?: number;
|
|
354
374
|
locator: AttemptLocator;
|
|
355
|
-
/**
|
|
356
|
-
* 证据能力标记:`eval`(运行时 Eval 源码已保存)、`execution`(执行事件非空)、
|
|
357
|
-
* `timing`(执行事件 + 这次运行接入过 OTel)、`diff`(工作区有文件改动)。逐位定义与
|
|
358
|
-
* `AttemptEvidence.capabilities` 完全一致——这里复用同一个类型,不重复声明四个布尔位的门槛。
|
|
359
|
-
* 计算函数从 `EvalResult` 上已有的 `hasEvents` / `hasSources` / `hasTrace` 摘要位与一次
|
|
360
|
-
* `attempt.diff()` 懒加载算出,不对每个 item 调用完整的 `loadAttemptEvidence`
|
|
361
|
-
* (那还会额外装配 Eval 源码标注与 ExecutionTree,这里只要四个布尔位)。
|
|
362
|
-
*/
|
|
363
|
-
capabilities: AttemptEvidenceCapabilities;
|
|
364
375
|
}
|
|
365
376
|
|
|
366
377
|
/**
|
|
367
|
-
* `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)
|
|
368
|
-
* (
|
|
369
|
-
*
|
|
370
|
-
* 的全部 Attempt(升序,供进一步展开到 `AttemptList`)。
|
|
378
|
+
* `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)、该 Eval 内
|
|
379
|
+
* attempt 的平均耗时/成本(两级聚合引擎在单一 eval 上退化成组内均值),以及这道题的全部
|
|
380
|
+
* Attempt(升序,供进一步展开到 `AttemptList`)。失败原因属于各 Attempt,不在父行挑一轮重复。
|
|
371
381
|
*/
|
|
372
382
|
export interface ExperimentListEvalRow {
|
|
373
383
|
evalId: string;
|
|
374
384
|
/** 折叠判定(任一 attempt 通过则通过,否则取最严重的)。 */
|
|
375
385
|
verdict: Verdict;
|
|
376
|
-
reason?: string;
|
|
377
386
|
/** 这道题内 attempt 的平均耗时(`computeCell(durationMs, …)`,单一 eval 分组下即均值)。 */
|
|
378
387
|
duration: MetricCell;
|
|
379
388
|
/** 这道题内 attempt 的平均成本。 */
|
|
@@ -385,7 +394,7 @@ export interface ExperimentListEvalRow {
|
|
|
385
394
|
/**
|
|
386
395
|
* `ExperimentList.data(selection)` 的一项 = 一个 experiment:身份(experimentId/agent/model)、
|
|
387
396
|
* 声明的 flags、Eval 判定构成(`foldEvalVerdict` 计票,与 view 榜单同一口径)、官方两级聚合
|
|
388
|
-
* 汇总指标(
|
|
397
|
+
* 汇总指标(endToEndPassRate/cost/duration/tokens,直接来自 `computeCell`,不现场重算),以及展开到
|
|
389
398
|
* 这个 experiment 每道 Eval 的 `evalRows`(按 eval id 升序)。
|
|
390
399
|
*/
|
|
391
400
|
export interface ExperimentListItem {
|
|
@@ -395,7 +404,7 @@ export interface ExperimentListItem {
|
|
|
395
404
|
flags?: Record<string, unknown>;
|
|
396
405
|
/** eval 级折叠计票(foldEvalVerdict 口径,与 `TableRowMeta.verdicts`、view 榜单同一套)。 */
|
|
397
406
|
verdicts: { passed: number; failed: number; errored: number; skipped: number };
|
|
398
|
-
/**
|
|
407
|
+
/** 官方两级聚合口径(endToEndPassRate),与 `MetricTable.data(..., columns: [endToEndPassRate])` 同一台引擎。 */
|
|
399
408
|
passRate: MetricCell;
|
|
400
409
|
cost: MetricCell;
|
|
401
410
|
duration: MetricCell;
|
|
@@ -413,14 +422,13 @@ export interface ExperimentListItem {
|
|
|
413
422
|
/**
|
|
414
423
|
* `EvalList.data(selection)` 的一项 = 一个 `experimentId + evalId`(同一个 Eval 跑在两个
|
|
415
424
|
* experiment 上是两条不同结果,不合并)。判定、分数(examScore 的两级聚合)、这道题内 attempt
|
|
416
|
-
*
|
|
417
|
-
*
|
|
425
|
+
* 的平均耗时/成本,外加展开到这道题全部 Attempt 的 `attempts`(按 attempt 序号升序)。失败
|
|
426
|
+
* 原因只存在于各 `AttemptListItem` 的 error / assertions,不会在 Eval 父项重复一份。
|
|
418
427
|
*/
|
|
419
428
|
export interface EvalListItem {
|
|
420
429
|
evalId: string;
|
|
421
430
|
experimentId: string;
|
|
422
431
|
verdict: Verdict;
|
|
423
|
-
reason?: string;
|
|
424
432
|
/** examScore 的两级聚合;单一 eval 分组下即这道题的题级分数。 */
|
|
425
433
|
score: MetricCell;
|
|
426
434
|
duration: MetricCell;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/results/cases.md
|
|
1
2
|
// buildAnnotatedEvalSource 的单测(定稿见 docs/concepts.md「标注 Eval 源码」)。
|
|
2
3
|
// 覆盖:同一行多条断言、无 loc 断言进 unmapped、loc 指向别的文件/越界行也进 unmapped
|
|
3
4
|
// (never silently dropped 的边界情况)、空断言数组、summary 计数、哈希与归一化行为。
|
|
4
5
|
|
|
5
6
|
import { describe, expect, it } from "vitest";
|
|
6
|
-
import type { AssertionResult } from "../types.ts";
|
|
7
|
-
import { buildAnnotatedEvalSource } from "./annotated-source.ts";
|
|
7
|
+
import type { AssertionResult, PhaseTiming, StreamEvent } from "../types.ts";
|
|
8
|
+
import { buildAnnotatedEvalSource, deriveSendAnnotations, type SendAnnotation } from "./annotated-source.ts";
|
|
8
9
|
import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
|
|
9
10
|
|
|
10
11
|
const SOURCE_PATH = "evals/weather.eval.ts";
|
|
@@ -13,9 +14,9 @@ function assertion(over: Partial<AssertionResult> & Pick<AssertionResult, "name"
|
|
|
13
14
|
return {
|
|
14
15
|
severity: "soft",
|
|
15
16
|
score: 1,
|
|
16
|
-
|
|
17
|
+
outcome: "passed" as const,
|
|
17
18
|
...over,
|
|
18
|
-
};
|
|
19
|
+
} as AssertionResult;
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
describe("buildAnnotatedEvalSource", () => {
|
|
@@ -94,10 +95,10 @@ describe("buildAnnotatedEvalSource", () => {
|
|
|
94
95
|
it("computes summary counts across passed/failed and gate/soft, mapped and unmapped alike", () => {
|
|
95
96
|
const content = "line 1\nline 2\n";
|
|
96
97
|
const assertions = [
|
|
97
|
-
assertion({ name: "a", severity: "gate",
|
|
98
|
-
assertion({ name: "b", severity: "gate",
|
|
99
|
-
assertion({ name: "c", severity: "soft",
|
|
100
|
-
assertion({ name: "d", severity: "soft",
|
|
98
|
+
assertion({ name: "a", severity: "gate", outcome: "passed" as const, loc: { file: SOURCE_PATH, line: 1 } }),
|
|
99
|
+
assertion({ name: "b", severity: "gate", outcome: "failed" as const, loc: { file: SOURCE_PATH, line: 1 } }),
|
|
100
|
+
assertion({ name: "c", severity: "soft", outcome: "passed" as const, loc: { file: SOURCE_PATH, line: 2 } }),
|
|
101
|
+
assertion({ name: "d", severity: "soft", outcome: "failed" as const }), // unmapped
|
|
101
102
|
];
|
|
102
103
|
|
|
103
104
|
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, assertions);
|
|
@@ -129,7 +130,7 @@ describe("buildAnnotatedEvalSource", () => {
|
|
|
129
130
|
|
|
130
131
|
it("treats an empty source file as a single empty line", () => {
|
|
131
132
|
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content: "" }, []);
|
|
132
|
-
expect(model.lines).toEqual([{ line: 1, text: "", assertions: [] }]);
|
|
133
|
+
expect(model.lines).toEqual([{ line: 1, text: "", assertions: [], sends: [] }]);
|
|
133
134
|
});
|
|
134
135
|
|
|
135
136
|
it("sourceSha256 matches hashEvalSource(normalizeEvalSource(content)) and is stable across CRLF/LF", () => {
|
|
@@ -143,4 +144,56 @@ describe("buildAnnotatedEvalSource", () => {
|
|
|
143
144
|
expect(modelLf.sourceSha256).toBe(modelCrlf.sourceSha256);
|
|
144
145
|
expect(modelLf.lines.map((l) => l.text)).toEqual(modelCrlf.lines.map((l) => l.text));
|
|
145
146
|
});
|
|
147
|
+
|
|
148
|
+
it("send 标注按 loc 落到对应行,一行多轮逐轮保留;别的文件或越界行直接丢(全量面在 --execution)", () => {
|
|
149
|
+
const content = "await t.send('a');\nawait t.send('b');\n";
|
|
150
|
+
const sends: SendAnnotation[] = [
|
|
151
|
+
{ label: "s1/t1", status: "completed", durationMs: 1200, loc: { file: SOURCE_PATH, line: 1, column: 9 } },
|
|
152
|
+
{ label: "s1/t2", status: "failed", durationMs: 800, loc: { file: SOURCE_PATH, line: 1, column: 9 } },
|
|
153
|
+
{ label: "s1/t3", status: "completed", loc: { file: "evals/other.eval.ts", line: 1 } },
|
|
154
|
+
{ label: "s1/t4", status: "completed", loc: { file: SOURCE_PATH, line: 99 } },
|
|
155
|
+
];
|
|
156
|
+
const model = buildAnnotatedEvalSource({ path: SOURCE_PATH, content }, [], sends);
|
|
157
|
+
expect(model.lines[0]!.sends.map((s) => s.label)).toEqual(["s1/t1", "s1/t2"]);
|
|
158
|
+
expect(model.lines[1]!.sends).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe("deriveSendAnnotations", () => {
|
|
163
|
+
it("第 i 条用户消息配第 i 个 turn 节点(与 --execution 分轮同一规则);无 loc 的轮不产出", () => {
|
|
164
|
+
const events: StreamEvent[] = [
|
|
165
|
+
{ type: "message", role: "user", text: "first", loc: { file: SOURCE_PATH, line: 3, column: 5 } },
|
|
166
|
+
{ type: "message", role: "assistant", text: "reply" },
|
|
167
|
+
{ type: "message", role: "user", text: "second (no loc)" },
|
|
168
|
+
{ type: "message", role: "user", text: "third", loc: { file: SOURCE_PATH, line: 9, column: 5 } },
|
|
169
|
+
];
|
|
170
|
+
const phases: PhaseTiming[] = [{
|
|
171
|
+
name: "eval.run" as PhaseTiming["name"],
|
|
172
|
+
durationMs: 5000,
|
|
173
|
+
children: [
|
|
174
|
+
{ id: "n1", kind: "command", label: "git", startOffsetMs: 0, durationMs: 10 },
|
|
175
|
+
{ id: "n2", kind: "turn", label: "s1/t1", startOffsetMs: 10, durationMs: 1500 },
|
|
176
|
+
{ id: "n3", kind: "turn", label: "s1/t2", startOffsetMs: 1510, durationMs: 900, failed: true },
|
|
177
|
+
{ id: "n4", kind: "turn", label: "s1/t3", startOffsetMs: 2410, durationMs: 300 },
|
|
178
|
+
],
|
|
179
|
+
}];
|
|
180
|
+
|
|
181
|
+
const sends = deriveSendAnnotations(events, phases);
|
|
182
|
+
expect(sends).toEqual([
|
|
183
|
+
{ label: "s1/t1", status: "completed", durationMs: 1500, loc: { file: SOURCE_PATH, line: 3, column: 5 } },
|
|
184
|
+
// 第二条用户消息没有 loc → 不产出;第三条配第 3 个 turn 节点,不因跳过而错位
|
|
185
|
+
{ label: "s1/t3", status: "completed", durationMs: 300, loc: { file: SOURCE_PATH, line: 9, column: 5 } },
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("时间树缺 turn 节点时回退 t<i> 标签、无墙钟;没有事件时为空", () => {
|
|
190
|
+
const events: StreamEvent[] = [
|
|
191
|
+
{ type: "message", role: "user", text: "only", loc: { file: SOURCE_PATH, line: 1 } },
|
|
192
|
+
];
|
|
193
|
+
expect(deriveSendAnnotations(events, undefined)).toEqual([
|
|
194
|
+
{ label: "t1", status: "completed", loc: { file: SOURCE_PATH, line: 1 } },
|
|
195
|
+
]);
|
|
196
|
+
expect(deriveSendAnnotations(null, undefined)).toEqual([]);
|
|
197
|
+
expect(deriveSendAnnotations([], undefined)).toEqual([]);
|
|
198
|
+
});
|
|
146
199
|
});
|