Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 27 additions & 145 deletions commitlint.config.ts
Original file line number Diff line number Diff line change
@@ -1,102 +1,14 @@
import type { UserConfig } from '@commitlint/types'
import { execFileSync } from 'node:child_process'

/**
* Single-package repo: scopes are a static enum, not discovered from a
* workspace. `solutions` covers docs/solutions/ entries; `global` covers
* repo-wide docs and plans.
* systemfsoftware shared commitlint config.
*
* Design rule: a rule is `error` (2) ONLY if a wrong value has real downstream
* impact in THIS org. Releases run on changesets (.changeset/*.md), NOT commit
* messages, so scope / punctuation / type-vs-diff have ZERO release impact and
* must never fail a commit. The one message policy that matters is the
* AI-coauthor ban.
*/
const SCOPES = ['repo', 'deps', 'release', 'ci', 'global', 'solutions'] as const

const matchesAny = (...patterns: readonly RegExp[]) => (path: string) => patterns.some((p) => p.test(path))

const isDoc = matchesAny(
/\.mdx?$/,
/^docs\//,
/(^|\/)README\.md$/i,
/(^|\/)AGENTS\.md$/i,
/(^|\/)CLAUDE\.md$/i,
/(^|\/)CHANGELOG\.md$/i,
)

const isTest = matchesAny(
/\.(test|spec|tst)\.(ts|tsx|js|jsx|mjs|cjs)$/,
/(^|\/)__tests__\//,
/(^|\/)__mocks__\//,
/(^|\/)tests\//,
/(^|\/)test-helpers\//,
/(^|\/)e2e\//,
/(^|\/)fixtures\//,
)

const isCI = matchesAny(
/^\.github\/workflows\//,
/^\.github\/actions\//,
/^\.github\/dependabot\.ya?ml$/,
)

const isLockfile = matchesAny(
/(^|\/)pnpm-lock\.yaml$/,
/(^|\/)package-lock\.json$/,
/(^|\/)bun\.lockb?$/,
/(^|\/)yarn\.lock$/,
)

const isTooling = matchesAny(
/^\.claude\//,
/^\.husky\//,
/(^|\/)commitlint\.config\.[mc]?[jt]s$/,
/(^|\/)\.lintstagedrc(\..+)?$/,
/(^|\/)tsconfig.*\.json$/,
/(^|\/)vitest\.config\.[mc]?[jt]s$/,
/(^|\/)stryker\.conf(ig)?\.[mc]?[jt]s$/,
/(^|\/)stryker(\..+)?\.json$/,
/(^|\/)\.editorconfig$/,
/(^|\/)\.gitignore$/,
/(^|\/)biome\.json$/,
/(^|\/)oxlint\.config\.[mc]?[jt]s$/,
/(^|\/)\.dprint\.jsonc?$/,
/(^|\/)package\.json$/,
/(^|\/)pnpm-workspace\.yaml$/,
/(^|\/)\.npmrc$/,
/(^|\/)dprint\.json$/,
/(^|\/)\.envrc$/,
/^nix\//,
/^bin\//,
/^flake\.nix$/,
/^flake\.lock$/,
)

const ALLOWED_BY_SHAPE: readonly {
readonly name: string
readonly match: (path: string) => boolean
readonly allowed: Readonly<Record<string, true>>
}[] = [
{ name: 'docs', match: isDoc, allowed: { docs: true, chore: true, ai: true } },
{ name: 'test', match: isTest, allowed: { test: true, chore: true } },
{ name: 'CI', match: isCI, allowed: { ci: true, chore: true } },
{ name: 'lockfile', match: isLockfile, allowed: { deps: true, chore: true } },
{
name: 'tooling',
match: isTooling,
allowed: { chore: true, build: true, ci: true, deps: true, ai: true, security: true },
},
]

const stagedFiles = (): readonly string[] => {
try {
const output = execFileSync('git', ['diff', '--cached', '--name-only'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'] as const,
})
return output
.split('\n')
.map((l: string) => l.trim())
.filter((l: string) => l.length > 0)
} catch {
return []
}
}

const configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
Expand Down Expand Up @@ -139,45 +51,17 @@ const configuration: UserConfig = {
if (hasAICoauthor) return [false, message]
return [true, 'OK']
},

'type-matches-diff-shape': ({ type }) => {
const files = stagedFiles()
if (files.length === 0 || type == null || type === '') return [true, 'OK']

const allMatch = (m: (p: string) => boolean) => files.every(m)

for (const shape of ALLOWED_BY_SHAPE) {
if (allMatch(shape.match) && !(type in shape.allowed)) {
const allowed = Object.keys(shape.allowed).sort().join(' / ')
return [false, `'${type}' with 100% ${shape.name} paths — REQUIRED type: ${allowed}`]
}
}

if (type === 'feat' || type === 'fix') {
const hasProductionSource = files.some(
(p) => !isDoc(p) && !isTest(p) && !isCI(p) && !isLockfile(p) && !isTooling(p),
)
if (!hasProductionSource) {
return [
false,
`'${type}' MUST touch >=1 production source file (none of: docs, test, CI, lockfile, tooling)`,
]
}
}

return [true, 'OK']
},
},
},
],

rules: {
// AI co-author prevention (enforced)
// The one policy that matters.
'no-ai-coauthors': [2, 'always'],
'type-matches-diff-shape': [2, 'always'],

// Commit types — aligned with semantic-release changelog filtering
// feat/fix/perf/api/revert/improvement/deps/security bump a version; the rest are noise-filtered out of the changelog
// Type: tidy log/PR grouping only (releases run on changesets).
'type-empty': [2, 'never'],
'type-case': [2, 'always', 'lower-case'],
'type-enum': [
2,
'always',
Expand All @@ -201,32 +85,30 @@ const configuration: UserConfig = {
],
],

// Static scope enum — single-package repo, no workspace discovery
'scope-enum': [2, 'always', [...SCOPES]],
'scope-case': [2, 'always', 'kebab-case'],
// Subject: must exist; everything else about it is cosmetic.
'subject-empty': [2, 'never'],
'subject-case': [0],
'subject-full-stop': [0],

// Type constraints
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
// Punctuation: zero impact. OFF. (This is the reported pain.)
'header-full-stop': [0],
'body-full-stop': [0],

// Subject — case disabled (agents capitalize; cosmetic, no release impact)
'subject-case': [0],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
// Scope: zero release impact and agents can't guess it. OFF; nudge casing.
'scope-enum': [0],
'scope-case': [1, 'always', 'kebab-case'],

// Disabled — length / blank-line cosmetics that burn tokens on retries; semantic-release ignores them
// Length: burns retry tokens, no impact. OFF.
'header-max-length': [0],
'body-max-line-length': [0],
'footer-max-line-length': [0],
'body-leading-blank': [0],
'footer-leading-blank': [0],

// Structural constraints (kept — low friction, prevent trailing-period noise)
'header-full-stop': [2, 'never', '.'],
'body-full-stop': [2, 'never', '.'],

// References encouraged but not required (warning, non-blocking)
// Readability nudges — warn, never block.
'body-leading-blank': [1, 'always'],
'footer-leading-blank': [1, 'always'],
'references-empty': [1, 'never'],
// DELETED: 'type-matches-diff-shape' — heuristic that hard-failed agents
// and its plugin for no downstream benefit. Do not reintroduce.
},

defaultIgnores: true,
Expand Down
Loading