@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.
- package/.agents/plugins/marketplace.json +2 -2
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/.cursor-plugin/plugin.json +1 -1
- package/README.md +96 -149
- package/capabilities/cognitive-routing/SKILL.md +21 -129
- package/capabilities/cognitive-routing/references/coordinator.md +163 -0
- package/capabilities/cognitive-routing/references/worker.md +38 -0
- package/capabilities/compaction/SKILL.md +3 -3
- package/capabilities/compaction/references/summary-format.md +26 -15
- package/capabilities/messaging/SKILL.md +49 -0
- package/capabilities/task-memory/SKILL.md +87 -0
- package/command-surface.json +6 -54
- package/gemini-extension.json +1 -1
- package/hooks/claude/hooks.json +13 -3
- package/hooks/codex/hooks.json +2 -1
- package/hooks/shared/runtime-context.mjs +69 -77
- package/package.json +25 -8
- package/pi-extension/dist/ask-user/ask.js +108 -0
- package/pi-extension/dist/ask-user/form.js +250 -0
- package/pi-extension/dist/ask-user/questions.js +66 -0
- package/pi-extension/dist/ask-user/tool.js +124 -0
- package/pi-extension/dist/cognitive-routing/assembler.js +35 -11
- package/pi-extension/dist/cognitive-routing/bootstrap.js +90 -0
- package/pi-extension/dist/cognitive-routing/config.js +3 -4
- package/pi-extension/dist/cognitive-routing/event-store.js +4 -1
- package/pi-extension/dist/cognitive-routing/gate.js +20 -5
- package/pi-extension/dist/cognitive-routing/handlers.js +15 -13
- package/pi-extension/dist/cognitive-routing/model-control.js +17 -8
- package/pi-extension/dist/cognitive-routing/projection.js +23 -6
- package/pi-extension/dist/cognitive-routing/provenance.js +3 -1
- package/pi-extension/dist/cognitive-routing/render.js +15 -15
- package/pi-extension/dist/cognitive-routing/runtime.js +4 -2
- package/pi-extension/dist/cognitive-routing/schemas.js +4 -4
- package/pi-extension/dist/cognitive-routing/session.js +3 -1
- package/pi-extension/dist/cognitive-routing/sources.js +29 -4
- package/pi-extension/dist/cognitive-routing/state.js +18 -8
- package/pi-extension/dist/cognitive-routing/status.js +4 -3
- package/pi-extension/dist/cognitive-routing/tools.js +29 -15
- package/pi-extension/dist/cognitive-routing/types.js +10 -1
- package/pi-extension/dist/compaction/carry.js +36 -22
- package/pi-extension/dist/compaction/config.js +4 -2
- package/pi-extension/dist/compaction/controller.js +52 -29
- package/pi-extension/dist/compaction/harness.js +19 -3
- package/pi-extension/dist/compaction/results.js +14 -13
- package/pi-extension/dist/compaction/tool.js +6 -6
- package/pi-extension/dist/host/catalog.js +20 -35
- package/pi-extension/dist/host/config-validation.js +233 -0
- package/pi-extension/dist/host/config.js +78 -91
- package/pi-extension/dist/host/inbound.js +142 -0
- package/pi-extension/dist/host/projection-tags.js +1 -1
- package/pi-extension/dist/host/prompts.js +24 -13
- package/pi-extension/dist/host/read-only-session.js +6 -1
- package/pi-extension/dist/host/request-history.js +7 -3
- package/pi-extension/dist/host/rows.js +25 -0
- package/pi-extension/dist/host/runtime-state.js +17 -2
- package/pi-extension/dist/host/section-keeper.js +85 -0
- package/pi-extension/dist/host/settings/freeflow-command.js +846 -93
- package/pi-extension/dist/host/settings/settings-view.js +27 -8
- package/pi-extension/dist/host/status.js +19 -5
- package/pi-extension/dist/index.js +638 -72
- package/pi-extension/dist/messaging/config.js +27 -0
- package/pi-extension/dist/messaging/messaging.js +398 -0
- package/pi-extension/dist/messaging/registry.js +57 -0
- package/pi-extension/dist/messaging/render.js +90 -0
- package/pi-extension/dist/messaging/tools.js +109 -0
- package/pi-extension/dist/messaging/transport.js +101 -0
- package/pi-extension/dist/provider-support/cache/keepalive.js +16 -4
- package/pi-extension/dist/task-memory/config.js +49 -0
- package/pi-extension/dist/task-memory/registry.js +119 -0
- package/pi-extension/dist/task-memory/render.js +107 -0
- package/pi-extension/dist/task-memory/runtime.js +994 -0
- package/pi-extension/dist/task-memory/tool.js +233 -0
- package/pi-extension/dist/tool-execution/apply-patch/tool.js +1 -1
- package/pi-extension/dist/tool-execution/background.js +220 -28
- package/pi-extension/dist/tool-execution/config.js +14 -3
- package/pi-extension/dist/tool-execution/file-tracking.js +24 -4
- package/pi-extension/dist/tool-execution/guidance-notices.js +124 -0
- package/pi-extension/dist/tool-execution/skill-reads.js +143 -0
- package/pi-extension/dist/tool-execution/tools.js +2 -2
- package/runtime/prompts/cognitive-routing.md +4 -4
- package/runtime/prompts/core.md +24 -14
- package/runtime/prompts/interaction-contract.md +7 -4
- package/runtime/prompts/tool-execution.md +3 -16
- package/runtime/prompts/working-method.md +16 -99
- package/skills/deliver-work/SKILL.md +76 -0
- package/skills/deliver-work/references/branch-closeout.md +134 -0
- package/skills/deliver-work/references/commit.md +83 -0
- package/skills/deliver-work/references/launch.md +94 -0
- package/skills/deliver-work/references/release.md +85 -0
- package/skills/deliver-work/scripts/commit-state.mjs +139 -0
- package/skills/deliver-work/scripts/delivery-state.mjs +107 -0
- package/skills/deliver-work/scripts/lib/git-state.mjs +178 -0
- package/skills/deliver-work/scripts/push-route.mjs +122 -0
- package/skills/deliver-work/scripts/release-preflight.mjs +177 -0
- package/skills/discuss-work/SKILL.md +148 -0
- package/skills/{discuss → discuss-work}/references/discussion-continuity.md +10 -27
- package/skills/discuss-work/references/learning-action.md +36 -0
- package/skills/discuss-work/scripts/choices.mjs +101 -0
- package/skills/discuss-work/scripts/lib/choices.mjs +159 -0
- package/skills/evaluate-behavior/SKILL.md +107 -0
- package/skills/evaluate-behavior/references/authoring.md +381 -0
- package/skills/evaluate-behavior/references/coding-tasks.md +145 -0
- package/skills/evaluate-behavior/references/conversations-pipelines-judges.md +177 -0
- package/skills/evaluate-behavior/references/design-and-measurement.md +273 -0
- package/skills/evaluate-behavior/references/running-and-evidence.md +245 -0
- package/skills/evaluate-behavior/scripts/claude-guard.mjs +76 -0
- package/skills/evaluate-behavior/scripts/guard-rules.mjs +309 -0
- package/skills/evaluate-behavior/scripts/lib/agreement.mjs +90 -0
- package/skills/evaluate-behavior/scripts/lib/arms.mjs +55 -0
- package/skills/evaluate-behavior/scripts/lib/budget.mjs +87 -0
- package/skills/evaluate-behavior/scripts/lib/build-source.mjs +287 -0
- package/skills/evaluate-behavior/scripts/lib/clear.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/container.mjs +395 -0
- package/skills/evaluate-behavior/scripts/lib/decision.mjs +418 -0
- package/skills/evaluate-behavior/scripts/lib/definitions.mjs +1072 -0
- package/skills/evaluate-behavior/scripts/lib/freeflow.mjs +103 -0
- package/skills/evaluate-behavior/scripts/lib/grade-command.mjs +591 -0
- package/skills/evaluate-behavior/scripts/lib/grade-comparison.mjs +90 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-interaction.mjs +19 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-support.mjs +32 -5
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/grade-workspace.mjs +28 -12
- package/skills/evaluate-behavior/scripts/lib/grade.mjs +101 -0
- package/skills/evaluate-behavior/scripts/lib/hooks.mjs +98 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/confinement.mjs +196 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/constraints.mjs +111 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/index.mjs +48 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/isolation.mjs +74 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/launch.mjs +123 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/model-call.mjs +73 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/recorder.mjs +178 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/session.mjs +494 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/claude-code/skills.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/index.mjs +31 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/names.mjs +3 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/confinement.mjs +173 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/host.mjs +148 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/index.mjs +70 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/isolation.mjs +81 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/model-call.mjs +79 -0
- package/skills/evaluate-behavior/scripts/lib/hosts/pi/process.mjs +241 -0
- package/skills/{evaluate-skill/scripts/lib → evaluate-behavior/scripts/lib/hosts/pi}/rpc.mjs +144 -26
- package/skills/evaluate-behavior/scripts/lib/identity.mjs +82 -0
- package/skills/evaluate-behavior/scripts/lib/judge.mjs +688 -0
- package/skills/evaluate-behavior/scripts/lib/metrics.mjs +332 -0
- package/skills/evaluate-behavior/scripts/lib/model-call.mjs +17 -0
- package/skills/evaluate-behavior/scripts/lib/participants.mjs +616 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/process.mjs +22 -0
- package/skills/evaluate-behavior/scripts/lib/queue.mjs +54 -0
- package/skills/evaluate-behavior/scripts/lib/reference.mjs +343 -0
- package/skills/evaluate-behavior/scripts/lib/reuse.mjs +277 -0
- package/skills/evaluate-behavior/scripts/lib/review.mjs +933 -0
- package/skills/evaluate-behavior/scripts/lib/runner.mjs +732 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/sandbox.mjs +119 -14
- package/skills/evaluate-behavior/scripts/lib/scan-shell.mjs +457 -0
- package/skills/evaluate-behavior/scripts/lib/scan.mjs +479 -0
- package/skills/evaluate-behavior/scripts/lib/seal.mjs +91 -0
- package/skills/evaluate-behavior/scripts/lib/shell.mjs +560 -0
- package/skills/evaluate-behavior/scripts/lib/source-override.mjs +64 -0
- package/skills/evaluate-behavior/scripts/lib/subject-file.mjs +88 -0
- package/skills/{evaluate-skill/scripts/lib/pi.mjs → evaluate-behavior/scripts/lib/subject.mjs} +235 -265
- package/skills/evaluate-behavior/scripts/lib/task-copy.mjs +124 -0
- package/skills/evaluate-behavior/scripts/lib/trials.mjs +524 -0
- package/skills/evaluate-behavior/scripts/lib/validity.mjs +68 -0
- package/skills/evaluate-behavior/scripts/lib/view-html.mjs +454 -0
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/view.mjs +159 -18
- package/skills/{evaluate-skill → evaluate-behavior}/scripts/pi-command-tool.mjs +21 -6
- package/skills/evaluate-behavior/scripts/pi-container-route.mjs +23 -0
- package/skills/evaluate-behavior/scripts/pi-guard.mjs +77 -0
- package/skills/evaluate-behavior/scripts/pi-login-refresh.mjs +31 -0
- package/skills/evaluate-behavior/scripts/skill-eval.mjs +586 -0
- package/skills/execute-work/SKILL.md +96 -104
- package/skills/execute-work/references/code-practices.md +27 -51
- package/skills/execute-work/references/design.md +271 -0
- package/skills/execute-work/references/diagnose.md +117 -0
- package/skills/execute-work/references/migration.md +108 -0
- package/skills/execute-work/references/simplify.md +65 -0
- package/skills/execute-work/scripts/self-review.mjs +422 -0
- package/skills/execute-work/scripts/touch-map.mjs +290 -0
- package/skills/review-work/SKILL.md +89 -65
- package/skills/review-work/references/adjudication.md +94 -0
- package/skills/review-work/references/artifact.md +198 -0
- package/skills/review-work/references/evals-and-judges.md +66 -0
- package/skills/review-work/references/work.md +177 -0
- package/skills/review-work/scripts/check-brief.mjs +128 -0
- package/skills/review-work/scripts/review-packet.mjs +233 -0
- package/skills/setup-freeflow/SKILL.md +68 -17
- package/skills/track-work/SKILL.md +52 -20
- package/skills/track-work/references/handoff.md +190 -0
- package/skills/track-work/references/projects.md +51 -0
- package/skills/track-work/references/working-record-format.md +36 -18
- package/skills/track-work/scripts/lib/document.mjs +17 -13
- package/skills/track-work/scripts/lib/format.mjs +2 -0
- package/skills/track-work/scripts/lib/recover.mjs +254 -0
- package/skills/track-work/scripts/lib/registry.mjs +656 -0
- package/skills/track-work/scripts/lib/search.mjs +40 -0
- package/skills/track-work/scripts/lib/store.mjs +46 -15
- package/skills/track-work/scripts/lib/views.mjs +50 -1
- package/skills/track-work/scripts/working-record.mjs +232 -13
- package/skills/verify-work/SKILL.md +45 -37
- package/skills/verify-work/references/feature-map.md +120 -0
- package/skills/verify-work/references/runtime-evidence.md +203 -0
- package/skills/verify-work/references/test-design.md +25 -6
- package/skills/verify-work/scripts/test-changes.mjs +246 -0
- package/skills/write-docs/SKILL.md +113 -25
- package/skills/write-docs/references/documentation-shapes.md +68 -3
- package/skills/write-docs/references/spec-and-plan-shapes.md +308 -0
- package/skills/write-docs/scripts/artifact-check.mjs +148 -0
- package/skills/write-docs/scripts/docs-check.mjs +96 -6
- package/skills/write-docs/scripts/plan-lint.mjs +123 -0
- package/skills/write-skill/SKILL.md +92 -52
- package/skills/write-skill/references/activation-and-wording.md +143 -0
- package/skills/write-skill/references/placement-and-dependencies.md +154 -0
- package/skills/write-skill/scripts/lib/compare.mjs +701 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +197 -3
- package/skills/write-skill/scripts/skill-author.mjs +51 -8
- package/capabilities/cognitive-routing/references/both-mode.md +0 -36
- package/capabilities/cognitive-routing/references/executor-mode.md +0 -48
- package/capabilities/cognitive-routing/references/helper-mode.md +0 -47
- package/plugin.json +0 -33
- package/skills/action-selection/SKILL.md +0 -92
- package/skills/action-selection/references/trajectory-stalls.md +0 -73
- package/skills/bypass/SKILL.md +0 -153
- package/skills/commit-work/SKILL.md +0 -93
- package/skills/commit-work/references/git-state-edges.md +0 -94
- package/skills/commit-work/references/simple-push.md +0 -94
- package/skills/commit-work/references/staging-decisions.md +0 -106
- package/skills/decision-gate/SKILL.md +0 -87
- package/skills/design-for-depth/SKILL.md +0 -108
- package/skills/design-for-depth/references/design-pressure-signals.md +0 -84
- package/skills/design-for-depth/references/implementation-decisions.md +0 -76
- package/skills/design-for-depth/references/interface-design-loop.md +0 -105
- package/skills/design-for-depth/references/module-and-dependency-design.md +0 -117
- package/skills/design-for-depth/references/state-and-failure-boundaries.md +0 -125
- package/skills/diagnose-failure/SKILL.md +0 -140
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +0 -50
- package/skills/diagnose-failure/references/flaky-and-performance-diagnosis.md +0 -51
- package/skills/discuss/SKILL.md +0 -140
- package/skills/evaluate-skill/SKILL.md +0 -188
- package/skills/evaluate-skill/references/evaluation-definition-schema.md +0 -415
- package/skills/evaluate-skill/references/evaluation-design.md +0 -112
- package/skills/evaluate-skill/references/execution-and-evidence.md +0 -111
- package/skills/evaluate-skill/references/review-and-revision.md +0 -94
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +0 -448
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +0 -60
- package/skills/evaluate-skill/scripts/lib/grade.mjs +0 -64
- package/skills/evaluate-skill/scripts/lib/review.mjs +0 -196
- package/skills/evaluate-skill/scripts/lib/runner.mjs +0 -405
- package/skills/evaluate-skill/scripts/lib/trials.mjs +0 -103
- package/skills/evaluate-skill/scripts/pi-guard.mjs +0 -181
- package/skills/evaluate-skill/scripts/skill-eval.mjs +0 -182
- package/skills/execute-work/references/execute-work-edges.md +0 -78
- package/skills/finish-branch/SKILL.md +0 -117
- package/skills/finish-branch/references/integration-options.md +0 -80
- package/skills/handoff/SKILL.md +0 -82
- package/skills/handoff/references/handoff-templates.md +0 -150
- package/skills/launch-work/SKILL.md +0 -116
- package/skills/launch-work/references/launch-readiness.md +0 -85
- package/skills/migration-work/SKILL.md +0 -115
- package/skills/migration-work/references/migration-lifecycle.md +0 -82
- package/skills/release-work/SKILL.md +0 -118
- package/skills/release-work/references/release-evidence.md +0 -78
- package/skills/review-artifact/SKILL.md +0 -143
- package/skills/review-artifact/references/adjudicate-artifact-review.md +0 -101
- package/skills/review-artifact/references/independent-artifact-reviewer-contract.md +0 -159
- package/skills/review-work/references/adjudicate-work-review.md +0 -97
- package/skills/review-work/references/independent-work-reviewer-contract.md +0 -139
- package/skills/review-work/references/security-risk-lens.md +0 -67
- package/skills/setup-freeflow/references/activation-contract.md +0 -103
- package/skills/setup-freeflow/references/host-setup.md +0 -67
- package/skills/simplify-code/SKILL.md +0 -85
- package/skills/simplify-code/references/simplification-patterns.md +0 -70
- package/skills/verify-work/references/browser-runtime-evidence.md +0 -51
- package/skills/verify-work/references/integration-evidence.md +0 -49
- package/skills/verify-work/references/performance-evidence.md +0 -59
- package/skills/workflow/SKILL.md +0 -171
- package/skills/workflow/references/domain-skill-composition.md +0 -45
- package/skills/write-plan/SKILL.md +0 -92
- package/skills/write-plan/references/plan-shapes.md +0 -156
- package/skills/write-skill/references/activation-boundaries.md +0 -106
- package/skills/write-skill/references/agent-first-instructions.md +0 -133
- package/skills/write-skill/references/progressive-disclosure.md +0 -129
- package/skills/write-skill/references/skill-dependency-graphs.md +0 -192
- package/skills/write-spec/SKILL.md +0 -85
- package/skills/write-spec/references/artifact-standards.md +0 -86
- package/skills/write-spec/references/decision-records.md +0 -80
- package/skills/write-spec/references/spec-shapes.md +0 -143
- /package/skills/{evaluate-skill → evaluate-behavior}/scripts/lib/evidence.mjs +0 -0
- /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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
|
83
|
+
return singleTurnReadPaths(run) ?? [];
|
|
64
84
|
}
|
|
65
85
|
if (!Array.isArray(run.turns)) {
|
|
66
|
-
return turn === 1
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
64
|
+
placement !== null ||
|
|
61
65
|
!["equals", "excludes"].includes(expectation.expect) ||
|
|
62
66
|
!validPathArray(expectation.paths)
|
|
63
67
|
) {
|
|
64
|
-
return
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|