@lucern/graph-sync 0.3.0-alpha.10
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/CHANGELOG.md +13 -0
- package/README.md +121 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +3249 -0
- package/dist/index.js.map +1 -0
- package/dist/neo4jDriver.d.ts +95 -0
- package/dist/neo4jDriver.js +343 -0
- package/dist/neo4jDriver.js.map +1 -0
- package/dist/neo4jEdgeAPI.d.ts +10 -0
- package/dist/neo4jEdgeAPI.js +523 -0
- package/dist/neo4jEdgeAPI.js.map +1 -0
- package/dist/neo4jQueries-eF7YNiqS.d.ts +297 -0
- package/dist/neo4jQueries.d.ts +1 -0
- package/dist/neo4jQueries.js +1327 -0
- package/dist/neo4jQueries.js.map +1 -0
- package/dist/neo4jQueryRoute.d.ts +20 -0
- package/dist/neo4jQueryRoute.js +187 -0
- package/dist/neo4jQueryRoute.js.map +1 -0
- package/dist/neo4jSync.d.ts +29 -0
- package/dist/neo4jSync.js +1000 -0
- package/dist/neo4jSync.js.map +1 -0
- package/dist/neo4jSyncHelpers-vxe1-Gvw.d.ts +58 -0
- package/dist/neo4jSyncHelpers.d.ts +1 -0
- package/dist/neo4jSyncHelpers.js +332 -0
- package/dist/neo4jSyncHelpers.js.map +1 -0
- package/package.json +62 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3249 @@
|
|
|
1
|
+
import neo4j from 'neo4j-driver';
|
|
2
|
+
import { v } from 'convex/values';
|
|
3
|
+
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
4
|
+
import { NODE_TYPE_TO_LABEL, EDGE_TYPE_TO_REL, getNeo4jRelType } from '@lucern/graph-primitives/graphTypes';
|
|
5
|
+
import { componentsGeneric, anyApi, internalActionGeneric, actionGeneric, internalMutationGeneric, internalQueryGeneric } from 'convex/server';
|
|
6
|
+
|
|
7
|
+
var __defProp = Object.defineProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// src/neo4jDriver.ts
|
|
14
|
+
var neo4jDriver_exports = {};
|
|
15
|
+
__export(neo4jDriver_exports, {
|
|
16
|
+
COMPLEX_QUERY_TIMEOUT_MS: () => COMPLEX_QUERY_TIMEOUT_MS,
|
|
17
|
+
DEFAULT_QUERY_TIMEOUT_MS: () => DEFAULT_QUERY_TIMEOUT_MS,
|
|
18
|
+
batchUpsertEdges: () => batchUpsertEdges,
|
|
19
|
+
batchUpsertNodes: () => batchUpsertNodes,
|
|
20
|
+
closeDriver: () => closeDriver,
|
|
21
|
+
deleteEdge: () => deleteEdge,
|
|
22
|
+
deleteNode: () => deleteNode,
|
|
23
|
+
getConnectionInfo: () => getConnectionInfo,
|
|
24
|
+
healthCheck: () => healthCheck,
|
|
25
|
+
runBatchTransaction: () => runBatchTransaction,
|
|
26
|
+
runCypher: () => runCypher,
|
|
27
|
+
runWriteTransaction: () => runWriteTransaction,
|
|
28
|
+
upsertEdge: () => upsertEdge,
|
|
29
|
+
upsertNode: () => upsertNode,
|
|
30
|
+
validateLabel: () => validateLabel,
|
|
31
|
+
validateRelType: () => validateRelType
|
|
32
|
+
});
|
|
33
|
+
var VALID_NODE_LABELS = /* @__PURE__ */ new Set([
|
|
34
|
+
// Ontological
|
|
35
|
+
"ValueChain",
|
|
36
|
+
"Function",
|
|
37
|
+
"FinSector",
|
|
38
|
+
"Company",
|
|
39
|
+
"Person",
|
|
40
|
+
"Investor",
|
|
41
|
+
// Epistemic
|
|
42
|
+
"Theme",
|
|
43
|
+
"Belief",
|
|
44
|
+
"Question",
|
|
45
|
+
"Evidence",
|
|
46
|
+
"Source",
|
|
47
|
+
"Decision",
|
|
48
|
+
"Sprint",
|
|
49
|
+
"Claim",
|
|
50
|
+
"Synthesis",
|
|
51
|
+
"Answer"
|
|
52
|
+
]);
|
|
53
|
+
var VALID_RELATIONSHIP_TYPES = /* @__PURE__ */ new Set([
|
|
54
|
+
// Cross-layer edges
|
|
55
|
+
"EXTRACTED_FROM",
|
|
56
|
+
"ANSWERS",
|
|
57
|
+
"RESPONDS_TO",
|
|
58
|
+
"INFORMS",
|
|
59
|
+
"QUALIFIES",
|
|
60
|
+
"TESTS",
|
|
61
|
+
"EXPLORES",
|
|
62
|
+
"BASED_ON",
|
|
63
|
+
"RELATES_TO_THESIS",
|
|
64
|
+
"BELONGS_TO",
|
|
65
|
+
"PLAYS_THEME",
|
|
66
|
+
// Same-layer edges
|
|
67
|
+
"SUPERSEDES",
|
|
68
|
+
"SAME_AS",
|
|
69
|
+
"DEPENDS_ON",
|
|
70
|
+
"REINFORCES",
|
|
71
|
+
"PARENT_OF",
|
|
72
|
+
"CHILD_OF",
|
|
73
|
+
"FALSIFIED_BY",
|
|
74
|
+
"EXCLUSIVE_WITH",
|
|
75
|
+
"COLLAPSES_IF",
|
|
76
|
+
"CASCADE_FROM",
|
|
77
|
+
"STRENGTHENED_BY",
|
|
78
|
+
"WEAKENED_BY",
|
|
79
|
+
"ALTERNATIVE_TO",
|
|
80
|
+
"SUBSUMES",
|
|
81
|
+
"VALIDATED_BY",
|
|
82
|
+
"REQUIRED_FOR",
|
|
83
|
+
"PREREQUISITE_FOR",
|
|
84
|
+
"PARALLEL_TO",
|
|
85
|
+
"CORROBORATES",
|
|
86
|
+
"EXTENDS",
|
|
87
|
+
"SAME_SOURCE_AS",
|
|
88
|
+
"SAME_THEME_AS",
|
|
89
|
+
// Ontological
|
|
90
|
+
"EVALUATES",
|
|
91
|
+
"PERSPECTIVE_ON",
|
|
92
|
+
"WORKS_AT",
|
|
93
|
+
"PARTICIPATES_IN",
|
|
94
|
+
"PERFORMS",
|
|
95
|
+
"FUNCTION_IN",
|
|
96
|
+
"IMPACTS",
|
|
97
|
+
"INVESTED_IN",
|
|
98
|
+
"RAISED_FROM",
|
|
99
|
+
"BASED_ON_BELIEF",
|
|
100
|
+
"BASED_ON_QUESTION",
|
|
101
|
+
"BLOCKED_BY_CONTRADICTION",
|
|
102
|
+
"INFORMED_BY_THEME"
|
|
103
|
+
]);
|
|
104
|
+
function validateLabel(label) {
|
|
105
|
+
if (!VALID_NODE_LABELS.has(label)) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`[Neo4j Security] Invalid node label: ${label}. Must be one of: ${Array.from(VALID_NODE_LABELS).join(", ")}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function validateRelType(relType) {
|
|
112
|
+
if (!VALID_RELATIONSHIP_TYPES.has(relType)) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`[Neo4j Security] Invalid relationship type: ${relType}. Must be one of: ${Array.from(VALID_RELATIONSHIP_TYPES).join(", ")}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
var driver = null;
|
|
119
|
+
function getDriver() {
|
|
120
|
+
if (!driver) {
|
|
121
|
+
const uri = process.env.NEO4J_URI;
|
|
122
|
+
const user = process.env.NEO4J_USER;
|
|
123
|
+
const password = process.env.NEO4J_PASSWORD;
|
|
124
|
+
if (!uri || !user || !password) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"[Neo4j Driver] Missing credentials. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD via `npx convex env set`"
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
driver = neo4j.driver(uri, neo4j.auth.basic(user, password), {
|
|
130
|
+
// Connection pool settings
|
|
131
|
+
maxConnectionPoolSize: 50,
|
|
132
|
+
connectionAcquisitionTimeout: 3e4,
|
|
133
|
+
// Logging
|
|
134
|
+
logging: {
|
|
135
|
+
level: "warn",
|
|
136
|
+
logger: (level, message) => console.log(`[Neo4j ${level}] ${message}`)
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
return driver;
|
|
141
|
+
}
|
|
142
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
143
|
+
var COMPLEX_QUERY_TIMEOUT_MS = 6e4;
|
|
144
|
+
function toNeo4jParams(params) {
|
|
145
|
+
const result = {};
|
|
146
|
+
for (const [key, value] of Object.entries(params)) {
|
|
147
|
+
if (typeof value === "number" && Number.isInteger(value)) {
|
|
148
|
+
result[key] = neo4j.int(value);
|
|
149
|
+
} else if (Array.isArray(value)) {
|
|
150
|
+
result[key] = value.map(
|
|
151
|
+
(v5) => typeof v5 === "number" && Number.isInteger(v5) ? neo4j.int(v5) : v5
|
|
152
|
+
);
|
|
153
|
+
} else {
|
|
154
|
+
result[key] = value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
async function runCypher(query, params = {}, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS) {
|
|
160
|
+
const neo4jDriver = getDriver();
|
|
161
|
+
const session = neo4jDriver.session();
|
|
162
|
+
try {
|
|
163
|
+
const neo4jParams = toNeo4jParams(params);
|
|
164
|
+
const result = await session.run(query, neo4jParams, {
|
|
165
|
+
timeout: neo4j.int(timeoutMs)
|
|
166
|
+
});
|
|
167
|
+
return result.records.map((record) => {
|
|
168
|
+
const obj = {};
|
|
169
|
+
for (const key of record.keys) {
|
|
170
|
+
const field = String(key);
|
|
171
|
+
obj[field] = convertNeo4jValue(record.get(field));
|
|
172
|
+
}
|
|
173
|
+
return obj;
|
|
174
|
+
});
|
|
175
|
+
} finally {
|
|
176
|
+
await session.close();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function runWriteTransaction(query, params = {}, timeoutMs = DEFAULT_QUERY_TIMEOUT_MS) {
|
|
180
|
+
const neo4jDriver = getDriver();
|
|
181
|
+
const session = neo4jDriver.session();
|
|
182
|
+
try {
|
|
183
|
+
const neo4jParams = toNeo4jParams(params);
|
|
184
|
+
const result = await session.executeWrite(
|
|
185
|
+
async (tx) => {
|
|
186
|
+
return await tx.run(query, neo4jParams);
|
|
187
|
+
},
|
|
188
|
+
{ timeout: timeoutMs }
|
|
189
|
+
);
|
|
190
|
+
return result.records.map((record) => {
|
|
191
|
+
const obj = {};
|
|
192
|
+
for (const key of record.keys) {
|
|
193
|
+
const field = String(key);
|
|
194
|
+
obj[field] = convertNeo4jValue(record.get(field));
|
|
195
|
+
}
|
|
196
|
+
return obj;
|
|
197
|
+
});
|
|
198
|
+
} finally {
|
|
199
|
+
await session.close();
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
async function runBatchTransaction(queries, timeoutMs = COMPLEX_QUERY_TIMEOUT_MS) {
|
|
203
|
+
const neo4jDriver = getDriver();
|
|
204
|
+
const session = neo4jDriver.session();
|
|
205
|
+
try {
|
|
206
|
+
await session.executeWrite(
|
|
207
|
+
async (tx) => {
|
|
208
|
+
for (const { query, params } of queries) {
|
|
209
|
+
await tx.run(query, params);
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
{ timeout: timeoutMs }
|
|
213
|
+
);
|
|
214
|
+
} finally {
|
|
215
|
+
await session.close();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
async function upsertNode(label, globalId, properties) {
|
|
219
|
+
validateLabel(label);
|
|
220
|
+
await runWriteTransaction(
|
|
221
|
+
`
|
|
222
|
+
MERGE (n:${label} {globalId: $globalId})
|
|
223
|
+
SET n += $properties
|
|
224
|
+
SET n.updatedAt = timestamp()
|
|
225
|
+
`,
|
|
226
|
+
{ globalId, properties }
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async function deleteNode(globalId) {
|
|
230
|
+
await runWriteTransaction(
|
|
231
|
+
`
|
|
232
|
+
MATCH (n {globalId: $globalId})
|
|
233
|
+
DETACH DELETE n
|
|
234
|
+
`,
|
|
235
|
+
{ globalId }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
async function batchUpsertNodes(label, nodes) {
|
|
239
|
+
if (nodes.length === 0) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
validateLabel(label);
|
|
243
|
+
await runWriteTransaction(
|
|
244
|
+
`
|
|
245
|
+
UNWIND $nodes as node
|
|
246
|
+
MERGE (n:${label} {globalId: node.globalId})
|
|
247
|
+
SET n += node.properties
|
|
248
|
+
SET n.updatedAt = timestamp()
|
|
249
|
+
`,
|
|
250
|
+
{ nodes }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
async function upsertEdge(relType, globalId, fromGlobalId, toGlobalId, properties = {}) {
|
|
254
|
+
validateRelType(relType);
|
|
255
|
+
await runWriteTransaction(
|
|
256
|
+
`
|
|
257
|
+
MATCH (from {globalId: $fromGlobalId})
|
|
258
|
+
MATCH (to {globalId: $toGlobalId})
|
|
259
|
+
MERGE (from)-[r:${relType} {globalId: $globalId}]->(to)
|
|
260
|
+
SET r += $properties
|
|
261
|
+
SET r.updatedAt = timestamp()
|
|
262
|
+
`,
|
|
263
|
+
{ globalId, fromGlobalId, toGlobalId, properties }
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
async function deleteEdge(globalId) {
|
|
267
|
+
await runWriteTransaction(
|
|
268
|
+
`
|
|
269
|
+
MATCH ()-[r {globalId: $globalId}]->()
|
|
270
|
+
DELETE r
|
|
271
|
+
`,
|
|
272
|
+
{ globalId }
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
async function batchUpsertEdges(edges) {
|
|
276
|
+
if (edges.length === 0) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const byType = /* @__PURE__ */ new Map();
|
|
280
|
+
for (const edge of edges) {
|
|
281
|
+
const existing = byType.get(edge.relType) || [];
|
|
282
|
+
existing.push(edge);
|
|
283
|
+
byType.set(edge.relType, existing);
|
|
284
|
+
}
|
|
285
|
+
const queries = [];
|
|
286
|
+
for (const [relType, typeEdges] of byType) {
|
|
287
|
+
queries.push({
|
|
288
|
+
query: `
|
|
289
|
+
UNWIND $edges as edge
|
|
290
|
+
MATCH (from {globalId: edge.fromGlobalId})
|
|
291
|
+
MATCH (to {globalId: edge.toGlobalId})
|
|
292
|
+
MERGE (from)-[r:${relType} {globalId: edge.globalId}]->(to)
|
|
293
|
+
SET r += edge.properties
|
|
294
|
+
SET r.updatedAt = timestamp()
|
|
295
|
+
`,
|
|
296
|
+
params: {
|
|
297
|
+
edges: typeEdges.map((e) => ({
|
|
298
|
+
globalId: e.globalId,
|
|
299
|
+
fromGlobalId: e.fromGlobalId,
|
|
300
|
+
toGlobalId: e.toGlobalId,
|
|
301
|
+
properties: e.properties || {}
|
|
302
|
+
}))
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
await runBatchTransaction(queries);
|
|
307
|
+
}
|
|
308
|
+
async function healthCheck() {
|
|
309
|
+
try {
|
|
310
|
+
const result = await runCypher(
|
|
311
|
+
"MATCH (n) RETURN count(n) as count LIMIT 1"
|
|
312
|
+
);
|
|
313
|
+
return {
|
|
314
|
+
healthy: true,
|
|
315
|
+
nodeCount: result[0]?.count || 0
|
|
316
|
+
};
|
|
317
|
+
} catch (error) {
|
|
318
|
+
return {
|
|
319
|
+
healthy: false,
|
|
320
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function getConnectionInfo() {
|
|
325
|
+
return {
|
|
326
|
+
uri: process.env.NEO4J_URI,
|
|
327
|
+
user: process.env.NEO4J_USER,
|
|
328
|
+
configured: Boolean(
|
|
329
|
+
process.env.NEO4J_URI && process.env.NEO4J_USER && process.env.NEO4J_PASSWORD
|
|
330
|
+
)
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function convertNeo4jValue(value) {
|
|
334
|
+
if (value === null || value === void 0) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
if (neo4j.isInt(value)) {
|
|
338
|
+
return neo4j.integer.toNumber(value);
|
|
339
|
+
}
|
|
340
|
+
if (neo4j.isDate(value) || neo4j.isDateTime(value) || neo4j.isTime(value)) {
|
|
341
|
+
return value.toString();
|
|
342
|
+
}
|
|
343
|
+
if (Array.isArray(value)) {
|
|
344
|
+
return value.map(convertNeo4jValue);
|
|
345
|
+
}
|
|
346
|
+
if (value && typeof value === "object" && "properties" in value) {
|
|
347
|
+
const nodeObj = value;
|
|
348
|
+
const result = {};
|
|
349
|
+
for (const [k, v5] of Object.entries(nodeObj.properties)) {
|
|
350
|
+
result[k] = convertNeo4jValue(v5);
|
|
351
|
+
}
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
if (typeof value === "object") {
|
|
355
|
+
const result = {};
|
|
356
|
+
for (const [k, v5] of Object.entries(value)) {
|
|
357
|
+
result[k] = convertNeo4jValue(v5);
|
|
358
|
+
}
|
|
359
|
+
return result;
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
async function closeDriver() {
|
|
364
|
+
if (driver) {
|
|
365
|
+
await driver.close();
|
|
366
|
+
driver = null;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/neo4jEdgeAPI.ts
|
|
371
|
+
var neo4jEdgeAPI_exports = {};
|
|
372
|
+
__export(neo4jEdgeAPI_exports, {
|
|
373
|
+
DUAL_WRITE_EDGE_TYPES: () => DUAL_WRITE_EDGE_TYPES,
|
|
374
|
+
createEdge: () => createEdge,
|
|
375
|
+
deleteEdge: () => deleteEdge2,
|
|
376
|
+
getEdge: () => getEdge,
|
|
377
|
+
needsDualWrite: () => needsDualWrite,
|
|
378
|
+
retryProjectionByGlobalId: () => retryProjectionByGlobalId,
|
|
379
|
+
updateEdge: () => updateEdge
|
|
380
|
+
});
|
|
381
|
+
componentsGeneric();
|
|
382
|
+
var internal = anyApi;
|
|
383
|
+
var action = actionGeneric;
|
|
384
|
+
var internalAction = internalActionGeneric;
|
|
385
|
+
var internalMutation = internalMutationGeneric;
|
|
386
|
+
var internalQuery = internalQueryGeneric;
|
|
387
|
+
|
|
388
|
+
// src/neo4jEdgeAPI.ts
|
|
389
|
+
var DUAL_WRITE_EDGE_TYPES = [
|
|
390
|
+
"supports",
|
|
391
|
+
"informs",
|
|
392
|
+
"tests",
|
|
393
|
+
"depends_on",
|
|
394
|
+
"derived_from",
|
|
395
|
+
"contains",
|
|
396
|
+
"supersedes",
|
|
397
|
+
"extracted_from",
|
|
398
|
+
"responds_to",
|
|
399
|
+
"based_on",
|
|
400
|
+
"answers",
|
|
401
|
+
"belongs_to",
|
|
402
|
+
"relates_to_thesis",
|
|
403
|
+
"corroborates",
|
|
404
|
+
"extends",
|
|
405
|
+
"same_source_as",
|
|
406
|
+
"same_theme_as",
|
|
407
|
+
"plays_theme",
|
|
408
|
+
"impacts",
|
|
409
|
+
"evaluates",
|
|
410
|
+
"mentioned_in",
|
|
411
|
+
"perspective_on"
|
|
412
|
+
];
|
|
413
|
+
function needsDualWrite(edgeType) {
|
|
414
|
+
return DUAL_WRITE_EDGE_TYPES.includes(edgeType);
|
|
415
|
+
}
|
|
416
|
+
function normalizeEdgeType(edgeType) {
|
|
417
|
+
const normalized = edgeType.trim().toLowerCase();
|
|
418
|
+
if (!/^[a-z0-9_]+$/u.test(normalized)) {
|
|
419
|
+
throw new Error(`[Neo4j Edge API] Invalid edge type: ${edgeType}`);
|
|
420
|
+
}
|
|
421
|
+
return normalized;
|
|
422
|
+
}
|
|
423
|
+
function resolveRelationshipType(edgeType) {
|
|
424
|
+
const relType = getNeo4jRelType(edgeType);
|
|
425
|
+
if (!/^[A-Z0-9_]+$/u.test(relType)) {
|
|
426
|
+
throw new Error(`[Neo4j Edge API] Invalid relationship type: ${relType}`);
|
|
427
|
+
}
|
|
428
|
+
return relType;
|
|
429
|
+
}
|
|
430
|
+
function readStringProperty(source, key) {
|
|
431
|
+
const value = source?.[key];
|
|
432
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
|
|
433
|
+
}
|
|
434
|
+
function readNumberProperty(source, key) {
|
|
435
|
+
const value = source?.[key];
|
|
436
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
437
|
+
}
|
|
438
|
+
function metadataSummary(metadata) {
|
|
439
|
+
if (!metadata) {
|
|
440
|
+
return void 0;
|
|
441
|
+
}
|
|
442
|
+
return Object.entries(metadata).map(([key, value]) => `${key}=${String(value)}`).slice(0, 8).join(" | ");
|
|
443
|
+
}
|
|
444
|
+
async function mirrorEdgeToConvex(ctx, args) {
|
|
445
|
+
await ctx.runMutation(internal.epistemicEdges.mirrorEdgeToConvex, args);
|
|
446
|
+
}
|
|
447
|
+
async function queueEdgeRetry(ctx, args) {
|
|
448
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.queueForRetry, {
|
|
449
|
+
entityType: "edge",
|
|
450
|
+
entityId: args.globalId,
|
|
451
|
+
operation: args.operation,
|
|
452
|
+
error: args.error
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
var createEdge = internalAction({
|
|
456
|
+
args: {
|
|
457
|
+
globalId: v.string(),
|
|
458
|
+
fromGlobalId: v.string(),
|
|
459
|
+
toGlobalId: v.string(),
|
|
460
|
+
edgeType: v.string(),
|
|
461
|
+
weight: v.optional(v.number()),
|
|
462
|
+
confidence: v.optional(v.number()),
|
|
463
|
+
context: v.optional(v.string()),
|
|
464
|
+
derivationType: v.optional(v.string()),
|
|
465
|
+
metadata: v.optional(v.any()),
|
|
466
|
+
createdBy: v.string(),
|
|
467
|
+
topicId: v.optional(v.string()),
|
|
468
|
+
tenantId: v.optional(v.string()),
|
|
469
|
+
workspaceId: v.optional(v.string()),
|
|
470
|
+
fromLayer: v.optional(v.string()),
|
|
471
|
+
toLayer: v.optional(v.string()),
|
|
472
|
+
fromNodeType: v.optional(v.string()),
|
|
473
|
+
toNodeType: v.optional(v.string()),
|
|
474
|
+
reasoningMethod: v.optional(v.string()),
|
|
475
|
+
logicalRole: v.optional(v.string()),
|
|
476
|
+
temporalClass: v.optional(v.string()),
|
|
477
|
+
validFrom: v.optional(v.number()),
|
|
478
|
+
validUntil: v.optional(v.number())
|
|
479
|
+
},
|
|
480
|
+
returns: permissiveReturn,
|
|
481
|
+
handler: async (ctx, args) => {
|
|
482
|
+
const connInfo = getConnectionInfo();
|
|
483
|
+
if (!connInfo.configured) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
"[Neo4j Edge API] Neo4j not configured. Set NEO4J_URI, NEO4J_USER, NEO4J_PASSWORD"
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
const edgeType = normalizeEdgeType(args.edgeType);
|
|
489
|
+
const relType = resolveRelationshipType(edgeType);
|
|
490
|
+
const metadata = args.metadata && typeof args.metadata === "object" ? args.metadata : void 0;
|
|
491
|
+
const now = Date.now();
|
|
492
|
+
const properties = {
|
|
493
|
+
globalId: args.globalId,
|
|
494
|
+
edgeType,
|
|
495
|
+
weight: args.weight ?? 1,
|
|
496
|
+
confidence: args.confidence ?? readNumberProperty(metadata, "confidence") ?? 1,
|
|
497
|
+
context: args.context ?? metadataSummary(metadata) ?? "",
|
|
498
|
+
derivationType: args.derivationType ?? "",
|
|
499
|
+
createdBy: args.createdBy,
|
|
500
|
+
createdAt: now,
|
|
501
|
+
updatedAt: now,
|
|
502
|
+
topicId: args.topicId ?? readStringProperty(metadata, "topicId") ?? "",
|
|
503
|
+
tenantId: args.tenantId ?? readStringProperty(metadata, "tenantId") ?? "",
|
|
504
|
+
workspaceId: args.workspaceId ?? readStringProperty(metadata, "workspaceId") ?? "",
|
|
505
|
+
fromLayer: args.fromLayer ?? "",
|
|
506
|
+
toLayer: args.toLayer ?? "",
|
|
507
|
+
fromNodeType: args.fromNodeType ?? "",
|
|
508
|
+
toNodeType: args.toNodeType ?? "",
|
|
509
|
+
reasoningMethod: args.reasoningMethod ?? "",
|
|
510
|
+
logicalRole: args.logicalRole ?? "",
|
|
511
|
+
temporalClass: args.temporalClass ?? "structural",
|
|
512
|
+
validFrom: args.validFrom ?? now,
|
|
513
|
+
validUntil: args.validUntil ?? null
|
|
514
|
+
};
|
|
515
|
+
const result = await runWriteTransaction(
|
|
516
|
+
`
|
|
517
|
+
MATCH (from {globalId: $fromGlobalId})
|
|
518
|
+
MATCH (to {globalId: $toGlobalId})
|
|
519
|
+
MERGE (from)-[r:${relType} {globalId: $globalId}]->(to)
|
|
520
|
+
SET r += $properties
|
|
521
|
+
RETURN r.globalId as globalId
|
|
522
|
+
`,
|
|
523
|
+
{
|
|
524
|
+
fromGlobalId: args.fromGlobalId,
|
|
525
|
+
toGlobalId: args.toGlobalId,
|
|
526
|
+
globalId: args.globalId,
|
|
527
|
+
properties
|
|
528
|
+
}
|
|
529
|
+
);
|
|
530
|
+
if (result.length === 0) {
|
|
531
|
+
await queueEdgeRetry(ctx, {
|
|
532
|
+
globalId: args.globalId,
|
|
533
|
+
operation: "upsert",
|
|
534
|
+
error: `Source or target node not yet synced to Neo4j (from: ${args.fromGlobalId}, to: ${args.toGlobalId})`
|
|
535
|
+
});
|
|
536
|
+
if (needsDualWrite(edgeType)) {
|
|
537
|
+
try {
|
|
538
|
+
await mirrorEdgeToConvex(ctx, {
|
|
539
|
+
globalId: args.globalId,
|
|
540
|
+
fromGlobalId: args.fromGlobalId,
|
|
541
|
+
toGlobalId: args.toGlobalId,
|
|
542
|
+
edgeType,
|
|
543
|
+
weight: args.weight,
|
|
544
|
+
confidence: args.confidence,
|
|
545
|
+
context: args.context,
|
|
546
|
+
derivationType: args.derivationType,
|
|
547
|
+
createdBy: args.createdBy,
|
|
548
|
+
topicId: args.topicId,
|
|
549
|
+
fromLayer: args.fromLayer,
|
|
550
|
+
toLayer: args.toLayer,
|
|
551
|
+
fromNodeType: args.fromNodeType,
|
|
552
|
+
toNodeType: args.toNodeType,
|
|
553
|
+
reasoningMethod: args.reasoningMethod,
|
|
554
|
+
logicalRole: args.logicalRole,
|
|
555
|
+
temporalClass: args.temporalClass,
|
|
556
|
+
validFrom: args.validFrom,
|
|
557
|
+
validUntil: args.validUntil
|
|
558
|
+
});
|
|
559
|
+
} catch {
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
success: false,
|
|
564
|
+
globalId: args.globalId,
|
|
565
|
+
edgeType,
|
|
566
|
+
queuedForRetry: true,
|
|
567
|
+
reason: "nodes_not_synced"
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (needsDualWrite(edgeType)) {
|
|
571
|
+
try {
|
|
572
|
+
await mirrorEdgeToConvex(ctx, {
|
|
573
|
+
globalId: args.globalId,
|
|
574
|
+
fromGlobalId: args.fromGlobalId,
|
|
575
|
+
toGlobalId: args.toGlobalId,
|
|
576
|
+
edgeType,
|
|
577
|
+
weight: args.weight,
|
|
578
|
+
confidence: args.confidence,
|
|
579
|
+
context: args.context,
|
|
580
|
+
derivationType: args.derivationType,
|
|
581
|
+
createdBy: args.createdBy,
|
|
582
|
+
topicId: args.topicId,
|
|
583
|
+
fromLayer: args.fromLayer,
|
|
584
|
+
toLayer: args.toLayer,
|
|
585
|
+
fromNodeType: args.fromNodeType,
|
|
586
|
+
toNodeType: args.toNodeType,
|
|
587
|
+
reasoningMethod: args.reasoningMethod,
|
|
588
|
+
logicalRole: args.logicalRole,
|
|
589
|
+
temporalClass: args.temporalClass,
|
|
590
|
+
validFrom: args.validFrom,
|
|
591
|
+
validUntil: args.validUntil
|
|
592
|
+
});
|
|
593
|
+
} catch (error) {
|
|
594
|
+
await queueEdgeRetry(ctx, {
|
|
595
|
+
globalId: args.globalId,
|
|
596
|
+
operation: "upsert",
|
|
597
|
+
error: `Convex mirror failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
success: true,
|
|
603
|
+
globalId: args.globalId,
|
|
604
|
+
edgeType,
|
|
605
|
+
dualWritten: needsDualWrite(edgeType)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
var deleteEdge2 = internalAction({
|
|
610
|
+
args: {
|
|
611
|
+
globalId: v.string()
|
|
612
|
+
},
|
|
613
|
+
returns: permissiveReturn,
|
|
614
|
+
handler: async (ctx, args) => {
|
|
615
|
+
const connInfo = getConnectionInfo();
|
|
616
|
+
if (!connInfo.configured) {
|
|
617
|
+
throw new Error("[Neo4j Edge API] Neo4j not configured");
|
|
618
|
+
}
|
|
619
|
+
await runWriteTransaction(
|
|
620
|
+
`
|
|
621
|
+
MATCH ()-[r {globalId: $globalId}]->()
|
|
622
|
+
DELETE r
|
|
623
|
+
`,
|
|
624
|
+
{ globalId: args.globalId }
|
|
625
|
+
);
|
|
626
|
+
try {
|
|
627
|
+
await ctx.runMutation(internal.epistemicEdges.deleteEdgeFromConvex, {
|
|
628
|
+
globalId: args.globalId
|
|
629
|
+
});
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
return { success: true };
|
|
633
|
+
}
|
|
634
|
+
});
|
|
635
|
+
var updateEdge = internalAction({
|
|
636
|
+
args: {
|
|
637
|
+
globalId: v.string(),
|
|
638
|
+
weight: v.optional(v.number()),
|
|
639
|
+
confidence: v.optional(v.number()),
|
|
640
|
+
context: v.optional(v.string()),
|
|
641
|
+
derivationType: v.optional(v.string())
|
|
642
|
+
},
|
|
643
|
+
returns: permissiveReturn,
|
|
644
|
+
handler: async (ctx, args) => {
|
|
645
|
+
const connInfo = getConnectionInfo();
|
|
646
|
+
if (!connInfo.configured) {
|
|
647
|
+
throw new Error("[Neo4j Edge API] Neo4j not configured");
|
|
648
|
+
}
|
|
649
|
+
const updates = { updatedAt: Date.now() };
|
|
650
|
+
if (args.weight !== void 0) updates.weight = args.weight;
|
|
651
|
+
if (args.confidence !== void 0) updates.confidence = args.confidence;
|
|
652
|
+
if (args.context !== void 0) updates.context = args.context;
|
|
653
|
+
if (args.derivationType !== void 0) {
|
|
654
|
+
updates.derivationType = args.derivationType;
|
|
655
|
+
}
|
|
656
|
+
const result = await runWriteTransaction(
|
|
657
|
+
`
|
|
658
|
+
MATCH ()-[r {globalId: $globalId}]->()
|
|
659
|
+
SET r += $updates
|
|
660
|
+
RETURN true as updated, r.edgeType as edgeType
|
|
661
|
+
`,
|
|
662
|
+
{ globalId: args.globalId, updates }
|
|
663
|
+
);
|
|
664
|
+
if (result.length === 0) {
|
|
665
|
+
return { success: false, error: "Edge not found" };
|
|
666
|
+
}
|
|
667
|
+
const edgeType = result[0]?.edgeType;
|
|
668
|
+
if (edgeType && needsDualWrite(edgeType)) {
|
|
669
|
+
try {
|
|
670
|
+
await ctx.runMutation(internal.epistemicEdges.updateEdgeInConvex, {
|
|
671
|
+
globalId: args.globalId,
|
|
672
|
+
weight: args.weight,
|
|
673
|
+
confidence: args.confidence,
|
|
674
|
+
context: args.context,
|
|
675
|
+
derivationType: args.derivationType
|
|
676
|
+
});
|
|
677
|
+
} catch {
|
|
678
|
+
await queueEdgeRetry(ctx, {
|
|
679
|
+
globalId: args.globalId,
|
|
680
|
+
operation: "upsert",
|
|
681
|
+
error: "Convex mirror update failed"
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
return { success: true, globalId: args.globalId };
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
var getEdge = internalAction({
|
|
689
|
+
args: {
|
|
690
|
+
globalId: v.string()
|
|
691
|
+
},
|
|
692
|
+
returns: permissiveReturn,
|
|
693
|
+
handler: async (_ctx, args) => {
|
|
694
|
+
const connInfo = getConnectionInfo();
|
|
695
|
+
if (!connInfo.configured) {
|
|
696
|
+
return null;
|
|
697
|
+
}
|
|
698
|
+
const result = await runCypher(
|
|
699
|
+
`
|
|
700
|
+
MATCH (from)-[r {globalId: $globalId}]->(to)
|
|
701
|
+
RETURN r.globalId as globalId,
|
|
702
|
+
r.edgeType as edgeType,
|
|
703
|
+
from.globalId as fromGlobalId,
|
|
704
|
+
to.globalId as toGlobalId,
|
|
705
|
+
properties(r) as properties
|
|
706
|
+
LIMIT 1
|
|
707
|
+
`,
|
|
708
|
+
{ globalId: args.globalId }
|
|
709
|
+
);
|
|
710
|
+
return result[0] ?? null;
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
var retryProjectionByGlobalId = internalAction({
|
|
714
|
+
args: {
|
|
715
|
+
globalId: v.string()
|
|
716
|
+
},
|
|
717
|
+
returns: permissiveReturn,
|
|
718
|
+
handler: async (ctx, args) => {
|
|
719
|
+
const connInfo = getConnectionInfo();
|
|
720
|
+
if (!connInfo.configured) {
|
|
721
|
+
return { success: false, error: "[Neo4j Edge API] Neo4j not configured" };
|
|
722
|
+
}
|
|
723
|
+
const result = await runCypher(
|
|
724
|
+
`
|
|
725
|
+
MATCH (from)-[r {globalId: $globalId}]->(to)
|
|
726
|
+
RETURN r.edgeType as edgeType,
|
|
727
|
+
from.globalId as fromGlobalId,
|
|
728
|
+
to.globalId as toGlobalId,
|
|
729
|
+
properties(r) as properties
|
|
730
|
+
LIMIT 1
|
|
731
|
+
`,
|
|
732
|
+
{ globalId: args.globalId }
|
|
733
|
+
);
|
|
734
|
+
if (result.length === 0) {
|
|
735
|
+
return {
|
|
736
|
+
success: false,
|
|
737
|
+
error: `[Neo4j Edge API] Edge not found in Neo4j: ${args.globalId}`
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
const edge = result[0];
|
|
741
|
+
if (!needsDualWrite(edge.edgeType)) {
|
|
742
|
+
return {
|
|
743
|
+
success: true,
|
|
744
|
+
skipped: true,
|
|
745
|
+
reason: "edge_type_not_projected",
|
|
746
|
+
edgeType: edge.edgeType
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
const props = edge.properties || {};
|
|
750
|
+
await mirrorEdgeToConvex(ctx, {
|
|
751
|
+
globalId: args.globalId,
|
|
752
|
+
fromGlobalId: edge.fromGlobalId,
|
|
753
|
+
toGlobalId: edge.toGlobalId,
|
|
754
|
+
edgeType: edge.edgeType,
|
|
755
|
+
weight: readNumberProperty(props, "weight"),
|
|
756
|
+
confidence: readNumberProperty(props, "confidence"),
|
|
757
|
+
context: readStringProperty(props, "context"),
|
|
758
|
+
derivationType: readStringProperty(props, "derivationType"),
|
|
759
|
+
createdBy: readStringProperty(props, "createdBy") ?? "neo4j_projection_retry",
|
|
760
|
+
topicId: readStringProperty(props, "topicId"),
|
|
761
|
+
fromLayer: readStringProperty(props, "fromLayer"),
|
|
762
|
+
toLayer: readStringProperty(props, "toLayer"),
|
|
763
|
+
fromNodeType: readStringProperty(props, "fromNodeType"),
|
|
764
|
+
toNodeType: readStringProperty(props, "toNodeType"),
|
|
765
|
+
reasoningMethod: readStringProperty(props, "reasoningMethod"),
|
|
766
|
+
logicalRole: readStringProperty(props, "logicalRole"),
|
|
767
|
+
temporalClass: readStringProperty(props, "temporalClass"),
|
|
768
|
+
validFrom: readNumberProperty(props, "validFrom"),
|
|
769
|
+
validUntil: readNumberProperty(props, "validUntil")
|
|
770
|
+
});
|
|
771
|
+
return { success: true, globalId: args.globalId, projected: true };
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/neo4jQueries.ts
|
|
776
|
+
var neo4jQueries_exports = {};
|
|
777
|
+
__export(neo4jQueries_exports, {
|
|
778
|
+
findPotentialContradictions: () => findPotentialContradictions,
|
|
779
|
+
getAnchoringBiasDetection: () => getAnchoringBiasDetection,
|
|
780
|
+
getBeliefEvidenceGraph: () => getBeliefEvidenceGraph,
|
|
781
|
+
getBeliefHalfLife: () => getBeliefHalfLife,
|
|
782
|
+
getBeliefsByCompany: () => getBeliefsByCompany,
|
|
783
|
+
getBeliefsByEpistemicStatus: () => getBeliefsByEpistemicStatus,
|
|
784
|
+
getCausalChains: () => getCausalChains,
|
|
785
|
+
getChallengedBeliefs: () => getChallengedBeliefs,
|
|
786
|
+
getCompaniesByTheme: () => getCompaniesByTheme,
|
|
787
|
+
getConfirmationBiasScore: () => getConfirmationBiasScore,
|
|
788
|
+
getConnectedNodesGraph: () => getConnectedNodesGraph,
|
|
789
|
+
getContradictionTensionMap: () => getContradictionTensionMap,
|
|
790
|
+
getCrossThemeBeliefs: () => getCrossThemeBeliefs,
|
|
791
|
+
getEvidenceByCompany: () => getEvidenceByCompany,
|
|
792
|
+
getEvidenceByMethodology: () => getEvidenceByMethodology,
|
|
793
|
+
getEvidenceToBeliefPath: () => getEvidenceToBeliefPath,
|
|
794
|
+
getFalsificationQuestions: () => getFalsificationQuestions,
|
|
795
|
+
getFunctionsByValueChain: () => getFunctionsByValueChain,
|
|
796
|
+
getGraphStats: () => getGraphStats,
|
|
797
|
+
getHighPriorityQuestions: () => getHighPriorityQuestions,
|
|
798
|
+
getKnowledgeFrontier: () => getKnowledgeFrontier,
|
|
799
|
+
getMeetingPrepBrief: () => getMeetingPrepBrief,
|
|
800
|
+
getMinimumFalsificationSet: () => getMinimumFalsificationSet,
|
|
801
|
+
getMissingQuestionDetection: () => getMissingQuestionDetection,
|
|
802
|
+
getNecessaryEvidence: () => getNecessaryEvidence,
|
|
803
|
+
getNodeLineageGraph: () => getNodeLineageGraph,
|
|
804
|
+
getNodeRelationships: () => getNodeRelationships,
|
|
805
|
+
getNonConsensusBeliefs: () => getNonConsensusBeliefs,
|
|
806
|
+
getPeopleByCompany: () => getPeopleByCompany,
|
|
807
|
+
getPeopleByTheme: () => getPeopleByTheme,
|
|
808
|
+
getPortfolioConviction: () => getPortfolioConviction,
|
|
809
|
+
getPredictions: () => getPredictions,
|
|
810
|
+
getProprietaryEvidence: () => getProprietaryEvidence,
|
|
811
|
+
getProprietarySignals: () => getProprietarySignals,
|
|
812
|
+
getQuestionsByTheme: () => getQuestionsByTheme,
|
|
813
|
+
getQuestionsByValueChain: () => getQuestionsByValueChain,
|
|
814
|
+
getReasoningDepthScore: () => getReasoningDepthScore,
|
|
815
|
+
getSemanticBridges: () => getSemanticBridges,
|
|
816
|
+
getSemanticOrphans: () => getSemanticOrphans,
|
|
817
|
+
getSoftContradictions: () => getSoftContradictions,
|
|
818
|
+
getSourceConcentrationRisk: () => getSourceConcentrationRisk,
|
|
819
|
+
getStaleThemes: () => getStaleThemes,
|
|
820
|
+
getSurpriseDetection: () => getSurpriseDetection,
|
|
821
|
+
getThemeBeliefsGraph: () => getThemeBeliefsGraph,
|
|
822
|
+
getThemeStats: () => getThemeStats,
|
|
823
|
+
getThemeSubgraph: () => getThemeSubgraph,
|
|
824
|
+
getThemeValueChainCandidates: () => getThemeValueChainCandidates,
|
|
825
|
+
getThemesImpactingCompany: () => getThemesImpactingCompany,
|
|
826
|
+
getValueChainsByTheme: () => getValueChainsByTheme,
|
|
827
|
+
graphAwareSearch: () => graphAwareSearch,
|
|
828
|
+
queryGraph: () => queryGraph,
|
|
829
|
+
searchAllNodes: () => searchAllNodes,
|
|
830
|
+
semanticSearch: () => semanticSearch
|
|
831
|
+
});
|
|
832
|
+
function toInt(value, defaultValue) {
|
|
833
|
+
if (value === void 0 || value === null) {
|
|
834
|
+
return defaultValue;
|
|
835
|
+
}
|
|
836
|
+
const parsed = Number(value);
|
|
837
|
+
return Number.isFinite(parsed) ? Math.floor(parsed) : defaultValue;
|
|
838
|
+
}
|
|
839
|
+
function isAllowedProxyUrl(urlString) {
|
|
840
|
+
try {
|
|
841
|
+
const url = new URL(urlString);
|
|
842
|
+
const isLocalhost = url.hostname === "localhost" || url.hostname === "127.0.0.1";
|
|
843
|
+
if (!isLocalhost && url.protocol !== "https:") {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
if (url.username || url.password) {
|
|
847
|
+
return false;
|
|
848
|
+
}
|
|
849
|
+
const configuredHosts = (process.env.LUCERN_GRAPH_SYNC_ALLOWED_PROXY_HOSTS || "").split(",").map((host) => host.trim().toLowerCase()).filter(Boolean);
|
|
850
|
+
if (configuredHosts.length === 0) {
|
|
851
|
+
return true;
|
|
852
|
+
}
|
|
853
|
+
const hostname = url.hostname.toLowerCase();
|
|
854
|
+
return configuredHosts.some(
|
|
855
|
+
(host) => host.startsWith(".") ? hostname.endsWith(host) : hostname === host
|
|
856
|
+
);
|
|
857
|
+
} catch {
|
|
858
|
+
console.warn("[Neo4j Queries] Rejected malformed proxy URL", urlString);
|
|
859
|
+
return false;
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
function createNeo4jQueryTransportSuccess(data) {
|
|
863
|
+
return { success: true, data };
|
|
864
|
+
}
|
|
865
|
+
function createNeo4jQueryTransportFailure(error) {
|
|
866
|
+
return { success: false, error };
|
|
867
|
+
}
|
|
868
|
+
function resolveProxyBaseUrl(apiBaseUrl) {
|
|
869
|
+
const resolved = apiBaseUrl || process.env.LUCERN_GRAPH_SYNC_QUERY_BASE_URL || process.env.NEXT_PUBLIC_APP_URL;
|
|
870
|
+
const normalized = resolved?.trim();
|
|
871
|
+
return normalized ? normalized.replace(/\/+$/u, "") : null;
|
|
872
|
+
}
|
|
873
|
+
function buildProxyEndpoint(proxyBaseUrl) {
|
|
874
|
+
const endpoint = new URL(proxyBaseUrl);
|
|
875
|
+
if (!endpoint.pathname.endsWith("/api/neo4j-query")) {
|
|
876
|
+
endpoint.pathname = `${endpoint.pathname.replace(/\/+$/u, "")}/api/neo4j-query`;
|
|
877
|
+
}
|
|
878
|
+
return endpoint.toString();
|
|
879
|
+
}
|
|
880
|
+
async function callNeo4jQuery(queryName, params, apiBaseUrl) {
|
|
881
|
+
const proxyUrl = resolveProxyBaseUrl(apiBaseUrl);
|
|
882
|
+
if (!proxyUrl) {
|
|
883
|
+
return createNeo4jQueryTransportFailure(
|
|
884
|
+
"Neo4j query proxy URL not configured"
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
if (!isAllowedProxyUrl(proxyUrl)) {
|
|
888
|
+
console.error(
|
|
889
|
+
"[Neo4j Queries] Blocked request to disallowed URL:",
|
|
890
|
+
proxyUrl
|
|
891
|
+
);
|
|
892
|
+
return createNeo4jQueryTransportFailure("Invalid proxy URL");
|
|
893
|
+
}
|
|
894
|
+
const syncSecret = process.env.NEO4J_SYNC_SECRET;
|
|
895
|
+
if (!syncSecret) {
|
|
896
|
+
console.error("[Neo4j Queries] NEO4J_SYNC_SECRET not configured");
|
|
897
|
+
return createNeo4jQueryTransportFailure(
|
|
898
|
+
"Neo4j sync secret not configured"
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
try {
|
|
902
|
+
const explicitTenant = typeof params.tenantId === "string" ? params.tenantId.trim() : "";
|
|
903
|
+
const scopedTopic = typeof params.topicId === "string" ? params.topicId.trim() : "";
|
|
904
|
+
const legacyProject = typeof params.projectId === "string" ? params.projectId.trim() : "";
|
|
905
|
+
const scopedTopicId = scopedTopic || legacyProject;
|
|
906
|
+
const projectTenant = scopedTopicId ? `project:${scopedTopicId}` : "";
|
|
907
|
+
const defaultTenant = process.env.LUCERN_DEFAULT_TENANT_ID?.trim() || "";
|
|
908
|
+
const tenantId = explicitTenant || projectTenant || defaultTenant;
|
|
909
|
+
if (!tenantId) {
|
|
910
|
+
return createNeo4jQueryTransportFailure(
|
|
911
|
+
"Missing required tenant context (tenantId or topicId)"
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
const scopedParams = { ...params, tenantId };
|
|
915
|
+
const headers = {
|
|
916
|
+
"Content-Type": "application/json",
|
|
917
|
+
Authorization: `Bearer ${syncSecret}`
|
|
918
|
+
};
|
|
919
|
+
const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
|
|
920
|
+
if (bypassSecret && apiBaseUrl) {
|
|
921
|
+
headers["x-vercel-protection-bypass"] = bypassSecret;
|
|
922
|
+
}
|
|
923
|
+
const controller = new AbortController();
|
|
924
|
+
const timeoutId = setTimeout(() => controller.abort(), 1e4);
|
|
925
|
+
try {
|
|
926
|
+
const response = await fetch(buildProxyEndpoint(proxyUrl), {
|
|
927
|
+
method: "POST",
|
|
928
|
+
headers,
|
|
929
|
+
body: JSON.stringify({ queryName, params: scopedParams }),
|
|
930
|
+
signal: controller.signal
|
|
931
|
+
});
|
|
932
|
+
clearTimeout(timeoutId);
|
|
933
|
+
const result = await response.json();
|
|
934
|
+
if (!response.ok) {
|
|
935
|
+
return createNeo4jQueryTransportFailure(result.error || "Query failed");
|
|
936
|
+
}
|
|
937
|
+
return createNeo4jQueryTransportSuccess(result.data || []);
|
|
938
|
+
} catch (fetchError) {
|
|
939
|
+
clearTimeout(timeoutId);
|
|
940
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
941
|
+
console.warn("[Neo4j Queries] Request timed out for:", queryName);
|
|
942
|
+
return createNeo4jQueryTransportFailure("Query timed out (10s)");
|
|
943
|
+
}
|
|
944
|
+
throw fetchError;
|
|
945
|
+
}
|
|
946
|
+
} catch (error) {
|
|
947
|
+
console.error("[Neo4j Queries] Error in callNeo4jQuery:", queryName, error);
|
|
948
|
+
return createNeo4jQueryTransportFailure(
|
|
949
|
+
error instanceof Error ? error.message : "Network error connecting to query proxy"
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
function withTopicScope(args, params) {
|
|
954
|
+
return args.topicId ? { ...params, topicId: args.topicId } : params;
|
|
955
|
+
}
|
|
956
|
+
var getNodeLineageGraph = action({
|
|
957
|
+
args: {
|
|
958
|
+
globalId: v.string(),
|
|
959
|
+
apiBaseUrl: v.optional(v.string()),
|
|
960
|
+
topicId: v.optional(v.string())
|
|
961
|
+
},
|
|
962
|
+
returns: permissiveReturn,
|
|
963
|
+
handler: async (_ctx, args) => {
|
|
964
|
+
const result = await callNeo4jQuery(
|
|
965
|
+
"nodeLineage",
|
|
966
|
+
withTopicScope(args, {
|
|
967
|
+
globalId: args.globalId
|
|
968
|
+
}),
|
|
969
|
+
args.apiBaseUrl
|
|
970
|
+
);
|
|
971
|
+
if (!result.success) {
|
|
972
|
+
console.error("[Neo4j] Lineage query failed:", result.error);
|
|
973
|
+
return { lineage: [], error: result.error };
|
|
974
|
+
}
|
|
975
|
+
const lineage = result.data?.[0]?.lineage || [];
|
|
976
|
+
return { lineage };
|
|
977
|
+
}
|
|
978
|
+
});
|
|
979
|
+
var getConnectedNodesGraph = action({
|
|
980
|
+
args: {
|
|
981
|
+
globalId: v.string(),
|
|
982
|
+
maxHops: v.optional(v.number()),
|
|
983
|
+
limit: v.optional(v.number()),
|
|
984
|
+
apiBaseUrl: v.optional(v.string()),
|
|
985
|
+
topicId: v.optional(v.string())
|
|
986
|
+
},
|
|
987
|
+
returns: permissiveReturn,
|
|
988
|
+
handler: async (_ctx, args) => {
|
|
989
|
+
const maxHops = Math.min(toInt(args.maxHops, 2), 5);
|
|
990
|
+
const limit = toInt(args.limit, 50);
|
|
991
|
+
const result = await callNeo4jQuery(
|
|
992
|
+
"connectedNodes",
|
|
993
|
+
withTopicScope(args, {
|
|
994
|
+
globalId: args.globalId,
|
|
995
|
+
maxHops,
|
|
996
|
+
limit
|
|
997
|
+
}),
|
|
998
|
+
args.apiBaseUrl
|
|
999
|
+
);
|
|
1000
|
+
if (!result.success) {
|
|
1001
|
+
console.error("[Neo4j] Connected nodes query failed:", result.error);
|
|
1002
|
+
return { nodes: [], error: result.error };
|
|
1003
|
+
}
|
|
1004
|
+
return { nodes: result.data || [] };
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
var getThemeBeliefsGraph = action({
|
|
1008
|
+
args: {
|
|
1009
|
+
themeGlobalId: v.string()
|
|
1010
|
+
},
|
|
1011
|
+
returns: permissiveReturn,
|
|
1012
|
+
handler: async (_ctx, args) => {
|
|
1013
|
+
const result = await callNeo4jQuery("themeBeliefs", {
|
|
1014
|
+
themeGlobalId: args.themeGlobalId
|
|
1015
|
+
});
|
|
1016
|
+
if (!result.success) {
|
|
1017
|
+
return { beliefs: [], error: result.error };
|
|
1018
|
+
}
|
|
1019
|
+
return {
|
|
1020
|
+
beliefs: result.data?.map((r) => r.belief) || []
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
});
|
|
1024
|
+
var getBeliefEvidenceGraph = action({
|
|
1025
|
+
args: {
|
|
1026
|
+
beliefGlobalId: v.string(),
|
|
1027
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1028
|
+
topicId: v.optional(v.string())
|
|
1029
|
+
},
|
|
1030
|
+
returns: permissiveReturn,
|
|
1031
|
+
handler: async (_ctx, args) => {
|
|
1032
|
+
const result = await callNeo4jQuery(
|
|
1033
|
+
"beliefEvidence",
|
|
1034
|
+
withTopicScope(args, {
|
|
1035
|
+
beliefGlobalId: args.beliefGlobalId
|
|
1036
|
+
}),
|
|
1037
|
+
args.apiBaseUrl
|
|
1038
|
+
);
|
|
1039
|
+
if (!result.success) {
|
|
1040
|
+
return { evidence: [], error: result.error };
|
|
1041
|
+
}
|
|
1042
|
+
return {
|
|
1043
|
+
evidence: result.data?.map((r) => r.evidence) || []
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
var getCrossThemeBeliefs = action({
|
|
1048
|
+
args: {
|
|
1049
|
+
limit: v.optional(v.number()),
|
|
1050
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1051
|
+
topicId: v.optional(v.string())
|
|
1052
|
+
},
|
|
1053
|
+
returns: permissiveReturn,
|
|
1054
|
+
handler: async (_ctx, args) => {
|
|
1055
|
+
const result = await callNeo4jQuery(
|
|
1056
|
+
"crossThemeBeliefs",
|
|
1057
|
+
withTopicScope(args, {
|
|
1058
|
+
limit: toInt(args.limit, 20)
|
|
1059
|
+
}),
|
|
1060
|
+
args.apiBaseUrl
|
|
1061
|
+
);
|
|
1062
|
+
if (!result.success) {
|
|
1063
|
+
return { beliefs: [], error: result.error };
|
|
1064
|
+
}
|
|
1065
|
+
return {
|
|
1066
|
+
beliefs: result.data?.map((r) => r.belief) || []
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
});
|
|
1070
|
+
var findPotentialContradictions = action({
|
|
1071
|
+
args: {
|
|
1072
|
+
limit: v.optional(v.number()),
|
|
1073
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1074
|
+
topicId: v.optional(v.string())
|
|
1075
|
+
},
|
|
1076
|
+
returns: permissiveReturn,
|
|
1077
|
+
handler: async (_ctx, args) => {
|
|
1078
|
+
const result = await callNeo4jQuery(
|
|
1079
|
+
"potentialContradictions",
|
|
1080
|
+
withTopicScope(args, {
|
|
1081
|
+
limit: toInt(args.limit, 10)
|
|
1082
|
+
}),
|
|
1083
|
+
args.apiBaseUrl
|
|
1084
|
+
);
|
|
1085
|
+
if (!result.success) {
|
|
1086
|
+
return { contradictions: [], error: result.error };
|
|
1087
|
+
}
|
|
1088
|
+
return {
|
|
1089
|
+
contradictions: result.data?.map(
|
|
1090
|
+
(r) => r.contradiction
|
|
1091
|
+
) || []
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
var getThemeSubgraph = action({
|
|
1096
|
+
args: {
|
|
1097
|
+
themeGlobalId: v.string(),
|
|
1098
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1099
|
+
topicId: v.optional(v.string())
|
|
1100
|
+
},
|
|
1101
|
+
returns: permissiveReturn,
|
|
1102
|
+
handler: async (_ctx, args) => {
|
|
1103
|
+
const result = await callNeo4jQuery(
|
|
1104
|
+
"themeSubgraph",
|
|
1105
|
+
withTopicScope(args, {
|
|
1106
|
+
themeGlobalId: args.themeGlobalId
|
|
1107
|
+
}),
|
|
1108
|
+
args.apiBaseUrl
|
|
1109
|
+
);
|
|
1110
|
+
if (!result.success) {
|
|
1111
|
+
return { subgraph: null, error: result.error };
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
subgraph: result.data?.[0]?.subgraph || null
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
var getEvidenceToBeliefPath = action({
|
|
1119
|
+
args: {
|
|
1120
|
+
evidenceGlobalId: v.string(),
|
|
1121
|
+
beliefGlobalId: v.string(),
|
|
1122
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1123
|
+
topicId: v.optional(v.string())
|
|
1124
|
+
},
|
|
1125
|
+
returns: permissiveReturn,
|
|
1126
|
+
handler: async (_ctx, args) => {
|
|
1127
|
+
const result = await callNeo4jQuery(
|
|
1128
|
+
"evidenceToBeliefPath",
|
|
1129
|
+
withTopicScope(args, {
|
|
1130
|
+
evidenceGlobalId: args.evidenceGlobalId,
|
|
1131
|
+
beliefGlobalId: args.beliefGlobalId
|
|
1132
|
+
}),
|
|
1133
|
+
args.apiBaseUrl
|
|
1134
|
+
);
|
|
1135
|
+
if (!result.success) {
|
|
1136
|
+
return { path: null, error: result.error };
|
|
1137
|
+
}
|
|
1138
|
+
const pathResult = result.data?.[0];
|
|
1139
|
+
if (!pathResult?.pathResult) {
|
|
1140
|
+
return { path: null, error: "No path found between evidence and belief" };
|
|
1141
|
+
}
|
|
1142
|
+
return {
|
|
1143
|
+
path: {
|
|
1144
|
+
nodes: pathResult.pathResult.nodes || [],
|
|
1145
|
+
relationships: pathResult.pathResult.relationships || [],
|
|
1146
|
+
length: pathResult.pathResult.length || 0
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
var getThemeStats = action({
|
|
1152
|
+
args: {
|
|
1153
|
+
themeGlobalId: v.string()
|
|
1154
|
+
},
|
|
1155
|
+
returns: permissiveReturn,
|
|
1156
|
+
handler: async (_ctx, args) => {
|
|
1157
|
+
const result = await callNeo4jQuery("themeStats", {
|
|
1158
|
+
themeGlobalId: args.themeGlobalId
|
|
1159
|
+
});
|
|
1160
|
+
if (!result.success) {
|
|
1161
|
+
return { stats: null, error: result.error };
|
|
1162
|
+
}
|
|
1163
|
+
return { stats: result.data?.[0]?.stats || null };
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
var getThemeValueChainCandidates = action({
|
|
1167
|
+
args: {
|
|
1168
|
+
limit: v.optional(v.number()),
|
|
1169
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1170
|
+
topicId: v.optional(v.string())
|
|
1171
|
+
},
|
|
1172
|
+
returns: permissiveReturn,
|
|
1173
|
+
handler: async (_ctx, args) => {
|
|
1174
|
+
const result = await callNeo4jQuery(
|
|
1175
|
+
"themeValueChainCandidates",
|
|
1176
|
+
withTopicScope(args, {
|
|
1177
|
+
limit: toInt(args.limit, 100)
|
|
1178
|
+
}),
|
|
1179
|
+
args.apiBaseUrl
|
|
1180
|
+
);
|
|
1181
|
+
if (!result.success) {
|
|
1182
|
+
return { candidates: [], error: result.error };
|
|
1183
|
+
}
|
|
1184
|
+
return {
|
|
1185
|
+
candidates: result.data?.map((r) => r.candidate) || []
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
});
|
|
1189
|
+
var getThemesImpactingCompany = action({
|
|
1190
|
+
args: {
|
|
1191
|
+
companyName: v.string(),
|
|
1192
|
+
limit: v.optional(v.number())
|
|
1193
|
+
},
|
|
1194
|
+
returns: permissiveReturn,
|
|
1195
|
+
handler: async (_ctx, args) => {
|
|
1196
|
+
const result = await callNeo4jQuery("themesImpactingCompany", {
|
|
1197
|
+
companyName: args.companyName,
|
|
1198
|
+
limit: toInt(args.limit, 20)
|
|
1199
|
+
});
|
|
1200
|
+
if (!result.success) {
|
|
1201
|
+
return { results: [], error: result.error };
|
|
1202
|
+
}
|
|
1203
|
+
return { results: result.data || [] };
|
|
1204
|
+
}
|
|
1205
|
+
});
|
|
1206
|
+
var getCompaniesByTheme = action({
|
|
1207
|
+
args: {
|
|
1208
|
+
themeName: v.string(),
|
|
1209
|
+
limit: v.optional(v.number())
|
|
1210
|
+
},
|
|
1211
|
+
returns: permissiveReturn,
|
|
1212
|
+
handler: async (_ctx, args) => {
|
|
1213
|
+
const result = await callNeo4jQuery("companiesByTheme", {
|
|
1214
|
+
themeName: args.themeName,
|
|
1215
|
+
limit: toInt(args.limit, 50)
|
|
1216
|
+
});
|
|
1217
|
+
if (!result.success) {
|
|
1218
|
+
return { results: [], error: result.error };
|
|
1219
|
+
}
|
|
1220
|
+
return { results: result.data || [] };
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
var getQuestionsByValueChain = action({
|
|
1224
|
+
args: {
|
|
1225
|
+
valueChainName: v.string(),
|
|
1226
|
+
limit: v.optional(v.number())
|
|
1227
|
+
},
|
|
1228
|
+
returns: permissiveReturn,
|
|
1229
|
+
handler: async (_ctx, args) => {
|
|
1230
|
+
const result = await callNeo4jQuery("questionsByValueChain", {
|
|
1231
|
+
valueChainName: args.valueChainName,
|
|
1232
|
+
limit: toInt(args.limit, 50)
|
|
1233
|
+
});
|
|
1234
|
+
if (!result.success) {
|
|
1235
|
+
return { results: [], error: result.error };
|
|
1236
|
+
}
|
|
1237
|
+
return { results: result.data || [] };
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
var getQuestionsByTheme = action({
|
|
1241
|
+
args: {
|
|
1242
|
+
themeName: v.string(),
|
|
1243
|
+
limit: v.optional(v.number())
|
|
1244
|
+
},
|
|
1245
|
+
returns: permissiveReturn,
|
|
1246
|
+
handler: async (_ctx, args) => {
|
|
1247
|
+
const result = await callNeo4jQuery("questionsByTheme", {
|
|
1248
|
+
themeName: args.themeName,
|
|
1249
|
+
limit: toInt(args.limit, 50)
|
|
1250
|
+
});
|
|
1251
|
+
if (!result.success) {
|
|
1252
|
+
return { results: [], error: result.error };
|
|
1253
|
+
}
|
|
1254
|
+
return { results: result.data || [] };
|
|
1255
|
+
}
|
|
1256
|
+
});
|
|
1257
|
+
var getPeopleByTheme = action({
|
|
1258
|
+
args: {
|
|
1259
|
+
themeName: v.string(),
|
|
1260
|
+
limit: v.optional(v.number())
|
|
1261
|
+
},
|
|
1262
|
+
returns: permissiveReturn,
|
|
1263
|
+
handler: async (_ctx, args) => {
|
|
1264
|
+
const result = await callNeo4jQuery("peopleByTheme", {
|
|
1265
|
+
themeName: args.themeName,
|
|
1266
|
+
limit: toInt(args.limit, 50)
|
|
1267
|
+
});
|
|
1268
|
+
if (!result.success) {
|
|
1269
|
+
return { results: [], error: result.error };
|
|
1270
|
+
}
|
|
1271
|
+
return { results: result.data || [] };
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
var getPeopleByCompany = action({
|
|
1275
|
+
args: {
|
|
1276
|
+
companyName: v.string(),
|
|
1277
|
+
limit: v.optional(v.number())
|
|
1278
|
+
},
|
|
1279
|
+
returns: permissiveReturn,
|
|
1280
|
+
handler: async (_ctx, args) => {
|
|
1281
|
+
const result = await callNeo4jQuery("peopleByCompany", {
|
|
1282
|
+
companyName: args.companyName,
|
|
1283
|
+
limit: toInt(args.limit, 50)
|
|
1284
|
+
});
|
|
1285
|
+
if (!result.success) {
|
|
1286
|
+
return { results: [], error: result.error };
|
|
1287
|
+
}
|
|
1288
|
+
return { results: result.data || [] };
|
|
1289
|
+
}
|
|
1290
|
+
});
|
|
1291
|
+
var getValueChainsByTheme = action({
|
|
1292
|
+
args: {
|
|
1293
|
+
themeName: v.string(),
|
|
1294
|
+
limit: v.optional(v.number())
|
|
1295
|
+
},
|
|
1296
|
+
returns: permissiveReturn,
|
|
1297
|
+
handler: async (_ctx, args) => {
|
|
1298
|
+
const result = await callNeo4jQuery("valueChainsByTheme", {
|
|
1299
|
+
themeName: args.themeName,
|
|
1300
|
+
limit: toInt(args.limit, 20)
|
|
1301
|
+
});
|
|
1302
|
+
if (!result.success) {
|
|
1303
|
+
return { results: [], error: result.error };
|
|
1304
|
+
}
|
|
1305
|
+
return { results: result.data || [] };
|
|
1306
|
+
}
|
|
1307
|
+
});
|
|
1308
|
+
var getFunctionsByValueChain = action({
|
|
1309
|
+
args: {
|
|
1310
|
+
valueChainName: v.string(),
|
|
1311
|
+
limit: v.optional(v.number())
|
|
1312
|
+
},
|
|
1313
|
+
returns: permissiveReturn,
|
|
1314
|
+
handler: async (_ctx, args) => {
|
|
1315
|
+
const result = await callNeo4jQuery("functionsByValueChain", {
|
|
1316
|
+
valueChainName: args.valueChainName,
|
|
1317
|
+
limit: toInt(args.limit, 50)
|
|
1318
|
+
});
|
|
1319
|
+
if (!result.success) {
|
|
1320
|
+
return { results: [], error: result.error };
|
|
1321
|
+
}
|
|
1322
|
+
return { results: result.data || [] };
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
var getBeliefsByCompany = action({
|
|
1326
|
+
args: {
|
|
1327
|
+
companyName: v.string(),
|
|
1328
|
+
limit: v.optional(v.number())
|
|
1329
|
+
},
|
|
1330
|
+
returns: permissiveReturn,
|
|
1331
|
+
handler: async (_ctx, args) => {
|
|
1332
|
+
const result = await callNeo4jQuery("beliefsByCompany", {
|
|
1333
|
+
companyName: args.companyName,
|
|
1334
|
+
limit: toInt(args.limit, 30)
|
|
1335
|
+
});
|
|
1336
|
+
if (!result.success) {
|
|
1337
|
+
return { results: [], error: result.error };
|
|
1338
|
+
}
|
|
1339
|
+
return { results: result.data || [] };
|
|
1340
|
+
}
|
|
1341
|
+
});
|
|
1342
|
+
var getEvidenceByCompany = action({
|
|
1343
|
+
args: {
|
|
1344
|
+
companyName: v.string(),
|
|
1345
|
+
limit: v.optional(v.number())
|
|
1346
|
+
},
|
|
1347
|
+
returns: permissiveReturn,
|
|
1348
|
+
handler: async (_ctx, args) => {
|
|
1349
|
+
const result = await callNeo4jQuery("evidenceByCompany", {
|
|
1350
|
+
companyName: args.companyName,
|
|
1351
|
+
limit: toInt(args.limit, 50)
|
|
1352
|
+
});
|
|
1353
|
+
if (!result.success) {
|
|
1354
|
+
return { results: [], error: result.error };
|
|
1355
|
+
}
|
|
1356
|
+
return { results: result.data || [] };
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
var searchAllNodes = action({
|
|
1360
|
+
args: {
|
|
1361
|
+
searchText: v.string(),
|
|
1362
|
+
limit: v.optional(v.number())
|
|
1363
|
+
},
|
|
1364
|
+
returns: permissiveReturn,
|
|
1365
|
+
handler: async (_ctx, args) => {
|
|
1366
|
+
const result = await callNeo4jQuery("searchAllNodes", {
|
|
1367
|
+
searchText: args.searchText,
|
|
1368
|
+
limit: toInt(args.limit, 50)
|
|
1369
|
+
});
|
|
1370
|
+
if (!result.success) {
|
|
1371
|
+
return { results: [], error: result.error };
|
|
1372
|
+
}
|
|
1373
|
+
return { results: result.data || [] };
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
var getNodeRelationships = action({
|
|
1377
|
+
args: {
|
|
1378
|
+
globalId: v.optional(v.string()),
|
|
1379
|
+
searchText: v.optional(v.string()),
|
|
1380
|
+
limit: v.optional(v.number())
|
|
1381
|
+
},
|
|
1382
|
+
returns: permissiveReturn,
|
|
1383
|
+
handler: async (_ctx, args) => {
|
|
1384
|
+
const result = await callNeo4jQuery("nodeRelationships", {
|
|
1385
|
+
globalId: args.globalId || "",
|
|
1386
|
+
searchText: args.searchText || "",
|
|
1387
|
+
limit: toInt(args.limit, 50)
|
|
1388
|
+
});
|
|
1389
|
+
if (!result.success) {
|
|
1390
|
+
return { results: [], error: result.error };
|
|
1391
|
+
}
|
|
1392
|
+
return { results: result.data || [] };
|
|
1393
|
+
}
|
|
1394
|
+
});
|
|
1395
|
+
var getGraphStats = action({
|
|
1396
|
+
args: {},
|
|
1397
|
+
returns: permissiveReturn,
|
|
1398
|
+
handler: async () => {
|
|
1399
|
+
const result = await callNeo4jQuery("graphStats", {});
|
|
1400
|
+
if (!result.success) {
|
|
1401
|
+
return { stats: [], error: result.error };
|
|
1402
|
+
}
|
|
1403
|
+
return { stats: result.data || [] };
|
|
1404
|
+
}
|
|
1405
|
+
});
|
|
1406
|
+
var queryGraph = action({
|
|
1407
|
+
args: {
|
|
1408
|
+
queryType: v.union(
|
|
1409
|
+
v.literal("themesImpactingCompany"),
|
|
1410
|
+
v.literal("companiesByTheme"),
|
|
1411
|
+
v.literal("questionsByValueChain"),
|
|
1412
|
+
v.literal("questionsByTheme"),
|
|
1413
|
+
v.literal("peopleByTheme"),
|
|
1414
|
+
v.literal("peopleByCompany"),
|
|
1415
|
+
v.literal("valueChainsByTheme"),
|
|
1416
|
+
v.literal("functionsByValueChain"),
|
|
1417
|
+
v.literal("beliefsByCompany"),
|
|
1418
|
+
v.literal("evidenceByCompany"),
|
|
1419
|
+
v.literal("searchAllNodes"),
|
|
1420
|
+
v.literal("nodeRelationships"),
|
|
1421
|
+
v.literal("graphStats")
|
|
1422
|
+
),
|
|
1423
|
+
params: v.object({
|
|
1424
|
+
companyName: v.optional(v.string()),
|
|
1425
|
+
themeName: v.optional(v.string()),
|
|
1426
|
+
valueChainName: v.optional(v.string()),
|
|
1427
|
+
searchText: v.optional(v.string()),
|
|
1428
|
+
globalId: v.optional(v.string()),
|
|
1429
|
+
limit: v.optional(v.number())
|
|
1430
|
+
}),
|
|
1431
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1432
|
+
topicId: v.optional(v.string())
|
|
1433
|
+
},
|
|
1434
|
+
returns: permissiveReturn,
|
|
1435
|
+
handler: async (_ctx, args) => {
|
|
1436
|
+
const limit = toInt(args.params.limit, 30);
|
|
1437
|
+
const themeRequiredQueries = [
|
|
1438
|
+
"questionsByTheme",
|
|
1439
|
+
"peopleByTheme",
|
|
1440
|
+
"companiesByTheme",
|
|
1441
|
+
"valueChainsByTheme"
|
|
1442
|
+
];
|
|
1443
|
+
const companyRequiredQueries = [
|
|
1444
|
+
"themesImpactingCompany",
|
|
1445
|
+
"peopleByCompany",
|
|
1446
|
+
"beliefsByCompany",
|
|
1447
|
+
"evidenceByCompany"
|
|
1448
|
+
];
|
|
1449
|
+
const valueChainRequiredQueries = [
|
|
1450
|
+
"questionsByValueChain",
|
|
1451
|
+
"functionsByValueChain"
|
|
1452
|
+
];
|
|
1453
|
+
if (themeRequiredQueries.includes(args.queryType) && !args.params.themeName) {
|
|
1454
|
+
return {
|
|
1455
|
+
results: [],
|
|
1456
|
+
error: `Query type '${args.queryType}' requires 'themeName' parameter`
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
if (companyRequiredQueries.includes(args.queryType) && !args.params.companyName) {
|
|
1460
|
+
return {
|
|
1461
|
+
results: [],
|
|
1462
|
+
error: `Query type '${args.queryType}' requires 'companyName' parameter`
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
if (valueChainRequiredQueries.includes(args.queryType) && !args.params.valueChainName) {
|
|
1466
|
+
return {
|
|
1467
|
+
results: [],
|
|
1468
|
+
error: `Query type '${args.queryType}' requires 'valueChainName' parameter`
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
const result = await callNeo4jQuery(
|
|
1472
|
+
args.queryType,
|
|
1473
|
+
withTopicScope(args, {
|
|
1474
|
+
...args.params,
|
|
1475
|
+
limit
|
|
1476
|
+
}),
|
|
1477
|
+
args.apiBaseUrl
|
|
1478
|
+
);
|
|
1479
|
+
if (!result.success) {
|
|
1480
|
+
return { results: [], error: result.error };
|
|
1481
|
+
}
|
|
1482
|
+
return { results: result.data || [] };
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
var getHighPriorityQuestions = action({
|
|
1486
|
+
args: {
|
|
1487
|
+
limit: v.optional(v.number()),
|
|
1488
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1489
|
+
topicId: v.optional(v.string())
|
|
1490
|
+
},
|
|
1491
|
+
returns: permissiveReturn,
|
|
1492
|
+
handler: async (_ctx, args) => {
|
|
1493
|
+
const result = await callNeo4jQuery(
|
|
1494
|
+
"highPriorityQuestions",
|
|
1495
|
+
withTopicScope(args, {
|
|
1496
|
+
limit: toInt(args.limit, 50)
|
|
1497
|
+
}),
|
|
1498
|
+
args.apiBaseUrl
|
|
1499
|
+
);
|
|
1500
|
+
if (!result.success) {
|
|
1501
|
+
return { questions: [], error: result.error };
|
|
1502
|
+
}
|
|
1503
|
+
return { questions: result.data || [] };
|
|
1504
|
+
}
|
|
1505
|
+
});
|
|
1506
|
+
var getEvidenceByMethodology = action({
|
|
1507
|
+
args: {
|
|
1508
|
+
methodology: v.string(),
|
|
1509
|
+
limit: v.optional(v.number()),
|
|
1510
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1511
|
+
topicId: v.optional(v.string())
|
|
1512
|
+
},
|
|
1513
|
+
returns: permissiveReturn,
|
|
1514
|
+
handler: async (_ctx, args) => {
|
|
1515
|
+
const result = await callNeo4jQuery(
|
|
1516
|
+
"evidenceByMethodology",
|
|
1517
|
+
withTopicScope(args, {
|
|
1518
|
+
methodology: args.methodology,
|
|
1519
|
+
limit: toInt(args.limit, 50)
|
|
1520
|
+
}),
|
|
1521
|
+
args.apiBaseUrl
|
|
1522
|
+
);
|
|
1523
|
+
if (!result.success) {
|
|
1524
|
+
return { evidence: [], error: result.error };
|
|
1525
|
+
}
|
|
1526
|
+
return { evidence: result.data || [] };
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
var getProprietaryEvidence = action({
|
|
1530
|
+
args: {
|
|
1531
|
+
limit: v.optional(v.number()),
|
|
1532
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1533
|
+
topicId: v.optional(v.string())
|
|
1534
|
+
},
|
|
1535
|
+
returns: permissiveReturn,
|
|
1536
|
+
handler: async (_ctx, args) => {
|
|
1537
|
+
const result = await callNeo4jQuery(
|
|
1538
|
+
"proprietaryEvidence",
|
|
1539
|
+
withTopicScope(args, {
|
|
1540
|
+
limit: toInt(args.limit, 50)
|
|
1541
|
+
}),
|
|
1542
|
+
args.apiBaseUrl
|
|
1543
|
+
);
|
|
1544
|
+
if (!result.success) {
|
|
1545
|
+
return { evidence: [], error: result.error };
|
|
1546
|
+
}
|
|
1547
|
+
return { evidence: result.data || [] };
|
|
1548
|
+
}
|
|
1549
|
+
});
|
|
1550
|
+
var getBeliefsByEpistemicStatus = action({
|
|
1551
|
+
args: {
|
|
1552
|
+
epistemicStatus: v.string(),
|
|
1553
|
+
limit: v.optional(v.number()),
|
|
1554
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1555
|
+
topicId: v.optional(v.string())
|
|
1556
|
+
},
|
|
1557
|
+
returns: permissiveReturn,
|
|
1558
|
+
handler: async (_ctx, args) => {
|
|
1559
|
+
const result = await callNeo4jQuery(
|
|
1560
|
+
"beliefsByEpistemicStatus",
|
|
1561
|
+
withTopicScope(args, {
|
|
1562
|
+
epistemicStatus: args.epistemicStatus,
|
|
1563
|
+
limit: toInt(args.limit, 50)
|
|
1564
|
+
}),
|
|
1565
|
+
args.apiBaseUrl
|
|
1566
|
+
);
|
|
1567
|
+
if (!result.success) {
|
|
1568
|
+
return { beliefs: [], error: result.error };
|
|
1569
|
+
}
|
|
1570
|
+
return { beliefs: result.data || [] };
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
var getChallengedBeliefs = action({
|
|
1574
|
+
args: {
|
|
1575
|
+
limit: v.optional(v.number()),
|
|
1576
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1577
|
+
topicId: v.optional(v.string())
|
|
1578
|
+
},
|
|
1579
|
+
returns: permissiveReturn,
|
|
1580
|
+
handler: async (_ctx, args) => {
|
|
1581
|
+
const result = await callNeo4jQuery(
|
|
1582
|
+
"challengedBeliefs",
|
|
1583
|
+
withTopicScope(args, {
|
|
1584
|
+
limit: toInt(args.limit, 30)
|
|
1585
|
+
}),
|
|
1586
|
+
args.apiBaseUrl
|
|
1587
|
+
);
|
|
1588
|
+
if (!result.success) {
|
|
1589
|
+
return { beliefs: [], error: result.error };
|
|
1590
|
+
}
|
|
1591
|
+
return { beliefs: result.data || [] };
|
|
1592
|
+
}
|
|
1593
|
+
});
|
|
1594
|
+
var getPredictions = action({
|
|
1595
|
+
args: {
|
|
1596
|
+
limit: v.optional(v.number()),
|
|
1597
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1598
|
+
topicId: v.optional(v.string())
|
|
1599
|
+
},
|
|
1600
|
+
returns: permissiveReturn,
|
|
1601
|
+
handler: async (_ctx, args) => {
|
|
1602
|
+
const result = await callNeo4jQuery(
|
|
1603
|
+
"predictions",
|
|
1604
|
+
withTopicScope(args, {
|
|
1605
|
+
limit: toInt(args.limit, 50)
|
|
1606
|
+
}),
|
|
1607
|
+
args.apiBaseUrl
|
|
1608
|
+
);
|
|
1609
|
+
if (!result.success) {
|
|
1610
|
+
return { predictions: [], error: result.error };
|
|
1611
|
+
}
|
|
1612
|
+
return { predictions: result.data || [] };
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
var getCausalChains = action({
|
|
1616
|
+
args: {
|
|
1617
|
+
limit: v.optional(v.number()),
|
|
1618
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1619
|
+
topicId: v.optional(v.string())
|
|
1620
|
+
},
|
|
1621
|
+
returns: permissiveReturn,
|
|
1622
|
+
handler: async (_ctx, args) => {
|
|
1623
|
+
const result = await callNeo4jQuery(
|
|
1624
|
+
"causalChains",
|
|
1625
|
+
withTopicScope(args, {
|
|
1626
|
+
limit: toInt(args.limit, 50)
|
|
1627
|
+
}),
|
|
1628
|
+
args.apiBaseUrl
|
|
1629
|
+
);
|
|
1630
|
+
if (!result.success) {
|
|
1631
|
+
return { chains: [], error: result.error };
|
|
1632
|
+
}
|
|
1633
|
+
return { chains: result.data || [] };
|
|
1634
|
+
}
|
|
1635
|
+
});
|
|
1636
|
+
var getNecessaryEvidence = action({
|
|
1637
|
+
args: {
|
|
1638
|
+
limit: v.optional(v.number()),
|
|
1639
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1640
|
+
topicId: v.optional(v.string())
|
|
1641
|
+
},
|
|
1642
|
+
returns: permissiveReturn,
|
|
1643
|
+
handler: async (_ctx, args) => {
|
|
1644
|
+
const result = await callNeo4jQuery(
|
|
1645
|
+
"necessaryEvidence",
|
|
1646
|
+
withTopicScope(args, {
|
|
1647
|
+
limit: toInt(args.limit, 50)
|
|
1648
|
+
}),
|
|
1649
|
+
args.apiBaseUrl
|
|
1650
|
+
);
|
|
1651
|
+
if (!result.success) {
|
|
1652
|
+
return { results: [], error: result.error };
|
|
1653
|
+
}
|
|
1654
|
+
return { results: result.data || [] };
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
var getFalsificationQuestions = action({
|
|
1658
|
+
args: {
|
|
1659
|
+
limit: v.optional(v.number()),
|
|
1660
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1661
|
+
topicId: v.optional(v.string())
|
|
1662
|
+
},
|
|
1663
|
+
returns: permissiveReturn,
|
|
1664
|
+
handler: async (_ctx, args) => {
|
|
1665
|
+
const result = await callNeo4jQuery(
|
|
1666
|
+
"falsificationQuestions",
|
|
1667
|
+
withTopicScope(args, {
|
|
1668
|
+
limit: toInt(args.limit, 50)
|
|
1669
|
+
}),
|
|
1670
|
+
args.apiBaseUrl
|
|
1671
|
+
);
|
|
1672
|
+
if (!result.success) {
|
|
1673
|
+
return { questions: [], error: result.error };
|
|
1674
|
+
}
|
|
1675
|
+
return { questions: result.data || [] };
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
var getConfirmationBiasScore = action({
|
|
1679
|
+
args: {
|
|
1680
|
+
limit: v.optional(v.number()),
|
|
1681
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1682
|
+
topicId: v.optional(v.string())
|
|
1683
|
+
},
|
|
1684
|
+
returns: permissiveReturn,
|
|
1685
|
+
handler: async (_ctx, args) => {
|
|
1686
|
+
const result = await callNeo4jQuery(
|
|
1687
|
+
"confirmationBiasScore",
|
|
1688
|
+
{
|
|
1689
|
+
...args.topicId ? { topicId: args.topicId } : {},
|
|
1690
|
+
limit: toInt(args.limit, 30)
|
|
1691
|
+
},
|
|
1692
|
+
args.apiBaseUrl
|
|
1693
|
+
);
|
|
1694
|
+
if (!result.success) {
|
|
1695
|
+
return { results: [], error: result.error };
|
|
1696
|
+
}
|
|
1697
|
+
return { results: result.data || [] };
|
|
1698
|
+
}
|
|
1699
|
+
});
|
|
1700
|
+
var getAnchoringBiasDetection = action({
|
|
1701
|
+
args: {
|
|
1702
|
+
limit: v.optional(v.number()),
|
|
1703
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1704
|
+
topicId: v.optional(v.string())
|
|
1705
|
+
},
|
|
1706
|
+
returns: permissiveReturn,
|
|
1707
|
+
handler: async (_ctx, args) => {
|
|
1708
|
+
const result = await callNeo4jQuery(
|
|
1709
|
+
"anchoringBiasDetection",
|
|
1710
|
+
withTopicScope(args, {
|
|
1711
|
+
limit: toInt(args.limit, 30)
|
|
1712
|
+
}),
|
|
1713
|
+
args.apiBaseUrl
|
|
1714
|
+
);
|
|
1715
|
+
if (!result.success) {
|
|
1716
|
+
return { results: [], error: result.error };
|
|
1717
|
+
}
|
|
1718
|
+
return { results: result.data || [] };
|
|
1719
|
+
}
|
|
1720
|
+
});
|
|
1721
|
+
var getSourceConcentrationRisk = action({
|
|
1722
|
+
args: {
|
|
1723
|
+
limit: v.optional(v.number()),
|
|
1724
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1725
|
+
topicId: v.optional(v.string())
|
|
1726
|
+
},
|
|
1727
|
+
returns: permissiveReturn,
|
|
1728
|
+
handler: async (_ctx, args) => {
|
|
1729
|
+
const result = await callNeo4jQuery(
|
|
1730
|
+
"sourceConcentrationRisk",
|
|
1731
|
+
withTopicScope(args, {
|
|
1732
|
+
limit: toInt(args.limit, 30)
|
|
1733
|
+
}),
|
|
1734
|
+
args.apiBaseUrl
|
|
1735
|
+
);
|
|
1736
|
+
if (!result.success) {
|
|
1737
|
+
return { results: [], error: result.error };
|
|
1738
|
+
}
|
|
1739
|
+
return { results: result.data || [] };
|
|
1740
|
+
}
|
|
1741
|
+
});
|
|
1742
|
+
var getMinimumFalsificationSet = action({
|
|
1743
|
+
args: {
|
|
1744
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1745
|
+
topicId: v.optional(v.string())
|
|
1746
|
+
},
|
|
1747
|
+
returns: permissiveReturn,
|
|
1748
|
+
handler: async (_ctx, args) => {
|
|
1749
|
+
const result = await callNeo4jQuery(
|
|
1750
|
+
"minimumFalsificationSet",
|
|
1751
|
+
withTopicScope(args, {}),
|
|
1752
|
+
args.apiBaseUrl
|
|
1753
|
+
);
|
|
1754
|
+
if (!result.success) {
|
|
1755
|
+
return { result: null, error: result.error };
|
|
1756
|
+
}
|
|
1757
|
+
const data = result.data;
|
|
1758
|
+
return { result: Array.isArray(data) && data.length > 0 ? data[0] : null };
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
var getContradictionTensionMap = action({
|
|
1762
|
+
args: {
|
|
1763
|
+
limit: v.optional(v.number()),
|
|
1764
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1765
|
+
topicId: v.optional(v.string())
|
|
1766
|
+
},
|
|
1767
|
+
returns: permissiveReturn,
|
|
1768
|
+
handler: async (_ctx, args) => {
|
|
1769
|
+
const result = await callNeo4jQuery(
|
|
1770
|
+
"contradictionTensionMap",
|
|
1771
|
+
withTopicScope(args, {
|
|
1772
|
+
limit: toInt(args.limit, 30)
|
|
1773
|
+
}),
|
|
1774
|
+
args.apiBaseUrl
|
|
1775
|
+
);
|
|
1776
|
+
if (!result.success) {
|
|
1777
|
+
return { results: [], error: result.error };
|
|
1778
|
+
}
|
|
1779
|
+
return { results: result.data || [] };
|
|
1780
|
+
}
|
|
1781
|
+
});
|
|
1782
|
+
var getReasoningDepthScore = action({
|
|
1783
|
+
args: {
|
|
1784
|
+
limit: v.optional(v.number()),
|
|
1785
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1786
|
+
topicId: v.optional(v.string())
|
|
1787
|
+
},
|
|
1788
|
+
returns: permissiveReturn,
|
|
1789
|
+
handler: async (_ctx, args) => {
|
|
1790
|
+
const result = await callNeo4jQuery(
|
|
1791
|
+
"reasoningDepthScore",
|
|
1792
|
+
withTopicScope(args, {
|
|
1793
|
+
limit: toInt(args.limit, 50)
|
|
1794
|
+
}),
|
|
1795
|
+
args.apiBaseUrl
|
|
1796
|
+
);
|
|
1797
|
+
if (!result.success) {
|
|
1798
|
+
return { results: [], error: result.error };
|
|
1799
|
+
}
|
|
1800
|
+
return { results: result.data || [] };
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
var getKnowledgeFrontier = action({
|
|
1804
|
+
args: {
|
|
1805
|
+
limit: v.optional(v.number()),
|
|
1806
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1807
|
+
topicId: v.optional(v.string())
|
|
1808
|
+
},
|
|
1809
|
+
returns: permissiveReturn,
|
|
1810
|
+
handler: async (_ctx, args) => {
|
|
1811
|
+
const result = await callNeo4jQuery(
|
|
1812
|
+
"knowledgeFrontier",
|
|
1813
|
+
withTopicScope(args, {
|
|
1814
|
+
limit: toInt(args.limit, 30)
|
|
1815
|
+
}),
|
|
1816
|
+
args.apiBaseUrl
|
|
1817
|
+
);
|
|
1818
|
+
if (!result.success) {
|
|
1819
|
+
return { results: [], error: result.error };
|
|
1820
|
+
}
|
|
1821
|
+
return { results: result.data || [] };
|
|
1822
|
+
}
|
|
1823
|
+
});
|
|
1824
|
+
var getBeliefHalfLife = action({
|
|
1825
|
+
args: {
|
|
1826
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1827
|
+
topicId: v.optional(v.string())
|
|
1828
|
+
},
|
|
1829
|
+
returns: permissiveReturn,
|
|
1830
|
+
handler: async (_ctx, args) => {
|
|
1831
|
+
const result = await callNeo4jQuery(
|
|
1832
|
+
"beliefHalfLife",
|
|
1833
|
+
withTopicScope(args, {}),
|
|
1834
|
+
args.apiBaseUrl
|
|
1835
|
+
);
|
|
1836
|
+
if (!result.success) {
|
|
1837
|
+
return { result: null, error: result.error };
|
|
1838
|
+
}
|
|
1839
|
+
const data = result.data;
|
|
1840
|
+
return { result: Array.isArray(data) && data.length > 0 ? data[0] : null };
|
|
1841
|
+
}
|
|
1842
|
+
});
|
|
1843
|
+
var getMeetingPrepBrief = action({
|
|
1844
|
+
args: {
|
|
1845
|
+
personGlobalId: v.string(),
|
|
1846
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1847
|
+
topicId: v.optional(v.string())
|
|
1848
|
+
},
|
|
1849
|
+
returns: permissiveReturn,
|
|
1850
|
+
handler: async (_ctx, args) => {
|
|
1851
|
+
const result = await callNeo4jQuery(
|
|
1852
|
+
"meetingPrepBrief",
|
|
1853
|
+
withTopicScope(args, {
|
|
1854
|
+
personGlobalId: args.personGlobalId
|
|
1855
|
+
}),
|
|
1856
|
+
args.apiBaseUrl
|
|
1857
|
+
);
|
|
1858
|
+
if (!result.success) {
|
|
1859
|
+
return { brief: null, error: result.error };
|
|
1860
|
+
}
|
|
1861
|
+
const data = result.data;
|
|
1862
|
+
return { brief: Array.isArray(data) && data.length > 0 ? data[0] : null };
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
var getProprietarySignals = action({
|
|
1866
|
+
args: {
|
|
1867
|
+
limit: v.optional(v.number()),
|
|
1868
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1869
|
+
topicId: v.optional(v.string())
|
|
1870
|
+
},
|
|
1871
|
+
returns: permissiveReturn,
|
|
1872
|
+
handler: async (_ctx, args) => {
|
|
1873
|
+
const result = await callNeo4jQuery(
|
|
1874
|
+
"proprietarySignals",
|
|
1875
|
+
withTopicScope(args, {
|
|
1876
|
+
limit: toInt(args.limit, 30)
|
|
1877
|
+
}),
|
|
1878
|
+
args.apiBaseUrl
|
|
1879
|
+
);
|
|
1880
|
+
if (!result.success) {
|
|
1881
|
+
return { results: [], error: result.error };
|
|
1882
|
+
}
|
|
1883
|
+
return { results: result.data || [] };
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
var getPortfolioConviction = action({
|
|
1887
|
+
args: {
|
|
1888
|
+
limit: v.optional(v.number()),
|
|
1889
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1890
|
+
topicId: v.optional(v.string())
|
|
1891
|
+
},
|
|
1892
|
+
returns: permissiveReturn,
|
|
1893
|
+
handler: async (_ctx, args) => {
|
|
1894
|
+
const result = await callNeo4jQuery(
|
|
1895
|
+
"portfolioConviction",
|
|
1896
|
+
withTopicScope(args, {
|
|
1897
|
+
limit: toInt(args.limit, 50)
|
|
1898
|
+
}),
|
|
1899
|
+
args.apiBaseUrl
|
|
1900
|
+
);
|
|
1901
|
+
if (!result.success) {
|
|
1902
|
+
return { portfolio: [], error: result.error };
|
|
1903
|
+
}
|
|
1904
|
+
return { portfolio: result.data || [] };
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
var getStaleThemes = action({
|
|
1908
|
+
args: {
|
|
1909
|
+
limit: v.optional(v.number()),
|
|
1910
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1911
|
+
topicId: v.optional(v.string())
|
|
1912
|
+
},
|
|
1913
|
+
returns: permissiveReturn,
|
|
1914
|
+
handler: async (_ctx, args) => {
|
|
1915
|
+
const result = await callNeo4jQuery(
|
|
1916
|
+
"staleThemes",
|
|
1917
|
+
withTopicScope(args, {
|
|
1918
|
+
limit: toInt(args.limit, 30)
|
|
1919
|
+
}),
|
|
1920
|
+
args.apiBaseUrl
|
|
1921
|
+
);
|
|
1922
|
+
if (!result.success) {
|
|
1923
|
+
return { themes: [], error: result.error };
|
|
1924
|
+
}
|
|
1925
|
+
return { themes: result.data || [] };
|
|
1926
|
+
}
|
|
1927
|
+
});
|
|
1928
|
+
var getMissingQuestionDetection = action({
|
|
1929
|
+
args: {
|
|
1930
|
+
limit: v.optional(v.number()),
|
|
1931
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1932
|
+
topicId: v.optional(v.string())
|
|
1933
|
+
},
|
|
1934
|
+
returns: permissiveReturn,
|
|
1935
|
+
handler: async (_ctx, args) => {
|
|
1936
|
+
const result = await callNeo4jQuery(
|
|
1937
|
+
"missingQuestionDetection",
|
|
1938
|
+
withTopicScope(args, {
|
|
1939
|
+
limit: toInt(args.limit, 30)
|
|
1940
|
+
}),
|
|
1941
|
+
args.apiBaseUrl
|
|
1942
|
+
);
|
|
1943
|
+
if (!result.success) {
|
|
1944
|
+
return { results: [], error: result.error };
|
|
1945
|
+
}
|
|
1946
|
+
return { results: result.data || [] };
|
|
1947
|
+
}
|
|
1948
|
+
});
|
|
1949
|
+
var getSurpriseDetection = action({
|
|
1950
|
+
args: {
|
|
1951
|
+
limit: v.optional(v.number()),
|
|
1952
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1953
|
+
topicId: v.optional(v.string())
|
|
1954
|
+
},
|
|
1955
|
+
returns: permissiveReturn,
|
|
1956
|
+
handler: async (_ctx, args) => {
|
|
1957
|
+
const result = await callNeo4jQuery(
|
|
1958
|
+
"surpriseDetection",
|
|
1959
|
+
withTopicScope(args, {
|
|
1960
|
+
limit: toInt(args.limit, 30)
|
|
1961
|
+
}),
|
|
1962
|
+
args.apiBaseUrl
|
|
1963
|
+
);
|
|
1964
|
+
if (!result.success) {
|
|
1965
|
+
return { results: [], error: result.error };
|
|
1966
|
+
}
|
|
1967
|
+
return { results: result.data || [] };
|
|
1968
|
+
}
|
|
1969
|
+
});
|
|
1970
|
+
var getNonConsensusBeliefs = action({
|
|
1971
|
+
args: {
|
|
1972
|
+
limit: v.optional(v.number()),
|
|
1973
|
+
apiBaseUrl: v.optional(v.string()),
|
|
1974
|
+
topicId: v.optional(v.string())
|
|
1975
|
+
},
|
|
1976
|
+
returns: permissiveReturn,
|
|
1977
|
+
handler: async (_ctx, args) => {
|
|
1978
|
+
const result = await callNeo4jQuery(
|
|
1979
|
+
"nonConsensusBeliefs",
|
|
1980
|
+
withTopicScope(args, {
|
|
1981
|
+
limit: toInt(args.limit, 30)
|
|
1982
|
+
}),
|
|
1983
|
+
args.apiBaseUrl
|
|
1984
|
+
);
|
|
1985
|
+
if (!result.success) {
|
|
1986
|
+
return { results: [], error: result.error };
|
|
1987
|
+
}
|
|
1988
|
+
return { results: result.data || [] };
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
var EMBEDDING_DIMENSIONS = 1024;
|
|
1992
|
+
var VALID_INDEX_NAMES = /* @__PURE__ */ new Set([
|
|
1993
|
+
"belief_embedding_index",
|
|
1994
|
+
"question_embedding_index",
|
|
1995
|
+
"evidence_embedding_index",
|
|
1996
|
+
"theme_embedding_index",
|
|
1997
|
+
"source_embedding_index",
|
|
1998
|
+
"synthesis_embedding_index"
|
|
1999
|
+
]);
|
|
2000
|
+
function clamp(value, min, max) {
|
|
2001
|
+
return Math.max(min, Math.min(max, value));
|
|
2002
|
+
}
|
|
2003
|
+
var semanticSearch = action({
|
|
2004
|
+
args: {
|
|
2005
|
+
embedding: v.array(v.float64()),
|
|
2006
|
+
indexName: v.optional(v.string()),
|
|
2007
|
+
topK: v.optional(v.number()),
|
|
2008
|
+
minScore: v.optional(v.number()),
|
|
2009
|
+
apiBaseUrl: v.optional(v.string()),
|
|
2010
|
+
topicId: v.optional(v.string())
|
|
2011
|
+
},
|
|
2012
|
+
returns: permissiveReturn,
|
|
2013
|
+
handler: async (_ctx, args) => {
|
|
2014
|
+
if (args.embedding.length !== EMBEDDING_DIMENSIONS) {
|
|
2015
|
+
return {
|
|
2016
|
+
results: [],
|
|
2017
|
+
error: `Embedding must be ${EMBEDDING_DIMENSIONS} dimensions, got ${args.embedding.length}`
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
const indexName = args.indexName || "belief_embedding_index";
|
|
2021
|
+
if (!VALID_INDEX_NAMES.has(indexName)) {
|
|
2022
|
+
return { results: [], error: `Invalid index name: ${indexName}` };
|
|
2023
|
+
}
|
|
2024
|
+
const result = await callNeo4jQuery(
|
|
2025
|
+
"semanticSearch",
|
|
2026
|
+
{
|
|
2027
|
+
embedding: args.embedding,
|
|
2028
|
+
indexName,
|
|
2029
|
+
topK: toInt(args.topK, 10),
|
|
2030
|
+
minScore: clamp(args.minScore ?? 0.5, 0, 1)
|
|
2031
|
+
},
|
|
2032
|
+
args.apiBaseUrl
|
|
2033
|
+
);
|
|
2034
|
+
if (!result.success) {
|
|
2035
|
+
return { results: [], error: result.error };
|
|
2036
|
+
}
|
|
2037
|
+
return { results: result.data || [] };
|
|
2038
|
+
}
|
|
2039
|
+
});
|
|
2040
|
+
var getSemanticOrphans = action({
|
|
2041
|
+
args: {
|
|
2042
|
+
threshold: v.optional(v.number()),
|
|
2043
|
+
limit: v.optional(v.number()),
|
|
2044
|
+
apiBaseUrl: v.optional(v.string()),
|
|
2045
|
+
topicId: v.optional(v.string())
|
|
2046
|
+
},
|
|
2047
|
+
returns: permissiveReturn,
|
|
2048
|
+
handler: async (_ctx, args) => {
|
|
2049
|
+
const result = await callNeo4jQuery(
|
|
2050
|
+
"semanticOrphans",
|
|
2051
|
+
withTopicScope(args, {
|
|
2052
|
+
threshold: clamp(args.threshold ?? 0.4, 0, 1),
|
|
2053
|
+
limit: toInt(args.limit, 20)
|
|
2054
|
+
}),
|
|
2055
|
+
args.apiBaseUrl
|
|
2056
|
+
);
|
|
2057
|
+
if (!result.success) {
|
|
2058
|
+
return { results: [], error: result.error };
|
|
2059
|
+
}
|
|
2060
|
+
return { results: result.data || [] };
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
var getSoftContradictions = action({
|
|
2064
|
+
args: {
|
|
2065
|
+
similarityThreshold: v.optional(v.number()),
|
|
2066
|
+
limit: v.optional(v.number()),
|
|
2067
|
+
apiBaseUrl: v.optional(v.string()),
|
|
2068
|
+
topicId: v.optional(v.string())
|
|
2069
|
+
},
|
|
2070
|
+
returns: permissiveReturn,
|
|
2071
|
+
handler: async (_ctx, args) => {
|
|
2072
|
+
const result = await callNeo4jQuery(
|
|
2073
|
+
"softContradictions",
|
|
2074
|
+
withTopicScope(args, {
|
|
2075
|
+
similarityThreshold: clamp(args.similarityThreshold ?? 0.7, 0, 1),
|
|
2076
|
+
limit: toInt(args.limit, 20)
|
|
2077
|
+
}),
|
|
2078
|
+
args.apiBaseUrl
|
|
2079
|
+
);
|
|
2080
|
+
if (!result.success) {
|
|
2081
|
+
return { results: [], error: result.error };
|
|
2082
|
+
}
|
|
2083
|
+
return { results: result.data || [] };
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
var getSemanticBridges = action({
|
|
2087
|
+
args: {
|
|
2088
|
+
similarityThreshold: v.optional(v.number()),
|
|
2089
|
+
limit: v.optional(v.number()),
|
|
2090
|
+
apiBaseUrl: v.optional(v.string()),
|
|
2091
|
+
topicId: v.optional(v.string())
|
|
2092
|
+
},
|
|
2093
|
+
returns: permissiveReturn,
|
|
2094
|
+
handler: async (_ctx, args) => {
|
|
2095
|
+
const result = await callNeo4jQuery(
|
|
2096
|
+
"semanticBridges",
|
|
2097
|
+
withTopicScope(args, {
|
|
2098
|
+
similarityThreshold: clamp(args.similarityThreshold ?? 0.7, 0, 1),
|
|
2099
|
+
limit: toInt(args.limit, 20)
|
|
2100
|
+
}),
|
|
2101
|
+
args.apiBaseUrl
|
|
2102
|
+
);
|
|
2103
|
+
if (!result.success) {
|
|
2104
|
+
return { results: [], error: result.error };
|
|
2105
|
+
}
|
|
2106
|
+
return { results: result.data || [] };
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
var graphAwareSearch = action({
|
|
2110
|
+
args: {
|
|
2111
|
+
embedding: v.array(v.float64()),
|
|
2112
|
+
indexName: v.optional(v.string()),
|
|
2113
|
+
topK: v.optional(v.number()),
|
|
2114
|
+
minScore: v.optional(v.number()),
|
|
2115
|
+
apiBaseUrl: v.optional(v.string()),
|
|
2116
|
+
topicId: v.optional(v.string())
|
|
2117
|
+
},
|
|
2118
|
+
returns: permissiveReturn,
|
|
2119
|
+
handler: async (_ctx, args) => {
|
|
2120
|
+
if (args.embedding.length !== EMBEDDING_DIMENSIONS) {
|
|
2121
|
+
return {
|
|
2122
|
+
results: [],
|
|
2123
|
+
error: `Embedding must be ${EMBEDDING_DIMENSIONS} dimensions, got ${args.embedding.length}`
|
|
2124
|
+
};
|
|
2125
|
+
}
|
|
2126
|
+
const indexName = args.indexName || "belief_embedding_index";
|
|
2127
|
+
if (!VALID_INDEX_NAMES.has(indexName)) {
|
|
2128
|
+
return { results: [], error: `Invalid index name: ${indexName}` };
|
|
2129
|
+
}
|
|
2130
|
+
const result = await callNeo4jQuery(
|
|
2131
|
+
"graphAwareSearch",
|
|
2132
|
+
withTopicScope(args, {
|
|
2133
|
+
embedding: args.embedding,
|
|
2134
|
+
indexName,
|
|
2135
|
+
topK: toInt(args.topK, 10),
|
|
2136
|
+
minScore: clamp(args.minScore ?? 0.5, 0, 1)
|
|
2137
|
+
}),
|
|
2138
|
+
args.apiBaseUrl
|
|
2139
|
+
);
|
|
2140
|
+
if (!result.success) {
|
|
2141
|
+
return { results: [], error: result.error };
|
|
2142
|
+
}
|
|
2143
|
+
return { results: result.data || [] };
|
|
2144
|
+
}
|
|
2145
|
+
});
|
|
2146
|
+
|
|
2147
|
+
// src/neo4jQueryRoute.ts
|
|
2148
|
+
var neo4jQueryRoute_exports = {};
|
|
2149
|
+
__export(neo4jQueryRoute_exports, {
|
|
2150
|
+
createNeo4jQueryRouteHandler: () => createNeo4jQueryRouteHandler
|
|
2151
|
+
});
|
|
2152
|
+
function jsonResponse(body, init) {
|
|
2153
|
+
return new Response(JSON.stringify(body), {
|
|
2154
|
+
...init,
|
|
2155
|
+
headers: {
|
|
2156
|
+
"Content-Type": "application/json",
|
|
2157
|
+
...init?.headers
|
|
2158
|
+
}
|
|
2159
|
+
});
|
|
2160
|
+
}
|
|
2161
|
+
function readBearerSecret(request) {
|
|
2162
|
+
const authorization = request.headers.get("authorization") ?? "";
|
|
2163
|
+
const match = authorization.match(/^Bearer\s+(.+)$/iu);
|
|
2164
|
+
return match?.[1]?.trim() || null;
|
|
2165
|
+
}
|
|
2166
|
+
function hasTenantContext(params) {
|
|
2167
|
+
const tenantId = params.tenantId;
|
|
2168
|
+
const topicId = params.topicId;
|
|
2169
|
+
const projectId = params.projectId;
|
|
2170
|
+
return [tenantId, topicId, projectId].some(
|
|
2171
|
+
(value) => typeof value === "string" && value.trim().length > 0
|
|
2172
|
+
);
|
|
2173
|
+
}
|
|
2174
|
+
function normalizeConnectedNodesQuery(queryName, params, queries) {
|
|
2175
|
+
if (queryName !== "connectedNodes") {
|
|
2176
|
+
return queryName;
|
|
2177
|
+
}
|
|
2178
|
+
const hops = typeof params.maxHops === "number" && Number.isFinite(params.maxHops) ? Math.min(Math.max(Math.floor(params.maxHops), 1), 5) : 2;
|
|
2179
|
+
const aliased = `connectedNodes${hops}`;
|
|
2180
|
+
return queries[aliased] ? aliased : queryName;
|
|
2181
|
+
}
|
|
2182
|
+
function createNeo4jQueryRouteHandler(options) {
|
|
2183
|
+
return async function handleNeo4jQuery(request) {
|
|
2184
|
+
const expectedSecret = options.syncSecret ?? process.env.NEO4J_SYNC_SECRET?.trim();
|
|
2185
|
+
if (!expectedSecret) {
|
|
2186
|
+
return jsonResponse(
|
|
2187
|
+
{ error: "Neo4j sync secret not configured" },
|
|
2188
|
+
{ status: 500 }
|
|
2189
|
+
);
|
|
2190
|
+
}
|
|
2191
|
+
if (readBearerSecret(request) !== expectedSecret) {
|
|
2192
|
+
return jsonResponse({ error: "Unauthorized" }, { status: 401 });
|
|
2193
|
+
}
|
|
2194
|
+
let body;
|
|
2195
|
+
try {
|
|
2196
|
+
body = await request.json();
|
|
2197
|
+
} catch {
|
|
2198
|
+
return jsonResponse({ error: "Invalid JSON body" }, { status: 400 });
|
|
2199
|
+
}
|
|
2200
|
+
if (typeof body.queryName !== "string" || body.queryName.length === 0) {
|
|
2201
|
+
return jsonResponse({ error: "Missing queryName" }, { status: 400 });
|
|
2202
|
+
}
|
|
2203
|
+
const params = body.params && typeof body.params === "object" && !Array.isArray(body.params) ? body.params : {};
|
|
2204
|
+
if ((options.requireTenantContext ?? true) && !hasTenantContext(params)) {
|
|
2205
|
+
return jsonResponse(
|
|
2206
|
+
{ error: "Missing required tenant context" },
|
|
2207
|
+
{ status: 400 }
|
|
2208
|
+
);
|
|
2209
|
+
}
|
|
2210
|
+
const queryName = normalizeConnectedNodesQuery(
|
|
2211
|
+
body.queryName,
|
|
2212
|
+
params,
|
|
2213
|
+
options.queries
|
|
2214
|
+
);
|
|
2215
|
+
const query = options.queries[queryName];
|
|
2216
|
+
if (!query) {
|
|
2217
|
+
return jsonResponse(
|
|
2218
|
+
{ error: `Unknown query: ${body.queryName}` },
|
|
2219
|
+
{ status: 400 }
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
try {
|
|
2223
|
+
const data = await runCypher(
|
|
2224
|
+
query.cypher,
|
|
2225
|
+
params,
|
|
2226
|
+
query.timeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS
|
|
2227
|
+
);
|
|
2228
|
+
return jsonResponse({ data, queryName });
|
|
2229
|
+
} catch (error) {
|
|
2230
|
+
return jsonResponse(
|
|
2231
|
+
{
|
|
2232
|
+
error: error instanceof Error ? error.message : "Neo4j query failed",
|
|
2233
|
+
queryName
|
|
2234
|
+
},
|
|
2235
|
+
{ status: 500 }
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
};
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// src/neo4jSync.ts
|
|
2242
|
+
var neo4jSync_exports = {};
|
|
2243
|
+
__export(neo4jSync_exports, {
|
|
2244
|
+
backfillAllToNeo4j: () => backfillAllToNeo4j,
|
|
2245
|
+
checkNeo4jHealth: () => checkNeo4jHealth,
|
|
2246
|
+
processRetryQueue: () => processRetryQueue,
|
|
2247
|
+
resyncAllEdges: () => resyncAllEdges,
|
|
2248
|
+
resyncAllNodes: () => resyncAllNodes,
|
|
2249
|
+
syncAllEdgesToNeo4j: () => syncAllEdgesToNeo4j,
|
|
2250
|
+
syncAllNodesToNeo4j: () => syncAllNodesToNeo4j,
|
|
2251
|
+
syncEdgeToNeo4j: () => syncEdgeToNeo4j,
|
|
2252
|
+
syncEmbeddingToNeo4j: () => syncEmbeddingToNeo4j,
|
|
2253
|
+
syncNodeToNeo4j: () => syncNodeToNeo4j
|
|
2254
|
+
});
|
|
2255
|
+
function buildSyncResponse(entityType, operation, fields) {
|
|
2256
|
+
return {
|
|
2257
|
+
entityType,
|
|
2258
|
+
operation,
|
|
2259
|
+
...fields
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2262
|
+
function buildSyncSuccess(entityType, operation, fields) {
|
|
2263
|
+
return buildSyncResponse(entityType, operation, {
|
|
2264
|
+
success: true,
|
|
2265
|
+
...fields
|
|
2266
|
+
});
|
|
2267
|
+
}
|
|
2268
|
+
function buildSyncFailure(entityType, operation, error, fields) {
|
|
2269
|
+
return buildSyncResponse(entityType, operation, {
|
|
2270
|
+
success: false,
|
|
2271
|
+
error,
|
|
2272
|
+
...fields
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
function buildNodeProperties(node) {
|
|
2276
|
+
const metadata = node.metadata || {};
|
|
2277
|
+
const pillar = metadata.pillar || metadata.topic || "";
|
|
2278
|
+
const stage = metadata.stage || metadata.beliefStage || "";
|
|
2279
|
+
const criticality = metadata.criticality || "";
|
|
2280
|
+
const synthesizedFrom = metadata.synthesizedFrom || [];
|
|
2281
|
+
const epistemicStatus = node.epistemicStatus || "";
|
|
2282
|
+
const methodology = node.methodology || "";
|
|
2283
|
+
const informationAsymmetry = node.informationAsymmetry || "";
|
|
2284
|
+
const questionType = node.questionType || "";
|
|
2285
|
+
const questionPriority = node.questionPriority || "";
|
|
2286
|
+
const answerQuality = node.answerQuality || "";
|
|
2287
|
+
const reversibility = node.reversibility || "";
|
|
2288
|
+
const predictionMeta = node.predictionMeta;
|
|
2289
|
+
return {
|
|
2290
|
+
convexId: node._id,
|
|
2291
|
+
canonicalText: node.canonicalText || "",
|
|
2292
|
+
title: node.title || "",
|
|
2293
|
+
status: node.status || "active",
|
|
2294
|
+
subtype: node.subtype || "",
|
|
2295
|
+
domain: node.domain || "",
|
|
2296
|
+
confidence: node.confidence || 0,
|
|
2297
|
+
verificationStatus: node.verificationStatus || "unverified",
|
|
2298
|
+
sourceType: node.sourceType || "unknown",
|
|
2299
|
+
createdAt: node.createdAt,
|
|
2300
|
+
updatedAt: node.updatedAt || Date.now(),
|
|
2301
|
+
createdBy: node.createdBy || "",
|
|
2302
|
+
nodeType: node.nodeType,
|
|
2303
|
+
epistemicLayer: node.epistemicLayer || "",
|
|
2304
|
+
// Project and metadata fields
|
|
2305
|
+
projectId: node.projectId || "",
|
|
2306
|
+
tenantId: node.tenantId || "",
|
|
2307
|
+
workspaceId: node.workspaceId || "",
|
|
2308
|
+
pillar,
|
|
2309
|
+
stage,
|
|
2310
|
+
criticality,
|
|
2311
|
+
synthesizedFromCount: synthesizedFrom.length,
|
|
2312
|
+
// Classification fields (Logic Machine)
|
|
2313
|
+
epistemicStatus,
|
|
2314
|
+
methodology,
|
|
2315
|
+
informationAsymmetry,
|
|
2316
|
+
questionType,
|
|
2317
|
+
questionPriority,
|
|
2318
|
+
answerQuality,
|
|
2319
|
+
reversibility,
|
|
2320
|
+
isPrediction: predictionMeta?.isPrediction ? "true" : "false",
|
|
2321
|
+
expectedBy: predictionMeta?.expectedBy ? String(predictionMeta.expectedBy) : ""
|
|
2322
|
+
};
|
|
2323
|
+
}
|
|
2324
|
+
function buildEdgeProperties(edge) {
|
|
2325
|
+
return {
|
|
2326
|
+
convexId: edge._id,
|
|
2327
|
+
weight: edge.weight || 1,
|
|
2328
|
+
confidence: edge.confidence || 0,
|
|
2329
|
+
context: edge.context || "",
|
|
2330
|
+
derivationType: edge.derivationType || "",
|
|
2331
|
+
createdAt: edge.createdAt,
|
|
2332
|
+
createdBy: edge.createdBy || "",
|
|
2333
|
+
edgeType: edge.edgeType,
|
|
2334
|
+
fromLayer: edge.fromLayer || "",
|
|
2335
|
+
toLayer: edge.toLayer || "",
|
|
2336
|
+
projectId: edge.projectId || "",
|
|
2337
|
+
tenantId: edge.tenantId || "",
|
|
2338
|
+
workspaceId: edge.workspaceId || "",
|
|
2339
|
+
// Classification fields (Logic Machine)
|
|
2340
|
+
reasoningMethod: edge.reasoningMethod || "",
|
|
2341
|
+
logicalRole: edge.logicalRole || "",
|
|
2342
|
+
temporalClass: edge.temporalClass || ""
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
var syncNodeToNeo4j = internalAction({
|
|
2346
|
+
args: {
|
|
2347
|
+
nodeId: v.id("epistemicNodes"),
|
|
2348
|
+
operation: v.union(v.literal("upsert"), v.literal("delete"))
|
|
2349
|
+
},
|
|
2350
|
+
returns: permissiveReturn,
|
|
2351
|
+
handler: async (ctx, args) => {
|
|
2352
|
+
const connInfo = getConnectionInfo();
|
|
2353
|
+
if (!connInfo.configured) {
|
|
2354
|
+
console.warn(
|
|
2355
|
+
"[Neo4j Sync] Missing Neo4j credentials, skipping sync. Set via `npx convex env set`"
|
|
2356
|
+
);
|
|
2357
|
+
return buildSyncFailure("node", args.operation, "Missing credentials", {
|
|
2358
|
+
skippedReason: "credentials_missing"
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
const node = await ctx.runQuery(internal.neo4jSyncHelpers.getNodeForSync, {
|
|
2362
|
+
nodeId: args.nodeId
|
|
2363
|
+
});
|
|
2364
|
+
if (!node) {
|
|
2365
|
+
if (args.operation === "upsert") {
|
|
2366
|
+
return buildSyncFailure("node", args.operation, "Node not found", {
|
|
2367
|
+
skippedReason: "source_node_missing"
|
|
2368
|
+
});
|
|
2369
|
+
}
|
|
2370
|
+
console.log("[Neo4j Sync] Node not found for delete, skipping");
|
|
2371
|
+
return buildSyncSuccess("node", args.operation, {
|
|
2372
|
+
skipped: true,
|
|
2373
|
+
skippedReason: "source_node_missing"
|
|
2374
|
+
});
|
|
2375
|
+
}
|
|
2376
|
+
const label = NODE_TYPE_TO_LABEL[node.nodeType] || "Node";
|
|
2377
|
+
try {
|
|
2378
|
+
if (args.operation === "delete") {
|
|
2379
|
+
await deleteNode(node.globalId);
|
|
2380
|
+
console.log(`[Neo4j Sync] Deleted node ${node.globalId}`);
|
|
2381
|
+
} else {
|
|
2382
|
+
const props = buildNodeProperties(node);
|
|
2383
|
+
const embedding = await ctx.runQuery(
|
|
2384
|
+
internal.neo4jSyncHelpers.getEmbeddingForSync,
|
|
2385
|
+
{ nodeId: args.nodeId }
|
|
2386
|
+
);
|
|
2387
|
+
if (embedding) {
|
|
2388
|
+
props.embedding = embedding;
|
|
2389
|
+
}
|
|
2390
|
+
await upsertNode(label, node.globalId, props);
|
|
2391
|
+
console.log(
|
|
2392
|
+
`[Neo4j Sync] Upserted node ${node.globalId} as ${label} with projectId=${node.projectId}` + (embedding ? ` (with ${embedding.length}-dim embedding)` : "")
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
|
|
2396
|
+
eventType: args.operation === "delete" ? "node_deleted" : node ? "node_updated" : "node_created",
|
|
2397
|
+
entityId: args.nodeId,
|
|
2398
|
+
entityType: node.nodeType,
|
|
2399
|
+
status: "success"
|
|
2400
|
+
});
|
|
2401
|
+
return buildSyncSuccess("node", args.operation);
|
|
2402
|
+
} catch (error) {
|
|
2403
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2404
|
+
console.error("[Neo4j Sync] Node sync error:", errorMsg);
|
|
2405
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
|
|
2406
|
+
eventType: args.operation === "delete" ? "node_deleted" : "node_updated",
|
|
2407
|
+
entityId: args.nodeId,
|
|
2408
|
+
entityType: node.nodeType,
|
|
2409
|
+
status: "failed",
|
|
2410
|
+
error: errorMsg
|
|
2411
|
+
});
|
|
2412
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.queueForRetry, {
|
|
2413
|
+
entityType: "node",
|
|
2414
|
+
entityId: args.nodeId,
|
|
2415
|
+
operation: args.operation,
|
|
2416
|
+
error: errorMsg
|
|
2417
|
+
});
|
|
2418
|
+
return buildSyncFailure("node", args.operation, errorMsg);
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
});
|
|
2422
|
+
var syncEdgeToNeo4j = internalAction({
|
|
2423
|
+
args: {
|
|
2424
|
+
edgeId: v.id("epistemicEdges"),
|
|
2425
|
+
operation: v.union(v.literal("upsert"), v.literal("delete"))
|
|
2426
|
+
},
|
|
2427
|
+
returns: permissiveReturn,
|
|
2428
|
+
handler: async (ctx, args) => {
|
|
2429
|
+
const connInfo = getConnectionInfo();
|
|
2430
|
+
if (!connInfo.configured) {
|
|
2431
|
+
console.warn(
|
|
2432
|
+
"[Neo4j Sync] Missing Neo4j credentials, skipping sync. Set via `npx convex env set`"
|
|
2433
|
+
);
|
|
2434
|
+
return buildSyncFailure("edge", args.operation, "Missing credentials", {
|
|
2435
|
+
skippedReason: "credentials_missing"
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
const edge = await ctx.runQuery(internal.neo4jSyncHelpers.getEdgeForSync, {
|
|
2439
|
+
edgeId: args.edgeId
|
|
2440
|
+
});
|
|
2441
|
+
if (!edge) {
|
|
2442
|
+
if (args.operation === "upsert") {
|
|
2443
|
+
return buildSyncFailure("edge", args.operation, "Edge not found", {
|
|
2444
|
+
skippedReason: "source_edge_missing"
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
console.log("[Neo4j Sync] Edge not found for delete, skipping");
|
|
2448
|
+
return buildSyncSuccess("edge", args.operation, {
|
|
2449
|
+
skipped: true,
|
|
2450
|
+
skippedReason: "source_edge_missing"
|
|
2451
|
+
});
|
|
2452
|
+
}
|
|
2453
|
+
if (!edge.fromGlobalId || !edge.toGlobalId) {
|
|
2454
|
+
console.warn(
|
|
2455
|
+
"[Neo4j Sync] Edge missing fromGlobalId or toGlobalId, skipping"
|
|
2456
|
+
);
|
|
2457
|
+
return buildSyncFailure(
|
|
2458
|
+
"edge",
|
|
2459
|
+
args.operation,
|
|
2460
|
+
"Edge missing connected node globalIds",
|
|
2461
|
+
{
|
|
2462
|
+
skippedReason: "edge_endpoint_missing"
|
|
2463
|
+
}
|
|
2464
|
+
);
|
|
2465
|
+
}
|
|
2466
|
+
const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
|
|
2467
|
+
try {
|
|
2468
|
+
if (args.operation === "delete") {
|
|
2469
|
+
await deleteEdge(edge.globalId);
|
|
2470
|
+
console.log(`[Neo4j Sync] Deleted edge ${edge.globalId}`);
|
|
2471
|
+
} else {
|
|
2472
|
+
await upsertEdge(
|
|
2473
|
+
relType,
|
|
2474
|
+
edge.globalId,
|
|
2475
|
+
edge.fromGlobalId,
|
|
2476
|
+
edge.toGlobalId,
|
|
2477
|
+
{
|
|
2478
|
+
convexId: args.edgeId,
|
|
2479
|
+
weight: edge.weight || 1,
|
|
2480
|
+
confidence: edge.confidence || 0,
|
|
2481
|
+
context: edge.context || "",
|
|
2482
|
+
derivationType: edge.derivationType || "",
|
|
2483
|
+
createdAt: edge.createdAt,
|
|
2484
|
+
createdBy: edge.createdBy || "",
|
|
2485
|
+
edgeType: edge.edgeType,
|
|
2486
|
+
fromLayer: edge.fromLayer || "",
|
|
2487
|
+
toLayer: edge.toLayer || "",
|
|
2488
|
+
// Classification fields (Logic Machine)
|
|
2489
|
+
reasoningMethod: edge.reasoningMethod || "",
|
|
2490
|
+
logicalRole: edge.logicalRole || "",
|
|
2491
|
+
temporalClass: edge.temporalClass || ""
|
|
2492
|
+
}
|
|
2493
|
+
);
|
|
2494
|
+
console.log(
|
|
2495
|
+
`[Neo4j Sync] Upserted edge ${edge.globalId} as ${relType}`
|
|
2496
|
+
);
|
|
2497
|
+
}
|
|
2498
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
|
|
2499
|
+
eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
|
|
2500
|
+
entityId: args.edgeId,
|
|
2501
|
+
entityType: edge.edgeType,
|
|
2502
|
+
status: "success"
|
|
2503
|
+
});
|
|
2504
|
+
return buildSyncSuccess("edge", args.operation);
|
|
2505
|
+
} catch (error) {
|
|
2506
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2507
|
+
console.error("[Neo4j Sync] Edge sync error:", errorMsg);
|
|
2508
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.logSyncEvent, {
|
|
2509
|
+
eventType: args.operation === "delete" ? "edge_deleted" : "edge_created",
|
|
2510
|
+
entityId: args.edgeId,
|
|
2511
|
+
entityType: edge.edgeType,
|
|
2512
|
+
status: "failed",
|
|
2513
|
+
error: errorMsg
|
|
2514
|
+
});
|
|
2515
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.queueForRetry, {
|
|
2516
|
+
entityType: "edge",
|
|
2517
|
+
entityId: args.edgeId,
|
|
2518
|
+
operation: args.operation,
|
|
2519
|
+
error: errorMsg
|
|
2520
|
+
});
|
|
2521
|
+
return buildSyncFailure("edge", args.operation, errorMsg);
|
|
2522
|
+
}
|
|
2523
|
+
}
|
|
2524
|
+
});
|
|
2525
|
+
var syncAllNodesToNeo4j = internalAction({
|
|
2526
|
+
args: {
|
|
2527
|
+
batchSize: v.optional(v.number()),
|
|
2528
|
+
cursor: v.optional(v.string())
|
|
2529
|
+
},
|
|
2530
|
+
returns: permissiveReturn,
|
|
2531
|
+
handler: async (ctx, args) => {
|
|
2532
|
+
const batchSize = args.batchSize ?? 100;
|
|
2533
|
+
const result = await ctx.runQuery(
|
|
2534
|
+
internal.neo4jSyncHelpers.getNodeBatchForSync,
|
|
2535
|
+
{
|
|
2536
|
+
limit: batchSize,
|
|
2537
|
+
cursor: args.cursor
|
|
2538
|
+
}
|
|
2539
|
+
);
|
|
2540
|
+
if (result.nodes.length === 0) {
|
|
2541
|
+
return { synced: 0, failed: 0, hasMore: false };
|
|
2542
|
+
}
|
|
2543
|
+
const nodesByLabel = /* @__PURE__ */ new Map();
|
|
2544
|
+
for (const node of result.nodes) {
|
|
2545
|
+
const label = NODE_TYPE_TO_LABEL[node.nodeType] || "Node";
|
|
2546
|
+
if (!nodesByLabel.has(label)) {
|
|
2547
|
+
nodesByLabel.set(label, []);
|
|
2548
|
+
}
|
|
2549
|
+
nodesByLabel.get(label)?.push({
|
|
2550
|
+
globalId: node.globalId,
|
|
2551
|
+
properties: buildNodeProperties(node)
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
let synced = 0;
|
|
2555
|
+
let failed = 0;
|
|
2556
|
+
for (const [label, nodes] of nodesByLabel) {
|
|
2557
|
+
try {
|
|
2558
|
+
await batchUpsertNodes(label, nodes);
|
|
2559
|
+
synced += nodes.length;
|
|
2560
|
+
console.log(
|
|
2561
|
+
`[Neo4j Sync] Batch upserted ${nodes.length} ${label} nodes`
|
|
2562
|
+
);
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
console.error(`[Neo4j Sync] Batch upsert failed for ${label}:`, error);
|
|
2565
|
+
failed += nodes.length;
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
return {
|
|
2569
|
+
synced,
|
|
2570
|
+
failed,
|
|
2571
|
+
hasMore: result.hasMore,
|
|
2572
|
+
nextCursor: result.nextCursor
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2575
|
+
});
|
|
2576
|
+
var syncAllEdgesToNeo4j = internalAction({
|
|
2577
|
+
args: {
|
|
2578
|
+
batchSize: v.optional(v.number()),
|
|
2579
|
+
cursor: v.optional(v.string())
|
|
2580
|
+
},
|
|
2581
|
+
returns: permissiveReturn,
|
|
2582
|
+
handler: async (ctx, args) => {
|
|
2583
|
+
const batchSize = args.batchSize ?? 100;
|
|
2584
|
+
const result = await ctx.runQuery(
|
|
2585
|
+
internal.neo4jSyncHelpers.getEdgeBatchForSync,
|
|
2586
|
+
{
|
|
2587
|
+
limit: batchSize,
|
|
2588
|
+
cursor: args.cursor
|
|
2589
|
+
}
|
|
2590
|
+
);
|
|
2591
|
+
if (result.edges.length === 0) {
|
|
2592
|
+
return { synced: 0, failed: 0, hasMore: false };
|
|
2593
|
+
}
|
|
2594
|
+
const edgesToSync = [];
|
|
2595
|
+
for (const edge of result.edges) {
|
|
2596
|
+
if (!edge.fromGlobalId || !edge.toGlobalId) {
|
|
2597
|
+
console.warn(
|
|
2598
|
+
`[Neo4j Sync] Skipping edge ${edge.globalId} - missing globalIds`
|
|
2599
|
+
);
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
const relType = EDGE_TYPE_TO_REL[edge.edgeType] || edge.edgeType.toUpperCase();
|
|
2603
|
+
edgesToSync.push({
|
|
2604
|
+
relType,
|
|
2605
|
+
globalId: edge.globalId,
|
|
2606
|
+
fromGlobalId: edge.fromGlobalId,
|
|
2607
|
+
toGlobalId: edge.toGlobalId,
|
|
2608
|
+
properties: buildEdgeProperties(edge)
|
|
2609
|
+
});
|
|
2610
|
+
}
|
|
2611
|
+
let synced = 0;
|
|
2612
|
+
let failed = 0;
|
|
2613
|
+
try {
|
|
2614
|
+
await batchUpsertEdges(edgesToSync);
|
|
2615
|
+
synced = edgesToSync.length;
|
|
2616
|
+
console.log(`[Neo4j Sync] Batch upserted ${synced} edges`);
|
|
2617
|
+
} catch (error) {
|
|
2618
|
+
console.error("[Neo4j Sync] Batch edge upsert failed:", error);
|
|
2619
|
+
failed = edgesToSync.length;
|
|
2620
|
+
}
|
|
2621
|
+
return {
|
|
2622
|
+
synced,
|
|
2623
|
+
failed,
|
|
2624
|
+
hasMore: result.hasMore,
|
|
2625
|
+
nextCursor: result.nextCursor
|
|
2626
|
+
};
|
|
2627
|
+
}
|
|
2628
|
+
});
|
|
2629
|
+
var backfillAllToNeo4j = internalAction({
|
|
2630
|
+
args: {
|
|
2631
|
+
batchSize: v.optional(v.number())
|
|
2632
|
+
},
|
|
2633
|
+
returns: permissiveReturn,
|
|
2634
|
+
handler: async (ctx, args) => {
|
|
2635
|
+
const batchSize = args.batchSize ?? 100;
|
|
2636
|
+
let totalNodes = 0;
|
|
2637
|
+
let totalEdges = 0;
|
|
2638
|
+
let totalFailed = 0;
|
|
2639
|
+
console.log("[Neo4j Sync] Starting full backfill...");
|
|
2640
|
+
let nodeCursor;
|
|
2641
|
+
do {
|
|
2642
|
+
const result = await ctx.runAction(
|
|
2643
|
+
internal.neo4jSync.syncAllNodesToNeo4j,
|
|
2644
|
+
{
|
|
2645
|
+
batchSize,
|
|
2646
|
+
cursor: nodeCursor
|
|
2647
|
+
}
|
|
2648
|
+
);
|
|
2649
|
+
totalNodes += result.synced;
|
|
2650
|
+
totalFailed += result.failed;
|
|
2651
|
+
nodeCursor = result.hasMore ? result.nextCursor : void 0;
|
|
2652
|
+
console.log(
|
|
2653
|
+
`[Neo4j Sync] Nodes progress: ${totalNodes} synced, ${totalFailed} failed`
|
|
2654
|
+
);
|
|
2655
|
+
} while (nodeCursor);
|
|
2656
|
+
console.log(`[Neo4j Sync] Finished nodes: ${totalNodes} synced`);
|
|
2657
|
+
let edgeCursor;
|
|
2658
|
+
do {
|
|
2659
|
+
const result = await ctx.runAction(
|
|
2660
|
+
internal.neo4jSync.syncAllEdgesToNeo4j,
|
|
2661
|
+
{
|
|
2662
|
+
batchSize,
|
|
2663
|
+
cursor: edgeCursor
|
|
2664
|
+
}
|
|
2665
|
+
);
|
|
2666
|
+
totalEdges += result.synced;
|
|
2667
|
+
totalFailed += result.failed;
|
|
2668
|
+
edgeCursor = result.hasMore ? result.nextCursor : void 0;
|
|
2669
|
+
console.log(
|
|
2670
|
+
`[Neo4j Sync] Edges progress: ${totalEdges} synced, ${totalFailed} failed`
|
|
2671
|
+
);
|
|
2672
|
+
} while (edgeCursor);
|
|
2673
|
+
console.log(
|
|
2674
|
+
`[Neo4j Sync] Backfill complete: ${totalNodes} nodes, ${totalEdges} edges, ${totalFailed} failed`
|
|
2675
|
+
);
|
|
2676
|
+
return {
|
|
2677
|
+
totalNodes,
|
|
2678
|
+
totalEdges,
|
|
2679
|
+
totalFailed
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
});
|
|
2683
|
+
var processRetryQueue = internalAction({
|
|
2684
|
+
args: {
|
|
2685
|
+
limit: v.optional(v.number())
|
|
2686
|
+
},
|
|
2687
|
+
returns: permissiveReturn,
|
|
2688
|
+
handler: async (ctx, args) => {
|
|
2689
|
+
const limit = args.limit ?? 10;
|
|
2690
|
+
const pendingItems = await ctx.runQuery(
|
|
2691
|
+
internal.neo4jSyncHelpers.getPendingRetries,
|
|
2692
|
+
{ limit }
|
|
2693
|
+
);
|
|
2694
|
+
if (pendingItems.length === 0) {
|
|
2695
|
+
return { processed: 0, succeeded: 0, failed: 0 };
|
|
2696
|
+
}
|
|
2697
|
+
let succeeded = 0;
|
|
2698
|
+
let failed = 0;
|
|
2699
|
+
for (const item of pendingItems) {
|
|
2700
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.updateQueueStatus, {
|
|
2701
|
+
queueId: item._id,
|
|
2702
|
+
status: "in_progress"
|
|
2703
|
+
});
|
|
2704
|
+
let result;
|
|
2705
|
+
if (item.entityType === "node") {
|
|
2706
|
+
result = await ctx.runAction(internal.neo4jSync.syncNodeToNeo4j, {
|
|
2707
|
+
nodeId: item.entityId,
|
|
2708
|
+
operation: item.operation
|
|
2709
|
+
});
|
|
2710
|
+
} else {
|
|
2711
|
+
const resolved = await ctx.runQuery(
|
|
2712
|
+
internal.neo4jSyncHelpers.resolveEdgeRetryTarget,
|
|
2713
|
+
{
|
|
2714
|
+
entityId: item.entityId
|
|
2715
|
+
}
|
|
2716
|
+
);
|
|
2717
|
+
if (resolved.mode === "convex_id" || resolved.mode === "global_id_in_convex") {
|
|
2718
|
+
result = await ctx.runAction(internal.neo4jSync.syncEdgeToNeo4j, {
|
|
2719
|
+
edgeId: resolved.edgeId,
|
|
2720
|
+
operation: item.operation
|
|
2721
|
+
});
|
|
2722
|
+
} else {
|
|
2723
|
+
result = await ctx.runAction(
|
|
2724
|
+
internal.neo4jEdgeAPI.retryProjectionByGlobalId,
|
|
2725
|
+
{
|
|
2726
|
+
globalId: resolved.edgeGlobalId
|
|
2727
|
+
}
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
}
|
|
2731
|
+
if (result.success) {
|
|
2732
|
+
await ctx.runMutation(internal.neo4jSyncHelpers.updateQueueStatus, {
|
|
2733
|
+
queueId: item._id,
|
|
2734
|
+
status: "succeeded"
|
|
2735
|
+
});
|
|
2736
|
+
succeeded++;
|
|
2737
|
+
} else {
|
|
2738
|
+
const updated = await ctx.runMutation(
|
|
2739
|
+
internal.neo4jSyncHelpers.incrementAttempts,
|
|
2740
|
+
{
|
|
2741
|
+
queueId: item._id,
|
|
2742
|
+
error: result.error || "Unknown error"
|
|
2743
|
+
}
|
|
2744
|
+
);
|
|
2745
|
+
if (updated.failed) {
|
|
2746
|
+
failed++;
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
}
|
|
2750
|
+
return { processed: pendingItems.length, succeeded, failed };
|
|
2751
|
+
}
|
|
2752
|
+
});
|
|
2753
|
+
var syncEmbeddingToNeo4j = internalAction({
|
|
2754
|
+
args: {
|
|
2755
|
+
nodeId: v.id("epistemicNodes")
|
|
2756
|
+
},
|
|
2757
|
+
returns: permissiveReturn,
|
|
2758
|
+
handler: async (ctx, args) => {
|
|
2759
|
+
const connInfo = getConnectionInfo();
|
|
2760
|
+
if (!connInfo.configured) {
|
|
2761
|
+
return buildSyncFailure("embedding", "sync", "Missing credentials", {
|
|
2762
|
+
skippedReason: "credentials_missing"
|
|
2763
|
+
});
|
|
2764
|
+
}
|
|
2765
|
+
const node = await ctx.runQuery(internal.neo4jSyncHelpers.getNodeForSync, {
|
|
2766
|
+
nodeId: args.nodeId
|
|
2767
|
+
});
|
|
2768
|
+
if (!node?.globalId) {
|
|
2769
|
+
return buildSyncFailure("embedding", "sync", "Node not found", {
|
|
2770
|
+
skippedReason: "source_node_missing"
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
const embedding = await ctx.runQuery(
|
|
2774
|
+
internal.neo4jSyncHelpers.getEmbeddingForSync,
|
|
2775
|
+
{ nodeId: args.nodeId }
|
|
2776
|
+
);
|
|
2777
|
+
if (!embedding) {
|
|
2778
|
+
return buildSyncFailure("embedding", "sync", "Embedding not found", {
|
|
2779
|
+
skippedReason: "embedding_missing"
|
|
2780
|
+
});
|
|
2781
|
+
}
|
|
2782
|
+
try {
|
|
2783
|
+
await runWriteTransaction(
|
|
2784
|
+
"MATCH (n {globalId: $globalId}) SET n.embedding = $embedding",
|
|
2785
|
+
{ globalId: node.globalId, embedding }
|
|
2786
|
+
);
|
|
2787
|
+
console.log(
|
|
2788
|
+
`[Neo4j Sync] Synced ${embedding.length}-dim embedding for node ${node.globalId}`
|
|
2789
|
+
);
|
|
2790
|
+
return buildSyncSuccess("embedding", "sync");
|
|
2791
|
+
} catch (error) {
|
|
2792
|
+
const errorMsg = error instanceof Error ? error.message : "Unknown error";
|
|
2793
|
+
console.error("[Neo4j Sync] Embedding sync error:", errorMsg);
|
|
2794
|
+
return buildSyncFailure("embedding", "sync", errorMsg);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
});
|
|
2798
|
+
var checkNeo4jHealth = internalAction({
|
|
2799
|
+
args: {},
|
|
2800
|
+
returns: permissiveReturn,
|
|
2801
|
+
handler: async () => {
|
|
2802
|
+
const connInfo = getConnectionInfo();
|
|
2803
|
+
if (!connInfo.configured) {
|
|
2804
|
+
return {
|
|
2805
|
+
healthy: false,
|
|
2806
|
+
error: "Neo4j not configured",
|
|
2807
|
+
uri: connInfo.uri
|
|
2808
|
+
};
|
|
2809
|
+
}
|
|
2810
|
+
const health = await healthCheck();
|
|
2811
|
+
return {
|
|
2812
|
+
...health,
|
|
2813
|
+
uri: connInfo.uri
|
|
2814
|
+
};
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2817
|
+
var resyncAllNodes = internalAction({
|
|
2818
|
+
args: {
|
|
2819
|
+
batchSize: v.optional(v.number()),
|
|
2820
|
+
nodeType: v.optional(v.string()),
|
|
2821
|
+
cursor: v.optional(v.string())
|
|
2822
|
+
},
|
|
2823
|
+
returns: permissiveReturn,
|
|
2824
|
+
handler: async (ctx, args) => {
|
|
2825
|
+
const batchSize = args.batchSize ?? 50;
|
|
2826
|
+
const result = await ctx.runQuery(
|
|
2827
|
+
internal.neo4jSyncHelpers.getAllNodesForResync,
|
|
2828
|
+
{
|
|
2829
|
+
nodeType: args.nodeType,
|
|
2830
|
+
limit: batchSize,
|
|
2831
|
+
cursor: args.cursor
|
|
2832
|
+
}
|
|
2833
|
+
);
|
|
2834
|
+
let synced = 0;
|
|
2835
|
+
let failed = 0;
|
|
2836
|
+
for (const node of result.nodes) {
|
|
2837
|
+
try {
|
|
2838
|
+
const syncResult = await ctx.runAction(
|
|
2839
|
+
internal.neo4jSync.syncNodeToNeo4j,
|
|
2840
|
+
{
|
|
2841
|
+
nodeId: node._id,
|
|
2842
|
+
operation: "upsert"
|
|
2843
|
+
}
|
|
2844
|
+
);
|
|
2845
|
+
if (syncResult.success) {
|
|
2846
|
+
synced++;
|
|
2847
|
+
} else {
|
|
2848
|
+
failed++;
|
|
2849
|
+
}
|
|
2850
|
+
} catch (error) {
|
|
2851
|
+
console.error(`[Neo4j Resync] Failed to sync node ${node._id}:`, error);
|
|
2852
|
+
failed++;
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return {
|
|
2856
|
+
synced,
|
|
2857
|
+
failed,
|
|
2858
|
+
total: result.nodes.length,
|
|
2859
|
+
hasMore: result.hasMore,
|
|
2860
|
+
nextCursor: result.nextCursor
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
});
|
|
2864
|
+
var resyncAllEdges = internalAction({
|
|
2865
|
+
args: {
|
|
2866
|
+
batchSize: v.optional(v.number()),
|
|
2867
|
+
cursor: v.optional(v.string())
|
|
2868
|
+
},
|
|
2869
|
+
returns: permissiveReturn,
|
|
2870
|
+
handler: async (ctx, args) => {
|
|
2871
|
+
const batchSize = args.batchSize ?? 50;
|
|
2872
|
+
const result = await ctx.runQuery(
|
|
2873
|
+
internal.neo4jSyncHelpers.getAllEdgesForResync,
|
|
2874
|
+
{
|
|
2875
|
+
limit: batchSize,
|
|
2876
|
+
cursor: args.cursor
|
|
2877
|
+
}
|
|
2878
|
+
);
|
|
2879
|
+
let synced = 0;
|
|
2880
|
+
let failed = 0;
|
|
2881
|
+
for (const edge of result.edges) {
|
|
2882
|
+
try {
|
|
2883
|
+
const syncResult = await ctx.runAction(
|
|
2884
|
+
internal.neo4jSync.syncEdgeToNeo4j,
|
|
2885
|
+
{
|
|
2886
|
+
edgeId: edge._id,
|
|
2887
|
+
operation: "upsert"
|
|
2888
|
+
}
|
|
2889
|
+
);
|
|
2890
|
+
if (syncResult.success) {
|
|
2891
|
+
synced++;
|
|
2892
|
+
} else {
|
|
2893
|
+
failed++;
|
|
2894
|
+
}
|
|
2895
|
+
} catch (error) {
|
|
2896
|
+
console.error(`[Neo4j Resync] Failed to sync edge ${edge._id}:`, error);
|
|
2897
|
+
failed++;
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
return {
|
|
2901
|
+
synced,
|
|
2902
|
+
failed,
|
|
2903
|
+
total: result.edges.length,
|
|
2904
|
+
hasMore: result.hasMore,
|
|
2905
|
+
nextCursor: result.nextCursor
|
|
2906
|
+
};
|
|
2907
|
+
}
|
|
2908
|
+
});
|
|
2909
|
+
|
|
2910
|
+
// src/neo4jSyncHelpers.ts
|
|
2911
|
+
var neo4jSyncHelpers_exports = {};
|
|
2912
|
+
__export(neo4jSyncHelpers_exports, {
|
|
2913
|
+
checkSyncHealth: () => checkSyncHealth,
|
|
2914
|
+
getAllEdgesForResync: () => getAllEdgesForResync,
|
|
2915
|
+
getAllNodesForResync: () => getAllNodesForResync,
|
|
2916
|
+
getEdgeBatchForSync: () => getEdgeBatchForSync,
|
|
2917
|
+
getEdgeForSync: () => getEdgeForSync,
|
|
2918
|
+
getEmbeddingForSync: () => getEmbeddingForSync,
|
|
2919
|
+
getNodeBatchForSync: () => getNodeBatchForSync,
|
|
2920
|
+
getNodeForSync: () => getNodeForSync,
|
|
2921
|
+
getPendingRetries: () => getPendingRetries,
|
|
2922
|
+
incrementAttempts: () => incrementAttempts,
|
|
2923
|
+
logSyncEvent: () => logSyncEvent,
|
|
2924
|
+
queueForRetry: () => queueForRetry,
|
|
2925
|
+
resolveEdgeRetryTarget: () => resolveEdgeRetryTarget,
|
|
2926
|
+
updateQueueStatus: () => updateQueueStatus
|
|
2927
|
+
});
|
|
2928
|
+
function logRetryTargetFallback(context, error) {
|
|
2929
|
+
const env = globalThis.process?.env;
|
|
2930
|
+
if (env?.LUCERN_GRAPH_SYNC_DEBUG !== "1") {
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
console.debug("[graph-sync][neo4jSyncHelpers]", context, error);
|
|
2934
|
+
}
|
|
2935
|
+
var logSyncEvent = internalMutation({
|
|
2936
|
+
args: {
|
|
2937
|
+
eventType: v.union(
|
|
2938
|
+
v.literal("node_created"),
|
|
2939
|
+
v.literal("node_updated"),
|
|
2940
|
+
v.literal("node_deleted"),
|
|
2941
|
+
v.literal("edge_created"),
|
|
2942
|
+
v.literal("edge_deleted")
|
|
2943
|
+
),
|
|
2944
|
+
entityId: v.string(),
|
|
2945
|
+
entityType: v.string(),
|
|
2946
|
+
status: v.union(
|
|
2947
|
+
v.literal("pending"),
|
|
2948
|
+
v.literal("success"),
|
|
2949
|
+
v.literal("failed")
|
|
2950
|
+
),
|
|
2951
|
+
error: v.optional(v.string())
|
|
2952
|
+
},
|
|
2953
|
+
returns: permissiveReturn,
|
|
2954
|
+
handler: async (_ctx, args) => {
|
|
2955
|
+
console.log(
|
|
2956
|
+
`[Neo4j Sync] ${args.eventType} ${args.entityType}:${args.entityId} - ${args.status}`,
|
|
2957
|
+
args.error || ""
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
});
|
|
2961
|
+
var getNodeForSync = internalQuery({
|
|
2962
|
+
args: { nodeId: v.id("epistemicNodes") },
|
|
2963
|
+
returns: permissiveReturn,
|
|
2964
|
+
handler: async (ctx, args) => {
|
|
2965
|
+
return await ctx.db.get(args.nodeId);
|
|
2966
|
+
}
|
|
2967
|
+
});
|
|
2968
|
+
var getEmbeddingForSync = internalQuery({
|
|
2969
|
+
args: { nodeId: v.id("epistemicNodes") },
|
|
2970
|
+
returns: permissiveReturn,
|
|
2971
|
+
handler: async (ctx, args) => {
|
|
2972
|
+
const record = await ctx.db.query("epistemicNodeEmbeddings").withIndex("by_nodeId", (q) => q.eq("nodeId", args.nodeId)).first();
|
|
2973
|
+
return record?.embedding ?? null;
|
|
2974
|
+
}
|
|
2975
|
+
});
|
|
2976
|
+
var getAllNodesForResync = internalQuery({
|
|
2977
|
+
args: {
|
|
2978
|
+
nodeType: v.optional(v.string()),
|
|
2979
|
+
limit: v.optional(v.number()),
|
|
2980
|
+
cursor: v.optional(v.string())
|
|
2981
|
+
},
|
|
2982
|
+
returns: permissiveReturn,
|
|
2983
|
+
handler: async (ctx, args) => {
|
|
2984
|
+
const limit = args.limit ?? 100;
|
|
2985
|
+
const paginationOpts = {
|
|
2986
|
+
numItems: limit,
|
|
2987
|
+
cursor: args.cursor ?? null
|
|
2988
|
+
};
|
|
2989
|
+
if (args.nodeType) {
|
|
2990
|
+
const result2 = await ctx.db.query("epistemicNodes").withIndex(
|
|
2991
|
+
"by_nodeType",
|
|
2992
|
+
(q) => q.eq("nodeType", args.nodeType)
|
|
2993
|
+
).paginate(paginationOpts);
|
|
2994
|
+
return {
|
|
2995
|
+
nodes: result2.page,
|
|
2996
|
+
hasMore: !result2.isDone,
|
|
2997
|
+
nextCursor: result2.continueCursor
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
const result = await ctx.db.query("epistemicNodes").order("asc").paginate(paginationOpts);
|
|
3001
|
+
return {
|
|
3002
|
+
nodes: result.page,
|
|
3003
|
+
hasMore: !result.isDone,
|
|
3004
|
+
nextCursor: result.continueCursor
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
var getEdgeForSync = internalQuery({
|
|
3009
|
+
args: { edgeId: v.id("epistemicEdges") },
|
|
3010
|
+
returns: permissiveReturn,
|
|
3011
|
+
handler: async (ctx, args) => {
|
|
3012
|
+
const edge = await ctx.db.get(args.edgeId);
|
|
3013
|
+
if (!edge) {
|
|
3014
|
+
return null;
|
|
3015
|
+
}
|
|
3016
|
+
const fromNode = edge.fromNodeId ? await ctx.db.get(edge.fromNodeId) : null;
|
|
3017
|
+
const toNode = edge.toNodeId ? await ctx.db.get(edge.toNodeId) : null;
|
|
3018
|
+
return {
|
|
3019
|
+
...edge,
|
|
3020
|
+
// Cross-graph edges may not have toNodeId/fromNodeId in Convex mirror.
|
|
3021
|
+
// Fall back to denormalized global IDs when node lookup is unavailable.
|
|
3022
|
+
fromGlobalId: fromNode?.globalId || edge.sourceGlobalId,
|
|
3023
|
+
toGlobalId: toNode?.globalId || edge.targetGlobalId
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
});
|
|
3027
|
+
var getAllEdgesForResync = internalQuery({
|
|
3028
|
+
args: {
|
|
3029
|
+
limit: v.optional(v.number()),
|
|
3030
|
+
cursor: v.optional(v.string())
|
|
3031
|
+
},
|
|
3032
|
+
returns: permissiveReturn,
|
|
3033
|
+
handler: async (ctx, args) => {
|
|
3034
|
+
const limit = args.limit ?? 100;
|
|
3035
|
+
const paginationOpts = {
|
|
3036
|
+
numItems: limit,
|
|
3037
|
+
cursor: args.cursor ?? null
|
|
3038
|
+
};
|
|
3039
|
+
const result = await ctx.db.query("epistemicEdges").order("asc").paginate(paginationOpts);
|
|
3040
|
+
return {
|
|
3041
|
+
edges: result.page,
|
|
3042
|
+
hasMore: !result.isDone,
|
|
3043
|
+
nextCursor: result.continueCursor
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
});
|
|
3047
|
+
var getNodeBatchForSync = internalQuery({
|
|
3048
|
+
args: {
|
|
3049
|
+
limit: v.number(),
|
|
3050
|
+
cursor: v.optional(v.string())
|
|
3051
|
+
},
|
|
3052
|
+
returns: permissiveReturn,
|
|
3053
|
+
handler: async (ctx, args) => {
|
|
3054
|
+
const paginationOpts = {
|
|
3055
|
+
numItems: args.limit,
|
|
3056
|
+
cursor: args.cursor ?? null
|
|
3057
|
+
};
|
|
3058
|
+
const result = await ctx.db.query("epistemicNodes").order("asc").paginate(paginationOpts);
|
|
3059
|
+
return {
|
|
3060
|
+
nodes: result.page,
|
|
3061
|
+
hasMore: !result.isDone,
|
|
3062
|
+
nextCursor: result.continueCursor
|
|
3063
|
+
};
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
var getEdgeBatchForSync = internalQuery({
|
|
3067
|
+
args: {
|
|
3068
|
+
limit: v.number(),
|
|
3069
|
+
cursor: v.optional(v.string())
|
|
3070
|
+
},
|
|
3071
|
+
returns: permissiveReturn,
|
|
3072
|
+
handler: async (ctx, args) => {
|
|
3073
|
+
const paginationOpts = {
|
|
3074
|
+
numItems: args.limit,
|
|
3075
|
+
cursor: args.cursor ?? null
|
|
3076
|
+
};
|
|
3077
|
+
const result = await ctx.db.query("epistemicEdges").order("asc").paginate(paginationOpts);
|
|
3078
|
+
const enrichedEdges = await Promise.all(
|
|
3079
|
+
result.page.map(async (edge) => {
|
|
3080
|
+
const fromNode = await ctx.db.get(edge.fromNodeId);
|
|
3081
|
+
const toNode = edge.toNodeId ? await ctx.db.get(edge.toNodeId) : null;
|
|
3082
|
+
return {
|
|
3083
|
+
...edge,
|
|
3084
|
+
fromGlobalId: fromNode?.globalId || edge.sourceGlobalId,
|
|
3085
|
+
toGlobalId: toNode?.globalId || edge.targetGlobalId
|
|
3086
|
+
};
|
|
3087
|
+
})
|
|
3088
|
+
);
|
|
3089
|
+
return {
|
|
3090
|
+
edges: enrichedEdges,
|
|
3091
|
+
hasMore: !result.isDone,
|
|
3092
|
+
nextCursor: result.continueCursor
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
});
|
|
3096
|
+
var resolveEdgeRetryTarget = internalQuery({
|
|
3097
|
+
args: {
|
|
3098
|
+
entityId: v.string()
|
|
3099
|
+
},
|
|
3100
|
+
returns: permissiveReturn,
|
|
3101
|
+
handler: async (ctx, args) => {
|
|
3102
|
+
try {
|
|
3103
|
+
const byId = await ctx.db.get(args.entityId);
|
|
3104
|
+
if (byId && "edgeType" in byId && "fromNodeId" in byId) {
|
|
3105
|
+
return {
|
|
3106
|
+
mode: "convex_id",
|
|
3107
|
+
edgeId: byId._id,
|
|
3108
|
+
edgeGlobalId: byId.globalId
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
} catch (error) {
|
|
3112
|
+
logRetryTargetFallback(
|
|
3113
|
+
`direct edge lookup fallback for entityId=${args.entityId}`,
|
|
3114
|
+
error
|
|
3115
|
+
);
|
|
3116
|
+
}
|
|
3117
|
+
const byGlobalId = await ctx.db.query("epistemicEdges").withIndex("by_globalId", (q) => q.eq("globalId", args.entityId)).first();
|
|
3118
|
+
if (byGlobalId) {
|
|
3119
|
+
return {
|
|
3120
|
+
mode: "global_id_in_convex",
|
|
3121
|
+
edgeId: byGlobalId._id,
|
|
3122
|
+
edgeGlobalId: byGlobalId.globalId
|
|
3123
|
+
};
|
|
3124
|
+
}
|
|
3125
|
+
return {
|
|
3126
|
+
mode: "global_id_only",
|
|
3127
|
+
edgeGlobalId: args.entityId
|
|
3128
|
+
};
|
|
3129
|
+
}
|
|
3130
|
+
});
|
|
3131
|
+
var queueForRetry = internalMutation({
|
|
3132
|
+
args: {
|
|
3133
|
+
entityType: v.union(v.literal("node"), v.literal("edge")),
|
|
3134
|
+
entityId: v.string(),
|
|
3135
|
+
operation: v.union(v.literal("upsert"), v.literal("delete")),
|
|
3136
|
+
error: v.string()
|
|
3137
|
+
},
|
|
3138
|
+
returns: permissiveReturn,
|
|
3139
|
+
handler: async (ctx, args) => {
|
|
3140
|
+
const now = Date.now();
|
|
3141
|
+
const existing = await ctx.db.query("neo4jSyncQueue").withIndex(
|
|
3142
|
+
"by_entity",
|
|
3143
|
+
(q) => q.eq("entityType", args.entityType).eq("entityId", args.entityId)
|
|
3144
|
+
).first();
|
|
3145
|
+
if (existing) {
|
|
3146
|
+
const attempts = existing.attempts ?? 0;
|
|
3147
|
+
const maxAttempts = existing.maxAttempts ?? 5;
|
|
3148
|
+
await ctx.db.patch(existing._id, {
|
|
3149
|
+
attempts: attempts + 1,
|
|
3150
|
+
maxAttempts,
|
|
3151
|
+
lastAttemptAt: now,
|
|
3152
|
+
lastError: args.error,
|
|
3153
|
+
status: attempts + 1 >= maxAttempts ? "failed" : "pending",
|
|
3154
|
+
updatedAt: now
|
|
3155
|
+
});
|
|
3156
|
+
return { queued: true, updated: true, queueId: existing._id };
|
|
3157
|
+
}
|
|
3158
|
+
const queueId = await ctx.db.insert("neo4jSyncQueue", {
|
|
3159
|
+
entityType: args.entityType,
|
|
3160
|
+
entityId: args.entityId,
|
|
3161
|
+
operation: args.operation,
|
|
3162
|
+
attempts: 1,
|
|
3163
|
+
maxAttempts: 5,
|
|
3164
|
+
lastAttemptAt: now,
|
|
3165
|
+
lastError: args.error,
|
|
3166
|
+
status: "pending",
|
|
3167
|
+
createdAt: now,
|
|
3168
|
+
updatedAt: now
|
|
3169
|
+
});
|
|
3170
|
+
return { queued: true, updated: false, queueId };
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
var getPendingRetries = internalQuery({
|
|
3174
|
+
args: {
|
|
3175
|
+
limit: v.number()
|
|
3176
|
+
},
|
|
3177
|
+
returns: permissiveReturn,
|
|
3178
|
+
handler: async (ctx, args) => {
|
|
3179
|
+
return await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "pending")).take(args.limit);
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
3182
|
+
var updateQueueStatus = internalMutation({
|
|
3183
|
+
args: {
|
|
3184
|
+
queueId: v.id("neo4jSyncQueue"),
|
|
3185
|
+
status: v.union(
|
|
3186
|
+
v.literal("pending"),
|
|
3187
|
+
v.literal("in_progress"),
|
|
3188
|
+
v.literal("failed"),
|
|
3189
|
+
v.literal("succeeded")
|
|
3190
|
+
)
|
|
3191
|
+
},
|
|
3192
|
+
returns: permissiveReturn,
|
|
3193
|
+
handler: async (ctx, args) => {
|
|
3194
|
+
await ctx.db.patch(args.queueId, {
|
|
3195
|
+
status: args.status,
|
|
3196
|
+
updatedAt: Date.now()
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
});
|
|
3200
|
+
var incrementAttempts = internalMutation({
|
|
3201
|
+
args: {
|
|
3202
|
+
queueId: v.id("neo4jSyncQueue"),
|
|
3203
|
+
error: v.string()
|
|
3204
|
+
},
|
|
3205
|
+
returns: permissiveReturn,
|
|
3206
|
+
handler: async (ctx, args) => {
|
|
3207
|
+
const item = await ctx.db.get(args.queueId);
|
|
3208
|
+
if (!item) {
|
|
3209
|
+
return { failed: false };
|
|
3210
|
+
}
|
|
3211
|
+
const attempts = item.attempts ?? 0;
|
|
3212
|
+
const maxAttempts = item.maxAttempts ?? 5;
|
|
3213
|
+
const newAttempts = attempts + 1;
|
|
3214
|
+
const isFailed = newAttempts >= maxAttempts;
|
|
3215
|
+
await ctx.db.patch(args.queueId, {
|
|
3216
|
+
attempts: newAttempts,
|
|
3217
|
+
maxAttempts,
|
|
3218
|
+
lastAttemptAt: Date.now(),
|
|
3219
|
+
lastError: args.error,
|
|
3220
|
+
status: isFailed ? "failed" : "pending",
|
|
3221
|
+
updatedAt: Date.now()
|
|
3222
|
+
});
|
|
3223
|
+
return { failed: isFailed };
|
|
3224
|
+
}
|
|
3225
|
+
});
|
|
3226
|
+
var checkSyncHealth = internalQuery({
|
|
3227
|
+
args: {},
|
|
3228
|
+
returns: permissiveReturn,
|
|
3229
|
+
handler: async (ctx) => {
|
|
3230
|
+
const now = Date.now();
|
|
3231
|
+
const oneHourAgo = now - 60 * 60 * 1e3;
|
|
3232
|
+
const recentNodes = await ctx.db.query("epistemicNodes").filter((q) => q.gte(q.field("updatedAt"), oneHourAgo)).collect();
|
|
3233
|
+
const recentEdges = await ctx.db.query("epistemicEdges").filter((q) => q.gte(q.field("createdAt"), oneHourAgo)).collect();
|
|
3234
|
+
const pendingRetries = await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "pending")).collect();
|
|
3235
|
+
const failedRetries = await ctx.db.query("neo4jSyncQueue").withIndex("by_status", (q) => q.eq("status", "failed")).collect();
|
|
3236
|
+
return {
|
|
3237
|
+
recentNodesUpdated: recentNodes.length,
|
|
3238
|
+
recentEdgesUpdated: recentEdges.length,
|
|
3239
|
+
pendingRetries: pendingRetries.length,
|
|
3240
|
+
failedRetries: failedRetries.length,
|
|
3241
|
+
healthStatus: failedRetries.length > 10 ? "unhealthy" : pendingRetries.length > 50 ? "degraded" : "healthy",
|
|
3242
|
+
checkedAt: now
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
});
|
|
3246
|
+
|
|
3247
|
+
export { neo4jDriver_exports as neo4jDriver, neo4jEdgeAPI_exports as neo4jEdgeAPI, neo4jQueries_exports as neo4jQueries, neo4jQueryRoute_exports as neo4jQueryRoute, neo4jSync_exports as neo4jSync, neo4jSyncHelpers_exports as neo4jSyncHelpers };
|
|
3248
|
+
//# sourceMappingURL=index.js.map
|
|
3249
|
+
//# sourceMappingURL=index.js.map
|