delivery-friction-analyzer 0.26.0 → 0.28.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.26.0",
3
+ "version": "0.28.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,22 @@
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
+
13
+ ### 2026-07-04 — Guided Setup Guard Clarity
14
+
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.
16
+ - Why it matters: First-time users and maintainers get the blocking self-analysis decision before answering unrelated setup questions, while PR class setup stays easier to answer without losing the guidance needed to avoid misleading classifications.
17
+ - Who is affected: Users running `--interactive` live GitHub setup, especially when they enter the analyzer's own repository or review the optional Conventional Commit PR class preset.
18
+ - Action needed: None.
19
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/74
20
+
5
21
  ### 2026-07-04 — Local Sample Checkout Command
6
22
 
7
23
  - What changed: Local checkouts now expose `npm run sample` for the bundled tutorial sample, while README guidance keeps the public `npx` package path first and shows checkout usage separately.
@@ -38,7 +38,8 @@ const PACKAGE_JSON_URL = new URL("../../package.json", import.meta.url);
38
38
  const SAMPLE_SOURCE_BUNDLE_URL = new URL("../../examples/tutorial/source-bundle.json", import.meta.url);
39
39
  const SAMPLE_PROFILE_URL = new URL("../../examples/tutorial/profile.json", import.meta.url);
40
40
  const STARTER_PROFILE_PROMPT_COPY = "Starter profiles are valid, but until you add rules the report may classify PR classes, file roles, and functional surfaces as unknown.";
41
- const CONVENTIONAL_COMMIT_PRESET_PROMPT_COPY = "Add Conventional Commit PR class rules?\nUse this when PR titles usually start with feat:, fix:, docs:, test:, chore(deps):, or similar prefixes.\nIt writes title-based dependency, feature, fix, docs, test, and maintenance classes so fewer PRs are classified as unknown.\nSkip it for release titles, ticket prefixes, free-form titles, or another custom PR taxonomy.\nIt does not change scoring, rankings, GitHub collection, or CSV export shape.";
41
+ const CONVENTIONAL_COMMIT_PRESET_PROMPT_COPY = "Add Conventional Commit PR class rules? Use this only when PR titles usually start with prefixes like feat:, fix:, docs:, test:, or chore(deps):.";
42
+ const CONVENTIONAL_COMMIT_PRESET_HELP_COPY = "Writes title-based dependency, feature, fix, docs, test, and maintenance PR class rules to the repository profile. Accept only when most PR titles use Conventional Commit prefixes such as feat:, fix:, docs:, test:, or chore(deps):. Skip for release titles, ticket prefixes, free-form titles, or another custom PR taxonomy. Existing PR class rules are kept. This changes profile interpretation only; it does not change scoring, rankings, GitHub collection, or CSV export shape.";
42
43
  const STARTER_PROFILE_COMPLETION_COPY = "Created a starter profile. Review or refine it before relying on report labels for PR classes, file roles, or functional surfaces.";
43
44
  const MISSING_PROFILE_GUIDANCE = `Live GitHub analysis needs a repository-profile.v1 JSON file.
44
45
  Run with --interactive to create a starter profile, or pass --profile path/to/profile.json.
@@ -1058,15 +1059,11 @@ function withAvailablePrClassRuleIds(profile, rules) {
1058
1059
  }
1059
1060
 
1060
1061
  async function promptConventionalCommitPrClassPreset(promptAdapter, output, profile) {
1061
- const hasExistingPrClasses = Array.isArray(profile.prClasses) && profile.prClasses.length > 0;
1062
- const message = hasExistingPrClasses
1063
- ? `${CONVENTIONAL_COMMIT_PRESET_PROMPT_COPY} Existing PR class rules will be kept.`
1064
- : CONVENTIONAL_COMMIT_PRESET_PROMPT_COPY;
1065
1062
  const shouldAddPreset = await askUntilValid(promptAdapter, {
1066
1063
  id: "addConventionalCommitPrClasses",
1067
1064
  type: "confirm",
1068
- message,
1069
- help: "Optional profile setup: add reusable title rules when Conventional Commit prefixes match this repository. This changes profile classification only, not scoring or GitHub collection.",
1065
+ message: CONVENTIONAL_COMMIT_PRESET_PROMPT_COPY,
1066
+ help: CONVENTIONAL_COMMIT_PRESET_HELP_COPY,
1070
1067
  defaultValue: false,
1071
1068
  }, {
1072
1069
  output,
@@ -1318,6 +1315,13 @@ function shouldPromptInteractiveOption(options, key) {
1318
1315
  return !hasOwnOption(options, key);
1319
1316
  }
1320
1317
 
1318
+ function rejectInteractiveProductRepository(repository, { productRepository, allowProductRepository = false } = {}) {
1319
+ const targetInput = targetInputFromRepositorySlug(repository);
1320
+ if (!allowProductRepository && isProductRepositoryTarget(targetInput, productRepository ?? DEFAULT_PRODUCT_REPOSITORY)) {
1321
+ throw new Error(productRepositoryTargetError(targetInput));
1322
+ }
1323
+ }
1324
+
1321
1325
  export async function collectInteractiveAnalyzeGithubOptions(options, {
1322
1326
  promptAdapter = null,
1323
1327
  input = process.stdin,
@@ -1350,6 +1354,10 @@ export async function collectInteractiveAnalyzeGithubOptions(options, {
1350
1354
  validate: validateRepositorySlug,
1351
1355
  });
1352
1356
  }
1357
+ rejectInteractiveProductRepository(resolved.repository, {
1358
+ productRepository: options.productRepository,
1359
+ allowProductRepository: options.allowProductRepository,
1360
+ });
1353
1361
 
1354
1362
  if (resolved.limit === undefined) {
1355
1363
  resolved.limit = await askUntilValid(adapter, {
@@ -1860,7 +1868,7 @@ export async function runAnalyzeGithub(options, {
1860
1868
  paths: generatedPaths,
1861
1869
  sourceBundle,
1862
1870
  requestedLimit: options.limit,
1863
- sampledLimit: collectionLimit,
1871
+ sampledLimit: sourceBundle.selection?.collectedCount ?? collectionLimit,
1864
1872
  csv: false,
1865
1873
  analysisFilter: requestedAnalysisFilter(options.excludedPrClasses ?? []),
1866
1874
  savedProfilePath: options.savedProfilePath,
@@ -1988,6 +1996,81 @@ function coverageCaveats(coverage) {
1988
1996
  });
1989
1997
  }
1990
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
+
1991
2074
  export function formatAnalyzeGithubCompletion(result) {
1992
2075
  const target = result.targetRepository?.owner && result.targetRepository?.name
1993
2076
  ? `${result.targetRepository.owner}/${result.targetRepository.name}`
@@ -1997,9 +2080,13 @@ export function formatAnalyzeGithubCompletion(result) {
1997
2080
  if (result.dryRun) {
1998
2081
  lines.push(
1999
2082
  `Dry run complete for ${target}.`,
2000
- `Sampled pull requests: ${result.sampledLimit} of ${result.requestedLimit} requested.`,
2083
+ `Sampled pull requests: ${dryRunCollectedCount(result)} of ${result.requestedLimit} requested.`,
2001
2084
  "Artifacts: not written.",
2002
2085
  );
2086
+ const shortSampleExplanation = dryRunShortSampleExplanation(result);
2087
+ if (shortSampleExplanation) {
2088
+ lines.push(shortSampleExplanation);
2089
+ }
2003
2090
  } else {
2004
2091
  const paths = result.artifactPaths ?? {};
2005
2092
  lines.push(
@@ -103,10 +103,12 @@ export function productRepositoryTargetError(input) {
103
103
  : "the requested repository";
104
104
  return `Cannot analyze ${repository} because it is this tool's product repository. The guard prevents accidental self-analysis during normal live runs; it is not a data-security boundary.
105
105
 
106
+ No GitHub data was collected, and no files were written.
107
+
106
108
  To learn the tool, run the bundled sample:
107
109
  delivery-friction-analyzer --source sample --out reports/tutorial
108
110
 
109
111
  To analyze a different repository, pass --repo owner/name.
110
112
 
111
- If you intentionally want to analyze this product repository, rerun with --allow-product-repository. The CLI will check that required data is readable before writing artifacts. No GitHub data was collected.`;
113
+ If you intentionally want to analyze this product repository, rerun with --allow-product-repository. The CLI will check that required data is readable before writing artifacts.`;
112
114
  }