delivery-friction-analyzer 0.27.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.27.0",
3
+ "version": "0.28.1",
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-07-05 — Dry-Run Short-Sample Explanations
6
+
7
+ - What changed: Live dry-run completion now explains why fewer pull requests were sampled than requested when the analyzer has enough evidence, or says the reason is unknown when it cannot prove the cause.
8
+ - Why it matters: First-time users and maintainers can interpret short coverage probes without assuming the repository has too few eligible PRs, filters were applied, or GitHub data was complete.
9
+ - Who is affected: Users running live GitHub `--dry-run` or `--metadata-only` checks, especially with requested limits larger than the collected sample.
10
+ - Action needed: None.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/75
12
+
5
13
  ### 2026-07-04 — Guided Setup Guard Clarity
6
14
 
7
15
  - What changed: Interactive setup now stops as soon as users enter this tool's product repository, explains that no GitHub data was collected and no files were written, and keeps Conventional Commit PR class setup as a short prompt with detailed `?` help.
@@ -1868,7 +1868,7 @@ export async function runAnalyzeGithub(options, {
1868
1868
  paths: generatedPaths,
1869
1869
  sourceBundle,
1870
1870
  requestedLimit: options.limit,
1871
- sampledLimit: collectionLimit,
1871
+ sampledLimit: sourceBundle.selection?.collectedCount ?? collectionLimit,
1872
1872
  csv: false,
1873
1873
  analysisFilter: requestedAnalysisFilter(options.excludedPrClasses ?? []),
1874
1874
  savedProfilePath: options.savedProfilePath,
@@ -1996,6 +1996,81 @@ function coverageCaveats(coverage) {
1996
1996
  });
1997
1997
  }
1998
1998
 
1999
+ function dryRunCollectedCount(result) {
2000
+ const count = result.selection?.collectedCount;
2001
+ return Number.isInteger(count) ? count : result.sampledLimit;
2002
+ }
2003
+
2004
+ function relevantShortSampleCoverageStatus(coverage) {
2005
+ const relevantFamilies = new Set([
2006
+ "pull_request_inventory",
2007
+ "pull_request_details",
2008
+ "review_threads",
2009
+ "workflow_runs",
2010
+ ]);
2011
+ const statuses = (coverage?.sourceFamilies ?? []).filter(family => (
2012
+ relevantFamilies.has(family.family)
2013
+ && family.status
2014
+ && family.status !== "available"
2015
+ )).map(family => family.status);
2016
+
2017
+ if (!statuses.length) return null;
2018
+ if (statuses.includes("partial") && statuses.includes("unavailable")) return "incomplete";
2019
+ if (statuses.includes("partial")) return "partial";
2020
+ if (statuses.includes("unavailable")) return "unavailable";
2021
+ return "incomplete";
2022
+ }
2023
+
2024
+ function pullRequestCountLabel(count) {
2025
+ return count === 1 ? "one pull request" : `${count} pull requests`;
2026
+ }
2027
+
2028
+ function dryRunShortSampleExplanation(result) {
2029
+ if (!result.dryRun) return null;
2030
+ const requestedCount = result.requestedLimit;
2031
+ const collectedCount = dryRunCollectedCount(result);
2032
+ if (!Number.isInteger(requestedCount)
2033
+ || !Number.isInteger(collectedCount)
2034
+ || collectedCount >= requestedCount) {
2035
+ return null;
2036
+ }
2037
+
2038
+ if (Number.isInteger(result.analysisFilter?.originalPullRequests)
2039
+ && Number.isInteger(result.analysisFilter?.filteredPullRequests)
2040
+ && result.analysisFilter.originalPullRequests >= requestedCount
2041
+ && result.analysisFilter.filteredPullRequests < requestedCount) {
2042
+ return `Sampled ${collectedCount} of ${requestedCount} requested after applying the requested PR class filter.`;
2043
+ }
2044
+
2045
+ if (Number.isInteger(result.selection?.requestedLimit)
2046
+ && Number.isInteger(result.selection?.collectedCount)
2047
+ && result.selection.collectedCount < result.selection.requestedLimit) {
2048
+ return `Sampled ${collectedCount} of ${requestedCount} requested because GitHub returned ${result.selection.collectedCount} merged pull request(s) for this query.`;
2049
+ }
2050
+
2051
+ if (Number.isInteger(result.selection?.requestedLimit)
2052
+ && result.selection.requestedLimit < requestedCount
2053
+ && collectedCount >= result.selection.requestedLimit) {
2054
+ return `Sampled ${collectedCount} of ${requestedCount} requested because dry-run mode intentionally checks ${pullRequestCountLabel(result.selection.requestedLimit)} to verify GitHub access and coverage without writing artifacts.`;
2055
+ }
2056
+
2057
+ const relevantCoverageStatus = relevantShortSampleCoverageStatus(result.collectionCoverage);
2058
+ if (relevantCoverageStatus === "partial") {
2059
+ return `Sampled ${collectedCount} of ${requestedCount} requested. Collection coverage was partial; check the coverage details before treating the sample as complete.`;
2060
+ }
2061
+ if (relevantCoverageStatus === "unavailable") {
2062
+ return `Sampled ${collectedCount} of ${requestedCount} requested. Collection coverage was unavailable for one or more relevant sources; check the coverage details before treating the sample as complete.`;
2063
+ }
2064
+ if (relevantCoverageStatus === "incomplete") {
2065
+ return `Sampled ${collectedCount} of ${requestedCount} requested. Collection coverage was incomplete; check the coverage details before treating the sample as complete.`;
2066
+ }
2067
+
2068
+ const eligiblePullRequests = collectedCount === 1
2069
+ ? "one eligible pull request"
2070
+ : `${collectedCount} eligible pull requests`;
2071
+ return `Sampled ${collectedCount} of ${requestedCount} requested. The analyzer could not determine the reason from available dry-run evidence; check coverage details before assuming the repository has only ${eligiblePullRequests}.`;
2072
+ }
2073
+
1999
2074
  export function formatAnalyzeGithubCompletion(result) {
2000
2075
  const target = result.targetRepository?.owner && result.targetRepository?.name
2001
2076
  ? `${result.targetRepository.owner}/${result.targetRepository.name}`
@@ -2005,9 +2080,13 @@ export function formatAnalyzeGithubCompletion(result) {
2005
2080
  if (result.dryRun) {
2006
2081
  lines.push(
2007
2082
  `Dry run complete for ${target}.`,
2008
- `Sampled pull requests: ${result.sampledLimit} of ${result.requestedLimit} requested.`,
2083
+ `Sampled pull requests: ${dryRunCollectedCount(result)} of ${result.requestedLimit} requested.`,
2009
2084
  "Artifacts: not written.",
2010
2085
  );
2086
+ const shortSampleExplanation = dryRunShortSampleExplanation(result);
2087
+ if (shortSampleExplanation) {
2088
+ lines.push(shortSampleExplanation);
2089
+ }
2011
2090
  } else {
2012
2091
  const paths = result.artifactPaths ?? {};
2013
2092
  lines.push(