delivery-friction-analyzer 0.22.1 → 0.23.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.
@@ -32,12 +32,13 @@ The command reads local `friction-metrics.v1` JSON and writes deterministic `fri
32
32
  - `configuredWorkflow`: optional user-configured workflow context from the repository profile. It is not observed GitHub evidence and does not change scoring, ranking, CSV exports, or PR class matching.
33
33
  - `contributorSource`: optional sanitized contributor-source metadata from the repository profile and collection path. It records source type, path, coverage status, parsed hint count, and guardrail note. It does not include raw contributor file contents or contributor rankings.
34
34
  - `collectionCoverage`: optional source collection coverage metadata copied into live-generated report JSON artifacts. `collectionCoverage.status` records aggregate coverage, and `collectionCoverage.sourceFamilies` lists source-family entries with `family`, `status`, `attempts`, `source`, `diagnostics`, and `downstreamImpact`. This uses the generic `source-bundle.v1` `coverage.sourceFamilies` name; legacy GitHub `apiFamilies` is not a live `friction-report.v1` field.
35
- - `summary`: repository totals and top bottleneck identifiers.
35
+ - `summary`: repository totals, positive-signal top bottleneck identifiers, and no-signal bottleneck identifiers.
36
36
  - `coverage`: PR-open diff, workflow-run, and review-thread coverage counts plus caveats.
37
37
  - `commentSources`: total and source-grouped review comments for Copilot, human, bot, scanner, author replies, and unknown sources.
38
38
  - `surfaces`: core, low-signal, generated, support-surface, role, and functional-surface breakdowns.
39
39
  - `prClasses`: PR class distribution for the analyzed sample, including PR counts, changed lines, share of PRs, and classification source counts by class.
40
- - `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.
40
+ - `bottlenecks`: ranked positive-signal 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.
41
+ - `noSignalBottlenecks`: ranked no-signal context entries using the same item property names as `bottlenecks`. Entries move here when their raw, unrounded report-owned representative scores are zero or when no displayed representative score is positive. They are excluded from `summary.topBottleneckIds` and recommendation category counts; `summary.noSignalBottleneckIds` records their IDs in deterministic order.
41
42
  - `bottlenecks[].observedData[]`: representative PR examples with PR identity, score/value, PR class evidence, final/current additions, deletions, changed-file count, and changed-line count.
42
43
  - `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.
43
44
  - `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.
@@ -49,7 +50,7 @@ The command reads local `friction-metrics.v1` JSON and writes deterministic `fri
49
50
  - `guardrails`: machine-readable checks that the report avoids individual ranking, separates evidence from inference, and does not use an opaque composite score.
50
51
  - `followUp`: non-automated future work suggested by the report.
51
52
 
52
- 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.
53
+ Bottlenecks are ordered by their strongest observed representative metric value, with stable category order used only to break ties. `noSignalBottlenecks` preserves the same ranked-candidate ordering after positive-signal entries are removed, with stable bottleneck IDs used for no-signal 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.
53
54
 
54
55
  ## Markdown Output
55
56
 
@@ -71,6 +72,7 @@ The Markdown renderer presents the same report data for human review:
71
72
  - outlier and sensitivity analysis when displayed examples are dominated by one PR;
72
73
  - 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;
73
74
  - ranked bottlenecks with representative PR examples rendered as compact PR-size tables;
75
+ - a no-signal section for zero-score or no-positive-score bottlenecks, explaining that they are future-run context rather than top findings or recommendation drivers;
74
76
  - validation, review, and source-label evidence for representative PR examples rendered as compact evidence tables;
75
77
  - text-backed status labels such as observed, partial, unavailable, configured, warning, and healthy in Markdown evidence tables where they improve scanability;
76
78
  - 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.22.1",
3
+ "version": "0.23.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-28 — Low-Signal Report Quieting
6
+
7
+ - 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.
8
+ - Why it matters: Users and maintainers can distinguish observed friction from inactive checks without losing machine-readable evidence or report context.
9
+ - Who is affected: Maintainers and users reading generated Markdown reports, `friction-report.json`, or JSON completion receipts from sample or live GitHub analysis.
10
+ - Action needed: None.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/69
12
+
5
13
  ### 2026-06-28 — CLI Artifact Truthfulness
6
14
 
7
15
  - What changed: Live dry-run JSON receipts now show requested PR-class exclusions without implying filtered metrics were computed, and `pr-metrics.csv` fills `review_threads` when sample or live review-thread evidence is available.
@@ -1679,6 +1679,7 @@ function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report,
1679
1679
  collectionCoverage: sourceBundle.coverage,
1680
1680
  totals: metrics?.totals ?? null,
1681
1681
  topBottleneckIds: report?.summary?.topBottleneckIds ?? null,
1682
+ noSignalBottleneckIds: report?.summary?.noSignalBottleneckIds ?? null,
1682
1683
  };
1683
1684
  if (savedProfilePath) {
1684
1685
  summary.savedProfilePath = savedProfilePath;
@@ -469,6 +469,32 @@ function summarizeBottlenecks(metricsSummary, prClasses = summarizePrClasses(met
469
469
  .map(({ definitionIndex, rankValue, ...bottleneck }) => bottleneck);
470
470
  }
471
471
 
472
+ function hasPositiveRepresentativeScore(bottleneck) {
473
+ return (bottleneck.observedData ?? [])
474
+ .some(evidence => Number(evidence.value ?? 0) > 0);
475
+ }
476
+
477
+ function splitBottleneckSignals(bottlenecks) {
478
+ const positiveSignalBottlenecks = [];
479
+ const noSignalBottlenecks = [];
480
+
481
+ for (const bottleneck of bottlenecks ?? []) {
482
+ if (hasPositiveRepresentativeScore(bottleneck)) {
483
+ positiveSignalBottlenecks.push(bottleneck);
484
+ } else {
485
+ noSignalBottlenecks.push(bottleneck);
486
+ }
487
+ }
488
+
489
+ noSignalBottlenecks.sort((left, right) => {
490
+ const leftRank = Number(left.observedData?.[0]?.value ?? 0);
491
+ const rightRank = Number(right.observedData?.[0]?.value ?? 0);
492
+ return (rightRank - leftRank) || left.id.localeCompare(right.id);
493
+ });
494
+
495
+ return { positiveSignalBottlenecks, noSignalBottlenecks };
496
+ }
497
+
472
498
  function summarizeEvidenceDominance(evidence) {
473
499
  const values = evidence
474
500
  .map(entry => Number(entry.value ?? 0))
@@ -866,7 +892,9 @@ function summarizeSensitivity(metricsSummary, baselineBottlenecks) {
866
892
  return {
867
893
  summaries: dominantPrNumbers.map(excludedPrNumber => {
868
894
  const excludedPr = findPullRequest(metricsSummary, excludedPrNumber);
869
- const filteredBottlenecks = summarizeBottlenecks(metricsWithoutPullRequest(metricsSummary, excludedPrNumber));
895
+ const { positiveSignalBottlenecks: filteredBottlenecks } = splitBottleneckSignals(
896
+ summarizeBottlenecks(metricsWithoutPullRequest(metricsSummary, excludedPrNumber)),
897
+ );
870
898
  const filteredTopBottleneckIds = filteredBottlenecks.slice(0, 3).map(bottleneck => bottleneck.id);
871
899
  const affectedBottlenecks = baselineBottlenecks
872
900
  .filter(bottleneck => bottleneck.dominance?.status === "single_pr_dominates"
@@ -904,9 +932,15 @@ function summarizeSensitivity(metricsSummary, baselineBottlenecks) {
904
932
  export function generateRepositoryFrictionReport(metricsSummary, options = {}) {
905
933
  const { workflowContext, contributorSource } = options ?? {};
906
934
  const prClasses = summarizePrClasses(metricsSummary);
907
- const bottlenecksWithSharedSignalKeys = summarizeBottlenecks(metricsSummary, prClasses);
935
+ const rankedBottlenecksWithSharedSignalKeys = summarizeBottlenecks(metricsSummary, prClasses);
936
+ const {
937
+ positiveSignalBottlenecks: bottlenecksWithSharedSignalKeys,
938
+ noSignalBottlenecks: noSignalBottlenecksWithSharedSignalKeys,
939
+ } = splitBottleneckSignals(rankedBottlenecksWithSharedSignalKeys);
908
940
  const sharedSignals = summarizeSharedSignals(bottlenecksWithSharedSignalKeys);
909
941
  const bottlenecks = bottlenecksWithSharedSignalKeys.map(({ rankingKey, ...bottleneck }) => bottleneck);
942
+ const noSignalBottlenecks = noSignalBottlenecksWithSharedSignalKeys
943
+ .map(({ rankingKey, ...bottleneck }) => bottleneck);
910
944
  const configuredWorkflow = normalizeConfiguredWorkflowContext(workflowContext);
911
945
  const contributorSourceMetadata = normalizeContributorSourceMetadata(contributorSource);
912
946
  return {
@@ -925,12 +959,14 @@ export function generateRepositoryFrictionReport(metricsSummary, options = {}) {
925
959
  failedChecks: metricsSummary.totals?.failedChecks ?? 0,
926
960
  cancelledWorkflowRuns: metricsSummary.totals?.cancelledWorkflowRuns ?? 0,
927
961
  topBottleneckIds: bottlenecks.slice(0, 3).map(bottleneck => bottleneck.id),
962
+ noSignalBottleneckIds: noSignalBottlenecks.map(bottleneck => bottleneck.id),
928
963
  },
929
964
  coverage: summarizeCoverage(metricsSummary),
930
965
  commentSources: summarizeCommentSources(metricsSummary),
931
966
  surfaces: summarizeSurfaces(metricsSummary),
932
967
  prClasses,
933
968
  bottlenecks,
969
+ noSignalBottlenecks,
934
970
  sharedSignals,
935
971
  sensitivity: summarizeSensitivity(metricsSummary, bottlenecks),
936
972
  recommendationCategories: summarizeRecommendationCategories(bottlenecks),
@@ -1263,6 +1299,7 @@ function renderPriorityExplanation() {
1263
1299
  return renderList([
1264
1300
  "Bottlenecks are ordered by their strongest displayed representative score, not by an opaque composite priority score.",
1265
1301
  "Each score comes from one metric family, such as review-loop drag, validation failures, change scope, planning signals, review surprise, or post-review commits.",
1302
+ "Bottlenecks with no positive raw representative score are shown as no-signal context instead of top findings or recommendation drivers.",
1266
1303
  "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.",
1267
1304
  "PR size columns show final/current additions, deletions, changed files, and changed lines so readers can compare size against the detected friction signals.",
1268
1305
  "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.",
@@ -1270,6 +1307,27 @@ function renderPriorityExplanation() {
1270
1307
  ]);
1271
1308
  }
1272
1309
 
1310
+ function renderNoSignalBottlenecks(bottlenecks) {
1311
+ const noSignalBottlenecks = bottlenecks ?? [];
1312
+ if (!noSignalBottlenecks.length) return "";
1313
+
1314
+ return [
1315
+ "## No Signal Observed",
1316
+ "",
1317
+ "These bottlenecks had representative report-owned scores of zero or no positive representative score in the displayed evidence. They remain useful context for future runs when matching evidence appears, but they are not treated as top findings or recommendation drivers in this report.",
1318
+ "",
1319
+ renderMarkdownTable(
1320
+ ["Bottleneck", "Metric", "Potential use when evidence exists"],
1321
+ noSignalBottlenecks.map(bottleneck => [
1322
+ bottleneck.title,
1323
+ bottleneck.metricLabel,
1324
+ recommendationActionText(bottleneck),
1325
+ ]),
1326
+ ),
1327
+ "",
1328
+ ].join("\n");
1329
+ }
1330
+
1273
1331
  function topBottleneckLabels(report) {
1274
1332
  const bottlenecksById = new Map((report.bottlenecks ?? []).map(bottleneck => [bottleneck.id, bottleneck]));
1275
1333
  const ids = report.summary?.topBottleneckIds ?? [];
@@ -1712,6 +1770,11 @@ export function renderRepositoryFrictionMarkdown(report) {
1712
1770
  );
1713
1771
  }
1714
1772
 
1773
+ const noSignalSection = renderNoSignalBottlenecks(report.noSignalBottlenecks);
1774
+ if (noSignalSection) {
1775
+ lines.push(noSignalSection);
1776
+ }
1777
+
1715
1778
  lines.push(
1716
1779
  "## Recommendation Categories",
1717
1780
  "",
@@ -1733,6 +1796,7 @@ export function renderRepositoryFrictionMarkdown(report) {
1733
1796
  ? "- Profile suggestions are optional interpretation improvements derived from existing report evidence; they do not change scores, rankings, CSV exports, or JSON report fields."
1734
1797
  : "- No profile suggestion thresholds were triggered by this report's PR class, role, functional-surface, or workflow-coverage evidence.",
1735
1798
  "- Bottlenecks are ranked by their strongest representative observed signal, with stable category order only used to break ties.",
1799
+ "- Zero-score or no-positive-score bottlenecks are retained in no-signal context and excluded from top findings and recommendation category counts.",
1736
1800
  "- Recommendations are inferred from transparent component evidence and representative PR examples; they are not automated changes.",
1737
1801
  "- Missing or partial source evidence remains visible in coverage tables rather than being inferred from unrelated fields.",
1738
1802
  ...(hasConfiguredWorkflowContext(report.configuredWorkflow)