@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.
Files changed (92) 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/entityLifecycle.js +1 -12
  11. package/dist/entityLifecycle.js.map +1 -1
  12. package/dist/epistemicAnswers.js +1 -12
  13. package/dist/epistemicAnswers.js.map +1 -1
  14. package/dist/epistemicBeliefs.admin.js.map +1 -1
  15. package/dist/epistemicBeliefs.backfills.d.ts +1 -1
  16. package/dist/epistemicBeliefs.backfills.js +63 -35
  17. package/dist/epistemicBeliefs.backfills.js.map +1 -1
  18. package/dist/epistemicBeliefs.confidence.d.ts +1 -1
  19. package/dist/epistemicBeliefs.confidence.js +70 -41
  20. package/dist/epistemicBeliefs.confidence.js.map +1 -1
  21. package/dist/epistemicBeliefs.core.js +946 -566
  22. package/dist/epistemicBeliefs.core.js.map +1 -1
  23. package/dist/epistemicBeliefs.d.ts +2 -2
  24. package/dist/epistemicBeliefs.forkEvidence.d.ts +18 -0
  25. package/dist/epistemicBeliefs.forkEvidence.js +121 -0
  26. package/dist/epistemicBeliefs.forkEvidence.js.map +1 -0
  27. package/dist/epistemicBeliefs.helpers.d.ts +2 -2
  28. package/dist/epistemicBeliefs.helpers.js +60 -32
  29. package/dist/epistemicBeliefs.helpers.js.map +1 -1
  30. package/dist/epistemicBeliefs.internal.js +175 -51
  31. package/dist/epistemicBeliefs.internal.js.map +1 -1
  32. package/dist/epistemicBeliefs.js +437 -84
  33. package/dist/epistemicBeliefs.js.map +1 -1
  34. package/dist/epistemicBeliefs.lifecycle.d.ts +2 -2
  35. package/dist/epistemicBeliefs.lifecycle.js +75 -47
  36. package/dist/epistemicBeliefs.lifecycle.js.map +1 -1
  37. package/dist/epistemicBeliefs.links.js +47 -13
  38. package/dist/epistemicBeliefs.links.js.map +1 -1
  39. package/dist/epistemicBeliefs.topicAnchor.d.ts +29 -0
  40. package/dist/epistemicBeliefs.topicAnchor.js +105 -0
  41. package/dist/epistemicBeliefs.topicAnchor.js.map +1 -0
  42. package/dist/epistemicContracts.evaluators.js +71 -42
  43. package/dist/epistemicContracts.evaluators.js.map +1 -1
  44. package/dist/epistemicContracts.handlers.js +72 -54
  45. package/dist/epistemicContracts.handlers.js.map +1 -1
  46. package/dist/epistemicContracts.js +72 -54
  47. package/dist/epistemicContracts.js.map +1 -1
  48. package/dist/epistemicContracts.metrics.js +1 -1
  49. package/dist/epistemicContracts.metrics.js.map +1 -1
  50. package/dist/epistemicContracts.types.d.ts +1 -1
  51. package/dist/epistemicEdgeCreation.js +1 -12
  52. package/dist/epistemicEdgeCreation.js.map +1 -1
  53. package/dist/epistemicEdges.helpers.d.ts +1 -1
  54. package/dist/epistemicEvidence.js +173 -93
  55. package/dist/epistemicEvidence.js.map +1 -1
  56. package/dist/epistemicEvidenceMutations.js +173 -93
  57. package/dist/epistemicEvidenceMutations.js.map +1 -1
  58. package/dist/epistemicHelpers.js +1 -12
  59. package/dist/epistemicHelpers.js.map +1 -1
  60. package/dist/epistemicNodeCreation.js +1 -10
  61. package/dist/epistemicNodeCreation.js.map +1 -1
  62. package/dist/epistemicNodes.internal.js.map +1 -1
  63. package/dist/epistemicNodes.js +2 -2
  64. package/dist/epistemicNodes.js.map +1 -1
  65. package/dist/epistemicNodes.mutations.js +2 -2
  66. package/dist/epistemicNodes.mutations.js.map +1 -1
  67. package/dist/epistemicQuestions.create.js +1 -12
  68. package/dist/epistemicQuestions.create.js.map +1 -1
  69. package/dist/epistemicQuestions.evidence.js +1 -12
  70. package/dist/epistemicQuestions.evidence.js.map +1 -1
  71. package/dist/epistemicQuestions.js +1 -12
  72. package/dist/epistemicQuestions.js.map +1 -1
  73. package/dist/epistemicQuestions.tail.js +1 -12
  74. package/dist/epistemicQuestions.tail.js.map +1 -1
  75. package/dist/epistemicSources.js +1 -12
  76. package/dist/epistemicSources.js.map +1 -1
  77. package/dist/evaluators/index.js +1 -1
  78. package/dist/evaluators/index.js.map +1 -1
  79. package/dist/globalId-4y9SPpC_.d.ts +10 -0
  80. package/dist/globalId.d.ts +1 -1
  81. package/dist/globalId.js +1 -13
  82. package/dist/globalId.js.map +1 -1
  83. package/dist/helpers.js +1 -12
  84. package/dist/helpers.js.map +1 -1
  85. package/dist/index.d.ts +4 -3
  86. package/dist/index.js +771 -247
  87. package/dist/index.js.map +1 -1
  88. package/dist/invariantEnforcement.js +2 -2
  89. package/dist/invariantEnforcement.js.map +1 -1
  90. package/dist/proof-attestation.json +3 -3
  91. package/package.json +4 -4
  92. 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/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
- }
142
+ // src/topicProjectOverlay.ts
70
143
  var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
71
- function asMappedProjectId(topic) {
72
- if (!topic) {
144
+ function readNonEmptyString(value) {
145
+ if (typeof value !== "string") {
73
146
  return;
74
147
  }
75
- const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD]);
76
- if (directLegacyProjectId) {
77
- return directLegacyProjectId;
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
- const metadata = topic.metadata || {};
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 normalizeScopeValue(value) {
84
- if (typeof value !== "string") {
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
- const normalized = value.trim();
88
- return normalized.length > 0 ? normalized : void 0;
164
+ return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
89
165
  }
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];
166
+ function coerceVisibility(value) {
167
+ return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
104
168
  }
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,
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
- 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
- });
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
- return await ctx.runQuery(api.topics.get, {
133
- id: topicId
134
- }) ?? null;
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
- "[topicScope] Failed to resolve topic by host query",
232
+ "[topicProjectOverlay] Failed to resolve topic by ID query",
138
233
  {
139
234
  error,
140
- topicId
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
- return await ctx.runQuery(api.topics.getByLegacyScopeId, {
152
- projectId: legacyScopeId
153
- }) ?? null;
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
- "[topicScope] Failed to resolve topic by legacy scope",
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 { tenantId, workspaceId };
252
+ return null;
185
253
  }
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
- };
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 (args.projectId) {
232
- let directTopic = null;
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
- directTopic = await ctx.db.get(
235
- args.projectId
236
- );
303
+ allTopics = await ctx.db.query("topics").collect();
237
304
  } catch (error) {
238
305
  debugGraphPrimitiveFallback(
239
- "[topicScope] Failed to load direct project topic",
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
- 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;
312
+ if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
313
+ allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
296
314
  }
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;
315
+ return allTopics.filter(
316
+ (topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
317
+ ).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
308
318
  }
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;
319
+ async function patchTopicProjectOverlay(ctx, scopeId, value) {
320
+ const topic = await resolveTopicDoc(ctx, scopeId);
321
+ if (!topic) {
322
+ return null;
317
323
  }
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
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
- if (!scopeWorkspaceId) {
349
- return nodeWorkspaceId === void 0;
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 scopeWorkspaceId === nodeWorkspaceId;
403
+ return materializeTopicProjectOverlay({
404
+ ...topic,
405
+ ...patch,
406
+ metadata: nextMetadata
407
+ });
352
408
  }
353
409
 
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;
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 readStringArray(value) {
364
- if (!Array.isArray(value)) {
365
- return [];
417
+ function getErrorMessage2(error) {
418
+ if (error instanceof Error) {
419
+ return error.message;
366
420
  }
367
- return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
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 readMetadata(topic) {
370
- return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
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 readLegacyProjectId(value) {
373
- if (!value) {
374
- return;
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 coerceVisibility(value) {
379
- return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
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
- function coerceStatus(value) {
382
- return value === "active" || value === "archived" || value === "watching" ? value : void 0;
461
+ var resolverOverrides = {};
462
+ function resolveGraphPrimitivesAppResolvers(_ctx) {
463
+ return {
464
+ ...defaultResolvers(),
465
+ ...resolverOverrides
466
+ };
383
467
  }
384
- function mapProjectType(topic, metadata) {
385
- const explicit = readNonEmptyString(metadata.projectType);
386
- if (explicit) {
387
- return explicit;
468
+ var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
469
+ function asMappedProjectId(topic) {
470
+ if (!topic) {
471
+ return;
388
472
  }
389
- if (topic.type === "theme") {
390
- return "thematic";
473
+ const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
474
+ if (directLegacyProjectId) {
475
+ return directLegacyProjectId;
391
476
  }
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");
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 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;
481
+ function normalizeScopeValue(value) {
482
+ if (typeof value !== "string") {
483
+ return;
410
484
  }
411
- return "unknown error";
485
+ const normalized = value.trim();
486
+ return normalized.length > 0 ? normalized : void 0;
412
487
  }
413
- async function resolveTopicDoc(ctx, scopeId) {
414
- if (ctx?.db && typeof ctx.db.get === "function") {
415
- try {
416
- const directTopic = await ctx.db.get(
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
- } catch (error) {
423
- debugGraphPrimitiveFallback(
424
- "[topicProjectOverlay] Failed to resolve topic by direct ID",
425
- {
426
- error,
427
- scopeId
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
- 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
- }
530
+ return await ctx.runQuery(api.topics.get, {
531
+ id: topicId
532
+ }) ?? null;
442
533
  } catch (error) {
443
534
  debugGraphPrimitiveFallback(
444
- "[topicProjectOverlay] Failed to resolve topic by ID query",
535
+ "[topicScope] Failed to resolve topic by host query",
445
536
  {
446
537
  error,
447
- scopeId
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
- 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
- }
549
+ return await ctx.runQuery(api.topics.getByLegacyScopeId, {
550
+ projectId: legacyScopeId
551
+ }) ?? null;
458
552
  } catch (error) {
459
553
  debugGraphPrimitiveFallback(
460
- "[topicProjectOverlay] Failed to resolve topic by legacy scope ID",
461
- { error, scopeId }
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 resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
502
- const topic = await resolveTopicDoc(ctx, scopeId);
503
- if (!topic) {
504
- return null;
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
- if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
507
- return null;
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 materializeTopicProjectOverlay(topic, options.idMode);
582
+ return { tenantId, workspaceId };
510
583
  }
511
- async function listTopicProjectOverlays(ctx, options = {}) {
512
- let allTopics = [];
513
- if (ctx?.db?.query && typeof ctx.db.query === "function") {
584
+ async function resolveTopicProjectScope(ctx, args) {
585
+ if (args.topicId) {
586
+ let topic = null;
514
587
  try {
515
- allTopics = await ctx.db.query("topics").collect();
588
+ topic = await ctx.db.get(
589
+ args.topicId
590
+ );
516
591
  } catch (error) {
517
592
  debugGraphPrimitiveFallback(
518
- "[topicProjectOverlay] Failed to read topics table; falling back to API",
519
- { error }
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
- 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
- }
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
- patch.updatedAt = Date.now();
597
- patch.metadata = nextMetadata;
598
- topicUpdateArgs.metadata = nextMetadata;
599
- if (typeof ctx.runMutation === "function") {
629
+ if (args.projectId) {
630
+ let directTopic = null;
600
631
  try {
601
- await ctx.runMutation(api.topics.update, topicUpdateArgs);
632
+ directTopic = await ctx.db.get(
633
+ args.projectId
634
+ );
602
635
  } 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);
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
- "Cannot patch topic without component adapter (ctx.runMutation unavailable)"
680
+ `Legacy project scope ${String(args.projectId)} has no mapped topic.`
613
681
  );
614
682
  }
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");
683
+ throw new Error(
684
+ "Missing scope: provide topicId (preferred) or legacy projectId alias."
685
+ );
628
686
  }
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;
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
- return "unknown error";
695
+ const normalized = value.trim();
696
+ return normalized.length > 0 ? normalized : void 0;
637
697
  }
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));
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
- 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
- );
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 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
- };
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 ?? "manual_assessment",
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 scope = await resolveTopicProjectScope(ctx, {
908
- topicId: args.topicId,
909
- projectId: args.projectId
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: topic?.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 worktree completion via modulateConfidence()
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: "manual_assessment"
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 { newNodeId, parentNodeId: args.parentNodeId };
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