# ARCHITECTURAL ANALYSIS: MILESTONE 1 REMEDIATION & MILESTONE 2 INTEGRATION

**Author**: `explorer_rem_3` (Explorer / Architecture Investigator)  
**Parent Agent**: `orchestrator_22` (`34037784-62e1-41f8-bfe6-912696fdec14`)  
**Scope**: Holistic analysis of Milestone 1 remediation, interface contracts with Cocos Creator 3.8.x `SpriteAtlasRenderer.ts`, and step-by-step remediation plan for the Worker  
**Date**: 2026-10-04T13:22:00Z  

---

## 1. Executive Summary

Milestone 1 review conducted by `reviewer_m1_2` returned **REQUEST_CHANGES** due to two specific defects:
1. **Scaffold UV Coordinate Overflow**: In `tools/asset_pipeline/monster_character_pipeline_scaffold.py`, UV coordinates were normalized against a hardcoded 2048px height while laying out 48 monster rows (7,680px) and 80 character rows (15,360px), causing up to **$V = 3.75$** for monsters and **$V = 7.50$** for exile characters.
2. **Codebase Hygiene Limit Breach**: `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` stood at 574 lines, breaching the 500-line Hard Cap in `GEMINI.md` and failing `python tools/lint/check_code_and_doc_hygiene.py --strict`.

This investigation establishes the holistic bridge between the Milestone 1 remediation and Milestone 2 (`SpriteAtlasRenderer.ts` zero-allocation refactor and flexible manifest adapter). It confirms that:
- The 8-column layout (`cols: 8`) and bottom-center anchor pivot `[0.5, 0.90]` remain invariant across the entire engine.
- Dimensioning the Power-of-Two texture height to $8192$ (monsters) and $16384$ (characters) completely eliminates UV overflow, placing all coordinates strictly within $[0.0, 1.0]$ without disrupting sequential grid indexing.
- Modularizing `test_vfx_texture_atlas_pipeline_e2e.py` by extracting fixtures/catalogs or splitting across tiers satisfies the 500-line Hard Cap, resolves the hygiene gate, and closes the test assertion blind spot.

---

## 2. Analysis Item 1: How Monster & Character Manifests are Consumed by `SpriteAtlasRenderer.ts`

### 2.1 Current Architecture of `SpriteAtlasRenderer.ts`
Located at `client/cocos/assets/scripts/animation/SpriteAtlasRenderer.ts` (132 lines):
- **Grid Assumptions**: Enforces an 8-column layout (`public cols: number = 8;`), standard bottom-center pivot `[0.5, 0.90]`, and dimensions `frameWidth: 160`, `frameHeight: 192` (characters) or `160x160` (monsters).
- **Current `getFrameSourceRect` Implementation**:
  ```typescript
  const colIndex = frameIndex % this.cols;
  const sx = colIndex * this.frameWidth;
  const sy = rowIndex * this.frameHeight;
  return new Rect(sx, sy, this.frameWidth, this.frameHeight);
  ```
  Notice that:
  - `colIndex = frameIndex % this.cols` assumes that for every animation clip, frame 0 starts at column 0 (`sx = 0`).
  - `sy = rowIndex * this.frameHeight` calculates the pixel vertical offset based on `rowIndex`.
- **Current `getFrameUVs` Implementation**:
  ```typescript
  const rect = this.getFrameSourceRect(clipName, frameIndex, direction, manifest);
  const u0 = rect.x / textureWidth;
  const v0 = rect.y / textureHeight;
  const u1 = (rect.x + rect.width) / textureWidth;
  const v1 = (rect.y + rect.height) / textureHeight;
  return [u0, v0, u1, v1];
  ```

### 2.2 Integration with Milestone 2 Features
In `PROJECT.md`, Milestone 2 specifies two core upgrades to `SpriteAtlasRenderer.ts`:
1. **Feature 10: Zero-Allocation `SpriteAtlasRenderer`**:
   - Replaces `return new Rect(...)` in `getFrameSourceRect` with pre-allocated scratch objects (`private _scratchRect: Rect = new Rect();`) or `out` parameters to maintain 120 FPS ProMotion without GC pause spikes.
2. **Feature 11: Flexible Manifest Adapter**:
   - Supports **two lookup paths**:
     - *Sequential Path*: For clips without explicit coordinates, computes `(sx, sy)` from grid rows/cols.
     - *Explicit Manifest Path*: For clips with explicit metadata (`manifest.clips[clipName].frames[frameIndex]`), reads `frame.x`, `frame.y`, `frame.w`, `frame.h`, and `frame.uv` directly.
   - When explicit `frame.uv` is provided, `getFrameUVs` directly reads `[u0, v0, u1, v1]`, bypassing runtime division.

### 2.3 The Impact of Out-of-Bounds UV Coordinates
- If `frame.uv` contains vertical coordinates $V \in [1.0, 7.5]$, sending these values to Cocos Creator 3.8.x render quads or Apple Metal vertex shaders causes:
  - Metal texture sampler wrapping (`MTLSamplerAddressModeClampToEdge` clamps to row 12; `Repeat` samples wrong animation actions).
  - Broken sprite display (distorted frames, invisible entities, or visual noise).
- By correctly scaling UVs to the actual Power-of-Two texture height ($H=8192$ or $H=16384$), all coordinates satisfy $0.0 \le u_0, v_0, u_1, v_1 \le 1.0$, enabling the Flexible Manifest Adapter to function seamlessly.

---

## 3. Analysis Item 2: Interface Contract Adherence & Engine Compatibility

### 3.1 Interface Contract Schema in `PROJECT.md`
The interface contract defined in `PROJECT.md` (lines 46-69) requires:
```json
{
  "name": "<atlas_name>",
  "textureWidth": <number>,
  "textureHeight": <number>,
  "frameWidth": <number>,
  "frameHeight": <number>,
  "defaultPivot": [0.5, 0.90],
  "clips": {
    "<clip_key>": {
      "frameCount": <number>,
      "fps": <number>,
      "loop": <boolean>,
      "frames": [
        {"index": 0, "x": <px>, "y": <px>, "w": <px>, "h": <px>, "pivot": [0.5, 0.90], "uv": [u0, v0, u1, v1]}
      ]
    }
  }
}
```

### 3.2 Dual-Naming Property Compatibility
The scaffold engine generates manifests containing dual-naming aliases:
- `textureWidth` AND `texture_width`
- `frameWidth` AND `frame_width`
- `defaultPivot` AND `pivot`
- `clips` AND `frames`
This dual-naming contract ensures zero breakage:
- **Cocos 3.8.x TypeScript** reads `textureWidth` / `frameWidth` / `defaultPivot`.
- **Legacy WebApp JS** reads `texture_width` / `frame_width` / `pivot`.
- Both runtime engines can consume the manifest without regression.

### 3.3 Engine & Test Expectation Verification
1. **No Hardcoded 2048px Height Assumption**:
   - Static analysis across all test files (`tests/`) reveals that only `savage_primal_skills_vfx_atlas.json` is asserted to be $2048 \times 2048$.
   - No test, shader, or TypeScript component requires monster or character manifests to be square or clamped to $2048$ height.
2. **TypeScript Compilation (`npm run build:web`)**:
   - `tsc --noEmit` validates `SpriteAtlasRenderer.ts` against `AtlasManifest`. Manifest files in `assets/resources/` are loaded dynamically at runtime via Cocos `resources.load()`, so changing manifest dimensions does not impact compilation.

---

## 4. Analysis Item 3: Mathematical Root Cause & Fix for `monster_character_pipeline_scaffold.py`

### 4.1 The Defect
In `monster_character_pipeline_scaffold.py`:
- Lines 82 & 153 declared `"textureHeight": 2048`.
- Lines 107 & 178 calculated:
  `"uv": [x / 2048.0, y / 2048.0, (x + fw) / 2048.0, (y + fh) / 2048.0]`
- But each `(action, direction)` clip increments `row_idx` by 1:
  - **Monsters**: 6 actions $\times$ 8 directions = 48 rows. $48 \times 160\text{ px} = 7,680\text{ px}$.  
    Max $y + h = 7680$. UV $V_{\max} = 7680 / 2048 = 3.75$.
  - **Characters**: 10 actions $\times$ 8 directions = 80 rows. $80 \times 192\text{ px} = 15,360\text{ px}$.  
    Max $y + h = 15360$. UV $V_{\max} = 15360 / 2048 = 7.50$.

### 4.2 Mathematical Resolution
1. **Maintain 8-Column Grid Layout**:
   - Width: $8 \text{ columns} \times 160\text{ px} = 1,280\text{ px}$.  
     Smallest Power-of-Two width: $W_{\text{PoT}} = 2048$ ($2^{11}$).
2. **Calculate Power-of-Two Height**:
   - Smallest Power-of-Two height $H_{\text{PoT}}$ covering total height:
     - Monsters: $H = 7680 \implies H_{\text{PoT}} = 8192$ ($2^{13}$).
     - Characters: $H = 15360 \implies H_{\text{PoT}} = 16384$ ($2^{14}$).
3. **Normalized UV Coordinates**:
   $$\begin{aligned}
   u_0 &= \frac{x}{W_{\text{PoT}}}, & v_0 &= \frac{y}{H_{\text{PoT}}} \\
   u_1 &= \frac{x + w}{W_{\text{PoT}}}, & v_1 &= \frac{y + h}{H_{\text{PoT}}}
   \end{aligned}$$
   - **Monsters** ($W=2048, H=8192$):
     - $u_0 \in [0.0, 0.546875]$, $u_1 \in [0.078125, 0.625] \le 1.0$.
     - $v_0 \in [0.0, 0.91796875]$, $v_1 \in [0.01953125, 0.9375] \le 1.0$.
     - **0 frames out of bounds. Max V = 0.9375.**
   - **Characters** ($W=2048, H=16384$):
     - $u_0 \in [0.0, 0.546875]$, $u_1 \in [0.078125, 0.625] \le 1.0$.
     - $v_0 \in [0.0, 0.92578125]$, $v_1 \in [0.01171875, 0.9375] \le 1.0$.
     - **0 frames out of bounds. Max V = 0.9375.**

---

## 5. Analysis Item 4: Modularization Strategy for `test_vfx_texture_atlas_pipeline_e2e.py`

### 5.1 Current Problem
- `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` currently has **574 lines**, violating the 500-line Hard Cap in `GEMINI.md` and causing `python tools/lint/check_code_and_doc_hygiene.py --strict` to fail with exit code 1.

### 5.2 Modularization Approaches Compared

| Criteria | Option A: Extract Helper Catalog | Option B: Split into Two Test Files |
|---|---|---|
| **Structure** | Extract `CANONICAL_*` constants and fixtures to `vfx_pipeline_test_helpers.py` | Split into `test_vfx_texture_atlas_pipeline_e2e.py` (Tiers 1-2) & `test_vfx_pipeline_integration_e2e.py` (Tiers 3-4) |
| **Line Counts** | Main test file ~420 lines; helper file ~120 lines | Test file 1 ~270 lines; Test file 2 ~240 lines |
| **Hygiene Hard Cap (<= 500)** | PASS (420 <= 500) | PASS (270 <= 500, 240 <= 500) |
| **Hygiene Soft Cap (<= 350)** | Exceeds Soft Cap (420 > 350) | PASS (both <= 350) |
| **Command Compatibility** | 100% matches `TEST_READY.md` commands (`pytest tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py -v`) | Requires running `pytest tests/e2e_cocos/` or updating `TEST_READY.md` |

### 5.3 Optimal Combined Solution (Recommended)
Adopt **Option A with streamlined fixtures** or **Option B**:
- By creating `tests/e2e_cocos/vfx_pipeline_test_helpers.py` (catalog of active skills, support sigils, mobility skill, and helper `match_clip_identifier`), both test files or the main test file remain cleanly structured.
- To achieve the highest architectural hygiene (strictly under the 350-line Soft Cap), splitting into:
  1. `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py` (Tiers 1 & 2: Feature Coverage & Boundary Analysis, ~265 lines)
  2. `tests/e2e_cocos/test_vfx_pipeline_integration_e2e.py` (Tiers 3 & 4: Cross-Feature State & Real-World Workload Scenarios, ~225 lines)
  3. Updating `TEST_READY.md` so that both commands and `pytest tests/e2e_cocos/` are documented.
- Both files pass the 350-line Soft Cap and 500-line Hard Cap with 0 warnings.

### 5.4 Strengthening Tier 1 Scaffolding Assertions
In `test_t1_monster_and_character_scaffolding_structure`, replace the shallow existence check with a deep integrity loop:
```python
# Validate all 10 monster archetypes
for genus_dir in monsters_dir.glob("archetypes/*"):
    if not genus_dir.is_dir():
        continue
    manifest_path = genus_dir / f"{genus_dir.name}_anim_manifest.json"
    assert manifest_path.exists(), f"Missing monster manifest: {manifest_path}"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    assert manifest["textureWidth"] >= 2048 and manifest["textureHeight"] >= 8192
    for frame_id, f_meta in manifest["frames"].items():
        uv = f_meta["uv"]
        assert len(uv) == 4
        assert 0.0 <= uv[0] <= 1.0 and 0.0 <= uv[1] <= 1.0
        assert 0.0 <= uv[2] <= 1.0 and 0.0 <= uv[3] <= 1.0
        assert uv[2] > uv[0] and uv[3] > uv[1]
        assert f_meta["pivot"] == [0.5, 0.90]

# Validate all 6 exile characters
for char_dir in characters_dir.glob("char_*"):
    if not char_dir.is_dir():
        continue
    manifest_path = char_dir / f"{char_dir.name}_anim_manifest.json"
    assert manifest_path.exists(), f"Missing character manifest: {manifest_path}"
    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    assert manifest["textureWidth"] >= 2048 and manifest["textureHeight"] >= 16384
    for frame_id, f_meta in manifest["frames"].items():
        uv = f_meta["uv"]
        assert len(uv) == 4
        assert 0.0 <= uv[0] <= 1.0 and 0.0 <= uv[1] <= 1.0
        assert 0.0 <= uv[2] <= 1.0 and 0.0 <= uv[3] <= 1.0
        assert uv[2] > uv[0] and uv[3] > uv[1]
        assert f_meta["pivot"] == [0.5, 0.90]
```

---

## 6. Step-by-Step Remediation Plan for the Worker

### Step 1: Update `monster_character_pipeline_scaffold.py`
- **File**: `tools/asset_pipeline/monster_character_pipeline_scaffold.py`
- **Modifications**:
  1. Add helper function `next_power_of_two(n: int) -> int`:
     ```python
     def next_power_of_two(n: int) -> int:
         return 1 << (n - 1).bit_length() if n > 0 else 1
     ```
  2. In `scaffold_monsters`:
     - Calculate total rows: `total_rows = len(archetype["actions"]) * len(CANONICAL_DIRECTIONS)` (48 rows).
     - Calculate total height: `total_h = total_rows * archetype["frame_height"]` (7,680px).
     - Compute Power-of-Two dimensions:
       `tex_w = 2048`
       `tex_h = next_power_of_two(total_h)` (8192).
     - Set manifest:
       `"textureWidth": tex_w, "textureHeight": tex_h`
       `"texture_width": tex_w, "texture_height": tex_h`
     - Compute UVs:
       `"uv": [x / float(tex_w), y / float(tex_h), (x + fw) / float(tex_w), (y + fh) / float(tex_h)]`
     - Set pipeline config: `"target_resolution": [tex_w, tex_h]`.
  3. In `scaffold_characters`:
     - Calculate total rows: `total_rows = len(char_info["actions"]) * len(CANONICAL_DIRECTIONS)` (80 rows).
     - Calculate total height: `total_h = total_rows * char_info["frame_height"]` (15,360px).
     - Compute Power-of-Two dimensions:
       `tex_w = 2048`
       `tex_h = next_power_of_two(total_h)` (16384).
     - Set manifest:
       `"textureWidth": tex_w, "textureHeight": tex_h`
       `"texture_width": tex_w, "texture_height": tex_h`
     - Compute UVs:
       `"uv": [x / float(tex_w), y / float(tex_h), (x + fw) / float(tex_w), (y + fh) / float(tex_h)]`
     - Set pipeline config: `"target_resolution": [tex_w, tex_h]`.
  4. Ensure line count remains $\le 240$ lines.

### Step 2: Regenerate All Manifests
- Execute generator:
  ```bash
  python tools/asset_pipeline/monster_character_pipeline_scaffold.py
  ```
- Verify output files:
  - All 10 monster manifests in `client/cocos/assets/resources/monsters/archetypes/*/*_anim_manifest.json` have 0 out-of-bounds UVs and `Max V = 0.9375`.
  - All 6 character manifests in `client/cocos/assets/resources/characters/*/*_anim_manifest.json` have 0 out-of-bounds UVs and `Max V = 0.9375`.

### Step 3: Refactor / Modularize Test Suite & Add Strict UV Checks
- **Target Files**:
  - `tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py`
  - `tests/e2e_cocos/vfx_pipeline_test_helpers.py` (or `test_vfx_pipeline_integration_e2e.py`)
- **Modifications**:
  1. Extract canonical definitions (`CANONICAL_ACTIVE_SKILLS`, `CANONICAL_MOBILITY_SKILL`, `CANONICAL_SUPPORT_SIGILS`, `match_clip_identifier`) and fixtures to `vfx_pipeline_test_helpers.py`.
  2. Update `test_t1_monster_and_character_scaffolding_structure` to enforce strict bounds ($0.0 \le u_0, v_0, u_1, v_1 \le 1.0$) across all 10 monsters and 6 characters.
  3. Ensure all test files are strictly $\le 500$ lines (ideally $\le 350$ lines).

### Step 4: Verification Gate Execution
Run all validation gates:
```bash
# 1. Pipeline Test Suite (must be 57+ passed, 100%)
pytest tests/unit/test_asset_pipeline_tools.py tests/e2e/test_asset_campaign_and_pipeline_e2e.py tests/e2e_cocos/test_vfx_texture_atlas_pipeline_e2e.py -v

# 2. Full Cocos E2E Test Suite (must be 202+ passed, 100%)
pytest tests/e2e_cocos/ -v

# 3. Code & Documentation Hygiene Audit (must exit code 0)
python tools/lint/check_code_and_doc_hygiene.py --strict

# 4. Cocos TypeScript Build Gate (must exit code 0)
cd client/cocos && npm run build:web
```
