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

231
src/checks/comments.ts Normal file
View File

@@ -0,0 +1,231 @@
/**
* checks/comments.ts — comments hygiene check (first end-to-end reference).
*
* This is the canonical `CheckDefinition` that future checks (08+) copy. It
* reuses the generic `scanner`/`fixer` agents shipped in `agents/*.md`; the
* check-specific rubric is embedded in the task text handed to the sub-agent
* (see {@link buildCommentsScanTask} / {@link buildCommentsFixTask}) so no
* per-check agent `.md` file is required.
*
* Rubric (the user's spec — short + high value):
* - comments that restate the code they sit on ("what" comments) → REMOVE
* - verbose narration / long-winded explanations → TIGHTEN (shorten)
* - "why" comments that explain intent, rationale, or gotchas → KEEP
* - code self-explanatory with no comment → no comment needed (don't add one)
*
* Artifacts (under `<cwd>/pygienium/checks/comments/`):
* - `findings.md` — per-file line refs for each smell
* - `changes.md` — summary of edits + human-review items
*
* @module pygienium/checks/comments
*/
import { registerCheck, type CheckScope } from "./registry.js";
/** Phase-strip phase this check belongs to. */
export const COMMENTS_PHASE_ID = "C1";
/**
* Directory where this check writes its `findings.md` and `changes.md`
* artifacts: `<cwd>/pygienium/checks/comments/`. Based on `scope.cwd` (the
* project root, always a directory) so the path is valid whether the scan
* target is a single file or a directory. Matches the spec's
* `pygienium/checks/comments/findings.md` relative-path notation.
*/
export function commentsArtifactDir(scope: CheckScope): string {
const base = scope.cwd.replace(/\/+$/, "");
return `${base}/pygienium/checks/comments`;
}
/** Absolute path to the findings artifact for this check. */
export function findingsPath(scope: CheckScope): string {
return `${commentsArtifactDir(scope)}/findings.md`;
}
/** Absolute path to the changes artifact for this check. */
export function changesPath(scope: CheckScope): string {
return `${commentsArtifactDir(scope)}/changes.md`;
}
/**
* Shared rubric block, injected into both scan and fix task text so the analysis
* and remediation sub-agents apply identical judgement.
*/
const RUBRIC = `# Comments hygiene rubric
Short + high value is the goal. Evaluate every comment in the target:
- **RESTATE → REMOVE.** A comment that paraphrases the line(s) it sits on adds
no information. Examples: \`// increment i\` over \`i++\`, \`// return the
result\` over \`return result\`. Delete it.
- **VERBOSE → TIGHTEN.** A comment that is high-value but needlessly long.
Rewrite it to one tight sentence preserving the key insight. Do not delete.
- **"WHY" → KEEP.** A comment explaining intent, rationale, a non-obvious
decision, a workaround, a gotcha, or a constraint the code cannot express.
Leave it untouched (tighten only if it is also verbose).
- **NO COMMENT NEEDED.** When the code is self-explanatory, do not add a comment.
- Keep inline section headers/dividers that aid navigation only if they mark a
real boundary; remove pure decoration.`;
/**
* Build the analysis sub-agent task. Instructs the agent to read candidate
* source files, identify comment smells per the rubric, and write per-file line
* references to `pygienium/comments/findings.md`.
*/
export function buildCommentsScanTask(_cwd: string, scope: CheckScope): string {
const outDir = commentsArtifactDir(scope);
const findingsFile = findingsPath(scope);
return `# Task: comments hygiene scan
You are running the **comments** hygiene check.
## Target
- Scan target: \`${scope.target}\`
## What to do
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 source file, read it and locate every comment (inline \`//\`,
block \`/* */\`, doc \`/** */\`, \`#\` for scripting languages, etc.).
3. Apply the rubric below to each comment and classify it: RESTATE, VERBOSE,
WHY, or OK.
4. Write a findings report to \`${findingsFile}\` with per-file line refs.
5. Return the findings report text as your final message (same content as the
file). The host captures it as the analysis-phase findings.
${RUBRIC}
## findings.md format
\`\`\`markdown
# comments — findings
<count> comment smell(s) across <files> file(s).
## <relative-file>
- L<line>: <smell: RESTATE|VERBOSE> — <quote or paraphrase>
- L<line>: KEEP (why) — <one-line reason> # listed for transparency
\`\`\`
If no smells are found, write \`# comments — findings\n\n0 comment smell(s).\`
and return that text. Always create findings.md so the run has an artifact.
Write the report under \`${outDir}\` (create directories as needed).
`;
}
/**
* Build the fix sub-agent task from the scan findings. Instructs the agent to
* apply safe removals/tightenings, leave "why" comments, and write a summary
* of edits plus anything needing human review to `pygienium/comments/changes.md`.
*/
export function buildCommentsFixTask(
_cwd: string,
scope: CheckScope,
findings: string,
): string {
const outDir = commentsArtifactDir(scope);
const changesFile = changesPath(scope);
return `# Task: comments hygiene fix
You are running the **comments** hygiene fix phase.
## Target
- Fix target: \`${scope.target}\`
## Input: scan findings
${findings.trim().length > 0 ? findings : "(no findings text provided)"}
## What to do
1. For each RESTATE finding: remove the comment entirely.
2. For each VERBOSE finding: replace the comment with a tightened one-sentence
version that keeps the key insight.
3. For every WHY comment: leave it untouched (tighten only if it is also
verbose, preserving the rationale).
4. Do not change any code logic, formatting, or ordering — only comments.
5. Write a summary to \`${changesFile}\` and return it as your final message.
${RUBRIC}
## changes.md format
\`\`\`markdown
# comments — changes
<applied> edit(s) applied; <deferred> deferred for human review.
## Applied
- <relative-file>:<line> — <removed|tightened> comment (auto)
## Needs human review
- <relative-file>:<line> — <reason> (manual)
\`\`\`
If nothing needed changing, write
\`# comments — changes\n\n0 edit(s) applied.\` and return that text. Always
create changes.md so the run has an artifact. Write it under \`${outDir}\`.
`;
}
/**
* Precondition gate. Returns an error string when the comments check cannot
* proceed (target path missing or not a real file/directory), else
* `undefined`. Idempotent — passes identically before analysis and at verify.
*/
async function commentsGate(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}`;
}
}
/**
* Verify hook: confirms the check actually produced its artifacts. After the
* scan phase `findings.md` must exist; after the fix phase `changes.md` must
* exist too (the scan-phase-only run skips `changes.md` by design). Returns an
* error string to fail verify, or `undefined` to pass. Replaces the historical
* no-op verify (which only re-ran the existence gate) so the verify phase now
* genuinely asserts the run produced its report.
*/
async function commentsVerify(scope: CheckScope): Promise<string | undefined> {
const { stat } = await import("node:fs/promises");
const f = findingsPath(scope);
try {
await stat(f);
} catch {
return `comments verify: expected findings.md at ${f} after scan, none found.`;
}
if (scope.fix) {
const c = changesPath(scope);
try {
await stat(c);
} catch {
return `comments verify: expected changes.md at ${c} after --fix, none found.`;
}
}
return undefined;
}
/** The comments hygiene check definition. */
export const commentsCheck = {
name: "comments",
label: "Comments",
description:
'Remove low-value/restating comments, tighten verbose ones, keep "why" comments.',
agentName: "scanner",
fixAgentName: "fixer",
phaseId: COMMENTS_PHASE_ID,
buildScanTask: buildCommentsScanTask,
buildFixTask: buildCommentsFixTask,
gate: commentsGate,
verify: commentsVerify,
} as const;
// Self-register on import so index.ts auto-discovery picks it up.
registerCheck(commentsCheck);