@hyperfrontend/versioning/commits/classifyclassify/
Commit classification engine for monorepo changelog attribution.
Overview
This module provides intelligent commit classification for monorepo versioning. It determines whether a commit should appear in a project's changelog and how its scope should be displayed based on multiple attribution sources.
Commit Sources
| Source | Description | Included | Scope Display |
|---|---|---|---|
direct-scope |
Scope matches project | ✅ | Omitted |
direct-file |
Files touched in project | ✅ | Preserved |
unscoped-file |
No scope, but touched project files | ✅ | None |
indirect-dependency |
Commit to a dependency package | ✅ | Preserved |
indirect-infra |
Commit to build/tooling infrastructure | ✅ | Preserved |
unscoped-global |
No scope, no project files touched | ❌ | N/A |
excluded |
Does not relate to project | ❌ | N/A |
Usage Examples
Basic Classification
import { classifyCommits, createClassificationContext, deriveProjectScopes } from '@hyperfrontend/versioning'
// Create context with project info
const context = createClassificationContext({
projectScopes: deriveProjectScopes('lib-cryptography'),
fileCommitHashes: new Set(['abc123', 'def456']), // From git log --path
})
// Classify commits
const result = classifyCommits(commits, context)
// Access results
console.log(result.included.length) // Commits for changelog
console.log(result.summary) // Statistics
With Dependency Tracking
import { classifyCommits, createClassificationContext, deriveProjectScopes } from '@hyperfrontend/versioning'
const context = createClassificationContext({
projectScopes: deriveProjectScopes('lib-app'),
fileCommitHashes: new Set(['abc123']),
// Track dependency changes
dependencyCommitMap: new Map([
['lib-utils', new Set(['xyz789'])],
['lib-core', new Set(['uvw456'])],
]),
})
const result = classifyCommits(commits, context)
// Commits to lib-utils and lib-core appear as indirect-dependency
result.included.filter((c) => c.source === 'indirect-dependency')
With Infrastructure Detection
import {
classifyCommits,
createClassificationContext,
deriveProjectScopes,
scopeMatcher,
scopePrefixMatcher,
anyOf,
} from '@hyperfrontend/versioning'
const context = createClassificationContext({
projectScopes: deriveProjectScopes('lib-app'),
fileCommitHashes: new Set(['abc123']),
// Track infrastructure commits
infrastructureCommitHashes: new Set(['infra789']),
})
// Or use composable matchers for scope-based detection
const infraMatcher = anyOf(scopeMatcher(['ci', 'build']), scopePrefixMatcher(['tool-']))
Converting to Changelog Format
import { classifyCommits, toChangelogCommit, filterIncluded } from '@hyperfrontend/versioning'
const result = classifyCommits(commits, context)
// Get changelog-ready commits with scope rules applied
const changelogCommits = filterIncluded(result).map(toChangelogCommit)
// direct-scope commits have scope omitted (redundant)
// indirect commits have scope preserved (provides context)
Scope Display Rules
The classification engine applies intelligent scope display rules:
| Source | Original Scope | Displayed Scope | Rationale |
|---|---|---|---|
direct-scope |
lib-cryptography |
(omitted) | Redundant in project's CHANGELOG |
direct-file |
lib-other |
lib-other |
Informative context |
indirect-dependency |
lib-utils |
lib-utils |
Shows dependency chain |
indirect-infra |
tool-package |
tool-package |
Shows infrastructure source |
Filtering Strategies
Three strategies are supported via the flow configuration:
| Strategy | Description | Use Case |
|---|---|---|
hybrid |
Scope matching + file validation | Default, most accurate |
scope-only |
Trust scope completely | Disciplined teams, fast |
file-only |
Ignore scopes, use file paths only | Non-scoped repositories |
inferred |
Auto-detect from commit history | External codebases |
Configuration
Classification is configured via ScopeFilteringConfig in the flow:
import { createVersionFlow } from '@hyperfrontend/versioning'
const flow = createVersionFlow('conventional', {
scopeFiltering: {
strategy: 'hybrid', // default
includeScopes: ['shared-utils'], // extra scopes to include
excludeScopes: ['release', 'deps'], // default exclusions
trackDependencyChanges: true, // enable Phase 4
infrastructure: {
paths: ['tools/', '.github/'],
scopes: ['ci', 'build'],
},
},
})
Design Principles
- Accuracy over speed: Multi-source validation ensures correct attribution
- Opt-in behavior: Dependency and infrastructure tracking are explicit
- Graceful degradation: Missing context results in conservative classification
- Transparency: Classification source is preserved for auditing
API Reference
ƒ Functions
Parameters
| Name | Type | Description |
|---|---|---|
§...matchers | unknown | Matchers to combine |
Returns
InfrastructureMatcherExample
Combining matchers with AND logic
const combined = allOf(
scopeMatcher(['deps']),
messageMatcher(['security'])
)Parameters
| Name | Type | Description |
|---|---|---|
§...matchers | unknown | Matchers to combine |
Returns
InfrastructureMatcherExample
Combining matchers with OR logic
const combined = anyOf(
scopeMatcher(['ci', 'build']),
messageMatcher(['[infra]']),
custom((ctx) => ctx.scope.some((s) => s.startsWith('tool-')))
)Combines scope-based matching with any custom matcher using OR logic. Path-based matching is handled separately via git queries.
Parameters
| Name | Type | Description |
|---|---|---|
§config | InfrastructureConfig | Infrastructure configuration |
Returns
InfrastructureMatcherExample
Building an infrastructure matcher
const matcher = buildInfrastructureMatcher({
scopes: ['ci', 'build'],
matcher: (ctx) => ctx.scope.some((s) => s.startsWith('tool-'))
})Implements the hybrid classification strategy:
- Check scope match (fast path)
- Check file touch (validation/catch-all)
- Check dependency touch (indirect)
- Fallback to excluded
Parameters
Returns
ClassifiedCommitExample
Classifying a single commit
const classified = classifyCommit(
{ commit: parsedCommit, raw: gitCommit },
{ projectScopes: ['versioning'], fileCommitHashes: new Set(['abc123']) }
)Parameters
Returns
ClassificationResultExample
Classifying multiple commits
const commits = [{ commit: parsedCommit, raw: gitCommit }]
const context = createClassificationContext(['auth'], fileHashes)
const result = classifyCommits(commits, context)
// => { commits: [...], included: [...], excluded: [...], summary: { total: 1, ... } }createClassificationContext(projectScopes: unknown, fileCommitHashes: ReadonlySet<string>, options?: ClassificationContextOptions): ClassificationContext
Parameters
Returns
ClassificationContextExample
Creating a classification context
const context = createClassificationContext(
['auth', 'lib-auth'],
new Set(['abc123', 'def456']),
{ excludeScopes: ['release'] }
)createClassifiedCommit(commit: ConventionalCommit, raw: GitCommit, source: CommitSource, options?: CreateClassifiedCommitOptions): ClassifiedCommit
Parameters
Returns
ClassifiedCommitExample
Creating a classified commit
const commit = { type: 'feat', subject: 'add feature', footers: [], breaking: false, raw: '...' }
const raw = { hash: 'abc123', subject: 'feat: add feature', message: '...' }
const classified = createClassifiedCommit(commit, raw, 'direct-scope')
// => { commit, raw, source: 'direct-scope', include: true, preserveScope: false, ... }Returns
ClassificationSummaryExample
Creating an empty classification summary
const summary = createEmptyClassificationSummary()
// => { total: 0, included: 0, excluded: 0, bySource: { 'direct-scope': 0, ... } }Extracts scope from conventional commit message if present.
Parameters
Returns
InfrastructureMatchContextExample
Creating match context from a git commit
const commit = { hash: 'abc123', subject: 'chore(ci): update workflow', message: 'chore(ci): update workflow' }
createMatchContext(commit, ['ci'])
// => { commit, scope: ['ci'], subject: 'chore(ci): update workflow', message: 'chore(ci): update workflow' }Given a project named 'lib-versioning' with package '@hyperfrontend/versioning', this generates variations like:
- 'lib-versioning' (full project name)
- 'versioning' (without lib- prefix)
Parameters
| Name | Type | Description |
|---|---|---|
§options | DeriveProjectScopesOptions | Project identification options |
Returns
unknownExamples
Deriving scopes for a library project
deriveProjectScopes({ projectName: 'lib-versioning', packageName: '@hyperfrontend/versioning' })
// Returns: ['lib-versioning', 'versioning']Deriving scopes for an app project
deriveProjectScopes({ projectName: 'app-demo', packageName: 'demo-app' })
// Returns: ['app-demo', 'demo']evaluateInfrastructure(commit: GitCommit, matcher: InfrastructureMatcher, scope?: unknown): boolean
Parameters
Returns
booleanExample
Evaluating a commit against infrastructure matcher
const commit = { hash: 'abc123', subject: 'chore(ci): update workflow', message: '...' }
const ciMatcher = (ctx) => ctx.scope.includes('ci')
evaluateInfrastructure(commit, ciMatcher, ['ci'])
// => trueParameters
| Name | Type | Description |
|---|---|---|
§commits | unknown | Array of classified commits |
Returns
unknownExample
Extracting conventional commits
const classified = classifyCommits(commits, context)
const conventional = extractConventionalCommits(classified.included)
// => [{ type: 'feat', subject: 'add login', ... }]Parameters
| Name | Type | Description |
|---|---|---|
§commits | unknown | Array of classified commits |
Returns
unknownExample
Filtering for included commits
const classified = classifyCommits(commits, context)
const included = filterIncluded(classified.commits)
// => [{ commit, raw, source: 'direct-scope', include: true, ... }]Parameters
| Name | Type | Description |
|---|---|---|
§patterns | unknown | Patterns to search for in commit message (case-insensitive) |
Returns
InfrastructureMatcherExample
Matching message patterns
const matcher = messageMatcher(['[infra]', '[ci skip]'])Parameters
| Name | Type | Description |
|---|---|---|
§matcher | InfrastructureMatcher | Matcher to negate |
Returns
InfrastructureMatcherExample
Negating a matcher
const notRelease = not(scopeMatcher(['release']))A commit is excluded only when every scope entry matches an exclude scope. Commits with no scope entries are never considered excluded here.
Parameters
Returns
booleanExample
Checking if a scope should be excluded
scopeIsExcluded(['release'], ['release', 'deps'])
// => true
scopeIsExcluded(['auth'], ['release', 'deps'])
// => false
scopeIsExcluded([], ['release'])
// => false
scopeIsExcluded(['release', 'auth'], ['release'])
// => false (not every scope is excluded)Parameters
| Name | Type | Description |
|---|---|---|
§scopes | unknown | Scopes to match against (case-insensitive) |
Returns
InfrastructureMatcherExample
Matching against specific scopes
const matcher = scopeMatcher(['ci', 'build', 'tooling'])
matcher({ scope: ['CI'], ... }) // true
matcher({ scope: ['feat'], ... }) // false
matcher({ scope: ['feat', 'ci'], ... }) // true (any element matches)A commit matches the project when any of its scope entries matches a project scope. Empty commit scopes never match.
Parameters
Returns
booleanExample
Matching scope to project
scopeMatchesProject(['versioning'], ['lib-versioning', 'versioning']) // true
scopeMatchesProject(['logging'], ['lib-versioning', 'versioning']) // false
scopeMatchesProject(['versioning', 'questions'], ['lib-questions']) // true
scopeMatchesProject([], ['lib-versioning']) // falseParameters
| Name | Type | Description |
|---|---|---|
§prefixes | unknown | Scope prefixes to match (case-insensitive) |
Returns
InfrastructureMatcherExample
Matching prefixed scopes
const matcher = scopePrefixMatcher(['tool-', 'infra-'])
matcher({ scope: ['tool-package'], ... }) // true
matcher({ scope: ['lib-utils'], ... }) // falseParameters
| Name | Type | Description |
|---|---|---|
§pattern | RegExp | Regex pattern to test against scopes |
Returns
InfrastructureMatcherExample
Matching with regex pattern
const matcher = scopeRegexMatcher(/^(ci|build|tool)-.+/)For direct commits, the scope is removed (redundant in project changelog). For indirect commits, the scope is preserved (provides context).
Parameters
| Name | Type | Description |
|---|---|---|
§classified | ClassifiedCommit | Commit with classification metadata determining scope display |
Returns
ConventionalCommitExample
Converting classified commit for changelog
const classified = { commit: { type: 'feat', scope: ['auth'], subject: 'add login', ... }, source: 'direct-scope', preserveScope: false, ... }
toChangelogCommit(classified)
// => { type: 'feat', scope: [], subject: 'add login', ... }◈ Interfaces
Properties
readonly dependencyCommitMap?:ReadonlyMap<string, ReadonlySet<string>>— readonly infrastructureCommitHashes?:ReadonlySet<string>— Contains the original commit data plus attribution information that determines inclusion and presentation in changelogs.
Properties
readonly dependencyPath?:unknown— Properties
Supports multiple detection methods that can be combined:
- Path-based: Commits touching specific file paths
- Scope-based: Commits with specific conventional scopes
- Custom matcher: User-provided matching logic
Properties
readonly matcher?:InfrastructureMatcher— readonly paths?:unknown— readonly scopes?:unknown— Contains information about a commit that helps determine whether it should be classified as infrastructure-related.
◆ Types
Classification determines whether a commit should appear in a project's changelog and how its scope should be displayed.
type CommitSource = "direct-scope" | "direct-file" | "unscoped-file" | "indirect-dependency" | "indirect-infra" | "unscoped-global" | "excluded"Returns true if the commit should be classified as infrastructure-related. Matchers are composable - combine them using
anyOf, allOf, and not.type InfrastructureMatcher = (context: InfrastructureMatchContext) => boolean● Variables
Matches: ci, cd, build, pipeline, workflow, actions
These represent repository-level or infrastructure changes that typically don't belong in individual project changelogs.
Combines CI, tooling, and tool-prefix matchers.
Matches: tooling, workspace, monorepo, nx, root