Initial commit: pygenium as git submodule
This commit is contained in:
128
src/recon.ts
Normal file
128
src/recon.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* recon.ts — shared Q0 reconnaissance phase.
|
||||
*
|
||||
* Runs once per hygiene run (before any check's analysis phase) and writes a
|
||||
* project snapshot to `<cwd>/.pygienium/recon.json`. Each check can read this
|
||||
* snapshot so the recon work isn't repeated per check. The snapshot is minimal
|
||||
* and dependency-free (git state + source-file inventory) — real checks layer
|
||||
* their own analysis on top via sub-agents.
|
||||
*
|
||||
* @module pygienium/recon
|
||||
*/
|
||||
|
||||
import { exec } from "node:child_process";
|
||||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { promisify } from "node:util";
|
||||
import { join } from "node:path";
|
||||
import { stateDir, RECON_FILENAME } from "./run-state.js";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface ReconSnapshot {
|
||||
cwd: string;
|
||||
createdAt: number;
|
||||
gitBranch?: string;
|
||||
gitDirty?: boolean;
|
||||
/** Count of source files by extension. */
|
||||
fileCounts: Record<string, number>;
|
||||
totalSourceFiles: number;
|
||||
}
|
||||
|
||||
/** Source extensions worth inventorying for hygiene checks. */
|
||||
const SOURCE_EXTENSIONS = new Set([
|
||||
".ts",
|
||||
".tsx",
|
||||
".js",
|
||||
".jsx",
|
||||
".mjs",
|
||||
".cjs",
|
||||
".py",
|
||||
".rb",
|
||||
".go",
|
||||
".rs",
|
||||
".java",
|
||||
".kt",
|
||||
".swift",
|
||||
".php",
|
||||
".cs",
|
||||
".lua",
|
||||
]);
|
||||
|
||||
/** Run `git ls-files` when possible to get a clean source inventory. */
|
||||
async function listSourceFiles(cwd: string): Promise<string[]> {
|
||||
try {
|
||||
const { stdout } = await execAsync(
|
||||
`git -C ${JSON.stringify(cwd)} ls-files --cached --others --exclude-standard`,
|
||||
{ maxBuffer: 64 * 1024 * 1024 },
|
||||
);
|
||||
return stdout
|
||||
.split("\n")
|
||||
.map((l) => l.trim())
|
||||
.filter((l) => l.length > 0)
|
||||
.filter((l) => {
|
||||
const dot = l.lastIndexOf(".");
|
||||
if (dot === -1) return false;
|
||||
return SOURCE_EXTENSIONS.has(l.slice(dot).toLowerCase());
|
||||
});
|
||||
} catch {
|
||||
/* not a git repo or git unavailable — empty inventory */
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/** Run the shared recon phase for `cwd`, writing the snapshot if missing. */
|
||||
export async function runRecon(cwd: string): Promise<ReconSnapshot> {
|
||||
const dir = stateDir(cwd);
|
||||
const path = join(dir, RECON_FILENAME);
|
||||
|
||||
// Reuse a fresh-enough snapshot (< 5 min) when present.
|
||||
try {
|
||||
const raw = await readFile(path, "utf8");
|
||||
const existing = JSON.parse(raw) as ReconSnapshot;
|
||||
if (
|
||||
existing.createdAt &&
|
||||
Date.now() - existing.createdAt < 5 * 60 * 1000 &&
|
||||
existing.cwd === cwd
|
||||
) {
|
||||
return existing;
|
||||
}
|
||||
} catch {
|
||||
/* no existing snapshot */
|
||||
}
|
||||
|
||||
const files = await listSourceFiles(cwd);
|
||||
const fileCounts: Record<string, number> = {};
|
||||
for (const f of files) {
|
||||
const dot = f.lastIndexOf(".");
|
||||
const ext = dot === -1 ? "" : f.slice(dot).toLowerCase();
|
||||
fileCounts[ext] = (fileCounts[ext] ?? 0) + 1;
|
||||
}
|
||||
|
||||
let gitBranch: string | undefined;
|
||||
let gitDirty: boolean | undefined;
|
||||
try {
|
||||
const branchOut = await execAsync(
|
||||
`git -C ${JSON.stringify(cwd)} rev-parse --abbrev-ref HEAD`,
|
||||
);
|
||||
gitBranch = branchOut.stdout.trim() || undefined;
|
||||
const statusOut = await execAsync(
|
||||
`git -C ${JSON.stringify(cwd)} status --porcelain`,
|
||||
);
|
||||
gitDirty = statusOut.stdout.trim().length > 0;
|
||||
} catch {
|
||||
/* not a git repo */
|
||||
}
|
||||
|
||||
const snapshot: ReconSnapshot = {
|
||||
cwd,
|
||||
createdAt: Date.now(),
|
||||
gitBranch,
|
||||
gitDirty,
|
||||
fileCounts,
|
||||
totalSourceFiles: files.length,
|
||||
};
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
await writeFile(path, JSON.stringify(snapshot, null, 2) + "\n", "utf8");
|
||||
return snapshot;
|
||||
}
|
||||
Reference in New Issue
Block a user