@lucern/graph-primitives 1.0.16 → 1.0.18
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/beliefEvidenceLinks.js +144 -99
- package/dist/beliefEvidenceLinks.js.map +1 -1
- package/dist/beliefEvidenceLinks.operational.d.ts +29 -0
- package/dist/beliefEvidenceLinks.operational.js +157 -0
- package/dist/beliefEvidenceLinks.operational.js.map +1 -0
- package/dist/{beliefLifecycle-y8WLXqQj.d.ts → beliefLifecycle-CXwdDw5e.d.ts} +7 -4
- package/dist/beliefLifecycle.d.ts +1 -1
- package/dist/beliefLifecycle.js +75 -18
- package/dist/beliefLifecycle.js.map +1 -1
- package/dist/entityLifecycle.js +1 -12
- package/dist/entityLifecycle.js.map +1 -1
- package/dist/epistemicAnswers.js +1 -12
- package/dist/epistemicAnswers.js.map +1 -1
- package/dist/epistemicBeliefs.admin.js.map +1 -1
- package/dist/epistemicBeliefs.backfills.d.ts +1 -1
- package/dist/epistemicBeliefs.backfills.js +63 -35
- package/dist/epistemicBeliefs.backfills.js.map +1 -1
- package/dist/epistemicBeliefs.confidence.d.ts +1 -1
- package/dist/epistemicBeliefs.confidence.js +70 -41
- package/dist/epistemicBeliefs.confidence.js.map +1 -1
- package/dist/epistemicBeliefs.core.js +946 -566
- package/dist/epistemicBeliefs.core.js.map +1 -1
- package/dist/epistemicBeliefs.d.ts +2 -2
- package/dist/epistemicBeliefs.forkEvidence.d.ts +18 -0
- package/dist/epistemicBeliefs.forkEvidence.js +121 -0
- package/dist/epistemicBeliefs.forkEvidence.js.map +1 -0
- package/dist/epistemicBeliefs.helpers.d.ts +2 -2
- package/dist/epistemicBeliefs.helpers.js +60 -32
- package/dist/epistemicBeliefs.helpers.js.map +1 -1
- package/dist/epistemicBeliefs.internal.js +175 -51
- package/dist/epistemicBeliefs.internal.js.map +1 -1
- package/dist/epistemicBeliefs.js +437 -84
- package/dist/epistemicBeliefs.js.map +1 -1
- package/dist/epistemicBeliefs.lifecycle.d.ts +2 -2
- package/dist/epistemicBeliefs.lifecycle.js +75 -47
- package/dist/epistemicBeliefs.lifecycle.js.map +1 -1
- package/dist/epistemicBeliefs.links.js +47 -13
- package/dist/epistemicBeliefs.links.js.map +1 -1
- package/dist/epistemicBeliefs.topicAnchor.d.ts +29 -0
- package/dist/epistemicBeliefs.topicAnchor.js +105 -0
- package/dist/epistemicBeliefs.topicAnchor.js.map +1 -0
- package/dist/epistemicContracts.evaluators.js +71 -42
- package/dist/epistemicContracts.evaluators.js.map +1 -1
- package/dist/epistemicContracts.handlers.js +72 -54
- package/dist/epistemicContracts.handlers.js.map +1 -1
- package/dist/epistemicContracts.js +72 -54
- package/dist/epistemicContracts.js.map +1 -1
- package/dist/epistemicContracts.metrics.js +1 -1
- package/dist/epistemicContracts.metrics.js.map +1 -1
- package/dist/epistemicContracts.types.d.ts +1 -1
- package/dist/epistemicEdgeCreation.js +1 -12
- package/dist/epistemicEdgeCreation.js.map +1 -1
- package/dist/epistemicEdges.helpers.d.ts +1 -1
- package/dist/epistemicEvidence.js +173 -93
- package/dist/epistemicEvidence.js.map +1 -1
- package/dist/epistemicEvidenceMutations.js +173 -93
- package/dist/epistemicEvidenceMutations.js.map +1 -1
- package/dist/epistemicHelpers.js +1 -12
- package/dist/epistemicHelpers.js.map +1 -1
- package/dist/epistemicNodeCreation.js +1 -10
- package/dist/epistemicNodeCreation.js.map +1 -1
- package/dist/epistemicNodes.internal.js.map +1 -1
- package/dist/epistemicNodes.js +2 -2
- package/dist/epistemicNodes.js.map +1 -1
- package/dist/epistemicNodes.mutations.js +2 -2
- package/dist/epistemicNodes.mutations.js.map +1 -1
- package/dist/epistemicQuestions.create.js +1 -12
- package/dist/epistemicQuestions.create.js.map +1 -1
- package/dist/epistemicQuestions.evidence.js +1 -12
- package/dist/epistemicQuestions.evidence.js.map +1 -1
- package/dist/epistemicQuestions.js +1 -12
- package/dist/epistemicQuestions.js.map +1 -1
- package/dist/epistemicQuestions.tail.js +1 -12
- package/dist/epistemicQuestions.tail.js.map +1 -1
- package/dist/epistemicSources.js +1 -12
- package/dist/epistemicSources.js.map +1 -1
- package/dist/evaluators/index.js +1 -1
- package/dist/evaluators/index.js.map +1 -1
- package/dist/globalId-4y9SPpC_.d.ts +10 -0
- package/dist/globalId.d.ts +1 -1
- package/dist/globalId.js +1 -13
- package/dist/globalId.js.map +1 -1
- package/dist/helpers.js +1 -12
- package/dist/helpers.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +771 -247
- package/dist/index.js.map +1 -1
- package/dist/invariantEnforcement.js +2 -2
- package/dist/invariantEnforcement.js.map +1 -1
- package/dist/proof-attestation.json +3 -3
- package/package.json +4 -4
- package/dist/globalId-DKh9d_uD.d.ts +0 -20
|
@@ -4,11 +4,123 @@ import { assertSchemaEnumValue } from '@lucern/contracts/schema-helpers/enumVali
|
|
|
4
4
|
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
5
5
|
import { componentsGeneric, anyApi, mutationGeneric, queryGeneric } from 'convex/server';
|
|
6
6
|
import { normalizeTupleContradictionPolicy, createInheritedContractRecord, confidenceFromSL } from '@lucern/confidence';
|
|
7
|
-
import { isNodeType, getLayerForNodeType } from '@lucern/contracts/schema-helpers/spine/tables/epistemicNodes';
|
|
8
7
|
import '@lucern/access-control/audience';
|
|
9
8
|
import { getCurrentUserId } from '@lucern/access-control/auth';
|
|
9
|
+
import { isNodeType, getLayerForNodeType } from '@lucern/contracts/schema-helpers/spine/tables/epistemicNodes';
|
|
10
|
+
import { generateGlobalId } from '@lucern/contracts/ids';
|
|
10
11
|
|
|
11
12
|
// src/epistemicBeliefs.core.ts
|
|
13
|
+
|
|
14
|
+
// src/beliefLifecycle.ts
|
|
15
|
+
var BELIEF_STATUS_VALUES = [
|
|
16
|
+
"assumption",
|
|
17
|
+
"hypothesis",
|
|
18
|
+
"active",
|
|
19
|
+
"superseded",
|
|
20
|
+
"resolved_true",
|
|
21
|
+
"resolved_false"
|
|
22
|
+
];
|
|
23
|
+
function isBeliefLifecycleStatus(value) {
|
|
24
|
+
return typeof value === "string" && BELIEF_STATUS_VALUES.includes(value);
|
|
25
|
+
}
|
|
26
|
+
function normalizeLegacyBeliefStatus(value) {
|
|
27
|
+
if (isBeliefLifecycleStatus(value)) {
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
if (value === "belief" || value === "established" || value === "emerging") {
|
|
31
|
+
return "active";
|
|
32
|
+
}
|
|
33
|
+
if (value === "fact" || value === "confirmed") {
|
|
34
|
+
return "resolved_true";
|
|
35
|
+
}
|
|
36
|
+
if (value === "disconfirmed" || value === "expired") {
|
|
37
|
+
return "resolved_false";
|
|
38
|
+
}
|
|
39
|
+
if (value === "deprecated") {
|
|
40
|
+
return "superseded";
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
function normalizeBeliefConfidence(confidence) {
|
|
45
|
+
if (typeof confidence !== "number" || !Number.isFinite(confidence)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
if (confidence >= 0 && confidence <= 1) {
|
|
49
|
+
return confidence;
|
|
50
|
+
}
|
|
51
|
+
if (confidence > 1 && confidence <= 100) {
|
|
52
|
+
return confidence / 100;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function isResolvedByConfidence(confidence) {
|
|
57
|
+
const normalized = normalizeBeliefConfidence(confidence);
|
|
58
|
+
if (normalized === null) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return normalized <= 0 || normalized >= 1;
|
|
62
|
+
}
|
|
63
|
+
function getPredictionMetaFromMetadata(metadata) {
|
|
64
|
+
return metadata?.predictionMeta;
|
|
65
|
+
}
|
|
66
|
+
function resolvedPredictionStatus(predictionMeta) {
|
|
67
|
+
if (!predictionMeta || typeof predictionMeta !== "object") {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const outcome = predictionMeta.outcome;
|
|
71
|
+
if (outcome === "confirmed") {
|
|
72
|
+
return "resolved_true";
|
|
73
|
+
}
|
|
74
|
+
if (outcome === "disconfirmed" || outcome === "expired") {
|
|
75
|
+
return "resolved_false";
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
function shouldTreatBeliefAsResolved(opts) {
|
|
80
|
+
if (isResolvedByConfidence(opts.confidence)) {
|
|
81
|
+
const normalized = normalizeBeliefConfidence(opts.confidence);
|
|
82
|
+
return normalized === 0 ? "resolved_false" : "resolved_true";
|
|
83
|
+
}
|
|
84
|
+
const directPredictionStatus = resolvedPredictionStatus(opts.predictionMeta);
|
|
85
|
+
if (directPredictionStatus) {
|
|
86
|
+
return directPredictionStatus;
|
|
87
|
+
}
|
|
88
|
+
const metadataPredictionStatus = resolvedPredictionStatus(
|
|
89
|
+
getPredictionMetaFromMetadata(opts.metadata)
|
|
90
|
+
);
|
|
91
|
+
if (metadataPredictionStatus) {
|
|
92
|
+
return metadataPredictionStatus;
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
function resolveBeliefLifecycleStatus(opts) {
|
|
97
|
+
const resolvedStatus = shouldTreatBeliefAsResolved(opts);
|
|
98
|
+
if (resolvedStatus) {
|
|
99
|
+
return resolvedStatus;
|
|
100
|
+
}
|
|
101
|
+
const direct = opts.beliefStatus;
|
|
102
|
+
const normalizedDirect = normalizeLegacyBeliefStatus(direct);
|
|
103
|
+
if (normalizedDirect) {
|
|
104
|
+
const normalized = normalizeBeliefConfidence(opts.confidence);
|
|
105
|
+
if (normalized !== null && isPreValidationBeliefStatus(normalizedDirect)) {
|
|
106
|
+
return "active";
|
|
107
|
+
}
|
|
108
|
+
return normalizedDirect;
|
|
109
|
+
}
|
|
110
|
+
const metaStatus = opts.metadata?.beliefStatus;
|
|
111
|
+
const normalizedMetaStatus = normalizeLegacyBeliefStatus(metaStatus);
|
|
112
|
+
if (normalizedMetaStatus) {
|
|
113
|
+
const normalized = normalizeBeliefConfidence(opts.confidence);
|
|
114
|
+
if (normalized !== null && isPreValidationBeliefStatus(normalizedMetaStatus)) {
|
|
115
|
+
return "active";
|
|
116
|
+
}
|
|
117
|
+
return normalizedMetaStatus;
|
|
118
|
+
}
|
|
119
|
+
return "assumption";
|
|
120
|
+
}
|
|
121
|
+
function isPreValidationBeliefStatus(status) {
|
|
122
|
+
return status === "assumption" || status === "hypothesis";
|
|
123
|
+
}
|
|
12
124
|
var api = anyApi;
|
|
13
125
|
componentsGeneric();
|
|
14
126
|
var internal = anyApi;
|
|
@@ -27,655 +139,614 @@ function debugGraphPrimitiveFallback(message, context) {
|
|
|
27
139
|
console.debug(message, context ?? {});
|
|
28
140
|
}
|
|
29
141
|
|
|
30
|
-
// src/
|
|
31
|
-
async function scheduleEmbeddingGeneration(args) {
|
|
32
|
-
try {
|
|
33
|
-
await args.ctx.scheduler.runAfter(
|
|
34
|
-
0,
|
|
35
|
-
"embeddingActions:generateEpistemicNodeEmbedding",
|
|
36
|
-
{
|
|
37
|
-
nodeId: args.nodeId,
|
|
38
|
-
projectId: args.projectId ? String(args.projectId) : void 0,
|
|
39
|
-
topicId: args.topicId ? String(args.topicId) : void 0,
|
|
40
|
-
createdBy: args.createdBy,
|
|
41
|
-
nodeType: args.nodeType,
|
|
42
|
-
text: args.text.slice(0, 2e4),
|
|
43
|
-
hasAnswer: args.hasAnswer,
|
|
44
|
-
confidence: args.confidence
|
|
45
|
-
}
|
|
46
|
-
);
|
|
47
|
-
} catch (error) {
|
|
48
|
-
debugGraphPrimitiveFallback(
|
|
49
|
-
"[embeddingTrigger] Failed to schedule embedding generation",
|
|
50
|
-
{
|
|
51
|
-
error,
|
|
52
|
-
nodeId: String(args.nodeId),
|
|
53
|
-
nodeType: args.nodeType
|
|
54
|
-
}
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// src/globalId.ts
|
|
60
|
-
function generateGlobalId() {
|
|
61
|
-
const bytes = new Uint8Array(16);
|
|
62
|
-
crypto.getRandomValues(bytes);
|
|
63
|
-
bytes[6] = bytes[6] & 15 | 64;
|
|
64
|
-
bytes[8] = bytes[8] & 63 | 128;
|
|
65
|
-
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(
|
|
66
|
-
""
|
|
67
|
-
);
|
|
68
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
69
|
-
}
|
|
142
|
+
// src/topicProjectOverlay.ts
|
|
70
143
|
var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
|
|
71
|
-
function
|
|
72
|
-
if (
|
|
144
|
+
function readNonEmptyString(value) {
|
|
145
|
+
if (typeof value !== "string") {
|
|
73
146
|
return;
|
|
74
147
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
148
|
+
const normalized = value.trim();
|
|
149
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
150
|
+
}
|
|
151
|
+
function readStringArray(value) {
|
|
152
|
+
if (!Array.isArray(value)) {
|
|
153
|
+
return [];
|
|
78
154
|
}
|
|
79
|
-
|
|
80
|
-
const candidate = metadata[LEGACY_SCOPE_FIELD] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
|
|
81
|
-
return candidate ? candidate : void 0;
|
|
155
|
+
return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
|
|
82
156
|
}
|
|
83
|
-
function
|
|
84
|
-
|
|
157
|
+
function readMetadata(topic) {
|
|
158
|
+
return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
|
|
159
|
+
}
|
|
160
|
+
function readLegacyProjectId(value) {
|
|
161
|
+
if (!value) {
|
|
85
162
|
return;
|
|
86
163
|
}
|
|
87
|
-
|
|
88
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
164
|
+
return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
|
|
89
165
|
}
|
|
90
|
-
function
|
|
91
|
-
return
|
|
92
|
-
const depthA = a.depth ?? 9999;
|
|
93
|
-
const depthB = b.depth ?? 9999;
|
|
94
|
-
if (depthA !== depthB) {
|
|
95
|
-
return depthA - depthB;
|
|
96
|
-
}
|
|
97
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
98
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
99
|
-
if (createdA !== createdB) {
|
|
100
|
-
return createdA - createdB;
|
|
101
|
-
}
|
|
102
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
103
|
-
})[0];
|
|
166
|
+
function coerceVisibility(value) {
|
|
167
|
+
return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
|
|
104
168
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
169
|
+
function coerceStatus(value) {
|
|
170
|
+
return value === "active" || value === "archived" || value === "watching" ? value : void 0;
|
|
171
|
+
}
|
|
172
|
+
function mapProjectType(topic, metadata) {
|
|
173
|
+
const explicit = readNonEmptyString(metadata.projectType);
|
|
174
|
+
if (explicit) {
|
|
175
|
+
return explicit;
|
|
176
|
+
}
|
|
177
|
+
if (topic.type === "theme") {
|
|
178
|
+
return "thematic";
|
|
179
|
+
}
|
|
180
|
+
return readNonEmptyString(topic.type) || "general";
|
|
181
|
+
}
|
|
182
|
+
function isProjectLikeTopic(topic) {
|
|
183
|
+
const metadata = readMetadata(topic);
|
|
184
|
+
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
185
|
+
}
|
|
186
|
+
function isMissingLucernChildComponentError(error) {
|
|
187
|
+
const message = getErrorMessage(error);
|
|
188
|
+
return message.includes(
|
|
189
|
+
'Child component ComponentName(Identifier("lucern")) not found'
|
|
190
|
+
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
191
|
+
}
|
|
192
|
+
function getErrorMessage(error) {
|
|
193
|
+
if (error instanceof Error) {
|
|
194
|
+
return error.message;
|
|
195
|
+
}
|
|
196
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
197
|
+
return error.message;
|
|
198
|
+
}
|
|
199
|
+
return "unknown error";
|
|
200
|
+
}
|
|
201
|
+
async function resolveTopicDoc(ctx, scopeId) {
|
|
202
|
+
if (ctx?.db && typeof ctx.db.get === "function") {
|
|
203
|
+
try {
|
|
204
|
+
const directTopic = await ctx.db.get(
|
|
116
205
|
scopeId
|
|
206
|
+
);
|
|
207
|
+
if (directTopic) {
|
|
208
|
+
return directTopic;
|
|
117
209
|
}
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
210
|
+
} catch (error) {
|
|
211
|
+
debugGraphPrimitiveFallback(
|
|
212
|
+
"[topicProjectOverlay] Failed to resolve topic by direct ID",
|
|
213
|
+
{
|
|
214
|
+
error,
|
|
215
|
+
scopeId
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
}
|
|
125
219
|
}
|
|
126
|
-
}
|
|
127
|
-
async function tryResolveHostTopicById(ctx, topicId) {
|
|
128
220
|
if (typeof ctx.runQuery !== "function") {
|
|
129
221
|
return null;
|
|
130
222
|
}
|
|
131
223
|
try {
|
|
132
|
-
|
|
133
|
-
id:
|
|
134
|
-
})
|
|
224
|
+
const topic = await ctx.runQuery(api.topics.get, {
|
|
225
|
+
id: String(scopeId)
|
|
226
|
+
});
|
|
227
|
+
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
228
|
+
return topic;
|
|
229
|
+
}
|
|
135
230
|
} catch (error) {
|
|
136
231
|
debugGraphPrimitiveFallback(
|
|
137
|
-
"[
|
|
232
|
+
"[topicProjectOverlay] Failed to resolve topic by ID query",
|
|
138
233
|
{
|
|
139
234
|
error,
|
|
140
|
-
|
|
235
|
+
scopeId
|
|
141
236
|
}
|
|
142
237
|
);
|
|
143
|
-
return null;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
147
|
-
if (typeof ctx.runQuery !== "function") {
|
|
148
|
-
return null;
|
|
149
238
|
}
|
|
150
239
|
try {
|
|
151
|
-
|
|
152
|
-
projectId:
|
|
153
|
-
})
|
|
240
|
+
const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
241
|
+
projectId: String(scopeId)
|
|
242
|
+
});
|
|
243
|
+
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
244
|
+
return topic;
|
|
245
|
+
}
|
|
154
246
|
} catch (error) {
|
|
155
247
|
debugGraphPrimitiveFallback(
|
|
156
|
-
"[
|
|
157
|
-
{
|
|
158
|
-
error,
|
|
159
|
-
legacyScopeId
|
|
160
|
-
}
|
|
248
|
+
"[topicProjectOverlay] Failed to resolve topic by legacy scope ID",
|
|
249
|
+
{ error, scopeId }
|
|
161
250
|
);
|
|
162
|
-
return null;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
166
|
-
const MAX_DEPTH = 10;
|
|
167
|
-
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
168
|
-
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
169
|
-
if (tenantId && workspaceId) {
|
|
170
|
-
return { tenantId, workspaceId };
|
|
171
|
-
}
|
|
172
|
-
let current = topic;
|
|
173
|
-
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
174
|
-
current = await ctx.db.get(current.parentTopicId);
|
|
175
|
-
if (!current) break;
|
|
176
|
-
if (!tenantId) {
|
|
177
|
-
tenantId = normalizeScopeValue(current.tenantId);
|
|
178
|
-
}
|
|
179
|
-
if (!workspaceId) {
|
|
180
|
-
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
181
|
-
}
|
|
182
|
-
if (tenantId && workspaceId) break;
|
|
183
251
|
}
|
|
184
|
-
return
|
|
252
|
+
return null;
|
|
185
253
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
return
|
|
225
|
-
topicId: topic._id,
|
|
226
|
-
tenantId: inherited.tenantId,
|
|
227
|
-
workspaceId: inherited.workspaceId,
|
|
228
|
-
source: "topic"
|
|
229
|
-
};
|
|
254
|
+
function materializeTopicProjectOverlay(topic, idMode = "legacy") {
|
|
255
|
+
const metadata = readMetadata(topic);
|
|
256
|
+
const topicId = String(topic._id);
|
|
257
|
+
const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
|
|
258
|
+
const storageProjectId = legacyProjectId || topicId;
|
|
259
|
+
const outwardId = idMode === "topic" ? topicId : storageProjectId;
|
|
260
|
+
const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
|
|
261
|
+
const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
|
|
262
|
+
const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
|
|
263
|
+
const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
|
|
264
|
+
return {
|
|
265
|
+
...metadata,
|
|
266
|
+
_id: outwardId,
|
|
267
|
+
projectId: outwardId,
|
|
268
|
+
topicId,
|
|
269
|
+
storageProjectId,
|
|
270
|
+
legacyProjectId,
|
|
271
|
+
name: readNonEmptyString(topic.name) || "Untitled Theme",
|
|
272
|
+
type: mapProjectType(topic, metadata),
|
|
273
|
+
description: readNonEmptyString(topic.description),
|
|
274
|
+
ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
|
|
275
|
+
sharedWith: readStringArray(metadata.sharedWith),
|
|
276
|
+
visibility,
|
|
277
|
+
tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
|
|
278
|
+
workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
|
|
279
|
+
status,
|
|
280
|
+
tags: readStringArray(metadata.tags),
|
|
281
|
+
chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
|
|
282
|
+
artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
|
|
283
|
+
lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
|
|
284
|
+
_creationTime: typeof topic._creationTime === "number" ? topic._creationTime : createdAt,
|
|
285
|
+
createdAt,
|
|
286
|
+
updatedAt
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
|
|
290
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
291
|
+
if (!topic) {
|
|
292
|
+
return null;
|
|
230
293
|
}
|
|
231
|
-
if (
|
|
232
|
-
|
|
294
|
+
if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return materializeTopicProjectOverlay(topic, options.idMode);
|
|
298
|
+
}
|
|
299
|
+
async function listTopicProjectOverlays(ctx, options = {}) {
|
|
300
|
+
let allTopics = [];
|
|
301
|
+
if (ctx?.db?.query && typeof ctx.db.query === "function") {
|
|
233
302
|
try {
|
|
234
|
-
|
|
235
|
-
args.projectId
|
|
236
|
-
);
|
|
303
|
+
allTopics = await ctx.db.query("topics").collect();
|
|
237
304
|
} catch (error) {
|
|
238
305
|
debugGraphPrimitiveFallback(
|
|
239
|
-
"[
|
|
240
|
-
{
|
|
241
|
-
error,
|
|
242
|
-
projectId: args.projectId
|
|
243
|
-
}
|
|
306
|
+
"[topicProjectOverlay] Failed to read topics table; falling back to API",
|
|
307
|
+
{ error }
|
|
244
308
|
);
|
|
309
|
+
allTopics = [];
|
|
245
310
|
}
|
|
246
|
-
if (directTopic) {
|
|
247
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
248
|
-
const mapped = asMappedProjectId(directTopic);
|
|
249
|
-
return {
|
|
250
|
-
topicId: directTopic._id,
|
|
251
|
-
projectId: mapped ?? args.projectId,
|
|
252
|
-
tenantId: inherited.tenantId,
|
|
253
|
-
workspaceId: inherited.workspaceId,
|
|
254
|
-
source: "topic_inferred"
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
directTopic = await tryResolveHostTopicByLegacyScope(ctx, args.projectId);
|
|
258
|
-
if (directTopic) {
|
|
259
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
260
|
-
const mapped = asMappedProjectId(directTopic);
|
|
261
|
-
return {
|
|
262
|
-
topicId: directTopic._id,
|
|
263
|
-
projectId: mapped ?? args.projectId,
|
|
264
|
-
tenantId: inherited.tenantId,
|
|
265
|
-
workspaceId: inherited.workspaceId,
|
|
266
|
-
source: "topic_inferred"
|
|
267
|
-
};
|
|
268
|
-
}
|
|
269
|
-
const topics = await findTopicsByScopeAlias(ctx, args.projectId);
|
|
270
|
-
const primary = pickPrimaryTopic(topics);
|
|
271
|
-
if (primary) {
|
|
272
|
-
const inherited = await resolveInheritedWorkspaceScope(ctx, primary);
|
|
273
|
-
return {
|
|
274
|
-
topicId: primary._id,
|
|
275
|
-
projectId: args.projectId,
|
|
276
|
-
tenantId: inherited.tenantId,
|
|
277
|
-
workspaceId: inherited.workspaceId,
|
|
278
|
-
source: "project_mapped_topic"
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
throw new Error(
|
|
282
|
-
`Legacy project scope ${String(args.projectId)} has no mapped topic.`
|
|
283
|
-
);
|
|
284
311
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
var optionalScopeArgs = {
|
|
290
|
-
projectId: v.optional(v.string()),
|
|
291
|
-
topicId: v.optional(v.string())
|
|
292
|
-
};
|
|
293
|
-
function normalizeScopeValue2(value) {
|
|
294
|
-
if (typeof value !== "string") {
|
|
295
|
-
return;
|
|
312
|
+
if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
|
|
313
|
+
allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
|
|
296
314
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
function throwWorkspaceIsolationError(args) {
|
|
301
|
-
const error = new Error(args.message);
|
|
302
|
-
error.status = 409;
|
|
303
|
-
error.code = "INVARIANT_VIOLATION";
|
|
304
|
-
error.invariantCode = args.invariantCode;
|
|
305
|
-
error.suggestion = args.suggestion;
|
|
306
|
-
error.details = args.details;
|
|
307
|
-
throw error;
|
|
315
|
+
return allTopics.filter(
|
|
316
|
+
(topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
|
|
317
|
+
).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
|
|
308
318
|
}
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
const workspaceId = normalizeScopeValue2(args.scope.workspaceId);
|
|
315
|
-
if (workspaceId) {
|
|
316
|
-
return;
|
|
319
|
+
async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
320
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
321
|
+
if (!topic) {
|
|
322
|
+
return null;
|
|
317
323
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
projectId:
|
|
324
|
+
const nextMetadata = { ...readMetadata(topic) };
|
|
325
|
+
const patch = {};
|
|
326
|
+
const topicUpdateArgs = {
|
|
327
|
+
id: String(topic._id)
|
|
328
|
+
};
|
|
329
|
+
for (const [key, rawValue] of Object.entries(value)) {
|
|
330
|
+
switch (key) {
|
|
331
|
+
case "_id":
|
|
332
|
+
case "projectId":
|
|
333
|
+
case "topicId":
|
|
334
|
+
case "legacyProjectId":
|
|
335
|
+
case "storageProjectId":
|
|
336
|
+
break;
|
|
337
|
+
case "name":
|
|
338
|
+
case "description":
|
|
339
|
+
patch[key] = rawValue;
|
|
340
|
+
topicUpdateArgs[key] = rawValue;
|
|
341
|
+
break;
|
|
342
|
+
case "tenantId":
|
|
343
|
+
case "workspaceId":
|
|
344
|
+
case "ownerId":
|
|
345
|
+
throw new Error(
|
|
346
|
+
`patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
|
|
347
|
+
);
|
|
348
|
+
case "status": {
|
|
349
|
+
const status = coerceStatus(rawValue);
|
|
350
|
+
if (status) {
|
|
351
|
+
patch.status = status;
|
|
352
|
+
topicUpdateArgs.status = status;
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "visibility": {
|
|
357
|
+
const visibility = coerceVisibility(rawValue);
|
|
358
|
+
if (visibility) {
|
|
359
|
+
patch.visibility = visibility;
|
|
360
|
+
topicUpdateArgs.visibility = visibility;
|
|
361
|
+
}
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
case "type": {
|
|
365
|
+
const projectType = readNonEmptyString(rawValue);
|
|
366
|
+
if (projectType) {
|
|
367
|
+
nextMetadata.projectType = projectType;
|
|
368
|
+
} else {
|
|
369
|
+
delete nextMetadata.projectType;
|
|
370
|
+
}
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
case "updatedAt":
|
|
374
|
+
case "createdAt":
|
|
375
|
+
break;
|
|
376
|
+
default:
|
|
377
|
+
if (rawValue === void 0) {
|
|
378
|
+
delete nextMetadata[key];
|
|
379
|
+
} else {
|
|
380
|
+
nextMetadata[key] = rawValue;
|
|
381
|
+
}
|
|
327
382
|
}
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
function nodeMatchesWorkspaceReasoningScope(node, scope) {
|
|
331
|
-
if (!node) {
|
|
332
|
-
return false;
|
|
333
|
-
}
|
|
334
|
-
const scopeTenantId = normalizeScopeValue2(scope.tenantId);
|
|
335
|
-
const scopeWorkspaceId = normalizeScopeValue2(scope.workspaceId);
|
|
336
|
-
const nodeTenantId = normalizeScopeValue2(node.tenantId);
|
|
337
|
-
const nodeWorkspaceId = normalizeScopeValue2(node.workspaceId);
|
|
338
|
-
const epistemicLayer = typeof node.epistemicLayer === "string" ? node.epistemicLayer : void 0;
|
|
339
|
-
if (scopeTenantId && nodeTenantId && scopeTenantId !== nodeTenantId) {
|
|
340
|
-
return false;
|
|
341
|
-
}
|
|
342
|
-
if (epistemicLayer === "ontological" && nodeWorkspaceId === void 0) {
|
|
343
|
-
return true;
|
|
344
|
-
}
|
|
345
|
-
if (!scopeWorkspaceId && node.publicationStatus === "published") {
|
|
346
|
-
return true;
|
|
347
383
|
}
|
|
348
|
-
|
|
349
|
-
|
|
384
|
+
patch.updatedAt = Date.now();
|
|
385
|
+
patch.metadata = nextMetadata;
|
|
386
|
+
topicUpdateArgs.metadata = nextMetadata;
|
|
387
|
+
if (typeof ctx.runMutation === "function") {
|
|
388
|
+
try {
|
|
389
|
+
await ctx.runMutation(api.topics.update, topicUpdateArgs);
|
|
390
|
+
} catch (error) {
|
|
391
|
+
if (!isMissingLucernChildComponentError(error) || !ctx?.db || typeof ctx.db.patch !== "function") {
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
await ctx.db.patch(String(topic._id), patch);
|
|
395
|
+
}
|
|
396
|
+
} else if (ctx?.db && typeof ctx.db.patch === "function") {
|
|
397
|
+
await ctx.db.patch(String(topic._id), patch);
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(
|
|
400
|
+
"Cannot patch topic without component adapter (ctx.runMutation unavailable)"
|
|
401
|
+
);
|
|
350
402
|
}
|
|
351
|
-
return
|
|
403
|
+
return materializeTopicProjectOverlay({
|
|
404
|
+
...topic,
|
|
405
|
+
...patch,
|
|
406
|
+
metadata: nextMetadata
|
|
407
|
+
});
|
|
352
408
|
}
|
|
353
409
|
|
|
354
|
-
// src/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
const normalized = value.trim();
|
|
361
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
410
|
+
// src/resolvers.ts
|
|
411
|
+
function isMissingLucernChildComponentError2(error) {
|
|
412
|
+
const message = getErrorMessage2(error);
|
|
413
|
+
return message.includes(
|
|
414
|
+
'Child component ComponentName(Identifier("lucern")) not found'
|
|
415
|
+
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
362
416
|
}
|
|
363
|
-
function
|
|
364
|
-
if (
|
|
365
|
-
return
|
|
417
|
+
function getErrorMessage2(error) {
|
|
418
|
+
if (error instanceof Error) {
|
|
419
|
+
return error.message;
|
|
366
420
|
}
|
|
367
|
-
|
|
421
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
422
|
+
return error.message;
|
|
423
|
+
}
|
|
424
|
+
return "unknown error";
|
|
368
425
|
}
|
|
369
|
-
function
|
|
370
|
-
|
|
426
|
+
function isAdvisoryTopicPatch(value) {
|
|
427
|
+
const advisoryKeys = /* @__PURE__ */ new Set(["lastActivityAt", "updatedAt"]);
|
|
428
|
+
const keys = Object.keys(value);
|
|
429
|
+
return keys.length > 0 && keys.every((key) => advisoryKeys.has(key));
|
|
371
430
|
}
|
|
372
|
-
function
|
|
373
|
-
|
|
374
|
-
|
|
431
|
+
async function patchProjectWithTolerance(ctx, projectId, value) {
|
|
432
|
+
try {
|
|
433
|
+
await patchTopicProjectOverlay(ctx, projectId, value);
|
|
434
|
+
} catch (error) {
|
|
435
|
+
if (!isAdvisoryTopicPatch(value) || !isMissingLucernChildComponentError2(error)) {
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
console.warn(
|
|
439
|
+
"[lucern graph-primitives] Non-fatal advisory topic patch failure",
|
|
440
|
+
{
|
|
441
|
+
projectId,
|
|
442
|
+
keys: Object.keys(value),
|
|
443
|
+
error: getErrorMessage2(error)
|
|
444
|
+
}
|
|
445
|
+
);
|
|
375
446
|
}
|
|
376
|
-
return readNonEmptyString(value[LEGACY_SCOPE_FIELD2]);
|
|
377
447
|
}
|
|
378
|
-
function
|
|
379
|
-
return
|
|
448
|
+
function defaultResolvers() {
|
|
449
|
+
return {
|
|
450
|
+
getProject: (ctx, projectId) => resolveTopicProjectOverlay(ctx, projectId, {
|
|
451
|
+
idMode: "legacy",
|
|
452
|
+
projectLikeOnly: false
|
|
453
|
+
}),
|
|
454
|
+
patchProject: (ctx, projectId, value) => patchProjectWithTolerance(ctx, projectId, value),
|
|
455
|
+
listTopics: (ctx) => listTopicProjectOverlays(ctx, {
|
|
456
|
+
idMode: "legacy"
|
|
457
|
+
}),
|
|
458
|
+
getFinalArtifact: (ctx, artifactId) => ctx.db.get(artifactId)
|
|
459
|
+
};
|
|
380
460
|
}
|
|
381
|
-
|
|
382
|
-
|
|
461
|
+
var resolverOverrides = {};
|
|
462
|
+
function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
463
|
+
return {
|
|
464
|
+
...defaultResolvers(),
|
|
465
|
+
...resolverOverrides
|
|
466
|
+
};
|
|
383
467
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
return
|
|
468
|
+
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
469
|
+
function asMappedProjectId(topic) {
|
|
470
|
+
if (!topic) {
|
|
471
|
+
return;
|
|
388
472
|
}
|
|
389
|
-
|
|
390
|
-
|
|
473
|
+
const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
|
|
474
|
+
if (directLegacyProjectId) {
|
|
475
|
+
return directLegacyProjectId;
|
|
391
476
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const metadata = readMetadata(topic);
|
|
396
|
-
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
397
|
-
}
|
|
398
|
-
function isMissingLucernChildComponentError(error) {
|
|
399
|
-
const message = getErrorMessage(error);
|
|
400
|
-
return message.includes(
|
|
401
|
-
'Child component ComponentName(Identifier("lucern")) not found'
|
|
402
|
-
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
477
|
+
const metadata = topic.metadata || {};
|
|
478
|
+
const candidate = metadata[LEGACY_SCOPE_FIELD2] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
|
|
479
|
+
return candidate ? candidate : void 0;
|
|
403
480
|
}
|
|
404
|
-
function
|
|
405
|
-
if (
|
|
406
|
-
return
|
|
407
|
-
}
|
|
408
|
-
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
409
|
-
return error.message;
|
|
481
|
+
function normalizeScopeValue(value) {
|
|
482
|
+
if (typeof value !== "string") {
|
|
483
|
+
return;
|
|
410
484
|
}
|
|
411
|
-
|
|
485
|
+
const normalized = value.trim();
|
|
486
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
412
487
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
488
|
+
function pickPrimaryTopic(candidates) {
|
|
489
|
+
return [...candidates].sort((a, b) => {
|
|
490
|
+
const depthA = a.depth ?? 9999;
|
|
491
|
+
const depthB = b.depth ?? 9999;
|
|
492
|
+
if (depthA !== depthB) {
|
|
493
|
+
return depthA - depthB;
|
|
494
|
+
}
|
|
495
|
+
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
496
|
+
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
497
|
+
if (createdA !== createdB) {
|
|
498
|
+
return createdA - createdB;
|
|
499
|
+
}
|
|
500
|
+
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
501
|
+
})[0];
|
|
502
|
+
}
|
|
503
|
+
async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
504
|
+
try {
|
|
505
|
+
return await ctx.db.query("topics").withIndex(
|
|
506
|
+
"by_graph_scope_project",
|
|
507
|
+
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
508
|
+
).collect();
|
|
509
|
+
} catch (error) {
|
|
510
|
+
debugGraphPrimitiveFallback(
|
|
511
|
+
"[topicScope] Failed to resolve scope alias via index",
|
|
512
|
+
{
|
|
513
|
+
error,
|
|
417
514
|
scopeId
|
|
418
|
-
);
|
|
419
|
-
if (directTopic) {
|
|
420
|
-
return directTopic;
|
|
421
515
|
}
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
);
|
|
430
|
-
}
|
|
516
|
+
);
|
|
517
|
+
const topics = await ctx.db.query("topics").collect();
|
|
518
|
+
return topics.filter((topic) => {
|
|
519
|
+
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
520
|
+
const mappedProjectId = asMappedProjectId(topic);
|
|
521
|
+
return String(topic._id) === scopeId || normalizedGlobalId === scopeId || mappedProjectId === scopeId;
|
|
522
|
+
});
|
|
431
523
|
}
|
|
524
|
+
}
|
|
525
|
+
async function tryResolveHostTopicById(ctx, topicId) {
|
|
432
526
|
if (typeof ctx.runQuery !== "function") {
|
|
433
527
|
return null;
|
|
434
528
|
}
|
|
435
529
|
try {
|
|
436
|
-
|
|
437
|
-
id:
|
|
438
|
-
});
|
|
439
|
-
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
440
|
-
return topic;
|
|
441
|
-
}
|
|
530
|
+
return await ctx.runQuery(api.topics.get, {
|
|
531
|
+
id: topicId
|
|
532
|
+
}) ?? null;
|
|
442
533
|
} catch (error) {
|
|
443
534
|
debugGraphPrimitiveFallback(
|
|
444
|
-
"[
|
|
535
|
+
"[topicScope] Failed to resolve topic by host query",
|
|
445
536
|
{
|
|
446
537
|
error,
|
|
447
|
-
|
|
538
|
+
topicId
|
|
448
539
|
}
|
|
449
540
|
);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
545
|
+
if (typeof ctx.runQuery !== "function") {
|
|
546
|
+
return null;
|
|
450
547
|
}
|
|
451
548
|
try {
|
|
452
|
-
|
|
453
|
-
projectId:
|
|
454
|
-
});
|
|
455
|
-
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
456
|
-
return topic;
|
|
457
|
-
}
|
|
549
|
+
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
550
|
+
projectId: legacyScopeId
|
|
551
|
+
}) ?? null;
|
|
458
552
|
} catch (error) {
|
|
459
553
|
debugGraphPrimitiveFallback(
|
|
460
|
-
"[
|
|
461
|
-
{
|
|
554
|
+
"[topicScope] Failed to resolve topic by legacy scope",
|
|
555
|
+
{
|
|
556
|
+
error,
|
|
557
|
+
legacyScopeId
|
|
558
|
+
}
|
|
462
559
|
);
|
|
560
|
+
return null;
|
|
463
561
|
}
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
function materializeTopicProjectOverlay(topic, idMode = "legacy") {
|
|
467
|
-
const metadata = readMetadata(topic);
|
|
468
|
-
const topicId = String(topic._id);
|
|
469
|
-
const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
|
|
470
|
-
const storageProjectId = legacyProjectId || topicId;
|
|
471
|
-
const outwardId = idMode === "topic" ? topicId : storageProjectId;
|
|
472
|
-
const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
|
|
473
|
-
const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
|
|
474
|
-
const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
|
|
475
|
-
const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
|
|
476
|
-
return {
|
|
477
|
-
...metadata,
|
|
478
|
-
_id: outwardId,
|
|
479
|
-
projectId: outwardId,
|
|
480
|
-
topicId,
|
|
481
|
-
storageProjectId,
|
|
482
|
-
legacyProjectId,
|
|
483
|
-
name: readNonEmptyString(topic.name) || "Untitled Theme",
|
|
484
|
-
type: mapProjectType(topic, metadata),
|
|
485
|
-
description: readNonEmptyString(topic.description),
|
|
486
|
-
ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
|
|
487
|
-
sharedWith: readStringArray(metadata.sharedWith),
|
|
488
|
-
visibility,
|
|
489
|
-
tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
|
|
490
|
-
workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
|
|
491
|
-
status,
|
|
492
|
-
tags: readStringArray(metadata.tags),
|
|
493
|
-
chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
|
|
494
|
-
artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
|
|
495
|
-
lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
|
|
496
|
-
_creationTime: typeof topic._creationTime === "number" ? topic._creationTime : createdAt,
|
|
497
|
-
createdAt,
|
|
498
|
-
updatedAt
|
|
499
|
-
};
|
|
500
562
|
}
|
|
501
|
-
async function
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
563
|
+
async function resolveInheritedWorkspaceScope(ctx, topic) {
|
|
564
|
+
const MAX_DEPTH = 10;
|
|
565
|
+
let tenantId = normalizeScopeValue(topic.tenantId);
|
|
566
|
+
let workspaceId = normalizeScopeValue(topic.workspaceId);
|
|
567
|
+
if (tenantId && workspaceId) {
|
|
568
|
+
return { tenantId, workspaceId };
|
|
505
569
|
}
|
|
506
|
-
|
|
507
|
-
|
|
570
|
+
let current = topic;
|
|
571
|
+
for (let i = 0; i < MAX_DEPTH && current?.parentTopicId; i++) {
|
|
572
|
+
current = await ctx.db.get(current.parentTopicId);
|
|
573
|
+
if (!current) break;
|
|
574
|
+
if (!tenantId) {
|
|
575
|
+
tenantId = normalizeScopeValue(current.tenantId);
|
|
576
|
+
}
|
|
577
|
+
if (!workspaceId) {
|
|
578
|
+
workspaceId = normalizeScopeValue(current.workspaceId);
|
|
579
|
+
}
|
|
580
|
+
if (tenantId && workspaceId) break;
|
|
508
581
|
}
|
|
509
|
-
return
|
|
582
|
+
return { tenantId, workspaceId };
|
|
510
583
|
}
|
|
511
|
-
async function
|
|
512
|
-
|
|
513
|
-
|
|
584
|
+
async function resolveTopicProjectScope(ctx, args) {
|
|
585
|
+
if (args.topicId) {
|
|
586
|
+
let topic = null;
|
|
514
587
|
try {
|
|
515
|
-
|
|
588
|
+
topic = await ctx.db.get(
|
|
589
|
+
args.topicId
|
|
590
|
+
);
|
|
516
591
|
} catch (error) {
|
|
517
592
|
debugGraphPrimitiveFallback(
|
|
518
|
-
"[
|
|
519
|
-
{
|
|
593
|
+
"[topicScope] Failed to load topic by direct id",
|
|
594
|
+
{
|
|
595
|
+
error,
|
|
596
|
+
topicId: args.topicId
|
|
597
|
+
}
|
|
520
598
|
);
|
|
521
|
-
allTopics = [];
|
|
522
599
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
case "projectId":
|
|
545
|
-
case "topicId":
|
|
546
|
-
case "legacyProjectId":
|
|
547
|
-
case "storageProjectId":
|
|
548
|
-
break;
|
|
549
|
-
case "name":
|
|
550
|
-
case "description":
|
|
551
|
-
patch[key] = rawValue;
|
|
552
|
-
topicUpdateArgs[key] = rawValue;
|
|
553
|
-
break;
|
|
554
|
-
case "tenantId":
|
|
555
|
-
case "workspaceId":
|
|
556
|
-
case "ownerId":
|
|
557
|
-
throw new Error(
|
|
558
|
-
`patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
|
|
559
|
-
);
|
|
560
|
-
case "status": {
|
|
561
|
-
const status = coerceStatus(rawValue);
|
|
562
|
-
if (status) {
|
|
563
|
-
patch.status = status;
|
|
564
|
-
topicUpdateArgs.status = status;
|
|
565
|
-
}
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
case "visibility": {
|
|
569
|
-
const visibility = coerceVisibility(rawValue);
|
|
570
|
-
if (visibility) {
|
|
571
|
-
patch.visibility = visibility;
|
|
572
|
-
topicUpdateArgs.visibility = visibility;
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "type": {
|
|
577
|
-
const projectType = readNonEmptyString(rawValue);
|
|
578
|
-
if (projectType) {
|
|
579
|
-
nextMetadata.projectType = projectType;
|
|
580
|
-
} else {
|
|
581
|
-
delete nextMetadata.projectType;
|
|
582
|
-
}
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "updatedAt":
|
|
586
|
-
case "createdAt":
|
|
587
|
-
break;
|
|
588
|
-
default:
|
|
589
|
-
if (rawValue === void 0) {
|
|
590
|
-
delete nextMetadata[key];
|
|
591
|
-
} else {
|
|
592
|
-
nextMetadata[key] = rawValue;
|
|
593
|
-
}
|
|
600
|
+
if (!topic) {
|
|
601
|
+
topic = await tryResolveHostTopicById(ctx, String(args.topicId));
|
|
602
|
+
}
|
|
603
|
+
if (!topic) {
|
|
604
|
+
topic = pickPrimaryTopic(
|
|
605
|
+
await findTopicsByScopeAlias(ctx, String(args.topicId))
|
|
606
|
+
) ?? null;
|
|
607
|
+
}
|
|
608
|
+
if (!topic) {
|
|
609
|
+
throw new Error(`Topic not found: ${String(args.topicId)}`);
|
|
610
|
+
}
|
|
611
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, topic);
|
|
612
|
+
const mapped = asMappedProjectId(topic);
|
|
613
|
+
if (mapped) {
|
|
614
|
+
return {
|
|
615
|
+
topicId: topic._id,
|
|
616
|
+
projectId: mapped,
|
|
617
|
+
tenantId: inherited.tenantId,
|
|
618
|
+
workspaceId: inherited.workspaceId,
|
|
619
|
+
source: "topic"
|
|
620
|
+
};
|
|
594
621
|
}
|
|
622
|
+
return {
|
|
623
|
+
topicId: topic._id,
|
|
624
|
+
tenantId: inherited.tenantId,
|
|
625
|
+
workspaceId: inherited.workspaceId,
|
|
626
|
+
source: "topic"
|
|
627
|
+
};
|
|
595
628
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
topicUpdateArgs.metadata = nextMetadata;
|
|
599
|
-
if (typeof ctx.runMutation === "function") {
|
|
629
|
+
if (args.projectId) {
|
|
630
|
+
let directTopic = null;
|
|
600
631
|
try {
|
|
601
|
-
await ctx.
|
|
632
|
+
directTopic = await ctx.db.get(
|
|
633
|
+
args.projectId
|
|
634
|
+
);
|
|
602
635
|
} catch (error) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
636
|
+
debugGraphPrimitiveFallback(
|
|
637
|
+
"[topicScope] Failed to load direct project topic",
|
|
638
|
+
{
|
|
639
|
+
error,
|
|
640
|
+
projectId: args.projectId
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
if (directTopic) {
|
|
645
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
646
|
+
const mapped = asMappedProjectId(directTopic);
|
|
647
|
+
return {
|
|
648
|
+
topicId: directTopic._id,
|
|
649
|
+
projectId: mapped ?? args.projectId,
|
|
650
|
+
tenantId: inherited.tenantId,
|
|
651
|
+
workspaceId: inherited.workspaceId,
|
|
652
|
+
source: "topic_inferred"
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
directTopic = await tryResolveHostTopicByLegacyScope(ctx, args.projectId);
|
|
656
|
+
if (directTopic) {
|
|
657
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
658
|
+
const mapped = asMappedProjectId(directTopic);
|
|
659
|
+
return {
|
|
660
|
+
topicId: directTopic._id,
|
|
661
|
+
projectId: mapped ?? args.projectId,
|
|
662
|
+
tenantId: inherited.tenantId,
|
|
663
|
+
workspaceId: inherited.workspaceId,
|
|
664
|
+
source: "topic_inferred"
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const topics = await findTopicsByScopeAlias(ctx, args.projectId);
|
|
668
|
+
const primary = pickPrimaryTopic(topics);
|
|
669
|
+
if (primary) {
|
|
670
|
+
const inherited = await resolveInheritedWorkspaceScope(ctx, primary);
|
|
671
|
+
return {
|
|
672
|
+
topicId: primary._id,
|
|
673
|
+
projectId: args.projectId,
|
|
674
|
+
tenantId: inherited.tenantId,
|
|
675
|
+
workspaceId: inherited.workspaceId,
|
|
676
|
+
source: "project_mapped_topic"
|
|
677
|
+
};
|
|
607
678
|
}
|
|
608
|
-
} else if (ctx?.db && typeof ctx.db.patch === "function") {
|
|
609
|
-
await ctx.db.patch(String(topic._id), patch);
|
|
610
|
-
} else {
|
|
611
679
|
throw new Error(
|
|
612
|
-
|
|
680
|
+
`Legacy project scope ${String(args.projectId)} has no mapped topic.`
|
|
613
681
|
);
|
|
614
682
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
metadata: nextMetadata
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// src/resolvers.ts
|
|
623
|
-
function isMissingLucernChildComponentError2(error) {
|
|
624
|
-
const message = getErrorMessage2(error);
|
|
625
|
-
return message.includes(
|
|
626
|
-
'Child component ComponentName(Identifier("lucern")) not found'
|
|
627
|
-
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
683
|
+
throw new Error(
|
|
684
|
+
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
685
|
+
);
|
|
628
686
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
687
|
+
var optionalScopeArgs = {
|
|
688
|
+
projectId: v.optional(v.string()),
|
|
689
|
+
topicId: v.optional(v.string())
|
|
690
|
+
};
|
|
691
|
+
function normalizeScopeValue2(value) {
|
|
692
|
+
if (typeof value !== "string") {
|
|
693
|
+
return;
|
|
635
694
|
}
|
|
636
|
-
|
|
695
|
+
const normalized = value.trim();
|
|
696
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
637
697
|
}
|
|
638
|
-
function
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
698
|
+
function throwWorkspaceIsolationError(args) {
|
|
699
|
+
const error = new Error(args.message);
|
|
700
|
+
error.status = 409;
|
|
701
|
+
error.code = "INVARIANT_VIOLATION";
|
|
702
|
+
error.invariantCode = args.invariantCode;
|
|
703
|
+
error.suggestion = args.suggestion;
|
|
704
|
+
error.details = args.details;
|
|
705
|
+
throw error;
|
|
642
706
|
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (!isAdvisoryTopicPatch(value) || !isMissingLucernChildComponentError2(error)) {
|
|
648
|
-
throw error;
|
|
649
|
-
}
|
|
650
|
-
console.warn(
|
|
651
|
-
"[lucern graph-primitives] Non-fatal advisory topic patch failure",
|
|
652
|
-
{
|
|
653
|
-
projectId,
|
|
654
|
-
keys: Object.keys(value),
|
|
655
|
-
error: getErrorMessage2(error)
|
|
656
|
-
}
|
|
657
|
-
);
|
|
707
|
+
function assertWorkspaceScopedEpistemicNodeScope(args) {
|
|
708
|
+
const layer = isNodeType(args.nodeType) ? getLayerForNodeType(args.nodeType) : void 0;
|
|
709
|
+
if (layer === "ontological") {
|
|
710
|
+
return;
|
|
658
711
|
}
|
|
712
|
+
const workspaceId = normalizeScopeValue2(args.scope.workspaceId);
|
|
713
|
+
if (workspaceId) {
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
throwWorkspaceIsolationError({
|
|
717
|
+
message: "Workspace-scoped reasoning isolation requires workspaceId on non-ontological node creation.",
|
|
718
|
+
invariantCode: "workspace.scope_required_for_epistemic_nodes",
|
|
719
|
+
suggestion: "Resolve the topic/project scope through a workspace-bound topic before creating epistemic nodes.",
|
|
720
|
+
details: {
|
|
721
|
+
mutationName: args.mutationName,
|
|
722
|
+
nodeType: args.nodeType,
|
|
723
|
+
topicId: args.scope.topicId,
|
|
724
|
+
projectId: args.scope.projectId
|
|
725
|
+
}
|
|
726
|
+
});
|
|
659
727
|
}
|
|
660
|
-
function
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
728
|
+
function nodeMatchesWorkspaceReasoningScope(node, scope) {
|
|
729
|
+
if (!node) {
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
const scopeTenantId = normalizeScopeValue2(scope.tenantId);
|
|
733
|
+
const scopeWorkspaceId = normalizeScopeValue2(scope.workspaceId);
|
|
734
|
+
const nodeTenantId = normalizeScopeValue2(node.tenantId);
|
|
735
|
+
const nodeWorkspaceId = normalizeScopeValue2(node.workspaceId);
|
|
736
|
+
const epistemicLayer = typeof node.epistemicLayer === "string" ? node.epistemicLayer : void 0;
|
|
737
|
+
if (scopeTenantId && nodeTenantId && scopeTenantId !== nodeTenantId) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
if (epistemicLayer === "ontological" && nodeWorkspaceId === void 0) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
if (!scopeWorkspaceId && node.publicationStatus === "published") {
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
if (!scopeWorkspaceId) {
|
|
747
|
+
return nodeWorkspaceId === void 0;
|
|
748
|
+
}
|
|
749
|
+
return scopeWorkspaceId === nodeWorkspaceId;
|
|
679
750
|
}
|
|
680
751
|
|
|
681
752
|
// src/epistemicBeliefs.helpers.ts
|
|
@@ -722,7 +793,7 @@ function buildBeliefConfidenceRow(args) {
|
|
|
722
793
|
disbelief: args.disbelief,
|
|
723
794
|
uncertainty: args.uncertainty,
|
|
724
795
|
baseRate: args.baseRate,
|
|
725
|
-
slOperator: args.slOperator ?? "
|
|
796
|
+
slOperator: args.slOperator ?? "prior_seed",
|
|
726
797
|
trigger: args.trigger,
|
|
727
798
|
...args.rationale ? { rationale: args.rationale } : {},
|
|
728
799
|
assessedBy: args.assessedBy,
|
|
@@ -856,10 +927,234 @@ async function requireProjectWriteAccess(ctx, projectId, userId) {
|
|
|
856
927
|
}
|
|
857
928
|
}
|
|
858
929
|
|
|
930
|
+
// src/epistemicBeliefs.forkEvidence.ts
|
|
931
|
+
function normalizeForkTriggerRelation(value) {
|
|
932
|
+
if (value === "supports" || value === "supporting") {
|
|
933
|
+
return "supports";
|
|
934
|
+
}
|
|
935
|
+
if (value === "contradicts" || value === "contradicting") {
|
|
936
|
+
return "contradicts";
|
|
937
|
+
}
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
async function resolveForkTriggerEvidence(ctx, args) {
|
|
941
|
+
const evidence = await ctx.db.get(args.triggeringEvidenceId);
|
|
942
|
+
if (!evidence || evidence.nodeType !== "evidence") {
|
|
943
|
+
throwStructuredMutationError({
|
|
944
|
+
message: "Fork requires an existing evidence node.",
|
|
945
|
+
status: 400,
|
|
946
|
+
code: "INVALID_ARGUMENT",
|
|
947
|
+
invariantCode: "belief.fork_requires_evidence",
|
|
948
|
+
suggestion: "Create or link evidence first, then fork with triggeringEvidenceId.",
|
|
949
|
+
details: { triggeringEvidenceId: args.triggeringEvidenceId }
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
if (evidence.topicId && evidence.topicId !== args.parent.topicId) {
|
|
953
|
+
throwStructuredMutationError({
|
|
954
|
+
message: "Fork evidence belongs to a different topic scope.",
|
|
955
|
+
status: 400,
|
|
956
|
+
code: "INVALID_ARGUMENT",
|
|
957
|
+
invariantCode: "belief.fork_evidence_scope",
|
|
958
|
+
suggestion: "Use evidence from the same topic/workspace scope as the parent belief.",
|
|
959
|
+
details: {
|
|
960
|
+
parentNodeId: args.parentNodeId,
|
|
961
|
+
triggeringEvidenceId: args.triggeringEvidenceId
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
const evidenceMetadata = evidence.metadata && typeof evidence.metadata === "object" ? evidence.metadata : {};
|
|
966
|
+
const parentRefs = new Set(
|
|
967
|
+
[
|
|
968
|
+
String(args.parentNodeId),
|
|
969
|
+
String(args.parent.globalId ?? ""),
|
|
970
|
+
String(args.parent._id)
|
|
971
|
+
].filter(Boolean)
|
|
972
|
+
);
|
|
973
|
+
const evidenceRefs = new Set(
|
|
974
|
+
[
|
|
975
|
+
String(args.triggeringEvidenceId),
|
|
976
|
+
String(evidence.globalId ?? ""),
|
|
977
|
+
String(evidence._id)
|
|
978
|
+
].filter(Boolean)
|
|
979
|
+
);
|
|
980
|
+
let relation = null;
|
|
981
|
+
const linkedBeliefNodeId = String(
|
|
982
|
+
evidenceMetadata.linkedBeliefNodeId ?? ""
|
|
983
|
+
);
|
|
984
|
+
if (linkedBeliefNodeId && parentRefs.has(linkedBeliefNodeId)) {
|
|
985
|
+
relation = normalizeForkTriggerRelation(evidenceMetadata.evidenceRelation);
|
|
986
|
+
}
|
|
987
|
+
if (!relation) {
|
|
988
|
+
for (const parentRef of parentRefs) {
|
|
989
|
+
const links = await ctx.db.query("beliefEvidenceLinks").withIndex("by_beliefId", (q) => q.eq("beliefId", parentRef)).collect();
|
|
990
|
+
const matched = links.find((link) => evidenceRefs.has(String(link.insightId)));
|
|
991
|
+
if (matched) {
|
|
992
|
+
relation = normalizeForkTriggerRelation(matched.relation);
|
|
993
|
+
break;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
if (!relation) {
|
|
998
|
+
throwStructuredMutationError({
|
|
999
|
+
message: "Fork evidence must already be attached to the parent belief through an SL evidence relation.",
|
|
1000
|
+
status: 409,
|
|
1001
|
+
code: "CONFLICT",
|
|
1002
|
+
invariantCode: "belief.fork_requires_attached_evidence",
|
|
1003
|
+
suggestion: "Attach the evidence to the parent belief as supports or contradicts before forking.",
|
|
1004
|
+
details: {
|
|
1005
|
+
parentNodeId: args.parentNodeId,
|
|
1006
|
+
triggeringEvidenceId: args.triggeringEvidenceId
|
|
1007
|
+
}
|
|
1008
|
+
});
|
|
1009
|
+
}
|
|
1010
|
+
if (args.forkMode === "supersede" && relation !== "contradicts") {
|
|
1011
|
+
throwStructuredMutationError({
|
|
1012
|
+
message: "Superseding fork requires contradicting evidence against the parent belief.",
|
|
1013
|
+
status: 409,
|
|
1014
|
+
code: "CONFLICT",
|
|
1015
|
+
invariantCode: "belief.supersede_requires_contradiction",
|
|
1016
|
+
suggestion: "Use forkMode='branch' for a non-replacing fork, or attach contradicting evidence before superseding.",
|
|
1017
|
+
details: {
|
|
1018
|
+
parentNodeId: args.parentNodeId,
|
|
1019
|
+
triggeringEvidenceId: args.triggeringEvidenceId,
|
|
1020
|
+
relation
|
|
1021
|
+
}
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
return { evidenceNodeId: args.triggeringEvidenceId, relation };
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/epistemicBeliefs.topicAnchor.ts
|
|
1028
|
+
function cleanString(value) {
|
|
1029
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
1030
|
+
}
|
|
1031
|
+
function topicNodeCandidates(topicRef) {
|
|
1032
|
+
const normalized = topicRef.trim();
|
|
1033
|
+
if (!normalized) {
|
|
1034
|
+
return [];
|
|
1035
|
+
}
|
|
1036
|
+
const candidates = [normalized];
|
|
1037
|
+
if (normalized.startsWith("top_")) {
|
|
1038
|
+
candidates.push(normalized.slice(4));
|
|
1039
|
+
}
|
|
1040
|
+
return [...new Set(candidates)];
|
|
1041
|
+
}
|
|
1042
|
+
function readTopicNodeRef(args) {
|
|
1043
|
+
return cleanString(args.topicGlobalId) ?? cleanString(args.topicNodeId) ?? cleanString(args.topicId);
|
|
1044
|
+
}
|
|
1045
|
+
async function resolveRequiredTopicAnchor(ctx, topicRef) {
|
|
1046
|
+
for (const candidate of topicNodeCandidates(topicRef)) {
|
|
1047
|
+
try {
|
|
1048
|
+
const direct = await ctx.db.get(candidate);
|
|
1049
|
+
if (direct?.nodeType === "topic" && cleanString(direct.globalId)) {
|
|
1050
|
+
return direct;
|
|
1051
|
+
}
|
|
1052
|
+
} catch (_) {
|
|
1053
|
+
}
|
|
1054
|
+
const byGlobalId = await ctx.db.query("epistemicNodes").withIndex("by_globalId", (q) => q.eq("globalId", candidate)).first();
|
|
1055
|
+
if (byGlobalId?.nodeType === "topic" && cleanString(byGlobalId.globalId)) {
|
|
1056
|
+
return byGlobalId;
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
throw new Error(
|
|
1060
|
+
"Belief creation requires topicGlobalId or topicNodeId for a topic node in epistemicNodes. Legacy topics-table IDs are not valid belief anchors."
|
|
1061
|
+
);
|
|
1062
|
+
}
|
|
1063
|
+
function scopeFromTopicAnchor(topicNode) {
|
|
1064
|
+
return {
|
|
1065
|
+
topicId: topicNode.globalId,
|
|
1066
|
+
projectId: cleanString(topicNode.projectId),
|
|
1067
|
+
tenantId: cleanString(topicNode.tenantId),
|
|
1068
|
+
workspaceId: cleanString(topicNode.workspaceId),
|
|
1069
|
+
source: "topic"
|
|
1070
|
+
};
|
|
1071
|
+
}
|
|
1072
|
+
async function createRequiredBeliefTopicEdge(ctx, args) {
|
|
1073
|
+
const topicGlobalId = args.topicNode.globalId;
|
|
1074
|
+
const edgeGlobalId = `edge:${args.beliefGlobalId}:${topicGlobalId}:scoped_by`;
|
|
1075
|
+
const now = Date.now();
|
|
1076
|
+
const existing = await ctx.db.query("epistemicEdges").withIndex("by_globalId", (q) => q.eq("globalId", edgeGlobalId)).first();
|
|
1077
|
+
if (!existing) {
|
|
1078
|
+
await ctx.db.insert("epistemicEdges", {
|
|
1079
|
+
globalId: edgeGlobalId,
|
|
1080
|
+
fromNodeId: String(args.beliefNodeId),
|
|
1081
|
+
toNodeId: String(args.topicNode._id),
|
|
1082
|
+
sourceGlobalId: args.beliefGlobalId,
|
|
1083
|
+
targetGlobalId: topicGlobalId,
|
|
1084
|
+
edgeType: "scoped_by",
|
|
1085
|
+
weight: 1,
|
|
1086
|
+
confidence: 1,
|
|
1087
|
+
context: "Belief creation topic anchor invariant.",
|
|
1088
|
+
reasoningMethod: "implicit",
|
|
1089
|
+
derivationType: "topic_scope_invariant",
|
|
1090
|
+
metadata: { invariant: "belief.topic_edge_required" },
|
|
1091
|
+
createdBy: args.createdBy,
|
|
1092
|
+
createdAt: now,
|
|
1093
|
+
updatedAt: now,
|
|
1094
|
+
projectId: cleanString(args.topicNode.projectId),
|
|
1095
|
+
topicId: topicGlobalId,
|
|
1096
|
+
tenantId: cleanString(args.topicNode.tenantId),
|
|
1097
|
+
workspaceId: cleanString(args.topicNode.workspaceId),
|
|
1098
|
+
fromNodeType: "belief",
|
|
1099
|
+
toNodeType: "topic",
|
|
1100
|
+
fromLayer: "L3",
|
|
1101
|
+
toLayer: args.topicNode.epistemicLayer ?? "ontological"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
await ctx.scheduler.runAfter(0, internal.neo4jEdgeAPI.createEdge, {
|
|
1105
|
+
globalId: edgeGlobalId,
|
|
1106
|
+
fromGlobalId: args.beliefGlobalId,
|
|
1107
|
+
toGlobalId: topicGlobalId,
|
|
1108
|
+
edgeType: "scoped_by",
|
|
1109
|
+
weight: 1,
|
|
1110
|
+
confidence: 1,
|
|
1111
|
+
context: "Belief creation topic anchor invariant.",
|
|
1112
|
+
projectId: cleanString(args.topicNode.projectId),
|
|
1113
|
+
topicId: topicGlobalId,
|
|
1114
|
+
createdBy: args.createdBy,
|
|
1115
|
+
fromNodeType: "belief",
|
|
1116
|
+
toNodeType: "topic",
|
|
1117
|
+
fromLayer: "L3",
|
|
1118
|
+
toLayer: args.topicNode.epistemicLayer ?? "ontological",
|
|
1119
|
+
metadata: { invariant: "belief.topic_edge_required" }
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// src/embeddingTrigger.ts
|
|
1124
|
+
async function scheduleEmbeddingGeneration(args) {
|
|
1125
|
+
try {
|
|
1126
|
+
await args.ctx.scheduler.runAfter(
|
|
1127
|
+
0,
|
|
1128
|
+
"embeddingActions:generateEpistemicNodeEmbedding",
|
|
1129
|
+
{
|
|
1130
|
+
nodeId: args.nodeId,
|
|
1131
|
+
projectId: args.projectId ? String(args.projectId) : void 0,
|
|
1132
|
+
topicId: args.topicId ? String(args.topicId) : void 0,
|
|
1133
|
+
createdBy: args.createdBy,
|
|
1134
|
+
nodeType: args.nodeType,
|
|
1135
|
+
text: args.text.slice(0, 2e4),
|
|
1136
|
+
hasAnswer: args.hasAnswer,
|
|
1137
|
+
confidence: args.confidence
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
} catch (error) {
|
|
1141
|
+
debugGraphPrimitiveFallback(
|
|
1142
|
+
"[embeddingTrigger] Failed to schedule embedding generation",
|
|
1143
|
+
{
|
|
1144
|
+
error,
|
|
1145
|
+
nodeId: String(args.nodeId),
|
|
1146
|
+
nodeType: args.nodeType
|
|
1147
|
+
}
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
859
1152
|
// src/epistemicBeliefs.core.ts
|
|
860
1153
|
var create = mutation({
|
|
861
1154
|
args: {
|
|
862
1155
|
...optionalBeliefScopeArgs,
|
|
1156
|
+
topicNodeId: v.optional(v.string()),
|
|
1157
|
+
topicGlobalId: v.optional(v.string()),
|
|
863
1158
|
formulation: v.string(),
|
|
864
1159
|
beliefType: v.optional(v.string()),
|
|
865
1160
|
rationale: v.optional(v.string()),
|
|
@@ -904,20 +1199,32 @@ var create = mutation({
|
|
|
904
1199
|
returns: permissiveReturn,
|
|
905
1200
|
handler: async (ctx, args) => {
|
|
906
1201
|
const authenticatedUserId = await requireAuthenticatedUserId(ctx);
|
|
907
|
-
const
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1202
|
+
const topicRef = readTopicNodeRef(args);
|
|
1203
|
+
if (!topicRef) {
|
|
1204
|
+
throwStructuredMutationError({
|
|
1205
|
+
message: "Belief creation requires an explicit topic epistemic node.",
|
|
1206
|
+
status: 400,
|
|
1207
|
+
code: "INVALID_ARGUMENT",
|
|
1208
|
+
invariantCode: "belief.topic_node_required",
|
|
1209
|
+
suggestion: "Pass topicGlobalId or topicNodeId for a topic in epistemicNodes before creating a belief.",
|
|
1210
|
+
details: {
|
|
1211
|
+
topicId: args.topicId,
|
|
1212
|
+
topicNodeId: args.topicNodeId,
|
|
1213
|
+
topicGlobalId: args.topicGlobalId
|
|
1214
|
+
}
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
const topicNode = await resolveRequiredTopicAnchor(ctx, topicRef);
|
|
1218
|
+
const scope = scopeFromTopicAnchor(topicNode);
|
|
911
1219
|
assertWorkspaceScopedEpistemicNodeScope({
|
|
912
1220
|
scope,
|
|
913
1221
|
nodeType: "belief",
|
|
914
1222
|
mutationName: "epistemicBeliefs.create"
|
|
915
1223
|
});
|
|
916
|
-
const topic = await ctx.db.get(scope.topicId);
|
|
917
1224
|
const normalizedBeliefType = await assertSchemaEnumValue(ctx, {
|
|
918
1225
|
category: "belief_type",
|
|
919
1226
|
value: args.beliefType,
|
|
920
|
-
tenantId:
|
|
1227
|
+
tenantId: scope.tenantId,
|
|
921
1228
|
context: "epistemicBeliefs.create"
|
|
922
1229
|
});
|
|
923
1230
|
if (scope.projectId) {
|
|
@@ -950,7 +1257,7 @@ var create = mutation({
|
|
|
950
1257
|
title: args.formulation.slice(0, 100) + (args.formulation.length > 100 ? "..." : ""),
|
|
951
1258
|
metadata: {
|
|
952
1259
|
pillar,
|
|
953
|
-
// No confidenceLevel — only set after
|
|
1260
|
+
// No confidenceLevel — only set after evidence-backed SL scoring.
|
|
954
1261
|
status: "active",
|
|
955
1262
|
worktreeId: args.worktreeId,
|
|
956
1263
|
beliefStatus: initialBeliefStatus,
|
|
@@ -994,9 +1301,15 @@ var create = mutation({
|
|
|
994
1301
|
rationale: "LKC-2 mandatory prior: seeded vacuous opinion at belief creation.",
|
|
995
1302
|
assessedBy: authenticatedUserId,
|
|
996
1303
|
assessedAt: now,
|
|
997
|
-
slOperator: "
|
|
1304
|
+
slOperator: "prior_seed"
|
|
998
1305
|
})
|
|
999
1306
|
);
|
|
1307
|
+
await createRequiredBeliefTopicEdge(ctx, {
|
|
1308
|
+
beliefNodeId: nodeId,
|
|
1309
|
+
beliefGlobalId,
|
|
1310
|
+
topicNode,
|
|
1311
|
+
createdBy: authenticatedUserId
|
|
1312
|
+
});
|
|
1000
1313
|
await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
|
|
1001
1314
|
nodeId,
|
|
1002
1315
|
operation: "upsert"
|
|
@@ -1333,9 +1646,10 @@ var forkBelief = mutation({
|
|
|
1333
1646
|
v.literal("refinement"),
|
|
1334
1647
|
v.literal("contradiction_response"),
|
|
1335
1648
|
v.literal("scope_change"),
|
|
1336
|
-
v.literal("confidence_collapse")
|
|
1337
|
-
v.literal("manual")
|
|
1649
|
+
v.literal("confidence_collapse")
|
|
1338
1650
|
),
|
|
1651
|
+
forkMode: v.optional(v.union(v.literal("supersede"), v.literal("branch"))),
|
|
1652
|
+
triggeringEvidenceId: v.id("epistemicNodes"),
|
|
1339
1653
|
rationale: v.optional(v.string()),
|
|
1340
1654
|
userId: v.string()
|
|
1341
1655
|
},
|
|
@@ -1378,6 +1692,33 @@ var forkBelief = mutation({
|
|
|
1378
1692
|
await requireProjectWriteAccess(ctx, parent.projectId, authenticatedUserId);
|
|
1379
1693
|
const metadata = parent.metadata;
|
|
1380
1694
|
const forkBeliefStatus = "hypothesis";
|
|
1695
|
+
const forkMode = args.forkMode ?? "supersede";
|
|
1696
|
+
const triggerEvidence = await resolveForkTriggerEvidence(ctx, {
|
|
1697
|
+
parentNodeId: args.parentNodeId,
|
|
1698
|
+
parent,
|
|
1699
|
+
triggeringEvidenceId: args.triggeringEvidenceId,
|
|
1700
|
+
forkMode
|
|
1701
|
+
});
|
|
1702
|
+
const parentLifecycleStatus = resolveBeliefLifecycleStatus({
|
|
1703
|
+
beliefStatus: parent.beliefStatus,
|
|
1704
|
+
confidence: parent.confidence,
|
|
1705
|
+
predictionMeta: parent.predictionMeta,
|
|
1706
|
+
metadata
|
|
1707
|
+
});
|
|
1708
|
+
if (forkMode === "supersede" && parentLifecycleStatus !== "active") {
|
|
1709
|
+
throwStructuredMutationError({
|
|
1710
|
+
message: "Superseding fork requires an active parent belief. Attach evidence first so the lifecycle can promote deterministically.",
|
|
1711
|
+
status: 409,
|
|
1712
|
+
code: "CONFLICT",
|
|
1713
|
+
invariantCode: "belief.supersede_requires_active_parent",
|
|
1714
|
+
suggestion: "Attach the contradicting evidence to the parent belief, let the evidence path promote it to active, then supersede.",
|
|
1715
|
+
details: {
|
|
1716
|
+
parentNodeId: args.parentNodeId,
|
|
1717
|
+
parentLifecycleStatus,
|
|
1718
|
+
triggeringEvidenceId: args.triggeringEvidenceId
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
}
|
|
1381
1722
|
const newBeliefGlobalId = generateGlobalId();
|
|
1382
1723
|
const newNodeId = await ctx.db.insert("epistemicNodes", {
|
|
1383
1724
|
globalId: newBeliefGlobalId,
|
|
@@ -1391,6 +1732,9 @@ var forkBelief = mutation({
|
|
|
1391
1732
|
...metadata,
|
|
1392
1733
|
forkedFrom: args.parentNodeId,
|
|
1393
1734
|
forkReason: args.forkReason,
|
|
1735
|
+
forkMode,
|
|
1736
|
+
triggeringEvidenceId: args.triggeringEvidenceId,
|
|
1737
|
+
triggeringEvidenceRelation: triggerEvidence.relation,
|
|
1394
1738
|
forkTimestamp: now,
|
|
1395
1739
|
forkedBy: authenticatedUserId,
|
|
1396
1740
|
status: "active",
|
|
@@ -1409,6 +1753,27 @@ var forkBelief = mutation({
|
|
|
1409
1753
|
createdAt: now,
|
|
1410
1754
|
updatedAt: now
|
|
1411
1755
|
});
|
|
1756
|
+
if (forkMode === "supersede") {
|
|
1757
|
+
await ctx.db.patch(args.parentNodeId, {
|
|
1758
|
+
status: "superseded",
|
|
1759
|
+
beliefStatus: "superseded",
|
|
1760
|
+
epistemicStatus: "superseded",
|
|
1761
|
+
supersededBy: newNodeId,
|
|
1762
|
+
updatedAt: now,
|
|
1763
|
+
metadata: {
|
|
1764
|
+
...metadata ?? {},
|
|
1765
|
+
status: "superseded",
|
|
1766
|
+
beliefStatus: "superseded",
|
|
1767
|
+
epistemicStatus: "superseded",
|
|
1768
|
+
supersededBy: String(newNodeId),
|
|
1769
|
+
supersededByEvidenceId: String(triggerEvidence.evidenceNodeId)
|
|
1770
|
+
}
|
|
1771
|
+
});
|
|
1772
|
+
await ctx.scheduler.runAfter(0, internal.neo4jSync.syncNodeToNeo4j, {
|
|
1773
|
+
nodeId: args.parentNodeId,
|
|
1774
|
+
operation: "upsert"
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1412
1777
|
const inheritedContracts = await ctx.db.query("epistemicContracts").withIndex(
|
|
1413
1778
|
"by_belief",
|
|
1414
1779
|
(q) => q.eq("beliefNodeId", args.parentNodeId)
|
|
@@ -1435,12 +1800,17 @@ var forkBelief = mutation({
|
|
|
1435
1800
|
globalId: generateGlobalId(),
|
|
1436
1801
|
fromGlobalId: newBeliefGlobalId,
|
|
1437
1802
|
toGlobalId: parent.globalId,
|
|
1438
|
-
edgeType: "supersedes",
|
|
1439
|
-
context: `Fork reason: ${args.forkReason}`,
|
|
1803
|
+
edgeType: forkMode === "supersede" ? "supersedes" : "derived_from",
|
|
1804
|
+
context: `Fork reason: ${args.forkReason}; triggering evidence: ${triggerEvidence.evidenceNodeId}`,
|
|
1440
1805
|
createdBy: authenticatedUserId,
|
|
1441
1806
|
topicId: parent.projectId ? String(parent.projectId) : void 0,
|
|
1442
1807
|
fromNodeType: "belief",
|
|
1443
|
-
toNodeType: "belief"
|
|
1808
|
+
toNodeType: "belief",
|
|
1809
|
+
metadata: {
|
|
1810
|
+
forkMode,
|
|
1811
|
+
triggeringEvidenceId: String(triggerEvidence.evidenceNodeId),
|
|
1812
|
+
triggeringEvidenceRelation: triggerEvidence.relation
|
|
1813
|
+
}
|
|
1444
1814
|
});
|
|
1445
1815
|
await scheduleEmbeddingGeneration({
|
|
1446
1816
|
ctx,
|
|
@@ -1465,6 +1835,9 @@ var forkBelief = mutation({
|
|
|
1465
1835
|
newState: {
|
|
1466
1836
|
formulation: args.newFormulation,
|
|
1467
1837
|
forkReason: args.forkReason,
|
|
1838
|
+
forkMode,
|
|
1839
|
+
triggeringEvidenceId: String(triggerEvidence.evidenceNodeId),
|
|
1840
|
+
triggeringEvidenceRelation: triggerEvidence.relation,
|
|
1468
1841
|
tupleContradicted: false
|
|
1469
1842
|
},
|
|
1470
1843
|
projectId: parent.projectId,
|
|
@@ -1499,7 +1872,14 @@ var forkBelief = mutation({
|
|
|
1499
1872
|
projectId: parent.projectId,
|
|
1500
1873
|
topicId: parent.topicId
|
|
1501
1874
|
});
|
|
1502
|
-
return {
|
|
1875
|
+
return {
|
|
1876
|
+
newNodeId,
|
|
1877
|
+
parentNodeId: args.parentNodeId,
|
|
1878
|
+
forkMode,
|
|
1879
|
+
forkReason: args.forkReason,
|
|
1880
|
+
triggeringEvidenceId: triggerEvidence.evidenceNodeId,
|
|
1881
|
+
triggeringEvidenceRelation: triggerEvidence.relation
|
|
1882
|
+
};
|
|
1503
1883
|
}
|
|
1504
1884
|
});
|
|
1505
1885
|
|