@promptowl/contextnest-community 1.17.0 → 1.18.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.
Files changed (27) hide show
  1. package/README.md +6 -2
  2. package/dist/{chunk-UXJSO54R.js → chunk-34UXRZXD.js} +59 -14
  3. package/dist/chunk-F66VCBLA.js +136 -0
  4. package/dist/{chunk-SNFGU5Q7.js → chunk-FUUJG6XP.js} +597 -326
  5. package/dist/{chunk-4YGJSUJC.js → chunk-HGN45I5P.js} +29 -21
  6. package/dist/{chunk-JMJLNEXE.js → chunk-HYSTFMFG.js} +43 -2
  7. package/dist/chunk-OOBZG6BW.js +379 -0
  8. package/dist/{chunk-HWDPCPYT.js → chunk-QUZXRLUA.js} +1 -1
  9. package/dist/{chunk-YF4OFT2V.js → chunk-TQACHVVA.js} +35 -4
  10. package/dist/{chunk-SLTQACJW.js → chunk-YB3LKF7U.js} +3 -1
  11. package/dist/{client-OBS5HOTA.js → client-SW5I6ZBK.js} +2 -2
  12. package/dist/engine-34E3YNX7.js +14 -0
  13. package/dist/external-edit-service-6Q6SHEOF.js +33 -0
  14. package/dist/{grants-service-4NXPBYWD.js → grants-service-ME7HIVHU.js} +3 -3
  15. package/dist/index.js +496 -470
  16. package/dist/{keys-73STFJJB.js → keys-CTOGAG3W.js} +5 -1
  17. package/dist/{migrations.postgres-CYKO5CFV.js → migrations.postgres-5VI4RDTS.js} +41 -4
  18. package/dist/{review-service-3CCKDO5N.js → review-service-H2M5ACPQ.js} +7 -7
  19. package/dist/{stewardship-service-OHTTZFND.js → stewardship-service-U4CKL7Y5.js} +8 -4
  20. package/dist/{version-service-EFZGGL3Y.js → version-service-JWEN5UUW.js} +6 -4
  21. package/dist/web3/assets/index-DONv5_Ni.css +1 -0
  22. package/dist/web3/assets/index-jSGrnboh.js +1065 -0
  23. package/dist/web3/index.html +2 -2
  24. package/package.json +3 -3
  25. package/dist/chunk-XRK6SQSC.js +0 -58
  26. package/dist/web3/assets/index-D1299AIf.css +0 -1
  27. package/dist/web3/assets/index-W6tEA3RK.js +0 -1046
@@ -1,20 +1,22 @@
1
+ import {
2
+ canUserAccess,
3
+ canUserApprove,
4
+ resolveNestWideRoles,
5
+ resolveStewardsForNode,
6
+ stewardCoverageForUser
7
+ } from "./chunk-TQACHVVA.js";
1
8
  import {
2
9
  grantCoversNode,
3
10
  listUserGrants,
4
11
  resolveNodeGrant
5
- } from "./chunk-HWDPCPYT.js";
12
+ } from "./chunk-QUZXRLUA.js";
6
13
  import {
7
14
  createVersion,
8
15
  getApprovedVersion,
9
16
  getApprovedVersions,
10
17
  getCurrentVersion,
11
18
  setApprovedVersion
12
- } from "./chunk-UXJSO54R.js";
13
- import {
14
- canUserAccess,
15
- canUserApprove,
16
- resolveStewardsForNode
17
- } from "./chunk-YF4OFT2V.js";
19
+ } from "./chunk-34UXRZXD.js";
18
20
  import {
19
21
  buildDocContext,
20
22
  buildTitleMap,
@@ -28,11 +30,12 @@ import {
28
30
  notifyEmailForNest,
29
31
  nowExpr,
30
32
  opContext,
33
+ permissionLabel,
31
34
  prettyFolderPath,
32
35
  resolveNestPermission,
33
36
  sendEmailToRecipient,
34
37
  titleForNode
35
- } from "./chunk-SNFGU5Q7.js";
38
+ } from "./chunk-FUUJG6XP.js";
36
39
  import {
37
40
  ConflictError,
38
41
  NotFoundError,
@@ -41,7 +44,7 @@ import {
41
44
  import {
42
45
  config,
43
46
  getDb
44
- } from "./chunk-JMJLNEXE.js";
47
+ } from "./chunk-HYSTFMFG.js";
45
48
 
46
49
  // src/governance/review-service.ts
47
50
  import { v4 as uuid3 } from "uuid";
@@ -169,7 +172,7 @@ async function notifyNestInvite(params) {
169
172
  nestId,
170
173
  inviteeEmail.toLowerCase(),
171
174
  nestId,
172
- `You were added to "${nestName2}" as ${permission}`,
175
+ `You were added to "${nestName2}" as ${permissionLabel(permission)}`,
173
176
  (/* @__PURE__ */ new Date()).toISOString()
174
177
  ]
175
178
  );
@@ -180,10 +183,16 @@ async function notifyNestInvite(params) {
180
183
  async function listNotifications(userEmail, opts = {}) {
181
184
  const limit = Math.min(opts.limit ?? 50, 200);
182
185
  return await getDb().all(
183
- `SELECT id, nest_id, kind, subject_id, message, created_at, read_at
184
- FROM notifications
185
- WHERE LOWER(user_email) = LOWER(?)${opts.unreadOnly ? " AND read_at IS NULL" : ""}
186
- ORDER BY created_at DESC LIMIT ${limit}`,
186
+ `SELECT n.id, n.nest_id, n.kind, n.subject_id, n.message, n.created_at, n.read_at,
187
+ COALESCE(
188
+ rr.node_id,
189
+ CASE WHEN dr.target_type = 'document' THEN dr.node_id END
190
+ ) AS node_id
191
+ FROM notifications n
192
+ LEFT JOIN review_requests rr ON rr.id = n.subject_id
193
+ LEFT JOIN deletion_requests dr ON dr.id = n.subject_id
194
+ WHERE LOWER(n.user_email) = LOWER(?)${opts.unreadOnly ? " AND n.read_at IS NULL" : ""}
195
+ ORDER BY n.created_at DESC LIMIT ${limit}`,
187
196
  [userEmail]
188
197
  );
189
198
  }
@@ -251,13 +260,12 @@ async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersio
251
260
  if (await resolveNestPermission(nestId, userId) !== "none") return nodes;
252
261
  return nodes.filter((n) => grantCoversNode(grants, n.id));
253
262
  }
254
- const accessible = [];
255
- for (const n of nodes) {
256
- if ((await canUserAccess(nestId, n.id, userEmail)).allowed || grantCoversNode(grants, n.id)) {
257
- accessible.push(n);
258
- }
259
- }
260
- return accessible;
263
+ if ((await resolveNestWideRoles(nestId, userEmail)).length > 0) return nodes;
264
+ const coverage = await stewardCoverageForUser(nestId, userEmail);
265
+ if (coverage.nestWide) return nodes;
266
+ return nodes.filter(
267
+ (n) => coverage.nodeIds.has(n.id) || grantCoversNode(grants, n.id)
268
+ );
261
269
  }
262
270
 
263
271
  // src/workflow/edge-type-routes.ts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ANON_USER_ID
3
- } from "./chunk-SLTQACJW.js";
3
+ } from "./chunk-YB3LKF7U.js";
4
4
 
5
5
  // src/db/client.ts
6
6
  import Database from "better-sqlite3";
@@ -741,6 +741,9 @@ function runMigrations(db) {
741
741
  if (!nestCols.includes("prime_tags")) {
742
742
  db.exec("ALTER TABLE nests ADD COLUMN prime_tags TEXT NOT NULL DEFAULT 'prime-document'");
743
743
  }
744
+ if (!nestCols.includes("index_synced_at")) {
745
+ db.exec("ALTER TABLE nests ADD COLUMN index_synced_at TEXT");
746
+ }
744
747
  const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
745
748
  if (!userCols.includes("is_admin")) {
746
749
  db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
@@ -1531,6 +1534,44 @@ function runMigrations(db) {
1531
1534
  `);
1532
1535
  recordMigration("034_versions_author_status_index");
1533
1536
  }
1537
+ if (!hasMigration("035_multiple_api_keys_per_user")) {
1538
+ db.exec(`DROP INDEX IF EXISTS idx_api_keys_user_unique;`);
1539
+ recordMigration("035_multiple_api_keys_per_user");
1540
+ }
1541
+ if (!hasMigration("036_node_index")) {
1542
+ db.transaction(() => {
1543
+ db.exec(`
1544
+ CREATE TABLE IF NOT EXISTS node_index (
1545
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1546
+ node_id TEXT NOT NULL,
1547
+ folder TEXT NOT NULL DEFAULT '',
1548
+ title TEXT NOT NULL DEFAULT '',
1549
+ type TEXT NOT NULL DEFAULT 'document',
1550
+ status TEXT NOT NULL DEFAULT 'draft',
1551
+ updated_at TEXT,
1552
+ frontmatter_json TEXT NOT NULL,
1553
+ PRIMARY KEY (nest_id, node_id)
1554
+ );
1555
+ CREATE INDEX IF NOT EXISTS idx_node_index_folder
1556
+ ON node_index(nest_id, folder);
1557
+
1558
+ -- Which documents have an external edit staged against them.
1559
+ --
1560
+ -- Its own table rather than a node_index column: node_index rows are
1561
+ -- upserted with INSERT OR REPLACE on every document write, which
1562
+ -- resets any column the write does not carry \u2014 and a suggestion's
1563
+ -- existence has nothing to do with the document write that would
1564
+ -- clobber it. Kept in step at the three points that stage, approve or
1565
+ -- reject a suggestion, and refreshed wholesale by a rebuild.
1566
+ CREATE TABLE IF NOT EXISTS node_suggestions (
1567
+ nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
1568
+ node_id TEXT NOT NULL,
1569
+ PRIMARY KEY (nest_id, node_id)
1570
+ );
1571
+ `);
1572
+ })();
1573
+ recordMigration("036_node_index");
1574
+ }
1534
1575
  }
1535
1576
  function mergeCaseCollidingUsers(db) {
1536
1577
  const groups = db.prepare(
@@ -1748,7 +1789,7 @@ async function initDb() {
1748
1789
  if (config.DB_DRIVER === "postgres") {
1749
1790
  const { Pool } = await import("pg");
1750
1791
  const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
1751
- const { runPostgresMigrations } = await import("./migrations.postgres-CYKO5CFV.js");
1792
+ const { runPostgresMigrations } = await import("./migrations.postgres-5VI4RDTS.js");
1752
1793
  const pool = new Pool(buildPgConfig());
1753
1794
  adapter = new PostgresAdapter(pool);
1754
1795
  await runPostgresMigrations(adapter);
@@ -0,0 +1,379 @@
1
+ import {
2
+ engineCache,
3
+ indexedNodeIds,
4
+ markIndexStale,
5
+ resolveNestPath,
6
+ setSuggestionFlag
7
+ } from "./chunk-FUUJG6XP.js";
8
+ import {
9
+ getDb
10
+ } from "./chunk-HYSTFMFG.js";
11
+
12
+ // src/governance/external-edit-service.ts
13
+ import { readFile, readdir } from "fs/promises";
14
+ import { join, relative, sep } from "path";
15
+ import {
16
+ detectDrift,
17
+ stageSuggestion,
18
+ ChainEventLog,
19
+ approveSuggestion,
20
+ rejectSuggestion,
21
+ listSuggestions,
22
+ readSuggestion,
23
+ parseDocument,
24
+ VersionManager,
25
+ computeContentHash,
26
+ getChecksumContent
27
+ } from "@promptowl/contextnest-engine";
28
+ var communityRbac = {
29
+ isCzar: () => false,
30
+ canIngest: () => true,
31
+ isDocOwner: () => true
32
+ };
33
+ function docPath(nestId, documentId) {
34
+ return join(resolveNestPath(nestId), `${documentId}.md`);
35
+ }
36
+ async function readRaw(nestId, documentId) {
37
+ try {
38
+ return await readFile(docPath(nestId, documentId), "utf-8");
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+ async function loadChainHead(storage, documentId) {
44
+ const history = await storage.readHistory(documentId);
45
+ if (!history || history.versions.length === 0) return null;
46
+ const latest = history.versions[history.versions.length - 1];
47
+ try {
48
+ const content = await new VersionManager(storage).reconstructVersion(
49
+ documentId,
50
+ latest.version
51
+ );
52
+ return { version: latest.version, content };
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+ async function loadLatestApprovedNode(nestId, documentId) {
58
+ const { storage } = await engineCache.get(nestId);
59
+ const head = await loadChainHead(storage, documentId);
60
+ if (!head) return null;
61
+ return parseDocument(docPath(nestId, documentId), head.content, documentId);
62
+ }
63
+ async function bodyMatchesLatestVersion(storage, documentId, liveBodyHash) {
64
+ const head = await loadChainHead(storage, documentId);
65
+ if (!head) return false;
66
+ const headBodyHash = computeContentHash(getChecksumContent(head.content));
67
+ return headBodyHash === liveBodyHash;
68
+ }
69
+ async function scanDocumentForDrift(nestId, documentId, actor = "system:scanner") {
70
+ const res = await scanDocumentForDriftInternal(nestId, documentId, actor);
71
+ return res?.meta ?? null;
72
+ }
73
+ async function scanDocumentForDriftInternal(nestId, documentId, actor) {
74
+ const { storage } = await engineCache.get(nestId);
75
+ const node = await storage.readDocument(documentId).catch(() => null);
76
+ if (!node) return null;
77
+ const raw = await readRaw(nestId, documentId);
78
+ if (raw == null) return null;
79
+ const drift = detectDrift(raw, node.frontmatter.checksum);
80
+ if (!drift.drifted) return null;
81
+ if (await bodyMatchesLatestVersion(storage, documentId, drift.actualHash)) {
82
+ return null;
83
+ }
84
+ const approved = await loadChainHead(storage, documentId);
85
+ if (!approved) return null;
86
+ const existing = await listSuggestions(storage, documentId);
87
+ const dup = existing.find((s) => s.proposed_hash === drift.actualHash);
88
+ if (dup) {
89
+ await setSuggestionFlag(nestId, documentId, true);
90
+ return { meta: dup, created: false };
91
+ }
92
+ const result = await stageSuggestion({
93
+ storage,
94
+ documentId,
95
+ approvedRawContent: approved.content,
96
+ proposedRawContent: raw,
97
+ source: "out-of-band-edit",
98
+ actor,
99
+ docTier: "standard"
100
+ });
101
+ await setSuggestionFlag(nestId, documentId, true);
102
+ return { meta: result.meta, created: true };
103
+ }
104
+ async function refreshSuggestionFlag(nestId, storage, documentId) {
105
+ try {
106
+ const remaining = await listSuggestions(storage, documentId);
107
+ await setSuggestionFlag(nestId, documentId, remaining.length > 0);
108
+ } catch (err) {
109
+ console.error("[external-edit] flag refresh failed", nestId, documentId, err);
110
+ }
111
+ }
112
+ async function scanNestForDrift(nestId, actor = "system:scanner") {
113
+ const { storage, dropDiscoveryCache } = await engineCache.get(nestId);
114
+ dropDiscoveryCache();
115
+ const docs = await storage.discoverDocuments();
116
+ const results = await Promise.all(
117
+ docs.map((doc) => scanDocumentForDriftInternal(nestId, doc.id, actor))
118
+ );
119
+ const staged = results.filter((r) => r?.created).length;
120
+ const onDisk = await storage.discoverDocuments({ includeRetired: true });
121
+ const indexed = await indexedNodeIds(nestId);
122
+ const membershipMoved = indexed.size !== onDisk.length || onDisk.some((d) => !indexed.has(d.id));
123
+ if (staged > 0 || membershipMoved) await markIndexStale(nestId);
124
+ return { scanned: docs.length, staged };
125
+ }
126
+ async function documentsWithSuggestions(nestId) {
127
+ const root = resolveNestPath(nestId);
128
+ const ids = /* @__PURE__ */ new Set();
129
+ let entries;
130
+ try {
131
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
132
+ } catch (err) {
133
+ if (err?.code !== "ENOENT") {
134
+ console.error("[external-edit] suggestion scan failed", nestId, err);
135
+ }
136
+ return ids;
137
+ }
138
+ for (const e of entries) {
139
+ if (!e.isFile() || !e.name.endsWith(".meta.yaml")) continue;
140
+ const dir = e.parentPath ?? e.path ?? root;
141
+ const rel = relative(root, dir).split(sep).join("/");
142
+ const segs = rel.split("/");
143
+ const marker = segs.lastIndexOf("_suggestions");
144
+ if (marker === -1 || segs.length !== marker + 2) continue;
145
+ ids.add([...segs.slice(0, marker), segs[marker + 1]].filter(Boolean).join("/"));
146
+ }
147
+ return ids;
148
+ }
149
+ async function getPendingChange(nestId, documentId) {
150
+ const { storage } = await engineCache.get(nestId);
151
+ const list = await listSuggestions(storage, documentId);
152
+ for (let i = list.length - 1; i >= 0; i--) {
153
+ const meta = list[i];
154
+ if (await bodyMatchesLatestVersion(storage, documentId, meta.proposed_hash)) {
155
+ continue;
156
+ }
157
+ return {
158
+ suggestion_id: meta.suggestion_id,
159
+ detected_at: meta.detected_at,
160
+ source: meta.source,
161
+ proposed_hash: meta.proposed_hash
162
+ };
163
+ }
164
+ return null;
165
+ }
166
+ async function listNestExternalEdits(nestId) {
167
+ const { storage } = await engineCache.get(nestId);
168
+ const docs = await storage.discoverDocuments();
169
+ const lists = await Promise.all(
170
+ docs.map(async (doc) => {
171
+ const metas = await listSuggestions(storage, doc.id);
172
+ const fresh = [];
173
+ for (const meta of metas) {
174
+ if (await bodyMatchesLatestVersion(storage, doc.id, meta.proposed_hash)) {
175
+ continue;
176
+ }
177
+ fresh.push(meta);
178
+ }
179
+ return fresh;
180
+ })
181
+ );
182
+ const entries = lists.flat().map((meta) => ({
183
+ suggestion_id: meta.suggestion_id,
184
+ nest_id: nestId,
185
+ document_id: meta.document_id,
186
+ source: meta.source,
187
+ detected_at: meta.detected_at,
188
+ actor: meta.actor,
189
+ target_hash: meta.target_hash,
190
+ proposed_hash: meta.proposed_hash,
191
+ note: meta.note
192
+ }));
193
+ return entries.sort((a, b) => b.detected_at.localeCompare(a.detected_at));
194
+ }
195
+ async function getExternalEditDetail(nestId, documentId, suggestionId) {
196
+ const { storage } = await engineCache.get(nestId);
197
+ const found = await readSuggestion(storage, documentId, suggestionId);
198
+ if (!found) return null;
199
+ return {
200
+ suggestion_id: found.meta.suggestion_id,
201
+ nest_id: nestId,
202
+ document_id: found.meta.document_id,
203
+ source: found.meta.source,
204
+ detected_at: found.meta.detected_at,
205
+ actor: found.meta.actor,
206
+ target_hash: found.meta.target_hash,
207
+ proposed_hash: found.meta.proposed_hash,
208
+ note: found.meta.note,
209
+ patch: found.patch
210
+ };
211
+ }
212
+ async function recordVerdict(nestId, event) {
213
+ try {
214
+ const { storage } = await engineCache.get(nestId);
215
+ await new ChainEventLog(storage).append(event);
216
+ return true;
217
+ } catch (err) {
218
+ console.error(
219
+ `[external-edit] failed to record verdict ${event.event_id} for nest ${nestId}:`,
220
+ err
221
+ );
222
+ return false;
223
+ }
224
+ }
225
+ async function listExternalEditVerdicts(nestId, documentId) {
226
+ try {
227
+ const { storage } = await engineCache.get(nestId);
228
+ const events = await new ChainEventLog(storage).readByDocument(documentId);
229
+ return events.flatMap((e) => {
230
+ const meta = e.action_metadata || {};
231
+ if (typeof meta.suggestion_id !== "string") return [];
232
+ const reason = meta.rejection_reason;
233
+ const comment = meta.approval_comment;
234
+ return [
235
+ {
236
+ suggestion_id: meta.suggestion_id,
237
+ status: typeof reason === "string" ? "rejected" : "approved",
238
+ actor: e.actor,
239
+ at: e.timestamp,
240
+ note: typeof reason === "string" ? reason : typeof comment === "string" ? comment : void 0,
241
+ source: typeof meta.source === "string" ? meta.source : void 0
242
+ }
243
+ ];
244
+ });
245
+ } catch (err) {
246
+ console.error(
247
+ `[external-edit] failed to read verdicts for ${nestId}/${documentId}:`,
248
+ err
249
+ );
250
+ return [];
251
+ }
252
+ }
253
+ async function mirrorVersion(input) {
254
+ const { storage } = await engineCache.get(input.nestId);
255
+ const node = await storage.readDocument(input.documentId);
256
+ const { upsertVersion, setApprovedVersion } = await import("./version-service-JWEN5UUW.js");
257
+ await upsertVersion({
258
+ nestId: input.nestId,
259
+ nodeId: input.documentId,
260
+ version: input.version,
261
+ content: node.body || "",
262
+ author: input.actor,
263
+ status: "published",
264
+ tags: node.frontmatter.tags || [],
265
+ changeNote: input.note
266
+ });
267
+ await setApprovedVersion(
268
+ input.nestId,
269
+ input.documentId,
270
+ input.version,
271
+ input.actor
272
+ );
273
+ }
274
+ async function approveExternalEdit(input) {
275
+ const { storage } = await engineCache.get(input.nestId);
276
+ let result;
277
+ try {
278
+ result = await approveSuggestion({
279
+ storage,
280
+ rbac: communityRbac,
281
+ documentId: input.documentId,
282
+ suggestionId: input.suggestionId,
283
+ actor: input.actor,
284
+ zone: "default",
285
+ comment: input.comment
286
+ });
287
+ } catch (err) {
288
+ console.error(
289
+ `[external-edit] approveSuggestion failed for ${input.nestId}/${input.documentId} suggestion=${input.suggestionId}:`,
290
+ err
291
+ );
292
+ throw err;
293
+ }
294
+ try {
295
+ await mirrorVersion({
296
+ nestId: input.nestId,
297
+ documentId: input.documentId,
298
+ version: result.versionEntry.version,
299
+ actor: input.actor,
300
+ note: input.comment ? `External edit approved: ${input.comment}` : "External edit approved"
301
+ });
302
+ } catch (err) {
303
+ console.error(
304
+ `[external-edit] failed to mirror approved version into node_versions for ${input.nestId}/${input.documentId}:`,
305
+ err
306
+ );
307
+ }
308
+ const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
309
+ await refreshSuggestionFlag(input.nestId, storage, input.documentId);
310
+ return { ...result, historyRecorded };
311
+ }
312
+ async function rejectExternalEdit(input) {
313
+ const { storage } = await engineCache.get(input.nestId);
314
+ const result = await rejectSuggestion({
315
+ storage,
316
+ rbac: communityRbac,
317
+ documentId: input.documentId,
318
+ suggestionId: input.suggestionId,
319
+ actor: input.actor,
320
+ zone: "default",
321
+ reason: input.reason
322
+ });
323
+ const historyRecorded = await recordVerdict(input.nestId, result.chainEvent);
324
+ try {
325
+ const approved = await loadChainHead(storage, input.documentId);
326
+ if (approved) {
327
+ await storage.writeDocument(input.documentId, approved.content);
328
+ }
329
+ } catch (err) {
330
+ console.error(
331
+ `[external-edit] revert-on-reject failed for ${input.nestId}/${input.documentId}:`,
332
+ err
333
+ );
334
+ }
335
+ await refreshSuggestionFlag(input.nestId, storage, input.documentId);
336
+ return { ...result, historyRecorded };
337
+ }
338
+ var scannerTimer = null;
339
+ async function scanAllNests() {
340
+ const db = getDb();
341
+ const rows = await db.all("SELECT id FROM nests");
342
+ await Promise.all(
343
+ rows.map(
344
+ ({ id }) => scanNestForDrift(id).catch(
345
+ (err) => console.error(`[external-edit] scan failed for nest ${id}:`, err)
346
+ )
347
+ )
348
+ );
349
+ }
350
+ function startDriftScanner(intervalMs = 3e4) {
351
+ if (scannerTimer) return;
352
+ scannerTimer = setInterval(() => {
353
+ scanAllNests().catch(
354
+ (err) => console.error("[external-edit] scanner tick failed:", err)
355
+ );
356
+ }, intervalMs);
357
+ scannerTimer.unref?.();
358
+ }
359
+ function stopDriftScanner() {
360
+ if (scannerTimer) {
361
+ clearInterval(scannerTimer);
362
+ scannerTimer = null;
363
+ }
364
+ }
365
+
366
+ export {
367
+ loadLatestApprovedNode,
368
+ scanDocumentForDrift,
369
+ scanNestForDrift,
370
+ documentsWithSuggestions,
371
+ getPendingChange,
372
+ listNestExternalEdits,
373
+ getExternalEditDetail,
374
+ listExternalEditVerdicts,
375
+ approveExternalEdit,
376
+ rejectExternalEdit,
377
+ startDriftScanner,
378
+ stopDriftScanner
379
+ };
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GUNJTORH.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-JMJLNEXE.js";
6
+ } from "./chunk-HYSTFMFG.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -11,7 +11,7 @@ import {
11
11
  resolveNestPermission,
12
12
  resolveTeamRolesForUser,
13
13
  sendEmailToRecipient
14
- } from "./chunk-SNFGU5Q7.js";
14
+ } from "./chunk-FUUJG6XP.js";
15
15
  import {
16
16
  ConflictError,
17
17
  ValidationError
@@ -20,7 +20,7 @@ import {
20
20
  config,
21
21
  getDb,
22
22
  isEmailish
23
- } from "./chunk-JMJLNEXE.js";
23
+ } from "./chunk-HYSTFMFG.js";
24
24
 
25
25
  // src/governance/stewardship-service.ts
26
26
  import { v4 as uuid } from "uuid";
@@ -229,7 +229,7 @@ async function createStewardRecord(params) {
229
229
  );
230
230
  let userId = userRow?.id;
231
231
  if (!userId) {
232
- const { hashPassword } = await import("./keys-73STFJJB.js");
232
+ const { hashPassword } = await import("./keys-CTOGAG3W.js");
233
233
  userId = uuid();
234
234
  await db.run(
235
235
  "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
@@ -389,7 +389,7 @@ async function resolveGrantRoles(nestId, userEmail, userId) {
389
389
  )
390
390
  ];
391
391
  }
392
- async function resolveUserRoles(nestId, userEmail, opts) {
392
+ async function resolveNestWideRoles(nestId, userEmail) {
393
393
  const roles = /* @__PURE__ */ new Set();
394
394
  if (isSuperAdmin(userEmail)) roles.add("admin");
395
395
  const owner = await getNestOwnerEmail(nestId);
@@ -404,6 +404,35 @@ async function resolveUserRoles(nestId, userEmail, opts) {
404
404
  roles.add(role);
405
405
  }
406
406
  }
407
+ return [...roles];
408
+ }
409
+ async function stewardCoverageForUser(nestId, userEmail) {
410
+ const db = getDb();
411
+ const rows = await db.all(
412
+ `SELECT scope, node_pattern, tag_name FROM stewards
413
+ WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)`,
414
+ [nestId, userEmail]
415
+ );
416
+ const nodeIds = /* @__PURE__ */ new Set();
417
+ const tags = [];
418
+ for (const r of rows) {
419
+ if (r.scope === "nest") return { nestWide: true, nodeIds };
420
+ if (r.scope === "document" && r.node_pattern) nodeIds.add(r.node_pattern);
421
+ else if (r.scope === "tag" && r.tag_name) tags.push(r.tag_name);
422
+ }
423
+ if (tags.length) {
424
+ const placeholders = tags.map(() => "?").join(", ");
425
+ const tagged = await db.all(
426
+ `SELECT DISTINCT node_id FROM node_tag_index
427
+ WHERE nest_id = ? AND tag_name IN (${placeholders})`,
428
+ [nestId, ...tags]
429
+ );
430
+ for (const t of tagged) nodeIds.add(t.node_id);
431
+ }
432
+ return { nestWide: false, nodeIds };
433
+ }
434
+ async function resolveUserRoles(nestId, userEmail, opts) {
435
+ const roles = new Set(await resolveNestWideRoles(nestId, userEmail));
407
436
  const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
408
437
  (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
409
438
  ).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
@@ -614,6 +643,8 @@ export {
614
643
  getStewardsForUser,
615
644
  getCollaboratorRole,
616
645
  resolveGrantRoles,
646
+ resolveNestWideRoles,
647
+ stewardCoverageForUser,
617
648
  resolveUserRoles,
618
649
  canManageStewards,
619
650
  canCreateInNest,
@@ -1,8 +1,10 @@
1
1
  // src/shared/constants.ts
2
2
  var ANON_USER_ID = "00000000-0000-0000-0000-000000000000";
3
3
  var ANON_EMAIL = "admin@localhost";
4
+ var MAX_FOLDER_DEPTH = 8;
4
5
 
5
6
  export {
6
7
  ANON_USER_ID,
7
- ANON_EMAIL
8
+ ANON_EMAIL,
9
+ MAX_FOLDER_DEPTH
8
10
  };
@@ -2,8 +2,8 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-JMJLNEXE.js";
6
- import "./chunk-SLTQACJW.js";
5
+ } from "./chunk-HYSTFMFG.js";
6
+ import "./chunk-YB3LKF7U.js";
7
7
  export {
8
8
  getDb,
9
9
  initDb,
@@ -0,0 +1,14 @@
1
+ import {
2
+ engineApi,
3
+ engineCache,
4
+ opContext
5
+ } from "./chunk-FUUJG6XP.js";
6
+ import "./chunk-GUNJTORH.js";
7
+ import "./chunk-HYSTFMFG.js";
8
+ import "./chunk-YB3LKF7U.js";
9
+ import "./chunk-FRQJWGN3.js";
10
+ export {
11
+ engineApi,
12
+ engineCache,
13
+ opContext
14
+ };