feat: keep run artifacts under hidden .pygienium/ and out of git
All per-check artifacts, all-summary, and export move from pygienium/ to .pygienium/; ensureRunStateIgnored appends .pygienium/ to the target repo's .gitignore on first run (opt out with --no-gitignore), so a run never stages its own output. Export now reads a single canonical root.
This commit is contained in:
@@ -126,9 +126,9 @@ registerCheck(def) ← checks/*.ts self-register on load
|
|||||||
├─ Q0 recon (shared, run once per run) (src/recon.ts)
|
├─ Q0 recon (shared, run once per run) (src/recon.ts)
|
||||||
│ git state + source-file inventory → .pygienium/recon.json
|
│ git state + source-file inventory → .pygienium/recon.json
|
||||||
├─ analysis sub-agent (buildScanTask) ← scanner/<check> agent
|
├─ analysis sub-agent (buildScanTask) ← scanner/<check> agent
|
||||||
│ writes pygienium/checks/<name>/findings.md
|
│ writes .pygienium/checks/<name>/findings.md
|
||||||
├─ fix sub-agent (buildFixTask) ← fixer, only with --fix
|
├─ fix sub-agent (buildFixTask) ← fixer, only with --fix
|
||||||
│ writes pygienium/checks/<name>/changes.md
|
│ writes .pygienium/checks/<name>/changes.md
|
||||||
├─ verify gate (re-runs check.gate)
|
├─ verify gate (re-runs check.gate)
|
||||||
└─ cleanup (drops transient scratch artifacts)
|
└─ cleanup (drops transient scratch artifacts)
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* - "why" comments that explain intent, rationale, or gotchas → KEEP
|
* - "why" comments that explain intent, rationale, or gotchas → KEEP
|
||||||
* - code self-explanatory with no comment → no comment needed (don't add one)
|
* - code self-explanatory with no comment → no comment needed (don't add one)
|
||||||
*
|
*
|
||||||
* Artifacts (under `<cwd>/pygienium/checks/comments/`):
|
* Artifacts (under `<cwd>/.pygienium/checks/comments/`):
|
||||||
* - `findings.md` — per-file line refs for each smell
|
* - `findings.md` — per-file line refs for each smell
|
||||||
* - `changes.md` — summary of edits + human-review items
|
* - `changes.md` — summary of edits + human-review items
|
||||||
*
|
*
|
||||||
@@ -28,14 +28,14 @@ export const COMMENTS_PHASE_ID = "C1";
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Directory where this check writes its `findings.md` and `changes.md`
|
* Directory where this check writes its `findings.md` and `changes.md`
|
||||||
* artifacts: `<cwd>/pygienium/checks/comments/`. Based on `scope.cwd` (the
|
* artifacts: `<cwd>/.pygienium/checks/comments/`. Based on `scope.cwd` (the
|
||||||
* project root, always a directory) so the path is valid whether the scan
|
* 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
|
* target is a single file or a directory. Matches the spec's
|
||||||
* `pygienium/checks/comments/findings.md` relative-path notation.
|
* `.pygienium/checks/comments/findings.md` relative-path notation.
|
||||||
*/
|
*/
|
||||||
export function commentsArtifactDir(scope: CheckScope): string {
|
export function commentsArtifactDir(scope: CheckScope): string {
|
||||||
const base = scope.cwd.replace(/\/+$/, "");
|
const base = scope.cwd.replace(/\/+$/, "");
|
||||||
return `${base}/pygienium/checks/comments`;
|
return `${base}/.pygienium/checks/comments`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Absolute path to the findings artifact for this check. */
|
/** Absolute path to the findings artifact for this check. */
|
||||||
@@ -71,7 +71,7 @@ Short + high value is the goal. Evaluate every comment in the target:
|
|||||||
/**
|
/**
|
||||||
* Build the analysis sub-agent task. Instructs the agent to read candidate
|
* 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
|
* source files, identify comment smells per the rubric, and write per-file line
|
||||||
* references to `pygienium/comments/findings.md`.
|
* references to `.pygienium/comments/findings.md`.
|
||||||
*/
|
*/
|
||||||
export function buildCommentsScanTask(_cwd: string, scope: CheckScope): string {
|
export function buildCommentsScanTask(_cwd: string, scope: CheckScope): string {
|
||||||
const outDir = commentsArtifactDir(scope);
|
const outDir = commentsArtifactDir(scope);
|
||||||
@@ -119,7 +119,7 @@ Write the report under \`${outDir}\` (create directories as needed).
|
|||||||
/**
|
/**
|
||||||
* Build the fix sub-agent task from the scan findings. Instructs the agent to
|
* 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
|
* apply safe removals/tightenings, leave "why" comments, and write a summary
|
||||||
* of edits plus anything needing human review to `pygienium/comments/changes.md`.
|
* of edits plus anything needing human review to `.pygienium/comments/changes.md`.
|
||||||
*/
|
*/
|
||||||
export function buildCommentsFixTask(
|
export function buildCommentsFixTask(
|
||||||
_cwd: string,
|
_cwd: string,
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import { scopeRulesMarkdown } from "./scope.js";
|
|||||||
export const COMPLEXITY_PHASE_ID = "C4";
|
export const COMPLEXITY_PHASE_ID = "C4";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Artifact directory: `<cwd>/pygienium/checks/complexity/`.
|
* Artifact directory: `<cwd>/.pygienium/checks/complexity/`.
|
||||||
*/
|
*/
|
||||||
export function complexityArtifactDir(scope: CheckScope): string {
|
export function complexityArtifactDir(scope: CheckScope): string {
|
||||||
const base = scope.cwd.replace(/\/+$/, "");
|
const base = scope.cwd.replace(/\/+$/, "");
|
||||||
return `${base}/pygienium/checks/complexity`;
|
return `${base}/.pygienium/checks/complexity`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Absolute path to findings artifact. */
|
/** Absolute path to findings artifact. */
|
||||||
@@ -84,13 +84,12 @@ count decision points (if/else if/for/while/case/&&/||/catch) per function.
|
|||||||
* Build the analysis sub-agent task. Instructs the agent to:
|
* Build the analysis sub-agent task. Instructs the agent to:
|
||||||
* 1. Compute cyclomatic complexity per function
|
* 1. Compute cyclomatic complexity per function
|
||||||
* 2. Identify structural complexity smells
|
* 2. Identify structural complexity smells
|
||||||
* 3. Write findings to pygienium/checks/complexity/findings.md
|
* 3. Write findings to .pygienium/checks/complexity/findings.md
|
||||||
*/
|
*/
|
||||||
export function buildComplexityScanTask(
|
export function buildComplexityScanTask(
|
||||||
_cwd: string,
|
_cwd: string,
|
||||||
scope: CheckScope,
|
scope: CheckScope,
|
||||||
): string {
|
): string {
|
||||||
const outDir = complexityArtifactDir(scope);
|
|
||||||
const findingsFile = findingsPath(scope);
|
const findingsFile = findingsPath(scope);
|
||||||
return `# Task: excessive complexity scan
|
return `# Task: excessive complexity scan
|
||||||
|
|
||||||
@@ -173,7 +172,7 @@ Always create findings.md so the run has an artifact.
|
|||||||
* 1. Split 50+ complexity functions
|
* 1. Split 50+ complexity functions
|
||||||
* 2. Refactor or justify 35–49 functions
|
* 2. Refactor or justify 35–49 functions
|
||||||
* 3. Apply safe refactors for structural smells
|
* 3. Apply safe refactors for structural smells
|
||||||
* 4. Write changes summary to pygienium/checks/complexity/changes.md
|
* 4. Write changes summary to .pygienium/checks/complexity/changes.md
|
||||||
*/
|
*/
|
||||||
export function buildComplexityFixTask(
|
export function buildComplexityFixTask(
|
||||||
_cwd: string,
|
_cwd: string,
|
||||||
|
|||||||
@@ -950,14 +950,14 @@ export async function applyDeadCodeFixes(
|
|||||||
// Artifact paths
|
// Artifact paths
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const CHECK_DIRNAME = "pygienium/checks/dead-code";
|
const CHECK_DIRNAME = ".pygienium/checks/dead-code";
|
||||||
|
|
||||||
/** `<target>/pygienium/checks/dead-code/findings.md` */
|
/** `<target>/.pygienium/checks/dead-code/findings.md` */
|
||||||
export function findingsPath(target: string): string {
|
export function findingsPath(target: string): string {
|
||||||
return join(target, CHECK_DIRNAME, "findings.md");
|
return join(target, CHECK_DIRNAME, "findings.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `<target>/pygienium/checks/dead-code/changes.md` */
|
/** `<target>/.pygienium/checks/dead-code/changes.md` */
|
||||||
export function changesPath(target: string): string {
|
export function changesPath(target: string): string {
|
||||||
return join(target, CHECK_DIRNAME, "changes.md");
|
return join(target, CHECK_DIRNAME, "changes.md");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
*
|
*
|
||||||
* Lifecycle:
|
* Lifecycle:
|
||||||
* gate (need source files) → recon (shared) → scan sub-agent writes
|
* gate (need source files) → recon (shared) → scan sub-agent writes
|
||||||
* `<cwd>/pygienium/checks/deep-modules/findings.md` → [with --fix] fix
|
* `<cwd>/.pygienium/checks/deep-modules/findings.md` → [with --fix] fix
|
||||||
* sub-agent writes `changes.md`, inlines safe pass-throughs, and lists
|
* sub-agent writes `changes.md`, inlines safe pass-throughs, and lists
|
||||||
* risky consolidations (external importers / public API) for human review.
|
* risky consolidations (external importers / public API) for human review.
|
||||||
*
|
*
|
||||||
@@ -32,7 +32,7 @@ import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
|||||||
|
|
||||||
/** Output directory for this check's persistent reports. */
|
/** Output directory for this check's persistent reports. */
|
||||||
export function deepModulesOutputDir(cwd: string): string {
|
export function deepModulesOutputDir(cwd: string): string {
|
||||||
return join(cwd, "pygienium", "checks", "deep-modules");
|
return join(cwd, ".pygienium", "checks", "deep-modules");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `findings.md` path for this check. */
|
/** `findings.md` path for this check. */
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
*
|
*
|
||||||
* Lifecycle:
|
* Lifecycle:
|
||||||
* gate (need source files) → recon (shared) → scan sub-agent writes
|
* gate (need source files) → recon (shared) → scan sub-agent writes
|
||||||
* `<cwd>/pygienium/checks/defensive-guards/findings.md` separating redundant
|
* `<cwd>/.pygienium/checks/defensive-guards/findings.md` separating redundant
|
||||||
* guards from boundary guards → [with --fix] fix sub-agent removes redundant
|
* guards from boundary guards → [with --fix] fix sub-agent removes redundant
|
||||||
* guards, preserves boundary guards, and writes `changes.md` distinguishing
|
* guards, preserves boundary guards, and writes `changes.md` distinguishing
|
||||||
* removed vs kept-with-reason.
|
* removed vs kept-with-reason.
|
||||||
@@ -52,7 +52,7 @@ import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
|||||||
|
|
||||||
/** Output directory for this check's persistent reports. */
|
/** Output directory for this check's persistent reports. */
|
||||||
export function defensiveGuardsOutputDir(cwd: string): string {
|
export function defensiveGuardsOutputDir(cwd: string): string {
|
||||||
return join(cwd, "pygienium", "checks", "defensive-guards");
|
return join(cwd, ".pygienium", "checks", "defensive-guards");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `findings.md` path for this check. */
|
/** `findings.md` path for this check. */
|
||||||
|
|||||||
@@ -60,12 +60,24 @@ function resolveCwd(args: string, ctxCwd: string): string {
|
|||||||
return resolve(ctxCwd, tok);
|
return resolve(ctxCwd, tok);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Strip a leading flag token (`--fix`) from args, returning the remainder. */
|
/**
|
||||||
function splitFlags(args: string): { fix: boolean; rest: string } {
|
* Strip leading flag tokens (`--fix`, `--fresh`, `--no-gitignore`) from args,
|
||||||
|
* returning the remainder (the positional `[path]`).
|
||||||
|
*/
|
||||||
|
function splitFlags(args: string): {
|
||||||
|
fix: boolean;
|
||||||
|
fresh: boolean;
|
||||||
|
rest: string;
|
||||||
|
noGitignore: boolean;
|
||||||
|
} {
|
||||||
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
||||||
const fix = tokens.includes("--fix");
|
const fix = tokens.includes("--fix");
|
||||||
const rest = tokens.filter((t) => t !== "--fix").join(" ");
|
const fresh = tokens.includes("--fresh");
|
||||||
return { fix, rest };
|
const noGitignore = tokens.includes("--no-gitignore");
|
||||||
|
const rest = tokens
|
||||||
|
.filter((t) => t !== "--fix" && t !== "--fresh" && t !== "--no-gitignore")
|
||||||
|
.join(" ");
|
||||||
|
return { fix, fresh, rest, noGitignore };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,12 +88,13 @@ function splitFlags(args: string): { fix: boolean; rest: string } {
|
|||||||
function parseResumeArgs(
|
function parseResumeArgs(
|
||||||
args: string,
|
args: string,
|
||||||
ctxCwd: string,
|
ctxCwd: string,
|
||||||
): { cwd: string; fresh: boolean } {
|
): { cwd: string; fresh: boolean; gitignore: boolean } {
|
||||||
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
||||||
const fresh = tokens.includes("--fresh");
|
const fresh = tokens.includes("--fresh");
|
||||||
|
const gitignore = !tokens.includes("--no-gitignore");
|
||||||
const positional = tokens.find((t) => !t.startsWith("--"));
|
const positional = tokens.find((t) => !t.startsWith("--"));
|
||||||
const cwd = positional ? resolve(ctxCwd, positional) : ctxCwd;
|
const cwd = positional ? resolve(ctxCwd, positional) : ctxCwd;
|
||||||
return { cwd, fresh };
|
return { cwd, fresh, gitignore };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** `/pygienium-help` */
|
/** `/pygienium-help` */
|
||||||
@@ -116,9 +129,12 @@ export async function handleCheckCommand(
|
|||||||
hasUI: ctx.hasUI,
|
hasUI: ctx.hasUI,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const giNote = outcome.gitignoreAppended
|
||||||
|
? " · .pygienium/ added to .gitignore"
|
||||||
|
: "";
|
||||||
print(
|
print(
|
||||||
ctx,
|
ctx,
|
||||||
`pygienium ${check.label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}`,
|
`pygienium ${check.label}: ${outcome.status}${outcome.error ? ` — ${outcome.error}` : ""}${giNote}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,13 +158,17 @@ export async function handleAllCommand(
|
|||||||
target: parsed.target,
|
target: parsed.target,
|
||||||
fix: parsed.fix,
|
fix: parsed.fix,
|
||||||
fresh: parsed.fresh,
|
fresh: parsed.fresh,
|
||||||
|
gitignore: parsed.gitignore,
|
||||||
only: parsed.only,
|
only: parsed.only,
|
||||||
ui: ctx.ui,
|
ui: ctx.ui,
|
||||||
hasUI: ctx.hasUI,
|
hasUI: ctx.hasUI,
|
||||||
});
|
});
|
||||||
|
const giNote = outcome.gitignoreAppended
|
||||||
|
? " · .pygienium/ added to .gitignore"
|
||||||
|
: "";
|
||||||
print(
|
print(
|
||||||
ctx,
|
ctx,
|
||||||
`pygienium: all-run ${outcome.status} — ${outcome.ran.length} ran, ${outcome.skipped.length} skipped; summary → ${allSummaryPath(ctx.cwd)} (${runStatePath(ctx.cwd)})`,
|
`pygienium: all-run ${outcome.status} — ${outcome.ran.length} ran, ${outcome.skipped.length} skipped; summary → ${allSummaryPath(ctx.cwd)} (${runStatePath(ctx.cwd)})${giNote}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +198,7 @@ export async function handleResumeCommand(
|
|||||||
args: string,
|
args: string,
|
||||||
ctx: PygieniumCtx,
|
ctx: PygieniumCtx,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { cwd, fresh } = parseResumeArgs(args, ctx.cwd);
|
const { cwd, fresh, gitignore } = parseResumeArgs(args, ctx.cwd);
|
||||||
let state = await loadRunState(cwd);
|
let state = await loadRunState(cwd);
|
||||||
if (!state) {
|
if (!state) {
|
||||||
print(ctx, "pygienium: no run state to resume.");
|
print(ctx, "pygienium: no run state to resume.");
|
||||||
@@ -202,6 +222,7 @@ export async function handleResumeCommand(
|
|||||||
// unless --fresh.
|
// unless --fresh.
|
||||||
let ran = 0;
|
let ran = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
|
let giAppended = false;
|
||||||
for (const entry of Object.values(state.checks)) {
|
for (const entry of Object.values(state.checks)) {
|
||||||
const def = getCheck(entry.name);
|
const def = getCheck(entry.name);
|
||||||
if (!def) {
|
if (!def) {
|
||||||
@@ -258,7 +279,7 @@ export async function handleExportCommand(
|
|||||||
if (result.entries.length === 0) {
|
if (result.entries.length === 0) {
|
||||||
print(
|
print(
|
||||||
ctx,
|
ctx,
|
||||||
`pygienium: nothing to export (no findings.md/changes.md under ${cwd}/pygienium/checks/).`,
|
`pygienium: nothing to export (no findings.md/changes.md under ${cwd}/.pygienium/checks/).`,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
*
|
*
|
||||||
* `/pygienium-export` walks each check's artifact directory (where
|
* `/pygienium-export` walks each check's artifact directory (where
|
||||||
* `findings.md` and `changes.md` live), applies `--check=` / `--status=`
|
* `findings.md` and `changes.md` live), applies `--check=` / `--status=`
|
||||||
* filters, and writes a single bundle to `pygienium/export.{md|json}`.
|
* filters, and writes a single bundle to `.pygienium/export.{md|json}`.
|
||||||
*
|
*
|
||||||
* Artifact root: `<cwd>/pygienium/checks/<name>/` — the single canonical
|
* Artifact root: `<cwd>/.pygienium/checks/<name>/` — the single canonical
|
||||||
* location every shipped check writes to.
|
* location every shipped check writes to.
|
||||||
*
|
*
|
||||||
* Statuses for `--status=` filtering come from the run-state; a check dir
|
* Statuses for `--status=` filtering come from the run-state; a check dir
|
||||||
@@ -21,23 +21,23 @@ import type { RunState } from "./run-state.js";
|
|||||||
export type ExportFormat = "md" | "json";
|
export type ExportFormat = "md" | "json";
|
||||||
|
|
||||||
/** Directory name (relative to cwd) that holds `checks/` and `export.md`. */
|
/** Directory name (relative to cwd) that holds `checks/` and `export.md`. */
|
||||||
export const PYGIENIUM_ARTIFACT_DIR = "pygienium";
|
export const PYGIENIUM_ARTIFACT_DIR = ".pygienium";
|
||||||
/** Subdirectory holding per-check `findings.md`/`changes.md`. */
|
/** Subdirectory holding per-check `findings.md`/`changes.md`. */
|
||||||
export const CHECKS_SUBDIR = "checks";
|
export const CHECKS_SUBDIR = "checks";
|
||||||
/** Base filename for the bundle (`export.md` / `export.json`). */
|
/** Base filename for the bundle (`export.md` / `export.json`). */
|
||||||
export const EXPORT_FILENAME_BASE = "export";
|
export const EXPORT_FILENAME_BASE = "export";
|
||||||
|
|
||||||
/** Resolve `<cwd>/pygienium/` (the artifact root). */
|
/** Resolve `<cwd>/.pygienium/` (the artifact root). */
|
||||||
export function pygieniumArtifactDir(cwd: string): string {
|
export function pygieniumArtifactDir(cwd: string): string {
|
||||||
return join(cwd, PYGIENIUM_ARTIFACT_DIR);
|
return join(cwd, PYGIENIUM_ARTIFACT_DIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve `<cwd>/pygienium/checks/`. */
|
/** Resolve `<cwd>/.pygienium/checks/`. */
|
||||||
export function canonicalChecksRoot(cwd: string): string {
|
export function canonicalChecksRoot(cwd: string): string {
|
||||||
return join(pygieniumArtifactDir(cwd), CHECKS_SUBDIR);
|
return join(pygieniumArtifactDir(cwd), CHECKS_SUBDIR);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Resolve `<cwd>/pygienium/export.<format>`. */
|
/** Resolve `<cwd>/.pygienium/export.<format>`. */
|
||||||
export function exportBundlePath(cwd: string, format: ExportFormat): string {
|
export function exportBundlePath(cwd: string, format: ExportFormat): string {
|
||||||
return join(pygieniumArtifactDir(cwd), `${EXPORT_FILENAME_BASE}.${format}`);
|
return join(pygieniumArtifactDir(cwd), `${EXPORT_FILENAME_BASE}.${format}`);
|
||||||
}
|
}
|
||||||
@@ -153,7 +153,7 @@ async function gatherFromRoot(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gather artifact entries from the canonical `<cwd>/pygienium/checks/` root,
|
* Gather artifact entries from the canonical `<cwd>/.pygienium/checks/` root,
|
||||||
* one entry per check directory. Entries are sorted alphabetically. Marks an
|
* one entry per check directory. Entries are sorted alphabetically. Marks an
|
||||||
* entry `unknown` when its check is absent from `state`.
|
* entry `unknown` when its check is absent from `state`.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -62,6 +62,12 @@ export const CLI_FLAGS: HelpFlag[] = [
|
|||||||
scope: "all",
|
scope: "all",
|
||||||
description: "Comma-separated check names to run (subset of the registry).",
|
description: "Comma-separated check names to run (subset of the registry).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "--no-gitignore",
|
||||||
|
scope: "<check>, all, resume",
|
||||||
|
description:
|
||||||
|
"Don't add `.pygienium/` to the target repo's .gitignore (added by default so runs never stage their own output).",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "--check=",
|
name: "--check=",
|
||||||
scope: "export",
|
scope: "export",
|
||||||
@@ -118,7 +124,7 @@ export const COMMANDS: HelpCommand[] = [
|
|||||||
{
|
{
|
||||||
usage: "pygienium-export [path] [--check=] [--status=] [--out=md|json]",
|
usage: "pygienium-export [path] [--check=] [--status=] [--out=md|json]",
|
||||||
description:
|
description:
|
||||||
"Bundle every check's findings.md + changes.md into pygienium/export.{md|json}.",
|
"Bundle every check's findings.md + changes.md into .pygienium/export.{md|json}.",
|
||||||
example: "/pygienium-export --out=json",
|
example: "/pygienium-export --out=json",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
* init single run (mode "all") → run shared recon once →
|
* init single run (mode "all") → run shared recon once →
|
||||||
* for each registered check (in registry order): call `runCheck` with the
|
* for each registered check (in registry order): call `runCheck` with the
|
||||||
* SHARED run-state record (not a fresh one per check) → reconcile run
|
* SHARED run-state record (not a fresh one per check) → reconcile run
|
||||||
* status → write `pygienium/all-summary.md`.
|
* status → write `.pygienium/all-summary.md`.
|
||||||
*
|
*
|
||||||
* Resumability: terminal checks (`complete`/`skipped`) are skipped on resume;
|
* Resumability: terminal checks (`complete`/`skipped`) are skipped on resume;
|
||||||
* `in_progress`/`failed`/`pending` checks re-run. `--fresh` resets every check
|
* `in_progress`/`failed`/`pending` checks re-run. `--fresh` resets every check
|
||||||
@@ -29,6 +29,7 @@ import { createPhaseStrip } from "../phases.js";
|
|||||||
import { runRecon } from "../recon.js";
|
import { runRecon } from "../recon.js";
|
||||||
import {
|
import {
|
||||||
applyPhaseStatus,
|
applyPhaseStatus,
|
||||||
|
ensureRunStateIgnored,
|
||||||
initRunState,
|
initRunState,
|
||||||
loadRunState,
|
loadRunState,
|
||||||
markRunStatus,
|
markRunStatus,
|
||||||
@@ -42,11 +43,11 @@ import {
|
|||||||
} from "../run-state.js";
|
} from "../run-state.js";
|
||||||
|
|
||||||
/** Artifact directory name (relative to cwd) that holds `all-summary.md`. */
|
/** Artifact directory name (relative to cwd) that holds `all-summary.md`. */
|
||||||
export const ALL_ARTIFACT_DIR = "pygienium";
|
export const ALL_ARTIFACT_DIR = ".pygienium";
|
||||||
/** Filename for the unified per-check summary report. */
|
/** Filename for the unified per-check summary report. */
|
||||||
export const ALL_SUMMARY_FILENAME = "all-summary.md";
|
export const ALL_SUMMARY_FILENAME = "all-summary.md";
|
||||||
|
|
||||||
/** Resolve `<cwd>/pygienium/all-summary.md`. */
|
/** Resolve `<cwd>/.pygienium/all-summary.md`. */
|
||||||
export function allSummaryPath(cwd: string): string {
|
export function allSummaryPath(cwd: string): string {
|
||||||
return join(cwd, ALL_ARTIFACT_DIR, ALL_SUMMARY_FILENAME);
|
return join(cwd, ALL_ARTIFACT_DIR, ALL_SUMMARY_FILENAME);
|
||||||
}
|
}
|
||||||
@@ -62,10 +63,17 @@ export interface AllRunOptions {
|
|||||||
only?: string[];
|
only?: string[];
|
||||||
/** Reset and re-run every check, ignoring prior terminal state (`--fresh`). */
|
/** Reset and re-run every check, ignoring prior terminal state (`--fresh`). */
|
||||||
fresh?: boolean;
|
fresh?: boolean;
|
||||||
|
/**
|
||||||
|
* Ensure `<cwd>/.gitignore` excludes `.pygienium/` before this run writes
|
||||||
|
* state/artifacts (default true). Set false with `--no-gitignore`.
|
||||||
|
*/
|
||||||
|
gitignore?: boolean;
|
||||||
/** UI context (optional; null in print mode). */
|
/** UI context (optional; null in print mode). */
|
||||||
ui?: ExtensionUIContext;
|
ui?: ExtensionUIContext;
|
||||||
/** Whether dialog-capable UI is available. */
|
/** Whether dialog-capable UI is available. */
|
||||||
hasUI?: boolean;
|
hasUI?: boolean;
|
||||||
|
/** Optional callback to post completion messages into the chat. */
|
||||||
|
sendChatMessage?: SendChatMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Outcome of {@link runAllChecks}. */
|
/** Outcome of {@link runAllChecks}. */
|
||||||
@@ -80,6 +88,8 @@ export interface AllRunOutcome {
|
|||||||
ran: string[];
|
ran: string[];
|
||||||
/** Checks skipped because they were already terminal. */
|
/** Checks skipped because they were already terminal. */
|
||||||
skipped: string[];
|
skipped: string[];
|
||||||
|
/** True when this run appended `.pygienium/` to the repo's .gitignore. */
|
||||||
|
gitignoreAppended?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,11 +114,13 @@ export function parseAllArgs(
|
|||||||
target: string;
|
target: string;
|
||||||
fix: boolean;
|
fix: boolean;
|
||||||
fresh: boolean;
|
fresh: boolean;
|
||||||
|
gitignore: boolean;
|
||||||
only: string[];
|
only: string[];
|
||||||
} {
|
} {
|
||||||
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
const tokens = args.trim().length > 0 ? args.trim().split(/\s+/) : [];
|
||||||
let fix = false;
|
let fix = false;
|
||||||
let fresh = false;
|
let fresh = false;
|
||||||
|
let gitignore = true;
|
||||||
let target = cwd;
|
let target = cwd;
|
||||||
const only: string[] = [];
|
const only: string[] = [];
|
||||||
for (const tok of tokens) {
|
for (const tok of tokens) {
|
||||||
@@ -116,6 +128,8 @@ export function parseAllArgs(
|
|||||||
fix = true;
|
fix = true;
|
||||||
} else if (tok === "--fresh") {
|
} else if (tok === "--fresh") {
|
||||||
fresh = true;
|
fresh = true;
|
||||||
|
} else if (tok === "--no-gitignore") {
|
||||||
|
gitignore = false;
|
||||||
} else if (tok.startsWith("--only=")) {
|
} else if (tok.startsWith("--only=")) {
|
||||||
for (const name of tok.slice("--only=".length).split(",")) {
|
for (const name of tok.slice("--only=".length).split(",")) {
|
||||||
const trimmed = name.trim();
|
const trimmed = name.trim();
|
||||||
@@ -125,7 +139,7 @@ export function parseAllArgs(
|
|||||||
target = tok;
|
target = tok;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { target: resolve(cwd, target), fix, fresh, only };
|
return { target: resolve(cwd, target), fix, fresh, gitignore, only };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Count non-empty lines in captured findings/changes text. */
|
/** Count non-empty lines in captured findings/changes text. */
|
||||||
@@ -219,6 +233,11 @@ export async function runAllChecks(
|
|||||||
const fresh = opts.fresh ?? false;
|
const fresh = opts.fresh ?? false;
|
||||||
const hasUI = opts.hasUI ?? false;
|
const hasUI = opts.hasUI ?? false;
|
||||||
|
|
||||||
|
// Keep pygienium's own output out of the scanned repo's git index unless
|
||||||
|
// the caller opted out with --no-gitignore.
|
||||||
|
const gitignoreAppended =
|
||||||
|
opts.gitignore === false ? false : await ensureRunStateIgnored(cwd);
|
||||||
|
|
||||||
const selected = selectChecks(opts.only);
|
const selected = selectChecks(opts.only);
|
||||||
if (selected.length === 0) {
|
if (selected.length === 0) {
|
||||||
// `--only` selected nothing (or no checks registered). Still produce a
|
// `--only` selected nothing (or no checks registered). Still produce a
|
||||||
@@ -227,7 +246,14 @@ export async function runAllChecks(
|
|||||||
markRunStatus(state, reconcileRunStatus(state));
|
markRunStatus(state, reconcileRunStatus(state));
|
||||||
await saveRunState(state);
|
await saveRunState(state);
|
||||||
const summaryPath = await writeAllSummary(state, []);
|
const summaryPath = await writeAllSummary(state, []);
|
||||||
return { status: state.status, state, summaryPath, ran: [], skipped: [] };
|
return {
|
||||||
|
status: state.status,
|
||||||
|
state,
|
||||||
|
summaryPath,
|
||||||
|
ran: [],
|
||||||
|
skipped: [],
|
||||||
|
gitignoreAppended,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Init / resume the single shared run-state --------------------------
|
// --- Init / resume the single shared run-state --------------------------
|
||||||
@@ -312,6 +338,7 @@ export async function runAllChecks(
|
|||||||
ui: opts.ui,
|
ui: opts.ui,
|
||||||
hasUI,
|
hasUI,
|
||||||
existingState: state,
|
existingState: state,
|
||||||
|
gitignore: opts.gitignore,
|
||||||
});
|
});
|
||||||
state = outcome.state;
|
state = outcome.state;
|
||||||
ran.push(check.name);
|
ran.push(check.name);
|
||||||
|
|||||||
@@ -82,12 +82,14 @@ export interface RunCheckOptions {
|
|||||||
hasUI?: boolean;
|
hasUI?: boolean;
|
||||||
/** Pre-existing run state to update (for `/pygienium-all` and resume). */
|
/** Pre-existing run state to update (for `/pygienium-all` and resume). */
|
||||||
existingState?: RunState;
|
existingState?: RunState;
|
||||||
|
gitignore?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Outcome of a single check run. */
|
/** Outcome of a single check run. */
|
||||||
export interface CheckRunOutcome {
|
export interface CheckRunOutcome {
|
||||||
/** Final check status. */
|
/** Final check status. */
|
||||||
status: "complete" | "failed" | "skipped";
|
status: "complete" | "failed" | "skipped";
|
||||||
|
gitignoreAppended?: boolean;
|
||||||
/** Findings text from the analysis phase. */
|
/** Findings text from the analysis phase. */
|
||||||
findings?: string;
|
findings?: string;
|
||||||
/** Changes text from the fix phase (when run with --fix). */
|
/** Changes text from the fix phase (when run with --fix). */
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
* @module pygienium/run-state
|
* @module pygienium/run-state
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||||
import { dirname, join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
export const RUN_STATE_DIRNAME = ".pygienium";
|
export const RUN_STATE_DIRNAME = ".pygienium";
|
||||||
export const RUN_STATE_FILENAME = "run-state.json";
|
export const RUN_STATE_FILENAME = "run-state.json";
|
||||||
@@ -152,6 +152,46 @@ export async function saveRunState(state: RunState): Promise<void> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memo of cwds whose `.gitignore` was already ensured this process, so the
|
||||||
|
* check runs at most once per target per session.
|
||||||
|
*/
|
||||||
|
const gitIgnoreMemo = new Set<string>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make sure `<cwd>/.gitignore` excludes `.pygienium/` (run-state + artifacts)
|
||||||
|
* so a run never stages its own output into the scanned repo's git index.
|
||||||
|
* Best-effort and idempotent: no-op outside a git work tree or when the entry
|
||||||
|
* already exists. Returns true when it appended the entry (or created the file).
|
||||||
|
*/
|
||||||
|
export async function ensureRunStateIgnored(cwd: string): Promise<boolean> {
|
||||||
|
if (gitIgnoreMemo.has(cwd)) return false;
|
||||||
|
gitIgnoreMemo.add(cwd);
|
||||||
|
try {
|
||||||
|
// Only act inside a git work tree (works for worktrees too: .git is a file).
|
||||||
|
await stat(join(cwd, ".git"));
|
||||||
|
const ignorePath = join(cwd, ".gitignore");
|
||||||
|
const marker = ".pygienium/";
|
||||||
|
let content: string;
|
||||||
|
try {
|
||||||
|
content = await readFile(ignorePath, "utf8");
|
||||||
|
} catch {
|
||||||
|
await writeFile(ignorePath, `${marker}\n`, "utf8");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (content.split(/\r?\n/).some((l) => l.trim() === marker)) return false;
|
||||||
|
const prefix = content.endsWith("\n") ? "" : "\n";
|
||||||
|
await appendFile(
|
||||||
|
ignorePath,
|
||||||
|
`${prefix}# pygienium run-state and check artifacts\n${marker}\n`,
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false; // not a git work tree, or a best-effort write failed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Mark a phase's status (and optionally an error message). */
|
/** Mark a phase's status (and optionally an error message). */
|
||||||
export function applyPhaseStatus(
|
export function applyPhaseStatus(
|
||||||
state: RunState,
|
state: RunState,
|
||||||
|
|||||||
@@ -35,9 +35,9 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
fixAgentName: "fixer",
|
fixAgentName: "fixer",
|
||||||
phaseId: "scan",
|
phaseId: "scan",
|
||||||
buildScanTask: (_cwd, scope) =>
|
buildScanTask: (_cwd, scope) =>
|
||||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||||
buildFixTask: (_cwd, _scope, findings) =>
|
buildFixTask: (_cwd, _scope, findings) =>
|
||||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||||
gate: () => undefined,
|
gate: () => undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Mirrors the spec scenario: run `/pygienium-all` on a small repo and assert:
|
* Mirrors the spec scenario: run `/pygienium-all` on a small repo and assert:
|
||||||
* - every registered check runs exactly once in registry order;
|
* - every registered check runs exactly once in registry order;
|
||||||
* - run-state shows all checks complete and the overall run complete;
|
* - run-state shows all checks complete and the overall run complete;
|
||||||
* - `pygienium/all-summary.md` is present and lists per-check outcomes;
|
* - `.pygienium/all-summary.md` is present and lists per-check outcomes;
|
||||||
* - `--only=alpha,gamma` narrows the candidate set preserving order;
|
* - `--only=alpha,gamma` narrows the candidate set preserving order;
|
||||||
* - interrupted/resumed runs re-dispatch non-terminal checks while skipping
|
* - interrupted/resumed runs re-dispatch non-terminal checks while skipping
|
||||||
* terminal ones, unless `--fresh` resets everything.
|
* terminal ones, unless `--fresh` resets everything.
|
||||||
@@ -57,9 +57,9 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
fixAgentName: "fixer",
|
fixAgentName: "fixer",
|
||||||
phaseId: "scan",
|
phaseId: "scan",
|
||||||
buildScanTask: (_cwd, scope) =>
|
buildScanTask: (_cwd, scope) =>
|
||||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||||
buildFixTask: (_cwd, _scope, findings) =>
|
buildFixTask: (_cwd, _scope, findings) =>
|
||||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||||
gate: () => undefined,
|
gate: () => undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -132,12 +132,16 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
await rm(cwd, { recursive: true, force: true });
|
await rm(cwd, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parseAllArgs parses path, --fix, --fresh, and --only", () => {
|
it("parseAllArgs parses path, --fix, --fresh, --no-gitignore, and --only", () => {
|
||||||
const p = parseAllArgs("subdir --fix --only=alpha,beta --fresh", cwd);
|
const p = parseAllArgs("subdir --fix --only=alpha,beta --fresh", cwd);
|
||||||
expect(p.target).toBe(join(cwd, "subdir"));
|
expect(p.target).toBe(join(cwd, "subdir"));
|
||||||
expect(p.fix).toBe(true);
|
expect(p.fix).toBe(true);
|
||||||
expect(p.fresh).toBe(true);
|
expect(p.fresh).toBe(true);
|
||||||
|
expect(p.gitignore).toBe(true); // default: keep the .gitignore guard on
|
||||||
expect(p.only).toEqual(["alpha", "beta"]);
|
expect(p.only).toEqual(["alpha", "beta"]);
|
||||||
|
const noGi = parseAllArgs("--no-gitignore", cwd);
|
||||||
|
expect(noGi.gitignore).toBe(false);
|
||||||
|
expect(noGi.target).toBe(cwd);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("selectChecks preserves registry order for the --only subset", () => {
|
it("selectChecks preserves registry order for the --only subset", () => {
|
||||||
@@ -176,7 +180,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
expect(state?.recon.complete).toBe(true);
|
expect(state?.recon.complete).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes pygienium/all-summary.md listing per-check outcomes", async () => {
|
it("writes .pygienium/all-summary.md listing per-check outcomes", async () => {
|
||||||
registerCheck(fakeCheck("alpha"));
|
registerCheck(fakeCheck("alpha"));
|
||||||
registerCheck(fakeCheck("beta"));
|
registerCheck(fakeCheck("beta"));
|
||||||
|
|
||||||
@@ -193,7 +197,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
expect(summary).toContain("changes:");
|
expect(summary).toContain("changes:");
|
||||||
// Artifacts actually exist on disk.
|
// Artifacts actually exist on disk.
|
||||||
const alphaFindings = await readFile(
|
const alphaFindings = await readFile(
|
||||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
expect(alphaFindings).toContain("alpha findings");
|
expect(alphaFindings).toContain("alpha findings");
|
||||||
@@ -233,7 +237,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
|
await captureStdout(() => handleAllCommand("--fix", stubCtx(cwd)));
|
||||||
expect(track.dispatched.length).toBe(4); // 2 checks × 2 phases
|
expect(track.dispatched.length).toBe(4); // 2 checks × 2 phases
|
||||||
const firstAlphaFix = await readFile(
|
const firstAlphaFix = await readFile(
|
||||||
join(cwd, "pygienium", "checks", "alpha", "changes.md"),
|
join(cwd, ".pygienium", "checks", "alpha", "changes.md"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -255,7 +259,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
expect(state3?.status).toBe("complete");
|
expect(state3?.status).toBe("complete");
|
||||||
// The fresh re-run overwrote alpha's changes.md (still valid content).
|
// The fresh re-run overwrote alpha's changes.md (still valid content).
|
||||||
const alphaFix2 = await readFile(
|
const alphaFix2 = await readFile(
|
||||||
join(cwd, "pygienium", "checks", "alpha", "changes.md"),
|
join(cwd, ".pygienium", "checks", "alpha", "changes.md"),
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
expect(alphaFix2).toContain("alpha changes");
|
expect(alphaFix2).toContain("alpha changes");
|
||||||
@@ -278,7 +282,7 @@ describe("/pygienium-all orchestrator (task 12)", () => {
|
|||||||
finishedAt: Date.now(),
|
finishedAt: Date.now(),
|
||||||
};
|
};
|
||||||
// Pre-create alpha's on-disk artifacts so its completed entry has artifacts.
|
// Pre-create alpha's on-disk artifacts so its completed entry has artifacts.
|
||||||
const alphaDir = join(cwd, "pygienium", "checks", "alpha");
|
const alphaDir = join(cwd, ".pygienium", "checks", "alpha");
|
||||||
await mkdir(alphaDir, { recursive: true });
|
await mkdir(alphaDir, { recursive: true });
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(alphaDir, "findings.md"),
|
join(alphaDir, "findings.md"),
|
||||||
|
|||||||
@@ -145,15 +145,15 @@ describe("deep-modules check", () => {
|
|||||||
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("findings.md and changes.md live under pygienium/checks/deep-modules/", async () => {
|
it("findings.md and changes.md live under .pygienium/checks/deep-modules/", async () => {
|
||||||
await seedPassThrough(cwd);
|
await seedPassThrough(cwd);
|
||||||
const check = getCheck("deep-modules")!;
|
const check = getCheck("deep-modules")!;
|
||||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||||
expect(findingsPath(cwd)).toBe(
|
expect(findingsPath(cwd)).toBe(
|
||||||
join(cwd, "pygienium", "checks", "deep-modules", "findings.md"),
|
join(cwd, ".pygienium", "checks", "deep-modules", "findings.md"),
|
||||||
);
|
);
|
||||||
expect(changesPath(cwd)).toBe(
|
expect(changesPath(cwd)).toBe(
|
||||||
join(cwd, "pygienium", "checks", "deep-modules", "changes.md"),
|
join(cwd, ".pygienium", "checks", "deep-modules", "changes.md"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -208,16 +208,16 @@ describe("defensive-guards check", () => {
|
|||||||
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("findings.md and changes.md live under pygienium/checks/defensive-guards/", async () => {
|
it("findings.md and changes.md live under .pygienium/checks/defensive-guards/", async () => {
|
||||||
await seedNoise(cwd);
|
await seedNoise(cwd);
|
||||||
await seedBoundary(cwd);
|
await seedBoundary(cwd);
|
||||||
const check = getCheck("defensive-guards")!;
|
const check = getCheck("defensive-guards")!;
|
||||||
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
await handleCheckCommand(check, "--fix", stubCtx(cwd));
|
||||||
expect(findingsPath(cwd)).toBe(
|
expect(findingsPath(cwd)).toBe(
|
||||||
join(cwd, "pygienium", "checks", "defensive-guards", "findings.md"),
|
join(cwd, ".pygienium", "checks", "defensive-guards", "findings.md"),
|
||||||
);
|
);
|
||||||
expect(changesPath(cwd)).toBe(
|
expect(changesPath(cwd)).toBe(
|
||||||
join(cwd, "pygienium", "checks", "defensive-guards", "changes.md"),
|
join(cwd, ".pygienium", "checks", "defensive-guards", "changes.md"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -71,9 +71,9 @@ function fakeCheck(name: string): CheckDefinition {
|
|||||||
fixAgentName: "fixer",
|
fixAgentName: "fixer",
|
||||||
phaseId: "scan",
|
phaseId: "scan",
|
||||||
buildScanTask: (_cwd, scope) =>
|
buildScanTask: (_cwd, scope) =>
|
||||||
`!write pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
`!write .pygienium/checks/${name}/findings.md # ${name} findings\nscan-target:${scope.target}\n!echo ${name}-scan`,
|
||||||
buildFixTask: (_cwd, _scope, findings) =>
|
buildFixTask: (_cwd, _scope, findings) =>
|
||||||
`!write pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
`!write .pygienium/checks/${name}/changes.md # ${name} changes\nbased-on:${findings.split("\n")[0] ?? ""}\n!echo ${name}-fix`,
|
||||||
gate: () => undefined,
|
gate: () => undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ function stubCtx(cwd: string): PygieniumCtx {
|
|||||||
function trackingRunner(): { runner: AgentRunner; dispatched: string[] } {
|
function trackingRunner(): { runner: AgentRunner; dispatched: string[] } {
|
||||||
const dispatched: string[] = [];
|
const dispatched: string[] = [];
|
||||||
const runner: AgentRunner = async (opts) => {
|
const runner: AgentRunner = async (opts) => {
|
||||||
// Tag by check name from the task text (`!write pygienium/checks/<name>/`).
|
// Tag by check name from the task text (`!write .pygienium/checks/<name>/`).
|
||||||
const m = /pygienium\/checks\/([^/]+)\//.exec(opts.task);
|
const m = /pygienium\/checks\/([^/]+)\//.exec(opts.task);
|
||||||
if (m) dispatched.push(m[1] as string);
|
if (m) dispatched.push(m[1] as string);
|
||||||
return fakeAgentRunner(opts);
|
return fakeAgentRunner(opts);
|
||||||
@@ -297,7 +297,7 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
void state;
|
void state;
|
||||||
|
|
||||||
await captureStdout(() => handleExportCommand("", stubCtx(cwd)));
|
await captureStdout(() => handleExportCommand("", stubCtx(cwd)));
|
||||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||||
expect(bundle).toContain("# Pygienium export");
|
expect(bundle).toContain("# Pygienium export");
|
||||||
expect(bundle).toContain("## alpha (complete)");
|
expect(bundle).toContain("## alpha (complete)");
|
||||||
expect(bundle).toContain("## beta (complete)");
|
expect(bundle).toContain("## beta (complete)");
|
||||||
@@ -314,7 +314,7 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
await captureStdout(() =>
|
await captureStdout(() =>
|
||||||
handleExportCommand("--check=beta", stubCtx(cwd)),
|
handleExportCommand("--check=beta", stubCtx(cwd)),
|
||||||
);
|
);
|
||||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||||
expect(bundle).toContain("## beta (complete)");
|
expect(bundle).toContain("## beta (complete)");
|
||||||
expect(bundle).not.toContain("## alpha");
|
expect(bundle).not.toContain("## alpha");
|
||||||
});
|
});
|
||||||
@@ -330,7 +330,7 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
await captureStdout(() =>
|
await captureStdout(() =>
|
||||||
handleExportCommand("--status=failed", stubCtx(cwd)),
|
handleExportCommand("--status=failed", stubCtx(cwd)),
|
||||||
);
|
);
|
||||||
const bundle = await readFile(join(cwd, "pygienium", "export.md"), "utf8");
|
const bundle = await readFile(join(cwd, ".pygienium", "export.md"), "utf8");
|
||||||
expect(bundle).toContain("## alpha (failed)");
|
expect(bundle).toContain("## alpha (failed)");
|
||||||
expect(bundle).not.toContain("## beta");
|
expect(bundle).not.toContain("## beta");
|
||||||
});
|
});
|
||||||
@@ -345,7 +345,7 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
handleExportCommand("--out=json", stubCtx(cwd)),
|
handleExportCommand("--out=json", stubCtx(cwd)),
|
||||||
);
|
);
|
||||||
expect(out.join("\n")).toContain("export.json");
|
expect(out.join("\n")).toContain("export.json");
|
||||||
const raw = await readFile(join(cwd, "pygienium", "export.json"), "utf8");
|
const raw = await readFile(join(cwd, ".pygienium", "export.json"), "utf8");
|
||||||
const parsed = JSON.parse(raw) as {
|
const parsed = JSON.parse(raw) as {
|
||||||
checks: Array<{ name: string; status: string; findings: string }>;
|
checks: Array<{ name: string; status: string; findings: string }>;
|
||||||
};
|
};
|
||||||
@@ -370,20 +370,22 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
expect(f.out).toBe("json");
|
expect(f.out).toBe("json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gatherExportEntries reads only the canonical pygienium/checks/ root", async () => {
|
it("gatherExportEntries reads only the canonical .pygienium/checks/ root", async () => {
|
||||||
await mkdir(join(cwd, "pygienium", "checks", "alpha"), { recursive: true });
|
await mkdir(join(cwd, ".pygienium", "checks", "alpha"), {
|
||||||
await writeFile(
|
|
||||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
|
||||||
"# alpha findings\n",
|
|
||||||
"utf8",
|
|
||||||
);
|
|
||||||
// A stray .pygienium/checks/ dir (the removed legacy root) is ignored now
|
|
||||||
// that all checks write to the single canonical `pygienium/checks/` root.
|
|
||||||
await mkdir(join(cwd, ".pygienium", "checks", "ghost"), {
|
|
||||||
recursive: true,
|
recursive: true,
|
||||||
});
|
});
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(cwd, ".pygienium", "checks", "ghost", "findings.md"),
|
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||||
|
"# alpha findings\n",
|
||||||
|
"utf8",
|
||||||
|
);
|
||||||
|
// A stray pygienium/checks/ dir (the old non-hidden root) is ignored now
|
||||||
|
// that all checks write to the single canonical `.pygienium/checks/` root.
|
||||||
|
await mkdir(join(cwd, "pygienium", "checks", "ghost"), {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
await writeFile(
|
||||||
|
join(cwd, "pygienium", "checks", "ghost", "findings.md"),
|
||||||
"# ghost findings\n",
|
"# ghost findings\n",
|
||||||
"utf8",
|
"utf8",
|
||||||
);
|
);
|
||||||
@@ -393,7 +395,7 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
expect(alpha?.findings).toContain("alpha findings");
|
expect(alpha?.findings).toContain("alpha findings");
|
||||||
expect(alpha?.status).toBe("unknown");
|
expect(alpha?.status).toBe("unknown");
|
||||||
expect(alpha?.findingsPath).toBe(
|
expect(alpha?.findingsPath).toBe(
|
||||||
join(cwd, "pygienium", "checks", "alpha", "findings.md"),
|
join(cwd, ".pygienium", "checks", "alpha", "findings.md"),
|
||||||
);
|
);
|
||||||
expect(entries.find((e) => e.name === "ghost")).toBeUndefined();
|
expect(entries.find((e) => e.name === "ghost")).toBeUndefined();
|
||||||
});
|
});
|
||||||
@@ -415,6 +417,6 @@ describe("status / resume / export (task 13)", () => {
|
|||||||
it("exportRun writes nothing useful and reports zero entries cleanly", async () => {
|
it("exportRun writes nothing useful and reports zero entries cleanly", async () => {
|
||||||
const result = await exportRun(cwd, undefined, {});
|
const result = await exportRun(cwd, undefined, {});
|
||||||
expect(result.entries).toHaveLength(0);
|
expect(result.entries).toHaveLength(0);
|
||||||
expect(relative(cwd, result.path)).toBe(join("pygienium", "export.md"));
|
expect(relative(cwd, result.path)).toBe(join(".pygienium", "export.md"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user