@lucern/graph-primitives 0.1.0-alpha.4 → 0.3.0-alpha.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.
Files changed (78) hide show
  1. package/dist/beliefDecay.js +229 -1115
  2. package/dist/beliefDecay.js.map +1 -1
  3. package/dist/beliefEvidenceLinks.js +53 -834
  4. package/dist/beliefEvidenceLinks.js.map +1 -1
  5. package/dist/confidencePropagationDispatch.d.ts +3 -3
  6. package/dist/confidencePropagationDispatch.js +30 -308
  7. package/dist/confidencePropagationDispatch.js.map +1 -1
  8. package/dist/contradictions.js +5 -797
  9. package/dist/contradictions.js.map +1 -1
  10. package/dist/edges/contradicts.js +1 -122
  11. package/dist/edges/contradicts.js.map +1 -1
  12. package/dist/edges/dependsOn.js +14 -172
  13. package/dist/edges/dependsOn.js.map +1 -1
  14. package/dist/edges/elaborates.js +1 -49
  15. package/dist/edges/elaborates.js.map +1 -1
  16. package/dist/edges/index.js +14 -277
  17. package/dist/edges/index.js.map +1 -1
  18. package/dist/edges/informs.js +1 -62
  19. package/dist/edges/informs.js.map +1 -1
  20. package/dist/edges/propagationTypes.d.ts +2 -2
  21. package/dist/edges/propagationTypes.js.map +1 -1
  22. package/dist/edges/refutes.js +1 -62
  23. package/dist/edges/refutes.js.map +1 -1
  24. package/dist/edges/supports.js +1 -122
  25. package/dist/edges/supports.js.map +1 -1
  26. package/dist/edges/utils.d.ts +6 -6
  27. package/dist/edges/utils.js +1 -130
  28. package/dist/edges/utils.js.map +1 -1
  29. package/dist/entityBridge.js +2 -17
  30. package/dist/entityBridge.js.map +1 -1
  31. package/dist/entityLifecycle.js +62 -848
  32. package/dist/entityLifecycle.js.map +1 -1
  33. package/dist/epistemicAnswers.js +27 -838
  34. package/dist/epistemicAnswers.js.map +1 -1
  35. package/dist/epistemicBeliefs.js +186 -2214
  36. package/dist/epistemicBeliefs.js.map +1 -1
  37. package/dist/epistemicContractHelpers.js +1 -318
  38. package/dist/epistemicContractHelpers.js.map +1 -1
  39. package/dist/epistemicContracts.js +163 -2467
  40. package/dist/epistemicContracts.js.map +1 -1
  41. package/dist/epistemicEdges.js +60 -863
  42. package/dist/epistemicEdges.js.map +1 -1
  43. package/dist/epistemicEvidence.js +116 -1647
  44. package/dist/epistemicEvidence.js.map +1 -1
  45. package/dist/epistemicHelpers.js +3 -2
  46. package/dist/epistemicHelpers.js.map +1 -1
  47. package/dist/epistemicLinking.js +2 -785
  48. package/dist/epistemicLinking.js.map +1 -1
  49. package/dist/epistemicNodes.js +34 -1427
  50. package/dist/epistemicNodes.js.map +1 -1
  51. package/dist/epistemicQuestions.js +88 -1637
  52. package/dist/epistemicQuestions.js.map +1 -1
  53. package/dist/epistemicSources.js +28 -1421
  54. package/dist/epistemicSources.js.map +1 -1
  55. package/dist/evaluators/index.js +163 -2467
  56. package/dist/evaluators/index.js.map +1 -1
  57. package/dist/index.js +486 -3649
  58. package/dist/index.js.map +1 -1
  59. package/dist/ontology-matching.js +1 -344
  60. package/dist/ontology-matching.js.map +1 -1
  61. package/dist/ontologyApproval.js +1 -13
  62. package/dist/ontologyApproval.js.map +1 -1
  63. package/dist/ontologyDefinitions.js +2 -17
  64. package/dist/ontologyDefinitions.js.map +1 -1
  65. package/dist/ontologyRegistry.js +2 -17
  66. package/dist/ontologyRegistry.js.map +1 -1
  67. package/dist/projectionReconciliation.js +2 -17
  68. package/dist/projectionReconciliation.js.map +1 -1
  69. package/dist/questionEvidenceLinks.js +242 -837
  70. package/dist/questionEvidenceLinks.js.map +1 -1
  71. package/dist/text-matching.js +1 -244
  72. package/dist/text-matching.js.map +1 -1
  73. package/dist/workflowBridge.d.ts +27 -0
  74. package/dist/workflowBridge.js +303 -0
  75. package/dist/workflowBridge.js.map +1 -0
  76. package/dist/workspaceIsolation.js +8 -609
  77. package/dist/workspaceIsolation.js.map +1 -1
  78. package/package.json +6 -6
@@ -1,792 +1,11 @@
1
1
  import { v } from 'convex/values';
2
+ import { checkProjectAccess } from '@lucern/access-control/access';
3
+ import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
2
4
  import { componentsGeneric, mutationGeneric, queryGeneric, anyApi } from 'convex/server';
3
5
 
4
6
  // src/questionEvidenceLinks.ts
5
7
  var api = anyApi;
6
8
  componentsGeneric();
7
-
8
- // ../access-control/src/topicProjectOverlay.ts
9
- var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
10
- function readNonEmptyString(value) {
11
- if (typeof value !== "string") {
12
- return;
13
- }
14
- const normalized = value.trim();
15
- return normalized.length > 0 ? normalized : void 0;
16
- }
17
- function readStringArray(value) {
18
- if (!Array.isArray(value)) {
19
- return [];
20
- }
21
- return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
22
- }
23
- function readMetadata(topic) {
24
- return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
25
- }
26
- function readLegacyProjectId(value) {
27
- if (!value) {
28
- return;
29
- }
30
- return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
31
- }
32
- function coerceVisibility(value) {
33
- return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
34
- }
35
- function coerceStatus(value) {
36
- return value === "active" || value === "archived" || value === "watching" ? value : void 0;
37
- }
38
- function mapProjectType(topic, metadata) {
39
- const explicit = readNonEmptyString(metadata.projectType);
40
- if (explicit) {
41
- return explicit;
42
- }
43
- if (topic.type === "theme") {
44
- return "thematic";
45
- }
46
- return readNonEmptyString(topic.type) || "general";
47
- }
48
- function isProjectLikeTopic(topic) {
49
- const metadata = readMetadata(topic);
50
- return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
51
- }
52
- async function resolveTopicDoc(ctx, scopeId) {
53
- if (ctx?.db && typeof ctx.db.get === "function") {
54
- try {
55
- const directTopic = await ctx.db.get(scopeId);
56
- if (directTopic) {
57
- return directTopic;
58
- }
59
- } catch {
60
- }
61
- }
62
- if (typeof ctx.runQuery !== "function") {
63
- return null;
64
- }
65
- try {
66
- const topic = await ctx.runQuery(api.topics.get, {
67
- id: String(scopeId)
68
- });
69
- if (topic?.name !== void 0 && topic?.type !== void 0) {
70
- return topic;
71
- }
72
- } catch {
73
- }
74
- try {
75
- const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
76
- projectId: String(scopeId)
77
- });
78
- if (topic?.name !== void 0 && topic?.type !== void 0) {
79
- return topic;
80
- }
81
- } catch {
82
- }
83
- return null;
84
- }
85
- function materializeTopicProjectOverlay(topic, idMode = "legacy") {
86
- const metadata = readMetadata(topic);
87
- const topicId = String(topic._id);
88
- const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
89
- const storageProjectId = legacyProjectId || topicId;
90
- const outwardId = idMode === "topic" ? topicId : storageProjectId;
91
- const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
92
- const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
93
- const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
94
- const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
95
- return {
96
- ...metadata,
97
- _id: outwardId,
98
- projectId: outwardId,
99
- topicId,
100
- storageProjectId,
101
- legacyProjectId,
102
- name: readNonEmptyString(topic.name) || "Untitled Theme",
103
- type: mapProjectType(topic, metadata),
104
- description: readNonEmptyString(topic.description),
105
- ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
106
- sharedWith: readStringArray(metadata.sharedWith),
107
- visibility,
108
- tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
109
- workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
110
- status,
111
- tags: readStringArray(metadata.tags),
112
- chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
113
- artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
114
- lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
115
- _creationTime: typeof topic._creationTime === "number" ? topic._creationTime : createdAt,
116
- createdAt,
117
- updatedAt
118
- };
119
- }
120
- async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
121
- const topic = await resolveTopicDoc(ctx, scopeId);
122
- if (!topic) {
123
- return null;
124
- }
125
- if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
126
- return null;
127
- }
128
- return materializeTopicProjectOverlay(topic, options.idMode);
129
- }
130
- async function listTopicProjectOverlays(ctx, options = {}) {
131
- let allTopics = [];
132
- if (ctx?.db?.query && typeof ctx.db.query === "function") {
133
- try {
134
- allTopics = await ctx.db.query("topics").collect();
135
- } catch {
136
- allTopics = [];
137
- }
138
- }
139
- if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
140
- allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
141
- }
142
- return allTopics.filter(
143
- (topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
144
- ).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
145
- }
146
-
147
- // ../access-control/src/projectGrantsBridge.ts
148
- var PROJECT_GRANT_STATUSES = ["active", "revoked", "expired"];
149
- function normalizeString(value) {
150
- if (typeof value !== "string") {
151
- return;
152
- }
153
- const trimmed = value.trim();
154
- return trimmed.length > 0 ? trimmed : void 0;
155
- }
156
- async function resolveGrantScopeIds(ctx, args) {
157
- const topicId = normalizeString(args.topicId);
158
- const projectId = normalizeString(args.projectId);
159
- for (const scopeId of [topicId, projectId]) {
160
- if (!scopeId) {
161
- continue;
162
- }
163
- try {
164
- const overlay = await resolveTopicProjectOverlay(ctx, scopeId, {
165
- idMode: "legacy",
166
- projectLikeOnly: false
167
- });
168
- if (overlay) {
169
- return {
170
- topicId: normalizeString(overlay.topicId) ?? topicId,
171
- projectId: normalizeString(overlay.projectId) ?? projectId ?? scopeId
172
- };
173
- }
174
- } catch {
175
- }
176
- }
177
- return { topicId, projectId };
178
- }
179
- async function normalizeProjectGrantRow(ctx, row) {
180
- const scope = await resolveGrantScopeIds(ctx, {
181
- topicId: row.topicId,
182
- projectId: row.projectId
183
- });
184
- return {
185
- ...row,
186
- ...scope.topicId ? { topicId: scope.topicId } : {},
187
- ...scope.projectId ?? scope.topicId ? { projectId: scope.projectId ?? scope.topicId } : {}
188
- };
189
- }
190
- async function normalizeProjectGrantRows(ctx, rows) {
191
- return await Promise.all(rows.map((row) => normalizeProjectGrantRow(ctx, row)));
192
- }
193
- async function listProjectGrantsByPrincipal(ctx, principalId) {
194
- const rows = await Promise.all(
195
- PROJECT_GRANT_STATUSES.map(
196
- (status) => ctx.db.query("projectGrants").withIndex(
197
- "by_principal_status",
198
- (q) => q.eq("principalId", principalId).eq("status", status)
199
- ).collect()
200
- )
201
- );
202
- return await normalizeProjectGrantRows(ctx, rows.flat());
203
- }
204
- async function listProjectGrantsByGroup(ctx, groupId) {
205
- const rows = await Promise.all(
206
- PROJECT_GRANT_STATUSES.map(
207
- (status) => ctx.db.query("projectGrants").withIndex(
208
- "by_group_status",
209
- (q) => q.eq("groupId", groupId).eq("status", status)
210
- ).collect()
211
- )
212
- );
213
- return await normalizeProjectGrantRows(ctx, rows.flat());
214
- }
215
- function buildScopeMatchers(inputScopeId, resolved) {
216
- return new Set(
217
- [inputScopeId, resolved.topicId, resolved.projectId].map((value) => normalizeString(value)).filter((value) => Boolean(value))
218
- );
219
- }
220
- function matchesResolvedScope(row, scopeIds) {
221
- const rowTopicId = normalizeString(row.topicId);
222
- const rowProjectId = normalizeString(row.projectId);
223
- return rowTopicId !== void 0 && scopeIds.has(rowTopicId) || rowProjectId !== void 0 && scopeIds.has(rowProjectId);
224
- }
225
- async function bridgeListProjectGrantsByTopicAndPrincipal(ctx, topicId, principalId) {
226
- const resolved = await resolveGrantScopeIds(ctx, { topicId });
227
- const scopeIds = buildScopeMatchers(topicId, resolved);
228
- const rows = await listProjectGrantsByPrincipal(ctx, principalId);
229
- return rows.filter((row) => matchesResolvedScope(row, scopeIds));
230
- }
231
- async function bridgeListProjectGrantsByTopicAndGroup(ctx, topicId, groupId) {
232
- const resolved = await resolveGrantScopeIds(ctx, { topicId });
233
- const scopeIds = buildScopeMatchers(topicId, resolved);
234
- const rows = await listProjectGrantsByGroup(ctx, groupId);
235
- return rows.filter((row) => matchesResolvedScope(row, scopeIds));
236
- }
237
- async function bridgeListProjectGrantsByPrincipalStatus(ctx, principalId, status) {
238
- const rows = await listProjectGrantsByPrincipal(ctx, principalId);
239
- return rows.filter((row) => row.status === status);
240
- }
241
- async function bridgeListProjectGrantsByGroupStatus(ctx, groupId, status) {
242
- const rows = await listProjectGrantsByGroup(ctx, groupId);
243
- return rows.filter((row) => row.status === status);
244
- }
245
- async function bridgeInsertProjectGrant(ctx, value) {
246
- const resolved = await resolveGrantScopeIds(ctx, value);
247
- return await ctx.db.insert("projectGrants", {
248
- ...value,
249
- ...resolved.topicId ? { topicId: resolved.topicId } : {},
250
- ...resolved.projectId ?? resolved.topicId ? { projectId: resolved.projectId ?? resolved.topicId } : {}
251
- });
252
- }
253
-
254
- // ../access-control/src/resolvers.ts
255
- async function findUserByClerkId(ctx, clerkId) {
256
- const normalizedClerkId = clerkId.trim();
257
- if (!normalizedClerkId) {
258
- return null;
259
- }
260
- if (typeof ctx.runQuery === "function") {
261
- try {
262
- const bridgedUser = await ctx.runQuery(api.users.getUserByClerkId, {
263
- clerkId: normalizedClerkId
264
- });
265
- if (bridgedUser) {
266
- return bridgedUser;
267
- }
268
- } catch {
269
- }
270
- }
271
- try {
272
- const users = await ctx.db.query("users").collect();
273
- return users.find((user) => String(user.clerkId ?? "") === normalizedClerkId) ?? null;
274
- } catch {
275
- return null;
276
- }
277
- }
278
- async function findUserByPrincipalId(ctx, principalId) {
279
- const normalizedPrincipalId = principalId.trim();
280
- if (!normalizedPrincipalId) {
281
- return null;
282
- }
283
- try {
284
- const users = await ctx.db.query("users").collect();
285
- return users.find(
286
- (user) => String(user.defaultPrincipalId ?? "") === normalizedPrincipalId
287
- ) ?? null;
288
- } catch {
289
- return null;
290
- }
291
- }
292
- async function findAgentByPrincipalId(ctx, principalId) {
293
- const normalizedPrincipalId = principalId.trim();
294
- if (!normalizedPrincipalId) {
295
- return null;
296
- }
297
- if (typeof ctx.runQuery === "function") {
298
- try {
299
- const bridgedAgent = await ctx.runQuery(
300
- api.agents.getAgentByPrincipalId,
301
- {
302
- principalId: normalizedPrincipalId
303
- }
304
- );
305
- if (bridgedAgent) {
306
- return bridgedAgent;
307
- }
308
- } catch {
309
- }
310
- }
311
- try {
312
- const agents = await ctx.db.query("agents").collect();
313
- return agents.find(
314
- (agent) => String(agent.principalId ?? "") === normalizedPrincipalId
315
- ) ?? null;
316
- } catch {
317
- return null;
318
- }
319
- }
320
- function defaultResolvers() {
321
- return {
322
- async getProject(ctx, topicId) {
323
- return await resolveTopicProjectOverlay(ctx, topicId, {
324
- idMode: "legacy",
325
- projectLikeOnly: false
326
- });
327
- },
328
- async listTopics(ctx) {
329
- return await listTopicProjectOverlays(ctx, { idMode: "legacy" });
330
- },
331
- async listTopicsByOwner(ctx, ownerId) {
332
- const topics = await listTopicProjectOverlays(ctx, { idMode: "legacy" });
333
- return topics.filter((topic) => topic.ownerId === ownerId);
334
- },
335
- async listTopicsByVisibility(ctx, visibility) {
336
- const topics = await listTopicProjectOverlays(ctx, { idMode: "legacy" });
337
- return topics.filter((topic) => topic.visibility === visibility);
338
- },
339
- async listProjectGrantsByProjectAndPrincipal(ctx, topicId, principalId) {
340
- return await bridgeListProjectGrantsByTopicAndPrincipal(
341
- ctx,
342
- topicId,
343
- principalId
344
- );
345
- },
346
- async listProjectGrantsByProjectAndGroup(ctx, topicId, groupId) {
347
- return await bridgeListProjectGrantsByTopicAndGroup(ctx, topicId, groupId);
348
- },
349
- async listProjectGrantsByPrincipalStatus(ctx, principalId, status) {
350
- return await bridgeListProjectGrantsByPrincipalStatus(
351
- ctx,
352
- principalId,
353
- status
354
- );
355
- },
356
- async listProjectGrantsByGroupStatus(ctx, groupId, status) {
357
- return await bridgeListProjectGrantsByGroupStatus(ctx, groupId, status);
358
- },
359
- async insertProjectGrant(ctx, value) {
360
- return await bridgeInsertProjectGrant(ctx, value);
361
- },
362
- async getAgentByPrincipalId(ctx, principalId) {
363
- return await findAgentByPrincipalId(ctx, principalId);
364
- },
365
- async getUserByClerkId(ctx, clerkId) {
366
- return await findUserByClerkId(ctx, clerkId);
367
- },
368
- async getUserByPrincipalId(ctx, principalId) {
369
- return await findUserByPrincipalId(ctx, principalId);
370
- }
371
- };
372
- }
373
- var resolverOverrides = {};
374
- function resolveAccessControlAppResolvers(_ctx) {
375
- return {
376
- ...defaultResolvers(),
377
- ...resolverOverrides
378
- };
379
- }
380
-
381
- // ../access-control/src/principalContext.ts
382
- function requireCanonicalResolvedUser(user, clerkId) {
383
- const resolved = user;
384
- if (!resolved) {
385
- throw new Error(
386
- `[AccessControl] Canonical user identity required for ${clerkId}. Sync users.upsertUser before user-bound access checks.`
387
- );
388
- }
389
- const { mcRole, defaultTenantId, defaultWorkspaceId, defaultPrincipalId } = resolved;
390
- if (mcRole !== "platform_admin" && mcRole !== "tenant_admin" && mcRole !== "workspace_admin" && mcRole !== "editor" && mcRole !== "viewer" && mcRole !== "auditor" && mcRole !== "service_agent") {
391
- throw new Error(
392
- `[AccessControl] Canonical MC role required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
393
- );
394
- }
395
- if (typeof defaultTenantId !== "string" || defaultTenantId.trim().length === 0) {
396
- throw new Error(
397
- `[AccessControl] Canonical home tenant required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
398
- );
399
- }
400
- if (typeof defaultWorkspaceId !== "string" || defaultWorkspaceId.trim().length === 0) {
401
- throw new Error(
402
- `[AccessControl] Canonical home workspace required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
403
- );
404
- }
405
- if (typeof defaultPrincipalId !== "string" || defaultPrincipalId.trim().length === 0) {
406
- throw new Error(
407
- `[AccessControl] Canonical federated principal required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
408
- );
409
- }
410
- return {
411
- mcRole,
412
- defaultTenantId: defaultTenantId.trim(),
413
- defaultWorkspaceId: defaultWorkspaceId.trim(),
414
- defaultPrincipalId: defaultPrincipalId.trim()
415
- };
416
- }
417
- function isPrincipalIdInput(value) {
418
- return value.startsWith("user:") || value.startsWith("group:") || value.startsWith("service:") || value.startsWith("agent:") || value.startsWith("external_viewer:");
419
- }
420
- async function resolveCanonicalUserRecord(ctx, actorId) {
421
- const normalizedActorId = actorId.trim();
422
- const clerkId = isPrincipalIdInput(normalizedActorId) && normalizedActorId.startsWith("user:") ? normalizedActorId.slice("user:".length) : normalizedActorId;
423
- const resolvers = resolveAccessControlAppResolvers();
424
- const resolvedByClerkId = await resolvers.getUserByClerkId(ctx, clerkId);
425
- if (resolvedByClerkId) {
426
- return {
427
- resolvedUser: resolvedByClerkId,
428
- clerkId,
429
- contextClerkId: clerkId
430
- };
431
- }
432
- const resolvedByPrincipalId = await resolvers.getUserByPrincipalId(
433
- ctx,
434
- normalizedActorId
435
- );
436
- return {
437
- resolvedUser: resolvedByPrincipalId ?? null,
438
- clerkId,
439
- contextClerkId: normalizedActorId.startsWith("user:") && clerkId.length > 0 ? clerkId : normalizedActorId
440
- };
441
- }
442
- function uniqRoles(roles) {
443
- const roleSet = /* @__PURE__ */ new Set();
444
- for (const role of roles) {
445
- if (role === "platform_admin" || role === "tenant_admin" || role === "workspace_admin" || role === "editor" || role === "viewer" || role === "auditor" || role === "service_agent") {
446
- roleSet.add(role);
447
- }
448
- }
449
- return [...roleSet];
450
- }
451
- function normalizeGroupIds(value) {
452
- if (!Array.isArray(value)) {
453
- return [];
454
- }
455
- return [...new Set(
456
- value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean)
457
- )];
458
- }
459
- function requireServiceAgentUser(user, actorId) {
460
- const canonicalUser = requireCanonicalResolvedUser(user, actorId);
461
- if (canonicalUser.mcRole !== "service_agent") {
462
- throw new Error(
463
- `[AccessControl] Canonical service_agent identity required for ${actorId}. Sync users.upsertUser before agent-bound access checks.`
464
- );
465
- }
466
- return canonicalUser;
467
- }
468
- function requireCanonicalResolvedAgent(agent, actorId) {
469
- const resolved = agent;
470
- if (!resolved) {
471
- throw new Error(
472
- `[AccessControl] Agent "${actorId}" not found in agents or users table.`
473
- );
474
- }
475
- if (typeof resolved.principalId !== "string" || resolved.principalId.trim().length === 0) {
476
- throw new Error(
477
- `[AccessControl] Canonical agent principalId required for ${actorId}.`
478
- );
479
- }
480
- if (typeof resolved.tenantId !== "string" || resolved.tenantId.trim().length === 0) {
481
- throw new Error(
482
- `[AccessControl] Canonical home tenant required for ${actorId}.`
483
- );
484
- }
485
- if (typeof resolved.workspaceId !== "string" || resolved.workspaceId.trim().length === 0) {
486
- throw new Error(
487
- `[AccessControl] Canonical home workspace required for ${actorId}.`
488
- );
489
- }
490
- return {
491
- principalId: resolved.principalId.trim(),
492
- tenantId: resolved.tenantId.trim(),
493
- workspaceId: resolved.workspaceId.trim(),
494
- roles: uniqRoles(Array.isArray(resolved.roles) ? resolved.roles : []) ?? ["service_agent"],
495
- groupIds: normalizeGroupIds(resolved.groupIds)
496
- };
497
- }
498
- async function resolvePrincipalContext(ctx, actorId) {
499
- if (actorId.startsWith("agent:")) {
500
- const resolvers = resolveAccessControlAppResolvers();
501
- const resolvedAgent = await resolvers.getAgentByPrincipalId(ctx, actorId);
502
- if (resolvedAgent) {
503
- const agent = requireCanonicalResolvedAgent(
504
- resolvedAgent,
505
- actorId
506
- );
507
- return {
508
- principalId: agent.principalId,
509
- principalType: "service",
510
- clerkId: actorId,
511
- tenantId: agent.tenantId,
512
- workspaceId: agent.workspaceId,
513
- roles: agent.roles.length > 0 ? agent.roles : ["service_agent"],
514
- groupIds: agent.groupIds,
515
- isPlatformAdmin: false,
516
- isTenantAdmin: false,
517
- isWorkspaceAdmin: false,
518
- isSystemFallback: false
519
- };
520
- }
521
- const resolvedUser2 = await resolvers.getUserByClerkId(
522
- ctx,
523
- actorId
524
- );
525
- if (!resolvedUser2) {
526
- throw new Error(
527
- `[AccessControl] Agent "${actorId}" not found in agents or users table.`
528
- );
529
- }
530
- const user2 = requireServiceAgentUser(
531
- resolvedUser2,
532
- actorId
533
- );
534
- console.warn(
535
- `[AccessControl] Deprecated legacy service-agent fallback for ${actorId}; migrate this principal into identity.agents.`
536
- );
537
- return {
538
- principalId: user2.defaultPrincipalId,
539
- principalType: "service",
540
- clerkId: actorId,
541
- tenantId: user2.defaultTenantId,
542
- workspaceId: user2.defaultWorkspaceId,
543
- roles: ["service_agent"],
544
- groupIds: normalizeGroupIds(resolvedUser2?.principalGroupIds),
545
- isPlatformAdmin: false,
546
- isTenantAdmin: false,
547
- isWorkspaceAdmin: false,
548
- isSystemFallback: false
549
- };
550
- }
551
- const {
552
- resolvedUser,
553
- contextClerkId
554
- } = await resolveCanonicalUserRecord(ctx, actorId);
555
- const user = requireCanonicalResolvedUser(
556
- resolvedUser,
557
- contextClerkId
558
- );
559
- if (!user.defaultPrincipalId) {
560
- throw new Error(
561
- `[AccessControl] Canonical federated principal required for ${contextClerkId}. Re-sync Master Control identity before user-bound access checks.`
562
- );
563
- }
564
- if (user.mcRole === "service_agent") {
565
- return {
566
- principalId: user.defaultPrincipalId,
567
- principalType: "service",
568
- clerkId: contextClerkId,
569
- tenantId: user.defaultTenantId,
570
- workspaceId: user.defaultWorkspaceId,
571
- roles: ["service_agent"],
572
- groupIds: normalizeGroupIds(resolvedUser?.principalGroupIds),
573
- isPlatformAdmin: false,
574
- isTenantAdmin: false,
575
- isWorkspaceAdmin: false,
576
- isSystemFallback: false
577
- };
578
- }
579
- const principalId = user.defaultPrincipalId;
580
- const effectiveRole = user.mcRole;
581
- const roles = effectiveRole === "platform_admin" ? ["platform_admin", "tenant_admin"] : effectiveRole === "tenant_admin" ? ["tenant_admin"] : [effectiveRole];
582
- const tenantId = user.defaultTenantId;
583
- const workspaceId = user.defaultWorkspaceId;
584
- const isPlatformAdmin = effectiveRole === "platform_admin";
585
- return {
586
- principalId,
587
- principalType: "user",
588
- clerkId: contextClerkId,
589
- tenantId,
590
- workspaceId,
591
- roles: uniqRoles(roles),
592
- groupIds: normalizeGroupIds(resolvedUser?.principalGroupIds),
593
- isPlatformAdmin,
594
- isTenantAdmin: isPlatformAdmin || effectiveRole === "tenant_admin",
595
- isWorkspaceAdmin: isPlatformAdmin || effectiveRole === "tenant_admin" || effectiveRole === "workspace_admin",
596
- isSystemFallback: false
597
- };
598
- }
599
-
600
- // ../access-control/src/access.ts
601
- function isTopicInPrincipalTenant(topic, principalTenantId) {
602
- if (!topic.tenantId) {
603
- return false;
604
- }
605
- if (!principalTenantId) {
606
- return false;
607
- }
608
- return String(topic.tenantId) === String(principalTenantId);
609
- }
610
- function isTopicInPrincipalWorkspace(topic, principalWorkspaceId) {
611
- if (!topic.workspaceId) {
612
- return false;
613
- }
614
- if (!principalWorkspaceId) {
615
- return false;
616
- }
617
- return String(topic.workspaceId) === String(principalWorkspaceId);
618
- }
619
- function isLegacyUnscopedTopic(topic) {
620
- return !topic.tenantId || !topic.workspaceId;
621
- }
622
- function isGrantScopeAlignedToTopic(topic, grant) {
623
- if (topic.tenantId && grant.tenantId && String(topic.tenantId) !== String(grant.tenantId)) {
624
- return false;
625
- }
626
- if (topic.workspaceId && grant.workspaceId && String(topic.workspaceId) !== String(grant.workspaceId)) {
627
- return false;
628
- }
629
- return true;
630
- }
631
- function isGrantSourceAllowedForVisibility(visibility, source) {
632
- if (source !== "external_share") {
633
- return true;
634
- }
635
- return visibility === "external" || visibility === "public";
636
- }
637
- function isGrantActive(grant) {
638
- if (grant.status !== "active") {
639
- return false;
640
- }
641
- if (grant.expiresAt !== void 0 && grant.expiresAt <= Date.now()) {
642
- return false;
643
- }
644
- return true;
645
- }
646
- async function hasPrincipalGrant(ctx, args) {
647
- const grants = await resolveAccessControlAppResolvers().listProjectGrantsByProjectAndPrincipal(
648
- ctx,
649
- args.topic._id,
650
- args.principalId
651
- );
652
- if (grants.some(
653
- (grant) => isGrantActive(grant) && isGrantScopeAlignedToTopic(args.topic, grant) && isGrantSourceAllowedForVisibility(
654
- args.topic.visibility,
655
- grant.source
656
- ) && (!args.principalIsExternal || args.topic.visibility === "public" || grant.source === "external_share")
657
- )) {
658
- return true;
659
- }
660
- return false;
661
- }
662
- async function hasGroupGrant(ctx, args) {
663
- if (args.groupIds.length === 0) {
664
- return false;
665
- }
666
- for (const groupId of args.groupIds) {
667
- const grants = await resolveAccessControlAppResolvers().listProjectGrantsByProjectAndGroup(ctx, args.topic._id, groupId);
668
- if (grants.some(
669
- (grant) => isGrantActive(grant) && isGrantScopeAlignedToTopic(args.topic, grant) && isGrantSourceAllowedForVisibility(
670
- args.topic.visibility,
671
- grant.source
672
- )
673
- )) {
674
- return true;
675
- }
676
- }
677
- return false;
678
- }
679
- function isExternalPrincipal(_ctx, _args) {
680
- return false;
681
- }
682
- async function evaluateTopicAccessDetailed(ctx, args) {
683
- if (args.legacyUserId) {
684
- return {
685
- hasAccess: true,
686
- isAdmin: false,
687
- isOwner: false,
688
- isShared: false,
689
- hasGrant: true,
690
- isFirmVisible: true,
691
- isExternalVisible: false,
692
- isPublicVisible: false,
693
- isTenantScopeMatch: true,
694
- isWorkspaceScopeMatch: true,
695
- isPrincipalExternal: false
696
- };
697
- }
698
- const topic = await resolveAccessControlAppResolvers().getProject(
699
- ctx,
700
- args.topicId
701
- );
702
- if (!topic) {
703
- return {
704
- hasAccess: false,
705
- isAdmin: false,
706
- isOwner: false,
707
- isShared: false,
708
- hasGrant: false,
709
- isFirmVisible: false,
710
- isExternalVisible: false,
711
- isPublicVisible: false,
712
- isTenantScopeMatch: false,
713
- isWorkspaceScopeMatch: false,
714
- isPrincipalExternal: false
715
- };
716
- }
717
- const { principalContext, legacyUserId } = args;
718
- const userIsAdmin = principalContext.isPlatformAdmin;
719
- const isOwner = topic.ownerId === legacyUserId;
720
- const isShared = (topic.sharedWith ?? []).includes(legacyUserId);
721
- const principalIsExternal = await isExternalPrincipal(ctx, {
722
- groupIds: principalContext.groupIds,
723
- topicTenantId: topic.tenantId,
724
- topicWorkspaceId: topic.workspaceId
725
- });
726
- const hasPrincipalGrantResult = await hasPrincipalGrant(ctx, {
727
- topic,
728
- principalId: principalContext.principalId,
729
- principalIsExternal
730
- });
731
- const hasGroupGrantResult = await hasGroupGrant(ctx, {
732
- topic,
733
- groupIds: principalContext.groupIds
734
- });
735
- const hasGrant = isShared || hasPrincipalGrantResult || hasGroupGrantResult;
736
- const legacyUnscoped = isLegacyUnscopedTopic(topic);
737
- const tenantScopeMatch = isTopicInPrincipalTenant(
738
- topic,
739
- principalContext.tenantId
740
- );
741
- const workspaceScopeMatch = isTopicInPrincipalWorkspace(
742
- topic,
743
- principalContext.workspaceId
744
- );
745
- const isPublicVisible = topic.visibility === "public";
746
- const isFirmVisible = topic.visibility === "firm" && !legacyUnscoped && tenantScopeMatch && workspaceScopeMatch && !principalIsExternal;
747
- const hasScopedGrant = hasGrant && (legacyUnscoped || tenantScopeMatch && workspaceScopeMatch);
748
- const isExternalVisible = topic.visibility === "external" && hasScopedGrant;
749
- const hasAccess = userIsAdmin || isOwner || hasScopedGrant || isPublicVisible || isFirmVisible;
750
- return {
751
- hasAccess,
752
- isAdmin: userIsAdmin,
753
- isOwner,
754
- isShared,
755
- hasGrant,
756
- isFirmVisible,
757
- isExternalVisible,
758
- isPublicVisible,
759
- isTenantScopeMatch: tenantScopeMatch,
760
- isWorkspaceScopeMatch: workspaceScopeMatch,
761
- isPrincipalExternal: principalIsExternal
762
- };
763
- }
764
- async function checkTopicAccessDetailed(ctx, topicId, userId) {
765
- const principalContext = await resolvePrincipalContext(ctx, userId);
766
- return evaluateTopicAccessDetailed(ctx, {
767
- topicId,
768
- legacyUserId: userId,
769
- principalContext
770
- });
771
- }
772
- async function checkTopicAccess(ctx, topicId, userId) {
773
- const result = await checkTopicAccessDetailed(ctx, topicId, userId);
774
- return result.hasAccess;
775
- }
776
- var checkProjectAccess = checkTopicAccess;
777
- var permissiveReturn = v.optional(v.any());
778
- var looseJsonObject = v.record(v.string(), v.any());
779
- var looseJsonArray = v.array(v.any());
780
- v.union(
781
- v.string(),
782
- v.number(),
783
- v.boolean(),
784
- v.null(),
785
- looseJsonObject,
786
- looseJsonArray
787
- );
788
- var api2 = anyApi;
789
- componentsGeneric();
790
9
  var internal = anyApi;
791
10
  var mutation = mutationGeneric;
792
11
  var query = queryGeneric;
@@ -824,48 +43,48 @@ function deriveMatcherReviewStatus(input) {
824
43
  }
825
44
 
826
45
  // src/topicProjectOverlay.ts
827
- var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
828
- function readNonEmptyString2(value) {
46
+ var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
47
+ function readNonEmptyString(value) {
829
48
  if (typeof value !== "string") {
830
49
  return;
831
50
  }
832
51
  const normalized = value.trim();
833
52
  return normalized.length > 0 ? normalized : void 0;
834
53
  }
835
- function readStringArray2(value) {
54
+ function readStringArray(value) {
836
55
  if (!Array.isArray(value)) {
837
56
  return [];
838
57
  }
839
- return value.map((entry) => readNonEmptyString2(entry)).filter((entry) => Boolean(entry));
58
+ return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
840
59
  }
841
- function readMetadata2(topic) {
60
+ function readMetadata(topic) {
842
61
  return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
843
62
  }
844
- function readLegacyProjectId2(value) {
63
+ function readLegacyProjectId(value) {
845
64
  if (!value) {
846
65
  return;
847
66
  }
848
- return readNonEmptyString2(value[LEGACY_SCOPE_FIELD2]);
67
+ return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
849
68
  }
850
- function coerceVisibility2(value) {
69
+ function coerceVisibility(value) {
851
70
  return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
852
71
  }
853
- function coerceStatus2(value) {
72
+ function coerceStatus(value) {
854
73
  return value === "active" || value === "archived" || value === "watching" ? value : void 0;
855
74
  }
856
- function mapProjectType2(topic, metadata) {
857
- const explicit = readNonEmptyString2(metadata.projectType);
75
+ function mapProjectType(topic, metadata) {
76
+ const explicit = readNonEmptyString(metadata.projectType);
858
77
  if (explicit) {
859
78
  return explicit;
860
79
  }
861
80
  if (topic.type === "theme") {
862
81
  return "thematic";
863
82
  }
864
- return readNonEmptyString2(topic.type) || "general";
83
+ return readNonEmptyString(topic.type) || "general";
865
84
  }
866
- function isProjectLikeTopic2(topic) {
867
- const metadata = readMetadata2(topic);
868
- return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId2(topic) !== void 0 || readNonEmptyString2(metadata.projectType) !== void 0;
85
+ function isProjectLikeTopic(topic) {
86
+ const metadata = readMetadata(topic);
87
+ return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
869
88
  }
870
89
  function isMissingLucernChildComponentError(error) {
871
90
  const message = error instanceof Error ? error.message : String(error);
@@ -873,7 +92,7 @@ function isMissingLucernChildComponentError(error) {
873
92
  'Child component ComponentName(Identifier("lucern")) not found'
874
93
  ) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
875
94
  }
876
- async function resolveTopicDoc2(ctx, scopeId) {
95
+ async function resolveTopicDoc(ctx, scopeId) {
877
96
  if (ctx?.db && typeof ctx.db.get === "function") {
878
97
  try {
879
98
  const directTopic = await ctx.db.get(scopeId);
@@ -887,7 +106,7 @@ async function resolveTopicDoc2(ctx, scopeId) {
887
106
  return null;
888
107
  }
889
108
  try {
890
- const topic = await ctx.runQuery(api2.topics.get, {
109
+ const topic = await ctx.runQuery(api.topics.get, {
891
110
  id: String(scopeId)
892
111
  });
893
112
  if (topic?.name !== void 0 && topic?.type !== void 0) {
@@ -896,7 +115,7 @@ async function resolveTopicDoc2(ctx, scopeId) {
896
115
  } catch {
897
116
  }
898
117
  try {
899
- const topic = await ctx.runQuery(api2.topics.getByLegacyScopeId, {
118
+ const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
900
119
  projectId: String(scopeId)
901
120
  });
902
121
  if (topic?.name !== void 0 && topic?.type !== void 0) {
@@ -906,14 +125,14 @@ async function resolveTopicDoc2(ctx, scopeId) {
906
125
  }
907
126
  return null;
908
127
  }
909
- function materializeTopicProjectOverlay2(topic, idMode = "legacy") {
910
- const metadata = readMetadata2(topic);
128
+ function materializeTopicProjectOverlay(topic, idMode = "legacy") {
129
+ const metadata = readMetadata(topic);
911
130
  const topicId = String(topic._id);
912
- const legacyProjectId = readLegacyProjectId2(topic) || readLegacyProjectId2(metadata) || readNonEmptyString2(metadata.legacyProjectId);
131
+ const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
913
132
  const storageProjectId = legacyProjectId || topicId;
914
133
  const outwardId = idMode === "topic" ? topicId : storageProjectId;
915
- const visibility = coerceVisibility2(topic.visibility) || coerceVisibility2(metadata.visibility) || "private";
916
- const status = coerceStatus2(topic.status) || coerceStatus2(metadata.status) || "active";
134
+ const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
135
+ const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
917
136
  const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
918
137
  const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
919
138
  return {
@@ -923,16 +142,16 @@ function materializeTopicProjectOverlay2(topic, idMode = "legacy") {
923
142
  topicId,
924
143
  storageProjectId,
925
144
  legacyProjectId,
926
- name: readNonEmptyString2(topic.name) || "Untitled Theme",
927
- type: mapProjectType2(topic, metadata),
928
- description: readNonEmptyString2(topic.description),
929
- ownerId: readNonEmptyString2(metadata.ownerId) || readNonEmptyString2(topic.createdBy) || "system",
930
- sharedWith: readStringArray2(metadata.sharedWith),
145
+ name: readNonEmptyString(topic.name) || "Untitled Theme",
146
+ type: mapProjectType(topic, metadata),
147
+ description: readNonEmptyString(topic.description),
148
+ ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
149
+ sharedWith: readStringArray(metadata.sharedWith),
931
150
  visibility,
932
- tenantId: readNonEmptyString2(topic.tenantId) || readNonEmptyString2(metadata.tenantId),
933
- workspaceId: readNonEmptyString2(topic.workspaceId) || readNonEmptyString2(metadata.workspaceId),
151
+ tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
152
+ workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
934
153
  status,
935
- tags: readStringArray2(metadata.tags),
154
+ tags: readStringArray(metadata.tags),
936
155
  chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
937
156
  artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
938
157
  lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
@@ -941,17 +160,17 @@ function materializeTopicProjectOverlay2(topic, idMode = "legacy") {
941
160
  updatedAt
942
161
  };
943
162
  }
944
- async function resolveTopicProjectOverlay2(ctx, scopeId, options = {}) {
945
- const topic = await resolveTopicDoc2(ctx, scopeId);
163
+ async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
164
+ const topic = await resolveTopicDoc(ctx, scopeId);
946
165
  if (!topic) {
947
166
  return null;
948
167
  }
949
- if (options.projectLikeOnly !== false && !isProjectLikeTopic2(topic)) {
168
+ if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
950
169
  return null;
951
170
  }
952
- return materializeTopicProjectOverlay2(topic, options.idMode);
171
+ return materializeTopicProjectOverlay(topic, options.idMode);
953
172
  }
954
- async function listTopicProjectOverlays2(ctx, options = {}) {
173
+ async function listTopicProjectOverlays(ctx, options = {}) {
955
174
  let allTopics = [];
956
175
  if (ctx?.db?.query && typeof ctx.db.query === "function") {
957
176
  try {
@@ -961,18 +180,18 @@ async function listTopicProjectOverlays2(ctx, options = {}) {
961
180
  }
962
181
  }
963
182
  if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
964
- allTopics = (await ctx.runQuery(api2.topics.list, {}) ?? []) || [];
183
+ allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
965
184
  }
966
185
  return allTopics.filter(
967
- (topic) => options.projectLikeOnly === false || isProjectLikeTopic2(topic)
968
- ).map((topic) => materializeTopicProjectOverlay2(topic, options.idMode));
186
+ (topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
187
+ ).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
969
188
  }
970
189
  async function patchTopicProjectOverlay(ctx, scopeId, value) {
971
- const topic = await resolveTopicDoc2(ctx, scopeId);
190
+ const topic = await resolveTopicDoc(ctx, scopeId);
972
191
  if (!topic) {
973
192
  return null;
974
193
  }
975
- const nextMetadata = { ...readMetadata2(topic) };
194
+ const nextMetadata = { ...readMetadata(topic) };
976
195
  const patch = {};
977
196
  const topicUpdateArgs = {
978
197
  id: String(topic._id)
@@ -997,7 +216,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
997
216
  `patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
998
217
  );
999
218
  case "status": {
1000
- const status = coerceStatus2(rawValue);
219
+ const status = coerceStatus(rawValue);
1001
220
  if (status) {
1002
221
  patch.status = status;
1003
222
  topicUpdateArgs.status = status;
@@ -1005,7 +224,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
1005
224
  break;
1006
225
  }
1007
226
  case "visibility": {
1008
- const visibility = coerceVisibility2(rawValue);
227
+ const visibility = coerceVisibility(rawValue);
1009
228
  if (visibility) {
1010
229
  patch.visibility = visibility;
1011
230
  topicUpdateArgs.visibility = visibility;
@@ -1013,7 +232,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
1013
232
  break;
1014
233
  }
1015
234
  case "type": {
1016
- const projectType = readNonEmptyString2(rawValue);
235
+ const projectType = readNonEmptyString(rawValue);
1017
236
  if (projectType) {
1018
237
  nextMetadata.projectType = projectType;
1019
238
  } else {
@@ -1037,7 +256,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
1037
256
  topicUpdateArgs.metadata = nextMetadata;
1038
257
  if (typeof ctx.runMutation === "function") {
1039
258
  try {
1040
- await ctx.runMutation(api2.topics.update, topicUpdateArgs);
259
+ await ctx.runMutation(api.topics.update, topicUpdateArgs);
1041
260
  } catch (error) {
1042
261
  if (!isMissingLucernChildComponentError(error) || !ctx?.db || typeof ctx.db.patch !== "function") {
1043
262
  throw error;
@@ -1051,7 +270,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
1051
270
  "Cannot patch topic without component adapter (ctx.runMutation unavailable)"
1052
271
  );
1053
272
  }
1054
- return materializeTopicProjectOverlay2(
273
+ return materializeTopicProjectOverlay(
1055
274
  {
1056
275
  ...topic,
1057
276
  ...patch,
@@ -1084,10 +303,10 @@ async function patchProjectWithTolerance(ctx, projectId, value) {
1084
303
  });
1085
304
  }
1086
305
  }
1087
- function defaultResolvers2() {
306
+ function defaultResolvers() {
1088
307
  return {
1089
308
  async getProject(ctx, projectId) {
1090
- return await resolveTopicProjectOverlay2(ctx, projectId, {
309
+ return await resolveTopicProjectOverlay(ctx, projectId, {
1091
310
  idMode: "legacy",
1092
311
  projectLikeOnly: false
1093
312
  });
@@ -1096,7 +315,7 @@ function defaultResolvers2() {
1096
315
  await patchProjectWithTolerance(ctx, projectId, value);
1097
316
  },
1098
317
  async listTopics(ctx) {
1099
- return await listTopicProjectOverlays2(ctx, {
318
+ return await listTopicProjectOverlays(ctx, {
1100
319
  idMode: "legacy"
1101
320
  });
1102
321
  },
@@ -1105,13 +324,195 @@ function defaultResolvers2() {
1105
324
  }
1106
325
  };
1107
326
  }
1108
- var resolverOverrides2 = {};
327
+ var resolverOverrides = {};
1109
328
  function resolveGraphPrimitivesAppResolvers(_ctx) {
1110
329
  return {
1111
- ...defaultResolvers2(),
1112
- ...resolverOverrides2
330
+ ...defaultResolvers(),
331
+ ...resolverOverrides
1113
332
  };
1114
333
  }
334
+ var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
335
+ function asMappedProjectId(topic) {
336
+ if (!topic) {
337
+ return;
338
+ }
339
+ const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
340
+ if (directLegacyProjectId) {
341
+ return directLegacyProjectId;
342
+ }
343
+ const metadata = topic.metadata || {};
344
+ const candidate = metadata[LEGACY_SCOPE_FIELD2] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
345
+ return candidate ? candidate : void 0;
346
+ }
347
+ function normalizeScopeValue(value) {
348
+ if (typeof value !== "string") {
349
+ return;
350
+ }
351
+ const normalized = value.trim();
352
+ return normalized.length > 0 ? normalized : void 0;
353
+ }
354
+ function pickPrimaryTopic(candidates) {
355
+ return [...candidates].sort((a, b) => {
356
+ const depthA = a.depth ?? 9999;
357
+ const depthB = b.depth ?? 9999;
358
+ if (depthA !== depthB) {
359
+ return depthA - depthB;
360
+ }
361
+ const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
362
+ const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
363
+ if (createdA !== createdB) {
364
+ return createdA - createdB;
365
+ }
366
+ return String(a.name || "").localeCompare(String(b.name || ""));
367
+ })[0];
368
+ }
369
+ async function findTopicsByScopeAlias(ctx, scopeId) {
370
+ try {
371
+ return await ctx.db.query("topics").withIndex(
372
+ "by_graph_scope_project",
373
+ (q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
374
+ ).collect();
375
+ } catch {
376
+ const topics = await ctx.db.query("topics").collect();
377
+ return topics.filter((topic) => {
378
+ const normalizedGlobalId = normalizeScopeValue(topic.globalId);
379
+ const mappedProjectId = asMappedProjectId(topic);
380
+ return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
381
+ });
382
+ }
383
+ }
384
+ async function tryResolveHostTopicById(ctx, topicId) {
385
+ if (typeof ctx.runQuery !== "function") {
386
+ return null;
387
+ }
388
+ try {
389
+ return await ctx.runQuery(api.topics.get, {
390
+ id: topicId
391
+ }) ?? null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+ async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
397
+ if (typeof ctx.runQuery !== "function") {
398
+ return null;
399
+ }
400
+ try {
401
+ return await ctx.runQuery(api.topics.getByLegacyScopeId, {
402
+ projectId: legacyScopeId
403
+ }) ?? null;
404
+ } catch {
405
+ return null;
406
+ }
407
+ }
408
+ async function resolveInheritedWorkspaceScope(ctx, topic) {
409
+ const MAX_DEPTH = 10;
410
+ let tenantId = normalizeScopeValue(topic.tenantId);
411
+ let workspaceId = normalizeScopeValue(topic.workspaceId);
412
+ if (tenantId && workspaceId) {
413
+ return { tenantId, workspaceId };
414
+ }
415
+ let current = topic;
416
+ for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
417
+ current = await ctx.db.get(current.parentTopicId);
418
+ if (!current) break;
419
+ if (!tenantId) {
420
+ tenantId = normalizeScopeValue(current.tenantId);
421
+ }
422
+ if (!workspaceId) {
423
+ workspaceId = normalizeScopeValue(current.workspaceId);
424
+ }
425
+ if (tenantId && workspaceId) break;
426
+ }
427
+ return { tenantId, workspaceId };
428
+ }
429
+ async function resolveTopicProjectScope(ctx, args) {
430
+ if (args.topicId) {
431
+ let topic = null;
432
+ try {
433
+ topic = await ctx.db.get(args.topicId);
434
+ } catch {
435
+ }
436
+ if (!topic) {
437
+ topic = await tryResolveHostTopicById(ctx, String(args.topicId));
438
+ }
439
+ if (!topic) {
440
+ topic = pickPrimaryTopic(
441
+ await findTopicsByScopeAlias(ctx, String(args.topicId))
442
+ ) ?? null;
443
+ }
444
+ if (!topic) {
445
+ throw new Error(`Topic not found: ${String(args.topicId)}`);
446
+ }
447
+ const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
448
+ const mapped = asMappedProjectId(topic);
449
+ if (mapped) {
450
+ return {
451
+ topicId: topic._id,
452
+ projectId: mapped,
453
+ tenantId: inherited.tenantId,
454
+ workspaceId: inherited.workspaceId,
455
+ source: "topic"
456
+ };
457
+ }
458
+ return {
459
+ topicId: topic._id,
460
+ tenantId: inherited.tenantId,
461
+ workspaceId: inherited.workspaceId,
462
+ source: "topic"
463
+ };
464
+ }
465
+ if (args.projectId) {
466
+ let directTopic = null;
467
+ try {
468
+ directTopic = await ctx.db.get(
469
+ args.projectId
470
+ );
471
+ } catch {
472
+ }
473
+ if (directTopic) {
474
+ const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
475
+ const mapped = asMappedProjectId(directTopic);
476
+ return {
477
+ topicId: directTopic._id,
478
+ projectId: mapped ?? args.projectId,
479
+ tenantId: inherited.tenantId,
480
+ workspaceId: inherited.workspaceId,
481
+ source: "topic_inferred"
482
+ };
483
+ }
484
+ directTopic = await tryResolveHostTopicByLegacyScope(ctx, args.projectId);
485
+ if (directTopic) {
486
+ const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
487
+ const mapped = asMappedProjectId(directTopic);
488
+ return {
489
+ topicId: directTopic._id,
490
+ projectId: mapped ?? args.projectId,
491
+ tenantId: inherited.tenantId,
492
+ workspaceId: inherited.workspaceId,
493
+ source: "topic_inferred"
494
+ };
495
+ }
496
+ const topics = await findTopicsByScopeAlias(ctx, args.projectId);
497
+ const primary = pickPrimaryTopic(topics);
498
+ if (primary) {
499
+ const inherited = await resolveInheritedWorkspaceScope(ctx, primary);
500
+ return {
501
+ topicId: primary._id,
502
+ projectId: args.projectId,
503
+ tenantId: inherited.tenantId,
504
+ workspaceId: inherited.workspaceId,
505
+ source: "project_mapped_topic"
506
+ };
507
+ }
508
+ throw new Error(
509
+ `Legacy project scope ${String(args.projectId)} has no mapped topic.`
510
+ );
511
+ }
512
+ throw new Error(
513
+ "Missing scope: provide topicId (preferred) or legacy projectId alias."
514
+ );
515
+ }
1115
516
  var optionalScopeArgs = {
1116
517
  projectId: v.optional(v.string()),
1117
518
  topicId: v.optional(v.string())
@@ -1618,11 +1019,15 @@ var getByProject = query({
1618
1019
  if (!hasAccess) {
1619
1020
  return [];
1620
1021
  }
1022
+ const scope = await resolveTopicProjectScope(ctx, args).catch(() => null);
1023
+ if (!scope) {
1024
+ return [];
1025
+ }
1621
1026
  const pageSize = Math.max(1, Math.min(Math.floor(args.limit ?? 300), 1e3));
1622
1027
  const questionScanLimit = Math.min(pageSize * 2, 1e3);
1623
1028
  const questions = await ctx.db.query("epistemicNodes").withIndex(
1624
- args.topicId ? "by_topic_type" : "by_project_type",
1625
- (q) => args.topicId ? q.eq("topicId", args.topicId).eq("nodeType", "question") : q.eq("projectId", args.projectId).eq("nodeType", "question")
1029
+ "by_topic_type",
1030
+ (q) => q.eq("topicId", scope.topicId).eq("nodeType", "question")
1626
1031
  ).order("desc").take(questionScanLimit);
1627
1032
  const questionIds = questions.slice(0, pageSize).map((q) => q._id);
1628
1033
  const allLinks = await Promise.all(