/** * checks/deep-modules.ts — "deep modules, not shallow ones" check. * * Detects modules with shallow abstractions (thin pass-throughs, one-line * re-export barrels, trivial getter classes, unnecessary adapter layers) and * recommends/applies consolidation. The rubric encodes John Ousterhout's * "deep modules" definition from *A Philosophy of Software Design*: a module * is valuable when it hides a substantial implementation behind a small * interface; a shallow one exposes as much complexity as it hides, so its * indirection adds cost without abstraction payoff. * * Lifecycle: * gate (need source files) → recon (shared) → scan sub-agent writes * `/.pygienium/checks/deep-modules/findings.md` → [with --fix] fix * sub-agent writes `changes.md`, inlines safe pass-throughs, and lists * risky consolidations (external importers / public API) for human review. * * Registering this file is the ONLY wiring needed: `index.ts` auto-discovers * `src/checks/*.ts`, so dropping this file exposes `/pygienium-deep-modules`. * * @module pygienium/checks/deep-modules */ import { join } from "node:path"; import { registerCheck, type CheckDefinition, type CheckScope, } from "./registry.js"; import { hasScopeSources, scopeRulesMarkdown } from "./scope.js"; /** Output directory for this check's persistent reports. */ export function deepModulesOutputDir(cwd: string): string { return join(cwd, ".pygienium", "checks", "deep-modules"); } /** `findings.md` path for this check. */ export function findingsPath(cwd: string): string { return join(deepModulesOutputDir(cwd), "findings.md"); } /** `changes.md` path for this check. */ export function changesPath(cwd: string): string { return join(deepModulesOutputDir(cwd), "changes.md"); } /** * Gate: skip when the cwd has no inspectable source files at all. A workspace * with zero source files gives the scanner nothing to classify. */ function deepModulesGate(cwd: string): string | undefined { try { if (hasScopeSources(cwd)) return undefined; } catch { // unreadable cwd → let the agent decide; don't block. return undefined; } return "no source files found to inspect"; } /** * Verify hook: confirms the check actually produced its artifacts (mirrors * {@link commentsVerify} / {@link todosVerify}). After scan `findings.md` must * exist; after `--fix` `changes.md` must exist too. Catches a sub-agent that * returns ok with no output — which would otherwise be a false `complete`. */ async function deepModulesVerify( scope: CheckScope, ): Promise { const { stat } = await import("node:fs/promises"); const f = findingsPath(scope.cwd); try { await stat(f); } catch { return `deep-modules verify: expected findings.md at ${f} after scan, none found.`; } if (scope.fix) { const c = changesPath(scope.cwd); try { await stat(c); } catch { return `deep-modules verify: expected changes.md at ${c} after --fix, none found.`; } } return undefined; } /** * Build the scan task. The deep-modules scanner agent inspects the target, * classifies modules by abstraction depth against the rubric, and writes a * structured findings report to `findings.md`. The output path is passed into * the task so both the real agent (which uses its `write` tool) and the * deterministic fake runner (which understands `!write `) persist * the report to the same location. * * Note: the `!write`/`!echo` lines are the deterministic fallback the fake * runner executes for tests/smoke runs; a real model-driven agent receives the * whole prompt and writes a real analysis. */ function buildDeepScanTask(cwd: string, scope: CheckScope): string { const findings = findingsPath(cwd); const target = scope.target; // The expected findings document shape, shown to a real model-driven agent // as the format spec. The `!write`/`!echo` lines below are the deterministic // fallback the fake runner executes for tests/smoke runs. return [ `Inspect the target "${target}" (cwd: ${cwd}) for shallow modules.`, `Classify every source module by abstraction depth (see your rubric).`, `Write your full findings report to: ${findings}.`, `findings.md must list each flagged module with: kind, evidence, importer`, `count, recommendation, and risk (low if no external importers, high else).`, `Then emit a one-line summary referencing the findings path.`, "", scopeRulesMarkdown(), "", `# Deterministic fallback (executed by the fake runner in tests):`, `!write ${findings} # Deep-modules findings | summary: 1 shallow module(s) flagged | ## 1. ${target}/wrapper.ts | kind: pass-through-wrapper | evidence: forwards every argument to a single library call with no added logic | importers: 0 | recommendation: inline-and-remove | risk: low`, `!echo deep-modules: 1 issue — see ${findings}`, ].join("\n"); } /** * Build the fix task. The fixer consumes the scan findings and applies ONLY safe * consolidations: inline-and-remove pass-through wrappers that have **zero** * external importers. Risky consolidations (any importer, or unclear * ownership) are listed in `changes.md` as `review-manual` and NOT applied. * Every action — applied or deferred — is recorded in `changes.md`. */ function buildDeepFixTask( cwd: string, scope: CheckScope, findings: string, ): string { const changes = changesPath(cwd); const findingsFile = findingsPath(cwd); const target = scope.target; return [ `Consolidate shallow modules found in the scan.`, `cwd: ${cwd} target: ${target}`, `Findings report (also persisted at ${findingsFile}):`, `---`, findings, `---`, ``, `Rules:`, `- Apply ONLY safe consolidations: a pass-through wrapper with zero external`, ` importers may be inlined at its single use site and the wrapper removed.`, `- NEVER auto-delete or rewrite a module with any external importer — list it`, ` for human review instead.`, `- Preserve public API boundaries; when in doubt, defer to manual review.`, `- Write changes.md to ${changes} describing every action (auto | manual) with`, ` the file, the finding, and the disposition.`, ``, `# Deterministic consolidation (executed by the fake runner in tests):`, `# Safe: zero-importer pass-through rewritten/removed (auto).`, `# Risky: external-importer adapter left in place (manual).`, `!write ${target}/wrapper.ts // Consolidated by pygienium-deep-modules: pass-through wrapper removed; callers now use the underlying implementation directly.`, `!write ${changes} # Deep-modules changes | 1. ${target}/wrapper.ts — pass-through-wrapper — consolidated: inlined the underlying call at the use site and removed the wrapper module (auto) | 2. ${target}/risky-adapter.ts — adapter-layer — 2 external importer(s): left in place; listed for review (manual)`, `!echo deep-modules: 1 auto-applied, 1 deferred to review — see ${changes}`, ].join("\n"); } /** The check definition; registers itself on import. */ const deepModulesCheck: CheckDefinition = { name: "deep-modules", label: "Deep modules", description: "Detect shallow modules (pass-throughs, trivial wrappers, re-export barrels) and consolidate the safe ones.", agentName: "deep-modules", phaseId: "analysis", buildScanTask: buildDeepScanTask, buildFixTask: buildDeepFixTask, gate: deepModulesGate, verify: deepModulesVerify, }; registerCheck(deepModulesCheck); export { deepModulesCheck };