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
4 changes: 2 additions & 2 deletions src/signals/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) ||
Expand All @@ -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);
}

Expand Down
99 changes: 99 additions & 0 deletions src/signals/slop.ts
Original file line number Diff line number Diff line change
@@ -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));
}
79 changes: 79 additions & 0 deletions test/unit/slop.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});