delivery-friction-analyzer 0.3.0 → 0.4.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.
@@ -53,3 +53,29 @@ If both matchers are present on one rule, both must match. If no rule matches, t
53
53
  ```
54
54
 
55
55
  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
+
57
+ ## Workflow Context
58
+
59
+ `workflow` is optional user-configured context. It records repository workflow assumptions that later setup and report milestones can rely on, but M2 does not infer these values from GitHub and does not change scoring, rankings, collection, PR class matching, or report wording.
60
+
61
+ When provided, `workflow` must include at least one supported field.
62
+
63
+ Supported fields:
64
+
65
+ - `primaryMergeMethod`: `merge_commit`, `squash_merge`, `rebase_merge`, `mixed`, or `unknown`.
66
+ - `releaseStrategy`: `release_prs`, `direct_tags`, `release_branches`, `mixed`, or `unknown`.
67
+ - `branchStrategy`: `trunk_based`, `main_plus_release_branches`, `long_lived_development_branches`, `mixed`, or `unknown`.
68
+
69
+ Example:
70
+
71
+ ```json
72
+ {
73
+ "workflow": {
74
+ "primaryMergeMethod": "squash_merge",
75
+ "releaseStrategy": "release_prs",
76
+ "branchStrategy": "main_plus_release_branches"
77
+ }
78
+ }
79
+ ```
80
+
81
+ Use stable identifiers exactly as shown above. Display labels such as "squash merges" or "release PRs" belong in CLI prompts or documentation, not in profile data.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "delivery-friction-analyzer",
3
- "version": "0.3.0",
3
+ "version": "0.4.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-17 — Workflow Profile Contract
6
+
7
+ - What changed: Repository profiles can now declare optional workflow context for merge method, release strategy, and branch strategy using validated stable identifiers.
8
+ - Why it matters: Future interactive setup and report milestones can rely on a documented profile contract without inferring workflow assumptions from GitHub history or changing scoring.
9
+ - Who is affected: Maintainers authoring or validating repository profiles.
10
+ - Action needed: Add `workflow` context only when you want to record repository workflow assumptions; existing profiles remain valid without it.
11
+ - PR: https://github.com/hannasdev/delivery-friction-analyzer/pull/35
12
+
5
13
  ### 2026-06-17 — Opt-In Interactive CLI Setup
6
14
 
7
15
  - What changed: GitHub analysis now supports `--interactive` to prompt for existing run options such as repository, PR limit, profile path, output directory, dry-run mode, CSV exports, JSON completion output, and configured PR class exclusions.
@@ -87,6 +87,22 @@
87
87
  "notes": { "type": "string" }
88
88
  }
89
89
  }
90
+ },
91
+ "workflow": {
92
+ "type": "object",
93
+ "additionalProperties": false,
94
+ "properties": {
95
+ "primaryMergeMethod": {
96
+ "enum": ["merge_commit", "squash_merge", "rebase_merge", "mixed", "unknown"]
97
+ },
98
+ "releaseStrategy": {
99
+ "enum": ["release_prs", "direct_tags", "release_branches", "mixed", "unknown"]
100
+ },
101
+ "branchStrategy": {
102
+ "enum": ["trunk_based", "main_plus_release_branches", "long_lived_development_branches", "mixed", "unknown"]
103
+ }
104
+ },
105
+ "minProperties": 1
90
106
  }
91
107
  }
92
108
  }
@@ -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 { assertValidWorkflowContext } from "../profile/workflow.js";
20
21
 
21
22
  const ALLOWED_OPTIONS = new Set([
22
23
  "repo",
@@ -194,6 +195,7 @@ async function readProfile(profilePath) {
194
195
  }
195
196
  try {
196
197
  assertValidPrClassRules(profile);
198
+ assertValidWorkflowContext(profile);
197
199
  } catch (error) {
198
200
  throw new Error(`profile is invalid: ${error.message}`);
199
201
  }
@@ -1,6 +1,7 @@
1
1
  import { classifyCommentSource, groupByCommentSource } from "../github/comment-source.js";
2
2
  import { classifyFilePath } from "../profile/file-role.js";
3
3
  import { assertValidPrClassRules, classifyPullRequest } from "../profile/pr-class.js";
4
+ import { assertValidWorkflowContext } from "../profile/workflow.js";
4
5
 
5
6
  function minDate(values) {
6
7
  return values.filter(Boolean).sort()[0] ?? null;
@@ -112,6 +113,7 @@ function normalizeCommit(commit) {
112
113
  export function normalizeFixtureBundle(bundle, { repositoryProfile } = {}) {
113
114
  const profile = repositoryProfile ?? {};
114
115
  assertValidPrClassRules(profile);
116
+ assertValidWorkflowContext(profile);
115
117
 
116
118
  const pullRequests = (bundle.pullRequests ?? []).map(pr => {
117
119
  const reviewDates = (pr.reviews ?? []).map(review => review.submittedAt);
@@ -0,0 +1,75 @@
1
+ export const WORKFLOW_PRIMARY_MERGE_METHODS = Object.freeze([
2
+ "merge_commit",
3
+ "squash_merge",
4
+ "rebase_merge",
5
+ "mixed",
6
+ "unknown",
7
+ ]);
8
+
9
+ export const WORKFLOW_RELEASE_STRATEGIES = Object.freeze([
10
+ "release_prs",
11
+ "direct_tags",
12
+ "release_branches",
13
+ "mixed",
14
+ "unknown",
15
+ ]);
16
+
17
+ export const WORKFLOW_BRANCH_STRATEGIES = Object.freeze([
18
+ "trunk_based",
19
+ "main_plus_release_branches",
20
+ "long_lived_development_branches",
21
+ "mixed",
22
+ "unknown",
23
+ ]);
24
+
25
+ const WORKFLOW_FIELDS = Object.freeze({
26
+ primaryMergeMethod: WORKFLOW_PRIMARY_MERGE_METHODS,
27
+ releaseStrategy: WORKFLOW_RELEASE_STRATEGIES,
28
+ branchStrategy: WORKFLOW_BRANCH_STRATEGIES,
29
+ });
30
+
31
+ function allowedValues(values) {
32
+ return values.join(", ");
33
+ }
34
+
35
+ export function validateWorkflowContext(profile = {}) {
36
+ const errors = [];
37
+ if (!profile || typeof profile !== "object" || Array.isArray(profile)) {
38
+ return errors;
39
+ }
40
+
41
+ if (!Object.prototype.hasOwnProperty.call(profile, "workflow")) {
42
+ return errors;
43
+ }
44
+
45
+ const workflow = profile.workflow;
46
+ if (!workflow || typeof workflow !== "object" || Array.isArray(workflow)) {
47
+ return ["workflow must be an object when provided"];
48
+ }
49
+ if (Object.keys(workflow).length === 0) {
50
+ return ["workflow must include at least one field when provided"];
51
+ }
52
+
53
+ for (const key of Object.keys(workflow)) {
54
+ if (!Object.prototype.hasOwnProperty.call(WORKFLOW_FIELDS, key)) {
55
+ errors.push(`workflow.${key} is not supported`);
56
+ }
57
+ }
58
+
59
+ for (const [field, values] of Object.entries(WORKFLOW_FIELDS)) {
60
+ const value = workflow[field];
61
+ if (value === undefined) continue;
62
+ if (!values.includes(value)) {
63
+ errors.push(`workflow.${field} must be one of: ${allowedValues(values)}`);
64
+ }
65
+ }
66
+
67
+ return errors;
68
+ }
69
+
70
+ export function assertValidWorkflowContext(profile = {}) {
71
+ const errors = validateWorkflowContext(profile);
72
+ if (errors.length > 0) {
73
+ throw new Error(`invalid workflow profile context: ${errors.join("; ")}`);
74
+ }
75
+ }