@zergai/ztc 0.2.2-dev.35680553693 → 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.
@@ -1,795 +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
- export function recordSimulationFact(input) {
13
- const graph = normalizeSimulationFactGraph(input.graph);
14
- const fact = normalizeSimulationGraphFact(input.fact);
15
- const existingFacts = graph.facts.filter((item) => item.id !== fact.id);
16
- const supersedesEdges = uniqueText(input.fact.supersedesIds ?? [])
17
- .filter((targetId) => targetId !== fact.id)
18
- .map((targetId) => ({
19
- sourceId: fact.id,
20
- targetId,
21
- type: 'supersedes',
22
- tick: fact.validFrom,
23
- data: {
24
- sourceEventId: fact.sourceEventId,
25
- },
26
- }));
27
- return {
28
- schemaVersion: 1,
29
- facts: [...existingFacts, fact],
30
- edges: dedupeSimulationEdges([...graph.edges, ...supersedesEdges]),
31
- };
32
- }
33
- export function projectActiveFacts(input) {
34
- const graph = normalizeSimulationFactGraph(input.graph);
35
- const atTick = finiteNumber(input.atTick, Number.POSITIVE_INFINITY);
36
- const subjectIds = new Set(uniqueText(input.subjectIds ?? []));
37
- const predicates = new Set(uniqueText(input.predicates ?? []));
38
- const supersededIds = new Set(graph.edges
39
- .filter((edge) => edge.type === 'supersedes' && edge.tick <= atTick)
40
- .map((edge) => edge.targetId));
41
- return graph.facts
42
- .filter((fact) => fact.validFrom <= atTick)
43
- .filter((fact) => fact.invalidAt === null || fact.invalidAt > atTick)
44
- .filter((fact) => !supersededIds.has(fact.id))
45
- .filter((fact) => subjectIds.size === 0 || subjectIds.has(fact.subjectId))
46
- .filter((fact) => predicates.size === 0 || predicates.has(fact.predicate))
47
- .sort((a, b) => {
48
- if (a.validFrom !== b.validFrom)
49
- return a.validFrom - b.validFrom;
50
- return a.id.localeCompare(b.id);
51
- });
52
- }
53
- export function traverseSimulationNeighborhood(input) {
54
- const graph = normalizeSimulationFactGraph(input.graph);
55
- const factById = new Map(graph.facts.map((fact) => [fact.id, fact]));
56
- const maxDepth = Math.max(0, Math.floor(finiteNumber(input.maxDepth, 1)));
57
- const allowedEdgeTypes = new Set(uniqueText(input.edgeTypes ?? []));
58
- const eligibleEdges = graph.edges.filter((edge) => (allowedEdgeTypes.size === 0 || allowedEdgeTypes.has(edge.type)));
59
- const depthByFactId = {};
60
- const factIds = [];
61
- const selectedEdges = [];
62
- const queue = [];
63
- for (const id of uniqueText(input.startIds)) {
64
- if (!factById.has(id) || depthByFactId[id] !== undefined)
65
- continue;
66
- depthByFactId[id] = 0;
67
- factIds.push(id);
68
- queue.push({ id, depth: 0 });
69
- }
70
- while (queue.length > 0) {
71
- const current = queue.shift();
72
- if (current.depth >= maxDepth)
73
- continue;
74
- for (const edge of eligibleEdges) {
75
- const neighborId = edge.sourceId === current.id
76
- ? edge.targetId
77
- : edge.targetId === current.id
78
- ? edge.sourceId
79
- : '';
80
- if (!neighborId || !factById.has(neighborId))
81
- continue;
82
- addUniqueSimulationEdge(selectedEdges, edge);
83
- if (depthByFactId[neighborId] !== undefined)
84
- continue;
85
- depthByFactId[neighborId] = current.depth + 1;
86
- factIds.push(neighborId);
87
- queue.push({ id: neighborId, depth: current.depth + 1 });
88
- }
89
- }
90
- return {
91
- schemaVersion: 1,
92
- factIds,
93
- facts: factIds.map((id) => factById.get(id)).filter((fact) => fact !== undefined),
94
- edges: selectedEdges.filter((edge) => (depthByFactId[edge.sourceId] !== undefined &&
95
- depthByFactId[edge.targetId] !== undefined)),
96
- depthByFactId,
97
- };
98
- }
99
- export function buildSimulationFocusGraphContext(input) {
100
- const focusIds = uniqueText(input.focusIds ?? [])
101
- .slice(0, Math.max(0, Math.floor(finiteNumber(input.maxFocusIds, 20))));
102
- const nodes = new Map((input.nodes ?? [])
103
- .map((node) => normalizeFocusGraphNode(node))
104
- .filter((node) => node.id)
105
- .map((node) => [node.id, node]));
106
- const edges = (input.edges ?? [])
107
- .map((edge) => normalizeFocusGraphEdge(edge))
108
- .filter((edge) => edge.sourceId && edge.targetId && edge.type);
109
- return {
110
- version: 1,
111
- focusIds,
112
- lineages: input.lineage
113
- ? focusIds.map((focusId) => buildSimulationFocusLineage(focusId, nodes, edges, input.lineage))
114
- : [],
115
- relationGroups: (input.relations ?? []).flatMap((relation) => (focusIds.map((focusId) => buildSimulationFocusRelationGroup(focusId, edges, relation)))),
116
- };
117
- }
118
- export function buildSimulationSteeringInfluenceGraph(input) {
119
- const events = (input.steeringEvents ?? [])
120
- .map((event) => normalizeSteeringEvent(event))
121
- .filter((event) => event.id);
122
- const tasks = (input.tasks ?? [])
123
- .map((task) => normalizeSteeringTask(task))
124
- .filter((task) => task.id);
125
- const outputs = (input.outputs ?? [])
126
- .map((output) => normalizeSteeringOutput(output))
127
- .filter((output) => output.id && output.taskId);
128
- const ignoreVerdictKinds = new Set(uniqueText(input.ignoreVerdictKinds ?? ['hypothesis_submitted']));
129
- const taskEdges = [];
130
- const outputEdges = [];
131
- for (const event of events) {
132
- for (const task of tasks) {
133
- for (const signal of task.signals ?? []) {
134
- if (!steeringSignalMatchesEvent(event, task, signal, ignoreVerdictKinds))
135
- continue;
136
- const taskEdge = steeringTaskEdge(event, task, signal);
137
- addUniqueSteeringInfluenceEdge(taskEdges, taskEdge);
138
- for (const output of outputs.filter((candidate) => candidate.taskId === task.id)) {
139
- addUniqueSteeringInfluenceEdge(outputEdges, steeringOutputEdge(event, task, signal, output));
140
- }
141
- }
142
- }
143
- }
144
- return {
145
- version: 1,
146
- taskEdges,
147
- outputEdges,
148
- };
149
- }
150
- export function buildSimulationRestartMemory(input) {
151
- const activeFacts = Array.isArray(input.activeFacts) ? [...input.activeFacts] : [];
152
- const graphStats = input.graphStats ?? {};
153
- const maxFocusIds = Math.max(0, Math.floor(finiteNumber(input.maxFocusIds, 20)));
154
- return {
155
- version: 1,
156
- runId: textValue(input.runId),
157
- focusIds: uniqueText(input.focusIds ?? []).slice(0, maxFocusIds),
158
- activeFacts,
159
- graphStats: {
160
- nodes: wholeNumber(graphStats.nodes, 0),
161
- edges: wholeNumber(graphStats.edges, 0),
162
- activeFactCount: wholeNumber(graphStats.activeFactCount, activeFacts.length),
163
- },
164
- };
165
- }
166
- function steeringTaskEdge(event, task, signal) {
167
- return {
168
- sourceId: event.id,
169
- targetId: task.id,
170
- type: 'steering_influenced_task',
171
- createdAt: textValue(task.createdAt),
172
- data: steeringInfluenceData(task, signal),
173
- };
174
- }
175
- function steeringOutputEdge(event, task, signal, output) {
176
- const { priority: _priority, ...influenceData } = steeringInfluenceData(task, signal);
177
- return {
178
- sourceId: event.id,
179
- targetId: output.id,
180
- type: 'steering_influenced_output',
181
- createdAt: textValue(output.createdAt),
182
- data: {
183
- ...influenceData,
184
- ...(textValue(output.outputType) ? { outputType: textValue(output.outputType) } : {}),
185
- ...(isRecord(output.data) ? { ...output.data } : {}),
186
- },
187
- };
188
- }
189
- function steeringInfluenceData(task, signal) {
190
- return {
191
- taskId: task.id,
192
- taskType: textValue(task.taskType),
193
- source: textValue(task.source),
194
- steeringKind: textValue(signal.kind),
195
- subjectId: textValue(signal.subjectId),
196
- verdict: textValue(signal.verdict),
197
- selectionStatus: textValue(signal.selectionStatus),
198
- priority: finiteNumber(signal.priority, 0),
199
- };
200
- }
201
- function steeringSignalMatchesEvent(event, task, signal, ignoreVerdictKinds) {
202
- const eventTime = Date.parse(textValue(event.createdAt));
203
- const taskTime = Date.parse(textValue(task.createdAt));
204
- if (Number.isFinite(eventTime) && Number.isFinite(taskTime) && taskTime < eventTime)
205
- return false;
206
- const signalKind = textValue(signal.kind);
207
- const acceptedKinds = new Set(uniqueText(event.acceptedSignalKinds ?? []));
208
- if (acceptedKinds.size > 0 && !acceptedKinds.has(signalKind))
209
- return false;
210
- if (!matchingTextWhenBothPresent(event.subjectId, signal.subjectId))
211
- return false;
212
- if (!ignoreVerdictKinds.has(signalKind) && !matchingTextWhenBothPresent(event.verdict, signal.verdict))
213
- return false;
214
- if (!matchingTextWhenBothPresent(event.selectionStatus, signal.selectionStatus))
215
- return false;
216
- return true;
217
- }
218
- function matchingTextWhenBothPresent(left, right) {
219
- const leftText = textValue(left);
220
- const rightText = textValue(right);
221
- return !leftText || !rightText || leftText === rightText;
222
- }
223
- function addUniqueSteeringInfluenceEdge(edges, edge) {
224
- const key = `${edge.sourceId}\0${edge.targetId}\0${edge.type}`;
225
- if (edges.some((candidate) => `${candidate.sourceId}\0${candidate.targetId}\0${candidate.type}` === key)) {
226
- return;
227
- }
228
- edges.push(edge);
229
- }
230
- function normalizeSteeringEvent(event) {
231
- return {
232
- id: textValue(event.id),
233
- eventType: textValue(event.eventType),
234
- acceptedSignalKinds: uniqueText(event.acceptedSignalKinds ?? []),
235
- subjectId: textValue(event.subjectId),
236
- verdict: textValue(event.verdict),
237
- selectionStatus: textValue(event.selectionStatus),
238
- createdAt: textValue(event.createdAt),
239
- data: isRecord(event.data) ? { ...event.data } : {},
240
- };
241
- }
242
- function normalizeSteeringTask(task) {
243
- return {
244
- id: textValue(task.id),
245
- taskType: textValue(task.taskType),
246
- source: textValue(task.source),
247
- createdAt: textValue(task.createdAt),
248
- signals: (task.signals ?? []).map((signal) => normalizeSteeringSignal(signal)),
249
- };
250
- }
251
- function normalizeSteeringSignal(signal) {
252
- return {
253
- kind: textValue(signal.kind),
254
- subjectId: textValue(signal.subjectId),
255
- verdict: textValue(signal.verdict),
256
- selectionStatus: textValue(signal.selectionStatus),
257
- priority: finiteNumber(signal.priority, 0),
258
- data: isRecord(signal.data) ? { ...signal.data } : {},
259
- };
260
- }
261
- function normalizeSteeringOutput(output) {
262
- return {
263
- id: textValue(output.id),
264
- taskId: textValue(output.taskId),
265
- outputType: textValue(output.outputType),
266
- createdAt: textValue(output.createdAt),
267
- data: isRecord(output.data) ? { ...output.data } : {},
268
- };
269
- }
270
- function buildSimulationFocusLineage(focusId, nodes, edges, lineage) {
271
- const edgeTypes = new Set(uniqueText(lineage.edgeTypes));
272
- const direction = lineage.direction ?? 'out';
273
- const maxDepth = Math.max(0, Math.floor(finiteNumber(lineage.maxDepth, 12)));
274
- const pathIds = [];
275
- const pathEdges = [];
276
- const visited = new Set();
277
- let currentId = focusId;
278
- for (let depth = 0; currentId && !visited.has(currentId) && depth <= maxDepth; depth++) {
279
- visited.add(currentId);
280
- if (nodes.has(currentId))
281
- pathIds.push(currentId);
282
- const nextEdge = edges.find((edge) => (edgeTypes.has(edge.type) &&
283
- (direction === 'out' ? edge.sourceId === currentId : edge.targetId === currentId)));
284
- if (!nextEdge)
285
- break;
286
- pathEdges.push(nextEdge);
287
- currentId = direction === 'out' ? nextEdge.targetId : nextEdge.sourceId;
288
- }
289
- const nodeIds = lineage.includeFocus === false
290
- ? pathIds.slice(1).reverse()
291
- : pathIds.reverse();
292
- return {
293
- focusId,
294
- nodeIds,
295
- edgeTypes: uniqueText(pathEdges.map((edge) => edge.type)),
296
- edges: pathEdges,
297
- };
298
- }
299
- function buildSimulationFocusRelationGroup(focusId, edges, relation) {
300
- const edgeTypes = new Set(uniqueText(relation.edgeTypes));
301
- const direction = relation.direction ?? 'both';
302
- const limit = Math.max(0, Math.floor(finiteNumber(relation.limit, Number.MAX_SAFE_INTEGER)));
303
- const selectedEdges = edges
304
- .filter((edge) => edgeTypes.has(edge.type))
305
- .filter((edge) => focusRelationEdgeMatches(edge, focusId, direction))
306
- .slice(0, limit);
307
- return {
308
- name: textValue(relation.name),
309
- focusId,
310
- nodeIds: uniqueText(selectedEdges.map((edge) => focusRelationOtherNodeId(edge, focusId, direction))),
311
- edges: selectedEdges,
312
- };
313
- }
314
- function focusRelationEdgeMatches(edge, focusId, direction) {
315
- if (direction === 'out')
316
- return edge.sourceId === focusId;
317
- if (direction === 'in')
318
- return edge.targetId === focusId;
319
- return edge.sourceId === focusId || edge.targetId === focusId;
320
- }
321
- function focusRelationOtherNodeId(edge, focusId, direction) {
322
- if (direction === 'out')
323
- return edge.targetId;
324
- if (direction === 'in')
325
- return edge.sourceId;
326
- return edge.sourceId === focusId ? edge.targetId : edge.sourceId;
327
- }
328
- function normalizeFocusGraphNode(node) {
329
- return {
330
- id: textValue(node.id),
331
- kind: textValue(node.kind),
332
- label: textValue(node.label),
333
- data: isRecord(node.data) ? { ...node.data } : {},
334
- };
335
- }
336
- function normalizeFocusGraphEdge(edge) {
337
- return {
338
- sourceId: textValue(edge.sourceId),
339
- targetId: textValue(edge.targetId),
340
- type: textValue(edge.type),
341
- createdAt: textValue(edge.createdAt),
342
- data: isRecord(edge.data) ? { ...edge.data } : {},
343
- };
344
- }
345
- function normalizeSimulationFactGraph(graph) {
346
- return {
347
- schemaVersion: 1,
348
- facts: (graph?.facts ?? []).map((fact) => normalizeSimulationGraphFact(fact)),
349
- edges: dedupeSimulationEdges(graph?.edges ?? []),
350
- };
351
- }
352
- function normalizeSimulationGraphFact(fact) {
353
- const id = textValue(fact.id);
354
- if (!id) {
355
- throw new Error('simulation fact id is required');
356
- }
357
- const subjectId = textValue(fact.subjectId);
358
- if (!subjectId) {
359
- throw new Error('simulation fact subject id is required');
360
- }
361
- const predicate = textValue(fact.predicate);
362
- if (!predicate) {
363
- throw new Error('simulation fact predicate is required');
364
- }
365
- return {
366
- id,
367
- subjectId,
368
- predicate,
369
- objectId: textValue(fact.objectId),
370
- content: textValue(fact.content),
371
- validFrom: finiteNumber(fact.validFrom, 0),
372
- invalidAt: fact.invalidAt === null || fact.invalidAt === undefined
373
- ? null
374
- : finiteNumber(fact.invalidAt, 0),
375
- sourceEventId: textValue(fact.sourceEventId),
376
- data: isRecord(fact.data) ? { ...fact.data } : {},
377
- };
378
- }
379
- function dedupeSimulationEdges(edges) {
380
- const output = [];
381
- for (const edge of edges) {
382
- addUniqueSimulationEdge(output, {
383
- sourceId: textValue(edge.sourceId),
384
- targetId: textValue(edge.targetId),
385
- type: textValue(edge.type),
386
- tick: finiteNumber(edge.tick, 0),
387
- data: isRecord(edge.data) ? { ...edge.data } : {},
388
- });
389
- }
390
- return output;
391
- }
392
- function addUniqueSimulationEdge(output, edge) {
393
- if (!edge.sourceId || !edge.targetId || !edge.type)
394
- return;
395
- const key = `${edge.sourceId}\0${edge.targetId}\0${edge.type}\0${edge.tick}`;
396
- if (output.some((candidate) => (`${candidate.sourceId}\0${candidate.targetId}\0${candidate.type}\0${candidate.tick}` === key))) {
397
- return;
398
- }
399
- output.push(edge);
400
- }
401
- function textValue(value) {
402
- if (typeof value === 'string')
403
- return value.trim();
404
- if (typeof value === 'number' || typeof value === 'boolean')
405
- return String(value);
406
- return '';
407
- }
408
- function finiteNumber(value, fallback) {
409
- const parsed = Number(value);
410
- return Number.isFinite(parsed) ? parsed : fallback;
411
- }
412
- function wholeNumber(value, fallback) {
413
- return Math.max(0, Math.floor(finiteNumber(value, fallback)));
414
- }
415
- function uniqueText(values) {
416
- const output = [];
417
- for (const value of values) {
418
- const text = textValue(value);
419
- if (!text || output.includes(text))
420
- continue;
421
- output.push(text);
422
- }
423
- return output;
424
- }
425
- function isRecord(value) {
426
- return typeof value === 'object' && value !== null && !Array.isArray(value);
427
- }
428
- // ── SimGraph ──────────────────────────────────────────────────────
429
- export class SimGraph {
430
- db;
431
- graph;
432
- /** Per-instance counter (review #8: avoid module-level mutable state) */
433
- factCounter = 0;
434
- constructor(db, graph) {
435
- this.db = db;
436
- this.graph = graph;
437
- }
438
- /**
439
- * Open an embedded FalkorDB instance and select the simulation graph.
440
- * If dataPath is provided, data persists to disk.
441
- */
442
- static async open(dataPath) {
443
- const { FalkorDB } = await import('falkordblite');
444
- const db = await FalkorDB.open(dataPath ? { path: dataPath, logLevel: 'warning' } : { logLevel: 'warning' });
445
- const graph = db.selectGraph('sim');
446
- const sg = new SimGraph(db, graph);
447
- await sg.initSchema();
448
- return sg;
449
- }
450
- /** Close the graph database and stop the embedded server. */
451
- async close() {
452
- await this.db.close();
453
- }
454
- /**
455
- * Escape hatch for domain-specific adapters that want to use labels/
456
- * relationships outside the built-in schema (e.g. cybersim's `:CS*`
457
- * nodes). Returned shape matches FalkorDB's `graph.query` result.
458
- */
459
- async rawQuery(cypher, params) {
460
- const result = await this.graph.query(cypher, params ? { params } : undefined);
461
- return { data: result.data };
462
- }
463
- /**
464
- * Register a range index on a node label/property pair. Safe to call
465
- * for already-indexed pairs — creation errors are swallowed.
466
- */
467
- async ensureNodeIndex(label, property) {
468
- try {
469
- await this.graph.createNodeRangeIndex(label, property);
470
- }
471
- catch {
472
- // Index may already exist — ignore
473
- }
474
- }
475
- /** Create indices for common query patterns. */
476
- async initSchema() {
477
- // Node indices
478
- const indices = [
479
- ['Agent', 'id'], ['Fact', 'id'], ['Fact', 'category'],
480
- ['FeedPost', 'id'], ['Topic', 'name'], ['Event', 'id'],
481
- ['Company', 'id'],
482
- ];
483
- for (const [label, prop] of indices) {
484
- try {
485
- await this.graph.createNodeRangeIndex(label, prop);
486
- }
487
- catch {
488
- // Index may already exist — ignore
489
- }
490
- }
491
- }
492
- // ── Entity Management ───────────────────────────────────────────
493
- async addAgent(agent) {
494
- await this.graph.query(`MERGE (a:Agent {id: $id})
495
- SET a.name = $name, a.role = $role, a.organization = $organization, a.type = $type`, { params: { id: agent.id, name: agent.name, role: agent.role, organization: agent.organization, type: agent.type } });
496
- }
497
- async addCompany(company) {
498
- await this.graph.query(`MERGE (c:Company {id: $id})
499
- SET c.name = $name, c.industry = $industry, c.stage = $stage`, { params: { id: company.id, name: company.name, industry: company.industry, stage: company.stage } });
500
- }
501
- async addCompetitor(companyIdA, companyIdB) {
502
- await this.graph.query(`MATCH (a:Company {id: $a}), (b:Company {id: $b})
503
- MERGE (a)-[:COMPETES_WITH]->(b)
504
- MERGE (b)-[:COMPETES_WITH]->(a)`, { params: { a: companyIdA, b: companyIdB } });
505
- }
506
- async addWorksAt(agentId, companyId) {
507
- await this.graph.query(`MATCH (a:Agent {id: $agentId}), (c:Company {id: $companyId})
508
- MERGE (a)-[:WORKS_AT]->(c)`, { params: { agentId, companyId } });
509
- }
510
- // ── Fact Management (Temporal) ──────────────────────────────────
511
- /** Add a fact stated by an agent. Returns the fact ID. */
512
- async addFact(agentId, content, category, tick, aboutEntityId) {
513
- const factId = `fact-${++this.factCounter}`;
514
- await this.graph.query(`MATCH (a:Agent {id: $agentId})
515
- CREATE (f:Fact {id: $factId, content: $content, category: $category,
516
- source_tick: $tick, valid_from: $tick, invalid_at: -1})
517
- CREATE (a)-[:STATED {tick: $tick}]->(f)`, { params: { agentId, factId, content, category, tick } });
518
- // Link fact to entity if specified
519
- if (aboutEntityId) {
520
- await this.graph.query(`MATCH (f:Fact {id: $factId})
521
- OPTIONAL MATCH (e:Company {id: $entityId})
522
- OPTIONAL MATCH (a:Agent {id: $entityId})
523
- OPTIONAL MATCH (t:Topic {name: $entityId})
524
- WITH f, coalesce(e, a, t) AS target
525
- WHERE target IS NOT NULL
526
- CREATE (f)-[:ABOUT]->(target)`, { params: { factId, entityId: aboutEntityId } });
527
- }
528
- return factId;
529
- }
530
- /** Invalidate a fact at the given tick. */
531
- async invalidateFact(factId, tick) {
532
- await this.graph.query(`MATCH (f:Fact {id: $factId}) SET f.invalid_at = $tick`, { params: { factId, tick } });
533
- }
534
- /**
535
- * Add a fact, checking for contradictions. If the same agent has
536
- * a current fact in the same category about the same entity,
537
- * invalidate the old fact and create a SUPERSEDES edge.
538
- */
539
- async addFactWithSupersede(agentId, content, category, tick, aboutEntityId) {
540
- // Find existing current facts by this agent in this category
541
- if (aboutEntityId) {
542
- const existing = await this.graph.query(`MATCH (a:Agent {id: $agentId})-[:STATED]->(f:Fact)-[:ABOUT]->(e {id: $entityId})
543
- WHERE f.category = $category AND f.invalid_at = -1
544
- RETURN f.id AS id`, { params: { agentId, category, entityId: aboutEntityId } });
545
- if (existing.data && existing.data.length > 0) {
546
- for (const row of existing.data) {
547
- const oldId = row.id;
548
- if (oldId)
549
- await this.invalidateFact(oldId, tick);
550
- }
551
- }
552
- }
553
- const newFactId = await this.addFact(agentId, content, category, tick, aboutEntityId);
554
- // Create SUPERSEDES edges
555
- if (aboutEntityId) {
556
- await this.graph.query(`MATCH (newF:Fact {id: $newId})
557
- MATCH (a:Agent {id: $agentId})-[:STATED]->(oldF:Fact)-[:ABOUT]->(e {id: $entityId})
558
- WHERE oldF.category = $category AND oldF.invalid_at = $tick AND oldF.id <> $newId
559
- MERGE (newF)-[:SUPERSEDES]->(oldF)`, { params: { newId: newFactId, agentId, category, entityId: aboutEntityId, tick } });
560
- }
561
- return newFactId;
562
- }
563
- // ── Fact Queries ────────────────────────────────────────────────
564
- /** Get all facts stated by an agent. */
565
- async getAgentFacts(agentId, currentOnly = true) {
566
- const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
567
- const result = await this.graph.query(`MATCH (a:Agent {id: $agentId})-[:STATED]->(f:Fact)
568
- WHERE 1=1 ${whereClause}
569
- RETURN f.id AS id, f.content AS content, f.category AS category,
570
- f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
571
- ORDER BY f.source_tick DESC`, { params: { agentId } });
572
- return this.parseFactRows(result.data);
573
- }
574
- /** Get all facts about an entity. */
575
- async getFactsAbout(entityId, currentOnly = true) {
576
- const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
577
- const result = await this.graph.query(`MATCH (f:Fact)-[:ABOUT]->(e {id: $entityId})
578
- WHERE 1=1 ${whereClause}
579
- RETURN f.id AS id, f.content AS content, f.category AS category,
580
- f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
581
- ORDER BY f.source_tick DESC`, { params: { entityId } });
582
- return this.parseFactRows(result.data);
583
- }
584
- /** Get facts matching a keyword in content. */
585
- async getFactsByKeyword(keyword, currentOnly = true) {
586
- const whereClause = currentOnly ? 'AND f.invalid_at = -1' : '';
587
- const result = await this.graph.query(`MATCH (f:Fact)
588
- WHERE f.content CONTAINS $keyword ${whereClause}
589
- RETURN f.id AS id, f.content AS content, f.category AS category,
590
- f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
591
- ORDER BY f.source_tick DESC
592
- LIMIT 20`, { params: { keyword } });
593
- return this.parseFactRows(result.data);
594
- }
595
- // ── Relationship Management ─────────────────────────────────────
596
- async setRelationship(agentIdA, agentIdB, sentiment, trust, interactionCount, note) {
597
- await this.graph.query(`MATCH (a:Agent {id: $a}), (b:Agent {id: $b})
598
- MERGE (a)-[r:KNOWS]->(b)
599
- SET r.sentiment = $sentiment, r.trust = $trust,
600
- r.interaction_count = $interactionCount,
601
- r.last_note = $note`, { params: { a: agentIdA, b: agentIdB, sentiment, trust, interactionCount, note: note ?? '' } });
602
- }
603
- /** Review #12: Query both directions for KNOWS relationship. */
604
- async getRelationship(agentIdA, agentIdB) {
605
- const result = await this.graph.query(`MATCH (a:Agent {id: $a})-[r:KNOWS]-(b:Agent {id: $b})
606
- RETURN a.id AS agentIdA, b.id AS agentIdB, a.name AS nameA, b.name AS nameB,
607
- r.sentiment AS sentiment, r.trust AS trust,
608
- r.interaction_count AS interactionCount, r.last_note AS lastNote
609
- LIMIT 1`, { params: { a: agentIdA, b: agentIdB } });
610
- if (!result.data || result.data.length === 0)
611
- return null;
612
- const row = result.data[0];
613
- return {
614
- agentIdA: String(row.agentIdA),
615
- agentIdB: String(row.agentIdB),
616
- nameA: String(row.nameA),
617
- nameB: String(row.nameB),
618
- sentiment: Number(row.sentiment ?? 0),
619
- trust: Number(row.trust ?? 0),
620
- interactionCount: Number(row.interactionCount ?? 0),
621
- lastNote: row.lastNote ? String(row.lastNote) : null,
622
- };
623
- }
624
- async getRelationshipsFor(agentId) {
625
- // Review #12: Query both directions
626
- const result = await this.graph.query(`MATCH (a:Agent {id: $id})-[r:KNOWS]-(b:Agent)
627
- RETURN a.id AS agentIdA, b.id AS agentIdB, a.name AS nameA, b.name AS nameB,
628
- r.sentiment AS sentiment, r.trust AS trust,
629
- r.interaction_count AS interactionCount, r.last_note AS lastNote
630
- ORDER BY r.interaction_count DESC`, { params: { id: agentId } });
631
- return (result.data ?? []).map((row) => {
632
- const r = row;
633
- return {
634
- agentIdA: String(r.agentIdA),
635
- agentIdB: String(r.agentIdB),
636
- nameA: String(r.nameA),
637
- nameB: String(r.nameB),
638
- sentiment: Number(r.sentiment ?? 0),
639
- trust: Number(r.trust ?? 0),
640
- interactionCount: Number(r.interactionCount ?? 0),
641
- lastNote: r.lastNote ? String(r.lastNote) : null,
642
- };
643
- });
644
- }
645
- // ── Feed Posts ──────────────────────────────────────────────────
646
- async addPost(post) {
647
- await this.graph.query(`MATCH (a:Agent {id: $authorId})
648
- CREATE (p:FeedPost {id: $id, content: $content, author_id: $authorId,
649
- author_name: $authorName, author_role: $authorRole,
650
- source: $source, created_at: $tick,
651
- reply_to: $replyTo})
652
- CREATE (a)-[:POSTED]->(p)`, {
653
- params: {
654
- id: post.id, content: post.content, authorId: post.authorId,
655
- authorName: post.authorName, authorRole: post.authorRole,
656
- source: post.source, tick: post.tick, replyTo: post.replyTo ?? '',
657
- },
658
- });
659
- // Create reply edge
660
- if (post.replyTo) {
661
- await this.graph.query(`MATCH (child:FeedPost {id: $childId}), (parent:FeedPost {id: $parentId})
662
- CREATE (child)-[:REPLY_TO]->(parent)`, { params: { childId: post.id, parentId: post.replyTo } });
663
- }
664
- // Tag topics
665
- if (post.topicTags && post.topicTags.length > 0) {
666
- for (const tag of post.topicTags) {
667
- await this.graph.query(`MATCH (p:FeedPost {id: $postId})
668
- MERGE (t:Topic {name: $tag})
669
- CREATE (p)-[:TAGGED]->(t)`, { params: { postId: post.id, tag } });
670
- }
671
- }
672
- }
673
- async addReaction(agentId, postId) {
674
- await this.graph.query(`MATCH (a:Agent {id: $agentId}), (p:FeedPost {id: $postId})
675
- MERGE (a)-[:REACTED_TO]->(p)`, { params: { agentId, postId } });
676
- }
677
- async getRecentPosts(limit = 50, sinceTick) {
678
- const whereClause = sinceTick !== undefined ? `WHERE p.created_at > $since` : '';
679
- const result = await this.graph.query(`MATCH (p:FeedPost)
680
- ${whereClause}
681
- OPTIONAL MATCH (p)<-[:REACTED_TO]-(reactor:Agent)
682
- OPTIONAL MATCH (p)-[:TAGGED]->(t:Topic)
683
- RETURN p.id AS id, p.content AS content, p.author_id AS authorId,
684
- p.author_name AS authorName, p.author_role AS authorRole,
685
- p.source AS source, p.created_at AS createdAt,
686
- p.reply_to AS replyTo,
687
- count(DISTINCT reactor) AS reactionCount,
688
- collect(DISTINCT t.name) AS topicTags
689
- ORDER BY p.created_at DESC
690
- LIMIT $limit`, { params: { limit, since: sinceTick ?? 0 } });
691
- return this.parsePostRows(result.data);
692
- }
693
- // ── Events ──────────────────────────────────────────────────────
694
- async addEvent(event) {
695
- await this.graph.query(`CREATE (e:Event {id: $id, type: $type, description: $description, tick: $tick})`, { params: { id: event.id, type: event.type, description: event.description, tick: event.tick } });
696
- for (const pid of event.participantIds) {
697
- await this.graph.query(`MATCH (e:Event {id: $eventId}), (a:Agent {id: $agentId})
698
- CREATE (e)-[:INVOLVED]->(a)`, { params: { eventId: event.id, agentId: pid } });
699
- }
700
- }
701
- // ── Graph Traversal ─────────────────────────────────────────────
702
- /**
703
- * Get an agent's full world view at a given tick:
704
- * - Facts they've stated (current)
705
- * - Facts their peers have stated (current)
706
- * - Facts about topics they've discussed
707
- */
708
- async getAgentWorldView(agentId, tick) {
709
- // Own facts
710
- const ownFacts = await this.getAgentFacts(agentId, true);
711
- // Peer facts (from agents they KNOW — both directions, review #12)
712
- const peerResult = await this.graph.query(`MATCH (a:Agent {id: $agentId})-[:KNOWS]-(peer:Agent)-[:STATED]->(f:Fact)
713
- WHERE f.invalid_at = -1
714
- RETURN peer.name AS peerName, peer.role AS peerRole,
715
- f.id AS id, f.content AS content, f.category AS category,
716
- f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
717
- ORDER BY f.source_tick DESC
718
- LIMIT 30`, { params: { agentId } });
719
- // Group peer facts by peer
720
- const peerMap = new Map();
721
- for (const row of (peerResult.data ?? [])) {
722
- const peerName = String(row.peerName);
723
- if (!peerMap.has(peerName)) {
724
- peerMap.set(peerName, { peerName, peerRole: String(row.peerRole), facts: [] });
725
- }
726
- peerMap.get(peerName).facts.push({
727
- id: String(row.id), content: String(row.content), category: String(row.category),
728
- sourceTick: Number(row.sourceTick), validFrom: Number(row.validFrom),
729
- invalidAt: Number(row.invalidAt) === -1 ? null : Number(row.invalidAt),
730
- });
731
- }
732
- return {
733
- ownFacts,
734
- peerFacts: [...peerMap.values()],
735
- topicFacts: [], // populated by caller if needed
736
- };
737
- }
738
- /**
739
- * Get context around any entity — all facts, agents, and relationships.
740
- * Review #9: removed unused depth param (queries are always 1-hop).
741
- */
742
- async getEntityContext(entityId) {
743
- const factsResult = await this.graph.query(`MATCH (e {id: $entityId})<-[:ABOUT]-(f:Fact)
744
- WHERE f.invalid_at = -1
745
- RETURN f.id AS id, f.content AS content, f.category AS category,
746
- f.source_tick AS sourceTick, f.valid_from AS validFrom, f.invalid_at AS invalidAt
747
- ORDER BY f.source_tick DESC
748
- LIMIT 20`, { params: { entityId } });
749
- const agentsResult = await this.graph.query(`MATCH (a:Agent)-[:STATED]->(f:Fact)-[:ABOUT]->(e {id: $entityId})
750
- RETURN DISTINCT a.id AS id, a.name AS name, a.role AS role`, { params: { entityId } });
751
- return {
752
- facts: this.parseFactRows(factsResult.data),
753
- agents: (agentsResult.data ?? []).map((r) => {
754
- const row = r;
755
- return { id: String(row.id), name: String(row.name), role: String(row.role) };
756
- }),
757
- relationships: [],
758
- };
759
- }
760
- // ── Helpers ─────────────────────────────────────────────────────
761
- parseFactRows(data) {
762
- if (!data)
763
- return [];
764
- return data.map(row => {
765
- const r = row;
766
- return {
767
- id: String(r.id ?? ''),
768
- content: String(r.content ?? ''),
769
- category: String(r.category ?? ''),
770
- sourceTick: Number(r.sourceTick ?? 0),
771
- validFrom: Number(r.validFrom ?? 0),
772
- invalidAt: Number(r.invalidAt) === -1 ? null : Number(r.invalidAt ?? null),
773
- };
774
- });
775
- }
776
- parsePostRows(data) {
777
- if (!data)
778
- return [];
779
- return data.map(row => {
780
- const r = row;
781
- return {
782
- id: String(r.id ?? ''),
783
- content: String(r.content ?? ''),
784
- authorId: String(r.authorId ?? ''),
785
- authorName: String(r.authorName ?? ''),
786
- authorRole: String(r.authorRole ?? ''),
787
- source: String(r.source ?? ''),
788
- createdAt: Number(r.createdAt ?? 0),
789
- replyTo: r.replyTo ? String(r.replyTo) : null,
790
- reactionCount: Number(r.reactionCount ?? 0),
791
- topicTags: Array.isArray(r.topicTags) ? r.topicTags.map(String) : [],
792
- };
793
- });
794
- }
795
- }