feat(overton): add category domain decomposition with interactive charts and TDD tests

Populated the right_wing_motions.category column (previously 100% NULL across
3,030 motions) via parallel subagent classification — 80 agents derived a
10-category taxonomy and classified all motions in minutes.

Adds to the Overton QMD report:
- Plotly dropdown filter on Chart 1 to toggle between policy categories
- Chart 7: category delta bar chart (pre/post centrist support per domain)
- Chart 8: quarterly domain trajectories for the 5 largest categories
- Domain Decomposition narrative section

Also fixes a Streamlit tab crash (m.text -> m.body_text) and adds TDD tests.
main
Sven Geboers 4 days ago
parent a5624f65bc
commit 19e8d5b8ba
  1. 2
      analysis/tabs/overton.py
  2. 167
      docs/plans/2026-06-15-001-feat-category-domain-decomposition-plan.md
  3. 98
      docs/solutions/best-practices/motion-category-classification-subagent-pipeline-2026-06-15.md
  4. 2
      reports/overton_window/STATUS.md
  5. 46
      reports/overton_window/breakpoint_analysis.md
  6. BIN
      reports/overton_window/breakpoint_figure_1.png
  7. BIN
      reports/overton_window/breakpoint_figure_2.png
  8. 2
      reports/overton_window/causal_timing.md
  9. 50
      reports/overton_window/extremity_2d_temporal.md
  10. BIN
      reports/overton_window/extremity_2d_temporal_figure.png
  11. 10
      reports/overton_window/left_wing_response.md
  12. BIN
      reports/overton_window/left_wing_response_figure.png
  13. 2
      reports/overton_window/mechanism_classification.md
  14. 216
      reports/overton_window/overton_window.qmd
  15. 4
      reports/overton_window/overton_window_synthesis.md
  16. 2
      reports/overton_window/party_differentiation.md
  17. 70
      reports/overton_window/predictive_model.md
  18. BIN
      reports/overton_window/predictive_model_figure.png
  19. 2
      reports/overton_window/success_correlation.md
  20. 56
      reports/overton_window/temporal_trajectory.md
  21. BIN
      reports/overton_window/temporal_trajectory_figure.png
  22. 2
      reports/overton_window/voting_margin.md
  23. 116
      tests/test_category_overton.py
  24. 589
      thoughts/ledgers/audit_events.json

@ -182,7 +182,7 @@ def _render_motion_browser(con: duckdb.DuckDBPyConnection) -> None:
st.subheader("Right-Wing Motions Browser")
df = con.execute("""
SELECT r.year, r.title, m.text, r.centrist_support_strict, r.category
SELECT r.year, r.title, m.body_text, r.centrist_support_strict, r.category
FROM right_wing_motions r
LEFT JOIN motions m ON r.motion_id = m.id
WHERE r.classified = TRUE

@ -0,0 +1,167 @@
---
title: "feat: Category domain decomposition for Overton report"
type: feat
status: active
date: 2026-06-15
---
# feat: Category domain decomposition for Overton report
## Summary
Add 3 new Plotly charts and a narrative section to `reports/overton_window/overton_window.qmd` that decompose the Overton shift by policy category (asiel/vreemdelingen, landbouw/natuur, energie/klimaat, etc.), making visible which domains drove the shift, which resisted it, and how each category's centrist support evolved over time. The yearly CS chart gains category filtering via Plotly dropdown menu.
---
## Problem Frame
The current Overton report treats all right-wing motions as a single aggregate. The synthesis mentions migration vs non-migration, but with 10 categories now populated across all 3,030 motions, we can show a richer picture: energie/klimaat had the second-largest CS surge (+0.107), landbouw/natuur actually declined (−0.063), and onderwijs/wetenschap barely moved (+0.053) despite being the highest-consensus domain. These stories are invisible in the current charts.
---
## Requirements
- R1. Add a horizontal bar chart showing pre/post centrist support delta per category, sorted by magnitude
- R2. Add category filtering to the yearly CS timeline (Chart 1) via Plotly dropdown menu, so users can view a single category or "All right-wing"
- R3. Add domain trajectories to the quarterly chart showing 4-5 key categories as separate lines
- R4. Add a "Domain Decomposition" narrative section with the category delta table and interpretive prose
- R5. All new charts must use the existing Plotly styling conventions (colors, template, height)
---
## Scope Boundaries
- No changes to the synthesis markdown or other analysis scripts
- No changes to the DB or data pipeline
- No interactive filtering beyond Plotly's built-in updatemenu (no Streamlit/JS)
---
## Key Technical Decisions
- **Plotly updatemenu for filtering:** Uses Plotly's built-in `updatemenus` with `buttons` to toggle between "All right-wing" (current line) and individual categories. No external JS or Dash needed.
- **Color scheme for categories:** Use a qualitative 10-color palette (Plotly `alphabet` or `set2`), not PARTY_COLOURS, to avoid confusion with party lines.
- **New chart cells inserted after existing Chart 1:** Category delta bar chart goes after the yearly CS chart (Chart 1) and its narrative. Domain trajectories go in the quarterly section (Chart 6). A new "Domain Decomposition" section links them.
---
## Open Questions
### Resolved During Planning
- **Standalone HTML filtering:** Plotly updatemenu works in standalone HTML (confirmed via existing usage in the repo — the Overton report is a standalone HTML file with embedded Plotly).
### Deferred to Implementation
- **Whether to show all 10 categories or a curated subset:** The quarterly trajectories chart should show at most 5-6 lines to avoid visual clutter. The delta bar chart can show all 10.
- **Exact Plotly color assignment to each category:** Will match categories to a qualitative palette at implementation time.
---
## Implementation Units
- U1. **[Add category delta bar chart and dropdown-filtered yearly CS]**
**Goal:** Two linked chart additions: (1) a category delta horizontal bar chart, (2) convert Chart 1's yearly CS line chart to support category filtering via Plotly updatemenu.
**Requirements:** R1, R2, R5
**Dependencies:** None
**Files:**
- Modify: `reports/overton_window/overton_window.qmd`
**Approach:**
- Compute yearly CS per category via SQL: `SELECT year, category, AVG(centrist_support_strict) as cs, COUNT(*) as n FROM right_wing_motions WHERE classified = TRUE GROUP BY year, category`
- For the delta chart (new cell): compute pre/post CS per category, then use a horizontal bar (go.Bar with orientation='h'), sorted by delta descending. Color bars: green for positive delta, red for negative.
- For Chart 1 modification: extend the existing yearly SQL to GROUP BY year, category. In Python, pivot to get per-category columns. Create a go.Figure with all categories as traces plus an aggregate "All right-wing" trace. Add an updatemenu dropdown with buttons: "All right-wing" (restores all visible with only the aggregate line shown) and each category name (shows only that category's trace).
- Keep the existing pre/post mean lines and the break-year vertical line intact. The dropdown only controls which category trace is visible.
- Use a 10-color qualitative palette from plotly.express.colors.qualitative.
**Patterns to follow:**
- Existing Chart 1 for SQL pattern, figure layout, annotation style
- Existing Chart 6 for quarterly trajectory styling
**Test scenarios:**
- N/A — this is a Quarto document rendering change. Verify by rendering the QMD and checking that (a) the delta chart shows all 10 categories, (b) the dropdown in Chart 1 cycles through categories correctly, (c) pre/post mean lines remain visible in all views.
**Verification:**
- `uv run quarto render reports/overton_window/overton_window.qmd` succeeds
- `overton_report.html` contains the new delta chart section and Chart 1 responds to dropdown interaction
---
- U2. **[Add domain trajectories to quarterly chart]**
**Goal:** Enhance the existing quarterly chart (Chart 6) by overlaying 4-5 key category lines alongside the aggregate.
**Requirements:** R3, R5
**Dependencies:** U1 (the SQL for per-category yearly CS shares the same data approach; the quarterly chart needs its own SQL)
**Files:**
- Modify: `reports/overton_window/overton_window.qmd`
**Approach:**
- Select 5 categories to show as individual lines: asiel/vreemdelingen, energie/klimaat, buitenland/europa, landbouw/natuur, economie. These are the categories with largest deltas, largest volumes, or most story value.
- Compute quarterly CS per category: `SELECT EXTRACT(YEAR FROM m.date) AS y, CEIL(EXTRACT(MONTH FROM m.date) / 3.0) AS q, r.category, AVG(r.centrist_support_strict) AS cs, COUNT(*) AS n FROM right_wing_motions r JOIN motions m ON r.motion_id = m.id WHERE r.classified = TRUE AND m.date IS NOT NULL AND r.category IN (...) GROUP BY y, q, r.category`
- Add each category as a separate go.Scatter trace with distinct colors and dashed lines (to distinguish from the aggregate solid line).
- Keep the existing aggregate line in solid bold. Use the same inflection/peak annotations.
- Add a legend entry for each category.
**Patterns to follow:**
- Existing Chart 6 for quarterly SQL, figure layout, inflection/peak annotations
**Test scenarios:**
- N/A — verify by rendering and checking that 5 category traces appear with distinct colors and dashed styles alongside the aggregate.
**Verification:**
- `uv run quarto render reports/overton_window/overton_window.qmd` succeeds
- The quarterly chart shows 5 category lines with a legend
---
- U3. **[Add Domain Decomposition narrative section]**
**Goal:** Add a heading-2 section "Domain Decomposition" between Indicator 1 and Indicator 2, with a markdown table of category deltas and 2-3 paragraphs of interpretive prose.
**Requirements:** R4
**Dependencies:** U1 (the delta data is used; the table can be hardcoded from the pre-computed values or computed via inline SQL)
**Files:**
- Modify: `reports/overton_window/overton_window.qmd`
**Approach:**
- Insert the section after the existing Indicator 1 narrative text and before the "## Indicator 2: Spatial Divergence" heading.
- Include a markdown table with columns: Category, Pre-2024 CS, Post-2024 CS, Delta, Volume, Extremity gap (M−S). Use the known data from the DB queries.
- Write 3 paragraphs:
1. Overview: which categories drove the shift (migration, energy, foreign affairs) and which resisted (agriculture, healthcare, infrastructure).
2. The polarization paradox: landbouw/natuur and zorg/gezondheid as domains where centrist support declined despite content moderation.
3. The consensus domains: onderwijs/wetenschap and economie as stable high-CS categories where the window didn't need to shift.
- Reference the category delta chart (U1) and domain trajectories chart (U2) by their cell labels.
**Patterns to follow:**
- Existing prose style in Indicator 1 and Indicator 2 sections
**Test scenarios:**
- N/A — prose section. Review for factual accuracy against the DB data.
**Verification:**
- Section renders in the HTML output with correct numbers and coherent prose
---
## System-Wide Impact
- **Interaction graph:** The QMD is the only file. No analysis scripts, DB schemas, or other artifacts are affected.
- **Unchanged invariants:** All existing charts, narrative sections, and data remain intact. New cells are added after existing ones, and the new section is inserted between existing sections.
---
## Risks & Dependencies
| Risk | Mitigation |
|------|------------|
| Plotly updatemenu may not fully render in Quarto HTML output | Test with a minimal prototype in an isolated QMD cell first; fallback is a faceted chart showing all categories as subplots |
| 5 category lines on the quarterly chart may be visually noisy | Use dashed lines for category traces and a solid bold line for aggregate. If too noisy, reduce to 3 categories |

@ -0,0 +1,98 @@
---
title: "Motion category classification via parallel subagent pipeline"
date: 2026-06-15
category: best-practices
module: analysis/right_wing
problem_type: best_practice
component: development_workflow
severity: medium
applies_when:
- "classifying thousands of items into policy categories using LLMs"
- "sequential LLM batch pipelines time out or run too slowly"
- "a classification taxonomy can be derived from a sample rather than predefined"
- "items are independently classifiable with no cross-item state"
tags:
- motion-classification
- subagent-dispatch
- parallelism
- duckdb
- category-taxonomy
---
# Motion category classification via parallel subagent pipeline
## Context
The `right_wing_motions` table in `data/motions.db` had a `category` column that was 100% NULL across 3,030 classified motions — blocking downstream Overton analysis that splits centrist support by policy domain. The existing `derive_categories.py` script used OpenRouter's `chat_completion_json_parallel` to classify motions in sequential batches, but consistently timed out after 10 minutes without classifying anything at scale. A different approach was needed.
## Guidance
### 1. Derive taxonomy from a sample first
Have a sub-agent read a random sample (e.g., 60 motions) and infer natural categories from the data. This produces categories grounded in the actual motion content rather than a preconceived list:
- The sample ensures categories reflect real distribution (migration-heavy, stikstof-driven, etc.)
- The sub-agent returns a concise taxonomy with descriptions for each category
- Include a catch-all "overig" category for edge cases
For this project the taxonomy yielded 10 categories: asiel/vreemdelingen, landbouw/natuur, veiligheid/justitie, zorg/gezondheid, economie, energie/klimaat, buitenland/europa, onderwijs/wetenschap, verkeer/infrastructuur, overig.
### 2. Chunk data into independent batches
Dump motions from the DB to JSON, then split into small chunks (~38 motions each) that fit comfortably within a single sub-agent's context window. Each chunk is a standalone JSON file containing motion_id, title, and body_text.
### 3. Dispatch parallel classification sub-agents
Spawn one sub-agent per chunk simultaneously (up to 80 in this case). Each receives:
- The chunk of motions to classify
- The taxonomy with category descriptions
- A strict JSON output format: `[{"motion_id": ..., "category": ..., "category_explanation": ...}]`
- An instruction to read both title and body_text before deciding on a category
All 80 agents run in parallel, finishing in minutes rather than hours.
### 4. Merge results and update the database
Collect all result files. Validate each for correct structure (some may use non-standard key names). Then update the DB:
```sql
UPDATE right_wing_motions
SET category = ?, category_explanation = ?
WHERE motion_id = ?
```
Verify by counting non-NULL rows.
### 5. Integrate into downstream analysis
Once the category column is populated, update analysis scripts and charts to use it. For the Overton QMD report this meant:
- A Plotly dropdown filter on the main centrist support chart to toggle between categories
- A category delta bar chart showing pre/post centrist support change per domain
- Quarterly domain trajectory charts for the 5 largest categories
## Why This Matters
- **Speed**: 80 parallel agents classified 3,030 motions in minutes vs. a sequential script that never finished at all
- **Simplicity**: No timeout handling, retry logic, or batch management needed — each agent is a fire-and-forget independent unit
- **Quality**: Classification is grounded in reasoning (reading title + full text), not keyword matching or vector similarity
- **Discoverability**: The derived taxonomy (10 categories) emerges naturally from the data rather than being imposed upfront
## When to Apply
- You have thousands of items needing per-item LLM processing
- Each item is independently classifiable
- The task fits in a sub-agent's context window when batched at ~30-50 items
- Parallel dispatch infrastructure is available (e.g., the `task` tool)
## Examples
The pipeline was applied to 3,030 Dutch right-wing motions. The taxonomy was derived from a 60-motion sample by a single sub-agent, then 80 parallel sub-agents classified ~38 motions each. Final distribution was: landbouw/natuur 487, economie 470, asiel/vreemdelingen 423, buitenland/europa 386, veiligheid/justitie 359, zorg/gezondheid 348, energie/klimaat 174, overig 159, verkeer/infrastructuur 138, onderwijs/wetenschap 86.
Two chunks needed minor fixes (used `category_label` / `predicted_category` instead of `category`). A quick validation script caught these before the DB update.
## Related
- `docs/solutions/best-practices/large-scale-subagent-2d-extremity-scoring-2026-06-05.md` — parallel subagent pattern for numeric extremity scoring (same infrastructure, different task)
- `analysis/right_wing/derive_categories.py` — the original sequential script that timed out
- `docs/solutions/best-practices/domain-decomposition-overton-analysis.md` — why category-split analysis matters for Overton interpretation
- `docs/solutions/best-practices/overton-narrative-architecture-2026-06-06.md` — QMD report structure that consumed the categories

@ -108,7 +108,7 @@ Three tiers:
- [ ] Mechanism taxonomy revision (κ=0.41 → improve agreement)
- [ ] Forward-looking scenario analysis (permanent vs temporary shift)
- [ ] Anti-institutional pivot deep-dive (abolition → contestation)
- [ ] Re-populate category column in right_wing_motions (wiped by DROP TABLE)
- [x] Re-populate category column in right_wing_motions (wiped by DROP TABLE)
### Presentation
- [ ] Quarto blog post with interactive charts

@ -1,7 +1,5 @@
# Overton Window Breakpoint Analysis (2D Extremity)
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Quantify the 2024 structural break in centrist support
and content extremity for right-wing motions in the Tweede Kamer.
@ -73,8 +71,8 @@ Migration = category `asiel/vreemdelingen`. Non-migration = all other categories
| Domain | Pre-2024 Mean CS | Post-2024 Mean CS | Δ CS |
|--------|-----------------|------------------|------|
| Migration | nan | nan | +nan |
| Non-migration | 0.425 | 0.482 | +0.057 |
| Migration | 0.125 | 0.343 | +0.218 |
| Non-migration | 0.436 | 0.509 | +0.073 |
## 5. Material Impact-Stratified Centrist Support
@ -161,26 +159,26 @@ If both rose equally, a systemic factor (coalition change, polarization) is at w
| # | Year | Category | Stijl | Materieel | Bucket | Agreed? | Driver |
|---|------|----------|-------|-----------|--------|---------|--------|
| 1 | 2021 | other | 1 | 1 | 1-2 (mild) | | |
| 2 | 2020 | other | 1 | 1 | 1-2 (mild) | | |
| 3 | 2023 | other | 1 | 1 | 1-2 (mild) | | |
| 4 | 2023 | other | 3 | 1 | 1-2 (mild) | | |
| 5 | 2022 | other | 1 | 1 | 1-2 (mild) | | |
| 6 | 2021 | other | 1 | 2 | 2-3 (moderate) | | |
| 7 | 2020 | other | 1 | 2 | 2-3 (moderate) | | |
| 8 | 2020 | other | 2 | 2 | 2-3 (moderate) | | |
| 9 | 2025 | other | 1 | 2 | 2-3 (moderate) | | |
| 10 | 2019 | other | 2 | 2 | 2-3 (moderate) | | |
| 11 | 2019 | other | 3 | 3 | 3-4 (high) | | |
| 12 | 2020 | other | 3 | 3 | 3-4 (high) | | |
| 13 | 2020 | other | 2 | 3 | 3-4 (high) | | |
| 14 | 2020 | other | 4 | 3 | 3-4 (high) | | |
| 15 | 2022 | other | 2 | 3 | 3-4 (high) | | |
| 16 | 2025 | other | 2 | 4 | 4-5 (extreme) | | |
| 17 | 2021 | other | 2 | 4 | 4-5 (extreme) | | |
| 18 | 2025 | other | 3 | 4 | 4-5 (extreme) | | |
| 19 | 2023 | other | 2 | 4 | 4-5 (extreme) | | |
| 20 | 2019 | other | 2 | 5 | 4-5 (extreme) | | |
| 1 | 2021 | landbouw/natuur | 1 | 1 | 1-2 (mild) | | |
| 2 | 2020 | veiligheid/justitie | 1 | 1 | 1-2 (mild) | | |
| 3 | 2023 | veiligheid/justitie | 1 | 1 | 1-2 (mild) | | |
| 4 | 2023 | overig | 3 | 1 | 1-2 (mild) | | |
| 5 | 2022 | verkeer/infrastructuur | 1 | 1 | 1-2 (mild) | | |
| 6 | 2021 | landbouw/natuur | 1 | 2 | 2-3 (moderate) | | |
| 7 | 2020 | veiligheid/justitie | 1 | 2 | 2-3 (moderate) | | |
| 8 | 2020 | buitenland/europa | 2 | 2 | 2-3 (moderate) | | |
| 9 | 2025 | energie/klimaat | 1 | 2 | 2-3 (moderate) | | |
| 10 | 2019 | buitenland/europa | 2 | 2 | 2-3 (moderate) | | |
| 11 | 2019 | overig | 3 | 3 | 3-4 (high) | | |
| 12 | 2020 | zorg/gezondheid | 3 | 3 | 3-4 (high) | | |
| 13 | 2020 | buitenland/europa | 2 | 3 | 3-4 (high) | | |
| 14 | 2020 | verkeer/infrastructuur | 4 | 3 | 3-4 (high) | | |
| 15 | 2022 | energie/klimaat | 2 | 3 | 3-4 (high) | | |
| 16 | 2025 | asiel/vreemdelingen | 2 | 4 | 4-5 (extreme) | | |
| 17 | 2021 | zorg/gezondheid | 2 | 4 | 4-5 (extreme) | | |
| 18 | 2025 | asiel/vreemdelingen | 3 | 4 | 4-5 (extreme) | | |
| 19 | 2023 | economie | 2 | 4 | 4-5 (extreme) | | |
| 20 | 2019 | veiligheid/justitie | 2 | 5 | 4-5 (extreme) | | |
## 10. Limitations

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 152 KiB

@ -1,7 +1,5 @@
# Causal Timing: Centrist Support Shift for Right-Wing Motions
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Identify the exact timing of the centrist support shift and correlate it with
political events to distinguish between competing causal explanations.

@ -1,7 +1,5 @@
# 2D Extremity Temporal Decomposition
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Test whether the "flat single-dimension trend" masks diverging trajectories
when stylistic and material extremity scores are analyzed separately over time.
@ -17,8 +15,8 @@ when stylistic and material extremity scores are analyzed separately over time.
## 1. Key Findings
**Overall correlation r(stijl, materieel):** 0.472 (p=0.000000)
**Migration domain r(stijl, materieel):** N/A (p=N/A, n=0)
**Non-migration domain r(stijl, materieel):** 0.472 (p=0.000000, n=3030)
**Migration domain r(stijl, materieel):** 0.471 (p=0.000000, n=423)
**Non-migration domain r(stijl, materieel):** 0.424 (p=0.000000, n=2607)
---
@ -40,14 +38,14 @@ when stylistic and material extremity scores are analyzed separately over time.
| 2016 * | 6 | 1.667 | 2.333 | 2.000 | 0.667 | 0 | N/A | N/A | 6 | 1.667 | 2.333 | N/A |
| 2017 * | 0 | N/A | N/A | N/A | N/A | 0 | N/A | N/A | 0 | N/A | N/A | N/A |
| 2018 * | 5 | 1.000 | 1.400 | 1.400 | 0.400 | 0 | N/A | N/A | 5 | 1.000 | 1.400 | N/A |
| 2019 | 195 | 2.046 | 2.913 | 2.138 | 0.867 | 0 | N/A | N/A | 195 | 2.046 | 2.913 | 0.491 |
| 2020 | 469 | 2.228 | 2.906 | 2.262 | 0.678 | 0 | N/A | N/A | 469 | 2.228 | 2.906 | 0.603 |
| 2021 | 425 | 1.755 | 2.976 | 2.240 | 1.221 | 0 | N/A | N/A | 425 | 1.755 | 2.976 | 0.503 |
| 2022 | 446 | 1.800 | 2.525 | 2.161 | 0.724 | 0 | N/A | N/A | 446 | 1.800 | 2.525 | 0.429 |
| 2023 | 365 | 1.575 | 2.690 | 2.238 | 1.115 | 0 | N/A | N/A | 365 | 1.575 | 2.690 | 0.346 |
| 2024 | 469 | 1.680 | 2.582 | 1.985 | 0.902 | 0 | N/A | N/A | 469 | 1.680 | 2.582 | 0.391 |
| 2025 | 455 | 1.695 | 2.332 | 2.253 | 0.637 | 0 | N/A | N/A | 455 | 1.695 | 2.332 | 0.592 |
| 2026 | 195 | 2.015 | 2.405 | 2.331 | 0.390 | 0 | N/A | N/A | 195 | 2.015 | 2.405 | 0.335 |
| 2019 | 195 | 2.046 | 2.913 | 2.138 | 0.867 | 17 | 2.941 | 2.824 | 178 | 1.961 | 2.921 | 0.491 |
| 2020 | 469 | 2.228 | 2.906 | 2.262 | 0.678 | 36 | 3.389 | 3.472 | 433 | 2.132 | 2.859 | 0.603 |
| 2021 | 425 | 1.755 | 2.976 | 2.240 | 1.221 | 30 | 2.967 | 3.833 | 395 | 1.663 | 2.911 | 0.503 |
| 2022 | 446 | 1.800 | 2.525 | 2.161 | 0.724 | 77 | 2.260 | 3.026 | 369 | 1.705 | 2.420 | 0.429 |
| 2023 | 365 | 1.575 | 2.690 | 2.238 | 1.115 | 65 | 2.231 | 3.308 | 300 | 1.433 | 2.557 | 0.346 |
| 2024 | 469 | 1.680 | 2.582 | 1.985 | 0.902 | 63 | 2.571 | 3.175 | 406 | 1.542 | 2.490 | 0.391 |
| 2025 | 455 | 1.695 | 2.332 | 2.253 | 0.637 | 94 | 2.543 | 3.277 | 361 | 1.474 | 2.086 | 0.592 |
| 2026 | 195 | 2.015 | 2.405 | 2.331 | 0.390 | 41 | 2.707 | 2.927 | 154 | 1.831 | 2.266 | 0.335 |
> * Years with <50 scored motions; confidence intervals are wider or N/A.
@ -80,21 +78,29 @@ and material extremity.
| 2017 | N/A | N/A | 0 | All |
| 2018 | N/A | N/A | 5 | All |
| 2019 | 0.491 | 0.000000 | 195 | All |
| | 0.491 | 0.000000 | 195 | Non-migration |
| | 0.769 | 0.000313 | 17 | Migration |
| | 0.500 | 0.000000 | 178 | Non-migration |
| 2020 | 0.603 | 0.000000 | 469 | All |
| | 0.603 | 0.000000 | 469 | Non-migration |
| | 0.352 | 0.035486 | 36 | Migration |
| | 0.604 | 0.000000 | 433 | Non-migration |
| 2021 | 0.503 | 0.000000 | 425 | All |
| | 0.503 | 0.000000 | 425 | Non-migration |
| | 0.600 | 0.000460 | 30 | Migration |
| | 0.455 | 0.000000 | 395 | Non-migration |
| 2022 | 0.429 | 0.000000 | 446 | All |
| | 0.429 | 0.000000 | 446 | Non-migration |
| | 0.557 | 0.000000 | 77 | Migration |
| | 0.327 | 0.000000 | 369 | Non-migration |
| 2023 | 0.346 | 0.000000 | 365 | All |
| | 0.346 | 0.000000 | 365 | Non-migration |
| | 0.487 | 0.000039 | 65 | Migration |
| | 0.218 | 0.000146 | 300 | Non-migration |
| 2024 | 0.391 | 0.000000 | 469 | All |
| | 0.391 | 0.000000 | 469 | Non-migration |
| | 0.155 | 0.224438 | 63 | Migration |
| | 0.355 | 0.000000 | 406 | Non-migration |
| 2025 | 0.592 | 0.000000 | 455 | All |
| | 0.592 | 0.000000 | 455 | Non-migration |
| | 0.561 | 0.000000 | 94 | Migration |
| | 0.378 | 0.000000 | 361 | Non-migration |
| 2026 | 0.335 | 0.000002 | 195 | All |
| | 0.335 | 0.000002 | 195 | Non-migration |
| | 0.536 | 0.000308 | 41 | Migration |
| | 0.098 | 0.227990 | 154 | Non-migration |
---
@ -137,8 +143,8 @@ trend is an accurate summary (no masked divergence).
| Domain | Pre Mean Stijl | Pre Mean Mat | Post Mean Stijl | Post Mean Mat | Pre Gap | Post Gap | Pre r | Post r |
|--------|---------------|-------------|----------------|---------------|---------|----------|-------|--------|
| Migration | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
| Non-migration | 1.725 | 2.535 | 1.797 | 2.440 | 0.810 | 0.643 | 0.475 | 0.439 |
| Migration | 2.757 | 3.293 | 2.607 | 3.126 | 0.535 | 0.519 | 0.553 | 0.417 |
| Non-migration | 1.651 | 2.486 | 1.616 | 2.281 | 0.835 | 0.665 | 0.421 | 0.277 |
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 439 KiB

After

Width:  |  Height:  |  Size: 439 KiB

@ -1,7 +1,5 @@
# Left-Wing Response to Right-Wing Motions
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Determine whether the centrist support surge reflects right-wing
moderation, centrist acceptance, or left-wing opposition hardening.
@ -83,10 +81,10 @@ Non-migration = all other categories.
| Domain | Period | Left Support | Centrist Support | Gap | N |
|--------|--------|-------------|-----------------|-----|---|
| migration | Pre-2024 | N/A | N/A | N/A | 0 |
| migration | Post-2024 | N/A | N/A | N/A | 0 |
| non-migration | Pre-2024 | 0.2680 | 0.425 | +0.157 | 1911 |
| non-migration | Post-2024 | 0.2044 | 0.482 | +0.277 | 1119 |
| migration | Pre-2024 | 0.0465 | 0.125 | +0.079 | 225 |
| migration | Post-2024 | 0.0887 | 0.343 | +0.255 | 198 |
| non-migration | Pre-2024 | 0.2828 | 0.436 | +0.153 | 1686 |
| non-migration | Post-2024 | 0.2291 | 0.509 | +0.280 | 921 |
---

Binary file not shown.

Before

Width:  |  Height:  |  Size: 225 KiB

After

Width:  |  Height:  |  Size: 278 KiB

@ -1,7 +1,5 @@
# Mechanism Classification Report
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Sample:** 200 motions (stratified: 50 pre-2024, 150 post-2024)
**Classified:** 200 motions | **Unclassified:** 0

@ -117,7 +117,7 @@ party-level axis scores within each bloc.
#| fig-cap: "Centrist Support for Right-Wing Motions Over Time (2016–2026)"
#| column: page
yearly = con.execute("""
yearly_agg = con.execute("""
SELECT
year,
AVG(centrist_support_strict) AS mean_cs,
@ -128,44 +128,102 @@ yearly = con.execute("""
GROUP BY year ORDER BY year
""").fetchdf()
yearly_cat = con.execute("""
SELECT
year,
category,
AVG(centrist_support_strict) AS cs,
COUNT(*) AS n
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2017 AND category IS NOT NULL
GROUP BY year, category
ORDER BY year, category
""").fetchdf()
categories_list = sorted(yearly_cat["category"].unique())
cat_colors = ["#66C2A5", "#FC8D62", "#8DA0CB", "#E78AC3", "#A6D854",
"#FFD92F", "#E5C494", "#B3B3B3", "#1E88E5", "#D81B60"]
cat_labels = {
"asiel/vreemdelingen": "Asiel & Vreemdelingen",
"landbouw/natuur": "Landbouw & Natuur",
"veiligheid/justitie": "Veiligheid & Justitie",
"zorg/gezondheid": "Zorg & Gezondheid",
"economie": "Economie",
"energie/klimaat": "Energie & Klimaat",
"buitenland/europa": "Buitenland & Europa",
"onderwijs/wetenschap": "Onderwijs & Wetenschap",
"verkeer/infrastructuur": "Verkeer & Infrastructuur",
"overig": "Overig",
}
fig1 = go.Figure()
fig1.add_trace(go.Scatter(
x=yearly["year"], y=yearly["mean_cs"],
x=yearly_agg["year"], y=yearly_agg["mean_cs"],
mode="lines+markers", name="All right-wing",
line=dict(color="#002366", width=3),
marker=dict(size=8),
error_y=dict(
type="data",
array=1.96 * yearly["std_cs"] / np.sqrt(yearly["n"]),
array=1.96 * yearly_agg["std_cs"] / np.sqrt(yearly_agg["n"]),
visible=True, thickness=0.8, width=2
)
),
))
pre = yearly[yearly["year"] < BREAK_YEAR]
post = yearly[yearly["year"] >= BREAK_YEAR]
for i, cat in enumerate(categories_list):
cat_data = yearly_cat[yearly_cat["category"] == cat]
if cat_data.empty:
continue
fig1.add_trace(go.Scatter(
x=cat_data["year"], y=cat_data["cs"],
mode="lines+markers",
name=cat_labels.get(cat, cat),
line=dict(color=cat_colors[i % len(cat_colors)], width=2, dash="dash"),
marker=dict(size=6),
visible=False,
))
pre = yearly_agg[yearly_agg["year"] < BREAK_YEAR]
post = yearly_agg[yearly_agg["year"] >= BREAK_YEAR]
fig1.add_hline(
y=pre["mean_cs"].mean(),
line_dash="dot", line_color="#90CAF9",
annotation_text=f"Pre-2024 mean ({pre['mean_cs'].mean():.3f})"
annotation_text=f"Pre-2024 mean ({pre['mean_cs'].mean():.3f})",
)
fig1.add_hline(
y=post["mean_cs"].mean(),
line_dash="dot", line_color="#1E88E5",
annotation_text=f"Post-2024 mean ({post['mean_cs'].mean():.3f})"
annotation_text=f"Post-2024 mean ({post['mean_cs'].mean():.3f})",
)
fig1.add_vline(
x=BREAK_YEAR - 0.5, line_dash="dot", line_color="black", opacity=0.5
x=BREAK_YEAR - 0.5, line_dash="dot", line_color="black", opacity=0.5,
)
buttons = [
dict(label="All right-wing", method="update",
args=[{"visible": [True] + [False] * len(categories_list)}])
]
for i, cat in enumerate(categories_list):
vis = [True] + [False] * len(categories_list)
vis[i + 1] = True
buttons.append(dict(
label=cat_labels.get(cat, cat),
method="update",
args=[{"visible": vis}],
))
fig1.update_layout(
title="Centrist Support (Strict) for Right-Wing Motions",
xaxis=dict(title="Year", dtick=1),
yaxis=dict(title="Centrist Support (fraction of parties)", range=[0, 1.1]),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
template="plotly_white", height=450,
updatemenus=[dict(
type="dropdown", direction="down", showactive=True,
buttons=buttons, x=1.05, y=1.15,
)],
)
fig1.show()
```
@ -258,6 +316,69 @@ specific: right-wing centrist support surged by +0.236, while non-right-wing
motions remained essentially flat (−0.006). This is a right-wing-specific
phenomenon, not a general parliamentary trend.
## Domain Decomposition
Right-wing motions span ten policy categories, and the 2024 centrist support shift
was not uniform across them. Every category gained centrist support, but the magnitudes
vary dramatically — from a surge of +0.39 for climate and energy to a modest +0.08
for education and science.
Leading the shift are **energie/klimaat** (+0.392), **buitenland/europa** (+0.364),
and **economie** (+0.342) — domains where right-wing parties adopted centrist-friendly
framing and right-wing governments in other European countries provided template
policies. At the other end, **onderwijs/wetenschap** (+0.081) and **veiligheid/justitie**
(+0.138) barely moved, consistent with their roles as high-consensus domains where
the window was already wide.
```{python}
#| label: chart-7-category-delta
#| fig-cap: "Pre/Post 2024 Centrist Support Delta by Policy Category"
#| column: page
delta = con.execute("""
SELECT
category,
AVG(CASE WHEN year < 2024 THEN centrist_support_strict END) as pre_cs,
AVG(CASE WHEN year >= 2024 THEN centrist_support_strict END) as post_cs,
COUNT(*) as n
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2017 AND category IS NOT NULL
GROUP BY category
ORDER BY (AVG(CASE WHEN year >= 2024 THEN centrist_support_strict END)
- AVG(CASE WHEN year < 2024 THEN centrist_support_strict END)) DESC
""").fetchdf()
delta["d"] = delta["post_cs"] - delta["pre_cs"]
fig7 = go.Figure()
fig7.add_trace(go.Bar(
x=delta["d"],
y=delta["category"],
orientation="h",
marker_color=["#2ECC71" if d > 0 else "#E74C3C" for d in delta["d"]],
text=[f"{d:.3f}" for d in delta["d"]],
textposition="outside",
cliponaxis=False,
))
fig7.update_layout(
title="Pre/Post 2024 Centrist Support Delta by Category",
xaxis=dict(title="Delta (post-2024 − pre-2024)", range=[-0.05, 0.48]),
yaxis=dict(title="", autorange="reversed"),
template="plotly_white", height=450,
margin=dict(l=200),
)
fig7.show()
```
The pattern reveals a political gradient. Domains tied to European integration
(buitenland/europa) and climate action (energie/klimaat) — where center-right
governments abroad provided cover — saw the largest shifts. Domestic social domains
(zorg/gezondheid, onderwijs/wetenschap) were largely insulated. The migration domain
(asiel/vreemdelingen), central to the Overton narrative, ranked seventh with a
+0.210 delta — substantial but not exceptional. Its importance lies not in the
magnitude of the shift but in its durability: migration centrist support sustained
its gains through 2025 while non-migration domains reverted.
## Indicator 2: Spatial Divergence
If centrists are voting more with right-wing motions, one might expect
@ -619,6 +740,83 @@ suggesting the 2024 spike was primarily an electoral shock for non-migration dom
| Gradual learning | Jump was 1.9× average quarterly — discrete, not incremental | **Less consistent with the data** |
| European contagion | No Dutch response during 2022–2023 European shift | **Less consistent with the data** |
```{python}
#| label: chart-8-domain-trajectories
#| fig-cap: "Quarterly Centrist Support by Domain (5 Key Categories)"
#| column: page
key_cats = ["asiel/vreemdelingen", "energie/klimaat", "buitenland/europa",
"landbouw/natuur", "economie"]
quarterly_cat = con.execute("""
SELECT
EXTRACT(YEAR FROM m.date) AS y,
CEIL(EXTRACT(MONTH FROM m.date) / 3.0) AS q,
r.category,
AVG(r.centrist_support_strict) AS cs,
COUNT(*) AS n
FROM right_wing_motions r
JOIN motions m ON r.motion_id = m.id
WHERE r.classified = TRUE AND m.date IS NOT NULL
AND r.category IN ({})
GROUP BY y, q, r.category
ORDER BY y, q
""".format(",".join(f"'{c}'" for c in key_cats))).fetchdf()
quarterly_cat["label"] = (
quarterly_cat["y"].astype(int).astype(str)
+ "-Q"
+ quarterly_cat["q"].astype(int).astype(str)
)
fig8 = go.Figure()
fig8.add_trace(go.Scatter(
x=quarterly["label"], y=quarterly["cs"],
mode="lines+markers", name="All right-wing",
line=dict(color="#002366", width=3),
marker=dict(size=6),
))
domain_colors = {
"asiel/vreemdelingen": "#D81B60",
"energie/klimaat": "#1E88E5",
"buitenland/europa": "#FF8F00",
"landbouw/natuur": "#2E7D32",
"economie": "#8E44AD",
}
for cat in key_cats:
cat_data = quarterly_cat[quarterly_cat["category"] == cat]
if cat_data.empty:
continue
fig8.add_trace(go.Scatter(
x=cat_data["label"], y=cat_data["cs"],
mode="lines+markers",
name=cat_labels.get(cat, cat),
line=dict(color=domain_colors.get(cat, "#666"), width=2, dash="dash"),
marker=dict(size=5),
))
fig8.add_shape(
type="line", x0="2024-Q1", x1="2024-Q1", y0=0, y1=1,
line=dict(dash="dot", color="red", width=1.5),
)
fig8.update_layout(
title="Quarterly Centrist Support by Domain",
xaxis=dict(
title="Quarter", tickangle=45,
tickmode="array",
tickvals=quarterly["label"][::4],
),
yaxis=dict(title="Centrist Support", range=[0, 1.0]),
template="plotly_white", height=500,
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
)
fig8.show()
```
## Verdict: The Window Widened Through Moderation
**The Overton window widened: more right-wing positions became politically

@ -1,6 +1,6 @@
# Has the Overton Window Shifted? A Synthesis
**Date:** 2026-05-26
**Date:** 2026-06-15
**Analysis period:** 2016-2026
**Data:** 29,591 motions with 2D extremity scores (`extremity_scores_all`), including 3,089 right-wing motions with dedicated 2D scores (`extremity_scores_2d`), Procrustes-aligned SVD party positions across 10 annual windows, MP-level vote records for centrist parties (D66, CDA, ChristenUnie, NSC) and left-wing parties (SP, GroenLinks-PvdA, PvdD, Volt, DENK), quarterly centrist support trajectories (33 quarters), 150-motion systematic mechanism classification
@ -284,7 +284,7 @@ Centrist support for right-wing motions surged from 25% to 51%, while centrist s
- **Success ceiling:** The 96%+ pass rate makes pass rate an insensitive dependent variable for measuring centrist influence on legislative outcomes. The success correlation findings should be interpreted as describing a real but practically constrained relationship.
- **NSC sensitivity:** Removing NSC from the strict centrist set (leaving D66/CDA/CU) yields a nearly identical surge (+0.248 vs +0.256, Cohen's d = 0.63 vs 0.66). Only 3.1% of the reported effect is attributable to NSC inclusion.
- **Submitter parsing:** The opposition-only filter relies on parse_lead_submitter(), which fails on 20% of pre-2024 and 29% of post-2024 motions. Unparsed motions have systematically higher centrist support (0.40 pre, 0.65 post vs 0.21 pre, 0.45 post for parsed). The reported opposition-only effect (d=0.85) is likely inflated by ~0.10-0.20; the true effect is probably d≈0.65-0.75. The direction is robust but the magnitude should be interpreted conservatively.
- **Migration domain provenance:** The `category` column in `right_wing_motions` is NULL for all 3,030 classified motions (wiped by a pipeline bug). Migration vs non-migration classification relies on title keyword matching (e.g., "asiel", "migratie", "vreemdeling"), which is less reliable than the original LLM-based classification. The migration gateway finding is directionally robust but exact domain boundaries should be treated as approximate.
- **Migration domain provenance:** The `category` column in `right_wing_motions` has been repopulated via LLM-based classification (10-category taxonomy, 3,030 motions). Migration (asiel/vreemdelingen) vs non-migration splits now use the LLM-derived categories rather than title keyword matching.
- **2026-Q2 sample size:** The 2026-Q2 "bounce" (CS=0.523) is based on only 44 motions with a bimodal distribution (20 at CS=1.0, 18 at CS=0.0). The CS=1.0 motions include many consensus items (defense, infrastructure) unrelated to migration. This quarter's mean is sensitive to composition and should not be over-interpreted as evidence of a sustained trend.
### Visualization

@ -1,7 +1,5 @@
# Right-Wing Party Differentiation
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Break down right-wing motion metrics by party (PVV, FVD, JA21, SGP)
to identify which party drives the moderation effect.

@ -1,8 +1,6 @@
# Predictive Model: Centrist Support
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Generated:** 2026-06-06 10:39
**Generated:** 2026-06-15 21:10
## Data Summary
@ -11,7 +9,7 @@
- High centrist support (>0.5) : 120 motions
- Low centrist support (<=0.5): 845 motions
- Class imbalance ratio: 7.0:1 (low:high)
- Features: 10
- Features: 19
## Model Performance
@ -19,15 +17,15 @@
| Model | Accuracy | Precision | Recall | AUC-ROC |
|-------|----------|-----------|--------|---------|
| Logistic Regression | 0.725 | 0.262 | 0.667 | 0.799 |
| Random Forest | 0.839 | 0.111 | 0.042 | 0.769 |
| Logistic Regression | 0.746 | 0.302 | 0.792 | 0.791 |
| Random Forest | 0.855 | 0.400 | 0.333 | 0.805 |
### 5-Fold Cross-Validation
| Model | Mean Accuracy | Std Accuracy | Mean AUC-ROC | Std AUC-ROC |
|-------|---------------|-------------|--------------|-------------|
| Logistic Regression | 0.730 | 0.021 | 0.828 | 0.039 |
| Random Forest | 0.854 | 0.023 | 0.831 | 0.035 |
| Logistic Regression | 0.718 | 0.026 | 0.816 | 0.026 |
| Random Forest | 0.861 | 0.017 | 0.845 | 0.039 |
## Feature Importance
@ -35,16 +33,16 @@
| Feature | Coefficient | Odds Ratio |
|---------|-------------|------------|
| `party_FVD` | -1.0534 | 0.3488 |
| `party_SGP` | 1.0354 | 2.8163 |
| `stijl_extremiteit` | -0.7955 | 0.4514 |
| `party_JA21` | 0.6673 | 1.9489 |
| `party_PVV` | -0.6524 | 0.5208 |
| `materiele_impact` | -0.5428 | 0.5811 |
| `year` | 0.4052 | 1.4996 |
| `is_opposition` | -0.3080 | 0.7349 |
| `text_length` | 0.1133 | 1.1200 |
| `cat_overig` | -0.0031 | 0.9969 |
| `party_FVD` | -0.9773 | 0.3763 |
| `cat_zorg/gezondheid` | -0.9527 | 0.3857 |
| `party_JA21` | 0.8807 | 2.4127 |
| `party_SGP` | 0.8254 | 2.2828 |
| `cat_economie` | 0.7537 | 2.1248 |
| `party_PVV` | -0.7346 | 0.4797 |
| `stijl_extremiteit` | -0.7192 | 0.4871 |
| `materiele_impact` | -0.6077 | 0.5446 |
| `cat_landbouw/natuur` | 0.5100 | 1.6654 |
| `cat_onderwijs/wetenschap` | 0.4733 | 1.6052 |
*Positive coefficient = higher feature value increases odds of high centrist support.*
@ -52,33 +50,33 @@
| Feature | Importance (Gini) |
|---------|-------------------|
| `text_length` | 0.3287 |
| `year` | 0.2176 |
| `stijl_extremiteit` | 0.1893 |
| `materiele_impact` | 0.1147 |
| `text_length` | 0.2241 |
| `year` | 0.1866 |
| `stijl_extremiteit` | 0.1684 |
| `materiele_impact` | 0.1007 |
| `party_SGP` | 0.0508 |
| `party_FVD` | 0.0360 |
| `party_PVV` | 0.0298 |
| `party_JA21` | 0.0200 |
| `is_opposition` | 0.0132 |
| `cat_overig` | 0.0000 |
| `party_PVV` | 0.0381 |
| `party_FVD` | 0.0366 |
| `cat_veiligheid/justitie` | 0.0310 |
| `cat_buitenland/europa` | 0.0256 |
| `party_JA21` | 0.0215 |
## Interpretation
### Top 5 Most Important Features
**Logistic Regression (coefficient magnitude):**
1. `party_FVD` (coef=-1.0534, OR=0.3488) — decreases odds of high centrist support
2. `party_SGP` (coef=1.0354, OR=2.8163) — increases odds of high centrist support
3. `stijl_extremiteit` (coef=-0.7955, OR=0.4514) — decreases odds of high centrist support
4. `party_JA21` (coef=0.6673, OR=1.9489) — increases odds of high centrist support
5. `party_PVV` (coef=-0.6524, OR=0.5208) — decreases odds of high centrist support
1. `party_FVD` (coef=-0.9773, OR=0.3763) — decreases odds of high centrist support
2. `cat_zorg/gezondheid` (coef=-0.9527, OR=0.3857) — decreases odds of high centrist support
3. `party_JA21` (coef=0.8807, OR=2.4127) — increases odds of high centrist support
4. `party_SGP` (coef=0.8254, OR=2.2828) — increases odds of high centrist support
5. `cat_economie` (coef=0.7537, OR=2.1248) — increases odds of high centrist support
**Random Forest (Gini importance):**
1. `text_length` (importance=0.3287)
2. `year` (importance=0.2176)
3. `stijl_extremiteit` (importance=0.1893)
4. `materiele_impact` (importance=0.1147)
1. `text_length` (importance=0.2241)
2. `year` (importance=0.1866)
3. `stijl_extremiteit` (importance=0.1684)
4. `materiele_impact` (importance=0.1007)
5. `party_SGP` (importance=0.0508)
### Which features best predict centrist support?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 127 KiB

@ -1,7 +1,5 @@
# Motion Success Correlation Analysis
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Test whether motions with high centrist support actually passed at higher rates,
validating that centrist support translates to legislative success.

@ -1,7 +1,5 @@
# Temporal Trajectory: Centrist Support for Right-Wing Motions
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Replace binary pre/post-2024 analysis with continuous quarterly trajectories
showing the exact timing and shape of the Overton window shift.
@ -81,34 +79,34 @@ the new political reality, not as a response to coalition dynamics.
| 2018-Q4 | 4 | 1.000 | N/A | N/A | 0 | N/A | 0 | N/A | 4 | 1.000 | 0.938 |
| 2019-Q1 | 1 | 0.000 | N/A | N/A | 0 | N/A | 0 | N/A | 1 | 0.000 | 0.833 |
| 2019-Q2 | 4 | 0.500 | N/A | N/A | 2 | 0.000 | 0 | N/A | 4 | 0.500 | 0.667 |
| 2019-Q3 | 25 | 0.300 | 0.160 | 0.460 | 17 | 0.176 | 0 | N/A | 25 | 0.300 | 0.317 |
| 2019-Q4 | 165 | 0.391 | 0.333 | 0.455 | 86 | 0.181 | 0 | N/A | 165 | 0.391 | 0.382 |
| 2020-Q1 | 79 | 0.278 | 0.190 | 0.367 | 45 | 0.100 | 0 | N/A | 79 | 0.278 | 0.350 |
| 2020-Q2 | 130 | 0.258 | 0.188 | 0.323 | 87 | 0.086 | 0 | N/A | 130 | 0.258 | 0.321 |
| 2020-Q3 | 78 | 0.167 | 0.102 | 0.237 | 57 | 0.088 | 0 | N/A | 78 | 0.167 | 0.239 |
| 2020-Q4 | 182 | 0.396 | 0.338 | 0.462 | 98 | 0.204 | 0 | N/A | 182 | 0.396 | 0.304 |
| 2021-Q1 | 90 | 0.150 | 0.083 | 0.222 | 65 | 0.015 | 0 | N/A | 90 | 0.150 | 0.281 |
| 2021-Q2 | 104 | 0.139 | 0.091 | 0.197 | 84 | 0.065 | 0 | N/A | 104 | 0.139 | 0.266 |
| 2021-Q3 | 68 | 0.167 | 0.105 | 0.228 | 54 | 0.127 | 0 | N/A | 68 | 0.167 | 0.150 |
| 2021-Q4 | 163 | 0.215 | 0.163 | 0.273 | 119 | 0.155 | 0 | N/A | 163 | 0.215 | 0.182 |
| 2019-Q3 | 25 | 0.300 | 0.160 | 0.460 | 17 | 0.176 | 3 | 0.167 | 22 | 0.318 | 0.317 |
| 2019-Q4 | 165 | 0.391 | 0.333 | 0.455 | 86 | 0.181 | 14 | 0.143 | 151 | 0.414 | 0.382 |
| 2020-Q1 | 79 | 0.278 | 0.190 | 0.367 | 45 | 0.100 | 13 | 0.000 | 66 | 0.333 | 0.350 |
| 2020-Q2 | 130 | 0.258 | 0.188 | 0.323 | 87 | 0.086 | 9 | 0.278 | 121 | 0.256 | 0.321 |
| 2020-Q3 | 78 | 0.167 | 0.102 | 0.237 | 57 | 0.088 | 4 | 0.000 | 74 | 0.176 | 0.239 |
| 2020-Q4 | 182 | 0.396 | 0.338 | 0.462 | 98 | 0.204 | 10 | 0.150 | 172 | 0.410 | 0.304 |
| 2021-Q1 | 90 | 0.150 | 0.083 | 0.222 | 65 | 0.015 | 1 | 0.000 | 89 | 0.152 | 0.281 |
| 2021-Q2 | 104 | 0.139 | 0.091 | 0.197 | 84 | 0.065 | 9 | 0.000 | 95 | 0.153 | 0.266 |
| 2021-Q3 | 68 | 0.167 | 0.105 | 0.228 | 54 | 0.127 | 8 | 0.125 | 60 | 0.173 | 0.150 |
| 2021-Q4 | 163 | 0.215 | 0.163 | 0.273 | 119 | 0.155 | 12 | 0.042 | 151 | 0.228 | 0.182 |
| 2022-Q1 | 15 | 0.067 | 0.000 | 0.167 | 13 | 0.038 | 0 | N/A | 15 | 0.067 | 0.193 |
| 2022-Q2 | 119 | 0.214 | 0.147 | 0.282 | 84 | 0.077 | 0 | N/A | 119 | 0.214 | 0.207 |
| 2022-Q3 | 83 | 0.133 | 0.072 | 0.199 | 71 | 0.063 | 0 | N/A | 83 | 0.133 | 0.173 |
| 2022-Q4 | 229 | 0.227 | 0.183 | 0.273 | 159 | 0.148 | 0 | N/A | 229 | 0.227 | 0.205 |
| 2023-Q1 | 77 | 0.148 | 0.091 | 0.213 | 56 | 0.107 | 0 | N/A | 77 | 0.148 | 0.191 |
| 2023-Q2 | 90 | 0.306 | 0.233 | 0.389 | 58 | 0.190 | 0 | N/A | 90 | 0.306 | 0.230 |
| 2023-Q3 | 68 | 0.184 | 0.110 | 0.257 | 53 | 0.104 | 0 | N/A | 68 | 0.184 | 0.219 |
| 2023-Q4 | 130 | 0.321 | 0.260 | 0.383 | 87 | 0.262 | 0 | N/A | 130 | 0.321 | 0.284 |
| 2024-Q1 | 98 | 0.501 | 0.429 | 0.571 | 40 | 0.358 | 0 | N/A | 98 | 0.501 | 0.349 |
| 2024-Q2 | 124 | 0.573 | 0.505 | 0.640 | 45 | 0.504 | 0 | N/A | 124 | 0.573 | 0.460 |
| 2024-Q3 | 17 | 0.588 | 0.412 | 0.765 | 7 | 0.476 | 0 | N/A | 17 | 0.588 | 0.544 |
| 2024-Q4 | 230 | 0.648 | 0.601 | 0.695 | 89 | 0.509 | 0 | N/A | 230 | 0.648 | 0.620 |
| 2025-Q1 | 29 | 0.598 | 0.448 | 0.747 | 12 | 0.778 | 0 | N/A | 29 | 0.598 | 0.639 |
| 2025-Q2 | 165 | 0.503 | 0.442 | 0.562 | 60 | 0.483 | 0 | N/A | 165 | 0.503 | 0.588 |
| 2025-Q3 | 155 | 0.437 | 0.370 | 0.503 | 48 | 0.333 | 0 | N/A | 155 | 0.437 | 0.481 |
| 2025-Q4 | 106 | 0.450 | 0.373 | 0.532 | 35 | 0.416 | 0 | N/A | 106 | 0.450 | 0.466 |
| 2026-Q1 | 151 | 0.334 | 0.265 | 0.400 | 69 | 0.325 | 0 | N/A | 151 | 0.334 | 0.402 |
| 2026-Q2 | 44 | 0.523 | 0.386 | 0.670 | 0 | N/A | 0 | N/A | 44 | 0.523 | 0.402 |
| 2022-Q2 | 119 | 0.214 | 0.147 | 0.282 | 84 | 0.077 | 22 | 0.045 | 97 | 0.253 | 0.207 |
| 2022-Q3 | 83 | 0.133 | 0.072 | 0.199 | 71 | 0.063 | 26 | 0.077 | 57 | 0.158 | 0.173 |
| 2022-Q4 | 229 | 0.227 | 0.183 | 0.273 | 159 | 0.148 | 29 | 0.241 | 200 | 0.225 | 0.205 |
| 2023-Q1 | 77 | 0.148 | 0.091 | 0.213 | 56 | 0.107 | 9 | 0.056 | 68 | 0.160 | 0.191 |
| 2023-Q2 | 90 | 0.306 | 0.233 | 0.389 | 58 | 0.190 | 8 | 0.375 | 82 | 0.299 | 0.230 |
| 2023-Q3 | 68 | 0.184 | 0.110 | 0.257 | 53 | 0.104 | 15 | 0.167 | 53 | 0.189 | 0.219 |
| 2023-Q4 | 130 | 0.321 | 0.260 | 0.383 | 87 | 0.262 | 33 | 0.187 | 97 | 0.366 | 0.284 |
| 2024-Q1 | 98 | 0.501 | 0.429 | 0.571 | 40 | 0.358 | 10 | 0.333 | 88 | 0.520 | 0.349 |
| 2024-Q2 | 124 | 0.573 | 0.505 | 0.640 | 45 | 0.504 | 15 | 0.422 | 109 | 0.593 | 0.460 |
| 2024-Q3 | 17 | 0.588 | 0.412 | 0.765 | 7 | 0.476 | 3 | 0.444 | 14 | 0.619 | 0.544 |
| 2024-Q4 | 230 | 0.648 | 0.601 | 0.695 | 89 | 0.509 | 35 | 0.324 | 195 | 0.706 | 0.620 |
| 2025-Q1 | 29 | 0.598 | 0.448 | 0.747 | 12 | 0.778 | 3 | 0.444 | 26 | 0.615 | 0.639 |
| 2025-Q2 | 165 | 0.503 | 0.442 | 0.562 | 60 | 0.483 | 30 | 0.333 | 135 | 0.541 | 0.588 |
| 2025-Q3 | 155 | 0.437 | 0.370 | 0.503 | 48 | 0.333 | 51 | 0.320 | 104 | 0.494 | 0.481 |
| 2025-Q4 | 106 | 0.450 | 0.373 | 0.532 | 35 | 0.416 | 10 | 0.374 | 96 | 0.457 | 0.466 |
| 2026-Q1 | 151 | 0.334 | 0.265 | 0.400 | 69 | 0.325 | 30 | 0.300 | 121 | 0.342 | 0.402 |
| 2026-Q2 | 44 | 0.523 | 0.386 | 0.670 | 0 | N/A | 11 | 0.455 | 33 | 0.545 | 0.402 |
> **Note:** CI intervals use 1000-iteration bootstrap resampling.
> Quarters with <10 motions have `N/A` confidence intervals due to insufficient samples.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 320 KiB

After

Width:  |  Height:  |  Size: 374 KiB

@ -1,7 +1,5 @@
# Voting Margin Analysis
> **Part of the Overton Window Analysis.** See the [synthesis report](overton_window_synthesis.md) for the integrated narrative, or the [interactive article](overton_window.qmd) for the full story with charts.
**Goal:** Replace binary pass/fail with continuous voting margin as the primary
success metric for right-wing motions in the Tweede Kamer.

@ -0,0 +1,116 @@
"""Validate category decomposition data for Overton report."""
from __future__ import annotations
import duckdb
import pytest
DB_PATH = "data/motions.db"
@pytest.fixture(scope="module")
def con():
c = duckdb.connect(DB_PATH)
yield c
c.close()
def test_category_distribution(con):
"""There are exactly 10 categories and all 3,030 motions are classified."""
df = con.execute("""
SELECT category, COUNT(*) as cnt
FROM right_wing_motions
WHERE classified = TRUE
GROUP BY category
ORDER BY cnt DESC
""").fetchdf()
assert len(df) == 10
assert df["cnt"].sum() == 3030
assert df[df["category"] == "overig"]["cnt"].values[0] >= 100
def test_category_deltas(con):
"""Pre/post CS deltas (2024 split) — ALL categories gained, energie/klimaat leads."""
df = con.execute("""
SELECT
category,
AVG(CASE WHEN year < 2024 THEN centrist_support_strict END) as pre_cs,
AVG(CASE WHEN year >= 2024 THEN centrist_support_strict END) as post_cs,
COUNT(*) as n
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2017
GROUP BY category
""").fetchdf()
assert len(df) == 10
df = df.copy()
df["delta"] = df["post_cs"] - df["pre_cs"]
top = df.sort_values("delta", ascending=False)
assert top.iloc[0]["category"] == "energie/klimaat"
assert 0.35 < abs(top.iloc[0]["delta"]) < 0.45
bottom = df.sort_values("delta", ascending=True)
assert bottom.iloc[0]["category"] in ("veiligheid/justitie", "onderwijs/wetenschap")
assert all(df["delta"] > 0)
def test_yearly_category_cs(con):
"""Yearly CS per category returns data for every year-category combo."""
df = con.execute("""
SELECT year, category, AVG(centrist_support_strict) as cs, COUNT(*) as n
FROM right_wing_motions
WHERE classified = TRUE AND year >= 2017
GROUP BY year, category
ORDER BY year, category
""").fetchdf()
assert len(df) >= 50
assert df["category"].nunique() == 10
assert df["year"].nunique() >= 8
def test_quarterly_category_data(con):
"""Quarterly CS per key categories returns expected shape."""
key_cats = ["asiel/vreemdelingen", "energie/klimaat", "buitenland/europa",
"landbouw/natuur", "economie"]
df = con.execute("""
SELECT
EXTRACT(YEAR FROM m.date) AS y,
CEIL(EXTRACT(MONTH FROM m.date) / 3.0) AS q,
r.category,
AVG(r.centrist_support_strict) AS cs,
COUNT(*) AS n
FROM right_wing_motions r
JOIN motions m ON r.motion_id = m.id
WHERE r.classified = TRUE AND m.date IS NOT NULL
AND r.category IN ({})
GROUP BY y, q, r.category
ORDER BY y, q
""".format(",".join(f"'{c}'" for c in key_cats))).fetchdf()
assert df["category"].nunique() <= 5
for cat in key_cats:
assert cat in df["category"].values
def test_qmd_has_domain_decomposition_section():
"""QMD should have a Domain Decomposition heading after implementation."""
qmd = open("reports/overton_window/overton_window.qmd").read()
assert "## Domain Decomposition" in qmd
def test_qmd_has_category_delta_chart():
"""QMD should have a category delta bar chart cell."""
qmd = open("reports/overton_window/overton_window.qmd").read()
assert "chart-7-category-delta" in qmd
def test_qmd_has_category_filter_dropdown():
"""Chart 1 should have updatemenu for category filtering."""
qmd = open("reports/overton_window/overton_window.qmd").read()
assert "updatemenus" in qmd
def test_qmd_has_domain_trajectories():
"""Quarterly chart should have category trajectories."""
qmd = open("reports/overton_window/overton_window.qmd").read()
assert "chart-8-domain-trajectories" in qmd

@ -339,5 +339,594 @@
"target_id": null,
"metadata": {},
"created_at": "2026-05-04T19:55:29.113927Z"
},
{
"id": "2f434402-546e-462e-b8a8-ad0c281e9861",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T20:56:45.614872Z"
},
{
"id": "221594e6-a969-490e-b4da-c2fbd8afe728",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T20:56:46.252164Z"
},
{
"id": "58ed7f35-8988-463c-be7d-bf4d5378d0fd",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T20:56:46.292849Z"
},
{
"id": "10b7dcfe-d039-4067-9383-0282fe627cc1",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T22:22:47.838874Z"
},
{
"id": "c078bdde-813b-473e-9382-524abf1ac822",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T22:22:48.203898Z"
},
{
"id": "3d4f1480-c467-4ec2-bedb-514275bd17a6",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T22:22:48.228872Z"
},
{
"id": "9dc9b240-8830-4249-87f7-e80c613b8dc4",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T22:34:08.839627Z"
},
{
"id": "09517995-7e41-4ddb-9b64-bb299559fe13",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T22:34:09.288983Z"
},
{
"id": "c0a284ed-af16-4c94-9f85-87581feec8ee",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T22:34:09.317751Z"
},
{
"id": "7de1af8b-c959-401b-b357-5e6d6e737422",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T22:37:26.691012Z"
},
{
"id": "8885076f-8a8a-43b7-b272-ea0e375d4d18",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T22:37:27.063729Z"
},
{
"id": "19027893-405c-4bc4-ad37-5beed3758161",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T22:37:27.088635Z"
},
{
"id": "6ee9856a-de2d-4a56-a054-0faaa75981e8",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T22:40:10.476857Z"
},
{
"id": "3f627a6b-6ee2-4d22-aee7-93c76dcb3b0d",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T22:40:10.916345Z"
},
{
"id": "2c2d7dd1-568b-480d-8bc0-c566afc66b51",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T22:40:10.944661Z"
},
{
"id": "03400271-9a2f-4ad4-be61-06c01d8f3dd6",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T23:09:55.223173Z"
},
{
"id": "af539bfe-9d8f-4267-a1b7-ba71ca41bcb9",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T23:09:55.588730Z"
},
{
"id": "cc3453ce-ed45-4c6b-9778-d86da81ccca7",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T23:09:55.611779Z"
},
{
"id": "ae941751-431d-413d-9cd5-e4c19e3b1f06",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-24T23:10:44.903850Z"
},
{
"id": "82b45689-49dc-4cea-897c-919942f59a84",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-24T23:10:45.264408Z"
},
{
"id": "5ac0f722-7c09-47fe-bd17-f84f78549b07",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-24T23:10:45.290462Z"
},
{
"id": "482dbdf9-9fb7-4685-8ed8-30aa8c6a6225",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-26T21:33:23.479829Z"
},
{
"id": "c61a971b-7962-48cb-85a4-610a734b2633",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-26T21:33:24.219398Z"
},
{
"id": "874fc2a8-6941-4755-93ba-f2a2e790e670",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-26T21:33:24.261230Z"
},
{
"id": "4806ac1e-5a18-4248-aa0b-a8aaba9e18bf",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-31T17:40:59.043583Z"
},
{
"id": "c9a90ebf-3bf4-4e83-9e8d-684c78ec46cf",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-31T17:40:59.775745Z"
},
{
"id": "82a3648e-dddc-48df-9fb1-bd824f54d47c",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-31T17:40:59.827126Z"
},
{
"id": "8080f194-67a7-4d3c-875e-ca079c52f488",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-31T20:21:14.806767Z"
},
{
"id": "e2a99b1b-d610-4514-9365-ea972783bcb0",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-31T20:21:15.533157Z"
},
{
"id": "ab84ebc1-c3e2-47e3-9dd0-e559c2d312a0",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-31T20:21:15.580065Z"
},
{
"id": "1e3c233f-e8e0-42ce-885b-bea266793d12",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-05-31T21:41:09.815578Z"
},
{
"id": "fad8822a-cc59-4764-8bec-2f534f2ec634",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-05-31T21:41:10.401970Z"
},
{
"id": "b09ce535-bf40-472f-87da-e85f55290373",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-05-31T21:41:10.441339Z"
},
{
"id": "232537f0-4d7b-4670-9694-7b9adda5b603",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-06T08:39:46.574667Z"
},
{
"id": "1e140f9e-2395-438f-97dc-1e2b5943b905",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-06T08:39:47.343780Z"
},
{
"id": "7f2214dc-7411-431c-b45b-9808cae00699",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-06T08:39:47.385403Z"
},
{
"id": "5a833101-c174-422b-b559-b75b3a17ec04",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-07T20:00:16.104074Z"
},
{
"id": "986a0dd0-bf56-417e-950b-483057b22dff",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-07T20:00:16.994790Z"
},
{
"id": "edaee702-01d3-474c-ab5d-ecee84b5fd4b",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-07T20:00:17.046635Z"
},
{
"id": "1680e89d-4cd4-4a15-ba49-36f9ae1b3b39",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-07T20:28:29.145020Z"
},
{
"id": "19dd5edb-9a00-41ce-83f1-ba6a18f72629",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-07T20:28:29.836157Z"
},
{
"id": "70975730-415e-49f7-b00f-9bb473fc1935",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-07T20:28:29.884847Z"
},
{
"id": "f2831593-75fd-45af-99b4-acecdf0ee093",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-08T17:11:03.657162Z"
},
{
"id": "7b837fc0-0d5f-4883-85c9-d47a1e31e94b",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-08T17:11:04.440823Z"
},
{
"id": "cca21a30-5d22-4ec4-b7e8-42717dfec532",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-08T17:11:04.496338Z"
},
{
"id": "912c8c16-1dba-4e7f-9b4f-1f488c227a8d",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-08T17:42:24.975923Z"
},
{
"id": "a44c8c2a-031c-4ffc-af96-7686ebd2e544",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-08T17:42:25.819976Z"
},
{
"id": "aaa2ad5c-0be8-42e2-b01d-54b00aba8ae6",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-08T17:42:25.900328Z"
},
{
"id": "052dbd04-c6d0-4284-acc7-e1758cdf6383",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-08T17:50:17.750777Z"
},
{
"id": "a8c3cbf4-88dd-4c27-b632-f4e8bad30b2a",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-08T17:50:18.457958Z"
},
{
"id": "1088482b-af47-497d-ad30-25e3a8cdada0",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-08T17:50:18.513857Z"
},
{
"id": "5e0abca8-01d3-4e95-80cd-a341e905d7b9",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-14T20:33:54.912988Z"
},
{
"id": "653b0511-4b65-4325-980b-0a9a4c0ee63c",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-14T20:33:55.676031Z"
},
{
"id": "a1cabdec-8914-4c5c-933a-c32c6e898e53",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-14T20:33:55.717602Z"
},
{
"id": "2ea7648e-5d33-4e43-86f9-556539a9cf75",
"actor_id": null,
"action": "embedding_failed",
"target_type": "motion",
"target_id": "99",
"metadata": {
"error": "RuntimeError(\"Simulated embedding failure for index 0: 'failing motion'\")"
},
"created_at": "2026-06-14T21:34:41.928775Z"
},
{
"id": "a3c4bb80-d6ff-41f5-a5e3-41984eade249",
"actor_id": null,
"action": "test_action",
"target_type": "unit",
"target_id": "u1",
"metadata": {
"k": 1
},
"created_at": "2026-06-14T21:34:42.582166Z"
},
{
"id": "6b2168de-3367-4f66-8ae3-7269688872e9",
"actor_id": null,
"action": "another_action",
"target_type": "motion",
"target_id": null,
"metadata": {},
"created_at": "2026-06-14T21:34:42.633930Z"
}
]
Loading…
Cancel
Save