docs: add improvement roadmap, research notes, and solution docs

- Add 2026-04-24 ROADMAP with 5 phases / 17 items
- Add detailed implementation plans for P1-001 through P4-005
- Add research artifacts and solution docs from ledger merge
- Add test for SVD component 1 compass alignment
This commit is contained in:
2026-04-30 23:55:24 +02:00
parent ad7286ddc8
commit c85a367a8e
35 changed files with 3549 additions and 0 deletions
@@ -0,0 +1,90 @@
---
module: svd
date: 2026-04-16
category: docs/solutions/logic-errors
problem_type: logic_error
component: rails_view
severity: medium
symptoms:
- "SVD tab displayed VVD component 1 score of 0.108"
- "Compass displayed VVD component 1 score of 0.335"
- "Significant numerical discrepancy between two views showing same data"
root_cause: scope_issue
resolution_type: code_fix
tags:
- svd
- voting-analysis
- filter-scope
- party-scores
---
# SVD Tab Party Scores Don't Match Compass
## Problem
The SVD tab displayed VVD component 1 score as 0.108 while the compass visualization showed 0.335 — a 3x discrepancy caused by including inactive/historical MPs in the SVD calculation.
## Symptoms
- **SVD tab**: VVD comp1 score = 0.108 (incorrect)
- **Compass**: VVD comp1 score = 0.335 (correct, verified against compass reference)
- **Discrepancy**: ~3x difference between views
- **Scope**: Affects all parties but most visible for VVD (82 historical MPs vs ~50 active)
## What Didn't Work
N/A — straightforward fix once root cause identified through comparison with compass implementation at `explorer.py:1473`.
## Solution
The `_get_aligned_party_scores()` function in `views/svd.py` was missing an `active_MP` filter when calculating party means for the current parliament window.
**Before (buggy code):**
```python
def _get_aligned_party_scores(party_id: str, dimension: str, ...) -> list:
raw_scores = execute_query(score_query, ...)
# Missing: no active_MP filter
return raw_scores
```
**After (fixed code):**
```python
def get_aligned_party_scores(party_id: str, dimension: str, ...) -> list:
raw_scores = execute_query(score_query, ...)
# Filter to only active MPs (matches compass behavior at explorer.py:1473)
active_mps = {m[0] for m in active_mp_query if m[0] is not None}
scores = [s for s in raw_scores if s[0] in active_mps]
return scores
```
Key changes:
1. Extracted function to module-level for testability
2. Added active MP filtering using the same query pattern as compass (`explorer.py:1473`)
3. Filter ensures only MPs in current parliament window are included
**Verification:**
- Without filter: VVD comp1 = 0.1083
- With filter: VVD comp1 = 0.3366 (matches compass reference of 0.3350)
- Test suite: 169/169 tests passing
## Why This Works
The root cause was including all 82 historical VVD MPs instead of only the active ones. The database (`data/motions.db`) contains MPs from multiple parliaments, and the `_get_aligned_party_scores()` function wasn't filtering by `active_MP`. The compass correctly applied this filter, explaining the discrepancy.
## Prevention
1. **Test suite**: Comprehensive tests in `tests/svd_test.py` covering alignment calculations with active MP filtering
2. **Cross-view validation**: Compare SVD and compass scores for each party — assert values match within tolerance
3. **Query pattern documentation**: All score queries must include `active_MP` filter when calculating party means
4. **Code review checklist**: Require active_MP filter for any new score calculation queries
5. **Automated regression**: Add CI check that runs comparison between SVD tab and compass for all parties
## Related Issues
- `docs/solutions/logic-errors/svd-theme-divergence-from-party-positions.md` — Related domain issue: SVD scores not matching actual party positions
- `docs/solutions/logic-errors/svd-component-labels-mismatch.md` — Related theme: Labels/data alignment mismatches
- `docs/solutions/best-practices/svd-labels-voting-patterns-not-semantics.md` — Core principle: SVD captures voting patterns, verify against actual voting data