delivery-friction-analyzer 0.22.1 → 0.23.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.
|
@@ -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
|
|
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
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 =
|
|
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
|
|
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)
|