@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.
@@ -4,209 +4,27 @@ import {
4
4
  NotFoundError,
5
5
  ValidationError
6
6
  } from "./chunk-GUNJTORH.js";
7
- import {
8
- normalizeEmail
9
- } from "./chunk-FRQJWGN3.js";
10
- import {
11
- insertOrReplace,
12
- nowExpr
13
- } from "./chunk-XQ46F76G.js";
14
7
  import {
15
8
  config,
16
9
  getDb,
17
10
  isEmailish
18
- } from "./chunk-I5KYGMIT.js";
11
+ } from "./chunk-JMJLNEXE.js";
19
12
  import {
20
13
  ANON_USER_ID
21
14
  } from "./chunk-SLTQACJW.js";
22
-
23
- // src/governance/stewardship-service.ts
24
- import { v4 as uuid3 } from "uuid";
25
-
26
- // src/governance/access-service.ts
27
- import { readFileSync, existsSync } from "fs";
28
- import { join } from "path";
29
- var accessConfig = null;
30
- var grantedSuperAdmins = /* @__PURE__ */ new Set();
31
- async function loadGrantedSuperAdmins() {
32
- const rows = await getDb().all(
33
- "SELECT email FROM super_admins"
34
- );
35
- grantedSuperAdmins = new Set(rows.map((r) => r.email.toLowerCase()));
36
- }
37
- function loadAccessConfig() {
38
- const candidates = [
39
- join(config.DATA_ROOT, "access.yaml"),
40
- join(config.DATA_ROOT, "access.yml")
41
- ];
42
- for (const path of candidates) {
43
- if (existsSync(path)) {
44
- const content = readFileSync(path, "utf-8");
45
- accessConfig = parseAccessYaml(content);
46
- return accessConfig;
47
- }
48
- }
49
- accessConfig = null;
50
- return null;
51
- }
52
- function getAccessConfig() {
53
- return accessConfig;
54
- }
55
- function isSuperAdmin(email) {
56
- const lower = email.toLowerCase();
57
- if (grantedSuperAdmins.has(lower)) return true;
58
- return isConfigSuperAdmin(lower);
59
- }
60
- function isConfigSuperAdmin(email) {
61
- if (!accessConfig?.super_admins) return false;
62
- return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
63
- }
64
- function parseAccessYaml(content) {
65
- const result = {};
66
- const lines = content.split("\n");
67
- let currentSection = null;
68
- let currentGroup = null;
69
- let inMembers = false;
70
- for (const rawLine of lines) {
71
- const line = rawLine.trimEnd();
72
- if (!line || line.startsWith("#")) continue;
73
- if (!line.startsWith(" ") && !line.startsWith(" ")) {
74
- const match = line.match(/^(\w+):\s*(.*)?$/);
75
- if (!match) continue;
76
- const key = match[1];
77
- const value = match[2]?.trim();
78
- if (key === "mode") {
79
- result.mode = value;
80
- currentSection = null;
81
- } else if (key === "allowed_users") {
82
- currentSection = "allowed_users";
83
- result.allowed_users = [];
84
- } else if (key === "groups") {
85
- currentSection = "groups";
86
- result.groups = {};
87
- } else if (key === "super_admins") {
88
- currentSection = "super_admins";
89
- result.super_admins = [];
90
- }
91
- currentGroup = null;
92
- inMembers = false;
93
- continue;
94
- }
95
- const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
96
- if (currentSection === "allowed_users" && listMatch) {
97
- result.allowed_users.push(listMatch[1].trim());
98
- continue;
99
- }
100
- if (currentSection === "super_admins" && listMatch) {
101
- result.super_admins.push(listMatch[1].trim());
102
- continue;
103
- }
104
- if (currentSection === "groups") {
105
- const groupMatch = line.match(/^ (\w+):$/);
106
- if (groupMatch) {
107
- currentGroup = groupMatch[1];
108
- result.groups[currentGroup] = { members: [], default_permission: "read" };
109
- inMembers = false;
110
- continue;
111
- }
112
- if (currentGroup) {
113
- const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
114
- if (propMatch) {
115
- const prop = propMatch[1];
116
- const val = propMatch[2]?.trim();
117
- if (prop === "default_permission" && val) {
118
- result.groups[currentGroup].default_permission = val;
119
- }
120
- if (prop === "members") {
121
- inMembers = true;
122
- }
123
- continue;
124
- }
125
- if (inMembers && listMatch) {
126
- result.groups[currentGroup].members.push(listMatch[1].trim());
127
- }
128
- }
129
- }
130
- }
131
- return result;
132
- }
133
-
134
- // src/nodes/engine.ts
135
15
  import {
136
- NestStorage as NestStorage3,
137
- GraphQueryEngine,
138
- VersionManager
139
- } from "@promptowl/contextnest-engine";
140
-
141
- // src/nests/service.ts
142
- import { rmSync as rmSync2, mkdirSync } from "fs";
143
- import { v4 as uuid2 } from "uuid";
144
- import { NestStorage } from "@promptowl/contextnest-engine";
16
+ normalizeEmail
17
+ } from "./chunk-FRQJWGN3.js";
145
18
 
146
- // src/fs/vault-import.ts
147
- import { rmSync } from "fs";
148
- import { writeFile, mkdir } from "fs/promises";
149
- import { join as join2, dirname, isAbsolute } from "path";
150
- function isSkippedSegment(segment) {
151
- return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
19
+ // src/db/sql.ts
20
+ function nowExpr(db) {
21
+ return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
152
22
  }
153
- var ALLOWED_DOT_DIRS = /* @__PURE__ */ new Set([".versions", ".context"]);
154
- function safeSegments(rel) {
155
- if (!rel || isAbsolute(rel)) return null;
156
- const segs = rel.split(/[\\/]/).filter(Boolean);
157
- if (!segs.length) return null;
158
- for (const s of segs) {
159
- if (s === "..") return null;
160
- if (!ALLOWED_DOT_DIRS.has(s) && isSkippedSegment(s)) return null;
161
- }
162
- return segs;
163
- }
164
- var WRITE_CONCURRENCY = 32;
165
- async function writeImportedFiles(dest, files) {
166
- const targets = [];
167
- const skipped = [];
168
- for (const f of files) {
169
- const segs = safeSegments(f?.path || "");
170
- if (!segs) {
171
- skipped.push(f?.path || "(empty)");
172
- continue;
173
- }
174
- targets.push({ full: join2(dest, ...segs), content: f.content ?? "" });
175
- }
176
- const dirs = new Set(targets.map((t) => dirname(t.full)));
177
- await Promise.all([...dirs].map((d) => mkdir(d, { recursive: true })));
178
- let written = 0;
179
- for (let i = 0; i < targets.length; i += WRITE_CONCURRENCY) {
180
- const batch = targets.slice(i, i + WRITE_CONCURRENCY);
181
- await Promise.all(
182
- batch.map(async (t) => {
183
- await writeFile(t.full, t.content, "utf-8");
184
- written++;
185
- })
186
- );
187
- }
188
- console.log(
189
- `[import] writeImportedFiles: received=${files.length} written=${written} skipped=${skipped.length}`
190
- );
191
- if (skipped.length) {
192
- console.log("[import] skipped (unsafe/hidden path):", skipped.join(", "));
193
- }
194
- return written;
195
- }
196
- function discardImportDir(dest) {
197
- try {
198
- rmSync(dest, { recursive: true, force: true });
199
- } catch {
200
- }
201
- }
202
-
203
- // src/shared/paths.ts
204
- import { join as join3 } from "path";
205
- function nestStorageRoot() {
206
- return config.NEST_STORAGE_ROOT;
23
+ function insertOrIgnore(db, insertSql) {
24
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
207
25
  }
208
- function resolveNestPath(nestId) {
209
- return join3(nestStorageRoot(), nestId);
26
+ function insertOrReplace(db, insertSql, conflictCols, set) {
27
+ return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
210
28
  }
211
29
 
212
30
  // src/telemetry/tracker.ts
@@ -640,8 +458,113 @@ async function _validateLicenseImpl(forceFresh) {
640
458
  }
641
459
  }
642
460
 
643
- // src/governance/teams-service.ts
644
- import { v4 as uuid } from "uuid";
461
+ // src/governance/access-service.ts
462
+ import { readFileSync, existsSync } from "fs";
463
+ import { join } from "path";
464
+ var accessConfig = null;
465
+ var grantedSuperAdmins = /* @__PURE__ */ new Set();
466
+ async function loadGrantedSuperAdmins() {
467
+ const rows = await getDb().all(
468
+ "SELECT email FROM super_admins"
469
+ );
470
+ grantedSuperAdmins = new Set(rows.map((r) => r.email.toLowerCase()));
471
+ }
472
+ function loadAccessConfig() {
473
+ const candidates = [
474
+ join(config.DATA_ROOT, "access.yaml"),
475
+ join(config.DATA_ROOT, "access.yml")
476
+ ];
477
+ for (const path of candidates) {
478
+ if (existsSync(path)) {
479
+ const content = readFileSync(path, "utf-8");
480
+ accessConfig = parseAccessYaml(content);
481
+ return accessConfig;
482
+ }
483
+ }
484
+ accessConfig = null;
485
+ return null;
486
+ }
487
+ function getAccessConfig() {
488
+ return accessConfig;
489
+ }
490
+ function isSuperAdmin(email) {
491
+ const lower = email.toLowerCase();
492
+ if (grantedSuperAdmins.has(lower)) return true;
493
+ return isConfigSuperAdmin(lower);
494
+ }
495
+ function isConfigSuperAdmin(email) {
496
+ if (!accessConfig?.super_admins) return false;
497
+ return accessConfig.super_admins.map((e) => e.toLowerCase()).includes(email.toLowerCase());
498
+ }
499
+ function parseAccessYaml(content) {
500
+ const result = {};
501
+ const lines = content.split("\n");
502
+ let currentSection = null;
503
+ let currentGroup = null;
504
+ let inMembers = false;
505
+ for (const rawLine of lines) {
506
+ const line = rawLine.trimEnd();
507
+ if (!line || line.startsWith("#")) continue;
508
+ if (!line.startsWith(" ") && !line.startsWith(" ")) {
509
+ const match = line.match(/^(\w+):\s*(.*)?$/);
510
+ if (!match) continue;
511
+ const key = match[1];
512
+ const value = match[2]?.trim();
513
+ if (key === "mode") {
514
+ result.mode = value;
515
+ currentSection = null;
516
+ } else if (key === "allowed_users") {
517
+ currentSection = "allowed_users";
518
+ result.allowed_users = [];
519
+ } else if (key === "groups") {
520
+ currentSection = "groups";
521
+ result.groups = {};
522
+ } else if (key === "super_admins") {
523
+ currentSection = "super_admins";
524
+ result.super_admins = [];
525
+ }
526
+ currentGroup = null;
527
+ inMembers = false;
528
+ continue;
529
+ }
530
+ const listMatch = line.match(/^\s+-\s+["']?([^"'\n]+?)["']?\s*$/);
531
+ if (currentSection === "allowed_users" && listMatch) {
532
+ result.allowed_users.push(listMatch[1].trim());
533
+ continue;
534
+ }
535
+ if (currentSection === "super_admins" && listMatch) {
536
+ result.super_admins.push(listMatch[1].trim());
537
+ continue;
538
+ }
539
+ if (currentSection === "groups") {
540
+ const groupMatch = line.match(/^ (\w+):$/);
541
+ if (groupMatch) {
542
+ currentGroup = groupMatch[1];
543
+ result.groups[currentGroup] = { members: [], default_permission: "read" };
544
+ inMembers = false;
545
+ continue;
546
+ }
547
+ if (currentGroup) {
548
+ const propMatch = line.match(/^\s{4}(\w+):\s*(.*)?$/);
549
+ if (propMatch) {
550
+ const prop = propMatch[1];
551
+ const val = propMatch[2]?.trim();
552
+ if (prop === "default_permission" && val) {
553
+ result.groups[currentGroup].default_permission = val;
554
+ }
555
+ if (prop === "members") {
556
+ inMembers = true;
557
+ }
558
+ continue;
559
+ }
560
+ if (inMembers && listMatch) {
561
+ result.groups[currentGroup].members.push(listMatch[1].trim());
562
+ }
563
+ }
564
+ }
565
+ }
566
+ return result;
567
+ }
645
568
 
646
569
  // src/notify/email-render.ts
647
570
  function escapeHtml(s) {
@@ -669,9 +592,24 @@ var STATUS_META = {
669
592
  },
670
593
  shared: { subjectWord: "shared with you", label: "Shared", color: "#2563eb" },
671
594
  invited: { subjectWord: "invited you", label: "Invitation", color: "#2563eb" },
672
- steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" }
673
- };
674
- var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
595
+ steward: { subjectWord: "steward", label: "Steward", color: "#7c3aed" },
596
+ deletion_requested: {
597
+ subjectWord: "flagged for deletion",
598
+ label: "Deletion requested",
599
+ color: "#d97706"
600
+ },
601
+ deletion_declined: {
602
+ subjectWord: "kept",
603
+ label: "Deletion rejected",
604
+ color: "#d97706"
605
+ },
606
+ deletion_deleted: {
607
+ subjectWord: "deleted",
608
+ label: "Deleted",
609
+ color: "#dc2626"
610
+ }
611
+ };
612
+ var cap = (s) => s ? s.charAt(0).toUpperCase() + s.slice(1) : s;
675
613
  var detailRow = (label, valueHtml) => `<tr>
676
614
  <td style="padding:5px 0;font-size:13px;color:#6b7280;width:90px;vertical-align:top;">${label}</td>
677
615
  <td style="padding:5px 0;font-size:13px;color:#111827;vertical-align:top;">${valueHtml}</td>
@@ -739,6 +677,32 @@ var TEMPLATES = {
739
677
  ] : []
740
678
  ]
741
679
  },
680
+ // Deletion requests reuse the governance details block — same "what/where/
681
+ // status" shape, and the reason/note rides in `note` like a review note.
682
+ deletion_requested: {
683
+ heading: (x) => `${x.title} is ${x.meta.subjectWord}`,
684
+ detailsHeader: "Document details",
685
+ linkLabel: "Review the request",
686
+ lineT: (x) => `${x.title} has been flagged for deletion${x.byT}${x.noteT}. Delete it or reject the request from the nest.`,
687
+ lineH: (x) => `${x.doc} has been flagged for deletion${x.byH}${x.noteH}. Delete it or reject the request from the nest.`,
688
+ rows: governanceRows
689
+ },
690
+ deletion_declined: {
691
+ heading: (x) => `Your deletion request for ${x.title} was rejected`,
692
+ detailsHeader: "Document details",
693
+ linkLabel: "Open document",
694
+ lineT: (x) => `Your request to delete ${x.title} was rejected${x.byT}${x.noteT}. The document stays.`,
695
+ lineH: (x) => `Your request to delete ${x.doc} was rejected${x.byH}${x.noteH}. The document stays.`,
696
+ rows: governanceRows
697
+ },
698
+ deletion_deleted: {
699
+ heading: (x) => `${x.title} was ${x.meta.subjectWord}`,
700
+ detailsHeader: "Document details",
701
+ linkLabel: "Open nest",
702
+ lineT: (x) => `${x.title} has been deleted${x.byT}${x.noteT} \u2014 your deletion request was accepted.`,
703
+ lineH: (x) => `${x.doc} has been deleted${x.byH}${x.noteH} \u2014 your deletion request was accepted.`,
704
+ rows: governanceRows
705
+ },
742
706
  steward: {
743
707
  heading: (x) => `You're now a steward of ${x.title}`,
744
708
  detailsHeader: "Details",
@@ -1008,6 +972,225 @@ async function sendEmailToRecipient(to, details) {
1008
972
  }
1009
973
  }
1010
974
 
975
+ // src/nests/service.ts
976
+ import { mkdirSync } from "fs";
977
+ import { v4 as uuid2 } from "uuid";
978
+ import { NestStorage as NestStorage3 } from "@promptowl/contextnest-engine";
979
+
980
+ // src/shared/paths.ts
981
+ import { join as join2 } from "path";
982
+ function nestStorageRoot() {
983
+ return config.NEST_STORAGE_ROOT;
984
+ }
985
+ function resolveNestPath(nestId) {
986
+ return join2(nestStorageRoot(), nestId);
987
+ }
988
+
989
+ // src/governance/teams-service.ts
990
+ import { v4 as uuid } from "uuid";
991
+
992
+ // src/nodes/engine.ts
993
+ import {
994
+ NestStorage as NestStorage2,
995
+ GraphQueryEngine,
996
+ VersionManager
997
+ } from "@promptowl/contextnest-engine";
998
+ import { createEngineApi } from "@promptowl/contextnest-engine/api";
999
+
1000
+ // src/nodes/flat-storage.ts
1001
+ import { readdir } from "fs/promises";
1002
+ import { join as join3, relative, sep } from "path";
1003
+ import { NestStorage } from "@promptowl/contextnest-engine";
1004
+
1005
+ // src/fs/vault-import.ts
1006
+ function isSkippedSegment(segment) {
1007
+ return segment.startsWith(".") || segment === "node_modules" || segment === "_suggestions";
1008
+ }
1009
+
1010
+ // src/shared/batch.ts
1011
+ var IO_CONCURRENCY = 32;
1012
+ async function mapBatched(items, fn, concurrency = IO_CONCURRENCY) {
1013
+ const out = [];
1014
+ for (let i = 0; i < items.length; i += concurrency) {
1015
+ out.push(...await Promise.all(items.slice(i, i + concurrency).map(fn)));
1016
+ }
1017
+ return out;
1018
+ }
1019
+
1020
+ // src/nodes/flat-storage.ts
1021
+ var META_FILES = /* @__PURE__ */ new Set([
1022
+ "INDEX.md",
1023
+ "CONTEXT.md",
1024
+ "CLAUDE.md",
1025
+ "GEMINI.md",
1026
+ "AGENTS.md",
1027
+ "QWEN.md"
1028
+ ]);
1029
+ var FlatNestStorage = class extends NestStorage {
1030
+ async discoverDocuments() {
1031
+ const root = this.root;
1032
+ let entries;
1033
+ try {
1034
+ entries = await readdir(root, { recursive: true, withFileTypes: true });
1035
+ } catch (err) {
1036
+ console.error("[FlatNestStorage] cannot read folder", root, err);
1037
+ return [];
1038
+ }
1039
+ const ids = [];
1040
+ for (const e of entries) {
1041
+ if (!e.isFile() || !e.name.endsWith(".md")) continue;
1042
+ if (META_FILES.has(e.name)) continue;
1043
+ const dir = e.parentPath ?? e.path ?? root;
1044
+ const rel = relative(root, join3(dir, e.name)).split(sep).join("/");
1045
+ if (rel.split("/").some(isSkippedSegment)) continue;
1046
+ ids.push(rel.replace(/\.md$/, ""));
1047
+ }
1048
+ ids.sort();
1049
+ const read = await mapBatched(ids, async (id) => {
1050
+ try {
1051
+ return await this.readDocument(id);
1052
+ } catch (err) {
1053
+ console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1054
+ return null;
1055
+ }
1056
+ });
1057
+ return read.filter((n) => n !== null);
1058
+ }
1059
+ };
1060
+
1061
+ // src/nodes/engine.ts
1062
+ var DISCOVERY_TTL_MS = 3e4;
1063
+ var DOCUMENT_WRITERS = [
1064
+ "writeDocument",
1065
+ "deleteDocument",
1066
+ "writeVaultFile",
1067
+ "init"
1068
+ ];
1069
+ function withDiscoveryCache(storage) {
1070
+ const entries = /* @__PURE__ */ new Map();
1071
+ const discover = storage.discoverDocuments.bind(storage);
1072
+ let generation = 0;
1073
+ storage.discoverDocuments = async (options = {}) => {
1074
+ const key = options.includeRetired || options.includeSuperseded ? "all" : "live";
1075
+ const hit = entries.get(key);
1076
+ if (hit && Date.now() - hit.ts < DISCOVERY_TTL_MS) return hit.docs;
1077
+ const startedAt = generation;
1078
+ const docs = await discover(options);
1079
+ if (generation === startedAt) entries.set(key, { docs, ts: Date.now() });
1080
+ return docs;
1081
+ };
1082
+ const invalidate = () => {
1083
+ generation++;
1084
+ entries.clear();
1085
+ };
1086
+ for (const name of DOCUMENT_WRITERS) {
1087
+ const write = storage[name].bind(storage);
1088
+ storage[name] = async (...args) => {
1089
+ try {
1090
+ return await write(...args);
1091
+ } finally {
1092
+ invalidate();
1093
+ }
1094
+ };
1095
+ }
1096
+ return { storage, invalidate };
1097
+ }
1098
+ var NestEngineCache = class {
1099
+ cache = /* @__PURE__ */ new Map();
1100
+ async get(nestId) {
1101
+ let engine = this.cache.get(nestId);
1102
+ if (!engine) {
1103
+ const nestPath = resolveNestPath(nestId);
1104
+ const { storage, invalidate } = withDiscoveryCache(
1105
+ await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage2(nestPath)
1106
+ );
1107
+ const query = new GraphQueryEngine(storage);
1108
+ const versions = new VersionManager(storage);
1109
+ engine = { storage, query, versions, invalidateDiscovery: invalidate };
1110
+ this.cache.set(nestId, engine);
1111
+ }
1112
+ return engine;
1113
+ }
1114
+ evict(nestId) {
1115
+ this.cache.delete(nestId);
1116
+ }
1117
+ };
1118
+ var engineCache = new NestEngineCache();
1119
+ var engineApi = createEngineApi();
1120
+ async function opContext(nestId, actor, onProgress) {
1121
+ const { storage, query, versions } = await engineCache.get(nestId);
1122
+ return { storage, query, versions, actor, onProgress };
1123
+ }
1124
+
1125
+ // src/governance/title-resolver.ts
1126
+ async function buildTitleMap(nestId) {
1127
+ try {
1128
+ const { storage } = await engineCache.get(nestId);
1129
+ const docs = await storage.discoverDocuments();
1130
+ return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1131
+ } catch {
1132
+ return /* @__PURE__ */ new Map();
1133
+ }
1134
+ }
1135
+ function folderForNode(nodeId) {
1136
+ const segments = nodeId.split("/");
1137
+ const root = segments.indexOf("nodes");
1138
+ return segments.slice(root + 1, -1).join("/");
1139
+ }
1140
+ async function describeNode(nestId, nodeId) {
1141
+ const title = await titleForNode(nestId, nodeId);
1142
+ const folder = folderForNode(nodeId);
1143
+ return folder ? `${title} (in ${folder})` : title;
1144
+ }
1145
+ async function titleForNode(nestId, nodeId) {
1146
+ const titles = await buildTitleMap(nestId);
1147
+ return titles.get(nodeId) || titleFallback(titles, nodeId);
1148
+ }
1149
+ function titleFallback(titles, nodeId) {
1150
+ const at = nodeId.indexOf("nodes/");
1151
+ if (at > 0) {
1152
+ const normalized = nodeId.slice(at);
1153
+ const hit = titles.get(normalized);
1154
+ if (hit) return hit;
1155
+ }
1156
+ return nodeId.split("/").pop() || nodeId;
1157
+ }
1158
+ function prettyFolderPath(path) {
1159
+ return path.replace(/^nodes\//, "").split("/").filter(Boolean).map(
1160
+ (seg) => seg.split(/[-_]/).map((s) => s ? s[0].toUpperCase() + s.slice(1) : s).join(" ")
1161
+ ).join(" / ");
1162
+ }
1163
+ async function nestName(nestId) {
1164
+ try {
1165
+ const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1166
+ nestId
1167
+ ]);
1168
+ return row?.name || nestId;
1169
+ } catch {
1170
+ return nestId;
1171
+ }
1172
+ }
1173
+ function docLink(nestId, nodeId, baseUrl) {
1174
+ const base = baseUrl || config.PUBLIC_BASE_URL;
1175
+ if (!base) return "";
1176
+ const q = new URLSearchParams({ nest: nestId });
1177
+ if (nodeId) q.set("doc", nodeId);
1178
+ return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1179
+ }
1180
+ async function buildDocContext(nestId, nodeId, baseUrl) {
1181
+ const titles = await buildTitleMap(nestId);
1182
+ const docTitle = titles.get(nodeId) || nodeId;
1183
+ const folder = folderForNode(nodeId);
1184
+ const name = await nestName(nestId);
1185
+ return {
1186
+ docTitle,
1187
+ // Slack one-liner label ("Title (in Folder)"), same as describeNode().
1188
+ label: folder ? `${docTitle} (in ${folder})` : docTitle,
1189
+ path: folder ? `${name} / ${folder}` : name,
1190
+ link: docLink(nestId, nodeId, baseUrl)
1191
+ };
1192
+ }
1193
+
1011
1194
  // src/governance/teams-service.ts
1012
1195
  var VALID_TEAM_ROLES = ["admin", "editor", "viewer"];
1013
1196
  function parseMembers(json) {
@@ -1467,6 +1650,7 @@ function teamRoleToPermission(role) {
1467
1650
  var includesAny = (roles, wanted) => roles.some((r) => wanted.includes(r));
1468
1651
  var canViewWith = (roles) => roles.length > 0;
1469
1652
  var canEditWith = (roles) => includesAny(roles, ["owner", "admin", "editor"]);
1653
+ var canApproveWith = (roles) => includesAny(roles, ["owner", "admin", "reviewer"]);
1470
1654
  var PRECEDENCE = [
1471
1655
  "owner",
1472
1656
  "admin",
@@ -1562,7 +1746,41 @@ async function isPublicReader(nestId, userId) {
1562
1746
  return true;
1563
1747
  }
1564
1748
 
1749
+ // src/governance/tag-index-service.ts
1750
+ function normalizeTag(raw) {
1751
+ return raw.trim().replace(/^#+/, "").toLowerCase();
1752
+ }
1753
+ async function syncNodeTags(nestId, nodeId, tags) {
1754
+ const db = getDb();
1755
+ const normalized = Array.from(
1756
+ new Set(
1757
+ tags.filter((t) => typeof t === "string").map(normalizeTag).filter(Boolean)
1758
+ )
1759
+ );
1760
+ const insertSql = insertOrIgnore(
1761
+ db,
1762
+ "INSERT INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
1763
+ );
1764
+ await db.transaction(async (tx) => {
1765
+ await tx.run(
1766
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1767
+ [nestId, nodeId]
1768
+ );
1769
+ for (const tag of normalized) {
1770
+ await tx.run(insertSql, [nestId, nodeId, tag]);
1771
+ }
1772
+ });
1773
+ }
1774
+ async function removeNodeFromTagIndex(nestId, nodeId) {
1775
+ const db = getDb();
1776
+ await db.run(
1777
+ "DELETE FROM node_tag_index WHERE nest_id = ? AND node_id = ?",
1778
+ [nestId, nodeId]
1779
+ );
1780
+ }
1781
+
1565
1782
  // src/nests/service.ts
1783
+ import { rm } from "fs/promises";
1566
1784
  async function isImportedNest(nestId) {
1567
1785
  const row = await getDb().get(
1568
1786
  "SELECT is_imported FROM nests WHERE id = ?",
@@ -1619,6 +1837,43 @@ async function setAllowSelfApprove(nestId, allow) {
1619
1837
  nestId
1620
1838
  ]);
1621
1839
  }
1840
+ async function nestReviewsPrimeOnly(nestId) {
1841
+ const db = getDb();
1842
+ const row = await db.get(
1843
+ "SELECT prime_only_review FROM nests WHERE id = ?",
1844
+ [nestId]
1845
+ );
1846
+ return !!row?.prime_only_review;
1847
+ }
1848
+ async function setPrimeOnlyReview(nestId, enabled) {
1849
+ const db = getDb();
1850
+ await db.run("UPDATE nests SET prime_only_review = ? WHERE id = ?", [
1851
+ enabled ? 1 : 0,
1852
+ nestId
1853
+ ]);
1854
+ }
1855
+ async function nestPrimeTags(nestId) {
1856
+ const db = getDb();
1857
+ const row = await db.get("SELECT prime_tags FROM nests WHERE id = ?", [
1858
+ nestId
1859
+ ]);
1860
+ return parsePrimeTags(row?.prime_tags);
1861
+ }
1862
+ async function setPrimeTags(nestId, tags) {
1863
+ const db = getDb();
1864
+ await db.run("UPDATE nests SET prime_tags = ? WHERE id = ?", [
1865
+ parsePrimeTags(tags.join(",")).join(","),
1866
+ nestId
1867
+ ]);
1868
+ }
1869
+ function parsePrimeTags(raw) {
1870
+ if (!raw) return [];
1871
+ return [
1872
+ ...new Set(
1873
+ raw.split(",").map((t) => normalizeTag(t)).filter(Boolean)
1874
+ )
1875
+ ];
1876
+ }
1622
1877
  async function disableStewardshipAndWipeGovernance(nestId) {
1623
1878
  const db = getDb();
1624
1879
  return db.transaction(async (tx) => {
@@ -1700,32 +1955,21 @@ async function createNest(userId, name, description) {
1700
1955
  );
1701
1956
  const path = resolveNestPath(id);
1702
1957
  mkdirSync(path, { recursive: true });
1703
- const storage = new NestStorage(path);
1958
+ const storage = new NestStorage3(path);
1704
1959
  await storage.init(trimmed);
1705
1960
  trackEvent("nest.create", { nestId: id, userId });
1706
1961
  return await db.get("SELECT * FROM nests WHERE id = ?", [id]);
1707
1962
  }
1708
- async function importNest(userId, name, files) {
1963
+ async function importNest(userId, name) {
1709
1964
  const nm = (name || "").trim();
1710
1965
  if (!nm) {
1711
1966
  throw new ValidationError("name is required");
1712
1967
  }
1713
- if (!Array.isArray(files)) {
1714
- throw new ValidationError("files are required");
1715
- }
1716
1968
  const db = getDb();
1717
1969
  const id = uuid2();
1718
1970
  const slug = toSlug(nm);
1719
1971
  const visibility = userId === ANON_USER_ID ? "public" : "private";
1720
- const dest = resolveNestPath(id);
1721
- mkdirSync(dest, { recursive: true });
1722
- try {
1723
- await writeImportedFiles(dest, files);
1724
- } catch (err) {
1725
- console.error("[nests] import write failed", dest, err);
1726
- discardImportDir(dest);
1727
- throw new ValidationError("Failed to import folder");
1728
- }
1972
+ mkdirSync(resolveNestPath(id), { recursive: true });
1729
1973
  await db.run(
1730
1974
  "INSERT INTO nests (id, user_id, name, slug, description, visibility, is_imported) VALUES (?, ?, ?, ?, ?, ?, 1)",
1731
1975
  [id, userId, nm, slug, null, visibility]
@@ -1889,703 +2133,17 @@ async function deleteNest(nestId) {
1889
2133
  });
1890
2134
  const path = resolveNestPath(nestId);
1891
2135
  try {
1892
- rmSync2(path, { recursive: true, force: true });
2136
+ await rm(path, { recursive: true, force: true });
1893
2137
  } catch (err) {
1894
2138
  console.warn(`[nests] failed to remove nest directory ${path}:`, err);
1895
2139
  }
1896
2140
  trackEvent("nest.delete", { nestId });
1897
2141
  }
1898
2142
 
1899
- // src/nodes/flat-storage.ts
1900
- import { readdirSync } from "fs";
1901
- import { join as join4, relative, sep } from "path";
1902
- import { NestStorage as NestStorage2 } from "@promptowl/contextnest-engine";
1903
- var META_FILES = /* @__PURE__ */ new Set([
1904
- "INDEX.md",
1905
- "CONTEXT.md",
1906
- "CLAUDE.md",
1907
- "GEMINI.md",
1908
- "AGENTS.md",
1909
- "QWEN.md"
1910
- ]);
1911
- var FlatNestStorage = class extends NestStorage2 {
1912
- async discoverDocuments() {
1913
- const root = this.root;
1914
- let entries;
1915
- try {
1916
- entries = readdirSync(root, { recursive: true, withFileTypes: true });
1917
- } catch (err) {
1918
- console.error("[FlatNestStorage] cannot read folder", root, err);
1919
- return [];
1920
- }
1921
- const ids = [];
1922
- for (const e of entries) {
1923
- if (!e.isFile() || !e.name.endsWith(".md")) continue;
1924
- if (META_FILES.has(e.name)) continue;
1925
- const dir = e.parentPath ?? e.path ?? root;
1926
- const rel = relative(root, join4(dir, e.name)).split(sep).join("/");
1927
- if (rel.split("/").some(isSkippedSegment)) continue;
1928
- ids.push(rel.replace(/\.md$/, ""));
1929
- }
1930
- const nodes = [];
1931
- for (const id of ids.sort()) {
1932
- try {
1933
- nodes.push(await this.readDocument(id));
1934
- } catch (err) {
1935
- console.error("[FlatNestStorage] skipped unreadable doc", id, err);
1936
- }
1937
- }
1938
- return nodes;
1939
- }
1940
- };
1941
-
1942
- // src/nodes/engine.ts
1943
- var NestEngineCache = class {
1944
- cache = /* @__PURE__ */ new Map();
1945
- async get(nestId) {
1946
- let engine = this.cache.get(nestId);
1947
- if (!engine) {
1948
- const nestPath = resolveNestPath(nestId);
1949
- const storage = await isImportedNest(nestId) ? new FlatNestStorage(nestPath) : new NestStorage3(nestPath);
1950
- const query = new GraphQueryEngine(storage);
1951
- const versions = new VersionManager(storage);
1952
- engine = { storage, query, versions };
1953
- this.cache.set(nestId, engine);
1954
- }
1955
- return engine;
1956
- }
1957
- evict(nestId) {
1958
- this.cache.delete(nestId);
1959
- }
1960
- };
1961
- var engineCache = new NestEngineCache();
1962
-
1963
- // src/governance/title-resolver.ts
1964
- async function buildTitleMap(nestId) {
1965
- try {
1966
- const { storage } = await engineCache.get(nestId);
1967
- const docs = await storage.discoverDocuments();
1968
- return new Map(docs.map((d) => [d.id, d.frontmatter.title]));
1969
- } catch {
1970
- return /* @__PURE__ */ new Map();
1971
- }
1972
- }
1973
- function folderForNode(nodeId) {
1974
- return nodeId.replace(/^nodes\//, "").split("/").slice(0, -1).join("/");
1975
- }
1976
- async function describeNode(nestId, nodeId) {
1977
- const titles = await buildTitleMap(nestId);
1978
- const title = titles.get(nodeId) || nodeId;
1979
- const folder = folderForNode(nodeId);
1980
- return folder ? `${title} (in ${folder})` : title;
1981
- }
1982
- async function nestName(nestId) {
1983
- try {
1984
- const row = await getDb().get("SELECT name FROM nests WHERE id = ?", [
1985
- nestId
1986
- ]);
1987
- return row?.name || nestId;
1988
- } catch {
1989
- return nestId;
1990
- }
1991
- }
1992
- function docLink(nestId, nodeId, baseUrl) {
1993
- const base = baseUrl || config.PUBLIC_BASE_URL;
1994
- if (!base) return "";
1995
- const q = new URLSearchParams({ nest: nestId });
1996
- if (nodeId) q.set("doc", nodeId);
1997
- return `${base.replace(/\/$/, "")}/?${q.toString()}`;
1998
- }
1999
- async function buildDocContext(nestId, nodeId, baseUrl) {
2000
- const titles = await buildTitleMap(nestId);
2001
- const docTitle = titles.get(nodeId) || nodeId;
2002
- const folder = folderForNode(nodeId);
2003
- const name = await nestName(nestId);
2004
- return {
2005
- docTitle,
2006
- // Slack one-liner label ("Title (in Folder)"), same as describeNode().
2007
- label: folder ? `${docTitle} (in ${folder})` : docTitle,
2008
- path: folder ? `${name} / ${folder}` : name,
2009
- link: docLink(nestId, nodeId, baseUrl)
2010
- };
2011
- }
2012
-
2013
- // src/governance/stewardship-service.ts
2014
- async function assignSteward(data) {
2015
- const db = getDb();
2016
- const id = uuid3();
2017
- await db.run(
2018
- `INSERT INTO stewards
2019
- (id, nest_id, scope, node_pattern, tag_name, user_email, user_id, role, assigned_by, is_active)
2020
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
2021
- [
2022
- id,
2023
- data.nestId,
2024
- data.scope,
2025
- data.nodePattern || null,
2026
- data.tagName || null,
2027
- data.userEmail,
2028
- data.userId || null,
2029
- data.role,
2030
- data.assignedBy,
2031
- data.isActive ? 1 : 0
2032
- ]
2033
- );
2034
- return { ...data, id };
2035
- }
2036
- async function removeSteward(id) {
2037
- const db = getDb();
2038
- await db.run("DELETE FROM stewards WHERE id = ?", [id]);
2039
- }
2040
- var VALID_STEWARD_ROLES = ["editor", "reviewer", "viewer"];
2041
- async function updateSteward(id, update) {
2042
- const db = getDb();
2043
- const current = await db.get(
2044
- "SELECT * FROM stewards WHERE id = ? AND is_active = 1",
2045
- [id]
2046
- );
2047
- if (!current) {
2048
- throw new ValidationError("Steward not found");
2049
- }
2050
- const role = update.role ?? current.role;
2051
- if (!VALID_STEWARD_ROLES.includes(role)) {
2052
- throw new ValidationError(
2053
- `Invalid role "${role}" \u2014 must be editor, reviewer, or viewer.`
2054
- );
2055
- }
2056
- let scope = current.scope;
2057
- let nodePattern = current.node_pattern ?? null;
2058
- let tagName = current.tag_name ?? null;
2059
- if (update.scope) {
2060
- scope = update.scope;
2061
- nodePattern = null;
2062
- tagName = null;
2063
- switch (scope) {
2064
- case "document":
2065
- if (!update.documentId)
2066
- throw new ValidationError("documentId required for document scope");
2067
- nodePattern = update.documentId;
2068
- break;
2069
- case "tag":
2070
- if (!update.tagName)
2071
- throw new ValidationError("tagName required for tag scope");
2072
- tagName = update.tagName.trim().replace(/^#+/, "").toLowerCase();
2073
- break;
2074
- case "nest":
2075
- break;
2076
- }
2077
- const dup = await db.get(
2078
- `SELECT role FROM stewards
2079
- WHERE nest_id = ? AND is_active = 1 AND id != ? AND scope = ?
2080
- AND user_email = ?
2081
- AND COALESCE(node_pattern, '') = COALESCE(?, '')
2082
- AND COALESCE(tag_name, '') = COALESCE(?, '')`,
2083
- [current.nest_id, id, scope, current.user_email, nodePattern, tagName]
2084
- );
2085
- if (dup) {
2086
- const scopeLabel = scope === "document" ? `document "${nodePattern}"` : scope === "tag" ? `tag "#${tagName}"` : "this nest";
2087
- throw new ConflictError(
2088
- `"${current.user_email}" is already a steward of ${scopeLabel} with the "${dup.role}" role.`
2089
- );
2090
- }
2091
- }
2092
- await db.run(
2093
- "UPDATE stewards SET role = ?, scope = ?, node_pattern = ?, tag_name = ? WHERE id = ?",
2094
- [role, scope, nodePattern, tagName, id]
2095
- );
2096
- const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
2097
- return rowToSteward(row);
2098
- }
2099
- async function updateStewardRole(id, role) {
2100
- return updateSteward(id, { role });
2101
- }
2102
- async function getSteward(id) {
2103
- const db = getDb();
2104
- const row = await db.get("SELECT * FROM stewards WHERE id = ?", [id]);
2105
- return row ? rowToSteward(row) : null;
2106
- }
2107
- async function getStewardsForNest(nestId) {
2108
- const db = getDb();
2109
- const rows = await db.all(
2110
- "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1",
2111
- [nestId]
2112
- );
2113
- return enrichWithTitles(nestId, rows.map(rowToSteward));
2114
- }
2115
- async function getStewardsForScope(params) {
2116
- const db = getDb();
2117
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
2118
- const args = [params.nestId];
2119
- if (params.scope) {
2120
- sql += " AND scope = ?";
2121
- args.push(params.scope);
2122
- }
2123
- if (params.scopeTarget) {
2124
- sql += " AND (node_pattern = ? OR tag_name = ?)";
2125
- args.push(params.scopeTarget, params.scopeTarget);
2126
- }
2127
- return (await db.all(sql, args)).map(rowToSteward);
2128
- }
2129
- async function listStewards(params) {
2130
- const db = getDb();
2131
- let sql = "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1";
2132
- const args = [params.nestId];
2133
- if (params.scope) {
2134
- sql += " AND scope = ?";
2135
- args.push(params.scope);
2136
- }
2137
- if (params.search) {
2138
- sql += " AND (user_email LIKE ? OR tag_name LIKE ? OR node_pattern LIKE ?)";
2139
- const like = `%${params.search.toLowerCase()}%`;
2140
- args.push(like, like, like);
2141
- }
2142
- sql += " ORDER BY scope, COALESCE(node_pattern, tag_name, ''), user_email";
2143
- const stewards = (await db.all(sql, args)).map(rowToSteward);
2144
- return enrichWithTitles(params.nestId, stewards);
2145
- }
2146
- async function enrichWithTitles(nestId, stewards) {
2147
- if (!stewards.some((s) => s.scope === "document")) return stewards;
2148
- const titleByNodeId = await buildTitleMap(nestId);
2149
- for (const s of stewards) {
2150
- if (s.scope === "document" && s.nodePattern) {
2151
- s.nodeTitle = titleByNodeId.get(s.nodePattern);
2152
- }
2153
- }
2154
- return stewards;
2155
- }
2156
- async function createStewardRecord(params) {
2157
- if (params.users.length === 0) {
2158
- throw new Error("At least one user is required");
2159
- }
2160
- let nodePattern;
2161
- let tagName;
2162
- switch (params.scope) {
2163
- case "document":
2164
- if (!params.documentId) throw new Error("documentId required for document scope");
2165
- nodePattern = params.documentId;
2166
- break;
2167
- case "tag":
2168
- if (!params.tagName) throw new Error("tagName required for tag scope");
2169
- tagName = params.tagName.trim().replace(/^#+/, "").toLowerCase();
2170
- break;
2171
- case "nest":
2172
- break;
2173
- }
2174
- const db = getDb();
2175
- const results = [];
2176
- const actor = (params.assignedBy || "").trim().toLowerCase();
2177
- const ownerEmail = (await getNestOwnerEmail(params.nestId) || "").toLowerCase();
2178
- for (const user of params.users) {
2179
- const email = user.email.trim().toLowerCase();
2180
- if (!email) continue;
2181
- if (!isEmailish(email)) {
2182
- throw new ValidationError(`"${user.email}" is not a valid email address.`);
2183
- }
2184
- if (email === actor) {
2185
- throw new ValidationError(
2186
- "You already manage this nest, so you can't add yourself as a steward."
2187
- );
2188
- }
2189
- if (ownerEmail && email === ownerEmail) {
2190
- throw new ValidationError(
2191
- "The nest owner already has full access and doesn't need a steward role."
2192
- );
2193
- }
2194
- const existing = await db.get(
2195
- `SELECT * FROM stewards
2196
- WHERE nest_id = ? AND is_active = 1 AND scope = ? AND user_email = ?
2197
- AND COALESCE(node_pattern, '') = COALESCE(?, '')
2198
- AND COALESCE(tag_name, '') = COALESCE(?, '')`,
2199
- [
2200
- params.nestId,
2201
- params.scope,
2202
- email,
2203
- nodePattern ?? null,
2204
- tagName ?? null
2205
- ]
2206
- );
2207
- if (existing) {
2208
- const scopeLabel = params.scope === "document" ? `document "${nodePattern}"` : params.scope === "tag" ? `tag "#${tagName}"` : "this nest";
2209
- throw new ConflictError(
2210
- `"${email}" is already a steward of ${scopeLabel} with the "${existing.role}" role. Remove the existing assignment first to change the role.`
2211
- );
2212
- }
2213
- const userRow = await db.get(
2214
- "SELECT id FROM users WHERE email = ?",
2215
- [email]
2216
- );
2217
- let userId = userRow?.id;
2218
- if (!userId) {
2219
- const { hashPassword } = await import("./keys-73STFJJB.js");
2220
- userId = uuid3();
2221
- await db.run(
2222
- "INSERT INTO users (id, email, name, password_hash, is_invited) VALUES (?, ?, ?, ?, 1)",
2223
- [userId, email, null, await hashPassword(uuid3())]
2224
- );
2225
- }
2226
- const created = await assignSteward({
2227
- nestId: params.nestId,
2228
- scope: params.scope,
2229
- nodePattern,
2230
- tagName,
2231
- userEmail: email,
2232
- userId,
2233
- role: user.role ?? "reviewer",
2234
- assignedBy: params.assignedBy,
2235
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
2236
- isActive: true
2237
- });
2238
- results.push(created);
2239
- }
2240
- await db.run(
2241
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ? AND stewardship_enabled = 0",
2242
- [params.nestId]
2243
- );
2244
- if (results.length > 0) {
2245
- let scopeLabel;
2246
- if (params.scope === "document") {
2247
- const titles = await buildTitleMap(params.nestId);
2248
- const docTitle = titles.get(nodePattern) || nodePattern;
2249
- const capitalized = docTitle.charAt(0).toUpperCase() + docTitle.slice(1);
2250
- scopeLabel = `"${capitalized}" document`;
2251
- } else if (params.scope === "tag") {
2252
- scopeLabel = `#${tagName}`;
2253
- } else {
2254
- scopeLabel = "the whole nest";
2255
- }
2256
- const shareNestName = await nestName(params.nestId);
2257
- const link = docLink(params.nestId, void 0, params.baseUrl);
2258
- for (const s of results) {
2259
- void sendEmailToRecipient(s.userEmail, {
2260
- status: "steward",
2261
- docTitle: shareNestName,
2262
- path: shareNestName,
2263
- permission: s.role,
2264
- scopeLabel,
2265
- by: params.assignedBy || void 0,
2266
- link
2267
- });
2268
- }
2269
- }
2270
- return results;
2271
- }
2272
- async function resolveStewardsForNode(nestId, nodeId) {
2273
- return (await resolve(nestId, nodeId)).stewards;
2274
- }
2275
- async function resolveStewardsWithFallback(nestId, nodeId) {
2276
- return resolve(nestId, nodeId);
2277
- }
2278
- async function resolve(nestId, nodeId) {
2279
- const db = getDb();
2280
- const rows = await db.all(
2281
- `
2282
- SELECT s.*, 1 AS priority, ('document: ' || s.node_pattern) AS match_source
2283
- FROM stewards s
2284
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'document'
2285
- AND s.node_pattern = ?
2286
- UNION ALL
2287
- SELECT s.*, 2 AS priority, ('tag: ' || s.tag_name) AS match_source
2288
- FROM stewards s
2289
- JOIN node_tag_index nt
2290
- ON nt.nest_id = s.nest_id
2291
- AND nt.tag_name = s.tag_name
2292
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'tag'
2293
- AND nt.node_id = ?
2294
- UNION ALL
2295
- SELECT s.*, 3 AS priority, 'nest-level steward' AS match_source
2296
- FROM stewards s
2297
- WHERE s.nest_id = ? AND s.is_active = 1 AND s.scope = 'nest'
2298
- ORDER BY priority ASC, user_email ASC
2299
- `,
2300
- [
2301
- nestId,
2302
- nodeId,
2303
- // document branch
2304
- nestId,
2305
- nodeId,
2306
- // tag branch
2307
- nestId
2308
- // nest branch
2309
- ]
2310
- );
2311
- const resolved = rows.map((row) => ({
2312
- steward: rowToSteward(row),
2313
- priority: row.priority,
2314
- source: row.match_source
2315
- }));
2316
- if (resolved.length > 0) {
2317
- return { stewards: resolved, fallbackToOwner: false };
2318
- }
2319
- const owner = await db.get(
2320
- `SELECT u.email FROM nests n
2321
- JOIN users u ON u.id = n.user_id
2322
- WHERE n.id = ?`,
2323
- [nestId]
2324
- );
2325
- return {
2326
- stewards: [],
2327
- fallbackToOwner: true,
2328
- ownerEmail: owner?.email
2329
- };
2330
- }
2331
- async function userIdForEmail(email) {
2332
- const db = getDb();
2333
- const row = await db.get(
2334
- "SELECT id FROM users WHERE LOWER(email) = LOWER(?)",
2335
- [email]
2336
- );
2337
- return row?.id;
2338
- }
2339
- async function getStewardRolesForUser(nestId, userEmail) {
2340
- const db = getDb();
2341
- const rows = await db.all(
2342
- "SELECT DISTINCT role FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
2343
- [nestId, userEmail]
2344
- );
2345
- return rows.map((r) => r.role);
2346
- }
2347
- async function getStewardsForUser(nestId, userEmail) {
2348
- const db = getDb();
2349
- const rows = await db.all(
2350
- "SELECT * FROM stewards WHERE nest_id = ? AND is_active = 1 AND LOWER(user_email) = LOWER(?)",
2351
- [nestId, userEmail]
2352
- );
2353
- return rows.map(rowToSteward);
2354
- }
2355
- async function getCollaboratorRole(nestId, userEmail) {
2356
- const userId = await userIdForEmail(userEmail);
2357
- if (!userId) return null;
2358
- const db = getDb();
2359
- const row = await db.get(
2360
- "SELECT permission FROM nest_collaborators WHERE nest_id = ? AND user_id = ?",
2361
- [nestId, userId]
2362
- );
2363
- return row?.permission ?? null;
2364
- }
2365
- async function resolveGrantRoles(nestId, userEmail, userId) {
2366
- const [grantRaw, stewardRoles, teamRoles] = await Promise.all([
2367
- getCollaboratorRole(nestId, userEmail),
2368
- getStewardRolesForUser(nestId, userEmail),
2369
- resolveTeamRolesForUser(nestId, userId)
2370
- ]);
2371
- return [
2372
- ...new Set(
2373
- [collabPermToRole(grantRaw), ...stewardRoles, ...teamRoles].filter(
2374
- Boolean
2375
- )
2376
- )
2377
- ];
2378
- }
2379
- async function resolveUserRoles(nestId, userEmail, opts) {
2380
- const roles = /* @__PURE__ */ new Set();
2381
- if (isSuperAdmin(userEmail)) roles.add("admin");
2382
- const owner = await getNestOwnerEmail(nestId);
2383
- if (owner && owner.toLowerCase() === userEmail.toLowerCase()) {
2384
- roles.add("owner");
2385
- }
2386
- const userId = await userIdForEmail(userEmail);
2387
- if (userId) {
2388
- const collabRole = collabPermToRole(await resolveNestPermission(nestId, userId));
2389
- if (collabRole) roles.add(collabRole);
2390
- for (const role of await resolveTeamRolesForUser(nestId, userId)) {
2391
- roles.add(role);
2392
- }
2393
- }
2394
- const stewardRoles = opts?.nodeId ? (await resolveStewardsForNode(nestId, opts.nodeId)).filter(
2395
- (r) => r.steward.userEmail.toLowerCase() === userEmail.toLowerCase()
2396
- ).map((r) => r.steward.role) : await getStewardRolesForUser(nestId, userEmail);
2397
- for (const role of stewardRoles) roles.add(role);
2398
- return [...roles];
2399
- }
2400
- async function canManageStewards(nestId, userId) {
2401
- if (config.AUTH_MODE === "open") return true;
2402
- const perm = await resolveNestPermission(nestId, userId);
2403
- return perm === "owner" || perm === "admin";
2404
- }
2405
- async function canCreateInNest(nestId, userEmail) {
2406
- if (config.AUTH_MODE === "open" || isSuperAdmin(userEmail)) return true;
2407
- const userId = await userIdForEmail(userEmail);
2408
- if (userId) {
2409
- const perm = await resolveNestPermission(nestId, userId);
2410
- if (perm === "owner" || perm === "admin" || perm === "write") return true;
2411
- }
2412
- return (await getStewardsForUser(nestId, userEmail)).some(
2413
- (s) => s.role === "editor" && s.scope === "nest"
2414
- );
2415
- }
2416
- async function getNestOwnerEmail(nestId) {
2417
- const db = getDb();
2418
- const row = await db.get(
2419
- `SELECT u.email FROM nests n
2420
- JOIN users u ON u.id = n.user_id
2421
- WHERE n.id = ?`,
2422
- [nestId]
2423
- );
2424
- return row?.email ?? null;
2425
- }
2426
- async function canUserEdit(nestId, nodeId, userEmail) {
2427
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2428
- if (roles.includes("owner")) {
2429
- return { allowed: true, reason: "nest owner", role: "owner" };
2430
- }
2431
- if (isSuperAdmin(userEmail)) {
2432
- return { allowed: true, reason: "super admin", role: "super_admin" };
2433
- }
2434
- if (canEditWith(roles)) {
2435
- return {
2436
- allowed: true,
2437
- reason: "editor access (collaborator or steward)",
2438
- role: roles.includes("admin") ? "admin" : "editor"
2439
- };
2440
- }
2441
- return { allowed: false, reason: "no editor role on this node", role: null };
2442
- }
2443
- async function getCurrentVersionAuthor(nestId, nodeId) {
2444
- const db = getDb();
2445
- const row = await db.get(
2446
- `SELECT author FROM node_versions
2447
- WHERE nest_id = ? AND node_id = ?
2448
- ORDER BY version DESC LIMIT 1`,
2449
- [nestId, nodeId]
2450
- );
2451
- return row?.author ?? null;
2452
- }
2453
- async function getPendingReviewRequester(nestId, nodeId) {
2454
- const db = getDb();
2455
- const row = await db.get(
2456
- `SELECT requested_by FROM review_requests
2457
- WHERE nest_id = ? AND node_id = ? AND status = 'pending'
2458
- ORDER BY requested_at DESC LIMIT 1`,
2459
- [nestId, nodeId]
2460
- );
2461
- return row?.requested_by ?? null;
2462
- }
2463
- async function canUserApprove(nestId, nodeId, userEmail) {
2464
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2465
- const isOwner = roles.includes("owner");
2466
- const isSuper = isSuperAdmin(userEmail);
2467
- const allowSelf = await nestAllowsSelfApprove(nestId);
2468
- const hasStewardApprove = roles.includes("admin") || roles.includes("reviewer");
2469
- const actor = await getPendingReviewRequester(nestId, nodeId) ?? await getCurrentVersionAuthor(nestId, nodeId);
2470
- const isOwnSubmission = !!actor && actor.toLowerCase() === userEmail.toLowerCase();
2471
- if (hasStewardApprove) {
2472
- if (isOwnSubmission && !((isOwner || isSuper) && allowSelf)) {
2473
- return {
2474
- allowed: false,
2475
- reason: "You submitted this version for review, so you can't approve it yourself. Ask another reviewer to approve it (separation of duties).",
2476
- role: roles.includes("admin") ? "admin" : "reviewer"
2477
- };
2478
- }
2479
- return {
2480
- allowed: true,
2481
- reason: "reviewer access (collaborator or steward)",
2482
- role: roles.includes("admin") ? "admin" : "reviewer"
2483
- };
2484
- }
2485
- if (isOwner || isSuper) {
2486
- if (allowSelf) {
2487
- return {
2488
- allowed: true,
2489
- reason: isOwner ? "nest owner (self-approve enabled)" : "super admin",
2490
- role: isOwner ? "owner" : "super_admin"
2491
- };
2492
- }
2493
- return {
2494
- allowed: false,
2495
- reason: "Approvals go to assigned reviewers while self-approve is off. Enable self-approve in nest settings to approve directly (e.g. while seeding), or add a reviewer steward.",
2496
- role: isOwner ? "owner" : "super_admin"
2497
- };
2498
- }
2499
- const held = primaryRole(roles);
2500
- return {
2501
- allowed: false,
2502
- reason: held ? `You're a "${held}" on this document \u2014 only reviewers (or admins) can approve. Ask the nest owner to grant you the reviewer steward role.` : "You're not a steward on this document, so you can't approve it. Ask the nest owner to add you as a reviewer.",
2503
- role: held
2504
- };
2505
- }
2506
- async function canUserAccess(nestId, nodeId, userEmail) {
2507
- const roles = await resolveUserRoles(nestId, userEmail, { nodeId });
2508
- if (roles.includes("owner")) {
2509
- return { allowed: true, reason: "nest owner", role: "owner" };
2510
- }
2511
- if (isSuperAdmin(userEmail)) {
2512
- return { allowed: true, reason: "super admin", role: "super_admin" };
2513
- }
2514
- if (canViewWith(roles)) {
2515
- return {
2516
- allowed: true,
2517
- reason: "collaborator or steward access",
2518
- role: primaryRole(roles)
2519
- };
2520
- }
2521
- return { allowed: false, reason: "no collaborator or steward access", role: null };
2522
- }
2523
- async function syncFromConfig(nestId, config2) {
2524
- const db = getDb();
2525
- let count = 0;
2526
- await db.run(
2527
- "UPDATE stewards SET is_active = 0 WHERE nest_id = ?",
2528
- [nestId]
2529
- );
2530
- await db.run(
2531
- "UPDATE nests SET stewardship_enabled = 1 WHERE id = ?",
2532
- [nestId]
2533
- );
2534
- const addEntries = async (scope, entries, target) => {
2535
- for (const entry of entries) {
2536
- const user = await db.get(
2537
- "SELECT id FROM users WHERE email = ?",
2538
- [entry.email]
2539
- );
2540
- const rawRole = entry.role || "reviewer";
2541
- const role = rawRole === "admin" ? "reviewer" : rawRole;
2542
- await assignSteward({
2543
- nestId,
2544
- scope,
2545
- nodePattern: target?.nodePattern,
2546
- tagName: target?.tagName ? target.tagName.trim().replace(/^#+/, "").toLowerCase() : void 0,
2547
- userEmail: entry.email.toLowerCase(),
2548
- userId: user?.id,
2549
- role,
2550
- assignedBy: "config",
2551
- assignedAt: (/* @__PURE__ */ new Date()).toISOString(),
2552
- isActive: true
2553
- });
2554
- count++;
2555
- }
2556
- };
2557
- if (config2.nest) {
2558
- await addEntries("nest", config2.nest);
2559
- }
2560
- if (config2.tags) {
2561
- for (const [tagName, entries] of Object.entries(config2.tags)) {
2562
- await addEntries("tag", entries, { tagName });
2563
- }
2564
- }
2565
- if (config2.documents) {
2566
- for (const [docPattern, entries] of Object.entries(config2.documents)) {
2567
- await addEntries("document", entries, { nodePattern: docPattern });
2568
- }
2569
- }
2570
- return count;
2571
- }
2572
- function rowToSteward(row) {
2573
- return {
2574
- id: row.id,
2575
- nestId: row.nest_id,
2576
- scope: row.scope,
2577
- nodePattern: row.node_pattern || void 0,
2578
- tagName: row.tag_name || void 0,
2579
- userEmail: row.user_email,
2580
- userId: row.user_id || void 0,
2581
- role: row.role,
2582
- assignedBy: row.assigned_by,
2583
- assignedAt: row.assigned_at,
2584
- isActive: !!row.is_active
2585
- };
2586
- }
2587
-
2588
2143
  export {
2144
+ nowExpr,
2145
+ insertOrIgnore,
2146
+ insertOrReplace,
2589
2147
  trackEvent,
2590
2148
  startTelemetryLoop,
2591
2149
  persistSetting,
@@ -2608,11 +2166,18 @@ export {
2608
2166
  sendEmailToRecipient,
2609
2167
  nestStorageRoot,
2610
2168
  resolveNestPath,
2169
+ normalizeTag,
2170
+ syncNodeTags,
2171
+ removeNodeFromTagIndex,
2611
2172
  uniqueNestName,
2612
2173
  isStewardshipEnabled,
2613
2174
  setStewardshipEnabled,
2614
2175
  nestAllowsSelfApprove,
2615
2176
  setAllowSelfApprove,
2177
+ nestReviewsPrimeOnly,
2178
+ setPrimeOnlyReview,
2179
+ nestPrimeTags,
2180
+ setPrimeTags,
2616
2181
  disableStewardshipAndWipeGovernance,
2617
2182
  renameNest,
2618
2183
  createNest,
@@ -2624,8 +2189,13 @@ export {
2624
2189
  getNest,
2625
2190
  deleteNest,
2626
2191
  engineCache,
2192
+ engineApi,
2193
+ opContext,
2627
2194
  buildTitleMap,
2195
+ folderForNode,
2628
2196
  describeNode,
2197
+ titleForNode,
2198
+ prettyFolderPath,
2629
2199
  nestName,
2630
2200
  docLink,
2631
2201
  buildDocContext,
@@ -2643,30 +2213,14 @@ export {
2643
2213
  listNestTeams,
2644
2214
  shareTeamWithNest,
2645
2215
  unshareTeamFromNest,
2216
+ resolveTeamRolesForUser,
2217
+ collabPermToRole,
2218
+ canViewWith,
2219
+ canEditWith,
2220
+ canApproveWith,
2221
+ primaryRole,
2646
2222
  isServerAdminUserId,
2647
2223
  resolveNestPermission,
2648
2224
  permissionLevel,
2649
- isPublicReader,
2650
- assignSteward,
2651
- removeSteward,
2652
- updateSteward,
2653
- updateStewardRole,
2654
- getSteward,
2655
- getStewardsForNest,
2656
- getStewardsForScope,
2657
- listStewards,
2658
- createStewardRecord,
2659
- resolveStewardsForNode,
2660
- resolveStewardsWithFallback,
2661
- getStewardRolesForUser,
2662
- getStewardsForUser,
2663
- getCollaboratorRole,
2664
- resolveGrantRoles,
2665
- resolveUserRoles,
2666
- canManageStewards,
2667
- canCreateInNest,
2668
- canUserEdit,
2669
- canUserApprove,
2670
- canUserAccess,
2671
- syncFromConfig
2225
+ isPublicReader
2672
2226
  };