@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.
Files changed (127) hide show
  1. package/CHANGELOG.md +20 -1
  2. package/README.md +20 -4
  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 +54 -2
  36. package/dist/src/commands/runExperimentDescribeCommand.js +60 -2
  37. package/dist/src/commands/runExperimentRunCommand.js +208 -5
  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/evaluation/runMyDevKitRetrieval.js +1 -0
  68. package/dist/src/experiments/defaultRegistry.js +4 -0
  69. package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
  70. package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
  71. package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
  72. package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
  73. package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
  74. package/dist/src/experiments/plugins/agentSuccessRate/config.js +123 -0
  75. package/dist/src/experiments/plugins/agentSuccessRate/contextGeneration.js +497 -0
  76. package/dist/src/experiments/plugins/agentSuccessRate/execution.js +441 -0
  77. package/dist/src/experiments/plugins/agentSuccessRate/executionArtifact.js +126 -0
  78. package/dist/src/experiments/plugins/agentSuccessRate/executionTypes.js +2 -0
  79. package/dist/src/experiments/plugins/agentSuccessRate/index.js +20 -0
  80. package/dist/src/experiments/plugins/agentSuccessRate/metadata.js +28 -0
  81. package/dist/src/experiments/plugins/agentSuccessRate/metrics.js +76 -0
  82. package/dist/src/experiments/plugins/agentSuccessRate/plugin.js +507 -0
  83. package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +250 -0
  84. package/dist/src/experiments/plugins/agentSuccessRate/realAgentPrompt.js +43 -0
  85. package/dist/src/experiments/plugins/agentSuccessRate/repairAnalysis.js +148 -0
  86. package/dist/src/experiments/plugins/agentSuccessRate/repairExecution.js +103 -0
  87. package/dist/src/experiments/plugins/agentSuccessRate/repairFeedback.js +91 -0
  88. package/dist/src/experiments/plugins/agentSuccessRate/repairPolicy.js +88 -0
  89. package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
  90. package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
  91. package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
  92. package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
  93. package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
  94. package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
  95. package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
  96. package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
  97. package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
  98. package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
  99. package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
  100. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
  101. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
  102. package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
  103. package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
  104. package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
  105. package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
  106. package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
  107. package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
  108. package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
  109. package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
  110. package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
  111. package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
  112. package/dist/src/report/experiments/buildAgentSuccessRateReport.js +255 -0
  113. package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
  114. package/dist/src/report/experiments/buildPluginExperimentReport.js +49 -2
  115. package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
  116. package/dist/src/report/experiments/index.js +9 -0
  117. package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
  118. package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +169 -0
  119. package/dist/src/report/experiments/renderAgentSuccessRateText.js +139 -0
  120. package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
  121. package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
  122. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
  123. package/dist/src/report/experiments/renderPluginExperimentReportText.js +12 -0
  124. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
  125. package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
  126. package/docs/METRICS.md +98 -1
  127. package/package.json +1 -1
@@ -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
+ });
@@ -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
+ }