| name | enforce-code-discipline |
| description | Extend an existing guardrail installation with LLM discipline rules: complexity limits, cognitive complexity, naming conventions, ESM idioms, documentation coverage, and test coverage thresholds. Run after setup-guardrails.
|
Overview
Extend your TypeScript project's guardrail stack with a discipline layer that
forces LLMs to write code that is architecturally correct, consistent, and
easy to maintain.
What this skill adds:
- Complexity & size limits (
max-lines, max-lines-per-function, max-params, max-depth)
- Code quality checks (cognitive complexity, duplicate code, dead branches)
- Naming conventions (camelCase / PascalCase / UPPER_CASE enforcement)
- ESM idioms (unicorn plugin —
no-array-for-each, filename-case, etc.)
- Documentation coverage for public APIs (jsdoc — starts at
warn, flip to error in Wave 4)
- Test coverage thresholds (hard gates in Vitest)
Prerequisites: setup-guardrails skill already applied to this project.
eslint.config.js and vitest.config.ts must exist at the project root.
Step 1: Detect Project Context
Read these files:
package.json — detect package manager (lockfile: pnpm-lock.yaml → pnpm, yarn.lock → yarn, else npm)
eslint.config.js — verify guardrails are installed; identify whether this is single-package or monorepo (monorepo configs have a boundaries plugin import)
vitest.config.ts — read current test config to know what to preserve
1a. Detect project type
Monorepo if eslint.config.js imports eslint-plugin-boundaries OR packages/ directory exists.
Single package otherwise.
1b. Assess greenfield vs retrofit
Run:
npx eslint 'src/**/*.ts' --no-error-on-unmatched-pattern 2>&1 | tail -10
For monorepos:
npx eslint 'packages/*/src/**/*.ts' --no-error-on-unmatched-pattern 2>&1 | tail -10
- Greenfield (<10 violations): new rules at
error (except jsdoc — always warn)
- Retrofit (≥10 violations): new rules at
warn with a warning budget header
1c. Detect existing src/ structure
Run:
ls src/ 2>/dev/null || echo "(no src/ directory)"
For monorepos:
ls packages/ 2>/dev/null
Note the top-level directory names — you will list them in a comment block in the ESLint config.
Step 2: Install Plugins
npm install -D eslint-plugin-unicorn eslint-plugin-sonarjs eslint-plugin-jsdoc
Adapt for detected package manager:
- pnpm:
pnpm add -D eslint-plugin-unicorn eslint-plugin-sonarjs eslint-plugin-jsdoc
- yarn:
yarn add -D eslint-plugin-unicorn eslint-plugin-sonarjs eslint-plugin-jsdoc
Step 3: Add Imports to eslint.config.js
Do NOT rewrite the file. Open eslint.config.js and add these three imports after the existing import statements, before the export default line:
import unicorn from 'eslint-plugin-unicorn';
import sonarjs from 'eslint-plugin-sonarjs';
import jsdoc from 'eslint-plugin-jsdoc';
Step 4: Append Discipline Block to eslint.config.js
Find the eslintConfigPrettier entry at the bottom of the tseslint.config(...) call.
Insert the discipline blocks before eslintConfigPrettier (it must always be last).
First, add the detected structure comment. Replace [detected dirs] with the actual
directory names from Step 1c:
Then append the discipline config blocks.
Greenfield mode (all error except jsdoc)
For single-package projects (file glob: src/**/*.ts):
{
files: ['src/**/*.ts'],
rules: {
'max-lines': ['error', { max: 300, skipBlankLines: true, skipComments: true }],
'max-lines-per-function': ['error', { max: 40, skipBlankLines: true, skipComments: true }],
'max-params': ['error', { max: 4 }],
'max-depth': ['error', { max: 4 }],
'max-classes-per-file': ['error', { max: 1 }],
'no-magic-numbers': ['error', { ignore: [-1, 0, 1, 2], ignoreArrayIndexes: true, ignoreDefaultValues: true }],
'no-nested-ternary': 'error',
},
},
{
files: ['src/**/*.ts'],
plugins: { sonarjs },
rules: {
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-duplicate-string': ['error', { minDuplicates: 3 }],
'sonarjs/no-identical-functions': 'error',
'sonarjs/no-collapsible-if': 'error',
'sonarjs/no-gratuitous-expressions': 'error',
'sonarjs/no-redundant-jump': 'error',
'sonarjs/prefer-immediate-return': 'error',
},
},
{
files: ['src/**/*.ts'],
rules: {
'@typescript-eslint/naming-convention': [
'error',
{ selector: 'variable', format: ['camelCase', 'UPPER_CASE'] },
{ selector: 'function', format: ['camelCase'] },
{ selector: 'parameter', format: ['camelCase'] },
{ selector: 'property', format: ['camelCase'] },
{ selector: 'typeLike', format: ['PascalCase'] },
{ selector: 'enumMember', format: ['UPPER_CASE'] },
],
},
},
{
files: ['src/**/*.ts'],
plugins: { unicorn },
rules: {
'unicorn/filename-case': ['error', { case: 'kebabCase' }],
'unicorn/no-array-for-each': 'error',
'unicorn/no-for-loop': 'error',
'unicorn/explicit-length-check': 'error',
'unicorn/no-useless-undefined': 'error',
'unicorn/no-array-push-push': 'error',
'unicorn/no-lonely-if': 'error',
'unicorn/prefer-string-slice': 'error',
'unicorn/no-process-exit': 'error',
'unicorn/prefer-module': 'error',
},
},
{
files: ['src/**/*.ts'],
ignores: ['**/__tests__/**', '**/*.test.ts', '**/*.spec.ts'],
plugins: { jsdoc },
rules: {
'jsdoc/require-jsdoc': ['warn', {
publicOnly: true,
require: { FunctionDeclaration: true, ClassDeclaration: true },
}],
'jsdoc/require-param': 'warn',
'jsdoc/require-returns': 'warn',
'jsdoc/check-param-names': 'warn',
},
},
For monorepo projects (file glob: packages/*/src/**/*.ts, apps/*/src/**/*.ts):
Use the same blocks above but replace every 'src/**/*.ts' glob with
['packages/*/src/**/*.ts', 'apps/*/src/**/*.ts'].
Retrofit mode (all warn + warning budget header)
Add this header comment immediately after the structure comment block:
Replace the ? values by running the full lint pass after appending the config at warn:
npx eslint 'src/**/*.ts' --format json 2>/dev/null | node -e "
const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));
const c={};
for(const f of d) for(const m of f.messages) c[m.ruleId]=(c[m.ruleId]||0)+1;
for(const [r,n] of Object.entries(c).sort((a,b)=>b[1]-a[1])) console.log(n,r);
"
This shows per-rule violation counts. Fill in each ? with the count for that rule.
Then use 'warn' instead of 'error' in all five discipline blocks above
(jsdoc stays 'warn' regardless).
Step 5: Update vitest.config.ts
Open vitest.config.ts. Find the coverage block and add thresholds.
Do NOT rewrite the file. Only add the thresholds key inside the existing coverage block — preserve all other config.
Greenfield — set hard thresholds:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
include: ['src/**/*.test.ts'],
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
thresholds: {
lines: 80,
branches: 75,
functions: 80,
statements: 80,
},
},
},
});
For monorepos, include should be ['packages/*/src/**/*.test.ts', 'apps/*/src/**/*.test.ts'].
Retrofit — set thresholds at current baseline:
First run npx vitest run --coverage 2>&1 | grep -E 'Lines|Branches|Functions|Statements' to
get current coverage numbers. Use those values as the thresholds with a comment:
coverage: {
provider: 'v8',
reporter: ['text', 'lcov'],
thresholds: {
lines: 52,
branches: 44,
functions: 58,
statements: 52,
},
},
Step 6: Verify Setup
git add -A
git commit --allow-empty -m "chore: verify enforce-code-discipline setup"
Greenfield success criteria:
- Commit passes all hooks
- No errors from the new discipline rules (zero violations in a fresh project)
Retrofit success criteria:
- Pre-commit hooks run without hooks errors (warnings expected and tracked)
- Warning budget header is present and the
? values have been filled in
- Coverage thresholds are set to actual baseline values
If the empty commit fails, read the errors — they indicate a misconfigured ESLint block
(likely a missing import or a syntax error in the appended config). Fix and retry.
Wave Sequencing (Retrofit Only)
Drive each rule category to zero violations one wave at a time. Do NOT mix waves.
Wave 1 — auto-fixable (same day)
Run npx eslint --fix 'src/**/*.ts' to auto-fix:
unicorn/no-array-for-each — converts .forEach() to for...of
unicorn/no-for-loop — converts index-based loops to for...of
unicorn/no-lonely-if — merges else { if () } to else if
unicorn/no-array-push-push — merges consecutive .push() calls
unicorn/no-useless-undefined — removes explicit undefined returns
unicorn/prefer-string-slice — replaces .substring() with .slice()
unicorn/prefer-module — converts require() to import (partial)
unicorn/explicit-length-check — adds explicit > 0 to length checks
sonarjs/prefer-immediate-return — removes unnecessary temp variables
sonarjs/no-redundant-jump — removes unnecessary return/break
When count reaches 0 → flip each to error. Commit: refactor(lint): flip [rule] warn→error (0 violations)
Wave 2 — complexity & structural idioms (1–3 days)
Manually refactor:
unicorn/filename-case — rename files to kebab-case (update all imports)
unicorn/no-process-exit — replace process.exit() with thrown errors or callbacks
sonarjs/cognitive-complexity — break up nested logic into named helper functions
sonarjs/no-identical-functions — extract duplicate function bodies
sonarjs/no-collapsible-if — combine if (a) { if (b) } into if (a && b)
sonarjs/no-gratuitous-expressions — remove always-true/false conditions
max-lines — split large files at their natural boundaries
max-lines-per-function — extract private helper functions
max-params — introduce options objects for functions with >4 params
max-depth — use early returns and guard clauses to reduce nesting
max-classes-per-file — one class per file
no-nested-ternary — replace with if/else
When each reaches 0 → flip to error.
Wave 3 — naming & literals (3–7 days)
naming-convention — rename variables, run npx eslint --fix for auto-fixable renames
sonarjs/no-duplicate-string — extract repeated strings to named constants
no-magic-numbers — replace raw numbers with named constants
When each reaches 0 → flip to error.
Wave 4 — documentation (1–2 weeks)
jsdoc/require-jsdoc — add JSDoc to all exported functions and classes
jsdoc/require-param — document all parameters
jsdoc/require-returns — document return values
jsdoc/check-param-names — fix param name mismatches
When all jsdoc rules reach 0 → flip to error.
Related Skills
- setup-guardrails — Initial guardrail stack installation (run before this skill)
- enforce-architecture — Tier-based dependency boundary enforcement
- self-correcting-loop — How to handle commit rejections efficiently