@dailephd/my-dev-kit-lab 0.8.0 → 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.
Files changed (62) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +7 -4
  3. package/dist/src/cli/help.js +35 -1
  4. package/dist/src/commands/runExperimentDescribeCommand.js +15 -1
  5. package/dist/src/commands/runExperimentRunCommand.js +177 -4
  6. package/dist/src/evaluation/retrievalQuality/metrics.js +27 -2
  7. package/dist/src/evaluation/retrievalQueryStrategies.js +26 -0
  8. package/dist/src/evaluation/retrievalQueryStrategyEvidence.js +58 -0
  9. package/dist/src/evaluation/runMyDevKitRetrieval.js +60 -51
  10. package/dist/src/evaluation/runSemanticRetrievalStrategy.js +303 -0
  11. package/dist/src/experiments/defaultRegistry.js +4 -0
  12. package/dist/src/experiments/plugins/contextPackGeneration/analysis.js +157 -0
  13. package/dist/src/experiments/plugins/contextPackGeneration/analysisArtifact.js +22 -0
  14. package/dist/src/experiments/plugins/contextPackGeneration/analysisTypes.js +1 -0
  15. package/dist/src/experiments/plugins/contextPackGeneration/buildContextPack.js +84 -0
  16. package/dist/src/experiments/plugins/contextPackGeneration/config.js +39 -0
  17. package/dist/src/experiments/plugins/contextPackGeneration/execution.js +361 -0
  18. package/dist/src/experiments/plugins/contextPackGeneration/executionArtifact.js +17 -0
  19. package/dist/src/experiments/plugins/contextPackGeneration/executionTypes.js +11 -0
  20. package/dist/src/experiments/plugins/contextPackGeneration/identityEvidence.js +71 -0
  21. package/dist/src/experiments/plugins/contextPackGeneration/index.js +19 -0
  22. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectExecution.js +146 -0
  23. package/dist/src/experiments/plugins/contextPackGeneration/localSubjectPrivacy.js +276 -0
  24. package/dist/src/experiments/plugins/contextPackGeneration/metadata.js +24 -0
  25. package/dist/src/experiments/plugins/contextPackGeneration/metrics.js +46 -0
  26. package/dist/src/experiments/plugins/contextPackGeneration/packArtifact.js +17 -0
  27. package/dist/src/experiments/plugins/contextPackGeneration/packEvidence.js +152 -0
  28. package/dist/src/experiments/plugins/contextPackGeneration/packSelectionPolicy.js +305 -0
  29. package/dist/src/experiments/plugins/contextPackGeneration/plugin.js +355 -0
  30. package/dist/src/experiments/plugins/contextPackGeneration/renderContextPack.js +61 -0
  31. package/dist/src/experiments/plugins/contextPackGeneration/types.js +29 -0
  32. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +2 -0
  33. package/dist/src/experiments/plugins/index.js +1 -0
  34. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysis.js +172 -0
  35. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisArtifact.js +28 -0
  36. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisTypes.js +2 -0
  37. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/config.js +39 -0
  38. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/execution.js +225 -0
  39. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/executionArtifact.js +15 -0
  40. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/index.js +12 -0
  41. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectExecution.js +124 -0
  42. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectPrivacy.js +176 -0
  43. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metadata.js +51 -0
  44. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metrics.js +60 -0
  45. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/plugin.js +292 -0
  46. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/types.js +1 -0
  47. package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
  48. package/dist/src/report/experiments/buildPluginExperimentReport.js +47 -2
  49. package/dist/src/report/experiments/buildRetrievalQueryStrategyComparisonReport.js +91 -0
  50. package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
  51. package/dist/src/report/experiments/index.js +9 -0
  52. package/dist/src/report/experiments/loadContextPackArtifacts.js +43 -0
  53. package/dist/src/report/experiments/renderContextPackGenerationHtml.js +122 -0
  54. package/dist/src/report/experiments/renderContextPackGenerationText.js +101 -0
  55. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
  56. package/dist/src/report/experiments/renderPluginExperimentReportText.js +16 -0
  57. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +96 -0
  58. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonText.js +58 -0
  59. package/dist/src/report/experiments/retrievalQueryStrategyComparisonReportModel.js +1 -0
  60. package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
  61. package/docs/METRICS.md +149 -2
  62. package/package.json +1 -1
@@ -0,0 +1,84 @@
1
+ import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../../../core/countTokens.js";
2
+ import { compareCodeUnits } from "../../../evaluation/indexSnapshot.js";
3
+ import { buildContextPackSelectionPolicy, selectCallRelationships, selectRelevantFiles, selectRelevantSymbols, selectSourceSlices, selectTestFiles } from "./packSelectionPolicy.js";
4
+ import { renderContextPack } from "./renderContextPack.js";
5
+ import { CONTEXT_PACK_SCHEMA_VERSION, CONTEXT_PACK_SECTION_IDS } from "./types.js";
6
+ /**
7
+ * Pure context-pack composition. No filesystem, process, environment, network, clock or randomness: identical input
8
+ * yields structurally identical output. It selects within the frozen caps, renders deterministically, and measures size
9
+ * from the exact rendered text.
10
+ */
11
+ export function buildContextPack(input) {
12
+ const files = selectRelevantFiles(input.fileCandidates);
13
+ const symbols = selectRelevantSymbols(input.symbolCandidates);
14
+ const sourceSlices = selectSourceSlices(input.sourceSliceCandidates);
15
+ const calls = selectCallRelationships(input.callCandidates);
16
+ const tests = selectTestFiles(input.testCandidates);
17
+ const evidenceNotes = [
18
+ ...new Set([
19
+ ...(input.evidenceNotes ?? []),
20
+ ...files.notes,
21
+ ...symbols.notes,
22
+ ...sourceSlices.notes,
23
+ ...calls.notes,
24
+ ...tests.notes
25
+ ])
26
+ ].sort(compareCodeUnits);
27
+ const sectionAvailability = {
28
+ task: { availability: "available", reason: null },
29
+ files: { ...input.sectionAvailability.files },
30
+ symbols: { ...input.sectionAvailability.symbols },
31
+ sourceSlices: { ...input.sectionAvailability.sourceSlices },
32
+ callRelationships: { ...input.sectionAvailability.callRelationships },
33
+ tests: { ...input.sectionAvailability.tests },
34
+ evidenceNotes: { ...input.sectionAvailability.evidenceNotes }
35
+ };
36
+ const itemCounts = {
37
+ task: 1,
38
+ files: files.items.length,
39
+ symbols: symbols.items.length,
40
+ sourceSlices: sourceSlices.items.length,
41
+ callRelationships: calls.items.length,
42
+ tests: tests.items.length,
43
+ evidenceNotes: evidenceNotes.length
44
+ };
45
+ const rendered = renderContextPack({
46
+ task: input.task,
47
+ files: files.items,
48
+ symbols: symbols.items,
49
+ sourceSlices: sourceSlices.items,
50
+ callRelationships: calls.items,
51
+ tests: tests.items,
52
+ evidenceNotes,
53
+ sectionAvailability
54
+ });
55
+ return {
56
+ schemaVersion: CONTEXT_PACK_SCHEMA_VERSION,
57
+ caseId: input.caseId,
58
+ benchmarkProject: input.benchmarkProject,
59
+ taskLocality: input.taskLocality,
60
+ task: { title: input.task.title, summary: input.task.summary },
61
+ selectionPolicy: buildContextPackSelectionPolicy(input.myDevKitVersion),
62
+ files: files.items,
63
+ symbols: symbols.items,
64
+ sourceSlices: sourceSlices.items,
65
+ callRelationships: calls.items,
66
+ tests: tests.items,
67
+ evidenceNotes,
68
+ sections: CONTEXT_PACK_SECTION_IDS.map((id) => ({
69
+ id,
70
+ availability: sectionAvailability[id].availability,
71
+ reason: sectionAvailability[id].reason,
72
+ itemCount: itemCounts[id],
73
+ estimatedTokens: countEstimatedTokens(rendered.sections[id])
74
+ })),
75
+ size: {
76
+ totalChars: countTextChars(rendered.renderedText),
77
+ totalEstimatedTokens: countEstimatedTokens(rendered.renderedText),
78
+ tokenCountMethod
79
+ },
80
+ availability: input.availability,
81
+ reason: input.reason,
82
+ renderedText: rendered.renderedText
83
+ };
84
+ }
@@ -0,0 +1,39 @@
1
+ import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
2
+ export const defaultContextPackGenerationConfig = {
3
+ outDir: "lab-output/context-pack-generation",
4
+ kitCommand: "npx @dailephd/my-dev-kit@latest"
5
+ };
6
+ const SUPPORTED_FIELDS = ["outDir", "kitCommand", "caseIds", "benchmarkProjects"];
7
+ export const contextPackGenerationConfigDefinition = {
8
+ fields: [
9
+ { name: "outDir", type: "string", required: true, description: "Experiment output root." },
10
+ { name: "kitCommand", type: "string", description: "my-dev-kit command used for index and retrieval." },
11
+ { name: "caseIds", type: "array", description: "Bundled evaluation case IDs to include; corpus order is preserved." },
12
+ { name: "benchmarkProjects", type: "array", description: "Bundled benchmark project IDs to include; corpus order is preserved." }
13
+ ]
14
+ };
15
+ export function validateContextPackGenerationConfig(config) {
16
+ if (config !== undefined && !isPlainObject(config)) {
17
+ return invalidExperimentConfig(["context pack generation config must be an object."]);
18
+ }
19
+ const errors = [];
20
+ const unsupported = Object.keys(config ?? {}).filter((key) => !SUPPORTED_FIELDS.includes(key));
21
+ if (unsupported.length > 0) {
22
+ errors.push(`Unsupported context-pack-generation config field(s): ${unsupported.sort().join(", ")}.`);
23
+ }
24
+ const normalized = mergeConfig(defaultContextPackGenerationConfig, config);
25
+ for (const field of ["outDir", "kitCommand"]) {
26
+ const value = normalized[field];
27
+ if (typeof value !== "string" || !value.trim())
28
+ errors.push(`${field} must be a non-empty string.`);
29
+ }
30
+ for (const field of ["caseIds", "benchmarkProjects"]) {
31
+ const value = normalized[field];
32
+ if (value === undefined)
33
+ continue;
34
+ if (!Array.isArray(value) || value.length === 0 || !value.every((item) => typeof item === "string" && item.trim())) {
35
+ errors.push(`${field} must be a non-empty array of non-empty strings when provided.`);
36
+ }
37
+ }
38
+ return errors.length > 0 ? invalidExperimentConfig(errors) : validExperimentConfig(normalized);
39
+ }
@@ -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";