diff --git a/src/scoring/preview.ts b/src/scoring/preview.ts index 77f2ec15c3..03eafc9341 100644 --- a/src/scoring/preview.ts +++ b/src/scoring/preview.ts @@ -895,6 +895,16 @@ function selectLabelMultiplier(labels: string[], multipliers: Record 0 ? Math.max(...matched) : fallback || 1; } +/** True when `label` matches the configured multiplier `pattern` under the SAME case-insensitive fnmatch glob + * semantics scoring uses to resolve label multipliers (see {@link labelPatternToRegExp}). Exported so the + * signals surfaces that audit configured label keys (config-quality, label-audit) match them as the GLOBS they + * are: a `type:*` key must count `type:bug-fix` as observed/configured, not silently report it missing because + * the literal pattern never appears verbatim on a real issue/PR. A literal key (no glob metacharacter) still + * matches only its exact label, so existing configs behave identically. */ +export function labelMatchesPattern(label: string, pattern: string): boolean { + return labelPatternToRegExp(pattern.toLowerCase()).test(label.toLowerCase()); +} + // Upstream resolves label multipliers by matching each configured key as a Python `fnmatch` GLOB, not a // literal string: `fnmatch(label.lower(), pattern.lower())` in // gittensor/validator/oss_contributions/label_resolution.py, so a repo can configure `type:*`, `kind/*`, or diff --git a/src/signals/engine.ts b/src/signals/engine.ts index d0f52272bc..f948be62d8 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -25,7 +25,7 @@ import type { FocusManifestReviewConfig, ReviewFieldKey } from "./focus-manifest import type { GittensorContributorSnapshot } from "../gittensor/api"; import { nowIso } from "../utils/json"; import { sanitizePublicComment } from "../queue-intelligence"; -import { projectLinkedIssueMultiplierForPlannedSolve, type LinkedIssueMultiplierStatus } from "../scoring/preview"; +import { labelMatchesPattern, projectLinkedIssueMultiplierForPlannedSolve, type LinkedIssueMultiplierStatus } from "../scoring/preview"; import { hasLocalTestEvidence } from "./test-evidence"; import { isDuplicateClusterWinner } from "./duplicate-winner"; import { PREFLIGHT_LIMITS } from "./preflight-limits"; @@ -1009,7 +1009,11 @@ export function buildConfigQuality( const lane = buildLaneAdvice(repo, fullName); const configuredLabels = Object.keys(repo?.registryConfig?.labelMultipliers ?? {}).sort(); const observedLabels = [...new Set([...issues, ...pullRequests].flatMap((record) => record.labels))].sort(); - const notObservedConfiguredLabels = configuredLabels.filter((label) => !observedLabels.includes(label)); + // Configured keys are fnmatch GLOBS (scoring resolves them via labelMatchesPattern), so a key is "observed" + // when it matches any cached label — not only when the literal pattern string appears verbatim. The old + // exact `.includes` reported every wildcard key (e.g. `type:*`) as not-observed even when `type:bug-fix` is in + // active use, spuriously docking the config-quality score for glob-configured repos. (#1769) + const notObservedConfiguredLabels = configuredLabels.filter((pattern) => !observedLabels.some((label) => labelMatchesPattern(label, pattern))); const findings: SignalFinding[] = []; let score = 100; @@ -1097,10 +1101,14 @@ export function buildLabelAudit(repo: RepositoryRecord | null, repoLabels: RepoL .map(([name, count]) => ({ name, count, - configured: configuredLabels.includes(name), + // Match each observed label against the configured GLOB keys (fnmatch), the same way scoring resolves a + // label's multiplier — so a label covered by a `type:*` key is reported as configured, not unconfigured. (#1769) + configured: configuredLabels.some((pattern) => labelMatchesPattern(name, pattern)), existsOnGitHub: liveLabels.includes(name), })); - const missingConfiguredLabels = configuredLabels.filter((label) => !liveLabels.includes(label)); + // A configured key is "missing" only when NO live GitHub label matches it as a glob; a `type:*` key backed by a + // real `type:bug` label is present, not missing (the old exact `.includes` flagged every wildcard key missing). (#1769) + const missingConfiguredLabels = configuredLabels.filter((pattern) => !liveLabels.some((live) => labelMatchesPattern(live, pattern))); // Require a real separator (`:`/`/`/`-`) OR end-of-string after the keyword so this flags prefix-style labels // (`status:ready`, `reward/x`) and bare keywords (`bot`) — but NOT mid-word matches like `bottleneck` (`bot`), // `scoreboard` (`score`), or `riskier` (`risk`). The old optional+unanchored `[:/-]?` over-matched those. diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 988174be93..094f833847 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -1675,6 +1675,36 @@ describe("signal coverage edge cases", () => { expect(audit.findings.map((finding) => finding.code)).toContain("suspicious_configured_labels"); }); + it("matches configured glob label keys against observed and live labels, not literal strings (#1769)", () => { + // `type:*` is a wildcard multiplier key; `area:*` matches nothing in use; `bug` is a plain literal key. + const globRepo = repo("owner/glob-labels", { labelMultipliers: { "type:*": 1.3, "area:*": 1.2, bug: 1.1 }, trustedLabelPipeline: true }); + + // buildConfigQuality: a glob key with a matching cached label is "observed" (not flagged); one with no match is. + const quality = buildConfigQuality( + globRepo, + [issue(globRepo.fullName, 1, "Crash", { labels: ["type:bug-fix"] })], + [pr(globRepo.fullName, 2, "Fix", { labels: ["bug"] })], + globRepo.fullName, + ); + expect(quality.notObservedConfiguredLabels).toEqual(["area:*"]); + expect(quality.notObservedConfiguredLabels).not.toContain("type:*"); + expect(quality.findings.map((finding) => finding.code)).toContain("configured_labels_not_observed"); + + // buildLabelAudit: live `type:bug` satisfies the `type:*` glob; `area:*` has no live label → it alone is missing. + const liveLabels: RepoLabelRecord[] = [ + { repoFullName: globRepo.fullName, name: "type:bug", isConfigured: true, observedCount: 2, payload: {}, lastSeenAt: "2026-05-25T00:00:00.000Z" }, + { repoFullName: globRepo.fullName, name: "bug", isConfigured: true, observedCount: 1, payload: {}, lastSeenAt: "2026-05-25T00:00:00.000Z" }, + { repoFullName: globRepo.fullName, name: "wontfix", isConfigured: false, observedCount: 1, payload: {}, lastSeenAt: "2026-05-25T00:00:00.000Z" }, + ]; + const audit = buildLabelAudit(globRepo, liveLabels, [], [], globRepo.fullName); + expect(audit.missingConfiguredLabels).toEqual(["area:*"]); + expect(audit.missingConfiguredLabels).not.toContain("type:*"); + // `type:bug` is covered by the `type:*` glob (configured: true); the unrelated `wontfix` label is not (false). + const byName = new Map(audit.observedLabels.map((label) => [label.name, label.configured])); + expect(byName.get("type:bug")).toBe(true); + expect(byName.get("wontfix")).toBe(false); + }); + it("awards the personalFit language bonus only on a real repo-language match", () => { const targetRepo = repo("owner/lang-fit"); const profile = buildContributorProfile("dev", { login: "dev", topLanguages: ["TypeScript"], source: "github" }, [], []);