@praneeth_54/agentdoctor 0.1.4-beta → 0.3.0-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +53 -3
  2. package/README.md +72 -35
  3. package/dist/cli/commands/doctor.js +1 -1
  4. package/dist/cli/commands/explain.js +2 -2
  5. package/dist/cli/commands/fix.d.ts +7 -5
  6. package/dist/cli/commands/fix.js +57 -12
  7. package/dist/cli/commands/scan.js +4 -12
  8. package/dist/cli/commands/verify.d.ts +13 -0
  9. package/dist/cli/commands/verify.js +56 -0
  10. package/dist/cli/program.js +54 -31
  11. package/dist/constants.d.ts +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/core/fix/apply.d.ts +10 -0
  14. package/dist/core/fix/apply.js +27 -0
  15. package/dist/core/fix/patterns.d.ts +7 -0
  16. package/dist/core/fix/patterns.js +21 -0
  17. package/dist/core/fix/plan.d.ts +9 -0
  18. package/dist/core/fix/plan.js +140 -0
  19. package/dist/core/fix/render.d.ts +6 -0
  20. package/dist/core/fix/render.js +68 -0
  21. package/dist/core/fix/run.d.ts +15 -0
  22. package/dist/core/fix/run.js +54 -0
  23. package/dist/core/fix/types.d.ts +34 -0
  24. package/dist/core/fix/types.js +2 -0
  25. package/dist/core/fix/writers/cursorignore.d.ts +13 -0
  26. package/dist/core/fix/writers/cursorignore.js +74 -0
  27. package/dist/core/rules/context/generated-directory.js +10 -0
  28. package/dist/core/rules/instructions/missing-path-reference.js +90 -1
  29. package/dist/core/rules/path-kind.d.ts +14 -0
  30. package/dist/core/rules/path-kind.js +77 -0
  31. package/dist/core/rules/security/env-file-exposure.js +14 -14
  32. package/dist/core/rules/security/private-key-file.js +4 -0
  33. package/dist/core/scanner/scan.d.ts +1 -2
  34. package/dist/core/scanner/scan.js +8 -5
  35. package/dist/core/scoring/compute-scores.d.ts +6 -0
  36. package/dist/core/scoring/compute-scores.js +90 -0
  37. package/dist/core/scoring/placeholder.d.ts +3 -3
  38. package/dist/core/scoring/placeholder.js +3 -3
  39. package/dist/core/verify/compare.d.ts +29 -0
  40. package/dist/core/verify/compare.js +56 -0
  41. package/dist/core/verify/load-baseline.d.ts +15 -0
  42. package/dist/core/verify/load-baseline.js +71 -0
  43. package/dist/core/verify/verify.d.ts +26 -0
  44. package/dist/core/verify/verify.js +40 -0
  45. package/dist/index.d.ts +8 -0
  46. package/dist/index.js +5 -0
  47. package/dist/reporters/terminal/report.js +7 -1
  48. package/dist/reporters/verify/json.d.ts +5 -0
  49. package/dist/reporters/verify/json.js +51 -0
  50. package/dist/reporters/verify/terminal.d.ts +2 -0
  51. package/dist/reporters/verify/terminal.js +45 -0
  52. package/dist/types/index.d.ts +3 -3
  53. package/package.json +1 -1
@@ -0,0 +1,10 @@
1
+ import type { FixApplyResult, FixPlan } from "./types.js";
2
+ import { previewCursorignoreActions, readCursorignore } from "./writers/cursorignore.js";
3
+ /**
4
+ * Apply (or dry-run) a fix plan.
5
+ * Week 1: only `.cursorignore` mutations are supported.
6
+ */
7
+ export declare function applyFixPlan(plan: FixPlan, options: {
8
+ dryRun: boolean;
9
+ }): Promise<FixApplyResult>;
10
+ export { previewCursorignoreActions, readCursorignore };
@@ -0,0 +1,27 @@
1
+ import { previewCursorignoreActions, readCursorignore, writeCursorignore, } from "./writers/cursorignore.js";
2
+ /**
3
+ * Apply (or dry-run) a fix plan.
4
+ * Week 1: only `.cursorignore` mutations are supported.
5
+ */
6
+ export async function applyFixPlan(plan, options) {
7
+ const current = await readCursorignore(plan.root);
8
+ const preview = previewCursorignoreActions(current, plan.actions);
9
+ if (!preview) {
10
+ return {
11
+ plan,
12
+ dryRun: options.dryRun,
13
+ writtenFiles: [],
14
+ changedFiles: [],
15
+ };
16
+ }
17
+ if (!options.dryRun) {
18
+ await writeCursorignore(plan.root, preview.after);
19
+ }
20
+ return {
21
+ plan,
22
+ dryRun: options.dryRun,
23
+ writtenFiles: options.dryRun ? [] : [preview.targetRelativePath],
24
+ changedFiles: [preview.targetRelativePath],
25
+ };
26
+ }
27
+ export { previewCursorignoreActions, readCursorignore };
@@ -0,0 +1,7 @@
1
+ import type { Finding } from "../../types/index.js";
2
+ export declare function isSafeContextFixRule(ruleId: string): boolean;
3
+ /**
4
+ * Build a gitignore-style pattern that excludes the finding evidence path.
5
+ * Directories (generated-directory) get a trailing slash.
6
+ */
7
+ export declare function patternForFinding(finding: Finding): string | null;
@@ -0,0 +1,21 @@
1
+ const SAFE_CONTEXT_RULES = new Set(["context/generated-directory", "context/large-log-file"]);
2
+ export function isSafeContextFixRule(ruleId) {
3
+ return SAFE_CONTEXT_RULES.has(ruleId);
4
+ }
5
+ /**
6
+ * Build a gitignore-style pattern that excludes the finding evidence path.
7
+ * Directories (generated-directory) get a trailing slash.
8
+ */
9
+ export function patternForFinding(finding) {
10
+ const evidencePath = finding.evidence?.path?.replace(/\\/g, "/").replace(/^\/+/, "");
11
+ if (!evidencePath) {
12
+ return null;
13
+ }
14
+ if (finding.ruleId === "context/generated-directory") {
15
+ return evidencePath.endsWith("/") ? evidencePath : `${evidencePath}/`;
16
+ }
17
+ if (finding.ruleId === "context/large-log-file") {
18
+ return evidencePath;
19
+ }
20
+ return null;
21
+ }
@@ -0,0 +1,9 @@
1
+ import type { ScanResult } from "../../types/index.js";
2
+ import type { FixPlan } from "./types.js";
3
+ /**
4
+ * Build a fix plan from a scan result.
5
+ * Week 1: Cursor `.cursorignore` appends for safe context findings only.
6
+ */
7
+ export declare function buildFixPlan(result: ScanResult): Promise<FixPlan>;
8
+ /** Patterns that would be newly appended given current file contents. */
9
+ export declare function missingPatternsForCursorignore(currentContent: string | null, patterns: string[]): string[];
@@ -0,0 +1,140 @@
1
+ import path from "node:path";
2
+ import { readTextFile } from "../../utils/fs.js";
3
+ import { createIgnoreIndex, parseIgnoreFile, pathMatchesAny } from "../rules/ignore.js";
4
+ import { isSafeContextFixRule, patternForFinding } from "./patterns.js";
5
+ const DEFAULT_MAX_IGNORE_BYTES = 512 * 1024;
6
+ function cursorConfigured(result) {
7
+ return result.agents.some((a) => a.id === "cursor" && (a.detected || a.configured));
8
+ }
9
+ async function loadCursorignorePatterns(root) {
10
+ const filePath = path.join(root, ".cursorignore");
11
+ const text = await readTextFile(filePath, DEFAULT_MAX_IGNORE_BYTES);
12
+ if (text === null) {
13
+ return [];
14
+ }
15
+ return parseIgnoreFile(text);
16
+ }
17
+ /**
18
+ * Build a fix plan from a scan result.
19
+ * Week 1: Cursor `.cursorignore` appends for safe context findings only.
20
+ */
21
+ export async function buildFixPlan(result) {
22
+ const root = result.repository.root;
23
+ const skipped = [];
24
+ const actionsByKey = new Map();
25
+ const existingCursorPatterns = await loadCursorignorePatterns(root);
26
+ const cursorIndex = createIgnoreIndex({
27
+ gitignorePatterns: [],
28
+ cursorignorePatterns: existingCursorPatterns,
29
+ });
30
+ const hasCursor = cursorConfigured(result);
31
+ for (const finding of result.findings) {
32
+ if (finding.fixability !== "safe") {
33
+ skipped.push({
34
+ findingId: finding.id,
35
+ ruleId: finding.ruleId,
36
+ reason: finding.fixability === "review"
37
+ ? "Requires human review (not auto-fixed)"
38
+ : finding.fixability === "manual"
39
+ ? "Manual remediation required (not auto-fixed)"
40
+ : "Not auto-fixable",
41
+ });
42
+ continue;
43
+ }
44
+ if (!isSafeContextFixRule(finding.ruleId)) {
45
+ skipped.push({
46
+ findingId: finding.id,
47
+ ruleId: finding.ruleId,
48
+ reason: "No automated fixer for this safe rule yet",
49
+ });
50
+ continue;
51
+ }
52
+ const pattern = patternForFinding(finding);
53
+ const evidencePath = finding.evidence?.path?.replace(/\\/g, "/");
54
+ if (!pattern || !evidencePath) {
55
+ skipped.push({
56
+ findingId: finding.id,
57
+ ruleId: finding.ruleId,
58
+ reason: "Finding has no usable evidence path",
59
+ });
60
+ continue;
61
+ }
62
+ const wantsCursor = finding.affectedAgents.includes("cursor");
63
+ if (wantsCursor && hasCursor) {
64
+ if (cursorIndex.matchesCursorignore(evidencePath) ||
65
+ cursorIndex.matchesCursorignore(pattern)) {
66
+ skipped.push({
67
+ findingId: finding.id,
68
+ ruleId: finding.ruleId,
69
+ reason: "Already excluded by .cursorignore",
70
+ });
71
+ }
72
+ else {
73
+ mergeCursorAction(actionsByKey, finding, pattern, evidencePath);
74
+ }
75
+ }
76
+ else if (wantsCursor && !hasCursor) {
77
+ skipped.push({
78
+ findingId: finding.id,
79
+ ruleId: finding.ruleId,
80
+ reason: "Cursor not detected; skipping .cursorignore fix",
81
+ });
82
+ }
83
+ for (const agent of finding.affectedAgents) {
84
+ if (agent === "cursor") {
85
+ continue;
86
+ }
87
+ skipped.push({
88
+ findingId: finding.id,
89
+ ruleId: finding.ruleId,
90
+ reason: `Fix writer for ${agent} not implemented yet`,
91
+ });
92
+ }
93
+ }
94
+ return {
95
+ root,
96
+ actions: [...actionsByKey.values()].sort((a, b) => a.id.localeCompare(b.id)),
97
+ skipped,
98
+ };
99
+ }
100
+ function mergeCursorAction(actionsByKey, finding, pattern, evidencePath) {
101
+ const key = `cursor:.cursorignore:${pattern}`;
102
+ const existing = actionsByKey.get(key);
103
+ if (existing) {
104
+ if (!existing.findingIds.includes(finding.id)) {
105
+ existing.findingIds.push(finding.id);
106
+ }
107
+ return;
108
+ }
109
+ actionsByKey.set(key, {
110
+ id: key,
111
+ kind: "append-ignore-pattern",
112
+ agent: "cursor",
113
+ targetRelativePath: ".cursorignore",
114
+ pattern,
115
+ evidencePath,
116
+ findingIds: [finding.id],
117
+ description: `Exclude ${evidencePath} from Cursor agent context via .cursorignore`,
118
+ });
119
+ }
120
+ /** Patterns that would be newly appended given current file contents. */
121
+ export function missingPatternsForCursorignore(currentContent, patterns) {
122
+ const existing = currentContent === null ? [] : parseIgnoreFile(currentContent);
123
+ const missing = [];
124
+ for (const pattern of patterns) {
125
+ if (existing.includes(pattern)) {
126
+ continue;
127
+ }
128
+ if (pathMatchesAny(pattern.replace(/\/$/, ""), existing) &&
129
+ existing.some((p) => p === pattern)) {
130
+ continue;
131
+ }
132
+ // Also skip if an existing pattern already excludes this path
133
+ const evidenceGuess = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
134
+ if (pathMatchesAny(evidenceGuess, existing) || pathMatchesAny(pattern, existing)) {
135
+ continue;
136
+ }
137
+ missing.push(pattern);
138
+ }
139
+ return missing;
140
+ }
@@ -0,0 +1,6 @@
1
+ import type { FixApplyResult, FixPlan } from "./types.js";
2
+ export declare function renderFixPlanTerminal(plan: FixPlan, options: {
3
+ dryRun: boolean;
4
+ cursorContent: string | null;
5
+ applyResult?: FixApplyResult;
6
+ }): string;
@@ -0,0 +1,68 @@
1
+ import { previewCursorignoreActions } from "./writers/cursorignore.js";
2
+ export function renderFixPlanTerminal(plan, options) {
3
+ const lines = [];
4
+ lines.push("");
5
+ lines.push(options.dryRun ? "AgentDoctor fix (dry-run)" : "AgentDoctor fix");
6
+ lines.push("");
7
+ if (plan.actions.length === 0) {
8
+ lines.push(" No automatic fixes available for this scan.");
9
+ if (plan.skipped.length > 0) {
10
+ lines.push("");
11
+ lines.push(` ${plan.skipped.length} finding(s) need manual or review action:`);
12
+ const reasons = summarizeSkipReasons(plan.skipped.map((s) => s.reason));
13
+ for (const [reason, count] of reasons) {
14
+ lines.push(` - ${reason} (${count})`);
15
+ }
16
+ lines.push("");
17
+ lines.push(" Next: address remaining findings, then run `agentdoctor verify`.");
18
+ }
19
+ else {
20
+ lines.push(" Scan found nothing that Fix can change.");
21
+ }
22
+ lines.push("");
23
+ return lines.join("\n");
24
+ }
25
+ lines.push(` Proposed actions: ${plan.actions.length}`);
26
+ for (const action of plan.actions) {
27
+ lines.push(` • [${action.agent}] ${action.description}`);
28
+ lines.push(` pattern: ${action.pattern}`);
29
+ lines.push(` findings: ${action.findingIds.length}`);
30
+ }
31
+ const preview = previewCursorignoreActions(options.cursorContent, plan.actions);
32
+ if (preview) {
33
+ lines.push("");
34
+ lines.push(" File changes:");
35
+ lines.push(` ${preview.targetRelativePath} (+${preview.patternsToAdd.length} pattern(s))`);
36
+ lines.push("");
37
+ for (const previewLine of preview.preview.split("\n")) {
38
+ lines.push(` ${previewLine}`);
39
+ }
40
+ }
41
+ if (options.applyResult && !options.dryRun) {
42
+ lines.push("");
43
+ if (options.applyResult.writtenFiles.length > 0) {
44
+ lines.push(` Wrote: ${options.applyResult.writtenFiles.join(", ")}`);
45
+ }
46
+ else {
47
+ lines.push(" No files written.");
48
+ }
49
+ }
50
+ else if (options.dryRun) {
51
+ lines.push("");
52
+ lines.push(" No files were modified (dry-run).");
53
+ lines.push(" Re-run without --dry-run to apply.");
54
+ }
55
+ if (plan.skipped.length > 0) {
56
+ lines.push("");
57
+ lines.push(` Skipped: ${plan.skipped.length} finding(s) (writers pending or already fixed)`);
58
+ }
59
+ lines.push("");
60
+ return lines.join("\n");
61
+ }
62
+ function summarizeSkipReasons(reasons) {
63
+ const map = new Map();
64
+ for (const reason of reasons) {
65
+ map.set(reason, (map.get(reason) ?? 0) + 1);
66
+ }
67
+ return [...map.entries()].sort((a, b) => b[1] - a[1]);
68
+ }
@@ -0,0 +1,15 @@
1
+ import type { FixApplyResult, FixPlan } from "./types.js";
2
+ export interface RunFixOptions {
3
+ cwd: string;
4
+ dryRun: boolean;
5
+ yes: boolean;
6
+ }
7
+ export interface RunFixResult {
8
+ plan: FixPlan;
9
+ applyResult: FixApplyResult;
10
+ cancelled: boolean;
11
+ }
12
+ /**
13
+ * Scan → plan → (confirm) → apply Safe Repository Mutation actions.
14
+ */
15
+ export declare function runFix(options: RunFixOptions): Promise<RunFixResult>;
@@ -0,0 +1,54 @@
1
+ import readline from "node:readline/promises";
2
+ import { stdin as input, stdout as output } from "node:process";
3
+ import { scan } from "../scanner/scan.js";
4
+ import { applyFixPlan, readCursorignore } from "./apply.js";
5
+ import { buildFixPlan } from "./plan.js";
6
+ import { renderFixPlanTerminal } from "./render.js";
7
+ /**
8
+ * Scan → plan → (confirm) → apply Safe Repository Mutation actions.
9
+ */
10
+ export async function runFix(options) {
11
+ const result = await scan({ cwd: options.cwd });
12
+ const plan = await buildFixPlan(result);
13
+ const cursorContent = await readCursorignore(plan.root);
14
+ if (plan.actions.length === 0 || options.dryRun) {
15
+ const applyResult = await applyFixPlan(plan, { dryRun: true });
16
+ return { plan, applyResult, cancelled: false };
17
+ }
18
+ if (!options.yes) {
19
+ const previewText = renderFixPlanTerminal(plan, {
20
+ dryRun: true,
21
+ cursorContent,
22
+ });
23
+ process.stdout.write(previewText);
24
+ const confirmed = await confirm("Apply these changes?");
25
+ if (!confirmed) {
26
+ return {
27
+ plan,
28
+ applyResult: {
29
+ plan,
30
+ dryRun: true,
31
+ writtenFiles: [],
32
+ changedFiles: [],
33
+ },
34
+ cancelled: true,
35
+ };
36
+ }
37
+ }
38
+ const applyResult = await applyFixPlan(plan, { dryRun: false });
39
+ return { plan, applyResult, cancelled: false };
40
+ }
41
+ async function confirm(question) {
42
+ if (!input.isTTY || !output.isTTY) {
43
+ process.stderr.write("Error: confirmation required but stdin is not a TTY. Re-run with --yes or --dry-run.\n");
44
+ return false;
45
+ }
46
+ const rl = readline.createInterface({ input, output });
47
+ try {
48
+ const answer = await rl.question(`${question} [y/N] `);
49
+ return /^y(es)?$/i.test(answer.trim());
50
+ }
51
+ finally {
52
+ rl.close();
53
+ }
54
+ }
@@ -0,0 +1,34 @@
1
+ import type { AgentId } from "../../types/index.js";
2
+ export type FixKind = "append-ignore-pattern";
3
+ /** Allowlisted relative paths fix may create or modify. */
4
+ export declare const FIX_PATH_ALLOWLIST: readonly [".cursorignore"];
5
+ export type FixAllowlistedPath = (typeof FIX_PATH_ALLOWLIST)[number];
6
+ export interface FixAction {
7
+ id: string;
8
+ kind: FixKind;
9
+ agent: AgentId;
10
+ targetRelativePath: FixAllowlistedPath;
11
+ /** Ignore pattern to ensure is present (gitignore-style). */
12
+ pattern: string;
13
+ /** Evidence path this pattern is meant to exclude. */
14
+ evidencePath: string;
15
+ findingIds: string[];
16
+ description: string;
17
+ }
18
+ export interface SkippedFix {
19
+ findingId: string;
20
+ ruleId: string;
21
+ reason: string;
22
+ }
23
+ export interface FixPlan {
24
+ root: string;
25
+ actions: FixAction[];
26
+ skipped: SkippedFix[];
27
+ }
28
+ export interface FixApplyResult {
29
+ plan: FixPlan;
30
+ dryRun: boolean;
31
+ writtenFiles: string[];
32
+ /** Relative paths that would be / were written */
33
+ changedFiles: string[];
34
+ }
@@ -0,0 +1,2 @@
1
+ /** Allowlisted relative paths fix may create or modify. */
2
+ export const FIX_PATH_ALLOWLIST = [".cursorignore"];
@@ -0,0 +1,13 @@
1
+ import type { FixAction } from "../types.js";
2
+ export interface CursorignoreWritePreview {
3
+ targetRelativePath: ".cursorignore";
4
+ patternsToAdd: string[];
5
+ before: string;
6
+ after: string;
7
+ /** Unified-diff-like preview (simplified). */
8
+ preview: string;
9
+ }
10
+ export declare function buildCursorignoreContent(currentContent: string | null, patternsToAdd: string[]): string;
11
+ export declare function previewCursorignoreActions(currentContent: string | null, actions: FixAction[]): CursorignoreWritePreview | null;
12
+ export declare function readCursorignore(root: string): Promise<string | null>;
13
+ export declare function writeCursorignore(root: string, content: string): Promise<void>;
@@ -0,0 +1,74 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { readTextFile } from "../../../utils/fs.js";
4
+ import { missingPatternsForCursorignore } from "../plan.js";
5
+ const MAX_BYTES = 512 * 1024;
6
+ const BANNER = "# Added by AgentDoctor";
7
+ export function buildCursorignoreContent(currentContent, patternsToAdd) {
8
+ if (patternsToAdd.length === 0) {
9
+ return currentContent ?? "";
10
+ }
11
+ const base = currentContent ?? "";
12
+ const trimmed = base.replace(/\s+$/, "");
13
+ const block = [BANNER, ...patternsToAdd].join("\n");
14
+ if (trimmed.length === 0) {
15
+ return `${block}\n`;
16
+ }
17
+ return `${trimmed}\n\n${block}\n`;
18
+ }
19
+ export function previewCursorignoreActions(currentContent, actions) {
20
+ const cursorActions = actions.filter((a) => a.agent === "cursor" && a.targetRelativePath === ".cursorignore");
21
+ if (cursorActions.length === 0) {
22
+ return null;
23
+ }
24
+ const requested = [...new Set(cursorActions.map((a) => a.pattern))];
25
+ const patternsToAdd = missingPatternsForCursorignore(currentContent, requested);
26
+ if (patternsToAdd.length === 0) {
27
+ return null;
28
+ }
29
+ const before = currentContent ?? "";
30
+ const after = buildCursorignoreContent(currentContent, patternsToAdd);
31
+ const preview = formatSimpleDiff(".cursorignore", before, after);
32
+ return {
33
+ targetRelativePath: ".cursorignore",
34
+ patternsToAdd,
35
+ before,
36
+ after,
37
+ preview,
38
+ };
39
+ }
40
+ export async function readCursorignore(root) {
41
+ return readTextFile(path.join(root, ".cursorignore"), MAX_BYTES);
42
+ }
43
+ export async function writeCursorignore(root, content) {
44
+ const target = path.join(root, ".cursorignore");
45
+ const dir = path.dirname(target);
46
+ await fs.mkdir(dir, { recursive: true });
47
+ const tmp = path.join(dir, `.cursorignore.agentdoctor.${process.pid}.tmp`);
48
+ await fs.writeFile(tmp, content, "utf8");
49
+ await fs.rename(tmp, target);
50
+ }
51
+ function formatSimpleDiff(fileLabel, before, after) {
52
+ const beforeLines = before.length === 0 ? [] : before.split(/\r?\n/);
53
+ const afterLines = after.split(/\r?\n/);
54
+ const lines = [];
55
+ lines.push(`--- a/${fileLabel}`);
56
+ lines.push(`+++ b/${fileLabel}`);
57
+ // Show only newly added trailing block for clarity
58
+ const addedStart = beforeLines.length === 0 ? 0 : beforeLines.length;
59
+ for (let i = addedStart; i < afterLines.length; i++) {
60
+ const line = afterLines[i];
61
+ if (line === undefined) {
62
+ continue;
63
+ }
64
+ // Skip pure trailing empty presentation noise except keep structure
65
+ lines.push(`+${line}`);
66
+ }
67
+ if (before.length === 0) {
68
+ lines.splice(2, 0, "@@ new file @@");
69
+ }
70
+ else {
71
+ lines.splice(2, 0, "@@ append @@");
72
+ }
73
+ return lines.join("\n");
74
+ }
@@ -1,3 +1,4 @@
1
+ import { isSampleOrTestPath, isSourceNamedArtifactCollision } from "../path-kind.js";
1
2
  /** Directories commonly generated/build-related. vendor/ is ecosystem-aware. */
2
3
  const GENERATED = [
3
4
  { name: "dist", label: "build output" },
@@ -34,6 +35,12 @@ export const generatedDirectoryRule = {
34
35
  for (const entry of GENERATED) {
35
36
  const matches = context.discovery.directoriesSkipped.filter((dir) => isGeneratedPath(dir, entry.name));
36
37
  for (const relativePath of matches) {
38
+ if (isSampleOrTestPath(relativePath)) {
39
+ continue;
40
+ }
41
+ if (isSourceNamedArtifactCollision(relativePath)) {
42
+ continue;
43
+ }
37
44
  if (isExcluded(context, relativePath)) {
38
45
  continue;
39
46
  }
@@ -57,6 +64,9 @@ export const generatedDirectoryRule = {
57
64
  !context.repository.packageManagers.includes("composer") &&
58
65
  context.repository.primaryLanguage !== "php") {
59
66
  for (const relativePath of vendorPaths) {
67
+ if (isSampleOrTestPath(relativePath)) {
68
+ continue;
69
+ }
60
70
  if (isExcluded(context, relativePath)) {
61
71
  continue;
62
72
  }
@@ -12,6 +12,73 @@ const INSTRUCTION_KINDS = new Set([
12
12
  ]);
13
13
  /** Extensions accepted for bare filenames (no `/`) — precision over recall. */
14
14
  const BARE_FILE_EXTENSION = /\.(mdc?|txt|tsx?|jsx?|mjs|cjs|json|ya?ml|toml|php|py|rb|go|rs|java|kt|css|scss|html|vue|svelte|sh|bash|zsh|sql|xml|svg|png|jpe?g|gif|webp|wasm|lock)$/i;
15
+ /** Host-like first segments for Go/npm module paths (`github.com/…`, `golang.org/…`). */
16
+ const MODULE_HOST_SEGMENT = /^(?:[a-z0-9-]+\.)+(?:com|org|net|io|dev|ai|app|cloud|co|edu|gov|info|me|tv|xyz|in|to|cc)$/i;
17
+ /** Control-flow / language tokens that appear as `a/b` in prose, not filesystem paths. */
18
+ const CODE_SLASH_TOKEN = /^(?:try\/finally|if\/else|for\/of|for\/in|async\/await|do\/while)$/i;
19
+ /**
20
+ * First path segment of Go standard-library import paths (`io/ioutil`, `net/http`).
21
+ * Kept narrow so `src/utils`-style repo paths still validate.
22
+ */
23
+ const GO_STDLIB_ROOTS = new Set([
24
+ "archive",
25
+ "bufio",
26
+ "bytes",
27
+ "cmp",
28
+ "compress",
29
+ "container",
30
+ "context",
31
+ "crypto",
32
+ "database",
33
+ "debug",
34
+ "embed",
35
+ "encoding",
36
+ "errors",
37
+ "expvar",
38
+ "flag",
39
+ "fmt",
40
+ "go",
41
+ "hash",
42
+ "html",
43
+ "image",
44
+ "index",
45
+ "io",
46
+ "log",
47
+ "maps",
48
+ "math",
49
+ "mime",
50
+ "net",
51
+ "os",
52
+ "path",
53
+ "plugin",
54
+ "reflect",
55
+ "regexp",
56
+ "runtime",
57
+ "slices",
58
+ "structs",
59
+ "sync",
60
+ "syscall",
61
+ "testing",
62
+ "text",
63
+ "time",
64
+ "unicode",
65
+ "unique",
66
+ "unsafe",
67
+ "weak",
68
+ ]);
69
+ /** Optional build-output roots often documented before `npm run build`. */
70
+ const OPTIONAL_BUILD_ROOTS = new Set(["dist", "build", "out", "target", ".next", "coverage"]);
71
+ function isGoStdImportPath(normalized) {
72
+ const parts = normalized.split("/");
73
+ if (parts.length < 2 || parts.length > 4) {
74
+ return false;
75
+ }
76
+ const root = parts[0] ?? "";
77
+ if (!GO_STDLIB_ROOTS.has(root)) {
78
+ return false;
79
+ }
80
+ return parts.every((part) => /^[a-z][a-z0-9]*$/.test(part));
81
+ }
15
82
  function stripAnchor(value) {
16
83
  return value.split("#")[0] ?? value;
17
84
  }
@@ -42,12 +109,34 @@ export function isLikelyLocalPathReference(value) {
42
109
  // CSS class / id-like bare selectors: `.content`, `.button`
43
110
  if (/^\.[A-Za-z_][\w-]*$/.test(value))
44
111
  return false;
112
+ // npm scoped packages: `@scope/name`
113
+ if (/^@[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/.test(value))
114
+ return false;
115
+ // Glob / brace patterns are not concrete paths
116
+ if (/[*?[\]{}]/.test(value))
117
+ return false;
118
+ // Language tokens that look like paths (`try/finally`)
119
+ if (CODE_SLASH_TOKEN.test(value))
120
+ return false;
121
+ const withoutDotSlash = value.replace(/^\.\//, "");
122
+ const trimmedDir = withoutDotSlash.replace(/\/+$/, "");
123
+ // Documented build outputs often missing until a local build
124
+ if (OPTIONAL_BUILD_ROOTS.has(trimmedDir))
125
+ return false;
45
126
  if (value.includes("/")) {
46
- // Reject empty segments like `foo//bar` while allowing trailing slash dirs
47
127
  const normalized = value.replace(/\/+$/, "");
48
128
  if (!normalized || normalized.split("/").some((part) => part === "")) {
49
129
  return false;
50
130
  }
131
+ const firstSegment = normalized.split("/")[0] ?? "";
132
+ // Go/npm module imports: `github.com/foo/bar`, `golang.org/x/…`
133
+ if (MODULE_HOST_SEGMENT.test(firstSegment)) {
134
+ return false;
135
+ }
136
+ // Go stdlib: `io/ioutil`, `net/http`, `path/filepath`
137
+ if (isGoStdImportPath(normalized)) {
138
+ return false;
139
+ }
51
140
  return true;
52
141
  }
53
142
  // Bare filename: require a known file extension (not `.content`-style tokens)
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Shared path classification for rule precision.
3
+ * Paths under intentional sample/test/demo trees are not production secrets for agent readiness.
4
+ */
5
+ /**
6
+ * True when any path segment marks fixture, test, example, or sample material.
7
+ * Uses POSIX relative paths (discovery output).
8
+ */
9
+ export declare function isSampleOrTestPath(relativePath: string): boolean;
10
+ /**
11
+ * True for paths like `scripts/build` or `internal/build` that are source trees
12
+ * named after a common artifact directory, not generated output.
13
+ */
14
+ export declare function isSourceNamedArtifactCollision(relativePath: string): boolean;