@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 +166 -6
- package/dist/engine/liveVisualizer.js +598 -40
- package/package.json +1 -1
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
|
|
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.
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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">
|
|
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
|
-
<!--
|
|
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>
|
|
1710
|
-
<div class="
|
|
1711
|
-
<
|
|
1712
|
-
<
|
|
1713
|
-
|
|
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
|
-
<!--
|
|
1719
|
-
<div class="
|
|
1720
|
-
<
|
|
1721
|
-
|
|
1722
|
-
<
|
|
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
|
-
<!--
|
|
1728
|
-
<div class="
|
|
1729
|
-
<
|
|
1730
|
-
|
|
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 All outbound emails must be reviewed Block deletion of production data Limit API calls to 100 per minute 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
|
-
<!--
|
|
1734
|
-
<div class="
|
|
1735
|
-
<
|
|
1736
|
-
|
|
1737
|
-
<
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
<
|
|
1746
|
-
<
|
|
1747
|
-
|
|
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">📄</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='{ "name": "Marketing Governance", "thesis": "All marketing actions are governed", "invariants": [...], "rules": [...], "gates": [...] }'></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
|
-
<!--
|
|
1753
|
-
<div class="ctrl-section">
|
|
1754
|
-
<h3>
|
|
1755
|
-
<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
|
-
|
|
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
|
|
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