@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** Stable source boundary; it cannot be confused with Markdown code-fence nesting. */
|
|
2
|
+
export const CONTEXT_PACK_SOURCE_BEGIN = "<<<SOURCE";
|
|
3
|
+
export const CONTEXT_PACK_SOURCE_END = "SOURCE>>>";
|
|
4
|
+
export const CONTEXT_PACK_SECTION_HEADINGS = {
|
|
5
|
+
task: "## Task",
|
|
6
|
+
files: "## Relevant files",
|
|
7
|
+
symbols: "## Relevant symbols",
|
|
8
|
+
sourceSlices: "## Source slices",
|
|
9
|
+
callRelationships: "## Call relationships",
|
|
10
|
+
tests: "## Tests",
|
|
11
|
+
evidenceNotes: "## Evidence notes"
|
|
12
|
+
};
|
|
13
|
+
const lf = (text) => text.replace(/\r\n?/g, "\n");
|
|
14
|
+
function renderBody(id, lines, availability) {
|
|
15
|
+
const heading = CONTEXT_PACK_SECTION_HEADINGS[id];
|
|
16
|
+
if (lines.length > 0)
|
|
17
|
+
return [heading, ...lines].join("\n");
|
|
18
|
+
const { availability: state, reason } = availability[id];
|
|
19
|
+
const suffix = reason === null ? "" : `: ${reason}`;
|
|
20
|
+
const placeholder = state === "available" ? "(none)" : `(${state}${suffix})`;
|
|
21
|
+
return `${heading}\n${placeholder}`;
|
|
22
|
+
}
|
|
23
|
+
function renderSlice(slice) {
|
|
24
|
+
return [
|
|
25
|
+
`file: ${slice.file}`,
|
|
26
|
+
`lines: ${slice.startLine}-${slice.endLine}`,
|
|
27
|
+
`boundary: ${slice.boundaryKnown ? "known" : "unknown"}`,
|
|
28
|
+
`truncated: ${slice.truncated ? "yes" : "no"}`,
|
|
29
|
+
CONTEXT_PACK_SOURCE_BEGIN,
|
|
30
|
+
lf(slice.text),
|
|
31
|
+
CONTEXT_PACK_SOURCE_END
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
/** Deterministic, language-agnostic rendering. Source text is verbatim apart from line-ending normalization. */
|
|
35
|
+
export function renderContextPack(input) {
|
|
36
|
+
const availability = input.sectionAvailability;
|
|
37
|
+
const sections = {
|
|
38
|
+
task: `${CONTEXT_PACK_SECTION_HEADINGS.task}\nTitle: ${lf(input.task.title)}\nSummary: ${lf(input.task.summary)}`,
|
|
39
|
+
files: renderBody("files", input.files.map((file) => `- ${file.path}`), availability),
|
|
40
|
+
symbols: renderBody("symbols", input.symbols.map((symbol) => (symbol.file === null ? `- ${symbol.name}` : `- ${symbol.name} (${symbol.file})`)), availability),
|
|
41
|
+
sourceSlices: renderSourceSlices(input.sourceSlices, availability),
|
|
42
|
+
callRelationships: renderBody("callRelationships", input.callRelationships.map((call) => `${call.fromNodeId} -> ${call.toNodeId}`), availability),
|
|
43
|
+
tests: renderBody("tests", input.tests.map((test) => `- ${test.path}`), availability),
|
|
44
|
+
evidenceNotes: renderBody("evidenceNotes", input.evidenceNotes.map((code) => `- ${code}`), availability)
|
|
45
|
+
};
|
|
46
|
+
const renderedText = `${[
|
|
47
|
+
sections.task,
|
|
48
|
+
sections.files,
|
|
49
|
+
sections.symbols,
|
|
50
|
+
sections.sourceSlices,
|
|
51
|
+
sections.callRelationships,
|
|
52
|
+
sections.tests,
|
|
53
|
+
sections.evidenceNotes
|
|
54
|
+
].join("\n\n")}\n`;
|
|
55
|
+
return { sections, renderedText };
|
|
56
|
+
}
|
|
57
|
+
function renderSourceSlices(slices, availability) {
|
|
58
|
+
if (slices.length === 0)
|
|
59
|
+
return renderBody("sourceSlices", [], availability);
|
|
60
|
+
return `${CONTEXT_PACK_SECTION_HEADINGS.sourceSlices}\n${slices.map(renderSlice).join("\n\n")}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Domain schema of the Lab-owned experimental context pack. It is a measurement artifact, not a production contract. */
|
|
2
|
+
export const CONTEXT_PACK_SCHEMA_VERSION = "my-dev-kit-lab-context-pack-experiment-v1";
|
|
3
|
+
/** Logical section order of the pack. */
|
|
4
|
+
export const CONTEXT_PACK_SECTION_IDS = [
|
|
5
|
+
"task",
|
|
6
|
+
"files",
|
|
7
|
+
"symbols",
|
|
8
|
+
"sourceSlices",
|
|
9
|
+
"callRelationships",
|
|
10
|
+
"tests",
|
|
11
|
+
"evidenceNotes"
|
|
12
|
+
];
|
|
13
|
+
/**
|
|
14
|
+
* Closed fixed vocabulary. Only codes produced by the pure Batch 1 domain exist; no free-form warning text, and no
|
|
15
|
+
* execution, indexing, privacy or persistence failure codes.
|
|
16
|
+
*/
|
|
17
|
+
export const CONTEXT_PACK_EVIDENCE_NOTE_CODES = [
|
|
18
|
+
"symbol-end-unknown",
|
|
19
|
+
"source-slice-cap-reached",
|
|
20
|
+
"total-source-line-cap-reached",
|
|
21
|
+
"source-slice-count-cap-reached",
|
|
22
|
+
"seed-cap-reached",
|
|
23
|
+
"file-cap-reached",
|
|
24
|
+
"symbol-cap-reached",
|
|
25
|
+
"test-file-cap-reached",
|
|
26
|
+
"call-relationship-cap-reached",
|
|
27
|
+
"invalid-repository-path-ignored",
|
|
28
|
+
"invalid-source-slice-ignored"
|
|
29
|
+
];
|
|
@@ -83,6 +83,8 @@ export function projectRunForExternalLocalPersistence(run, target) {
|
|
|
83
83
|
metadata.outputRoot = EXTERNAL_LOCAL_REDACTED_VALUE;
|
|
84
84
|
if (typeof metadata.executionArtifactPath === "string")
|
|
85
85
|
metadata.executionArtifactPath = path.basename(metadata.executionArtifactPath);
|
|
86
|
+
if (typeof metadata.analysisArtifactPath === "string")
|
|
87
|
+
metadata.analysisArtifactPath = path.basename(metadata.analysisArtifactPath);
|
|
86
88
|
return { ...run, target, metadata: metadata };
|
|
87
89
|
}
|
|
88
90
|
const GENERIC_ISSUE_TEXT = {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { cp, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveWithinRoot } from "../../../core/pathSafety.js";
|
|
4
|
+
import { validateRetrievalPrecisionRecallCase } from "../../../evaluation/retrievalQuality/index.js";
|
|
5
|
+
import { RETRIEVAL_QUERY_STRATEGY_IDS, isCoreGraphRetrievalQueryStrategyId } from "../../../evaluation/retrievalQueryStrategies.js";
|
|
6
|
+
import { buildMyDevKitIndex, runMyDevKitRetrievalStrategyFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
|
|
7
|
+
import { runSemanticRetrievalStrategyFromIndex } from "../../../evaluation/runSemanticRetrievalStrategy.js";
|
|
8
|
+
import { groupWarmIndexCases, taskOutputSegment } from "../warmIndexReuse/selection.js";
|
|
9
|
+
import { boundedRetrievalSafeMessage } from "../retrievalPrecisionRecall/execution.js";
|
|
10
|
+
const MAX_WARNINGS = 20;
|
|
11
|
+
export const SEMANTIC_INDEX_CLEANUP_FAILED_WARNING = "semantic-index-cleanup-failed";
|
|
12
|
+
function failedTreatment(strategyId, code, message) {
|
|
13
|
+
return { strategyId, status: "failed", retrieval: null, evidence: null, errors: [{ code, message }] };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The single normalization point from an executed strategy result into bounded comparison evidence. It takes no
|
|
17
|
+
* contextText and inspects no raw command output. An available observation (including an available empty one)
|
|
18
|
+
* is COMPLETED; partial or unavailable evidence from a measured result is PARTIAL.
|
|
19
|
+
*/
|
|
20
|
+
export function buildQueryStrategyTreatmentEvidence(strategyId, result) {
|
|
21
|
+
const evidence = result.queryStrategyEvidence;
|
|
22
|
+
return {
|
|
23
|
+
strategyId,
|
|
24
|
+
status: evidence.availability === "available" ? "completed" : "partial",
|
|
25
|
+
retrieval: {
|
|
26
|
+
skipped: result.skipped,
|
|
27
|
+
durationMs: result.durationMs,
|
|
28
|
+
totalEstimatedTokens: result.totalEstimatedTokens,
|
|
29
|
+
tokenCountMethod: result.tokenCountMethod,
|
|
30
|
+
warnings: result.warnings.slice(0, MAX_WARNINGS).map((warning) => boundedRetrievalSafeMessage(warning)),
|
|
31
|
+
evidenceAvailability: evidence.availability,
|
|
32
|
+
evidenceAvailabilityReason: evidence.availabilityReason,
|
|
33
|
+
retrievedFileCount: evidence.files.length,
|
|
34
|
+
retrievedSymbolCount: evidence.symbols.length,
|
|
35
|
+
steps: evidence.steps.map((step) => ({ ...step }))
|
|
36
|
+
},
|
|
37
|
+
evidence: structuredClone(evidence),
|
|
38
|
+
errors: []
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function failedCase(evaluationCase, code, message) {
|
|
42
|
+
return {
|
|
43
|
+
caseId: evaluationCase.id,
|
|
44
|
+
caseName: evaluationCase.title,
|
|
45
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
46
|
+
taskLocality: evaluationCase.taskLocality ?? null,
|
|
47
|
+
treatments: RETRIEVAL_QUERY_STRATEGY_IDS.map((strategyId) => failedTreatment(strategyId, code, message))
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Matched seven-treatment execution. One base index per benchmark project; the five core strategies share it;
|
|
52
|
+
* each semantic case/treatment gets a private copy of it because upstream `data-model` writes derived artifacts
|
|
53
|
+
* into the index directory. No agent runs and no quality metric is calculated. Only bounded, context-free
|
|
54
|
+
* evidence is returned.
|
|
55
|
+
*/
|
|
56
|
+
export async function executeRetrievalQueryStrategyComparison(options) {
|
|
57
|
+
const dependencies = {
|
|
58
|
+
buildIndex: options.dependencies?.buildIndex ?? buildMyDevKitIndex,
|
|
59
|
+
runCoreStrategy: options.dependencies?.runCoreStrategy ?? runMyDevKitRetrievalStrategyFromIndex,
|
|
60
|
+
runSemanticStrategy: options.dependencies?.runSemanticStrategy ?? runSemanticRetrievalStrategyFromIndex
|
|
61
|
+
};
|
|
62
|
+
const results = new Map();
|
|
63
|
+
const validCases = [];
|
|
64
|
+
for (const evaluationCase of options.cases) {
|
|
65
|
+
if (validateRetrievalPrecisionRecallCase(evaluationCase).length > 0) {
|
|
66
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "ground-truth-invalid", "Retrieval comparison ground truth is invalid for this case."));
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
validCases.push(evaluationCase);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
for (const group of groupWarmIndexCases(validCases)) {
|
|
73
|
+
if (group.structuralErrors.length > 0) {
|
|
74
|
+
for (const evaluationCase of group.cases) {
|
|
75
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "project-group-inconsistent", "Cases of one benchmark project disagree on target root or source roots."));
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const baseIndexDir = resolveWithinRoot(options.outputRoot, path.join("indexes", group.projectSegment, "base"));
|
|
80
|
+
const projectCommandsDir = resolveWithinRoot(options.outputRoot, path.join("commands", group.projectSegment));
|
|
81
|
+
let indexReady = false;
|
|
82
|
+
try {
|
|
83
|
+
// requireKit: false keeps a failed index as measurable evidence instead of throwing. Full, ordinary index only.
|
|
84
|
+
const build = await dependencies.buildIndex({
|
|
85
|
+
target: { absoluteTargetRoot: group.targetRoot, sourceRoots: [...group.sourceRoots] },
|
|
86
|
+
kitCommand: options.kitCommand,
|
|
87
|
+
indexDir: baseIndexDir,
|
|
88
|
+
commandsDir: path.join(projectCommandsDir, "index"),
|
|
89
|
+
requireKit: false
|
|
90
|
+
});
|
|
91
|
+
indexReady = build.ok;
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
indexReady = false;
|
|
95
|
+
}
|
|
96
|
+
if (!indexReady) {
|
|
97
|
+
for (const evaluationCase of group.cases) {
|
|
98
|
+
results.set(evaluationCase.id, failedCase(evaluationCase, "project-index-failed", "The benchmark-project index could not be prepared."));
|
|
99
|
+
}
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
for (const evaluationCase of group.cases) {
|
|
103
|
+
const caseSegment = taskOutputSegment(evaluationCase.id);
|
|
104
|
+
results.set(evaluationCase.id, await executeRetrievalQueryStrategyCaseFromPreparedIndex({
|
|
105
|
+
evaluationCase,
|
|
106
|
+
kitCommand: options.kitCommand,
|
|
107
|
+
baseIndexDir,
|
|
108
|
+
commandsDir: resolveWithinRoot(projectCommandsDir, caseSegment),
|
|
109
|
+
semanticIndexesRoot: resolveWithinRoot(options.outputRoot, path.join("strategy-indexes", group.projectSegment, caseSegment)),
|
|
110
|
+
dependencies
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Selection order is the corpus order after filtering, regardless of how projects were grouped.
|
|
115
|
+
return options.cases.map((evaluationCase) => results.get(evaluationCase.id));
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Executes exactly seven treatments for one already-validated case against one already-prepared base index. Core
|
|
119
|
+
* treatments share `baseIndexDir`; each semantic treatment gets its own fresh recursive copy under
|
|
120
|
+
* `semanticIndexesRoot`. It builds no index, validates no ground truth, and calculates no scientific analysis.
|
|
121
|
+
*/
|
|
122
|
+
export async function executeRetrievalQueryStrategyCaseFromPreparedIndex(options) {
|
|
123
|
+
const { evaluationCase } = options;
|
|
124
|
+
const dependencies = {
|
|
125
|
+
runCoreStrategy: options.dependencies?.runCoreStrategy ?? runMyDevKitRetrievalStrategyFromIndex,
|
|
126
|
+
runSemanticStrategy: options.dependencies?.runSemanticStrategy ?? runSemanticRetrievalStrategyFromIndex
|
|
127
|
+
};
|
|
128
|
+
const treatments = [];
|
|
129
|
+
// Strictly sequential, in the frozen strategy order: deterministic chronology and failure ordering.
|
|
130
|
+
for (const strategyId of RETRIEVAL_QUERY_STRATEGY_IDS) {
|
|
131
|
+
const commandsDir = resolveWithinRoot(options.commandsDir, strategyId);
|
|
132
|
+
treatments.push(isCoreGraphRetrievalQueryStrategyId(strategyId)
|
|
133
|
+
? await executeCoreTreatment({
|
|
134
|
+
strategyId,
|
|
135
|
+
evaluationCase,
|
|
136
|
+
kitCommand: options.kitCommand,
|
|
137
|
+
baseIndexDir: options.baseIndexDir,
|
|
138
|
+
commandsDir,
|
|
139
|
+
dependencies
|
|
140
|
+
})
|
|
141
|
+
: await executeSemanticTreatment({
|
|
142
|
+
strategyId,
|
|
143
|
+
evaluationCase,
|
|
144
|
+
kitCommand: options.kitCommand,
|
|
145
|
+
baseIndexDir: options.baseIndexDir,
|
|
146
|
+
treatmentIndexDir: resolveWithinRoot(options.semanticIndexesRoot, strategyId),
|
|
147
|
+
commandsDir,
|
|
148
|
+
dependencies
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
caseId: evaluationCase.id,
|
|
153
|
+
caseName: evaluationCase.title,
|
|
154
|
+
benchmarkProject: evaluationCase.benchmarkProject,
|
|
155
|
+
taskLocality: evaluationCase.taskLocality ?? null,
|
|
156
|
+
treatments
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const SEMANTIC_INDEX_CLEANUP_FAILED_MESSAGE = "The isolated semantic-strategy index could not be removed.";
|
|
160
|
+
const RETRIEVAL_THREW_MESSAGE = "The retrieval strategy threw before producing a measurement.";
|
|
161
|
+
async function executeCoreTreatment(args) {
|
|
162
|
+
try {
|
|
163
|
+
const result = await args.dependencies.runCoreStrategy({
|
|
164
|
+
strategyId: args.strategyId,
|
|
165
|
+
evaluationCase: args.evaluationCase,
|
|
166
|
+
kitCommand: args.kitCommand,
|
|
167
|
+
indexDir: args.baseIndexDir,
|
|
168
|
+
commandsDir: args.commandsDir,
|
|
169
|
+
requireKit: false
|
|
170
|
+
});
|
|
171
|
+
const evidence = result.queryStrategyEvidence;
|
|
172
|
+
if (!evidence)
|
|
173
|
+
return failedTreatment(args.strategyId, "retrieval-failed", RETRIEVAL_THREW_MESSAGE);
|
|
174
|
+
return buildQueryStrategyTreatmentEvidence(args.strategyId, { ...result, queryStrategyEvidence: evidence });
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return failedTreatment(args.strategyId, "retrieval-failed", RETRIEVAL_THREW_MESSAGE);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function executeSemanticTreatment(args) {
|
|
181
|
+
try {
|
|
182
|
+
// Fresh copy of the untouched base index; the destination must not pre-exist.
|
|
183
|
+
await rm(args.treatmentIndexDir, { recursive: true, force: true });
|
|
184
|
+
await cp(args.baseIndexDir, args.treatmentIndexDir, { recursive: true });
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
await rm(args.treatmentIndexDir, { recursive: true, force: true }).catch(() => undefined);
|
|
188
|
+
return failedTreatment(args.strategyId, "strategy-index-copy-failed", "The isolated semantic-strategy index could not be prepared.");
|
|
189
|
+
}
|
|
190
|
+
let treatment;
|
|
191
|
+
let cleanupFailed = false;
|
|
192
|
+
try {
|
|
193
|
+
const result = await args.dependencies.runSemanticStrategy({
|
|
194
|
+
strategyId: args.strategyId,
|
|
195
|
+
evaluationCase: args.evaluationCase,
|
|
196
|
+
kitCommand: args.kitCommand,
|
|
197
|
+
indexDir: args.treatmentIndexDir,
|
|
198
|
+
commandsDir: args.commandsDir,
|
|
199
|
+
requireKit: false
|
|
200
|
+
});
|
|
201
|
+
treatment = buildQueryStrategyTreatmentEvidence(args.strategyId, result);
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
treatment = failedTreatment(args.strategyId, "retrieval-failed", RETRIEVAL_THREW_MESSAGE);
|
|
205
|
+
}
|
|
206
|
+
finally {
|
|
207
|
+
try {
|
|
208
|
+
await rm(args.treatmentIndexDir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
// A cleanup failure never invalidates a measurement; it is recorded as a fixed, path-free warning.
|
|
212
|
+
cleanupFailed = true;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (cleanupFailed) {
|
|
216
|
+
if (treatment.retrieval) {
|
|
217
|
+
treatment.retrieval.warnings = [...treatment.retrieval.warnings, SEMANTIC_INDEX_CLEANUP_FAILED_WARNING];
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
// No measurement exists: keep the primary failure and make the leftover copied index auditable (path-free).
|
|
221
|
+
treatment.errors = [...treatment.errors, { code: "strategy-index-cleanup-failed", message: SEMANTIC_INDEX_CLEANUP_FAILED_MESSAGE }];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return treatment;
|
|
225
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { RETRIEVAL_QUERY_STRATEGY_IDS } from "../../../evaluation/retrievalQueryStrategies.js";
|
|
2
|
+
export const RETRIEVAL_QUERY_STRATEGY_COMPARISON_EXECUTION_ARTIFACT_FILE = "retrieval-query-strategy-comparison-execution.json";
|
|
3
|
+
export const RETRIEVAL_QUERY_STRATEGY_COMPARISON_EXECUTION_SCHEMA_VERSION = "my-dev-kit-lab-retrieval-query-strategy-comparison-execution-v1";
|
|
4
|
+
export function buildRetrievalQueryStrategyComparisonExecutionArtifact(args) {
|
|
5
|
+
return {
|
|
6
|
+
schemaVersion: RETRIEVAL_QUERY_STRATEGY_COMPARISON_EXECUTION_SCHEMA_VERSION,
|
|
7
|
+
runId: args.runId,
|
|
8
|
+
pluginId: args.pluginId,
|
|
9
|
+
pluginSchemaVersion: args.pluginSchemaVersion,
|
|
10
|
+
startedAt: args.startedAt,
|
|
11
|
+
completedAt: args.completedAt,
|
|
12
|
+
strategyOrder: [...RETRIEVAL_QUERY_STRATEGY_IDS],
|
|
13
|
+
cases: structuredClone(args.cases)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from "./analysis.js";
|
|
2
|
+
export * from "./analysisArtifact.js";
|
|
3
|
+
export * from "./analysisTypes.js";
|
|
4
|
+
export * from "./config.js";
|
|
5
|
+
export * from "./execution.js";
|
|
6
|
+
export * from "./executionArtifact.js";
|
|
7
|
+
export * from "./localSubjectExecution.js";
|
|
8
|
+
export * from "./localSubjectPrivacy.js";
|
|
9
|
+
export * from "./metadata.js";
|
|
10
|
+
export * from "./metrics.js";
|
|
11
|
+
export * from "./plugin.js";
|
|
12
|
+
export * from "./types.js";
|