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
|
@@ -50,6 +50,30 @@ export function evalIdOf(item) {
|
|
|
50
50
|
export function historicalOf(item) {
|
|
51
51
|
return item.attempt.carried || item.run.startedAt < item.watermark.startedAt;
|
|
52
52
|
}
|
|
53
|
+
/** 一个 ISO 时刻距渲染时刻(`Date.now()`)的毫秒数,恒不小于 0。 */
|
|
54
|
+
export function msSince(iso) {
|
|
55
|
+
return Math.max(0, Date.now() - Date.parse(iso));
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 覆盖缺口 / 对照矩阵缺席格的「过期结论」参考(docs/feature/reports/components/summaries/
|
|
59
|
+
* experiment-table.md「覆盖缺口的两档占位行」、show/compare.md 同一套口径):候选已经是与
|
|
60
|
+
* 当前基准 configHash 不可比的那些,取其中最近一条;两个消费方(entity-lists 的占位行、
|
|
61
|
+
* DeltaTable 的缺席格)共用同一份「取最新」判据,不各自实现一遍。
|
|
62
|
+
*/
|
|
63
|
+
export function staleReferenceOf(candidates) {
|
|
64
|
+
if (candidates.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
const newest = candidates.reduce((a, b) => ((a.result.startedAt ?? "") >= (b.result.startedAt ?? "") ? a : b));
|
|
67
|
+
const startedAt = newest.result.startedAt;
|
|
68
|
+
if (!startedAt)
|
|
69
|
+
return undefined; // 无时刻的 legacy 落盘算不出时距,不伪造参考
|
|
70
|
+
return {
|
|
71
|
+
locator: newest.locator ??
|
|
72
|
+
encodeAttemptLocator({ runId: newest.run.runId, evalId: newest.evalId, attempt: newest.result.attempt }),
|
|
73
|
+
verdict: newest.result.verdict,
|
|
74
|
+
staleSinceMs: msSince(startedAt),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
53
77
|
/** 快照键:"<experimentId> @ <startedAt>"("run" 维度与手挑快照数组的对比用)。 */
|
|
54
78
|
export function snapshotKeyOf(run) {
|
|
55
79
|
return `${run.experimentId} @ ${run.startedAt}`;
|
|
@@ -60,7 +60,6 @@ declare const en: {
|
|
|
60
60
|
readonly "experimentList.cost": "Cost";
|
|
61
61
|
readonly "experimentList.result": "Record";
|
|
62
62
|
readonly "experimentList.status": "Status";
|
|
63
|
-
readonly "experimentList.coverage": "Coverage";
|
|
64
63
|
readonly "experimentList.evalAttempt": "Eval / Attempt";
|
|
65
64
|
readonly "experimentList.duration": "Duration";
|
|
66
65
|
readonly "experimentList.filterPlaceholder": "Filter experiments…";
|
|
@@ -86,7 +86,6 @@ const en = {
|
|
|
86
86
|
"experimentList.cost": "Cost",
|
|
87
87
|
"experimentList.result": "Record",
|
|
88
88
|
"experimentList.status": "Status",
|
|
89
|
-
"experimentList.coverage": "Coverage",
|
|
90
89
|
"experimentList.evalAttempt": "Eval / Attempt",
|
|
91
90
|
"experimentList.duration": "Duration",
|
|
92
91
|
"experimentList.filterPlaceholder": "Filter experiments…",
|
|
@@ -269,7 +268,6 @@ const zhCN = {
|
|
|
269
268
|
"experimentList.cost": "成本",
|
|
270
269
|
"experimentList.result": "结果",
|
|
271
270
|
"experimentList.status": "状态",
|
|
272
|
-
"experimentList.coverage": "覆盖构成",
|
|
273
271
|
"experimentList.evalAttempt": "题目 / Attempt",
|
|
274
272
|
"experimentList.duration": "耗时",
|
|
275
273
|
"experimentList.filterPlaceholder": "筛选实验…",
|
|
@@ -242,6 +242,8 @@ export interface DeltaCell {
|
|
|
242
242
|
totalCostUSD?: number;
|
|
243
243
|
/** true 时该格来自跨快照携带的历史执行(时效标注见 experiment-table.md「时效不写字」)。 */
|
|
244
244
|
historical: boolean;
|
|
245
|
+
/** `historical` 为 true 时,距今的毫秒数(渲染时刻起算);新执行时省略,不伪造 0。 */
|
|
246
|
+
staleSinceMs?: number;
|
|
245
247
|
}
|
|
246
248
|
export interface DeltaData {
|
|
247
249
|
byDimension: string;
|
|
@@ -262,6 +264,12 @@ export interface DeltaData {
|
|
|
262
264
|
tokens?: number;
|
|
263
265
|
costUSD?: number;
|
|
264
266
|
}>;
|
|
267
|
+
/**
|
|
268
|
+
* 键是条件值,只在该条件缺席这道题(`cells` 无键)且记录里存在不可比历史判定时才有条目
|
|
269
|
+
* (docs/feature/reports/show/compare.md「— ✓ 12d」);它不进 `totals`、`delta` 与配对覆盖的
|
|
270
|
+
* 任何一个数,只提供 locator 下钻。
|
|
271
|
+
*/
|
|
272
|
+
references?: globalThis.Record<string, StaleConclusionReference>;
|
|
265
273
|
}>;
|
|
266
274
|
/** 各条件自身覆盖面的描述,分母是该条件有结果的 eval 数;不用于跨条件直接归因。 */
|
|
267
275
|
totals: globalThis.Record<string, {
|
|
@@ -18,21 +18,68 @@ export type DimensionDeclarations = Readonly<Record<string, DimensionDeclaration
|
|
|
18
18
|
export type DimensionPins = Readonly<Record<string, Readonly<Record<string, number>>>>;
|
|
19
19
|
export declare const VISUAL_SLOT_COUNT = 24;
|
|
20
20
|
export type SeriesVariant = 1 | 2 | 3 | 4;
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* 槽 1..24 → (色板下标 1..6, 形状变体 1..4)。
|
|
23
|
+
* 与 components/README「视觉编码容量」槽序表一致:1–6 第一变体、7–12 第二变体,依此类推。
|
|
24
|
+
* `colorIndex = (slot-1) % 6 + 1`,`variant = floor((slot-1)/6) + 1`。
|
|
25
|
+
*/
|
|
22
26
|
export declare function seriesChannelsOf(slot: number): {
|
|
23
27
|
colorIndex: number;
|
|
24
28
|
variant: SeriesVariant;
|
|
25
29
|
};
|
|
26
|
-
export interface
|
|
30
|
+
export interface PresentationIdentity {
|
|
31
|
+
/** 完整维度值,作为排序、筛选、React key 与证据身份。 */
|
|
27
32
|
readonly value: string;
|
|
33
|
+
/** 当前页完整 label keyset 内生成的显示名。 */
|
|
28
34
|
readonly label: string;
|
|
29
|
-
readonly seriesSlot?: number;
|
|
30
|
-
readonly colorIndex?: number;
|
|
31
|
-
readonly variant?: SeriesVariant;
|
|
32
35
|
}
|
|
36
|
+
export interface LabelPresentation extends PresentationIdentity {
|
|
37
|
+
readonly kind: "label";
|
|
38
|
+
}
|
|
39
|
+
export interface ColorPresentation extends PresentationIdentity {
|
|
40
|
+
readonly kind: "color";
|
|
41
|
+
/** `var(--niceeval-color-series-N)`。 */
|
|
42
|
+
readonly color: string;
|
|
43
|
+
}
|
|
44
|
+
export interface LineSeriesPresentation extends PresentationIdentity {
|
|
45
|
+
readonly kind: "series";
|
|
46
|
+
readonly mark: "line";
|
|
47
|
+
readonly stroke: string;
|
|
48
|
+
readonly strokeDasharray: string;
|
|
49
|
+
readonly marker: {
|
|
50
|
+
readonly path: string;
|
|
51
|
+
readonly viewBox: string;
|
|
52
|
+
readonly fill: string;
|
|
53
|
+
readonly stroke: string;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface ScatterSeriesPresentation extends PresentationIdentity {
|
|
57
|
+
readonly kind: "series";
|
|
58
|
+
readonly mark: "scatter";
|
|
59
|
+
readonly marker: LineSeriesPresentation["marker"];
|
|
60
|
+
}
|
|
61
|
+
export interface FillSeriesPresentation extends PresentationIdentity {
|
|
62
|
+
readonly kind: "series";
|
|
63
|
+
readonly mark: "bar" | "area";
|
|
64
|
+
/** 颜色,或可直接使用的 `url(#pattern-id)`。 */
|
|
65
|
+
readonly fill: string;
|
|
66
|
+
readonly stroke: string;
|
|
67
|
+
readonly strokeDasharray: string;
|
|
68
|
+
}
|
|
69
|
+
export type DimensionPresentation = LabelPresentation | ColorPresentation | LineSeriesPresentation | ScatterSeriesPresentation | FillSeriesPresentation;
|
|
70
|
+
export type PresentationFor<E extends DimensionEncoding> = E extends {
|
|
71
|
+
kind: "label";
|
|
72
|
+
} ? LabelPresentation : E extends {
|
|
73
|
+
kind: "color";
|
|
74
|
+
} ? ColorPresentation : LineSeriesPresentation | ScatterSeriesPresentation | FillSeriesPresentation;
|
|
75
|
+
/**
|
|
76
|
+
* 句柄上的呈现面。`at(i)` 返回可直接交给 SVG/CSS 的值;text 面恒为 label 支。
|
|
77
|
+
* `labels` 是本维度整页 label keyset 的最短唯一后缀表,便于探针与调试。
|
|
78
|
+
*/
|
|
33
79
|
export interface PresentedDimension {
|
|
80
|
+
readonly length: number;
|
|
34
81
|
readonly labels: ReadonlyMap<string, string>;
|
|
35
|
-
at(index: number):
|
|
82
|
+
at(index: number): DimensionPresentation;
|
|
36
83
|
}
|
|
37
84
|
export declare class UndeclaredDimensionValueError extends Error {
|
|
38
85
|
readonly handle: string;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { shortestUniqueLabels as labels } from "./model/format.js";
|
|
2
|
+
import { seriesColorVar, seriesFill, seriesMarker, seriesStrokeDasharray, } from "./assets/series-encoding.js";
|
|
2
3
|
export const VISUAL_SLOT_COUNT = 24;
|
|
3
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* 槽 1..24 → (色板下标 1..6, 形状变体 1..4)。
|
|
6
|
+
* 与 components/README「视觉编码容量」槽序表一致:1–6 第一变体、7–12 第二变体,依此类推。
|
|
7
|
+
* `colorIndex = (slot-1) % 6 + 1`,`variant = floor((slot-1)/6) + 1`。
|
|
8
|
+
*/
|
|
4
9
|
export function seriesChannelsOf(slot) {
|
|
5
10
|
if (!Number.isInteger(slot) || slot < 1 || slot > VISUAL_SLOT_COUNT) {
|
|
6
11
|
throw new Error(`seriesSlot must be an integer in [1, ${VISUAL_SLOT_COUNT}], got ${slot}.`);
|
|
@@ -8,7 +13,7 @@ export function seriesChannelsOf(slot) {
|
|
|
8
13
|
const zero = slot - 1;
|
|
9
14
|
return {
|
|
10
15
|
colorIndex: (zero % 6) + 1,
|
|
11
|
-
variant: (
|
|
16
|
+
variant: (Math.floor(zero / 6) + 1),
|
|
12
17
|
};
|
|
13
18
|
}
|
|
14
19
|
export class UndeclaredDimensionValueError extends Error {
|
|
@@ -33,6 +38,35 @@ export function presentDimension(declaration) {
|
|
|
33
38
|
return presented;
|
|
34
39
|
}
|
|
35
40
|
export const shortestUniqueLabels = labels;
|
|
41
|
+
function presentChannels(value, label, seriesSlot, encoding) {
|
|
42
|
+
const { colorIndex, variant } = seriesChannelsOf(seriesSlot);
|
|
43
|
+
if (encoding.kind === "color") {
|
|
44
|
+
return { kind: "color", value, label, color: seriesColorVar(colorIndex) };
|
|
45
|
+
}
|
|
46
|
+
if (encoding.kind !== "series") {
|
|
47
|
+
return { kind: "label", value, label };
|
|
48
|
+
}
|
|
49
|
+
const stroke = seriesColorVar(colorIndex);
|
|
50
|
+
const strokeDasharray = seriesStrokeDasharray(variant);
|
|
51
|
+
const marker = seriesMarker(colorIndex, variant);
|
|
52
|
+
switch (encoding.mark) {
|
|
53
|
+
case "line":
|
|
54
|
+
return { kind: "series", mark: "line", value, label, stroke, strokeDasharray, marker };
|
|
55
|
+
case "scatter":
|
|
56
|
+
return { kind: "series", mark: "scatter", value, label, marker };
|
|
57
|
+
case "bar":
|
|
58
|
+
case "area":
|
|
59
|
+
return {
|
|
60
|
+
kind: "series",
|
|
61
|
+
mark: encoding.mark,
|
|
62
|
+
value,
|
|
63
|
+
label,
|
|
64
|
+
fill: seriesFill(colorIndex, variant),
|
|
65
|
+
stroke,
|
|
66
|
+
strokeDasharray,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
36
70
|
/**
|
|
37
71
|
* 页级呈现分配:收集整页 dimensions() 声明,产出 label / visual 两套 keyset 上的映射。
|
|
38
72
|
* pins 原样占位;未钉键以稳定散列为起点、撞槽按显示键字典序线性探测。
|
|
@@ -79,6 +113,7 @@ export function allocatePageDimensions(handles, pins = {}, options = { face: "we
|
|
|
79
113
|
const values = declaration.values;
|
|
80
114
|
const encoding = declaration.encoding;
|
|
81
115
|
byHandle.set(handle, {
|
|
116
|
+
length: values.length,
|
|
82
117
|
labels: dimLabels,
|
|
83
118
|
at(index) {
|
|
84
119
|
const value = values[index];
|
|
@@ -86,15 +121,15 @@ export function allocatePageDimensions(handles, pins = {}, options = { face: "we
|
|
|
86
121
|
throw new UndeclaredDimensionValueError(`dimension handle "${handle}" has no value at index ${index} (declared length ${values.length}).`, handle, index);
|
|
87
122
|
}
|
|
88
123
|
const label = dimLabels.get(value) ?? value;
|
|
124
|
+
// text 面恒返回 label 支:不上 ANSI 色,拿不到颜色、线型或 pattern。
|
|
89
125
|
if (options.face === "text" || encoding.kind === "label") {
|
|
90
|
-
return { value, label };
|
|
126
|
+
return { kind: "label", value, label };
|
|
91
127
|
}
|
|
92
128
|
const seriesSlot = slots?.get(value);
|
|
93
129
|
if (seriesSlot === undefined) {
|
|
94
130
|
throw new UndeclaredDimensionValueError(`dimension handle "${handle}" value ${JSON.stringify(value)} has no seriesSlot on this page.`, handle, index);
|
|
95
131
|
}
|
|
96
|
-
|
|
97
|
-
return { value, label, seriesSlot, colorIndex: channels.colorIndex, variant: channels.variant };
|
|
132
|
+
return presentChannels(value, label, seriesSlot, encoding);
|
|
98
133
|
},
|
|
99
134
|
});
|
|
100
135
|
}
|
|
@@ -8,6 +8,6 @@ export { formatCellText } from "../definition/cell.ts";
|
|
|
8
8
|
export type { AttemptEvidence, AttemptEvidenceCapabilities } from "../../record/attempt-evidence.ts";
|
|
9
9
|
export { formatAxisTick, formatInstant, formatMetricValue, formatTimeDistance, missingText } from "../model/format.ts";
|
|
10
10
|
export { presentDimension, shortestUniqueLabels } from "../presentation.ts";
|
|
11
|
-
export type { DimensionDeclaration, DimensionEncoding, PresentedDimension } from "../presentation.ts";
|
|
11
|
+
export type { ColorPresentation, DimensionDeclaration, DimensionEncoding, DimensionPresentation, FillSeriesPresentation, LabelPresentation, LineSeriesPresentation, PresentedDimension, ScatterSeriesPresentation, } from "../presentation.ts";
|
|
12
12
|
export { DEFAULT_REPORT_LOCALE, localizedTextEquals, resolveLocalizedText, resolveMetricLabel, } from "../model/locale.ts";
|
|
13
13
|
export type { LocalizedText, ReportLocale } from "../model/locale.ts";
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// - core 中立:只认 Metric / Dimension 接口,不出现具体 agent 名的分支。
|
|
13
13
|
import { comparabilityConfigOf, deepEqualJson } from "../../sample/index.js";
|
|
14
14
|
import { foldEvalVerdict } from "../../shared/verdict.js";
|
|
15
|
-
import { assertUniqueMetricNames, axisValueOf, collectItems, computeCell, dimensionKey, dimensionName, evalGroupOf, evalIdOf, evaluateMetric, experimentIdOf, filterItems, fullEvalKey, groupItems, historicalOf, locatorOf, refDisplayKey, resolveInput, seriesKey, seriesName, toColumn, } from "../model/aggregate.js";
|
|
15
|
+
import { assertUniqueMetricNames, axisValueOf, collectItems, computeCell, dimensionKey, dimensionName, evalGroupOf, evalIdOf, evaluateMetric, experimentIdOf, filterItems, fullEvalKey, groupItems, historicalOf, locatorOf, msSince, refDisplayKey, resolveInput, seriesKey, seriesName, staleReferenceOf, toColumn, } from "../model/aggregate.js";
|
|
16
16
|
import { costUSD as costUSDMetric, examScore, tokens as tokensMetric, totalScore as totalScoreMetric } from "../model/metrics.js";
|
|
17
17
|
import { formatMetricValue, formatPlainNumber } from "../model/format.js";
|
|
18
18
|
import { metricFieldOf } from "../model/dataset.js";
|
|
@@ -420,6 +420,10 @@ async function buildDeltaCell(items) {
|
|
|
420
420
|
const verdict = foldEvalVerdict(items.map((item) => ({ verdict: item.attempt.result.verdict })));
|
|
421
421
|
const refs = new Set();
|
|
422
422
|
let historical = false;
|
|
423
|
+
// 距今最久的历史执行决定这一格的时距——多个 attempt 折进同一格时,时距说的是「这批结论里
|
|
424
|
+
// 最旧的证据有多旧」,不是最新那条(与 experiment-table 的 attempt 行时距同一份数据来源,
|
|
425
|
+
// 只是这里先跨 attempt 折叠成一格)。
|
|
426
|
+
let staleStartedAt;
|
|
423
427
|
let scoreSum = 0;
|
|
424
428
|
let scoreCount = 0;
|
|
425
429
|
let tokensSum = 0;
|
|
@@ -428,8 +432,12 @@ async function buildDeltaCell(items) {
|
|
|
428
432
|
let costCount = 0;
|
|
429
433
|
for (const item of items) {
|
|
430
434
|
refs.add(locatorOf(item));
|
|
431
|
-
if (historicalOf(item))
|
|
435
|
+
if (historicalOf(item)) {
|
|
432
436
|
historical = true;
|
|
437
|
+
const startedAt = item.attempt.result.startedAt ?? item.run.startedAt;
|
|
438
|
+
if (staleStartedAt === undefined || startedAt < staleStartedAt)
|
|
439
|
+
staleStartedAt = startedAt;
|
|
440
|
+
}
|
|
433
441
|
if (evaluationKind === "points") {
|
|
434
442
|
const value = await evaluateMetric(totalScoreMetric, item.attempt);
|
|
435
443
|
if (value !== null) {
|
|
@@ -458,10 +466,21 @@ async function buildDeltaCell(items) {
|
|
|
458
466
|
...(tokensCount > 0 ? { totalTokens: tokensSum } : {}),
|
|
459
467
|
...(costCount > 0 ? { totalCostUSD: costSum } : {}),
|
|
460
468
|
historical,
|
|
469
|
+
...(staleStartedAt !== undefined ? { staleSinceMs: msSince(staleStartedAt) } : {}),
|
|
461
470
|
};
|
|
462
471
|
}
|
|
472
|
+
/** 每个 experiment 当前基准的 configHash:该 experiment 在 `runs` 里最新快照的 configHash。 */
|
|
473
|
+
function latestConfigHashByExperiment(runs) {
|
|
474
|
+
const latest = new Map();
|
|
475
|
+
for (const run of runs) {
|
|
476
|
+
const existing = latest.get(run.experimentId);
|
|
477
|
+
if (existing === undefined || run.startedAt > existing.startedAt)
|
|
478
|
+
latest.set(run.experimentId, run);
|
|
479
|
+
}
|
|
480
|
+
return new Map([...latest].map(([id, run]) => [id, run.configHash]));
|
|
481
|
+
}
|
|
463
482
|
export async function deltaTableData(input, options) {
|
|
464
|
-
const { runs, attempts } = resolveInput(input);
|
|
483
|
+
const { runs, attempts, historyAttempts, fresh } = resolveInput(input);
|
|
465
484
|
const items = filterItems(collectItems(runs, attempts), options.evals);
|
|
466
485
|
let conditions;
|
|
467
486
|
let experiments;
|
|
@@ -526,6 +545,13 @@ export async function deltaTableData(input, options) {
|
|
|
526
545
|
for (const evalId of byEval.keys())
|
|
527
546
|
evalIdsSet.add(evalId);
|
|
528
547
|
const knownEvalIds = [...evalIdsSet].sort();
|
|
548
|
+
// 每个条件背后的 experiment id 集合(by: "experiment" 时条件字符串本身通常就是唯一一个 id);
|
|
549
|
+
// 过期结论参考按各自 experiment 的当前基准 configHash 判定。
|
|
550
|
+
const experimentIdsByCondition = new Map();
|
|
551
|
+
for (const [condition, conditionItems] of itemsByCondition) {
|
|
552
|
+
experimentIdsByCondition.set(condition, new Set(conditionItems.map((item) => experimentIdOf(item))));
|
|
553
|
+
}
|
|
554
|
+
const anchorConfigHashByExperiment = latestConfigHashByExperiment(runs);
|
|
529
555
|
const baseline = conditions[0];
|
|
530
556
|
const rows = [];
|
|
531
557
|
for (const evalId of knownEvalIds) {
|
|
@@ -558,7 +584,29 @@ export async function deltaTableData(input, options) {
|
|
|
558
584
|
}
|
|
559
585
|
}
|
|
560
586
|
}
|
|
561
|
-
|
|
587
|
+
// 过期结论参考(show/compare.md「— ✓ 12d」):只在该条件缺席这道题、且记录里存在与该
|
|
588
|
+
// experiment 当前基准不可比的历史判定时才给,口径与 experiment-table 两档占位行一致——
|
|
589
|
+
// `fresh` 为 true 时不给参考,读者已声明只看新执行。它只写进 references,不进 cells,
|
|
590
|
+
// 因此天然不参与 totals / delta / 配对覆盖的任何一个数。
|
|
591
|
+
let references;
|
|
592
|
+
if (!fresh) {
|
|
593
|
+
for (const condition of conditions) {
|
|
594
|
+
if (cells[condition] !== undefined)
|
|
595
|
+
continue;
|
|
596
|
+
const experimentIds = experimentIdsByCondition.get(condition);
|
|
597
|
+
if (!experimentIds || experimentIds.size === 0)
|
|
598
|
+
continue;
|
|
599
|
+
const candidates = historyAttempts.filter((a) => experimentIds.has(a.experimentId) &&
|
|
600
|
+
a.evalId === evalId &&
|
|
601
|
+
a.run.configHash !== anchorConfigHashByExperiment.get(a.experimentId));
|
|
602
|
+
const reference = staleReferenceOf(candidates);
|
|
603
|
+
if (reference) {
|
|
604
|
+
references ??= {};
|
|
605
|
+
references[condition] = reference;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
rows.push({ key: evalId, flipped, cells, ...(delta ? { delta } : {}), ...(references ? { references } : {}) });
|
|
562
610
|
}
|
|
563
611
|
// 各条件自身覆盖面:分母是该条件有结果的 eval 数,不看其它条件是否也覆盖了这道题。
|
|
564
612
|
const totals = {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { TableContent, TableContentRow } from "../definition/cell.ts";
|
|
2
2
|
import type { DeltaData, StabilityMatrixCell, StabilityMatrixData } from "../model/types.ts";
|
|
3
|
-
|
|
3
|
+
import { type ReportLocale } from "../model/locale.ts";
|
|
4
|
+
export declare function deltaTableContent(data: DeltaData, locale?: ReportLocale): TableContent;
|
|
4
5
|
export interface StabilityContentRow extends TableContentRow {
|
|
5
6
|
readonly evalId: string;
|
|
6
7
|
/** 全部条件历史执行中通过次数为 0 且执行数 > 0。 */
|
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
// 指标视图 TableContent 投影:compute *Data → Table 原语可直接消费的形状。
|
|
2
|
-
import { localizedMessage } from "../model/locale.js";
|
|
3
|
-
|
|
2
|
+
import { DEFAULT_REPORT_LOCALE, localizedMessage } from "../model/locale.js";
|
|
3
|
+
import { formatMetricValue, formatPlainNumber, formatPoints, formatTimeDistance, verdictMark } from "../model/format.js";
|
|
4
|
+
/**
|
|
5
|
+
* 带符号的美元/tokens 差值:复用 `formatMetricValue` 折终值(内建 unit 格式不分 locale,
|
|
6
|
+
* presentation.md「unit 决定格式」),只在外面补一个恒定的正负号——负值 `formatMetricValue`
|
|
7
|
+
* 已经自带 "-",这里只需要给非负值补 "+"。
|
|
8
|
+
*/
|
|
9
|
+
function signedMetricText(value, unit) {
|
|
10
|
+
const sign = value > 0 ? "+" : "";
|
|
11
|
+
return `${sign}${formatMetricValue(value, unit)}`;
|
|
12
|
+
}
|
|
13
|
+
/** 带符号的挣分差值("+4 pts" / "-4 pts");`formatPointsSuffix` 恒加 "+" 号,不适用于可能为负的差值。 */
|
|
14
|
+
function signedPointsText(value) {
|
|
15
|
+
const sign = value > 0 ? "+" : value < 0 ? "-" : "";
|
|
16
|
+
const abs = Math.abs(value);
|
|
17
|
+
return `${sign}${formatPlainNumber(abs)} ${abs === 1 ? "pt" : "pts"}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 一格的判定读法(docs/feature/reports/show/compare.md):有数据时通过制只留判定符(`bare`,
|
|
21
|
+
* 历史执行再接相对时距)、计分制显示挣分;该条件缺席这道题时,记录里有过期结论参考才给
|
|
22
|
+
* `— <判定符> <时距>`(`—` 不被参考替换,它标记「这一格没有可聚合的结果」,参考只是附注),
|
|
23
|
+
* 没有参考就是纯 `notApplicable`。参考不进任何计数——它压根不出现在 `cells` 里,只在这一格
|
|
24
|
+
* 的显示上多一句附注。
|
|
25
|
+
*/
|
|
26
|
+
function conditionVerdictCell(cell, reference, locale) {
|
|
27
|
+
if (!cell) {
|
|
28
|
+
if (!reference)
|
|
29
|
+
return { kind: "notApplicable" };
|
|
30
|
+
const mark = verdictMark(reference.verdict === "skipped" ? "skipped" : reference.verdict);
|
|
31
|
+
return { kind: "text", text: `— ${mark} ${formatTimeDistance(reference.staleSinceMs, locale)}` };
|
|
32
|
+
}
|
|
33
|
+
if (cell.evaluationKind === "points") {
|
|
34
|
+
return { kind: "text", text: cell.totalScore !== undefined ? formatPoints(cell.totalScore) : "—" };
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
kind: "verdict",
|
|
38
|
+
verdict: cell.verdict,
|
|
39
|
+
bare: true,
|
|
40
|
+
...(cell.historical && cell.staleSinceMs !== undefined ? { staleSinceMs: cell.staleSinceMs } : {}),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function deltaTableContent(data, locale = DEFAULT_REPORT_LOCALE) {
|
|
4
44
|
const conditionColumns = data.conditions.flatMap((condition) => [
|
|
5
45
|
{ key: `${condition}:verdict` },
|
|
6
46
|
{ key: `${condition}:tokens` },
|
|
@@ -20,31 +60,24 @@ export function deltaTableContent(data) {
|
|
|
20
60
|
};
|
|
21
61
|
for (const condition of data.conditions) {
|
|
22
62
|
const cell = row.cells[condition];
|
|
23
|
-
cells[`${condition}:verdict`] = cell
|
|
24
|
-
? cell.evaluationKind === "points"
|
|
25
|
-
? {
|
|
26
|
-
kind: "text",
|
|
27
|
-
text: cell.totalScore !== undefined ? String(cell.totalScore) : "—",
|
|
28
|
-
}
|
|
29
|
-
: { kind: "verdict", verdict: cell.verdict }
|
|
30
|
-
: { kind: "notApplicable" };
|
|
63
|
+
cells[`${condition}:verdict`] = conditionVerdictCell(cell, row.references?.[condition], locale);
|
|
31
64
|
cells[`${condition}:tokens`] =
|
|
32
65
|
cell?.totalTokens !== undefined
|
|
33
|
-
? { kind: "
|
|
66
|
+
? { kind: "metric", metric: { value: cell.totalTokens, unit: "tokens", basis: "eval", samples: 1, total: 1, refs: cell.attempts } }
|
|
34
67
|
: { kind: "notApplicable" };
|
|
35
68
|
cells[`${condition}:cost`] =
|
|
36
69
|
cell?.totalCostUSD !== undefined
|
|
37
|
-
? { kind: "
|
|
70
|
+
? { kind: "metric", metric: { value: cell.totalCostUSD, unit: "$", basis: "eval", samples: 1, total: 1, refs: cell.attempts } }
|
|
38
71
|
: { kind: "notApplicable" };
|
|
39
72
|
}
|
|
40
73
|
for (const condition of data.conditions.slice(1)) {
|
|
41
74
|
const delta = row.delta?.[condition];
|
|
42
75
|
cells[`${condition}:Δscore`] =
|
|
43
|
-
delta?.score !== undefined ? { kind: "text", text:
|
|
76
|
+
delta?.score !== undefined ? { kind: "text", text: signedPointsText(delta.score) } : { kind: "notApplicable" };
|
|
44
77
|
cells[`${condition}:Δtokens`] =
|
|
45
|
-
delta?.tokens !== undefined ? { kind: "text", text:
|
|
78
|
+
delta?.tokens !== undefined ? { kind: "text", text: signedMetricText(delta.tokens, "tokens") } : { kind: "notApplicable" };
|
|
46
79
|
cells[`${condition}:Δcost`] =
|
|
47
|
-
delta?.costUSD !== undefined ? { kind: "text", text:
|
|
80
|
+
delta?.costUSD !== undefined ? { kind: "text", text: signedMetricText(delta.costUSD, "$") } : { kind: "notApplicable" };
|
|
48
81
|
}
|
|
49
82
|
return { key: row.key, cells };
|
|
50
83
|
}),
|
|
@@ -129,6 +129,20 @@ function deltaCellProblem(value, path) {
|
|
|
129
129
|
return `"${path}.totalCostUSD" must be a number`;
|
|
130
130
|
if (typeof value.historical !== "boolean")
|
|
131
131
|
return `"${path}.historical" must be a boolean`;
|
|
132
|
+
if (value.staleSinceMs !== undefined && typeof value.staleSinceMs !== "number")
|
|
133
|
+
return `"${path}.staleSinceMs" must be a number`;
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/** 缺席格的过期结论参考(与 experiment-table 占位行同一份 StaleConclusionReference 形状)。 */
|
|
137
|
+
function staleReferenceProblem(value, path) {
|
|
138
|
+
if (!isObject(value))
|
|
139
|
+
return `"${path}" must be an object { locator, verdict, staleSinceMs }`;
|
|
140
|
+
if (typeof value.locator !== "string")
|
|
141
|
+
return `"${path}.locator" must be a string`;
|
|
142
|
+
if (typeof value.verdict !== "string")
|
|
143
|
+
return `"${path}.verdict" must be a string`;
|
|
144
|
+
if (typeof value.staleSinceMs !== "number")
|
|
145
|
+
return `"${path}.staleSinceMs" must be a number`;
|
|
132
146
|
return null;
|
|
133
147
|
}
|
|
134
148
|
function deltaTotalsProblem(value, path) {
|
|
@@ -206,6 +220,15 @@ export const validateDeltaData = (data) => {
|
|
|
206
220
|
return `"${path}.delta.${condition}.costUSD" must be a number`;
|
|
207
221
|
}
|
|
208
222
|
}
|
|
223
|
+
if (row.references !== undefined) {
|
|
224
|
+
if (!isObject(row.references))
|
|
225
|
+
return `"${path}.references" must be an object`;
|
|
226
|
+
for (const [condition, reference] of Object.entries(row.references)) {
|
|
227
|
+
const problem = staleReferenceProblem(reference, `${path}.references.${condition}`);
|
|
228
|
+
if (problem !== null)
|
|
229
|
+
return problem;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
209
232
|
return null;
|
|
210
233
|
});
|
|
211
234
|
if (rowsProblem !== null)
|