delivery-friction-analyzer 0.7.3 → 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.
@@ -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 each representative PR example rendered as plain Markdown detail lists;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.7.3",
3
+ "version": "0.8.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-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 renderDetailList(items) {
937
- return items.length ? items.map(item => `- ${escapeMarkdownText(item)}`).join("\n") : "- None";
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 renderEvidenceDetails(observedData) {
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
- `Evidence details for PR #${evidence.number}:`,
953
- "",
954
- "Validation:",
955
- "",
956
- renderDetailList([
957
- `Workflow coverage: ${validationEvidence.workflowRunCoverage ?? "unavailable"}`,
958
- `Workflow conclusions: ${formatNamedValues(workflowRunConclusions)}`,
959
- `Failed checks: ${validationEvidence.failedCheckRuns ?? 0}`,
960
- `Failed workflows: ${validationEvidence.failedWorkflowRuns ?? 0}`,
961
- `Cancelled workflows: ${validationEvidence.cancelledWorkflowRuns ?? 0}`,
962
- ]),
963
- "",
964
- "Review:",
965
- "",
966
- renderDetailList([
967
- `Review thread source: ${reviewEvidence.reviewThreadSource ?? "unavailable"}`,
968
- `Threads: ${reviewEvidence.reviewThreads ?? 0}`,
969
- `Resolved threads: ${reviewEvidence.resolvedThreads ?? 0}`,
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) {