diff --git a/README.md b/README.md
index 63ea5b4..e40dcf3 100644
--- a/README.md
+++ b/README.md
@@ -30,6 +30,32 @@
| đź”’ **Strict and predictable** | Rejects malformed JSON, unknown options, duplicate keys, and invalid UTF-8. |
| 🌍 **International subjects** | Counts Unicode code points while keeping syntax checks fast and ASCII-based. |
+## 🔎 Quick Commitlint vs Commitlint
+
+This comparison uses `@commitlint/cli` 21.2.1 and the `@commitlint/config-conventional` and `@commitlint/config-angular` 21.2.0 packages pinned by this repository.
+
+| Area | Quick Commitlint | Commitlint |
+| -------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
+| Conventional preset | Built in as `conventional` and used by default | Available through `@commitlint/config-conventional` or the CLI's `--default-config` fallback |
+| Angular preset | Built in as `angular` and selected in strict JSON | Available as the shareable `@commitlint/config-angular` package |
+| Runtime and dependencies | Small Node.js launcher plus a bundled native Zig executable; no runtime npm dependencies | Node.js CLI with its JavaScript configuration, parsing, formatting, and rule dependencies |
+| Configuration | One strict `.quick-commitlint.json` format, two presets, and 14 supported rule overrides | JS, TS, JSON, YAML, and package configuration with shareable configs, plugins, and custom behavior |
+| Inputs and ignored commits | Reads standard input or one commit-message file and lints every message | Also reads Git ranges, tags, the last commit, and edit/environment files; supports default ignores |
+| Best fit | Projects using the included presets that value predictable configuration and native linting | Projects needing broader rules, custom parsers, plugins, shareable configs, ignores, or history linting |
+
+The supported Conventional and Angular defaults are protected by differential tests, but Quick Commitlint is not a drop-in replacement for every Commitlint setup. Read the [full compatibility guide](https://celtian.github.io/quick-commitlint/docs/compatibility/) for the exact boundary.
+
+## 📊 Performance
+
+Reference cold-process benchmark:
+
+| Preset | Quick Commitlint median | Commitlint median | Improvement |
+| ------------ | ----------------------: | ----------------: | ----------: |
+| Conventional | 20.686 ms | 250.597 ms | 12.1Ă— |
+| Angular | 20.535 ms | 250.228 ms | 12.2Ă— |
+
+These are median wall-clock results from 40 measured launches after 3 warmups on Linux x64 under WSL2, using a 13th Gen Intel Core i9-13900H and Node.js 24.18.0. See the dedicated [Performance documentation](https://celtian.github.io/quick-commitlint/docs/performance/) for the complete method and reproduction steps.
+
## 🚀 Install
With npm:
diff --git a/bun.lock b/bun.lock
index 86d1f5d..6e4a0d6 100644
--- a/bun.lock
+++ b/bun.lock
@@ -6,9 +6,9 @@
"name": "quick-commitlint",
"devDependencies": {
"@angular-eslint/builder": "22.1.0",
- "@angular/build": "^22.0.8",
+ "@angular/build": "^22.0.9",
"@angular/cdk": "^22.0.6",
- "@angular/cli": "^22.0.8",
+ "@angular/cli": "^22.0.9",
"@angular/common": "^22.0.8",
"@angular/compiler": "^22.0.8",
"@angular/compiler-cli": "^22.0.8",
@@ -18,7 +18,7 @@
"@angular/platform-browser": "^22.0.8",
"@angular/platform-server": "^22.0.8",
"@angular/router": "^22.0.8",
- "@angular/ssr": "^22.0.8",
+ "@angular/ssr": "^22.0.9",
"@commitlint/cli": "21.2.1",
"@commitlint/config-angular": "21.2.0",
"@commitlint/config-conventional": "21.2.0",
@@ -32,14 +32,14 @@
"axe-core": "^4.12.1",
"eslint": "^10.8.0",
"fs-extra": "^11.4.0",
- "globals": "^17.7.0",
+ "globals": "^17.8.0",
"husky": "^9.1.7",
- "jsdom": "^29.1.1",
- "lint-staged": "^17.0.8",
+ "jsdom": "^30.0.1",
+ "lint-staged": "^17.2.0",
"marked": "^18.0.7",
"ngx-markdown": "^22.0.0",
"ngx-scrolltop": "^22.0.0",
- "postcss": "^8.5.23",
+ "postcss": "^8.5.24",
"prettier": "^3.9.6",
"rimraf": "^6.1.3",
"rxjs": "^7.8.2",
@@ -88,7 +88,7 @@
"@angular-devkit/core": ["@angular-devkit/core@22.0.7", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" } }, "sha512-r8XiflsVYcsvWp+zVvaNv5GsDoihaZ2OwWfn++N6YqTUZLcqDzqhsxeNk60sJ5V+Jn4ck1aKF4A9flmvSY+tpQ=="],
- "@angular-devkit/schematics": ["@angular-devkit/schematics@22.0.8", "", { "dependencies": { "@angular-devkit/core": "22.0.8", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.4.0", "rxjs": "7.8.2" } }, "sha512-9WjTKnW/5oIw4hNNcd0rSYtgHRrHKKAkG6+bdLqTCY68P2brO2yEJM2JjYtMYk/WSovknb0GxUP4n3dY3IxPug=="],
+ "@angular-devkit/schematics": ["@angular-devkit/schematics@22.0.9", "", { "dependencies": { "@angular-devkit/core": "22.0.9", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.4.0", "rxjs": "7.8.2" } }, "sha512-r7tPuiXI7pnwcaLQY4hQLOM9R5rkKRc1mKhQQb074Shh8XzTcTaw2ktDXLeiJm/SUUWQPhaxClnjjO3qU8AJRw=="],
"@angular-eslint/builder": ["@angular-eslint/builder@22.1.0", "", { "dependencies": { "@angular-devkit/architect": ">= 0.2200.0 < 0.2300.0", "@angular-devkit/core": ">= 22.0.0 < 23.0.0" } }, "sha512-gmdk06PK0whNJlapIQjRnhud2/n+oIIDBIoLUHWFFxhHRs+ZstVc6+81gf+sYDOm1Ehmb0V3nZAg3YpT1Mk0Eg=="],
@@ -104,11 +104,11 @@
"@angular-eslint/utils": ["@angular-eslint/utils@22.1.0", "", { "dependencies": { "@angular-eslint/bundled-angular-compiler": "22.1.0" } }, "sha512-zDPJqgOxlkG20UJWsVDSvnEtC2MfIP0+yKMaQUBL+2nrEknV/fVLrz7ApnerWiDrGTcAms5si49KV10MarMpRA=="],
- "@angular/build": ["@angular/build@22.0.8", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.2200.8", "@babel/core": "7.29.7", "@babel/helper-annotate-as-pure": "7.29.7", "@babel/helper-split-export-declaration": "7.24.7", "@inquirer/confirm": "6.0.12", "@vitejs/plugin-basic-ssl": "2.3.0", "beasties": "0.4.2", "browserslist": "^4.26.0", "esbuild": "0.28.1", "https-proxy-agent": "9.0.0", "jsonc-parser": "3.3.1", "listr2": "10.2.1", "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.4", "piscina": "5.2.0", "rollup": "4.60.2", "sass": "1.99.0", "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.16", "vite": "7.3.6", "watchpack": "2.5.1" }, "optionalDependencies": { "lmdb": "3.5.4" } }, "sha512-QJVVTROVb27Ixk+RU4FSKwwyRbLdtNH20zFm1CwOKzA0wQHrWfxHTqlwMhnTOsOt5Cb3eX+6IjDXoGmAfJ81mA=="],
+ "@angular/build": ["@angular/build@22.0.9", "", { "dependencies": { "@ampproject/remapping": "2.3.0", "@angular-devkit/architect": "0.2200.9", "@babel/core": "7.29.7", "@babel/helper-annotate-as-pure": "7.29.7", "@babel/helper-split-export-declaration": "7.24.7", "@inquirer/confirm": "6.0.12", "@vitejs/plugin-basic-ssl": "2.3.0", "beasties": "0.4.2", "browserslist": "^4.26.0", "esbuild": "0.28.1", "https-proxy-agent": "9.0.0", "jsonc-parser": "3.3.1", "listr2": "10.2.1", "magic-string": "0.30.21", "mrmime": "2.0.1", "parse5-html-rewriting-stream": "8.0.1", "picomatch": "4.0.4", "piscina": "5.2.0", "rollup": "4.60.2", "sass": "1.99.0", "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.16", "vite": "7.3.6", "watchpack": "2.5.1" }, "optionalDependencies": { "lmdb": "3.5.4" }, "peerDependencies": { "@angular/compiler": "^22.0.0", "@angular/compiler-cli": "^22.0.0", "@angular/core": "^22.0.0", "@angular/localize": "^22.0.0", "@angular/platform-browser": "^22.0.0", "@angular/platform-server": "^22.0.0", "@angular/service-worker": "^22.0.0", "@angular/ssr": "^22.0.9", "istanbul-lib-instrument": "^6.0.0", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^22.0.0", "postcss": "^8.4.0", "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", "typescript": ">=6.0 <6.1", "vitest": "^4.0.8" }, "optionalPeers": ["@angular/core", "@angular/localize", "@angular/platform-browser", "@angular/platform-server", "@angular/service-worker", "@angular/ssr", "istanbul-lib-instrument", "karma", "less", "ng-packagr", "postcss", "tailwindcss", "vitest"] }, "sha512-j5eYaMTGFXdPVSX0xtWRRWHE2Ezz2CFiBPsAKrWHitvzccv95nURQYk5jbB+FHCYuN60zJNoTZMIlT7rXYAvag=="],
"@angular/cdk": ["@angular/cdk@22.0.6", "", { "dependencies": { "parse5": "^8.0.0", "tslib": "^2.3.0" } }, "sha512-b36f4b+7yQpuulG9Ai2O38SV2tsqyNeYTYCH9TehMir18XlTx+NtLELwA8Sfi24tHByJHEDADQZtHPWWUj71/w=="],
- "@angular/cli": ["@angular/cli@22.0.8", "", { "dependencies": { "@angular-devkit/architect": "0.2200.8", "@angular-devkit/core": "22.0.8", "@angular-devkit/schematics": "22.0.8", "@inquirer/prompts": "8.4.2", "@listr2/prompt-adapter-inquirer": "4.2.3", "@modelcontextprotocol/sdk": "1.29.0", "@schematics/angular": "22.0.8", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.52.0", "ini": "6.0.0", "jsonc-parser": "3.3.1", "listr2": "10.2.1", "npm-package-arg": "13.0.2", "pacote": "21.5.1", "parse5-html-rewriting-stream": "8.0.1", "semver": "7.7.4", "yargs": "18.0.0", "zod": "4.4.2" }, "bin": { "ng": "bin/ng.js" } }, "sha512-kefbTsmf7sEmW5g3NIRvH0OrM18I1jjZHlB9KABgRQhsDfD/WgfLt6aSLJQeFDlAOs+ehDPzzOwd9l48wyBlFQ=="],
+ "@angular/cli": ["@angular/cli@22.0.9", "", { "dependencies": { "@angular-devkit/architect": "0.2200.9", "@angular-devkit/core": "22.0.9", "@angular-devkit/schematics": "22.0.9", "@inquirer/prompts": "8.4.2", "@listr2/prompt-adapter-inquirer": "4.2.3", "@modelcontextprotocol/sdk": "1.29.0", "@schematics/angular": "22.0.9", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.52.0", "ini": "6.0.0", "jsonc-parser": "3.3.1", "listr2": "10.2.1", "npm-package-arg": "13.0.2", "pacote": "21.5.1", "parse5-html-rewriting-stream": "8.0.1", "semver": "7.7.4", "yargs": "18.0.0", "zod": "4.4.2" }, "bin": { "ng": "bin/ng.js" } }, "sha512-nkdz9sc9OjTLBG2QwPH8HuFc4fOl489AzrKInM2y+Q0u1+/HFt9QpSeupPnDJeOYGImDIP0B2VJJ3teDRM1DGg=="],
"@angular/common": ["@angular/common@22.0.8", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-FnwkXndUgAyWk1/p5flMfocIUtuuneJ01mxC1FxRc5/bdWTyNVeQDsM9Lw4PEAPc0AHu//EnblegRq8o6LGdUQ=="],
@@ -128,15 +128,11 @@
"@angular/router": ["@angular/router@22.0.8", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-9NO5K03QqfWMWWZ+Uu4WqBSLw/YAot918cBUFdgl6mTUOfoHi/xWO5pAt0SEpbXMHmWafJBdb99GRlAGybGSTg=="],
- "@angular/ssr": ["@angular/ssr@22.0.8", "", { "dependencies": { "tslib": "^2.3.0" } }, "sha512-T3N0w40R7rQQ3oBhkbupNf0g6Vt1F1huQqNgdblImdOFAt05nXPlrwjV37MJ78vYpJnAjNSuGmqIfnm3OyflhQ=="],
+ "@angular/ssr": ["@angular/ssr@22.0.9", "", { "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/common": "^22.0.0", "@angular/core": "^22.0.0", "@angular/platform-server": "^22.0.0", "@angular/router": "^22.0.0" }, "optionalPeers": ["@angular/platform-server"] }, "sha512-l9JEdesOFfmoiI9o4LWGSjhF8pFtTB5xkOHe/tRQ2BJFfUg2QvBgOPs5JW5pbiqDy4damwILM4H7gOWxdrohBA=="],
- "@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
+ "@asamuzakjp/css-color": ["@asamuzakjp/css-color@6.0.5", "", { "dependencies": { "@csstools/css-calc": "^3.2.1", "@csstools/css-color-parser": "^4.1.9", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0", "lru-cache": "^11.5.2" } }, "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA=="],
- "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
-
- "@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
-
- "@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
+ "@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@8.3.0", "", { "dependencies": { "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2" } }, "sha512-UJLfKXBhrc8i1vH2eJXuYQMwlsLKWFw3O+CPqXSuVEiikeAim3UgrfWX0k4tA/X8cRFM8iZ7OaqBokFGbYusdg=="],
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
@@ -566,7 +562,7 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.2", "", { "os": "win32", "cpu": "x64" }, "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA=="],
- "@schematics/angular": ["@schematics/angular@22.0.8", "", { "dependencies": { "@angular-devkit/core": "22.0.8", "@angular-devkit/schematics": "22.0.8", "jsonc-parser": "3.3.1", "typescript": "6.0.3" } }, "sha512-zD2WLe15SoKYyBZH1GTLLaVABNNObHkH4soQqXb9agN6zkrpE0naeWaBSFSWD+95VKCiMZl8LkiOXvK+6b8S6w=="],
+ "@schematics/angular": ["@schematics/angular@22.0.9", "", { "dependencies": { "@angular-devkit/core": "22.0.9", "@angular-devkit/schematics": "22.0.9", "jsonc-parser": "3.3.1", "typescript": "6.0.3" } }, "sha512-moQI3UnqUt6GZ81ykxJRUbf+IpM6SDFRyMMj6A2VNSiUGVpSAETNFIuTk/6YDzNG8yhMN9zOICeBwvvPH0ylbA=="],
"@sigstore/bundle": ["@sigstore/bundle@4.0.0", "", { "dependencies": { "@sigstore/protobuf-specs": "^0.5.0" } }, "sha512-NwCl5Y0V6Di0NexvkTqdoVfmjTaQwoLM236r89KEojGmq/jMls8S+zb7yOwAPdXvbwfKDlP+lmXgAL4vKSQT+A=="],
@@ -1040,7 +1036,7 @@
"js-yaml": ["js-yaml@4.3.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q=="],
- "jsdom": ["jsdom@29.1.1", "", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" } }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="],
+ "jsdom": ["jsdom@30.0.1", "", { "dependencies": { "@asamuzakjp/css-color": "^6.0.5", "@asamuzakjp/dom-selector": "^8.3.0", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.7", "@exodus/bytes": "^1.15.1", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.5.2", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.2", "undici": "^8.9.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^17.1.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.2.3" }, "optionalPeers": ["canvas"] }, "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
@@ -1256,7 +1252,7 @@
"pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="],
- "postcss": ["postcss@8.5.23", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg=="],
+ "postcss": ["postcss@8.5.24", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-8RyVklq0owXUTa4xlpzu4l9AaVKIdQvAcOHZWaMh98HgySsUtxRVf/chRe3dsSLqb6i40BzGRzEUddRaI+9TSw=="],
"postcss-media-query-parser": ["postcss-media-query-parser@0.2.3", "", {}, "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig=="],
@@ -1416,7 +1412,7 @@
"uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="],
- "undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
+ "undici": ["undici@8.9.0", "", {}, "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA=="],
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
@@ -1446,7 +1442,7 @@
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
- "whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
+ "whatwg-url": ["whatwg-url@17.1.0", "", { "dependencies": { "@exodus/bytes": "^1.15.1", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -1486,7 +1482,7 @@
"@angular-devkit/core/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
- "@angular-devkit/schematics/@angular-devkit/core": ["@angular-devkit/core@22.0.8", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" } }, "sha512-Hnr4SCkxQM+xtq4uERC09qwTZpt97t4CwDZYzv/HbQA4pMYDfTxvIRHCyl+UHf4NbkC7ZVgyiab+KzTfuc609Q=="],
+ "@angular-devkit/schematics/@angular-devkit/core": ["@angular-devkit/core@22.0.9", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, "peerDependencies": { "chokidar": "^5.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-u53kltrh1ZKHSqGfEDbPW2RzSC1WBPpan3LN61TuzjAkiHqqaQXrN6EmUvYkX3CIT5PKhO6RPRsptMH9y7hs0w=="],
"@angular-eslint/schematics/@angular-devkit/schematics": ["@angular-devkit/schematics@22.0.7", "", { "dependencies": { "@angular-devkit/core": "22.0.7", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.4.0", "rxjs": "7.8.2" } }, "sha512-bKgnBB0LPAj44uVXfW0UO1rQBb3HGXDZxa1bLtESr/KCK4j5iiaXlHqvJjc/z0em1Ds9WNQOfNXjV3IdJo9sSw=="],
@@ -1494,11 +1490,11 @@
"@angular-eslint/schematics/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
- "@angular/build/@angular-devkit/architect": ["@angular-devkit/architect@0.2200.8", "", { "dependencies": { "@angular-devkit/core": "22.0.8", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" } }, "sha512-LFKK2v96nO0ESM9nftlCkTy+O/66AxNkwUpG+Q1er7LLNR2S8UReZnQ1/RhMkldCWxsmp2rQzdgHWnubaY/ZfA=="],
+ "@angular/build/@angular-devkit/architect": ["@angular-devkit/architect@0.2200.9", "", { "dependencies": { "@angular-devkit/core": "22.0.9", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" } }, "sha512-kwiyEFfJsnJ4o7ondW8KswwJbp+qri5yWLS0wMMG+uBwOolZfrQReorqsQNCALhwmojhDNZc0wEWXEHEhKnotw=="],
- "@angular/cli/@angular-devkit/architect": ["@angular-devkit/architect@0.2200.8", "", { "dependencies": { "@angular-devkit/core": "22.0.8", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" } }, "sha512-LFKK2v96nO0ESM9nftlCkTy+O/66AxNkwUpG+Q1er7LLNR2S8UReZnQ1/RhMkldCWxsmp2rQzdgHWnubaY/ZfA=="],
+ "@angular/cli/@angular-devkit/architect": ["@angular-devkit/architect@0.2200.9", "", { "dependencies": { "@angular-devkit/core": "22.0.9", "rxjs": "7.8.2" }, "bin": { "architect": "bin/cli.js" } }, "sha512-kwiyEFfJsnJ4o7ondW8KswwJbp+qri5yWLS0wMMG+uBwOolZfrQReorqsQNCALhwmojhDNZc0wEWXEHEhKnotw=="],
- "@angular/cli/@angular-devkit/core": ["@angular-devkit/core@22.0.8", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" } }, "sha512-Hnr4SCkxQM+xtq4uERC09qwTZpt97t4CwDZYzv/HbQA4pMYDfTxvIRHCyl+UHf4NbkC7ZVgyiab+KzTfuc609Q=="],
+ "@angular/cli/@angular-devkit/core": ["@angular-devkit/core@22.0.9", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, "peerDependencies": { "chokidar": "^5.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-u53kltrh1ZKHSqGfEDbPW2RzSC1WBPpan3LN61TuzjAkiHqqaQXrN6EmUvYkX3CIT5PKhO6RPRsptMH9y7hs0w=="],
"@angular/compiler-cli/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
@@ -1550,7 +1546,7 @@
"@rolldown/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw=="],
- "@schematics/angular/@angular-devkit/core": ["@angular-devkit/core@22.0.8", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" } }, "sha512-Hnr4SCkxQM+xtq4uERC09qwTZpt97t4CwDZYzv/HbQA4pMYDfTxvIRHCyl+UHf4NbkC7ZVgyiab+KzTfuc609Q=="],
+ "@schematics/angular/@angular-devkit/core": ["@angular-devkit/core@22.0.9", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, "peerDependencies": { "chokidar": "^5.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-u53kltrh1ZKHSqGfEDbPW2RzSC1WBPpan3LN61TuzjAkiHqqaQXrN6EmUvYkX3CIT5PKhO6RPRsptMH9y7hs0w=="],
"@tailwindcss/postcss/postcss": ["postcss@8.5.19", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-Mz8SaolMd8nB+G13WkORcxQKHZ/NE4xXevtkJHVuG+guo9/wYKlIMTKAqGdEmYOXR2ijPjTYNHssizdaVSUNdQ=="],
@@ -1604,6 +1600,8 @@
"cosmiconfig-typescript-loader/jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="],
+ "data-urls/whatwg-url": ["whatwg-url@16.0.1", "", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
+
"dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"handlebars/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
@@ -1678,7 +1676,7 @@
"@angular-devkit/schematics/@angular-devkit/core/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
- "@angular/build/@angular-devkit/architect/@angular-devkit/core": ["@angular-devkit/core@22.0.8", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" } }, "sha512-Hnr4SCkxQM+xtq4uERC09qwTZpt97t4CwDZYzv/HbQA4pMYDfTxvIRHCyl+UHf4NbkC7ZVgyiab+KzTfuc609Q=="],
+ "@angular/build/@angular-devkit/architect/@angular-devkit/core": ["@angular-devkit/core@22.0.9", "", { "dependencies": { "ajv": "8.20.0", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.4", "rxjs": "7.8.2", "source-map": "0.7.6" }, "peerDependencies": { "chokidar": "^5.0.0" }, "optionalPeers": ["chokidar"] }, "sha512-u53kltrh1ZKHSqGfEDbPW2RzSC1WBPpan3LN61TuzjAkiHqqaQXrN6EmUvYkX3CIT5PKhO6RPRsptMH9y7hs0w=="],
"@angular/cli/@angular-devkit/core/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
diff --git a/package.json b/package.json
index 457b997..0f66647 100644
--- a/package.json
+++ b/package.json
@@ -52,9 +52,9 @@
},
"devDependencies": {
"@angular-eslint/builder": "22.1.0",
- "@angular/build": "^22.0.8",
+ "@angular/build": "^22.0.9",
"@angular/cdk": "^22.0.6",
- "@angular/cli": "^22.0.8",
+ "@angular/cli": "^22.0.9",
"@angular/common": "^22.0.8",
"@angular/compiler": "^22.0.8",
"@angular/compiler-cli": "^22.0.8",
@@ -64,7 +64,7 @@
"@angular/platform-browser": "^22.0.8",
"@angular/platform-server": "^22.0.8",
"@angular/router": "^22.0.8",
- "@angular/ssr": "^22.0.8",
+ "@angular/ssr": "^22.0.9",
"@commitlint/cli": "21.2.1",
"@commitlint/config-angular": "21.2.0",
"@commitlint/config-conventional": "21.2.0",
@@ -78,14 +78,14 @@
"axe-core": "^4.12.1",
"eslint": "^10.8.0",
"fs-extra": "^11.4.0",
- "globals": "^17.7.0",
+ "globals": "^17.8.0",
"husky": "^9.1.7",
- "jsdom": "^29.1.1",
- "lint-staged": "^17.0.8",
+ "jsdom": "^30.0.1",
+ "lint-staged": "^17.2.0",
"marked": "^18.0.7",
"ngx-markdown": "^22.0.0",
"ngx-scrolltop": "^22.0.0",
- "postcss": "^8.5.23",
+ "postcss": "^8.5.24",
"prettier": "^3.9.6",
"rimraf": "^6.1.3",
"rxjs": "^7.8.2",
diff --git a/projects/portal/public/sitemap.xml b/projects/portal/public/sitemap.xml
index 30c9ca1..add2443 100644
--- a/projects/portal/public/sitemap.xml
+++ b/projects/portal/public/sitemap.xml
@@ -8,6 +8,7 @@
https://celtian.github.io/quick-commitlint/docs/presets/
https://celtian.github.io/quick-commitlint/docs/rules/
https://celtian.github.io/quick-commitlint/docs/cli/
+ https://celtian.github.io/quick-commitlint/docs/performance/
https://celtian.github.io/quick-commitlint/docs/compatibility/
https://celtian.github.io/quick-commitlint/docs/development/
diff --git a/projects/portal/src/app/docs/content/compatibility.md b/projects/portal/src/app/docs/content/compatibility.md
index d49a2a3..b8c1bef 100644
--- a/projects/portal/src/app/docs/content/compatibility.md
+++ b/projects/portal/src/app/docs/content/compatibility.md
@@ -1,33 +1,43 @@
# Commitlint compatibility
-Quick Commitlint is a focused native implementation of a tested subset of commitlint behavior. It is designed for projects that want the included Conventional or Angular rules without loading commitlint's JavaScript configuration and rule stack for every commit.
+Quick Commitlint is a focused native implementation of a tested subset of Commitlint behavior. It is designed for projects that want the included Conventional or Angular rules without loading Commitlint's JavaScript configuration and rule stack for every commit.
-It is not a drop-in replacement for every commitlint configuration.
+The comparison baseline is `@commitlint/cli` 21.2.1 with `@commitlint/config-conventional` 21.2.0 and `@commitlint/config-angular` 21.2.0, matching the packages pinned by this repository. Consult Commitlint's official [CLI](https://commitlint.js.org/reference/cli.html), [configuration](https://commitlint.js.org/reference/configuration.html), and [plugin](https://commitlint.js.org/reference/plugins.html) references for its complete feature set.
-## Supported behavior
+Quick Commitlint is not a drop-in replacement for every Commitlint configuration.
-- Built-in `conventional` and `angular` presets matching the local commitlint 21.2 reference packages
-- The 14 rules listed in the [Rules reference](docs/rules/)
-- Commitlint-style severity, condition, and value tuples
-- Conventional header, body, footer, scope, subject, and breaking-marker checks
-- JSON rule overrides layered on a preset
-- Warning-only success and rule-error failure statuses
+## At a glance
+
+| Area | Quick Commitlint | Commitlint |
+| --------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
+| Runtime | Small Node.js launcher selects a bundled native Zig linting executable; the package has no runtime npm dependencies | Node.js CLI with JavaScript packages for loading configuration, parsing, formatting, and linting |
+| Default configuration | Uses the built-in `conventional` preset when no configuration is found | Normally loads discovered or supplied rules; `--default-config` can provide a Conventional fallback |
+| Configuration files | Strict `.quick-commitlint.json` JSON | JS, TS, JSON, YAML, extensionless, and `package.json` or `package.yaml` configuration |
+| Presets | Exactly `conventional` and `angular`, embedded in the executable | Shareable npm or local configurations composed through `extends` |
+| Rules | Fixed 14-rule subset with static JSON values | Larger built-in rule set plus plugins, local rules, and function or promise values |
+| Parser | Fixed native parser | Configurable parser presets and parser options |
+| Ignores | Lints every supplied message | Provides default generated-message ignores and custom ignore functions |
+| Inputs | Standard input or one commit-message file | Standard input, edit or environment files, the last commit, Git ranges, and ranges from the last tag |
+| Output | Colored lint report on stderr; help and version on stdout | Configurable colors, formatters, quiet and verbose modes, help URLs, and strict warning/error exit codes |
+| Platforms | Bundled for macOS arm64/x64, Linux arm64/x64, and Windows x64 | Runs where its supported Node.js and Git versions are available |
+
+## Conventional and Angular presets
-The repository runs differential cases against `@commitlint/cli`, `@commitlint/config-conventional`, and `@commitlint/config-angular` 21.2 to protect the intended preset behavior.
+| Mode | Quick Commitlint | Commitlint reference | Tested compatibility |
+| ------------ | -------------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------------------------------------------- |
+| Conventional | Built-in `conventional` preset, active by default | `@commitlint/config-conventional` 21.2.0 | Supported preset defaults and diagnostics are compared through `@commitlint/cli` 21.2.1 |
+| Angular | Built-in `angular` preset, selected with `"preset": "angular"` | `@commitlint/config-angular` 21.2.0 | Supported preset defaults and diagnostics are compared through `@commitlint/cli` 21.2.1 |
-## Deliberate differences
+The differential corpus currently exercises 66 Conventional and Angular cases. It checks pass/fail behavior and expected diagnostics for the supported preset rules. This protects the intended compatibility boundary; it does not claim parity for Commitlint rules or features that Quick Commitlint does not implement.
-| Area | Quick Commitlint | Full commitlint |
-| --------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------- |
-| Runtime | Small Node.js launcher plus a native Zig linting executable | Node.js |
-| Default configuration | Built-in `conventional` when no config is found | Normally requires a discovered or supplied configuration |
-| Configuration files | Strict `.quick-commitlint.json` JSON | Multiple JS/TS/JSON/YAML formats through its configuration stack |
-| Presets | Exactly `conventional` and `angular` | Shareable npm configurations and `extends` |
-| Rules | Fixed 14-rule subset | Larger rule set plus plugins and local rules |
-| Rule values | Static JSON values | Values may be supplied by JavaScript functions and promises |
-| Parser | Fixed native parser | Configurable parser presets and parser options |
-| Ignores | Every message is linted | Default and custom ignore functions are available |
-| Output | Colored report is always written to stderr; help/version use stdout | More output and formatting controls |
+Within that boundary, Quick Commitlint supports:
+
+- The 14-rule union listed in the [Rules reference](docs/rules/)
+- The complete embedded defaults documented in the [Presets reference](docs/presets/)
+- Commitlint-style severity, condition, and value tuples
+- Conventional header, body, footer, scope, subject, and breaking-marker checks
+- JSON rule overrides layered on either preset
+- Warning-only success and rule-error failure statuses
## Configuration features not supported
@@ -46,13 +56,15 @@ Quick Commitlint does not implement:
Adding any unknown top-level key or rule is an error rather than being silently ignored.
-## Parser differences
+## Parser and ignore differences
The native parser expects an exact `: ` header separator and an ASCII alphanumeric or underscore type. It does not provide custom header patterns, custom scope delimiters, or multiple-scope parsing. Only a recognized final paragraph becomes the footer.
Generated commit messages—merge, revert, fixup, tag, and initial messages—are linted. If a workflow wants to ignore them, it must avoid invoking Quick Commitlint for those messages or preprocess the input before calling it.
-## Choosing Quick Commitlint
+For measured Conventional and Angular cold-start numbers, methodology, and reproduction steps, see the dedicated [Performance](docs/performance/) page.
+
+## Choosing between them
Quick Commitlint is a good fit when:
@@ -61,4 +73,4 @@ Quick Commitlint is a good fit when:
- fast native linting and no additional runtime npm dependencies matter;
- macOS arm64/x64, Linux arm64/x64, or Windows x64 is the deployment platform.
-Use full commitlint when the project depends on shareable npm configurations, plugins, custom parsers, custom ignores, or rules outside the supported subset.
+Use Commitlint when the project depends on shareable npm configurations, plugins, custom parsers, custom ignores, Git-range linting, or rules outside the supported subset.
diff --git a/projects/portal/src/app/docs/content/development.md b/projects/portal/src/app/docs/content/development.md
index 8358ed8..51a69cd 100644
--- a/projects/portal/src/app/docs/content/development.md
+++ b/projects/portal/src/app/docs/content/development.md
@@ -25,12 +25,16 @@ bun run package
## Benchmark
-Run the cold-process benchmark against `@commitlint/cli`:
+Run the cold-process benchmark against `@commitlint/cli` with both the Conventional and Angular presets:
```bash
bun run benchmark
```
+The script reports the environment, median wall-clock time for each tool, and the relative improvement after three warmups and 40 measured launches per command by default. Set `BENCHMARK_ITERATIONS` to change the measured sample count.
+
+The current reference results and methodology are published on the [Performance](docs/performance/) page.
+
The documentation portal is an Angular 22 static site. Run it locally with `bun run portal:start` and validate it with `bun run portal:validate`.
The pre-commit hook runs ESLint and Prettier fixes on staged TypeScript and Angular template files. Run `bun run validate` before handoff because the hook does not run the full test and build suite.
diff --git a/projects/portal/src/app/docs/content/performance.md b/projects/portal/src/app/docs/content/performance.md
new file mode 100644
index 0000000..c430ca4
--- /dev/null
+++ b/projects/portal/src/app/docs/content/performance.md
@@ -0,0 +1,41 @@
+# Performance
+
+Quick Commitlint runs message parsing, JSON configuration, and lint rules in a bundled native Zig executable. The npm command uses a small Node.js launcher to select that executable. Commitlint loads its Node.js configuration and linting stack in the CLI process.
+
+## Reference cold-process benchmark
+
+| Preset | Quick Commitlint median | Commitlint median | Improvement |
+| ------------ | ----------------------: | ----------------: | ----------: |
+| Conventional | 20.686 ms | 250.597 ms | 12.1Ă— |
+| Angular | 20.535 ms | 250.228 ms | 12.2Ă— |
+
+## Method
+
+The table contains median wall-clock results from 40 measured launches after 3 warmups on:
+
+- Linux x64 under WSL2
+- 13th Gen Intel Core i9-13900H
+- Node.js 24.18.0
+- `@commitlint/cli` 21.2.1
+- `@commitlint/config-conventional` and `@commitlint/config-angular` 21.2.0
+
+Each command received the same valid message through standard input. Quick Commitlint used its built-in default for Conventional and a strict JSON file for Angular. Commitlint extended the corresponding configuration package.
+
+This benchmark measures complete cold-process startup and one lint operation. It does not measure long-running API throughput. Results vary with the machine, operating system, filesystem, and process state, so treat the numbers as a reference run rather than a performance guarantee.
+
+## Reproduce the benchmark
+
+Build the release package, then run the benchmark:
+
+```bash
+bun run package
+bun run benchmark
+```
+
+The command reports the environment, median time for each tool, and the relative improvement for both presets. Set `BENCHMARK_ITERATIONS` to change the measured sample count:
+
+```bash
+BENCHMARK_ITERATIONS=100 bun run benchmark
+```
+
+See [Commitlint compatibility](docs/compatibility/) for differences in configuration, extensibility, parsing, ignores, and input modes.
diff --git a/projects/portal/src/app/docs/document-registry.spec.ts b/projects/portal/src/app/docs/document-registry.spec.ts
index be1ae96..dfd7b52 100644
--- a/projects/portal/src/app/docs/document-registry.spec.ts
+++ b/projects/portal/src/app/docs/document-registry.spec.ts
@@ -61,4 +61,21 @@ describe('document registry', () => {
expect(presetMarkdown).toContain('`conventional`');
expect(presetMarkdown).toContain('`angular`');
});
+
+ it('identifies the commitlint CLI and both reference preset versions', () => {
+ const compatibilityMarkdown = markdownFor('compatibility');
+ const performanceMarkdown = markdownFor('performance');
+
+ expect(compatibilityMarkdown).toContain('`@commitlint/cli` 21.2.1');
+ expect(compatibilityMarkdown).toContain('`@commitlint/config-conventional` 21.2.0');
+ expect(compatibilityMarkdown).toContain('`@commitlint/config-angular` 21.2.0');
+ expect(compatibilityMarkdown).toContain('[Performance](docs/performance/)');
+ expect(performanceMarkdown).toContain('## Reference cold-process benchmark');
+ expect(performanceMarkdown).toMatch(
+ /\|\s+Conventional\s+\|\s+\d+\.\d{3} ms\s+\|\s+\d+\.\d{3} ms\s+\|\s+\d+\.\dĂ—\s+\|/u,
+ );
+ expect(performanceMarkdown).toMatch(
+ /\|\s+Angular\s+\|\s+\d+\.\d{3} ms\s+\|\s+\d+\.\d{3} ms\s+\|\s+\d+\.\dĂ—\s+\|/u,
+ );
+ });
});
diff --git a/projects/portal/src/app/docs/document-registry.ts b/projects/portal/src/app/docs/document-registry.ts
index 369576f..95690dc 100644
--- a/projects/portal/src/app/docs/document-registry.ts
+++ b/projects/portal/src/app/docs/document-registry.ts
@@ -82,6 +82,16 @@ export const DOCUMENTS = [
heading: 'CLI reference',
load: () => import('./content/cli.md').then((module) => module.default),
},
+ {
+ path: 'performance',
+ label: 'Performance',
+ group: 'Reference',
+ title: 'Performance | Quick Commitlint',
+ description:
+ 'Compare Quick Commitlint and Commitlint cold-process performance for Conventional and Angular presets.',
+ heading: 'Performance',
+ load: () => import('./content/performance.md').then((module) => module.default),
+ },
{
path: 'compatibility',
label: 'Commitlint compatibility',
diff --git a/projects/portal/tsconfig.app.json b/projects/portal/tsconfig.app.json
index 6c6f9b1..63c60ed 100644
--- a/projects/portal/tsconfig.app.json
+++ b/projects/portal/tsconfig.app.json
@@ -4,6 +4,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/app",
+ "rootDir": "src",
"types": ["node"]
},
"include": ["src/**/*.ts"],
diff --git a/projects/portal/tsconfig.spec.json b/projects/portal/tsconfig.spec.json
index 48fcc2f..7e245b7 100644
--- a/projects/portal/tsconfig.spec.json
+++ b/projects/portal/tsconfig.spec.json
@@ -4,6 +4,7 @@
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../out-tsc/spec",
+ "rootDir": "src/app",
"types": ["vitest/globals"]
},
"include": ["src/**/*.d.ts", "src/**/*.spec.ts"]
diff --git a/scripts/benchmark.ts b/scripts/benchmark.ts
index 71a62b6..d24f9bd 100644
--- a/scripts/benchmark.ts
+++ b/scripts/benchmark.ts
@@ -1,6 +1,6 @@
import { spawnSync } from 'child_process';
import { writeFileSync, mkdtempSync, rmSync } from 'fs';
-import { tmpdir } from 'os';
+import { arch, cpus, platform, tmpdir } from 'os';
import { join, resolve } from 'path';
const root = resolve(__dirname, '..');
@@ -9,12 +9,15 @@ const commitlint = resolve(root, 'node_modules', '@commitlint', 'cli', 'cli.js')
const iterations = Number(process.env.BENCHMARK_ITERATIONS ?? 40);
const temp = mkdtempSync(join(tmpdir(), 'quick-commitlint-benchmark-'));
const messagePath = join(temp, 'COMMIT_EDITMSG');
-const configPath = join(temp, 'quick-commitlint.json');
-writeFileSync(messagePath, 'feat(benchmark): measure native startup\n');
+const conventionalConfigPath = join(temp, 'quick-commitlint-conventional.json');
+const angularConfigPath = join(temp, 'quick-commitlint-angular.json');
+const message = 'feat(benchmark): measure native startup';
+writeFileSync(messagePath, `${message}\n`);
writeFileSync(
- configPath,
+ conventionalConfigPath,
'{"preset":"conventional","rules":{"header-max-length":[2,"always",100]}}\n',
);
+writeFileSync(angularConfigPath, '{"preset":"angular"}\n');
function median(values: number[]): number {
const sorted = [...values].sort((a, b) => a - b);
@@ -38,25 +41,53 @@ function measure(command: string, args: string[], input?: string): number {
}
try {
+ const nodeVersionResult = spawnSync('node', ['--version'], { encoding: 'utf8' });
+ if (nodeVersionResult.status !== 0) throw new Error('Could not determine the Node.js version.');
+ const cpu = cpus()[0]?.model ?? 'unknown CPU';
const packagedFile = measure('node', [packaged, messagePath]);
- const packagedStdin = measure('node', [packaged], 'feat(benchmark): measure native startup');
- const packagedConfig = measure(
+ const quickConventional = measure('node', [packaged], message);
+ const quickConventionalConfig = measure(
'node',
- [packaged, '--config', configPath],
- 'feat(benchmark): measure native startup',
+ [packaged, '--config', conventionalConfigPath],
+ message,
);
- const nodeStdin = measure(
+ const commitlintConventional = measure(
'node',
[commitlint, '--extends', '@commitlint/config-conventional'],
- 'feat(benchmark): measure native startup',
+ message,
);
- const ratio = nodeStdin / packagedStdin;
+ const quickAngular = measure('node', [packaged, '--config', angularConfigPath], message);
+ const commitlintAngular = measure(
+ 'node',
+ [commitlint, '--extends', '@commitlint/config-angular'],
+ message,
+ );
+ const conventionalRatio = commitlintConventional / quickConventional;
+ const angularRatio = commitlintAngular / quickAngular;
+
+ console.log(
+ `environment: ${platform()} ${arch()}, ${cpu}, Node ${nodeVersionResult.stdout.trim()}`,
+ );
+ console.log(`measured iterations per command: ${iterations} after 3 warmups`);
console.log(`quick-commitlint file median: ${packagedFile.toFixed(3)} ms`);
- console.log(`quick-commitlint stdin median: ${packagedStdin.toFixed(3)} ms`);
- console.log(`quick-commitlint JSON config median: ${packagedConfig.toFixed(3)} ms`);
- console.log(`commitlint stdin median: ${nodeStdin.toFixed(3)} ms`);
- console.log(`cold-process improvement: ${ratio.toFixed(1)}x`);
- if (ratio < 10) throw new Error(`Performance gate failed: ${ratio.toFixed(1)}x is below 10x.`);
+ console.log(`quick-commitlint conventional median: ${quickConventional.toFixed(3)} ms`);
+ console.log(
+ `quick-commitlint conventional JSON config median: ${quickConventionalConfig.toFixed(3)} ms`,
+ );
+ console.log(`commitlint conventional median: ${commitlintConventional.toFixed(3)} ms`);
+ console.log(`conventional cold-process improvement: ${conventionalRatio.toFixed(1)}x`);
+ console.log(`quick-commitlint angular median: ${quickAngular.toFixed(3)} ms`);
+ console.log(`commitlint angular median: ${commitlintAngular.toFixed(3)} ms`);
+ console.log(`angular cold-process improvement: ${angularRatio.toFixed(1)}x`);
+
+ for (const [preset, ratio] of [
+ ['conventional', conventionalRatio],
+ ['angular', angularRatio],
+ ] as const) {
+ if (ratio < 10) {
+ throw new Error(`Performance gate failed for ${preset}: ${ratio.toFixed(1)}x is below 10x.`);
+ }
+ }
} finally {
rmSync(temp, { recursive: true, force: true });
}
diff --git a/scripts/verify-portal-ssg.ts b/scripts/verify-portal-ssg.ts
index 99b867a..6fa2e79 100644
--- a/scripts/verify-portal-ssg.ts
+++ b/scripts/verify-portal-ssg.ts
@@ -64,6 +64,13 @@ const pages: readonly ExpectedPage[] = [
title: 'Rules | Quick Commitlint',
description: 'Reference the rules supported by Quick Commitlint and its built-in presets.',
},
+ {
+ route: 'docs/performance',
+ heading: 'Performance',
+ title: 'Performance | Quick Commitlint',
+ description:
+ 'Compare Quick Commitlint and Commitlint cold-process performance for Conventional and Angular presets.',
+ },
{
route: 'docs/compatibility',
heading: 'Commitlint compatibility',
@@ -148,6 +155,14 @@ async function main(): Promise {
);
assert.ok(presetReference.includes('Angular'), 'preset reference must document Angular');
+ const performanceReference = renderedPages.get('docs/performance') ?? '';
+ for (const result of ['20.686 ms', '250.597 ms', '12.1Ă—', '20.535 ms', '250.228 ms', '12.2Ă—']) {
+ assert.ok(
+ performanceReference.includes(result),
+ `performance reference must include ${result}`,
+ );
+ }
+
await readFile(path.join(outputDirectory, 'robots.txt'), 'utf8');
const sitemap = await readFile(path.join(outputDirectory, 'sitemap.xml'), 'utf8');
for (const page of pages.filter((page) => page.route !== 'docs' && page.route !== '404')) {