niceeval 0.5.4 → 0.6.0
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/INDEX.md +55 -0
- package/dist/agents/types.d.ts +317 -0
- package/dist/context/types.d.ts +335 -0
- package/dist/i18n/core.d.ts +11 -0
- package/dist/i18n/en.d.ts +181 -0
- package/dist/i18n/index.d.ts +5 -0
- package/dist/i18n/zh-CN.d.ts +183 -0
- package/dist/o11y/execution-tree.d.ts +103 -0
- package/dist/o11y/otlp/parse.d.ts +3 -0
- package/dist/o11y/otlp/receiver.d.ts +21 -0
- package/dist/o11y/otlp/select.d.ts +22 -0
- package/dist/o11y/otlp/turn-otel.d.ts +43 -0
- package/dist/o11y/types.d.ts +190 -0
- package/dist/report/aggregate.d.ts +57 -0
- package/dist/report/aggregate.js +212 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.d.ts +1 -0
- package/dist/report/built-ins/cost-pass-rate-comparison.js +17 -0
- package/dist/report/built-ins/index.d.ts +1 -0
- package/dist/report/built-ins/index.js +2 -0
- package/dist/report/components.d.ts +178 -0
- package/dist/report/components.js +132 -0
- package/dist/report/compute.d.ts +106 -0
- package/dist/report/compute.js +618 -0
- package/dist/report/flag.d.ts +6 -0
- package/dist/report/flag.js +8 -0
- package/dist/report/format.d.ts +32 -0
- package/dist/report/format.js +126 -0
- package/dist/report/index.d.ts +19 -0
- package/dist/report/index.js +30 -0
- package/dist/report/load.d.ts +12 -0
- package/dist/report/load.js +56 -0
- package/dist/report/locale.d.ts +83 -0
- package/dist/report/locale.js +160 -0
- package/dist/report/metrics.d.ts +21 -0
- package/dist/report/metrics.js +115 -0
- package/dist/report/primitives.d.ts +64 -0
- package/dist/report/primitives.js +113 -0
- package/dist/report/react/AttemptList.d.ts +23 -0
- package/dist/report/react/AttemptList.js +20 -0
- package/dist/report/react/DeltaTable.d.ts +8 -0
- package/dist/report/react/DeltaTable.js +24 -0
- package/dist/report/react/EvalList.d.ts +10 -0
- package/dist/report/react/EvalList.js +0 -0
- package/dist/report/react/ExperimentList.d.ts +10 -0
- package/dist/report/react/ExperimentList.js +15 -0
- package/dist/report/react/GroupSummary.d.ts +8 -0
- package/dist/report/react/GroupSummary.js +8 -0
- package/dist/report/react/MetricBars.d.ts +10 -0
- package/dist/report/react/MetricBars.js +48 -0
- package/dist/report/react/MetricLine.d.ts +9 -0
- package/dist/report/react/MetricLine.js +68 -0
- package/dist/report/react/MetricMatrix.d.ts +10 -0
- package/dist/report/react/MetricMatrix.js +23 -0
- package/dist/report/react/MetricScatter.d.ts +9 -0
- package/dist/report/react/MetricScatter.js +100 -0
- package/dist/report/react/MetricTable.d.ts +11 -0
- package/dist/report/react/MetricTable.js +26 -0
- package/dist/report/react/RunOverview.d.ts +8 -0
- package/dist/report/react/RunOverview.js +12 -0
- package/dist/report/react/Scoreboard.d.ts +8 -0
- package/dist/report/react/Scoreboard.js +26 -0
- package/dist/report/react/cell.d.ts +9 -0
- package/dist/report/react/cell.js +9 -0
- package/dist/report/react/chart-math.d.ts +14 -0
- package/dist/report/react/chart-math.js +68 -0
- package/dist/report/react/colors.d.ts +14 -0
- package/dist/report/react/colors.js +45 -0
- package/dist/report/react/fixtures.d.ts +19 -0
- package/dist/report/react/fixtures.js +393 -0
- package/dist/report/react/format.d.ts +3 -0
- package/dist/report/react/format.js +7 -0
- package/dist/report/react/index.d.ts +6 -0
- package/dist/report/react/index.js +14 -0
- package/dist/report/report.d.ts +24 -0
- package/dist/report/report.js +49 -0
- package/dist/report/text/faces.d.ts +29 -0
- package/dist/report/text/faces.js +503 -0
- package/dist/report/text/layout.d.ts +75 -0
- package/dist/report/text/layout.js +173 -0
- package/dist/report/text/plot.d.ts +37 -0
- package/dist/report/text/plot.js +81 -0
- package/dist/report/text/table.d.ts +8 -0
- package/dist/report/text/table.js +86 -0
- package/dist/report/tree.d.ts +99 -0
- package/dist/report/tree.js +228 -0
- package/dist/report/types.d.ts +420 -0
- package/dist/report/types.js +4 -0
- package/dist/report/web.d.ts +15 -0
- package/dist/report/web.js +37 -0
- package/dist/results/annotated-source.d.ts +61 -0
- package/dist/results/attempt-evidence.d.ts +69 -0
- package/dist/results/attempt-source.d.ts +15 -0
- package/dist/results/locator.d.ts +75 -0
- package/dist/results/locator.js +0 -0
- package/dist/results/select.d.ts +45 -0
- package/dist/results/select.js +230 -0
- package/dist/results/source-hash.d.ts +9 -0
- package/dist/results/types.d.ts +204 -0
- package/dist/runner/eval-source.d.ts +22 -0
- package/dist/runner/fingerprint.d.ts +23 -0
- package/dist/runner/types.d.ts +332 -0
- package/dist/sandbox/docker-stream.d.ts +24 -0
- package/dist/sandbox/docker.d.ts +84 -0
- package/dist/sandbox/e2b.d.ts +31 -0
- package/dist/sandbox/errors.d.ts +4 -0
- package/dist/sandbox/local-files.d.ts +6 -0
- package/dist/sandbox/paths.d.ts +4 -0
- package/dist/sandbox/registry.d.ts +13 -0
- package/dist/sandbox/resolve.d.ts +40 -0
- package/dist/sandbox/retry.d.ts +10 -0
- package/dist/sandbox/shell.d.ts +11 -0
- package/dist/sandbox/source-files.d.ts +13 -0
- package/dist/sandbox/types.d.ts +203 -0
- package/dist/sandbox/vercel.d.ts +37 -0
- package/dist/scoring/types.d.ts +70 -0
- package/dist/shared/aggregate.d.ts +29 -0
- package/dist/shared/aggregate.js +55 -0
- package/dist/shared/types.d.ts +27 -0
- package/dist/shared/verdict.d.ts +27 -0
- package/dist/shared/verdict.js +34 -0
- package/dist/tty-line.d.ts +6 -0
- package/dist/types.d.ts +7 -0
- package/dist/util.d.ts +22 -0
- package/docs-site/zh/concepts/adapter.mdx +5 -5
- package/docs-site/zh/concepts/assert.mdx +8 -8
- package/docs-site/zh/concepts/evals.mdx +5 -5
- package/docs-site/zh/concepts/experiment.mdx +2 -2
- package/docs-site/zh/concepts/judge.mdx +9 -9
- package/docs-site/zh/concepts/overview.mdx +2 -2
- package/docs-site/zh/concepts/tier.mdx +6 -6
- package/docs-site/zh/example/claude-code-codex-plugin.mdx +9 -5
- package/docs-site/zh/example/claude-code-codex-skill.mdx +12 -7
- package/docs-site/zh/guides/agent-feedback-loop.mdx +235 -78
- package/docs-site/zh/guides/connect-otel.mdx +3 -3
- package/docs-site/zh/guides/connect-your-agent.mdx +2 -2
- package/docs-site/zh/guides/custom-reports.mdx +196 -36
- package/docs-site/zh/guides/experiments.mdx +7 -1
- package/docs-site/zh/guides/fixtures.mdx +1 -1
- package/docs-site/zh/guides/official-adapters.mdx +31 -9
- package/docs-site/zh/guides/publish-report.mdx +3 -3
- package/docs-site/zh/guides/report-components.mdx +194 -55
- package/docs-site/zh/guides/reporters.mdx +2 -2
- package/docs-site/zh/guides/results-data.mdx +35 -34
- package/docs-site/zh/guides/runner.mdx +2 -2
- package/docs-site/zh/guides/sandbox-agent.mdx +2 -2
- package/docs-site/zh/guides/{sandbox-backends.mdx → sandbox-providers.mdx} +11 -11
- package/docs-site/zh/guides/viewing-results.mdx +199 -79
- package/docs-site/zh/guides/write-experiment.mdx +9 -1
- package/docs-site/zh/guides/write-send.mdx +2 -2
- package/docs-site/zh/index.mdx +4 -3
- package/docs-site/zh/introduction.mdx +2 -1
- package/docs-site/zh/quickstart.mdx +5 -3
- package/docs-site/zh/reference/builtin-agents.mdx +38 -13
- package/docs-site/zh/reference/capabilities.mdx +1 -1
- package/docs-site/zh/reference/cli.mdx +36 -13
- package/docs-site/zh/reference/define-agent.mdx +12 -12
- package/docs-site/zh/reference/define-config.mdx +1 -1
- package/docs-site/zh/reference/define-eval.mdx +3 -3
- package/docs-site/zh/reference/events.mdx +7 -1
- package/docs-site/zh/reference/expect.mdx +3 -3
- package/package.json +10 -6
- package/src/agents/bub.ts +129 -42
- package/src/agents/claude-code.test.ts +152 -0
- package/src/agents/claude-code.ts +127 -13
- package/src/agents/codex.test.ts +192 -0
- package/src/agents/codex.ts +151 -12
- package/src/agents/index.ts +9 -3
- package/src/agents/manifest.ts +41 -0
- package/src/agents/plugin-config.test.ts +94 -0
- package/src/agents/shared.ts +33 -2
- package/src/agents/skills.test.ts +214 -0
- package/src/agents/skills.ts +304 -0
- package/src/agents/streaming.ts +5 -5
- package/src/agents/types.ts +54 -2
- package/src/cli.ts +74 -47
- package/src/context/context.test.ts +20 -2
- package/src/context/control-flow.ts +1 -1
- package/src/context/session.test.ts +92 -1
- package/src/context/session.ts +23 -1
- package/src/context/types.ts +4 -4
- package/src/define.test.ts +8 -8
- package/src/define.ts +11 -11
- package/src/expect/index.ts +5 -5
- package/src/i18n/en.ts +34 -15
- package/src/i18n/zh-CN.ts +37 -21
- package/src/index.ts +1 -1
- package/src/o11y/execution-tree.test.ts +452 -0
- package/src/o11y/execution-tree.ts +367 -0
- package/src/o11y/otlp/receiver.ts +1 -1
- package/src/o11y/otlp/select.ts +7 -3
- package/src/o11y/parsers/claude-code.test.ts +175 -0
- package/src/o11y/parsers/claude-code.ts +32 -0
- package/src/o11y/types.ts +6 -1
- package/src/report/aggregate.ts +26 -20
- package/src/report/built-in-user-parity.test.tsx +643 -0
- package/src/report/built-ins/cost-pass-rate-comparison.tsx +23 -0
- package/src/report/built-ins/index.ts +2 -0
- package/src/report/components.tsx +223 -50
- package/src/report/compute.ts +358 -53
- package/src/report/dual-render.test.tsx +1125 -0
- package/src/report/flag.ts +1 -1
- package/src/report/format.ts +50 -2
- package/src/report/index.ts +48 -14
- package/src/report/load.ts +1 -1
- package/src/report/locale.ts +206 -0
- package/src/report/metrics.ts +39 -15
- package/src/report/primitives.tsx +100 -2
- package/src/report/react/AttemptList.tsx +115 -0
- package/src/report/react/DeltaTable.tsx +9 -6
- package/src/report/react/EvalList.tsx +0 -0
- package/src/report/react/ExperimentList.tsx +108 -0
- package/src/report/react/GroupSummary.tsx +66 -0
- package/src/report/react/MetricBars.tsx +24 -13
- package/src/report/react/MetricLine.tsx +21 -21
- package/src/report/react/MetricMatrix.tsx +8 -4
- package/src/report/react/MetricScatter.tsx +136 -96
- package/src/report/react/MetricTable.tsx +115 -15
- package/src/report/react/RunOverview.tsx +48 -26
- package/src/report/react/Scoreboard.tsx +15 -7
- package/src/report/react/cell.tsx +18 -9
- package/src/{view/app/lib/chart.ts → report/react/chart-math.ts} +2 -6
- package/src/report/react/colors.ts +24 -15
- package/src/report/react/enhance.js +129 -0
- package/src/report/react/fixtures.ts +163 -34
- package/src/report/react/format.ts +1 -1
- package/src/report/react/index.tsx +22 -6
- package/src/report/react/render.test.tsx +164 -31
- package/src/report/react/styles.css +405 -213
- package/src/report/report.test.ts +403 -94
- package/src/report/report.ts +29 -11
- package/src/report/text/faces.ts +303 -95
- package/src/report/text/layout.ts +76 -11
- package/src/report/text/table.ts +98 -0
- package/src/report/tree.ts +103 -20
- package/src/report/types.ts +196 -25
- package/src/report/web.ts +46 -19
- package/src/results/annotated-source.test.ts +146 -0
- package/src/results/annotated-source.ts +128 -0
- package/src/results/attempt-evidence.test.ts +158 -0
- package/src/results/attempt-evidence.ts +158 -0
- package/src/results/attempt-source.ts +48 -0
- package/src/results/copy.ts +154 -139
- package/src/results/format.ts +54 -47
- package/src/results/host-equivalence.test.ts +527 -0
- package/src/results/index.ts +56 -14
- package/src/results/locator.test.ts +198 -0
- package/src/results/locator.ts +0 -0
- package/src/results/open.ts +392 -175
- package/src/results/results.test.ts +943 -494
- package/src/results/select.ts +129 -23
- package/src/results/skipped-notice.ts +0 -0
- package/src/results/source-hash.ts +28 -0
- package/src/results/types.ts +98 -62
- package/src/results/writer.ts +304 -176
- package/src/runner/attempt.test.ts +170 -0
- package/src/runner/attempt.ts +48 -13
- package/src/runner/discover.test.ts +65 -0
- package/src/runner/discover.ts +6 -2
- package/src/runner/eval-source.test.ts +121 -0
- package/src/runner/eval-source.ts +45 -0
- package/src/runner/fingerprint.ts +54 -1
- package/src/runner/remote-sandbox.ts +1 -1
- package/src/runner/report.test.ts +6 -8
- package/src/runner/report.ts +5 -4
- package/src/runner/reporters/artifacts.ts +27 -41
- package/src/runner/reporters/braintrust.test.ts +3 -3
- package/src/runner/reporters/braintrust.ts +1 -1
- package/src/runner/reporters/console.ts +6 -6
- package/src/runner/reporters/json.ts +4 -4
- package/src/runner/reporters/live.test.ts +56 -0
- package/src/runner/reporters/live.ts +36 -10
- package/src/runner/reporters/quiet.test.ts +66 -0
- package/src/runner/reporters/quiet.ts +49 -0
- package/src/runner/reporters/shared.ts +5 -5
- package/src/runner/reporters/table.ts +17 -17
- package/src/runner/run.ts +61 -102
- package/src/runner/types.ts +43 -32
- package/src/sandbox/checkpoint.ts +1 -1
- package/src/sandbox/docker.ts +15 -2
- package/src/sandbox/e2b.ts +11 -5
- package/src/sandbox/errors.ts +13 -0
- package/src/sandbox/index.ts +3 -3
- package/src/sandbox/local-files.ts +1 -1
- package/src/sandbox/registry.ts +6 -5
- package/src/sandbox/resolve.ts +38 -23
- package/src/sandbox/retry.test.ts +68 -0
- package/src/sandbox/retry.ts +44 -0
- package/src/sandbox/types.ts +24 -24
- package/src/sandbox/vercel.ts +17 -3
- package/src/scoring/collector.ts +3 -3
- package/src/scoring/judge.ts +2 -2
- package/src/scoring/scoped.ts +13 -1
- package/src/scoring/types.ts +7 -7
- package/src/scoring/verdict.ts +4 -4
- package/src/shared/aggregate.ts +23 -3
- package/src/shared/types.ts +2 -2
- package/src/shared/{outcome.ts → verdict.ts} +15 -15
- package/src/show/compose.ts +12 -132
- package/src/show/index.ts +125 -94
- package/src/show/render.ts +660 -160
- package/src/show/show.test.ts +472 -253
- package/src/source-loc.ts +1 -1
- package/src/tty-line.ts +32 -0
- package/src/types.ts +2 -2
- package/src/util.test.ts +21 -1
- package/src/util.ts +5 -1
- package/src/view/app/App.tsx +33 -198
- package/src/view/app/components/AttemptModal.tsx +6 -6
- package/src/view/app/components/CodeView.tsx +2 -2
- package/src/view/app/components/CopyControls.tsx +32 -36
- package/src/view/app/components/SkippedRunsBanner.tsx +140 -0
- package/src/view/app/components/ui/badge.tsx +1 -1
- package/src/view/app/components/ui/dialog.tsx +2 -2
- package/src/view/app/i18n.ts +40 -31
- package/src/view/app/lib/artifact-url.ts +1 -1
- package/src/view/app/lib/attempt-route.test.ts +37 -47
- package/src/view/app/lib/attempt-route.ts +26 -26
- package/src/view/app/lib/rows.ts +9 -112
- package/src/view/app/lib/verdict.ts +25 -0
- package/src/view/app/main.tsx +9 -12
- package/src/view/app/pages/RunsPage.tsx +4 -4
- package/src/view/app/pages/TracesPage.tsx +3 -3
- package/src/view/app/shared.ts +0 -1
- package/src/view/app/types.ts +6 -51
- package/src/view/artifact-serving.test.ts +136 -0
- package/src/view/client-dist/app.css +1 -1
- package/src/view/client-dist/app.js +21 -21
- package/src/view/data.test.ts +214 -143
- package/src/view/data.ts +166 -120
- package/src/view/index.ts +23 -11
- package/src/view/server.ts +52 -17
- package/src/view/shared/types.ts +22 -31
- package/src/view/styles.css +117 -327
- package/src/view/view-report.test.ts +146 -98
- package/src/report/default-report.tsx +0 -222
- package/src/report/dual-face.test.tsx +0 -527
- package/src/report/react/CaseList.tsx +0 -70
- package/src/view/app/components/CostScoreChart.tsx +0 -147
- package/src/view/app/components/ExperimentTable.tsx +0 -284
- package/src/view/app/components/GroupSelector.tsx +0 -61
- package/src/view/app/components/primitives.tsx +0 -43
- package/src/view/app/lib/outcome.ts +0 -82
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { AlertCircle, ChevronRight, Copy, Check } from "lucide-react";
|
|
3
|
+
import type { T } from "../shared.ts";
|
|
4
|
+
import type { MessageKey } from "../i18n.ts";
|
|
5
|
+
import type { SkippedRunNotice } from "../types.ts";
|
|
6
|
+
import { copyText } from "./CopyControls.tsx";
|
|
7
|
+
|
|
8
|
+
// 折叠前默认可见的条目数(跨分组累计);超出的部分收进「展开其余」。
|
|
9
|
+
const DEFAULT_VISIBLE = 3;
|
|
10
|
+
|
|
11
|
+
type GroupKind = "incompatible" | "incompatibleForeign" | "malformed" | "incomplete";
|
|
12
|
+
|
|
13
|
+
interface SkippedGroup {
|
|
14
|
+
key: string;
|
|
15
|
+
kind: GroupKind;
|
|
16
|
+
messageKey: MessageKey;
|
|
17
|
+
vars: Record<string, string | number>;
|
|
18
|
+
items: SkippedRunNotice[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** 同一原因(且描述文案完全相同)的失败 run 归成一组,组内条目只保留目录名 + 命令,
|
|
22
|
+
* 避免把完整原因句子重复 N 遍——这正是原实现把首屏挤满的根因。 */
|
|
23
|
+
function groupSkippedRuns(runs: SkippedRunNotice[]): SkippedGroup[] {
|
|
24
|
+
const groups = new Map<string, SkippedGroup>();
|
|
25
|
+
for (const run of runs) {
|
|
26
|
+
let kind: GroupKind;
|
|
27
|
+
let key: string;
|
|
28
|
+
let messageKey: MessageKey;
|
|
29
|
+
let vars: Record<string, string | number>;
|
|
30
|
+
|
|
31
|
+
if (run.reason === "malformed") {
|
|
32
|
+
kind = "malformed";
|
|
33
|
+
vars = { detail: run.detail ?? "?" };
|
|
34
|
+
key = `malformed:${vars.detail}`;
|
|
35
|
+
messageKey = "banner.skipped.malformed";
|
|
36
|
+
} else if (run.reason === "incomplete") {
|
|
37
|
+
kind = "incomplete";
|
|
38
|
+
vars = {};
|
|
39
|
+
key = "incomplete";
|
|
40
|
+
messageKey = "banner.skipped.incomplete";
|
|
41
|
+
} else if (run.producerName && run.producerName !== "niceeval") {
|
|
42
|
+
kind = "incompatibleForeign";
|
|
43
|
+
vars = { name: run.producerName, version: run.producerVersion ?? "?", schemaVersion: run.schemaVersion ?? "?" };
|
|
44
|
+
key = `foreign:${vars.name}:${vars.version}:${vars.schemaVersion}`;
|
|
45
|
+
messageKey = "banner.skipped.incompatibleForeign";
|
|
46
|
+
} else {
|
|
47
|
+
kind = "incompatible";
|
|
48
|
+
vars = { producer: run.producerVersion ?? "?", schemaVersion: run.schemaVersion ?? "?" };
|
|
49
|
+
key = `incompatible:${vars.producer}:${vars.schemaVersion}`;
|
|
50
|
+
messageKey = "banner.skipped.incompatible";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const existing = groups.get(key);
|
|
54
|
+
if (existing) {
|
|
55
|
+
existing.items.push(run);
|
|
56
|
+
} else {
|
|
57
|
+
groups.set(key, { key, kind, messageKey, vars, items: [run] });
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return [...groups.values()];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function CopyCommandButton({ command, t }: { command: string; t: T }) {
|
|
64
|
+
const [copied, setCopied] = useState(false);
|
|
65
|
+
return (
|
|
66
|
+
<button
|
|
67
|
+
type="button"
|
|
68
|
+
className={`ib-copy${copied ? " is-copied" : ""}`}
|
|
69
|
+
title={t("banner.copyCommand")}
|
|
70
|
+
onClick={async () => {
|
|
71
|
+
await copyText(command);
|
|
72
|
+
setCopied(true);
|
|
73
|
+
setTimeout(() => setCopied(false), 1200);
|
|
74
|
+
}}
|
|
75
|
+
>
|
|
76
|
+
{copied ? <Check aria-hidden="true" /> : <Copy aria-hidden="true" />}
|
|
77
|
+
</button>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function SkippedRunsBanner({ skippedRuns, t }: { skippedRuns: SkippedRunNotice[]; t: T }) {
|
|
82
|
+
const [expanded, setExpanded] = useState(false);
|
|
83
|
+
if (skippedRuns.length === 0) return null;
|
|
84
|
+
|
|
85
|
+
const groups = groupSkippedRuns(skippedRuns);
|
|
86
|
+
const total = skippedRuns.length;
|
|
87
|
+
let budget = expanded ? total : DEFAULT_VISIBLE;
|
|
88
|
+
const hiddenCount = expanded ? 0 : Math.max(0, total - DEFAULT_VISIBLE);
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<section className="incompatible-banner" role="alert">
|
|
92
|
+
<div className="ib-head">
|
|
93
|
+
<AlertCircle className="ib-icon" aria-hidden="true" />
|
|
94
|
+
<b>{t("banner.skippedTitle", { count: total })}</b>
|
|
95
|
+
</div>
|
|
96
|
+
<div className="ib-groups">
|
|
97
|
+
{groups.map((group) => {
|
|
98
|
+
const visibleItems = group.items.slice(0, Math.max(0, budget));
|
|
99
|
+
budget -= visibleItems.length;
|
|
100
|
+
const hiddenInGroup = group.items.length - visibleItems.length;
|
|
101
|
+
return (
|
|
102
|
+
<div className="ib-group" key={group.key}>
|
|
103
|
+
<div className="ib-group-head">
|
|
104
|
+
<span className="ib-group-count">{group.items.length}</span>
|
|
105
|
+
<span className="ib-group-desc">{t(group.messageKey, group.vars)}</span>
|
|
106
|
+
</div>
|
|
107
|
+
{visibleItems.length > 0 && (
|
|
108
|
+
<ul>
|
|
109
|
+
{visibleItems.map((item) => (
|
|
110
|
+
<li key={item.dir}>
|
|
111
|
+
<span className="ib-dir">{item.dir}</span>
|
|
112
|
+
{item.command && <CopyCommandButton command={item.command} t={t} />}
|
|
113
|
+
</li>
|
|
114
|
+
))}
|
|
115
|
+
{hiddenInGroup > 0 && !expanded && (
|
|
116
|
+
<li className="ib-more" aria-hidden="true">
|
|
117
|
+
+{hiddenInGroup}
|
|
118
|
+
</li>
|
|
119
|
+
)}
|
|
120
|
+
</ul>
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
})}
|
|
125
|
+
</div>
|
|
126
|
+
{hiddenCount > 0 && (
|
|
127
|
+
<button type="button" className="ib-toggle" onClick={() => setExpanded(true)}>
|
|
128
|
+
<ChevronRight aria-hidden="true" />
|
|
129
|
+
{t("banner.expandRest", { count: hiddenCount })}
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
{expanded && total > DEFAULT_VISIBLE && (
|
|
133
|
+
<button type="button" className="ib-toggle" onClick={() => setExpanded(false)}>
|
|
134
|
+
<ChevronRight className="is-open" aria-hidden="true" />
|
|
135
|
+
{t("banner.collapse")}
|
|
136
|
+
</button>
|
|
137
|
+
)}
|
|
138
|
+
</section>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -3,7 +3,7 @@ import { cn } from "../../lib/cn.ts";
|
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* 状态小标签:边框 + 浅底都取自当前文字色,tone(.good/.bad/.warn/.infra-err)决定颜色。
|
|
6
|
-
* 取代旧的 .modal-
|
|
6
|
+
* 取代旧的 .modal-verdict / 内联状态 chip。
|
|
7
7
|
*/
|
|
8
8
|
export function Badge({ tone, className, children }: { tone?: string; className?: string; children: ReactNode }) {
|
|
9
9
|
return (
|
|
@@ -17,10 +17,10 @@ export function DialogContent({
|
|
|
17
17
|
}: ComponentPropsWithoutRef<typeof DialogPrimitive.Content>) {
|
|
18
18
|
return (
|
|
19
19
|
<DialogPrimitive.Portal>
|
|
20
|
-
<DialogPrimitive.Overlay className="fixed inset-0 z-[200] bg-black/
|
|
20
|
+
<DialogPrimitive.Overlay className="fixed inset-0 z-[200] bg-black/90" />
|
|
21
21
|
<DialogPrimitive.Content
|
|
22
22
|
className={cn(
|
|
23
|
-
"fixed left-1/2 top-1/2 z-[200] flex max-h-[min(86vh,820px)] w-[min(
|
|
23
|
+
"fixed left-1/2 top-1/2 z-[200] flex max-h-[min(86vh,820px)] w-[min(1120px,calc(100vw-48px))]",
|
|
24
24
|
"-translate-x-1/2 -translate-y-1/2 flex-col overflow-hidden rounded-xl border border-line-strong",
|
|
25
25
|
"bg-panel shadow-[0_20px_60px_rgba(0,0,0,0.45)] focus:outline-none",
|
|
26
26
|
className,
|
package/src/view/app/i18n.ts
CHANGED
|
@@ -33,9 +33,9 @@ export type MessageKey =
|
|
|
33
33
|
| "table.successRate"
|
|
34
34
|
| "table.tokens"
|
|
35
35
|
| "table.estCost"
|
|
36
|
-
| "table.
|
|
36
|
+
| "table.verdicts"
|
|
37
37
|
| "table.evalId"
|
|
38
|
-
| "table.
|
|
38
|
+
| "table.verdict"
|
|
39
39
|
| "table.ranAt"
|
|
40
40
|
| "detail.evalResult"
|
|
41
41
|
| "detail.evalResults"
|
|
@@ -104,15 +104,18 @@ export type MessageKey =
|
|
|
104
104
|
| "assert.fail"
|
|
105
105
|
| "assert.soft"
|
|
106
106
|
| "assert.evidence"
|
|
107
|
-
| "
|
|
108
|
-
| "
|
|
109
|
-
| "
|
|
110
|
-
| "
|
|
107
|
+
| "verdict.passed"
|
|
108
|
+
| "verdict.failed"
|
|
109
|
+
| "verdict.errored"
|
|
110
|
+
| "verdict.skipped"
|
|
111
111
|
| "banner.skippedTitle"
|
|
112
112
|
| "banner.skipped.incompatible"
|
|
113
113
|
| "banner.skipped.incompatibleForeign"
|
|
114
114
|
| "banner.skipped.malformed"
|
|
115
115
|
| "banner.skipped.incomplete"
|
|
116
|
+
| "banner.expandRest"
|
|
117
|
+
| "banner.collapse"
|
|
118
|
+
| "banner.copyCommand"
|
|
116
119
|
| "banner.warningsTitle"
|
|
117
120
|
| "hero.composedFrom"
|
|
118
121
|
| "chart.costVsScore"
|
|
@@ -141,7 +144,7 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
141
144
|
"section.traces": "Traces",
|
|
142
145
|
"search.experiments": "Filter experiment, agent, model, or eval...",
|
|
143
146
|
"search.runs": "Filter eval ID or experiment...",
|
|
144
|
-
"empty.summary": "No
|
|
147
|
+
"empty.summary": "No snapshots found. Run niceeval or pass niceeval view path/to/snapshot.json.",
|
|
145
148
|
"empty.individualRuns": "No individual runs found.",
|
|
146
149
|
"empty.runsFilter": "No results match the filter.",
|
|
147
150
|
"empty.traces": "No traces available. Traces are collected during eval runs when artifacts are saved.",
|
|
@@ -152,9 +155,9 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
152
155
|
"table.successRate": "Success Rate",
|
|
153
156
|
"table.tokens": "Tokens",
|
|
154
157
|
"table.estCost": "Est. Cost",
|
|
155
|
-
"table.
|
|
158
|
+
"table.verdicts": "Verdicts",
|
|
156
159
|
"table.evalId": "Eval ID",
|
|
157
|
-
"table.
|
|
160
|
+
"table.verdict": "Verdict",
|
|
158
161
|
"table.ranAt": "Ran At",
|
|
159
162
|
"detail.evalResult": "eval",
|
|
160
163
|
"detail.evalResults": "evals",
|
|
@@ -223,15 +226,18 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
223
226
|
"assert.fail": "fail",
|
|
224
227
|
"assert.soft": "soft",
|
|
225
228
|
"assert.evidence": "What was checked",
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"banner.skippedTitle": "
|
|
231
|
-
"banner.skipped.incompatible": "written by niceeval {{producer}} (schemaVersion {{schemaVersion}}) —
|
|
232
|
-
"banner.skipped.incompatibleForeign": "written by {{name}} {{version}} (schemaVersion {{schemaVersion}}) — this viewer cannot read
|
|
233
|
-
"banner.skipped.malformed": "unreadable report ({{detail}}) —
|
|
234
|
-
"banner.skipped.incomplete": "
|
|
229
|
+
"verdict.passed": "passed",
|
|
230
|
+
"verdict.failed": "failed",
|
|
231
|
+
"verdict.errored": "errors",
|
|
232
|
+
"verdict.skipped": "skipped",
|
|
233
|
+
"banner.skippedTitle": "{{count}} run(s) could not be loaded and are not shown here",
|
|
234
|
+
"banner.skipped.incompatible": "written by niceeval {{producer}} (schemaVersion {{schemaVersion}}) — current version can't read it, expand for the view command",
|
|
235
|
+
"banner.skipped.incompatibleForeign": "written by {{name}} {{version}} (schemaVersion {{schemaVersion}}) — this viewer cannot read them; open with the tool that produced them",
|
|
236
|
+
"banner.skipped.malformed": "unreadable report ({{detail}}) — may be corrupted; re-run the eval or delete the run directory",
|
|
237
|
+
"banner.skipped.incomplete": "snapshot.json was never written (a narrow crash window) — completed attempt artifacts remain on disk for manual inspection; delete the directory if you no longer need them",
|
|
238
|
+
"banner.expandRest": "Show {{count}} more",
|
|
239
|
+
"banner.collapse": "Collapse",
|
|
240
|
+
"banner.copyCommand": "Copy view command",
|
|
235
241
|
"banner.warningsTitle": "Heads-up about the current leaderboard selection:",
|
|
236
242
|
"hero.composedFrom": "Composed from {{count}} run(s)",
|
|
237
243
|
"chart.costVsScore": "Cost vs. Score",
|
|
@@ -257,10 +263,10 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
257
263
|
"section.traces": "追踪",
|
|
258
264
|
"search.experiments": "筛选实验、agent、model 或 eval...",
|
|
259
265
|
"search.runs": "筛选 eval ID 或实验...",
|
|
260
|
-
"empty.summary": "
|
|
266
|
+
"empty.summary": "没有找到快照。请先运行 niceeval,或传入 niceeval view path/to/snapshot.json。",
|
|
261
267
|
"empty.individualRuns": "没有单次运行结果。",
|
|
262
268
|
"empty.runsFilter": "没有匹配筛选条件的结果。",
|
|
263
|
-
"empty.traces": "
|
|
269
|
+
"empty.traces": "没有可用追踪。保存 artifact 的 eval run 会收集 traces。",
|
|
264
270
|
"table.experiment": "实验",
|
|
265
271
|
"table.model": "模型",
|
|
266
272
|
"table.agent": "Agent",
|
|
@@ -268,9 +274,9 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
268
274
|
"table.successRate": "成功率",
|
|
269
275
|
"table.tokens": "Tokens",
|
|
270
276
|
"table.estCost": "预估成本",
|
|
271
|
-
"table.
|
|
277
|
+
"table.verdicts": "结果",
|
|
272
278
|
"table.evalId": "Eval ID",
|
|
273
|
-
"table.
|
|
279
|
+
"table.verdict": "状态",
|
|
274
280
|
"table.ranAt": "运行时间",
|
|
275
281
|
"detail.evalResult": "个 eval",
|
|
276
282
|
"detail.evalResults": "个 eval",
|
|
@@ -334,20 +340,23 @@ const dictionaries: Record<Locale, Dictionary> = {
|
|
|
334
340
|
"code.checks": "检查",
|
|
335
341
|
"code.conversation": "会话",
|
|
336
342
|
"code.noSource": "源码未捕获。此 run 可能早于 source-loc,或源码不可读。重跑此 eval 即可看到代码视图。",
|
|
337
|
-
"code.sourceUnavailable": "此 run
|
|
343
|
+
"code.sourceUnavailable": "此 run 捕获过源码,但当前部署里缺少它的 artifact 文件。用 `niceeval view --out <目录>` 重新导出(目录模式会带上 artifact),或在本地 `niceeval view` 查看。",
|
|
338
344
|
"assert.pass": "通过",
|
|
339
345
|
"assert.fail": "失败",
|
|
340
346
|
"assert.soft": "soft",
|
|
341
347
|
"assert.evidence": "实际被检查的内容",
|
|
342
|
-
"
|
|
343
|
-
"
|
|
344
|
-
"
|
|
345
|
-
"
|
|
346
|
-
"banner.skippedTitle": "
|
|
347
|
-
"banner.skipped.incompatible": "由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}})——
|
|
348
|
+
"verdict.passed": "通过",
|
|
349
|
+
"verdict.failed": "失败",
|
|
350
|
+
"verdict.errored": "错误",
|
|
351
|
+
"verdict.skipped": "跳过",
|
|
352
|
+
"banner.skippedTitle": "{{count}} 个 run 读取失败,此处不展示",
|
|
353
|
+
"banner.skipped.incompatible": "由 niceeval {{producer}} 写入(schemaVersion {{schemaVersion}})—— 当前版本读不了,展开查看命令",
|
|
348
354
|
"banner.skipped.incompatibleForeign": "由 {{name}} {{version}} 写入(schemaVersion {{schemaVersion}})—— 当前查看器读不了;请用写出它的工具查看",
|
|
349
|
-
"banner.skipped.malformed": "报告读不了({{detail}})—— 可能已损坏;重跑该 eval
|
|
350
|
-
"banner.skipped.incomplete": "
|
|
355
|
+
"banner.skipped.malformed": "报告读不了({{detail}})—— 可能已损坏;重跑该 eval 或删除对应 run 目录",
|
|
356
|
+
"banner.skipped.incomplete": "快照目录已创建但从未写出 snapshot.json(极窄的崩溃窗口)—— 已完成的 attempt artifact 仍在盘上供手工排查;不需要就删除对应目录",
|
|
357
|
+
"banner.expandRest": "展开其余 {{count}} 个",
|
|
358
|
+
"banner.collapse": "收起",
|
|
359
|
+
"banner.copyCommand": "复制查看命令",
|
|
351
360
|
"banner.warningsTitle": "当前榜单挑选的提醒:",
|
|
352
361
|
"hero.composedFrom": "合成自 {{count}} 个 run",
|
|
353
362
|
"chart.costVsScore": "成本 × 通过率",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// artifact fetch 的 URL:相对路径 `artifact/<rel>`。
|
|
2
2
|
// 本地 dev server(server.ts 的 /artifact/ 路由)和目录式静态导出(buildView 拷到 <out>/artifact/)
|
|
3
3
|
// 共用同一布局,前端不需要知道自己被谁托管。相对路径也保证子路径部署(如 host/foo/)不断链。
|
|
4
4
|
export function artifactUrl(rel: string): string {
|
|
@@ -1,46 +1,39 @@
|
|
|
1
|
-
// #/attempt
|
|
1
|
+
// #/attempt/@<locator> 深链的纯函数单测:解析 / 格式化往返、坏输入、按 locator 定位。
|
|
2
2
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { formatAttemptHash, parseAttemptHash,
|
|
5
|
-
import type { ViewResult, ViewSnapshot } from "../types.ts";
|
|
4
|
+
import { formatAttemptHash, parseAttemptHash, resolveAttemptLocator } from "./attempt-route.ts";
|
|
5
|
+
import type { AttemptLocator, ViewResult, ViewSnapshot } from "../types.ts";
|
|
6
6
|
|
|
7
|
-
const
|
|
7
|
+
const LOCATOR_A = "@1abc123" as AttemptLocator;
|
|
8
|
+
const LOCATOR_B = "@1zzz999" as AttemptLocator;
|
|
9
|
+
|
|
10
|
+
const attempt = (locator: AttemptLocator | undefined, index: number): ViewResult => ({
|
|
8
11
|
id: `demo/eval-${index}`,
|
|
9
12
|
agent: "demo-agent",
|
|
10
|
-
|
|
13
|
+
verdict: "passed",
|
|
11
14
|
attempt: 0,
|
|
12
15
|
durationMs: 1,
|
|
13
16
|
assertions: [],
|
|
14
|
-
|
|
17
|
+
...(locator !== undefined ? { locator } : {}),
|
|
15
18
|
});
|
|
16
19
|
|
|
17
20
|
// 快照只有 results 参与定位,其余元信息字段与路由无关。
|
|
18
21
|
const snap = (results: ViewResult[]): ViewSnapshot => ({ results }) as ViewSnapshot;
|
|
19
22
|
|
|
20
|
-
describe("parseAttemptHash", () => {
|
|
21
|
-
it("parses the canonical
|
|
22
|
-
expect(parseAttemptHash("#/attempt
|
|
23
|
-
run: "2026-07-02T03-10-24-123Z",
|
|
24
|
-
result: 4,
|
|
25
|
-
});
|
|
23
|
+
describe("parseAttemptHash / formatAttemptHash", () => {
|
|
24
|
+
it("parses the canonical `@<scheme><body>` locator shape", () => {
|
|
25
|
+
expect(parseAttemptHash("#/attempt/@1abc123")).toBe("@1abc123");
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it("round-trips through formatAttemptHash
|
|
29
|
-
for (const
|
|
30
|
-
|
|
31
|
-
{ run: "nested/2026-01-01T00-00-00-000Z", result: 12 },
|
|
32
|
-
{ run: "with space", result: 3 },
|
|
33
|
-
{ run: ".", result: 7 }, // 单文件入口的 run 占位
|
|
34
|
-
]) {
|
|
35
|
-
expect(parseAttemptHash(formatAttemptHash(ref))).toEqual(ref);
|
|
28
|
+
it("round-trips through formatAttemptHash", () => {
|
|
29
|
+
for (const locator of [LOCATOR_A, LOCATOR_B, "@10000000" as AttemptLocator]) {
|
|
30
|
+
expect(parseAttemptHash(formatAttemptHash(locator))).toBe(locator);
|
|
36
31
|
}
|
|
37
32
|
});
|
|
38
33
|
|
|
39
|
-
it("
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
result: 12,
|
|
43
|
-
});
|
|
34
|
+
it("formatAttemptHash matches the report tree's DEFAULT_WEB_CONTEXT.attemptHref format exactly", () => {
|
|
35
|
+
// src/report/tree.ts: attemptHref: (locator) => `#/attempt/${locator}` — locator 本身自带 "@" 前缀。
|
|
36
|
+
expect(formatAttemptHash(LOCATOR_A)).toBe(`#/attempt/${LOCATOR_A}`);
|
|
44
37
|
});
|
|
45
38
|
|
|
46
39
|
it("rejects non-attempt hashes and malformed shapes", () => {
|
|
@@ -52,38 +45,35 @@ describe("parseAttemptHash", () => {
|
|
|
52
45
|
"#/compare/a/b",
|
|
53
46
|
"#/attempt",
|
|
54
47
|
"#/attempt/",
|
|
55
|
-
"#/attempt/
|
|
56
|
-
"#/attempt
|
|
57
|
-
"#/attempt
|
|
58
|
-
"#/attempt
|
|
59
|
-
"#/attempt
|
|
60
|
-
"#/attempt/
|
|
61
|
-
"#/attempt/run/1/", // 末段为空
|
|
62
|
-
"#/attempt/%zz/1", // 非法 % 转义
|
|
48
|
+
"#/attempt/exp/snap/eval/a0", // 旧的两段式路由,不再被接受
|
|
49
|
+
"#/attempt/nolocatorprefix",
|
|
50
|
+
"#/attempt/@", // 只有前缀,没有 scheme + body
|
|
51
|
+
"#/attempt/@ABC123", // 大写不合法(base36 恒小写)
|
|
52
|
+
"#/attempt/@1abc 123", // 含空格
|
|
53
|
+
"#/attempt/@1abc/123", // 含 "/"
|
|
63
54
|
]) {
|
|
64
55
|
expect(parseAttemptHash(hash), hash).toBeNull();
|
|
65
56
|
}
|
|
66
57
|
});
|
|
67
58
|
});
|
|
68
59
|
|
|
69
|
-
describe("
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
60
|
+
describe("resolveAttemptLocator", () => {
|
|
61
|
+
const snapshots = [
|
|
62
|
+
snap([attempt(LOCATOR_A, 0), attempt(LOCATOR_B, 1)]),
|
|
63
|
+
snap([attempt("@1other01" as AttemptLocator, 2)]),
|
|
64
|
+
];
|
|
73
65
|
|
|
74
|
-
it("finds the attempt whose injected
|
|
75
|
-
expect(
|
|
76
|
-
expect(
|
|
66
|
+
it("finds the attempt whose injected locator matches", () => {
|
|
67
|
+
expect(resolveAttemptLocator(snapshots, LOCATOR_A)).toBe(snapshots[0]!.results[0]);
|
|
68
|
+
expect(resolveAttemptLocator(snapshots, LOCATOR_B)).toBe(snapshots[0]!.results[1]);
|
|
77
69
|
});
|
|
78
70
|
|
|
79
|
-
it("returns null for unknown
|
|
80
|
-
expect(
|
|
81
|
-
expect(resolveAttemptRef(snapshots, { run: runA, result: 99 })).toBeNull();
|
|
71
|
+
it("returns null for unknown locators", () => {
|
|
72
|
+
expect(resolveAttemptLocator(snapshots, "@1nosuch1" as AttemptLocator)).toBeNull();
|
|
82
73
|
});
|
|
83
74
|
|
|
84
|
-
it("returns null when results predate
|
|
85
|
-
const legacy = attempt(
|
|
86
|
-
|
|
87
|
-
expect(resolveAttemptRef([snap([legacy])], { run: runA, result: 0 })).toBeNull();
|
|
75
|
+
it("returns null when results predate locator injection (old baked data)", () => {
|
|
76
|
+
const legacy = attempt(undefined, 0);
|
|
77
|
+
expect(resolveAttemptLocator([snap([legacy])], LOCATOR_A)).toBeNull();
|
|
88
78
|
});
|
|
89
79
|
});
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
// attempt 级 hash 深链:`#/attempt
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// attempt 级 hash 深链:`#/attempt/@<locator>`(docs/feature/reports/view.md「打开与收窄」)。
|
|
2
|
+
// 路由参数就是不透明的 AttemptLocator(src/results/locator.ts),由 loader 注入到每条
|
|
3
|
+
// result 上;这里只做纯解析 / 格式化 / 匹配,不碰 location / history,方便单测。与报告槽
|
|
4
|
+
// DEFAULT_WEB_CONTEXT.attemptHref(src/report/tree.ts)同一格式,两条深链来源互通。
|
|
4
5
|
// hash 目前只有这一种路由:tab 切换是纯组件 state,旧版 modal 深链走 ?modal= 查询参数,互不占用。
|
|
6
|
+
//
|
|
7
|
+
// AttemptLocator 的编码/解码本体住在 src/results/locator.ts,但那个模块顶层 import 了
|
|
8
|
+
// node:crypto(encodeAttemptLocator 用于生成 locator),不能被这个浏览器打包的 app/ 目录
|
|
9
|
+
// 静态 import——这里只需要「像不像一个 locator」的轻量语法校验,不需要真校验 scheme/body
|
|
10
|
+
// 长度(那是 reader 建索引时的事,view 前端拿到的 locator 恒来自可信的 loader 注入)。
|
|
5
11
|
|
|
6
|
-
import type {
|
|
12
|
+
import type { AttemptLocator, ViewResult, ViewSnapshot } from "../types.ts";
|
|
7
13
|
|
|
8
14
|
export const ATTEMPT_HASH_PREFIX = "#/attempt/";
|
|
9
15
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
/** locator 串的最小形状:`@` + 至少一个 base36 字符(scheme 字符 + body)。 */
|
|
17
|
+
const LOCATOR_SHAPE = /^@[0-9a-z]+$/;
|
|
18
|
+
|
|
19
|
+
/** AttemptLocator → 可分享的 hash:locator 本身就是路由参数,原样拼在前缀后面,不需要分段编码。 */
|
|
20
|
+
export function formatAttemptHash(locator: AttemptLocator): string {
|
|
21
|
+
return `${ATTEMPT_HASH_PREFIX}${locator}`;
|
|
13
22
|
}
|
|
14
23
|
|
|
15
24
|
/**
|
|
16
|
-
* hash →
|
|
17
|
-
*
|
|
25
|
+
* hash → AttemptLocator;不是本路由 / 形状不像 locator 返回 null(由调用方决定 warn 与否)。
|
|
26
|
+
* 只做前缀 + 粗粒度字符集校验,不重新实现 decodeAttemptLocator 的 scheme/body 长度校验——
|
|
27
|
+
* 那份权威校验属于 src/results/locator.ts,这里刻意保持浏览器打包安全(不引入 node:crypto)。
|
|
18
28
|
*/
|
|
19
|
-
export function parseAttemptHash(hash: string):
|
|
29
|
+
export function parseAttemptHash(hash: string): AttemptLocator | null {
|
|
20
30
|
if (!hash.startsWith(ATTEMPT_HASH_PREFIX)) return null;
|
|
21
31
|
const rest = hash.slice(ATTEMPT_HASH_PREFIX.length);
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const indexPart = rest.slice(cut + 1);
|
|
25
|
-
if (!/^\d+$/.test(indexPart)) return null;
|
|
26
|
-
let run: string;
|
|
27
|
-
try {
|
|
28
|
-
run = decodeURIComponent(rest.slice(0, cut));
|
|
29
|
-
} catch {
|
|
30
|
-
return null; // 非法 % 转义
|
|
31
|
-
}
|
|
32
|
-
if (!run) return null;
|
|
33
|
-
return { run, result: parseInt(indexPart, 10) };
|
|
32
|
+
if (!LOCATOR_SHAPE.test(rest)) return null;
|
|
33
|
+
return rest as AttemptLocator;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
/** 在全部快照(含历史)里找
|
|
37
|
-
export function
|
|
36
|
+
/** 在全部快照(含历史)里找 locator 指向的 attempt;旧格式烘焙的数据没有 locator,自然找不到。 */
|
|
37
|
+
export function resolveAttemptLocator(snapshots: ViewSnapshot[], locator: AttemptLocator): ViewResult | null {
|
|
38
38
|
for (const snapshot of snapshots) {
|
|
39
39
|
for (const result of snapshot.results ?? []) {
|
|
40
|
-
if (result.
|
|
40
|
+
if (result.locator === locator) return result;
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
@@ -47,6 +47,6 @@ export function resolveAttemptRef(snapshots: ViewSnapshot[], ref: AttemptRef): V
|
|
|
47
47
|
export function unresolvedAttemptWarning(hash: string): string {
|
|
48
48
|
return (
|
|
49
49
|
`[niceeval view] Ignoring attempt link "${hash}": no matching attempt in this view ` +
|
|
50
|
-
`(
|
|
50
|
+
`(snapshot not loaded, attempt not found, or the data was baked without a locator).`
|
|
51
51
|
);
|
|
52
52
|
}
|