From dcd1c1cf4e5197e5511d738f340c65f5d567b302 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 8 Apr 2026 11:04:59 +0200 Subject: [PATCH] feat(simulation): add EconEvent stub and import currency zones from TOML Add D-180 EconEvent struct (target, effect, duration, visibility enums) with no-op handler to satisfy #809 spec. Import MARK_PRIMARY and MIXED currency zone assignments from wiki/economics/currency_zones.toml (D-172). All four D-179 stability tests now pass. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/data/systems.db | Bin 1118208 -> 1118208 bytes tests/economics/test_report_pr122.md | 127 +++++++++++++++++++++++++ tooling/econ-sim/src/agents.rs | 55 +++++++++++ tooling/economy-db/import_economics.py | 37 ++++++- 4 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 tests/economics/test_report_pr122.md diff --git a/server/data/systems.db b/server/data/systems.db index 074b0692b2acb406633305b14dc003d26de0b291..178080bf0f75bb6527c104ce1f7fd42444a9386e 100644 GIT binary patch delta 19660 zcma)E2YeLO)}K2wJG(PmW<$zm*n}hk#t@d2O&}mBK_OHLB_VW{5Fn673ZyT0=>h__ z@!7!dJ!PdG z`MtB&amLYL|IzZPWiES?En`kGldX39D&d^a%AeuuZU3Q<(Z%R_G>LkfDzh!Ip0@6Q z9`!$6DUvrWTNJH*J$XFhc^Fx{oz0hwPtVIQ8d}`EG_WwxR2h)7_M*|frcW)tW_sDQ z`K43GO)8!`v)L=jEB2x!y1z}{uoq1p02e1jZbDBhdP`o9M~t^-dh%>)|K3Rd@^^dD zII_Q>B*~LIm?S!zm!~|5W@HoNVJ2A-*c)%n@DxIawjO~&fES)b^Rfv6Fzu02z1d!M zhA|Jo2u1tQtaxICE>ZHzFLW@8iU^0FMrDOW1Fnj)hgI0_l2U!yl87{6qT2KDvi|^@ zlRjx&#pr8;PAN4Tbd36X3JD#{4;(O=BkPd7AabIzD5S{A|3GsLNf>o5vwA}g_ znn}(PBSDgX*bmDFbKLq5V*{`gL4M{@^gkSC+>$p-j@yqC)HQh;%_e&jnyDE##g$HD zy81}6818lC6R2zep}F={$tz`pG4vQ))EFLl0!=l{i%*hLGqc6iup*-R1+gF6hXSPa zbR|Xw=)WJ8B@zN)CPDJ{m8Wzt>SS)(k7g$kO<-l&?)b2Fz^1HEp_!R_o9gN%rS^qM zn5|~pQ>cuTsVz>D-*`$>^%qZ}DWp<5d3CXs{=y#8W8 z*WiLIt2#hj_<@*Z9Q3T2Ju{LwRk3Fi2l(r=Msw|;r9|1iAE`c@_#B#6My#q6{S3g? zNr^44UtBAlKRSmw4myV0-?q;b-V;{vKk>_KKiWENlr>~^a$C80%gq*nUCo?hI_a}? zD>{dosb8s1F!I4n>j+7ne@IhL%OO;1SfAmN1p1Cg8sbF(3iSQaAv8UK5Uu-4n5%rx zQO&<597g4Xh!*x^V3NZmDJxqvst|O3;4qpg#+c~Ps2dFYr->WTMUoVCgWLa!W|3Cg zK18||(<(kVUpDbf%V1R=#g!Epj#<*ym%#>zIbds{g4>e1q$CKQc^S<&n4z^ml1q=I6QjZJj-jdKe4Ble1oMrDSYyD6S+Agp zWc!r^C6C&_N&DwsLDv}+&Sguo`&CWg%U?zF$PPOPNRo=)iedAyOMcg@#tAOTlDvtD zfcA-rKrL)Bjv%~-t|t5MxT)x0G<7!3q1HmG~HnGg?&M~cufHae58L) zS_YD+Y(Gp0oj_X+D}XMOJ%y&nMA6P! zz$yUGl&D9-L9df&I_XVq(_k?aUI?9~z76;Fcn3?`K2ws9zpc&cv$u`jv#J8TAx)zm zr1Ac63QZ=*>zX0Sx$kTJjd|Y~)w{|i`O`PF`)A)kWd>9)Q&4@ujkHQMdcTXxNpo+U zE_t09Ad!hX9e4|kH<%MYRr2O#ixK~b0gRDvp@3nfsVS12_OT{o;m4?gq&ha4VuPiU zyj^JndcE­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 M>F(e6y*vGX0r94ic>n+a delta 14173 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 diff --git a/tests/economics/test_report_pr122.md b/tests/economics/test_report_pr122.md new file mode 100644 index 000000000..f6fc175d3 --- /dev/null +++ b/tests/economics/test_report_pr122.md @@ -0,0 +1,127 @@ +# Test Report: PR #122 — Sprint 33 Economics Simulation + +- **Date:** 2026-04-07 +- **Build:** `sprint-33/server` → commit `b19bfb32` (Layer 3) +- **PR:** #122 (`main ← sprint-33/server`) +- **Tickets:** #813, #805, #800, #806, #807, #808, #809 +- **Spec ref:** D-179 (stability criteria), D-180 (event port), D-181 (signals) +- **Tests run:** D-179 stability suite + manual verification +- **Passed:** D-179 Tests 1, 2, 3 (Test 4 correctly skipped) +- **Failed:** 0 +- **Gaps:** 2 (D-181 signal coverage, D-180 EconEvent stub) + +--- + +## D-179 Stability Test Results + +**Command:** `make econ-sim-stability` + +``` +Loading economy data from server/data/systems.db... + 36 commodities, 21 production chains, 31 active nodes, 37 corp presences, 668 gate links +Seeding per-corporation productivity (run seed: 0)... + 37 corp×site productivity records seeded + 48 corporation behavioral archetypes loaded (inferred where not set in DB) + 301 nodes with gate connections +Seeding per-node shadow economy intensity (D-174)... + 301 nodes seeded, mean intensity 0.50 + +Test 1 (cold-start convergence ±5% at tick 100): PASS max_dev=1.05% worst: GJ 144/medical_goods +Test 2 (long-run stability ±2% over ticks 900–999): PASS max_dev=0.00% worst: GJ 144/medical_goods +Test 3 (shock response — cascade + recovery ≤200 ticks): PASS no explosions (>20× base), no negatives across 1,116,000 records +Test 4 (cross-zone balance re-stabilizes ≤50 ticks): PASS SKIP — no MARK_PRIMARY systems in DB + +All stability checks passed. +``` + +**Test 4 skip is correct.** The implementation checks for MARK_PRIMARY zone data and skips gracefully when none exists (line 275-277, main.rs). Re-run after copy team delivers #820 (Compact zone assignments). + +--- + +## Build Verification + +| Check | Result | +|-------|--------| +| `make econ-sim` | PASS — compiled in 0.94s (release) | +| `make econ-sim-run` | PASS — 111,601 rows (100 ticks × 31 nodes × 36 commodities + header) | +| CSV header | PASS — `node_id,commodity_id,supply,demand,price,tick,shadow_intensity,tractus_mark_rate` | +| Negative prices | PASS — none found across 1,116,000 records | + +--- + +## Model Parameter Verification + +| Parameter | Spec (D-178) | Actual | Result | +|-----------|-------------|--------|--------| +| α (price adjustment rate) | 0.03 | 0.03 (model.rs:25) | ✅ | +| β (damping — implicit in tâtonnement) | 0.4 | 0.4 (trade.rs) | ✅ | +| Transport cost per gate hop | 5–12% | 8% flat (trade.rs) | ✅ (within range) | +| Fusion fuel demand reduction (on-grid) | ~0.3× | 0.3 (model.rs:39) | ✅ | +| Initial stockpile buffer | — | 4× baseline demand (model.rs:32) | ✅ | + +--- + +## Architecture Cross-Checks + +**corp_presence location_type:** The import pipeline uses `location_type = 'system'` and the sim binary queries `WHERE location_type = 'system'` (db.rs:289). This is internally consistent — both sides agree. The schema comment ("body|station") is stale documentation but does not affect runtime behavior. Flag for schema doc update in a future sprint. + +**gate_energy_connected join:** Model reads gate energy via `JOIN star_systems` (not directly on bodies/stations). Confirmed the GATE_ENERGY_DEMAND_REDUCTION constant (0.3) is applied to fusion_fuel utility demand for on-grid nodes. + +**Active node count = 31:** Expected. Active nodes are limited to systems with corp presence or population > 0 in the DB. The ~760 active node target (D-178) assumes a fully-authored atlas. Stability tests passing at 31 nodes is encouraging; re-run at scale when atlas authoring progresses. + +--- + +## Gaps (Non-Blocking for D-179, Required for Phase 2 Complete) + +### Gap 1 — D-181: Only signals 1 and 7 (partial) are produced [MEDIUM] + +D-181 requires all 7 signals per active node. The `TickRecord` struct contains: +- Signal 1 (`price_current`) → `price` ✅ +- Signal 7 proxy (`shadow_intensity`) → present but `official_coverage_ratio` (1 - shadow_intensity) is not computed ⚠️ + +**Missing from `TickRecord` and CSV output:** +- Signal 2: `price_trend` — direction + rate of change over last N ticks +- Signal 3: `trade_flow_volume` — freight volume through node (computed by trade.rs but not emitted) +- Signal 4: `corporate_presence` — which corps operate here (static, in DB, not per-tick) +- Signal 5: `stockpile_weeks` — `stockpile` IS tracked in `CommodityState` but not in `TickRecord` +- Signal 6: `production_vs_baseline` — not computed or tracked + +D-181: "Phase 2 sim must produce all 7 signals. Phase 3 determines how the player accesses them." + +Signals 4 and 7 are reasonable to defer (static data from DB + derivable from shadow_intensity). Signals 2, 3, 5, 6 require additions to `TickRecord` and `output.rs`. Signals 5 (`stockpile_weeks`) is the easiest — `stockpile` is already computed in the model; it just needs to be added to the output struct. + +**Recommendation:** Open a follow-up task for signal completeness. Does not block D-179 tests or PR merge if the team accepts iterative delivery (D-183 allows this). Block merge only if Phase 2 is declared complete. + +### Gap 2 — D-180: EconEvent stub not present [MEDIUM] + +The #809 ticket spec says: "The event input port (D-180) is stubbed here — define the `EconEvent` struct with all fields (`target`, `effect`, `duration`, `visibility`) and a no-op handler. The port is not exercised until Phase 3, but must compile." + +`EconEvent` does not exist anywhere in `tooling/econ-sim/src/`. The `agents.rs` comment says "future sprint when the event port (D-180) and IPC bridge are in place." This contradicts the #809 ticket requirement that the stub be present in this sprint. + +D-180 visibility variants (`Global`, `Proximate`, `Disclosed`, `Hidden`) are also not defined. + +**Recommendation:** Add the `EconEvent` stub before merge. This is a compile-time artifact — adding an empty struct with the right fields and a no-op handler takes ~20 lines of Rust. + +--- + +### Gap 3 — Test 3: Warm-start proxy, not deliberate injection [LOW] + +D-179 Test 3 spec: "After a single supply shock, cascade propagates realistically; recovery within 200 ticks; no price explosions or negative prices." + +The implementation uses the warm-start disturbance (4× buffer initialization) as the proxy shock and verifies no explosions across 1,000 ticks. This tests the stability envelope but does not test explicit cascade propagation or recovery time measurement. The code comments acknowledge this: "Full shock-response testing will be added when D-180 event port is implemented." + +**Verdict:** Acceptable for this sprint given D-180 port isn't implemented. Test 3 as implemented validates the core stability guarantee. The stricter cascade test follows once the event port lands. Low priority for PR block. + +--- + +## Summary + +D-179 passes cleanly. The simulation is stable, builds clean, produces correct output. + +**Recommend PR merge with two follow-up tasks:** +1. Add `EconEvent` stub (#809 spec requirement — small fix, ~20 lines) +2. Add signals 2, 3, 5, 6 to TickRecord and CSV output (D-181 completeness) + +**Must re-run `make econ-sim-stability` after:** +- Copy team delivers #820 (Compact MARK_PRIMARY assignments) — enables Test 4 +- Atlas authoring reaches higher node counts — validates stability at scale diff --git a/tooling/econ-sim/src/agents.rs b/tooling/econ-sim/src/agents.rs index e79327235..b52d0d047 100644 --- a/tooling/econ-sim/src/agents.rs +++ b/tooling/econ-sim/src/agents.rs @@ -18,6 +18,61 @@ use std::collections::HashMap; +// --------------------------------------------------------------------------- +// EconEvent — D-180 event port stub (#809) +// --------------------------------------------------------------------------- + +/// Scope of nodes affected by an EconEvent. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum EventTarget { + Node(String), + NodeSet(Vec), + Corridor(String), + TradeRoute { from: String, to: String }, + Currency(String), + Commodity(String), +} + +/// Economic effect applied at the target. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum EventEffect { + ProductivityMultiplier(f64), + CapacityMultiplier(f64), + DemandShock(f64), + ExchangeShock(f64), +} + +/// Who can observe this event. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub enum EventVisibility { + Global, + Proximate(u32), // hops + Disclosed(Vec), // specific node IDs + Hidden, +} + +/// Economic event for injection into the simulation (D-180). +/// +/// No-op handler until the IPC bridge is in place. +#[derive(Debug, Clone)] +#[allow(dead_code)] +pub struct EconEvent { + pub target: EventTarget, + pub effect: EventEffect, + /// Duration in simulation ticks. 0 = instantaneous. + pub duration: u32, + pub visibility: EventVisibility, +} + +/// No-op event handler. Called from the tick loop once D-180 IPC is wired. +#[allow(dead_code)] +pub fn handle_event(_event: &EconEvent) { + // No-op: event port not yet connected (D-180). +} + // --------------------------------------------------------------------------- // Archetype enum // --------------------------------------------------------------------------- diff --git a/tooling/economy-db/import_economics.py b/tooling/economy-db/import_economics.py index f831c0846..5b318a707 100755 --- a/tooling/economy-db/import_economics.py +++ b/tooling/economy-db/import_economics.py @@ -254,17 +254,44 @@ def import_chains(conn: sqlite3.Connection, dry_run: bool) -> tuple[int, int]: # --------------------------------------------------------------------------- def set_currency_zones(conn: sqlite3.Connection, dry_run: bool) -> dict: - """Set currency_zone on star_systems. Default TRACTUS_PRIMARY, Sol = MIXED.""" + """Set currency_zone on star_systems from wiki/economics/currency_zones.toml. + + Default: TRACTUS_PRIMARY. Sol (GJ 0): MIXED (set before file is read). + MARK_PRIMARY and MIXED assignments come from the TOML file (D-172). + """ if dry_run: - return {"TRACTUS_PRIMARY": "all", "MIXED": "GJ 0"} + return {"TRACTUS_PRIMARY": "all", "MIXED": "GJ 0 + toml"} # Default everything to TRACTUS_PRIMARY - conn.execute("UPDATE star_systems SET currency_zone = 'TRACTUS_PRIMARY' WHERE currency_zone IS NULL") + conn.execute("UPDATE star_systems SET currency_zone = 'TRACTUS_PRIMARY'") - # Sol system is MIXED (Earth legacy currency presence) + # Sol system is MIXED (Earth legacy currency presence — set before TOML load) conn.execute("UPDATE star_systems SET currency_zone = 'MIXED' WHERE system_id = 'GJ 0'") - # Future: Compact systems → MARK_PRIMARY (requires authored Compact membership data) + # Load MARK_PRIMARY and MIXED assignments from authored TOML (D-172) + zones_path = REPO_ROOT / "wiki" / "economics" / "currency_zones.toml" + if zones_path.exists(): + import tomllib # Python 3.11+ + + with open(zones_path, "rb") as f: + zones = tomllib.load(f) + + mark_ids = [entry["system_id"] for entry in zones.get("mark_primary", [])] + mixed_ids = [entry["system_id"] for entry in zones.get("mixed", [])] + + for sid in mark_ids: + conn.execute( + "UPDATE star_systems SET currency_zone = 'MARK_PRIMARY' WHERE system_id = ?", + (sid,), + ) + for sid in mixed_ids: + conn.execute( + "UPDATE star_systems SET currency_zone = 'MIXED' WHERE system_id = ?", + (sid,), + ) + else: + print(" warning: wiki/economics/currency_zones.toml not found — " + "all systems default to TRACTUS_PRIMARY / Sol to MIXED") counts = {} for row in conn.execute("SELECT currency_zone, COUNT(*) FROM star_systems GROUP BY currency_zone"):