@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.
- package/dist/beliefDecay.js +34 -186
- package/dist/beliefEvidenceLinks.js +32 -187
- package/dist/contradictions.js +34 -187
- package/dist/entityLifecycle.js +88 -205
- package/dist/epistemicAnswers.js +35 -187
- package/dist/epistemicBeliefs.admin.js +34 -187
- package/dist/epistemicBeliefs.backfills.js +34 -188
- package/dist/epistemicBeliefs.confidence.d.ts +1 -1
- package/dist/epistemicBeliefs.confidence.js +32 -188
- package/dist/epistemicBeliefs.core.js +37 -187
- package/dist/epistemicBeliefs.d.ts +1 -1
- package/dist/epistemicBeliefs.helpers.d.ts +1 -1
- package/dist/epistemicBeliefs.helpers.js +34 -186
- package/dist/epistemicBeliefs.internal.js +37 -187
- package/dist/epistemicBeliefs.js +37 -187
- package/dist/epistemicBeliefs.lifecycle.js +32 -188
- package/dist/epistemicBeliefs.links.js +34 -188
- package/dist/epistemicContracts.evaluators.js +34 -188
- package/dist/epistemicContracts.handlers.js +34 -188
- package/dist/epistemicContracts.js +34 -188
- package/dist/epistemicEdges.d.ts +1 -1
- package/dist/epistemicEdges.helpers.d.ts +1 -1
- package/dist/epistemicEdges.js +32 -187
- package/dist/epistemicEdges.mutations.js +34 -186
- package/dist/epistemicEdges.queries.js +35 -188
- package/dist/epistemicEdges.types.d.ts +1 -1
- package/dist/epistemicEvidence.d.ts +1 -1
- package/dist/epistemicEvidence.js +37 -187
- package/dist/epistemicEvidenceHelpers.d.ts +1 -1
- package/dist/epistemicEvidenceHelpers.js +34 -186
- package/dist/epistemicEvidenceMutations.js +37 -187
- package/dist/epistemicEvidenceQueries.js +34 -186
- package/dist/epistemicHelpers.js +4 -1
- package/dist/epistemicInsert.js +4 -1
- package/dist/epistemicNodeCreation.js +4 -1
- package/dist/epistemicNodes.helpers.d.ts +1 -1
- package/dist/epistemicNodes.internal.js +35 -188
- package/dist/epistemicNodes.js +37 -188
- package/dist/epistemicNodes.mutations.js +35 -188
- package/dist/epistemicNodes.queries.js +35 -188
- package/dist/epistemicQuestions.conviction.js +34 -186
- package/dist/epistemicQuestions.create.js +37 -187
- package/dist/epistemicQuestions.d.ts +1 -1
- package/dist/epistemicQuestions.evidence.js +37 -187
- package/dist/epistemicQuestions.helpers.d.ts +1 -1
- package/dist/epistemicQuestions.helpers.js +34 -186
- package/dist/epistemicQuestions.js +37 -187
- package/dist/epistemicQuestions.lifecycle.js +34 -186
- package/dist/epistemicQuestions.queries.js +34 -186
- package/dist/epistemicQuestions.sprint.js +35 -188
- package/dist/epistemicQuestions.tail.js +37 -187
- package/dist/epistemicSources.js +35 -188
- package/dist/index.d.ts +1 -1
- package/dist/index.js +98 -213
- package/dist/proof-attestation.json +1 -1
- package/dist/questionEvidenceLinks.js +34 -187
- package/dist/scopeResolverCompat.d.ts +1 -1
- package/dist/scopeResolverCompat.js +32 -193
- package/dist/topicOntologyResolver.d.ts +3 -3
- package/dist/topicOntologyResolver.js +57 -18
- package/dist/{topicScope-DJVa0mLa.d.ts → topicScope-CL1IVOmv.d.ts} +2 -2
- package/dist/topicScope.d.ts +1 -1
- package/dist/topicScope.js +32 -193
- package/dist/workflowBridge.js +32 -193
- package/dist/workspaceIsolation.d.ts +1 -1
- package/dist/workspaceIsolation.js +32 -193
- package/package.json +4 -4
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { v } from 'convex/values';
|
|
2
|
-
import {
|
|
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 =
|
|
37
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
44
38
|
if (!scopeKey) {
|
|
45
|
-
|
|
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
|
-
|
|
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
|
|
188
|
-
|
|
189
|
-
|
|
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: ${
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
|
4
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
45
|
-
sourceTopicId: args.current.
|
|
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
|
|
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
|
-
|
|
96
|
+
startTopicGlobalId
|
|
59
97
|
});
|
|
60
98
|
if (resolved !== "continue") {
|
|
61
99
|
return resolved;
|
|
62
100
|
}
|
|
63
|
-
|
|
101
|
+
const parent = parentTopicGlobalId(current);
|
|
102
|
+
if (!parent) {
|
|
64
103
|
break;
|
|
65
104
|
}
|
|
66
|
-
current = await loadTopic(ctx,
|
|
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:
|
|
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">;
|
package/dist/topicScope.d.ts
CHANGED
|
@@ -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-
|
|
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';
|
package/dist/topicScope.js
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { v } from 'convex/values';
|
|
2
|
-
import {
|
|
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 =
|
|
39
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
46
40
|
if (!scopeKey) {
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
198
|
-
|
|
199
|
-
|
|
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: ${
|
|
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()),
|