Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/signals/path-matchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const LOCKFILE_NAMES: ReadonlySet<string> = new Set([
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lock",
"bun.lockb",
"cargo.lock",
"poetry.lock",
Expand All @@ -39,12 +40,16 @@ const LOCKFILE_NAMES: ReadonlySet<string> = new Set([
"uv.lock",
"packages.lock.json",
"flake.lock",
"deno.lock",
"pubspec.lock",
"podfile.lock",
]);

const DEPENDENCY_MANIFEST_NAMES: ReadonlySet<string> = new Set([
"package.json",
"cargo.toml",
"go.mod",
"go.work",
"requirements.txt",
"pyproject.toml",
"pipfile",
Expand All @@ -53,6 +58,11 @@ const DEPENDENCY_MANIFEST_NAMES: ReadonlySet<string> = new Set([
"build.gradle",
"build.gradle.kts",
"pom.xml",
"deno.json",
"deno.jsonc",
"pubspec.yaml",
"mix.exs",
"uv.toml",
]);

const DOCS_EXTENSIONS: ReadonlySet<string> = new Set(["md", "mdx", "markdown", "rst", "adoc", "asciidoc"]);
Expand All @@ -61,17 +71,41 @@ const DOCS_EXTENSIONS: ReadonlySet<string> = new Set(["md", "mdx", "markdown", "
const CONFIG_FILE_NAMES: ReadonlySet<string> = new Set([
"dockerfile",
"makefile",
"caddyfile",
"justfile",
"procfile",
"codeowners",
".editorconfig",
".nvmrc",
".npmrc",
".browserslistrc",
".tool-versions",
".pre-commit-config.yaml",
".gitleaks.toml",
".gittensory.yml",
".gittensory.yml.example",
".gitlab-ci.yml",
"azure-pipelines.yml",
"jenkinsfile",
// Monorepo / task-runner config (Turborepo, Nx, Lerna).
"turbo.json",
"nx.json",
"lerna.json",
// Linter / formatter config that does not follow the `.eslintrc` / `*.config.*` shapes (Biome).
"biome.json",
"biome.jsonc",
// Deploy / automation / quality config.
"codecov.yml",
"renovate.json",
"railway.json",
"vercel.json",
"netlify.toml",
"mise.toml",
"cliff.toml",
"taskfile.yml",
"taskfile.yaml",
"docker-compose.yml",
"docker-compose.yaml",
// VCS and build ignore/attribute config (siblings to the existing Dockerfile entry).
".gitignore",
".gitattributes",
Expand All @@ -90,6 +124,8 @@ const CONFIG_FILE_PREFIXES: readonly string[] = [
"postcss.config",
"tailwind.config",
"next.config",
"playwright.config",
"eslint.config",
".env",
".eslint",
".prettier",
Expand All @@ -99,6 +135,16 @@ const CONFIG_FILE_PREFIXES: readonly string[] = [
"wrangler.",
];

/** CI workflow and automation entrypoints under conventional directories. */
function isCiConfigPath(path: string): boolean {
const norm = normalize(path);
return (
/(^|\/)\.github\/workflows\/[^/]+\.(ya?ml)$/.test(norm) ||
/(^|\/)\.github\/dependabot\.ya?ml$/.test(norm) ||
/(^|\/)\.circleci\/config\.ya?ml$/.test(norm)
);
}

/** Machine-generated output (codegen, protobuf, source maps, typegen). */
export function isGeneratedFile(path: string): boolean {
const norm = normalize(path);
Expand Down Expand Up @@ -145,6 +191,7 @@ export function isDependencyManifestFile(path: string): boolean {
* lower-effort than genuine source changes, so slop signals can weight them differently (#561).
*/
export function isConfigFile(path: string): boolean {
if (isCiConfigPath(path)) return true;
const base = basename(path);
if (CONFIG_FILE_NAMES.has(base)) return true;
if (CONFIG_FILE_PREFIXES.some((prefix) => base.startsWith(prefix))) return true;
Expand Down
71 changes: 66 additions & 5 deletions test/unit/path-matchers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ describe("isLockfile", () => {
"go.sum",
"uv.lock",
"poetry.lock",
"deno.lock",
"bun.lock",
"pubspec.lock",
"ios/Podfile.lock",
]) {
expect(isLockfile(path)).toBe(true);
}
Expand Down Expand Up @@ -115,7 +119,20 @@ describe("defensive input handling", () => {

describe("isDependencyManifestFile", () => {
it("matches dependency manifests", () => {
for (const path of ["package.json", "Cargo.toml", "go.mod", "requirements.txt", "pyproject.toml", "build.gradle.kts"]) {
for (const path of [
"package.json",
"Cargo.toml",
"go.mod",
"go.work",
"requirements.txt",
"pyproject.toml",
"build.gradle.kts",
"deno.json",
"deno.jsonc",
"pubspec.yaml",
"mix.exs",
"uv.toml",
]) {
expect(isDependencyManifestFile(path)).toBe(true);
}
});
Expand Down Expand Up @@ -148,7 +165,7 @@ describe("isConfigFile", () => {
}
});

it("matches monorepo, linter, and VCS/build config by exact basename", () => {
it("matches monorepo, linter, VCS/build, deploy, and toolchain config by exact basename", () => {
for (const path of [
"turbo.json",
"nx.json",
Expand All @@ -158,11 +175,49 @@ describe("isConfigFile", () => {
"packages/app/.gitignore",
".gitattributes",
"services/api/.dockerignore",
".pre-commit-config.yaml",
".gitleaks.toml",
".gittensory.yml",
".gittensory.yml.example",
"codecov.yml",
"renovate.json",
"railway.json",
"vercel.json",
"netlify.toml",
"mise.toml",
".tool-versions",
"Taskfile.yml",
"Taskfile.yaml",
"justfile",
"Caddyfile",
"docker-compose.yml",
"Procfile",
"CODEOWNERS",
".gitlab-ci.yml",
"azure-pipelines.yml",
"Jenkinsfile",
]) {
expect(isConfigFile(path)).toBe(true);
}
});

it("matches GitHub Actions and other CI workflow entrypoints", () => {
for (const path of [
".github/workflows/ci.yml",
".github/workflows/release.yaml",
".github/dependabot.yml",
".circleci/config.yml",
]) {
expect(isConfigFile(path)).toBe(true);
}
});

it("matches test-runner and lint config by known filename prefix", () => {
for (const path of ["tsconfig.build.json", "vitest.config.ts", ".env.local", ".eslintrc.json", ".prettierrc.js", "playwright.config.ts", "eslint.config.js"]) {
expect(isConfigFile(path)).toBe(true);
}
});

it("matches Cloudflare Workers deploy config by the wrangler prefix", () => {
for (const path of ["wrangler.toml", "wrangler.jsonc", "apps/ui/wrangler.vitest.jsonc"]) {
expect(isConfigFile(path)).toBe(true);
Expand All @@ -175,9 +230,9 @@ describe("isConfigFile", () => {
}
});

it("matches config files by known filename prefix", () => {
for (const path of ["tsconfig.build.json", "vitest.config.ts", ".env.local", ".eslintrc.json", ".prettierrc.js"]) {
expect(isConfigFile(path)).toBe(true);
it("does not classify workflow-like names outside CI directories as config", () => {
for (const path of ["docs/ci.yml", "scripts/release.yaml", "src/workflow.ts"]) {
expect(isConfigFile(path)).toBe(false);
}
});

Expand Down Expand Up @@ -207,10 +262,16 @@ describe("classifyChangedFile", () => {
["src/api.generated.ts", "generated"],
["vendor/lib.go", "vendored"],
["package-lock.json", "lockfile"],
["deno.lock", "lockfile"],
["pubspec.lock", "lockfile"],
["package.json", "dependency_manifest"],
["deno.json", "dependency_manifest"],
["pubspec.yaml", "dependency_manifest"],
["tsconfig.json", "config"],
["vitest.config.ts", "config"],
["wrangler.jsonc", "config"],
[".github/workflows/ci.yml", "config"],
[".pre-commit-config.yaml", "config"],
["turbo.json", "config"],
["test/unit/app.test.ts", "test"],
["README.md", "docs"],
Expand Down
Loading