@dailephd/my-dev-kit-lab 0.7.2 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -1
- package/README.md +9 -3
- package/benchmarks/contracts/warm-index-benchmark-cases.json +330 -0
- package/dist/src/cli/help.js +32 -1
- package/dist/src/commands/runExperimentDescribeCommand.js +16 -1
- package/dist/src/commands/runExperimentRunCommand.js +181 -4
- package/dist/src/evaluation/benchmarkMetadata.js +13 -0
- package/dist/src/evaluation/retrievalQuality/buildRetrievalEvidence.js +292 -0
- package/dist/src/evaluation/retrievalQuality/corpusCompleteness.js +122 -0
- package/dist/src/evaluation/retrievalQuality/factContextTargets.js +44 -0
- package/dist/src/evaluation/retrievalQuality/index.js +5 -0
- package/dist/src/evaluation/retrievalQuality/metrics.js +258 -0
- package/dist/src/evaluation/retrievalQuality/types.js +9 -0
- package/dist/src/evaluation/retrievalQueryStrategies.js +26 -0
- package/dist/src/evaluation/retrievalQueryStrategyEvidence.js +58 -0
- package/dist/src/evaluation/runMyDevKitRetrieval.js +61 -50
- package/dist/src/evaluation/runSemanticRetrievalStrategy.js +303 -0
- package/dist/src/experiments/defaultRegistry.js +4 -0
- package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +2 -0
- package/dist/src/experiments/plugins/index.js +2 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/config.js +39 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/execution.js +152 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/executionArtifact.js +15 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/index.js +9 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectExecution.js +139 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectPrivacy.js +119 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metadata.js +21 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/metrics.js +158 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/plugin.js +253 -0
- package/dist/src/experiments/plugins/retrievalPrecisionRecall/types.js +8 -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/buildPluginExperimentReport.js +44 -2
- package/dist/src/report/experiments/buildRetrievalPrecisionRecallReport.js +83 -0
- package/dist/src/report/experiments/buildRetrievalQueryStrategyComparisonReport.js +91 -0
- package/dist/src/report/experiments/index.js +8 -0
- package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
- package/dist/src/report/experiments/renderPluginExperimentReportText.js +18 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallHtml.js +96 -0
- package/dist/src/report/experiments/renderRetrievalPrecisionRecallText.js +121 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +96 -0
- package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonText.js +58 -0
- package/dist/src/report/experiments/retrievalPrecisionRecallReportModel.js +3 -0
- package/dist/src/report/experiments/retrievalQueryStrategyComparisonReportModel.js +1 -0
- package/docs/METRICS.md +152 -2
- package/package.json +1 -1
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { serializeLocalRepositorySubjectManifest } from "../../../evaluation/localRepositorySubject/index.js";
|
|
5
|
+
import { summarizeExperimentRun } from "../../results.js";
|
|
6
|
+
import { LocalSubjectExecutionError } from "../contextWindowScaling/localSubjectErrors.js";
|
|
7
|
+
import { projectExternalLocalTarget } from "../contextWindowScaling/localSubjectPrivacy.js";
|
|
8
|
+
import { isSamePhysicalDirectory } from "../contextWindowScaling/localSubjectScratch.js";
|
|
9
|
+
import { selectWarmIndexCases } from "../warmIndexReuse/selection.js";
|
|
10
|
+
import { defaultRetrievalPrecisionRecallConfig, retrievalPrecisionRecallConfigDefinition, validateRetrievalPrecisionRecallConfig } from "./config.js";
|
|
11
|
+
import { executeRetrievalPrecisionRecall } from "./execution.js";
|
|
12
|
+
import { executeLocalRepositorySubjectRetrievalPrecisionRecall } from "./localSubjectExecution.js";
|
|
13
|
+
import { assertExternalLocalProjectionIsPrivate, describeRetrievalLocalSubjectFailureForPersistence, projectRetrievalEvidenceForExternalLocalPersistence, UNEXPECTED_EXTERNAL_FAILURE_MESSAGE } from "./localSubjectPrivacy.js";
|
|
14
|
+
import { buildRetrievalPrecisionRecallExecutionArtifact, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE } from "./executionArtifact.js";
|
|
15
|
+
import { aggregateRetrievalPrecisionRecall, retrievalRatioOf, toRetrievalPrecisionRecallOutcomeMetrics, toRetrievalPrecisionRecallRunMetrics } from "./metrics.js";
|
|
16
|
+
import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID, RETRIEVAL_PRECISION_RECALL_VARIANTS, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, retrievalPrecisionRecallMetadata } from "./metadata.js";
|
|
17
|
+
import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "./types.js";
|
|
18
|
+
/** Privacy-safe Batch 1 manifest persisted beside the execution artifact for external-local runs (same file as context-window-scaling). */
|
|
19
|
+
export const RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE = "local-repository-subject-manifest.json";
|
|
20
|
+
/** An external target must always arrive with a loaded local repository subject; there is no bundled fallback against it. */
|
|
21
|
+
export const RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE = "External-local retrieval-precision-recall targets require a loaded local repository subject (--local-subject-config).";
|
|
22
|
+
export const retrievalPrecisionRecallPlugin = {
|
|
23
|
+
metadata: retrievalPrecisionRecallMetadata,
|
|
24
|
+
defaultConfig: defaultRetrievalPrecisionRecallConfig,
|
|
25
|
+
configDefinition: retrievalPrecisionRecallConfigDefinition,
|
|
26
|
+
supportedVariants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => variant.id),
|
|
27
|
+
validateConfig: validateRetrievalPrecisionRecallConfig,
|
|
28
|
+
async run(context) {
|
|
29
|
+
const startedAt = context.startedAt.toISOString();
|
|
30
|
+
// The runner does not enforce supportedTargets, so the plugin fails closed on its own.
|
|
31
|
+
const localSubject = readLocalSubjectInput(context.inputs);
|
|
32
|
+
if (localSubject)
|
|
33
|
+
return runLocalSubject(context, localSubject, startedAt);
|
|
34
|
+
if (context.target.kind !== "self" || context.target.isSelf !== true) {
|
|
35
|
+
// Never fall back to the bundled corpus against an external repository.
|
|
36
|
+
throw new Error(RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE);
|
|
37
|
+
}
|
|
38
|
+
const cases = selectWarmIndexCases(readCasesInput(context.inputs), context.config);
|
|
39
|
+
const outDir = context.outputRoot ?? path.resolve(context.toolRoot, context.config.outDir);
|
|
40
|
+
await mkdir(outDir, { recursive: true });
|
|
41
|
+
const caseEvidence = await executeRetrievalPrecisionRecall({
|
|
42
|
+
cases,
|
|
43
|
+
kitCommand: context.config.kitCommand,
|
|
44
|
+
outputRoot: outDir,
|
|
45
|
+
dependencies: readDependenciesInput(context.inputs)
|
|
46
|
+
});
|
|
47
|
+
// Scientific truth is calculated before persistence; the artifact and every report consume it unchanged.
|
|
48
|
+
const aggregate = aggregateRetrievalPrecisionRecall(caseEvidence);
|
|
49
|
+
const completedAt = new Date().toISOString();
|
|
50
|
+
const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
|
|
51
|
+
runId: context.runId,
|
|
52
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
53
|
+
pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
|
|
54
|
+
startedAt,
|
|
55
|
+
completedAt,
|
|
56
|
+
cases: caseEvidence,
|
|
57
|
+
aggregate
|
|
58
|
+
});
|
|
59
|
+
const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
|
|
60
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
|
|
61
|
+
return mapRetrievalPrecisionRecallToRun({
|
|
62
|
+
runId: context.runId,
|
|
63
|
+
startedAt,
|
|
64
|
+
completedAt,
|
|
65
|
+
target: context.target,
|
|
66
|
+
caseEvidence,
|
|
67
|
+
aggregate,
|
|
68
|
+
artifactPath
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
summarize(result) {
|
|
72
|
+
return result.summary ?? summarizeExperimentRun(result);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function readLocalSubjectInput(inputs) {
|
|
76
|
+
const value = inputs?.localSubject;
|
|
77
|
+
return value && typeof value === "object" ? value : undefined;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* External-local path. The safe seam owns execution, immutability and scratch cleanup and either returns evidence or
|
|
81
|
+
* throws; only after it succeeds is anything durable written: the case evidence is privacy-projected, the aggregate
|
|
82
|
+
* (calculated from the unprojected numbers) is preserved, and the artifact and manifest are written, in that order.
|
|
83
|
+
*/
|
|
84
|
+
async function runLocalSubject(context, subject, startedAt) {
|
|
85
|
+
if (context.target.isSelf || context.target.kind !== "external-local") {
|
|
86
|
+
throw new Error("Local-repository subject mode requires an external-local target.");
|
|
87
|
+
}
|
|
88
|
+
if (context.config.caseIds !== undefined || context.config.benchmarkProjects !== undefined) {
|
|
89
|
+
throw new Error("Case and benchmark-project filters are not supported for an external local repository subject; the subject config owns the case set.");
|
|
90
|
+
}
|
|
91
|
+
if (!(await isSamePhysicalDirectory(context.target.targetRoot, subject.repositoryRoot))) {
|
|
92
|
+
throw new Error("The selected --target is not the repository the local subject was loaded from.");
|
|
93
|
+
}
|
|
94
|
+
const outDir = context.outputRoot;
|
|
95
|
+
if (!outDir)
|
|
96
|
+
throw new Error("Local-repository subject mode requires an experiment output directory.");
|
|
97
|
+
let localResult;
|
|
98
|
+
try {
|
|
99
|
+
localResult = await executeLocalRepositorySubjectRetrievalPrecisionRecall({
|
|
100
|
+
subject,
|
|
101
|
+
kitCommand: context.config.kitCommand,
|
|
102
|
+
workRoot: outDir,
|
|
103
|
+
dependencies: readDependenciesInput(context.inputs)
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof LocalSubjectExecutionError)
|
|
108
|
+
throw new Error(describeRetrievalLocalSubjectFailureForPersistence(error));
|
|
109
|
+
throw new Error(UNEXPECTED_EXTERNAL_FAILURE_MESSAGE);
|
|
110
|
+
}
|
|
111
|
+
// Scientific numbers are calculated once from the unprojected evidence; projection only withholds identities.
|
|
112
|
+
const aggregate = aggregateRetrievalPrecisionRecall(localResult.caseEvidence);
|
|
113
|
+
const projected = projectRetrievalEvidenceForExternalLocalPersistence(localResult.caseEvidence);
|
|
114
|
+
assertExternalLocalProjectionIsPrivate(projected, {
|
|
115
|
+
filePaths: [...subject.eligibleFiles, ...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles],
|
|
116
|
+
caseTitles: subject.evaluationCases.map((evaluationCase) => evaluationCase.title),
|
|
117
|
+
warnings: localResult.caseEvidence.flatMap((entry) => entry.retrieval?.warnings ?? [])
|
|
118
|
+
});
|
|
119
|
+
await mkdir(outDir, { recursive: true });
|
|
120
|
+
const completedAt = new Date().toISOString();
|
|
121
|
+
const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
|
|
122
|
+
runId: context.runId,
|
|
123
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
124
|
+
pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
|
|
125
|
+
startedAt,
|
|
126
|
+
completedAt,
|
|
127
|
+
cases: projected,
|
|
128
|
+
aggregate
|
|
129
|
+
});
|
|
130
|
+
const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
|
|
131
|
+
await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}
|
|
132
|
+
`, "utf8");
|
|
133
|
+
const manifestPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE);
|
|
134
|
+
await writeFile(manifestPath, serializeLocalRepositorySubjectManifest(subject.manifest), "utf8");
|
|
135
|
+
const run = mapRetrievalPrecisionRecallToRun({
|
|
136
|
+
runId: context.runId,
|
|
137
|
+
startedAt,
|
|
138
|
+
completedAt,
|
|
139
|
+
target: projectExternalLocalTarget(subject.manifest),
|
|
140
|
+
caseEvidence: projected,
|
|
141
|
+
aggregate,
|
|
142
|
+
artifactPath
|
|
143
|
+
});
|
|
144
|
+
run.artifacts.push({
|
|
145
|
+
id: "local-repository-subject-manifest",
|
|
146
|
+
label: "Privacy-safe local repository subject manifest",
|
|
147
|
+
path: manifestPath,
|
|
148
|
+
kind: "artifact",
|
|
149
|
+
mimeType: "application/json",
|
|
150
|
+
description: "Privacy-safe logical subject, Git, safety and aggregate inventory metadata without source or private identity lists."
|
|
151
|
+
});
|
|
152
|
+
return run;
|
|
153
|
+
}
|
|
154
|
+
export function mapRetrievalPrecisionRecallToRun(args) {
|
|
155
|
+
const cases = args.caseEvidence.map((entry) => ({
|
|
156
|
+
id: entry.caseId,
|
|
157
|
+
name: entry.caseName,
|
|
158
|
+
outcomes: [buildOutcome(entry)],
|
|
159
|
+
metadata: { benchmarkProject: entry.benchmarkProject, taskLocality: entry.taskLocality }
|
|
160
|
+
}));
|
|
161
|
+
const failures = args.caseEvidence.flatMap((entry) => entry.errors.map((error) => ({
|
|
162
|
+
code: error.code,
|
|
163
|
+
message: error.message,
|
|
164
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
165
|
+
caseId: entry.caseId,
|
|
166
|
+
recoverable: true
|
|
167
|
+
})));
|
|
168
|
+
const run = {
|
|
169
|
+
runId: args.runId,
|
|
170
|
+
pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
|
|
171
|
+
startedAt: args.startedAt,
|
|
172
|
+
completedAt: args.completedAt,
|
|
173
|
+
status: aggregateStatus(args.caseEvidence.map((entry) => entry.status)),
|
|
174
|
+
target: args.target,
|
|
175
|
+
variants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => ({ ...variant })),
|
|
176
|
+
cases,
|
|
177
|
+
metrics: toRetrievalPrecisionRecallRunMetrics(args.aggregate),
|
|
178
|
+
artifacts: [
|
|
179
|
+
{
|
|
180
|
+
id: "retrieval-precision-recall-execution",
|
|
181
|
+
label: "Retrieval precision/recall execution evidence",
|
|
182
|
+
path: args.artifactPath,
|
|
183
|
+
kind: "artifact",
|
|
184
|
+
mimeType: "application/json",
|
|
185
|
+
description: "Per-case retrieval-quality evidence and aggregate without context text or raw command output."
|
|
186
|
+
}
|
|
187
|
+
],
|
|
188
|
+
warnings: [],
|
|
189
|
+
failures,
|
|
190
|
+
metadata: { executionArtifactPath: args.artifactPath },
|
|
191
|
+
caseExecutionEvidence: args.caseEvidence.map((entry) => structuredClone(entry)),
|
|
192
|
+
aggregate: structuredClone(args.aggregate)
|
|
193
|
+
};
|
|
194
|
+
run.summary = summarizeExperimentRun(run);
|
|
195
|
+
return run;
|
|
196
|
+
}
|
|
197
|
+
function buildOutcome(entry) {
|
|
198
|
+
const warnings = (entry.retrieval?.warnings ?? []).map((message) => ({
|
|
199
|
+
code: "retrieval-warning",
|
|
200
|
+
message,
|
|
201
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
202
|
+
caseId: entry.caseId
|
|
203
|
+
}));
|
|
204
|
+
const failures = entry.errors.map((error) => ({
|
|
205
|
+
code: error.code,
|
|
206
|
+
message: error.message,
|
|
207
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
208
|
+
caseId: entry.caseId,
|
|
209
|
+
recoverable: true
|
|
210
|
+
}));
|
|
211
|
+
// Availability and reason stay visible even though an unavailable/not-applicable ratio projects as a null value.
|
|
212
|
+
const metricAvailability = {};
|
|
213
|
+
for (const key of RETRIEVAL_PRECISION_RECALL_RATIO_KEYS) {
|
|
214
|
+
const ratio = retrievalRatioOf(entry.quality, key);
|
|
215
|
+
metricAvailability[key] = { availability: ratio.availability, reason: ratio.reason };
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
id: `${entry.caseId}:${RETRIEVAL_PRECISION_RECALL_VARIANT_ID}`,
|
|
219
|
+
caseId: entry.caseId,
|
|
220
|
+
variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
|
|
221
|
+
status: entry.status,
|
|
222
|
+
metrics: toRetrievalPrecisionRecallOutcomeMetrics(entry.quality, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, entry.caseId),
|
|
223
|
+
artifacts: [],
|
|
224
|
+
warnings,
|
|
225
|
+
failures,
|
|
226
|
+
metadata: {
|
|
227
|
+
benchmarkProject: entry.benchmarkProject,
|
|
228
|
+
evidenceAvailability: entry.retrieval?.evidenceAvailability ?? null,
|
|
229
|
+
metricAvailability
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** Same aggregation rule as the generic per-case summary: uniform statuses win, mixed is partial. */
|
|
234
|
+
function aggregateStatus(statuses) {
|
|
235
|
+
if (statuses.length === 0)
|
|
236
|
+
return "skipped";
|
|
237
|
+
for (const status of ["completed", "skipped", "failed"]) {
|
|
238
|
+
if (statuses.every((value) => value === status))
|
|
239
|
+
return status;
|
|
240
|
+
}
|
|
241
|
+
return "partial";
|
|
242
|
+
}
|
|
243
|
+
function readCasesInput(inputs) {
|
|
244
|
+
const value = inputs?.cases;
|
|
245
|
+
if (!Array.isArray(value)) {
|
|
246
|
+
throw new Error("Retrieval precision/recall requires cases input.");
|
|
247
|
+
}
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
function readDependenciesInput(inputs) {
|
|
251
|
+
const value = inputs?.retrievalDependencies;
|
|
252
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
253
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { RETRIEVAL_QUERY_STRATEGY_IDS } from "../../../evaluation/retrievalQueryStrategies.js";
|
|
2
|
+
import { calculateRetrievalQualityMetricsFromIdentityEvidence } from "../../../evaluation/retrievalQuality/metrics.js";
|
|
3
|
+
import { RETRIEVAL_QUERY_STRATEGY_COMPARISON_SCOPE_IDS } from "./analysisTypes.js";
|
|
4
|
+
function toRetrievalQualityIdentityEvidence(evidence) {
|
|
5
|
+
return {
|
|
6
|
+
availability: evidence.availability,
|
|
7
|
+
...(evidence.availabilityReason !== null ? { availabilityReason: evidence.availabilityReason } : {}),
|
|
8
|
+
files: evidence.files.map((file) => file.path),
|
|
9
|
+
symbols: evidence.symbols.map((symbol) => ({
|
|
10
|
+
name: symbol.name,
|
|
11
|
+
...(symbol.file !== null ? { file: symbol.file } : {})
|
|
12
|
+
}))
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Balanced F1 from identity-set sizes: 2TP / (2TP + FP + FN). Well defined when nothing was retrieved but
|
|
17
|
+
* relevant identities exist (F1 = 0). Never averages precision and recall.
|
|
18
|
+
*/
|
|
19
|
+
export function calculateBalancedIdentityF1(relevant, irrelevant, missed) {
|
|
20
|
+
if (relevant === null || irrelevant === null || missed === null) {
|
|
21
|
+
return { availability: "unavailable", numerator: null, denominator: null, value: null, reason: "quality-identity-sets-unavailable" };
|
|
22
|
+
}
|
|
23
|
+
const numerator = 2 * relevant.length;
|
|
24
|
+
const denominator = numerator + irrelevant.length + missed.length;
|
|
25
|
+
if (denominator === 0) {
|
|
26
|
+
return { availability: "not-applicable", numerator: null, denominator: null, value: null, reason: "no-positive-or-retrieved-identities" };
|
|
27
|
+
}
|
|
28
|
+
return { availability: "available", numerator, denominator, value: numerator / denominator, reason: null };
|
|
29
|
+
}
|
|
30
|
+
const noMeasurementF1 = () => ({
|
|
31
|
+
availability: "unavailable",
|
|
32
|
+
numerator: null,
|
|
33
|
+
denominator: null,
|
|
34
|
+
value: null,
|
|
35
|
+
reason: "no-retrieval-measurement"
|
|
36
|
+
});
|
|
37
|
+
/** Scores already-completed retrieval evidence. It never influences retrieval. */
|
|
38
|
+
export function analyzeRetrievalQueryStrategyTreatment(evaluationCase, treatment) {
|
|
39
|
+
if (treatment.evidence === null || treatment.retrieval === null) {
|
|
40
|
+
return {
|
|
41
|
+
strategyId: treatment.strategyId,
|
|
42
|
+
executionStatus: treatment.status,
|
|
43
|
+
quality: null,
|
|
44
|
+
fileF1: noMeasurementF1(),
|
|
45
|
+
symbolF1: noMeasurementF1()
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const quality = calculateRetrievalQualityMetricsFromIdentityEvidence({
|
|
49
|
+
evaluationCase,
|
|
50
|
+
retrieval: {
|
|
51
|
+
identityEvidence: toRetrievalQualityIdentityEvidence(treatment.evidence),
|
|
52
|
+
totalEstimatedTokens: treatment.retrieval.totalEstimatedTokens,
|
|
53
|
+
tokenCountMethod: treatment.retrieval.tokenCountMethod
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
strategyId: treatment.strategyId,
|
|
58
|
+
executionStatus: treatment.status,
|
|
59
|
+
quality,
|
|
60
|
+
fileF1: calculateBalancedIdentityF1(quality.file.relevantRetrievedFiles, quality.file.irrelevantRetrievedFiles, quality.file.missedFiles),
|
|
61
|
+
symbolF1: calculateBalancedIdentityF1(quality.symbol.relevantRetrievedSymbols, quality.symbol.irrelevantRetrievedSymbols, quality.symbol.missedSymbols)
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function analyzeRetrievalQueryStrategyCase(evaluationCase, executionCase) {
|
|
65
|
+
if (evaluationCase.id !== executionCase.caseId) {
|
|
66
|
+
throw new Error("Retrieval query strategy analysis case identity mismatch.");
|
|
67
|
+
}
|
|
68
|
+
const order = executionCase.treatments.map((treatment) => treatment.strategyId);
|
|
69
|
+
if (order.length !== RETRIEVAL_QUERY_STRATEGY_IDS.length || order.some((id, index) => id !== RETRIEVAL_QUERY_STRATEGY_IDS[index])) {
|
|
70
|
+
throw new Error("Retrieval query strategy analysis requires the seven canonical treatments in canonical order.");
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
caseId: executionCase.caseId,
|
|
74
|
+
benchmarkProject: executionCase.benchmarkProject,
|
|
75
|
+
taskLocality: executionCase.taskLocality,
|
|
76
|
+
treatments: executionCase.treatments.map((treatment) => analyzeRetrievalQueryStrategyTreatment(evaluationCase, treatment))
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function isTreatmentComparable(treatment) {
|
|
80
|
+
const quality = treatment.quality;
|
|
81
|
+
return (quality !== null &&
|
|
82
|
+
treatment.fileF1.availability === "available" &&
|
|
83
|
+
treatment.symbolF1.availability === "available" &&
|
|
84
|
+
quality.fact.coverage.availability === "available" &&
|
|
85
|
+
typeof quality.retrievedTokenCount === "number" &&
|
|
86
|
+
Number.isFinite(quality.retrievedTokenCount) &&
|
|
87
|
+
quality.retrievedTokenCount >= 0);
|
|
88
|
+
}
|
|
89
|
+
/** Matched design: a case counts only when all seven strategies expose all four objectives. */
|
|
90
|
+
export function isCaseComparableAcrossAllStrategies(caseAnalysis) {
|
|
91
|
+
return caseAnalysis.treatments.length === RETRIEVAL_QUERY_STRATEGY_IDS.length && caseAnalysis.treatments.every(isTreatmentComparable);
|
|
92
|
+
}
|
|
93
|
+
function casesForScope(cases, scopeId) {
|
|
94
|
+
return scopeId === "overall" ? [...cases] : cases.filter((entry) => entry.taskLocality === scopeId);
|
|
95
|
+
}
|
|
96
|
+
/** No epsilon, tolerance, rounding or weighting. */
|
|
97
|
+
export function paretoDominatesRetrievalStrategy(left, right) {
|
|
98
|
+
const noWorse = left.meanFileF1 >= right.meanFileF1 &&
|
|
99
|
+
left.meanSymbolF1 >= right.meanSymbolF1 &&
|
|
100
|
+
left.meanFactCoverage >= right.meanFactCoverage &&
|
|
101
|
+
left.meanRetrievedTokenCount <= right.meanRetrievedTokenCount;
|
|
102
|
+
const strictlyBetter = left.meanFileF1 > right.meanFileF1 ||
|
|
103
|
+
left.meanSymbolF1 > right.meanSymbolF1 ||
|
|
104
|
+
left.meanFactCoverage > right.meanFactCoverage ||
|
|
105
|
+
left.meanRetrievedTokenCount < right.meanRetrievedTokenCount;
|
|
106
|
+
return noWorse && strictlyBetter;
|
|
107
|
+
}
|
|
108
|
+
/** Every nondominated strategy in the order given (canonical strategy order); never a performance ranking. */
|
|
109
|
+
export function findRetrievalStrategyParetoFront(summaries) {
|
|
110
|
+
const candidates = summaries.filter((summary) => summary.objectives !== null);
|
|
111
|
+
return candidates
|
|
112
|
+
.filter((candidate) => !candidates.some((other) => other !== candidate && paretoDominatesRetrievalStrategy(other.objectives, candidate.objectives)))
|
|
113
|
+
.map((candidate) => candidate.strategyId);
|
|
114
|
+
}
|
|
115
|
+
export function aggregateRetrievalQueryStrategyScope(scopeId, cases) {
|
|
116
|
+
const scopeCases = casesForScope(cases, scopeId);
|
|
117
|
+
const comparisonCases = scopeCases.filter(isCaseComparableAcrossAllStrategies);
|
|
118
|
+
const strategySummaries = RETRIEVAL_QUERY_STRATEGY_IDS.map((strategyId, index) => {
|
|
119
|
+
const treatmentsOfScope = scopeCases.map((entry) => entry.treatments[index]);
|
|
120
|
+
const countStatus = (status) => treatmentsOfScope.filter((treatment) => treatment.executionStatus === status).length;
|
|
121
|
+
let objectives = null;
|
|
122
|
+
if (comparisonCases.length > 0) {
|
|
123
|
+
const values = comparisonCases.map((entry) => {
|
|
124
|
+
const treatment = entry.treatments[index];
|
|
125
|
+
return {
|
|
126
|
+
fileF1: treatment.fileF1.value,
|
|
127
|
+
symbolF1: treatment.symbolF1.value,
|
|
128
|
+
factCoverage: treatment.quality.fact.coverage.value,
|
|
129
|
+
retrievedTokens: treatment.quality.retrievedTokenCount
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
const mean = (select) => values.reduce((sum, value) => sum + select(value), 0) / values.length;
|
|
133
|
+
objectives = {
|
|
134
|
+
meanFileF1: mean((value) => value.fileF1),
|
|
135
|
+
meanSymbolF1: mean((value) => value.symbolF1),
|
|
136
|
+
meanFactCoverage: mean((value) => value.factCoverage),
|
|
137
|
+
meanRetrievedTokenCount: mean((value) => value.retrievedTokens)
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
strategyId,
|
|
142
|
+
completedCaseCount: countStatus("completed"),
|
|
143
|
+
partialCaseCount: countStatus("partial"),
|
|
144
|
+
failedCaseCount: countStatus("failed"),
|
|
145
|
+
objectives
|
|
146
|
+
};
|
|
147
|
+
});
|
|
148
|
+
const paretoFrontStrategyIds = comparisonCases.length === 0 ? [] : findRetrievalStrategyParetoFront(strategySummaries);
|
|
149
|
+
const unique = paretoFrontStrategyIds.length === 1;
|
|
150
|
+
return {
|
|
151
|
+
scopeId,
|
|
152
|
+
caseCount: scopeCases.length,
|
|
153
|
+
comparisonCaseCount: comparisonCases.length,
|
|
154
|
+
excludedCaseCount: scopeCases.length - comparisonCases.length,
|
|
155
|
+
comparisonCaseIds: comparisonCases.map((entry) => entry.caseId),
|
|
156
|
+
strategySummaries,
|
|
157
|
+
paretoFrontStrategyIds,
|
|
158
|
+
bestStrategyId: unique ? paretoFrontStrategyIds[0] : null,
|
|
159
|
+
interpretation: comparisonCases.length === 0 ? "unavailable" : unique ? "unique-best" : "tradeoff"
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export function analyzeRetrievalQueryStrategyComparison(evaluationCases, executionCases) {
|
|
163
|
+
if (evaluationCases.length !== executionCases.length ||
|
|
164
|
+
evaluationCases.some((evaluationCase, index) => evaluationCase.id !== executionCases[index].caseId)) {
|
|
165
|
+
throw new Error("Retrieval query strategy analysis requires matching selected case IDs in the same order.");
|
|
166
|
+
}
|
|
167
|
+
const cases = evaluationCases.map((evaluationCase, index) => analyzeRetrievalQueryStrategyCase(evaluationCase, executionCases[index]));
|
|
168
|
+
return {
|
|
169
|
+
cases,
|
|
170
|
+
scopes: RETRIEVAL_QUERY_STRATEGY_COMPARISON_SCOPE_IDS.map((scopeId) => aggregateRetrievalQueryStrategyScope(scopeId, cases))
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const RETRIEVAL_QUERY_STRATEGY_COMPARISON_ANALYSIS_ARTIFACT_FILE = "retrieval-query-strategy-comparison-analysis.json";
|
|
2
|
+
export const RETRIEVAL_QUERY_STRATEGY_COMPARISON_ANALYSIS_SCHEMA_VERSION = "my-dev-kit-lab-retrieval-query-strategy-comparison-analysis-v1";
|
|
3
|
+
/** Single owner of the frozen methodology; the artifact and the report both use it. */
|
|
4
|
+
export const RETRIEVAL_QUERY_STRATEGY_COMPARISON_METHODOLOGY = {
|
|
5
|
+
fileF1: "balanced-f1",
|
|
6
|
+
symbolF1: "balanced-f1",
|
|
7
|
+
aggregation: "matched-complete-case-macro-mean",
|
|
8
|
+
multiObjectiveComparison: "pareto-dominance",
|
|
9
|
+
uniqueBestRule: "single-member-pareto-front",
|
|
10
|
+
objectiveDirections: {
|
|
11
|
+
meanFileF1: "maximize",
|
|
12
|
+
meanSymbolF1: "maximize",
|
|
13
|
+
meanFactCoverage: "maximize",
|
|
14
|
+
meanRetrievedTokenCount: "minimize"
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
export function buildRetrievalQueryStrategyComparisonAnalysisArtifact(args) {
|
|
18
|
+
return {
|
|
19
|
+
schemaVersion: RETRIEVAL_QUERY_STRATEGY_COMPARISON_ANALYSIS_SCHEMA_VERSION,
|
|
20
|
+
runId: args.runId,
|
|
21
|
+
pluginId: args.pluginId,
|
|
22
|
+
pluginSchemaVersion: args.pluginSchemaVersion,
|
|
23
|
+
startedAt: args.startedAt,
|
|
24
|
+
completedAt: args.completedAt,
|
|
25
|
+
methodology: structuredClone(RETRIEVAL_QUERY_STRATEGY_COMPARISON_METHODOLOGY),
|
|
26
|
+
analysis: structuredClone(args.analysis)
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { invalidExperimentConfig, isPlainObject, mergeConfig, validExperimentConfig } from "../../config.js";
|
|
2
|
+
export const defaultRetrievalQueryStrategyComparisonConfig = {
|
|
3
|
+
outDir: "lab-output/retrieval-query-strategy-comparison",
|
|
4
|
+
kitCommand: "npx @dailephd/my-dev-kit@latest"
|
|
5
|
+
};
|
|
6
|
+
const SUPPORTED_FIELDS = ["outDir", "kitCommand", "caseIds", "benchmarkProjects"];
|
|
7
|
+
export const retrievalQueryStrategyComparisonConfigDefinition = {
|
|
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 validateRetrievalQueryStrategyComparisonConfig(config) {
|
|
16
|
+
if (config !== undefined && !isPlainObject(config)) {
|
|
17
|
+
return invalidExperimentConfig(["retrieval query strategy comparison 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 retrieval-query-strategy-comparison config field(s): ${unsupported.sort().join(", ")}.`);
|
|
23
|
+
}
|
|
24
|
+
const normalized = mergeConfig(defaultRetrievalQueryStrategyComparisonConfig, 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
|
+
}
|