delivery-friction-analyzer 0.23.1 → 0.24.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.
@@ -58,14 +58,15 @@ The Markdown renderer presents the same report data for human review:
58
58
 
59
59
  - explicit analysis filter labels when downstream artifacts were generated from a filtered PR class sample;
60
60
  - executive summary totals plus top findings, triggered recommendation categories, and filter status in a table;
61
- - a top-of-report focus snapshot that names focus areas, action categories, evidence reviewed, and confidence caveats before detailed bottlenecks;
61
+ - a top-of-report focus snapshot that names focus areas, action categories, evidence reviewed, and points to grouped confidence caveats before detailed bottlenecks;
62
+ - a top-level confidence digest table adjacent to the focus snapshot that groups decision-affecting caveats by driver, including partial coverage, dominant PRs, dominant PR classes, and shared evidence when present;
62
63
  - a compact recommendation-category snapshot before detailed bottlenecks, with the full category reference retained later in the report;
63
64
  - a short "How To Read This Report" guide that distinguishes observed evidence, interpretation, recommendations, and caveats;
64
65
  - a configured workflow context section only when repository profile workflow fields are present, labeled as user-configured profile context rather than observed GitHub evidence;
65
66
  - a contributor source context section only when a contributor source is configured, labeled as metadata that may improve comment-source classification coverage without changing scores, authorship conclusions, reviewer attribution, CSV export shape, person-level CSV output, or individual ranking guardrails;
66
67
  - workflow data caveats when configured workflow context clarifies unavailable PR-open diff or workflow-run evidence;
67
68
  - evidence-quality and coverage tables before detailed recommendations;
68
- - key findings that highlight top bottlenecks, strongest displayed signal, outlier caveats, PR class caveats, and coverage caveats;
69
+ - key findings that highlight top bottlenecks, strongest displayed signal, and route readers to the confidence digest instead of repeating sample-level caveat prose;
69
70
  - a PR class context table that shows analyzed PR counts, changed lines, sample share, and classification sources by class;
70
71
  - profile suggestions when fallback `unknown` PR classes, unknown file role/surface evidence, or omitted workflow context with relevant unavailable coverage cross deterministic thresholds;
71
72
  - a top-level shared-signal interpretation callout when multiple displayed bottlenecks share a ranking key or representative PR evidence;
@@ -76,6 +77,7 @@ The Markdown renderer presents the same report data for human review:
76
77
  - validation, review, and source-label evidence for representative PR examples rendered as compact evidence tables;
77
78
  - text-backed status labels such as observed, partial, unavailable, configured, warning, and healthy in Markdown evidence tables where they improve scanability;
78
79
  - separately labeled inferred diagnosis, suggested action, and confidence/caveat blocks for each bottleneck;
80
+ - concise confidence/caveat links from per-bottleneck blocks to the confidence digest, PR class context, shared-signal interpretation, or sensitivity table when the same sample-level caveat is already grouped at the top of the report;
79
81
  - shared-evidence notes when multiple recommendation categories use the same representative PR set;
80
82
  - recommendation-category, comment-source, and core/support-surface tables;
81
83
  - a concise methodology summary;
@@ -106,6 +108,15 @@ The M3 report contract supports these recommendation categories:
106
108
 
107
109
  Reports must label unavailable or partial source evidence instead of inferring unavailable values from merge-time data. Final/current PR metadata can come from source-bundle PR evidence, but PR-open diff growth remains unavailable unless an open-time snapshot or equivalent captured state exists. Workflow coverage and review-thread sources are summarized separately.
108
110
 
111
+ The confidence digest is Markdown presentation only, not a `friction-report.v1` JSON field. It must be derived from existing report data and should act as top-level routing for decisions:
112
+
113
+ - partial coverage rows state affected source families with available/unavailable counts when those counts are present, plus the user impact of using available evidence only;
114
+ - dominant PR rows group affected bottlenecks by PR and distinguish focus areas from other affected signals using `summary.topBottleneckIds`;
115
+ - dominant PR-class rows group affected bottlenecks by class, show contribution values or ranges, state the class sample size, and point to PR class context or filtering as the next check;
116
+ - shared-evidence rows render only when shared ranking-key or representative-evidence groups exist, and point to shared-signal interpretation before readers treat affected recommendations as independent findings.
117
+
118
+ The digest is not a replacement for audit trails. Evidence-quality coverage tables, workflow data caveats, PR class context, shared-signal interpretation, outlier/sensitivity analysis, per-bottleneck evidence tables, methodology summaries, and companion CSV/methodology artifacts remain the detailed sources for verification.
119
+
109
120
  Representative examples should carry enough source evidence to trace a report claim back to generated artifacts. Validation examples should name the workflow-run source and conclusions. Review churn examples should name the review-thread source, review decision evidence, and comment sources. PR class evidence should be visible in representative bottleneck examples so readers can distinguish workflow populations such as release, dependency, development, or repository-specific classes. When `reviewThreads` is zero, review decision evidence should make clean human approval distinguishable from unavailable review evidence and from observed absence of human review. When displayed examples are dominated by one PR or one PR class, the report should say so instead of implying a repository-wide pattern from an outlier or workflow population.
110
121
 
111
122
  Shared-signal groups are interpretation support only. The report may group bottlenecks by the metric ranking key they use or by identical displayed representative PR sets, but it must still preserve each bottleneck and its recommendation category so readers can distinguish one underlying signal from different possible actions.
@@ -2,6 +2,6 @@
2
2
 
3
3
  Source: Bundled synthetic sample, not live GitHub data.
4
4
 
5
- The generated sample report opens by pointing readers to Change scope, Review churn, and Repo guidance gap. It also shows why the first findings should not be overgeneralized: PR #104 is intentionally broad and outlier-sensitive, PR-open diff coverage is partial, and one PR has unavailable workflow-run history.
5
+ The generated sample report opens by pointing readers to Change scope, Review churn, and Repo guidance gap. Its Confidence Digest groups why the first findings should not be overgeneralized: PR #104 is intentionally broad and outlier-sensitive, feature-class PRs drive much of the displayed signal, PR-open diff coverage is partial, and one PR has unavailable workflow-run history.
6
6
 
7
7
  Use the excerpt in public docs to set expectations: the sample is synthetic tutorial evidence, not observed GitHub data, and its value is showing how caveats, profile classifications, PR classes, workflow context, and CSV evidence appear in a local report bundle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.23.1",
3
+ "version": "0.24.0",
4
4
  "description": "Local GitHub pull request analytics for delivery friction reports.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/release-log.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ### 2026-06-29 — Report Confidence Digest Usability
6
+
7
+ - What changed: Generated Markdown reports now keep edge-case confidence caveats visible in the top digest, including review-thread-only coverage gaps, one-class PR-class comparison limits, and correctly ordered contribution ranges.
8
+ - Why it matters: Maintainers can see which caveats should affect report interpretation before acting on top findings, without losing the detailed audit sections.
9
+ - Who is affected: Maintainers and users reading generated Markdown friction reports.
10
+ - Action needed: None.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/71
12
+
5
13
  ### 2026-06-28 — Low-Signal Report Quieting
6
14
 
7
15
  - What changed: Friction report JSON and Markdown now separate zero-score or no-positive-evidence bottlenecks into no-signal context, and completion receipts expose matching `noSignalBottleneckIds` for report-generating runs while leaving coverage-only runs unranked.
@@ -1401,51 +1401,29 @@ function renderContributorSourceContext(contributorSource) {
1401
1401
  ].join("\n");
1402
1402
  }
1403
1403
 
1404
- function renderKeyFindings(report) {
1404
+ function renderKeyFindings(report, sharedSignals = report.sharedSignals) {
1405
1405
  const topBottlenecks = topBottleneckLabels(report);
1406
1406
  const strongest = report.bottlenecks?.[0];
1407
- const dominanceCallouts = (report.bottlenecks ?? [])
1408
- .filter(bottleneck => bottleneck.dominance?.status === "single_pr_dominates")
1409
- .map(bottleneck => `${bottleneck.title}: ${bottleneck.dominance.note}`);
1410
- const classDominanceCallouts = (report.bottlenecks ?? [])
1411
- .filter(bottleneck => bottleneck.classDominance?.status === "single_class_dominates")
1412
- .map(bottleneck => `${bottleneck.title}: ${bottleneck.classDominance.note}`);
1413
- const coverageNotes = report.coverage?.notes?.length
1414
- ? report.coverage.notes.join(" ")
1415
- : "No coverage caveats were recorded for the displayed evidence.";
1407
+ const digestRows = confidenceDigestRows(report, sharedSignals);
1416
1408
 
1417
1409
  return renderList([
1418
1410
  `Top bottlenecks: ${topBottlenecks}.`,
1419
1411
  strongest
1420
1412
  ? `Strongest displayed signal: ${strongest.title} (${strongest.metricLabel}).`
1421
1413
  : "No bottleneck evidence was available.",
1422
- dominanceCallouts.length
1423
- ? `Outlier caveat: ${dominanceCallouts.join(" ")}`
1424
- : "Outlier caveat: displayed bottleneck examples are not dominated by a single PR.",
1425
- classDominanceCallouts.length
1426
- ? `PR class caveat: ${classDominanceCallouts.join(" ")}`
1427
- : classDominanceFallback(report.prClasses),
1428
- `Coverage caveat: ${coverageNotes}`,
1414
+ digestRows.length
1415
+ ? "Confidence digest: review the grouped caveat drivers below before generalizing from the top findings."
1416
+ : "Confidence digest: no early confidence caveats were recorded for the displayed evidence.",
1429
1417
  ]);
1430
1418
  }
1431
1419
 
1432
- function summarizeFocusCaveats(report) {
1433
- const caveats = [];
1434
- const coverageNotes = report.coverage?.notes ?? [];
1435
- const dominantPrs = (report.bottlenecks ?? [])
1436
- .filter(bottleneck => bottleneck.dominance?.status === "single_pr_dominates");
1437
- const dominantClasses = (report.bottlenecks ?? [])
1438
- .filter(bottleneck => bottleneck.classDominance?.status === "single_class_dominates");
1439
-
1440
- if (coverageNotes.length) caveats.push(formatCount(coverageNotes.length, "coverage caveat"));
1441
- if (dominantPrs.length) caveats.push(formatCount(dominantPrs.length, "outlier caveat"));
1442
- if (dominantClasses.length) caveats.push(formatCount(dominantClasses.length, "PR class caveat"));
1443
- if (!caveats.length) return "No early confidence caveats were recorded for the displayed evidence.";
1444
-
1445
- return `${caveats.join(", ")}. Read the evidence and caveat sections before generalizing.`;
1420
+ function summarizeFocusCaveats(report, sharedSignals = report.sharedSignals) {
1421
+ const rows = confidenceDigestRows(report, sharedSignals);
1422
+ if (!rows.length) return "No early confidence caveats were recorded for the displayed evidence.";
1423
+ return `Confidence Digest groups ${formatCount(rows.length, "digest row")} by caveat group. Read it before acting on top findings.`;
1446
1424
  }
1447
1425
 
1448
- function renderFocusSnapshot(report) {
1426
+ function renderFocusSnapshot(report, sharedSignals = report.sharedSignals) {
1449
1427
  const summary = report.summary ?? {};
1450
1428
  const categories = triggeredCategoryLabels(report);
1451
1429
  const evidenceReviewed = [
@@ -1468,19 +1446,203 @@ function renderFocusSnapshot(report) {
1468
1446
  ["Focus first", firstInspection],
1469
1447
  ["Action categories", categories],
1470
1448
  ["Evidence reviewed", evidenceReviewed],
1471
- ["Confidence caveats", summarizeFocusCaveats(report)],
1449
+ ["Confidence caveats", summarizeFocusCaveats(report, sharedSignals)],
1472
1450
  ],
1473
1451
  );
1474
1452
  }
1475
1453
 
1476
- function classDominanceFallback(prClasses) {
1477
- const sampleClasses = (prClasses?.distribution ?? []).filter(entry => entry.pullRequests > 0);
1478
- if (!sampleClasses.length) {
1479
- return "PR class caveat: PR class context was not available for the analyzed sample.";
1454
+ function focusBottleneckIdSet(report) {
1455
+ return new Set(report.summary?.topBottleneckIds ?? []);
1456
+ }
1457
+
1458
+ function affectedBottleneckLabel(report, bottlenecks) {
1459
+ const focusIds = focusBottleneckIdSet(report);
1460
+ const focus = [];
1461
+ const supporting = [];
1462
+ for (const bottleneck of bottlenecks) {
1463
+ if (focusIds.has(bottleneck.id)) focus.push(bottleneck.title);
1464
+ else supporting.push(bottleneck.title);
1465
+ }
1466
+
1467
+ const parts = [];
1468
+ if (focus.length) parts.push(`Focus areas: ${focus.join(", ")}`);
1469
+ if (supporting.length) parts.push(`Other affected signals: ${supporting.join(", ")}`);
1470
+ return parts.join("; ") || "No displayed bottlenecks";
1471
+ }
1472
+
1473
+ function availableCoverageCount(entries = {}) {
1474
+ return Object.entries(entries)
1475
+ .filter(([name]) => name !== "unavailable")
1476
+ .reduce((sum, [, value]) => sum + Number(value ?? 0), 0);
1477
+ }
1478
+
1479
+ function totalCoverageCount(entries = {}) {
1480
+ return Object.values(entries).reduce((sum, value) => sum + Number(value ?? 0), 0);
1481
+ }
1482
+
1483
+ function coverageDigestPart(label, entries = {}) {
1484
+ const unavailable = Number(entries.unavailable ?? 0);
1485
+ const total = totalCoverageCount(entries);
1486
+ if (!unavailable || !total) return null;
1487
+ return `${label} ${availableCoverageCount(entries)}/${total} available (${unavailable} unavailable)`;
1488
+ }
1489
+
1490
+ function coverageDigestRow(report) {
1491
+ const parts = [
1492
+ coverageDigestPart("PR-open diff", report.coverage?.prOpenDiff),
1493
+ coverageDigestPart("workflow runs", report.coverage?.workflowRuns),
1494
+ coverageDigestPart("review threads", report.coverage?.reviewThreads),
1495
+ ].filter(Boolean);
1496
+ if (!parts.length) return null;
1497
+
1498
+ const families = [];
1499
+ if (Number(report.coverage?.prOpenDiff?.unavailable ?? 0) > 0) families.push("diff-growth");
1500
+ if (Number(report.coverage?.workflowRuns?.unavailable ?? 0) > 0) families.push("validation");
1501
+ if (Number(report.coverage?.reviewThreads?.unavailable ?? 0) > 0) families.push("review-thread");
1502
+
1503
+ return [
1504
+ "Partial coverage",
1505
+ parts.join("; "),
1506
+ `${families.length ? `${formatInlineList(families)} signals` : "Displayed signals"} use available evidence only.`,
1507
+ "Check Evidence Quality And Coverage and `methodology.md`/`collection-coverage.csv` before comparing trends.",
1508
+ ];
1509
+ }
1510
+
1511
+ function formatInlineList(items) {
1512
+ if (items.length < 3) return items.join(" and ");
1513
+ return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
1514
+ }
1515
+
1516
+ function formatShareRange(shares) {
1517
+ const labels = [...new Set(
1518
+ shares
1519
+ .map(share => Number(share ?? 0))
1520
+ .sort((left, right) => left - right)
1521
+ .map(classDominancePercentageLabel),
1522
+ )];
1523
+ return labels.length === 1 ? labels[0] : `${labels[0]}-${labels[labels.length - 1]}`;
1524
+ }
1525
+
1526
+ function dominantPrDigestRows(report) {
1527
+ const groups = new Map();
1528
+ for (const bottleneck of report.bottlenecks ?? []) {
1529
+ if (bottleneck.dominance?.status !== "single_pr_dominates") continue;
1530
+ const prNumber = bottleneck.dominance.topPrNumber;
1531
+ if (!Number.isInteger(prNumber)) continue;
1532
+ const current = groups.get(prNumber) ?? { prNumber, bottlenecks: [], shares: [] };
1533
+ current.bottlenecks.push(bottleneck);
1534
+ current.shares.push(Number(bottleneck.dominance.topShare ?? 0));
1535
+ groups.set(prNumber, current);
1536
+ }
1537
+
1538
+ const sensitivityByPr = new Map(
1539
+ (report.sensitivity?.summaries ?? []).map(summary => [summary.excludedPr.number, summary]),
1540
+ );
1541
+
1542
+ return [...groups.values()]
1543
+ .sort((left, right) => left.prNumber - right.prNumber)
1544
+ .map(group => {
1545
+ const sensitivity = sensitivityByPr.get(group.prNumber);
1546
+ const prLabel = sensitivity?.excludedPr?.url
1547
+ ? `[PR #${group.prNumber}](${sensitivity.excludedPr.url})`
1548
+ : `PR #${group.prNumber}`;
1549
+ return [
1550
+ "Dominant PR",
1551
+ affectedBottleneckLabel(report, group.bottlenecks),
1552
+ rawMarkdownCell(`${prLabel} contributes ${formatShareRange(group.shares)} of displayed signal; ${sensitivity?.changedTopBottlenecks ? "top focus ordering changes without it" : "top focus ordering is unchanged without it"}.`),
1553
+ `Inspect PR #${group.prNumber} and Outlier And Sensitivity Analysis before changing process.`,
1554
+ ];
1555
+ });
1556
+ }
1557
+
1558
+ function dominantClassDigestRows(report) {
1559
+ const groups = new Map();
1560
+ for (const bottleneck of report.bottlenecks ?? []) {
1561
+ if (bottleneck.classDominance?.status !== "single_class_dominates") continue;
1562
+ const className = bottleneck.classDominance.class ?? "unknown";
1563
+ const current = groups.get(className) ?? { className, bottlenecks: [], shares: [], bases: new Set() };
1564
+ current.bottlenecks.push(bottleneck);
1565
+ current.shares.push(Number(bottleneck.classDominance.topShare ?? 0));
1566
+ current.bases.add(bottleneck.classDominance.basis === "score_value" ? "displayed score value" : "displayed example count");
1567
+ groups.set(className, current);
1568
+ }
1569
+
1570
+ return [...groups.values()]
1571
+ .sort((left, right) => left.className.localeCompare(right.className))
1572
+ .map(group => {
1573
+ const distribution = (report.prClasses?.distribution ?? [])
1574
+ .find(entry => entry.class === group.className);
1575
+ const sampleSize = Number(distribution?.pullRequests ?? group.bottlenecks[0]?.classDominance?.samplePullRequests ?? 0);
1576
+ const basisLabel = [...group.bases].join(" and ");
1577
+ return [
1578
+ "Dominant PR class",
1579
+ affectedBottleneckLabel(report, group.bottlenecks),
1580
+ `${group.className} class drives ${formatShareRange(group.shares)} of ${basisLabel}; class sample size is ${formatCount(sampleSize, "PR")}.`,
1581
+ "Compare against PR Class Context or rerun with class filters before generalizing.",
1582
+ ];
1583
+ });
1584
+ }
1585
+
1586
+ function classComparisonLimitedDigestRows(report) {
1587
+ const distribution = report.prClasses?.distribution ?? [];
1588
+ if (distribution.length !== 1) return [];
1589
+ const onlyClass = distribution[0];
1590
+ const bottlenecks = (report.bottlenecks ?? [])
1591
+ .filter(bottleneck => bottleneck.classDominance?.status === "not_applicable");
1592
+ if (!bottlenecks.length) return [];
1593
+
1594
+ return [[
1595
+ "PR class comparison limited",
1596
+ affectedBottleneckLabel(report, bottlenecks),
1597
+ `${onlyClass.class} is the only PR class in the analyzed sample; class comparison is not meaningful across ${formatCount(Number(onlyClass.pullRequests ?? 0), "PR")}.`,
1598
+ "Compare against PR Class Context or rerun with a broader class mix before generalizing by PR class.",
1599
+ ]];
1600
+ }
1601
+
1602
+ function sharedSignalDigestRow(report, sharedSignals) {
1603
+ const groups = sharedSignals?.groups ?? [];
1604
+ if (!groups.length) return null;
1605
+ const bottlenecksById = new Map((report.bottlenecks ?? []).map(bottleneck => [bottleneck.id, bottleneck]));
1606
+ const affected = new Map();
1607
+ for (const group of groups) {
1608
+ for (const bottleneck of group.bottlenecks ?? []) {
1609
+ const source = bottlenecksById.get(bottleneck.id);
1610
+ if (source) affected.set(source.id, source);
1611
+ }
1480
1612
  }
1481
- return sampleClasses.length < 2
1482
- ? "PR class caveat: only one PR class appears in the analyzed sample, so class dominance comparison is not meaningful."
1483
- : "PR class caveat: displayed bottleneck examples are not dominated by one PR class.";
1613
+ return [
1614
+ "Shared evidence",
1615
+ affectedBottleneckLabel(report, [...affected.values()]),
1616
+ `${formatCount(groups.length, "shared-signal group")} means some recommendations interpret the same metric or representative PR evidence.`,
1617
+ "Read Shared Signal Interpretation before treating affected recommendations as independent findings.",
1618
+ ];
1619
+ }
1620
+
1621
+ function confidenceDigestRows(report, sharedSignals = report.sharedSignals) {
1622
+ return [
1623
+ coverageDigestRow(report),
1624
+ ...dominantPrDigestRows(report),
1625
+ ...dominantClassDigestRows(report),
1626
+ ...classComparisonLimitedDigestRows(report),
1627
+ sharedSignalDigestRow(report, sharedSignals),
1628
+ ].filter(Boolean);
1629
+ }
1630
+
1631
+ function renderConfidenceDigest(report, sharedSignals) {
1632
+ const rows = confidenceDigestRows(report, sharedSignals);
1633
+ if (!rows.length) return "";
1634
+
1635
+ return [
1636
+ "## Confidence Digest",
1637
+ "",
1638
+ "Top-level routing for caveats that can change how you act on the findings; detailed audit trails remain in the sections below.",
1639
+ "",
1640
+ renderMarkdownTable(
1641
+ ["Caveat driver", "Affects", "Why it matters", "Next check"],
1642
+ rows,
1643
+ ),
1644
+ "",
1645
+ ].join("\n");
1484
1646
  }
1485
1647
 
1486
1648
  function renderPrClassContext(prClasses) {
@@ -1556,10 +1718,23 @@ function renderWorkflowDataCaveats(report) {
1556
1718
  ].join("\n");
1557
1719
  }
1558
1720
 
1559
- function classDominanceCaveat(bottleneck) {
1560
- return bottleneck.classDominance?.status === "single_class_dominates"
1561
- ? bottleneck.classDominance.note
1562
- : null;
1721
+ function renderBottleneckCaveats(bottleneck, sharedNotes) {
1722
+ const caveats = [];
1723
+ if (bottleneck.dominance?.status === "single_pr_dominates") {
1724
+ caveats.push("See [Confidence Digest](#confidence-digest) for dominant PR context; inspect this bottleneck's evidence rows before generalizing.");
1725
+ } else {
1726
+ caveats.push(bottleneck.dominance?.note ?? "Not enough positive examples to evaluate outlier dominance.");
1727
+ }
1728
+
1729
+ if (bottleneck.classDominance?.status === "single_class_dominates") {
1730
+ caveats.push("See [Confidence Digest](#confidence-digest) and [PR Class Context](#pr-class-context) for repeated class-dominance context.");
1731
+ }
1732
+
1733
+ if (sharedNotes.get(bottleneck.id)) {
1734
+ caveats.push("See [Confidence Digest](#confidence-digest) and [Shared Signal Interpretation](#shared-signal-interpretation) for shared-evidence context.");
1735
+ }
1736
+
1737
+ return renderList(caveats);
1563
1738
  }
1564
1739
 
1565
1740
  function renderSharedSignalInterpretation(sharedSignals) {
@@ -1694,8 +1869,9 @@ export function renderRepositoryFrictionMarkdown(report) {
1694
1869
  "",
1695
1870
  "## Focus Snapshot",
1696
1871
  "",
1697
- renderFocusSnapshot(report),
1872
+ renderFocusSnapshot(report, sharedSignals),
1698
1873
  "",
1874
+ renderConfidenceDigest(report, sharedSignals),
1699
1875
  "## Recommendation Category Snapshot",
1700
1876
  "",
1701
1877
  renderRecommendationCategorySnapshot(report.recommendationCategories ?? []),
@@ -1729,7 +1905,7 @@ export function renderRepositoryFrictionMarkdown(report) {
1729
1905
  "",
1730
1906
  "## Key Findings",
1731
1907
  "",
1732
- renderKeyFindings(report),
1908
+ renderKeyFindings(report, sharedSignals),
1733
1909
  "",
1734
1910
  renderPrClassContext(report.prClasses),
1735
1911
  renderProfileSuggestions(report),
@@ -1761,11 +1937,7 @@ export function renderRepositoryFrictionMarkdown(report) {
1761
1937
  "",
1762
1938
  `#### ${bottleneck.title} Confidence And Caveats`,
1763
1939
  "",
1764
- renderList([
1765
- bottleneck.dominance?.note ?? "Not enough positive examples to evaluate outlier dominance.",
1766
- classDominanceCaveat(bottleneck),
1767
- sharedNotes.get(bottleneck.id),
1768
- ].filter(Boolean)),
1940
+ renderBottleneckCaveats(bottleneck, sharedNotes),
1769
1941
  "",
1770
1942
  );
1771
1943
  }