Commit Graph
72 Commits
Author SHA1 Message Date
sgeboers 57083e496d chore(ansible): add ansible.cfg with SSH agent forwarding for private repo access 2026-03-28 20:25:36 +01:00
sgeboers c0d1c59bd7 chore(deploy): rewrite ansible for motief on motief.sgeboers.nl, remove drone
- Replace gtfs/bokeh deploy with motief/streamlit (port 8501)
- Update inventory to motief.sgeboers.nl
- Remove stale .drone.yml
- Add CI guard to forbid .env in repo
- Add env removal report and secrets rotation checklist
2026-03-28 20:12:36 +01:00
sgeboers 35f4667982 chore(secrets): stop tracking .env and add to .gitignore 2026-03-28 20:02:54 +01:00
sgeboers 445f0bfb24 feat(ansible-example): add @ansible/example package, tests, CI, publish & deploy workflows, docs and changelog 2026-03-28 20:00:31 +01:00
sgeboers b5c14d0c65 deploy to server 2026-03-26 22:52:11 +01:00
sgeboers c3f74433b2 chore(data): regenerate top SVD motions JSON with party-expanded SVD vectors 2026-03-25 23:49:35 +01:00
sgeboers 6cb89126a7 fix(svd): expand party votes to individual MPs before SVD computation
The mp_votes table contains both party-aggregate rows (e.g. 'PVV', 'NSC')
and individual MP rows (e.g. 'Aardema, M.'). Running SVD on both together
creates a block-diagonal vote matrix where party codes and individual MPs
occupy disjoint SVD dimensions — causing dim 0 to be zero for all 421 MPs.

Fix: _build_expanded_rows() converts every party-level vote to individual
MP votes using mp_metadata date ranges (active MPs on motion date). Motions
that already have individual MP records are kept as-is. A party name mapping
handles NSC/Nieuw Sociaal Contract and other canonical name variants.

Results for current_parliament: 517 individual MPs, all 8732 motions covered,
dim 0 std=23.1 (was 0.0 for all MPs). PVV/NSC/BBB on positive end, SP/GL/PvdD
on negative end — matches expected left-right political axis.

All 11 annual windows (2016-2026) re-run with the new pipeline.
2026-03-25 23:48:55 +01:00
sgeboers a7517bb6ae fix(svd): use averaged individual MP vectors for party axis scores, fix scree to %, re-add component 4
- load_party_axis_scores now loads individual MP vectors and averages
  per party (same data source as the political compass), so SVD axis
  rankings are consistent between the two tabs. Previously it used
  party-aggregate rows which gave structurally 0 signal on dim 3.

- Re-add component 4 (dim 3) to SVD_THEMES and revert comp_options
  filter — with individual MPs averaged, dim 3 now shows real party
  separation (the 'publieke voorzieningen vs marktwerking' axis).

- Scree plot y-axis now shows percentage of total variance instead of
  raw L2-norms; hover also updated to show '% van totaal'.
2026-03-25 23:06:35 +01:00
sgeboers 0a2238f2ba fix(svd): remove component 4 from SVD_THEMES (dim 3 has zero party signal)
Dim 3 captures within-party individual disagreement (MPs splitting
from party lines). Party-aggregate votes are structurally 0 on this
dimension, so the axis chart showed all parties at ~0 with no
discrimination. Confirmed via dry-run: identical to existing data.

Also filter comp_options to only show components with a defined theme,
so component 4 is hidden from the selectbox entirely.
2026-03-25 22:56:47 +01:00
sgeboers 26acd8b964 fix: scree plot uses party vectors instead of individual MPs
current_parliament has two separate SVD data spaces mixed together.
Party vectors (entity_id without comma) carry the between-party signal
in dims 0-15. Individual MP vectors only have signal in dim 3 and
dims 16-49 (within-party variance). The axis chart uses party vectors,
so the scree must too.
2026-03-25 22:44:57 +01:00
sgeboers 559e1adb82 fix(compass): fix annual window detection in get_uniform_dim_windows
Previous query used DISTINCT ON without ordering by dim, picking arbitrary
(often non-50) dim per window. Rewritten to find the dominant dim per window
(highest count) and include only windows where dominant dim = 50 with >= 10
entities. This surfaces annual windows 2016/2018/2019/2022-2026 that were
previously excluded due to mixed-dim rows from multiple pipeline runs.
2026-03-25 22:41:35 +01:00
sgeboers c386073430 feat(compass): switch to annual SVD windows by default
- load_positions annual mode now selects actual annual window_ids
  ('2022', '2023', etc.) instead of Q4 quarterly approximations,
  with current_parliament appended as the most-recent anchor
- Sidebar radio defaults to 'Per jaar' (annual) instead of quarterly
- Dutch labels for window size radio: 'Per jaar' / 'Per kwartaal'
2026-03-25 22:37:01 +01:00
sgeboers 96ea4c5522 feat(compass): fix duplicates, axes, controls, add party/MP toggle
- Add import re (needed for strip_paren deduplication)
- Deduplicate MPs with parenthetical first-name variants (e.g. 'Dijk, J.P.'
  and 'Dijk, J.P. (Jimmy)') by stripping parens and averaging positions;
  fixes 22 duplicate groups across all windows
- Replace select_slider with selectbox for time window control
- Remove non-functional 'Toon namen' checkbox and its usage
- Remove 'Min. MPs per partij' slider and party-size filter
- Add 'Weergave' radio toggle: Kamerleden (individual MPs) vs Partijen
  (party centroids computed as mean x/y per party)
- Fix axis ranges: x [-1, 1], y [-0.6, 0.6]
- Party view shows party abbreviation as dot label and hover shows MP count
2026-03-25 22:24:05 +01:00
sgeboers cf22ffc093 fix(explorer): fix scree plot data and add bar+line combo chart
- Use all individual MPs (not party aggregates) for L2-norm computation;
  party-aggregated vectors have near-zero values on some dims due to
  Procrustes alignment, producing spurious zeros
- Sort importances descending so scree plot is properly monotonic
- Relabel x-axis as 'Rang' since dim ordering after Procrustes alignment
  no longer matches original singular value order
- Add Scatter line trace connecting bar tops for elbow visibility
2026-03-25 22:11:09 +01:00
sgeboers c5cbc89c1f feat(explorer): add scree plot and clean up SVD axis chart
- Add load_scree_data() cached loader computing L2-norm of party scores
  per SVD dimension as a proxy for component importance
- Add _render_scree_plot() rendering a bar chart of the first 15 components
- Insert scree plot + Dutch explanation at the top of build_svd_components_tab
- Clean up _render_party_axis_chart: remove tick numbers, axis line, grid,
  and zero-line from the x-axis (pole labels remain as chart title)
2026-03-25 22:03:01 +01:00
sgeboers a20bd834fc chore(repo): remove stale scripts, caches, and old workflow
Cleanup performed by assistant: removed generated caches and stale files: __pycache__, *.pyc, .pytest_cache, .ruff_cache, dummy/, test.py, read.py, reset.py, fix_database.py, thoughts/thoughts/, .github/workflows/mindmodel-validate.yml. No push performed.
2026-03-25 21:37:57 +01:00
sgeboers 867fcd1989 feat: recompute SVD for full current parliament (2023-11-22 to 2026-03-17)
Adds new SVD window 'current_parliament' covering 8732 motions and 451 MPs
(vs 7424 motions in old '2025' window, adding ~1300 motions from 2025-Q4+).
Updates explorer.py to query the new window. Regenerates top_svd_top_motions.json.
Also clarifies axis 3 explanation noting FVD's anti-American positioning.
2026-03-25 01:16:33 +01:00
sgeboers 9daa899885 fix: remove motion title truncation, add SVD JSON generation script
Removes the raw_title[:80] cap on expander labels so full titles show.
Adds scripts/generate_svd_json.py to regenerate top_svd_top_motions.json
from any SVD window after a recompute.
2026-03-25 01:15:42 +01:00
sgeboers 49a1f2f67d fix: correct axis 4 and 5 pole labels and flip orientation in SVD_THEMES
Axes 4 and 5 had inverted sign conventions relative to actual party votes.
Diagnostic confirmed SP/PvdD scored negative on axis 4 (free trade motions)
and FVD scored negative on axis 5 (secular motions), opposite to their
voting behaviour. Fix: swap positive_pole/negative_pole for both axes and
set correct flip direction so progressive parties appear on the left.
2026-03-25 00:43:18 +01:00
sgeboers 361cf9fd35 feat(explorer): SVD tab redesign — pole-split motions, party axis chart, inline expanders with voting
- Add ChristenUnie colour alias and CURRENT_PARLIAMENT_PARTIES frozenset (15 parties)
- Add load_party_axis_scores() — queries party SVD vectors from window=2025, cached
- Add _render_party_axis_chart() — 1D Plotly scatter of party positions per axis
- Restructure build_svd_components_tab: replace session-state button/detail-pane with
  inline st.expander per motion, split into pos/neg pole columns, batch DB query for
  all 10 motions including voting_results, rendered via _render_voting_results

Smoke-tested: 15 parties loaded, all 10 axis-1 motions returned with voting data.
2026-03-25 00:11:37 +01:00
sgeboers 9caaa8baca feat(explorer): finalise SVD tab helper robustness and constants
Include plan: docs/superpowers/plans/2026-03-24-svd-tab-redesign.md
2026-03-25 00:04:54 +01:00
sgeboers 32fe3aed18 feat(explorer): harden _render_party_axis_chart axis bounds and range
Plan: docs/superpowers/plans/2026-03-24-svd-tab-redesign.md
2026-03-25 00:01:18 +01:00
sgeboers 9f3ae15a16 feat(explorer): harden load_party_axis_scores (close DB, deterministic params)
Plan: docs/superpowers/plans/2026-03-24-svd-tab-redesign.md
2026-03-25 00:01:12 +01:00
sgeboers 35dbc8118a feat(explorer): add load_party_axis_scores helper
docs/superpowers/plans/2026-03-24-svd-tab-redesign.md
2026-03-24 23:56:37 +01:00
sgeboers 521385c832 docs: add SVD tab redesign implementation plan 2026-03-24 23:52:54 +01:00
sgeboers 8c79aaf917 docs: add SVD tab redesign spec 2026-03-24 23:50:40 +01:00
sgeboers c96c681641 feat(explorer): apply rigorous subagent-produced SVD axis themes
Replace draft SVD_THEMES with themes produced by per-axis analysis of all
10 unique top motions (zero cross-axis overlap, window=2025). Each axis now
has a detailed Dutch-language explanation, positive_pole and negative_pole
labels, displayed as colour-coded columns in the UI.
2026-03-24 23:38:50 +01:00
sgeboers b85a9cebd7 fix(data): remove 18 duplicate motions and redo SVD top-motions analysis
Deduplication:
- Identified 18 motion pairs with identical body_text and externe_identifier
- Kept the lower ID (first inserted) from each pair
- Cascaded deletes: 18 motions, 18 embeddings, 28 svd_vectors, 23 fused_embeddings
- motions table: 28172 → 28154, zero body_text duplicate groups remaining

SVD analysis:
- Regenerated top_svd_top_motions.json for window=2025 with clean data
  (7424 vectors, down from 7430)
- 100 unique motions across 10 axes, no title or ID duplicates
- De Vos huiseigenaren motie no longer appears twice in axis 3
2026-03-24 23:29:18 +01:00
sgeboers 8b73ab8bce feat(explorer): add SVD axis themes and unique-motion deduplication
- Regenerated top_svd_top_motions.json for window=2025 with strict
  cross-axis deduplication: 100 unique motions across 10 axes (10 per
  axis, zero overlap), sorted by absolute SVD score
- Added SVD_THEMES dict to build_svd_components_tab with Dutch-language
  theme label and political-polarisation explanation for each of the 10
  axes (e.g. 'Confessioneel-conservatief vs. seculier-progressief')
- Selectbox now shows 'As N — <theme>' instead of bare component number
- Each selected axis shows an info banner with the full explanation
- Motion list buttons show ▲/▼ to indicate positive/negative SVD loading
- Translated UI strings to Dutch for consistency
2026-03-24 23:11:51 +01:00
sgeboers e29d8a8055 ci(mindmodel): add scheduled validation workflow 2026-03-24 22:41:43 +01:00
sgeboers 2755dc373a chore(format): add pre-commit and formatter configs 2026-03-24 22:41:40 +01:00
sgeboers c24ab48704 chore(deps): move pytest to dev-dependencies 2026-03-24 22:41:36 +01:00
sgeboers 540099f2b7 ci(mindmodel): add report-only mindmodel validation workflow 2026-03-24 22:41:33 +01:00
sgeboers d1faf2b3e4 feat(mindmodel): add CLI wrapper, edge-case tests, and manifest schema tests 2026-03-24 22:41:28 +01:00
sgeboers ed289ff582 chore(mindmodel): add sanitized read-only manifest and reviewer README 2026-03-24 22:41:24 +01:00
sgeboers f091846dc8 feat(mindmodel): add report-only validator skeleton, types, and tests 2026-03-24 22:41:15 +01:00
sgeboers dbd8cc801a docs(mindmodel): add README and tooling note; chore(tooling): add minimal pre-commit 2026-03-24 21:31:53 +01:00
sgeboers 5caebe1331 chore(ci): add mindmodel validator workflow 2026-03-24 21:30:45 +01:00
sgeboers f77875ed54 feat(mindmodel): add CLI wrapper and tests 2026-03-24 21:29:10 +01:00
sgeboers a74e6006f5 feat(mindmodel): add validator and tests 2026-03-24 21:28:17 +01:00
sgeboers 7bd7d0d18c feat(mindmodel): add checks utilities and tests 2026-03-24 21:26:38 +01:00
sgeboers 2efd7ba3a0 feat(mindmodel): add manifest loader and tests 2026-03-24 21:24:58 +01:00
sgeboers 9c82962d47 chore: add .mindmodel/ project constraints and conventions
Generated mindmodel covering stack, architecture, domain glossary,
coding conventions, DuckDB/requests/embeddings/error-handling patterns,
anti-patterns, and explicit constraints for naming, imports, DB access,
error handling, and testing.
2026-03-24 20:51:53 +01:00
sgeboers 504400faf2 fix: quiz seed from motions with actual individual MP votes
Root causes:
- Seed selection sorted by controversy_score across all 28k motions, but
  only 282 have individual MP vote records. Top controversial motions only
  have party-level votes, so match_mps_for_votes always returned empty.
- global_db singleton was used for match/discriminate instead of the db_path
  passed to the tab builder.

Fixes:
- Add MotionDatabase.get_motions_with_individual_votes(k) which queries
  motions with comma-formatted mp_name votes, ordered by controversy_score
- Replace broken seed logic in build_mp_quiz_tab with this new method
- Replace global_db usages with a local MotionDatabase(db_path) instance
- Guard against motion IDs present in votes but absent from motions DataFrame
2026-03-24 20:29:05 +01:00
sgeboers 238d9e9ec2 test: add full quiz tab test suite and fix Geen stem normalization
- Fix match_mps_for_votes: 'Geen stem'/'no vote' now normalize to None (skipped),
  not 'afwezig' — so unanswered questions don't inflate overlap count
- Add 5 additional tests to test_match_mps.py: zero-overlap exclusion,
  empty input validation, Geen stem overlap skip, excluded motions respected
- Add tests/test_explorer_quiz.py: builder import smoke test plus 3 real-DB
  end-to-end scenarios (unique match, indistinguishable MPs, discriminating
  question reduces candidate set)
- Full suite: 73 passed, 2 skipped
2026-03-24 20:18:20 +01:00
sgeboers eb73275f32 feat(mp-quiz): add MP quiz tab and DB helpers; add design and plan docs 2026-03-24 19:59:25 +01:00
sgeboers b09e580f65 feat: motion content enrichment pipeline hardening
- ai_provider_wrapper: retry/fallback with exponential backoff, None sentinel for failed items
- text_pipeline: use wrapper, return 5-tuple (stored, skipped_existing, skipped_no_text, errors, failed_ids)
- similarity/compute: filter trivial 1.0 matches on identical short titles (<12 chars)
- rerun_embeddings: --retry-missing mode, calls ensure_text_embeddings_for_ids on failed ids
- sync_motion_content: per-ext_id retries, HTTPAdapter pool, --max-body-workers CLI flag, audit on failure
- qa_similarity script: samples motions, writes JSON ledger to thoughts/ledgers/
- All tests green: 61 passed, 2 skipped
2026-03-23 21:31:39 +01:00
sgeboers aef7c45074 Refactor tests: replace sys.modules hacks with real DI + in-memory DB
- Add db=None, embedder=None params to ai_provider_wrapper, text_pipeline, compute_similarities
- New conftest.py: FakeEmbedder, mem_db (in-memory DuckDB), fake_embedder fixtures
- Rewrite test_ai_provider_wrapper (4 tests), test_rerun_embeddings_retry (2 tests), test_similarity_compute_filter (1 test) with real implementations
- Fix rerun_embeddings tests hanging on _get_all_windows by patching it alongside _clear_embeddings
- All 53 tests pass (2 skipped), 0 sys.modules hacks in refactored files
2026-03-23 21:21:10 +01:00
sgeboers b7350d8f87 test: rewrite test_database_audit using mem_db fixture, no disk writes required 2026-03-23 21:01:09 +01:00
sgeboers e4f2c7ff59 fix: update integration test to unpack 5-tuple from ensure_text_embeddings 2026-03-23 20:30:35 +01:00