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.
- package/dist/agents/types.d.ts +67 -5
- 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 -1
- package/dist/report/built-ins/experiment-comparison.js +116 -10
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +1 -1
- package/dist/report/components.d.ts +8 -2
- package/dist/report/components.js +3 -3
- package/dist/report/compute.d.ts +11 -18
- package/dist/report/compute.js +54 -34
- package/dist/report/flag.d.ts +16 -1
- package/dist/report/flag.js +19 -1
- package/dist/report/format.d.ts +16 -8
- package/dist/report/format.js +27 -12
- package/dist/report/index.d.ts +4 -3
- package/dist/report/index.js +5 -4
- package/dist/report/locale.d.ts +11 -2
- package/dist/report/locale.js +23 -5
- package/dist/report/metrics.d.ts +13 -1
- package/dist/report/metrics.js +65 -14
- package/dist/report/primitives.d.ts +6 -0
- package/dist/report/react/AttemptList.d.ts +2 -2
- package/dist/report/react/AttemptList.js +5 -6
- 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 +2 -1
- package/dist/report/react/ExperimentList.js +8 -10
- package/dist/report/react/MetricScatter.js +5 -11
- 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 +21 -14
- 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 +42 -41
- package/dist/report/text/table.js +36 -5
- package/dist/report/types.d.ts +39 -21
- 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 +22 -4
- package/docs-site/zh/concepts/experiment.mdx +1 -1
- package/docs-site/zh/concepts/overview.mdx +6 -6
- package/docs-site/zh/guides/agent-feedback-loop.mdx +28 -26
- 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/official-adapters.mdx +26 -2
- package/docs-site/zh/guides/publish-report.mdx +30 -16
- package/docs-site/zh/guides/report-components.mdx +42 -30
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +17 -9
- package/docs-site/zh/guides/runner.mdx +17 -7
- package/docs-site/zh/guides/sandbox-agent.mdx +56 -7
- package/docs-site/zh/guides/sandbox-providers.mdx +257 -9
- package/docs-site/zh/guides/scoring-guide.mdx +4 -4
- package/docs-site/zh/guides/viewing-results.mdx +79 -36
- package/docs-site/zh/guides/write-experiment.mdx +5 -3
- package/docs-site/zh/guides/write-send.mdx +17 -1
- 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 +33 -7
- package/docs-site/zh/reference/define-agent.mdx +57 -4
- 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/expect.mdx +26 -1
- 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 +68 -5
- 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 +64 -2
- package/src/i18n/zh-CN.ts +65 -3
- 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 +110 -153
- package/src/report/built-ins/experiment-comparison.tsx +173 -13
- package/src/report/built-ins/index.ts +6 -1
- package/src/report/components.tsx +9 -3
- package/src/report/compute.ts +70 -40
- package/src/report/dual-render.test.tsx +194 -67
- package/src/report/flag.ts +30 -2
- package/src/report/format.ts +35 -11
- package/src/report/index.ts +22 -4
- package/src/report/locale.ts +25 -5
- package/src/report/metrics.ts +67 -14
- package/src/report/primitives.tsx +6 -0
- package/src/report/react/AttemptList.tsx +6 -31
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentComparison.tsx +68 -0
- package/src/report/react/ExperimentList.tsx +15 -9
- package/src/report/react/MetricScatter.tsx +12 -14
- 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 +33 -1
- package/src/report/react/fixtures.ts +24 -17
- package/src/report/react/render.test.tsx +9 -64
- package/src/report/react/styles.css +73 -2
- package/src/report/report.test.ts +306 -98
- package/src/report/report.ts +6 -2
- package/src/report/text/faces.ts +47 -43
- package/src/report/text/table.ts +42 -5
- package/src/report/types.ts +41 -21
- 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 +8 -6
- 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 +21 -1
- package/src/show/render.ts +619 -104
- package/src/show/show.test.ts +235 -19
- 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 +1 -0
- package/src/view/data.ts +11 -1
- package/src/view/index.ts +11 -0
- package/src/view/server.ts +2 -0
- package/src/view/styles.css +3 -0
- package/src/view/view-report.test.ts +6 -5
- 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
|
@@ -23,6 +23,7 @@ import type { TextContext } from "../tree.ts";
|
|
|
23
23
|
import type { TableColumn, TableRow } from "../primitives.tsx";
|
|
24
24
|
import {
|
|
25
25
|
attemptItemReason,
|
|
26
|
+
experimentDisplayName,
|
|
26
27
|
formatDurationMs,
|
|
27
28
|
formatMetricValue,
|
|
28
29
|
formatPlainNumber,
|
|
@@ -30,7 +31,7 @@ import {
|
|
|
30
31
|
verdictMark,
|
|
31
32
|
} from "../format.ts";
|
|
32
33
|
import { countText, localeText, resolveMetricLabel, type ReportLocale } from "../locale.ts";
|
|
33
|
-
import { indentBlock, padDisplay, textBar, wrapDisplay } from "./layout.ts";
|
|
34
|
+
import { indentBlock, padDisplay, stringWidth, textBar, wrapDisplay } from "./layout.ts";
|
|
34
35
|
import { renderTableText } from "./table.ts";
|
|
35
36
|
import { renderCharPlot, renderCoordinateTable, type PlotPoint } from "./plot.ts";
|
|
36
37
|
|
|
@@ -447,7 +448,7 @@ export function deltaText(data: DeltaData, ctx: TextContext): string {
|
|
|
447
448
|
// 三面共用的紧凑标记:`locator✓`(判定符紧跟 locator,中间不留空格)。
|
|
448
449
|
// ExperimentList / EvalList 逐 attempt 只列这一个标记 + 各自的原因/耗时摘要,不重复整段
|
|
449
450
|
// niceeval show 命令;要看某个 attempt 的完整证据,agent 自己拼 `niceeval show <locator>`——
|
|
450
|
-
//
|
|
451
|
+
// 比较列表都不内联完整断言或命令模板。
|
|
451
452
|
|
|
452
453
|
function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict"] }): string {
|
|
453
454
|
return `${item.locator}${verdictMark(item.verdict)}`;
|
|
@@ -455,7 +456,7 @@ function locatorBadge(item: { locator: string; verdict: AttemptListItem["verdict
|
|
|
455
456
|
|
|
456
457
|
// ── ExperimentList ──
|
|
457
458
|
|
|
458
|
-
function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext): string {
|
|
459
|
+
function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
459
460
|
const locale = ctx.locale;
|
|
460
461
|
const compact = ctx.width < 100;
|
|
461
462
|
const columns: TableColumn[] = [
|
|
@@ -471,7 +472,7 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext):
|
|
|
471
472
|
const rows: TableRow[] = items.map((item) => ({
|
|
472
473
|
key: item.experimentId,
|
|
473
474
|
cells: {
|
|
474
|
-
experiment: item.experimentId,
|
|
475
|
+
experiment: experimentDisplayName(item.experimentId, relativeTo),
|
|
475
476
|
model: item.model ?? localeText(locale, "experimentList.defaultModel"),
|
|
476
477
|
agent: item.agent,
|
|
477
478
|
duration: cellText(item.duration),
|
|
@@ -483,22 +484,35 @@ function experimentSummaryTable(items: ExperimentListItem[], ctx: TextContext):
|
|
|
483
484
|
}));
|
|
484
485
|
const metadata = items.flatMap((item) =>
|
|
485
486
|
wrapDisplay(
|
|
486
|
-
`${item.experimentId}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
|
|
487
|
+
`${experimentDisplayName(item.experimentId, relativeTo)}: ${localeText(locale, "overview.evalsCount", { n: item.evals })} · ${localeText(locale, "overview.attemptsCount", { n: item.attempts })} · ${item.lastRunAt}`,
|
|
487
488
|
Math.max(8, ctx.width - 2),
|
|
488
489
|
).map((line) => ` ${line}`),
|
|
489
490
|
);
|
|
490
491
|
return [renderTableText({ columns, rows, locale }, ctx), metadata.join("\n")].join("\n");
|
|
491
492
|
}
|
|
492
493
|
|
|
493
|
-
function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): string {
|
|
494
|
+
function experimentDetailTable(item: ExperimentListItem, ctx: TextContext, relativeTo?: string): string {
|
|
494
495
|
const locale = ctx.locale;
|
|
495
496
|
const columns: TableColumn[] = [
|
|
496
497
|
{ key: "status", header: localeText(locale, "experimentList.status") },
|
|
497
498
|
{ key: "entity", header: localeText(locale, "experimentList.evalAttempt") },
|
|
498
|
-
|
|
499
|
+
// Result 是可扫读的失败预览,不是证据面:两行放不下的以 … 收口,完整值走 locator 下钻。
|
|
500
|
+
{ key: "result", header: localeText(locale, "experimentList.result"), maxLines: 2 },
|
|
499
501
|
{ key: "duration", header: localeText(locale, "experimentList.duration"), align: "right" },
|
|
500
502
|
{ key: "cost", header: localeText(locale, "experimentList.cost"), align: "right" },
|
|
501
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);
|
|
502
516
|
const rows: TableRow[] = item.evalRows.flatMap((row) => {
|
|
503
517
|
const parent: TableRow = {
|
|
504
518
|
key: row.evalId,
|
|
@@ -506,8 +520,8 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): stri
|
|
|
506
520
|
status: `${verdictMark(row.verdict)} ${localeText(locale, `verdict.${row.verdict}`)}`,
|
|
507
521
|
entity: row.evalId,
|
|
508
522
|
result: "",
|
|
509
|
-
duration: "",
|
|
510
|
-
cost: "",
|
|
523
|
+
duration: localeText(locale, "entityList.average", { value: cellText(row.duration) }),
|
|
524
|
+
cost: localeText(locale, "entityList.average", { value: cellText(row.cost) }),
|
|
511
525
|
},
|
|
512
526
|
};
|
|
513
527
|
const attempts: TableRow[] = row.attempts.map((attempt, index) => ({
|
|
@@ -515,7 +529,7 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): stri
|
|
|
515
529
|
cells: {
|
|
516
530
|
status: ` ${verdictMark(attempt.verdict)}`,
|
|
517
531
|
entity: `${index === row.attempts.length - 1 ? "└─" : "├─"} ${attempt.locator}`,
|
|
518
|
-
result: attemptItemReason(attempt) ?? MISSING_MARK,
|
|
532
|
+
result: attemptItemReason(attempt, resultBudget) ?? MISSING_MARK,
|
|
519
533
|
duration: attempt.verdict === "skipped" && attempt.durationMs === 0 ? null : formatDurationMs(attempt.durationMs),
|
|
520
534
|
cost: attempt.costUSD === undefined ? null : formatUSD(attempt.costUSD),
|
|
521
535
|
},
|
|
@@ -525,18 +539,24 @@ function experimentDetailTable(item: ExperimentListItem, ctx: TextContext): stri
|
|
|
525
539
|
const flags = item.flags && Object.keys(item.flags).length > 0
|
|
526
540
|
? `${localeText(locale, "experimentList.flags")} ${Object.entries(item.flags).map(([key, value]) => `${key}=${String(value)}`).join(" · ")}`
|
|
527
541
|
: undefined;
|
|
528
|
-
return [item.experimentId, flags, renderTableText({ columns, rows, locale }, ctx)]
|
|
542
|
+
return [experimentDisplayName(item.experimentId, relativeTo), flags, renderTableText({ columns, rows, locale }, ctx)]
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.join("\n");
|
|
529
545
|
}
|
|
530
546
|
|
|
531
|
-
export function experimentListText(items: ExperimentListItem[], ctx: TextContext): string {
|
|
547
|
+
export function experimentListText(items: ExperimentListItem[], ctx: TextContext, relativeTo?: string): string {
|
|
532
548
|
if (items.length === 0) return localeText(ctx.locale, "attemptList.empty");
|
|
533
|
-
return [
|
|
549
|
+
return [
|
|
550
|
+
experimentSummaryTable(items, ctx, relativeTo),
|
|
551
|
+
...items.map((item) => experimentDetailTable(item, ctx, relativeTo)),
|
|
552
|
+
].join("\n\n");
|
|
534
553
|
}
|
|
535
554
|
|
|
536
555
|
// ── EvalList ──
|
|
537
556
|
|
|
538
|
-
function evalListAttemptLine(item: AttemptListItem): string {
|
|
539
|
-
|
|
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));
|
|
540
560
|
return ` ${locatorBadge(item)}${reason ? ` · ${reason}` : ""}`;
|
|
541
561
|
}
|
|
542
562
|
|
|
@@ -548,10 +568,14 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
|
548
568
|
const summary = [
|
|
549
569
|
localeText(locale, "attemptList.score", { score: cellText(item.score) }),
|
|
550
570
|
localeText(locale, "overview.attemptsCount", { n: item.attempts.length }),
|
|
551
|
-
|
|
552
|
-
|
|
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
|
+
}),
|
|
553
577
|
].join(" · ");
|
|
554
|
-
const attemptLines = item.attempts.map(evalListAttemptLine);
|
|
578
|
+
const attemptLines = item.attempts.map((attempt) => evalListAttemptLine(attempt, ctx));
|
|
555
579
|
return [identity, ` ${summary}`, ...attemptLines].join("\n");
|
|
556
580
|
});
|
|
557
581
|
return blocks.join("\n\n");
|
|
@@ -559,9 +583,8 @@ export function evalListText(items: EvalListItem[], ctx: TextContext): string {
|
|
|
559
583
|
|
|
560
584
|
// ── AttemptList ──
|
|
561
585
|
|
|
562
|
-
/**
|
|
563
|
-
|
|
564
|
-
function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: ReportLocale): string {
|
|
586
|
+
/** Attempt 比较卡片:只显示一条主失败摘要(至多两行终端宽);完整 assertions 走 locator 下钻。 */
|
|
587
|
+
function attemptListItemText(item: AttemptListItem, ctx: TextContext): string {
|
|
565
588
|
const head = [
|
|
566
589
|
`${verdictMark(item.verdict)} ${item.locator}`,
|
|
567
590
|
item.evalId,
|
|
@@ -570,34 +593,15 @@ function attemptListItemText(item: AttemptListItem, ctx: TextContext, locale: Re
|
|
|
570
593
|
...(item.costUSD !== undefined ? [formatUSD(item.costUSD)] : []),
|
|
571
594
|
].join(" · ");
|
|
572
595
|
const lines = [head];
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
}
|
|
576
|
-
for (const assertion of item.assertions) {
|
|
577
|
-
const scoreText =
|
|
578
|
-
assertion.threshold !== undefined
|
|
579
|
-
? `${formatPlainNumber(assertion.score)}/${formatPlainNumber(assertion.threshold)}`
|
|
580
|
-
: formatPlainNumber(assertion.score);
|
|
581
|
-
lines.push(
|
|
582
|
-
` ${assertion.severity} ${assertion.name} · ${localeText(locale, `verdict.${assertion.passed ? "passed" : "failed"}`)}${assertion.severity === "soft" ? ` ${scoreText}` : ""}`,
|
|
583
|
-
);
|
|
584
|
-
if (assertion.detail) lines.push(indentBlock(wrapDisplay(assertion.detail, ctx.width - 4).join("\n"), " "));
|
|
585
|
-
if (assertion.evidence) {
|
|
586
|
-
const limit = Math.max(240, ctx.width * 5);
|
|
587
|
-
const evidence =
|
|
588
|
-
assertion.evidence.length <= limit
|
|
589
|
-
? assertion.evidence
|
|
590
|
-
: `${assertion.evidence.slice(0, limit)}… (${assertion.evidence.length - limit} more chars; open ${item.locator} for full evidence)`;
|
|
591
|
-
lines.push(indentBlock(wrapDisplay(evidence, ctx.width - 6).join("\n"), " "));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
596
|
+
const reason = attemptItemReason(item, Math.max(24, ctx.width * 2 - 4));
|
|
597
|
+
if (reason) lines.push(` ${reason}`);
|
|
594
598
|
return lines.join("\n");
|
|
595
599
|
}
|
|
596
600
|
|
|
597
601
|
export function attemptListText(items: AttemptListItem[], total: number | undefined, ctx: TextContext): string {
|
|
598
602
|
const locale = ctx.locale;
|
|
599
603
|
if (items.length === 0) return localeText(locale, "attemptList.empty");
|
|
600
|
-
const blocks = items.map((item) => attemptListItemText(item, ctx
|
|
604
|
+
const blocks = items.map((item) => attemptListItemText(item, ctx));
|
|
601
605
|
const remaining = (total ?? items.length) - items.length;
|
|
602
606
|
if (remaining > 0) blocks.push(localeText(locale, "attemptList.truncatedText", { n: remaining }));
|
|
603
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,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AttemptHandle, SelectionWarning } from "../results/types.ts";
|
|
6
6
|
import type { AttemptLocator } from "../results/locator.ts";
|
|
7
|
-
import type { AssertionResult, Verdict } from "../types.ts";
|
|
7
|
+
import type { AssertionResult, AttemptError, DiagnosticRecord, Verdict } from "../types.ts";
|
|
8
8
|
import type { LocalizedLabel, ReportLocale } from "./locale.ts";
|
|
9
9
|
|
|
10
10
|
export type { SelectionWarning };
|
|
@@ -31,7 +31,7 @@ export interface MetricAggregate {
|
|
|
31
31
|
* 指标:纯函数,吃一个 AttemptHandle 吐一个值(null = 此 attempt 测不了这个指标,
|
|
32
32
|
* 不进聚合;0 = 测了结果是零,照常进),外加名字、两级聚合方式和渲染提示。
|
|
33
33
|
* 内置指标与自定义指标是同一个类型,没有特权。name 走字面量泛型:列键锚在指标
|
|
34
|
-
* 对象上(`row.cells[
|
|
34
|
+
* 对象上(`row.cells[taskPassRate.name]`),拼错列名编译不过。
|
|
35
35
|
*/
|
|
36
36
|
export interface Metric<Name extends string = string> {
|
|
37
37
|
/** MetricColumn.key 与列头的来源;同一次计算里重名是错误。 */
|
|
@@ -86,8 +86,24 @@ export interface FlagRef {
|
|
|
86
86
|
readonly unit?: string;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
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;
|
|
91
107
|
|
|
92
108
|
// ───────────────────────── 计算产物(组件 data props)─────────────────────────
|
|
93
109
|
|
|
@@ -265,7 +281,7 @@ export interface GroupSummaryData {
|
|
|
265
281
|
/**
|
|
266
282
|
* 组的通过率:eval 级折叠计票的 `passed / (passed + failed + errored)`(`skipped` 不进
|
|
267
283
|
* 分母)——这是旧 `GroupSelector` 卡片的口径,不是 `OverviewData.totals.passRate` 那种
|
|
268
|
-
* `computeCell` 两级聚合(两者服务不同问题:「这组题多少算过」vs
|
|
284
|
+
* `computeCell` 两级聚合(两者服务不同问题:「这组题多少算过」vs「每次运行成功多少」)。
|
|
269
285
|
* 分母为 0(组内没有任何已跑的 eval)时 `value` 为 `null`,不编 0%。
|
|
270
286
|
*/
|
|
271
287
|
passRate: MetricCell;
|
|
@@ -289,10 +305,11 @@ export interface OverviewData {
|
|
|
289
305
|
errored: number;
|
|
290
306
|
skipped: number;
|
|
291
307
|
/**
|
|
292
|
-
*
|
|
293
|
-
* columns: [
|
|
308
|
+
* 默认成功率的唯一官方口径:`computeCell(endToEndPassRate, items)`,与 `MetricTable.data(...,
|
|
309
|
+
* columns: [endToEndPassRate])` 同一台两级聚合引擎(题内折叠 perEval、跨题折叠 across,默认都是
|
|
294
310
|
* mean)——一道题内多个 attempt 部分通过,贡献的是小数份额而不是二元票。`samples`/`total`
|
|
295
|
-
* 是两级聚合口径下的 attempt 计数(`total` 含 skipped
|
|
311
|
+
* 是两级聚合口径下的 attempt 计数(`total` 含 skipped 与 errored——endToEndPassRate 对
|
|
312
|
+
* errored 记 0、只对 skipped 记 null,`samples` 因此只不含 skipped),不等于上面四个
|
|
296
313
|
* verdict 计票的任何一个之和。
|
|
297
314
|
*/
|
|
298
315
|
passRate: MetricCell;
|
|
@@ -335,8 +352,11 @@ export interface DeltaData<K extends string = string> {
|
|
|
335
352
|
// `EvalListItem.attempts` 的元素,报告作者可以直接把这些嵌套数组喂给 `<AttemptList items={...} />`。
|
|
336
353
|
|
|
337
354
|
/**
|
|
338
|
-
* `AttemptList` 一项 = 一个 Attempt
|
|
339
|
-
*
|
|
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」)。
|
|
340
360
|
*/
|
|
341
361
|
export interface AttemptListItem {
|
|
342
362
|
evalId: string;
|
|
@@ -344,7 +364,10 @@ export interface AttemptListItem {
|
|
|
344
364
|
attempt: number;
|
|
345
365
|
agent: string;
|
|
346
366
|
verdict: Verdict;
|
|
347
|
-
error
|
|
367
|
+
/** 结构化执行错误(与 `EvalResult.error` 同构):列表只显示 `message` 一层摘要。 */
|
|
368
|
+
error?: AttemptError;
|
|
369
|
+
/** 本 attempt 的有界诊断(teardown / cleanup 失败等,与 verdict 独立);属于下钻详情。 */
|
|
370
|
+
diagnostics?: DiagnosticRecord[];
|
|
348
371
|
assertions: AssertionResult[];
|
|
349
372
|
durationMs: number;
|
|
350
373
|
costUSD?: number;
|
|
@@ -352,16 +375,14 @@ export interface AttemptListItem {
|
|
|
352
375
|
}
|
|
353
376
|
|
|
354
377
|
/**
|
|
355
|
-
* `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)
|
|
356
|
-
* (
|
|
357
|
-
*
|
|
358
|
-
* 的全部 Attempt(升序,供进一步展开到 `AttemptList`)。
|
|
378
|
+
* `ExperimentList` 一项里,一个 Eval 的展开行:折叠判定(`foldEvalVerdict`)、该 Eval 内
|
|
379
|
+
* attempt 的平均耗时/成本(两级聚合引擎在单一 eval 上退化成组内均值),以及这道题的全部
|
|
380
|
+
* Attempt(升序,供进一步展开到 `AttemptList`)。失败原因属于各 Attempt,不在父行挑一轮重复。
|
|
359
381
|
*/
|
|
360
382
|
export interface ExperimentListEvalRow {
|
|
361
383
|
evalId: string;
|
|
362
384
|
/** 折叠判定(任一 attempt 通过则通过,否则取最严重的)。 */
|
|
363
385
|
verdict: Verdict;
|
|
364
|
-
reason?: string;
|
|
365
386
|
/** 这道题内 attempt 的平均耗时(`computeCell(durationMs, …)`,单一 eval 分组下即均值)。 */
|
|
366
387
|
duration: MetricCell;
|
|
367
388
|
/** 这道题内 attempt 的平均成本。 */
|
|
@@ -373,7 +394,7 @@ export interface ExperimentListEvalRow {
|
|
|
373
394
|
/**
|
|
374
395
|
* `ExperimentList.data(selection)` 的一项 = 一个 experiment:身份(experimentId/agent/model)、
|
|
375
396
|
* 声明的 flags、Eval 判定构成(`foldEvalVerdict` 计票,与 view 榜单同一口径)、官方两级聚合
|
|
376
|
-
* 汇总指标(
|
|
397
|
+
* 汇总指标(endToEndPassRate/cost/duration/tokens,直接来自 `computeCell`,不现场重算),以及展开到
|
|
377
398
|
* 这个 experiment 每道 Eval 的 `evalRows`(按 eval id 升序)。
|
|
378
399
|
*/
|
|
379
400
|
export interface ExperimentListItem {
|
|
@@ -383,7 +404,7 @@ export interface ExperimentListItem {
|
|
|
383
404
|
flags?: Record<string, unknown>;
|
|
384
405
|
/** eval 级折叠计票(foldEvalVerdict 口径,与 `TableRowMeta.verdicts`、view 榜单同一套)。 */
|
|
385
406
|
verdicts: { passed: number; failed: number; errored: number; skipped: number };
|
|
386
|
-
/**
|
|
407
|
+
/** 官方两级聚合口径(endToEndPassRate),与 `MetricTable.data(..., columns: [endToEndPassRate])` 同一台引擎。 */
|
|
387
408
|
passRate: MetricCell;
|
|
388
409
|
cost: MetricCell;
|
|
389
410
|
duration: MetricCell;
|
|
@@ -401,14 +422,13 @@ export interface ExperimentListItem {
|
|
|
401
422
|
/**
|
|
402
423
|
* `EvalList.data(selection)` 的一项 = 一个 `experimentId + evalId`(同一个 Eval 跑在两个
|
|
403
424
|
* experiment 上是两条不同结果,不合并)。判定、分数(examScore 的两级聚合)、这道题内 attempt
|
|
404
|
-
*
|
|
405
|
-
*
|
|
425
|
+
* 的平均耗时/成本,外加展开到这道题全部 Attempt 的 `attempts`(按 attempt 序号升序)。失败
|
|
426
|
+
* 原因只存在于各 `AttemptListItem` 的 error / assertions,不会在 Eval 父项重复一份。
|
|
406
427
|
*/
|
|
407
428
|
export interface EvalListItem {
|
|
408
429
|
evalId: string;
|
|
409
430
|
experimentId: string;
|
|
410
431
|
verdict: Verdict;
|
|
411
|
-
reason?: string;
|
|
412
432
|
/** examScore 的两级聚合;单一 eval 分组下即这道题的题级分数。 */
|
|
413
433
|
score: MetricCell;
|
|
414
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
|
});
|