@workflow-cannon/workspace-kit 0.2.0 → 0.4.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 (36) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.js +168 -2
  3. package/dist/contracts/index.d.ts +1 -1
  4. package/dist/contracts/module-contract.d.ts +14 -0
  5. package/dist/core/index.d.ts +2 -0
  6. package/dist/core/index.js +2 -0
  7. package/dist/core/policy.d.ts +24 -0
  8. package/dist/core/policy.js +102 -0
  9. package/dist/core/workspace-kit-config.d.ts +49 -0
  10. package/dist/core/workspace-kit-config.js +218 -0
  11. package/dist/modules/documentation/index.d.ts +1 -1
  12. package/dist/modules/documentation/index.js +63 -38
  13. package/dist/modules/documentation/runtime.d.ts +5 -1
  14. package/dist/modules/documentation/runtime.js +87 -4
  15. package/dist/modules/documentation/types.d.ts +14 -0
  16. package/dist/modules/index.d.ts +3 -1
  17. package/dist/modules/index.js +2 -1
  18. package/dist/modules/task-engine/generator.d.ts +2 -0
  19. package/dist/modules/task-engine/generator.js +101 -0
  20. package/dist/modules/task-engine/importer.d.ts +8 -0
  21. package/dist/modules/task-engine/importer.js +157 -0
  22. package/dist/modules/task-engine/index.d.ts +7 -0
  23. package/dist/modules/task-engine/index.js +237 -2
  24. package/dist/modules/task-engine/service.d.ts +21 -0
  25. package/dist/modules/task-engine/service.js +105 -0
  26. package/dist/modules/task-engine/store.d.ts +16 -0
  27. package/dist/modules/task-engine/store.js +88 -0
  28. package/dist/modules/task-engine/suggestions.d.ts +2 -0
  29. package/dist/modules/task-engine/suggestions.js +51 -0
  30. package/dist/modules/task-engine/transitions.d.ts +23 -0
  31. package/dist/modules/task-engine/transitions.js +109 -0
  32. package/dist/modules/task-engine/types.d.ts +82 -0
  33. package/dist/modules/task-engine/types.js +1 -0
  34. package/dist/modules/workspace-config/index.d.ts +2 -0
  35. package/dist/modules/workspace-config/index.js +72 -0
  36. package/package.json +1 -1
@@ -1,7 +1,29 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs/promises";
3
+ import { TaskStore } from "./store.js";
4
+ import { TransitionService } from "./service.js";
5
+ import { TaskEngineError } from "./transitions.js";
6
+ import { generateTasksMd } from "./generator.js";
7
+ import { importTasksFromMarkdown } from "./importer.js";
8
+ import { getNextActions } from "./suggestions.js";
9
+ export { TaskStore } from "./store.js";
10
+ export { TransitionService } from "./service.js";
11
+ export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
12
+ export { generateTasksMd } from "./generator.js";
13
+ export { importTasksFromMarkdown } from "./importer.js";
14
+ export { getNextActions } from "./suggestions.js";
15
+ function taskStorePath(ctx) {
16
+ const tasks = ctx.effectiveConfig?.tasks;
17
+ if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
18
+ return undefined;
19
+ }
20
+ const p = tasks.storeRelativePath;
21
+ return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
22
+ }
1
23
  export const taskEngineModule = {
2
24
  registration: {
3
25
  id: "task-engine",
4
- version: "0.1.0",
26
+ version: "0.4.0",
5
27
  contractVersion: "1",
6
28
  capabilities: ["task-engine"],
7
29
  dependsOn: [],
@@ -22,9 +44,222 @@ export const taskEngineModule = {
22
44
  {
23
45
  name: "run-transition",
24
46
  file: "run-transition.md",
25
- description: "Run a validated task status transition."
47
+ description: "Execute a validated task status transition."
48
+ },
49
+ {
50
+ name: "get-task",
51
+ file: "get-task.md",
52
+ description: "Retrieve a single task by ID."
53
+ },
54
+ {
55
+ name: "list-tasks",
56
+ file: "list-tasks.md",
57
+ description: "List tasks with optional status/phase filters."
58
+ },
59
+ {
60
+ name: "get-ready-queue",
61
+ file: "get-ready-queue.md",
62
+ description: "Get ready tasks sorted by priority."
63
+ },
64
+ {
65
+ name: "import-tasks",
66
+ file: "import-tasks.md",
67
+ description: "One-time import from TASKS.md into engine state."
68
+ },
69
+ {
70
+ name: "generate-tasks-md",
71
+ file: "generate-tasks-md.md",
72
+ description: "Generate read-only TASKS.md from engine state."
73
+ },
74
+ {
75
+ name: "get-next-actions",
76
+ file: "get-next-actions.md",
77
+ description: "Get prioritized next-action suggestions with blocking analysis."
26
78
  }
27
79
  ]
28
80
  }
81
+ },
82
+ async onCommand(command, ctx) {
83
+ const args = command.args ?? {};
84
+ const store = new TaskStore(ctx.workspacePath, taskStorePath(ctx));
85
+ try {
86
+ await store.load();
87
+ }
88
+ catch (err) {
89
+ if (err instanceof TaskEngineError) {
90
+ return { ok: false, code: err.code, message: err.message };
91
+ }
92
+ return {
93
+ ok: false,
94
+ code: "storage-read-error",
95
+ message: `Failed to load task store: ${err.message}`
96
+ };
97
+ }
98
+ if (command.name === "run-transition") {
99
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
100
+ const action = typeof args.action === "string" ? args.action : undefined;
101
+ const actor = typeof args.actor === "string"
102
+ ? args.actor
103
+ : ctx.resolvedActor !== undefined
104
+ ? ctx.resolvedActor
105
+ : undefined;
106
+ if (!taskId || !action) {
107
+ return {
108
+ ok: false,
109
+ code: "invalid-task-schema",
110
+ message: "run-transition requires 'taskId' and 'action' arguments"
111
+ };
112
+ }
113
+ try {
114
+ const service = new TransitionService(store);
115
+ const result = await service.runTransition({ taskId, action, actor });
116
+ return {
117
+ ok: true,
118
+ code: "transition-applied",
119
+ message: `${taskId}: ${result.evidence.fromState} → ${result.evidence.toState} (${action})`,
120
+ data: {
121
+ evidence: result.evidence,
122
+ autoUnblocked: result.autoUnblocked
123
+ }
124
+ };
125
+ }
126
+ catch (err) {
127
+ if (err instanceof TaskEngineError) {
128
+ return { ok: false, code: err.code, message: err.message };
129
+ }
130
+ return {
131
+ ok: false,
132
+ code: "invalid-transition",
133
+ message: err.message
134
+ };
135
+ }
136
+ }
137
+ if (command.name === "get-task") {
138
+ const taskId = typeof args.taskId === "string" ? args.taskId : undefined;
139
+ if (!taskId) {
140
+ return {
141
+ ok: false,
142
+ code: "invalid-task-schema",
143
+ message: "get-task requires 'taskId' argument"
144
+ };
145
+ }
146
+ const task = store.getTask(taskId);
147
+ if (!task) {
148
+ return {
149
+ ok: false,
150
+ code: "task-not-found",
151
+ message: `Task '${taskId}' not found`
152
+ };
153
+ }
154
+ return {
155
+ ok: true,
156
+ code: "task-retrieved",
157
+ data: { task }
158
+ };
159
+ }
160
+ if (command.name === "list-tasks") {
161
+ const statusFilter = typeof args.status === "string" ? args.status : undefined;
162
+ const phaseFilter = typeof args.phase === "string" ? args.phase : undefined;
163
+ let tasks = store.getAllTasks();
164
+ if (statusFilter) {
165
+ tasks = tasks.filter((t) => t.status === statusFilter);
166
+ }
167
+ if (phaseFilter) {
168
+ tasks = tasks.filter((t) => t.phase === phaseFilter);
169
+ }
170
+ return {
171
+ ok: true,
172
+ code: "tasks-listed",
173
+ message: `Found ${tasks.length} tasks`,
174
+ data: { tasks, count: tasks.length }
175
+ };
176
+ }
177
+ if (command.name === "get-ready-queue") {
178
+ const tasks = store.getAllTasks();
179
+ const ready = tasks
180
+ .filter((t) => t.status === "ready")
181
+ .sort((a, b) => {
182
+ const pa = a.priority ?? "P9";
183
+ const pb = b.priority ?? "P9";
184
+ return pa.localeCompare(pb);
185
+ });
186
+ return {
187
+ ok: true,
188
+ code: "ready-queue-retrieved",
189
+ message: `${ready.length} tasks in ready queue`,
190
+ data: { tasks: ready, count: ready.length }
191
+ };
192
+ }
193
+ if (command.name === "import-tasks") {
194
+ const sourcePath = typeof args.sourcePath === "string"
195
+ ? path.resolve(ctx.workspacePath, args.sourcePath)
196
+ : path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
197
+ try {
198
+ const result = await importTasksFromMarkdown(sourcePath);
199
+ store.replaceAllTasks(result.tasks);
200
+ await store.save();
201
+ return {
202
+ ok: true,
203
+ code: "tasks-imported",
204
+ message: `Imported ${result.imported} tasks (${result.skipped} skipped)`,
205
+ data: {
206
+ imported: result.imported,
207
+ skipped: result.skipped,
208
+ errors: result.errors
209
+ }
210
+ };
211
+ }
212
+ catch (err) {
213
+ if (err instanceof TaskEngineError) {
214
+ return { ok: false, code: err.code, message: err.message };
215
+ }
216
+ return {
217
+ ok: false,
218
+ code: "import-parse-error",
219
+ message: err.message
220
+ };
221
+ }
222
+ }
223
+ if (command.name === "generate-tasks-md") {
224
+ const outputPath = typeof args.outputPath === "string"
225
+ ? path.resolve(ctx.workspacePath, args.outputPath)
226
+ : path.resolve(ctx.workspacePath, "docs/maintainers/TASKS.md");
227
+ const tasks = store.getAllTasks();
228
+ const markdown = generateTasksMd(tasks);
229
+ try {
230
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
231
+ await fs.writeFile(outputPath, markdown, "utf8");
232
+ }
233
+ catch (err) {
234
+ return {
235
+ ok: false,
236
+ code: "storage-write-error",
237
+ message: `Failed to write TASKS.md: ${err.message}`
238
+ };
239
+ }
240
+ return {
241
+ ok: true,
242
+ code: "tasks-md-generated",
243
+ message: `Generated TASKS.md with ${tasks.length} tasks`,
244
+ data: { outputPath, taskCount: tasks.length }
245
+ };
246
+ }
247
+ if (command.name === "get-next-actions") {
248
+ const tasks = store.getAllTasks();
249
+ const suggestion = getNextActions(tasks);
250
+ return {
251
+ ok: true,
252
+ code: "next-actions-retrieved",
253
+ message: suggestion.suggestedNext
254
+ ? `Suggested next: ${suggestion.suggestedNext.id} — ${suggestion.suggestedNext.title}`
255
+ : "No tasks in ready queue",
256
+ data: suggestion
257
+ };
258
+ }
259
+ return {
260
+ ok: false,
261
+ code: "unsupported-command",
262
+ message: `Task Engine does not support command '${command.name}'`
263
+ };
29
264
  }
30
265
  };
@@ -0,0 +1,21 @@
1
+ import type { TransitionEvidence, TransitionGuard } from "./types.js";
2
+ import { TransitionValidator } from "./transitions.js";
3
+ import { TaskStore } from "./store.js";
4
+ export type TransitionRequest = {
5
+ taskId: string;
6
+ action: string;
7
+ actor?: string;
8
+ };
9
+ export type TransitionResult = {
10
+ evidence: TransitionEvidence;
11
+ autoUnblocked: TransitionEvidence[];
12
+ };
13
+ export declare class TransitionService {
14
+ private readonly store;
15
+ private readonly validator;
16
+ constructor(store: TaskStore, customGuards?: TransitionGuard[]);
17
+ runTransition(request: TransitionRequest): Promise<TransitionResult>;
18
+ private autoUnblock;
19
+ getStore(): TaskStore;
20
+ getValidator(): TransitionValidator;
21
+ }
@@ -0,0 +1,105 @@
1
+ import crypto from "node:crypto";
2
+ import { TaskEngineError, TransitionValidator, getTransitionAction, resolveTargetState } from "./transitions.js";
3
+ export class TransitionService {
4
+ store;
5
+ validator;
6
+ constructor(store, customGuards = []) {
7
+ this.store = store;
8
+ this.validator = new TransitionValidator(customGuards);
9
+ }
10
+ async runTransition(request) {
11
+ const task = this.store.getTask(request.taskId);
12
+ if (!task) {
13
+ throw new TaskEngineError("task-not-found", `Task '${request.taskId}' not found`);
14
+ }
15
+ const targetState = resolveTargetState(task.status, request.action);
16
+ if (!targetState) {
17
+ throw new TaskEngineError("invalid-transition", `Action '${request.action}' is not valid from state '${task.status}'`);
18
+ }
19
+ const timestamp = new Date().toISOString();
20
+ const context = {
21
+ allTasks: this.store.getAllTasks(),
22
+ timestamp,
23
+ actor: request.actor
24
+ };
25
+ const validation = this.validator.validate(task, targetState, context);
26
+ if (!validation.allowed) {
27
+ const rejection = validation.guardResults.find((r) => !r.allowed);
28
+ if (rejection?.code === "dependency-unsatisfied") {
29
+ throw new TaskEngineError("dependency-unsatisfied", rejection.message ?? "Dependencies not satisfied");
30
+ }
31
+ if (rejection?.code === "invalid-transition") {
32
+ throw new TaskEngineError("invalid-transition", rejection.message ?? "Invalid transition");
33
+ }
34
+ throw new TaskEngineError("guard-rejected", rejection?.message ?? "Transition rejected by guard");
35
+ }
36
+ const fromState = task.status;
37
+ const updatedTask = {
38
+ ...task,
39
+ status: targetState,
40
+ updatedAt: timestamp
41
+ };
42
+ this.store.updateTask(updatedTask);
43
+ const action = getTransitionAction(fromState, targetState) ?? request.action;
44
+ const autoUnblockResults = targetState === "completed"
45
+ ? this.autoUnblock(request.taskId, timestamp, request.actor)
46
+ : [];
47
+ const evidence = {
48
+ transitionId: `${request.taskId}-${timestamp}-${crypto.randomUUID().slice(0, 8)}`,
49
+ taskId: request.taskId,
50
+ fromState,
51
+ toState: targetState,
52
+ action,
53
+ guardResults: validation.guardResults,
54
+ dependentsUnblocked: autoUnblockResults.map((r) => r.taskId),
55
+ timestamp,
56
+ actor: request.actor
57
+ };
58
+ this.store.addEvidence(evidence);
59
+ for (const unblockEvidence of autoUnblockResults) {
60
+ this.store.addEvidence(unblockEvidence);
61
+ }
62
+ await this.store.save();
63
+ return { evidence, autoUnblocked: autoUnblockResults };
64
+ }
65
+ autoUnblock(completedTaskId, timestamp, actor) {
66
+ const results = [];
67
+ const allTasks = this.store.getAllTasks();
68
+ const dependents = allTasks.filter((t) => t.status === "blocked" && t.dependsOn?.includes(completedTaskId));
69
+ for (const dependent of dependents) {
70
+ const deps = dependent.dependsOn ?? [];
71
+ const allDepsComplete = deps.every((depId) => {
72
+ if (depId === completedTaskId)
73
+ return true;
74
+ const depTask = this.store.getTask(depId);
75
+ return depTask?.status === "completed";
76
+ });
77
+ if (!allDepsComplete)
78
+ continue;
79
+ const updatedDependent = {
80
+ ...dependent,
81
+ status: "ready",
82
+ updatedAt: timestamp
83
+ };
84
+ this.store.updateTask(updatedDependent);
85
+ results.push({
86
+ transitionId: `${dependent.id}-${timestamp}-${crypto.randomUUID().slice(0, 8)}`,
87
+ taskId: dependent.id,
88
+ fromState: "blocked",
89
+ toState: "ready",
90
+ action: "unblock",
91
+ guardResults: [{ allowed: true, guardName: "auto-unblock" }],
92
+ dependentsUnblocked: [],
93
+ timestamp,
94
+ actor
95
+ });
96
+ }
97
+ return results;
98
+ }
99
+ getStore() {
100
+ return this.store;
101
+ }
102
+ getValidator() {
103
+ return this.validator;
104
+ }
105
+ }
@@ -0,0 +1,16 @@
1
+ import type { TaskEntity, TransitionEvidence } from "./types.js";
2
+ export declare class TaskStore {
3
+ private document;
4
+ private readonly filePath;
5
+ constructor(workspacePath: string, storePath?: string);
6
+ load(): Promise<void>;
7
+ save(): Promise<void>;
8
+ getAllTasks(): TaskEntity[];
9
+ getTask(id: string): TaskEntity | undefined;
10
+ addTask(task: TaskEntity): void;
11
+ updateTask(task: TaskEntity): void;
12
+ addEvidence(evidence: TransitionEvidence): void;
13
+ getTransitionLog(): TransitionEvidence[];
14
+ replaceAllTasks(tasks: TaskEntity[]): void;
15
+ getFilePath(): string;
16
+ }
@@ -0,0 +1,88 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import crypto from "node:crypto";
4
+ import { TaskEngineError } from "./transitions.js";
5
+ const DEFAULT_STORE_PATH = ".workspace-kit/tasks/state.json";
6
+ function emptyStore() {
7
+ return {
8
+ schemaVersion: 1,
9
+ tasks: [],
10
+ transitionLog: [],
11
+ lastUpdated: new Date().toISOString()
12
+ };
13
+ }
14
+ export class TaskStore {
15
+ document;
16
+ filePath;
17
+ constructor(workspacePath, storePath) {
18
+ this.filePath = path.resolve(workspacePath, storePath ?? DEFAULT_STORE_PATH);
19
+ this.document = emptyStore();
20
+ }
21
+ async load() {
22
+ try {
23
+ const raw = await fs.readFile(this.filePath, "utf8");
24
+ const parsed = JSON.parse(raw);
25
+ if (parsed.schemaVersion !== 1) {
26
+ throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
27
+ }
28
+ this.document = parsed;
29
+ }
30
+ catch (err) {
31
+ if (err.code === "ENOENT") {
32
+ this.document = emptyStore();
33
+ return;
34
+ }
35
+ if (err instanceof TaskEngineError)
36
+ throw err;
37
+ throw new TaskEngineError("storage-read-error", `Failed to read task store: ${err.message}`);
38
+ }
39
+ }
40
+ async save() {
41
+ this.document.lastUpdated = new Date().toISOString();
42
+ const dir = path.dirname(this.filePath);
43
+ const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
44
+ try {
45
+ await fs.mkdir(dir, { recursive: true });
46
+ await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
47
+ await fs.rename(tmpPath, this.filePath);
48
+ }
49
+ catch (err) {
50
+ try {
51
+ await fs.unlink(tmpPath);
52
+ }
53
+ catch { /* cleanup best-effort */ }
54
+ throw new TaskEngineError("storage-write-error", `Failed to write task store: ${err.message}`);
55
+ }
56
+ }
57
+ getAllTasks() {
58
+ return [...this.document.tasks];
59
+ }
60
+ getTask(id) {
61
+ return this.document.tasks.find((t) => t.id === id);
62
+ }
63
+ addTask(task) {
64
+ if (this.document.tasks.some((t) => t.id === task.id)) {
65
+ throw new TaskEngineError("duplicate-task-id", `Task '${task.id}' already exists`);
66
+ }
67
+ this.document.tasks.push({ ...task });
68
+ }
69
+ updateTask(task) {
70
+ const idx = this.document.tasks.findIndex((t) => t.id === task.id);
71
+ if (idx === -1) {
72
+ throw new TaskEngineError("task-not-found", `Task '${task.id}' not found`);
73
+ }
74
+ this.document.tasks[idx] = { ...task };
75
+ }
76
+ addEvidence(evidence) {
77
+ this.document.transitionLog.push(evidence);
78
+ }
79
+ getTransitionLog() {
80
+ return [...this.document.transitionLog];
81
+ }
82
+ replaceAllTasks(tasks) {
83
+ this.document.tasks = tasks.map((t) => ({ ...t }));
84
+ }
85
+ getFilePath() {
86
+ return this.filePath;
87
+ }
88
+ }
@@ -0,0 +1,2 @@
1
+ import type { TaskEntity, NextActionSuggestion } from "./types.js";
2
+ export declare function getNextActions(tasks: TaskEntity[]): NextActionSuggestion;
@@ -0,0 +1,51 @@
1
+ const PRIORITY_ORDER = {
2
+ P1: 0,
3
+ P2: 1,
4
+ P3: 2
5
+ };
6
+ function priorityRank(task) {
7
+ return PRIORITY_ORDER[task.priority ?? ""] ?? 99;
8
+ }
9
+ function buildStateSummary(tasks) {
10
+ const counts = {
11
+ proposed: 0,
12
+ ready: 0,
13
+ in_progress: 0,
14
+ blocked: 0,
15
+ completed: 0,
16
+ cancelled: 0
17
+ };
18
+ for (const task of tasks) {
19
+ counts[task.status]++;
20
+ }
21
+ return { ...counts, total: tasks.length };
22
+ }
23
+ function buildBlockingAnalysis(tasks) {
24
+ const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
25
+ const entries = [];
26
+ for (const task of tasks) {
27
+ if (task.status !== "blocked")
28
+ continue;
29
+ const deps = task.dependsOn ?? [];
30
+ const blockedBy = deps.filter((depId) => !completedIds.has(depId));
31
+ if (blockedBy.length > 0) {
32
+ entries.push({
33
+ taskId: task.id,
34
+ blockedBy,
35
+ blockingCount: blockedBy.length
36
+ });
37
+ }
38
+ }
39
+ return entries.sort((a, b) => b.blockingCount - a.blockingCount);
40
+ }
41
+ export function getNextActions(tasks) {
42
+ const readyQueue = tasks
43
+ .filter((t) => t.status === "ready")
44
+ .sort((a, b) => priorityRank(a) - priorityRank(b));
45
+ return {
46
+ readyQueue,
47
+ suggestedNext: readyQueue[0] ?? null,
48
+ stateSummary: buildStateSummary(tasks),
49
+ blockingAnalysis: buildBlockingAnalysis(tasks)
50
+ };
51
+ }
@@ -0,0 +1,23 @@
1
+ import type { TaskEntity, TaskStatus, TransitionGuard, GuardResult, TransitionContext, TaskEngineErrorCode } from "./types.js";
2
+ export declare class TaskEngineError extends Error {
3
+ readonly code: TaskEngineErrorCode;
4
+ constructor(code: TaskEngineErrorCode, message: string);
5
+ }
6
+ export declare function isTransitionAllowed(from: TaskStatus, to: TaskStatus): boolean;
7
+ export declare function getTransitionAction(from: TaskStatus, to: TaskStatus): string | undefined;
8
+ export declare function resolveTargetState(from: TaskStatus, action: string): TaskStatus | undefined;
9
+ export declare function getAllowedTransitionsFrom(status: TaskStatus): {
10
+ to: TaskStatus;
11
+ action: string;
12
+ }[];
13
+ export declare const stateValidityGuard: TransitionGuard;
14
+ export declare const dependencyCheckGuard: TransitionGuard;
15
+ export declare class TransitionValidator {
16
+ private readonly guards;
17
+ constructor(customGuards?: TransitionGuard[]);
18
+ validate(task: TaskEntity, targetState: TaskStatus, context: TransitionContext): {
19
+ allowed: boolean;
20
+ guardResults: GuardResult[];
21
+ };
22
+ getGuards(): TransitionGuard[];
23
+ }
@@ -0,0 +1,109 @@
1
+ export class TaskEngineError extends Error {
2
+ code;
3
+ constructor(code, message) {
4
+ super(message);
5
+ this.name = "TaskEngineError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ const ALLOWED_TRANSITIONS = {
10
+ "proposed->ready": { action: "accept" },
11
+ "proposed->cancelled": { action: "reject" },
12
+ "ready->in_progress": { action: "start" },
13
+ "ready->blocked": { action: "block" },
14
+ "ready->cancelled": { action: "cancel" },
15
+ "in_progress->completed": { action: "complete" },
16
+ "in_progress->blocked": { action: "block" },
17
+ "in_progress->ready": { action: "pause" },
18
+ "blocked->ready": { action: "unblock" },
19
+ "blocked->cancelled": { action: "cancel" }
20
+ };
21
+ export function isTransitionAllowed(from, to) {
22
+ return `${from}->${to}` in ALLOWED_TRANSITIONS;
23
+ }
24
+ export function getTransitionAction(from, to) {
25
+ return ALLOWED_TRANSITIONS[`${from}->${to}`]?.action;
26
+ }
27
+ export function resolveTargetState(from, action) {
28
+ for (const [key, entry] of Object.entries(ALLOWED_TRANSITIONS)) {
29
+ if (entry.action === action && key.startsWith(`${from}->`)) {
30
+ return key.split("->")[1];
31
+ }
32
+ }
33
+ return undefined;
34
+ }
35
+ export function getAllowedTransitionsFrom(status) {
36
+ const results = [];
37
+ for (const [key, entry] of Object.entries(ALLOWED_TRANSITIONS)) {
38
+ const [from, to] = key.split("->");
39
+ if (from === status) {
40
+ results.push({ to, action: entry.action });
41
+ }
42
+ }
43
+ return results;
44
+ }
45
+ export const stateValidityGuard = {
46
+ name: "state-validity",
47
+ canTransition(task, targetState) {
48
+ if (isTransitionAllowed(task.status, targetState)) {
49
+ return { allowed: true, guardName: "state-validity" };
50
+ }
51
+ return {
52
+ allowed: false,
53
+ guardName: "state-validity",
54
+ code: "invalid-transition",
55
+ message: `Transition from '${task.status}' to '${targetState}' is not allowed`
56
+ };
57
+ }
58
+ };
59
+ export const dependencyCheckGuard = {
60
+ name: "dependency-check",
61
+ canTransition(task, targetState, context) {
62
+ const needsDepCheck = (task.status === "ready" && targetState === "in_progress") ||
63
+ (task.status === "blocked" && targetState === "ready");
64
+ if (!needsDepCheck) {
65
+ return { allowed: true, guardName: "dependency-check" };
66
+ }
67
+ const deps = task.dependsOn ?? [];
68
+ if (deps.length === 0) {
69
+ return { allowed: true, guardName: "dependency-check" };
70
+ }
71
+ const taskMap = new Map(context.allTasks.map((t) => [t.id, t]));
72
+ const unsatisfied = [];
73
+ for (const depId of deps) {
74
+ const depTask = taskMap.get(depId);
75
+ if (!depTask || depTask.status !== "completed") {
76
+ unsatisfied.push(depId);
77
+ }
78
+ }
79
+ if (unsatisfied.length > 0) {
80
+ return {
81
+ allowed: false,
82
+ guardName: "dependency-check",
83
+ code: "dependency-unsatisfied",
84
+ message: `Dependencies not satisfied: ${unsatisfied.join(", ")}`
85
+ };
86
+ }
87
+ return { allowed: true, guardName: "dependency-check" };
88
+ }
89
+ };
90
+ export class TransitionValidator {
91
+ guards;
92
+ constructor(customGuards = []) {
93
+ this.guards = [stateValidityGuard, dependencyCheckGuard, ...customGuards];
94
+ }
95
+ validate(task, targetState, context) {
96
+ const guardResults = [];
97
+ for (const guard of this.guards) {
98
+ const result = guard.canTransition(task, targetState, context);
99
+ guardResults.push(result);
100
+ if (!result.allowed) {
101
+ return { allowed: false, guardResults };
102
+ }
103
+ }
104
+ return { allowed: true, guardResults };
105
+ }
106
+ getGuards() {
107
+ return [...this.guards];
108
+ }
109
+ }