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
6 changes: 6 additions & 0 deletions .gittensory-miner.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ preferredLabels:
- bug
- enhancement

# Issue/PR labels a miner must not target; candidates carrying one are skipped.
# String list. Default: [] (nothing blocked).
blockedLabels:
- wontfix
- duplicate

# Maximum issues a single miner may hold claimed on this repo at once, so one
# miner cannot monopolize the queue. A positive integer (>= 1); a non-integer is
# floored. Default: 1.
Expand Down
5 changes: 5 additions & 0 deletions packages/gittensory-engine/src/miner-goal-lane-fit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export function computeMinerGoalLaneFit(
spec: MinerGoalSpec,
): number {
const issueLabels = normalizeLabels(issue.labels);
const blocked = normalizeLabels(spec.blockedLabels);
if (blocked.length > 0 && blocked.some((label) => issueLabels.includes(label))) {
return 0;
}

const preferred = normalizeLabels(spec.preferredLabels);

let score: number;
Expand Down
2 changes: 2 additions & 0 deletions packages/gittensory-engine/src/miner-goal-spec-parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ function freezeSpec(spec: MinerGoalSpec): Readonly<MinerGoalSpec> {
wantedPaths: Object.freeze([...spec.wantedPaths]),
blockedPaths: Object.freeze([...spec.blockedPaths]),
preferredLabels: Object.freeze([...spec.preferredLabels]),
blockedLabels: Object.freeze([...spec.blockedLabels]),
});
}

Expand Down Expand Up @@ -96,6 +97,7 @@ export function parseMinerGoalSpec(raw: unknown): MinerGoalSpecParseResult {
wantedPaths: parseStringList(record.wantedPaths, "wantedPaths", warnings),
blockedPaths: parseStringList(record.blockedPaths, "blockedPaths", warnings),
preferredLabels: parseStringList(record.preferredLabels, "preferredLabels", warnings),
blockedLabels: parseStringList(record.blockedLabels, "blockedLabels", warnings),
maxConcurrentClaims: parseClaims(record.maxConcurrentClaims, warnings),
issueDiscoveryPolicy: parsePolicy(record.issueDiscoveryPolicy, warnings),
});
Expand Down
9 changes: 9 additions & 0 deletions packages/gittensory-engine/src/miner-goal-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export type MinerGoalSpec = {
* Default: [] (no preference).
*/
preferredLabels: readonly string[];
/**
* Issue/PR labels a miner must not target; a candidate carrying one should be skipped. String list.
* Default: [] (nothing blocked).
*/
blockedLabels: readonly string[];
/**
* Maximum number of issues a single miner may hold claimed on this repo at once, so one miner cannot monopolize
* a repo's queue. A positive integer (`>= 1`); the parser is expected to floor a non-integer toward zero
Expand Down Expand Up @@ -69,6 +74,7 @@ export const DEFAULT_MINER_GOAL_SPEC: Readonly<MinerGoalSpec> = Object.freeze({
wantedPaths: Object.freeze([]),
blockedPaths: Object.freeze([]),
preferredLabels: Object.freeze([]),
blockedLabels: Object.freeze([]),
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
Expand All @@ -83,6 +89,7 @@ function cloneDefaultMinerGoalSpec(): MinerGoalSpec {
wantedPaths: [...DEFAULT_MINER_GOAL_SPEC.wantedPaths],
blockedPaths: [...DEFAULT_MINER_GOAL_SPEC.blockedPaths],
preferredLabels: [...DEFAULT_MINER_GOAL_SPEC.preferredLabels],
blockedLabels: [...DEFAULT_MINER_GOAL_SPEC.blockedLabels],
};
}

Expand Down Expand Up @@ -172,6 +179,7 @@ function hasConfiguredGoalFields(spec: MinerGoalSpec): boolean {
spec.wantedPaths.length > 0 ||
spec.blockedPaths.length > 0 ||
spec.preferredLabels.length > 0 ||
spec.blockedLabels.length > 0 ||
spec.maxConcurrentClaims !== DEFAULT_MINER_GOAL_SPEC.maxConcurrentClaims ||
spec.issueDiscoveryPolicy !== DEFAULT_MINER_GOAL_SPEC.issueDiscoveryPolicy
);
Expand Down Expand Up @@ -201,6 +209,7 @@ export function parseMinerGoalSpec(raw: unknown): ParsedMinerGoalSpec {
wantedPaths: normalizeStringList(record.wantedPaths, "wantedPaths", warnings),
blockedPaths: normalizeStringList(record.blockedPaths, "blockedPaths", warnings),
preferredLabels: normalizeStringList(record.preferredLabels, "preferredLabels", warnings),
blockedLabels: normalizeStringList(record.blockedLabels, "blockedLabels", warnings),
maxConcurrentClaims: normalizePositiveInteger(
record.maxConcurrentClaims,
"maxConcurrentClaims",
Expand Down
6 changes: 6 additions & 0 deletions packages/gittensory-engine/test/miner-goal-lane-fit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ test("computeMinerGoalLaneFit applies issueDiscoveryPolicy modifiers", () => {
assert.equal(computeMinerGoalLaneFit({ labels: ["feature"] }, discouraged), 1);
});

test("computeMinerGoalLaneFit returns 0 when a blocked label matches case-insensitively", () => {
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix"] };
assert.equal(computeMinerGoalLaneFit({ labels: ["WontFix"] }, spec), 0);
assert.equal(computeMinerGoalLaneFit({ labels: ["bug"] }, spec), 1);
});

test("computeMinerGoalLaneFit ignores malformed label entries safely", () => {
assert.equal(
computeMinerGoalLaneFit({ labels: ["bug", "", 42 as unknown as string, " "] }, {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ test("parseMinerGoalSpec coerces a full valid object and marks present", () => {
wantedPaths: [" src/** ", "src/**"],
blockedPaths: ["dist/**"],
preferredLabels: [" bug ", "feature"],
blockedLabels: [" wontfix ", "duplicate"],
maxConcurrentClaims: 3,
issueDiscoveryPolicy: "ENCOURAGED",
});
Expand All @@ -29,6 +30,7 @@ test("parseMinerGoalSpec coerces a full valid object and marks present", () => {
wantedPaths: ["src/**"],
blockedPaths: ["dist/**"],
preferredLabels: ["bug", "feature"],
blockedLabels: ["wontfix", "duplicate"],
maxConcurrentClaims: 3,
issueDiscoveryPolicy: "encouraged",
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ test("parseMinerGoalSpec: valid raw config normalizes every field and keeps non-
wantedPaths: ["src/**", " src/** ", "", "docs/**"],
blockedPaths: ["dist/**"],
preferredLabels: ["help wanted", "help wanted", "gittensor:feature"],
blockedLabels: ["duplicate", " duplicate "],
maxConcurrentClaims: 2.9,
issueDiscoveryPolicy: "encouraged",
});
Expand All @@ -41,6 +42,7 @@ test("parseMinerGoalSpec: valid raw config normalizes every field and keeps non-
wantedPaths: ["src/**", "docs/**"],
blockedPaths: ["dist/**"],
preferredLabels: ["help wanted", "gittensor:feature"],
blockedLabels: ["duplicate"],
maxConcurrentClaims: 2,
issueDiscoveryPolicy: "encouraged",
});
Expand All @@ -63,6 +65,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
wantedPaths: "src/**",
blockedPaths: [123, " dist/** ", "", longEntry],
preferredLabels: [false, "bugfix"],
blockedLabels: [123, " wontfix "],
maxConcurrentClaims: 0.9,
issueDiscoveryPolicy: "always",
});
Expand All @@ -73,6 +76,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
wantedPaths: [],
blockedPaths: ["dist/**", longEntry.slice(0, 256)],
preferredLabels: ["bugfix"],
blockedLabels: ["wontfix"],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
Expand All @@ -81,6 +85,7 @@ test("parseMinerGoalSpec: malformed fields fall back independently with targeted
assert.match(warningText, /wantedPaths/i);
assert.match(warningText, /blockedPaths/i);
assert.match(warningText, /preferredLabels/i);
assert.match(warningText, /blockedLabels/i);
assert.match(warningText, /maxConcurrentClaims/i);
assert.match(warningText, /issueDiscoveryPolicy/i);
assert.match(warningText, /truncated an over-long entry/i);
Expand All @@ -97,6 +102,7 @@ test("parseMinerGoalSpec: unknown-only or default-only content stays absent with
wantedPaths: [],
blockedPaths: [],
preferredLabels: [],
blockedLabels: [],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
Expand Down
3 changes: 3 additions & 0 deletions packages/gittensory-engine/test/miner-goal-spec.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ test("DEFAULT_MINER_GOAL_SPEC carries the documented safe defaults", () => {
wantedPaths: [],
blockedPaths: [],
preferredLabels: [],
blockedLabels: [],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
});
Expand All @@ -26,10 +27,12 @@ test("DEFAULT_MINER_GOAL_SPEC is deep-frozen so the shared singleton can't be mu
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.wantedPaths));
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.blockedPaths));
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.preferredLabels));
assert.ok(Object.isFrozen(DEFAULT_MINER_GOAL_SPEC.blockedLabels));
});

test("DEFAULT_MINER_GOAL_SPEC exposes exactly the specified field surface", () => {
assert.deepEqual(Object.keys(DEFAULT_MINER_GOAL_SPEC).sort(), [
"blockedLabels",
"blockedPaths",
"issueDiscoveryPolicy",
"maxConcurrentClaims",
Expand Down
29 changes: 29 additions & 0 deletions test/unit/miner-goal-lane-fit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import {
computeMinerGoalLaneFit,
DEFAULT_MINER_GOAL_SPEC,
isMinerRepoTargetable,
} from "../../packages/gittensory-engine/src/index";

describe("computeMinerGoalLaneFit", () => {
it("respects minerEnabled opt-out", () => {
expect(isMinerRepoTargetable(DEFAULT_MINER_GOAL_SPEC)).toBe(true);
expect(isMinerRepoTargetable({ ...DEFAULT_MINER_GOAL_SPEC, minerEnabled: false })).toBe(false);
});

it("returns 0 when a blocked label matches case-insensitively", () => {
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix", "duplicate"] };
expect(computeMinerGoalLaneFit({ labels: ["WontFix"] }, spec)).toBe(0);
expect(computeMinerGoalLaneFit({ labels: ["DUPLICATE"] }, spec)).toBe(0);
});

it("continues scoring when blocked labels are configured but none match", () => {
const spec = { ...DEFAULT_MINER_GOAL_SPEC, blockedLabels: ["wontfix"], preferredLabels: ["bug"] };
expect(computeMinerGoalLaneFit({ labels: ["bug"] }, spec)).toBe(1);
expect(computeMinerGoalLaneFit({ labels: ["feature"] }, spec)).toBe(0.25);
});

it("scores normally when no blocked labels are configured", () => {
expect(computeMinerGoalLaneFit({ labels: ["docs"] }, DEFAULT_MINER_GOAL_SPEC)).toBe(1);
});
});
6 changes: 6 additions & 0 deletions test/unit/miner-goal-spec-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
wantedPaths: ["src/**", " src/** ", "", "docs/**"],
blockedPaths: ["dist/**", longEntry],
preferredLabels: ["help wanted", "help wanted", "gittensor:feature"],
blockedLabels: ["duplicate", " duplicate "],
maxConcurrentClaims: 2.9,
issueDiscoveryPolicy: "encouraged",
});
Expand All @@ -49,6 +50,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
wantedPaths: ["src/**", "docs/**"],
blockedPaths: ["dist/**", longEntry.slice(0, 256)],
preferredLabels: ["help wanted", "gittensor:feature"],
blockedLabels: ["duplicate"],
maxConcurrentClaims: 2,
issueDiscoveryPolicy: "encouraged",
},
Expand Down Expand Up @@ -82,6 +84,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
wantedPaths: "src/**",
blockedPaths: [123, " dist/** "],
preferredLabels: [false, "bugfix"],
blockedLabels: [123, " wontfix "],
maxConcurrentClaims: "3",
issueDiscoveryPolicy: "always",
});
Expand All @@ -93,6 +96,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
wantedPaths: [],
blockedPaths: ["dist/**"],
preferredLabels: ["bugfix"],
blockedLabels: ["wontfix"],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
},
Expand All @@ -101,6 +105,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
expect.stringMatching(/wantedPaths/i),
expect.stringMatching(/blockedPaths/i),
expect.stringMatching(/preferredLabels/i),
expect.stringMatching(/blockedLabels/i),
expect.stringMatching(/maxConcurrentClaims/i),
expect.stringMatching(/issueDiscoveryPolicy/i),
]),
Expand Down Expand Up @@ -131,6 +136,7 @@ describe("MinerGoalSpec parser (#2301)", () => {
wantedPaths: [],
blockedPaths: [],
preferredLabels: [],
blockedLabels: [],
maxConcurrentClaims: 1,
issueDiscoveryPolicy: "neutral",
}),
Expand Down
Loading