# Quality & Adversarial Review Report: Milestone M2

- **Reviewer**: `reviewer_m2_progression_2`
- **Roles**: reviewer, critic
- **Target**: Milestone M2 Deliverables (Combat Integration, Concurrency & Robustness)
- **Implementer**: `worker_m2_progression_1`
- **Parent**: `orchestrator_4` (`6f4a2aa2-4315-4660-8cb7-8352a7220c95`)
- **Date**: 2026-10-01T03:08:00Z
- **Verdict**: **REQUEST_CHANGES**

---

## 1. Review Summary

**Verdict**: **REQUEST_CHANGES**

Milestone M2 implements the core features of `LevelProgressionService`, `level_progression_types.py`, and combat fatal damage hooks in `combat_engine.py`. The implementation demonstrates strong baseline engineering:
- `DamageEventResult` properly exposes `is_fatal: bool = False`.
- `attach_progression_service` integrates with `on_fatal_damage` with **zero circular dependencies** (uses duck typing without importing progression modules) and executes well within the < 25ms SLA (< 0.1ms synchronous memory operation).
- Memory-efficient immutable DTOs (`@dataclass(slots=True, frozen=True)`) and strict typing (Mypy passes with 0 errors).
- All hygiene constraints are respected (code length $\le 337$ lines, functions $\le 50$ lines).
- Unit tests (`pytest tests/unit/test_level_progression_service.py`) pass 33/33, E2E tests pass 47/47 (3 M3 tests xfailed as designed), and security audit passes 100%.
- Zero integrity violations (no hardcoded test outputs, no fake facades, no bypassing).

However, deep adversarial stress-testing focused on **combat integration, boundary handling, and concurrency** revealed **two Major functional defects** and **two Medium flaws** that must be remediated before production release:
1. **[Major] Corpse Overkill / Multi-Hit Fatal Damage Duplication**: In `server/world/combat_engine.py:175`, `is_fatal = (defender.current_hp <= 0.0)` triggers on *every* subsequent damage event delivered to an already dead defender (`current_hp == 0.0`). Multi-projectile attacks, AoE bursts, or DoT ticks hitting a dead player before respawn trigger `apply_death_penalty()` repeatedly for the same death, multiplying the penalty (e.g. 2x or 3x, losing 30-75% EXP instead of 15-25%).
2. **[Major] Level 100 Death Bypasses Death Count Tracking and Death Listeners**: In `server/world/level_progression_service.py:298-300`, an early return for `player.level >= 100` exits before incrementing `player.deaths_count` and before notifying `_death_listeners`. Combat deaths at Level 100 are completely swallowed: telemetry, respawn hooks, and hardcore permadeath listeners never fire.
3. **[Medium] Phantom Evasion i-Frame at Instance Initialization**: In `server/world/combat_engine.py:138-139`, `last_evasion_timestamp_ms` defaults to `0`. Any attack processed with `0 <= current_timestamp_ms <= 250` (instance start, ticks 0-7, or tests with timestamp 100) is falsely treated as an active i-frame dodge.
4. **[Medium] Level 100 Cumulative EXP Overflow**: When advancing from Level 99 to Level 100 with excess EXP, `cumulative_exp` is not clamped to the canonical Level 100 benchmark cap (`24,285,477,315`), violating the "zero overflow" requirement.
5. **[Minor] Negative Base EXP Input Sanitation**: `award_monster_exp()` does not sanitize `raw_exp >= 0`, allowing negative base EXP awards to reduce player `current_exp`.

---

## 2. Findings

### [Major] Finding 1: Corpse Overkill / Multi-Hit Fatal Damage Duplication
- **Where**: `server/world/combat_engine.py:174-189`
- **What**: In `calculate_damage()`:
  ```python
  defender.current_hp = max(0.0, defender.current_hp - final_damage)
  is_fatal = (defender.current_hp <= 0.0)
  ...
  if is_fatal and self.on_fatal_damage is not None:
      self.on_fatal_damage(result, attacker, defender)
  ```
  `is_fatal` is evaluated solely as `defender.current_hp <= 0.0`. If defender's HP was *already* 0.0 prior to this attack, `is_fatal` evaluates to `True` again.
- **Empirical Demonstration**:
  ```python
  # Hit 1 kills player (current_hp: 500 -> 0): deaths_count = 1
  # Hit 2 (second projectile of volley strikes corpse): deaths_count = 2!
  ```
- **Why**: Multi-projectile attacks (e.g. shotgunning spells), rapid melee combos, AoE explosions, or lingering ground DoT effects that strike the player's corpse before zone evacuation trigger `on_fatal_damage` multiple times. At Level 99, 2 strikes wipe 50% EXP; 4 strikes wipe 100% of accumulated EXP for a single death.
- **Suggestion**: Check whether the defender was alive before damage was applied:
  ```python
  was_alive = (defender.current_hp > 0.0)
  defender.current_hp = max(0.0, defender.current_hp - final_damage)
  is_fatal = was_alive and (defender.current_hp <= 0.0)
  ```

### [Major] Finding 2: Level 100 Death Bypasses Death Tracking and Death Listeners
- **Where**: `server/world/level_progression_service.py:298-300`
- **What**: In `apply_death_penalty()`:
  ```python
  player = self.get_player_state(player_id)
  if player.level >= 100:
      return self._build_death_result(player, 0, 0.0, player.current_exp)
  ```
  This early return returns before:
  1. `deaths_count = player.deaths_count + 1` is updated in `_players[player_id]`.
  2. `for listener in self._death_listeners: listener(result)` is dispatched.
- **Empirical Demonstration**:
  ```python
  s = LevelProgressionService()
  s.set_player_state('p100', level=100)
  s.add_death_penalty_listener(lambda r: calls.append(r))
  s.apply_death_penalty('p100')
  # Result: deaths_count: 0, listener called: 0
  ```
- **Why**: Level 100 characters become "ghosts" in death telemetry. Downstream systems relying on `_death_listeners` (such as Hardcore permadeath exile to Standard Realm, death screen UI, or respawn coordinates) never receive the death event.
- **Suggestion**: Remove the early return. `get_death_penalty_ratio(100)` already returns `0.0`. Allowing standard execution naturally yields `nominal_loss = 0`, `exp_lost = 0`, updates `deaths_count += 1`, and dispatches the death event to all listeners.

### [Medium] Finding 3: Phantom Evasion i-Frame at Instance Initialization
- **Where**: `server/world/combat_engine.py:30, 138-149`
- **What**: In `CombatActor`, `last_evasion_timestamp_ms: int = 0`.
  In `_check_special_damage_cases()`:
  ```python
  elapsed = current_timestamp_ms - defender.last_evasion_timestamp_ms
  if 0 <= elapsed <= defender.evasion_iframe_duration_ms:
      return DamageEventResult(..., is_evaded=True)
  ```
  If `current_timestamp_ms` is between `0` and `250` (e.g. game instance boot, tick 0-7, or test harnesses), `elapsed = current_timestamp_ms - 0` satisfies `0 <= elapsed <= 250`, granting complete invulnerability without `trigger_phantom_evasion()` ever being called.
- **Suggestion**: Initialize `last_evasion_timestamp_ms: int = -999999` (or `None`), or verify `defender.last_evasion_timestamp_ms > 0` before checking the elapsed window:
  ```python
  if defender.last_evasion_timestamp_ms > 0:
      elapsed = current_timestamp_ms - defender.last_evasion_timestamp_ms
      if 0 <= elapsed <= defender.evasion_iframe_duration_ms:
          ...
  ```

### [Medium] Finding 4: Cumulative EXP Overflows Canonical Cap at Level 100
- **Where**: `server/world/level_progression_service.py:253` and `111`
- **What**: In `_apply_exp_gain()`, `cumulative_exp` is updated as `player.cumulative_exp + exp_awarded` even when `curr_lvl >= 100`. It is never clamped to `self.get_benchmark(100).cumulative_exp` (`24,285,477,315`).
- **Why**: Reaching Level 100 with excess EXP causes cumulative EXP to overflow past the canonical lifetime cap by the rollover amount, violating the "zero overflow past Level 100" requirement.
- **Suggestion**:
  ```python
  b100 = self.get_benchmark(100)
  max_cum = b100.cumulative_exp if b100 else 0
  new_cum = min(max_cum, player.cumulative_exp + exp_awarded) if curr_lvl >= 100 else player.cumulative_exp + exp_awarded
  ```

### [Minor] Finding 5: Unclamped Negative `base_exp` Input
- **Where**: `server/world/level_progression_service.py:151-158`
- **What**: `award_monster_exp()` does not sanitize `raw_exp >= 0`. If `base_exp = -50` is passed, `current_exp` decrements into negative numbers (e.g. `-11`).
- **Suggestion**: Clamp `raw_exp = max(0, raw_exp)`.

### [Minor] Finding 6: Unsynchronized `_players` State Dictionary in Multi-Threaded Environments
- **Where**: `server/world/level_progression_service.py:36, 248, 307`
- **What**: `self._players[player_id]` is read and updated in multi-step non-atomic operations without an explicit lock (`threading.Lock`).
- **Context**: FreeExile's Zone Engine runs on a single-threaded 30Hz event loop (Actor model), where synchronous methods do not yield, making this safe in the current Zone Server. However, if invoked concurrently from background worker threads or thread pools, read-modify-write race conditions could cause lost updates on `current_exp`.
- **Suggestion**: Document single-thread actor isolation guarantee, or introduce `threading.RLock()` guarding player state mutations.

---

## 3. Verified Claims

| Claim from Worker M2 | Verification Method | Status | Notes |
|---|---|---|---|
| `DamageEventResult` has `is_fatal: bool = False` | `view_file` inspecting line 46 | **PASS** | Correctly default-valued |
| Zero circular dependencies | Import analysis across `combat_engine` & `level_progression_service` | **PASS** | `combat_engine` uses duck typing (`Any`) |
| Non-blocking execution (< 25ms SLA) | Code inspection & benchmarking | **PASS** | Microsecond in-memory arithmetic (< 0.05ms) |
| Level gap decay ($\Delta \le 5 \implies 1.0$, $\Delta = 10 \implies \le 0.05$) | `pytest test_level_progression_service.py -k TestLevelGapDecayFormula` | **PASS** | Matches $\exp(-0.60 \cdot (\Delta - 5))$ |
| Tiered death penalty ratios (0%, 5%, 10%, 15%, 25%, 0%) | `pytest test_level_progression_service.py -k TestTieredDeathPenalty` | **PASS** | Exact ratios across all level brackets |
| Safe floor at 0% (never de-level) | Empirical tests at 0% and 10% EXP at Lv 99 | **PASS** | `de_leveled = False`, `new_exp = 0` |
| Unit tests passing | `pytest tests/unit/test_level_progression_service.py -k "test_combat or test_death"` | **PASS** | 20 passed, 13 deselected in 0.17s |
| E2E test suite passing | `pytest tests/e2e/test_level_progression_e2e.py -v` | **PASS** | 47 passed, 3 xfailed (M3 pending) in 0.38s |
| Independent security audit passing | `python tools/security/run_independent_security_audit.py` | **PASS** | 0 Critical, 0 High vulnerabilities |
| Mypy strict type checking | `python -m mypy ...` | **PASS** | Success: no issues found in 4 source files |
| Code & doc hygiene | `python tools/lint/check_code_and_doc_hygiene.py --strict` | **PASS** | Compliant with Hard Cap (no function > 50 lines) |
| Corpse overkill immunity | Adversarial multi-hit simulation | **FAIL** | Multiple death penalties applied on corpse hits |
| Level 100 death listener dispatch | Adversarial Level 100 death test | **FAIL** | Swallowed by early return; 0 listeners notified |
| Level 100 cumulative EXP cap | Adversarial Level 99->100 transition | **FAIL** | Overflows past benchmark cumulative EXP cap |

---

## 4. Adversarial Challenges

### Challenge 1: Multi-Projectile Corpse Overkill Penalty Stacking
- **Assumption Challenged**: Fatal damage detection only triggers once per character death.
- **Attack Scenario**: A monster casts a 3-projectile fire strike. Projectile 1 reduces the player to 0 HP. Projectiles 2 and 3 strike the player in the same frame before the player entity despawns.
- **Observed Behavior**: `is_fatal` is evaluated as `True` 3 times. `apply_death_penalty()` is invoked 3 times. A Level 90 player loses $15\% \times 3 = 45\%$ of their level bar.
- **Blast Radius**: Severe, unfair progression loss; player wipes out days of endgame farming due to multi-hit overkill.
- **Mitigation**: Add `was_alive = (defender.current_hp > 0.0)` guard in `calculate_damage()`.

### Challenge 2: Phantom Evasion at Server Initialization
- **Assumption Challenged**: Evasion i-frames only activate after calling `trigger_phantom_evasion()`.
- **Attack Scenario**: Player is attacked at `timestamp = 100ms` immediately after instance connection.
- **Observed Behavior**: Attack is recorded as `is_evaded: True`, `final_damage: 0.0`, despite player never dodging.
- **Blast Radius**: Unintended invulnerability during initial 250ms of gameplay or tests starting at low timestamps.
- **Mitigation**: Require `defender.last_evasion_timestamp_ms > 0` before checking `elapsed <= 250`.

### Challenge 3: Level 100 Hardcore Permadeath Bypass
- **Assumption Challenged**: Level 100 players dying in Hardcore mode are exiled to the Standard Realm.
- **Attack Scenario**: A Level 100 character on the Hardcore Realm dies to a pinnacle boss. Hardcore realm watcher is attached via `add_death_penalty_listener()`.
- **Observed Behavior**: Early return skips `_death_listeners`. Realm watcher is never notified. Character remains alive on Hardcore Realm!
- **Blast Radius**: Complete circumvention of Hardcore Permadeath rules for Level 100 characters.
- **Mitigation**: Remove early return; allow standard event dispatch with `exp_lost = 0`.

---

## 5. Required Remediations for Approval

Before Milestone M2 can be approved, worker must implement the following targeted fixes:
1. In `server/world/combat_engine.py`:
   - Guard `is_fatal = was_alive and (defender.current_hp <= 0.0)` where `was_alive = (defender.current_hp > 0.0)`.
   - Guard i-frame check with `if defender.last_evasion_timestamp_ms > 0:`.
2. In `server/world/level_progression_service.py`:
   - Remove early return on line 299 of `apply_death_penalty()` so `deaths_count` increments and `_death_listeners` fire for Level 100 deaths.
   - Clamp `cumulative_exp` to `self.get_benchmark(100).cumulative_exp` when `curr_lvl >= 100`.
   - Sanitize `raw_exp = max(0, raw_exp)` in `award_monster_exp()`.
3. Add unit test assertions in `tests/unit/test_level_progression_service.py` verifying corpse overkill protection, Level 100 death listener dispatch, and zero cumulative EXP overflow.
