@praneeth_54/agentdoctor 0.2.0-beta → 1.0.0
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.
- package/CHANGELOG.md +107 -3
- package/README.md +86 -35
- package/dist/agents/inspect.js +58 -51
- package/dist/cli/commands/doctor.js +1 -1
- package/dist/cli/commands/explain.js +2 -2
- package/dist/cli/commands/fix.d.ts +7 -5
- package/dist/cli/commands/fix.js +73 -12
- package/dist/cli/commands/scan.d.ts +5 -0
- package/dist/cli/commands/scan.js +25 -6
- package/dist/cli/commands/verify.d.ts +19 -0
- package/dist/cli/commands/verify.js +70 -0
- package/dist/cli/program.js +97 -33
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/fix/apply.d.ts +14 -0
- package/dist/core/fix/apply.js +47 -0
- package/dist/core/fix/patterns.d.ts +7 -0
- package/dist/core/fix/patterns.js +21 -0
- package/dist/core/fix/plan.d.ts +9 -0
- package/dist/core/fix/plan.js +247 -0
- package/dist/core/fix/render.d.ts +8 -0
- package/dist/core/fix/render.js +116 -0
- package/dist/core/fix/run.d.ts +15 -0
- package/dist/core/fix/run.js +58 -0
- package/dist/core/fix/types.d.ts +34 -0
- package/dist/core/fix/types.js +6 -0
- package/dist/core/fix/writers/claude-settings.d.ts +16 -0
- package/dist/core/fix/writers/claude-settings.js +100 -0
- package/dist/core/fix/writers/codex-config.d.ts +22 -0
- package/dist/core/fix/writers/codex-config.js +183 -0
- package/dist/core/fix/writers/cursorignore.d.ts +13 -0
- package/dist/core/fix/writers/cursorignore.js +68 -0
- package/dist/core/path-resolution/index.d.ts +5 -0
- package/dist/core/path-resolution/index.js +5 -0
- package/dist/core/path-resolution/prepare.d.ts +33 -0
- package/dist/core/path-resolution/prepare.js +98 -0
- package/dist/core/policy/evaluate.d.ts +44 -0
- package/dist/core/policy/evaluate.js +120 -0
- package/dist/core/rules/claude-deny.d.ts +8 -0
- package/dist/core/rules/claude-deny.js +39 -0
- package/dist/core/rules/codex-deny.d.ts +5 -0
- package/dist/core/rules/codex-deny.js +42 -0
- package/dist/core/rules/context/generated-directory.js +52 -8
- package/dist/core/rules/context/large-log-file.js +27 -10
- package/dist/core/rules/ignore.js +2 -5
- package/dist/core/rules/instructions/missing-path-reference.d.ts +5 -1
- package/dist/core/rules/instructions/missing-path-reference.js +114 -7
- package/dist/core/rules/path-kind.d.ts +27 -0
- package/dist/core/rules/path-kind.js +180 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -38
- package/dist/core/rules/security/private-key-file.js +4 -0
- package/dist/core/rules/text-cache.js +46 -40
- package/dist/core/verify/compare.d.ts +29 -0
- package/dist/core/verify/compare.js +56 -0
- package/dist/core/verify/load-baseline.d.ts +15 -0
- package/dist/core/verify/load-baseline.js +71 -0
- package/dist/core/verify/verify.d.ts +26 -0
- package/dist/core/verify/verify.js +40 -0
- package/dist/discovery/files.js +7 -2
- package/dist/discovery/log-like.d.ts +2 -0
- package/dist/discovery/log-like.js +7 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +6 -0
- package/dist/reporters/github/annotations.d.ts +8 -0
- package/dist/reporters/github/annotations.js +42 -0
- package/dist/reporters/github/emit.d.ts +23 -0
- package/dist/reporters/github/emit.js +31 -0
- package/dist/reporters/github/summary.d.ts +20 -0
- package/dist/reporters/github/summary.js +111 -0
- package/dist/reporters/terminal/report.d.ts +5 -0
- package/dist/reporters/terminal/report.js +73 -4
- package/dist/reporters/verify/json.d.ts +5 -0
- package/dist/reporters/verify/json.js +51 -0
- package/dist/reporters/verify/terminal.d.ts +6 -0
- package/dist/reporters/verify/terminal.js +71 -0
- package/dist/utils/fs.d.ts +5 -0
- package/dist/utils/fs.js +34 -0
- package/dist/utils/path.d.ts +5 -0
- package/dist/utils/path.js +16 -0
- package/package.json +11 -1
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { claudeDeniesPath } from "../claude-deny.js";
|
|
2
|
+
import { codexDeniesPath } from "../codex-deny.js";
|
|
3
|
+
import { isCheckedInGithubActionDist, isSampleOrTestPath, isSourceNamedArtifactCollision, } from "../path-kind.js";
|
|
1
4
|
/** Directories commonly generated/build-related. vendor/ is ecosystem-aware. */
|
|
2
5
|
const GENERATED = [
|
|
3
6
|
{ name: "dist", label: "build output" },
|
|
@@ -12,9 +15,27 @@ const GENERATED = [
|
|
|
12
15
|
function isGeneratedPath(relativePath, name) {
|
|
13
16
|
return relativePath === name || relativePath.endsWith(`/${name}`);
|
|
14
17
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
function isGitIgnored(context, relativePath) {
|
|
19
|
+
return (context.ignore.matchesGitignore(relativePath) ||
|
|
20
|
+
context.ignore.matchesGitignore(`${relativePath}/`));
|
|
21
|
+
}
|
|
22
|
+
async function agentsStillExposed(context, relativePath, configured) {
|
|
23
|
+
const exposed = [];
|
|
24
|
+
for (const agent of configured) {
|
|
25
|
+
if (agent === "cursor" &&
|
|
26
|
+
(context.ignore.matchesCursorignore(relativePath) ||
|
|
27
|
+
context.ignore.matchesCursorignore(`${relativePath}/`))) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (agent === "claude-code" && (await claudeDeniesPath(context, relativePath))) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (agent === "codex" && (await codexDeniesPath(context, relativePath))) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
exposed.push(agent);
|
|
37
|
+
}
|
|
38
|
+
return exposed;
|
|
18
39
|
}
|
|
19
40
|
export const generatedDirectoryRule = {
|
|
20
41
|
id: "context/generated-directory",
|
|
@@ -27,14 +48,27 @@ export const generatedDirectoryRule = {
|
|
|
27
48
|
recommendation: "Ensure generated directories are listed in .gitignore and agent ignore configuration where applicable.",
|
|
28
49
|
async check(context) {
|
|
29
50
|
const findings = [];
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
51
|
+
const configured = context.agents.filter((a) => a.configured || a.detected).map((a) => a.id);
|
|
52
|
+
if (configured.length === 0) {
|
|
32
53
|
return [];
|
|
33
54
|
}
|
|
34
55
|
for (const entry of GENERATED) {
|
|
35
56
|
const matches = context.discovery.directoriesSkipped.filter((dir) => isGeneratedPath(dir, entry.name));
|
|
36
57
|
for (const relativePath of matches) {
|
|
37
|
-
if (
|
|
58
|
+
if (isSampleOrTestPath(relativePath)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (isSourceNamedArtifactCollision(relativePath)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (isCheckedInGithubActionDist(relativePath)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (isGitIgnored(context, relativePath)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const affected = await agentsStillExposed(context, relativePath, configured);
|
|
71
|
+
if (affected.length === 0) {
|
|
38
72
|
continue;
|
|
39
73
|
}
|
|
40
74
|
findings.push({
|
|
@@ -44,7 +78,7 @@ export const generatedDirectoryRule = {
|
|
|
44
78
|
title: "Generated directory may enter agent context",
|
|
45
79
|
message: `${relativePath}/ (${entry.label}) is present and no project ignore pattern was detected`,
|
|
46
80
|
whyItMatters: "Generated directories are usually low-value for coding agents and can bloat indexing/context when not excluded.",
|
|
47
|
-
recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and
|
|
81
|
+
recommendation: `Add an ignore pattern covering ${relativePath}/ to .gitignore and agent exclusions (.cursorignore, Claude Code Read deny, and/or Codex filesystem deny).`,
|
|
48
82
|
affectedAgents: affected,
|
|
49
83
|
evidence: { path: relativePath },
|
|
50
84
|
fixability: "safe",
|
|
@@ -57,7 +91,17 @@ export const generatedDirectoryRule = {
|
|
|
57
91
|
!context.repository.packageManagers.includes("composer") &&
|
|
58
92
|
context.repository.primaryLanguage !== "php") {
|
|
59
93
|
for (const relativePath of vendorPaths) {
|
|
60
|
-
if (
|
|
94
|
+
if (isSampleOrTestPath(relativePath)) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (isSourceNamedArtifactCollision(relativePath)) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (isGitIgnored(context, relativePath)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const affected = await agentsStillExposed(context, relativePath, configured);
|
|
104
|
+
if (affected.length === 0) {
|
|
61
105
|
continue;
|
|
62
106
|
}
|
|
63
107
|
findings.push({
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { isLogLikePath } from "../../../discovery/log-like.js";
|
|
2
|
+
import { claudeDeniesPath } from "../claude-deny.js";
|
|
3
|
+
import { codexDeniesPath } from "../codex-deny.js";
|
|
1
4
|
import { THRESHOLDS } from "../thresholds.js";
|
|
2
|
-
const LOG_LIKE = /\.(log|out|dump)$/i;
|
|
3
|
-
const HEAVY_NAME = /(^|\/)(debug|trace|coverage-final|chrome-devtools|heapdump)/i;
|
|
4
5
|
export const largeLogFileRule = {
|
|
5
6
|
id: "context/large-log-file",
|
|
6
7
|
title: "Large log or dump file in repository",
|
|
@@ -12,19 +13,35 @@ export const largeLogFileRule = {
|
|
|
12
13
|
recommendation: "Delete or ignore large logs; add them to .gitignore and agent ignore files.",
|
|
13
14
|
async check(context) {
|
|
14
15
|
const findings = [];
|
|
15
|
-
const
|
|
16
|
+
const configured = context.agents
|
|
17
|
+
.filter((a) => a.configured || a.detected)
|
|
18
|
+
.map((a) => a.id);
|
|
16
19
|
for (const file of context.discovery.files) {
|
|
17
|
-
|
|
18
|
-
const looksLog = LOG_LIKE.test(base) || HEAVY_NAME.test(file.relativePath);
|
|
19
|
-
if (!looksLog) {
|
|
20
|
+
if (!isLogLikePath(file.relativePath)) {
|
|
20
21
|
continue;
|
|
21
22
|
}
|
|
22
23
|
if (file.sizeBytes < THRESHOLDS.largeLogBytes) {
|
|
23
24
|
continue;
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (context.ignore.matchesGitignore(file.relativePath)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const defaultAgents = ["cursor", "claude-code", "codex"];
|
|
30
|
+
const candidates = configured.length > 0 ? configured : defaultAgents;
|
|
31
|
+
const affected = [];
|
|
32
|
+
for (const agent of candidates) {
|
|
33
|
+
if (agent === "cursor" && context.ignore.matchesCursorignore(file.relativePath)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (agent === "claude-code" && (await claudeDeniesPath(context, file.relativePath))) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (agent === "codex" && (await codexDeniesPath(context, file.relativePath))) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
affected.push(agent);
|
|
43
|
+
}
|
|
44
|
+
if (affected.length === 0) {
|
|
28
45
|
continue;
|
|
29
46
|
}
|
|
30
47
|
findings.push({
|
|
@@ -35,7 +52,7 @@ export const largeLogFileRule = {
|
|
|
35
52
|
message: `${file.relativePath} is ${(file.sizeBytes / 1024).toFixed(0)} KB`,
|
|
36
53
|
whyItMatters: "Large logs and dumps rarely help coding agents and can crowd out useful source context.",
|
|
37
54
|
recommendation: "Remove or ignore this file for both git and AI agent tooling.",
|
|
38
|
-
affectedAgents: affected
|
|
55
|
+
affectedAgents: affected,
|
|
39
56
|
evidence: { path: file.relativePath, detail: `${file.sizeBytes} bytes` },
|
|
40
57
|
fixability: "safe",
|
|
41
58
|
});
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Minimal gitignore-style pattern matching for .gitignore / .cursorignore.
|
|
3
|
-
* Supports common patterns used in agent ignore files — not a full gitignore clone.
|
|
4
|
-
*/
|
|
1
|
+
import { toRepoRelativePosix } from "../../utils/path.js";
|
|
5
2
|
function normalizePattern(pattern) {
|
|
6
3
|
const trimmed = pattern.trim();
|
|
7
4
|
if (!trimmed || trimmed.startsWith("#")) {
|
|
@@ -48,7 +45,7 @@ export function matchIgnorePattern(relativePath, pattern) {
|
|
|
48
45
|
if (pat.startsWith("!")) {
|
|
49
46
|
pat = pat.slice(1);
|
|
50
47
|
}
|
|
51
|
-
const pathNorm = relativePath
|
|
48
|
+
const pathNorm = toRepoRelativePosix(relativePath);
|
|
52
49
|
let matched = false;
|
|
53
50
|
if (pat.endsWith("/")) {
|
|
54
51
|
const dir = pat.slice(0, -1);
|
|
@@ -16,7 +16,11 @@ export declare function extractPathCandidates(text: string): string[];
|
|
|
16
16
|
/**
|
|
17
17
|
* Resolve a candidate path reference with repository-aware semantics:
|
|
18
18
|
* - `./` and `../` → relative to the instruction file directory
|
|
19
|
-
* - otherwise → repository-root relative
|
|
19
|
+
* - otherwise → repository-root relative, plus the same path under the
|
|
20
|
+
* instruction file directory when nested (monorepo package docs)
|
|
21
|
+
*
|
|
22
|
+
* Does not search sibling packages: a root `AGENTS.md` reference must still
|
|
23
|
+
* exist at the repository root (or via `./` / `../`).
|
|
20
24
|
*/
|
|
21
25
|
export declare function resolveInstructionPathReference(instructionRelativePath: string, candidate: string): ResolvedPathReference;
|
|
22
26
|
export declare const missingPathReferenceRule: RuleDefinition;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { pathExistsInsideRoot } from "../../../agents/inspect.js";
|
|
3
|
+
import { preparePathReference } from "../../path-resolution/index.js";
|
|
3
4
|
import { isPathInsideRoot } from "../../../utils/path.js";
|
|
4
5
|
const INSTRUCTION_KINDS = new Set([
|
|
5
6
|
"cursor-rule-mdc",
|
|
@@ -12,6 +13,73 @@ const INSTRUCTION_KINDS = new Set([
|
|
|
12
13
|
]);
|
|
13
14
|
/** Extensions accepted for bare filenames (no `/`) — precision over recall. */
|
|
14
15
|
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;
|
|
16
|
+
/** Host-like first segments for Go/npm module paths (`github.com/…`, `golang.org/…`). */
|
|
17
|
+
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;
|
|
18
|
+
/** Control-flow / language tokens that appear as `a/b` in prose, not filesystem paths. */
|
|
19
|
+
const CODE_SLASH_TOKEN = /^(?:try\/finally|if\/else|for\/of|for\/in|async\/await|do\/while)$/i;
|
|
20
|
+
/**
|
|
21
|
+
* First path segment of Go standard-library import paths (`io/ioutil`, `net/http`).
|
|
22
|
+
* Kept narrow so `src/utils`-style repo paths still validate.
|
|
23
|
+
*/
|
|
24
|
+
const GO_STDLIB_ROOTS = new Set([
|
|
25
|
+
"archive",
|
|
26
|
+
"bufio",
|
|
27
|
+
"bytes",
|
|
28
|
+
"cmp",
|
|
29
|
+
"compress",
|
|
30
|
+
"container",
|
|
31
|
+
"context",
|
|
32
|
+
"crypto",
|
|
33
|
+
"database",
|
|
34
|
+
"debug",
|
|
35
|
+
"embed",
|
|
36
|
+
"encoding",
|
|
37
|
+
"errors",
|
|
38
|
+
"expvar",
|
|
39
|
+
"flag",
|
|
40
|
+
"fmt",
|
|
41
|
+
"go",
|
|
42
|
+
"hash",
|
|
43
|
+
"html",
|
|
44
|
+
"image",
|
|
45
|
+
"index",
|
|
46
|
+
"io",
|
|
47
|
+
"log",
|
|
48
|
+
"maps",
|
|
49
|
+
"math",
|
|
50
|
+
"mime",
|
|
51
|
+
"net",
|
|
52
|
+
"os",
|
|
53
|
+
"path",
|
|
54
|
+
"plugin",
|
|
55
|
+
"reflect",
|
|
56
|
+
"regexp",
|
|
57
|
+
"runtime",
|
|
58
|
+
"slices",
|
|
59
|
+
"structs",
|
|
60
|
+
"sync",
|
|
61
|
+
"syscall",
|
|
62
|
+
"testing",
|
|
63
|
+
"text",
|
|
64
|
+
"time",
|
|
65
|
+
"unicode",
|
|
66
|
+
"unique",
|
|
67
|
+
"unsafe",
|
|
68
|
+
"weak",
|
|
69
|
+
]);
|
|
70
|
+
/** Optional build-output roots often documented before `npm run build`. */
|
|
71
|
+
const OPTIONAL_BUILD_ROOTS = new Set(["dist", "build", "out", "target", ".next", "coverage"]);
|
|
72
|
+
function isGoStdImportPath(normalized) {
|
|
73
|
+
const parts = normalized.split("/");
|
|
74
|
+
if (parts.length < 2 || parts.length > 4) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const root = parts[0] ?? "";
|
|
78
|
+
if (!GO_STDLIB_ROOTS.has(root)) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return parts.every((part) => /^[a-z][a-z0-9]*$/.test(part));
|
|
82
|
+
}
|
|
15
83
|
function stripAnchor(value) {
|
|
16
84
|
return value.split("#")[0] ?? value;
|
|
17
85
|
}
|
|
@@ -42,12 +110,34 @@ export function isLikelyLocalPathReference(value) {
|
|
|
42
110
|
// CSS class / id-like bare selectors: `.content`, `.button`
|
|
43
111
|
if (/^\.[A-Za-z_][\w-]*$/.test(value))
|
|
44
112
|
return false;
|
|
113
|
+
// npm scoped packages: `@scope/name`
|
|
114
|
+
if (/^@[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+/.test(value))
|
|
115
|
+
return false;
|
|
116
|
+
// Glob / brace patterns are not concrete paths
|
|
117
|
+
if (/[*?[\]{}]/.test(value))
|
|
118
|
+
return false;
|
|
119
|
+
// Language tokens that look like paths (`try/finally`)
|
|
120
|
+
if (CODE_SLASH_TOKEN.test(value))
|
|
121
|
+
return false;
|
|
122
|
+
const withoutDotSlash = value.replace(/^\.\//, "");
|
|
123
|
+
const trimmedDir = withoutDotSlash.replace(/\/+$/, "");
|
|
124
|
+
// Documented build outputs often missing until a local build
|
|
125
|
+
if (OPTIONAL_BUILD_ROOTS.has(trimmedDir))
|
|
126
|
+
return false;
|
|
45
127
|
if (value.includes("/")) {
|
|
46
|
-
// Reject empty segments like `foo//bar` while allowing trailing slash dirs
|
|
47
128
|
const normalized = value.replace(/\/+$/, "");
|
|
48
129
|
if (!normalized || normalized.split("/").some((part) => part === "")) {
|
|
49
130
|
return false;
|
|
50
131
|
}
|
|
132
|
+
const firstSegment = normalized.split("/")[0] ?? "";
|
|
133
|
+
// Go/npm module imports: `github.com/foo/bar`, `golang.org/x/…`
|
|
134
|
+
if (MODULE_HOST_SEGMENT.test(firstSegment)) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
// Go stdlib: `io/ioutil`, `net/http`, `path/filepath`
|
|
138
|
+
if (isGoStdImportPath(normalized)) {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
51
141
|
return true;
|
|
52
142
|
}
|
|
53
143
|
// Bare filename: require a known file extension (not `.content`-style tokens)
|
|
@@ -75,7 +165,11 @@ function isEscapingRepoRelative(relativePath) {
|
|
|
75
165
|
/**
|
|
76
166
|
* Resolve a candidate path reference with repository-aware semantics:
|
|
77
167
|
* - `./` and `../` → relative to the instruction file directory
|
|
78
|
-
* - otherwise → repository-root relative
|
|
168
|
+
* - otherwise → repository-root relative, plus the same path under the
|
|
169
|
+
* instruction file directory when nested (monorepo package docs)
|
|
170
|
+
*
|
|
171
|
+
* Does not search sibling packages: a root `AGENTS.md` reference must still
|
|
172
|
+
* exist at the repository root (or via `./` / `../`).
|
|
79
173
|
*/
|
|
80
174
|
export function resolveInstructionPathReference(instructionRelativePath, candidate) {
|
|
81
175
|
const instructionDir = path.posix.dirname(instructionRelativePath);
|
|
@@ -96,9 +190,18 @@ export function resolveInstructionPathReference(instructionRelativePath, candida
|
|
|
96
190
|
if (isEscapingRepoRelative(rootRelative) || rootRelative.startsWith("/")) {
|
|
97
191
|
return { status: "escape", attempted: rootRelative };
|
|
98
192
|
}
|
|
193
|
+
const pathsToCheck = [rootRelative];
|
|
194
|
+
if (instructionDir !== ".") {
|
|
195
|
+
const fromInstruction = path.posix.normalize(path.posix.join(instructionDir, rootRelative));
|
|
196
|
+
if (!isEscapingRepoRelative(fromInstruction) &&
|
|
197
|
+
!fromInstruction.startsWith("/") &&
|
|
198
|
+
fromInstruction !== rootRelative) {
|
|
199
|
+
pathsToCheck.push(fromInstruction);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
99
202
|
return {
|
|
100
203
|
status: "ok",
|
|
101
|
-
pathsToCheck
|
|
204
|
+
pathsToCheck,
|
|
102
205
|
primaryRelative: rootRelative,
|
|
103
206
|
};
|
|
104
207
|
}
|
|
@@ -129,20 +232,24 @@ export const missingPathReferenceRule = {
|
|
|
129
232
|
}
|
|
130
233
|
const candidates = extractPathCandidates(cached.text);
|
|
131
234
|
for (const candidate of candidates) {
|
|
132
|
-
const
|
|
235
|
+
const prepared = preparePathReference(candidate);
|
|
236
|
+
if (prepared.status === "reject") {
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
const resolved = resolveInstructionPathReference(file.relativePath, prepared.normalized);
|
|
133
240
|
if (resolved.status === "escape") {
|
|
134
241
|
findings.push({
|
|
135
242
|
ruleId: "instructions/missing-path-reference",
|
|
136
243
|
category: "instructions",
|
|
137
244
|
severity: "warning",
|
|
138
245
|
title: "Instruction references a missing path",
|
|
139
|
-
message: `${file.relativePath} references \`${
|
|
246
|
+
message: `${file.relativePath} references \`${prepared.original}\`, which escapes the repository root`,
|
|
140
247
|
whyItMatters: "Path references outside the repository cannot be validated and may confuse agents.",
|
|
141
248
|
recommendation: "Use repository-relative paths only.",
|
|
142
249
|
affectedAgents: context.agents
|
|
143
250
|
.filter((a) => a.configPaths.includes(file.relativePath))
|
|
144
251
|
.map((a) => a.id),
|
|
145
|
-
evidence: { path: file.relativePath, detail: `ref=${
|
|
252
|
+
evidence: { path: file.relativePath, detail: `ref=${prepared.original}` },
|
|
146
253
|
fixability: "manual",
|
|
147
254
|
});
|
|
148
255
|
continue;
|
|
@@ -166,7 +273,7 @@ export const missingPathReferenceRule = {
|
|
|
166
273
|
category: "instructions",
|
|
167
274
|
severity: "warning",
|
|
168
275
|
title: "Instruction references a missing path",
|
|
169
|
-
message: `${file.relativePath} references \`${
|
|
276
|
+
message: `${file.relativePath} references \`${prepared.original}\`, but that path does not exist`,
|
|
170
277
|
whyItMatters: "Agents may follow documented paths that no longer exist, causing failed reads and wasted context.",
|
|
171
278
|
recommendation: "Fix the path or remove the stale reference.",
|
|
172
279
|
affectedAgents: context.agents
|
|
@@ -0,0 +1,27 @@
|
|
|
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 a single path segment denotes sample/test/fixture material.
|
|
7
|
+
* Handles exact names, leading-underscore variants, hyphen/underscore compounds,
|
|
8
|
+
* and camelCase forms (dockerTest, testFixtures) without matching production roots.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isSampleOrTestSegment(segment: string): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* True when any path segment marks fixture, test, example, or sample material.
|
|
13
|
+
* Uses POSIX relative paths (discovery output).
|
|
14
|
+
* Filenames never qualify alone (root `test-private-key.pem` must still flag).
|
|
15
|
+
*/
|
|
16
|
+
export declare function isSampleOrTestPath(relativePath: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* True for paths like `scripts/build`, `internal/build`, or `packages/foo/src/core/build`
|
|
19
|
+
* that are source trees named after a common artifact directory, not generated output.
|
|
20
|
+
* Any source-marker ancestor qualifies (not only the immediate parent).
|
|
21
|
+
*/
|
|
22
|
+
export declare function isSourceNamedArtifactCollision(relativePath: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* True for checked-in GitHub Action package output (`.github/actions/<name>/dist`).
|
|
25
|
+
* These are intentional publish artifacts, not ambient generated junk.
|
|
26
|
+
*/
|
|
27
|
+
export declare function isCheckedInGithubActionDist(relativePath: string): boolean;
|
|
@@ -0,0 +1,180 @@
|
|
|
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
|
+
const SAMPLE_DIRECTORY_NAMES = new Set([
|
|
6
|
+
"example",
|
|
7
|
+
"examples",
|
|
8
|
+
"fixture",
|
|
9
|
+
"fixtures",
|
|
10
|
+
"__fixtures__",
|
|
11
|
+
"integration",
|
|
12
|
+
"integrations",
|
|
13
|
+
"mock",
|
|
14
|
+
"mocks",
|
|
15
|
+
"__mocks__",
|
|
16
|
+
"sample",
|
|
17
|
+
"samples",
|
|
18
|
+
"test",
|
|
19
|
+
"tests",
|
|
20
|
+
"testing",
|
|
21
|
+
"__tests__",
|
|
22
|
+
"testdata",
|
|
23
|
+
"test_data",
|
|
24
|
+
"spec",
|
|
25
|
+
"specs",
|
|
26
|
+
"demo",
|
|
27
|
+
"demos",
|
|
28
|
+
"e2e",
|
|
29
|
+
"bench",
|
|
30
|
+
"benchmark",
|
|
31
|
+
"benchmarks",
|
|
32
|
+
"playground",
|
|
33
|
+
"playgrounds",
|
|
34
|
+
"sandbox",
|
|
35
|
+
"sandboxes",
|
|
36
|
+
]);
|
|
37
|
+
/** Strong tokens that mark intentional non-production material when present in a segment. */
|
|
38
|
+
const STRONG_TEST_TOKENS = new Set([
|
|
39
|
+
"test",
|
|
40
|
+
"tests",
|
|
41
|
+
"testing",
|
|
42
|
+
"fixture",
|
|
43
|
+
"fixtures",
|
|
44
|
+
"mock",
|
|
45
|
+
"mocks",
|
|
46
|
+
"testdata",
|
|
47
|
+
"spec",
|
|
48
|
+
"specs",
|
|
49
|
+
"e2e",
|
|
50
|
+
"bench",
|
|
51
|
+
"benchmark",
|
|
52
|
+
"benchmarks",
|
|
53
|
+
"playground",
|
|
54
|
+
"playgrounds",
|
|
55
|
+
"sandbox",
|
|
56
|
+
"sandboxes",
|
|
57
|
+
"example",
|
|
58
|
+
"examples",
|
|
59
|
+
"sample",
|
|
60
|
+
"samples",
|
|
61
|
+
"demo",
|
|
62
|
+
"demos",
|
|
63
|
+
]);
|
|
64
|
+
/**
|
|
65
|
+
* Artifact dir names that commonly collide with source packages
|
|
66
|
+
* (scripts/build, packages/foo/src/core/build, src/.../vendor).
|
|
67
|
+
* Intentionally excludes dist — paths like src/js/dist are often real build output.
|
|
68
|
+
*/
|
|
69
|
+
const ARTIFACT_NAMES_WITH_SOURCE_COLLISIONS = new Set(["build", "target", "vendor"]);
|
|
70
|
+
/**
|
|
71
|
+
* Dir names that host source code packages/modules, not generated output trees.
|
|
72
|
+
* packages/foo/dist is still treated as generated (no source-marker ancestor).
|
|
73
|
+
*/
|
|
74
|
+
const SOURCE_CODE_MARKER_DIRS = new Set([
|
|
75
|
+
"bin",
|
|
76
|
+
"cmd",
|
|
77
|
+
"internal",
|
|
78
|
+
"lib",
|
|
79
|
+
"pkg",
|
|
80
|
+
"script",
|
|
81
|
+
"scripts",
|
|
82
|
+
"src",
|
|
83
|
+
]);
|
|
84
|
+
function splitCamelCase(segment) {
|
|
85
|
+
return segment
|
|
86
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1\0$2")
|
|
87
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1\0$2")
|
|
88
|
+
.split("\0")
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* True when a single path segment denotes sample/test/fixture material.
|
|
93
|
+
* Handles exact names, leading-underscore variants, hyphen/underscore compounds,
|
|
94
|
+
* and camelCase forms (dockerTest, testFixtures) without matching production roots.
|
|
95
|
+
*/
|
|
96
|
+
export function isSampleOrTestSegment(segment) {
|
|
97
|
+
if (!segment) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const lower = segment.toLowerCase();
|
|
101
|
+
if (SAMPLE_DIRECTORY_NAMES.has(lower)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
// _fixture, _fixtures, _testdata, __fixture
|
|
105
|
+
const stripped = lower.replace(/^_+/, "");
|
|
106
|
+
if (stripped !== lower && SAMPLE_DIRECTORY_NAMES.has(stripped)) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
// integration-test, smoke-test, docker-test, integration_tests, test-certs
|
|
110
|
+
const hyphenTokens = lower.split(/[-_]+/).filter(Boolean);
|
|
111
|
+
if (hyphenTokens.length >= 2 && hyphenTokens.some((token) => STRONG_TEST_TOKENS.has(token))) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
// dockerTest, testFixtures, httpTestServer, integrationTests
|
|
115
|
+
if (/[A-Z]/.test(segment)) {
|
|
116
|
+
const camelTokens = splitCamelCase(segment).map((part) => part.toLowerCase());
|
|
117
|
+
if (camelTokens.some((token) => STRONG_TEST_TOKENS.has(token))) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* True when any path segment marks fixture, test, example, or sample material.
|
|
125
|
+
* Uses POSIX relative paths (discovery output).
|
|
126
|
+
* Filenames never qualify alone (root `test-private-key.pem` must still flag).
|
|
127
|
+
*/
|
|
128
|
+
export function isSampleOrTestPath(relativePath) {
|
|
129
|
+
if (!relativePath) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
133
|
+
if (!normalized) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
const parts = normalized.split("/").filter(Boolean);
|
|
137
|
+
return parts.some((segment, index) => {
|
|
138
|
+
const isLast = index === parts.length - 1;
|
|
139
|
+
const looksLikeFile = isLast && /\.[A-Za-z0-9_+-]+$/.test(segment);
|
|
140
|
+
if (looksLikeFile) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return isSampleOrTestSegment(segment);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* True for paths like `scripts/build`, `internal/build`, or `packages/foo/src/core/build`
|
|
148
|
+
* that are source trees named after a common artifact directory, not generated output.
|
|
149
|
+
* Any source-marker ancestor qualifies (not only the immediate parent).
|
|
150
|
+
*/
|
|
151
|
+
export function isSourceNamedArtifactCollision(relativePath) {
|
|
152
|
+
if (!relativePath) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
const parts = relativePath
|
|
156
|
+
.replace(/\\/g, "/")
|
|
157
|
+
.replace(/^\.\//, "")
|
|
158
|
+
.replace(/\/+$/, "")
|
|
159
|
+
.split("/")
|
|
160
|
+
.filter(Boolean);
|
|
161
|
+
if (parts.length < 2) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
const leaf = parts[parts.length - 1].toLowerCase();
|
|
165
|
+
if (!ARTIFACT_NAMES_WITH_SOURCE_COLLISIONS.has(leaf)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
return parts.slice(0, -1).some((segment) => SOURCE_CODE_MARKER_DIRS.has(segment.toLowerCase()));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* True for checked-in GitHub Action package output (`.github/actions/<name>/dist`).
|
|
172
|
+
* These are intentional publish artifacts, not ambient generated junk.
|
|
173
|
+
*/
|
|
174
|
+
export function isCheckedInGithubActionDist(relativePath) {
|
|
175
|
+
if (!relativePath) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
const normalized = relativePath.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
179
|
+
return /^\.github\/actions\/[^/]+\/dist$/i.test(normalized);
|
|
180
|
+
}
|