delivery-friction-analyzer 0.1.0 → 0.2.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Delivery Friction Analyzer is a product concept for measuring where AI-assisted software delivery still wastes time: review loops, CI churn, scope drift, missing validation, and repeated corrective work after a pull request opens.
4
4
 
5
- The core idea is to use GitHub data as the first durable signal. Pull request diffs, review comments by source, check runs, commits, changed-file spread, file roles, and merge timelines can reveal which repositories, modules, and workflow stages create the most back-and-forth before work becomes mergeable.
5
+ The core idea is to use GitHub data as the first durable signal. Pull request diffs, review comments by source, check runs, commits, change scope, file roles, and merge timelines can reveal which repositories, modules, and workflow stages create the most back-and-forth before work becomes mergeable.
6
6
 
7
7
  ## Product Direction
8
8
 
@@ -32,7 +32,7 @@ The command reads local `friction-metrics.v1` JSON and writes deterministic `fri
32
32
  - `commentSources`: total and source-grouped review comments for Copilot, human, bot, scanner, author replies, and unknown sources.
33
33
  - `surfaces`: core, low-signal, generated, support-surface, role, and functional-surface breakdowns.
34
34
  - `prClasses`: PR class distribution for the analyzed sample, including PR counts, changed lines, share of PRs, and classification source counts by class.
35
- - `bottlenecks`: ranked friction patterns with observed data, inferred diagnosis, and suggested action kept as separate fields.
35
+ - `bottlenecks`: ranked friction patterns with observed data, inferred diagnosis, and suggested action kept as separate fields. `bottlenecks[].title` is a reader-facing display label and may change for clarity while stable IDs such as `changed-file-spread` remain unchanged.
36
36
  - `bottlenecks[].observedData[]`: representative PR examples with PR identity, score/value, PR class evidence, final/current additions, deletions, changed-file count, and changed-line count.
37
37
  - `bottlenecks[].observedData[].validationEvidence`: workflow-run source label, workflow-run coverage, workflow-run conclusions, failed check-run count, failed workflow-run count, and cancelled workflow-run count for representative PR examples.
38
38
  - `bottlenecks[].observedData[].reviewEvidence`: review-thread source label, thread counts, resolution/outdated counts, review decision label/source, human reviewer count, human approval / changes-requested booleans, comment-source breakdown, bot comment count, human reviewer comment count, and author reply count for representative PR examples.
@@ -44,21 +44,23 @@ The command reads local `friction-metrics.v1` JSON and writes deterministic `fri
44
44
  - `guardrails`: machine-readable checks that the report avoids individual ranking, separates evidence from inference, and does not use an opaque composite score.
45
45
  - `followUp`: non-automated future work suggested by the report.
46
46
 
47
- Bottlenecks are ordered by their strongest observed representative metric value, with stable category order used only to break ties. Final/current PR size fields are context for comparing size against friction signals; they only affect ordering for metric families that explicitly measure changed-file spread.
47
+ Bottlenecks are ordered by their strongest observed representative metric value, with stable category order used only to break ties. Final/current PR size fields are context for comparing displayed examples against friction signals; they are not separate ordering inputs. The internal `changedFileSpread` / `changed-file-spread` signal remains the stable contract name for the metric that sums core files touched, directories touched, and functional surfaces touched. It is not a changed-line-count metric.
48
48
 
49
49
  ## Markdown Output
50
50
 
51
51
  The Markdown renderer presents the same report data for human review:
52
52
 
53
- - executive summary totals in a table;
54
53
  - explicit analysis filter labels when downstream artifacts were generated from a filtered PR class sample;
54
+ - executive summary totals plus top findings, triggered recommendation categories, and filter status in a table;
55
+ - a top-of-report focus snapshot that names focus areas, action categories, evidence reviewed, and confidence caveats before detailed bottlenecks;
56
+ - a compact recommendation-category snapshot before detailed bottlenecks, with the full category reference retained later in the report;
55
57
  - a short "How To Read This Report" guide that distinguishes observed evidence, interpretation, recommendations, and caveats;
56
58
  - evidence-quality and coverage tables before detailed recommendations;
57
59
  - key findings that highlight top bottlenecks, strongest displayed signal, outlier caveats, PR class caveats, and coverage caveats;
58
60
  - a PR class context table that shows analyzed PR counts, changed lines, sample share, and classification sources by class;
59
61
  - a top-level shared-signal interpretation callout when multiple displayed bottlenecks share a ranking key or representative PR evidence;
60
62
  - outlier and sensitivity analysis when displayed examples are dominated by one PR;
61
- - a prioritization explanation that describes strongest-signal ordering and how PR size is used as context;
63
+ - a prioritization explanation that describes strongest-signal ordering and how PR size is used as context, using reader-facing change-scope language while mapping back to the internal changed-file-spread signal when needed;
62
64
  - ranked bottlenecks with representative PR examples rendered as compact PR-size tables;
63
65
  - validation, review, and source-label evidence for each representative PR example rendered as plain Markdown detail lists;
64
66
  - separately labeled inferred diagnosis, suggested action, and confidence/caveat blocks for each bottleneck;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.1.0",
3
+ "version": "0.2.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-15 — First-Glance Report Opening
6
+
7
+ - What changed: Friction reports now open with a focus snapshot, early recommendation-category summary, reviewed-evidence counts, and confidence caveats before detailed bottleneck evidence.
8
+ - Why it matters: Maintainers can see what deserves attention, why the report is confident or caveated, and which action themes apply without reading through the full ranked bottleneck details first.
9
+ - Who is affected: Maintainers and contributors reviewing generated friction reports.
10
+ - Action needed: None.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/30
12
+
5
13
  ### 2026-06-14 — NPM Release Automation
6
14
 
7
15
  - What changed: The package is prepared for public npm distribution with CLI metadata, a tight npm package allowlist, CI package dry-runs, automated conventional-commit versioning, GitHub release creation, and tag-triggered npm publishing.
@@ -375,7 +375,7 @@ export function renderRepositoryFrictionMethodology({
375
375
  "",
376
376
  "## Scores And Rankings",
377
377
  "",
378
- "The report ranks bottlenecks by transparent component metrics from `friction-metrics.v1`: review churn, changed-file spread, validation gap, planning gap, review surprise, and fix amplification. These are not an opaque composite score, and they are not individual contributor or reviewer rankings.",
378
+ "The report ranks bottlenecks by transparent component metrics from `friction-metrics.v1`: review churn, change scope (the internal changed-file-spread signal: core files touched plus directories touched plus functional surfaces touched), validation gap, planning gap, review surprise, and fix amplification. These are not an opaque composite score, and they are not individual contributor or reviewer rankings.",
379
379
  "",
380
380
  "## Coverage And Limitations",
381
381
  "",
@@ -54,7 +54,7 @@ const RECOMMENDATION_CATEGORY_LABELS = new Map(
54
54
 
55
55
  const RANKING_SIGNAL_LABELS = new Map([
56
56
  ["reviewChurn", "review churn"],
57
- ["changedFileSpread", "changed-file spread"],
57
+ ["changedFileSpread", "change scope"],
58
58
  ["validationGap", "validation gap"],
59
59
  ["planningGap", "planning gap"],
60
60
  ["reviewSurprise", "review surprise"],
@@ -74,7 +74,7 @@ const BOTTLENECK_DEFINITIONS = [
74
74
  {
75
75
  id: "changed-file-spread",
76
76
  rankingKey: "changedFileSpread",
77
- title: "Changed-file spread",
77
+ title: "Change scope",
78
78
  metricLabel: "spread score",
79
79
  recommendationCategory: "smaller_milestones",
80
80
  action: "Break broad changes into smaller milestones when core files, directories, or surfaces spread out.",
@@ -950,6 +950,30 @@ function renderRecommendationCategories(categories) {
950
950
  );
951
951
  }
952
952
 
953
+ function triggeredRecommendationCategories(categories) {
954
+ return (categories ?? []).filter(category => Number(category.triggeredBottlenecks ?? 0) > 0);
955
+ }
956
+
957
+ function renderRecommendationCategorySnapshot(categories) {
958
+ const triggered = triggeredRecommendationCategories(categories);
959
+ if (!triggered.length) {
960
+ return "No recommendation categories were triggered by the displayed bottleneck evidence.";
961
+ }
962
+
963
+ return renderMarkdownTable(
964
+ ["Category", "Triggered bottlenecks"],
965
+ triggered.map(category => [
966
+ category.label,
967
+ category.triggeredBottlenecks,
968
+ ]),
969
+ );
970
+ }
971
+
972
+ function formatCount(value, singular, plural = `${singular}s`) {
973
+ const count = Number(value ?? 0);
974
+ return `${count} ${count === 1 ? singular : plural}`;
975
+ }
976
+
953
977
  function renderInterpretationAndRecommendation(bottleneck) {
954
978
  return renderMarkdownTable(
955
979
  ["Field", "Value"],
@@ -963,14 +987,38 @@ function renderInterpretationAndRecommendation(bottleneck) {
963
987
  function renderPriorityExplanation() {
964
988
  return renderList([
965
989
  "Bottlenecks are ordered by their strongest displayed representative score, not by an opaque composite priority score.",
966
- "Each score comes from one metric family, such as review-loop drag, validation failures, changed-file spread, planning signals, review surprise, or post-review commits.",
990
+ "Each score comes from one metric family, such as review-loop drag, validation failures, change scope, planning signals, review surprise, or post-review commits.",
991
+ "Change scope is the internal changed-file-spread signal: core files touched plus directories touched plus functional surfaces touched. It is not a line-count metric.",
967
992
  "PR size columns show final/current additions, deletions, changed files, and changed lines so readers can compare size against the detected friction signals.",
968
- "PR size is context for interpretation; it only affects ordering when the bottleneck metric itself is about changed-file spread.",
993
+ "PR size columns are context for interpreting displayed examples; bottleneck ordering uses each metric family's representative score and stable tie-breaks, not the PR size columns.",
969
994
  "Coverage caveats and outlier dominance should be considered before treating the first bottleneck as the most important repository problem.",
970
995
  ]);
971
996
  }
972
997
 
973
- function renderSummaryTable(summary) {
998
+ function topBottleneckLabels(report) {
999
+ const bottlenecksById = new Map((report.bottlenecks ?? []).map(bottleneck => [bottleneck.id, bottleneck]));
1000
+ const ids = report.summary?.topBottleneckIds ?? [];
1001
+ const labels = ids
1002
+ .map(id => bottlenecksById.get(id)?.title ?? id)
1003
+ .filter(Boolean);
1004
+ return labels.length ? labels.join(", ") : "none";
1005
+ }
1006
+
1007
+ function triggeredCategoryLabels(report) {
1008
+ const categories = triggeredRecommendationCategories(report.recommendationCategories);
1009
+ return categories.length
1010
+ ? categories.map(category => `${category.label} (${category.triggeredBottlenecks})`).join(", ")
1011
+ : "none";
1012
+ }
1013
+
1014
+ function formatAnalysisFilterStatus(report) {
1015
+ const filter = report.analysisFilter;
1016
+ if (!filter?.excludedPrClasses?.length) return "none";
1017
+ return `excluded PR class(es): ${filter.excludedPrClasses.join(", ")}; filtered sample ${filter.filteredPullRequests} of ${formatCount(filter.originalPullRequests, "collected PR")}`;
1018
+ }
1019
+
1020
+ function renderSummaryTable(report) {
1021
+ const summary = report.summary ?? {};
974
1022
  return renderMarkdownTable(
975
1023
  ["Metric", "Value"],
976
1024
  [
@@ -981,7 +1029,9 @@ function renderSummaryTable(summary) {
981
1029
  ["Review threads", summary.reviewThreads],
982
1030
  ["Failed checks", summary.failedChecks ?? 0],
983
1031
  ["Cancelled workflow runs", summary.cancelledWorkflowRuns ?? 0],
984
- ["Top bottlenecks", (summary.topBottleneckIds ?? []).join(", ") || "none"],
1032
+ ["Top findings", topBottleneckLabels(report)],
1033
+ ["Triggered recommendation categories", triggeredCategoryLabels(report)],
1034
+ ["Analysis filter", formatAnalysisFilterStatus(report)],
985
1035
  ],
986
1036
  );
987
1037
  }
@@ -998,7 +1048,7 @@ function renderCoverageSummary(coverage) {
998
1048
  }
999
1049
 
1000
1050
  function renderKeyFindings(report) {
1001
- const topBottlenecks = (report.summary.topBottleneckIds ?? []).join(", ") || "none";
1051
+ const topBottlenecks = topBottleneckLabels(report);
1002
1052
  const strongest = report.bottlenecks?.[0];
1003
1053
  const dominanceCallouts = (report.bottlenecks ?? [])
1004
1054
  .filter(bottleneck => bottleneck.dominance?.status === "single_pr_dominates")
@@ -1025,6 +1075,50 @@ function renderKeyFindings(report) {
1025
1075
  ]);
1026
1076
  }
1027
1077
 
1078
+ function summarizeFocusCaveats(report) {
1079
+ const caveats = [];
1080
+ const coverageNotes = report.coverage?.notes ?? [];
1081
+ const dominantPrs = (report.bottlenecks ?? [])
1082
+ .filter(bottleneck => bottleneck.dominance?.status === "single_pr_dominates");
1083
+ const dominantClasses = (report.bottlenecks ?? [])
1084
+ .filter(bottleneck => bottleneck.classDominance?.status === "single_class_dominates");
1085
+
1086
+ if (coverageNotes.length) caveats.push(formatCount(coverageNotes.length, "coverage caveat"));
1087
+ if (dominantPrs.length) caveats.push(formatCount(dominantPrs.length, "outlier caveat"));
1088
+ if (dominantClasses.length) caveats.push(formatCount(dominantClasses.length, "PR class caveat"));
1089
+ if (!caveats.length) return "No early confidence caveats were recorded for the displayed evidence.";
1090
+
1091
+ return `${caveats.join(", ")}. Read the evidence and caveat sections before generalizing.`;
1092
+ }
1093
+
1094
+ function renderFocusSnapshot(report) {
1095
+ const summary = report.summary ?? {};
1096
+ const categories = triggeredCategoryLabels(report);
1097
+ const evidenceReviewed = [
1098
+ formatCount(summary.pullRequests, "PR"),
1099
+ formatCount(summary.changedLines, "changed line"),
1100
+ formatCount(summary.nonGeneratedChangedLines, "non-generated changed line"),
1101
+ formatCount(summary.reviewComments, "review comment"),
1102
+ formatCount(summary.reviewThreads, "review thread"),
1103
+ formatCount(summary.failedChecks, "failed check"),
1104
+ formatCount(summary.cancelledWorkflowRuns, "cancelled workflow run"),
1105
+ ].join(", ");
1106
+ const firstInspection = (report.bottlenecks ?? [])
1107
+ .slice(0, 3)
1108
+ .map(bottleneck => bottleneck.title)
1109
+ .join(", ") || "No detailed bottleneck evidence was available.";
1110
+
1111
+ return renderMarkdownTable(
1112
+ ["Question", "Answer"],
1113
+ [
1114
+ ["Focus first", firstInspection],
1115
+ ["Action categories", categories],
1116
+ ["Evidence reviewed", evidenceReviewed],
1117
+ ["Confidence caveats", summarizeFocusCaveats(report)],
1118
+ ],
1119
+ );
1120
+ }
1121
+
1028
1122
  function classDominanceFallback(prClasses) {
1029
1123
  const sampleClasses = (prClasses?.distribution ?? []).filter(entry => entry.pullRequests > 0);
1030
1124
  if (!sampleClasses.length) {
@@ -1189,7 +1283,15 @@ export function renderRepositoryFrictionMarkdown(report) {
1189
1283
  ...analysisFilterLines,
1190
1284
  "## Executive Summary",
1191
1285
  "",
1192
- renderSummaryTable(report.summary),
1286
+ renderSummaryTable(report),
1287
+ "",
1288
+ "## Focus Snapshot",
1289
+ "",
1290
+ renderFocusSnapshot(report),
1291
+ "",
1292
+ "## Recommendation Category Snapshot",
1293
+ "",
1294
+ renderRecommendationCategorySnapshot(report.recommendationCategories ?? []),
1193
1295
  "",
1194
1296
  "## How To Read This Report",
1195
1297
  "",