@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,418 @@
|
|
|
1
|
+
// A suite's decision rule: the measure, the two arms, the direction and the test, declared before the run.
|
|
2
|
+
// The unit is the case (one suite group): each case's trials are summarized per arm, the non-reference arm is
|
|
3
|
+
// better, worse or equal, and an exact two-sided sign test over the decided cases gives the verdict.
|
|
4
|
+
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
import { ARM_NAME } from "./arms.mjs";
|
|
8
|
+
|
|
9
|
+
const DECISION_FIELDS = new Set(["measure", "arms", "direction", "method", "alpha", "min_cases", "trials"]);
|
|
10
|
+
const SCHEDULE_FIELDS = ["initial", "max", "step"];
|
|
11
|
+
// Where each measure kind is read: comparisons from the deterministic counts, the others from aggregator sections.
|
|
12
|
+
const MEASURE_KINDS = { comparison: null, metric: "metrics", judged: "judged", score: "scores" };
|
|
13
|
+
const MEASURE_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/;
|
|
14
|
+
/**
|
|
15
|
+
* Metric names as decision rules and `perTrial` values name them (metrics.mjs records them); nested run.json fields
|
|
16
|
+
* use a dotted path.
|
|
17
|
+
*/
|
|
18
|
+
export const METRIC_NAMES = [
|
|
19
|
+
"durationMs",
|
|
20
|
+
"turns",
|
|
21
|
+
"toolCalls",
|
|
22
|
+
"toolErrors",
|
|
23
|
+
"tokens.input",
|
|
24
|
+
"tokens.output",
|
|
25
|
+
"tokens.cacheRead",
|
|
26
|
+
"tokens.cacheWrite",
|
|
27
|
+
"cost",
|
|
28
|
+
"cacheHitRate",
|
|
29
|
+
"peakContext",
|
|
30
|
+
"skillReads",
|
|
31
|
+
"skillReadTokens",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const DIRECTIONS = new Set(["higher", "lower"]);
|
|
35
|
+
const DEFAULTS = { method: "sign-test", alpha: 0.05, min_cases: 10 };
|
|
36
|
+
const VERDICT_TEXT = { beats: "beats", worse: "worse", "no-difference-shown": "no difference shown" };
|
|
37
|
+
|
|
38
|
+
function isPlainObject(value) {
|
|
39
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function isInteger(value, minimum) {
|
|
43
|
+
return Number.isInteger(value) && value >= minimum;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates a suite's `decision` and returns it with defaults applied (`method` sign-test, `alpha` 0.05,
|
|
48
|
+
* `min_cases` 10), which is the form that is hashed and recorded. `fail(message)` must throw.
|
|
49
|
+
*/
|
|
50
|
+
export function validateDecision(value, fail) {
|
|
51
|
+
if (!isPlainObject(value)) fail("suite.decision must be an object");
|
|
52
|
+
for (const key of Object.keys(value)) {
|
|
53
|
+
if (!DECISION_FIELDS.has(key)) fail(`suite.decision contains unsupported field ${key}`);
|
|
54
|
+
}
|
|
55
|
+
for (const key of ["measure", "arms", "direction"]) {
|
|
56
|
+
if (!Object.hasOwn(value, key)) fail(`suite.decision is missing ${key}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const measure = value.measure;
|
|
60
|
+
const kinds = isPlainObject(measure) ? Object.keys(measure) : [];
|
|
61
|
+
if (kinds.length !== 1 || !Object.hasOwn(MEASURE_KINDS, kinds[0])) {
|
|
62
|
+
fail("suite.decision.measure must have exactly one of comparison, metric, judged or score");
|
|
63
|
+
}
|
|
64
|
+
const [kind] = kinds;
|
|
65
|
+
if (typeof measure[kind] !== "string" || !MEASURE_NAME.test(measure[kind])) {
|
|
66
|
+
fail(`suite.decision.measure.${kind} must be a name of letters, numbers, dots, underscores or hyphens`);
|
|
67
|
+
}
|
|
68
|
+
if (kind === "metric" && !METRIC_NAMES.includes(measure.metric)) {
|
|
69
|
+
fail(`suite.decision.measure.metric must be one of ${METRIC_NAMES.join(", ")}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const arms = value.arms;
|
|
73
|
+
if (!Array.isArray(arms) || arms.length !== 2 || arms.some((arm) => typeof arm !== "string" || !ARM_NAME.test(arm))) {
|
|
74
|
+
fail("suite.decision.arms must be two arm names, the reference first");
|
|
75
|
+
}
|
|
76
|
+
if (arms[0] === arms[1]) fail("suite.decision.arms must name two different arms");
|
|
77
|
+
if (!DIRECTIONS.has(value.direction)) fail("suite.decision.direction must be higher or lower");
|
|
78
|
+
|
|
79
|
+
const method = value.method ?? DEFAULTS.method;
|
|
80
|
+
if (method !== "sign-test") fail("suite.decision.method must be sign-test");
|
|
81
|
+
const alpha = value.alpha ?? DEFAULTS.alpha;
|
|
82
|
+
if (typeof alpha !== "number" || !(alpha > 0 && alpha < 1)) fail("suite.decision.alpha must be between 0 and 1");
|
|
83
|
+
const minCases = value.min_cases ?? DEFAULTS.min_cases;
|
|
84
|
+
if (!isInteger(minCases, 1)) fail("suite.decision.min_cases must be a positive integer");
|
|
85
|
+
|
|
86
|
+
const rule = { measure: { [kind]: measure[kind] }, arms: [...arms], direction: value.direction, method, alpha };
|
|
87
|
+
Object.assign(rule, { min_cases: minCases });
|
|
88
|
+
if (Object.hasOwn(value, "trials")) {
|
|
89
|
+
const trials = value.trials;
|
|
90
|
+
if (!isPlainObject(trials)) fail("suite.decision.trials must be an object");
|
|
91
|
+
for (const key of Object.keys(trials)) {
|
|
92
|
+
if (!SCHEDULE_FIELDS.includes(key)) fail(`suite.decision.trials contains unsupported field ${key}`);
|
|
93
|
+
}
|
|
94
|
+
for (const key of SCHEDULE_FIELDS) {
|
|
95
|
+
if (!isInteger(trials[key], 1)) fail(`suite.decision.trials.${key} must be a positive integer`);
|
|
96
|
+
}
|
|
97
|
+
if (trials.max < trials.initial) fail("suite.decision.trials.max must be at least trials.initial");
|
|
98
|
+
rule.trials = { initial: trials.initial, max: trials.max, step: trials.step };
|
|
99
|
+
}
|
|
100
|
+
return rule;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Why a suite's decision could only exclude cases, decided by the definition alone, or null: a group without a
|
|
105
|
+
* decision arm; a group that records the measure in one decision arm but not the other; or a measure no group
|
|
106
|
+
* records in both. A group that records the measure in neither arm, such as a control case, stays in the suite
|
|
107
|
+
* and its case is excluded and counted. Checked when the suite loads, before any trial is paid for.
|
|
108
|
+
*
|
|
109
|
+
* @returns {{ group: any, message: string } | null} the group at fault, or null for the suite as a whole
|
|
110
|
+
*/
|
|
111
|
+
export function decisionProblem(rule, groups) {
|
|
112
|
+
const { kind, name } = measureOf(rule);
|
|
113
|
+
let recorded = false;
|
|
114
|
+
for (const group of groups) {
|
|
115
|
+
const missing = rule.arms.filter((arm) => !Object.hasOwn(group.variants ?? {}, arm));
|
|
116
|
+
if (missing.length > 0) {
|
|
117
|
+
return { group, message: `decision arm ${missing.join(", ")} is not an arm of group ${group.id}` };
|
|
118
|
+
}
|
|
119
|
+
const reporting = rule.arms.filter((arm) => recordsMeasure(group, arm, kind, name));
|
|
120
|
+
if (reporting.length === rule.arms.length) recorded = true;
|
|
121
|
+
else if (reporting.length > 0) {
|
|
122
|
+
const silent = rule.arms.filter((arm) => !reporting.includes(arm));
|
|
123
|
+
return {
|
|
124
|
+
group,
|
|
125
|
+
message: `group ${group.id} records ${kind} ${name} for arm ${reporting.join(", ")} but not ${silent.join(", ")}, so its case could only be excluded`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (recorded) return null;
|
|
130
|
+
return {
|
|
131
|
+
group: null,
|
|
132
|
+
message: `no group records ${kind} ${name} in both arms ${rule.arms.join(" and ")}, so every case would be excluded`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Whether a loaded group records the measure for an arm: every run records metrics; the rest need a declaration. */
|
|
137
|
+
function recordsMeasure(group, arm, kind, name) {
|
|
138
|
+
if (kind === "metric") return true;
|
|
139
|
+
if (kind === "judged") return (group.rubric ?? []).some((dimension) => dimension.id === name);
|
|
140
|
+
// A score is recorded per arm as `score.<id>` and `score.<comparison>`, from test-graded command checks only.
|
|
141
|
+
return (group.expectations ?? []).some(
|
|
142
|
+
(expectation) =>
|
|
143
|
+
expectation.variant === arm &&
|
|
144
|
+
(kind === "comparison"
|
|
145
|
+
? expectation.comparison === name
|
|
146
|
+
: expectation.kind === "command" &&
|
|
147
|
+
expectation.pass?.tests !== undefined &&
|
|
148
|
+
(expectation.id === name || expectation.comparison === name)),
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** The rule's identity: sha256 over its JSON with sorted keys, so the same rule always hashes the same. */
|
|
153
|
+
export function ruleSha256(rule) {
|
|
154
|
+
return createHash("sha256").update(canonicalJson(rule)).digest("hex");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function canonicalJson(value) {
|
|
158
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
|
|
159
|
+
if (isPlainObject(value)) {
|
|
160
|
+
const keys = Object.keys(value).sort();
|
|
161
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`;
|
|
162
|
+
}
|
|
163
|
+
return JSON.stringify(value);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function measureOf(rule) {
|
|
167
|
+
const [kind] = Object.keys(rule.measure);
|
|
168
|
+
return { kind, name: rule.measure[kind] };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* One case's usable values per decision arm, from an aggregate group entry. A comparison gives 1 per pass and 0
|
|
173
|
+
* per fail; a section's `perTrial` gives true as 1, false as 0, numbers as themselves. Null, unavailable and
|
|
174
|
+
* anything else are excluded and counted. `absent` names why the case has no such measure at all.
|
|
175
|
+
*/
|
|
176
|
+
function armValues(group, rule) {
|
|
177
|
+
const { kind, name } = measureOf(rule);
|
|
178
|
+
const arms = Object.fromEntries(rule.arms.map((arm) => [arm, { values: [], excluded: 0 }]));
|
|
179
|
+
if (kind === "comparison") {
|
|
180
|
+
const comparison = (group.comparisons ?? []).find((entry) => entry.id === name);
|
|
181
|
+
if (comparison === undefined) return { absent: `no comparison ${name}` };
|
|
182
|
+
for (const arm of rule.arms) {
|
|
183
|
+
const counts = comparison.arms !== undefined ? comparison.arms[arm] : comparison[arm];
|
|
184
|
+
if (!isPlainObject(counts)) continue;
|
|
185
|
+
arms[arm].values.push(...Array(counts.pass ?? 0).fill(1), ...Array(counts.fail ?? 0).fill(0));
|
|
186
|
+
arms[arm].excluded += counts.unavailable ?? 0;
|
|
187
|
+
}
|
|
188
|
+
return { arms };
|
|
189
|
+
}
|
|
190
|
+
const section = group[MEASURE_KINDS[kind]];
|
|
191
|
+
if (!isPlainObject(section) || !Array.isArray(section.perTrial)) {
|
|
192
|
+
return { absent: `no ${MEASURE_KINDS[kind]} section` };
|
|
193
|
+
}
|
|
194
|
+
for (const entry of section.perTrial) {
|
|
195
|
+
const slot = arms[entry?.arm];
|
|
196
|
+
if (slot === undefined) continue;
|
|
197
|
+
// A score measure names a command expectation or its comparison; the section records it as `score.<id>`.
|
|
198
|
+
const value = entry.values?.[kind === "score" ? `score.${name}` : name];
|
|
199
|
+
if (typeof value === "boolean") slot.values.push(value ? 1 : 0);
|
|
200
|
+
else if (typeof value === "number" && Number.isFinite(value)) slot.values.push(value);
|
|
201
|
+
else slot.excluded += 1;
|
|
202
|
+
}
|
|
203
|
+
return { arms };
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function summarize({ values, excluded }) {
|
|
207
|
+
const n = values.length;
|
|
208
|
+
return { n, mean: n === 0 ? null : values.reduce((sum, value) => sum + value, 0) / n, excluded };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** One case: each arm's summary and the non-reference arm's sign, plus what the adaptive schedule needs. */
|
|
212
|
+
function caseOutcome(group, rule) {
|
|
213
|
+
const [reference, arm] = rule.arms;
|
|
214
|
+
const found = armValues(group, rule);
|
|
215
|
+
const outcome = { id: group.id, trials: group.trials };
|
|
216
|
+
if (found.absent !== undefined) return { ...outcome, sign: "excluded", reason: found.absent, stable: true };
|
|
217
|
+
const summaries = { [reference]: summarize(found.arms[reference]), [arm]: summarize(found.arms[arm]) };
|
|
218
|
+
Object.assign(outcome, { arms: summaries });
|
|
219
|
+
if (summaries[reference].n === 0 || summaries[arm].n === 0) {
|
|
220
|
+
// More trials of an arm that produced no usable value are unlikely to help, so the schedule leaves it.
|
|
221
|
+
return { ...outcome, sign: "excluded", reason: "an arm has no usable value", stable: true };
|
|
222
|
+
}
|
|
223
|
+
const raw = summaries[arm].mean - summaries[reference].mean;
|
|
224
|
+
const difference = rule.direction === "higher" ? raw : -raw;
|
|
225
|
+
const scale = Math.max(1, Math.abs(summaries[arm].mean), Math.abs(summaries[reference].mean));
|
|
226
|
+
const equal = Math.abs(difference) <= 1e-12 * scale;
|
|
227
|
+
const sign = equal ? "equal" : difference > 0 ? "better" : "worse";
|
|
228
|
+
// Undecided: a tie, or a difference smaller than one trial's worth (the observed range over the fewer values).
|
|
229
|
+
const all = [...found.arms[reference].values, ...found.arms[arm].values];
|
|
230
|
+
const worth = (Math.max(...all) - Math.min(...all)) / Math.min(summaries[reference].n, summaries[arm].n);
|
|
231
|
+
return { ...outcome, sign, stable: !equal && Math.abs(raw) >= worth };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Exact two-sided binomial sign test with p = 1/2 over `better` and `worse`; ties are not passed in. */
|
|
235
|
+
export function signTest(better, worse) {
|
|
236
|
+
const n = better + worse;
|
|
237
|
+
if (n === 0) return 1;
|
|
238
|
+
const k = Math.min(better, worse);
|
|
239
|
+
if (n <= 1000) {
|
|
240
|
+
// Exact: the tail's binomial coefficients as integers over 2^n, rounded once.
|
|
241
|
+
let choose = 1n;
|
|
242
|
+
let sum = 0n;
|
|
243
|
+
for (let i = 0; i <= k; i += 1) {
|
|
244
|
+
if (i > 0) choose = (choose * BigInt(n - i + 1)) / BigInt(i);
|
|
245
|
+
sum += choose;
|
|
246
|
+
}
|
|
247
|
+
return Math.min(1, Number(2n * sum) / 2 ** n);
|
|
248
|
+
}
|
|
249
|
+
let logChoose = 0;
|
|
250
|
+
let tail = 0;
|
|
251
|
+
for (let i = 0; i <= k; i += 1) {
|
|
252
|
+
if (i > 0) logChoose += Math.log(n - i + 1) - Math.log(i);
|
|
253
|
+
tail += Math.exp(logChoose - n * Math.LN2);
|
|
254
|
+
}
|
|
255
|
+
return Math.min(1, 2 * tail);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The decision over an aggregate's groups. "beats" needs at least `min_cases` decided cases, p < alpha and the
|
|
260
|
+
* split in the declared direction; the same evidence the other way is "worse"; anything else is
|
|
261
|
+
* "no-difference-shown".
|
|
262
|
+
*/
|
|
263
|
+
export function decide(rule, groups) {
|
|
264
|
+
const cases = groups.map((group) => caseOutcome(group, rule));
|
|
265
|
+
const count = (sign) => cases.filter((entry) => entry.sign === sign).length;
|
|
266
|
+
const better = count("better");
|
|
267
|
+
const worse = count("worse");
|
|
268
|
+
const decided = better + worse;
|
|
269
|
+
const p = signTest(better, worse);
|
|
270
|
+
let verdict = "no-difference-shown";
|
|
271
|
+
if (decided >= rule.min_cases && p < rule.alpha && better !== worse) verdict = better > worse ? "beats" : "worse";
|
|
272
|
+
const excludedValues = cases.reduce(
|
|
273
|
+
(sum, entry) => sum + Object.values(entry.arms ?? {}).reduce((armSum, arm) => armSum + arm.excluded, 0),
|
|
274
|
+
0,
|
|
275
|
+
);
|
|
276
|
+
return {
|
|
277
|
+
rule,
|
|
278
|
+
ruleSha256: ruleSha256(rule),
|
|
279
|
+
reference: rule.arms[0],
|
|
280
|
+
arm: rule.arms[1],
|
|
281
|
+
verdict,
|
|
282
|
+
better,
|
|
283
|
+
worse,
|
|
284
|
+
ties: count("equal"),
|
|
285
|
+
decided,
|
|
286
|
+
excluded: count("excluded"),
|
|
287
|
+
excludedValues,
|
|
288
|
+
p,
|
|
289
|
+
cases: cases.map(({ stable, ...entry }) => entry),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Case IDs the adaptive schedule extends: tied or within one trial's worth, and below the schedule's maximum. */
|
|
294
|
+
export function undecidedCases(rule, groups) {
|
|
295
|
+
if (rule.trials === undefined) return [];
|
|
296
|
+
return groups
|
|
297
|
+
.map((group) => caseOutcome(group, rule))
|
|
298
|
+
.filter((entry) => !entry.stable && entry.trials < rule.trials.max)
|
|
299
|
+
.map((entry) => ({ id: entry.id, trials: entry.trials }));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The aggregate's claim when the suite declares a decision: the verdict reached, with its numbers and rule hash. */
|
|
303
|
+
export function decisionClaim(decision, { k, supported, reason }) {
|
|
304
|
+
return {
|
|
305
|
+
class: supported ? decision.verdict : "none",
|
|
306
|
+
...(reason ? { reason } : {}),
|
|
307
|
+
k,
|
|
308
|
+
ruleSha256: decision.ruleSha256,
|
|
309
|
+
better: decision.better,
|
|
310
|
+
worse: decision.worse,
|
|
311
|
+
ties: decision.ties,
|
|
312
|
+
decided: decision.decided,
|
|
313
|
+
excluded: decision.excluded,
|
|
314
|
+
p: decision.p,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Trials run against trials asked for. With an adaptive schedule the full invocations and the trials its rounds
|
|
320
|
+
* added are counted apart, so the header never presents added trials as requested ones.
|
|
321
|
+
*/
|
|
322
|
+
export function trialCounts(aggregate) {
|
|
323
|
+
const trials = Array.isArray(aggregate.trials) ? aggregate.trials : [];
|
|
324
|
+
if (aggregate.schedule === undefined) return `${trials.length}/${aggregate.requestedTrials}`;
|
|
325
|
+
const added = trials.filter((trial) => trial.round !== undefined).length;
|
|
326
|
+
const scheduled = (aggregate.schedule.rounds ?? []).reduce((sum, round) => sum + (round.scheduled ?? 0), 0);
|
|
327
|
+
return (
|
|
328
|
+
`${trials.length - added}/${aggregate.requestedTrials} initial, ${added}/${scheduled} adaptive` +
|
|
329
|
+
` (up to ${aggregate.schedule.max} per case)`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** A verdict's words: "beats", "worse" or "no difference shown". */
|
|
334
|
+
export function verdictText(verdict) {
|
|
335
|
+
return VERDICT_TEXT[verdict] ?? String(verdict);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The aggregate's claim in words, shared by the text and HTML views: `reached` is the class reached ("rate on this
|
|
340
|
+
* suite, k=3", "beats", "none (dry run)"), and `detail` what it is about (the decision's arms, measure and
|
|
341
|
+
* direction) or, without a decision, why no "beats" claim is supported. Null when the aggregate has no claim.
|
|
342
|
+
*/
|
|
343
|
+
export function claimSummary(aggregate) {
|
|
344
|
+
const claim = aggregate?.claim;
|
|
345
|
+
if (!claim) return null;
|
|
346
|
+
const none = `none${claim.reason ? ` (${claim.reason})` : ""}`;
|
|
347
|
+
const decision = aggregate.decision;
|
|
348
|
+
if (decision === undefined) {
|
|
349
|
+
return {
|
|
350
|
+
reached: claim.class === "rate-on-suite" ? `rate on this suite, k=${claim.k}` : none,
|
|
351
|
+
detail: `no "beats" claim: ${String(claim.beats).replace(/^not supported: /, "")}`,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
const { kind, name } = measureOf(decision.rule);
|
|
355
|
+
return {
|
|
356
|
+
reached: claim.class === "none" ? none : verdictText(claim.class),
|
|
357
|
+
detail: `${decision.arm} against ${decision.reference} on ${kind} ${name}, ${decision.rule.direction} is better`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** The claim lines of an aggregate view: the rate claim without a decision, else the rule, split, p and verdict. */
|
|
362
|
+
export function renderClaim(aggregate) {
|
|
363
|
+
const summary = claimSummary(aggregate);
|
|
364
|
+
if (summary === null) return [];
|
|
365
|
+
const decision = aggregate.decision;
|
|
366
|
+
if (decision === undefined) return [`Claim: ${summary.reached}; ${summary.detail}`];
|
|
367
|
+
const rule = decision.rule;
|
|
368
|
+
const lines = [
|
|
369
|
+
`Claim: ${summary.reached}; ${summary.detail}`,
|
|
370
|
+
row("decision", [
|
|
371
|
+
`rule=sha256:${decision.ruleSha256}`,
|
|
372
|
+
`method=${rule.method}`,
|
|
373
|
+
`alpha=${formatNumber(rule.alpha)}`,
|
|
374
|
+
`min-cases=${rule.min_cases}`,
|
|
375
|
+
...(rule.trials ? [`trials=${rule.trials.initial}..${rule.trials.max} step ${rule.trials.step}`] : []),
|
|
376
|
+
]),
|
|
377
|
+
row("decision", [
|
|
378
|
+
`verdict=${VERDICT_TEXT[decision.verdict]}`,
|
|
379
|
+
`better=${decision.better}`,
|
|
380
|
+
`worse=${decision.worse}`,
|
|
381
|
+
`ties=${decision.ties}`,
|
|
382
|
+
`decided=${decision.decided}`,
|
|
383
|
+
`excluded=${decision.excluded}`,
|
|
384
|
+
`excluded-values=${decision.excludedValues}`,
|
|
385
|
+
`p=${formatNumber(decision.p)}`,
|
|
386
|
+
]),
|
|
387
|
+
];
|
|
388
|
+
for (const round of aggregate.schedule?.rounds ?? []) {
|
|
389
|
+
lines.push(
|
|
390
|
+
row("adaptive", [
|
|
391
|
+
`round=${round.round}`,
|
|
392
|
+
`trials=${round.trials}/${round.scheduled}`,
|
|
393
|
+
`cases=${round.groups.join(",")}`,
|
|
394
|
+
]),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
for (const entry of decision.cases) {
|
|
398
|
+
const arms = Object.entries(entry.arms ?? {}).map(
|
|
399
|
+
([arm, summary]) => `${arm}=${summary.mean === null ? "-" : formatNumber(summary.mean)} (n=${summary.n})`,
|
|
400
|
+
);
|
|
401
|
+
lines.push(
|
|
402
|
+
row("case", [entry.id, entry.sign, `trials=${entry.trials}`, ...arms, ...(entry.reason ? [entry.reason] : [])]),
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
return lines;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function row(kind, values) {
|
|
409
|
+
return [kind, ...values]
|
|
410
|
+
.map((value) =>
|
|
411
|
+
String(value).replace(/\\/g, "\\\\").replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t"),
|
|
412
|
+
)
|
|
413
|
+
.join("\t");
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function formatNumber(value) {
|
|
417
|
+
return Number.isInteger(value) ? String(value) : String(Number(value.toFixed(8)));
|
|
418
|
+
}
|