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
8 changes: 6 additions & 2 deletions .gittensory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ review:
# fallback already has zero equivalent verification) so they propagate even when the PR author isn't a
# formal GitHub assignee of the issue — our issues are almost always maintainer-authored for open pickup and
# rarely formally assigned. priority intentionally omits the flag: it is the scarce, maintainer-hand-picked
# reward label, and must still require the PR author to be the issue's actual author/assignee.
# reward label, and must still require the PR author to be the issue's actual author/assignee. priority is
# also `removeOtherTypeLabels: false` (additive) -- unlike bug/feature, which are mutually-exclusive TYPE
# categories, priority is a separate reward dimension that coexists WITH whichever type already applies (an
# issue is routinely both gittensor:feature AND gittensor:priority at once); resolvePrTypeLabel composes every
# additive match alongside the one exclusive winner, rather than the two categories competing for a single slot.
#
# Review-evasion protection: closing or converting-to-draft your OWN PR while gittensory has an active
# review pass running, a prior recorded gate failure, or a repeated ready<->draft cycle on this PR, is
Expand All @@ -92,7 +96,7 @@ settings:
trustMaintainerAuthoredIssue: true
- issueLabel: "gittensor:priority"
prLabel: "gittensor:priority"
removeOtherTypeLabels: true
removeOtherTypeLabels: false
reviewEvasionProtection: close

# Repo-doc generation roadmap (#2993/#3002) — opt-in only, off by default. Uncomment to let Gittensory open a
Expand Down
22 changes: 12 additions & 10 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -603,22 +603,24 @@ settings:
# label like `gittensor:priority` -- it is NEVER inferred from a PR's title, changed files, AI
# output, or existing PR labels, only ever copied from a linked/closing issue ("Fixes #123") that
# ALREADY carries the configured issue label. Generic beyond the priority use case: any issue label
# can map to any PR label. `removeOtherTypeLabels: true` REPLACES the type label entirely (bug/
# feature are removed), matching how `gittensor:priority` behaves today; `false` applies the mapped
# label ADDITIVELY alongside the normal title-based bug/feature label, leaving it untouched -- useful
# for a mapping unrelated to the bug/feature/priority triad (e.g. a `customer:vip` issue label
# copied to a `triage:vip` PR label). Disabled by default (no mappings) -- a self-hoster opts in per
# repo. If your labels carry reward or moderation weight, configure this in PRIVATE per-repo/global
# config (see `config/examples/README.md`) rather than the public `.gittensory.yml`, so contributors
# cannot see the exact mapping rules. A per-repo `mappings` override in the private-config layer
# REPLACES the global default list wholesale -- it does not merge with it.
# can map to any PR label. `removeOtherTypeLabels: true` marks the mapping EXCLUSIVE -- it REPLACES
# the type label entirely (bug/feature are removed), for genuinely mutually-exclusive categories; only
# the FIRST-configured exclusive match wins when more than one applies. `false` (e.g. `gittensor:priority`,
# which is a reward tag that coexists WITH whichever type already applies, not a type of its own) applies
# the mapped label ADDITIVELY alongside whichever exclusive match (or the normal title-based bug/feature
# label) already won -- every additive match composes together rather than competing for a single slot.
# Disabled by default (no mappings) -- a self-hoster opts in per repo. If your labels carry reward or
# moderation weight, configure this in PRIVATE per-repo/global config (see `config/examples/README.md`)
# rather than the public `.gittensory.yml`, so contributors cannot see the exact mapping rules. A per-repo
# `mappings` override in the private-config layer REPLACES the global default list wholesale -- it does
# not merge with it.
# linkedIssueLabelPropagation:
# enabled: true
# mode: exclusive_type_label
# mappings:
# - issueLabel: gittensor:priority
# prLabel: gittensor:priority
# removeOtherTypeLabels: true
# removeOtherTypeLabels: false

# Create the label if it does not yet exist. Bool. Default: true.
createMissingLabel: true
Expand Down
22 changes: 12 additions & 10 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -616,22 +616,24 @@ settings:
# label like `gittensor:priority` -- it is NEVER inferred from a PR's title, changed files, AI
# output, or existing PR labels, only ever copied from a linked/closing issue ("Fixes #123") that
# ALREADY carries the configured issue label. Generic beyond the priority use case: any issue label
# can map to any PR label. `removeOtherTypeLabels: true` REPLACES the type label entirely (bug/
# feature are removed), matching how `gittensor:priority` behaves today; `false` applies the mapped
# label ADDITIVELY alongside the normal title-based bug/feature label, leaving it untouched -- useful
# for a mapping unrelated to the bug/feature/priority triad (e.g. a `customer:vip` issue label
# copied to a `triage:vip` PR label). Disabled by default (no mappings) -- a self-hoster opts in per
# repo. If your labels carry reward or moderation weight, configure this in PRIVATE per-repo/global
# config (see `config/examples/README.md`) rather than the public `.gittensory.yml`, so contributors
# cannot see the exact mapping rules. A per-repo `mappings` override in the private-config layer
# REPLACES the global default list wholesale -- it does not merge with it.
# can map to any PR label. `removeOtherTypeLabels: true` marks the mapping EXCLUSIVE -- it REPLACES
# the type label entirely (bug/feature are removed), for genuinely mutually-exclusive categories; only
# the FIRST-configured exclusive match wins when more than one applies. `false` (e.g. `gittensor:priority`,
# which is a reward tag that coexists WITH whichever type already applies, not a type of its own) applies
# the mapped label ADDITIVELY alongside whichever exclusive match (or the normal title-based bug/feature
# label) already won -- every additive match composes together rather than competing for a single slot.
# Disabled by default (no mappings) -- a self-hoster opts in per repo. If your labels carry reward or
# moderation weight, configure this in PRIVATE per-repo/global config (see `config/examples/README.md`)
# rather than the public `.gittensory.yml`, so contributors cannot see the exact mapping rules. A per-repo
# `mappings` override in the private-config layer REPLACES the global default list wholesale -- it does
# not merge with it.
# linkedIssueLabelPropagation:
# enabled: true
# mode: exclusive_type_label
# mappings:
# - issueLabel: gittensor:priority
# prLabel: gittensor:priority
# removeOtherTypeLabels: true
# removeOtherTypeLabels: false

# Create the label if it does not yet exist. Bool. Default: true.
createMissingLabel: true
Expand Down
8 changes: 6 additions & 2 deletions src/config/gittensory-repo-focus-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ review:
# fallback already has zero equivalent verification) so they propagate even when the PR author isn't a
# formal GitHub assignee of the issue — our issues are almost always maintainer-authored for open pickup and
# rarely formally assigned. priority intentionally omits the flag: it is the scarce, maintainer-hand-picked
# reward label, and must still require the PR author to be the issue's actual author/assignee.
# reward label, and must still require the PR author to be the issue's actual author/assignee. priority is
# also \`removeOtherTypeLabels: false\` (additive) -- unlike bug/feature, which are mutually-exclusive TYPE
# categories, priority is a separate reward dimension that coexists WITH whichever type already applies (an
# issue is routinely both gittensor:feature AND gittensor:priority at once); resolvePrTypeLabel composes every
# additive match alongside the one exclusive winner, rather than the two categories competing for a single slot.
#
# Review-evasion protection: closing or converting-to-draft your OWN PR while gittensory has an active
# review pass running, a prior recorded gate failure, or a repeated ready<->draft cycle on this PR, is
Expand All @@ -96,7 +100,7 @@ settings:
trustMaintainerAuthoredIssue: true
- issueLabel: "gittensor:priority"
prLabel: "gittensor:priority"
removeOtherTypeLabels: true
removeOtherTypeLabels: false
reviewEvasionProtection: close

# Repo-doc generation roadmap (#2993/#3002) — opt-in only, off by default. Uncomment to let Gittensory open a
Expand Down
20 changes: 18 additions & 2 deletions src/settings/pr-type-label.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
// moves away from it. Public + neutral categorization (NOT the reputation signal). Review-time +
// independent of the gate / autonomy / dry-run (matches reviewbot, where auto-label runs at review
// start). Fail-safe.
import type { LinkedIssueLabelPropagationConfig, PrTypeLabelSet } from "../types";
import type { LinkedIssueLabelPropagationConfig, LinkedIssueLabelPropagationMapping, PrTypeLabelSet } from "../types";

export type { PrTypeLabelSet } from "../types";

Expand Down Expand Up @@ -156,9 +156,25 @@ export function resolvePrTypeLabel(input: {

if (input.propagation?.enabled) {
const wanted = new Set((input.linkedIssueLabels ?? []).map((label) => label.toLowerCase()));
// Collect EVERY mapping the linked issue's labels satisfy, not just the first. An exclusive mapping
// (removeOtherTypeLabels: true -- e.g. bug/feature, genuinely mutually-exclusive categories) still only
// ever lets the FIRST-configured match win, same precedence as before. But an additive mapping (e.g.
// priority -- a maintainer-hand-picked reward tag that coexists WITH whichever type already applies, not a
// type of its own) must compose with that winner instead of being skipped just because an earlier mapping
// in the array already matched and returned. Before this, an additive match was unreachable whenever the
// SAME linked issue also carried a label an earlier (exclusive) mapping matched -- the overwhelmingly common
// case for gittensor:priority, which is applied ALONGSIDE gittensor:bug/gittensor:feature on the issue, never
// instead of it (#priority-linked-issue-gate).
let exclusiveMatch: LinkedIssueLabelPropagationMapping | undefined;
const additiveMatches: LinkedIssueLabelPropagationMapping[] = [];
for (const mapping of input.propagation.mappings) {
if (!wanted.has(mapping.issueLabel.toLowerCase())) continue;
return mapping.removeOtherTypeLabels ? decide([mapping.prLabel], "propagation_exclusive") : decide([titleLabel, mapping.prLabel], "propagation_additive");
if (mapping.removeOtherTypeLabels) exclusiveMatch ??= mapping;
else additiveMatches.push(mapping);
}
if (exclusiveMatch || additiveMatches.length > 0) {
const applyLabels = [exclusiveMatch ? exclusiveMatch.prLabel : titleLabel, ...additiveMatches.map((mapping) => mapping.prLabel)];
return decide(applyLabels, exclusiveMatch ? "propagation_exclusive" : "propagation_additive");
}
}
return decide([titleLabel], "title");
Expand Down
60 changes: 60 additions & 0 deletions test/unit/pr-type-label.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,66 @@ describe("resolvePrTypeLabel (#priority-linked-issue-gate)", () => {
expect(result.source).toBe("propagation_exclusive");
});

describe("additive matches compose with the exclusive winner (#priority-linked-issue-gate composition fix)", () => {
const bugFeaturePriorityMappings = [
{ issueLabel: "gittensor:bug", prLabel: "gittensor:bug", removeOtherTypeLabels: true },
{ issueLabel: "gittensor:feature", prLabel: "gittensor:feature", removeOtherTypeLabels: true },
{ issueLabel: "gittensor:priority", prLabel: "gittensor:priority", removeOtherTypeLabels: false },
];

it("REGRESSION: a linked issue carrying BOTH gittensor:feature (exclusive) and gittensor:priority (additive) gets both labels, not just the first match", () => {
// Before this fix, the loop returned on the FIRST wanted mapping (feature, since it's checked before
// priority) and never even looked at priority's own mapping -- silently dropping priority whenever the
// SAME issue also carried a type label, which is the overwhelmingly common case in practice.
const result = resolvePrTypeLabel({
title: "fix: y",
linkedIssueLabels: ["gittensor:feature", "gittensor:priority"],
propagation: propagation({ mappings: bugFeaturePriorityMappings }),
});
expect(result).toEqual({ applyLabels: ["gittensor:feature", "gittensor:priority"], removeLabels: ["gittensor:bug"], source: "propagation_exclusive" });
});

it("REGRESSION: a linked issue carrying BOTH gittensor:bug (exclusive) and gittensor:priority (additive) gets both labels", () => {
const result = resolvePrTypeLabel({
title: "feat: add provider fallback", // title alone would say "feature" -- propagation must still win
linkedIssueLabels: ["gittensor:bug", "gittensor:priority"],
propagation: propagation({ mappings: bugFeaturePriorityMappings }),
});
expect(result).toEqual({ applyLabels: ["gittensor:bug", "gittensor:priority"], removeLabels: ["gittensor:feature"], source: "propagation_exclusive" });
});

it("priority alone (no bug/feature match): applies additively alongside the title-based label, source is propagation_additive", () => {
const result = resolvePrTypeLabel({
title: "fix: y",
linkedIssueLabels: ["gittensor:priority"],
propagation: propagation({ mappings: bugFeaturePriorityMappings }),
});
expect(result).toEqual({ applyLabels: ["gittensor:bug", "gittensor:priority"], removeLabels: ["gittensor:feature"], source: "propagation_additive" });
});

it("multiple additive matches all compose together alongside a single exclusive winner", () => {
const result = resolvePrTypeLabel({
title: "fix: y",
linkedIssueLabels: ["gittensor:bug", "gittensor:priority", "customer:vip"],
propagation: propagation({
mappings: [...bugFeaturePriorityMappings, { issueLabel: "customer:vip", prLabel: "triage:vip", removeOtherTypeLabels: false }],
}),
});
expect(result.applyLabels).toEqual(["gittensor:bug", "gittensor:priority", "triage:vip"]);
expect(result.source).toBe("propagation_exclusive");
});

it("two exclusive candidates + an additive one: only the FIRST-configured exclusive mapping wins, additive still composes", () => {
const result = resolvePrTypeLabel({
title: "fix: y",
linkedIssueLabels: ["gittensor:bug", "gittensor:feature", "gittensor:priority"],
propagation: propagation({ mappings: bugFeaturePriorityMappings }),
});
// bug is configured before feature, so bug wins the exclusive slot even though both matched.
expect(result).toEqual({ applyLabels: ["gittensor:bug", "gittensor:priority"], removeLabels: ["gittensor:feature"], source: "propagation_exclusive" });
});
});

it("respects a custom typeLabels set for both the title fallback and the removal set", () => {
const custom = { bug: "kind:bug", feature: "kind:feature", priority: "kind:priority" };
const result = resolvePrTypeLabel({ title: "feat: add provider fallback", labels: custom });
Expand Down
10 changes: 8 additions & 2 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26673,9 +26673,15 @@ describe("queue processors", () => {
},
});

// JSONbored/gittensory falls back to its own bundled manifest (GITTENSORY_REPO_FOCUS_MANIFEST_YAML) when
// no other manifest source responds, which REPLACES this test's DB-configured single-mapping override
// with its own bug/feature (exclusive) + priority (additive) mapping list -- so the linked issue's
// gittensor:priority label composes with the title-derived "fix" -> gittensor:bug, rather than replacing
// it. Priority is additive (not a type of its own; see resolvePrTypeLabel's composition fix), so bug
// still applies from the title and only feature (never matched) needs removing.
expect(seen.issueFetches).toBe(1);
expect(seen.posted).toEqual(["gittensor:priority"]);
expect(seen.removed.sort()).toEqual(["gittensor:bug", "gittensor:feature"]);
expect(seen.posted).toEqual(["gittensor:bug", "gittensor:priority"]);
expect(seen.removed).toEqual(["gittensor:feature"]);
});

it("fails open to the normal title-based label when the linked issue's fetch fails (#priority-linked-issue-gate)", async () => {
Expand Down