@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
@@ -3,6 +3,7 @@ import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/
3
3
  import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
4
4
  import { interpretToolVersionOutput } from "./indexSnapshot.js";
5
5
  import { buildRetrievalEvidenceFromCommands } from "./retrievalQuality/buildRetrievalEvidence.js";
6
+ import { buildQueryStrategyEvidenceFromRetrievalEvidence } from "./retrievalQueryStrategyEvidence.js";
6
7
  const VERSION_PROBE_TIMEOUT_MS = 30_000;
7
8
  function parseJsonIfPossible(text) {
8
9
  try {
@@ -182,6 +183,7 @@ export async function buildMyDevKitIndex(options) {
182
183
  ...(options.excludePaths ?? []).flatMap((excludePath) => ["--exclude", excludePath]),
183
184
  "--out",
184
185
  options.indexDir,
186
+ ...(options.callGraph === true ? ["--call-graph"] : []),
185
187
  ...(mode.kind === "incremental" ? ["--incremental", "--refresh-scope", mode.refreshScope] : []),
186
188
  "--json"
187
189
  ]
@@ -239,11 +241,31 @@ export async function probeMyDevKitVersion(options) {
239
241
  });
240
242
  }
241
243
  }
244
+ function selectRetrievalStrategyContextText(strategyId, outputs) {
245
+ const ordered = strategyId === "keyword-search"
246
+ ? [outputs.search]
247
+ : strategyId === "symbol-lookup"
248
+ ? [outputs.lookup, outputs.search]
249
+ : strategyId === "graph-neighborhood"
250
+ ? [outputs.slice, outputs.search]
251
+ : strategyId === "source-slice"
252
+ ? [outputs.source, outputs.search]
253
+ : [outputs.source, outputs.slice, outputs.lookup, outputs.search];
254
+ return ordered.find((text) => text && text.trim().length > 0) ?? "";
255
+ }
256
+ const STRATEGY_DOWNSTREAM_COMMANDS = {
257
+ "keyword-search": [],
258
+ "symbol-lookup": ["lookup"],
259
+ "graph-neighborhood": ["slice"],
260
+ "source-slice": ["source"],
261
+ "combined-graph-guided": ["lookup", "slice", "source"]
262
+ };
242
263
  /**
243
- * Runs search -> lookup -> slice -> source against an already prepared index. Never invokes
244
- * `index`; `durationMs` covers retrieval only.
264
+ * Executes exactly one core retrieval query strategy against an already prepared index. Every
265
+ * strategy starts with `search` and expands at most the first candidate. Never invokes `index`;
266
+ * `durationMs` covers retrieval only.
245
267
  */
246
- export async function runMyDevKitRetrievalFromIndex(options) {
268
+ export async function runMyDevKitRetrievalStrategyFromIndex(options) {
247
269
  const started = Date.now();
248
270
  const warnings = [];
249
271
  const commands = [];
@@ -274,58 +296,37 @@ export async function runMyDevKitRetrievalFromIndex(options) {
274
296
  const selectedNodeId = candidate.nodeId;
275
297
  const selectedFile = candidate.file;
276
298
  const selectedSymbol = candidate.symbol;
277
- let lookupOutput = "";
278
- let sliceOutput = "";
279
- let sourceOutput = "";
280
- if (selectedNodeId) {
281
- const lookupCommand = await runMeasuredCommand({
282
- commandId: "lookup",
283
- commandString: options.kitCommand,
284
- cwd: process.cwd(),
285
- outDir: commandsDir,
286
- extraArgs: ["lookup", "--index", indexDir, "--node", selectedNodeId, "--json"]
287
- });
288
- commands.push(lookupCommand);
289
- if (lookupCommand.ok) {
290
- lookupOutput = lookupCommand.stdout;
291
- }
292
- else {
293
- warnings.push("my-dev-kit lookup command failed.");
294
- }
295
- const sliceCommand = await runMeasuredCommand({
296
- commandId: "slice",
297
- commandString: options.kitCommand,
298
- cwd: process.cwd(),
299
- outDir: commandsDir,
300
- extraArgs: ["slice", "--index", indexDir, "--node", selectedNodeId, "--json"]
301
- });
302
- commands.push(sliceCommand);
303
- if (sliceCommand.ok) {
304
- sliceOutput = sliceCommand.stdout;
305
- }
306
- else {
307
- warnings.push("my-dev-kit slice command failed.");
308
- }
309
- const sourceCommand = await runMeasuredCommand({
310
- commandId: "source",
311
- commandString: options.kitCommand,
312
- cwd: process.cwd(),
313
- outDir: commandsDir,
314
- extraArgs: ["source", "--index", indexDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
315
- });
316
- commands.push(sourceCommand);
317
- if (sourceCommand.ok) {
318
- sourceOutput = sourceCommand.stdout;
299
+ const outputs = { search: searchCommand.stdout, lookup: "", slice: "", source: "" };
300
+ const downstream = STRATEGY_DOWNSTREAM_COMMANDS[options.strategyId];
301
+ if (downstream.length > 0) {
302
+ if (selectedNodeId) {
303
+ for (const commandId of downstream) {
304
+ const extraArgs = commandId === "source"
305
+ ? ["source", "--index", indexDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
306
+ : [commandId, "--index", indexDir, "--node", selectedNodeId, "--json"];
307
+ const command = await runMeasuredCommand({
308
+ commandId,
309
+ commandString: options.kitCommand,
310
+ cwd: process.cwd(),
311
+ outDir: commandsDir,
312
+ extraArgs
313
+ });
314
+ commands.push(command);
315
+ if (command.ok) {
316
+ outputs[commandId] = command.stdout;
317
+ }
318
+ else {
319
+ warnings.push(`my-dev-kit ${commandId} command failed.`);
320
+ }
321
+ }
319
322
  }
320
323
  else {
321
- warnings.push("my-dev-kit source command failed.");
324
+ warnings.push("No my-dev-kit node id was available after search.");
322
325
  }
323
326
  }
324
- else {
325
- warnings.push("No my-dev-kit node id was available after search.");
326
- }
327
- const contextText = [sourceOutput, sliceOutput, lookupOutput, searchCommand.stdout].find((text) => text && text.trim().length > 0) ?? "";
327
+ const contextText = selectRetrievalStrategyContextText(options.strategyId, outputs);
328
328
  const filesRead = selectedFile ? [selectedFile] : [];
329
+ const retrievalEvidence = buildRetrievalEvidenceFromCommands(commands, { nodeId: selectedNodeId, file: selectedFile });
329
330
  return {
330
331
  caseId: options.evaluationCase.id,
331
332
  skipped: contextText.length === 0,
@@ -339,10 +340,18 @@ export async function runMyDevKitRetrievalFromIndex(options) {
339
340
  selectedNodeId,
340
341
  selectedFile,
341
342
  selectedSymbol,
342
- retrievalEvidence: buildRetrievalEvidenceFromCommands(commands, { nodeId: selectedNodeId, file: selectedFile }),
343
+ retrievalEvidence,
344
+ queryStrategyEvidence: buildQueryStrategyEvidenceFromRetrievalEvidence(options.strategyId, retrievalEvidence),
343
345
  durationMs: Date.now() - started
344
346
  };
345
347
  }
348
+ /**
349
+ * v0.8.0 compatibility entrypoint: the historical search -> lookup -> slice -> source lifecycle,
350
+ * delegating to the `combined-graph-guided` strategy. Never invokes `index`.
351
+ */
352
+ export async function runMyDevKitRetrievalFromIndex(options) {
353
+ return runMyDevKitRetrievalStrategyFromIndex({ ...options, strategyId: "combined-graph-guided" });
354
+ }
346
355
  /**
347
356
  * Legacy per-case lifecycle: builds a case-specific index and retrieves from it. `commands`
348
357
  * starts with the index command and `durationMs` covers the whole lifecycle including indexing.
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Semantic retrieval strategies (`data-model-graph`, `model-view-lineage`) executed against an
3
+ * already prepared my-dev-kit index through `data-model` queries.
4
+ *
5
+ * The target is chosen from the task query and the upstream data-model graph only; the answer key is
6
+ * never read. Output is normalized into `retrieval-query-strategy-evidence-v1`, never into
7
+ * `retrieval-evidence-v1`, because these commands are not search/lookup/slice/source.
8
+ */
9
+ import { readFile } from "node:fs/promises";
10
+ import path from "node:path";
11
+ import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/countTokens.js";
12
+ import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
13
+ import { compareCodeUnits } from "./indexSnapshot.js";
14
+ import { normalizeRetrievedRepositoryPath } from "./retrievalQuality/buildRetrievalEvidence.js";
15
+ import { canonicalizeQueryStrategyIdentities } from "./retrievalQueryStrategyEvidence.js";
16
+ const REJECTED_REASON = "some-semantic-identities-were-rejected";
17
+ function isRecord(value) {
18
+ return !!value && typeof value === "object" && !Array.isArray(value);
19
+ }
20
+ function parseJsonIfPossible(text) {
21
+ try {
22
+ return JSON.parse(text);
23
+ }
24
+ catch {
25
+ return undefined;
26
+ }
27
+ }
28
+ /**
29
+ * Lowercases, splits on every run of non ASCII alphanumerics, drops tokens shorter than two
30
+ * characters, deduplicates, and sorts in code-unit order. No stemming, synonyms, or stop words.
31
+ */
32
+ export function tokenizeSemanticRetrievalQuery(query) {
33
+ const tokens = query
34
+ .toLowerCase()
35
+ .split(/[^A-Za-z0-9]+/)
36
+ .filter((token) => token.length >= 2);
37
+ return [...new Set(tokens)].sort(compareCodeUnits);
38
+ }
39
+ /** Count of unique query tokens that occur exactly as label tokens. */
40
+ export function scoreSemanticLabel(queryTokens, label) {
41
+ const labelTokens = new Set(tokenizeSemanticRetrievalQuery(label));
42
+ let score = 0;
43
+ for (const token of new Set(queryTokens)) {
44
+ if (labelTokens.has(token))
45
+ score += 1;
46
+ }
47
+ return score;
48
+ }
49
+ function step(kind, succeeded, evidenceAvailable, reason) {
50
+ return { kind, succeeded, evidenceAvailable, reason };
51
+ }
52
+ function buildEvidence(strategyId, availability, availabilityReason, identities, steps) {
53
+ const canonical = canonicalizeQueryStrategyIdentities(identities.files.map((file) => ({ path: file })), identities.symbols);
54
+ return {
55
+ schemaVersion: "retrieval-query-strategy-evidence-v1",
56
+ strategyId,
57
+ availability,
58
+ availabilityReason,
59
+ files: canonical.files,
60
+ symbols: canonical.symbols,
61
+ steps
62
+ };
63
+ }
64
+ function unavailableEvidence(strategyId, reason, steps) {
65
+ return buildEvidence(strategyId, "unavailable", reason, { files: [], symbols: [] }, steps);
66
+ }
67
+ async function readDataModelGraph(indexDir) {
68
+ let text;
69
+ try {
70
+ text = await readFile(path.join(indexDir, "data-model-graph.json"), "utf8");
71
+ }
72
+ catch {
73
+ return { ok: false, reason: "data-model-graph-artifact-missing" };
74
+ }
75
+ const payload = parseJsonIfPossible(text);
76
+ if (!isRecord(payload))
77
+ return { ok: false, reason: "data-model-graph-artifact-malformed" };
78
+ if (payload.artifactKind !== "my-dev-kit-v1-data-model-graph" ||
79
+ typeof payload.schemaVersion !== "string" ||
80
+ !payload.schemaVersion.startsWith("1.")) {
81
+ return { ok: false, reason: "data-model-graph-schema-unsupported" };
82
+ }
83
+ if (!Array.isArray(payload.nodes) || !Array.isArray(payload.edges)) {
84
+ return { ok: false, reason: "data-model-graph-artifact-malformed" };
85
+ }
86
+ const nodes = [];
87
+ for (const node of payload.nodes) {
88
+ if (isRecord(node) &&
89
+ typeof node.id === "string" &&
90
+ typeof node.label === "string" &&
91
+ (node.kind === "entity" || node.kind === "field")) {
92
+ nodes.push({ id: node.id, kind: node.kind, label: node.label });
93
+ }
94
+ }
95
+ return { ok: true, nodes };
96
+ }
97
+ /** Highest score wins; ties break on node id in code-unit order. No candidate means no match. */
98
+ function selectSemanticCandidate(queryTokens, nodes) {
99
+ let best;
100
+ for (const node of nodes) {
101
+ const score = scoreSemanticLabel(queryTokens, node.label);
102
+ if (score <= 0)
103
+ continue;
104
+ if (!best || score > best.score || (score === best.score && compareCodeUnits(node.id, best.node.id) < 0)) {
105
+ best = { node, score };
106
+ }
107
+ }
108
+ return best?.node;
109
+ }
110
+ /** Normalizes `filePath` of every ref; unsafe values are counted, never kept. */
111
+ function collectSafeFiles(refs) {
112
+ const files = [];
113
+ let rejected = 0;
114
+ for (const ref of refs) {
115
+ const normalized = isRecord(ref) ? normalizeRetrievedRepositoryPath(ref.filePath) : null;
116
+ if (normalized === null) {
117
+ rejected += 1;
118
+ }
119
+ else {
120
+ files.push(normalized);
121
+ }
122
+ }
123
+ return { files, rejected };
124
+ }
125
+ function uniqueFileOrNull(files) {
126
+ const unique = [...new Set(files)];
127
+ return unique.length === 1 ? unique[0] : null;
128
+ }
129
+ function readEntityOutput(payload, expectedMode) {
130
+ if (!isRecord(payload) || payload.status !== "ok" || typeof payload.mode !== "string")
131
+ return "malformed";
132
+ if (payload.mode !== expectedMode)
133
+ return "mismatched";
134
+ return payload;
135
+ }
136
+ function readIdentity(value) {
137
+ return isRecord(value) && typeof value.id === "string" && typeof value.name === "string"
138
+ ? { id: value.id, name: value.name }
139
+ : undefined;
140
+ }
141
+ function buildDataModelGraphStrategyEvidence(selectedNode, lookupPayload, generationCommand, lookupCommand) {
142
+ const strategyId = "data-model-graph";
143
+ const generationStep = step("data-model", generationCommand.ok, generationCommand.ok, null);
144
+ const failed = (reason) => unavailableEvidence(strategyId, reason, [generationStep, step("data-model", lookupCommand.ok, false, reason)]);
145
+ if (!lookupCommand.ok)
146
+ return failed("data-model-lookup-command-failed");
147
+ const expectedMode = selectedNode.kind === "entity" ? "entity" : "field";
148
+ const output = readEntityOutput(lookupPayload, expectedMode);
149
+ if (output === "malformed")
150
+ return failed("data-model-lookup-output-malformed");
151
+ if (output === "mismatched")
152
+ return failed("data-model-lookup-output-mismatched");
153
+ const selected = readIdentity(expectedMode === "entity" ? output.entity : output.field);
154
+ if (!selected || !Array.isArray(output.sourceRefs))
155
+ return failed("data-model-lookup-output-malformed");
156
+ const { files, rejected } = collectSafeFiles(output.sourceRefs);
157
+ const symbol = {
158
+ name: selected.name,
159
+ nodeId: selected.id,
160
+ file: uniqueFileOrNull(files)
161
+ };
162
+ const partial = rejected > 0;
163
+ return buildEvidence(strategyId, partial ? "partial" : "available", partial ? REJECTED_REASON : null, { files, symbols: [symbol] }, [generationStep, step("data-model", true, !partial, partial ? REJECTED_REASON : null)]);
164
+ }
165
+ function buildModelViewLineageStrategyEvidence(tracePayload, generationCommand, traceCommand, expectedMode) {
166
+ const strategyId = "model-view-lineage";
167
+ const generationStep = step("data-model", generationCommand.ok, generationCommand.ok, null);
168
+ const failed = (reason) => unavailableEvidence(strategyId, reason, [generationStep, step("model-view-lineage", traceCommand.ok, false, reason)]);
169
+ if (!traceCommand.ok)
170
+ return failed("model-view-lineage-command-failed");
171
+ const output = readEntityOutput(tracePayload, expectedMode);
172
+ if (output === "malformed")
173
+ return failed("model-view-lineage-output-malformed");
174
+ if (output === "mismatched")
175
+ return failed("model-view-lineage-output-mismatched");
176
+ const lineage = output.lineage;
177
+ if (!readIdentity(output.entity) || !isRecord(lineage) || !Array.isArray(lineage.nodes) || !Array.isArray(lineage.edges)) {
178
+ return failed("model-view-lineage-output-malformed");
179
+ }
180
+ const files = [];
181
+ const symbols = [];
182
+ let rejected = 0;
183
+ for (const node of lineage.nodes) {
184
+ if (!isRecord(node) || typeof node.id !== "string" || typeof node.label !== "string") {
185
+ rejected += 1;
186
+ continue;
187
+ }
188
+ let nodeFiles = [];
189
+ if (Array.isArray(node.evidenceRefs)) {
190
+ const collected = collectSafeFiles(node.evidenceRefs);
191
+ nodeFiles = collected.files;
192
+ rejected += collected.rejected;
193
+ }
194
+ else if (node.evidenceRefs !== undefined) {
195
+ rejected += 1;
196
+ }
197
+ files.push(...nodeFiles);
198
+ symbols.push({ name: node.label, nodeId: node.id, file: uniqueFileOrNull(nodeFiles) });
199
+ }
200
+ const partial = rejected > 0;
201
+ return buildEvidence(strategyId, partial ? "partial" : "available", partial ? REJECTED_REASON : null, { files, symbols }, [generationStep, step("model-view-lineage", true, !partial, partial ? REJECTED_REASON : null)]);
202
+ }
203
+ async function runDataModelCommand(options, commandId, args, commands) {
204
+ const command = await runMeasuredCommand({
205
+ commandId,
206
+ commandString: options.kitCommand,
207
+ cwd: process.cwd(),
208
+ outDir: options.commandsDir,
209
+ extraArgs: ["data-model", "--index", options.indexDir, ...args, "--json"]
210
+ });
211
+ commands.push(command);
212
+ if (!command.ok && options.requireKit) {
213
+ throw new Error(command.error || `my-dev-kit data-model failed with exit code ${command.exitCode}`);
214
+ }
215
+ return command;
216
+ }
217
+ function buildResult(options, started, commands, evidence, contextText) {
218
+ const warnings = [];
219
+ if (evidence.availability === "unavailable") {
220
+ warnings.push(`my-dev-kit semantic retrieval was unavailable: ${evidence.availabilityReason}.`);
221
+ }
222
+ else if (evidence.availability === "partial") {
223
+ warnings.push(`my-dev-kit semantic retrieval was partially interpreted: ${evidence.availabilityReason}.`);
224
+ }
225
+ const text = evidence.availability === "unavailable" ? "" : contextText;
226
+ return {
227
+ caseId: options.evaluationCase.id,
228
+ strategyId: options.strategyId,
229
+ skipped: text.length === 0,
230
+ warnings,
231
+ totalChars: countTextChars(text),
232
+ totalEstimatedTokens: countEstimatedTokens(text),
233
+ tokenCountMethod,
234
+ contextText: text,
235
+ commands,
236
+ queryStrategyEvidence: evidence,
237
+ durationMs: Date.now() - started
238
+ };
239
+ }
240
+ /** Shared by both strategies: generate artifacts, validate the graph, select one semantic node. */
241
+ async function resolveSemanticTarget(options, commands) {
242
+ const generation = await runDataModelCommand(options, "data-model", [], commands);
243
+ if (!generation.ok) {
244
+ return {
245
+ kind: "done",
246
+ evidence: unavailableEvidence(options.strategyId, "data-model-command-failed", [
247
+ step("data-model", false, false, "data-model-command-failed")
248
+ ])
249
+ };
250
+ }
251
+ const generationStep = step("data-model", true, true, null);
252
+ const graph = await readDataModelGraph(options.indexDir);
253
+ if (!graph.ok) {
254
+ return {
255
+ kind: "done",
256
+ evidence: unavailableEvidence(options.strategyId, graph.reason, [step("data-model", true, false, graph.reason)])
257
+ };
258
+ }
259
+ const node = selectSemanticCandidate(tokenizeSemanticRetrievalQuery(options.evaluationCase.query), graph.nodes);
260
+ if (!node) {
261
+ return {
262
+ kind: "done",
263
+ evidence: buildEvidence(options.strategyId, "available", null, { files: [], symbols: [] }, [generationStep])
264
+ };
265
+ }
266
+ return { kind: "node", node, generation };
267
+ }
268
+ async function executeDataModelGraphStrategy(options, started) {
269
+ const commands = [];
270
+ const resolved = await resolveSemanticTarget(options, commands);
271
+ if (resolved.kind === "done")
272
+ return buildResult(options, started, commands, resolved.evidence, "");
273
+ const selector = resolved.node.kind === "entity" ? ["--entity", resolved.node.label] : ["--field", resolved.node.label];
274
+ const lookup = await runDataModelCommand(options, "data-model-selected", selector, commands);
275
+ const evidence = buildDataModelGraphStrategyEvidence(resolved.node, lookup.ok ? parseJsonIfPossible(lookup.stdout) : undefined, resolved.generation, lookup);
276
+ return buildResult(options, started, commands, evidence, lookup.stdout);
277
+ }
278
+ async function executeModelViewLineageStrategy(options, started) {
279
+ const commands = [];
280
+ const resolved = await resolveSemanticTarget(options, commands);
281
+ if (resolved.kind === "done")
282
+ return buildResult(options, started, commands, resolved.evidence, "");
283
+ const isEntity = resolved.node.kind === "entity";
284
+ const selector = isEntity ? ["--trace-view", resolved.node.label] : ["--field", resolved.node.label, "--trace-view"];
285
+ const trace = await runDataModelCommand(options, "model-view-lineage", selector, commands);
286
+ const evidence = buildModelViewLineageStrategyEvidence(trace.ok ? parseJsonIfPossible(trace.stdout) : undefined, resolved.generation, trace, isEntity ? "trace-entity" : "trace-field");
287
+ return buildResult(options, started, commands, evidence, trace.stdout);
288
+ }
289
+ /**
290
+ * Executes one semantic retrieval strategy against an already prepared index. Never invokes `index`,
291
+ * never reads the answer key, never runs agents.
292
+ */
293
+ export async function runSemanticRetrievalStrategyFromIndex(options) {
294
+ const started = Date.now();
295
+ switch (options.strategyId) {
296
+ case "data-model-graph":
297
+ return executeDataModelGraphStrategy(options, started);
298
+ case "model-view-lineage":
299
+ return executeModelViewLineageStrategy(options, started);
300
+ default:
301
+ throw new Error(`Unsupported semantic retrieval strategy: ${String(options.strategyId)}`);
302
+ }
303
+ }
@@ -1,8 +1,10 @@
1
1
  import { ExperimentPluginRegistry } from "./registry.js";
2
2
  import { contextStrategyComparisonPlugin } from "./plugins/contextStrategyComparison/index.js";
3
+ import { contextPackGenerationPlugin } from "./plugins/contextPackGeneration/index.js";
3
4
  import { contextWindowScalingPlugin } from "./plugins/contextWindowScaling/index.js";
4
5
  import { incrementalChangeStalenessPlugin } from "./plugins/incrementalChangeStaleness/index.js";
5
6
  import { retrievalPrecisionRecallPlugin } from "./plugins/retrievalPrecisionRecall/index.js";
7
+ import { retrievalQueryStrategyComparisonPlugin } from "./plugins/retrievalQueryStrategyComparison/index.js";
6
8
  import { warmIndexReusePlugin } from "./plugins/warmIndexReuse/index.js";
7
9
  export function createDefaultExperimentPluginRegistry() {
8
10
  const registry = new ExperimentPluginRegistry();
@@ -11,5 +13,7 @@ export function createDefaultExperimentPluginRegistry() {
11
13
  registry.register(incrementalChangeStalenessPlugin);
12
14
  registry.register(contextWindowScalingPlugin);
13
15
  registry.register(retrievalPrecisionRecallPlugin);
16
+ registry.register(retrievalQueryStrategyComparisonPlugin);
17
+ registry.register(contextPackGenerationPlugin);
14
18
  return registry;
15
19
  }
@@ -0,0 +1,157 @@
1
+ import { calculateRetrievalQualityMetricsFromIdentityEvidence } from "../../../evaluation/retrievalQuality/metrics.js";
2
+ import { calculateBalancedIdentityF1 } from "../retrievalQueryStrategyComparison/analysis.js";
3
+ import { RETRIEVAL_QUERY_STRATEGY_COMPARISON_SCOPE_IDS } from "../retrievalQueryStrategyComparison/analysisTypes.js";
4
+ import { CONTEXT_PACK_GENERATION_TREATMENT_IDS } from "./metadata.js";
5
+ const noMeasurementF1 = () => ({
6
+ availability: "unavailable",
7
+ numerator: null,
8
+ denominator: null,
9
+ value: null,
10
+ reason: "no-measurement"
11
+ });
12
+ /** Scores already-completed treatment evidence with the frozen retrieval-quality science. It never influences retrieval. */
13
+ export function analyzeContextPackGenerationTreatment(evaluationCase, treatment) {
14
+ if (treatment.identityEvidence === null || treatment.size === null) {
15
+ return {
16
+ treatmentId: treatment.treatmentId,
17
+ executionStatus: treatment.status,
18
+ availability: treatment.availability,
19
+ quality: null,
20
+ fileF1: noMeasurementF1(),
21
+ symbolF1: noMeasurementF1(),
22
+ estimatedTokens: null,
23
+ tokenCountMethod: null
24
+ };
25
+ }
26
+ const quality = calculateRetrievalQualityMetricsFromIdentityEvidence({
27
+ evaluationCase,
28
+ retrieval: {
29
+ identityEvidence: treatment.identityEvidence,
30
+ totalEstimatedTokens: treatment.size.totalEstimatedTokens,
31
+ tokenCountMethod: treatment.size.tokenCountMethod
32
+ }
33
+ });
34
+ return {
35
+ treatmentId: treatment.treatmentId,
36
+ executionStatus: treatment.status,
37
+ availability: treatment.availability,
38
+ quality,
39
+ fileF1: calculateBalancedIdentityF1(quality.file.relevantRetrievedFiles, quality.file.irrelevantRetrievedFiles, quality.file.missedFiles),
40
+ symbolF1: calculateBalancedIdentityF1(quality.symbol.relevantRetrievedSymbols, quality.symbol.irrelevantRetrievedSymbols, quality.symbol.missedSymbols),
41
+ estimatedTokens: treatment.size.totalEstimatedTokens,
42
+ tokenCountMethod: treatment.size.tokenCountMethod
43
+ };
44
+ }
45
+ /**
46
+ * Same arithmetic as the existing repository token-savings comparison (tokensSaved = raw - other; percentSaved =
47
+ * tokensSaved / raw * 100, 0 when raw is 0). Negative savings are valid and never clamped.
48
+ */
49
+ export function calculateContextPackTokenSavings(rawTokens, packTokens) {
50
+ const tokensSaved = rawTokens - packTokens;
51
+ return { tokensSaved, percentSaved: rawTokens === 0 ? 0 : (tokensSaved / rawTokens) * 100 };
52
+ }
53
+ const valueOf = (metric) => (metric && metric.availability === "available" ? metric.value : null);
54
+ function compareTreatments(raw, pack) {
55
+ const delta = (packValue, rawValue) => (packValue === null || rawValue === null ? null : packValue - rawValue);
56
+ const factPack = valueOf(pack.quality?.fact.coverage);
57
+ const factRaw = valueOf(raw.quality?.fact.coverage);
58
+ const sizes = raw.estimatedTokens !== null && pack.estimatedTokens !== null ? calculateContextPackTokenSavings(raw.estimatedTokens, pack.estimatedTokens) : null;
59
+ return {
60
+ fileF1Delta: delta(valueOf(pack.fileF1), valueOf(raw.fileF1)),
61
+ symbolF1Delta: delta(valueOf(pack.symbolF1), valueOf(raw.symbolF1)),
62
+ factCoverageDelta: delta(factPack, factRaw),
63
+ estimatedTokenDelta: delta(pack.estimatedTokens, raw.estimatedTokens),
64
+ tokensSaved: sizes?.tokensSaved ?? null,
65
+ percentSaved: sizes?.percentSaved ?? null
66
+ };
67
+ }
68
+ export function analyzeContextPackGenerationCase(evaluationCase, executionCase) {
69
+ if (evaluationCase.id !== executionCase.caseId)
70
+ throw new Error("Context pack generation analysis case identity mismatch.");
71
+ const order = executionCase.treatments.map((treatment) => treatment.treatmentId);
72
+ if (order.length !== CONTEXT_PACK_GENERATION_TREATMENT_IDS.length || order.some((id, index) => id !== CONTEXT_PACK_GENERATION_TREATMENT_IDS[index])) {
73
+ throw new Error("Context pack generation analysis requires the two canonical treatments in canonical order.");
74
+ }
75
+ const treatments = executionCase.treatments.map((treatment) => analyzeContextPackGenerationTreatment(evaluationCase, treatment));
76
+ return {
77
+ caseId: executionCase.caseId,
78
+ benchmarkProject: executionCase.benchmarkProject,
79
+ taskLocality: executionCase.taskLocality,
80
+ treatments,
81
+ comparison: compareTreatments(treatments[0], treatments[1])
82
+ };
83
+ }
84
+ function isComparable(treatment) {
85
+ const quality = treatment.quality;
86
+ return (quality !== null &&
87
+ treatment.fileF1.availability === "available" &&
88
+ treatment.symbolF1.availability === "available" &&
89
+ quality.fact.coverage.availability === "available" &&
90
+ typeof quality.retrievedTokenCount === "number" &&
91
+ Number.isFinite(quality.retrievedTokenCount) &&
92
+ quality.retrievedTokenCount >= 0);
93
+ }
94
+ /** Matched design: a case counts only when BOTH treatments expose all four required measures. */
95
+ export function isContextPackCaseComparable(caseAnalysis) {
96
+ return caseAnalysis.treatments.length === CONTEXT_PACK_GENERATION_TREATMENT_IDS.length && caseAnalysis.treatments.every(isComparable);
97
+ }
98
+ const mean = (values) => values.reduce((sum, value) => sum + value, 0) / values.length;
99
+ function casesForScope(cases, scopeId) {
100
+ return scopeId === "overall" ? [...cases] : cases.filter((entry) => entry.taskLocality === scopeId);
101
+ }
102
+ export function aggregateContextPackGenerationScope(scopeId, cases) {
103
+ const scopeCases = casesForScope(cases, scopeId);
104
+ const included = scopeCases.filter(isContextPackCaseComparable);
105
+ const excluded = scopeCases.filter((entry) => !isContextPackCaseComparable(entry));
106
+ const treatmentSummaries = CONTEXT_PACK_GENERATION_TREATMENT_IDS.map((treatmentId, index) => {
107
+ const countStatus = (status) => scopeCases.filter((entry) => entry.treatments[index].executionStatus === status).length;
108
+ const rows = included.map((entry) => entry.treatments[index]);
109
+ return {
110
+ treatmentId,
111
+ completedCaseCount: countStatus("completed"),
112
+ partialCaseCount: countStatus("partial"),
113
+ failedCaseCount: countStatus("failed"),
114
+ objectives: rows.length === 0
115
+ ? null
116
+ : {
117
+ meanFileF1: mean(rows.map((row) => row.fileF1.value)),
118
+ meanSymbolF1: mean(rows.map((row) => row.symbolF1.value)),
119
+ meanFactCoverage: mean(rows.map((row) => row.quality.fact.coverage.value)),
120
+ meanEstimatedTokenCount: mean(rows.map((row) => row.quality.retrievedTokenCount))
121
+ }
122
+ };
123
+ });
124
+ const [rawSummary, packSummary] = treatmentSummaries;
125
+ const rawObjectives = rawSummary.objectives;
126
+ const packObjectives = packSummary.objectives;
127
+ const paired = rawObjectives && packObjectives;
128
+ return {
129
+ scopeId,
130
+ caseCount: scopeCases.length,
131
+ includedCaseCount: included.length,
132
+ excludedCaseCount: excluded.length,
133
+ includedCaseIds: included.map((entry) => entry.caseId),
134
+ excludedCaseIds: excluded.map((entry) => entry.caseId),
135
+ treatmentSummaries,
136
+ pairedDeltas: paired
137
+ ? {
138
+ meanFileF1Delta: mean(included.map((entry) => entry.comparison.fileF1Delta)),
139
+ meanSymbolF1Delta: mean(included.map((entry) => entry.comparison.symbolF1Delta)),
140
+ meanFactCoverageDelta: mean(included.map((entry) => entry.comparison.factCoverageDelta)),
141
+ meanEstimatedTokenDelta: mean(included.map((entry) => entry.comparison.estimatedTokenDelta))
142
+ }
143
+ : null,
144
+ tokenSavings: paired
145
+ ? {
146
+ meanTokensSaved: mean(included.map((entry) => entry.comparison.tokensSaved)),
147
+ percentSavedOfMeans: calculateContextPackTokenSavings(rawObjectives.meanEstimatedTokenCount, packObjectives.meanEstimatedTokenCount).percentSaved
148
+ }
149
+ : null
150
+ };
151
+ }
152
+ export function analyzeContextPackGeneration(evaluationCases, executionCases) {
153
+ if (evaluationCases.length !== executionCases.length)
154
+ throw new Error("Context pack generation analysis requires one execution case per evaluation case.");
155
+ const cases = evaluationCases.map((evaluationCase, index) => analyzeContextPackGenerationCase(evaluationCase, executionCases[index]));
156
+ return { cases, scopes: RETRIEVAL_QUERY_STRATEGY_COMPARISON_SCOPE_IDS.map((scopeId) => aggregateContextPackGenerationScope(scopeId, cases)) };
157
+ }
@@ -0,0 +1,22 @@
1
+ export const CONTEXT_PACK_GENERATION_ANALYSIS_ARTIFACT_FILE = "context-pack-generation-analysis.json";
2
+ export const CONTEXT_PACK_GENERATION_ANALYSIS_SCHEMA_VERSION = "my-dev-kit-lab-context-pack-generation-analysis-v1";
3
+ /** Single owner of the frozen methodology. No ranking, winner, composite score or Pareto front exists for this experiment. */
4
+ export const CONTEXT_PACK_GENERATION_METHODOLOGY = {
5
+ fileF1: "balanced-f1",
6
+ symbolF1: "balanced-f1",
7
+ aggregation: "matched-complete-case-macro-mean",
8
+ treatmentComparison: "paired-descriptive-delta",
9
+ sizeMeasure: "estimated-tokens-of-rendered-text"
10
+ };
11
+ export function buildContextPackGenerationAnalysisArtifact(args) {
12
+ return {
13
+ schemaVersion: CONTEXT_PACK_GENERATION_ANALYSIS_SCHEMA_VERSION,
14
+ runId: args.runId,
15
+ pluginId: args.pluginId,
16
+ pluginSchemaVersion: args.pluginSchemaVersion,
17
+ startedAt: args.startedAt,
18
+ completedAt: args.completedAt,
19
+ methodology: { ...CONTEXT_PACK_GENERATION_METHODOLOGY },
20
+ analysis: structuredClone(args.analysis)
21
+ };
22
+ }