Commit Graph
100 Commits
Author SHA1 Message Date
sgeboers 2a081ade25 fix(overton): strict centrist definition + left support analysis
- Reclassified centrist to {D66, CDA, CU, NSC} — removing VVD/BBB
  which are center-right coalition partners
- Added centrist_support_strict (0.251→0.507, d=+0.65), center_right_support,
  and left_support_mp columns via migration script
- Figure 1 now shows center-right (VVD/BBB) support as orange dashed line
- New Figure 3: bar chart of left-party support for right-wing motions
  (0.268→0.202, left opposition hardened)
- New report Section 6 covering left-wing support trends
- All analysis now uses strict centrist definition throughout
2026-05-09 00:45:38 +02:00
sgeboers e478235c84 fix(overton): correct SVD axis interpretation, drop pass rate, synthesis rewrite
- SVD axis 2 sign corrected: negative = nationalist (PVV -0.56, FVD -0.36), positive = kosmopolitisch (Volt +0.27). Centrists moved LEFT on both axes while right-wing moved further right culturally (+0.146 gap). 'Acceptance without conversion' named as unifying interpretation.
- U1: Figure 1 merged to single panel, pass rate removed, 5 centrist_support lines
- U2: Pass rate columns dropped from all breakpoint tables, PR narrative cut
- U3: Findings report rewritten: SVD section replaced, synthesis restructured into 3 tiers, extremity LLM bias qualified
- U4: Axis labels and sign convention added to svd_stability_report.md
- Added centrist_support_mp column (MP-weighted, correlates 0.998 with party-level)
2026-05-09 00:21:46 +02:00
sgeboers 76b499cdc0 feat(analysis): Overton window breakpoint analysis with opposition control and SVD drift
Quantify 2024 breakpoint in centrist support (d=+0.68 overall, d=+0.85 opposition-only),
domain decomposition, extremity-stratified pass rates, and manual LLM audit (75% agreement).
SVD center drift aborted due to axis instability (9/10 consecutive window pairs fail stability threshold).
2026-05-08 23:14:34 +02:00
sgeboers d170444bda feat(analysis): add migration anti-democratic overlap analysis 2026-05-08 22:56:38 +02:00
sgeboers fbf92c82cf feat(right-wing): dual-scoring extremity/sentiment + derived categories
Extremity Scorer (U4 enhanced):
- Now scores BOTH original motion text AND layman explanation separately
- Schema: text_score, text_explanation, layman_score, layman_explanation
- Text scores: 1→7, 2→33, 3→5, 4→5 (mild-to-moderate)
- Layman scores: 1→12, 2→20, 3→17, 4→1 (slightly milder)

Sentiment Analysis (U5 enhanced):
- Now scores BOTH original motion text AND layman explanation separately
- Schema: text_score, text_explanation, layman_score, layman_explanation
- Text sentiment avg: 0.294 (slightly positive)
- Layman sentiment avg: 0.416 (more positive - summaries tone down hostility)

Category Derivation (new):
- Two-phase LLM approach: derive taxonomy from sample, then apply to all
- Discovered 7 categories from 30-motion sample:
  veiligheid/justitie, corona/pandemie, economie/belasting, klimaat/milieu,
  defensie/buitenland, asiel/vreemdelingen, overig
- Applied to 50 motions with distribution shown in DB
- Adds category + category_explanation columns to right_wing_motions
2026-05-05 21:40:58 +02:00
sgeboers f94edc3d04 feat(right-wing): sentiment analysis pipeline for right-wing motions
Implements U5: sentiment_analysis.py uses LLM batch calls (fallback when no
local Dutch sentiment model is available) to score motion sentiment on [-1, 1]
scale.

Design:
- Prompt asks for sentiment from -1 (hostile/aggressive) to 1 (constructive)
- JSON schema enforces numeric score + Dutch explanation
- Batch size 10, max_workers 5 for parallel API calls
- Stores results in  table
- Updates  with avg_sentiment, sentiment_std,
  pct_strongly_negative per year

Sample validation (50 motions): good variance across [-0.9, 1.0] range.
2026-05-05 21:25:42 +02:00
sgeboers d2310edfc4 feat(right-wing): LLM-based policy extremity scoring
Implements U4: extremity_scorer.py uses ai_provider.chat_completion_json_parallel
with a JSON schema enforcing integer 1-5 + Dutch explanation.

Design:
- Batch size 10, max_workers 5 for parallel API calls
- Prompt asks for concrete policy + radicalism score in Dutch
- Stores results in  table (motion_id, score, explanation, error)
- Updates  with yearly averages
- Default sample=50 for validation; --sample -1 scores all motions

Sample validation (50 motions): scores distributed 1→2, 2→34, 3→7, 4→7,
yearly averages ~2.0-2.5 (mild-to-moderate radicalism).
2026-05-05 21:23:08 +02:00
sgeboers 1bc83c4384 feat(right-wing): temporal aggregation of right-wing motion trends
Implements U3: temporal_analysis.py computes yearly_summary from the
right_wing_motions table (U2 output).

Metrics per year:
- total_right_wing, pct_of_total, total_motions
- avg_right_support, avg_left_opposition, centrist_support
- avg_right_keyword_matches, extremity_index (U4 placeholder)
- yoy_right_wing_delta, yoy_pct_delta

Key finding: right-wing motions grew from ~4% (2018) to ~12% (2024-2025)
of all motions, with rising centrist support over time.
2026-05-05 21:20:12 +02:00
sgeboers d3dfb0ce2f feat(right-wing): hybrid motion classifier using keywords + votes
Implements U2: classify_motions.py loads keywords from U1 and classifies
motions as right-wing when:
- right_support >= 60% (CANONICAL_RIGHT parties voting 'voor')
- left_opposition >= 40% (CANONICAL_LEFT parties voting 'tegen')
- AND at least 1 right-wing keyword match in title/body_text

Outputs DuckDB table  with:
- motion_id, year, title, right_support, left_opposition, centrist_support
- right_keyword_matches, left_keyword_matches, classified flag

Classified 2986 of 28331 motions (10.5%) as right-wing.
2026-05-05 21:18:38 +02:00
sgeboers c6f8540671 feat(right-wing): derive right-wing keywords via differential TF-IDF
Implements U1: derive_keywords.py uses party voting patterns to classify
motions as right-wing vs left-wing, then computes differential TF-IDF on
cleaned motion titles to surface policy terms distinctive to right-wing
motions.

Key design choices:
- Vote threshold: 60% of parties in group must vote 'voor'
- Text cleaning strips motion prefixes aggressively (handles multi-word
  surnames, plural 'leden', t.v.v. parentheticals)
- Expanded Dutch stopword list filters procedural and generic noise
- Results written to analysis/right_wing/right_wing_keywords.json

Produces ~50 filtered terms including: asielzoekers, defensie, kernenergie,
boeren, vreemdelingenbeleid, stikstof, asielstop, strafrecht.
2026-05-05 21:14:11 +02:00
sgeboers 3a46485067 added ansible again 2026-05-04 22:05:06 +02:00
sgeboers 272d839a42 feat: agent-native refactor, SVD consistency fixes, UX cleanup, mobile support
- Refactor agent_tools to atomic primitives (24 tools, delete workflows)
- Fix SVD component score inconsistency between single-window and trajectory views
  (same PCA basis, same flip handling, same active-MP filter for current_parliament)
- Fix Dutch spelling: Huidig parliament -> Huidig parlement
- Remove all decorative emojis from UI (app.py, explorer.py, analysis tabs)
- Add dark theme matching sgeboers.nl (mint accent on dark background)
- Remove browser tab favicon and Streamlit chrome (deploy button, running status)
- Remove trajectories debug UI and EMA settings (hardcoded smooth_alpha=0.35)
- Switch layout to centered for mobile readability
- Add responsive CSS for mobile (touch targets, font sizing, overflow prevention)
- Update AGENTS.md and SYSTEM_PROMPT.md with active tool instructions
- Add compound docs for SVD consistency bug
- Update tests: 214 passed, 3 skipped
2026-05-04 21:56:40 +02:00
sgeboers efb3a8fbd2 fix: agent-native audit — parameterize thresholds, add CRUD tests, tool discovery
Audit fixes for agent-native architecture gaps:

- agent_tools/content.py: parameterize healthy_threshold in check_embedding_quality
- agent_tools/__init__.py: add __all__ exports and list_tools() runtime discovery
- agent_tools/database.py: add CRUD primitives (create_motion, update_motion, delete_report)
  plus query_embeddings, query_similar_motions, query_compass_positions
- tests/agent_tools/test_database_tools.py: add CRUD tool tests
- tests/agent_tools/test_content_tools.py: add parameterized threshold test
- tests/agent_tools/test_package.py: test list_tools() and package imports

Tests: 245 passed, 3 skipped
2026-05-04 20:05:59 +02:00
sgeboers 8af27bbf04 feat: implement agent-native architecture (U1-U6)
Implements the agent-native architecture plan (docs/plans/2026-05-01-002-agent-native-architecture-plan.md):

- U1: Database query primitives (agent_tools/database.py)
  - query_motions, query_votes, query_svd_vectors, query_party_positions, query_pipeline_status
- U2: Pipeline control primitives (agent_tools/pipeline.py)
  - pipeline_run_stage, pipeline_run_full, pipeline_check_health, pipeline_get_logs, pipeline_validate_output
- U3: Analysis & report generation (agent_tools/analysis.py, reports.py)
  - analyze_party_shift, analyze_axis_stability, validate_svd_labels, generate_report
- U4: Content validation primitives (agent_tools/content.py)
  - validate_motion_coverage, validate_layman_explanations, suggest_svd_label, check_embedding_quality
- U5: System prompt & context injection (SYSTEM_PROMPT.md, context.py, context.md)
- U6: Parity verification tests (tests/agent_tools/test_parity.py)

Tests: 238 passed, 2 skipped
AGENTS.md updated to surface agent_tools/
2026-05-04 19:38:01 +02:00
sgeboers 98358344a0 docs: add STRATEGY.md with product strategy
Captures target problem, approach, primary persona, key metrics,
tracks of work, and explicit non-goals for the Stemwijzer project.
2026-05-04 19:16:50 +02:00
sgeboers a634ceba2d cleanup: remove Docker, Ansible, and deployment infrastructure
Removes unused deployment and packaging infrastructure:
- Dockerfile and docker-compose.yml (Docker deployment not used)
- ansible/ directory (playbooks, inventory, config)
- packages/@ansible/example/ (npm package for Ansible example)
- docs/deployment/ansible-package-deploy.md
- docs/plans/2026-04-24-002-fix-docker-compose-scheduler-plan.md (obsolete)
- .github/workflows/publish-ansible-example.yml
- .github/workflows/ci-node-packages.yml (only tested packages/)

Updates:
- README.md: remove Deployment section
- docs/plans/2026-04-24-ROADMAP-stemwijzer-improvements.md: mark P1-002 as removed, update sprint 1
2026-05-04 19:11:37 +02:00
sgeboers 1f053f7d91 refactor: simplify Explorer to 3 focused tabs — compass, trajectories, SVD
Removes embedding-heavy search and generic browser tabs from the Explorer.
The project does not currently use embeddings meaningfully, so the similarity
search and browser features were dead weight.

Changes:
- explorer.py: Remove search and browser tabs, keep compass/trajectories/SVD
- explorer.py: Remove fused_embeddings and similarity_cache stats from sidebar
- Home.py: Update Explorer description to match new focused layout
- analysis/tabs/__init__.py: Remove search and browser exports
- tests: Update decomposition and import tests for new tab set

Result: Explorer now has 3 focused analytical tabs instead of 5.
2026-05-01 14:46:52 +02:00
sgeboers 2c60f41f29 cleanup: archive stale scripts and delete orphaned generate_extra_charts
Archives 8 one-off/backfill/research scripts to scripts/archive/:
- compare_svd_exclude_parties.py (diagnostic)
- compute_test_batch.py (test utility)
- fill_mp_votes_parties.py (backfill)
- generate_compass.py (generates to deleted outputs/)
- inspect_axis.py (diagnostic)
- qa_similarity.py (QA script, references deleted thoughts/ledgers/)
- recompute_svd.py (one-off recompute)
- semantic_gravity_examples.py (research)

Deletes:
- generate_extra_charts.py (0 references, generates to deleted outputs/)
- tests/test_qa_similarity.py (test for archived script)

Adds:
- scripts/archive/README.md explaining archive purpose
- docs/plans/2026-05-01-001-scripts-audit-cleanup-plan.md
2026-05-01 12:22:55 +02:00
sgeboers 07dd393533 cleanup: remove stale .mindmodel, old venvs, orphaned code, and transient artifacts
Removes:
- .mindmodel/ directory and related CI workflows (mindmodel-schedule.yml, mindmodel-validation.yml)
- scripts/mindmodel/ and scripts/validate_mindmodel.py
- src/types/ and src/validators/ (orphaned type modules, only used by mindmodel)
- tests/ci/, tests/scripts/mindmodel/, tests/types/, tests/validators/ (mindmodel-only tests)
- thoughts/ledgers/ and thoughts/shared/ (stale transient directories)
- .venv_axis and .venv_plotly (orphaned virtual environments, ~1.1 GB)
- outputs/blog-charts/ (stale generated HTML files)
- data/*.json sidecars (empty cache artifacts)
- __pycache__ and *.pyc files across repo

Updates:
- .gitignore: remove thoughts/shared/analyses/ entry

Space reclaimed: ~1.1 GB+
2026-05-01 12:11:06 +02:00
sgeboers 6e36fa2604 feat: persist and load explained variance for scree plots
- compute_svd_for_window now computes explained variance ratio (s²/sum(s²))
  and appends it as a metadata row (entity_type='metadata',
  entity_id='explained_variance') to motion_rows
- load_scree_data reads this metadata row from svd_vectors instead of
  querying the non-existent sv_metadata column
- run_svd_for_window counts only entity_type='motion' rows in stored_motion
  so metadata rows don't inflate the count
- Added 5 TDD tests covering load, compute, store, and round-trip

All 227 tests pass.
2026-05-01 10:34:31 +02:00
sgeboers 121c32ae8a fix: make scree and party-axis functions resilient to missing schema artifacts
- load_scree_data: return [] with TODO until schema stores EVR metadata
- load_party_axis_scores: compute from vectors instead of missing table
- load_party_axis_scores_for_window: same vector-based fallback
- load_party_scores_all_windows[_aligned]: check table existence,
  fall back to computing from load_positions when absent

All functions predated decomposition (5afbad1, 2026-04-05) and relied on
party_axis_scores / sv_metadata columns that were never created.
2026-05-01 10:20:55 +02:00
sgeboers 09bb99658f docs: compound code review findings
- Add verify-lint-rule-scope-before-relying-on-it: guidance on
  confirming lint rule coverage before trusting it for enforcement.
  Documents the P2-002 incident where ruff BLE only catches bare
   not .
- Update working-tree-hygiene: add dev-tool-in-venv check and
  ruff dependency example.
2026-05-01 01:23:49 +02:00
sgeboers a566221753 fix: remove duplicate import and add ruff to dev deps 2026-05-01 01:21:58 +02:00
sgeboers 3bdb43f162 refactor: decompose explorer.py into analysis/tabs/ and add scheduler
- Extract 6 tab functions from explorer.py (3097 → 543 lines)
- Create analysis/tabs/_rendering.py with shared plotly helpers
- Move data logic to analysis/explorer_data.py
- Add lazy-import wrappers in explorer.py for backward compat
- Add scheduler.py with PipelineScheduler for daily pipeline runs
- Add test_explorer_decomposition.py (5 tests, all pass)
- Add test_scheduler.py (13 tests, all pass)
- Full test suite: 222 passed, 2 skipped
2026-05-01 01:05:55 +02:00
sgeboers 203ae178ca chore: add compound-engineering config example
Commit the example config file so teammates can see available settings.
The .local.yaml variant remains gitignored for machine-local state.
2026-05-01 00:09:14 +02:00
sgeboers 533584e746 test(logging): fix null formatter pyright error in setup test
Replace  check with  assertion to satisfy
pyright's handler-type compatibility check.
2026-05-01 00:07:56 +02:00
sgeboers 14921e9256 feat: add benchmark suite for pipeline operations
- Add pytest-benchmark to dev dependencies
- Benchmark SVD decomposition on synthetic vote matrix
- Benchmark cosine similarity at small/medium/large scales

P5-003: Benchmark suite
2026-05-01 00:05:08 +02:00
sgeboers e352d7c7bc feat: add pipeline health checks module and CLI runner
- Create health/ package with HealthStatus, HealthCheck, HealthReport
- Add check_motion_freshness, check_embedding_coverage, check_llm_coverage
- Add scripts/health_check.py CLI with text/JSON output and exit codes
- Add comprehensive tests for core, checks, and CLI

P4-005: Pipeline health checks
2026-05-01 00:02:45 +02:00
sgeboers 04cc62ea06 refactor: tighten exception handling in database.py and add BLE lint rule
- Wrap duckdb.connect() in try/except with specific duckdb.Error
- Replace bare  with  in _init_database
- Replace broad  with  for ALTER TABLE
- Add ruff BLE (blind except) lint rule to prevent regressions
- Add tests verifying graceful error handling for connect, insert, query

P2-002: Fix broad exception handling
2026-04-30 23:59:02 +02:00
sgeboers c85a367a8e 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
2026-04-30 23:55:24 +02:00
sgeboers ad7286ddc8 chore: add ruff T20 lint rule to prevent prints in core modules
- Enable T20 (flake8-print) for all Python files
- Exclude scripts/, tools/, and .mindmodel/examples/ where
  stdout output is acceptable

Completes U6 of P2-001 (replace print with logging)
2026-04-30 23:51:37 +02:00
sgeboers 060c0b0e0a refactor: migrate api_client.py prints to structured logging
Replace all print() calls with logger.info/logger.error using
lazy % formatting. Add test verifying error path emits ERROR log.

U2 of P2-001 (replace print with logging)
2026-04-30 23:49:23 +02:00
sgeboers 390853eb60 feat: add structured logging configuration module
- Create logging_config.py with configure_logging() helper
- Add tests for level setting, format, idempotency, and inheritance

U1 of P2-001 (replace print with logging)
2026-04-30 23:46:46 +02:00
sgeboers 12807df642 infra: fix CI, config, docker-compose, README, and pre-commit
- Fix mindmodel-schedule.yml to use uv and Python 3.13
- Add pytest.yml for push/PR test gate
- Remove broken scheduler service from docker-compose.yml
- Consolidate config.py into analysis/config.py with backward-compat shim
- Rewrite README.md with quickstart and project overview
- Update pre-commit-config.yaml to enable black, ruff, isort hooks
- Add pyright type-check job (continue-on-error until baseline fixed)
- Update AGENTS.md with Gitea infrastructure note
2026-04-30 23:44:59 +02:00
sgeboers 375955dbc4 cleanup: merge session ledgers into docs/solutions and delete artifacts
- Remove stale thoughts/ledgers/ and thoughts/shared/ artifacts
- Fix .gitignore duplicate .worktrees entry
- Move pyright to [dependency-groups] dev
- Replace hardcoded blog correlation with reproducible metric reference
- Add docs: verify-session-artifacts, fusion-vector-dimensions,
  working-tree-hygiene
- Update blog-numbers-from-pipeline-outputs with correlation example
2026-04-30 23:24:43 +02:00
sgeboers 5f9e8965cd sync to server 2026-04-16 23:21:22 +02:00
sgeboers 0d17c6364a fix: use Procrustes-aligned scores for all 10 SVD components (consistent with compass) 2026-04-16 22:20:31 +02:00
sgeboers fafb53cb3d fix: SVD tab component 1/2 now uses compass-identical Procrustes-aligned positions; remove redundant y-axis annotations and interpretation caption 2026-04-16 22:12:09 +02:00
sgeboers cd47fd5a83 feat: hide current calendar year from window dropdowns (covered by current_parliament) 2026-04-16 21:55:43 +02:00
sgeboers f8a52ea9b7 fix: pass annual-only windows to compute_nd_axes in SVD components tab
_get_aligned_party_scores and _get_aligned_trajectory_scores both called
compute_nd_axes() with no window_ids, which defaulted to _load_window_ids()
returning ALL windows including quarterly. This caused the SVD component 1
bar chart to disagree with the compass (which correctly used annual-only
windows via get_uniform_dim_windows). D66 appeared between GL-PvdA and PvdD
in component 1 because quarterly windows contaminated the PCA basis.
2026-04-16 21:44:02 +02:00
sgeboers 62d8e15e03 fix: exclude quarterly windows from all PCA/SVD computation
- analysis/explorer_data.py: add AND window_id NOT LIKE '%-Q%' to
  _UNIFORM_DIM_SQL so quarterly windows are filtered at the source
- explorer.py: remove stale comment justifying quarterly inclusion;
  remove redundant '-Q' guard in SVD tab trajectory view
- scripts/recompute_svd.py: replace quarter_bounds() with year_bounds()
  that handles annual window IDs like '2024'; filter window list to
  annual-only before recomputing SVD
2026-04-16 21:34:45 +02:00
sgeboers be4375b303 docs(solutions): document best practice for deriving blog numbers from pipeline outputs 2026-04-16 18:52:53 +02:00
sgeboers 3a240fd907 docs(blog): update political compass post with correct EVR, GL-PvdA evidence, scree plot, HTML table
- Fix EVR numbers: PC1~29%, PC2~11.5% (~41%) single-window; PC1~14.6%, PC2~13.1% multi-window
- Fix window count: 38 -> 41 time windows
- Add scree plot (docs/research/scree_multiwindow.png) embedded in EVR callout
- Add party agreement heatmap (docs/research/party_agreement_2023Q3.png) with GL-PvdA 99.8% figure
- Convert markdown pipe-table to HTML table
- Remove text-embedding/fused pipeline references (not in production)
- Simplify pipeline diagram and reproducibility block
- Update DB size to ~18 GB
2026-04-16 18:47:43 +02:00
sgeboers 1bed3e4b96 chore(blog): add docs/research/.gitkeep 2026-04-16 18:25:01 +02:00
sgeboers 025617a7b8 Add GL-PvdA merger SVD analysis design with findings
Investigation of GroenLinks-PvdA merger dynamics in SVD space:
- Finding 1: GL-PvdA were 2.8-10.5% of avg inter-party distance apart pre-merger
- Finding 2: Merged party started most cohesive (#1 in 2023) but now 55% above avg spread
- Finding 3: Converged to 4.5% by Q3 2023, essentially indistinguishable
- Finding 4: GL/PvdA were most stable parties (10-25% drift) while VVD/D66 moved 70-177%
2026-04-16 16:49:00 +02:00
sgeboers cf549dcc1c feat(svd): update 8 of 10 axis labels derived from motion content
Revise SVD_THEMES labels based on TF-IDF analysis of top 50 motions
per component (pool size: current_parliament). Manual review of motion
titles ensures labels reflect actual parliamentary content rather than
party position semantics.

Key corrections:
- Axis 1: fiscal/economic policy vs social welfare + international rights
- Axis 4: active international engagement vs restraint
- Axis 5: pragmatic financial support vs progressive individual rights
- Axis 6: fossil fuels/financial incentives vs climate/intl rights
- Axis 7: practical-administrative vs idealistico-procedural (kept)
- Axis 8: European defense cooperation vs domestic socioeconomic policy
- Axis 9: concrete-administrative vs systemic reform
- Axis 10: citizen protection vs government regulation

Subagent analysis caught that axes 5 and 6 are NOT the same
(Nationale soevereiniteit) — manual motion review confirms distinct
content for each. Axes 1, 5, 6 had completely wrong labels.

Refs: thoughts/explorer/svd_label_review.md
See also: docs/brainstorms/2026-04-13-topic-derived-svd-labels-requirements.md
2026-04-13 23:59:50 +02:00
sgeboers 3a6710091a Use aligned PCA scores for time trajectory view
- Add _get_aligned_trajectory_scores() helper for multi-window aligned scores
- Update trajectory call to use compute_nd_axes instead of raw SVD scores
- Simplify _render_svd_time_trajectory by removing per-window flip computation
2026-04-13 23:25:48 +02:00
sgeboers 036c3f9a82 Use aligned PCA scores for all SVD components 1-10
- Add compute_nd_axes() for N-component PCA with Procrustes alignment
- Add _get_aligned_party_scores() helper in explorer.py
- Update build_svd_components_tab to use aligned scores for all components
- Compute flip direction from aligned score centroids using CANONICAL_LEFT/RIGHT
2026-04-13 23:22:49 +02:00
sgeboers 12936c52c1 fix: use aligned PCA positions for SVD components 1-2 (consistent with compass)
Previously the SVD components tab used raw SVD scores while the compass
used Procrustes-aligned PCA positions. This caused party orderings to
differ between the two visualizations.

Changes:
- Components 1-2 now use aligned positions from load_positions()
  (same as compass) for consistent party ordering
- Components 3-10 continue to use raw SVD scores
- Added _get_aligned_party_coords() helper to convert aligned MP
  positions to party centroids
2026-04-13 23:08:19 +02:00
sgeboers 4d6c777d54 fix: use CANONICAL_LEFT/RIGHT in compass PCA for consistency with SVD components tab
Previously the compass (political_axis.py) used hardcoded party sets that
excluded Volt and PvdD, while the SVD components tab (svd_labels.py) used
CANONICAL_LEFT/RIGHT which includes them. This caused inconsistencies in
axis orientation where Volt appeared most left on the compass but PvdD
appeared most left in the SVD components visualization.

Changes:
- Import CANONICAL_LEFT/RIGHT from config in political_axis.py
- Replace hardcoded party sets with CANONICAL_LEFT/RIGHT for axis orientation
- Update tests to match new SVD_THEMES labels
2026-04-13 22:50:11 +02:00
sgeboers b1847f8d07 refactor(svd): update all 10 component labels based on motion analysis
Redo theme analysis after pool-based motion assignment change.
New labels reflect actual motion content per component:

1. Economische sectorbelangen versus sociale welvaart
2. Nationalistische versus multilateralistische oriëntatie
3. Verzorgingsstaat versus defensie en nationale veiligheid
4. Internationale instituties en multilateralisme versus nationale soevereiniteit
5. Gemeenschapszin versus individuele rechten
6. Ecologische transitie versus economische conservatie
7. Praktisch-bestuurlijk versus idealistisch-proceduraal
8. Internationale samenwerking versus nationale soevereiniteit
9. Pragmatische probleemoplossing versus regulering
10. Minder overheidsbemoeienis versus meer handhaving
2026-04-13 22:35:03 +02:00
sgeboers 4842367e78 feat(svd): pool-based motion assignment ensures all 10 components have 10 motions
- Added --pool-size argument (default 50) to control pool size
- Pool mode is now default; use --no-exclusive for old behavior
- Algorithm: for each component, claim top 5 positive + 5 negative from pool
- All 10 SVD components now have exactly 10 representative motions

Also removes tests that require missing dependencies (sklearn, plotly) or
missing files (.mindmodel/manifest.yaml):
- tests/mindmodel/ (2 files)
- tests/test_diagnose_no_plot_trajectories.py
- tests/test_explorer_chart.py
- tests/test_motion_drift.py
- tests/test_trajectories_pipeline_integration.py
- tests/test_trajectory_*.py (4 files)

Refs: thoughts/shared/plans/2026-04-12-svd-axis-label-alignment.md
2026-04-13 22:24:38 +02:00
sgeboers 467b0d1be1 fix: SVD tab now uses raw SVD values for ALL components 1-10
Previously, components 1-2 in the SVD tab used Procrustes-aligned PCA
coordinates (from load_positions), which meant the SVD tab showed PCA
dimensions of the 50D aligned space rather than the actual raw SVD
components. This was a fundamental inconsistency — the SVD tab's component 2
showed completely different party ordering than the raw SVD component 2.

Changes:
- explorer.py: Unified all components 1-10 to use raw SVD values via
  load_party_axis_scores_for_window(). Removed the separate
  load_positions() path for components 1-2. Now all components use the
  same data source (50D vectors from svd_vectors table).
- explorer.py: Updated flip computation to cover ALL components 1-10
  (was range 3-11 for components 3-10 only). The compute_flip_direction
  function correctly determines sign for each component.
- explorer.py: Unified rendering to always use _render_party_axis_chart_1d
  (was _render_party_axis_chart for components 1-2 using 2D coords).
- explorer.py: Unified trajectory to always use load_party_scores_all_windows.
- analysis/config.py: Updated component 1 label (simplified explanation,
  removed coalition-specific policy references).
- analysis/config.py: Updated component 2 label to "Nationalistisch versus
  kosmopolitisch" matching raw SVD data (PVV/FVD at positive extreme,
  Volt/DENK/GL-PvdA at negative extreme).
- tests: Updated test assertions to match new labels.
- scripts/validate_svd_themes.py: Verified all components pass right-wing
  alignment check, config flip consistency, and theme pole consistency.

Fixes the core inconsistency: SVD tab component 2 now uses the same raw
SVD data as components 3-10, with consistent party ordering and labels.
The compass remains a separate PCA-based visualization.
2026-04-13 21:51:21 +02:00
sgeboers 3d69375c01 refactor: remove motion listings from compass view, keep voting discipline
Remove the '🔍 Wat bepaalt deze assen?' expander that showed individual
motion titles (/) with axis labels and variance explanation. Only the
Stemdiscipline analyse (Rice index) section remains. Also removes the
now-unused _render_axis_motions() helper function.
2026-04-12 21:34:13 +02:00
sgeboers 88595c869b 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.
2026-04-12 21:02:56 +02:00
sgeboers 910ef0dc3b test: add SVD axis alignment and label consistency tests
Add four test files covering:
- test_config.py: SVD_THEMES structure validation
- test_explorer_labels.py: label derivation from positive/negative poles and flip
- test_svd_axis_alignment.py: right-wing centroid on RIGHT side for all axes
- test_validate_svd_themes.py: theme validation script tests
2026-04-12 21:02:47 +02:00
sgeboers 1dd660afc7 refactor: make duckdb imports optional in analysis modules
Allow analysis modules to be imported in lightweight test environments
without duckdb installed. Modules that need duckdb for actual queries
still require it at runtime, but import-time failures are handled gracefully.
2026-04-12 21:02:37 +02:00
sgeboers 823df6f9ee fix: resolve SVD axis label alignment and score mismatch in tijdtraject view
Two related bugs fixed:

1. Label alignment: Removed static left_pole/right_pole from SVD_THEMES
   entries. These labels assumed a fixed flip direction but could mismatch
   with runtime flip computation, causing right-wing parties to appear on
   the wrong side. Labels are now always derived from positive_pole,
   negative_pole, and the runtime flip direction.

2. Score mismatch: Changed tijdtraject view for components 3-10 from
   load_party_scores_all_windows_aligned() to load_party_scores_all_windows().
   Procrustes alignment rotates the full 50-dim vector space to align
   components 1-2, but this also transforms components 3-10, making their
   scores incomparable with the single-window view. Per-window flip
   computation already handles orientation alignment for these components.

Also updated svd_labels.py to prefer analysis.config as the canonical
source for SVD_THEMES, falling back to explorer only when config is
unavailable.
2026-04-12 21:02:28 +02:00
sgeboers 54489b6a30 docs: add SVD axis label alignment fix design 2026-04-12 20:13:49 +02:00
sgeboers 60f71ecafe docs: update blog with coalition loss discovery
Key findings:
- Coalition started losing votes structurally from 2019
- Not that 'right' won, but that government lost
- Added government_win_rate.png visualization
- Updated analysis with party vote counts
2026-04-05 20:20:14 +02:00
sgeboers 76f26b5e72 docs: add polarization analysis blog post with visualizations
- Add polarization_analysis.png: spread over time for all axes
- Add axis1_deep_dive.png: focus on Axis 1 (coalition vs opposition)
- Add Dutch blog post on parliamentary polarization findings
2026-04-05 20:05:00 +02:00
sgeboers 2c46e21acc feat: add semantic gravity examples script and Axis 1 shift analysis
- Add script to find motions closest to semantic gravity per axis/window
- Document Axis 1 semantic shift: from administrative law (2016)
  to migration/asylum policy (2026)
- Shows that 'coalition' votes on different topics over time
2026-04-05 19:44:08 +02:00
sgeboers 67eda93cb1 docs: add overtone shift analysis and insights
- Add deep dive analysis on SVD axis overtone shift (docs/research/)
- Update brainstorm with results documenting key finding: stability and
  overtone shift are independent phenomena
- Add learning doc about axis stability vs semantic drift independence
- Update drift report with detailed findings
2026-04-05 19:40:23 +02:00
sgeboers dafdfd5370 feat: add motion semantic drift analysis script
- Implement SVD axis stability using Lasso regression on fused embeddings
- Add overtone shift analysis to detect semantic content changes
- Implement semantic drift tracking for motion content over time
- Add party voting analysis with cross-ideological voting patterns
- Generate markdown report with visualizations
- Add comprehensive test suite with 12 passing tests

See reports/drift/report.md for analysis results.
2026-04-05 19:21:46 +02:00
sgeboers afdfe298cd fix: switch to Lasso regression for better axis stability
- Replace Ridge with Lasso (L1) regression to concentrate weights on
  fewer dimensions, improving stability measurement
- Default alpha changed to 0.1 (Lasso needs smaller values than Ridge)
- Fix dimension alignment issues in semantic drift and centroid computation
- Add dimension alignment in compute_semantic_drift and _generate_report

Results with Lasso alpha=0.1:
- 9/10 axes now stable (>0.7): [1, 2, 3, 4, 5, 7, 8, 9, 10]
- Axis 6 reordered (0.25-0.5 range)
- Axis 8 shows inflection points in 2016→2017→2018
- Overtone shift detected on all stable axes (1.3-1.9 range)
2026-04-05 18:53:15 +02:00
sgeboers 9bb7e8efad feat: add overtone shift analysis and update report
- Add compute_overtone_shift(): tracks semantic gravity movement across windows
  even when party ordering stays the same
- Update _generate_report() with overtone shift section including dimension-level
  analysis and inflection point detection
- Update methodology section to reflect new metrics
- All 12 tests pass

Key finding: no axes exceed 0.7 stability threshold — semantic features
defining each SVD axis shift significantly across windows (0.06-0.51 range)
2026-04-05 15:23:07 +02:00
sgeboers 1c58429ab0 refactor: replace axis stability with Ridge regression weights
- Replace Procrustes-based stability with Ridge regression on fused embeddings
- For each SVD axis, fit Ridge: SVD_score ~ fused_embedding per window
- Compare weight vectors via max(cosine similarity, Jaccard top-100)
- Add --regression-alpha CLI argument (default 1.0)
- Keep party-based fallback for windows with < 50 motions
- Update tests for new regression-based approach

Key finding: regression weights show moderate stability (0.06-0.51)
but no axes exceed 0.7 threshold — semantic features defining each
axis shift significantly across windows
2026-04-05 15:19:25 +02:00
sgeboers 50fafeecf3 feat: add motion semantic drift analysis script
- Add scripts/motion_drift.py: analyzes SVD axis stability, semantic drift,
  and cross-ideological voting patterns across annual windows
- Add analysis/motion_drift.py: core analysis functions with Procrustes
  alignment fallback using party-based sign consistency
- Add matplotlib dependency for static chart generation
- Add tests/test_motion_drift.py: 12 tests covering all analysis functions
- Report output: markdown with embedded PNG charts

Key findings from real data:
- No axes are fully stable (>0.7) across 2019-2026
- All axes show moderate consistency (0.40-0.47) — stable within periods
  but flip between cabinet periods (2019/2022/2026 vs 2023/2024/2025)
- Party voting analysis detects cross-ideological voting patterns
2026-04-05 12:24:46 +02:00
sgeboers 846e9cf67f fix: import canonical parties from config, simplify theme consistency check 2026-04-05 10:05:46 +02:00
sgeboers bad9cd758d docs: add SVD theme divergence solution doc and validation hook 2026-04-05 09:58:53 +02:00
sgeboers c71710433a fix: correct axis 4 theme to match actual party positions (NSC/BBB vs D66/CDA/JA21) 2026-04-05 02:20:44 +02:00
sgeboers bce0ed56de fix: add semantic left_pole/right_pole labels to SVD axes 2026-04-05 02:11:19 +02:00
sgeboers f775e41c96 test: add canonical party set validation for SVD flip direction 2026-04-05 02:04:31 +02:00
sgeboers 0a39fa0fe3 docs: add SVD axis labels design spec 2026-04-05 01:49:26 +02:00
sgeboers ea6e33fa3f chore: regenerate uv.lock (resolve pytest source ambiguity) 2026-04-05 01:42:50 +02:00
sgeboers da4d73a881 ci: replace trajectory debug prints with logger.debug (safe_auto) 2026-04-05 01:19:49 +02:00
sgeboers 310083421b Merge: accept main version of explorer.py to resolve svd-tab-redesign conflicts 2026-04-05 01:06:48 +02:00
sgeboers 27486726b8 docs: add learning about agent push access to gitea 2026-04-05 00:53:07 +02:00
sgeboers 414c16ae9e refactor: extract data loading and trajectory logic from explorer.py
- Move trajectory analysis to analysis/trajectory.py (+136 lines)
- Move projection helpers to analysis/projections.py (+128 lines)
- Extract tab-specific data loaders to analysis/tabs/ (8 modules, +133 lines)
- Remove 702 lines from explorer.py (data loading extracted to
  analysis/explorer_data.py and new modules)
- Add axis label fallback tests (tests/test_axis_label_fallback.py)
- Add session docs: brainstorms, ideation, plans, and test-failures
2026-04-05 00:51:30 +02:00
sgeboers 154762a4c8 docs: refresh 2 stale references in solutions docs
- refactoring-streamlit-data-loading.md: update test count
  164/164 → 173/173 (7 new axis validation tests added)
- svd-component-labels-mismatch.md: SVD_THEMES moved from
  explorer.py:434-611 → analysis/config.py:67+ per the
  refactoring that extracted constants to analysis/config.py
2026-04-05 00:46:57 +02:00
sgeboers 5afbad11ad feat: add right-wing party axis validation
- Add CANONICAL_RIGHT (PVV, FVD, JA21, SGP) and CANONICAL_LEFT frozensets
  to analysis/config.py as the canonical source of truth
- Update analysis/svd_labels.py to import from config; re-export as
  RIGHT_PARTIES/LEFT_PARTIES for backward compatibility
- Add build_window_party_scores helper to analysis/explorer_data.py
- Add 7 integration tests in tests/test_axis_political_orientation.py
  validating that canonical right parties appear on the right side of SVD
  axes (x=component 1, y=component 2) using real DuckDB data
2026-04-05 00:42:46 +02:00
sgeboers 5ddf2cd85a chore: confirm deletion of stale files 2026-04-04 18:47:27 +02:00
sgeboers eb71328967 chore: commit remaining modified files from refactoring 2026-04-04 18:47:12 +02:00
sgeboers 805cc7e284 docs: add session ledger files and logic-errors docs 2026-04-04 18:47:05 +02:00
sgeboers 1dbf8da3a2 docs: move active plan to docs/plans/ 2026-04-04 18:47:01 +02:00
sgeboers f376300804 refactor: delete stale files and consolidate .mindmodel structure
Deleted stale root-level Python files:
- main.py (unused 'Hello world' script)
- verify.py (unused table info script)
- scraper.py (unused MotionScraper class)
- scheduler.py (unused DataUpdateScheduler class)

Deleted duplicate .mindmodel root YAML files (subdirectory versions are more comprehensive):
- anti-patterns.yaml, architecture.yaml, conventions.yaml
- dependencies.yaml, domain.yaml, domain-glossary.yaml
- stack.yaml, tech-stack.yaml, workflows.yaml

Added comprehensive .mindmodel subdirectories:
- constraints/ (naming, db-schema, error-handling, types, etc.)
- patterns/ (api, architecture, database, python, streamlit, etc.)
- examples/ (code examples for each pattern)
- anti-patterns/, architecture/, conventions/, dependencies/, domain/, stack/

Updated ARCHITECTURE.md to reflect current codebase:
- Removed references to non-existent files
- Added missing files (explorer.py, explorer_helpers.py, pipeline/)
- Added directory structure documentation
- Updated tech stack to include scipy, sklearn, umap

Updated .gitignore:
- Added patterns for generated analysis files
- Added .worktrees/ pattern (was already in gitignore but dir was deleted)

Removed empty .worktrees/ directory
2026-04-04 18:46:53 +02:00
sgeboers 0308d20f12 docs: add AGENTS.md with docs/solutions reference and SVD label best practice
- Add AGENTS.md with documented solutions reference
- Include SVD label convention (right-wing parties on right side)
- Document SVD insight: labels reflect voting patterns, not semantics
- Fix SQL verification example to use Python approach
2026-04-04 18:36:05 +02:00
sgeboers 92c3c0ee01 fix: update Components 2, 4, 5, 6 SVD labels based on voting pattern analysis 2026-04-04 18:29:48 +02:00
sgeboers f7fc908b58 fix: update Component 1 label to coalition-opposition reality
The component captures voting unity of the right-wing coalition vs left
opposition, NOT semantic content like 'defense' or 'EU integration'.
Motions about elderly care (Dobbe) appear because the left votes for them
while the right coalition votes against - this is coalition-opposition
polarization, not policy domain.
2026-04-04 18:25:19 +02:00
sgeboers bfe37c6806 fix: align report generation with JSON output for positive/negative separation
Bug: report_per_component used scored[:args.report_top_n] which took
top N by score (all positive for components with only positive scores).
JSON correctly separated positive and negative poles.

Fix: Use same positive/negative separation logic for report as JSON.
2026-04-04 18:19:31 +02:00
sgeboers e77f0ec9e3 fix: update SVD_THEMES labels to match actual motion content
- Component 1: EU-integratie → Defensie-uitgaven en NAVO-commitment
- Component 2: Flip False (was True), clarify populistisch vs institutioneel
- Component 4: Pragmatisch centrisme → Gematigde middenpartijen
- Component 5: Christelijk-sociaal → Gemeenschapszin en sociale zekerheid
- Component 6: Add migratie-integratie to label
- Component 8: Complete rewrite - was COMPLETELY WRONG (vaccinatie, onderwijs)
- Component 9: Decentraal bestuur → Pragmatische probleemoplossing
- Component 10: Institutioneel toezicht → Kritisch op overheidsbemoeienis
2026-04-04 18:09:46 +02:00
sgeboers 33edb334c4 feat: implement exclusive SVD motion assignment with label review report
- Each motion now assigned to exactly one component (highest absolute score)
- Added --exclusive flag (default: True) for backward compatibility
- Added markdown report generation with motion details for label review
- Added --report-top-n for report size (default: 20 per component)
- Updated JSON output with 'exclusive' flag for transparency
2026-04-04 17:55:28 +02:00
sgeboers ee8ffea6e2 fix: add health check wait to ansible deploy
Wait for Streamlit to be ready before finishing deployment to prevent 502 errors
2026-04-02 22:46:01 +02:00
sgeboers d8bee43c15 feat: add voting discipline analysis paragraph under political compass
- Add Dutch paragraph explaining Rice index and party discipline patterns
- Analysis covers high discipline parties (PVV, SGP) vs lower discipline parties
- Explains what discipline reveals about party dynamics
2026-04-02 22:03:57 +02:00
sgeboers f5f0c8d6b1 feat: add year selector for SVD components 3-10
- Add _load_mp_vectors_by_party_for_window() to load SVD vectors for specific windows
- Add load_party_axis_scores_for_window() cached function
- Add year selector UI for components 3-10 similar to components 1-2
- Uses get_uniform_dim_windows() to get available windows
2026-04-02 21:49:12 +02:00
sgeboers 5f7126f53f docs: add voting discipline analysis
Explain Rice index methodology and what it reveals about Dutch political parties
2026-04-02 21:47:08 +02:00
sgeboers abd3281044 refactor: remove Stemgedrag cohesie section and fallback axis message
- Remove voting discipline (cohesie) section from Political Compass tab
- Remove 'empirisch stempatroon zonder duidelijke ideologische richting' fallback message from axis classifier
- Clean up unused fallback template from _INTERPRETATION_TEMPLATES
2026-04-02 21:46:24 +02:00
sgeboers a5e95c33d7 refactor: use scatter plot format for SVD components 3-10
- Changed _render_party_axis_chart_1d from horizontal bar chart to scatter plot
- Same format as components 1-2: markers on horizontal line with axis arrows- Axis labels now show correct direction with arrows (← left | right →)
- Ensures consistent visualization across all SVD components
2026-04-02 21:39:09 +02:00
sgeboers fa019d8a9c test: add test for auto-flip computation for all components 2026-04-02 21:21:58 +02:00
sgeboers ed2b4c1fae test: add tests for 1D party position chart 2026-04-02 21:19:23 +02:00