From 507a3cbcce2f2960c2686d2b994202be071e5c81 Mon Sep 17 00:00:00 2001 From: kiannidev <156195510+kiannidev@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:38:45 +0200 Subject: [PATCH] feat(signals): add missing test evidence slop signal Scaffold buildSlopAssessment with band thresholds and raise a deterministic missing-test-evidence finding when code-only diffs lack test paths or supplied test evidence. Co-authored-by: Cursor --- src/signals/local-branch.ts | 4 +- src/signals/slop.ts | 99 +++++++++++++++++++++++++++++++++++++ test/unit/slop.test.ts | 79 +++++++++++++++++++++++++++++ 3 files changed, 180 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..8e79267625 --- /dev/null +++ b/src/signals/slop.ts @@ -0,0 +1,99 @@ +import type { SignalFinding } from "./engine"; +import { isCodeFile, isTestFile } from "./local-branch"; +import { hasLocalTestEvidence, isTestPath } from "./test-evidence"; +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; + tests?: string[] | undefined; + testFiles?: string[] | undefined; +}; + +export type SlopAssessment = { + slopRisk: number; + band: SlopBand; + findings: SignalFinding[]; +}; + +export const SLOP_WEIGHTS = { + missingTestEvidence: 30, +} 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:", + "- missing test evidence", +].join("\n"); + +export function buildSlopAssessment(input: SlopAssessmentInput): SlopAssessment { + const findings: SignalFinding[] = []; + const missingTestEvidenceFinding = buildMissingTestEvidenceFinding(input); + if (missingTestEvidenceFinding) findings.push(missingTestEvidenceFinding); + + const slopRisk = clamp(missingTestEvidenceFinding ? SLOP_WEIGHTS.missingTestEvidence : 0, 0, 100); + + return { + slopRisk, + band: slopBandFor(slopRisk), + findings, + }; +} + +export function buildMissingTestEvidenceFinding(input: SlopAssessmentInput): SignalFinding | null { + const changedFiles = input.changedFiles ?? []; + const changedPaths = changedFiles.map((file) => file.path).filter(Boolean); + const codePaths = changedPaths.filter(isCodeFile); + if (codePaths.length === 0) return null; + + const hasChangedTestPaths = + changedPaths.some((path) => isTestFile(path) || isTestPath(path)) || + hasLocalTestEvidence({ tests: input.tests, testFiles: input.testFiles }); + if (hasChangedTestPaths) return null; + + const detail = ensurePublicSafeText( + `Changed paths include ${codePaths.length} code file(s) without accompanying test evidence.`, + "Code changes were detected without accompanying test evidence.", + ); + const action = ensurePublicSafeText( + "Add focused regression tests or explain why existing coverage is sufficient.", + "Add focused tests or explain why existing coverage is sufficient.", + ); + + return { + code: "missing_test_evidence", + title: "Code changes lack test evidence", + severity: "warning", + detail, + action, + publicText: detail, + }; +} + +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..294394577a --- /dev/null +++ b/test/unit/slop.test.ts @@ -0,0 +1,79 @@ +import { describe, expect, it } from "vitest"; +import { + buildMissingTestEvidenceFinding, + buildSlopAssessment, + 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("clean"); + expect(SLOP_RUBRIC_MARKDOWN).toContain("missing test evidence"); + + const clean = buildSlopAssessment({}); + expect(clean).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + expect(buildSlopAssessment({})).toEqual(clean); + }); + + it("raises missing-test-evidence slop for code-only diffs without tests", () => { + const result = buildSlopAssessment({ + changedFiles: [{ path: "src/registry/sync.ts", additions: 24, deletions: 2 }], + }); + + expect(result.slopRisk).toBe(SLOP_WEIGHTS.missingTestEvidence); + expect(result.band).toBe("elevated"); + expect(result.findings).toEqual([ + expect.objectContaining({ + code: "missing_test_evidence", + severity: "warning", + }), + ]); + expect(JSON.stringify(result)).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); + + it("does not raise missing-test-evidence when changed test files are present", () => { + expect( + buildSlopAssessment({ + changedFiles: [ + { path: "src/registry/sync.ts", additions: 24, deletions: 2 }, + { path: "test/unit/registry-sync.test.ts", additions: 18, deletions: 0 }, + ], + }), + ).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + }); + + it("does not raise missing-test-evidence when external test evidence is supplied", () => { + expect( + buildSlopAssessment({ + changedFiles: [{ path: "src/registry/sync.ts", additions: 12, deletions: 0 }], + testFiles: ["internal/cache_test.go"], + }), + ).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + }); + + it("ignores docs-only diffs without code files", () => { + expect( + buildSlopAssessment({ + changedFiles: [{ path: "README.md", additions: 40, deletions: 0 }], + }), + ).toEqual({ slopRisk: 0, band: "clean", findings: [] }); + }); +}); + +describe("buildMissingTestEvidenceFinding", () => { + it("keeps public reason strings sanitized", () => { + const finding = buildMissingTestEvidenceFinding({ + changedFiles: [{ path: "src/api/routes.ts", additions: 3, deletions: 0 }], + }); + + expect(finding).toMatchObject({ + code: "missing_test_evidence", + publicText: expect.any(String), + }); + expect(JSON.stringify(finding)).not.toMatch(FORBIDDEN_PUBLIC_TERMS); + }); +});