@lucern/graph-primitives 1.0.50 → 1.0.53
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
|
@@ -2,10 +2,10 @@ import { canAudienceClassAccess, normalizeAudienceKey, classFromAudienceKey } fr
|
|
|
2
2
|
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
3
3
|
import { componentsGeneric, mutationGeneric, internalMutationGeneric, queryGeneric, internalQueryGeneric } from 'convex/server';
|
|
4
4
|
import { v } from 'convex/values';
|
|
5
|
+
import { generateGlobalId, assertUuidV7Identity, assertUuidV7Reference, assertStorageEdgeVocabulary, assertUuidShapedEdgeEndpoint, isUuidV7 } from '@lucern/contracts/ids';
|
|
5
6
|
import { requireScopeWriteAccess, checkScopeAccess } from '@lucern/access-control/access';
|
|
6
7
|
import { EVIDENCE_SOURCE_QUALITY_VALUES, EVIDENCE_METHODOLOGY_VALUES, EVIDENCE_INFORMATION_ASYMMETRY_VALUES } from '@lucern/contracts/evidence-taxonomy-target-scope.contract';
|
|
7
8
|
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
8
|
-
import { generateGlobalId, assertUuidV7Identity, assertStorageEdgeVocabulary, assertUuidShapedEdgeEndpoint } 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
|
import { isNodeType, getLayerForNodeType } from '@lucern/contracts/schema-helpers/spine/tables/epistemicNodes';
|
|
@@ -409,6 +409,8 @@ function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
|
409
409
|
...resolverOverrides
|
|
410
410
|
};
|
|
411
411
|
}
|
|
412
|
+
|
|
413
|
+
// src/topicScope.ts
|
|
412
414
|
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
413
415
|
async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
414
416
|
if (!ctx?.db || typeof ctx.db.query !== "function") {
|
|
@@ -429,16 +431,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
429
431
|
if (!node) {
|
|
430
432
|
return null;
|
|
431
433
|
}
|
|
432
|
-
const scopeKey =
|
|
434
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
433
435
|
if (!scopeKey) {
|
|
434
|
-
|
|
436
|
+
throw new Error(
|
|
437
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
438
|
+
);
|
|
435
439
|
}
|
|
440
|
+
const metadata = node.metadata ?? {};
|
|
436
441
|
return {
|
|
437
442
|
topicId: scopeKey,
|
|
438
443
|
projectId: asMappedProjectId(node),
|
|
439
|
-
source: "topic_node"
|
|
444
|
+
source: "topic_node",
|
|
445
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
446
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
440
447
|
};
|
|
441
448
|
}
|
|
449
|
+
function canonicalTopicGlobalId(node) {
|
|
450
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
451
|
+
if (globalId && isUuidV7(globalId)) {
|
|
452
|
+
return globalId;
|
|
453
|
+
}
|
|
454
|
+
const topicId = normalizeScopeValue(node.topicId);
|
|
455
|
+
return topicId && isUuidV7(topicId) ? topicId : null;
|
|
456
|
+
}
|
|
457
|
+
function requireUuidV7TopicScope(field, value) {
|
|
458
|
+
if (!isUuidV7(value)) {
|
|
459
|
+
throw new Error(
|
|
460
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
442
464
|
function asMappedProjectId(topic) {
|
|
443
465
|
if (!topic) {
|
|
444
466
|
return;
|
|
@@ -460,200 +482,25 @@ function normalizeScopeValue(value) {
|
|
|
460
482
|
const normalized = value.trim();
|
|
461
483
|
return normalized.length > 0 ? normalized : void 0;
|
|
462
484
|
}
|
|
463
|
-
function pickPrimaryTopic(candidates) {
|
|
464
|
-
return [...candidates].sort((a, b) => {
|
|
465
|
-
const depthA = a.depth ?? 9999;
|
|
466
|
-
const depthB = b.depth ?? 9999;
|
|
467
|
-
if (depthA !== depthB) {
|
|
468
|
-
return depthA - depthB;
|
|
469
|
-
}
|
|
470
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
471
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
472
|
-
if (createdA !== createdB) {
|
|
473
|
-
return createdA - createdB;
|
|
474
|
-
}
|
|
475
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
476
|
-
})[0];
|
|
477
|
-
}
|
|
478
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
479
|
-
const query2 = ctx.db.query("topics");
|
|
480
|
-
try {
|
|
481
|
-
return await query2.withIndex(
|
|
482
|
-
"by_graph_scope_project",
|
|
483
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
484
|
-
).collect();
|
|
485
|
-
} catch (error) {
|
|
486
|
-
debugGraphPrimitiveFallback(
|
|
487
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
488
|
-
{
|
|
489
|
-
error,
|
|
490
|
-
scopeId
|
|
491
|
-
}
|
|
492
|
-
);
|
|
493
|
-
const topics = await query2.collect();
|
|
494
|
-
return topics.filter((topic) => {
|
|
495
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
496
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
497
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
502
|
-
if (typeof ctx.runQuery !== "function") {
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
try {
|
|
506
|
-
return await ctx.runQuery(api.topics.get, {
|
|
507
|
-
id: topicId
|
|
508
|
-
}) ?? null;
|
|
509
|
-
} catch (error) {
|
|
510
|
-
debugGraphPrimitiveFallback(
|
|
511
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
512
|
-
{
|
|
513
|
-
error,
|
|
514
|
-
topicId
|
|
515
|
-
}
|
|
516
|
-
);
|
|
517
|
-
return null;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
521
|
-
if (typeof ctx.runQuery !== "function") {
|
|
522
|
-
return null;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
526
|
-
projectId: legacyScopeId
|
|
527
|
-
}) ?? null;
|
|
528
|
-
} catch (error) {
|
|
529
|
-
debugGraphPrimitiveFallback(
|
|
530
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
531
|
-
{
|
|
532
|
-
error,
|
|
533
|
-
legacyScopeId
|
|
534
|
-
}
|
|
535
|
-
);
|
|
536
|
-
return null;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
540
|
-
const MAX_DEPTH = 10;
|
|
541
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
542
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
543
|
-
if (tenantId && workspaceId) {
|
|
544
|
-
return { tenantId, workspaceId };
|
|
545
|
-
}
|
|
546
|
-
let current = topic;
|
|
547
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
548
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
549
|
-
if (!current) {
|
|
550
|
-
break;
|
|
551
|
-
}
|
|
552
|
-
if (!tenantId) {
|
|
553
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
554
|
-
}
|
|
555
|
-
if (!workspaceId) {
|
|
556
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
557
|
-
}
|
|
558
|
-
if (tenantId && workspaceId) {
|
|
559
|
-
break;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
return { tenantId, workspaceId };
|
|
563
|
-
}
|
|
564
485
|
async function resolveTopicProjectScope(ctx, args) {
|
|
565
486
|
if (args.topicId) {
|
|
566
487
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
567
488
|
}
|
|
568
489
|
if (args.projectId) {
|
|
569
|
-
|
|
490
|
+
throw new Error(
|
|
491
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
492
|
+
);
|
|
570
493
|
}
|
|
571
|
-
throw new Error(
|
|
572
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
573
|
-
);
|
|
494
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
574
495
|
}
|
|
575
496
|
async function resolveScopeFromTopicId(ctx, topicId) {
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
}
|
|
580
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
|
|
497
|
+
const topicGlobalId = String(topicId);
|
|
498
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
499
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
581
500
|
if (nodeScope) {
|
|
582
501
|
return nodeScope;
|
|
583
502
|
}
|
|
584
|
-
throw new Error(`Topic not found: ${
|
|
585
|
-
}
|
|
586
|
-
async function resolveTopicDocFromTopicId(ctx, topicId) {
|
|
587
|
-
const direct = await tryReadTopicDoc(ctx, topicId, {
|
|
588
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
589
|
-
idLogKey: "topicId"
|
|
590
|
-
});
|
|
591
|
-
if (direct) {
|
|
592
|
-
return direct;
|
|
593
|
-
}
|
|
594
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
|
|
595
|
-
if (hostTopic) {
|
|
596
|
-
return hostTopic;
|
|
597
|
-
}
|
|
598
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
|
|
599
|
-
}
|
|
600
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
601
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
602
|
-
ctx,
|
|
603
|
-
legacyProjectId
|
|
604
|
-
);
|
|
605
|
-
if (directTopic) {
|
|
606
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
607
|
-
fallbackProjectId: legacyProjectId
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
const primary = pickPrimaryTopic(
|
|
611
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
612
|
-
);
|
|
613
|
-
if (primary) {
|
|
614
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
615
|
-
fallbackProjectId: legacyProjectId
|
|
616
|
-
});
|
|
617
|
-
}
|
|
618
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
619
|
-
if (nodeScope) {
|
|
620
|
-
return {
|
|
621
|
-
...nodeScope,
|
|
622
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
623
|
-
};
|
|
624
|
-
}
|
|
625
|
-
throw new Error(
|
|
626
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
627
|
-
);
|
|
628
|
-
}
|
|
629
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
630
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
631
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
632
|
-
idLogKey: "projectId"
|
|
633
|
-
});
|
|
634
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
635
|
-
}
|
|
636
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
637
|
-
try {
|
|
638
|
-
return await ctx.db.get(id);
|
|
639
|
-
} catch (error) {
|
|
640
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
641
|
-
error,
|
|
642
|
-
[log.idLogKey]: id
|
|
643
|
-
});
|
|
644
|
-
return null;
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
648
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
649
|
-
const mapped = asMappedProjectId(topic);
|
|
650
|
-
return {
|
|
651
|
-
topicId: topic._id,
|
|
652
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
653
|
-
tenantId: inherited.tenantId,
|
|
654
|
-
workspaceId: inherited.workspaceId,
|
|
655
|
-
source
|
|
656
|
-
};
|
|
503
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
657
504
|
}
|
|
658
505
|
var optionalScopeArgs = {
|
|
659
506
|
projectId: v.optional(v.string()),
|
|
@@ -896,6 +743,9 @@ async function scheduleEmbeddingGeneration(args) {
|
|
|
896
743
|
}
|
|
897
744
|
function insertEpistemicNode(ctx, doc) {
|
|
898
745
|
assertUuidV7Identity("epistemicNodes", doc.globalId);
|
|
746
|
+
if (doc.topicId !== void 0 && doc.topicId !== null) {
|
|
747
|
+
assertUuidV7Reference("epistemicNodes.topicId", doc.topicId);
|
|
748
|
+
}
|
|
899
749
|
return ctx.db.insert("epistemicNodes", doc);
|
|
900
750
|
}
|
|
901
751
|
async function assertExistingNodeEndpoint(ctx, endpointRole, endpoint) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _lucern_access_control_convex_js from '@lucern/access-control/convex.js';
|
|
2
|
-
import { r as resolveTopicProjectScope, T as TopicProjectScope } from './topicScope-
|
|
2
|
+
import { r as resolveTopicProjectScope, T as TopicProjectScope } from './topicScope-CL1IVOmv.js';
|
|
3
3
|
import * as convex_values from 'convex/values';
|
|
4
4
|
import { Doc, QueryCtx, MutationCtx } from './convex.js';
|
|
5
5
|
import '@lucern/access-control/convex';
|
|
@@ -2,6 +2,7 @@ import { normalizeAudienceKey, classFromAudienceKey } from '@lucern/access-contr
|
|
|
2
2
|
import { unsafeConvexAnyApi } from '@lucern/contracts/convex/unsafeAnyApi';
|
|
3
3
|
import { componentsGeneric } from 'convex/server';
|
|
4
4
|
import { v } from 'convex/values';
|
|
5
|
+
import { isUuidV7 } from '@lucern/contracts/ids';
|
|
5
6
|
|
|
6
7
|
// src/epistemicEvidenceHelpers.ts
|
|
7
8
|
var unsafeApi = unsafeConvexAnyApi(
|
|
@@ -397,6 +398,8 @@ function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
|
397
398
|
...resolverOverrides
|
|
398
399
|
};
|
|
399
400
|
}
|
|
401
|
+
|
|
402
|
+
// src/topicScope.ts
|
|
400
403
|
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
401
404
|
async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
402
405
|
if (!ctx?.db || typeof ctx.db.query !== "function") {
|
|
@@ -417,16 +420,36 @@ async function resolveTopicNodeScopeOrNull(ctx, ref) {
|
|
|
417
420
|
if (!node) {
|
|
418
421
|
return null;
|
|
419
422
|
}
|
|
420
|
-
const scopeKey =
|
|
423
|
+
const scopeKey = canonicalTopicGlobalId(node);
|
|
421
424
|
if (!scopeKey) {
|
|
422
|
-
|
|
425
|
+
throw new Error(
|
|
426
|
+
`topic.uuidv7_identity_required: topic node ${ref} is missing a UUIDv7 globalId.`
|
|
427
|
+
);
|
|
423
428
|
}
|
|
429
|
+
const metadata = node.metadata ?? {};
|
|
424
430
|
return {
|
|
425
431
|
topicId: scopeKey,
|
|
426
432
|
projectId: asMappedProjectId(node),
|
|
427
|
-
source: "topic_node"
|
|
433
|
+
source: "topic_node",
|
|
434
|
+
tenantId: normalizeScopeValue(node.tenantId) ?? normalizeScopeValue(metadata.tenantId),
|
|
435
|
+
workspaceId: normalizeScopeValue(node.workspaceId) ?? normalizeScopeValue(metadata.workspaceId)
|
|
428
436
|
};
|
|
429
437
|
}
|
|
438
|
+
function canonicalTopicGlobalId(node) {
|
|
439
|
+
const globalId = normalizeScopeValue(node.globalId);
|
|
440
|
+
if (globalId && isUuidV7(globalId)) {
|
|
441
|
+
return globalId;
|
|
442
|
+
}
|
|
443
|
+
const topicId = normalizeScopeValue(node.topicId);
|
|
444
|
+
return topicId && isUuidV7(topicId) ? topicId : null;
|
|
445
|
+
}
|
|
446
|
+
function requireUuidV7TopicScope(field, value) {
|
|
447
|
+
if (!isUuidV7(value)) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`topic.uuidv7_required: ${field} must be a UUIDv7 public topic identifier.`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
430
453
|
function asMappedProjectId(topic) {
|
|
431
454
|
if (!topic) {
|
|
432
455
|
return;
|
|
@@ -448,200 +471,25 @@ function normalizeScopeValue(value) {
|
|
|
448
471
|
const normalized = value.trim();
|
|
449
472
|
return normalized.length > 0 ? normalized : void 0;
|
|
450
473
|
}
|
|
451
|
-
function pickPrimaryTopic(candidates) {
|
|
452
|
-
return [...candidates].sort((a, b) => {
|
|
453
|
-
const depthA = a.depth ?? 9999;
|
|
454
|
-
const depthB = b.depth ?? 9999;
|
|
455
|
-
if (depthA !== depthB) {
|
|
456
|
-
return depthA - depthB;
|
|
457
|
-
}
|
|
458
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
459
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
460
|
-
if (createdA !== createdB) {
|
|
461
|
-
return createdA - createdB;
|
|
462
|
-
}
|
|
463
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
464
|
-
})[0];
|
|
465
|
-
}
|
|
466
|
-
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
467
|
-
const query = ctx.db.query("topics");
|
|
468
|
-
try {
|
|
469
|
-
return await query.withIndex(
|
|
470
|
-
"by_graph_scope_project",
|
|
471
|
-
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
472
|
-
).collect();
|
|
473
|
-
} catch (error) {
|
|
474
|
-
debugGraphPrimitiveFallback(
|
|
475
|
-
"[topicScope] Failed to resolve scope alias via index",
|
|
476
|
-
{
|
|
477
|
-
error,
|
|
478
|
-
scopeId
|
|
479
|
-
}
|
|
480
|
-
);
|
|
481
|
-
const topics = await query.collect();
|
|
482
|
-
return topics.filter((topic) => {
|
|
483
|
-
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
484
|
-
const mappedProjectId = asMappedProjectId(topic);
|
|
485
|
-
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
490
|
-
if (typeof ctx.runQuery !== "function") {
|
|
491
|
-
return null;
|
|
492
|
-
}
|
|
493
|
-
try {
|
|
494
|
-
return await ctx.runQuery(api.topics.get, {
|
|
495
|
-
id: topicId
|
|
496
|
-
}) ?? null;
|
|
497
|
-
} catch (error) {
|
|
498
|
-
debugGraphPrimitiveFallback(
|
|
499
|
-
"[topicScope] Failed to resolve topic by host query",
|
|
500
|
-
{
|
|
501
|
-
error,
|
|
502
|
-
topicId
|
|
503
|
-
}
|
|
504
|
-
);
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
509
|
-
if (typeof ctx.runQuery !== "function") {
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
try {
|
|
513
|
-
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
514
|
-
projectId: legacyScopeId
|
|
515
|
-
}) ?? null;
|
|
516
|
-
} catch (error) {
|
|
517
|
-
debugGraphPrimitiveFallback(
|
|
518
|
-
"[topicScope] Failed to resolve topic by legacy scope",
|
|
519
|
-
{
|
|
520
|
-
error,
|
|
521
|
-
legacyScopeId
|
|
522
|
-
}
|
|
523
|
-
);
|
|
524
|
-
return null;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
528
|
-
const MAX_DEPTH = 10;
|
|
529
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
530
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
531
|
-
if (tenantId && workspaceId) {
|
|
532
|
-
return { tenantId, workspaceId };
|
|
533
|
-
}
|
|
534
|
-
let current = topic;
|
|
535
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
536
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
537
|
-
if (!current) {
|
|
538
|
-
break;
|
|
539
|
-
}
|
|
540
|
-
if (!tenantId) {
|
|
541
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
542
|
-
}
|
|
543
|
-
if (!workspaceId) {
|
|
544
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
545
|
-
}
|
|
546
|
-
if (tenantId && workspaceId) {
|
|
547
|
-
break;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
return { tenantId, workspaceId };
|
|
551
|
-
}
|
|
552
474
|
async function resolveTopicProjectScope(ctx, args) {
|
|
553
475
|
if (args.topicId) {
|
|
554
476
|
return await resolveScopeFromTopicId(ctx, args.topicId);
|
|
555
477
|
}
|
|
556
478
|
if (args.projectId) {
|
|
557
|
-
|
|
479
|
+
throw new Error(
|
|
480
|
+
"topic.uuidv7_required: projectId scope aliases are retired; pass topicId as the UUIDv7 topic globalId."
|
|
481
|
+
);
|
|
558
482
|
}
|
|
559
|
-
throw new Error(
|
|
560
|
-
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
561
|
-
);
|
|
483
|
+
throw new Error("topic.uuidv7_required: Missing scope: provide topicId.");
|
|
562
484
|
}
|
|
563
485
|
async function resolveScopeFromTopicId(ctx, topicId) {
|
|
564
|
-
const
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
568
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, String(topicId));
|
|
486
|
+
const topicGlobalId = String(topicId);
|
|
487
|
+
requireUuidV7TopicScope("topicId", topicGlobalId);
|
|
488
|
+
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, topicGlobalId);
|
|
569
489
|
if (nodeScope) {
|
|
570
490
|
return nodeScope;
|
|
571
491
|
}
|
|
572
|
-
throw new Error(`Topic not found: ${
|
|
573
|
-
}
|
|
574
|
-
async function resolveTopicDocFromTopicId(ctx, topicId) {
|
|
575
|
-
const direct = await tryReadTopicDoc(ctx, topicId, {
|
|
576
|
-
failureLog: "[topicScope] Failed to load topic by direct id",
|
|
577
|
-
idLogKey: "topicId"
|
|
578
|
-
});
|
|
579
|
-
if (direct) {
|
|
580
|
-
return direct;
|
|
581
|
-
}
|
|
582
|
-
const hostTopic = await tryResolveHostTopicById(ctx, String(topicId));
|
|
583
|
-
if (hostTopic) {
|
|
584
|
-
return hostTopic;
|
|
585
|
-
}
|
|
586
|
-
return pickPrimaryTopic(await findTopicsByScopeAlias(ctx, String(topicId))) ?? null;
|
|
587
|
-
}
|
|
588
|
-
async function resolveScopeFromLegacyProjectId(ctx, legacyProjectId) {
|
|
589
|
-
const directTopic = await resolveDirectLegacyProjectTopic(
|
|
590
|
-
ctx,
|
|
591
|
-
legacyProjectId
|
|
592
|
-
);
|
|
593
|
-
if (directTopic) {
|
|
594
|
-
return await buildTopicScope(ctx, directTopic, "topic_inferred", {
|
|
595
|
-
fallbackProjectId: legacyProjectId
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
const primary = pickPrimaryTopic(
|
|
599
|
-
await findTopicsByScopeAlias(ctx, legacyProjectId)
|
|
600
|
-
);
|
|
601
|
-
if (primary) {
|
|
602
|
-
return await buildTopicScope(ctx, primary, "project_mapped_topic", {
|
|
603
|
-
fallbackProjectId: legacyProjectId
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
const nodeScope = await resolveTopicNodeScopeOrNull(ctx, legacyProjectId);
|
|
607
|
-
if (nodeScope) {
|
|
608
|
-
return {
|
|
609
|
-
...nodeScope,
|
|
610
|
-
projectId: nodeScope.projectId ?? legacyProjectId
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
throw new Error(
|
|
614
|
-
`Legacy project scope ${legacyProjectId} has no mapped topic.`
|
|
615
|
-
);
|
|
616
|
-
}
|
|
617
|
-
async function resolveDirectLegacyProjectTopic(ctx, legacyProjectId) {
|
|
618
|
-
const directTopic = await tryReadTopicDoc(ctx, legacyProjectId, {
|
|
619
|
-
failureLog: "[topicScope] Failed to load direct project topic",
|
|
620
|
-
idLogKey: "projectId"
|
|
621
|
-
});
|
|
622
|
-
return directTopic ?? tryResolveHostTopicByLegacyScope(ctx, legacyProjectId);
|
|
623
|
-
}
|
|
624
|
-
async function tryReadTopicDoc(ctx, id, log) {
|
|
625
|
-
try {
|
|
626
|
-
return await ctx.db.get(id);
|
|
627
|
-
} catch (error) {
|
|
628
|
-
debugGraphPrimitiveFallback(log.failureLog, {
|
|
629
|
-
error,
|
|
630
|
-
[log.idLogKey]: id
|
|
631
|
-
});
|
|
632
|
-
return null;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
async function buildTopicScope(ctx, topic, source, options = {}) {
|
|
636
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
637
|
-
const mapped = asMappedProjectId(topic);
|
|
638
|
-
return {
|
|
639
|
-
topicId: topic._id,
|
|
640
|
-
...mapped || options.fallbackProjectId ? { projectId: mapped ?? options.fallbackProjectId } : {},
|
|
641
|
-
tenantId: inherited.tenantId,
|
|
642
|
-
workspaceId: inherited.workspaceId,
|
|
643
|
-
source
|
|
644
|
-
};
|
|
492
|
+
throw new Error(`Topic not found: ${topicGlobalId}`);
|
|
645
493
|
}
|
|
646
494
|
var optionalScopeArgs = {
|
|
647
495
|
projectId: v.optional(v.string()),
|