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
16 changes: 16 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,20 @@
"items": {
"type": "string"
}
},
"multiplierTier": {
"type": "string",
"enum": [
"maintainer_created",
"community"
]
},
"availability": {
"type": "string",
"enum": [
"ready",
"maintainer_wip"
]
}
},
"required": [
Expand All @@ -1453,6 +1467,8 @@
"fit",
"score",
"lane",
"multiplierTier",
"availability",
"reasons",
"warnings"
]
Expand Down
2 changes: 2 additions & 0 deletions src/openapi/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@ export const ContributorOpportunitySchema = z
fit: z.enum(["good", "caution", "hold"]),
score: z.number(),
lane: z.enum(["direct_pr", "issue_discovery", "split", "inactive", "unknown"]),
multiplierTier: z.enum(["maintainer_created", "community"]),
availability: z.enum(["ready", "maintainer_wip"]),
reasons: z.array(z.string()),
warnings: z.array(z.string()),
})
Expand Down
56 changes: 53 additions & 3 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,10 +181,39 @@ export type ContributorOpportunity = {
fit: "good" | "caution" | "hold";
score: number;
lane: ParticipationLane;
/** Reward-multiplier tier of the issue. Maintainer-CREATED issues typically carry the biggest Gittensor
* multiplier, so they rank highest when grabbable — surfacing them is the core of issue-watch (#699). */
multiplierTier: "maintainer_created" | "community";
/** Whether the issue is a real outside-contributor target. `maintainer_wip` = maintainer-authored AND
* labelled in-progress/internal → downgraded, not steered to outsiders (the #186 reconciliation). */
availability: "ready" | "maintainer_wip";
reasons: string[];
warnings: string[];
};

// Labels that signal a maintainer's OWN in-progress / internal work — NOT an open outside-contributor
// target. Combined with a maintainer author association, these downgrade an issue (#186) even though
// maintainer-CREATED open issues are otherwise the highest-multiplier targets (#699).
const MAINTAINER_WIP_LABELS = new Set([
"wip",
"work in progress",
"work-in-progress",
"in progress",
"in-progress",
"blocked",
"on hold",
"on-hold",
"draft",
"do not work",
"do-not-work",
"internal",
]);

/** True iff a maintainer-authored issue is labelled as the maintainer's own in-progress/internal work. */
function isMaintainerWipIssue(issue: IssueRecord): boolean {
return isMaintainerAssociation(issue.authorAssociation) && issue.labels.some((label) => MAINTAINER_WIP_LABELS.has(label.toLowerCase().trim()));
}

export type ContributorFit = {
login: string;
generatedAt: string;
Expand Down Expand Up @@ -1302,6 +1331,14 @@ export function buildContributorOpportunities(
: quality?.status === "hold"
? -15
: 0;
const maintainerAuthored = isMaintainerAssociation(issue.authorAssociation);
const maintainerWip = isMaintainerWipIssue(issue);
const multiplierTier: ContributorOpportunity["multiplierTier"] = maintainerAuthored ? "maintainer_created" : "community";
const availability: ContributorOpportunity["availability"] = maintainerWip ? "maintainer_wip" : "ready";
// Maintainer-CREATED grabbable issues carry the biggest Gittensor multiplier → rank them up (#699).
// A maintainer's own WIP/internal issue is heavily downgraded so outsiders aren't steered to it (#186).
const multiplierBoost = maintainerAuthored && !maintainerWip ? 12 : 0;
const maintainerWipPenalty = maintainerWip ? 45 : 0;
const score = clamp(
50 +
(touchedRepos.has(repo.fullName) ? 20 : 0) +
Expand All @@ -1311,11 +1348,13 @@ export function buildContributorOpportunities(
queuePenalty -
bountyPenalty -
(lane.lane === "inactive" || lane.lane === "unknown" ? 35 : 0) +
qualityAdjustment,
qualityAdjustment +
multiplierBoost -
maintainerWipPenalty,
0,
100,
);
const baseFit = score >= 70 ? "good" : score >= 40 ? "caution" : "hold";
const baseFit = maintainerWip ? "hold" : score >= 70 ? "good" : score >= 40 ? "caution" : "hold";
const downgradeToCaution = (bountyPenalty > 0 || quality?.status === "needs_proof") && baseFit === "good";
repoOpportunities.push({
repoFullName: repo.fullName,
Expand All @@ -1324,14 +1363,19 @@ export function buildContributorOpportunities(
fit: downgradeToCaution ? "caution" : baseFit,
score,
lane: lane.lane,
multiplierTier,
availability,
reasons: [
lane.summary,
...(maintainerAuthored && !maintainerWip ? ["Maintainer-created issue — typically the highest contribution multiplier on Gittensor."] : []),
...(touchedRepos.has(repo.fullName) ? ["Contributor has prior activity in this registered repo."] : []),
...(labelFit > 0 ? [`Issue labels overlap contributor history: ${issue.labels.filter((label) => labelHistory.has(label)).join(", ")}.`] : []),
...(bountyLifecycle === "active" ? ["An active bounty is attached as contribution context (not guaranteed payout)."] : []),
...(quality?.status === "ready" ? ["Issue quality report rates this issue as ready."] : []),
],
warnings: [
...(maintainerAuthored && !maintainerWip ? ["Maintainer-authored; confirm it is open for outside contribution before starting."] : []),
...(maintainerWip ? ["Maintainer-authored and labelled in-progress/internal; not a recommended outside-contributor target without confirmation."] : []),
...(repoPullRequests.length >= 8 ? ["This repo has a busy open PR queue."] : []),
...(lane.lane === "issue_discovery" ? ["This repo is not a direct-PR-first lane."] : []),
...(lane.lane === "unknown" || lane.lane === "inactive" ? ["Gittensory cannot recommend this as a strong contribution target right now."] : []),
Expand Down Expand Up @@ -2749,6 +2793,10 @@ export function buildIssueQualityReport(
const bountyContext = bounty ? buildBountyOpportunityContext(bounty, issue, linkedPrs, linkedMergedPrs) : undefined;
const linkedWorkCount = linkedPrs.length + linkedMergedPrs.length + issue.linkedPrs.length;
const linkage = buildIssueLinkageRecord(issue, lifecycleEntry, linkedPrs, linkedMergedPrs);
// #186: maintainer-authored issues must not silently read as "ready" for outside contributors —
// always warn to confirm intent, and downgrade ones labelled as the maintainer's own in-progress work.
const maintainerAuthored = isMaintainerAssociation(issue.authorAssociation);
const maintainerWip = isMaintainerWipIssue(issue);
const reasons = [
...(bodyLength >= 200 ? ["Issue has enough body detail to evaluate."] : []),
...(issue.labels.length > 0 ? [`Labels: ${issue.labels.join(", ")}.`] : []),
Expand All @@ -2769,14 +2817,16 @@ export function buildIssueQualityReport(
...(bountyLifecycle === "historical" ? ["Historical bounty context is attached; this is not an active opportunity without upstream confirmation."] : []),
...(bountyLifecycle === "stale" ? ["Bounty context for this issue looks stale; confirm it is still active before acting."] : []),
...(bountyLifecycle === "ambiguous" ? ["Bounty state for this issue is ambiguous; verify it before acting."] : []),
...(maintainerAuthored && !maintainerWip ? ["Maintainer-authored; confirm it is open for outside contribution before starting."] : []),
...(maintainerWip ? ["Maintainer-authored and labelled in-progress/internal; not a recommended outside-contributor target without confirmation."] : []),
];
const score = clamp(100 - warnings.length * 18 + reasons.length * 5 - (age > 180 ? 15 : 0), 0, 100);
const bountyBlocks = bountyLifecycle === "completed" || bountyLifecycle === "cancelled" || bountyLifecycle === "historical";
const bountyCaution = bountyLifecycle === "stale" || bountyLifecycle === "ambiguous";
const status: IssueQualityReport["issues"][number]["status"] =
linkedWorkCount > 0 || issueCollisions.some((cluster) => cluster.risk === "high") || bountyBlocks || ["duplicate", "invalid", "solved", "valid_solved"].includes(lifecycle)
? "do_not_use"
: warnings.some((warning) => /thin|stale|direct-PR/i.test(warning)) || bountyCaution || lifecycle === "stale"
: maintainerWip || warnings.some((warning) => /thin|stale|direct-PR/i.test(warning)) || bountyCaution || lifecycle === "stale"
? "needs_proof"
: score < 45
? "hold"
Expand Down
2 changes: 2 additions & 0 deletions test/unit/decision-pack.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -850,6 +850,8 @@ describe("decision-pack service", () => {
fit: "good",
score: 82,
lane: "split",
multiplierTier: "community",
availability: "ready",
reasons: ["Active bounty context is available."],
warnings: [],
},
Expand Down
28 changes: 28 additions & 0 deletions test/unit/issue-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,34 @@ describe("issue quality reports", () => {
expect(report.issues[0]?.warnings).toEqual(expect.arrayContaining([expect.stringMatching(/direct-PR first/i)]));
});

it("warns to confirm scope on a maintainer-authored issue but still lets a clean one read ready (#186)", () => {
const repo = issueDiscoveryRepo("owner/maintainer-open");
const report = buildIssueQualityReport(
repo,
[issue(repo.fullName, 30, "Maintainer-created: add reconnect backoff", { body: "x".repeat(220), labels: ["feature"], authorAssociation: "OWNER", updatedAt: now() })],
[],
repo.fullName,
);
expect(report.issues[0]).toMatchObject({
status: "ready",
warnings: expect.arrayContaining(["Maintainer-authored; confirm it is open for outside contribution before starting."]),
});
});

it("never reads a maintainer-authored WIP/internal issue as ready (#186)", () => {
const repo = issueDiscoveryRepo("owner/maintainer-wip");
const report = buildIssueQualityReport(
repo,
[issue(repo.fullName, 31, "Maintainer-created: internal refactor", { body: "x".repeat(220), labels: ["feature", "internal"], authorAssociation: "OWNER", updatedAt: now() })],
[],
repo.fullName,
);
expect(report.issues[0]).toMatchObject({
status: "needs_proof",
warnings: expect.arrayContaining(["Maintainer-authored and labelled in-progress/internal; not a recommended outside-contributor target without confirmation."]),
});
});

it("respects a worker-budget cap of 100 issues per repo", () => {
const repo = issueDiscoveryRepo("owner/big");
const issues = Array.from({ length: 150 }, (_, index) => issue(repo.fullName, index + 1, `bulk ${index}`, { body: "x".repeat(220) }));
Expand Down
60 changes: 60 additions & 0 deletions test/unit/signals.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,66 @@ describe("world-class backend signals", () => {
const opportunities = buildContributorOpportunities(profile, [repo], issues, pullRequests);
expect(profile.trustSignals.level).toBe("new");
expect(opportunities[0]?.repoFullName).toBe(repo.fullName);
for (const opportunity of opportunities) {
expect(opportunity.multiplierTier).toBe("community");
expect(opportunity.availability).toBe("ready");
}
});

it("ranks grabbable maintainer-created issues above community issues and downgrades maintainer WIP (#699/#186)", () => {
const profile = buildContributorProfile("scout", { login: "scout", topLanguages: ["TypeScript"], source: "github" }, [], []);
const maintainerOpen: IssueRecord = {
repoFullName: repo.fullName,
number: 40,
title: "Maintainer-created: implement reconnect backoff",
state: "open",
authorLogin: "entrius",
authorAssociation: "OWNER",
labels: ["feature"],
linkedPrs: [],
};
const maintainerWip: IssueRecord = {
repoFullName: repo.fullName,
number: 41,
title: "Maintainer-created: internal refactor",
state: "open",
authorLogin: "entrius",
authorAssociation: "OWNER",
labels: ["feature", "WIP"],
linkedPrs: [],
};
const community: IssueRecord = {
repoFullName: repo.fullName,
number: 42,
title: "Community-reported: same feature label",
state: "open",
authorLogin: "outsider",
authorAssociation: "NONE",
labels: ["feature"],
linkedPrs: [],
};

const opportunities = buildContributorOpportunities(profile, [repo], [maintainerOpen, maintainerWip, community], []);
const byNumber = new Map(opportunities.map((opportunity) => [opportunity.issueNumber, opportunity]));

const open = byNumber.get(40)!;
expect(open.multiplierTier).toBe("maintainer_created");
expect(open.availability).toBe("ready");
expect(open.reasons).toContain("Maintainer-created issue — typically the highest contribution multiplier on Gittensor.");
expect(open.warnings).toContain("Maintainer-authored; confirm it is open for outside contribution before starting.");

const wip = byNumber.get(41)!;
expect(wip.multiplierTier).toBe("maintainer_created");
expect(wip.availability).toBe("maintainer_wip");
expect(wip.fit).toBe("hold");
expect(wip.warnings).toContain("Maintainer-authored and labelled in-progress/internal; not a recommended outside-contributor target without confirmation.");

const open42 = byNumber.get(42)!;
expect(open42.multiplierTier).toBe("community");

// Same labels/lane: the grabbable maintainer-created issue outscores the community one and the WIP one.
expect(open.score).toBeGreaterThan(open42.score);
expect(open.score).toBeGreaterThan(wip.score);
});

it("profiles contributors from cached repo stats when sampled PR rows miss their history", () => {
Expand Down
Loading