---
date: 2026-03-23
topic: "motion content enrichment - next steps"
status: draft
---

## Problem Statement

We successfully ingested SyncFeed motion content, fetched body texts, re-embedded motives, ran fusion (SVD-based) and rebuilt the similarity cache. The pipeline ran end-to-end but showed intermittent failures (embedding provider batch failures, connection-pool warnings) and produced a small number of missing body_texts and potential spurious similarity hits.

**Goal:** Stabilize and harden the motion content enrichment + embedding/fusion/similarity pipeline so it runs reliably, is testable, and produces high-quality similarity results for production use.

## Constraints

- **Do not modify** app.py or scheduler.py.
- Use **DuckDB only** (data/motions.db) and open/close connections per method; avoid long-lived global connections.
- No print() calls in library modules — use logging.getLogger(__name__).
- Tests must continue to run under the existing pytest setup and monkeypatching in CI.
- Avoid YAGNI features: only add monitoring/metrics that are actionable and low-effort.

## Approach (chosen)

I'm leaning toward an **incremental hardening** approach: small, high-impact fixes and QA steps first (low effort, immediate benefit), then follow with a short set of robustness improvements (retries, backoff, audit events) and targeted tests. This minimizes risk and gives quick confidence that the bulk import can be re-run safely.

Alternatives considered:
- Full rewrite of SyncFeed walker to a resilient state-machine (higher effort; unnecessary today).
- Push heavy-duty observability (Prometheus + Grafana) immediately (high overhead; defer to specific metrics and logs first).

I chose incremental hardening because it fixes the concrete failures we saw (provider batch errors, connection pool warnings, one 404 body) quickly and keeps the codebase small and testable.

## Architecture

High-level components:
- **SyncFeed sync script** (scripts/sync_motion_content.py): walk feeds, build title/ext-id maps, fetch body text, update DB.
- **Text embedding pipeline** (pipeline/text_pipeline.py, scripts/rerun_embeddings.py): convert selected text into embeddings, with provider retry logic.
- **Fusion/SVD pipeline** (pipeline/fusion.py, pipeline/svd_pipeline.py): fuse embeddings per-window and produce fused vectors.
- **Similarity compute & lookup** (similarity/compute.py, similarity/lookup.py): compute pairwise similarities and populate cache.
- **DB layer** (database.py, migrations): motions table (body_text, externe_identifier), fused_embeddings, svd_vectors, similarity_cache and audit events.
- **Audit & continuity** (thoughts/ledgers/*, audit_events table): record run summaries and per-window results.

Responsibilities are unchanged; we add a small **ai_provider wrapper** and an **operations script** for QA and rerun orchestration.

## Components & Responsibilities

- **sync_motion_content.py**: keep as-is; add more granular logging and a CLI flag to limit to a subset (for QA). Responsible for idempotent updates.
- **_fetch_body_text / fetch_body_texts**: reduce max_workers or add retry on transient HTTP errors; wrap requests.Session with adapters to control pool size.
- **text_pipeline.ai_provider**: add a small retry/backoff wrapper that retries failed batches with exponential backoff and a fallback to smaller batch_size.
- **scripts/rerun_embeddings.py**: expose a `--retry-missing` mode that detects missing embeddings and retries with smaller batches.
- **similarity.compute**: keep padding logic; add a filter to avoid trivial 1.0 matches for extremely short titles (query/UI should also filter but apply DB-side filter for safety).
- **migrations**: add audit_events or mark which motions failed fetch/embedding for manual review.
- **tests**: add deterministic tests for retry behavior and for the QA-sample similarity checks.

## Data Flow

1. Walk SyncFeed (Besluit, Zaak, Document, DocumentVersie) → parse elements.
2. Build **title_map** and **ext_id_map** in-memory.
3. Fetch body_texts in parallel (ThreadPoolExecutor) → map ext_id -> body_text.
4. Update motions table with title, externe_identifier, body_text.
5. Run text embeddings for motions (COALESCE priority: layman_explanation → body_text → description → title).
6. Fuse embeddings per-window (svd_vectors) → produce fused_embeddings.
7. Compute similarity cache per-window and insert rows.
8. QA checks and audit logs produced for runs.

## Error Handling Strategy

- **HTTP / body fetches:** add per-ext_id retries (3 attempts) with short exponential backoff; capture and store failures in audit_events table for manual follow-up.
- **Connection pool warnings:** reduce ThreadPoolExecutor concurrency (configurable flag) and attach a requests.adapters HTTPAdapter with a limited pool size to avoid 'Connection pool is full' warnings.
- **Embedding provider failures:** implement a wrapper which:
  - retries batches up to N times with exponential backoff,
  - on persistent failure, retry missing items with a smaller batch_size,
  - mark failed motion ids in an audit table rather than blocking the entire run.
- **Similarity anomalies (1.0 scores):** filter out identity matches and very-short-text matches when building similarity cache; record these in diagnostics output.

## Testing Strategy

- Add unit tests for parser functions (already present) to cover edge cases seen in real SyncFeed XML.
- Add a unit test for the ai_provider retry wrapper that simulates provider failures and verifies fallback to smaller batches.
- Add an integration QA script (scripts/qa_similarity.py) that:
  - samples N motions across windows,
  - runs lookup.similarity and asserts results are within expected ranges (e.g., top-5 not all 1.0 unless identical text),
  - outputs a short summary JSON saved to thoughts/ledgers/ for each run.
- CI: run the new provider-retry test and the QA script with a small dataset (mocked provider) to ensure no regressions.

## Actionable Next Steps (prioritized)

1. Quick QA (1 day) — sample 50 motions and inspect similarity quality.
   - Implement scripts/qa_similarity.py (sample + assert heuristics).
   - Run locally and record summary in thoughts/ledgers.

2. Small robustness fixes (1–2 days) — low-risk changes with big wins.
   - Add ai_provider retry/backoff wrapper and unit tests.
   - Add `--max-body-workers` CLI flag and drop default to 10; add per-request retries.
   - Add `--retry-missing` mode to rerun_embeddings to retry failed batches with smaller sizes.

3. Observability & audit (1 day) — make failures visible and actionable.
   - Add audit_events table rows when body_text fetch or embedding fails.
   - Write an end-of-run JSON summary (already done) and attach per-window stats to ledger.

4. Safety filters & dedupe (0.5 day)
   - Add a small DB-side filter to skip trivial identical-title matches in similarity cache.
   - Audit SVD windows for duplication and dedupe if needed.

5. Run full re-run (off-peak) and validate (1 day)
   - Re-run embeddings, fusion and similarity; run QA script and review ledgers.

Estimated total: 3–5 days of focused work.

## Open Questions

- Do we want to persist per-item failure flags in DuckDB (audit_events) or just in ledgers? I recommend adding an **audit_events** table to speed triage.
- What SLA / acceptance criteria should we use for similarity quality? E.g., maximum allowed fraction of top-1 exact-title matches for non-identical motions.
- Are we comfortable reducing body fetch concurrency by default, or should we attempt a more adaptive concurrency strategy?

---

I'm proceeding to create the design doc. Interrupt if you want changes.
