@hassangameryt/freeflow 0.2.0 → 0.4.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 +1 -1
- package/.claude-plugin/marketplace.json +4 -4
- package/{plugins/freeflow/.claude-plugin → .claude-plugin}/plugin.json +5 -2
- package/{plugins/freeflow/.codex-plugin → .codex-plugin}/plugin.json +8 -9
- package/README.md +194 -127
- package/assets/feedback-loop.png +0 -0
- package/assets/freeflow-plugin-icon.png +0 -0
- package/assets/freeflow_workflow_map.png +0 -0
- package/assets/workflow.png +0 -0
- package/command-surface.json +129 -0
- package/hooks/freeflow-runtime-context.mjs +419 -0
- package/hooks/tests/check-runtime-context-hook.sh +223 -0
- package/package.json +41 -22
- package/pi-extension/dist/index.js +269 -0
- package/pi-extension/dist/integrations/mcp-config.js +29 -0
- package/pi-extension/dist/output-router/host-producer-identification.js +232 -0
- package/pi-extension/dist/output-router/native-safety-net.js +216 -0
- package/pi-extension/dist/output-router/observed-tool-routing.js +122 -0
- package/pi-extension/dist/output-router/renderers.js +1023 -0
- package/pi-extension/dist/output-router/router-tools.js +554 -0
- package/pi-extension/dist/output-router/schemas.js +383 -0
- package/pi-extension/dist/output-router/status.js +744 -0
- package/pi-extension/dist/output-router/utils.js +502 -0
- package/pi-extension/dist/runtime/runtime-context.js +798 -0
- package/pi-extension/dist/settings/settings-tui.js +314 -0
- package/pi-extension/dist/settings/settings-ui.js +1487 -0
- package/pi-extension/freeflow/index.js +1 -0
- package/pi-extension/src/index.ts +295 -0
- package/pi-extension/src/integrations/mcp-config.ts +32 -0
- package/pi-extension/src/output-router/host-producer-identification.ts +256 -0
- package/pi-extension/src/output-router/native-safety-net.ts +248 -0
- package/pi-extension/src/output-router/observed-tool-routing.ts +134 -0
- package/pi-extension/src/output-router/renderers.ts +1108 -0
- package/pi-extension/src/output-router/router-tools.ts +579 -0
- package/pi-extension/src/output-router/schemas.ts +387 -0
- package/pi-extension/src/output-router/status.ts +791 -0
- package/pi-extension/src/output-router/utils.ts +554 -0
- package/pi-extension/src/runtime/runtime-context.ts +895 -0
- package/pi-extension/src/settings/settings-tui.ts +408 -0
- package/pi-extension/src/settings/settings-ui.ts +1730 -0
- package/pi-extension/tests/integration/pi-extension.test.js +4018 -0
- package/pi-extension/tests/output-router/observed-producers.test.js +132 -0
- package/pi-extension/tests/output-router/search.test.js +722 -0
- package/pi-extension/tests/settings/settings-tui.test.js +243 -0
- package/pi-extension/tsconfig.json +16 -0
- package/{plugins/freeflow/router/dist → router/dist/benchmarks}/benchmark-harness.d.ts +16 -13
- package/router/dist/benchmarks/benchmark-harness.js +92 -0
- package/router/dist/benchmarks/benchmarks.d.ts +109 -0
- package/router/dist/benchmarks/benchmarks.js +979 -0
- package/router/dist/benchmarks/codex-qa-benchmarks.d.ts +95 -0
- package/router/dist/benchmarks/codex-qa-benchmarks.js +500 -0
- package/router/dist/benchmarks/command-benchmarks.d.ts +143 -0
- package/router/dist/benchmarks/command-benchmarks.js +698 -0
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.d.ts +82 -0
- package/router/dist/benchmarks/context-mode-normalized-benchmarks.js +773 -0
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.d.ts +132 -0
- package/router/dist/benchmarks/context-mode-real-deep-benchmark.js +1380 -0
- package/router/dist/benchmarks/context-saving-baseline.d.ts +60 -0
- package/router/dist/benchmarks/context-saving-baseline.js +470 -0
- package/router/dist/benchmarks/index-benchmarks.d.ts +103 -0
- package/router/dist/benchmarks/index-benchmarks.js +862 -0
- package/router/dist/benchmarks/storage-policy-benchmarks.d.ts +101 -0
- package/router/dist/benchmarks/storage-policy-benchmarks.js +618 -0
- package/router/dist/benchmarks/vault-index-benchmarks.d.ts +56 -0
- package/router/dist/benchmarks/vault-index-benchmarks.js +322 -0
- package/router/dist/config/config.d.ts +103 -0
- package/router/dist/config/config.js +632 -0
- package/{plugins/freeflow/router/dist → router/dist/config}/router-contract.d.ts +2 -2
- package/router/dist/config/router-contract.js +54 -0
- package/{plugins/freeflow/router/dist → router/dist/config}/schema.d.ts +19 -10
- package/router/dist/config/schema.js +648 -0
- package/router/dist/config/types.d.ts +573 -0
- package/router/dist/config/types.js +41 -0
- package/router/dist/evidence/bounded-evidence.d.ts +40 -0
- package/router/dist/evidence/bounded-evidence.js +323 -0
- package/router/dist/evidence/evidence-range-selector.d.ts +24 -0
- package/router/dist/evidence/evidence-range-selector.js +232 -0
- package/router/dist/evidence/evidence-search.d.ts +27 -0
- package/router/dist/evidence/evidence-search.js +490 -0
- package/{plugins/freeflow/router/dist → router/dist/evidence}/evidence.d.ts +18 -18
- package/router/dist/evidence/evidence.js +200 -0
- package/router/dist/evidence/failure-contracts.d.ts +37 -0
- package/router/dist/evidence/failure-contracts.js +166 -0
- package/router/dist/evidence/line-ranges.d.ts +24 -0
- package/router/dist/evidence/line-ranges.js +22 -0
- package/router/dist/experiments/local-index.d.ts +64 -0
- package/router/dist/experiments/local-index.js +581 -0
- package/router/dist/index.d.ts +30 -0
- package/router/dist/index.js +32 -0
- package/router/dist/local/local-traversal.d.ts +18 -0
- package/router/dist/local/local-traversal.js +334 -0
- package/router/dist/processing/engine.d.ts +132 -0
- package/router/dist/processing/engine.js +533 -0
- package/router/dist/processing/reducers.d.ts +280 -0
- package/router/dist/processing/reducers.js +1673 -0
- package/router/dist/processing/renderers.d.ts +31 -0
- package/router/dist/processing/renderers.js +195 -0
- package/router/dist/processing/scripts.d.ts +91 -0
- package/router/dist/processing/scripts.js +386 -0
- package/{plugins/freeflow/router/dist → router/dist/repo}/repo-traversal.d.ts +9 -9
- package/router/dist/repo/repo-traversal.js +245 -0
- package/router/dist/routing/observed-reducers.d.ts +41 -0
- package/router/dist/routing/observed-reducers.js +661 -0
- package/router/dist/routing/observed-routing.d.ts +26 -0
- package/router/dist/routing/observed-routing.js +378 -0
- package/router/dist/routing/parsers.d.ts +16 -0
- package/router/dist/routing/parsers.js +521 -0
- package/router/dist/routing/run-filters.d.ts +60 -0
- package/router/dist/routing/run-filters.js +238 -0
- package/router/dist/routing/run-reducers.d.ts +49 -0
- package/router/dist/routing/run-reducers.js +122 -0
- package/router/dist/sandbox/adapter-roots.d.ts +46 -0
- package/router/dist/sandbox/adapter-roots.js +66 -0
- package/router/dist/sandbox/eryx-python-adapter.d.ts +13 -0
- package/router/dist/sandbox/eryx-python-adapter.js +868 -0
- package/router/dist/sandbox/jq-wasm-adapter.d.ts +9 -0
- package/router/dist/sandbox/jq-wasm-adapter.js +429 -0
- package/router/dist/sandbox/quickjs-wasi-adapter.d.ts +13 -0
- package/router/dist/sandbox/quickjs-wasi-adapter.js +384 -0
- package/router/dist/sandbox/script-sandbox.d.ts +134 -0
- package/router/dist/sandbox/script-sandbox.js +337 -0
- package/router/dist/setup/script-transform-adapters.d.ts +32 -0
- package/router/dist/setup/script-transform-adapters.js +278 -0
- package/router/dist/tools/batch.d.ts +33 -0
- package/router/dist/tools/batch.js +586 -0
- package/router/dist/tools/run.d.ts +62 -0
- package/router/dist/tools/run.js +1304 -0
- package/router/dist/tools/search.d.ts +66 -0
- package/router/dist/tools/search.js +1364 -0
- package/router/dist/transform/engine.d.ts +168 -0
- package/router/dist/transform/engine.js +2072 -0
- package/router/dist/vault/vault-index.d.ts +125 -0
- package/router/dist/vault/vault-index.js +416 -0
- package/router/dist/vault/vault.d.ts +138 -0
- package/router/dist/vault/vault.js +630 -0
- package/runtime/interaction-contract.md +16 -0
- package/skills/bypass/SKILL.md +82 -0
- package/skills/commit-work/SKILL.md +106 -0
- package/skills/commit-work/references/staging-decisions.md +73 -0
- package/skills/decision-gate/SKILL.md +48 -0
- package/skills/design-for-depth/SKILL.md +106 -0
- package/skills/design-for-depth/references/design-pressure-signals.md +314 -0
- package/skills/design-for-depth/references/interface-design-loop.md +156 -0
- package/skills/design-for-depth/references/software-design-philosophy.md +248 -0
- package/skills/diagnose-failure/SKILL.md +105 -0
- package/skills/diagnose-failure/references/diagnostic-loop-catalog.md +36 -0
- package/skills/diagnose-failure/references/flaky-and-performance.md +43 -0
- package/skills/discuss/SKILL.md +121 -0
- package/skills/discuss/references/checkpoints.md +66 -0
- package/skills/evaluate-skill/SKILL.md +105 -0
- package/skills/evaluate-skill/references/definition-schema.md +345 -0
- package/skills/evaluate-skill/references/evaluation-design.md +86 -0
- package/skills/evaluate-skill/references/execution-and-evidence.md +85 -0
- package/skills/evaluate-skill/references/review-and-revision.md +51 -0
- package/skills/evaluate-skill/scripts/lib/definitions.mjs +296 -0
- package/skills/evaluate-skill/scripts/lib/evidence.mjs +46 -0
- package/skills/evaluate-skill/scripts/lib/grade-comparison.mjs +60 -0
- package/skills/evaluate-skill/scripts/lib/grade-interaction.mjs +148 -0
- package/skills/evaluate-skill/scripts/lib/grade-support.mjs +126 -0
- package/skills/evaluate-skill/scripts/lib/grade-workspace.mjs +192 -0
- package/skills/evaluate-skill/scripts/lib/grade.mjs +56 -0
- package/skills/evaluate-skill/scripts/lib/pi.mjs +489 -0
- package/skills/evaluate-skill/scripts/lib/process.mjs +79 -0
- package/skills/evaluate-skill/scripts/lib/rpc.mjs +444 -0
- package/skills/evaluate-skill/scripts/lib/runner.mjs +346 -0
- package/skills/evaluate-skill/scripts/lib/sandbox.mjs +409 -0
- package/skills/evaluate-skill/scripts/lib/view.mjs +337 -0
- package/skills/evaluate-skill/scripts/pi-guard.mjs +164 -0
- package/skills/evaluate-skill/scripts/skill-eval.mjs +101 -0
- package/skills/execute-work/SKILL.md +99 -0
- package/skills/execute-work/references/code-practices.md +78 -0
- package/skills/execute-work/references/execution-loop.md +77 -0
- package/skills/finish-branch/SKILL.md +117 -0
- package/skills/finish-branch/references/integration-options.md +80 -0
- package/skills/handoff/SKILL.md +92 -0
- package/skills/handoff/references/templates.md +141 -0
- package/skills/launch-work/SKILL.md +116 -0
- package/skills/launch-work/references/launch-readiness.md +85 -0
- package/skills/migration-work/SKILL.md +115 -0
- package/skills/migration-work/references/migration-lifecycle.md +82 -0
- package/skills/mode-contract/SKILL.md +82 -0
- package/skills/output-router/SKILL.md +206 -0
- package/skills/output-router/references/safety-policy.md +40 -0
- package/skills/release-work/SKILL.md +118 -0
- package/skills/release-work/references/release-evidence.md +78 -0
- package/skills/review-artifact/SKILL.md +126 -0
- package/skills/review-artifact/references/reviewer-prompt.md +148 -0
- package/skills/review-work/SKILL.md +126 -0
- package/skills/review-work/references/reviewer-prompt.md +134 -0
- package/skills/review-work/references/security-risk-lens.md +67 -0
- package/skills/setup-freeflow/SKILL.md +98 -0
- package/skills/setup-freeflow/references/activation-contract.md +120 -0
- package/skills/setup-freeflow/references/host-setup.md +72 -0
- package/skills/setup-freeflow/references/output-router-setup.md +110 -0
- package/skills/simplify-code/SKILL.md +80 -0
- package/skills/simplify-code/references/simplification-patterns.md +70 -0
- package/skills/tdd/SKILL.md +96 -0
- package/skills/tdd/references/test-design.md +96 -0
- package/skills/track-work/SKILL.md +84 -0
- package/skills/track-work/references/working-record-schema.md +378 -0
- package/skills/verify-work/SKILL.md +85 -0
- package/skills/verify-work/references/browser-runtime-evidence.md +55 -0
- package/skills/verify-work/references/integration-evidence.md +51 -0
- package/skills/verify-work/references/performance-evidence.md +57 -0
- package/skills/workflow/SKILL.md +91 -0
- package/skills/workflow/references/domain-skill-composition.md +57 -0
- package/skills/workflow/references/workflow-loop.md +92 -0
- package/skills/write-plan/SKILL.md +123 -0
- package/skills/write-plan/references/plan-shapes.md +156 -0
- package/skills/write-skill/SKILL.md +119 -0
- package/skills/write-skill/references/activation-boundaries.md +62 -0
- package/skills/write-skill/references/agent-first-instructions.md +87 -0
- package/skills/write-skill/references/progressive-disclosure.md +63 -0
- package/skills/write-skill/scripts/lib/skill-author.mjs +653 -0
- package/skills/write-skill/scripts/skill-author.mjs +104 -0
- package/skills/write-spec/SKILL.md +104 -0
- package/skills/write-spec/references/artifact-standards.md +86 -0
- package/skills/write-spec/references/decision-records.md +77 -0
- package/skills/write-spec/references/spec-shapes.md +128 -0
- package/plugins/freeflow/command-surface.json +0 -77
- package/plugins/freeflow/docs/README.md +0 -40
- package/plugins/freeflow/docs/adr/0001-three-modes.md +0 -27
- package/plugins/freeflow/docs/adr/0002-evals-before-hooks.md +0 -17
- package/plugins/freeflow/docs/adr/0003-release-boundary.md +0 -21
- package/plugins/freeflow/docs/adr/0004-research-replaces-shallow-discovery-skills.md +0 -26
- package/plugins/freeflow/docs/adr/README.md +0 -8
- package/plugins/freeflow/docs/architecture.md +0 -86
- package/plugins/freeflow/docs/output-router.md +0 -212
- package/plugins/freeflow/docs/release-evidence.md +0 -69
- package/plugins/freeflow/docs/skills.md +0 -43
- package/plugins/freeflow/docs/workflow.md +0 -57
- package/plugins/freeflow/hooks/freeflow-runtime-context.mjs +0 -257
- package/plugins/freeflow/pi-extension/index.js +0 -1176
- package/plugins/freeflow/router/dist/benchmark-harness.js +0 -92
- package/plugins/freeflow/router/dist/benchmarks.d.ts +0 -102
- package/plugins/freeflow/router/dist/benchmarks.js +0 -880
- package/plugins/freeflow/router/dist/bounded-evidence.d.ts +0 -40
- package/plugins/freeflow/router/dist/bounded-evidence.js +0 -310
- package/plugins/freeflow/router/dist/codex-qa-benchmarks.d.ts +0 -91
- package/plugins/freeflow/router/dist/codex-qa-benchmarks.js +0 -461
- package/plugins/freeflow/router/dist/command-benchmarks.d.ts +0 -128
- package/plugins/freeflow/router/dist/command-benchmarks.js +0 -652
- package/plugins/freeflow/router/dist/config.d.ts +0 -22
- package/plugins/freeflow/router/dist/config.js +0 -114
- package/plugins/freeflow/router/dist/evidence-range-selector.d.ts +0 -22
- package/plugins/freeflow/router/dist/evidence-range-selector.js +0 -180
- package/plugins/freeflow/router/dist/evidence-search.d.ts +0 -25
- package/plugins/freeflow/router/dist/evidence-search.js +0 -450
- package/plugins/freeflow/router/dist/evidence.js +0 -196
- package/plugins/freeflow/router/dist/experimental-local-index.d.ts +0 -1
- package/plugins/freeflow/router/dist/experimental-local-index.js +0 -1
- package/plugins/freeflow/router/dist/experiments/local-index.d.ts +0 -58
- package/plugins/freeflow/router/dist/experiments/local-index.js +0 -562
- package/plugins/freeflow/router/dist/index-benchmarks.d.ts +0 -84
- package/plugins/freeflow/router/dist/index-benchmarks.js +0 -458
- package/plugins/freeflow/router/dist/index.d.ts +0 -12
- package/plugins/freeflow/router/dist/index.js +0 -12
- package/plugins/freeflow/router/dist/line-ranges.d.ts +0 -22
- package/plugins/freeflow/router/dist/line-ranges.js +0 -22
- package/plugins/freeflow/router/dist/parsers.d.ts +0 -16
- package/plugins/freeflow/router/dist/parsers.js +0 -473
- package/plugins/freeflow/router/dist/repo-traversal.js +0 -242
- package/plugins/freeflow/router/dist/retrieve.d.ts +0 -33
- package/plugins/freeflow/router/dist/retrieve.js +0 -855
- package/plugins/freeflow/router/dist/router-contract.js +0 -54
- package/plugins/freeflow/router/dist/run.d.ts +0 -26
- package/plugins/freeflow/router/dist/run.js +0 -356
- package/plugins/freeflow/router/dist/schema.js +0 -354
- package/plugins/freeflow/router/dist/types.d.ts +0 -234
- package/plugins/freeflow/router/dist/types.js +0 -9
- package/plugins/freeflow/router/dist/vault.d.ts +0 -80
- package/plugins/freeflow/router/dist/vault.js +0 -484
- package/plugins/freeflow/skills/bypass/SKILL.md +0 -68
- package/plugins/freeflow/skills/commit-work/SKILL.md +0 -98
- package/plugins/freeflow/skills/commit-work/references/staging-decisions.md +0 -51
- package/plugins/freeflow/skills/diagnose-failure/SKILL.md +0 -95
- package/plugins/freeflow/skills/diagnose-failure/references/feedback-loop-catalog.md +0 -25
- package/plugins/freeflow/skills/diagnose-failure/references/flaky-and-performance.md +0 -31
- package/plugins/freeflow/skills/evaluate-skill/SKILL.md +0 -96
- package/plugins/freeflow/skills/evaluate-skill/references/eval-patterns.md +0 -41
- package/plugins/freeflow/skills/evaluate-skill/references/grading-priority.md +0 -33
- package/plugins/freeflow/skills/execute-plan/SKILL.md +0 -132
- package/plugins/freeflow/skills/execute-plan/references/execution-map.md +0 -122
- package/plugins/freeflow/skills/handoff/SKILL.md +0 -66
- package/plugins/freeflow/skills/handoff/references/templates.md +0 -64
- package/plugins/freeflow/skills/interview-gate/SKILL.md +0 -90
- package/plugins/freeflow/skills/mode-contract/SKILL.md +0 -79
- package/plugins/freeflow/skills/output-router/SKILL.md +0 -69
- package/plugins/freeflow/skills/output-router/references/safety-policy.md +0 -33
- package/plugins/freeflow/skills/research/ARTIFACT-DESTINATIONS.md +0 -58
- package/plugins/freeflow/skills/research/CHECKPOINTS.md +0 -74
- package/plugins/freeflow/skills/research/LANGUAGE.md +0 -61
- package/plugins/freeflow/skills/research/SKILL.md +0 -128
- package/plugins/freeflow/skills/review-artifact/SKILL.md +0 -115
- package/plugins/freeflow/skills/review-artifact/references/reviewer-prompt.md +0 -56
- package/plugins/freeflow/skills/review-work/SKILL.md +0 -96
- package/plugins/freeflow/skills/review-work/references/reviewer-prompt.md +0 -97
- package/plugins/freeflow/skills/setup-freeflow/SKILL.md +0 -94
- package/plugins/freeflow/skills/setup-freeflow/references/activation-contract.md +0 -99
- package/plugins/freeflow/skills/setup-freeflow/references/host-setup.md +0 -35
- package/plugins/freeflow/skills/setup-freeflow/references/output-router-setup.md +0 -52
- package/plugins/freeflow/skills/verify-work/SKILL.md +0 -62
- package/plugins/freeflow/skills/workflow/SKILL.md +0 -95
- package/plugins/freeflow/skills/workflow/references/workflow-map.md +0 -102
- package/plugins/freeflow/skills/write-plan/SKILL.md +0 -88
- package/plugins/freeflow/skills/write-plan/references/plan-shapes.md +0 -70
- package/plugins/freeflow/skills/write-skill/SKILL.md +0 -88
- package/plugins/freeflow/skills/write-spec/SKILL.md +0 -90
- package/plugins/freeflow/skills/write-spec/references/artifact-standards.md +0 -67
- package/plugins/freeflow/skills/write-spec/references/spec-shapes.md +0 -69
- /package/{plugins/freeflow/assets → assets}/freeflow-plugin-icon-transparent.png +0 -0
- /package/{plugins/freeflow/hooks → hooks}/hooks.json +0 -0
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
const ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
5
|
+
const EVALUATION_TYPES = new Set(["description", "body", "end-to-end"]);
|
|
6
|
+
const VARIANTS = ["baseline", "candidate"];
|
|
7
|
+
|
|
8
|
+
export class DefinitionError extends Error {
|
|
9
|
+
constructor(code, message, definitionPath = null) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = "DefinitionError";
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.definitionPath = definitionPath;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fail(code, message, definitionPath) {
|
|
18
|
+
throw new DefinitionError(code, message, definitionPath);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isPlainObject(value) {
|
|
22
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function requireObject(value, label, definitionPath) {
|
|
26
|
+
if (!isPlainObject(value)) fail("invalid-definition", `${label} must be an object`, definitionPath);
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function requireExactKeys(value, required, optional, label, definitionPath) {
|
|
31
|
+
const object = requireObject(value, label, definitionPath);
|
|
32
|
+
const allowed = new Set([...required, ...optional]);
|
|
33
|
+
for (const key of required) {
|
|
34
|
+
if (!Object.hasOwn(object, key)) fail("invalid-definition", `${label} is missing ${key}`, definitionPath);
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(object)) {
|
|
37
|
+
if (!allowed.has(key)) fail("invalid-definition", `${label} contains unsupported field ${key}`, definitionPath);
|
|
38
|
+
}
|
|
39
|
+
return object;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function requireId(value, label, definitionPath) {
|
|
43
|
+
if (typeof value !== "string" || value.length > 64 || !ID.test(value)) {
|
|
44
|
+
fail(
|
|
45
|
+
"invalid-definition",
|
|
46
|
+
`${label} must start with a lowercase letter and use lowercase letters, numbers, or single hyphens up to 64 characters`,
|
|
47
|
+
definitionPath,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
return value;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireString(value, label, definitionPath) {
|
|
54
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
55
|
+
fail("invalid-definition", `${label} must be a non-empty string`, definitionPath);
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requireStringArray(value, label, definitionPath, { paths = false, unique = false } = {}) {
|
|
61
|
+
if (!Array.isArray(value)) fail("invalid-definition", `${label} must be an array`, definitionPath);
|
|
62
|
+
const seen = new Set();
|
|
63
|
+
return value.map((entry, index) => {
|
|
64
|
+
const string = requireString(entry, `${label}[${index}]`, definitionPath);
|
|
65
|
+
if (paths) requireSafeRelativePath(string, `${label}[${index}]`, definitionPath);
|
|
66
|
+
if (unique && seen.has(string)) {
|
|
67
|
+
fail("invalid-definition", `${label} contains duplicate ${string}`, definitionPath);
|
|
68
|
+
}
|
|
69
|
+
seen.add(string);
|
|
70
|
+
return string;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function requireSafeRelativePath(value, label, definitionPath) {
|
|
75
|
+
const string = requireString(value, label, definitionPath);
|
|
76
|
+
const normalized = path.normalize(string);
|
|
77
|
+
if (path.isAbsolute(string) || normalized === "." || normalized === ".." || normalized.startsWith(`..${path.sep}`)) {
|
|
78
|
+
fail("unsafe-path", `${label} must stay inside the definition root`, definitionPath);
|
|
79
|
+
}
|
|
80
|
+
return string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function requireSuiteReference(value, label, definitionPath) {
|
|
84
|
+
const string = requireString(value, label, definitionPath);
|
|
85
|
+
if (path.isAbsolute(string) || path.normalize(string) === ".") {
|
|
86
|
+
fail("unsafe-path", `${label} must be relative to the suite file`, definitionPath);
|
|
87
|
+
}
|
|
88
|
+
return string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function validateInput(value, definitionPath) {
|
|
92
|
+
const input = requireExactKeys(value, [], ["prompt", "turns"], "group.input", definitionPath);
|
|
93
|
+
const hasPrompt = Object.hasOwn(input, "prompt");
|
|
94
|
+
const hasTurns = Object.hasOwn(input, "turns");
|
|
95
|
+
if (hasPrompt === hasTurns) {
|
|
96
|
+
fail("invalid-definition", "group.input must contain exactly one of prompt or turns", definitionPath);
|
|
97
|
+
}
|
|
98
|
+
if (hasPrompt) requireString(input.prompt, "group.input.prompt", definitionPath);
|
|
99
|
+
if (hasTurns) {
|
|
100
|
+
const turns = requireStringArray(input.turns, "group.input.turns", definitionPath);
|
|
101
|
+
if (turns.length === 0) fail("invalid-definition", "group.input.turns must not be empty", definitionPath);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function validateSource(value, label, definitionPath) {
|
|
106
|
+
const source = requireObject(value, label, definitionPath);
|
|
107
|
+
if (source.kind === "working-tree") {
|
|
108
|
+
requireExactKeys(source, ["kind"], [], label, definitionPath);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (source.kind === "git") {
|
|
112
|
+
requireExactKeys(source, ["kind", "ref"], [], label, definitionPath);
|
|
113
|
+
requireString(source.ref, `${label}.ref`, definitionPath);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
fail("invalid-definition", `${label}.kind must be working-tree or git`, definitionPath);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function validateEnvironment(value, label, definitionPath) {
|
|
120
|
+
const environment = requireExactKeys(value, ["source", "skills", "target", "context"], [], label, definitionPath);
|
|
121
|
+
validateSource(environment.source, `${label}.source`, definitionPath);
|
|
122
|
+
const skills = requireStringArray(environment.skills, `${label}.skills`, definitionPath, { paths: true });
|
|
123
|
+
requireStringArray(environment.context, `${label}.context`, definitionPath, { paths: true });
|
|
124
|
+
if (
|
|
125
|
+
environment.target !== null &&
|
|
126
|
+
(!Number.isInteger(environment.target) || environment.target < 0 || environment.target >= skills.length)
|
|
127
|
+
) {
|
|
128
|
+
fail("invalid-definition", `${label}.target must be null or an index into skills`, definitionPath);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function validateExpectations(value, definitionPath) {
|
|
133
|
+
if (!Array.isArray(value)) fail("invalid-definition", "group.expectations must be an array", definitionPath);
|
|
134
|
+
const ids = new Set();
|
|
135
|
+
for (const [index, entry] of value.entries()) {
|
|
136
|
+
const expectation = requireObject(entry, `group.expectations[${index}]`, definitionPath);
|
|
137
|
+
requireId(expectation.id, `group.expectations[${index}].id`, definitionPath);
|
|
138
|
+
requireString(expectation.kind, `group.expectations[${index}].kind`, definitionPath);
|
|
139
|
+
if (ids.has(expectation.id)) {
|
|
140
|
+
fail("invalid-definition", `group.expectations contains duplicate ID ${expectation.id}`, definitionPath);
|
|
141
|
+
}
|
|
142
|
+
ids.add(expectation.id);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateModel(value, definitionPath) {
|
|
147
|
+
const model = requireExactKeys(value, ["model"], ["thinking"], "group.model", definitionPath);
|
|
148
|
+
requireString(model.model, "group.model.model", definitionPath);
|
|
149
|
+
if (Object.hasOwn(model, "thinking")) requireString(model.thinking, "group.model.thinking", definitionPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function validateGroup(value, definitionPath) {
|
|
153
|
+
const group = requireExactKeys(
|
|
154
|
+
value,
|
|
155
|
+
[
|
|
156
|
+
"schema_version",
|
|
157
|
+
"kind",
|
|
158
|
+
"id",
|
|
159
|
+
"type",
|
|
160
|
+
"input",
|
|
161
|
+
"fixture",
|
|
162
|
+
"tools",
|
|
163
|
+
"variants",
|
|
164
|
+
"expectations",
|
|
165
|
+
"review_questions",
|
|
166
|
+
],
|
|
167
|
+
["model"],
|
|
168
|
+
"group",
|
|
169
|
+
definitionPath,
|
|
170
|
+
);
|
|
171
|
+
if (group.schema_version !== 1) fail("unsupported-schema", "group.schema_version must be 1", definitionPath);
|
|
172
|
+
if (group.kind !== "group") fail("invalid-definition", "group.kind must be group", definitionPath);
|
|
173
|
+
requireId(group.id, "group.id", definitionPath);
|
|
174
|
+
if (!EVALUATION_TYPES.has(group.type)) {
|
|
175
|
+
fail("invalid-definition", "group.type must be description, body, or end-to-end", definitionPath);
|
|
176
|
+
}
|
|
177
|
+
validateInput(group.input, definitionPath);
|
|
178
|
+
if (group.fixture !== null) requireSafeRelativePath(group.fixture, "group.fixture", definitionPath);
|
|
179
|
+
requireStringArray(group.tools, "group.tools", definitionPath, { unique: true });
|
|
180
|
+
const variants = requireExactKeys(group.variants, VARIANTS, [], "group.variants", definitionPath);
|
|
181
|
+
for (const variant of VARIANTS) validateEnvironment(variants[variant], `group.variants.${variant}`, definitionPath);
|
|
182
|
+
if (variants.candidate.target === null) {
|
|
183
|
+
fail(
|
|
184
|
+
"invalid-definition",
|
|
185
|
+
"group.variants.candidate.target must identify the candidate target skill",
|
|
186
|
+
definitionPath,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
validateExpectations(group.expectations, definitionPath);
|
|
190
|
+
requireStringArray(group.review_questions, "group.review_questions", definitionPath);
|
|
191
|
+
if (Object.hasOwn(group, "model")) validateModel(group.model, definitionPath);
|
|
192
|
+
return group;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function validateSuite(value, definitionPath) {
|
|
196
|
+
const suite = requireExactKeys(value, ["schema_version", "kind", "id", "groups"], [], "suite", definitionPath);
|
|
197
|
+
if (suite.schema_version !== 1) fail("unsupported-schema", "suite.schema_version must be 1", definitionPath);
|
|
198
|
+
if (suite.kind !== "suite") fail("invalid-definition", "suite.kind must be suite", definitionPath);
|
|
199
|
+
requireId(suite.id, "suite.id", definitionPath);
|
|
200
|
+
const groups = requireStringArray(suite.groups, "suite.groups", definitionPath, { unique: true });
|
|
201
|
+
for (const [index, reference] of groups.entries()) {
|
|
202
|
+
requireSuiteReference(reference, `suite.groups[${index}]`, definitionPath);
|
|
203
|
+
}
|
|
204
|
+
if (groups.length === 0) fail("invalid-definition", "suite.groups must not be empty", definitionPath);
|
|
205
|
+
return suite;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isContained(root, target) {
|
|
209
|
+
const relative = path.relative(root, target);
|
|
210
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function readJsonFile(file, root) {
|
|
214
|
+
const lexicalFile = path.resolve(file);
|
|
215
|
+
if (!isContained(root, lexicalFile)) fail("unsafe-path", `Definition escapes root: ${file}`, lexicalFile);
|
|
216
|
+
|
|
217
|
+
let canonicalRoot;
|
|
218
|
+
let canonicalFile;
|
|
219
|
+
try {
|
|
220
|
+
[canonicalRoot, canonicalFile] = await Promise.all([realpath(root), realpath(lexicalFile)]);
|
|
221
|
+
} catch {
|
|
222
|
+
fail("unreadable-definition", `Cannot resolve definition: ${lexicalFile}`, lexicalFile);
|
|
223
|
+
}
|
|
224
|
+
if (!isContained(canonicalRoot, canonicalFile)) {
|
|
225
|
+
fail("unsafe-path", `Definition symlink escapes root: ${lexicalFile}`, lexicalFile);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let contents;
|
|
229
|
+
try {
|
|
230
|
+
contents = await readFile(canonicalFile, "utf8");
|
|
231
|
+
} catch {
|
|
232
|
+
fail("unreadable-definition", `Cannot read definition: ${lexicalFile}`, lexicalFile);
|
|
233
|
+
}
|
|
234
|
+
try {
|
|
235
|
+
return { value: JSON.parse(contents), file: lexicalFile };
|
|
236
|
+
} catch {
|
|
237
|
+
fail("invalid-json", `Definition is not valid JSON: ${lexicalFile}`, lexicalFile);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function loadDefinition(file, { root = process.cwd() } = {}) {
|
|
242
|
+
const definitionRoot = path.resolve(root);
|
|
243
|
+
const loaded = await readJsonFile(file, definitionRoot);
|
|
244
|
+
const object = requireObject(loaded.value, "definition", loaded.file);
|
|
245
|
+
if (object.kind === "group") return validateGroup(object, loaded.file);
|
|
246
|
+
if (object.kind !== "suite") fail("invalid-definition", "definition.kind must be group or suite", loaded.file);
|
|
247
|
+
|
|
248
|
+
const suite = validateSuite(object, loaded.file);
|
|
249
|
+
const groups = [];
|
|
250
|
+
const ids = new Set();
|
|
251
|
+
for (const reference of suite.groups) {
|
|
252
|
+
const groupFile = path.resolve(path.dirname(loaded.file), reference);
|
|
253
|
+
const groupLoaded = await readJsonFile(groupFile, definitionRoot);
|
|
254
|
+
const group = validateGroup(groupLoaded.value, groupLoaded.file);
|
|
255
|
+
if (ids.has(group.id)) fail("invalid-definition", `suite contains duplicate group ID ${group.id}`, loaded.file);
|
|
256
|
+
ids.add(group.id);
|
|
257
|
+
groups.push(group);
|
|
258
|
+
}
|
|
259
|
+
return { ...suite, groups };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function variantSelection(selected) {
|
|
263
|
+
return {
|
|
264
|
+
baseline: selected === null || selected === "baseline" ? "selected" : "not-selected",
|
|
265
|
+
candidate: selected === null || selected === "candidate" ? "selected" : "not-selected",
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function selectDefinition(definition, { group = null, variant = null } = {}) {
|
|
270
|
+
if (variant !== null && !VARIANTS.includes(variant)) {
|
|
271
|
+
fail("invalid-selector", "variant must be baseline or candidate", null);
|
|
272
|
+
}
|
|
273
|
+
if (definition.kind === "group" && group !== null) {
|
|
274
|
+
fail("invalid-selector", "--group cannot be used with a direct group definition", null);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const available = definition.kind === "group" ? [definition] : definition.groups;
|
|
278
|
+
let selected = available;
|
|
279
|
+
if (group !== null) {
|
|
280
|
+
const position = typeof group === "number" || /^\d+$/.test(group) ? Number(group) : null;
|
|
281
|
+
selected =
|
|
282
|
+
position === null ? available.filter((entry) => entry.id === group) : [available[position - 1]].filter(Boolean);
|
|
283
|
+
if (selected.length === 0) fail("unknown-group", `Unknown group selector: ${group}`, null);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
kind: "selection",
|
|
288
|
+
definitionKind: definition.kind,
|
|
289
|
+
id: definition.id,
|
|
290
|
+
groups: selected.map((entry) => ({
|
|
291
|
+
position: available.indexOf(entry) + 1,
|
|
292
|
+
group: entry,
|
|
293
|
+
variants: variantSelection(variant),
|
|
294
|
+
})),
|
|
295
|
+
};
|
|
296
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export function createInvocationId(now = new Date()) {
|
|
6
|
+
const timestamp = now
|
|
7
|
+
.toISOString()
|
|
8
|
+
.replaceAll(/[-:.TZ]/g, "")
|
|
9
|
+
.slice(0, 14);
|
|
10
|
+
return `${timestamp}-${randomBytes(4).toString("hex")}`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function writeJson(file, value) {
|
|
14
|
+
const contents = `${JSON.stringify(value, null, 2)}\n`;
|
|
15
|
+
await writeAtomic(file, contents);
|
|
16
|
+
return { contents, sha256: sha256(contents) };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function writeText(file, contents) {
|
|
20
|
+
await writeAtomic(file, contents);
|
|
21
|
+
return { contents, sha256: sha256(contents) };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function fileIdentity(file) {
|
|
25
|
+
const contents = await readFile(file);
|
|
26
|
+
return {
|
|
27
|
+
path: file,
|
|
28
|
+
sha256: createHash("sha256").update(contents).digest("hex"),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sha256(value) {
|
|
33
|
+
return createHash("sha256").update(value).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function writeAtomic(file, contents) {
|
|
37
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
38
|
+
const temporary = `${file}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
39
|
+
try {
|
|
40
|
+
await writeFile(temporary, contents);
|
|
41
|
+
await rename(temporary, file);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
await rm(temporary, { force: true }).catch(() => {});
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { isDeepStrictEqual } from "node:util";
|
|
2
|
+
|
|
3
|
+
import { hasOwn } from "./grade-support.mjs";
|
|
4
|
+
|
|
5
|
+
export function gradeComparisons(expectations, checks) {
|
|
6
|
+
/** @type {Map<string, any[]>} */
|
|
7
|
+
const grouped = new Map();
|
|
8
|
+
const errors = /** @type {any[]} */ ([]);
|
|
9
|
+
for (const expectation of expectations) {
|
|
10
|
+
if (!hasOwn(expectation, "comparison")) continue;
|
|
11
|
+
if (typeof expectation.comparison !== "string" || !/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/.test(expectation.comparison)) {
|
|
12
|
+
errors.push({ id: expectation.id, reason: "invalid comparison identity" });
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const entries = grouped.get(expectation.comparison) ?? [];
|
|
16
|
+
entries.push(expectation);
|
|
17
|
+
grouped.set(expectation.comparison, entries);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** @type {Map<string, any>} */
|
|
21
|
+
const byCheck = new Map(checks.map((check) => [check.id, check]));
|
|
22
|
+
const comparisons = /** @type {any[]} */ ([]);
|
|
23
|
+
for (const [id, entries] of grouped) {
|
|
24
|
+
const baselineExpectation = entries.find((entry) => entry.variant === "baseline");
|
|
25
|
+
const candidateExpectation = entries.find((entry) => entry.variant === "candidate");
|
|
26
|
+
if (entries.length !== 2 || !baselineExpectation || !candidateExpectation) {
|
|
27
|
+
errors.push({ id, reason: "comparison requires one baseline and one candidate expectation" });
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const baseline = byCheck.get(baselineExpectation.id);
|
|
31
|
+
const candidate = byCheck.get(candidateExpectation.id);
|
|
32
|
+
if (!sameCheck(baseline, candidate)) {
|
|
33
|
+
errors.push({ id, reason: "comparison expectations must describe the same deterministic check" });
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
comparisons.push({
|
|
37
|
+
id,
|
|
38
|
+
kind: baseline.kind,
|
|
39
|
+
baseline: { check: baseline.id, state: baseline.state },
|
|
40
|
+
candidate: { check: candidate.id, state: candidate.state },
|
|
41
|
+
transition: transition(baseline.state, candidate.state),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { comparisons, errors };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function sameCheck(baseline, candidate) {
|
|
48
|
+
return (
|
|
49
|
+
baseline !== undefined &&
|
|
50
|
+
candidate !== undefined &&
|
|
51
|
+
baseline.kind === candidate.kind &&
|
|
52
|
+
isDeepStrictEqual(baseline.expected, candidate.expected)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function transition(baseline, candidate) {
|
|
57
|
+
return [baseline, candidate].every((state) => state === "pass" || state === "fail")
|
|
58
|
+
? `${baseline}-to-${candidate}`
|
|
59
|
+
: "unavailable";
|
|
60
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { realpath } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
completedCheck,
|
|
6
|
+
hasOwn,
|
|
7
|
+
isContained,
|
|
8
|
+
isSafeRelativePath,
|
|
9
|
+
readPaths,
|
|
10
|
+
RESPONSE_EXPECTATIONS,
|
|
11
|
+
textMatches,
|
|
12
|
+
unavailableCheck,
|
|
13
|
+
validBodyExpectation,
|
|
14
|
+
validTurn,
|
|
15
|
+
VARIANTS,
|
|
16
|
+
} from "./grade-support.mjs";
|
|
17
|
+
|
|
18
|
+
const ACTIVATION_EXPECTATIONS = new Set(["never", "on-turn", "by-turn", "not-before-turn"]);
|
|
19
|
+
|
|
20
|
+
export async function gradeSkillRead(group, runs, expectation) {
|
|
21
|
+
const turn = expectation.turn ?? null;
|
|
22
|
+
const timingExpectation = expectation.expect !== "never";
|
|
23
|
+
if (
|
|
24
|
+
group.type !== "description" ||
|
|
25
|
+
!VARIANTS.includes(expectation.variant) ||
|
|
26
|
+
!ACTIVATION_EXPECTATIONS.has(expectation.expect) ||
|
|
27
|
+
(timingExpectation ? turn === null || !validTurn(group, turn) : hasOwn(expectation, "turn"))
|
|
28
|
+
) {
|
|
29
|
+
return { error: "invalid skill-read expectation" };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const expected = { read: expectation.expect, turn };
|
|
33
|
+
const run = runs[expectation.variant];
|
|
34
|
+
if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
|
|
35
|
+
|
|
36
|
+
const observed = {
|
|
37
|
+
targetRead: run.activation.targetRead,
|
|
38
|
+
firstReadTurn: run.activation.firstReadTurn,
|
|
39
|
+
readTurns: run.activation.readTurns,
|
|
40
|
+
};
|
|
41
|
+
return completedCheck(expectation, expected, observed, activationMatches(expectation, observed));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function gradeResourceRead(group, runs, expectation) {
|
|
45
|
+
const turn = expectation.turn ?? null;
|
|
46
|
+
const indexed = expectation.resource === "skill" || expectation.resource === "context";
|
|
47
|
+
if (
|
|
48
|
+
!VARIANTS.includes(expectation.variant) ||
|
|
49
|
+
!["workspace", "skill", "context"].includes(expectation.resource) ||
|
|
50
|
+
!["read", "not-read"].includes(expectation.expect) ||
|
|
51
|
+
typeof expectation.path !== "string" ||
|
|
52
|
+
!isSafeRelativePath(expectation.path) ||
|
|
53
|
+
!validTurn(group, turn) ||
|
|
54
|
+
(indexed && (!Number.isInteger(expectation.index) || expectation.index < 0)) ||
|
|
55
|
+
(!indexed && hasOwn(expectation, "index"))
|
|
56
|
+
) {
|
|
57
|
+
return { error: "invalid resource-read expectation" };
|
|
58
|
+
}
|
|
59
|
+
const expected = {
|
|
60
|
+
expect: expectation.expect,
|
|
61
|
+
resource: expectation.resource,
|
|
62
|
+
index: indexed ? expectation.index : null,
|
|
63
|
+
path: expectation.path,
|
|
64
|
+
turn,
|
|
65
|
+
};
|
|
66
|
+
const run = runs[expectation.variant];
|
|
67
|
+
if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
|
|
68
|
+
const reads = readPaths(run, turn);
|
|
69
|
+
if (reads === null) return unavailableCheck(expectation, expected);
|
|
70
|
+
|
|
71
|
+
const resources = /** @type {any} */ (run.resources);
|
|
72
|
+
let resource = null;
|
|
73
|
+
if (expectation.resource === "skill") resource = resources?.skills?.[expectation.index];
|
|
74
|
+
if (expectation.resource === "context") resource = resources?.context?.[expectation.index];
|
|
75
|
+
const root = expectation.resource === "workspace" ? run.workspace : resource?.path;
|
|
76
|
+
if (typeof root !== "string") return { error: "declared resource is unavailable" };
|
|
77
|
+
if (resource !== null && !resource.files?.some((file) => file.path === expectation.path)) {
|
|
78
|
+
return { error: "declared resource path is unavailable" };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const canonicalRoot = await realpath(root);
|
|
82
|
+
const declaredPath = path.resolve(canonicalRoot, expectation.path);
|
|
83
|
+
const canonicalPath = await realpath(declaredPath).catch(() => declaredPath);
|
|
84
|
+
if (!isContained(canonicalRoot, canonicalPath)) return { error: "resource-read path escapes its declared root" };
|
|
85
|
+
if (
|
|
86
|
+
expectation.resource === "workspace" &&
|
|
87
|
+
!run.effects?.before?.some((file) => file.path === expectation.path) &&
|
|
88
|
+
!run.effects?.after?.some((file) => file.path === expectation.path) &&
|
|
89
|
+
!reads.includes(canonicalPath)
|
|
90
|
+
) {
|
|
91
|
+
return { error: "declared workspace resource path is unavailable" };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const observed = { path: canonicalPath, read: reads.includes(canonicalPath), turn };
|
|
95
|
+
return completedCheck(
|
|
96
|
+
expectation,
|
|
97
|
+
expected,
|
|
98
|
+
observed,
|
|
99
|
+
expectation.expect === "read" ? observed.read : !observed.read,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function gradeResponseText(group, runs, expectation) {
|
|
104
|
+
const turn = expectation.turn ?? null;
|
|
105
|
+
if (
|
|
106
|
+
!validBodyExpectation(group, expectation, turn) ||
|
|
107
|
+
!RESPONSE_EXPECTATIONS.has(expectation.expect) ||
|
|
108
|
+
typeof expectation.value !== "string"
|
|
109
|
+
) {
|
|
110
|
+
return { error: "invalid response-text expectation" };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const expected = { expect: expectation.expect, value: expectation.value, turn };
|
|
114
|
+
const run = runs[expectation.variant];
|
|
115
|
+
if (!run || run.state !== "complete") return unavailableCheck(expectation, expected);
|
|
116
|
+
const observed = responseObservation(run, turn);
|
|
117
|
+
if (observed === null) return unavailableCheck(expectation, expected);
|
|
118
|
+
return completedCheck(
|
|
119
|
+
expectation,
|
|
120
|
+
expected,
|
|
121
|
+
observed,
|
|
122
|
+
textMatches(expectation.expect, expectation.value, observed.response),
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function activationMatches(expectation, observed) {
|
|
127
|
+
if (expectation.expect === "never") return !observed.targetRead;
|
|
128
|
+
if (!Number.isInteger(expectation.turn) || expectation.turn < 1) return false;
|
|
129
|
+
if (expectation.expect === "on-turn") return observed.readTurns.includes(expectation.turn);
|
|
130
|
+
if (expectation.expect === "by-turn") {
|
|
131
|
+
return observed.firstReadTurn !== null && observed.firstReadTurn <= expectation.turn;
|
|
132
|
+
}
|
|
133
|
+
return observed.firstReadTurn === null || observed.firstReadTurn >= expectation.turn;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function responseObservation(run, turn) {
|
|
137
|
+
if (turn === null) {
|
|
138
|
+
const observedTurn = Array.isArray(run.turns) && run.turns.length > 0 ? run.turns.at(-1).turn : 1;
|
|
139
|
+
return { response: typeof run.response === "string" ? run.response : "", turn: observedTurn };
|
|
140
|
+
}
|
|
141
|
+
if (!Array.isArray(run.turns)) {
|
|
142
|
+
if (turn !== 1) return null;
|
|
143
|
+
return { response: typeof run.response === "string" ? run.response : "", turn };
|
|
144
|
+
}
|
|
145
|
+
const observedTurn = run.turns.find((entry) => entry.turn === turn);
|
|
146
|
+
if (!observedTurn) return null;
|
|
147
|
+
return { response: observedTurn.response ?? "", turn };
|
|
148
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, readFile, realpath } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const VARIANTS = ["baseline", "candidate"];
|
|
6
|
+
export const RESPONSE_EXPECTATIONS = new Set(["contains", "not-contains", "equals"]);
|
|
7
|
+
|
|
8
|
+
export function hasOwn(value, key) {
|
|
9
|
+
return Object.getOwnPropertyDescriptor(value, key) !== undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function validBodyExpectation(group, expectation, turn) {
|
|
13
|
+
return group.type === "body" && VARIANTS.includes(expectation.variant) && validTurn(group, turn);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function validTurn(group, turn) {
|
|
17
|
+
if (turn === null) return true;
|
|
18
|
+
const declaredTurnCount = group.input.prompt === undefined ? group.input.turns.length : 1;
|
|
19
|
+
return Number.isInteger(turn) && turn >= 1 && turn <= declaredTurnCount;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function completedCheck(expectation, expected, observed, passed) {
|
|
23
|
+
return {
|
|
24
|
+
id: expectation.id,
|
|
25
|
+
kind: expectation.kind,
|
|
26
|
+
variant: expectation.variant,
|
|
27
|
+
state: passed ? "pass" : "fail",
|
|
28
|
+
expected,
|
|
29
|
+
observed,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function unavailableCheck(expectation, expected) {
|
|
34
|
+
return {
|
|
35
|
+
id: expectation.id,
|
|
36
|
+
kind: expectation.kind,
|
|
37
|
+
variant: expectation.variant,
|
|
38
|
+
state: "unavailable",
|
|
39
|
+
expected,
|
|
40
|
+
observed: null,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function workspaceEvidence(run, turn) {
|
|
45
|
+
if (turn === null || (!Array.isArray(run.turns) && turn === 1)) {
|
|
46
|
+
if (!run.workspace || !Array.isArray(run.effects?.after)) return null;
|
|
47
|
+
return { root: run.workspace, files: run.effects.after, changes: run.effects.changes ?? null };
|
|
48
|
+
}
|
|
49
|
+
const observedTurn = run.turns?.find((entry) => entry.turn === turn);
|
|
50
|
+
if (!observedTurn?.workspace?.path || !Array.isArray(observedTurn.workspace.files)) return null;
|
|
51
|
+
return {
|
|
52
|
+
root: observedTurn.workspace.path,
|
|
53
|
+
files: observedTurn.workspace.files,
|
|
54
|
+
changes: observedTurn.workspace.changes ?? null,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function readPaths(run, turn) {
|
|
59
|
+
if (turn === null) {
|
|
60
|
+
if (Array.isArray(run.turns)) {
|
|
61
|
+
return run.turns.flatMap((entry) => (Array.isArray(entry.successfulReadPaths) ? entry.successfulReadPaths : []));
|
|
62
|
+
}
|
|
63
|
+
return Array.isArray(run.successfulReadPaths) ? run.successfulReadPaths : [];
|
|
64
|
+
}
|
|
65
|
+
if (!Array.isArray(run.turns)) {
|
|
66
|
+
return turn === 1 && Array.isArray(run.successfulReadPaths) ? run.successfulReadPaths : null;
|
|
67
|
+
}
|
|
68
|
+
const observedTurn = run.turns.find((entry) => entry.turn === turn);
|
|
69
|
+
return observedTurn && Array.isArray(observedTurn.successfulReadPaths) ? observedTurn.successfulReadPaths : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function textMatches(expect, value, text) {
|
|
73
|
+
if (expect === "contains") return text.includes(value);
|
|
74
|
+
if (expect === "not-contains") return !text.includes(value);
|
|
75
|
+
return text === value;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function validPathArray(value) {
|
|
79
|
+
return (
|
|
80
|
+
Array.isArray(value) &&
|
|
81
|
+
value.every((entry) => typeof entry === "string" && isSafeRelativePath(entry)) &&
|
|
82
|
+
new Set(value).size === value.length
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function readWorkspaceFile(workspace, relativePath) {
|
|
87
|
+
const entry = workspace.files.find((file) => file.path === relativePath);
|
|
88
|
+
if (!entry) return { state: "missing", sha256: null };
|
|
89
|
+
if (entry.type !== undefined && entry.type !== "file") {
|
|
90
|
+
throw new Error(`file assertion requires a regular file: ${relativePath}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const root = await realpath(workspace.root);
|
|
94
|
+
const declaredPath = path.resolve(root, relativePath);
|
|
95
|
+
const declaredStat = await lstat(declaredPath);
|
|
96
|
+
if (declaredStat.isSymbolicLink()) {
|
|
97
|
+
throw new Error(`file assertion requires a regular file: ${relativePath}`);
|
|
98
|
+
}
|
|
99
|
+
const file = await realpath(declaredPath);
|
|
100
|
+
if (!isContained(root, file)) throw new Error(`workspace evidence path escapes its root: ${relativePath}`);
|
|
101
|
+
const contents = await readFile(file);
|
|
102
|
+
const actualHash = createHash("sha256").update(contents).digest("hex");
|
|
103
|
+
if (actualHash !== entry.sha256) throw new Error(`workspace evidence changed after capture: ${relativePath}`);
|
|
104
|
+
return { state: "present", sha256: actualHash, text: contents.toString("utf8") };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function isSafeRelativePath(value) {
|
|
108
|
+
return (
|
|
109
|
+
value.length > 0 &&
|
|
110
|
+
!path.isAbsolute(value) &&
|
|
111
|
+
!value.includes("\\") &&
|
|
112
|
+
!value.includes("\0") &&
|
|
113
|
+
!value.split("/").some((part) => part === "" || part === "." || part === "..")
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function isContained(root, target) {
|
|
118
|
+
const relative = path.relative(root, target);
|
|
119
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function compareText(left, right) {
|
|
123
|
+
if (left < right) return -1;
|
|
124
|
+
if (left > right) return 1;
|
|
125
|
+
return 0;
|
|
126
|
+
}
|