niceeval 0.11.4-canary.21 → 0.12.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/dist/report/assets/series-encoding.d.ts +36 -0
- package/dist/report/assets/series-encoding.js +104 -0
- package/dist/report/components/entity-lists/compute.js +5 -17
- package/dist/report/components/entity-lists/content.d.ts +2 -0
- package/dist/report/components/entity-lists/content.js +22 -7
- package/dist/report/definition/cell.d.ts +4 -0
- package/dist/report/definition/cell.js +5 -1
- package/dist/report/definition/primitives/chart.js +125 -25
- package/dist/report/definition/primitives.js +3 -2
- package/dist/report/definition/tree.d.ts +5 -2
- package/dist/report/definition/tree.js +1 -0
- package/dist/report/index.d.ts +1 -1
- package/dist/report/model/aggregate.d.ts +10 -1
- package/dist/report/model/aggregate.js +24 -0
- package/dist/report/model/locale.d.ts +0 -1
- package/dist/report/model/locale.js +0 -2
- package/dist/report/model/types.d.ts +8 -0
- package/dist/report/presentation.d.ts +53 -6
- package/dist/report/presentation.js +40 -5
- package/dist/report/react/index.d.ts +1 -1
- package/dist/report/slices/compute.js +52 -4
- package/dist/report/slices/content.d.ts +2 -1
- package/dist/report/slices/content.js +48 -15
- package/dist/report/slices/validate.js +23 -0
- package/package.json +1 -1
- package/src/report/assets/series-encoding.tsx +177 -0
- package/src/report/assets/styles.css +47 -5
- package/src/report/components/compute.test.ts +15 -9
- package/src/report/components/entity-lists/compute.ts +5 -19
- package/src/report/components/entity-lists/content.test.ts +13 -8
- package/src/report/components/entity-lists/content.ts +24 -7
- package/src/report/definition/cell.ts +8 -1
- package/src/report/definition/primitives/chart.tsx +197 -32
- package/src/report/definition/primitives.tsx +8 -3
- package/src/report/definition/tree.ts +6 -2
- package/src/report/index.ts +11 -1
- package/src/report/model/aggregate.ts +26 -0
- package/src/report/model/locale.ts +0 -2
- package/src/report/model/types.ts +8 -0
- package/src/report/presentation.test.tsx +179 -19
- package/src/report/presentation.ts +114 -11
- package/src/report/react/index.tsx +11 -1
- package/src/report/slices/compute.ts +59 -4
- package/src/report/slices/content.ts +55 -16
- package/src/report/slices/delta-table.test.ts +138 -3
- package/src/report/slices/validate.test.ts +29 -0
- package/src/report/slices/validate.ts +16 -0
- package/src/runner/gate-lease.test.ts +38 -14
- package/src/runner/lock.test.ts +40 -16
- package/src/sandbox/compose.ts +3 -1
- package/src/sandbox/dockerfile-build.ts +4 -1
- package/src/sandbox/runtime.ts +4 -1
- package/src/sandbox/vercel.test.ts +12 -3
- package/src/show/index.ts +10 -9
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// 24 视觉身份 = 六色 × 四变体的可直接使用 SVG/CSS 值。
|
|
2
|
+
// 契约:docs/feature/reports/library/presentation.md「实验颜色与维度呈现」、
|
|
3
|
+
// docs/feature/reports/components/README.md「视觉编码容量(24 个身份)」。
|
|
4
|
+
// pattern id 全局唯一、颜色走主题令牌 --niceeval-color-series-N,换 basalt/chalk 自动换色。
|
|
5
|
+
|
|
6
|
+
import type { ReactNode } from "react";
|
|
7
|
+
import type { SeriesVariant } from "../presentation.ts";
|
|
8
|
+
|
|
9
|
+
/** 槽位色板下标 1..6 → CSS 变量,原样交给 fill / stroke / color。 */
|
|
10
|
+
export function seriesColorVar(colorIndex: number): string {
|
|
11
|
+
if (!Number.isInteger(colorIndex) || colorIndex < 1 || colorIndex > 6) {
|
|
12
|
+
throw new Error(`colorIndex must be an integer in [1, 6], got ${colorIndex}.`);
|
|
13
|
+
}
|
|
14
|
+
return `var(--niceeval-color-series-${colorIndex})`;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** SVG pattern id:niceeval-series-pat-v{2|3|4}-c{1..6}。variant 1 是实心,无 pattern。 */
|
|
18
|
+
export function seriesPatternId(colorIndex: number, variant: SeriesVariant): string {
|
|
19
|
+
if (variant === 1) {
|
|
20
|
+
throw new Error("variant 1 is solid fill; it has no pattern id.");
|
|
21
|
+
}
|
|
22
|
+
return `niceeval-series-pat-v${variant}-c${colorIndex}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function seriesFill(colorIndex: number, variant: SeriesVariant): string {
|
|
26
|
+
if (variant === 1) return seriesColorVar(colorIndex);
|
|
27
|
+
return `url(#${seriesPatternId(colorIndex, variant)})`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 线型变体 → strokeDasharray;variant 1 为空串(实线)。 */
|
|
31
|
+
export function seriesStrokeDasharray(variant: SeriesVariant): string {
|
|
32
|
+
switch (variant) {
|
|
33
|
+
case 1:
|
|
34
|
+
return "";
|
|
35
|
+
case 2:
|
|
36
|
+
return "6 4";
|
|
37
|
+
case 3:
|
|
38
|
+
return "2 3";
|
|
39
|
+
case 4:
|
|
40
|
+
return "8 3 2 3";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface SeriesMarkerShape {
|
|
45
|
+
readonly path: string;
|
|
46
|
+
readonly viewBox: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 四种 marker 形状(viewBox 0 0 12 12),变体按 docs 槽序表 1–4。 */
|
|
50
|
+
export function seriesMarkerShape(variant: SeriesVariant): SeriesMarkerShape {
|
|
51
|
+
switch (variant) {
|
|
52
|
+
case 1:
|
|
53
|
+
// 圆
|
|
54
|
+
return { path: "M6 1.5a4.5 4.5 0 1 0 0.01 0z", viewBox: "0 0 12 12" };
|
|
55
|
+
case 2:
|
|
56
|
+
// 方
|
|
57
|
+
return { path: "M2.5 2.5h7v7h-7z", viewBox: "0 0 12 12" };
|
|
58
|
+
case 3:
|
|
59
|
+
// 菱
|
|
60
|
+
return { path: "M6 1.5 10.5 6 6 10.5 1.5 6z", viewBox: "0 0 12 12" };
|
|
61
|
+
case 4:
|
|
62
|
+
// 三角
|
|
63
|
+
return { path: "M6 1.5 10.5 10.5 1.5 10.5z", viewBox: "0 0 12 12" };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function seriesMarker(colorIndex: number, variant: SeriesVariant): {
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly viewBox: string;
|
|
70
|
+
readonly fill: string;
|
|
71
|
+
readonly stroke: string;
|
|
72
|
+
} {
|
|
73
|
+
const shape = seriesMarkerShape(variant);
|
|
74
|
+
const color = seriesColorVar(colorIndex);
|
|
75
|
+
return {
|
|
76
|
+
path: shape.path,
|
|
77
|
+
viewBox: shape.viewBox,
|
|
78
|
+
fill: color,
|
|
79
|
+
stroke: color,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 从 presentation.fill 反推 HTML 柱需要的系列 class。
|
|
85
|
+
* SVG 用 url(#pattern)/var();HTML 柱不能引用 SVG pattern,改挂 series-cN + fill-vN 类,
|
|
86
|
+
* 由 styles.css 用 repeating-linear-gradient 画等效图案,颜色仍走 --series 令牌。
|
|
87
|
+
*/
|
|
88
|
+
export function seriesClassesFromFill(fill: string): string {
|
|
89
|
+
const pattern = /^url\(#niceeval-series-pat-v([2-4])-c([1-6])\)$/.exec(fill);
|
|
90
|
+
if (pattern) {
|
|
91
|
+
const variant = pattern[1]!;
|
|
92
|
+
const colorIndex = Number(pattern[2]);
|
|
93
|
+
return `niceeval-series-c${colorIndex - 1} niceeval-series-fill-v${variant}`;
|
|
94
|
+
}
|
|
95
|
+
const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(fill);
|
|
96
|
+
if (solid) {
|
|
97
|
+
return `niceeval-series-c${Number(solid[1]) - 1}`;
|
|
98
|
+
}
|
|
99
|
+
return "niceeval-series-none";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** 从 color 呈现或 series 的 stroke/fill 反推 series-cN 类(图例色点回落)。 */
|
|
103
|
+
export function seriesClassFromColorVar(color: string): string {
|
|
104
|
+
const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(color);
|
|
105
|
+
if (solid) return `niceeval-series-c${Number(solid[1]) - 1}`;
|
|
106
|
+
return "niceeval-series-none";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 页内注入一次的 SVG pattern defs。
|
|
111
|
+
* 18 个 pattern(3 非实心变体 × 6 色);id 与 seriesFill() 产出的 url(#…) 对齐。
|
|
112
|
+
* 子元素直接写 var(--niceeval-color-series-N),不走 currentColor
|
|
113
|
+
* (SVG pattern 内部 currentColor 取自 pattern 自身,引用者传不进来)。
|
|
114
|
+
*/
|
|
115
|
+
export function SeriesPatternDefs(): ReactNode {
|
|
116
|
+
const patterns: ReactNode[] = [];
|
|
117
|
+
for (let colorIndex = 1; colorIndex <= 6; colorIndex++) {
|
|
118
|
+
const color = seriesColorVar(colorIndex);
|
|
119
|
+
// v2:对角斜线
|
|
120
|
+
patterns.push(
|
|
121
|
+
<pattern
|
|
122
|
+
key={`v2-c${colorIndex}`}
|
|
123
|
+
id={seriesPatternId(colorIndex, 2)}
|
|
124
|
+
patternUnits="userSpaceOnUse"
|
|
125
|
+
width="6"
|
|
126
|
+
height="6"
|
|
127
|
+
>
|
|
128
|
+
<rect width="6" height="6" fill={color} fillOpacity={0.22} />
|
|
129
|
+
<path
|
|
130
|
+
d="M-1 1l2-2M0 6l6-6M5 7l2-2"
|
|
131
|
+
stroke={color}
|
|
132
|
+
strokeWidth={1.4}
|
|
133
|
+
fill="none"
|
|
134
|
+
/>
|
|
135
|
+
</pattern>,
|
|
136
|
+
);
|
|
137
|
+
// v3:水平条纹
|
|
138
|
+
patterns.push(
|
|
139
|
+
<pattern
|
|
140
|
+
key={`v3-c${colorIndex}`}
|
|
141
|
+
id={seriesPatternId(colorIndex, 3)}
|
|
142
|
+
patternUnits="userSpaceOnUse"
|
|
143
|
+
width="6"
|
|
144
|
+
height="6"
|
|
145
|
+
>
|
|
146
|
+
<rect width="6" height="6" fill={color} fillOpacity={0.18} />
|
|
147
|
+
<path d="M0 1.5h6M0 4.5h6" stroke={color} strokeWidth={1.5} fill="none" />
|
|
148
|
+
</pattern>,
|
|
149
|
+
);
|
|
150
|
+
// v4:点阵
|
|
151
|
+
patterns.push(
|
|
152
|
+
<pattern
|
|
153
|
+
key={`v4-c${colorIndex}`}
|
|
154
|
+
id={seriesPatternId(colorIndex, 4)}
|
|
155
|
+
patternUnits="userSpaceOnUse"
|
|
156
|
+
width="6"
|
|
157
|
+
height="6"
|
|
158
|
+
>
|
|
159
|
+
<rect width="6" height="6" fill={color} fillOpacity={0.16} />
|
|
160
|
+
<circle cx="2" cy="2" r="1.15" fill={color} />
|
|
161
|
+
<circle cx="5" cy="5" r="1.15" fill={color} />
|
|
162
|
+
</pattern>,
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return (
|
|
166
|
+
<svg
|
|
167
|
+
className="niceeval-series-defs"
|
|
168
|
+
width={0}
|
|
169
|
+
height={0}
|
|
170
|
+
aria-hidden="true"
|
|
171
|
+
focusable="false"
|
|
172
|
+
style={{ position: "absolute", width: 0, height: 0, overflow: "hidden" }}
|
|
173
|
+
>
|
|
174
|
+
<defs>{patterns}</defs>
|
|
175
|
+
</svg>
|
|
176
|
+
);
|
|
177
|
+
}
|
|
@@ -110,6 +110,41 @@
|
|
|
110
110
|
.niceeval-report .niceeval-series-c5 { --series: var(--c5); }
|
|
111
111
|
.niceeval-report .niceeval-series-none { --series: var(--muted); }
|
|
112
112
|
|
|
113
|
+
/*
|
|
114
|
+
* HTML 横向柱的 variant 填充图案(CSS 等效 SVG <pattern>)。
|
|
115
|
+
* variant 1 = 实心(默认 .niceeval-chart-bar-fill);2 对角斜线 / 3 水平条纹 / 4 点阵。
|
|
116
|
+
* 颜色始终读 --series,换主题令牌自动换色,不写死 hex。
|
|
117
|
+
*/
|
|
118
|
+
.niceeval-report .niceeval-series-fill-v2 {
|
|
119
|
+
background-color: transparent;
|
|
120
|
+
background-image: repeating-linear-gradient(
|
|
121
|
+
-45deg,
|
|
122
|
+
color-mix(in srgb, var(--series) 78%, var(--panel)),
|
|
123
|
+
color-mix(in srgb, var(--series) 78%, var(--panel)) 2px,
|
|
124
|
+
color-mix(in srgb, var(--series) 28%, var(--panel)) 2px,
|
|
125
|
+
color-mix(in srgb, var(--series) 28%, var(--panel)) 5px
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
.niceeval-report .niceeval-series-fill-v3 {
|
|
129
|
+
background-color: transparent;
|
|
130
|
+
background-image: repeating-linear-gradient(
|
|
131
|
+
0deg,
|
|
132
|
+
color-mix(in srgb, var(--series) 78%, var(--panel)),
|
|
133
|
+
color-mix(in srgb, var(--series) 78%, var(--panel)) 2px,
|
|
134
|
+
color-mix(in srgb, var(--series) 28%, var(--panel)) 2px,
|
|
135
|
+
color-mix(in srgb, var(--series) 28%, var(--panel)) 5px
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
.niceeval-report .niceeval-series-fill-v4 {
|
|
139
|
+
background-color: color-mix(in srgb, var(--series) 28%, var(--panel));
|
|
140
|
+
background-image: radial-gradient(
|
|
141
|
+
circle at 25% 25%,
|
|
142
|
+
color-mix(in srgb, var(--series) 88%, var(--panel)) 1.2px,
|
|
143
|
+
transparent 1.3px
|
|
144
|
+
);
|
|
145
|
+
background-size: 6px 6px;
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
/* ---- 表格通用:圆角 panel、细分隔线、tabular-nums 数字、uppercase 小标签 ---- */
|
|
114
149
|
table.niceeval-report,
|
|
115
150
|
.niceeval-report table {
|
|
@@ -1183,12 +1218,19 @@ table.niceeval-report,
|
|
|
1183
1218
|
align-items: center;
|
|
1184
1219
|
gap: 5px;
|
|
1185
1220
|
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1221
|
+
/* 图例色块由 renderer 显式输出(含 variant 图案 / 线型 / marker),不再用 ::before 单色方块。 */
|
|
1222
|
+
.niceeval-chart .niceeval-chart-legend-swatch {
|
|
1223
|
+
display: inline-block;
|
|
1224
|
+
width: 10px;
|
|
1225
|
+
height: 10px;
|
|
1226
|
+
flex: 0 0 auto;
|
|
1190
1227
|
border-radius: var(--radius);
|
|
1191
|
-
background: var(--series);
|
|
1228
|
+
background: color-mix(in srgb, var(--series) 74%, var(--panel));
|
|
1229
|
+
}
|
|
1230
|
+
.niceeval-chart .niceeval-chart-legend-swatch-svg {
|
|
1231
|
+
display: block;
|
|
1232
|
+
flex: 0 0 auto;
|
|
1233
|
+
overflow: visible;
|
|
1192
1234
|
}
|
|
1193
1235
|
.niceeval-chart .niceeval-chart-missing {
|
|
1194
1236
|
max-width: 880px;
|
|
@@ -18,7 +18,7 @@ import type { AttemptHandle, Sample, SampleIssue, Run } from "../../record/index
|
|
|
18
18
|
import { attemptHandleOf, scopeOf } from "./scope.harness.ts";
|
|
19
19
|
import { makeSample } from "../../sample/index.ts";
|
|
20
20
|
import { encodeAttemptLocator } from "../../record/locator.ts";
|
|
21
|
-
import { experimentListContent } from "./entity-lists/content.ts";
|
|
21
|
+
import { COVERAGE_ROW_PREFIX, experimentListContent } from "./entity-lists/content.ts";
|
|
22
22
|
import { formatCellText, type Cell } from "../definition/cell.ts";
|
|
23
23
|
import type { Record } from "../../record/types.ts";
|
|
24
24
|
import {
|
|
@@ -844,10 +844,16 @@ describe("实体列表 data", () => {
|
|
|
844
844
|
]);
|
|
845
845
|
}
|
|
846
846
|
|
|
847
|
+
/** experiment 行的覆盖构成副行(subRows 末尾,key 以 `coverage:` 起头)的 record 格。 */
|
|
848
|
+
function coverageCellOf(content: ReturnType<typeof experimentListContent>, experimentId: string): Cell {
|
|
849
|
+
const row = content.rows[0]!.subRows!.find((r) => r.key === `${COVERAGE_ROW_PREFIX}${experimentId}`)!;
|
|
850
|
+
return row.cells.record!;
|
|
851
|
+
}
|
|
852
|
+
|
|
847
853
|
it("四段互斥且合计等于已知题数;一题同时有新执行与携带 attempt 时只落新执行段", async () => {
|
|
848
854
|
const [item] = await experimentListData(compositionScope());
|
|
849
855
|
const content = experimentListContent([item!]);
|
|
850
|
-
const coverageCell = content
|
|
856
|
+
const coverageCell = coverageCellOf(content, "exp/composition");
|
|
851
857
|
if (coverageCell.kind !== "composition") throw new Error("expected composition cell");
|
|
852
858
|
const byLabel = new Map(
|
|
853
859
|
coverageCell.segments.map((segment) => [
|
|
@@ -867,7 +873,7 @@ describe("实体列表 data", () => {
|
|
|
867
873
|
const s = snap({ experimentId: "exp/all-fresh", results: [res("q", "passed")] });
|
|
868
874
|
const [item] = await experimentListData([s]);
|
|
869
875
|
const content = experimentListContent([item!]);
|
|
870
|
-
const coverageCell = content
|
|
876
|
+
const coverageCell = coverageCellOf(content, "exp/all-fresh");
|
|
871
877
|
if (coverageCell.kind !== "composition") throw new Error("expected composition cell");
|
|
872
878
|
expect(coverageCell.segments.filter((s) => s.count > 0)).toHaveLength(1);
|
|
873
879
|
expect(formatCellText(coverageCell, "en")).toBe("1 fresh");
|
|
@@ -877,7 +883,7 @@ describe("实体列表 data", () => {
|
|
|
877
883
|
it("段名走 LocalizedText:zh-CN 与 en 取同一份 segments,只有文案不同,计数与段序逐字相同", async () => {
|
|
878
884
|
const [item] = await experimentListData(compositionScope());
|
|
879
885
|
const content = experimentListContent([item!]);
|
|
880
|
-
const coverageCell = content
|
|
886
|
+
const coverageCell = coverageCellOf(content, "exp/composition");
|
|
881
887
|
if (coverageCell.kind !== "composition") throw new Error("expected composition cell");
|
|
882
888
|
const en = formatCellText(coverageCell, "en");
|
|
883
889
|
const zh = formatCellText(coverageCell, "zh-CN");
|
|
@@ -886,13 +892,13 @@ describe("实体列表 data", () => {
|
|
|
886
892
|
expect(coverageCell.segments.map((s) => s.count)).toEqual([1, 1, 1, 1]);
|
|
887
893
|
});
|
|
888
894
|
|
|
889
|
-
it("
|
|
895
|
+
it("构成格不携带业务语义:它是 experiment 副行独有的格,换一套无关段名照常渲染,渲染面不认识段含义", async () => {
|
|
890
896
|
const [item] = await experimentListData(compositionScope());
|
|
891
897
|
const content = experimentListContent([item!]);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
expect(
|
|
898
|
+
// 副行紧跟在 Eval / 组行之后,key 前缀把它与题目行区分开——它不是又一道 "retry" 之类的题。
|
|
899
|
+
const rowKeys = content.rows[0]!.subRows!.map((r) => r.key);
|
|
900
|
+
expect(rowKeys.at(-1)).toBe(`${COVERAGE_ROW_PREFIX}exp/composition`);
|
|
901
|
+
expect(rowKeys).toContain("retry");
|
|
896
902
|
|
|
897
903
|
const arbitrarySegments: Cell = {
|
|
898
904
|
kind: "composition",
|
|
@@ -18,7 +18,6 @@ import type {
|
|
|
18
18
|
} from "../../model/types.ts";
|
|
19
19
|
import type { EvalResult } from "../../../types.ts";
|
|
20
20
|
import type { AttemptHandle, Run } from "../../../record/types.ts";
|
|
21
|
-
import { encodeAttemptLocator } from "../../../record/locator.ts";
|
|
22
21
|
import { comparabilityConfigOf, deepEqualJson } from "../../../sample/index.ts";
|
|
23
22
|
import { foldEvalVerdict } from "../../../shared/verdict.ts";
|
|
24
23
|
import {
|
|
@@ -30,7 +29,9 @@ import {
|
|
|
30
29
|
groupItems,
|
|
31
30
|
historicalOf,
|
|
32
31
|
locatorOf,
|
|
32
|
+
msSince,
|
|
33
33
|
resolveInput,
|
|
34
|
+
staleReferenceOf,
|
|
34
35
|
type Item,
|
|
35
36
|
} from "../../model/aggregate.ts";
|
|
36
37
|
import { attemptCostUSD, costUSD, durationMs, examScore, passRate, tokens, totalScore } from "../../model/metrics.ts";
|
|
@@ -95,16 +96,11 @@ async function attemptListItemOf(item: Item): Promise<AttemptListItem> {
|
|
|
95
96
|
costUSD: attemptCostUSD(result),
|
|
96
97
|
startedAt,
|
|
97
98
|
historical,
|
|
98
|
-
...(historical ? { staleSinceMs:
|
|
99
|
+
...(historical ? { staleSinceMs: msSince(startedAt) } : {}),
|
|
99
100
|
locator: locatorOf(item),
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
/** 一个 ISO 时刻距渲染时刻(`Date.now()`)的毫秒数,恒不小于 0。 */
|
|
104
|
-
function staleSinceMsOf(startedAtIso: string): number {
|
|
105
|
-
return Math.max(0, Date.now() - Date.parse(startedAtIso));
|
|
106
|
-
}
|
|
107
|
-
|
|
108
104
|
/**
|
|
109
105
|
* 「只看新执行」开关在场的判据(docs/feature/reports/components/summaries/experiment-table.md
|
|
110
106
|
* 「只看新执行」):Sample 里既没有历史执行也没有过期结论时不画开关——一个永远不改变行集的
|
|
@@ -238,18 +234,8 @@ function staleReferencesFor(
|
|
|
238
234
|
}
|
|
239
235
|
const out: globalThis.Record<string, StaleConclusionReference> = {};
|
|
240
236
|
for (const [evalId, candidates] of candidatesByEval) {
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
);
|
|
244
|
-
const startedAt = newest.result.startedAt;
|
|
245
|
-
if (!startedAt) continue; // 无时刻的 legacy 落盘算不出时距,不伪造参考
|
|
246
|
-
out[evalId] = {
|
|
247
|
-
locator:
|
|
248
|
-
newest.locator ??
|
|
249
|
-
encodeAttemptLocator({ runId: newest.run.runId, evalId: newest.evalId, attempt: newest.result.attempt }),
|
|
250
|
-
verdict: newest.result.verdict,
|
|
251
|
-
staleSinceMs: staleSinceMsOf(startedAt),
|
|
252
|
-
};
|
|
237
|
+
const reference = staleReferenceOf(candidates);
|
|
238
|
+
if (reference) out[evalId] = reference;
|
|
253
239
|
}
|
|
254
240
|
return out;
|
|
255
241
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { describe, expect, it } from "vitest";
|
|
8
8
|
import type { AttemptListItem, ExperimentListEvalRow, ExperimentListItem, MetricValue } from "../../model/types.ts";
|
|
9
|
-
import { attemptListContent, evalListContent, experimentListContent } from "./content.ts";
|
|
9
|
+
import { attemptListContent, COVERAGE_ROW_PREFIX, evalListContent, experimentListContent } from "./content.ts";
|
|
10
10
|
import type { Cell, TableContentRow } from "../../definition/cell.ts";
|
|
11
11
|
import { resolveLocalizedText } from "../../model/locale.ts";
|
|
12
12
|
import type { AttemptLocator } from "../../../record/locator.ts";
|
|
@@ -105,6 +105,11 @@ function entityText(cell: Cell | undefined): string {
|
|
|
105
105
|
return "";
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
+
/** experiment 行的 subRows 排除末尾的覆盖构成副行,只留 Eval / 组行(Eval 分组层的断言面)。 */
|
|
109
|
+
function realSubRows(row: TableContentRow): TableContentRow[] {
|
|
110
|
+
return (row.subRows ?? []).filter((r) => !r.key.startsWith(COVERAGE_ROW_PREFIX));
|
|
111
|
+
}
|
|
112
|
+
|
|
108
113
|
describe("experimentListContent Eval 分组层", () => {
|
|
109
114
|
it("按目录前缀分区:组行带聚合读数,子行去掉前缀但 key 仍是完整 evalId", () => {
|
|
110
115
|
const content = experimentListContent([
|
|
@@ -118,7 +123,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
118
123
|
missingEvalIds: [],
|
|
119
124
|
}),
|
|
120
125
|
]);
|
|
121
|
-
const sub = content.rows[0]
|
|
126
|
+
const sub = realSubRows(content.rows[0]!);
|
|
122
127
|
expect(sub.map((row) => row.variant)).toEqual(["group", "group"]);
|
|
123
128
|
expect(sub.map((row) => row.key)).toEqual(["group:downshift", "group:weather"]);
|
|
124
129
|
// 两组通过率都是 50%,按 groupKey 字典序收口
|
|
@@ -145,7 +150,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
145
150
|
missingEvalIds: [],
|
|
146
151
|
}),
|
|
147
152
|
]);
|
|
148
|
-
const sub = content.rows[0]
|
|
153
|
+
const sub = realSubRows(content.rows[0]!);
|
|
149
154
|
expect(sub.every((row) => row.variant !== "group")).toBe(true);
|
|
150
155
|
expect(sub.map((row) => row.key)).toEqual(["algebra/retry", "algebra/simple"]);
|
|
151
156
|
expect(sub.map((row) => entityText(row.cells.entity))).toEqual(["algebra/retry", "algebra/simple"]);
|
|
@@ -161,7 +166,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
161
166
|
missingEvalIds: [],
|
|
162
167
|
}),
|
|
163
168
|
]);
|
|
164
|
-
const sub = content.rows[0]
|
|
169
|
+
const sub = realSubRows(content.rows[0]!);
|
|
165
170
|
expect(sub.every((row) => row.variant !== "group")).toBe(true);
|
|
166
171
|
expect(sub.map((row) => entityText(row.cells.entity))).toEqual(["algebra/retry", "weather/tool"]);
|
|
167
172
|
});
|
|
@@ -177,7 +182,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
177
182
|
missingEvalIds: [],
|
|
178
183
|
}),
|
|
179
184
|
]);
|
|
180
|
-
const sub = content.rows[0]
|
|
185
|
+
const sub = realSubRows(content.rows[0]!);
|
|
181
186
|
expect(sub.map((row) => row.key)).toEqual(["group:downshift", "standalone"]);
|
|
182
187
|
expect(sub[0]!.variant).toBe("group");
|
|
183
188
|
expect(sub[1]!.variant).toBeUndefined();
|
|
@@ -197,7 +202,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
197
202
|
missingEvalIds: ["weather/gap", "ghost/a", "ghost/b"],
|
|
198
203
|
}),
|
|
199
204
|
]);
|
|
200
|
-
const sub = content.rows[0]
|
|
205
|
+
const sub = realSubRows(content.rows[0]!);
|
|
201
206
|
// weather 有 2 道实题 + 1 占位 → 保留组;ghost 两道全缺失也保留组
|
|
202
207
|
expect(sub.map((row) => row.key)).toEqual(["group:weather", "group:ghost"]);
|
|
203
208
|
|
|
@@ -295,7 +300,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
295
300
|
missingEvalIds: [],
|
|
296
301
|
}),
|
|
297
302
|
]);
|
|
298
|
-
const sub = content.rows[0]
|
|
303
|
+
const sub = realSubRows(content.rows[0]!);
|
|
299
304
|
// 顶层只有 pkg → 剥壳;下层 sub/other 各两题 → 插组
|
|
300
305
|
expect(sub.map((row) => row.key)).toEqual(["group:pkg/other", "group:pkg/sub"]);
|
|
301
306
|
expect(sub.map((row) => entityText(row.cells.entity))).toEqual(["other (2 evals)", "sub (2 evals)"]);
|
|
@@ -314,7 +319,7 @@ describe("experimentListContent Eval 分组层", () => {
|
|
|
314
319
|
missingEvalIds: [],
|
|
315
320
|
}),
|
|
316
321
|
]);
|
|
317
|
-
const sub = content.rows[0]
|
|
322
|
+
const sub = realSubRows(content.rows[0]!);
|
|
318
323
|
expect(sub.every((row) => row.variant !== "group")).toBe(true);
|
|
319
324
|
expect(sub.map((row) => entityText(row.cells.entity))).toEqual(["111/111/aaa", "111/111/bbb"]);
|
|
320
325
|
});
|
|
@@ -37,7 +37,6 @@ const HEADER = {
|
|
|
37
37
|
verdict: localizedMessage("experimentList.status"),
|
|
38
38
|
result: localizedMessage("experimentList.result"),
|
|
39
39
|
score: localizedMessage("experimentList.totalScore"),
|
|
40
|
-
coverage: localizedMessage("experimentList.coverage"),
|
|
41
40
|
};
|
|
42
41
|
|
|
43
42
|
/**
|
|
@@ -479,13 +478,35 @@ function coverageSegments(item: ExperimentListItem): { label: LocalizedText; cou
|
|
|
479
478
|
}));
|
|
480
479
|
}
|
|
481
480
|
|
|
482
|
-
/**
|
|
481
|
+
/** 覆盖构成副行的 key 前缀;测试与消费方靠它把这一行从 Eval / 组行里筛出去。 */
|
|
482
|
+
export const COVERAGE_ROW_PREFIX = "coverage:";
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* 覆盖构成副行(docs/feature/reports/components/summaries/experiment-table.md「覆盖构成」):
|
|
486
|
+
* experiment 行 subRows 的最后一条,把已知题按结论出身分成四段互斥的构成格,交给中立的
|
|
487
|
+
* `composition` 格——渲染在同一个 `record` 列位置(与 Eval / Attempt 行的判定构成、占位行的
|
|
488
|
+
* missing 格同一个槽位,三种形态各自对应不同的行语义,不是同一行的三种读法)。
|
|
489
|
+
* 它不是 Eval / 组行,没有身份(entity 是 notApplicable),不参与嵌套排序或收起判定。
|
|
490
|
+
*/
|
|
491
|
+
function coverageRow(item: ExperimentListItem, view: HierarchyView): TableContentRow {
|
|
492
|
+
const bag: CellBag = {
|
|
493
|
+
entity: { kind: "notApplicable" },
|
|
494
|
+
record: { kind: "composition", segments: coverageSegments(item) },
|
|
495
|
+
};
|
|
496
|
+
return {
|
|
497
|
+
key: `${COVERAGE_ROW_PREFIX}${item.experimentId}`,
|
|
498
|
+
cells: projectCells(bag, view.columns),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/** experiment 的 evalRows + missingEvalIds → 递归嵌套的 subRows,末尾追加覆盖构成副行。 */
|
|
483
503
|
function experimentSubRows(item: ExperimentListItem, view: HierarchyView): TableContentRow[] {
|
|
484
504
|
const members: LeafMember[] = [
|
|
485
505
|
...item.evalRows.map((row): LeafMember => ({ kind: "eval", row })),
|
|
486
506
|
...item.missingEvalIds.map((evalId): LeafMember => ({ kind: "missing", evalId })),
|
|
487
507
|
];
|
|
488
|
-
|
|
508
|
+
const nested = nestLevel(members, "", "", item, view);
|
|
509
|
+
return members.length > 0 ? [...nested, coverageRow(item, view)] : nested;
|
|
489
510
|
}
|
|
490
511
|
|
|
491
512
|
function experimentRow(item: ExperimentListItem, view: HierarchyView): TableContentRow {
|
|
@@ -499,9 +520,6 @@ function experimentRow(item: ExperimentListItem, view: HierarchyView): TableCont
|
|
|
499
520
|
tokens: measureCell(item.tokens),
|
|
500
521
|
costUSD: measureCell(item.costUSD),
|
|
501
522
|
record: verdictCell(item.evalVerdicts),
|
|
502
|
-
// 覆盖构成是 experiment 这一行独有的事实(experiment-table.md「覆盖构成」),Eval / Attempt /
|
|
503
|
-
// 路径段组行没有这一格——projectCells 按列集自动填 notApplicable,不额外分支。
|
|
504
|
-
coverage: { kind: "composition", segments: coverageSegments(item) },
|
|
505
523
|
};
|
|
506
524
|
return {
|
|
507
525
|
key: item.experimentId,
|
|
@@ -522,7 +540,6 @@ function experimentColumns(composition: EvaluationKindComposition): ColumnSpec[]
|
|
|
522
540
|
{ key: "tokens", better: "lower", header: HEADER.tokens },
|
|
523
541
|
{ key: "costUSD", better: "lower", header: HEADER.costUSD },
|
|
524
542
|
{ key: "record", header: HEADER.record },
|
|
525
|
-
{ key: "coverage", header: HEADER.coverage },
|
|
526
543
|
];
|
|
527
544
|
}
|
|
528
545
|
|
|
@@ -37,6 +37,10 @@ export type Cell =
|
|
|
37
37
|
readonly refs?: readonly AttemptLocator[];
|
|
38
38
|
// 两种形态:counts = 判定构成计票(experiment / Eval 行);verdict = 单判定(attempt 行)。
|
|
39
39
|
// 格子只带值,计票怎么来的(折叠、分桶)在实体投影侧,渲染面不算数。
|
|
40
|
+
/** 单判定形态历史执行的距今毫秒数;新执行时省略,不伪造 0(与 locator 格同一条纪律)。 */
|
|
41
|
+
readonly staleSinceMs?: number;
|
|
42
|
+
/** 单判定形态省略判定词、只留判定符(如对照矩阵逐格只放得下一个符号的场景)。 */
|
|
43
|
+
readonly bare?: boolean;
|
|
40
44
|
}
|
|
41
45
|
| { readonly kind: "score"; readonly earned: number; readonly possible?: number }
|
|
42
46
|
| { readonly kind: "summary"; readonly text: string; readonly more?: number }
|
|
@@ -138,8 +142,11 @@ export function formatCellText(cell: Cell | null | undefined, locale?: ReportLoc
|
|
|
138
142
|
}
|
|
139
143
|
if (cell.verdict !== undefined) {
|
|
140
144
|
const v = cell.verdict === "skipped" ? "skipped" : cell.verdict;
|
|
145
|
+
const stale = cell.staleSinceMs !== undefined ? ` ${formatTimeDistance(cell.staleSinceMs, loc)}` : "";
|
|
141
146
|
// 判定符与判定词同场,与 locator 格、web 面同一条纪律:单色打印下照样读得出。
|
|
142
|
-
|
|
147
|
+
// bare 省略判定词,只留判定符(+ 可选时距),供逐格空间紧张的场景(如对照矩阵)使用。
|
|
148
|
+
if (cell.bare) return `${verdictMark(v)}${stale}`;
|
|
149
|
+
return `${verdictMark(v)} ${localeText(loc, `verdict.${v}`)}${stale}`;
|
|
143
150
|
}
|
|
144
151
|
return "—";
|
|
145
152
|
}
|