288 lines
9.3 KiB
TypeScript
288 lines
9.3 KiB
TypeScript
/**
|
||
* checks/complexity.ts — excessive complexity check.
|
||
*
|
||
* Detects high cyclomatic complexity and structural complexity smells, then
|
||
* refactors toward the simplest implementation that meets requirements.
|
||
*
|
||
* Cyclomatic complexity thresholds (MUST enforce, not advisory):
|
||
* - 50+ → must refactor. No exceptions.
|
||
* - 35–49 → heavy skepticism. Only keep if critical path + justified.
|
||
* - <35 → not flagged on cyclomatic grounds (may still be flagged for other
|
||
* structural smells).
|
||
*
|
||
* Structural smells detected:
|
||
* - deep nesting (>3 levels)
|
||
* - speculative abstractions
|
||
* - premature config indirection
|
||
* - non-idiomatic patterns
|
||
* - over-engineered generics
|
||
* - unnecessary wrappers
|
||
*
|
||
* @module pygienium/checks/complexity
|
||
*/
|
||
|
||
import { registerCheck, type CheckScope } from "./registry.js";
|
||
|
||
/** Phase-strip phase this check belongs to. */
|
||
export const COMPLEXITY_PHASE_ID = "C4";
|
||
|
||
/**
|
||
* Artifact directory: `<cwd>/pygienium/checks/complexity/`.
|
||
*/
|
||
export function complexityArtifactDir(scope: CheckScope): string {
|
||
const base = scope.cwd.replace(/\/+$/, "");
|
||
return `${base}/pygienium/checks/complexity`;
|
||
}
|
||
|
||
/** Absolute path to findings artifact. */
|
||
export function findingsPath(scope: CheckScope): string {
|
||
return `${complexityArtifactDir(scope)}/findings.md`;
|
||
}
|
||
|
||
/** Absolute path to changes artifact. */
|
||
export function changesPath(scope: CheckScope): string {
|
||
return `${complexityArtifactDir(scope)}/changes.md`;
|
||
}
|
||
|
||
/**
|
||
* Shared rubric for complexity analysis, injected into both scan and fix tasks.
|
||
*/
|
||
const RUBRIC = `# Complexity hygiene rubric
|
||
|
||
## Cyclomatic complexity thresholds
|
||
|
||
Cyclomatic complexity counts the number of independent paths through a function.
|
||
Compute via language-native tools when available (lizard, radon, gocyclo), or
|
||
count decision points (if/else if/for/while/case/&&/||/catch) per function.
|
||
|
||
| Score | Action |
|
||
|-------|--------|
|
||
| 50+ | **MUST refactor.** No exceptions. Break the function into smaller pieces. |
|
||
| 35–49 | **Heavy skepticism.** Only keep if this is a massively critical point along the main path AND the complexity genuinely must be here. Document justification in findings.md; otherwise refactor. |
|
||
| <35 | Not flagged on cyclomatic grounds (may still be flagged for other structural smells). |
|
||
|
||
## Structural complexity smells
|
||
|
||
- **Deep nesting (>3 levels).** Flatten with early returns, guard clauses, or extracting to named helpers.
|
||
- **Speculative abstractions.** Remove abstractions created "just in case" — no concrete use case yet.
|
||
- **Premature config indirection.** Remove configuration layers that add no value yet.
|
||
- **Non-idiomatic patterns.** Replace with common conventions for the language.
|
||
- **Over-engineered generics.** Simplify to concrete types when only one type is used.
|
||
- **Unnecessary wrappers.** Inline trivial wrappers that add no logic.
|
||
|
||
## Refactoring principles
|
||
|
||
1. **Simplest implementation.** Choose the simplest implementation that fully meets current requirements.
|
||
2. **No backward-compat baggage.** Remove obsolete paths rather than adding compatibility layers.
|
||
3. **Grow in layers.** Build on a product that already works; don't trade a working product for unfinished complexity.
|
||
4. **Use existing libraries.** Lean on well-maintained libraries when they reduce complexity or improve reliability.
|
||
5. **Long-term decisions.** Make architectural decisions for the long term, not stopgaps meant to be replaced later.
|
||
`;
|
||
|
||
/**
|
||
* Build the analysis sub-agent task. Instructs the agent to:
|
||
* 1. Compute cyclomatic complexity per function
|
||
* 2. Identify structural complexity smells
|
||
* 3. Write findings to pygienium/checks/complexity/findings.md
|
||
*/
|
||
export function buildComplexityScanTask(
|
||
_cwd: string,
|
||
scope: CheckScope,
|
||
): string {
|
||
const outDir = complexityArtifactDir(scope);
|
||
const findingsFile = findingsPath(scope);
|
||
return `# Task: excessive complexity scan
|
||
|
||
You are running the **complexity** hygiene check.
|
||
|
||
## Target
|
||
- Scan target: \`${scope.target}\`
|
||
|
||
## What to do
|
||
|
||
### 1. Compute cyclomatic complexity
|
||
|
||
For each source file in the target:
|
||
|
||
1. Read the recon snapshot at \`${scope.cwd}/.pygienium/recon.json\` if it
|
||
exists; otherwise enumerate source files directly under the target.
|
||
2. For each file, identify every function/method/class.
|
||
3. Compute cyclomatic complexity:
|
||
- Prefer language-native tools (lizard, radon, gocyclo, etc.) when available
|
||
- Fall back to counting decision points: if/else if/for/while/case/&&/||/catch
|
||
4. Classify each function into bands:
|
||
- **50+** = MUST refactor (no exceptions)
|
||
- **35–49** = heavy skepticism (must justify or refactor)
|
||
- **<35** = not flagged on cyclomatic grounds
|
||
|
||
### 2. Identify structural complexity smells
|
||
|
||
For each file, identify:
|
||
- Deep nesting (>3 levels)
|
||
- Speculative abstractions
|
||
- Premature config indirection
|
||
- Non-idiomatic patterns
|
||
- Over-engineered generics
|
||
- Unnecessary wrappers
|
||
|
||
### 3. Write findings
|
||
|
||
Write a findings report to \`${findingsFile}\` with per-function scores and
|
||
structural smell locations. Include a proposed simpler form for every flagged
|
||
function.
|
||
|
||
${RUBRIC}
|
||
|
||
## findings.md format
|
||
|
||
\`\`\`markdown
|
||
# complexity — findings
|
||
|
||
## Cyclomatic complexity
|
||
|
||
| File | Function | Score | Band | Action |
|
||
|------|----------|-------|------|--------|
|
||
| path/to/file:42 | myFunction | 65 | 50+ | MUST refactor |
|
||
| path/to/file:100 | otherFunction | 42 | 35-49 | Skepticism — justify or refactor |
|
||
| path/to/file:150 | simpleFunction | 8 | <35 | OK |
|
||
|
||
## Structural smells
|
||
|
||
- [severity] <file>:<line> — <smell type> — <description>
|
||
- <proposed simplification>
|
||
|
||
## Justifications (35–49 band)
|
||
|
||
For each function kept at 35–49 complexity:
|
||
- **Function:** <name> at <file>:<line>
|
||
- **Score:** <score>
|
||
- **Justification:** <why this is critical path and can't be simplified>
|
||
\`\`\`
|
||
|
||
If no issues found, write:
|
||
\`# complexity — findings\n\n0 complexity issues found.\`
|
||
|
||
Always create findings.md so the run has an artifact.
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Build the fix sub-agent task from the scan findings. Instructs the agent to:
|
||
* 1. Split 50+ complexity functions
|
||
* 2. Refactor or justify 35–49 functions
|
||
* 3. Apply safe refactors for structural smells
|
||
* 4. Write changes summary to pygienium/checks/complexity/changes.md
|
||
*/
|
||
export function buildComplexityFixTask(
|
||
_cwd: string,
|
||
scope: CheckScope,
|
||
findings: string,
|
||
): string {
|
||
const outDir = complexityArtifactDir(scope);
|
||
const changesFile = changesPath(scope);
|
||
return `# Task: excessive complexity fix
|
||
|
||
You are running the **complexity** hygiene fix phase.
|
||
|
||
## Target
|
||
- Fix target: \`${scope.target}\`
|
||
|
||
## Input: scan findings
|
||
${findings.trim().length > 0 ? findings : "(no findings text provided)"}
|
||
|
||
## What to do
|
||
|
||
### 1. Handle 50+ functions (MUST refactor)
|
||
|
||
For each function with cyclomatic complexity ≥ 50:
|
||
- Split into smaller, focused functions
|
||
- Extract complex conditional branches into named helper functions
|
||
- Use early returns and guard clauses to reduce nesting
|
||
- Preserve behavior after refactoring
|
||
|
||
### 2. Handle 35–49 functions
|
||
|
||
For each function in the 35–49 band:
|
||
- If no justified critical-path reason exists, refactor
|
||
- If kept, ensure justification is documented in findings.md
|
||
- Prefer refactoring over keeping
|
||
|
||
### 3. Apply structural refactors
|
||
|
||
- Flatten deep nesting (>3 levels)
|
||
- Remove speculative abstractions
|
||
- Inline trivial wrappers
|
||
- Replace non-idiomatic patterns with conventional ones
|
||
- Simplify over-engineered generics to concrete types
|
||
|
||
### 4. Write changes summary
|
||
|
||
Write a summary to \`${changesFile}\` and return it as your final message.
|
||
|
||
${RUBRIC}
|
||
|
||
## changes.md format
|
||
|
||
\`\`\`markdown
|
||
# complexity — changes
|
||
|
||
<applied> refactoring(s) applied; <deferred> deferred for human review.
|
||
|
||
## Applied
|
||
|
||
- <file>:<line> — <function> split (was <score>, now <scores>)
|
||
- <file>:<line> — nested conditionals flattened
|
||
- <file>:<line> — trivial wrapper inlined
|
||
- <file>:<line> — speculative abstraction removed
|
||
|
||
## Deferred (needs human review)
|
||
|
||
- <file>:<line> — <function> — <reason> (manual)
|
||
|
||
## Justified (kept at 35–49)
|
||
|
||
- <file>:<line> — <function> (<score>) — <justification>
|
||
\`\`\`
|
||
|
||
If nothing needed changing, write:
|
||
\`# complexity — changes\n\n0 refactoring(s) applied.\`
|
||
|
||
Always create changes.md so the run has an artifact. Write it under \`${outDir}\`.
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* Precondition gate. Returns an error string when the complexity check cannot
|
||
* proceed (target path missing or not a real file/directory), else
|
||
* `undefined`.
|
||
*/
|
||
async function complexityGate(cwd: string): Promise<string | undefined> {
|
||
const { stat } = await import("node:fs/promises");
|
||
const { resolve } = await import("node:path");
|
||
const target = resolve(cwd);
|
||
try {
|
||
const s = await stat(target);
|
||
if (s.isDirectory() || s.isFile()) return undefined;
|
||
return `target is not a file or directory: ${target}`;
|
||
} catch {
|
||
return `target path does not exist: ${target}`;
|
||
}
|
||
}
|
||
|
||
/** The excessive complexity check definition. */
|
||
export const complexityCheck = {
|
||
name: "complexity",
|
||
label: "Complexity",
|
||
description:
|
||
"Detect and refactor excessive complexity: high cyclomatic complexity (50+ must refactor, 35-49 needs justification), deep nesting, and speculative abstractions.",
|
||
agentName: "scanner",
|
||
fixAgentName: "fixer",
|
||
phaseId: COMPLEXITY_PHASE_ID,
|
||
buildScanTask: buildComplexityScanTask,
|
||
buildFixTask: buildComplexityFixTask,
|
||
gate: complexityGate,
|
||
} as const;
|
||
|
||
// Self-register on import so index.ts auto-discovery picks it up.
|
||
registerCheck(complexityCheck);
|