Initial commit: pygenium as git submodule
This commit is contained in:
287
src/checks/complexity.ts
Normal file
287
src/checks/complexity.ts
Normal file
@@ -0,0 +1,287 @@
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user