@lucern/graph-primitives 1.0.50 → 1.0.52

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 (67) hide show
  1. package/dist/beliefDecay.js +34 -186
  2. package/dist/beliefEvidenceLinks.js +32 -187
  3. package/dist/contradictions.js +34 -187
  4. package/dist/entityLifecycle.js +88 -205
  5. package/dist/epistemicAnswers.js +35 -187
  6. package/dist/epistemicBeliefs.admin.js +34 -187
  7. package/dist/epistemicBeliefs.backfills.js +34 -188
  8. package/dist/epistemicBeliefs.confidence.d.ts +1 -1
  9. package/dist/epistemicBeliefs.confidence.js +32 -188
  10. package/dist/epistemicBeliefs.core.js +37 -187
  11. package/dist/epistemicBeliefs.d.ts +1 -1
  12. package/dist/epistemicBeliefs.helpers.d.ts +1 -1
  13. package/dist/epistemicBeliefs.helpers.js +34 -186
  14. package/dist/epistemicBeliefs.internal.js +37 -187
  15. package/dist/epistemicBeliefs.js +37 -187
  16. package/dist/epistemicBeliefs.lifecycle.js +32 -188
  17. package/dist/epistemicBeliefs.links.js +34 -188
  18. package/dist/epistemicContracts.evaluators.js +34 -188
  19. package/dist/epistemicContracts.handlers.js +34 -188
  20. package/dist/epistemicContracts.js +34 -188
  21. package/dist/epistemicEdges.d.ts +1 -1
  22. package/dist/epistemicEdges.helpers.d.ts +1 -1
  23. package/dist/epistemicEdges.js +32 -187
  24. package/dist/epistemicEdges.mutations.js +34 -186
  25. package/dist/epistemicEdges.queries.js +35 -188
  26. package/dist/epistemicEdges.types.d.ts +1 -1
  27. package/dist/epistemicEvidence.d.ts +1 -1
  28. package/dist/epistemicEvidence.js +37 -187
  29. package/dist/epistemicEvidenceHelpers.d.ts +1 -1
  30. package/dist/epistemicEvidenceHelpers.js +34 -186
  31. package/dist/epistemicEvidenceMutations.js +37 -187
  32. package/dist/epistemicEvidenceQueries.js +34 -186
  33. package/dist/epistemicHelpers.js +4 -1
  34. package/dist/epistemicInsert.js +4 -1
  35. package/dist/epistemicNodeCreation.js +4 -1
  36. package/dist/epistemicNodes.helpers.d.ts +1 -1
  37. package/dist/epistemicNodes.internal.js +35 -188
  38. package/dist/epistemicNodes.js +37 -188
  39. package/dist/epistemicNodes.mutations.js +35 -188
  40. package/dist/epistemicNodes.queries.js +35 -188
  41. package/dist/epistemicQuestions.conviction.js +34 -186
  42. package/dist/epistemicQuestions.create.js +37 -187
  43. package/dist/epistemicQuestions.d.ts +1 -1
  44. package/dist/epistemicQuestions.evidence.js +37 -187
  45. package/dist/epistemicQuestions.helpers.d.ts +1 -1
  46. package/dist/epistemicQuestions.helpers.js +34 -186
  47. package/dist/epistemicQuestions.js +37 -187
  48. package/dist/epistemicQuestions.lifecycle.js +34 -186
  49. package/dist/epistemicQuestions.queries.js +34 -186
  50. package/dist/epistemicQuestions.sprint.js +35 -188
  51. package/dist/epistemicQuestions.tail.js +37 -187
  52. package/dist/epistemicSources.js +35 -188
  53. package/dist/index.d.ts +1 -1
  54. package/dist/index.js +98 -213
  55. package/dist/proof-attestation.json +1 -1
  56. package/dist/questionEvidenceLinks.js +34 -187
  57. package/dist/scopeResolverCompat.d.ts +1 -1
  58. package/dist/scopeResolverCompat.js +32 -193
  59. package/dist/topicOntologyResolver.d.ts +3 -3
  60. package/dist/topicOntologyResolver.js +57 -18
  61. package/dist/{topicScope-DJVa0mLa.d.ts → topicScope-CL1IVOmv.d.ts} +2 -2
  62. package/dist/topicScope.d.ts +1 -1
  63. package/dist/topicScope.js +32 -193
  64. package/dist/workflowBridge.js +32 -193
  65. package/dist/workspaceIsolation.d.ts +1 -1
  66. package/dist/workspaceIsolation.js +32 -193
  67. package/package.json +4 -4
@@ -1,6 +1,5 @@
1
1
  import { v } from 'convex/values';
2
- import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
3
- import { componentsGeneric } from 'convex/server';
2
+ import { isUuidV7 } from '@lucern/contracts/ids';
4
3
 
5
4
  // src/debug.ts
6
5
  function isGraphPrimitiveDebugEnabled() {
@@ -13,11 +12,6 @@ function debugGraphPrimitiveFallback(message, context) {
13
12
  }
14
13
  console.debug(message, context ?? {});
15
14
  }
16
- var unsafeApi = unsafeConvexAnyApi(
17
- "graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
18
- );
19
- var api = unsafeApi;
20
- componentsGeneric();
21
15
 
22
16
  // src/topicScope.ts
23
17
  var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
@@ -40,16 +34,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
40
34
  if (!node) {
41
35
  return null;
42
36
  }
43
- const scopeKey = normalizeScopeValue(node.topicId) ?? normalizeScopeValue(node.globalId);
37
+ const scopeKey = canonicalTopicGlobalId(node);
44
38
  if (!scopeKey) {
45
- return null;
39
+ throw new Error(
40
+ `topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
41
+ );
46
42
  }
43
+ const metadata = node.metadata ?? {};
47
44
  return {
48
45
  topicId: scopeKey,
49
46
  projectId: asMappedProjectId(node),
50
- source: "topic_node"
47
+ source: "topic_node",
48
+ tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
49
+ workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
51
50
  };
52
51
  }
52
+ function canonicalTopicGlobalId(node) {
53
+ const globalId = normalizeScopeValue(node.globalId);
54
+ if (globalId && isUuidV7(globalId)) {
55
+ return globalId;
56
+ }
57
+ const topicId = normalizeScopeValue(node.topicId);
58
+ return topicId && isUuidV7(topicId) ? topicId : null;
59
+ }
60
+ function requireUuidV7TopicScope(field, value) {
61
+ if (!isUuidV7(value)) {
62
+ throw new Error(
63
+ `topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
64
+ );
65
+ }
66
+ }
53
67
  function asMappedProjectId(topic) {
54
68
  if (!topic) {
55
69
  return;
@@ -71,200 +85,25 @@ function normalizeScopeValue(value) {
71
85
  const normalized = value.trim();
72
86
  return normalized.length > 0 ? normalized : void 0;
73
87
  }
74
- function pickPrimaryTopic(candidates) {
75
- return [...candidates].sort((a, b) => {
76
- const depthA = a.depth ?? 9999;
77
- const depthB = b.depth ?? 9999;
78
- if (depthA !== depthB) {
79
- return depthA - depthB;
80
- }
81
- const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
82
- const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
83
- if (createdA !== createdB) {
84
- return createdA - createdB;
85
- }
86
- return String(a.name || "").localeCompare(String(b.name || ""));
87
- })[0];
88
- }
89
- async function findTopicsByScopeAlias(ctx, scopeId) {
90
- const query = ctx.db.query("topics");
91
- try {
92
- return await query.withIndex(
93
- "by_graph_scope_project",
94
- (q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
95
- ).collect();
96
- } catch (error) {
97
- debugGraphPrimitiveFallback(
98
- "[topicScope] Failed to resolve scope alias via index",
99
- {
100
- error,
101
- scopeId
102
- }
103
- );
104
- const topics = await query.collect();
105
- return topics.filter((topic) => {
106
- const normalizedGlobalId = normalizeScopeValue(topic.globalId);
107
- const mappedProjectId = asMappedProjectId(topic);
108
- return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
109
- });
110
- }
111
- }
112
- async function tryResolveHostTopicById(ctx, topicId) {
113
- if (typeof ctx.runQuery !== "function") {
114
- return null;
115
- }
116
- try {
117
- return await ctx.runQuery(api.topics.get, {
118
- id: topicId
119
- }) ?? null;
120
- } catch (error) {
121
- debugGraphPrimitiveFallback(
122
- "[topicScope] Failed to resolve topic by host query",
123
- {
124
- error,
125
- topicId
126
- }
127
- );
128
- return null;
129
- }
130
- }
131
- async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
132
- if (typeof ctx.runQuery !== "function") {
133
- return null;
134
- }
135
- try {
136
- return await ctx.runQuery(api.topics.getByLegacyScopeId, {
137
- projectId: legacyScopeId
138
- }) ?? null;
139
- } catch (error) {
140
- debugGraphPrimitiveFallback(
141
- "[topicScope] Failed to resolve topic by legacy scope",
142
- {
143
- error,
144
- legacyScopeId
145
- }
146
- );
147
- return null;
148
- }
149
- }
150
- async function resolveInheritedWorkspaceScope(ctx, topic) {
151
- const MAX_DEPTH = 10;
152
- let tenantId = normalizeScopeValue(topic.tenantId);
153
- let workspaceId = normalizeScopeValue(topic.workspaceId);
154
- if (tenantId && workspaceId) {
155
- return { tenantId, workspaceId };
156
- }
157
- let current = topic;
158
- for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
159
- current = await ctx.db.get(current.parentTopicId);
160
- if (!current) {
161
- break;
162
- }
163
- if (!tenantId) {
164
- tenantId = normalizeScopeValue(current.tenantId);
165
- }
166
- if (!workspaceId) {
167
- workspaceId = normalizeScopeValue(current.workspaceId);
168
- }
169
- if (tenantId && workspaceId) {
170
- break;
171
- }
172
- }
173
- return { tenantId, workspaceId };
174
- }
175
88
  async function resolveTopicProjectScope(ctx, args) {
176
89
  if (args.topicId) {
177
90
  return await resolveScopeFromTopicId(ctx, args.topicId);
178
91
  }
179
92
  if (args.projectId) {
180
- return await resolveScopeFromLegacyProjectId(ctx, args.projectId);
93
+ throw new Error(
94
+ "topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
95
+ );
181
96
  }
182
- throw new Error(
183
- "Missing scope: provide topicId (preferred) or legacy projectId alias."
184
- );
97
+ throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
185
98
  }
186
99
  async function resolveScopeFromTopicId(ctx, topicId) {
187
- const topic = await resolveTopicDocFromTopicId(ctx, topicId);
188
- if (topic) {
189
- return await buildTopicScope(ctx, topic, "topic");
190
- }
191
- const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
100
+ const topicGlobalId = String(topicId);
101
+ requireUuidV7TopicScope("topicId", topicGlobalId);
102
+ const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
192
103
  if (nodeScope) {
193
104
  return nodeScope;
194
105
  }
195
- throw new Error(`Topic not found: ${String(topicId)}`);
196
- }
197
- async function resolveTopicDocFromTopicId(ctx, topicId) {
198
- const direct = await tryReadTopicDoc(ctx, topicId, {
199
- failureLog: "[topicScope] Failed to load topic by direct id",
200
- idLogKey: "topicId"
201
- });
202
- if (direct) {
203
- return direct;
204
- }
205
- const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
206
- if (hostTopic) {
207
- return hostTopic;
208
- }
209
- return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
210
- }
211
- async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
212
- const directTopic = await resolveDirectLegacyProjectTopic(
213
- ctx,
214
- legacyProjectId
215
- );
216
- if (directTopic) {
217
- return await buildTopicScope(ctx, directTopic, "topic_inferred", {
218
- fallbackProjectId: legacyProjectId
219
- });
220
- }
221
- const primary = pickPrimaryTopic(
222
- await findTopicsByScopeAlias(ctx, legacyProjectId)
223
- );
224
- if (primary) {
225
- return await buildTopicScope(ctx, primary, "project_mapped_topic", {
226
- fallbackProjectId: legacyProjectId
227
- });
228
- }
229
- const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
230
- if (nodeScope) {
231
- return {
232
- ...nodeScope,
233
- projectId: nodeScope.projectId ?? legacyProjectId
234
- };
235
- }
236
- throw new Error(
237
- `Legacy project scope ${legacyProjectId} has no mapped topic.`
238
- );
239
- }
240
- async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
241
- const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
242
- failureLog: "[topicScope] Failed to load direct project topic",
243
- idLogKey: "projectId"
244
- });
245
- return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
246
- }
247
- async function tryReadTopicDoc(ctx, id, log) {
248
- try {
249
- return await ctx.db.get(id);
250
- } catch (error) {
251
- debugGraphPrimitiveFallback(log.failureLog, {
252
- error,
253
- [log.idLogKey]: id
254
- });
255
- return null;
256
- }
257
- }
258
- async function buildTopicScope(ctx, topic, source, options = {}) {
259
- const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
260
- const mapped = asMappedProjectId(topic);
261
- return {
262
- topicId: topic._id,
263
- ...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
264
- tenantId: inherited.tenantId,
265
- workspaceId: inherited.workspaceId,
266
- source
267
- };
106
+ throw new Error(`Topic not found: ${topicGlobalId}`);
268
107
  }
269
108
  var optionalScopeArgs = {
270
109
  projectId: v.optional(v.string()),
@@ -43,7 +43,7 @@ interface ResolvedTopicOntology {
43
43
  /** The published version, if any */
44
44
  publishedVersion: OntologyVer | null;
45
45
  source: "direct" | "inherited";
46
- sourceTopicId: Id<"topics">;
46
+ sourceTopicId: string;
47
47
  tier: string;
48
48
  /** Valid edge type values from the latest published version */
49
49
  validEdgeTypes: string[];
@@ -60,7 +60,7 @@ interface TopicOntologyCtx {
60
60
  * @param topicId - The topic to resolve the ontology for
61
61
  * @returns The resolved ontology or null if none is bound in the ancestor chain
62
62
  */
63
- declare function resolveTopicOntologyInternal(ctx: TopicOntologyCtx, topicId: Id<"topics">): Promise<ResolvedTopicOntology | null>;
63
+ declare function resolveTopicOntologyInternal(ctx: TopicOntologyCtx, topicId: string): Promise<ResolvedTopicOntology | null>;
64
64
  /**
65
65
  * Validate that a nodeType is allowed by the resolved ontology for a topic.
66
66
  *
@@ -71,7 +71,7 @@ declare function resolveTopicOntologyInternal(ctx: TopicOntologyCtx, topicId: Id
71
71
  * Returns { valid: false, error } if:
72
72
  * - The topic has a bound ontology and the nodeType is not in its entity types
73
73
  */
74
- declare function validateEntityTypeForTopic(ctx: TopicOntologyCtx, topicId: Id<"topics">, nodeType: string): Promise<{
74
+ declare function validateEntityTypeForTopic(ctx: TopicOntologyCtx, topicId: string, nodeType: string): Promise<{
75
75
  valid: true;
76
76
  } | {
77
77
  valid: false;
@@ -1,7 +1,50 @@
1
+ import { isUuidV7 } from '@lucern/contracts/ids';
2
+
3
+ // src/globalId.ts
4
+
1
5
  // src/topicOntologyResolver.ts
2
6
  var MAX_RESOLUTION_DEPTH = 10;
3
- async function loadTopic(ctx, topicId) {
4
- return await ctx.db.get(topicId);
7
+ async function loadTopic(ctx, topicGlobalId) {
8
+ requireUuidV7TopicGlobalId(topicGlobalId);
9
+ const topic = await ctx.db.query("epistemicNodes").withIndex("by_globalId", (q) => q.eq("globalId", topicGlobalId)).first();
10
+ return requireTopicGlobalId(topic);
11
+ }
12
+ function requireUuidV7TopicGlobalId(topicGlobalId) {
13
+ if (!isUuidV7(topicGlobalId)) {
14
+ throw new Error(
15
+ `topic.uuidv7_required: topic ontology resolution requires a UUIDv7 topic globalId, received ${topicGlobalId}.`
16
+ );
17
+ }
18
+ }
19
+ function requireTopicGlobalId(topic) {
20
+ if (!topic) {
21
+ return null;
22
+ }
23
+ if (topic.nodeType !== "topic") {
24
+ return null;
25
+ }
26
+ if (!isUuidV7(topic.globalId)) {
27
+ throw new Error(
28
+ `topic.uuidv7_required: topic node ${String(topic._id)} is missing a UUIDv7 globalId.`
29
+ );
30
+ }
31
+ return topic;
32
+ }
33
+ function metadataString(topic, field) {
34
+ const value = topic.metadata?.[field];
35
+ return typeof value === "string" && value.trim() ? value : null;
36
+ }
37
+ function topicOntologyId(topic) {
38
+ const ontologyId = metadataString(topic, "ontologyId");
39
+ return ontologyId ? ontologyId : null;
40
+ }
41
+ function parentTopicGlobalId(topic) {
42
+ const parent = metadataString(topic, "parentTopicGlobalId");
43
+ if (!parent) {
44
+ return null;
45
+ }
46
+ requireUuidV7TopicGlobalId(parent);
47
+ return parent;
5
48
  }
6
49
  async function loadOntologyDefinition(ctx, ontologyId) {
7
50
  return await ctx.db.get(ontologyId);
@@ -24,25 +67,20 @@ function resolvedTopicOntology(args) {
24
67
  };
25
68
  }
26
69
  async function resolveCurrentTopicOntology(args) {
27
- if (!args.current.ontologyId) {
70
+ const ontologyId = topicOntologyId(args.current);
71
+ if (!ontologyId) {
28
72
  return "continue";
29
73
  }
30
- const ontologyDef = await loadOntologyDefinition(
31
- args.ctx,
32
- args.current.ontologyId
33
- );
74
+ const ontologyDef = await loadOntologyDefinition(args.ctx, ontologyId);
34
75
  if (!ontologyDef || ontologyDef.status === "archived") {
35
- return args.current.parentTopicId ? "continue" : null;
76
+ return parentTopicGlobalId(args.current) ? "continue" : null;
36
77
  }
37
- const published = await loadPublishedOntologyVersions(
38
- args.ctx,
39
- args.current.ontologyId
40
- );
78
+ const published = await loadPublishedOntologyVersions(args.ctx, ontologyId);
41
79
  return resolvedTopicOntology({
42
80
  latestPublished: published[0] ?? null,
43
81
  ontologyDef,
44
- source: args.current._id === args.startTopicId ? "direct" : "inherited",
45
- sourceTopicId: args.current._id
82
+ source: args.current.globalId === args.startTopicGlobalId ? "direct" : "inherited",
83
+ sourceTopicId: args.current.globalId
46
84
  });
47
85
  }
48
86
  async function resolveTopicOntologyInternal(ctx, topicId) {
@@ -50,20 +88,21 @@ async function resolveTopicOntologyInternal(ctx, topicId) {
50
88
  if (!current) {
51
89
  return null;
52
90
  }
53
- const startTopicId = topicId;
91
+ const startTopicGlobalId = topicId;
54
92
  for (let i = 0; i < MAX_RESOLUTION_DEPTH && current; i++) {
55
93
  const resolved = await resolveCurrentTopicOntology({
56
94
  ctx,
57
95
  current,
58
- startTopicId
96
+ startTopicGlobalId
59
97
  });
60
98
  if (resolved !== "continue") {
61
99
  return resolved;
62
100
  }
63
- if (!current.parentTopicId) {
101
+ const parent = parentTopicGlobalId(current);
102
+ if (!parent) {
64
103
  break;
65
104
  }
66
- current = await loadTopic(ctx, current.parentTopicId);
105
+ current = await loadTopic(ctx, parent);
67
106
  }
68
107
  return null;
69
108
  }
@@ -20,7 +20,7 @@ interface TopicProjectScope {
20
20
  projectId?: string;
21
21
  source: "topic" | "project_mapped_topic" | "topic_inferred" | "topic_node";
22
22
  tenantId?: string;
23
- topicId: Id<"topics">;
23
+ topicId: string;
24
24
  workspaceId?: string;
25
25
  }
26
26
  interface MaterializedTopicNodeDoc {
@@ -32,7 +32,7 @@ declare function resolveTopicProjectScope(ctx: TopicScopeContext, args: {
32
32
  topicId?: Id<"topics"> | string;
33
33
  projectId?: string;
34
34
  }): Promise<TopicProjectScope>;
35
- /** Shared scope args for graph-primitive functions. topicId is canonical; projectId is a legacy alias. */
35
+ /** Shared scope args for graph-primitive functions. topicId is canonical UUIDv7; projectId is a refused legacy alias. */
36
36
  declare const optionalScopeArgs: {
37
37
  readonly projectId: convex_values.VString<string | undefined, "optional">;
38
38
  readonly topicId: convex_values.VString<string | undefined, "optional">;
@@ -1,5 +1,5 @@
1
1
  import 'convex/values';
2
2
  import './convex.js';
3
- export { T as TopicProjectScope, o as optionalScopeArgs, a as readMaterializedTopicTableId, r as resolveTopicProjectScope } from './topicScope-DJVa0mLa.js';
3
+ export { T as TopicProjectScope, o as optionalScopeArgs, a as readMaterializedTopicTableId, r as resolveTopicProjectScope } from './topicScope-CL1IVOmv.js';
4
4
  import '@lucern/access-control/convex';
5
5
  import '@lucern/contracts/convex/unsafeAnyApi';
@@ -1,13 +1,7 @@
1
1
  import { v } from 'convex/values';
2
- import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
3
- import { componentsGeneric } from 'convex/server';
2
+ import { isUuidV7 } from '@lucern/contracts/ids';
4
3
 
5
4
  // src/topicScope.ts
6
- var unsafeApi = unsafeConvexAnyApi(
7
- "graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
8
- );
9
- var api = unsafeApi;
10
- componentsGeneric();
11
5
 
12
6
  // src/debug.ts
13
7
  function isGraphPrimitiveDebugEnabled() {
@@ -42,16 +36,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
42
36
  if (!node) {
43
37
  return null;
44
38
  }
45
- const scopeKey = normalizeScopeValue(node.topicId) ?? normalizeScopeValue(node.globalId);
39
+ const scopeKey = canonicalTopicGlobalId(node);
46
40
  if (!scopeKey) {
47
- return null;
41
+ throw new Error(
42
+ `topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
43
+ );
48
44
  }
45
+ const metadata = node.metadata ?? {};
49
46
  return {
50
47
  topicId: scopeKey,
51
48
  projectId: asMappedProjectId(node),
52
- source: "topic_node"
49
+ source: "topic_node",
50
+ tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
51
+ workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
53
52
  };
54
53
  }
54
+ function canonicalTopicGlobalId(node) {
55
+ const globalId = normalizeScopeValue(node.globalId);
56
+ if (globalId && isUuidV7(globalId)) {
57
+ return globalId;
58
+ }
59
+ const topicId = normalizeScopeValue(node.topicId);
60
+ return topicId && isUuidV7(topicId) ? topicId : null;
61
+ }
62
+ function requireUuidV7TopicScope(field, value) {
63
+ if (!isUuidV7(value)) {
64
+ throw new Error(
65
+ `topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
66
+ );
67
+ }
68
+ }
55
69
  function asMappedProjectId(topic) {
56
70
  if (!topic) {
57
71
  return;
@@ -73,82 +87,6 @@ function normalizeScopeValue(value) {
73
87
  const normalized = value.trim();
74
88
  return normalized.length > 0 ? normalized : void 0;
75
89
  }
76
- function pickPrimaryTopic(candidates) {
77
- return [...candidates].sort((a, b) => {
78
- const depthA = a.depth ?? 9999;
79
- const depthB = b.depth ?? 9999;
80
- if (depthA !== depthB) {
81
- return depthA - depthB;
82
- }
83
- const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
84
- const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
85
- if (createdA !== createdB) {
86
- return createdA - createdB;
87
- }
88
- return String(a.name || "").localeCompare(String(b.name || ""));
89
- })[0];
90
- }
91
- async function findTopicsByScopeAlias(ctx, scopeId) {
92
- const query = ctx.db.query("topics");
93
- try {
94
- return await query.withIndex(
95
- "by_graph_scope_project",
96
- (q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
97
- ).collect();
98
- } catch (error) {
99
- debugGraphPrimitiveFallback(
100
- "[topicScope] Failed to resolve scope alias via index",
101
- {
102
- error,
103
- scopeId
104
- }
105
- );
106
- const topics = await query.collect();
107
- return topics.filter((topic) => {
108
- const normalizedGlobalId = normalizeScopeValue(topic.globalId);
109
- const mappedProjectId = asMappedProjectId(topic);
110
- return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
111
- });
112
- }
113
- }
114
- async function tryResolveHostTopicById(ctx, topicId) {
115
- if (typeof ctx.runQuery !== "function") {
116
- return null;
117
- }
118
- try {
119
- return await ctx.runQuery(api.topics.get, {
120
- id: topicId
121
- }) ?? null;
122
- } catch (error) {
123
- debugGraphPrimitiveFallback(
124
- "[topicScope] Failed to resolve topic by host query",
125
- {
126
- error,
127
- topicId
128
- }
129
- );
130
- return null;
131
- }
132
- }
133
- async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
134
- if (typeof ctx.runQuery !== "function") {
135
- return null;
136
- }
137
- try {
138
- return await ctx.runQuery(api.topics.getByLegacyScopeId, {
139
- projectId: legacyScopeId
140
- }) ?? null;
141
- } catch (error) {
142
- debugGraphPrimitiveFallback(
143
- "[topicScope] Failed to resolve topic by legacy scope",
144
- {
145
- error,
146
- legacyScopeId
147
- }
148
- );
149
- return null;
150
- }
151
- }
152
90
  function readMaterializedTopicTableId(topicNode) {
153
91
  if (!topicNode) {
154
92
  return;
@@ -157,124 +95,25 @@ function readMaterializedTopicTableId(topicNode) {
157
95
  const topicTableId = metadata.topicTableId || metadata.topicId;
158
96
  return typeof topicTableId === "string" && topicTableId.trim().length > 0 ? topicTableId.trim() : void 0;
159
97
  }
160
- async function resolveInheritedWorkspaceScope(ctx, topic) {
161
- const MAX_DEPTH = 10;
162
- let tenantId = normalizeScopeValue(topic.tenantId);
163
- let workspaceId = normalizeScopeValue(topic.workspaceId);
164
- if (tenantId && workspaceId) {
165
- return { tenantId, workspaceId };
166
- }
167
- let current = topic;
168
- for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
169
- current = await ctx.db.get(current.parentTopicId);
170
- if (!current) {
171
- break;
172
- }
173
- if (!tenantId) {
174
- tenantId = normalizeScopeValue(current.tenantId);
175
- }
176
- if (!workspaceId) {
177
- workspaceId = normalizeScopeValue(current.workspaceId);
178
- }
179
- if (tenantId && workspaceId) {
180
- break;
181
- }
182
- }
183
- return { tenantId, workspaceId };
184
- }
185
98
  async function resolveTopicProjectScope(ctx, args) {
186
99
  if (args.topicId) {
187
100
  return await resolveScopeFromTopicId(ctx, args.topicId);
188
101
  }
189
102
  if (args.projectId) {
190
- return await resolveScopeFromLegacyProjectId(ctx, args.projectId);
103
+ throw new Error(
104
+ "topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
105
+ );
191
106
  }
192
- throw new Error(
193
- "Missing scope: provide topicId (preferred) or legacy projectId alias."
194
- );
107
+ throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
195
108
  }
196
109
  async function resolveScopeFromTopicId(ctx, topicId) {
197
- const topic = await resolveTopicDocFromTopicId(ctx, topicId);
198
- if (topic) {
199
- return await buildTopicScope(ctx, topic, "topic");
200
- }
201
- const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
110
+ const topicGlobalId = String(topicId);
111
+ requireUuidV7TopicScope("topicId", topicGlobalId);
112
+ const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
202
113
  if (nodeScope) {
203
114
  return nodeScope;
204
115
  }
205
- throw new Error(`Topic not found: ${String(topicId)}`);
206
- }
207
- async function resolveTopicDocFromTopicId(ctx, topicId) {
208
- const direct = await tryReadTopicDoc(ctx, topicId, {
209
- failureLog: "[topicScope] Failed to load topic by direct id",
210
- idLogKey: "topicId"
211
- });
212
- if (direct) {
213
- return direct;
214
- }
215
- const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
216
- if (hostTopic) {
217
- return hostTopic;
218
- }
219
- return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
220
- }
221
- async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
222
- const directTopic = await resolveDirectLegacyProjectTopic(
223
- ctx,
224
- legacyProjectId
225
- );
226
- if (directTopic) {
227
- return await buildTopicScope(ctx, directTopic, "topic_inferred", {
228
- fallbackProjectId: legacyProjectId
229
- });
230
- }
231
- const primary = pickPrimaryTopic(
232
- await findTopicsByScopeAlias(ctx, legacyProjectId)
233
- );
234
- if (primary) {
235
- return await buildTopicScope(ctx, primary, "project_mapped_topic", {
236
- fallbackProjectId: legacyProjectId
237
- });
238
- }
239
- const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
240
- if (nodeScope) {
241
- return {
242
- ...nodeScope,
243
- projectId: nodeScope.projectId ?? legacyProjectId
244
- };
245
- }
246
- throw new Error(
247
- `Legacy project scope ${legacyProjectId} has no mapped topic.`
248
- );
249
- }
250
- async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
251
- const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
252
- failureLog: "[topicScope] Failed to load direct project topic",
253
- idLogKey: "projectId"
254
- });
255
- return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
256
- }
257
- async function tryReadTopicDoc(ctx, id, log) {
258
- try {
259
- return await ctx.db.get(id);
260
- } catch (error) {
261
- debugGraphPrimitiveFallback(log.failureLog, {
262
- error,
263
- [log.idLogKey]: id
264
- });
265
- return null;
266
- }
267
- }
268
- async function buildTopicScope(ctx, topic, source, options = {}) {
269
- const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
270
- const mapped = asMappedProjectId(topic);
271
- return {
272
- topicId: topic._id,
273
- ...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
274
- tenantId: inherited.tenantId,
275
- workspaceId: inherited.workspaceId,
276
- source
277
- };
116
+ throw new Error(`Topic not found: ${topicGlobalId}`);
278
117
  }
279
118
  var optionalScopeArgs = {
280
119
  projectId: v.optional(v.string()),