@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.
- package/README.md +55 -106
- package/dist/modules/approvals/review-runtime.js +3 -11
- package/dist/modules/improvement/generate-recommendations-runtime.js +3 -11
- package/dist/modules/task-engine/index.d.ts +2 -0
- package/dist/modules/task-engine/index.js +44 -46
- package/dist/modules/task-engine/migrate-task-persistence-runtime.d.ts +2 -0
- package/dist/modules/task-engine/migrate-task-persistence-runtime.js +192 -0
- package/dist/modules/task-engine/planning-config.d.ts +10 -0
- package/dist/modules/task-engine/planning-config.js +37 -0
- package/dist/modules/task-engine/planning-open.d.ts +16 -0
- package/dist/modules/task-engine/planning-open.js +34 -0
- package/dist/modules/task-engine/sqlite-dual-planning.d.ts +21 -0
- package/dist/modules/task-engine/sqlite-dual-planning.js +137 -0
- package/dist/modules/task-engine/store.d.ts +12 -3
- package/dist/modules/task-engine/store.js +62 -38
- package/dist/modules/task-engine/wishlist-store.d.ts +12 -3
- package/dist/modules/task-engine/wishlist-store.js +62 -40
- package/package.json +11 -2
- package/src/modules/documentation/README.md +1 -0
- package/src/modules/documentation/instructions/document-project.md +1 -0
- package/src/modules/documentation/instructions/generate-document.md +1 -0
- package/src/modules/documentation/templates/README.md +89 -0
|
@@ -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
|
|
5
|
-
constructor(
|
|
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
|
|
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
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
this.
|
|
17
|
+
persistence;
|
|
18
|
+
constructor(persistence) {
|
|
19
|
+
this.persistence = persistence;
|
|
20
20
|
this.document = emptyStore();
|
|
21
21
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
5
|
-
constructor(
|
|
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
|
-
|
|
16
|
-
constructor(
|
|
17
|
-
this.
|
|
15
|
+
persistence;
|
|
16
|
+
constructor(persistence) {
|
|
17
|
+
this.persistence = persistence;
|
|
18
18
|
this.document = emptyWishlistDoc();
|
|
19
19
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
}
|