Files
pygienium/src/checks/registry.ts

140 lines
4.8 KiB
TypeScript

/**
* checks/registry.ts — pluggable check registry.
*
* A `CheckDefinition` describes one hygiene check (e.g. `comments`, `complexity`).
* The registry is a module-level `Map` so that adding a check only requires a
* new file in `src/checks/` plus one `registerCheck(def)` call — no changes to
* `index.ts` command wiring. At startup, `index.ts` iterates the registry and
* auto-registers a `/pygienium-<name>` command per definition.
*
* Lifecycle of a single check run (orchestrated by `src/modes/check-runner.ts`):
* Q0 recon (shared) → analysis sub-agent (buildScanTask) →
* fix sub-agent (buildFixTask, only with --fix) → verify gate → cleanup.
*
* @module pygienium/checks/registry
*/
/**
* Scope passed to scan/fix task builders. Resolved from the command args:
* a positional path (absolute or relative to `cwd`) plus parsed flags.
*/
export interface CheckScope {
/** Absolute working directory the check operates on. */
cwd: string;
/** Target path (absolute) the check scans; defaults to `cwd` when none given. */
target: string;
/** Whether fixes should be applied (the `--fix` flag). */
fix: boolean;
/** Remaining raw tokens after flag parsing, for check-specific use. */
rest: string[];
}
/**
* Verifies preconditions before a check runs its analysis phase. Returns an
* error string when the check cannot proceed (e.g. no source files match),
* or `undefined` when the gate passes. Implemented per-check so generic
* checks can bail early without spawning an agent.
*/
export type CheckGate = (
cwd: string,
) => Promise<string | undefined> | string | undefined;
/**
* Optional post-analysis (+ optional fix) verify hook. Confirms the check
* actually produced its artifacts (e.g. `findings.md`/`changes.md`). Returns an
* error string to fail the verify phase, or `undefined` to pass. When omitted,
* the verify phase falls back to re-running {@link CheckDefinition.gate},
* preserving the historical behaviour for checks that have nothing to verify.
*/
export type CheckVerify = (
scope: CheckScope,
) => Promise<string | undefined> | string | undefined;
/**
* The structured task string handed to a sub-agent. `buildScanTask` produces
* the analysis prompt; `buildFixTask` consumes the findings text the scan
* agent emitted and produces a fix prompt.
*
* Task builders may be async: a check can pre-compute deterministic candidates
* (e.g. an import-graph scan) before assembling the prompt, so the sub-agent's
* job is to verify/refine rather than re-derive everything from scratch.
*/
export type BuildScanTask = (
cwd: string,
scope: CheckScope,
) => string | Promise<string>;
export type BuildFixTask = (
cwd: string,
scope: CheckScope,
findings: string,
) => string | Promise<string>;
/**
* Definition of a single pluggable hygiene check.
*/
export interface CheckDefinition {
/** Lowercase kebab command suffix → `/pygienium-<name>`. Must be unique. */
name: string;
/** Human label shown in help and status strips. */
label: string;
/** One-line description for `/pygienium-help`. */
description: string;
/**
* Name of the agent definition (from `agents/*.md`) used for the analysis
* phase. The fix phase uses the `fixer` agent unless `fixAgentName`
* overrides it.
*/
agentName: string;
/** Optional override for the fix-phase agent (defaults to `fixer`). */
fixAgentName?: string;
/** Identifier of the phase-strip phase this check belongs to (task 05). */
phaseId: string;
/** Builds the analysis sub-agent task. */
buildScanTask: BuildScanTask;
/** Builds the fix sub-agent task from scan findings. */
buildFixTask: BuildFixTask;
/**
* Precondition gate. Returning a string skips the check (recorded as
* `skipped`); returning `undefined` proceeds normally.
*/
gate: CheckGate;
/**
* Optional verify hook confirming artifacts landed (see {@link CheckVerify}).
* Falls back to re-running `gate` when omitted.
*/
verify?: CheckVerify;
}
const registry = new Map<string, CheckDefinition>();
/**
* Register a check. Throws on duplicate names so wiring mistakes surface
* loudly at startup rather than silently shadowing a command.
*/
export function registerCheck(def: CheckDefinition): void {
if (!def.name || !/^[a-z0-9][a-z0-9-]*$/.test(def.name)) {
throw new Error(
`Invalid check name "${def.name}": must be lowercase kebab (e.g. "comments").`,
);
}
if (registry.has(def.name)) {
throw new Error(`Duplicate pygienium check name: "${def.name}".`);
}
registry.set(def.name, def);
}
/** Look up a registered check by name. */
export function getCheck(name: string): CheckDefinition | undefined {
return registry.get(name);
}
/** All registered checks in insertion order. */
export function getAllChecks(): CheckDefinition[] {
return [...registry.values()];
}
/** Test-only: reset the registry between tests. */
export function clearChecks(): void {
registry.clear();
}