@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.
Files changed (57) hide show
  1. package/CHANGELOG.md +33 -1
  2. package/README.md +9 -3
  3. package/benchmarks/contracts/warm-index-benchmark-cases.json +330 -0
  4. package/dist/src/cli/help.js +32 -1
  5. package/dist/src/commands/runExperimentDescribeCommand.js +16 -1
  6. package/dist/src/commands/runExperimentRunCommand.js +181 -4
  7. package/dist/src/evaluation/benchmarkMetadata.js +13 -0
  8. package/dist/src/evaluation/retrievalQuality/buildRetrievalEvidence.js +292 -0
  9. package/dist/src/evaluation/retrievalQuality/corpusCompleteness.js +122 -0
  10. package/dist/src/evaluation/retrievalQuality/factContextTargets.js +44 -0
  11. package/dist/src/evaluation/retrievalQuality/index.js +5 -0
  12. package/dist/src/evaluation/retrievalQuality/metrics.js +258 -0
  13. package/dist/src/evaluation/retrievalQuality/types.js +9 -0
  14. package/dist/src/evaluation/retrievalQueryStrategies.js +26 -0
  15. package/dist/src/evaluation/retrievalQueryStrategyEvidence.js +58 -0
  16. package/dist/src/evaluation/runMyDevKitRetrieval.js +61 -50
  17. package/dist/src/evaluation/runSemanticRetrievalStrategy.js +303 -0
  18. package/dist/src/experiments/defaultRegistry.js +4 -0
  19. package/dist/src/experiments/plugins/contextWindowScaling/localSubjectPrivacy.js +2 -0
  20. package/dist/src/experiments/plugins/index.js +2 -0
  21. package/dist/src/experiments/plugins/retrievalPrecisionRecall/config.js +39 -0
  22. package/dist/src/experiments/plugins/retrievalPrecisionRecall/execution.js +152 -0
  23. package/dist/src/experiments/plugins/retrievalPrecisionRecall/executionArtifact.js +15 -0
  24. package/dist/src/experiments/plugins/retrievalPrecisionRecall/index.js +9 -0
  25. package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectExecution.js +139 -0
  26. package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectPrivacy.js +119 -0
  27. package/dist/src/experiments/plugins/retrievalPrecisionRecall/metadata.js +21 -0
  28. package/dist/src/experiments/plugins/retrievalPrecisionRecall/metrics.js +158 -0
  29. package/dist/src/experiments/plugins/retrievalPrecisionRecall/plugin.js +253 -0
  30. package/dist/src/experiments/plugins/retrievalPrecisionRecall/types.js +8 -0
  31. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysis.js +172 -0
  32. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisArtifact.js +28 -0
  33. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/analysisTypes.js +2 -0
  34. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/config.js +39 -0
  35. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/execution.js +225 -0
  36. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/executionArtifact.js +15 -0
  37. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/index.js +12 -0
  38. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectExecution.js +124 -0
  39. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/localSubjectPrivacy.js +176 -0
  40. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metadata.js +51 -0
  41. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/metrics.js +60 -0
  42. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/plugin.js +292 -0
  43. package/dist/src/experiments/plugins/retrievalQueryStrategyComparison/types.js +1 -0
  44. package/dist/src/report/experiments/buildPluginExperimentReport.js +44 -2
  45. package/dist/src/report/experiments/buildRetrievalPrecisionRecallReport.js +83 -0
  46. package/dist/src/report/experiments/buildRetrievalQueryStrategyComparisonReport.js +91 -0
  47. package/dist/src/report/experiments/index.js +8 -0
  48. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +6 -0
  49. package/dist/src/report/experiments/renderPluginExperimentReportText.js +18 -0
  50. package/dist/src/report/experiments/renderRetrievalPrecisionRecallHtml.js +96 -0
  51. package/dist/src/report/experiments/renderRetrievalPrecisionRecallText.js +121 -0
  52. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonHtml.js +96 -0
  53. package/dist/src/report/experiments/renderRetrievalQueryStrategyComparisonText.js +58 -0
  54. package/dist/src/report/experiments/retrievalPrecisionRecallReportModel.js +3 -0
  55. package/dist/src/report/experiments/retrievalQueryStrategyComparisonReportModel.js +1 -0
  56. package/docs/METRICS.md +152 -2
  57. package/package.json +1 -1
@@ -0,0 +1,139 @@
1
+ import path from "node:path";
2
+ import { relativeWithinRoot } from "../../../core/pathSafety.js";
3
+ import { normalizeRetrievedRepositoryPath, validateRetrievalPrecisionRecallCase } from "../../../evaluation/retrievalQuality/index.js";
4
+ import { buildMyDevKitIndex, runMyDevKitRetrievalFromIndex } from "../../../evaluation/runMyDevKitRetrieval.js";
5
+ import { captureTargetSnapshot, compareTargetSnapshots } from "../../../evaluation/targetImmutability/index.js";
6
+ import { LocalSubjectExecutionError } from "../contextWindowScaling/localSubjectErrors.js";
7
+ import { deriveGuidedIndexExclusions } from "../contextWindowScaling/localSubjectExclusions.js";
8
+ import { assertWorkRootOutsideTarget, createPrivateScratch, defaultPrivateScratchIo, removePrivateScratch } from "../contextWindowScaling/localSubjectScratch.js";
9
+ import { buildCaseEvidenceFromRetrieval } from "./execution.js";
10
+ function toRepositoryRelative(repositoryRoot, file) {
11
+ if (path.isAbsolute(file)) {
12
+ try {
13
+ return relativeWithinRoot(repositoryRoot, file);
14
+ }
15
+ catch {
16
+ return file.replace(/\\/g, "/");
17
+ }
18
+ }
19
+ return normalizeRetrievedRepositoryPath(file) ?? file.replace(/\\/g, "/");
20
+ }
21
+ /**
22
+ * Counts distinct retrieved identities outside the eligible subject universe. It inspects every file identity the
23
+ * retrieval exposes: evidence files, evidence symbol files, the selected file, and the files read. Only the count
24
+ * leaves this function; an offending identity is never named.
25
+ */
26
+ export function countRetrievedIdentitiesOutsideEligibleUniverse(retrieval, eligibleFiles, repositoryRoot) {
27
+ const identities = new Set();
28
+ for (const file of retrieval.retrievalEvidence?.files ?? [])
29
+ identities.add(toRepositoryRelative(repositoryRoot, file.path));
30
+ for (const symbol of retrieval.retrievalEvidence?.symbols ?? []) {
31
+ if (symbol.file !== undefined)
32
+ identities.add(toRepositoryRelative(repositoryRoot, symbol.file));
33
+ }
34
+ if (retrieval.selectedFile)
35
+ identities.add(toRepositoryRelative(repositoryRoot, retrieval.selectedFile));
36
+ for (const file of retrieval.filesRead)
37
+ identities.add(toRepositoryRelative(repositoryRoot, file));
38
+ return [...identities].filter((identity) => !eligibleFiles.has(identity)).length;
39
+ }
40
+ /**
41
+ * Internal programmatic seam: runs a loaded local subject through the existing index and retrieval owners, one
42
+ * private index per configured case (each case keeps its own exact source roots), with a private scratch outside the
43
+ * target and pre/post target-immutability enforcement. Order: validate work root, derive exclusions, validate
44
+ * ground truth, capture before, create scratch, execute cases, capture after, compare, remove scratch, then decide.
45
+ * Success is returned only when every safety gate passed. A mutated target is never reverted.
46
+ *
47
+ * Every issue message is safe by construction: fixed text plus logical case ids and counts, never a file name,
48
+ * symbol, fact id, path, or caught error text.
49
+ */
50
+ export async function executeLocalRepositorySubjectRetrievalPrecisionRecall(args) {
51
+ const { subject } = args;
52
+ const buildIndex = args.dependencies?.buildIndex ?? buildMyDevKitIndex;
53
+ const retrieveFromIndex = args.dependencies?.retrieveFromIndex ?? runMyDevKitRetrievalFromIndex;
54
+ await assertWorkRootOutsideTarget(args.workRoot, subject.repositoryRoot);
55
+ const guidedExclusions = deriveGuidedIndexExclusions({
56
+ eligibleFiles: subject.eligibleFiles,
57
+ excludedFiles: [...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles]
58
+ });
59
+ const invalidCaseIds = subject.evaluationCases.filter((evaluationCase) => validateRetrievalPrecisionRecallCase(evaluationCase).length > 0).map((evaluationCase) => evaluationCase.id);
60
+ if (invalidCaseIds.length > 0) {
61
+ throw new LocalSubjectExecutionError([
62
+ { code: "GROUND_TRUTH_INVALID", message: `retrieval ground truth is incomplete for case(s): ${invalidCaseIds.join(", ")}.` }
63
+ ]);
64
+ }
65
+ const snapshotConfig = { targetRootPath: subject.repositoryRoot, relativeFilePaths: subject.eligibleFiles };
66
+ const snapshotOptions = { externalLocalSafe: { maxHashedFileBytes: subject.manifest.safetyPolicy.maxFileBytes } };
67
+ const before = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
68
+ if (!before.ok) {
69
+ throw new LocalSubjectExecutionError([{ code: "BEFORE_SNAPSHOT_FAILED", message: `target snapshot before execution failed (${before.code}).` }]);
70
+ }
71
+ const scratch = await createPrivateScratch(args.workRoot, subject.repositoryRoot);
72
+ const issues = [];
73
+ const caseEvidence = [];
74
+ const eligible = new Set(subject.eligibleFiles);
75
+ for (const [position, evaluationCase] of subject.evaluationCases.entries()) {
76
+ // Short, fixed segments: private case or repository names never become directory names.
77
+ const indexDir = path.join(scratch.path, `i${position + 1}`);
78
+ const commandsDir = path.join(scratch.path, `c${position + 1}`);
79
+ try {
80
+ // requireKit: false keeps a failed index as a result we can classify instead of an arbitrary thrown message.
81
+ const build = await buildIndex({
82
+ target: { absoluteTargetRoot: evaluationCase.absoluteTargetRoot, sourceRoots: [...evaluationCase.sourceRoots] },
83
+ kitCommand: args.kitCommand,
84
+ indexDir,
85
+ commandsDir: path.join(commandsDir, "index"),
86
+ requireKit: false,
87
+ excludePaths: guidedExclusions
88
+ });
89
+ if (!build.ok) {
90
+ issues.push({ code: "EXECUTION_FAILED", message: `case ${evaluationCase.id}: the my-dev-kit index could not be prepared.` });
91
+ break;
92
+ }
93
+ const retrieval = await retrieveFromIndex({
94
+ evaluationCase,
95
+ kitCommand: args.kitCommand,
96
+ indexDir,
97
+ commandsDir: path.join(commandsDir, "retrieval"),
98
+ requireKit: false
99
+ });
100
+ const outside = countRetrievedIdentitiesOutsideEligibleUniverse(retrieval, eligible, subject.repositoryRoot);
101
+ if (outside > 0) {
102
+ issues.push({
103
+ code: "RETRIEVAL_OUTSIDE_ELIGIBLE_UNIVERSE",
104
+ message: `case ${evaluationCase.id}: retrieval exposed ${outside} file identit${outside === 1 ? "y" : "ies"} outside the eligible subject universe.`
105
+ });
106
+ break;
107
+ }
108
+ caseEvidence.push(buildCaseEvidenceFromRetrieval(evaluationCase, retrieval));
109
+ }
110
+ catch {
111
+ issues.push({ code: "EXECUTION_FAILED", message: `case ${evaluationCase.id}: execution threw before completing (details withheld).` });
112
+ break;
113
+ }
114
+ }
115
+ let comparison = null;
116
+ const after = await captureTargetSnapshot(snapshotConfig, snapshotOptions);
117
+ if (!after.ok) {
118
+ issues.push({ code: "AFTER_SNAPSHOT_FAILED", message: `target snapshot after execution failed (${after.code}).` });
119
+ }
120
+ else {
121
+ comparison = compareTargetSnapshots(before.snapshot, after.snapshot);
122
+ if (comparison.status === "mutated") {
123
+ issues.push({ code: "TARGET_MUTATED", message: "the target changed during execution; it was not restored." });
124
+ }
125
+ }
126
+ const cleanupFailure = await removePrivateScratch(scratch, args.scratchIo ?? defaultPrivateScratchIo);
127
+ if (cleanupFailure) {
128
+ issues.push({ code: "SCRATCH_CLEANUP_FAILED", message: "the private scratch could not be removed." });
129
+ }
130
+ if (issues.length > 0 || !comparison) {
131
+ throw new LocalSubjectExecutionError(issues.length > 0 ? issues : [{ code: "EXECUTION_FAILED", message: "execution did not complete." }], {
132
+ immutability: comparison
133
+ });
134
+ }
135
+ return {
136
+ caseEvidence,
137
+ immutability: { status: "unchanged", preExistingGitStatusEntryCount: comparison.preExistingGitStatusEntryCount, newMutationCount: 0 }
138
+ };
139
+ }
@@ -0,0 +1,119 @@
1
+ import { redactedFileList } from "../contextWindowScaling/localSubjectPrivacy.js";
2
+ /** Single fixed placeholder for a withheld case title. */
3
+ export const REDACTED_CASE_TITLE = "<redacted case title>";
4
+ /** The one V1 marker stamped on every externally projected case, so placeholders are never mistaken for real values. */
5
+ export const RETRIEVAL_IDENTITY_REDACTION = {
6
+ fileIdentities: "redacted",
7
+ symbolIdentities: "redacted",
8
+ factIdentities: "redacted",
9
+ warningText: "redacted",
10
+ caseTitle: "redacted"
11
+ };
12
+ /** One opaque placeholder per identity: counts stay exact; null stays null; an empty list stays empty. */
13
+ function placeholders(kind, count) {
14
+ return Array.from({ length: count }, (_, index) => `<redacted ${kind} ${index + 1}>`);
15
+ }
16
+ const filePlaceholders = (list) => (list === null ? null : redactedFileList(list.length));
17
+ const symbolPlaceholders = (list) => (list === null ? null : placeholders("symbol", list.length));
18
+ const factPlaceholders = (list) => (list === null ? null : placeholders("fact", list.length));
19
+ /** Plugin-owned text per error code; a caught Error.message is never copied into durable external evidence. */
20
+ const FIXED_ERROR_TEXT = {
21
+ "ground-truth-invalid": "The case ground truth was invalid.",
22
+ "project-group-inconsistent": "The case source configuration was inconsistent.",
23
+ "project-index-failed": "The my-dev-kit index could not be prepared.",
24
+ "retrieval-failed": "The retrieval did not complete."
25
+ };
26
+ /** Only identity lists change; every number, availability and fixed-vocabulary reason is carried through untouched. */
27
+ function projectQuality(quality) {
28
+ return {
29
+ ...quality,
30
+ file: {
31
+ ...quality.file,
32
+ relevantRetrievedFiles: filePlaceholders(quality.file.relevantRetrievedFiles),
33
+ irrelevantRetrievedFiles: filePlaceholders(quality.file.irrelevantRetrievedFiles),
34
+ missedFiles: filePlaceholders(quality.file.missedFiles)
35
+ },
36
+ symbol: {
37
+ ...quality.symbol,
38
+ relevantRetrievedSymbols: symbolPlaceholders(quality.symbol.relevantRetrievedSymbols),
39
+ irrelevantRetrievedSymbols: symbolPlaceholders(quality.symbol.irrelevantRetrievedSymbols),
40
+ missedSymbols: symbolPlaceholders(quality.symbol.missedSymbols)
41
+ },
42
+ fact: {
43
+ ...quality.fact,
44
+ coveredFactIds: factPlaceholders(quality.fact.coveredFactIds),
45
+ uncoveredFactIds: factPlaceholders(quality.fact.uncoveredFactIds)
46
+ }
47
+ };
48
+ }
49
+ /**
50
+ * Projects case evidence for durable persistence of an external-local run. Redacts file, symbol and fact identity
51
+ * lists, the case title, and all retrieval warning prose; keeps logical ids, locality, statuses, numbers and
52
+ * fixed-vocabulary evidence. It performs no metric calculation.
53
+ */
54
+ export function projectRetrievalEvidenceForExternalLocalPersistence(evidence) {
55
+ return evidence.map((entry) => {
56
+ const cloned = structuredClone(entry);
57
+ return {
58
+ ...cloned,
59
+ caseName: REDACTED_CASE_TITLE,
60
+ retrieval: cloned.retrieval === null
61
+ ? null
62
+ : { ...cloned.retrieval, warnings: placeholders("warning", cloned.retrieval.warnings.length) },
63
+ quality: cloned.quality === null ? null : projectQuality(cloned.quality),
64
+ errors: cloned.errors.map((error) => ({ code: error.code, message: FIXED_ERROR_TEXT[error.code] })),
65
+ identityRedaction: { ...RETRIEVAL_IDENTITY_REDACTION }
66
+ };
67
+ });
68
+ }
69
+ /**
70
+ * Defense in depth: fails closed if a known private file path, case title, or warning text survived projection.
71
+ * It deliberately does not substring-search short symbol or fact identifiers, which would collide with ordinary words.
72
+ */
73
+ export function assertExternalLocalProjectionIsPrivate(projected, privateValues) {
74
+ const serialized = JSON.stringify(projected);
75
+ const candidates = [...privateValues.filePaths, ...privateValues.caseTitles, ...privateValues.warnings].filter((value) => value.length > 0);
76
+ const leaked = candidates.some((value) => serialized.includes(value) || serialized.includes(JSON.stringify(value).slice(1, -1)));
77
+ if (leaked) {
78
+ throw new Error("External-local privacy projection check failed; no durable output was written.");
79
+ }
80
+ }
81
+ const GENERIC_ISSUE_TEXT = {
82
+ WORK_ROOT_INSIDE_TARGET: "the output directory must be outside the local subject repository",
83
+ BEFORE_SNAPSHOT_FAILED: "the target snapshot before execution could not be captured",
84
+ AFTER_SNAPSHOT_FAILED: "the target snapshot after execution could not be captured",
85
+ SCRATCH_CLEANUP_FAILED: "the private scratch could not be removed",
86
+ GUIDED_EXCLUSION_LIMIT: "the guided-index exclusions exceeded the safe limit",
87
+ GUIDED_EXCLUSION_UNREPRESENTABLE: "the guided-index exclusions could not be expressed exactly"
88
+ };
89
+ /**
90
+ * Safe, persistable description of an external-local failure: issue codes, logical case ids, counts and mutation
91
+ * kinds only. The issue messages built by the execution seam are safe by construction and are included for the
92
+ * three case-scoped codes; nothing else (no file name, symbol, fact id, path, or caught error text) can appear.
93
+ */
94
+ export function describeRetrievalLocalSubjectFailureForPersistence(error) {
95
+ const parts = [];
96
+ const seen = new Set();
97
+ for (const issue of error.issues) {
98
+ if (issue.code === "EXECUTION_FAILED" || issue.code === "GROUND_TRUTH_INVALID" || issue.code === "RETRIEVAL_OUTSIDE_ELIGIBLE_UNIVERSE") {
99
+ if (!seen.has(issue.message)) {
100
+ seen.add(issue.message);
101
+ parts.push(issue.message.replace(/\.$/, ""));
102
+ }
103
+ continue;
104
+ }
105
+ if (seen.has(issue.code))
106
+ continue;
107
+ seen.add(issue.code);
108
+ if (issue.code === "TARGET_MUTATED") {
109
+ const kinds = [...new Set((error.immutability?.mutations ?? []).map((mutation) => mutation.kind))].sort();
110
+ parts.push(`the target changed during execution${kinds.length > 0 ? ` (${kinds.join(", ")})` : ""}; it was not restored`);
111
+ }
112
+ else {
113
+ parts.push(GENERIC_ISSUE_TEXT[issue.code] ?? `issue ${issue.code}`);
114
+ }
115
+ }
116
+ return `Local subject execution failed (${error.code}): ${parts.join("; ")}.`;
117
+ }
118
+ /** Fixed message for any non-LocalSubjectExecutionError thrown inside the external path: no caught text is exposed. */
119
+ export const UNEXPECTED_EXTERNAL_FAILURE_MESSAGE = "Local subject execution failed (EXECUTION_FAILED): execution failed before completing (details withheld).";
@@ -0,0 +1,21 @@
1
+ export const RETRIEVAL_PRECISION_RECALL_PLUGIN_ID = "retrieval-precision-recall";
2
+ export const RETRIEVAL_PRECISION_RECALL_VARIANT_ID = "my-dev-kit-retrieval";
3
+ /** The frozen bundled corpus; the public plugin offers no way to substitute another. */
4
+ export const RETRIEVAL_PRECISION_RECALL_CASES_RESOURCE = "benchmarks/contracts/warm-index-benchmark-cases.json";
5
+ export const RETRIEVAL_PRECISION_RECALL_PROJECT_PROFILES_RESOURCE = "benchmarks/contracts/benchmark-project-profiles.json";
6
+ export const retrievalPrecisionRecallMetadata = {
7
+ id: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
8
+ name: "Retrieval Precision/Recall",
9
+ description: "Measure deterministic file, symbol, fact and irrelevant-context retrieval quality for the existing my-dev-kit retrieval lifecycle without agents.",
10
+ schemaVersion: "1.0.0",
11
+ status: "experimental",
12
+ supportedTargets: ["self", "external-local"],
13
+ supportedOutputs: ["json", "html", "text", "artifact"]
14
+ };
15
+ export const RETRIEVAL_PRECISION_RECALL_VARIANTS = [
16
+ {
17
+ id: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
18
+ name: "my-dev-kit retrieval",
19
+ description: "search, then lookup, slice and source for the top candidate, from one index prepared per benchmark project."
20
+ }
21
+ ];
@@ -0,0 +1,158 @@
1
+ import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "./types.js";
2
+ const UNAVAILABLE_RATIO = {
3
+ availability: "unavailable",
4
+ numerator: null,
5
+ denominator: null,
6
+ value: null,
7
+ reason: "no-retrieval-measurement"
8
+ };
9
+ /** Per-case ratio selector. A failed case has no quality result and is counted as unavailable, never as zero. */
10
+ export function retrievalRatioOf(quality, key) {
11
+ if (quality === null)
12
+ return UNAVAILABLE_RATIO;
13
+ switch (key) {
14
+ case "filePrecision":
15
+ return quality.file.precision;
16
+ case "fileRecall":
17
+ return quality.file.recall;
18
+ case "symbolPrecision":
19
+ return quality.symbol.precision;
20
+ case "symbolRecall":
21
+ return quality.symbol.recall;
22
+ case "factCoverage":
23
+ return quality.fact.coverage;
24
+ case "irrelevantContextRatio":
25
+ return quality.irrelevantContextRatio;
26
+ }
27
+ }
28
+ function aggregateRatio(metrics) {
29
+ const available = metrics.filter((metric) => metric.availability === "available" && typeof metric.value === "number");
30
+ return {
31
+ availableCount: available.length,
32
+ unavailableCount: metrics.filter((metric) => metric.availability === "unavailable").length,
33
+ notApplicableCount: metrics.filter((metric) => metric.availability === "not-applicable").length,
34
+ meanValue: available.length === 0 ? null : available.reduce((sum, metric) => sum + metric.value, 0) / available.length
35
+ };
36
+ }
37
+ function aggregateTokens(cases) {
38
+ const counts = cases
39
+ .map((entry) => entry.quality?.retrievedTokenCount)
40
+ .filter((count) => typeof count === "number" && Number.isFinite(count) && count >= 0);
41
+ const total = counts.reduce((sum, count) => sum + count, 0);
42
+ return {
43
+ availableCount: counts.length,
44
+ unavailableCount: cases.length - counts.length,
45
+ totalTokens: counts.length === 0 ? null : total,
46
+ meanTokens: counts.length === 0 ? null : total / counts.length,
47
+ minTokens: counts.length === 0 ? null : Math.min(...counts),
48
+ maxTokens: counts.length === 0 ? null : Math.max(...counts)
49
+ };
50
+ }
51
+ const countStatus = (cases, status) => cases.filter((entry) => entry.status === status).length;
52
+ /**
53
+ * Pure macro aggregation over per-case Batch 2 results. It never recalculates a per-case metric, never weights by
54
+ * denominators, and never produces a score, winner, ranking or threshold.
55
+ */
56
+ export function aggregateRetrievalPrecisionRecall(cases) {
57
+ const ratios = Object.fromEntries(RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => [key, aggregateRatio(cases.map((entry) => retrievalRatioOf(entry.quality, key)))]));
58
+ const sumLists = (select) => {
59
+ let total = 0;
60
+ let evidenceCases = 0;
61
+ for (const entry of cases) {
62
+ const list = entry.quality === null ? null : select(entry.quality);
63
+ if (list === null)
64
+ continue;
65
+ total += list.length;
66
+ evidenceCases += 1;
67
+ }
68
+ return { total, evidenceCases };
69
+ };
70
+ const missedFiles = sumLists((quality) => quality.file.missedFiles);
71
+ const irrelevantFiles = sumLists((quality) => quality.file.irrelevantRetrievedFiles);
72
+ const missedSymbols = sumLists((quality) => quality.symbol.missedSymbols);
73
+ const irrelevantSymbols = sumLists((quality) => quality.symbol.irrelevantRetrievedSymbols);
74
+ const uncoveredFacts = sumLists((quality) => quality.fact.uncoveredFactIds);
75
+ const evidenceCount = (availability) => cases.filter((entry) => entry.retrieval?.evidenceAvailability === availability).length;
76
+ return {
77
+ runSummary: {
78
+ projectCount: new Set(cases.map((entry) => entry.benchmarkProject)).size,
79
+ caseCount: cases.length,
80
+ completedCaseCount: countStatus(cases, "completed"),
81
+ partialCaseCount: countStatus(cases, "partial"),
82
+ failedCaseCount: countStatus(cases, "failed"),
83
+ retrievalEvidenceAvailableCaseCount: evidenceCount("available"),
84
+ retrievalEvidencePartialCaseCount: evidenceCount("partial"),
85
+ retrievalEvidenceUnavailableCaseCount: cases.filter((entry) => entry.retrieval !== null && entry.retrieval.evidenceAvailability !== "available" && entry.retrieval.evidenceAvailability !== "partial").length
86
+ },
87
+ ratios,
88
+ tokens: aggregateTokens(cases),
89
+ occurrences: {
90
+ totalMissedFileOccurrences: missedFiles.total,
91
+ totalMissedSymbolOccurrences: missedSymbols.total,
92
+ totalUncoveredFactOccurrences: uncoveredFacts.total,
93
+ totalIrrelevantRetrievedFileOccurrences: irrelevantFiles.total,
94
+ totalIrrelevantRetrievedSymbolOccurrences: irrelevantSymbols.total,
95
+ fileOccurrenceEvidenceCaseCount: missedFiles.evidenceCases,
96
+ symbolOccurrenceEvidenceCaseCount: missedSymbols.evidenceCases,
97
+ factOccurrenceEvidenceCaseCount: uncoveredFacts.evidenceCases
98
+ }
99
+ };
100
+ }
101
+ const RATIO_METRIC_IDS = {
102
+ filePrecision: { id: "file-precision", name: "File precision" },
103
+ fileRecall: { id: "file-recall", name: "File recall" },
104
+ symbolPrecision: { id: "symbol-precision", name: "Symbol precision" },
105
+ symbolRecall: { id: "symbol-recall", name: "Symbol recall" },
106
+ factCoverage: { id: "fact-coverage", name: "Fact coverage" },
107
+ irrelevantContextRatio: { id: "irrelevant-context-ratio", name: "Irrelevant context ratio" }
108
+ };
109
+ /** Run-level generic metric projection. Ratios stay canonical (0..1, never percent); null means no available evidence. */
110
+ export function toRetrievalPrecisionRecallRunMetrics(aggregate) {
111
+ const summary = aggregate.runSummary;
112
+ const occurrences = aggregate.occurrences;
113
+ return [
114
+ { id: "case-count", name: "Cases", value: summary.caseCount, unit: "count" },
115
+ { id: "completed-case-count", name: "Completed cases", value: summary.completedCaseCount, unit: "count" },
116
+ { id: "partial-case-count", name: "Partial cases", value: summary.partialCaseCount, unit: "count" },
117
+ { id: "failed-case-count", name: "Failed cases", value: summary.failedCaseCount, unit: "count" },
118
+ ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => ({
119
+ id: `mean-${RATIO_METRIC_IDS[key].id}`,
120
+ name: `Mean ${RATIO_METRIC_IDS[key].name.toLowerCase()}`,
121
+ value: aggregate.ratios[key].meanValue,
122
+ unit: "ratio"
123
+ })),
124
+ { id: "total-retrieved-tokens", name: "Total retrieved tokens", value: aggregate.tokens.totalTokens, unit: "tokens" },
125
+ { id: "mean-retrieved-tokens", name: "Mean retrieved tokens", value: aggregate.tokens.meanTokens, unit: "tokens" },
126
+ { id: "total-missed-file-occurrences", name: "Total missed file occurrences", value: occurrences.totalMissedFileOccurrences, unit: "count" },
127
+ { id: "total-missed-symbol-occurrences", name: "Total missed symbol occurrences", value: occurrences.totalMissedSymbolOccurrences, unit: "count" },
128
+ { id: "total-uncovered-fact-occurrences", name: "Total uncovered fact occurrences", value: occurrences.totalUncoveredFactOccurrences, unit: "count" },
129
+ { id: "total-irrelevant-file-occurrences", name: "Total irrelevant file occurrences", value: occurrences.totalIrrelevantRetrievedFileOccurrences, unit: "count" }
130
+ ];
131
+ }
132
+ /** Per-case metric projection for the single variant; unavailable and not-applicable ratios project as null. */
133
+ export function toRetrievalPrecisionRecallOutcomeMetrics(quality, variantId, caseId) {
134
+ const base = { variantId, caseId };
135
+ const ratio = (key) => ({
136
+ ...base,
137
+ id: RATIO_METRIC_IDS[key].id,
138
+ name: RATIO_METRIC_IDS[key].name,
139
+ value: retrievalRatioOf(quality, key).value,
140
+ unit: "ratio"
141
+ });
142
+ const count = (id, name, value) => ({
143
+ ...base,
144
+ id,
145
+ name,
146
+ value: value ?? null,
147
+ unit: "count"
148
+ });
149
+ return [
150
+ ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map(ratio),
151
+ { ...base, id: "retrieved-token-count", name: "Retrieved token count", value: quality?.retrievedTokenCount ?? null, unit: "tokens" },
152
+ count("missed-file-count", "Missed file count", quality?.file.missedFileCount),
153
+ count("missed-symbol-count", "Missed symbol count", quality?.symbol.missedSymbolCount),
154
+ count("uncovered-fact-count", "Uncovered fact count", quality?.fact.uncoveredFactCount),
155
+ count("irrelevant-retrieved-file-count", "Irrelevant retrieved file count", quality?.file.irrelevantRetrievedFiles?.length),
156
+ count("irrelevant-retrieved-symbol-count", "Irrelevant retrieved symbol count", quality?.symbol.irrelevantRetrievedSymbols?.length)
157
+ ];
158
+ }