complexity-reduction
Skill0 starsChanged 7 days agoMITView on GitHub
Reduce cyclomatic complexity in NMG code (CodeFactor Complex Method, or local tools/complexity-gate.ts). Use when a method is flagged as too complex, or before committing code that grows a method's branching.
In this repository8 other files it gives its agents.
- AGENTS.mdAGENTS.md
- docs/AGENTS.mdAGENTS.md
- doc-maintenanceSkill
- nmg-memorySkill
- repo-developmentSkill
- script-reuseSkill
- verification-traceabilitySkill
- working-state-maintenanceSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 2 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 56
If a flagged method is unchanged from the baseline it is pre-existing debt, but CodeFactor still requires it under the threshold when the …
- Very specific: line 90
If a function does multiple things — often signalled by comment blocks like // Step 2: ...
- Distinctive: line 166
Run the file's tests after the refactor — the extraction must not change behavior.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Complexity reduction
简体中文 · Terms used
Cyclomatic complexity counts linearly independent paths through a method: every
if, else if, for, while, case, catch, &&, ||, and ?: adds one.
High complexity means many paths to test and maintain. CodeFactor enforces a
threshold on the PR diff; the local
complexity:gate does the same before push.
When to use
- CodeFactor reports
Complex Methodon your PR. npm run complexity:gatefails (a changed method grew, or a new method exceeds the cap).- You are about to add branching to an already-large method.
Diagnose first
Identify which methods are over the threshold and whether you introduced them:
npx eslint --no-config-lookup --format json \
--rule '{"complexity": ["error", 15]}' <file> # per-method complexity
git show <base>:<file> | npx eslint --no-config-lookup --format json \
--stdin --stdin-filename <file> --rule '{"complexity": ["error", 15]}' # baseline (was it already complex?)Rule-only runs must pass --no-config-lookup (ESLint 9 otherwise merges the
repo config into the run) and --format json (the text formatters are not
stable to parse).
Baseline gotcha: the gate is self-referential on main
complexity:gate defaults its baseline to the merge base with origin/main.
That is correct on a feature branch, but on main itself the merge base is
HEAD, so every violation reads "unchanged from baseline" and the gate always
reports ok. When auditing on main, or after a merge, measure against the
actually landed state:
git fetch origin
npm run complexity:gate -- --base origin/mainA failed gate names the skill to consult (this file); CodeFactor enforces the same cap on PR diffs even for methods unchanged from the baseline when their file is in the diff — reduce them anyway.
If a flagged method is unchanged from the baseline it is pre-existing debt, but CodeFactor still requires it under the threshold when the file is in the diff — reduce it anyway (it is a reviewable improvement, not a no-op).
The four techniques (in order of value)
1. Guard clauses (fail fast, keep the main path linear)
Replace nested if trees with early returns. This is the single biggest win.
// Before — arrow anti-pattern (deep nesting)
function collect(exec, result) {
const name = exec && exec.name
if (name) {
const fileIndex = ctx.get('fileIndex')
if (fileIndex && typeof fileIndex.addScopePath === 'function') {
// ... work
}
}
}
// After — linear flow
function collect(exec, result) {
const name = exec && exec.name
if (!name) return
const fileIndex = ctx.get('fileIndex')
if (!fileIndex || typeof fileIndex.addScopePath !== 'function') return
// ... work at one nesting level
}2. Composed functions (extract one thing per function)
If a function does multiple things — often signalled by comment blocks like
// Step 2: ... — extract each into a small helper. The caller becomes
orchestration; each helper has few branches.
// Before: one function with grep + read extraction + recording
function observe(exec, result) { /* 20+ branches */ }
// After: orchestration + helpers
function observe(exec, result) {
if (!isObservable(exec)) return
const index = ctx.get('fileIndex')
if (!index) return
for (const path of extractPaths(exec, result)) index.addScopePath(path)
}
function isObservable(exec) { /* 1 branch */ }
function extractPaths(exec, result) { /* dispatches to per-tool helpers */ }
function grepHitPaths(value, args) { /* one tool's extraction */ }
function readHitPaths(value, args) { /* the other */ }3. Lookup tables instead of switch/if chains
Replace bulky switch or long if/else with a Record/Map keyed by the
discriminant. Removes the branching entirely.
// Before
if (kind === 'grep') return grepHitPaths(v, a)
if (kind === 'read') return readHitPaths(v, a)
return []
// After (when the branches are data-driven)
const EXTRACTORS = { grep: grepHitPaths, read: readHitPaths }
return EXTRACTORS[kind] ? EXTRACTORS[kind](v, a) : []4. Modern language features (optional chaining, nullish coalescing)
value && value.x and value ? value.x : default are branches. Prefer
value?.x and value ?? default where the semantics match — they read as one
expression, not a decision.
// Before
const v = result && result.isError ? undefined : result && result.value
const args = (exec && exec.arguments) || {}
// After
const v = result && !result.isError ? result.value : undefined // keep the guard
const args = exec?.arguments ?? {}(Do not force this where the original &&/|| has different semantics — guard
clauses and extraction are the reliable wins; this is a readability polish.)
Worked example (NMG dsh-nmg scope observer)
A real fix reduced collectScopePaths from 37 → below threshold:
- Guard clauses: tool-name check and FileIndex-service check became early returns before any extraction logic.
- Composed functions: the body was split into
extractHitPaths(dispatch) →grepHitPaths/readHitPaths(per-tool extraction). The recorder loop stayed in the caller. - The per-tool helpers each kept their defensive
typeofchecks, so behavior was unchanged — only the branch count per function dropped.
Verify
npx eslint --rule '{"complexity": ["error", 15]}' <changed-file> # every method ≤ 15
npm run complexity:gate # diff-aware gate
npm run check # typesRun the file's tests after the refactor — the extraction must not change behavior. If CodeFactor still flags a method, check whether it is a different method than the one you fixed (it re-scans the whole diff file).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
Reviews can't be read right now.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

