Skip to main content
Glama
activityDistributor.ts9.46 kB
import { DayActivity, GitLabActivity } from '../types/index.js'; import { eachDayOfInterval } from 'date-fns'; interface DistributionResult { activities: DayActivity[]; distributionInfo: { daysWithGaps: number; daysDistributed: number; message: string; }; } export class ActivityDistributor { /** * Distributes activities from days with work to preceding days without activity * This handles cases where developers commit multiple days of work at once */ distributeActivities(activities: DayActivity[]): DistributionResult { const distributed = [...activities]; let gapDays = 0; let distributedDays = 0; // Sort by date to ensure proper order distributed.sort((a, b) => a.date.getTime() - b.date.getTime()); for (let i = 0; i < distributed.length; i++) { const current = distributed[i]; const hasActivity = this.hasAnyActivity(current); if (!hasActivity) { // Found a day without activity - look ahead for the next day with activity const nextActivityIndex = this.findNextActivityDay(distributed, i); if (nextActivityIndex !== -1) { // Calculate the gap (how many days without activity) const gapSize = nextActivityIndex - i; const nextActivity = distributed[nextActivityIndex]; // Distribute the next day's work across the gap this.distributeWorkAcrossGap(distributed, i, nextActivityIndex, gapSize); gapDays += gapSize; distributedDays++; } } } let message = ''; if (distributedDays > 0) { message = `Note: ${gapDays} day(s) had no recorded activity. Work from subsequent days was distributed proportionally. Distributed entries are marked with [Distributed].`; } return { activities: distributed, distributionInfo: { daysWithGaps: gapDays, daysDistributed: distributedDays, message, }, }; } private hasAnyActivity(day: DayActivity): boolean { return ( day.meetings.length > 0 || day.gitlabActivity.commits.length > 0 || day.gitlabActivity.mergeRequests.length > 0 || day.gitlabActivity.issues.length > 0 ); } private findNextActivityDay(activities: DayActivity[], startIndex: number): number { for (let i = startIndex + 1; i < activities.length; i++) { if (this.hasAnyActivity(activities[i])) { return i; } } return -1; // No activity found } private distributeWorkAcrossGap( activities: DayActivity[], gapStart: number, activityIndex: number, gapSize: number ): void { const sourceActivity = activities[activityIndex]; // Calculate how to split the work const commitsPerDay = Math.floor(sourceActivity.gitlabActivity.commits.length / (gapSize + 1)); const mrsPerDay = Math.floor(sourceActivity.gitlabActivity.mergeRequests.length / (gapSize + 1)); const issuesPerDay = Math.floor(sourceActivity.gitlabActivity.issues.length / (gapSize + 1)); // Distribute commits let commitIndex = 0; for (let i = gapStart; i < activityIndex; i++) { const commitsToAdd = Math.min(commitsPerDay, sourceActivity.gitlabActivity.commits.length - commitIndex); if (commitsToAdd > 0) { const distributedCommits = sourceActivity.gitlabActivity.commits .slice(commitIndex, commitIndex + commitsToAdd) .map(commit => ({ ...commit, message: `[Distributed] ${commit.message}`, })); activities[i].gitlabActivity.commits.push(...distributedCommits); commitIndex += commitsToAdd; } } // Distribute MRs let mrIndex = 0; for (let i = gapStart; i < activityIndex; i++) { const mrsToAdd = Math.min(mrsPerDay, sourceActivity.gitlabActivity.mergeRequests.length - mrIndex); if (mrsToAdd > 0) { const distributedMRs = sourceActivity.gitlabActivity.mergeRequests .slice(mrIndex, mrIndex + mrsToAdd) .map(mr => ({ ...mr, title: `[Distributed] ${mr.title}`, })); activities[i].gitlabActivity.mergeRequests.push(...distributedMRs); mrIndex += mrsToAdd; } } // Distribute issues let issueIndex = 0; for (let i = gapStart; i < activityIndex; i++) { const issuesToAdd = Math.min(issuesPerDay, sourceActivity.gitlabActivity.issues.length - issueIndex); if (issuesToAdd > 0) { const distributedIssues = sourceActivity.gitlabActivity.issues .slice(issueIndex, issueIndex + issuesToAdd) .map(issue => ({ ...issue, title: `[Distributed] ${issue.title}`, })); activities[i].gitlabActivity.issues.push(...distributedIssues); issueIndex += issuesToAdd; } } // Add a note to the original day that some work was distributed if (commitIndex > 0 || mrIndex > 0 || issueIndex > 0) { const note = { message: `[Note: Some activities from this day were distributed to previous ${gapSize} day(s) without recorded work]`, project: 'System', branch: 'distribution', }; sourceActivity.gitlabActivity.commits.unshift(note); } } /** * More intelligent distribution that considers work patterns * For example, if there are 3 commits on day 4, and days 1-3 are empty, * it might distribute as: day1: planning/research, day2-3: implementation, day4: finalization */ distributeActivitiesIntelligent(activities: DayActivity[]): DistributionResult { const distributed = [...activities]; let gapDays = 0; let distributedDays = 0; distributed.sort((a, b) => a.date.getTime() - b.date.getTime()); for (let i = 0; i < distributed.length; i++) { const current = distributed[i]; const hasActivity = this.hasAnyActivity(current); if (!hasActivity) { const nextActivityIndex = this.findNextActivityDay(distributed, i); if (nextActivityIndex !== -1) { const gapSize = nextActivityIndex - i; const nextActivity = distributed[nextActivityIndex]; // Create work phases based on gap size this.distributeWorkPhases(distributed, i, nextActivityIndex, gapSize, nextActivity); gapDays += gapSize; distributedDays++; } } } let message = ''; if (distributedDays > 0) { message = `Note: ${gapDays} day(s) had no recorded activity. Work phases were inferred and distributed. Entries are marked with [Phase: X].`; } return { activities: distributed, distributionInfo: { daysWithGaps: gapDays, daysDistributed: distributedDays, message, }, }; } private distributeWorkPhases( activities: DayActivity[], gapStart: number, activityIndex: number, gapSize: number, sourceActivity: DayActivity ): void { const phases = this.generateWorkPhases(gapSize, sourceActivity); for (let i = 0; i < phases.length && (gapStart + i) < activityIndex; i++) { const phase = phases[i]; const dayIndex = gapStart + i; // Add phase description as a synthetic commit activities[dayIndex].gitlabActivity.commits.push({ message: phase, project: 'Distributed Work', branch: 'phase-distribution', }); } } private generateWorkPhases(gapSize: number, sourceActivity: DayActivity): string[] { const phases: string[] = []; const hasCommits = sourceActivity.gitlabActivity.commits.length > 0; const hasMRs = sourceActivity.gitlabActivity.mergeRequests.length > 0; const hasIssues = sourceActivity.gitlabActivity.issues.length > 0; // Determine work type from source activity const workSummary = this.summarizeWork(sourceActivity); if (gapSize === 1) { phases.push(`[Phase: Research & Planning] Preparation for: ${workSummary}`); } else if (gapSize === 2) { phases.push(`[Phase: Analysis] Initial work on: ${workSummary}`); phases.push(`[Phase: Development] Continued work on: ${workSummary}`); } else if (gapSize === 3) { phases.push(`[Phase: Planning] Planned work for: ${workSummary}`); phases.push(`[Phase: Implementation] Developed features for: ${workSummary}`); phases.push(`[Phase: Testing] Testing and refinement for: ${workSummary}`); } else { // For longer gaps, distribute evenly const phaseNames = ['Planning', 'Research', 'Design', 'Implementation', 'Testing', 'Refinement', 'Documentation']; for (let i = 0; i < gapSize; i++) { const phaseName = phaseNames[i % phaseNames.length]; phases.push(`[Phase: ${phaseName}] Work on: ${workSummary}`); } } return phases; } private summarizeWork(activity: DayActivity): string { const parts: string[] = []; if (activity.gitlabActivity.commits.length > 0) { const firstCommit = activity.gitlabActivity.commits[0]; parts.push(firstCommit.message.substring(0, 50)); } if (activity.gitlabActivity.mergeRequests.length > 0) { const firstMR = activity.gitlabActivity.mergeRequests[0]; parts.push(firstMR.title.substring(0, 50)); } if (activity.gitlabActivity.issues.length > 0) { const firstIssue = activity.gitlabActivity.issues[0]; parts.push(firstIssue.title.substring(0, 50)); } return parts.join(', ') || 'project work'; } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sharadmathuratthepsi/timesheet-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server