@hyperfrontend/versioning 0.1.0 → 0.3.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.
Files changed (167) hide show
  1. package/ARCHITECTURE.md +50 -1
  2. package/CHANGELOG.md +37 -23
  3. package/README.md +19 -14
  4. package/changelog/index.cjs.js +38 -6
  5. package/changelog/index.cjs.js.map +1 -1
  6. package/changelog/index.esm.js +38 -6
  7. package/changelog/index.esm.js.map +1 -1
  8. package/changelog/models/entry.d.ts +5 -0
  9. package/changelog/models/entry.d.ts.map +1 -1
  10. package/changelog/models/index.cjs.js +2 -0
  11. package/changelog/models/index.cjs.js.map +1 -1
  12. package/changelog/models/index.esm.js +2 -0
  13. package/changelog/models/index.esm.js.map +1 -1
  14. package/changelog/operations/index.cjs.js.map +1 -1
  15. package/changelog/operations/index.esm.js.map +1 -1
  16. package/changelog/parse/index.cjs.js +85 -6
  17. package/changelog/parse/index.cjs.js.map +1 -1
  18. package/changelog/parse/index.esm.js +85 -6
  19. package/changelog/parse/index.esm.js.map +1 -1
  20. package/changelog/parse/line.d.ts.map +1 -1
  21. package/changelog/parse/parser.d.ts +0 -6
  22. package/changelog/parse/parser.d.ts.map +1 -1
  23. package/commits/classify/classifier.d.ts +73 -0
  24. package/commits/classify/classifier.d.ts.map +1 -0
  25. package/commits/classify/index.cjs.js +707 -0
  26. package/commits/classify/index.cjs.js.map +1 -0
  27. package/commits/classify/index.d.ts +8 -0
  28. package/commits/classify/index.d.ts.map +1 -0
  29. package/commits/classify/index.esm.js +679 -0
  30. package/commits/classify/index.esm.js.map +1 -0
  31. package/commits/classify/infrastructure.d.ts +205 -0
  32. package/commits/classify/infrastructure.d.ts.map +1 -0
  33. package/commits/classify/models.d.ts +108 -0
  34. package/commits/classify/models.d.ts.map +1 -0
  35. package/commits/classify/project-scopes.d.ts +69 -0
  36. package/commits/classify/project-scopes.d.ts.map +1 -0
  37. package/commits/index.cjs.js +704 -0
  38. package/commits/index.cjs.js.map +1 -1
  39. package/commits/index.d.ts +1 -0
  40. package/commits/index.d.ts.map +1 -1
  41. package/commits/index.esm.js +678 -1
  42. package/commits/index.esm.js.map +1 -1
  43. package/flow/executor/execute.d.ts +6 -0
  44. package/flow/executor/execute.d.ts.map +1 -1
  45. package/flow/executor/index.cjs.js +1617 -43
  46. package/flow/executor/index.cjs.js.map +1 -1
  47. package/flow/executor/index.esm.js +1623 -49
  48. package/flow/executor/index.esm.js.map +1 -1
  49. package/flow/index.cjs.js +6749 -2938
  50. package/flow/index.cjs.js.map +1 -1
  51. package/flow/index.esm.js +6751 -2944
  52. package/flow/index.esm.js.map +1 -1
  53. package/flow/models/index.cjs.js +138 -0
  54. package/flow/models/index.cjs.js.map +1 -1
  55. package/flow/models/index.d.ts +1 -1
  56. package/flow/models/index.d.ts.map +1 -1
  57. package/flow/models/index.esm.js +138 -1
  58. package/flow/models/index.esm.js.map +1 -1
  59. package/flow/models/types.d.ts +180 -3
  60. package/flow/models/types.d.ts.map +1 -1
  61. package/flow/presets/conventional.d.ts +9 -8
  62. package/flow/presets/conventional.d.ts.map +1 -1
  63. package/flow/presets/independent.d.ts.map +1 -1
  64. package/flow/presets/index.cjs.js +3641 -303
  65. package/flow/presets/index.cjs.js.map +1 -1
  66. package/flow/presets/index.esm.js +3641 -303
  67. package/flow/presets/index.esm.js.map +1 -1
  68. package/flow/presets/synced.d.ts.map +1 -1
  69. package/flow/steps/analyze-commits.d.ts +9 -6
  70. package/flow/steps/analyze-commits.d.ts.map +1 -1
  71. package/flow/steps/calculate-bump.d.ts.map +1 -1
  72. package/flow/steps/fetch-registry.d.ts.map +1 -1
  73. package/flow/steps/generate-changelog.d.ts +5 -0
  74. package/flow/steps/generate-changelog.d.ts.map +1 -1
  75. package/flow/steps/index.cjs.js +3663 -328
  76. package/flow/steps/index.cjs.js.map +1 -1
  77. package/flow/steps/index.d.ts +2 -1
  78. package/flow/steps/index.d.ts.map +1 -1
  79. package/flow/steps/index.esm.js +3661 -329
  80. package/flow/steps/index.esm.js.map +1 -1
  81. package/flow/steps/resolve-repository.d.ts +36 -0
  82. package/flow/steps/resolve-repository.d.ts.map +1 -0
  83. package/flow/steps/update-packages.d.ts.map +1 -1
  84. package/git/factory.d.ts +14 -0
  85. package/git/factory.d.ts.map +1 -1
  86. package/git/index.cjs.js +65 -0
  87. package/git/index.cjs.js.map +1 -1
  88. package/git/index.esm.js +66 -2
  89. package/git/index.esm.js.map +1 -1
  90. package/git/operations/index.cjs.js +40 -0
  91. package/git/operations/index.cjs.js.map +1 -1
  92. package/git/operations/index.d.ts +1 -1
  93. package/git/operations/index.d.ts.map +1 -1
  94. package/git/operations/index.esm.js +41 -2
  95. package/git/operations/index.esm.js.map +1 -1
  96. package/git/operations/log.d.ts +23 -0
  97. package/git/operations/log.d.ts.map +1 -1
  98. package/index.cjs.js +7547 -4947
  99. package/index.cjs.js.map +1 -1
  100. package/index.d.ts +3 -1
  101. package/index.d.ts.map +1 -1
  102. package/index.esm.js +7550 -4954
  103. package/index.esm.js.map +1 -1
  104. package/package.json +39 -1
  105. package/registry/index.cjs.js +3 -3
  106. package/registry/index.cjs.js.map +1 -1
  107. package/registry/index.esm.js +3 -3
  108. package/registry/index.esm.js.map +1 -1
  109. package/registry/models/index.cjs.js +2 -0
  110. package/registry/models/index.cjs.js.map +1 -1
  111. package/registry/models/index.esm.js +2 -0
  112. package/registry/models/index.esm.js.map +1 -1
  113. package/registry/models/version-info.d.ts +10 -0
  114. package/registry/models/version-info.d.ts.map +1 -1
  115. package/registry/npm/client.d.ts.map +1 -1
  116. package/registry/npm/index.cjs.js +1 -3
  117. package/registry/npm/index.cjs.js.map +1 -1
  118. package/registry/npm/index.esm.js +1 -3
  119. package/registry/npm/index.esm.js.map +1 -1
  120. package/repository/index.cjs.js +998 -0
  121. package/repository/index.cjs.js.map +1 -0
  122. package/repository/index.d.ts +4 -0
  123. package/repository/index.d.ts.map +1 -0
  124. package/repository/index.esm.js +981 -0
  125. package/repository/index.esm.js.map +1 -0
  126. package/repository/models/index.cjs.js +301 -0
  127. package/repository/models/index.cjs.js.map +1 -0
  128. package/repository/models/index.d.ts +7 -0
  129. package/repository/models/index.d.ts.map +1 -0
  130. package/repository/models/index.esm.js +290 -0
  131. package/repository/models/index.esm.js.map +1 -0
  132. package/repository/models/platform.d.ts +58 -0
  133. package/repository/models/platform.d.ts.map +1 -0
  134. package/repository/models/repository-config.d.ts +132 -0
  135. package/repository/models/repository-config.d.ts.map +1 -0
  136. package/repository/models/resolution.d.ts +121 -0
  137. package/repository/models/resolution.d.ts.map +1 -0
  138. package/repository/parse/index.cjs.js +755 -0
  139. package/repository/parse/index.cjs.js.map +1 -0
  140. package/repository/parse/index.d.ts +5 -0
  141. package/repository/parse/index.d.ts.map +1 -0
  142. package/repository/parse/index.esm.js +749 -0
  143. package/repository/parse/index.esm.js.map +1 -0
  144. package/repository/parse/package-json.d.ts +100 -0
  145. package/repository/parse/package-json.d.ts.map +1 -0
  146. package/repository/parse/url.d.ts +81 -0
  147. package/repository/parse/url.d.ts.map +1 -0
  148. package/repository/url/compare.d.ts +84 -0
  149. package/repository/url/compare.d.ts.map +1 -0
  150. package/repository/url/index.cjs.js +178 -0
  151. package/repository/url/index.cjs.js.map +1 -0
  152. package/repository/url/index.d.ts +3 -0
  153. package/repository/url/index.d.ts.map +1 -0
  154. package/repository/url/index.esm.js +176 -0
  155. package/repository/url/index.esm.js.map +1 -0
  156. package/workspace/discovery/changelog-path.d.ts +3 -7
  157. package/workspace/discovery/changelog-path.d.ts.map +1 -1
  158. package/workspace/discovery/index.cjs.js +408 -335
  159. package/workspace/discovery/index.cjs.js.map +1 -1
  160. package/workspace/discovery/index.esm.js +408 -335
  161. package/workspace/discovery/index.esm.js.map +1 -1
  162. package/workspace/discovery/packages.d.ts +0 -6
  163. package/workspace/discovery/packages.d.ts.map +1 -1
  164. package/workspace/index.cjs.js +84 -11
  165. package/workspace/index.cjs.js.map +1 -1
  166. package/workspace/index.esm.js +84 -11
  167. package/workspace/index.esm.js.map +1 -1
@@ -1,5 +1,682 @@
1
1
  'use strict';
2
2
 
3
+ /**
4
+ * Creates an empty classification summary.
5
+ *
6
+ * @returns A new ClassificationSummary with all counts at zero
7
+ */
8
+ function createEmptyClassificationSummary() {
9
+ return {
10
+ total: 0,
11
+ included: 0,
12
+ excluded: 0,
13
+ bySource: {
14
+ 'direct-scope': 0,
15
+ 'direct-file': 0,
16
+ 'unscoped-file': 0,
17
+ 'indirect-dependency': 0,
18
+ 'indirect-infra': 0,
19
+ 'unscoped-global': 0,
20
+ excluded: 0,
21
+ },
22
+ };
23
+ }
24
+ /**
25
+ * Creates a classified commit.
26
+ *
27
+ * @param commit - The parsed conventional commit
28
+ * @param raw - The raw git commit
29
+ * @param source - How the commit relates to the project
30
+ * @param options - Additional classification options
31
+ * @param options.touchedFiles - Files in the project modified by this commit
32
+ * @param options.dependencyPath - Chain of dependencies leading to indirect inclusion
33
+ * @returns A new ClassifiedCommit object
34
+ */
35
+ function createClassifiedCommit(commit, raw, source, options) {
36
+ const include = isIncludedSource(source);
37
+ const preserveScope = shouldPreserveScope(source);
38
+ return {
39
+ commit,
40
+ raw,
41
+ source,
42
+ include,
43
+ preserveScope,
44
+ touchedFiles: options?.touchedFiles,
45
+ dependencyPath: options?.dependencyPath,
46
+ };
47
+ }
48
+ /**
49
+ * Determines if a source type should be included in changelog.
50
+ *
51
+ * @param source - The commit source type
52
+ * @returns True if commits with this source should be included
53
+ */
54
+ function isIncludedSource(source) {
55
+ switch (source) {
56
+ case 'direct-scope':
57
+ case 'direct-file':
58
+ case 'unscoped-file':
59
+ case 'indirect-dependency':
60
+ case 'indirect-infra':
61
+ return true;
62
+ case 'unscoped-global':
63
+ case 'excluded':
64
+ return false;
65
+ }
66
+ }
67
+ /**
68
+ * Determines if scope should be preserved for a source type.
69
+ *
70
+ * Direct commits omit scope (redundant in project changelog).
71
+ * Indirect commits preserve scope for context.
72
+ *
73
+ * @param source - The commit source type
74
+ * @returns True if scope should be preserved in changelog
75
+ */
76
+ function shouldPreserveScope(source) {
77
+ switch (source) {
78
+ case 'direct-scope':
79
+ case 'unscoped-file':
80
+ return false; // Scope would be redundant
81
+ case 'direct-file':
82
+ case 'indirect-dependency':
83
+ case 'indirect-infra':
84
+ return true; // Scope provides context
85
+ case 'unscoped-global':
86
+ case 'excluded':
87
+ return false; // Won't be shown
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Safe copies of Set built-in via factory function.
93
+ *
94
+ * Since constructors cannot be safely captured via Object.assign, this module
95
+ * provides a factory function that uses Reflect.construct internally.
96
+ *
97
+ * These references are captured at module initialization time to protect against
98
+ * prototype pollution attacks. Import only what you need for tree-shaking.
99
+ *
100
+ * @module @hyperfrontend/immutable-api-utils/built-in-copy/set
101
+ */
102
+ // Capture references at module initialization time
103
+ const _Set = globalThis.Set;
104
+ const _Reflect$1 = globalThis.Reflect;
105
+ /**
106
+ * (Safe copy) Creates a new Set using the captured Set constructor.
107
+ * Use this instead of `new Set()`.
108
+ *
109
+ * @param iterable - Optional iterable of values.
110
+ * @returns A new Set instance.
111
+ */
112
+ const createSet = (iterable) => _Reflect$1.construct(_Set, iterable ? [iterable] : []);
113
+
114
+ /**
115
+ * Derives all scope variations that should match a project.
116
+ *
117
+ * Given a project named 'lib-versioning' with package '@hyperfrontend/versioning',
118
+ * this generates variations like:
119
+ * - 'lib-versioning' (full project name)
120
+ * - 'versioning' (without lib- prefix)
121
+ *
122
+ * @param options - Project identification options
123
+ * @returns Array of scope strings that match this project
124
+ *
125
+ * @example
126
+ * deriveProjectScopes({ projectName: 'lib-versioning', packageName: '@hyperfrontend/versioning' })
127
+ * // Returns: ['lib-versioning', 'versioning']
128
+ *
129
+ * @example
130
+ * deriveProjectScopes({ projectName: 'app-demo', packageName: 'demo-app' })
131
+ * // Returns: ['app-demo', 'demo']
132
+ */
133
+ function deriveProjectScopes(options) {
134
+ const { projectName, packageName, additionalScopes = [], prefixes = DEFAULT_PROJECT_PREFIXES } = options;
135
+ const scopes = createSet();
136
+ // Always include the full project name
137
+ scopes.add(projectName);
138
+ // Add variations based on common prefixes
139
+ const prefixVariations = extractPrefixVariations(projectName, prefixes);
140
+ for (const variation of prefixVariations) {
141
+ scopes.add(variation);
142
+ }
143
+ // Add package name variations if provided
144
+ if (packageName) {
145
+ const packageVariations = extractPackageNameVariations(packageName);
146
+ for (const variation of packageVariations) {
147
+ scopes.add(variation);
148
+ }
149
+ }
150
+ // Add any additional scopes
151
+ for (const scope of additionalScopes) {
152
+ if (scope) {
153
+ scopes.add(scope);
154
+ }
155
+ }
156
+ return [...scopes];
157
+ }
158
+ /**
159
+ * Default project name prefixes that can be stripped for scope matching.
160
+ */
161
+ const DEFAULT_PROJECT_PREFIXES = ['lib-', 'app-', 'e2e-', 'tool-', 'plugin-', 'feature-', 'package-'];
162
+ /**
163
+ * Generates scope variations by stripping recognized project prefixes.
164
+ *
165
+ * @param projectName - The project name to extract variations from
166
+ * @param prefixes - Prefixes to check and strip
167
+ * @returns Array of scope name variations
168
+ */
169
+ function extractPrefixVariations(projectName, prefixes) {
170
+ const variations = [];
171
+ for (const prefix of prefixes) {
172
+ if (projectName.startsWith(prefix)) {
173
+ const withoutPrefix = projectName.slice(prefix.length);
174
+ if (withoutPrefix) {
175
+ variations.push(withoutPrefix);
176
+ }
177
+ break; // Only remove one prefix
178
+ }
179
+ }
180
+ return variations;
181
+ }
182
+ /**
183
+ * Extracts scope variations from an npm package name.
184
+ *
185
+ * @param packageName - The npm package name (e.g., '@scope/name')
186
+ * @returns Array of name variations
187
+ */
188
+ function extractPackageNameVariations(packageName) {
189
+ const variations = [];
190
+ // Handle scoped packages: @scope/name -> name
191
+ if (packageName.startsWith('@')) {
192
+ const slashIndex = packageName.indexOf('/');
193
+ if (slashIndex !== -1) {
194
+ const unscoped = packageName.slice(slashIndex + 1);
195
+ if (unscoped) {
196
+ variations.push(unscoped);
197
+ }
198
+ }
199
+ }
200
+ else {
201
+ // Non-scoped package: just use the name
202
+ variations.push(packageName);
203
+ }
204
+ return variations;
205
+ }
206
+ /**
207
+ * Checks if a commit scope matches any of the project scopes.
208
+ *
209
+ * @param commitScope - The scope from a conventional commit
210
+ * @param projectScopes - Array of scopes that match the project
211
+ * @returns True if the commit scope matches the project
212
+ *
213
+ * @example
214
+ * scopeMatchesProject('versioning', ['lib-versioning', 'versioning']) // true
215
+ * scopeMatchesProject('logging', ['lib-versioning', 'versioning']) // false
216
+ */
217
+ function scopeMatchesProject(commitScope, projectScopes) {
218
+ if (!commitScope) {
219
+ return false;
220
+ }
221
+ // Case-insensitive comparison
222
+ const normalizedScope = commitScope.toLowerCase();
223
+ return projectScopes.some((scope) => scope.toLowerCase() === normalizedScope);
224
+ }
225
+ /**
226
+ * Checks if a commit scope should be explicitly excluded.
227
+ *
228
+ * @param commitScope - The scope from a conventional commit
229
+ * @param excludeScopes - Array of scopes to exclude
230
+ * @returns True if the scope should be excluded
231
+ */
232
+ function scopeIsExcluded(commitScope, excludeScopes) {
233
+ if (!commitScope) {
234
+ return false;
235
+ }
236
+ const normalizedScope = commitScope.toLowerCase();
237
+ return excludeScopes.some((scope) => scope.toLowerCase() === normalizedScope);
238
+ }
239
+ /**
240
+ * Default scopes to exclude from changelogs.
241
+ *
242
+ * These represent repository-level or infrastructure changes
243
+ * that typically don't belong in individual project changelogs.
244
+ */
245
+ const DEFAULT_EXCLUDE_SCOPES = ['release', 'deps', 'workspace', 'root', 'repo', 'ci', 'build'];
246
+
247
+ /**
248
+ * Classifies a single commit against a project.
249
+ *
250
+ * Implements the hybrid classification strategy:
251
+ * 1. Check scope match (fast path)
252
+ * 2. Check file touch (validation/catch-all)
253
+ * 3. Check dependency touch (indirect)
254
+ * 4. Fallback to excluded
255
+ *
256
+ * @param input - The commit to classify
257
+ * @param context - Classification context with project info
258
+ * @returns Classified commit with source attribution
259
+ *
260
+ * @example
261
+ * const classified = classifyCommit(
262
+ * { commit: parsedCommit, raw: gitCommit },
263
+ * { projectScopes: ['versioning'], fileCommitHashes: new Set(['abc123']) }
264
+ * )
265
+ */
266
+ function classifyCommit(input, context) {
267
+ const { commit, raw } = input;
268
+ const { projectScopes, fileCommitHashes, dependencyCommitMap, infrastructureCommitHashes, excludeScopes = DEFAULT_EXCLUDE_SCOPES, includeScopes = [], } = context;
269
+ const scope = commit.scope;
270
+ const hasScope = !!scope;
271
+ const allProjectScopes = [...projectScopes, ...includeScopes];
272
+ // First check: Is this scope explicitly excluded?
273
+ if (hasScope && scopeIsExcluded(scope, excludeScopes)) {
274
+ return createClassifiedCommit(commit, raw, 'excluded');
275
+ }
276
+ // Priority 1: Scope-based direct match (fast path)
277
+ if (hasScope && scopeMatchesProject(scope, allProjectScopes)) {
278
+ return createClassifiedCommit(commit, raw, 'direct-scope');
279
+ }
280
+ // Priority 2: File-based direct match (validation/catch-all)
281
+ if (fileCommitHashes.has(raw.hash)) {
282
+ // Commit touched project files
283
+ if (hasScope) {
284
+ // Has a scope but it's different - likely a typo or cross-cutting change
285
+ return createClassifiedCommit(commit, raw, 'direct-file');
286
+ }
287
+ // No scope but touched project files
288
+ return createClassifiedCommit(commit, raw, 'unscoped-file');
289
+ }
290
+ // Priority 3: Indirect dependency match
291
+ if (hasScope && dependencyCommitMap) {
292
+ const dependencyPath = findDependencyPath(scope, raw.hash, dependencyCommitMap);
293
+ if (dependencyPath) {
294
+ return createClassifiedCommit(commit, raw, 'indirect-dependency', { dependencyPath });
295
+ }
296
+ }
297
+ // File-based infrastructure match
298
+ if (infrastructureCommitHashes?.has(raw.hash)) {
299
+ return createClassifiedCommit(commit, raw, 'indirect-infra');
300
+ }
301
+ // Fallback: No match found
302
+ if (!hasScope) {
303
+ // Unscoped commit that didn't touch any project files
304
+ return createClassifiedCommit(commit, raw, 'unscoped-global');
305
+ }
306
+ // Scoped commit that doesn't match anything
307
+ return createClassifiedCommit(commit, raw, 'excluded');
308
+ }
309
+ /**
310
+ * Classifies multiple commits against a project.
311
+ *
312
+ * @param commits - Array of commits to classify
313
+ * @param context - Classification context with project info
314
+ * @returns Classification result with all commits and summary
315
+ */
316
+ function classifyCommits(commits, context) {
317
+ const classified = [];
318
+ const included = [];
319
+ const excluded = [];
320
+ const summary = createEmptyClassificationSummary();
321
+ const bySource = { ...summary.bySource };
322
+ for (const input of commits) {
323
+ const result = classifyCommit(input, context);
324
+ classified.push(result);
325
+ // Update summary
326
+ bySource[result.source]++;
327
+ if (result.include) {
328
+ included.push(result);
329
+ }
330
+ else {
331
+ excluded.push(result);
332
+ }
333
+ }
334
+ return {
335
+ commits: classified,
336
+ included,
337
+ excluded,
338
+ summary: {
339
+ total: classified.length,
340
+ included: included.length,
341
+ excluded: excluded.length,
342
+ bySource,
343
+ },
344
+ };
345
+ }
346
+ /**
347
+ * Finds a dependency path for a given scope and commit hash.
348
+ *
349
+ * Verifies both:
350
+ * 1. The scope matches a dependency name (or variation)
351
+ * 2. The commit hash is in that dependency's commit set
352
+ *
353
+ * This prevents false positives from mislabeled commits.
354
+ *
355
+ * @param scope - The commit scope
356
+ * @param hash - The commit hash to verify
357
+ * @param dependencyCommitMap - Map of dependencies to their commit hashes
358
+ * @returns Dependency path if found and hash verified, undefined otherwise
359
+ */
360
+ function findDependencyPath(scope, hash, dependencyCommitMap) {
361
+ const normalizedScope = scope.toLowerCase();
362
+ for (const [depName, depHashes] of dependencyCommitMap) {
363
+ // Check if scope matches dependency name or variations
364
+ const depVariations = getDependencyVariations(depName);
365
+ if (depVariations.some((v) => v.toLowerCase() === normalizedScope)) {
366
+ // CRITICAL: Verify the commit actually touched this dependency's files
367
+ // This prevents false positives from mislabeled commits
368
+ if (depHashes.has(hash)) {
369
+ return [depName];
370
+ }
371
+ }
372
+ }
373
+ return undefined;
374
+ }
375
+ /**
376
+ * Generates name variations for a dependency to enable flexible scope matching.
377
+ *
378
+ * @param depName - The dependency project or package name
379
+ * @returns Array of name variations including stripped prefixes
380
+ */
381
+ function getDependencyVariations(depName) {
382
+ const variations = [depName];
383
+ // Handle lib- prefix
384
+ if (depName.startsWith('lib-')) {
385
+ variations.push(depName.slice(4));
386
+ }
387
+ // Handle @scope/name
388
+ if (depName.startsWith('@')) {
389
+ const slashIndex = depName.indexOf('/');
390
+ if (slashIndex !== -1) {
391
+ variations.push(depName.slice(slashIndex + 1));
392
+ }
393
+ }
394
+ return variations;
395
+ }
396
+ /**
397
+ * Creates a classification context from common inputs.
398
+ *
399
+ * @param projectScopes - Scopes that match the project
400
+ * @param fileCommitHashes - Set of commit hashes that touched project files
401
+ * @param options - Additional context options
402
+ * @param options.dependencyCommitMap - Map of dependency names to commit hashes touching them
403
+ * @param options.infrastructureCommitHashes - Set of commit hashes touching infrastructure paths
404
+ * @param options.excludeScopes - Scopes to explicitly exclude from classification
405
+ * @param options.includeScopes - Additional scopes to include as direct matches
406
+ * @returns A ClassificationContext object
407
+ */
408
+ function createClassificationContext(projectScopes, fileCommitHashes, options) {
409
+ return {
410
+ projectScopes,
411
+ fileCommitHashes,
412
+ dependencyCommitMap: options?.dependencyCommitMap,
413
+ infrastructureCommitHashes: options?.infrastructureCommitHashes,
414
+ excludeScopes: options?.excludeScopes ?? DEFAULT_EXCLUDE_SCOPES,
415
+ includeScopes: options?.includeScopes,
416
+ };
417
+ }
418
+ /**
419
+ * Filters an array of classified commits to only included ones.
420
+ *
421
+ * @param commits - Array of classified commits
422
+ * @returns Only commits marked for inclusion
423
+ */
424
+ function filterIncluded(commits) {
425
+ return commits.filter((c) => c.include);
426
+ }
427
+ /**
428
+ * Extracts conventional commits from classified commits for changelog generation.
429
+ *
430
+ * @param commits - Array of classified commits
431
+ * @returns Array of conventional commits
432
+ */
433
+ function extractConventionalCommits(commits) {
434
+ return commits.map((c) => c.commit);
435
+ }
436
+ /**
437
+ * Creates a modified conventional commit with scope handling based on classification.
438
+ *
439
+ * For direct commits, the scope is removed (redundant in project changelog).
440
+ * For indirect commits, the scope is preserved (provides context).
441
+ *
442
+ * @param classified - Commit with classification metadata determining scope display
443
+ * @returns A conventional commit with appropriate scope handling
444
+ */
445
+ function toChangelogCommit(classified) {
446
+ const { commit, preserveScope } = classified;
447
+ if (!preserveScope && commit.scope) {
448
+ // Remove the scope for direct commits
449
+ return {
450
+ ...commit,
451
+ scope: undefined,
452
+ // Rebuild raw to reflect removed scope
453
+ raw: rebuildRawWithoutScope(commit),
454
+ };
455
+ }
456
+ return commit;
457
+ }
458
+ /**
459
+ * Reconstructs a conventional commit message string without the scope portion.
460
+ *
461
+ * @param commit - The conventional commit to rebuild
462
+ * @returns Reconstructed raw message with scope removed
463
+ */
464
+ function rebuildRawWithoutScope(commit) {
465
+ const breaking = commit.breaking && !commit.breakingDescription ? '!' : '';
466
+ const header = `${commit.type}${breaking}: ${commit.subject}`;
467
+ if (!commit.body && commit.footers.length === 0) {
468
+ return header;
469
+ }
470
+ let raw = header;
471
+ if (commit.body) {
472
+ raw += `\n\n${commit.body}`;
473
+ }
474
+ for (const footer of commit.footers) {
475
+ raw += `\n${footer.key}${footer.separator}${footer.value}`;
476
+ }
477
+ return raw;
478
+ }
479
+
480
+ /**
481
+ * Creates a matcher that checks if commit scope matches any of the given scopes.
482
+ *
483
+ * @param scopes - Scopes to match against (case-insensitive)
484
+ * @returns Matcher that returns true if scope matches
485
+ *
486
+ * @example
487
+ * const matcher = scopeMatcher(['ci', 'build', 'tooling'])
488
+ * matcher({ scope: 'CI', ... }) // true
489
+ * matcher({ scope: 'feat', ... }) // false
490
+ */
491
+ function scopeMatcher(scopes) {
492
+ const normalizedScopes = createSet(scopes.map((s) => s.toLowerCase()));
493
+ return (ctx) => {
494
+ if (!ctx.scope)
495
+ return false;
496
+ return normalizedScopes.has(ctx.scope.toLowerCase());
497
+ };
498
+ }
499
+ /**
500
+ * Creates a matcher that checks if commit scope starts with any of the given prefixes.
501
+ *
502
+ * @param prefixes - Scope prefixes to match (case-insensitive)
503
+ * @returns Matcher that returns true if scope starts with any prefix
504
+ *
505
+ * @example
506
+ * const matcher = scopePrefixMatcher(['tool-', 'infra-'])
507
+ * matcher({ scope: 'tool-package', ... }) // true
508
+ * matcher({ scope: 'lib-utils', ... }) // false
509
+ */
510
+ function scopePrefixMatcher(prefixes) {
511
+ const normalizedPrefixes = prefixes.map((p) => p.toLowerCase());
512
+ return (ctx) => {
513
+ if (!ctx.scope)
514
+ return false;
515
+ const normalizedScope = ctx.scope.toLowerCase();
516
+ return normalizedPrefixes.some((prefix) => normalizedScope.startsWith(prefix));
517
+ };
518
+ }
519
+ /**
520
+ * Creates a matcher that checks if commit message contains any of the given patterns.
521
+ *
522
+ * @param patterns - Patterns to search for in commit message (case-insensitive)
523
+ * @returns Matcher that returns true if message contains any pattern
524
+ *
525
+ * @example
526
+ * const matcher = messageMatcher(['[infra]', '[ci skip]'])
527
+ */
528
+ function messageMatcher(patterns) {
529
+ const normalizedPatterns = patterns.map((p) => p.toLowerCase());
530
+ return (ctx) => {
531
+ const normalizedMessage = ctx.message.toLowerCase();
532
+ return normalizedPatterns.some((pattern) => normalizedMessage.includes(pattern));
533
+ };
534
+ }
535
+ /**
536
+ * Creates a matcher from a regex pattern tested against the scope.
537
+ *
538
+ * @param pattern - Regex pattern to test against scope
539
+ * @returns Matcher that returns true if scope matches regex
540
+ *
541
+ * @example
542
+ * const matcher = scopeRegexMatcher(/^(ci|build|tool)-.+/)
543
+ */
544
+ function scopeRegexMatcher(pattern) {
545
+ return (ctx) => {
546
+ if (!ctx.scope)
547
+ return false;
548
+ return pattern.test(ctx.scope);
549
+ };
550
+ }
551
+ /**
552
+ * Combines matchers with OR logic - returns true if ANY matcher matches.
553
+ *
554
+ * @param matchers - Matchers to combine
555
+ * @returns Combined matcher
556
+ *
557
+ * @example
558
+ * const combined = anyOf(
559
+ * scopeMatcher(['ci', 'build']),
560
+ * messageMatcher(['[infra]']),
561
+ * custom((ctx) => ctx.scope?.startsWith('tool-'))
562
+ * )
563
+ */
564
+ function anyOf(...matchers) {
565
+ return (ctx) => matchers.some((matcher) => matcher(ctx));
566
+ }
567
+ /**
568
+ * Combines matchers with AND logic - returns true if ALL matchers match.
569
+ *
570
+ * @param matchers - Matchers to combine
571
+ * @returns Combined matcher
572
+ *
573
+ * @example
574
+ * const combined = allOf(
575
+ * scopeMatcher(['deps']),
576
+ * messageMatcher(['security'])
577
+ * )
578
+ */
579
+ function allOf(...matchers) {
580
+ return (ctx) => matchers.every((matcher) => matcher(ctx));
581
+ }
582
+ /**
583
+ * Negates a matcher - returns true if matcher returns false.
584
+ *
585
+ * @param matcher - Matcher to negate
586
+ * @returns Negated matcher
587
+ *
588
+ * @example
589
+ * const notRelease = not(scopeMatcher(['release']))
590
+ */
591
+ function not(matcher) {
592
+ return (ctx) => !matcher(ctx);
593
+ }
594
+ /**
595
+ * Matches common CI/CD scopes.
596
+ *
597
+ * Matches: ci, cd, build, pipeline, workflow, actions
598
+ */
599
+ const CI_SCOPE_MATCHER = scopeMatcher(['ci', 'cd', 'build', 'pipeline', 'workflow', 'actions']);
600
+ /**
601
+ * Matches common tooling/workspace scopes.
602
+ *
603
+ * Matches: tooling, workspace, monorepo, nx, root
604
+ */
605
+ const TOOLING_SCOPE_MATCHER = scopeMatcher(['tooling', 'workspace', 'monorepo', 'nx', 'root']);
606
+ /**
607
+ * Matches tool-prefixed scopes (e.g., tool-package, tool-scripts).
608
+ */
609
+ const TOOL_PREFIX_MATCHER = scopePrefixMatcher(['tool-']);
610
+ /**
611
+ * Combined matcher for common infrastructure patterns.
612
+ *
613
+ * Combines CI, tooling, and tool-prefix matchers.
614
+ */
615
+ const DEFAULT_INFRA_SCOPE_MATCHER = anyOf(CI_SCOPE_MATCHER, TOOLING_SCOPE_MATCHER, TOOL_PREFIX_MATCHER);
616
+ /**
617
+ * Builds a combined matcher from infrastructure configuration.
618
+ *
619
+ * Combines scope-based matching with any custom matcher using OR logic.
620
+ * Path-based matching is handled separately via git queries.
621
+ *
622
+ * @param config - Infrastructure configuration
623
+ * @returns Combined matcher, or null if no matchers configured
624
+ *
625
+ * @example
626
+ * const matcher = buildInfrastructureMatcher({
627
+ * scopes: ['ci', 'build'],
628
+ * matcher: (ctx) => ctx.scope?.startsWith('tool-')
629
+ * })
630
+ */
631
+ function buildInfrastructureMatcher(config) {
632
+ const matchers = [];
633
+ // Add scope matcher if scopes configured
634
+ if (config.scopes && config.scopes.length > 0) {
635
+ matchers.push(scopeMatcher(config.scopes));
636
+ }
637
+ // Add custom matcher if provided
638
+ if (config.matcher) {
639
+ matchers.push(config.matcher);
640
+ }
641
+ // Return combined or null
642
+ if (matchers.length === 0) {
643
+ return null;
644
+ }
645
+ if (matchers.length === 1) {
646
+ return matchers[0];
647
+ }
648
+ return anyOf(...matchers);
649
+ }
650
+ /**
651
+ * Creates match context from a git commit.
652
+ *
653
+ * Extracts scope from conventional commit message if present.
654
+ *
655
+ * @param commit - Git commit to create context for
656
+ * @param scope - Pre-parsed scope (optional, saves re-parsing)
657
+ * @returns Match context for use with matchers
658
+ */
659
+ function createMatchContext(commit, scope) {
660
+ return {
661
+ commit,
662
+ scope,
663
+ subject: commit.subject,
664
+ message: commit.message,
665
+ };
666
+ }
667
+ /**
668
+ * Evaluates a commit against an infrastructure matcher.
669
+ *
670
+ * @param commit - Git commit to evaluate
671
+ * @param matcher - Matcher function to apply
672
+ * @param scope - Pre-parsed scope (optional)
673
+ * @returns True if commit matches infrastructure criteria
674
+ */
675
+ function evaluateInfrastructure(commit, matcher, scope) {
676
+ const context = createMatchContext(commit, scope);
677
+ return matcher(context);
678
+ }
679
+
3
680
  /**
4
681
  * Creates a commit footer.
5
682
  *
@@ -627,22 +1304,49 @@ function isConventionalCommit(message) {
627
1304
  return true;
628
1305
  }
629
1306
 
1307
+ exports.CI_SCOPE_MATCHER = CI_SCOPE_MATCHER;
630
1308
  exports.COMMIT_TYPES = COMMIT_TYPES;
1309
+ exports.DEFAULT_EXCLUDE_SCOPES = DEFAULT_EXCLUDE_SCOPES;
1310
+ exports.DEFAULT_INFRA_SCOPE_MATCHER = DEFAULT_INFRA_SCOPE_MATCHER;
1311
+ exports.DEFAULT_PROJECT_PREFIXES = DEFAULT_PROJECT_PREFIXES;
631
1312
  exports.MINOR_TYPES = MINOR_TYPES;
632
1313
  exports.PATCH_TYPES = PATCH_TYPES;
633
1314
  exports.RELEASE_TYPES = RELEASE_TYPES;
1315
+ exports.TOOLING_SCOPE_MATCHER = TOOLING_SCOPE_MATCHER;
1316
+ exports.TOOL_PREFIX_MATCHER = TOOL_PREFIX_MATCHER;
1317
+ exports.allOf = allOf;
1318
+ exports.anyOf = anyOf;
1319
+ exports.buildInfrastructureMatcher = buildInfrastructureMatcher;
1320
+ exports.classifyCommit = classifyCommit;
1321
+ exports.classifyCommits = classifyCommits;
634
1322
  exports.createBreakingFromFooter = createBreakingFromFooter;
635
1323
  exports.createBreakingFromSubject = createBreakingFromSubject;
1324
+ exports.createClassificationContext = createClassificationContext;
1325
+ exports.createClassifiedCommit = createClassifiedCommit;
636
1326
  exports.createCommitFooter = createCommitFooter;
637
1327
  exports.createConventionalCommit = createConventionalCommit;
1328
+ exports.createEmptyClassificationSummary = createEmptyClassificationSummary;
1329
+ exports.createMatchContext = createMatchContext;
638
1330
  exports.createNonBreaking = createNonBreaking;
1331
+ exports.deriveProjectScopes = deriveProjectScopes;
1332
+ exports.evaluateInfrastructure = evaluateInfrastructure;
1333
+ exports.extractConventionalCommits = extractConventionalCommits;
1334
+ exports.filterIncluded = filterIncluded;
639
1335
  exports.getSemverBump = getSemverBump;
640
1336
  exports.isBreakingFooterKey = isBreakingFooterKey;
641
1337
  exports.isConventionalCommit = isConventionalCommit;
642
1338
  exports.isReleaseType = isReleaseType;
643
1339
  exports.isStandardType = isStandardType;
1340
+ exports.messageMatcher = messageMatcher;
1341
+ exports.not = not;
644
1342
  exports.parseBody = parseBody;
645
1343
  exports.parseConventionalCommit = parseConventionalCommit;
646
1344
  exports.parseFooters = parseFooters;
647
1345
  exports.parseHeader = parseHeader;
1346
+ exports.scopeIsExcluded = scopeIsExcluded;
1347
+ exports.scopeMatcher = scopeMatcher;
1348
+ exports.scopeMatchesProject = scopeMatchesProject;
1349
+ exports.scopePrefixMatcher = scopePrefixMatcher;
1350
+ exports.scopeRegexMatcher = scopeRegexMatcher;
1351
+ exports.toChangelogCommit = toChangelogCommit;
648
1352
  //# sourceMappingURL=index.cjs.js.map