@lucern/graph-primitives 1.0.15 → 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.
- package/dist/beliefEvidenceLinks.js +144 -99
- package/dist/beliefEvidenceLinks.js.map +1 -1
- package/dist/beliefEvidenceLinks.operational.d.ts +29 -0
- package/dist/beliefEvidenceLinks.operational.js +157 -0
- package/dist/beliefEvidenceLinks.operational.js.map +1 -0
- package/dist/{beliefLifecycle-y8WLXqQj.d.ts → beliefLifecycle-CXwdDw5e.d.ts} +7 -4
- package/dist/beliefLifecycle.d.ts +1 -1
- package/dist/beliefLifecycle.js +75 -18
- package/dist/beliefLifecycle.js.map +1 -1
- package/dist/epistemicBeliefs.admin.js.map +1 -1
- package/dist/epistemicBeliefs.backfills.d.ts +1 -1
- package/dist/epistemicBeliefs.backfills.js +63 -35
- package/dist/epistemicBeliefs.backfills.js.map +1 -1
- package/dist/epistemicBeliefs.confidence.d.ts +1 -1
- package/dist/epistemicBeliefs.confidence.js +70 -41
- package/dist/epistemicBeliefs.confidence.js.map +1 -1
- package/dist/epistemicBeliefs.core.js +957 -566
- package/dist/epistemicBeliefs.core.js.map +1 -1
- package/dist/epistemicBeliefs.d.ts +2 -2
- package/dist/epistemicBeliefs.forkEvidence.d.ts +18 -0
- package/dist/epistemicBeliefs.forkEvidence.js +121 -0
- package/dist/epistemicBeliefs.forkEvidence.js.map +1 -0
- package/dist/epistemicBeliefs.helpers.d.ts +2 -2
- package/dist/epistemicBeliefs.helpers.js +60 -32
- package/dist/epistemicBeliefs.helpers.js.map +1 -1
- package/dist/epistemicBeliefs.internal.js +174 -39
- package/dist/epistemicBeliefs.internal.js.map +1 -1
- package/dist/epistemicBeliefs.js +436 -72
- package/dist/epistemicBeliefs.js.map +1 -1
- package/dist/epistemicBeliefs.lifecycle.d.ts +2 -2
- package/dist/epistemicBeliefs.lifecycle.js +75 -47
- package/dist/epistemicBeliefs.lifecycle.js.map +1 -1
- package/dist/epistemicBeliefs.links.js +46 -1
- package/dist/epistemicBeliefs.links.js.map +1 -1
- package/dist/epistemicBeliefs.topicAnchor.d.ts +29 -0
- package/dist/epistemicBeliefs.topicAnchor.js +105 -0
- package/dist/epistemicBeliefs.topicAnchor.js.map +1 -0
- package/dist/epistemicContracts.evaluators.js +71 -42
- package/dist/epistemicContracts.evaluators.js.map +1 -1
- package/dist/epistemicContracts.handlers.js +71 -42
- package/dist/epistemicContracts.handlers.js.map +1 -1
- package/dist/epistemicContracts.js +71 -42
- package/dist/epistemicContracts.js.map +1 -1
- package/dist/epistemicContracts.metrics.js +1 -1
- package/dist/epistemicContracts.metrics.js.map +1 -1
- package/dist/epistemicContracts.types.d.ts +1 -1
- package/dist/epistemicEdges.helpers.d.ts +1 -1
- package/dist/epistemicEvidence.js +172 -81
- package/dist/epistemicEvidence.js.map +1 -1
- package/dist/epistemicEvidenceMutations.js +172 -81
- package/dist/epistemicEvidenceMutations.js.map +1 -1
- package/dist/epistemicNodes.internal.js.map +1 -1
- package/dist/epistemicNodes.js +2 -2
- package/dist/epistemicNodes.js.map +1 -1
- package/dist/epistemicNodes.mutations.js +2 -2
- package/dist/epistemicNodes.mutations.js.map +1 -1
- package/dist/evaluators/index.js +1 -1
- package/dist/evaluators/index.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +767 -236
- package/dist/index.js.map +1 -1
- package/dist/invariantEnforcement.js +2 -2
- package/dist/invariantEnforcement.js.map +1 -1
- package/dist/proof-attestation.json +3 -3
- package/package.json +4 -4
|
@@ -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/
|
|
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
|
|
72
|
-
if (
|
|
143
|
+
function readNonEmptyString(value) {
|
|
144
|
+
if (typeof value !== "string") {
|
|
73
145
|
return;
|
|
74
146
|
}
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
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
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
163
|
+
return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
|
|
89
164
|
}
|
|
90
|
-
function
|
|
91
|
-
return
|
|
92
|
-
const depthA = a.depth ?? 9999;
|
|
93
|
-
const depthB = b.depth ?? 9999;
|
|
94
|
-
if (depthA !== depthB) {
|
|
95
|
-
return depthA - depthB;
|
|
96
|
-
}
|
|
97
|
-
const createdA = a.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
98
|
-
const createdB = b.createdAt ?? Number.MAX_SAFE_INTEGER;
|
|
99
|
-
if (createdA !== createdB) {
|
|
100
|
-
return createdA - createdB;
|
|
101
|
-
}
|
|
102
|
-
return String(a.name || "").localeCompare(String(b.name || ""));
|
|
103
|
-
})[0];
|
|
165
|
+
function coerceVisibility(value) {
|
|
166
|
+
return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
|
|
104
167
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
133
|
-
id:
|
|
134
|
-
})
|
|
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
|
-
"[
|
|
231
|
+
"[topicProjectOverlay] Failed to resolve topic by ID query",
|
|
138
232
|
{
|
|
139
233
|
error,
|
|
140
|
-
|
|
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
|
-
|
|
152
|
-
projectId:
|
|
153
|
-
})
|
|
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
|
-
"[
|
|
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
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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 (
|
|
232
|
-
|
|
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
|
-
|
|
235
|
-
args.projectId
|
|
236
|
-
);
|
|
302
|
+
allTopics = await ctx.db.query("topics").collect();
|
|
237
303
|
} catch (error) {
|
|
238
304
|
debugGraphPrimitiveFallback(
|
|
239
|
-
"[
|
|
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
|
-
|
|
286
|
-
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
|
-
var optionalScopeArgs = {
|
|
290
|
-
projectId: v.optional(v.string()),
|
|
291
|
-
topicId: v.optional(v.string())
|
|
292
|
-
};
|
|
293
|
-
function normalizeScopeValue2(value) {
|
|
294
|
-
if (typeof value !== "string") {
|
|
295
|
-
return;
|
|
311
|
+
if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
|
|
312
|
+
allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
|
|
296
313
|
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
function throwWorkspaceIsolationError(args) {
|
|
301
|
-
const error = new Error(args.message);
|
|
302
|
-
error.status = 409;
|
|
303
|
-
error.code = "INVARIANT_VIOLATION";
|
|
304
|
-
error.invariantCode = args.invariantCode;
|
|
305
|
-
error.suggestion = args.suggestion;
|
|
306
|
-
error.details = args.details;
|
|
307
|
-
throw error;
|
|
314
|
+
return allTopics.filter(
|
|
315
|
+
(topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
|
|
316
|
+
).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
|
|
308
317
|
}
|
|
309
|
-
function
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
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
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
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
|
|
402
|
+
return materializeTopicProjectOverlay({
|
|
403
|
+
...topic,
|
|
404
|
+
...patch,
|
|
405
|
+
metadata: nextMetadata
|
|
406
|
+
});
|
|
352
407
|
}
|
|
353
408
|
|
|
354
|
-
// src/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
364
|
-
if (
|
|
365
|
-
return
|
|
416
|
+
function getErrorMessage2(error) {
|
|
417
|
+
if (error instanceof Error) {
|
|
418
|
+
return error.message;
|
|
366
419
|
}
|
|
367
|
-
|
|
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
|
|
370
|
-
|
|
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
|
|
373
|
-
|
|
374
|
-
|
|
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
|
|
379
|
-
return
|
|
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
|
-
|
|
382
|
-
|
|
460
|
+
var resolverOverrides = {};
|
|
461
|
+
function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
462
|
+
return {
|
|
463
|
+
...defaultResolvers(),
|
|
464
|
+
...resolverOverrides
|
|
465
|
+
};
|
|
383
466
|
}
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (
|
|
387
|
-
return
|
|
467
|
+
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
468
|
+
function asMappedProjectId(topic) {
|
|
469
|
+
if (!topic) {
|
|
470
|
+
return;
|
|
388
471
|
}
|
|
389
|
-
|
|
390
|
-
|
|
472
|
+
const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
|
|
473
|
+
if (directLegacyProjectId) {
|
|
474
|
+
return directLegacyProjectId;
|
|
391
475
|
}
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
const metadata = readMetadata(topic);
|
|
396
|
-
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
397
|
-
}
|
|
398
|
-
function isMissingLucernChildComponentError(error) {
|
|
399
|
-
const message = getErrorMessage(error);
|
|
400
|
-
return message.includes(
|
|
401
|
-
'Child component ComponentName(Identifier("lucern")) not found'
|
|
402
|
-
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
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
|
|
405
|
-
if (
|
|
406
|
-
return
|
|
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
|
-
|
|
484
|
+
const normalized = value.trim();
|
|
485
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
412
486
|
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
437
|
-
id:
|
|
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
|
-
"[
|
|
534
|
+
"[topicScope] Failed to resolve topic by host query",
|
|
445
535
|
{
|
|
446
536
|
error,
|
|
447
|
-
|
|
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
|
-
|
|
453
|
-
projectId:
|
|
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
|
-
"[
|
|
461
|
-
{
|
|
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
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
507
|
-
|
|
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
|
|
581
|
+
return { tenantId, workspaceId };
|
|
510
582
|
}
|
|
511
|
-
async function
|
|
512
|
-
|
|
513
|
-
|
|
583
|
+
async function resolveTopicProjectScope(ctx, args) {
|
|
584
|
+
if (args.topicId) {
|
|
585
|
+
let topic = null;
|
|
514
586
|
try {
|
|
515
|
-
|
|
587
|
+
topic = await ctx.db.get(
|
|
588
|
+
args.topicId
|
|
589
|
+
);
|
|
516
590
|
} catch (error) {
|
|
517
591
|
debugGraphPrimitiveFallback(
|
|
518
|
-
"[
|
|
519
|
-
{
|
|
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
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
case "projectId":
|
|
545
|
-
case "topicId":
|
|
546
|
-
case "legacyProjectId":
|
|
547
|
-
case "storageProjectId":
|
|
548
|
-
break;
|
|
549
|
-
case "name":
|
|
550
|
-
case "description":
|
|
551
|
-
patch[key] = rawValue;
|
|
552
|
-
topicUpdateArgs[key] = rawValue;
|
|
553
|
-
break;
|
|
554
|
-
case "tenantId":
|
|
555
|
-
case "workspaceId":
|
|
556
|
-
case "ownerId":
|
|
557
|
-
throw new Error(
|
|
558
|
-
`patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
|
|
559
|
-
);
|
|
560
|
-
case "status": {
|
|
561
|
-
const status = coerceStatus(rawValue);
|
|
562
|
-
if (status) {
|
|
563
|
-
patch.status = status;
|
|
564
|
-
topicUpdateArgs.status = status;
|
|
565
|
-
}
|
|
566
|
-
break;
|
|
567
|
-
}
|
|
568
|
-
case "visibility": {
|
|
569
|
-
const visibility = coerceVisibility(rawValue);
|
|
570
|
-
if (visibility) {
|
|
571
|
-
patch.visibility = visibility;
|
|
572
|
-
topicUpdateArgs.visibility = visibility;
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
}
|
|
576
|
-
case "type": {
|
|
577
|
-
const projectType = readNonEmptyString(rawValue);
|
|
578
|
-
if (projectType) {
|
|
579
|
-
nextMetadata.projectType = projectType;
|
|
580
|
-
} else {
|
|
581
|
-
delete nextMetadata.projectType;
|
|
582
|
-
}
|
|
583
|
-
break;
|
|
584
|
-
}
|
|
585
|
-
case "updatedAt":
|
|
586
|
-
case "createdAt":
|
|
587
|
-
break;
|
|
588
|
-
default:
|
|
589
|
-
if (rawValue === void 0) {
|
|
590
|
-
delete nextMetadata[key];
|
|
591
|
-
} else {
|
|
592
|
-
nextMetadata[key] = rawValue;
|
|
593
|
-
}
|
|
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
|
-
|
|
597
|
-
|
|
598
|
-
topicUpdateArgs.metadata = nextMetadata;
|
|
599
|
-
if (typeof ctx.runMutation === "function") {
|
|
628
|
+
if (args.projectId) {
|
|
629
|
+
let directTopic = null;
|
|
600
630
|
try {
|
|
601
|
-
await ctx.
|
|
631
|
+
directTopic = await ctx.db.get(
|
|
632
|
+
args.projectId
|
|
633
|
+
);
|
|
602
634
|
} catch (error) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
-
|
|
679
|
+
`Legacy project scope ${String(args.projectId)} has no mapped topic.`
|
|
613
680
|
);
|
|
614
681
|
}
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
metadata: nextMetadata
|
|
619
|
-
});
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// src/resolvers.ts
|
|
623
|
-
function isMissingLucernChildComponentError2(error) {
|
|
624
|
-
const message = getErrorMessage2(error);
|
|
625
|
-
return message.includes(
|
|
626
|
-
'Child component ComponentName(Identifier("lucern")) not found'
|
|
627
|
-
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
682
|
+
throw new Error(
|
|
683
|
+
"Missing scope: provide topicId (preferred) or legacy projectId alias."
|
|
684
|
+
);
|
|
628
685
|
}
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
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
|
-
|
|
694
|
+
const normalized = value.trim();
|
|
695
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
637
696
|
}
|
|
638
|
-
function
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
if (!isAdvisoryTopicPatch(value) || !isMissingLucernChildComponentError2(error)) {
|
|
648
|
-
throw error;
|
|
649
|
-
}
|
|
650
|
-
console.warn(
|
|
651
|
-
"[lucern graph-primitives] Non-fatal advisory topic patch failure",
|
|
652
|
-
{
|
|
653
|
-
projectId,
|
|
654
|
-
keys: Object.keys(value),
|
|
655
|
-
error: getErrorMessage2(error)
|
|
656
|
-
}
|
|
657
|
-
);
|
|
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
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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 ?? "
|
|
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
|
|
908
|
-
|
|
909
|
-
|
|
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:
|
|
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
|
|
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: "
|
|
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 {
|
|
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
|
|