niceeval 0.11.4-canary.20 → 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 (181) hide show
  1. package/INDEX.md +1 -0
  2. package/dist/i18n/en.d.ts +9 -1
  3. package/dist/i18n/en.js +11 -2
  4. package/dist/i18n/zh-CN.d.ts +10 -2
  5. package/dist/i18n/zh-CN.js +11 -2
  6. package/dist/record/attempt-evidence.d.ts +1 -1
  7. package/dist/record/open.js +25 -0
  8. package/dist/report/assets/series-encoding.d.ts +36 -0
  9. package/dist/report/assets/series-encoding.js +104 -0
  10. package/dist/report/built-in/result-components.js +12 -2
  11. package/dist/report/components/attempt-detail/compute.d.ts +8 -1
  12. package/dist/report/components/attempt-detail/compute.js +19 -1
  13. package/dist/report/components/attempt-detail/index.js +25 -3
  14. package/dist/report/components/attempt-detail/validate.js +3 -2
  15. package/dist/report/components/entity-lists/compute.d.ts +6 -0
  16. package/dist/report/components/entity-lists/compute.js +55 -8
  17. package/dist/report/components/entity-lists/content.d.ts +2 -0
  18. package/dist/report/components/entity-lists/content.js +59 -6
  19. package/dist/report/components/entity-lists/index.d.ts +21 -1
  20. package/dist/report/components/entity-lists/index.js +40 -7
  21. package/dist/report/components/entity-lists/validate.js +21 -0
  22. package/dist/report/components/fixtures.js +2 -0
  23. package/dist/report/definition/cell.d.ts +22 -0
  24. package/dist/report/definition/cell.js +23 -9
  25. package/dist/report/definition/primitives/chart.js +125 -25
  26. package/dist/report/definition/primitives/conversation.d.ts +1 -1
  27. package/dist/report/definition/primitives/conversation.js +5 -3
  28. package/dist/report/definition/primitives.js +32 -7
  29. package/dist/report/definition/tree.d.ts +5 -2
  30. package/dist/report/definition/tree.js +1 -0
  31. package/dist/report/index.d.ts +5 -5
  32. package/dist/report/index.js +2 -2
  33. package/dist/report/model/aggregate.d.ts +14 -1
  34. package/dist/report/model/aggregate.js +34 -2
  35. package/dist/report/model/conversions.d.ts +1 -0
  36. package/dist/report/model/conversions.js +4 -1
  37. package/dist/report/model/format.d.ts +6 -5
  38. package/dist/report/model/format.js +30 -9
  39. package/dist/report/model/locale.d.ts +19 -3
  40. package/dist/report/model/locale.js +31 -5
  41. package/dist/report/model/types.d.ts +41 -4
  42. package/dist/report/presentation.d.ts +53 -6
  43. package/dist/report/presentation.js +40 -5
  44. package/dist/report/react/index.d.ts +3 -3
  45. package/dist/report/react/index.js +1 -1
  46. package/dist/report/slices/compute.js +52 -4
  47. package/dist/report/slices/content.d.ts +2 -1
  48. package/dist/report/slices/content.js +48 -15
  49. package/dist/report/slices/validate.js +23 -0
  50. package/dist/report/tasks.d.ts +7 -1
  51. package/dist/report/tasks.js +5 -3
  52. package/dist/runner/types.d.ts +30 -10
  53. package/dist/sample/index.d.ts +0 -9
  54. package/dist/sample/index.js +0 -18
  55. package/dist/sandbox/backend.d.ts +1 -1
  56. package/dist/sandbox/commands.d.ts +1 -1
  57. package/dist/sandbox/compose.d.ts +2 -1
  58. package/dist/sandbox/docker.d.ts +28 -7
  59. package/dist/sandbox/e2b.d.ts +11 -0
  60. package/dist/sandbox/identity.d.ts +6 -1
  61. package/dist/sandbox/layer.d.ts +36 -3
  62. package/dist/sandbox/local.d.ts +9 -0
  63. package/dist/sandbox/types.d.ts +14 -8
  64. package/dist/sandbox/vercel.d.ts +9 -0
  65. package/docs-site/zh/reference/builtin-agents.mdx +2 -0
  66. package/docs-site/zh/reference/cli.mdx +1 -0
  67. package/docs-site/zh/reference/define-agent.mdx +13 -9
  68. package/docs-site/zh/reference/define-config.mdx +8 -0
  69. package/docs-site/zh/reference/official-adapters.mdx +1 -1
  70. package/docs-site/zh/reference/results-data.mdx +1 -1
  71. package/docs-site/zh/tutorials/deploy-report-site.mdx +157 -0
  72. package/docs-site/zh/tutorials/publish-report.mdx +1 -1
  73. package/docs-site/zh/tutorials/rerun-and-cache.mdx +1 -1
  74. package/docs-site/zh/tutorials/sandbox-providers.mdx +19 -4
  75. package/package.json +1 -1
  76. package/src/agents/codex-env.test.ts +8 -1
  77. package/src/agents/codex.ts +8 -0
  78. package/src/agents/coding-cli-versions.ts +22 -6
  79. package/src/cli.ts +23 -1
  80. package/src/i18n/en.ts +14 -3
  81. package/src/i18n/zh-CN.ts +13 -2
  82. package/src/record/attempt-evidence.ts +1 -1
  83. package/src/record/open.ts +21 -0
  84. package/src/record/results.test.ts +74 -13
  85. package/src/record/truncate.ts +32 -7
  86. package/src/record/writer.ts +39 -8
  87. package/src/report/assets/series-encoding.tsx +177 -0
  88. package/src/report/assets/styles.css +97 -5
  89. package/src/report/built-in/result-components.tsx +23 -2
  90. package/src/report/components/attempt-detail/attempt-components.test.tsx +41 -0
  91. package/src/report/components/attempt-detail/compute.ts +20 -2
  92. package/src/report/components/attempt-detail/index.tsx +32 -2
  93. package/src/report/components/attempt-detail/validate.test.ts +1 -0
  94. package/src/report/components/attempt-detail/validate.tsx +3 -1
  95. package/src/report/components/compute.test.ts +196 -0
  96. package/src/report/components/entity-lists/compute.ts +73 -7
  97. package/src/report/components/entity-lists/content.test.ts +14 -8
  98. package/src/report/components/entity-lists/content.ts +63 -6
  99. package/src/report/components/entity-lists/index.tsx +93 -8
  100. package/src/report/components/entity-lists/validate.test.ts +9 -0
  101. package/src/report/components/entity-lists/validate.ts +16 -0
  102. package/src/report/components/fixtures.ts +2 -0
  103. package/src/report/definition/cell.ts +46 -11
  104. package/src/report/definition/conversation.test.tsx +5 -1
  105. package/src/report/definition/primitives/chart.tsx +197 -32
  106. package/src/report/definition/primitives/conversation.tsx +7 -4
  107. package/src/report/definition/primitives.tsx +81 -9
  108. package/src/report/definition/tree.ts +6 -2
  109. package/src/report/index.ts +15 -2
  110. package/src/report/model/aggregate.ts +40 -2
  111. package/src/report/model/conversions.ts +5 -0
  112. package/src/report/model/format.ts +32 -9
  113. package/src/report/model/locale.ts +35 -5
  114. package/src/report/model/presentation.test.ts +23 -1
  115. package/src/report/model/types.ts +40 -4
  116. package/src/report/presentation.test.tsx +179 -19
  117. package/src/report/presentation.ts +114 -11
  118. package/src/report/react/index.tsx +13 -2
  119. package/src/report/runtime/dual-render.test.tsx +38 -2
  120. package/src/report/slices/compute.ts +59 -4
  121. package/src/report/slices/content.ts +55 -16
  122. package/src/report/slices/delta-table.test.ts +138 -3
  123. package/src/report/slices/validate.test.ts +29 -0
  124. package/src/report/slices/validate.ts +16 -0
  125. package/src/report/tasks.ts +12 -2
  126. package/src/runner/accept.test.ts +328 -10
  127. package/src/runner/accept.ts +239 -72
  128. package/src/runner/attempt.test.ts +25 -13
  129. package/src/runner/attempt.ts +9 -9
  130. package/src/runner/config-identity.test.ts +25 -0
  131. package/src/runner/config-identity.ts +6 -7
  132. package/src/runner/feedback/coordinator.test.ts +51 -0
  133. package/src/runner/feedback/coordinator.ts +23 -0
  134. package/src/runner/feedback/failure.test.ts +15 -0
  135. package/src/runner/feedback/failure.ts +7 -0
  136. package/src/runner/feedback/human.test.ts +548 -27
  137. package/src/runner/feedback/human.ts +321 -47
  138. package/src/runner/feedback/index.ts +10 -0
  139. package/src/runner/feedback/input-guard.test.ts +251 -0
  140. package/src/runner/feedback/input-guard.ts +171 -0
  141. package/src/runner/feedback/reducer.ts +1 -0
  142. package/src/runner/fingerprint.test.ts +24 -0
  143. package/src/runner/gate-lease.test.ts +38 -14
  144. package/src/runner/ledger.test.ts +10 -7
  145. package/src/runner/ledger.ts +9 -2
  146. package/src/runner/lock.test.ts +40 -16
  147. package/src/runner/manifest.ts +8 -9
  148. package/src/runner/reporters/artifacts.ts +25 -3
  149. package/src/runner/run.test.ts +61 -0
  150. package/src/runner/run.ts +34 -9
  151. package/src/runner/sandbox-pool.test.ts +216 -12
  152. package/src/runner/sandbox-pool.ts +20 -2
  153. package/src/runner/types.ts +30 -10
  154. package/src/sample/index.ts +0 -16
  155. package/src/sample/sample.test.ts +78 -0
  156. package/src/sandbox/backend.ts +1 -1
  157. package/src/sandbox/commands.ts +4 -7
  158. package/src/sandbox/compose.test.ts +26 -1
  159. package/src/sandbox/compose.ts +7 -3
  160. package/src/sandbox/docker.test.ts +66 -1
  161. package/src/sandbox/docker.ts +89 -65
  162. package/src/sandbox/dockerfile-build.ts +4 -1
  163. package/src/sandbox/e2b-agent-template.test.ts +18 -1
  164. package/src/sandbox/e2b-agent-template.ts +22 -6
  165. package/src/sandbox/e2b.test.ts +64 -0
  166. package/src/sandbox/e2b.ts +27 -5
  167. package/src/sandbox/identity.ts +7 -2
  168. package/src/sandbox/layer.test.ts +23 -4
  169. package/src/sandbox/layer.ts +127 -43
  170. package/src/sandbox/link.test.ts +5 -2
  171. package/src/sandbox/local.test.ts +8 -8
  172. package/src/sandbox/local.ts +28 -6
  173. package/src/sandbox/operations.test.ts +4 -4
  174. package/src/sandbox/operations.ts +5 -5
  175. package/src/sandbox/runtime.ts +18 -3
  176. package/src/sandbox/types.ts +14 -8
  177. package/src/sandbox/vercel.test.ts +58 -3
  178. package/src/sandbox/vercel.ts +38 -6
  179. package/src/show/index.ts +11 -30
  180. package/src/show/render.test.ts +23 -1
  181. package/src/show/render.ts +6 -3
package/src/i18n/zh-CN.ts CHANGED
@@ -281,7 +281,7 @@ export const zhCN = {
281
281
  "cli.experimentGroup": "路径",
282
282
  "cli.fallbackCleanupTimeout": "\ngraceful 清理超时,强制清理沙箱…\n",
283
283
  "cli.forceCleanupExit": "\n强制清理沙箱并退出…\n",
284
- "cli.init.done": "已就绪:evals/、niceeval.config.ts,以及 AGENTS.md 里的 niceeval agent 指引区块(指向 node_modules/niceeval/docs-site/zh)。\n",
284
+ "cli.init.done": "已就绪:evals/、niceeval.config.ts,以及 AGENTS.md 里的 niceeval agent 指引区块(要求 coding agent 写 eval 前先读随包文档)。\n",
285
285
  "cli.init.esmHint":
286
286
  "提示:项目 package.json 没有 \"type\": \"module\"。niceeval 两种形态都能跑,但 CommonJS 形态下 config / eval 文件用不了顶层 await——建议在 package.json 加上 \"type\": \"module\"。\n",
287
287
  "cli.interruptCleanup": "\n收到中断,正在清理沙箱容器…(再按一次强制清理并退出)\n",
@@ -300,6 +300,9 @@ export const zhCN = {
300
300
  "cli.view.noResults": "{{root}} 下没有结果。先 `niceeval exp` 跑一轮,再 `niceeval view`。\n",
301
301
  "cli.view.incompatibleForeign": "{{dir}}: 由 {{name}} {{version}} 写入(schemaVersion {{schemaVersion}}),当前 CLI 只读 schemaVersion {{supported}}。\n请用写出它的那个工具查看这份报告。\n",
302
302
  "cli.view.urls": "niceeval view 可在浏览器打开:\n{{urls}}\n",
303
+ "codex.envPathManaged":
304
+ "codexAgent config.env.PATH 不支持:PATH 是 Sandbox 受管变量,静默丢弃或覆盖它会让 hooks 与子进程读到错误的可执行文件,且零报错。" +
305
+ "改用 Sandbox factory 的 pathPrepend 选项前置目录(见 docs/feature/sandbox/library.md)。",
303
306
  "context.capabilityMissing":
304
307
  "agent \"{{agent}}\" 不是沙箱型(defineSandboxAgent 构造),t.{{method}} 这类断言只有沙箱型 agent 可用。换用 defineSandboxAgent 构造的 agent,或去掉这条断言。\n" +
305
308
  " 文档:node_modules/niceeval/docs-site/zh/tutorials/sandbox-agent.mdx",
@@ -338,11 +341,16 @@ export const zhCN = {
338
341
  "共 {{total}} · 复用 {{reused}} · 运行中 {{running}} · 排队 {{queued}} · 通过 {{passed}} · 失败 {{failed}} · 出错 {{errored}} · 跳过 {{skipped}}",
339
342
  "feedback.human.diffHint": "Diff: niceeval show {{locator}} --diff",
340
343
  "feedback.human.evalHint": "Eval: niceeval show {{locator}} --source",
344
+ "feedback.human.exampleLocator": "例如 {{locator}}",
345
+ "feedback.human.failureFacts": "facts ×{{count}}",
341
346
  "feedback.human.failuresHeader": "FAILURES",
347
+ "feedback.human.failuresSoFar": "已发生 {{count}} 条",
348
+ "feedback.human.failuresTotalKinds": "共 {{total}} 条 · {{kinds}} 种形态",
342
349
  "feedback.human.heartbeat": "已运行 {{elapsed}} · {{counts}}",
343
350
  "feedback.human.inspect": "Inspect: niceeval show {{locator}}",
344
351
  "feedback.human.keptSandboxesHeader": "KEPT SANDBOXES",
345
352
  "feedback.human.moreActive": "… 还有 {{count}} 项运行中",
353
+ "feedback.human.moreFailureKinds": "还有 {{count}} 种形态 — niceeval view",
346
354
  "feedback.human.nextHeader": "NEXT",
347
355
  "feedback.human.plan": "{{total}} 个 attempt · {{evals}} 个 eval × {{configs}} 个配置 · 并发 {{concurrency}}",
348
356
  "feedback.human.planHeader": "PLAN",
@@ -359,6 +367,7 @@ export const zhCN = {
359
367
  "feedback.human.summaryAllReusedLine": "{{passed}} 通过 · {{failed}} 失败 · {{errored}} 出错 (全部 {{reused}} 条复用)",
360
368
  "feedback.human.suppressedFailures": "… 还有 {{count}} 条失败被折叠",
361
369
  "feedback.human.trace": "Trace: niceeval show {{locator}} --execution",
370
+ "feedback.human.warningsHeader": "WARNINGS",
362
371
  "feedback.phase.agentSetup": "agent 预置",
363
372
  "feedback.phase.agentEnsure": "正在准备 Agent",
364
373
  "feedback.phase.stateLoad": "载入实验状态",
@@ -413,7 +422,7 @@ export const zhCN = {
413
422
  "local.dirNotWritable": "本地沙箱目录不可写:{{dir}}({{message}})",
414
423
  "local.notARepo":
415
424
  "当前目录(及其上级目录)都不在任何 git 仓库内,localSandbox() 没有确定的 workdir 可用。cd 进要跑的仓库再执行,或显式传 localSandbox({ dir: \"/path/to/repo\" })。",
416
- "local.rootUnsupported": "本地 provider 不支持 { root: true }——niceeval 不会在你的机器上提权。需要 root 的步骤请改用容器 provider(docker / e2b / vercel)。",
425
+ "local.userUnsupported": "本地 provider 不支持切换执行身份,收到 { user: \"{{user}}\" }——niceeval 不会在你的机器上提权或换身份。需要别的身份请改用容器 provider(docker / e2b / vercel)。",
417
426
  "report.assertionThreshold": " (得分 {{score}} < {{threshold}})",
418
427
  "report.error": "错误",
419
428
  "report.errored": "错误",
@@ -516,6 +525,8 @@ export const zhCN = {
516
525
  "vercel.fileNotFound": "File not found: {{path}}",
517
526
  "vercel.rotateFailed": "[VercelSandbox] session rotate failed ({{seconds}}s): {{error}}",
518
527
  "vercel.rotated": "[VercelSandbox] session rotated after {{seconds}}s → {{sessionId}}",
528
+ "vercel.userUnsupported":
529
+ "Vercel Sandbox provider 的命令级 user 只认 { user: \"root\" }(映射 sudo: true),收到 { user: \"{{user}}\" }。需要别的身份请改用容器 provider(docker / e2b)。",
519
530
  } as const;
520
531
 
521
532
  export type MessageKey = keyof typeof zhCN;
@@ -91,7 +91,7 @@ export interface AttemptEvidence {
91
91
  diff: DiffData | null;
92
92
  /** OTel spans(`--timing` 把 turn 节点下的 agent/model/tool spans 挂回时间树);没有 trace 就是 null。 */
93
93
  trace: import("../types.ts").TraceSpan[] | null;
94
- /** 非零 Sandbox 命令的 stdout/stderr 证据(`--execution` 的独立命令卡);原样透传 attempt.commands()。 */
94
+ /** Sandbox 命令(成功与非零退出都记)的 stdout/stderr 证据(`--execution` 的独立命令卡);原样透传 attempt.commands()。 */
95
95
  commands: readonly CommandExitEvidence[] | null;
96
96
  artifactPaths: EvidencePaths;
97
97
  capabilities: AttemptEvidenceCapabilities;
@@ -505,9 +505,30 @@ async function readSnapshotDir(dir: string, meta: RunMeta, state: ScanState): Pr
505
505
  ev.attempts.sort((a, b) => a.result.attempt - b.result.attempt || (a.result.startedAt ?? "").localeCompare(b.result.startedAt ?? ""));
506
506
  }
507
507
  run.attempts = run.evals.flatMap((ev) => ev.attempts);
508
+ // 回退推导:run.json 缺 configHash(旧存量落盘,exp 写入面在这条修法之前从不写它)时,
509
+ // 若该快照全部 attempt 的 result.configHash 一致,取之为 Run 的 configHash——让存量记录
510
+ // 不重跑即痊愈。任一 attempt 缺失或值不一致就如实保持 undefined,不猜一个可能错的值
511
+ // (见 docs/feature/record/library.md「configHash:配置身份只算一次」)。
512
+ if (run.configHash === undefined) {
513
+ const derived = deriveConfigHashFromAttempts(run.attempts);
514
+ if (derived !== undefined) run.configHash = derived;
515
+ }
508
516
  return run;
509
517
  }
510
518
 
519
+ /** `readSnapshotDir` 的 configHash 回退推导:全部 attempt 都带且相等才取用,否则 undefined。 */
520
+ function deriveConfigHashFromAttempts(attempts: readonly AttemptHandle[]): string | undefined {
521
+ if (attempts.length === 0) return undefined;
522
+ let hash: string | undefined;
523
+ for (const attempt of attempts) {
524
+ const value = attempt.result.configHash;
525
+ if (value === undefined) return undefined;
526
+ if (hash === undefined) hash = value;
527
+ else if (hash !== value) return undefined;
528
+ }
529
+ return hash;
530
+ }
531
+
511
532
  function makeAttempt(run: Run, snapshotDir: string, attemptDir: string, record: EvalResult): AttemptHandle {
512
533
  // 候选 artifact 目录:本 attempt 目录为主;--resume 携带条目的 artifact 留在原快照里,
513
534
  // artifactBase(相对结果根 = 快照目录的上两级)指向那里,作为回退。
@@ -207,6 +207,38 @@ describe("openRecord · 实验 → 快照 → eval → attempt 分层", () => {
207
207
  });
208
208
  });
209
209
 
210
+ describe("openRecord · configHash 回退推导", () => {
211
+ // cases: docs/engineering/testing/unit/record.md「configHash 读取面回退推导」
212
+ // bug: memory/exp-runjson-missing-confighash-breaks-current-sample.md
213
+ it("run.json 缺 configHash 时,全部 attempt 的 result.configHash 一致才回退推导为 Run 的值;不一致则保持缺失", async () => {
214
+ const root = await makeRoot();
215
+ // meta() 不设置 configHash——模拟 exp 写入面在这条修法之前遗留的存量落盘。
216
+ const consistentDir = await writeSnapshot(
217
+ root,
218
+ "compare_a",
219
+ "2026-07-01T08-00-00-000Z-a1b2",
220
+ meta({ experimentId: "compare/a", agent: "codex", startedAt: "2026-07-01T08:00:00.000Z" }),
221
+ );
222
+ await writeResultFile(consistentDir, "q1/a1", record({ id: "q1", attempt: 1, configHash: "same-hash" }));
223
+ await writeResultFile(consistentDir, "q2/a1", record({ id: "q2", attempt: 1, configHash: "same-hash" }));
224
+
225
+ const inconsistentDir = await writeSnapshot(
226
+ root,
227
+ "compare_b",
228
+ "2026-07-01T08-00-00-000Z-c3d4",
229
+ meta({ experimentId: "compare/b", agent: "codex", startedAt: "2026-07-01T08:00:00.000Z" }),
230
+ );
231
+ await writeResultFile(inconsistentDir, "q1/a1", record({ id: "q1", attempt: 1, configHash: "hash-a" }));
232
+ await writeResultFile(inconsistentDir, "q2/a1", record({ id: "q2", attempt: 1, configHash: "hash-b" }));
233
+
234
+ const results = await openRecord(root);
235
+ const a = results.experiments.find((e) => e.id === "compare/a")!;
236
+ const b = results.experiments.find((e) => e.id === "compare/b")!;
237
+ expect(a.latestRun.configHash).toBe("same-hash");
238
+ expect(b.latestRun.configHash).toBeUndefined();
239
+ });
240
+ });
241
+
210
242
  describe("record · migratedFrom", () => {
211
243
  it("读写保留旧 opaque carryEpoch 的迁移来源", async () => {
212
244
  const root = await makeRoot();
@@ -524,6 +556,34 @@ describe("createWriter", () => {
524
556
  expect(JSON.parse(await readFile(join(snap.dir, "run.json"), "utf-8")).completedAt).toBeUndefined();
525
557
  });
526
558
 
559
+ // cases: docs/engineering/testing/unit/record.md「configHash 写入面」
560
+ // bug: memory/exp-runjson-missing-confighash-breaks-current-sample.md
561
+ it("configHash:writeAttemptFor 的隐式声明按 WriterOptions.configHashes 取值(exp 路径不必显式声明)", async () => {
562
+ const root = await makeRoot();
563
+ const writer = createWriter(root, {
564
+ producer: { name: "niceeval", version: "0.12.0" },
565
+ configHashes: new Map([["compare/a", "cfg-hash-v1"]]),
566
+ });
567
+ await writer.writeAttemptFor({
568
+ id: "q1",
569
+ experimentId: "compare/a",
570
+ agent: "codex",
571
+ verdict: "passed",
572
+ attempt: 0,
573
+ startedAt: "2026-07-01T08:00:00.000Z",
574
+ durationMs: 10,
575
+ assertions: [],
576
+ });
577
+ await finishAll(writer);
578
+
579
+ const dir = writer.snapshotDirs().find((s) => s.experimentId === "compare/a")!.dir;
580
+ const meta = JSON.parse(await readFile(join(dir, "run.json"), "utf-8"));
581
+ expect(meta.configHash).toBe("cfg-hash-v1");
582
+
583
+ const results = await openRecord(root);
584
+ expect(results.experiments[0]!.latestRun.configHash).toBe("cfg-hash-v1");
585
+ });
586
+
527
587
  it("没有清单的 Run 不生成 manifests.json(读取面如实为缺失,不合成一份空清单)", async () => {
528
588
  const root = await makeRoot();
529
589
  const writer = createWriter(root, { producer: { name: "niceeval", version: "0.12.0" } });
@@ -611,8 +671,8 @@ describe("createWriter", () => {
611
671
  expect(JSON.parse(await readFile(copied, "utf-8"))).toEqual(manifest);
612
672
  });
613
673
 
614
- // cases: docs/engineering/testing/unit/record.md「Usage、facts 与失败命令证据落盘」
615
- it("commands:落成 commands.json(不内联进 result.json),artifacts 含 commands;懒加载读回原样往返;没有非零命令的 attempt 恒 null;publish 缺省携带", async () => {
674
+ // cases: docs/engineering/testing/unit/record.md「命令证据落盘」
675
+ it("commands:落成 commands.json(不内联进 result.json),artifacts 含 commands;懒加载读回原样往返;没有 Sandbox 命令的 attempt 恒 null;publish 缺省携带", async () => {
616
676
  const root = await makeRoot();
617
677
  const writer = createWriter(root, { producer: { name: "niceeval", version: "0.12.0" } });
618
678
  const evidence = [
@@ -657,18 +717,19 @@ describe("createWriter", () => {
657
717
  expect(JSON.parse(await readFile(copied, "utf-8"))).toEqual(evidence);
658
718
  });
659
719
 
660
- it("commands:超 256 KiB 的失败输出不参与逐值截断,全量原样往返(起因在前段、summary 在尾部两端都要在)", async () => {
661
- const { ARTIFACT_VALUE_MAX_BYTES } = await import("./truncate.ts");
720
+ it("commands:stdout/stderr 各自超 64 KiB 时独立截断并打结构化 truncated 标记,未超限的流原样往返", async () => {
721
+ const { COMMAND_STREAM_MAX_BYTES } = await import("./truncate.ts");
662
722
  const root = await makeRoot();
663
723
  const writer = createWriter(root, { producer: { name: "niceeval", version: "0.12.0" } });
664
- // 真实形状:头部是失败起因,中段是几百 KB 噪声,尾部是 runner 的 summary——截哪一端都毁掉另一半。
665
- const hugeStdout = `E assert 429 == 200\n${"collecting …\n".repeat(Math.ceil(ARTIFACT_VALUE_MAX_BYTES / 13))}2 failed, 14 passed in 3.41s\n`;
666
- const hugeStderr = `Prepared 5 packages\n${"e".repeat(ARTIFACT_VALUE_MAX_BYTES + 500)}\nInstalled 5 packages`;
667
- expect(Buffer.byteLength(hugeStdout, "utf-8")).toBeGreaterThan(ARTIFACT_VALUE_MAX_BYTES);
724
+ // 真实形状:stdout 超限、stderr 未超限——两条流各自独立判定,不因一条超限就一起截。
725
+ const hugeStdout = `E assert 429 == 200\n${"collecting …\n".repeat(Math.ceil(COMMAND_STREAM_MAX_BYTES / 13))}2 failed, 14 passed in 3.41s\n`;
726
+ const shortStderr = "Prepared 5 packages\nInstalled 5 packages";
727
+ const hugeStdoutBytes = Buffer.byteLength(hugeStdout, "utf-8");
728
+ expect(hugeStdoutBytes).toBeGreaterThan(COMMAND_STREAM_MAX_BYTES);
668
729
 
669
730
  const snap = await writer.run({ experimentId: "huge/output", agent: "bub", startedAt: "2026-07-11T08:00:00.000Z" });
670
731
  const evidence = [
671
- { timingNodeId: "n1", phase: "eval.run" as const, display: "uv run pytest", exitCode: 1, checked: true, stdout: hugeStdout, stderr: hugeStderr },
732
+ { timingNodeId: "n1", phase: "eval.run" as const, display: "uv run pytest", exitCode: 1, checked: true, stdout: hugeStdout, stderr: shortStderr },
672
733
  ];
673
734
  await snap.writeAttempt({ id: "q1", verdict: "errored", attempt: 1, durationMs: 10, assertions: [] }, { commands: evidence });
674
735
  await finishAll(writer);
@@ -676,10 +737,10 @@ describe("createWriter", () => {
676
737
  const results = await openRecord(root);
677
738
  const q1 = results.experiments[0].latestRun.attempts[0]!;
678
739
  const [readBack] = (await q1.commands())!;
679
- expect(readBack.stdout).toBe(hugeStdout); // 逐字节相等,没有 marker 行、没有丢尾部 summary
680
- expect(readBack.stderr).toBe(hugeStderr);
681
- expect(JSON.stringify(readBack)).not.toContain("[niceeval] truncated");
682
- expect((readBack as { truncated?: unknown }).truncated).toBeUndefined(); // 不再产出 truncated 字段
740
+ expect(Buffer.byteLength(readBack.stdout, "utf-8")).toBeLessThanOrEqual(COMMAND_STREAM_MAX_BYTES + 100);
741
+ expect(readBack.stdout).toContain(`[niceeval] truncated ${hugeStdoutBytes} →`);
742
+ expect(readBack.stderr).toBe(shortStderr); // 未超限的流逐字节原样往返,不被同伴截断牵连
743
+ expect(readBack.truncated).toEqual([{ path: "stdout", originalBytes: hugeStdoutBytes }]);
683
744
  });
684
745
 
685
746
  it("每个 Run 各自独立封口:两个 Experiment 各自不同的 completedAt 与 diagnostics 不串味,空 diagnostics 省略字段", async () => {
@@ -1,30 +1,38 @@
1
1
  // 落盘大值截断(见 docs/feature/record/architecture.md「大值截断」)。
2
2
  // 运行时全量,落盘截断:落点唯一在 snap.writeAttempt(writer.ts)——不在 adapter、不在 OTLP
3
3
  // 解析、不在事件归一化里做。适用范围:events.json 的事件字段与 trace.json 的 span 属性里的
4
- // 任意字符串值;commands.json 不在适用范围内(失败诊断的完整语义单位,全量原样落盘)。
4
+ // 任意字符串值,按 256 KiB 上限;commands.json 的 stdout/stderr 各自按 64 KiB 上限独立截断
5
+ // (见「commandsjson」)——上限更小是因为现在成功命令也记录,体量不再天然有界于失败命令数。
5
6
  // 没有 flag、没有配置项;截断永远不影响判决(落盘是证据,不是评分输入)。
6
7
 
7
- import type { JsonValue, StreamEvent, TraceSpan, Truncation } from "../types.ts";
8
+ import type { CommandExitEvidence, JsonValue, StreamEvent, TraceSpan, Truncation } from "../types.ts";
8
9
 
9
- /** 每个字符串值的落盘上限(UTF-8 字节)。 */
10
+ /** events.json / trace.json 每个字符串值的落盘上限(UTF-8 字节)。 */
10
11
  export const ARTIFACT_VALUE_MAX_BYTES = 256 * 1024;
11
12
 
13
+ /** commands.json 每条命令 stdout / stderr 各自的落盘上限(UTF-8 字节)。 */
14
+ export const COMMAND_STREAM_MAX_BYTES = 64 * 1024;
15
+
12
16
  const encoder = new TextEncoder();
13
17
 
14
18
  /** 截断到 maxBytes(按 UTF-8 字符边界回退,不切断多字节字符),末尾追加人可读 marker。 */
15
- function truncateString(value: string, originalBytes: number): string {
19
+ function truncateToBytes(value: string, originalBytes: number, maxBytes: number): string {
16
20
  // 按字节预算切:先按「字节数 ≥ 字符数」粗切,再逐步回退到不超预算的字符边界。
17
- let sliceLen = Math.min(value.length, ARTIFACT_VALUE_MAX_BYTES);
21
+ let sliceLen = Math.min(value.length, maxBytes);
18
22
  let head = value.slice(0, sliceLen);
19
- while (encoder.encode(head).length > ARTIFACT_VALUE_MAX_BYTES && sliceLen > 0) {
23
+ while (encoder.encode(head).length > maxBytes && sliceLen > 0) {
20
24
  // 超出量 ÷ 4(UTF-8 最长 4 字节)是安全的最小回退步长;循环兜底处理边界。
21
- sliceLen -= Math.max(1, Math.ceil((encoder.encode(head).length - ARTIFACT_VALUE_MAX_BYTES) / 4));
25
+ sliceLen -= Math.max(1, Math.ceil((encoder.encode(head).length - maxBytes) / 4));
22
26
  head = value.slice(0, sliceLen);
23
27
  }
24
28
  const kept = encoder.encode(head).length;
25
29
  return `${head}\n[niceeval] truncated ${originalBytes} → ${kept} bytes`;
26
30
  }
27
31
 
32
+ function truncateString(value: string, originalBytes: number): string {
33
+ return truncateToBytes(value, originalBytes, ARTIFACT_VALUE_MAX_BYTES);
34
+ }
35
+
28
36
  /**
29
37
  * 深度遍历一个 JSON 值,截断其中超限的字符串;返回(可能新建的)值与收集到的截断记录。
30
38
  * `path` 用点分段(数组下标同样入段),事件里从字段名起、span 属性里从 attribute key 起。
@@ -89,3 +97,20 @@ export function truncateSpans(spans: readonly TraceSpan[]): TraceSpan[] {
89
97
  return { ...span, attributes, truncated: [...(span.truncated ?? []), ...out] };
90
98
  });
91
99
  }
100
+
101
+ /** commands.json 落盘前的截断:stdout / stderr 各自独立按 `COMMAND_STREAM_MAX_BYTES` 截断。 */
102
+ export function truncateCommands(commands: readonly CommandExitEvidence[]): CommandExitEvidence[] {
103
+ return commands.map((command) => {
104
+ const out: Truncation[] = [];
105
+ const next: { stdout?: string; stderr?: string } = {};
106
+ for (const path of ["stdout", "stderr"] as const) {
107
+ const value = command[path];
108
+ const bytes = encoder.encode(value).length;
109
+ if (bytes <= COMMAND_STREAM_MAX_BYTES) continue;
110
+ out.push({ path, originalBytes: bytes });
111
+ next[path] = truncateToBytes(value, bytes, COMMAND_STREAM_MAX_BYTES);
112
+ }
113
+ if (out.length === 0) return command;
114
+ return { ...command, ...next, truncated: [...(command.truncated ?? []), ...out] };
115
+ });
116
+ }
@@ -17,7 +17,7 @@ import { RESULT_FILE, RUN_FILE, artifactFileOf, attemptDirOf, experimentDirOf }
17
17
  import { encodeAttemptLocator } from "./locator.ts";
18
18
  import { MANIFESTS_FILE, type RunManifests } from "./manifest.ts";
19
19
  import { hashEvalSource, normalizeEvalSource } from "./source-hash.ts";
20
- import { truncateEvents, truncateSpans } from "./truncate.ts";
20
+ import { truncateCommands, truncateEvents, truncateSpans } from "./truncate.ts";
21
21
  import type { Producer, RunMeta } from "./types.ts";
22
22
 
23
23
  export interface WriterOptions {
@@ -48,6 +48,13 @@ export interface WriterOptions {
48
48
  * `RunDeclaration.manifests`,后者优先。
49
49
  */
50
50
  manifests?: ReadonlyMap<string, RunManifests>;
51
+ /**
52
+ * 本次 invocation 规划期算出的 Run 级配置身份,按 experimentId 分组(见
53
+ * `runner/types.ts` 的 `InvocationShape.configHashes`)。隐式 run 声明
54
+ * (`writeAttemptFor`)按 experimentId 取用;显式 `run()` 声明用 `RunDeclaration.configHash`,
55
+ * 后者优先——`niceeval accept` 走的正是这条显式路径。
56
+ */
57
+ configHashes?: ReadonlyMap<string, string>;
51
58
  }
52
59
 
53
60
  /** 快照级声明:一个 experiment 声明一次,这些字段不塞进每条 attempt。 */
@@ -130,6 +137,14 @@ export interface RunWriter {
130
137
  timings?: TimingActivity[];
131
138
  sandboxBuilds?: SandboxBuildRecord[];
132
139
  name?: LocalizedText;
140
+ /**
141
+ * 本 Run 携带合入的 eval id(见 `experiment:complete` 事件的 `carriedResults`)。并入
142
+ * `experiment.selectedEvalIds`,使这份快照声明的覆盖题集包含携带条目——`selectedEvalIds`
143
+ * 的语义是「这份快照声明覆盖的题集」(本次选择 ∪ 携带条目),不止是本次选择器求值的结果
144
+ * (见 docs/feature/record/architecture.md「selectedEvalIds」)。省略或没有 `experiment`
145
+ * 声明时不产生任何效果。
146
+ */
147
+ carriedEvalIds?: readonly string[];
133
148
  }): Promise<void>;
134
149
  }
135
150
 
@@ -152,6 +167,21 @@ export interface Writer {
152
167
  snapshotWriters(): Promise<{ experimentId: string; writer: RunWriter }[]>;
153
168
  }
154
169
 
170
+ /**
171
+ * `finish()` 的 `carriedEvalIds` 并入 `experiment.selectedEvalIds`;没有 `experiment` 声明
172
+ * 或没有携带 id 时原样返回,不凭空合成一份 `ExperimentRunInfo`。
173
+ */
174
+ function mergeCarriedEvalIds(
175
+ experiment: ExperimentRunInfo | undefined,
176
+ carriedEvalIds: readonly string[] | undefined,
177
+ ): ExperimentRunInfo | undefined {
178
+ if (experiment === undefined || !carriedEvalIds?.length) return experiment;
179
+ const merged = new Set(experiment.selectedEvalIds);
180
+ for (const id of carriedEvalIds) merged.add(id);
181
+ if (merged.size === experiment.selectedEvalIds.length) return experiment;
182
+ return { ...experiment, selectedEvalIds: [...merged] };
183
+ }
184
+
155
185
  interface SnapshotState {
156
186
  /** 快照的权威 meta(不含 completedAt;knownEvalIds 随重复声明累加)。 */
157
187
  meta: RunMeta;
@@ -173,6 +203,7 @@ export function createWriter(root: string, opts: WriterOptions): Writer {
173
203
  const created: { experimentId: string; dir: string }[] = [];
174
204
 
175
205
  async function buildSnapshot(decl: RunDeclaration): Promise<SnapshotHandle> {
206
+ const configHash = decl.configHash ?? opts.configHashes?.get(decl.experimentId);
176
207
  const meta: RunMeta = {
177
208
  format: RECORD_FORMAT,
178
209
  schemaVersion: RECORD_SCHEMA_VERSION,
@@ -184,7 +215,7 @@ export function createWriter(root: string, opts: WriterOptions): Writer {
184
215
  agent: decl.agent,
185
216
  ...(decl.model !== undefined ? { model: decl.model } : {}),
186
217
  startedAt: decl.startedAt,
187
- ...(decl.configHash !== undefined ? { configHash: decl.configHash } : {}),
218
+ ...(configHash !== undefined ? { configHash } : {}),
188
219
  ...(decl.knownEvalIds?.length ? { knownEvalIds: [...new Set(decl.knownEvalIds)] } : {}),
189
220
  ...(decl.name !== undefined ? { name: decl.name } : {}),
190
221
  };
@@ -221,13 +252,14 @@ export function createWriter(root: string, opts: WriterOptions): Writer {
221
252
  state.finished = true;
222
253
  const completedAt = finishOpts?.completedAt ?? state.declCompletedAt ?? new Date().toISOString();
223
254
  const name = finishOpts?.name ?? state.declName;
255
+ const experiment = mergeCarriedEvalIds(state.meta.experiment, finishOpts?.carriedEvalIds);
224
256
  const finalMeta: RunMeta = {
225
257
  format: state.meta.format,
226
258
  schemaVersion: state.meta.schemaVersion,
227
259
  producer: state.meta.producer,
228
260
  runId: state.meta.runId,
229
261
  experimentId: state.meta.experimentId,
230
- ...(state.meta.experiment !== undefined ? { experiment: state.meta.experiment } : {}),
262
+ ...(experiment !== undefined ? { experiment } : {}),
231
263
  agent: state.meta.agent,
232
264
  ...(state.meta.model !== undefined ? { model: state.meta.model } : {}),
233
265
  startedAt: state.meta.startedAt,
@@ -394,12 +426,11 @@ async function writeAttemptFiles(
394
426
 
395
427
  const writes: Promise<void>[] = [];
396
428
  // 大值截断只发生在这里(序列化的那一刻):events 的事件字段与 trace 的 span 属性里的任意
397
- // 字符串值按 ARTIFACT_VALUE_MAX_BYTES 截断并留结构化 truncated 标记;运行时(断言 / o11y
398
- // 派生)看到的始终是完整值。commands、sources 与 diff 不截断——失败命令的 stdout/stderr 是
399
- // 一个完整的诊断语义单位,截哪一端都毁掉另一半(见 docs/feature/record/architecture.md
400
- // 的证据 registry)。
429
+ // 字符串值按 ARTIFACT_VALUE_MAX_BYTES 截断,commands 的 stdout/stderr 各自按
430
+ // COMMAND_STREAM_MAX_BYTES 截断,均留结构化 truncated 标记;运行时(断言 / o11y 派生)看到的
431
+ // 始终是完整值。sources 与 diff 不截断(见 docs/feature/record/architecture.md 的证据 registry)。
401
432
  if (hasCommands)
402
- writes.push(writeFile(join(attemptDir, artifactFileOf("commands")), JSON.stringify(artifacts!.commands!), "utf-8"));
433
+ writes.push(writeFile(join(attemptDir, artifactFileOf("commands")), JSON.stringify(truncateCommands(artifacts!.commands!)), "utf-8"));
403
434
  if (hasEvents)
404
435
  writes.push(writeFile(join(attemptDir, "events.json"), JSON.stringify(truncateEvents(artifacts!.events!)), "utf-8"));
405
436
  if (hasSources) writes.push(writeSourcesRef(snapDir, attemptDir, artifacts!.sources!, sourceStore));
@@ -0,0 +1,177 @@
1
+ // 24 视觉身份 = 六色 × 四变体的可直接使用 SVG/CSS 值。
2
+ // 契约:docs/feature/reports/library/presentation.md「实验颜色与维度呈现」、
3
+ // docs/feature/reports/components/README.md「视觉编码容量(24 个身份)」。
4
+ // pattern id 全局唯一、颜色走主题令牌 --niceeval-color-series-N,换 basalt/chalk 自动换色。
5
+
6
+ import type { ReactNode } from "react";
7
+ import type { SeriesVariant } from "../presentation.ts";
8
+
9
+ /** 槽位色板下标 1..6 → CSS 变量,原样交给 fill / stroke / color。 */
10
+ export function seriesColorVar(colorIndex: number): string {
11
+ if (!Number.isInteger(colorIndex) || colorIndex < 1 || colorIndex > 6) {
12
+ throw new Error(`colorIndex must be an integer in [1, 6], got ${colorIndex}.`);
13
+ }
14
+ return `var(--niceeval-color-series-${colorIndex})`;
15
+ }
16
+
17
+ /** SVG pattern id:niceeval-series-pat-v{2|3|4}-c{1..6}。variant 1 是实心,无 pattern。 */
18
+ export function seriesPatternId(colorIndex: number, variant: SeriesVariant): string {
19
+ if (variant === 1) {
20
+ throw new Error("variant 1 is solid fill; it has no pattern id.");
21
+ }
22
+ return `niceeval-series-pat-v${variant}-c${colorIndex}`;
23
+ }
24
+
25
+ export function seriesFill(colorIndex: number, variant: SeriesVariant): string {
26
+ if (variant === 1) return seriesColorVar(colorIndex);
27
+ return `url(#${seriesPatternId(colorIndex, variant)})`;
28
+ }
29
+
30
+ /** 线型变体 → strokeDasharray;variant 1 为空串(实线)。 */
31
+ export function seriesStrokeDasharray(variant: SeriesVariant): string {
32
+ switch (variant) {
33
+ case 1:
34
+ return "";
35
+ case 2:
36
+ return "6 4";
37
+ case 3:
38
+ return "2 3";
39
+ case 4:
40
+ return "8 3 2 3";
41
+ }
42
+ }
43
+
44
+ export interface SeriesMarkerShape {
45
+ readonly path: string;
46
+ readonly viewBox: string;
47
+ }
48
+
49
+ /** 四种 marker 形状(viewBox 0 0 12 12),变体按 docs 槽序表 1–4。 */
50
+ export function seriesMarkerShape(variant: SeriesVariant): SeriesMarkerShape {
51
+ switch (variant) {
52
+ case 1:
53
+ // 圆
54
+ return { path: "M6 1.5a4.5 4.5 0 1 0 0.01 0z", viewBox: "0 0 12 12" };
55
+ case 2:
56
+ // 方
57
+ return { path: "M2.5 2.5h7v7h-7z", viewBox: "0 0 12 12" };
58
+ case 3:
59
+ // 菱
60
+ return { path: "M6 1.5 10.5 6 6 10.5 1.5 6z", viewBox: "0 0 12 12" };
61
+ case 4:
62
+ // 三角
63
+ return { path: "M6 1.5 10.5 10.5 1.5 10.5z", viewBox: "0 0 12 12" };
64
+ }
65
+ }
66
+
67
+ export function seriesMarker(colorIndex: number, variant: SeriesVariant): {
68
+ readonly path: string;
69
+ readonly viewBox: string;
70
+ readonly fill: string;
71
+ readonly stroke: string;
72
+ } {
73
+ const shape = seriesMarkerShape(variant);
74
+ const color = seriesColorVar(colorIndex);
75
+ return {
76
+ path: shape.path,
77
+ viewBox: shape.viewBox,
78
+ fill: color,
79
+ stroke: color,
80
+ };
81
+ }
82
+
83
+ /**
84
+ * 从 presentation.fill 反推 HTML 柱需要的系列 class。
85
+ * SVG 用 url(#pattern)/var();HTML 柱不能引用 SVG pattern,改挂 series-cN + fill-vN 类,
86
+ * 由 styles.css 用 repeating-linear-gradient 画等效图案,颜色仍走 --series 令牌。
87
+ */
88
+ export function seriesClassesFromFill(fill: string): string {
89
+ const pattern = /^url\(#niceeval-series-pat-v([2-4])-c([1-6])\)$/.exec(fill);
90
+ if (pattern) {
91
+ const variant = pattern[1]!;
92
+ const colorIndex = Number(pattern[2]);
93
+ return `niceeval-series-c${colorIndex - 1} niceeval-series-fill-v${variant}`;
94
+ }
95
+ const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(fill);
96
+ if (solid) {
97
+ return `niceeval-series-c${Number(solid[1]) - 1}`;
98
+ }
99
+ return "niceeval-series-none";
100
+ }
101
+
102
+ /** 从 color 呈现或 series 的 stroke/fill 反推 series-cN 类(图例色点回落)。 */
103
+ export function seriesClassFromColorVar(color: string): string {
104
+ const solid = /^var\(--niceeval-color-series-([1-6])\)$/.exec(color);
105
+ if (solid) return `niceeval-series-c${Number(solid[1]) - 1}`;
106
+ return "niceeval-series-none";
107
+ }
108
+
109
+ /**
110
+ * 页内注入一次的 SVG pattern defs。
111
+ * 18 个 pattern(3 非实心变体 × 6 色);id 与 seriesFill() 产出的 url(#…) 对齐。
112
+ * 子元素直接写 var(--niceeval-color-series-N),不走 currentColor
113
+ * (SVG pattern 内部 currentColor 取自 pattern 自身,引用者传不进来)。
114
+ */
115
+ export function SeriesPatternDefs(): ReactNode {
116
+ const patterns: ReactNode[] = [];
117
+ for (let colorIndex = 1; colorIndex <= 6; colorIndex++) {
118
+ const color = seriesColorVar(colorIndex);
119
+ // v2:对角斜线
120
+ patterns.push(
121
+ <pattern
122
+ key={`v2-c${colorIndex}`}
123
+ id={seriesPatternId(colorIndex, 2)}
124
+ patternUnits="userSpaceOnUse"
125
+ width="6"
126
+ height="6"
127
+ >
128
+ <rect width="6" height="6" fill={color} fillOpacity={0.22} />
129
+ <path
130
+ d="M-1 1l2-2M0 6l6-6M5 7l2-2"
131
+ stroke={color}
132
+ strokeWidth={1.4}
133
+ fill="none"
134
+ />
135
+ </pattern>,
136
+ );
137
+ // v3:水平条纹
138
+ patterns.push(
139
+ <pattern
140
+ key={`v3-c${colorIndex}`}
141
+ id={seriesPatternId(colorIndex, 3)}
142
+ patternUnits="userSpaceOnUse"
143
+ width="6"
144
+ height="6"
145
+ >
146
+ <rect width="6" height="6" fill={color} fillOpacity={0.18} />
147
+ <path d="M0 1.5h6M0 4.5h6" stroke={color} strokeWidth={1.5} fill="none" />
148
+ </pattern>,
149
+ );
150
+ // v4:点阵
151
+ patterns.push(
152
+ <pattern
153
+ key={`v4-c${colorIndex}`}
154
+ id={seriesPatternId(colorIndex, 4)}
155
+ patternUnits="userSpaceOnUse"
156
+ width="6"
157
+ height="6"
158
+ >
159
+ <rect width="6" height="6" fill={color} fillOpacity={0.16} />
160
+ <circle cx="2" cy="2" r="1.15" fill={color} />
161
+ <circle cx="5" cy="5" r="1.15" fill={color} />
162
+ </pattern>,
163
+ );
164
+ }
165
+ return (
166
+ <svg
167
+ className="niceeval-series-defs"
168
+ width={0}
169
+ height={0}
170
+ aria-hidden="true"
171
+ focusable="false"
172
+ style={{ position: "absolute", width: 0, height: 0, overflow: "hidden" }}
173
+ >
174
+ <defs>{patterns}</defs>
175
+ </svg>
176
+ );
177
+ }