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

26 KiB
Raw Blame History

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-171D-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.

-- 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

-- 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.

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

-- 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

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

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)

# 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.41.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.41.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.71.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.71.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

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:

grep -r "0\.03" tooling/econ-sim/src/
grep -r "0\.4" tooling/econ-sim/src/

Floyd-Warshall startup performance

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)

-- 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 700820 as the spec estimate may not match actual DB state)

807-2: Passive producer count is approximately 240

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 512% per hop (inter-system) 807-4: Orbital edges cost 13% (intra-system) 807-5: Transport costs are applied to commodity prices, not abstracted away

D-179 Stability Tests

tooling/econ-sim --stability-check

All four tests are run by this flag (Tests 12 in #807, Tests 34 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

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 34

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

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

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

-- 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

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

grep -r "EconEvent" tooling/econ-sim/src/
# Expected: struct definition with: target, effect, duration, visibility fields

809-7: Visibility variants are defined

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.

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-17 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.