@dailephd/my-dev-kit-lab 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +18 -3
- package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
- package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
- package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
- package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
- package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
- package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
- package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
- package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
- package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
- package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
- package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
- package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
- package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
- package/dist/src/cli/help.js +38 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +52 -1
- package/dist/src/commands/runExperimentRunCommand.js +118 -2
- package/dist/src/core/processTree.js +55 -0
- package/dist/src/core/runMeasuredCommand.js +104 -22
- package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
- package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
- package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
- package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
- package/dist/src/evaluation/agentSuccess/index.js +14 -0
- package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
- package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
- package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
- package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
- package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
- package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
- package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
- package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
- package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
- package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
- package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
- package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
- package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
- package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
- package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
- package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
- package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
- package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
- package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
- package/dist/src/evaluation/changeSet/index.js +2 -0
- package/dist/src/evaluation/changeSet/types.js +9 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
- package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
- package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
- package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
- package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
- package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
- package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
- package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
- package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
- package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
- package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +27 -2
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +5 -0
- package/docs/METRICS.md +70 -0
- package/package.json +1 -1
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { AGENT_SUCCESS_CORPUS_FORBIDDEN_DIRECTORIES, AGENT_SUCCESS_CORPUS_PROJECT_FILES, AGENT_SUCCESS_PROJECTS_DIRECTORY, AGENT_SUCCESS_PROJECT_PROFILES_PATH, AGENT_SUCCESS_TASK_CATALOG_PATH, AgentSuccessCorpusError } from "./corpusTypes.js";
|
|
4
|
+
import { validateAgentSuccessCorpus } from "./validateAgentSuccessCorpus.js";
|
|
5
|
+
const MAX_TREE_ENTRIES = 2_000;
|
|
6
|
+
const MAX_ISSUES = 60;
|
|
7
|
+
const MAX_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
8
|
+
const TEST_NAMING_PATTERN = /\.(test|spec)\.[cm]?[jt]sx?$/i;
|
|
9
|
+
class Collector {
|
|
10
|
+
issues = [];
|
|
11
|
+
add(code, issuePath, message) {
|
|
12
|
+
if (this.issues.length < MAX_ISSUES)
|
|
13
|
+
this.issues.push({ code, path: issuePath, message });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function readCatalog(collector, rootDir, relativePath) {
|
|
17
|
+
const absolute = path.join(rootDir, ...relativePath.split("/"));
|
|
18
|
+
let stats;
|
|
19
|
+
try {
|
|
20
|
+
stats = lstatSync(absolute);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
collector.add("CATALOG_MISSING", relativePath, "the catalog file is missing.");
|
|
24
|
+
return { found: false };
|
|
25
|
+
}
|
|
26
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
27
|
+
collector.add("CATALOG_NOT_FILE", relativePath, "the catalog must be a regular file, not a link or directory.");
|
|
28
|
+
return { found: false };
|
|
29
|
+
}
|
|
30
|
+
if (stats.size > MAX_CATALOG_BYTES) {
|
|
31
|
+
collector.add("CATALOG_UNREADABLE", relativePath, `the catalog exceeds ${MAX_CATALOG_BYTES} bytes.`);
|
|
32
|
+
return { found: false };
|
|
33
|
+
}
|
|
34
|
+
let text;
|
|
35
|
+
try {
|
|
36
|
+
text = readFileSync(absolute, "utf8");
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
collector.add("CATALOG_UNREADABLE", relativePath, "the catalog could not be read.");
|
|
40
|
+
return { found: false };
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
return { found: true, value: JSON.parse(text) };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
collector.add("CATALOG_INVALID_JSON", relativePath, `the catalog is not valid JSON (${error instanceof Error ? error.name : "error"}).`);
|
|
47
|
+
return { found: false };
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function realDirectory(absolute) {
|
|
51
|
+
try {
|
|
52
|
+
if (lstatSync(absolute).isSymbolicLink())
|
|
53
|
+
return null;
|
|
54
|
+
return realpathSync(absolute);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isInside(root, candidate) {
|
|
61
|
+
const relative = path.relative(root, candidate);
|
|
62
|
+
return relative !== "" && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
63
|
+
}
|
|
64
|
+
/** Lists project-relative POSIX paths of every entry below `projectRoot`, using lstat so links are never followed. */
|
|
65
|
+
function walkProject(collector, projectRoot, label) {
|
|
66
|
+
const files = new Set();
|
|
67
|
+
const directories = new Set();
|
|
68
|
+
let visited = 0;
|
|
69
|
+
const visit = (relative) => {
|
|
70
|
+
const directory = relative === "" ? projectRoot : path.join(projectRoot, ...relative.split("/"));
|
|
71
|
+
for (const name of readdirSync(directory).sort()) {
|
|
72
|
+
if (++visited > MAX_TREE_ENTRIES)
|
|
73
|
+
return;
|
|
74
|
+
const entryRelative = relative === "" ? name : `${relative}/${name}`;
|
|
75
|
+
const stats = lstatSync(path.join(directory, name));
|
|
76
|
+
if (stats.isSymbolicLink()) {
|
|
77
|
+
collector.add("PROJECT_SYMLINK", `${label}/${entryRelative}`, "links are not allowed in a canonical benchmark project.");
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (stats.isDirectory()) {
|
|
81
|
+
directories.add(entryRelative);
|
|
82
|
+
if (AGENT_SUCCESS_CORPUS_FORBIDDEN_DIRECTORIES.includes(name)) {
|
|
83
|
+
collector.add("PROJECT_FORBIDDEN_OUTPUT", `${label}/${entryRelative}`, "generated or tool output is not allowed in a canonical benchmark project.");
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
visit(entryRelative);
|
|
87
|
+
}
|
|
88
|
+
else if (stats.isFile()) {
|
|
89
|
+
files.add(entryRelative);
|
|
90
|
+
if (TEST_NAMING_PATTERN.test(name)) {
|
|
91
|
+
collector.add("PROJECT_TEST_NAMING", `${label}/${entryRelative}`, "benchmark checks must be named *.check.mjs so root test discovery never runs them.");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
collector.add("PROJECT_SYMLINK", `${label}/${entryRelative}`, "only regular files and directories are allowed.");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
visit("");
|
|
100
|
+
if (visited > MAX_TREE_ENTRIES)
|
|
101
|
+
collector.add("PROJECT_TREE_TOO_LARGE", label, `the project has more than ${MAX_TREE_ENTRIES} entries.`);
|
|
102
|
+
return { files, directories };
|
|
103
|
+
}
|
|
104
|
+
function checkProjectMetadata(collector, projectRoot, label) {
|
|
105
|
+
try {
|
|
106
|
+
const metadata = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8"));
|
|
107
|
+
const engines = metadata.engines;
|
|
108
|
+
const problems = [];
|
|
109
|
+
if (metadata.private !== true)
|
|
110
|
+
problems.push("private must be true");
|
|
111
|
+
if (metadata.type !== "module")
|
|
112
|
+
problems.push('type must be "module"');
|
|
113
|
+
if (engines?.node !== ">=24")
|
|
114
|
+
problems.push('engines.node must be ">=24"');
|
|
115
|
+
if (metadata.dependencies !== undefined || metadata.devDependencies !== undefined)
|
|
116
|
+
problems.push("the project must be dependency-free");
|
|
117
|
+
if (problems.length > 0)
|
|
118
|
+
collector.add("PROJECT_METADATA_INVALID", `${label}/package.json`, `${problems.join("; ")}.`);
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
collector.add("PROJECT_METADATA_INVALID", `${label}/package.json`, "package.json is missing or not valid JSON.");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function checkProject(collector, projectsRoot, rootDir, profile, tasks) {
|
|
125
|
+
const label = profile.rootPath;
|
|
126
|
+
const absolute = path.join(rootDir, ...label.split("/"));
|
|
127
|
+
const physical = realDirectory(absolute);
|
|
128
|
+
if (projectsRoot === null || physical === null || !isInside(projectsRoot, physical)) {
|
|
129
|
+
collector.add("PROJECT_ROOT_UNSAFE", label, "the project root must be a real directory inside the controlled benchmark projects directory.");
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const { files, directories } = walkProject(collector, physical, label);
|
|
133
|
+
const required = AGENT_SUCCESS_CORPUS_PROJECT_FILES[profile.projectId] ?? [];
|
|
134
|
+
for (const file of required) {
|
|
135
|
+
if (!files.has(file))
|
|
136
|
+
collector.add("PROJECT_FILE_MISSING", `${label}/${file}`, "a required project file is missing.");
|
|
137
|
+
}
|
|
138
|
+
for (const root of [...profile.sourceRoots, ...profile.testRoots]) {
|
|
139
|
+
if (!directories.has(root))
|
|
140
|
+
collector.add("PROJECT_SCOPE_MISSING", `${label}/${root}`, "a declared source or test root is missing.");
|
|
141
|
+
}
|
|
142
|
+
checkProjectMetadata(collector, physical, label);
|
|
143
|
+
for (const task of tasks.filter((candidate) => candidate.benchmarkProject === profile.projectId)) {
|
|
144
|
+
const referenced = [
|
|
145
|
+
...task.expectedEditFiles,
|
|
146
|
+
...task.allowedEditFiles,
|
|
147
|
+
...task.protectedFiles,
|
|
148
|
+
...[...task.taskChecks, ...task.regressionChecks].map((check) => check.args[1] ?? "")
|
|
149
|
+
];
|
|
150
|
+
for (const file of new Set(referenced)) {
|
|
151
|
+
if (!files.has(file))
|
|
152
|
+
collector.add("PROJECT_FILE_MISSING", `${task.id}:${file}`, "a file referenced by the task does not exist in the project.");
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Reads the dedicated agent-success task and project-profile catalogs from `rootDir`, validates their structure,
|
|
158
|
+
* and checks that the referenced canonical projects exist on disk with the expected layout. Synchronous because
|
|
159
|
+
* `scripts/verify-benchmarks.ts` is synchronous. It never executes a check or a reference patch and never writes.
|
|
160
|
+
*/
|
|
161
|
+
export function readAgentSuccessCorpus(rootDir) {
|
|
162
|
+
const collector = new Collector();
|
|
163
|
+
const profiles = readCatalog(collector, rootDir, AGENT_SUCCESS_PROJECT_PROFILES_PATH);
|
|
164
|
+
const tasks = readCatalog(collector, rootDir, AGENT_SUCCESS_TASK_CATALOG_PATH);
|
|
165
|
+
if (!profiles.found || !tasks.found)
|
|
166
|
+
return { ok: false, issues: collector.issues };
|
|
167
|
+
const structural = validateAgentSuccessCorpus({ profiles: profiles.value, tasks: tasks.value });
|
|
168
|
+
if (!structural.ok)
|
|
169
|
+
return { ok: false, issues: structural.issues };
|
|
170
|
+
const projectsRoot = realDirectory(path.join(rootDir, ...AGENT_SUCCESS_PROJECTS_DIRECTORY.split("/")));
|
|
171
|
+
for (const profile of structural.corpus.profiles) {
|
|
172
|
+
checkProject(collector, projectsRoot, rootDir, profile, structural.corpus.tasks);
|
|
173
|
+
}
|
|
174
|
+
if (collector.issues.length > 0)
|
|
175
|
+
return { ok: false, issues: collector.issues };
|
|
176
|
+
return { ok: true, corpus: structural.corpus };
|
|
177
|
+
}
|
|
178
|
+
export function assertReadAgentSuccessCorpus(rootDir) {
|
|
179
|
+
const result = readAgentSuccessCorpus(rootDir);
|
|
180
|
+
if (!result.ok)
|
|
181
|
+
throw new AgentSuccessCorpusError(result.issues);
|
|
182
|
+
return result.corpus;
|
|
183
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { runMeasuredCommand } from "../../core/runMeasuredCommand.js";
|
|
3
|
+
import { buildMinimalHostEnv } from "../benchmarkSandbox/index.js";
|
|
4
|
+
import { VERIFICATION_OUTPUT_LIMIT_BYTES } from "./verificationTypes.js";
|
|
5
|
+
function failureReasonFor(status, processError, exitCode) {
|
|
6
|
+
if (status === "passed")
|
|
7
|
+
return null;
|
|
8
|
+
if (status === "failed")
|
|
9
|
+
return `the check exited with code ${exitCode}.`;
|
|
10
|
+
return processError ?? "the check could not be evaluated.";
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Runs one trusted check: `node` (the running Node binary) with the contract's structured arguments, in the
|
|
14
|
+
* sandbox project directory, with no shell, a clean environment (plus CI=1 and NO_COLOR=1), bounded
|
|
15
|
+
* stdout/stderr, a timeout, and whole-process-tree termination. Evidence files land beside the project copy.
|
|
16
|
+
*/
|
|
17
|
+
export async function runVerificationCheck(options) {
|
|
18
|
+
const { sandbox, check, checkClass, phase } = options;
|
|
19
|
+
const limit = options.outputLimitBytes ?? VERIFICATION_OUTPUT_LIMIT_BYTES;
|
|
20
|
+
const startedAt = new Date().toISOString();
|
|
21
|
+
const started = Date.now();
|
|
22
|
+
try {
|
|
23
|
+
const measured = await runMeasuredCommand({
|
|
24
|
+
commandId: `${checkClass}-${check.id}`,
|
|
25
|
+
executable: process.execPath,
|
|
26
|
+
args: check.args,
|
|
27
|
+
cwd: sandbox.projectRoot,
|
|
28
|
+
outDir: path.join(sandbox.evidenceRoot, "verification", phase),
|
|
29
|
+
env: buildMinimalHostEnv(process.env, { CI: "1", NO_COLOR: "1" }),
|
|
30
|
+
inheritParentEnv: false,
|
|
31
|
+
resolveCommand: false,
|
|
32
|
+
timeoutMs: check.timeoutMs,
|
|
33
|
+
stdoutMaxBytes: limit,
|
|
34
|
+
stderrMaxBytes: limit,
|
|
35
|
+
terminateProcessTree: true
|
|
36
|
+
});
|
|
37
|
+
const timedOut = measured.timedOut === true;
|
|
38
|
+
const outputLimit = measured.outputLimit ?? null;
|
|
39
|
+
const status = timedOut
|
|
40
|
+
? "timeout"
|
|
41
|
+
: outputLimit !== null || (measured.exitCode === null && !measured.ok)
|
|
42
|
+
? "error"
|
|
43
|
+
: measured.exitCode === 0
|
|
44
|
+
? "passed"
|
|
45
|
+
: "failed";
|
|
46
|
+
const processError = status === "failed" || status === "passed" ? null : (measured.error ?? null);
|
|
47
|
+
return {
|
|
48
|
+
checkId: check.id,
|
|
49
|
+
class: checkClass,
|
|
50
|
+
executable: "node",
|
|
51
|
+
args: [...check.args],
|
|
52
|
+
startedAt: measured.startedAt,
|
|
53
|
+
endedAt: measured.endedAt,
|
|
54
|
+
durationMs: measured.durationMs,
|
|
55
|
+
status,
|
|
56
|
+
exitCode: measured.exitCode,
|
|
57
|
+
stdoutPath: measured.stdoutPath,
|
|
58
|
+
stderrPath: measured.stderrPath,
|
|
59
|
+
timedOut,
|
|
60
|
+
outputLimit,
|
|
61
|
+
processError,
|
|
62
|
+
failureReason: failureReasonFor(status, processError, measured.exitCode)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
return {
|
|
68
|
+
checkId: check.id,
|
|
69
|
+
class: checkClass,
|
|
70
|
+
executable: "node",
|
|
71
|
+
args: [...check.args],
|
|
72
|
+
startedAt,
|
|
73
|
+
endedAt: new Date().toISOString(),
|
|
74
|
+
durationMs: Date.now() - started,
|
|
75
|
+
status: "error",
|
|
76
|
+
exitCode: null,
|
|
77
|
+
stdoutPath: "",
|
|
78
|
+
stderrPath: "",
|
|
79
|
+
timedOut: false,
|
|
80
|
+
outputLimit: null,
|
|
81
|
+
processError: message,
|
|
82
|
+
failureReason: message
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Runs every task check and then every regression check in contract order. It never short-circuits and
|
|
88
|
+
* never derives a success flag: the caller receives separate raw evidence for each class.
|
|
89
|
+
*/
|
|
90
|
+
export async function runVerificationChecks(options) {
|
|
91
|
+
const taskResults = [];
|
|
92
|
+
for (const check of options.task.taskChecks) {
|
|
93
|
+
taskResults.push(await runVerificationCheck({ sandbox: options.sandbox, check, checkClass: "task", phase: options.phase, outputLimitBytes: options.outputLimitBytes }));
|
|
94
|
+
}
|
|
95
|
+
const regressionResults = [];
|
|
96
|
+
for (const check of options.task.regressionChecks) {
|
|
97
|
+
regressionResults.push(await runVerificationCheck({ sandbox: options.sandbox, check, checkClass: "regression", phase: options.phase, outputLimitBytes: options.outputLimitBytes }));
|
|
98
|
+
}
|
|
99
|
+
return { phase: options.phase, taskResults, regressionResults };
|
|
100
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Normalizes a project-relative file path to POSIX form, or reports why it is unsafe. Shared by the task
|
|
4
|
+
* contract and the patch policy so both apply exactly the same path rules.
|
|
5
|
+
*
|
|
6
|
+
* Rejected: non-strings, empty, NUL, absolute (POSIX or UNC style), drive-qualified, "." alone, any ".."
|
|
7
|
+
* segment (before or after normalization), any ".git" segment (case-insensitive), a trailing slash.
|
|
8
|
+
*/
|
|
9
|
+
export function normalizeProjectRelativePath(raw) {
|
|
10
|
+
if (typeof raw !== "string")
|
|
11
|
+
return { ok: false, code: "PATH_NOT_STRING" };
|
|
12
|
+
if (raw.trim() === "")
|
|
13
|
+
return { ok: false, code: "PATH_EMPTY" };
|
|
14
|
+
if (raw.includes("\0"))
|
|
15
|
+
return { ok: false, code: "PATH_NUL" };
|
|
16
|
+
const slashed = raw.replace(/\\/g, "/");
|
|
17
|
+
if (/^[A-Za-z]:/.test(slashed))
|
|
18
|
+
return { ok: false, code: "PATH_DRIVE" };
|
|
19
|
+
if (slashed.startsWith("/"))
|
|
20
|
+
return { ok: false, code: "PATH_ABSOLUTE" };
|
|
21
|
+
if (slashed.split("/").some((segment) => segment === ".."))
|
|
22
|
+
return { ok: false, code: "PATH_TRAVERSAL" };
|
|
23
|
+
if (slashed.endsWith("/"))
|
|
24
|
+
return { ok: false, code: "PATH_TRAILING_SLASH" };
|
|
25
|
+
const normalized = path.posix.normalize(slashed);
|
|
26
|
+
if (normalized === ".")
|
|
27
|
+
return { ok: false, code: "PATH_DOT" };
|
|
28
|
+
if (normalized === ".." || normalized.startsWith("../"))
|
|
29
|
+
return { ok: false, code: "PATH_TRAVERSAL" };
|
|
30
|
+
if (normalized.split("/").some((segment) => segment.toLowerCase() === ".git")) {
|
|
31
|
+
return { ok: false, code: "PATH_GIT_SEGMENT" };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, path: normalized };
|
|
34
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const AGENT_SUCCESS_TASK_SCHEMA_VERSION = "my-dev-kit-lab-agent-success-task-v1";
|
|
2
|
+
/** Upper bound for one verification check; bounded so a hung check cannot stall an evaluation indefinitely. */
|
|
3
|
+
export const MAX_VERIFICATION_CHECK_TIMEOUT_MS = 300_000;
|
|
4
|
+
export class AgentSuccessTaskValidationError extends Error {
|
|
5
|
+
issues;
|
|
6
|
+
constructor(issues) {
|
|
7
|
+
super(`Invalid agent-success task: ${issues
|
|
8
|
+
.slice(0, 5)
|
|
9
|
+
.map((issue) => `${issue.path}: ${issue.code}`)
|
|
10
|
+
.join("; ")}${issues.length > 5 ? `; +${issues.length - 5} more` : ""}`);
|
|
11
|
+
this.name = "AgentSuccessTaskValidationError";
|
|
12
|
+
this.issues = [...issues];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { AGENT_SUCCESS_CORPUS_PROJECT_IDS, AGENT_SUCCESS_CORPUS_RAW_INCLUDE_GLOBS, AGENT_SUCCESS_CORPUS_TASKS, AGENT_SUCCESS_PROJECTS_DIRECTORY, AGENT_SUCCESS_PROJECT_PROFILES_SCHEMA_VERSION, AgentSuccessCorpusError } from "./corpusTypes.js";
|
|
2
|
+
import { extractPatchCandidate } from "./extractPatchCandidate.js";
|
|
3
|
+
import { parseUnifiedDiff } from "./parseUnifiedDiff.js";
|
|
4
|
+
import { normalizeProjectRelativePath } from "./taskPaths.js";
|
|
5
|
+
import { validateAgentSuccessTask } from "./validateAgentSuccessTask.js";
|
|
6
|
+
import { validatePatchPolicy } from "./validatePatchPolicy.js";
|
|
7
|
+
const MAX_ISSUES = 60;
|
|
8
|
+
const PROJECT_FIELDS = ["projectId", "displayName", "description", "rootPath", "sourceRoots", "testRoots", "primaryLanguage", "runtime"];
|
|
9
|
+
const PROFILES_FIELDS = ["schemaVersion", "projects"];
|
|
10
|
+
const CHECK_FILE_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*\.check\.mjs$/;
|
|
11
|
+
const MIN_LEAK_LINE_LENGTH = 12;
|
|
12
|
+
/** Files every task must protect in addition to its check files. */
|
|
13
|
+
const ALWAYS_PROTECTED = ["README.md", "package.json", "tests/regression.check.mjs"];
|
|
14
|
+
class Collector {
|
|
15
|
+
issues = [];
|
|
16
|
+
add(code, path, message) {
|
|
17
|
+
if (this.issues.length < MAX_ISSUES)
|
|
18
|
+
this.issues.push({ code, path, message });
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
function sameList(left, right) {
|
|
25
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
26
|
+
}
|
|
27
|
+
function isStringList(value) {
|
|
28
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
29
|
+
}
|
|
30
|
+
function readProfiles(collector, input) {
|
|
31
|
+
if (!isRecord(input)) {
|
|
32
|
+
collector.add("PROFILES_INVALID", "profiles", "the project-profile catalog must be an object.");
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
for (const key of Object.keys(input)) {
|
|
36
|
+
if (!PROFILES_FIELDS.includes(key))
|
|
37
|
+
collector.add("PROFILES_INVALID", `profiles.${key}`, `unknown field "${key}".`);
|
|
38
|
+
}
|
|
39
|
+
if (input.schemaVersion !== AGENT_SUCCESS_PROJECT_PROFILES_SCHEMA_VERSION) {
|
|
40
|
+
collector.add("PROFILES_INVALID", "profiles.schemaVersion", `must equal ${AGENT_SUCCESS_PROJECT_PROFILES_SCHEMA_VERSION}.`);
|
|
41
|
+
}
|
|
42
|
+
if (!Array.isArray(input.projects)) {
|
|
43
|
+
collector.add("PROFILES_INVALID", "profiles.projects", "must be an array.");
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
const profiles = [];
|
|
47
|
+
input.projects.forEach((entry, index) => {
|
|
48
|
+
const base = `profiles.projects[${index}]`;
|
|
49
|
+
if (!isRecord(entry)) {
|
|
50
|
+
collector.add("PROFILE_INVALID", base, "must be an object.");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
for (const key of Object.keys(entry)) {
|
|
54
|
+
if (!PROJECT_FIELDS.includes(key))
|
|
55
|
+
collector.add("PROFILE_INVALID", `${base}.${key}`, `unknown field "${key}".`);
|
|
56
|
+
}
|
|
57
|
+
for (const key of ["projectId", "displayName", "description", "rootPath"]) {
|
|
58
|
+
if (typeof entry[key] !== "string" || entry[key].trim() === "") {
|
|
59
|
+
collector.add("PROFILE_INVALID", `${base}.${key}`, "must be a non-empty string.");
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!isStringList(entry.sourceRoots) || !sameList(entry.sourceRoots, ["src"])) {
|
|
63
|
+
collector.add("PROFILE_INVALID", `${base}.sourceRoots`, 'must equal ["src"].');
|
|
64
|
+
}
|
|
65
|
+
if (!isStringList(entry.testRoots) || !sameList(entry.testRoots, ["tests"])) {
|
|
66
|
+
collector.add("PROFILE_INVALID", `${base}.testRoots`, 'must equal ["tests"].');
|
|
67
|
+
}
|
|
68
|
+
if (entry.primaryLanguage !== "javascript")
|
|
69
|
+
collector.add("PROFILE_INVALID", `${base}.primaryLanguage`, 'must equal "javascript".');
|
|
70
|
+
if (entry.runtime !== "node>=24")
|
|
71
|
+
collector.add("PROFILE_INVALID", `${base}.runtime`, 'must equal "node>=24".');
|
|
72
|
+
if (typeof entry.projectId === "string" && typeof entry.rootPath === "string") {
|
|
73
|
+
const normalized = normalizeProjectRelativePath(entry.rootPath);
|
|
74
|
+
const expected = `${AGENT_SUCCESS_PROJECTS_DIRECTORY}/${entry.projectId}`;
|
|
75
|
+
if (!normalized.ok || normalized.path !== expected) {
|
|
76
|
+
collector.add("PROFILE_ROOT_INVALID", `${base}.rootPath`, `must equal ${expected} (inside the controlled benchmark projects directory).`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
profiles.push({
|
|
80
|
+
projectId: String(entry.projectId ?? ""),
|
|
81
|
+
displayName: String(entry.displayName ?? ""),
|
|
82
|
+
description: String(entry.description ?? ""),
|
|
83
|
+
rootPath: String(entry.rootPath ?? ""),
|
|
84
|
+
sourceRoots: isStringList(entry.sourceRoots) ? [...entry.sourceRoots] : [],
|
|
85
|
+
testRoots: isStringList(entry.testRoots) ? [...entry.testRoots] : [],
|
|
86
|
+
primaryLanguage: "javascript",
|
|
87
|
+
runtime: "node>=24"
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
return profiles;
|
|
91
|
+
}
|
|
92
|
+
function validateProfileSet(collector, profiles) {
|
|
93
|
+
const ids = profiles.map((profile) => profile.projectId);
|
|
94
|
+
if (new Set(ids).size !== ids.length)
|
|
95
|
+
collector.add("PROJECT_SET_MISMATCH", "profiles.projects", "project ids must be unique.");
|
|
96
|
+
if (!sameList(ids, AGENT_SUCCESS_CORPUS_PROJECT_IDS)) {
|
|
97
|
+
collector.add("PROJECT_SET_MISMATCH", "profiles.projects", `expected exactly these project ids in order: ${AGENT_SUCCESS_CORPUS_PROJECT_IDS.join(", ")}.`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function checkFileOf(args, testRoot) {
|
|
101
|
+
if (args.length !== 2 || args[0] !== "--test")
|
|
102
|
+
return null;
|
|
103
|
+
const normalized = normalizeProjectRelativePath(args[1]);
|
|
104
|
+
if (!normalized.ok)
|
|
105
|
+
return null;
|
|
106
|
+
const segments = normalized.path.split("/");
|
|
107
|
+
if (segments.length !== 2 || segments[0] !== testRoot)
|
|
108
|
+
return null;
|
|
109
|
+
return CHECK_FILE_PATTERN.test(segments[1]) ? normalized.path : null;
|
|
110
|
+
}
|
|
111
|
+
function validateChecks(collector, task, index, profile) {
|
|
112
|
+
const base = `tasks[${index}]`;
|
|
113
|
+
if (task.taskChecks.length !== 2)
|
|
114
|
+
collector.add("CHECK_COUNT_INVALID", `${base}.taskChecks`, "exactly two task checks are required.");
|
|
115
|
+
if (task.regressionChecks.length < 1)
|
|
116
|
+
collector.add("CHECK_COUNT_INVALID", `${base}.regressionChecks`, "at least one regression check is required.");
|
|
117
|
+
const files = [];
|
|
118
|
+
const testRoot = profile?.testRoots[0] ?? "tests";
|
|
119
|
+
const groups = [
|
|
120
|
+
["taskChecks", task.taskChecks],
|
|
121
|
+
["regressionChecks", task.regressionChecks]
|
|
122
|
+
];
|
|
123
|
+
for (const [key, checks] of groups) {
|
|
124
|
+
checks.forEach((check, checkIndex) => {
|
|
125
|
+
const path = `${base}.${key}[${checkIndex}].args`;
|
|
126
|
+
const unsafe = check.args.some((arg) => normalizeProjectRelativePath(arg).ok === false && arg !== "--test");
|
|
127
|
+
const file = checkFileOf(check.args, testRoot);
|
|
128
|
+
if (file === null) {
|
|
129
|
+
collector.add(unsafe ? "CHECK_PATH_UNSAFE" : "CHECK_COMMAND_INVALID", path, `must be exactly ["--test", "${testRoot}/<name>.check.mjs"].`);
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
files.push(file);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
if (new Set(files).size !== files.length)
|
|
137
|
+
collector.add("CHECK_COMMAND_INVALID", `${base}.taskChecks`, "check files must be unique within a task.");
|
|
138
|
+
return files;
|
|
139
|
+
}
|
|
140
|
+
function validateScopes(collector, task, index, checkFiles) {
|
|
141
|
+
const base = `tasks[${index}]`;
|
|
142
|
+
if (task.expectedEditFiles.length === 0)
|
|
143
|
+
collector.add("EDIT_SCOPE_INVALID", `${base}.expectedEditFiles`, "must not be empty.");
|
|
144
|
+
for (const key of ["expectedEditFiles", "allowedEditFiles"]) {
|
|
145
|
+
for (const file of task[key]) {
|
|
146
|
+
if (!file.startsWith("src/"))
|
|
147
|
+
collector.add("EDIT_SCOPE_INVALID", `${base}.${key}`, `${file} is outside the project source root.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const protectedSet = new Set(task.protectedFiles);
|
|
151
|
+
for (const required of [...ALWAYS_PROTECTED, ...checkFiles]) {
|
|
152
|
+
if (!protectedSet.has(required))
|
|
153
|
+
collector.add("PROTECTED_SCOPE_INVALID", `${base}.protectedFiles`, `${required} must be protected.`);
|
|
154
|
+
}
|
|
155
|
+
for (const file of task.protectedFiles) {
|
|
156
|
+
if (file.startsWith("src/"))
|
|
157
|
+
collector.add("PROTECTED_SCOPE_INVALID", `${base}.protectedFiles`, `${file} is source and must not be protected.`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function validateFacts(collector, task, index) {
|
|
161
|
+
const base = `tasks[${index}].behaviorFacts`;
|
|
162
|
+
if (!task.behaviorFacts.some((fact) => fact.required))
|
|
163
|
+
collector.add("FACT_COVERAGE_INVALID", base, "at least one required fact is needed.");
|
|
164
|
+
if (!task.behaviorFacts.some((fact) => !fact.required))
|
|
165
|
+
collector.add("FACT_COVERAGE_INVALID", base, "at least one optional fact is needed.");
|
|
166
|
+
task.behaviorFacts.forEach((fact, factIndex) => {
|
|
167
|
+
if (fact.verificationCheckIds.length === 0) {
|
|
168
|
+
collector.add("FACT_COVERAGE_INVALID", `${base}[${factIndex}].verificationCheckIds`, "every fact must reference at least one check.");
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function changedLines(patch) {
|
|
173
|
+
return patch
|
|
174
|
+
.split("\n")
|
|
175
|
+
.filter((line) => (line.startsWith("+") && !line.startsWith("+++")) || (line.startsWith("-") && !line.startsWith("---")))
|
|
176
|
+
.map((line) => line.slice(1).trim())
|
|
177
|
+
.filter((line) => line.length >= MIN_LEAK_LINE_LENGTH);
|
|
178
|
+
}
|
|
179
|
+
function validateFixture(collector, task, index) {
|
|
180
|
+
const base = `tasks[${index}].deterministicFixture`;
|
|
181
|
+
const fixture = task.deterministicFixture;
|
|
182
|
+
if (fixture === undefined) {
|
|
183
|
+
collector.add("FIXTURE_MISSING", base, "a deterministic reference fixture is required.");
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const extracted = extractPatchCandidate(fixture.patch);
|
|
187
|
+
if (!extracted.ok) {
|
|
188
|
+
collector.add("FIXTURE_INVALID", base, `the reference patch is not an acceptable patch (${extracted.code}).`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
const parsed = parseUnifiedDiff(extracted.patch);
|
|
192
|
+
if (!parsed.ok) {
|
|
193
|
+
collector.add("FIXTURE_INVALID", base, `the reference patch does not parse (${parsed.code}).`);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
const policy = validatePatchPolicy(parsed.files, { protectedFiles: task.protectedFiles });
|
|
197
|
+
if (!policy.ok) {
|
|
198
|
+
collector.add("FIXTURE_INVALID", base, `the reference patch violates the patch policy (${policy.rejections.map((r) => r.code).join(", ")}).`);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const paths = policy.files.map((file) => file.path);
|
|
202
|
+
if (!sameList(paths, task.expectedEditFiles) || policy.files.some((file) => file.status !== "modified")) {
|
|
203
|
+
collector.add("FIXTURE_SCOPE_MISMATCH", base, "the reference patch must modify exactly the task's expectedEditFiles.");
|
|
204
|
+
}
|
|
205
|
+
const visibleText = `${task.query}\n${task.instruction}\n${task.title}`;
|
|
206
|
+
if (/(^|\n)(diff --git |@@ -)/.test(visibleText) || changedLines(fixture.patch).some((line) => visibleText.includes(line))) {
|
|
207
|
+
collector.add("FIXTURE_LEAKED", `tasks[${index}]`, "the task query, title or instruction reveals reference-patch content.");
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function validateTasks(collector, input, profiles) {
|
|
211
|
+
if (!Array.isArray(input)) {
|
|
212
|
+
collector.add("TASKS_NOT_ARRAY", "tasks", "the task catalog must be an array.");
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
const tasks = [];
|
|
216
|
+
input.forEach((entry, index) => {
|
|
217
|
+
const result = validateAgentSuccessTask(entry);
|
|
218
|
+
if (!result.ok) {
|
|
219
|
+
for (const issue of result.issues.slice(0, 5))
|
|
220
|
+
collector.add("TASK_INVALID", `tasks[${index}].${issue.path}`.replace(/\.$/, ""), `${issue.code}: ${issue.message}`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
tasks.push(result.task);
|
|
224
|
+
});
|
|
225
|
+
if (tasks.length !== input.length)
|
|
226
|
+
return tasks;
|
|
227
|
+
const ids = tasks.map((task) => task.id);
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
ids.forEach((id, index) => {
|
|
230
|
+
if (seen.has(id))
|
|
231
|
+
collector.add("TASK_ID_DUPLICATE", `tasks[${index}].id`, `duplicate task id: ${id}.`);
|
|
232
|
+
seen.add(id);
|
|
233
|
+
});
|
|
234
|
+
const expectedIds = AGENT_SUCCESS_CORPUS_TASKS.map((task) => task.id);
|
|
235
|
+
if (tasks.length !== expectedIds.length) {
|
|
236
|
+
collector.add("TASK_COUNT_MISMATCH", "tasks", `expected exactly ${expectedIds.length} tasks, found ${tasks.length}.`);
|
|
237
|
+
}
|
|
238
|
+
else if (!sameList(ids, expectedIds) && new Set(ids).size === ids.length) {
|
|
239
|
+
collector.add("TASK_ORDER_MISMATCH", "tasks", `expected task ids in this order: ${expectedIds.join(", ")}.`);
|
|
240
|
+
}
|
|
241
|
+
const profileById = new Map(profiles.map((profile) => [profile.projectId, profile]));
|
|
242
|
+
const fixtureIds = new Set();
|
|
243
|
+
const fixturePatches = new Set();
|
|
244
|
+
const perProject = new Map();
|
|
245
|
+
tasks.forEach((task, index) => {
|
|
246
|
+
const base = `tasks[${index}]`;
|
|
247
|
+
const expectation = AGENT_SUCCESS_CORPUS_TASKS.find((candidate) => candidate.id === task.id);
|
|
248
|
+
const profile = profileById.get(task.projectProfileRef);
|
|
249
|
+
if (task.projectProfileRef !== task.benchmarkProject || profile === undefined) {
|
|
250
|
+
collector.add("PROJECT_REF_INVALID", `${base}.projectProfileRef`, "must equal benchmarkProject and resolve to a known project profile.");
|
|
251
|
+
}
|
|
252
|
+
else if (expectation !== undefined && expectation.project !== task.benchmarkProject) {
|
|
253
|
+
collector.add("PROJECT_REF_INVALID", `${base}.benchmarkProject`, `task ${task.id} belongs to ${expectation.project}.`);
|
|
254
|
+
}
|
|
255
|
+
if (expectation !== undefined && expectation.locality !== task.taskLocality) {
|
|
256
|
+
collector.add("LOCALITY_MISMATCH", `${base}.taskLocality`, `task ${task.id} must be ${expectation.locality}.`);
|
|
257
|
+
}
|
|
258
|
+
perProject.set(task.benchmarkProject, [...(perProject.get(task.benchmarkProject) ?? []), task.taskLocality]);
|
|
259
|
+
const sourceRoots = profile?.sourceRoots ?? ["src"];
|
|
260
|
+
if (!sameList(task.sourceRoots, sourceRoots)) {
|
|
261
|
+
collector.add("SOURCE_SCOPE_INVALID", `${base}.sourceRoots`, `must equal the project's source roots (${sourceRoots.join(", ")}).`);
|
|
262
|
+
}
|
|
263
|
+
if (!sameList(task.rawIncludeGlobs, AGENT_SUCCESS_CORPUS_RAW_INCLUDE_GLOBS)) {
|
|
264
|
+
collector.add("SOURCE_SCOPE_INVALID", `${base}.rawIncludeGlobs`, `must equal ${AGENT_SUCCESS_CORPUS_RAW_INCLUDE_GLOBS.join(", ")} so tests and contracts stay out of agent context.`);
|
|
265
|
+
}
|
|
266
|
+
const checkFiles = validateChecks(collector, task, index, profile);
|
|
267
|
+
validateScopes(collector, task, index, checkFiles);
|
|
268
|
+
validateFacts(collector, task, index);
|
|
269
|
+
validateFixture(collector, task, index);
|
|
270
|
+
if (task.deterministicFixture !== undefined) {
|
|
271
|
+
if (fixtureIds.has(task.deterministicFixture.id))
|
|
272
|
+
collector.add("FIXTURE_DUPLICATE", `${base}.deterministicFixture.id`, "fixture ids must be unique.");
|
|
273
|
+
if (fixturePatches.has(task.deterministicFixture.patch))
|
|
274
|
+
collector.add("FIXTURE_DUPLICATE", `${base}.deterministicFixture.patch`, "tasks must not share a reference patch.");
|
|
275
|
+
fixtureIds.add(task.deterministicFixture.id);
|
|
276
|
+
fixturePatches.add(task.deterministicFixture.patch);
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
for (const [project, localities] of perProject) {
|
|
280
|
+
const sorted = [...localities].sort();
|
|
281
|
+
if (!sameList(sorted, ["broad-change", "cross-module", "localized"])) {
|
|
282
|
+
collector.add("LOCALITY_DISTRIBUTION", "tasks", `project ${project} must have exactly one localized, one cross-module and one broad-change task.`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return tasks;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Structural validation of the agent-success implementation corpus. Pure: it only inspects the two parsed
|
|
289
|
+
* catalogs. It never touches the filesystem and never applies or executes a reference patch; that is dynamic
|
|
290
|
+
* benchmark acceptance and is tested separately. The task shape is validated by validateAgentSuccessTask.
|
|
291
|
+
*/
|
|
292
|
+
export function validateAgentSuccessCorpus(input) {
|
|
293
|
+
const collector = new Collector();
|
|
294
|
+
const profiles = readProfiles(collector, input.profiles);
|
|
295
|
+
validateProfileSet(collector, profiles);
|
|
296
|
+
const tasks = validateTasks(collector, input.tasks, profiles);
|
|
297
|
+
if (collector.issues.length > 0)
|
|
298
|
+
return { ok: false, issues: collector.issues };
|
|
299
|
+
const corpus = { profiles, tasks };
|
|
300
|
+
return { ok: true, corpus };
|
|
301
|
+
}
|
|
302
|
+
export function assertAgentSuccessCorpus(input) {
|
|
303
|
+
const result = validateAgentSuccessCorpus(input);
|
|
304
|
+
if (!result.ok)
|
|
305
|
+
throw new AgentSuccessCorpusError(result.issues);
|
|
306
|
+
return result.corpus;
|
|
307
|
+
}
|