@workflow-cannon/workspace-kit 0.1.0 → 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/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +1 -0
- package/dist/cli.js +60 -2
- package/dist/contracts/index.d.ts +1 -0
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/module-contract.d.ts +62 -0
- package/dist/contracts/module-contract.js +1 -0
- package/dist/core/index.d.ts +3 -0
- package/dist/core/index.js +2 -0
- package/dist/core/module-command-router.d.ts +27 -0
- package/dist/core/module-command-router.js +84 -0
- package/dist/core/module-registry.d.ts +24 -0
- package/dist/core/module-registry.js +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/modules/approvals/index.d.ts +2 -0
- package/dist/modules/approvals/index.js +30 -0
- package/dist/modules/documentation/index.d.ts +3 -0
- package/dist/modules/documentation/index.js +98 -0
- package/dist/modules/documentation/runtime.d.ts +12 -0
- package/dist/modules/documentation/runtime.js +368 -0
- package/dist/modules/documentation/types.d.ts +46 -0
- package/dist/modules/documentation/types.js +1 -0
- package/dist/modules/improvement/index.d.ts +2 -0
- package/dist/modules/improvement/index.js +30 -0
- package/dist/modules/index.d.ts +7 -0
- package/dist/modules/index.js +5 -0
- package/dist/modules/planning/index.d.ts +2 -0
- package/dist/modules/planning/index.js +30 -0
- package/dist/modules/task-engine/generator.d.ts +2 -0
- package/dist/modules/task-engine/generator.js +101 -0
- package/dist/modules/task-engine/importer.d.ts +8 -0
- package/dist/modules/task-engine/importer.js +157 -0
- package/dist/modules/task-engine/index.d.ts +9 -0
- package/dist/modules/task-engine/index.js +253 -0
- package/dist/modules/task-engine/service.d.ts +21 -0
- package/dist/modules/task-engine/service.js +105 -0
- package/dist/modules/task-engine/store.d.ts +16 -0
- package/dist/modules/task-engine/store.js +88 -0
- package/dist/modules/task-engine/suggestions.d.ts +2 -0
- package/dist/modules/task-engine/suggestions.js +51 -0
- package/dist/modules/task-engine/transitions.d.ts +23 -0
- package/dist/modules/task-engine/transitions.js +109 -0
- package/dist/modules/task-engine/types.d.ts +82 -0
- package/dist/modules/task-engine/types.js +1 -0
- package/dist/ops/index.d.ts +1 -0
- package/dist/ops/index.js +1 -0
- package/package.json +4 -2
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export type TaskStatus = "proposed" | "ready" | "in_progress" | "blocked" | "completed" | "cancelled";
|
|
2
|
+
export type TaskPriority = "P1" | "P2" | "P3";
|
|
3
|
+
export type TaskEntity = {
|
|
4
|
+
id: string;
|
|
5
|
+
status: TaskStatus;
|
|
6
|
+
type: string;
|
|
7
|
+
title: string;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
updatedAt: string;
|
|
10
|
+
priority?: TaskPriority;
|
|
11
|
+
dependsOn?: string[];
|
|
12
|
+
unblocks?: string[];
|
|
13
|
+
phase?: string;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
ownership?: string;
|
|
16
|
+
approach?: string;
|
|
17
|
+
technicalScope?: string[];
|
|
18
|
+
acceptanceCriteria?: string[];
|
|
19
|
+
};
|
|
20
|
+
export type GuardResult = {
|
|
21
|
+
allowed: boolean;
|
|
22
|
+
guardName: string;
|
|
23
|
+
code?: string;
|
|
24
|
+
message?: string;
|
|
25
|
+
};
|
|
26
|
+
export type TransitionContext = {
|
|
27
|
+
allTasks: TaskEntity[];
|
|
28
|
+
timestamp: string;
|
|
29
|
+
actor?: string;
|
|
30
|
+
};
|
|
31
|
+
export type TransitionGuard = {
|
|
32
|
+
name: string;
|
|
33
|
+
canTransition: (task: TaskEntity, targetState: TaskStatus, context: TransitionContext) => GuardResult;
|
|
34
|
+
};
|
|
35
|
+
export type TransitionEvidence = {
|
|
36
|
+
transitionId: string;
|
|
37
|
+
taskId: string;
|
|
38
|
+
fromState: TaskStatus;
|
|
39
|
+
toState: TaskStatus;
|
|
40
|
+
action: string;
|
|
41
|
+
guardResults: GuardResult[];
|
|
42
|
+
dependentsUnblocked: string[];
|
|
43
|
+
timestamp: string;
|
|
44
|
+
actor?: string;
|
|
45
|
+
};
|
|
46
|
+
export type TaskStoreDocument = {
|
|
47
|
+
schemaVersion: 1;
|
|
48
|
+
tasks: TaskEntity[];
|
|
49
|
+
transitionLog: TransitionEvidence[];
|
|
50
|
+
lastUpdated: string;
|
|
51
|
+
};
|
|
52
|
+
export type TaskEngineError = {
|
|
53
|
+
code: TaskEngineErrorCode;
|
|
54
|
+
message: string;
|
|
55
|
+
};
|
|
56
|
+
export type TaskEngineErrorCode = "invalid-transition" | "guard-rejected" | "dependency-unsatisfied" | "task-not-found" | "duplicate-task-id" | "invalid-task-schema" | "storage-read-error" | "storage-write-error" | "invalid-adapter" | "import-parse-error";
|
|
57
|
+
export type TaskAdapter = {
|
|
58
|
+
name: string;
|
|
59
|
+
supports: () => TaskAdapterCapability[];
|
|
60
|
+
load: () => Promise<TaskEntity[]>;
|
|
61
|
+
save?: (tasks: TaskEntity[]) => Promise<void>;
|
|
62
|
+
};
|
|
63
|
+
export type TaskAdapterCapability = "read" | "write" | "watch";
|
|
64
|
+
export type NextActionSuggestion = {
|
|
65
|
+
readyQueue: TaskEntity[];
|
|
66
|
+
suggestedNext: TaskEntity | null;
|
|
67
|
+
stateSummary: {
|
|
68
|
+
proposed: number;
|
|
69
|
+
ready: number;
|
|
70
|
+
in_progress: number;
|
|
71
|
+
blocked: number;
|
|
72
|
+
completed: number;
|
|
73
|
+
cancelled: number;
|
|
74
|
+
total: number;
|
|
75
|
+
};
|
|
76
|
+
blockingAnalysis: BlockingAnalysisEntry[];
|
|
77
|
+
};
|
|
78
|
+
export type BlockingAnalysisEntry = {
|
|
79
|
+
taskId: string;
|
|
80
|
+
blockedBy: string[];
|
|
81
|
+
blockingCount: number;
|
|
82
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type OpsVersion = "0.1";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@workflow-cannon/workspace-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "pnpm@10.0.0",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,9 @@
|
|
|
22
22
|
"check": "tsc -p tsconfig.json --noEmit",
|
|
23
23
|
"clean": "rm -rf dist",
|
|
24
24
|
"test": "pnpm run build && node --test test/**/*.test.mjs",
|
|
25
|
-
"pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack"
|
|
25
|
+
"pack:dry-run": "pnpm run build && pnpm pack --pack-destination ./artifacts/workspace-kit-pack",
|
|
26
|
+
"check-release-metadata": "node scripts/check-release-metadata.mjs",
|
|
27
|
+
"parity": "node scripts/run-parity.mjs"
|
|
26
28
|
},
|
|
27
29
|
"devDependencies": {
|
|
28
30
|
"@types/node": "^25.5.0",
|