@hyperfrontend/versioning/commits/classify

classify/

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

SourceDescriptionIncludedScope Display
direct-scopeScope matches project✅Omitted
direct-fileFiles touched in project✅Preserved
unscoped-fileNo scope, but touched project files✅None
indirect-dependencyCommit to a dependency package✅Preserved
indirect-infraCommit to build/tooling infrastructure✅Preserved
unscoped-globalNo scope, no project files touched❌N/A
excludedDoes 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:

SourceOriginal ScopeDisplayed ScopeRationale
direct-scopelib-cryptography(omitted)Redundant in project's CHANGELOG
direct-filelib-otherlib-otherInformative context
indirect-dependencylib-utilslib-utilsShows dependency chain
indirect-infratool-packagetool-packageShows infrastructure source

Filtering Strategies

Three strategies are supported via the flow configuration:

StrategyDescriptionUse Case
hybridScope matching + file validationDefault, most accurate
scope-onlyTrust scope completelyDisciplined teams, fast
file-onlyIgnore scopes, use file paths onlyNon-scoped repositories
inferredAuto-detect from commit historyExternal 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

  1. Accuracy over speed: Multi-source validation ensures correct attribution
  2. Opt-in behavior: Dependency and infrastructure tracking are explicit
  3. Graceful degradation: Missing context results in conservative classification
  4. Transparency: Classification source is preserved for auditing

API Reference§

ƒ Functions

§function

allOf(...matchers: unknown): InfrastructureMatcher

Combines matchers with AND logic - returns true if ALL matchers match.
Parameters
NameTypeDescription
§...matchers
unknownMatchers to combine
Returns
InfrastructureMatcher
Combined matcher
Example
Combining matchers with AND logic
const combined = allOf(
  scopeMatcher(['deps']),
  messageMatcher(['security'])
)
§function

anyOf(...matchers: unknown): InfrastructureMatcher

Combines matchers with OR logic - returns true if ANY matcher matches.
Parameters
NameTypeDescription
§...matchers
unknownMatchers to combine
Returns
InfrastructureMatcher
Combined matcher
Example
Combining matchers with OR logic
const combined = anyOf(
  scopeMatcher(['ci', 'build']),
  messageMatcher(['[infra]']),
  custom((ctx) => ctx.scope.some((s) => s.startsWith('tool-')))
)
§function

buildInfrastructureMatcher(config: InfrastructureConfig): InfrastructureMatcher

Builds a combined matcher from infrastructure configuration.
Combines scope-based matching with any custom matcher using OR logic. Path-based matching is handled separately via git queries.
Parameters
NameTypeDescription
§config
InfrastructureConfigInfrastructure configuration
Returns
InfrastructureMatcher
Combined matcher, or null if no matchers configured
Example
Building an infrastructure matcher
const matcher = buildInfrastructureMatcher({
  scopes: ['ci', 'build'],
  matcher: (ctx) => ctx.scope.some((s) => s.startsWith('tool-'))
})
§function

classifyCommit(input: CommitWithRaw, context: ClassificationContext): ClassifiedCommit

Classifies a single commit against a project.
Implements the hybrid classification strategy:
  1. Check scope match (fast path)
  2. Check file touch (validation/catch-all)
  3. Check dependency touch (indirect)
  4. Fallback to excluded
Parameters
NameTypeDescription
§input
CommitWithRawThe commit to classify
§context
ClassificationContextClassification context with project info
Returns
ClassifiedCommit
Classified commit with source attribution
Example
Classifying a single commit
const classified = classifyCommit(
  { commit: parsedCommit, raw: gitCommit },
  { projectScopes: ['versioning'], fileCommitHashes: new Set(['abc123']) }
)
§function

classifyCommits(commits: unknown, context: ClassificationContext): ClassificationResult

Classifies multiple commits against a project.
Parameters
NameTypeDescription
§commits
unknownArray of commits to classify
§context
ClassificationContextClassification context with project info
Returns
ClassificationResult
Classification result with all commits and summary
Example

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, ... } }
§function

createClassificationContext(projectScopes: unknown, fileCommitHashes: ReadonlySet<string>, options?: ClassificationContextOptions): ClassificationContext

Creates a classification context from common inputs.
Parameters
NameTypeDescription
§projectScopes
unknownScopes that match the project
§fileCommitHashes
ReadonlySet<string>Set of commit hashes that touched project files
§options?
ClassificationContextOptionsAdditional context options
Returns
ClassificationContext
A ClassificationContext object
Example

Creating a classification context

const context = createClassificationContext(
  ['auth', 'lib-auth'],
  new Set(['abc123', 'def456']),
  { excludeScopes: ['release'] }
)
§function

createClassifiedCommit(commit: ConventionalCommit, raw: GitCommit, source: CommitSource, options?: CreateClassifiedCommitOptions): ClassifiedCommit

Creates a classified commit.
Parameters
NameTypeDescription
§commit
ConventionalCommitThe parsed conventional commit
§raw
GitCommitThe raw git commit
§source
CommitSourceHow the commit relates to the project
§options?
CreateClassifiedCommitOptionsAdditional classification options
Returns
ClassifiedCommit
A new ClassifiedCommit object
Example

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, ... }
§function

createEmptyClassificationSummary(): ClassificationSummary

Creates an empty classification summary.
Returns
ClassificationSummary
A new ClassificationSummary with all counts at zero
Example

Creating an empty classification summary

const summary = createEmptyClassificationSummary()
// => { total: 0, included: 0, excluded: 0, bySource: { 'direct-scope': 0, ... } }
§function

createMatchContext(commit: GitCommit, scope?: unknown): InfrastructureMatchContext

Creates match context from a git commit.
Extracts scope from conventional commit message if present.
Parameters
NameTypeDescription
§commit
GitCommitGit commit to create context for
§scope?
unknownPre-parsed scope array (optional, saves re-parsing)
Returns
InfrastructureMatchContext
Match context for use with matchers
Example

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' }
§function

deriveProjectScopes(options: DeriveProjectScopesOptions): unknown

Derives all scope variations that should match a project.
Given a project named 'lib-versioning' with package '@hyperfrontend/versioning', this generates variations like:
  • 'lib-versioning' (full project name)
  • 'versioning' (without lib- prefix)
Parameters
NameTypeDescription
§options
DeriveProjectScopesOptionsProject identification options
Returns
unknown
Array of scope strings that match this project
Examples
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']
§function

evaluateInfrastructure(commit: GitCommit, matcher: InfrastructureMatcher, scope?: unknown): boolean

Evaluates a commit against an infrastructure matcher.
Parameters
NameTypeDescription
§commit
GitCommitGit commit to evaluate
§matcher
InfrastructureMatcherMatcher function to apply
§scope?
unknownPre-parsed scope (optional)
Returns
boolean
True if commit matches infrastructure criteria
Example

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'])
// => true
§function

extractConventionalCommits(commits: unknown): unknown

Extracts conventional commits from classified commits for changelog generation.
Parameters
NameTypeDescription
§commits
unknownArray of classified commits
Returns
unknown
Array of conventional commits
Example

Extracting conventional commits

const classified = classifyCommits(commits, context)
const conventional = extractConventionalCommits(classified.included)
// => [{ type: 'feat', subject: 'add login', ... }]
§function

filterIncluded(commits: unknown): unknown

Filters an array of classified commits to only included ones.
Parameters
NameTypeDescription
§commits
unknownArray of classified commits
Returns
unknown
Only commits marked for inclusion
Example

Filtering for included commits

const classified = classifyCommits(commits, context)
const included = filterIncluded(classified.commits)
// => [{ commit, raw, source: 'direct-scope', include: true, ... }]
§function

messageMatcher(patterns: unknown): InfrastructureMatcher

Creates a matcher that checks if commit message contains any of the given patterns.
Parameters
NameTypeDescription
§patterns
unknownPatterns to search for in commit message (case-insensitive)
Returns
InfrastructureMatcher
Matcher that returns true if message contains any pattern
Example
Matching message patterns
const matcher = messageMatcher(['[infra]', '[ci skip]'])
Negates a matcher - returns true if matcher returns false.
Parameters
NameTypeDescription
§matcher
InfrastructureMatcherMatcher to negate
Returns
InfrastructureMatcher
Negated matcher
Example
Negating a matcher
const notRelease = not(scopeMatcher(['release']))
§function

scopeIsExcluded(commitScopes: unknown, excludeScopes: unknown): boolean

Checks if all scopes of a commit are in the exclude list.
A commit is excluded only when every scope entry matches an exclude scope. Commits with no scope entries are never considered excluded here.
Parameters
NameTypeDescription
§commitScopes
unknownScopes from a conventional commit
§excludeScopes
unknownArray of scopes to exclude
Returns
boolean
True if every commit scope is in the exclude list
Example

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)
§function

scopeMatcher(scopes: unknown): InfrastructureMatcher

Creates a matcher that checks if any commit scope matches any of the given scopes.
Parameters
NameTypeDescription
§scopes
unknownScopes to match against (case-insensitive)
Returns
InfrastructureMatcher
Matcher that returns true if any scope element matches
Example
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)
§function

scopeMatchesProject(commitScopes: unknown, projectScopes: unknown): boolean

Checks if any element of a commit's scope list matches any of the project scopes.
A commit matches the project when any of its scope entries matches a project scope. Empty commit scopes never match.
Parameters
NameTypeDescription
§commitScopes
unknownScopes from a conventional commit
§projectScopes
unknownArray of scopes that match the project
Returns
boolean
True if at least one commit scope matches the project
Example
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']) // false
§function

scopePrefixMatcher(prefixes: unknown): InfrastructureMatcher

Creates a matcher that checks if any commit scope starts with any of the given prefixes.
Parameters
NameTypeDescription
§prefixes
unknownScope prefixes to match (case-insensitive)
Returns
InfrastructureMatcher
Matcher that returns true if any scope element starts with any prefix
Example
Matching prefixed scopes
const matcher = scopePrefixMatcher(['tool-', 'infra-'])
matcher({ scope: ['tool-package'], ... }) // true
matcher({ scope: ['lib-utils'], ... }) // false
§function

scopeRegexMatcher(pattern: RegExp): InfrastructureMatcher

Creates a matcher from a regex pattern tested against each scope element.
Parameters
NameTypeDescription
§pattern
RegExpRegex pattern to test against scopes
Returns
InfrastructureMatcher
Matcher that returns true if any scope element matches the regex
Example
Matching with regex pattern
const matcher = scopeRegexMatcher(/^(ci|build|tool)-.+/)
§function

toChangelogCommit(classified: ClassifiedCommit): ConventionalCommit

Creates a modified conventional commit with scope handling based on classification.
For direct commits, the scope is removed (redundant in project changelog). For indirect commits, the scope is preserved (provides context).
Parameters
NameTypeDescription
§classified
ClassifiedCommitCommit with classification metadata determining scope display
Returns
ConventionalCommit
A conventional commit with appropriate scope handling
Example

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

§interface

ClassificationContext

Classification context containing project and workspace info.
Properties
§readonly dependencyCommitMap?:ReadonlyMap<string, ReadonlySet<string>>Map of dependency name to set of commit hashes touching that dependency
§readonly excludeScopes?:unknownScopes to always exclude
§readonly fileCommitHashes:ReadonlySet<string>Set of commit hashes that touched project files
§readonly includeScopes?:unknownAdditional scopes to include as direct
§readonly infrastructureCommitHashes?:ReadonlySet<string>Set of commit hashes that touched infrastructure paths. These commits will be classified as 'indirect-infra'.
§readonly projectScopes:unknownScopes that should be considered direct matches
§interface

ClassificationResult

Result of classifying multiple commits.
Properties
§readonly commits:unknownAll classified commits
§readonly excluded:unknownCommits excluded from changelog
§readonly included:unknownCommits to include in changelog
§readonly summary:ClassificationSummarySummary statistics
§interface

ClassificationSummary

Summary statistics from classification.
Properties
§readonly bySource:Readonly<Record<CommitSource, number>>Breakdown by source type
§readonly excluded:numberCommits excluded from changelog
§readonly included:numberCommits included in changelog
§readonly total:numberTotal commits processed
§interface

ClassifiedCommit

A commit with classification metadata for changelog generation.
Contains the original commit data plus attribution information that determines inclusion and presentation in changelogs.
Properties
§readonly commit:ConventionalCommitThe parsed conventional commit
§readonly dependencyPath?:unknownDependency chain if indirect (e.g., ['lib-utils'] for lib-app → lib-utils)
§readonly include:booleanWhether to include this commit in changelog
§readonly preserveScope:booleanWhether to preserve scope in changelog output
§readonly raw:GitCommitThe raw git commit (for hash, date, etc.)
§readonly source:CommitSourceHow this commit relates to the project
§readonly touchedFiles?:unknownFiles in this project touched by the commit (if applicable)
§interface

CommitWithRaw

Input for classification - a parsed commit with its raw git data.
Properties
§readonly commit:ConventionalCommitThe parsed conventional commit
§readonly raw:GitCommitThe raw git commit data
§interface

DeriveProjectScopesOptions

Options for deriving project scopes.
Properties
§readonly additionalScopes?:unknownAdditional scopes to include
§readonly packageName?:stringThe npm package name (e.g., '@hyperfrontend/versioning')
§readonly prefixes?:unknownProject name prefixes to strip for scope matching.
§readonly projectName:stringThe project name (e.g., 'lib-versioning')
§interface

InfrastructureConfig

Configuration for building infrastructure commit sets.
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?:InfrastructureMatcherCustom matcher function for complex logic. Combined with paths/scopes using OR logic.
§readonly paths?:unknownFile/directory paths that indicate infrastructure changes. Used to query git for commits touching these paths.
§readonly scopes?:unknownConventional commit scopes that indicate infrastructure. Matched against parsed commit scope.
§interface

InfrastructureMatchContext

Context provided to infrastructure matchers for decision making.
Contains information about a commit that helps determine whether it should be classified as infrastructure-related.
Properties
§readonly commit:GitCommitThe git commit being evaluated
§readonly message:stringFull commit message
§readonly scope:unknownParsed conventional scopes (empty array when the commit has no scope)
§readonly subject:stringCommit message subject

◆ Types

§type

CommitSource

Source of how a commit relates to a project.
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"
§type

InfrastructureMatcher

Infrastructure matcher function type.
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

§type

CI_SCOPE_MATCHER

Matches common CI/CD scopes.
Matches: ci, cd, build, pipeline, workflow, actions
§type

DEFAULT_EXCLUDE_SCOPES

Default scopes to exclude from changelogs.
These represent repository-level or infrastructure changes that typically don't belong in individual project changelogs.
§type

DEFAULT_INFRA_SCOPE_MATCHER

Combined matcher for common infrastructure patterns.
Combines CI, tooling, and tool-prefix matchers.
§type

DEFAULT_PROJECT_PREFIXES

Default project name prefixes that can be stripped for scope matching.
§type

TOOL_PREFIX_MATCHER

Matches tool-prefixed scopes (e.g., tool-package, tool-scripts).
§type

TOOLING_SCOPE_MATCHER

Matches common tooling/workspace scopes.
Matches: tooling, workspace, monorepo, nx, root