Files
settled-reach/tests/economics/test_plan_sprint_33.md
T
jpmschweitzerandClaude Sonnet 4.6 4eaa669aff docs(meta): add Sprint 33 economics test plan
Test plan covering #806, #807, #808, #809 acceptance criteria aligned
with D-179 stability tests and Phase 2 economics deliverable requirements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:57:06 +02:00

692 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Test Plan: Sprint 33 — Pecunia (Economics Simulation)
- **Sprint:** 33
- **Date:** 2026-04-07
- **Author:** Hoshe (QA)
- **Branch:** `sprint-33/server`
- **Tickets:** #813, #805, #806, #807, #808, #809
- **Key spec:** `decisions/economics.md` (D-171–D-187), D-179 (stability acceptance criteria)
---
## How to Use This Document
Verification queries are written for `tooling/db/sqlite-query`. Stability tests run via
`tooling/econ-sim --stability-check` once #807 lands. All SQL queries assume a fully-imported
`server/data/systems.db` (after running `make economy-db`).
**Pass/fail convention:** Each test has an **Expected** clause. A test fails if the output
deviates from Expected in any measurable way. Failures from #807 and later that involve
oscillation or divergence indicate a broken model — tune α/β before calling it a feature (D-179).
---
## Pre-Flight: Baseline Data Sanity
Run these before testing any ticket. If they fail, the DB state is corrupted and ticket-level
tests are meaningless.
```sql
-- BF-1: Commodity count must be 36 (D-184)
SELECT COUNT(*) FROM commodities;
-- Expected: 36
-- BF-2: Commodity tier breakdown must match D-184 (9/10/9/5/3)
SELECT tier, COUNT(*) FROM commodities GROUP BY tier ORDER BY tier;
-- Expected:
-- intermediate 10
-- raw 9
-- final 9
-- service_professional 5
-- service_luxury 3
-- BF-3: Production chain count must be 21 (D-184)
SELECT COUNT(*) FROM production_chains;
-- Expected: 21
-- BF-4: Chain input count must be 40 (count inputs from production_chains.toml)
SELECT COUNT(*) FROM chain_inputs;
-- Expected: 40
-- BF-5: Gate links must be bidirectional (every from→to has a matching to→from)
SELECT COUNT(*) FROM gate_links gl
WHERE NOT EXISTS (
SELECT 1 FROM gate_links rev
WHERE rev.from_system_id = gl.to_system_id
AND rev.to_system_id = gl.from_system_id
);
-- Expected: 0
-- BF-6: All chain inputs reference valid commodities
SELECT COUNT(*) FROM chain_inputs ci
LEFT JOIN commodities c ON ci.input_commodity_id = c.commodity_id
WHERE c.commodity_id IS NULL;
-- Expected: 0
-- BF-7: All chain outputs reference valid commodities
SELECT COUNT(*) FROM production_chains pc
LEFT JOIN commodities c ON pc.output_commodity_id = c.commodity_id
WHERE c.commodity_id IS NULL;
-- Expected: 0
```
**Known discrepancy to verify:** The `commodities.toml` section header reads
`# PROFESSIONAL SERVICES (7)` but only 5 entries follow. The total must be 36 (matching
D-184: 9+10+9+5+3). Flag if the count is 38.
---
## #813 — Energy-over-gate Schema Extension
**Spec ref:** D-186
**Assigned to:** Tyre
**Status:** in_progress
### What was changed
- `gate_energy_connected INTEGER DEFAULT 1` column added to `star_systems`
(system-level, not per body/station — all nodes in a system inherit the system's setting
via a join. Gate Corp energy is a system-wide commercial contract, not a per-node toggle.)
- `MARK_PRIMARY` zones default to `false` (0)
- All other zones default to `true` (1)
- Migration is idempotent (safe to re-run via COLUMN_MIGRATIONS)
- `set_gate_energy()` runs as step 6, after `set_currency_zones()` step 5 (correct ordering)
### Verification queries
```sql
-- 813-1: Column exists on star_systems table
PRAGMA table_info(star_systems);
-- Expected: row with name='gate_energy_connected' and type='INTEGER'
-- 813-2: MARK_PRIMARY systems have gate_energy_connected = 0
SELECT COUNT(*) FROM star_systems
WHERE currency_zone = 'MARK_PRIMARY'
AND gate_energy_connected != 0;
-- Expected: 0
-- 813-3: TRACTUS_PRIMARY systems have gate_energy_connected = 1
SELECT COUNT(*) FROM star_systems
WHERE currency_zone = 'TRACTUS_PRIMARY'
AND gate_energy_connected != 1;
-- Expected: 0
-- 813-4: MIXED systems have gate_energy_connected = 1
SELECT COUNT(*) FROM star_systems
WHERE currency_zone = 'MIXED'
AND gate_energy_connected != 1;
-- Expected: 0
-- 813-5: gate_energy_connected is never NULL
SELECT COUNT(*) FROM star_systems WHERE gate_energy_connected IS NULL;
-- Expected: 0
-- 813-6: At least one MARK_PRIMARY system exists (validates zone data is present)
SELECT COUNT(*) FROM star_systems WHERE currency_zone = 'MARK_PRIMARY';
-- Expected: > 0 (requires #820 copy work to be merged first; skip if copy branch not merged)
-- 813-7: Sim binary can read gate_energy via join (integration spot-check)
-- The sim must join bodies/stations to star_systems to get gate_energy_connected.
-- Verify the join is correct:
SELECT b.body_id, ss.gate_energy_connected
FROM bodies b
JOIN star_systems ss ON b.system_id = ss.system_id
WHERE b.inhabited = 1
LIMIT 5;
-- Expected: 5 rows with gate_energy_connected = 0 or 1 (not NULL)
```
### Edge cases
**813-E1: Idempotent migration**
Run `make economy-db` twice on the same DB. Second run must not raise an error, and query
813-1 through 813-7 must still pass.
**813-E2: Systems with NULL currency_zone**
If any star system has `currency_zone IS NULL`, the migration logic must treat it as
`TRACTUS_PRIMARY` (default to `true`). Verify no bodies end up with `gate_energy_connected = 0`
due to a NULL zone.
```sql
SELECT COUNT(*) FROM star_systems WHERE currency_zone IS NULL;
-- Expected: 0 (import pipeline sets default; but verify regardless)
```
**813-E3: Demand reduction is NOT implemented here**
Confirm the `~0.3× fusion_fuel` utility demand reduction is absent from the schema-only ticket.
The demand model lives in the sim binary (#806). Verify:
- No column named `utility_demand_modifier` or similar on star_systems
- No new columns beyond `gate_energy_connected` on star_systems
### Regression markers
- `tooling/economy-db/import_economics.py` migration block must still be idempotent
- Existing BF-1 through BF-7 must still pass after #813 migration
---
## #805 — Corporation Pipeline and Validation
**Spec ref:** D-175, D-182
**Assigned to:** Dudley
**Status:** in_progress
### What was changed
- `import_economics.py` (or a new companion script) reads `wiki/corporations/` markdown files
- Populates `corp_presence` table from authored location data
- Validates wiki corp names ↔ DB `corporations.proper_name` sync (D-182 sync constraint)
- Coverage rules: 3+ corps per major commodity type, 1+ per inhabited system >100K pop
- Chain completeness validation: every intermediate commodity has ≥1 producing chain
- Coverage failures exit non-zero (D-175 phase gate)
### Verification queries
```sql
-- 805-1: corp_presence is no longer empty after pipeline run
SELECT COUNT(*) FROM corp_presence;
-- Expected: > 0
-- 805-2: All corp_presence rows reference valid corp_id
SELECT COUNT(*) FROM corp_presence cp
LEFT JOIN corporations c ON cp.corp_id = c.corp_id
WHERE c.corp_id IS NULL;
-- Expected: 0
-- 805-3: All corp_presence rows reference valid location_id
-- (either a body_id or station_id — location_type determines which table)
SELECT COUNT(*) FROM corp_presence WHERE location_type = 'body'
AND location_id NOT IN (SELECT body_id FROM bodies);
-- Expected: 0
SELECT COUNT(*) FROM corp_presence WHERE location_type = 'station'
AND location_id NOT IN (SELECT station_id FROM stations);
-- Expected: 0
-- 805-4: Chain completeness — every intermediate must have a producing chain
SELECT c.commodity_id, c.name
FROM commodities c
WHERE c.tier = 'intermediate'
AND c.commodity_id NOT IN (
SELECT output_commodity_id FROM production_chains
);
-- Expected: 0 rows (all 10 intermediates have a producing chain)
-- 805-5: Chain completeness — every final good must have a producing chain
SELECT c.commodity_id, c.name
FROM commodities c
WHERE c.tier = 'final'
AND c.commodity_id NOT IN (
SELECT output_commodity_id FROM production_chains
);
-- Expected: 0 rows (all 9 finals have a producing chain)
-- 805-6: Services have NO producing chains (they are demand sinks, not outputs)
SELECT c.commodity_id, c.name
FROM commodities c
WHERE c.tier IN ('service_professional', 'service_luxury')
AND c.commodity_id IN (SELECT output_commodity_id FROM production_chains);
-- Expected: 0 rows
-- 805-7: Raw materials have NO producing chains (they are inputs, not outputs)
SELECT c.commodity_id, c.name
FROM commodities c
WHERE c.tier = 'raw'
AND c.commodity_id IN (SELECT output_commodity_id FROM production_chains);
-- Expected: 0 rows (fusion_fuel is intermediate, not raw — verify separately)
-- 805-8: Coverage rule — corporations per major commodity type (D-175: 3+ per major type)
-- "Major commodity type" = intermediates and finals with demand_model = 'market'
-- This requires corp_presence.primary_operation to reference a commodity_id; adjust
-- query if the schema uses a different field. Flag if the field is absent.
SELECT commodity_id, COUNT(DISTINCT cp.corp_id) AS corp_count
FROM corp_presence cp
JOIN corporations c ON cp.corp_id = c.corp_id
WHERE cp.primary_operation IS NOT NULL
GROUP BY cp.primary_operation
HAVING corp_count < 3;
-- Expected: 0 rows (every commodity with corp presence has 3+ corps)
-- 805-9: Coverage rule — inhabited systems > 100K pop have at least one corp
SELECT ss.system_id, ss.proper_name
FROM star_systems ss
JOIN system_economy se ON ss.system_id = se.system_id
WHERE se.population > 100000
AND NOT EXISTS (
SELECT 1 FROM corp_presence cp
JOIN bodies b ON cp.location_id = b.body_id AND cp.location_type = 'body'
WHERE b.system_id = ss.system_id
UNION
SELECT 1 FROM corp_presence cp
JOIN stations s ON cp.location_id = s.station_id AND cp.location_type = 'station'
WHERE s.system_id = ss.system_id
);
-- Expected: 0 rows
```
### Exit code tests
Run the pipeline with intentional violations and verify non-zero exit:
**805-E1: Wiki name mismatch causes hard error**
Temporarily rename a corporation in the DB to something the wiki doesn't know, re-run pipeline.
Expected: non-zero exit with clear error message identifying the mismatch.
**805-E2: Missing commodity coverage causes hard error**
If coverage drops below 3 corps for any major commodity type, pipeline must exit non-zero.
Expected: non-zero exit with specific commodity identified.
**805-E3: Coverage failure for underpopulated system causes hard error**
If an inhabited system with >100K pop has zero corp presence, pipeline must exit non-zero.
Expected: non-zero exit with system_id identified.
**805-E4: Dry-run still works**
`python3 tooling/economy-db/import_economics.py --dry-run` must:
- Not write to corp_presence
- Run all validations and report but not halt on coverage gaps (dry-run output is informational)
- Exit 0 (dry-run is for inspection, not a gate)
Wait — check this with Dudley. If dry-run is meant to be a gate too, this should exit non-zero
on validation failure. The existing pipeline exits 0 on dry-run. Confirm expected behavior
before locking this test.
### Regression markers
- BF-1 through BF-7 still pass (new pipeline must not corrupt commodity/chain data)
- `corp_presence` table's FK constraint still enforced (BF-6 analog for corps)
- Existing `gate_links` bidirectionality (BF-5) unaffected
---
## #806 — Skeleton Economy_sim Binary
**Spec ref:** D-176, D-177, D-178
**Assigned to:** Dudley
**Status:** backlog (blocked on #805)
### What was changed
- New Rust binary at `tooling/econ-sim/`
- Reads systems.db: gate_links, commodities, production_chains, chain_inputs, corp_presence
- Seeds per-corp-site productivity (5 dimensions, PRNG, log-normal distribution)
- Layer 1 Leontief only (no inter-system trade, no currency)
- Outputs per-node CSV: node_id, commodity_id, supply, demand, price, tick
- `--stability-check` flag compiles (stub, not yet meaningful)
### Build verification
```bash
cd tooling/econ-sim && cargo build
# Expected: exits 0, no compile errors
tooling/econ-sim --help
# Expected: help text with --db, --stability-check, and --output flags visible
```
### CSV output verification
```bash
tooling/econ-sim --db server/data/systems.db --output /tmp/econ_out.csv
```
**806-1:** CSV file is created at the specified path
**806-2:** CSV header contains: `node_id,commodity_id,supply,demand,price,tick`
**806-3:** Row count is `active_nodes × 36` (760 active nodes × 36 commodities = ~27,360 rows)
- Acceptable range: ±10% of 27,360 (active node count may differ slightly from spec estimate)
**806-4:** No `price` values are negative or zero for commodity types with non-zero base_price
**806-5:** `tick` column is 0 for initial seeding output (first tick)
### Productivity seeding verification
**806-6: Standard node range (D-176)**
```python
# Pseudocode — inspect CSV output
import csv, math
prices = [float(row['price']) for row in csv.DictReader(open('/tmp/econ_out.csv'))]
# For standard commodities, productivity multiplier range is 0.4–1.8x
# Price variation relative to base_price should reflect this range
base_price_by_id = { ... } # from commodities table
multipliers = [price / base_price_by_id[row['commodity_id']] for row in rows]
assert all(0.3 <= m <= 2.0 for m in multipliers), "multiplier out of expected range"
# Actual range check: most values should fall within 0.4–1.8x (log-normal tails permitted)
```
**806-7: Monopoly-source node range (D-176)**
Nodes producing `lattice_grade_material` (production_ubiquity = 'monopolistic') must show
tighter multiplier range: 0.7–1.4×. Verify variance is lower than standard nodes.
**806-8: D-177 constraints — what must NOT vary**
Verify the binary never seeds or varies:
- Location of production (the set of nodes producing each commodity is fixed from DB data,
not random)
- `lattice_grade_material` productivity: must stay within 0.7–1.4× (monopolistic ceiling)
- Absence of seeded "starting disruptions" (no negative productivity, no corps with zero
initial output as a seeded state)
**806-9: Corridor correlation (D-176 ~0.6)**
Nodes in the same geographic corridor should have correlated productivity across runs with
similar PRNG seeds. Spot-check: run binary twice with seeds differing by 1; nodes in same
corridor should show ~0.6 Pearson correlation on their multipliers.
### Stub stability check
```bash
tooling/econ-sim --stability-check
# Expected: exits with some non-panic output, even if it's "stability tests not yet implemented"
# Must NOT crash or segfault
```
### Regression markers
- Atlas binary still builds: `cargo build --bin atlas`
- D-177 lore constraints respected (see 806-8)
---
## #807 — Trade Flows and Stability Testing
**Spec ref:** D-178, D-179
**Assigned to:** Dudley
**Status:** backlog (blocked on #806)
**This is the most critical ticket. D-179 defines the exit condition for Phase 2.**
### Tâtonnement parameters
Verify from source code:
- α = 0.03 (price adjustment speed)
- β = 0.4 (damping coefficient)
If these are configurable via CLI flags, document the defaults. If hardcoded, grep for them:
```bash
grep -r "0\.03" tooling/econ-sim/src/
grep -r "0\.4" tooling/econ-sim/src/
```
### Floyd-Warshall startup performance
```bash
time tooling/econ-sim --stability-check 2>&1 | head -5
# Expected: FW initialization completes in < 2s (D-178 spec: ~0.5s, allow 4x margin)
# Flag if > 5s: likely iterating over all 3700 nodes instead of the ~760 active subgraph
```
### Market node tiering (D-178)
**807-1:** Active node count is approximately 760 (inhabited bodies + all stations)
```sql
-- Count active market nodes per D-178 definition
SELECT COUNT(*) FROM (
SELECT body_id AS node_id FROM bodies WHERE inhabited = 1
UNION ALL
SELECT station_id FROM stations
);
-- Expected: ~760 (accept 700–820 as the spec estimate may not match actual DB state)
```
**807-2:** Passive producer count is approximately 240
```sql
SELECT COUNT(*) FROM bodies WHERE inhabited = 0 AND population > 0;
-- Expected: ~240 (bodies with economic activity but no market function)
-- Adjust query based on how the sim defines "passive producer"
```
### Transport cost model
Verify in source or via output that:
**807-3:** Gate edges cost 5–12% per hop (inter-system)
**807-4:** Orbital edges cost 1–3% (intra-system)
**807-5:** Transport costs are applied to commodity prices, not abstracted away
### D-179 Stability Tests
```bash
tooling/econ-sim --stability-check
```
All four tests are run by this flag (Tests 1–2 in #807, Tests 3–4 in #808). After #807:
**Test 1: Cold-start convergence (D-179)**
- Simulate 100 game-days from cold start
- Measure price deviation from equilibrium at tick 100
- **Pass criterion:** All active commodity prices within ±5% of equilibrium
- **Fail indicators:** oscillation, monotonic drift, any price < 0
**Test 2: Long-run stability (D-179)**
- Simulate 1,000 game-days with zero external events
- Measure maximum price drift from tick-0 equilibrium
- **Pass criterion:** Zero drift > ±2% over the full 1,000-tick run
- **Fail indicators:** slow drift accumulation, oscillation amplitude > 2%, any negative price
### Stockpile buffer test
**807-6:** Single-tick supply removal does not cause price explosion
```
procedure:
1. Run sim to equilibrium (100 ticks)
2. Inject a single tick of zero supply for one commodity at one node
3. Observe price at that node for next 5 ticks
Expected: price rises but does not exceed 10× base_price
Fail: price goes to infinity, NaN, or negative
```
### Regression markers
- Test 1 and Test 2 must pass with `--stability-check` before #808 begins
- If either test fails: do NOT mark #807 done, do NOT proceed to #808
- α/β must be documented (in source comments or README) so future tuning is traceable
---
## #808 — Currency Zones and Exchange Rates
**Spec ref:** D-171, D-172, D-174, D-181, D-186
**Assigned to:** Dudley
**Status:** backlog (blocked on #807)
### Currency zone model
**808-1:** Tractus↔Mark friction = ~3%
Verify in cross-zone trade: cost of a commodity transiting from a TRACTUS_PRIMARY to a
MARK_PRIMARY node is ~3% higher than same-zone transit at equal hop distance.
**808-2:** Zero friction within MARK_PRIMARY zones
Two nodes both in MARK_PRIMARY zones trading with each other incur no currency conversion cost
beyond the standard transport cost.
**808-3:** Sol is NOT a zone flag
```sql
SELECT COUNT(*) FROM star_systems WHERE currency_zone = 'SOL_PRIMARY';
-- Expected: 0 (Sol is shadow economy only, D-171)
```
**808-4:** Exchange rate is driven by trade balance, not hardcoded
The Tractus/Mark exchange rate must change between runs (or across ticks as trade flows change).
Hardcoded rates are a test failure.
### Signal vocabulary (D-181)
All 7 signals must be present in sim output per active node:
**808-5:**
```
1. price_current — present in output
2. price_trend — present in output (direction + rate)
3. trade_flow_volume — present in output
4. corporate_presence — present in output
5. stockpile_weeks — present in output
6. production_vs_baseline — present in output
7. official_coverage_ratio — present in output (derived from shadow_economy_intensity)
```
Edge case: For `official_coverage_ratio`, verify nodes with no shadow economy intensity
(TRACTUS_PRIMARY core systems) produce `official_coverage_ratio = 1.0` (formal economy
covers 100% of activity), not NULL.
### gate_energy_connected demand reduction (D-186)
**808-6:** Nodes with `gate_energy_connected = true` show `fusion_fuel` demand ~0.3× baseline
- Run sim on a TRACTUS_PRIMARY system (gate_energy_connected = true)
- Run sim on a MARK_PRIMARY system (gate_energy_connected = false)
- Compare `fusion_fuel` demand signal: on-grid node demand must be ~30% of off-grid
**808-7:** Industrial chain inputs are NOT reduced (D-186)
- `smelt_ore` still requires `fusion_fuel` at 0.3 coefficient regardless of gate energy
- `alloy_fabrication` still requires `fusion_fuel` at 0.2 coefficient
- `electronics_fabrication` still requires `fusion_fuel` at 0.2 coefficient
### D-179 Tests 3–4
**Test 3: Shock response (D-179)**
- Apply a single supply shock to one commodity at one node
- **Pass criteria:**
- Cascade propagates to dependent commodities (Leontief input scarcity visible)
- Recovery to within 10% of pre-shock price within 200 ticks
- No price explosions (no value > 100× base_price)
- No negative prices
- **Fail indicators:** runaway cascade, no recovery, shock isolated (no cascade = broken Leontief)
**Test 4: Cross-zone trade balance (D-179)**
- Increase trade volume across a TRACTUS_PRIMARY / MARK_PRIMARY boundary
- **Pass criteria:**
- Exchange rate adjusts in response (Tractus/Mark ratio changes)
- Rate re-stabilizes within 50 ticks
- Friction cost is visible (cross-zone goods 3% more expensive than same-zone equivalent)
- **Fail indicators:** no rate adjustment, infinite oscillation, rate diverges
All four D-179 tests must pass before #809 begins.
### Regression markers
- Tests 1 and 2 from #807 must still pass with currency layer active
- Tractus prices are still the numeraire (no price expressed in Mark or Sol units)
---
## #809 — Corporate Agent Behavior
**Spec ref:** D-175, D-178, D-180, D-181
**Assigned to:** Dudley
**Status:** backlog (blocked on #808, #799, #800)
### Corporate data loading
**809-1:** Corporations are loaded from DB, not hardcoded
```bash
grep -r "hardcoded\|\"Gate Corporation\"\|\"Vethara\"" tooling/econ-sim/src/
# Expected: corporation names should appear only in test fixtures or SQL queries,
# not as string literals in behavioral logic
```
**809-2:** Behavioral archetype template is read from TOML
```bash
ls wiki/economics/archetypes/behavioral.toml
# Expected: file exists (created by copy team per sprint briefing)
```
**809-3:** Each archetype is instantiated per corporation from corp_presence
```sql
-- Every corporation with corp_presence rows has a behavioral_archetype in DB
SELECT COUNT(*) FROM corp_presence cp
JOIN corporations c ON cp.corp_id = c.corp_id
WHERE c.behavioral_archetype IS NULL;
-- Expected: 0 (all corps with presence have an archetype assigned)
```
### Six behavioral archetypes (D-175)
**809-4:** All 6 archetypes are implemented
```bash
grep -r "Monopolist\|Distributor\|Producer\|Specialist\|Cooperative\|Intermediary" \
tooling/econ-sim/src/
# Expected: all 6 appear in behavioral logic, not just data loading
```
**809-5:** Archetypes produce distinguishably different behavior
Run stability check with only Monopolist corps vs. only Cooperative corps in a test system.
Price signals should differ between the two runs. If all archetypes produce identical output,
the behavioral differentiation is not implemented.
### EconEvent stub (D-180)
**809-6:** EconEvent struct compiles with all required fields
```bash
grep -r "EconEvent" tooling/econ-sim/src/
# Expected: struct definition with: target, effect, duration, visibility fields
```
**809-7:** Visibility variants are defined
```bash
grep -r "Global\|Proximate\|Disclosed\|Hidden" tooling/econ-sim/src/
# Expected: all 4 visibility variants present in the EconEvent type
```
**809-8:** Event handler is a no-op (not exercised in Phase 2)
Any call to `handle_event(EconEvent { ... })` should produce no observable simulation change.
The port must compile and accept events without crashing.
### Signal completeness (D-181)
**809-9:** All 7 signals produced per active node with agents active
Repeat 808-5 checks with corporate agents running. Agent behavior must not suppress or break
signal production.
**809-10:** `production_vs_baseline` reflects agent output vs seeded baseline
A Monopolist corp restricting supply should show `production_vs_baseline < 1.0`.
A Cooperative corp operating at full capacity should show `production_vs_baseline ≈ 1.0`.
### D-179 Full Test Suite with Agents Active
**This is the Phase 2 exit condition.**
```bash
tooling/econ-sim --stability-check
```
**809-11:** All four stability tests pass with corporate agents active:
- Test 1: Cold-start convergence ±5% within 100 game-days
- Test 2: Long-run stability ±2% over 1,000 game-days
- Test 3: Shock response, recovery within 200 ticks, no explosions or negatives
- Test 4: Cross-zone balance re-stabilizes within 50 ticks
If agents CAUSE instability that wasn't present in #808, the agent behavioral parameters need
tuning — this is a model bug, not a design decision. Investigate price-setting behavior before
concluding the architecture is wrong.
### Regression markers
- All prior D-179 tests still pass
- EconEvent handler does not crash on any valid input permutation
- `behavioral.toml` is a required file — binary must error on missing file with a clear message
---
## Checklist: Verification Order
| Order | Ticket | Gate condition | Who verifies |
|-------|--------|----------------|--------------|
| 1 | Pre-flight BF-1–7 | DB baseline valid | Hoshe, post #804 |
| 2 | #813 | Schema correct, defaults correct | Hoshe, when Tyre delivers |
| 3 | #805 | corp_presence populated, coverage valid, exits non-zero on failure | Hoshe, when Dudley delivers |
| 4 | #806 | Binary builds, CSV output correct, seeding in range | Hoshe, when Dudley delivers |
| 5 | #807 | Tests 1+2 pass `--stability-check` | Hoshe, when Dudley delivers |
| 6 | #808 | Tests 3+4 pass, all 7 signals present | Hoshe, when Dudley delivers |
| 7 | #809 | All 4 D-179 tests pass with agents active | Hoshe, when Dudley delivers |
**Phase 2 is complete only when step 7 passes.** Steps 5 through 7 are the formal exit gate
per D-179 and D-183.
---
## Appendix: Quick Reference — D-179 Stability Criteria
| Test | Condition | Pass threshold | Run at |
|------|-----------|---------------|--------|
| 1 | Cold-start convergence | ±5% of equilibrium within 100 game-days | #807 |
| 2 | Long-run stability | ±2% drift over 1,000 game-days, zero events | #807 |
| 3 | Shock response | Recovery within 200 ticks, no explosions, no negatives | #808 |
| 4 | Cross-zone trade balance | Re-stabilizes within 50 ticks | #808 |
All four must pass simultaneously with corporate agents active (#809) for Phase 2 sign-off.