@promptowl/contextnest-community 1.0.0 → 1.0.1
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/{chunk-XDCW4HTW.js → chunk-5VHKEIAW.js} +100 -4
- package/dist/{chunk-BLOPZDPL.js → chunk-JMZ75ZCD.js} +22 -7
- package/dist/{chunk-2FXVMVZJ.js → chunk-K22GWPT4.js} +21 -63
- package/dist/{chunk-2TW25QEA.js → chunk-KQCWNHDM.js} +143 -22
- package/dist/index.js +757 -169
- package/dist/{review-service-2JHZHZWJ.js → review-service-4WS3XL6K.js} +4 -3
- package/dist/{stewardship-service-ZJATH6OM.js → stewardship-service-C5D2O7ZE.js} +2 -2
- package/dist/{version-service-2MZJGE3H.js → version-service-TFEYNPH7.js} +8 -4
- package/dist/web3/assets/index-DkLevP7k.js +624 -0
- package/dist/web3/assets/index-DpoBdKrd.css +1 -0
- package/dist/web3/index.html +2 -2
- package/package.json +11 -2
- package/dist/web3/assets/index-BlGzOlFt.css +0 -1
- package/dist/web3/assets/index-C3W5d7fT.js +0 -591
|
@@ -1,13 +1,74 @@
|
|
|
1
1
|
import {
|
|
2
2
|
canUserApprove,
|
|
3
3
|
resolveStewardsForNode
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-K22GWPT4.js";
|
|
5
5
|
import {
|
|
6
|
+
createVersion,
|
|
7
|
+
setApprovedVersion
|
|
8
|
+
} from "./chunk-JMZ75ZCD.js";
|
|
9
|
+
import {
|
|
10
|
+
config,
|
|
6
11
|
getDb
|
|
7
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-KQCWNHDM.js";
|
|
8
13
|
|
|
9
14
|
// src/governance/review-service.ts
|
|
10
15
|
import { v4 as uuid } from "uuid";
|
|
16
|
+
|
|
17
|
+
// src/nodes/engine.ts
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import {
|
|
20
|
+
NestStorage,
|
|
21
|
+
GraphQueryEngine,
|
|
22
|
+
VersionManager
|
|
23
|
+
} from "@promptowl/contextnest-engine";
|
|
24
|
+
var NestEngineCache = class {
|
|
25
|
+
cache = /* @__PURE__ */ new Map();
|
|
26
|
+
get(nestId) {
|
|
27
|
+
let engine = this.cache.get(nestId);
|
|
28
|
+
if (!engine) {
|
|
29
|
+
const nestPath = join(config.DATA_ROOT, "nests", nestId);
|
|
30
|
+
const storage = new NestStorage(nestPath);
|
|
31
|
+
const query = new GraphQueryEngine(storage);
|
|
32
|
+
const versions = new VersionManager(storage);
|
|
33
|
+
engine = { storage, query, versions };
|
|
34
|
+
this.cache.set(nestId, engine);
|
|
35
|
+
}
|
|
36
|
+
return engine;
|
|
37
|
+
}
|
|
38
|
+
evict(nestId) {
|
|
39
|
+
this.cache.delete(nestId);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var engineCache = new NestEngineCache();
|
|
43
|
+
|
|
44
|
+
// src/governance/safe-publish.ts
|
|
45
|
+
import {
|
|
46
|
+
publishDocument,
|
|
47
|
+
serializeDocument
|
|
48
|
+
} from "@promptowl/contextnest-engine";
|
|
49
|
+
async function safePublishDocument(storage, docId, options) {
|
|
50
|
+
const node = await storage.readDocument(docId);
|
|
51
|
+
const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
|
|
52
|
+
const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
|
|
53
|
+
await storage.writeDocument(docId, serializeDocument(cleanedNode));
|
|
54
|
+
return publishDocument(storage, docId, options);
|
|
55
|
+
}
|
|
56
|
+
function stripUndefinedDeep(value) {
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
|
|
59
|
+
}
|
|
60
|
+
if (value && typeof value === "object") {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const [k, v] of Object.entries(value)) {
|
|
63
|
+
if (v === void 0) continue;
|
|
64
|
+
out[k] = stripUndefinedDeep(v);
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// src/governance/review-service.ts
|
|
11
72
|
function submitForReview(params) {
|
|
12
73
|
const db = getDb();
|
|
13
74
|
const existing = db.prepare(
|
|
@@ -35,7 +96,7 @@ function submitForReview(params) {
|
|
|
35
96
|
).run(params.nestId, params.nodeId, params.version);
|
|
36
97
|
return getReviewRequest(id);
|
|
37
98
|
}
|
|
38
|
-
function approve(params) {
|
|
99
|
+
async function approve(params) {
|
|
39
100
|
const db = getDb();
|
|
40
101
|
const pending = db.prepare(
|
|
41
102
|
"SELECT * FROM review_requests WHERE nest_id = ? AND node_id = ? AND status = 'pending' ORDER BY requested_at DESC LIMIT 1"
|
|
@@ -65,12 +126,45 @@ function approve(params) {
|
|
|
65
126
|
pending.id
|
|
66
127
|
);
|
|
67
128
|
db.prepare(
|
|
68
|
-
"UPDATE node_versions SET status = '
|
|
129
|
+
"UPDATE node_versions SET status = 'published' WHERE nest_id = ? AND node_id = ? AND version = ?"
|
|
69
130
|
).run(params.nestId, params.nodeId, params.version);
|
|
70
131
|
db.prepare(
|
|
71
132
|
`INSERT OR REPLACE INTO approved_versions (nest_id, node_id, approved_version, approved_by)
|
|
72
133
|
VALUES (?, ?, ?, ?)`
|
|
73
134
|
).run(params.nestId, params.nodeId, params.version, params.approvedBy);
|
|
135
|
+
try {
|
|
136
|
+
const { storage } = engineCache.get(params.nestId);
|
|
137
|
+
const result = await safePublishDocument(storage, params.nodeId, {
|
|
138
|
+
editedBy: params.approvedBy,
|
|
139
|
+
note: params.note || `Approved review request ${pending.id}`
|
|
140
|
+
});
|
|
141
|
+
const engineVersion = result.versionEntry.version;
|
|
142
|
+
if (engineVersion !== params.version) {
|
|
143
|
+
const node = result.node;
|
|
144
|
+
const tags = node.frontmatter.tags || [];
|
|
145
|
+
createVersion({
|
|
146
|
+
nestId: params.nestId,
|
|
147
|
+
nodeId: params.nodeId,
|
|
148
|
+
version: engineVersion,
|
|
149
|
+
content: node.body || "",
|
|
150
|
+
author: params.approvedBy,
|
|
151
|
+
status: "published",
|
|
152
|
+
tags,
|
|
153
|
+
changeNote: params.note || `Approved review request ${pending.id}`
|
|
154
|
+
});
|
|
155
|
+
setApprovedVersion(
|
|
156
|
+
params.nestId,
|
|
157
|
+
params.nodeId,
|
|
158
|
+
engineVersion,
|
|
159
|
+
params.approvedBy
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
} catch (err) {
|
|
163
|
+
console.error(
|
|
164
|
+
`publishDocument failed for ${params.nestId}/${params.nodeId} on approve:`,
|
|
165
|
+
err
|
|
166
|
+
);
|
|
167
|
+
}
|
|
74
168
|
return getReviewRequest(pending.id);
|
|
75
169
|
}
|
|
76
170
|
function reject(params) {
|
|
@@ -189,6 +283,8 @@ function rowToReviewRequest(row) {
|
|
|
189
283
|
}
|
|
190
284
|
|
|
191
285
|
export {
|
|
286
|
+
engineCache,
|
|
287
|
+
safePublishDocument,
|
|
192
288
|
submitForReview,
|
|
193
289
|
approve,
|
|
194
290
|
reject,
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getDb
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KQCWNHDM.js";
|
|
4
4
|
|
|
5
5
|
// src/governance/version-service.ts
|
|
6
6
|
import { createHash } from "crypto";
|
|
7
7
|
function hashContent(content) {
|
|
8
8
|
return createHash("sha256").update(content).digest("hex");
|
|
9
9
|
}
|
|
10
|
+
var SYSTEM_AUTHOR_PREFIX = "system:auto-publish:";
|
|
11
|
+
function systemAuthor(email) {
|
|
12
|
+
return `${SYSTEM_AUTHOR_PREFIX}${email}`;
|
|
13
|
+
}
|
|
10
14
|
function createVersion(params) {
|
|
11
15
|
const db = getDb();
|
|
12
16
|
const contentHash = hashContent(params.content);
|
|
@@ -49,8 +53,10 @@ function getVersion(nestId, nodeId, version) {
|
|
|
49
53
|
function getCurrentVersion(nestId, nodeId) {
|
|
50
54
|
const db = getDb();
|
|
51
55
|
const row = db.prepare(
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
`SELECT MAX(version) as v FROM node_versions
|
|
57
|
+
WHERE nest_id = ? AND node_id = ?
|
|
58
|
+
AND author NOT LIKE ?`
|
|
59
|
+
).get(nestId, nodeId, `${SYSTEM_AUTHOR_PREFIX}%`);
|
|
54
60
|
return row?.v || 0;
|
|
55
61
|
}
|
|
56
62
|
function getApprovedVersion(nestId, nodeId) {
|
|
@@ -70,8 +76,13 @@ function setApprovedVersion(nestId, nodeId, version, approvedBy) {
|
|
|
70
76
|
function checkConflict(nestId, nodeId, baseVersion) {
|
|
71
77
|
const db = getDb();
|
|
72
78
|
const current = db.prepare(
|
|
73
|
-
|
|
74
|
-
|
|
79
|
+
`SELECT version, content_hash, author, created_at
|
|
80
|
+
FROM node_versions
|
|
81
|
+
WHERE nest_id = ? AND node_id = ?
|
|
82
|
+
AND author NOT LIKE ?
|
|
83
|
+
ORDER BY version DESC
|
|
84
|
+
LIMIT 1`
|
|
85
|
+
).get(nestId, nodeId, `${SYSTEM_AUTHOR_PREFIX}%`);
|
|
75
86
|
if (!current) {
|
|
76
87
|
return { conflict: false, currentVersion: 0, currentHash: "" };
|
|
77
88
|
}
|
|
@@ -114,11 +125,13 @@ function getDisplayStatus(nestId, nodeId) {
|
|
|
114
125
|
ORDER BY version DESC LIMIT 1`
|
|
115
126
|
).get(nestId, nodeId);
|
|
116
127
|
if (!current) return "draft";
|
|
117
|
-
if (current.status === "approved") {
|
|
128
|
+
if (current.status === "published" || current.status === "approved") {
|
|
118
129
|
const approved = db.prepare(
|
|
119
130
|
"SELECT approved_version FROM approved_versions WHERE nest_id = ? AND node_id = ?"
|
|
120
131
|
).get(nestId, nodeId);
|
|
121
|
-
if (approved?.approved_version === current.version)
|
|
132
|
+
if (approved?.approved_version === current.version) {
|
|
133
|
+
return current.status === "published" ? "published" : "approved";
|
|
134
|
+
}
|
|
122
135
|
return "draft";
|
|
123
136
|
}
|
|
124
137
|
if (current.status === "rejected") return "rejected";
|
|
@@ -138,6 +151,8 @@ function rowToVersion(row) {
|
|
|
138
151
|
|
|
139
152
|
export {
|
|
140
153
|
hashContent,
|
|
154
|
+
SYSTEM_AUTHOR_PREFIX,
|
|
155
|
+
systemAuthor,
|
|
141
156
|
createVersion,
|
|
142
157
|
getVersions,
|
|
143
158
|
getVersion,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
config,
|
|
3
3
|
getDb
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-KQCWNHDM.js";
|
|
5
5
|
|
|
6
6
|
// src/governance/stewardship-service.ts
|
|
7
7
|
import { v4 as uuid } from "uuid";
|
|
@@ -108,8 +108,8 @@ function assignSteward(data) {
|
|
|
108
108
|
const id = uuid();
|
|
109
109
|
db.prepare(
|
|
110
110
|
`INSERT INTO stewards
|
|
111
|
-
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role,
|
|
112
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
111
|
+
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
113
113
|
).run(
|
|
114
114
|
id,
|
|
115
115
|
data.nestId,
|
|
@@ -119,8 +119,6 @@ function assignSteward(data) {
|
|
|
119
119
|
data.userEmail,
|
|
120
120
|
data.userId || null,
|
|
121
121
|
data.role,
|
|
122
|
-
data.canApprove ? 1 : 0,
|
|
123
|
-
data.canReject ? 1 : 0,
|
|
124
122
|
data.assignedBy,
|
|
125
123
|
data.isActive ? 1 : 0
|
|
126
124
|
);
|
|
@@ -128,7 +126,21 @@ function assignSteward(data) {
|
|
|
128
126
|
}
|
|
129
127
|
function removeSteward(id) {
|
|
130
128
|
const db = getDb();
|
|
131
|
-
|
|
129
|
+
const remove = db.transaction((stewardId) => {
|
|
130
|
+
const row = db.prepare("SELECT nest_id, user_email FROM stewards WHERE id = ?").get(stewardId);
|
|
131
|
+
db.prepare("DELETE FROM stewards WHERE id = ?").run(stewardId);
|
|
132
|
+
if (!row) return;
|
|
133
|
+
const remaining = db.prepare(
|
|
134
|
+
"SELECT 1 FROM stewards WHERE nest_id = ? AND LOWER(user_email) = LOWER(?) AND is_active = 1 LIMIT 1"
|
|
135
|
+
).get(row.nest_id, row.user_email);
|
|
136
|
+
if (remaining) return;
|
|
137
|
+
const user = db.prepare("SELECT id FROM users WHERE LOWER(email) = LOWER(?)").get(row.user_email);
|
|
138
|
+
if (!user) return;
|
|
139
|
+
db.prepare(
|
|
140
|
+
"DELETE FROM nest_collaborators WHERE nest_id = ? AND user_id = ?"
|
|
141
|
+
).run(row.nest_id, user.id);
|
|
142
|
+
});
|
|
143
|
+
remove(id);
|
|
132
144
|
}
|
|
133
145
|
function getSteward(id) {
|
|
134
146
|
const db = getDb();
|
|
@@ -208,10 +220,6 @@ async function createStewardRecord(params) {
|
|
|
208
220
|
if (!params.documentId) throw new Error("documentId required for document scope");
|
|
209
221
|
nodePattern = params.documentId;
|
|
210
222
|
break;
|
|
211
|
-
case "folder":
|
|
212
|
-
if (!params.folderPath) throw new Error("folderPath required for folder scope");
|
|
213
|
-
nodePattern = params.folderPath;
|
|
214
|
-
break;
|
|
215
223
|
case "tag":
|
|
216
224
|
if (!params.tagName) throw new Error("tagName required for tag scope");
|
|
217
225
|
tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
|
|
@@ -249,8 +257,6 @@ async function createStewardRecord(params) {
|
|
|
249
257
|
userEmail: email,
|
|
250
258
|
userId: userRow?.id,
|
|
251
259
|
role: user.role ?? "reviewer",
|
|
252
|
-
canApprove: user.canApprove !== false,
|
|
253
|
-
canReject: user.canReject !== false,
|
|
254
260
|
assignedBy: params.assignedBy,
|
|
255
261
|
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
256
262
|
isActive: true
|
|
@@ -283,17 +289,7 @@ function resolve(nestId, nodeId) {
|
|
|
283
289
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
|
|
284
290
|
AND s.node_pattern = ?
|
|
285
291
|
UNION ALL
|
|
286
|
-
SELECT s.*, 2 AS priority, ('
|
|
287
|
-
FROM stewards s
|
|
288
|
-
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'folder'
|
|
289
|
-
AND s.node_pattern IS NOT NULL
|
|
290
|
-
AND instr(s.node_pattern, '*') = 0
|
|
291
|
-
AND (
|
|
292
|
-
? LIKE s.node_pattern || '/%'
|
|
293
|
-
OR s.node_pattern = ?
|
|
294
|
-
)
|
|
295
|
-
UNION ALL
|
|
296
|
-
SELECT s.*, 3 AS priority, ('tag: ' || s.tag_name) AS match_source
|
|
292
|
+
SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
|
|
297
293
|
FROM stewards s
|
|
298
294
|
JOIN node_tag_index nt
|
|
299
295
|
ON nt.nest_id = s.nest_id
|
|
@@ -301,7 +297,7 @@ function resolve(nestId, nodeId) {
|
|
|
301
297
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
|
|
302
298
|
AND nt.node_id = ?
|
|
303
299
|
UNION ALL
|
|
304
|
-
SELECT s.*,
|
|
300
|
+
SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
|
|
305
301
|
FROM stewards s
|
|
306
302
|
WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
|
|
307
303
|
ORDER BY priority ASC, user_email ASC
|
|
@@ -312,10 +308,6 @@ function resolve(nestId, nodeId) {
|
|
|
312
308
|
// document branch
|
|
313
309
|
nestId,
|
|
314
310
|
nodeId,
|
|
315
|
-
nestId,
|
|
316
|
-
// folder branch (path-prefix OR whole-nest folder)
|
|
317
|
-
nestId,
|
|
318
|
-
nodeId,
|
|
319
311
|
// tag branch
|
|
320
312
|
nestId
|
|
321
313
|
// nest branch
|
|
@@ -325,23 +317,6 @@ function resolve(nestId, nodeId) {
|
|
|
325
317
|
priority: row.priority,
|
|
326
318
|
source: row.match_source
|
|
327
319
|
}));
|
|
328
|
-
const legacyFolderGlobs = db.prepare(
|
|
329
|
-
`SELECT * FROM stewards
|
|
330
|
-
WHERE nest_id = ? AND is_active = 1 AND scope = 'folder'
|
|
331
|
-
AND node_pattern IS NOT NULL AND instr(node_pattern, '*') > 0`
|
|
332
|
-
).all(nestId);
|
|
333
|
-
for (const row of legacyFolderGlobs) {
|
|
334
|
-
if (globMatch(nodeId, row.node_pattern)) {
|
|
335
|
-
resolved.push({
|
|
336
|
-
steward: rowToSteward(row),
|
|
337
|
-
priority: 2,
|
|
338
|
-
source: `folder: ${row.node_pattern}`
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
if (legacyFolderGlobs.length > 0) {
|
|
343
|
-
resolved.sort((a, b) => a.priority - b.priority);
|
|
344
|
-
}
|
|
345
320
|
if (resolved.length > 0) {
|
|
346
321
|
return { stewards: resolved, fallbackToOwner: false };
|
|
347
322
|
}
|
|
@@ -408,7 +383,7 @@ function canUserApprove(nestId, nodeId, userEmail) {
|
|
|
408
383
|
};
|
|
409
384
|
}
|
|
410
385
|
const match = resolved.find(
|
|
411
|
-
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer"
|
|
386
|
+
(r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase() && r.steward.role === "reviewer"
|
|
412
387
|
);
|
|
413
388
|
if (!match) {
|
|
414
389
|
return {
|
|
@@ -444,14 +419,6 @@ function canUserAccess(nestId, nodeId, userEmail) {
|
|
|
444
419
|
}
|
|
445
420
|
return { allowed: false, reason: "no steward assignment", role: null };
|
|
446
421
|
}
|
|
447
|
-
function globMatch(value, pattern) {
|
|
448
|
-
if (pattern === "*") return true;
|
|
449
|
-
if (!pattern.includes("*")) return value === pattern;
|
|
450
|
-
const regex = new RegExp(
|
|
451
|
-
"^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$"
|
|
452
|
-
);
|
|
453
|
-
return regex.test(value);
|
|
454
|
-
}
|
|
455
422
|
function syncFromConfig(nestId, config2) {
|
|
456
423
|
const db = getDb();
|
|
457
424
|
let count = 0;
|
|
@@ -474,8 +441,6 @@ function syncFromConfig(nestId, config2) {
|
|
|
474
441
|
userEmail: entry.email.toLowerCase(),
|
|
475
442
|
userId: user?.id,
|
|
476
443
|
role,
|
|
477
|
-
canApprove: entry.can_approve !== false,
|
|
478
|
-
canReject: entry.can_reject !== false,
|
|
479
444
|
assignedBy: "config",
|
|
480
445
|
assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
481
446
|
isActive: true
|
|
@@ -486,11 +451,6 @@ function syncFromConfig(nestId, config2) {
|
|
|
486
451
|
if (config2.nest) {
|
|
487
452
|
addEntries("nest", config2.nest);
|
|
488
453
|
}
|
|
489
|
-
if (config2.folders) {
|
|
490
|
-
for (const [pattern, entries] of Object.entries(config2.folders)) {
|
|
491
|
-
addEntries("folder", entries, { nodePattern: pattern });
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
454
|
if (config2.tags) {
|
|
495
455
|
for (const [tagName, entries] of Object.entries(config2.tags)) {
|
|
496
456
|
addEntries("tag", entries, { tagName });
|
|
@@ -513,8 +473,6 @@ function rowToSteward(row) {
|
|
|
513
473
|
userEmail: row.user_email,
|
|
514
474
|
userId: row.user_id || void 0,
|
|
515
475
|
role: row.role,
|
|
516
|
-
canApprove: !!row.can_approve,
|
|
517
|
-
canReject: !!row.can_reject,
|
|
518
476
|
assignedBy: row.assigned_by,
|
|
519
477
|
assignedAt: row.assigned_at,
|
|
520
478
|
isActive: !!row.is_active
|
|
@@ -10,12 +10,15 @@ var envCandidates = [
|
|
|
10
10
|
join(__dirname, "..", ".env")
|
|
11
11
|
];
|
|
12
12
|
var envFileLoaded = envCandidates.find((p) => existsSync(p)) || null;
|
|
13
|
-
|
|
13
|
+
var isTestRun = !!process.env.VITEST;
|
|
14
|
+
if (envFileLoaded && !isTestRun) {
|
|
14
15
|
dotenv.config({ path: envFileLoaded, override: true });
|
|
15
16
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
if (!isTestRun) {
|
|
18
|
+
console.log(
|
|
19
|
+
`[config] dotenv: ${envFileLoaded ? `loaded ${envFileLoaded}` : "no .env file found"}`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
19
22
|
function dataRoot() {
|
|
20
23
|
return process.env.DATA_ROOT || join(process.cwd(), "data");
|
|
21
24
|
}
|
|
@@ -78,6 +81,10 @@ import Database from "better-sqlite3";
|
|
|
78
81
|
import { mkdirSync } from "fs";
|
|
79
82
|
import { dirname as dirname2 } from "path";
|
|
80
83
|
|
|
84
|
+
// src/shared/constants.ts
|
|
85
|
+
var ANON_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
86
|
+
var ANON_EMAIL = "admin@localhost";
|
|
87
|
+
|
|
81
88
|
// src/db/migrations.ts
|
|
82
89
|
function runMigrations(db2) {
|
|
83
90
|
db2.exec(`
|
|
@@ -148,19 +155,17 @@ function runMigrations(db2) {
|
|
|
148
155
|
db2.exec(`
|
|
149
156
|
-- Steward assignments (mirrors PromptOwl ContextSteward model)
|
|
150
157
|
-- scope+target combination determines what the steward governs
|
|
151
|
-
-- Resolution priority: document(1) >
|
|
158
|
+
-- Resolution priority: document(1) > tag(2) > nest(3)
|
|
152
159
|
CREATE TABLE IF NOT EXISTS stewards (
|
|
153
160
|
id TEXT PRIMARY KEY,
|
|
154
161
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
155
|
-
scope TEXT NOT NULL CHECK(scope IN ('document', '
|
|
156
|
-
node_pattern TEXT, --
|
|
162
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
163
|
+
node_pattern TEXT, -- exact node id for document scope
|
|
157
164
|
tag_name TEXT, -- for tag scope
|
|
158
165
|
user_email TEXT NOT NULL,
|
|
159
166
|
user_id TEXT REFERENCES users(id),
|
|
160
167
|
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
161
168
|
CHECK(role IN ('editor', 'reviewer', 'admin')),
|
|
162
|
-
can_approve INTEGER NOT NULL DEFAULT 1,
|
|
163
|
-
can_reject INTEGER NOT NULL DEFAULT 1,
|
|
164
169
|
assigned_by TEXT NOT NULL,
|
|
165
170
|
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
166
171
|
is_active INTEGER NOT NULL DEFAULT 1
|
|
@@ -231,6 +236,19 @@ function runMigrations(db2) {
|
|
|
231
236
|
if (!userCols.includes("is_invited")) {
|
|
232
237
|
db2.exec("ALTER TABLE users ADD COLUMN is_invited INTEGER NOT NULL DEFAULT 0");
|
|
233
238
|
}
|
|
239
|
+
const stewardCols = db2.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
240
|
+
if (stewardCols.length > 0) {
|
|
241
|
+
if (!stewardCols.includes("can_approve")) {
|
|
242
|
+
db2.exec(
|
|
243
|
+
"ALTER TABLE stewards ADD COLUMN can_approve INTEGER NOT NULL DEFAULT 1"
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
if (!stewardCols.includes("can_reject")) {
|
|
247
|
+
db2.exec(
|
|
248
|
+
"ALTER TABLE stewards ADD COLUMN can_reject INTEGER NOT NULL DEFAULT 1"
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
234
252
|
db2.exec(`
|
|
235
253
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
236
254
|
id TEXT PRIMARY KEY,
|
|
@@ -245,24 +263,23 @@ function runMigrations(db2) {
|
|
|
245
263
|
CREATE TABLE stewards_new (
|
|
246
264
|
id TEXT PRIMARY KEY,
|
|
247
265
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
248
|
-
scope TEXT NOT NULL CHECK(scope IN ('document', '
|
|
266
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
249
267
|
node_pattern TEXT,
|
|
250
268
|
tag_name TEXT,
|
|
251
269
|
user_email TEXT NOT NULL,
|
|
252
270
|
user_id TEXT REFERENCES users(id),
|
|
253
271
|
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
254
272
|
CHECK(role IN ('editor', 'reviewer', 'viewer')),
|
|
255
|
-
can_approve INTEGER NOT NULL DEFAULT 1,
|
|
256
|
-
can_reject INTEGER NOT NULL DEFAULT 1,
|
|
257
273
|
assigned_by TEXT NOT NULL,
|
|
258
274
|
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
259
275
|
is_active INTEGER NOT NULL DEFAULT 1
|
|
260
276
|
);
|
|
261
277
|
|
|
262
|
-
-- Copy rows; map legacy 'admin' role to 'reviewer', normalize tag_name + email
|
|
278
|
+
-- Copy rows; map legacy 'admin' role to 'reviewer', normalize tag_name + email.
|
|
279
|
+
-- Legacy can_approve/can_reject columns are dropped here (role is now the sole signal).
|
|
263
280
|
INSERT INTO stewards_new
|
|
264
281
|
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
265
|
-
role,
|
|
282
|
+
role, assigned_by, assigned_at, is_active)
|
|
266
283
|
SELECT
|
|
267
284
|
id, nest_id, scope, node_pattern,
|
|
268
285
|
CASE
|
|
@@ -272,7 +289,7 @@ function runMigrations(db2) {
|
|
|
272
289
|
lower(user_email),
|
|
273
290
|
user_id,
|
|
274
291
|
CASE role WHEN 'admin' THEN 'reviewer' ELSE role END,
|
|
275
|
-
|
|
292
|
+
assigned_by, assigned_at, is_active
|
|
276
293
|
FROM stewards;
|
|
277
294
|
|
|
278
295
|
DROP TABLE stewards;
|
|
@@ -291,10 +308,6 @@ function runMigrations(db2) {
|
|
|
291
308
|
ON stewards(nest_id, node_pattern, user_email)
|
|
292
309
|
WHERE scope = 'document' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
293
310
|
|
|
294
|
-
CREATE UNIQUE INDEX idx_stewards_uniq_folder
|
|
295
|
-
ON stewards(nest_id, node_pattern, user_email)
|
|
296
|
-
WHERE scope = 'folder' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
297
|
-
|
|
298
311
|
CREATE UNIQUE INDEX idx_stewards_uniq_tag
|
|
299
312
|
ON stewards(nest_id, tag_name, user_email)
|
|
300
313
|
WHERE scope = 'tag' AND tag_name IS NOT NULL AND is_active = 1;
|
|
@@ -303,9 +316,6 @@ function runMigrations(db2) {
|
|
|
303
316
|
CREATE INDEX idx_stewards_tag_lookup
|
|
304
317
|
ON stewards(nest_id, tag_name)
|
|
305
318
|
WHERE scope = 'tag' AND is_active = 1;
|
|
306
|
-
CREATE INDEX idx_stewards_folder_lookup
|
|
307
|
-
ON stewards(nest_id, node_pattern)
|
|
308
|
-
WHERE scope = 'folder' AND is_active = 1;
|
|
309
319
|
CREATE INDEX idx_stewards_doc_lookup
|
|
310
320
|
ON stewards(nest_id, node_pattern)
|
|
311
321
|
WHERE scope = 'document' AND is_active = 1;
|
|
@@ -401,6 +411,115 @@ function runMigrations(db2) {
|
|
|
401
411
|
recordMigration("004_license_cache_owner_email");
|
|
402
412
|
})();
|
|
403
413
|
}
|
|
414
|
+
if (!hasMigration("005_anon_nest_public_default")) {
|
|
415
|
+
db2.transaction(() => {
|
|
416
|
+
db2.prepare(
|
|
417
|
+
"UPDATE nests SET visibility = 'public' WHERE user_id = ? AND visibility = 'private'"
|
|
418
|
+
).run(ANON_USER_ID);
|
|
419
|
+
recordMigration("005_anon_nest_public_default");
|
|
420
|
+
})();
|
|
421
|
+
}
|
|
422
|
+
if (!hasMigration("006_node_versions_published_status")) {
|
|
423
|
+
db2.transaction(() => {
|
|
424
|
+
db2.exec(`
|
|
425
|
+
CREATE TABLE node_versions_new (
|
|
426
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
427
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
428
|
+
node_id TEXT NOT NULL,
|
|
429
|
+
version INTEGER NOT NULL,
|
|
430
|
+
content_hash TEXT NOT NULL,
|
|
431
|
+
author TEXT NOT NULL,
|
|
432
|
+
status TEXT NOT NULL DEFAULT 'draft'
|
|
433
|
+
CHECK(status IN ('draft', 'pending_review', 'approved', 'published', 'rejected')),
|
|
434
|
+
change_note TEXT,
|
|
435
|
+
tags_json TEXT,
|
|
436
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
437
|
+
UNIQUE(nest_id, node_id, version)
|
|
438
|
+
);
|
|
439
|
+
INSERT INTO node_versions_new
|
|
440
|
+
(id, nest_id, node_id, version, content_hash, author, status, change_note, tags_json, created_at)
|
|
441
|
+
SELECT
|
|
442
|
+
id, nest_id, node_id, version, content_hash, author, status, change_note, tags_json, created_at
|
|
443
|
+
FROM node_versions;
|
|
444
|
+
DROP TABLE node_versions;
|
|
445
|
+
ALTER TABLE node_versions_new RENAME TO node_versions;
|
|
446
|
+
CREATE INDEX idx_versions_node ON node_versions(nest_id, node_id);
|
|
447
|
+
`);
|
|
448
|
+
recordMigration("006_node_versions_published_status");
|
|
449
|
+
})();
|
|
450
|
+
}
|
|
451
|
+
if (!hasMigration("007_drop_steward_capability_flags")) {
|
|
452
|
+
const stewardCols2 = db2.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
453
|
+
const hasLegacyCols = stewardCols2.includes("can_approve") || stewardCols2.includes("can_reject");
|
|
454
|
+
if (hasLegacyCols) {
|
|
455
|
+
db2.transaction(() => {
|
|
456
|
+
if (stewardCols2.includes("can_approve")) {
|
|
457
|
+
db2.exec("ALTER TABLE stewards DROP COLUMN can_approve");
|
|
458
|
+
}
|
|
459
|
+
if (stewardCols2.includes("can_reject")) {
|
|
460
|
+
db2.exec("ALTER TABLE stewards DROP COLUMN can_reject");
|
|
461
|
+
}
|
|
462
|
+
recordMigration("007_drop_steward_capability_flags");
|
|
463
|
+
})();
|
|
464
|
+
} else {
|
|
465
|
+
recordMigration("007_drop_steward_capability_flags");
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!hasMigration("008_drop_steward_folder_scope")) {
|
|
469
|
+
db2.transaction(() => {
|
|
470
|
+
db2.exec("DELETE FROM stewards WHERE scope = 'folder'");
|
|
471
|
+
db2.exec("DROP INDEX IF EXISTS idx_stewards_uniq_folder");
|
|
472
|
+
db2.exec("DROP INDEX IF EXISTS idx_stewards_folder_lookup");
|
|
473
|
+
const tbl = db2.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='stewards'").get();
|
|
474
|
+
if (tbl?.sql && tbl.sql.includes("'folder'")) {
|
|
475
|
+
db2.exec(`
|
|
476
|
+
CREATE TABLE stewards_new (
|
|
477
|
+
id TEXT PRIMARY KEY,
|
|
478
|
+
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
479
|
+
scope TEXT NOT NULL CHECK(scope IN ('document', 'tag', 'nest')),
|
|
480
|
+
node_pattern TEXT,
|
|
481
|
+
tag_name TEXT,
|
|
482
|
+
user_email TEXT NOT NULL,
|
|
483
|
+
user_id TEXT REFERENCES users(id),
|
|
484
|
+
role TEXT NOT NULL DEFAULT 'reviewer'
|
|
485
|
+
CHECK(role IN ('editor', 'reviewer', 'viewer')),
|
|
486
|
+
assigned_by TEXT NOT NULL,
|
|
487
|
+
assigned_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
488
|
+
is_active INTEGER NOT NULL DEFAULT 1
|
|
489
|
+
);
|
|
490
|
+
INSERT INTO stewards_new
|
|
491
|
+
(id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
492
|
+
role, assigned_by, assigned_at, is_active)
|
|
493
|
+
SELECT
|
|
494
|
+
id, nest_id, scope, node_pattern, tag_name, user_email, user_id,
|
|
495
|
+
role, assigned_by, assigned_at, is_active
|
|
496
|
+
FROM stewards;
|
|
497
|
+
DROP TABLE stewards;
|
|
498
|
+
ALTER TABLE stewards_new RENAME TO stewards;
|
|
499
|
+
|
|
500
|
+
CREATE INDEX idx_stewards_nest ON stewards(nest_id);
|
|
501
|
+
CREATE INDEX idx_stewards_email ON stewards(user_email);
|
|
502
|
+
CREATE INDEX idx_stewards_scope ON stewards(nest_id, scope);
|
|
503
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_nest
|
|
504
|
+
ON stewards(nest_id, user_email)
|
|
505
|
+
WHERE scope = 'nest' AND is_active = 1;
|
|
506
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_document
|
|
507
|
+
ON stewards(nest_id, node_pattern, user_email)
|
|
508
|
+
WHERE scope = 'document' AND node_pattern IS NOT NULL AND is_active = 1;
|
|
509
|
+
CREATE UNIQUE INDEX idx_stewards_uniq_tag
|
|
510
|
+
ON stewards(nest_id, tag_name, user_email)
|
|
511
|
+
WHERE scope = 'tag' AND tag_name IS NOT NULL AND is_active = 1;
|
|
512
|
+
CREATE INDEX idx_stewards_tag_lookup
|
|
513
|
+
ON stewards(nest_id, tag_name)
|
|
514
|
+
WHERE scope = 'tag' AND is_active = 1;
|
|
515
|
+
CREATE INDEX idx_stewards_doc_lookup
|
|
516
|
+
ON stewards(nest_id, node_pattern)
|
|
517
|
+
WHERE scope = 'document' AND is_active = 1;
|
|
518
|
+
`);
|
|
519
|
+
}
|
|
520
|
+
recordMigration("008_drop_steward_folder_scope");
|
|
521
|
+
})();
|
|
522
|
+
}
|
|
404
523
|
}
|
|
405
524
|
|
|
406
525
|
// src/db/client.ts
|
|
@@ -419,5 +538,7 @@ function getDb() {
|
|
|
419
538
|
|
|
420
539
|
export {
|
|
421
540
|
config,
|
|
541
|
+
ANON_USER_ID,
|
|
542
|
+
ANON_EMAIL,
|
|
422
543
|
getDb
|
|
423
544
|
};
|