@dailephd/my-dev-kit-lab 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -1
- package/README.md +5 -4
- package/dist/src/cli/help.js +16 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +8 -1
- package/dist/src/commands/runExperimentRunCommand.js +90 -3
- package/dist/src/evaluation/runMyDevKitRetrieval.js +1 -0
- package/dist/src/experiments/defaultRegistry.js +2 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
- package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
- package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
- package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
- package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
- package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
- package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
- package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
- package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
- package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
- package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
- package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
- package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
- package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +24 -2
- package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
- package/dist/src/report/experiments/index.js +5 -0
- package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
- package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
- package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +7 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +15 -15
- package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
- package/docs/METRICS.md +28 -1
- package/package.json +1 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { runMeasuredCommand } from "../../../core/runMeasuredCommand.js";
|
|
5
|
+
import { validateRetrievalPrecisionRecallCase } from "../../../evaluation/retrievalQuality/index.js";
|
|
6
|
+
import { buildMyDevKitIndex, probeMyDevKitVersion } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
7
|
+
import { runRawFullFileBaseline } from "../../../evaluation/runRawFullFileBaseline.js";
|
|
8
|
+
import { groupWarmIndexCases, taskOutputSegment } from "../warmIndexReuse/selection.js";
|
|
9
|
+
import { buildContextPack } from "./buildContextPack.js";
|
|
10
|
+
import { CONTEXT_PACK_GENERATION_ERROR_TEXT } from "./executionTypes.js";
|
|
11
|
+
import { buildContextPackIdentityEvidence, buildRawFullFileIdentityEvidence } from "./identityEvidence.js";
|
|
12
|
+
import { CONTEXT_PACK_GENERATION_TREATMENT_IDS } from "./metadata.js";
|
|
13
|
+
import { contextPackArtifactRelativePath } from "./packArtifact.js";
|
|
14
|
+
import { deriveSymbolSourceRange, findIndexedSymbolLine, interpretSymbolIndex, parseLookup, parseSearchHits, parseSlice, parseSource } from "./packEvidence.js";
|
|
15
|
+
import { GRAPH_DEPTH, MAX_SOURCE_LINES_PER_SLICE, MAX_SOURCE_SLICES, SEARCH_RESULT_LIMIT, selectRelevantSymbols, selectSeedNodes } from "./packSelectionPolicy.js";
|
|
16
|
+
const defaultDependencies = {
|
|
17
|
+
buildIndex: buildMyDevKitIndex,
|
|
18
|
+
runCommand: runMeasuredCommand,
|
|
19
|
+
runRawBaseline: runRawFullFileBaseline,
|
|
20
|
+
readSymbolIndex: async (indexDir) => JSON.parse(await readFile(path.join(indexDir, "symbol-index.json"), "utf8")),
|
|
21
|
+
probeVersion: probeMyDevKitVersion
|
|
22
|
+
};
|
|
23
|
+
const pad = (index) => String(index + 1).padStart(2, "0");
|
|
24
|
+
function failedTreatment(treatmentId, code, steps = []) {
|
|
25
|
+
return {
|
|
26
|
+
treatmentId,
|
|
27
|
+
status: "failed",
|
|
28
|
+
availability: null,
|
|
29
|
+
availabilityReason: null,
|
|
30
|
+
size: null,
|
|
31
|
+
identityEvidence: null,
|
|
32
|
+
includedFiles: [],
|
|
33
|
+
steps,
|
|
34
|
+
sections: null,
|
|
35
|
+
evidenceNotes: [],
|
|
36
|
+
packArtifactPath: null,
|
|
37
|
+
errors: [{ code, message: CONTEXT_PACK_GENERATION_ERROR_TEXT[code] }]
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function failedCase(evaluationCase, code) {
|
|
41
|
+
return {
|
|
42
|
+
evidence: {
|
|
43
|
+
caseId: evaluationCase.id,
|
|
44
|
+
caseName: evaluationCase.title,
|
|
45
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
46
|
+
taskLocality: evaluationCase.taskLocality ?? null,
|
|
47
|
+
treatments: CONTEXT_PACK_GENERATION_TREATMENT_IDS.map((treatmentId) => failedTreatment(treatmentId, code))
|
|
48
|
+
},
|
|
49
|
+
pack: null
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const statusOf = (availability) => (availability === "available" ? "completed" : availability === "partial" ? "partial" : "failed");
|
|
53
|
+
async function executeRawTreatment(context) {
|
|
54
|
+
try {
|
|
55
|
+
const baseline = await context.dependencies.runRawBaseline(context.evaluationCase);
|
|
56
|
+
const symbolIndex = context.symbolIndex;
|
|
57
|
+
const availability = symbolIndex ? "available" : "partial";
|
|
58
|
+
const reason = symbolIndex ? null : "symbol-index-unavailable";
|
|
59
|
+
return {
|
|
60
|
+
treatmentId: "raw-full-file",
|
|
61
|
+
status: statusOf(availability),
|
|
62
|
+
availability,
|
|
63
|
+
availabilityReason: reason,
|
|
64
|
+
size: { totalChars: baseline.totalChars, totalEstimatedTokens: baseline.totalEstimatedTokens, tokenCountMethod: baseline.tokenCountMethod },
|
|
65
|
+
identityEvidence: buildRawFullFileIdentityEvidence({
|
|
66
|
+
includedFiles: baseline.filesIncluded,
|
|
67
|
+
indexedFiles: symbolIndex?.files ?? [],
|
|
68
|
+
availability,
|
|
69
|
+
reason
|
|
70
|
+
}),
|
|
71
|
+
includedFiles: [...baseline.filesIncluded],
|
|
72
|
+
steps: [],
|
|
73
|
+
sections: null,
|
|
74
|
+
evidenceNotes: [],
|
|
75
|
+
packArtifactPath: null,
|
|
76
|
+
errors: []
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return failedTreatment("raw-full-file", "raw-baseline-failed");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
async function executePackTreatment(context) {
|
|
84
|
+
const { evaluationCase, indexDir, symbolIndex } = context;
|
|
85
|
+
const steps = [];
|
|
86
|
+
const run = (commandId, extraArgs) => context.dependencies.runCommand({ commandId, commandString: context.kitCommand, cwd: process.cwd(), outDir: context.commandsDir, extraArgs });
|
|
87
|
+
const fail = (code) => ({ treatment: failedTreatment("context-pack", code, steps), pack: null });
|
|
88
|
+
// 1. One search, capped, upstream rank preserved.
|
|
89
|
+
let hits;
|
|
90
|
+
try {
|
|
91
|
+
const search = await run("search", ["search", "--index", indexDir, "--query", evaluationCase.query, "--limit", String(SEARCH_RESULT_LIMIT), "--json"]);
|
|
92
|
+
const parsed = search.ok ? parseSearchHits(search.stdout, SEARCH_RESULT_LIMIT) : null;
|
|
93
|
+
if (!search.ok || !parsed || !parsed.ok) {
|
|
94
|
+
steps.push({ kind: "search", nodeId: null, sourceMode: null, succeeded: false, reason: search.ok ? "malformed-output" : "command-failed" });
|
|
95
|
+
return fail("retrieval-failed");
|
|
96
|
+
}
|
|
97
|
+
steps.push({ kind: "search", nodeId: null, sourceMode: null, succeeded: true, reason: null });
|
|
98
|
+
hits = parsed.value;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
steps.push({ kind: "search", nodeId: null, sourceMode: null, succeeded: false, reason: "command-failed" });
|
|
102
|
+
return fail("retrieval-failed");
|
|
103
|
+
}
|
|
104
|
+
const fileCandidates = [];
|
|
105
|
+
const symbolCandidates = [];
|
|
106
|
+
const testCandidates = [];
|
|
107
|
+
const callCandidates = [];
|
|
108
|
+
const lineByNodeId = new Map();
|
|
109
|
+
const extraNotes = [];
|
|
110
|
+
for (const hit of hits) {
|
|
111
|
+
if (hit.path !== null) {
|
|
112
|
+
fileCandidates.push({ path: hit.path, origin: "search", rank: hit.rank });
|
|
113
|
+
testCandidates.push({ path: hit.path, origin: "search-candidate", rank: hit.rank });
|
|
114
|
+
}
|
|
115
|
+
if (hit.kind === "symbol") {
|
|
116
|
+
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 });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// 2. Seeds in upstream order; lookup then depth-1 slice per seed.
|
|
120
|
+
const seeds = selectSeedNodes(hits.map((hit) => ({ nodeId: hit.nodeId, origin: "search", rank: hit.rank })));
|
|
121
|
+
extraNotes.push(...seeds.notes);
|
|
122
|
+
let graphIncomplete = false;
|
|
123
|
+
for (const [index, seed] of seeds.items.entries()) {
|
|
124
|
+
try {
|
|
125
|
+
const lookup = await run(`lookup-${pad(index)}`, ["lookup", "--index", indexDir, "--node", seed.nodeId, "--depth", String(GRAPH_DEPTH), "--json"]);
|
|
126
|
+
const parsed = lookup.ok ? parseLookup(lookup.stdout) : null;
|
|
127
|
+
if (parsed && parsed.ok) {
|
|
128
|
+
steps.push({ kind: "lookup", nodeId: seed.nodeId, sourceMode: null, succeeded: true, reason: null });
|
|
129
|
+
const node = parsed.value.node;
|
|
130
|
+
if (node.path !== null)
|
|
131
|
+
fileCandidates.push({ path: node.path, origin: "lookup", rank: seed.rank });
|
|
132
|
+
if (node.kind === "symbol" && node.symbolName !== null) {
|
|
133
|
+
symbolCandidates.push({ name: node.symbolName, nodeId: node.id, file: node.path, line: node.line, origin: "lookup", rank: seed.rank });
|
|
134
|
+
if (node.line !== null)
|
|
135
|
+
lineByNodeId.set(node.id, node.line);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
graphIncomplete = true;
|
|
140
|
+
steps.push({ kind: "lookup", nodeId: seed.nodeId, sourceMode: null, succeeded: false, reason: lookup.ok ? "malformed-output" : "command-failed" });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
graphIncomplete = true;
|
|
145
|
+
steps.push({ kind: "lookup", nodeId: seed.nodeId, sourceMode: null, succeeded: false, reason: "command-failed" });
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
const slice = await run(`slice-${pad(index)}`, ["slice", "--index", indexDir, "--node", seed.nodeId, "--depth", String(GRAPH_DEPTH), "--direction", "both", "--json"]);
|
|
149
|
+
const parsed = slice.ok ? parseSlice(slice.stdout) : null;
|
|
150
|
+
if (parsed && parsed.ok) {
|
|
151
|
+
steps.push({ kind: "slice", nodeId: seed.nodeId, sourceMode: null, succeeded: true, reason: null });
|
|
152
|
+
for (const node of parsed.value.nodes) {
|
|
153
|
+
if (node.path !== null) {
|
|
154
|
+
fileCandidates.push({ path: node.path, origin: "graph", rank: seed.rank });
|
|
155
|
+
testCandidates.push({ path: node.path, origin: "graph-neighbor", rank: seed.rank });
|
|
156
|
+
}
|
|
157
|
+
if (node.kind === "symbol" && node.symbolName !== null) {
|
|
158
|
+
symbolCandidates.push({ name: node.symbolName, nodeId: node.id, file: node.path, line: node.line, origin: "graph", rank: seed.rank });
|
|
159
|
+
if (node.line !== null && !lineByNodeId.has(node.id))
|
|
160
|
+
lineByNodeId.set(node.id, node.line);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
for (const edge of parsed.value.edges)
|
|
164
|
+
callCandidates.push({ fromNodeId: edge.source, toNodeId: edge.target, kind: edge.kind });
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
graphIncomplete = true;
|
|
168
|
+
steps.push({ kind: "slice", nodeId: seed.nodeId, sourceMode: null, succeeded: false, reason: slice.ok ? "malformed-output" : "command-failed" });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
graphIncomplete = true;
|
|
173
|
+
steps.push({ kind: "slice", nodeId: seed.nodeId, sourceMode: null, succeeded: false, reason: "command-failed" });
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// 3. Bounded source for the selected symbols, in selection order, at most MAX_SOURCE_SLICES commands.
|
|
177
|
+
const selectedSymbols = selectRelevantSymbols(symbolCandidates).items;
|
|
178
|
+
const sliceCandidates = [];
|
|
179
|
+
let sourceIncomplete = false;
|
|
180
|
+
let sourceCount = 0;
|
|
181
|
+
for (const symbol of selectedSymbols) {
|
|
182
|
+
if (sourceCount >= MAX_SOURCE_SLICES)
|
|
183
|
+
break;
|
|
184
|
+
if (symbol.file === null)
|
|
185
|
+
continue;
|
|
186
|
+
const line = (symbol.nodeId !== null ? lineByNodeId.get(symbol.nodeId) : undefined) ?? (symbolIndex ? findIndexedSymbolLine(symbolIndex, symbol.file, symbol.name) : null);
|
|
187
|
+
const range = symbolIndex && line !== null ? deriveSymbolSourceRange(symbolIndex, symbol.file, line) : null;
|
|
188
|
+
if (!range && symbol.nodeId === null)
|
|
189
|
+
continue;
|
|
190
|
+
sourceCount += 1;
|
|
191
|
+
const sourceMode = range ? "index-range" : "node";
|
|
192
|
+
const nodeId = symbol.nodeId;
|
|
193
|
+
try {
|
|
194
|
+
const args = range
|
|
195
|
+
? ["source", "--index", indexDir, "--file", symbol.file, "--start", String(range.startLine), "--end", String(range.endLine), "--json"]
|
|
196
|
+
: ["source", "--index", indexDir, "--node", nodeId, "--max-lines", String(MAX_SOURCE_LINES_PER_SLICE), "--json"];
|
|
197
|
+
const source = await run(`source-${pad(sourceCount - 1)}`, args);
|
|
198
|
+
const parsed = source.ok ? parseSource(source.stdout) : null;
|
|
199
|
+
if (!parsed || !parsed.ok) {
|
|
200
|
+
sourceIncomplete = true;
|
|
201
|
+
steps.push({ kind: "source", nodeId, sourceMode, succeeded: false, reason: source.ok ? "malformed-output" : "command-failed" });
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const value = parsed.value;
|
|
205
|
+
steps.push({ kind: "source", nodeId, sourceMode, succeeded: true, reason: value.boundaryKnown ? null : "symbol-end-unknown" });
|
|
206
|
+
if (range?.capped)
|
|
207
|
+
extraNotes.push("source-slice-cap-reached");
|
|
208
|
+
sliceCandidates.push({
|
|
209
|
+
file: symbol.file,
|
|
210
|
+
nodeId,
|
|
211
|
+
symbolName: symbol.name,
|
|
212
|
+
startLine: value.startLine,
|
|
213
|
+
endLine: value.endLine,
|
|
214
|
+
text: value.content,
|
|
215
|
+
boundaryKnown: value.boundaryKnown,
|
|
216
|
+
truncated: range?.capped === true,
|
|
217
|
+
continuationAvailable: range ? range.capped : !value.eof,
|
|
218
|
+
rank: symbol.rank
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
sourceIncomplete = true;
|
|
223
|
+
steps.push({ kind: "source", nodeId, sourceMode, succeeded: false, reason: "command-failed" });
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const boundaryUnknown = sliceCandidates.some((candidate) => !candidate.boundaryKnown);
|
|
227
|
+
// 4. Deterministic section availability decided here (execution), then pure composition.
|
|
228
|
+
const available = { availability: "available", reason: null };
|
|
229
|
+
const graphSection = graphIncomplete ? { availability: "partial", reason: "graph-step-incomplete" } : available;
|
|
230
|
+
const sourceSection = sourceIncomplete
|
|
231
|
+
? { availability: "partial", reason: "source-step-incomplete" }
|
|
232
|
+
: boundaryUnknown
|
|
233
|
+
? { availability: "partial", reason: "symbol-end-unknown" }
|
|
234
|
+
: available;
|
|
235
|
+
const sectionAvailability = { files: graphSection, symbols: graphSection, sourceSlices: sourceSection, callRelationships: graphSection, tests: graphSection, evidenceNotes: available };
|
|
236
|
+
const packAvailability = graphIncomplete || sourceIncomplete || boundaryUnknown ? "partial" : "available";
|
|
237
|
+
const packReason = graphIncomplete ? "graph-step-incomplete" : sourceIncomplete ? "source-step-incomplete" : boundaryUnknown ? "symbol-end-unknown" : null;
|
|
238
|
+
let pack;
|
|
239
|
+
try {
|
|
240
|
+
pack = buildContextPack({
|
|
241
|
+
caseId: evaluationCase.id,
|
|
242
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
243
|
+
taskLocality: evaluationCase.taskLocality ?? null,
|
|
244
|
+
task: { title: evaluationCase.title, summary: evaluationCase.query },
|
|
245
|
+
myDevKitVersion: context.myDevKitVersion,
|
|
246
|
+
fileCandidates,
|
|
247
|
+
symbolCandidates,
|
|
248
|
+
sourceSliceCandidates: sliceCandidates,
|
|
249
|
+
callCandidates,
|
|
250
|
+
testCandidates,
|
|
251
|
+
evidenceNotes: extraNotes,
|
|
252
|
+
sectionAvailability,
|
|
253
|
+
availability: packAvailability,
|
|
254
|
+
reason: packReason
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return fail("pack-construction-failed");
|
|
259
|
+
}
|
|
260
|
+
// Slice-contained symbols need the symbol index; without it the identity evidence is explicitly partial, never guessed.
|
|
261
|
+
const identitySource = symbolIndex || pack.availability !== "available" ? pack : { ...pack, availability: "partial", reason: "symbol-index-unavailable" };
|
|
262
|
+
return {
|
|
263
|
+
pack,
|
|
264
|
+
treatment: {
|
|
265
|
+
treatmentId: "context-pack",
|
|
266
|
+
status: statusOf(identitySource.availability),
|
|
267
|
+
availability: identitySource.availability,
|
|
268
|
+
availabilityReason: identitySource.reason,
|
|
269
|
+
size: pack.size,
|
|
270
|
+
identityEvidence: buildContextPackIdentityEvidence(identitySource, symbolIndex?.files ?? []),
|
|
271
|
+
includedFiles: pack.files.map((file) => file.path),
|
|
272
|
+
steps,
|
|
273
|
+
sections: pack.sections.map((section) => ({ ...section })),
|
|
274
|
+
evidenceNotes: [...pack.evidenceNotes],
|
|
275
|
+
packArtifactPath: contextPackArtifactRelativePath(evaluationCase.id),
|
|
276
|
+
errors: []
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Matched two-treatment execution (raw-full-file, context-pack). One base index per benchmark project, shared by both
|
|
282
|
+
* treatments of every case in it; cases run strictly sequentially in corpus order. Builds no scientific analysis and
|
|
283
|
+
* writes nothing durable: it returns bounded evidence plus the in-memory packs.
|
|
284
|
+
*/
|
|
285
|
+
export async function executeContextPackGeneration(options) {
|
|
286
|
+
const dependencies = { ...defaultDependencies, ...options.dependencies };
|
|
287
|
+
const results = new Map();
|
|
288
|
+
const validCases = [];
|
|
289
|
+
for (const evaluationCase of options.cases) {
|
|
290
|
+
if (validateRetrievalPrecisionRecallCase(evaluationCase).length > 0)
|
|
291
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "ground-truth-invalid"));
|
|
292
|
+
else
|
|
293
|
+
validCases.push(evaluationCase);
|
|
294
|
+
}
|
|
295
|
+
let myDevKitVersion = null;
|
|
296
|
+
for (const group of groupWarmIndexCases(validCases)) {
|
|
297
|
+
if (group.structuralErrors.length > 0) {
|
|
298
|
+
for (const evaluationCase of group.cases)
|
|
299
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "project-group-inconsistent"));
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
const baseIndexDir = resolveWithinRoot(options.outputRoot, path.join("indexes", group.projectSegment, "base"));
|
|
303
|
+
const projectCommandsDir = resolveWithinRoot(options.outputRoot, path.join("commands", group.projectSegment));
|
|
304
|
+
let indexReady = false;
|
|
305
|
+
try {
|
|
306
|
+
const build = await dependencies.buildIndex({
|
|
307
|
+
target: { absoluteTargetRoot: group.targetRoot, sourceRoots: [...group.sourceRoots] },
|
|
308
|
+
kitCommand: options.kitCommand,
|
|
309
|
+
indexDir: baseIndexDir,
|
|
310
|
+
commandsDir: path.join(projectCommandsDir, "index"),
|
|
311
|
+
requireKit: false,
|
|
312
|
+
// Context packs carry call relationships, which upstream emits only for call-graph indexes.
|
|
313
|
+
callGraph: true
|
|
314
|
+
});
|
|
315
|
+
indexReady = build.ok;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
indexReady = false;
|
|
319
|
+
}
|
|
320
|
+
if (!indexReady) {
|
|
321
|
+
for (const evaluationCase of group.cases)
|
|
322
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "project-index-failed"));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
if (myDevKitVersion === null) {
|
|
326
|
+
const tool = await dependencies.probeVersion({ kitCommand: options.kitCommand, commandsDir: resolveWithinRoot(options.outputRoot, path.join("commands", "version")) });
|
|
327
|
+
myDevKitVersion = tool.version ?? "unavailable";
|
|
328
|
+
}
|
|
329
|
+
let symbolIndex = null;
|
|
330
|
+
try {
|
|
331
|
+
symbolIndex = interpretSymbolIndex(await dependencies.readSymbolIndex(baseIndexDir));
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
symbolIndex = null;
|
|
335
|
+
}
|
|
336
|
+
for (const evaluationCase of group.cases) {
|
|
337
|
+
const context = {
|
|
338
|
+
evaluationCase,
|
|
339
|
+
kitCommand: options.kitCommand,
|
|
340
|
+
indexDir: baseIndexDir,
|
|
341
|
+
commandsDir: resolveWithinRoot(projectCommandsDir, taskOutputSegment(evaluationCase.id)),
|
|
342
|
+
symbolIndex,
|
|
343
|
+
myDevKitVersion,
|
|
344
|
+
dependencies
|
|
345
|
+
};
|
|
346
|
+
// Strictly sequential, fixed treatment order.
|
|
347
|
+
const raw = await executeRawTreatment(context);
|
|
348
|
+
const pack = await executePackTreatment(context);
|
|
349
|
+
const evidence = {
|
|
350
|
+
caseId: evaluationCase.id,
|
|
351
|
+
caseName: evaluationCase.title,
|
|
352
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
353
|
+
taskLocality: evaluationCase.taskLocality ?? null,
|
|
354
|
+
treatments: [raw, pack.treatment]
|
|
355
|
+
};
|
|
356
|
+
results.set(evaluationCase.id, { evidence, pack: pack.pack });
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
// Selection order is the corpus order after filtering, regardless of how projects were grouped.
|
|
360
|
+
return options.cases.map((evaluationCase) => results.get(evaluationCase.id));
|
|
361
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { CONTEXT_PACK_GENERATION_TREATMENT_IDS } from "./metadata.js";
|
|
2
|
+
import { CONTEXT_PACK_SELECTION_POLICY_ID } from "./packSelectionPolicy.js";
|
|
3
|
+
export const CONTEXT_PACK_GENERATION_EXECUTION_ARTIFACT_FILE = "context-pack-generation-execution.json";
|
|
4
|
+
export const CONTEXT_PACK_GENERATION_EXECUTION_SCHEMA_VERSION = "my-dev-kit-lab-context-pack-generation-execution-v1";
|
|
5
|
+
export function buildContextPackGenerationExecutionArtifact(args) {
|
|
6
|
+
return {
|
|
7
|
+
schemaVersion: CONTEXT_PACK_GENERATION_EXECUTION_SCHEMA_VERSION,
|
|
8
|
+
runId: args.runId,
|
|
9
|
+
pluginId: args.pluginId,
|
|
10
|
+
pluginSchemaVersion: args.pluginSchemaVersion,
|
|
11
|
+
startedAt: args.startedAt,
|
|
12
|
+
completedAt: args.completedAt,
|
|
13
|
+
treatmentOrder: [...CONTEXT_PACK_GENERATION_TREATMENT_IDS],
|
|
14
|
+
selectionPolicyId: CONTEXT_PACK_SELECTION_POLICY_ID,
|
|
15
|
+
cases: structuredClone(args.cases)
|
|
16
|
+
};
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Plugin-owned text per code; caught error messages and process stderr are never copied into durable evidence. */
|
|
2
|
+
export const CONTEXT_PACK_GENERATION_ERROR_TEXT = {
|
|
3
|
+
"ground-truth-invalid": "The case ground truth was invalid.",
|
|
4
|
+
"project-group-inconsistent": "The case source configuration was inconsistent.",
|
|
5
|
+
"project-index-failed": "The my-dev-kit index could not be prepared.",
|
|
6
|
+
"raw-baseline-failed": "The raw full-file baseline could not be built.",
|
|
7
|
+
"retrieval-failed": "The context-pack retrieval did not complete.",
|
|
8
|
+
"pack-construction-failed": "The context pack could not be constructed."
|
|
9
|
+
};
|
|
10
|
+
/** Fixed schema of the external-local identity-redaction marker. */
|
|
11
|
+
export const CONTEXT_PACK_IDENTITY_REDACTION_SCHEMA_VERSION = "my-dev-kit-lab-context-pack-identity-redaction-v1";
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { compareCodeUnits } from "../../../evaluation/indexSnapshot.js";
|
|
2
|
+
import { normalizeRetrievedRepositoryPath } from "../../../evaluation/retrievalQuality/buildRetrievalEvidence.js";
|
|
3
|
+
function canonicalSymbols(identities) {
|
|
4
|
+
const unique = new Map();
|
|
5
|
+
for (const identity of identities)
|
|
6
|
+
unique.set(`${identity.file ?? ""}\0${identity.name}`, identity);
|
|
7
|
+
return [...unique.values()].sort((a, b) => compareCodeUnits(a.file ?? "", b.file ?? "") || compareCodeUnits(a.name, b.name));
|
|
8
|
+
}
|
|
9
|
+
/** Raw-full-file rule: every indexed symbol defined in a file the raw baseline actually included. */
|
|
10
|
+
export function collectRawFullFileSymbols(indexedFiles, includedFiles) {
|
|
11
|
+
const included = new Set();
|
|
12
|
+
for (const file of includedFiles) {
|
|
13
|
+
const normalized = normalizeRetrievedRepositoryPath(file);
|
|
14
|
+
if (normalized !== null)
|
|
15
|
+
included.add(normalized);
|
|
16
|
+
}
|
|
17
|
+
const found = [];
|
|
18
|
+
for (const entry of indexedFiles) {
|
|
19
|
+
const path = normalizeRetrievedRepositoryPath(entry.path);
|
|
20
|
+
if (path === null || !included.has(path))
|
|
21
|
+
continue;
|
|
22
|
+
for (const symbol of entry.symbols)
|
|
23
|
+
found.push({ name: symbol.name, file: path });
|
|
24
|
+
}
|
|
25
|
+
return canonicalSymbols(found);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Context-pack rule: the union of the explicitly selected symbols and the indexed symbols whose definition line falls
|
|
29
|
+
* inside a selected (post-cap) source slice range of the same file.
|
|
30
|
+
*/
|
|
31
|
+
export function collectContextPackSymbols(pack, indexedFiles) {
|
|
32
|
+
const found = pack.symbols.map((symbol) => (symbol.file === null ? { name: symbol.name } : { name: symbol.name, file: symbol.file }));
|
|
33
|
+
for (const entry of indexedFiles) {
|
|
34
|
+
const path = normalizeRetrievedRepositoryPath(entry.path);
|
|
35
|
+
if (path === null)
|
|
36
|
+
continue;
|
|
37
|
+
const slices = pack.sourceSlices.filter((slice) => slice.file === path);
|
|
38
|
+
if (slices.length === 0)
|
|
39
|
+
continue;
|
|
40
|
+
for (const symbol of entry.symbols) {
|
|
41
|
+
if (slices.some((slice) => symbol.location.line >= slice.startLine && symbol.location.line <= slice.endLine)) {
|
|
42
|
+
found.push({ name: symbol.name, file: path });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return canonicalSymbols(found);
|
|
47
|
+
}
|
|
48
|
+
const withReason = (input) => input.reason === null ? { availability: input.availability } : { availability: input.availability, availabilityReason: input.reason };
|
|
49
|
+
/** Context-pack identity evidence for the existing retrieval-quality calculator. Tests stay descriptive and are excluded. */
|
|
50
|
+
export function buildContextPackIdentityEvidence(pack, indexedFiles) {
|
|
51
|
+
const files = new Set([...pack.files.map((file) => file.path), ...pack.sourceSlices.map((slice) => slice.file)]);
|
|
52
|
+
return {
|
|
53
|
+
...withReason({ availability: pack.availability, reason: pack.reason }),
|
|
54
|
+
files: [...files].sort(compareCodeUnits),
|
|
55
|
+
symbols: collectContextPackSymbols(pack, indexedFiles)
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Raw-full-file identity evidence; availability is supplied by the caller, never inferred from an empty list. */
|
|
59
|
+
export function buildRawFullFileIdentityEvidence(input) {
|
|
60
|
+
const files = new Set();
|
|
61
|
+
for (const file of input.includedFiles) {
|
|
62
|
+
const normalized = normalizeRetrievedRepositoryPath(file);
|
|
63
|
+
if (normalized !== null)
|
|
64
|
+
files.add(normalized);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
...withReason(input),
|
|
68
|
+
files: [...files].sort(compareCodeUnits),
|
|
69
|
+
symbols: collectRawFullFileSymbols(input.indexedFiles, input.includedFiles)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from "./analysis.js";
|
|
2
|
+
export * from "./analysisArtifact.js";
|
|
3
|
+
export * from "./analysisTypes.js";
|
|
4
|
+
export * from "./buildContextPack.js";
|
|
5
|
+
export * from "./config.js";
|
|
6
|
+
export * from "./execution.js";
|
|
7
|
+
export * from "./executionArtifact.js";
|
|
8
|
+
export * from "./executionTypes.js";
|
|
9
|
+
export * from "./identityEvidence.js";
|
|
10
|
+
export * from "./localSubjectExecution.js";
|
|
11
|
+
export * from "./localSubjectPrivacy.js";
|
|
12
|
+
export * from "./metadata.js";
|
|
13
|
+
export * from "./metrics.js";
|
|
14
|
+
export * from "./packArtifact.js";
|
|
15
|
+
export * from "./packEvidence.js";
|
|
16
|
+
export * from "./packSelectionPolicy.js";
|
|
17
|
+
export * from "./plugin.js";
|
|
18
|
+
export * from "./renderContextPack.js";
|
|
19
|
+
export * from "./types.js";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { validateRetrievalPrecisionRecallCase } from "../../../evaluation/retrievalQuality/index.js";
|
|
3
|
+
import { buildMyDevKitIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
4
|
+
import { runRawFullFileBaseline } from "../../../evaluation/runRawFullFileBaseline.js";
|
|
5
|
+
import { captureTargetSnapshot, compareTargetSnapshots } from "../../../evaluation/targetImmutability/index.js";
|
|
6
|
+
import { LocalSubjectExecutionError } from "../contextWindowScaling/localSubjectErrors.js";
|
|
7
|
+
import { deriveGuidedIndexExclusions } from "../contextWindowScaling/localSubjectExclusions.js";
|
|
8
|
+
import { assertWorkRootOutsideTarget, createPrivateScratch, defaultPrivateScratchIo, removePrivateScratch } from "../contextWindowScaling/localSubjectScratch.js";
|
|
9
|
+
import { executeContextPackGeneration } from "./execution.js";
|
|
10
|
+
/** Repository path carried by a `file:<path>` or `symbol:<path>#<name>` node ID; null when the ID has no such shape. */
|
|
11
|
+
export function repositoryPathOfNodeId(nodeId) {
|
|
12
|
+
const match = /^(?:file|symbol):([^#]+)(?:#.*)?$/.exec(nodeId);
|
|
13
|
+
return match ? match[1] : null;
|
|
14
|
+
}
|
|
15
|
+
/** Every real repository path identity one case result exposes through either treatment or the in-memory pack. */
|
|
16
|
+
export function collectContextPackFileIdentities(result) {
|
|
17
|
+
const identities = new Set();
|
|
18
|
+
for (const treatment of result.evidence.treatments) {
|
|
19
|
+
for (const file of treatment.includedFiles)
|
|
20
|
+
identities.add(file);
|
|
21
|
+
for (const file of treatment.identityEvidence?.files ?? [])
|
|
22
|
+
identities.add(file);
|
|
23
|
+
for (const symbol of treatment.identityEvidence?.symbols ?? []) {
|
|
24
|
+
if (symbol.file !== undefined)
|
|
25
|
+
identities.add(symbol.file);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
const pack = result.pack;
|
|
29
|
+
if (pack !== null) {
|
|
30
|
+
for (const file of pack.files)
|
|
31
|
+
identities.add(file.path);
|
|
32
|
+
for (const symbol of pack.symbols) {
|
|
33
|
+
if (symbol.file !== null)
|
|
34
|
+
identities.add(symbol.file);
|
|
35
|
+
}
|
|
36
|
+
for (const slice of pack.sourceSlices)
|
|
37
|
+
identities.add(slice.file);
|
|
38
|
+
for (const test of pack.tests)
|
|
39
|
+
identities.add(test.path);
|
|
40
|
+
for (const relationship of pack.callRelationships) {
|
|
41
|
+
for (const endpoint of [relationship.fromNodeId, relationship.toNodeId]) {
|
|
42
|
+
const endpointPath = repositoryPathOfNodeId(endpoint);
|
|
43
|
+
if (endpointPath !== null)
|
|
44
|
+
identities.add(endpointPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return identities;
|
|
49
|
+
}
|
|
50
|
+
/** Counts distinct file identities outside the eligible universe. Only the count leaves this function. */
|
|
51
|
+
export function countContextPackFilesOutsideEligibleUniverse(result, eligibleFiles) {
|
|
52
|
+
return [...collectContextPackFileIdentities(result)].filter((identity) => !eligibleFiles.has(identity)).length;
|
|
53
|
+
}
|
|
54
|
+
const FATAL_CASE_ERROR_CODES = new Set(["ground-truth-invalid", "project-group-inconsistent", "project-index-failed"]);
|
|
55
|
+
/**
|
|
56
|
+
* Runs a loaded local subject through the two-treatment context-pack experiment. One private base index per configured
|
|
57
|
+
* case (each case keeps its own exact source roots), a private scratch outside the target, guided exclusions so the index
|
|
58
|
+
* never sees ineligible files, the raw baseline restricted to the eligible universe, and pre/post target-immutability
|
|
59
|
+
* enforcement. It performs NO durable writes; success is returned only when every safety gate passed. A mutated target
|
|
60
|
+
* is never reverted. Issue messages are fixed text and counts.
|
|
61
|
+
*/
|
|
62
|
+
export async function executeLocalRepositorySubjectContextPackGeneration(args) {
|
|
63
|
+
const { subject } = args;
|
|
64
|
+
await assertWorkRootOutsideTarget(args.workRoot, subject.repositoryRoot);
|
|
65
|
+
const guidedExclusions = deriveGuidedIndexExclusions({
|
|
66
|
+
eligibleFiles: subject.eligibleFiles,
|
|
67
|
+
excludedFiles: [...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles]
|
|
68
|
+
});
|
|
69
|
+
if (subject.evaluationCases.some((evaluationCase) => validateRetrievalPrecisionRecallCase(evaluationCase).length > 0)) {
|
|
70
|
+
throw new LocalSubjectExecutionError([
|
|
71
|
+
{ code: "GROUND_TRUTH_INVALID", message: "context-pack ground truth is incomplete for configured case(s)." }
|
|
72
|
+
]);
|
|
73
|
+
}
|
|
74
|
+
const snapshotConfig = { targetRootPath: subject.repositoryRoot, relativeFilePaths: subject.eligibleFiles };
|
|
75
|
+
const snapshotOptions = { externalLocalSafe: { maxHashedFileBytes: subject.manifest.safetyPolicy.maxFileBytes } };
|
|
76
|
+
const before = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
|
|
77
|
+
if (!before.ok) {
|
|
78
|
+
throw new LocalSubjectExecutionError([{ code: "BEFORE_SNAPSHOT_FAILED", message: `target snapshot before execution failed (${before.code}).` }]);
|
|
79
|
+
}
|
|
80
|
+
const scratch = await createPrivateScratch(args.workRoot, subject.repositoryRoot);
|
|
81
|
+
const issues = [];
|
|
82
|
+
const results = [];
|
|
83
|
+
const eligible = new Set(subject.eligibleFiles);
|
|
84
|
+
const baseBuildIndex = args.dependencies?.buildIndex;
|
|
85
|
+
const baseRawBaseline = args.dependencies?.runRawBaseline ?? runRawFullFileBaseline;
|
|
86
|
+
for (const [position, evaluationCase] of subject.evaluationCases.entries()) {
|
|
87
|
+
try {
|
|
88
|
+
// The experiment owner writes its indexes and command output below the case's own scratch directory (short fixed segment).
|
|
89
|
+
const [caseResult] = await executeContextPackGeneration({
|
|
90
|
+
cases: [evaluationCase],
|
|
91
|
+
kitCommand: args.kitCommand,
|
|
92
|
+
outputRoot: path.join(scratch.path, `p${position + 1}`),
|
|
93
|
+
dependencies: {
|
|
94
|
+
...args.dependencies,
|
|
95
|
+
// Guided exclusions keep ignored and oversized files out of the index; the raw baseline reads only eligible files.
|
|
96
|
+
buildIndex: async (options) => {
|
|
97
|
+
return (baseBuildIndex ?? buildMyDevKitIndex)({ ...options, excludePaths: guidedExclusions });
|
|
98
|
+
},
|
|
99
|
+
runRawBaseline: (rawCase) => baseRawBaseline(rawCase, { eligibleFiles: subject.eligibleFiles })
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
if (caseResult.evidence.treatments.some((treatment) => treatment.errors.some((error) => FATAL_CASE_ERROR_CODES.has(error.code)))) {
|
|
103
|
+
issues.push({ code: "EXECUTION_FAILED", message: "context-pack index preparation failed for a configured case." });
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
const outside = countContextPackFilesOutsideEligibleUniverse(caseResult, eligible);
|
|
107
|
+
if (outside > 0) {
|
|
108
|
+
issues.push({
|
|
109
|
+
code: "RETRIEVAL_OUTSIDE_ELIGIBLE_UNIVERSE",
|
|
110
|
+
message: `context-pack generation exposed ${outside} file identit${outside === 1 ? "y" : "ies"} outside the eligible subject universe.`
|
|
111
|
+
});
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
results.push(caseResult);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
issues.push({ code: "EXECUTION_FAILED", message: "context-pack execution failed for a configured case." });
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Attempted even after a primary failure so a target mutation is never masked.
|
|
122
|
+
let comparison = null;
|
|
123
|
+
const after = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
|
|
124
|
+
if (!after.ok) {
|
|
125
|
+
issues.push({ code: "AFTER_SNAPSHOT_FAILED", message: `target snapshot after execution failed (${after.code}).` });
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
comparison = compareTargetSnapshots(before.snapshot, after.snapshot);
|
|
129
|
+
if (comparison.status === "mutated") {
|
|
130
|
+
issues.push({ code: "TARGET_MUTATED", message: "the target changed during execution; it was not restored." });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const cleanupFailure = await removePrivateScratch(scratch, args.scratchIo ?? defaultPrivateScratchIo);
|
|
134
|
+
if (cleanupFailure) {
|
|
135
|
+
issues.push({ code: "SCRATCH_CLEANUP_FAILED", message: "the private scratch could not be removed." });
|
|
136
|
+
}
|
|
137
|
+
if (issues.length > 0 || !comparison) {
|
|
138
|
+
throw new LocalSubjectExecutionError(issues.length > 0 ? issues : [{ code: "EXECUTION_FAILED", message: "execution did not complete." }], {
|
|
139
|
+
immutability: comparison
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
results,
|
|
144
|
+
immutability: { status: "unchanged", preExistingGitStatusEntryCount: comparison.preExistingGitStatusEntryCount, newMutationCount: 0 }
|
|
145
|
+
};
|
|
146
|
+
}
|