@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.
- package/CHANGELOG.md +27 -0
- package/README.md +7 -4
- package/dist/src/cli/help.js +35 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +15 -1
- package/dist/src/commands/runExperimentRunCommand.js +177 -4
- package/dist/src/evaluation/retrievalQuality/metrics.js +27 -2
- package/dist/src/evaluation/retrievalQueryStrategies.js +26 -0
- package/dist/src/evaluation/retrievalQueryStrategyEvidence.js +58 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +60 -51
- package/dist/src/evaluation/runSemanticRetrievalStrategy.js +303 -0
- package/dist/src/experiments/defaultRegistry.js +4 -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/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +2 -0
- package/dist/src/experiments/plugins/index.js +1 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysis.js +172 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisArtifact.js +28 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisTypes.js +2 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/config.js +39 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/execution.js +225 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/executionArtifact.js +15 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/index.js +12 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectExecution.js +124 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectPrivacy.js +176 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metadata.js +51 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metrics.js +60 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/plugin.js +292 -0
- package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/types.js +1 -0
- package/dist/src/report/experiments/buildContextPackGenerationReport.js +171 -0
- package/dist/src/report/experiments/buildPluginExperimentReport.js +47 -2
- package/dist/src/report/experiments/buildRetrievalQueryStrategyComparisonReport.js +91 -0
- package/dist/src/report/experiments/contextPackGenerationReportModel.js +9 -0
- package/dist/src/report/experiments/index.js +9 -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 +6 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +16 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +96 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonText.js +58 -0
- package/dist/src/report/experiments/retrievalQueryStrategyComparisonReportModel.js +1 -0
- package/dist/src/report/experiments/writePluginExperimentReports.js +2 -0
- package/docs/METRICS.md +149 -2
- 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
|
-
*
|
|
244
|
-
*
|
|
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
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
|
324
|
+
warnings.push("No my-dev-kit node id was available after search.");
|
|
322
325
|
}
|
|
323
326
|
}
|
|
324
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|