@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.
Files changed (52) hide show
  1. package/README.md +376 -66
  2. package/dist/adapters/mirofish.js +461 -0
  3. package/dist/adapters/scienceclaw.js +750 -0
  4. package/dist/assets/index-CHmUN8s0.js +532 -0
  5. package/dist/assets/index-DWgMnB7I.css +1 -0
  6. package/dist/assets/mirotir-logo-DUexumBH.svg +185 -0
  7. package/dist/assets/reportEngine-BVdQ2_nW.js +1 -0
  8. package/dist/components/ConstraintsPanel.js +11 -0
  9. package/dist/components/StakeholderBuilder.js +32 -0
  10. package/dist/components/ui/badge.js +24 -0
  11. package/dist/components/ui/button.js +70 -0
  12. package/dist/components/ui/card.js +57 -0
  13. package/dist/components/ui/input.js +44 -0
  14. package/dist/components/ui/label.js +45 -0
  15. package/dist/components/ui/select.js +70 -0
  16. package/dist/engine/aiProvider.js +681 -0
  17. package/dist/engine/auditTrace.js +352 -0
  18. package/dist/engine/behavioralAnalysis.js +605 -0
  19. package/dist/engine/cli.js +1408 -299
  20. package/dist/engine/dynamicsGovernance.js +588 -0
  21. package/dist/engine/fullGovernedLoop.js +367 -0
  22. package/dist/engine/governance.js +8 -3
  23. package/dist/engine/governedSimulation.js +114 -17
  24. package/dist/engine/index.js +56 -1
  25. package/dist/engine/liveAdapter.js +342 -0
  26. package/dist/engine/liveVisualizer.js +3063 -0
  27. package/dist/engine/metrics/science.metrics.js +335 -0
  28. package/dist/engine/narrativeInjection.js +305 -0
  29. package/dist/engine/policyEnforcement.js +1611 -0
  30. package/dist/engine/policyEngine.js +799 -0
  31. package/dist/engine/primeRadiant.js +540 -0
  32. package/dist/engine/reasoningEngine.js +57 -3
  33. package/dist/engine/reportEngine.js +97 -0
  34. package/dist/engine/scenarioComparison.js +463 -0
  35. package/dist/engine/scenarioLibrary.js +231 -0
  36. package/dist/engine/swarmSimulation.js +54 -1
  37. package/dist/engine/worldComparison.js +358 -0
  38. package/dist/engine/worldStorage.js +232 -0
  39. package/dist/favicon.ico +0 -0
  40. package/dist/index.html +23 -0
  41. package/dist/lib/reasoningEngine.js +290 -0
  42. package/dist/lib/simulationAdapter.js +686 -0
  43. package/dist/lib/swarmParser.js +291 -0
  44. package/dist/lib/types.js +2 -0
  45. package/dist/lib/utils.js +8 -0
  46. package/dist/placeholder.svg +1 -0
  47. package/dist/robots.txt +14 -0
  48. package/dist/runtime/govern.js +473 -0
  49. package/dist/runtime/index.js +75 -0
  50. package/dist/runtime/types.js +11 -0
  51. package/package.json +17 -12
  52. 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
+ };