# Plan: RiskAnalysisResult Redundancy Cleanup — COMPLETED

_Created: 2026-02-18_
_Completed: 2026-02-18_

## Context

`RiskAnalysisResult.to_api_response()` returns ~67KB of JSON with significant duplication. The class docstrings already note "raw fields maintained during Phase 1.x, may be deprecated in Phase 2." This is that cleanup pass.

We just added clean getter methods (`get_summary()`, `get_top_risk_contributors()`, `get_compliance_summary()`, `get_industry_concentration()`) and a `format="agent"` output. The getters use the normalized data. Good time to remove redundancy.

## Scope

**Remove clear redundancy only. Do NOT restructure.**

- Only modify `to_api_response()` output dict — not underlying class attributes
- Do NOT change getter methods, `to_cli_report()`, or `to_formatted_report()`
- Do NOT change the class constructor or field definitions
- Only remove a field from the API response after auditing ALL consumers

## Redundant Field Groups

### Group A: Raw CLI checks vs normalized versions

| Raw field | Normalized field | Difference |
|-----------|-----------------|------------|
| `risk_checks` — keys: `Metric`, `Actual`, `Limit`, `Pass` | `risk_limit_violations_summary` — keys: `metric`, `actual`, `limit`, `status`, `formatted_line` | Same data, different casing + `formatted_line` added |
| `beta_checks` — keys: `factor`, `portfolio_beta`, `max_allowed_beta`, `pass` | `beta_exposure_checks_table` — keys: `factor`, `portfolio_beta`, `max_allowed_beta`, `status`, `formatted_line` | Same data, `pass`→`status` + `formatted_line` added |

Both pairs exist in `to_api_response()`. The raw versions were kept for CLI parity. The normalized versions were added for API consumers.

**Task**: Grep all consumers of `risk_checks` and `beta_checks` (as API response fields, not as class attributes). If only internal code + `to_cli_report()` use the raw shape, remove from `to_api_response()` output. Keep the class attributes (used by `to_cli_report()` and `get_compliance_summary()`).

### Group B: Industry variance 4× duplication

All in `to_api_response()`:

| Field | Source | Shape |
|-------|--------|-------|
| `industry_variance` | `self.industry_variance` (raw nested dict) | `{absolute: {...}, percent_of_portfolio: {...}, per_industry_group_beta: {...}}` |
| `industry_variance_absolute` | `_build_industry_variance_absolute_table()` | Flattened from `industry_variance["absolute"]` |
| `industry_variance_percentage` | `_build_industry_variance_percentage_table()` | Flattened from `industry_variance["percent_of_portfolio"]` |
| `industry_group_betas` | `_build_industry_group_betas_table()` | Flattened from `industry_variance["per_industry_group_beta"]` |

The nested `industry_variance` is the raw source. The 3 flattened fields are derived from it. Both are in the response.

**Task**: Grep consumers of the nested `industry_variance` in API responses. If nothing depends on the nested shape (vs the 3 flattened fields), remove it from `to_api_response()`. Keep the class attribute (used by `get_industry_concentration()` and `to_cli_report()`).

### Group C: formatted_report bloat (audit only)

`formatted_report` is a ~50-500KB human-readable text string generated by `to_cli_report()`, included in every `format="full"` response via `to_api_response()`.

**Task**: Audit only — do NOT remove. Report:
- Who consumes `formatted_report` from the full response?
- Could it be opt-in (e.g., only included when `format="report"`)?
- How much would removing it reduce `to_api_response()` size?

## Implementation Steps

### Step 1: Audit consumers (report findings before making changes)

For each field, grep the entire codebase:

```bash
# Group A
grep -rn "risk_checks" --include="*.py" | grep -v "__pycache__" | grep -v ".pyc"
grep -rn "beta_checks" --include="*.py" | grep -v "__pycache__" | grep -v ".pyc"
grep -rn "risk_limit_violations_summary" --include="*.py" | grep -v "__pycache__"
grep -rn "beta_exposure_checks_table" --include="*.py" | grep -v "__pycache__"

# Group B
grep -rn "industry_variance" --include="*.py" | grep -v "__pycache__"

# Group C
grep -rn "formatted_report" --include="*.py" | grep -v "__pycache__"
```

#### Step 1 Findings (2026-02-19 audit, no code changes)

Audit scope included `*.py`, `*.ts`, `*.tsx` across the repo. Full grep also hit mirror/archive trees (`risk_module_secrets/`, `prototype/`) with duplicate definitions/usages; tables below focus on active paths used by current app/frontend/tests.

##### Group A — `risk_checks`, `beta_checks`, `risk_limit_violations_summary`, `beta_exposure_checks_table`

| File | Line | Category | Notes |
|------|------|----------|-------|
| `mcp_tools/risk.py` | 77 | API consumer | Section filter (`include=["compliance"]`) expects `risk_checks` in `result.to_api_response()`. |
| `mcp_tools/risk.py` | 78 | API consumer | Section filter expects `beta_checks`. |
| `mcp_tools/risk.py` | 79 | API consumer | Section filter expects `risk_limit_violations_summary`. |
| `mcp_tools/risk.py` | 80 | API consumer | Section filter expects `beta_exposure_checks_table`. |
| `frontend/src/chassis/types/index.ts` | 189 | API consumer | Type contract still declares `risk_checks` on risk analysis payload. |
| `frontend/src/chassis/types/index.ts` | 190 | API consumer | Type contract still declares `beta_checks` on risk analysis payload. |
| `core/result_objects.py` | 1303 | Definition | `RiskAnalysisResult.risk_checks` class attribute definition. |
| `core/result_objects.py` | 1308 | Definition | `RiskAnalysisResult.beta_checks` class attribute definition. |
| `core/result_objects.py` | 1545 | Definition | `_get_risk_limit_violations_summary()` normalized builder definition. |
| `core/result_objects.py` | 1607 | Definition | `_get_beta_exposure_checks_table()` normalized builder definition. |
| `core/result_objects.py` | 2200 | Definition | `to_api_response()` emits `risk_checks`. |
| `core/result_objects.py` | 2201 | Definition | `to_api_response()` emits `beta_checks`. |
| `core/result_objects.py` | 2224 | Definition | `to_api_response()` emits `risk_limit_violations_summary`. |
| `core/result_objects.py` | 2225 | Definition | `to_api_response()` emits `beta_exposure_checks_table`. |
| `core/result_objects.py` | 1423 | Internal | Reads `self.risk_checks` in `get_compliance_summary()` (unaffected by API serialization changes). |
| `core/result_objects.py` | 1434 | Internal | Reads `self.beta_checks` in `get_compliance_summary()` (unaffected). |
| `core/result_objects.py` | 2489 | Internal | CLI formatter reads `self.risk_checks` directly (unaffected). |
| `core/result_objects.py` | 2497 | Internal | CLI formatter reads `self.beta_checks` directly (unaffected). |
| `tests/core/test_risk_analysis_result_getters.py` | 96 | Test | Tests pass `risk_checks`/`beta_checks` into object constructor; class-attribute path, not serialized dict path. |
| `tests/services/test_portfolio_service_asset_class_perf.py` | 64 | Test | Fixture creates `RiskAnalysisResult` with `risk_checks`/`beta_checks`; not asserting serialized response keys. |

##### Group B — `industry_variance` (nested) vs flattened fields

Nested `industry_variance` hits:

| File | Line | Category | Notes |
|------|------|----------|-------|
| `mcp_tools/risk.py` | 70 | API consumer | Section filter (`include=["industry"]`) currently expects nested `industry_variance` key. |
| `frontend/src/chassis/types/index.ts` | 187 | API consumer | Type contract declares nested `industry_variance`. |
| `core/result_objects.py` | 1297 | Definition | `RiskAnalysisResult.industry_variance` class attribute definition (nested raw dict). |
| `core/result_objects.py` | 2207 | Definition | `to_api_response()` emits nested `industry_variance`. |
| `core/result_objects.py` | 1455 | Internal | `get_industry_concentration()` reads `self.industry_variance` directly (unaffected). |
| `core/result_objects.py` | 1713 | Internal | `_build_industry_group_betas_table()` reads `self.industry_variance` directly (unaffected). |
| `core/result_objects.py` | 1798 | Internal | `_build_industry_variance_percentage_table()` reads `self.industry_variance` directly (unaffected). |
| `core/result_objects.py` | 1984 | Internal | `_build_industry_variance_absolute_table()` reads `self.industry_variance` directly (unaffected). |
| `tests/test_portfolio_risk.py` | 403 | Test | Asserts nested `out["industry_variance"]["absolute"]` from `portfolio_risk.compute_variance_attribution()` output (not `RiskAnalysisResult.to_api_response()`). |
| `tests/core/test_risk_analysis_result_getters.py` | 29 | Test | Fixture provides nested `industry_variance` into `RiskAnalysisResult` constructor. |

Flattened-field hits (`industry_variance_absolute`, `industry_variance_percentage`, `industry_group_betas`):

| File | Line | Category | Notes |
|------|------|----------|-------|
| `mcp_tools/risk.py` | 67 | API consumer | Section filter expects `industry_group_betas`. |
| `mcp_tools/risk.py` | 68 | API consumer | Section filter expects `industry_variance_absolute`. |
| `mcp_tools/risk.py` | 69 | API consumer | Section filter expects `industry_variance_percentage`. |
| `core/result_objects.py` | 2190 | Definition | `to_api_response()` emits `industry_group_betas`. |
| `core/result_objects.py` | 2198 | Definition | `to_api_response()` emits `industry_variance_absolute`. |
| `core/result_objects.py` | 2199 | Definition | `to_api_response()` emits `industry_variance_percentage`. |
| `frontend/src/adapters/RiskAnalysisAdapter.ts` | 167 | API consumer | Frontend adapter type expects `industry_variance_absolute`. |
| `frontend/src/adapters/RiskAnalysisAdapter.ts` | 512 | API consumer | Adapter passes through `factorData.industry_variance_absolute`. |
| `frontend/src/components/dashboard/shared/charts/adapters/chartDataAdapters.ts` | 395 | API consumer | Chart adapter reads `factorData.industry_variance_absolute`. |
| `frontend/src/components/dashboard/views/modern/RiskAnalysisModernContainer.tsx` | 233 | API consumer | UI consumes `data.industry_variance_absolute` for industry risk display. |

##### Group C — `formatted_report`

| File | Line | Category | Notes |
|------|------|----------|-------|
| `mcp_tools/risk.py` | 91 | API consumer | Section filter (`include=["metadata"]`) expects `formatted_report` in full risk response. |
| `core/result_objects.py` | 2223 | Definition | `RiskAnalysisResult.to_api_response()` emits `formatted_report`. |
| `frontend/src/adapters/AnalysisReportAdapter.ts` | 466 | API consumer | Reads `input.analysis.analysis.formatted_report` (nested analysis payload from backend). |
| `frontend/src/chassis/types/index.ts` | 195 | API consumer | Risk-analysis type contract declares `formatted_report`. |
| `frontend/src/types/api-generated.ts` | 3951 | API consumer | Generated API type includes `formatted_report` contract. |
| `core/interpretation.py` | 114 | API consumer | Reads `portfolio_output.get("formatted_report")` from serialized dict input. |
| `tests/utils/show_api_output.py` | 443 | Test | Reads `data["data"].get("formatted_report")` for `/api/analyze` output display. |
| `tests/api/test_api_endpoints.py` | 78 | Test | Reads `result['risk_results']['formatted_report']` in endpoint smoke test script. |

Step 1 audit conclusion:
- Group A raw fields (`risk_checks`, `beta_checks`) currently have no active Python runtime dict-access consumer beyond MCP section filtering and TS type contracts.
- Group B nested `industry_variance` is consumed by MCP section filtering and TS contracts; frontend runtime charting currently uses flattened `industry_variance_absolute` instead.
- Group C `formatted_report` has active consumers in MCP filtering, frontend adapters/types, interpretation path, and test utilities.

Categorize each consumer as:
- **Internal** (core/, services/, run_portfolio_risk.py) — uses class attribute, unaffected by `to_api_response()` changes
- **API consumer** (mcp_tools/, mcp_server.py, tests/) — reads from the serialized API response dict
- **External** (frontend, other services) — if any

**Output: Write findings to this doc before proceeding to Step 2.**

### Step 2: Remove safe redundancies

Based on Step 1 findings:

- Remove raw fields from `to_api_response()` output dict **only if** no API consumer reads them
- Remove nested `industry_variance` from output dict **only if** no API consumer reads the nested shape
- Keep all class attributes unchanged
- Keep `to_cli_report()` unchanged (it reads class attributes, not the API response)
- Add a `# Phase 2 cleanup: removed {field}` comment where fields are removed

### Step 3: Update tests

- Any test asserting on removed fields → update or remove assertion
- Run full test suite: `python3 -m pytest tests/ -x -v`

### Step 4: Report

Document final state:
- What was removed and why (consumer audit evidence)
- What was kept and why
- Size reduction estimate for `to_api_response()`
- Test results

## Review Findings (Codex Meta-Review)

_Added: 2026-02-18 — from Codex review of this plan before execution_

### R1: `RISK_ANALYSIS_SECTIONS` map must stay in sync

`mcp_tools/risk.py` has a `RISK_ANALYSIS_SECTIONS` dict that maps section names (e.g., `"compliance"`, `"industry"`) to lists of field names from `to_api_response()`. It currently references `risk_checks`, `beta_checks`, and `industry_variance`. If those fields are removed from `to_api_response()`, the sections map must be updated too — otherwise `include=["compliance"]` would return empty/missing keys.

**Action**: In Step 2, after removing any field from `to_api_response()`, also remove it from `RISK_ANALYSIS_SECTIONS` in `mcp_tools/risk.py`.

### R2: Audit scope must include frontend TypeScript

The codebase has a `frontend/` directory with TypeScript types that may reference API response field names. The grep in Step 1 only covers `*.py` files.

**Action**: In Step 1, also grep `*.ts` and `*.tsx` files:
```bash
# Frontend type contracts
grep -rn "risk_checks\|beta_checks\|industry_variance\|formatted_report" --include="*.ts" --include="*.tsx" frontend/
```

If any TypeScript types reference removed fields, document the type contract dependency. Do NOT modify frontend files — just flag them in the audit.

### R3: `output="file"` dumps full `to_api_response()`

`_save_full_risk_analysis()` in `mcp_tools/risk.py` calls `result.to_api_response()` and writes it to disk. Fields removed from `to_api_response()` will also disappear from saved files. This is the intended behavior (saved files should match the API response), but worth noting for the audit report.

**Action**: No code change needed. In Step 4 report, note that saved files (`logs/risk_analysis/`) will also reflect the removals.

## Decision Criteria

| Scenario | Action |
|----------|--------|
| Field consumed only by internal code (class attributes) | Safe to remove from `to_api_response()` |
| Field consumed by MCP tool code reading the API dict | Keep — or migrate the MCP code first, then remove |
| Field consumed by tests only | Update tests, then remove |
| Field consumed by unknown/external consumers | Keep, add deprecation comment |
| Field consumed by frontend TypeScript types | Keep, add deprecation comment, flag for frontend cleanup |
| Ambiguous — can't determine all consumers | Keep, document uncertainty |

## Step 2-4 Execution Results (2026-02-19)

### Step 2 applied changes

Updated `RiskAnalysisResult.to_api_response()` in `core/result_objects.py`:

- Removed raw compliance fields from serialized API dict:
  - `risk_checks`
  - `beta_checks`
- Added inline comment at removal site:
  - `# Phase 2 cleanup: removed risk_checks, beta_checks (redundant with risk_limit_violations_summary, beta_exposure_checks_table)`
- Removed nested industry field from serialized API dict:
  - `industry_variance`
- Added inline comment at removal site:
  - `# Phase 2 cleanup: removed nested industry_variance (redundant with industry_variance_absolute, industry_variance_percentage, industry_group_betas)`

Updated section filtering map in `mcp_tools/risk.py`:

- Removed from `"compliance"` section:
  - `risk_checks`
  - `beta_checks`
- Removed from `"industry"` section:
  - `industry_variance`

### What was explicitly kept

- Kept class attributes/constructors/field definitions unchanged.
- Kept `to_cli_report()`, `to_formatted_report()`, and getter methods unchanged.
- Kept frontend TypeScript files unchanged.
- Kept `formatted_report` (Group C audit-only field).
- Kept flattened industry fields:
  - `industry_variance_absolute`
  - `industry_variance_percentage`
  - `industry_group_betas`
- Kept normalized compliance fields:
  - `risk_limit_violations_summary`
  - `beta_exposure_checks_table`

### Step 3 test updates and validation

- No existing tests were found asserting removed keys on `RiskAnalysisResult.to_api_response()` serialized output, so no assertion rewrites were required.
- Full suite command executed:
  - `python3 -m pytest tests/ -x -v`
  - Result: stopped early with unrelated import error during collection:
    - `ModuleNotFoundError: No module named 'routes.api'`
    - File: `tests/api/test_portfolio_api_crud.py`
- Focused verification for the touched risk paths:
  - `python3 -m pytest tests/mcp_tools/test_risk_matrices_flag.py tests/mcp_tools/test_risk_agent_format.py tests/core/test_risk_analysis_result_getters.py -v`
  - Result: `17 passed`

### Step 4 summary

- Removed redundant serialized API fields: `risk_checks`, `beta_checks`, `industry_variance`.
- Kept normalized and flattened replacements, plus `formatted_report`.
- `logs/risk_analysis/` JSON output (from `output="file"`) now reflects the same field removals because it writes `result.to_api_response()`.
