Skip to content
Merged
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
32 changes: 32 additions & 0 deletions src/review/changed-files-classify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { isConfigFile, isDocsFile, isGeneratedFile, isLockfile, isMinifiedFile, isVendoredFile } from "../signals/path-matchers";
import { isTestFile } from "../signals/local-branch";
import { isTestPath } from "../signals/test-evidence";

// Deterministic changed-file classifier for the review changed-files summary (#2143, part of #1957). Maps a changed
// file PATH to exactly one of five review-oriented buckets so the summary table (and future analytics) group
// deterministically. Pure + path-only — no diff content, no IO — composing the existing hardened path-matchers.
//
// NOTE: this is DISTINCT from `classifyChangedFile` in src/signals/path-matchers.ts, which returns the finer-grained
// 10-way slop category with a DIFFERENT precedence (it ranks config above test). This review classifier deliberately
// uses its own precedence below, so it can't just fold that one.

/** The five review-summary buckets a changed file maps to. */
export type ReviewFileClass = "source" | "test" | "docs" | "config" | "generated";

/**
* Classify a changed file path into one review bucket. FIXED precedence — `generated > test > docs > config > source`
* — so a file matching several buckets (a generated test file, a lockfile, a vendored fixture) always resolves to the
* higher-precedence class deterministically:
* - `generated`: machine-produced/imported output (generated markers, vendored trees, lockfiles, minified bundles) —
* never real hand-authored effort, so it outranks everything.
* - `test`, `docs`, `config`: the remaining recognized categories, in that order.
* - `source`: anything unrecognized (including plain code) falls through here.
* Pure.
*/
export function classifyChangedFile(path: string): ReviewFileClass {
if (isGeneratedFile(path) || isVendoredFile(path) || isLockfile(path) || isMinifiedFile(path)) return "generated";
if (isTestFile(path) || isTestPath(path)) return "test";
if (isDocsFile(path)) return "docs";
if (isConfigFile(path)) return "config";
return "source";
}
46 changes: 46 additions & 0 deletions test/unit/changed-files-classify.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { classifyChangedFile } from "../../src/review/changed-files-classify";
import { isConfigFile, isDocsFile, isGeneratedFile, isLockfile, isMinifiedFile, isVendoredFile } from "../../src/signals/path-matchers";
import { isTestFile } from "../../src/signals/local-branch";

describe("classifyChangedFile (#2143)", () => {
it("source: plain hand-authored code that matches no other bucket", () => {
expect(classifyChangedFile("src/app.ts")).toBe("source");
});

it("test: a test file", () => {
expect(isTestFile("src/app.test.ts")).toBe(true);
expect(classifyChangedFile("src/app.test.ts")).toBe("test");
});

it("docs: a markdown doc", () => {
expect(isDocsFile("docs/guide.md")).toBe(true);
expect(classifyChangedFile("docs/guide.md")).toBe("docs");
});

it("config: a config file", () => {
expect(isConfigFile(".eslintrc.json")).toBe(true);
expect(classifyChangedFile(".eslintrc.json")).toBe("config");
});

it("generated: generated / vendored / lockfile / minified all fold to generated", () => {
expect(isGeneratedFile("src/api.generated.ts")).toBe(true);
expect(classifyChangedFile("src/api.generated.ts")).toBe("generated");
expect(isLockfile("package-lock.json")).toBe(true);
expect(classifyChangedFile("package-lock.json")).toBe("generated");
expect(isVendoredFile("vendor/jquery.js")).toBe(true);
expect(classifyChangedFile("vendor/jquery.js")).toBe("generated");
expect(isMinifiedFile("dist/app.min.js")).toBe(true);
expect(classifyChangedFile("dist/app.min.js")).toBe("generated");
});

it("precedence: a vendored file that is ALSO a test → generated (generated > test)", () => {
expect(isTestFile("vendor/foo.test.js")).toBe(true); // it is a test file
expect(isVendoredFile("vendor/foo.test.js")).toBe(true); // and vendored
expect(classifyChangedFile("vendor/foo.test.js")).toBe("generated"); // generated outranks test
});

it("unknown → source (the 5-bucket set has no 'other')", () => {
expect(classifyChangedFile("assets/logo.bin")).toBe("source");
});
});
Loading