/** * 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: `/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] : - ## Justifications (35–49 band) For each function kept at 35–49 complexity: - **Function:** at : - **Score:** - **Justification:** \`\`\` 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 refactoring(s) applied; deferred for human review. ## Applied - : split (was , now ) - : — nested conditionals flattened - : — trivial wrapper inlined - : — speculative abstraction removed ## Deferred (needs human review) - : (manual) ## Justified (kept at 35–49) - : () — \`\`\` 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 { 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);