# SVD Axis Labels: Enforce Canonical Left/Right Orientation

## Problem

SVD axis labels do not consistently reflect the repository's political convention: right-wing parties (PVV, FVD, JA21, SGP) must appear on the RIGHT side of all axes in visualizations. Current code uses inconsistent inline party sets across files (`svd_labels.py` has 9 right parties, `political_axis.py` has 6), and no automated validation exists to enforce the convention.

## Goals

- Guarantee that canonical right-wing parties (PVV, FVD, JA21, SGP) appear on the RIGHT side of all SVD axes.
- Centralize canonical party sets in a single source of truth.
- Automatically flip axis orientation when canonical parties appear on the wrong side.
- Add unit tests and CI validation to prevent regressions.
- Maintain backward compatibility for existing callers.

## Architecture

### New Module: `analysis/config.py`

```python
"""Canonical political party sets for axis orientation validation."""

CANONICAL_RIGHT = frozenset({"PVV", "FVD", "JA21", "SGP"})
CANONICAL_LEFT = frozenset({
    "SP", "PvdA", "GL", "GroenLinks", "GroenLinks-PvdA",
    "DENK", "PvdD", "Volt"
})
```

### Updated Module: `analysis/svd_labels.py`

- Import canonical sets from `analysis.config`.
- Replace inline `RIGHT_PARTIES` / `LEFT_PARTIES` with config values.
- Export backward-compat aliases:
  ```python
  from analysis.config import CANONICAL_RIGHT, CANONICAL_LEFT
  RIGHT_PARTIES = CANONICAL_RIGHT
  LEFT_PARTIES = CANONICAL_LEFT
  ```
- Update `compute_flip_direction()` to use `CANONICAL_RIGHT` for flip decisions.

### Data Flow

1. Compute raw component scores/centroids (existing logic).
2. Project canonical right party vectors onto the component.
3. Determine where canonical right parties lie along the component axis.
4. If majority of `CANONICAL_RIGHT` items are on the left side, set `flip = True`.
5. When `flip` is True, swap/negate the axis and flip label semantics before returning to callers.
6. Numerical PCs remain intact; only orientation + labels change at presentation boundary.

### Error Handling & Fallbacks

- **No canonical parties present**: Log warning, fall back to existing behavior (no flip), optionally surface UI notice.
- **Ambiguous placement**: Use majority rule with deterministic tie-breaking (tie → do not flip, log warning).
- **Backward compatibility**: Preserve public functions (`get_svd_label`, `get_fallback_labels`, `compute_flip_direction`); only orientation may flip.

## Testing Strategy

### Unit Tests (`tests/test_svd_labels.py`)

- **Synthetic flip test**: Create synthetic vectors where canonical right parties have negative component values; assert `compute_flip_direction` returns `True` and labels map correctly.
- **Synthetic no-flip test**: Canonical right parties on positive side; assert `compute_flip_direction` returns `False`.
- **Real window test**: Load a representative window and assert every item from `CANONICAL_RIGHT` appears on the right side after flip logic.
- **Backward compat test**: Existing tests in `test_svd_labels.py` continue to pass after import swap.

### CI Validation

- Add pytest test group enforcing the rule:
  ```bash
  pytest -q tests/test_svd_labels.py::test_canonical_right_on_right
  ```
- Test loads at least one representative window and asserts PVV/FVD/JA21/SGP appear on the right side of the principal political axis.
- Prevents regressions in PR pipeline.

## Migration Plan

1. Create `analysis/config.py` with canonical sets.
2. Update `analysis/svd_labels.py` imports and export aliases.
3. Update `compute_flip_direction` to use canonical sets.
4. Add/update unit tests in `tests/test_svd_labels.py`.
5. Run full test suite to verify backward compatibility.
6. Commit changes locally (no push until user approves).

### Out of Scope (Follow-up)

- `analysis/political_axis.py` currently uses different party sets for PCA centroid orientation. Recommended follow-up: align with `config.py` in separate PR.
- UI notice for missing canonical parties (non-blocking enhancement).

## Success Criteria

- `CANONICAL_RIGHT` and `CANONICAL_LEFT` defined in `analysis/config.py`.
- `svd_labels.py` imports from config; exports aliases for backward compat.
- All existing tests pass after changes.
- New test `test_canonical_right_on_right` passes.
- Running `pytest tests/test_svd_labels.py -q` shows no regressions.
- Right-wing parties (PVV, FVD, JA21, SGP) consistently appear on the RIGHT side of all axes in visualizations.

## Related Files

- `analysis/config.py` (new)
- `analysis/svd_labels.py` (update)
- `tests/test_svd_labels.py` (update)
- `docs/solutions/best-practices/svd-labels-voting-patterns-not-semantics.md` (existing convention doc)
- `AGENTS.md` (convention reference)
