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
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// 页级呈现分配(两个 keyset、槽位分配、容量拒绝)、`dimensions` 必填与查询封闭性、text 面
|
|
3
3
|
// 降级、外壳 `dimensionPins` 的占位语义,以及公开 helper `presentDimension` 与页内分配同源。
|
|
4
4
|
//
|
|
5
|
-
// 断言面是映射本身(值 → 标签 / seriesSlot /
|
|
6
|
-
//
|
|
7
|
-
// renderReportToStaticHtml,断言的仍是探针组件打印出的映射文本。
|
|
5
|
+
// 断言面是映射本身(值 → 标签 / seriesSlot / 色板通道 / 可直接使用的 fill·stroke·marker)与
|
|
6
|
+
// 抛出的错误对象,不断言渲染出的像素色;需要证明「两个 renderer 都跑到」时才走
|
|
7
|
+
// renderReportToText / renderReportToStaticHtml,断言的仍是探针组件打印出的映射文本。
|
|
8
8
|
|
|
9
9
|
import { describe, expect, it } from "vitest";
|
|
10
10
|
|
|
@@ -24,14 +24,17 @@ import {
|
|
|
24
24
|
VISUAL_SLOT_COUNT,
|
|
25
25
|
type DimensionEncoding,
|
|
26
26
|
type DimensionPins,
|
|
27
|
+
type DimensionPresentation,
|
|
27
28
|
type PresentedDimension,
|
|
28
29
|
} from "./presentation.ts";
|
|
30
|
+
import { seriesFill, seriesStrokeDasharray } from "./assets/series-encoding.tsx";
|
|
29
31
|
import { renderReportToText } from "./runtime/text.ts";
|
|
30
32
|
import { renderReportToStaticHtml } from "./runtime/web.ts";
|
|
31
33
|
|
|
32
34
|
// ───────────────────────── 探针组件 ─────────────────────────
|
|
33
35
|
|
|
34
36
|
const SERIES: DimensionEncoding = { kind: "series", mark: "line" };
|
|
37
|
+
const FILL: DimensionEncoding = { kind: "series", mark: "bar" };
|
|
35
38
|
const LABEL_ONLY: DimensionEncoding = { kind: "label" };
|
|
36
39
|
|
|
37
40
|
interface ProbeProps {
|
|
@@ -44,12 +47,59 @@ interface ProbeProps {
|
|
|
44
47
|
query?: string;
|
|
45
48
|
}
|
|
46
49
|
|
|
50
|
+
/** 从呈现值还原槽位通道,便于探针打印与槽位断言同源。 */
|
|
51
|
+
function channelsOf(at: DimensionPresentation): { colorIndex: string; variant: string; detail: string } {
|
|
52
|
+
if (at.kind === "label") return { colorIndex: "-", variant: "-", detail: "label" };
|
|
53
|
+
if (at.kind === "color") {
|
|
54
|
+
const m = /^var\(--niceeval-color-series-([1-6])\)$/.exec(at.color);
|
|
55
|
+
return { colorIndex: m?.[1] ?? "?", variant: "1", detail: at.color };
|
|
56
|
+
}
|
|
57
|
+
if (at.kind === "series" && at.mark === "line") {
|
|
58
|
+
// 反查 24 槽里哪个 (color, variant) 产出这份 stroke + dash
|
|
59
|
+
for (let slot = 1; slot <= VISUAL_SLOT_COUNT; slot++) {
|
|
60
|
+
const ch = seriesChannelsOf(slot);
|
|
61
|
+
if (
|
|
62
|
+
at.stroke === `var(--niceeval-color-series-${ch.colorIndex})` &&
|
|
63
|
+
at.strokeDasharray === seriesStrokeDasharray(ch.variant)
|
|
64
|
+
) {
|
|
65
|
+
return { colorIndex: String(ch.colorIndex), variant: String(ch.variant), detail: at.strokeDasharray || "solid" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { colorIndex: "?", variant: "?", detail: at.stroke };
|
|
69
|
+
}
|
|
70
|
+
if (at.kind === "series" && (at.mark === "bar" || at.mark === "area")) {
|
|
71
|
+
for (let slot = 1; slot <= VISUAL_SLOT_COUNT; slot++) {
|
|
72
|
+
const ch = seriesChannelsOf(slot);
|
|
73
|
+
if (at.fill === seriesFill(ch.colorIndex, ch.variant)) {
|
|
74
|
+
return { colorIndex: String(ch.colorIndex), variant: String(ch.variant), detail: at.fill };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { colorIndex: "?", variant: "?", detail: at.fill };
|
|
78
|
+
}
|
|
79
|
+
if (at.kind === "series" && at.mark === "scatter") {
|
|
80
|
+
for (let slot = 1; slot <= VISUAL_SLOT_COUNT; slot++) {
|
|
81
|
+
const ch = seriesChannelsOf(slot);
|
|
82
|
+
if (at.marker.fill === `var(--niceeval-color-series-${ch.colorIndex})`) {
|
|
83
|
+
// scatter 同色四 variant 靠 path 区分;用 path 末段粗判
|
|
84
|
+
return { colorIndex: String(ch.colorIndex), variant: String(ch.variant), detail: at.marker.path.slice(0, 12) };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { colorIndex: "?", variant: "?", detail: "?" };
|
|
89
|
+
}
|
|
90
|
+
|
|
47
91
|
/** `值|标签|槽|色板下标|形状变体`,槽面缺席时打 `-`——两个 renderer 打同一行,好逐字比。 */
|
|
48
|
-
function probeLine(
|
|
92
|
+
function probeLine(
|
|
93
|
+
props: ProbeProps,
|
|
94
|
+
presented: PresentedDimension,
|
|
95
|
+
slots?: ReadonlyMap<string, number>,
|
|
96
|
+
): string {
|
|
49
97
|
return props.values
|
|
50
|
-
.map((
|
|
98
|
+
.map((value, index) => {
|
|
51
99
|
const at = presented.at(index);
|
|
52
|
-
|
|
100
|
+
const slot = slots?.get(value);
|
|
101
|
+
const ch = channelsOf(at);
|
|
102
|
+
return `${at.value}|${at.label}|${slot ?? "-"}|${ch.colorIndex}|${ch.variant}`;
|
|
53
103
|
})
|
|
54
104
|
.join(";");
|
|
55
105
|
}
|
|
@@ -63,7 +113,26 @@ const Probe = defineComponent<ProbeProps>({
|
|
|
63
113
|
},
|
|
64
114
|
}),
|
|
65
115
|
text: (props, ctx) => probeLine(props, ctx.dimension(props.query ?? props.handle ?? "keys")),
|
|
66
|
-
web: (props, ctx) =>
|
|
116
|
+
web: (props, ctx) => {
|
|
117
|
+
// web 探针从页级 plan 拿槽位,呈现值本身不再暴露 seriesSlot。
|
|
118
|
+
const presented = ctx.dimension(props.query ?? props.handle ?? "keys");
|
|
119
|
+
// slots 只能从 at 反推的 color/variant 对不上唯一槽时(多值同通道不该发生)——
|
|
120
|
+
// 这里用 channels 反查唯一槽:24 组 (color,variant) 两两不同。
|
|
121
|
+
const slots = new Map<string, number>();
|
|
122
|
+
props.values.forEach((value, index) => {
|
|
123
|
+
const at = presented.at(index);
|
|
124
|
+
if (at.kind === "label") return;
|
|
125
|
+
for (let slot = 1; slot <= VISUAL_SLOT_COUNT; slot++) {
|
|
126
|
+
const ch = seriesChannelsOf(slot);
|
|
127
|
+
const recovered = channelsOf(at);
|
|
128
|
+
if (recovered.colorIndex === String(ch.colorIndex) && recovered.variant === String(ch.variant)) {
|
|
129
|
+
slots.set(value, slot);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
return <span data-probe={probeLine(props, presented, slots)} />;
|
|
135
|
+
},
|
|
67
136
|
});
|
|
68
137
|
Probe.displayName = "Probe";
|
|
69
138
|
|
|
@@ -75,12 +144,12 @@ function elementOf(node: unknown): ReportElement {
|
|
|
75
144
|
function slotsOf(node: ReportNode, element: ReportElement, pins: DimensionPins = {}): Map<string, number> {
|
|
76
145
|
const plan = collectPageDimensions(node, pins, "web");
|
|
77
146
|
const props = element.props as unknown as ProbeProps;
|
|
78
|
-
const
|
|
147
|
+
const byValue = plan.slotsByDimension.get(props.dimension) ?? new Map();
|
|
79
148
|
const out = new Map<string, number>();
|
|
80
|
-
props.values
|
|
81
|
-
const slot =
|
|
149
|
+
for (const value of props.values) {
|
|
150
|
+
const slot = byValue.get(value);
|
|
82
151
|
if (slot !== undefined) out.set(value, slot);
|
|
83
|
-
}
|
|
152
|
+
}
|
|
84
153
|
return out;
|
|
85
154
|
}
|
|
86
155
|
|
|
@@ -95,11 +164,11 @@ describe("页级呈现分配", () => {
|
|
|
95
164
|
const plan = collectPageDimensions([table, chart], {}, "web");
|
|
96
165
|
|
|
97
166
|
const tableKeys = plan.dimension(table.props, "keys");
|
|
98
|
-
expect(tableKeys.at(0)).toEqual({ value: "alpha", label: "alpha" });
|
|
99
|
-
expect(tableKeys.at(1).
|
|
167
|
+
expect(tableKeys.at(0)).toEqual({ kind: "label", value: "alpha", label: "alpha" });
|
|
168
|
+
expect(tableKeys.at(1).kind).toBe("label");
|
|
100
169
|
|
|
101
170
|
const chartKeys = plan.dimension(chart.props, "keys");
|
|
102
|
-
expect(chartKeys.at(0).
|
|
171
|
+
expect(chartKeys.at(0).kind).toBe("series");
|
|
103
172
|
// label keyset 收了三个值,visual keyset 只有 gamma —— 标签面两组都在
|
|
104
173
|
expect([...chartKeys.labels.keys()].sort()).toEqual(["alpha", "beta", "gamma"]);
|
|
105
174
|
});
|
|
@@ -116,11 +185,12 @@ describe("页级呈现分配", () => {
|
|
|
116
185
|
const chart = elementOf(<Probe dimension="agent" values={chartValues} />);
|
|
117
186
|
const plan = collectPageDimensions([table, chart], {}, "web");
|
|
118
187
|
const chartKeys = plan.dimension(chart.props, "keys");
|
|
188
|
+
const slots = plan.slotsByDimension.get("agent")!;
|
|
119
189
|
|
|
120
190
|
// visual keyset 只有 3 个成员:容量按 3 算,不按 30 算
|
|
121
|
-
const
|
|
122
|
-
expect(new Set(
|
|
123
|
-
expect(
|
|
191
|
+
const slotList = chartValues.map((v) => slots.get(v)!);
|
|
192
|
+
expect(new Set(slotList).size).toBe(3);
|
|
193
|
+
expect(slotList.every((slot) => slot >= 1 && slot <= VISUAL_SLOT_COUNT)).toBe(true);
|
|
124
194
|
|
|
125
195
|
// label keyset 是 30 个值:one / two / three 三处重名,标签被逼长到两段
|
|
126
196
|
expect(chartValues.map((_v, i) => chartKeys.at(i).label)).toEqual(["alpha/one", "beta/two", "gamma/three"]);
|
|
@@ -132,13 +202,16 @@ describe("页级呈现分配", () => {
|
|
|
132
202
|
const chart = elementOf(<Probe dimension="agent" values={["acme/codex", "acme/claude"]} />);
|
|
133
203
|
const legend = elementOf(<Probe dimension="agent" handle="legend" values={["acme/claude"]} />);
|
|
134
204
|
const plan = collectPageDimensions([chart, legend], {}, "web");
|
|
205
|
+
const slots = plan.slotsByDimension.get("agent")!;
|
|
135
206
|
|
|
207
|
+
expect(slots.get("acme/claude")).toBe(slots.get("acme/claude"));
|
|
136
208
|
const fromChart = plan.dimension(chart.props, "keys").at(1);
|
|
137
209
|
const fromLegend = plan.dimension(legend.props, "legend").at(0);
|
|
138
|
-
expect(fromLegend.seriesSlot).toBe(fromChart.seriesSlot);
|
|
139
210
|
expect(fromLegend.label).toBe("claude");
|
|
140
211
|
// 取键用完整值:缩短后的 "claude" 不是另一个身份
|
|
141
212
|
expect(fromLegend.value).toBe("acme/claude");
|
|
213
|
+
// 两处呈现同一视觉身份(stroke 相等)
|
|
214
|
+
expect(fromLegend).toEqual(fromChart);
|
|
142
215
|
});
|
|
143
216
|
|
|
144
217
|
it("24 个值全部落槽且两两不同,24 个 (色, variant) 组合也两两不同", () => {
|
|
@@ -158,6 +231,19 @@ describe("页级呈现分配", () => {
|
|
|
158
231
|
expect(channels.size).toBe(VISUAL_SLOT_COUNT);
|
|
159
232
|
});
|
|
160
233
|
|
|
234
|
+
it("槽序表:1–6 第一变体、7–12 第二变体,与 docs 一致", () => {
|
|
235
|
+
expect(seriesChannelsOf(1)).toEqual({ colorIndex: 1, variant: 1 });
|
|
236
|
+
expect(seriesChannelsOf(6)).toEqual({ colorIndex: 6, variant: 1 });
|
|
237
|
+
expect(seriesChannelsOf(7)).toEqual({ colorIndex: 1, variant: 2 });
|
|
238
|
+
expect(seriesChannelsOf(14)).toEqual({ colorIndex: 2, variant: 3 });
|
|
239
|
+
expect(seriesChannelsOf(20)).toEqual({ colorIndex: 2, variant: 4 });
|
|
240
|
+
// baseline/obelisk 撞色根因:同 colorIndex 不同 variant,fill 必须可辨
|
|
241
|
+
expect(seriesFill(2, 1)).toBe("var(--niceeval-color-series-2)");
|
|
242
|
+
expect(seriesFill(2, 3)).toBe("url(#niceeval-series-pat-v3-c2)");
|
|
243
|
+
expect(seriesFill(2, 4)).toBe("url(#niceeval-series-pat-v4-c2)");
|
|
244
|
+
expect(seriesFill(2, 3)).not.toBe(seriesFill(2, 4));
|
|
245
|
+
});
|
|
246
|
+
|
|
161
247
|
it("visual keyset 超过 24 按完整用户反馈拒绝该页,fix 行不提 dimensionPins", () => {
|
|
162
248
|
const values = Array.from({ length: 27 }, (_, i) => `agent-${i}`);
|
|
163
249
|
const chart = elementOf(<Probe dimension="agent" values={values} />);
|
|
@@ -172,6 +258,40 @@ describe("页级呈现分配", () => {
|
|
|
172
258
|
expect(message).toContain("fix: filter the series, or split them into facets/pages");
|
|
173
259
|
expect(message).not.toContain("dimensionPins");
|
|
174
260
|
});
|
|
261
|
+
|
|
262
|
+
it("FillSeriesPresentation 按 mark 产出可直接用的 fill(含 pattern url)", () => {
|
|
263
|
+
const chart = elementOf(
|
|
264
|
+
<Probe dimension="memory" encoding={FILL} values={["baseline", "obelisk"]} />,
|
|
265
|
+
);
|
|
266
|
+
const plan = collectPageDimensions(chart, {}, "web");
|
|
267
|
+
const presented = plan.dimension(chart.props, "keys");
|
|
268
|
+
const slots = plan.slotsByDimension.get("memory")!;
|
|
269
|
+
// 复现真实撞色对:同页散列后 baseline=14 / obelisk=20 时 color 同 variant 不同
|
|
270
|
+
for (const value of ["baseline", "obelisk"] as const) {
|
|
271
|
+
// 不强钉槽位,只断言呈现是 fill 支且 fill 可区分
|
|
272
|
+
const at = presented.at(["baseline", "obelisk"].indexOf(value));
|
|
273
|
+
expect(at.kind).toBe("series");
|
|
274
|
+
if (at.kind === "series" && (at.mark === "bar" || at.mark === "area")) {
|
|
275
|
+
expect(at.fill.startsWith("var(") || at.fill.startsWith("url(#niceeval-series-pat-")).toBe(true);
|
|
276
|
+
expect(at.stroke).toMatch(/^var\(--niceeval-color-series-[1-6]\)$/);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// 若两值恰好同色不同 variant,fill 字符串必须不同
|
|
280
|
+
const a = presented.at(0);
|
|
281
|
+
const b = presented.at(1);
|
|
282
|
+
if (
|
|
283
|
+
a.kind === "series" &&
|
|
284
|
+
b.kind === "series" &&
|
|
285
|
+
(a.mark === "bar" || a.mark === "area") &&
|
|
286
|
+
(b.mark === "bar" || b.mark === "area")
|
|
287
|
+
) {
|
|
288
|
+
const sa = slots.get("baseline")!;
|
|
289
|
+
const sb = slots.get("obelisk")!;
|
|
290
|
+
if (seriesChannelsOf(sa).colorIndex === seriesChannelsOf(sb).colorIndex) {
|
|
291
|
+
expect(a.fill).not.toBe(b.fill);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
});
|
|
175
295
|
});
|
|
176
296
|
|
|
177
297
|
// ───────────────────────── dimensions 必填与查询封闭性 ─────────────────────────
|
|
@@ -307,8 +427,48 @@ describe("presentDimension", () => {
|
|
|
307
427
|
const chart = elementOf(<Probe dimension="agent" values={values} />);
|
|
308
428
|
const inPage = slotsOf(chart, chart);
|
|
309
429
|
const standalone = presentDimension({ dimension: "agent", encoding: SERIES, values });
|
|
430
|
+
// standalone 没有暴露 seriesSlot,用 stroke/dash 与页内对照
|
|
431
|
+
for (const [index, value] of values.entries()) {
|
|
432
|
+
const pageSlot = inPage.get(value)!;
|
|
433
|
+
const at = standalone.at(index);
|
|
434
|
+
expect(at.kind).toBe("series");
|
|
435
|
+
if (at.kind === "series" && at.mark === "line") {
|
|
436
|
+
const ch = seriesChannelsOf(pageSlot);
|
|
437
|
+
expect(at.stroke).toBe(`var(--niceeval-color-series-${ch.colorIndex})`);
|
|
438
|
+
expect(at.strokeDasharray).toBe(seriesStrokeDasharray(ch.variant));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
// ───────────────────────── MemoryBench 可辨性自检 ─────────────────────────
|
|
445
|
+
|
|
446
|
+
describe("MemoryBench leaderboard 五条件可辨", () => {
|
|
447
|
+
it("memory 五值拿到两两可辨的 fill 身份(baseline 与 obelisk 同色不同 variant)", () => {
|
|
448
|
+
const values = ["baseline", "mempal", "nowledge", "obelisk", "remem"] as const;
|
|
449
|
+
const chart = elementOf(<Probe dimension="memory" encoding={FILL} values={values} />);
|
|
450
|
+
const slots = slotsOf(chart, chart);
|
|
451
|
+
const presented = presentDimension({
|
|
452
|
+
dimension: "memory",
|
|
453
|
+
encoding: { kind: "series", mark: "bar" },
|
|
454
|
+
values,
|
|
455
|
+
});
|
|
456
|
+
const fills = new Map<string, string>();
|
|
310
457
|
for (const [index, value] of values.entries()) {
|
|
311
|
-
|
|
458
|
+
const at = presented.at(index);
|
|
459
|
+
expect(at.kind).toBe("series");
|
|
460
|
+
if (at.kind === "series" && at.mark === "bar") {
|
|
461
|
+
fills.set(value, at.fill);
|
|
462
|
+
}
|
|
312
463
|
}
|
|
464
|
+
// 五个 fill 两两不同
|
|
465
|
+
expect(new Set(fills.values()).size).toBe(5);
|
|
466
|
+
// 与截图复算一致:baseline slot 14 → c2 v3 pattern;obelisk slot 20 → c2 v4 pattern
|
|
467
|
+
expect(slots.get("baseline")).toBe(14);
|
|
468
|
+
expect(slots.get("obelisk")).toBe(20);
|
|
469
|
+
expect(seriesChannelsOf(14)).toEqual({ colorIndex: 2, variant: 3 });
|
|
470
|
+
expect(seriesChannelsOf(20)).toEqual({ colorIndex: 2, variant: 4 });
|
|
471
|
+
expect(fills.get("baseline")).toBe("url(#niceeval-series-pat-v3-c2)");
|
|
472
|
+
expect(fills.get("obelisk")).toBe("url(#niceeval-series-pat-v4-c2)");
|
|
313
473
|
});
|
|
314
474
|
});
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { shortestUniqueLabels as labels } from "./model/format.ts";
|
|
2
|
+
import {
|
|
3
|
+
seriesColorVar,
|
|
4
|
+
seriesFill,
|
|
5
|
+
seriesMarker,
|
|
6
|
+
seriesStrokeDasharray,
|
|
7
|
+
} from "./assets/series-encoding.tsx";
|
|
2
8
|
|
|
3
9
|
export type DimensionEncoding =
|
|
4
10
|
| { readonly kind: "label" }
|
|
@@ -21,7 +27,11 @@ export const VISUAL_SLOT_COUNT = 24;
|
|
|
21
27
|
|
|
22
28
|
export type SeriesVariant = 1 | 2 | 3 | 4;
|
|
23
29
|
|
|
24
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* 槽 1..24 → (色板下标 1..6, 形状变体 1..4)。
|
|
32
|
+
* 与 components/README「视觉编码容量」槽序表一致:1–6 第一变体、7–12 第二变体,依此类推。
|
|
33
|
+
* `colorIndex = (slot-1) % 6 + 1`,`variant = floor((slot-1)/6) + 1`。
|
|
34
|
+
*/
|
|
25
35
|
export function seriesChannelsOf(slot: number): { colorIndex: number; variant: SeriesVariant } {
|
|
26
36
|
if (!Number.isInteger(slot) || slot < 1 || slot > VISUAL_SLOT_COUNT) {
|
|
27
37
|
throw new Error(`seriesSlot must be an integer in [1, ${VISUAL_SLOT_COUNT}], got ${slot}.`);
|
|
@@ -29,21 +39,78 @@ export function seriesChannelsOf(slot: number): { colorIndex: number; variant: S
|
|
|
29
39
|
const zero = slot - 1;
|
|
30
40
|
return {
|
|
31
41
|
colorIndex: (zero % 6) + 1,
|
|
32
|
-
variant: (
|
|
42
|
+
variant: (Math.floor(zero / 6) + 1) as SeriesVariant,
|
|
33
43
|
};
|
|
34
44
|
}
|
|
35
45
|
|
|
36
|
-
|
|
46
|
+
// ─── docs/feature/reports/library/presentation.md 的呈现家族 ───
|
|
47
|
+
|
|
48
|
+
export interface PresentationIdentity {
|
|
49
|
+
/** 完整维度值,作为排序、筛选、React key 与证据身份。 */
|
|
37
50
|
readonly value: string;
|
|
51
|
+
/** 当前页完整 label keyset 内生成的显示名。 */
|
|
38
52
|
readonly label: string;
|
|
39
|
-
readonly seriesSlot?: number;
|
|
40
|
-
readonly colorIndex?: number;
|
|
41
|
-
readonly variant?: SeriesVariant;
|
|
42
53
|
}
|
|
43
54
|
|
|
55
|
+
export interface LabelPresentation extends PresentationIdentity {
|
|
56
|
+
readonly kind: "label";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ColorPresentation extends PresentationIdentity {
|
|
60
|
+
readonly kind: "color";
|
|
61
|
+
/** `var(--niceeval-color-series-N)`。 */
|
|
62
|
+
readonly color: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LineSeriesPresentation extends PresentationIdentity {
|
|
66
|
+
readonly kind: "series";
|
|
67
|
+
readonly mark: "line";
|
|
68
|
+
readonly stroke: string;
|
|
69
|
+
readonly strokeDasharray: string;
|
|
70
|
+
readonly marker: {
|
|
71
|
+
readonly path: string;
|
|
72
|
+
readonly viewBox: string;
|
|
73
|
+
readonly fill: string;
|
|
74
|
+
readonly stroke: string;
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ScatterSeriesPresentation extends PresentationIdentity {
|
|
79
|
+
readonly kind: "series";
|
|
80
|
+
readonly mark: "scatter";
|
|
81
|
+
readonly marker: LineSeriesPresentation["marker"];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface FillSeriesPresentation extends PresentationIdentity {
|
|
85
|
+
readonly kind: "series";
|
|
86
|
+
readonly mark: "bar" | "area";
|
|
87
|
+
/** 颜色,或可直接使用的 `url(#pattern-id)`。 */
|
|
88
|
+
readonly fill: string;
|
|
89
|
+
readonly stroke: string;
|
|
90
|
+
readonly strokeDasharray: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export type DimensionPresentation =
|
|
94
|
+
| LabelPresentation
|
|
95
|
+
| ColorPresentation
|
|
96
|
+
| LineSeriesPresentation
|
|
97
|
+
| ScatterSeriesPresentation
|
|
98
|
+
| FillSeriesPresentation;
|
|
99
|
+
|
|
100
|
+
export type PresentationFor<E extends DimensionEncoding> = E extends { kind: "label" }
|
|
101
|
+
? LabelPresentation
|
|
102
|
+
: E extends { kind: "color" }
|
|
103
|
+
? ColorPresentation
|
|
104
|
+
: LineSeriesPresentation | ScatterSeriesPresentation | FillSeriesPresentation;
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 句柄上的呈现面。`at(i)` 返回可直接交给 SVG/CSS 的值;text 面恒为 label 支。
|
|
108
|
+
* `labels` 是本维度整页 label keyset 的最短唯一后缀表,便于探针与调试。
|
|
109
|
+
*/
|
|
44
110
|
export interface PresentedDimension {
|
|
111
|
+
readonly length: number;
|
|
45
112
|
readonly labels: ReadonlyMap<string, string>;
|
|
46
|
-
at(index: number):
|
|
113
|
+
at(index: number): DimensionPresentation;
|
|
47
114
|
}
|
|
48
115
|
|
|
49
116
|
export class UndeclaredDimensionValueError extends Error {
|
|
@@ -83,6 +150,41 @@ export interface PageDimensionPlan {
|
|
|
83
150
|
dimension(handle: string): PresentedDimension;
|
|
84
151
|
}
|
|
85
152
|
|
|
153
|
+
function presentChannels(
|
|
154
|
+
value: string,
|
|
155
|
+
label: string,
|
|
156
|
+
seriesSlot: number,
|
|
157
|
+
encoding: DimensionEncoding,
|
|
158
|
+
): DimensionPresentation {
|
|
159
|
+
const { colorIndex, variant } = seriesChannelsOf(seriesSlot);
|
|
160
|
+
if (encoding.kind === "color") {
|
|
161
|
+
return { kind: "color", value, label, color: seriesColorVar(colorIndex) };
|
|
162
|
+
}
|
|
163
|
+
if (encoding.kind !== "series") {
|
|
164
|
+
return { kind: "label", value, label };
|
|
165
|
+
}
|
|
166
|
+
const stroke = seriesColorVar(colorIndex);
|
|
167
|
+
const strokeDasharray = seriesStrokeDasharray(variant);
|
|
168
|
+
const marker = seriesMarker(colorIndex, variant);
|
|
169
|
+
switch (encoding.mark) {
|
|
170
|
+
case "line":
|
|
171
|
+
return { kind: "series", mark: "line", value, label, stroke, strokeDasharray, marker };
|
|
172
|
+
case "scatter":
|
|
173
|
+
return { kind: "series", mark: "scatter", value, label, marker };
|
|
174
|
+
case "bar":
|
|
175
|
+
case "area":
|
|
176
|
+
return {
|
|
177
|
+
kind: "series",
|
|
178
|
+
mark: encoding.mark,
|
|
179
|
+
value,
|
|
180
|
+
label,
|
|
181
|
+
fill: seriesFill(colorIndex, variant),
|
|
182
|
+
stroke,
|
|
183
|
+
strokeDasharray,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
86
188
|
/**
|
|
87
189
|
* 页级呈现分配:收集整页 dimensions() 声明,产出 label / visual 两套 keyset 上的映射。
|
|
88
190
|
* pins 原样占位;未钉键以稳定散列为起点、撞槽按显示键字典序线性探测。
|
|
@@ -134,8 +236,9 @@ export function allocatePageDimensions(
|
|
|
134
236
|
const values = declaration.values;
|
|
135
237
|
const encoding = declaration.encoding;
|
|
136
238
|
byHandle.set(handle, {
|
|
239
|
+
length: values.length,
|
|
137
240
|
labels: dimLabels,
|
|
138
|
-
at(index: number):
|
|
241
|
+
at(index: number): DimensionPresentation {
|
|
139
242
|
const value = values[index];
|
|
140
243
|
if (value === undefined) {
|
|
141
244
|
throw new UndeclaredDimensionValueError(
|
|
@@ -145,8 +248,9 @@ export function allocatePageDimensions(
|
|
|
145
248
|
);
|
|
146
249
|
}
|
|
147
250
|
const label = dimLabels.get(value) ?? value;
|
|
251
|
+
// text 面恒返回 label 支:不上 ANSI 色,拿不到颜色、线型或 pattern。
|
|
148
252
|
if (options.face === "text" || encoding.kind === "label") {
|
|
149
|
-
return { value, label };
|
|
253
|
+
return { kind: "label", value, label };
|
|
150
254
|
}
|
|
151
255
|
const seriesSlot = slots?.get(value);
|
|
152
256
|
if (seriesSlot === undefined) {
|
|
@@ -156,8 +260,7 @@ export function allocatePageDimensions(
|
|
|
156
260
|
index,
|
|
157
261
|
);
|
|
158
262
|
}
|
|
159
|
-
|
|
160
|
-
return { value, label, seriesSlot, colorIndex: channels.colorIndex, variant: channels.variant };
|
|
263
|
+
return presentChannels(value, label, seriesSlot, encoding);
|
|
161
264
|
},
|
|
162
265
|
});
|
|
163
266
|
}
|
|
@@ -74,7 +74,17 @@ export type { AttemptEvidence, AttemptEvidenceCapabilities } from "../../record/
|
|
|
74
74
|
// 格式化与呈现工具箱(docs/feature/reports/library/presentation.md):自有 React 页面与报告面同实现。
|
|
75
75
|
export { formatAxisTick, formatInstant, formatMetricValue, formatTimeDistance, missingText } from "../model/format.ts";
|
|
76
76
|
export { presentDimension, shortestUniqueLabels } from "../presentation.ts";
|
|
77
|
-
export type {
|
|
77
|
+
export type {
|
|
78
|
+
ColorPresentation,
|
|
79
|
+
DimensionDeclaration,
|
|
80
|
+
DimensionEncoding,
|
|
81
|
+
DimensionPresentation,
|
|
82
|
+
FillSeriesPresentation,
|
|
83
|
+
LabelPresentation,
|
|
84
|
+
LineSeriesPresentation,
|
|
85
|
+
PresentedDimension,
|
|
86
|
+
ScatterSeriesPresentation,
|
|
87
|
+
} from "../presentation.ts";
|
|
78
88
|
|
|
79
89
|
// locale(官方组件 chrome 文案的语言;LocalizedText 的按 locale 解析也用它)
|
|
80
90
|
export {
|
|
@@ -27,10 +27,11 @@ import type { DatasetField,
|
|
|
27
27
|
ScoreboardData,
|
|
28
28
|
SeriesInput,
|
|
29
29
|
StabilityMatrixData,
|
|
30
|
+
StaleConclusionReference,
|
|
30
31
|
} from "../model/types.ts";
|
|
31
32
|
import type { JsonValue, Verdict } from "../../types.ts";
|
|
32
33
|
import type { AttemptLocator } from "../../record/locator.ts";
|
|
33
|
-
import type { Run } from "../../record/types.ts";
|
|
34
|
+
import type { AttemptHandle, Run } from "../../record/types.ts";
|
|
34
35
|
import { comparabilityConfigOf, deepEqualJson } from "../../sample/index.ts";
|
|
35
36
|
import { foldEvalVerdict } from "../../shared/verdict.ts";
|
|
36
37
|
import {
|
|
@@ -49,10 +50,12 @@ import {
|
|
|
49
50
|
groupItems,
|
|
50
51
|
historicalOf,
|
|
51
52
|
locatorOf,
|
|
53
|
+
msSince,
|
|
52
54
|
refDisplayKey,
|
|
53
55
|
resolveInput,
|
|
54
56
|
seriesKey,
|
|
55
57
|
seriesName,
|
|
58
|
+
staleReferenceOf,
|
|
56
59
|
toColumn,
|
|
57
60
|
type Item,
|
|
58
61
|
} from "../model/aggregate.ts";
|
|
@@ -565,6 +568,10 @@ async function buildDeltaCell(items: readonly Item[]): Promise<DeltaCell> {
|
|
|
565
568
|
const verdict: Verdict = foldEvalVerdict(items.map((item) => ({ verdict: item.attempt.result.verdict })));
|
|
566
569
|
const refs = new Set<AttemptLocator>();
|
|
567
570
|
let historical = false;
|
|
571
|
+
// 距今最久的历史执行决定这一格的时距——多个 attempt 折进同一格时,时距说的是「这批结论里
|
|
572
|
+
// 最旧的证据有多旧」,不是最新那条(与 experiment-table 的 attempt 行时距同一份数据来源,
|
|
573
|
+
// 只是这里先跨 attempt 折叠成一格)。
|
|
574
|
+
let staleStartedAt: string | undefined;
|
|
568
575
|
let scoreSum = 0;
|
|
569
576
|
let scoreCount = 0;
|
|
570
577
|
let tokensSum = 0;
|
|
@@ -573,7 +580,11 @@ async function buildDeltaCell(items: readonly Item[]): Promise<DeltaCell> {
|
|
|
573
580
|
let costCount = 0;
|
|
574
581
|
for (const item of items) {
|
|
575
582
|
refs.add(locatorOf(item));
|
|
576
|
-
if (historicalOf(item))
|
|
583
|
+
if (historicalOf(item)) {
|
|
584
|
+
historical = true;
|
|
585
|
+
const startedAt = item.attempt.result.startedAt ?? item.run.startedAt;
|
|
586
|
+
if (staleStartedAt === undefined || startedAt < staleStartedAt) staleStartedAt = startedAt;
|
|
587
|
+
}
|
|
577
588
|
if (evaluationKind === "points") {
|
|
578
589
|
const value = await evaluateMetric(totalScoreMetric, item.attempt);
|
|
579
590
|
if (value !== null) {
|
|
@@ -602,11 +613,22 @@ async function buildDeltaCell(items: readonly Item[]): Promise<DeltaCell> {
|
|
|
602
613
|
...(tokensCount > 0 ? { totalTokens: tokensSum } : {}),
|
|
603
614
|
...(costCount > 0 ? { totalCostUSD: costSum } : {}),
|
|
604
615
|
historical,
|
|
616
|
+
...(staleStartedAt !== undefined ? { staleSinceMs: msSince(staleStartedAt) } : {}),
|
|
605
617
|
};
|
|
606
618
|
}
|
|
607
619
|
|
|
620
|
+
/** 每个 experiment 当前基准的 configHash:该 experiment 在 `runs` 里最新快照的 configHash。 */
|
|
621
|
+
function latestConfigHashByExperiment(runs: readonly Run[]): Map<string, string | undefined> {
|
|
622
|
+
const latest = new Map<string, Run>();
|
|
623
|
+
for (const run of runs) {
|
|
624
|
+
const existing = latest.get(run.experimentId);
|
|
625
|
+
if (existing === undefined || run.startedAt > existing.startedAt) latest.set(run.experimentId, run);
|
|
626
|
+
}
|
|
627
|
+
return new Map([...latest].map(([id, run]) => [id, run.configHash]));
|
|
628
|
+
}
|
|
629
|
+
|
|
608
630
|
export async function deltaTableData(input: ReportInput, options: DeltaTableOptions): Promise<DeltaData> {
|
|
609
|
-
const { runs, attempts } = resolveInput(input);
|
|
631
|
+
const { runs, attempts, historyAttempts, fresh } = resolveInput(input);
|
|
610
632
|
const items = filterItems(collectItems(runs, attempts), options.evals);
|
|
611
633
|
|
|
612
634
|
let conditions: string[];
|
|
@@ -673,6 +695,14 @@ export async function deltaTableData(input: ReportInput, options: DeltaTableOpti
|
|
|
673
695
|
for (const byEval of byConditionByEval.values()) for (const evalId of byEval.keys()) evalIdsSet.add(evalId);
|
|
674
696
|
const knownEvalIds = [...evalIdsSet].sort();
|
|
675
697
|
|
|
698
|
+
// 每个条件背后的 experiment id 集合(by: "experiment" 时条件字符串本身通常就是唯一一个 id);
|
|
699
|
+
// 过期结论参考按各自 experiment 的当前基准 configHash 判定。
|
|
700
|
+
const experimentIdsByCondition = new Map<string, Set<string>>();
|
|
701
|
+
for (const [condition, conditionItems] of itemsByCondition) {
|
|
702
|
+
experimentIdsByCondition.set(condition, new Set(conditionItems.map((item) => experimentIdOf(item))));
|
|
703
|
+
}
|
|
704
|
+
const anchorConfigHashByExperiment = latestConfigHashByExperiment(runs);
|
|
705
|
+
|
|
676
706
|
const baseline = conditions[0];
|
|
677
707
|
const rows: DeltaData["rows"] = [];
|
|
678
708
|
for (const evalId of knownEvalIds) {
|
|
@@ -701,7 +731,32 @@ export async function deltaTableData(input: ReportInput, options: DeltaTableOpti
|
|
|
701
731
|
}
|
|
702
732
|
}
|
|
703
733
|
}
|
|
704
|
-
|
|
734
|
+
|
|
735
|
+
// 过期结论参考(show/compare.md「— ✓ 12d」):只在该条件缺席这道题、且记录里存在与该
|
|
736
|
+
// experiment 当前基准不可比的历史判定时才给,口径与 experiment-table 两档占位行一致——
|
|
737
|
+
// `fresh` 为 true 时不给参考,读者已声明只看新执行。它只写进 references,不进 cells,
|
|
738
|
+
// 因此天然不参与 totals / delta / 配对覆盖的任何一个数。
|
|
739
|
+
let references: DeltaData["rows"][number]["references"] | undefined;
|
|
740
|
+
if (!fresh) {
|
|
741
|
+
for (const condition of conditions) {
|
|
742
|
+
if (cells[condition] !== undefined) continue;
|
|
743
|
+
const experimentIds = experimentIdsByCondition.get(condition);
|
|
744
|
+
if (!experimentIds || experimentIds.size === 0) continue;
|
|
745
|
+
const candidates = historyAttempts.filter(
|
|
746
|
+
(a) =>
|
|
747
|
+
experimentIds.has(a.experimentId) &&
|
|
748
|
+
a.evalId === evalId &&
|
|
749
|
+
a.run.configHash !== anchorConfigHashByExperiment.get(a.experimentId),
|
|
750
|
+
);
|
|
751
|
+
const reference = staleReferenceOf(candidates);
|
|
752
|
+
if (reference) {
|
|
753
|
+
references ??= {};
|
|
754
|
+
references[condition] = reference;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
rows.push({ key: evalId, flipped, cells, ...(delta ? { delta } : {}), ...(references ? { references } : {}) });
|
|
705
760
|
}
|
|
706
761
|
|
|
707
762
|
// 各条件自身覆盖面:分母是该条件有结果的 eval 数,不看其它条件是否也覆盖了这道题。
|