@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

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

  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
unknown
Matchers 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
unknown
Matchers 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
InfrastructureConfig
Infrastructure 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
CommitWithRaw
The commit to classify
§context
ClassificationContext
Classification 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
unknown
Array of commits to classify
§context
ClassificationContext
Classification 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
unknown
Scopes that match the project
§fileCommitHashes
ReadonlySet<string>
Set of commit hashes that touched project files
§options?
ClassificationContextOptions
Additional 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
ConventionalCommit
The parsed conventional commit
§raw
GitCommit
The raw git commit
§source
CommitSource
How the commit relates to the project
§options?
CreateClassifiedCommitOptions
Additional 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
GitCommit
Git commit to create context for
§scope?
unknown
Pre-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
DeriveProjectScopesOptions
Project 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
GitCommit
Git commit to evaluate
§matcher
InfrastructureMatcher
Matcher function to apply
§scope?
unknown
Pre-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
unknown
Array 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
unknown
Array 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
unknown
Patterns 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]'])
§function

not(matcher: InfrastructureMatcher): InfrastructureMatcher

Negates a matcher - returns true if matcher returns false.

Parameters

NameTypeDescription
§matcher
InfrastructureMatcher
Matcher 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
unknown
Scopes from a conventional commit
§excludeScopes
unknown
Array 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
unknown
Scopes 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
unknown
Scopes from a conventional commit
§projectScopes
unknown
Array 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
unknown
Scope 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
RegExp
Regex 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
ClassifiedCommit
Commit 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?:unknown
Scopes to always exclude
§readonly fileCommitHashes:ReadonlySet<string>
Set of commit hashes that touched project files
§readonly includeScopes?:unknown
Additional 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:unknown
Scopes that should be considered direct matches
§interface

ClassificationResult

Result of classifying multiple commits.

Properties

§readonly commits:unknown
All classified commits
§readonly excluded:unknown
Commits excluded from changelog
§readonly included:unknown
Commits to include in changelog
§readonly summary:ClassificationSummary
Summary statistics
§interface

ClassificationSummary

Summary statistics from classification.

Properties

§readonly bySource:Readonly<Record<CommitSource, number>>
Breakdown by source type
§readonly excluded:number
Commits excluded from changelog
§readonly included:number
Commits included in changelog
§readonly total:number
Total 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:ConventionalCommit
The parsed conventional commit
§readonly dependencyPath?:unknown
Dependency chain if indirect (e.g., ['lib-utils'] for lib-app → lib-utils)
§readonly include:boolean
Whether to include this commit in changelog
§readonly preserveScope:boolean
Whether to preserve scope in changelog output
§readonly raw:GitCommit
The raw git commit (for hash, date, etc.)
§readonly source:CommitSource
How this commit relates to the project
§readonly touchedFiles?:unknown
Files 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:ConventionalCommit
The parsed conventional commit
§readonly raw:GitCommit
The raw git commit data
§interface

DeriveProjectScopesOptions

Options for deriving project scopes.

Properties

§readonly additionalScopes?:unknown
Additional scopes to include
§readonly packageName?:string
The npm package name (e.g., '@hyperfrontend/versioning')
§readonly prefixes?:unknown
Project name prefixes to strip for scope matching.
§readonly projectName:string
The 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?:InfrastructureMatcher
Custom matcher function for complex logic. Combined with paths/scopes using OR logic.
§readonly paths?:unknown
File/directory paths that indicate infrastructure changes. Used to query git for commits touching these paths.
§readonly scopes?:unknown
Conventional 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:GitCommit
The git commit being evaluated
§readonly message:string
Full commit message
§readonly scope:unknown
Parsed conventional scopes (empty array when the commit has no scope)
§readonly subject:string
Commit 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