delivery-friction-analyzer 0.7.2 → 0.8.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/docs/contracts/friction-report.md +3 -1
- package/package.json +1 -1
- package/release-log.md +8 -0
- package/src/report/friction-report.js +124 -43
|
@@ -64,7 +64,8 @@ The Markdown renderer presents the same report data for human review:
|
|
|
64
64
|
- outlier and sensitivity analysis when displayed examples are dominated by one PR;
|
|
65
65
|
- 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
66
|
- ranked bottlenecks with representative PR examples rendered as compact PR-size tables;
|
|
67
|
-
- validation, review, and source-label evidence for
|
|
67
|
+
- validation, review, and source-label evidence for representative PR examples rendered as compact evidence tables;
|
|
68
|
+
- text-backed status labels such as observed, partial, unavailable, configured, warning, and healthy in Markdown evidence tables where they improve scanability;
|
|
68
69
|
- separately labeled inferred diagnosis, suggested action, and confidence/caveat blocks for each bottleneck;
|
|
69
70
|
- shared-evidence notes when multiple recommendation categories use the same representative PR set;
|
|
70
71
|
- recommendation-category, comment-source, and core/support-surface tables;
|
|
@@ -73,6 +74,7 @@ The Markdown renderer presents the same report data for human review:
|
|
|
73
74
|
- guardrails, follow-up, and artifact-sensitivity guidance.
|
|
74
75
|
|
|
75
76
|
Markdown output should not include individual contributor or reviewer rankings.
|
|
77
|
+
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.
|
|
76
78
|
|
|
77
79
|
## Recommendation Boundaries
|
|
78
80
|
|
package/package.json
CHANGED
package/release-log.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-06-20 — Report Evidence Status Tables
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- Why it matters: Maintainers can compare bottleneck evidence more quickly while still seeing when evidence is configured, incomplete, unavailable, or directly observed.
|
|
9
|
+
- Who is affected: Maintainers and contributors reviewing generated Markdown friction reports.
|
|
10
|
+
- Action needed: None.
|
|
11
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/44
|
|
12
|
+
|
|
5
13
|
### 2026-06-19 — Interactive Setup Choice Presets
|
|
6
14
|
|
|
7
15
|
- 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.
|
|
@@ -933,56 +933,137 @@ function renderEvidenceTable(observedData) {
|
|
|
933
933
|
);
|
|
934
934
|
}
|
|
935
935
|
|
|
936
|
-
function
|
|
937
|
-
return
|
|
936
|
+
function statusLabel(status, detail) {
|
|
937
|
+
return `[${status}] ${detail}`;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function coverageStatusLabel(value) {
|
|
941
|
+
switch (value) {
|
|
942
|
+
case "observed":
|
|
943
|
+
case "available":
|
|
944
|
+
return "observed";
|
|
945
|
+
case "partial":
|
|
946
|
+
return "partial";
|
|
947
|
+
default:
|
|
948
|
+
return "unavailable";
|
|
949
|
+
}
|
|
938
950
|
}
|
|
939
951
|
|
|
940
|
-
function
|
|
952
|
+
function validationStatusSummary(validationEvidence = {}) {
|
|
953
|
+
const coverage = validationEvidence.workflowRunCoverage ?? "unavailable";
|
|
954
|
+
const coverageStatus = coverageStatusLabel(coverage);
|
|
955
|
+
const failedCheckRuns = Number(validationEvidence.failedCheckRuns ?? 0);
|
|
956
|
+
const failedWorkflowRuns = Number(validationEvidence.failedWorkflowRuns ?? 0);
|
|
957
|
+
const cancelledWorkflowRuns = Number(validationEvidence.cancelledWorkflowRuns ?? 0);
|
|
958
|
+
const interruptionCount = failedCheckRuns + failedWorkflowRuns + cancelledWorkflowRuns;
|
|
959
|
+
const coverageLabel = statusLabel(coverageStatus, `workflow coverage: ${coverage}`);
|
|
960
|
+
let outcomeLabel;
|
|
961
|
+
if (interruptionCount > 0) {
|
|
962
|
+
outcomeLabel = statusLabel("warning", `${failedCheckRuns} failed checks, ${failedWorkflowRuns} failed workflows, ${cancelledWorkflowRuns} cancelled workflow runs`);
|
|
963
|
+
} else if (coverageStatus === "observed") {
|
|
964
|
+
outcomeLabel = statusLabel("healthy", "no failed or cancelled validation runs");
|
|
965
|
+
} else if (coverageStatus === "partial") {
|
|
966
|
+
outcomeLabel = statusLabel("partial", "no failed or cancelled validation runs in sampled workflow evidence");
|
|
967
|
+
} else {
|
|
968
|
+
outcomeLabel = statusLabel("unavailable", "validation outcome unavailable");
|
|
969
|
+
}
|
|
970
|
+
const conclusions = formatNamedValues(validationEvidence.workflowRunConclusions ?? []);
|
|
971
|
+
|
|
972
|
+
return `${coverageLabel}; ${outcomeLabel}; conclusions: ${conclusions}`;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function reviewThreadStatusLabel(reviewEvidence = {}) {
|
|
976
|
+
const source = reviewEvidence.reviewThreadSource ?? "unavailable";
|
|
977
|
+
if (source === "unavailable") return "unavailable";
|
|
978
|
+
if (source.startsWith("not_sampled")) return "partial";
|
|
979
|
+
return "observed";
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function reviewStatusSummary(reviewEvidence = {}) {
|
|
983
|
+
const threadLabel = statusLabel(
|
|
984
|
+
reviewThreadStatusLabel(reviewEvidence),
|
|
985
|
+
`threads: ${reviewEvidence.reviewThreads ?? 0}, resolved: ${reviewEvidence.resolvedThreads ?? 0}, outdated: ${reviewEvidence.outdatedThreads ?? 0}`,
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
return threadLabel;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function reviewDecisionSummary(reviewEvidence = {}) {
|
|
992
|
+
const observedReviewDecision = hasObservedReviewDecision({
|
|
993
|
+
state: reviewEvidence.reviewDecision,
|
|
994
|
+
source: reviewEvidence.reviewDecisionSource,
|
|
995
|
+
});
|
|
996
|
+
const status = observedReviewDecision ? "observed" : "unavailable";
|
|
997
|
+
const humanApproved = formatObservedBoolean(reviewEvidence.humanApproved, observedReviewDecision);
|
|
998
|
+
const humanChangesRequested = formatObservedBoolean(reviewEvidence.humanChangesRequested, observedReviewDecision);
|
|
999
|
+
const humanReviewerCount = formatObservedCount(reviewEvidence.humanReviewerCount, observedReviewDecision);
|
|
1000
|
+
const approvalStatus = observedReviewDecision && reviewEvidence.humanApproved
|
|
1001
|
+
? `; ${statusLabel("healthy", "human approval observed")}`
|
|
1002
|
+
: "";
|
|
1003
|
+
|
|
1004
|
+
return `${statusLabel(status, `${reviewEvidence.reviewDecision ?? "unavailable"} from ${reviewEvidence.reviewDecisionSource ?? "unavailable"}`)}; human reviewers: ${humanReviewerCount}; approved: ${humanApproved}; changes requested: ${humanChangesRequested}${approvalStatus}`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function commentSourceSummary(reviewEvidence = {}) {
|
|
1008
|
+
const commentSources = reviewEvidence.commentSources ?? [];
|
|
1009
|
+
if (commentSources.length) return statusLabel("observed", formatNamedValues(commentSources));
|
|
1010
|
+
const reviewStatus = reviewThreadStatusLabel(reviewEvidence);
|
|
1011
|
+
if (reviewStatus === "observed") return statusLabel("observed", "none");
|
|
1012
|
+
if (reviewStatus === "partial") return statusLabel("partial", "none in sampled review-thread evidence");
|
|
1013
|
+
return statusLabel("unavailable", "comment sources unavailable");
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function prClassStatusSummary(prClass = {}) {
|
|
1017
|
+
const source = prClass.classificationSource ?? "fallback_rule";
|
|
1018
|
+
const status = source === "fallback_rule"
|
|
1019
|
+
? "observed"
|
|
1020
|
+
: source === "unavailable" ? "unavailable" : "configured";
|
|
1021
|
+
return statusLabel(status, formatPrClass(prClass));
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function workflowSourceSummary(validationEvidence = {}) {
|
|
1025
|
+
const source = validationEvidence.workflowRunSource ?? "unavailable";
|
|
1026
|
+
const status = source === "unavailable" ? "unavailable" : "observed";
|
|
1027
|
+
return statusLabel(status, source);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
function reviewThreadSourceSummary(reviewEvidence = {}) {
|
|
1031
|
+
const source = reviewEvidence.reviewThreadSource ?? "unavailable";
|
|
1032
|
+
return statusLabel(reviewThreadStatusLabel(reviewEvidence), source);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function sourceLabelSummary(evidence = {}) {
|
|
1036
|
+
return [
|
|
1037
|
+
`PR class: ${prClassStatusSummary(evidence.prClass)}`,
|
|
1038
|
+
`Review thread source: ${reviewThreadSourceSummary(evidence.reviewEvidence)}`,
|
|
1039
|
+
`Workflow source: ${workflowSourceSummary(evidence.validationEvidence)}`,
|
|
1040
|
+
].join("; ");
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function evidenceDetailRows(observedData) {
|
|
941
1044
|
return (observedData ?? []).map(evidence => {
|
|
942
1045
|
const validationEvidence = evidence.validationEvidence ?? {};
|
|
943
1046
|
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
1047
|
|
|
951
1048
|
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");
|
|
1049
|
+
prReference(evidence),
|
|
1050
|
+
validationStatusSummary(validationEvidence),
|
|
1051
|
+
`${reviewStatusSummary(reviewEvidence)}; ${reviewDecisionSummary(reviewEvidence)}; comments: ${commentSourceSummary(reviewEvidence)}`,
|
|
1052
|
+
sourceLabelSummary(evidence),
|
|
1053
|
+
];
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function renderEvidenceDetails(observedData) {
|
|
1058
|
+
return renderMarkdownTable(
|
|
1059
|
+
[
|
|
1060
|
+
"PR",
|
|
1061
|
+
"Validation",
|
|
1062
|
+
"Review",
|
|
1063
|
+
"Source labels",
|
|
1064
|
+
],
|
|
1065
|
+
evidenceDetailRows(observedData),
|
|
1066
|
+
);
|
|
986
1067
|
}
|
|
987
1068
|
|
|
988
1069
|
function recommendationCategoryLabel(bottleneck) {
|