delivery-friction-analyzer 0.22.0 → 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.
|
@@ -28,16 +28,17 @@ The command reads local `friction-metrics.v1` JSON and writes deterministic `fri
|
|
|
28
28
|
- `source`: optional source-bundle provenance copied into live-generated report
|
|
29
29
|
artifacts, including `kind` and a human-readable label.
|
|
30
30
|
- `targetRepository`: analyzed repository identity; live analysis sample size is encoded as `targetRepository.analysisPullRequestLimit` from collection metadata.
|
|
31
|
-
- `analysisFilter`: optional metadata for explicit filters applied before metrics computation, including excluded PR classes and before/after PR counts.
|
|
31
|
+
- `analysisFilter`: optional metadata for explicit filters applied before metrics computation, including excluded PR classes and before/after PR counts. Live dry-run completion receipts may also include requested `analysisFilter.excludedPrClasses` with null before/after counts because dry-run coverage probes do not compute filtered metrics or report artifacts.
|
|
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;
|
|
@@ -128,7 +130,7 @@ Full live analysis writes `methodology.md` as a hybrid artifact: stable explanat
|
|
|
128
130
|
|
|
129
131
|
The methodology artifact should stay aligned with this contract and the Markdown methodology summary, but it may be more detailed than the main report.
|
|
130
132
|
|
|
131
|
-
When PR class filtering is applied, `source-bundle.json` remains the full collected sample, while `normalized.json`, `metrics-summary.json`, `friction-report.json`, `friction-report.md`, `methodology.md`, and CSV exports describe the filtered sample. The methodology and CLI completion output must name the excluded PR classes and show the filtered PR count. If filtering excludes every collected PR, the analysis must fail without writing complete-looking empty reports.
|
|
133
|
+
When PR class filtering is applied to a full analysis, `source-bundle.json` remains the full collected sample, while `normalized.json`, `metrics-summary.json`, `friction-report.json`, `friction-report.md`, `methodology.md`, and CSV exports describe the filtered sample. The methodology and CLI completion output must name the excluded PR classes and show the filtered PR count. If filtering excludes every collected PR, the analysis must fail without writing complete-looking empty reports. Live dry-run receipts record requested PR class exclusions without a filtered count because dry-run does not build normalized metrics.
|
|
132
134
|
|
|
133
135
|
## CSV Evidence Exports
|
|
134
136
|
|
|
@@ -143,7 +145,7 @@ Minimum CSV column groups:
|
|
|
143
145
|
- `comment-sources.csv`: source name, total comments, bot/scanner classification, human/author classification, and share of all comments.
|
|
144
146
|
- `collection-coverage.csv`: source family, status, attempts, source label, diagnostics, and downstream impact.
|
|
145
147
|
|
|
146
|
-
Empty CSV cells mean unavailable or not applicable. Numeric zero should be used only for observed or computed zero counts. Count columns that depend on optional GitHub coverage should keep source or coverage labels nearby so spreadsheet readers can tell unavailable evidence apart from observed zeroes. CSVs must not include raw comment bodies, raw workflow logs, tokens, secret-bearing environment details, or individual contributor/reviewer rankings.
|
|
148
|
+
Empty CSV cells mean unavailable or not applicable. Numeric zero should be used only for observed or computed zero counts. Count columns that depend on optional GitHub or sample-bundle coverage should keep source or coverage labels nearby so spreadsheet readers can tell unavailable evidence apart from observed zeroes. `pr-metrics.csv` `review_threads` is populated when normalized review-thread source evidence is available, including synthetic sample evidence and live GraphQL review-thread evidence. CSVs must not include raw comment bodies, raw workflow logs, tokens, secret-bearing environment details, or individual contributor/reviewer rankings.
|
|
147
149
|
|
|
148
150
|
Contributor-source coverage appears in `collection-coverage.csv` as the `contributor_source` source family when configured. CSVs may include aggregate comment-source counts influenced by contributor hints, but they must not include raw `.all-contributorsrc` contents, contributor names, contributor login lists, or person rankings.
|
|
149
151
|
|
package/package.json
CHANGED
package/release-log.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
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
|
+
|
|
13
|
+
### 2026-06-28 — CLI Artifact Truthfulness
|
|
14
|
+
|
|
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.
|
|
16
|
+
- Why it matters: Maintainers and scripts can trust completion receipts and CSV evidence to reflect accepted filters and available review-thread counts.
|
|
17
|
+
- Who is affected: Maintainers and users reading JSON completion receipts, CSV exports, and contract docs from sample or live GitHub analysis.
|
|
18
|
+
- Action needed: None.
|
|
19
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/68
|
|
20
|
+
|
|
5
21
|
### 2026-06-28 — CLI Command Help Clarity
|
|
6
22
|
|
|
7
23
|
- What changed: CLI help now separates the bundled sample, live GitHub analysis, live coverage probes, output controls, interactive setup, and reusable live-run presets, with mode-specific help for `--source sample --help` and `--source github --help`.
|
|
@@ -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;
|
|
@@ -1695,6 +1696,15 @@ function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report,
|
|
|
1695
1696
|
return summary;
|
|
1696
1697
|
}
|
|
1697
1698
|
|
|
1699
|
+
function requestedAnalysisFilter(excludedPrClasses = []) {
|
|
1700
|
+
if (!excludedPrClasses.length) return null;
|
|
1701
|
+
return {
|
|
1702
|
+
excludedPrClasses,
|
|
1703
|
+
originalPullRequests: null,
|
|
1704
|
+
filteredPullRequests: null,
|
|
1705
|
+
};
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1698
1708
|
function applyPrClassFilter(normalized, excludedPrClasses = []) {
|
|
1699
1709
|
if (!excludedPrClasses.length) return normalized;
|
|
1700
1710
|
const excluded = new Set(excludedPrClasses);
|
|
@@ -1824,7 +1834,7 @@ export async function runAnalyzeGithub(options, {
|
|
|
1824
1834
|
requestedLimit: options.limit,
|
|
1825
1835
|
sampledLimit: collectionLimit,
|
|
1826
1836
|
csv: false,
|
|
1827
|
-
analysisFilter:
|
|
1837
|
+
analysisFilter: requestedAnalysisFilter(options.excludedPrClasses ?? []),
|
|
1828
1838
|
savedProfilePath: options.savedProfilePath,
|
|
1829
1839
|
savedRunPresetPath: options.savedRunPresetPath,
|
|
1830
1840
|
prClassRulesWritten: options.prClassRulesWritten,
|
|
@@ -1988,10 +1998,13 @@ export function formatAnalyzeGithubCompletion(result) {
|
|
|
1988
1998
|
}
|
|
1989
1999
|
|
|
1990
2000
|
if (result.analysisFilter?.excludedPrClasses?.length) {
|
|
1991
|
-
lines.push(
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
2001
|
+
lines.push(`Analysis filter: excluded PR class(es): ${result.analysisFilter.excludedPrClasses.join(", ")}.`);
|
|
2002
|
+
if (Number.isInteger(result.analysisFilter.filteredPullRequests)
|
|
2003
|
+
&& Number.isInteger(result.analysisFilter.originalPullRequests)) {
|
|
2004
|
+
lines.push(`Filtered sample: ${result.analysisFilter.filteredPullRequests} of ${result.analysisFilter.originalPullRequests} collected pull request(s).`);
|
|
2005
|
+
} else if (result.dryRun) {
|
|
2006
|
+
lines.push("Filter application: requested only; dry-run coverage probes do not compute filtered metrics or report artifacts.");
|
|
2007
|
+
}
|
|
1995
2008
|
}
|
|
1996
2009
|
|
|
1997
2010
|
if (result.savedProfilePath) {
|
|
@@ -94,9 +94,12 @@ function hasObservedWorkflowRuns(workflowRuns = {}) {
|
|
|
94
94
|
return (workflowRuns.coverage ?? workflowRuns.workflowRunCoverage) === "observed";
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function hasObservedReviewThreads(reviewThreads = {}) {
|
|
97
|
+
function hasObservedReviewThreads(reviewThreads = {}, coverage = {}) {
|
|
98
98
|
const source = reviewThreads.source ?? reviewThreads.reviewThreadSource ?? "unavailable";
|
|
99
|
-
|
|
99
|
+
if (String(source).startsWith("graphql")) return true;
|
|
100
|
+
if (String(source).startsWith("not_sampled") || source === "unavailable") return false;
|
|
101
|
+
if (String(source).includes("review_threads")) return true;
|
|
102
|
+
return coverage.status === "available";
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
function hasObservedReviewDecision(reviewDecision = {}) {
|
|
@@ -135,7 +138,7 @@ function prMetricsCsv(metricsSummary, analysisFilter) {
|
|
|
135
138
|
...SCORE_COLUMNS.map(([header]) => header),
|
|
136
139
|
];
|
|
137
140
|
const rows = sortedPullRequests(metricsSummary).map(pr => {
|
|
138
|
-
const observedReviewThreads = hasObservedReviewThreads(pr.review?.threads);
|
|
141
|
+
const observedReviewThreads = hasObservedReviewThreads(pr.review?.threads, pr.coverage?.reviewThreads);
|
|
139
142
|
const observedReviewDecision = hasObservedReviewDecision(pr.review?.decision);
|
|
140
143
|
const observedWorkflowRuns = hasObservedWorkflowRuns(pr.ci?.workflowRuns);
|
|
141
144
|
const row = {
|
|
@@ -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)
|