@opentask/taskin-task-manager 3.1.0 → 3.2.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.
Files changed (75) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/completion-blockers.test.d.ts +1 -0
  3. package/dist/completion-blockers.test.js +57 -0
  4. package/dist/completion-blockers.test.js.map +1 -0
  5. package/dist/filter-tasks/filter-criteria.d.ts +181 -0
  6. package/dist/filter-tasks/filter-criteria.js +115 -0
  7. package/dist/filter-tasks/filter-criteria.js.map +1 -0
  8. package/dist/filter-tasks/filter-criteria.test.d.ts +1 -0
  9. package/dist/filter-tasks/filter-criteria.test.js +86 -0
  10. package/dist/filter-tasks/filter-criteria.test.js.map +1 -0
  11. package/dist/filter-tasks/filter-tasks.js +17 -2
  12. package/dist/filter-tasks/filter-tasks.js.map +1 -1
  13. package/dist/filter-tasks/filter-tasks.test.js +38 -0
  14. package/dist/filter-tasks/filter-tasks.test.js.map +1 -1
  15. package/dist/filter-tasks/filter-tasks.types.d.ts +5 -23
  16. package/dist/filter-tasks/index.d.ts +3 -1
  17. package/dist/filter-tasks/index.js +1 -0
  18. package/dist/filter-tasks/index.js.map +1 -1
  19. package/dist/group-registry.contract.d.ts +18 -0
  20. package/dist/group-registry.contract.js +73 -0
  21. package/dist/group-registry.contract.js.map +1 -0
  22. package/dist/group-registry.types.d.ts +44 -0
  23. package/dist/group-registry.types.js +2 -0
  24. package/dist/group-registry.types.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/numerar-prioridade/index.d.ts +1 -0
  29. package/dist/numerar-prioridade/index.js +2 -0
  30. package/dist/numerar-prioridade/index.js.map +1 -0
  31. package/dist/numerar-prioridade/numerar-prioridade.d.ts +36 -0
  32. package/dist/numerar-prioridade/numerar-prioridade.js +75 -0
  33. package/dist/numerar-prioridade/numerar-prioridade.js.map +1 -0
  34. package/dist/numerar-prioridade/numerar-prioridade.test.d.ts +1 -0
  35. package/dist/numerar-prioridade/numerar-prioridade.test.js +55 -0
  36. package/dist/numerar-prioridade/numerar-prioridade.test.js.map +1 -0
  37. package/dist/ordenar-tarefas/index.d.ts +1 -0
  38. package/dist/ordenar-tarefas/index.js +2 -0
  39. package/dist/ordenar-tarefas/index.js.map +1 -0
  40. package/dist/ordenar-tarefas/ordenar-tarefas.d.ts +70 -0
  41. package/dist/ordenar-tarefas/ordenar-tarefas.js +79 -0
  42. package/dist/ordenar-tarefas/ordenar-tarefas.js.map +1 -0
  43. package/dist/ordenar-tarefas/ordenar-tarefas.test.d.ts +1 -0
  44. package/dist/ordenar-tarefas/ordenar-tarefas.test.js +70 -0
  45. package/dist/ordenar-tarefas/ordenar-tarefas.test.js.map +1 -0
  46. package/dist/task-manager.d.ts +26 -1
  47. package/dist/task-manager.js +48 -0
  48. package/dist/task-manager.js.map +1 -1
  49. package/dist/task-manager.types.d.ts +82 -0
  50. package/dist/testing.d.ts +8 -0
  51. package/dist/testing.js +9 -0
  52. package/dist/testing.js.map +1 -0
  53. package/dist/user-registry.contract.js +6 -6
  54. package/dist/user-registry.contract.js.map +1 -1
  55. package/package.json +7 -6
  56. package/src/completion-blockers.test.ts +77 -0
  57. package/src/filter-tasks/filter-criteria.test.ts +113 -0
  58. package/src/filter-tasks/filter-criteria.ts +175 -0
  59. package/src/filter-tasks/filter-tasks.test.ts +45 -0
  60. package/src/filter-tasks/filter-tasks.ts +17 -2
  61. package/src/filter-tasks/filter-tasks.types.ts +5 -23
  62. package/src/filter-tasks/index.ts +15 -1
  63. package/src/group-registry.contract.ts +99 -0
  64. package/src/group-registry.types.ts +47 -0
  65. package/src/index.ts +3 -0
  66. package/src/numerar-prioridade/index.ts +1 -0
  67. package/src/numerar-prioridade/numerar-prioridade.test.ts +69 -0
  68. package/src/numerar-prioridade/numerar-prioridade.ts +80 -0
  69. package/src/ordenar-tarefas/index.ts +1 -0
  70. package/src/ordenar-tarefas/ordenar-tarefas.test.ts +92 -0
  71. package/src/ordenar-tarefas/ordenar-tarefas.ts +135 -0
  72. package/src/task-manager.ts +59 -0
  73. package/src/task-manager.types.ts +83 -0
  74. package/src/testing.ts +8 -0
  75. package/src/user-registry.contract.ts +6 -6
@@ -0,0 +1,92 @@
1
+ import type { Task } from '@opentask/taskin-types';
2
+ import { parseGroupId, parseTaskId } from '@opentask/taskin-types';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { agruparTarefas, ordenarTarefas } from './ordenar-tarefas.js';
5
+
6
+ const t = (id: string, extra: Partial<Task> = {}): Task =>
7
+ ({ id: parseTaskId(id), title: `Tarefa ${id}`, status: 'pending', type: 'feat', ...extra }) as Task;
8
+
9
+ const ids = (ts: readonly Task[]) => ts.map((x) => String(x.id));
10
+
11
+ /**
12
+ * A ordenacao que o dashboard ja usava, agora onde as tres superficies alcancam.
13
+ *
14
+ * Ela vivia em `use-prioritization.ts`, dentro do pacote **Vue** — inalcancavel
15
+ * para a CLI e para o servidor MCP. O `taskin list` devolvia as tarefas na
16
+ * ordem em que o provider as achava, que na pratica e por id: a coluna de
17
+ * prioridade subia e descia sem padrao, e quem lia precisava reordenar de
18
+ * cabeca. Foi assim que um agente autonomo escolheu uma tarefa de prioridade 30
19
+ * tendo uma de 255 na mesma lista.
20
+ */
21
+ describe('ordenarTarefas', () => {
22
+ it('manual ordena por prioridade, com quem nao tem por ultimo', () => {
23
+ const tarefas = [t('001', { order: 30 }), t('002'), t('003', { order: 10 })];
24
+
25
+ expect(ids(ordenarTarefas(tarefas, 'manual'))).toEqual(['003', '001', '002']);
26
+ });
27
+
28
+ it('manual e estavel entre empatados', () => {
29
+ const tarefas = [t('001', { order: 10 }), t('002', { order: 10 }), t('003', { order: 10 })];
30
+
31
+ expect(ids(ordenarTarefas(tarefas, 'manual'))).toEqual(['001', '002', '003']);
32
+ });
33
+
34
+ it('difficulty ordena do mais facil ao mais dificil, e o inverso', () => {
35
+ const tarefas = [t('001', { difficulty: 5 }), t('002', { difficulty: 1 }), t('003', { difficulty: 3 })];
36
+
37
+ expect(ids(ordenarTarefas(tarefas, 'diff-asc'))).toEqual(['002', '003', '001']);
38
+ expect(ids(ordenarTarefas(tarefas, 'diff-desc'))).toEqual(['001', '003', '002']);
39
+ });
40
+
41
+ it('nao altera a lista recebida', () => {
42
+ const tarefas = [t('001', { order: 30 }), t('002', { order: 10 })];
43
+ ordenarTarefas(tarefas, 'manual');
44
+
45
+ expect(ids(tarefas)).toEqual(['001', '002']);
46
+ });
47
+ });
48
+
49
+ /**
50
+ * Agrupar e separado de ordenar, e de proposito.
51
+ *
52
+ * No dashboard as duas coisas vinham juntas porque a arvore era so para
53
+ * desenhar caixas. Na saida de maquina quem consome decide — e separar deixa a
54
+ * CLI ordenar sem agrupar quando ninguem pediu grupo.
55
+ */
56
+ describe('agruparTarefas', () => {
57
+ const g1 = parseGroupId('g-1');
58
+
59
+ it('junta membros do mesmo grupo, esteja onde estiver na ordem', () => {
60
+ const tarefas = [t('001', { groupId: g1 }), t('002'), t('003', { groupId: g1 })];
61
+
62
+ const nos = agruparTarefas(tarefas, { 'g-1': 'Sprint' });
63
+
64
+ expect(nos).toHaveLength(2);
65
+ expect(nos[0]).toMatchObject({ kind: 'group', groupId: 'g-1', groupName: 'Sprint', hidden: 0 });
66
+ expect(nos[0]?.kind === 'group' && ids(nos[0].tasks)).toEqual(['001', '003']);
67
+ });
68
+
69
+ /*
70
+ * O grupo parcial, decidido: mostra os membros que casam, e diz quantos o
71
+ * filtro escondeu. Nunca alarga o filtro para trazer o grupo inteiro.
72
+ */
73
+ it('diz quantos membros o filtro deixou de fora', () => {
74
+ const tarefas = [t('001', { groupId: g1 })];
75
+
76
+ const nos = agruparTarefas(tarefas, { 'g-1': 'Sprint' }, { 'g-1': 7 });
77
+
78
+ expect(nos[0]).toMatchObject({ kind: 'group', hidden: 6 });
79
+ });
80
+
81
+ it('tarefa sem grupo fica solta, na ordem em que veio', () => {
82
+ const nos = agruparTarefas([t('001'), t('002')], {});
83
+
84
+ expect(nos.map((n) => n.kind)).toEqual(['task', 'task']);
85
+ });
86
+
87
+ it('grupo sem nome no registro ainda aparece, pelo id', () => {
88
+ const nos = agruparTarefas([t('001', { groupId: g1 })], {});
89
+
90
+ expect(nos[0]).toMatchObject({ kind: 'group', groupId: 'g-1', groupName: undefined });
91
+ });
92
+ });
@@ -0,0 +1,135 @@
1
+ import type { Task } from '@opentask/taskin-types';
2
+
3
+ /**
4
+ * Como uma listagem pode ser ordenada.
5
+ *
6
+ * O mesmo vocabulario que o quadro de priorizacao do dashboard usa. Manter as
7
+ * grafias iguais entre as superficies vale mais que elegancia local: quem le a
8
+ * documentacao de uma reconhece a outra.
9
+ *
10
+ * @public
11
+ */
12
+ export type ModoDeOrdenacao = 'manual' | 'diff-asc' | 'diff-desc';
13
+
14
+ /**
15
+ * O que a ordenacao precisa saber, e nada alem disso.
16
+ *
17
+ * Exigir `Task` inteira deixaria a funcao inalcancavel para quem tem outra
18
+ * forma de tarefa — o quadro de priorizacao do dashboard, por exemplo, usa um
19
+ * modelo de tela proprio. Pedir so os dois campos que ela le e o que permite as
20
+ * duas superficies compartilharem a regra em vez de cada uma ter a sua copia.
21
+ *
22
+ * @public
23
+ */
24
+ export interface OrdenavelPorPrioridade {
25
+ readonly order?: number;
26
+ readonly difficulty?: number;
27
+ }
28
+
29
+ /** Um no da listagem: uma tarefa solta, ou um grupo com os membros que casaram. */
30
+ export type NoDaListagem<TTask extends Task = Task> =
31
+ | { readonly kind: 'task'; readonly task: TTask }
32
+ | {
33
+ readonly kind: 'group';
34
+ readonly groupId: string;
35
+ /** `undefined` quando o registro nao conhece o grupo. */
36
+ readonly groupName: string | undefined;
37
+ readonly tasks: readonly TTask[];
38
+ /** Quantos membros o filtro deixou de fora. Zero quando o grupo veio inteiro. */
39
+ readonly hidden: number;
40
+ };
41
+
42
+ /**
43
+ * Ordena uma listagem, sem alterar a lista recebida.
44
+ *
45
+ * Vivia dentro do pacote Vue do dashboard, inalcancavel para a CLI e para o
46
+ * servidor MCP — e por isso `taskin list` devolvia as tarefas na ordem em que o
47
+ * provider as achava, que na pratica e por id.
48
+ *
49
+ * `manual` usa o `order`, com quem nao tem por ultimo e estabilidade no resto.
50
+ * Os dois modos de dificuldade usam o `difficulty`.
51
+ *
52
+ * @public
53
+ */
54
+ export function ordenarTarefas<TTask extends OrdenavelPorPrioridade>(
55
+ tarefas: readonly TTask[],
56
+ modo: ModoDeOrdenacao = 'manual',
57
+ ): TTask[] {
58
+ const comIndice = tarefas.map((task, index) => ({ task, index }));
59
+
60
+ const campo = (t: TTask) => (modo === 'manual' ? t.order : t.difficulty);
61
+ const sinal = modo === 'diff-desc' ? -1 : 1;
62
+
63
+ comIndice.sort((a, b) => {
64
+ const va = campo(a.task);
65
+ const vb = campo(b.task);
66
+
67
+ // Quem nao tem valor vai para o fim, qualquer que seja a direcao.
68
+ if (va === undefined && vb === undefined) return a.index - b.index;
69
+ if (va === undefined) return 1;
70
+ if (vb === undefined) return -1;
71
+ if (va !== vb) return (va - vb) * sinal;
72
+
73
+ return a.index - b.index;
74
+ });
75
+
76
+ return comIndice.map(({ task }) => task);
77
+ }
78
+
79
+ /**
80
+ * Junta os membros de um grupo num no so, **por identidade**.
81
+ *
82
+ * Separado de {@link ordenarTarefas} de proposito. No dashboard as duas coisas
83
+ * vinham juntas porque a arvore existia para desenhar caixas; aqui quem consome
84
+ * decide se quer agrupar.
85
+ *
86
+ * Agrupa pelo `groupId`, e nao por adjacencia: membros separados por um filtro
87
+ * ou por uma ordenacao continuam sendo **um** grupo. Agrupar por adjacencia era
88
+ * o defeito da task-072, que partia o mesmo grupo em dois nos com o mesmo nome.
89
+ *
90
+ * @param tarefas - Ja ordenadas; a ordem dos grupos segue a do primeiro membro
91
+ * @param nomes - O que o registro de grupos sabe, por id
92
+ * @param totais - Quantos membros cada grupo tem **antes** do filtro. O que
93
+ * falta vira `hidden`, para o leitor saber que nao esta vendo o grupo inteiro
94
+ * em vez de o filtro se alargar sozinho e trazer o resto.
95
+ * @public
96
+ */
97
+ export function agruparTarefas<TTask extends Task>(
98
+ tarefas: readonly TTask[],
99
+ nomes: Readonly<Record<string, string>> = {},
100
+ totais: Readonly<Record<string, number>> = {},
101
+ ): NoDaListagem<TTask>[] {
102
+ const nos: NoDaListagem<TTask>[] = [];
103
+ const porGrupo = new Map<string, TTask[]>();
104
+
105
+ for (const task of tarefas) {
106
+ const id = task.groupId === undefined ? undefined : String(task.groupId);
107
+
108
+ if (id === undefined) {
109
+ nos.push({ kind: 'task', task });
110
+ continue;
111
+ }
112
+
113
+ const existente = porGrupo.get(id);
114
+ if (existente) {
115
+ existente.push(task);
116
+ continue;
117
+ }
118
+
119
+ const membros = [task];
120
+ porGrupo.set(id, membros);
121
+ nos.push({
122
+ kind: 'group',
123
+ groupId: id,
124
+ groupName: nomes[id],
125
+ tasks: membros,
126
+ hidden: 0,
127
+ });
128
+ }
129
+
130
+ return nos.map((no) =>
131
+ no.kind === 'group'
132
+ ? { ...no, hidden: Math.max(0, (totais[no.groupId] ?? no.tasks.length) - no.tasks.length) }
133
+ : no,
134
+ );
135
+ }
@@ -1,7 +1,10 @@
1
1
  import type { Task, TaskId, TaskStatus } from '@opentask/taskin-types';
2
+ import type { IGroupRegistry } from './group-registry.types';
3
+ import { numerarPrioridade } from './numerar-prioridade/index';
2
4
  import type {
3
5
  CreateTaskOptions,
4
6
  CreateTaskResult,
7
+ CriterioEmAberto,
5
8
  ITaskManager,
6
9
  ITaskProvider,
7
10
  LintResult,
@@ -76,6 +79,62 @@ export class TaskManager<TTask extends Task = Task> implements ITaskManager<TTas
76
79
  return await this.taskProvider.getAllTasks();
77
80
  }
78
81
 
82
+ /** Repassa o registro do provider, quando ele tem um. */
83
+ get groupRegistry(): IGroupRegistry | undefined {
84
+ return (this.taskProvider as { groupRegistry?: IGroupRegistry }).groupRegistry;
85
+ }
86
+
87
+ async prioritizeAll(options: { dryRun?: boolean } = {}): Promise<{
88
+ total: number;
89
+ withoutPriority: number;
90
+ changed: number;
91
+ }> {
92
+ const tarefas = await this.taskProvider.getAllTasks();
93
+
94
+ /*
95
+ * Quem ja tem numero define a ordem; quem nao tem entra depois, na sequencia
96
+ * em que o provider devolveu.
97
+ */
98
+ const ordenadas = [...tarefas].sort((a, b) => {
99
+ if (a.order === undefined && b.order === undefined) return 0;
100
+ if (a.order === undefined) return 1;
101
+ if (b.order === undefined) return -1;
102
+ return a.order - b.order;
103
+ });
104
+
105
+ const mudancas = numerarPrioridade(ordenadas);
106
+ const semNumero = tarefas.filter((t) => t.order === undefined).length;
107
+
108
+ if (!options.dryRun) {
109
+ for (const tarefa of mudancas) {
110
+ await this.taskProvider.updateTask(tarefa as TTask);
111
+ }
112
+ }
113
+
114
+ return { total: tarefas.length, withoutPriority: semNumero, changed: mudancas.length };
115
+ }
116
+
117
+ /**
118
+ * Conclui a tarefa e **relata** o que ficou em aberto.
119
+ *
120
+ * Avisa, e nao recusa. Fechar uma tarefa e um gesto que acontece uma vez,
121
+ * muitas vezes com pressa; recusar ali torna o comando fragil e ensina a
122
+ * contornar. O portao duro vive no `lint`, que roda em CI e quebra o build —
123
+ * aqui o papel e dizer, no momento em que a pessoa ainda esta olhando, o que
124
+ * ficou para tras.
125
+ *
126
+ * Um provider sem a capacidade conclui sem portao nenhum.
127
+ */
128
+ async finishTaskComRelato(taskId: TaskId): Promise<{ task: TTask; blockers: CriterioEmAberto[] }> {
129
+ const task = await this.taskProvider.findTask(taskId);
130
+ if (!task) throw new Error(`Task with ID '${taskId}' not found.`);
131
+
132
+ const blockers = (await this.taskProvider.getCompletionBlockers?.(task)) ?? [];
133
+ const atualizada = await this.finishTask(taskId);
134
+
135
+ return { task: atualizada, blockers };
136
+ }
137
+
79
138
  async finishTask(taskId: TaskId): Promise<TTask> {
80
139
  const task = await this.taskProvider.findTask(taskId);
81
140
 
@@ -1,4 +1,5 @@
1
1
  import type { Task, TaskId, TaskType } from '@opentask/taskin-types';
2
+ import type { IGroupRegistry } from './group-registry.types.js';
2
3
 
3
4
  /**
4
5
  * Options for creating a new task
@@ -40,6 +41,14 @@ export type ValidationSeverity = 'error' | 'warning' | 'info';
40
41
  * A validation error or warning found during linting
41
42
  * @public
42
43
  */
44
+ /** Um criterio que ainda impede a conclusao. */
45
+ export interface CriterioEmAberto {
46
+ /** O texto do item, como escrito. */
47
+ readonly texto: string;
48
+ /** Onde ele esta, quando a fonte sabe dizer. */
49
+ readonly linha?: number;
50
+ }
51
+
43
52
  export interface ValidationIssue {
44
53
  /** The file or task that has the issue */
45
54
  file: string;
@@ -130,6 +139,20 @@ export interface ITaskProvider<TTask extends Task = Task> {
130
139
  * @returns The lint result with any validation issues found
131
140
  */
132
141
  lint: (fix?: boolean) => Promise<LintResult>;
142
+
143
+ /**
144
+ * Os criterios de conclusao que ainda bloqueiam esta tarefa, se a fonte tiver
145
+ * o conceito.
146
+ *
147
+ * **Opcional de proposito.** Checklist e uma forma do provider de arquivos: o
148
+ * Jira tem subtarefas, o GitHub tem itens de lista na descricao. Um provider
149
+ * sem nada equivalente simplesmente nao implementa, e o `finishTask` conclui
150
+ * sem portao — em vez de chamar algo que falha.
151
+ *
152
+ * O conceito e de dominio ("o que falta para isto estar pronto?"); a
153
+ * representacao e de provider.
154
+ */
155
+ getCompletionBlockers?: (task: TTask) => Promise<CriterioEmAberto[]>;
133
156
  }
134
157
 
135
158
  /**
@@ -166,6 +189,52 @@ export interface ITaskManager<TTask extends Task = Task> {
166
189
  */
167
190
  getAllTasks: () => Promise<TTask[]>;
168
191
 
192
+ /**
193
+ * Da a cada tarefa um numero de prioridade, de uma vez.
194
+ *
195
+ * E operacao de dominio, e nao um `updateTask` generico, por dois motivos.
196
+ *
197
+ * O primeiro e de desenho: a regra da numeracao — preservar a ordem, manter o
198
+ * numero de quem ja tem, abrir espaco quando faltar — passa a viver num lugar
199
+ * so, em vez de ser reescrita pela CLI e pelo servidor MCP.
200
+ *
201
+ * O segundo e de tipo: um metodo que **consome** `TTask` torna a interface
202
+ * contravariante nele, e um `ITaskManager<TarefaEspecifica>` deixa de poder
203
+ * ser usado onde se espera `ITaskManager<Task>`. Uma operacao que so devolve
204
+ * numeros nao tem esse problema.
205
+ *
206
+ * Existe porque um projeto **meio numerado** cobra caro: mover uma tarefa do
207
+ * meio da regiao sem numero reescreve todos os antecessores — 124 arquivos num
208
+ * projeto de 500, medido. Depois desta operacao, todo movimento custa um.
209
+ *
210
+ * @param options - `dryRun` apenas conta, sem gravar
211
+ * @returns Quantas existem, quantas estavam sem numero, e quantas mudaram
212
+ */
213
+ /**
214
+ * Os grupos, quando a fonte tem o conceito — `undefined` quando nao tem.
215
+ *
216
+ * Opcional de proposito: o GitHub mapeia para milestone, o Redmine para
217
+ * categoria, e uma fonte sem nada equivalente simplesmente nao oferece. Quem
218
+ * consome descobre pela ausencia, em vez de chamar algo que falha.
219
+ */
220
+ readonly groupRegistry?: IGroupRegistry;
221
+
222
+ /**
223
+ * Conclui a tarefa e relata os criterios que ficaram em aberto.
224
+ *
225
+ * Avisa, e nao recusa: fechar e um gesto unico, muitas vezes com pressa, e
226
+ * recusar ali torna o comando fragil. O portao duro vive no `lint`.
227
+ *
228
+ * O relato vem vazio quando o provider nao tem o conceito de checklist.
229
+ */
230
+ finishTaskComRelato: (taskId: TaskId) => Promise<{ task: TTask; blockers: CriterioEmAberto[] }>;
231
+
232
+ prioritizeAll: (options?: { dryRun?: boolean }) => Promise<{
233
+ total: number;
234
+ withoutPriority: number;
235
+ changed: number;
236
+ }>;
237
+
169
238
  /**
170
239
  * Mark a task as ready for review.
171
240
  * Transitions the task from 'in-progress' to 'in-review' status.
@@ -209,4 +278,18 @@ export interface ITaskManager<TTask extends Task = Task> {
209
278
  * @returns The lint result with any validation issues found
210
279
  */
211
280
  lint: (fix?: boolean) => Promise<LintResult>;
281
+
282
+ /**
283
+ * Os criterios de conclusao que ainda bloqueiam esta tarefa, se a fonte tiver
284
+ * o conceito.
285
+ *
286
+ * **Opcional de proposito.** Checklist e uma forma do provider de arquivos: o
287
+ * Jira tem subtarefas, o GitHub tem itens de lista na descricao. Um provider
288
+ * sem nada equivalente simplesmente nao implementa, e o `finishTask` conclui
289
+ * sem portao — em vez de chamar algo que falha.
290
+ *
291
+ * O conceito e de dominio ("o que falta para isto estar pronto?"); a
292
+ * representacao e de provider.
293
+ */
294
+ getCompletionBlockers?: (task: TTask) => Promise<CriterioEmAberto[]>;
212
295
  }
package/src/testing.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Contratos que as implementacoes provam contra si mesmas.
3
+ *
4
+ * Exportado por `@opentask/taskin-task-manager/testing` para o `vitest` nunca
5
+ * entrar no grafo de execucao do pacote.
6
+ */
7
+ export * from './group-registry.contract.js';
8
+ export * from './user-registry.contract.js';
@@ -40,7 +40,7 @@ export function runUserRegistryContractTests(createSubject: () => Promise<IUserR
40
40
  const user = registry.resolveUser('joao.silva');
41
41
  expect(user).toBeDefined();
42
42
  expect(user!.id).toBe('joao.silva');
43
- expect(user!.avatar).toMatch(/^https:\/\/www\.gravatar\.com\/avatar\/[a-f0-9]{32}\?d=mp$/);
43
+ expect(user!.avatarHash).toMatch(/^[a-f0-9]{32}$/);
44
44
  });
45
45
 
46
46
  it('should return user by name (case-insensitive)', async () => {
@@ -54,7 +54,7 @@ export function runUserRegistryContractTests(createSubject: () => Promise<IUserR
54
54
  const user = registry.resolveUser('Maria Souza');
55
55
  expect(user).toBeDefined();
56
56
  expect(user!.id).toBe('maria');
57
- expect(user!.avatar).toMatch(/^https:\/\/www\.gravatar\.com\/avatar\/[a-f0-9]{32}\?d=mp$/);
57
+ expect(user!.avatarHash).toMatch(/^[a-f0-9]{32}$/);
58
58
  });
59
59
 
60
60
  it('should return undefined for unknown user', async () => {
@@ -65,21 +65,21 @@ export function runUserRegistryContractTests(createSubject: () => Promise<IUserR
65
65
  });
66
66
 
67
67
  describe('createTemporaryUser', () => {
68
- it('should create user with Gravatar URL', async () => {
68
+ it('should create user with an avatar identity hash', async () => {
69
69
  const registry = await createSubject();
70
70
  const user = registry.createTemporaryUser('Carlos');
71
71
 
72
72
  expect(user.id).toBe('carlos');
73
73
  expect(user.name).toBe('Carlos');
74
74
  expect(user.email).toBe('carlos@example.com');
75
- expect(user.avatar).toMatch(/^https:\/\/www\.gravatar\.com\/avatar\/[a-f0-9]{32}\?d=mp$/);
75
+ expect(user.avatarHash).toMatch(/^[a-f0-9]{32}$/);
76
76
  });
77
77
 
78
- it('should create different Gravatar URLs for different inputs', async () => {
78
+ it('should create different avatar hashes for different inputs', async () => {
79
79
  const registry = await createSubject();
80
80
  const user1 = registry.createTemporaryUser('Alice');
81
81
  const user2 = registry.createTemporaryUser('Bob');
82
- expect(user1.avatar).not.toBe(user2.avatar);
82
+ expect(user1.avatarHash).not.toBe(user2.avatarHash);
83
83
  });
84
84
  });
85
85