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
160 changes: 24 additions & 136 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.
* commitlint config.
*
* Design rule: a rule is `error` (2) ONLY if a wrong value has real downstream
* impact in this repo. 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 SUBTREE_TRAILERS = [/^git-subtree-dir: \S+$/m, /^git-subtree-split: [0-9a-f]{40}$/m] as const
const isSubtreeSquash = (commit: string) => SUBTREE_TRAILERS.every((trailer) => trailer.test(commit))
Expand Down Expand Up @@ -137,45 +55,15 @@ 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
// Commit types — tidy log/PR grouping only (releases run on changesets).
'type-enum': [
2,
'always',
Expand All @@ -199,32 +87,32 @@ const configuration: UserConfig = {
],
],

// Static scope enum — single-package repo, no workspace discovery
'scope-enum': [2, 'always', [...SCOPES]],
'scope-case': [2, 'always', 'kebab-case'],

// Type constraints
// Type constraints — deterministic, trivial for an agent to satisfy.
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],

// Subject — case disabled (agents capitalize; cosmetic, no release impact)
// Subject: must exist; everything else about it is cosmetic.
'subject-case': [0],
'subject-empty': [2, 'never'],
'subject-full-stop': [2, 'never', '.'],
'subject-full-stop': [0],

// 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 / punctuation: burn 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', '.'],
'header-full-stop': [0],
'body-full-stop': [0],

// 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
44 changes: 0 additions & 44 deletions sandbox-proofs/git-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,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', [...pinnedHooks, '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', [...pinnedHooks, 'commit', '-m', 'build(repo): probe'], { cwd: worktree, env })
Expand Down Expand Up @@ -93,31 +84,8 @@ Deno.test('git hooks run their dependency code inside the sandbox from a linked
)
})

await t.step('a named-path commit stays graded when ambient hook discovery fails', async () => {
await Deno.writeTextFile(`${worktree}/${PROBE}`, '{"probe":2}\n')
const refused = await run('git', [...pinnedHooks, 'commit', '-m', 'feat(repo): probe', PROBE], {
cwd: worktree,
env: {
GIT_CONFIG_COUNT: '1',
GIT_CONFIG_KEY_0: 'core.hooksPath',
GIT_CONFIG_VALUE_0: '/nonexistent-hooks',
},
})
expect(
refused.code !== 0 && refused.out.includes('100% tooling paths'),
'the commit was not graded',
refused,
)
})

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', [...pinnedHooks, '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', [...pinnedHooks, 'commit', '-m', 'build(repo): named path probe', PROBE], {
cwd: worktree,
})
Expand All @@ -131,18 +99,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