@neuroverseos/nv-sim 0.1.2 → 0.1.6
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/README.md +376 -66
- package/dist/adapters/mirofish.js +461 -0
- package/dist/adapters/scienceclaw.js +750 -0
- package/dist/assets/index-CHmUN8s0.js +532 -0
- package/dist/assets/index-DWgMnB7I.css +1 -0
- package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
- package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
- package/dist/components/ConstraintsPanel.js +11 -0
- package/dist/components/StakeholderBuilder.js +32 -0
- package/dist/components/ui/badge.js +24 -0
- package/dist/components/ui/button.js +70 -0
- package/dist/components/ui/card.js +57 -0
- package/dist/components/ui/input.js +44 -0
- package/dist/components/ui/label.js +45 -0
- package/dist/components/ui/select.js +70 -0
- package/dist/engine/aiProvider.js +681 -0
- package/dist/engine/auditTrace.js +352 -0
- package/dist/engine/behavioralAnalysis.js +605 -0
- package/dist/engine/cli.js +1408 -299
- package/dist/engine/dynamicsGovernance.js +588 -0
- package/dist/engine/fullGovernedLoop.js +367 -0
- package/dist/engine/governance.js +8 -3
- package/dist/engine/governedSimulation.js +114 -17
- package/dist/engine/index.js +56 -1
- package/dist/engine/liveAdapter.js +342 -0
- package/dist/engine/liveVisualizer.js +3063 -0
- package/dist/engine/metrics/science.metrics.js +335 -0
- package/dist/engine/narrativeInjection.js +305 -0
- package/dist/engine/policyEnforcement.js +1611 -0
- package/dist/engine/policyEngine.js +799 -0
- package/dist/engine/primeRadiant.js +540 -0
- package/dist/engine/reasoningEngine.js +57 -3
- package/dist/engine/reportEngine.js +97 -0
- package/dist/engine/scenarioComparison.js +463 -0
- package/dist/engine/scenarioLibrary.js +231 -0
- package/dist/engine/swarmSimulation.js +54 -1
- package/dist/engine/worldComparison.js +358 -0
- package/dist/engine/worldStorage.js +232 -0
- package/dist/favicon.ico +0 -0
- package/dist/index.html +23 -0
- package/dist/lib/reasoningEngine.js +290 -0
- package/dist/lib/simulationAdapter.js +686 -0
- package/dist/lib/swarmParser.js +291 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/utils.js +8 -0
- package/dist/placeholder.svg +1 -0
- package/dist/robots.txt +14 -0
- package/dist/runtime/govern.js +473 -0
- package/dist/runtime/index.js +75 -0
- package/dist/runtime/types.js +11 -0
- package/package.json +17 -12
- package/variants/.gitkeep +0 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Universal Simulation Adapter
|
|
4
|
+
*
|
|
5
|
+
* Converts output from ANY simulation engine into a normalized schema
|
|
6
|
+
* that Echelon can reason about. Supported formats:
|
|
7
|
+
*
|
|
8
|
+
* - MiroFish (JSON with agents + emergent_behaviors)
|
|
9
|
+
* - NetLogo (tick-based key:value output)
|
|
10
|
+
* - Mesa (Python agent action logs)
|
|
11
|
+
* - AnyLogic / CSV (time-series tabular data)
|
|
12
|
+
* - Generic JSON (any structured simulation output)
|
|
13
|
+
* - Freeform text (natural language simulation descriptions)
|
|
14
|
+
*
|
|
15
|
+
* Architecture:
|
|
16
|
+
* Raw simulation output → detectFormat() → parse*() → NormalizedSwarm
|
|
17
|
+
* NormalizedSwarm → Echelon reasoning → strategic insights
|
|
18
|
+
*
|
|
19
|
+
* Key insight: every simulation engine produces the same types of signals:
|
|
20
|
+
* agents, states, interactions, events, outcomes, timelines
|
|
21
|
+
* The adapter normalizes these into a universal internal schema.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.FORMAT_LABELS = void 0;
|
|
25
|
+
exports.detectFormat = detectFormat;
|
|
26
|
+
exports.parseSimulation = parseSimulation;
|
|
27
|
+
// ============================================
|
|
28
|
+
// FORMAT DETECTION
|
|
29
|
+
// ============================================
|
|
30
|
+
/**
|
|
31
|
+
* Detect the simulation format from raw input.
|
|
32
|
+
* Uses heuristics — no false positives needed, just best guess.
|
|
33
|
+
*/
|
|
34
|
+
function detectFormat(input) {
|
|
35
|
+
const trimmed = input.trim();
|
|
36
|
+
// Try JSON first
|
|
37
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
38
|
+
try {
|
|
39
|
+
const data = JSON.parse(trimmed);
|
|
40
|
+
if (looksLikeMiroFish(data))
|
|
41
|
+
return "mirofish";
|
|
42
|
+
return "generic_json";
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Not valid JSON, fall through
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// CSV: lines with commas and a header row
|
|
49
|
+
if (looksLikeCsv(trimmed))
|
|
50
|
+
return "anylogic_csv";
|
|
51
|
+
// NetLogo: tick-based key:value or key-value pairs
|
|
52
|
+
if (looksLikeNetLogo(trimmed))
|
|
53
|
+
return "netlogo";
|
|
54
|
+
// Mesa: agent action logs
|
|
55
|
+
if (looksLikeMesa(trimmed))
|
|
56
|
+
return "mesa";
|
|
57
|
+
return "freeform_text";
|
|
58
|
+
}
|
|
59
|
+
function looksLikeMiroFish(data) {
|
|
60
|
+
if (typeof data !== "object" || data === null)
|
|
61
|
+
return false;
|
|
62
|
+
const d = data;
|
|
63
|
+
return ((Array.isArray(d.agents) || Array.isArray(d.fish) || Array.isArray(d.swarm)) &&
|
|
64
|
+
(d.emergent_behaviors !== undefined ||
|
|
65
|
+
d.emergent_dynamics !== undefined ||
|
|
66
|
+
d.timeline !== undefined ||
|
|
67
|
+
d.simulation !== undefined));
|
|
68
|
+
}
|
|
69
|
+
function looksLikeCsv(text) {
|
|
70
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
71
|
+
if (lines.length < 2)
|
|
72
|
+
return false;
|
|
73
|
+
const header = lines[0];
|
|
74
|
+
// Must have commas and look like a header
|
|
75
|
+
if (!header.includes(","))
|
|
76
|
+
return false;
|
|
77
|
+
const headerParts = header.split(",").map((h) => h.trim());
|
|
78
|
+
const dataLine = lines[1].split(",");
|
|
79
|
+
// Header should have words, data should have numbers
|
|
80
|
+
const headerHasWords = headerParts.some((h) => /^[a-zA-Z_]/.test(h));
|
|
81
|
+
const dataHasNumbers = dataLine.some((d) => !isNaN(Number(d.trim())));
|
|
82
|
+
return headerHasWords && dataHasNumbers && headerParts.length >= 2;
|
|
83
|
+
}
|
|
84
|
+
function looksLikeNetLogo(text) {
|
|
85
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
86
|
+
// NetLogo: "key: value" or "key value" format with ticks
|
|
87
|
+
const kvPattern = /^[\w-]+\s*:\s*.+$/;
|
|
88
|
+
const tickPattern = /tick|step|round|iteration/i;
|
|
89
|
+
const kvMatches = lines.filter((l) => kvPattern.test(l.trim())).length;
|
|
90
|
+
const hasTick = lines.some((l) => tickPattern.test(l));
|
|
91
|
+
return (kvMatches >= 3 && kvMatches >= lines.length * 0.5) || (kvMatches >= 2 && hasTick);
|
|
92
|
+
}
|
|
93
|
+
function looksLikeMesa(text) {
|
|
94
|
+
const lines = text.split("\n").filter((l) => l.trim());
|
|
95
|
+
// Mesa: "Agent N: action" format
|
|
96
|
+
const agentPattern = /^(?:agent|player|entity)\s+\d+\s*:\s*.+$/i;
|
|
97
|
+
const agentMatches = lines.filter((l) => agentPattern.test(l.trim())).length;
|
|
98
|
+
return agentMatches >= 2 && agentMatches >= lines.length * 0.3;
|
|
99
|
+
}
|
|
100
|
+
// ============================================
|
|
101
|
+
// PARSERS
|
|
102
|
+
// ============================================
|
|
103
|
+
/**
|
|
104
|
+
* Parse any simulation output into NormalizedSwarm.
|
|
105
|
+
* Auto-detects format and dispatches to the appropriate parser.
|
|
106
|
+
*/
|
|
107
|
+
function parseSimulation(input) {
|
|
108
|
+
const format = detectFormat(input);
|
|
109
|
+
switch (format) {
|
|
110
|
+
case "mirofish":
|
|
111
|
+
return parseMiroFish(input);
|
|
112
|
+
case "netlogo":
|
|
113
|
+
return parseNetLogo(input);
|
|
114
|
+
case "mesa":
|
|
115
|
+
return parseMesa(input);
|
|
116
|
+
case "anylogic_csv":
|
|
117
|
+
return parseCsv(input);
|
|
118
|
+
case "generic_json":
|
|
119
|
+
return parseGenericJson(input);
|
|
120
|
+
case "freeform_text":
|
|
121
|
+
return parseFreeformText(input);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ── MiroFish Parser ──────────────────────────────────
|
|
125
|
+
function parseMiroFish(input) {
|
|
126
|
+
const data = JSON.parse(input.trim());
|
|
127
|
+
const agents = [];
|
|
128
|
+
const events = [];
|
|
129
|
+
const stateChanges = [];
|
|
130
|
+
const metrics = [];
|
|
131
|
+
const timeline = [];
|
|
132
|
+
const coalitions = [];
|
|
133
|
+
// Parse agents
|
|
134
|
+
const rawAgents = data.agents || data.fish || data.swarm || [];
|
|
135
|
+
for (const a of rawAgents) {
|
|
136
|
+
agents.push({
|
|
137
|
+
id: a.id || a.name || a.type || `agent_${agents.length}`,
|
|
138
|
+
type: a.type || a.role || "agent",
|
|
139
|
+
state: a.state || a.status,
|
|
140
|
+
action: a.reaction || a.action || a.behavior,
|
|
141
|
+
attributes: extractAttributes(a, ["id", "name", "type", "role", "state", "status", "reaction", "action", "behavior"]),
|
|
142
|
+
});
|
|
143
|
+
if (a.reaction || a.action) {
|
|
144
|
+
events.push({
|
|
145
|
+
agents: [a.id || a.name || a.type],
|
|
146
|
+
action: a.reaction || a.action,
|
|
147
|
+
category: "reaction",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Parse emergent behaviors
|
|
152
|
+
const emergentBehaviors = [];
|
|
153
|
+
if (data.emergent_behaviors) {
|
|
154
|
+
for (const b of Array.isArray(data.emergent_behaviors) ? data.emergent_behaviors : [data.emergent_behaviors]) {
|
|
155
|
+
emergentBehaviors.push(typeof b === "string" ? b : JSON.stringify(b));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (data.emergent_dynamics) {
|
|
159
|
+
for (const d of Array.isArray(data.emergent_dynamics) ? data.emergent_dynamics : [data.emergent_dynamics]) {
|
|
160
|
+
emergentBehaviors.push(typeof d === "string" ? d : JSON.stringify(d));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// Parse timeline
|
|
164
|
+
if (data.timeline && Array.isArray(data.timeline)) {
|
|
165
|
+
for (const entry of data.timeline) {
|
|
166
|
+
if (typeof entry === "object") {
|
|
167
|
+
const { time, step, round, tick, ...rest } = entry;
|
|
168
|
+
timeline.push({
|
|
169
|
+
time: time ?? step ?? round ?? tick ?? timeline.length,
|
|
170
|
+
state: rest,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// Parse steps (MiroFish simulation output)
|
|
176
|
+
if (data.steps && Array.isArray(data.steps)) {
|
|
177
|
+
for (const step of data.steps) {
|
|
178
|
+
const t = step.step ?? step.round ?? timeline.length;
|
|
179
|
+
if (step.actions && Array.isArray(step.actions)) {
|
|
180
|
+
for (const action of step.actions) {
|
|
181
|
+
events.push({
|
|
182
|
+
time: t,
|
|
183
|
+
agents: action.agent ? [action.agent] : undefined,
|
|
184
|
+
action: action.action || action.type || JSON.stringify(action),
|
|
185
|
+
effect: action.result || action.effect,
|
|
186
|
+
category: "action",
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// Extract metrics from any numeric top-level fields
|
|
193
|
+
for (const [key, value] of Object.entries(data)) {
|
|
194
|
+
if (typeof value === "number" && !["version", "seed", "id"].includes(key)) {
|
|
195
|
+
metrics.push({ name: humanize(key), value });
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
sourceFormat: "mirofish",
|
|
200
|
+
agents,
|
|
201
|
+
events,
|
|
202
|
+
stateChanges,
|
|
203
|
+
coalitions,
|
|
204
|
+
metrics,
|
|
205
|
+
timeline,
|
|
206
|
+
emergentBehaviors: emergentBehaviors.length > 0 ? emergentBehaviors : undefined,
|
|
207
|
+
summary: data.summary || data.description,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// ── NetLogo Parser ───────────────────────────────────
|
|
211
|
+
function parseNetLogo(input) {
|
|
212
|
+
const lines = input.split("\n").filter((l) => l.trim());
|
|
213
|
+
const metrics = [];
|
|
214
|
+
const stateChanges = [];
|
|
215
|
+
const agents = [];
|
|
216
|
+
let currentTick;
|
|
217
|
+
for (const line of lines) {
|
|
218
|
+
const trimmed = line.trim();
|
|
219
|
+
// Check for tick/step indicator
|
|
220
|
+
const tickMatch = trimmed.match(/(?:tick|step|round|iteration)\s*[:#=]?\s*(\d+)/i);
|
|
221
|
+
if (tickMatch) {
|
|
222
|
+
currentTick = parseInt(tickMatch[1]);
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
// Parse key: value pairs
|
|
226
|
+
const kvMatch = trimmed.match(/^([\w-]+)\s*:\s*(.+)$/);
|
|
227
|
+
if (kvMatch) {
|
|
228
|
+
const key = kvMatch[1].trim();
|
|
229
|
+
const rawValue = kvMatch[2].trim();
|
|
230
|
+
// Try to parse as number
|
|
231
|
+
const numValue = parseFloat(rawValue.replace("%", ""));
|
|
232
|
+
if (!isNaN(numValue)) {
|
|
233
|
+
const isPercent = rawValue.includes("%");
|
|
234
|
+
metrics.push({
|
|
235
|
+
name: humanize(key),
|
|
236
|
+
value: isPercent ? numValue / 100 : numValue,
|
|
237
|
+
unit: isPercent ? "%" : undefined,
|
|
238
|
+
});
|
|
239
|
+
stateChanges.push({
|
|
240
|
+
variable: key,
|
|
241
|
+
to: isPercent ? numValue / 100 : numValue,
|
|
242
|
+
time: currentTick,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
// Treat as agent or categorical state
|
|
247
|
+
agents.push({
|
|
248
|
+
id: key,
|
|
249
|
+
type: "entity",
|
|
250
|
+
state: rawValue,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
// Parse "key value" format (space-separated)
|
|
256
|
+
const spaceKv = trimmed.match(/^([\w-]+)\s+([-\d.]+%?)$/);
|
|
257
|
+
if (spaceKv) {
|
|
258
|
+
const numVal = parseFloat(spaceKv[2].replace("%", ""));
|
|
259
|
+
if (!isNaN(numVal)) {
|
|
260
|
+
metrics.push({
|
|
261
|
+
name: humanize(spaceKv[1]),
|
|
262
|
+
value: spaceKv[2].includes("%") ? numVal / 100 : numVal,
|
|
263
|
+
unit: spaceKv[2].includes("%") ? "%" : undefined,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// Infer agents from metric names that suggest agent types
|
|
269
|
+
const agentPatterns = /consumer|producer|trader|voter|citizen|firm|bank|government|regulator/i;
|
|
270
|
+
for (const m of metrics) {
|
|
271
|
+
const match = m.name.match(agentPatterns);
|
|
272
|
+
if (match && !agents.some((a) => a.type === match[0].toLowerCase())) {
|
|
273
|
+
agents.push({
|
|
274
|
+
id: match[0].toLowerCase(),
|
|
275
|
+
type: match[0].toLowerCase(),
|
|
276
|
+
attributes: { [m.name]: m.value },
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
sourceFormat: "netlogo",
|
|
282
|
+
agents,
|
|
283
|
+
events: [],
|
|
284
|
+
stateChanges,
|
|
285
|
+
coalitions: [],
|
|
286
|
+
metrics,
|
|
287
|
+
timeline: currentTick !== undefined
|
|
288
|
+
? [{ time: currentTick, state: Object.fromEntries(metrics.map((m) => [m.name, m.value])) }]
|
|
289
|
+
: [],
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
// ── Mesa Parser ──────────────────────────────────────
|
|
293
|
+
function parseMesa(input) {
|
|
294
|
+
const lines = input.split("\n").filter((l) => l.trim());
|
|
295
|
+
const agents = [];
|
|
296
|
+
const events = [];
|
|
297
|
+
const agentMap = new Map();
|
|
298
|
+
for (const line of lines) {
|
|
299
|
+
const trimmed = line.trim();
|
|
300
|
+
// "Agent N: action" format
|
|
301
|
+
const agentMatch = trimmed.match(/^(?:agent|player|entity)\s+(\w+)\s*:\s*(.+)$/i);
|
|
302
|
+
if (agentMatch) {
|
|
303
|
+
const agentId = agentMatch[1];
|
|
304
|
+
const action = agentMatch[2].trim();
|
|
305
|
+
if (!agentMap.has(agentId)) {
|
|
306
|
+
const agent = {
|
|
307
|
+
id: `agent_${agentId}`,
|
|
308
|
+
type: "agent",
|
|
309
|
+
action,
|
|
310
|
+
};
|
|
311
|
+
agentMap.set(agentId, agent);
|
|
312
|
+
agents.push(agent);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
agentMap.get(agentId).action = action;
|
|
316
|
+
}
|
|
317
|
+
events.push({
|
|
318
|
+
agents: [`agent_${agentId}`],
|
|
319
|
+
action,
|
|
320
|
+
category: "action",
|
|
321
|
+
});
|
|
322
|
+
continue;
|
|
323
|
+
}
|
|
324
|
+
// "Name: action" format (capitalized names)
|
|
325
|
+
const namedMatch = trimmed.match(/^([A-Z][\w\s]+?)\s*:\s*(.+)$/);
|
|
326
|
+
if (namedMatch) {
|
|
327
|
+
const agentId = namedMatch[1].trim();
|
|
328
|
+
const action = namedMatch[2].trim();
|
|
329
|
+
if (!agentMap.has(agentId)) {
|
|
330
|
+
const agent = {
|
|
331
|
+
id: agentId,
|
|
332
|
+
type: inferAgentType(agentId),
|
|
333
|
+
action,
|
|
334
|
+
};
|
|
335
|
+
agentMap.set(agentId, agent);
|
|
336
|
+
agents.push(agent);
|
|
337
|
+
}
|
|
338
|
+
events.push({
|
|
339
|
+
agents: [agentId],
|
|
340
|
+
action,
|
|
341
|
+
category: "action",
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
sourceFormat: "mesa",
|
|
347
|
+
agents,
|
|
348
|
+
events,
|
|
349
|
+
stateChanges: [],
|
|
350
|
+
coalitions: [],
|
|
351
|
+
metrics: [],
|
|
352
|
+
timeline: [],
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
// ── CSV / AnyLogic Parser ────────────────────────────
|
|
356
|
+
function parseCsv(input) {
|
|
357
|
+
const lines = input.split("\n").filter((l) => l.trim());
|
|
358
|
+
if (lines.length < 2) {
|
|
359
|
+
return emptySwarm("anylogic_csv");
|
|
360
|
+
}
|
|
361
|
+
const headers = lines[0].split(",").map((h) => h.trim().replace(/^["']|["']$/g, ""));
|
|
362
|
+
const timeline = [];
|
|
363
|
+
const metrics = [];
|
|
364
|
+
const metricSeries = new Map();
|
|
365
|
+
// Find time column
|
|
366
|
+
const timeIdx = headers.findIndex((h) => /^(time|tick|step|round|t|timestamp|period|year|month|day|date)$/i.test(h));
|
|
367
|
+
for (let i = 1; i < lines.length; i++) {
|
|
368
|
+
const cols = lines[i].split(",").map((c) => c.trim().replace(/^["']|["']$/g, ""));
|
|
369
|
+
if (cols.length < headers.length)
|
|
370
|
+
continue;
|
|
371
|
+
const time = timeIdx >= 0 ? (isNaN(Number(cols[timeIdx])) ? cols[timeIdx] : Number(cols[timeIdx])) : i;
|
|
372
|
+
const state = {};
|
|
373
|
+
for (let j = 0; j < headers.length; j++) {
|
|
374
|
+
if (j === timeIdx)
|
|
375
|
+
continue;
|
|
376
|
+
const val = cols[j];
|
|
377
|
+
const numVal = Number(val);
|
|
378
|
+
state[headers[j]] = isNaN(numVal) ? val : numVal;
|
|
379
|
+
// Build time series for each numeric column
|
|
380
|
+
if (!isNaN(numVal)) {
|
|
381
|
+
if (!metricSeries.has(headers[j]))
|
|
382
|
+
metricSeries.set(headers[j], []);
|
|
383
|
+
metricSeries.get(headers[j]).push({ time, value: numVal });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
timeline.push({ time, state });
|
|
387
|
+
}
|
|
388
|
+
// Build metrics from the last row + full series
|
|
389
|
+
for (const [name, series] of metricSeries) {
|
|
390
|
+
const lastValue = series[series.length - 1].value;
|
|
391
|
+
const firstValue = series[0].value;
|
|
392
|
+
const trend = series.length < 2 ? "stable" :
|
|
393
|
+
lastValue > firstValue * 1.05 ? "rising" :
|
|
394
|
+
lastValue < firstValue * 0.95 ? "falling" :
|
|
395
|
+
"stable";
|
|
396
|
+
metrics.push({
|
|
397
|
+
name: humanize(name),
|
|
398
|
+
value: lastValue,
|
|
399
|
+
trend,
|
|
400
|
+
series,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// Infer agents from column names
|
|
404
|
+
const agents = [];
|
|
405
|
+
const agentPatterns = /consumer|producer|trader|voter|citizen|firm|bank|government|regulator|price|supply|demand/i;
|
|
406
|
+
for (const header of headers) {
|
|
407
|
+
const match = header.match(agentPatterns);
|
|
408
|
+
if (match) {
|
|
409
|
+
const lastState = timeline[timeline.length - 1]?.state;
|
|
410
|
+
agents.push({
|
|
411
|
+
id: header,
|
|
412
|
+
type: inferAgentType(header),
|
|
413
|
+
attributes: lastState ? { value: lastState[header] } : undefined,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return {
|
|
418
|
+
sourceFormat: "anylogic_csv",
|
|
419
|
+
agents,
|
|
420
|
+
events: [],
|
|
421
|
+
stateChanges: [],
|
|
422
|
+
coalitions: [],
|
|
423
|
+
metrics,
|
|
424
|
+
timeline,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
// ── Generic JSON Parser ──────────────────────────────
|
|
428
|
+
function parseGenericJson(input) {
|
|
429
|
+
const data = JSON.parse(input.trim());
|
|
430
|
+
const agents = [];
|
|
431
|
+
const events = [];
|
|
432
|
+
const metrics = [];
|
|
433
|
+
const stateChanges = [];
|
|
434
|
+
const timeline = [];
|
|
435
|
+
const coalitions = [];
|
|
436
|
+
// Recursively extract known structures
|
|
437
|
+
function walk(obj, path = "") {
|
|
438
|
+
if (Array.isArray(obj)) {
|
|
439
|
+
// Array of agent-like objects
|
|
440
|
+
if (obj.length > 0 && typeof obj[0] === "object" && obj[0] !== null) {
|
|
441
|
+
const first = obj[0];
|
|
442
|
+
if (first.id || first.name || first.agent || first.type) {
|
|
443
|
+
for (const item of obj) {
|
|
444
|
+
const o = item;
|
|
445
|
+
agents.push({
|
|
446
|
+
id: String(o.id || o.name || o.agent || `${path}_${agents.length}`),
|
|
447
|
+
type: String(o.type || o.role || o.class || "agent"),
|
|
448
|
+
state: o.state ? String(o.state) : undefined,
|
|
449
|
+
action: o.action ? String(o.action) : o.reaction ? String(o.reaction) : undefined,
|
|
450
|
+
attributes: extractAttributes(o, ["id", "name", "agent", "type", "role", "class", "state", "action", "reaction"]),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (first.time || first.step || first.tick) {
|
|
456
|
+
for (const item of obj) {
|
|
457
|
+
const o = item;
|
|
458
|
+
const t = o.time ?? o.step ?? o.tick ?? timeline.length;
|
|
459
|
+
const state = {};
|
|
460
|
+
for (const [k, v] of Object.entries(o)) {
|
|
461
|
+
if (["time", "step", "tick"].includes(k))
|
|
462
|
+
continue;
|
|
463
|
+
if (typeof v === "number" || typeof v === "string" || typeof v === "boolean") {
|
|
464
|
+
state[k] = v;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
timeline.push({ time: t, state });
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (first.action || first.event) {
|
|
472
|
+
for (const item of obj) {
|
|
473
|
+
const o = item;
|
|
474
|
+
events.push({
|
|
475
|
+
agents: o.agent ? [String(o.agent)] : o.agents ? o.agents : undefined,
|
|
476
|
+
action: String(o.action || o.event || o.type || "unknown"),
|
|
477
|
+
effect: o.effect ? String(o.effect) : o.result ? String(o.result) : undefined,
|
|
478
|
+
time: (o.time ?? o.step),
|
|
479
|
+
category: "action",
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
else if (typeof obj === "object" && obj !== null) {
|
|
487
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
488
|
+
if (typeof value === "number") {
|
|
489
|
+
metrics.push({ name: humanize(key), value });
|
|
490
|
+
}
|
|
491
|
+
else if (typeof value === "object") {
|
|
492
|
+
walk(value, path ? `${path}.${key}` : key);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (typeof data === "object" && data !== null) {
|
|
498
|
+
// Handle top-level known arrays
|
|
499
|
+
const d = data;
|
|
500
|
+
if (d.agents)
|
|
501
|
+
walk(d.agents, "agents");
|
|
502
|
+
if (d.events)
|
|
503
|
+
walk(d.events, "events");
|
|
504
|
+
if (d.timeline)
|
|
505
|
+
walk(d.timeline, "timeline");
|
|
506
|
+
if (d.coalitions && Array.isArray(d.coalitions)) {
|
|
507
|
+
for (const c of d.coalitions) {
|
|
508
|
+
const co = c;
|
|
509
|
+
coalitions.push({
|
|
510
|
+
name: String(co.name || `coalition_${coalitions.length}`),
|
|
511
|
+
members: Array.isArray(co.members) ? co.members.map(String) : [],
|
|
512
|
+
objective: co.objective ? String(co.objective) : undefined,
|
|
513
|
+
strength: typeof co.strength === "number" ? co.strength : undefined,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
// Walk remaining fields
|
|
518
|
+
for (const [key, value] of Object.entries(d)) {
|
|
519
|
+
if (["agents", "events", "timeline", "coalitions"].includes(key))
|
|
520
|
+
continue;
|
|
521
|
+
if (typeof value === "number") {
|
|
522
|
+
metrics.push({ name: humanize(key), value });
|
|
523
|
+
}
|
|
524
|
+
else if (typeof value === "object" && value !== null) {
|
|
525
|
+
walk(value, key);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
sourceFormat: "generic_json",
|
|
531
|
+
agents,
|
|
532
|
+
events,
|
|
533
|
+
stateChanges,
|
|
534
|
+
coalitions,
|
|
535
|
+
metrics,
|
|
536
|
+
timeline,
|
|
537
|
+
summary: typeof data.summary === "string"
|
|
538
|
+
? data.summary
|
|
539
|
+
: undefined,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
// ── Freeform Text Parser ─────────────────────────────
|
|
543
|
+
function parseFreeformText(input) {
|
|
544
|
+
const text = input.trim();
|
|
545
|
+
const agents = [];
|
|
546
|
+
const events = [];
|
|
547
|
+
const metrics = [];
|
|
548
|
+
// Extract agent-like names
|
|
549
|
+
const agentPatterns = [
|
|
550
|
+
/(?:agent|player|actor|entity|stakeholder)\s*[:#]?\s*["']?(\w[\w\s]{1,25}?)["']?(?:\n|,|;|\.|$)/gi,
|
|
551
|
+
/^[-•*]\s*(\w[\w\s]{1,20}?)(?:\s*[-:—])/gm,
|
|
552
|
+
];
|
|
553
|
+
const seenAgents = new Set();
|
|
554
|
+
for (const pattern of agentPatterns) {
|
|
555
|
+
let match;
|
|
556
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
557
|
+
const name = match[1].trim();
|
|
558
|
+
if (name.length > 1 && name.length < 25 && !isStopWord(name) && !seenAgents.has(name.toLowerCase())) {
|
|
559
|
+
seenAgents.add(name.toLowerCase());
|
|
560
|
+
agents.push({
|
|
561
|
+
id: name,
|
|
562
|
+
type: inferAgentType(name),
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
// Extract actions from "Name: action" or "Name does X" patterns
|
|
568
|
+
const actionPattern = /(\w[\w\s]{1,20}?)\s*(?::|—)\s*(.+?)(?:\n|$)/g;
|
|
569
|
+
let actionMatch;
|
|
570
|
+
while ((actionMatch = actionPattern.exec(text)) !== null) {
|
|
571
|
+
const agentName = actionMatch[1].trim();
|
|
572
|
+
const action = actionMatch[2].trim();
|
|
573
|
+
if (!isStopWord(agentName) && action.length > 2) {
|
|
574
|
+
events.push({
|
|
575
|
+
agents: [agentName],
|
|
576
|
+
action,
|
|
577
|
+
category: "action",
|
|
578
|
+
});
|
|
579
|
+
if (!seenAgents.has(agentName.toLowerCase())) {
|
|
580
|
+
seenAgents.add(agentName.toLowerCase());
|
|
581
|
+
agents.push({ id: agentName, type: inferAgentType(agentName), action });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// Extract numbers as metrics
|
|
586
|
+
const numberPattern = /(\w[\w\s]{1,30}?)\s*(?:=|:)\s*([-\d.]+%?)/g;
|
|
587
|
+
let numMatch;
|
|
588
|
+
while ((numMatch = numberPattern.exec(text)) !== null) {
|
|
589
|
+
const name = numMatch[1].trim();
|
|
590
|
+
const rawVal = numMatch[2];
|
|
591
|
+
const val = parseFloat(rawVal.replace("%", ""));
|
|
592
|
+
if (!isNaN(val) && name.length > 1) {
|
|
593
|
+
metrics.push({
|
|
594
|
+
name: humanize(name),
|
|
595
|
+
value: rawVal.includes("%") ? val / 100 : val,
|
|
596
|
+
unit: rawVal.includes("%") ? "%" : undefined,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
sourceFormat: "freeform_text",
|
|
602
|
+
agents,
|
|
603
|
+
events,
|
|
604
|
+
stateChanges: [],
|
|
605
|
+
coalitions: [],
|
|
606
|
+
metrics,
|
|
607
|
+
timeline: [],
|
|
608
|
+
summary: text.length > 200 ? text.slice(0, 200) + "..." : text,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
// ============================================
|
|
612
|
+
// HELPERS
|
|
613
|
+
// ============================================
|
|
614
|
+
function emptySwarm(format) {
|
|
615
|
+
return {
|
|
616
|
+
sourceFormat: format,
|
|
617
|
+
agents: [],
|
|
618
|
+
events: [],
|
|
619
|
+
stateChanges: [],
|
|
620
|
+
coalitions: [],
|
|
621
|
+
metrics: [],
|
|
622
|
+
timeline: [],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
function humanize(key) {
|
|
626
|
+
return key
|
|
627
|
+
.replace(/[-_]/g, " ")
|
|
628
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
629
|
+
.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
630
|
+
.trim();
|
|
631
|
+
}
|
|
632
|
+
function inferAgentType(name) {
|
|
633
|
+
const lower = name.toLowerCase();
|
|
634
|
+
if (/consumer|buyer|customer|shopper/.test(lower))
|
|
635
|
+
return "consumer";
|
|
636
|
+
if (/producer|supplier|manufacturer|seller/.test(lower))
|
|
637
|
+
return "producer";
|
|
638
|
+
if (/government|regulator|fed|agency|authority/.test(lower))
|
|
639
|
+
return "regulator";
|
|
640
|
+
if (/firm|company|corp|bank|enterprise/.test(lower))
|
|
641
|
+
return "firm";
|
|
642
|
+
if (/trader|investor|speculator|market/.test(lower))
|
|
643
|
+
return "trader";
|
|
644
|
+
if (/citizen|voter|public|population/.test(lower))
|
|
645
|
+
return "citizen";
|
|
646
|
+
if (/politician|senator|president|leader/.test(lower))
|
|
647
|
+
return "politician";
|
|
648
|
+
if (/media|press|journalist/.test(lower))
|
|
649
|
+
return "media";
|
|
650
|
+
if (/military|army|defense|navy/.test(lower))
|
|
651
|
+
return "military";
|
|
652
|
+
return "agent";
|
|
653
|
+
}
|
|
654
|
+
function isStopWord(word) {
|
|
655
|
+
const stops = new Set([
|
|
656
|
+
"the", "and", "for", "with", "from", "that", "this", "then",
|
|
657
|
+
"but", "not", "are", "was", "were", "has", "have", "had",
|
|
658
|
+
"will", "would", "could", "should", "may", "might",
|
|
659
|
+
"step", "round", "tick", "time", "result", "output", "data",
|
|
660
|
+
]);
|
|
661
|
+
return stops.has(word.toLowerCase());
|
|
662
|
+
}
|
|
663
|
+
function extractAttributes(obj, exclude) {
|
|
664
|
+
const attrs = {};
|
|
665
|
+
let count = 0;
|
|
666
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
667
|
+
if (exclude.includes(key))
|
|
668
|
+
continue;
|
|
669
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
670
|
+
attrs[key] = value;
|
|
671
|
+
count++;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return count > 0 ? attrs : undefined;
|
|
675
|
+
}
|
|
676
|
+
// ============================================
|
|
677
|
+
// FORMAT LABELS (for UI)
|
|
678
|
+
// ============================================
|
|
679
|
+
exports.FORMAT_LABELS = {
|
|
680
|
+
mirofish: "MiroFish",
|
|
681
|
+
netlogo: "NetLogo",
|
|
682
|
+
mesa: "Mesa (Python)",
|
|
683
|
+
anylogic_csv: "AnyLogic / CSV",
|
|
684
|
+
generic_json: "JSON",
|
|
685
|
+
freeform_text: "Text",
|
|
686
|
+
};
|