@danypops/papyrus 0.1.0 → 0.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.
- package/README.md +22 -10
- package/extension/src/{facade-tools.ts → domain-tools.ts} +26 -7
- package/extension/src/index.ts +77 -5
- package/extension/src/task-driver.ts +132 -0
- package/extension/src/task-graph.ts +3 -3
- package/extension/src/tasks.ts +26 -9
- package/package.json +1 -1
- package/src/adapters/sqlite-artifact-store.ts +7 -0
- package/src/cli.ts +103 -3
- package/src/constants.ts +10 -4
- package/src/db.ts +1 -1
- package/src/domain/artifact.ts +1 -0
- package/src/domain/skill-definition.ts +217 -0
- package/src/{facades.ts → domain-services.ts} +1 -1
- package/src/service.ts +14 -3
- package/src/task-execution.ts +124 -0
- package/src/task-graph-view.ts +25 -6
- package/src/task-service.ts +98 -6
- package/src/version.ts +16 -0
package/src/task-service.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { TASK_EXECUTION_MAX_DEGREE, TASK_EXECUTION_MAX_EDGES, TASK_EXECUTION_MAX_NODES } from "./constants.ts";
|
|
1
2
|
import type { Artifact } from "./domain/artifact.ts";
|
|
2
3
|
import { validateChecklist, type Checklist } from "./domain/checklist.ts";
|
|
3
4
|
import type { Gate, GateResult } from "./domain/gate.ts";
|
|
4
5
|
import type { ArtifactStore } from "./ports/artifact-store.ts";
|
|
5
6
|
import type { GateRunner } from "./ports/gate-runner.ts";
|
|
7
|
+
import { assertDependencyEdgeAllowed } from "./task-execution.ts";
|
|
6
8
|
|
|
7
9
|
export interface TaskFilter {
|
|
8
10
|
status?: string;
|
|
@@ -25,10 +27,17 @@ export interface CreateTaskInput {
|
|
|
25
27
|
|
|
26
28
|
export type TaskTransition = "start" | "fail" | "retry";
|
|
27
29
|
|
|
30
|
+
export interface TaskBlockage {
|
|
31
|
+
artifact: Artifact;
|
|
32
|
+
dependencyIds: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
export interface TaskCompletion {
|
|
29
36
|
artifact: Artifact;
|
|
30
37
|
gates: GateResult[];
|
|
31
38
|
completed: boolean;
|
|
39
|
+
started: Artifact[];
|
|
40
|
+
blocked: TaskBlockage[];
|
|
32
41
|
}
|
|
33
42
|
|
|
34
43
|
export interface TaskNode {
|
|
@@ -63,6 +72,9 @@ export class Tasks {
|
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
create(input: CreateTaskInput): Artifact {
|
|
75
|
+
if ((input.dependsOn?.length ?? 0) > TASK_EXECUTION_MAX_DEGREE) {
|
|
76
|
+
throw new Error(`task cannot exceed ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
|
|
77
|
+
}
|
|
66
78
|
if (input.parentId) this.require(input.parentId);
|
|
67
79
|
for (const dependency of input.dependsOn ?? []) this.require(dependency);
|
|
68
80
|
const extra: Record<string, unknown> = { ...(input.extra ?? {}) };
|
|
@@ -87,7 +99,14 @@ export class Tasks {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
graph(filter: TaskFilter = {}): TaskGraph {
|
|
90
|
-
const
|
|
102
|
+
const requestedLimit = filter.limit ?? TASK_EXECUTION_MAX_NODES + 1;
|
|
103
|
+
if (!Number.isInteger(requestedLimit) || requestedLimit < 1 || requestedLimit > TASK_EXECUTION_MAX_NODES + 1) {
|
|
104
|
+
throw new Error(`task graph limit must be between 1 and ${TASK_EXECUTION_MAX_NODES + 1}`);
|
|
105
|
+
}
|
|
106
|
+
const tasks = this.list({ ...filter, limit: requestedLimit });
|
|
107
|
+
if (tasks.length > TASK_EXECUTION_MAX_NODES) {
|
|
108
|
+
throw new Error(`task execution graph exceeds ${TASK_EXECUTION_MAX_NODES} nodes`);
|
|
109
|
+
}
|
|
91
110
|
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
92
111
|
const nodes = new Map(tasks.map((task) => [task.id, {
|
|
93
112
|
task,
|
|
@@ -95,7 +114,15 @@ export class Tasks {
|
|
|
95
114
|
childIds: [] as string[],
|
|
96
115
|
dependencyIds: [] as string[],
|
|
97
116
|
}]));
|
|
98
|
-
|
|
117
|
+
const relationships = this.artifacts.relationships({
|
|
118
|
+
kind: "task",
|
|
119
|
+
artifactIds: [...byId.keys()],
|
|
120
|
+
limit: TASK_EXECUTION_MAX_EDGES + 1,
|
|
121
|
+
});
|
|
122
|
+
if (relationships.length > TASK_EXECUTION_MAX_EDGES) {
|
|
123
|
+
throw new Error(`task execution graph exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
|
|
124
|
+
}
|
|
125
|
+
for (const edge of relationships) {
|
|
99
126
|
if (!byId.has(edge.from) || !byId.has(edge.to)) continue;
|
|
100
127
|
const parentId = edge.relation === "contains" ? edge.from : edge.relation === "part_of" ? edge.to : undefined;
|
|
101
128
|
const childId = edge.relation === "contains" ? edge.to : edge.relation === "part_of" ? edge.from : undefined;
|
|
@@ -125,22 +152,30 @@ export class Tasks {
|
|
|
125
152
|
const task = this.require(id);
|
|
126
153
|
const transition = TASK_TRANSITIONS[action];
|
|
127
154
|
if (!transition.from.includes(task.status)) throw new Error(`cannot ${action} task from ${task.status}`);
|
|
155
|
+
if (action === "start") {
|
|
156
|
+
const blocking = this.dependencyIds(id).filter((dependencyId) => this.require(dependencyId).status !== "done");
|
|
157
|
+
if (blocking.length > 0) throw new Error(`task "${id}" is blocked by dependencies: ${blocking.join(", ")}`);
|
|
158
|
+
}
|
|
128
159
|
return this.artifacts.setStatus(id, transition.to)!;
|
|
129
160
|
}
|
|
130
161
|
|
|
131
162
|
complete(id: string): TaskCompletion {
|
|
132
163
|
const task = this.requireActive(id);
|
|
133
164
|
const results = this.gates.run(id);
|
|
134
|
-
if (results.some((gate) => !gate.passed))
|
|
135
|
-
|
|
165
|
+
if (results.some((gate) => !gate.passed)) {
|
|
166
|
+
return { artifact: task, gates: results, completed: false, started: [], blocked: [] };
|
|
167
|
+
}
|
|
168
|
+
return this.finish(id, results);
|
|
136
169
|
}
|
|
137
170
|
|
|
138
171
|
async completeAsync(id: string): Promise<TaskCompletion> {
|
|
139
172
|
this.requireActive(id);
|
|
140
173
|
const results = await this.gates.runAsync(id);
|
|
141
|
-
if (results.some((gate) => !gate.passed))
|
|
174
|
+
if (results.some((gate) => !gate.passed)) {
|
|
175
|
+
return { artifact: this.require(id), gates: results, completed: false, started: [], blocked: [] };
|
|
176
|
+
}
|
|
142
177
|
const current = this.requireActive(id);
|
|
143
|
-
return
|
|
178
|
+
return this.finish(current.id, results);
|
|
144
179
|
}
|
|
145
180
|
|
|
146
181
|
runGates(id: string): Promise<GateResult[]> {
|
|
@@ -156,6 +191,17 @@ export class Tasks {
|
|
|
156
191
|
depend(id: string, dependencyId: string): Artifact {
|
|
157
192
|
this.require(id);
|
|
158
193
|
this.require(dependencyId);
|
|
194
|
+
const graph = this.graph();
|
|
195
|
+
assertDependencyEdgeAllowed(graph, id, dependencyId);
|
|
196
|
+
const node = graph.nodes.find((entry) => entry.task.id === id)!;
|
|
197
|
+
if (node.dependencyIds.includes(dependencyId)) return this.show(id);
|
|
198
|
+
if (node.dependencyIds.length >= TASK_EXECUTION_MAX_DEGREE) {
|
|
199
|
+
throw new Error(`task "${id}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
|
|
200
|
+
}
|
|
201
|
+
const successorCount = graph.nodes.filter((entry) => entry.dependencyIds.includes(dependencyId)).length;
|
|
202
|
+
if (successorCount >= TASK_EXECUTION_MAX_DEGREE) {
|
|
203
|
+
throw new Error(`task "${dependencyId}" cannot exceed ${TASK_EXECUTION_MAX_DEGREE} successors`);
|
|
204
|
+
}
|
|
159
205
|
this.artifacts.link({ from: id, relation: "depends_on", to: dependencyId });
|
|
160
206
|
return this.show(id);
|
|
161
207
|
}
|
|
@@ -168,6 +214,52 @@ export class Tasks {
|
|
|
168
214
|
return this.show(parentId);
|
|
169
215
|
}
|
|
170
216
|
|
|
217
|
+
private relationships(id: string) {
|
|
218
|
+
const relationships = this.artifacts.relationships({
|
|
219
|
+
kind: "task",
|
|
220
|
+
artifactIds: [id],
|
|
221
|
+
limit: TASK_EXECUTION_MAX_EDGES + 1,
|
|
222
|
+
});
|
|
223
|
+
if (relationships.length > TASK_EXECUTION_MAX_EDGES) {
|
|
224
|
+
throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_EDGES} relationships`);
|
|
225
|
+
}
|
|
226
|
+
return relationships;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private dependencyIds(id: string): string[] {
|
|
230
|
+
const ids = this.relationships(id)
|
|
231
|
+
.filter((edge) => edge.relation === "depends_on" && edge.from === id)
|
|
232
|
+
.map((edge) => edge.to);
|
|
233
|
+
if (ids.length > TASK_EXECUTION_MAX_DEGREE) {
|
|
234
|
+
throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_DEGREE} prerequisites`);
|
|
235
|
+
}
|
|
236
|
+
return ids;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private finish(id: string, gates: GateResult[]): TaskCompletion {
|
|
240
|
+
const successorIds = this.relationships(id)
|
|
241
|
+
.filter((edge) => edge.relation === "depends_on" && edge.to === id)
|
|
242
|
+
.map((edge) => edge.from);
|
|
243
|
+
if (successorIds.length > TASK_EXECUTION_MAX_DEGREE) {
|
|
244
|
+
throw new Error(`task "${id}" exceeds ${TASK_EXECUTION_MAX_DEGREE} successors`);
|
|
245
|
+
}
|
|
246
|
+
const artifact = this.artifacts.setStatus(id, "done")!;
|
|
247
|
+
const started: Artifact[] = [];
|
|
248
|
+
const blocked: TaskBlockage[] = [];
|
|
249
|
+
for (const successorId of successorIds) {
|
|
250
|
+
const successor = this.require(successorId);
|
|
251
|
+
if (successor.status !== "pending") continue;
|
|
252
|
+
const dependencyIds = this.dependencyIds(successorId)
|
|
253
|
+
.filter((dependencyId) => this.require(dependencyId).status !== "done");
|
|
254
|
+
if (dependencyIds.length > 0) {
|
|
255
|
+
blocked.push({ artifact: successor, dependencyIds });
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
started.push(this.artifacts.setStatus(successorId, "active")!);
|
|
259
|
+
}
|
|
260
|
+
return { artifact, gates, completed: true, started, blocked };
|
|
261
|
+
}
|
|
262
|
+
|
|
171
263
|
private requireActive(id: string): Artifact {
|
|
172
264
|
const task = this.require(id);
|
|
173
265
|
if (task.status !== "active") throw new Error(`cannot complete task from ${task.status}`);
|
package/src/version.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
function packageVersion(): string {
|
|
4
|
+
const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")) as unknown;
|
|
5
|
+
if (typeof manifest !== "object" || manifest === null || Array.isArray(manifest)) {
|
|
6
|
+
throw new Error("Papyrus package manifest must be an object");
|
|
7
|
+
}
|
|
8
|
+
const version = (manifest as Record<string, unknown>)["version"];
|
|
9
|
+
if (typeof version !== "string" || !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
10
|
+
throw new Error("Papyrus package manifest has an invalid version");
|
|
11
|
+
}
|
|
12
|
+
return version;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Runtime package version; package.json is the single release source of truth. */
|
|
16
|
+
export const VERSION = packageVersion();
|