Skip to content
Closed
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
21 changes: 20 additions & 1 deletion src/review_assign/services/github.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { execSync } from 'child_process';
import { createLogger } from '../utils/logger.js';
import { PullRequestInfo, ReviewedPR, TeamMember } from '../types/index.js';
import { PullRequestInfo, ReviewedPR, PendingReviewPR, TeamMember } from '../types/index.js';

const githubLogger = createLogger('review_assign_service', 'github');
const teamMembersCache = new Map<string, { data: TeamMember[]; fetchedAt: number }>();
Expand Down Expand Up @@ -60,6 +60,25 @@ export const searchReviewedPRs = async (username: string, daysAgo: number): Prom
}
}

/**
* Busca los PRs pendientes de revisión por un usuario
* @param username Nombre de usuario de GitHub
* @returns Lista de PRs pendientes de revisión
*/
export const searchPendingReviewPRs = async (username: string): Promise<PendingReviewPR[]> => {
try {
// Busca PRs abiertos donde el usuario está asignado como revisor y que aún requieren revisión
const query = `"is:open review-requested:${username} -review:required"`;
const cmd = `gh search prs ${query} --json number,repository,url,title,requestedReviewers --limit 100`;
const output = execSync(cmd).toString();

return JSON.parse(output);
} catch (error) {
githubLogger.error(`Error al buscar PRs pendientes de revisión para ${username}: ${error}`);
return [];
}
}

/**
* Genera una clave para el cache de miembros del equipo
* @param org Nombre de la organización
Expand Down
46 changes: 40 additions & 6 deletions src/review_assign/services/reviewer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TeamMember } from '../types/index.js';
import { searchReviewedPRs } from './github.js';
import { ReviewerSelection, ReviewerStats, ReviewedPR } from '../types/index.js';
import { searchReviewedPRs, searchPendingReviewPRs } from './github.js';
import { ReviewerSelection, ReviewerStats, ReviewedPR, PendingReviewPR } from '../types/index.js';

/**
* Cuenta el número de revisiones realizadas por un miembro en los últimos días
Expand Down Expand Up @@ -32,6 +32,34 @@ export const countMemberReviews = async (
}
}

/**
* Cuenta el número de revisiones pendientes asignadas a un miembro
* @param member Miembro del equipo
* @param teamRepositories Repositorios del equipo
* @returns Número de revisiones pendientes asignadas al miembro
*/
export const countMemberPendingReviews = async (
member: TeamMember,
teamRepositories: string[]
): Promise<number> => {
try {
const pendingReviews: PendingReviewPR[] = await searchPendingReviewPRs(member.nickname_github);
let teamPendingReviews = 0;

for (const review of pendingReviews) {
const repoName = review.repository.nameWithOwner;
if (teamRepositories.includes(repoName)) {
teamPendingReviews++;
}
}

return teamPendingReviews;
} catch (error) {
console.error(`Error al contar revisiones pendientes para ${member.nickname_github}: ${error}`);
return 0;
}
}

/**
* Selecciona el revisor óptimo para un PR
* @param availableMembers Miembros disponibles para revisar
Expand All @@ -47,13 +75,19 @@ export const selectOptimalReviewer = async (
const reviewStats: ReviewerStats[] = [];

for (const member of availableMembers) {
const count: number = await countMemberReviews(member, teamRepositories, days);
const completedCount: number = await countMemberReviews(member, teamRepositories, days);
const pendingCount: number = await countMemberPendingReviews(member, teamRepositories);
const workloadFactor = member.workloadFactor ?? 1.0;
const normalizedCount = count / workloadFactor;


// Consideramos tanto las revisiones completadas como las pendientes en la carga de trabajo
// Las pendientes tienen un peso mayor ya que son trabajo actual que debe realizarse
const weightedCount = completedCount + (pendingCount * 2); // Las pendientes pesan el doble
const normalizedCount = weightedCount / workloadFactor;

reviewStats.push({
member,
reviewCount: count,
reviewCount: completedCount,
pendingReviewCount: pendingCount,
normalizedCount: normalizedCount
});
}
Expand Down
7 changes: 5 additions & 2 deletions src/review_assign/tools/assign-reviewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ export const registerAssignReviewerTool = (server: McpServer) => {
name: stat.member.name,
github: stat.member.nickname_github,
email: stat.member.email,
reviews_count: stat.normalizedCount
completed_reviews: stat.reviewCount,
pending_reviews: stat.pendingReviewCount,
weighted_score: stat.normalizedCount
}));

return {
Expand All @@ -133,7 +135,8 @@ export const registerAssignReviewerTool = (server: McpServer) => {
team: matchingTeam.team_name,
thread_key: actualThreadKey,
selection_criteria: {
method: "Menor carga de trabajo en los últimos " + days + " días",
method: "Menor carga de trabajo considerando revisiones completadas en los últimos " + days + " días y revisiones pendientes",
weighting: "Las revisiones pendientes tienen un peso doble en el cálculo de carga",
available_reviewers: reviewersInfo
}
}, null, 2)
Expand Down
15 changes: 15 additions & 0 deletions src/review_assign/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export interface LogConfig {
export interface ReviewerStats {
member: TeamMember;
reviewCount: number;
pendingReviewCount: number;
normalizedCount: number;
}

Expand All @@ -54,3 +55,17 @@ export interface ReviewedPR {
url: string;
title: string;
}

export interface PendingReviewPR {
number: number;
repository: { name: string, nameWithOwner: string };
url: string;
title: string;
requestedReviewers?: { login: string }[];
}

export interface LastReviewer {
repository: string;
nickname_github: string;
assignedAt: number;
}