@dailephd/my-dev-kit-lab 0.7.2 → 0.8.0

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 (36) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +7 -3
  3. package/benchmarks/contracts/warm-index-benchmark-cases.json +330 -0
  4. package/dist/src/cli/help.js +12 -0
  5. package/dist/src/commands/runExperimentDescribeCommand.js +9 -1
  6. package/dist/src/commands/runExperimentRunCommand.js +95 -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 +233 -0
  13. package/dist/src/evaluation/retrievalQuality/types.js +9 -0
  14. package/dist/src/evaluation/runMyDevKitRetrieval.js +3 -0
  15. package/dist/src/experiments/defaultRegistry.js +2 -0
  16. package/dist/src/experiments/plugins/index.js +1 -0
  17. package/dist/src/experiments/plugins/retrievalPrecisionRecall/config.js +39 -0
  18. package/dist/src/experiments/plugins/retrievalPrecisionRecall/execution.js +152 -0
  19. package/dist/src/experiments/plugins/retrievalPrecisionRecall/executionArtifact.js +15 -0
  20. package/dist/src/experiments/plugins/retrievalPrecisionRecall/index.js +9 -0
  21. package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectExecution.js +139 -0
  22. package/dist/src/experiments/plugins/retrievalPrecisionRecall/localSubjectPrivacy.js +119 -0
  23. package/dist/src/experiments/plugins/retrievalPrecisionRecall/metadata.js +21 -0
  24. package/dist/src/experiments/plugins/retrievalPrecisionRecall/metrics.js +158 -0
  25. package/dist/src/experiments/plugins/retrievalPrecisionRecall/plugin.js +253 -0
  26. package/dist/src/experiments/plugins/retrievalPrecisionRecall/types.js +8 -0
  27. package/dist/src/report/experiments/buildPluginExperimentReport.js +21 -2
  28. package/dist/src/report/experiments/buildRetrievalPrecisionRecallReport.js +83 -0
  29. package/dist/src/report/experiments/index.js +4 -0
  30. package/dist/src/report/experiments/renderPluginExperimentReportHtml.js +3 -0
  31. package/dist/src/report/experiments/renderPluginExperimentReportText.js +9 -0
  32. package/dist/src/report/experiments/renderRetrievalPrecisionRecallHtml.js +96 -0
  33. package/dist/src/report/experiments/renderRetrievalPrecisionRecallText.js +121 -0
  34. package/dist/src/report/experiments/retrievalPrecisionRecallReportModel.js +3 -0
  35. package/docs/METRICS.md +32 -2
  36. package/package.json +1 -1
@@ -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
+ }
@@ -0,0 +1,253 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveWithinRoot } from "../../../core/pathSafety.js";
4
+ import { serializeLocalRepositorySubjectManifest } from "../../../evaluation/localRepositorySubject/index.js";
5
+ import { summarizeExperimentRun } from "../../results.js";
6
+ import { LocalSubjectExecutionError } from "../contextWindowScaling/localSubjectErrors.js";
7
+ import { projectExternalLocalTarget } from "../contextWindowScaling/localSubjectPrivacy.js";
8
+ import { isSamePhysicalDirectory } from "../contextWindowScaling/localSubjectScratch.js";
9
+ import { selectWarmIndexCases } from "../warmIndexReuse/selection.js";
10
+ import { defaultRetrievalPrecisionRecallConfig, retrievalPrecisionRecallConfigDefinition, validateRetrievalPrecisionRecallConfig } from "./config.js";
11
+ import { executeRetrievalPrecisionRecall } from "./execution.js";
12
+ import { executeLocalRepositorySubjectRetrievalPrecisionRecall } from "./localSubjectExecution.js";
13
+ import { assertExternalLocalProjectionIsPrivate, describeRetrievalLocalSubjectFailureForPersistence, projectRetrievalEvidenceForExternalLocalPersistence, UNEXPECTED_EXTERNAL_FAILURE_MESSAGE } from "./localSubjectPrivacy.js";
14
+ import { buildRetrievalPrecisionRecallExecutionArtifact, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE } from "./executionArtifact.js";
15
+ import { aggregateRetrievalPrecisionRecall, retrievalRatioOf, toRetrievalPrecisionRecallOutcomeMetrics, toRetrievalPrecisionRecallRunMetrics } from "./metrics.js";
16
+ import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID, RETRIEVAL_PRECISION_RECALL_VARIANTS, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, retrievalPrecisionRecallMetadata } from "./metadata.js";
17
+ import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "./types.js";
18
+ /** Privacy-safe Batch 1 manifest persisted beside the execution artifact for external-local runs (same file as context-window-scaling). */
19
+ export const RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE = "local-repository-subject-manifest.json";
20
+ /** An external target must always arrive with a loaded local repository subject; there is no bundled fallback against it. */
21
+ export const RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE = "External-local retrieval-precision-recall targets require a loaded local repository subject (--local-subject-config).";
22
+ export const retrievalPrecisionRecallPlugin = {
23
+ metadata: retrievalPrecisionRecallMetadata,
24
+ defaultConfig: defaultRetrievalPrecisionRecallConfig,
25
+ configDefinition: retrievalPrecisionRecallConfigDefinition,
26
+ supportedVariants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => variant.id),
27
+ validateConfig: validateRetrievalPrecisionRecallConfig,
28
+ async run(context) {
29
+ const startedAt = context.startedAt.toISOString();
30
+ // The runner does not enforce supportedTargets, so the plugin fails closed on its own.
31
+ const localSubject = readLocalSubjectInput(context.inputs);
32
+ if (localSubject)
33
+ return runLocalSubject(context, localSubject, startedAt);
34
+ if (context.target.kind !== "self" || context.target.isSelf !== true) {
35
+ // Never fall back to the bundled corpus against an external repository.
36
+ throw new Error(RETRIEVAL_PRECISION_RECALL_EXTERNAL_REQUIRES_SUBJECT_MESSAGE);
37
+ }
38
+ const cases = selectWarmIndexCases(readCasesInput(context.inputs), context.config);
39
+ const outDir = context.outputRoot ?? path.resolve(context.toolRoot, context.config.outDir);
40
+ await mkdir(outDir, { recursive: true });
41
+ const caseEvidence = await executeRetrievalPrecisionRecall({
42
+ cases,
43
+ kitCommand: context.config.kitCommand,
44
+ outputRoot: outDir,
45
+ dependencies: readDependenciesInput(context.inputs)
46
+ });
47
+ // Scientific truth is calculated before persistence; the artifact and every report consume it unchanged.
48
+ const aggregate = aggregateRetrievalPrecisionRecall(caseEvidence);
49
+ const completedAt = new Date().toISOString();
50
+ const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
51
+ runId: context.runId,
52
+ pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
53
+ pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
54
+ startedAt,
55
+ completedAt,
56
+ cases: caseEvidence,
57
+ aggregate
58
+ });
59
+ const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
60
+ await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}\n`, "utf8");
61
+ return mapRetrievalPrecisionRecallToRun({
62
+ runId: context.runId,
63
+ startedAt,
64
+ completedAt,
65
+ target: context.target,
66
+ caseEvidence,
67
+ aggregate,
68
+ artifactPath
69
+ });
70
+ },
71
+ summarize(result) {
72
+ return result.summary ?? summarizeExperimentRun(result);
73
+ }
74
+ };
75
+ function readLocalSubjectInput(inputs) {
76
+ const value = inputs?.localSubject;
77
+ return value && typeof value === "object" ? value : undefined;
78
+ }
79
+ /**
80
+ * External-local path. The safe seam owns execution, immutability and scratch cleanup and either returns evidence or
81
+ * throws; only after it succeeds is anything durable written: the case evidence is privacy-projected, the aggregate
82
+ * (calculated from the unprojected numbers) is preserved, and the artifact and manifest are written, in that order.
83
+ */
84
+ async function runLocalSubject(context, subject, startedAt) {
85
+ if (context.target.isSelf || context.target.kind !== "external-local") {
86
+ throw new Error("Local-repository subject mode requires an external-local target.");
87
+ }
88
+ if (context.config.caseIds !== undefined || context.config.benchmarkProjects !== undefined) {
89
+ throw new Error("Case and benchmark-project filters are not supported for an external local repository subject; the subject config owns the case set.");
90
+ }
91
+ if (!(await isSamePhysicalDirectory(context.target.targetRoot, subject.repositoryRoot))) {
92
+ throw new Error("The selected --target is not the repository the local subject was loaded from.");
93
+ }
94
+ const outDir = context.outputRoot;
95
+ if (!outDir)
96
+ throw new Error("Local-repository subject mode requires an experiment output directory.");
97
+ let localResult;
98
+ try {
99
+ localResult = await executeLocalRepositorySubjectRetrievalPrecisionRecall({
100
+ subject,
101
+ kitCommand: context.config.kitCommand,
102
+ workRoot: outDir,
103
+ dependencies: readDependenciesInput(context.inputs)
104
+ });
105
+ }
106
+ catch (error) {
107
+ if (error instanceof LocalSubjectExecutionError)
108
+ throw new Error(describeRetrievalLocalSubjectFailureForPersistence(error));
109
+ throw new Error(UNEXPECTED_EXTERNAL_FAILURE_MESSAGE);
110
+ }
111
+ // Scientific numbers are calculated once from the unprojected evidence; projection only withholds identities.
112
+ const aggregate = aggregateRetrievalPrecisionRecall(localResult.caseEvidence);
113
+ const projected = projectRetrievalEvidenceForExternalLocalPersistence(localResult.caseEvidence);
114
+ assertExternalLocalProjectionIsPrivate(projected, {
115
+ filePaths: [...subject.eligibleFiles, ...subject.runtimeSafetyExclusions.gitIgnoredFiles, ...subject.runtimeSafetyExclusions.oversizedFiles],
116
+ caseTitles: subject.evaluationCases.map((evaluationCase) => evaluationCase.title),
117
+ warnings: localResult.caseEvidence.flatMap((entry) => entry.retrieval?.warnings ?? [])
118
+ });
119
+ await mkdir(outDir, { recursive: true });
120
+ const completedAt = new Date().toISOString();
121
+ const artifact = buildRetrievalPrecisionRecallExecutionArtifact({
122
+ runId: context.runId,
123
+ pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
124
+ pluginSchemaVersion: retrievalPrecisionRecallMetadata.schemaVersion,
125
+ startedAt,
126
+ completedAt,
127
+ cases: projected,
128
+ aggregate
129
+ });
130
+ const artifactPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_EXECUTION_ARTIFACT_FILE);
131
+ await writeFile(artifactPath, `${JSON.stringify(artifact, null, 2)}
132
+ `, "utf8");
133
+ const manifestPath = resolveWithinRoot(outDir, RETRIEVAL_PRECISION_RECALL_LOCAL_SUBJECT_MANIFEST_FILE);
134
+ await writeFile(manifestPath, serializeLocalRepositorySubjectManifest(subject.manifest), "utf8");
135
+ const run = mapRetrievalPrecisionRecallToRun({
136
+ runId: context.runId,
137
+ startedAt,
138
+ completedAt,
139
+ target: projectExternalLocalTarget(subject.manifest),
140
+ caseEvidence: projected,
141
+ aggregate,
142
+ artifactPath
143
+ });
144
+ run.artifacts.push({
145
+ id: "local-repository-subject-manifest",
146
+ label: "Privacy-safe local repository subject manifest",
147
+ path: manifestPath,
148
+ kind: "artifact",
149
+ mimeType: "application/json",
150
+ description: "Privacy-safe logical subject, Git, safety and aggregate inventory metadata without source or private identity lists."
151
+ });
152
+ return run;
153
+ }
154
+ export function mapRetrievalPrecisionRecallToRun(args) {
155
+ const cases = args.caseEvidence.map((entry) => ({
156
+ id: entry.caseId,
157
+ name: entry.caseName,
158
+ outcomes: [buildOutcome(entry)],
159
+ metadata: { benchmarkProject: entry.benchmarkProject, taskLocality: entry.taskLocality }
160
+ }));
161
+ const failures = args.caseEvidence.flatMap((entry) => entry.errors.map((error) => ({
162
+ code: error.code,
163
+ message: error.message,
164
+ variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
165
+ caseId: entry.caseId,
166
+ recoverable: true
167
+ })));
168
+ const run = {
169
+ runId: args.runId,
170
+ pluginId: RETRIEVAL_PRECISION_RECALL_PLUGIN_ID,
171
+ startedAt: args.startedAt,
172
+ completedAt: args.completedAt,
173
+ status: aggregateStatus(args.caseEvidence.map((entry) => entry.status)),
174
+ target: args.target,
175
+ variants: RETRIEVAL_PRECISION_RECALL_VARIANTS.map((variant) => ({ ...variant })),
176
+ cases,
177
+ metrics: toRetrievalPrecisionRecallRunMetrics(args.aggregate),
178
+ artifacts: [
179
+ {
180
+ id: "retrieval-precision-recall-execution",
181
+ label: "Retrieval precision/recall execution evidence",
182
+ path: args.artifactPath,
183
+ kind: "artifact",
184
+ mimeType: "application/json",
185
+ description: "Per-case retrieval-quality evidence and aggregate without context text or raw command output."
186
+ }
187
+ ],
188
+ warnings: [],
189
+ failures,
190
+ metadata: { executionArtifactPath: args.artifactPath },
191
+ caseExecutionEvidence: args.caseEvidence.map((entry) => structuredClone(entry)),
192
+ aggregate: structuredClone(args.aggregate)
193
+ };
194
+ run.summary = summarizeExperimentRun(run);
195
+ return run;
196
+ }
197
+ function buildOutcome(entry) {
198
+ const warnings = (entry.retrieval?.warnings ?? []).map((message) => ({
199
+ code: "retrieval-warning",
200
+ message,
201
+ variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
202
+ caseId: entry.caseId
203
+ }));
204
+ const failures = entry.errors.map((error) => ({
205
+ code: error.code,
206
+ message: error.message,
207
+ variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
208
+ caseId: entry.caseId,
209
+ recoverable: true
210
+ }));
211
+ // Availability and reason stay visible even though an unavailable/not-applicable ratio projects as a null value.
212
+ const metricAvailability = {};
213
+ for (const key of RETRIEVAL_PRECISION_RECALL_RATIO_KEYS) {
214
+ const ratio = retrievalRatioOf(entry.quality, key);
215
+ metricAvailability[key] = { availability: ratio.availability, reason: ratio.reason };
216
+ }
217
+ return {
218
+ id: `${entry.caseId}:${RETRIEVAL_PRECISION_RECALL_VARIANT_ID}`,
219
+ caseId: entry.caseId,
220
+ variantId: RETRIEVAL_PRECISION_RECALL_VARIANT_ID,
221
+ status: entry.status,
222
+ metrics: toRetrievalPrecisionRecallOutcomeMetrics(entry.quality, RETRIEVAL_PRECISION_RECALL_VARIANT_ID, entry.caseId),
223
+ artifacts: [],
224
+ warnings,
225
+ failures,
226
+ metadata: {
227
+ benchmarkProject: entry.benchmarkProject,
228
+ evidenceAvailability: entry.retrieval?.evidenceAvailability ?? null,
229
+ metricAvailability
230
+ }
231
+ };
232
+ }
233
+ /** Same aggregation rule as the generic per-case summary: uniform statuses win, mixed is partial. */
234
+ function aggregateStatus(statuses) {
235
+ if (statuses.length === 0)
236
+ return "skipped";
237
+ for (const status of ["completed", "skipped", "failed"]) {
238
+ if (statuses.every((value) => value === status))
239
+ return status;
240
+ }
241
+ return "partial";
242
+ }
243
+ function readCasesInput(inputs) {
244
+ const value = inputs?.cases;
245
+ if (!Array.isArray(value)) {
246
+ throw new Error("Retrieval precision/recall requires cases input.");
247
+ }
248
+ return value;
249
+ }
250
+ function readDependenciesInput(inputs) {
251
+ const value = inputs?.retrievalDependencies;
252
+ return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
253
+ }
@@ -0,0 +1,8 @@
1
+ export const RETRIEVAL_PRECISION_RECALL_RATIO_KEYS = [
2
+ "filePrecision",
3
+ "fileRecall",
4
+ "symbolPrecision",
5
+ "symbolRecall",
6
+ "factCoverage",
7
+ "irrelevantContextRatio"
8
+ ];
@@ -3,9 +3,12 @@ import { buildContextStrategyComparisonV043Report } from "./buildContextStrategy
3
3
  import { buildWarmIndexReuseReport } from "./buildWarmIndexReuseReport.js";
4
4
  import { buildIncrementalChangeStalenessPluginReport } from "./buildIncrementalChangeStalenessPluginReport.js";
5
5
  import { buildContextWindowScalingReport } from "./buildContextWindowScalingReport.js";
6
+ import { buildRetrievalPrecisionRecallReport } from "./buildRetrievalPrecisionRecallReport.js";
6
7
  // Bulk context-window-scaling evidence is presented by the typed report section; the complete
7
8
  // evidence stays in context-window-scaling-execution.json.
8
9
  const CONTEXT_WINDOW_SCALING_BULK_KEYS = ["executionEvidence", "aggregate"];
10
+ // Same for retrieval-precision-recall: the typed section presents the evidence; the execution artifact keeps all of it.
11
+ const RETRIEVAL_PRECISION_RECALL_BULK_KEYS = ["caseExecutionEvidence", "aggregate"];
9
12
  const V043_BULK_ARRAY_KEYS = [
10
13
  "v043StageContextExecutions",
11
14
  "v043StageContextEvaluations",
@@ -18,6 +21,7 @@ export function buildPluginExperimentReport(args) {
18
21
  const warmIndexReuse = buildWarmIndexReuseReport(args.run);
19
22
  const incrementalChangeStaleness = buildIncrementalChangeStalenessPluginReport(args.run);
20
23
  const contextWindowScaling = buildContextWindowScalingReport(args.run);
24
+ const retrievalPrecisionRecall = buildRetrievalPrecisionRecallReport(args.run);
21
25
  const rawRun = { ...args.run, artifacts: relativizeArtifacts(args.run.artifacts, outputRoot) };
22
26
  for (const key of V043_BULK_ARRAY_KEYS) {
23
27
  delete rawRun[key];
@@ -27,6 +31,11 @@ export function buildPluginExperimentReport(args) {
27
31
  delete rawRun[key];
28
32
  }
29
33
  }
34
+ if (retrievalPrecisionRecall) {
35
+ for (const key of RETRIEVAL_PRECISION_RECALL_BULK_KEYS) {
36
+ delete rawRun[key];
37
+ }
38
+ }
30
39
  return {
31
40
  metadata: {
32
41
  generatedAt: args.generatedAt ?? new Date().toISOString(),
@@ -53,8 +62,9 @@ export function buildPluginExperimentReport(args) {
53
62
  warmIndexReuse,
54
63
  incrementalChangeStaleness,
55
64
  contextWindowScaling,
65
+ retrievalPrecisionRecall,
56
66
  contextStrategyComparisonV043,
57
- interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling),
67
+ interpretation: buildInterpretation(args.run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling, retrievalPrecisionRecall),
58
68
  rawRun,
59
69
  };
60
70
  }
@@ -133,7 +143,16 @@ function affectedNeighborhoodSentence(report) {
133
143
  `reindex recommendations were recommended=${summary.recommendedReindexCount}, not-indicated=${summary.notIndicatedReindexCount}, unknown=${summary.unknownReindexRecommendationCount}. ` +
134
144
  "Affected-neighborhood evidence uses a one-hop baseline graph around confirmed changed indexed files/symbols. It is observational and does not alter retrieval or execution status.");
135
145
  }
136
- function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling) {
146
+ function buildInterpretation(run, contextStrategyComparisonV043, warmIndexReuse, contextWindowScaling, retrievalPrecisionRecall) {
147
+ if (retrievalPrecisionRecall) {
148
+ const summary = retrievalPrecisionRecall.runSummary;
149
+ return {
150
+ summary: `Retrieval quality was measured over ${summary.caseCount} case${summary.caseCount === 1 ? "" : "s"} (${summary.completedCaseCount} completed, ${summary.partialCaseCount} partial, ${summary.failedCaseCount} failed). ` +
151
+ "Available case-level file, symbol and fact metrics are summarized without ranking strategies. " +
152
+ "Unavailable and not-applicable evidence is excluded from macro means rather than treated as zero.",
153
+ recommendedNextStep: "Review per-case missed and irrelevant context together with availability before drawing conclusions."
154
+ };
155
+ }
137
156
  if (contextWindowScaling) {
138
157
  const summary = contextWindowScaling.runSummary;
139
158
  return {