@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,44 @@
|
|
|
1
|
+
import { normalizeRetrievedRepositoryPath } from "./buildRetrievalEvidence.js";
|
|
2
|
+
/** True when the target participates in fact mapping, i.e. it carries a `factIds` field. */
|
|
3
|
+
export function hasFactMapping(target) {
|
|
4
|
+
return typeof target === "object" && target !== null && !Array.isArray(target) && target.factIds !== undefined;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Single owner of the `ExpectedContextTarget.factIds` contract. Pure. Problems are fixed bounded strings and
|
|
8
|
+
* never echo the offending value. Unsafe file identities are rejected, never repaired.
|
|
9
|
+
*/
|
|
10
|
+
export function interpretFactContextTarget(target, knownFactIds) {
|
|
11
|
+
if (typeof target !== "object" || target === null || Array.isArray(target)) {
|
|
12
|
+
return { ok: false, problem: "target must be an object" };
|
|
13
|
+
}
|
|
14
|
+
const record = target;
|
|
15
|
+
const file = normalizeRetrievedRepositoryPath(record.file);
|
|
16
|
+
if (file === null) {
|
|
17
|
+
return { ok: false, problem: "file must be a safe repository-relative path" };
|
|
18
|
+
}
|
|
19
|
+
const rawFactIds = record.factIds;
|
|
20
|
+
if (!Array.isArray(rawFactIds) || rawFactIds.length === 0 || rawFactIds.some((id) => typeof id !== "string" || id.length === 0)) {
|
|
21
|
+
return { ok: false, problem: "factIds must be a nonempty array of nonempty strings" };
|
|
22
|
+
}
|
|
23
|
+
const factIds = rawFactIds;
|
|
24
|
+
if (new Set(factIds).size !== factIds.length) {
|
|
25
|
+
return { ok: false, problem: "factIds must not contain duplicates" };
|
|
26
|
+
}
|
|
27
|
+
if (factIds.some((id) => !knownFactIds.has(id))) {
|
|
28
|
+
return { ok: false, problem: "factIds must reference existing expectedFacts ids" };
|
|
29
|
+
}
|
|
30
|
+
let symbols = [];
|
|
31
|
+
if (record.symbols !== undefined) {
|
|
32
|
+
if (!Array.isArray(record.symbols) || record.symbols.some((symbol) => typeof symbol !== "string" || symbol.length === 0)) {
|
|
33
|
+
return { ok: false, problem: "symbols must be an array of nonempty strings" };
|
|
34
|
+
}
|
|
35
|
+
symbols = record.symbols;
|
|
36
|
+
if (new Set(symbols).size !== symbols.length) {
|
|
37
|
+
return { ok: false, problem: "symbols must not contain duplicates" };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (record.required !== undefined && typeof record.required !== "boolean") {
|
|
41
|
+
return { ok: false, problem: "required must be a boolean when present" };
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, target: { file, symbols: [...symbols], required: record.required !== false, factIds: [...factIds] } };
|
|
44
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { compareCodeUnits } from "../indexSnapshot.js";
|
|
2
|
+
import { normalizeRetrievedRepositoryPath } from "./buildRetrievalEvidence.js";
|
|
3
|
+
import { hasFactMapping, interpretFactContextTarget } from "./factContextTargets.js";
|
|
4
|
+
const sortedUnique = (values) => [...new Set(values)].sort(compareCodeUnits);
|
|
5
|
+
function ratio(numerator, denominator, notApplicableReason) {
|
|
6
|
+
if (denominator === 0) {
|
|
7
|
+
return { availability: "not-applicable", numerator: null, denominator: null, value: null, reason: notApplicableReason };
|
|
8
|
+
}
|
|
9
|
+
return { availability: "available", numerator, denominator, value: numerator / denominator, reason: null };
|
|
10
|
+
}
|
|
11
|
+
function unavailableRatio(reason) {
|
|
12
|
+
return { availability: "unavailable", numerator: null, denominator: null, value: null, reason };
|
|
13
|
+
}
|
|
14
|
+
function normalizeExpectedFiles(values) {
|
|
15
|
+
if (!Array.isArray(values))
|
|
16
|
+
return { ok: false, reason: "expected-files-missing" };
|
|
17
|
+
const normalized = [];
|
|
18
|
+
for (const value of values) {
|
|
19
|
+
const path = normalizeRetrievedRepositoryPath(value);
|
|
20
|
+
if (path === null)
|
|
21
|
+
return { ok: false, reason: "unsafe-expected-file-identity" };
|
|
22
|
+
normalized.push(path);
|
|
23
|
+
}
|
|
24
|
+
return { ok: true, values: sortedUnique(normalized) };
|
|
25
|
+
}
|
|
26
|
+
function normalizeExpectedSymbols(values) {
|
|
27
|
+
if (!Array.isArray(values))
|
|
28
|
+
return { ok: false, reason: "expected-symbols-missing" };
|
|
29
|
+
if (values.some((value) => typeof value !== "string" || value.length === 0))
|
|
30
|
+
return { ok: false, reason: "invalid-expected-symbol" };
|
|
31
|
+
return { ok: true, values: sortedUnique(values) };
|
|
32
|
+
}
|
|
33
|
+
function sameSet(left, right) {
|
|
34
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The answer key is the semantic source of relevance. A top-level expectation that is also present must agree
|
|
38
|
+
* with it after normalization; disagreement is never silently reconciled.
|
|
39
|
+
*/
|
|
40
|
+
function resolveExpectation(topLevel, answerKeyValues, hasAnswerKey, normalize, mismatchReason) {
|
|
41
|
+
if (!hasAnswerKey)
|
|
42
|
+
return normalize(topLevel);
|
|
43
|
+
const fromKey = normalize(answerKeyValues);
|
|
44
|
+
if (!fromKey.ok)
|
|
45
|
+
return fromKey;
|
|
46
|
+
if (Array.isArray(topLevel)) {
|
|
47
|
+
const fromTop = normalize(topLevel);
|
|
48
|
+
if (!fromTop.ok)
|
|
49
|
+
return fromTop;
|
|
50
|
+
if (!sameSet(fromTop.values, fromKey.values))
|
|
51
|
+
return { ok: false, reason: mismatchReason };
|
|
52
|
+
}
|
|
53
|
+
return fromKey;
|
|
54
|
+
}
|
|
55
|
+
const expectationStatus = (resolved) => resolved.ok ? { availability: "available", reason: null } : { availability: "unavailable", reason: resolved.reason };
|
|
56
|
+
/** Symbol name -> normalized files it was retrieved from. Symbols without file identity cannot satisfy a file-specific target. */
|
|
57
|
+
function symbolFilesByName(evidence) {
|
|
58
|
+
const byName = new Map();
|
|
59
|
+
for (const symbol of evidence.symbols) {
|
|
60
|
+
if (symbol.file === undefined)
|
|
61
|
+
continue;
|
|
62
|
+
const files = byName.get(symbol.name) ?? new Set();
|
|
63
|
+
files.add(symbol.file);
|
|
64
|
+
byName.set(symbol.name, files);
|
|
65
|
+
}
|
|
66
|
+
return byName;
|
|
67
|
+
}
|
|
68
|
+
function calculateFactCoverage(answerKey, evidence, retrievedFiles) {
|
|
69
|
+
if (!answerKey || typeof answerKey !== "object")
|
|
70
|
+
return { kind: "unavailable", reason: "answer-key-missing" };
|
|
71
|
+
const facts = answerKey.expectedFacts;
|
|
72
|
+
if (!Array.isArray(facts))
|
|
73
|
+
return { kind: "unavailable", reason: "expected-facts-invalid" };
|
|
74
|
+
const factIds = [];
|
|
75
|
+
for (const fact of facts) {
|
|
76
|
+
const id = typeof fact === "object" && fact !== null ? fact.id : undefined;
|
|
77
|
+
if (typeof id !== "string" || id.length === 0 || factIds.includes(id)) {
|
|
78
|
+
return { kind: "unavailable", reason: "expected-facts-invalid" };
|
|
79
|
+
}
|
|
80
|
+
factIds.push(id);
|
|
81
|
+
}
|
|
82
|
+
if (factIds.length === 0)
|
|
83
|
+
return { kind: "not-applicable", reason: "no-expected-facts" };
|
|
84
|
+
const targets = answerKey.expectedContextTargets;
|
|
85
|
+
if (!Array.isArray(targets))
|
|
86
|
+
return { kind: "unavailable", reason: "fact-context-mapping-unavailable" };
|
|
87
|
+
const known = new Set(factIds);
|
|
88
|
+
const targetsByFact = new Map(factIds.map((id) => [id, []]));
|
|
89
|
+
for (const target of targets) {
|
|
90
|
+
if (!hasFactMapping(target))
|
|
91
|
+
continue;
|
|
92
|
+
const required = target.required !== false;
|
|
93
|
+
const interpretation = interpretFactContextTarget(target, known);
|
|
94
|
+
if (!interpretation.ok) {
|
|
95
|
+
// An invalid optional target cannot gate coverage; an invalid required one makes the mapping untrustworthy.
|
|
96
|
+
if (required)
|
|
97
|
+
return { kind: "unavailable", reason: "fact-context-mapping-invalid" };
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!interpretation.target.required)
|
|
101
|
+
continue;
|
|
102
|
+
for (const id of interpretation.target.factIds)
|
|
103
|
+
targetsByFact.get(id)?.push(interpretation.target);
|
|
104
|
+
}
|
|
105
|
+
if (factIds.some((id) => (targetsByFact.get(id) ?? []).length === 0)) {
|
|
106
|
+
return { kind: "unavailable", reason: "fact-context-mapping-unavailable" };
|
|
107
|
+
}
|
|
108
|
+
const symbolFiles = symbolFilesByName(evidence);
|
|
109
|
+
const isCovered = (target) => retrievedFiles.has(target.file) && target.symbols.every((symbol) => symbolFiles.get(symbol)?.has(target.file) === true);
|
|
110
|
+
const covered = factIds.filter((id) => (targetsByFact.get(id) ?? []).every(isCovered));
|
|
111
|
+
const coveredSet = new Set(covered);
|
|
112
|
+
return {
|
|
113
|
+
kind: "computed",
|
|
114
|
+
covered: sortedUnique(covered),
|
|
115
|
+
uncovered: sortedUnique(factIds.filter((id) => !coveredSet.has(id))),
|
|
116
|
+
total: factIds.length
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function evidenceReason(evidence) {
|
|
120
|
+
if (evidence === undefined)
|
|
121
|
+
return "retrieval-evidence-missing";
|
|
122
|
+
if (evidence.availability === "partial")
|
|
123
|
+
return "retrieval-evidence-partial";
|
|
124
|
+
if (evidence.availability === "unavailable")
|
|
125
|
+
return "retrieval-evidence-unavailable";
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Pure per-case retrieval-quality calculation. Formulas are planner-frozen:
|
|
130
|
+
* file precision = |R_file ∩ E_file| / |R_file|; file recall = |R_file ∩ E_file| / |E_file|;
|
|
131
|
+
* irrelevant context ratio = |R_file - E_file| / |R_file|; symbol precision/recall use the same set formulas
|
|
132
|
+
* over exact unique symbol names; fact coverage = covered facts / expected facts, unweighted and derived only
|
|
133
|
+
* from explicit `factIds` target mappings. No input is mutated and nothing is read from disk or text.
|
|
134
|
+
* Unavailable or not-applicable results are never converted to zero.
|
|
135
|
+
*/
|
|
136
|
+
export function calculateRetrievalQualityMetrics(input) {
|
|
137
|
+
const evidence = input.retrieval.retrievalEvidence;
|
|
138
|
+
return calculateRetrievalQualityMetricsFromIdentityEvidence({
|
|
139
|
+
evaluationCase: input.evaluationCase,
|
|
140
|
+
retrieval: {
|
|
141
|
+
identityEvidence: evidence === undefined
|
|
142
|
+
? undefined
|
|
143
|
+
: {
|
|
144
|
+
availability: evidence.availability,
|
|
145
|
+
...(evidence.availabilityReason !== undefined ? { availabilityReason: evidence.availabilityReason } : {}),
|
|
146
|
+
files: evidence.files.map((file) => file.path),
|
|
147
|
+
symbols: evidence.symbols.map((symbol) => ({
|
|
148
|
+
name: symbol.name,
|
|
149
|
+
...(symbol.file !== undefined ? { file: symbol.file } : {})
|
|
150
|
+
}))
|
|
151
|
+
},
|
|
152
|
+
totalEstimatedTokens: input.retrieval.totalEstimatedTokens,
|
|
153
|
+
tokenCountMethod: input.retrieval.tokenCountMethod
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Single mathematical owner for retrieval-quality calculation. Applies the v0.8.0 formulas and availability
|
|
159
|
+
* semantics to already-normalized identity evidence, so any evidence contract can be scored identically.
|
|
160
|
+
*/
|
|
161
|
+
export function calculateRetrievalQualityMetricsFromIdentityEvidence(input) {
|
|
162
|
+
const { evaluationCase, retrieval } = input;
|
|
163
|
+
const evidence = retrieval.identityEvidence;
|
|
164
|
+
const evidenceBlocked = evidenceReason(evidence);
|
|
165
|
+
const answerKey = evaluationCase.answerKey;
|
|
166
|
+
const hasAnswerKey = answerKey !== undefined && answerKey !== null && typeof answerKey === "object";
|
|
167
|
+
const expectedFiles = resolveExpectation(evaluationCase.expectedFiles, hasAnswerKey ? answerKey.expectedFiles : undefined, hasAnswerKey, normalizeExpectedFiles, "expected-files-answer-key-mismatch");
|
|
168
|
+
const expectedSymbols = resolveExpectation(evaluationCase.expectedSymbols, hasAnswerKey ? answerKey.expectedSymbols : undefined, hasAnswerKey, normalizeExpectedSymbols, "expected-symbols-answer-key-mismatch");
|
|
169
|
+
const usable = evidenceBlocked === null && evidence !== undefined ? evidence : undefined;
|
|
170
|
+
const retrievedFiles = usable ? sortedUnique(usable.files) : undefined;
|
|
171
|
+
const retrievedSymbols = usable ? sortedUnique(usable.symbols.map((symbol) => symbol.name)) : undefined;
|
|
172
|
+
// ---- files
|
|
173
|
+
let file = {
|
|
174
|
+
relevantRetrievedFiles: null,
|
|
175
|
+
irrelevantRetrievedFiles: null,
|
|
176
|
+
missedFiles: null,
|
|
177
|
+
missedFileCount: null,
|
|
178
|
+
precision: unavailableRatio(evidenceBlocked ?? (expectedFiles.ok ? "unavailable" : expectedFiles.reason)),
|
|
179
|
+
recall: unavailableRatio(evidenceBlocked ?? (expectedFiles.ok ? "unavailable" : expectedFiles.reason))
|
|
180
|
+
};
|
|
181
|
+
let irrelevantContextRatio = unavailableRatio(evidenceBlocked ?? (expectedFiles.ok ? "unavailable" : expectedFiles.reason));
|
|
182
|
+
if (retrievedFiles !== undefined && expectedFiles.ok) {
|
|
183
|
+
const expected = new Set(expectedFiles.values);
|
|
184
|
+
const retrieved = new Set(retrievedFiles);
|
|
185
|
+
const relevant = retrievedFiles.filter((path) => expected.has(path));
|
|
186
|
+
const irrelevant = retrievedFiles.filter((path) => !expected.has(path));
|
|
187
|
+
const missed = expectedFiles.values.filter((path) => !retrieved.has(path));
|
|
188
|
+
file = {
|
|
189
|
+
relevantRetrievedFiles: relevant,
|
|
190
|
+
irrelevantRetrievedFiles: irrelevant,
|
|
191
|
+
missedFiles: missed,
|
|
192
|
+
missedFileCount: missed.length,
|
|
193
|
+
precision: ratio(relevant.length, retrievedFiles.length, "no-retrieved-files"),
|
|
194
|
+
recall: ratio(relevant.length, expectedFiles.values.length, "no-expected-files")
|
|
195
|
+
};
|
|
196
|
+
irrelevantContextRatio = ratio(irrelevant.length, retrievedFiles.length, "no-retrieved-files");
|
|
197
|
+
}
|
|
198
|
+
// ---- symbols (exact unique names)
|
|
199
|
+
let symbol = {
|
|
200
|
+
relevantRetrievedSymbols: null,
|
|
201
|
+
irrelevantRetrievedSymbols: null,
|
|
202
|
+
missedSymbols: null,
|
|
203
|
+
missedSymbolCount: null,
|
|
204
|
+
precision: unavailableRatio(evidenceBlocked ?? (expectedSymbols.ok ? "unavailable" : expectedSymbols.reason)),
|
|
205
|
+
recall: unavailableRatio(evidenceBlocked ?? (expectedSymbols.ok ? "unavailable" : expectedSymbols.reason))
|
|
206
|
+
};
|
|
207
|
+
if (retrievedSymbols !== undefined && expectedSymbols.ok) {
|
|
208
|
+
const expected = new Set(expectedSymbols.values);
|
|
209
|
+
const retrieved = new Set(retrievedSymbols);
|
|
210
|
+
const relevant = retrievedSymbols.filter((name) => expected.has(name));
|
|
211
|
+
const missed = expectedSymbols.values.filter((name) => !retrieved.has(name));
|
|
212
|
+
symbol = {
|
|
213
|
+
relevantRetrievedSymbols: relevant,
|
|
214
|
+
irrelevantRetrievedSymbols: retrievedSymbols.filter((name) => !expected.has(name)),
|
|
215
|
+
missedSymbols: missed,
|
|
216
|
+
missedSymbolCount: missed.length,
|
|
217
|
+
precision: ratio(relevant.length, retrievedSymbols.length, "no-retrieved-symbols"),
|
|
218
|
+
recall: ratio(relevant.length, expectedSymbols.values.length, "no-expected-symbols")
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
// ---- facts
|
|
222
|
+
let fact = {
|
|
223
|
+
coveredFactIds: null,
|
|
224
|
+
uncoveredFactIds: null,
|
|
225
|
+
uncoveredFactCount: null,
|
|
226
|
+
coverage: unavailableRatio(evidenceBlocked ?? "unavailable")
|
|
227
|
+
};
|
|
228
|
+
if (usable !== undefined && retrievedFiles !== undefined) {
|
|
229
|
+
const outcome = calculateFactCoverage(answerKey, usable, new Set(retrievedFiles));
|
|
230
|
+
if (outcome.kind === "computed") {
|
|
231
|
+
fact = {
|
|
232
|
+
coveredFactIds: outcome.covered,
|
|
233
|
+
uncoveredFactIds: outcome.uncovered,
|
|
234
|
+
uncoveredFactCount: outcome.uncovered.length,
|
|
235
|
+
coverage: ratio(outcome.covered.length, outcome.total, "no-expected-facts")
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
fact = { ...fact, coverage: outcome.kind === "unavailable" ? unavailableRatio(outcome.reason) : { availability: "not-applicable", numerator: null, denominator: null, value: null, reason: outcome.reason } };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const tokensValid = typeof retrieval.totalEstimatedTokens === "number" && Number.isFinite(retrieval.totalEstimatedTokens) && retrieval.totalEstimatedTokens >= 0;
|
|
243
|
+
return {
|
|
244
|
+
schemaVersion: "retrieval-quality-metrics-v1",
|
|
245
|
+
caseId: evaluationCase.id,
|
|
246
|
+
evidence: {
|
|
247
|
+
availability: evidence === undefined ? "missing" : evidence.availability,
|
|
248
|
+
reason: evidence === undefined ? "retrieval-evidence-missing" : (evidence.availabilityReason ?? null)
|
|
249
|
+
},
|
|
250
|
+
expectations: { files: expectationStatus(expectedFiles), symbols: expectationStatus(expectedSymbols) },
|
|
251
|
+
file,
|
|
252
|
+
symbol,
|
|
253
|
+
fact,
|
|
254
|
+
irrelevantContextRatio,
|
|
255
|
+
retrievedTokenCount: tokensValid ? retrieval.totalEstimatedTokens : null,
|
|
256
|
+
tokenCountMethod: tokensValid && typeof retrieval.tokenCountMethod === "string" ? retrieval.tokenCountMethod : null
|
|
257
|
+
};
|
|
258
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized retrieval-evidence contract (V1).
|
|
3
|
+
*
|
|
4
|
+
* Describes which files and symbols the existing my-dev-kit search -> lookup -> slice -> source
|
|
5
|
+
* lifecycle actually surfaced. It is observation only: it carries no relevance judgement and no
|
|
6
|
+
* score. It deliberately contains no source text, stdout/stderr bodies, or machine-local paths.
|
|
7
|
+
*/
|
|
8
|
+
/** Retrieval command families in fixed lifecycle order. */
|
|
9
|
+
export const RETRIEVAL_COMMAND_FAMILIES = ["search", "lookup", "slice", "source"];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const RETRIEVAL_QUERY_STRATEGY_IDS = [
|
|
2
|
+
"keyword-search",
|
|
3
|
+
"symbol-lookup",
|
|
4
|
+
"graph-neighborhood",
|
|
5
|
+
"source-slice",
|
|
6
|
+
"data-model-graph",
|
|
7
|
+
"model-view-lineage",
|
|
8
|
+
"combined-graph-guided"
|
|
9
|
+
];
|
|
10
|
+
export const CORE_GRAPH_RETRIEVAL_QUERY_STRATEGY_IDS = [
|
|
11
|
+
"keyword-search",
|
|
12
|
+
"symbol-lookup",
|
|
13
|
+
"graph-neighborhood",
|
|
14
|
+
"source-slice",
|
|
15
|
+
"combined-graph-guided"
|
|
16
|
+
];
|
|
17
|
+
export function isCoreGraphRetrievalQueryStrategyId(value) {
|
|
18
|
+
return CORE_GRAPH_RETRIEVAL_QUERY_STRATEGY_IDS.includes(value);
|
|
19
|
+
}
|
|
20
|
+
export const SEMANTIC_RETRIEVAL_QUERY_STRATEGY_IDS = [
|
|
21
|
+
"data-model-graph",
|
|
22
|
+
"model-view-lineage"
|
|
23
|
+
];
|
|
24
|
+
export function isSemanticRetrievalQueryStrategyId(value) {
|
|
25
|
+
return SEMANTIC_RETRIEVAL_QUERY_STRATEGY_IDS.includes(value);
|
|
26
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strategy-neutral retrieval identity evidence (V1).
|
|
3
|
+
*
|
|
4
|
+
* Additive to `retrieval-evidence-v1`, which is frozen to the search/lookup/slice/source command
|
|
5
|
+
* families. This contract records only which file and symbol identities a query strategy exposed and
|
|
6
|
+
* which steps ran. It carries no relevance judgement, score, answer-key data, source text,
|
|
7
|
+
* stdout/stderr, or machine-local paths.
|
|
8
|
+
*/
|
|
9
|
+
import { compareCodeUnits } from "./indexSnapshot.js";
|
|
10
|
+
/**
|
|
11
|
+
* Deduplicates and canonically orders identities. Files are keyed by path; symbols by `nodeId` when
|
|
12
|
+
* present, otherwise by `file + NUL + name`. Ordering is code-unit order, never locale-sensitive.
|
|
13
|
+
*/
|
|
14
|
+
export function canonicalizeQueryStrategyIdentities(files, symbols) {
|
|
15
|
+
const fileByPath = new Map();
|
|
16
|
+
for (const file of files) {
|
|
17
|
+
if (!fileByPath.has(file.path))
|
|
18
|
+
fileByPath.set(file.path, { path: file.path });
|
|
19
|
+
}
|
|
20
|
+
const symbolByKey = new Map();
|
|
21
|
+
for (const symbol of symbols) {
|
|
22
|
+
const key = symbol.nodeId !== null ? symbol.nodeId : `${symbol.file ?? ""}\0${symbol.name}`;
|
|
23
|
+
if (!symbolByKey.has(key)) {
|
|
24
|
+
symbolByKey.set(key, { name: symbol.name, nodeId: symbol.nodeId, file: symbol.file });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
files: [...fileByPath.values()].sort((a, b) => compareCodeUnits(a.path, b.path)),
|
|
29
|
+
symbols: [...symbolByKey.values()].sort((a, b) => compareCodeUnits(a.file ?? "", b.file ?? "") ||
|
|
30
|
+
compareCodeUnits(a.name, b.name) ||
|
|
31
|
+
compareCodeUnits(a.nodeId ?? "", b.nodeId ?? ""))
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Converts an already-normalized `retrieval-evidence-v1` observation into the strategy-neutral
|
|
36
|
+
* contract. It recalculates no identity, parses no stdout, and does not reinterpret availability.
|
|
37
|
+
*/
|
|
38
|
+
export function buildQueryStrategyEvidenceFromRetrievalEvidence(strategyId, evidence) {
|
|
39
|
+
return {
|
|
40
|
+
schemaVersion: "retrieval-query-strategy-evidence-v1",
|
|
41
|
+
strategyId,
|
|
42
|
+
availability: evidence.availability,
|
|
43
|
+
availabilityReason: evidence.availabilityReason ?? null,
|
|
44
|
+
files: evidence.files.map(({ path }) => ({ path })),
|
|
45
|
+
symbols: evidence.symbols.map((symbol) => ({
|
|
46
|
+
name: symbol.name,
|
|
47
|
+
nodeId: symbol.nodeId ?? null,
|
|
48
|
+
file: symbol.file ?? null
|
|
49
|
+
})),
|
|
50
|
+
steps: evidence.commands.map((command) => ({
|
|
51
|
+
kind: command.family,
|
|
52
|
+
succeeded: command.succeeded,
|
|
53
|
+
evidenceAvailable: (command.parseState === "parsed" || command.parseState === "selection-attributed") &&
|
|
54
|
+
command.rejectedIdentityCount === 0,
|
|
55
|
+
reason: command.reason ?? null
|
|
56
|
+
}))
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -2,6 +2,8 @@ import path from "node:path";
|
|
|
2
2
|
import { countEstimatedTokens, countTextChars, tokenCountMethod } from "../core/countTokens.js";
|
|
3
3
|
import { runMeasuredCommand } from "../core/runMeasuredCommand.js";
|
|
4
4
|
import { interpretToolVersionOutput } from "./indexSnapshot.js";
|
|
5
|
+
import { buildRetrievalEvidenceFromCommands } from "./retrievalQuality/buildRetrievalEvidence.js";
|
|
6
|
+
import { buildQueryStrategyEvidenceFromRetrievalEvidence } from "./retrievalQueryStrategyEvidence.js";
|
|
5
7
|
const VERSION_PROBE_TIMEOUT_MS = 30_000;
|
|
6
8
|
function parseJsonIfPossible(text) {
|
|
7
9
|
try {
|
|
@@ -52,6 +54,7 @@ function skippedRetrieval(caseId, warnings, commands, durationMs) {
|
|
|
52
54
|
contextText: "",
|
|
53
55
|
filesRead: [],
|
|
54
56
|
commands,
|
|
57
|
+
retrievalEvidence: buildRetrievalEvidenceFromCommands(commands),
|
|
55
58
|
durationMs
|
|
56
59
|
};
|
|
57
60
|
}
|
|
@@ -237,11 +240,31 @@ export async function probeMyDevKitVersion(options) {
|
|
|
237
240
|
});
|
|
238
241
|
}
|
|
239
242
|
}
|
|
243
|
+
function selectRetrievalStrategyContextText(strategyId, outputs) {
|
|
244
|
+
const ordered = strategyId === "keyword-search"
|
|
245
|
+
? [outputs.search]
|
|
246
|
+
: strategyId === "symbol-lookup"
|
|
247
|
+
? [outputs.lookup, outputs.search]
|
|
248
|
+
: strategyId === "graph-neighborhood"
|
|
249
|
+
? [outputs.slice, outputs.search]
|
|
250
|
+
: strategyId === "source-slice"
|
|
251
|
+
? [outputs.source, outputs.search]
|
|
252
|
+
: [outputs.source, outputs.slice, outputs.lookup, outputs.search];
|
|
253
|
+
return ordered.find((text) => text && text.trim().length > 0) ?? "";
|
|
254
|
+
}
|
|
255
|
+
const STRATEGY_DOWNSTREAM_COMMANDS = {
|
|
256
|
+
"keyword-search": [],
|
|
257
|
+
"symbol-lookup": ["lookup"],
|
|
258
|
+
"graph-neighborhood": ["slice"],
|
|
259
|
+
"source-slice": ["source"],
|
|
260
|
+
"combined-graph-guided": ["lookup", "slice", "source"]
|
|
261
|
+
};
|
|
240
262
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
263
|
+
* Executes exactly one core retrieval query strategy against an already prepared index. Every
|
|
264
|
+
* strategy starts with `search` and expands at most the first candidate. Never invokes `index`;
|
|
265
|
+
* `durationMs` covers retrieval only.
|
|
243
266
|
*/
|
|
244
|
-
export async function
|
|
267
|
+
export async function runMyDevKitRetrievalStrategyFromIndex(options) {
|
|
245
268
|
const started = Date.now();
|
|
246
269
|
const warnings = [];
|
|
247
270
|
const commands = [];
|
|
@@ -272,58 +295,37 @@ export async function runMyDevKitRetrievalFromIndex(options) {
|
|
|
272
295
|
const selectedNodeId = candidate.nodeId;
|
|
273
296
|
const selectedFile = candidate.file;
|
|
274
297
|
const selectedSymbol = candidate.symbol;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
extraArgs: ["slice", "--index", indexDir, "--node", selectedNodeId, "--json"]
|
|
299
|
-
});
|
|
300
|
-
commands.push(sliceCommand);
|
|
301
|
-
if (sliceCommand.ok) {
|
|
302
|
-
sliceOutput = sliceCommand.stdout;
|
|
303
|
-
}
|
|
304
|
-
else {
|
|
305
|
-
warnings.push("my-dev-kit slice command failed.");
|
|
306
|
-
}
|
|
307
|
-
const sourceCommand = await runMeasuredCommand({
|
|
308
|
-
commandId: "source",
|
|
309
|
-
commandString: options.kitCommand,
|
|
310
|
-
cwd: process.cwd(),
|
|
311
|
-
outDir: commandsDir,
|
|
312
|
-
extraArgs: ["source", "--index", indexDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
|
|
313
|
-
});
|
|
314
|
-
commands.push(sourceCommand);
|
|
315
|
-
if (sourceCommand.ok) {
|
|
316
|
-
sourceOutput = sourceCommand.stdout;
|
|
298
|
+
const outputs = { search: searchCommand.stdout, lookup: "", slice: "", source: "" };
|
|
299
|
+
const downstream = STRATEGY_DOWNSTREAM_COMMANDS[options.strategyId];
|
|
300
|
+
if (downstream.length > 0) {
|
|
301
|
+
if (selectedNodeId) {
|
|
302
|
+
for (const commandId of downstream) {
|
|
303
|
+
const extraArgs = commandId === "source"
|
|
304
|
+
? ["source", "--index", indexDir, "--node", selectedNodeId, "--max-lines", "160", "--format", "numbered"]
|
|
305
|
+
: [commandId, "--index", indexDir, "--node", selectedNodeId, "--json"];
|
|
306
|
+
const command = await runMeasuredCommand({
|
|
307
|
+
commandId,
|
|
308
|
+
commandString: options.kitCommand,
|
|
309
|
+
cwd: process.cwd(),
|
|
310
|
+
outDir: commandsDir,
|
|
311
|
+
extraArgs
|
|
312
|
+
});
|
|
313
|
+
commands.push(command);
|
|
314
|
+
if (command.ok) {
|
|
315
|
+
outputs[commandId] = command.stdout;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
warnings.push(`my-dev-kit ${commandId} command failed.`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
317
321
|
}
|
|
318
322
|
else {
|
|
319
|
-
warnings.push("my-dev-kit
|
|
323
|
+
warnings.push("No my-dev-kit node id was available after search.");
|
|
320
324
|
}
|
|
321
325
|
}
|
|
322
|
-
|
|
323
|
-
warnings.push("No my-dev-kit node id was available after search.");
|
|
324
|
-
}
|
|
325
|
-
const contextText = [sourceOutput, sliceOutput, lookupOutput, searchCommand.stdout].find((text) => text && text.trim().length > 0) ?? "";
|
|
326
|
+
const contextText = selectRetrievalStrategyContextText(options.strategyId, outputs);
|
|
326
327
|
const filesRead = selectedFile ? [selectedFile] : [];
|
|
328
|
+
const retrievalEvidence = buildRetrievalEvidenceFromCommands(commands, { nodeId: selectedNodeId, file: selectedFile });
|
|
327
329
|
return {
|
|
328
330
|
caseId: options.evaluationCase.id,
|
|
329
331
|
skipped: contextText.length === 0,
|
|
@@ -337,9 +339,18 @@ export async function runMyDevKitRetrievalFromIndex(options) {
|
|
|
337
339
|
selectedNodeId,
|
|
338
340
|
selectedFile,
|
|
339
341
|
selectedSymbol,
|
|
342
|
+
retrievalEvidence,
|
|
343
|
+
queryStrategyEvidence: buildQueryStrategyEvidenceFromRetrievalEvidence(options.strategyId, retrievalEvidence),
|
|
340
344
|
durationMs: Date.now() - started
|
|
341
345
|
};
|
|
342
346
|
}
|
|
347
|
+
/**
|
|
348
|
+
* v0.8.0 compatibility entrypoint: the historical search -> lookup -> slice -> source lifecycle,
|
|
349
|
+
* delegating to the `combined-graph-guided` strategy. Never invokes `index`.
|
|
350
|
+
*/
|
|
351
|
+
export async function runMyDevKitRetrievalFromIndex(options) {
|
|
352
|
+
return runMyDevKitRetrievalStrategyFromIndex({ ...options, strategyId: "combined-graph-guided" });
|
|
353
|
+
}
|
|
343
354
|
/**
|
|
344
355
|
* Legacy per-case lifecycle: builds a case-specific index and retrieves from it. `commands`
|
|
345
356
|
* starts with the index command and `durationMs` covers the whole lifecycle including indexing.
|