port: sync from Mike/pygienium@63c2f73a
This commit is contained in:
@@ -21,14 +21,13 @@
|
|||||||
* @module pygienium/checks/deep-modules
|
* @module pygienium/checks/deep-modules
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import {
|
import {
|
||||||
registerCheck,
|
registerCheck,
|
||||||
type CheckDefinition,
|
type CheckDefinition,
|
||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
import { hasScopeSources, 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 {
|
||||||
@@ -50,23 +49,13 @@ export function changesPath(cwd: string): string {
|
|||||||
* with zero source files gives the scanner nothing to classify.
|
* with zero source files gives the scanner nothing to classify.
|
||||||
*/
|
*/
|
||||||
function deepModulesGate(cwd: string): string | undefined {
|
function deepModulesGate(cwd: string): string | undefined {
|
||||||
let found = false;
|
|
||||||
try {
|
try {
|
||||||
const entries = readdirSync(cwd);
|
if (hasScopeSources(cwd)) return undefined;
|
||||||
for (const entry of entries) {
|
|
||||||
if (isScopeSource(entry)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// unreadable cwd → let the agent decide; don't block.
|
// unreadable cwd → let the agent decide; don't block.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (!found) {
|
|
||||||
return "no source files found to inspect";
|
return "no source files found to inspect";
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -41,14 +41,13 @@
|
|||||||
* @module pygienium/checks/defensive-guards
|
* @module pygienium/checks/defensive-guards
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import {
|
import {
|
||||||
registerCheck,
|
registerCheck,
|
||||||
type CheckDefinition,
|
type CheckDefinition,
|
||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
import { isScopeSource, scopeRulesMarkdown } from "./scope.js";
|
import { hasScopeSources, 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 {
|
||||||
@@ -70,23 +69,13 @@ export function changesPath(cwd: string): string {
|
|||||||
* with zero source files gives the scanner nothing to analyse.
|
* with zero source files gives the scanner nothing to analyse.
|
||||||
*/
|
*/
|
||||||
function defensiveGuardsGate(cwd: string): string | undefined {
|
function defensiveGuardsGate(cwd: string): string | undefined {
|
||||||
let found = false;
|
|
||||||
try {
|
try {
|
||||||
const entries = readdirSync(cwd);
|
if (hasScopeSources(cwd)) return undefined;
|
||||||
for (const entry of entries) {
|
|
||||||
if (isScopeSource(entry)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// unreadable cwd → let the agent decide; don't block.
|
// unreadable cwd → let the agent decide; don't block.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (!found) {
|
|
||||||
return "no source files found to inspect";
|
return "no source files found to inspect";
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -9,6 +9,10 @@
|
|||||||
* @module pygienium/checks/scope
|
* @module pygienium/checks/scope
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { readdirSync, statSync } from "node:fs";
|
||||||
|
import type { Dirent } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation-code file extensions pygienium inspects.
|
* Implementation-code file extensions pygienium inspects.
|
||||||
*
|
*
|
||||||
@@ -106,6 +110,38 @@ export function isScopeSource(path: string): boolean {
|
|||||||
return SCOPE_EXTENSIONS.has(lower.slice(dot));
|
return SCOPE_EXTENSIONS.has(lower.slice(dot));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when the tree rooted at `root` contains at least one in-scope source
|
||||||
|
* file. Walks recursively (honoring {@link SCOPE_EXCLUDE_DIRS}) — a top-level
|
||||||
|
* entry scan alone would skip any repo whose source lives in subdirectories,
|
||||||
|
* e.g. `game/` or `src/`, even though recon's git inventory finds hundreds of
|
||||||
|
* files. A file root is judged by {@link isScopeSource} directly.
|
||||||
|
*/
|
||||||
|
export function hasScopeSources(root: string): boolean {
|
||||||
|
const st = statSync(root, { throwIfNoEntry: false });
|
||||||
|
if (!st) return false;
|
||||||
|
if (st.isFile()) return isScopeSource(root);
|
||||||
|
const stack = [root];
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const dir = stack.pop() as string;
|
||||||
|
let entries: Dirent[];
|
||||||
|
try {
|
||||||
|
entries = readdirSync(dir, { withFileTypes: true });
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
if (SCOPE_EXCLUDE_DIRS.has(entry.name)) continue;
|
||||||
|
stack.push(join(dir, entry.name));
|
||||||
|
} else if (entry.isFile() && isScopeSource(entry.name)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Markdown section injected into every scan task string so the sub-agent knows
|
* Markdown section injected into every scan task string so the sub-agent knows
|
||||||
* exactly what to inspect and what to skip — stated once here, not copy-pasted
|
* exactly what to inspect and what to skip — stated once here, not copy-pasted
|
||||||
|
|||||||
@@ -36,7 +36,6 @@
|
|||||||
* @module pygienium/checks/todos
|
* @module pygienium/checks/todos
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readdirSync } from "node:fs";
|
|
||||||
import { readFile, readdir, stat } from "node:fs/promises";
|
import { readFile, readdir, stat } from "node:fs/promises";
|
||||||
import { join, relative } from "node:path";
|
import { join, relative } from "node:path";
|
||||||
import { loadRunState } from "../run-state.js";
|
import { loadRunState } from "../run-state.js";
|
||||||
@@ -46,6 +45,7 @@ import {
|
|||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
import {
|
import {
|
||||||
|
hasScopeSources,
|
||||||
isScopeSource,
|
isScopeSource,
|
||||||
SCOPE_EXCLUDE_DIRS,
|
SCOPE_EXCLUDE_DIRS,
|
||||||
scopeRulesMarkdown,
|
scopeRulesMarkdown,
|
||||||
@@ -405,23 +405,13 @@ function renderFindings(
|
|||||||
* with zero source files gives the scanner nothing to analyse.
|
* with zero source files gives the scanner nothing to analyse.
|
||||||
*/
|
*/
|
||||||
function todosGate(cwd: string): string | undefined {
|
function todosGate(cwd: string): string | undefined {
|
||||||
let found = false;
|
|
||||||
try {
|
try {
|
||||||
const entries = readdirSync(cwd);
|
if (hasScopeSources(cwd)) return undefined;
|
||||||
for (const entry of entries) {
|
|
||||||
if (isScopeSource(entry)) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// unreadable cwd → let the agent decide; don't block.
|
// unreadable cwd → let the agent decide; don't block.
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
if (!found) {
|
|
||||||
return "no source files found to inspect";
|
return "no source files found to inspect";
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -168,4 +168,17 @@ describe("deep-modules check", () => {
|
|||||||
await rm(empty, { recursive: true, force: true }).catch(() => {});
|
await rm(empty, { recursive: true, force: true }).catch(() => {});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||||
|
// Top level holds only a directory — the old shallow gate skipped any
|
||||||
|
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||||
|
await mkdir(join(cwd, "game"), { recursive: true });
|
||||||
|
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||||
|
const check = getCheck("deep-modules")!;
|
||||||
|
|
||||||
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||||
|
|
||||||
|
const state = await loadRunState(cwd);
|
||||||
|
expect(state?.checks["deep-modules"]?.status).toBe("complete");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -229,4 +229,17 @@ describe("defensive-guards check", () => {
|
|||||||
const state = await loadRunState(cwd);
|
const state = await loadRunState(cwd);
|
||||||
expect(state?.checks["defensive-guards"]?.status).toBe("skipped");
|
expect(state?.checks["defensive-guards"]?.status).toBe("skipped");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||||
|
// Top level holds only a directory — the old shallow gate skipped any
|
||||||
|
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||||
|
await mkdir(join(cwd, "game"), { recursive: true });
|
||||||
|
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||||
|
const check = getCheck("defensive-guards")!;
|
||||||
|
|
||||||
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||||
|
|
||||||
|
const state = await loadRunState(cwd);
|
||||||
|
expect(state?.checks["defensive-guards"]?.status).toBe("complete");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -335,6 +335,19 @@ describe("todos check", () => {
|
|||||||
expect(state?.checks["todos"]?.status).toBe("skipped");
|
expect(state?.checks["todos"]?.status).toBe("skipped");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not skip when sources live in subdirectories (gate scans recursively)", async () => {
|
||||||
|
// Top level holds only a directory — the old shallow gate skipped any
|
||||||
|
// repo whose source lives under `game/`/`src/`-style subdirs.
|
||||||
|
await mkdir(join(cwd, "game"), { recursive: true });
|
||||||
|
await writeFile(join(cwd, "game", "main.lua"), "return {}\n", "utf8");
|
||||||
|
const check = getCheck("todos")!;
|
||||||
|
|
||||||
|
await handleCheckCommand(check, "", stubCtx(cwd));
|
||||||
|
|
||||||
|
const state = await loadRunState(cwd);
|
||||||
|
expect(state?.checks["todos"]?.status).toBe("complete");
|
||||||
|
});
|
||||||
|
|
||||||
it("reports a new/resolved delta against the previous run's counts", async () => {
|
it("reports a new/resolved delta against the previous run's counts", async () => {
|
||||||
await seedStubs(cwd);
|
await seedStubs(cwd);
|
||||||
const check = getCheck("todos")!;
|
const check = getCheck("todos")!;
|
||||||
|
|||||||
Reference in New Issue
Block a user