From 6d939119b54257be957c7d233232be243f58b757 Mon Sep 17 00:00:00 2001 From: jaso0n0818 Date: Mon, 22 Jun 2026 15:46:12 +0000 Subject: [PATCH] feat(signals): classify build and CI configuration files in path-matchers (#561) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `isConfigFile()` to distinguish build, lint, test-runner, and environment config files from source code and non-substantive padding. Expand `ChangedFileCategory` with a `"config"` bucket and wire it into `classifyChangedFile` after dependency manifests so config-only diffs are no longer lumped with source effort. Recognised patterns: exact basenames (Dockerfile, Makefile, .editorconfig …), known prefixes (tsconfig, vitest.config, .env, .eslint …), the generic `.config.ext` / `.rc.ext` form, and dot-prefixed bare-rc files (.stylelintrc, .huskyrc). Lockfiles and dependency manifests still rank above config in the classifier, preserving the existing priority order. Co-Authored-By: Claude Sonnet 4.6 --- src/signals/path-matchers.ts | 44 +++++++++++++++++++++++++++++++++ test/unit/path-matchers.test.ts | 38 +++++++++++++++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) diff --git a/src/signals/path-matchers.ts b/src/signals/path-matchers.ts index 15a384806a..8b18f878dc 100644 --- a/src/signals/path-matchers.ts +++ b/src/signals/path-matchers.ts @@ -57,6 +57,34 @@ const DEPENDENCY_MANIFEST_NAMES: ReadonlySet = new Set([ const DOCS_EXTENSIONS: ReadonlySet = new Set(["md", "mdx", "markdown", "rst", "adoc", "asciidoc"]); +// Exact basenames (lowercased) that are unambiguously build/CI config files regardless of directory. +const CONFIG_FILE_NAMES: ReadonlySet = new Set([ + "dockerfile", + "makefile", + ".editorconfig", + ".nvmrc", + ".npmrc", + ".browserslistrc", +]); + +// Filename prefixes that identify build, lint, test-runner, and environment config files. +const CONFIG_FILE_PREFIXES: readonly string[] = [ + "tsconfig", + "jsconfig", + "jest.config", + "vitest.config", + "vite.config", + "webpack.config", + "rollup.config", + "postcss.config", + "tailwind.config", + "next.config", + ".env", + ".eslint", + ".prettier", + ".babel", +]; + /** Machine-generated output (codegen, protobuf, source maps, typegen). */ export function isGeneratedFile(path: string): boolean { const norm = normalize(path); @@ -97,6 +125,20 @@ export function isDependencyManifestFile(path: string): boolean { return DEPENDENCY_MANIFEST_NAMES.has(basename(path)); } +/** + * Build, lint, test-runner, and environment configuration files. Distinct from dependency manifests + * (which declare external dependencies) and source code. Config-only diffs are lower-effort than + * genuine source changes, so slop signals can weight them differently (#561). + */ +export function isConfigFile(path: string): boolean { + const base = basename(path); + if (CONFIG_FILE_NAMES.has(base)) return true; + if (CONFIG_FILE_PREFIXES.some((prefix) => base.startsWith(prefix))) return true; + if (/\.(config|rc)\.[a-z0-9]+$/i.test(base)) return true; + // `.stylelintrc`-style: dot-prefixed name with no extension after "rc"; `custom.rc`: dotted rc extension. + return base.endsWith(".rc") || /^\.[^.]+rc$/i.test(base); +} + /** * Files that masquerade as substantive source/work but are machine-produced or imported — the set a * padded diff inflates its size with. Lockfiles, dependency manifests, and docs are legitimate change @@ -112,6 +154,7 @@ export type ChangedFileCategory = | "vendored" | "lockfile" | "dependency_manifest" + | "config" | "test" | "docs" | "source" @@ -128,6 +171,7 @@ export function classifyChangedFile(path: string): ChangedFileCategory { if (isVendoredFile(path)) return "vendored"; if (isLockfile(path)) return "lockfile"; if (isDependencyManifestFile(path)) return "dependency_manifest"; + if (isConfigFile(path)) return "config"; if (isTestFile(path) || isTestPath(path)) return "test"; if (isDocsFile(path)) return "docs"; if (isCodeFile(path)) return "source"; diff --git a/test/unit/path-matchers.test.ts b/test/unit/path-matchers.test.ts index 0550f59b65..bb625044af 100644 --- a/test/unit/path-matchers.test.ts +++ b/test/unit/path-matchers.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { classifyChangedFile, isDependencyManifestFile, + isConfigFile, isDocsFile, isGeneratedFile, isLockfile, @@ -140,6 +141,38 @@ describe("isNonSubstantivePaddingFile", () => { }); }); +describe("isConfigFile", () => { + it("matches config files by exact basename (case-insensitive)", () => { + for (const path of ["Dockerfile", "frontend/Makefile", ".editorconfig", "ci/.nvmrc", ".npmrc"]) { + expect(isConfigFile(path)).toBe(true); + } + }); + + 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("matches config files by the .config.ext or .rc.ext pattern", () => { + for (const path of ["babel.config.cjs", "stylelint.config.mjs", "lint-staged.rc.js"]) { + expect(isConfigFile(path)).toBe(true); + } + }); + + it("matches bare .rc suffix config files", () => { + for (const path of [".stylelintrc", ".huskyrc", "config/custom.rc"]) { + expect(isConfigFile(path)).toBe(true); + } + }); + + it("does not classify source, test, doc, or lockfiles as config", () => { + for (const path of ["src/app.ts", "README.md", "package.json", "package-lock.json", "test/unit/app.test.ts"]) { + expect(isConfigFile(path)).toBe(false); + } + }); +}); + describe("classifyChangedFile", () => { it("classifies each representative path into its category", () => { const cases: Array<[string, ReturnType]> = [ @@ -148,6 +181,8 @@ describe("classifyChangedFile", () => { ["vendor/lib.go", "vendored"], ["package-lock.json", "lockfile"], ["package.json", "dependency_manifest"], + ["tsconfig.json", "config"], + ["vitest.config.ts", "config"], ["test/unit/app.test.ts", "test"], ["README.md", "docs"], ["src/app.ts", "source"], @@ -158,9 +193,10 @@ describe("classifyChangedFile", () => { } }); - it("prioritizes padding categories over test/source so they are never counted as effort", () => { + it("prioritizes padding categories over config/test/source so they are never counted as effort", () => { expect(classifyChangedFile("__generated__/schema.test.ts")).toBe("generated"); expect(classifyChangedFile("vendor/pkg/index.test.js")).toBe("vendored"); expect(classifyChangedFile("dist/bundle.min.js")).toBe("minified"); + expect(classifyChangedFile("vendor/tsconfig.json")).toBe("vendored"); }); });