vanara 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/bin/vanara.js +66 -2
- package/catalog/agents/api-designer/AGENT.md +140 -0
- package/catalog/agents/api-designer/examples/openapi-snippet.yaml +130 -0
- package/catalog/agents/api-designer/examples/review-notes.md +61 -0
- package/catalog/agents/api-designer/references/contract-and-openapi.md +82 -0
- package/catalog/agents/api-designer/references/design-checklist.md +68 -0
- package/catalog/agents/api-designer/references/versioning-and-evolution.md +63 -0
- package/catalog/agents/api-designer/scripts/lint-openapi.mjs +180 -0
- package/catalog/agents/code-reviewer/AGENT.md +168 -0
- package/catalog/agents/code-reviewer/examples/pr-comment-template.md +48 -0
- package/catalog/agents/code-reviewer/examples/sample-review-output.md +38 -0
- package/catalog/agents/code-reviewer/references/review-checklist.md +46 -0
- package/catalog/agents/code-reviewer/references/security-review.md +62 -0
- package/catalog/agents/code-reviewer/references/severity-rubric.md +63 -0
- package/catalog/agents/code-reviewer/scripts/review-guard.mjs +102 -0
- package/catalog/agents/debugger/AGENT.md +112 -0
- package/catalog/agents/debugger/examples/postmortem-template.md +59 -0
- package/catalog/agents/debugger/examples/repro-template.md +50 -0
- package/catalog/agents/debugger/references/bisection.md +63 -0
- package/catalog/agents/debugger/references/debugging-method.md +57 -0
- package/catalog/agents/debugger/references/observability.md +61 -0
- package/catalog/agents/debugger/scripts/parse-stacktrace.mjs +109 -0
- package/catalog/agents/pr-summarizer/AGENT.md +156 -0
- package/catalog/agents/pr-summarizer/examples/sample-pr-summary.md +46 -0
- package/catalog/agents/pr-summarizer/examples/weak-vs-strong-summary.md +68 -0
- package/catalog/agents/pr-summarizer/references/identifying-risk.md +68 -0
- package/catalog/agents/pr-summarizer/references/reading-the-diff.md +68 -0
- package/catalog/agents/pr-summarizer/references/test-plan-and-missing-tests.md +64 -0
- package/catalog/agents/pr-summarizer/references/writing-the-summary.md +63 -0
- package/catalog/agents/pr-summarizer/scripts/diff-risk.mjs +111 -0
- package/catalog/agents/refactoring-specialist/AGENT.md +185 -0
- package/catalog/agents/refactoring-specialist/examples/before-after.md +91 -0
- package/catalog/agents/refactoring-specialist/examples/refactor-plan.md +97 -0
- package/catalog/agents/refactoring-specialist/references/large-scale-refactor.md +50 -0
- package/catalog/agents/refactoring-specialist/references/safe-workflow.md +75 -0
- package/catalog/agents/refactoring-specialist/references/smell-catalog.md +76 -0
- package/catalog/agents/refactoring-specialist/scripts/detect-smells.mjs +150 -0
- package/catalog/agents/security-auditor/AGENT.md +168 -0
- package/catalog/agents/security-auditor/examples/audit-report.md +68 -0
- package/catalog/agents/security-auditor/examples/finding-template.md +31 -0
- package/catalog/agents/security-auditor/references/audit-workflow.md +116 -0
- package/catalog/agents/security-auditor/references/severity-and-reporting.md +56 -0
- package/catalog/agents/security-auditor/references/vuln-classes.md +129 -0
- package/catalog/agents/security-auditor/scripts/scan-secrets.mjs +86 -0
- package/catalog/agents/technical-writer/AGENT.md +163 -0
- package/catalog/agents/technical-writer/examples/how-to-example.md +76 -0
- package/catalog/agents/technical-writer/examples/reference-example.md +75 -0
- package/catalog/agents/technical-writer/references/clarity-and-style.md +52 -0
- package/catalog/agents/technical-writer/references/diataxis-and-doc-types.md +61 -0
- package/catalog/agents/technical-writer/references/structure-and-maintenance.md +59 -0
- package/catalog/agents/technical-writer/scripts/readability.mjs +103 -0
- package/catalog/agents/test-author/AGENT.md +177 -0
- package/catalog/agents/test-author/examples/test-plan.md +54 -0
- package/catalog/agents/test-author/examples/unit-test-example.ts +55 -0
- package/catalog/agents/test-author/references/aaa-and-naming.md +75 -0
- package/catalog/agents/test-author/references/mocking-and-fixtures.md +94 -0
- package/catalog/agents/test-author/references/test-types-and-pyramid.md +60 -0
- package/catalog/agents/test-author/scripts/check-coverage.mjs +145 -0
- package/catalog/agents/threat-modeler/AGENT.md +163 -0
- package/catalog/agents/threat-modeler/examples/dfd-example.md +55 -0
- package/catalog/agents/threat-modeler/examples/threat-model.md +81 -0
- package/catalog/agents/threat-modeler/references/dfd-and-trust-boundaries.md +76 -0
- package/catalog/agents/threat-modeler/references/mitigation-catalog.md +77 -0
- package/catalog/agents/threat-modeler/references/stride-method.md +68 -0
- package/catalog/agents/threat-modeler/scripts/stride-checklist.mjs +183 -0
- package/catalog/agents/vuln-scanner/AGENT.md +146 -0
- package/catalog/agents/vuln-scanner/examples/finding-template.md +57 -0
- package/catalog/agents/vuln-scanner/examples/scan-report.md +49 -0
- package/catalog/agents/vuln-scanner/references/remediation-and-severity.md +68 -0
- package/catalog/agents/vuln-scanner/references/scan-types-and-tools.md +83 -0
- package/catalog/agents/vuln-scanner/references/triage-and-false-positives.md +73 -0
- package/catalog/agents/vuln-scanner/scripts/parse-scan-results.mjs +133 -0
- package/catalog/packs/code-review-pack/PACK.md +100 -0
- package/catalog/packs/code-review-pack/examples/pr-flow-example.md +50 -0
- package/catalog/packs/code-review-pack/references/rollout-guide.md +40 -0
- package/catalog/packs/security-pack/PACK.md +128 -0
- package/catalog/packs/security-pack/examples/owasp-vuln-triage-and-fix.md +66 -0
- package/catalog/packs/security-pack/examples/threat-model-then-audit.md +77 -0
- package/catalog/packs/security-pack/references/choosing-the-right-tool.md +37 -0
- package/catalog/packs/security-pack/references/threat-model-to-remediation.md +69 -0
- package/catalog/packs/security-pack/references/workflow.md +56 -0
- package/catalog/skills/api-pagination/SKILL.md +83 -0
- package/catalog/skills/api-pagination/examples/keyset-queries.sql +38 -0
- package/catalog/skills/api-pagination/examples/response-shapes.md +51 -0
- package/catalog/skills/api-pagination/references/consuming-pages.md +39 -0
- package/catalog/skills/api-pagination/references/counting-and-totals.md +41 -0
- package/catalog/skills/api-pagination/references/cursor-encoding.md +34 -0
- package/catalog/skills/api-pagination/scripts/check-cursor.mjs +51 -0
- package/catalog/skills/caching-strategies/SKILL.md +180 -0
- package/catalog/skills/caching-strategies/examples/http-cache-headers.md +82 -0
- package/catalog/skills/caching-strategies/examples/redis-cache-aside.js +110 -0
- package/catalog/skills/caching-strategies/references/cache-patterns.md +93 -0
- package/catalog/skills/caching-strategies/references/eviction-and-ttl.md +67 -0
- package/catalog/skills/caching-strategies/references/invalidation-and-stampede.md +99 -0
- package/catalog/skills/caching-strategies/scripts/ttl-jitter.mjs +99 -0
- package/catalog/skills/conventional-commits/SKILL.md +135 -0
- package/catalog/skills/conventional-commits/examples/commit-examples.md +60 -0
- package/catalog/skills/conventional-commits/examples/commitlint.config.js +31 -0
- package/catalog/skills/conventional-commits/references/breaking-changes-semver.md +50 -0
- package/catalog/skills/conventional-commits/references/scoping-commits.md +42 -0
- package/catalog/skills/conventional-commits/references/spec.md +78 -0
- package/catalog/skills/conventional-commits/scripts/lint-commit.mjs +97 -0
- package/catalog/skills/database-migrations/SKILL.md +137 -0
- package/catalog/skills/database-migrations/examples/add-column-safe.sql +34 -0
- package/catalog/skills/database-migrations/examples/migration-config.example.json +27 -0
- package/catalog/skills/database-migrations/references/expand-contract.md +74 -0
- package/catalog/skills/database-migrations/references/rollback-and-safety.md +71 -0
- package/catalog/skills/database-migrations/references/zero-downtime-changes.md +84 -0
- package/catalog/skills/database-migrations/scripts/check-migration-reversible.mjs +139 -0
- package/catalog/skills/error-handling-patterns/SKILL.md +136 -0
- package/catalog/skills/error-handling-patterns/examples/express-error-middleware.js +93 -0
- package/catalog/skills/error-handling-patterns/examples/result-pattern.ts +81 -0
- package/catalog/skills/error-handling-patterns/references/error-taxonomy.md +91 -0
- package/catalog/skills/error-handling-patterns/references/language-patterns.md +102 -0
- package/catalog/skills/error-handling-patterns/references/retry-and-backoff.md +76 -0
- package/catalog/skills/error-handling-patterns/scripts/lint-empty-catch.mjs +113 -0
- package/catalog/skills/git-collaboration-workflows/SKILL.md +104 -0
- package/catalog/skills/git-collaboration-workflows/examples/branch-protection.md +48 -0
- package/catalog/skills/git-collaboration-workflows/examples/pr-review-playbook.md +48 -0
- package/catalog/skills/git-collaboration-workflows/references/branching-models.md +52 -0
- package/catalog/skills/git-collaboration-workflows/references/history-hygiene.md +44 -0
- package/catalog/skills/git-collaboration-workflows/scripts/check-branch-hygiene.mjs +62 -0
- package/catalog/skills/owasp-top10/SKILL.md +112 -0
- package/catalog/skills/owasp-top10/examples/sql-injection-fix.md +33 -0
- package/catalog/skills/owasp-top10/examples/xss-fix.md +27 -0
- package/catalog/skills/owasp-top10/references/access-control.md +36 -0
- package/catalog/skills/owasp-top10/references/crypto-failures.md +29 -0
- package/catalog/skills/owasp-top10/references/injection.md +39 -0
- package/catalog/skills/owasp-top10/references/ssrf-and-design.md +35 -0
- package/catalog/skills/owasp-top10/scripts/scan-injection.mjs +73 -0
- package/catalog/skills/prompt-engineering/SKILL.md +100 -0
- package/catalog/skills/prompt-engineering/examples/classification-prompt.md +29 -0
- package/catalog/skills/prompt-engineering/examples/extraction-prompt.md +25 -0
- package/catalog/skills/prompt-engineering/references/prompt-injection.md +34 -0
- package/catalog/skills/prompt-engineering/references/structured-output.md +34 -0
- package/catalog/skills/prompt-engineering/references/techniques.md +33 -0
- package/catalog/skills/prompt-engineering/scripts/validate-output.mjs +71 -0
- package/catalog/skills/readme-writing/SKILL.md +176 -0
- package/catalog/skills/readme-writing/examples/README.template.md +73 -0
- package/catalog/skills/readme-writing/examples/good-readme-example.md +95 -0
- package/catalog/skills/readme-writing/references/anatomy.md +107 -0
- package/catalog/skills/readme-writing/references/maintenance.md +68 -0
- package/catalog/skills/readme-writing/references/writing-style.md +79 -0
- package/catalog/skills/readme-writing/scripts/lint-readme.mjs +117 -0
- package/catalog/skills/refactoring-patterns/SKILL.md +178 -0
- package/catalog/skills/refactoring-patterns/examples/extract-function.before-after.js +57 -0
- package/catalog/skills/refactoring-patterns/examples/replace-conditional-with-polymorphism.before-after.ts +63 -0
- package/catalog/skills/refactoring-patterns/references/code-smells.md +92 -0
- package/catalog/skills/refactoring-patterns/references/refactoring-catalog.md +78 -0
- package/catalog/skills/refactoring-patterns/references/safe-workflow.md +74 -0
- package/catalog/skills/refactoring-patterns/scripts/detect-smells.mjs +137 -0
- package/catalog/skills/rest-api-design/SKILL.md +147 -0
- package/catalog/skills/rest-api-design/examples/error-envelope.json +26 -0
- package/catalog/skills/rest-api-design/examples/orders-api.openapi.yaml +106 -0
- package/catalog/skills/rest-api-design/references/error-handling.md +43 -0
- package/catalog/skills/rest-api-design/references/pagination.md +41 -0
- package/catalog/skills/rest-api-design/references/status-codes.md +32 -0
- package/catalog/skills/rest-api-design/references/versioning.md +31 -0
- package/catalog/skills/rest-api-design/scripts/check-envelope.mjs +62 -0
- package/catalog/skills/secrets-management/SKILL.md +134 -0
- package/catalog/skills/secrets-management/examples/env-example.md +41 -0
- package/catalog/skills/secrets-management/examples/rotation-runbook.md +50 -0
- package/catalog/skills/secrets-management/references/leak-response.md +47 -0
- package/catalog/skills/secrets-management/references/rotation.md +46 -0
- package/catalog/skills/secrets-management/references/secret-managers.md +52 -0
- package/catalog/skills/secrets-management/scripts/detect-hardcoded.mjs +96 -0
- package/catalog/skills/secure-auth/SKILL.md +135 -0
- package/catalog/skills/secure-auth/examples/auth-flow.md +60 -0
- package/catalog/skills/secure-auth/examples/jwt-verify.ts +89 -0
- package/catalog/skills/secure-auth/references/auth-attacks.md +76 -0
- package/catalog/skills/secure-auth/references/oauth2-oidc.md +65 -0
- package/catalog/skills/secure-auth/references/password-hashing.md +70 -0
- package/catalog/skills/secure-auth/references/sessions-vs-jwt.md +69 -0
- package/catalog/skills/secure-auth/scripts/check-password-policy.mjs +114 -0
- package/catalog/skills/sql-index-tuning/SKILL.md +171 -0
- package/catalog/skills/sql-index-tuning/examples/explain-walkthrough.sql +91 -0
- package/catalog/skills/sql-index-tuning/examples/index-ddl.sql +67 -0
- package/catalog/skills/sql-index-tuning/references/btree-internals.md +58 -0
- package/catalog/skills/sql-index-tuning/references/composite-and-covering.md +60 -0
- package/catalog/skills/sql-index-tuning/references/reading-explain.md +54 -0
- package/catalog/skills/sql-index-tuning/references/selectivity-and-cardinality.md +62 -0
- package/catalog/skills/sql-index-tuning/scripts/suggest-index.mjs +213 -0
- package/catalog/skills/test-plan-design/SKILL.md +154 -0
- package/catalog/skills/test-plan-design/examples/test-case-template.md +45 -0
- package/catalog/skills/test-plan-design/examples/test-plan.md +57 -0
- package/catalog/skills/test-plan-design/references/case-design.md +79 -0
- package/catalog/skills/test-plan-design/references/risk-prioritization.md +58 -0
- package/catalog/skills/test-plan-design/references/test-pyramid.md +58 -0
- package/catalog/skills/test-plan-design/scripts/coverage-gaps.mjs +109 -0
- package/free-tier.json +15 -0
- package/package.json +15 -4
- package/premium-index.json +1 -0
- package/src/config.js +31 -1
- package/src/license.js +143 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable helper: compute a jittered TTL so a batch of cache keys never expires
|
|
3
|
+
// in lockstep (the synchronized-expiry stampede). Demonstrates an *executed*,
|
|
4
|
+
// verifiable example asset.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node ttl-jitter.mjs <baseSeconds> [jitterRatio] # print one jittered TTL
|
|
8
|
+
// node ttl-jitter.mjs --selftest # run built-in test cases
|
|
9
|
+
|
|
10
|
+
const MIN_TTL = 1;
|
|
11
|
+
|
|
12
|
+
// Returns an integer TTL within [base*(1-ratio), base*(1+ratio)], floored at MIN_TTL.
|
|
13
|
+
export function jitteredTtl(baseSeconds, jitterRatio = 0.1, rand = Math.random) {
|
|
14
|
+
if (!Number.isFinite(baseSeconds) || baseSeconds <= 0)
|
|
15
|
+
throw new RangeError('baseSeconds must be a positive number');
|
|
16
|
+
if (!Number.isFinite(jitterRatio) || jitterRatio < 0 || jitterRatio > 1)
|
|
17
|
+
throw new RangeError('jitterRatio must be in [0, 1]');
|
|
18
|
+
const delta = baseSeconds * jitterRatio;
|
|
19
|
+
const offset = (rand() * 2 - 1) * delta; // uniform in [-delta, +delta]
|
|
20
|
+
return Math.max(MIN_TTL, Math.round(baseSeconds + offset));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Inclusive bounds the result is guaranteed to fall within.
|
|
24
|
+
export function jitterBounds(baseSeconds, jitterRatio = 0.1) {
|
|
25
|
+
const delta = baseSeconds * jitterRatio;
|
|
26
|
+
return {
|
|
27
|
+
min: Math.max(MIN_TTL, Math.round(baseSeconds - delta)),
|
|
28
|
+
max: Math.max(MIN_TTL, Math.round(baseSeconds + delta)),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function selftest() {
|
|
33
|
+
const cases = [];
|
|
34
|
+
const record = (name, pass, detail = '') => cases.push({ name, pass, detail });
|
|
35
|
+
|
|
36
|
+
// 1. Many samples stay within ±ratio bounds (fuzz with the real RNG).
|
|
37
|
+
{
|
|
38
|
+
const base = 300, ratio = 0.1;
|
|
39
|
+
const { min, max } = jitterBounds(base, ratio);
|
|
40
|
+
let allInBounds = true;
|
|
41
|
+
let sawSpread = new Set();
|
|
42
|
+
for (let i = 0; i < 10000; i++) {
|
|
43
|
+
const t = jitteredTtl(base, ratio);
|
|
44
|
+
if (t < min || t > max) { allInBounds = false; break; }
|
|
45
|
+
sawSpread.add(t);
|
|
46
|
+
}
|
|
47
|
+
record('stays within ±10% bounds over 10k samples', allInBounds, `[${min}, ${max}]`);
|
|
48
|
+
record('produces a spread of values (not constant)', sawSpread.size > 10,
|
|
49
|
+
`${sawSpread.size} distinct values`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Extreme RNG values hit the exact bounds (deterministic injection).
|
|
53
|
+
{
|
|
54
|
+
const lo = jitteredTtl(300, 0.1, () => 0); // rand=0 -> -delta
|
|
55
|
+
const hi = jitteredTtl(300, 0.1, () => 1); // rand=1 -> +delta (1 is exclusive in Math.random, tested as edge)
|
|
56
|
+
record('rand=0 yields the low bound (270)', lo === 270, `got ${lo}`);
|
|
57
|
+
record('rand~1 yields the high bound (330)', hi === 330, `got ${hi}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. Floor at MIN_TTL: tiny base with full jitter never drops below 1.
|
|
61
|
+
{
|
|
62
|
+
let neverBelowMin = true;
|
|
63
|
+
for (let i = 0; i < 1000; i++) {
|
|
64
|
+
if (jitteredTtl(1, 1.0) < MIN_TTL) { neverBelowMin = false; break; }
|
|
65
|
+
}
|
|
66
|
+
record('never returns below MIN_TTL', neverBelowMin, `MIN_TTL=${MIN_TTL}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 4. Zero jitter is a pure passthrough.
|
|
70
|
+
record('jitterRatio=0 returns base unchanged', jitteredTtl(300, 0) === 300,
|
|
71
|
+
`got ${jitteredTtl(300, 0)}`);
|
|
72
|
+
|
|
73
|
+
// 5. Invalid inputs throw.
|
|
74
|
+
const throws = (fn) => { try { fn(); return false; } catch { return true; } };
|
|
75
|
+
record('rejects non-positive base', throws(() => jitteredTtl(0)));
|
|
76
|
+
record('rejects out-of-range ratio', throws(() => jitteredTtl(300, 2)));
|
|
77
|
+
|
|
78
|
+
let allPass = true;
|
|
79
|
+
for (const { name, pass, detail } of cases) {
|
|
80
|
+
if (!pass) allPass = false;
|
|
81
|
+
console.log(`${pass ? '✓' : '✗'} ${name}${detail ? ` (${detail})` : ''}`);
|
|
82
|
+
}
|
|
83
|
+
console.log(allPass ? '\nAll selftests passed.' : '\nSelftests FAILED.');
|
|
84
|
+
process.exit(allPass ? 0 : 1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const arg = process.argv[2];
|
|
88
|
+
if (arg === '--selftest') {
|
|
89
|
+
selftest();
|
|
90
|
+
} else if (arg) {
|
|
91
|
+
const base = Number(arg);
|
|
92
|
+
const ratio = process.argv[3] !== undefined ? Number(process.argv[3]) : 0.1;
|
|
93
|
+
const { min, max } = jitterBounds(base, ratio);
|
|
94
|
+
console.log(`jitteredTtl(${base}, ${ratio}) = ${jitteredTtl(base, ratio)} (range [${min}, ${max}])`);
|
|
95
|
+
} else {
|
|
96
|
+
console.log('Usage: node ttl-jitter.mjs <baseSeconds> [jitterRatio]');
|
|
97
|
+
console.log(' node ttl-jitter.mjs --selftest');
|
|
98
|
+
process.exit(2);
|
|
99
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: conventional-commits
|
|
3
|
+
description: Write Conventional Commits — the type(scope)!: subject + body + footer spec — so history is readable and changelogs and SemVer bumps can be derived automatically. Use when committing, configuring commitlint, designing release tooling, or deciding feat vs fix vs breaking change.
|
|
4
|
+
type: skill
|
|
5
|
+
version: 2.0.0
|
|
6
|
+
updated: 2026-06-29
|
|
7
|
+
---
|
|
8
|
+
# Conventional Commits
|
|
9
|
+
|
|
10
|
+
A commit message is the only documentation guaranteed to travel with a change forever. Conventional
|
|
11
|
+
Commits turn that prose into a **structured, machine-parseable record**: a tool can read your history and
|
|
12
|
+
derive the next version number and a categorized changelog without a human touching either. This skill is
|
|
13
|
+
the deep reference for the spec, the trade-offs, and the failure modes. Heavy detail lives in
|
|
14
|
+
`references/`; copy-paste config in `examples/`; a runnable linter in `scripts/`.
|
|
15
|
+
|
|
16
|
+
## Mental model
|
|
17
|
+
|
|
18
|
+
Every commit answers three questions, and the format maps one-to-one onto them:
|
|
19
|
+
|
|
20
|
+
| Question | Where it lives |
|
|
21
|
+
|---|---|
|
|
22
|
+
| What kind of change? | the **type** (`feat`, `fix`, …) |
|
|
23
|
+
| Where, narrowly? | the optional **scope** (`feat(auth):`) |
|
|
24
|
+
| Does it break callers? | the `!` marker and/or `BREAKING CHANGE:` footer |
|
|
25
|
+
| Why, in prose? | the **body** |
|
|
26
|
+
| What does it reference/close? | the **footer** (`Refs:`, `Closes:`) |
|
|
27
|
+
|
|
28
|
+
The header is for machines and scanners; the body is for the next human. Get the header structurally
|
|
29
|
+
correct and your release tooling does the rest for free.
|
|
30
|
+
|
|
31
|
+
## The spec
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
<type>(<optional scope>)<optional !>: <subject>
|
|
35
|
+
<blank line>
|
|
36
|
+
<optional body — wrapped prose explaining the why, may span paragraphs>
|
|
37
|
+
<blank line>
|
|
38
|
+
<optional footer(s) — BREAKING CHANGE: …, Refs: #123, Closes: #456, Co-Authored-By: …>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Rules that the linter enforces (see `scripts/lint-commit.mjs`):
|
|
42
|
+
|
|
43
|
+
1. **Type** is required, lowercase, from the allowed set below.
|
|
44
|
+
2. **Scope** is optional, in parentheses, a lowercase noun for the affected area (`api`, `auth`, `deps`).
|
|
45
|
+
3. **`!`** before the colon flags a breaking change.
|
|
46
|
+
4. **Subject** follows `: ` (colon-space), is imperative mood, lowercase, no trailing period, and the
|
|
47
|
+
whole header is **≤ 72 characters** (50 is the ideal — it keeps `git log --oneline` and GitHub from
|
|
48
|
+
truncating).
|
|
49
|
+
|
|
50
|
+
The full grammar, footer tokens, and revert/merge conventions are in `references/spec.md`.
|
|
51
|
+
|
|
52
|
+
## Allowed types
|
|
53
|
+
|
|
54
|
+
| Type | Use for | SemVer impact |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `feat` | a new user-facing feature | **MINOR** |
|
|
57
|
+
| `fix` | a bug fix | **PATCH** |
|
|
58
|
+
| `docs` | documentation only | none |
|
|
59
|
+
| `style` | formatting, whitespace, no code change | none |
|
|
60
|
+
| `refactor` | code change that neither fixes a bug nor adds a feature | none |
|
|
61
|
+
| `perf` | a performance improvement | PATCH |
|
|
62
|
+
| `test` | adding or correcting tests | none |
|
|
63
|
+
| `build` | build system or dependencies | none |
|
|
64
|
+
| `ci` | CI configuration and scripts | none |
|
|
65
|
+
| `chore` | maintenance, no production code change | none |
|
|
66
|
+
| `revert` | reverts a previous commit | varies |
|
|
67
|
+
|
|
68
|
+
Any commit with a `!` or `BREAKING CHANGE:` footer is a **MAJOR** bump, regardless of type. Keep the set
|
|
69
|
+
small and team-agreed — inventing per-developer types defeats the automation. See
|
|
70
|
+
`references/breaking-changes-semver.md` for the precise type → version mapping.
|
|
71
|
+
|
|
72
|
+
## Why bother (the automation payoff)
|
|
73
|
+
|
|
74
|
+
The structure is not bureaucracy — it unlocks tooling you'd otherwise hand-maintain:
|
|
75
|
+
|
|
76
|
+
- **Automated versioning:** `semantic-release` / Changesets read the commits since the last tag and pick
|
|
77
|
+
MAJOR / MINOR / PATCH from the types. No more "what should this version be?" debates.
|
|
78
|
+
- **Generated changelogs:** commits group by type into a categorized `CHANGELOG.md` with links to PRs and
|
|
79
|
+
issues, written from the footers.
|
|
80
|
+
- **Scannable history:** filtering by type answers "what features shipped this quarter?" in one command:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
git log --oneline --grep '^feat' v1.4.0..HEAD # every feature since the last release
|
|
84
|
+
git log --oneline --grep 'BREAKING CHANGE' # every breaking change, ever
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- **Reviewable diffs:** one logical change per commit means reviewers and `git bisect` operate on
|
|
88
|
+
coherent units instead of tangled mega-commits.
|
|
89
|
+
|
|
90
|
+
## Scoping commits (one logical change)
|
|
91
|
+
|
|
92
|
+
A perfect message on a tangled commit is still a bad commit. Each commit should be **one coherent change**
|
|
93
|
+
that builds and passes tests on its own — don't mix a refactor with a feature, or a fix with a formatting
|
|
94
|
+
sweep. This makes `git revert`, `git bisect`, and cherry-picks surgical instead of all-or-nothing. The
|
|
95
|
+
discipline of staging hunks (`git add -p`) to separate concerns is covered in `references/scoping-commits.md`.
|
|
96
|
+
|
|
97
|
+
## Common pitfalls and failure modes
|
|
98
|
+
|
|
99
|
+
- **Past tense / capitalized subject** (`Added login`) — the convention is imperative, lowercase: `add
|
|
100
|
+
login`. A useful test: the subject should complete the sentence "If applied, this commit will ___".
|
|
101
|
+
- **A scope that's really a type** (`feat(fix):`) — scope is a *place* (`auth`), not a kind of change.
|
|
102
|
+
- **Forgotten breaking-change marker** — renaming a public field as a plain `refactor:` ships a MAJOR
|
|
103
|
+
break as a no-bump release and silently breaks downstream consumers. Always add `!` + a `BREAKING
|
|
104
|
+
CHANGE:` footer explaining the migration.
|
|
105
|
+
- **Junk-drawer `chore:`** — using `chore` for everything erases the signal. A dependency bump that fixes
|
|
106
|
+
a CVE is a `fix`; a new capability is a `feat`.
|
|
107
|
+
- **Header over 72 chars** — it truncates in `git log --oneline`, GitHub, and changelog output. Move
|
|
108
|
+
detail into the body.
|
|
109
|
+
- **Mega-commits** ("WIP", "fixes") — unparseable by tooling and impossible to revert cleanly.
|
|
110
|
+
- **Enforcing only on the final squash** — if you squash-merge, the *PR title* becomes the commit; lint
|
|
111
|
+
that, not just local commits, or the rule has no teeth.
|
|
112
|
+
|
|
113
|
+
## When NOT to use / trade-offs
|
|
114
|
+
|
|
115
|
+
- **Solo throwaway prototypes** — the changelog/versioning payoff is zero; the ceremony is pure overhead.
|
|
116
|
+
- **Squash-merge-only teams** — per-commit discipline matters less; instead enforce the convention on PR
|
|
117
|
+
titles via a CI check and let local commits be messy.
|
|
118
|
+
- **Non-software repos** (docs sites, infra-as-data) — the type vocabulary often doesn't fit; a lighter
|
|
119
|
+
convention may serve better.
|
|
120
|
+
- **The cost:** it adds friction and requires a `commitlint` gate plus team buy-in to stay consistent.
|
|
121
|
+
Half-adopted, it's worse than nothing because the automation can't trust the data. Adopt it fully (with
|
|
122
|
+
the `commit-msg` hook in `examples/commitlint.config.js`) or not at all.
|
|
123
|
+
|
|
124
|
+
## Files in this package
|
|
125
|
+
|
|
126
|
+
- `references/spec.md` — full grammar: header, body, footers, revert/merge, FAQ
|
|
127
|
+
- `references/breaking-changes-semver.md` — `!` vs `BREAKING CHANGE:` and exact type → SemVer mapping
|
|
128
|
+
- `references/scoping-commits.md` — splitting work into atomic commits with `git add -p`
|
|
129
|
+
- `examples/commit-examples.md` — annotated good and bad messages across every type
|
|
130
|
+
- `examples/commitlint.config.js` — zero-config-friendly commitlint setup + husky `commit-msg` hook
|
|
131
|
+
- `scripts/lint-commit.mjs` — runnable Node linter for a commit header, with `--selftest`
|
|
132
|
+
|
|
133
|
+
Pairs with the `ci-pipeline-design` skill (gate commits in CI), the `changelog-writing` skill (turn the
|
|
134
|
+
history into release notes), and the `code-reviewer` agent. See the
|
|
135
|
+
[Conventional Commits spec](https://www.conventionalcommits.org/) for the canonical wording.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Annotated Commit Examples
|
|
2
|
+
|
|
3
|
+
Copy these shapes. Each shows the header, and where useful, body and footers.
|
|
4
|
+
|
|
5
|
+
## Good
|
|
6
|
+
|
|
7
|
+
A feature with context and an issue reference:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
feat(orders): add cursor pagination to the list endpoint
|
|
11
|
+
|
|
12
|
+
Offset pagination scanned the whole table on deep pages and double-counted
|
|
13
|
+
rows under concurrent inserts. Cursor pagination keeps queries O(limit)
|
|
14
|
+
regardless of depth.
|
|
15
|
+
|
|
16
|
+
Refs: #214
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
A plain bug fix (PATCH):
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
fix(auth): treat an expired token as 401, not 500
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
A breaking change — `!` plus a migration footer (MAJOR):
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
refactor(api)!: drop the deprecated v0 auth header
|
|
29
|
+
|
|
30
|
+
BREAKING CHANGE: clients must send `Authorization: Bearer <token>`.
|
|
31
|
+
The `X-Api-Key` header is no longer read.
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
A performance fix (PATCH) and a docs-only change (no release):
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
perf(search): memoize the analyzer to cut p99 by 40ms
|
|
38
|
+
```
|
|
39
|
+
```
|
|
40
|
+
docs: document the cursor pagination contract
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A scoped dependency bump that closes a security issue — note it's a `fix`, not `chore`:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
fix(deps): bump qs to 6.11.2 to patch CVE-2022-24999
|
|
47
|
+
|
|
48
|
+
Closes: #301
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Bad — and the fix
|
|
52
|
+
|
|
53
|
+
| Bad | Why | Fixed |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `Added user login` | past tense, capitalized | `feat(auth): add user login` |
|
|
56
|
+
| `fix: Fixed the bug.` | capital + trailing period + vague | `fix(cart): reject negative quantities` |
|
|
57
|
+
| `feat(fix): patch thing` | scope is a type, not a place | `fix(thing): handle empty payload` |
|
|
58
|
+
| `chore: rewrite payment retries` | hides a real fix as no-release noise | `fix(payments): retry idempotently on timeout` |
|
|
59
|
+
| `update stuff and refactor and fix tests` | three changes, no type | three separate commits |
|
|
60
|
+
| `feat: implement the entire new dashboard with charts, filters, export, and dark mode toggle` | 80+ char header | shorten subject, push detail to body |
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// commitlint configuration enforcing Conventional Commits.
|
|
2
|
+
//
|
|
3
|
+
// Setup (install dev deps in your own project — this skill ships zero deps):
|
|
4
|
+
// npm i -D @commitlint/cli @commitlint/config-conventional husky
|
|
5
|
+
// npx husky init
|
|
6
|
+
// echo 'npx --no -- commitlint --edit "$1"' > .husky/commit-msg
|
|
7
|
+
//
|
|
8
|
+
// With the commit-msg hook in place, every `git commit` is validated locally.
|
|
9
|
+
// In CI, also lint PR titles for squash-merge workflows (see ci-pipeline-design).
|
|
10
|
+
|
|
11
|
+
/** @type {import('@commitlint/types').UserConfig} */
|
|
12
|
+
module.exports = {
|
|
13
|
+
extends: ['@commitlint/config-conventional'],
|
|
14
|
+
rules: {
|
|
15
|
+
// Restrict types to the agreed set (mirrors this skill's "Allowed types").
|
|
16
|
+
'type-enum': [
|
|
17
|
+
2,
|
|
18
|
+
'always',
|
|
19
|
+
['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert'],
|
|
20
|
+
],
|
|
21
|
+
// Scope is optional, but when present must be one of these known areas.
|
|
22
|
+
// Drop this rule, or extend the list, to match your repo's modules.
|
|
23
|
+
'scope-enum': [2, 'always', ['api', 'web', 'auth', 'orders', 'payments', 'deps', 'ci', 'release']],
|
|
24
|
+
'subject-case': [2, 'never', ['sentence-case', 'start-case', 'pascal-case', 'upper-case']],
|
|
25
|
+
'subject-empty': [2, 'never'],
|
|
26
|
+
'subject-full-stop': [2, 'never', '.'],
|
|
27
|
+
'header-max-length': [2, 'always', 72],
|
|
28
|
+
'body-leading-blank': [2, 'always'],
|
|
29
|
+
'footer-leading-blank': [2, 'always'],
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Breaking Changes and SemVer Mapping
|
|
2
|
+
|
|
3
|
+
Conventional Commits exists to make [Semantic Versioning](https://semver.org/) (`MAJOR.MINOR.PATCH`)
|
|
4
|
+
automatic. This is the exact mapping release tools use.
|
|
5
|
+
|
|
6
|
+
## Type → version bump
|
|
7
|
+
|
|
8
|
+
| Commit | Bump | Example: from `1.4.2` |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| any commit with `!` or `BREAKING CHANGE:` footer | **MAJOR** | → `2.0.0` |
|
|
11
|
+
| `feat:` | **MINOR** | → `1.5.0` |
|
|
12
|
+
| `fix:` | **PATCH** | → `1.4.3` |
|
|
13
|
+
| `perf:` | PATCH | → `1.4.3` |
|
|
14
|
+
| `docs:` / `style:` / `refactor:` / `test:` / `build:` / `ci:` / `chore:` | no release | `1.4.2` |
|
|
15
|
+
|
|
16
|
+
The highest-priority change across all commits since the last tag wins: one `feat` among twenty `chore`s
|
|
17
|
+
still produces a MINOR release; one breaking change produces a MAJOR.
|
|
18
|
+
|
|
19
|
+
## Two ways to mark a break
|
|
20
|
+
|
|
21
|
+
Both are valid; you can use either or both. The `!` is the scannable signal; the footer carries the
|
|
22
|
+
migration note. **Prefer using both** so humans get the explanation:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
feat(api)!: require Bearer tokens on all endpoints
|
|
26
|
+
|
|
27
|
+
BREAKING CHANGE: the legacy `X-Api-Key` header is no longer accepted.
|
|
28
|
+
Migrate by exchanging your key for a token at POST /v1/auth/token.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
A breaking change can ride on **any** type — `fix!:` is a bug fix that also breaks the contract.
|
|
32
|
+
|
|
33
|
+
## Pre-1.0.0 caveat
|
|
34
|
+
|
|
35
|
+
SemVer treats `0.y.z` as unstable: anything may break at any time. Many tools therefore map a breaking
|
|
36
|
+
change to a **MINOR** bump (`0.4.0 → 0.5.0`) rather than `1.0.0` until you explicitly cut `1.0.0`.
|
|
37
|
+
Configure this deliberately — accidentally shipping `1.0.0` signals an API-stability promise you may not
|
|
38
|
+
be ready to make.
|
|
39
|
+
|
|
40
|
+
## What actually counts as "breaking"
|
|
41
|
+
|
|
42
|
+
Breaking = an existing consumer following the documented contract would now fail. Examples:
|
|
43
|
+
|
|
44
|
+
- Removing or renaming a public field, endpoint, function, or CLI flag.
|
|
45
|
+
- Changing a default value, response shape, or error code clients branch on.
|
|
46
|
+
- Tightening validation so previously-accepted input is now rejected.
|
|
47
|
+
- Raising a minimum runtime/engine version.
|
|
48
|
+
|
|
49
|
+
Adding an *optional* field, a new endpoint, or a new enum value is **additive** (MINOR), not breaking —
|
|
50
|
+
provided clients are built to ignore unknown fields.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Scoping Commits — One Logical Change
|
|
2
|
+
|
|
3
|
+
A flawless message on a tangled commit is still a bad commit. The hardest part of good history isn't
|
|
4
|
+
wording — it's deciding **what belongs in one commit**.
|
|
5
|
+
|
|
6
|
+
## The atomic-commit rule
|
|
7
|
+
|
|
8
|
+
Each commit should be a single coherent change that **builds and passes tests on its own**. If you can't
|
|
9
|
+
describe it in one Conventional Commit subject without using "and", it's probably two commits.
|
|
10
|
+
|
|
11
|
+
Good signs:
|
|
12
|
+
- `git revert <sha>` would cleanly back out exactly one concern.
|
|
13
|
+
- `git bisect` landing on this commit points at a single suspect.
|
|
14
|
+
- A reviewer can hold the whole change in their head.
|
|
15
|
+
|
|
16
|
+
## Splitting a messy working tree
|
|
17
|
+
|
|
18
|
+
When you've already written tangled changes, stage them by concern instead of `git add .`:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
git add -p # interactively stage hunks — accept only the fix's hunks
|
|
22
|
+
git commit -m "fix: reject negative quantity in cart total"
|
|
23
|
+
git add -p # now stage the refactor hunks
|
|
24
|
+
git commit -m "refactor: extract priceFor() from CartTotal"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`git add -p` walks each hunk and asks y/n/s (split). It's the single most useful habit for clean history.
|
|
28
|
+
For changes that can't be hunk-split (intertwined lines), use `git stash` to park one concern while you
|
|
29
|
+
commit the other, or `git restore --staged -p` to unstage.
|
|
30
|
+
|
|
31
|
+
## Anti-patterns
|
|
32
|
+
|
|
33
|
+
- **"WIP" / "fixes" / "stuff"** — unparseable, unreviewable. Squash or reword before merge.
|
|
34
|
+
- **The end-of-day mega-commit** — a day's unrelated work in one blob. Commit as you finish each unit.
|
|
35
|
+
- **Mixing formatting with logic** — a Prettier sweep buried in a feature hides the real change in noise.
|
|
36
|
+
Do formatting in its own `style:` commit.
|
|
37
|
+
|
|
38
|
+
## Rewriting before you push
|
|
39
|
+
|
|
40
|
+
History is cheap to fix *before* it's shared. Use `git rebase -i` to squash fixups, reorder, and reword
|
|
41
|
+
local commits into a clean series. Once pushed to a shared branch, leave it alone (or coordinate) — never
|
|
42
|
+
rewrite published history others have built on.
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# The Conventional Commits Grammar
|
|
2
|
+
|
|
3
|
+
The canonical spec is [conventionalcommits.org](https://www.conventionalcommits.org/). This is a working
|
|
4
|
+
summary with the parts teams trip over.
|
|
5
|
+
|
|
6
|
+
## Structure
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
<type>[(<scope>)][!]: <subject>
|
|
10
|
+
|
|
11
|
+
[body]
|
|
12
|
+
|
|
13
|
+
[footer(s)]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Exactly one blank line separates header, body, and footers.
|
|
17
|
+
- The header is a single line. Everything machine-read lives here.
|
|
18
|
+
|
|
19
|
+
## Header
|
|
20
|
+
|
|
21
|
+
| Part | Required | Rules |
|
|
22
|
+
|---|---|---|
|
|
23
|
+
| type | yes | lowercase; from the agreed set |
|
|
24
|
+
| scope | no | `(noun)`; lowercase; the affected area |
|
|
25
|
+
| `!` | no | breaking-change flag, placed right before `:` |
|
|
26
|
+
| `: ` | yes | colon then a single space |
|
|
27
|
+
| subject | yes | imperative, lowercase start, no trailing `.`, header ≤ 72 chars |
|
|
28
|
+
|
|
29
|
+
The "imperative mood" test: the subject should complete *"If applied, this commit will ___"*. So write
|
|
30
|
+
`fix: handle null cursor`, not `fixed null cursor` or `fixes null cursor`.
|
|
31
|
+
|
|
32
|
+
## Body
|
|
33
|
+
|
|
34
|
+
- Free-form, wrapped at ~72 columns.
|
|
35
|
+
- Explain **why** the change exists and any context the diff can't show. Do not restate the diff.
|
|
36
|
+
- May contain multiple paragraphs and bullet lists.
|
|
37
|
+
|
|
38
|
+
## Footers
|
|
39
|
+
|
|
40
|
+
Footers follow the [git trailer](https://git-scm.com/docs/git-interpret-trailers) format —
|
|
41
|
+
`Token: value`, one per line, after a blank line:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
BREAKING CHANGE: the `token` field was renamed to `accessToken`.
|
|
45
|
+
Refs: #214
|
|
46
|
+
Closes: #220
|
|
47
|
+
Reviewed-by: Jordan Lee
|
|
48
|
+
Co-Authored-By: A. Dev <a.dev@example.com>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `BREAKING CHANGE:` (uppercase, with a space) is special: its presence forces a MAJOR bump. `BREAKING-CHANGE:`
|
|
52
|
+
(hyphen) is accepted as a synonym by parsers.
|
|
53
|
+
- Issue references (`Closes #220`) let platforms auto-close issues on merge.
|
|
54
|
+
|
|
55
|
+
## Reverts
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
revert: let us never speak of the night deploy again
|
|
59
|
+
|
|
60
|
+
This reverts commit 676104e.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The footer `This reverts commit <sha>.` is what `git revert` generates and what tooling looks for.
|
|
64
|
+
|
|
65
|
+
## Merge commits
|
|
66
|
+
|
|
67
|
+
Merge commits are typically excluded from changelog generation (they have no single type). Squash-merge
|
|
68
|
+
workflows collapse a branch into one commit — make sure the **squash subject** is itself a valid
|
|
69
|
+
Conventional Commit, because that is the message that lands on the main branch.
|
|
70
|
+
|
|
71
|
+
## FAQ
|
|
72
|
+
|
|
73
|
+
- **Multiple types in one change?** Split the commit. One logical change → one type.
|
|
74
|
+
- **Where do `scope`s come from?** Define a fixed list per repo (e.g. `api`, `web`, `auth`, `deps`) and
|
|
75
|
+
enforce it via `scope-enum` in commitlint so they stay consistent.
|
|
76
|
+
- **Capital or lowercase subject?** Lowercase by convention; some teams allow sentence case — pick one and
|
|
77
|
+
lint it.
|
|
78
|
+
- **Emoji (gitmoji)?** A different convention. Don't mix it with Conventional Commits or parsers choke.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Runnable check: validates a Conventional Commit *header* against the spec from
|
|
3
|
+
// this skill — type, optional scope, optional `!`, colon-space, and a subject
|
|
4
|
+
// within a 72-char total header. Zero dependencies, Node built-ins only.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node lint-commit.mjs "feat(api): add pagination" # check a literal header
|
|
8
|
+
// echo "fix: handle null" | node lint-commit.mjs # check from stdin
|
|
9
|
+
// node lint-commit.mjs --selftest # run built-in test cases
|
|
10
|
+
|
|
11
|
+
const ALLOWED_TYPES = [
|
|
12
|
+
'feat', 'fix', 'docs', 'style', 'refactor',
|
|
13
|
+
'perf', 'test', 'build', 'ci', 'chore', 'revert',
|
|
14
|
+
];
|
|
15
|
+
const MAX_HEADER = 72;
|
|
16
|
+
|
|
17
|
+
// type(scope)!: subject — scope and ! are optional; capture each part.
|
|
18
|
+
const HEADER_RE = /^(?<type>[a-z]+)(?:\((?<scope>[^()\s]+)\))?(?<bang>!)?: (?<subject>.+)$/;
|
|
19
|
+
|
|
20
|
+
export function lintHeader(header) {
|
|
21
|
+
const failures = [];
|
|
22
|
+
if (typeof header !== 'string' || header.length === 0) {
|
|
23
|
+
return { ok: false, failures: ['header is empty'] };
|
|
24
|
+
}
|
|
25
|
+
if (header.length > MAX_HEADER) {
|
|
26
|
+
failures.push(`header is ${header.length} chars (max ${MAX_HEADER})`);
|
|
27
|
+
}
|
|
28
|
+
const m = header.match(HEADER_RE);
|
|
29
|
+
if (!m) {
|
|
30
|
+
failures.push('does not match "type(scope)!: subject" (need a lowercase type and ": " separator)');
|
|
31
|
+
return { ok: false, failures };
|
|
32
|
+
}
|
|
33
|
+
const { type, scope, subject } = m.groups;
|
|
34
|
+
if (!ALLOWED_TYPES.includes(type)) {
|
|
35
|
+
failures.push(`type "${type}" is not allowed (use one of: ${ALLOWED_TYPES.join(', ')})`);
|
|
36
|
+
}
|
|
37
|
+
if (scope !== undefined && scope !== scope.toLowerCase()) {
|
|
38
|
+
failures.push(`scope "${scope}" must be lowercase`);
|
|
39
|
+
}
|
|
40
|
+
if (subject.trim().length === 0) {
|
|
41
|
+
failures.push('subject is empty');
|
|
42
|
+
}
|
|
43
|
+
if (/^[A-Z]/.test(subject)) {
|
|
44
|
+
failures.push('subject should start lowercase (imperative mood)');
|
|
45
|
+
}
|
|
46
|
+
if (/\.$/.test(subject)) {
|
|
47
|
+
failures.push('subject must not end with a period');
|
|
48
|
+
}
|
|
49
|
+
return { ok: failures.length === 0, failures };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function check(label, header) {
|
|
53
|
+
const trimmed = String(header).split('\n')[0].trimEnd();
|
|
54
|
+
const { ok, failures } = lintHeader(trimmed);
|
|
55
|
+
if (ok) console.log(`✓ ${label}: valid commit header`);
|
|
56
|
+
else {
|
|
57
|
+
console.error(`✗ ${label}: "${trimmed}"`);
|
|
58
|
+
failures.forEach((f) => console.error(` - ${f}`));
|
|
59
|
+
}
|
|
60
|
+
return ok;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function selftest() {
|
|
64
|
+
const cases = {
|
|
65
|
+
'valid feat': 'feat: add cursor pagination',
|
|
66
|
+
'valid scope': 'feat(orders): add cursor pagination',
|
|
67
|
+
'valid breaking': 'refactor(api)!: drop v0 auth header',
|
|
68
|
+
'valid fix': 'fix(auth): treat expired token as 401',
|
|
69
|
+
'invalid type': 'feature: add thing',
|
|
70
|
+
'invalid capitalized': 'feat: Add thing',
|
|
71
|
+
'invalid period': 'fix: handle null cursor.',
|
|
72
|
+
'invalid no-colon': 'feat add thing',
|
|
73
|
+
'invalid too-long': 'feat: ' + 'x'.repeat(80),
|
|
74
|
+
'invalid upper scope': 'feat(API): add thing',
|
|
75
|
+
};
|
|
76
|
+
let allExpected = true;
|
|
77
|
+
for (const [name, header] of Object.entries(cases)) {
|
|
78
|
+
const { ok } = lintHeader(header);
|
|
79
|
+
const shouldPass = name.startsWith('valid');
|
|
80
|
+
const correct = ok === shouldPass;
|
|
81
|
+
allExpected &&= correct;
|
|
82
|
+
console.log(
|
|
83
|
+
`${correct ? '✓' : '✗'} selftest ${name}: ` +
|
|
84
|
+
`${ok ? 'valid' : 'invalid'} (expected ${shouldPass ? 'valid' : 'invalid'})`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
process.exit(allExpected ? 0 : 1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const arg = process.argv[2];
|
|
91
|
+
if (arg === '--selftest') selftest();
|
|
92
|
+
else if (arg) process.exit(check('header', arg) ? 0 : 1);
|
|
93
|
+
else {
|
|
94
|
+
let buf = '';
|
|
95
|
+
process.stdin.on('data', (c) => (buf += c));
|
|
96
|
+
process.stdin.on('end', () => process.exit(check('stdin', buf) ? 0 : 1));
|
|
97
|
+
}
|