@dailephd/my-dev-kit-lab 0.8.1 → 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 +20 -1
- package/README.md +20 -4
- 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 +54 -2
- package/dist/src/commands/runExperimentDescribeCommand.js +60 -2
- package/dist/src/commands/runExperimentRunCommand.js +208 -5
- 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/evaluation/runMyDevKitRetrieval.js +1 -0
- package/dist/src/experiments/defaultRegistry.js +4 -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/contextPackGeneration/analysis.js +157 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
- package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
- package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
- package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
- package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
- package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
- package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
- package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
- package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -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/buildContextPackGenerationReport.js +171 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +49 -2
- package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
- package/dist/src/report/experiments/index.js +9 -0
- package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
- package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
- package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +12 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
- package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
- package/docs/METRICS.md +98 -1
- package/package.json +1 -1
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { TASK_LOCALITIES } from "../types.js";
|
|
2
|
+
import { normalizeProjectRelativePath } from "./taskPaths.js";
|
|
3
|
+
import { AGENT_SUCCESS_TASK_SCHEMA_VERSION, AgentSuccessTaskValidationError, MAX_VERIFICATION_CHECK_TIMEOUT_MS } from "./taskTypes.js";
|
|
4
|
+
const MAX_ISSUES = 50;
|
|
5
|
+
const MAX_SHORT_TEXT = 1000;
|
|
6
|
+
const MAX_INSTRUCTION = 20_000;
|
|
7
|
+
const MAX_FIXTURE_PATCH_BYTES = 1_048_576;
|
|
8
|
+
const MAX_CHECK_ARGS = 64;
|
|
9
|
+
const MAX_CHECK_ARG_LENGTH = 4096;
|
|
10
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
11
|
+
const CHECK_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
12
|
+
const TASK_FIELDS = [
|
|
13
|
+
"schemaVersion",
|
|
14
|
+
"id",
|
|
15
|
+
"title",
|
|
16
|
+
"benchmarkProject",
|
|
17
|
+
"projectProfileRef",
|
|
18
|
+
"taskLocality",
|
|
19
|
+
"query",
|
|
20
|
+
"sourceRoots",
|
|
21
|
+
"rawIncludeGlobs",
|
|
22
|
+
"instruction",
|
|
23
|
+
"expectedEditFiles",
|
|
24
|
+
"allowedEditFiles",
|
|
25
|
+
"protectedFiles",
|
|
26
|
+
"taskChecks",
|
|
27
|
+
"regressionChecks",
|
|
28
|
+
"behaviorFacts",
|
|
29
|
+
"deterministicFixture"
|
|
30
|
+
];
|
|
31
|
+
const CHECK_FIELDS = ["id", "executable", "args", "timeoutMs"];
|
|
32
|
+
const FACT_FIELDS = ["id", "text", "required", "verificationCheckIds"];
|
|
33
|
+
const FIXTURE_FIELDS = ["id", "patch", "notes"];
|
|
34
|
+
function isRecord(value) {
|
|
35
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36
|
+
}
|
|
37
|
+
class IssueCollector {
|
|
38
|
+
issues = [];
|
|
39
|
+
add(code, path, message) {
|
|
40
|
+
if (this.issues.length < MAX_ISSUES)
|
|
41
|
+
this.issues.push({ code, path, message });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function rejectUnknownFields(collector, value, allowed, base) {
|
|
45
|
+
for (const key of Object.keys(value)) {
|
|
46
|
+
if (!allowed.includes(key))
|
|
47
|
+
collector.add("UNKNOWN_FIELD", base === "" ? key : `${base}.${key}`, `unknown field "${key}".`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function readText(collector, source, key, base, maxLength) {
|
|
51
|
+
const path = base === "" ? key : `${base}.${key}`;
|
|
52
|
+
const value = source[key];
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
collector.add("MISSING_FIELD", path, "required field is missing.");
|
|
55
|
+
return "";
|
|
56
|
+
}
|
|
57
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
58
|
+
collector.add("INVALID_TYPE", path, "must be a non-empty string.");
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
61
|
+
if (value.length > maxLength) {
|
|
62
|
+
collector.add("INVALID_VALUE", path, `must be at most ${maxLength} characters.`);
|
|
63
|
+
}
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
function readArray(collector, source, key) {
|
|
67
|
+
const value = source[key];
|
|
68
|
+
if (value === undefined) {
|
|
69
|
+
collector.add("MISSING_FIELD", key, "required field is missing.");
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
if (!Array.isArray(value)) {
|
|
73
|
+
collector.add("INVALID_TYPE", key, "must be an array.");
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return value;
|
|
77
|
+
}
|
|
78
|
+
/** Normalizes, rejects duplicates, and returns the list sorted in code-unit order. */
|
|
79
|
+
function readPathList(collector, source, key, requireNonEmpty) {
|
|
80
|
+
const raw = readArray(collector, source, key);
|
|
81
|
+
if (raw === undefined)
|
|
82
|
+
return [];
|
|
83
|
+
if (requireNonEmpty && raw.length === 0)
|
|
84
|
+
collector.add("INVALID_VALUE", key, "must not be empty.");
|
|
85
|
+
const seen = new Set();
|
|
86
|
+
raw.forEach((entry, index) => {
|
|
87
|
+
const normalized = normalizeProjectRelativePath(entry);
|
|
88
|
+
const path = `${key}[${index}]`;
|
|
89
|
+
if (!normalized.ok) {
|
|
90
|
+
collector.add(normalized.code, path, `unsafe or malformed project-relative path.`);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (seen.has(normalized.path)) {
|
|
94
|
+
collector.add("DUPLICATE_PATH", path, `duplicate path after normalization: ${normalized.path}.`);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
seen.add(normalized.path);
|
|
98
|
+
});
|
|
99
|
+
return [...seen].sort();
|
|
100
|
+
}
|
|
101
|
+
function readGlobList(collector, source, key) {
|
|
102
|
+
const raw = readArray(collector, source, key);
|
|
103
|
+
if (raw === undefined)
|
|
104
|
+
return [];
|
|
105
|
+
if (raw.length === 0)
|
|
106
|
+
collector.add("INVALID_VALUE", key, "must not be empty.");
|
|
107
|
+
const globs = [];
|
|
108
|
+
raw.forEach((entry, index) => {
|
|
109
|
+
const path = `${key}[${index}]`;
|
|
110
|
+
if (typeof entry !== "string" || entry.trim() === "" || entry.includes("\0")) {
|
|
111
|
+
collector.add("INVALID_TYPE", path, "must be a non-empty string without NUL.");
|
|
112
|
+
}
|
|
113
|
+
else if (/^([A-Za-z]:|[\\/])/.test(entry) || entry.replace(/\\/g, "/").split("/").includes("..")) {
|
|
114
|
+
collector.add("INVALID_VALUE", path, "must be a project-relative glob without traversal.");
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
globs.push(entry);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return globs;
|
|
121
|
+
}
|
|
122
|
+
function readChecks(collector, source, key) {
|
|
123
|
+
const raw = readArray(collector, source, key);
|
|
124
|
+
if (raw === undefined)
|
|
125
|
+
return [];
|
|
126
|
+
if (raw.length < 1)
|
|
127
|
+
collector.add("TOO_FEW_CHECKS", key, "at least one check is required.");
|
|
128
|
+
const checks = [];
|
|
129
|
+
raw.forEach((entry, index) => {
|
|
130
|
+
const base = `${key}[${index}]`;
|
|
131
|
+
if (!isRecord(entry)) {
|
|
132
|
+
collector.add("NOT_OBJECT", base, "must be an object.");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
rejectUnknownFields(collector, entry, CHECK_FIELDS, base);
|
|
136
|
+
const id = entry.id;
|
|
137
|
+
if (typeof id !== "string" || !CHECK_ID_PATTERN.test(id)) {
|
|
138
|
+
collector.add("INVALID_VALUE", `${base}.id`, `must match ${CHECK_ID_PATTERN.source}.`);
|
|
139
|
+
}
|
|
140
|
+
if (entry.executable !== "node") {
|
|
141
|
+
collector.add("INVALID_EXECUTABLE", `${base}.executable`, 'the only supported executable is "node".');
|
|
142
|
+
}
|
|
143
|
+
const args = entry.args;
|
|
144
|
+
if (!Array.isArray(args) ||
|
|
145
|
+
args.length > MAX_CHECK_ARGS ||
|
|
146
|
+
!args.every((arg) => typeof arg === "string" && !arg.includes("\0") && arg.length <= MAX_CHECK_ARG_LENGTH)) {
|
|
147
|
+
collector.add("INVALID_TYPE", `${base}.args`, `must be an array of at most ${MAX_CHECK_ARGS} strings without NUL.`);
|
|
148
|
+
}
|
|
149
|
+
const timeoutMs = entry.timeoutMs;
|
|
150
|
+
if (typeof timeoutMs !== "number" || !Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > MAX_VERIFICATION_CHECK_TIMEOUT_MS) {
|
|
151
|
+
collector.add("INVALID_TIMEOUT", `${base}.timeoutMs`, `must be a finite number in (0, ${MAX_VERIFICATION_CHECK_TIMEOUT_MS}].`);
|
|
152
|
+
}
|
|
153
|
+
if (typeof id === "string" && Array.isArray(args) && typeof timeoutMs === "number") {
|
|
154
|
+
checks.push({ id, executable: "node", args: args, timeoutMs });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
return checks;
|
|
158
|
+
}
|
|
159
|
+
function readFacts(collector, source, checkIds) {
|
|
160
|
+
const raw = readArray(collector, source, "behaviorFacts");
|
|
161
|
+
if (raw === undefined)
|
|
162
|
+
return [];
|
|
163
|
+
const facts = [];
|
|
164
|
+
const seen = new Set();
|
|
165
|
+
raw.forEach((entry, index) => {
|
|
166
|
+
const base = `behaviorFacts[${index}]`;
|
|
167
|
+
if (!isRecord(entry)) {
|
|
168
|
+
collector.add("NOT_OBJECT", base, "must be an object.");
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
rejectUnknownFields(collector, entry, FACT_FIELDS, base);
|
|
172
|
+
const id = readText(collector, entry, "id", base, 128);
|
|
173
|
+
const text = readText(collector, entry, "text", base, MAX_SHORT_TEXT);
|
|
174
|
+
if (id !== "" && seen.has(id))
|
|
175
|
+
collector.add("DUPLICATE_FACT_ID", `${base}.id`, `duplicate fact id: ${id}.`);
|
|
176
|
+
seen.add(id);
|
|
177
|
+
if (typeof entry.required !== "boolean")
|
|
178
|
+
collector.add("INVALID_TYPE", `${base}.required`, "must be a boolean.");
|
|
179
|
+
const ids = entry.verificationCheckIds;
|
|
180
|
+
if (!Array.isArray(ids) || !ids.every((value) => typeof value === "string")) {
|
|
181
|
+
collector.add("INVALID_TYPE", `${base}.verificationCheckIds`, "must be an array of strings.");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
ids.forEach((checkId, checkIndex) => {
|
|
185
|
+
if (!checkIds.has(checkId)) {
|
|
186
|
+
collector.add("UNRESOLVED_CHECK_ID", `${base}.verificationCheckIds[${checkIndex}]`, `unknown check id: ${checkId}.`);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
if (entry.required === true && ids.length === 0) {
|
|
190
|
+
collector.add("REQUIRED_FACT_UNMAPPED", `${base}.verificationCheckIds`, "a required fact must reference at least one check.");
|
|
191
|
+
}
|
|
192
|
+
facts.push({ id, text, required: entry.required === true, verificationCheckIds: [...ids] });
|
|
193
|
+
});
|
|
194
|
+
return facts;
|
|
195
|
+
}
|
|
196
|
+
function readFixture(collector, source) {
|
|
197
|
+
const raw = source.deterministicFixture;
|
|
198
|
+
if (raw === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
if (!isRecord(raw)) {
|
|
201
|
+
collector.add("NOT_OBJECT", "deterministicFixture", "must be an object.");
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
rejectUnknownFields(collector, raw, FIXTURE_FIELDS, "deterministicFixture");
|
|
205
|
+
const id = readText(collector, raw, "id", "deterministicFixture", 128);
|
|
206
|
+
const patch = readText(collector, raw, "patch", "deterministicFixture", MAX_FIXTURE_PATCH_BYTES);
|
|
207
|
+
const fixture = { id, patch };
|
|
208
|
+
if (raw.notes !== undefined) {
|
|
209
|
+
if (typeof raw.notes === "string")
|
|
210
|
+
fixture.notes = raw.notes;
|
|
211
|
+
else
|
|
212
|
+
collector.add("INVALID_TYPE", "deterministicFixture.notes", "must be a string.");
|
|
213
|
+
}
|
|
214
|
+
return fixture;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Validates an AgentSuccessTaskV1 candidate. Closed-set: unknown fields are rejected. Returns the normalized
|
|
218
|
+
* task (POSIX paths, sorted, de-duplicated by rejection) or a bounded, deterministically ordered issue list.
|
|
219
|
+
*/
|
|
220
|
+
export function validateAgentSuccessTask(input) {
|
|
221
|
+
const collector = new IssueCollector();
|
|
222
|
+
if (!isRecord(input)) {
|
|
223
|
+
collector.add("NOT_OBJECT", "", "a task must be an object.");
|
|
224
|
+
return { ok: false, issues: collector.issues };
|
|
225
|
+
}
|
|
226
|
+
rejectUnknownFields(collector, input, TASK_FIELDS, "");
|
|
227
|
+
if (input.schemaVersion !== AGENT_SUCCESS_TASK_SCHEMA_VERSION) {
|
|
228
|
+
collector.add("INVALID_SCHEMA_VERSION", "schemaVersion", `must equal ${AGENT_SUCCESS_TASK_SCHEMA_VERSION}.`);
|
|
229
|
+
}
|
|
230
|
+
const id = readText(collector, input, "id", "", 128);
|
|
231
|
+
if (id !== "" && !ID_PATTERN.test(id))
|
|
232
|
+
collector.add("INVALID_VALUE", "id", `must match ${ID_PATTERN.source}.`);
|
|
233
|
+
const title = readText(collector, input, "title", "", MAX_SHORT_TEXT);
|
|
234
|
+
const benchmarkProject = readText(collector, input, "benchmarkProject", "", MAX_SHORT_TEXT);
|
|
235
|
+
const projectProfileRef = readText(collector, input, "projectProfileRef", "", MAX_SHORT_TEXT);
|
|
236
|
+
const query = readText(collector, input, "query", "", MAX_SHORT_TEXT);
|
|
237
|
+
const instruction = readText(collector, input, "instruction", "", MAX_INSTRUCTION);
|
|
238
|
+
const taskLocality = input.taskLocality;
|
|
239
|
+
if (!TASK_LOCALITIES.includes(taskLocality)) {
|
|
240
|
+
collector.add("INVALID_VALUE", "taskLocality", `must be one of ${TASK_LOCALITIES.join(", ")}.`);
|
|
241
|
+
}
|
|
242
|
+
const sourceRoots = readPathList(collector, input, "sourceRoots", true);
|
|
243
|
+
const rawIncludeGlobs = readGlobList(collector, input, "rawIncludeGlobs");
|
|
244
|
+
const expectedEditFiles = readPathList(collector, input, "expectedEditFiles", false);
|
|
245
|
+
const allowedEditFiles = readPathList(collector, input, "allowedEditFiles", false);
|
|
246
|
+
const protectedFiles = readPathList(collector, input, "protectedFiles", false);
|
|
247
|
+
const allowed = new Set(allowedEditFiles);
|
|
248
|
+
expectedEditFiles.forEach((file) => {
|
|
249
|
+
if (!allowed.has(file))
|
|
250
|
+
collector.add("EXPECTED_NOT_ALLOWED", "expectedEditFiles", `${file} is not in allowedEditFiles.`);
|
|
251
|
+
});
|
|
252
|
+
const protectedSet = new Set(protectedFiles);
|
|
253
|
+
for (const file of [...expectedEditFiles, ...allowedEditFiles]) {
|
|
254
|
+
if (protectedSet.has(file))
|
|
255
|
+
collector.add("PROTECTED_OVERLAP", "protectedFiles", `${file} is also an expected or allowed edit file.`);
|
|
256
|
+
}
|
|
257
|
+
const taskChecks = readChecks(collector, input, "taskChecks");
|
|
258
|
+
const regressionChecks = readChecks(collector, input, "regressionChecks");
|
|
259
|
+
const checkIds = new Set();
|
|
260
|
+
for (const check of [...taskChecks, ...regressionChecks]) {
|
|
261
|
+
if (checkIds.has(check.id))
|
|
262
|
+
collector.add("DUPLICATE_CHECK_ID", "taskChecks", `check id used more than once: ${check.id}.`);
|
|
263
|
+
checkIds.add(check.id);
|
|
264
|
+
}
|
|
265
|
+
const behaviorFacts = readFacts(collector, input, checkIds);
|
|
266
|
+
const deterministicFixture = readFixture(collector, input);
|
|
267
|
+
if (collector.issues.length > 0) {
|
|
268
|
+
return { ok: false, issues: collector.issues };
|
|
269
|
+
}
|
|
270
|
+
const task = {
|
|
271
|
+
schemaVersion: AGENT_SUCCESS_TASK_SCHEMA_VERSION,
|
|
272
|
+
id,
|
|
273
|
+
title,
|
|
274
|
+
benchmarkProject,
|
|
275
|
+
projectProfileRef,
|
|
276
|
+
taskLocality: taskLocality,
|
|
277
|
+
query,
|
|
278
|
+
sourceRoots,
|
|
279
|
+
rawIncludeGlobs,
|
|
280
|
+
instruction,
|
|
281
|
+
expectedEditFiles,
|
|
282
|
+
allowedEditFiles,
|
|
283
|
+
protectedFiles,
|
|
284
|
+
taskChecks,
|
|
285
|
+
regressionChecks,
|
|
286
|
+
behaviorFacts,
|
|
287
|
+
...(deterministicFixture ? { deterministicFixture } : {})
|
|
288
|
+
};
|
|
289
|
+
return { ok: true, task };
|
|
290
|
+
}
|
|
291
|
+
/** Throwing variant for callers that treat an invalid task as a programming/configuration error. */
|
|
292
|
+
export function assertAgentSuccessTask(input) {
|
|
293
|
+
const result = validateAgentSuccessTask(input);
|
|
294
|
+
if (!result.ok)
|
|
295
|
+
throw new AgentSuccessTaskValidationError(result.issues);
|
|
296
|
+
return result.task;
|
|
297
|
+
}
|