simt-dev 0.7.0 → 0.8.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/CHANGELOG.md +191 -870
- package/README.md +188 -213
- package/bin/simt.mjs +19 -55
- package/docs/decisions/ADR-014-workspace-is-configuration.md +2 -2
- package/docs/decisions/ADR-025-a-baseline-is-a-region-in-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-026-a-config-without-a-region-is-a-file-the-repo-owns.md +1 -1
- package/docs/decisions/ADR-027-the-one-gitattributes-line-that-is-neither-dangerous-nor-empty.md +1 -1
- package/docs/decisions/ADR-028-a-base-file-is-a-floor-so-the-disagreement-it-cannot-repair-becomes-a-check.md +1 -1
- package/docs/decisions/ADR-029-the-pins-a-repository-declares-are-its-own.md +1 -1
- package/docs/decisions/ADR-030-what-a-plan-does-not-ship-is-a-decision-with-a-record.md +1 -1
- package/docs/decisions/ADR-048-a-baseline-carries-only-keys-a-reader-honours.md +1 -1
- package/docs/decisions/ADR-082-the-tracker-override-is-a-manifest-key.md +1 -1
- package/docs/decisions/ADR-099-the-release-verifies-served-bytes-and-the-formula-mirrors-them.md +1 -1
- package/docs/decisions/ADR-117-the-darwin-release-builds-on-supported-macos-15-runners-and-a-re-run-is-idempotent.md +81 -0
- package/docs/decisions/ADR-118-the-bench-corpus-is-keyed-on-history-is-append-only-and-is-portable-as-a-sealed-document.md +91 -0
- package/docs/decisions/ADR-119-the-environment-variable-points-into-a-workspace-the-repo-declared.md +90 -0
- package/docs/decisions/ADR-120-a-pre-push-whose-tree-is-already-proven-green-skips-the-test-gate-on-that-evidence.md +96 -0
- package/docs/decisions/ADR-122-the-shims-move-into-the-state-directory-and-the-legacy-directory-removes-itself.md +94 -0
- package/docs/decisions/ADR-123-a-surplus-positional-is-not-refused-because-arity-is-not-in-the-source.md +57 -0
- package/docs/decisions/ADR-124-the-manifest-has-a-schema-in-the-tool-and-its-unknown-keys-stay-the-repositorys.md +64 -0
- package/docs/decisions/ADR-125-a-shared-context-belongs-to-a-read-and-a-writer-reads-directly.md +65 -0
- package/docs/decisions/ADR-126-the-mcp-surface-stays-a-reader-so-there-is-no-write-path-to-build.md +68 -0
- package/docs/decisions/ADR-127-the-mcp-tool-list-is-written-by-hand-and-roots-is-not-asked-for.md +65 -0
- package/docs/decisions/ADR-128-a-research-plan-is-a-plan-and-the-tracker-reads-it.md +42 -0
- package/ecosystems/bun.json +6 -0
- package/ecosystems/cargo.json +25 -0
- package/ecosystems/composer.json +6 -0
- package/ecosystems/dart.json +24 -0
- package/ecosystems/deno.json +21 -0
- package/ecosystems/devcontainers.json +6 -0
- package/ecosystems/docker-compose.json +6 -0
- package/ecosystems/docker.json +6 -0
- package/ecosystems/dotnet.json +25 -0
- package/ecosystems/elixir.json +23 -0
- package/ecosystems/gitsubmodule.json +6 -0
- package/ecosystems/go.json +25 -0
- package/ecosystems/gradle.json +13 -0
- package/ecosystems/maven.json +23 -0
- package/ecosystems/node.json +25 -0
- package/ecosystems/python.json +20 -0
- package/ecosystems/ruby.json +23 -0
- package/ecosystems/swift.json +25 -0
- package/ecosystems/terraform.json +6 -0
- package/ecosystems/uv.json +6 -0
- package/harnesses/antigravity.json +162 -0
- package/harnesses/claude.json +206 -0
- package/harnesses/codex.json +136 -0
- package/harnesses/copilot.json +88 -0
- package/harnesses/cursor.json +140 -0
- package/harnesses/deepseek.json +99 -0
- package/harnesses/gemini.json +122 -0
- package/harnesses/opencode.json +173 -0
- package/package.json +12 -2
- package/rules/core.md +7 -0
- package/rules/mdc/intake-questions.mdc +8 -0
- package/rules/mdc/slice-commit.mdc +57 -4
- package/src/advice/advice.mjs +3 -2
- package/src/agenthooks/prompt.mjs +5 -2
- package/src/agenthooks/render.mjs +86 -180
- package/src/agenthooks/shim.mjs +1 -1
- package/src/agents/render.mjs +15 -18
- package/src/api.mjs +31 -0
- package/src/argv.mjs +48 -3
- package/src/bench/ablate.mjs +3 -4
- package/src/bench/adapters.json +163 -105
- package/src/bench/adapters.mjs +466 -0
- package/src/bench/arms.mjs +333 -0
- package/src/bench/grade.mjs +33 -73
- package/src/bench/graders.mjs +42 -0
- package/src/bench/mine.mjs +121 -475
- package/src/bench/probe-ledger.mjs +149 -0
- package/src/bench/probes.catalog.mjs +918 -0
- package/src/bench/probes.mjs +79 -1064
- package/src/bench/render.mjs +111 -0
- package/src/bench/report.mjs +120 -962
- package/src/bench/run.mjs +185 -802
- package/src/bench/variants.mjs +4 -5
- package/src/bench/workspace.mjs +160 -0
- package/src/brief/assemble.mjs +48 -15
- package/src/ci/dependabot.mjs +25 -55
- package/src/ci/workflow.mjs +30 -7
- package/src/cli.mjs +45 -0
- package/src/commands/agent.mjs +4 -13
- package/src/commands/agenthook.mjs +70 -40
- package/src/commands/bench.mjs +191 -18
- package/src/commands/brief.mjs +5 -4
- package/src/commands/ci.mjs +13 -14
- package/src/commands/compare.mjs +42 -42
- package/src/commands/completions.mjs +2 -6
- package/src/commands/decide.mjs +4 -4
- package/src/commands/distill.mjs +4 -3
- package/src/commands/docs.mjs +88 -0
- package/src/commands/doctor.mjs +11 -7
- package/src/commands/evidence.mjs +6 -9
- package/src/commands/explain.mjs +0 -1
- package/src/commands/gate.mjs +26 -25
- package/src/commands/graph.mjs +2 -3
- package/src/commands/harness.mjs +6 -5
- package/src/commands/help.mjs +168 -19
- package/src/commands/hooks.mjs +119 -49
- package/src/commands/ignore.mjs +13 -8
- package/src/commands/impact.mjs +8 -15
- package/src/commands/index.mjs +318 -193
- package/src/commands/init-scan.mjs +90 -0
- package/src/commands/init.mjs +124 -105
- package/src/commands/lease.mjs +73 -25
- package/src/commands/lessons.mjs +10 -9
- package/src/commands/local.mjs +3 -3
- package/src/commands/map.mjs +24 -14
- package/src/commands/mcp.mjs +2 -18
- package/src/commands/pack.mjs +9 -6
- package/src/commands/plan.mjs +6 -5
- package/src/commands/plugin.mjs +2 -2
- package/src/commands/prbody.mjs +3 -3
- package/src/commands/repoindex.mjs +12 -11
- package/src/commands/review.mjs +2 -2
- package/src/commands/rules.mjs +37 -16
- package/src/commands/shared.mjs +14 -19
- package/src/commands/skills.mjs +33 -32
- package/src/commands/skillscompile.mjs +7 -8
- package/src/commands/skillsinstall.mjs +7 -6
- package/src/commands/slice.mjs +220 -74
- package/src/commands/spec.mjs +167 -0
- package/src/commands/status.mjs +27 -18
- package/src/commands/sync.mjs +27 -25
- package/src/commands/tests.mjs +18 -21
- package/src/commands/tokens.mjs +21 -26
- package/src/commands/undo.mjs +5 -15
- package/src/commands/upgrade.mjs +15 -7
- package/src/commands/why.mjs +15 -21
- package/src/compare/hooksstate.mjs +2 -6
- package/src/compare/learn.mjs +3 -1
- package/src/compare/row.mjs +0 -2
- package/src/context.mjs +64 -0
- package/src/decisions/index-doc.mjs +46 -0
- package/src/decisions/index.mjs +3 -2
- package/src/diff.mjs +1 -1
- package/src/distill/distill.mjs +14 -1
- package/src/distill/engine.mjs +5 -1
- package/src/docs/commands.mjs +65 -0
- package/src/ecosystem/records.mjs +219 -0
- package/src/explain.mjs +31 -0
- package/src/gates/index.mjs +385 -0
- package/src/graph/compile.mjs +8 -4
- package/src/graph/load.mjs +6 -2
- package/src/graph/spec.mjs +10 -8
- package/src/harness/records.mjs +143 -0
- package/src/hooks/capability.mjs +7 -5
- package/src/hooks/dispatcher.mjs +209 -60
- package/src/hooks/gates.mjs +10 -43
- package/src/hooks/resolve.mjs +4 -14
- package/src/hooks/shim.mjs +5 -2
- package/src/hooks/where.mjs +2 -7
- package/src/identity.mjs +31 -12
- package/src/index/coverage.mjs +58 -0
- package/src/index/extract.mjs +6 -16
- package/src/index/gate.mjs +1 -1
- package/src/index/govern.mjs +26 -9
- package/src/index/impact.mjs +4 -8
- package/src/index/map.mjs +14 -15
- package/src/index/open.mjs +173 -0
- package/src/index/persist.mjs +224 -57
- package/src/index/store.mjs +82 -39
- package/src/index/tests.mjs +2 -6
- package/src/journal.mjs +94 -20
- package/src/kernel/card.mjs +37 -63
- package/src/kernel/checks.mjs +4 -10
- package/src/lease.mjs +102 -7
- package/src/ledger/index.mjs +150 -14
- package/src/local/client.mjs +4 -6
- package/src/locate.mjs +33 -9
- package/src/manifest/schema.mjs +213 -0
- package/src/mcp/paginate.mjs +33 -0
- package/src/mcp/resources.mjs +7 -10
- package/src/mcp/schemas.mjs +512 -0
- package/src/mcp/server.mjs +119 -25
- package/src/mcp/tools-facts.mjs +326 -0
- package/src/mcp/tools-read.mjs +221 -0
- package/src/mcp/tools.mjs +4 -967
- package/src/measure/corpus.mjs +150 -0
- package/src/measure/metrics.mjs +236 -0
- package/src/measure/records.mjs +211 -0
- package/src/measure/session.mjs +3 -2
- package/src/measure/stats.mjs +538 -0
- package/src/measure/task-schema.mjs +125 -0
- package/src/measure/tasks.mjs +69 -0
- package/src/memory/decide.mjs +6 -15
- package/src/memory/lesson-text.mjs +61 -0
- package/src/memory/lessons.mjs +29 -82
- package/src/name.mjs +7 -0
- package/src/node-version.mjs +37 -0
- package/src/output/index.mjs +209 -10
- package/src/packs/manage.mjs +32 -12
- package/src/packs.mjs +20 -2
- package/src/plan/index.mjs +1 -1
- package/src/plugins.mjs +22 -16
- package/src/prbody.mjs +7 -11
- package/src/projection/loaders.mjs +7 -692
- package/src/projection/lock.mjs +11 -1
- package/src/projection/manifest.mjs +29 -468
- package/src/projection/mdc.mjs +7 -8
- package/src/projection/project.mjs +126 -221
- package/src/projection/region.mjs +561 -0
- package/src/projection/rules.mjs +20 -550
- package/src/projection/surfaces.mjs +43 -210
- package/src/projection/targets.mjs +5 -5
- package/src/repo-root.mjs +31 -0
- package/src/resolve/eval.mjs +2 -1
- package/src/resolve/fingerprint.mjs +19 -22
- package/src/resolve/resolve.mjs +1 -41
- package/src/review/critic.mjs +10 -13
- package/src/run.mjs +98 -0
- package/src/secrets.mjs +14 -22
- package/src/state/events.mjs +129 -0
- package/src/status-snapshot.mjs +233 -0
- package/src/status.mjs +2 -6
- package/src/sys/git.mjs +82 -0
- package/src/sys/proc.mjs +151 -0
- package/src/testing/parsers.mjs +265 -0
- package/src/testing/patches.mjs +23 -0
- package/src/testing/paths.mjs +49 -0
- package/src/testing/runners.mjs +186 -0
- package/src/testing/vault.mjs +68 -0
- package/src/text/frontmatter.mjs +49 -0
- package/src/text/tokens.mjs +52 -0
- package/src/toolwords.mjs +3 -0
- package/src/util/fs.mjs +20 -0
- package/src/util/sort.mjs +12 -0
- package/src/workspace.mjs +58 -11
- package/src/write/plan.mjs +82 -0
- package/templates/plan/tracker.json +1 -0
- package/docs/decisions/ADR-033-three-plan-rows-closed-by-decision-not-by-authoring.md +0 -82
- package/docs/decisions/ADR-102-the-two-queued-review-rows-are-closed-by-ruling.md +0 -49
- package/docs/decisions/README.md +0 -600
- package/src/bench/tasks.mjs +0 -36
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
// Statistical estimators over task-level rows.
|
|
2
|
+
//
|
|
3
|
+
// Every estimator here is deterministic: the bootstrap draws come from a seeded PRNG and
|
|
4
|
+
// the exact tests enumerate rather than sample, so a report a pull request diffs is the
|
|
5
|
+
// same report on the next machine (plan 0004 P1).
|
|
6
|
+
//
|
|
7
|
+
// Reps of a task are not independent tasks. Pairing happens at the task level only: the
|
|
8
|
+
// old run-level pairing paired (task, rep) records whenever both arms shared a key, so one
|
|
9
|
+
// task with ten reps produced ten "paired runs" and an inflated p-value (review 0003 Q46).
|
|
10
|
+
export const DEFAULT_BOOTSTRAP_SAMPLES = 1000;
|
|
11
|
+
export const DEFAULT_BOOTSTRAP_SEED = 0x51772026;
|
|
12
|
+
|
|
13
|
+
/** Deterministic 32-bit PRNG (Mulberry32). */
|
|
14
|
+
function createRng(seed = DEFAULT_BOOTSTRAP_SEED) {
|
|
15
|
+
let state = seed >>> 0;
|
|
16
|
+
return () => {
|
|
17
|
+
state = (state + 0x6d2b79f5) >>> 0;
|
|
18
|
+
let t = Math.imul(state ^ (state >>> 15), 1 | state);
|
|
19
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
20
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The mean of a list, or 0 for an empty one. */
|
|
25
|
+
export function mean(values) {
|
|
26
|
+
if (values.length === 0) return 0;
|
|
27
|
+
let sum = 0;
|
|
28
|
+
for (const value of values) sum += value;
|
|
29
|
+
return sum / values.length;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Aggregate task rows into one arm's summary metrics.
|
|
34
|
+
*
|
|
35
|
+
* `C` is null when any row's cost is missing — a partial cost must not read as a complete
|
|
36
|
+
* one. `CPI` is total cost divided by resolved outcomes, and is undefined when none
|
|
37
|
+
* resolve, never zero and never a favourable finite score (plan 0004 §1).
|
|
38
|
+
*
|
|
39
|
+
* @param {object[]} rows
|
|
40
|
+
* @returns {{R: number, Q: number, C: number|null, CPI: number|null, turns: number,
|
|
41
|
+
* toolCalls: number, cacheHitRatio: number}}
|
|
42
|
+
*/
|
|
43
|
+
export function summarizeRows(rows) {
|
|
44
|
+
if (rows.length === 0) {
|
|
45
|
+
return { R: 0, Q: 0, C: null, CPI: null, turns: 0, toolCalls: 0, cacheHitRatio: 0 };
|
|
46
|
+
}
|
|
47
|
+
const R = mean(rows.map((row) => row.R));
|
|
48
|
+
const Q = mean(rows.map((row) => row.Q));
|
|
49
|
+
const costComplete = rows.every((row) => row.C !== null && row.C !== undefined);
|
|
50
|
+
const C = costComplete ? mean(rows.map((row) => row.C)) : null;
|
|
51
|
+
const turns = mean(rows.map((row) => row.turns));
|
|
52
|
+
const toolCalls = mean(rows.map((row) => row.toolCalls));
|
|
53
|
+
let totalInput = 0;
|
|
54
|
+
let totalCacheRead = 0;
|
|
55
|
+
for (const row of rows) {
|
|
56
|
+
totalInput += row.input;
|
|
57
|
+
totalCacheRead += row.cacheRead;
|
|
58
|
+
}
|
|
59
|
+
const cacheHitRatio = totalInput + totalCacheRead > 0 ? totalCacheRead / (totalInput + totalCacheRead) : 0;
|
|
60
|
+
|
|
61
|
+
let totalCost = 0;
|
|
62
|
+
let resolvedCount = 0;
|
|
63
|
+
for (const row of rows) {
|
|
64
|
+
const reps = Number(row.reps ?? 1);
|
|
65
|
+
totalCost += (row.C ?? 0) * reps;
|
|
66
|
+
resolvedCount += row.R * reps;
|
|
67
|
+
}
|
|
68
|
+
const CPI = costComplete && resolvedCount > 0 ? totalCost / resolvedCount : null;
|
|
69
|
+
|
|
70
|
+
return { R, Q, C, CPI, turns, toolCalls, cacheHitRatio };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Group one arm's evaluated runs by task, so multi-repetition tasks form one task unit
|
|
75
|
+
* for task-level means and bootstrap resampling.
|
|
76
|
+
*
|
|
77
|
+
* @param {object[]} evaluatedRuns
|
|
78
|
+
* @returns {object[]} rows sorted by task
|
|
79
|
+
*/
|
|
80
|
+
export function collapseArmByTask(evaluatedRuns) {
|
|
81
|
+
const byTask = new Map();
|
|
82
|
+
for (const run of evaluatedRuns) {
|
|
83
|
+
const list = byTask.get(run.task) ?? [];
|
|
84
|
+
list.push(run);
|
|
85
|
+
byTask.set(run.task, list);
|
|
86
|
+
}
|
|
87
|
+
const tasks = [...byTask.keys()].sort();
|
|
88
|
+
return tasks.map((task) => {
|
|
89
|
+
const group = byTask.get(task);
|
|
90
|
+
const R = mean(group.map((row) => row.R));
|
|
91
|
+
const Q = mean(group.map((row) => row.Q));
|
|
92
|
+
const costComplete = group.every((row) => row.C !== null && row.C !== undefined);
|
|
93
|
+
const C = costComplete ? mean(group.map((row) => row.C)) : null;
|
|
94
|
+
const turns = mean(group.map((row) => row.turns));
|
|
95
|
+
const toolCalls = mean(group.map((row) => row.toolCalls));
|
|
96
|
+
const input = mean(group.map((row) => row.input));
|
|
97
|
+
const cacheWrite = mean(group.map((row) => row.cacheWrite));
|
|
98
|
+
const cacheRead = mean(group.map((row) => row.cacheRead));
|
|
99
|
+
const output = mean(group.map((row) => row.output));
|
|
100
|
+
const cacheHitRatio = input + cacheRead > 0 ? cacheRead / (input + cacheRead) : 0;
|
|
101
|
+
return {
|
|
102
|
+
task,
|
|
103
|
+
stratum: group[0]?.stratum ?? 'unknown',
|
|
104
|
+
reps: group.length,
|
|
105
|
+
R,
|
|
106
|
+
Q,
|
|
107
|
+
C,
|
|
108
|
+
turns,
|
|
109
|
+
toolCalls,
|
|
110
|
+
input,
|
|
111
|
+
cacheWrite,
|
|
112
|
+
cacheRead,
|
|
113
|
+
output,
|
|
114
|
+
cacheHitRatio,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function percentilePair(sortedValues) {
|
|
120
|
+
if (sortedValues.length === 0) return [0, 0];
|
|
121
|
+
if (sortedValues.length === 1) return [sortedValues[0], sortedValues[0]];
|
|
122
|
+
const loIdx = Math.floor(0.025 * (sortedValues.length - 1));
|
|
123
|
+
const hiIdx = Math.ceil(0.975 * (sortedValues.length - 1));
|
|
124
|
+
return [sortedValues[loIdx], sortedValues[hiIdx]];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Paired bootstrap 95% confidence intervals over task rows.
|
|
129
|
+
*
|
|
130
|
+
* When any task's cost is missing the cost intervals are undefined, not degenerate:
|
|
131
|
+
* dropping the undefined resamples quietly narrowed the interval to the tasks that
|
|
132
|
+
* happened to price out (review 0003 Q46).
|
|
133
|
+
*
|
|
134
|
+
* @param {object[]} taskRows
|
|
135
|
+
* @param {{samples?: number, seed?: number}} [options]
|
|
136
|
+
*/
|
|
137
|
+
export function bootstrapArmCi(taskRows, { samples = DEFAULT_BOOTSTRAP_SAMPLES, seed = DEFAULT_BOOTSTRAP_SEED } = {}) {
|
|
138
|
+
const n = taskRows.length;
|
|
139
|
+
const costComplete = taskRows.every((row) => row.C !== null && row.C !== undefined);
|
|
140
|
+
if (n === 0) {
|
|
141
|
+
return {
|
|
142
|
+
R: [0, 0],
|
|
143
|
+
Q: [0, 0],
|
|
144
|
+
C: null,
|
|
145
|
+
CPI: null,
|
|
146
|
+
turns: [0, 0],
|
|
147
|
+
toolCalls: [0, 0],
|
|
148
|
+
cacheHitRatio: [0, 0],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (n === 1) {
|
|
152
|
+
const summary = summarizeRows(taskRows);
|
|
153
|
+
return {
|
|
154
|
+
R: [summary.R, summary.R],
|
|
155
|
+
Q: [summary.Q, summary.Q],
|
|
156
|
+
C: summary.C === null ? null : [summary.C, summary.C],
|
|
157
|
+
CPI: summary.CPI === null ? null : [summary.CPI, summary.CPI],
|
|
158
|
+
turns: [summary.turns, summary.turns],
|
|
159
|
+
toolCalls: [summary.toolCalls, summary.toolCalls],
|
|
160
|
+
cacheHitRatio: [summary.cacheHitRatio, summary.cacheHitRatio],
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const rng = createRng(seed);
|
|
165
|
+
const rValues = [];
|
|
166
|
+
const qValues = [];
|
|
167
|
+
const cValues = [];
|
|
168
|
+
const cpiValues = [];
|
|
169
|
+
const turnsValues = [];
|
|
170
|
+
const toolsValues = [];
|
|
171
|
+
const cacheValues = [];
|
|
172
|
+
|
|
173
|
+
for (let b = 0; b < samples; b += 1) {
|
|
174
|
+
const draw = new Array(n);
|
|
175
|
+
for (let i = 0; i < n; i += 1) draw[i] = taskRows[Math.floor(rng() * n)];
|
|
176
|
+
const summary = summarizeRows(draw);
|
|
177
|
+
rValues.push(summary.R);
|
|
178
|
+
qValues.push(summary.Q);
|
|
179
|
+
if (costComplete) cValues.push(summary.C);
|
|
180
|
+
if (summary.CPI !== null) cpiValues.push(summary.CPI);
|
|
181
|
+
turnsValues.push(summary.turns);
|
|
182
|
+
toolsValues.push(summary.toolCalls);
|
|
183
|
+
cacheValues.push(summary.cacheHitRatio);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const numSort = (a, b) => a - b;
|
|
187
|
+
rValues.sort(numSort);
|
|
188
|
+
qValues.sort(numSort);
|
|
189
|
+
cValues.sort(numSort);
|
|
190
|
+
cpiValues.sort(numSort);
|
|
191
|
+
turnsValues.sort(numSort);
|
|
192
|
+
toolsValues.sort(numSort);
|
|
193
|
+
cacheValues.sort(numSort);
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
R: percentilePair(rValues),
|
|
197
|
+
Q: percentilePair(qValues),
|
|
198
|
+
C: costComplete ? percentilePair(cValues) : null,
|
|
199
|
+
CPI: cpiValues.length > 0 ? percentilePair(cpiValues) : null,
|
|
200
|
+
turns: percentilePair(turnsValues),
|
|
201
|
+
toolCalls: percentilePair(toolsValues),
|
|
202
|
+
cacheHitRatio: percentilePair(cacheValues),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Complementary error function, relative error below 1.5e-7 (Abramowitz & Stegun 7.1.26).
|
|
208
|
+
*/
|
|
209
|
+
function erfc(x) {
|
|
210
|
+
const z = Math.abs(x);
|
|
211
|
+
const t = 1 / (1 + 0.3275911 * z);
|
|
212
|
+
const a1 = 0.254829592;
|
|
213
|
+
const a2 = -0.284496736;
|
|
214
|
+
const a3 = 1.421413741;
|
|
215
|
+
const a4 = -1.453152027;
|
|
216
|
+
const a5 = 1.061405429;
|
|
217
|
+
const poly = ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * Math.exp(-z * z);
|
|
218
|
+
return x >= 0 ? poly : 2 - poly;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Two-sided exact binomial p-value for discordant counts (b, c) under p = 0.5. */
|
|
222
|
+
function exactBinomialTwoSided(b, c) {
|
|
223
|
+
const m = b + c;
|
|
224
|
+
if (m === 0) return 1;
|
|
225
|
+
const k = Math.min(b, c);
|
|
226
|
+
let term = Math.pow(0.5, m);
|
|
227
|
+
let cdf = term;
|
|
228
|
+
for (let j = 1; j <= k; j += 1) {
|
|
229
|
+
term *= (m - j + 1) / j;
|
|
230
|
+
cdf += term;
|
|
231
|
+
}
|
|
232
|
+
return Math.min(1, 2 * cdf);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* McNemar's test on paired binary resolution outcomes.
|
|
237
|
+
*
|
|
238
|
+
* @param {[number|boolean, number|boolean][]} pairs
|
|
239
|
+
*/
|
|
240
|
+
export function mcnemarTest(pairs) {
|
|
241
|
+
let n11 = 0;
|
|
242
|
+
let n10 = 0;
|
|
243
|
+
let n01 = 0;
|
|
244
|
+
let n00 = 0;
|
|
245
|
+
|
|
246
|
+
for (const [a, b] of pairs) {
|
|
247
|
+
const aRes = typeof a === 'number' ? a >= 0.5 : Boolean(a);
|
|
248
|
+
const bRes = typeof b === 'number' ? b >= 0.5 : Boolean(b);
|
|
249
|
+
if (aRes && bRes) n11 += 1;
|
|
250
|
+
else if (aRes && !bRes) n10 += 1;
|
|
251
|
+
else if (!aRes && bRes) n01 += 1;
|
|
252
|
+
else n00 += 1;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const discordant = n10 + n01;
|
|
256
|
+
const statistic = discordant > 0 ? ((n10 - n01) * (n10 - n01)) / discordant : 0;
|
|
257
|
+
const pValue = exactBinomialTwoSided(n10, n01);
|
|
258
|
+
const pValueAsymptotic = discordant > 0 ? erfc(Math.sqrt(statistic / 2)) : 1;
|
|
259
|
+
|
|
260
|
+
return { n11, n10, n01, n00, b: n10, c: n01, statistic, pValue, pValueAsymptotic };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Non-inferiority margin for resolution rate (plan 0004 §1): the candidate is
|
|
265
|
+
* non-inferior only when the one-sided 95% lower bound of ΔR stays above -2 pp.
|
|
266
|
+
*/
|
|
267
|
+
export const NON_INFERIORITY_MARGIN = -0.02;
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Below this many paired tasks the two-sided exact sign test cannot reject at α = 0.05
|
|
271
|
+
* (its smallest achievable p-value at n = 4 is 0.125), so the verdict is
|
|
272
|
+
* `insufficient-tasks` instead of a p-value the sample cannot support.
|
|
273
|
+
*/
|
|
274
|
+
export const MIN_PAIRD_TASKS = 5;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Exact two-sided sign test on paired outcomes.
|
|
278
|
+
*
|
|
279
|
+
* Zero differences carry no evidence and are excluded, mirroring the Wilcoxon treatment
|
|
280
|
+
* of ties. This replaces McNemar on task means: the sign of a task-mean difference is the
|
|
281
|
+
* paired discrete evidence, and it does not assume the 0/1 contingency McNemar requires.
|
|
282
|
+
*
|
|
283
|
+
* @param {[number, number][]} pairs
|
|
284
|
+
*/
|
|
285
|
+
export function signTest(pairs) {
|
|
286
|
+
let plus = 0;
|
|
287
|
+
let minus = 0;
|
|
288
|
+
let zero = 0;
|
|
289
|
+
for (const [a, b] of pairs) {
|
|
290
|
+
const d = Number(b) - Number(a);
|
|
291
|
+
if (d > 1e-12) plus += 1;
|
|
292
|
+
else if (d < -1e-12) minus += 1;
|
|
293
|
+
else zero += 1;
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
n: pairs.length,
|
|
297
|
+
plus,
|
|
298
|
+
minus,
|
|
299
|
+
zero,
|
|
300
|
+
statistic: Math.min(plus, minus),
|
|
301
|
+
pValue: exactBinomialTwoSided(plus, minus),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Holm step-down p-value adjustment: conservative FWER control for the family of
|
|
307
|
+
* comparisons tested in one run.
|
|
308
|
+
*
|
|
309
|
+
* @param {number[]} pValues raw p-values in any order
|
|
310
|
+
* @returns {number[]} adjusted p-values in the original order, monotone and in [0, 1]
|
|
311
|
+
*/
|
|
312
|
+
export function holmAdjust(pValues) {
|
|
313
|
+
if (pValues.length === 0) return [];
|
|
314
|
+
const m = pValues.length;
|
|
315
|
+
const indexed = pValues.map((p, i) => [i, Number(p)]).sort((a, b) => a[1] - b[1]);
|
|
316
|
+
const adjusted = new Array(m);
|
|
317
|
+
let runningMax = 0;
|
|
318
|
+
for (let j = 0; j < m; j += 1) {
|
|
319
|
+
runningMax = Math.max(runningMax, Math.min(1, indexed[j][1] * (m - j)));
|
|
320
|
+
adjusted[indexed[j][0]] = runningMax;
|
|
321
|
+
}
|
|
322
|
+
return adjusted;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Wilcoxon signed-rank test on paired numerical observations, with average ranks for
|
|
327
|
+
* ties: exact two-sided permutation p-value for at most 20 non-zero differences, the
|
|
328
|
+
* tie-adjusted normal approximation above that.
|
|
329
|
+
*
|
|
330
|
+
* @param {[number, number][]} pairs
|
|
331
|
+
*/
|
|
332
|
+
export function wilcoxonSignedRank(pairs) {
|
|
333
|
+
const diffs = [];
|
|
334
|
+
for (const [a, b] of pairs) {
|
|
335
|
+
const d = Number(b) - Number(a);
|
|
336
|
+
if (Math.abs(d) > 1e-12) diffs.push({ d, abs: Math.abs(d), sign: d > 0 ? 1 : -1 });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const n = pairs.length;
|
|
340
|
+
const nonZero = diffs.length;
|
|
341
|
+
if (nonZero === 0) {
|
|
342
|
+
return { n, nonZero: 0, wPlus: 0, wMinus: 0, statistic: 0, pValue: 1 };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
diffs.sort((x, y) => x.abs - y.abs);
|
|
346
|
+
|
|
347
|
+
const ranks = new Array(nonZero);
|
|
348
|
+
const tieGroups = [];
|
|
349
|
+
let i = 0;
|
|
350
|
+
while (i < nonZero) {
|
|
351
|
+
let j = i + 1;
|
|
352
|
+
while (j < nonZero && Math.abs(diffs[j].abs - diffs[i].abs) <= 1e-12) j += 1;
|
|
353
|
+
const avgRank = (i + 1 + j) / 2;
|
|
354
|
+
for (let k = i; k < j; k += 1) ranks[k] = avgRank;
|
|
355
|
+
const groupSize = j - i;
|
|
356
|
+
if (groupSize > 1) tieGroups.push(groupSize);
|
|
357
|
+
i = j;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
let wPlus = 0;
|
|
361
|
+
let wMinus = 0;
|
|
362
|
+
for (let k = 0; k < nonZero; k += 1) {
|
|
363
|
+
if (diffs[k].sign > 0) wPlus += ranks[k];
|
|
364
|
+
else wMinus += ranks[k];
|
|
365
|
+
}
|
|
366
|
+
const statistic = Math.min(wPlus, wMinus);
|
|
367
|
+
|
|
368
|
+
let pValue;
|
|
369
|
+
if (nonZero <= 20) {
|
|
370
|
+
const totalConfigurations = 1 << nonZero;
|
|
371
|
+
const target = statistic + 1e-9;
|
|
372
|
+
let tailCount = 0;
|
|
373
|
+
for (let mask = 0; mask < totalConfigurations; mask += 1) {
|
|
374
|
+
let sum = 0;
|
|
375
|
+
for (let bit = 0; bit < nonZero; bit += 1) {
|
|
376
|
+
if ((mask & (1 << bit)) !== 0) {
|
|
377
|
+
sum += ranks[bit];
|
|
378
|
+
if (sum > target) break;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (sum <= target) tailCount += 1;
|
|
382
|
+
}
|
|
383
|
+
pValue = Math.min(1, (2 * tailCount) / totalConfigurations);
|
|
384
|
+
} else {
|
|
385
|
+
const meanW = (nonZero * (nonZero + 1)) / 4;
|
|
386
|
+
let tieCorrection = 0;
|
|
387
|
+
for (const t of tieGroups) tieCorrection += t * t * t - t;
|
|
388
|
+
const variance = (nonZero * (nonZero + 1) * (2 * nonZero + 1) - 0.5 * tieCorrection) / 24;
|
|
389
|
+
const z = variance > 0 ? Math.abs(wPlus - meanW) / Math.sqrt(variance) : 0;
|
|
390
|
+
pValue = erfc(z / Math.SQRT2);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return { n, nonZero, wPlus, wMinus, statistic, pValue };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Pair two arms at the task level and compute deltas, paired bootstrap 95% CIs, an exact
|
|
398
|
+
* sign test on task-mean R, a Wilcoxon signed-rank test on task-mean C, and a one-sided
|
|
399
|
+
* non-inferiority verdict (plan 0004 §1, review 0003 Q46).
|
|
400
|
+
*
|
|
401
|
+
* `verdict`:
|
|
402
|
+
* 'insufficient-tasks' — fewer than MIN_PAIRD_TASKS paired tasks
|
|
403
|
+
* 'non-inferior' — one-sided 95% lower bound of ΔR above NON_INFERIORITY_MARGIN
|
|
404
|
+
* 'inferior' — two-sided 95% CI of ΔR entirely below 0
|
|
405
|
+
* 'inconclusive' — otherwise (CI too wide to decide)
|
|
406
|
+
*
|
|
407
|
+
* @param {string} armAName
|
|
408
|
+
* @param {object[]} armARuns
|
|
409
|
+
* @param {string} armBName
|
|
410
|
+
* @param {object[]} armBRuns
|
|
411
|
+
* @param {{samples?: number, seed?: number}} [options]
|
|
412
|
+
*/
|
|
413
|
+
export function compareArms(
|
|
414
|
+
armAName,
|
|
415
|
+
armARuns,
|
|
416
|
+
armBName,
|
|
417
|
+
armBRuns,
|
|
418
|
+
{ samples = DEFAULT_BOOTSTRAP_SAMPLES, seed = DEFAULT_BOOTSTRAP_SEED } = {},
|
|
419
|
+
) {
|
|
420
|
+
const tasksA = new Map(collapseArmByTask(armARuns).map((row) => [row.task, row]));
|
|
421
|
+
const tasksB = new Map(collapseArmByTask(armBRuns).map((row) => [row.task, row]));
|
|
422
|
+
const sharedTasks = [...tasksA.keys()].filter((task) => tasksB.has(task)).sort();
|
|
423
|
+
|
|
424
|
+
const pairedTasks = sharedTasks.map((task) => ({ task, a: tasksA.get(task), b: tasksB.get(task) }));
|
|
425
|
+
|
|
426
|
+
const rPairs = pairedTasks.map((pair) => [pair.a.R, pair.b.R]);
|
|
427
|
+
const cPairs = pairedTasks
|
|
428
|
+
.filter((pair) => pair.a.C !== null && pair.b.C !== null)
|
|
429
|
+
.map((pair) => [pair.a.C, pair.b.C]);
|
|
430
|
+
|
|
431
|
+
const sumA = summarizeRows(pairedTasks.map((pair) => pair.a));
|
|
432
|
+
const sumB = summarizeRows(pairedTasks.map((pair) => pair.b));
|
|
433
|
+
|
|
434
|
+
const deltas = {
|
|
435
|
+
R: sumB.R - sumA.R,
|
|
436
|
+
Q: sumB.Q - sumA.Q,
|
|
437
|
+
C: sumB.C !== null && sumA.C !== null ? sumB.C - sumA.C : null,
|
|
438
|
+
CPI: sumB.CPI !== null && sumA.CPI !== null ? sumB.CPI - sumA.CPI : null,
|
|
439
|
+
cpiRatio: sumB.CPI !== null && sumA.CPI !== null && sumA.CPI > 0 ? sumB.CPI / sumA.CPI : null,
|
|
440
|
+
turns: sumB.turns - sumA.turns,
|
|
441
|
+
toolCalls: sumB.toolCalls - sumA.toolCalls,
|
|
442
|
+
cacheHitRatio: sumB.cacheHitRatio - sumA.cacheHitRatio,
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const n = pairedTasks.length;
|
|
446
|
+
const costComplete =
|
|
447
|
+
pairedTasks.every((pair) => pair.a.C !== null && pair.a.C !== undefined) &&
|
|
448
|
+
pairedTasks.every((pair) => pair.b.C !== null && pair.b.C !== undefined);
|
|
449
|
+
let ci95;
|
|
450
|
+
if (n <= 1) {
|
|
451
|
+
ci95 = {
|
|
452
|
+
R: [deltas.R, deltas.R],
|
|
453
|
+
Q: [deltas.Q, deltas.Q],
|
|
454
|
+
C: deltas.C === null ? null : [deltas.C, deltas.C],
|
|
455
|
+
CPI: deltas.CPI === null ? null : [deltas.CPI, deltas.CPI],
|
|
456
|
+
turns: [deltas.turns, deltas.turns],
|
|
457
|
+
toolCalls: [deltas.toolCalls, deltas.toolCalls],
|
|
458
|
+
cacheHitRatio: [deltas.cacheHitRatio, deltas.cacheHitRatio],
|
|
459
|
+
};
|
|
460
|
+
} else {
|
|
461
|
+
const rng = createRng(seed);
|
|
462
|
+
const dR = [];
|
|
463
|
+
const dQ = [];
|
|
464
|
+
const dC = [];
|
|
465
|
+
const dCPI = [];
|
|
466
|
+
const dTurns = [];
|
|
467
|
+
const dTools = [];
|
|
468
|
+
const dCache = [];
|
|
469
|
+
|
|
470
|
+
for (let b = 0; b < samples; b += 1) {
|
|
471
|
+
const drawA = new Array(n);
|
|
472
|
+
const drawB = new Array(n);
|
|
473
|
+
for (let i = 0; i < n; i += 1) {
|
|
474
|
+
const pair = pairedTasks[Math.floor(rng() * n)];
|
|
475
|
+
drawA[i] = pair.a;
|
|
476
|
+
drawB[i] = pair.b;
|
|
477
|
+
}
|
|
478
|
+
const sA = summarizeRows(drawA);
|
|
479
|
+
const sB = summarizeRows(drawB);
|
|
480
|
+
dR.push(sB.R - sA.R);
|
|
481
|
+
dQ.push(sB.Q - sA.Q);
|
|
482
|
+
if (costComplete) dC.push(sB.C - sA.C);
|
|
483
|
+
if (sB.CPI !== null && sA.CPI !== null) dCPI.push(sB.CPI - sA.CPI);
|
|
484
|
+
dTurns.push(sB.turns - sA.turns);
|
|
485
|
+
dTools.push(sB.toolCalls - sA.toolCalls);
|
|
486
|
+
dCache.push(sB.cacheHitRatio - sA.cacheHitRatio);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const numSort = (x, y) => x - y;
|
|
490
|
+
dR.sort(numSort);
|
|
491
|
+
dQ.sort(numSort);
|
|
492
|
+
dC.sort(numSort);
|
|
493
|
+
dCPI.sort(numSort);
|
|
494
|
+
dTurns.sort(numSort);
|
|
495
|
+
dTools.sort(numSort);
|
|
496
|
+
dCache.sort(numSort);
|
|
497
|
+
|
|
498
|
+
ci95 = {
|
|
499
|
+
R: percentilePair(dR),
|
|
500
|
+
Q: percentilePair(dQ),
|
|
501
|
+
C: costComplete ? percentilePair(dC) : null,
|
|
502
|
+
CPI: dCPI.length > 0 ? percentilePair(dCPI) : null,
|
|
503
|
+
turns: percentilePair(dTurns),
|
|
504
|
+
toolCalls: percentilePair(dTools),
|
|
505
|
+
cacheHitRatio: percentilePair(dCache),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const rTest = signTest(rPairs);
|
|
510
|
+
const pairedRuns = pairedTasks.reduce(
|
|
511
|
+
(sum, pair) => sum + Math.min(Number(pair.a.reps ?? 1), Number(pair.b.reps ?? 1)),
|
|
512
|
+
0,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const bound = ci95.R[0];
|
|
516
|
+
const met = n < MIN_PAIRD_TASKS ? null : bound > NON_INFERIORITY_MARGIN;
|
|
517
|
+
const verdict =
|
|
518
|
+
n < MIN_PAIRD_TASKS
|
|
519
|
+
? 'insufficient-tasks'
|
|
520
|
+
: bound > NON_INFERIORITY_MARGIN
|
|
521
|
+
? 'non-inferior'
|
|
522
|
+
: ci95.R[1] < 0
|
|
523
|
+
? 'inferior'
|
|
524
|
+
: 'inconclusive';
|
|
525
|
+
|
|
526
|
+
return {
|
|
527
|
+
baseline: armAName,
|
|
528
|
+
candidate: armBName,
|
|
529
|
+
pairedTasks: n,
|
|
530
|
+
pairedRuns,
|
|
531
|
+
deltas,
|
|
532
|
+
ci95,
|
|
533
|
+
rTest,
|
|
534
|
+
wilcoxon: wilcoxonSignedRank(cPairs),
|
|
535
|
+
nonInferiority: { margin: NON_INFERIORITY_MARGIN, bound, met },
|
|
536
|
+
verdict,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Benchmark task descriptors, schema v2.
|
|
2
|
+
//
|
|
3
|
+
// Schema v1 was whatever the miner happened to write: nothing validated a field, a task
|
|
4
|
+
// said nothing about how hard it is or what the agent may see, and the grader assumed the
|
|
5
|
+
// only oracle in the world was a hidden test patch (review 0003 Q78). A field that is not
|
|
6
|
+
// checked is a guess about what it means, and an oracle that is not declared cannot be
|
|
7
|
+
// swapped for a different kind of task.
|
|
8
|
+
//
|
|
9
|
+
// v1 stays readable. `normaliseTask` fills the v2 fields and keeps every v1 key it does
|
|
10
|
+
// not own, so a corpus mined before this module existed runs without a re-mine and a
|
|
11
|
+
// stored record still means what it meant.
|
|
12
|
+
export const TASK_SCHEMA_VERSION = 2;
|
|
13
|
+
|
|
14
|
+
/** Gold-file counts that separate the strata. Declared here, so a report can name one. */
|
|
15
|
+
const STRATUM_LIMITS = Object.freeze({ small: 2, medium: 5 });
|
|
16
|
+
|
|
17
|
+
/** The oracle kinds a grader registry can answer for. Only `tests` ships. */
|
|
18
|
+
const ORACLE_TYPES = new Set(['tests']);
|
|
19
|
+
|
|
20
|
+
/** What the agent may see. A repository task shows the checked-out tree at `base`. */
|
|
21
|
+
const AGENT_VISIBLE_KINDS = new Set(['repository']);
|
|
22
|
+
|
|
23
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The stratum a task belongs to, from the size of the change its gold patch makes.
|
|
27
|
+
*
|
|
28
|
+
* A stratum is a property of the task, not of the run: two arms on the same task are
|
|
29
|
+
* compared within it, so it cannot be recomputed differently by each reader.
|
|
30
|
+
*
|
|
31
|
+
* @param {{files?: number}} [gold]
|
|
32
|
+
* @returns {'small'|'medium'|'large'}
|
|
33
|
+
*/
|
|
34
|
+
function stratumOf(gold = {}) {
|
|
35
|
+
const files = Number(gold?.files ?? 0);
|
|
36
|
+
if (files <= STRATUM_LIMITS.small) return 'small';
|
|
37
|
+
if (files <= STRATUM_LIMITS.medium) return 'medium';
|
|
38
|
+
return 'large';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fail(source, message) {
|
|
42
|
+
throw new Error(`${source}: ${message}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function stringList(value) {
|
|
46
|
+
return Array.isArray(value) ? value.map((entry) => String(entry)) : null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate one task descriptor and answer its v2 form.
|
|
51
|
+
*
|
|
52
|
+
* Every problem is reported in words that name the file and the field, because a corpus
|
|
53
|
+
* is operator input: a task the tool cannot read is the operator's to fix, not a stack
|
|
54
|
+
* trace to decode.
|
|
55
|
+
*
|
|
56
|
+
* @param {object} raw a v1 or v2 descriptor
|
|
57
|
+
* @param {{source?: string}} [options]
|
|
58
|
+
* @returns {object} the v2 descriptor
|
|
59
|
+
*/
|
|
60
|
+
export function normaliseTask(raw, { source = 'task' } = {}) {
|
|
61
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
62
|
+
fail(source, 'must be a JSON object');
|
|
63
|
+
}
|
|
64
|
+
const id = String(raw.id ?? '');
|
|
65
|
+
if (!ID_PATTERN.test(id)) {
|
|
66
|
+
fail(source, `field "id" must match ${ID_PATTERN} (received ${JSON.stringify(raw.id ?? null)})`);
|
|
67
|
+
}
|
|
68
|
+
// v1 descriptors in the wild are partial — a fixture may name only an id and a prompt.
|
|
69
|
+
// A v2 descriptor is written by this package and is held to its own shape, so the
|
|
70
|
+
// strictness lands on the writer that can satisfy it.
|
|
71
|
+
const base = raw.base ?? null;
|
|
72
|
+
if (base !== null && (typeof base !== 'string' || base.length === 0)) {
|
|
73
|
+
fail(source, `field "base" must name the commit the task is mined from (received ${JSON.stringify(base)})`);
|
|
74
|
+
}
|
|
75
|
+
const prompt = typeof raw.prompt === 'string' ? raw.prompt : null;
|
|
76
|
+
if (prompt === null && raw.prompt !== undefined) {
|
|
77
|
+
fail(source, 'field "prompt" must be the text an agent is given');
|
|
78
|
+
}
|
|
79
|
+
const declaredV2 = Number(raw.schemaVersion ?? 1) >= TASK_SCHEMA_VERSION;
|
|
80
|
+
if (declaredV2 && (base === null || prompt === null || prompt.trim().length === 0)) {
|
|
81
|
+
fail(source, 'a schema v2 task names its base commit and its prompt');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const f2p = stringList(raw.f2p);
|
|
85
|
+
const p2p = stringList(raw.p2p);
|
|
86
|
+
if (raw.f2p !== undefined && f2p === null) fail(source, 'field "f2p" must be an array of test ids');
|
|
87
|
+
if (raw.p2p !== undefined && p2p === null) fail(source, 'field "p2p" must be an array of test ids');
|
|
88
|
+
|
|
89
|
+
const oracleRaw = raw.oracle && typeof raw.oracle === 'object' ? raw.oracle : {};
|
|
90
|
+
const type = String(oracleRaw.type ?? 'tests');
|
|
91
|
+
if (!ORACLE_TYPES.has(type)) {
|
|
92
|
+
fail(source, `oracle type "${type}" is not one this package grades; known: ${[...ORACLE_TYPES].join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
if (declaredV2 && type === 'tests' && f2p === null) {
|
|
95
|
+
fail(source, 'a schema v2 task with a "tests" oracle lists its fail-to-pass ids in "f2p" (an empty list is a task nothing proves)');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const visibleRaw = raw.agentVisible && typeof raw.agentVisible === 'object' ? raw.agentVisible : {};
|
|
99
|
+
const visibleKind = String(visibleRaw.kind ?? 'repository');
|
|
100
|
+
if (!AGENT_VISIBLE_KINDS.has(visibleKind)) {
|
|
101
|
+
fail(source, `agentVisible kind "${visibleKind}" is not one this package knows; known: ${[...AGENT_VISIBLE_KINDS].join(', ')}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const goldRaw = raw.gold && typeof raw.gold === 'object' ? raw.gold : {};
|
|
105
|
+
const gold = {
|
|
106
|
+
files: Number(goldRaw.files ?? 0),
|
|
107
|
+
linesAdded: Number(goldRaw.linesAdded ?? 0),
|
|
108
|
+
linesRemoved: Number(goldRaw.linesRemoved ?? 0),
|
|
109
|
+
};
|
|
110
|
+
if (!Number.isFinite(gold.files) || gold.files < 0) fail(source, 'field "gold.files" must be a non-negative number');
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
...raw,
|
|
114
|
+
schemaVersion: TASK_SCHEMA_VERSION,
|
|
115
|
+
id,
|
|
116
|
+
base,
|
|
117
|
+
prompt: prompt ?? '',
|
|
118
|
+
stratum: raw.stratum ?? stratumOf(gold),
|
|
119
|
+
agentVisible: { ...visibleRaw, kind: visibleKind },
|
|
120
|
+
oracle: { ...oracleRaw, type, tests: { f2p: f2p ?? [], p2p: p2p ?? [] } },
|
|
121
|
+
f2p: f2p ?? [],
|
|
122
|
+
p2p: p2p ?? [],
|
|
123
|
+
gold,
|
|
124
|
+
};
|
|
125
|
+
}
|