@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,6 @@
|
|
|
1
1
|
import { checkScopeAccess } from '@lucern/access-control/access';
|
|
2
2
|
import { normalizeTupleContradictionPolicy, readOpinionFromRecord, mkOpinion, confidenceFromSL } from '@lucern/confidence';
|
|
3
|
-
import { generateUuidV7 } from '@lucern/contracts/ids';
|
|
3
|
+
import { generateUuidV7, isUuidV7 } from '@lucern/contracts/ids';
|
|
4
4
|
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
5
5
|
import { v } from 'convex/values';
|
|
6
6
|
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
@@ -14,7 +14,6 @@ import '@lucern/contracts/schema-helpers/spine/tables/epistemicNodes';
|
|
|
14
14
|
var unsafeApi = unsafeConvexAnyApi(
|
|
15
15
|
"graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
|
|
16
16
|
);
|
|
17
|
-
var api = unsafeApi;
|
|
18
17
|
componentsGeneric();
|
|
19
18
|
var internal = unsafeApi;
|
|
20
19
|
var internalMutation = internalMutationGeneric;
|
|
@@ -143,6 +142,8 @@ function resolveBeliefLifecycleStatus(opts) {
|
|
|
143
142
|
function isPreValidationBeliefStatus(status) {
|
|
144
143
|
return status === "assumption" || status === "hypothesis";
|
|
145
144
|
}
|
|
145
|
+
|
|
146
|
+
// src/topicScope.ts
|
|
146
147
|
var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
|
|
147
148
|
async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
148
149
|
if (!ctx?.db || typeof ctx.db.query !== "function") {
|
|
@@ -163,16 +164,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
163
164
|
if (!node) {
|
|
164
165
|
return null;
|
|
165
166
|
}
|
|
166
|
-
const scopeKey =
|
|
167
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
167
168
|
if (!scopeKey) {
|
|
168
|
-
|
|
169
|
+
throw new Error(
|
|
170
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
171
|
+
);
|
|
169
172
|
}
|
|
173
|
+
const metadata = node.metadata ?? {};
|
|
170
174
|
return {
|
|
171
175
|
topicId: scopeKey,
|
|
172
176
|
projectId: asMappedProjectId(node),
|
|
173
|
-
source: "topic_node"
|
|
177
|
+
source: "topic_node",
|
|
178
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
179
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
174
180
|
};
|
|
175
181
|
}
|
|
182
|
+
function canonicalTopicGlobalId(node) {
|
|
183
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
184
|
+
if (globalId && isUuidV7(globalId)) {
|
|
185
|
+
return globalId;
|
|
186
|
+
}
|
|
187
|
+
const topicId2 = normalizeScopeValue(node.topicId);
|
|
188
|
+
return topicId2 && isUuidV7(topicId2) ? topicId2 : null;
|
|
189
|
+
}
|
|
190
|
+
function requireUuidV7TopicScope(field, value) {
|
|
191
|
+
if (!isUuidV7(value)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
176
197
|
function asMappedProjectId(topic) {
|
|
177
198
|
if (!topic) {
|
|
178
199
|
return;
|
|
@@ -194,200 +215,25 @@ function normalizeScopeValue(value) {
|
|
|
194
215
|
const normalized = value.trim();
|
|
195
216
|
return normalized.length > 0 ? normalized : void 0;
|
|
196
217
|
}
|
|
197
|
-
function pickPrimaryTopic(candidates) {
|
|
198
|
-
return [...candidates].sort((a, b) => {
|
|
199
|
-
const depthA = a.depth ?? 9999;
|
|
200
|
-
const depthB = b.depth ?? 9999;
|
|
201
|
-
if (depthA !== depthB) {
|
|
202
|
-
return depthA - depthB;
|
|
203
|
-
}
|
|
204
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
205
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
206
|
-
if (createdA !== createdB) {
|
|
207
|
-
return createdA - createdB;
|
|
208
|
-
}
|
|
209
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
210
|
-
})[0];
|
|
211
|
-
}
|
|
212
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
213
|
-
const query2 = ctx.db.query("topics");
|
|
214
|
-
try {
|
|
215
|
-
return await query2.withIndex(
|
|
216
|
-
"by_graph_scope_project",
|
|
217
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
|
|
218
|
-
).collect();
|
|
219
|
-
} catch (error) {
|
|
220
|
-
debugGraphPrimitiveFallback(
|
|
221
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
222
|
-
{
|
|
223
|
-
error,
|
|
224
|
-
scopeId
|
|
225
|
-
}
|
|
226
|
-
);
|
|
227
|
-
const topics = await query2.collect();
|
|
228
|
-
return topics.filter((topic) => {
|
|
229
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
230
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
231
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
async function tryResolveHostTopicById(ctx, topicId2) {
|
|
236
|
-
if (typeof ctx.runQuery !== "function") {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
try {
|
|
240
|
-
return await ctx.runQuery(api.topics.get, {
|
|
241
|
-
id: topicId2
|
|
242
|
-
}) ?? null;
|
|
243
|
-
} catch (error) {
|
|
244
|
-
debugGraphPrimitiveFallback(
|
|
245
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
246
|
-
{
|
|
247
|
-
error,
|
|
248
|
-
topicId: topicId2
|
|
249
|
-
}
|
|
250
|
-
);
|
|
251
|
-
return null;
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
255
|
-
if (typeof ctx.runQuery !== "function") {
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
260
|
-
projectId: legacyScopeId
|
|
261
|
-
}) ?? null;
|
|
262
|
-
} catch (error) {
|
|
263
|
-
debugGraphPrimitiveFallback(
|
|
264
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
265
|
-
{
|
|
266
|
-
error,
|
|
267
|
-
legacyScopeId
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
return null;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
274
|
-
const MAX_DEPTH = 10;
|
|
275
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
276
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
277
|
-
if (tenantId && workspaceId) {
|
|
278
|
-
return { tenantId, workspaceId };
|
|
279
|
-
}
|
|
280
|
-
let current = topic;
|
|
281
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
282
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
283
|
-
if (!current) {
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
if (!tenantId) {
|
|
287
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
288
|
-
}
|
|
289
|
-
if (!workspaceId) {
|
|
290
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
291
|
-
}
|
|
292
|
-
if (tenantId && workspaceId) {
|
|
293
|
-
break;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return { tenantId, workspaceId };
|
|
297
|
-
}
|
|
298
218
|
async function resolveTopicProjectScope(ctx, args) {
|
|
299
219
|
if (args.topicId) {
|
|
300
220
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
301
221
|
}
|
|
302
222
|
if (args.projectId) {
|
|
303
|
-
|
|
223
|
+
throw new Error(
|
|
224
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
225
|
+
);
|
|
304
226
|
}
|
|
305
|
-
throw new Error(
|
|
306
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
307
|
-
);
|
|
227
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
308
228
|
}
|
|
309
229
|
async function resolveScopeFromTopicId(ctx, topicId2) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
314
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId2));
|
|
230
|
+
const topicGlobalId = String(topicId2);
|
|
231
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
232
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
315
233
|
if (nodeScope) {
|
|
316
234
|
return nodeScope;
|
|
317
235
|
}
|
|
318
|
-
throw new Error(`Topic not found: ${
|
|
319
|
-
}
|
|
320
|
-
async function resolveTopicDocFromTopicId(ctx, topicId2) {
|
|
321
|
-
const direct = await tryReadTopicDoc(ctx, topicId2, {
|
|
322
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
323
|
-
idLogKey: "topicId"
|
|
324
|
-
});
|
|
325
|
-
if (direct) {
|
|
326
|
-
return direct;
|
|
327
|
-
}
|
|
328
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId2));
|
|
329
|
-
if (hostTopic) {
|
|
330
|
-
return hostTopic;
|
|
331
|
-
}
|
|
332
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId2))) ?? null;
|
|
333
|
-
}
|
|
334
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
335
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
336
|
-
ctx,
|
|
337
|
-
legacyProjectId
|
|
338
|
-
);
|
|
339
|
-
if (directTopic) {
|
|
340
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
341
|
-
fallbackProjectId: legacyProjectId
|
|
342
|
-
});
|
|
343
|
-
}
|
|
344
|
-
const primary = pickPrimaryTopic(
|
|
345
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
346
|
-
);
|
|
347
|
-
if (primary) {
|
|
348
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
349
|
-
fallbackProjectId: legacyProjectId
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
353
|
-
if (nodeScope) {
|
|
354
|
-
return {
|
|
355
|
-
...nodeScope,
|
|
356
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
throw new Error(
|
|
360
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
361
|
-
);
|
|
362
|
-
}
|
|
363
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
364
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
365
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
366
|
-
idLogKey: "projectId"
|
|
367
|
-
});
|
|
368
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
369
|
-
}
|
|
370
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
371
|
-
try {
|
|
372
|
-
return await ctx.db.get(id);
|
|
373
|
-
} catch (error) {
|
|
374
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
375
|
-
error,
|
|
376
|
-
[log.idLogKey]: id
|
|
377
|
-
});
|
|
378
|
-
return null;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
382
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
383
|
-
const mapped = asMappedProjectId(topic);
|
|
384
|
-
return {
|
|
385
|
-
topicId: topic._id,
|
|
386
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
387
|
-
tenantId: inherited.tenantId,
|
|
388
|
-
workspaceId: inherited.workspaceId,
|
|
389
|
-
source
|
|
390
|
-
};
|
|
236
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
391
237
|
}
|
|
392
238
|
var optionalScopeArgs = {
|
|
393
239
|
projectId: v.optional(v.string()),
|
|
@@ -4,7 +4,7 @@ import { BeliefConfidenceTrigger } from './epistemicBeliefs.helpers.js';
|
|
|
4
4
|
import '@lucern/access-control/convex';
|
|
5
5
|
import '@lucern/contracts/convex/unsafeAnyApi';
|
|
6
6
|
import 'convex/values';
|
|
7
|
-
import './topicScope-
|
|
7
|
+
import './topicScope-CL1IVOmv.js';
|
|
8
8
|
import './beliefLifecycle-CXwdDw5e.js';
|
|
9
9
|
import '@lucern/access-control/structuredMutationError';
|
|
10
10
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { requireScopeWriteAccess } from '@lucern/access-control/access';
|
|
2
2
|
import { normalizeTupleContradictionPolicy, mkOpinion, conditionalDeduction, project, dampedDependencyCascade, trustDiscount, applyNegativeSupport, cumulativeFusion, applyNegativeEvidence, confidenceFromSL, detectTupleContradiction, evaluateTupleContradictionTransition, readOpinionFromRecord, hasProjectedOpinionChanged } from '@lucern/confidence';
|
|
3
|
-
import { generateUuidV7 } from '@lucern/contracts/ids';
|
|
3
|
+
import { generateUuidV7, isUuidV7 } from '@lucern/contracts/ids';
|
|
4
4
|
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
5
5
|
import { v } from 'convex/values';
|
|
6
6
|
import '@lucern/contracts/schema-helpers/spine/tables/epistemicNodes';
|
|
@@ -421,7 +421,6 @@ function getTraversalDirections(direction) {
|
|
|
421
421
|
var unsafeApi = unsafeConvexAnyApi(
|
|
422
422
|
"graph-primitives top-level module bundle lacks a committed Convex _generated/api surface"
|
|
423
423
|
);
|
|
424
|
-
var api = unsafeApi;
|
|
425
424
|
componentsGeneric();
|
|
426
425
|
var internal = unsafeApi;
|
|
427
426
|
var internalMutation = internalMutationGeneric;
|
|
@@ -459,16 +458,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
459
458
|
if (!node) {
|
|
460
459
|
return null;
|
|
461
460
|
}
|
|
462
|
-
const scopeKey =
|
|
461
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
463
462
|
if (!scopeKey) {
|
|
464
|
-
|
|
463
|
+
throw new Error(
|
|
464
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
465
|
+
);
|
|
465
466
|
}
|
|
467
|
+
const metadata = node.metadata ?? {};
|
|
466
468
|
return {
|
|
467
469
|
topicId: scopeKey,
|
|
468
470
|
projectId: asMappedProjectId(node),
|
|
469
|
-
source: "topic_node"
|
|
471
|
+
source: "topic_node",
|
|
472
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
473
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
470
474
|
};
|
|
471
475
|
}
|
|
476
|
+
function canonicalTopicGlobalId(node) {
|
|
477
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
478
|
+
if (globalId && isUuidV7(globalId)) {
|
|
479
|
+
return globalId;
|
|
480
|
+
}
|
|
481
|
+
const topicId = normalizeScopeValue(node.topicId);
|
|
482
|
+
return topicId && isUuidV7(topicId) ? topicId : null;
|
|
483
|
+
}
|
|
484
|
+
function requireUuidV7TopicScope(field, value) {
|
|
485
|
+
if (!isUuidV7(value)) {
|
|
486
|
+
throw new Error(
|
|
487
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
472
491
|
function asMappedProjectId(topic) {
|
|
473
492
|
if (!topic) {
|
|
474
493
|
return;
|
|
@@ -490,200 +509,25 @@ function normalizeScopeValue(value) {
|
|
|
490
509
|
const normalized = value.trim();
|
|
491
510
|
return normalized.length > 0 ? normalized : void 0;
|
|
492
511
|
}
|
|
493
|
-
function pickPrimaryTopic(candidates) {
|
|
494
|
-
return [...candidates].sort((a, b) => {
|
|
495
|
-
const depthA = a.depth ?? 9999;
|
|
496
|
-
const depthB = b.depth ?? 9999;
|
|
497
|
-
if (depthA !== depthB) {
|
|
498
|
-
return depthA - depthB;
|
|
499
|
-
}
|
|
500
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
501
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
502
|
-
if (createdA !== createdB) {
|
|
503
|
-
return createdA - createdB;
|
|
504
|
-
}
|
|
505
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
506
|
-
})[0];
|
|
507
|
-
}
|
|
508
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
509
|
-
const query = ctx.db.query("topics");
|
|
510
|
-
try {
|
|
511
|
-
return await query.withIndex(
|
|
512
|
-
"by_graph_scope_project",
|
|
513
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD, scopeId)
|
|
514
|
-
).collect();
|
|
515
|
-
} catch (error) {
|
|
516
|
-
debugGraphPrimitiveFallback(
|
|
517
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
518
|
-
{
|
|
519
|
-
error,
|
|
520
|
-
scopeId
|
|
521
|
-
}
|
|
522
|
-
);
|
|
523
|
-
const topics = await query.collect();
|
|
524
|
-
return topics.filter((topic) => {
|
|
525
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
526
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
527
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
528
|
-
});
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
532
|
-
if (typeof ctx.runQuery !== "function") {
|
|
533
|
-
return null;
|
|
534
|
-
}
|
|
535
|
-
try {
|
|
536
|
-
return await ctx.runQuery(api.topics.get, {
|
|
537
|
-
id: topicId
|
|
538
|
-
}) ?? null;
|
|
539
|
-
} catch (error) {
|
|
540
|
-
debugGraphPrimitiveFallback(
|
|
541
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
542
|
-
{
|
|
543
|
-
error,
|
|
544
|
-
topicId
|
|
545
|
-
}
|
|
546
|
-
);
|
|
547
|
-
return null;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
551
|
-
if (typeof ctx.runQuery !== "function") {
|
|
552
|
-
return null;
|
|
553
|
-
}
|
|
554
|
-
try {
|
|
555
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
556
|
-
projectId: legacyScopeId
|
|
557
|
-
}) ?? null;
|
|
558
|
-
} catch (error) {
|
|
559
|
-
debugGraphPrimitiveFallback(
|
|
560
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
561
|
-
{
|
|
562
|
-
error,
|
|
563
|
-
legacyScopeId
|
|
564
|
-
}
|
|
565
|
-
);
|
|
566
|
-
return null;
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
570
|
-
const MAX_DEPTH = 10;
|
|
571
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
572
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
573
|
-
if (tenantId && workspaceId) {
|
|
574
|
-
return { tenantId, workspaceId };
|
|
575
|
-
}
|
|
576
|
-
let current = topic;
|
|
577
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
578
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
579
|
-
if (!current) {
|
|
580
|
-
break;
|
|
581
|
-
}
|
|
582
|
-
if (!tenantId) {
|
|
583
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
584
|
-
}
|
|
585
|
-
if (!workspaceId) {
|
|
586
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
587
|
-
}
|
|
588
|
-
if (tenantId && workspaceId) {
|
|
589
|
-
break;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
return { tenantId, workspaceId };
|
|
593
|
-
}
|
|
594
512
|
async function resolveTopicProjectScope(ctx, args) {
|
|
595
513
|
if (args.topicId) {
|
|
596
514
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
597
515
|
}
|
|
598
516
|
if (args.projectId) {
|
|
599
|
-
|
|
517
|
+
throw new Error(
|
|
518
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
519
|
+
);
|
|
600
520
|
}
|
|
601
|
-
throw new Error(
|
|
602
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
603
|
-
);
|
|
521
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
604
522
|
}
|
|
605
523
|
async function resolveScopeFromTopicId(ctx, topicId) {
|
|
606
|
-
const
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
|
|
524
|
+
const topicGlobalId = String(topicId);
|
|
525
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
526
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
611
527
|
if (nodeScope) {
|
|
612
528
|
return nodeScope;
|
|
613
529
|
}
|
|
614
|
-
throw new Error(`Topic not found: ${
|
|
615
|
-
}
|
|
616
|
-
async function resolveTopicDocFromTopicId(ctx, topicId) {
|
|
617
|
-
const direct = await tryReadTopicDoc(ctx, topicId, {
|
|
618
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
619
|
-
idLogKey: "topicId"
|
|
620
|
-
});
|
|
621
|
-
if (direct) {
|
|
622
|
-
return direct;
|
|
623
|
-
}
|
|
624
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
|
|
625
|
-
if (hostTopic) {
|
|
626
|
-
return hostTopic;
|
|
627
|
-
}
|
|
628
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
|
|
629
|
-
}
|
|
630
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
631
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
632
|
-
ctx,
|
|
633
|
-
legacyProjectId
|
|
634
|
-
);
|
|
635
|
-
if (directTopic) {
|
|
636
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
637
|
-
fallbackProjectId: legacyProjectId
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
const primary = pickPrimaryTopic(
|
|
641
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
642
|
-
);
|
|
643
|
-
if (primary) {
|
|
644
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
645
|
-
fallbackProjectId: legacyProjectId
|
|
646
|
-
});
|
|
647
|
-
}
|
|
648
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
649
|
-
if (nodeScope) {
|
|
650
|
-
return {
|
|
651
|
-
...nodeScope,
|
|
652
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
throw new Error(
|
|
656
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
657
|
-
);
|
|
658
|
-
}
|
|
659
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
660
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
661
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
662
|
-
idLogKey: "projectId"
|
|
663
|
-
});
|
|
664
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
665
|
-
}
|
|
666
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
667
|
-
try {
|
|
668
|
-
return await ctx.db.get(id);
|
|
669
|
-
} catch (error) {
|
|
670
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
671
|
-
error,
|
|
672
|
-
[log.idLogKey]: id
|
|
673
|
-
});
|
|
674
|
-
return null;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
678
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
679
|
-
const mapped = asMappedProjectId(topic);
|
|
680
|
-
return {
|
|
681
|
-
topicId: topic._id,
|
|
682
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
683
|
-
tenantId: inherited.tenantId,
|
|
684
|
-
workspaceId: inherited.workspaceId,
|
|
685
|
-
source
|
|
686
|
-
};
|
|
530
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
687
531
|
}
|
|
688
532
|
({
|
|
689
533
|
projectId: v.optional(v.string()),
|