@promptowl/contextnest-community 1.14.0 → 1.16.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.
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-GUNJTORH.js";
4
4
  import {
5
5
  getDb
6
- } from "./chunk-I5KYGMIT.js";
6
+ } from "./chunk-JMJLNEXE.js";
7
7
 
8
8
  // src/governance/grants-service.ts
9
9
  import { v4 as uuid } from "uuid";
@@ -2,39 +2,45 @@ import {
2
2
  grantCoversNode,
3
3
  listUserGrants,
4
4
  resolveNodeGrant
5
- } from "./chunk-IHGPZ6NY.js";
5
+ } from "./chunk-OTGZQZKK.js";
6
6
  import {
7
7
  createVersion,
8
8
  getApprovedVersion,
9
+ getApprovedVersions,
9
10
  getCurrentVersion,
10
11
  setApprovedVersion
11
- } from "./chunk-JP67WXLA.js";
12
+ } from "./chunk-D6HICFVW.js";
12
13
  import {
13
- buildDocContext,
14
- buildTitleMap,
15
14
  canUserAccess,
16
15
  canUserApprove,
16
+ resolveStewardsForNode
17
+ } from "./chunk-CLKZZZS2.js";
18
+ import {
19
+ buildDocContext,
20
+ buildTitleMap,
17
21
  describeNode,
22
+ engineApi,
18
23
  engineCache,
24
+ insertOrReplace,
19
25
  isPublicReader,
20
26
  isStewardshipEnabled,
27
+ nestName,
21
28
  notifyEmailForNest,
22
- resolveStewardsForNode,
23
- sendEmailToRecipient
24
- } from "./chunk-KPK656BH.js";
29
+ nowExpr,
30
+ opContext,
31
+ prettyFolderPath,
32
+ sendEmailToRecipient,
33
+ titleForNode
34
+ } from "./chunk-L7UCMGWZ.js";
25
35
  import {
26
36
  ConflictError,
27
37
  NotFoundError,
28
38
  ValidationError
29
39
  } from "./chunk-GUNJTORH.js";
30
- import {
31
- insertOrReplace,
32
- nowExpr
33
- } from "./chunk-XQ46F76G.js";
34
40
  import {
35
41
  config,
36
42
  getDb
37
- } from "./chunk-I5KYGMIT.js";
43
+ } from "./chunk-JMJLNEXE.js";
38
44
 
39
45
  // src/governance/review-service.ts
40
46
  import { v4 as uuid3 } from "uuid";
@@ -83,6 +89,7 @@ async function notifyReviewRequested(params) {
83
89
  [nestId, nodeId, nestId, nodeId]
84
90
  );
85
91
  const now = (/* @__PURE__ */ new Date()).toISOString();
92
+ const label = await describeNode(nestId, nodeId);
86
93
  let created = 0;
87
94
  for (const r of rows) {
88
95
  if (r.user_email.toLowerCase() === requestedBy.toLowerCase()) continue;
@@ -94,7 +101,7 @@ async function notifyReviewRequested(params) {
94
101
  nestId,
95
102
  r.user_email.toLowerCase(),
96
103
  reviewId,
97
- r.via === nodeId ? `"${nodeId}" has a new version awaiting review (submitted by ${requestedBy})` : `Agent "${r.via}" produced output on "${nodeId}" \u2014 awaiting review`,
104
+ r.via === nodeId ? `"${label}" has a new version awaiting review (submitted by ${requestedBy})` : `Agent "${await describeNode(nestId, r.via)}" produced output on "${label}" \u2014 awaiting review`,
98
105
  now
99
106
  ]
100
107
  );
@@ -126,6 +133,7 @@ async function notifyReviewResolved(params) {
126
133
  recipients.add(requestedBy.toLowerCase());
127
134
  recipients.delete(resolvedBy.toLowerCase());
128
135
  const now = (/* @__PURE__ */ new Date()).toISOString();
136
+ const label = await describeNode(nestId, nodeId);
129
137
  let created = 0;
130
138
  for (const email of recipients) {
131
139
  await db.run(
@@ -137,7 +145,7 @@ async function notifyReviewResolved(params) {
137
145
  email,
138
146
  `review_${status}`,
139
147
  reviewId,
140
- `"${nodeId}" was ${status} by ${resolvedBy}`,
148
+ `"${label}" was ${status} by ${resolvedBy}`,
141
149
  now
142
150
  ]
143
151
  );
@@ -150,7 +158,7 @@ async function notifyReviewResolved(params) {
150
158
  }
151
159
  }
152
160
  async function notifyNestInvite(params) {
153
- const { nestId, inviteeEmail, permission, nestName } = params;
161
+ const { nestId, inviteeEmail, permission, nestName: nestName2 } = params;
154
162
  try {
155
163
  await getDb().run(
156
164
  `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
@@ -160,7 +168,7 @@ async function notifyNestInvite(params) {
160
168
  nestId,
161
169
  inviteeEmail.toLowerCase(),
162
170
  nestId,
163
- `You were added to "${nestName}" as ${permission}`,
171
+ `You were added to "${nestName2}" as ${permission}`,
164
172
  (/* @__PURE__ */ new Date()).toISOString()
165
173
  ]
166
174
  );
@@ -217,15 +225,10 @@ async function canReadNode(nestId, nodeId, userId, userEmail) {
217
225
  if ((await canUserAccess(nestId, nodeId, userEmail)).allowed) return true;
218
226
  return await resolveNodeGrant(nestId, userId, nodeId) !== null;
219
227
  }
220
- async function filterAccessible(nestId, userId, userEmail, nodes) {
228
+ async function filterAccessible(nestId, userId, userEmail, nodes, approvedVersions) {
221
229
  if (await isPublicReader(nestId, userId)) {
222
- const filtered = [];
223
- for (const n of nodes) {
224
- if (await getApprovedVersion(nestId, n.id) !== null) {
225
- filtered.push(n);
226
- }
227
- }
228
- return filtered;
230
+ const approved = approvedVersions ?? await getApprovedVersions(nestId);
231
+ return nodes.filter((n) => approved.has(n.id));
229
232
  }
230
233
  if (!await isStewardshipEnabled(nestId)) return nodes;
231
234
  const grants = await listUserGrants(nestId, userId);
@@ -628,8 +631,8 @@ function msTeamsPayload(title, message) {
628
631
  }
629
632
  async function notifyMsTeamsForNest(nestId, message) {
630
633
  if (!config.MSTEAMS_WEBHOOK_URL) return;
631
- const nestName = await resolveNestName(nestId);
632
- return notifyMsTeams(nestName, message);
634
+ const nestName2 = await resolveNestName(nestId);
635
+ return notifyMsTeams(nestName2, message);
633
636
  }
634
637
  async function notifyMsTeams(title, message) {
635
638
  const url = config.MSTEAMS_WEBHOOK_URL;
@@ -640,8 +643,8 @@ async function notifyMsTeams(title, message) {
640
643
  // src/notify/slack.ts
641
644
  async function notifySlackForNest(nestId, message) {
642
645
  if (!config.SLACK_WEBHOOK_URL) return;
643
- const nestName = await resolveNestName(nestId);
644
- return notifySlack(`*[${nestName}]* ${message}`);
646
+ const nestName2 = await resolveNestName(nestId);
647
+ return notifySlack(`*[${nestName2}]* ${message}`);
645
648
  }
646
649
  function escapeMrkdwn(s) {
647
650
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
@@ -657,25 +660,31 @@ var EVENT_EMOJI = {
657
660
  review_requested: ":memo:",
658
661
  review_approved: ":white_check_mark:",
659
662
  review_rejected: ":x:",
660
- run_failed: ":rotating_light:"
663
+ run_failed: ":rotating_light:",
664
+ deletion_requested: ":wastebasket:",
665
+ deletion_deleted: ":wastebasket:",
666
+ deletion_declined: ":no_entry_sign:"
661
667
  };
662
668
  var DIGEST_DELIVERED_KINDS = /* @__PURE__ */ new Set([
663
669
  "review_requested",
664
670
  "review_approved",
665
- "review_rejected"
671
+ "review_rejected",
672
+ "deletion_requested",
673
+ "deletion_deleted",
674
+ "deletion_declined"
666
675
  ]);
667
- function formatBody(channel, nestName, ev) {
668
- const line = `[${nestName}] ${ev.message}`;
676
+ function formatBody(channel, nestName2, ev) {
677
+ const line = `[${nestName2}] ${ev.message}`;
669
678
  if (channel === "slack") {
670
- return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${nestName}]* ${ev.message}`) };
679
+ return { text: escapeMrkdwn(`${EVENT_EMOJI[ev.kind] ?? ""} *[${nestName2}]* ${ev.message}`) };
671
680
  }
672
681
  if (channel === "teams") {
673
- return msTeamsPayload(nestName, `${EVENT_EMOJI[ev.kind] ?? ""} ${ev.message}`.trim());
682
+ return msTeamsPayload(nestName2, `${EVENT_EMOJI[ev.kind] ?? ""} ${ev.message}`.trim());
674
683
  }
675
684
  return {
676
685
  kind: ev.kind,
677
686
  nest_id: ev.nestId,
678
- nest_name: nestName,
687
+ nest_name: nestName2,
679
688
  message: ev.message,
680
689
  subject_id: ev.subjectId ?? null,
681
690
  actor: ev.actor ?? null,
@@ -716,18 +725,18 @@ async function resolveUrl(nestId, raw) {
716
725
  }
717
726
  async function dispatchEvent(ev) {
718
727
  const db = getDb();
719
- const nestName = await resolveNestName(ev.nestId);
728
+ const nestName2 = await resolveNestName(ev.nestId);
720
729
  if (config.SLACK_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
721
730
  void postWebhook(
722
731
  config.SLACK_WEBHOOK_URL,
723
- formatBody("slack", nestName, ev),
732
+ formatBody("slack", nestName2, ev),
724
733
  "server-slack"
725
734
  );
726
735
  }
727
736
  if (config.MSTEAMS_WEBHOOK_URL && !DIGEST_DELIVERED_KINDS.has(ev.kind)) {
728
737
  void postWebhook(
729
738
  config.MSTEAMS_WEBHOOK_URL,
730
- formatBody("teams", nestName, ev),
739
+ formatBody("teams", nestName2, ev),
731
740
  "server-msteams"
732
741
  );
733
742
  }
@@ -749,7 +758,7 @@ async function dispatchEvent(ev) {
749
758
  if (!kinds.includes("*") && !kinds.includes(ev.kind)) continue;
750
759
  const url = await resolveUrl(ev.nestId, r.url);
751
760
  if (!url) continue;
752
- void postWebhook(url, formatBody(r.channel, nestName, ev), `connector:${r.id}`);
761
+ void postWebhook(url, formatBody(r.channel, nestName2, ev), `connector:${r.id}`);
753
762
  }
754
763
  }
755
764
  async function sendTestNotification(channel) {
@@ -823,33 +832,6 @@ function withNodeWriteLock(key, fn) {
823
832
  return run;
824
833
  }
825
834
 
826
- // src/governance/safe-publish.ts
827
- import {
828
- publishDocument,
829
- serializeDocument
830
- } from "@promptowl/contextnest-engine";
831
- async function safePublishDocument(storage, docId, options) {
832
- const node = await storage.readDocument(docId);
833
- const cleanedFrontmatter = stripUndefinedDeep(node.frontmatter);
834
- const cleanedNode = { ...node, frontmatter: cleanedFrontmatter };
835
- await storage.writeDocument(docId, serializeDocument(cleanedNode));
836
- return publishDocument(storage, docId, options);
837
- }
838
- function stripUndefinedDeep(value) {
839
- if (Array.isArray(value)) {
840
- return value.filter((v) => v !== void 0).map((v) => stripUndefinedDeep(v));
841
- }
842
- if (value && typeof value === "object") {
843
- const out = {};
844
- for (const [k, v] of Object.entries(value)) {
845
- if (v === void 0) continue;
846
- out[k] = stripUndefinedDeep(v);
847
- }
848
- return out;
849
- }
850
- return value;
851
- }
852
-
853
835
  // src/governance/review-service.ts
854
836
  async function submitForReview(params) {
855
837
  const db = getDb();
@@ -1015,13 +997,14 @@ async function approve(params) {
1015
997
  );
1016
998
  try {
1017
999
  const { storage } = await engineCache.get(params.nestId);
1018
- const result = await safePublishDocument(storage, params.nodeId, {
1019
- editedBy: params.approvedBy,
1020
- note: params.note || `Approved review request ${pending.id}`
1021
- });
1022
- const engineVersion = result.versionEntry.version;
1000
+ const note = params.note || `Approved review request ${pending.id}`;
1001
+ const { version: engineVersion } = await engineApi.run(
1002
+ "context_publish",
1003
+ { id: params.nodeId, note },
1004
+ await opContext(params.nestId, params.approvedBy)
1005
+ );
1023
1006
  if (engineVersion !== version) {
1024
- const node = result.node;
1007
+ const node = await storage.readDocument(params.nodeId);
1025
1008
  const tags = node.frontmatter.tags || [];
1026
1009
  await createVersion({
1027
1010
  nestId: params.nestId,
@@ -1031,7 +1014,7 @@ async function approve(params) {
1031
1014
  author: params.approvedBy,
1032
1015
  status: "published",
1033
1016
  tags,
1034
- changeNote: params.note || `Approved review request ${pending.id}`
1017
+ changeNote: note
1035
1018
  });
1036
1019
  await setApprovedVersion(
1037
1020
  params.nestId,
@@ -1248,6 +1231,297 @@ async function getPendingReview(nestId, nodeId) {
1248
1231
  );
1249
1232
  return row ? rowToReviewRequest(row) : null;
1250
1233
  }
1234
+ async function deletionTargetTitle(request, titles) {
1235
+ if (request.targetType === "folder") return prettyFolderPath(request.nodeId);
1236
+ if (request.targetType === "nest") return await nestName(request.nestId);
1237
+ return titles?.get(request.nodeId) ?? await titleForNode(request.nestId, request.nodeId);
1238
+ }
1239
+ async function nestAdminEmails(nestId) {
1240
+ const db = getDb();
1241
+ const rows = await db.all(
1242
+ `SELECT u.email FROM nests n JOIN users u ON u.id = n.user_id WHERE n.id = ?
1243
+ UNION
1244
+ SELECT u.email FROM nest_collaborators nc JOIN users u ON u.id = nc.user_id
1245
+ WHERE nc.nest_id = ? AND nc.permission IN ('owner', 'admin')`,
1246
+ [nestId, nestId]
1247
+ );
1248
+ return [...new Set(rows.map((r) => r.email.toLowerCase()))];
1249
+ }
1250
+ async function inboxInsert(nestId, recipients, kind, subjectId, message) {
1251
+ const db = getDb();
1252
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1253
+ for (const email of recipients) {
1254
+ try {
1255
+ await db.run(
1256
+ `INSERT INTO notifications (id, nest_id, user_email, kind, subject_id, message, created_at)
1257
+ VALUES (?, ?, ?, ?, ?, ?, ?)`,
1258
+ [uuid3(), nestId, email.toLowerCase(), kind, subjectId, message, now]
1259
+ );
1260
+ } catch (err) {
1261
+ console.error("[notify] deletion inbox insert failed", nestId, email, err);
1262
+ }
1263
+ }
1264
+ }
1265
+ async function deletionTargetContext(request, baseUrl) {
1266
+ if (request.targetType === "document") {
1267
+ const ctx = await buildDocContext(request.nestId, request.nodeId, baseUrl);
1268
+ return { noun: "document", ...ctx };
1269
+ }
1270
+ const name = await nestName(request.nestId);
1271
+ const label = request.targetType === "folder" ? prettyFolderPath(request.nodeId) : name;
1272
+ return {
1273
+ noun: request.targetType,
1274
+ label,
1275
+ docTitle: label,
1276
+ path: name,
1277
+ link: baseUrl ? `${baseUrl}/?nest=${encodeURIComponent(request.nestId)}` : ""
1278
+ };
1279
+ }
1280
+ async function notifyDeletionRequested(request, baseUrl) {
1281
+ try {
1282
+ const ctx = await deletionTargetContext(request, baseUrl);
1283
+ const line = `Deletion requested for ${ctx.noun} *${ctx.label}* by ${request.requestedBy} \u2014 "${request.reason}"`;
1284
+ const admins = (await nestAdminEmails(request.nestId)).filter(
1285
+ (e) => e !== request.requestedBy.toLowerCase()
1286
+ );
1287
+ await inboxInsert(
1288
+ request.nestId,
1289
+ admins,
1290
+ "deletion_requested",
1291
+ request.id,
1292
+ `${ctx.noun === "document" ? "" : `${ctx.noun} `}"${ctx.docTitle}" was flagged for deletion by ${request.requestedBy} \u2014 "${request.reason}"`
1293
+ );
1294
+ void dispatchEvent({
1295
+ kind: "deletion_requested",
1296
+ nestId: request.nestId,
1297
+ subjectId: request.nodeId,
1298
+ actor: request.requestedBy,
1299
+ message: line
1300
+ });
1301
+ void notifyNestEvent(
1302
+ request.nestId,
1303
+ `:wastebasket: ${line}`,
1304
+ {
1305
+ status: "deletion_requested",
1306
+ docTitle: ctx.docTitle,
1307
+ path: ctx.path,
1308
+ link: ctx.link,
1309
+ actor: request.requestedBy,
1310
+ note: request.reason
1311
+ },
1312
+ // The admins are emailed directly below; don't also hit the shared
1313
+ // NOTIFY_EMAIL_TO team inbox with the same event.
1314
+ { skipEmail: true }
1315
+ );
1316
+ if (admins.length > 0) {
1317
+ void sendEmailToRecipient(admins, {
1318
+ status: "deletion_requested",
1319
+ docTitle: ctx.docTitle,
1320
+ path: ctx.path,
1321
+ link: ctx.link,
1322
+ actor: request.requestedBy,
1323
+ note: request.reason
1324
+ });
1325
+ }
1326
+ } catch (err) {
1327
+ console.error(
1328
+ "[notify] deletion request fan-out failed",
1329
+ request.nestId,
1330
+ request.nodeId,
1331
+ err
1332
+ );
1333
+ }
1334
+ }
1335
+ async function notifyDeletionResolved(params) {
1336
+ try {
1337
+ const { outcome, resolvedBy, note } = params;
1338
+ const verb = outcome === "deleted" ? "deleted" : "rejected the deletion of";
1339
+ const line = `${resolvedBy} ${verb} *${params.docTitle}*${note ? ` \u2014 "${note}"` : ""}`;
1340
+ const recipient = params.requestedBy.toLowerCase();
1341
+ const canInbox = !(params.targetType === "nest" && outcome === "deleted");
1342
+ if (canInbox && recipient !== resolvedBy.toLowerCase()) {
1343
+ await inboxInsert(
1344
+ params.nestId,
1345
+ [recipient],
1346
+ `deletion_${outcome}`,
1347
+ params.requestId,
1348
+ outcome === "deleted" ? `"${params.docTitle}" was deleted by ${resolvedBy} \u2014 your request was accepted` : `Your deletion request for "${params.docTitle}" was rejected by ${resolvedBy}${note ? ` \u2014 "${note}"` : ""}`
1349
+ );
1350
+ }
1351
+ void dispatchEvent({
1352
+ kind: `deletion_${outcome}`,
1353
+ nestId: params.nestId,
1354
+ subjectId: params.nodeId,
1355
+ actor: resolvedBy,
1356
+ message: line
1357
+ });
1358
+ void notifyNestEvent(
1359
+ params.nestId,
1360
+ `${outcome === "deleted" ? ":wastebasket:" : ":no_entry_sign:"} ${line}`,
1361
+ void 0,
1362
+ // Verdict emails the requester directly (below), not the team inbox.
1363
+ { skipEmail: true }
1364
+ );
1365
+ if (recipient !== resolvedBy.toLowerCase()) {
1366
+ const link = params.baseUrl ? `${params.baseUrl}/?nest=${encodeURIComponent(params.nestId)}` : "";
1367
+ void sendEmailToRecipient(recipient, {
1368
+ status: outcome === "deleted" ? "deletion_deleted" : "deletion_declined",
1369
+ docTitle: params.docTitle,
1370
+ path: params.nestId,
1371
+ link: outcome === "deleted" ? link : void 0,
1372
+ actor: resolvedBy,
1373
+ note: note || null
1374
+ });
1375
+ }
1376
+ } catch (err) {
1377
+ console.error(
1378
+ "[notify] deletion verdict fan-out failed",
1379
+ params.nestId,
1380
+ params.nodeId,
1381
+ err
1382
+ );
1383
+ }
1384
+ }
1385
+ async function requestDeletion(params) {
1386
+ const db = getDb();
1387
+ const targetType = params.targetType || "document";
1388
+ const noun = targetType === "document" ? "node" : targetType === "folder" ? "folder" : "nest";
1389
+ const existing = await db.get(
1390
+ "SELECT id FROM deletion_requests WHERE nest_id = ? AND node_id = ? AND target_type = ? AND status = 'pending'",
1391
+ [params.nestId, params.nodeId, targetType]
1392
+ );
1393
+ if (existing) {
1394
+ throw new ConflictError(`A deletion request is already pending for this ${noun}`);
1395
+ }
1396
+ const id = uuid3();
1397
+ try {
1398
+ await db.run(
1399
+ `INSERT INTO deletion_requests (id, nest_id, node_id, target_type, requested_by, reason)
1400
+ VALUES (?, ?, ?, ?, ?, ?)`,
1401
+ [
1402
+ id,
1403
+ params.nestId,
1404
+ params.nodeId,
1405
+ targetType,
1406
+ params.requestedBy,
1407
+ params.reason
1408
+ ]
1409
+ );
1410
+ } catch (err) {
1411
+ console.error(
1412
+ "[deletion] request insert failed",
1413
+ params.nestId,
1414
+ params.nodeId,
1415
+ err
1416
+ );
1417
+ throw new ConflictError(`A deletion request is already pending for this ${noun}`);
1418
+ }
1419
+ const request = await getDeletionRequest(id);
1420
+ await notifyDeletionRequested(request, params.baseUrl);
1421
+ return request;
1422
+ }
1423
+ async function getLatestDeletionRequest(nestId, nodeId) {
1424
+ const db = getDb();
1425
+ const row = await db.get(
1426
+ `SELECT * FROM deletion_requests WHERE nest_id = ? AND node_id = ?
1427
+ ORDER BY created_at DESC LIMIT 1`,
1428
+ [nestId, nodeId]
1429
+ );
1430
+ return row ? rowToDeletionRequest(row) : null;
1431
+ }
1432
+ async function getDeletionRequest(id) {
1433
+ const db = getDb();
1434
+ const row = await db.get(
1435
+ "SELECT * FROM deletion_requests WHERE id = ?",
1436
+ [id]
1437
+ );
1438
+ return row ? rowToDeletionRequest(row) : null;
1439
+ }
1440
+ async function listDeletionRequests(params) {
1441
+ const db = getDb();
1442
+ const status = params.status || "pending";
1443
+ const where = [];
1444
+ const args = [];
1445
+ if (params.nestId) {
1446
+ where.push("nest_id = ?");
1447
+ args.push(params.nestId);
1448
+ }
1449
+ if (params.nestIds) {
1450
+ if (params.nestIds.length === 0) return [];
1451
+ where.push(`nest_id IN (${params.nestIds.map(() => "?").join(",")})`);
1452
+ args.push(...params.nestIds);
1453
+ }
1454
+ if (status !== "all") {
1455
+ where.push("status = ?");
1456
+ args.push(status);
1457
+ }
1458
+ const rows = await db.all(
1459
+ `SELECT * FROM deletion_requests${where.length ? ` WHERE ${where.join(" AND ")}` : ""} ORDER BY created_at DESC`,
1460
+ args
1461
+ );
1462
+ if (rows.length === 0) return [];
1463
+ const titlesByNest = /* @__PURE__ */ new Map();
1464
+ for (const nid of new Set(rows.map((r) => r.nest_id))) {
1465
+ titlesByNest.set(nid, await buildTitleMap(nid));
1466
+ }
1467
+ const out = [];
1468
+ for (const r of rows) {
1469
+ const request = rowToDeletionRequest(r);
1470
+ out.push({
1471
+ ...request,
1472
+ title: await deletionTargetTitle(request, titlesByNest.get(r.nest_id))
1473
+ });
1474
+ }
1475
+ return out;
1476
+ }
1477
+ async function dropDeletionRequest(id) {
1478
+ await getDb().run("DELETE FROM deletion_requests WHERE id = ?", [id]);
1479
+ }
1480
+ async function declineDeletion(params) {
1481
+ const db = getDb();
1482
+ const pending = await getDeletionRequest(params.id);
1483
+ if (!pending || pending.nestId !== params.nestId || pending.status !== "pending") {
1484
+ return null;
1485
+ }
1486
+ const { changes } = await db.run(
1487
+ `UPDATE deletion_requests
1488
+ SET status = 'declined', resolved_by = ?, resolved_at = ${nowExpr(db)},
1489
+ resolution_note = ?
1490
+ WHERE id = ? AND status = 'pending'`,
1491
+ [params.declinedBy, params.note, params.id]
1492
+ );
1493
+ if (changes === 0) return null;
1494
+ const declined = await getDeletionRequest(params.id);
1495
+ if (declined) {
1496
+ await notifyDeletionResolved({
1497
+ nestId: params.nestId,
1498
+ nodeId: declined.nodeId,
1499
+ docTitle: await deletionTargetTitle(declined),
1500
+ outcome: "declined",
1501
+ requestedBy: declined.requestedBy,
1502
+ resolvedBy: params.declinedBy,
1503
+ requestId: declined.id,
1504
+ note: params.note,
1505
+ baseUrl: params.baseUrl
1506
+ });
1507
+ }
1508
+ return declined;
1509
+ }
1510
+ function rowToDeletionRequest(row) {
1511
+ return {
1512
+ id: row.id,
1513
+ nestId: row.nest_id,
1514
+ nodeId: row.node_id,
1515
+ targetType: row.target_type || "document",
1516
+ requestedBy: row.requested_by,
1517
+ reason: row.reason,
1518
+ status: row.status,
1519
+ createdAt: row.created_at,
1520
+ resolvedBy: row.resolved_by || void 0,
1521
+ resolvedAt: row.resolved_at || void 0,
1522
+ resolutionNote: row.resolution_note || void 0
1523
+ };
1524
+ }
1251
1525
  async function getReviewRequest(id) {
1252
1526
  const db = getDb();
1253
1527
  const row = await db.get(
@@ -1274,8 +1548,6 @@ function rowToReviewRequest(row) {
1274
1548
  }
1275
1549
 
1276
1550
  export {
1277
- safePublishDocument,
1278
- stripUndefinedDeep,
1279
1551
  listWatchers,
1280
1552
  addWatcher,
1281
1553
  removeWatcher,
@@ -1306,5 +1578,13 @@ export {
1306
1578
  cancelReview,
1307
1579
  getReviewQueue,
1308
1580
  getReviewHistory,
1309
- getPendingReview
1581
+ getPendingReview,
1582
+ deletionTargetTitle,
1583
+ notifyDeletionResolved,
1584
+ requestDeletion,
1585
+ getLatestDeletionRequest,
1586
+ getDeletionRequest,
1587
+ listDeletionRequests,
1588
+ dropDeletionRequest,
1589
+ declineDeletion
1310
1590
  };
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  initDb,
4
4
  resetDb
5
- } from "./chunk-I5KYGMIT.js";
5
+ } from "./chunk-JMJLNEXE.js";
6
6
  import "./chunk-SLTQACJW.js";
7
7
  export {
8
8
  getDb,
@@ -6,9 +6,9 @@ import {
6
6
  listGrants,
7
7
  listUserGrants,
8
8
  resolveNodeGrant
9
- } from "./chunk-IHGPZ6NY.js";
9
+ } from "./chunk-OTGZQZKK.js";
10
10
  import "./chunk-GUNJTORH.js";
11
- import "./chunk-I5KYGMIT.js";
11
+ import "./chunk-JMJLNEXE.js";
12
12
  import "./chunk-SLTQACJW.js";
13
13
  export {
14
14
  createGrant,