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
18 changes: 10 additions & 8 deletions src/signals/slop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,8 @@ export const ISSUE_SLOP_RUBRIC_MARKDOWN = [
"# Gittensory issue slop triage rubric",
"",
"- `clean`: 0",
"- `low`: 1-24",
"- `elevated`: 25-59",
"- `low`: 1-30",
"- `elevated`: 31-59",
"- `high`: 60-100",
"",
"Advisory-only (issues never block). Current deterministic signals:",
Expand Down Expand Up @@ -500,14 +500,16 @@ function ensurePublicSafeText(text: string, fallback: string): string {
return isFocusManifestPublicSafe(text) ? text : fallback;
}

// Documented thresholds (#565): the deterministic slopRisk (0-100) maps to fixed bands — clean = 0,
// low = 1-24, elevated = 25-59, high = 60-100. Strong signals (trivial churn, non-substantive padding)
// weigh 30 (any two reach `high`); weak/corroborating/traceability signals — including missing-test-evidence
// — weigh 15. Identical metadata always yields an identical band (see golden fixtures).
// Documented thresholds (#565, recalibrated by #3939): the deterministic slopRisk (0-100) maps to fixed
// bands — clean = 0, low = 1-30, elevated = 31-59, high = 60-100. Strong signals (trivial churn,
// non-substantive padding) weigh 30 (any two reach `high`); weak/corroborating/traceability signals —
// including missing-test-evidence — weigh 15, so a single strong or a pair of weak signals alone now lands
// in `low`, not `elevated` — `elevated` needs genuine multi-signal evidence (strong+weak ≥ 45, or 3×weak =
// 45). Identical metadata always yields an identical band (see golden fixtures).
function slopBandFor(slopRisk: number): SlopBand {
if (slopRisk <= 0) return "clean";
if (slopRisk < 31) return "low"; // raised from 25: a single strong signal (30pts) is low, not elevated
if (slopRisk < 60) return "elevated"; // elevated now requires multi-signal evidence (strong+weak ≥ 45, or 3×weak = 45)
if (slopRisk < 31) return "low";
if (slopRisk < 60) return "elevated";
return "high";
}

Expand Down
40 changes: 26 additions & 14 deletions test/unit/slop.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,16 +113,18 @@ describe("buildSlopAssessment", () => {
expect(buildDuplicateClusterFinding({ inDuplicateCluster: false })).toBeNull();
});

it("stacks the duplicate-cluster weight with another signal into the expected band (#563)", () => {
it("stacks the duplicate-cluster weight with two other signals into the expected band (#563, #3939 recalibration)", () => {
const result = buildSlopAssessment({
// code file with no test evidence → missing_test_evidence (30); non-empty description suppresses empty_description.
// code file with no test evidence → missing_test_evidence (15); non-empty description suppresses empty_description.
changedFiles: [{ path: "src/parser.ts", additions: 10, deletions: 1 }],
description: "Refactor the parser.",
inDuplicateCluster: true, // → duplicate_cluster_membership (15)
hasLinkedIssue: false, // → no_linked_issue_without_rationale (15) -- a third weak signal, needed post-#3939:
// two weak signals alone (30) now land in `low` (1-30), not `elevated` (31-59); three reaches 45.
});
expect(result.slopRisk).toBe(SLOP_WEIGHTS.missingTestEvidence + SLOP_WEIGHTS.duplicateClusterMembership);
expect(result.slopRisk).toBe(SLOP_WEIGHTS.missingTestEvidence + SLOP_WEIGHTS.duplicateClusterMembership + SLOP_WEIGHTS.noLinkedIssueWithoutRationale);
expect(result.band).toBe("elevated");
expect(result.findings.map((finding) => finding.code).sort()).toEqual(["duplicate_cluster_membership", "missing_test_evidence"]);
expect(result.findings.map((finding) => finding.code).sort()).toEqual(["duplicate_cluster_membership", "missing_test_evidence", "no_linked_issue_without_rationale"]);
expect(JSON.stringify(result)).not.toMatch(FORBIDDEN_PUBLIC_TERMS);
});

Expand All @@ -132,7 +134,7 @@ describe("buildSlopAssessment", () => {
description: "Add retry-with-backoff to the registry sync client.",
});

// De-weighted to 15: missing-test alone is corroborating, not decisive, and lands in `low` (1-24).
// De-weighted to 15: missing-test alone is corroborating, not decisive, and lands in `low` (1-30).
expect(result.slopRisk).toBe(SLOP_WEIGHTS.missingTestEvidence);
expect(result.band).toBe("low");
expect(result.findings).toEqual([
Expand Down Expand Up @@ -196,7 +198,8 @@ describe("buildSlopAssessment", () => {
});

expect(result.slopRisk).toBe(SLOP_WEIGHTS.trivialWhitespaceChurn);
expect(result.band).toBe("elevated");
// A single strong signal (30) alone is `low` (1-30), not `elevated` (31-59) — post-#3939 recalibration.
expect(result.band).toBe("low");
expect(result.findings).toEqual([
expect.objectContaining({
code: "trivial_whitespace_churn",
Expand Down Expand Up @@ -307,9 +310,10 @@ describe("buildSlopAssessment", () => {
});

it("reaches the high band when multiple strong signals stack", () => {
// Code change, no tests, no description: missing-test-evidence (15) + empty-description (15) = 30 = elevated.
const elevated = buildSlopAssessment({ changedFiles: [{ path: "src/x.ts", additions: 10, deletions: 1 }], description: "" });
expect(elevated.band).toBe("elevated");
// Code change, no tests, no description: missing-test-evidence (15) + empty-description (15) = 30 = low
// (post-#3939 recalibration: two weak signals alone no longer reach `elevated`, which now needs ≥31).
const twoWeakSignals = buildSlopAssessment({ changedFiles: [{ path: "src/x.ts", additions: 10, deletions: 1 }], description: "" });
expect(twoWeakSignals.band).toBe("low");

// High-whitespace-churn code change + no tests + no description: 30 + 15 + 15 = 60 -> high (>=60).
const high = buildSlopAssessment({
Expand Down Expand Up @@ -625,7 +629,8 @@ describe("buildNonSubstantivePaddingFinding (#561 path-matcher signal)", () => {
});
expect(result.findings.map((finding) => finding.code)).toEqual(["non_substantive_padding"]);
expect(result.slopRisk).toBe(SLOP_WEIGHTS.nonSubstantivePadding);
expect(result.band).toBe("elevated");
// A single strong signal (30) alone is `low` (1-30), not `elevated` (31-59) — post-#3939 recalibration.
expect(result.band).toBe("low");
expect(JSON.stringify(result)).not.toMatch(FORBIDDEN);
});
});
Expand All @@ -643,11 +648,18 @@ describe("slop golden fixtures & determinism (#565)", () => {
codes: ["missing_test_evidence"],
},
{
name: "elevated — untested code change inside a duplicate cluster",
input: { changedFiles: [{ path: "src/svc.ts", additions: 12, deletions: 3 }], description: "Add retry logic to the sync client.", inDuplicateCluster: true },
slopRisk: 30,
// Three weak signals (45), not two (30): post-#3939 recalibration, two weak signals alone land in `low`
// (1-30) -- `elevated` (31-59) now needs genuine multi-signal evidence.
name: "elevated — untested, unlinked code change inside a duplicate cluster",
input: {
changedFiles: [{ path: "src/svc.ts", additions: 12, deletions: 3 }],
description: "Add retry logic to the sync client.",
inDuplicateCluster: true,
hasLinkedIssue: false,
},
slopRisk: 45,
band: "elevated",
codes: ["duplicate_cluster_membership", "missing_test_evidence"],
codes: ["duplicate_cluster_membership", "missing_test_evidence", "no_linked_issue_without_rationale"],
},
{
name: "high — whitespace churn, untested code, and empty description",
Expand Down