/** * 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 = 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 = 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 = 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 \`/.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\`). `; }