feat(checks): unify inspection scope in checks/scope.ts
Single source of truth for what pygienium inspects (implementation-code extensions, exclude dirs, .d.ts/.min.* exclusions) shared by recon, dead-code, deep-modules, defensive-guards, comments, and complexity. Every scan task and agent prompt injects the shared scope rules instead of copy-pasted per-check lists.
This commit is contained in:
@@ -58,6 +58,39 @@ aggregates many scattered modules), or public API stability boundaries.
|
|||||||
importers or that sits on a public API boundary is riskier to consolidate —
|
importers or that sits on a public API boundary is riskier to consolidate —
|
||||||
note the importer count so the fixer can decide.
|
note the importer count so the fixer can decide.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
`.cs`, `.cjs`, `.go`, `.java`, `.js`, `.jsx`, `.kt`, `.lua`, `.mjs`, `.php`,
|
||||||
|
`.py`, `.rb`, `.rs`, `.swift`, `.ts`, `.tsx`
|
||||||
|
|
||||||
|
## Skip (directory names — never descend into)
|
||||||
|
|
||||||
|
`.cache`, `.git`, `.hg`, `.idea`, `.next`, `.nuxt`, `.pygienium`, `.ralpi`,
|
||||||
|
`.svelte-kit`, `.svn`, `.turbo`, `.vscode`, `__pycache__`, `build`, `coverage`,
|
||||||
|
`dist`, `node_modules`, `out`, `vendor`, `venv` (and `.venv`)
|
||||||
|
|
||||||
|
## Skip (file patterns)
|
||||||
|
|
||||||
|
- Type declarations: `*.d.ts`, `*.d.mts`, `*.d.cts` — generated contracts, not impl
|
||||||
|
- Minified bundles: `*.min.js`, `*.min.mjs`, `*.min.cjs`
|
||||||
|
- 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.
|
||||||
|
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`).
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
|
|
||||||
Write your full findings report to the **findings path** given in the task
|
Write your full findings report to the **findings path** given in the task
|
||||||
|
|||||||
@@ -74,6 +74,39 @@ guarantees is noise.
|
|||||||
checked (`grep` for its declaration/annotation). A `null` check on a
|
checked (`grep` for its declaration/annotation). A `null` check on a
|
||||||
`string | null` union is legitimate; on a bare `string` it is redundant.
|
`string | null` union is legitimate; on a bare `string` it is redundant.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
`.cs`, `.cjs`, `.go`, `.java`, `.js`, `.jsx`, `.kt`, `.lua`, `.mjs`, `.php`,
|
||||||
|
`.py`, `.rb`, `.rs`, `.swift`, `.ts`, `.tsx`
|
||||||
|
|
||||||
|
## Skip (directory names — never descend into)
|
||||||
|
|
||||||
|
`.cache`, `.git`, `.hg`, `.idea`, `.next`, `.nuxt`, `.pygienium`, `.ralpi`,
|
||||||
|
`.svelte-kit`, `.svn`, `.turbo`, `.vscode`, `__pycache__`, `build`, `coverage`,
|
||||||
|
`dist`, `node_modules`, `out`, `vendor`, `venv` (and `.venv`)
|
||||||
|
|
||||||
|
## Skip (file patterns)
|
||||||
|
|
||||||
|
- Type declarations: `*.d.ts`, `*.d.mts`, `*.d.cts` — generated contracts, not impl
|
||||||
|
- Minified bundles: `*.min.js`, `*.min.mjs`, `*.min.cjs`
|
||||||
|
- 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.
|
||||||
|
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`).
|
||||||
|
|
||||||
# Output
|
# Output
|
||||||
|
|
||||||
Write your full findings report to the **findings path** given in the task
|
Write your full findings report to the **findings path** given in the task
|
||||||
|
|||||||
@@ -22,6 +22,39 @@ files; that is the fixer's job. You only inspect and report.
|
|||||||
Never mutate files.
|
Never mutate files.
|
||||||
- Emit a concise findings report as your final message.
|
- Emit a concise findings report as your final message.
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
`.cs`, `.cjs`, `.go`, `.java`, `.js`, `.jsx`, `.kt`, `.lua`, `.mjs`, `.php`,
|
||||||
|
`.py`, `.rb`, `.rs`, `.swift`, `.ts`, `.tsx`
|
||||||
|
|
||||||
|
## Skip (directory names — never descend into)
|
||||||
|
|
||||||
|
`.cache`, `.git`, `.hg`, `.idea`, `.next`, `.nuxt`, `.pygienium`, `.ralpi`,
|
||||||
|
`.svelte-kit`, `.svn`, `.turbo`, `.vscode`, `__pycache__`, `build`, `coverage`,
|
||||||
|
`dist`, `node_modules`, `out`, `vendor`, `venv` (and `.venv`)
|
||||||
|
|
||||||
|
## Skip (file patterns)
|
||||||
|
|
||||||
|
- Type declarations: `*.d.ts`, `*.d.mts`, `*.d.cts` — generated contracts, not impl
|
||||||
|
- Minified bundles: `*.min.js`, `*.min.mjs`, `*.min.cjs`
|
||||||
|
- 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.
|
||||||
|
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`).
|
||||||
|
|
||||||
# Findings format
|
# Findings format
|
||||||
|
|
||||||
End your response with a fenced `findings` block summarising what you found:
|
End your response with a fenced `findings` block summarising what you found:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { registerCheck, type CheckScope } from "./registry.js";
|
import { registerCheck, type CheckScope } from "./registry.js";
|
||||||
|
import { scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Phase-strip phase this check belongs to. */
|
/** Phase-strip phase this check belongs to. */
|
||||||
export const COMMENTS_PHASE_ID = "C1";
|
export const COMMENTS_PHASE_ID = "C1";
|
||||||
@@ -82,6 +83,7 @@ You are running the **comments** hygiene check.
|
|||||||
## Target
|
## Target
|
||||||
- Scan target: \`${scope.target}\`
|
- Scan target: \`${scope.target}\`
|
||||||
|
|
||||||
|
${scopeRulesMarkdown()}
|
||||||
## What to do
|
## What to do
|
||||||
1. Read the recon snapshot at \`${scope.cwd}/.pygienium/recon.json\` if it
|
1. Read the recon snapshot at \`${scope.cwd}/.pygienium/recon.json\` if it
|
||||||
exists; otherwise enumerate source files directly under the target.
|
exists; otherwise enumerate source files directly under the target.
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { registerCheck, type CheckScope } from "./registry.js";
|
import { registerCheck, type CheckScope } from "./registry.js";
|
||||||
|
import { scopeRulesMarkdown } from "./scope.js";
|
||||||
|
|
||||||
/** Phase-strip phase this check belongs to. */
|
/** Phase-strip phase this check belongs to. */
|
||||||
export const COMPLEXITY_PHASE_ID = "C4";
|
export const COMPLEXITY_PHASE_ID = "C4";
|
||||||
@@ -98,6 +99,7 @@ You are running the **complexity** hygiene check.
|
|||||||
## Target
|
## Target
|
||||||
- Scan target: \`${scope.target}\`
|
- Scan target: \`${scope.target}\`
|
||||||
|
|
||||||
|
${scopeRulesMarkdown()}
|
||||||
## What to do
|
## What to do
|
||||||
|
|
||||||
### 1. Compute cyclomatic complexity
|
### 1. Compute cyclomatic complexity
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ import {
|
|||||||
type CheckDefinition,
|
type CheckDefinition,
|
||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
|
import {
|
||||||
|
isScopeSource,
|
||||||
|
SCOPE_EXCLUDE_DIRS,
|
||||||
|
scopeRulesMarkdown,
|
||||||
|
} from "./scope.js";
|
||||||
|
|
||||||
/** Dead-code categories recognised by the rubric. */
|
/** Dead-code categories recognised by the rubric. */
|
||||||
export type DeadCategory = "export" | "file" | "shim" | "dep";
|
export type DeadCategory = "export" | "file" | "shim" | "dep";
|
||||||
@@ -82,43 +87,6 @@ export const CATEGORY_LABELS: Record<DeadCategory, string> = {
|
|||||||
dep: "Unused dependencies",
|
dep: "Unused dependencies",
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Source extensions worth scanning for import graphs. */
|
|
||||||
const SOURCE_EXTENSIONS = new Set([
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".js",
|
|
||||||
".jsx",
|
|
||||||
".mjs",
|
|
||||||
".cjs",
|
|
||||||
".py",
|
|
||||||
".rb",
|
|
||||||
".go",
|
|
||||||
".rs",
|
|
||||||
".java",
|
|
||||||
".kt",
|
|
||||||
".swift",
|
|
||||||
".php",
|
|
||||||
".cs",
|
|
||||||
".lua",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Directories never scanned (build output, deps, tooling). */
|
|
||||||
const IGNORED_DIRS = new Set([
|
|
||||||
"node_modules",
|
|
||||||
".git",
|
|
||||||
".pygienium",
|
|
||||||
"dist",
|
|
||||||
"build",
|
|
||||||
"out",
|
|
||||||
"coverage",
|
|
||||||
".next",
|
|
||||||
".nuxt",
|
|
||||||
".venv",
|
|
||||||
"__pycache__",
|
|
||||||
"vendor",
|
|
||||||
".ralpi",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/** Extensions tried when resolving a bare import specifier. */
|
/** Extensions tried when resolving a bare import specifier. */
|
||||||
const RESOLVE_EXTENSIONS = [
|
const RESOLVE_EXTENSIONS = [
|
||||||
".ts",
|
".ts",
|
||||||
@@ -131,11 +99,6 @@ const RESOLVE_EXTENSIONS = [
|
|||||||
".cts",
|
".cts",
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXT = (name: string): string => {
|
|
||||||
const i = name.lastIndexOf(".");
|
|
||||||
return i === -1 ? "" : name.slice(i).toLowerCase();
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Filename signals of a compat/deprecated/migration shim. */
|
/** Filename signals of a compat/deprecated/migration shim. */
|
||||||
const SHIM_NAME_RE =
|
const SHIM_NAME_RE =
|
||||||
/(?:compat|legacy|deprecated|obsolete|obsoleted|migration|migrate|backcompat|back-compat|fallback|shim)/i;
|
/(?:compat|legacy|deprecated|obsolete|obsoleted|migration|migrate|backcompat|back-compat|fallback|shim)/i;
|
||||||
@@ -161,7 +124,7 @@ function depStem(spec: string): string {
|
|||||||
async function walkSourceFiles(root: string): Promise<string[]> {
|
async function walkSourceFiles(root: string): Promise<string[]> {
|
||||||
const st = await stat(root).catch(() => undefined);
|
const st = await stat(root).catch(() => undefined);
|
||||||
if (!st) return [];
|
if (!st) return [];
|
||||||
if (st.isFile()) return SOURCE_EXTENSIONS.has(EXT(root)) ? [root] : [];
|
if (st.isFile()) return isScopeSource(root) ? [root] : [];
|
||||||
const out: string[] = [];
|
const out: string[] = [];
|
||||||
const stack = [root];
|
const stack = [root];
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
@@ -175,9 +138,9 @@ async function walkSourceFiles(root: string): Promise<string[]> {
|
|||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const full = join(dir, entry.name);
|
const full = join(dir, entry.name);
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
if (IGNORED_DIRS.has(entry.name)) continue;
|
if (SCOPE_EXCLUDE_DIRS.has(entry.name)) continue;
|
||||||
stack.push(full);
|
stack.push(full);
|
||||||
} else if (entry.isFile() && SOURCE_EXTENSIONS.has(EXT(entry.name))) {
|
} else if (entry.isFile() && isScopeSource(entry.name)) {
|
||||||
out.push(full);
|
out.push(full);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1069,6 +1032,8 @@ export async function buildDeadCodeScanTask(
|
|||||||
"```",
|
"```",
|
||||||
``,
|
``,
|
||||||
`Target root: ${scope.target}. Work dir: ${cwd}.`,
|
`Target root: ${scope.target}. Work dir: ${cwd}.`,
|
||||||
|
``,
|
||||||
|
scopeRulesMarkdown(),
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
type CheckDefinition,
|
type CheckDefinition,
|
||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
|
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 {
|
||||||
@@ -44,26 +45,6 @@ export function changesPath(cwd: string): string {
|
|||||||
return join(deepModulesOutputDir(cwd), "changes.md");
|
return join(deepModulesOutputDir(cwd), "changes.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Source extensions this check inspects. */
|
|
||||||
const DEEP_MODULES_EXTENSIONS = new Set([
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".js",
|
|
||||||
".jsx",
|
|
||||||
".mjs",
|
|
||||||
".cjs",
|
|
||||||
".py",
|
|
||||||
".rb",
|
|
||||||
".go",
|
|
||||||
".rs",
|
|
||||||
".java",
|
|
||||||
".kt",
|
|
||||||
".swift",
|
|
||||||
".php",
|
|
||||||
".cs",
|
|
||||||
".lua",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gate: skip when the cwd has no inspectable source files at all. A workspace
|
* Gate: skip when the cwd has no inspectable source files at all. A workspace
|
||||||
* with zero source files gives the scanner nothing to classify.
|
* with zero source files gives the scanner nothing to classify.
|
||||||
@@ -73,9 +54,7 @@ function deepModulesGate(cwd: string): string | undefined {
|
|||||||
try {
|
try {
|
||||||
const entries = readdirSync(cwd);
|
const entries = readdirSync(cwd);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const dot = entry.lastIndexOf(".");
|
if (isScopeSource(entry)) {
|
||||||
const ext = dot === -1 ? "" : entry.slice(dot).toLowerCase();
|
|
||||||
if (DEEP_MODULES_EXTENSIONS.has(ext)) {
|
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -116,6 +95,8 @@ function buildDeepScanTask(cwd: string, scope: CheckScope): string {
|
|||||||
`count, recommendation, and risk (low if no external importers, high else).`,
|
`count, recommendation, and risk (low if no external importers, high else).`,
|
||||||
`Then emit a one-line summary referencing the findings path.`,
|
`Then emit a one-line summary referencing the findings path.`,
|
||||||
"",
|
"",
|
||||||
|
scopeRulesMarkdown(),
|
||||||
|
"",
|
||||||
`# Deterministic fallback (executed by the fake runner in tests):`,
|
`# Deterministic fallback (executed by the fake runner in tests):`,
|
||||||
`!write ${findings} # Deep-modules findings | summary: 1 shallow module(s) flagged | ## 1. ${target}/wrapper.ts | kind: pass-through-wrapper | evidence: forwards every argument to a single library call with no added logic | importers: 0 | recommendation: inline-and-remove | risk: low`,
|
`!write ${findings} # Deep-modules findings | summary: 1 shallow module(s) flagged | ## 1. ${target}/wrapper.ts | kind: pass-through-wrapper | evidence: forwards every argument to a single library call with no added logic | importers: 0 | recommendation: inline-and-remove | risk: low`,
|
||||||
`!echo deep-modules: 1 issue — see ${findings}`,
|
`!echo deep-modules: 1 issue — see ${findings}`,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
type CheckDefinition,
|
type CheckDefinition,
|
||||||
type CheckScope,
|
type CheckScope,
|
||||||
} from "./registry.js";
|
} from "./registry.js";
|
||||||
|
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 {
|
||||||
@@ -64,26 +65,6 @@ export function changesPath(cwd: string): string {
|
|||||||
return join(defensiveGuardsOutputDir(cwd), "changes.md");
|
return join(defensiveGuardsOutputDir(cwd), "changes.md");
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Source extensions this check inspects. */
|
|
||||||
const DEFENSIVE_GUARDS_EXTENSIONS = new Set([
|
|
||||||
".ts",
|
|
||||||
".tsx",
|
|
||||||
".js",
|
|
||||||
".jsx",
|
|
||||||
".mjs",
|
|
||||||
".cjs",
|
|
||||||
".py",
|
|
||||||
".rb",
|
|
||||||
".go",
|
|
||||||
".rs",
|
|
||||||
".java",
|
|
||||||
".kt",
|
|
||||||
".swift",
|
|
||||||
".php",
|
|
||||||
".cs",
|
|
||||||
".lua",
|
|
||||||
]);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gate: skip when the cwd has no inspectable source files at all — a workspace
|
* Gate: skip when the cwd has no inspectable source files at all — a workspace
|
||||||
* with zero source files gives the scanner nothing to analyse.
|
* with zero source files gives the scanner nothing to analyse.
|
||||||
@@ -93,9 +74,7 @@ function defensiveGuardsGate(cwd: string): string | undefined {
|
|||||||
try {
|
try {
|
||||||
const entries = readdirSync(cwd);
|
const entries = readdirSync(cwd);
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
const dot = entry.lastIndexOf(".");
|
if (isScopeSource(entry)) {
|
||||||
const ext = dot === -1 ? "" : entry.slice(dot).toLowerCase();
|
|
||||||
if (DEFENSIVE_GUARDS_EXTENSIONS.has(ext)) {
|
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -137,6 +116,8 @@ function buildDefensiveGuardsScanTask(cwd: string, scope: CheckScope): string {
|
|||||||
`reason.`,
|
`reason.`,
|
||||||
`Then emit a one-line summary referencing the findings path.`,
|
`Then emit a one-line summary referencing the findings path.`,
|
||||||
``,
|
``,
|
||||||
|
scopeRulesMarkdown(),
|
||||||
|
``,
|
||||||
`# Deterministic fallback (executed by the fake runner in tests):`,
|
`# Deterministic fallback (executed by the fake runner in tests):`,
|
||||||
`!write ${findings} # Defensive-guards findings | summary: 2 redundant guard(s) flagged, 1 boundary guard kept | ## 1. ${target}/noise.ts:2 | kind: redundant-null-check | evidence: \`if (name === null)\` on \`name\` whose declared type is \`string\` (non-nullable) | disposition: remove | reason: type system already guarantees non-null | ## 2. ${target}/noise.ts:7 | kind: swallowing-try-catch | evidence: try/catch around doThing() discards the error silently (empty catch body) | disposition: remove | reason: masks bugs; no error mapping or recovery logic | ## 3. ${target}/boundary.ts:2 | kind: parsing-guard | evidence: try/catch around JSON.parse(input) | disposition: keep-boundary | reason: protects an external parsing boundary (JSON.parse of untrusted input)`,
|
`!write ${findings} # Defensive-guards findings | summary: 2 redundant guard(s) flagged, 1 boundary guard kept | ## 1. ${target}/noise.ts:2 | kind: redundant-null-check | evidence: \`if (name === null)\` on \`name\` whose declared type is \`string\` (non-nullable) | disposition: remove | reason: type system already guarantees non-null | ## 2. ${target}/noise.ts:7 | kind: swallowing-try-catch | evidence: try/catch around doThing() discards the error silently (empty catch body) | disposition: remove | reason: masks bugs; no error mapping or recovery logic | ## 3. ${target}/boundary.ts:2 | kind: parsing-guard | evidence: try/catch around JSON.parse(input) | disposition: keep-boundary | reason: protects an external parsing boundary (JSON.parse of untrusted input)`,
|
||||||
`!echo defensive-guards: 2 redundant, 1 boundary kept — see ${findings}`,
|
`!echo defensive-guards: 2 redundant, 1 boundary kept — see ${findings}`,
|
||||||
|
|||||||
145
src/checks/scope.ts
Normal file
145
src/checks/scope.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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",
|
||||||
|
"__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));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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\`).
|
||||||
|
`;
|
||||||
|
}
|
||||||
23
src/recon.ts
23
src/recon.ts
@@ -15,6 +15,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|||||||
import { promisify } from "node:util";
|
import { promisify } from "node:util";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { stateDir, RECON_FILENAME } from "./run-state.js";
|
import { stateDir, RECON_FILENAME } from "./run-state.js";
|
||||||
|
import { SCOPE_EXTENSIONS } from "./checks/scope.js";
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -28,26 +29,6 @@ export interface ReconSnapshot {
|
|||||||
totalSourceFiles: 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. */
|
/** Run `git ls-files` when possible to get a clean source inventory. */
|
||||||
async function listSourceFiles(cwd: string): Promise<string[]> {
|
async function listSourceFiles(cwd: string): Promise<string[]> {
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +43,7 @@ async function listSourceFiles(cwd: string): Promise<string[]> {
|
|||||||
.filter((l) => {
|
.filter((l) => {
|
||||||
const dot = l.lastIndexOf(".");
|
const dot = l.lastIndexOf(".");
|
||||||
if (dot === -1) return false;
|
if (dot === -1) return false;
|
||||||
return SOURCE_EXTENSIONS.has(l.slice(dot).toLowerCase());
|
return SCOPE_EXTENSIONS.has(l.slice(dot).toLowerCase());
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
/* not a git repo or git unavailable — empty inventory */
|
/* not a git repo or git unavailable — empty inventory */
|
||||||
|
|||||||
Reference in New Issue
Block a user