@workflow-cannon/workspace-kit 0.15.0 → 0.16.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.
@@ -0,0 +1,10 @@
1
+ import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ export type TaskPersistenceBackend = "json" | "sqlite";
3
+ export declare function getTaskPersistenceBackend(config: Record<string, unknown> | undefined): TaskPersistenceBackend;
4
+ export declare function planningTaskStoreRelativePath(ctx: {
5
+ effectiveConfig?: Record<string, unknown>;
6
+ }): string | undefined;
7
+ export declare function planningWishlistStoreRelativePath(ctx: {
8
+ effectiveConfig?: Record<string, unknown>;
9
+ }): string | undefined;
10
+ export declare function planningSqliteDatabaseRelativePath(ctx: ModuleLifecycleContext): string;
@@ -0,0 +1,37 @@
1
+ export function getTaskPersistenceBackend(config) {
2
+ const tasks = config?.tasks;
3
+ if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
4
+ return "json";
5
+ }
6
+ const b = tasks.persistenceBackend;
7
+ if (b === "sqlite") {
8
+ return "sqlite";
9
+ }
10
+ return "json";
11
+ }
12
+ export function planningTaskStoreRelativePath(ctx) {
13
+ const tasks = ctx.effectiveConfig?.tasks;
14
+ if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
15
+ return undefined;
16
+ }
17
+ const p = tasks.storeRelativePath;
18
+ return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
19
+ }
20
+ export function planningWishlistStoreRelativePath(ctx) {
21
+ const tasks = ctx.effectiveConfig?.tasks;
22
+ if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
23
+ return undefined;
24
+ }
25
+ const p = tasks.wishlistStoreRelativePath;
26
+ return typeof p === "string" && p.trim().length > 0 ? p.trim() : undefined;
27
+ }
28
+ export function planningSqliteDatabaseRelativePath(ctx) {
29
+ const tasks = ctx.effectiveConfig?.tasks;
30
+ if (!tasks || typeof tasks !== "object" || Array.isArray(tasks)) {
31
+ return ".workspace-kit/tasks/workspace-kit.db";
32
+ }
33
+ const p = tasks.sqliteDatabaseRelativePath;
34
+ return typeof p === "string" && p.trim().length > 0
35
+ ? p.trim()
36
+ : ".workspace-kit/tasks/workspace-kit.db";
37
+ }
@@ -0,0 +1,16 @@
1
+ import type { ModuleLifecycleContext } from "../../contracts/module-contract.js";
2
+ import { TaskStore } from "./store.js";
3
+ import { WishlistStore } from "./wishlist-store.js";
4
+ import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
5
+ export type OpenedPlanningStores = {
6
+ kind: "json";
7
+ taskStore: TaskStore;
8
+ sqliteDual: null;
9
+ openWishlist: () => Promise<WishlistStore>;
10
+ } | {
11
+ kind: "sqlite";
12
+ taskStore: TaskStore;
13
+ sqliteDual: SqliteDualPlanningStore;
14
+ openWishlist: () => Promise<WishlistStore>;
15
+ };
16
+ export declare function openPlanningStores(ctx: ModuleLifecycleContext): Promise<OpenedPlanningStores>;
@@ -0,0 +1,34 @@
1
+ import { TaskStore } from "./store.js";
2
+ import { WishlistStore } from "./wishlist-store.js";
3
+ import { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
4
+ import { getTaskPersistenceBackend, planningSqliteDatabaseRelativePath, planningTaskStoreRelativePath, planningWishlistStoreRelativePath } from "./planning-config.js";
5
+ export async function openPlanningStores(ctx) {
6
+ if (getTaskPersistenceBackend(ctx.effectiveConfig) === "sqlite") {
7
+ const dual = new SqliteDualPlanningStore(ctx.workspacePath, planningSqliteDatabaseRelativePath(ctx));
8
+ dual.loadFromDisk();
9
+ const taskStore = TaskStore.forSqliteDual(dual);
10
+ await taskStore.load(); // binds task document reference from dual
11
+ return {
12
+ kind: "sqlite",
13
+ sqliteDual: dual,
14
+ taskStore,
15
+ openWishlist: async () => {
16
+ const w = WishlistStore.forSqliteDual(dual);
17
+ await w.load();
18
+ return w;
19
+ }
20
+ };
21
+ }
22
+ const taskStore = TaskStore.forJsonFile(ctx.workspacePath, planningTaskStoreRelativePath(ctx));
23
+ await taskStore.load();
24
+ return {
25
+ kind: "json",
26
+ sqliteDual: null,
27
+ taskStore,
28
+ openWishlist: async () => {
29
+ const w = WishlistStore.forJsonFile(ctx.workspacePath, planningWishlistStoreRelativePath(ctx));
30
+ await w.load();
31
+ return w;
32
+ }
33
+ };
34
+ }
@@ -0,0 +1,21 @@
1
+ import type { TaskStoreDocument } from "./types.js";
2
+ import type { WishlistStoreDocument } from "./wishlist-types.js";
3
+ /** Single-file SQLite backing for task + wishlist JSON documents (atomic convert-wishlist). */
4
+ export declare class SqliteDualPlanningStore {
5
+ private db;
6
+ readonly dbPath: string;
7
+ private _taskDoc;
8
+ private _wishlistDoc;
9
+ constructor(workspacePath: string, databaseRelativePath: string);
10
+ get taskDocument(): TaskStoreDocument;
11
+ get wishlistDocument(): WishlistStoreDocument;
12
+ getDisplayPath(): string;
13
+ private ensureDb;
14
+ /** Load documents from an existing database file; otherwise start empty (no file created). */
15
+ loadFromDisk(): void;
16
+ /** Replace in-memory documents (used by migrate). */
17
+ seedFromDocuments(taskDoc: TaskStoreDocument, wishlistDoc: WishlistStoreDocument): void;
18
+ persistSync(): void;
19
+ /** Run synchronous work inside one SQLite transaction and flush both blobs at the end. */
20
+ withTransaction(work: () => void): void;
21
+ }
@@ -0,0 +1,137 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import Database from "better-sqlite3";
4
+ import { TaskEngineError } from "./transitions.js";
5
+ const DDL = `
6
+ CREATE TABLE IF NOT EXISTS workspace_planning_state (
7
+ id INTEGER PRIMARY KEY CHECK (id = 1),
8
+ task_store_json TEXT NOT NULL,
9
+ wishlist_store_json TEXT NOT NULL
10
+ );
11
+ `;
12
+ function emptyTaskStoreDocument() {
13
+ return {
14
+ schemaVersion: 1,
15
+ tasks: [],
16
+ transitionLog: [],
17
+ mutationLog: [],
18
+ lastUpdated: new Date().toISOString()
19
+ };
20
+ }
21
+ function emptyWishlistDocument() {
22
+ return {
23
+ schemaVersion: 1,
24
+ items: [],
25
+ lastUpdated: new Date().toISOString()
26
+ };
27
+ }
28
+ /** Single-file SQLite backing for task + wishlist JSON documents (atomic convert-wishlist). */
29
+ export class SqliteDualPlanningStore {
30
+ db = null;
31
+ dbPath;
32
+ _taskDoc;
33
+ _wishlistDoc;
34
+ constructor(workspacePath, databaseRelativePath) {
35
+ this.dbPath = path.resolve(workspacePath, databaseRelativePath);
36
+ this._taskDoc = emptyTaskStoreDocument();
37
+ this._wishlistDoc = emptyWishlistDocument();
38
+ }
39
+ get taskDocument() {
40
+ return this._taskDoc;
41
+ }
42
+ get wishlistDocument() {
43
+ return this._wishlistDoc;
44
+ }
45
+ getDisplayPath() {
46
+ return this.dbPath;
47
+ }
48
+ ensureDb() {
49
+ if (!this.db) {
50
+ const dir = path.dirname(this.dbPath);
51
+ fs.mkdirSync(dir, { recursive: true });
52
+ this.db = new Database(this.dbPath);
53
+ this.db.pragma("journal_mode = WAL");
54
+ this.db.exec(DDL);
55
+ }
56
+ return this.db;
57
+ }
58
+ /** Load documents from an existing database file; otherwise start empty (no file created). */
59
+ loadFromDisk() {
60
+ if (!fs.existsSync(this.dbPath)) {
61
+ this._taskDoc = emptyTaskStoreDocument();
62
+ this._wishlistDoc = emptyWishlistDocument();
63
+ return;
64
+ }
65
+ const db = this.ensureDb();
66
+ const row = db
67
+ .prepare("SELECT task_store_json, wishlist_store_json FROM workspace_planning_state WHERE id = 1")
68
+ .get();
69
+ if (!row) {
70
+ this._taskDoc = emptyTaskStoreDocument();
71
+ this._wishlistDoc = emptyWishlistDocument();
72
+ return;
73
+ }
74
+ try {
75
+ const taskParsed = JSON.parse(row.task_store_json);
76
+ const wishParsed = JSON.parse(row.wishlist_store_json);
77
+ if (taskParsed.schemaVersion !== 1) {
78
+ throw new TaskEngineError("storage-read-error", `Unsupported task store schema in SQLite: ${taskParsed.schemaVersion}`);
79
+ }
80
+ if (wishParsed.schemaVersion !== 1) {
81
+ throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema in SQLite: ${wishParsed.schemaVersion}`);
82
+ }
83
+ if (!Array.isArray(taskParsed.mutationLog)) {
84
+ taskParsed.mutationLog = [];
85
+ }
86
+ if (!Array.isArray(wishParsed.items)) {
87
+ throw new TaskEngineError("storage-read-error", "Wishlist items missing in SQLite row");
88
+ }
89
+ this._taskDoc = taskParsed;
90
+ this._wishlistDoc = wishParsed;
91
+ }
92
+ catch (err) {
93
+ if (err instanceof TaskEngineError) {
94
+ throw err;
95
+ }
96
+ throw new TaskEngineError("storage-read-error", `Failed to parse SQLite planning row: ${err.message}`);
97
+ }
98
+ }
99
+ /** Replace in-memory documents (used by migrate). */
100
+ seedFromDocuments(taskDoc, wishlistDoc) {
101
+ this._taskDoc = taskDoc;
102
+ this._wishlistDoc = wishlistDoc;
103
+ }
104
+ persistSync() {
105
+ this._taskDoc.lastUpdated = new Date().toISOString();
106
+ this._wishlistDoc.lastUpdated = new Date().toISOString();
107
+ const db = this.ensureDb();
108
+ const t = JSON.stringify(this._taskDoc);
109
+ const w = JSON.stringify(this._wishlistDoc);
110
+ const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
111
+ if (exists) {
112
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
113
+ }
114
+ else {
115
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
116
+ }
117
+ }
118
+ /** Run synchronous work inside one SQLite transaction and flush both blobs at the end. */
119
+ withTransaction(work) {
120
+ const db = this.ensureDb();
121
+ const txn = db.transaction(() => {
122
+ work();
123
+ this._taskDoc.lastUpdated = new Date().toISOString();
124
+ this._wishlistDoc.lastUpdated = new Date().toISOString();
125
+ const t = JSON.stringify(this._taskDoc);
126
+ const w = JSON.stringify(this._wishlistDoc);
127
+ const exists = db.prepare("SELECT 1 AS ok FROM workspace_planning_state WHERE id = 1").get();
128
+ if (exists) {
129
+ db.prepare("UPDATE workspace_planning_state SET task_store_json = ?, wishlist_store_json = ? WHERE id = 1").run(t, w);
130
+ }
131
+ else {
132
+ db.prepare("INSERT INTO workspace_planning_state (id, task_store_json, wishlist_store_json) VALUES (1, ?, ?)").run(t, w);
133
+ }
134
+ });
135
+ txn();
136
+ }
137
+ }
@@ -1,8 +1,17 @@
1
- import type { TaskEntity, TaskMutationEvidence, TransitionEvidence } from "./types.js";
1
+ import type { TaskEntity, TaskMutationEvidence, TaskStoreDocument, TransitionEvidence } from "./types.js";
2
+ import type { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
3
+ export declare const DEFAULT_TASK_STORE_PATH = ".workspace-kit/tasks/state.json";
4
+ export type TaskStorePersistence = {
5
+ loadDocument: () => Promise<TaskStoreDocument>;
6
+ saveDocument: (doc: TaskStoreDocument) => Promise<void>;
7
+ pathLabel: string;
8
+ };
2
9
  export declare class TaskStore {
3
10
  private document;
4
- private readonly filePath;
5
- constructor(workspacePath: string, storePath?: string);
11
+ private readonly persistence;
12
+ constructor(persistence: TaskStorePersistence);
13
+ static forJsonFile(workspacePath: string, storeRelativePath?: string): TaskStore;
14
+ static forSqliteDual(dual: SqliteDualPlanningStore): TaskStore;
6
15
  load(): Promise<void>;
7
16
  save(): Promise<void>;
8
17
  getAllTasks(): TaskEntity[];
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { TaskEngineError } from "./transitions.js";
5
- const DEFAULT_STORE_PATH = ".workspace-kit/tasks/state.json";
5
+ export const DEFAULT_TASK_STORE_PATH = ".workspace-kit/tasks/state.json";
6
6
  function emptyStore() {
7
7
  return {
8
8
  schemaVersion: 1,
@@ -14,49 +14,73 @@ function emptyStore() {
14
14
  }
15
15
  export class TaskStore {
16
16
  document;
17
- filePath;
18
- constructor(workspacePath, storePath) {
19
- this.filePath = path.resolve(workspacePath, storePath ?? DEFAULT_STORE_PATH);
17
+ persistence;
18
+ constructor(persistence) {
19
+ this.persistence = persistence;
20
20
  this.document = emptyStore();
21
21
  }
22
- async load() {
23
- try {
24
- const raw = await fs.readFile(this.filePath, "utf8");
25
- const parsed = JSON.parse(raw);
26
- if (parsed.schemaVersion !== 1) {
27
- throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
28
- }
29
- this.document = parsed;
30
- if (!Array.isArray(this.document.mutationLog)) {
31
- this.document.mutationLog = [];
22
+ static forJsonFile(workspacePath, storeRelativePath) {
23
+ const filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_TASK_STORE_PATH);
24
+ return new TaskStore({
25
+ pathLabel: filePath,
26
+ loadDocument: async () => {
27
+ try {
28
+ const raw = await fs.readFile(filePath, "utf8");
29
+ const parsed = JSON.parse(raw);
30
+ if (parsed.schemaVersion !== 1) {
31
+ throw new TaskEngineError("storage-read-error", `Unsupported schema version: ${parsed.schemaVersion}`);
32
+ }
33
+ if (!Array.isArray(parsed.mutationLog)) {
34
+ parsed.mutationLog = [];
35
+ }
36
+ return parsed;
37
+ }
38
+ catch (err) {
39
+ if (err.code === "ENOENT") {
40
+ return emptyStore();
41
+ }
42
+ if (err instanceof TaskEngineError) {
43
+ throw err;
44
+ }
45
+ throw new TaskEngineError("storage-read-error", `Failed to read task store: ${err.message}`);
46
+ }
47
+ },
48
+ saveDocument: async (doc) => {
49
+ const dir = path.dirname(filePath);
50
+ const tmpPath = `${filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
51
+ try {
52
+ await fs.mkdir(dir, { recursive: true });
53
+ await fs.writeFile(tmpPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
54
+ await fs.rename(tmpPath, filePath);
55
+ }
56
+ catch (err) {
57
+ try {
58
+ await fs.unlink(tmpPath);
59
+ }
60
+ catch {
61
+ /* cleanup best-effort */
62
+ }
63
+ throw new TaskEngineError("storage-write-error", `Failed to write task store: ${err.message}`);
64
+ }
32
65
  }
33
- }
34
- catch (err) {
35
- if (err.code === "ENOENT") {
36
- this.document = emptyStore();
37
- return;
66
+ });
67
+ }
68
+ static forSqliteDual(dual) {
69
+ return new TaskStore({
70
+ pathLabel: `${dual.getDisplayPath()}#task_engine`,
71
+ loadDocument: async () => dual.taskDocument,
72
+ saveDocument: async (doc) => {
73
+ dual.seedFromDocuments(doc, dual.wishlistDocument);
74
+ dual.persistSync();
38
75
  }
39
- if (err instanceof TaskEngineError)
40
- throw err;
41
- throw new TaskEngineError("storage-read-error", `Failed to read task store: ${err.message}`);
42
- }
76
+ });
77
+ }
78
+ async load() {
79
+ this.document = await this.persistence.loadDocument();
43
80
  }
44
81
  async save() {
45
82
  this.document.lastUpdated = new Date().toISOString();
46
- const dir = path.dirname(this.filePath);
47
- const tmpPath = `${this.filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
48
- try {
49
- await fs.mkdir(dir, { recursive: true });
50
- await fs.writeFile(tmpPath, JSON.stringify(this.document, null, 2) + "\n", "utf8");
51
- await fs.rename(tmpPath, this.filePath);
52
- }
53
- catch (err) {
54
- try {
55
- await fs.unlink(tmpPath);
56
- }
57
- catch { /* cleanup best-effort */ }
58
- throw new TaskEngineError("storage-write-error", `Failed to write task store: ${err.message}`);
59
- }
83
+ await this.persistence.saveDocument(this.document);
60
84
  }
61
85
  getAllTasks() {
62
86
  return [...this.document.tasks];
@@ -99,7 +123,7 @@ export class TaskStore {
99
123
  this.document.tasks = tasks.map((t) => ({ ...t }));
100
124
  }
101
125
  getFilePath() {
102
- return this.filePath;
126
+ return this.persistence.pathLabel;
103
127
  }
104
128
  getLastUpdated() {
105
129
  return this.document.lastUpdated;
@@ -1,8 +1,17 @@
1
- import type { WishlistItem } from "./wishlist-types.js";
1
+ import type { WishlistItem, WishlistStoreDocument } from "./wishlist-types.js";
2
+ import type { SqliteDualPlanningStore } from "./sqlite-dual-planning.js";
3
+ export declare const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
4
+ export type WishlistStorePersistence = {
5
+ loadDocument: () => Promise<WishlistStoreDocument>;
6
+ saveDocument: (doc: WishlistStoreDocument) => Promise<void>;
7
+ pathLabel: string;
8
+ };
2
9
  export declare class WishlistStore {
3
10
  private document;
4
- private readonly filePath;
5
- constructor(workspacePath: string, storeRelativePath?: string);
11
+ private readonly persistence;
12
+ constructor(persistence: WishlistStorePersistence);
13
+ static forJsonFile(workspacePath: string, storeRelativePath?: string): WishlistStore;
14
+ static forSqliteDual(dual: SqliteDualPlanningStore): WishlistStore;
6
15
  load(): Promise<void>;
7
16
  save(): Promise<void>;
8
17
  getAllItems(): WishlistItem[];
@@ -2,7 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import crypto from "node:crypto";
4
4
  import { TaskEngineError } from "./transitions.js";
5
- const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
5
+ export const DEFAULT_WISHLIST_PATH = ".workspace-kit/wishlist/state.json";
6
6
  function emptyWishlistDoc() {
7
7
  return {
8
8
  schemaVersion: 1,
@@ -12,51 +12,73 @@ function emptyWishlistDoc() {
12
12
  }
13
13
  export class WishlistStore {
14
14
  document;
15
- filePath;
16
- constructor(workspacePath, storeRelativePath) {
17
- this.filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
15
+ persistence;
16
+ constructor(persistence) {
17
+ this.persistence = persistence;
18
18
  this.document = emptyWishlistDoc();
19
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");
20
+ static forJsonFile(workspacePath, storeRelativePath) {
21
+ const filePath = path.resolve(workspacePath, storeRelativePath ?? DEFAULT_WISHLIST_PATH);
22
+ return new WishlistStore({
23
+ pathLabel: filePath,
24
+ loadDocument: async () => {
25
+ try {
26
+ const raw = await fs.readFile(filePath, "utf8");
27
+ const parsed = JSON.parse(raw);
28
+ if (parsed.schemaVersion !== 1) {
29
+ throw new TaskEngineError("storage-read-error", `Unsupported wishlist schema version: ${parsed.schemaVersion}`);
30
+ }
31
+ if (!Array.isArray(parsed.items)) {
32
+ throw new TaskEngineError("storage-read-error", "Wishlist store 'items' must be an array");
33
+ }
34
+ return parsed;
35
+ }
36
+ catch (err) {
37
+ if (err.code === "ENOENT") {
38
+ return emptyWishlistDoc();
39
+ }
40
+ if (err instanceof TaskEngineError) {
41
+ throw err;
42
+ }
43
+ throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
44
+ }
45
+ },
46
+ saveDocument: async (doc) => {
47
+ const dir = path.dirname(filePath);
48
+ const tmpPath = `${filePath}.${crypto.randomUUID().slice(0, 8)}.tmp`;
49
+ try {
50
+ await fs.mkdir(dir, { recursive: true });
51
+ await fs.writeFile(tmpPath, JSON.stringify(doc, null, 2) + "\n", "utf8");
52
+ await fs.rename(tmpPath, filePath);
53
+ }
54
+ catch (err) {
55
+ try {
56
+ await fs.unlink(tmpPath);
57
+ }
58
+ catch {
59
+ /* cleanup best-effort */
60
+ }
61
+ throw new TaskEngineError("storage-write-error", `Failed to write wishlist store: ${err.message}`);
62
+ }
29
63
  }
30
- this.document = parsed;
31
- }
32
- catch (err) {
33
- if (err.code === "ENOENT") {
34
- this.document = emptyWishlistDoc();
35
- return;
64
+ });
65
+ }
66
+ static forSqliteDual(dual) {
67
+ return new WishlistStore({
68
+ pathLabel: `${dual.getDisplayPath()}#wishlist`,
69
+ loadDocument: async () => dual.wishlistDocument,
70
+ saveDocument: async (doc) => {
71
+ dual.seedFromDocuments(dual.taskDocument, doc);
72
+ dual.persistSync();
36
73
  }
37
- if (err instanceof TaskEngineError)
38
- throw err;
39
- throw new TaskEngineError("storage-read-error", `Failed to read wishlist store: ${err.message}`);
40
- }
74
+ });
75
+ }
76
+ async load() {
77
+ this.document = await this.persistence.loadDocument();
41
78
  }
42
79
  async save() {
43
80
  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
- }
81
+ await this.persistence.saveDocument(this.document);
60
82
  }
61
83
  getAllItems() {
62
84
  return [...this.document.items];
@@ -78,7 +100,7 @@ export class WishlistStore {
78
100
  this.document.items[idx] = { ...item };
79
101
  }
80
102
  getFilePath() {
81
- return this.filePath;
103
+ return this.persistence.pathLabel;
82
104
  }
83
105
  getLastUpdated() {
84
106
  return this.document.lastUpdated;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workflow-cannon/workspace-kit",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "private": false,
5
5
  "packageManager": "pnpm@10.0.0",
6
6
  "license": "MIT",
@@ -41,6 +41,7 @@
41
41
  "ui:watch": "cd extensions/cursor-workflow-cannon && npm run watch"
42
42
  },
43
43
  "devDependencies": {
44
+ "@types/better-sqlite3": "^7.6.13",
44
45
  "@types/node": "^25.5.0",
45
46
  "typescript": "^5.9.3"
46
47
  },
@@ -48,5 +49,13 @@
48
49
  "dist",
49
50
  "src/modules/documentation",
50
51
  "package.json"
51
- ]
52
+ ],
53
+ "dependencies": {
54
+ "better-sqlite3": "^12.8.0"
55
+ },
56
+ "pnpm": {
57
+ "onlyBuiltDependencies": [
58
+ "better-sqlite3"
59
+ ]
60
+ }
52
61
  }
@@ -24,6 +24,7 @@ Files under `templates/`; `documentType` is the filename (basename). Keep this l
24
24
  - `AGENTS.md`
25
25
  - `ARCHITECTURE.md`
26
26
  - `PRINCIPLES.md`
27
+ - `README.md`
27
28
  - `RELEASING.md`
28
29
  - `ROADMAP.md`
29
30
  - `SECURITY.md`
@@ -18,6 +18,7 @@ All `.md` files under `sources.templatesRoot` (default `src/modules/documentatio
18
18
  - `AGENTS.md`
19
19
  - `ARCHITECTURE.md`
20
20
  - `PRINCIPLES.md`
21
+ - `README.md`
21
22
  - `RELEASING.md`
22
23
  - `ROADMAP.md`
23
24
  - `SECURITY.md`
@@ -8,6 +8,7 @@ Generate a single document for both canonical AI and human-readable surfaces usi
8
8
  - `AGENTS.md`
9
9
  - `ARCHITECTURE.md`
10
10
  - `PRINCIPLES.md`
11
+ - `README.md`
11
12
  - `RELEASING.md`
12
13
  - `ROADMAP.md`
13
14
  - `SECURITY.md`