Initial commit: pygenium as git submodule

This commit is contained in:
2026-08-07 14:54:45 -04:00
commit 581436ed23
61 changed files with 9331 additions and 0 deletions

287
src/checks/complexity.ts Normal file
View 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.
* - 3549 → 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. |
| 3549 | **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)
- **3549** = 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 (3549 band)
For each function kept at 3549 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 3549 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 3549 functions
For each function in the 3549 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 3549)
- <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);