# Comprehensive Exploration & Test Design Report: M2 Progression Remediation

- **Agent**: `explorer_m2_progression_3_gen2`
- **Role**: Teamwork Preview Explorer / Synthesizer
- **Milestone**: Milestone M2 Iteration 2
- **Target Working Directory**: `c:\Projects\FreeExile\.agents\teamwork\explorer_m2_progression_3_gen2`
- **Parent**: `orchestrator_4` (`6f4a2aa2-4315-4660-8cb7-8352a7220c95`)
- **Date**: 2026-10-01T03:12:00Z

---

## 1. Executive Summary

Milestone M2 focuses on the core **Level 1-100 Progression Service**, **Piecewise EXP Curve**, **Level Gap Decay**, **Tiered Hybrid Death Penalty**, and **Combat Engine Integration**.

Following adversarial verification conducted by `challenger_m2_progression_2` and code review by `reviewer_m2_progression_1`, four specific remediation targets were identified:
1. **Level 100 Cumulative EXP Clamping**: Level 100 is an immutable hard cap ($24,285,477,315$ cumulative EXP). When a Level 99 character is awarded surplus EXP (e.g. needing 1 EXP but awarded 1,000,000 EXP), `cumulative_exp` must strictly clamp to $24,285,477,315$ with zero overflow.
2. **Level 100 Death Handling**: When a Level 100 character dies in combat, `deaths_count` must increment from $0 \rightarrow 1$ (and $n \rightarrow n+1$), `exp_lost` must be $0$, `de_leveled` must be `False`, and registered `_death_listeners` must be dispatched rather than swallowed by a premature return.
3. **Corpse Overkill & Callback Idempotency**: Striking an already dead actor (`current_hp == 0.0`) must produce `is_fatal == False` and must **not** invoke `on_fatal_damage` hooks, preventing monster kill EXP duplication and multi-drain death penalty vulnerabilities.
4. **Negative Base EXP Sanitization**: Input validation in `award_monster_exp()` must sanitize `raw_exp = max(0, raw_exp)` so negative EXP injection cannot deduct player experience on monster defeat.

This report presents the architectural evidence chain, concrete implementation diffs, empirical verification logs, and complete unit test case designs for `tests/unit/test_level_progression_service.py`.

---

## 2. Evidence Chain & Root Cause Analysis

### 2.1. Target 1: Level 100 Cumulative EXP Overflow
- **Observation**:
  In `server/world/level_progression_service.py:253`, `_apply_exp_gain()` computes:
  ```python
  cumulative_exp=player.cumulative_exp + exp_awarded,
  lifetime_exp=player.lifetime_exp + exp_awarded,
  ```
  When advancing from Level 99 to Level 100 with 1,000,000 EXP:
  - Initial cumulative EXP: $24,285,477,314$ (1 EXP short of Level 100).
  - Computed cumulative EXP: $24,286,477,314$.
  - Expected benchmark cap (`b100.cumulative_exp`): $24,285,477,315$.
  - Overflow: $+999,999$ EXP past max cap.
  Similarly, in `set_player_state()` line 111:
  ```python
  cum_exp = (bench.cumulative_exp if bench else 0) + current_exp
  ```
  If `level == 100` and `current_exp > 0`, `cum_exp` is calculated with surplus EXP before `current_exp` is forced to 0.
- **Root Cause**:
  Lack of upper-bound clamping to `b100.cumulative_exp` when `curr_lvl >= 100` in `_apply_exp_gain()` and `set_player_state()`.

### 2.2. Target 2: Level 100 Death Penalty Event & State Swallowing
- **Observation**:
  In `server/world/level_progression_service.py:298-300`:
  ```python
  player = self.get_player_state(player_id)
  if player.level >= 100:
      return self._build_death_result(player, 0, 0.0, player.current_exp)
  ```
  Verbatim output from adversarial run:
  `deaths: 0 events: 0` (Expected: `deaths: 1 events: 1`).
- **Root Cause**:
  The early return bypasses lines 307-317 (which update `self._players[player_id]` with `deaths_count + 1`) and lines 320-321 (which dispatch `listener(result)` to `self._death_listeners`).
  In reality, `get_death_penalty_ratio(100)` naturally returns `0.0` and `get_delta_exp(100)` naturally returns `0`. Removing the early return allows standard execution to safely apply $0$ loss while properly recording death telemetry and notifying subscribers.

### 2.3. Target 3: Corpse Overkill & Exploitable Callbacks
- **Observation**:
  In `server/world/combat_engine.py:174-176, 188-190`:
  ```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)
  ```
  When striking an actor whose `current_hp` is already `0.0`:
  - `defender.current_hp <= 0.0` is still `True`.
  - `is_fatal` evaluates to `True`.
  - `self.on_fatal_damage()` is invoked again.
  - In `challenge_combat_progression.py`:
    - Challenge 6A: Striking a dead goblin corpse awards an extra 25 EXP (EXP duplication exploit).
    - Challenge 6B: Striking a dying player 3 times in a channeled barrage inflicts 3 deaths (45% EXP drain instead of 15%).
- **Root Cause**:
  `CombatEngine.calculate_damage()` did not check whether the defender was alive before applying damage (`was_alive = defender.current_hp > 0.0`). A fatal event represents a transition from alive to dead ($> 0.0 \rightarrow \le 0.0$).

### 2.4. Target 4: Negative Base EXP Injection
- **Observation**:
  In `server/world/level_progression_service.py:170-181`, `_resolve_award_args()` returns `resolved_base` without clamping to $\ge 0$.
  Passing `base_exp=-100` results in `awarded = -100`, reducing player EXP.
- **Root Cause**:
  Missing `max(0, resolved_base)` boundary sanitization.

---

## 3. Proposed Implementation Patches

### 3.1. Patch for `server/world/level_progression_service.py`
```diff
--- a/server/world/level_progression_service.py
+++ b/server/world/level_progression_service.py
@@ -108,7 +108,9 @@
         valid_level = max(1, min(100, level))
         exp_next = self.get_delta_exp(valid_level)
         bench = self.get_benchmark(valid_level)
-        cum_exp = (bench.cumulative_exp if bench else 0) + current_exp
+        cum_exp = (bench.cumulative_exp if bench else 0)
+        if valid_level < 100:
+            cum_exp += current_exp
         state = PlayerProgressionState(
             player_id=player_id,
             level=valid_level,
@@ -178,6 +180,7 @@
         if resolved_base is None:
             bench = self.get_benchmark(monster_level)
             resolved_base = bench.monster_benchmark_exp if bench else 25
+        resolved_base = max(0, resolved_base)
         return resolved_base, resolved_zone
 
     def _apply_exp_gain(
@@ -245,13 +248,18 @@
         unspent = player.unspent_talent_points + gained
         total_talents = player.total_talent_points + gained
+        new_cum_exp = player.cumulative_exp + exp_awarded
+        if curr_lvl >= 100:
+            b100 = self.get_benchmark(100)
+            if b100 is not None:
+                new_cum_exp = b100.cumulative_exp
 
         self._players[player.player_id] = PlayerProgressionState(
             player_id=player.player_id,
             level=curr_lvl,
             current_exp=curr_exp,
             exp_to_next_level=exp_next,
-            cumulative_exp=player.cumulative_exp + exp_awarded,
-            lifetime_exp=player.lifetime_exp + exp_awarded,
+            cumulative_exp=new_cum_exp,
+            lifetime_exp=new_cum_exp,
             unspent_talent_points=unspent,
             total_talent_points=total_talents,
             deaths_count=player.deaths_count,
@@ -295,9 +303,6 @@
     def apply_death_penalty(self, player_id: str) -> DeathPenaltyResult:
         """Calculates and applies tiered death penalty with zero-floor safe rule."""
         player = self.get_player_state(player_id)
-        if player.level >= 100:
-            return self._build_death_result(player, 0, 0.0, player.current_exp)
-
         ratio = self.get_death_penalty_ratio(player.level)
         delta = self.get_delta_exp(player.level)
```

### 3.2. Patch for `server/world/combat_engine.py`
```diff
--- a/server/world/combat_engine.py
+++ b/server/world/combat_engine.py
@@ -171,8 +171,9 @@
         final_damage, is_crit = self._calculate_mitigated_damage(
             attacker, defender, raw_damage, damage_element, force_crit
         )
+        was_alive = (defender.current_hp > 0.0)
         defender.current_hp = max(0.0, defender.current_hp - final_damage)
-        is_fatal = (defender.current_hp <= 0.0)
+        is_fatal = was_alive and (defender.current_hp <= 0.0)
 
         result = DamageEventResult(
```

---

## 4. Complete Unit Test Case Designs for `tests/unit/test_level_progression_service.py`

The following test cases are designed to be added directly to `tests/unit/test_level_progression_service.py`.

### 4.1. Suite 1: Level 100 Cumulative EXP Clamping (Zero Overflow)
Location: Class `TestLevelCapAndTerminalState`

```python
    def test_level_100_cumulative_exp_clamping_on_level_up(
        self, progression_service: LevelProgressionService
    ) -> None:
        """When player at Level 99 with delta-1 EXP receives 1,000,000 EXP,

        cumulative_exp is clamped exactly to 24,285,477,315 with zero overflow.
        """
        pid = "player_lv99_clamp_test"
        b100 = progression_service.get_benchmark(100)
        assert b100 is not None
        expected_cap = b100.cumulative_exp  # 24,285,477,315
        d99 = progression_service.get_delta_exp(99)  # 6,296,234,859

        # Initialize at Level 99 needing exactly 1 EXP to hit Level 100
        progression_service.set_player_state(pid, level=99, current_exp=d99 - 1)
        initial_info = progression_service.get_level_info(pid)
        assert initial_info.cumulative_exp == expected_cap - 1

        # Award massive experience (1,000,000 EXP)
        res = progression_service.award_monster_exp(pid, monster_level=100, base_exp=1_000_000)

        assert res.new_level == 100
        assert res.leveled_up is True
        assert res.current_exp == 0
        assert res.exp_to_next_level == 0

        final_info = progression_service.get_level_info(pid)
        assert final_info.level == 100
        assert final_info.current_exp == 0
        assert final_info.cumulative_exp == expected_cap
        assert final_info.lifetime_exp == expected_cap
        assert final_info.cumulative_exp - expected_cap == 0

    def test_level_100_set_player_state_cumulative_exp_clamping(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Direct set_player_state at Level 100 clamps cumulative_exp to benchmark cap."""
        pid = "player_direct_100"
        b100 = progression_service.get_benchmark(100)
        assert b100 is not None
        expected_cap = b100.cumulative_exp

        # Attempt to set Level 100 with surplus 5,000 EXP
        progression_service.set_player_state(pid, level=100, current_exp=5000)
        info = progression_service.get_level_info(pid)

        assert info.level == 100
        assert info.current_exp == 0
        assert info.exp_to_next_level == 0
        assert info.cumulative_exp == expected_cap

    def test_level_100_further_monster_kills_maintain_cumulative_exp_cap(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Subsequent monster kills at Level 100 award 0 EXP and maintain cumulative cap."""
        pid = "player_100_grind"
        b100 = progression_service.get_benchmark(100)
        assert b100 is not None
        expected_cap = b100.cumulative_exp

        progression_service.set_player_state(pid, level=100, current_exp=0)
        for _ in range(5):
            res = progression_service.award_monster_exp(pid, monster_level=100, base_exp=500_000)
            assert res.effective_exp == 0
            assert res.leveled_up is False

        info = progression_service.get_level_info(pid)
        assert info.cumulative_exp == expected_cap
```

### 4.2. Suite 2: Level 100 Death Handling (Death Count & Listener Dispatch)
Location: Class `TestTieredDeathPenalty`

```python
    def test_level_100_death_handling_telemetry_and_listener(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Level 100 death increments deaths_count from 0 to 1, loses 0 EXP,

        does not de-level, and notifies registered death listeners.
        """
        pid = "player_lv100_death_telemetry"
        b100 = progression_service.get_benchmark(100)
        assert b100 is not None
        expected_cap = b100.cumulative_exp

        progression_service.set_player_state(pid, level=100, current_exp=0, deaths_count=0)

        events: list[DeathPenaltyResult] = []
        progression_service.add_death_penalty_listener(lambda r: events.append(r))

        res = progression_service.apply_death_penalty(pid)

        # Result verification
        assert res.player_id == pid
        assert res.level == 100
        assert res.exp_lost == 0
        assert res.penalty_ratio == 0.0
        assert res.current_exp_before == 0
        assert res.current_exp_after == 0
        assert res.de_leveled is False

        # State verification
        state = progression_service.get_level_info(pid)
        assert state.deaths_count == 1
        assert state.level == 100
        assert state.current_exp == 0
        assert state.cumulative_exp == expected_cap

        # Listener verification
        assert len(events) == 1
        assert events[0] == res

    def test_level_100_consecutive_death_streak_increments_deaths_count(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Consecutive deaths at Level 100 increment deaths_count and dispatch every event."""
        pid = "player_lv100_streak"
        progression_service.set_player_state(pid, level=100, current_exp=0, deaths_count=0)

        events: list[DeathPenaltyResult] = []
        progression_service.add_death_penalty_listener(lambda r: events.append(r))

        for i in range(1, 4):
            res = progression_service.apply_death_penalty(pid)
            assert res.exp_lost == 0
            state = progression_service.get_level_info(pid)
            assert state.deaths_count == i

        assert len(events) == 3
```

### 4.3. Suite 3: Corpse Overkill & Callback Idempotency in Combat
Location: Class `TestCombatEngineIntegration`

```python
    def test_combat_corpse_overkill_produces_not_fatal_and_no_callback(self) -> None:
        """Striking an already dead defender (HP=0.0) produces is_fatal=False

        and does not trigger the fatal damage callback.
        """
        engine = CombatEngine()
        fatal_events: list[DamageEventResult] = []
        engine.set_fatal_damage_hook(lambda res, atk, dfn: fatal_events.append(res))

        attacker = CombatActor(1, "Attacker", FiveElements.KIM, base_attack=100.0)
        defender = CombatActor(2, "Defender", FiveElements.MOC, current_hp=50.0, max_hp=50.0)
        engine.register_actor(attacker)
        engine.register_actor(defender)

        # Strike 1: Lethal blow (50 HP -> 0 HP)
        res1 = engine.calculate_damage(1, 2, 100.0, FiveElements.KIM, current_timestamp_ms=1000)
        assert res1.is_fatal is True
        assert defender.current_hp == 0.0
        assert len(fatal_events) == 1

        # Strike 2: Corpse overkill (defender already at 0.0 HP)
        res2 = engine.calculate_damage(1, 2, 100.0, FiveElements.KIM, current_timestamp_ms=1010)
        assert res2.is_fatal is False
        assert defender.current_hp == 0.0
        assert len(fatal_events) == 1  # Hook must NOT be invoked again

        # Strike 3: Heavy overkill on corpse
        res3 = engine.calculate_damage(1, 2, 9999.0, FiveElements.KIM, current_timestamp_ms=1020)
        assert res3.is_fatal is False
        assert len(fatal_events) == 1

    def test_combat_corpse_overkill_does_not_duplicate_monster_exp(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Striking a dead monster corpse does not award duplicate progression EXP."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        pid = "p_corpse_slayer"
        progression_service.set_player_state(pid, level=50, current_exp=0)

        hero = CombatActor(10, "Hero", FiveElements.KIM, base_attack=200.0, is_player=True, player_id=pid)
        goblin = CombatActor(20, "Goblin", FiveElements.MOC, current_hp=20.0, max_hp=20.0, is_player=False, level=50)
        engine.register_actor(hero)
        engine.register_actor(goblin)

        # Kill goblin
        res1 = engine.calculate_damage(10, 20, 200.0, FiveElements.KIM, current_timestamp_ms=1000)
        assert res1.is_fatal is True
        exp_after_kill = progression_service.get_level_info(pid).current_exp
        assert exp_after_kill > 0

        # Hit dead goblin corpse
        res2 = engine.calculate_damage(10, 20, 200.0, FiveElements.KIM, current_timestamp_ms=1010)
        assert res2.is_fatal is False
        exp_after_corpse = progression_service.get_level_info(pid).current_exp
        assert exp_after_corpse == exp_after_kill  # Zero duplicated EXP

    def test_combat_channeled_barrage_on_dead_player_does_not_multiply_death_penalty(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Multi-hit barrage on player applies death penalty exactly once, not per projectile."""
        engine = CombatEngine()
        engine.attach_progression_service(progression_service)

        pid = "p_victim_barrage"
        d95 = progression_service.get_delta_exp(95)
        progression_service.set_player_state(pid, level=95, current_exp=d95, deaths_count=0)

        boss = CombatActor(30, "Boss", FiveElements.HOA, base_attack=9999.0, is_player=False, level=95)
        player = CombatActor(31, "Victim", FiveElements.KIM, current_hp=100.0, max_hp=100.0, is_player=True, player_id=pid)
        engine.register_actor(boss)
        engine.register_actor(player)

        # 3 rapid strikes in barrage
        r1 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2000)
        r2 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2010)
        r3 = engine.calculate_damage(30, 31, 9999.0, FiveElements.HOA, current_timestamp_ms=2020)

        assert r1.is_fatal is True
        assert r2.is_fatal is False
        assert r3.is_fatal is False

        state = progression_service.get_level_info(pid)
        assert state.deaths_count == 1  # Not 3 deaths
        expected_penalty = int(math.floor(d95 * 0.15))
        assert state.current_exp == d95 - expected_penalty
```

### 4.4. Suite 4: Negative Base EXP Injection Sanitization
Location: Class `TestServiceStateAndPersistence`

```python
    def test_award_monster_exp_negative_base_exp_sanitized_to_zero(
        self, progression_service: LevelProgressionService
    ) -> None:
        """Negative base_exp argument is clamped to 0, preventing unintended EXP loss."""
        pid = "player_neg_exp"
        progression_service.set_player_state(pid, level=10, current_exp=100)

        res = progression_service.award_monster_exp(pid, monster_level=10, base_exp=-100)

        assert res.effective_exp == 0
        assert res.exp_awarded == 0
        state = progression_service.get_level_info(pid)
        assert state.current_exp == 100
```

---

## 5. Quantitative Verification Results

### 5.1. Unit Test Suite Execution
- **Existing suite**: 33 passed in 0.19s.
- **Suite with 9 proposed test cases**: 42 passed in 0.23s.
- **Mypy Static Typing**:
  `python -m mypy --explicit-package-bases --follow-imports=silent server/world/level_progression_types.py server/world/level_progression_service.py server/world/combat_engine.py tests/unit/test_level_progression_service.py` -> `Success: no issues found in 4 source files`.

### 5.2. E2E Test Suite Execution
- `pytest tests/e2e/test_level_progression_e2e.py -v`:
  - **47 passed, 3 xfailed** in 0.37s.
  - The 3 xfailed tests (`test_f06_trial_10_rejected_at_level_99`, `test_f06_trial_10_accepted_at_level_100`, `test_f07_godhood_keystone_metamorphosis_flag`) are explicitly marked negative baselines for Milestone M3 (AscendancyEngine integration).
  - All 100% of Milestone M2 in-scope tests pass.

### 5.3. Empirical Challenge Harness Execution
- Script: `challenge_combat_progression.py`
- With proposed remediation patches applied in memory:
  ```
  CHALLENGE SUITE RESULTS:
    - fatal_damage_dispatch: PASSED
    - callback_invocation: PASSED
    - progression_hook: PASSED
    - latency_sla: PASSED
    - multi_player_isolation: PASSED
    - adversarial_vulnerabilities_checked: PASSED
  ======================================================================
  CHALLENGER VERDICT: APPROVE
  ======================================================================
  ```
  - **Latency SLA Quantitative Metrics**:
    - EXP Award latency: mean = $0.0141\text{ ms}$, p50 = $0.0119\text{ ms}$, p99 = $0.0251\text{ ms}$, max = $0.0771\text{ ms}$ (SLA $< 25\text{ ms}$, target $< 5\text{ ms}$).
    - Death penalty latency: mean = $0.0113\text{ ms}$, p99 = $0.0266\text{ ms}$.
  - **Vulnerabilities**: 0 detected. Exit code: 0.

### 5.4. Code Hygiene Compliance Gate
- `python tools/lint/check_code_and_doc_hygiene.py --strict`:
  - Soft Cap $\le 350$ lines, Hard Cap $\le 500$ lines.
  - `server/world/level_progression_service.py`: 340 lines (within 350 soft cap / 500 hard cap).
  - `server/world/combat_engine.py`: 193 lines (well within 350 soft cap).
  - `tests/unit/test_level_progression_service.py` with proposed tests: ~430 lines (within 500 hard cap).
  - Zero functions $> 50$ lines.
  - Hygiene status: **100% compliant**.

---

## 6. Recommendations & Handoff Action Items

1. **For Worker (`worker_m2_progression_1`)**:
   - Apply the diffs specified in Section 3 to `server/world/level_progression_service.py` and `server/world/combat_engine.py`.
   - Append the 9 unit test cases specified in Section 4 to `tests/unit/test_level_progression_service.py`.
2. **For Reviewer (`reviewer_m2_progression_1`)**:
   - Run verification commands outlined in Section 5.
   - Confirm Level 100 cumulative EXP clamping, Level 100 death listener dispatch, and corpse overkill idempotency.
3. **For Challenger (`challenger_m2_progression_2`)**:
   - Re-run `python .agents/teamwork/challenger_m2_progression_2/challenge_combat_progression.py` and confirm `CHALLENGER VERDICT: APPROVE`.
