@opentask/taskin-task-manager 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import tsConfig from '../../eslint/typescript.js';
2
+
3
+ export default [
4
+ ...tsConfig,
5
+ {
6
+ ignores: ['vitest.config.ts', '*.config.js', '*.config.ts', 'dist/**'],
7
+ },
8
+ ];
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@opentask/taskin-task-manager",
3
+ "version": "1.0.6",
4
+ "description": "Task lifecycle management for Taskin",
5
+ "type": "module",
6
+ "motivation": "To manage the lifecycle of tasks, including creation, status updates, and retrieval.",
7
+ "solve": "Provides a structured API for interacting with task data, abstracting the underlying storage mechanism.",
8
+ "scope": "core",
9
+ "status": "active",
10
+ "since": "2025-11-05",
11
+ "keywords": [
12
+ "taskin",
13
+ "task-manager",
14
+ "task-management",
15
+ "lifecycle"
16
+ ],
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/sidartaveloso/taskin.git",
21
+ "directory": "packages/task-manager"
22
+ },
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "main": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ }
33
+ },
34
+ "dependencies": {
35
+ "@opentask/taskin-types": "1.0.5"
36
+ },
37
+ "devDependencies": {
38
+ "typescript": "^5.9.3"
39
+ },
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "clean": "rm -rf dist coverage .turbo",
43
+ "dev": "tsc --watch",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check .",
46
+ "lint": "eslint .",
47
+ "lint:fix": "eslint . --fix",
48
+ "test": "vitest run",
49
+ "test:coverage": "vitest run --coverage",
50
+ "typecheck": "tsc --noEmit"
51
+ }
52
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from './task-manager';
2
+ export * from './task-manager.types';
3
+
4
+ // Re-export commonly used types from @opentask/taskin-types for convenience
5
+ export type { Task, TaskStatus, TaskType, User } from '@opentask/taskin-types';
@@ -0,0 +1,96 @@
1
+ import type {
2
+ StatsQuery,
3
+ TaskStats,
4
+ TeamStats,
5
+ UserStats,
6
+ } from '@opentask/taskin-types';
7
+
8
+ /**
9
+ * Manages metrics and statistics for users, teams, and tasks.
10
+ * Aggregates data from Git history and task files to provide productivity insights.
11
+ *
12
+ * @remarks
13
+ * This interface separates analytics/reporting responsibilities from ITaskProvider,
14
+ * following the Single Responsibility Principle.
15
+ *
16
+ * @example
17
+ * ```ts
18
+ * const metrics = new FileSystemMetricsAdapter(tasksDir, userRegistry, gitAnalyzer);
19
+ * const stats = await metrics.getUserMetrics('john-doe', { period: 'week' });
20
+ * console.log(`Commits: ${stats.codeMetrics.commits}`);
21
+ * ```
22
+ *
23
+ * @public
24
+ */
25
+ export interface IMetricsManager {
26
+ /**
27
+ * Get productivity metrics for a specific user.
28
+ *
29
+ * Includes code metrics (commits, lines of code), temporal patterns
30
+ * (day of week, time of day), and engagement statistics (completion rate, streaks).
31
+ *
32
+ * @param userId - User identifier (username or registry ID)
33
+ * @param query - Optional query parameters to filter results
34
+ * @param query.period - Time period to analyze ('day' | 'week' | 'month' | 'year')
35
+ * @returns Promise resolving to user statistics
36
+ *
37
+ * @example
38
+ * ```ts
39
+ * const stats = await metrics.getUserMetrics('alice', { period: 'month' });
40
+ * console.log(`Tasks completed: ${stats.contributionMetrics.tasksCompleted}`);
41
+ * console.log(`Completion rate: ${(stats.engagementMetrics.completionRate * 100).toFixed(1)}%`);
42
+ * ```
43
+ */
44
+ getUserMetrics(userId: string, query?: StatsQuery): Promise<UserStats>;
45
+
46
+ /**
47
+ * Get aggregated metrics for an entire team.
48
+ *
49
+ * Provides overview of team productivity with breakdown by individual contributors.
50
+ * Useful for team retrospectives, capacity planning, and identifying top contributors.
51
+ *
52
+ * @param teamId - Team identifier
53
+ * @param query - Optional query parameters to filter results
54
+ * @returns Promise resolving to team statistics with per-contributor breakdown
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const stats = await metrics.getTeamMetrics('frontend', { period: 'week' });
59
+ * console.log(`Total commits: ${stats.totalCommits}`);
60
+ * console.log(`Contributors: ${stats.totalContributors}`);
61
+ * stats.contributors.forEach(c => {
62
+ * console.log(`${c.username}: ${c.commits} commits, ${c.tasksCompleted} tasks`);
63
+ * });
64
+ * ```
65
+ */
66
+ getTeamMetrics(teamId: string, query?: StatsQuery): Promise<TeamStats>;
67
+
68
+ /**
69
+ * Get detailed metrics for a specific task.
70
+ *
71
+ * Includes timeline analysis, contributor activity, code impact metrics,
72
+ * and work patterns. Useful for task retrospectives and understanding
73
+ * effort distribution.
74
+ *
75
+ * @param taskId - Task identifier (task number like '015' or full ID like 'task-015')
76
+ * @param query - Optional query parameters
77
+ * @returns Promise resolving to task-specific statistics
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const stats = await metrics.getTaskMetrics('task-015', {});
82
+ * console.log(`Duration: ${stats.duration} days`);
83
+ * console.log(`Contributors: ${stats.contributors.map(c => c.name).join(', ')}`);
84
+ * if (stats.refactoringMetrics) {
85
+ * console.log(`Simplification ratio: ${stats.refactoringMetrics.simplificationRatio.toFixed(2)}x`);
86
+ * }
87
+ * ```
88
+ */
89
+ getTaskMetrics(taskId: string, query?: StatsQuery): Promise<TaskStats>;
90
+ }
91
+
92
+ export type {
93
+ TaskStats as TaskMetrics,
94
+ TeamStats as TeamMetrics,
95
+ UserStats as UserMetrics,
96
+ };
@@ -0,0 +1,20 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { IMetricsManager } from './metrics.types';
3
+
4
+ describe('IMetricsManager contract', () => {
5
+ it('accepts an object implementing IMetricsManager', async () => {
6
+ const metrics: IMetricsManager = {
7
+ getUserMetrics: async (userId: string) => ({ userId }) as any,
8
+ getTeamMetrics: async (teamId: string) => ({ teamId }) as any,
9
+ getTaskMetrics: async (taskId: string) => ({ taskId }) as any,
10
+ };
11
+
12
+ expect(typeof metrics.getUserMetrics).toBe('function');
13
+ expect(typeof metrics.getTeamMetrics).toBe('function');
14
+ expect(typeof metrics.getTaskMetrics).toBe('function');
15
+
16
+ await expect(metrics.getUserMetrics('u1')).resolves.toBeDefined();
17
+ await expect(metrics.getTeamMetrics('t1')).resolves.toBeDefined();
18
+ await expect(metrics.getTaskMetrics('task1')).resolves.toBeDefined();
19
+ });
20
+ });
@@ -0,0 +1,24 @@
1
+ import type { TaskId } from '@opentask/taskin-types';
2
+ import { vi } from 'vitest';
3
+ import type { ITaskProvider, TaskFile } from './task-manager.types';
4
+
5
+ export const createMockTask = (overrides?: Partial<TaskFile>): TaskFile => ({
6
+ content: '# Task 001 - Implement feature',
7
+ createdAt: new Date().toISOString(),
8
+ description: 'A test feature',
9
+ filePath: '/tasks/task-001.md',
10
+ id: 'task-001' satisfies string as TaskId,
11
+ status: 'pending',
12
+ title: 'Implement feature',
13
+ type: 'feat',
14
+ userId: 'user-123',
15
+ ...overrides,
16
+ });
17
+
18
+ export const createMockTaskProvider = (): ITaskProvider => ({
19
+ findTask: vi.fn(),
20
+ getAllTasks: vi.fn(),
21
+ updateTask: vi.fn(),
22
+ createTask: vi.fn(),
23
+ lint: vi.fn(),
24
+ });
@@ -0,0 +1,133 @@
1
+ import type { Mock } from 'vitest';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { TaskManager } from './task-manager';
4
+ import { createMockTask, createMockTaskProvider } from './task-manager.mock';
5
+ import type { ITaskProvider, TaskFile } from './task-manager.types';
6
+
7
+ describe('TaskManager', () => {
8
+ let taskManager: TaskManager;
9
+ let mockTaskProvider: ITaskProvider;
10
+ let mockTask: TaskFile;
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks();
14
+ mockTaskProvider = createMockTaskProvider();
15
+ mockTask = createMockTask();
16
+ taskManager = new TaskManager(mockTaskProvider);
17
+ });
18
+
19
+ describe('startTask', () => {
20
+ it('should start a pending task', async () => {
21
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(mockTask);
22
+
23
+ const updatedTask = await taskManager.startTask('task-001');
24
+
25
+ expect(mockTaskProvider.findTask).toHaveBeenCalledWith('task-001');
26
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
27
+ expect.objectContaining({ status: 'in-progress' }),
28
+ );
29
+ expect(updatedTask.status).toBe('in-progress');
30
+ });
31
+
32
+ it('should throw an error if task is not found', async () => {
33
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
34
+ await expect(taskManager.startTask('not-found')).rejects.toThrow(
35
+ "Task with ID 'not-found' not found.",
36
+ );
37
+ });
38
+
39
+ it('should throw an error if task is already in progress', async () => {
40
+ const inProgressTask = { ...mockTask, status: 'in-progress' as const };
41
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
42
+
43
+ await expect(taskManager.startTask('task-001')).rejects.toThrow(
44
+ "Task 'task-001' is already in progress.",
45
+ );
46
+ });
47
+ });
48
+
49
+ describe('finishTask', () => {
50
+ it('should finish an in-progress task', async () => {
51
+ const inProgressTask = { ...mockTask, status: 'in-progress' as const };
52
+ (mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
53
+
54
+ const updatedTask = await taskManager.finishTask('task-001');
55
+
56
+ expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
57
+ expect.objectContaining({ status: 'done' }),
58
+ );
59
+ expect(updatedTask.status).toBe('done');
60
+ });
61
+ });
62
+
63
+ describe('createTask', () => {
64
+ it('should delegate task creation to provider', async () => {
65
+ const createOptions = { title: 'New Task', type: 'feat' as const };
66
+ const createResult = {
67
+ task: mockTask,
68
+ taskId: '001',
69
+ filePath: '/tasks/task-001.md',
70
+ };
71
+
72
+ (mockTaskProvider.createTask as Mock).mockResolvedValue(createResult);
73
+
74
+ const result = await taskManager.createTask(createOptions);
75
+
76
+ expect(mockTaskProvider.createTask).toHaveBeenCalledWith(createOptions);
77
+ expect(result).toEqual(createResult);
78
+ });
79
+
80
+ it('should pass through description and assignee to provider', async () => {
81
+ const createOptions = {
82
+ title: 'New Task',
83
+ type: 'fix' as const,
84
+ description: 'Fix something',
85
+ assignee: 'john-doe',
86
+ };
87
+ const createResult = {
88
+ task: mockTask,
89
+ taskId: '002',
90
+ filePath: '/tasks/task-002.md',
91
+ };
92
+
93
+ (mockTaskProvider.createTask as Mock).mockResolvedValue(createResult);
94
+
95
+ const result = await taskManager.createTask(createOptions);
96
+
97
+ expect(mockTaskProvider.createTask).toHaveBeenCalledWith(createOptions);
98
+ expect(result.taskId).toBe('002');
99
+ });
100
+ });
101
+
102
+ describe('lint', () => {
103
+ it('should delegate lint without fix to provider', async () => {
104
+ const lintResult = { errors: 0, warnings: 2, details: [] };
105
+ (mockTaskProvider.lint as Mock).mockResolvedValue(lintResult);
106
+
107
+ const result = await taskManager.lint();
108
+
109
+ expect(mockTaskProvider.lint).toHaveBeenCalledWith(undefined);
110
+ expect(result).toEqual(lintResult);
111
+ });
112
+
113
+ it('should delegate lint with fix=true to provider', async () => {
114
+ const lintResult = { errors: 0, warnings: 0, details: [] };
115
+ (mockTaskProvider.lint as Mock).mockResolvedValue(lintResult);
116
+
117
+ const result = await taskManager.lint(true);
118
+
119
+ expect(mockTaskProvider.lint).toHaveBeenCalledWith(true);
120
+ expect(result).toEqual(lintResult);
121
+ });
122
+
123
+ it('should delegate lint with fix=false to provider', async () => {
124
+ const lintResult = { errors: 1, warnings: 2, details: [] };
125
+ (mockTaskProvider.lint as Mock).mockResolvedValue(lintResult);
126
+
127
+ const result = await taskManager.lint(false);
128
+
129
+ expect(mockTaskProvider.lint).toHaveBeenCalledWith(false);
130
+ expect(result).toEqual(lintResult);
131
+ });
132
+ });
133
+ });
@@ -0,0 +1,54 @@
1
+ import type {
2
+ CreateTaskOptions,
3
+ CreateTaskResult,
4
+ ITaskManager,
5
+ ITaskProvider,
6
+ LintResult,
7
+ TaskFile,
8
+ } from './task-manager.types';
9
+
10
+ export class TaskManager implements ITaskManager {
11
+ constructor(private taskProvider: ITaskProvider) {}
12
+
13
+ async startTask(taskId: string): Promise<TaskFile> {
14
+ const task = await this.taskProvider.findTask(taskId);
15
+
16
+ if (!task) {
17
+ throw new Error(`Task with ID '${taskId}' not found.`);
18
+ }
19
+
20
+ if (task.status === 'in-progress') {
21
+ throw new Error(`Task '${taskId}' is already in progress.`);
22
+ }
23
+
24
+ if (task.status === 'done') {
25
+ throw new Error(`Task '${taskId}' is already done.`);
26
+ }
27
+
28
+ const updatedTask: TaskFile = { ...task, status: 'in-progress' };
29
+ await this.taskProvider.updateTask(updatedTask);
30
+
31
+ return updatedTask;
32
+ }
33
+
34
+ async finishTask(taskId: string): Promise<TaskFile> {
35
+ const task = await this.taskProvider.findTask(taskId);
36
+
37
+ if (!task) {
38
+ throw new Error(`Task with ID '${taskId}' not found.`);
39
+ }
40
+
41
+ const updatedTask: TaskFile = { ...task, status: 'done' };
42
+ await this.taskProvider.updateTask(updatedTask);
43
+
44
+ return updatedTask;
45
+ }
46
+
47
+ async createTask(options: CreateTaskOptions): Promise<CreateTaskResult> {
48
+ return this.taskProvider.createTask(options);
49
+ }
50
+
51
+ async lint(fix?: boolean): Promise<LintResult> {
52
+ return this.taskProvider.lint(fix);
53
+ }
54
+ }
@@ -0,0 +1,167 @@
1
+ import type { Task, TaskType, User } from '@opentask/taskin-types';
2
+
3
+ /**
4
+ * Options for creating a new task
5
+ * @public
6
+ */
7
+ export interface CreateTaskOptions {
8
+ /** Task title */
9
+ title: string;
10
+ /** Task type (feat, fix, chore, etc.) */
11
+ type: TaskType;
12
+ /** Optional task description */
13
+ description?: string;
14
+ /** Optional assignee user ID or name */
15
+ assignee?: string;
16
+ }
17
+
18
+ /**
19
+ * Result of creating a new task
20
+ * @public
21
+ */
22
+ export interface CreateTaskResult {
23
+ /** The created task */
24
+ task: TaskFile;
25
+ /** The generated task ID */
26
+ taskId: string;
27
+ /** The file path where the task was created */
28
+ filePath: string;
29
+ }
30
+
31
+ /**
32
+ * Severity level for validation issues
33
+ * @public
34
+ */
35
+ export type ValidationSeverity = 'error' | 'warning' | 'info';
36
+
37
+ /**
38
+ * A validation error or warning found during linting
39
+ * @public
40
+ */
41
+ export interface ValidationIssue {
42
+ /** The file or task that has the issue */
43
+ file: string;
44
+ /** Optional line number where the issue occurs */
45
+ line?: number;
46
+ /** Human-readable description of the issue */
47
+ message: string;
48
+ /** Severity level of the issue */
49
+ severity: ValidationSeverity;
50
+ /** Optional suggestion for fixing the issue */
51
+ suggestion?: string;
52
+ }
53
+
54
+ /**
55
+ * Result of a lint operation
56
+ * @public
57
+ */
58
+ export interface LintResult {
59
+ /** Whether the lint passed without errors */
60
+ valid: boolean;
61
+ /** List of validation issues found */
62
+ issues: ValidationIssue[];
63
+ /** Number of errors found */
64
+ errorCount: number;
65
+ /** Number of warnings found */
66
+ warningCount: number;
67
+ /** Number of info messages */
68
+ infoCount: number;
69
+ }
70
+
71
+ /**
72
+ * A task with additional file system metadata.
73
+ * Extends the base Task type with content and path information.
74
+ * @public
75
+ */
76
+ export type TaskFile = Task & {
77
+ /** The raw markdown content of the task file */
78
+ content: string;
79
+ /** Absolute or relative path to the task file */
80
+ filePath: string;
81
+ /** The type of work this task represents */
82
+ type: TaskType;
83
+ /** Optional user assigned to this task */
84
+ assignee?: User;
85
+ };
86
+
87
+ /**
88
+ * Interface for task storage providers.
89
+ * Implementations handle reading and writing tasks from different sources
90
+ * (e.g., file system, database, API).
91
+ * @public
92
+ */
93
+ export interface ITaskProvider {
94
+ /**
95
+ * Find a specific task by its ID.
96
+ * @param taskId - The unique identifier of the task
97
+ * @returns The task if found, undefined otherwise
98
+ */
99
+ findTask(taskId: string): Promise<TaskFile | undefined>;
100
+
101
+ /**
102
+ * Retrieve all tasks from the provider.
103
+ * @returns Array of all tasks
104
+ */
105
+ getAllTasks(): Promise<TaskFile[]>;
106
+
107
+ /**
108
+ * Update an existing task.
109
+ * @param task - The task with updated information
110
+ */
111
+ updateTask(task: TaskFile): Promise<void>;
112
+
113
+ /**
114
+ * Create a new task.
115
+ * @param options - Options for creating the task
116
+ * @returns The created task information
117
+ */
118
+ createTask(options: CreateTaskOptions): Promise<CreateTaskResult>;
119
+
120
+ /**
121
+ * Validate all tasks managed by this provider.
122
+ * Each provider knows its own format and validation rules.
123
+ * @param fix - If true, attempt to automatically fix validation issues
124
+ * @returns The lint result with any validation issues found
125
+ */
126
+ lint(fix?: boolean): Promise<LintResult>;
127
+ }
128
+
129
+ /**
130
+ * Interface for task management operations.
131
+ * Provides high-level methods for managing task workflow and state transitions.
132
+ * @public
133
+ */
134
+ export interface ITaskManager {
135
+ /**
136
+ * Mark a task as finished.
137
+ * Transitions the task to 'done' status.
138
+ * @param taskId - The unique identifier of the task
139
+ * @returns The updated task
140
+ * @throws Error if task is not found
141
+ */
142
+ finishTask(taskId: string): Promise<TaskFile>;
143
+
144
+ /**
145
+ * Start working on a task.
146
+ * Transitions the task to 'in-progress' status.
147
+ * @param taskId - The unique identifier of the task
148
+ * @returns The updated task
149
+ * @throws Error if task is not found, already in progress, or already done
150
+ */
151
+ startTask(taskId: string): Promise<TaskFile>;
152
+
153
+ /**
154
+ * Create a new task.
155
+ * @param options - Options for creating the task
156
+ * @returns The created task information
157
+ */
158
+ createTask(options: CreateTaskOptions): Promise<CreateTaskResult>;
159
+
160
+ /**
161
+ * Validate all tasks in the system.
162
+ * Delegates to the underlying provider's lint implementation.
163
+ * @param fix - If true, attempt to automatically fix validation issues
164
+ * @returns The lint result with any validation issues found
165
+ */
166
+ lint(fix?: boolean): Promise<LintResult>;
167
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "module": "ESNext",
5
+ "outDir": "./dist",
6
+ "rootDir": "./src"
7
+ },
8
+ "extends": "../../tsconfig.base.json",
9
+ "include": ["src/**/*"],
10
+ "references": [{ "path": "../types-ts" }]
11
+ }