delivery-friction-analyzer 0.7.3 → 0.9.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.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Friction Report Contract
|
|
2
2
|
|
|
3
|
-
Milestone 3
|
|
3
|
+
Milestone 3 introduced `friction-report.v1`, a deterministic report generated from a `friction-metrics.v1` repository metrics summary. Milestone 4 adds Markdown and methodology profile suggestions without adding report JSON fields. The report layer does not fetch GitHub data, mutate repositories, rank individuals, or depend on services beyond the data collection path that produced the metrics summary.
|
|
4
4
|
|
|
5
5
|
## Outputs
|
|
6
6
|
|
|
@@ -60,11 +60,13 @@ The Markdown renderer presents the same report data for human review:
|
|
|
60
60
|
- evidence-quality and coverage tables before detailed recommendations;
|
|
61
61
|
- key findings that highlight top bottlenecks, strongest displayed signal, outlier caveats, PR class caveats, and coverage caveats;
|
|
62
62
|
- a PR class context table that shows analyzed PR counts, changed lines, sample share, and classification sources by class;
|
|
63
|
+
- profile suggestions when fallback `unknown` PR classes or unknown file role/surface evidence cross deterministic thresholds;
|
|
63
64
|
- a top-level shared-signal interpretation callout when multiple displayed bottlenecks share a ranking key or representative PR evidence;
|
|
64
65
|
- outlier and sensitivity analysis when displayed examples are dominated by one PR;
|
|
65
66
|
- 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;
|
|
66
67
|
- ranked bottlenecks with representative PR examples rendered as compact PR-size tables;
|
|
67
|
-
- validation, review, and source-label evidence for
|
|
68
|
+
- validation, review, and source-label evidence for representative PR examples rendered as compact evidence tables;
|
|
69
|
+
- text-backed status labels such as observed, partial, unavailable, configured, warning, and healthy in Markdown evidence tables where they improve scanability;
|
|
68
70
|
- separately labeled inferred diagnosis, suggested action, and confidence/caveat blocks for each bottleneck;
|
|
69
71
|
- shared-evidence notes when multiple recommendation categories use the same representative PR set;
|
|
70
72
|
- recommendation-category, comment-source, and core/support-surface tables;
|
|
@@ -73,6 +75,8 @@ The Markdown renderer presents the same report data for human review:
|
|
|
73
75
|
- guardrails, follow-up, and artifact-sensitivity guidance.
|
|
74
76
|
|
|
75
77
|
Markdown output should not include individual contributor or reviewer rankings.
|
|
78
|
+
Status labels are Markdown presentation helpers, not `friction-report.v1` fields. They should preserve the underlying source labels and counts rather than replacing auditable evidence.
|
|
79
|
+
Profile suggestions are also presentation helpers, not `friction-report.v1` fields. They are derived from existing PR class and file-surface evidence, appear at most once per suggestion category, and do not change scores, rankings, CSV exports, filtering, or PR class matching. Because the report JSON does not carry repository-profile rule inventory, all analyzed PRs using fallback `unknown` PR class evidence is the renderer's small-sample proxy for no configured PR class rule producing usable classification evidence.
|
|
76
80
|
|
|
77
81
|
## Recommendation Boundaries
|
|
78
82
|
|
|
@@ -107,6 +111,7 @@ Full live analysis writes `methodology.md` as a hybrid artifact: stable explanat
|
|
|
107
111
|
- target repository and report/metric versions;
|
|
108
112
|
- profile path when available;
|
|
109
113
|
- configured workflow context when supplied by the repository profile, labeled as user-configured context rather than observed GitHub evidence;
|
|
114
|
+
- profile suggestions when PR class or file/path profile evidence crosses deterministic fallback thresholds, or an explicit no-threshold note when none were triggered;
|
|
110
115
|
- requested and collected PR counts;
|
|
111
116
|
- collection coverage status and API-family diagnostics;
|
|
112
117
|
- scoring, ranking, dominance, sensitivity, and limitation explanations;
|
package/package.json
CHANGED
package/release-log.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-06-20 — Profile Improvement Suggestions
|
|
6
|
+
|
|
7
|
+
- What changed: Markdown friction reports and methodology now suggest PR class or file/path profile improvements when fallback `unknown` evidence dominates the analyzed sample.
|
|
8
|
+
- Why it matters: Maintainers can see where repository profile rules would improve interpretation without treating the suggestions as score changes or required fixes.
|
|
9
|
+
- Who is affected: Maintainers and contributors reviewing generated reports or authoring repository profiles.
|
|
10
|
+
- Action needed: Optional; add or refine profile rules when the suggestions match repository conventions.
|
|
11
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/45
|
|
12
|
+
|
|
13
|
+
### 2026-06-20 — Report Evidence Status Tables
|
|
14
|
+
|
|
15
|
+
- What changed: Markdown friction reports now show representative validation, review, and source-label evidence in compact tables with text status labels for observed, partial, unavailable, configured, warning, and healthy states.
|
|
16
|
+
- Why it matters: Maintainers can compare bottleneck evidence more quickly while still seeing when evidence is configured, incomplete, unavailable, or directly observed.
|
|
17
|
+
- Who is affected: Maintainers and contributors reviewing generated Markdown friction reports.
|
|
18
|
+
- Action needed: None.
|
|
19
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/44
|
|
20
|
+
|
|
5
21
|
### 2026-06-19 — Interactive Setup Choice Presets
|
|
6
22
|
|
|
7
23
|
- What changed: Interactive setup now shows workflow profile choices as labeled selections and can add an opt-in Conventional Commit PR class preset to generated or updated repository profiles.
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
CONFIGURED_WORKFLOW_NOTE,
|
|
3
3
|
configuredWorkflowEntries,
|
|
4
4
|
hasConfiguredWorkflowContext,
|
|
5
|
+
profileSuggestions,
|
|
5
6
|
} from "./friction-report.js";
|
|
6
7
|
|
|
7
8
|
const BOT_OR_SCANNER_SOURCES = new Set([
|
|
@@ -357,6 +358,30 @@ function formatConfiguredWorkflowContext(report) {
|
|
|
357
358
|
];
|
|
358
359
|
}
|
|
359
360
|
|
|
361
|
+
function formatProfileSuggestions(report) {
|
|
362
|
+
const suggestions = profileSuggestions(report);
|
|
363
|
+
if (!suggestions.length) {
|
|
364
|
+
return [
|
|
365
|
+
"## Profile Suggestions",
|
|
366
|
+
"",
|
|
367
|
+
"- No profile suggestion thresholds were triggered by this report's PR class, role, or functional-surface evidence.",
|
|
368
|
+
"",
|
|
369
|
+
];
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return [
|
|
373
|
+
"## Profile Suggestions",
|
|
374
|
+
"",
|
|
375
|
+
"Profile suggestions are optional interpretation improvements derived from existing report evidence. They do not change scores, rankings, CSV exports, or JSON report fields.",
|
|
376
|
+
"",
|
|
377
|
+
...suggestions.map(suggestion => [
|
|
378
|
+
`- ${suggestion.area}: ${suggestion.evidence}`,
|
|
379
|
+
` Suggested next step: ${suggestion.suggestion}`,
|
|
380
|
+
].join("\n")),
|
|
381
|
+
"",
|
|
382
|
+
];
|
|
383
|
+
}
|
|
384
|
+
|
|
360
385
|
export function renderRepositoryFrictionMethodology({
|
|
361
386
|
report,
|
|
362
387
|
sourceBundle,
|
|
@@ -394,6 +419,7 @@ export function renderRepositoryFrictionMethodology({
|
|
|
394
419
|
"",
|
|
395
420
|
"The repository profile maps file paths to categories, roles, and functional surfaces. Those classifications drive non-generated changed-line counts, support-surface summaries, planning-document signals, and low-signal weighting.",
|
|
396
421
|
"",
|
|
422
|
+
...formatProfileSuggestions(report),
|
|
397
423
|
...formatConfiguredWorkflowContext(report),
|
|
398
424
|
"## Scores And Rankings",
|
|
399
425
|
"",
|
|
@@ -83,6 +83,12 @@ const WORKFLOW_CONTEXT_VALUE_LABELS = new Map([
|
|
|
83
83
|
|
|
84
84
|
export const CONFIGURED_WORKFLOW_NOTE = "Configured workflow context comes from the repository profile. It is user-configured context, not observed GitHub evidence, and it does not change scores, rankings, CSV exports, or PR class matching.";
|
|
85
85
|
|
|
86
|
+
const PROFILE_SUGGESTION_THRESHOLDS = {
|
|
87
|
+
minimumPrClassSample: 3,
|
|
88
|
+
unknownPrClassShare: 0.8,
|
|
89
|
+
unknownFileShare: 0.25,
|
|
90
|
+
};
|
|
91
|
+
|
|
86
92
|
const BOTTLENECK_DEFINITIONS = [
|
|
87
93
|
{
|
|
88
94
|
id: "review-churn",
|
|
@@ -585,6 +591,61 @@ function summarizeRecommendationCategories(bottlenecks) {
|
|
|
585
591
|
}));
|
|
586
592
|
}
|
|
587
593
|
|
|
594
|
+
function entryValue(entries = [], name) {
|
|
595
|
+
return Number((entries ?? []).find(entry => entry.name === name)?.value ?? 0);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function classSourceValue(entry = {}, source) {
|
|
599
|
+
return entryValue(entry.classificationSources, source);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function profileSuggestions(report = {}) {
|
|
603
|
+
const suggestions = [];
|
|
604
|
+
const summary = report.summary ?? {};
|
|
605
|
+
const prClasses = report.prClasses ?? {};
|
|
606
|
+
const classDistribution = prClasses.distribution ?? [];
|
|
607
|
+
const totalPullRequests = Number(prClasses.totalPullRequests ?? summary.pullRequests ?? 0);
|
|
608
|
+
const unknownClass = classDistribution.find(entry => entry.class === "unknown");
|
|
609
|
+
const unknownClassPullRequests = Number(unknownClass?.pullRequests ?? 0);
|
|
610
|
+
const fallbackUnknownPullRequests = classSourceValue(unknownClass, "fallback_rule");
|
|
611
|
+
const fallbackUnknownClassShare = totalPullRequests > 0
|
|
612
|
+
? fallbackUnknownPullRequests / totalPullRequests
|
|
613
|
+
: 0;
|
|
614
|
+
const everyPrFallbackUnknown = totalPullRequests > 0
|
|
615
|
+
&& unknownClassPullRequests === totalPullRequests
|
|
616
|
+
&& fallbackUnknownPullRequests === unknownClassPullRequests;
|
|
617
|
+
|
|
618
|
+
if ((totalPullRequests >= PROFILE_SUGGESTION_THRESHOLDS.minimumPrClassSample
|
|
619
|
+
&& fallbackUnknownClassShare >= PROFILE_SUGGESTION_THRESHOLDS.unknownPrClassShare)
|
|
620
|
+
|| everyPrFallbackUnknown) {
|
|
621
|
+
suggestions.push({
|
|
622
|
+
id: "pr-class-rules",
|
|
623
|
+
area: "PR class rules",
|
|
624
|
+
evidence: `${fallbackUnknownPullRequests} of ${totalPullRequests} analyzed PRs (${percentageLabel(fallbackUnknownClassShare)}) use fallback unknown PR class evidence.`,
|
|
625
|
+
suggestion: "Add or refine repository-profile PR class title rules, or rerun interactive setup to add the Conventional Commit preset when it matches the repository.",
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const nonGeneratedChangedLines = Number(summary.nonGeneratedChangedLines ?? 0);
|
|
630
|
+
const surfaces = report.surfaces ?? {};
|
|
631
|
+
const unknownRoleLines = entryValue(surfaces.byRole, "unknown");
|
|
632
|
+
const unknownSurfaceLines = entryValue(surfaces.byFunctionalSurface, "unknown");
|
|
633
|
+
const unknownRoleShare = nonGeneratedChangedLines > 0 ? unknownRoleLines / nonGeneratedChangedLines : 0;
|
|
634
|
+
const unknownSurfaceShare = nonGeneratedChangedLines > 0 ? unknownSurfaceLines / nonGeneratedChangedLines : 0;
|
|
635
|
+
|
|
636
|
+
if (unknownRoleShare >= PROFILE_SUGGESTION_THRESHOLDS.unknownFileShare
|
|
637
|
+
|| unknownSurfaceShare >= PROFILE_SUGGESTION_THRESHOLDS.unknownFileShare) {
|
|
638
|
+
suggestions.push({
|
|
639
|
+
id: "file-path-rules",
|
|
640
|
+
area: "File/path rules",
|
|
641
|
+
evidence: `Unknown role lines: ${unknownRoleLines} of ${nonGeneratedChangedLines} (${percentageLabel(unknownRoleShare)}); unknown functional-surface lines: ${unknownSurfaceLines} of ${nonGeneratedChangedLines} (${percentageLabel(unknownSurfaceShare)}).`,
|
|
642
|
+
suggestion: "Add repository-profile path rules for high-volume unknown roles or functional surfaces, starting with the directories that account for the most non-generated changed lines.",
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return suggestions;
|
|
647
|
+
}
|
|
648
|
+
|
|
588
649
|
export function normalizeConfiguredWorkflowContext(workflowContext) {
|
|
589
650
|
if (!workflowContext || typeof workflowContext !== "object" || Array.isArray(workflowContext)) {
|
|
590
651
|
return null;
|
|
@@ -933,56 +994,137 @@ function renderEvidenceTable(observedData) {
|
|
|
933
994
|
);
|
|
934
995
|
}
|
|
935
996
|
|
|
936
|
-
function
|
|
937
|
-
return
|
|
997
|
+
function statusLabel(status, detail) {
|
|
998
|
+
return `[${status}] ${detail}`;
|
|
938
999
|
}
|
|
939
1000
|
|
|
940
|
-
function
|
|
1001
|
+
function coverageStatusLabel(value) {
|
|
1002
|
+
switch (value) {
|
|
1003
|
+
case "observed":
|
|
1004
|
+
case "available":
|
|
1005
|
+
return "observed";
|
|
1006
|
+
case "partial":
|
|
1007
|
+
return "partial";
|
|
1008
|
+
default:
|
|
1009
|
+
return "unavailable";
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function validationStatusSummary(validationEvidence = {}) {
|
|
1014
|
+
const coverage = validationEvidence.workflowRunCoverage ?? "unavailable";
|
|
1015
|
+
const coverageStatus = coverageStatusLabel(coverage);
|
|
1016
|
+
const failedCheckRuns = Number(validationEvidence.failedCheckRuns ?? 0);
|
|
1017
|
+
const failedWorkflowRuns = Number(validationEvidence.failedWorkflowRuns ?? 0);
|
|
1018
|
+
const cancelledWorkflowRuns = Number(validationEvidence.cancelledWorkflowRuns ?? 0);
|
|
1019
|
+
const interruptionCount = failedCheckRuns + failedWorkflowRuns + cancelledWorkflowRuns;
|
|
1020
|
+
const coverageLabel = statusLabel(coverageStatus, `workflow coverage: ${coverage}`);
|
|
1021
|
+
let outcomeLabel;
|
|
1022
|
+
if (interruptionCount > 0) {
|
|
1023
|
+
outcomeLabel = statusLabel("warning", `${failedCheckRuns} failed checks, ${failedWorkflowRuns} failed workflows, ${cancelledWorkflowRuns} cancelled workflow runs`);
|
|
1024
|
+
} else if (coverageStatus === "observed") {
|
|
1025
|
+
outcomeLabel = statusLabel("healthy", "no failed or cancelled validation runs");
|
|
1026
|
+
} else if (coverageStatus === "partial") {
|
|
1027
|
+
outcomeLabel = statusLabel("partial", "no failed or cancelled validation runs in sampled workflow evidence");
|
|
1028
|
+
} else {
|
|
1029
|
+
outcomeLabel = statusLabel("unavailable", "validation outcome unavailable");
|
|
1030
|
+
}
|
|
1031
|
+
const conclusions = formatNamedValues(validationEvidence.workflowRunConclusions ?? []);
|
|
1032
|
+
|
|
1033
|
+
return `${coverageLabel}; ${outcomeLabel}; conclusions: ${conclusions}`;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function reviewThreadStatusLabel(reviewEvidence = {}) {
|
|
1037
|
+
const source = reviewEvidence.reviewThreadSource ?? "unavailable";
|
|
1038
|
+
if (source === "unavailable") return "unavailable";
|
|
1039
|
+
if (source.startsWith("not_sampled")) return "partial";
|
|
1040
|
+
return "observed";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function reviewStatusSummary(reviewEvidence = {}) {
|
|
1044
|
+
const threadLabel = statusLabel(
|
|
1045
|
+
reviewThreadStatusLabel(reviewEvidence),
|
|
1046
|
+
`threads: ${reviewEvidence.reviewThreads ?? 0}, resolved: ${reviewEvidence.resolvedThreads ?? 0}, outdated: ${reviewEvidence.outdatedThreads ?? 0}`,
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
return threadLabel;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function reviewDecisionSummary(reviewEvidence = {}) {
|
|
1053
|
+
const observedReviewDecision = hasObservedReviewDecision({
|
|
1054
|
+
state: reviewEvidence.reviewDecision,
|
|
1055
|
+
source: reviewEvidence.reviewDecisionSource,
|
|
1056
|
+
});
|
|
1057
|
+
const status = observedReviewDecision ? "observed" : "unavailable";
|
|
1058
|
+
const humanApproved = formatObservedBoolean(reviewEvidence.humanApproved, observedReviewDecision);
|
|
1059
|
+
const humanChangesRequested = formatObservedBoolean(reviewEvidence.humanChangesRequested, observedReviewDecision);
|
|
1060
|
+
const humanReviewerCount = formatObservedCount(reviewEvidence.humanReviewerCount, observedReviewDecision);
|
|
1061
|
+
const approvalStatus = observedReviewDecision && reviewEvidence.humanApproved
|
|
1062
|
+
? `; ${statusLabel("healthy", "human approval observed")}`
|
|
1063
|
+
: "";
|
|
1064
|
+
|
|
1065
|
+
return `${statusLabel(status, `${reviewEvidence.reviewDecision ?? "unavailable"} from ${reviewEvidence.reviewDecisionSource ?? "unavailable"}`)}; human reviewers: ${humanReviewerCount}; approved: ${humanApproved}; changes requested: ${humanChangesRequested}${approvalStatus}`;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function commentSourceSummary(reviewEvidence = {}) {
|
|
1069
|
+
const commentSources = reviewEvidence.commentSources ?? [];
|
|
1070
|
+
if (commentSources.length) return statusLabel("observed", formatNamedValues(commentSources));
|
|
1071
|
+
const reviewStatus = reviewThreadStatusLabel(reviewEvidence);
|
|
1072
|
+
if (reviewStatus === "observed") return statusLabel("observed", "none");
|
|
1073
|
+
if (reviewStatus === "partial") return statusLabel("partial", "none in sampled review-thread evidence");
|
|
1074
|
+
return statusLabel("unavailable", "comment sources unavailable");
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
function prClassStatusSummary(prClass = {}) {
|
|
1078
|
+
const source = prClass.classificationSource ?? "fallback_rule";
|
|
1079
|
+
const status = source === "fallback_rule"
|
|
1080
|
+
? "observed"
|
|
1081
|
+
: source === "unavailable" ? "unavailable" : "configured";
|
|
1082
|
+
return statusLabel(status, formatPrClass(prClass));
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
function workflowSourceSummary(validationEvidence = {}) {
|
|
1086
|
+
const source = validationEvidence.workflowRunSource ?? "unavailable";
|
|
1087
|
+
const status = source === "unavailable" ? "unavailable" : "observed";
|
|
1088
|
+
return statusLabel(status, source);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function reviewThreadSourceSummary(reviewEvidence = {}) {
|
|
1092
|
+
const source = reviewEvidence.reviewThreadSource ?? "unavailable";
|
|
1093
|
+
return statusLabel(reviewThreadStatusLabel(reviewEvidence), source);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function sourceLabelSummary(evidence = {}) {
|
|
1097
|
+
return [
|
|
1098
|
+
`PR class: ${prClassStatusSummary(evidence.prClass)}`,
|
|
1099
|
+
`Review thread source: ${reviewThreadSourceSummary(evidence.reviewEvidence)}`,
|
|
1100
|
+
`Workflow source: ${workflowSourceSummary(evidence.validationEvidence)}`,
|
|
1101
|
+
].join("; ");
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
function evidenceDetailRows(observedData) {
|
|
941
1105
|
return (observedData ?? []).map(evidence => {
|
|
942
1106
|
const validationEvidence = evidence.validationEvidence ?? {};
|
|
943
1107
|
const reviewEvidence = evidence.reviewEvidence ?? {};
|
|
944
|
-
const workflowRunConclusions = validationEvidence.workflowRunConclusions ?? [];
|
|
945
|
-
const reviewCommentSources = reviewEvidence.commentSources ?? [];
|
|
946
|
-
const observedReviewDecision = hasObservedReviewDecision({
|
|
947
|
-
state: reviewEvidence.reviewDecision,
|
|
948
|
-
source: reviewEvidence.reviewDecisionSource,
|
|
949
|
-
});
|
|
950
1108
|
|
|
951
1109
|
return [
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
"",
|
|
964
|
-
"
|
|
965
|
-
"",
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
`Outdated threads: ${reviewEvidence.outdatedThreads ?? 0}`,
|
|
971
|
-
`Review decision: ${reviewEvidence.reviewDecision ?? "unavailable"} (source: ${reviewEvidence.reviewDecisionSource ?? "unavailable"})`,
|
|
972
|
-
`Human reviewers: ${formatObservedCount(reviewEvidence.humanReviewerCount, observedReviewDecision)}`,
|
|
973
|
-
`Human approved: ${formatObservedBoolean(reviewEvidence.humanApproved, observedReviewDecision)}`,
|
|
974
|
-
`Human changes requested: ${formatObservedBoolean(reviewEvidence.humanChangesRequested, observedReviewDecision)}`,
|
|
975
|
-
`Comment sources: ${formatNamedValues(reviewCommentSources)}`,
|
|
976
|
-
]),
|
|
977
|
-
"",
|
|
978
|
-
"Source labels:",
|
|
979
|
-
"",
|
|
980
|
-
renderDetailList([
|
|
981
|
-
`PR class: ${formatPrClass(evidence.prClass)}`,
|
|
982
|
-
`Workflow source: ${validationEvidence.workflowRunSource ?? "unavailable"}`,
|
|
983
|
-
]),
|
|
984
|
-
].join("\n");
|
|
985
|
-
}).join("\n\n");
|
|
1110
|
+
prReference(evidence),
|
|
1111
|
+
validationStatusSummary(validationEvidence),
|
|
1112
|
+
`${reviewStatusSummary(reviewEvidence)}; ${reviewDecisionSummary(reviewEvidence)}; comments: ${commentSourceSummary(reviewEvidence)}`,
|
|
1113
|
+
sourceLabelSummary(evidence),
|
|
1114
|
+
];
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function renderEvidenceDetails(observedData) {
|
|
1119
|
+
return renderMarkdownTable(
|
|
1120
|
+
[
|
|
1121
|
+
"PR",
|
|
1122
|
+
"Validation",
|
|
1123
|
+
"Review",
|
|
1124
|
+
"Source labels",
|
|
1125
|
+
],
|
|
1126
|
+
evidenceDetailRows(observedData),
|
|
1127
|
+
);
|
|
986
1128
|
}
|
|
987
1129
|
|
|
988
1130
|
function recommendationCategoryLabel(bottleneck) {
|
|
@@ -1213,6 +1355,27 @@ function renderPrClassContext(prClasses) {
|
|
|
1213
1355
|
].join("\n");
|
|
1214
1356
|
}
|
|
1215
1357
|
|
|
1358
|
+
function renderProfileSuggestions(report) {
|
|
1359
|
+
const suggestions = profileSuggestions(report);
|
|
1360
|
+
if (!suggestions.length) return "";
|
|
1361
|
+
|
|
1362
|
+
return [
|
|
1363
|
+
"## Profile Suggestions",
|
|
1364
|
+
"",
|
|
1365
|
+
"Optional profile improvements based on this report's existing evidence. These suggestions do not change scores, rankings, CSV exports, or JSON report fields.",
|
|
1366
|
+
"",
|
|
1367
|
+
renderMarkdownTable(
|
|
1368
|
+
["Profile area", "Evidence", "Suggested next step"],
|
|
1369
|
+
suggestions.map(suggestion => [
|
|
1370
|
+
suggestion.area,
|
|
1371
|
+
suggestion.evidence,
|
|
1372
|
+
suggestion.suggestion,
|
|
1373
|
+
]),
|
|
1374
|
+
),
|
|
1375
|
+
"",
|
|
1376
|
+
].join("\n");
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1216
1379
|
function renderConfiguredWorkflowContext(configuredWorkflow) {
|
|
1217
1380
|
const entries = configuredWorkflowEntries(configuredWorkflow);
|
|
1218
1381
|
if (!entries.length) return "";
|
|
@@ -1397,6 +1560,7 @@ export function renderRepositoryFrictionMarkdown(report) {
|
|
|
1397
1560
|
renderKeyFindings(report),
|
|
1398
1561
|
"",
|
|
1399
1562
|
renderPrClassContext(report.prClasses),
|
|
1563
|
+
renderProfileSuggestions(report),
|
|
1400
1564
|
renderSharedSignalInterpretation(sharedSignals),
|
|
1401
1565
|
renderSensitivityAnalysis(report.sensitivity),
|
|
1402
1566
|
"## How Bottlenecks Are Prioritized",
|
|
@@ -1451,6 +1615,9 @@ export function renderRepositoryFrictionMarkdown(report) {
|
|
|
1451
1615
|
"",
|
|
1452
1616
|
"- Pull requests are selected upstream by the collection or fixture workflow; this renderer explains the resulting metrics summary.",
|
|
1453
1617
|
"- File roles and functional surfaces come from repository-profile classification, not from language names alone.",
|
|
1618
|
+
profileSuggestions(report).length
|
|
1619
|
+
? "- Profile suggestions are optional interpretation improvements derived from existing report evidence; they do not change scores, rankings, CSV exports, or JSON report fields."
|
|
1620
|
+
: "- No profile suggestion thresholds were triggered by this report's PR class, role, or functional-surface evidence.",
|
|
1454
1621
|
"- Bottlenecks are ranked by their strongest representative observed signal, with stable category order only used to break ties.",
|
|
1455
1622
|
"- Recommendations are inferred from transparent component evidence and representative PR examples; they are not automated changes.",
|
|
1456
1623
|
"- Missing or partial GitHub data remains visible in coverage tables rather than being inferred from unrelated fields.",
|