@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.
@@ -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";
@@ -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,5 @@
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 {};
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",