Files
pygienium/src/checks/scope.ts
Michael Freno 63c2f73a5e
All checks were successful
port-to-omp / port (push) Successful in 6s
better recon info in certain (nested) situations
2026-08-12 21:59:49 -04:00

189 lines
5.9 KiB
TypeScript

/**
* scope.ts — canonical source-of-truth for what pygienium checks inspect.
*
* Every check (recon, dead-code, deep-modules, defensive-guards, complexity,
* comments) and every scanner agent prompt shares these definitions so the
* "only inspect implementation code" rule is stated once, not copy-pasted
* across four files that drift apart.
*
* @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.
*
* Deliberately excludes documentation (`.md`, `.txt`, `.rst`), config
* (`.json`, `.yaml`, `.yml`, `.toml`, `.env`, `.ini`), type declarations
* (`.d.ts`), styles (`.css`, `.scss`), markup (`.html`, `.svg`), and lock
* files. These are not implementation code — a comments or complexity check
* flagging prose in a `.md` or a key in `package.json` is noise.
*/
export const SCOPE_EXTENSIONS: ReadonlySet<string> = new Set([
".ts",
".tsx",
".js",
".jsx",
".mjs",
".cjs",
".py",
".rb",
".go",
".rs",
".java",
".kt",
".swift",
".php",
".cs",
".lua",
]);
/**
* Directories pygienium never descends into — build output, dependency caches,
* tooling state, and VCS metadata. When walking the tree with `find`/`grep`/
* `readdir`, skip these by name to avoid wasting tokens on vendored code and
* generated artifacts the user can't act on.
*/
export const SCOPE_EXCLUDE_DIRS: ReadonlySet<string> = new Set([
"node_modules",
".git",
".hg",
".svn",
"dist",
"build",
"out",
"coverage",
".next",
".nuxt",
".turbo",
".svelte-kit",
// Framework/deploy build output: Nitro, Vercel, Netlify artifact dirs.
// Generated bundles dominate a naive marker/stub scan (freno-dev alone
// flagged 119 of 124 candidates inside `.output/`/`.vercel/` minified
// bundles) and must never feed the pre-scan or the agent's file walk.
".output",
".vercel",
".netlify",
"__pycache__",
".venv",
"venv",
"vendor",
".cache",
".pygienium",
".ralpi",
".idea",
".vscode",
]);
/**
* Compound extensions (checked after the simple extension lookup) that should
* be treated as non-source even though their tail extension appears in
* {@link SCOPE_EXTENSIONS}. The primary case: `.d.ts` type declarations are
* generated contracts, not implementation code.
*/
export const SCOPE_EXCLUDE_SUFFIXES: ReadonlySet<string> = new Set([
".d.ts",
".d.mts",
".d.cts",
".min.js",
".min.mjs",
".min.cjs",
]);
/**
* Test if a file path is implementation source pygienium should inspect.
*
* Returns `true` when the extension is in {@link SCOPE_EXTENSIONS} AND the
* path does not end with a {@link SCOPE_EXCLUDE_SUFFIXES} pattern (e.g.
* `.d.ts`).
*/
export function isScopeSource(path: string): boolean {
const lower = path.toLowerCase();
for (const suffix of SCOPE_EXCLUDE_SUFFIXES) {
if (lower.endsWith(suffix)) return false;
}
const dot = lower.lastIndexOf(".");
if (dot === -1) return false;
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
* exactly what to inspect and what to skip — stated once here, not copy-pasted
* into each task builder.
*
* Agents that use `find`/`grep`/`readdir` for their own file discovery read
* this before exploring, so the exclusion list governs their search too.
*/
export function scopeRulesMarkdown(): string {
const extensions = [...SCOPE_EXTENSIONS]
.sort((a, b) => a.localeCompare(b))
.join("`, `");
const excludeDirs = [...SCOPE_EXCLUDE_DIRS]
.sort((a, b) => a.localeCompare(b))
.join("`, `");
return `## Scope of inspection
**Only inspect implementation source files.** Do not analyse documentation,
config, type declarations, build output, or dependencies — flagging those is
noise the user cannot act on.
### Inspect (extensions)
\`${extensions}\`
### Skip (directory names — never descend into)
\`${excludeDirs}\`
### Skip (file patterns)
- Type declarations: \`*.d.ts\`, \`*.d.mts\`, \`*.d.cts\` — generated contracts, not impl
- Minified bundles: \`*.min.js\`, \`*.min.mjs\`, \`*.min.cjs\` — generated, not editable
- Docs: \`*.md\`, \`*.txt\`, \`*.rst\` — prose, not code
- Config: \`*.json\`, \`*.yaml\`, \`*.yml\`, \`*.toml\`, \`*.ini\`, \`*.env\`
- Styles/markup: \`*.css\`, \`*.scss\`, \`*.html\`, \`*.svg\`
- Lock files: \`package-lock.json\`, \`*.lock\`, \`bun.lockb\`
### File discovery preference
1. **Prefer the recon snapshot** at \`<cwd>/.pygienium/recon.json\` when it
exists — it is the authoritative source inventory (git-tracked, extension-
filtered, exclude-aware). Read its \`fileCounts\` for the quick picture.
2. Otherwise enumerate files yourself, applying the rules above.
3. When using \`find\`/\`grep\`, add prune clauses for the skip directories
(e.g. \`find . -type d -name node_modules -prune -o -name '*.ts' -print\`).
`;
}