Skip to content

Commit 2038e11

Browse files
feat(engine): add blockedLabels to miner goal spec and lane fit (#2729)
* feat(engine): add blockedLabels to miner goal spec and lane fit Let maintainers declare issue labels miners must skip; lane-fit returns zero when a candidate carries a blocked label, mirroring blockedPaths for discovery metadata. Co-authored-by: Cursor <cursoragent@cursor.com> * test(engine): cover blockedLabels lane-fit in vitest for codecov patch Add root vitest coverage for computeMinerGoalLaneFit blocked-label exclusion so codecov/patch hits the changed lines in miner-goal-lane-fit.ts. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 7306f88 commit 2038e11

10 files changed

Lines changed: 74 additions & 0 deletions

.gittensory-miner.yml.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,12 @@ preferredLabels:
3838
- bug
3939
- enhancement
4040

41+
# Issue/PR labels a miner must not target; candidates carrying one are skipped.
42+
# String list. Default: [] (nothing blocked).
43+
blockedLabels:
44+
- wontfix
45+
- duplicate
46+
4147
# Maximum issues a single miner may hold claimed on this repo at once, so one
4248
# miner cannot monopolize the queue. A positive integer (>= 1); a non-integer is
4349
# floored. Default: 1.

packages/gittensory-engine/src/miner-goal-lane-fit.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ export function computeMinerGoalLaneFit(
2626
spec: MinerGoalSpec,
2727
): number {
2828
const issueLabels = normalizeLabels(issue.labels);
29+
const blocked = normalizeLabels(spec.blockedLabels);
30+
if (blocked.length > 0 && blocked.some((label) => issueLabels.includes(label))) {
31+
return 0;
32+
}
33+
2934
const preferred = normalizeLabels(spec.preferredLabels);
3035

3136
let score: number;

packages/gittensory-engine/src/miner-goal-spec-parse.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ function freezeSpec(spec: MinerGoalSpec): Readonly<MinerGoalSpec> {
2020
wantedPaths: Object.freeze([...spec.wantedPaths]),
2121
blockedPaths: Object.freeze([...spec.blockedPaths]),
2222
preferredLabels: Object.freeze([...spec.preferredLabels]),
23+
blockedLabels: Object.freeze([...spec.blockedLabels]),
2324
});
2425
}
2526

@@ -96,6 +97,7 @@ export function parseMinerGoalSpec(raw: unknown): MinerGoalSpecParseResult {
9697
wantedPaths: parseStringList(record.wantedPaths, "wantedPaths", warnings),
9798
blockedPaths: parseStringList(record.blockedPaths, "blockedPaths", warnings),
9899
preferredLabels: parseStringList(record.preferredLabels, "preferredLabels", warnings),
100+
blockedLabels: parseStringList(record.blockedLabels, "blockedLabels", warnings),
99101
maxConcurrentClaims: parseClaims(record.maxConcurrentClaims, warnings),
100102
issueDiscoveryPolicy: parsePolicy(record.issueDiscoveryPolicy, warnings),
101103
});

packages/gittensory-engine/src/miner-goal-spec.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export type MinerGoalSpec = {
3333
* Default: [] (no preference).
3434
*/
3535
preferredLabels: readonly string[];
36+
/**
37+
* Issue/PR labels a miner must not target; a candidate carrying one should be skipped. String list.
38+
* Default: [] (nothing blocked).
39+
*/
40+
blockedLabels: readonly string[];
3641
/**
3742
* Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize
3843
* a repo's queue. A positive integer (`>= 1`); the parser is expected to floor a non-integer toward zero
@@ -69,6 +74,7 @@ export const DEFAULT_MINER_GOAL_SPEC: Readonly<MinerGoalSpec> = Object.freeze({
6974
wantedPaths: Object.freeze([]),
7075
blockedPaths: Object.freeze([]),
7176
preferredLabels: Object.freeze([]),
77+
blockedLabels: Object.freeze([]),
7278
maxConcurrentClaims: 1,
7379
issueDiscoveryPolicy: "neutral",
7480
});
@@ -83,6 +89,7 @@ function cloneDefaultMinerGoalSpec(): MinerGoalSpec {
8389
wantedPaths: [...DEFAULT_MINER_GOAL_SPEC.wantedPaths],
8490
blockedPaths: [...DEFAULT_MINER_GOAL_SPEC.blockedPaths],
8591
preferredLabels: [...DEFAULT_MINER_GOAL_SPEC.preferredLabels],
92+
blockedLabels: [...DEFAULT_MINER_GOAL_SPEC.blockedLabels],
8693
};
8794
}
8895

@@ -172,6 +179,7 @@ function hasConfiguredGoalFields(spec: MinerGoalSpec): boolean {
172179
spec.wantedPaths.length > 0 ||
173180
spec.blockedPaths.length > 0 ||
174181
spec.preferredLabels.length > 0 ||
182+
spec.blockedLabels.length > 0 ||
175183
spec.maxConcurrentClaims !== DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims ||
176184
spec.issueDiscoveryPolicy !== DEFAULT_MINER_GOAL_SPEC.issueDiscoveryPolicy
177185
);
@@ -201,6 +209,7 @@ export function parseMinerGoalSpec(raw: unknown): ParsedMinerGoalSpec {
201209
wantedPaths: normalizeStringList(record.wantedPaths, "wantedPaths", warnings),
202210
blockedPaths: normalizeStringList(record.blockedPaths, "blockedPaths", warnings),
203211
preferredLabels: normalizeStringList(record.preferredLabels, "preferredLabels", warnings),
212+
blockedLabels: normalizeStringList(record.blockedLabels, "blockedLabels", warnings),
204213
maxConcurrentClaims: normalizePositiveInteger(
205214
record.maxConcurrentClaims,
206215
"maxConcurrentClaims",

packages/gittensory-engine/test/miner-goal-lane-fit.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ test("computeMinerGoalLaneFit applies issueDiscoveryPolicy modifiers", () => {
3636
assert.equal(computeMinerGoalLaneFit({ labels: ["feature"] }, discouraged), 1);
3737
});
3838

39+
test("computeMinerGoalLaneFit returns 0 when a blocked label matches case-insensitively", () => {
40+
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix"] };
41+
assert.equal(computeMinerGoalLaneFit({ labels: ["WontFix"] }, spec), 0);
42+
assert.equal(computeMinerGoalLaneFit({ labels: ["bug"] }, spec), 1);
43+
});
44+
3945
test("computeMinerGoalLaneFit ignores malformed label entries safely", () => {
4046
assert.equal(
4147
computeMinerGoalLaneFit({ labels: ["bug", "", 42 as unknown as string, " "] }, {

packages/gittensory-engine/test/miner-goal-spec-parse.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ test("parseMinerGoalSpec coerces a full valid object and marks present", () => {
1919
wantedPaths: [" src/** ", "src/**"],
2020
blockedPaths: ["dist/**"],
2121
preferredLabels: [" bug ", "feature"],
22+
blockedLabels: [" wontfix ", "duplicate"],
2223
maxConcurrentClaims: 3,
2324
issueDiscoveryPolicy: "ENCOURAGED",
2425
});
@@ -29,6 +30,7 @@ test("parseMinerGoalSpec coerces a full valid object and marks present", () => {
2930
wantedPaths: ["src/**"],
3031
blockedPaths: ["dist/**"],
3132
preferredLabels: ["bug", "feature"],
33+
blockedLabels: ["wontfix", "duplicate"],
3234
maxConcurrentClaims: 3,
3335
issueDiscoveryPolicy: "encouraged",
3436
});

packages/gittensory-engine/test/miner-goal-spec-parser.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ test("parseMinerGoalSpec: valid raw config normalizes every field and keeps non-
3131
wantedPaths: ["src/**", " src/** ", "", "docs/**"],
3232
blockedPaths: ["dist/**"],
3333
preferredLabels: ["help wanted", "help wanted", "gittensor:feature"],
34+
blockedLabels: ["duplicate", " duplicate "],
3435
maxConcurrentClaims: 2.9,
3536
issueDiscoveryPolicy: "encouraged",
3637
});
@@ -41,6 +42,7 @@ test("parseMinerGoalSpec: valid raw config normalizes every field and keeps non-
4142
wantedPaths: ["src/**", "docs/**"],
4243
blockedPaths: ["dist/**"],
4344
preferredLabels: ["help wanted", "gittensor:feature"],
45+
blockedLabels: ["duplicate"],
4446
maxConcurrentClaims: 2,
4547
issueDiscoveryPolicy: "encouraged",
4648
});
@@ -63,6 +65,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
6365
wantedPaths: "src/**",
6466
blockedPaths: [123, " dist/** ", "", longEntry],
6567
preferredLabels: [false, "bugfix"],
68+
blockedLabels: [123, " wontfix "],
6669
maxConcurrentClaims: 0.9,
6770
issueDiscoveryPolicy: "always",
6871
});
@@ -73,6 +76,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
7376
wantedPaths: [],
7477
blockedPaths: ["dist/**", longEntry.slice(0, 256)],
7578
preferredLabels: ["bugfix"],
79+
blockedLabels: ["wontfix"],
7680
maxConcurrentClaims: 1,
7781
issueDiscoveryPolicy: "neutral",
7882
});
@@ -81,6 +85,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
8185
assert.match(warningText, /wantedPaths/i);
8286
assert.match(warningText, /blockedPaths/i);
8387
assert.match(warningText, /preferredLabels/i);
88+
assert.match(warningText, /blockedLabels/i);
8489
assert.match(warningText, /maxConcurrentClaims/i);
8590
assert.match(warningText, /issueDiscoveryPolicy/i);
8691
assert.match(warningText, /truncated an over-long entry/i);
@@ -97,6 +102,7 @@ test("parseMinerGoalSpec: unknown-only or default-only content stays absent with
97102
wantedPaths: [],
98103
blockedPaths: [],
99104
preferredLabels: [],
105+
blockedLabels: [],
100106
maxConcurrentClaims: 1,
101107
issueDiscoveryPolicy: "neutral",
102108
});

packages/gittensory-engine/test/miner-goal-spec.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ test("DEFAULT_MINER_GOAL_SPEC carries the documented safe defaults", () => {
1616
wantedPaths: [],
1717
blockedPaths: [],
1818
preferredLabels: [],
19+
blockedLabels: [],
1920
maxConcurrentClaims: 1,
2021
issueDiscoveryPolicy: "neutral",
2122
});
@@ -26,10 +27,12 @@ test("DEFAULT_MINER_GOAL_SPEC is deep-frozen so the shared singleton can't be mu
2627
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.wantedPaths));
2728
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.blockedPaths));
2829
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.preferredLabels));
30+
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.blockedLabels));
2931
});
3032

3133
test("DEFAULT_MINER_GOAL_SPEC exposes exactly the specified field surface", () => {
3234
assert.deepEqual(Object.keys(DEFAULT_MINER_GOAL_SPEC).sort(), [
35+
"blockedLabels",
3336
"blockedPaths",
3437
"issueDiscoveryPolicy",
3538
"maxConcurrentClaims",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
computeMinerGoalLaneFit,
4+
DEFAULT_MINER_GOAL_SPEC,
5+
isMinerRepoTargetable,
6+
} from "../../packages/gittensory-engine/src/index";
7+
8+
describe("computeMinerGoalLaneFit", () => {
9+
it("respects minerEnabled opt-out", () => {
10+
expect(isMinerRepoTargetable(DEFAULT_MINER_GOAL_SPEC)).toBe(true);
11+
expect(isMinerRepoTargetable({ ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false })).toBe(false);
12+
});
13+
14+
it("returns 0 when a blocked label matches case-insensitively", () => {
15+
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix", "duplicate"] };
16+
expect(computeMinerGoalLaneFit({ labels: ["WontFix"] }, spec)).toBe(0);
17+
expect(computeMinerGoalLaneFit({ labels: ["DUPLICATE"] }, spec)).toBe(0);
18+
});
19+
20+
it("continues scoring when blocked labels are configured but none match", () => {
21+
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix"], preferredLabels: ["bug"] };
22+
expect(computeMinerGoalLaneFit({ labels: ["bug"] }, spec)).toBe(1);
23+
expect(computeMinerGoalLaneFit({ labels: ["feature"] }, spec)).toBe(0.25);
24+
});
25+
26+
it("scores normally when no blocked labels are configured", () => {
27+
expect(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC)).toBe(1);
28+
});
29+
});

test/unit/miner-goal-spec-parser.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
3838
wantedPaths: ["src/**", " src/** ", "", "docs/**"],
3939
blockedPaths: ["dist/**", longEntry],
4040
preferredLabels: ["help wanted", "help wanted", "gittensor:feature"],
41+
blockedLabels: ["duplicate", " duplicate "],
4142
maxConcurrentClaims: 2.9,
4243
issueDiscoveryPolicy: "encouraged",
4344
});
@@ -49,6 +50,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
4950
wantedPaths: ["src/**", "docs/**"],
5051
blockedPaths: ["dist/**", longEntry.slice(0, 256)],
5152
preferredLabels: ["help wanted", "gittensor:feature"],
53+
blockedLabels: ["duplicate"],
5254
maxConcurrentClaims: 2,
5355
issueDiscoveryPolicy: "encouraged",
5456
},
@@ -82,6 +84,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
8284
wantedPaths: "src/**",
8385
blockedPaths: [123, " dist/** "],
8486
preferredLabels: [false, "bugfix"],
87+
blockedLabels: [123, " wontfix "],
8588
maxConcurrentClaims: "3",
8689
issueDiscoveryPolicy: "always",
8790
});
@@ -93,6 +96,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
9396
wantedPaths: [],
9497
blockedPaths: ["dist/**"],
9598
preferredLabels: ["bugfix"],
99+
blockedLabels: ["wontfix"],
96100
maxConcurrentClaims: 1,
97101
issueDiscoveryPolicy: "neutral",
98102
},
@@ -101,6 +105,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
101105
expect.stringMatching(/wantedPaths/i),
102106
expect.stringMatching(/blockedPaths/i),
103107
expect.stringMatching(/preferredLabels/i),
108+
expect.stringMatching(/blockedLabels/i),
104109
expect.stringMatching(/maxConcurrentClaims/i),
105110
expect.stringMatching(/issueDiscoveryPolicy/i),
106111
]),
@@ -131,6 +136,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
131136
wantedPaths: [],
132137
blockedPaths: [],
133138
preferredLabels: [],
139+
blockedLabels: [],
134140
maxConcurrentClaims: 1,
135141
issueDiscoveryPolicy: "neutral",
136142
}),

0 commit comments

Comments
 (0)