@lucern/graph-primitives 1.0.16 → 1.0.17

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