@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,124 @@
|
|
|
1
|
+
// What a task's copy lacks against the setting it was taken from. A fixture is copied into each workspace as plain
|
|
2
|
+
// files, so a subject never has the original's Git history, its ignored files (a `.env`, built output, installed
|
|
3
|
+
// packages), or tools and network the original had. `check-tasks` lists each such gap, without a model, so the author
|
|
4
|
+
// can judge whether the task is still fair before paying for runs. Gaps are reported, never refused.
|
|
5
|
+
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { stat } from "node:fs/promises";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
|
|
11
|
+
import { armNames } from "./arms.mjs";
|
|
12
|
+
import { armSealing } from "./seal.mjs";
|
|
13
|
+
|
|
14
|
+
const run = promisify(execFile);
|
|
15
|
+
const GIT_WORDS = /\b(?:git|commit|commits|diff|branch|blame|stash|rebase|history|log)\b/i;
|
|
16
|
+
const RUN_WORDS = /\b(?:run|test|tests|build|install|npm|node|pytest|make|cargo|go test|execute)\b/i;
|
|
17
|
+
const NETWORK_WORDS = /\b(?:npm install|pip install|git clone|curl|wget|download|https?:\/\/\S+)/i;
|
|
18
|
+
|
|
19
|
+
/** Everything the subject is told: the prompt or turns, and each pipeline stage's own input. */
|
|
20
|
+
function taskText(group) {
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (typeof group.input?.prompt === "string") parts.push(group.input.prompt);
|
|
23
|
+
for (const turn of group.input?.turns ?? []) if (typeof turn === "string") parts.push(turn);
|
|
24
|
+
for (const stage of group.participants?.stages ?? []) {
|
|
25
|
+
if (typeof stage.input === "string") parts.push(stage.input);
|
|
26
|
+
else if (typeof stage.input?.prompt === "string") parts.push(stage.input.prompt);
|
|
27
|
+
}
|
|
28
|
+
return parts.join("\n");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Words of the task that look like file or folder names: `src/a.js`, `.env`, `report.md`, `node_modules/`. */
|
|
32
|
+
function namedPaths(text) {
|
|
33
|
+
const names = new Set();
|
|
34
|
+
for (const raw of text.split(/[\s`'"(),;:]+/)) {
|
|
35
|
+
const word = raw.replace(/[.!?]+$/, "");
|
|
36
|
+
if (word === "" || /^[a-z]+:\/\//i.test(word) || word.startsWith("/") || word.includes("..")) continue;
|
|
37
|
+
if (word.includes("/") || /^\.[A-Za-z0-9_-]+/.test(word) || /^[\w-]+\.[A-Za-z0-9]{1,8}$/.test(word)) {
|
|
38
|
+
names.add(word.replace(/\/+$/, ""));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return [...names];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function git(cwd, args) {
|
|
45
|
+
try {
|
|
46
|
+
const { stdout } = await run("git", args, { cwd, maxBuffer: 16 * 1024 * 1024 });
|
|
47
|
+
return { ok: true, stdout };
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return { ok: false, stdout: String(error?.stdout ?? "") };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The gaps between one group's task copy and its original setting, with no model: `gaps` (each a line saying what
|
|
55
|
+
* the copy lacks), and the `tools` and `network` each arm's subject has.
|
|
56
|
+
*
|
|
57
|
+
* @param {any} group a loaded group
|
|
58
|
+
* @param {{ root: string }} options
|
|
59
|
+
*/
|
|
60
|
+
export async function taskCopyGaps(group, { root }) {
|
|
61
|
+
const text = taskText(group);
|
|
62
|
+
const gaps = [];
|
|
63
|
+
const fixture = group.fixture === null ? null : path.resolve(root, group.fixture);
|
|
64
|
+
|
|
65
|
+
// Git history: a fixture is copied as plain files, so the workspace is never a repository.
|
|
66
|
+
const hasGit = fixture !== null && (await stat(path.join(fixture, ".git")).catch(() => null)) !== null;
|
|
67
|
+
if (!hasGit) {
|
|
68
|
+
const words = [...new Set((text.match(new RegExp(GIT_WORDS, "gi")) ?? []).map((word) => word.toLowerCase()))];
|
|
69
|
+
gaps.push(
|
|
70
|
+
`git history: the workspace is not a Git repository${words.length > 0 ? `, and the task mentions ${words.join(", ")}` : ""}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (fixture !== null) {
|
|
75
|
+
// Ignored files the fixture holds here: a run copies them, but no clone has them, so the task changes by machine.
|
|
76
|
+
const ignored = await git(fixture, ["ls-files", "--others", "--ignored", "--exclude-standard", "--", "."]);
|
|
77
|
+
const present = ignored.ok ? ignored.stdout.split("\n").filter(Boolean) : [];
|
|
78
|
+
if (present.length > 0) {
|
|
79
|
+
gaps.push(
|
|
80
|
+
`ignored files: the fixture holds ${present.slice(0, 5).join(", ")}${present.length > 5 ? ` and ${present.length - 5} more` : ""}, which Git ignores, so a clone of the repository lacks them`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Ignored files the task names but the copy lacks: the original setting had them (a .env, built output).
|
|
84
|
+
const missing = [];
|
|
85
|
+
for (const name of namedPaths(text)) {
|
|
86
|
+
if ((await stat(path.join(fixture, name)).catch(() => null)) !== null) continue;
|
|
87
|
+
const checked = await git(fixture, ["check-ignore", "--no-index", "-q", "--", name]);
|
|
88
|
+
if (checked.ok) missing.push(name);
|
|
89
|
+
}
|
|
90
|
+
if (missing.length > 0) {
|
|
91
|
+
gaps.push(`ignored files the task names: ${missing.join(", ")}; Git ignores them here, and the copy lacks them`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Tools and network, per arm: what the subject can run, and what its code can reach.
|
|
96
|
+
const tools = {};
|
|
97
|
+
const network = {};
|
|
98
|
+
for (const arm of armNames(group)) {
|
|
99
|
+
const shell = group.variants?.[arm]?.shell;
|
|
100
|
+
const bash = !group.tools.includes("bash")
|
|
101
|
+
? null
|
|
102
|
+
: shell?.container !== undefined
|
|
103
|
+
? `bash in ${shell.container.image}`
|
|
104
|
+
: `bash in ${shell?.isolation ?? "an undeclared shell"}`;
|
|
105
|
+
const commands = (group.commands ?? []).map((command) => command.id);
|
|
106
|
+
tools[arm] = [
|
|
107
|
+
...group.tools.filter((tool) => tool !== "bash" && tool !== "run_command"),
|
|
108
|
+
...(bash === null ? [] : [bash]),
|
|
109
|
+
...(commands.length > 0 ? [`run_command (${commands.join(", ")})`] : []),
|
|
110
|
+
];
|
|
111
|
+
const sealing = armSealing(group, arm);
|
|
112
|
+
network[arm] = sealing.network;
|
|
113
|
+
// A description group only reads, so only a body group's tools and network can fall short of the task.
|
|
114
|
+
if (group.type !== "body") continue;
|
|
115
|
+
if (bash === null && commands.length === 0 && RUN_WORDS.test(text)) {
|
|
116
|
+
gaps.push(`tools: arm ${arm} has no bash or run_command, and the task asks to run something`);
|
|
117
|
+
}
|
|
118
|
+
const wants = NETWORK_WORDS.exec(text);
|
|
119
|
+
if (wants !== null && sealing.network === "none") {
|
|
120
|
+
gaps.push(`network: arm ${arm} has none, and the task refers to it (${wants[0]})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return { gaps: [...new Set(gaps)], tools, network };
|
|
124
|
+
}
|
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { mkdir, readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { comparisonArms, referenceArm, storedArms } from "./arms.mjs";
|
|
5
|
+
import { budgetEvidence, createBudget } from "./budget.mjs";
|
|
6
|
+
import { decide, decisionClaim, undecidedCases } from "./decision.mjs";
|
|
7
|
+
import { loadDefinition, selectDefinition } from "./definitions.mjs";
|
|
8
|
+
import { createInvocationId, writeJson } from "./evidence.mjs";
|
|
9
|
+
import { aggregateSections, hasAggregators } from "./hooks.mjs";
|
|
10
|
+
import { createLimiter } from "./queue.mjs";
|
|
11
|
+
import { createRunReuse, refreshSelectedLogins, resultsDirectory, runEvaluation } from "./runner.mjs";
|
|
12
|
+
import { unclearedFlags } from "./scan.mjs";
|
|
13
|
+
|
|
14
|
+
const CHECK_STATES = ["pass", "fail", "unavailable"];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Runs the selection as independent complete invocations and records an aggregate beside them.
|
|
18
|
+
* Each trial remains an ordinary result; the aggregate only counts their deterministic grades.
|
|
19
|
+
*
|
|
20
|
+
* When the suite declares a `decision`, the aggregate records it and its claim is the decision's verdict. When
|
|
21
|
+
* the decision also declares `trials`, that schedule sets the trial count: `trials.initial` complete invocations,
|
|
22
|
+
* then rounds in which each case whose sign is not yet stable runs up to `trials.step` more trials, that group
|
|
23
|
+
* alone, until `trials.max`. A `trials` option that differs from `trials.initial` is refused; without a schedule,
|
|
24
|
+
* an omitted `trials` means one.
|
|
25
|
+
*
|
|
26
|
+
* Every invocation shares one subject limiter (`jobs` subjects at once), one budget and one reuse state, and
|
|
27
|
+
* logins are refreshed once before any of them. With one job, invocations run one after another as before.
|
|
28
|
+
* With `pilot`, the first `pilot` groups run once first; any problem stops the run before the batch (no
|
|
29
|
+
* aggregate is written), and otherwise the pilot, with one invocation of the remaining groups, is the first trial.
|
|
30
|
+
*
|
|
31
|
+
* @param {string} definitionFile
|
|
32
|
+
* @param {any} selectors
|
|
33
|
+
* @param {{root?: string, signal?: AbortSignal, trials?: number | null, maxCost?: number | null, onTrial?: (result: any) => void, dryRun?: boolean, evaluate?: typeof runEvaluation, jobs?: number, results?: string | null, reuse?: boolean, pilot?: number | null}} options
|
|
34
|
+
*/
|
|
35
|
+
export async function runTrials(definitionFile, selectors, options = {}) {
|
|
36
|
+
const { root = process.cwd(), signal, trials = null, maxCost = null, onTrial, dryRun = false } = options;
|
|
37
|
+
const { evaluate = runEvaluation, jobs = 1, pilot = null } = options;
|
|
38
|
+
const { selection, rule } = await loadSelection(definitionFile, selectors, root);
|
|
39
|
+
const schedule = rule?.trials ?? null;
|
|
40
|
+
if (schedule !== null && trials !== null && trials !== schedule.initial) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
`the suite's decision schedules ${schedule.initial} initial trials; omit --trials or set it to ${schedule.initial}`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const initial = schedule?.initial ?? trials ?? 1;
|
|
46
|
+
if (!Number.isInteger(initial) || initial < 1) throw new Error("trials must be a positive integer");
|
|
47
|
+
|
|
48
|
+
const runsDirectory = resultsDirectory(root, options.results);
|
|
49
|
+
const budget = createBudget(maxCost);
|
|
50
|
+
const shared = {
|
|
51
|
+
root,
|
|
52
|
+
signal,
|
|
53
|
+
budget,
|
|
54
|
+
dryRun,
|
|
55
|
+
subjects: createLimiter(jobs),
|
|
56
|
+
results: options.results ?? null,
|
|
57
|
+
reuse: createRunReuse(runsDirectory, options.reuse === true),
|
|
58
|
+
loginsRefreshed: true,
|
|
59
|
+
skipReferenceCheck: options.skipReferenceCheck === true,
|
|
60
|
+
sources: options.sources ?? null,
|
|
61
|
+
};
|
|
62
|
+
await refreshSelectedLogins(selection);
|
|
63
|
+
const stopped = () => Boolean(signal?.aborted || budget?.exhausted);
|
|
64
|
+
const results = [];
|
|
65
|
+
const entries = [];
|
|
66
|
+
const invoke = async (runSelectors, entry, extra = {}) => {
|
|
67
|
+
const result = await evaluate(definitionFile, runSelectors, { ...shared, ...extra });
|
|
68
|
+
onTrial?.(result);
|
|
69
|
+
// Each trial's own spend; the aggregate's budget holds the run's total.
|
|
70
|
+
const cost = result.summary?.cost?.spent;
|
|
71
|
+
return { result, entry: { id: result.id, state: result.state, ...(cost === undefined ? {} : { cost }), ...entry } };
|
|
72
|
+
};
|
|
73
|
+
// Invocations in order with one job; otherwise all at once, sharing the subject slots. Recorded in plan order.
|
|
74
|
+
const runAll = async (tasks) => {
|
|
75
|
+
const settled = [];
|
|
76
|
+
if (jobs === 1) {
|
|
77
|
+
for (const task of tasks) {
|
|
78
|
+
if (stopped()) break;
|
|
79
|
+
settled.push(await task());
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
settled.push(...(await Promise.all(tasks.map((task) => (stopped() ? null : task())))));
|
|
83
|
+
}
|
|
84
|
+
for (const outcome of settled) {
|
|
85
|
+
if (outcome === null) continue;
|
|
86
|
+
results.push(outcome.result);
|
|
87
|
+
entries.push(outcome.entry);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
let planned = 0;
|
|
92
|
+
const tasks = [];
|
|
93
|
+
let pilotRecord = null;
|
|
94
|
+
if (pilot !== null) {
|
|
95
|
+
const outcome = await invoke(selectors, { part: "pilot" }, { groupRange: [0, pilot] });
|
|
96
|
+
const problems = await pilotProblems(outcome.result);
|
|
97
|
+
pilotRecord = { id: outcome.result.id, groups: outcome.result.summary.groups.length, problems };
|
|
98
|
+
if (problems.length > 0)
|
|
99
|
+
return { id: null, path: null, state: "pilot-stopped", pilot: pilotRecord, aggregate: null };
|
|
100
|
+
results.push(outcome.result);
|
|
101
|
+
entries.push(outcome.entry);
|
|
102
|
+
planned += 1;
|
|
103
|
+
// The pilot is the first trial of its groups; the remaining groups run once to complete that trial.
|
|
104
|
+
if (pilot < selection.groups.length) {
|
|
105
|
+
tasks.push(() => invoke(selectors, { part: "rest" }, { groupRange: [pilot, selection.groups.length] }));
|
|
106
|
+
planned += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const full = pilot === null ? initial : initial - 1;
|
|
110
|
+
for (let trial = 0; trial < full; trial += 1) tasks.push(() => invoke(selectors, {}));
|
|
111
|
+
planned += full;
|
|
112
|
+
await runAll(tasks);
|
|
113
|
+
let groups = await aggregateGroups(results);
|
|
114
|
+
|
|
115
|
+
// Adaptive trials: only the declared schedule adds trials, and only to cases whose sign could still change.
|
|
116
|
+
const rounds = [];
|
|
117
|
+
const maxRounds = schedule === null ? 0 : Math.ceil((schedule.max - schedule.initial) / schedule.step);
|
|
118
|
+
while (rounds.length < maxRounds && results.length === planned && !stopped()) {
|
|
119
|
+
const undecided = undecidedCases(rule, groups);
|
|
120
|
+
if (undecided.length === 0) break;
|
|
121
|
+
const extra = undecided.map(({ id, trials: done }) => ({ id, add: Math.min(schedule.step, schedule.max - done) }));
|
|
122
|
+
const scheduled = extra.reduce((sum, { add }) => sum + add, 0);
|
|
123
|
+
const round = { round: rounds.length + 1, groups: undecided.map(({ id }) => id), scheduled, trials: 0 };
|
|
124
|
+
rounds.push(round);
|
|
125
|
+
planned += scheduled;
|
|
126
|
+
// Interleaved by step, so a budget that runs out leaves the cases evenly extended.
|
|
127
|
+
const roundTasks = [];
|
|
128
|
+
for (let step = 0; step < schedule.step; step += 1) {
|
|
129
|
+
for (const { id, add } of extra) {
|
|
130
|
+
if (step < add) roundTasks.push(() => invoke({ ...selectors, group: id }, { round: round.round, group: id }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const before = results.length;
|
|
134
|
+
await runAll(roundTasks);
|
|
135
|
+
round.trials = results.length - before;
|
|
136
|
+
groups = await aggregateGroups(results);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const id = createInvocationId();
|
|
140
|
+
const directory = path.join(runsDirectory, id);
|
|
141
|
+
await mkdir(directory, { recursive: true });
|
|
142
|
+
const aggregate = {
|
|
143
|
+
schema_version: 1,
|
|
144
|
+
kind: "aggregate",
|
|
145
|
+
id,
|
|
146
|
+
state: aggregateState(results, planned),
|
|
147
|
+
// The full invocations asked for; trials an adaptive round added are counted in `schedule.rounds`.
|
|
148
|
+
requestedTrials: initial,
|
|
149
|
+
trials: entries,
|
|
150
|
+
groups,
|
|
151
|
+
dryRun,
|
|
152
|
+
claim: null,
|
|
153
|
+
completedAt: new Date().toISOString(),
|
|
154
|
+
};
|
|
155
|
+
if (pilotRecord !== null) aggregate.pilot = pilotRecord;
|
|
156
|
+
const reused = reusedRuns(results);
|
|
157
|
+
if (reused.length > 0) aggregate.reused = reused;
|
|
158
|
+
const held = heldRuns(groups);
|
|
159
|
+
if (held.length > 0) aggregate.held = held;
|
|
160
|
+
if (schedule !== null) aggregate.schedule = { ...schedule, rounds };
|
|
161
|
+
if (rule !== null) aggregate.decision = decide(rule, groups);
|
|
162
|
+
aggregate.claim = claimFor(results, initial, dryRun, aggregate.decision, held.length);
|
|
163
|
+
if (budget !== null) aggregate.budget = budgetEvidence(budget);
|
|
164
|
+
await writeJson(path.join(directory, "aggregate.json"), aggregate);
|
|
165
|
+
return { id, path: directory, state: aggregate.state, aggregate };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* What stops a pilot: a run that was cancelled, failed its infrastructure or was invalid, a grade that is
|
|
170
|
+
* unreadable or has errors, a group that recorded errors, or a comparison that no arm passed.
|
|
171
|
+
*/
|
|
172
|
+
export async function pilotProblems(result) {
|
|
173
|
+
const problems = [];
|
|
174
|
+
for (const group of result.summary?.groups ?? []) {
|
|
175
|
+
for (const [arm, state] of Object.entries(group.variants ?? {})) {
|
|
176
|
+
if (["cancelled", "infrastructure-failed", "invalid"].includes(state)) {
|
|
177
|
+
const reason = state === "invalid" ? group.invalid?.[arm] : undefined;
|
|
178
|
+
problems.push(`${group.id}: arm ${arm} is ${state}${reason ? ` (${reason})` : ""}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const error of group.errors ?? []) problems.push(`${group.id}: ${error.message ?? error.kind}`);
|
|
182
|
+
for (const [arm, flags] of Object.entries(group.flagged ?? {})) {
|
|
183
|
+
problems.push(
|
|
184
|
+
`${group.id}: arm ${arm} has transcript flags no one has cleared (${flags.join(", ")}); read them, then clear them with skill-eval clear or fix the setup`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
const grade = await readJson(path.join(result.path, "groups", group.id, group.artifacts?.grade ?? ""));
|
|
188
|
+
if (grade === null) {
|
|
189
|
+
problems.push(`${group.id}: grade unreadable`);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const error of grade.errors ?? []) problems.push(`${group.id}: grade error: ${error.reason ?? error}`);
|
|
193
|
+
for (const comparison of grade.comparisons ?? []) {
|
|
194
|
+
const { arms } = comparisonArms(comparison);
|
|
195
|
+
if (!Object.values(arms ?? {}).some((outcome) => outcome?.state === "pass")) {
|
|
196
|
+
problems.push(`${group.id}: no arm passed ${comparison.id}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return problems;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Every run held out of the counts for transcript flags no one has cleared: the trial, group, arm and flags. */
|
|
204
|
+
function heldRuns(groups) {
|
|
205
|
+
return groups.flatMap((group) => (group.held ?? []).map((entry) => ({ group: group.id, ...entry })));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Every run a trial took from the index instead of running it: the trial, group and arm, and where it came from. */
|
|
209
|
+
function reusedRuns(results) {
|
|
210
|
+
const reused = [];
|
|
211
|
+
for (const result of results) {
|
|
212
|
+
for (const group of result.summary?.groups ?? []) {
|
|
213
|
+
for (const [arm, from] of Object.entries(group.reused ?? {})) {
|
|
214
|
+
reused.push({ trial: result.id, group: group.id, arm, from, reused: true });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return reused;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Rereads every trial an aggregate lists and rewrites its aggregate.json: groups, aggregator sections, decision
|
|
223
|
+
* and claim. Everything else (its ID, trials, schedule, budget, times) is kept, so rebuilding an unchanged
|
|
224
|
+
* aggregate reproduces it exactly. A trial whose summary cannot be read is refused rather than dropped. Trials
|
|
225
|
+
* are read from the directory that holds the aggregate, where they were stored beside it.
|
|
226
|
+
*
|
|
227
|
+
* @param {string} aggregatePath the aggregate's directory or its aggregate.json
|
|
228
|
+
* @param {{root?: string, results?: string | null}} [options]
|
|
229
|
+
*/
|
|
230
|
+
export async function rebuildAggregate(aggregatePath, { root = process.cwd(), results = null } = {}) {
|
|
231
|
+
const file = aggregatePath.endsWith(".json") ? aggregatePath : path.join(aggregatePath, "aggregate.json");
|
|
232
|
+
const aggregate = await readJson(file);
|
|
233
|
+
if (aggregate === null || aggregate.kind !== "aggregate" || !Array.isArray(aggregate.trials)) {
|
|
234
|
+
throw new Error(`not a readable aggregate: ${file}`);
|
|
235
|
+
}
|
|
236
|
+
const runsDirectory =
|
|
237
|
+
results === null ? path.dirname(path.dirname(path.resolve(root, file))) : path.resolve(root, results);
|
|
238
|
+
const trialResults = [];
|
|
239
|
+
for (const trial of aggregate.trials) {
|
|
240
|
+
const directory = path.join(runsDirectory, trial.id);
|
|
241
|
+
const summary = await readJson(path.join(directory, "summary.json"));
|
|
242
|
+
if (summary === null) throw new Error(`trial ${trial.id} has no readable summary.json`);
|
|
243
|
+
trialResults.push({ id: trial.id, path: directory, state: summary.state, summary });
|
|
244
|
+
}
|
|
245
|
+
const groups = await aggregateGroups(trialResults);
|
|
246
|
+
const { held: _held, ...kept } = aggregate;
|
|
247
|
+
const rebuilt = { ...kept, groups };
|
|
248
|
+
const held = heldRuns(groups);
|
|
249
|
+
if (held.length > 0) rebuilt.held = held;
|
|
250
|
+
if (aggregate.decision !== undefined) rebuilt.decision = decide(aggregate.decision.rule, groups);
|
|
251
|
+
const k = aggregate.schedule?.initial ?? aggregate.requestedTrials;
|
|
252
|
+
rebuilt.claim = claimFor(trialResults, k, aggregate.dryRun === true, rebuilt.decision, held.length);
|
|
253
|
+
await writeJson(file, rebuilt);
|
|
254
|
+
return rebuilt;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* The selection, and the suite's validated decision rule or null. A decision run must select both of its arms;
|
|
259
|
+
* loading the suite has already checked that every group has them and records the measure in both.
|
|
260
|
+
*/
|
|
261
|
+
async function loadSelection(definitionFile, selectors, root) {
|
|
262
|
+
const definitionPath = await realpath(path.resolve(root, definitionFile));
|
|
263
|
+
const definition = await loadDefinition(definitionPath, { root });
|
|
264
|
+
const selection = selectDefinition(definition, selectors);
|
|
265
|
+
const rule = definition.kind === "suite" ? (definition.decision ?? null) : null;
|
|
266
|
+
if (rule === null) return { selection, rule };
|
|
267
|
+
if (selectors?.variant !== null && selectors?.variant !== undefined) {
|
|
268
|
+
throw new Error("the suite's decision compares two arms; --variant would run only one");
|
|
269
|
+
}
|
|
270
|
+
return { selection, rule };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The strongest claim the aggregate supports. Without a decision rule, counts over complete trials support a rate
|
|
275
|
+
* on this suite. With one, the claim is the rule's verdict ("beats", "worse" or "no-difference-shown") with its
|
|
276
|
+
* split, p and rule hash. A dry run read inputs no commit identifies, so it supports no claim either way.
|
|
277
|
+
*/
|
|
278
|
+
function claimFor(results, k, dryRun, decision, held = 0) {
|
|
279
|
+
const complete = results.filter((result) => result.state === "complete").length;
|
|
280
|
+
// Runs held for uncleared transcript flags are left out of the counts and the decision; the claim says how many.
|
|
281
|
+
const heldNote = held > 0 ? { held } : {};
|
|
282
|
+
if (decision !== undefined) {
|
|
283
|
+
const reason = dryRun ? "dry run" : complete === 0 ? "no complete trial" : null;
|
|
284
|
+
return { ...decisionClaim(decision, { k, supported: reason === null, reason }), ...heldNote };
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
class: complete > 0 && !dryRun ? "rate-on-suite" : "none",
|
|
288
|
+
...(dryRun ? { reason: "dry run" } : {}),
|
|
289
|
+
k,
|
|
290
|
+
beats: "not supported: the suite declares no decision rule",
|
|
291
|
+
...heldNote,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function aggregateState(results, requested) {
|
|
296
|
+
if (results.length < requested || results.some((result) => result.state === "cancelled")) return "cancelled";
|
|
297
|
+
if (results.every((result) => result.state === "complete")) return "complete";
|
|
298
|
+
return "partially-complete";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Counts each group's checks and comparisons over every trial that ran it. A trial that recorded no state for a
|
|
303
|
+
* check or comparison (its grade was unreadable, or that check or comparison could not be graded) counts as
|
|
304
|
+
* `unavailable` for it, so every count, and pass^k, covers all the group's trials. Comparisons are counted per
|
|
305
|
+
* arm, and each non-reference arm's transitions and pairs against the reference.
|
|
306
|
+
*/
|
|
307
|
+
export async function aggregateGroups(results) {
|
|
308
|
+
/** @type {Map<string, any>} */
|
|
309
|
+
const groups = new Map();
|
|
310
|
+
const collectTrials = hasAggregators();
|
|
311
|
+
for (const result of results) {
|
|
312
|
+
for (const group of result.summary.groups) {
|
|
313
|
+
const groupDirectory = path.join(result.path, "groups", group.id);
|
|
314
|
+
let entry = groups.get(group.id);
|
|
315
|
+
if (entry === undefined) {
|
|
316
|
+
entry = {
|
|
317
|
+
id: group.id,
|
|
318
|
+
trials: 0,
|
|
319
|
+
unreadableGrades: 0,
|
|
320
|
+
gradeErrors: 0,
|
|
321
|
+
runStates: {},
|
|
322
|
+
comparisons: new Map(),
|
|
323
|
+
checks: new Map(),
|
|
324
|
+
definition: null,
|
|
325
|
+
perTrial: [],
|
|
326
|
+
held: [],
|
|
327
|
+
};
|
|
328
|
+
groups.set(group.id, entry);
|
|
329
|
+
}
|
|
330
|
+
entry.trials += 1;
|
|
331
|
+
if (entry.definition === null) {
|
|
332
|
+
entry.definition = await readJson(path.join(groupDirectory, "definition.json"));
|
|
333
|
+
seedChecks(entry);
|
|
334
|
+
}
|
|
335
|
+
const arms = storedArms(group, entry.definition);
|
|
336
|
+
for (const arm of arms) {
|
|
337
|
+
const states = (entry.runStates[arm] ??= {});
|
|
338
|
+
const state = group.variants?.[arm];
|
|
339
|
+
if (typeof state === "string") states[state] = (states[state] ?? 0) + 1;
|
|
340
|
+
}
|
|
341
|
+
const runs = Object.fromEntries(
|
|
342
|
+
await Promise.all(arms.map(async (arm) => [arm, await readJson(path.join(groupDirectory, arm, "run.json"))])),
|
|
343
|
+
);
|
|
344
|
+
// A run with transcript flags no one has cleared counts as unavailable and is named as held; read fresh, so a
|
|
345
|
+
// clearance made after the run takes effect when the aggregate is rebuilt.
|
|
346
|
+
const held = [];
|
|
347
|
+
for (const arm of arms) {
|
|
348
|
+
const flags = await unclearedFlags(runs[arm], path.join(groupDirectory, arm));
|
|
349
|
+
if (flags.length === 0) continue;
|
|
350
|
+
held.push(arm);
|
|
351
|
+
entry.held.push({ trial: result.id ?? null, arm, flags });
|
|
352
|
+
}
|
|
353
|
+
// One trial's missing or corrupt grade is counted, not allowed to discard every other trial's evidence.
|
|
354
|
+
const grade = await readJson(path.join(groupDirectory, group.artifacts?.grade ?? "deterministic-grade.json"));
|
|
355
|
+
if (grade === null) entry.unreadableGrades += 1;
|
|
356
|
+
else countGrade(entry, grade, new Set(held));
|
|
357
|
+
if (collectTrials) {
|
|
358
|
+
entry.perTrial.push({
|
|
359
|
+
id: result.id ?? null,
|
|
360
|
+
directory: groupDirectory,
|
|
361
|
+
state: group.state ?? null,
|
|
362
|
+
grade,
|
|
363
|
+
runs,
|
|
364
|
+
held,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
const aggregated = [];
|
|
370
|
+
for (const entry of groups.values()) {
|
|
371
|
+
const output = {
|
|
372
|
+
id: entry.id,
|
|
373
|
+
trials: entry.trials,
|
|
374
|
+
unreadableGrades: entry.unreadableGrades,
|
|
375
|
+
gradeErrors: entry.gradeErrors,
|
|
376
|
+
runStates: entry.runStates,
|
|
377
|
+
comparisons: [...entry.comparisons.values()].map((counts) => comparisonOutput(counts, entry.trials)),
|
|
378
|
+
// pass^k: the check passed in every one of the group's k trials.
|
|
379
|
+
checks: [...entry.checks.values()].map((check) => {
|
|
380
|
+
const counted = check.pass + check.fail + check.unavailable;
|
|
381
|
+
const filled = { ...check, unavailable: check.unavailable + Math.max(0, entry.trials - counted) };
|
|
382
|
+
const graded = filled.pass + filled.fail + filled.unavailable;
|
|
383
|
+
return { ...filled, trials: graded, passAll: graded > 0 && filled.pass === graded };
|
|
384
|
+
}),
|
|
385
|
+
};
|
|
386
|
+
if (entry.held.length > 0) output.held = entry.held;
|
|
387
|
+
if (collectTrials) {
|
|
388
|
+
Object.assign(
|
|
389
|
+
output,
|
|
390
|
+
await aggregateSections({
|
|
391
|
+
id: entry.id,
|
|
392
|
+
arms: Object.keys(entry.runStates),
|
|
393
|
+
reference: referenceArm(entry.definition),
|
|
394
|
+
trials: entry.perTrial,
|
|
395
|
+
}),
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
aggregated.push(output);
|
|
399
|
+
}
|
|
400
|
+
return aggregated;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async function readJson(file) {
|
|
404
|
+
try {
|
|
405
|
+
const value = JSON.parse(await readFile(file, "utf8"));
|
|
406
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
407
|
+
} catch {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Declared expectations become checks before any grade is read, so one that never graded is still counted. */
|
|
413
|
+
function seedChecks(entry) {
|
|
414
|
+
const expectations = Array.isArray(entry.definition?.expectations) ? entry.definition.expectations : [];
|
|
415
|
+
for (const expectation of expectations) {
|
|
416
|
+
if (typeof expectation?.id !== "string" || entry.checks.has(expectation.id)) continue;
|
|
417
|
+
entry.checks.set(expectation.id, {
|
|
418
|
+
id: expectation.id,
|
|
419
|
+
variant: expectation.variant,
|
|
420
|
+
kind: expectation.kind,
|
|
421
|
+
...zeroCounts(),
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function countGrade(entry, grade, held = new Set()) {
|
|
427
|
+
// A check that could not be graded is counted, so it cannot silently leave every denominator.
|
|
428
|
+
entry.gradeErrors += Array.isArray(grade.errors) ? grade.errors.length : 0;
|
|
429
|
+
for (const check of grade.checks ?? []) {
|
|
430
|
+
const counts = entry.checks.get(check.id) ?? {
|
|
431
|
+
id: check.id,
|
|
432
|
+
variant: check.variant,
|
|
433
|
+
kind: check.kind,
|
|
434
|
+
...zeroCounts(),
|
|
435
|
+
};
|
|
436
|
+
entry.checks.set(check.id, counts);
|
|
437
|
+
if (held.has(check.variant)) holdOne(counts);
|
|
438
|
+
else if (CHECK_STATES.includes(check.state)) counts[check.state] += 1;
|
|
439
|
+
}
|
|
440
|
+
for (const comparison of grade.comparisons ?? []) {
|
|
441
|
+
const { reference, arms, transitions } = comparisonArms(comparison);
|
|
442
|
+
let counts = entry.comparisons.get(comparison.id);
|
|
443
|
+
if (counts === undefined) {
|
|
444
|
+
counts = {
|
|
445
|
+
id: comparison.id,
|
|
446
|
+
kind: comparison.kind,
|
|
447
|
+
classic: comparison.arms === undefined,
|
|
448
|
+
reference,
|
|
449
|
+
arms: {},
|
|
450
|
+
outcomes: {},
|
|
451
|
+
recorded: 0,
|
|
452
|
+
};
|
|
453
|
+
entry.comparisons.set(comparison.id, counts);
|
|
454
|
+
}
|
|
455
|
+
counts.recorded += 1;
|
|
456
|
+
// A held run's outcome is unavailable here, so the pair it makes is incomplete.
|
|
457
|
+
const stateOf = (arm) => (held.has(arm) ? "held" : arms?.[arm]?.state);
|
|
458
|
+
for (const [arm, outcome] of Object.entries(arms ?? {})) {
|
|
459
|
+
const armCounts = (counts.arms[arm] ??= zeroCounts());
|
|
460
|
+
if (held.has(arm)) holdOne(armCounts);
|
|
461
|
+
else if (CHECK_STATES.includes(outcome?.state)) armCounts[outcome.state] += 1;
|
|
462
|
+
if (arm === reference) continue;
|
|
463
|
+
const versus = (counts.outcomes[arm] ??= { transitions: {}, pairs: zeroPairs(reference, arm) });
|
|
464
|
+
const transition = held.has(arm) || held.has(reference) ? "held" : transitions[arm];
|
|
465
|
+
versus.transitions[transition] = (versus.transitions[transition] ?? 0) + 1;
|
|
466
|
+
versus.pairs[pairOf(stateOf(reference), stateOf(arm), reference, arm)] += 1;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* A comparison's counts with every trial accounted for. A baseline/candidate comparison keeps its original shape
|
|
473
|
+
* (`baseline`, `candidate`, `transitions`, `pairs`); any other has `reference`, per-arm `arms`, and `outcomes`
|
|
474
|
+
* holding each non-reference arm's transitions and pairs against the reference.
|
|
475
|
+
*/
|
|
476
|
+
function comparisonOutput(counts, trials) {
|
|
477
|
+
const missing = Math.max(0, trials - counts.recorded);
|
|
478
|
+
for (const armCounts of Object.values(counts.arms)) {
|
|
479
|
+
armCounts.unavailable += Math.max(0, trials - (armCounts.pass + armCounts.fail + armCounts.unavailable));
|
|
480
|
+
}
|
|
481
|
+
for (const versus of Object.values(counts.outcomes)) {
|
|
482
|
+
if (missing > 0) versus.transitions.unavailable = (versus.transitions.unavailable ?? 0) + missing;
|
|
483
|
+
versus.pairs.incomplete += missing;
|
|
484
|
+
}
|
|
485
|
+
if (counts.classic) {
|
|
486
|
+
return {
|
|
487
|
+
id: counts.id,
|
|
488
|
+
kind: counts.kind,
|
|
489
|
+
baseline: counts.arms.baseline ?? zeroCounts(),
|
|
490
|
+
candidate: counts.arms.candidate ?? zeroCounts(),
|
|
491
|
+
transitions: counts.outcomes.candidate?.transitions ?? {},
|
|
492
|
+
pairs: counts.outcomes.candidate?.pairs ?? zeroPairs("baseline", "candidate"),
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
return {
|
|
496
|
+
id: counts.id,
|
|
497
|
+
kind: counts.kind,
|
|
498
|
+
reference: counts.reference,
|
|
499
|
+
arms: counts.arms,
|
|
500
|
+
outcomes: counts.outcomes,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/** One trial's pair of outcomes for an arm against the reference; anything but pass or fail on either side is incomplete. */
|
|
505
|
+
function pairOf(referenceState, armState, reference, arm) {
|
|
506
|
+
const graded = (state) => state === "pass" || state === "fail";
|
|
507
|
+
if (!graded(referenceState) || !graded(armState)) return "incomplete";
|
|
508
|
+
if (referenceState === "pass") return armState === "pass" ? "both-pass" : `${reference}-only`;
|
|
509
|
+
return armState === "pass" ? `${arm}-only` : "both-fail";
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function zeroPairs(reference, arm) {
|
|
513
|
+
return { "both-pass": 0, "both-fail": 0, [`${reference}-only`]: 0, [`${arm}-only`]: 0, incomplete: 0 };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function zeroCounts() {
|
|
517
|
+
return { pass: 0, fail: 0, unavailable: 0 };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** One held outcome: unavailable in every denominator and pass^k, and named in `held`. */
|
|
521
|
+
function holdOne(counts) {
|
|
522
|
+
counts.unavailable += 1;
|
|
523
|
+
counts.held = (counts.held ?? 0) + 1;
|
|
524
|
+
}
|