chore: convert mindmodel from YAML to markdown and clean up
Delete 17 malformed YAML constraint files and 10 stale numbered constraint files. Convert domain glossary, patterns, stack, and anti-patterns to markdown format. Update manifest.yaml to reference new markdown files.
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
---
|
||||
title: Anti-Patterns in Stemwijzer
|
||||
category: anti-patterns
|
||||
severity: critical
|
||||
---
|
||||
|
||||
# Anti-Patterns
|
||||
|
||||
> **NOTE**: Some anti-patterns below were investigated and found to be resolved or invalid. See individual entries for details.
|
||||
|
||||
## CRITICAL: print() Instead of Logging
|
||||
|
||||
**File**: `api_client.py`
|
||||
**Evidence**: 11 instances of `print(f"...")` instead of `_logger.info(...)`
|
||||
|
||||
**Broken code**:
|
||||
```python
|
||||
def get_motions(self, ...):
|
||||
try:
|
||||
# ...
|
||||
print(f"Fetched {len(voting_records)} voting records from API") # BAD
|
||||
print(f"Processed into {len(motions)} unique motions") # BAD
|
||||
except Exception as e:
|
||||
print(f"Error fetching motions from API: {e}") # BAD - no traceback
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```python
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
def get_motions(self, ...):
|
||||
try:
|
||||
_logger.info("Fetched %d voting records from API", len(voting_records))
|
||||
_logger.info("Processed into %d unique motions", len(motions))
|
||||
except Exception as e:
|
||||
_logger.exception("Error fetching motions from API: %s", e)
|
||||
return []
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CRITICAL: Global `_DummySt` Replacement
|
||||
|
||||
**File**: `explorer.py`
|
||||
**Evidence**: Lines ~50-70, module-level `st = _DummySt()` global replacement
|
||||
|
||||
**Problem**: Creates a module-level variable `st` that shadows `streamlit` module, causing subtle bugs.
|
||||
|
||||
**Fix**: Use conditional flags instead of global replacement:
|
||||
```python
|
||||
# GOOD: Use conditional logic
|
||||
try:
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
HAS_PLOTLY = True
|
||||
except ImportError:
|
||||
HAS_PLOTLY = False
|
||||
px = None
|
||||
go = None
|
||||
|
||||
def render_chart(data):
|
||||
if not HAS_PLOTLY:
|
||||
_logger.warning("Plotly not available")
|
||||
return
|
||||
# ... rest of chart logic
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WARNING: Logger Naming Inconsistency
|
||||
|
||||
**Evidence**: 16 files use `logger`, 17 files use `_logger`
|
||||
|
||||
**Files with `logger`** (without underscore):
|
||||
- api_client.py, ai_provider.py, pipeline files, analysis files
|
||||
|
||||
**Files with `_logger`** (with underscore):
|
||||
- database.py, explorer.py, explorer_helpers.py
|
||||
|
||||
**Recommendation**: Standardize on `_logger` for module-level loggers.
|
||||
|
||||
---
|
||||
|
||||
## WARNING: Bare except with pass
|
||||
|
||||
**File**: `database.py`, line 47
|
||||
|
||||
```python
|
||||
# BAD - catches KeyboardInterrupt, SystemExit, MemoryError
|
||||
try:
|
||||
conn.execute("CREATE SEQUENCE IF NOT EXISTS motions_id_seq START 1")
|
||||
except: # bare except
|
||||
pass
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```python
|
||||
try:
|
||||
conn.execute("CREATE SEQUENCE IF NOT EXISTS motions_id_seq START 1")
|
||||
except Exception as exc:
|
||||
_logger.debug("Sequence creation skipped: %s", exc)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## INVESTIGATED: Entity-ID / Party-Name Mismatch
|
||||
|
||||
**Status**: INVALID - investigated and resolved
|
||||
|
||||
**Investigation Summary**: `svd_vectors.entity_id` only contains MP names (not party names). Party centroids are correctly computed via `mp_metadata` lookups. No production bug exists.
|
||||
|
||||
---
|
||||
|
||||
## Pattern: Three Separate Party Alias Dictionaries
|
||||
|
||||
**Problem**: Party name variations exist in 3+ places with no canonical alias mapping.
|
||||
|
||||
**Fix**: Create one `PARTY_ALIASES` dict in `config.py`:
|
||||
```python
|
||||
PARTY_ALIASES = {
|
||||
"GroenLinks-PvdA": ["GL-PvdA", "GroenLinks PvdA", "PvdA-GroenLinks"],
|
||||
"PVV": ["Partij voor de Vrijheid"],
|
||||
# ...
|
||||
}
|
||||
```
|
||||
@@ -1,146 +0,0 @@
|
||||
# Anti-Patterns
|
||||
|
||||
> ⚠️ **NOTE**: Section 1 below was **investigated and resolved** — it is NOT a bug (see §1 for details).
|
||||
|
||||
---
|
||||
|
||||
## 1. ~~CRITICAL: Entity-ID / Party-Name Mismatch in `compute_party_coords`~~ → **INVALID — INVESTIGATED & RESOLVED**
|
||||
|
||||
**Investigation Date**: 2026-03-31
|
||||
|
||||
**Investigation Summary**: After thorough analysis of the database schema and code, this anti-pattern is **INVALID**. The original concern was based on a false assumption about `svd_vectors.entity_id` containing party names.
|
||||
|
||||
**Investigation Findings**:
|
||||
1. **`svd_vectors` table has NO rows with `entity_type='party'`** — only `mp` and `motion` entity types exist in practice.
|
||||
2. **`entity_ids in svd_vectors are always MP names** (e.g., `"Van Dijk, I."`), never party names. The party centroids are correctly computed via `mp_metadata` lookups.
|
||||
3. **The trajectories plot WORKS correctly** — no production bug exists. The code path for party-level visualization does not rely on `svd_vectors.entity_id` containing party names.
|
||||
|
||||
**Conclusion**: The original anti-pattern was a false positive caused by incorrect assumptions about data contents. The `party_map` reverse-lookup (`mp_name → party_name`) works correctly because `entity_id` values are always MP names, not party names.
|
||||
|
||||
---
|
||||
|
||||
## 2. Bare `except: pass`
|
||||
|
||||
**File**: `database.py`, line 47
|
||||
|
||||
**Problem**: Catches **all** exceptions including `KeyboardInterrupt`, `SystemExit`, `MemoryError`.
|
||||
Silently swallows errors — no logging, no fallback.
|
||||
|
||||
**Broken code**:
|
||||
```python
|
||||
try:
|
||||
self.conn.execute(sql)
|
||||
except: # ← bare except
|
||||
pass
|
||||
```
|
||||
|
||||
**Fix**:
|
||||
```python
|
||||
try:
|
||||
self.conn.execute(sql)
|
||||
except ibis.errors.IbisError as e:
|
||||
st.warning(f"Query failed: {e}")
|
||||
raise # or return a default
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Nested Exception Handling
|
||||
|
||||
**File**: `explorer.py`, lines 244–261
|
||||
|
||||
**Problem**: Try/except inside try/except creates opaque error paths. Inner exception silently swallows outer intent.
|
||||
|
||||
**Broken code**:
|
||||
```python
|
||||
try:
|
||||
result = compute_svd(motions)
|
||||
# ...
|
||||
except Exception:
|
||||
try:
|
||||
# Try fallback approach
|
||||
result = fallback_compute(motions)
|
||||
except Exception:
|
||||
pass # ← both exceptions silently dropped
|
||||
```
|
||||
|
||||
**Fix**: Flatten — handle each case explicitly, or use a decorator.
|
||||
|
||||
---
|
||||
|
||||
## 4. Catch-All `Exception` Used Everywhere
|
||||
|
||||
**Problem**: `except Exception:` catches 50+ exception types including `ValueError`, `TypeError`, `KeyError`.
|
||||
Overly broad — masks real bugs.
|
||||
|
||||
**Occurrence**: 850+ instances of bare/generic exception handlers across codebase.
|
||||
|
||||
**Fix**: Catch specific exceptions. If you must catch multiple, chain them:
|
||||
```python
|
||||
except (KeyError, ValueError) as e:
|
||||
logger.warning(f"Missing field: {e}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. No `entity_id` Format Validation
|
||||
|
||||
**Problem**: `svd_vectors.entity_id` can be either:
|
||||
- An MP name (e.g., `"Van Dijk, I."`) for individual-level SVD
|
||||
- A party name (e.g., `"GroenLinks-PvdA"`) for party-level SVD
|
||||
|
||||
No validation distinguishes which is which. Code must infer from context. (Note: In practice `svd_vectors.entity_id` only contains MP names — see §1 for investigation findings.)
|
||||
|
||||
**Fix**: Add explicit format marker or separate columns:
|
||||
```python
|
||||
# Option A: separate columns
|
||||
svd_vectors = pd.DataFrame({
|
||||
'mp_name': [...], # nullable
|
||||
'party_name': [...], # nullable
|
||||
'window': [...],
|
||||
'vector_2d': [...]
|
||||
})
|
||||
|
||||
# Option B: format prefix
|
||||
# "mp:Van Dijk, I." or "party:GroenLinks-PvdA"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Silent Fallback When Party Centroids Fail
|
||||
|
||||
**Problem**: If `party_map` lookup fails (entity is a party, not MP), the code silently produces
|
||||
`party_map_count: 0` and empty `parties_with_centroid_counts`. No warning is raised.
|
||||
|
||||
**Fix**: Add validation and warning:
|
||||
```python
|
||||
if party_map_count == 0:
|
||||
st.warning(f"No party mappings found for {len(svd_df)} entities in window '{window}'")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Three Separate Party Alias Dictionaries (No Single Source of Truth)
|
||||
|
||||
**Problem**: Party name variations exist in 3+ places:
|
||||
- `PARTY_COLOURS` keys
|
||||
- `party_map` values (from `mp_party_history`)
|
||||
- Raw data column values
|
||||
|
||||
No canonical alias mapping. Spelling mismatches cause silent failures.
|
||||
|
||||
**Fix**: Create one `PARTY_ALIASES` dict in `config.py`:
|
||||
```python
|
||||
PARTY_ALIASES = {
|
||||
"GroenLinks-PvdA": ["GL-PvdA", "GroenLinks PvdA", "PvdA-GroenLinks"],
|
||||
"PVV": ["Partij voor de Vrijheid"],
|
||||
...
|
||||
}
|
||||
|
||||
def resolve_party(name: str) -> str:
|
||||
"""Normalize any party name variant to canonical form."""
|
||||
for canonical, aliases in PARTY_ALIASES.items():
|
||||
if name in aliases or name == canonical:
|
||||
return canonical
|
||||
return name # no alias found
|
||||
```
|
||||
Reference in New Issue
Block a user