@opentask/taskin-task-manager 2.0.1 → 3.0.0
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.
- package/CHANGELOG.md +237 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/metrics.types.d.ts +64 -73
- package/dist/provider-stats.test.js.map +1 -1
- package/dist/task-manager.d.ts +23 -7
- package/dist/task-manager.js +31 -3
- package/dist/task-manager.js.map +1 -1
- package/dist/task-manager.mock.d.ts +3 -4
- package/dist/task-manager.mock.js +2 -3
- package/dist/task-manager.mock.js.map +1 -1
- package/dist/task-manager.test.js +58 -17
- package/dist/task-manager.test.js.map +1 -1
- package/dist/task-manager.types.d.ts +49 -37
- package/dist/user-registry.contract.d.ts +13 -0
- package/dist/user-registry.contract.js +130 -0
- package/dist/user-registry.contract.js.map +1 -0
- package/dist/user-registry.types.d.ts +51 -0
- package/dist/user-registry.types.js +2 -0
- package/dist/user-registry.types.js.map +1 -0
- package/package.json +17 -9
- package/src/index.ts +3 -3
- package/src/metrics.types.ts +2 -11
- package/src/provider-stats.test.ts +6 -3
- package/src/task-manager.mock.ts +4 -6
- package/src/task-manager.test.ts +83 -34
- package/src/task-manager.ts +44 -10
- package/src/task-manager.types.ts +56 -38
- package/src/user-registry.contract.ts +151 -0
- package/src/user-registry.types.ts +55 -0
- package/tsconfig.json +1 -0
- package/.turbo/turbo-build.log +0 -5
- package/.turbo/turbo-format.log +0 -18
- package/.turbo/turbo-install.log +0 -6
- package/.turbo/turbo-lint$colon$fix.log +0 -12
- package/.turbo/turbo-lint.log +0 -13
- package/.turbo/turbo-test.log +0 -15
- package/.turbo/turbo-typecheck.log +0 -5
- package/eslint.config.js +0 -8
- package/tsconfig.tsbuildinfo +0 -1
package/src/metrics.types.ts
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
StatsQuery,
|
|
3
|
-
TaskStats,
|
|
4
|
-
TeamStats,
|
|
5
|
-
UserStats,
|
|
6
|
-
} from '@opentask/taskin-types';
|
|
1
|
+
import type { StatsQuery, TaskStats, TeamStats, UserStats } from '@opentask/taskin-types';
|
|
7
2
|
|
|
8
3
|
/**
|
|
9
4
|
* Manages metrics and statistics for users, teams, and tasks.
|
|
@@ -89,8 +84,4 @@ export interface IMetricsManager {
|
|
|
89
84
|
getTaskMetrics(taskId: string, query?: StatsQuery): Promise<TaskStats>;
|
|
90
85
|
}
|
|
91
86
|
|
|
92
|
-
export type {
|
|
93
|
-
TaskStats as TaskMetrics,
|
|
94
|
-
TeamStats as TeamMetrics,
|
|
95
|
-
UserStats as UserMetrics,
|
|
96
|
-
};
|
|
87
|
+
export type { TaskStats as TaskMetrics, TeamStats as TeamMetrics, UserStats as UserMetrics };
|
|
@@ -4,9 +4,12 @@ import type { IMetricsManager } from './metrics.types';
|
|
|
4
4
|
describe('IMetricsManager contract', () => {
|
|
5
5
|
it('accepts an object implementing IMetricsManager', async () => {
|
|
6
6
|
const metrics: IMetricsManager = {
|
|
7
|
-
getUserMetrics: async (userId: string) =>
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
getUserMetrics: async (userId: string) =>
|
|
8
|
+
({ userId }) as unknown as Awaited<ReturnType<IMetricsManager['getUserMetrics']>>,
|
|
9
|
+
getTeamMetrics: async (teamId: string) =>
|
|
10
|
+
({ teamId }) as unknown as Awaited<ReturnType<IMetricsManager['getTeamMetrics']>>,
|
|
11
|
+
getTaskMetrics: async (taskId: string) =>
|
|
12
|
+
({ taskId }) as unknown as Awaited<ReturnType<IMetricsManager['getTaskMetrics']>>,
|
|
10
13
|
};
|
|
11
14
|
|
|
12
15
|
expect(typeof metrics.getUserMetrics).toBe('function');
|
package/src/task-manager.mock.ts
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { parseTaskId, type Task } from '@opentask/taskin-types';
|
|
2
2
|
import { vi } from 'vitest';
|
|
3
|
-
import type { ITaskProvider
|
|
3
|
+
import type { ITaskProvider } from './task-manager.types';
|
|
4
4
|
|
|
5
|
-
export const createMockTask = (overrides?: Partial<
|
|
6
|
-
content: '# Task 001 - Implement feature',
|
|
5
|
+
export const createMockTask = (overrides?: Partial<Task>): Task => ({
|
|
7
6
|
createdAt: new Date().toISOString(),
|
|
8
7
|
description: 'A test feature',
|
|
9
|
-
|
|
10
|
-
id: 'task-001' satisfies string as TaskId,
|
|
8
|
+
id: parseTaskId('001'),
|
|
11
9
|
status: 'pending',
|
|
12
10
|
title: 'Implement feature',
|
|
13
11
|
type: 'feat',
|
package/src/task-manager.test.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { parseTaskId, type Task } from '@opentask/taskin-types';
|
|
1
2
|
import type { Mock } from 'vitest';
|
|
2
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { TaskManager } from './task-manager';
|
|
4
5
|
import { createMockTask, createMockTaskProvider } from './task-manager.mock';
|
|
5
|
-
import type { ITaskProvider
|
|
6
|
+
import type { ITaskProvider } from './task-manager.types';
|
|
6
7
|
|
|
7
8
|
describe('TaskManager', () => {
|
|
8
9
|
let taskManager: TaskManager;
|
|
9
10
|
let mockTaskProvider: ITaskProvider;
|
|
10
|
-
let mockTask:
|
|
11
|
+
let mockTask: Task;
|
|
11
12
|
|
|
12
13
|
beforeEach(() => {
|
|
13
14
|
vi.clearAllMocks();
|
|
@@ -20,30 +21,58 @@ describe('TaskManager', () => {
|
|
|
20
21
|
it('should start a pending task', async () => {
|
|
21
22
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(mockTask);
|
|
22
23
|
|
|
23
|
-
const updatedTask = await taskManager.startTask('
|
|
24
|
+
const updatedTask = await taskManager.startTask(parseTaskId('001'));
|
|
24
25
|
|
|
25
|
-
expect(mockTaskProvider.findTask).toHaveBeenCalledWith('
|
|
26
|
-
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
|
|
27
|
-
expect.objectContaining({ status: 'in-progress' }),
|
|
28
|
-
);
|
|
26
|
+
expect(mockTaskProvider.findTask).toHaveBeenCalledWith('001');
|
|
27
|
+
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'in-progress' }));
|
|
29
28
|
expect(updatedTask.status).toBe('in-progress');
|
|
30
29
|
});
|
|
31
30
|
|
|
32
31
|
it('should throw an error if task is not found', async () => {
|
|
33
32
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
|
|
34
|
-
await expect(taskManager.startTask('
|
|
35
|
-
"Task with ID 'not-found' not found.",
|
|
36
|
-
);
|
|
33
|
+
await expect(taskManager.startTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
|
|
37
34
|
});
|
|
38
35
|
|
|
39
36
|
it('should throw an error if task is already in progress', async () => {
|
|
40
37
|
const inProgressTask = { ...mockTask, status: 'in-progress' as const };
|
|
41
38
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
|
|
42
39
|
|
|
43
|
-
await expect(taskManager.startTask('
|
|
44
|
-
|
|
40
|
+
await expect(taskManager.startTask(parseTaskId('001'))).rejects.toThrow("Task '001' is already in progress.");
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('pauseTask', () => {
|
|
45
|
+
it('should pause an in-progress task', async () => {
|
|
46
|
+
const inProgressTask = { ...mockTask, status: 'in-progress' as const };
|
|
47
|
+
(mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
|
|
48
|
+
|
|
49
|
+
const updatedTask = await taskManager.pauseTask(parseTaskId('001'));
|
|
50
|
+
|
|
51
|
+
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'paused' }));
|
|
52
|
+
expect(updatedTask.status).toBe('paused');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should throw an error if task is not found', async () => {
|
|
56
|
+
(mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
|
|
57
|
+
|
|
58
|
+
await expect(taskManager.pauseTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should throw an error if task is not in-progress', async () => {
|
|
62
|
+
(mockTaskProvider.findTask as Mock).mockResolvedValue({ ...mockTask, status: 'pending' as const });
|
|
63
|
+
|
|
64
|
+
await expect(taskManager.pauseTask(parseTaskId('001'))).rejects.toThrow(
|
|
65
|
+
"Task '001' must be in 'in-progress' status to be paused",
|
|
45
66
|
);
|
|
46
67
|
});
|
|
68
|
+
|
|
69
|
+
it('should let startTask resume a paused task', async () => {
|
|
70
|
+
(mockTaskProvider.findTask as Mock).mockResolvedValue({ ...mockTask, status: 'paused' as const });
|
|
71
|
+
|
|
72
|
+
const updatedTask = await taskManager.startTask(parseTaskId('001'));
|
|
73
|
+
|
|
74
|
+
expect(updatedTask.status).toBe('in-progress');
|
|
75
|
+
});
|
|
47
76
|
});
|
|
48
77
|
|
|
49
78
|
describe('finishTask', () => {
|
|
@@ -51,11 +80,9 @@ describe('TaskManager', () => {
|
|
|
51
80
|
const inProgressTask = { ...mockTask, status: 'in-progress' as const };
|
|
52
81
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
|
|
53
82
|
|
|
54
|
-
const updatedTask = await taskManager.finishTask('
|
|
83
|
+
const updatedTask = await taskManager.finishTask(parseTaskId('001'));
|
|
55
84
|
|
|
56
|
-
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
|
|
57
|
-
expect.objectContaining({ status: 'done' }),
|
|
58
|
-
);
|
|
85
|
+
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'done' }));
|
|
59
86
|
expect(updatedTask.status).toBe('done');
|
|
60
87
|
});
|
|
61
88
|
});
|
|
@@ -65,29 +92,25 @@ describe('TaskManager', () => {
|
|
|
65
92
|
const inProgressTask = { ...mockTask, status: 'in-progress' as const };
|
|
66
93
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(inProgressTask);
|
|
67
94
|
|
|
68
|
-
const updatedTask = await taskManager.reviewTask('
|
|
95
|
+
const updatedTask = await taskManager.reviewTask(parseTaskId('001'));
|
|
69
96
|
|
|
70
|
-
expect(mockTaskProvider.findTask).toHaveBeenCalledWith('
|
|
71
|
-
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(
|
|
72
|
-
expect.objectContaining({ status: 'in-review' }),
|
|
73
|
-
);
|
|
97
|
+
expect(mockTaskProvider.findTask).toHaveBeenCalledWith('001');
|
|
98
|
+
expect(mockTaskProvider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ status: 'in-review' }));
|
|
74
99
|
expect(updatedTask.status).toBe('in-review');
|
|
75
100
|
});
|
|
76
101
|
|
|
77
102
|
it('should throw an error if task is not found', async () => {
|
|
78
103
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(undefined);
|
|
79
104
|
|
|
80
|
-
await expect(taskManager.reviewTask('
|
|
81
|
-
"Task with ID 'not-found' not found.",
|
|
82
|
-
);
|
|
105
|
+
await expect(taskManager.reviewTask(parseTaskId('999'))).rejects.toThrow("Task with ID '999' not found.");
|
|
83
106
|
});
|
|
84
107
|
|
|
85
108
|
it('should throw an error if task is not in-progress', async () => {
|
|
86
109
|
const pendingTask = { ...mockTask, status: 'pending' as const };
|
|
87
110
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(pendingTask);
|
|
88
111
|
|
|
89
|
-
await expect(taskManager.reviewTask('
|
|
90
|
-
"Task '
|
|
112
|
+
await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
|
|
113
|
+
"Task '001' must be in 'in-progress' status to be reviewed",
|
|
91
114
|
);
|
|
92
115
|
});
|
|
93
116
|
|
|
@@ -95,8 +118,8 @@ describe('TaskManager', () => {
|
|
|
95
118
|
const doneTask = { ...mockTask, status: 'done' as const };
|
|
96
119
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(doneTask);
|
|
97
120
|
|
|
98
|
-
await expect(taskManager.reviewTask('
|
|
99
|
-
"Task '
|
|
121
|
+
await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
|
|
122
|
+
"Task '001' must be in 'in-progress' status to be reviewed",
|
|
100
123
|
);
|
|
101
124
|
});
|
|
102
125
|
|
|
@@ -104,18 +127,42 @@ describe('TaskManager', () => {
|
|
|
104
127
|
const reviewTask = { ...mockTask, status: 'in-review' as const };
|
|
105
128
|
(mockTaskProvider.findTask as Mock).mockResolvedValue(reviewTask);
|
|
106
129
|
|
|
107
|
-
await expect(taskManager.reviewTask('
|
|
108
|
-
"Task '
|
|
130
|
+
await expect(taskManager.reviewTask(parseTaskId('001'))).rejects.toThrow(
|
|
131
|
+
"Task '001' must be in 'in-progress' status to be reviewed",
|
|
109
132
|
);
|
|
110
133
|
});
|
|
111
134
|
});
|
|
112
135
|
|
|
136
|
+
describe('provider-specific fields', () => {
|
|
137
|
+
// The manager is generic over the provider's task shape, so extra fields a
|
|
138
|
+
// provider carries (filePath/content for the file system, an issue number
|
|
139
|
+
// for a tracker) must survive a status transition untouched.
|
|
140
|
+
type ProviderTask = Task & { filePath: string; content: string };
|
|
141
|
+
|
|
142
|
+
it('should preserve provider fields through a transition', async () => {
|
|
143
|
+
const providerTask: ProviderTask = {
|
|
144
|
+
...createMockTask(),
|
|
145
|
+
filePath: '/tasks/task-001.md',
|
|
146
|
+
content: '# Task 001',
|
|
147
|
+
};
|
|
148
|
+
const provider = createMockTaskProvider() as unknown as ITaskProvider<ProviderTask>;
|
|
149
|
+
(provider.findTask as Mock).mockResolvedValue(providerTask);
|
|
150
|
+
|
|
151
|
+
const manager = new TaskManager(provider);
|
|
152
|
+
const updated = await manager.startTask(parseTaskId('001'));
|
|
153
|
+
|
|
154
|
+
expect(updated.filePath).toBe('/tasks/task-001.md');
|
|
155
|
+
expect(updated.content).toBe('# Task 001');
|
|
156
|
+
expect(updated.status).toBe('in-progress');
|
|
157
|
+
expect(provider.updateTask).toHaveBeenCalledWith(expect.objectContaining({ filePath: '/tasks/task-001.md' }));
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
113
161
|
describe('createTask', () => {
|
|
114
162
|
it('should delegate task creation to provider', async () => {
|
|
115
163
|
const createOptions = { title: 'New Task', type: 'feat' as const };
|
|
116
164
|
const createResult = {
|
|
117
165
|
task: mockTask,
|
|
118
|
-
taskId: '001',
|
|
119
166
|
filePath: '/tasks/task-001.md',
|
|
120
167
|
};
|
|
121
168
|
|
|
@@ -134,9 +181,11 @@ describe('TaskManager', () => {
|
|
|
134
181
|
description: 'Fix something',
|
|
135
182
|
assignee: 'john-doe',
|
|
136
183
|
};
|
|
184
|
+
// Antes o assert batia no campo `taskId` do result, que dizia '002'
|
|
185
|
+
// enquanto o `task` dizia outro id. Com um campo so, o teste tem que
|
|
186
|
+
// devolver a task que ele afirma ter criado.
|
|
137
187
|
const createResult = {
|
|
138
|
-
task:
|
|
139
|
-
taskId: '002',
|
|
188
|
+
task: createMockTask({ id: parseTaskId('002') }),
|
|
140
189
|
filePath: '/tasks/task-002.md',
|
|
141
190
|
};
|
|
142
191
|
|
|
@@ -145,7 +194,7 @@ describe('TaskManager', () => {
|
|
|
145
194
|
const result = await taskManager.createTask(createOptions);
|
|
146
195
|
|
|
147
196
|
expect(mockTaskProvider.createTask).toHaveBeenCalledWith(createOptions);
|
|
148
|
-
expect(result.
|
|
197
|
+
expect(result.task.id).toBe('002');
|
|
149
198
|
});
|
|
150
199
|
});
|
|
151
200
|
|
package/src/task-manager.ts
CHANGED
|
@@ -1,16 +1,33 @@
|
|
|
1
|
+
import type { Task, TaskId, TaskStatus } from '@opentask/taskin-types';
|
|
1
2
|
import type {
|
|
2
3
|
CreateTaskOptions,
|
|
3
4
|
CreateTaskResult,
|
|
4
5
|
ITaskManager,
|
|
5
6
|
ITaskProvider,
|
|
6
7
|
LintResult,
|
|
7
|
-
TaskFile,
|
|
8
8
|
} from './task-manager.types';
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Orchestrates task state transitions on top of any {@link ITaskProvider}.
|
|
12
|
+
*
|
|
13
|
+
* `TTask` is inferred from the provider passed to the constructor, so callers
|
|
14
|
+
* get their provider's task shape back without this class ever naming it.
|
|
15
|
+
*/
|
|
16
|
+
export class TaskManager<TTask extends Task = Task> implements ITaskManager<TTask> {
|
|
17
|
+
constructor(private taskProvider: ITaskProvider<TTask>) {}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns a copy of `task` with a new status.
|
|
21
|
+
*
|
|
22
|
+
* TypeScript cannot prove that spreading a generic yields that same generic,
|
|
23
|
+
* so the assertion is required. It is sound here: `status` is a known key of
|
|
24
|
+
* `Task`, and every other field is carried over untouched.
|
|
25
|
+
*/
|
|
26
|
+
private withStatus(task: TTask, status: TaskStatus): TTask {
|
|
27
|
+
return { ...task, status } as TTask;
|
|
28
|
+
}
|
|
12
29
|
|
|
13
|
-
async startTask(taskId:
|
|
30
|
+
async startTask(taskId: TaskId): Promise<TTask> {
|
|
14
31
|
const task = await this.taskProvider.findTask(taskId);
|
|
15
32
|
|
|
16
33
|
if (!task) {
|
|
@@ -25,26 +42,43 @@ export class TaskManager implements ITaskManager {
|
|
|
25
42
|
throw new Error(`Task '${taskId}' is already done.`);
|
|
26
43
|
}
|
|
27
44
|
|
|
28
|
-
const updatedTask
|
|
45
|
+
const updatedTask = this.withStatus(task, 'in-progress');
|
|
46
|
+
await this.taskProvider.updateTask(updatedTask);
|
|
47
|
+
|
|
48
|
+
return updatedTask;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async pauseTask(taskId: TaskId): Promise<TTask> {
|
|
52
|
+
const task = await this.taskProvider.findTask(taskId);
|
|
53
|
+
|
|
54
|
+
if (!task) {
|
|
55
|
+
throw new Error(`Task with ID '${taskId}' not found.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (task.status !== 'in-progress') {
|
|
59
|
+
throw new Error(`Task '${taskId}' must be in 'in-progress' status to be paused. Current status: ${task.status}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const updatedTask = this.withStatus(task, 'paused');
|
|
29
63
|
await this.taskProvider.updateTask(updatedTask);
|
|
30
64
|
|
|
31
65
|
return updatedTask;
|
|
32
66
|
}
|
|
33
67
|
|
|
34
|
-
async finishTask(taskId:
|
|
68
|
+
async finishTask(taskId: TaskId): Promise<TTask> {
|
|
35
69
|
const task = await this.taskProvider.findTask(taskId);
|
|
36
70
|
|
|
37
71
|
if (!task) {
|
|
38
72
|
throw new Error(`Task with ID '${taskId}' not found.`);
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
const updatedTask
|
|
75
|
+
const updatedTask = this.withStatus(task, 'done');
|
|
42
76
|
await this.taskProvider.updateTask(updatedTask);
|
|
43
77
|
|
|
44
78
|
return updatedTask;
|
|
45
79
|
}
|
|
46
80
|
|
|
47
|
-
async reviewTask(taskId:
|
|
81
|
+
async reviewTask(taskId: TaskId): Promise<TTask> {
|
|
48
82
|
const task = await this.taskProvider.findTask(taskId);
|
|
49
83
|
|
|
50
84
|
if (!task) {
|
|
@@ -57,13 +91,13 @@ export class TaskManager implements ITaskManager {
|
|
|
57
91
|
);
|
|
58
92
|
}
|
|
59
93
|
|
|
60
|
-
const updatedTask
|
|
94
|
+
const updatedTask = this.withStatus(task, 'in-review');
|
|
61
95
|
await this.taskProvider.updateTask(updatedTask);
|
|
62
96
|
|
|
63
97
|
return updatedTask;
|
|
64
98
|
}
|
|
65
99
|
|
|
66
|
-
async createTask(options: CreateTaskOptions): Promise<CreateTaskResult
|
|
100
|
+
async createTask(options: CreateTaskOptions): Promise<CreateTaskResult<TTask>> {
|
|
67
101
|
return this.taskProvider.createTask(options);
|
|
68
102
|
}
|
|
69
103
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Task,
|
|
1
|
+
import type { Task, TaskId, TaskType } from '@opentask/taskin-types';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for creating a new task
|
|
@@ -16,16 +16,18 @@ export interface CreateTaskOptions {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
* Result of creating a new task
|
|
19
|
+
* Result of creating a new task.
|
|
20
|
+
*
|
|
21
|
+
* Providers are free to return a wider object (e.g. the file system provider
|
|
22
|
+
* adds `filePath`); returning extra fields is allowed because the result is
|
|
23
|
+
* only ever consumed through this contract.
|
|
24
|
+
*
|
|
25
|
+
* @typeParam TTask - The task shape produced by the provider
|
|
20
26
|
* @public
|
|
21
27
|
*/
|
|
22
|
-
export interface CreateTaskResult {
|
|
28
|
+
export interface CreateTaskResult<TTask extends Task = Task> {
|
|
23
29
|
/** The created task */
|
|
24
|
-
task:
|
|
25
|
-
/** The generated task ID */
|
|
26
|
-
taskId: string;
|
|
27
|
-
/** The file path where the task was created */
|
|
28
|
-
filePath: string;
|
|
30
|
+
task: TTask;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
/**
|
|
@@ -68,60 +70,58 @@ export interface LintResult {
|
|
|
68
70
|
infoCount: number;
|
|
69
71
|
}
|
|
70
72
|
|
|
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
73
|
/**
|
|
88
74
|
* Interface for task storage providers.
|
|
89
75
|
* Implementations handle reading and writing tasks from different sources
|
|
90
|
-
* (e.g., file system,
|
|
76
|
+
* (e.g., file system, GitHub issues, Redmine).
|
|
77
|
+
*
|
|
78
|
+
* The task shape is a type parameter so that a provider can enrich `Task` with
|
|
79
|
+
* whatever its backing store requires — the file system provider carries
|
|
80
|
+
* `content`/`filePath`, a Redmine provider would carry its own fields — without
|
|
81
|
+
* that shape leaking into this package. Consumers that do not care about the
|
|
82
|
+
* extra fields can simply use the default and work with plain `Task`.
|
|
83
|
+
*
|
|
84
|
+
* @typeParam TTask - The task shape this provider reads and writes
|
|
91
85
|
* @public
|
|
92
86
|
*/
|
|
93
|
-
|
|
87
|
+
/*
|
|
88
|
+
* Os membros sao propriedades de funcao, nao metodos, de proposito: TypeScript
|
|
89
|
+
* trata metodos como bivariantes mesmo com `strictFunctionTypes`, e isso
|
|
90
|
+
* deixava `ITaskProvider<TaskFile>` ser atribuido a `ITaskProvider<Task>` — o
|
|
91
|
+
* que compila e depois quebra em `updateTask`, que le `task.filePath`.
|
|
92
|
+
*/
|
|
93
|
+
export interface ITaskProvider<TTask extends Task = Task> {
|
|
94
94
|
/**
|
|
95
95
|
* Initialize the provider, performing any necessary setup or loading.
|
|
96
96
|
* This may involve reading existing tasks, setting up connections, etc.
|
|
97
97
|
*/
|
|
98
|
-
initialize()
|
|
98
|
+
initialize: () => Promise<void>;
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
101
|
* Find a specific task by its ID.
|
|
102
102
|
* @param taskId - The unique identifier of the task
|
|
103
103
|
* @returns The task if found, undefined otherwise
|
|
104
104
|
*/
|
|
105
|
-
findTask(taskId:
|
|
105
|
+
findTask: (taskId: TaskId) => Promise<TTask | undefined>;
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Retrieve all tasks from the provider.
|
|
109
109
|
* @returns Array of all tasks
|
|
110
110
|
*/
|
|
111
|
-
getAllTasks()
|
|
111
|
+
getAllTasks: () => Promise<TTask[]>;
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
114
|
* Update an existing task.
|
|
115
115
|
* @param task - The task with updated information
|
|
116
116
|
*/
|
|
117
|
-
updateTask(task:
|
|
117
|
+
updateTask: (task: TTask) => Promise<void>;
|
|
118
118
|
|
|
119
119
|
/**
|
|
120
120
|
* Create a new task.
|
|
121
121
|
* @param options - Options for creating the task
|
|
122
122
|
* @returns The created task information
|
|
123
123
|
*/
|
|
124
|
-
createTask(options: CreateTaskOptions)
|
|
124
|
+
createTask: (options: CreateTaskOptions) => Promise<CreateTaskResult<TTask>>;
|
|
125
125
|
|
|
126
126
|
/**
|
|
127
127
|
* Validate all tasks managed by this provider.
|
|
@@ -129,15 +129,22 @@ export interface ITaskProvider {
|
|
|
129
129
|
* @param fix - If true, attempt to automatically fix validation issues
|
|
130
130
|
* @returns The lint result with any validation issues found
|
|
131
131
|
*/
|
|
132
|
-
lint(fix?: boolean)
|
|
132
|
+
lint: (fix?: boolean) => Promise<LintResult>;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
/**
|
|
136
136
|
* Interface for task management operations.
|
|
137
137
|
* Provides high-level methods for managing task workflow and state transitions.
|
|
138
|
+
*
|
|
139
|
+
* Mirrors the provider's task shape: a manager built on top of the file system
|
|
140
|
+
* provider hands back the provider's richer task, while a manager built on any
|
|
141
|
+
* other provider hands back that provider's shape. This package never needs to
|
|
142
|
+
* know which one it is.
|
|
143
|
+
*
|
|
144
|
+
* @typeParam TTask - The task shape produced by the underlying provider
|
|
138
145
|
* @public
|
|
139
146
|
*/
|
|
140
|
-
export interface ITaskManager {
|
|
147
|
+
export interface ITaskManager<TTask extends Task = Task> {
|
|
141
148
|
/**
|
|
142
149
|
* Mark a task as finished.
|
|
143
150
|
* Transitions the task to 'done' status.
|
|
@@ -145,7 +152,7 @@ export interface ITaskManager {
|
|
|
145
152
|
* @returns The updated task
|
|
146
153
|
* @throws Error if task is not found
|
|
147
154
|
*/
|
|
148
|
-
finishTask(taskId:
|
|
155
|
+
finishTask: (taskId: TaskId) => Promise<TTask>;
|
|
149
156
|
|
|
150
157
|
/**
|
|
151
158
|
* Mark a task as ready for review.
|
|
@@ -154,23 +161,34 @@ export interface ITaskManager {
|
|
|
154
161
|
* @returns The updated task
|
|
155
162
|
* @throws Error if task is not found or not in 'in-progress' status
|
|
156
163
|
*/
|
|
157
|
-
reviewTask(taskId:
|
|
164
|
+
reviewTask: (taskId: TaskId) => Promise<TTask>;
|
|
158
165
|
|
|
159
166
|
/**
|
|
160
167
|
* Start working on a task.
|
|
161
168
|
* Transitions the task to 'in-progress' status.
|
|
169
|
+
* Also used to resume a paused task.
|
|
162
170
|
* @param taskId - The unique identifier of the task
|
|
163
171
|
* @returns The updated task
|
|
164
172
|
* @throws Error if task is not found, already in progress, or already done
|
|
165
173
|
*/
|
|
166
|
-
startTask(taskId:
|
|
174
|
+
startTask: (taskId: TaskId) => Promise<TTask>;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Pause work on a task.
|
|
178
|
+
* Transitions the task from 'in-progress' to 'paused' status.
|
|
179
|
+
* Resume with {@link ITaskManager.startTask}.
|
|
180
|
+
* @param taskId - The unique identifier of the task
|
|
181
|
+
* @returns The updated task
|
|
182
|
+
* @throws Error if task is not found or not in 'in-progress' status
|
|
183
|
+
*/
|
|
184
|
+
pauseTask: (taskId: TaskId) => Promise<TTask>;
|
|
167
185
|
|
|
168
186
|
/**
|
|
169
187
|
* Create a new task.
|
|
170
188
|
* @param options - Options for creating the task
|
|
171
189
|
* @returns The created task information
|
|
172
190
|
*/
|
|
173
|
-
createTask(options: CreateTaskOptions)
|
|
191
|
+
createTask: (options: CreateTaskOptions) => Promise<CreateTaskResult<TTask>>;
|
|
174
192
|
|
|
175
193
|
/**
|
|
176
194
|
* Validate all tasks in the system.
|
|
@@ -178,5 +196,5 @@ export interface ITaskManager {
|
|
|
178
196
|
* @param fix - If true, attempt to automatically fix validation issues
|
|
179
197
|
* @returns The lint result with any validation issues found
|
|
180
198
|
*/
|
|
181
|
-
lint(fix?: boolean)
|
|
199
|
+
lint: (fix?: boolean) => Promise<LintResult>;
|
|
182
200
|
}
|