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
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
|
|
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:
|
|
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
|
|
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
|
|
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
|
}
|