diff --git a/analysis/tabs/overton.py b/analysis/tabs/overton.py index bcb92b4..8d6ec03 100644 --- a/analysis/tabs/overton.py +++ b/analysis/tabs/overton.py @@ -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 diff --git a/docs/plans/2026-06-15-001-feat-category-domain-decomposition-plan.md b/docs/plans/2026-06-15-001-feat-category-domain-decomposition-plan.md new file mode 100644 index 0000000..367e18d --- /dev/null +++ b/docs/plans/2026-06-15-001-feat-category-domain-decomposition-plan.md @@ -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 | diff --git a/docs/solutions/best-practices/motion-category-classification-subagent-pipeline-2026-06-15.md b/docs/solutions/best-practices/motion-category-classification-subagent-pipeline-2026-06-15.md new file mode 100644 index 0000000..3781676 --- /dev/null +++ b/docs/solutions/best-practices/motion-category-classification-subagent-pipeline-2026-06-15.md @@ -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 diff --git a/reports/overton_window/STATUS.md b/reports/overton_window/STATUS.md index a67ebf7..f6b8822 100644 --- a/reports/overton_window/STATUS.md +++ b/reports/overton_window/STATUS.md @@ -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 diff --git a/reports/overton_window/breakpoint_analysis.md b/reports/overton_window/breakpoint_analysis.md index 4df24a2..da9b35e 100644 --- a/reports/overton_window/breakpoint_analysis.md +++ b/reports/overton_window/breakpoint_analysis.md @@ -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 diff --git a/reports/overton_window/breakpoint_figure_1.png b/reports/overton_window/breakpoint_figure_1.png index 5516cd5..5135db1 100644 Binary files a/reports/overton_window/breakpoint_figure_1.png and b/reports/overton_window/breakpoint_figure_1.png differ diff --git a/reports/overton_window/breakpoint_figure_2.png b/reports/overton_window/breakpoint_figure_2.png index 39604e1..b030b8b 100644 Binary files a/reports/overton_window/breakpoint_figure_2.png and b/reports/overton_window/breakpoint_figure_2.png differ diff --git a/reports/overton_window/causal_timing.md b/reports/overton_window/causal_timing.md index 7181dfe..ec3ba69 100644 --- a/reports/overton_window/causal_timing.md +++ b/reports/overton_window/causal_timing.md @@ -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. diff --git a/reports/overton_window/extremity_2d_temporal.md b/reports/overton_window/extremity_2d_temporal.md index ea4d4bc..7023c02 100644 --- a/reports/overton_window/extremity_2d_temporal.md +++ b/reports/overton_window/extremity_2d_temporal.md @@ -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 | --- diff --git a/reports/overton_window/extremity_2d_temporal_figure.png b/reports/overton_window/extremity_2d_temporal_figure.png index 6016353..9957fd7 100644 Binary files a/reports/overton_window/extremity_2d_temporal_figure.png and b/reports/overton_window/extremity_2d_temporal_figure.png differ diff --git a/reports/overton_window/left_wing_response.md b/reports/overton_window/left_wing_response.md index c4d8bce..77aa0c2 100644 --- a/reports/overton_window/left_wing_response.md +++ b/reports/overton_window/left_wing_response.md @@ -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 | --- diff --git a/reports/overton_window/left_wing_response_figure.png b/reports/overton_window/left_wing_response_figure.png index ba28f26..6eae8fa 100644 Binary files a/reports/overton_window/left_wing_response_figure.png and b/reports/overton_window/left_wing_response_figure.png differ diff --git a/reports/overton_window/mechanism_classification.md b/reports/overton_window/mechanism_classification.md index ceeda9d..6aade67 100644 --- a/reports/overton_window/mechanism_classification.md +++ b/reports/overton_window/mechanism_classification.md @@ -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 diff --git a/reports/overton_window/overton_window.qmd b/reports/overton_window/overton_window.qmd index 878ffbd..6171407 100644 --- a/reports/overton_window/overton_window.qmd +++ b/reports/overton_window/overton_window.qmd @@ -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 diff --git a/reports/overton_window/overton_window_synthesis.md b/reports/overton_window/overton_window_synthesis.md index b035216..5acd0dc 100644 --- a/reports/overton_window/overton_window_synthesis.md +++ b/reports/overton_window/overton_window_synthesis.md @@ -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 diff --git a/reports/overton_window/party_differentiation.md b/reports/overton_window/party_differentiation.md index 0cb04ce..9ec08ca 100644 --- a/reports/overton_window/party_differentiation.md +++ b/reports/overton_window/party_differentiation.md @@ -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. diff --git a/reports/overton_window/predictive_model.md b/reports/overton_window/predictive_model.md index 5985ba5..ba67a86 100644 --- a/reports/overton_window/predictive_model.md +++ b/reports/overton_window/predictive_model.md @@ -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? diff --git a/reports/overton_window/predictive_model_figure.png b/reports/overton_window/predictive_model_figure.png index a8c1d2f..f328f44 100644 Binary files a/reports/overton_window/predictive_model_figure.png and b/reports/overton_window/predictive_model_figure.png differ diff --git a/reports/overton_window/success_correlation.md b/reports/overton_window/success_correlation.md index db71091..dcee7d6 100644 --- a/reports/overton_window/success_correlation.md +++ b/reports/overton_window/success_correlation.md @@ -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. diff --git a/reports/overton_window/temporal_trajectory.md b/reports/overton_window/temporal_trajectory.md index 2eda92c..32045c6 100644 --- a/reports/overton_window/temporal_trajectory.md +++ b/reports/overton_window/temporal_trajectory.md @@ -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. diff --git a/reports/overton_window/temporal_trajectory_figure.png b/reports/overton_window/temporal_trajectory_figure.png index 79b9d2a..fe6570e 100644 Binary files a/reports/overton_window/temporal_trajectory_figure.png and b/reports/overton_window/temporal_trajectory_figure.png differ diff --git a/reports/overton_window/voting_margin.md b/reports/overton_window/voting_margin.md index 7e441c4..9719c7f 100644 --- a/reports/overton_window/voting_margin.md +++ b/reports/overton_window/voting_margin.md @@ -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. diff --git a/tests/test_category_overton.py b/tests/test_category_overton.py new file mode 100644 index 0000000..529463d --- /dev/null +++ b/tests/test_category_overton.py @@ -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 diff --git a/thoughts/ledgers/audit_events.json b/thoughts/ledgers/audit_events.json index 9e72d18..e047190 100644 --- a/thoughts/ledgers/audit_events.json +++ b/thoughts/ledgers/audit_events.json @@ -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" } ] \ No newline at end of file