delivery-friction-analyzer 0.6.1 → 0.7.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/README.md +16 -2
- package/docs/reference/repository-profile.md +79 -2
- package/package.json +1 -1
- package/release-log.md +8 -0
- package/src/cli/analyze-github.js +127 -9
- package/src/profile/pr-class-presets.js +47 -0
package/README.md
CHANGED
|
@@ -64,7 +64,21 @@ Profiles can define:
|
|
|
64
64
|
- PR classes such as release, dependency, feature, or other repository-specific groups;
|
|
65
65
|
- workflow context such as merge method, release strategy, and branch strategy.
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
For a new repository, the easiest path is to let interactive setup create the profile file you plan to use:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm run analyze:github -- \
|
|
71
|
+
--interactive \
|
|
72
|
+
--repo owner/name \
|
|
73
|
+
--limit 30 \
|
|
74
|
+
--profile profiles/owner-name.json \
|
|
75
|
+
--out reports/owner-name \
|
|
76
|
+
--dry-run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If `profiles/owner-name.json` does not exist, interactive setup asks whether to create it, then writes a minimal `repository-profile.v1` profile with confirmed workflow context and optional release PR title rules. `--dry-run` still validates repository access, profile JSON, output directory writability, and a small sample of GitHub API coverage. It may create the output directory and briefly write then remove a temporary probe file to confirm writability, but it does not write the full report bundle. When interactive setup saves or generates a profile during a dry run, the completion output prints the saved profile path so you can inspect and edit it before a full run.
|
|
80
|
+
|
|
81
|
+
Use `fixtures/github/mcp-writing/profile.json` as a starting point when you prefer to copy an existing profile by hand. The full profile format is documented in `docs/reference/repository-profile.md`, and the schema lives at `schemas/repository-profile.schema.json`.
|
|
68
82
|
|
|
69
83
|
## Outputs
|
|
70
84
|
|
|
@@ -85,7 +99,7 @@ Each ranked bottleneck example includes source references, workflow-run conclusi
|
|
|
85
99
|
|
|
86
100
|
## Common Options
|
|
87
101
|
|
|
88
|
-
Use `--dry-run` or `--metadata-only` to validate repository access, profile JSON, output directory writability, and sampled API coverage without writing full report artifacts.
|
|
102
|
+
Use `--dry-run` or `--metadata-only` to validate repository access, profile JSON, output directory writability, and sampled API coverage without writing full report artifacts. The output directory may be created during this check, and a temporary probe file may be written and removed.
|
|
89
103
|
|
|
90
104
|
Use `--no-csv` when you want the Markdown, JSON, source, normalized, metrics, and methodology artifacts without spreadsheet-friendly CSV exports.
|
|
91
105
|
|
|
@@ -37,7 +37,7 @@ This keeps validation-target details in profile data rather than hardcoded produ
|
|
|
37
37
|
|
|
38
38
|
## Pull Request Classes
|
|
39
39
|
|
|
40
|
-
`prClasses` is optional. Rules are evaluated in order and the first matching rule wins.
|
|
40
|
+
`prClasses` is optional. Rules are evaluated in order and the first matching rule wins. The current profile contract supports title-only matchers:
|
|
41
41
|
|
|
42
42
|
- `titleIncludes`: literal substring match against the PR title.
|
|
43
43
|
- `titleRegex`: JavaScript regular expression matched against the PR title.
|
|
@@ -52,9 +52,86 @@ If both matchers are present on one rule, both must match. If no rule matches, t
|
|
|
52
52
|
}
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
+
In reports, `unknown` means no configured `prClasses` rule matched that PR title. It is the transparent no-matching-rule fallback, not a collection failure and not an inferred repository taxonomy.
|
|
56
|
+
|
|
55
57
|
Class identifiers are validated as lower-kebab-case or lower_snake_case strings. Profile validation rejects duplicate PR class rule IDs, empty match objects, invalid class identifiers, and invalid title regexes.
|
|
56
58
|
|
|
57
|
-
|
|
59
|
+
PR class evidence is interpretive and profile-driven. It helps reports show class distributions, dominance notes, and explicit `--exclude-pr-class` filtering when you request filtering, but configured PR class rules do not change default scoring, ranking formulas, collection, or CSV export shape by themselves.
|
|
60
|
+
|
|
61
|
+
Interactive setup can add a release PR class rule from a confirmed title convention using the current title-only matcher shape. Branch strategy answers stay in `workflow` context only; they do not create branch-based PR class matching. Branch-based class matching is deferred until a future matcher contract supports branch fields explicitly.
|
|
62
|
+
|
|
63
|
+
### Copyable PR Class Examples
|
|
64
|
+
|
|
65
|
+
Add one `prClasses` array to the top level of a profile, next to `repository` and `rules`. Keep the rules ordered from most specific to broadest because the first match wins.
|
|
66
|
+
|
|
67
|
+
Release PRs with titles such as `Release 2026.06.19`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"prClasses": [
|
|
72
|
+
{
|
|
73
|
+
"id": "release-title",
|
|
74
|
+
"class": "release",
|
|
75
|
+
"match": { "titleRegex": "^Release\\b" }
|
|
76
|
+
}
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Dependency PRs with Dependabot-style titles and common dependency prefixes:
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"prClasses": [
|
|
86
|
+
{
|
|
87
|
+
"id": "dependency-title",
|
|
88
|
+
"class": "dependency",
|
|
89
|
+
"match": { "titleRegex": "^(?:deps!?|(?:build|chore|fix)\\(deps\\)!?):|^Bump\\b" }
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Conventional Commit-style PR titles. Dependency-scoped rules come first so `chore(deps): ...` and `fix(deps): ...` do not fall through to maintenance or fix classes:
|
|
96
|
+
|
|
97
|
+
```json
|
|
98
|
+
{
|
|
99
|
+
"prClasses": [
|
|
100
|
+
{
|
|
101
|
+
"id": "conventional-dependency",
|
|
102
|
+
"class": "dependency",
|
|
103
|
+
"match": { "titleRegex": "^(?:deps!?|(?:build|chore|fix)\\(deps\\)!?):|^Bump\\b" }
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
"id": "conventional-feature",
|
|
107
|
+
"class": "feature",
|
|
108
|
+
"match": { "titleRegex": "^feat(?:\\([^)]+\\))?!?:" }
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"id": "conventional-fix",
|
|
112
|
+
"class": "fix",
|
|
113
|
+
"match": { "titleRegex": "^fix(?:\\((?!deps\\))[^)]+\\))?!?:" }
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
"id": "conventional-docs",
|
|
117
|
+
"class": "docs",
|
|
118
|
+
"match": { "titleRegex": "^docs(?:\\([^)]+\\))?!?:" }
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "conventional-test",
|
|
122
|
+
"class": "test",
|
|
123
|
+
"match": { "titleRegex": "^test(?:\\([^)]+\\))?!?:" }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"id": "conventional-maintenance",
|
|
127
|
+
"class": "maintenance",
|
|
128
|
+
"match": { "titleRegex": "^(refactor|perf|style|build|ci)(?:\\([^)]+\\))?!?:|^chore(?:\\((?!deps\\))[^)]+\\))?!?:" }
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
These examples are starting points, not a universal PR taxonomy. Adjust class names and title patterns to match the repository's own conventions.
|
|
58
135
|
|
|
59
136
|
## Workflow Context
|
|
60
137
|
|
package/package.json
CHANGED
package/release-log.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-06-19 — Interactive Setup Choice Presets
|
|
6
|
+
|
|
7
|
+
- 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.
|
|
8
|
+
- Why it matters: Maintainers can capture common repository assumptions without typing schema identifiers or hand-writing the first set of title-based PR class rules.
|
|
9
|
+
- Who is affected: Maintainers running `--interactive` to create or update repository profiles.
|
|
10
|
+
- Action needed: None.
|
|
11
|
+
- PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/40
|
|
12
|
+
|
|
5
13
|
### 2026-06-19 — Workflow Context Surfacing
|
|
6
14
|
|
|
7
15
|
- What changed: Friction reports and methodology now show configured repository workflow context from the profile when it is present.
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
renderRepositoryFrictionMarkdown,
|
|
18
18
|
} from "../report/friction-report.js";
|
|
19
19
|
import { assertValidPrClassRules } from "../profile/pr-class.js";
|
|
20
|
+
import { conventionalCommitPrClassRules } from "../profile/pr-class-presets.js";
|
|
20
21
|
import {
|
|
21
22
|
WORKFLOW_BRANCH_STRATEGIES,
|
|
22
23
|
WORKFLOW_PRIMARY_MERGE_METHODS,
|
|
@@ -197,6 +198,20 @@ function defaultOutDirForRepository(repository) {
|
|
|
197
198
|
return join("reports", name || "analysis");
|
|
198
199
|
}
|
|
199
200
|
|
|
201
|
+
function choiceValue(choice) {
|
|
202
|
+
return typeof choice === "object" && choice !== null ? choice.value : choice;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function choiceLabel(choice) {
|
|
206
|
+
return typeof choice === "object" && choice !== null ? choice.label : choice;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function formatChoiceList(choices) {
|
|
210
|
+
return choices
|
|
211
|
+
.map((choice, index) => `${index + 1}. ${choiceLabel(choice)} (${choiceValue(choice)})`)
|
|
212
|
+
.join("\n");
|
|
213
|
+
}
|
|
214
|
+
|
|
200
215
|
function formatInteractivePrompt(prompt) {
|
|
201
216
|
const suffix = prompt.defaultValue === undefined
|
|
202
217
|
? ""
|
|
@@ -207,10 +222,10 @@ function formatInteractivePrompt(prompt) {
|
|
|
207
222
|
return `${prompt.message}${prompt.defaultValue ? " [Y/n]" : " [y/N]"} `;
|
|
208
223
|
}
|
|
209
224
|
if (prompt.type === "multi-select" && prompt.choices?.length) {
|
|
210
|
-
return `${prompt.message} (${prompt.choices.join(",")})${suffix}: `;
|
|
225
|
+
return `${prompt.message} (${prompt.choices.map(choiceValue).join(",")})${suffix}: `;
|
|
211
226
|
}
|
|
212
227
|
if (prompt.type === "select" && prompt.choices?.length) {
|
|
213
|
-
return `${prompt.message}
|
|
228
|
+
return `${prompt.message}\n${formatChoiceList(prompt.choices)}\nChoose a number or identifier${suffix}: `;
|
|
214
229
|
}
|
|
215
230
|
return `${prompt.message}${suffix}: `;
|
|
216
231
|
}
|
|
@@ -277,8 +292,14 @@ function normalizeConfirmAnswer(raw, prompt) {
|
|
|
277
292
|
|
|
278
293
|
function normalizeChoiceAnswer(raw, prompt) {
|
|
279
294
|
const value = normalizeTextAnswer(raw, prompt);
|
|
280
|
-
|
|
281
|
-
|
|
295
|
+
const choices = prompt.choices ?? [];
|
|
296
|
+
const numericIndex = Number(value);
|
|
297
|
+
if (Number.isInteger(numericIndex) && numericIndex >= 1 && numericIndex <= choices.length) {
|
|
298
|
+
return choiceValue(choices[numericIndex - 1]);
|
|
299
|
+
}
|
|
300
|
+
const match = choices.find(choice => choiceValue(choice) === value || choiceLabel(choice) === value);
|
|
301
|
+
if (match) return choiceValue(match);
|
|
302
|
+
throw new Error(`${prompt.id} must be one of: ${choices.map(choiceValue).join(", ")}`);
|
|
282
303
|
}
|
|
283
304
|
|
|
284
305
|
function normalizeMultiSelectAnswer(raw, prompt) {
|
|
@@ -519,12 +540,74 @@ function releasePrClassRule(profile, titleIncludes, existingRule = null) {
|
|
|
519
540
|
};
|
|
520
541
|
}
|
|
521
542
|
|
|
543
|
+
function withAvailablePrClassRuleIds(profile, rules) {
|
|
544
|
+
const profileWithIds = {
|
|
545
|
+
...profile,
|
|
546
|
+
prClasses: Array.isArray(profile.prClasses) ? [...profile.prClasses] : [],
|
|
547
|
+
};
|
|
548
|
+
return rules.map(rule => {
|
|
549
|
+
const id = nextPrClassRuleId(profileWithIds, rule.id);
|
|
550
|
+
const nextRule = {
|
|
551
|
+
...rule,
|
|
552
|
+
id,
|
|
553
|
+
match: { ...rule.match },
|
|
554
|
+
};
|
|
555
|
+
profileWithIds.prClasses.push(nextRule);
|
|
556
|
+
return nextRule;
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function promptConventionalCommitPrClassPreset(promptAdapter, output, profile) {
|
|
561
|
+
const hasExistingPrClasses = Array.isArray(profile.prClasses) && profile.prClasses.length > 0;
|
|
562
|
+
const message = hasExistingPrClasses
|
|
563
|
+
? "Add Conventional Commit PR class preset to existing PR class rules"
|
|
564
|
+
: "Add Conventional Commit PR class preset";
|
|
565
|
+
const shouldAddPreset = await askUntilValid(promptAdapter, {
|
|
566
|
+
id: "addConventionalCommitPrClasses",
|
|
567
|
+
type: "confirm",
|
|
568
|
+
message,
|
|
569
|
+
defaultValue: false,
|
|
570
|
+
}, {
|
|
571
|
+
output,
|
|
572
|
+
normalize: normalizeConfirmAnswer,
|
|
573
|
+
validate() {},
|
|
574
|
+
});
|
|
575
|
+
if (!shouldAddPreset) return { profile, prClassRulesWritten: false };
|
|
576
|
+
|
|
577
|
+
const updated = cloneJson(profile);
|
|
578
|
+
updated.prClasses = Array.isArray(updated.prClasses) ? [...updated.prClasses] : [];
|
|
579
|
+
const presetRules = withAvailablePrClassRuleIds(updated, conventionalCommitPrClassRules());
|
|
580
|
+
updated.prClasses.push(...presetRules);
|
|
581
|
+
return { profile: updated, prClassRulesWritten: true };
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const WORKFLOW_CHOICE_LABELS = Object.freeze({
|
|
585
|
+
merge_commit: "Merge commits",
|
|
586
|
+
squash_merge: "Squash merges",
|
|
587
|
+
rebase_merge: "Rebase merges",
|
|
588
|
+
release_prs: "Release PRs",
|
|
589
|
+
direct_tags: "Direct tags",
|
|
590
|
+
release_branches: "Release branches",
|
|
591
|
+
trunk_based: "Trunk-based",
|
|
592
|
+
main_plus_release_branches: "Main plus release branches",
|
|
593
|
+
long_lived_development_branches: "Long-lived development branches",
|
|
594
|
+
mixed: "Mixed",
|
|
595
|
+
unknown: "Unknown",
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
function workflowChoices(values) {
|
|
599
|
+
return values.map(value => ({
|
|
600
|
+
value,
|
|
601
|
+
label: WORKFLOW_CHOICE_LABELS[value] ?? value,
|
|
602
|
+
}));
|
|
603
|
+
}
|
|
604
|
+
|
|
522
605
|
async function promptWorkflowField(promptAdapter, output, { id, message, choices, defaultValue }) {
|
|
523
606
|
return askUntilValid(promptAdapter, {
|
|
524
607
|
id,
|
|
525
608
|
type: "select",
|
|
526
609
|
message,
|
|
527
|
-
choices,
|
|
610
|
+
choices: workflowChoices(choices),
|
|
528
611
|
defaultValue,
|
|
529
612
|
}, {
|
|
530
613
|
output,
|
|
@@ -535,6 +618,7 @@ async function promptWorkflowField(promptAdapter, output, { id, message, choices
|
|
|
535
618
|
|
|
536
619
|
async function promptWorkflowProfileUpdate(promptAdapter, output, profile, { isNewProfile }) {
|
|
537
620
|
const updated = cloneJson(profile);
|
|
621
|
+
let prClassRulesWritten = false;
|
|
538
622
|
updated.workflow = {
|
|
539
623
|
primaryMergeMethod: await promptWorkflowField(promptAdapter, output, {
|
|
540
624
|
id: "primaryMergeMethod",
|
|
@@ -588,6 +672,7 @@ async function promptWorkflowProfileUpdate(promptAdapter, output, profile, { isN
|
|
|
588
672
|
titleIncludes,
|
|
589
673
|
updated.prClasses[updateableReleaseIndex],
|
|
590
674
|
);
|
|
675
|
+
prClassRulesWritten = true;
|
|
591
676
|
}
|
|
592
677
|
} else {
|
|
593
678
|
const shouldAddReleaseRule = isNewProfile
|
|
@@ -604,13 +689,18 @@ async function promptWorkflowProfileUpdate(promptAdapter, output, profile, { isN
|
|
|
604
689
|
});
|
|
605
690
|
if (shouldAddReleaseRule) {
|
|
606
691
|
updated.prClasses.push(releasePrClassRule(updated, titleIncludes));
|
|
692
|
+
prClassRulesWritten = true;
|
|
607
693
|
}
|
|
608
694
|
}
|
|
609
695
|
}
|
|
610
696
|
}
|
|
611
697
|
|
|
612
|
-
|
|
613
|
-
|
|
698
|
+
const presetUpdate = await promptConventionalCommitPrClassPreset(promptAdapter, output, updated);
|
|
699
|
+
validateProfile(presetUpdate.profile);
|
|
700
|
+
return {
|
|
701
|
+
profile: presetUpdate.profile,
|
|
702
|
+
prClassRulesWritten: prClassRulesWritten || presetUpdate.prClassRulesWritten,
|
|
703
|
+
};
|
|
614
704
|
}
|
|
615
705
|
|
|
616
706
|
async function maybeConfigureInteractiveProfile(promptAdapter, output, profileState, repository) {
|
|
@@ -644,15 +734,33 @@ async function maybeConfigureInteractiveProfile(promptAdapter, output, profileSt
|
|
|
644
734
|
validate() {},
|
|
645
735
|
});
|
|
646
736
|
if (!shouldConfigureWorkflow) {
|
|
737
|
+
const presetUpdate = await promptConventionalCommitPrClassPreset(promptAdapter, output, profile);
|
|
738
|
+
validateProfile(presetUpdate.profile);
|
|
739
|
+
if (presetUpdate.prClassRulesWritten) {
|
|
740
|
+
const savedProfilePath = await writeInteractiveProfile(profileState.profilePath, presetUpdate.profile, {
|
|
741
|
+
exists: profileState.exists,
|
|
742
|
+
originalProfile,
|
|
743
|
+
originalText: profileState.text,
|
|
744
|
+
originalIsSymbolicLink: profileState.isSymbolicLink,
|
|
745
|
+
});
|
|
746
|
+
return {
|
|
747
|
+
profile: presetUpdate.profile,
|
|
748
|
+
profilePath: savedProfilePath,
|
|
749
|
+
savedProfilePath,
|
|
750
|
+
prClassRulesWritten: true,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
647
753
|
return {
|
|
648
754
|
profile,
|
|
649
755
|
profilePath: profileState.profilePath,
|
|
650
756
|
savedProfilePath: null,
|
|
757
|
+
prClassRulesWritten: false,
|
|
651
758
|
};
|
|
652
759
|
}
|
|
653
760
|
}
|
|
654
761
|
|
|
655
|
-
|
|
762
|
+
const profileUpdate = await promptWorkflowProfileUpdate(promptAdapter, output, profile, { isNewProfile });
|
|
763
|
+
profile = profileUpdate.profile;
|
|
656
764
|
const savedProfilePath = await writeInteractiveProfile(profileState.profilePath, profile, {
|
|
657
765
|
exists: profileState.exists,
|
|
658
766
|
originalProfile,
|
|
@@ -663,6 +771,7 @@ async function maybeConfigureInteractiveProfile(promptAdapter, output, profileSt
|
|
|
663
771
|
profile,
|
|
664
772
|
profilePath: savedProfilePath,
|
|
665
773
|
savedProfilePath,
|
|
774
|
+
prClassRulesWritten: profileUpdate.prClassRulesWritten,
|
|
666
775
|
};
|
|
667
776
|
}
|
|
668
777
|
|
|
@@ -774,6 +883,7 @@ export async function collectInteractiveAnalyzeGithubOptions(options, {
|
|
|
774
883
|
resolved.profilePath = profileUpdate.profilePath;
|
|
775
884
|
if (profileUpdate.savedProfilePath) {
|
|
776
885
|
resolved.savedProfilePath = profileUpdate.savedProfilePath;
|
|
886
|
+
resolved.prClassRulesWritten = profileUpdate.prClassRulesWritten;
|
|
777
887
|
if (typeof onSavedProfilePath === "function") {
|
|
778
888
|
onSavedProfilePath(profileUpdate.savedProfilePath);
|
|
779
889
|
}
|
|
@@ -1022,7 +1132,7 @@ function attachCollectionCoverage(report, sourceBundle) {
|
|
|
1022
1132
|
};
|
|
1023
1133
|
}
|
|
1024
1134
|
|
|
1025
|
-
function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report, requestedLimit, sampledLimit, csv, analysisFilter, savedProfilePath }) {
|
|
1135
|
+
function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report, requestedLimit, sampledLimit, csv, analysisFilter, savedProfilePath, prClassRulesWritten }) {
|
|
1026
1136
|
const summary = {
|
|
1027
1137
|
ok: true,
|
|
1028
1138
|
dryRun,
|
|
@@ -1041,6 +1151,9 @@ function summarizeResult({ dryRun, outDir, paths, sourceBundle, metrics, report,
|
|
|
1041
1151
|
if (savedProfilePath) {
|
|
1042
1152
|
summary.savedProfilePath = savedProfilePath;
|
|
1043
1153
|
}
|
|
1154
|
+
if (prClassRulesWritten) {
|
|
1155
|
+
summary.prClassRulesWritten = true;
|
|
1156
|
+
}
|
|
1044
1157
|
return summary;
|
|
1045
1158
|
}
|
|
1046
1159
|
|
|
@@ -1114,6 +1227,7 @@ export async function runAnalyzeGithub(options, {
|
|
|
1114
1227
|
csv: false,
|
|
1115
1228
|
analysisFilter: null,
|
|
1116
1229
|
savedProfilePath: options.savedProfilePath,
|
|
1230
|
+
prClassRulesWritten: options.prClassRulesWritten,
|
|
1117
1231
|
});
|
|
1118
1232
|
}
|
|
1119
1233
|
|
|
@@ -1166,6 +1280,7 @@ export async function runAnalyzeGithub(options, {
|
|
|
1166
1280
|
csv: csvEnabled,
|
|
1167
1281
|
analysisFilter: normalized.analysisFilter ?? null,
|
|
1168
1282
|
savedProfilePath: options.savedProfilePath,
|
|
1283
|
+
prClassRulesWritten: options.prClassRulesWritten,
|
|
1169
1284
|
});
|
|
1170
1285
|
}
|
|
1171
1286
|
|
|
@@ -1238,6 +1353,9 @@ export function formatAnalyzeGithubCompletion(result) {
|
|
|
1238
1353
|
if (result.savedProfilePath) {
|
|
1239
1354
|
lines.push(`Repository profile saved: ${result.savedProfilePath}.`);
|
|
1240
1355
|
}
|
|
1356
|
+
if (result.prClassRulesWritten) {
|
|
1357
|
+
lines.push("PR class rules written: Conventional Commit preset or release title rule.");
|
|
1358
|
+
}
|
|
1241
1359
|
|
|
1242
1360
|
lines.push(`Collection coverage: ${result.collectionCoverage?.status ?? "unknown"}.`);
|
|
1243
1361
|
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export const CONVENTIONAL_COMMIT_PR_CLASS_PRESET = Object.freeze([
|
|
2
|
+
Object.freeze({
|
|
3
|
+
id: "conventional-dependency",
|
|
4
|
+
class: "dependency",
|
|
5
|
+
match: Object.freeze({ titleRegex: "^(?:deps!?|(?:build|chore|fix)\\(deps\\)!?):|^Bump\\b" }),
|
|
6
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
7
|
+
}),
|
|
8
|
+
Object.freeze({
|
|
9
|
+
id: "conventional-feature",
|
|
10
|
+
class: "feature",
|
|
11
|
+
match: Object.freeze({ titleRegex: "^feat(?:\\([^)]+\\))?!?:" }),
|
|
12
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
13
|
+
}),
|
|
14
|
+
Object.freeze({
|
|
15
|
+
id: "conventional-fix",
|
|
16
|
+
class: "fix",
|
|
17
|
+
match: Object.freeze({ titleRegex: "^fix(?:\\((?!deps\\))[^)]+\\))?!?:" }),
|
|
18
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
19
|
+
}),
|
|
20
|
+
Object.freeze({
|
|
21
|
+
id: "conventional-docs",
|
|
22
|
+
class: "docs",
|
|
23
|
+
match: Object.freeze({ titleRegex: "^docs(?:\\([^)]+\\))?!?:" }),
|
|
24
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
25
|
+
}),
|
|
26
|
+
Object.freeze({
|
|
27
|
+
id: "conventional-test",
|
|
28
|
+
class: "test",
|
|
29
|
+
match: Object.freeze({ titleRegex: "^test(?:\\([^)]+\\))?!?:" }),
|
|
30
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
31
|
+
}),
|
|
32
|
+
Object.freeze({
|
|
33
|
+
id: "conventional-maintenance",
|
|
34
|
+
class: "maintenance",
|
|
35
|
+
match: Object.freeze({ titleRegex: "^(refactor|perf|style|build|ci)(?:\\([^)]+\\))?!?:|^chore(?:\\((?!deps\\))[^)]+\\))?!?:" }),
|
|
36
|
+
notes: "Generated by interactive setup from the Conventional Commit PR title preset.",
|
|
37
|
+
}),
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
export function conventionalCommitPrClassRules() {
|
|
41
|
+
return CONVENTIONAL_COMMIT_PR_CLASS_PRESET.map(rule => ({
|
|
42
|
+
id: rule.id,
|
|
43
|
+
class: rule.class,
|
|
44
|
+
match: { ...rule.match },
|
|
45
|
+
notes: rule.notes,
|
|
46
|
+
}));
|
|
47
|
+
}
|