@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
package/dist/contradictions.js
CHANGED
|
@@ -3,12 +3,12 @@ import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
|
3
3
|
import { v } from 'convex/values';
|
|
4
4
|
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
5
5
|
import { componentsGeneric, queryGeneric, mutationGeneric } from 'convex/server';
|
|
6
|
+
import { isUuidV7 } from '@lucern/contracts/ids';
|
|
6
7
|
|
|
7
8
|
// src/contradictions.ts
|
|
8
9
|
var unsafeApi = unsafeConvexAnyApi(
|
|
9
10
|
"graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
|
|
10
11
|
);
|
|
11
|
-
var api = unsafeApi;
|
|
12
12
|
componentsGeneric();
|
|
13
13
|
var internal = unsafeApi;
|
|
14
14
|
var mutation = mutationGeneric;
|
|
@@ -25,6 +25,8 @@ function debugGraphPrimitiveFallback(message, context) {
|
|
|
25
25
|
}
|
|
26
26
|
console.debug(message, context ?? {});
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
// src/topicScope.ts
|
|
28
30
|
var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
|
|
29
31
|
async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
30
32
|
if (!ctx?.db || typeof ctx.db.query !== "function") {
|
|
@@ -45,16 +47,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
45
47
|
if (!node) {
|
|
46
48
|
return null;
|
|
47
49
|
}
|
|
48
|
-
const scopeKey =
|
|
50
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
49
51
|
if (!scopeKey) {
|
|
50
|
-
|
|
52
|
+
throw new Error(
|
|
53
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
54
|
+
);
|
|
51
55
|
}
|
|
56
|
+
const metadata = node.metadata ?? {};
|
|
52
57
|
return {
|
|
53
58
|
topicId: scopeKey,
|
|
54
59
|
projectId: asMappedProjectId(node),
|
|
55
|
-
source: "topic_node"
|
|
60
|
+
source: "topic_node",
|
|
61
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
62
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
56
63
|
};
|
|
57
64
|
}
|
|
65
|
+
function canonicalTopicGlobalId(node) {
|
|
66
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
67
|
+
if (globalId && isUuidV7(globalId)) {
|
|
68
|
+
return globalId;
|
|
69
|
+
}
|
|
70
|
+
const topicId = normalizeScopeValue(node.topicId);
|
|
71
|
+
return topicId && isUuidV7(topicId) ? topicId : null;
|
|
72
|
+
}
|
|
73
|
+
function requireUuidV7TopicScope(field, value) {
|
|
74
|
+
if (!isUuidV7(value)) {
|
|
75
|
+
throw new Error(
|
|
76
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
58
80
|
function asMappedProjectId(topic) {
|
|
59
81
|
if (!topic) {
|
|
60
82
|
return;
|
|
@@ -76,200 +98,25 @@ function normalizeScopeValue(value) {
|
|
|
76
98
|
const normalized = value.trim();
|
|
77
99
|
return normalized.length > 0 ? normalized : void 0;
|
|
78
100
|
}
|
|
79
|
-
function pickPrimaryTopic(candidates) {
|
|
80
|
-
return [...candidates].sort((a, b) => {
|
|
81
|
-
const depthA = a.depth ?? 9999;
|
|
82
|
-
const depthB = b.depth ?? 9999;
|
|
83
|
-
if (depthA !== depthB) {
|
|
84
|
-
return depthA - depthB;
|
|
85
|
-
}
|
|
86
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
87
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
88
|
-
if (createdA !== createdB) {
|
|
89
|
-
return createdA - createdB;
|
|
90
|
-
}
|
|
91
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
92
|
-
})[0];
|
|
93
|
-
}
|
|
94
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
95
|
-
const query2 = ctx.db.query("topics");
|
|
96
|
-
try {
|
|
97
|
-
return await query2.withIndex(
|
|
98
|
-
"by_graph_scope_project",
|
|
99
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
|
|
100
|
-
).collect();
|
|
101
|
-
} catch (error) {
|
|
102
|
-
debugGraphPrimitiveFallback(
|
|
103
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
104
|
-
{
|
|
105
|
-
error,
|
|
106
|
-
scopeId
|
|
107
|
-
}
|
|
108
|
-
);
|
|
109
|
-
const topics = await query2.collect();
|
|
110
|
-
return topics.filter((topic) => {
|
|
111
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
112
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
113
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
118
|
-
if (typeof ctx.runQuery !== "function") {
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
try {
|
|
122
|
-
return await ctx.runQuery(api.topics.get, {
|
|
123
|
-
id: topicId
|
|
124
|
-
}) ?? null;
|
|
125
|
-
} catch (error) {
|
|
126
|
-
debugGraphPrimitiveFallback(
|
|
127
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
128
|
-
{
|
|
129
|
-
error,
|
|
130
|
-
topicId
|
|
131
|
-
}
|
|
132
|
-
);
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
137
|
-
if (typeof ctx.runQuery !== "function") {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
142
|
-
projectId: legacyScopeId
|
|
143
|
-
}) ?? null;
|
|
144
|
-
} catch (error) {
|
|
145
|
-
debugGraphPrimitiveFallback(
|
|
146
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
147
|
-
{
|
|
148
|
-
error,
|
|
149
|
-
legacyScopeId
|
|
150
|
-
}
|
|
151
|
-
);
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
156
|
-
const MAX_DEPTH = 10;
|
|
157
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
158
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
159
|
-
if (tenantId && workspaceId) {
|
|
160
|
-
return { tenantId, workspaceId };
|
|
161
|
-
}
|
|
162
|
-
let current = topic;
|
|
163
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
164
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
165
|
-
if (!current) {
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
if (!tenantId) {
|
|
169
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
170
|
-
}
|
|
171
|
-
if (!workspaceId) {
|
|
172
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
173
|
-
}
|
|
174
|
-
if (tenantId && workspaceId) {
|
|
175
|
-
break;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return { tenantId, workspaceId };
|
|
179
|
-
}
|
|
180
101
|
async function resolveTopicProjectScope(ctx, args) {
|
|
181
102
|
if (args.topicId) {
|
|
182
103
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
183
104
|
}
|
|
184
105
|
if (args.projectId) {
|
|
185
|
-
|
|
106
|
+
throw new Error(
|
|
107
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
108
|
+
);
|
|
186
109
|
}
|
|
187
|
-
throw new Error(
|
|
188
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
189
|
-
);
|
|
110
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
190
111
|
}
|
|
191
112
|
async function resolveScopeFromTopicId(ctx, topicId) {
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
}
|
|
196
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
|
|
113
|
+
const topicGlobalId = String(topicId);
|
|
114
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
115
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
197
116
|
if (nodeScope) {
|
|
198
117
|
return nodeScope;
|
|
199
118
|
}
|
|
200
|
-
throw new Error(`Topic not found: ${
|
|
201
|
-
}
|
|
202
|
-
async function resolveTopicDocFromTopicId(ctx, topicId) {
|
|
203
|
-
const direct = await tryReadTopicDoc(ctx, topicId, {
|
|
204
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
205
|
-
idLogKey: "topicId"
|
|
206
|
-
});
|
|
207
|
-
if (direct) {
|
|
208
|
-
return direct;
|
|
209
|
-
}
|
|
210
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
|
|
211
|
-
if (hostTopic) {
|
|
212
|
-
return hostTopic;
|
|
213
|
-
}
|
|
214
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
|
|
215
|
-
}
|
|
216
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
217
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
218
|
-
ctx,
|
|
219
|
-
legacyProjectId
|
|
220
|
-
);
|
|
221
|
-
if (directTopic) {
|
|
222
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
223
|
-
fallbackProjectId: legacyProjectId
|
|
224
|
-
});
|
|
225
|
-
}
|
|
226
|
-
const primary = pickPrimaryTopic(
|
|
227
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
228
|
-
);
|
|
229
|
-
if (primary) {
|
|
230
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
231
|
-
fallbackProjectId: legacyProjectId
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
235
|
-
if (nodeScope) {
|
|
236
|
-
return {
|
|
237
|
-
...nodeScope,
|
|
238
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
throw new Error(
|
|
242
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
246
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
247
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
248
|
-
idLogKey: "projectId"
|
|
249
|
-
});
|
|
250
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
251
|
-
}
|
|
252
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
253
|
-
try {
|
|
254
|
-
return await ctx.db.get(id);
|
|
255
|
-
} catch (error) {
|
|
256
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
257
|
-
error,
|
|
258
|
-
[log.idLogKey]: id
|
|
259
|
-
});
|
|
260
|
-
return null;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
264
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
265
|
-
const mapped = asMappedProjectId(topic);
|
|
266
|
-
return {
|
|
267
|
-
topicId: topic._id,
|
|
268
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
269
|
-
tenantId: inherited.tenantId,
|
|
270
|
-
workspaceId: inherited.workspaceId,
|
|
271
|
-
source
|
|
272
|
-
};
|
|
119
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
273
120
|
}
|
|
274
121
|
var optionalScopeArgs = {
|
|
275
122
|
projectId: v.optional(v.string()),
|
package/dist/entityLifecycle.js
CHANGED
|
@@ -5,7 +5,7 @@ import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
|
5
5
|
import { v } from 'convex/values';
|
|
6
6
|
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
7
7
|
import { componentsGeneric, queryGeneric, mutationGeneric } from 'convex/server';
|
|
8
|
-
import { generateGlobalId, assertUuidV7Identity, assertStorageEdgeVocabulary, assertUuidShapedEdgeEndpoint } from '@lucern/contracts/ids';
|
|
8
|
+
import { generateGlobalId, assertUuidV7Identity, assertUuidV7Reference, assertStorageEdgeVocabulary, assertUuidShapedEdgeEndpoint, isUuidV7 } from '@lucern/contracts/ids';
|
|
9
9
|
import { assertEdgePolicyAllowed } from '@lucern/contracts/manifests/edge-policy-manifest';
|
|
10
10
|
import { edgePolicyManifest } from '@lucern/contracts/manifests/edge-policy-manifest.data';
|
|
11
11
|
|
|
@@ -166,6 +166,9 @@ async function validateEntityMetadata(ctx, nodeType, metadata, tenantId) {
|
|
|
166
166
|
}
|
|
167
167
|
function insertEpistemicNode(ctx, doc) {
|
|
168
168
|
assertUuidV7Identity("epistemicNodes", doc.globalId);
|
|
169
|
+
if (doc.topicId !== void 0 && doc.topicId !== null) {
|
|
170
|
+
assertUuidV7Reference("epistemicNodes.topicId", doc.topicId);
|
|
171
|
+
}
|
|
169
172
|
return ctx.db.insert("epistemicNodes", doc);
|
|
170
173
|
}
|
|
171
174
|
async function assertExistingNodeEndpoint(ctx, endpointRole, endpoint) {
|
|
@@ -603,8 +606,47 @@ function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
|
603
606
|
|
|
604
607
|
// src/topicOntologyResolver.ts
|
|
605
608
|
var MAX_RESOLUTION_DEPTH = 10;
|
|
606
|
-
async function loadTopic(ctx,
|
|
607
|
-
|
|
609
|
+
async function loadTopic(ctx, topicGlobalId) {
|
|
610
|
+
requireUuidV7TopicGlobalId(topicGlobalId);
|
|
611
|
+
const topic = await ctx.db.query("epistemicNodes").withIndex("by_globalId", (q) => q.eq("globalId", topicGlobalId)).first();
|
|
612
|
+
return requireTopicGlobalId(topic);
|
|
613
|
+
}
|
|
614
|
+
function requireUuidV7TopicGlobalId(topicGlobalId) {
|
|
615
|
+
if (!isUuidV7(topicGlobalId)) {
|
|
616
|
+
throw new Error(
|
|
617
|
+
`topic.uuidv7_required: topic ontology resolution requires a UUIDv7 topic globalId, received ${topicGlobalId}.`
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
function requireTopicGlobalId(topic) {
|
|
622
|
+
if (!topic) {
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
625
|
+
if (topic.nodeType !== "topic") {
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
if (!isUuidV7(topic.globalId)) {
|
|
629
|
+
throw new Error(
|
|
630
|
+
`topic.uuidv7_required: topic node ${String(topic._id)} is missing a UUIDv7 globalId.`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
return topic;
|
|
634
|
+
}
|
|
635
|
+
function metadataString(topic, field) {
|
|
636
|
+
const value = topic.metadata?.[field];
|
|
637
|
+
return typeof value === "string" && value.trim() ? value : null;
|
|
638
|
+
}
|
|
639
|
+
function topicOntologyId(topic) {
|
|
640
|
+
const ontologyId = metadataString(topic, "ontologyId");
|
|
641
|
+
return ontologyId ? ontologyId : null;
|
|
642
|
+
}
|
|
643
|
+
function parentTopicGlobalId(topic) {
|
|
644
|
+
const parent = metadataString(topic, "parentTopicGlobalId");
|
|
645
|
+
if (!parent) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
requireUuidV7TopicGlobalId(parent);
|
|
649
|
+
return parent;
|
|
608
650
|
}
|
|
609
651
|
async function loadOntologyDefinition(ctx, ontologyId) {
|
|
610
652
|
return await ctx.db.get(ontologyId);
|
|
@@ -627,25 +669,20 @@ function resolvedTopicOntology(args) {
|
|
|
627
669
|
};
|
|
628
670
|
}
|
|
629
671
|
async function resolveCurrentTopicOntology(args) {
|
|
630
|
-
|
|
672
|
+
const ontologyId = topicOntologyId(args.current);
|
|
673
|
+
if (!ontologyId) {
|
|
631
674
|
return "continue";
|
|
632
675
|
}
|
|
633
|
-
const ontologyDef = await loadOntologyDefinition(
|
|
634
|
-
args.ctx,
|
|
635
|
-
args.current.ontologyId
|
|
636
|
-
);
|
|
676
|
+
const ontologyDef = await loadOntologyDefinition(args.ctx, ontologyId);
|
|
637
677
|
if (!ontologyDef || ontologyDef.status === "archived") {
|
|
638
|
-
return args.current
|
|
678
|
+
return parentTopicGlobalId(args.current) ? "continue" : null;
|
|
639
679
|
}
|
|
640
|
-
const published = await loadPublishedOntologyVersions(
|
|
641
|
-
args.ctx,
|
|
642
|
-
args.current.ontologyId
|
|
643
|
-
);
|
|
680
|
+
const published = await loadPublishedOntologyVersions(args.ctx, ontologyId);
|
|
644
681
|
return resolvedTopicOntology({
|
|
645
682
|
latestPublished: published[0] ?? null,
|
|
646
683
|
ontologyDef,
|
|
647
|
-
source: args.current.
|
|
648
|
-
sourceTopicId: args.current.
|
|
684
|
+
source: args.current.globalId === args.startTopicGlobalId ? "direct" : "inherited",
|
|
685
|
+
sourceTopicId: args.current.globalId
|
|
649
686
|
});
|
|
650
687
|
}
|
|
651
688
|
async function resolveTopicOntologyInternal(ctx, topicId) {
|
|
@@ -653,20 +690,21 @@ async function resolveTopicOntologyInternal(ctx, topicId) {
|
|
|
653
690
|
if (!current) {
|
|
654
691
|
return null;
|
|
655
692
|
}
|
|
656
|
-
const
|
|
693
|
+
const startTopicGlobalId = topicId;
|
|
657
694
|
for (let i = 0; i < MAX_RESOLUTION_DEPTH && current; i++) {
|
|
658
695
|
const resolved = await resolveCurrentTopicOntology({
|
|
659
696
|
ctx,
|
|
660
697
|
current,
|
|
661
|
-
|
|
698
|
+
startTopicGlobalId
|
|
662
699
|
});
|
|
663
700
|
if (resolved !== "continue") {
|
|
664
701
|
return resolved;
|
|
665
702
|
}
|
|
666
|
-
|
|
703
|
+
const parent = parentTopicGlobalId(current);
|
|
704
|
+
if (!parent) {
|
|
667
705
|
break;
|
|
668
706
|
}
|
|
669
|
-
current = await loadTopic(ctx,
|
|
707
|
+
current = await loadTopic(ctx, parent);
|
|
670
708
|
}
|
|
671
709
|
return null;
|
|
672
710
|
}
|
|
@@ -706,16 +744,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
706
744
|
if (!node) {
|
|
707
745
|
return null;
|
|
708
746
|
}
|
|
709
|
-
const scopeKey =
|
|
747
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
710
748
|
if (!scopeKey) {
|
|
711
|
-
|
|
749
|
+
throw new Error(
|
|
750
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
751
|
+
);
|
|
712
752
|
}
|
|
753
|
+
const metadata = node.metadata ?? {};
|
|
713
754
|
return {
|
|
714
755
|
topicId: scopeKey,
|
|
715
756
|
projectId: asMappedProjectId(node),
|
|
716
|
-
source: "topic_node"
|
|
757
|
+
source: "topic_node",
|
|
758
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
759
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
717
760
|
};
|
|
718
761
|
}
|
|
762
|
+
function canonicalTopicGlobalId(node) {
|
|
763
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
764
|
+
if (globalId && isUuidV7(globalId)) {
|
|
765
|
+
return globalId;
|
|
766
|
+
}
|
|
767
|
+
const topicId = normalizeScopeValue(node.topicId);
|
|
768
|
+
return topicId && isUuidV7(topicId) ? topicId : null;
|
|
769
|
+
}
|
|
770
|
+
function requireUuidV7TopicScope(field, value) {
|
|
771
|
+
if (!isUuidV7(value)) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
719
777
|
function asMappedProjectId(topic) {
|
|
720
778
|
if (!topic) {
|
|
721
779
|
return;
|
|
@@ -737,200 +795,25 @@ function normalizeScopeValue(value) {
|
|
|
737
795
|
const normalized = value.trim();
|
|
738
796
|
return normalized.length > 0 ? normalized : void 0;
|
|
739
797
|
}
|
|
740
|
-
function pickPrimaryTopic(candidates) {
|
|
741
|
-
return [...candidates].sort((a, b) => {
|
|
742
|
-
const depthA = a.depth ?? 9999;
|
|
743
|
-
const depthB = b.depth ?? 9999;
|
|
744
|
-
if (depthA !== depthB) {
|
|
745
|
-
return depthA - depthB;
|
|
746
|
-
}
|
|
747
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
748
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
749
|
-
if (createdA !== createdB) {
|
|
750
|
-
return createdA - createdB;
|
|
751
|
-
}
|
|
752
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
753
|
-
})[0];
|
|
754
|
-
}
|
|
755
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
756
|
-
const query2 = ctx.db.query("topics");
|
|
757
|
-
try {
|
|
758
|
-
return await query2.withIndex(
|
|
759
|
-
"by_graph_scope_project",
|
|
760
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
761
|
-
).collect();
|
|
762
|
-
} catch (error) {
|
|
763
|
-
debugGraphPrimitiveFallback(
|
|
764
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
765
|
-
{
|
|
766
|
-
error,
|
|
767
|
-
scopeId
|
|
768
|
-
}
|
|
769
|
-
);
|
|
770
|
-
const topics = await query2.collect();
|
|
771
|
-
return topics.filter((topic) => {
|
|
772
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
773
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
774
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
779
|
-
if (typeof ctx.runQuery !== "function") {
|
|
780
|
-
return null;
|
|
781
|
-
}
|
|
782
|
-
try {
|
|
783
|
-
return await ctx.runQuery(api.topics.get, {
|
|
784
|
-
id: topicId
|
|
785
|
-
}) ?? null;
|
|
786
|
-
} catch (error) {
|
|
787
|
-
debugGraphPrimitiveFallback(
|
|
788
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
789
|
-
{
|
|
790
|
-
error,
|
|
791
|
-
topicId
|
|
792
|
-
}
|
|
793
|
-
);
|
|
794
|
-
return null;
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
798
|
-
if (typeof ctx.runQuery !== "function") {
|
|
799
|
-
return null;
|
|
800
|
-
}
|
|
801
|
-
try {
|
|
802
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
803
|
-
projectId: legacyScopeId
|
|
804
|
-
}) ?? null;
|
|
805
|
-
} catch (error) {
|
|
806
|
-
debugGraphPrimitiveFallback(
|
|
807
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
808
|
-
{
|
|
809
|
-
error,
|
|
810
|
-
legacyScopeId
|
|
811
|
-
}
|
|
812
|
-
);
|
|
813
|
-
return null;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
817
|
-
const MAX_DEPTH = 10;
|
|
818
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
819
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
820
|
-
if (tenantId && workspaceId) {
|
|
821
|
-
return { tenantId, workspaceId };
|
|
822
|
-
}
|
|
823
|
-
let current = topic;
|
|
824
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
825
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
826
|
-
if (!current) {
|
|
827
|
-
break;
|
|
828
|
-
}
|
|
829
|
-
if (!tenantId) {
|
|
830
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
831
|
-
}
|
|
832
|
-
if (!workspaceId) {
|
|
833
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
834
|
-
}
|
|
835
|
-
if (tenantId && workspaceId) {
|
|
836
|
-
break;
|
|
837
|
-
}
|
|
838
|
-
}
|
|
839
|
-
return { tenantId, workspaceId };
|
|
840
|
-
}
|
|
841
798
|
async function resolveTopicProjectScope(ctx, args) {
|
|
842
799
|
if (args.topicId) {
|
|
843
800
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
844
801
|
}
|
|
845
802
|
if (args.projectId) {
|
|
846
|
-
|
|
803
|
+
throw new Error(
|
|
804
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
805
|
+
);
|
|
847
806
|
}
|
|
848
|
-
throw new Error(
|
|
849
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
850
|
-
);
|
|
807
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
851
808
|
}
|
|
852
809
|
async function resolveScopeFromTopicId(ctx, topicId) {
|
|
853
|
-
const
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
}
|
|
857
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
|
|
810
|
+
const topicGlobalId = String(topicId);
|
|
811
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
812
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
858
813
|
if (nodeScope) {
|
|
859
814
|
return nodeScope;
|
|
860
815
|
}
|
|
861
|
-
throw new Error(`Topic not found: ${
|
|
862
|
-
}
|
|
863
|
-
async function resolveTopicDocFromTopicId(ctx, topicId) {
|
|
864
|
-
const direct = await tryReadTopicDoc(ctx, topicId, {
|
|
865
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
866
|
-
idLogKey: "topicId"
|
|
867
|
-
});
|
|
868
|
-
if (direct) {
|
|
869
|
-
return direct;
|
|
870
|
-
}
|
|
871
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
|
|
872
|
-
if (hostTopic) {
|
|
873
|
-
return hostTopic;
|
|
874
|
-
}
|
|
875
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
|
|
876
|
-
}
|
|
877
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
878
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
879
|
-
ctx,
|
|
880
|
-
legacyProjectId
|
|
881
|
-
);
|
|
882
|
-
if (directTopic) {
|
|
883
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
884
|
-
fallbackProjectId: legacyProjectId
|
|
885
|
-
});
|
|
886
|
-
}
|
|
887
|
-
const primary = pickPrimaryTopic(
|
|
888
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
889
|
-
);
|
|
890
|
-
if (primary) {
|
|
891
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
892
|
-
fallbackProjectId: legacyProjectId
|
|
893
|
-
});
|
|
894
|
-
}
|
|
895
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
896
|
-
if (nodeScope) {
|
|
897
|
-
return {
|
|
898
|
-
...nodeScope,
|
|
899
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
throw new Error(
|
|
903
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
904
|
-
);
|
|
905
|
-
}
|
|
906
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
907
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
908
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
909
|
-
idLogKey: "projectId"
|
|
910
|
-
});
|
|
911
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
912
|
-
}
|
|
913
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
914
|
-
try {
|
|
915
|
-
return await ctx.db.get(id);
|
|
916
|
-
} catch (error) {
|
|
917
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
918
|
-
error,
|
|
919
|
-
[log.idLogKey]: id
|
|
920
|
-
});
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
925
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
926
|
-
const mapped = asMappedProjectId(topic);
|
|
927
|
-
return {
|
|
928
|
-
topicId: topic._id,
|
|
929
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
930
|
-
tenantId: inherited.tenantId,
|
|
931
|
-
workspaceId: inherited.workspaceId,
|
|
932
|
-
source
|
|
933
|
-
};
|
|
816
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
934
817
|
}
|
|
935
818
|
var optionalScopeArgs = {
|
|
936
819
|
projectId: v.optional(v.string()),
|