@zergai/ztc 0.2.1-dev.35568243511 → 0.2.2-dev.35681806994
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/zlm-embed.mjs +1 -1
- package/package.json +2 -3
- package/node_modules/@ztc/sim-graph/dist/index.d.ts +0 -1
- package/node_modules/@ztc/sim-graph/dist/index.js +0 -1
- package/node_modules/@ztc/sim-graph/dist/sim_graph.d.ts +0 -351
- package/node_modules/@ztc/sim-graph/dist/sim_graph.js +0 -795
- package/node_modules/@ztc/sim-graph/package.json +0 -34
- package/node_modules/@ztc/sim-graph/src/index.ts +0 -1
- package/node_modules/@ztc/sim-graph/src/sim_graph.ts +0 -1305
|
@@ -1,1305 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SimGraph — FalkorDB Graph Backbone for Simulation
|
|
3
|
-
*
|
|
4
|
-
* Property graph storing entities, temporal facts, relationships,
|
|
5
|
-
* feed posts, and events. Replaces flat in-memory stores with
|
|
6
|
-
* structured, queryable graph data.
|
|
7
|
-
*
|
|
8
|
-
* Inspired by MiroFish/Graphiti's temporal knowledge graph but
|
|
9
|
-
* implemented natively in TypeScript with FalkorDBLite (embedded,
|
|
10
|
-
* no Docker, no Python).
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
// falkordblite (embedded graph DB + native binding) is imported lazily in
|
|
14
|
-
// SimGraph.open() so that merely importing this module stays cheap.
|
|
15
|
-
import type { FalkorDB, Graph } from 'falkordblite';
|
|
16
|
-
|
|
17
|
-
// ── Types ─────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Scalar + nested shape accepted by FalkorDB parameter binding. Mirrors
|
|
21
|
-
* the internal `QueryParam` type from the `falkordb` package (which is
|
|
22
|
-
* not exported) so consumers of `rawQuery` get the same strictness.
|
|
23
|
-
*/
|
|
24
|
-
export type QueryParamValue =
|
|
25
|
-
| null
|
|
26
|
-
| string
|
|
27
|
-
| number
|
|
28
|
-
| boolean
|
|
29
|
-
| QueryParams
|
|
30
|
-
| QueryParamValue[];
|
|
31
|
-
export interface QueryParams {
|
|
32
|
-
[key: string]: QueryParamValue;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface GraphFact {
|
|
36
|
-
id: string;
|
|
37
|
-
content: string;
|
|
38
|
-
category: string;
|
|
39
|
-
sourceTick: number;
|
|
40
|
-
validFrom: number;
|
|
41
|
-
invalidAt: number | null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface GraphRelationship {
|
|
45
|
-
agentIdA: string;
|
|
46
|
-
agentIdB: string;
|
|
47
|
-
nameA: string;
|
|
48
|
-
nameB: string;
|
|
49
|
-
sentiment: number;
|
|
50
|
-
trust: number;
|
|
51
|
-
interactionCount: number;
|
|
52
|
-
lastNote: string | null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface GraphFeedPost {
|
|
56
|
-
id: string;
|
|
57
|
-
content: string;
|
|
58
|
-
authorId: string;
|
|
59
|
-
authorName: string;
|
|
60
|
-
authorRole: string;
|
|
61
|
-
source: string;
|
|
62
|
-
createdAt: number;
|
|
63
|
-
replyTo: string | null;
|
|
64
|
-
reactionCount: number;
|
|
65
|
-
topicTags: string[];
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface GraphContext {
|
|
69
|
-
facts: GraphFact[];
|
|
70
|
-
agents: Array<{ id: string; name: string; role: string }>;
|
|
71
|
-
relationships: GraphRelationship[];
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface WorldView {
|
|
75
|
-
ownFacts: GraphFact[];
|
|
76
|
-
peerFacts: Array<{ peerName: string; peerRole: string; facts: GraphFact[] }>;
|
|
77
|
-
topicFacts: GraphFact[];
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export interface SimulationGraphFact {
|
|
81
|
-
id: string;
|
|
82
|
-
subjectId: string;
|
|
83
|
-
predicate: string;
|
|
84
|
-
objectId: string;
|
|
85
|
-
content: string;
|
|
86
|
-
validFrom: number;
|
|
87
|
-
invalidAt: number | null;
|
|
88
|
-
sourceEventId: string;
|
|
89
|
-
data: Record<string, unknown>;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface SimulationGraphFactInput {
|
|
93
|
-
id: string;
|
|
94
|
-
subjectId: string;
|
|
95
|
-
predicate: string;
|
|
96
|
-
objectId?: string;
|
|
97
|
-
content: string;
|
|
98
|
-
validFrom: number;
|
|
99
|
-
invalidAt?: number | null;
|
|
100
|
-
sourceEventId?: string;
|
|
101
|
-
supersedesIds?: string[];
|
|
102
|
-
data?: Record<string, unknown>;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface SimulationGraphEdge {
|
|
106
|
-
sourceId: string;
|
|
107
|
-
targetId: string;
|
|
108
|
-
type: string;
|
|
109
|
-
tick: number;
|
|
110
|
-
data: Record<string, unknown>;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
export interface SimulationFactGraph {
|
|
114
|
-
schemaVersion: 1;
|
|
115
|
-
facts: SimulationGraphFact[];
|
|
116
|
-
edges: SimulationGraphEdge[];
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface RecordSimulationFactInput {
|
|
120
|
-
graph?: SimulationFactGraph;
|
|
121
|
-
fact: SimulationGraphFactInput;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export interface ProjectActiveFactsInput {
|
|
125
|
-
graph: SimulationFactGraph;
|
|
126
|
-
atTick: number;
|
|
127
|
-
subjectIds?: string[];
|
|
128
|
-
predicates?: string[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export interface SimulationNeighborhoodInput {
|
|
132
|
-
graph: SimulationFactGraph;
|
|
133
|
-
startIds: string[];
|
|
134
|
-
maxDepth?: number;
|
|
135
|
-
edgeTypes?: string[];
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export interface SimulationNeighborhood {
|
|
139
|
-
schemaVersion: 1;
|
|
140
|
-
factIds: string[];
|
|
141
|
-
facts: SimulationGraphFact[];
|
|
142
|
-
edges: SimulationGraphEdge[];
|
|
143
|
-
depthByFactId: Record<string, number>;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export type SimulationFocusGraphDirection = 'in' | 'out' | 'both';
|
|
147
|
-
|
|
148
|
-
export interface SimulationFocusGraphNode {
|
|
149
|
-
id: string;
|
|
150
|
-
kind?: string;
|
|
151
|
-
label?: string;
|
|
152
|
-
data?: Record<string, unknown>;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface SimulationFocusGraphEdge {
|
|
156
|
-
sourceId: string;
|
|
157
|
-
targetId: string;
|
|
158
|
-
type: string;
|
|
159
|
-
createdAt?: string;
|
|
160
|
-
data: Record<string, unknown>;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface SimulationFocusRelationSpec {
|
|
164
|
-
name: string;
|
|
165
|
-
edgeTypes: string[];
|
|
166
|
-
direction?: SimulationFocusGraphDirection;
|
|
167
|
-
limit?: number;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface SimulationFocusLineageSpec {
|
|
171
|
-
edgeTypes: string[];
|
|
172
|
-
direction?: Exclude<SimulationFocusGraphDirection, 'both'>;
|
|
173
|
-
includeFocus?: boolean;
|
|
174
|
-
maxDepth?: number;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface BuildSimulationFocusGraphContextInput {
|
|
178
|
-
nodes?: SimulationFocusGraphNode[];
|
|
179
|
-
edges?: SimulationFocusGraphEdge[];
|
|
180
|
-
focusIds?: string[];
|
|
181
|
-
relations?: SimulationFocusRelationSpec[];
|
|
182
|
-
lineage?: SimulationFocusLineageSpec;
|
|
183
|
-
maxFocusIds?: number;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export interface SimulationFocusLineage {
|
|
187
|
-
focusId: string;
|
|
188
|
-
nodeIds: string[];
|
|
189
|
-
edgeTypes: string[];
|
|
190
|
-
edges: SimulationFocusGraphEdge[];
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface SimulationFocusRelationGroup {
|
|
194
|
-
name: string;
|
|
195
|
-
focusId: string;
|
|
196
|
-
nodeIds: string[];
|
|
197
|
-
edges: SimulationFocusGraphEdge[];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export interface SimulationFocusGraphContext {
|
|
201
|
-
version: 1;
|
|
202
|
-
focusIds: string[];
|
|
203
|
-
lineages: SimulationFocusLineage[];
|
|
204
|
-
relationGroups: SimulationFocusRelationGroup[];
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
export interface SimulationSteeringEvent {
|
|
208
|
-
id: string;
|
|
209
|
-
eventType?: string;
|
|
210
|
-
acceptedSignalKinds?: string[];
|
|
211
|
-
subjectId?: string;
|
|
212
|
-
verdict?: string;
|
|
213
|
-
selectionStatus?: string;
|
|
214
|
-
createdAt?: string;
|
|
215
|
-
data?: Record<string, unknown>;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export interface SimulationSteeringSignal {
|
|
219
|
-
kind?: string;
|
|
220
|
-
subjectId?: string;
|
|
221
|
-
verdict?: string;
|
|
222
|
-
selectionStatus?: string;
|
|
223
|
-
priority?: number;
|
|
224
|
-
data?: Record<string, unknown>;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export interface SimulationSteeringTask {
|
|
228
|
-
id: string;
|
|
229
|
-
taskType?: string;
|
|
230
|
-
source?: string;
|
|
231
|
-
createdAt?: string;
|
|
232
|
-
signals?: SimulationSteeringSignal[];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
export interface SimulationSteeringOutput {
|
|
236
|
-
id: string;
|
|
237
|
-
taskId: string;
|
|
238
|
-
outputType?: string;
|
|
239
|
-
createdAt?: string;
|
|
240
|
-
data?: Record<string, unknown>;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export interface SimulationSteeringInfluenceEdge {
|
|
244
|
-
sourceId: string;
|
|
245
|
-
targetId: string;
|
|
246
|
-
type: 'steering_influenced_task' | 'steering_influenced_output';
|
|
247
|
-
createdAt: string;
|
|
248
|
-
data: Record<string, unknown>;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
export interface BuildSimulationSteeringInfluenceGraphInput {
|
|
252
|
-
steeringEvents?: SimulationSteeringEvent[];
|
|
253
|
-
tasks?: SimulationSteeringTask[];
|
|
254
|
-
outputs?: SimulationSteeringOutput[];
|
|
255
|
-
ignoreVerdictKinds?: string[];
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
export interface SimulationSteeringInfluenceGraph {
|
|
259
|
-
version: 1;
|
|
260
|
-
taskEdges: SimulationSteeringInfluenceEdge[];
|
|
261
|
-
outputEdges: SimulationSteeringInfluenceEdge[];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
export interface SimulationRestartMemoryGraphStats {
|
|
265
|
-
nodes: number;
|
|
266
|
-
edges: number;
|
|
267
|
-
activeFactCount: number;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
export interface SimulationRestartMemoryPacket<TFact = Record<string, unknown>> {
|
|
271
|
-
version: 1;
|
|
272
|
-
runId: string;
|
|
273
|
-
focusIds: string[];
|
|
274
|
-
activeFacts: TFact[];
|
|
275
|
-
graphStats: SimulationRestartMemoryGraphStats;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export interface BuildSimulationRestartMemoryInput<TFact = Record<string, unknown>> {
|
|
279
|
-
runId?: string;
|
|
280
|
-
focusIds?: string[];
|
|
281
|
-
activeFacts?: TFact[];
|
|
282
|
-
graphStats?: Partial<SimulationRestartMemoryGraphStats>;
|
|
283
|
-
maxFocusIds?: number;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function recordSimulationFact(input: RecordSimulationFactInput): SimulationFactGraph {
|
|
287
|
-
const graph = normalizeSimulationFactGraph(input.graph);
|
|
288
|
-
const fact = normalizeSimulationGraphFact(input.fact);
|
|
289
|
-
const existingFacts = graph.facts.filter((item) => item.id !== fact.id);
|
|
290
|
-
const supersedesEdges = uniqueText(input.fact.supersedesIds ?? [])
|
|
291
|
-
.filter((targetId) => targetId !== fact.id)
|
|
292
|
-
.map((targetId) => ({
|
|
293
|
-
sourceId: fact.id,
|
|
294
|
-
targetId,
|
|
295
|
-
type: 'supersedes',
|
|
296
|
-
tick: fact.validFrom,
|
|
297
|
-
data: {
|
|
298
|
-
sourceEventId: fact.sourceEventId,
|
|
299
|
-
},
|
|
300
|
-
}));
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
schemaVersion: 1,
|
|
304
|
-
facts: [...existingFacts, fact],
|
|
305
|
-
edges: dedupeSimulationEdges([...graph.edges, ...supersedesEdges]),
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
export function projectActiveFacts(input: ProjectActiveFactsInput): SimulationGraphFact[] {
|
|
310
|
-
const graph = normalizeSimulationFactGraph(input.graph);
|
|
311
|
-
const atTick = finiteNumber(input.atTick, Number.POSITIVE_INFINITY);
|
|
312
|
-
const subjectIds = new Set(uniqueText(input.subjectIds ?? []));
|
|
313
|
-
const predicates = new Set(uniqueText(input.predicates ?? []));
|
|
314
|
-
const supersededIds = new Set(
|
|
315
|
-
graph.edges
|
|
316
|
-
.filter((edge) => edge.type === 'supersedes' && edge.tick <= atTick)
|
|
317
|
-
.map((edge) => edge.targetId),
|
|
318
|
-
);
|
|
319
|
-
|
|
320
|
-
return graph.facts
|
|
321
|
-
.filter((fact) => fact.validFrom <= atTick)
|
|
322
|
-
.filter((fact) => fact.invalidAt === null || fact.invalidAt > atTick)
|
|
323
|
-
.filter((fact) => !supersededIds.has(fact.id))
|
|
324
|
-
.filter((fact) => subjectIds.size === 0 || subjectIds.has(fact.subjectId))
|
|
325
|
-
.filter((fact) => predicates.size === 0 || predicates.has(fact.predicate))
|
|
326
|
-
.sort((a, b) => {
|
|
327
|
-
if (a.validFrom !== b.validFrom) return a.validFrom - b.validFrom;
|
|
328
|
-
return a.id.localeCompare(b.id);
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export function traverseSimulationNeighborhood(
|
|
333
|
-
input: SimulationNeighborhoodInput,
|
|
334
|
-
): SimulationNeighborhood {
|
|
335
|
-
const graph = normalizeSimulationFactGraph(input.graph);
|
|
336
|
-
const factById = new Map(graph.facts.map((fact) => [fact.id, fact]));
|
|
337
|
-
const maxDepth = Math.max(0, Math.floor(finiteNumber(input.maxDepth, 1)));
|
|
338
|
-
const allowedEdgeTypes = new Set(uniqueText(input.edgeTypes ?? []));
|
|
339
|
-
const eligibleEdges = graph.edges.filter((edge) => (
|
|
340
|
-
allowedEdgeTypes.size === 0 || allowedEdgeTypes.has(edge.type)
|
|
341
|
-
));
|
|
342
|
-
const depthByFactId: Record<string, number> = {};
|
|
343
|
-
const factIds: string[] = [];
|
|
344
|
-
const selectedEdges: SimulationGraphEdge[] = [];
|
|
345
|
-
const queue: Array<{ id: string; depth: number }> = [];
|
|
346
|
-
|
|
347
|
-
for (const id of uniqueText(input.startIds)) {
|
|
348
|
-
if (!factById.has(id) || depthByFactId[id] !== undefined) continue;
|
|
349
|
-
depthByFactId[id] = 0;
|
|
350
|
-
factIds.push(id);
|
|
351
|
-
queue.push({ id, depth: 0 });
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
while (queue.length > 0) {
|
|
355
|
-
const current = queue.shift()!;
|
|
356
|
-
if (current.depth >= maxDepth) continue;
|
|
357
|
-
for (const edge of eligibleEdges) {
|
|
358
|
-
const neighborId = edge.sourceId === current.id
|
|
359
|
-
? edge.targetId
|
|
360
|
-
: edge.targetId === current.id
|
|
361
|
-
? edge.sourceId
|
|
362
|
-
: '';
|
|
363
|
-
if (!neighborId || !factById.has(neighborId)) continue;
|
|
364
|
-
addUniqueSimulationEdge(selectedEdges, edge);
|
|
365
|
-
if (depthByFactId[neighborId] !== undefined) continue;
|
|
366
|
-
depthByFactId[neighborId] = current.depth + 1;
|
|
367
|
-
factIds.push(neighborId);
|
|
368
|
-
queue.push({ id: neighborId, depth: current.depth + 1 });
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return {
|
|
373
|
-
schemaVersion: 1,
|
|
374
|
-
factIds,
|
|
375
|
-
facts: factIds.map((id) => factById.get(id)).filter((fact): fact is SimulationGraphFact => fact !== undefined),
|
|
376
|
-
edges: selectedEdges.filter((edge) => (
|
|
377
|
-
depthByFactId[edge.sourceId] !== undefined &&
|
|
378
|
-
depthByFactId[edge.targetId] !== undefined
|
|
379
|
-
)),
|
|
380
|
-
depthByFactId,
|
|
381
|
-
};
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
export function buildSimulationFocusGraphContext(
|
|
385
|
-
input: BuildSimulationFocusGraphContextInput,
|
|
386
|
-
): SimulationFocusGraphContext {
|
|
387
|
-
const focusIds = uniqueText(input.focusIds ?? [])
|
|
388
|
-
.slice(0, Math.max(0, Math.floor(finiteNumber(input.maxFocusIds, 20))));
|
|
389
|
-
const nodes = new Map(
|
|
390
|
-
(input.nodes ?? [])
|
|
391
|
-
.map((node) => normalizeFocusGraphNode(node))
|
|
392
|
-
.filter((node) => node.id)
|
|
393
|
-
.map((node) => [node.id, node]),
|
|
394
|
-
);
|
|
395
|
-
const edges = (input.edges ?? [])
|
|
396
|
-
.map((edge) => normalizeFocusGraphEdge(edge))
|
|
397
|
-
.filter((edge) => edge.sourceId && edge.targetId && edge.type);
|
|
398
|
-
|
|
399
|
-
return {
|
|
400
|
-
version: 1,
|
|
401
|
-
focusIds,
|
|
402
|
-
lineages: input.lineage
|
|
403
|
-
? focusIds.map((focusId) => buildSimulationFocusLineage(focusId, nodes, edges, input.lineage!))
|
|
404
|
-
: [],
|
|
405
|
-
relationGroups: (input.relations ?? []).flatMap((relation) => (
|
|
406
|
-
focusIds.map((focusId) => buildSimulationFocusRelationGroup(focusId, edges, relation))
|
|
407
|
-
)),
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
export function buildSimulationSteeringInfluenceGraph(
|
|
412
|
-
input: BuildSimulationSteeringInfluenceGraphInput,
|
|
413
|
-
): SimulationSteeringInfluenceGraph {
|
|
414
|
-
const events = (input.steeringEvents ?? [])
|
|
415
|
-
.map((event) => normalizeSteeringEvent(event))
|
|
416
|
-
.filter((event) => event.id);
|
|
417
|
-
const tasks = (input.tasks ?? [])
|
|
418
|
-
.map((task) => normalizeSteeringTask(task))
|
|
419
|
-
.filter((task) => task.id);
|
|
420
|
-
const outputs = (input.outputs ?? [])
|
|
421
|
-
.map((output) => normalizeSteeringOutput(output))
|
|
422
|
-
.filter((output) => output.id && output.taskId);
|
|
423
|
-
const ignoreVerdictKinds = new Set(uniqueText(input.ignoreVerdictKinds ?? ['hypothesis_submitted']));
|
|
424
|
-
const taskEdges: SimulationSteeringInfluenceEdge[] = [];
|
|
425
|
-
const outputEdges: SimulationSteeringInfluenceEdge[] = [];
|
|
426
|
-
|
|
427
|
-
for (const event of events) {
|
|
428
|
-
for (const task of tasks) {
|
|
429
|
-
for (const signal of task.signals ?? []) {
|
|
430
|
-
if (!steeringSignalMatchesEvent(event, task, signal, ignoreVerdictKinds)) continue;
|
|
431
|
-
const taskEdge = steeringTaskEdge(event, task, signal);
|
|
432
|
-
addUniqueSteeringInfluenceEdge(taskEdges, taskEdge);
|
|
433
|
-
for (const output of outputs.filter((candidate) => candidate.taskId === task.id)) {
|
|
434
|
-
addUniqueSteeringInfluenceEdge(outputEdges, steeringOutputEdge(event, task, signal, output));
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
return {
|
|
441
|
-
version: 1,
|
|
442
|
-
taskEdges,
|
|
443
|
-
outputEdges,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
export function buildSimulationRestartMemory<TFact = Record<string, unknown>>(
|
|
448
|
-
input: BuildSimulationRestartMemoryInput<TFact>,
|
|
449
|
-
): SimulationRestartMemoryPacket<TFact> {
|
|
450
|
-
const activeFacts = Array.isArray(input.activeFacts) ? [...input.activeFacts] : [];
|
|
451
|
-
const graphStats = input.graphStats ?? {};
|
|
452
|
-
const maxFocusIds = Math.max(0, Math.floor(finiteNumber(input.maxFocusIds, 20)));
|
|
453
|
-
|
|
454
|
-
return {
|
|
455
|
-
version: 1,
|
|
456
|
-
runId: textValue(input.runId),
|
|
457
|
-
focusIds: uniqueText(input.focusIds ?? []).slice(0, maxFocusIds),
|
|
458
|
-
activeFacts,
|
|
459
|
-
graphStats: {
|
|
460
|
-
nodes: wholeNumber(graphStats.nodes, 0),
|
|
461
|
-
edges: wholeNumber(graphStats.edges, 0),
|
|
462
|
-
activeFactCount: wholeNumber(graphStats.activeFactCount, activeFacts.length),
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function steeringTaskEdge(
|
|
468
|
-
event: SimulationSteeringEvent,
|
|
469
|
-
task: SimulationSteeringTask,
|
|
470
|
-
signal: SimulationSteeringSignal,
|
|
471
|
-
): SimulationSteeringInfluenceEdge {
|
|
472
|
-
return {
|
|
473
|
-
sourceId: event.id,
|
|
474
|
-
targetId: task.id,
|
|
475
|
-
type: 'steering_influenced_task',
|
|
476
|
-
createdAt: textValue(task.createdAt),
|
|
477
|
-
data: steeringInfluenceData(task, signal),
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function steeringOutputEdge(
|
|
482
|
-
event: SimulationSteeringEvent,
|
|
483
|
-
task: SimulationSteeringTask,
|
|
484
|
-
signal: SimulationSteeringSignal,
|
|
485
|
-
output: SimulationSteeringOutput,
|
|
486
|
-
): SimulationSteeringInfluenceEdge {
|
|
487
|
-
const { priority: _priority, ...influenceData } = steeringInfluenceData(task, signal);
|
|
488
|
-
return {
|
|
489
|
-
sourceId: event.id,
|
|
490
|
-
targetId: output.id,
|
|
491
|
-
type: 'steering_influenced_output',
|
|
492
|
-
createdAt: textValue(output.createdAt),
|
|
493
|
-
data: {
|
|
494
|
-
...influenceData,
|
|
495
|
-
...(textValue(output.outputType) ? { outputType: textValue(output.outputType) } : {}),
|
|
496
|
-
...(isRecord(output.data) ? { ...output.data } : {}),
|
|
497
|
-
},
|
|
498
|
-
};
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
function steeringInfluenceData(
|
|
502
|
-
task: SimulationSteeringTask,
|
|
503
|
-
signal: SimulationSteeringSignal,
|
|
504
|
-
): Record<string, unknown> {
|
|
505
|
-
return {
|
|
506
|
-
taskId: task.id,
|
|
507
|
-
taskType: textValue(task.taskType),
|
|
508
|
-
source: textValue(task.source),
|
|
509
|
-
steeringKind: textValue(signal.kind),
|
|
510
|
-
subjectId: textValue(signal.subjectId),
|
|
511
|
-
verdict: textValue(signal.verdict),
|
|
512
|
-
selectionStatus: textValue(signal.selectionStatus),
|
|
513
|
-
priority: finiteNumber(signal.priority, 0),
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function steeringSignalMatchesEvent(
|
|
518
|
-
event: SimulationSteeringEvent,
|
|
519
|
-
task: SimulationSteeringTask,
|
|
520
|
-
signal: SimulationSteeringSignal,
|
|
521
|
-
ignoreVerdictKinds: Set<string>,
|
|
522
|
-
): boolean {
|
|
523
|
-
const eventTime = Date.parse(textValue(event.createdAt));
|
|
524
|
-
const taskTime = Date.parse(textValue(task.createdAt));
|
|
525
|
-
if (Number.isFinite(eventTime) && Number.isFinite(taskTime) && taskTime < eventTime) return false;
|
|
526
|
-
|
|
527
|
-
const signalKind = textValue(signal.kind);
|
|
528
|
-
const acceptedKinds = new Set(uniqueText(event.acceptedSignalKinds ?? []));
|
|
529
|
-
if (acceptedKinds.size > 0 && !acceptedKinds.has(signalKind)) return false;
|
|
530
|
-
if (!matchingTextWhenBothPresent(event.subjectId, signal.subjectId)) return false;
|
|
531
|
-
if (!ignoreVerdictKinds.has(signalKind) && !matchingTextWhenBothPresent(event.verdict, signal.verdict)) return false;
|
|
532
|
-
if (!matchingTextWhenBothPresent(event.selectionStatus, signal.selectionStatus)) return false;
|
|
533
|
-
return true;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
function matchingTextWhenBothPresent(left: unknown, right: unknown): boolean {
|
|
537
|
-
const leftText = textValue(left);
|
|
538
|
-
const rightText = textValue(right);
|
|
539
|
-
return !leftText || !rightText || leftText === rightText;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function addUniqueSteeringInfluenceEdge(
|
|
543
|
-
edges: SimulationSteeringInfluenceEdge[],
|
|
544
|
-
edge: SimulationSteeringInfluenceEdge,
|
|
545
|
-
): void {
|
|
546
|
-
const key = `${edge.sourceId}\0${edge.targetId}\0${edge.type}`;
|
|
547
|
-
if (edges.some((candidate) => `${candidate.sourceId}\0${candidate.targetId}\0${candidate.type}` === key)) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
edges.push(edge);
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
function normalizeSteeringEvent(event: SimulationSteeringEvent): SimulationSteeringEvent {
|
|
554
|
-
return {
|
|
555
|
-
id: textValue(event.id),
|
|
556
|
-
eventType: textValue(event.eventType),
|
|
557
|
-
acceptedSignalKinds: uniqueText(event.acceptedSignalKinds ?? []),
|
|
558
|
-
subjectId: textValue(event.subjectId),
|
|
559
|
-
verdict: textValue(event.verdict),
|
|
560
|
-
selectionStatus: textValue(event.selectionStatus),
|
|
561
|
-
createdAt: textValue(event.createdAt),
|
|
562
|
-
data: isRecord(event.data) ? { ...event.data } : {},
|
|
563
|
-
};
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
function normalizeSteeringTask(task: SimulationSteeringTask): SimulationSteeringTask {
|
|
567
|
-
return {
|
|
568
|
-
id: textValue(task.id),
|
|
569
|
-
taskType: textValue(task.taskType),
|
|
570
|
-
source: textValue(task.source),
|
|
571
|
-
createdAt: textValue(task.createdAt),
|
|
572
|
-
signals: (task.signals ?? []).map((signal) => normalizeSteeringSignal(signal)),
|
|
573
|
-
};
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function normalizeSteeringSignal(signal: SimulationSteeringSignal): SimulationSteeringSignal {
|
|
577
|
-
return {
|
|
578
|
-
kind: textValue(signal.kind),
|
|
579
|
-
subjectId: textValue(signal.subjectId),
|
|
580
|
-
verdict: textValue(signal.verdict),
|
|
581
|
-
selectionStatus: textValue(signal.selectionStatus),
|
|
582
|
-
priority: finiteNumber(signal.priority, 0),
|
|
583
|
-
data: isRecord(signal.data) ? { ...signal.data } : {},
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function normalizeSteeringOutput(output: SimulationSteeringOutput): SimulationSteeringOutput {
|
|
588
|
-
return {
|
|
589
|
-
id: textValue(output.id),
|
|
590
|
-
taskId: textValue(output.taskId),
|
|
591
|
-
outputType: textValue(output.outputType),
|
|
592
|
-
createdAt: textValue(output.createdAt),
|
|
593
|
-
data: isRecord(output.data) ? { ...output.data } : {},
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
function buildSimulationFocusLineage(
|
|
598
|
-
focusId: string,
|
|
599
|
-
nodes: Map<string, SimulationFocusGraphNode>,
|
|
600
|
-
edges: SimulationFocusGraphEdge[],
|
|
601
|
-
lineage: SimulationFocusLineageSpec,
|
|
602
|
-
): SimulationFocusLineage {
|
|
603
|
-
const edgeTypes = new Set(uniqueText(lineage.edgeTypes));
|
|
604
|
-
const direction = lineage.direction ?? 'out';
|
|
605
|
-
const maxDepth = Math.max(0, Math.floor(finiteNumber(lineage.maxDepth, 12)));
|
|
606
|
-
const pathIds: string[] = [];
|
|
607
|
-
const pathEdges: SimulationFocusGraphEdge[] = [];
|
|
608
|
-
const visited = new Set<string>();
|
|
609
|
-
let currentId = focusId;
|
|
610
|
-
|
|
611
|
-
for (let depth = 0; currentId && !visited.has(currentId) && depth <= maxDepth; depth++) {
|
|
612
|
-
visited.add(currentId);
|
|
613
|
-
if (nodes.has(currentId)) pathIds.push(currentId);
|
|
614
|
-
const nextEdge = edges.find((edge) => (
|
|
615
|
-
edgeTypes.has(edge.type) &&
|
|
616
|
-
(direction === 'out' ? edge.sourceId === currentId : edge.targetId === currentId)
|
|
617
|
-
));
|
|
618
|
-
if (!nextEdge) break;
|
|
619
|
-
pathEdges.push(nextEdge);
|
|
620
|
-
currentId = direction === 'out' ? nextEdge.targetId : nextEdge.sourceId;
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
const nodeIds = lineage.includeFocus === false
|
|
624
|
-
? pathIds.slice(1).reverse()
|
|
625
|
-
: pathIds.reverse();
|
|
626
|
-
|
|
627
|
-
return {
|
|
628
|
-
focusId,
|
|
629
|
-
nodeIds,
|
|
630
|
-
edgeTypes: uniqueText(pathEdges.map((edge) => edge.type)),
|
|
631
|
-
edges: pathEdges,
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
function buildSimulationFocusRelationGroup(
|
|
636
|
-
focusId: string,
|
|
637
|
-
edges: SimulationFocusGraphEdge[],
|
|
638
|
-
relation: SimulationFocusRelationSpec,
|
|
639
|
-
): SimulationFocusRelationGroup {
|
|
640
|
-
const edgeTypes = new Set(uniqueText(relation.edgeTypes));
|
|
641
|
-
const direction = relation.direction ?? 'both';
|
|
642
|
-
const limit = Math.max(0, Math.floor(finiteNumber(relation.limit, Number.MAX_SAFE_INTEGER)));
|
|
643
|
-
const selectedEdges = edges
|
|
644
|
-
.filter((edge) => edgeTypes.has(edge.type))
|
|
645
|
-
.filter((edge) => focusRelationEdgeMatches(edge, focusId, direction))
|
|
646
|
-
.slice(0, limit);
|
|
647
|
-
|
|
648
|
-
return {
|
|
649
|
-
name: textValue(relation.name),
|
|
650
|
-
focusId,
|
|
651
|
-
nodeIds: uniqueText(selectedEdges.map((edge) => focusRelationOtherNodeId(edge, focusId, direction))),
|
|
652
|
-
edges: selectedEdges,
|
|
653
|
-
};
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
function focusRelationEdgeMatches(
|
|
657
|
-
edge: SimulationFocusGraphEdge,
|
|
658
|
-
focusId: string,
|
|
659
|
-
direction: SimulationFocusGraphDirection,
|
|
660
|
-
): boolean {
|
|
661
|
-
if (direction === 'out') return edge.sourceId === focusId;
|
|
662
|
-
if (direction === 'in') return edge.targetId === focusId;
|
|
663
|
-
return edge.sourceId === focusId || edge.targetId === focusId;
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
function focusRelationOtherNodeId(
|
|
667
|
-
edge: SimulationFocusGraphEdge,
|
|
668
|
-
focusId: string,
|
|
669
|
-
direction: SimulationFocusGraphDirection,
|
|
670
|
-
): string {
|
|
671
|
-
if (direction === 'out') return edge.targetId;
|
|
672
|
-
if (direction === 'in') return edge.sourceId;
|
|
673
|
-
return edge.sourceId === focusId ? edge.targetId : edge.sourceId;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function normalizeFocusGraphNode(node: SimulationFocusGraphNode): SimulationFocusGraphNode {
|
|
677
|
-
return {
|
|
678
|
-
id: textValue(node.id),
|
|
679
|
-
kind: textValue(node.kind),
|
|
680
|
-
label: textValue(node.label),
|
|
681
|
-
data: isRecord(node.data) ? { ...node.data } : {},
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function normalizeFocusGraphEdge(edge: SimulationFocusGraphEdge): SimulationFocusGraphEdge {
|
|
686
|
-
return {
|
|
687
|
-
sourceId: textValue(edge.sourceId),
|
|
688
|
-
targetId: textValue(edge.targetId),
|
|
689
|
-
type: textValue(edge.type),
|
|
690
|
-
createdAt: textValue(edge.createdAt),
|
|
691
|
-
data: isRecord(edge.data) ? { ...edge.data } : {},
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
function normalizeSimulationFactGraph(graph: SimulationFactGraph | undefined): SimulationFactGraph {
|
|
696
|
-
return {
|
|
697
|
-
schemaVersion: 1,
|
|
698
|
-
facts: (graph?.facts ?? []).map((fact) => normalizeSimulationGraphFact(fact)),
|
|
699
|
-
edges: dedupeSimulationEdges(graph?.edges ?? []),
|
|
700
|
-
};
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function normalizeSimulationGraphFact(fact: SimulationGraphFactInput | SimulationGraphFact): SimulationGraphFact {
|
|
704
|
-
const id = textValue(fact.id);
|
|
705
|
-
if (!id) {
|
|
706
|
-
throw new Error('simulation fact id is required');
|
|
707
|
-
}
|
|
708
|
-
const subjectId = textValue(fact.subjectId);
|
|
709
|
-
if (!subjectId) {
|
|
710
|
-
throw new Error('simulation fact subject id is required');
|
|
711
|
-
}
|
|
712
|
-
const predicate = textValue(fact.predicate);
|
|
713
|
-
if (!predicate) {
|
|
714
|
-
throw new Error('simulation fact predicate is required');
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
return {
|
|
718
|
-
id,
|
|
719
|
-
subjectId,
|
|
720
|
-
predicate,
|
|
721
|
-
objectId: textValue(fact.objectId),
|
|
722
|
-
content: textValue(fact.content),
|
|
723
|
-
validFrom: finiteNumber(fact.validFrom, 0),
|
|
724
|
-
invalidAt: fact.invalidAt === null || fact.invalidAt === undefined
|
|
725
|
-
? null
|
|
726
|
-
: finiteNumber(fact.invalidAt, 0),
|
|
727
|
-
sourceEventId: textValue(fact.sourceEventId),
|
|
728
|
-
data: isRecord(fact.data) ? { ...fact.data } : {},
|
|
729
|
-
};
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function dedupeSimulationEdges(edges: SimulationGraphEdge[]): SimulationGraphEdge[] {
|
|
733
|
-
const output: SimulationGraphEdge[] = [];
|
|
734
|
-
for (const edge of edges) {
|
|
735
|
-
addUniqueSimulationEdge(output, {
|
|
736
|
-
sourceId: textValue(edge.sourceId),
|
|
737
|
-
targetId: textValue(edge.targetId),
|
|
738
|
-
type: textValue(edge.type),
|
|
739
|
-
tick: finiteNumber(edge.tick, 0),
|
|
740
|
-
data: isRecord(edge.data) ? { ...edge.data } : {},
|
|
741
|
-
});
|
|
742
|
-
}
|
|
743
|
-
return output;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function addUniqueSimulationEdge(
|
|
747
|
-
output: SimulationGraphEdge[],
|
|
748
|
-
edge: SimulationGraphEdge,
|
|
749
|
-
): void {
|
|
750
|
-
if (!edge.sourceId || !edge.targetId || !edge.type) return;
|
|
751
|
-
const key = `${edge.sourceId}\0${edge.targetId}\0${edge.type}\0${edge.tick}`;
|
|
752
|
-
if (output.some((candidate) => (
|
|
753
|
-
`${candidate.sourceId}\0${candidate.targetId}\0${candidate.type}\0${candidate.tick}` === key
|
|
754
|
-
))) {
|
|
755
|
-
return;
|
|
756
|
-
}
|
|
757
|
-
output.push(edge);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
function textValue(value: unknown): string {
|
|
761
|
-
if (typeof value === 'string') return value.trim();
|
|
762
|
-
if (typeof value === 'number' || typeof value === 'boolean') return String(value);
|
|
763
|
-
return '';
|
|
764
|
-
}
|
|
765
|
-
|
|
766
|
-
function finiteNumber(value: unknown, fallback: number): number {
|
|
767
|
-
const parsed = Number(value);
|
|
768
|
-
return Number.isFinite(parsed) ? parsed : fallback;
|
|
769
|
-
}
|
|
770
|
-
|
|
771
|
-
function wholeNumber(value: unknown, fallback: number): number {
|
|
772
|
-
return Math.max(0, Math.floor(finiteNumber(value, fallback)));
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function uniqueText(values: string[]): string[] {
|
|
776
|
-
const output: string[] = [];
|
|
777
|
-
for (const value of values) {
|
|
778
|
-
const text = textValue(value);
|
|
779
|
-
if (!text || output.includes(text)) continue;
|
|
780
|
-
output.push(text);
|
|
781
|
-
}
|
|
782
|
-
return output;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
786
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
// ── SimGraph ──────────────────────────────────────────────────────
|
|
790
|
-
|
|
791
|
-
export class SimGraph {
|
|
792
|
-
private db: FalkorDB;
|
|
793
|
-
private graph: Graph;
|
|
794
|
-
/** Per-instance counter (review #8: avoid module-level mutable state) */
|
|
795
|
-
private factCounter = 0;
|
|
796
|
-
|
|
797
|
-
private constructor(db: FalkorDB, graph: Graph) {
|
|
798
|
-
this.db = db;
|
|
799
|
-
this.graph = graph;
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Open an embedded FalkorDB instance and select the simulation graph.
|
|
804
|
-
* If dataPath is provided, data persists to disk.
|
|
805
|
-
*/
|
|
806
|
-
static async open(dataPath?: string): Promise<SimGraph> {
|
|
807
|
-
const { FalkorDB } = await import('falkordblite');
|
|
808
|
-
const db = await FalkorDB.open(
|
|
809
|
-
dataPath ? { path: dataPath, logLevel: 'warning' } : { logLevel: 'warning' },
|
|
810
|
-
);
|
|
811
|
-
const graph = db.selectGraph('sim');
|
|
812
|
-
const sg = new SimGraph(db, graph);
|
|
813
|
-
await sg.initSchema();
|
|
814
|
-
return sg;
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
/** Close the graph database and stop the embedded server. */
|
|
818
|
-
async close(): Promise<void> {
|
|
819
|
-
await this.db.close();
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/**
|
|
823
|
-
* Escape hatch for domain-specific adapters that want to use labels/
|
|
824
|
-
* relationships outside the built-in schema (e.g. cybersim's `:CS*`
|
|
825
|
-
* nodes). Returned shape matches FalkorDB's `graph.query` result.
|
|
826
|
-
*/
|
|
827
|
-
async rawQuery<T = Record<string, unknown>>(
|
|
828
|
-
cypher: string,
|
|
829
|
-
params?: QueryParams,
|
|
830
|
-
): Promise<{ data?: T[] }> {
|
|
831
|
-
const result = await this.graph.query<[T]>(cypher, params ? { params } : undefined);
|
|
832
|
-
return { data: result.data as unknown as T[] | undefined };
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
/**
|
|
836
|
-
* Register a range index on a node label/property pair. Safe to call
|
|
837
|
-
* for already-indexed pairs — creation errors are swallowed.
|
|
838
|
-
*/
|
|
839
|
-
async ensureNodeIndex(label: string, property: string): Promise<void> {
|
|
840
|
-
try {
|
|
841
|
-
await this.graph.createNodeRangeIndex(label, property);
|
|
842
|
-
} catch {
|
|
843
|
-
// Index may already exist — ignore
|
|
844
|
-
}
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
/** Create indices for common query patterns. */
|
|
848
|
-
private async initSchema(): Promise<void> {
|
|
849
|
-
// Node indices
|
|
850
|
-
const indices = [
|
|
851
|
-
['Agent', 'id'], ['Fact', 'id'], ['Fact', 'category'],
|
|
852
|
-
['FeedPost', 'id'], ['Topic', 'name'], ['Event', 'id'],
|
|
853
|
-
['Company', 'id'],
|
|
854
|
-
];
|
|
855
|
-
for (const [label, prop] of indices) {
|
|
856
|
-
try {
|
|
857
|
-
await this.graph.createNodeRangeIndex(label, prop);
|
|
858
|
-
} catch {
|
|
859
|
-
// Index may already exist — ignore
|
|
860
|
-
}
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
// ── Entity Management ───────────────────────────────────────────
|
|
865
|
-
|
|
866
|
-
async addAgent(agent: {
|
|
867
|
-
id: string; name: string; role: string; organization: string; type: 'internal' | 'external';
|
|
868
|
-
}): Promise<void> {
|
|
869
|
-
await this.graph.query(
|
|
870
|
-
`MERGE (a:Agent {id: $id})
|
|
871
|
-
SET a.name = $name, a.role = $role, a.organization = $organization, a.type = $type`,
|
|
872
|
-
{ params: { id: agent.id, name: agent.name, role: agent.role, organization: agent.organization, type: agent.type } },
|
|
873
|
-
);
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
async addCompany(company: {
|
|
877
|
-
id: string; name: string; industry: string; stage: string;
|
|
878
|
-
}): Promise<void> {
|
|
879
|
-
await this.graph.query(
|
|
880
|
-
`MERGE (c:Company {id: $id})
|
|
881
|
-
SET c.name = $name, c.industry = $industry, c.stage = $stage`,
|
|
882
|
-
{ params: { id: company.id, name: company.name, industry: company.industry, stage: company.stage } },
|
|
883
|
-
);
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
async addCompetitor(companyIdA: string, companyIdB: string): Promise<void> {
|
|
887
|
-
await this.graph.query(
|
|
888
|
-
`MATCH (a:Company {id: $a}), (b:Company {id: $b})
|
|
889
|
-
MERGE (a)-[:COMPETES_WITH]->(b)
|
|
890
|
-
MERGE (b)-[:COMPETES_WITH]->(a)`,
|
|
891
|
-
{ params: { a: companyIdA, b: companyIdB } },
|
|
892
|
-
);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
async addWorksAt(agentId: string, companyId: string): Promise<void> {
|
|
896
|
-
await this.graph.query(
|
|
897
|
-
`MATCH (a:Agent {id: $agentId}), (c:Company {id: $companyId})
|
|
898
|
-
MERGE (a)-[:WORKS_AT]->(c)`,
|
|
899
|
-
{ params: { agentId, companyId } },
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
// ── Fact Management (Temporal) ──────────────────────────────────
|
|
904
|
-
|
|
905
|
-
/** Add a fact stated by an agent. Returns the fact ID. */
|
|
906
|
-
async addFact(
|
|
907
|
-
agentId: string,
|
|
908
|
-
content: string,
|
|
909
|
-
category: string,
|
|
910
|
-
tick: number,
|
|
911
|
-
aboutEntityId?: string,
|
|
912
|
-
): Promise<string> {
|
|
913
|
-
const factId = `fact-${++this.factCounter}`;
|
|
914
|
-
await this.graph.query(
|
|
915
|
-
`MATCH (a:Agent {id: $agentId})
|
|
916
|
-
CREATE (f:Fact {id: $factId, content: $content, category: $category,
|
|
917
|
-
source_tick: $tick, valid_from: $tick, invalid_at: -1})
|
|
918
|
-
CREATE (a)-[:STATED {tick: $tick}]->(f)`,
|
|
919
|
-
{ params: { agentId, factId, content, category, tick } },
|
|
920
|
-
);
|
|
921
|
-
|
|
922
|
-
// Link fact to entity if specified
|
|
923
|
-
if (aboutEntityId) {
|
|
924
|
-
await this.graph.query(
|
|
925
|
-
`MATCH (f:Fact {id: $factId})
|
|
926
|
-
OPTIONAL MATCH (e:Company {id: $entityId})
|
|
927
|
-
OPTIONAL MATCH (a:Agent {id: $entityId})
|
|
928
|
-
OPTIONAL MATCH (t:Topic {name: $entityId})
|
|
929
|
-
WITH f, coalesce(e, a, t) AS target
|
|
930
|
-
WHERE target IS NOT NULL
|
|
931
|
-
CREATE (f)-[:ABOUT]->(target)`,
|
|
932
|
-
{ params: { factId, entityId: aboutEntityId } },
|
|
933
|
-
);
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
return factId;
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
/** Invalidate a fact at the given tick. */
|
|
940
|
-
async invalidateFact(factId: string, tick: number): Promise<void> {
|
|
941
|
-
await this.graph.query(
|
|
942
|
-
`MATCH (f:Fact {id: $factId}) SET f.invalid_at = $tick`,
|
|
943
|
-
{ params: { factId, tick } },
|
|
944
|
-
);
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* Add a fact, checking for contradictions. If the same agent has
|
|
949
|
-
* a current fact in the same category about the same entity,
|
|
950
|
-
* invalidate the old fact and create a SUPERSEDES edge.
|
|
951
|
-
*/
|
|
952
|
-
async addFactWithSupersede(
|
|
953
|
-
agentId: string,
|
|
954
|
-
content: string,
|
|
955
|
-
category: string,
|
|
956
|
-
tick: number,
|
|
957
|
-
aboutEntityId?: string,
|
|
958
|
-
): Promise<string> {
|
|
959
|
-
// Find existing current facts by this agent in this category
|
|
960
|
-
if (aboutEntityId) {
|
|
961
|
-
const existing = await this.graph.query<[{ id: string }]>(
|
|
962
|
-
`MATCH (a:Agent {id: $agentId})-[:STATED]->(f:Fact)-[:ABOUT]->(e {id: $entityId})
|
|
963
|
-
WHERE f.category = $category AND f.invalid_at = -1
|
|
964
|
-
RETURN f.id AS id`,
|
|
965
|
-
{ params: { agentId, category, entityId: aboutEntityId } },
|
|
966
|
-
);
|
|
967
|
-
if (existing.data && existing.data.length > 0) {
|
|
968
|
-
for (const row of existing.data) {
|
|
969
|
-
const oldId = (row as unknown as Record<string, string>).id;
|
|
970
|
-
if (oldId) await this.invalidateFact(oldId, tick);
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const newFactId = await this.addFact(agentId, content, category, tick, aboutEntityId);
|
|
976
|
-
|
|
977
|
-
// Create SUPERSEDES edges
|
|
978
|
-
if (aboutEntityId) {
|
|
979
|
-
await this.graph.query(
|
|
980
|
-
`MATCH (newF:Fact {id: $newId})
|
|
981
|
-
MATCH (a:Agent {id: $agentId})-[:STATED]->(oldF:Fact)-[:ABOUT]->(e {id: $entityId})
|
|
982
|
-
WHERE oldF.category = $category AND oldF.invalid_at = $tick AND oldF.id <> $newId
|
|
983
|
-
MERGE (newF)-[:SUPERSEDES]->(oldF)`,
|
|
984
|
-
{ params: { newId: newFactId, agentId, category, entityId: aboutEntityId, tick } },
|
|
985
|
-
);
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
return newFactId;
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// ── Fact Queries ────────────────────────────────────────────────
|
|
992
|
-
|
|
993
|
-
/** Get all facts stated by an agent. */
|
|
994
|
-
async getAgentFacts(agentId: string, currentOnly: boolean = true): Promise<GraphFact[]> {
|
|
995
|
-
const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
|
|
996
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
997
|
-
`MATCH (a:Agent {id: $agentId})-[:STATED]->(f:Fact)
|
|
998
|
-
WHERE 1=1 ${whereClause}
|
|
999
|
-
RETURN f.id AS id, f.content AS content, f.category AS category,
|
|
1000
|
-
f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
|
|
1001
|
-
ORDER BY f.source_tick DESC`,
|
|
1002
|
-
{ params: { agentId } },
|
|
1003
|
-
);
|
|
1004
|
-
return this.parseFactRows(result.data);
|
|
1005
|
-
}
|
|
1006
|
-
|
|
1007
|
-
/** Get all facts about an entity. */
|
|
1008
|
-
async getFactsAbout(entityId: string, currentOnly: boolean = true): Promise<GraphFact[]> {
|
|
1009
|
-
const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
|
|
1010
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
1011
|
-
`MATCH (f:Fact)-[:ABOUT]->(e {id: $entityId})
|
|
1012
|
-
WHERE 1=1 ${whereClause}
|
|
1013
|
-
RETURN f.id AS id, f.content AS content, f.category AS category,
|
|
1014
|
-
f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
|
|
1015
|
-
ORDER BY f.source_tick DESC`,
|
|
1016
|
-
{ params: { entityId } },
|
|
1017
|
-
);
|
|
1018
|
-
return this.parseFactRows(result.data);
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
/** Get facts matching a keyword in content. */
|
|
1022
|
-
async getFactsByKeyword(keyword: string, currentOnly: boolean = true): Promise<GraphFact[]> {
|
|
1023
|
-
const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
|
|
1024
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
1025
|
-
`MATCH (f:Fact)
|
|
1026
|
-
WHERE f.content CONTAINS $keyword ${whereClause}
|
|
1027
|
-
RETURN f.id AS id, f.content AS content, f.category AS category,
|
|
1028
|
-
f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
|
|
1029
|
-
ORDER BY f.source_tick DESC
|
|
1030
|
-
LIMIT 20`,
|
|
1031
|
-
{ params: { keyword } },
|
|
1032
|
-
);
|
|
1033
|
-
return this.parseFactRows(result.data);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// ── Relationship Management ─────────────────────────────────────
|
|
1037
|
-
|
|
1038
|
-
async setRelationship(
|
|
1039
|
-
agentIdA: string, agentIdB: string,
|
|
1040
|
-
sentiment: number, trust: number, interactionCount: number,
|
|
1041
|
-
note?: string,
|
|
1042
|
-
): Promise<void> {
|
|
1043
|
-
await this.graph.query(
|
|
1044
|
-
`MATCH (a:Agent {id: $a}), (b:Agent {id: $b})
|
|
1045
|
-
MERGE (a)-[r:KNOWS]->(b)
|
|
1046
|
-
SET r.sentiment = $sentiment, r.trust = $trust,
|
|
1047
|
-
r.interaction_count = $interactionCount,
|
|
1048
|
-
r.last_note = $note`,
|
|
1049
|
-
{ params: { a: agentIdA, b: agentIdB, sentiment, trust, interactionCount, note: note ?? '' } },
|
|
1050
|
-
);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/** Review #12: Query both directions for KNOWS relationship. */
|
|
1054
|
-
async getRelationship(agentIdA: string, agentIdB: string): Promise<GraphRelationship | null> {
|
|
1055
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
1056
|
-
`MATCH (a:Agent {id: $a})-[r:KNOWS]-(b:Agent {id: $b})
|
|
1057
|
-
RETURN a.id AS agentIdA, b.id AS agentIdB, a.name AS nameA, b.name AS nameB,
|
|
1058
|
-
r.sentiment AS sentiment, r.trust AS trust,
|
|
1059
|
-
r.interaction_count AS interactionCount, r.last_note AS lastNote
|
|
1060
|
-
LIMIT 1`,
|
|
1061
|
-
{ params: { a: agentIdA, b: agentIdB } },
|
|
1062
|
-
);
|
|
1063
|
-
if (!result.data || result.data.length === 0) return null;
|
|
1064
|
-
const row = result.data[0] as unknown as Record<string, unknown>;
|
|
1065
|
-
return {
|
|
1066
|
-
agentIdA: String(row.agentIdA),
|
|
1067
|
-
agentIdB: String(row.agentIdB),
|
|
1068
|
-
nameA: String(row.nameA),
|
|
1069
|
-
nameB: String(row.nameB),
|
|
1070
|
-
sentiment: Number(row.sentiment ?? 0),
|
|
1071
|
-
trust: Number(row.trust ?? 0),
|
|
1072
|
-
interactionCount: Number(row.interactionCount ?? 0),
|
|
1073
|
-
lastNote: row.lastNote ? String(row.lastNote) : null,
|
|
1074
|
-
};
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
async getRelationshipsFor(agentId: string): Promise<GraphRelationship[]> {
|
|
1078
|
-
// Review #12: Query both directions
|
|
1079
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
1080
|
-
`MATCH (a:Agent {id: $id})-[r:KNOWS]-(b:Agent)
|
|
1081
|
-
RETURN a.id AS agentIdA, b.id AS agentIdB, a.name AS nameA, b.name AS nameB,
|
|
1082
|
-
r.sentiment AS sentiment, r.trust AS trust,
|
|
1083
|
-
r.interaction_count AS interactionCount, r.last_note AS lastNote
|
|
1084
|
-
ORDER BY r.interaction_count DESC`,
|
|
1085
|
-
{ params: { id: agentId } },
|
|
1086
|
-
);
|
|
1087
|
-
return (result.data ?? []).map((row: unknown) => {
|
|
1088
|
-
const r = row as Record<string, unknown>;
|
|
1089
|
-
return {
|
|
1090
|
-
agentIdA: String(r.agentIdA),
|
|
1091
|
-
agentIdB: String(r.agentIdB),
|
|
1092
|
-
nameA: String(r.nameA),
|
|
1093
|
-
nameB: String(r.nameB),
|
|
1094
|
-
sentiment: Number(r.sentiment ?? 0),
|
|
1095
|
-
trust: Number(r.trust ?? 0),
|
|
1096
|
-
interactionCount: Number(r.interactionCount ?? 0),
|
|
1097
|
-
lastNote: r.lastNote ? String(r.lastNote) : null,
|
|
1098
|
-
};
|
|
1099
|
-
});
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
// ── Feed Posts ──────────────────────────────────────────────────
|
|
1103
|
-
|
|
1104
|
-
async addPost(post: {
|
|
1105
|
-
id: string; content: string; authorId: string; authorName: string;
|
|
1106
|
-
authorRole: string; source: string; tick: number; replyTo?: string;
|
|
1107
|
-
topicTags?: string[];
|
|
1108
|
-
}): Promise<void> {
|
|
1109
|
-
await this.graph.query(
|
|
1110
|
-
`MATCH (a:Agent {id: $authorId})
|
|
1111
|
-
CREATE (p:FeedPost {id: $id, content: $content, author_id: $authorId,
|
|
1112
|
-
author_name: $authorName, author_role: $authorRole,
|
|
1113
|
-
source: $source, created_at: $tick,
|
|
1114
|
-
reply_to: $replyTo})
|
|
1115
|
-
CREATE (a)-[:POSTED]->(p)`,
|
|
1116
|
-
{
|
|
1117
|
-
params: {
|
|
1118
|
-
id: post.id, content: post.content, authorId: post.authorId,
|
|
1119
|
-
authorName: post.authorName, authorRole: post.authorRole,
|
|
1120
|
-
source: post.source, tick: post.tick, replyTo: post.replyTo ?? '',
|
|
1121
|
-
},
|
|
1122
|
-
},
|
|
1123
|
-
);
|
|
1124
|
-
|
|
1125
|
-
// Create reply edge
|
|
1126
|
-
if (post.replyTo) {
|
|
1127
|
-
await this.graph.query(
|
|
1128
|
-
`MATCH (child:FeedPost {id: $childId}), (parent:FeedPost {id: $parentId})
|
|
1129
|
-
CREATE (child)-[:REPLY_TO]->(parent)`,
|
|
1130
|
-
{ params: { childId: post.id, parentId: post.replyTo } },
|
|
1131
|
-
);
|
|
1132
|
-
}
|
|
1133
|
-
|
|
1134
|
-
// Tag topics
|
|
1135
|
-
if (post.topicTags && post.topicTags.length > 0) {
|
|
1136
|
-
for (const tag of post.topicTags) {
|
|
1137
|
-
await this.graph.query(
|
|
1138
|
-
`MATCH (p:FeedPost {id: $postId})
|
|
1139
|
-
MERGE (t:Topic {name: $tag})
|
|
1140
|
-
CREATE (p)-[:TAGGED]->(t)`,
|
|
1141
|
-
{ params: { postId: post.id, tag } },
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
async addReaction(agentId: string, postId: string): Promise<void> {
|
|
1148
|
-
await this.graph.query(
|
|
1149
|
-
`MATCH (a:Agent {id: $agentId}), (p:FeedPost {id: $postId})
|
|
1150
|
-
MERGE (a)-[:REACTED_TO]->(p)`,
|
|
1151
|
-
{ params: { agentId, postId } },
|
|
1152
|
-
);
|
|
1153
|
-
}
|
|
1154
|
-
|
|
1155
|
-
async getRecentPosts(limit: number = 50, sinceTick?: number): Promise<GraphFeedPost[]> {
|
|
1156
|
-
const whereClause = sinceTick !== undefined ? `WHERE p.created_at > $since` : '';
|
|
1157
|
-
const result = await this.graph.query<[Record<string, unknown>]>(
|
|
1158
|
-
`MATCH (p:FeedPost)
|
|
1159
|
-
${whereClause}
|
|
1160
|
-
OPTIONAL MATCH (p)<-[:REACTED_TO]-(reactor:Agent)
|
|
1161
|
-
OPTIONAL MATCH (p)-[:TAGGED]->(t:Topic)
|
|
1162
|
-
RETURN p.id AS id, p.content AS content, p.author_id AS authorId,
|
|
1163
|
-
p.author_name AS authorName, p.author_role AS authorRole,
|
|
1164
|
-
p.source AS source, p.created_at AS createdAt,
|
|
1165
|
-
p.reply_to AS replyTo,
|
|
1166
|
-
count(DISTINCT reactor) AS reactionCount,
|
|
1167
|
-
collect(DISTINCT t.name) AS topicTags
|
|
1168
|
-
ORDER BY p.created_at DESC
|
|
1169
|
-
LIMIT $limit`,
|
|
1170
|
-
{ params: { limit, since: sinceTick ?? 0 } },
|
|
1171
|
-
);
|
|
1172
|
-
return this.parsePostRows(result.data);
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// ── Events ──────────────────────────────────────────────────────
|
|
1176
|
-
|
|
1177
|
-
async addEvent(event: {
|
|
1178
|
-
id: string; type: string; description: string; tick: number;
|
|
1179
|
-
participantIds: string[];
|
|
1180
|
-
}): Promise<void> {
|
|
1181
|
-
await this.graph.query(
|
|
1182
|
-
`CREATE (e:Event {id: $id, type: $type, description: $description, tick: $tick})`,
|
|
1183
|
-
{ params: { id: event.id, type: event.type, description: event.description, tick: event.tick } },
|
|
1184
|
-
);
|
|
1185
|
-
for (const pid of event.participantIds) {
|
|
1186
|
-
await this.graph.query(
|
|
1187
|
-
`MATCH (e:Event {id: $eventId}), (a:Agent {id: $agentId})
|
|
1188
|
-
CREATE (e)-[:INVOLVED]->(a)`,
|
|
1189
|
-
{ params: { eventId: event.id, agentId: pid } },
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
}
|
|
1193
|
-
|
|
1194
|
-
// ── Graph Traversal ─────────────────────────────────────────────
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* Get an agent's full world view at a given tick:
|
|
1198
|
-
* - Facts they've stated (current)
|
|
1199
|
-
* - Facts their peers have stated (current)
|
|
1200
|
-
* - Facts about topics they've discussed
|
|
1201
|
-
*/
|
|
1202
|
-
async getAgentWorldView(agentId: string, tick: number): Promise<WorldView> {
|
|
1203
|
-
// Own facts
|
|
1204
|
-
const ownFacts = await this.getAgentFacts(agentId, true);
|
|
1205
|
-
|
|
1206
|
-
// Peer facts (from agents they KNOW — both directions, review #12)
|
|
1207
|
-
const peerResult = await this.graph.query<[Record<string, unknown>]>(
|
|
1208
|
-
`MATCH (a:Agent {id: $agentId})-[:KNOWS]-(peer:Agent)-[:STATED]->(f:Fact)
|
|
1209
|
-
WHERE f.invalid_at = -1
|
|
1210
|
-
RETURN peer.name AS peerName, peer.role AS peerRole,
|
|
1211
|
-
f.id AS id, f.content AS content, f.category AS category,
|
|
1212
|
-
f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
|
|
1213
|
-
ORDER BY f.source_tick DESC
|
|
1214
|
-
LIMIT 30`,
|
|
1215
|
-
{ params: { agentId } },
|
|
1216
|
-
);
|
|
1217
|
-
|
|
1218
|
-
// Group peer facts by peer
|
|
1219
|
-
const peerMap = new Map<string, { peerName: string; peerRole: string; facts: GraphFact[] }>();
|
|
1220
|
-
for (const row of (peerResult.data ?? []) as unknown as Record<string, unknown>[]) {
|
|
1221
|
-
const peerName = String(row.peerName);
|
|
1222
|
-
if (!peerMap.has(peerName)) {
|
|
1223
|
-
peerMap.set(peerName, { peerName, peerRole: String(row.peerRole), facts: [] });
|
|
1224
|
-
}
|
|
1225
|
-
peerMap.get(peerName)!.facts.push({
|
|
1226
|
-
id: String(row.id), content: String(row.content), category: String(row.category),
|
|
1227
|
-
sourceTick: Number(row.sourceTick), validFrom: Number(row.validFrom),
|
|
1228
|
-
invalidAt: Number(row.invalidAt) === -1 ? null : Number(row.invalidAt),
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return {
|
|
1233
|
-
ownFacts,
|
|
1234
|
-
peerFacts: [...peerMap.values()],
|
|
1235
|
-
topicFacts: [], // populated by caller if needed
|
|
1236
|
-
};
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
/**
|
|
1240
|
-
* Get context around any entity — all facts, agents, and relationships.
|
|
1241
|
-
* Review #9: removed unused depth param (queries are always 1-hop).
|
|
1242
|
-
*/
|
|
1243
|
-
async getEntityContext(entityId: string): Promise<GraphContext> {
|
|
1244
|
-
const factsResult = await this.graph.query<[Record<string, unknown>]>(
|
|
1245
|
-
`MATCH (e {id: $entityId})<-[:ABOUT]-(f:Fact)
|
|
1246
|
-
WHERE f.invalid_at = -1
|
|
1247
|
-
RETURN f.id AS id, f.content AS content, f.category AS category,
|
|
1248
|
-
f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
|
|
1249
|
-
ORDER BY f.source_tick DESC
|
|
1250
|
-
LIMIT 20`,
|
|
1251
|
-
{ params: { entityId } },
|
|
1252
|
-
);
|
|
1253
|
-
|
|
1254
|
-
const agentsResult = await this.graph.query<[Record<string, unknown>]>(
|
|
1255
|
-
`MATCH (a:Agent)-[:STATED]->(f:Fact)-[:ABOUT]->(e {id: $entityId})
|
|
1256
|
-
RETURN DISTINCT a.id AS id, a.name AS name, a.role AS role`,
|
|
1257
|
-
{ params: { entityId } },
|
|
1258
|
-
);
|
|
1259
|
-
|
|
1260
|
-
return {
|
|
1261
|
-
facts: this.parseFactRows(factsResult.data),
|
|
1262
|
-
agents: (agentsResult.data ?? []).map((r: unknown) => {
|
|
1263
|
-
const row = r as Record<string, unknown>;
|
|
1264
|
-
return { id: String(row.id), name: String(row.name), role: String(row.role) };
|
|
1265
|
-
}),
|
|
1266
|
-
relationships: [],
|
|
1267
|
-
};
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
// ── Helpers ─────────────────────────────────────────────────────
|
|
1271
|
-
|
|
1272
|
-
private parseFactRows(data: unknown[] | undefined): GraphFact[] {
|
|
1273
|
-
if (!data) return [];
|
|
1274
|
-
return data.map(row => {
|
|
1275
|
-
const r = row as unknown as Record<string, unknown>;
|
|
1276
|
-
return {
|
|
1277
|
-
id: String(r.id ?? ''),
|
|
1278
|
-
content: String(r.content ?? ''),
|
|
1279
|
-
category: String(r.category ?? ''),
|
|
1280
|
-
sourceTick: Number(r.sourceTick ?? 0),
|
|
1281
|
-
validFrom: Number(r.validFrom ?? 0),
|
|
1282
|
-
invalidAt: Number(r.invalidAt) === -1 ? null : Number(r.invalidAt ?? null),
|
|
1283
|
-
};
|
|
1284
|
-
});
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
private parsePostRows(data: unknown[] | undefined): GraphFeedPost[] {
|
|
1288
|
-
if (!data) return [];
|
|
1289
|
-
return data.map(row => {
|
|
1290
|
-
const r = row as unknown as Record<string, unknown>;
|
|
1291
|
-
return {
|
|
1292
|
-
id: String(r.id ?? ''),
|
|
1293
|
-
content: String(r.content ?? ''),
|
|
1294
|
-
authorId: String(r.authorId ?? ''),
|
|
1295
|
-
authorName: String(r.authorName ?? ''),
|
|
1296
|
-
authorRole: String(r.authorRole ?? ''),
|
|
1297
|
-
source: String(r.source ?? ''),
|
|
1298
|
-
createdAt: Number(r.createdAt ?? 0),
|
|
1299
|
-
replyTo: r.replyTo ? String(r.replyTo) : null,
|
|
1300
|
-
reactionCount: Number(r.reactionCount ?? 0),
|
|
1301
|
-
topicTags: Array.isArray(r.topicTags) ? r.topicTags.map(String) : [],
|
|
1302
|
-
};
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
}
|