@danypops/papyrus 0.2.1 → 0.3.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/README.md +35 -11
- package/extension/src/active-task-continuation.ts +12 -18
- package/extension/src/artifact-format.ts +5 -3
- package/extension/src/domain-tools.ts +18 -4
- package/extension/src/index.ts +16 -22
- package/extension/src/task-detail-format.ts +5 -3
- package/extension/src/task-graph.ts +11 -1
- package/extension/src/task-presentation.ts +26 -0
- package/extension/src/task-widget.ts +27 -20
- package/extension/src/tasks.ts +68 -31
- package/package.json +1 -1
- package/src/adapters/sqlite-task-focus-store.ts +31 -0
- package/src/cli.ts +54 -5
- package/src/client.ts +2 -2
- package/src/constants.ts +11 -10
- package/src/db.ts +73 -20
- package/src/ports/task-focus-store.ts +21 -0
- package/src/service.ts +44 -11
- package/src/task-context.ts +10 -9
- package/src/task-execution.ts +16 -3
- package/src/task-graph-view.ts +6 -3
- package/src/task-service.ts +110 -27
package/src/task-service.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { TASK_EXECUTION_MAX_DEGREE, TASK_EXECUTION_MAX_EDGES, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
|
|
2
2
|
import type { Artifact } from "./domain/artifact.ts";
|
|
3
|
-
import { validateChecklist, type Checklist } from "./domain/checklist.ts";
|
|
3
|
+
import { checklistEntries, validateChecklist, type Checklist, type ProofReference } from "./domain/checklist.ts";
|
|
4
4
|
import type { Gate, GateResult } from "./domain/gate.ts";
|
|
5
5
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
6
6
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
7
|
+
import { InMemoryTaskFocusStore, type TaskFocusStore } from "./ports/task-focus-store.ts";
|
|
7
8
|
import { assertDependencyEdgeAllowed } from "./task-execution.ts";
|
|
8
9
|
|
|
9
10
|
export interface TaskFilter {
|
|
@@ -12,10 +13,12 @@ export interface TaskFilter {
|
|
|
12
13
|
limit?: number;
|
|
13
14
|
}
|
|
14
15
|
|
|
16
|
+
export type TaskStatus = "todo" | "in-progress" | "review" | "rejected" | "done" | "canceled";
|
|
17
|
+
|
|
15
18
|
export interface CreateTaskInput {
|
|
16
19
|
title: string;
|
|
17
20
|
body?: string;
|
|
18
|
-
status?:
|
|
21
|
+
status?: TaskStatus;
|
|
19
22
|
labels?: string[];
|
|
20
23
|
extra?: Record<string, unknown>;
|
|
21
24
|
gates?: Gate[];
|
|
@@ -25,23 +28,32 @@ export interface CreateTaskInput {
|
|
|
25
28
|
dependsOn?: string[];
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export type TaskTransition = "start" | "
|
|
31
|
+
export type TaskTransition = "start" | "submit" | "reject" | "retry" | "cancel";
|
|
29
32
|
|
|
30
33
|
export interface TaskBlockage {
|
|
31
34
|
artifact: Artifact;
|
|
32
35
|
dependencyIds: string[];
|
|
33
36
|
}
|
|
34
37
|
|
|
38
|
+
export interface ChecklistReview {
|
|
39
|
+
item: string;
|
|
40
|
+
proof: ProofReference[];
|
|
41
|
+
accepted: boolean;
|
|
42
|
+
reason?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
35
45
|
export interface TaskCompletion {
|
|
36
46
|
artifact: Artifact;
|
|
37
47
|
gates: GateResult[];
|
|
48
|
+
checklist: ChecklistReview[];
|
|
38
49
|
completed: boolean;
|
|
39
|
-
|
|
50
|
+
focused: Artifact | null;
|
|
40
51
|
blocked: TaskBlockage[];
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface TaskNode {
|
|
44
55
|
task: Artifact;
|
|
56
|
+
active?: boolean;
|
|
45
57
|
parentIds: string[];
|
|
46
58
|
childIds: string[];
|
|
47
59
|
dependencyIds: string[];
|
|
@@ -52,16 +64,19 @@ export interface TaskGraph {
|
|
|
52
64
|
rootIds: string[];
|
|
53
65
|
}
|
|
54
66
|
|
|
55
|
-
const TASK_TRANSITIONS: Record<TaskTransition, { from:
|
|
56
|
-
start: { from: ["
|
|
57
|
-
|
|
58
|
-
|
|
67
|
+
const TASK_TRANSITIONS: Record<TaskTransition, { from: TaskStatus[]; to: TaskStatus }> = {
|
|
68
|
+
start: { from: ["todo"], to: "in-progress" },
|
|
69
|
+
submit: { from: ["in-progress"], to: "review" },
|
|
70
|
+
reject: { from: ["review"], to: "rejected" },
|
|
71
|
+
retry: { from: ["rejected"], to: "in-progress" },
|
|
72
|
+
cancel: { from: ["todo", "in-progress", "review", "rejected"], to: "canceled" },
|
|
59
73
|
};
|
|
60
74
|
|
|
61
75
|
export class Tasks {
|
|
62
76
|
constructor(
|
|
63
77
|
private readonly artifacts: ArtifactStore,
|
|
64
78
|
private readonly gates: GateRunner,
|
|
79
|
+
private readonly focusStore: TaskFocusStore = new InMemoryTaskFocusStore(),
|
|
65
80
|
) {}
|
|
66
81
|
|
|
67
82
|
private require(id: string): Artifact {
|
|
@@ -108,8 +123,10 @@ export class Tasks {
|
|
|
108
123
|
throw new Error(`task execution graph exceeds ${TASK_EXECUTION_MAX_NODES} nodes`);
|
|
109
124
|
}
|
|
110
125
|
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
126
|
+
const focusedId = this.focusStore.get();
|
|
111
127
|
const nodes = new Map(tasks.map((task) => [task.id, {
|
|
112
128
|
task,
|
|
129
|
+
active: task.id === focusedId,
|
|
113
130
|
parentIds: [] as string[],
|
|
114
131
|
childIds: [] as string[],
|
|
115
132
|
dependencyIds: [] as string[],
|
|
@@ -148,34 +165,63 @@ export class Tasks {
|
|
|
148
165
|
return this.artifacts.get(id, { tree: true })!;
|
|
149
166
|
}
|
|
150
167
|
|
|
168
|
+
active(): Artifact | null {
|
|
169
|
+
const id = this.focusStore.get();
|
|
170
|
+
if (!id) return null;
|
|
171
|
+
const task = this.artifacts.get(id);
|
|
172
|
+
if (!task || task.kind !== "task" || task.status === "done" || task.status === "canceled") {
|
|
173
|
+
this.focusStore.clear(id);
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
return task;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
focus(id: string): Artifact {
|
|
180
|
+
const task = this.require(id);
|
|
181
|
+
if (task.status === "done" || task.status === "canceled") {
|
|
182
|
+
throw new Error(`cannot focus task from ${task.status}`);
|
|
183
|
+
}
|
|
184
|
+
this.focusStore.set(id);
|
|
185
|
+
return task;
|
|
186
|
+
}
|
|
187
|
+
|
|
151
188
|
transition(id: string, action: TaskTransition): Artifact {
|
|
152
189
|
const task = this.require(id);
|
|
153
190
|
const transition = TASK_TRANSITIONS[action];
|
|
154
|
-
if (!transition.from.includes(task.status)) throw new Error(`cannot ${action} task from ${task.status}`);
|
|
191
|
+
if (!transition.from.includes(task.status as TaskStatus)) throw new Error(`cannot ${action} task from ${task.status}`);
|
|
155
192
|
if (action === "start") {
|
|
156
193
|
const blocking = this.dependencyIds(id).filter((dependencyId) => this.require(dependencyId).status !== "done");
|
|
157
194
|
if (blocking.length > 0) throw new Error(`task "${id}" is blocked by dependencies: ${blocking.join(", ")}`);
|
|
195
|
+
this.focusStore.set(id);
|
|
158
196
|
}
|
|
159
|
-
|
|
197
|
+
const updated = this.artifacts.setStatus(id, transition.to)!;
|
|
198
|
+
if (action === "start" || action === "retry") this.propagateProgressToAncestors(id);
|
|
199
|
+
if (action === "retry") this.focusStore.set(id);
|
|
200
|
+
if (action === "cancel") this.focusStore.clear(id);
|
|
201
|
+
return updated;
|
|
160
202
|
}
|
|
161
203
|
|
|
162
204
|
complete(id: string): TaskCompletion {
|
|
163
|
-
const task = this.
|
|
205
|
+
const task = this.requireReview(id);
|
|
206
|
+
const checklist = this.reviewChecklist(task);
|
|
164
207
|
const results = this.gates.run(id);
|
|
165
|
-
if (results.some((gate) => !gate.passed)) {
|
|
166
|
-
|
|
208
|
+
if (results.some((gate) => !gate.passed) || checklist.some((item) => !item.accepted)) {
|
|
209
|
+
const artifact = this.artifacts.setStatus(id, "rejected")!;
|
|
210
|
+
return { artifact, gates: results, checklist, completed: false, focused: this.active(), blocked: [] };
|
|
167
211
|
}
|
|
168
|
-
return this.finish(id, results);
|
|
212
|
+
return this.finish(id, results, checklist);
|
|
169
213
|
}
|
|
170
214
|
|
|
171
215
|
async completeAsync(id: string): Promise<TaskCompletion> {
|
|
172
|
-
this.
|
|
216
|
+
const task = this.requireReview(id);
|
|
217
|
+
const checklist = this.reviewChecklist(task);
|
|
173
218
|
const results = await this.gates.runAsync(id);
|
|
174
|
-
if (results.some((gate) => !gate.passed)) {
|
|
175
|
-
|
|
219
|
+
if (results.some((gate) => !gate.passed) || checklist.some((item) => !item.accepted)) {
|
|
220
|
+
const artifact = this.artifacts.setStatus(id, "rejected")!;
|
|
221
|
+
return { artifact, gates: results, checklist, completed: false, focused: this.active(), blocked: [] };
|
|
176
222
|
}
|
|
177
|
-
const current = this.
|
|
178
|
-
return this.finish(current.id, results);
|
|
223
|
+
const current = this.requireReview(id);
|
|
224
|
+
return this.finish(current.id, results, checklist);
|
|
179
225
|
}
|
|
180
226
|
|
|
181
227
|
runGates(id: string): Promise<GateResult[]> {
|
|
@@ -226,6 +272,39 @@ export class Tasks {
|
|
|
226
272
|
return relationships;
|
|
227
273
|
}
|
|
228
274
|
|
|
275
|
+
private parentIds(id: string): string[] {
|
|
276
|
+
return this.relationships(id)
|
|
277
|
+
.flatMap((edge) => {
|
|
278
|
+
if (edge.relation === "part_of" && edge.from === id) return [edge.to];
|
|
279
|
+
if (edge.relation === "contains" && edge.to === id) return [edge.from];
|
|
280
|
+
return [];
|
|
281
|
+
})
|
|
282
|
+
.filter((parentId, index, ids) => ids.indexOf(parentId) === index);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private propagateProgressToAncestors(id: string): void {
|
|
286
|
+
const pending = this.parentIds(id);
|
|
287
|
+
const visited = new Set<string>();
|
|
288
|
+
while (pending.length > 0) {
|
|
289
|
+
const parentId = pending.shift()!;
|
|
290
|
+
if (visited.has(parentId)) continue;
|
|
291
|
+
if (visited.size >= TASK_EXECUTION_MAX_NODES) throw new Error("task ancestry exceeds execution node bound");
|
|
292
|
+
visited.add(parentId);
|
|
293
|
+
const parent = this.require(parentId);
|
|
294
|
+
if (parent.status === "todo") this.artifacts.setStatus(parentId, "in-progress");
|
|
295
|
+
pending.push(...this.parentIds(parentId));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
private reviewChecklist(task: Artifact): ChecklistReview[] {
|
|
300
|
+
return checklistEntries(task.extra["checklist"]).map((entry) => ({
|
|
301
|
+
item: entry.item,
|
|
302
|
+
proof: entry.proof,
|
|
303
|
+
accepted: !entry.legacy && entry.proof.length > 0,
|
|
304
|
+
...((entry.legacy || entry.proof.length === 0) ? { reason: "typed proof reference required" } : {}),
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
|
|
229
308
|
private dependencyIds(id: string): string[] {
|
|
230
309
|
const ids = this.relationships(id)
|
|
231
310
|
.filter((edge) => edge.relation === "depends_on" && edge.from === id)
|
|
@@ -236,7 +315,7 @@ export class Tasks {
|
|
|
236
315
|
return ids;
|
|
237
316
|
}
|
|
238
317
|
|
|
239
|
-
private finish(id: string, gates: GateResult[]): TaskCompletion {
|
|
318
|
+
private finish(id: string, gates: GateResult[], checklist: ChecklistReview[]): TaskCompletion {
|
|
240
319
|
const successorIds = this.relationships(id)
|
|
241
320
|
.filter((edge) => edge.relation === "depends_on" && edge.to === id)
|
|
242
321
|
.map((edge) => edge.from);
|
|
@@ -244,25 +323,29 @@ export class Tasks {
|
|
|
244
323
|
throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_DEGREE} successors`);
|
|
245
324
|
}
|
|
246
325
|
const artifact = this.artifacts.setStatus(id, "done")!;
|
|
247
|
-
|
|
326
|
+
this.focusStore.clear(id);
|
|
248
327
|
const blocked: TaskBlockage[] = [];
|
|
249
|
-
|
|
328
|
+
let focused: Artifact | null = null;
|
|
329
|
+
for (const successorId of [...successorIds].sort()) {
|
|
250
330
|
const successor = this.require(successorId);
|
|
251
|
-
if (successor.status
|
|
331
|
+
if (successor.status === "done" || successor.status === "canceled") continue;
|
|
252
332
|
const dependencyIds = this.dependencyIds(successorId)
|
|
253
333
|
.filter((dependencyId) => this.require(dependencyId).status !== "done");
|
|
254
334
|
if (dependencyIds.length > 0) {
|
|
255
335
|
blocked.push({ artifact: successor, dependencyIds });
|
|
256
336
|
continue;
|
|
257
337
|
}
|
|
258
|
-
|
|
338
|
+
if (!focused) {
|
|
339
|
+
this.focusStore.set(successor.id);
|
|
340
|
+
focused = successor;
|
|
341
|
+
}
|
|
259
342
|
}
|
|
260
|
-
return { artifact, gates, completed: true,
|
|
343
|
+
return { artifact, gates, checklist, completed: true, focused, blocked };
|
|
261
344
|
}
|
|
262
345
|
|
|
263
|
-
private
|
|
346
|
+
private requireReview(id: string): Artifact {
|
|
264
347
|
const task = this.require(id);
|
|
265
|
-
if (task.status !== "
|
|
348
|
+
if (task.status !== "review") throw new Error(`cannot complete task from ${task.status}`);
|
|
266
349
|
return task;
|
|
267
350
|
}
|
|
268
351
|
}
|