Files
pygienium/src/checks/deep-modules.ts
Michael Freno 63c2f73a5e
All checks were successful
port-to-omp / port (push) Successful in 6s
better recon info in certain (nested) situations
2026-08-12 21:59:49 -04:00

180 lines
7.3 KiB
TypeScript

/**
* 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
* `<cwd>/.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<string | undefined> {
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 <path> <text>`) 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 };