@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.
Files changed (99) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +18 -3
  3. package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
  4. package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
  5. package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
  6. package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
  7. package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
  8. package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
  9. package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
  10. package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
  11. package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
  12. package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
  13. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
  14. package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
  15. package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
  16. package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
  17. package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
  18. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
  19. package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
  20. package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
  21. package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
  22. package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
  23. package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
  24. package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
  25. package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
  26. package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
  27. package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
  28. package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
  29. package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
  30. package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
  31. package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
  32. package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
  33. package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
  34. package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
  35. package/dist/src/cli/help.js +38 -1
  36. package/dist/src/commands/runExperimentDescribeCommand.js +52 -1
  37. package/dist/src/commands/runExperimentRunCommand.js +118 -2
  38. package/dist/src/core/processTree.js +55 -0
  39. package/dist/src/core/runMeasuredCommand.js +104 -22
  40. package/dist/src/evaluation/agentSuccess/applyPatchToSandbox.js +37 -0
  41. package/dist/src/evaluation/agentSuccess/assessBaseline.js +23 -0
  42. package/dist/src/evaluation/agentSuccess/corpusTypes.js +74 -0
  43. package/dist/src/evaluation/agentSuccess/extractPatchCandidate.js +46 -0
  44. package/dist/src/evaluation/agentSuccess/index.js +14 -0
  45. package/dist/src/evaluation/agentSuccess/parseUnifiedDiff.js +210 -0
  46. package/dist/src/evaluation/agentSuccess/patchTypes.js +5 -0
  47. package/dist/src/evaluation/agentSuccess/readAgentSuccessCorpus.js +183 -0
  48. package/dist/src/evaluation/agentSuccess/runVerificationCheck.js +100 -0
  49. package/dist/src/evaluation/agentSuccess/taskPaths.js +34 -0
  50. package/dist/src/evaluation/agentSuccess/taskTypes.js +14 -0
  51. package/dist/src/evaluation/agentSuccess/validateAgentSuccessCorpus.js +307 -0
  52. package/dist/src/evaluation/agentSuccess/validateAgentSuccessTask.js +297 -0
  53. package/dist/src/evaluation/agentSuccess/validatePatchPolicy.js +66 -0
  54. package/dist/src/evaluation/agentSuccess/verificationTypes.js +1 -0
  55. package/dist/src/evaluation/benchmarkSandbox/createBenchmarkSandbox.js +127 -0
  56. package/dist/src/evaluation/benchmarkSandbox/errors.js +11 -0
  57. package/dist/src/evaluation/benchmarkSandbox/gitExecutor.js +60 -0
  58. package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
  59. package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
  60. package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
  61. package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
  62. package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
  63. package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
  64. package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
  65. package/dist/src/evaluation/changeSet/index.js +2 -0
  66. package/dist/src/evaluation/changeSet/types.js +9 -0
  67. package/dist/src/experiments/defaultRegistry.js +2 -0
  68. package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
  69. package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
  70. package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
  71. package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
  72. package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
  73. package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
  74. package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
  75. package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
  76. package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
  77. package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
  78. package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
  79. package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
  80. package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
  81. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
  82. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
  83. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
  84. package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
  85. package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
  86. package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
  87. package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
  88. package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
  89. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
  90. package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
  91. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
  92. package/dist/src/report/experiments/buildPluginExperimentReport.js +27 -2
  93. package/dist/src/report/experiments/index.js +4 -0
  94. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
  95. package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
  96. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  97. package/dist/src/report/experiments/renderPluginExperimentReportText.js +5 -0
  98. package/docs/METRICS.md +70 -0
  99. package/package.json +1 -1
@@ -1,8 +1,9 @@
1
+ import { existsSync } from "node:fs";
1
2
  import { mkdir, writeFile } from "node:fs/promises";
2
3
  import path from "node:path";
3
4
  import { spawn } from "node:child_process";
4
5
  import { parseCommandString, serializeCommand } from "./commandLine.js";
5
- import { forceTerminateProcess } from "./processTree.js";
6
+ import { forceTerminateProcess, terminateProcessTree } from "./processTree.js";
6
7
  import { buildResolvedCommandInvocation, resolveCommandInvocation } from "./resolveCommand.js";
7
8
  export { parseCommandString } from "./commandLine.js";
8
9
  export async function runMeasuredCommand(options) {
@@ -10,6 +11,11 @@ export async function runMeasuredCommand(options) {
10
11
  const named = readCommandName(options);
11
12
  const commandString = named.commandString;
12
13
  const trailingArgs = [...named.args, ...(options.extraArgs ?? [])];
14
+ const bounded = options.stdoutMaxBytes !== undefined ||
15
+ options.stderrMaxBytes !== undefined ||
16
+ options.inheritParentEnv === false ||
17
+ options.terminateProcessTree === true;
18
+ const childEnv = options.inheritParentEnv === false ? { ...options.env } : { ...process.env, ...options.env };
13
19
  // Windows .cmd/.bat/.ps1 argument assembly is owned by resolveCommand.ts so
14
20
  // that every spawn site (measured commands and managed long-running processes)
15
21
  // shares exactly one shim rule.
@@ -24,7 +30,7 @@ export async function runMeasuredCommand(options) {
24
30
  }, trailingArgs)
25
31
  : resolveCommandInvocation(named.executable, trailingArgs, {
26
32
  cwd: options.cwd,
27
- env: { ...process.env, ...options.env },
33
+ env: childEnv,
28
34
  allowPowerShellShim: options.allowPowerShellShim
29
35
  });
30
36
  const resolution = invocation.resolvedCommand;
@@ -36,8 +42,9 @@ export async function runMeasuredCommand(options) {
36
42
  const startedAt = new Date().toISOString();
37
43
  const started = Date.now();
38
44
  const result = await new Promise((resolve) => {
39
- let stdout = "";
40
- let stderr = "";
45
+ const stdoutCollector = new OutputCollector(options.stdoutMaxBytes);
46
+ const stderrCollector = new OutputCollector(options.stderrMaxBytes);
47
+ let outputLimit;
41
48
  let spawnError;
42
49
  let timedOut = false;
43
50
  let timeout;
@@ -47,9 +54,12 @@ export async function runMeasuredCommand(options) {
47
54
  try {
48
55
  child = spawn(executable, args, {
49
56
  cwd: options.cwd,
50
- env: { ...process.env, ...options.env },
57
+ env: childEnv,
51
58
  shell: false,
52
- stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"]
59
+ stdio: options.stdinText !== undefined ? ["pipe", "pipe", "pipe"] : ["ignore", "pipe", "pipe"],
60
+ // Own process group so a bounded termination can reach descendants. Windows reaches the tree
61
+ // through `taskkill /T`, and `detached` there would open a new console, so it is not set.
62
+ ...(options.terminateProcessTree === true && process.platform !== "win32" ? { detached: true } : {})
53
63
  });
54
64
  }
55
65
  catch (error) {
@@ -66,8 +76,8 @@ export async function runMeasuredCommand(options) {
66
76
  endedAt,
67
77
  durationMs,
68
78
  exitCode: null,
69
- stdout,
70
- stderr,
79
+ stdout: "",
80
+ stderr: "",
71
81
  stdoutPath,
72
82
  stderrPath,
73
83
  telemetryPath,
@@ -76,22 +86,53 @@ export async function runMeasuredCommand(options) {
76
86
  resolvedCommand: resolution
77
87
  };
78
88
  void Promise.all([
79
- writeArtifact(stdoutPath, stdout),
80
- writeArtifact(stderrPath, stderr),
89
+ writeArtifact(stdoutPath, ""),
90
+ writeArtifact(stderrPath, ""),
81
91
  writeArtifact(telemetryPath, JSON.stringify(measured, null, 2))
82
92
  ]).then(() => resolve(measured));
83
93
  return;
84
94
  }
85
95
  // stdout/stderr are always piped in both stdio branches above.
86
- child.stdout.on("data", (chunk) => {
87
- stdout += String(chunk);
88
- });
89
- child.stderr.on("data", (chunk) => {
90
- stderr += String(chunk);
91
- });
96
+ // The process-termination step is shared by the timeout and the opt-in output limit. Bounded callers
97
+ // get an awaited tree termination; everyone else keeps the legacy fire-and-forget single termination.
98
+ const KILL_GRACE_MS = 3000;
99
+ const terminate = () => {
100
+ if (options.terminateProcessTree === true) {
101
+ void terminateProcessTree(child.pid);
102
+ }
103
+ else {
104
+ forceTerminateProcess(child.pid);
105
+ }
106
+ killGraceTimeout ??= setTimeout(() => {
107
+ void settle(null);
108
+ }, KILL_GRACE_MS);
109
+ };
110
+ const collect = (collector, stream, limit) => (chunk) => {
111
+ if (collector.push(chunk) && limit !== undefined && outputLimit === undefined) {
112
+ outputLimit = { stream, limitBytes: limit };
113
+ spawnError = `Command ${stream} exceeded ${limit} bytes.`;
114
+ terminate();
115
+ }
116
+ };
117
+ child.stdout.on("data", collect(stdoutCollector, "stdout", options.stdoutMaxBytes));
118
+ child.stderr.on("data", collect(stderrCollector, "stderr", options.stderrMaxBytes));
92
119
  child.on("error", (error) => {
93
120
  spawnError = error.message;
121
+ // A failed spawn in a working directory that exists is not a missing executable; the OS may be refusing the path (for
122
+ // example a very long cwd on Windows). Report the measured length
123
+ // without asserting a limit of our own.
124
+ if (error.code === "ENOENT" && existsSync(options.cwd)) {
125
+ spawnError += ` (the working directory exists but the process could not be started in it; cwd length ${options.cwd.length} characters)`;
126
+ }
94
127
  });
128
+ // A failed spawn can also make the OS report the never-connected stdout/stderr pipes as errors (Windows:
129
+ // read ENOTCONN). They are stream events of this one failed process: record the first cause once and let
130
+ // the 'close' event settle the result, instead of letting an unhandled 'error' event end the whole process.
131
+ const recordStreamError = (stream) => (error) => {
132
+ spawnError = spawnError ?? `Failed to read command ${stream}: ${error.message}`;
133
+ };
134
+ child.stdout.on("error", recordStreamError("stdout"));
135
+ child.stderr.on("error", recordStreamError("stderr"));
95
136
  if (options.stdinText !== undefined) {
96
137
  // A child that exits or closes stdin before we write must not crash the process (EPIPE).
97
138
  child.stdin?.on("error", (error) => {
@@ -106,7 +147,7 @@ export async function runMeasuredCommand(options) {
106
147
  // intermediate shim (for example a Windows .cmd launcher) can occasionally outlive an
107
148
  // already-killed intermediate process. Without a bounded fallback, a caller whose kill silently
108
149
  // failed would await this promise forever instead of receiving the already-known timeout error.
109
- const KILL_GRACE_MS = 3000;
150
+ // (KILL_GRACE_MS and the shared `terminate` step are declared above, beside the output collectors.)
110
151
  const settle = async (exitCode) => {
111
152
  if (settled)
112
153
  return;
@@ -128,6 +169,8 @@ export async function runMeasuredCommand(options) {
128
169
  child.unref();
129
170
  const endedAt = new Date().toISOString();
130
171
  const durationMs = Date.now() - started;
172
+ const stdout = stdoutCollector.toString();
173
+ const stderr = stderrCollector.toString();
131
174
  await writeArtifact(stdoutPath, stdout);
132
175
  await writeArtifact(stderrPath, stderr);
133
176
  const measured = {
@@ -147,7 +190,8 @@ export async function runMeasuredCommand(options) {
147
190
  telemetryPath,
148
191
  ok: exitCode === 0 && !spawnError && !timedOut,
149
192
  error: spawnError,
150
- resolvedCommand: resolution
193
+ resolvedCommand: resolution,
194
+ ...(bounded ? { timedOut, ...(outputLimit ? { outputLimit } : {}) } : {})
151
195
  };
152
196
  await writeArtifact(telemetryPath, JSON.stringify(measured, null, 2));
153
197
  resolve(measured);
@@ -156,10 +200,7 @@ export async function runMeasuredCommand(options) {
156
200
  timeout = setTimeout(() => {
157
201
  timedOut = true;
158
202
  spawnError = `Command timed out after ${options.timeoutMs}ms.`;
159
- forceTerminateProcess(child.pid);
160
- killGraceTimeout = setTimeout(() => {
161
- void settle(null);
162
- }, KILL_GRACE_MS);
203
+ terminate();
163
204
  }, options.timeoutMs);
164
205
  }
165
206
  child.on("close", (exitCode) => {
@@ -168,6 +209,47 @@ export async function runMeasuredCommand(options) {
168
209
  });
169
210
  return result;
170
211
  }
212
+ /**
213
+ * Accumulates one output stream. Without a limit it reproduces the legacy `text += String(chunk)`
214
+ * behavior exactly. With a limit it keeps raw bytes (so the cut is on a byte boundary), stops at the
215
+ * limit, and reports once when more output than the limit arrived.
216
+ */
217
+ class OutputCollector {
218
+ limit;
219
+ text = "";
220
+ chunks = [];
221
+ bytes = 0;
222
+ exceeded = false;
223
+ constructor(limit) {
224
+ this.limit = limit;
225
+ }
226
+ /** Returns true exactly once: on the chunk that first exceeds the limit. */
227
+ push(chunk) {
228
+ if (this.limit === undefined) {
229
+ this.text += String(chunk);
230
+ return false;
231
+ }
232
+ if (this.exceeded) {
233
+ return false;
234
+ }
235
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk), "utf8");
236
+ const remaining = this.limit - this.bytes;
237
+ if (buffer.length > remaining) {
238
+ if (remaining > 0) {
239
+ this.chunks.push(buffer.subarray(0, remaining));
240
+ }
241
+ this.bytes = this.limit;
242
+ this.exceeded = true;
243
+ return true;
244
+ }
245
+ this.chunks.push(buffer);
246
+ this.bytes += buffer.length;
247
+ return false;
248
+ }
249
+ toString() {
250
+ return this.limit === undefined ? this.text : Buffer.concat(this.chunks).toString("utf8");
251
+ }
252
+ }
171
253
  async function writeArtifact(filePath, value) {
172
254
  await mkdir(path.dirname(filePath), { recursive: true });
173
255
  await writeFile(filePath, value, "utf8");
@@ -0,0 +1,37 @@
1
+ import { excerptGitFailure, runSandboxGit } from "../benchmarkSandbox/index.js";
2
+ import { extractPatchCandidate } from "./extractPatchCandidate.js";
3
+ import { parseUnifiedDiff } from "./parseUnifiedDiff.js";
4
+ import { DEFAULT_PATCH_BOUNDS } from "./patchTypes.js";
5
+ import { validatePatchPolicy } from "./validatePatchPolicy.js";
6
+ const APPLY_FLAGS = ["--index", "--whitespace=nowarn", "-"];
7
+ /**
8
+ * Lab-owned patch application. The order is the safety contract:
9
+ * extract exactly one candidate -> parse -> policy -> `git apply --check --index` -> `git apply --index`.
10
+ * Nothing touches the sandbox until the check succeeds, so every earlier failure leaves it unchanged.
11
+ * `--unsafe-paths`, `--reject` and `--3way` are never used, and nothing the agent proposed is executed.
12
+ */
13
+ export async function applyPatchToSandbox(options) {
14
+ const bounds = options.bounds ?? DEFAULT_PATCH_BOUNDS;
15
+ const extracted = extractPatchCandidate(options.rawProposal, bounds);
16
+ if (!extracted.ok)
17
+ return { outcome: "parse-failure", code: extracted.code, message: extracted.message };
18
+ const parsed = parseUnifiedDiff(extracted.patch);
19
+ if (!parsed.ok)
20
+ return { outcome: "parse-failure", code: parsed.code, message: parsed.message };
21
+ const policy = validatePatchPolicy(parsed.files, { protectedFiles: options.protectedFiles, bounds });
22
+ if (!policy.ok)
23
+ return { outcome: "policy-rejection", rejections: policy.rejections };
24
+ const check = await runSandboxGit(options.sandbox, ["apply", "--check", ...APPLY_FLAGS], {
25
+ stdin: extracted.patch,
26
+ label: "apply-check"
27
+ });
28
+ if (!check.ok)
29
+ return { outcome: "git-check-failure", message: excerptGitFailure(check) };
30
+ const apply = await runSandboxGit(options.sandbox, ["apply", ...APPLY_FLAGS], {
31
+ stdin: extracted.patch,
32
+ label: "apply"
33
+ });
34
+ if (!apply.ok)
35
+ return { outcome: "git-apply-failure", message: excerptGitFailure(apply) };
36
+ return { outcome: "success", files: policy.files };
37
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Decides whether the unmodified benchmark can evaluate an implementation.
3
+ *
4
+ * Evaluable exactly when at least one task check fails (there is something to fix) and every regression
5
+ * check passes (there is nothing already broken), and no check was unable to produce a verdict. A check
6
+ * that timed out or errored is "not evaluable" rather than "failed": an indeterminate check cannot prove
7
+ * the task is unsolved. An invalid baseline is a benchmark defect, never an agent failure.
8
+ */
9
+ export function assessBaseline(baseline) {
10
+ const reasons = [];
11
+ const all = [...baseline.taskResults, ...baseline.regressionResults];
12
+ if (baseline.taskResults.length === 0 || baseline.regressionResults.length === 0)
13
+ reasons.push("NO_CHECKS");
14
+ if (all.some((result) => result.status === "timeout" || result.status === "error"))
15
+ reasons.push("CHECK_NOT_EVALUABLE");
16
+ if (baseline.taskResults.length > 0 && !baseline.taskResults.some((result) => result.status === "failed")) {
17
+ if (!reasons.includes("CHECK_NOT_EVALUABLE"))
18
+ reasons.push("ALL_TASK_CHECKS_PASSED");
19
+ }
20
+ if (baseline.regressionResults.some((result) => result.status === "failed"))
21
+ reasons.push("REGRESSION_CHECK_FAILED");
22
+ return { evaluable: reasons.length === 0, reasons };
23
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Contracts for the canonical agent-success implementation benchmark corpus. This is a separate contract family
3
+ * from the retrieval benchmark profiles (`benchmark-project-profiles.json`) and must not be merged into it.
4
+ */
5
+ export const AGENT_SUCCESS_PROJECT_PROFILES_SCHEMA_VERSION = "my-dev-kit-lab-agent-success-project-profiles-v1";
6
+ export const AGENT_SUCCESS_TASK_CATALOG_PATH = "benchmarks/contracts/agent-success-rate-tasks.json";
7
+ export const AGENT_SUCCESS_PROJECT_PROFILES_PATH = "benchmarks/contracts/agent-success-rate-project-profiles.json";
8
+ export const AGENT_SUCCESS_PROJECTS_DIRECTORY = "benchmarks/projects";
9
+ export const AGENT_SUCCESS_CORPUS_PROJECT_IDS = ["agent-success-task-board-node", "agent-success-inventory-node"];
10
+ /** The fixed catalog order. Three tasks per project: one localized, one cross-module, one broad-change. */
11
+ export const AGENT_SUCCESS_CORPUS_TASKS = Object.freeze([
12
+ { id: "asr-board-title-normalization", project: "agent-success-task-board-node", locality: "localized" },
13
+ { id: "asr-board-import-idempotency", project: "agent-success-task-board-node", locality: "cross-module" },
14
+ { id: "asr-board-project-summary", project: "agent-success-task-board-node", locality: "broad-change" },
15
+ { id: "asr-inventory-quantity-boundary", project: "agent-success-inventory-node", locality: "localized" },
16
+ { id: "asr-inventory-reservation-atomicity", project: "agent-success-inventory-node", locality: "cross-module" },
17
+ { id: "asr-inventory-fulfillment-report", project: "agent-success-inventory-node", locality: "broad-change" }
18
+ ]);
19
+ /** Files every project must contain, relative to the project root. */
20
+ export const AGENT_SUCCESS_CORPUS_PROJECT_FILES = Object.freeze({
21
+ "agent-success-task-board-node": Object.freeze([
22
+ "README.md",
23
+ "package.json",
24
+ "src/validation.js",
25
+ "src/taskStore.js",
26
+ "src/taskService.js",
27
+ "src/projectSummary.js",
28
+ "src/index.js",
29
+ "tests/title-primary.check.mjs",
30
+ "tests/title-edge.check.mjs",
31
+ "tests/import-primary.check.mjs",
32
+ "tests/import-edge.check.mjs",
33
+ "tests/summary-primary.check.mjs",
34
+ "tests/summary-edge.check.mjs",
35
+ "tests/regression.check.mjs"
36
+ ]),
37
+ "agent-success-inventory-node": Object.freeze([
38
+ "README.md",
39
+ "package.json",
40
+ "src/quantity.js",
41
+ "src/inventoryStore.js",
42
+ "src/reservationService.js",
43
+ "src/fulfillmentReport.js",
44
+ "src/index.js",
45
+ "tests/quantity-primary.check.mjs",
46
+ "tests/quantity-edge.check.mjs",
47
+ "tests/reservation-primary.check.mjs",
48
+ "tests/reservation-edge.check.mjs",
49
+ "tests/report-primary.check.mjs",
50
+ "tests/report-edge.check.mjs",
51
+ "tests/regression.check.mjs"
52
+ ])
53
+ });
54
+ export const AGENT_SUCCESS_CORPUS_RAW_INCLUDE_GLOBS = Object.freeze(["src/**/*.js"]);
55
+ /** Directory names that must never exist inside a canonical project. */
56
+ export const AGENT_SUCCESS_CORPUS_FORBIDDEN_DIRECTORIES = Object.freeze([
57
+ ".git",
58
+ "node_modules",
59
+ "dist",
60
+ "build",
61
+ "coverage",
62
+ "lab-output"
63
+ ]);
64
+ export class AgentSuccessCorpusError extends Error {
65
+ issues;
66
+ constructor(issues) {
67
+ super(`Invalid agent-success corpus: ${issues
68
+ .slice(0, 5)
69
+ .map((issue) => `${issue.path}: ${issue.code}`)
70
+ .join("; ")}${issues.length > 5 ? `; +${issues.length - 5} more` : ""}`);
71
+ this.name = "AgentSuccessCorpusError";
72
+ this.issues = [...issues];
73
+ }
74
+ }
@@ -0,0 +1,46 @@
1
+ import { DEFAULT_PATCH_BOUNDS } from "./patchTypes.js";
2
+ const FENCED_DIFF_BLOCK = /^```[ \t]*(?:diff|patch)[ \t]*\r?\n([\s\S]*?)^```[ \t]*$/gim;
3
+ function looksLikeDiffStart(text) {
4
+ return text.startsWith("diff --git ") || text.startsWith("--- ");
5
+ }
6
+ /** Removes trailing line breaks only: a final context line that is a single space must survive. */
7
+ function finalize(body) {
8
+ return `${body.replace(/(?:\r?\n)+$/, "")}\n`;
9
+ }
10
+ /**
11
+ * Extracts exactly one patch candidate from an agent's proposal: either one fenced `diff`/`patch` block, or
12
+ * the whole (trimmed) text when it is itself a unified diff. Ambiguity is rejected rather than guessed.
13
+ * Size and line bounds are enforced before any parsing.
14
+ */
15
+ export function extractPatchCandidate(text, bounds = DEFAULT_PATCH_BOUNDS) {
16
+ if (typeof text !== "string" || text.trim() === "") {
17
+ return { ok: false, code: "EMPTY", message: "the proposal is empty." };
18
+ }
19
+ if (Buffer.byteLength(text, "utf8") > bounds.maxPatchBytes) {
20
+ return { ok: false, code: "PATCH_TOO_LARGE", message: `the proposal exceeds ${bounds.maxPatchBytes} bytes.` };
21
+ }
22
+ if (text.split("\n").length > bounds.maxPatchLines) {
23
+ return { ok: false, code: "PATCH_TOO_MANY_LINES", message: `the proposal exceeds ${bounds.maxPatchLines} lines.` };
24
+ }
25
+ const fenced = [...text.matchAll(FENCED_DIFF_BLOCK)];
26
+ if (fenced.length > 1) {
27
+ return { ok: false, code: "AMBIGUOUS_CANDIDATES", message: "more than one fenced diff/patch block was found." };
28
+ }
29
+ if (fenced.length === 1) {
30
+ const match = fenced[0];
31
+ const outside = text.slice(0, match.index) + text.slice((match.index ?? 0) + match[0].length);
32
+ if (/^diff --git /m.test(outside)) {
33
+ return { ok: false, code: "AMBIGUOUS_CANDIDATES", message: "a fenced block and a raw diff were both present." };
34
+ }
35
+ const body = match[1] ?? "";
36
+ if (body.trim() === "") {
37
+ return { ok: false, code: "EMPTY", message: "the fenced diff block is empty." };
38
+ }
39
+ return { ok: true, patch: finalize(body) };
40
+ }
41
+ const trimmed = text.trimStart();
42
+ if (looksLikeDiffStart(trimmed)) {
43
+ return { ok: true, patch: finalize(trimmed) };
44
+ }
45
+ return { ok: false, code: "NO_CANDIDATE", message: "no fenced diff/patch block and no raw unified diff was found." };
46
+ }
@@ -0,0 +1,14 @@
1
+ export * from "./taskTypes.js";
2
+ export { normalizeProjectRelativePath } from "./taskPaths.js";
3
+ export { assertAgentSuccessTask, validateAgentSuccessTask } from "./validateAgentSuccessTask.js";
4
+ export * from "./patchTypes.js";
5
+ export { extractPatchCandidate } from "./extractPatchCandidate.js";
6
+ export { parseUnifiedDiff } from "./parseUnifiedDiff.js";
7
+ export { validatePatchPolicy } from "./validatePatchPolicy.js";
8
+ export { applyPatchToSandbox } from "./applyPatchToSandbox.js";
9
+ export * from "./verificationTypes.js";
10
+ export { runVerificationCheck, runVerificationChecks } from "./runVerificationCheck.js";
11
+ export { assessBaseline } from "./assessBaseline.js";
12
+ export * from "./corpusTypes.js";
13
+ export { assertAgentSuccessCorpus, validateAgentSuccessCorpus } from "./validateAgentSuccessCorpus.js";
14
+ export { assertReadAgentSuccessCorpus, readAgentSuccessCorpus } from "./readAgentSuccessCorpus.js";
@@ -0,0 +1,210 @@
1
+ const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
2
+ class PatchMalformed extends Error {
3
+ code;
4
+ constructor(code, message) {
5
+ super(message);
6
+ this.code = code;
7
+ }
8
+ }
9
+ function stripCr(line) {
10
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
11
+ }
12
+ /** Mirrors `git apply -p1`: drop exactly one leading path component. */
13
+ function stripOneComponent(rawPath) {
14
+ const slash = rawPath.indexOf("/");
15
+ if (slash < 0) {
16
+ throw new PatchMalformed("PATCH_MALFORMED", "a patch path has no directory component to strip.");
17
+ }
18
+ return rawPath.slice(slash + 1);
19
+ }
20
+ function parseHeaderPath(headerValue) {
21
+ const value = headerValue.split("\t")[0].trimEnd();
22
+ if (value.startsWith('"')) {
23
+ throw new PatchMalformed("UNSUPPORTED_PATH_QUOTING", "quoted patch paths are not supported.");
24
+ }
25
+ if (value === "/dev/null")
26
+ return null;
27
+ return stripOneComponent(value);
28
+ }
29
+ /** `diff --git <a> <b>`: returns the shared path when both sides agree after -p1 stripping, else null. */
30
+ function parseGitHeaderPath(rest) {
31
+ if (rest.startsWith('"')) {
32
+ throw new PatchMalformed("UNSUPPORTED_PATH_QUOTING", "quoted patch paths are not supported.");
33
+ }
34
+ for (let index = rest.indexOf(" "); index >= 0; index = rest.indexOf(" ", index + 1)) {
35
+ const left = rest.slice(0, index);
36
+ const right = rest.slice(index + 1);
37
+ if (!left.includes("/") || !right.includes("/"))
38
+ continue;
39
+ if (stripOneComponent(left) === stripOneComponent(right))
40
+ return stripOneComponent(right);
41
+ }
42
+ return null;
43
+ }
44
+ function newFile(gitHeaderPath) {
45
+ return {
46
+ oldPath: null,
47
+ newPath: null,
48
+ gitHeaderPath,
49
+ isNew: false,
50
+ isDelete: false,
51
+ binary: false,
52
+ rename: false,
53
+ copy: false,
54
+ oldMode: null,
55
+ newMode: null,
56
+ subproject: false,
57
+ hunkCount: 0
58
+ };
59
+ }
60
+ /**
61
+ * Parses a Git/unified diff into per-file records with exact hunk accounting: every hunk must contain
62
+ * precisely the old/new line counts its header declares, so truncated or padded patches are rejected here
63
+ * instead of being half-understood. Only structure is recorded; policy decisions live in validatePatchPolicy.
64
+ */
65
+ export function parseUnifiedDiff(patch) {
66
+ try {
67
+ const lines = patch.split("\n");
68
+ if (lines[lines.length - 1] === "")
69
+ lines.pop();
70
+ const files = [];
71
+ let index = 0;
72
+ const parseFileHeaders = (file) => {
73
+ const oldHeader = stripCr(lines[index]);
74
+ const plusLine = lines[index + 1];
75
+ if (plusLine === undefined || !stripCr(plusLine).startsWith("+++ ")) {
76
+ throw new PatchMalformed("PATCH_MALFORMED", 'a "---" header is not followed by a "+++" header.');
77
+ }
78
+ file.oldPath = parseHeaderPath(oldHeader.slice(4));
79
+ file.newPath = parseHeaderPath(stripCr(plusLine).slice(4));
80
+ if (file.oldPath === null)
81
+ file.isNew = true;
82
+ if (file.newPath === null)
83
+ file.isDelete = true;
84
+ index += 2;
85
+ while (index < lines.length && stripCr(lines[index]).startsWith("@@")) {
86
+ parseHunk(file);
87
+ }
88
+ };
89
+ const parseHunk = (file) => {
90
+ const match = HUNK_HEADER.exec(stripCr(lines[index]));
91
+ if (!match)
92
+ throw new PatchMalformed("PATCH_MALFORMED", "malformed hunk header.");
93
+ let oldRemaining = match[2] === undefined ? 1 : Number(match[2]);
94
+ let newRemaining = match[4] === undefined ? 1 : Number(match[4]);
95
+ index += 1;
96
+ while (oldRemaining > 0 || newRemaining > 0) {
97
+ const line = lines[index];
98
+ if (line === undefined)
99
+ throw new PatchMalformed("PATCH_MALFORMED", "a hunk ends before its declared line counts.");
100
+ const marker = line.length === 0 ? " " : line[0];
101
+ if (marker === " ") {
102
+ oldRemaining -= 1;
103
+ newRemaining -= 1;
104
+ }
105
+ else if (marker === "-") {
106
+ oldRemaining -= 1;
107
+ if (line.startsWith("-Subproject commit "))
108
+ file.subproject = true;
109
+ }
110
+ else if (marker === "+") {
111
+ newRemaining -= 1;
112
+ if (line.startsWith("+Subproject commit "))
113
+ file.subproject = true;
114
+ }
115
+ else if (marker !== "\\") {
116
+ throw new PatchMalformed("PATCH_MALFORMED", "unexpected line inside a hunk.");
117
+ }
118
+ if (oldRemaining < 0 || newRemaining < 0) {
119
+ throw new PatchMalformed("PATCH_MALFORMED", "a hunk has more lines than its header declares.");
120
+ }
121
+ index += 1;
122
+ }
123
+ while (index < lines.length && lines[index].startsWith("\\"))
124
+ index += 1;
125
+ file.hunkCount += 1;
126
+ };
127
+ while (index < lines.length) {
128
+ const line = stripCr(lines[index]);
129
+ if (line.startsWith("diff --git ")) {
130
+ const file = newFile(parseGitHeaderPath(line.slice("diff --git ".length)));
131
+ index += 1;
132
+ while (index < lines.length) {
133
+ const header = stripCr(lines[index]);
134
+ if (header.startsWith("--- ") || header.startsWith("diff --git "))
135
+ break;
136
+ let match;
137
+ if ((match = /^index [0-9a-f]+\.\.[0-9a-f]+(?: (\d+))?$/.exec(header))) {
138
+ if (match[1]) {
139
+ file.oldMode = match[1];
140
+ file.newMode = match[1];
141
+ }
142
+ }
143
+ else if ((match = /^old mode (\d+)$/.exec(header))) {
144
+ file.oldMode = match[1];
145
+ }
146
+ else if ((match = /^new mode (\d+)$/.exec(header))) {
147
+ file.newMode = match[1];
148
+ }
149
+ else if ((match = /^deleted file mode (\d+)$/.exec(header))) {
150
+ file.isDelete = true;
151
+ file.oldMode = match[1];
152
+ }
153
+ else if ((match = /^new file mode (\d+)$/.exec(header))) {
154
+ file.isNew = true;
155
+ file.newMode = match[1];
156
+ }
157
+ else if (/^(?:dis)?similarity index \d+%$/.test(header)) {
158
+ // informational; rename/copy intent is carried by the from/to lines
159
+ }
160
+ else if (/^rename (?:from|to) /.test(header)) {
161
+ file.rename = true;
162
+ }
163
+ else if (/^copy (?:from|to) /.test(header)) {
164
+ file.copy = true;
165
+ }
166
+ else if (/^Binary files .* differ$/.test(header) || header === "GIT binary patch") {
167
+ file.binary = true;
168
+ index += 1;
169
+ while (index < lines.length && !lines[index].startsWith("diff --git "))
170
+ index += 1;
171
+ break;
172
+ }
173
+ else {
174
+ throw new PatchMalformed("PATCH_MALFORMED", "unrecognized extended header line.");
175
+ }
176
+ index += 1;
177
+ }
178
+ if (!file.binary && index < lines.length && stripCr(lines[index]).startsWith("--- ")) {
179
+ parseFileHeaders(file);
180
+ }
181
+ files.push(file);
182
+ }
183
+ else if (line.startsWith("--- ") && lines[index + 1] !== undefined && stripCr(lines[index + 1]).startsWith("+++ ")) {
184
+ const file = newFile(null);
185
+ parseFileHeaders(file);
186
+ files.push(file);
187
+ }
188
+ else if (line.trim() === "") {
189
+ index += 1;
190
+ }
191
+ else {
192
+ throw new PatchMalformed("PATCH_MALFORMED", "unexpected content outside a file section.");
193
+ }
194
+ }
195
+ if (files.length === 0) {
196
+ throw new PatchMalformed("PATCH_MALFORMED", "the patch contains no file sections.");
197
+ }
198
+ for (const file of files) {
199
+ if ((file.oldPath ?? file.newPath ?? file.gitHeaderPath) === null && !file.rename && !file.copy) {
200
+ throw new PatchMalformed("PATCH_MALFORMED", "a file section has no determinable path.");
201
+ }
202
+ }
203
+ return { ok: true, files };
204
+ }
205
+ catch (error) {
206
+ if (error instanceof PatchMalformed)
207
+ return { ok: false, code: error.code, message: error.message };
208
+ throw error;
209
+ }
210
+ }
@@ -0,0 +1,5 @@
1
+ export const DEFAULT_PATCH_BOUNDS = Object.freeze({
2
+ maxPatchBytes: 1_048_576,
3
+ maxChangedPaths: 64,
4
+ maxPatchLines: 20_000
5
+ });