@neuroverseos/nv-sim 0.1.6 → 0.1.7

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Change the rules. See why the system changed.**
4
4
 
5
- NV-SIM doesn't predict outcomes — it shows how they change when you change the rules.
5
+ NV-SIM doesn't predict outcomes — it shows how systems change when you change the rules.
6
6
 
7
7
  Define a world. Set the constraints. Run the agents. Then change one rule and watch the entire system reorganize. Not in theory. You see exactly which agents shifted, what patterns emerged, and why the outcome changed.
8
8
 
@@ -33,6 +33,38 @@ Rule changed
33
33
 
34
34
  That's the loop. Every time.
35
35
 
36
+ ## This Is a Runtime, Not Just a Simulator
37
+
38
+ NV-SIM runs in two modes:
39
+
40
+ - **Simulate** — explore how systems behave under different rules
41
+ - **Act** — govern real agents, real workflows, and real decisions
42
+
43
+ The interface is the same. The difference is where the behavior comes from.
44
+
45
+ ```
46
+ Simulate → internal swarm engine
47
+ Act → external systems (agents, APIs, frameworks like OpenClaw)
48
+ ```
49
+
50
+ In both cases:
51
+
52
+ ```
53
+ Action → Governance → Decision → Behavioral Shift → System Outcome
54
+ ```
55
+
56
+ You're not switching tools. You're switching the source of reality.
57
+
58
+ That's what makes this a runtime.
59
+
60
+ The same world file can:
61
+
62
+ - simulate a crisis
63
+ - govern a live system
64
+ - produce comparable outcomes across both
65
+
66
+ Design the rules once. Run them anywhere.
67
+
36
68
  ## The Demo Moment
37
69
 
38
70
  **Before:**
@@ -61,9 +93,61 @@ system: unstable
61
93
 
62
94
  You didn't predict this. You caused it — by changing one rule — and watched the system tell you why.
63
95
 
96
+ ## Behavioral Analysis — The Proof Layer
97
+
98
+ Blocking actions is easy. The hard part is proving what changed and why. NV-SIM doesn't just count blocked actions — it tracks how agents actually reorganized.
99
+
100
+ Every simulation produces behavioral evidence:
101
+
102
+ - **Action classification** — each agent action is categorized as aggressive, defensive, cautious, cooperative, opportunistic, or neutral
103
+ - **Agent trajectories** — traces each agent's behavior across rounds, showing when and how they shifted
104
+ - **Behavioral shifts** — detects the moment agents change strategy in response to governance
105
+ - **Cross-run comparison** — same agents under different rules, measured side by side
106
+
107
+ ```
108
+ BEHAVIORAL ANALYSIS
109
+
110
+ Action Distribution (governed):
111
+ aggressive: 12% (was 67%)
112
+ cooperative: 41% (was 8%)
113
+ cautious: 31% (was 11%)
114
+ defensive: 16% (was 14%)
115
+
116
+ Shifts Detected:
117
+ → 80% of aggressive agents shifted to cooperative after round 3
118
+ → Panic selling replaced by coordinated holding
119
+ → New pattern: quality_competition (not present in baseline)
120
+
121
+ Trajectory: agent_hedge_fund_1
122
+ Round 1: aggressive → Round 2: aggressive → Round 3: [BLOCK] → Round 4: cautious → Round 5: cooperative
123
+ ```
124
+
125
+ The governance isn't the insight. The behavioral shift is the insight. You changed a rule, and the system tells you exactly who changed, when they changed, and what they changed to.
126
+
127
+ ## Audit Trail — Full Evidence Chain
128
+
129
+ Every governance decision is recorded in an append-only audit log. Every rule firing, every agent action, every verdict — persistent and queryable.
130
+
131
+ ```
132
+ AUDIT TRAIL (session: 2026-03-18T14:22:00)
133
+
134
+ [GOVERNANCE] agent_hedge_fund_1 → panic_sell → BLOCK
135
+ rule: no_panic_selling (invariant)
136
+ evidence: action matches blocked pattern during high volatility
137
+
138
+ [AGENT] agent_hedge_fund_1 → hold
139
+ adapted: true (shifted from aggressive to defensive)
140
+
141
+ [BEHAVIORAL_SHIFT] agent_hedge_fund_1
142
+ before: aggressive | after: cautious
143
+ trigger: governance block at round 3
144
+ ```
145
+
146
+ Stored as JSONL — one JSON object per line, human-readable, pipeable through `jq`. No cloud, no deletion. Complete evidence for governance accountability.
147
+
64
148
  ## Worlds — Where the Power Lives
65
149
 
66
- Integration is one line. Worlds are where you define what your system allows.
150
+ Integration is one line. The world file controls everything.
67
151
 
68
152
  The `world` you pass to `/api/evaluate` determines how actions are judged. Change the world, change the outcome.
69
153
 
@@ -192,7 +276,35 @@ Adjust rules → Inject events → Run → See the shift → Save as variant
192
276
 
193
277
  Variants capture the base world, state overrides, narrative events, and results. Store them in git. Share them. Replay them. This turns experiments into assets.
194
278
 
195
- ## Works With Anything
279
+ ## Governance Runtime — Run It For Your Own System
280
+
281
+ ```bash
282
+ npx @neuroverseos/nv-sim serve
283
+ ```
284
+
285
+ This starts a local governance server. Any simulator, agent framework, or application can POST actions and get governance decisions back.
286
+
287
+ ```
288
+ Endpoint: http://localhost:3456/api/evaluate
289
+ Method: POST
290
+ Contract: { actor, action, payload?, state?, world? }
291
+ Response: { decision: ALLOW|BLOCK|MODIFY, reason, evidence }
292
+ ```
293
+
294
+ Your agents call localhost. The world file decides what's allowed. No cloud. No cost.
295
+
296
+ Additional endpoints:
297
+
298
+ | Endpoint | What It Does |
299
+ |----------|-------------|
300
+ | `POST /api/evaluate` | Submit an action for governance |
301
+ | `GET /api/session` | Current session stats |
302
+ | `GET /api/session/report` | Full governance report |
303
+ | `POST /api/session/reset` | Reset session state |
304
+ | `POST /api/session/save` | Save session as experiment |
305
+ | `GET /api/events` | SSE stream of governance events |
306
+
307
+ ### Works With Anything
196
308
 
197
309
  If your system has actions, you can govern it. One API call.
198
310
 
@@ -266,6 +378,44 @@ Most systems generate behavior. This one shapes it.
266
378
 
267
379
  See [INTEGRATION.md](./INTEGRATION.md) for the full API contract and decision types.
268
380
 
381
+ ## AI Providers — Bring Your Own Model
382
+
383
+ AI is optional. AI is governed. AI is pluggable.
384
+
385
+ NV-SIM works without any AI — the deterministic engine runs on math, not tokens. But when you bring your own model, AI becomes a governed actor inside the system — subject to the same rules as every other agent.
386
+
387
+ ### How AI fits in
388
+
389
+ AI plays two governed roles:
390
+
391
+ | Role | What It Does | Constraints |
392
+ |------|-------------|-------------|
393
+ | `ai_translator` | Converts unstructured input into normalized events | Must output valid schema, no invention of events, must include confidence |
394
+ | `ai_analyst` | Generates reports from simulation traces | Must reference trace data, must include blocked actions, no unverifiable claims |
395
+
396
+ Both roles go through `/api/evaluate` like any other actor. The AI doesn't control the system — the system controls the AI.
397
+
398
+ ### Supported providers
399
+
400
+ NV-SIM auto-detects the best available provider from your environment:
401
+
402
+ | Provider | Env Var | What It Connects To |
403
+ |----------|---------|---------------------|
404
+ | Anthropic (Claude) | `ANTHROPIC_API_KEY` | Claude Sonnet, Opus, Haiku |
405
+ | OpenAI | `OPENAI_API_KEY` | GPT-4, GPT-4o, o1 |
406
+ | Groq | `GROQ_API_KEY` | Llama 3 70B |
407
+ | Together | `TOGETHER_API_KEY` | Llama, Mixtral |
408
+ | Mistral | `MISTRAL_API_KEY` | Mistral Large |
409
+ | Deepseek | `DEEPSEEK_API_KEY` | Deepseek Chat |
410
+ | Fireworks | `FIREWORKS_API_KEY` | Llama, custom models |
411
+ | Ollama | `OLLAMA_BASE_URL` | Any local model |
412
+ | Local LLM | `LOCAL_LLM_URL` | LM Studio, vLLM, llama.cpp |
413
+ | (none) | — | Deterministic fallback (no AI, no cost) |
414
+
415
+ Set the env var and run. No configuration files. No provider lock-in.
416
+
417
+ Any endpoint that speaks the OpenAI chat completions format (`POST /v1/chat/completions`) works out of the box.
418
+
269
419
  ## Quick Start
270
420
 
271
421
  ```bash
@@ -398,21 +548,23 @@ For full control over gates, state variables, and thesis, use JSON world files.
398
548
  | `nv-sim worlds <a> <b>` | Compare two rule environments |
399
549
  | `nv-sim chaos [preset] --runs N` | Stress test across randomized scenarios |
400
550
  | `nv-sim serve --port N` | Local governance runtime for any simulator |
551
+ | `nv-sim run <simulator>` | Connect external simulator to governance |
401
552
  | `nv-sim analyze <file>` | Analyze simulation from file or stdin |
402
553
  | `nv-sim presets` | List available world presets |
403
554
 
404
555
  ## How It Works
405
556
 
406
557
  ```
407
- event → narrative propagation → belief shift → agent action → governance → outcome
558
+ event → narrative propagation → belief shift → agent action → governance → behavioral analysis → outcome
408
559
  ```
409
560
 
410
- Four forces shape every simulation:
561
+ Five forces shape every simulation:
411
562
 
412
563
  1. **Agent behavior** — traders, voters, regulators, media
413
564
  2. **World rules** — leverage caps, circuit breakers, chokepoints
414
565
  3. **Narrative events** — information shocks that propagate through the system
415
566
  4. **Perception propagation** — different agents react differently to the same event
567
+ 5. **Behavioral analysis** — tracks how agents reorganize under governance, producing the evidence that rules actually changed the system
416
568
 
417
569
  This lets you ask compound questions:
418
570
 
@@ -427,15 +579,23 @@ That combination produces very different outcomes than any single factor alone.
427
579
 
428
580
  nv-sim engine ← world rules + narrative injection + swarm simulation
429
581
 
430
- nv-sim CLI scenarios, comparison, chaos testing
582
+ behavioral analysis shift detection, trajectory tracking, cross-run comparison
583
+
584
+ audit trail ← append-only evidence chain (JSONL)
585
+
586
+ nv-sim CLI ← scenarios, comparison, chaos testing, governance runtime
431
587
 
432
588
  control platform ← interactive browser UI + System Shift card
433
589
 
590
+ AI providers (optional) ← BYOM: Anthropic, OpenAI, Groq, local LLMs, or none
591
+
434
592
  world variants ← saved experiments as shareable assets
435
593
  ```
436
594
 
437
595
  Everything runs locally. NV-SIM uses [`@neuroverseos/governance`](https://www.npmjs.com/package/@neuroverseos/governance) for deterministic guard evaluation — no LLM, no cloud, no cost. Your agents call `localhost`, and the world file decides what's allowed.
438
596
 
597
+ AI is optional. When present, it's governed — subject to the same rules as any other actor in the system.
598
+
439
599
  ## License
440
600
 
441
601
  Apache 2.0
@@ -958,6 +958,127 @@ function startInteractiveServer(port, onReady) {
958
958
  }
959
959
  return;
960
960
  }
961
+ // ── Clear Rules ──
962
+ // Wipes all custom guards and resets governance to base world rules
963
+ if (req.url === "/api/clear-rules" && req.method === "POST") {
964
+ customGuards.length = 0;
965
+ jsonResponse(res, 200, {
966
+ status: "cleared",
967
+ message: "All custom rules removed. Governance reset to base world rules.",
968
+ });
969
+ return;
970
+ }
971
+ // ── Load World File ──
972
+ // Accept a full world definition JSON and use it as the active world
973
+ if (req.url === "/api/load-world-file" && req.method === "POST") {
974
+ try {
975
+ const body = await readBody(req);
976
+ const payload = JSON.parse(body);
977
+ if (!payload.world) {
978
+ jsonResponse(res, 400, { error: "world object is required" });
979
+ return;
980
+ }
981
+ const w = payload.world;
982
+ // Parse any plain-English rules in the world file into guards
983
+ customGuards.length = 0;
984
+ if (w.rules && Array.isArray(w.rules)) {
985
+ for (let i = 0; i < w.rules.length; i++) {
986
+ const rule = w.rules[i];
987
+ if (rule.intent_patterns && rule.intent_patterns.length > 0) {
988
+ // Already structured rule
989
+ customGuards.push({
990
+ id: rule.id || `world-rule-${i}`,
991
+ label: rule.description,
992
+ description: rule.description,
993
+ category: "world-file",
994
+ enforcement: rule.enforcement || "block",
995
+ immutable: false,
996
+ intent_patterns: rule.intent_patterns,
997
+ default_enabled: true,
998
+ });
999
+ }
1000
+ else {
1001
+ // Plain-English rule — parse it
1002
+ const parsed = parseNaturalLanguageRule(rule.description, i);
1003
+ if (parsed) {
1004
+ customGuards.push({
1005
+ id: parsed.id,
1006
+ label: parsed.description,
1007
+ description: parsed.description,
1008
+ category: "world-file",
1009
+ enforcement: parsed.enforcement,
1010
+ immutable: false,
1011
+ intent_patterns: parsed.intent_patterns,
1012
+ default_enabled: true,
1013
+ });
1014
+ }
1015
+ }
1016
+ }
1017
+ }
1018
+ // Build the response world definition for the UI
1019
+ const loadedWorld = {
1020
+ id: "custom-world",
1021
+ title: w.name || "Custom World",
1022
+ thesis: w.thesis || "User-defined world",
1023
+ stateVariables: w.state_variables || [],
1024
+ invariants: (w.invariants || []).map(inv => ({
1025
+ id: inv.id,
1026
+ description: inv.description,
1027
+ enforceable: inv.enforceable !== false,
1028
+ })),
1029
+ gates: (w.gates || []).map(g => ({
1030
+ id: g.id,
1031
+ label: g.label,
1032
+ condition: g.condition,
1033
+ severity: g.severity || "warning",
1034
+ })),
1035
+ rulesApplied: customGuards.length,
1036
+ };
1037
+ jsonResponse(res, 200, {
1038
+ status: "loaded",
1039
+ world: loadedWorld,
1040
+ rulesApplied: customGuards.length,
1041
+ message: `World "${loadedWorld.title}" loaded with ${loadedWorld.invariants.length} invariants, ${loadedWorld.gates.length} gates, and ${customGuards.length} rules.`,
1042
+ });
1043
+ }
1044
+ catch (err) {
1045
+ jsonResponse(res, 400, { error: "Invalid world file JSON" });
1046
+ }
1047
+ return;
1048
+ }
1049
+ // ── Export World File ──
1050
+ // Exports the current world configuration (base world + custom rules + overrides) as a world file
1051
+ if (req.url === "/api/export-world" && req.method === "GET") {
1052
+ const { resolveWorld } = await Promise.resolve().then(() => __importStar(require("./worldComparison")));
1053
+ const worldId = currentSession.world || "trading";
1054
+ let baseWorld;
1055
+ try {
1056
+ baseWorld = resolveWorld(worldId);
1057
+ }
1058
+ catch {
1059
+ baseWorld = null;
1060
+ }
1061
+ const exportedWorld = {
1062
+ name: baseWorld?.title || worldId,
1063
+ thesis: baseWorld?.world?.thesis || "Exported world",
1064
+ state_variables: baseWorld?.world?.state_variables || [],
1065
+ invariants: baseWorld?.world?.invariants || [],
1066
+ gates: baseWorld?.world?.gates || [],
1067
+ rules: customGuards.map(g => ({
1068
+ id: g.id,
1069
+ description: g.description,
1070
+ enforcement: g.enforcement,
1071
+ intent_patterns: g.intent_patterns,
1072
+ })),
1073
+ };
1074
+ res.writeHead(200, {
1075
+ "Content-Type": "application/json",
1076
+ "Content-Disposition": `attachment; filename="${worldId}-world.json"`,
1077
+ "Access-Control-Allow-Origin": "*",
1078
+ });
1079
+ res.end(JSON.stringify({ world: exportedWorld }, null, 2));
1080
+ return;
1081
+ }
961
1082
  // ── Session Reporting Endpoints ──
962
1083
  // Connect the serve runtime to the enforce reporting pipeline.
963
1084
  // Users can request reports, stats, and recommendations from live governance data.
@@ -1687,13 +1808,70 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1687
1808
  .rule-status.error { color: var(--red); }
1688
1809
  .rule-examples { font-size: 10px; color: var(--text-faint); margin-top: 6px; line-height: 1.6; }
1689
1810
  .rule-examples code { background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; color: var(--text-secondary); }
1811
+
1812
+ /* World Action Bar */
1813
+ .world-action-bar { display: flex; gap: 4px; margin-bottom: 16px; flex-wrap: wrap; }
1814
+ .btn-world-action { flex: 1; min-width: 0; padding: 6px 4px; font-size: 10px; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border); border-radius: 4px; cursor: pointer; font-family: inherit; text-align: center; transition: all 0.2s; white-space: nowrap; }
1815
+ .btn-world-action:hover { border-color: var(--accent); color: var(--accent); }
1816
+ .btn-world-action.btn-export { color: var(--green); border-color: var(--green); opacity: 0.7; }
1817
+ .btn-world-action.btn-export:hover { opacity: 1; }
1818
+
1819
+ /* World Source Tabs */
1820
+ .world-source-tabs { display: flex; gap: 4px; }
1821
+ .ws-tab { flex: 1; display: flex; flex-direction: column; align-items: center; padding: 10px 6px; background: var(--bg-surface); border: 2px solid var(--border); border-radius: 6px; cursor: pointer; transition: all 0.2s; text-align: center; }
1822
+ .ws-tab:hover { border-color: var(--text-muted); }
1823
+ .ws-tab.active { border-color: var(--accent); background: var(--accent-bg); }
1824
+ .ws-tab input[type="radio"] { display: none; }
1825
+ .ws-label { font-size: 11px; font-weight: 700; color: var(--text-primary); }
1826
+ .ws-hint { font-size: 9px; color: var(--text-muted); margin-top: 2px; }
1827
+ .ws-tab.active .ws-label { color: var(--accent); }
1828
+
1829
+ /* World Source Panels */
1830
+ .world-source-panel { animation: fadeIn 0.2s ease; }
1831
+ @keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
1832
+
1833
+ /* Custom World Header */
1834
+ .custom-world-header { margin-bottom: 12px; }
1835
+ .world-name-input { width: 100%; background: var(--bg-surface); color: var(--text-primary); border: 1px solid var(--border-subtle); padding: 8px; border-radius: 4px; font-family: inherit; font-size: 13px; font-weight: 600; margin-bottom: 6px; }
1836
+ .world-name-input:focus { border-color: var(--accent); outline: none; }
1837
+ .world-thesis-input { width: 100%; background: var(--bg-surface); color: var(--text-secondary); border: 1px solid var(--border-subtle); padding: 6px 8px; border-radius: 4px; font-family: inherit; font-size: 11px; resize: none; height: 36px; }
1838
+ .world-thesis-input:focus { border-color: var(--accent); outline: none; }
1839
+
1840
+ /* Rule editor enhancements */
1841
+ .rule-editor-label { font-size: 11px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 600; }
1842
+ .rule-input-large { min-height: 120px; }
1843
+ .btn-generate-world { background: var(--accent); color: #fff; margin-top: 8px; padding: 10px; font-size: 12px; font-weight: 700; width: 100%; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
1844
+ .btn-generate-world:hover { filter: brightness(1.1); }
1845
+
1846
+ /* Upload Zone */
1847
+ .upload-zone { border: 2px dashed var(--border-subtle); border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s; margin-bottom: 12px; }
1848
+ .upload-zone:hover, .upload-zone.dragover { border-color: var(--accent); background: var(--accent-bg); }
1849
+ .upload-icon { font-size: 28px; margin-bottom: 8px; }
1850
+ .upload-label { font-size: 12px; color: var(--text-secondary); }
1851
+ .upload-or { font-size: 10px; color: var(--text-faint); margin: 8px 0; }
1852
+ .btn-upload-browse { background: var(--bg-elevated); color: var(--text-primary); border: 1px solid var(--border); padding: 6px 16px; border-radius: 4px; cursor: pointer; font-family: inherit; font-size: 11px; }
1853
+ .upload-paste-section { margin-bottom: 12px; }
1854
+ .btn-load-world { background: var(--green); color: #0a0a0a; width: 100%; padding: 10px; font-size: 12px; font-weight: 700; border: none; border-radius: 4px; cursor: pointer; font-family: inherit; transition: filter 0.2s; }
1855
+ .btn-load-world:hover { filter: brightness(0.9); }
1856
+
1857
+ /* Loaded World Card */
1858
+ .loaded-world-card { background: var(--green-bg); border: 1px solid var(--green); border-radius: 6px; padding: 12px; margin-top: 10px; }
1859
+ .lw-name { font-size: 13px; font-weight: 700; color: var(--green); }
1860
+ .lw-thesis { font-size: 11px; color: var(--text-secondary); margin-top: 4px; font-style: italic; }
1861
+ .lw-stats { font-size: 10px; color: var(--text-muted); margin-top: 6px; }
1862
+
1863
+ /* Schema Reference */
1864
+ .schema-ref { font-size: 10px; color: var(--text-muted); }
1865
+ .schema-item { padding: 3px 0; border-bottom: 1px solid var(--border); }
1866
+ .schema-item:last-child { border-bottom: none; }
1867
+ .schema-item code { color: var(--accent); background: var(--bg-surface); padding: 1px 4px; border-radius: 2px; }
1690
1868
  </style>
1691
1869
  </head>
1692
1870
  <body>
1693
1871
  <div class="header">
1694
1872
  <div style="display:flex;align-items:center">
1695
1873
  <h1>NV-SIM</h1>
1696
- <span class="sub">Scenario Control Platform</span>
1874
+ <span class="sub">Governance Runtime</span>
1697
1875
  </div>
1698
1876
  <div class="header-right">
1699
1877
  <button class="theme-toggle" id="theme-toggle" title="Toggle light/dark mode">Light Mode</button>
@@ -1704,55 +1882,148 @@ const INTERACTIVE_DASHBOARD_HTML = `<!DOCTYPE html>
1704
1882
  <div class="layout">
1705
1883
  <!-- LEFT: CONTROLS -->
1706
1884
  <div class="controls" id="controls-panel">
1707
- <!-- Simulation Engine selector -->
1885
+ <!-- World Action Bar -->
1886
+ <div class="world-action-bar">
1887
+ <button class="btn btn-world-action" id="new-world-btn" title="Clear everything and start fresh">+ New World</button>
1888
+ <button class="btn btn-world-action" id="load-file-btn" title="Load a .json world file">Load World File</button>
1889
+ <button class="btn btn-world-action" id="clear-rules-btn" title="Clear custom rules only">Clear Rules</button>
1890
+ <button class="btn btn-world-action btn-export" id="export-world-btn" title="Export current world as JSON">Save as World File</button>
1891
+ </div>
1892
+
1893
+ <!-- World Source selector -->
1708
1894
  <div class="ctrl-section">
1709
- <h3>Simulation Engine</h3>
1710
- <div class="ctrl-row">
1711
- <select id="engine-select">
1712
- <option value="nv-sim" selected>NV-SIM (Built-in)</option>
1713
- </select>
1895
+ <h3>World Source</h3>
1896
+ <div class="world-source-tabs">
1897
+ <label class="ws-tab active" data-source="preset">
1898
+ <input type="radio" name="world-source" value="preset" checked>
1899
+ <span class="ws-label">Preset</span>
1900
+ <span class="ws-hint">Demo scenarios</span>
1901
+ </label>
1902
+ <label class="ws-tab" data-source="custom">
1903
+ <input type="radio" name="world-source" value="custom">
1904
+ <span class="ws-label">Custom Rules</span>
1905
+ <span class="ws-hint">Define your world</span>
1906
+ </label>
1907
+ <label class="ws-tab" data-source="upload">
1908
+ <input type="radio" name="world-source" value="upload">
1909
+ <span class="ws-label">World File</span>
1910
+ <span class="ws-hint">JSON / .nv-world</span>
1911
+ </label>
1714
1912
  </div>
1715
- <div id="engine-status" style="font-size:10px;color:#555;margin-top:4px"></div>
1716
1913
  </div>
1717
1914
 
1718
- <!-- World selector -->
1719
- <div class="ctrl-section">
1720
- <h3>World</h3>
1721
- <div class="ctrl-row">
1722
- <select id="world-select"></select>
1915
+ <!-- SOURCE: Preset -->
1916
+ <div class="world-source-panel" id="source-preset">
1917
+ <div class="ctrl-section">
1918
+ <h3>World</h3>
1919
+ <div class="ctrl-row">
1920
+ <select id="world-select"></select>
1921
+ </div>
1922
+ <div id="world-thesis" class="world-thesis"></div>
1923
+ </div>
1924
+
1925
+ <!-- State variables (dynamic sliders) -->
1926
+ <div class="ctrl-section" id="state-vars-section" style="display:none">
1927
+ <h3>World Rules</h3>
1928
+ <div id="state-vars"></div>
1929
+ </div>
1930
+
1931
+ <!-- Scenario presets -->
1932
+ <div class="ctrl-section">
1933
+ <h3>Scenarios</h3>
1934
+ <div id="scenario-list"></div>
1723
1935
  </div>
1724
- <div id="world-thesis" class="world-thesis"></div>
1725
1936
  </div>
1726
1937
 
1727
- <!-- State variables (dynamic sliders) -->
1728
- <div class="ctrl-section" id="state-vars-section" style="display:none">
1729
- <h3>World Rules</h3>
1730
- <div id="state-vars"></div>
1938
+ <!-- SOURCE: Custom Rules (Define Your World) -->
1939
+ <div class="world-source-panel" id="source-custom" style="display:none">
1940
+ <div class="ctrl-section">
1941
+ <h3>Define Your World</h3>
1942
+ <div class="custom-world-header">
1943
+ <input type="text" class="world-name-input" id="custom-world-name" placeholder="World name (e.g. Marketing Governance)">
1944
+ <textarea class="world-thesis-input" id="custom-world-thesis" placeholder="What is this world about? (thesis)"></textarea>
1945
+ </div>
1946
+ <div class="rule-editor">
1947
+ <div class="rule-editor-label">Type your governance rules:</div>
1948
+ <textarea class="rule-input rule-input-large" id="rule-input" placeholder="No agent may spend more than $10k without approval&#10;All outbound emails must be reviewed&#10;Block deletion of production data&#10;Limit API calls to 100 per minute&#10;Require manager approval for refunds over $500"></textarea>
1949
+ <button class="btn btn-generate-world" id="parse-rules-btn">Generate World</button>
1950
+ <div id="parsed-rules" class="parsed-rules"></div>
1951
+ <div id="rule-status" class="rule-status"></div>
1952
+ <div class="rule-examples">
1953
+ Rule patterns:<br>
1954
+ <code>Block [action]</code> — hard suppression<br>
1955
+ <code>Limit [X] to [N]</code> — cap extremes<br>
1956
+ <code>Require [X] for [Y]</code> — structural constraint<br>
1957
+ <code>Pause [X] for review</code> — human-in-the-loop<br>
1958
+ <code>Allow [X]</code> — explicit permission<br>
1959
+ <code>Monitor [X]</code> — circuit breaker gate
1960
+ </div>
1961
+ </div>
1962
+ </div>
1963
+
1964
+ <!-- Base world (optional) -->
1965
+ <div class="ctrl-section">
1966
+ <h3>Base World (Optional)</h3>
1967
+ <div class="ctrl-row">
1968
+ <select id="custom-base-world">
1969
+ <option value="">None — start from scratch</option>
1970
+ </select>
1971
+ <div style="font-size:10px;color:var(--text-faint);margin-top:4px">Layer your rules on top of a preset world</div>
1972
+ </div>
1973
+ </div>
1731
1974
  </div>
1732
1975
 
1733
- <!-- Plain-English Rule Editor -->
1734
- <div class="ctrl-section">
1735
- <h3>Write Rules in Plain English</h3>
1736
- <div class="rule-editor">
1737
- <textarea class="rule-input" id="rule-input" placeholder="Type rules in plain English, one per line:&#10;&#10;Block panic selling during high volatility&#10;Limit leverage to 5x maximum&#10;Pause any trade over $10M for review"></textarea>
1738
- <button class="btn btn-parse" id="parse-rules-btn">Parse Rules</button>
1739
- <div id="parsed-rules" class="parsed-rules"></div>
1740
- <div id="rule-status" class="rule-status"></div>
1741
- <div class="rule-examples">
1742
- Examples:<br>
1743
- <code>Block panic selling</code><br>
1744
- <code>Limit leverage to 3x</code><br>
1745
- <code>Pause large trades for review</code><br>
1746
- <code>Allow hedging positions</code><br>
1747
- <code>Block short selling during circuit breaker</code>
1976
+ <!-- SOURCE: Upload World File -->
1977
+ <div class="world-source-panel" id="source-upload" style="display:none">
1978
+ <div class="ctrl-section">
1979
+ <h3>Load World File</h3>
1980
+ <div class="upload-zone" id="upload-zone">
1981
+ <div class="upload-icon">&#x1F4C4;</div>
1982
+ <div class="upload-label">Drop a .json or .nv-world file here</div>
1983
+ <div class="upload-or">or</div>
1984
+ <button class="btn btn-upload-browse" id="upload-browse-btn">Browse Files</button>
1985
+ <input type="file" id="upload-file-input" accept=".json,.nv-world" style="display:none">
1986
+ </div>
1987
+ <div class="upload-paste-section">
1988
+ <div class="rule-editor-label">Or paste world JSON:</div>
1989
+ <textarea class="rule-input rule-input-large" id="world-json-input" placeholder='{&#10; "name": "Marketing Governance",&#10; "thesis": "All marketing actions are governed",&#10; "invariants": [...],&#10; "rules": [...],&#10; "gates": [...]&#10;}'></textarea>
1990
+ </div>
1991
+ <button class="btn btn-load-world" id="load-world-btn">Load into Runtime</button>
1992
+ <div id="upload-status" class="rule-status"></div>
1993
+
1994
+ <!-- Loaded world info -->
1995
+ <div id="loaded-world-info" style="display:none">
1996
+ <div class="loaded-world-card">
1997
+ <div class="lw-name" id="lw-name"></div>
1998
+ <div class="lw-thesis" id="lw-thesis"></div>
1999
+ <div class="lw-stats" id="lw-stats"></div>
2000
+ </div>
2001
+ </div>
2002
+ </div>
2003
+
2004
+ <!-- World file schema reference -->
2005
+ <div class="ctrl-section">
2006
+ <h3>World File Schema</h3>
2007
+ <div class="schema-ref">
2008
+ <div class="schema-item"><code>name</code> — world name</div>
2009
+ <div class="schema-item"><code>thesis</code> — what this world is about</div>
2010
+ <div class="schema-item"><code>rules[]</code> — governance rules (plain English or structured)</div>
2011
+ <div class="schema-item"><code>invariants[]</code> — rules that always hold <code>{id, description}</code></div>
2012
+ <div class="schema-item"><code>gates[]</code> — viability thresholds <code>{id, label, condition, severity}</code></div>
2013
+ <div class="schema-item"><code>state_variables[]</code> — sliders <code>{id, label, type, range, default_value}</code></div>
1748
2014
  </div>
1749
2015
  </div>
1750
2016
  </div>
1751
2017
 
1752
- <!-- Scenario presets -->
1753
- <div class="ctrl-section">
1754
- <h3>Scenario Presets</h3>
1755
- <div id="scenario-list"></div>
2018
+ <!-- Simulation Engine (demoted, below world source) -->
2019
+ <div class="ctrl-section" style="margin-top:8px">
2020
+ <h3>Engine</h3>
2021
+ <div class="ctrl-row">
2022
+ <select id="engine-select">
2023
+ <option value="nv-sim" selected>NV-SIM (Built-in)</option>
2024
+ </select>
2025
+ </div>
2026
+ <div id="engine-status" style="font-size:10px;color:var(--text-faint);margin-top:4px"></div>
1756
2027
  </div>
1757
2028
 
1758
2029
  <!-- Narrative injection -->
@@ -2018,6 +2289,9 @@ async function init() {
2018
2289
  // Load saved variants
2019
2290
  await loadVariants();
2020
2291
 
2292
+ // Populate base world selector for custom rules mode
2293
+ populateBaseWorldSelect();
2294
+
2021
2295
  // Connect SSE
2022
2296
  connectSSE();
2023
2297
  }
@@ -2898,6 +3172,257 @@ themeToggleBtn.addEventListener('click', () => {
2898
3172
  const savedTheme = localStorage.getItem('nv-theme');
2899
3173
  if (savedTheme) applyTheme(savedTheme);
2900
3174
 
3175
+ // ============================================
3176
+ // WORLD SOURCE SWITCHING
3177
+ // ============================================
3178
+ let currentWorldSource = 'preset';
3179
+ const worldSourceTabs = document.querySelectorAll('.ws-tab');
3180
+ const sourcePresetPanel = document.getElementById('source-preset');
3181
+ const sourceCustomPanel = document.getElementById('source-custom');
3182
+ const sourceUploadPanel = document.getElementById('source-upload');
3183
+
3184
+ worldSourceTabs.forEach(tab => {
3185
+ tab.addEventListener('click', () => {
3186
+ const source = tab.dataset.source;
3187
+ if (source === currentWorldSource) return;
3188
+
3189
+ currentWorldSource = source;
3190
+
3191
+ // Update tab visuals
3192
+ worldSourceTabs.forEach(t => t.classList.remove('active'));
3193
+ tab.classList.add('active');
3194
+ tab.querySelector('input').checked = true;
3195
+
3196
+ // Show/hide panels
3197
+ sourcePresetPanel.style.display = source === 'preset' ? '' : 'none';
3198
+ sourceCustomPanel.style.display = source === 'custom' ? '' : 'none';
3199
+ sourceUploadPanel.style.display = source === 'upload' ? '' : 'none';
3200
+ });
3201
+ });
3202
+
3203
+ // Populate base world selector in custom rules panel
3204
+ function populateBaseWorldSelect() {
3205
+ const select = document.getElementById('custom-base-world');
3206
+ if (!select) return;
3207
+ worlds.forEach(w => {
3208
+ const opt = document.createElement('option');
3209
+ opt.value = w.id;
3210
+ opt.textContent = w.title;
3211
+ select.appendChild(opt);
3212
+ });
3213
+ }
3214
+
3215
+ // ============================================
3216
+ // WORLD ACTION BAR
3217
+ // ============================================
3218
+
3219
+ // + New World
3220
+ document.getElementById('new-world-btn').addEventListener('click', () => {
3221
+ // Switch to custom rules mode
3222
+ currentWorldSource = 'custom';
3223
+ worldSourceTabs.forEach(t => {
3224
+ t.classList.toggle('active', t.dataset.source === 'custom');
3225
+ t.querySelector('input').checked = t.dataset.source === 'custom';
3226
+ });
3227
+ sourcePresetPanel.style.display = 'none';
3228
+ sourceCustomPanel.style.display = '';
3229
+ sourceUploadPanel.style.display = 'none';
3230
+
3231
+ // Clear everything
3232
+ document.getElementById('custom-world-name').value = '';
3233
+ document.getElementById('custom-world-thesis').value = '';
3234
+ document.getElementById('rule-input').value = '';
3235
+ document.getElementById('parsed-rules').innerHTML = '';
3236
+ document.getElementById('rule-status').textContent = '';
3237
+ document.getElementById('rule-status').className = 'rule-status';
3238
+ document.getElementById('custom-base-world').value = '';
3239
+
3240
+ // Clear active rules server-side
3241
+ fetch('/api/clear-rules', { method: 'POST' });
3242
+
3243
+ // Reset right panel
3244
+ document.getElementById('active-invariants').innerHTML = '<div style="font-size:11px;color:var(--text-muted)">No rules loaded. Define your world.</div>';
3245
+ });
3246
+
3247
+ // Clear Rules
3248
+ document.getElementById('clear-rules-btn').addEventListener('click', async () => {
3249
+ await fetch('/api/clear-rules', { method: 'POST' });
3250
+
3251
+ // Clear rule editor UI
3252
+ const ruleInput = document.getElementById('rule-input');
3253
+ if (ruleInput) ruleInput.value = '';
3254
+ const parsed = document.getElementById('parsed-rules');
3255
+ if (parsed) parsed.innerHTML = '';
3256
+ const status = document.getElementById('rule-status');
3257
+ if (status) { status.textContent = 'Rules cleared.'; status.className = 'rule-status success'; }
3258
+
3259
+ // Clear upload state
3260
+ const uploadStatus = document.getElementById('upload-status');
3261
+ if (uploadStatus) { uploadStatus.textContent = 'Rules cleared.'; uploadStatus.className = 'rule-status success'; }
3262
+ const loadedInfo = document.getElementById('loaded-world-info');
3263
+ if (loadedInfo) loadedInfo.style.display = 'none';
3264
+ });
3265
+
3266
+ // Load World File (switch to upload tab)
3267
+ document.getElementById('load-file-btn').addEventListener('click', () => {
3268
+ currentWorldSource = 'upload';
3269
+ worldSourceTabs.forEach(t => {
3270
+ t.classList.toggle('active', t.dataset.source === 'upload');
3271
+ t.querySelector('input').checked = t.dataset.source === 'upload';
3272
+ });
3273
+ sourcePresetPanel.style.display = 'none';
3274
+ sourceCustomPanel.style.display = 'none';
3275
+ sourceUploadPanel.style.display = '';
3276
+ });
3277
+
3278
+ // Save as World File (export)
3279
+ document.getElementById('export-world-btn').addEventListener('click', async () => {
3280
+ try {
3281
+ const resp = await fetch('/api/export-world');
3282
+ const data = await resp.json();
3283
+ const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
3284
+ const url = URL.createObjectURL(blob);
3285
+ const a = document.createElement('a');
3286
+ a.href = url;
3287
+ a.download = (currentWorld ? currentWorld.id : 'custom') + '-world.json';
3288
+ document.body.appendChild(a);
3289
+ a.click();
3290
+ document.body.removeChild(a);
3291
+ URL.revokeObjectURL(url);
3292
+ } catch (err) {
3293
+ alert('Export failed: ' + err.message);
3294
+ }
3295
+ });
3296
+
3297
+ // ============================================
3298
+ // WORLD FILE UPLOAD / PASTE
3299
+ // ============================================
3300
+ const uploadZone = document.getElementById('upload-zone');
3301
+ const uploadFileInput = document.getElementById('upload-file-input');
3302
+ const uploadBrowseBtn = document.getElementById('upload-browse-btn');
3303
+ const worldJsonInput = document.getElementById('world-json-input');
3304
+ const loadWorldBtn = document.getElementById('load-world-btn');
3305
+ const uploadStatusEl = document.getElementById('upload-status');
3306
+
3307
+ // Drag and drop
3308
+ uploadZone.addEventListener('dragover', (e) => { e.preventDefault(); uploadZone.classList.add('dragover'); });
3309
+ uploadZone.addEventListener('dragleave', () => { uploadZone.classList.remove('dragover'); });
3310
+ uploadZone.addEventListener('drop', (e) => {
3311
+ e.preventDefault();
3312
+ uploadZone.classList.remove('dragover');
3313
+ const file = e.dataTransfer.files[0];
3314
+ if (file) readWorldFile(file);
3315
+ });
3316
+
3317
+ // Browse button
3318
+ uploadBrowseBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadFileInput.click(); });
3319
+ uploadFileInput.addEventListener('change', () => {
3320
+ if (uploadFileInput.files[0]) readWorldFile(uploadFileInput.files[0]);
3321
+ });
3322
+
3323
+ // Click zone to browse
3324
+ uploadZone.addEventListener('click', () => { uploadFileInput.click(); });
3325
+
3326
+ function readWorldFile(file) {
3327
+ const reader = new FileReader();
3328
+ reader.onload = (e) => {
3329
+ worldJsonInput.value = e.target.result;
3330
+ uploadStatusEl.textContent = 'File loaded: ' + file.name + '. Click "Load into Runtime".';
3331
+ uploadStatusEl.className = 'rule-status success';
3332
+ };
3333
+ reader.readAsText(file);
3334
+ }
3335
+
3336
+ // Load into Runtime
3337
+ loadWorldBtn.addEventListener('click', async () => {
3338
+ const jsonText = worldJsonInput.value.trim();
3339
+ if (!jsonText) {
3340
+ uploadStatusEl.textContent = 'Paste or upload a world file first.';
3341
+ uploadStatusEl.className = 'rule-status error';
3342
+ return;
3343
+ }
3344
+
3345
+ let worldData;
3346
+ try {
3347
+ worldData = JSON.parse(jsonText);
3348
+ } catch (err) {
3349
+ uploadStatusEl.textContent = 'Invalid JSON: ' + err.message;
3350
+ uploadStatusEl.className = 'rule-status error';
3351
+ return;
3352
+ }
3353
+
3354
+ // Normalize: if the JSON is { world: {...} } or just {...}
3355
+ const worldPayload = worldData.world || worldData;
3356
+
3357
+ loadWorldBtn.textContent = 'Loading...';
3358
+ loadWorldBtn.disabled = true;
3359
+
3360
+ try {
3361
+ const resp = await fetch('/api/load-world-file', {
3362
+ method: 'POST',
3363
+ headers: { 'Content-Type': 'application/json' },
3364
+ body: JSON.stringify({ world: worldPayload }),
3365
+ });
3366
+ const result = await resp.json();
3367
+
3368
+ if (result.error) {
3369
+ uploadStatusEl.textContent = result.error;
3370
+ uploadStatusEl.className = 'rule-status error';
3371
+ } else {
3372
+ uploadStatusEl.textContent = result.message;
3373
+ uploadStatusEl.className = 'rule-status success';
3374
+
3375
+ // Show loaded world info
3376
+ const infoEl = document.getElementById('loaded-world-info');
3377
+ infoEl.style.display = '';
3378
+ document.getElementById('lw-name').textContent = result.world.title;
3379
+ document.getElementById('lw-thesis').textContent = '"' + result.world.thesis + '"';
3380
+ document.getElementById('lw-stats').textContent =
3381
+ result.world.invariants.length + ' invariants, ' +
3382
+ result.world.gates.length + ' gates, ' +
3383
+ result.rulesApplied + ' rules';
3384
+
3385
+ // Update active invariants in right panel
3386
+ const invHtml = result.world.invariants.map(inv =>
3387
+ '<div class="inv-item">[' + inv.id + '] ' + inv.description + '</div>'
3388
+ ).join('') + result.world.gates.map(g =>
3389
+ '<div class="inv-item" style="color:' + (g.severity === 'critical' ? 'var(--red)' : 'var(--yellow)') + '">[' + g.id + '] ' + g.label + '</div>'
3390
+ ).join('');
3391
+ activeInvEl.innerHTML = invHtml || '<div style="font-size:11px;color:var(--text-muted)">No invariants defined</div>';
3392
+
3393
+ // Update state variables if present
3394
+ if (result.world.stateVariables && result.world.stateVariables.length > 0) {
3395
+ // Store as a pseudo-world so sliders render
3396
+ currentWorld = {
3397
+ id: 'custom-world',
3398
+ title: result.world.title,
3399
+ thesis: result.world.thesis,
3400
+ stateVariables: result.world.stateVariables,
3401
+ invariants: result.world.invariants,
3402
+ gates: result.world.gates,
3403
+ };
3404
+ selectWorld('custom-world');
3405
+ } else {
3406
+ // Just set current world reference
3407
+ currentWorld = {
3408
+ id: 'custom-world',
3409
+ title: result.world.title,
3410
+ thesis: result.world.thesis,
3411
+ stateVariables: [],
3412
+ invariants: result.world.invariants,
3413
+ gates: result.world.gates,
3414
+ };
3415
+ }
3416
+ }
3417
+ } catch (err) {
3418
+ uploadStatusEl.textContent = 'Error: ' + err.message;
3419
+ uploadStatusEl.className = 'rule-status error';
3420
+ }
3421
+
3422
+ loadWorldBtn.textContent = 'Load into Runtime';
3423
+ loadWorldBtn.disabled = false;
3424
+ });
3425
+
2901
3426
  // ============================================
2902
3427
  // PLAIN-ENGLISH RULE EDITOR
2903
3428
  // ============================================
@@ -2947,25 +3472,58 @@ parseRulesBtn.addEventListener('click', async () => {
2947
3472
  }).join('');
2948
3473
 
2949
3474
  if (parsedRuleData.length > 0) {
2950
- parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation</button>';
3475
+ const btnLabel = currentWorldSource === 'custom' ? 'Generate World with ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') : 'Apply ' + parsedRuleData.length + ' Rule' + (parsedRuleData.length > 1 ? 's' : '') + ' to Simulation';
3476
+ parsedRulesEl.innerHTML += '<button class="btn btn-apply-rules" id="apply-rules-btn">' + btnLabel + '</button>';
2951
3477
  document.getElementById('apply-rules-btn').addEventListener('click', async () => {
2952
3478
  try {
3479
+ // If in custom rules mode, use base world if selected
3480
+ let worldId = currentWorld ? currentWorld.id : 'trading';
3481
+ if (currentWorldSource === 'custom') {
3482
+ const baseWorld = document.getElementById('custom-base-world').value;
3483
+ if (baseWorld) worldId = baseWorld;
3484
+ }
3485
+
2953
3486
  const applyResp = await fetch('/api/apply-rules', {
2954
3487
  method: 'POST',
2955
3488
  headers: { 'Content-Type': 'application/json' },
2956
- body: JSON.stringify({ rules: parsedRuleData, worldId: currentWorld ? currentWorld.id : 'trading' }),
3489
+ body: JSON.stringify({ rules: parsedRuleData, worldId }),
2957
3490
  });
2958
3491
  const applyData = await applyResp.json();
2959
3492
  if (applyData.status === 'applied') {
2960
3493
  ruleStatusEl.textContent = applyData.applied + ' rule(s) active. Run a simulation to see the effect.';
2961
3494
  ruleStatusEl.className = 'rule-status success';
3495
+
3496
+ // Update right panel invariants with custom rules
3497
+ const customName = document.getElementById('custom-world-name');
3498
+ const worldName = (customName && customName.value) ? customName.value : 'Custom World';
3499
+ const customThesis = document.getElementById('custom-world-thesis');
3500
+ const thesis = (customThesis && customThesis.value) ? customThesis.value : 'User-defined governance rules';
3501
+
3502
+ // Show rules in active invariants panel
3503
+ activeInvEl.innerHTML = parsedRuleData.map(r => {
3504
+ const enfType = r.enforcement || 'block';
3505
+ const colorMap = { block: 'var(--red)', allow: 'var(--green)', modify: 'var(--blue)', warn: 'var(--yellow)', pause: 'var(--yellow)' };
3506
+ const color = colorMap[enfType] || 'var(--text-secondary)';
3507
+ return '<div class="inv-item" style="color:' + color + '">[' + r.id + '] ' + r.description + '</div>';
3508
+ }).join('');
3509
+
3510
+ // In custom mode, set a custom world reference
3511
+ if (currentWorldSource === 'custom') {
3512
+ const baseWorld = document.getElementById('custom-base-world').value;
3513
+ if (baseWorld) {
3514
+ selectWorld(baseWorld);
3515
+ } else {
3516
+ currentWorld = { id: 'custom-world', title: worldName, thesis, stateVariables: [], invariants: [], gates: [] };
3517
+ }
3518
+ document.getElementById('world-thesis').textContent = '"' + thesis + '"';
3519
+ }
2962
3520
  }
2963
3521
  } catch (err) {
2964
3522
  ruleStatusEl.textContent = 'Error applying rules: ' + err.message;
2965
3523
  ruleStatusEl.className = 'rule-status error';
2966
3524
  }
2967
3525
  });
2968
- ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click Apply.';
3526
+ ruleStatusEl.textContent = 'Parsed ' + parsedRuleData.length + ' rule(s). Review and click ' + (currentWorldSource === 'custom' ? 'Generate.' : 'Apply.');
2969
3527
  ruleStatusEl.className = 'rule-status success';
2970
3528
  }
2971
3529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@neuroverseos/nv-sim",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "description": "CLI for running governed vs baseline agent simulations to explore how world rules shape emergent system behavior.",