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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// 证据覆盖的解析与折叠:Agent 级默认 + Turn 级降级 → attempt 级聚合
|
|
2
|
+
// (见 docs/feature/adapters/architecture/evidence.md「覆盖声明」)。
|
|
3
|
+
// 消费规则(正/负断言的三值折叠)在 scoped.ts;这里只管状态代数:
|
|
4
|
+
// unknown/unavailable < partial < complete,Turn 只能降级、聚合取最差。
|
|
5
|
+
|
|
6
|
+
import type { CoverageDeclaration, CoverageStatus, EvidenceCoverage } from "../agents/types.ts";
|
|
7
|
+
|
|
8
|
+
/** 证据通道全集(EvidenceCoverage 的键)。 */
|
|
9
|
+
export const COVERAGE_CHANNELS = ["events", "actions", "messages", "usage", "status", "data"] as const;
|
|
10
|
+
export type CoverageChannel = (typeof COVERAGE_CHANNELS)[number];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 解析后的通道状态:未声明按 "unknown" 落地(不是 complete)。unknown 与 unavailable 在
|
|
14
|
+
* 消费侧同样保守;区别只在展示(unknown =「Adapter 没说」,unavailable =「Adapter 说了拿不到」)。
|
|
15
|
+
*/
|
|
16
|
+
export type ResolvedCoverageStatus = CoverageStatus | "unknown";
|
|
17
|
+
|
|
18
|
+
/** 解析后的单通道声明。 */
|
|
19
|
+
export interface ResolvedCoverageChannel {
|
|
20
|
+
status: ResolvedCoverageStatus;
|
|
21
|
+
reason?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 全通道解析后的覆盖:每个通道必有一个状态(缺省 unknown)。 */
|
|
25
|
+
export type ResolvedCoverage = Readonly<Record<CoverageChannel, ResolvedCoverageChannel>>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 官方 SDK 适配器用的「全通道 complete」常量:完整事件流、完整 steps/output、经过生命周期
|
|
29
|
+
* fixture 验证的 transcript 才可以声明它(见 docs/feature/adapters/architecture/evidence.md)。
|
|
30
|
+
*/
|
|
31
|
+
export const completeCoverage: EvidenceCoverage = Object.freeze<EvidenceCoverage>({
|
|
32
|
+
events: { status: "complete" },
|
|
33
|
+
actions: { status: "complete" },
|
|
34
|
+
messages: { status: "complete" },
|
|
35
|
+
usage: { status: "complete" },
|
|
36
|
+
status: { status: "complete" },
|
|
37
|
+
data: { status: "complete" },
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// 状态序:折叠一律取最差。unknown 与 unavailable 保守程度相同,并列最差;
|
|
41
|
+
// 并列时取「有声明的那一个」(unavailable 带 reason,信息量更大)。
|
|
42
|
+
const RANK: Record<ResolvedCoverageStatus, number> = {
|
|
43
|
+
complete: 3,
|
|
44
|
+
partial: 2,
|
|
45
|
+
unavailable: 1,
|
|
46
|
+
unknown: 1,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function worseOf(a: ResolvedCoverageChannel, b: ResolvedCoverageChannel): ResolvedCoverageChannel {
|
|
50
|
+
const ra = RANK[a.status];
|
|
51
|
+
const rb = RANK[b.status];
|
|
52
|
+
if (ra < rb) return a;
|
|
53
|
+
if (rb < ra) return b;
|
|
54
|
+
// 并列:优先「Adapter 说了拿不到」(unavailable / 带 reason)而不是「没说」。
|
|
55
|
+
if (a.status === "unknown" && b.status !== "unknown") return b;
|
|
56
|
+
if (b.status === "unknown" && a.status !== "unknown") return a;
|
|
57
|
+
return a.reason !== undefined ? a : b;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Agent 级声明 → 全通道解析(未声明通道 = unknown)。 */
|
|
61
|
+
export function resolveAgentCoverage(declared: EvidenceCoverage | undefined): ResolvedCoverage {
|
|
62
|
+
const out = {} as Record<CoverageChannel, ResolvedCoverageChannel>;
|
|
63
|
+
for (const ch of COVERAGE_CHANNELS) {
|
|
64
|
+
const d: CoverageDeclaration | undefined = declared?.[ch];
|
|
65
|
+
out[ch] = d ? { status: d.status, ...(d.reason !== undefined ? { reason: d.reason } : {}) } : { status: "unknown" };
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Turn 级降级:相对 base(Agent 默认)只能变差,不能升格——Turn 声明比 base 更好的状态
|
|
72
|
+
* 直接被 base 压住(min 语义天然满足「不能把 Agent 未声明的通道升格成 complete」)。
|
|
73
|
+
*/
|
|
74
|
+
export function downgradeCoverage(base: ResolvedCoverage, turn: EvidenceCoverage | undefined): ResolvedCoverage {
|
|
75
|
+
if (!turn) return base;
|
|
76
|
+
const out = {} as Record<CoverageChannel, ResolvedCoverageChannel>;
|
|
77
|
+
for (const ch of COVERAGE_CHANNELS) {
|
|
78
|
+
const d = turn[ch];
|
|
79
|
+
out[ch] = d ? worseOf(base[ch], { status: d.status, ...(d.reason !== undefined ? { reason: d.reason } : {}) }) : base[ch];
|
|
80
|
+
}
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** attempt / session 级聚合:各 turn 的最差值(unknown/unavailable < partial < complete)。 */
|
|
85
|
+
export function worstCoverage(list: readonly ResolvedCoverage[]): ResolvedCoverage {
|
|
86
|
+
if (list.length === 0) return resolveAgentCoverage(undefined);
|
|
87
|
+
let acc = list[0]!;
|
|
88
|
+
for (let i = 1; i < list.length; i++) {
|
|
89
|
+
const next = list[i]!;
|
|
90
|
+
const out = {} as Record<CoverageChannel, ResolvedCoverageChannel>;
|
|
91
|
+
for (const ch of COVERAGE_CHANNELS) out[ch] = worseOf(acc[ch], next[ch]);
|
|
92
|
+
acc = out;
|
|
93
|
+
}
|
|
94
|
+
return acc;
|
|
95
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// agent 归因 diff 的派生视图:从落盘事实(DiffWindow[])派生文件级摘要与终态读取。
|
|
2
|
+
// 派生物可随时重算、不落盘——符合「聚合在消费方」铁律(docs/feature/results/architecture.md「diff.json」)。
|
|
3
|
+
|
|
4
|
+
import type { DiffArtifact, DiffData, DiffFileSummary, DiffWindow, WindowChange } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/** 从窗口序列派生 DiffData:files 摘要(net / 触及窗口)+ get(最后触及窗口的终态)。 */
|
|
7
|
+
export function deriveDiffData(windows: DiffArtifact): DiffData {
|
|
8
|
+
const touched = new Map<string, { first: WindowChange; last: WindowChange; windows: string[]; binary: boolean }>();
|
|
9
|
+
for (const window of windows) {
|
|
10
|
+
for (const [path, change] of Object.entries(window.changes)) {
|
|
11
|
+
const entry = touched.get(path);
|
|
12
|
+
if (entry) {
|
|
13
|
+
entry.last = change;
|
|
14
|
+
entry.windows.push(window.window);
|
|
15
|
+
if (change.binary) entry.binary = true;
|
|
16
|
+
} else {
|
|
17
|
+
touched.set(path, { first: change, last: change, windows: [window.window], binary: change.binary !== undefined });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const files: Record<string, DiffFileSummary> = {};
|
|
23
|
+
for (const [path, entry] of touched) {
|
|
24
|
+
files[path] = {
|
|
25
|
+
net: computeNet(entry.first, entry.last),
|
|
26
|
+
windows: entry.windows,
|
|
27
|
+
...(entry.binary ? { binary: true as const } : {}),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
windows,
|
|
33
|
+
files,
|
|
34
|
+
get(path: string): string | undefined {
|
|
35
|
+
const entry = touched.get(path);
|
|
36
|
+
if (!entry) return undefined;
|
|
37
|
+
if (entry.last.status === "deleted") return undefined;
|
|
38
|
+
return entry.last.after;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 净效果:首个触及窗口的起点 vs 最后触及窗口的终点。 */
|
|
44
|
+
function computeNet(first: WindowChange, last: WindowChange): DiffFileSummary["net"] {
|
|
45
|
+
const existedBefore = first.status !== "added";
|
|
46
|
+
const existsAfter = last.status !== "deleted";
|
|
47
|
+
if (!existedBefore && !existsAfter) return "none"; // 创建又删除
|
|
48
|
+
if (!existedBefore && existsAfter) return "added";
|
|
49
|
+
if (existedBefore && !existsAfter) return "deleted";
|
|
50
|
+
// 改回原样 = none;比较首窗口起点与末窗口终点的内容(二进制按字节数近似)。
|
|
51
|
+
const beforeContent = first.before ?? first.binary?.beforeBytes;
|
|
52
|
+
const afterContent = last.after ?? last.binary?.afterBytes;
|
|
53
|
+
if (beforeContent !== undefined && afterContent !== undefined && beforeContent === afterContent) return "none";
|
|
54
|
+
return "modified";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** DiffArtifact 里是否有任何窗口触及过任何文件。 */
|
|
58
|
+
export function diffIsEmpty(diff: DiffData): boolean {
|
|
59
|
+
return Object.keys(diff.files).length === 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 正则是否命中任何被触及的路径或任何窗口的 before/after 内容。 */
|
|
63
|
+
export function diffMatches(diff: DiffData, re: RegExp): boolean {
|
|
64
|
+
for (const path of Object.keys(diff.files)) {
|
|
65
|
+
if (re.test(path)) return true;
|
|
66
|
+
}
|
|
67
|
+
for (const window of diff.windows) {
|
|
68
|
+
for (const change of Object.values(window.changes)) {
|
|
69
|
+
if (change.after !== undefined && re.test(change.after)) return true;
|
|
70
|
+
if (change.before !== undefined && re.test(change.before)) return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** 空 diff(remote / skipped attempt)。 */
|
|
77
|
+
export function emptyDiffData(): DiffData {
|
|
78
|
+
return deriveDiffData([]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export type { DiffArtifact, DiffData, DiffWindow, WindowChange } from "./types.ts";
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/scoring/cases.md
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import type { AssertionResult } from "./types.ts";
|
|
4
|
+
import {
|
|
5
|
+
assertionSummaryLines,
|
|
6
|
+
compactAssertionSummary,
|
|
7
|
+
fitCompactAssertionSummary,
|
|
8
|
+
primaryAssertionSummary,
|
|
9
|
+
} from "./display.ts";
|
|
10
|
+
|
|
11
|
+
describe("primaryAssertionSummary", () => {
|
|
12
|
+
it("选择第一条失败 gate,保留领域 group、matcher 与 expected/received,并只计数其余 gate", () => {
|
|
13
|
+
const assertions: AssertionResult[] = [
|
|
14
|
+
{ name: "style", severity: "soft", outcome: "failed", score: 0.2, threshold: 0.8 },
|
|
15
|
+
{
|
|
16
|
+
name: "equals(4)",
|
|
17
|
+
groupPath: ["Issue 15193: selected proposal matches the accepted proposal"],
|
|
18
|
+
severity: "gate",
|
|
19
|
+
outcome: "failed",
|
|
20
|
+
score: 0,
|
|
21
|
+
expected: "4",
|
|
22
|
+
received: "3",
|
|
23
|
+
},
|
|
24
|
+
{ name: "matches(schema)", severity: "gate", outcome: "failed", score: 0 },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const summary = primaryAssertionSummary(assertions, "failed");
|
|
28
|
+
expect(summary).toEqual({
|
|
29
|
+
severity: "gate",
|
|
30
|
+
assertion: "Issue 15193: selected proposal matches the accepted proposal",
|
|
31
|
+
matcher: "equals(4)",
|
|
32
|
+
expected: "4",
|
|
33
|
+
received: "3",
|
|
34
|
+
additionalFailures: 1,
|
|
35
|
+
});
|
|
36
|
+
expect(assertionSummaryLines(summary!)).toEqual([
|
|
37
|
+
"gate: Issue 15193: selected proposal matches the accepted proposal",
|
|
38
|
+
"equals(4) · expected 4 · received 3 · +1 more failures",
|
|
39
|
+
]);
|
|
40
|
+
expect(compactAssertionSummary(summary!)).toBe(
|
|
41
|
+
"gate: Issue 15193: selected proposal matches the accepted proposal · equals(4) · expected 4 · received 3 · +1 more failures",
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("无 group 时不重复 matcher;failed verdict 没有 gate 才选择 soft", () => {
|
|
46
|
+
const summary = primaryAssertionSummary(
|
|
47
|
+
[{ name: "similarity", severity: "soft", outcome: "failed", score: 0.71, threshold: 0.9 }],
|
|
48
|
+
"failed",
|
|
49
|
+
);
|
|
50
|
+
expect(compactAssertionSummary(summary!)).toBe("similarity · score 0.71 · threshold 0.9");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("errored 可由首条非 optional unavailable 解释,passed 不产生摘要", () => {
|
|
54
|
+
const assertions: AssertionResult[] = [
|
|
55
|
+
{ name: "failed gate is not the errored root cause", severity: "gate", outcome: "failed", score: 0 },
|
|
56
|
+
{ name: "optional judge", severity: "soft", optional: true, outcome: "unavailable", reason: "no-key" },
|
|
57
|
+
{ name: "required judge", severity: "gate", outcome: "unavailable", reason: "judge-model-unresolved" },
|
|
58
|
+
];
|
|
59
|
+
expect(primaryAssertionSummary(assertions, "errored")).toMatchObject({
|
|
60
|
+
assertion: "required judge",
|
|
61
|
+
reason: "judge-model-unresolved",
|
|
62
|
+
});
|
|
63
|
+
expect(primaryAssertionSummary(assertions, "passed")).toBeUndefined();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("摘要把多行大值压成单行有界预览,完整断言证据不在这里展开", () => {
|
|
67
|
+
const assertions: AssertionResult[] = [{
|
|
68
|
+
name: "includes(/updateTag/)",
|
|
69
|
+
severity: "gate",
|
|
70
|
+
outcome: "failed",
|
|
71
|
+
score: 0,
|
|
72
|
+
expected: "matches /updateTag/",
|
|
73
|
+
received: `// app/actions/posts.ts\n'use server';\n${"const source = 1;\n".repeat(80)}`,
|
|
74
|
+
}];
|
|
75
|
+
|
|
76
|
+
const summary = primaryAssertionSummary(assertions, "failed")!;
|
|
77
|
+
expect(summary.received).not.toContain("\n");
|
|
78
|
+
expect(summary.received!.length).toBeLessThanOrEqual(240);
|
|
79
|
+
expect(summary.received).toMatch(/…$/);
|
|
80
|
+
const lines = assertionSummaryLines(summary);
|
|
81
|
+
expect(lines).toHaveLength(2);
|
|
82
|
+
expect(lines.every((line) => !line.includes("\n"))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("fitCompactAssertionSummary", () => {
|
|
87
|
+
const summary = primaryAssertionSummary(
|
|
88
|
+
[{
|
|
89
|
+
name: "includes(/['\"]use cache['\"];?/)",
|
|
90
|
+
groupPath: ["Catalog reads use use-cache directive and products cache tag"],
|
|
91
|
+
severity: "gate",
|
|
92
|
+
outcome: "failed",
|
|
93
|
+
score: 0,
|
|
94
|
+
expected: "matches /['\"]use cache['\"];?/",
|
|
95
|
+
received: `// next.config.ts\n${"import type { NextConfig } from 'next';\n".repeat(20)}`,
|
|
96
|
+
}],
|
|
97
|
+
"failed",
|
|
98
|
+
)!;
|
|
99
|
+
|
|
100
|
+
it("预算充足时与 compactAssertionSummary 完全一致", () => {
|
|
101
|
+
const full = compactAssertionSummary(summary);
|
|
102
|
+
expect(fitCompactAssertionSummary(summary, full.length)).toBe(full);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("空间不足先截语义标题,received 保留最大份额", () => {
|
|
106
|
+
const full = compactAssertionSummary(summary);
|
|
107
|
+
const fitted = fitCompactAssertionSummary(summary, full.length - 20);
|
|
108
|
+
expect(fitted.length).toBeLessThanOrEqual(full.length - 20);
|
|
109
|
+
// 标题被截(带 …),matcher 与 received 前缀仍在
|
|
110
|
+
expect(fitted).not.toContain("Catalog reads use use-cache directive and products cache tag");
|
|
111
|
+
expect(fitted).toContain("…");
|
|
112
|
+
expect(fitted).toContain("includes(");
|
|
113
|
+
expect(fitted).toContain("received");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("预算再小也收得住:整串不超预算且以 … 收口", () => {
|
|
117
|
+
const fitted = fitCompactAssertionSummary(summary, 80);
|
|
118
|
+
expect(fitted.length).toBeLessThanOrEqual(80);
|
|
119
|
+
expect(fitted).toMatch(/…/);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// scoring 结果的摘要投影。这里仅决定「摘要面显示哪一条、显示哪些事实」;完整诊断面继续
|
|
2
|
+
// 消费 AssertionResult[],不复用这个有损投影。
|
|
3
|
+
|
|
4
|
+
import type { AssertionResult, PrimaryAssertionSummary, Verdict } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Human/Agent 摘要是一条终端事实行,不是完整证据面。压成单行并设字符上限,避免 received
|
|
8
|
+
* 恰好是源码/工具输出时把多页内容灌进 scrollback;完整 AssertionResult 仍原样留给 show/view。
|
|
9
|
+
*/
|
|
10
|
+
const SUMMARY_TEXT_MAX_CHARS = 240;
|
|
11
|
+
|
|
12
|
+
/** 摘要面的单值收口:折单行 + 240 字符上限。任何把断言事实放进「行」里的面共用这一条。 */
|
|
13
|
+
export function summaryText(value: string): string {
|
|
14
|
+
const singleLine = value.replace(/\s+/g, " ").trim();
|
|
15
|
+
return singleLine.length <= SUMMARY_TEXT_MAX_CHARS
|
|
16
|
+
? singleLine
|
|
17
|
+
: `${singleLine.slice(0, SUMMARY_TEXT_MAX_CHARS - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 按公开展示契约选择主失败断言:failed gate 优先;只有 soft 促成 failed verdict 时才取 soft;
|
|
22
|
+
* errored 且没有结构化 error 时可由第一条非 optional unavailable 解释。
|
|
23
|
+
*/
|
|
24
|
+
export function primaryAssertionSummary(
|
|
25
|
+
assertions: readonly AssertionResult[],
|
|
26
|
+
verdict: Verdict,
|
|
27
|
+
): PrimaryAssertionSummary | undefined {
|
|
28
|
+
if (verdict === "failed") {
|
|
29
|
+
const failedGates = assertions.filter(
|
|
30
|
+
(assertion) => assertion.outcome === "failed" && assertion.severity === "gate",
|
|
31
|
+
);
|
|
32
|
+
if (failedGates.length > 0) return summaryOf(failedGates[0]!, failedGates.length - 1);
|
|
33
|
+
|
|
34
|
+
const failedSoft = assertions.filter(
|
|
35
|
+
(assertion) => assertion.outcome === "failed" && assertion.severity === "soft",
|
|
36
|
+
);
|
|
37
|
+
if (failedSoft.length > 0) return summaryOf(failedSoft[0]!, failedSoft.length - 1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (verdict === "errored") {
|
|
41
|
+
const unavailable = assertions.filter(
|
|
42
|
+
(assertion) => assertion.outcome === "unavailable" && assertion.optional !== true,
|
|
43
|
+
);
|
|
44
|
+
if (unavailable.length > 0) return summaryOf(unavailable[0]!, unavailable.length - 1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function summaryOf(assertion: AssertionResult, additionalFailures: number): PrimaryAssertionSummary {
|
|
51
|
+
const rawTitle = assertion.groupPath?.length ? assertion.groupPath.join(" > ") : assertion.name;
|
|
52
|
+
const rawMatcher = assertion.detail ?? assertion.name;
|
|
53
|
+
const title = summaryText(rawTitle);
|
|
54
|
+
return {
|
|
55
|
+
severity: assertion.severity,
|
|
56
|
+
assertion: title,
|
|
57
|
+
...(rawMatcher !== rawTitle ? { matcher: summaryText(rawMatcher) } : {}),
|
|
58
|
+
...(assertion.outcome === "unavailable"
|
|
59
|
+
? { reason: summaryText(assertion.reason) }
|
|
60
|
+
: {
|
|
61
|
+
...(assertion.expected !== undefined ? { expected: summaryText(assertion.expected) } : {}),
|
|
62
|
+
...(assertion.received !== undefined ? { received: summaryText(assertion.received) } : {}),
|
|
63
|
+
...(assertion.severity === "soft" || assertion.threshold !== undefined ? { score: assertion.score } : {}),
|
|
64
|
+
...(assertion.threshold !== undefined ? { threshold: assertion.threshold } : {}),
|
|
65
|
+
}),
|
|
66
|
+
additionalFailures,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 摘要的事实层;Human/Agent 用作第二行,表格可把它接在标题后。 */
|
|
71
|
+
export function assertionSummaryDetail(summary: PrimaryAssertionSummary): string | undefined {
|
|
72
|
+
const parts: string[] = [];
|
|
73
|
+
if (summary.matcher !== undefined) parts.push(summary.matcher);
|
|
74
|
+
if (summary.expected !== undefined) parts.push(`expected ${summary.expected}`);
|
|
75
|
+
if (summary.received !== undefined) parts.push(`received ${summary.received}`);
|
|
76
|
+
if (summary.score !== undefined) parts.push(`score ${summary.score}`);
|
|
77
|
+
if (summary.threshold !== undefined) parts.push(`threshold ${summary.threshold}`);
|
|
78
|
+
if (summary.reason !== undefined) parts.push(`reason ${summary.reason}`);
|
|
79
|
+
if (summary.additionalFailures > 0) parts.push(`+${summary.additionalFailures} more failures`);
|
|
80
|
+
return parts.length > 0 ? parts.join(" · ") : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Human/Agent 的至多两层文本。 */
|
|
84
|
+
export function assertionSummaryLines(summary: PrimaryAssertionSummary): [string] | [string, string] {
|
|
85
|
+
const head = `${summary.severity}: ${summary.assertion}`;
|
|
86
|
+
const detail = assertionSummaryDetail(summary);
|
|
87
|
+
return detail === undefined ? [head] : [head, detail];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** 比较列表的单元格投影;无 group 时不重复 `gate:` 前缀。 */
|
|
91
|
+
export function compactAssertionSummary(summary: PrimaryAssertionSummary): string {
|
|
92
|
+
const hasDistinctTitle = summary.matcher !== undefined;
|
|
93
|
+
const head = hasDistinctTitle ? `${summary.severity}: ${summary.assertion}` : summary.assertion;
|
|
94
|
+
const detail = assertionSummaryDetail(summary);
|
|
95
|
+
return detail === undefined ? head : `${head} · ${detail}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 收口用的截断:目标长度容不下时截到 target-1 并补 `…`。 */
|
|
99
|
+
function shrinkTo(text: string, target: number): string {
|
|
100
|
+
return text.length <= target ? text : `${text.slice(0, Math.max(0, target - 1))}…`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* 单元格投影的宽度收口。空间不足时按解释力从低到高让位:先截语义标题、再截 matcher,
|
|
105
|
+
* `expected / received` 与 `+N more failures` 最后截——它们直接解释为什么红。
|
|
106
|
+
* maxChars 由渲染面按可用宽度给(如两行单元格 = 2 × 列宽);字符数口径与
|
|
107
|
+
* SUMMARY_TEXT_MAX_CHARS 一致,显示宽度的精确裁剪仍归渲染面。
|
|
108
|
+
*/
|
|
109
|
+
export function fitCompactAssertionSummary(summary: PrimaryAssertionSummary, maxChars: number): string {
|
|
110
|
+
const budget = Math.max(24, Math.floor(maxChars));
|
|
111
|
+
let full = compactAssertionSummary(summary);
|
|
112
|
+
if (full.length <= budget) return full;
|
|
113
|
+
|
|
114
|
+
const TITLE_FLOOR = 24;
|
|
115
|
+
let fitted: PrimaryAssertionSummary = {
|
|
116
|
+
...summary,
|
|
117
|
+
assertion: shrinkTo(summary.assertion, Math.max(TITLE_FLOOR, summary.assertion.length - (full.length - budget))),
|
|
118
|
+
};
|
|
119
|
+
full = compactAssertionSummary(fitted);
|
|
120
|
+
if (full.length <= budget) return full;
|
|
121
|
+
|
|
122
|
+
const MATCHER_FLOOR = 16;
|
|
123
|
+
if (fitted.matcher !== undefined) {
|
|
124
|
+
fitted = {
|
|
125
|
+
...fitted,
|
|
126
|
+
matcher: shrinkTo(fitted.matcher, Math.max(MATCHER_FLOOR, fitted.matcher.length - (full.length - budget))),
|
|
127
|
+
};
|
|
128
|
+
full = compactAssertionSummary(fitted);
|
|
129
|
+
if (full.length <= budget) return full;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return shrinkTo(full, budget);
|
|
133
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// cases: docs/engineering/unit-tests/scoring/cases.md
|
|
2
|
+
// 证据完整性模型的单测:覆盖代数(resolve/downgrade/worst)、作用域断言的三值折叠、
|
|
3
|
+
// 判定折叠(非 optional unavailable → errored)、judge 未解析 → unavailable。
|
|
4
|
+
// 契约见 docs/feature/scoring/architecture/{severity-and-verdict,evidence}.md 与
|
|
5
|
+
// docs/feature/adapters/architecture/evidence.md。
|
|
6
|
+
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { completeCoverage, downgradeCoverage, resolveAgentCoverage, worstCoverage } from "./coverage.ts";
|
|
9
|
+
import { emptyDiffData } from "./diff.ts";
|
|
10
|
+
import { computeVerdict } from "./verdict.ts";
|
|
11
|
+
import { AssertionCollector } from "./collector.ts";
|
|
12
|
+
import * as Scoped from "./scoped.ts";
|
|
13
|
+
import { buildJudge } from "./judge.ts";
|
|
14
|
+
import { deriveRunFacts } from "../o11y/derive.ts";
|
|
15
|
+
import type { AssertionResult, ScoringContext, StreamEvent } from "../types.ts";
|
|
16
|
+
|
|
17
|
+
function ctxWith(over: Partial<ScoringContext> = {}): ScoringContext {
|
|
18
|
+
const events = (over.events ?? []) as StreamEvent[];
|
|
19
|
+
return {
|
|
20
|
+
events,
|
|
21
|
+
facts: deriveRunFacts(events),
|
|
22
|
+
diff: emptyDiffData(),
|
|
23
|
+
scripts: {},
|
|
24
|
+
usage: { inputTokens: 0, outputTokens: 0 },
|
|
25
|
+
status: "completed",
|
|
26
|
+
coverage: resolveAgentCoverage(completeCoverage),
|
|
27
|
+
readFile: async () => undefined,
|
|
28
|
+
...over,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const UNKNOWN = resolveAgentCoverage(undefined);
|
|
33
|
+
|
|
34
|
+
describe("coverage 代数", () => {
|
|
35
|
+
it("未声明 = unknown,不是 complete", () => {
|
|
36
|
+
expect(UNKNOWN.actions.status).toBe("unknown");
|
|
37
|
+
expect(UNKNOWN.usage.status).toBe("unknown");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("Turn 只能降级,不能把 Agent 未声明的通道升格成 complete", () => {
|
|
41
|
+
const upgraded = downgradeCoverage(UNKNOWN, { actions: { status: "complete" } });
|
|
42
|
+
expect(upgraded.actions.status).toBe("unknown");
|
|
43
|
+
const downgraded = downgradeCoverage(resolveAgentCoverage(completeCoverage), {
|
|
44
|
+
actions: { status: "partial", reason: "stream reconnected" },
|
|
45
|
+
});
|
|
46
|
+
expect(downgraded.actions).toEqual({ status: "partial", reason: "stream reconnected" });
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("聚合取最差值(unknown/unavailable < partial < complete)", () => {
|
|
50
|
+
const a = resolveAgentCoverage(completeCoverage);
|
|
51
|
+
const b = downgradeCoverage(a, { usage: { status: "unavailable" } });
|
|
52
|
+
const worst = worstCoverage([a, b]);
|
|
53
|
+
expect(worst.usage.status).toBe("unavailable");
|
|
54
|
+
expect(worst.events.status).toBe("complete");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
async function evaluate(spec: ReturnType<typeof Scoped.usedNoTools>, ctx: ScoringContext): Promise<AssertionResult> {
|
|
59
|
+
const collector = new AssertionCollector();
|
|
60
|
+
collector.record(spec);
|
|
61
|
+
const [result] = await collector.finalize(ctx);
|
|
62
|
+
return result!;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("作用域断言的三值折叠", () => {
|
|
66
|
+
const toolEvents: StreamEvent[] = [
|
|
67
|
+
{ type: "action.called", callId: "c1", name: "shell", input: { cmd: "ls" } } as StreamEvent,
|
|
68
|
+
{ type: "action.result", callId: "c1", output: {}, status: "completed" } as StreamEvent,
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
it("正断言:非 complete 通道上找到匹配仍通过(证据存在就是证据)", async () => {
|
|
72
|
+
const ctx = ctxWith({ events: toolEvents, coverage: UNKNOWN });
|
|
73
|
+
const r = await evaluate(Scoped.calledTool("shell"), ctx);
|
|
74
|
+
expect(r.outcome).toBe("passed");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("正断言:非 complete 通道上没找到记 unavailable,不判失败", async () => {
|
|
78
|
+
const ctx = ctxWith({ coverage: UNKNOWN });
|
|
79
|
+
const r = await evaluate(Scoped.calledTool("shell"), ctx);
|
|
80
|
+
expect(r.outcome).toBe("unavailable");
|
|
81
|
+
expect(r.outcome === "unavailable" && r.reason).toContain("coverage:actions=unknown");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("正断言:complete 通道上没找到才是 failed", async () => {
|
|
85
|
+
const r = await evaluate(Scoped.calledTool("shell"), ctxWith());
|
|
86
|
+
expect(r.outcome).toBe("failed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("负断言:找到反例即 failed(与覆盖无关)", async () => {
|
|
90
|
+
const ctx = ctxWith({ events: toolEvents, coverage: UNKNOWN });
|
|
91
|
+
const r = await evaluate(Scoped.notCalledTool("shell"), ctx);
|
|
92
|
+
expect(r.outcome).toBe("failed");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("负断言:空流 + 非 complete 通道 = unavailable(空流证明不了「没发生」)", async () => {
|
|
96
|
+
const ctx = ctxWith({ coverage: UNKNOWN });
|
|
97
|
+
const r = await evaluate(Scoped.usedNoTools(), ctx);
|
|
98
|
+
expect(r.outcome).toBe("unavailable");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("上限断言:实测已超限即 failed(partial 只会少采)", async () => {
|
|
102
|
+
const ctx = ctxWith({ usage: { inputTokens: 900, outputTokens: 200 }, coverage: UNKNOWN });
|
|
103
|
+
const r = await evaluate(Scoped.maxTokens(1000), ctx);
|
|
104
|
+
expect(r.outcome).toBe("failed");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("上限断言:未超限但 usage 通道非 complete = unavailable(不能按零聚合)", async () => {
|
|
108
|
+
const ctx = ctxWith({ coverage: UNKNOWN });
|
|
109
|
+
const r = await evaluate(Scoped.maxTokens(1000), ctx);
|
|
110
|
+
expect(r.outcome).toBe("unavailable");
|
|
111
|
+
expect(r.outcome === "unavailable" && r.reason).toContain("coverage:usage=unknown");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("eventsSatisfy 作者参数错误", () => {
|
|
116
|
+
it("旧的 (predicate, label) 顺序在记录时给出迁移提示,不留到 finalize 才报 predicate is not a function", () => {
|
|
117
|
+
expect(() =>
|
|
118
|
+
Reflect.apply(Scoped.eventsSatisfy, undefined, [() => true, "has an event"]),
|
|
119
|
+
).toThrow(/former \(predicate, label\) order is not supported/);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("label 必须是非空字符串", () => {
|
|
123
|
+
expect(() => Scoped.eventsSatisfy(" ", () => true)).toThrow(/non-empty string label/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("判定折叠:非 optional unavailable → errored", () => {
|
|
128
|
+
const unavailableGate: AssertionResult = {
|
|
129
|
+
name: "notCalledTool(bash)",
|
|
130
|
+
severity: "gate",
|
|
131
|
+
outcome: "unavailable",
|
|
132
|
+
reason: "coverage:actions=partial",
|
|
133
|
+
};
|
|
134
|
+
const passedGate: AssertionResult = { name: "ok", severity: "gate", outcome: "passed", score: 1 };
|
|
135
|
+
|
|
136
|
+
it("任一非 optional 断言 unavailable,attempt 即 errored(不分 gate/soft)", () => {
|
|
137
|
+
expect(computeVerdict({ assertions: [passedGate, unavailableGate] })).toBe("errored");
|
|
138
|
+
const softUnavailable: AssertionResult = { ...unavailableGate, severity: "soft" };
|
|
139
|
+
expect(computeVerdict({ assertions: [passedGate, softUnavailable] })).toBe("errored");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it(".optional() 的 unavailable 只保留在记录里,不影响 Verdict", () => {
|
|
143
|
+
const optional: AssertionResult = { ...unavailableGate, optional: true };
|
|
144
|
+
expect(computeVerdict({ assertions: [passedGate, optional] })).toBe("passed");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("errored 压过 failed;failed 压过 skipped", () => {
|
|
148
|
+
const failedGate: AssertionResult = { name: "x", severity: "gate", outcome: "failed", score: 0 };
|
|
149
|
+
expect(computeVerdict({ assertions: [failedGate, unavailableGate] })).toBe("errored");
|
|
150
|
+
expect(computeVerdict({ assertions: [failedGate], skipReason: "later" })).toBe("failed");
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe("judge 未解析到模型 / key:记 unavailable,绝不静默、不崩", () => {
|
|
155
|
+
it("缺 key 时该条断言照常记录,finalize 落 unavailable(reason: judge-key-unresolved)", async () => {
|
|
156
|
+
const collector = new AssertionCollector();
|
|
157
|
+
const judge = buildJudge({
|
|
158
|
+
record: (spec) => collector.record(spec),
|
|
159
|
+
judge: { model: "gpt-x", apiKeyEnv: "NICEEVAL_TEST_NO_SUCH_KEY" },
|
|
160
|
+
getOutput: () => "output",
|
|
161
|
+
getInput: () => "input",
|
|
162
|
+
});
|
|
163
|
+
judge.autoevals.closedQA("是否切题?");
|
|
164
|
+
const [r] = await collector.finalize(ctxWith());
|
|
165
|
+
expect(r!.outcome).toBe("unavailable");
|
|
166
|
+
expect(r!.outcome === "unavailable" && r!.reason).toContain("judge-key-unresolved");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("缺 model 时不在调用点抛错,落 unavailable(reason: judge-model-unresolved)", async () => {
|
|
170
|
+
const prevModel = process.env.NICEEVAL_JUDGE_MODEL;
|
|
171
|
+
delete process.env.NICEEVAL_JUDGE_MODEL;
|
|
172
|
+
try {
|
|
173
|
+
const collector = new AssertionCollector();
|
|
174
|
+
const judge = buildJudge({
|
|
175
|
+
record: (spec) => collector.record(spec),
|
|
176
|
+
judge: undefined,
|
|
177
|
+
getOutput: () => "output",
|
|
178
|
+
getInput: () => "input",
|
|
179
|
+
});
|
|
180
|
+
expect(() => judge.autoevals.factuality("参考答案").optional()).not.toThrow();
|
|
181
|
+
const [r] = await collector.finalize(ctxWith());
|
|
182
|
+
expect(r!.outcome).toBe("unavailable");
|
|
183
|
+
expect(r!.outcome === "unavailable" && r!.reason).toContain("judge-model-unresolved");
|
|
184
|
+
expect(r!.optional).toBe(true);
|
|
185
|
+
} finally {
|
|
186
|
+
if (prevModel !== undefined) process.env.NICEEVAL_JUDGE_MODEL = prevModel;
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
});
|