From c1b7206a4b534d09c014682a9a5d9b65e869ca49 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:40:23 +0200 Subject: [PATCH] feat(signals): add trivial whitespace churn slop signal Scaffold buildSlopAssessment and raise a deterministic churn finding when high-line-count diffs touch minimal substantive source code using the same line-split approach as local score input. Co-authored-by: Cursor --- src/signals/local-branch.ts | 4 +- src/signals/slop.ts | 125 ++++++++++++++++++++++++++++++++++++ test/unit/slop.test.ts | 87 +++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 2 deletions(-) create mode 100644 src/signals/slop.ts create mode 100644 test/unit/slop.test.ts diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index 694549061c..eb12febc68 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -1210,7 +1210,7 @@ function safeRepoPath(path: string): string { return /^(\/Users\/|\/home\/|\/tmp\/|[A-Z]:\/Users\/)/i.test(String(path).replace(/\\/g, "/")) ? "[local path hidden]" : String(path || "(unknown path)").replace(/\\/g, "/"); } -function isTestFile(file: string): boolean { +export function isTestFile(file: string): boolean { return ( /(^|\/)(test|tests|spec|__tests__)\//i.test(file) || /(^|\/)src\/test\//i.test(file) || @@ -1220,7 +1220,7 @@ function isTestFile(file: string): boolean { ); } -function isCodeFile(file: string): boolean { +export function isCodeFile(file: string): boolean { return /\.(ts|tsx|js|jsx|py|rb|rs|kt|scala|java|go|sql)$/i.test(file) && !isTestFile(file); } diff --git a/src/signals/slop.ts b/src/signals/slop.ts new file mode 100644 index 0000000000..ce4f6e8cb5 --- /dev/null +++ b/src/signals/slop.ts @@ -0,0 +1,125 @@ +import type { SignalFinding } from "./engine"; +import { isCodeFile, isTestFile } from "./local-branch"; +import { isFocusManifestPublicSafe } from "./focus-manifest"; + +export type SlopBand = "clean" | "low" | "elevated" | "high"; + +export type SlopChangedFile = { + path: string; + additions?: number | undefined; + deletions?: number | undefined; +}; + +export type SlopAssessmentInput = { + changedFiles?: SlopChangedFile[] | undefined; +}; + +export type SlopAssessment = { + slopRisk: number; + band: SlopBand; + findings: SignalFinding[]; +}; + +export const SLOP_WEIGHTS = { + trivialWhitespaceChurn: 25, +} as const; + +export const SLOP_RUBRIC_MARKDOWN = [ + "# Gittensory slop assessment rubric", + "", + "- `clean`: 0", + "- `low`: 1-24", + "- `elevated`: 25-59", + "- `high`: 60-100", + "", + "Current deterministic signals:", + "- trivial / whitespace-only churn", +].join("\n"); + +const MIN_CHURN_LINES = 40; +const MAX_SOURCE_LINE_SHARE = 0.15; + +export function buildSlopAssessment(input: SlopAssessmentInput): SlopAssessment { + const findings: SignalFinding[] = []; + const trivialChurnFinding = buildTrivialWhitespaceChurnFinding(input); + if (trivialChurnFinding) findings.push(trivialChurnFinding); + + const slopRisk = clamp(trivialChurnFinding ? SLOP_WEIGHTS.trivialWhitespaceChurn : 0, 0, 100); + + return { + slopRisk, + band: slopBandFor(slopRisk), + findings, + }; +} + +export function buildTrivialWhitespaceChurnFinding(input: SlopAssessmentInput): SignalFinding | null { + const changedFiles = input.changedFiles ?? []; + const lineTotals = summarizeChangedLines(changedFiles); + if (lineTotals.changedLineCount < MIN_CHURN_LINES) return null; + if (lineTotals.sourceLineCount === 0) { + return buildTrivialChurnFinding(lineTotals.changedLineCount, lineTotals.nonCodeLineCount); + } + const sourceShare = lineTotals.sourceLineCount / lineTotals.changedLineCount; + if (sourceShare > MAX_SOURCE_LINE_SHARE) return null; + return buildTrivialChurnFinding(lineTotals.changedLineCount, lineTotals.nonCodeLineCount); +} + +function summarizeChangedLines(changedFiles: SlopChangedFile[]): { + changedLineCount: number; + sourceLineCount: number; + testLineCount: number; + nonCodeLineCount: number; +} { + const changedLineCount = changedFiles.reduce( + (sum, file) => sum + nonNegative(file.additions) + nonNegative(file.deletions), + 0, + ); + const sourceLineCount = changedFiles + .filter((file) => isCodeFile(file.path)) + .reduce((sum, file) => sum + nonNegative(file.additions) + nonNegative(file.deletions), 0); + const testLineCount = changedFiles + .filter((file) => isTestFile(file.path)) + .reduce((sum, file) => sum + nonNegative(file.additions) + nonNegative(file.deletions), 0); + const nonCodeLineCount = Math.max(0, changedLineCount - sourceLineCount - testLineCount); + return { changedLineCount, sourceLineCount, testLineCount, nonCodeLineCount }; +} + +function buildTrivialChurnFinding(changedLineCount: number, nonCodeLineCount: number): SignalFinding { + const detail = ensurePublicSafeText( + `The diff churns ${changedLineCount} line(s) with only ${Math.max(0, changedLineCount - nonCodeLineCount)} substantive source line(s) touched.`, + "The diff shows high churn with minimal substantive source changes.", + ); + const action = ensurePublicSafeText( + "Reduce whitespace-only or formatting-only churn and keep the diff focused on substantive changes.", + "Reduce formatting-only churn and keep the diff focused on substantive changes.", + ); + + return { + code: "trivial_whitespace_churn", + title: "Diff looks like trivial or whitespace-only churn", + severity: "warning", + detail, + action, + publicText: detail, + }; +} + +function nonNegative(value: number | undefined): number { + return Number.isFinite(value) && (value ?? 0) > 0 ? Math.trunc(value as number) : 0; +} + +function ensurePublicSafeText(text: string, fallback: string): string { + return isFocusManifestPublicSafe(text) ? text : fallback; +} + +function slopBandFor(slopRisk: number): SlopBand { + if (slopRisk <= 0) return "clean"; + if (slopRisk < 25) return "low"; + if (slopRisk < 60) return "elevated"; + return "high"; +} + +function clamp(value: number, min: number, max: number): number { + return Math.min(max, Math.max(min, value)); +} diff --git a/test/unit/slop.test.ts b/test/unit/slop.test.ts new file mode 100644 index 0000000000..18d290b3d1 --- /dev/null +++ b/test/unit/slop.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; +import { + buildSlopAssessment, + buildTrivialWhitespaceChurnFinding, + SLOP_RUBRIC_MARKDOWN, + SLOP_WEIGHTS, +} from "../../src/signals/slop"; + +const FORBIDDEN_PUBLIC_TERMS = + /wallet|hotkey|coldkey|mnemonic|reward|payout|raw trust|trust score|scoreability|private reviewability|\/Users|\/home|\/tmp/i; + +describe("buildSlopAssessment", () => { + it("exports rubric bands and a deterministic assessment shell", () => { + expect(SLOP_RUBRIC_MARKDOWN).toContain("trivial / whitespace-only churn"); + + const clean = buildSlopAssessment({}); + expect(clean).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + expect(buildSlopAssessment({})).toEqual(clean); + }); + + it("raises trivial-churn slop for high-churn diffs with minimal source lines", () => { + const result = buildSlopAssessment({ + changedFiles: [ + { path: "README.md", additions: 30, deletions: 20 }, + { path: "docs/guide.md", additions: 25, deletions: 15 }, + { path: "src/widget.ts", additions: 2, deletions: 1 }, + ], + }); + + expect(result.slopRisk).toBe(SLOP_WEIGHTS.trivialWhitespaceChurn); + expect(result.band).toBe("elevated"); + expect(result.findings).toEqual([ + expect.objectContaining({ + code: "trivial_whitespace_churn", + severity: "warning", + }), + ]); + expect(JSON.stringify(result)).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); + + it("does not raise trivial-churn when substantive source edits dominate", () => { + expect( + buildSlopAssessment({ + changedFiles: [ + { path: "src/registry/sync.ts", additions: 80, deletions: 20 }, + { path: "test/unit/registry-sync.test.ts", additions: 40, deletions: 5 }, + ], + }), + ).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + }); + + it("does not raise trivial-churn for small diffs below the churn threshold", () => { + expect( + buildSlopAssessment({ + changedFiles: [{ path: "README.md", additions: 10, deletions: 8 }], + }), + ).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + }); + + it("raises trivial-churn for non-code-only high-churn diffs", () => { + expect( + buildSlopAssessment({ + changedFiles: [ + { path: "README.md", additions: 25, deletions: 20 }, + { path: "docs/guide.md", additions: 20, deletions: 15 }, + ], + }).findings.map((finding) => finding.code), + ).toEqual(["trivial_whitespace_churn"]); + }); +}); + +describe("buildTrivialWhitespaceChurnFinding", () => { + it("keeps public reason strings sanitized", () => { + const finding = buildTrivialWhitespaceChurnFinding({ + changedFiles: [ + { path: "README.md", additions: 30, deletions: 20 }, + { path: "docs/guide.md", additions: 25, deletions: 15 }, + ], + }); + + expect(finding).toMatchObject({ + code: "trivial_whitespace_churn", + publicText: expect.any(String), + }); + expect(JSON.stringify(finding)).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); +});