@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,83 @@
1
+ import { RETRIEVAL_PRECISION_RECALL_PLUGIN_ID } from "../../experiments/plugins/retrievalPrecisionRecall/metadata.js";
2
+ import { retrievalRatioOf } from "../../experiments/plugins/retrievalPrecisionRecall/metrics.js";
3
+ import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "../../experiments/plugins/retrievalPrecisionRecall/types.js";
4
+ import { MAX_RETRIEVAL_REPORT_IDENTITIES, RETRIEVAL_PRECISION_RECALL_REPORT_SCHEMA_VERSION } from "./retrievalPrecisionRecallReportModel.js";
5
+ /** Fixed wording; the report states observed evidence only and never ranks or scores anything. */
6
+ export const RETRIEVAL_PRECISION_RECALL_LIMITATIONS = [
7
+ "File and symbol precision and recall are set-based measures, not ranked metrics.",
8
+ "Symbol scoring uses exact unique symbol names, because the answer key lists names rather than node identities.",
9
+ "Fact coverage uses explicit fact-to-context mappings in the answer key; it does not judge generated answers or interpret fact text.",
10
+ "Irrelevant context ratio is file-identity based and is the complement of file precision when both are available.",
11
+ "Retrieved token count measures the selected retrieval context payload; it is not a relevant versus irrelevant token attribution.",
12
+ "The current lifecycle expands only the top search candidate (lookup, slice and source).",
13
+ "Unavailable and not-applicable values are not zeros and are excluded from macro means.",
14
+ "The experiment uses no real agents.",
15
+ "v0.8.0 does not compare retrieval strategies."
16
+ ];
17
+ function boundedList(items) {
18
+ if (items === null)
19
+ return null;
20
+ const displayed = items.slice(0, MAX_RETRIEVAL_REPORT_IDENTITIES);
21
+ return {
22
+ totalCount: items.length,
23
+ displayed: [...displayed],
24
+ displayedCount: displayed.length,
25
+ omittedCount: items.length - displayed.length
26
+ };
27
+ }
28
+ /**
29
+ * Presents the per-case metric results and aggregate already calculated on the run. It performs no precision, recall,
30
+ * coverage, ratio or mean calculation of its own; identity lists are only bounded for display.
31
+ */
32
+ export function buildRetrievalPrecisionRecallReport(run) {
33
+ if (run.pluginId !== RETRIEVAL_PRECISION_RECALL_PLUGIN_ID)
34
+ return null;
35
+ const candidate = run;
36
+ const evidence = candidate.caseExecutionEvidence;
37
+ const aggregate = candidate.aggregate;
38
+ if (!evidence || !aggregate) {
39
+ // A run that failed before producing evidence (for example an unknown case id or a non-self target) still gets the
40
+ // generic report with its failure; only a non-failed run with missing evidence is a broken report source.
41
+ if (run.status === "failed")
42
+ return null;
43
+ throw new Error("Invalid retrieval-precision-recall report source: case execution evidence and aggregate are required.");
44
+ }
45
+ if (aggregate.runSummary.caseCount !== evidence.length) {
46
+ throw new Error("Invalid retrieval-precision-recall report source: aggregate and case execution evidence are inconsistent.");
47
+ }
48
+ const cases = evidence.map((entry) => {
49
+ const quality = entry.quality;
50
+ const metrics = Object.fromEntries(RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => {
51
+ const ratio = retrievalRatioOf(quality, key);
52
+ return [key, { availability: ratio.availability, numerator: ratio.numerator, denominator: ratio.denominator, value: ratio.value, reason: ratio.reason }];
53
+ }));
54
+ return {
55
+ caseId: entry.caseId,
56
+ caseName: entry.caseName,
57
+ benchmarkProject: entry.benchmarkProject,
58
+ taskLocality: entry.taskLocality,
59
+ status: entry.status,
60
+ evidenceAvailability: entry.retrieval?.evidenceAvailability ?? null,
61
+ metrics,
62
+ retrievedTokenCount: quality?.retrievedTokenCount ?? null,
63
+ tokenCountMethod: quality?.tokenCountMethod ?? null,
64
+ missedFiles: boundedList(quality?.file.missedFiles ?? null),
65
+ irrelevantRetrievedFiles: boundedList(quality?.file.irrelevantRetrievedFiles ?? null),
66
+ missedSymbols: boundedList(quality?.symbol.missedSymbols ?? null),
67
+ irrelevantRetrievedSymbols: boundedList(quality?.symbol.irrelevantRetrievedSymbols ?? null),
68
+ uncoveredFactIds: boundedList(quality?.fact.uncoveredFactIds ?? null),
69
+ errors: entry.errors.map((error) => `${error.code}: ${error.message}`),
70
+ identityRedaction: entry.identityRedaction ? { ...entry.identityRedaction } : null
71
+ };
72
+ });
73
+ return {
74
+ schemaVersion: RETRIEVAL_PRECISION_RECALL_REPORT_SCHEMA_VERSION,
75
+ tokenCountMethod: evidence.find((entry) => entry.quality?.tokenCountMethod != null)?.quality?.tokenCountMethod ?? null,
76
+ runSummary: { ...aggregate.runSummary },
77
+ ratioSummaries: structuredClone(aggregate.ratios),
78
+ tokenSummary: { ...aggregate.tokens },
79
+ occurrenceSummary: { ...aggregate.occurrences },
80
+ cases,
81
+ limitations: [...RETRIEVAL_PRECISION_RECALL_LIMITATIONS]
82
+ };
83
+ }
@@ -24,3 +24,7 @@ export * from "./contextWindowScalingReportModel.js";
24
24
  export { buildContextWindowScalingReport, CONTEXT_WINDOW_SCALING_INTERPRETATION } from "./buildContextWindowScalingReport.js";
25
25
  export { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
26
26
  export { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
27
+ export * from "./retrievalPrecisionRecallReportModel.js";
28
+ export { buildRetrievalPrecisionRecallReport, RETRIEVAL_PRECISION_RECALL_LIMITATIONS } from "./buildRetrievalPrecisionRecallReport.js";
29
+ export { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
30
+ export { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
@@ -2,6 +2,7 @@ import { renderContextStrategyComparisonV043Html } from "./renderContextStrategy
2
2
  import { renderWarmIndexReuseHtml } from "./renderWarmIndexReuseHtml.js";
3
3
  import { renderIncrementalChangeStalenessHtml } from "./renderIncrementalChangeStalenessHtml.js";
4
4
  import { renderContextWindowScalingHtml } from "./renderContextWindowScalingHtml.js";
5
+ import { renderRetrievalPrecisionRecallHtml } from "./renderRetrievalPrecisionRecallHtml.js";
5
6
  export function renderPluginExperimentReportHtml(report) {
6
7
  const redactedTarget = report.target.privacyProjection === "external-local-redacted";
7
8
  return `<!DOCTYPE html>
@@ -106,6 +107,8 @@ export function renderPluginExperimentReportHtml(report) {
106
107
 
107
108
  ${renderContextWindowScalingHtml(report.contextWindowScaling)}
108
109
 
110
+ ${renderRetrievalPrecisionRecallHtml(report.retrievalPrecisionRecall ?? null)}
111
+
109
112
  <section>
110
113
  <h2>Warnings, Skips, And Failures</h2>
111
114
  ${report.findings.length ? table(["Severity", "Code", "Message", "Variant", "Case"], report.findings.map((finding) => [
@@ -3,6 +3,7 @@ import { isV2ReportSection } from "./buildIncrementalChangeStalenessReportV2.js"
3
3
  import { renderIncrementalChangeStalenessSectionV2 } from "./renderIncrementalChangeStalenessTextV2.js";
4
4
  import { REINDEX_RECOMMENDATION_EXPLANATIONS } from "./buildWarmIndexReuseReport.js";
5
5
  import { renderContextWindowScalingTextLines } from "./renderContextWindowScalingText.js";
6
+ import { renderRetrievalPrecisionRecallTextLines } from "./renderRetrievalPrecisionRecallText.js";
6
7
  function sanitizeScalar(value) {
7
8
  const text = String(value);
8
9
  let result = "";
@@ -671,6 +672,14 @@ export function renderPluginExperimentReportText(report) {
671
672
  else {
672
673
  lines.push(...renderContextWindowScalingTextLines(report.contextWindowScaling));
673
674
  }
675
+ pushSection(lines, "Retrieval Precision/Recall");
676
+ const retrievalPrecisionRecall = report.retrievalPrecisionRecall ?? null;
677
+ if (retrievalPrecisionRecall === null) {
678
+ lines.push("Not applicable to this plugin.");
679
+ }
680
+ else {
681
+ lines.push(...renderRetrievalPrecisionRecallTextLines(retrievalPrecisionRecall));
682
+ }
674
683
  pushSection(lines, "Warnings, Skips, And Failures");
675
684
  pushDashList(lines, report.findings.map((finding) => `${finding.severity} ${finding.code} ${finding.message} variant=${finding.variantId ?? ""} case=${finding.caseId ?? ""}`));
676
685
  pushSection(lines, "Artifacts");
@@ -0,0 +1,96 @@
1
+ import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "../../experiments/plugins/retrievalPrecisionRecall/types.js";
2
+ import { RETRIEVAL_IDENTITY_REDACTION_EXPLANATION, formatRetrievalCount, formatRetrievalList, formatRetrievalMean, formatRetrievalMetric, RETRIEVAL_RATIO_LABELS } from "./renderRetrievalPrecisionRecallText.js";
3
+ export function renderRetrievalPrecisionRecallHtml(section) {
4
+ if (section === null) {
5
+ return `<section>
6
+ <h2>Retrieval Precision/Recall</h2>
7
+ <p>Not applicable to this plugin.</p>
8
+ </section>`;
9
+ }
10
+ const tokens = section.tokenSummary;
11
+ const occurrences = section.occurrenceSummary;
12
+ return `<section>
13
+ <h2>Retrieval Precision/Recall</h2>
14
+ <p class="muted">Measures which expected files, symbols and facts the my-dev-kit retrieval lifecycle surfaced, and which irrelevant files it surfaced, without agents. It is descriptive and does not rank anything.</p>
15
+ ${table(["Field", "Value"], [
16
+ ["Schema", section.schemaVersion],
17
+ ["Token count method", section.tokenCountMethod ?? "unavailable"],
18
+ ...Object.entries(section.runSummary).map(([label, value]) => [label, String(value)])
19
+ ])}
20
+ ${section.cases.some((entry) => entry.identityRedaction !== null) ? `<p><strong>Identity redaction.</strong> ${escapeHtml(RETRIEVAL_IDENTITY_REDACTION_EXPLANATION)}</p>` : ""}
21
+ <h3>Macro Averages</h3>
22
+ <p class="muted">Mean over available per-case values only. Unavailable and not-applicable cases are counted separately and are never treated as zero.</p>
23
+ ${table(["Metric", "Available", "Unavailable", "Not applicable", "Mean"], RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => {
24
+ const summary = section.ratioSummaries[key];
25
+ return [RETRIEVAL_RATIO_LABELS[key], String(summary.availableCount), String(summary.unavailableCount), String(summary.notApplicableCount), formatRetrievalMean(summary.meanValue)];
26
+ }))}
27
+ <h3>Retrieved Token Summary</h3>
28
+ <p class="muted">Measured retrieval payload size; not a relevant versus irrelevant token attribution.</p>
29
+ ${table(["Available", "Unavailable", "Total", "Mean", "Min", "Max"], [[
30
+ String(tokens.availableCount),
31
+ String(tokens.unavailableCount),
32
+ formatRetrievalCount(tokens.totalTokens),
33
+ formatRetrievalMean(tokens.meanTokens),
34
+ formatRetrievalCount(tokens.minTokens),
35
+ formatRetrievalCount(tokens.maxTokens)
36
+ ]])}
37
+ <h3>Missed And Irrelevant Occurrence Totals</h3>
38
+ <p class="muted">Summed per case; identical paths in different cases or projects are not deduplicated.</p>
39
+ ${table(["Missed files", "Irrelevant files", "Cases with file evidence", "Missed symbols", "Irrelevant symbols", "Cases with symbol evidence", "Uncovered facts", "Cases with fact evidence"], [[
40
+ String(occurrences.totalMissedFileOccurrences),
41
+ String(occurrences.totalIrrelevantRetrievedFileOccurrences),
42
+ String(occurrences.fileOccurrenceEvidenceCaseCount),
43
+ String(occurrences.totalMissedSymbolOccurrences),
44
+ String(occurrences.totalIrrelevantRetrievedSymbolOccurrences),
45
+ String(occurrences.symbolOccurrenceEvidenceCaseCount),
46
+ String(occurrences.totalUncoveredFactOccurrences),
47
+ String(occurrences.factOccurrenceEvidenceCaseCount)
48
+ ]])}
49
+ <h3>Per-Case Results</h3>
50
+ ${table(["Case", "Project", "Locality", "Status", "Evidence", ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => RETRIEVAL_RATIO_LABELS[key]), "Retrieved tokens"], section.cases.map((entry) => [
51
+ entry.caseId,
52
+ entry.benchmarkProject,
53
+ entry.taskLocality ?? "unavailable",
54
+ entry.status,
55
+ entry.evidenceAvailability ?? "unavailable",
56
+ ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => formatRetrievalMetric(entry.metrics[key])),
57
+ formatRetrievalCount(entry.retrievedTokenCount)
58
+ ]))}
59
+ <h3>Per-Case Missed And Irrelevant Context</h3>
60
+ <p class="muted">Displayed lists are bounded; totals are exact.</p>
61
+ ${section.cases
62
+ .map((entry) => {
63
+ const lists = [
64
+ ["Missed files", entry.missedFiles],
65
+ ["Irrelevant retrieved files", entry.irrelevantRetrievedFiles],
66
+ ["Missed symbols", entry.missedSymbols],
67
+ ["Irrelevant retrieved symbols", entry.irrelevantRetrievedSymbols],
68
+ ["Uncovered fact IDs", entry.uncoveredFactIds]
69
+ ];
70
+ return `<h4><code>${escapeHtml(entry.caseId)}</code> (${escapeHtml(entry.status)})</h4>
71
+ ${entry.identityRedaction !== null ? `<p class="muted">Identity redaction: files, symbols, facts, warnings and case title withheld.</p>` : ""}
72
+ ${lists
73
+ .map(([label, list]) => `<p>${escapeHtml(label)}: ${escapeHtml(formatRetrievalList(list))}</p>${list && list.displayed.length > 0 ? listHtml(list.displayed) : ""}`)
74
+ .join("\n ")}${entry.errors.length > 0 ? `\n ${listHtml(entry.errors)}` : ""}`;
75
+ })
76
+ .join("\n ")}
77
+ <h3>Interpretation Limits</h3>
78
+ ${listHtml(section.limitations)}
79
+ </section>`;
80
+ }
81
+ function listHtml(items) {
82
+ return `<ul>${items.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>`;
83
+ }
84
+ function table(headers, rows) {
85
+ return `<table><thead><tr>${headers.map((header) => `<th>${escapeHtml(header)}</th>`).join("")}</tr></thead><tbody>${rows
86
+ .map((row) => `<tr>${row.map((cell) => `<td>${escapeHtml(cell)}</td>`).join("")}</tr>`)
87
+ .join("")}</tbody></table>`;
88
+ }
89
+ function escapeHtml(value) {
90
+ return String(value ?? "")
91
+ .replaceAll("&", "&amp;")
92
+ .replaceAll("<", "&lt;")
93
+ .replaceAll(">", "&gt;")
94
+ .replaceAll('"', "&quot;")
95
+ .replaceAll("'", "&#39;");
96
+ }
@@ -0,0 +1,121 @@
1
+ import { RETRIEVAL_PRECISION_RECALL_RATIO_KEYS } from "../../experiments/plugins/retrievalPrecisionRecall/types.js";
2
+ export const RETRIEVAL_RATIO_LABELS = {
3
+ filePrecision: "File precision",
4
+ fileRecall: "File recall",
5
+ symbolPrecision: "Symbol precision",
6
+ symbolRecall: "Symbol recall",
7
+ factCoverage: "Fact coverage",
8
+ irrelevantContextRatio: "Irrelevant context ratio"
9
+ };
10
+ // Render-time formatting only; the JSON report keeps exact unrounded values. Unavailable and not-applicable are
11
+ // always named, never shown as a number.
12
+ export function formatRetrievalMetric(metric) {
13
+ if (metric.availability === "available" && metric.value !== null) {
14
+ return `${metric.value.toFixed(4)} (${metric.numerator}/${metric.denominator})`;
15
+ }
16
+ return `${metric.availability}${metric.reason ? ` (${metric.reason})` : ""}`;
17
+ }
18
+ /** Explains the placeholders; shown only when at least one case was externally projected. */
19
+ export const RETRIEVAL_IDENTITY_REDACTION_EXPLANATION = "External-local run: file, symbol and fact identities, warning text and case titles are withheld. A placeholder such as <redacted file 1> stands for exactly one withheld identity; it is not a file name and not an absence of files. Counts and metric values are exact.";
20
+ export function formatRetrievalMean(value) {
21
+ return value === null ? "unavailable" : value.toFixed(4);
22
+ }
23
+ export function formatRetrievalCount(value) {
24
+ return value === null ? "unavailable" : String(value);
25
+ }
26
+ export function formatRetrievalList(list) {
27
+ if (list === null)
28
+ return "unavailable";
29
+ return `${list.totalCount} (displayed ${list.displayedCount}, omitted ${list.omittedCount})`;
30
+ }
31
+ function clean(value) {
32
+ return String(value).replace(/[\u0000-\u001f\u007f]/g, " ");
33
+ }
34
+ function row(parts) {
35
+ return `- ${parts.map(([label, value]) => `${label}: ${clean(value)}`).join(" | ")}`;
36
+ }
37
+ /** Lines for the "Retrieval Precision/Recall" section; every value comes from the report model. */
38
+ export function renderRetrievalPrecisionRecallTextLines(section) {
39
+ const lines = [];
40
+ lines.push(`Schema: ${clean(section.schemaVersion)}`);
41
+ lines.push(`Token Count Method: ${clean(section.tokenCountMethod ?? "unavailable")}`);
42
+ if (section.cases.some((entry) => entry.identityRedaction !== null)) {
43
+ lines.push(`Identity Redaction: ${RETRIEVAL_IDENTITY_REDACTION_EXPLANATION}`);
44
+ }
45
+ lines.push("Run Summary:");
46
+ for (const [label, value] of Object.entries(section.runSummary)) {
47
+ lines.push(`${label}: ${value}`);
48
+ }
49
+ lines.push("Macro Averages (mean over available per-case values only; unavailable and not-applicable are counted, never zero):");
50
+ for (const key of RETRIEVAL_PRECISION_RECALL_RATIO_KEYS) {
51
+ const summary = section.ratioSummaries[key];
52
+ lines.push(row([
53
+ ["Metric", RETRIEVAL_RATIO_LABELS[key]],
54
+ ["Available", summary.availableCount],
55
+ ["Unavailable", summary.unavailableCount],
56
+ ["Not Applicable", summary.notApplicableCount],
57
+ ["Mean", formatRetrievalMean(summary.meanValue)]
58
+ ]));
59
+ }
60
+ lines.push("Retrieved Token Summary (measured payload, not relevant/irrelevant attribution):");
61
+ const tokens = section.tokenSummary;
62
+ lines.push(row([
63
+ ["Available", tokens.availableCount],
64
+ ["Unavailable", tokens.unavailableCount],
65
+ ["Total", formatRetrievalCount(tokens.totalTokens)],
66
+ ["Mean", formatRetrievalMean(tokens.meanTokens)],
67
+ ["Min", formatRetrievalCount(tokens.minTokens)],
68
+ ["Max", formatRetrievalCount(tokens.maxTokens)]
69
+ ]));
70
+ lines.push("Missed And Irrelevant Occurrence Totals (summed per case; not deduplicated across cases or projects):");
71
+ const occurrences = section.occurrenceSummary;
72
+ lines.push(row([
73
+ ["Missed Files", occurrences.totalMissedFileOccurrences],
74
+ ["Irrelevant Files", occurrences.totalIrrelevantRetrievedFileOccurrences],
75
+ ["Cases With File Evidence", occurrences.fileOccurrenceEvidenceCaseCount],
76
+ ["Missed Symbols", occurrences.totalMissedSymbolOccurrences],
77
+ ["Irrelevant Symbols", occurrences.totalIrrelevantRetrievedSymbolOccurrences],
78
+ ["Cases With Symbol Evidence", occurrences.symbolOccurrenceEvidenceCaseCount],
79
+ ["Uncovered Facts", occurrences.totalUncoveredFactOccurrences],
80
+ ["Cases With Fact Evidence", occurrences.factOccurrenceEvidenceCaseCount]
81
+ ]));
82
+ lines.push("Per-Case Results (one row per case):");
83
+ for (const entry of section.cases) {
84
+ lines.push(row([
85
+ ["Case", entry.caseId],
86
+ ["Project", entry.benchmarkProject],
87
+ ["Locality", entry.taskLocality ?? "unavailable"],
88
+ ["Status", entry.status],
89
+ ["Evidence", entry.evidenceAvailability ?? "unavailable"],
90
+ ...RETRIEVAL_PRECISION_RECALL_RATIO_KEYS.map((key) => [RETRIEVAL_RATIO_LABELS[key], formatRetrievalMetric(entry.metrics[key])]),
91
+ ["Retrieved Tokens", formatRetrievalCount(entry.retrievedTokenCount)]
92
+ ]));
93
+ }
94
+ lines.push("Per-Case Missed And Irrelevant Context (displayed lists are bounded; totals are exact):");
95
+ for (const entry of section.cases) {
96
+ lines.push(`Case ${clean(entry.caseId)} (${clean(entry.status)})`);
97
+ if (entry.identityRedaction !== null)
98
+ lines.push(" Identity redaction: files, symbols, facts, warnings and case title withheld");
99
+ const lists = [
100
+ ["Missed Files", entry.missedFiles],
101
+ ["Irrelevant Retrieved Files", entry.irrelevantRetrievedFiles],
102
+ ["Missed Symbols", entry.missedSymbols],
103
+ ["Irrelevant Retrieved Symbols", entry.irrelevantRetrievedSymbols],
104
+ ["Uncovered Fact IDs", entry.uncoveredFactIds]
105
+ ];
106
+ for (const [label, list] of lists) {
107
+ lines.push(` ${label}: ${formatRetrievalList(list)}`);
108
+ for (const item of list?.displayed ?? []) {
109
+ lines.push(` - ${clean(item)}`);
110
+ }
111
+ }
112
+ for (const error of entry.errors) {
113
+ lines.push(` Error: ${clean(error)}`);
114
+ }
115
+ }
116
+ lines.push("Interpretation Limits:");
117
+ for (const limitation of section.limitations) {
118
+ lines.push(`- ${clean(limitation)}`);
119
+ }
120
+ return lines;
121
+ }
@@ -0,0 +1,3 @@
1
+ export const RETRIEVAL_PRECISION_RECALL_REPORT_SCHEMA_VERSION = "my-dev-kit-lab-retrieval-precision-recall-report-v1";
2
+ /** Fixed human-facing bound for every identity list; the execution artifact keeps the complete lists. */
3
+ export const MAX_RETRIEVAL_REPORT_IDENTITIES = 20;
package/docs/METRICS.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Metrics
2
2
 
3
- This document is the canonical metric glossary for my-dev-kit-lab. It defines every metric that appears in benchmark profiles, prompt variants, controlled experiment artifacts, and rendered reports across the registered experiment plugins (`context-strategy-comparison`, `warm-index-reuse`, and `incremental-change-staleness`).
3
+ This document is the canonical metric glossary for my-dev-kit-lab. It defines every metric that appears in benchmark profiles, prompt variants, controlled experiment artifacts, and rendered reports across the registered experiment plugins (`context-strategy-comparison`, `warm-index-reuse`, `incremental-change-staleness`, `context-window-scaling`, and `retrieval-precision-recall`).
4
4
 
5
5
  Related documentation:
6
6
  - [ARCHITECTURE.md](ARCHITECTURE.md) — how metrics flow through the pipeline
@@ -620,7 +620,7 @@ The generic `ExperimentMetric` projection emits run-level case and budget counts
620
620
 
621
621
  The report artifact schema is `my-dev-kit-lab-context-window-scaling-report-v1`. It includes run summaries, budget-treatment summaries, per-treatment context measurements, budget cells, relevant-file summary, and fixed interpretation limits. The execution artifact is `context-window-scaling-execution.json` (`my-dev-kit-lab-context-window-scaling-execution-v1`) and does not embed context text.
622
622
 
623
- **Synthetic-repository runs (v0.7.1).** The synthetic input introduces no new scientific metric: fit, utilization, correctness, success, and omitted-relevant-file semantics, the execution artifact schema, the report schema, and the plot set are identical to the bundled-corpus run. Each generated repository's `synthetic-repository-manifest.json` records generation evidence (per-file byte length, character count, SHA-256, generated-content estimated tokens, aggregate totals, and `repositoryContentIdentity`); these are reproducibility measurements, not context-window outcome metrics. The raw-full-file treatment context is measured by the existing baseline, which adds file headers and separators, so its estimated tokens differ from the manifest's generated-content estimate. Retrieval precision/recall remains v0.8.0 scope.
623
+ **Synthetic-repository runs (v0.7.1).** The synthetic input introduces no new scientific metric: fit, utilization, correctness, success, and omitted-relevant-file semantics, the execution artifact schema, the report schema, and the plot set are identical to the bundled-corpus run. Each generated repository's `synthetic-repository-manifest.json` records generation evidence (per-file byte length, character count, SHA-256, generated-content estimated tokens, aggregate totals, and `repositoryContentIdentity`); these are reproducibility measurements, not context-window outcome metrics. The raw-full-file treatment context is measured by the existing baseline, which adds file headers and separators, so its estimated tokens differ from the manifest's generated-content estimate. Retrieval precision/recall is documented separately in the v0.8.0 section below.
624
624
 
625
625
  **Local-repository runs (v0.7.2).** Running over an explicitly selected local Git repository introduces no new scientific metric: fit, utilization, correctness, success, and omitted-relevant-file semantics, the execution artifact and report schemas, and the plot set are the same. In this mode file identity lists are replaced by numbered placeholders (`<redacted file N>`) with the same length as the real list, and each treatment carries `fileIdentityRedaction: "redacted"`; a placeholder is a withheld identity, not an empty result, and an empty list or `null` keeps its usual meaning. Operational compatibility with a repository, the 1 MiB file-size limit, and the Git-ignore exclusions are safety policy, not quality measures: they do not imply retrieval precision, recall, ranking quality, or a retrieval winner.
626
626
 
@@ -691,3 +691,33 @@ Availability: the first three are always present for a validated incremental tre
691
691
  `no-observed-regression-relative-to-full` means only that the executed scenario showed no regression under those two evidence dimensions. It does not establish equivalence or safety and does not show that full refresh is unnecessary. `not-comparable-as-partial-refresh` says the run gives no evidence about the requested partial behavior. There is no winner, ranking, composite score, safety score, safe-to-skip-refresh flag, or stale-risk percentage. Ordinary per-treatment correctness `score` remains the existing deterministic fake-agent evidence.
692
692
 
693
693
  **Report presentation.** `report.incrementalChangeStaleness` (schema `my-dev-kit-lab-incremental-change-staleness-report-v2`) holds the summary counts (including per-treatment applied-partial, fallback, and classification counts), per-scenario lifecycle, four treatments and three comparisons identified by explicit treatment IDs, and fixed limitations text. Unavailable evidence is written as `unavailable` or `not applicable`, never zero. Historical V1 reports keep their original two-treatment presentation.
694
+
695
+ ## Retrieval precision/recall evidence (v0.8.0)
696
+
697
+ Status: released (v0.8.0). The `retrieval-precision-recall` plugin measures one retrieval per case (the existing my-dev-kit search, lookup, slice, and source lifecycle) against a deterministic answer key. It invokes no agent, and every value below is calculated by `calculateRetrievalQualityMetrics` and persisted in `retrieval-precision-recall-execution.json`; reports render only the persisted values.
698
+
699
+ **Availability.** Each ratio metric is in exactly one of three states. `available` carries a numerator, denominator, and value. `not-applicable` means the denominator is zero, so the metric does not describe that case. `unavailable` means required evidence was missing, partial, or invalid, and carries a fixed reason. Unavailable and not-applicable are never converted to zero, and an `available` zero is a real measured zero.
700
+
701
+ **File precision.** `relevant retrieved files / retrieved files`. Available when the retrieval evidence is trustworthy and at least one file was retrieved. When no file was retrieved it is `not-applicable`, not zero.
702
+
703
+ **File recall.** `relevant retrieved files / expected files`. An available retrieval that returned nothing against a nonempty expected set has recall `0`. With no expected files it is `not-applicable`.
704
+
705
+ **Symbol precision and recall.** The same set formulas over exact, case-sensitive unique symbol names: precision is `relevant retrieved symbol names / retrieved symbol names`, and recall is `relevant retrieved symbol names / expected symbol names`. There is no fuzzy or semantic matching, and several retrieved identities with the same name count once.
706
+
707
+ **Fact coverage.** `covered expected facts / all expected facts`, unweighted. A fact is covered only through the answer key's explicit `factIds`-to-context mappings (`expectedContextTargets` entries that list the fact IDs they support): a target is satisfied when its file was retrieved and each of its symbols was retrieved from that file. A fact mapped to several required targets is covered only when all of them are satisfied (conjunctive). There is no fact-prose matching, no embeddings, and no model judge. If a required fact has no usable mapping, coverage is `unavailable` rather than guessed.
708
+
709
+ **Irrelevant context ratio.** `irrelevant retrieved files / retrieved files`. It is calculated over file identities only. When both are available it is the diagnostic complement of file precision, so it is not an independent ranking metric. It makes no claim about which tokens were relevant.
710
+
711
+ **Retrieved token count.** The existing selected retrieval context's `totalEstimatedTokens`, calculated with the repository's character-based estimator (`ceil(characters / 4)`). It is not provider telemetry, not billing tokens, and not a per-file relevant-token attribution.
712
+
713
+ **Missed context.** Reported as three separate lists, never combined into a mixed-unit score: missed files, missed symbols, and uncovered fact IDs (each with a count).
714
+
715
+ **Aggregation.** `aggregateRetrievalPrecisionRecall` takes macro means over available per-case values only. An available zero stays in the mean; unavailable and not-applicable cases are counted separately and excluded from it. The aggregate has no weighting, score, winner, ranking, or threshold.
716
+
717
+ **External-local runs.** Numeric values and counts are identical in external-local mode. File, symbol, and fact identity lists are replaced by numbered placeholders of the same length (`<redacted file N>`, `<redacted symbol N>`, `<redacted fact N>`), case titles and retrieval warning text are replaced by fixed placeholders, and the artifact carries a fixed identity-redaction marker. A placeholder is a withheld identity, not an empty result.
718
+
719
+ **Not implemented in v0.8.0.** Ranked retrieval metrics such as MRR, NDCG, MAP, and ranked precision; retrieval-strategy comparison and winners; and query-strategy comparison. Those remain future scope.
720
+
721
+ **Sources.** Standard set-based precision and recall come from established information-retrieval evaluation: Manning, Raghavan, and Schütze, *Introduction to Information Retrieval*, "Evaluation of unranked retrieval sets" (Stanford online edition, <https://nlp.stanford.edu/IR-book/html/htmledition/evaluation-of-unranked-retrieval-sets-1.html>), and the National Institute of Standards and Technology's Text REtrieval Conference (TREC) material on relevance judgments, which treats relevance as a judged, answer-key property of a test collection (<https://trec.nist.gov/data/reljudge_eng.html>; program overview at <https://trec.nist.gov/>). Applying those formulas to file identities and exact symbol-name identities is this Lab's deterministic operationalization. Fact coverage (through the Lab's explicit `factIds` mapping contract) and irrelevant context ratio are Lab-specific deterministic diagnostics, not standardized TREC ranking metrics, and neither source defines the `factIds` contract.
722
+
723
+ See [COMMANDS.md](COMMANDS.md#retrieval-precision-recall-v080), [WORKFLOWS.md](WORKFLOWS.md#retrieval-precision-recall-experiment-v080), and [ARCHITECTURE.md](ARCHITECTURE.md#retrieval-precisionrecall-architecture-v080).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dailephd/my-dev-kit-lab",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "description": "Evidence, benchmark, and evaluation companion for my-dev-kit.",
6
6
  "bin": {