diff --git a/src/signals/engine.ts b/src/signals/engine.ts index eddf1a29f7..8d06036123 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1048,6 +1048,25 @@ export function buildConfigQuality( action: "Verify those labels exist and are actually used by maintainers or trusted automation.", }); } + // A label multiplier must be a positive, finite number — a penalty multiplier is below 1 but still + // positive, so it is valid; 0, negative, NaN, or Infinity are config errors that would silently + // misweight scoring. Distinct from notObservedConfiguredLabels (which checks whether a label is *used*, + // not whether its multiplier is *valid*). + // Surface each bad multiplier as `label=value` so a maintainer sees the offending value inline. + const invalidLabelMultipliers = Object.entries(repo?.registryConfig?.labelMultipliers ?? {}) + .filter(([, multiplier]) => !(typeof multiplier === "number" && Number.isFinite(multiplier) && multiplier > 0)) + .map(([label, multiplier]) => `${label}=${String(multiplier)}`) + .sort(); + if (invalidLabelMultipliers.length > 0) { + score -= Math.min(30, invalidLabelMultipliers.length * 10); + findings.push({ + code: "invalid_label_multipliers", + severity: "warning", + title: "Configured label multipliers are out of range", + detail: `Label multipliers must be positive, finite numbers; these are not: ${invalidLabelMultipliers.join(", ")}.`, + action: "Set each flagged label multiplier to a positive, finite number (a penalty multiplier below 1 is allowed) in the registry config.", + }); + } const finalScore = clamp(score, 0, 100); return { diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index a16ed1e508..fc0e6b4bb0 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -597,6 +597,39 @@ describe("world-class backend signals", () => { expect(noMultiplierQuality.findings.map((finding) => finding.code)).toContain("trusted_labels_without_multipliers"); }); + it("flags non-positive, non-finite, or non-numeric label multipliers as a config error", () => { + const badRepo = { + ...repo, + registryConfig: { + ...repo.registryConfig!, + labelMultipliers: { bug: -1, stale: 0, broken: Number.NaN, weird: "x" as unknown as number, good: 1.2, penalty: 0.5 }, + }, + }; + // Observe every configured label so the unrelated "not observed" penalty does not also fire. + const observed = [{ ...issues[0]!, labels: ["bug", "stale", "broken", "weird", "good", "penalty"] }]; + const quality = buildConfigQuality(badRepo, observed, [], repo.fullName); + const invalid = quality.findings.find((finding) => finding.code === "invalid_label_multipliers"); + expect(invalid).toBeDefined(); + // Each bad multiplier is named with its value (sorted); valid ones are not listed. + expect(invalid?.detail).toMatch(/broken=NaN, bug=-1, stale=0, weird=x/); + expect(invalid?.detail).not.toMatch(/good|penalty/); + // Four invalid entries deduct the capped 30 points; valid ones do not contribute. + const baseline = buildConfigQuality({ ...badRepo, registryConfig: { ...badRepo.registryConfig!, labelMultipliers: { good: 1.2, penalty: 0.5 } } }, observed, [], repo.fullName).score; + expect(quality.score).toBe(baseline - 30); + }); + + it("does not flag valid positive label multipliers, including penalty (<1) multipliers", () => { + const okRepo = { ...repo, registryConfig: { ...repo.registryConfig!, labelMultipliers: { feature: 1.25, refactor: 0.5 } } }; + const quality = buildConfigQuality(okRepo, [{ ...issues[0]!, labels: ["feature", "refactor"] }], [], repo.fullName); + expect(quality.findings.map((finding) => finding.code)).not.toContain("invalid_label_multipliers"); + }); + + it("treats a repo with no labelMultipliers key as having no invalid multipliers", () => { + const noLabelsRepo = { ...repo, registryConfig: { ...repo.registryConfig!, labelMultipliers: undefined as unknown as Record } }; + const quality = buildConfigQuality(noLabelsRepo, [], [], repo.fullName); + expect(quality.findings.map((finding) => finding.code)).not.toContain("invalid_label_multipliers"); + }); + it("keeps contributor detection and comment modes conservative", () => { const currentPr = pullRequests[0]!; const settings: RepositorySettings = {