Skip to content
Merged
135 changes: 135 additions & 0 deletions src/signals/path-matchers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { isCodeFile, isTestFile } from "./local-branch";

Check warning on line 1 in src/signals/path-matchers.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #561.

Check notice on line 1 in src/signals/path-matchers.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #561.

Check notice on line 1 in src/signals/path-matchers.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in src/signals/path-matchers.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { isTestPath } from "./test-evidence";

// Pure, deterministic path matchers for slop classification (#561). Siblings to `isTestFile` /
// `isTestPath`: they identify changed files that are NOT genuine hand-authored effort — machine-
// generated output, vendored/imported third-party code, minified bundles, dependency lockfiles, and
// docs — so slop signals can tell a padded diff from real work. Path-only and side-effect-free.

function normalize(path: string): string {
return String(path ?? "")
.replace(/\\/g, "/")
.toLowerCase();
}

function basename(path: string): string {
const norm = normalize(path);
const slash = norm.lastIndexOf("/");
return slash >= 0 ? norm.slice(slash + 1) : norm;
}

function extension(path: string): string {
const base = basename(path);
const dot = base.lastIndexOf(".");
return dot > 0 ? base.slice(dot + 1) : "";
}

const LOCKFILE_NAMES: ReadonlySet<string> = new Set([
"package-lock.json",
"npm-shrinkwrap.json",
"yarn.lock",
"pnpm-lock.yaml",
"bun.lockb",
"cargo.lock",
"poetry.lock",
"pipfile.lock",
"composer.lock",
"gemfile.lock",
"go.sum",
"uv.lock",
"packages.lock.json",
"flake.lock",
]);

const DEPENDENCY_MANIFEST_NAMES: ReadonlySet<string> = new Set([
"package.json",
"cargo.toml",
"go.mod",
"requirements.txt",
"pyproject.toml",
"pipfile",
"gemfile",
"composer.json",
"build.gradle",
"build.gradle.kts",
"pom.xml",
]);

const DOCS_EXTENSIONS: ReadonlySet<string> = new Set(["md", "mdx", "markdown", "rst", "adoc", "asciidoc"]);

/** Machine-generated output (codegen, protobuf, source maps, typegen). */
export function isGeneratedFile(path: string): boolean {
const norm = normalize(path);
return (
/(^|\/)(__generated__|generated)\//.test(norm) ||
/\.(generated|gen)\.[^/]+$/.test(norm) ||
/\.pb\.(go|ts|js)$/.test(norm) ||
/_pb2\.pyi?$/.test(norm) ||
/\.g\.dart$/.test(norm) ||
/\.(js|jsx|ts|tsx|css)\.map$/.test(norm) ||
basename(norm) === "worker-configuration.d.ts"
);
}

/** Third-party / imported code that lives in the repo but is not the contributor's work. */
export function isVendoredFile(path: string): boolean {
return /(^|\/)(vendor|vendored|third_party|third-party|node_modules)\//.test(normalize(path));
}

/** Dependency lockfiles (resolved trees), e.g. `package-lock.json`, `go.sum`, `Cargo.lock`. */
export function isLockfile(path: string): boolean {
return LOCKFILE_NAMES.has(basename(path));
}

/** Minified bundles, e.g. `app.min.js`, `styles.min.css`. */
export function isMinifiedFile(path: string): boolean {
return /\.min\.[a-z0-9]+$/.test(normalize(path));
}

/** Documentation files (by extension or a top-level `docs/` directory). */
export function isDocsFile(path: string): boolean {
const norm = normalize(path);
return /(^|\/)docs?\//.test(norm) || DOCS_EXTENSIONS.has(extension(norm));
}

/** Dependency manifests (declare dependencies), e.g. `package.json`, `go.mod`, `pyproject.toml`. */
export function isDependencyManifestFile(path: string): boolean {
return DEPENDENCY_MANIFEST_NAMES.has(basename(path));
}

/**
* 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
* categories and are deliberately excluded here (they have their own matchers for reuse).
*/
export function isNonSubstantivePaddingFile(path: string): boolean {
return isMinifiedFile(path) || isGeneratedFile(path) || isVendoredFile(path);
}

export type ChangedFileCategory =
| "minified"
| "generated"
| "vendored"
| "lockfile"
| "dependency_manifest"
| "test"
| "docs"
| "source"
| "other";

/**
* Classify a changed file into a single category. Non-substantive padding categories
* (minified/generated/vendored) take precedence so they are never miscounted as substantive source
* or test effort; lockfiles and dependency manifests are recognized before generic docs/source.
*/
export function classifyChangedFile(path: string): ChangedFileCategory {
if (isMinifiedFile(path)) return "minified";
if (isGeneratedFile(path)) return "generated";
if (isVendoredFile(path)) return "vendored";
if (isLockfile(path)) return "lockfile";
if (isDependencyManifestFile(path)) return "dependency_manifest";
if (isTestFile(path) || isTestPath(path)) return "test";
if (isDocsFile(path)) return "docs";
if (isCodeFile(path)) return "source";
return "other";
}
61 changes: 60 additions & 1 deletion src/signals/slop.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { SignalFinding } from "./engine";

Check warning on line 1 in src/signals/slop.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Items reference the same linked issue #561.

Check notice on line 1 in src/signals/slop.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Open PR work references issue #561.

Check notice on line 1 in src/signals/slop.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Possible duplicate overlap

Titles/paths share 6 meaningful terms.

Check notice on line 1 in src/signals/slop.ts

View check run for this annotation

Deleted GitHub App / Gittensory Context

Issue discovery is disabled for this repo

This repo is configured for direct contribution review rather than issue-discovery flow.
import { isCodeFile, isTestFile } from "./local-branch";
import { hasLocalTestEvidence, isTestPath } from "./test-evidence";
import { isFocusManifestPublicSafe } from "./focus-manifest";
import { classifyChangedFile } from "./path-matchers";

export type SlopBand = "clean" | "low" | "elevated" | "high";

Expand All @@ -27,10 +28,12 @@

// Deterministic, high-precision signals only — this score is the ONLY thing allowed to gate (block), so it
// must be false-positive-averse. Heuristic/AI "this reads low-effort" judgments stay ADVISORY elsewhere and
// never feed this score. Weights sum to 75 so the `high` band (>=60) is reachable from two strong signals.
// never feed this score. Each "strong" signal is weighted 30 so the `high` band (>=60) is reachable from any
// two of them; `clamp(.,0,100)` keeps the stacked score bounded.
export const SLOP_WEIGHTS = {
trivialWhitespaceChurn: 30,
missingTestEvidence: 30,
nonSubstantivePadding: 30,
emptyDescription: 15,
} as const;

Expand All @@ -45,24 +48,31 @@
"Current deterministic signals:",
"- trivial / whitespace-only churn",
"- missing test evidence",
"- non-substantive padding (generated / vendored / minified output as source)",
"- empty pull request description on a code change",
].join("\n");

const MIN_CHURN_LINES = 40;
const MAX_SOURCE_LINE_SHARE = 0.15;
// A padded diff is one whose churn is dominated by non-substantive output. Set at half the diff so a PR
// with any meaningful share of real, hand-authored files cannot trip it.
const PADDING_DOMINANCE_SHARE = 0.5;

export function buildSlopAssessment(input: SlopAssessmentInput): SlopAssessment {
const findings: SignalFinding[] = [];
const trivialChurnFinding = buildTrivialWhitespaceChurnFinding(input);
const missingTestEvidenceFinding = buildMissingTestEvidenceFinding(input);
const nonSubstantivePaddingFinding = buildNonSubstantivePaddingFinding(input);
const emptyDescriptionFinding = buildEmptyDescriptionFinding(input);
if (trivialChurnFinding) findings.push(trivialChurnFinding);
if (missingTestEvidenceFinding) findings.push(missingTestEvidenceFinding);
if (nonSubstantivePaddingFinding) findings.push(nonSubstantivePaddingFinding);
if (emptyDescriptionFinding) findings.push(emptyDescriptionFinding);

const slopRisk = clamp(
(trivialChurnFinding ? SLOP_WEIGHTS.trivialWhitespaceChurn : 0) +
(missingTestEvidenceFinding ? SLOP_WEIGHTS.missingTestEvidence : 0) +
(nonSubstantivePaddingFinding ? SLOP_WEIGHTS.nonSubstantivePadding : 0) +
(emptyDescriptionFinding ? SLOP_WEIGHTS.emptyDescription : 0),
0,
100,
Expand All @@ -75,6 +85,55 @@
};
}

// Fires when a high-churn diff is dominated by generated/vendored/minified output (files that carry code
// extensions and so slip past the source-share check in `trivialWhitespaceChurn`) while genuine source and
// test effort is negligible — i.e. the diff is padded to look substantive. Lockfiles, dependency manifests,
// and docs are legitimate change categories and never count toward the padding share, so dependency bumps
// and docs PRs cannot trip this.
export function buildNonSubstantivePaddingFinding(input: SlopAssessmentInput): SignalFinding | null {
const totals = summarizePaddingLines(input.changedFiles ?? []);
if (totals.changedLineCount < MIN_CHURN_LINES) return null;
if (totals.paddingLineCount === 0) return null;
if (totals.paddingLineCount / totals.changedLineCount < PADDING_DOMINANCE_SHARE) return null;
if (totals.substantiveLineCount / totals.changedLineCount > MAX_SOURCE_LINE_SHARE) return null;
return buildPaddingFinding(totals.changedLineCount, totals.paddingLineCount);
}

function summarizePaddingLines(changedFiles: SlopChangedFile[]): {
changedLineCount: number;
paddingLineCount: number;
substantiveLineCount: number;
} {
let changedLineCount = 0;
let paddingLineCount = 0;
let substantiveLineCount = 0;
for (const file of changedFiles) {
const lines = nonNegative(file.additions) + nonNegative(file.deletions);
if (lines === 0) continue;
changedLineCount += lines;
const category = classifyChangedFile(file.path);
if (category === "minified" || category === "generated" || category === "vendored") {
paddingLineCount += lines;
} else if (category === "source" || category === "test") {
substantiveLineCount += lines;
}
}
return { changedLineCount, paddingLineCount, substantiveLineCount };
}

function buildPaddingFinding(changedLineCount: number, paddingLineCount: number): SignalFinding {
// Only integer counts are interpolated, so the text is public-safe by construction.
const detail = `${paddingLineCount} of ${changedLineCount} changed line(s) are in generated, vendored, or minified files with little substantive source.`;
return {
code: "non_substantive_padding",
title: "Diff is mostly generated, vendored, or minified output",
severity: "warning",
detail,
action: "Exclude generated, vendored, and minified output and keep the diff focused on substantive changes.",
publicText: detail,
};
}

// Fires only when a real code change ships with an empty / whitespace-only description — a high-precision
// weak-effort signal. A non-empty description (even a terse one) never trips it, to avoid false positives.
export function buildEmptyDescriptionFinding(input: SlopAssessmentInput): SignalFinding | null {
Expand Down
Loading
Loading