@dailephd/my-dev-kit-lab 0.8.2 → 0.9.1
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 +19 -0
- package/README.md +19 -3
- package/benchmarks/contracts/agent-success-rate-project-profiles.json +33 -0
- package/benchmarks/contracts/agent-success-rate-tasks.json +671 -0
- package/benchmarks/projects/agent-success-inventory-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-inventory-node/README.md +74 -0
- package/benchmarks/projects/agent-success-inventory-node/package.json +14 -0
- package/benchmarks/projects/agent-success-inventory-node/src/fulfillmentReport.js +38 -0
- package/benchmarks/projects/agent-success-inventory-node/src/index.js +26 -0
- package/benchmarks/projects/agent-success-inventory-node/src/inventoryStore.js +114 -0
- package/benchmarks/projects/agent-success-inventory-node/src/quantity.js +19 -0
- package/benchmarks/projects/agent-success-inventory-node/src/reservationService.js +45 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-edge.check.mjs +51 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/quantity-primary.check.mjs +46 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/regression.check.mjs +115 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-edge.check.mjs +109 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/report-primary.check.mjs +47 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-edge.check.mjs +63 -0
- package/benchmarks/projects/agent-success-inventory-node/tests/reservation-primary.check.mjs +44 -0
- package/benchmarks/projects/agent-success-task-board-node/.gitattributes +2 -0
- package/benchmarks/projects/agent-success-task-board-node/README.md +77 -0
- package/benchmarks/projects/agent-success-task-board-node/package.json +14 -0
- package/benchmarks/projects/agent-success-task-board-node/src/index.js +24 -0
- package/benchmarks/projects/agent-success-task-board-node/src/projectSummary.js +15 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskService.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/taskStore.js +58 -0
- package/benchmarks/projects/agent-success-task-board-node/src/validation.js +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-edge.check.mjs +65 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/import-primary.check.mjs +31 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/regression.check.mjs +107 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-edge.check.mjs +67 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/summary-primary.check.mjs +54 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-edge.check.mjs +39 -0
- package/benchmarks/projects/agent-success-task-board-node/tests/title-primary.check.mjs +29 -0
- package/dist/src/agents/adapters/claudeAdapter.js +6 -3
- package/dist/src/agents/adapters/codexAdapter.js +51 -10
- package/dist/src/agents/index.js +1 -0
- package/dist/src/agents/normalizeAgentTelemetry.js +50 -0
- package/dist/src/agents/parseAgentTokenUsage.js +71 -49
- package/dist/src/agents/providerBuildObservation.js +97 -0
- package/dist/src/audits/core/projectInventory.js +1 -0
- package/dist/src/cli/help.js +44 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +83 -1
- package/dist/src/commands/runExperimentRunCommand.js +309 -6
- package/dist/src/core/processTree.js +55 -0
- package/dist/src/core/runMeasuredCommand.js +108 -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 +62 -0
- package/dist/src/evaluation/benchmarkSandbox/index.js +8 -0
- package/dist/src/evaluation/benchmarkSandbox/minimalHostEnv.js +37 -0
- package/dist/src/evaluation/benchmarkSandbox/pathPolicy.js +49 -0
- package/dist/src/evaluation/benchmarkSandbox/removeBenchmarkSandbox.js +74 -0
- package/dist/src/evaluation/benchmarkSandbox/treeSnapshot.js +57 -0
- package/dist/src/evaluation/benchmarkSandbox/types.js +3 -0
- package/dist/src/evaluation/changeSet/captureChangeSet.js +98 -0
- package/dist/src/evaluation/changeSet/index.js +2 -0
- package/dist/src/evaluation/changeSet/types.js +9 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/agentSuccessRate/agentTaskProjection.js +12 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysis.js +425 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisArtifact.js +155 -0
- package/dist/src/experiments/plugins/agentSuccessRate/analysisTypes.js +65 -0
- package/dist/src/experiments/plugins/agentSuccessRate/attemptEvidence.js +7 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpoint.js +216 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignCheckpointStore.js +588 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignEvidenceQuality.js +77 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignExecution.js +314 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignIdentity.js +111 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignProgress.js +173 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignQueue.js +11 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignRecovery.js +256 -0
- package/dist/src/experiments/plugins/agentSuccessRate/campaignResumableExecution.js +207 -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 +26 -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 +517 -0
- package/dist/src/experiments/plugins/agentSuccessRate/realAgentExecution.js +272 -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/timeoutPresets.js +9 -0
- package/dist/src/experiments/plugins/agentSuccessRate/types.js +1 -0
- package/dist/src/experiments/plugins/warmIndexReuse/plugin.js +2 -3
- package/dist/src/report/experiments/agentSuccessCampaignProgressReportModel.js +1 -0
- package/dist/src/report/experiments/agentSuccessRateReportModel.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessCampaignProgressReport.js +6 -0
- package/dist/src/report/experiments/buildAgentSuccessRateReport.js +266 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +27 -2
- package/dist/src/report/experiments/index.js +4 -0
- package/dist/src/report/experiments/renderAgentSuccessCampaignProgressHtml.js +6 -0
- package/dist/src/report/experiments/renderAgentSuccessCampaignProgressText.js +25 -0
- package/dist/src/report/experiments/renderAgentSuccessRateHtml.js +171 -0
- package/dist/src/report/experiments/renderAgentSuccessRateText.js +146 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +5 -0
- package/dist/src/report/experiments/writeAgentSuccessCampaignProgressReport.js +135 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +4 -3
- package/docs/METRICS.md +79 -0
- package/package.json +1 -1
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
import { lstat, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { countEstimatedTokens, countTextChars } from "../../../core/countTokens.js";
|
|
5
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
6
|
+
import { compareCodeUnits } from "../../../evaluation/indexSnapshot.js";
|
|
7
|
+
import { normalizeRetrievedRepositoryPath } from "../../../evaluation/retrievalQuality/buildRetrievalEvidence.js";
|
|
8
|
+
import { buildContextPack } from "../contextPackGeneration/buildContextPack.js";
|
|
9
|
+
import { deriveSymbolSourceRange, findIndexedSymbolLine, interpretSymbolIndex, parseLookup, parseSearchHits, parseSlice, parseSource } from "../contextPackGeneration/packEvidence.js";
|
|
10
|
+
import { CONTEXT_PACK_SELECTION_POLICY_ID, GRAPH_DEPTH, MAX_SOURCE_LINES_PER_SLICE, MAX_SOURCE_SLICES, SEARCH_RESULT_LIMIT, isTestFilePath, selectRelevantSymbols, selectSeedNodes } from "../contextPackGeneration/packSelectionPolicy.js";
|
|
11
|
+
export const AGENT_SUCCESS_RAW_CONTEXT_POLICY_ID = "raw-source-glob-v1";
|
|
12
|
+
export const AGENT_SUCCESS_PACK_CONTEXT_POLICY_ID = CONTEXT_PACK_SELECTION_POLICY_ID;
|
|
13
|
+
export const DEFAULT_AGENT_SUCCESS_KIT_COMMAND = "npx @dailephd/my-dev-kit@latest";
|
|
14
|
+
const MAX_RAW_FILE_BYTES = 262_144;
|
|
15
|
+
const MAX_RAW_TOTAL_BYTES = 1_048_576;
|
|
16
|
+
const FORBIDDEN_DIRECTORIES = new Set([".git", "node_modules", "dist", "build", "coverage", "lab-output"]);
|
|
17
|
+
const lf = (text) => text.replace(/\r\n?/g, "\n");
|
|
18
|
+
function normalizeRoot(root) {
|
|
19
|
+
const normalized = normalizeRetrievedRepositoryPath(root);
|
|
20
|
+
return normalized === null ? null : normalized.replace(/\/+$/, "");
|
|
21
|
+
}
|
|
22
|
+
/** Pure eligibility policy applied to every candidate before any source is read or retrieved. */
|
|
23
|
+
export function isEligibleSourcePath(candidate, input) {
|
|
24
|
+
const normalized = normalizeRetrievedRepositoryPath(candidate);
|
|
25
|
+
if (normalized === null || normalized !== candidate)
|
|
26
|
+
return false;
|
|
27
|
+
const roots = input.sourceRoots.map(normalizeRoot).filter((root) => root !== null);
|
|
28
|
+
if (!roots.some((root) => normalized === root || normalized.startsWith(`${root}/`)))
|
|
29
|
+
return false;
|
|
30
|
+
if (isTestFilePath(normalized))
|
|
31
|
+
return false;
|
|
32
|
+
if (normalized.split("/").some((segment) => FORBIDDEN_DIRECTORIES.has(segment)))
|
|
33
|
+
return false;
|
|
34
|
+
return !input.excludedPaths.includes(normalized);
|
|
35
|
+
}
|
|
36
|
+
/** `file:<path>` and `symbol:<path>#<name>` node identities; anything else yields null and is dropped. */
|
|
37
|
+
export function sourcePathOfNodeId(nodeId) {
|
|
38
|
+
const match = /^(?:file|symbol):([^#]+)/.exec(nodeId);
|
|
39
|
+
return match ? normalizeRetrievedRepositoryPath(match[1]) : null;
|
|
40
|
+
}
|
|
41
|
+
export function buildContextTaskInput(task) {
|
|
42
|
+
const excluded = new Set();
|
|
43
|
+
for (const check of [...task.taskChecks, ...task.regressionChecks]) {
|
|
44
|
+
for (const arg of check.args) {
|
|
45
|
+
const normalized = normalizeRetrievedRepositoryPath(arg);
|
|
46
|
+
if (normalized !== null && !arg.startsWith("-") && /[./]/.test(arg))
|
|
47
|
+
excluded.add(normalized);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
caseId: task.id,
|
|
52
|
+
benchmarkProject: task.benchmarkProject,
|
|
53
|
+
title: task.title,
|
|
54
|
+
query: task.query,
|
|
55
|
+
sourceRoots: [...task.sourceRoots],
|
|
56
|
+
rawIncludeGlobs: [...task.rawIncludeGlobs],
|
|
57
|
+
excludedPaths: [...excluded].sort(compareCodeUnits)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function unavailable(treatmentId, selectionPolicyId, reason, myDevKitVersion = null) {
|
|
61
|
+
return { treatmentId, availability: "unavailable", reason, selectionPolicyId, myDevKitVersion, includedSourceFiles: [], contextChars: 0, estimatedContextTokens: 0, text: null };
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
64
|
+
// raw-full-file
|
|
65
|
+
// ---------------------------------------------------------------------------------------------------------------
|
|
66
|
+
/** Directory names never entered while looking for raw source: trusted tests and generated/vendored trees. */
|
|
67
|
+
const SKIPPED_DIRECTORIES = new Set([...FORBIDDEN_DIRECTORIES, "tests", "test", "__tests__"]);
|
|
68
|
+
/**
|
|
69
|
+
* Standard glob semantics for the supported subset: `**` matches across directories (`**` followed by `/` matches zero
|
|
70
|
+
* or more directories), `*` and `?` stay within one path segment. Anything else is a literal. Returns null for a glob
|
|
71
|
+
* that is not a plain project-relative pattern.
|
|
72
|
+
*/
|
|
73
|
+
function globToRegExp(glob) {
|
|
74
|
+
const normalized = glob.replace(/\\/g, "/");
|
|
75
|
+
if (normalized.length === 0 || normalized.startsWith("/") || /^[A-Za-z]:/.test(normalized) || normalized.split("/").some((segment) => segment === ".."))
|
|
76
|
+
return null;
|
|
77
|
+
let pattern = "";
|
|
78
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
79
|
+
const char = normalized[index];
|
|
80
|
+
if (char === "*" && normalized[index + 1] === "*") {
|
|
81
|
+
if (normalized[index + 2] === "/") {
|
|
82
|
+
pattern += "(?:.*/)?";
|
|
83
|
+
index += 2;
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
pattern += ".*";
|
|
87
|
+
index += 1;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (char === "*")
|
|
91
|
+
pattern += "[^/]*";
|
|
92
|
+
else if (char === "?")
|
|
93
|
+
pattern += "[^/]";
|
|
94
|
+
else
|
|
95
|
+
pattern += char.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
96
|
+
}
|
|
97
|
+
return new RegExp(`^${pattern}$`);
|
|
98
|
+
}
|
|
99
|
+
/** Walks only the allowed source roots, so trusted-test and excluded directories are never even listed. */
|
|
100
|
+
async function listRawCandidates(projectRoot, input) {
|
|
101
|
+
const patterns = input.rawIncludeGlobs.map(globToRegExp);
|
|
102
|
+
if (patterns.some((pattern) => pattern === null))
|
|
103
|
+
return null;
|
|
104
|
+
const found = new Map();
|
|
105
|
+
const visit = async (relativeDirectory) => {
|
|
106
|
+
const entries = await readdir(resolveWithinRoot(projectRoot, relativeDirectory), { withFileTypes: true });
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (entry.name.startsWith("."))
|
|
109
|
+
continue;
|
|
110
|
+
const relativePath = `${relativeDirectory}/${entry.name}`;
|
|
111
|
+
if (entry.isDirectory()) {
|
|
112
|
+
if (!SKIPPED_DIRECTORIES.has(entry.name))
|
|
113
|
+
await visit(relativePath);
|
|
114
|
+
}
|
|
115
|
+
else if ((entry.isFile() || entry.isSymbolicLink()) && patterns.some((pattern) => pattern.test(relativePath))) {
|
|
116
|
+
found.set(relativePath, { absolutePath: resolveWithinRoot(projectRoot, relativePath), relativePath });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
for (const root of input.sourceRoots.map(normalizeRoot)) {
|
|
121
|
+
if (root === null || root === ".")
|
|
122
|
+
return null;
|
|
123
|
+
await visit(root);
|
|
124
|
+
}
|
|
125
|
+
return [...found.values()];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Every eligible source file matching the task's public glob selection, in code-unit order, LF-normalized. Reads only
|
|
129
|
+
* regular, non-link, text files below the allowed source roots; anything unreadable makes the context unavailable
|
|
130
|
+
* rather than partially fabricated.
|
|
131
|
+
*/
|
|
132
|
+
export async function buildRawFullFileContext(input, canonicalProjectRoot) {
|
|
133
|
+
const treatmentId = "raw-full-file";
|
|
134
|
+
const policy = AGENT_SUCCESS_RAW_CONTEXT_POLICY_ID;
|
|
135
|
+
let candidates;
|
|
136
|
+
try {
|
|
137
|
+
candidates = await listRawCandidates(canonicalProjectRoot, input);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
candidates = null;
|
|
141
|
+
}
|
|
142
|
+
if (candidates === null)
|
|
143
|
+
return unavailable(treatmentId, policy, "raw-glob-resolution-failed");
|
|
144
|
+
const eligible = candidates.filter((file) => isEligibleSourcePath(file.relativePath, input)).sort((a, b) => compareCodeUnits(a.relativePath, b.relativePath));
|
|
145
|
+
if (eligible.length === 0)
|
|
146
|
+
return unavailable(treatmentId, policy, "no-eligible-source-files");
|
|
147
|
+
const sections = [];
|
|
148
|
+
let totalBytes = 0;
|
|
149
|
+
for (const file of eligible) {
|
|
150
|
+
try {
|
|
151
|
+
const stats = await lstat(resolveWithinRoot(canonicalProjectRoot, file.relativePath));
|
|
152
|
+
if (stats.isSymbolicLink() || !stats.isFile())
|
|
153
|
+
return unavailable(treatmentId, policy, "source-file-not-regular");
|
|
154
|
+
if (stats.size > MAX_RAW_FILE_BYTES)
|
|
155
|
+
return unavailable(treatmentId, policy, "source-file-too-large");
|
|
156
|
+
totalBytes += stats.size;
|
|
157
|
+
if (totalBytes > MAX_RAW_TOTAL_BYTES)
|
|
158
|
+
return unavailable(treatmentId, policy, "source-context-too-large");
|
|
159
|
+
const data = await readFile(file.absolutePath);
|
|
160
|
+
if (data.includes(0))
|
|
161
|
+
return unavailable(treatmentId, policy, "source-file-not-text");
|
|
162
|
+
sections.push(`=== FILE: ${file.relativePath} ===\n${lf(data.toString("utf8"))}\n`);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return unavailable(treatmentId, policy, "source-file-unreadable");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const text = sections.join("\n");
|
|
169
|
+
return {
|
|
170
|
+
treatmentId,
|
|
171
|
+
availability: "available",
|
|
172
|
+
reason: null,
|
|
173
|
+
selectionPolicyId: policy,
|
|
174
|
+
myDevKitVersion: null,
|
|
175
|
+
includedSourceFiles: eligible.map((file) => file.relativePath),
|
|
176
|
+
contextChars: countTextChars(text),
|
|
177
|
+
estimatedContextTokens: countEstimatedTokens(text),
|
|
178
|
+
text
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const pad = (index) => String(index + 1).padStart(2, "0");
|
|
182
|
+
/**
|
|
183
|
+
* Builds the context-pack treatment. The index covers only the task's source roots of the clean canonical project, and
|
|
184
|
+
* every search hit, lookup result, graph neighbor, source candidate, symbol and call endpoint is filtered by the
|
|
185
|
+
* eligibility policy BEFORE it is selected or its source is retrieved. The pure v0.8.2 selection and rendering
|
|
186
|
+
* primitives are reused unchanged; the test-candidate input is always empty.
|
|
187
|
+
*/
|
|
188
|
+
export class AgentSuccessPackContextBuilder {
|
|
189
|
+
kitCommand;
|
|
190
|
+
dependencies;
|
|
191
|
+
workRoot = null;
|
|
192
|
+
version;
|
|
193
|
+
indexes = new Map();
|
|
194
|
+
constructor(kitCommand, dependencies) {
|
|
195
|
+
this.kitCommand = kitCommand;
|
|
196
|
+
this.dependencies = dependencies;
|
|
197
|
+
}
|
|
198
|
+
/** Removes the private work directory. Returns a bounded reason when it could not be removed. */
|
|
199
|
+
async dispose() {
|
|
200
|
+
const root = this.workRoot;
|
|
201
|
+
this.workRoot = null;
|
|
202
|
+
this.indexes.clear();
|
|
203
|
+
if (root === null)
|
|
204
|
+
return null;
|
|
205
|
+
try {
|
|
206
|
+
await rm(root, { recursive: true, force: true });
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
return "context work directory could not be removed.";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async ensureWorkRoot() {
|
|
214
|
+
if (this.workRoot === null)
|
|
215
|
+
this.workRoot = await mkdtemp(path.join(os.tmpdir(), "my-dev-kit-lab-asr-ctx-"));
|
|
216
|
+
return this.workRoot;
|
|
217
|
+
}
|
|
218
|
+
indexFor(input, canonicalProjectRoot) {
|
|
219
|
+
const key = `${canonicalProjectRoot}\0${[...input.sourceRoots].sort(compareCodeUnits).join("\0")}`;
|
|
220
|
+
let pending = this.indexes.get(key);
|
|
221
|
+
if (!pending) {
|
|
222
|
+
pending = this.buildIndex(input, canonicalProjectRoot, this.indexes.size);
|
|
223
|
+
this.indexes.set(key, pending);
|
|
224
|
+
}
|
|
225
|
+
return pending;
|
|
226
|
+
}
|
|
227
|
+
async buildIndex(input, canonicalProjectRoot, ordinal) {
|
|
228
|
+
try {
|
|
229
|
+
const root = await this.ensureWorkRoot();
|
|
230
|
+
const indexDir = path.join(root, `index-${ordinal}`, "base");
|
|
231
|
+
const commandsDir = path.join(root, `index-${ordinal}`, "commands");
|
|
232
|
+
const build = await this.dependencies.buildIndex({
|
|
233
|
+
target: { absoluteTargetRoot: canonicalProjectRoot, sourceRoots: [...input.sourceRoots] },
|
|
234
|
+
kitCommand: this.kitCommand,
|
|
235
|
+
indexDir,
|
|
236
|
+
commandsDir: path.join(commandsDir, "index"),
|
|
237
|
+
requireKit: false,
|
|
238
|
+
callGraph: true
|
|
239
|
+
});
|
|
240
|
+
if (!build.ok)
|
|
241
|
+
return { ok: false };
|
|
242
|
+
if (this.version === undefined) {
|
|
243
|
+
const tool = await this.dependencies.probeVersion({ kitCommand: this.kitCommand, commandsDir: path.join(root, "version") });
|
|
244
|
+
this.version = tool.version ?? null;
|
|
245
|
+
}
|
|
246
|
+
let symbolIndex = null;
|
|
247
|
+
try {
|
|
248
|
+
symbolIndex = interpretSymbolIndex(await this.dependencies.readSymbolIndex(indexDir));
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
symbolIndex = null;
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, indexDir, commandsDir, symbolIndex };
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return { ok: false };
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async build(input, canonicalProjectRoot) {
|
|
260
|
+
const treatmentId = "context-pack";
|
|
261
|
+
const policy = AGENT_SUCCESS_PACK_CONTEXT_POLICY_ID;
|
|
262
|
+
const project = await this.indexFor(input, canonicalProjectRoot);
|
|
263
|
+
if (!project.ok)
|
|
264
|
+
return unavailable(treatmentId, policy, "project-index-failed", this.version ?? null);
|
|
265
|
+
const version = this.version ?? null;
|
|
266
|
+
const failed = (reason) => unavailable(treatmentId, policy, reason, version);
|
|
267
|
+
const { indexDir, symbolIndex } = project;
|
|
268
|
+
const commandsDir = path.join(project.commandsDir, input.caseId.replace(/[^A-Za-z0-9._-]/g, "-"));
|
|
269
|
+
const run = (commandId, extraArgs) => this.dependencies.runCommand({ commandId, commandString: this.kitCommand, cwd: process.cwd(), outDir: commandsDir, extraArgs });
|
|
270
|
+
const eligiblePath = (candidate) => candidate !== null && isEligibleSourcePath(candidate, input);
|
|
271
|
+
const eligibleNode = (nodeId) => eligiblePath(sourcePathOfNodeId(nodeId));
|
|
272
|
+
// 1. One search; candidates outside the allowed source are removed before seeds are chosen.
|
|
273
|
+
let hits;
|
|
274
|
+
try {
|
|
275
|
+
const search = await run("search", ["search", "--index", indexDir, "--query", input.query, "--limit", String(SEARCH_RESULT_LIMIT), "--json"]);
|
|
276
|
+
const parsed = search.ok ? parseSearchHits(search.stdout, SEARCH_RESULT_LIMIT) : null;
|
|
277
|
+
if (!parsed || !parsed.ok)
|
|
278
|
+
return failed("retrieval-failed");
|
|
279
|
+
hits = parsed.value.filter((hit) => eligiblePath(hit.path) && eligibleNode(hit.nodeId));
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return failed("retrieval-failed");
|
|
283
|
+
}
|
|
284
|
+
const fileCandidates = [];
|
|
285
|
+
const symbolCandidates = [];
|
|
286
|
+
const callCandidates = [];
|
|
287
|
+
const lineByNodeId = new Map();
|
|
288
|
+
const extraNotes = [];
|
|
289
|
+
for (const hit of hits) {
|
|
290
|
+
if (hit.path !== null)
|
|
291
|
+
fileCandidates.push({ path: hit.path, origin: "search", rank: hit.rank });
|
|
292
|
+
if (hit.kind === "symbol") {
|
|
293
|
+
symbolCandidates.push({ name: hit.label ?? hit.nodeId.slice(hit.nodeId.indexOf("#") + 1), nodeId: hit.nodeId, file: hit.path, line: null, origin: "search", rank: hit.rank });
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// 2. Seeds in upstream order; lookup then depth-1 slice per seed.
|
|
297
|
+
const seeds = selectSeedNodes(hits.map((hit) => ({ nodeId: hit.nodeId, origin: "search", rank: hit.rank })));
|
|
298
|
+
extraNotes.push(...seeds.notes);
|
|
299
|
+
let graphIncomplete = false;
|
|
300
|
+
for (const [index, seed] of seeds.items.entries()) {
|
|
301
|
+
try {
|
|
302
|
+
const lookup = await run(`lookup-${pad(index)}`, ["lookup", "--index", indexDir, "--node", seed.nodeId, "--depth", String(GRAPH_DEPTH), "--json"]);
|
|
303
|
+
const parsed = lookup.ok ? parseLookup(lookup.stdout) : null;
|
|
304
|
+
if (parsed && parsed.ok) {
|
|
305
|
+
const node = parsed.value.node;
|
|
306
|
+
if (eligiblePath(node.path) && eligibleNode(node.id)) {
|
|
307
|
+
fileCandidates.push({ path: node.path, origin: "lookup", rank: seed.rank });
|
|
308
|
+
if (node.kind === "symbol" && node.symbolName !== null) {
|
|
309
|
+
symbolCandidates.push({ name: node.symbolName, nodeId: node.id, file: node.path, line: node.line, origin: "lookup", rank: seed.rank });
|
|
310
|
+
if (node.line !== null)
|
|
311
|
+
lineByNodeId.set(node.id, node.line);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else
|
|
316
|
+
graphIncomplete = true;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
graphIncomplete = true;
|
|
320
|
+
}
|
|
321
|
+
try {
|
|
322
|
+
const slice = await run(`slice-${pad(index)}`, ["slice", "--index", indexDir, "--node", seed.nodeId, "--depth", String(GRAPH_DEPTH), "--direction", "both", "--json"]);
|
|
323
|
+
const parsed = slice.ok ? parseSlice(slice.stdout) : null;
|
|
324
|
+
if (parsed && parsed.ok) {
|
|
325
|
+
for (const node of parsed.value.nodes) {
|
|
326
|
+
if (!eligiblePath(node.path) || !eligibleNode(node.id))
|
|
327
|
+
continue;
|
|
328
|
+
fileCandidates.push({ path: node.path, origin: "graph", rank: seed.rank });
|
|
329
|
+
if (node.kind === "symbol" && node.symbolName !== null) {
|
|
330
|
+
symbolCandidates.push({ name: node.symbolName, nodeId: node.id, file: node.path, line: node.line, origin: "graph", rank: seed.rank });
|
|
331
|
+
if (node.line !== null && !lineByNodeId.has(node.id))
|
|
332
|
+
lineByNodeId.set(node.id, node.line);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
for (const edge of parsed.value.edges) {
|
|
336
|
+
if (eligibleNode(edge.source) && eligibleNode(edge.target))
|
|
337
|
+
callCandidates.push({ fromNodeId: edge.source, toNodeId: edge.target, kind: edge.kind });
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else
|
|
341
|
+
graphIncomplete = true;
|
|
342
|
+
}
|
|
343
|
+
catch {
|
|
344
|
+
graphIncomplete = true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// 3. Bounded source only for already-filtered, selected symbols.
|
|
348
|
+
const selectedSymbols = selectRelevantSymbols(symbolCandidates).items;
|
|
349
|
+
const sliceCandidates = [];
|
|
350
|
+
let sourceIncomplete = false;
|
|
351
|
+
let sourceCount = 0;
|
|
352
|
+
for (const symbol of selectedSymbols) {
|
|
353
|
+
if (sourceCount >= MAX_SOURCE_SLICES)
|
|
354
|
+
break;
|
|
355
|
+
if (!eligiblePath(symbol.file))
|
|
356
|
+
continue;
|
|
357
|
+
const line = (symbol.nodeId !== null ? lineByNodeId.get(symbol.nodeId) : undefined) ?? (symbolIndex ? findIndexedSymbolLine(symbolIndex, symbol.file, symbol.name) : null);
|
|
358
|
+
const range = symbolIndex && line !== null ? deriveSymbolSourceRange(symbolIndex, symbol.file, line) : null;
|
|
359
|
+
if (!range && symbol.nodeId === null)
|
|
360
|
+
continue;
|
|
361
|
+
sourceCount += 1;
|
|
362
|
+
const nodeId = symbol.nodeId;
|
|
363
|
+
try {
|
|
364
|
+
const args = range
|
|
365
|
+
? ["source", "--index", indexDir, "--file", symbol.file, "--start", String(range.startLine), "--end", String(range.endLine), "--json"]
|
|
366
|
+
: ["source", "--index", indexDir, "--node", nodeId, "--max-lines", String(MAX_SOURCE_LINES_PER_SLICE), "--json"];
|
|
367
|
+
const source = await run(`source-${pad(sourceCount - 1)}`, args);
|
|
368
|
+
const parsed = source.ok ? parseSource(source.stdout) : null;
|
|
369
|
+
if (!parsed || !parsed.ok) {
|
|
370
|
+
sourceIncomplete = true;
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const value = parsed.value;
|
|
374
|
+
if (range?.capped)
|
|
375
|
+
extraNotes.push("source-slice-cap-reached");
|
|
376
|
+
sliceCandidates.push({
|
|
377
|
+
file: symbol.file,
|
|
378
|
+
nodeId,
|
|
379
|
+
symbolName: symbol.name,
|
|
380
|
+
startLine: value.startLine,
|
|
381
|
+
endLine: value.endLine,
|
|
382
|
+
text: value.content,
|
|
383
|
+
boundaryKnown: value.boundaryKnown,
|
|
384
|
+
truncated: range?.capped === true,
|
|
385
|
+
continuationAvailable: range ? range.capped : !value.eof,
|
|
386
|
+
rank: symbol.rank
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
sourceIncomplete = true;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
const boundaryUnknown = sliceCandidates.some((candidate) => !candidate.boundaryKnown);
|
|
394
|
+
// 4. Availability is decided here; composition is the pure v0.8.2 builder with an empty test-candidate input.
|
|
395
|
+
const available = { availability: "available", reason: null };
|
|
396
|
+
const graphSection = graphIncomplete ? { availability: "partial", reason: "graph-step-incomplete" } : available;
|
|
397
|
+
const sourceSection = sourceIncomplete
|
|
398
|
+
? { availability: "partial", reason: "source-step-incomplete" }
|
|
399
|
+
: boundaryUnknown
|
|
400
|
+
? { availability: "partial", reason: "symbol-end-unknown" }
|
|
401
|
+
: available;
|
|
402
|
+
const packReasonBase = graphIncomplete ? "graph-step-incomplete" : sourceIncomplete ? "source-step-incomplete" : boundaryUnknown ? "symbol-end-unknown" : null;
|
|
403
|
+
const packAvailabilityBase = packReasonBase === null ? "available" : "partial";
|
|
404
|
+
let pack;
|
|
405
|
+
try {
|
|
406
|
+
pack = buildContextPack({
|
|
407
|
+
caseId: input.caseId,
|
|
408
|
+
benchmarkProject: input.benchmarkProject,
|
|
409
|
+
taskLocality: null,
|
|
410
|
+
task: { title: input.title, summary: input.query },
|
|
411
|
+
myDevKitVersion: version ?? "unavailable",
|
|
412
|
+
fileCandidates,
|
|
413
|
+
symbolCandidates,
|
|
414
|
+
sourceSliceCandidates: sliceCandidates,
|
|
415
|
+
callCandidates,
|
|
416
|
+
testCandidates: [],
|
|
417
|
+
evidenceNotes: extraNotes,
|
|
418
|
+
sectionAvailability: {
|
|
419
|
+
files: graphSection,
|
|
420
|
+
symbols: graphSection,
|
|
421
|
+
sourceSlices: sourceSection,
|
|
422
|
+
callRelationships: graphSection,
|
|
423
|
+
tests: { availability: "unavailable", reason: "not-supplied" },
|
|
424
|
+
evidenceNotes: available
|
|
425
|
+
},
|
|
426
|
+
availability: packAvailabilityBase,
|
|
427
|
+
reason: packReasonBase
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
return failed("pack-construction-failed");
|
|
432
|
+
}
|
|
433
|
+
// 5. Defense in depth: nothing outside the allowed source may be present in the finished pack.
|
|
434
|
+
const violations = [
|
|
435
|
+
...pack.files.map((file) => file.path),
|
|
436
|
+
...pack.sourceSlices.map((slice) => slice.file),
|
|
437
|
+
...pack.symbols.map((symbol) => symbol.file).filter((file) => file !== null)
|
|
438
|
+
].filter((candidate) => !eligiblePath(candidate));
|
|
439
|
+
const endpointViolation = pack.callRelationships.some((call) => !eligibleNode(call.fromNodeId) || !eligibleNode(call.toNodeId));
|
|
440
|
+
if (violations.length > 0 || endpointViolation || pack.tests.length > 0)
|
|
441
|
+
return failed("context-isolation-violation");
|
|
442
|
+
if (pack.sourceSlices.length === 0)
|
|
443
|
+
return failed("no-source-slices");
|
|
444
|
+
const includedSourceFiles = [...new Set([...pack.files.map((file) => file.path), ...pack.sourceSlices.map((slice) => slice.file)])];
|
|
445
|
+
const degraded = symbolIndex === null && pack.availability === "available";
|
|
446
|
+
return {
|
|
447
|
+
treatmentId,
|
|
448
|
+
availability: degraded ? "partial" : pack.availability,
|
|
449
|
+
reason: degraded ? "symbol-index-unavailable" : pack.reason,
|
|
450
|
+
selectionPolicyId: pack.selectionPolicy.id,
|
|
451
|
+
myDevKitVersion: version,
|
|
452
|
+
includedSourceFiles,
|
|
453
|
+
contextChars: pack.size.totalChars,
|
|
454
|
+
estimatedContextTokens: pack.size.totalEstimatedTokens,
|
|
455
|
+
text: pack.renderedText
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Distinctive hidden values (reference patch, trusted-test paths, behavior-fact text and hyphenated identifiers) that
|
|
461
|
+
* must never appear in a prompt. Read from the task BEFORE it is sealed; used only to refuse a leaking prompt.
|
|
462
|
+
*/
|
|
463
|
+
export function collectForbiddenAgentValues(task) {
|
|
464
|
+
const values = [];
|
|
465
|
+
const add = (label, value) => {
|
|
466
|
+
if (typeof value === "string" && value.trim().length >= 6)
|
|
467
|
+
values.push({ label, value });
|
|
468
|
+
};
|
|
469
|
+
const fixture = task.deterministicFixture;
|
|
470
|
+
if (fixture) {
|
|
471
|
+
add("deterministicFixture.patch", fixture.patch);
|
|
472
|
+
add("deterministicFixture.id", fixture.id);
|
|
473
|
+
add("deterministicFixture.notes", fixture.notes);
|
|
474
|
+
}
|
|
475
|
+
for (const check of [...task.taskChecks, ...task.regressionChecks]) {
|
|
476
|
+
add("verificationCheck.id", /-/.test(check.id) ? check.id : undefined);
|
|
477
|
+
for (const arg of check.args)
|
|
478
|
+
if (!arg.startsWith("-") && /[./]/.test(arg))
|
|
479
|
+
add("verificationCheck.path", arg);
|
|
480
|
+
}
|
|
481
|
+
for (const fact of task.behaviorFacts) {
|
|
482
|
+
add("behaviorFact.text", fact.text);
|
|
483
|
+
add("behaviorFact.id", /-/.test(fact.id) ? fact.id : undefined);
|
|
484
|
+
}
|
|
485
|
+
for (const file of task.protectedFiles)
|
|
486
|
+
if (isTestFilePath(file))
|
|
487
|
+
add("protectedFile.test", file);
|
|
488
|
+
return values;
|
|
489
|
+
}
|
|
490
|
+
/** Returns the labels (never the values) of forbidden content found in a finished prompt. */
|
|
491
|
+
export function findPromptLeaks(promptText, forbidden) {
|
|
492
|
+
const found = new Set();
|
|
493
|
+
for (const entry of forbidden)
|
|
494
|
+
if (promptText.includes(entry.value))
|
|
495
|
+
found.add(entry.label);
|
|
496
|
+
return [...found].sort(compareCodeUnits);
|
|
497
|
+
}
|