delivery-friction-analyzer 0.8.0 → 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,6 +60,7 @@ 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;
|
|
@@ -75,6 +76,7 @@ The Markdown renderer presents the same report data for human review:
|
|
|
75
76
|
|
|
76
77
|
Markdown output should not include individual contributor or reviewer rankings.
|
|
77
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.
|
|
78
80
|
|
|
79
81
|
## Recommendation Boundaries
|
|
80
82
|
|
|
@@ -109,6 +111,7 @@ Full live analysis writes `methodology.md` as a hybrid artifact: stable explanat
|
|
|
109
111
|
- target repository and report/metric versions;
|
|
110
112
|
- profile path when available;
|
|
111
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;
|
|
112
115
|
- requested and collected PR counts;
|
|
113
116
|
- collection coverage status and API-family diagnostics;
|
|
114
117
|
- scoring, ranking, dominance, sensitivity, and limitation explanations;
|
package/package.json
CHANGED
package/release-log.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
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
|
+
|
|
5
13
|
### 2026-06-20 — Report Evidence Status Tables
|
|
6
14
|
|
|
7
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.
|
|
@@ -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;
|
|
@@ -1294,6 +1355,27 @@ function renderPrClassContext(prClasses) {
|
|
|
1294
1355
|
].join("\n");
|
|
1295
1356
|
}
|
|
1296
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
|
+
|
|
1297
1379
|
function renderConfiguredWorkflowContext(configuredWorkflow) {
|
|
1298
1380
|
const entries = configuredWorkflowEntries(configuredWorkflow);
|
|
1299
1381
|
if (!entries.length) return "";
|
|
@@ -1478,6 +1560,7 @@ export function renderRepositoryFrictionMarkdown(report) {
|
|
|
1478
1560
|
renderKeyFindings(report),
|
|
1479
1561
|
"",
|
|
1480
1562
|
renderPrClassContext(report.prClasses),
|
|
1563
|
+
renderProfileSuggestions(report),
|
|
1481
1564
|
renderSharedSignalInterpretation(sharedSignals),
|
|
1482
1565
|
renderSensitivityAnalysis(report.sensitivity),
|
|
1483
1566
|
"## How Bottlenecks Are Prioritized",
|
|
@@ -1532,6 +1615,9 @@ export function renderRepositoryFrictionMarkdown(report) {
|
|
|
1532
1615
|
"",
|
|
1533
1616
|
"- Pull requests are selected upstream by the collection or fixture workflow; this renderer explains the resulting metrics summary.",
|
|
1534
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.",
|
|
1535
1621
|
"- Bottlenecks are ranked by their strongest representative observed signal, with stable category order only used to break ties.",
|
|
1536
1622
|
"- Recommendations are inferred from transparent component evidence and representative PR examples; they are not automated changes.",
|
|
1537
1623
|
"- Missing or partial GitHub data remains visible in coverage tables rather than being inferred from unrelated fields.",
|