Skip to content
Merged
Show file tree
Hide file tree
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
166 changes: 27 additions & 139 deletions commitlint.config.ts
Original file line number Diff line number Diff line change
@@ -1,96 +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[] =>
execFileSync('git', ['diff', '--cached', '--name-only'], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'inherit'] as const,
})
.split('\n')
.map((l: string) => l.trim())
.filter((l: string) => l.length > 0)

const configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
Expand Down Expand Up @@ -133,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 @@ -195,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
27 changes: 0 additions & 27 deletions sandbox-proofs/git-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,15 +80,6 @@ Deno.test('git hooks run their dependency code inside the sandbox from a linked
await Deno.writeTextFile(`${worktree}/${PROBE}`, '{"probe":1}\n')
await must('git', ['add', PROBE], worktree)

await t.step('a feat commit of a tooling-only change is refused by commitlint', async () => {
const outcome = await run('git', ['commit', '-m', 'feat(repo): probe'], { cwd: worktree })
expect(
outcome.code !== 0 && outcome.out.includes('100% tooling paths'),
'the diff-shape rule did not refuse',
outcome,
)
})

await t.step('the hooks refuse to run when the sandbox is unavailable', async () => {
const env = { PATH: await withoutSandboxOnPath() }
const outcome = await run('git', ['commit', '-m', 'build(repo): probe'], { cwd: worktree, env })
Expand Down Expand Up @@ -118,12 +109,6 @@ Deno.test('git hooks run their dependency code inside the sandbox from a linked

await t.step("a commit of named paths is graded and formatted against git's temporary index", async () => {
await Deno.writeTextFile(`${worktree}/${PROBE}`, '{"probe":2}\n')
const refused = await run('git', ['commit', '-m', 'feat(repo): probe', PROBE], { cwd: worktree })
expect(
refused.code !== 0 && refused.out.includes('100% tooling paths'),
'the hooks graded the wrong index',
refused,
)
const landed = await run('git', ['commit', '-m', 'build(repo): named path probe', PROBE], { cwd: worktree })
const subject = await must('git', ['log', '-1', '--format=%s'], worktree)
const committed = await must('git', ['show', `HEAD:${PROBE}`], worktree)
Expand All @@ -135,18 +120,6 @@ Deno.test('git hooks run their dependency code inside the sandbox from a linked
)
})

await t.step("commitlint fails with git's error when git cannot read the index", async () => {
const outcome = await run('sandbox', ['--', 'env', 'GIT_DIR=/nonexistent', 'pnpm', 'exec', 'commitlint'], {
cwd: worktree,
stdin: 'feat(repo): probe\n',
})
expect(
outcome.code !== 0 && outcome.out.includes('Command failed: git diff --cached --name-only'),
'git failed silently',
outcome,
)
})

await t.step('the git directory outside the bound paths is unreadable', async () => {
await Deno.writeTextFile(commonCanary, 'secret')
const outcome = await refusedInside(worktree, `cat '${commonCanary}'`)
Expand Down
Loading