@hassangameryt/freeflow 0.8.0 → 0.9.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 (290) hide show
  1. package/.agents/plugins/marketplace.json +2 -2
  2. package/.claude-plugin/marketplace.json +1 -1
  3. package/.claude-plugin/plugin.json +2 -2
  4. package/.codex-plugin/plugin.json +3 -3
  5. package/.cursor-plugin/plugin.json +1 -1
  6. package/README.md +96 -149
  7. package/capabilities/cognitive-routing/SKILL.md +21 -129
  8. package/capabilities/cognitive-routing/references/coordinator.md +163 -0
  9. package/capabilities/cognitive-routing/references/worker.md +38 -0
  10. package/capabilities/compaction/SKILL.md +3 -3
  11. package/capabilities/compaction/references/summary-format.md +26 -15
  12. package/capabilities/messaging/SKILL.md +49 -0
  13. package/capabilities/task-memory/SKILL.md +87 -0
  14. package/command-surface.json +6 -54
  15. package/gemini-extension.json +1 -1
  16. package/hooks/claude/hooks.json +13 -3
  17. package/hooks/codex/hooks.json +2 -1
  18. package/hooks/shared/runtime-context.mjs +69 -77
  19. package/package.json +25 -8
  20. package/pi-extension/dist/ask-user/ask.js +108 -0
  21. package/pi-extension/dist/ask-user/form.js +250 -0
  22. package/pi-extension/dist/ask-user/questions.js +66 -0
  23. package/pi-extension/dist/ask-user/tool.js +124 -0
  24. package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
  25. package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
  26. package/pi-extension/dist/cognitive-routing/config.js +3 -4
  27. package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
  28. package/pi-extension/dist/cognitive-routing/gate.js +20 -5
  29. package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
  30. package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
  31. package/pi-extension/dist/cognitive-routing/projection.js +23 -6
  32. package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
  33. package/pi-extension/dist/cognitive-routing/render.js +15 -15
  34. package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
  35. package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
  36. package/pi-extension/dist/cognitive-routing/session.js +3 -1
  37. package/pi-extension/dist/cognitive-routing/sources.js +29 -4
  38. package/pi-extension/dist/cognitive-routing/state.js +18 -8
  39. package/pi-extension/dist/cognitive-routing/status.js +4 -3
  40. package/pi-extension/dist/cognitive-routing/tools.js +29 -15
  41. package/pi-extension/dist/cognitive-routing/types.js +10 -1
  42. package/pi-extension/dist/compaction/carry.js +36 -22
  43. package/pi-extension/dist/compaction/config.js +4 -2
  44. package/pi-extension/dist/compaction/controller.js +52 -29
  45. package/pi-extension/dist/compaction/harness.js +19 -3
  46. package/pi-extension/dist/compaction/results.js +14 -13
  47. package/pi-extension/dist/compaction/tool.js +6 -6
  48. package/pi-extension/dist/host/catalog.js +20 -35
  49. package/pi-extension/dist/host/config-validation.js +233 -0
  50. package/pi-extension/dist/host/config.js +78 -91
  51. package/pi-extension/dist/host/inbound.js +142 -0
  52. package/pi-extension/dist/host/projection-tags.js +1 -1
  53. package/pi-extension/dist/host/prompts.js +24 -13
  54. package/pi-extension/dist/host/read-only-session.js +6 -1
  55. package/pi-extension/dist/host/request-history.js +7 -3
  56. package/pi-extension/dist/host/rows.js +25 -0
  57. package/pi-extension/dist/host/runtime-state.js +17 -2
  58. package/pi-extension/dist/host/section-keeper.js +85 -0
  59. package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
  60. package/pi-extension/dist/host/settings/settings-view.js +27 -8
  61. package/pi-extension/dist/host/status.js +19 -5
  62. package/pi-extension/dist/index.js +638 -72
  63. package/pi-extension/dist/messaging/config.js +27 -0
  64. package/pi-extension/dist/messaging/messaging.js +398 -0
  65. package/pi-extension/dist/messaging/registry.js +57 -0
  66. package/pi-extension/dist/messaging/render.js +90 -0
  67. package/pi-extension/dist/messaging/tools.js +109 -0
  68. package/pi-extension/dist/messaging/transport.js +101 -0
  69. package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
  70. package/pi-extension/dist/task-memory/config.js +49 -0
  71. package/pi-extension/dist/task-memory/registry.js +119 -0
  72. package/pi-extension/dist/task-memory/render.js +107 -0
  73. package/pi-extension/dist/task-memory/runtime.js +994 -0
  74. package/pi-extension/dist/task-memory/tool.js +233 -0
  75. package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
  76. package/pi-extension/dist/tool-execution/background.js +220 -28
  77. package/pi-extension/dist/tool-execution/config.js +14 -3
  78. package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
  79. package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
  80. package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
  81. package/pi-extension/dist/tool-execution/tools.js +2 -2
  82. package/runtime/prompts/cognitive-routing.md +4 -4
  83. package/runtime/prompts/core.md +24 -14
  84. package/runtime/prompts/interaction-contract.md +7 -4
  85. package/runtime/prompts/tool-execution.md +3 -16
  86. package/runtime/prompts/working-method.md +16 -99
  87. package/skills/deliver-work/SKILL.md +76 -0
  88. package/skills/deliver-work/references/branch-closeout.md +134 -0
  89. package/skills/deliver-work/references/commit.md +83 -0
  90. package/skills/deliver-work/references/launch.md +94 -0
  91. package/skills/deliver-work/references/release.md +85 -0
  92. package/skills/deliver-work/scripts/commit-state.mjs +139 -0
  93. package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
  94. package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
  95. package/skills/deliver-work/scripts/push-route.mjs +122 -0
  96. package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
  97. package/skills/discuss-work/SKILL.md +148 -0
  98. package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
  99. package/skills/discuss-work/references/learning-action.md +36 -0
  100. package/skills/discuss-work/scripts/choices.mjs +101 -0
  101. package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
  102. package/skills/evaluate-behavior/SKILL.md +107 -0
  103. package/skills/evaluate-behavior/references/authoring.md +381 -0
  104. package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
  105. package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
  106. package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
  107. package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
  108. package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
  109. package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
  110. package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
  111. package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
  112. package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
  113. package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
  114. package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
  115. package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
  116. package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
  117. package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
  118. package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
  119. package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
  120. package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
  121. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
  122. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
  123. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
  124. package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
  125. package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
  126. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
  127. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
  128. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
  129. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
  130. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
  131. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
  132. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
  133. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
  134. package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
  135. package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
  136. package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
  137. package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
  138. package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
  139. package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
  140. package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
  141. package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
  142. package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
  143. package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
  144. package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
  145. package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
  146. package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
  147. package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
  148. package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
  149. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
  150. package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
  151. package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
  152. package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
  153. package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
  154. package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
  155. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
  156. package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
  157. package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
  158. package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
  159. package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
  160. package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
  161. package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
  162. package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
  163. package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
  164. package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
  165. package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
  166. package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
  167. package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
  168. package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
  169. package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
  170. package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
  171. package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
  172. package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
  173. package/skills/execute-work/SKILL.md +96 -104
  174. package/skills/execute-work/references/code-practices.md +27 -51
  175. package/skills/execute-work/references/design.md +271 -0
  176. package/skills/execute-work/references/diagnose.md +117 -0
  177. package/skills/execute-work/references/migration.md +108 -0
  178. package/skills/execute-work/references/simplify.md +65 -0
  179. package/skills/execute-work/scripts/self-review.mjs +422 -0
  180. package/skills/execute-work/scripts/touch-map.mjs +290 -0
  181. package/skills/review-work/SKILL.md +89 -65
  182. package/skills/review-work/references/adjudication.md +94 -0
  183. package/skills/review-work/references/artifact.md +198 -0
  184. package/skills/review-work/references/evals-and-judges.md +66 -0
  185. package/skills/review-work/references/work.md +177 -0
  186. package/skills/review-work/scripts/check-brief.mjs +128 -0
  187. package/skills/review-work/scripts/review-packet.mjs +233 -0
  188. package/skills/setup-freeflow/SKILL.md +68 -17
  189. package/skills/track-work/SKILL.md +52 -20
  190. package/skills/track-work/references/handoff.md +190 -0
  191. package/skills/track-work/references/projects.md +51 -0
  192. package/skills/track-work/references/working-record-format.md +36 -18
  193. package/skills/track-work/scripts/lib/document.mjs +17 -13
  194. package/skills/track-work/scripts/lib/format.mjs +2 -0
  195. package/skills/track-work/scripts/lib/recover.mjs +254 -0
  196. package/skills/track-work/scripts/lib/registry.mjs +656 -0
  197. package/skills/track-work/scripts/lib/search.mjs +40 -0
  198. package/skills/track-work/scripts/lib/store.mjs +46 -15
  199. package/skills/track-work/scripts/lib/views.mjs +50 -1
  200. package/skills/track-work/scripts/working-record.mjs +232 -13
  201. package/skills/verify-work/SKILL.md +45 -37
  202. package/skills/verify-work/references/feature-map.md +120 -0
  203. package/skills/verify-work/references/runtime-evidence.md +203 -0
  204. package/skills/verify-work/references/test-design.md +25 -6
  205. package/skills/verify-work/scripts/test-changes.mjs +246 -0
  206. package/skills/write-docs/SKILL.md +113 -25
  207. package/skills/write-docs/references/documentation-shapes.md +68 -3
  208. package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
  209. package/skills/write-docs/scripts/artifact-check.mjs +148 -0
  210. package/skills/write-docs/scripts/docs-check.mjs +96 -6
  211. package/skills/write-docs/scripts/plan-lint.mjs +123 -0
  212. package/skills/write-skill/SKILL.md +92 -52
  213. package/skills/write-skill/references/activation-and-wording.md +143 -0
  214. package/skills/write-skill/references/placement-and-dependencies.md +154 -0
  215. package/skills/write-skill/scripts/lib/compare.mjs +701 -0
  216. package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
  217. package/skills/write-skill/scripts/skill-author.mjs +51 -8
  218. package/capabilities/cognitive-routing/references/both-mode.md +0 -36
  219. package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
  220. package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
  221. package/plugin.json +0 -33
  222. package/skills/action-selection/SKILL.md +0 -92
  223. package/skills/action-selection/references/trajectory-stalls.md +0 -73
  224. package/skills/bypass/SKILL.md +0 -153
  225. package/skills/commit-work/SKILL.md +0 -93
  226. package/skills/commit-work/references/git-state-edges.md +0 -94
  227. package/skills/commit-work/references/simple-push.md +0 -94
  228. package/skills/commit-work/references/staging-decisions.md +0 -106
  229. package/skills/decision-gate/SKILL.md +0 -87
  230. package/skills/design-for-depth/SKILL.md +0 -108
  231. package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
  232. package/skills/design-for-depth/references/implementation-decisions.md +0 -76
  233. package/skills/design-for-depth/references/interface-design-loop.md +0 -105
  234. package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
  235. package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
  236. package/skills/diagnose-failure/SKILL.md +0 -140
  237. package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
  238. package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
  239. package/skills/discuss/SKILL.md +0 -140
  240. package/skills/evaluate-skill/SKILL.md +0 -188
  241. package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
  242. package/skills/evaluate-skill/references/evaluation-design.md +0 -112
  243. package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
  244. package/skills/evaluate-skill/references/review-and-revision.md +0 -94
  245. package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
  246. package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
  247. package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
  248. package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
  249. package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
  250. package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
  251. package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
  252. package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
  253. package/skills/execute-work/references/execute-work-edges.md +0 -78
  254. package/skills/finish-branch/SKILL.md +0 -117
  255. package/skills/finish-branch/references/integration-options.md +0 -80
  256. package/skills/handoff/SKILL.md +0 -82
  257. package/skills/handoff/references/handoff-templates.md +0 -150
  258. package/skills/launch-work/SKILL.md +0 -116
  259. package/skills/launch-work/references/launch-readiness.md +0 -85
  260. package/skills/migration-work/SKILL.md +0 -115
  261. package/skills/migration-work/references/migration-lifecycle.md +0 -82
  262. package/skills/release-work/SKILL.md +0 -118
  263. package/skills/release-work/references/release-evidence.md +0 -78
  264. package/skills/review-artifact/SKILL.md +0 -143
  265. package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
  266. package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
  267. package/skills/review-work/references/adjudicate-work-review.md +0 -97
  268. package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
  269. package/skills/review-work/references/security-risk-lens.md +0 -67
  270. package/skills/setup-freeflow/references/activation-contract.md +0 -103
  271. package/skills/setup-freeflow/references/host-setup.md +0 -67
  272. package/skills/simplify-code/SKILL.md +0 -85
  273. package/skills/simplify-code/references/simplification-patterns.md +0 -70
  274. package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
  275. package/skills/verify-work/references/integration-evidence.md +0 -49
  276. package/skills/verify-work/references/performance-evidence.md +0 -59
  277. package/skills/workflow/SKILL.md +0 -171
  278. package/skills/workflow/references/domain-skill-composition.md +0 -45
  279. package/skills/write-plan/SKILL.md +0 -92
  280. package/skills/write-plan/references/plan-shapes.md +0 -156
  281. package/skills/write-skill/references/activation-boundaries.md +0 -106
  282. package/skills/write-skill/references/agent-first-instructions.md +0 -133
  283. package/skills/write-skill/references/progressive-disclosure.md +0 -129
  284. package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
  285. package/skills/write-spec/SKILL.md +0 -85
  286. package/skills/write-spec/references/artifact-standards.md +0 -86
  287. package/skills/write-spec/references/decision-records.md +0 -80
  288. package/skills/write-spec/references/spec-shapes.md +0 -143
  289. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
  290. /package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-observer.mjs +0 -0
@@ -0,0 +1,90 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
3
+ import { DEFAULT_ARMS, DEFAULT_REFERENCE, isClassicPair } from "./arms.mjs";
4
+ import { hasOwn } from "./grade-support.mjs";
5
+
6
+ /**
7
+ * A comparison ID names exactly one expectation per arm, all of one kind and expected value. Each non-reference
8
+ * arm's outcome is a factual transition from the reference's state to its own. A baseline/candidate group keeps
9
+ * its original shape (`baseline`, `candidate`, `transition`); any other group records `reference`, `arms` and
10
+ * `transitions` keyed by arm.
11
+ *
12
+ * @param {any[]} expectations
13
+ * @param {any[]} checks
14
+ * @param {{arms?: string[], reference?: string}} [options]
15
+ */
16
+ export function gradeComparisons(expectations, checks, { arms = DEFAULT_ARMS, reference = DEFAULT_REFERENCE } = {}) {
17
+ /** @type {Map<string, any[]>} */
18
+ const grouped = new Map();
19
+ const errors = /** @type {any[]} */ ([]);
20
+ for (const expectation of expectations) {
21
+ if (!hasOwn(expectation, "comparison")) continue;
22
+ if (typeof expectation.comparison !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(expectation.comparison)) {
23
+ errors.push({ id: expectation.id, reason: "invalid comparison identity" });
24
+ continue;
25
+ }
26
+ const entries = grouped.get(expectation.comparison) ?? [];
27
+ entries.push(expectation);
28
+ grouped.set(expectation.comparison, entries);
29
+ }
30
+
31
+ const classic = isClassicPair(arms, reference);
32
+ const others = arms.filter((arm) => arm !== reference);
33
+ /** @type {Map<string, any>} */
34
+ const byCheck = new Map(checks.map((check) => [check.id, check]));
35
+ const comparisons = /** @type {any[]} */ ([]);
36
+ for (const [id, entries] of grouped) {
37
+ const expectationFor = Object.fromEntries(
38
+ arms.map((arm) => [arm, entries.filter((entry) => entry.variant === arm)]),
39
+ );
40
+ if (entries.length !== arms.length || arms.some((arm) => expectationFor[arm].length !== 1)) {
41
+ errors.push({
42
+ id,
43
+ reason: classic
44
+ ? "comparison requires one baseline and one candidate expectation"
45
+ : `comparison requires exactly one expectation for each arm: ${arms.join(", ")}`,
46
+ });
47
+ continue;
48
+ }
49
+ const checkFor = Object.fromEntries(arms.map((arm) => [arm, byCheck.get(expectationFor[arm][0].id)]));
50
+ if (!others.every((arm) => sameCheck(checkFor[reference], checkFor[arm]))) {
51
+ errors.push({ id, reason: "comparison expectations must describe the same deterministic check" });
52
+ continue;
53
+ }
54
+ if (classic) {
55
+ comparisons.push({
56
+ id,
57
+ kind: checkFor.baseline.kind,
58
+ baseline: { check: checkFor.baseline.id, state: checkFor.baseline.state },
59
+ candidate: { check: checkFor.candidate.id, state: checkFor.candidate.state },
60
+ transition: transition(checkFor.baseline.state, checkFor.candidate.state),
61
+ });
62
+ continue;
63
+ }
64
+ comparisons.push({
65
+ id,
66
+ kind: checkFor[reference].kind,
67
+ reference,
68
+ arms: Object.fromEntries(arms.map((arm) => [arm, { check: checkFor[arm].id, state: checkFor[arm].state }])),
69
+ transitions: Object.fromEntries(
70
+ others.map((arm) => [arm, transition(checkFor[reference].state, checkFor[arm].state)]),
71
+ ),
72
+ });
73
+ }
74
+ return { comparisons, errors };
75
+ }
76
+
77
+ function sameCheck(reference, other) {
78
+ return (
79
+ reference !== undefined &&
80
+ other !== undefined &&
81
+ reference.kind === other.kind &&
82
+ isDeepStrictEqual(reference.expected, other.expected)
83
+ );
84
+ }
85
+
86
+ function transition(reference, other) {
87
+ return [reference, other].every((state) => state === "pass" || state === "fail")
88
+ ? `${reference}-to-${other}`
89
+ : "unavailable";
90
+ }
@@ -5,15 +5,15 @@ import path from "node:path";
5
5
  import {
6
6
  completedCheck,
7
7
  hasOwn,
8
+ invalidExpectation,
8
9
  isContained,
9
10
  isSafeRelativePath,
11
+ placementProblem,
10
12
  readPaths,
11
13
  RESPONSE_EXPECTATIONS,
12
14
  textMatches,
13
15
  unavailableCheck,
14
- validBodyExpectation,
15
16
  validTurn,
16
- VARIANTS,
17
17
  } from "./grade-support.mjs";
18
18
 
19
19
  const ACTIVATION_EXPECTATIONS = new Set(["never", "on-turn", "by-turn", "not-before-turn"]);
@@ -21,17 +21,19 @@ const TOOL_CALL_EXPECTATIONS = new Set(["called", "succeeded", "failed", "not-ca
21
21
  const CONTEXT_TEXT_EXPECTATIONS = new Set(["contains", "not-contains", "equals"]);
22
22
  const CONTEXT_SURFACES = new Set(["system-prompt", "provider-context"]);
23
23
  const CONTEXT_REQUEST_SELECTORS = new Set(["first", "last"]);
24
+ const BODY = ["body"];
24
25
 
25
26
  export async function gradeSkillRead(group, runs, expectation) {
26
27
  const turn = expectation.turn ?? null;
27
28
  const timingExpectation = expectation.expect !== "never";
29
+ // The turn is checked here rather than as placement: a timing expectation needs one, and `never` takes none.
30
+ const placement = placementProblem(group, expectation, null, ["description"]);
28
31
  if (
29
- group.type !== "description" ||
30
- !VARIANTS.includes(expectation.variant) ||
32
+ placement !== null ||
31
33
  !ACTIVATION_EXPECTATIONS.has(expectation.expect) ||
32
34
  (timingExpectation ? turn === null || !validTurn(group, turn) : hasOwn(expectation, "turn"))
33
35
  ) {
34
- return { error: "invalid skill-read expectation" };
36
+ return invalidExpectation(expectation, placement);
35
37
  }
36
38
 
37
39
  const expected = { read: expectation.expect, turn };
@@ -49,17 +51,17 @@ export async function gradeSkillRead(group, runs, expectation) {
49
51
  export async function gradeResourceRead(group, runs, expectation) {
50
52
  const turn = expectation.turn ?? null;
51
53
  const indexed = expectation.resource === "skill" || expectation.resource === "context";
54
+ const placement = placementProblem(group, expectation, turn, ["description", "body"]);
52
55
  if (
53
- !VARIANTS.includes(expectation.variant) ||
56
+ placement !== null ||
54
57
  !["workspace", "skill", "context"].includes(expectation.resource) ||
55
58
  !["read", "not-read"].includes(expectation.expect) ||
56
59
  typeof expectation.path !== "string" ||
57
60
  !isSafeRelativePath(expectation.path) ||
58
- !validTurn(group, turn) ||
59
61
  (indexed && (!Number.isInteger(expectation.index) || expectation.index < 0)) ||
60
62
  (!indexed && hasOwn(expectation, "index"))
61
63
  ) {
62
- return { error: "invalid resource-read expectation" };
64
+ return invalidExpectation(expectation, placement);
63
65
  }
64
66
  const expected = {
65
67
  expect: expectation.expect,
@@ -109,15 +111,16 @@ export async function gradeContextText(group, runs, expectation) {
109
111
  const turn = expectation.turn ?? null;
110
112
  const surface = expectation.surface ?? "system-prompt";
111
113
  const request = expectation.request ?? "last";
114
+ const placement = placementProblem(group, expectation, turn, BODY);
112
115
  if (
113
- !validBodyExpectation(group, expectation, turn) ||
116
+ placement !== null ||
114
117
  !CONTEXT_SURFACES.has(surface) ||
115
118
  (!CONTEXT_REQUEST_SELECTORS.has(request) && !Number.isInteger(request)) ||
116
119
  (Number.isInteger(request) && request < 1) ||
117
120
  !CONTEXT_TEXT_EXPECTATIONS.has(expectation.expect) ||
118
121
  typeof expectation.value !== "string"
119
122
  ) {
120
- return { error: "invalid context-text expectation" };
123
+ return invalidExpectation(expectation, placement);
121
124
  }
122
125
 
123
126
  const expected = { expect: expectation.expect, value: expectation.value, surface, request, turn };
@@ -133,8 +136,9 @@ export async function gradeToolCall(group, runs, expectation) {
133
136
  const turn = expectation.turn ?? null;
134
137
  const contains = expectation.argumentContains ?? [];
135
138
  const notContains = expectation.argumentNotContains ?? [];
139
+ const placement = placementProblem(group, expectation, turn, BODY);
136
140
  if (
137
- !validBodyExpectation(group, expectation, turn) ||
141
+ placement !== null ||
138
142
  typeof expectation.tool !== "string" ||
139
143
  expectation.tool.trim() === "" ||
140
144
  !TOOL_CALL_EXPECTATIONS.has(expectation.expect) ||
@@ -143,7 +147,7 @@ export async function gradeToolCall(group, runs, expectation) {
143
147
  !Array.isArray(notContains) ||
144
148
  !notContains.every((value) => typeof value === "string")
145
149
  ) {
146
- return { error: "invalid tool-call expectation" };
150
+ return invalidExpectation(expectation, placement);
147
151
  }
148
152
 
149
153
  const expected = {
@@ -196,12 +200,9 @@ export async function gradeToolCall(group, runs, expectation) {
196
200
 
197
201
  export async function gradeResponseText(group, runs, expectation) {
198
202
  const turn = expectation.turn ?? null;
199
- if (
200
- !validBodyExpectation(group, expectation, turn) ||
201
- !RESPONSE_EXPECTATIONS.has(expectation.expect) ||
202
- typeof expectation.value !== "string"
203
- ) {
204
- return { error: "invalid response-text expectation" };
203
+ const placement = placementProblem(group, expectation, turn, BODY);
204
+ if (placement !== null || !RESPONSE_EXPECTATIONS.has(expectation.expect) || typeof expectation.value !== "string") {
205
+ return invalidExpectation(expectation, placement);
205
206
  }
206
207
 
207
208
  const expected = { expect: expectation.expect, value: expectation.value, turn };
@@ -2,15 +2,35 @@ import { createHash } from "node:crypto";
2
2
  import { lstat, readFile, realpath } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
 
5
- export const VARIANTS = ["baseline", "candidate"];
5
+ import { armNames } from "./arms.mjs";
6
+
6
7
  export const RESPONSE_EXPECTATIONS = new Set(["contains", "not-contains", "equals"]);
7
8
 
8
9
  export function hasOwn(value, key) {
9
10
  return Object.getOwnPropertyDescriptor(value, key) !== undefined;
10
11
  }
11
12
 
12
- export function validBodyExpectation(group, expectation, turn) {
13
- return group.type === "body" && VARIANTS.includes(expectation.variant) && validTurn(group, turn);
13
+ /** Whether an expectation's `variant` names one of the group's arms. */
14
+ export function isArm(group, name) {
15
+ return typeof name === "string" && armNames(group).includes(name);
16
+ }
17
+
18
+ /**
19
+ * Why an expectation cannot be graded in this group, decided by the group alone: its group type, its arm or its
20
+ * turn; null when it fits. Graders and definition loading share it, so a definition that loads is placed right.
21
+ */
22
+ export function placementProblem(group, expectation, turn, types) {
23
+ if (!types.includes(group.type)) return `it needs a ${types.join(" or ")} group`;
24
+ if (!isArm(group, expectation.variant)) {
25
+ return `its variant ${JSON.stringify(expectation.variant ?? null)} is not an arm of the group`;
26
+ }
27
+ if (!validTurn(group, turn)) return `its turn ${JSON.stringify(turn)} is not a declared turn`;
28
+ return null;
29
+ }
30
+
31
+ /** A grader's refusal of an expectation: its kind, and the placement problem when there is one. */
32
+ export function invalidExpectation(expectation, problem = null) {
33
+ return { error: `invalid ${expectation.kind} expectation${problem === null ? "" : `: ${problem}`}` };
14
34
  }
15
35
 
16
36
  export function validTurn(group, turn) {
@@ -60,10 +80,10 @@ export function readPaths(run, turn) {
60
80
  if (Array.isArray(run.turns)) {
61
81
  return run.turns.flatMap((entry) => (Array.isArray(entry.successfulReadPaths) ? entry.successfulReadPaths : []));
62
82
  }
63
- return Array.isArray(run.successfulReadPaths) ? run.successfulReadPaths : [];
83
+ return singleTurnReadPaths(run) ?? [];
64
84
  }
65
85
  if (!Array.isArray(run.turns)) {
66
- return turn === 1 && Array.isArray(run.successfulReadPaths) ? run.successfulReadPaths : null;
86
+ return turn === 1 ? singleTurnReadPaths(run) : null;
67
87
  }
68
88
  const observedTurn = run.turns.find((entry) => entry.turn === turn);
69
89
  return observedTurn && Array.isArray(observedTurn.successfulReadPaths) ? observedTurn.successfulReadPaths : null;
@@ -124,3 +144,10 @@ export function compareText(left, right) {
124
144
  if (left > right) return 1;
125
145
  return 0;
126
146
  }
147
+
148
+ // One-shot description runs record their reads under activation rather than at the top level.
149
+ function singleTurnReadPaths(run) {
150
+ if (Array.isArray(run.successfulReadPaths)) return run.successfulReadPaths;
151
+ if (Array.isArray(run.activation?.successfulReadPaths)) return run.activation.successfulReadPaths;
152
+ return null;
153
+ }
@@ -4,16 +4,18 @@ import {
4
4
  compareText,
5
5
  completedCheck,
6
6
  hasOwn,
7
+ invalidExpectation,
7
8
  isSafeRelativePath,
9
+ placementProblem,
8
10
  readWorkspaceFile,
9
11
  RESPONSE_EXPECTATIONS,
10
12
  textMatches,
11
13
  unavailableCheck,
12
- validBodyExpectation,
13
14
  validPathArray,
14
15
  workspaceEvidence,
15
16
  } from "./grade-support.mjs";
16
17
 
18
+ const BODY = ["body"];
17
19
  const JSON_EXPECTATIONS = new Set([
18
20
  "available",
19
21
  "missing",
@@ -26,13 +28,14 @@ const JSON_EXPECTATIONS = new Set([
26
28
 
27
29
  export async function gradePath(group, runs, expectation) {
28
30
  const turn = expectation.turn ?? null;
31
+ const placement = placementProblem(group, expectation, turn, BODY);
29
32
  if (
30
- !validBodyExpectation(group, expectation, turn) ||
33
+ placement !== null ||
31
34
  !["exists", "absent"].includes(expectation.expect) ||
32
35
  typeof expectation.path !== "string" ||
33
36
  !isSafeRelativePath(expectation.path)
34
37
  ) {
35
- return { error: "invalid path expectation" };
38
+ return invalidExpectation(expectation, placement);
36
39
  }
37
40
  const expected = { expect: expectation.expect, path: expectation.path, turn };
38
41
  const run = runs[expectation.variant];
@@ -56,12 +59,13 @@ export async function gradePath(group, runs, expectation) {
56
59
 
57
60
  export async function gradeChangedPaths(group, runs, expectation) {
58
61
  const turn = expectation.turn ?? null;
62
+ const placement = placementProblem(group, expectation, turn, BODY);
59
63
  if (
60
- !validBodyExpectation(group, expectation, turn) ||
64
+ placement !== null ||
61
65
  !["equals", "excludes"].includes(expectation.expect) ||
62
66
  !validPathArray(expectation.paths)
63
67
  ) {
64
- return { error: "invalid changed-paths expectation" };
68
+ return invalidExpectation(expectation, placement);
65
69
  }
66
70
  const expectedPaths = [...expectation.paths].sort(compareText);
67
71
  const expected = { expect: expectation.expect, paths: expectedPaths, turn };
@@ -81,16 +85,20 @@ export async function gradeChangedPaths(group, runs, expectation) {
81
85
 
82
86
  export async function gradeFileText(group, runs, expectation) {
83
87
  const turn = expectation.turn ?? null;
88
+ const line = expectation.line ?? null;
89
+ const placement = placementProblem(group, expectation, turn, BODY);
84
90
  if (
85
- !validBodyExpectation(group, expectation, turn) ||
91
+ placement !== null ||
86
92
  !RESPONSE_EXPECTATIONS.has(expectation.expect) ||
87
93
  typeof expectation.value !== "string" ||
88
94
  typeof expectation.path !== "string" ||
89
- !isSafeRelativePath(expectation.path)
95
+ !isSafeRelativePath(expectation.path) ||
96
+ (line !== null && !(Number.isInteger(line) && line >= 1))
90
97
  ) {
91
- return { error: "invalid file-text expectation" };
98
+ return invalidExpectation(expectation, placement);
92
99
  }
93
100
  const expected = { expect: expectation.expect, path: expectation.path, value: expectation.value, turn };
101
+ if (line !== null) expected.line = line;
94
102
  const run = runs[expectation.variant];
95
103
  if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
96
104
  const workspace = workspaceEvidence(run, turn);
@@ -102,13 +110,22 @@ export async function gradeFileText(group, runs, expectation) {
102
110
  sha256: file.sha256,
103
111
  turn,
104
112
  };
105
- const matches = file.state === "present" && textMatches(expectation.expect, expectation.value, file.text);
113
+ let text = file.state === "present" ? file.text : null;
114
+ if (line !== null) {
115
+ // One line, without its line ending. A line that does not exist fails every expectation,
116
+ // so an empty or short file cannot pass a not-contains check by having nothing to read.
117
+ const lines = text === null || text === "" ? [] : text.split(/\r?\n/);
118
+ text = line <= lines.length ? lines[line - 1] : null;
119
+ observed.line = text === null ? "missing" : "present";
120
+ }
121
+ const matches = text !== null && textMatches(expectation.expect, expectation.value, text);
106
122
  return completedCheck(expectation, expected, observed, matches);
107
123
  }
108
124
 
109
125
  export async function gradeJson(group, runs, expectation) {
110
126
  const turn = expectation.turn ?? null;
111
- if (!validJsonExpectation(group, expectation, turn)) return { error: "invalid json expectation" };
127
+ const placement = placementProblem(group, expectation, turn, BODY);
128
+ if (placement !== null || !validJsonExpectation(expectation)) return invalidExpectation(expectation, placement);
112
129
 
113
130
  const expected = {
114
131
  expect: expectation.expect,
@@ -126,10 +143,9 @@ export async function gradeJson(group, runs, expectation) {
126
143
  return completedCheck(expectation, expected, observed, jsonMatches(expectation, observed));
127
144
  }
128
145
 
129
- function validJsonExpectation(group, expectation, turn) {
146
+ function validJsonExpectation(expectation) {
130
147
  const fieldExpectation = ["field-present", "field-absent", "field-equals"].includes(expectation.expect);
131
148
  return (
132
- validBodyExpectation(group, expectation, turn) &&
133
149
  typeof expectation.path === "string" &&
134
150
  isSafeRelativePath(expectation.path) &&
135
151
  JSON_EXPECTATIONS.has(expectation.expect) &&
@@ -0,0 +1,101 @@
1
+ import { armNames, referenceArm } from "./arms.mjs";
2
+ import { gradeCommand } from "./grade-command.mjs";
3
+ import { gradeComparisons } from "./grade-comparison.mjs";
4
+ import {
5
+ gradeContextText,
6
+ gradeResourceRead,
7
+ gradeResponseText,
8
+ gradeSkillRead,
9
+ gradeToolCall,
10
+ } from "./grade-interaction.mjs";
11
+ import { gradeChangedPaths, gradeFileText, gradeJson, gradePath } from "./grade-workspace.mjs";
12
+
13
+ const GRADERS = new Map([
14
+ ["skill-read", gradeSkillRead],
15
+ ["resource-read", gradeResourceRead],
16
+ ["path", gradePath],
17
+ ["changed-paths", gradeChangedPaths],
18
+ ["file-text", gradeFileText],
19
+ ["json", gradeJson],
20
+ ["response-text", gradeResponseText],
21
+ ["context-text", gradeContextText],
22
+ ["tool-call", gradeToolCall],
23
+ ["command", gradeCommand],
24
+ ]);
25
+
26
+ /**
27
+ * @param {any} group
28
+ * @param {any} runs
29
+ * @param {any} evidence
30
+ * @returns {Promise<any>}
31
+ */
32
+ export async function gradeDeterministic(group, runs, evidence) {
33
+ const { checks, errors } = await gradeEach(group, runs, group.expectations);
34
+ const compared = gradeComparisons(group.expectations, checks, {
35
+ arms: armNames(group),
36
+ reference: referenceArm(group),
37
+ });
38
+ errors.push(...compared.errors);
39
+ return {
40
+ schema_version: 1,
41
+ state: reportState(checks, errors),
42
+ evidence,
43
+ checks,
44
+ comparisons: compared.comparisons,
45
+ errors,
46
+ };
47
+ }
48
+
49
+ /**
50
+ * What grading would refuse in a loaded group's expectations that the definition alone decides: a kind in a group
51
+ * type it cannot grade, an arm or turn the group lacks, field values its grader rejects, or comparison members
52
+ * that do not describe one check in every arm. Each grader runs with no run evidence, so the check is the
53
+ * grader's own and runs before anything is spent. An unknown kind, and any comparison it belongs to, is left to
54
+ * grading, where it is a grade error.
55
+ *
56
+ * @returns {Promise<Array<{ expectation?: string, comparison?: string, reason: string }>>}
57
+ */
58
+ export async function expectationProblems(group) {
59
+ const known = group.expectations.filter((expectation) => GRADERS.has(expectation.kind));
60
+ const { checks, errors } = await gradeEach(group, {}, known);
61
+ const unknownComparisons = new Set(
62
+ group.expectations
63
+ .filter((expectation) => !GRADERS.has(expectation.kind) && Object.hasOwn(expectation, "comparison"))
64
+ .map((expectation) => expectation.comparison),
65
+ );
66
+ const compared = gradeComparisons(
67
+ group.expectations.filter((expectation) => !unknownComparisons.has(expectation.comparison)),
68
+ checks,
69
+ { arms: armNames(group), reference: referenceArm(group) },
70
+ );
71
+ return [
72
+ ...errors.map(({ id, reason }) => ({ expectation: id, reason })),
73
+ ...compared.errors.map(({ id, reason }) => ({ comparison: id, reason })),
74
+ ];
75
+ }
76
+
77
+ async function gradeEach(group, runs, expectations) {
78
+ const checks = /** @type {Array<any>} */ ([]);
79
+ const errors = /** @type {Array<any>} */ ([]);
80
+ for (const expectation of expectations) {
81
+ const grader = GRADERS.get(expectation.kind);
82
+ if (!grader) {
83
+ errors.push({ id: expectation.id, reason: `unsupported expectation kind: ${expectation.kind}` });
84
+ continue;
85
+ }
86
+ try {
87
+ const result = await grader(group, runs, expectation);
88
+ if (result.error) errors.push({ id: expectation.id, reason: result.error });
89
+ else checks.push(result);
90
+ } catch (error) {
91
+ errors.push({ id: expectation.id, reason: error instanceof Error ? error.message : String(error) });
92
+ }
93
+ }
94
+ return { checks, errors };
95
+ }
96
+
97
+ function reportState(checks, errors) {
98
+ if (errors.length > 0) return "grade-error";
99
+ if (checks.length > 0 && checks.every((check) => check.state === "unavailable")) return "unavailable";
100
+ return "complete";
101
+ }
@@ -0,0 +1,98 @@
1
+ // Extension points for later evaluator features. Every registry starts empty, and with nothing registered the
2
+ // evaluator writes exactly what it wrote before. A hook that throws is recorded as `{ error }` in its own slot;
3
+ // it never fails the run, the grade or the aggregate.
4
+
5
+ const HOOK_NAME = /^[a-z][A-Za-z0-9]*$/;
6
+ // Fields an aggregate group entry already has; a section may not replace one.
7
+ const AGGREGATE_GROUP_FIELDS = new Set([
8
+ "id",
9
+ "trials",
10
+ "unreadableGrades",
11
+ "gradeErrors",
12
+ "runStates",
13
+ "comparisons",
14
+ "checks",
15
+ ]);
16
+
17
+ function createRegistry(kind, reserved = new Set()) {
18
+ const hooks = new Map();
19
+ return {
20
+ register(name, hook) {
21
+ if (typeof name !== "string" || !HOOK_NAME.test(name) || reserved.has(name)) {
22
+ throw new Error(`${kind} name must be a camelCase identifier that is not reserved: ${name}`);
23
+ }
24
+ if (typeof hook !== "function") throw new Error(`${kind} ${name} must be a function`);
25
+ if (hooks.has(name)) throw new Error(`${kind} ${name} is already registered`);
26
+ hooks.set(name, hook);
27
+ return () => {
28
+ if (hooks.get(name) === hook) hooks.delete(name);
29
+ };
30
+ },
31
+ entries() {
32
+ return [...hooks.entries()];
33
+ },
34
+ };
35
+ }
36
+
37
+ function failure(error) {
38
+ return { error: error instanceof Error ? error.message : String(error) };
39
+ }
40
+
41
+ const aggregators = createRegistry("aggregator", AGGREGATE_GROUP_FIELDS);
42
+ const postRunHooks = createRegistry("post-run hook");
43
+ const runMetricsHooks = createRegistry("run-metrics hook");
44
+
45
+ /** Aggregator: `hook({ id, arms, reference, trials: [{ id, directory, state, grade, runs }] })`, where `directory` is the trial's group directory; its result is stored on the aggregate group entry under `<name>`. */
46
+ export const registerAggregator = aggregators.register;
47
+
48
+ /** Post-run hook: `hook({ group, arm, run, root, directory, signal })` for each executed run, before run.json is written; its result is stored in run.json under `postRun.<name>`. */
49
+ export const registerPostRunHook = postRunHooks.register;
50
+
51
+ /** Run-metrics hook: synchronous `hook({ group, arm, subject, observation, run })` where pi.mjs assembles run.json; its returned object is merged into `run.metrics`. */
52
+ export const registerRunMetricsHook = runMetricsHooks.register;
53
+
54
+ export function hasAggregators() {
55
+ return aggregators.entries().length > 0;
56
+ }
57
+
58
+ /** Each registered aggregator's section for one aggregate group, by name. */
59
+ export async function aggregateSections(input) {
60
+ const sections = {};
61
+ for (const [name, hook] of aggregators.entries()) {
62
+ try {
63
+ sections[name] = await hook(input);
64
+ } catch (error) {
65
+ sections[name] = failure(error);
66
+ }
67
+ }
68
+ return sections;
69
+ }
70
+
71
+ /** Each registered post-run hook's record for one run, by name; a hook that returns undefined records nothing, and null means nothing was recorded. */
72
+ export async function postRunRecords(context) {
73
+ const records = {};
74
+ for (const [name, hook] of postRunHooks.entries()) {
75
+ try {
76
+ const record = await hook(context);
77
+ if (record !== undefined) records[name] = record;
78
+ } catch (error) {
79
+ records[name] = failure(error);
80
+ }
81
+ }
82
+ return Object.keys(records).length === 0 ? null : records;
83
+ }
84
+
85
+ /** The registered run-metrics hooks' fields merged in registration order; null when none is registered. */
86
+ export function runMetrics(context) {
87
+ const entries = runMetricsHooks.entries();
88
+ if (entries.length === 0) return null;
89
+ const metrics = {};
90
+ for (const [name, hook] of entries) {
91
+ try {
92
+ Object.assign(metrics, hook(context));
93
+ } catch (error) {
94
+ metrics[name] = failure(error);
95
+ }
96
+ }
97
+ return metrics;
98
+ }