Scenarios are YAML. The conductor loads them, executes phases in order, and applies the escalation policy between phases. Anything not in the interferer catalog (docs/interferers.md) is rejected at load.

name: worst-case-ramp
version: 1
band: 2.4ghz   # 2.4ghz | 5ghz | 6ghz — one scenario file tests one band;
               # bands are tested independently, graded on separate curves

dut:
  profile: glasses-plus-phone      # DUT profile; defines pass thresholds
  link: ble                        # link under test: ble | bredr | wifi

chamber:
  contained: true                  # shielded chamber; false = quiet room
  dut_path_loss_db: 60             # programmable attenuator setting

# Nodes available to phases. Params are validated against each node's schema.
nodes:
  inquiry-looper:    { enabled: true }
  ble-adv-spammer-1: { interval_ms: 20, tx_dbm: 8 }
  ble-adv-spammer-2: { interval_ms: 20, tx_dbm: 8 }
  ble-adv-spammer-3: { interval_ms: 100, tx_dbm: 8 }
  wifi-ap-farm:      { count: 4, forty_mhz: true }
  phonekey-sim:      { enabled: true }
  rf-observer:       { enabled: true }   # always on for citizenship scoring

phases:
  - name: baseline
    duration_s: 120
    nodes: [rf-observer]
    note: "DUT alone in chamber; measures its own good-citizen baseline."

  - name: busy-office
    duration_s: 300
    nodes: [rf-observer, ble-adv-spammer-3, wifi-ap-farm]
    node_overrides:
      wifi-ap-farm: { count: 2, forty_mhz: false }

  - name: parking-garage
    duration_s: 300
    nodes: [rf-observer, phonekey-sim, inquiry-looper, ble-active-scanner]

  - name: worst-case
    duration_s: 600
    nodes: [rf-observer, inquiry-looper, page-stormer, ble-adv-spammer-1,
            ble-adv-spammer-2, ble-adv-spammer-3, ble-active-scanner,
            ble-conn-saturator, auracast-tx, phonekey-sim,
            wifi-ap-farm, wifi-slow-client, probe-stormer, zigbee-chatter]

# Escalation: the conductor advances only while the DUT stays within
# thresholds. On breach it holds, then binary-searches the breaking point.
escalation:
  policy: ramp-while-healthy
  soak_s: 60                       # settle time before sampling each phase
  sample_window_s: 60
  on_breach: bisect                # hold | bisect | abort

# Abort guards: hard stops regardless of phase.
abort:
  max_disconnects: 10
  max_run_s: 3600

# Citizenship checks run in every phase via rf-observer.
citizenship:
  enabled: true
  spec: docs/dut-citizenship.md

Schema notes