@workflow-cannon/workspace-kit 0.14.0 → 0.15.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/dist/modules/index.d.ts +2 -0
- package/dist/modules/index.js +1 -0
- package/dist/modules/task-engine/index.d.ts +3 -0
- package/dist/modules/task-engine/index.js +316 -4
- package/dist/modules/task-engine/wishlist-store.d.ts +14 -0
- package/dist/modules/task-engine/wishlist-store.js +86 -0
- package/dist/modules/task-engine/wishlist-types.d.ts +33 -0
- package/dist/modules/task-engine/wishlist-types.js +5 -0
- package/dist/modules/task-engine/wishlist-validation.d.ts +16 -0
- package/dist/modules/task-engine/wishlist-validation.js +96 -0
- package/package.json +1 -1
package/dist/modules/index.d.ts
CHANGED
|
@@ -7,3 +7,5 @@ export { workspaceConfigModule } from "./workspace-config/index.js";
|
|
|
7
7
|
export { planningModule } from "./planning/index.js";
|
|
8
8
|
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, getNextActions } from "./task-engine/index.js";
|
|
9
9
|
export type { TaskEntity, TaskStatus, TaskPriority, TaskStoreDocument, TransitionEvidence, TransitionGuard, TransitionContext, GuardResult, TaskEngineErrorCode, TaskAdapter, TaskAdapterCapability, NextActionSuggestion, BlockingAnalysisEntry } from "./task-engine/index.js";
|
|
10
|
+
export type { WishlistItem, WishlistStatus, WishlistStoreDocument } from "./task-engine/index.js";
|
|
11
|
+
export { WishlistStore, validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./task-engine/index.js";
|
package/dist/modules/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { computeHeuristicConfidence, HEURISTIC_1_ADMISSION_THRESHOLD, shouldAdmi
|
|
|
5
5
|
export { workspaceConfigModule } from "./workspace-config/index.js";
|
|
6
6
|
export { planningModule } from "./planning/index.js";
|
|
7
7
|
export { taskEngineModule, TaskStore, TransitionService, TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard, getNextActions } from "./task-engine/index.js";
|
|
8
|
+
export { WishlistStore, validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./task-engine/index.js";
|
|
@@ -5,4 +5,7 @@ export { TransitionService } from "./service.js";
|
|
|
5
5
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
6
6
|
export { getNextActions } from "./suggestions.js";
|
|
7
7
|
export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
8
|
+
export { WishlistStore } from "./wishlist-store.js";
|
|
9
|
+
export type { WishlistItem, WishlistStatus, WishlistStoreDocument } from "./wishlist-types.js";
|
|
10
|
+
export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
8
11
|
export declare const taskEngineModule: WorkflowModule;
|
|
@@ -5,11 +5,15 @@ import { TransitionService } from "./service.js";
|
|
|
5
5
|
import { TaskEngineError, getAllowedTransitionsFrom } from "./transitions.js";
|
|
6
6
|
import { getNextActions } from "./suggestions.js";
|
|
7
7
|
import { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
8
|
+
import { WishlistStore } from "./wishlist-store.js";
|
|
9
|
+
import { buildWishlistItemFromIntake, validateWishlistIntakePayload, validateWishlistUpdatePayload, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
8
10
|
export { TaskStore } from "./store.js";
|
|
9
11
|
export { TransitionService } from "./service.js";
|
|
10
12
|
export { TaskEngineError, TransitionValidator, isTransitionAllowed, getTransitionAction, resolveTargetState, getAllowedTransitionsFrom, stateValidityGuard, dependencyCheckGuard } from "./transitions.js";
|
|
11
13
|
export { getNextActions } from "./suggestions.js";
|
|
12
14
|
export { readWorkspaceStatusSnapshot } from "./dashboard-status.js";
|
|
15
|
+
export { WishlistStore } from "./wishlist-store.js";
|
|
16
|
+
export { validateWishlistIntakePayload, validateWishlistUpdatePayload, buildWishlistItemFromIntake, WISHLIST_ID_RE } from "./wishlist-validation.js";
|
|
13
17
|
function taskStorePath(ctx) {
|
|
14
18
|
const tasks = ctx.effectiveConfig?.tasks;
|
|
15
19
|
if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
|
|
@@ -18,6 +22,14 @@ function taskStorePath(ctx) {
|
|
|
18
22
|
const p = tasks.storeRelativePath;
|
|
19
23
|
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
20
24
|
}
|
|
25
|
+
function wishlistStorePath(ctx) {
|
|
26
|
+
const tasks = ctx.effectiveConfig?.tasks;
|
|
27
|
+
if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const p = tasks.wishlistStoreRelativePath;
|
|
31
|
+
return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
|
|
32
|
+
}
|
|
21
33
|
const TASK_ID_RE = /^T\d+$/;
|
|
22
34
|
const MUTABLE_TASK_FIELDS = new Set([
|
|
23
35
|
"title",
|
|
@@ -35,6 +47,72 @@ const MUTABLE_TASK_FIELDS = new Set([
|
|
|
35
47
|
function nowIso() {
|
|
36
48
|
return new Date().toISOString();
|
|
37
49
|
}
|
|
50
|
+
function parseConversionDecomposition(raw) {
|
|
51
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
52
|
+
return { ok: false, message: "convert-wishlist requires 'decomposition' object" };
|
|
53
|
+
}
|
|
54
|
+
const o = raw;
|
|
55
|
+
const rationale = typeof o.rationale === "string" ? o.rationale.trim() : "";
|
|
56
|
+
const boundaries = typeof o.boundaries === "string" ? o.boundaries.trim() : "";
|
|
57
|
+
const dependencyIntent = typeof o.dependencyIntent === "string" ? o.dependencyIntent.trim() : "";
|
|
58
|
+
if (!rationale || !boundaries || !dependencyIntent) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
message: "decomposition requires non-empty rationale, boundaries, and dependencyIntent"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
return { ok: true, value: { rationale, boundaries, dependencyIntent } };
|
|
65
|
+
}
|
|
66
|
+
function buildTaskFromConversionPayload(row, timestamp) {
|
|
67
|
+
const id = typeof row.id === "string" ? row.id.trim() : "";
|
|
68
|
+
if (!TASK_ID_RE.test(id)) {
|
|
69
|
+
return { ok: false, message: "Each converted task requires 'id' matching T<number>" };
|
|
70
|
+
}
|
|
71
|
+
const title = typeof row.title === "string" ? row.title.trim() : "";
|
|
72
|
+
if (!title) {
|
|
73
|
+
return { ok: false, message: `Task '${id}' requires non-empty title` };
|
|
74
|
+
}
|
|
75
|
+
const phase = typeof row.phase === "string" ? row.phase.trim() : "";
|
|
76
|
+
if (!phase) {
|
|
77
|
+
return { ok: false, message: `Task '${id}' requires 'phase' for workable tasks` };
|
|
78
|
+
}
|
|
79
|
+
const type = typeof row.type === "string" && row.type.trim() ? row.type.trim() : "workspace-kit";
|
|
80
|
+
const priority = typeof row.priority === "string" && ["P1", "P2", "P3"].includes(row.priority)
|
|
81
|
+
? row.priority
|
|
82
|
+
: undefined;
|
|
83
|
+
const approach = typeof row.approach === "string" ? row.approach.trim() : "";
|
|
84
|
+
if (!approach) {
|
|
85
|
+
return { ok: false, message: `Task '${id}' requires 'approach'` };
|
|
86
|
+
}
|
|
87
|
+
const technicalScope = Array.isArray(row.technicalScope)
|
|
88
|
+
? row.technicalScope.filter((x) => typeof x === "string")
|
|
89
|
+
: [];
|
|
90
|
+
const acceptanceCriteria = Array.isArray(row.acceptanceCriteria)
|
|
91
|
+
? row.acceptanceCriteria.filter((x) => typeof x === "string")
|
|
92
|
+
: [];
|
|
93
|
+
if (technicalScope.length === 0) {
|
|
94
|
+
return { ok: false, message: `Task '${id}' requires non-empty technicalScope array` };
|
|
95
|
+
}
|
|
96
|
+
if (acceptanceCriteria.length === 0) {
|
|
97
|
+
return { ok: false, message: `Task '${id}' requires non-empty acceptanceCriteria array` };
|
|
98
|
+
}
|
|
99
|
+
const task = {
|
|
100
|
+
id,
|
|
101
|
+
title,
|
|
102
|
+
type,
|
|
103
|
+
status: "proposed",
|
|
104
|
+
createdAt: timestamp,
|
|
105
|
+
updatedAt: timestamp,
|
|
106
|
+
priority,
|
|
107
|
+
dependsOn: Array.isArray(row.dependsOn) ? row.dependsOn.filter((x) => typeof x === "string") : undefined,
|
|
108
|
+
unblocks: Array.isArray(row.unblocks) ? row.unblocks.filter((x) => typeof x === "string") : undefined,
|
|
109
|
+
phase,
|
|
110
|
+
approach,
|
|
111
|
+
technicalScope,
|
|
112
|
+
acceptanceCriteria
|
|
113
|
+
};
|
|
114
|
+
return { ok: true, task };
|
|
115
|
+
}
|
|
38
116
|
function mutationEvidence(mutationType, taskId, actor, details) {
|
|
39
117
|
return {
|
|
40
118
|
mutationId: `${mutationType}-${taskId}-${nowIso()}-${crypto.randomUUID().slice(0, 8)}`,
|
|
@@ -81,6 +159,11 @@ export const taskEngineModule = {
|
|
|
81
159
|
file: "update-task.md",
|
|
82
160
|
description: "Update mutable task fields without lifecycle bypass."
|
|
83
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: "update-wishlist",
|
|
164
|
+
file: "update-wishlist.md",
|
|
165
|
+
description: "Update mutable fields on an open Wishlist item."
|
|
166
|
+
},
|
|
84
167
|
{
|
|
85
168
|
name: "archive-task",
|
|
86
169
|
file: "archive-task.md",
|
|
@@ -126,6 +209,21 @@ export const taskEngineModule = {
|
|
|
126
209
|
file: "create-task-from-plan.md",
|
|
127
210
|
description: "Promote planning output into a canonical task."
|
|
128
211
|
},
|
|
212
|
+
{
|
|
213
|
+
name: "convert-wishlist",
|
|
214
|
+
file: "convert-wishlist.md",
|
|
215
|
+
description: "Convert a Wishlist item into one or more phased tasks and close the wishlist item."
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "create-wishlist",
|
|
219
|
+
file: "create-wishlist.md",
|
|
220
|
+
description: "Create a Wishlist ideation item with strict required fields (separate namespace from tasks)."
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: "get-wishlist",
|
|
224
|
+
file: "get-wishlist.md",
|
|
225
|
+
description: "Retrieve a single Wishlist item by ID."
|
|
226
|
+
},
|
|
129
227
|
{
|
|
130
228
|
name: "get-task",
|
|
131
229
|
file: "get-task.md",
|
|
@@ -136,6 +234,11 @@ export const taskEngineModule = {
|
|
|
136
234
|
file: "list-tasks.md",
|
|
137
235
|
description: "List tasks with optional status/phase filters."
|
|
138
236
|
},
|
|
237
|
+
{
|
|
238
|
+
name: "list-wishlist",
|
|
239
|
+
file: "list-wishlist.md",
|
|
240
|
+
description: "List Wishlist items (ideation-only; not part of task execution queues)."
|
|
241
|
+
},
|
|
139
242
|
{
|
|
140
243
|
name: "get-ready-queue",
|
|
141
244
|
file: "get-ready-queue.md",
|
|
@@ -463,6 +566,15 @@ export const taskEngineModule = {
|
|
|
463
566
|
phase: t.phase ?? null
|
|
464
567
|
}));
|
|
465
568
|
const blockedTop = suggestion.blockingAnalysis.slice(0, 15);
|
|
569
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
570
|
+
try {
|
|
571
|
+
await wishlistStore.load();
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
/* wishlist store optional */
|
|
575
|
+
}
|
|
576
|
+
const wishlistItems = wishlistStore.getAllItems();
|
|
577
|
+
const wishlistOpenCount = wishlistItems.filter((i) => i.status === "open").length;
|
|
466
578
|
const data = {
|
|
467
579
|
schemaVersion: 1,
|
|
468
580
|
taskStoreLastUpdated: store.getLastUpdated(),
|
|
@@ -470,6 +582,12 @@ export const taskEngineModule = {
|
|
|
470
582
|
stateSummary: suggestion.stateSummary,
|
|
471
583
|
readyQueueTop: readyTop,
|
|
472
584
|
readyQueueCount: suggestion.readyQueue.length,
|
|
585
|
+
executionPlanningScope: "tasks-only",
|
|
586
|
+
wishlist: {
|
|
587
|
+
schemaVersion: 1,
|
|
588
|
+
openCount: wishlistOpenCount,
|
|
589
|
+
totalCount: wishlistItems.length
|
|
590
|
+
},
|
|
473
591
|
blockedSummary: {
|
|
474
592
|
count: suggestion.blockingAnalysis.length,
|
|
475
593
|
top: blockedTop
|
|
@@ -507,7 +625,7 @@ export const taskEngineModule = {
|
|
|
507
625
|
ok: true,
|
|
508
626
|
code: "tasks-listed",
|
|
509
627
|
message: `Found ${tasks.length} tasks`,
|
|
510
|
-
data: { tasks, count: tasks.length }
|
|
628
|
+
data: { tasks, count: tasks.length, scope: "tasks-only" }
|
|
511
629
|
};
|
|
512
630
|
}
|
|
513
631
|
if (command.name === "get-ready-queue") {
|
|
@@ -523,7 +641,7 @@ export const taskEngineModule = {
|
|
|
523
641
|
ok: true,
|
|
524
642
|
code: "ready-queue-retrieved",
|
|
525
643
|
message: `${ready.length} tasks in ready queue`,
|
|
526
|
-
data: { tasks: ready, count: ready.length }
|
|
644
|
+
data: { tasks: ready, count: ready.length, scope: "tasks-only" }
|
|
527
645
|
};
|
|
528
646
|
}
|
|
529
647
|
if (command.name === "get-next-actions") {
|
|
@@ -535,7 +653,7 @@ export const taskEngineModule = {
|
|
|
535
653
|
message: suggestion.suggestedNext
|
|
536
654
|
? `Suggested next: ${suggestion.suggestedNext.id} — ${suggestion.suggestedNext.title}`
|
|
537
655
|
: "No tasks in ready queue",
|
|
538
|
-
data: suggestion
|
|
656
|
+
data: { ...suggestion, scope: "tasks-only" }
|
|
539
657
|
};
|
|
540
658
|
}
|
|
541
659
|
if (command.name === "get-task-summary") {
|
|
@@ -545,6 +663,7 @@ export const taskEngineModule = {
|
|
|
545
663
|
ok: true,
|
|
546
664
|
code: "task-summary",
|
|
547
665
|
data: {
|
|
666
|
+
scope: "tasks-only",
|
|
548
667
|
stateSummary: suggestion.stateSummary,
|
|
549
668
|
readyQueueCount: suggestion.readyQueue.length,
|
|
550
669
|
suggestedNext: suggestion.suggestedNext
|
|
@@ -565,10 +684,203 @@ export const taskEngineModule = {
|
|
|
565
684
|
code: "blocked-summary",
|
|
566
685
|
data: {
|
|
567
686
|
blockedCount: suggestion.blockingAnalysis.length,
|
|
568
|
-
blockedItems: suggestion.blockingAnalysis
|
|
687
|
+
blockedItems: suggestion.blockingAnalysis,
|
|
688
|
+
scope: "tasks-only"
|
|
569
689
|
}
|
|
570
690
|
};
|
|
571
691
|
}
|
|
692
|
+
if (command.name === "create-wishlist") {
|
|
693
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
694
|
+
await wishlistStore.load();
|
|
695
|
+
const raw = args;
|
|
696
|
+
const v = validateWishlistIntakePayload(raw);
|
|
697
|
+
if (!v.ok) {
|
|
698
|
+
return { ok: false, code: "invalid-task-schema", message: v.errors.join(" ") };
|
|
699
|
+
}
|
|
700
|
+
const ts = nowIso();
|
|
701
|
+
const item = buildWishlistItemFromIntake(raw, ts);
|
|
702
|
+
try {
|
|
703
|
+
wishlistStore.addItem(item);
|
|
704
|
+
}
|
|
705
|
+
catch (err) {
|
|
706
|
+
if (err instanceof TaskEngineError) {
|
|
707
|
+
return { ok: false, code: err.code, message: err.message };
|
|
708
|
+
}
|
|
709
|
+
throw err;
|
|
710
|
+
}
|
|
711
|
+
await wishlistStore.save();
|
|
712
|
+
return {
|
|
713
|
+
ok: true,
|
|
714
|
+
code: "wishlist-created",
|
|
715
|
+
message: `Created wishlist '${item.id}'`,
|
|
716
|
+
data: { item }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
if (command.name === "list-wishlist") {
|
|
720
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
721
|
+
await wishlistStore.load();
|
|
722
|
+
const statusFilter = typeof args.status === "string" ? args.status : undefined;
|
|
723
|
+
let items = wishlistStore.getAllItems();
|
|
724
|
+
if (statusFilter && ["open", "converted", "cancelled"].includes(statusFilter)) {
|
|
725
|
+
items = items.filter((i) => i.status === statusFilter);
|
|
726
|
+
}
|
|
727
|
+
return {
|
|
728
|
+
ok: true,
|
|
729
|
+
code: "wishlist-listed",
|
|
730
|
+
message: `Found ${items.length} wishlist items`,
|
|
731
|
+
data: { items, count: items.length, scope: "wishlist-only" }
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (command.name === "get-wishlist") {
|
|
735
|
+
const wishlistId = typeof args.wishlistId === "string" && args.wishlistId.trim().length > 0
|
|
736
|
+
? args.wishlistId.trim()
|
|
737
|
+
: typeof args.id === "string" && args.id.trim().length > 0
|
|
738
|
+
? args.id.trim()
|
|
739
|
+
: "";
|
|
740
|
+
if (!wishlistId) {
|
|
741
|
+
return { ok: false, code: "invalid-task-schema", message: "get-wishlist requires 'wishlistId' or 'id'" };
|
|
742
|
+
}
|
|
743
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
744
|
+
await wishlistStore.load();
|
|
745
|
+
const item = wishlistStore.getItem(wishlistId);
|
|
746
|
+
if (!item) {
|
|
747
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
748
|
+
}
|
|
749
|
+
return {
|
|
750
|
+
ok: true,
|
|
751
|
+
code: "wishlist-retrieved",
|
|
752
|
+
data: { item }
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
if (command.name === "update-wishlist") {
|
|
756
|
+
const wishlistId = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
|
|
757
|
+
const updates = typeof args.updates === "object" && args.updates !== null ? args.updates : undefined;
|
|
758
|
+
if (!wishlistId || !updates) {
|
|
759
|
+
return { ok: false, code: "invalid-task-schema", message: "update-wishlist requires wishlistId and updates" };
|
|
760
|
+
}
|
|
761
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
762
|
+
await wishlistStore.load();
|
|
763
|
+
const existing = wishlistStore.getItem(wishlistId);
|
|
764
|
+
if (!existing) {
|
|
765
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
766
|
+
}
|
|
767
|
+
if (existing.status !== "open") {
|
|
768
|
+
return { ok: false, code: "invalid-transition", message: "Only open wishlist items can be updated" };
|
|
769
|
+
}
|
|
770
|
+
const uv = validateWishlistUpdatePayload(updates);
|
|
771
|
+
if (!uv.ok) {
|
|
772
|
+
return { ok: false, code: "invalid-task-schema", message: uv.errors.join(" ") };
|
|
773
|
+
}
|
|
774
|
+
const merged = { ...existing, updatedAt: nowIso() };
|
|
775
|
+
const mutable = [
|
|
776
|
+
"title",
|
|
777
|
+
"problemStatement",
|
|
778
|
+
"expectedOutcome",
|
|
779
|
+
"impact",
|
|
780
|
+
"constraints",
|
|
781
|
+
"successSignals",
|
|
782
|
+
"requestor",
|
|
783
|
+
"evidenceRef"
|
|
784
|
+
];
|
|
785
|
+
for (const key of mutable) {
|
|
786
|
+
if (key in updates && typeof updates[key] === "string") {
|
|
787
|
+
merged[key] = updates[key].trim();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
wishlistStore.updateItem(merged);
|
|
791
|
+
await wishlistStore.save();
|
|
792
|
+
return {
|
|
793
|
+
ok: true,
|
|
794
|
+
code: "wishlist-updated",
|
|
795
|
+
message: `Updated wishlist '${wishlistId}'`,
|
|
796
|
+
data: { item: merged }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
if (command.name === "convert-wishlist") {
|
|
800
|
+
const wishlistId = typeof args.wishlistId === "string" ? args.wishlistId.trim() : "";
|
|
801
|
+
if (!wishlistId || !WISHLIST_ID_RE.test(wishlistId)) {
|
|
802
|
+
return {
|
|
803
|
+
ok: false,
|
|
804
|
+
code: "invalid-task-schema",
|
|
805
|
+
message: "convert-wishlist requires wishlistId matching W<number>"
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
const dec = parseConversionDecomposition(args.decomposition);
|
|
809
|
+
if (!dec.ok) {
|
|
810
|
+
return { ok: false, code: "invalid-task-schema", message: dec.message };
|
|
811
|
+
}
|
|
812
|
+
const tasksRaw = args.tasks;
|
|
813
|
+
if (!Array.isArray(tasksRaw) || tasksRaw.length === 0) {
|
|
814
|
+
return {
|
|
815
|
+
ok: false,
|
|
816
|
+
code: "invalid-task-schema",
|
|
817
|
+
message: "convert-wishlist requires non-empty tasks array"
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const wishlistStore = new WishlistStore(ctx.workspacePath, wishlistStorePath(ctx));
|
|
821
|
+
await wishlistStore.load();
|
|
822
|
+
const wlItem = wishlistStore.getItem(wishlistId);
|
|
823
|
+
if (!wlItem) {
|
|
824
|
+
return { ok: false, code: "task-not-found", message: `Wishlist item '${wishlistId}' not found` };
|
|
825
|
+
}
|
|
826
|
+
if (wlItem.status !== "open") {
|
|
827
|
+
return {
|
|
828
|
+
ok: false,
|
|
829
|
+
code: "invalid-transition",
|
|
830
|
+
message: "Only open wishlist items can be converted"
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
const actor = typeof args.actor === "string"
|
|
834
|
+
? args.actor
|
|
835
|
+
: ctx.resolvedActor !== undefined
|
|
836
|
+
? ctx.resolvedActor
|
|
837
|
+
: undefined;
|
|
838
|
+
const timestamp = nowIso();
|
|
839
|
+
const built = [];
|
|
840
|
+
for (const row of tasksRaw) {
|
|
841
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
842
|
+
return { ok: false, code: "invalid-task-schema", message: "Each task must be an object" };
|
|
843
|
+
}
|
|
844
|
+
const bt = buildTaskFromConversionPayload(row, timestamp);
|
|
845
|
+
if (!bt.ok) {
|
|
846
|
+
return { ok: false, code: "invalid-task-schema", message: bt.message };
|
|
847
|
+
}
|
|
848
|
+
if (store.getTask(bt.task.id)) {
|
|
849
|
+
return {
|
|
850
|
+
ok: false,
|
|
851
|
+
code: "duplicate-task-id",
|
|
852
|
+
message: `Task '${bt.task.id}' already exists`
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
built.push(bt.task);
|
|
856
|
+
}
|
|
857
|
+
for (const t of built) {
|
|
858
|
+
store.addTask(t);
|
|
859
|
+
store.addMutationEvidence(mutationEvidence("create-task", t.id, actor, {
|
|
860
|
+
initialStatus: t.status,
|
|
861
|
+
source: "convert-wishlist",
|
|
862
|
+
wishlistId
|
|
863
|
+
}));
|
|
864
|
+
}
|
|
865
|
+
const convertedIds = built.map((t) => t.id);
|
|
866
|
+
const updatedWishlist = {
|
|
867
|
+
...wlItem,
|
|
868
|
+
status: "converted",
|
|
869
|
+
updatedAt: timestamp,
|
|
870
|
+
convertedAt: timestamp,
|
|
871
|
+
convertedToTaskIds: convertedIds,
|
|
872
|
+
conversionDecomposition: dec.value
|
|
873
|
+
};
|
|
874
|
+
wishlistStore.updateItem(updatedWishlist);
|
|
875
|
+
await store.save();
|
|
876
|
+
await wishlistStore.save();
|
|
877
|
+
return {
|
|
878
|
+
ok: true,
|
|
879
|
+
code: "wishlist-converted",
|
|
880
|
+
message: `Converted wishlist '${wishlistId}' to tasks: ${convertedIds.join(", ")}`,
|
|
881
|
+
data: { wishlist: updatedWishlist, createdTasks: built }
|
|
882
|
+
};
|
|
883
|
+
}
|
|
572
884
|
return {
|
|
573
885
|
ok: false,
|
|
574
886
|
code: "unsupported-command",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { WishlistItem } from "./wishlist-types.js";
|
|
2
|
+
export declare class WishlistStore {
|
|
3
|
+
private document;
|
|
4
|
+
private readonly filePath;
|
|
5
|
+
constructor(workspacePath: string, storeRelativePath?: string);
|
|
6
|
+
load(): Promise<void>;
|
|
7
|
+
save(): Promise<void>;
|
|
8
|
+
getAllItems(): WishlistItem[];
|
|
9
|
+
getItem(id: string): WishlistItem | undefined;
|
|
10
|
+
addItem(item: WishlistItem): void;
|
|
11
|
+
updateItem(item: WishlistItem): void;
|
|
12
|
+
getFilePath(): string;
|
|
13
|
+
getLastUpdated(): string;
|
|
14
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
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_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
|
|
6
|
+
function emptyWishlistDoc() {
|
|
7
|
+
return {
|
|
8
|
+
schemaVersion: 1,
|
|
9
|
+
items: [],
|
|
10
|
+
lastUpdated: new Date().toISOString()
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export class WishlistStore {
|
|
14
|
+
document;
|
|
15
|
+
filePath;
|
|
16
|
+
constructor(workspacePath, storeRelativePath) {
|
|
17
|
+
this.filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
|
|
18
|
+
this.document = emptyWishlistDoc();
|
|
19
|
+
}
|
|
20
|
+
async load() {
|
|
21
|
+
try {
|
|
22
|
+
const raw = await fs.readFile(this.filePath, "utf8");
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
if (parsed.schemaVersion !== 1) {
|
|
25
|
+
throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema version: ${parsed.schemaVersion}`);
|
|
26
|
+
}
|
|
27
|
+
if (!Array.isArray(parsed.items)) {
|
|
28
|
+
throw new TaskEngineError("storage-read-error", "Wishlist store 'items' must be an array");
|
|
29
|
+
}
|
|
30
|
+
this.document = parsed;
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
if (err.code === "ENOENT") {
|
|
34
|
+
this.document = emptyWishlistDoc();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (err instanceof TaskEngineError)
|
|
38
|
+
throw err;
|
|
39
|
+
throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async save() {
|
|
43
|
+
this.document.lastUpdated = new Date().toISOString();
|
|
44
|
+
const dir = path.dirname(this.filePath);
|
|
45
|
+
const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
|
|
46
|
+
try {
|
|
47
|
+
await fs.mkdir(dir, { recursive: true });
|
|
48
|
+
await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
|
|
49
|
+
await fs.rename(tmpPath, this.filePath);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
try {
|
|
53
|
+
await fs.unlink(tmpPath);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* cleanup best-effort */
|
|
57
|
+
}
|
|
58
|
+
throw new TaskEngineError("storage-write-error", `Failed to write wishlist store: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
getAllItems() {
|
|
62
|
+
return [...this.document.items];
|
|
63
|
+
}
|
|
64
|
+
getItem(id) {
|
|
65
|
+
return this.document.items.find((i) => i.id === id);
|
|
66
|
+
}
|
|
67
|
+
addItem(item) {
|
|
68
|
+
if (this.document.items.some((i) => i.id === item.id)) {
|
|
69
|
+
throw new TaskEngineError("duplicate-task-id", `Wishlist item '${item.id}' already exists`);
|
|
70
|
+
}
|
|
71
|
+
this.document.items.push({ ...item });
|
|
72
|
+
}
|
|
73
|
+
updateItem(item) {
|
|
74
|
+
const idx = this.document.items.findIndex((i) => i.id === item.id);
|
|
75
|
+
if (idx === -1) {
|
|
76
|
+
throw new TaskEngineError("task-not-found", `Wishlist item '${item.id}' not found`);
|
|
77
|
+
}
|
|
78
|
+
this.document.items[idx] = { ...item };
|
|
79
|
+
}
|
|
80
|
+
getFilePath() {
|
|
81
|
+
return this.filePath;
|
|
82
|
+
}
|
|
83
|
+
getLastUpdated() {
|
|
84
|
+
return this.document.lastUpdated;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wishlist items live in a separate namespace from Task Engine tasks (`T###`).
|
|
3
|
+
* They are ideation-only until converted into canonical tasks via `convert-wishlist`.
|
|
4
|
+
*/
|
|
5
|
+
export type WishlistStatus = "open" | "converted" | "cancelled";
|
|
6
|
+
/** Recorded when a wishlist item is converted into one or more tasks. */
|
|
7
|
+
export type WishlistConversionDecomposition = {
|
|
8
|
+
rationale: string;
|
|
9
|
+
boundaries: string;
|
|
10
|
+
dependencyIntent: string;
|
|
11
|
+
};
|
|
12
|
+
export type WishlistItem = {
|
|
13
|
+
id: string;
|
|
14
|
+
status: WishlistStatus;
|
|
15
|
+
title: string;
|
|
16
|
+
problemStatement: string;
|
|
17
|
+
expectedOutcome: string;
|
|
18
|
+
impact: string;
|
|
19
|
+
constraints: string;
|
|
20
|
+
successSignals: string;
|
|
21
|
+
requestor: string;
|
|
22
|
+
evidenceRef: string;
|
|
23
|
+
createdAt: string;
|
|
24
|
+
updatedAt: string;
|
|
25
|
+
convertedAt?: string;
|
|
26
|
+
convertedToTaskIds?: string[];
|
|
27
|
+
conversionDecomposition?: WishlistConversionDecomposition;
|
|
28
|
+
};
|
|
29
|
+
export type WishlistStoreDocument = {
|
|
30
|
+
schemaVersion: 1;
|
|
31
|
+
items: WishlistItem[];
|
|
32
|
+
lastUpdated: string;
|
|
33
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { WishlistItem } from "./wishlist-types.js";
|
|
2
|
+
/** Wishlist identifiers use a dedicated namespace: `W` + digits (e.g. `W1`, `W42`). */
|
|
3
|
+
export declare const WISHLIST_ID_RE: RegExp;
|
|
4
|
+
export type WishlistValidationResult = {
|
|
5
|
+
ok: true;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
errors: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Validates intake fields for creating or replacing content on an open wishlist item.
|
|
12
|
+
* Wishlist items never carry a Task Engine `phase`; reject if present.
|
|
13
|
+
*/
|
|
14
|
+
export declare function validateWishlistIntakePayload(args: Record<string, unknown>): WishlistValidationResult;
|
|
15
|
+
export declare function buildWishlistItemFromIntake(args: Record<string, unknown>, timestamp: string): Omit<WishlistItem, "convertedAt" | "convertedToTaskIds" | "conversionDecomposition">;
|
|
16
|
+
export declare function validateWishlistUpdatePayload(updates: Record<string, unknown>): WishlistValidationResult;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/** Wishlist identifiers use a dedicated namespace: `W` + digits (e.g. `W1`, `W42`). */
|
|
2
|
+
export const WISHLIST_ID_RE = /^W\d+$/;
|
|
3
|
+
const REQUIRED_STRING_FIELDS = [
|
|
4
|
+
"title",
|
|
5
|
+
"problemStatement",
|
|
6
|
+
"expectedOutcome",
|
|
7
|
+
"impact",
|
|
8
|
+
"constraints",
|
|
9
|
+
"successSignals",
|
|
10
|
+
"requestor",
|
|
11
|
+
"evidenceRef"
|
|
12
|
+
];
|
|
13
|
+
function nonEmptyString(v, label) {
|
|
14
|
+
if (typeof v !== "string" || v.trim().length === 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return v.trim();
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Validates intake fields for creating or replacing content on an open wishlist item.
|
|
21
|
+
* Wishlist items never carry a Task Engine `phase`; reject if present.
|
|
22
|
+
*/
|
|
23
|
+
export function validateWishlistIntakePayload(args) {
|
|
24
|
+
const errors = [];
|
|
25
|
+
if ("phase" in args && args.phase !== undefined) {
|
|
26
|
+
errors.push("Wishlist items must not include 'phase'; only canonical tasks are phased.");
|
|
27
|
+
}
|
|
28
|
+
const id = typeof args.id === "string" ? args.id.trim() : "";
|
|
29
|
+
if (!id) {
|
|
30
|
+
errors.push("Wishlist 'id' is required.");
|
|
31
|
+
}
|
|
32
|
+
else if (!WISHLIST_ID_RE.test(id)) {
|
|
33
|
+
errors.push(`Wishlist 'id' must match ${WISHLIST_ID_RE.source} (e.g. W1).`);
|
|
34
|
+
}
|
|
35
|
+
for (const key of REQUIRED_STRING_FIELDS) {
|
|
36
|
+
const s = nonEmptyString(args[key], key);
|
|
37
|
+
if (s === null) {
|
|
38
|
+
errors.push(`Wishlist '${key}' is required and must be a non-empty string.`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (errors.length > 0) {
|
|
42
|
+
return { ok: false, errors };
|
|
43
|
+
}
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
export function buildWishlistItemFromIntake(args, timestamp) {
|
|
47
|
+
const id = args.id.trim();
|
|
48
|
+
const item = {
|
|
49
|
+
id,
|
|
50
|
+
status: "open",
|
|
51
|
+
title: args.title.trim(),
|
|
52
|
+
problemStatement: args.problemStatement.trim(),
|
|
53
|
+
expectedOutcome: args.expectedOutcome.trim(),
|
|
54
|
+
impact: args.impact.trim(),
|
|
55
|
+
constraints: args.constraints.trim(),
|
|
56
|
+
successSignals: args.successSignals.trim(),
|
|
57
|
+
requestor: args.requestor.trim(),
|
|
58
|
+
evidenceRef: args.evidenceRef.trim(),
|
|
59
|
+
createdAt: timestamp,
|
|
60
|
+
updatedAt: timestamp
|
|
61
|
+
};
|
|
62
|
+
return item;
|
|
63
|
+
}
|
|
64
|
+
export function validateWishlistUpdatePayload(updates) {
|
|
65
|
+
if ("phase" in updates && updates.phase !== undefined) {
|
|
66
|
+
return { ok: false, errors: ["Wishlist updates cannot set 'phase'."] };
|
|
67
|
+
}
|
|
68
|
+
const errors = [];
|
|
69
|
+
const allowed = new Set([
|
|
70
|
+
"title",
|
|
71
|
+
"problemStatement",
|
|
72
|
+
"expectedOutcome",
|
|
73
|
+
"impact",
|
|
74
|
+
"constraints",
|
|
75
|
+
"successSignals",
|
|
76
|
+
"requestor",
|
|
77
|
+
"evidenceRef"
|
|
78
|
+
]);
|
|
79
|
+
for (const key of Object.keys(updates)) {
|
|
80
|
+
if (!allowed.has(key)) {
|
|
81
|
+
errors.push(`Cannot update unknown or immutable wishlist field '${key}'.`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
for (const key of REQUIRED_STRING_FIELDS) {
|
|
85
|
+
if (key in updates) {
|
|
86
|
+
const s = nonEmptyString(updates[key], key);
|
|
87
|
+
if (s === null) {
|
|
88
|
+
errors.push(`Wishlist '${key}' must be a non-empty string when provided.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (errors.length > 0) {
|
|
93
|
+
return { ok: false, errors };
|
|
94
|
+
}
|
|
95
|
+
return { ok: true };
|
|
96
|
+
}
|