180 lines
7.3 KiB
TypeScript
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 };
|