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.
Files changed (54) hide show
  1. package/dist/report/assets/series-encoding.d.ts +36 -0
  2. package/dist/report/assets/series-encoding.js +104 -0
  3. package/dist/report/components/entity-lists/compute.js +5 -17
  4. package/dist/report/components/entity-lists/content.d.ts +2 -0
  5. package/dist/report/components/entity-lists/content.js +22 -7
  6. package/dist/report/definition/cell.d.ts +4 -0
  7. package/dist/report/definition/cell.js +5 -1
  8. package/dist/report/definition/primitives/chart.js +125 -25
  9. package/dist/report/definition/primitives.js +3 -2
  10. package/dist/report/definition/tree.d.ts +5 -2
  11. package/dist/report/definition/tree.js +1 -0
  12. package/dist/report/index.d.ts +1 -1
  13. package/dist/report/model/aggregate.d.ts +10 -1
  14. package/dist/report/model/aggregate.js +24 -0
  15. package/dist/report/model/locale.d.ts +0 -1
  16. package/dist/report/model/locale.js +0 -2
  17. package/dist/report/model/types.d.ts +8 -0
  18. package/dist/report/presentation.d.ts +53 -6
  19. package/dist/report/presentation.js +40 -5
  20. package/dist/report/react/index.d.ts +1 -1
  21. package/dist/report/slices/compute.js +52 -4
  22. package/dist/report/slices/content.d.ts +2 -1
  23. package/dist/report/slices/content.js +48 -15
  24. package/dist/report/slices/validate.js +23 -0
  25. package/package.json +1 -1
  26. package/src/report/assets/series-encoding.tsx +177 -0
  27. package/src/report/assets/styles.css +47 -5
  28. package/src/report/components/compute.test.ts +15 -9
  29. package/src/report/components/entity-lists/compute.ts +5 -19
  30. package/src/report/components/entity-lists/content.test.ts +13 -8
  31. package/src/report/components/entity-lists/content.ts +24 -7
  32. package/src/report/definition/cell.ts +8 -1
  33. package/src/report/definition/primitives/chart.tsx +197 -32
  34. package/src/report/definition/primitives.tsx +8 -3
  35. package/src/report/definition/tree.ts +6 -2
  36. package/src/report/index.ts +11 -1
  37. package/src/report/model/aggregate.ts +26 -0
  38. package/src/report/model/locale.ts +0 -2
  39. package/src/report/model/types.ts +8 -0
  40. package/src/report/presentation.test.tsx +179 -19
  41. package/src/report/presentation.ts +114 -11
  42. package/src/report/react/index.tsx +11 -1
  43. package/src/report/slices/compute.ts +59 -4
  44. package/src/report/slices/content.ts +55 -16
  45. package/src/report/slices/delta-table.test.ts +138 -3
  46. package/src/report/slices/validate.test.ts +29 -0
  47. package/src/report/slices/validate.ts +16 -0
  48. package/src/runner/gate-lease.test.ts +38 -14
  49. package/src/runner/lock.test.ts +40 -16
  50. package/src/sandbox/compose.ts +3 -1
  51. package/src/sandbox/dockerfile-build.ts +4 -1
  52. package/src/sandbox/runtime.ts +4 -1
  53. package/src/sandbox/vercel.test.ts +12 -3
  54. package/src/show/index.ts +10 -9
@@ -0,0 +1,36 @@
1
+ import type { ReactNode } from "react";
2
+ import type { SeriesVariant } from "../presentation.ts";
3
+ /** 槽位色板下标 1..6 → CSS 变量,原样交给 fill / stroke / color。 */
4
+ export declare function seriesColorVar(colorIndex: number): string;
5
+ /** SVG pattern id:niceeval-series-pat-v{2|3|4}-c{1..6}。variant 1 是实心,无 pattern。 */
6
+ export declare function seriesPatternId(colorIndex: number, variant: SeriesVariant): string;
7
+ export declare function seriesFill(colorIndex: number, variant: SeriesVariant): string;
8
+ /** 线型变体 → strokeDasharray;variant 1 为空串(实线)。 */
9
+ export declare function seriesStrokeDasharray(variant: SeriesVariant): string;
10
+ export interface SeriesMarkerShape {
11
+ readonly path: string;
12
+ readonly viewBox: string;
13
+ }
14
+ /** 四种 marker 形状(viewBox 0 0 12 12),变体按 docs 槽序表 1–4。 */
15
+ export declare function seriesMarkerShape(variant: SeriesVariant): SeriesMarkerShape;
16
+ export declare function seriesMarker(colorIndex: number, variant: SeriesVariant): {
17
+ readonly path: string;
18
+ readonly viewBox: string;
19
+ readonly fill: string;
20
+ readonly stroke: string;
21
+ };
22
+ /**
23
+ * 从 presentation.fill 反推 HTML 柱需要的系列 class。
24
+ * SVG 用 url(#pattern)/var();HTML 柱不能引用 SVG pattern,改挂 series-cN + fill-vN 类,
25
+ * 由 styles.css 用 repeating-linear-gradient 画等效图案,颜色仍走 --series 令牌。
26
+ */
27
+ export declare function seriesClassesFromFill(fill: string): string;
28
+ /** 从 color 呈现或 series 的 stroke/fill 反推 series-cN 类(图例色点回落)。 */
29
+ export declare function seriesClassFromColorVar(color: string): string;
30
+ /**
31
+ * 页内注入一次的 SVG pattern defs。
32
+ * 18 个 pattern(3 非实心变体 × 6 色);id 与 seriesFill() 产出的 url(#…) 对齐。
33
+ * 子元素直接写 var(--niceeval-color-series-N),不走 currentColor
34
+ * (SVG pattern 内部 currentColor 取自 pattern 自身,引用者传不进来)。
35
+ */
36
+ export declare function SeriesPatternDefs(): ReactNode;
@@ -0,0 +1,104 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /** 槽位色板下标 1..6 → CSS 变量,原样交给 fill / stroke / color。 */
3
+ export function seriesColorVar(colorIndex) {
4
+ if (!Number.isInteger(colorIndex) || colorIndex < 1 || colorIndex > 6) {
5
+ throw new Error(`colorIndex must be an integer in [1, 6], got ${colorIndex}.`);
6
+ }
7
+ return `var(--niceeval-color-series-${colorIndex})`;
8
+ }
9
+ /** SVG pattern id:niceeval-series-pat-v{2|3|4}-c{1..6}。variant 1 是实心,无 pattern。 */
10
+ export function seriesPatternId(colorIndex, variant) {
11
+ if (variant === 1) {
12
+ throw new Error("variant 1 is solid fill; it has no pattern id.");
13
+ }
14
+ return `niceeval-series-pat-v${variant}-c${colorIndex}`;
15
+ }
16
+ export function seriesFill(colorIndex, variant) {
17
+ if (variant === 1)
18
+ return seriesColorVar(colorIndex);
19
+ return `url(#${seriesPatternId(colorIndex, variant)})`;
20
+ }
21
+ /** 线型变体 → strokeDasharray;variant 1 为空串(实线)。 */
22
+ export function seriesStrokeDasharray(variant) {
23
+ switch (variant) {
24
+ case 1:
25
+ return "";
26
+ case 2:
27
+ return "6 4";
28
+ case 3:
29
+ return "2 3";
30
+ case 4:
31
+ return "8 3 2 3";
32
+ }
33
+ }
34
+ /** 四种 marker 形状(viewBox 0 0 12 12),变体按 docs 槽序表 1–4。 */
35
+ export function seriesMarkerShape(variant) {
36
+ switch (variant) {
37
+ case 1:
38
+ // 圆
39
+ return { path: "M6 1.5a4.5 4.5 0 1 0 0.01 0z", viewBox: "0 0 12 12" };
40
+ case 2:
41
+ // 方
42
+ return { path: "M2.5 2.5h7v7h-7z", viewBox: "0 0 12 12" };
43
+ case 3:
44
+ // 菱
45
+ return { path: "M6 1.5 10.5 6 6 10.5 1.5 6z", viewBox: "0 0 12 12" };
46
+ case 4:
47
+ // 三角
48
+ return { path: "M6 1.5 10.5 10.5 1.5 10.5z", viewBox: "0 0 12 12" };
49
+ }
50
+ }
51
+ export function seriesMarker(colorIndex, variant) {
52
+ const shape = seriesMarkerShape(variant);
53
+ const color = seriesColorVar(colorIndex);
54
+ return {
55
+ path: shape.path,
56
+ viewBox: shape.viewBox,
57
+ fill: color,
58
+ stroke: color,
59
+ };
60
+ }
61
+ /**
62
+ * 从 presentation.fill 反推 HTML 柱需要的系列 class。
63
+ * SVG 用 url(#pattern)/var();HTML 柱不能引用 SVG pattern,改挂 series-cN + fill-vN 类,
64
+ * 由 styles.css 用 repeating-linear-gradient 画等效图案,颜色仍走 --series 令牌。
65
+ */
66
+ export function seriesClassesFromFill(fill) {
67
+ const pattern = /^url\(#niceeval-series-pat-v([2-4])-c([1-6])\)$/.exec(fill);
68
+ if (pattern) {
69
+ const variant = pattern[1];
70
+ const colorIndex = Number(pattern[2]);
71
+ return `niceeval-series-c${colorIndex - 1} niceeval-series-fill-v${variant}`;
72
+ }
73
+ const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(fill);
74
+ if (solid) {
75
+ return `niceeval-series-c${Number(solid[1]) - 1}`;
76
+ }
77
+ return "niceeval-series-none";
78
+ }
79
+ /** 从 color 呈现或 series 的 stroke/fill 反推 series-cN 类(图例色点回落)。 */
80
+ export function seriesClassFromColorVar(color) {
81
+ const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(color);
82
+ if (solid)
83
+ return `niceeval-series-c${Number(solid[1]) - 1}`;
84
+ return "niceeval-series-none";
85
+ }
86
+ /**
87
+ * 页内注入一次的 SVG pattern defs。
88
+ * 18 个 pattern(3 非实心变体 × 6 色);id 与 seriesFill() 产出的 url(#…) 对齐。
89
+ * 子元素直接写 var(--niceeval-color-series-N),不走 currentColor
90
+ * (SVG pattern 内部 currentColor 取自 pattern 自身,引用者传不进来)。
91
+ */
92
+ export function SeriesPatternDefs() {
93
+ const patterns = [];
94
+ for (let colorIndex = 1; colorIndex <= 6; colorIndex++) {
95
+ const color = seriesColorVar(colorIndex);
96
+ // v2:对角斜线
97
+ patterns.push(_jsxs("pattern", { id: seriesPatternId(colorIndex, 2), patternUnits: "userSpaceOnUse", width: "6", height: "6", children: [_jsx("rect", { width: "6", height: "6", fill: color, fillOpacity: 0.22 }), _jsx("path", { d: "M-1 1l2-2M0 6l6-6M5 7l2-2", stroke: color, strokeWidth: 1.4, fill: "none" })] }, `v2-c${colorIndex}`));
98
+ // v3:水平条纹
99
+ patterns.push(_jsxs("pattern", { id: seriesPatternId(colorIndex, 3), patternUnits: "userSpaceOnUse", width: "6", height: "6", children: [_jsx("rect", { width: "6", height: "6", fill: color, fillOpacity: 0.18 }), _jsx("path", { d: "M0 1.5h6M0 4.5h6", stroke: color, strokeWidth: 1.5, fill: "none" })] }, `v3-c${colorIndex}`));
100
+ // v4:点阵
101
+ patterns.push(_jsxs("pattern", { id: seriesPatternId(colorIndex, 4), patternUnits: "userSpaceOnUse", width: "6", height: "6", children: [_jsx("rect", { width: "6", height: "6", fill: color, fillOpacity: 0.16 }), _jsx("circle", { cx: "2", cy: "2", r: "1.15", fill: color }), _jsx("circle", { cx: "5", cy: "5", r: "1.15", fill: color })] }, `v4-c${colorIndex}`));
102
+ }
103
+ return (_jsx("svg", { className: "niceeval-series-defs", width: 0, height: 0, "aria-hidden": "true", focusable: "false", style: { position: "absolute", width: 0, height: 0, overflow: "hidden" }, children: _jsx("defs", { children: patterns }) }));
104
+ }
@@ -6,10 +6,9 @@
6
6
  // - 聚合前按身份键去重(dedupeAttempts;missing-startedAt 不去重、如实保留、不透出警告);
7
7
  // - null ≠ 0:缺数据不编数,覆盖率经 samples/total 如实暴露;
8
8
  // - core 中立:只认 Metric / Dimension 接口,不出现具体 agent 名的分支。
9
- import { encodeAttemptLocator } from "../../../record/locator.js";
10
9
  import { comparabilityConfigOf, deepEqualJson } from "../../../sample/index.js";
11
10
  import { foldEvalVerdict } from "../../../shared/verdict.js";
12
- import { collectItems, computeCell, evalIdOf, experimentIdOf, fullEvalKey, groupItems, historicalOf, locatorOf, resolveInput, } from "../../model/aggregate.js";
11
+ import { collectItems, computeCell, evalIdOf, experimentIdOf, fullEvalKey, groupItems, historicalOf, locatorOf, msSince, resolveInput, staleReferenceOf, } from "../../model/aggregate.js";
13
12
  import { attemptCostUSD, costUSD, durationMs, examScore, passRate, tokens, totalScore } from "../../model/metrics.js";
14
13
  import { compactAssertionSummary, primaryAssertionSummary, summaryText } from "../../../assertions/display.js";
15
14
  import { firstLine } from "../../../util.js";
@@ -68,14 +67,10 @@ async function attemptListItemOf(item) {
68
67
  costUSD: attemptCostUSD(result),
69
68
  startedAt,
70
69
  historical,
71
- ...(historical ? { staleSinceMs: staleSinceMsOf(startedAt) } : {}),
70
+ ...(historical ? { staleSinceMs: msSince(startedAt) } : {}),
72
71
  locator: locatorOf(item),
73
72
  };
74
73
  }
75
- /** 一个 ISO 时刻距渲染时刻(`Date.now()`)的毫秒数,恒不小于 0。 */
76
- function staleSinceMsOf(startedAtIso) {
77
- return Math.max(0, Date.now() - Date.parse(startedAtIso));
78
- }
79
74
  /**
80
75
  * 「只看新执行」开关在场的判据(docs/feature/reports/components/summaries/experiment-table.md
81
76
  * 「只看新执行」):Sample 里既没有历史执行也没有过期结论时不画开关——一个永远不改变行集的
@@ -201,16 +196,9 @@ function staleReferencesFor(experimentId, missingEvalIds, historyAttempts, ancho
201
196
  }
202
197
  const out = {};
203
198
  for (const [evalId, candidates] of candidatesByEval) {
204
- const newest = candidates.reduce((a, b) => (a.result.startedAt ?? "") >= (b.result.startedAt ?? "") ? a : b);
205
- const startedAt = newest.result.startedAt;
206
- if (!startedAt)
207
- continue; // 无时刻的 legacy 落盘算不出时距,不伪造参考
208
- out[evalId] = {
209
- locator: newest.locator ??
210
- encodeAttemptLocator({ runId: newest.run.runId, evalId: newest.evalId, attempt: newest.result.attempt }),
211
- verdict: newest.result.verdict,
212
- staleSinceMs: staleSinceMsOf(startedAt),
213
- };
199
+ const reference = staleReferenceOf(candidates);
200
+ if (reference)
201
+ out[evalId] = reference;
214
202
  }
215
203
  return out;
216
204
  }
@@ -1,5 +1,7 @@
1
1
  import type { TableContent } from "../../definition/cell.ts";
2
2
  import type { AttemptListItem, EvalListItem, ExperimentListItem } from "../../model/types.ts";
3
+ /** 覆盖构成副行的 key 前缀;测试与消费方靠它把这一行从 Eval / 组行里筛出去。 */
4
+ export declare const COVERAGE_ROW_PREFIX = "coverage:";
3
5
  export declare function experimentListContent(items: readonly ExperimentListItem[]): TableContent;
4
6
  export declare function evalListContent(items: readonly EvalListItem[]): TableContent;
5
7
  export declare function attemptListContent(items: readonly AttemptListItem[]): TableContent;
@@ -23,7 +23,6 @@ const HEADER = {
23
23
  verdict: localizedMessage("experimentList.status"),
24
24
  result: localizedMessage("experimentList.result"),
25
25
  score: localizedMessage("experimentList.totalScore"),
26
- coverage: localizedMessage("experimentList.coverage"),
27
26
  };
28
27
  /** 原料 → 行 cells:列集外的原料丢掉,原料没覆盖的列显式填 notApplicable。 */
29
28
  function projectCells(bag, columns) {
@@ -382,13 +381,33 @@ function coverageSegments(item) {
382
381
  count: [fresh, historical, stale, notRun][i],
383
382
  }));
384
383
  }
385
- /** experiment evalRows + missingEvalIds 递归嵌套的 subRows。 */
384
+ /** 覆盖构成副行的 key 前缀;测试与消费方靠它把这一行从 Eval / 组行里筛出去。 */
385
+ export const COVERAGE_ROW_PREFIX = "coverage:";
386
+ /**
387
+ * 覆盖构成副行(docs/feature/reports/components/summaries/experiment-table.md「覆盖构成」):
388
+ * experiment 行 subRows 的最后一条,把已知题按结论出身分成四段互斥的构成格,交给中立的
389
+ * `composition` 格——渲染在同一个 `record` 列位置(与 Eval / Attempt 行的判定构成、占位行的
390
+ * missing 格同一个槽位,三种形态各自对应不同的行语义,不是同一行的三种读法)。
391
+ * 它不是 Eval / 组行,没有身份(entity 是 notApplicable),不参与嵌套排序或收起判定。
392
+ */
393
+ function coverageRow(item, view) {
394
+ const bag = {
395
+ entity: { kind: "notApplicable" },
396
+ record: { kind: "composition", segments: coverageSegments(item) },
397
+ };
398
+ return {
399
+ key: `${COVERAGE_ROW_PREFIX}${item.experimentId}`,
400
+ cells: projectCells(bag, view.columns),
401
+ };
402
+ }
403
+ /** experiment 的 evalRows + missingEvalIds → 递归嵌套的 subRows,末尾追加覆盖构成副行。 */
386
404
  function experimentSubRows(item, view) {
387
405
  const members = [
388
406
  ...item.evalRows.map((row) => ({ kind: "eval", row })),
389
407
  ...item.missingEvalIds.map((evalId) => ({ kind: "missing", evalId })),
390
408
  ];
391
- return nestLevel(members, "", "", item, view);
409
+ const nested = nestLevel(members, "", "", item, view);
410
+ return members.length > 0 ? [...nested, coverageRow(item, view)] : nested;
392
411
  }
393
412
  function experimentRow(item, view) {
394
413
  const bag = {
@@ -401,9 +420,6 @@ function experimentRow(item, view) {
401
420
  tokens: measureCell(item.tokens),
402
421
  costUSD: measureCell(item.costUSD),
403
422
  record: verdictCell(item.evalVerdicts),
404
- // 覆盖构成是 experiment 这一行独有的事实(experiment-table.md「覆盖构成」),Eval / Attempt /
405
- // 路径段组行没有这一格——projectCells 按列集自动填 notApplicable,不额外分支。
406
- coverage: { kind: "composition", segments: coverageSegments(item) },
407
423
  };
408
424
  return {
409
425
  key: item.experimentId,
@@ -423,7 +439,6 @@ function experimentColumns(composition) {
423
439
  { key: "tokens", better: "lower", header: HEADER.tokens },
424
440
  { key: "costUSD", better: "lower", header: HEADER.costUSD },
425
441
  { key: "record", header: HEADER.record },
426
- { key: "coverage", header: HEADER.coverage },
427
442
  ];
428
443
  }
429
444
  /** Eval / Attempt 平铺表的列集(两张表同一份)。 */
@@ -25,6 +25,10 @@ export type Cell = {
25
25
  readonly counts?: VerdictCounts;
26
26
  /** 计票覆盖的 attempt 引用(有证据可下钻的计票格才携带,如稳定性矩阵)。 */
27
27
  readonly refs?: readonly AttemptLocator[];
28
+ /** 单判定形态历史执行的距今毫秒数;新执行时省略,不伪造 0(与 locator 格同一条纪律)。 */
29
+ readonly staleSinceMs?: number;
30
+ /** 单判定形态省略判定词、只留判定符(如对照矩阵逐格只放得下一个符号的场景)。 */
31
+ readonly bare?: boolean;
28
32
  } | {
29
33
  readonly kind: "score";
30
34
  readonly earned: number;
@@ -40,8 +40,12 @@ export function formatCellText(cell, locale) {
40
40
  }
41
41
  if (cell.verdict !== undefined) {
42
42
  const v = cell.verdict === "skipped" ? "skipped" : cell.verdict;
43
+ const stale = cell.staleSinceMs !== undefined ? ` ${formatTimeDistance(cell.staleSinceMs, loc)}` : "";
43
44
  // 判定符与判定词同场,与 locator 格、web 面同一条纪律:单色打印下照样读得出。
44
- return `${verdictMark(v)} ${localeText(loc, `verdict.${v}`)}`;
45
+ // bare 省略判定词,只留判定符(+ 可选时距),供逐格空间紧张的场景(如对照矩阵)使用。
46
+ if (cell.bare)
47
+ return `${verdictMark(v)}${stale}`;
48
+ return `${verdictMark(v)} ${localeText(loc, `verdict.${v}`)}${stale}`;
45
49
  }
46
50
  return "—";
47
51
  }
@@ -1,4 +1,5 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { SeriesPatternDefs, seriesClassFromColorVar, seriesClassesFromFill, } from "../../assets/series-encoding.js";
2
3
  import { defineComponent } from "../tree.js";
3
4
  import { countText, localeText, resolveLocalizedText } from "../../model/locale.js";
4
5
  import { formatAxisTick, formatMetricValue, shortestUniqueLabels } from "../../model/format.js";
@@ -91,7 +92,8 @@ function chartFieldLabel(field, meta, locale) {
91
92
  const base = dictionary[field] ? localeText(locale, dictionary[field]) : field;
92
93
  return meta.unit ? `${base}(${meta.unit})` : base;
93
94
  }
94
- function seriesClass(mapped, series, point, ctx) {
95
+ /** 解析点所属 series 句柄与下标;单系列隐式图不声明视觉身份。 */
96
+ function seriesPresentationOf(mapped, series, point, ctx) {
95
97
  let handle;
96
98
  let index;
97
99
  if (series.byField !== undefined && point.seriesValue !== undefined) {
@@ -101,12 +103,104 @@ function seriesClass(mapped, series, point, ctx) {
101
103
  else {
102
104
  const ids = mapped.filter((item) => !item.hidden && item.byField === undefined).map((item) => item.id);
103
105
  if (ids.length < 2)
104
- return "niceeval-series-none";
106
+ return undefined;
105
107
  handle = IMPLICIT_SERIES_HANDLE;
106
108
  index = ids.indexOf(series.id);
107
109
  }
108
- const colorIndex = ctx.dimension(handle).at(index).colorIndex;
109
- return colorIndex === undefined ? "niceeval-series-none" : `niceeval-series-c${colorIndex - 1}`;
110
+ if (index < 0)
111
+ return undefined;
112
+ return ctx.dimension(handle).at(index);
113
+ }
114
+ /** SVG 路径:只挂 series-cN 设 --series;pattern 由 fill 属性 / style 承载。 */
115
+ function seriesColorClass(presentation) {
116
+ if (!presentation)
117
+ return "niceeval-series-none";
118
+ if (presentation.kind === "color")
119
+ return seriesClassFromColorVar(presentation.color);
120
+ if (presentation.kind !== "series")
121
+ return "niceeval-series-none";
122
+ // switch 按 mark 收窄;Fill 的 mark 是 "bar"|"area" 联合,if/|| 在部分 TS 版本下剔不干净。
123
+ switch (presentation.mark) {
124
+ case "bar":
125
+ case "area": {
126
+ // 只要色类,不要 HTML 专用的 fill-vN(SVG 用 url(#pattern))。
127
+ const classes = seriesClassesFromFill(presentation.fill).split(" ");
128
+ return classes.find((c) => c.startsWith("niceeval-series-c") || c === "niceeval-series-none") ?? "niceeval-series-none";
129
+ }
130
+ case "line":
131
+ return seriesClassFromColorVar(presentation.stroke);
132
+ case "scatter":
133
+ return seriesClassFromColorVar(presentation.marker.fill);
134
+ }
135
+ }
136
+ /** HTML 横向柱:色类 + fill-vN 图案类(CSS repeating-linear-gradient 等效 SVG pattern)。 */
137
+ function seriesHtmlBarClass(presentation) {
138
+ if (!presentation)
139
+ return "niceeval-series-none";
140
+ if (presentation.kind === "series" && (presentation.mark === "bar" || presentation.mark === "area")) {
141
+ return seriesClassesFromFill(presentation.fill);
142
+ }
143
+ return seriesColorClass(presentation);
144
+ }
145
+ /**
146
+ * 作者在 `<Series line>` 上显式声明的 dashed/dotted **优先于** 页级 variant 的 strokeDasharray。
147
+ * 未声明时消费 LineSeriesPresentation / FillSeriesPresentation 的 strokeDasharray(空串 = 实线)。
148
+ */
149
+ function resolveStrokeDasharray(series, presentation) {
150
+ if (series.line !== undefined)
151
+ return lineDash(series.line);
152
+ if (presentation?.kind === "series" && "strokeDasharray" in presentation) {
153
+ return presentation.strokeDasharray || undefined;
154
+ }
155
+ return undefined;
156
+ }
157
+ /** SVG 柱/面:pattern fill 必须用 style 压过 `.niceeval-chart-bar { fill: var(--series) }`。 */
158
+ function seriesSvgFillStyle(presentation) {
159
+ if (presentation?.kind !== "series")
160
+ return undefined;
161
+ if (presentation.mark !== "bar" && presentation.mark !== "area")
162
+ return undefined;
163
+ if (!presentation.fill.startsWith("url("))
164
+ return undefined;
165
+ return { fill: presentation.fill };
166
+ }
167
+ function renderMarkerShape(presentation, px, py, colorClass, title) {
168
+ const marker = presentation?.kind === "series" && (presentation.mark === "scatter" || presentation.mark === "line")
169
+ ? presentation.marker
170
+ : undefined;
171
+ if (!marker) {
172
+ return (_jsx("circle", { className: cx("niceeval-chart-dot", colorClass), cx: px, cy: py, r: 4.5, children: title }));
173
+ }
174
+ // path 在 0..12 viewBox;缩放到 ~9px 并居中到 (px, py)。
175
+ return (_jsx("path", { className: cx("niceeval-chart-dot", colorClass), d: marker.path, transform: `translate(${px} ${py}) scale(0.75) translate(-6 -6)`, fill: marker.fill, stroke: "var(--panel)", strokeWidth: 1.6, children: title }));
176
+ }
177
+ function legendSwatch(presentation, mark) {
178
+ if (!presentation || presentation.kind !== "series") {
179
+ return _jsx("span", { className: cx("niceeval-chart-legend-swatch", "niceeval-series-none") });
180
+ }
181
+ // 图例方块跟 series mark 取义:柱/面用填充图案,线用 dash+marker,散点用 marker 形状。
182
+ if (mark === "bar" || mark === "area") {
183
+ const fillClass = presentation.mark === "bar" || presentation.mark === "area"
184
+ ? seriesClassesFromFill(presentation.fill)
185
+ : seriesColorClass(presentation);
186
+ return _jsx("span", { className: cx("niceeval-chart-legend-swatch", fillClass) });
187
+ }
188
+ if (mark === "line") {
189
+ // 线系图例需要 stroke + marker;presentation 可能是 line 或 scatter(by 切分后 mark 对齐)。
190
+ if (presentation.mark === "line") {
191
+ return (_jsxs("svg", { className: "niceeval-chart-legend-swatch-svg", width: "16", height: "10", "aria-hidden": "true", children: [_jsx("line", { x1: "0", y1: "5", x2: "16", y2: "5", stroke: presentation.stroke, strokeWidth: 2, strokeDasharray: presentation.strokeDasharray || undefined }), _jsx("path", { d: presentation.marker.path, transform: "translate(8 5) scale(0.55) translate(-6 -6)", fill: presentation.marker.fill })] }));
192
+ }
193
+ if (presentation.mark === "scatter") {
194
+ return (_jsxs("svg", { className: "niceeval-chart-legend-swatch-svg", width: "16", height: "10", "aria-hidden": "true", children: [_jsx("line", { x1: "0", y1: "5", x2: "16", y2: "5", stroke: presentation.marker.fill, strokeWidth: 2 }), _jsx("path", { d: presentation.marker.path, transform: "translate(8 5) scale(0.55) translate(-6 -6)", fill: presentation.marker.fill })] }));
195
+ }
196
+ // fill presentation 配 line mark 的退化:只画色块。
197
+ return _jsx("span", { className: cx("niceeval-chart-legend-swatch", seriesColorClass(presentation)) });
198
+ }
199
+ // mark === "scatter"
200
+ if (presentation.mark === "scatter" || presentation.mark === "line") {
201
+ return (_jsx("svg", { className: "niceeval-chart-legend-swatch-svg", width: "10", height: "10", "aria-hidden": "true", children: _jsx("path", { d: presentation.marker.path, transform: "translate(5 5) scale(0.7) translate(-6 -6)", fill: presentation.marker.fill }) }));
202
+ }
203
+ return _jsx("span", { className: cx("niceeval-chart-legend-swatch", seriesColorClass(presentation)) });
110
204
  }
111
205
  function metricDisplay(point, axis, meta, locale) {
112
206
  if (meta.kind === "dimension") {
@@ -129,10 +223,7 @@ function renderLegend(mapped, visible, locale, ctx) {
129
223
  : implicitIds.indexOf(series.id);
130
224
  const presentation = handle ? ctx.dimension(handle).at(index) : undefined;
131
225
  const label = presentation?.label ?? (series.label ? resolveLocalizedText(series.label, locale) : series.id);
132
- const colorClass = presentation?.colorIndex === undefined
133
- ? "niceeval-series-none"
134
- : `niceeval-series-c${presentation.colorIndex - 1}`;
135
- return (_jsx("li", { className: cx("niceeval-chart-legend-item", colorClass), children: label }, `${series.id}:${value}`));
226
+ return (_jsxs("li", { className: "niceeval-chart-legend-item", children: [legendSwatch(presentation, series.mark), label] }, `${series.id}:${value}`));
136
227
  });
137
228
  }) }));
138
229
  }
@@ -142,12 +233,13 @@ function renderHorizontalBarsWeb(mapped, visible, axes, locale, ctx, options) {
142
233
  const values = entries.map(({ point }) => point.y);
143
234
  const boundMax = axes.yMeta.kind === "metric" ? axes.yMeta.bounds?.max : undefined;
144
235
  const max = boundMax !== undefined && boundMax > 0 ? boundMax : Math.max(0, ...values);
145
- return (_jsxs("figure", { className: cx("niceeval-report", "niceeval-chart", "niceeval-chart--bars-horizontal", options.className), children: [_jsx("div", { className: "niceeval-chart-bars-heading", children: chartFieldLabel(axes.yField, axes.yMeta, locale) }), _jsx("ol", { className: "niceeval-chart-bars", children: entries.map(({ series, point }) => {
236
+ return (_jsxs("figure", { className: cx("niceeval-report", "niceeval-chart", "niceeval-chart--bars-horizontal", options.className), children: [_jsx(SeriesPatternDefs, {}), _jsx("div", { className: "niceeval-chart-bars-heading", children: chartFieldLabel(axes.yField, axes.yMeta, locale) }), _jsx("ol", { className: "niceeval-chart-bars", children: entries.map(({ series, point }) => {
146
237
  const rawLabel = point.xLabel ?? point.pointLabel;
147
238
  const label = labels.get(rawLabel) ?? rawLabel;
148
239
  const display = metricDisplay(point, "y", axes.yMeta, locale);
149
240
  const href = pointHref(point, ctx, options.pointTarget);
150
- const colorClass = seriesClass(mapped, series, point, ctx);
241
+ const presentation = seriesPresentationOf(mapped, series, point, ctx);
242
+ const colorClass = seriesHtmlBarClass(presentation);
151
243
  const ratio = max > 0 ? Math.max(0, Math.min(1, point.y / max)) : 0;
152
244
  const value = (_jsxs("span", { className: "niceeval-chart-bar-value", children: [display, point.yCell && point.yCell.samples < point.yCell.total ? (_jsxs("sup", { children: [point.yCell.samples, "/", point.yCell.total] })) : null] }));
153
245
  return (_jsxs("li", { className: "niceeval-chart-bar-row", children: [_jsx("span", { className: "niceeval-chart-bar-label", title: rawLabel, children: label }), _jsx("span", { className: "niceeval-chart-bar-track", children: _jsx("span", { className: cx("niceeval-chart-bar-fill", colorClass), style: { width: `${ratio * 100}%` }, title: `${rawLabel}\n${axes.yField}: ${display}` }) }), href ? _jsx("a", { className: "niceeval-locator", href: href, children: value }) : value] }, `${series.id}:${point.key}`));
@@ -203,16 +295,20 @@ function renderChartWeb(mapped, axes, locale, ctx, options) {
203
295
  const labelByKey = shortestUniqueLabels(allPoints.map((p) => p.pointLabel));
204
296
  const xLabel = chartFieldLabel(axes.xField, axes.xMeta, locale);
205
297
  const yLabel = chartFieldLabel(axes.yField, axes.yMeta, locale);
206
- const drawable = visible.flatMap((series) => series.points.map((point) => ({
207
- ...point,
208
- sourceSeriesId: series.id,
209
- label: labelByKey.get(point.pointLabel) ?? point.pointLabel,
210
- px: xScale.scale(point.x),
211
- py: yScale.scale(point.y),
212
- seriesClass: seriesClass(mapped, series, point, ctx),
213
- })));
298
+ const drawable = visible.flatMap((series) => series.points.map((point) => {
299
+ const presentation = seriesPresentationOf(mapped, series, point, ctx);
300
+ return {
301
+ ...point,
302
+ sourceSeriesId: series.id,
303
+ label: labelByKey.get(point.pointLabel) ?? point.pointLabel,
304
+ px: xScale.scale(point.x),
305
+ py: yScale.scale(point.y),
306
+ presentation,
307
+ seriesClass: seriesColorClass(presentation),
308
+ };
309
+ }));
214
310
  const labels = placePointLabels(drawable.map((p) => ({ cx: p.px, cy: p.py, width: p.label.length * 6.4 + 10 })), { x0: 2, y0: 2, x1: WIDTH - 2, y1: HEIGHT - 2 });
215
- return (_jsxs("figure", { className: cx("niceeval-report", "niceeval-chart", "niceeval-chart--scatter", options.className), children: [_jsxs("svg", { className: "niceeval-chart-svg", viewBox: `0 0 ${WIDTH} ${HEIGHT}`, role: "img", "aria-label": `${axes.xField} × ${axes.yField}`, children: [options.grid !== false ? (_jsxs("g", { className: "niceeval-chart-grid", children: [yScale.ticks.map((tick) => (_jsx("line", { x1: MARGIN.left, x2: MARGIN.left + PLOT_W, y1: yScale.scale(tick), y2: yScale.scale(tick) }, `gy${tick}`))), xScale.ticks.map((tick) => (_jsx("line", { y1: MARGIN.top, y2: MARGIN.top + PLOT_H, x1: xScale.scale(tick), x2: xScale.scale(tick) }, `gx${tick}`)))] })) : null, _jsx("g", { className: "niceeval-chart-axis niceeval-chart-axis-y", children: (axes.yMeta.kind === "dimension"
311
+ return (_jsxs("figure", { className: cx("niceeval-report", "niceeval-chart", "niceeval-chart--scatter", options.className), children: [_jsx(SeriesPatternDefs, {}), _jsxs("svg", { className: "niceeval-chart-svg", viewBox: `0 0 ${WIDTH} ${HEIGHT}`, role: "img", "aria-label": `${axes.xField} × ${axes.yField}`, children: [options.grid !== false ? (_jsxs("g", { className: "niceeval-chart-grid", children: [yScale.ticks.map((tick) => (_jsx("line", { x1: MARGIN.left, x2: MARGIN.left + PLOT_W, y1: yScale.scale(tick), y2: yScale.scale(tick) }, `gy${tick}`))), xScale.ticks.map((tick) => (_jsx("line", { y1: MARGIN.top, y2: MARGIN.top + PLOT_H, x1: xScale.scale(tick), x2: xScale.scale(tick) }, `gx${tick}`)))] })) : null, _jsx("g", { className: "niceeval-chart-axis niceeval-chart-axis-y", children: (axes.yMeta.kind === "dimension"
216
312
  ? [...new Set(drawable.map((point) => point.y))]
217
313
  : yScale.ticks).map((tick) => (_jsx("text", { className: "niceeval-chart-tick", x: MARGIN.left - 8, y: yScale.scale(tick) + 3, textAnchor: "end", children: axes.yMeta.kind === "dimension"
218
314
  ? drawable.find((point) => point.y === tick)?.yLabel
@@ -226,7 +322,8 @@ function renderChartWeb(mapped, axes, locale, ctx, options) {
226
322
  const seriesPoints = drawable.filter((p) => p.sourceSeriesId === series.id &&
227
323
  (series.byField === undefined || p.seriesValue === value));
228
324
  const ordered = series.connect ? [...seriesPoints].sort((a, b) => a.x - b.x) : seriesPoints;
229
- const seriesClass = ordered[0]?.seriesClass ?? "niceeval-series-none";
325
+ const seriesClassName = ordered[0]?.seriesClass ?? "niceeval-series-none";
326
+ const seriesPresentation = ordered[0]?.presentation;
230
327
  const baseline = yScale.scale(0);
231
328
  const barGroups = [...new Set(visible
232
329
  .filter((item) => item.mark === "bar")
@@ -235,11 +332,13 @@ function renderChartWeb(mapped, axes, locale, ctx, options) {
235
332
  const groupIndex = Math.max(0, barGroups.indexOf(barGroup));
236
333
  const totalBarWidth = Math.max(8, Math.min(48, PLOT_W / Math.max(1, allPoints.length)));
237
334
  const barWidth = totalBarWidth / Math.max(1, barGroups.length);
238
- return (_jsxs("g", { className: cx("niceeval-chart-series", seriesClass), "data-series": `${series.id}:${value}`, children: [series.mark === "area" && ordered.length > 1 ? (_jsx("polygon", { className: "niceeval-chart-area", points: [
335
+ const dash = resolveStrokeDasharray(series, seriesPresentation);
336
+ const areaFillStyle = seriesSvgFillStyle(seriesPresentation);
337
+ return (_jsxs("g", { className: cx("niceeval-chart-series", seriesClassName), "data-series": `${series.id}:${value}`, children: [series.mark === "area" && ordered.length > 1 ? (_jsx("polygon", { className: "niceeval-chart-area", style: areaFillStyle, points: [
239
338
  `${ordered[0].px},${baseline}`,
240
339
  ...ordered.map((point) => `${point.px},${point.py}`),
241
340
  `${ordered[ordered.length - 1].px},${baseline}`,
242
- ].join(" ") })) : null, (series.mark === "line" || series.mark === "area" || series.connect) && ordered.length > 1 ? (_jsx("polyline", { className: "niceeval-chart-line", points: ordered.map((p) => `${p.px},${p.py}`).join(" "), strokeDasharray: lineDash(series.line) })) : null, ordered.map((p) => {
341
+ ].join(" ") })) : null, (series.mark === "line" || series.mark === "area" || series.connect) && ordered.length > 1 ? (_jsx("polyline", { className: "niceeval-chart-line", points: ordered.map((p) => `${p.px},${p.py}`).join(" "), strokeDasharray: dash })) : null, ordered.map((p) => {
243
342
  const placed = labels[drawable.indexOf(p)];
244
343
  const href = pointHref(p, ctx, options.pointTarget);
245
344
  let shape;
@@ -247,10 +346,10 @@ function renderChartWeb(mapped, axes, locale, ctx, options) {
247
346
  const baseValue = stackedBarBase(visible, series, p.x);
248
347
  const baseY = yScale.scale(baseValue);
249
348
  const topY = yScale.scale(baseValue + p.y);
250
- shape = (_jsx("rect", { className: cx("niceeval-chart-bar", p.seriesClass), x: p.px - totalBarWidth / 2 + groupIndex * barWidth, y: Math.min(topY, baseY), width: barWidth, height: Math.max(1, Math.abs(baseY - topY)), children: _jsx("title", { children: `${p.pointLabel}\n${series.id}: ${metricDisplay(p, "y", axes.yMeta, locale)}` }) }));
349
+ shape = (_jsx("rect", { className: cx("niceeval-chart-bar", p.seriesClass), style: seriesSvgFillStyle(p.presentation), x: p.px - totalBarWidth / 2 + groupIndex * barWidth, y: Math.min(topY, baseY), width: barWidth, height: Math.max(1, Math.abs(baseY - topY)), children: _jsx("title", { children: `${p.pointLabel}\n${series.id}: ${metricDisplay(p, "y", axes.yMeta, locale)}` }) }));
251
350
  }
252
351
  else {
253
- shape = (_jsx("circle", { className: cx("niceeval-chart-dot", p.seriesClass), cx: p.px, cy: p.py, r: 4.5, children: _jsx("title", { children: `${p.pointLabel}\n${axes.xField}: ${metricDisplay(p, "x", axes.xMeta, locale)}\n${axes.yField}: ${metricDisplay(p, "y", axes.yMeta, locale)}` }) }));
352
+ shape = renderMarkerShape(p.presentation, p.px, p.py, p.seriesClass, _jsx("title", { children: `${p.pointLabel}\n${axes.xField}: ${metricDisplay(p, "x", axes.xMeta, locale)}\n${axes.yField}: ${metricDisplay(p, "y", axes.yMeta, locale)}` }));
254
353
  }
255
354
  return (_jsxs("g", { className: "niceeval-chart-point", children: [href ? _jsx("a", { href: href, children: shape }) : shape, series.mark === "scatter" && placed ? (_jsx("text", { className: "niceeval-chart-point-label", x: placed.x, y: placed.y, textAnchor: placed.anchor, children: p.label })) : null] }, p.key));
256
355
  })] }, `${series.id}:${value}`));
@@ -480,7 +579,8 @@ export const Chart = defineComponent({
480
579
  continue;
481
580
  decls[spec.by] = {
482
581
  dimension: spec.by,
483
- encoding: { kind: "series", mark: spec.mark === "area" ? "line" : spec.mark },
582
+ // area FillSeriesPresentation(填充图案),不再折叠成 line
583
+ encoding: { kind: "series", mark: spec.mark },
484
584
  values,
485
585
  };
486
586
  }
@@ -1,4 +1,4 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { COMPONENT_RAW_CHILDREN, COMPONENT_ROLE, defineComponent, } from "./tree.js";
3
3
  import { hrefForLocator } from "../components/shared.js";
4
4
  import { localeText, resolveLocalizedText } from "../model/locale.js";
@@ -586,8 +586,9 @@ function renderCellWeb(cell, ctx) {
586
586
  return (_jsxs("span", { className: "niceeval-verdict-tally", children: [parts.map((kind) => (_jsxs("span", { className: `niceeval-verdict-${kind}`, children: [cell.counts[kind], " ", localeText(ctx.locale, `verdict.${kind === "skipped" ? "skipped" : kind}`)] }, kind))), parts.length === 0 ? _jsx("span", { className: "niceeval-missing", children: MISSING_MARK }) : null] }));
587
587
  }
588
588
  const verdict = cell.verdict ?? "skipped";
589
+ const stale = cell.staleSinceMs !== undefined;
589
590
  // 判定符走 verdictMark 单源,与 locator 格同一张表(errored 是 `!`,不并到 `✗`)。
590
- return (_jsxs("span", { className: `niceeval-verdict niceeval-verdict-${verdict}`, children: [verdictMark(verdict === "skipped" ? "skipped" : verdict), " ", localeText(ctx.locale, `verdict.${verdict === "skipped" ? "skipped" : verdict}`)] }));
591
+ return (_jsxs("span", { className: cx("niceeval-verdict", `niceeval-verdict-${verdict}`, stale ? "niceeval-stale" : undefined), title: stale ? localeText(ctx.locale, "experimentList.historicalTooltip") : undefined, children: [verdictMark(verdict === "skipped" ? "skipped" : verdict), !cell.bare ? _jsxs(_Fragment, { children: [" ", localeText(ctx.locale, `verdict.${verdict === "skipped" ? "skipped" : verdict}`)] }) : null, stale ? _jsx("span", { className: "niceeval-stale-distance", children: formatTimeDistance(cell.staleSinceMs, ctx.locale) }) : null] }));
591
592
  }
592
593
  case "metric":
593
594
  return (_jsx(MetricCellView, { cell: cell.metric, href: ctx.showMeasureRefs === false ? undefined : ctx.href, locale: ctx.locale }));
@@ -92,8 +92,9 @@ export interface WebContext {
92
92
  /** chrome 文案的 locale;官方组件渲染面经上下文读取,宿主外默认 "en"。 */
93
93
  locale: ReportLocale;
94
94
  /**
95
- * 取本组件 `dimensions()` 声明的某个句柄在这一页的呈现面( `seriesSlot` / 色板下标 /
96
- * 形状变体)。查别的组件的句柄或没声明的句柄抛 `UndeclaredDimensionValueError`。
95
+ * 取本组件 `dimensions()` 声明的某个句柄在这一页的呈现面(label / color / series
96
+ * fill·stroke·marker 等可直接使用的值)。查别的组件的句柄或没声明的句柄抛
97
+ * `UndeclaredDimensionValueError`。
97
98
  */
98
99
  dimension(handle: string): PresentedDimension;
99
100
  }
@@ -152,6 +153,8 @@ export type ReportComponent<P extends object> = ((props: P) => ReactNode) & {
152
153
  */
153
154
  export interface PageDimensions {
154
155
  dimension(props: object, handle: string): PresentedDimension;
156
+ /** 维度 name → 值 → seriesSlot;测试与调试观察槽位分配,组件渲染不读槽号。 */
157
+ readonly slotsByDimension: ReadonlyMap<string, ReadonlyMap<string, number>>;
155
158
  }
156
159
  /** 宿主渲染前把这一页的分配结果挂到渲染上下文上(text 与 web 同一条通道)。 */
157
160
  export declare function withPageDimensions<C extends object>(ctx: C, plan: PageDimensions | undefined): C;
@@ -142,6 +142,7 @@ export function collectPageDimensions(node, pins = {}, face = "web") {
142
142
  visit(node);
143
143
  const plan = allocatePageDimensions(handles, pins, { face });
144
144
  return {
145
+ slotsByDimension: plan.slotsByDimension,
145
146
  dimension(props, handle) {
146
147
  const entry = byProps.get(props);
147
148
  const key = entry?.keys.get(handle);
@@ -2,7 +2,7 @@ export { aggregate, agent, costUSD, dedupeLocators, durationMs, evalId, evidence
2
2
  export type { AggregateRow, AggregationSubject, Calculation, EvidenceRow, GroupFunction, MetricBasis, MetricFormat, MetricValue, Reducer, RollupOptions, } from "./model/calculation.ts";
3
3
  export { toAttemptAssertions, toAttemptFacts, toAttemptFixPrompt, toAttemptListRows, toAttemptNotices, toAttemptRows, toAttemptSource, toAttemptSummary, toAttemptUsage, toConversationTurns, toDiffFiles, toEvalRows, toExperimentDetails, toExperimentRows, toHeroData, toRunNotices, toSampleFixPrompt, toSampleNotices, toSummaryItems, toTimelineNodes, toTraceNodes, } from "./model/conversions.ts";
4
4
  export { presentDimension, shortestUniqueLabels } from "./presentation.ts";
5
- export type { DimensionDeclaration, DimensionEncoding, PresentedDimension } from "./presentation.ts";
5
+ export type { ColorPresentation, DimensionDeclaration, DimensionEncoding, DimensionPresentation, FillSeriesPresentation, LabelPresentation, LineSeriesPresentation, PresentedDimension, ScatterSeriesPresentation, } from "./presentation.ts";
6
6
  export { flag, label, numericFlag, numericLabel, numericRunConfig, runConfig } from "./model/flag.ts";
7
7
  export { evaluationKindComposition } from "./model/evaluation-kind.ts";
8
8
  export { annotatedSourceResult, attemptDetailsResult, comparisonResult, conversationResult, diffResult, historyResult, stabilityResult, standardOverviewResult, timingResult, usageResult, } from "./tasks.ts";
@@ -1,6 +1,6 @@
1
1
  import type { AttemptHandle, SampleCoverage, SampleIssue, Run } from "../../record/types.ts";
2
2
  import { type AttemptLocator } from "../../record/locator.ts";
3
- import type { Aggregator, DimensionInput, AttemptMetric, MetricValue, MetricColumn, NumericAxis, ReportInput, SeriesInput } from "./types.ts";
3
+ import type { Aggregator, DimensionInput, AttemptMetric, MetricValue, MetricColumn, NumericAxis, ReportInput, SeriesInput, StaleConclusionReference } from "./types.ts";
4
4
  import { type LocalizedText } from "./locale.ts";
5
5
  import { evalPrefixPredicate } from "../../shared/aggregate.ts";
6
6
  import type { JsonValue } from "../../shared/types.ts";
@@ -41,6 +41,15 @@ export declare function evalIdOf(item: Item): string;
41
41
  * 来源可能有多个,水位基准是其中 startedAt 最新的一个。
42
42
  */
43
43
  export declare function historicalOf(item: Item): boolean;
44
+ /** 一个 ISO 时刻距渲染时刻(`Date.now()`)的毫秒数,恒不小于 0。 */
45
+ export declare function msSince(iso: string): number;
46
+ /**
47
+ * 覆盖缺口 / 对照矩阵缺席格的「过期结论」参考(docs/feature/reports/components/summaries/
48
+ * experiment-table.md「覆盖缺口的两档占位行」、show/compare.md 同一套口径):候选已经是与
49
+ * 当前基准 configHash 不可比的那些,取其中最近一条;两个消费方(entity-lists 的占位行、
50
+ * DeltaTable 的缺席格)共用同一份「取最新」判据,不各自实现一遍。
51
+ */
52
+ export declare function staleReferenceOf(candidates: readonly AttemptHandle[]): StaleConclusionReference | undefined;
44
53
  /** 快照键:"<experimentId> @ <startedAt>"("run" 维度与手挑快照数组的对比用)。 */
45
54
  export declare function snapshotKeyOf(run: Run): string;
46
55
  /** 一组 Item 的 eval 全身份键:experimentId + eval id(聚合中的题级身份始终是这一对)。 */