chore: add .mindmodel/ project constraints and conventions
Generated mindmodel covering stack, architecture, domain glossary, coding conventions, DuckDB/requests/embeddings/error-handling patterns, anti-patterns, and explicit constraints for naming, imports, DB access, error handling, and testing.
This commit is contained in:
@@ -0,0 +1,43 @@
|
|||||||
|
# Known anti-patterns and recommended remediation (Phase 1 findings)
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- id: broad_except_swallows_errors
|
||||||
|
description: "Wide except: clauses that swallow exceptions without logging or re-raising."
|
||||||
|
examples:
|
||||||
|
- path: multiple
|
||||||
|
note: "Observed in various pipeline and ingestion spots where except Exception: returns a default without context."
|
||||||
|
remediation:
|
||||||
|
- "Replace broad except with specific exceptions."
|
||||||
|
- "When broad except is absolutely needed, call logger.exception(...) and re-raise or convert to a typed domain error."
|
||||||
|
- "Add unit tests to ensure critical errors are visible in CI logs."
|
||||||
|
|
||||||
|
- id: mixed_print_and_logging
|
||||||
|
description: "Mixing print() and logging() for errors and info messages."
|
||||||
|
examples:
|
||||||
|
- path: api_client.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
print(f"Fetched {len(voting_records)} voting records from API")
|
||||||
|
...
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching motions from API: {e}")
|
||||||
|
```
|
||||||
|
remediation:
|
||||||
|
- "Use logging.getLogger(__name__) and logger.info/warning/exception consistently."
|
||||||
|
- "Add a top-level logging configuration for Streamlit and scripts."
|
||||||
|
|
||||||
|
- id: no_lockfile
|
||||||
|
description: "No lockfile present -> unreproducible installs and CI unpredictability."
|
||||||
|
remediation:
|
||||||
|
- "Add a lockfile (poetry.lock, requirements.txt produced by pip-tools) and pin versions in CI."
|
||||||
|
- "Make CI use the lockfile for reproducible builds."
|
||||||
|
|
||||||
|
- id: declared_but_unused_dependency
|
||||||
|
description: "Dependency declared but unused (openai in pyproject)."
|
||||||
|
remediation:
|
||||||
|
- "Either remove the dependency or add clear adapter code/tests that exercise it. Keep pyproject tidy."
|
||||||
|
|
||||||
|
- id: brittle_identity_heuristics
|
||||||
|
description: "Heuristics for MP identity (comma-based parsing) are brittle."
|
||||||
|
remediation:
|
||||||
|
- "Add robust parsing rules and unit tests; prefer canonical identifiers (persoon_id) where available."
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Architecture overview and confidence levels
|
||||||
|
|
||||||
|
layers:
|
||||||
|
- name: ui
|
||||||
|
description: "Streamlit pages and app entrypoints (Home.py, pages/*)."
|
||||||
|
confidence: high
|
||||||
|
- name: ingestion
|
||||||
|
description: "API client and scrapers (api_client.py, scraper.py)."
|
||||||
|
confidence: high
|
||||||
|
- name: processing
|
||||||
|
description: "Pipelines for embeddings, SVD, fusion (pipeline/*, similarity/*)."
|
||||||
|
confidence: high
|
||||||
|
- name: storage
|
||||||
|
description: "DuckDB primary store; JSON fallback used in tests when duckdb missing."
|
||||||
|
confidence: high
|
||||||
|
- name: ai_provider
|
||||||
|
description: "Lightweight HTTP wrapper around OpenRouter/OpenAI-style backends in ai_provider.py."
|
||||||
|
confidence: medium
|
||||||
|
- name: orchestration
|
||||||
|
description: "Script-based orchestration (scripts/*.py), rerun_embeddings, scheduler."
|
||||||
|
confidence: medium
|
||||||
|
|
||||||
|
organization:
|
||||||
|
- Keep UI code separated from heavy compute — Streamlit runs should avoid heavy compute inline (use subprocess or schedule).
|
||||||
|
- Pipelines are implemented as re-entrant functions returning summary dicts to facilitate testing and subprocess usage (seen in svd_pipeline.compute_svd_for_window).
|
||||||
|
- DB access is centralised via MotionDatabase helper (database.py) with convenience methods (store_fused_embedding, append_audit_event).
|
||||||
|
|
||||||
|
design_decisions:
|
||||||
|
- Use DuckDB for local fast analytics storage; read_only connections used in compute stages to allow parallel workers.
|
||||||
|
- Embeddings and similarity cache are stored as JSON in DuckDB tables (vector columns).
|
||||||
|
- The ai_provider uses requests with retry/backoff rather than a heavy SDK to keep testing simple.
|
||||||
|
|
||||||
|
confidence_summary:
|
||||||
|
overall_confidence: high
|
||||||
|
notes: "Phase 1 input inspected files across the repo; design mapping is consistent with code samples."
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# DB connection handling constraints
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: use_context_managers_for_connections
|
||||||
|
rule: "Prefer using 'with duckdb.connect(path, read_only=...) as conn' for scoped DB interactions where possible."
|
||||||
|
rationale: "Ensures proper resource cleanup and avoids connection leaks."
|
||||||
|
|
||||||
|
- name: read_only_for_compute
|
||||||
|
rule: "Use read_only=True for compute steps that only read data (SVD, similarity compute)."
|
||||||
|
rationale: "Allows safe parallel workers and reduces write contention."
|
||||||
|
|
||||||
|
- name: short_lived_writes
|
||||||
|
rule: "When performing database writes, open short-lived connections, commit quickly and close."
|
||||||
|
rationale: "Avoids long-lived transactions and reduces lock windows."
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: pipeline/svd_pipeline.py
|
||||||
|
snippet: |
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
try:
|
||||||
|
rows = conn.execute(...).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
anti_patterns_and_remediations:
|
||||||
|
- bad: "Creating a global connection at import that performs migrations."
|
||||||
|
remediation: "Move migrations to an explicit init function that runs at deployment/upgrade time."
|
||||||
|
- bad: "Not closing connections on exceptions."
|
||||||
|
remediation: "Wrap connects in `with` or finally: conn.close() blocks."
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Error handling style rules (YAML constraint example)
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: explicit_exceptions
|
||||||
|
rule: "Raise explicit exceptions (ValueError, ProviderError) for known error conditions rather than returning magic values."
|
||||||
|
examples:
|
||||||
|
- good: |
|
||||||
|
if not isinstance(text, str):
|
||||||
|
raise ProviderError('text must be a string')
|
||||||
|
- bad: |
|
||||||
|
if not isinstance(text, str):
|
||||||
|
return []
|
||||||
|
|
||||||
|
- name: avoid_broad_except
|
||||||
|
rule: "Avoid 'except Exception:' that swallows errors. If broad except is used for best-effort, log the exception with logger.exception and re-raise or convert."
|
||||||
|
examples:
|
||||||
|
- bad: |
|
||||||
|
try:
|
||||||
|
do_work()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
- remediation: |
|
||||||
|
try:
|
||||||
|
do_work()
|
||||||
|
except SpecificError as exc:
|
||||||
|
logger.warning('Handled error: %s', exc)
|
||||||
|
raise
|
||||||
|
|
||||||
|
- name: logging_over_print
|
||||||
|
rule: "Prefer logger.* over print() for messages and errors."
|
||||||
|
examples:
|
||||||
|
- bad: "print('Error fetching motions from API: %s' % e)"
|
||||||
|
- good: "logger.exception('Error fetching motions from API')"
|
||||||
|
|
||||||
|
enforcement_examples:
|
||||||
|
- "Add a static code check to flag 'print(' in modules (except in simple scripts) and 'except Exception:' usages without logger.exception."
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Import grouping and ordering constraints
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: grouping
|
||||||
|
rule: "Group imports in three sections separated by a single blank line: stdlib, third-party, local."
|
||||||
|
examples:
|
||||||
|
- good: |
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
from .pipeline import text_pipeline
|
||||||
|
- bad: |
|
||||||
|
import duckdb
|
||||||
|
import json
|
||||||
|
from pipeline import text_pipeline
|
||||||
|
|
||||||
|
- name: from_imports
|
||||||
|
rule: "Prefer 'from x import y' only when it improves clarity or avoids circular import; otherwise import module and reference attributes."
|
||||||
|
|
||||||
|
enforcement_examples:
|
||||||
|
- "Run isort or ruff- import sorting in pre-commit or CI to enforce ordering."
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Naming constraint rules (example constraint file)
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: module_file_names
|
||||||
|
rule: "Use snake_case for Python module filenames (e.g., text_pipeline.py, ai_provider.py)."
|
||||||
|
examples:
|
||||||
|
- good: "text_pipeline.py"
|
||||||
|
- bad: "TextPipeline.py"
|
||||||
|
|
||||||
|
- name: function_names
|
||||||
|
rule: "Use snake_case for functions and methods."
|
||||||
|
examples:
|
||||||
|
- good: "def compute_similarities(...):"
|
||||||
|
- bad: "def ComputeSimilarities(...):"
|
||||||
|
|
||||||
|
- name: class_names
|
||||||
|
rule: "Use PascalCase for classes."
|
||||||
|
examples:
|
||||||
|
- good: "class MotionDatabase:"
|
||||||
|
- bad: "class motion_database:"
|
||||||
|
|
||||||
|
- name: constants
|
||||||
|
rule: "Constants use UPPER_SNAKE_CASE."
|
||||||
|
examples:
|
||||||
|
- good: "VOTE_MAP = { ... }"
|
||||||
|
- bad: "vote_map = { ... }"
|
||||||
|
|
||||||
|
enforcement_examples:
|
||||||
|
- "Add a linter rule in CI: ruff or flake8 naming plugin to detect violations."
|
||||||
|
- "Run `python -m pip install ruff` and `ruff check` as part of CI."
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
# Testing conventions constraint (YAML)
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- name: test_naming
|
||||||
|
rule: "Use pytest and name tests test_*.py and test_* functions."
|
||||||
|
examples:
|
||||||
|
- good: "tests/test_text_pipeline.py"
|
||||||
|
- bad: "tests/text_pipeline_test.py"
|
||||||
|
|
||||||
|
- name: fixtures_and_conftest
|
||||||
|
rule: "Place shared fixtures in tests/conftest.py or tests/fixtures/ for reuse."
|
||||||
|
examples:
|
||||||
|
- good: "use fixtures declared in tests/conftest.py"
|
||||||
|
|
||||||
|
- name: assert_raises
|
||||||
|
rule: "Explicitly assert expected exceptions with pytest.raises for invalid input."
|
||||||
|
examples:
|
||||||
|
- good: |
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
def test_invalid_input():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
function_under_test('bad')
|
||||||
|
|
||||||
|
enforcement_examples:
|
||||||
|
- "Run pytest in CI; fail if tests don't run or if there are regressions."
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# Coding conventions cheat-sheet (extracted from Phase 1)
|
||||||
|
|
||||||
|
naming:
|
||||||
|
module_files: snake_case (e.g., text_pipeline.py, ai_provider.py)
|
||||||
|
functions: snake_case
|
||||||
|
classes: PascalCase
|
||||||
|
constants: UPPER_SNAKE_CASE
|
||||||
|
module_singletons: module-level instances, named lower_snake (e.g., db = MotionDatabase())
|
||||||
|
|
||||||
|
imports:
|
||||||
|
order:
|
||||||
|
- stdlib
|
||||||
|
- third-party
|
||||||
|
- local application imports
|
||||||
|
style:
|
||||||
|
- group imports with a blank line between groups
|
||||||
|
- prefer "from x import y" only when needed to avoid circular imports
|
||||||
|
|
||||||
|
types_and_dataclasses:
|
||||||
|
- Use type hints broadly (functions, public APIs)
|
||||||
|
- config should be a dataclass in config.py
|
||||||
|
- Module-level singletons are allowed (but follow lifecycle rules in db_connection constraints)
|
||||||
|
|
||||||
|
tests:
|
||||||
|
- pytest
|
||||||
|
- tests/ directory, files named test_*.py
|
||||||
|
- Use fixtures in tests/fixtures and conftest.py
|
||||||
|
- Tests expect raises(...) for invalid input or ProviderError
|
||||||
|
|
||||||
|
error_handling:
|
||||||
|
- Prefer explicit exceptions (ValueError, ProviderError)
|
||||||
|
- Avoid overly-broad except: clauses (see anti-patterns)
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Dependencies map and recommended extras (Phase 1 authoritative)
|
||||||
|
declared:
|
||||||
|
- streamlit
|
||||||
|
- duckdb
|
||||||
|
- ibis-framework[duckdb]
|
||||||
|
- plotly
|
||||||
|
- scikit-learn
|
||||||
|
- scipy
|
||||||
|
- umap-learn
|
||||||
|
- openai # note: declared but not observed imported; review usage
|
||||||
|
- requests
|
||||||
|
|
||||||
|
observed:
|
||||||
|
- requests
|
||||||
|
- duckdb (used but sometimes import guarded)
|
||||||
|
- numpy
|
||||||
|
- pytest
|
||||||
|
|
||||||
|
grouped:
|
||||||
|
core:
|
||||||
|
- python >=3.13
|
||||||
|
- streamlit
|
||||||
|
- duckdb
|
||||||
|
- ibis-framework[duckdb]
|
||||||
|
- requests
|
||||||
|
ml:
|
||||||
|
- scikit-learn
|
||||||
|
- scipy
|
||||||
|
- umap-learn
|
||||||
|
- numpy
|
||||||
|
viz:
|
||||||
|
- plotly
|
||||||
|
testing:
|
||||||
|
- pytest
|
||||||
|
|
||||||
|
recommended_extras:
|
||||||
|
reproducibility:
|
||||||
|
- poetry (poetry.lock) or pip-tools (requirements.txt + requirements.in)
|
||||||
|
- pipx or virtualenv usage documented
|
||||||
|
linting_and_formatting:
|
||||||
|
- black
|
||||||
|
- ruff
|
||||||
|
- isort
|
||||||
|
- mypy
|
||||||
|
logging_and_monitoring:
|
||||||
|
- structlog (optional)
|
||||||
|
containerization:
|
||||||
|
- docker (already used)
|
||||||
|
heavy_analytics (optional):
|
||||||
|
- pandas
|
||||||
|
- altair
|
||||||
|
- dash (if more interactive dashboards are needed)
|
||||||
|
notes:
|
||||||
|
- Because no lockfile was present during Phase 1, adding one is high priority for reproducible CI builds.
|
||||||
|
- openai is declared but not imported anywhere in Phase 1 files; prefer to either remove or add an explicit adapter usage and tests.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Domain glossary (core concepts from Phase 1)
|
||||||
|
|
||||||
|
terms:
|
||||||
|
Motion:
|
||||||
|
short: "A parliamentary motion/decision"
|
||||||
|
keys: [id, title, description, date, body_text, url]
|
||||||
|
motie:
|
||||||
|
short: "Dutch: motion (motie). Equivalent to Motion in code comments and UI."
|
||||||
|
MP:
|
||||||
|
short: "Member of Parliament (kamerlid)"
|
||||||
|
keys: [mp_name, party, van, tot_en_met, persoon_id]
|
||||||
|
mp_votes:
|
||||||
|
short: "Raw voting rows: motion_id, mp_name, vote, date"
|
||||||
|
mp_metadata:
|
||||||
|
short: "Per-MP metadata table and fields"
|
||||||
|
user_sessions:
|
||||||
|
short: "Streamlit user quiz session state (session_id, user_votes, completed_motions...)"
|
||||||
|
embeddings:
|
||||||
|
short: "Raw text embeddings stored per motion (embeddings table)"
|
||||||
|
svd_vectors:
|
||||||
|
short: "SVD-derived vectors from the vote matrix (svd_vectors table)"
|
||||||
|
fused_embeddings:
|
||||||
|
short: "Concatenation of SVD and text embeddings (fused_embeddings table)"
|
||||||
|
similarity_cache:
|
||||||
|
short: "Precomputed nearest neighbors for each motion"
|
||||||
|
window_id:
|
||||||
|
short: "Processing window identifier used for SVD/fusion runs"
|
||||||
|
controversy_score:
|
||||||
|
short: "Numeric measure stored in motions table"
|
||||||
|
winning_margin:
|
||||||
|
short: "Numeric field indicating margin of win in a vote"
|
||||||
|
Politiek_Kompas:
|
||||||
|
short: "Political compass; also appears in UI features"
|
||||||
|
MP_quiz:
|
||||||
|
short: "Interactive quiz derived from motions and mp_votes"
|
||||||
|
notes:
|
||||||
|
- Use these canonical terms in docs, tests, variable names and DB schemas.
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
# Extracted pattern examples (representative snippets)
|
||||||
|
|
||||||
|
Note: snippets are verbatim extracts from repository files (Phase 1). Paths shown.
|
||||||
|
|
||||||
|
## DuckDB connect + schema init (database.py)
|
||||||
|
```python
|
||||||
|
conn = duckdb.connect(self.db_path)
|
||||||
|
|
||||||
|
# Create sequence for auto-incrementing IDs
|
||||||
|
try:
|
||||||
|
conn.execute("CREATE SEQUENCE IF NOT EXISTS motions_id_seq START 1")
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Create tables with proper ID handling
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS motions (
|
||||||
|
id INTEGER DEFAULT nextval('motions_id_seq'),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT,
|
||||||
|
date DATE,
|
||||||
|
policy_area TEXT,
|
||||||
|
voting_results JSON,
|
||||||
|
winning_margin FLOAT,
|
||||||
|
controversy_score FLOAT,
|
||||||
|
layman_explanation TEXT,
|
||||||
|
externe_identifier TEXT,
|
||||||
|
body_text TEXT,
|
||||||
|
url TEXT UNIQUE,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Read-only compute worker (svd_pipeline.py)
|
||||||
|
```python
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT motion_id, mp_name, vote FROM mp_votes WHERE date BETWEEN ? AND ?",
|
||||||
|
(start_date, end_date),
|
||||||
|
).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requests with retry/backoff (ai_provider.py)
|
||||||
|
```python
|
||||||
|
resp = requests.post(url, json=json, headers=headers, timeout=10)
|
||||||
|
...
|
||||||
|
if getattr(resp, "status_code", 0) == 429:
|
||||||
|
if attempt == retries:
|
||||||
|
raise ProviderError(f"Provider returned HTTP {resp.status_code}")
|
||||||
|
retry_after = None
|
||||||
|
raw = resp.headers.get("Retry-After") if getattr(resp, "headers", None) else None
|
||||||
|
if raw:
|
||||||
|
try:
|
||||||
|
retry_after = int(raw)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
dt = parsedate_to_datetime(raw)
|
||||||
|
now = datetime.now(tz=dt.tzinfo or timezone.utc)
|
||||||
|
secs = (dt - now).total_seconds()
|
||||||
|
retry_after = max(0, int(secs))
|
||||||
|
except Exception:
|
||||||
|
retry_after = None
|
||||||
|
|
||||||
|
if retry_after is not None:
|
||||||
|
time.sleep(retry_after)
|
||||||
|
continue
|
||||||
|
```
|
||||||
|
|
||||||
|
## Embedding batch + per-item fallback (pipeline/ai_provider_wrapper.py)
|
||||||
|
```python
|
||||||
|
for start in range(0, len(texts), batch_size):
|
||||||
|
chunk = texts[i:end]
|
||||||
|
emb_chunk, emb_exc = _attempt_batch(chunk, i)
|
||||||
|
if emb_chunk is not None:
|
||||||
|
for j, emb in enumerate(emb_chunk):
|
||||||
|
results[i + j] = emb
|
||||||
|
i = end
|
||||||
|
continue
|
||||||
|
|
||||||
|
# batch failed -> fallback to per-item attempts
|
||||||
|
for j in range(i, end):
|
||||||
|
t = texts[j]
|
||||||
|
single, single_exc = _attempt_batch([t], j)
|
||||||
|
if single:
|
||||||
|
results[j] = single[0]
|
||||||
|
continue
|
||||||
|
results[j] = None
|
||||||
|
```
|
||||||
|
|
||||||
|
## Similarity compute (similarity/compute.py)
|
||||||
|
```python
|
||||||
|
# Ensure consistent dimensionality: pad shorter vectors with zeros
|
||||||
|
lengths = [len(v) for v in vecs]
|
||||||
|
max_dim = max(lengths)
|
||||||
|
if len(set(lengths)) != 1:
|
||||||
|
logger.warning(
|
||||||
|
"Inconsistent vector dimensions detected (max=%d). Padding shorter vectors with zeros.",
|
||||||
|
max_dim,
|
||||||
|
)
|
||||||
|
|
||||||
|
matrix = np.zeros((len(vecs), max_dim), dtype=np.float32)
|
||||||
|
for i, v in enumerate(vecs):
|
||||||
|
matrix[i, : len(v)] = v
|
||||||
|
|
||||||
|
# Normalize rows and compute cosine similarity
|
||||||
|
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
|
||||||
|
norms[norms == 0] = 1.0
|
||||||
|
normalized = matrix / norms
|
||||||
|
sim = normalized @ normalized.T
|
||||||
|
```
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
name: stemwijzer
|
||||||
|
version: 2
|
||||||
|
categories:
|
||||||
|
- path: stack.yaml
|
||||||
|
description: Project technology stack (languages, frameworks, runtime)
|
||||||
|
group: stack
|
||||||
|
- path: dependencies.yaml
|
||||||
|
description: Declared and recommended dependencies grouped by purpose
|
||||||
|
group: stack
|
||||||
|
- path: system.md
|
||||||
|
description: System overview and architecture high-level notes
|
||||||
|
group: architecture
|
||||||
|
- path: architecture.yaml
|
||||||
|
description: Architectural layers, organization and confidence levels
|
||||||
|
group: architecture
|
||||||
|
- path: conventions.yaml
|
||||||
|
description: Coding conventions cheat-sheet (naming, imports, types)
|
||||||
|
group: style
|
||||||
|
- path: domain-glossary.yaml
|
||||||
|
description: Business domain glossary for the project
|
||||||
|
group: domain
|
||||||
|
- path: patterns/duckdb_access.yaml
|
||||||
|
description: DuckDB access patterns, examples, and anti-patterns
|
||||||
|
group: patterns
|
||||||
|
- path: patterns/requests_http.yaml
|
||||||
|
description: Requests/HTTP client usage and retry best-practices
|
||||||
|
group: patterns
|
||||||
|
- path: patterns/embeddings_similarity.yaml
|
||||||
|
description: Embedding, SVD, fusion and similarity pipeline patterns
|
||||||
|
group: patterns
|
||||||
|
- path: patterns/error_handling.yaml
|
||||||
|
description: Error handling patterns and rules
|
||||||
|
group: patterns
|
||||||
|
- path: patterns/validation.yaml
|
||||||
|
description: Input/domain validation patterns and examples
|
||||||
|
group: patterns
|
||||||
|
- path: patterns/module_singletons.yaml
|
||||||
|
description: Module-level singletons and lifecycle patterns
|
||||||
|
group: patterns
|
||||||
|
- path: anti-patterns.yaml
|
||||||
|
description: Known anti-patterns and remediation steps
|
||||||
|
group: patterns
|
||||||
|
- path: examples/pattern-examples.md
|
||||||
|
description: Consolidated extracted code examples across patterns
|
||||||
|
group: patterns
|
||||||
|
- path: constraints/naming.yaml
|
||||||
|
description: Enforce naming rules (snake_case, PascalCase, constants)
|
||||||
|
group: constraints
|
||||||
|
- path: constraints/imports.yaml
|
||||||
|
description: Enforce import grouping and ordering
|
||||||
|
group: constraints
|
||||||
|
- path: constraints/db_connection.yaml
|
||||||
|
description: Rules for opening/closing DB connections and read-only usage
|
||||||
|
group: constraints
|
||||||
|
- path: constraints/error_handling.yaml
|
||||||
|
description: Error handling style and allowed exception scopes
|
||||||
|
group: constraints
|
||||||
|
- path: constraints/testing.yaml
|
||||||
|
description: Test conventions (pytest, test naming, fixtures)
|
||||||
|
group: constraints
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
name: duckdb_access
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Prefer using read_only=True for compute-only subprocesses (e.g., SVD compute) to allow concurrent readers.
|
||||||
|
- Prefer "with duckdb.connect(db_path, read_only=True) as conn" for scoped connections so conn.close() is automatic.
|
||||||
|
- If a long-lived connection is created at module level, provide explicit close() or ensure operation is safe for Streamlit's lifecycle.
|
||||||
|
- Prefer parameterizing db_path in pipelines and creating connections locally (avoid global connections that cross threads).
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: database.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
conn = duckdb.connect(self.db_path)
|
||||||
|
...
|
||||||
|
conn.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS fused_embeddings (
|
||||||
|
id INTEGER DEFAULT nextval('fused_embeddings_id_seq'),
|
||||||
|
motion_id INTEGER NOT NULL,
|
||||||
|
window_id TEXT NOT NULL,
|
||||||
|
vector JSON NOT NULL,
|
||||||
|
svd_dims INTEGER NOT NULL,
|
||||||
|
text_dims INTEGER NOT NULL,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
note: explicit connect/close used when initializing schema
|
||||||
|
|
||||||
|
- path: pipeline/svd_pipeline.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
try:
|
||||||
|
rows = conn.execute(
|
||||||
|
"SELECT motion_id, mp_name, vote FROM mp_votes WHERE date BETWEEN ? AND ?",
|
||||||
|
(start_date, end_date),
|
||||||
|
).fetchall()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
```
|
||||||
|
note: read_only connection used for compute-heavy worker
|
||||||
|
|
||||||
|
- path: similarity/compute.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
except Exception:
|
||||||
|
logger.exception("duckdb import failed; cannot load vectors")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
with duckdb.connect(db.db_path) as conn:
|
||||||
|
rows = conn.execute(query, params).fetchall()
|
||||||
|
```
|
||||||
|
note: preferred 'with' context for automatic close
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: creating a connection without closure in a long-running process
|
||||||
|
remediation: use "with" context or ensure conn.close() in finally block
|
||||||
|
example: |
|
||||||
|
```python
|
||||||
|
# BAD: connection may leak if exception occurs before explicit close
|
||||||
|
conn = duckdb.connect(db_path)
|
||||||
|
rows = conn.execute("SELECT ...").fetchall()
|
||||||
|
# missing finally/close
|
||||||
|
```
|
||||||
|
- Bad: Opening write connections from many parallel workers without coordination
|
||||||
|
remediation: open read_only for compute processes and centralize writes via short-lived connections or a single writer worker.
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
name: embeddings_similarity_pipeline
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Keep embedding calls batched where possible; fallback to per-item attempts on persistent batch failure.
|
||||||
|
- Store raw embeddings, SVD vectors, and fused_embeddings separately; fused_embeddings are typically concatenation [svd + text].
|
||||||
|
- Compute similarity as normalized cosine on padded vectors; record top-k neighbors in similarity_cache.
|
||||||
|
- Use read_only DuckDB connections in compute workers to allow parallel runs.
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: pipeline/ai_provider_wrapper.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
for start in range(0, len(texts), batch_size):
|
||||||
|
chunk = texts[start : start + batch_size]
|
||||||
|
resp = _post_with_retries("/embeddings", json={"model": model, "input": chunk})
|
||||||
|
...
|
||||||
|
for j in range(i, end):
|
||||||
|
t = texts[j]
|
||||||
|
single, single_exc = _attempt_batch([t], j)
|
||||||
|
if single:
|
||||||
|
results[j] = single[0]
|
||||||
|
```
|
||||||
|
note: batched embed + fallback per-item retry
|
||||||
|
|
||||||
|
- path: pipeline/fusion.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
svd_vec = json.loads(svd_json)
|
||||||
|
except Exception:
|
||||||
|
_logger.exception("Invalid SVD vector JSON for entity %s", entity_id)
|
||||||
|
skipped_missing_svd += 1
|
||||||
|
continue
|
||||||
|
...
|
||||||
|
fused = list(svd_vec) + list(text_vec)
|
||||||
|
res = db.store_fused_embedding(
|
||||||
|
int(entity_id),
|
||||||
|
window_id,
|
||||||
|
fused,
|
||||||
|
svd_dims=len(svd_vec),
|
||||||
|
text_dims=len(text_vec),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
note: concatenation of vectors and storage via MotionDatabase
|
||||||
|
|
||||||
|
- path: similarity/compute.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
# Normalize rows
|
||||||
|
norms = np.linalg.norm(matrix, axis=1, keepdims=True)
|
||||||
|
norms[norms == 0] = 1.0
|
||||||
|
normalized = matrix / norms
|
||||||
|
sim = normalized @ normalized.T
|
||||||
|
...
|
||||||
|
# pick top-k neighbors and write to similarity_cache
|
||||||
|
```
|
||||||
|
note: numeric pipeline and padding to consistent dimensionality
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: Assuming consistent vector length without checks (leads to shape errors).
|
||||||
|
remediation: Detect inconsistent lengths, pad with zeros, and log a warning (as seen in compute.py).
|
||||||
|
- Bad: Recomputing heavy pipelines inline in UI requests.
|
||||||
|
remediation: schedule heavy work in scripts/subprocesses and read precomputed results in UI.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
name: error_handling
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Use explicit exceptions for domain/error classification (e.g., ProviderError, ValueError).
|
||||||
|
- Prefer logging.exception when catching an exception where stack trace is useful.
|
||||||
|
- Avoid broad except: clauses that swallow exceptions; if broad except is used for "best-effort" fallback, log at warning and include original exception context.
|
||||||
|
- For public library-like functions, prefer raising typed exceptions instead of returning magic values ([], False) — only return safe defaults where documented.
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: ai_provider.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
except requests.ConnectionError as exc:
|
||||||
|
if attempt == retries:
|
||||||
|
raise ProviderError(
|
||||||
|
f"Connection error when calling provider: {exc}"
|
||||||
|
) from exc
|
||||||
|
...
|
||||||
|
```
|
||||||
|
note: mapping network error to ProviderError with re-raise chaining
|
||||||
|
|
||||||
|
- path: pipeline/ai_provider_wrapper.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
except Exception:
|
||||||
|
_logger.exception("Failed to append audit event for embedding failure")
|
||||||
|
results[j] = None
|
||||||
|
```
|
||||||
|
note: logs and assigns None for failure; fallback behavior documented earlier in wrapper rule
|
||||||
|
|
||||||
|
- path: similarity/compute.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
import duckdb
|
||||||
|
except Exception:
|
||||||
|
logger.exception("duckdb import failed; cannot load vectors")
|
||||||
|
return 0
|
||||||
|
```
|
||||||
|
note: defensive import handling and early return on failure
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: Broad except without logging and without re-raising (silently hides bugs)
|
||||||
|
remediation: Narrow exception types or at minimum log.exception() and re-raise or convert to a domain error if truly handled.
|
||||||
|
example: |
|
||||||
|
```python
|
||||||
|
try:
|
||||||
|
do_work()
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
# BAD: hides the root cause and returns an ambiguous default
|
||||||
|
```
|
||||||
|
- Bad: Mixing print() and logging for errors
|
||||||
|
remediation: Replace print() calls with logger.* calls; use structured logging configuration.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
name: module_singletons
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Module-level singletons (e.g., db = MotionDatabase()) are acceptable but should be created carefully:
|
||||||
|
- Avoid expensive initialization at import time.
|
||||||
|
- Provide a way to construct with a test DB path or to reinitialize in tests.
|
||||||
|
- If a singleton holds resources (DB connections, sessions), ensure safe shutdown on program exit.
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: database.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
class MotionDatabase:
|
||||||
|
def __init__(self, db_path: str = config.DATABASE_PATH):
|
||||||
|
self.db_path = db_path
|
||||||
|
# If duckdb is not available, operate in lightweight file-backed mode
|
||||||
|
self._file_mode = duckdb is None
|
||||||
|
self._init_database()
|
||||||
|
```
|
||||||
|
note: class is safe to instantiate and creates DB at init; consider lazy init if heavy
|
||||||
|
|
||||||
|
- path: similarity/lookup.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
db = MotionDatabase(db_path=db_path) if db_path else MotionDatabase()
|
||||||
|
if hasattr(db, "get_cached_similarities"):
|
||||||
|
rows = db.get_cached_similarities(...)
|
||||||
|
```
|
||||||
|
note: consumers create local MotionDatabase instances, not relying on a single global
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: Creating connections and performing heavy schema migrations during import
|
||||||
|
remediation: Move heavy init to an explicit initialize() method and keep import fast.
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
name: requests_http
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Reuse requests.Session when making multiple calls to the same host to benefit from connection pooling.
|
||||||
|
- Wrap outbound HTTP calls with retry/backoff logic and respect Retry-After on 429.
|
||||||
|
- Treat 5xx as transient and retry; surface 4xx as configuration/client errors (do not retry unless 429).
|
||||||
|
- Raise or wrap non-OK responses into domain ProviderError to make behavior consistent across the codebase.
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: ai_provider.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
resp = requests.post(url, json=json, headers=headers, timeout=10)
|
||||||
|
...
|
||||||
|
if getattr(resp, "status_code", 0) == 429:
|
||||||
|
if attempt == retries:
|
||||||
|
raise ProviderError(f"Provider returned HTTP {resp.status_code}")
|
||||||
|
retry_after = None
|
||||||
|
raw = resp.headers.get("Retry-After") if getattr(resp, "headers", None) else None
|
||||||
|
if raw:
|
||||||
|
try:
|
||||||
|
retry_after = int(raw)
|
||||||
|
except Exception:
|
||||||
|
...
|
||||||
|
if retry_after is not None:
|
||||||
|
time.sleep(retry_after)
|
||||||
|
continue
|
||||||
|
```
|
||||||
|
note: explicit handling of 429 and Retry-After
|
||||||
|
|
||||||
|
- path: api_client.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
response = self.session.get(
|
||||||
|
base_url, params=params, timeout=config.API_TIMEOUT
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
```
|
||||||
|
note: uses session + raise_for_status() to surface HTTP errors
|
||||||
|
|
||||||
|
- path: pipeline/ai_provider_wrapper.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
def _attempt_batch(chunk_texts, start_index):
|
||||||
|
backoff = 0.5
|
||||||
|
for attempt in range(1, retries + 1):
|
||||||
|
try:
|
||||||
|
emb_chunk = _embedder(
|
||||||
|
chunk_texts, model=model, batch_size=len(chunk_texts)
|
||||||
|
)
|
||||||
|
return emb_chunk, None
|
||||||
|
except Exception as exc:
|
||||||
|
if attempt == retries:
|
||||||
|
break
|
||||||
|
sleep = backoff * (2 ** (attempt - 1))
|
||||||
|
time.sleep(sleep)
|
||||||
|
continue
|
||||||
|
```
|
||||||
|
note: wrapper adds retry/backoff and per-item fallback
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: Blindly catching all requests exceptions and returning empty response
|
||||||
|
remediation: map network exceptions to retryable vs terminal (ProviderError) and log details.
|
||||||
|
- Bad: Using print() for network errors instead of structured logging (see api_client.py where print() is used; prefer logging).
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
name: validation
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- Validate inputs early and raise ValueError or domain-specific exceptions (ProviderError) for invalid contract inputs.
|
||||||
|
- Tests should assert that invalid inputs raise the expected exceptions.
|
||||||
|
- Use explicit checks for types and shapes on public APIs (e.g., ensure text is str before embedding).
|
||||||
|
|
||||||
|
examples:
|
||||||
|
- path: ai_provider.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
if not isinstance(text, str):
|
||||||
|
raise ProviderError("text must be a string")
|
||||||
|
```
|
||||||
|
note: explicit type validation before network call
|
||||||
|
|
||||||
|
- path: pipeline/ai_provider_wrapper.py
|
||||||
|
excerpt: |
|
||||||
|
```python
|
||||||
|
if not texts:
|
||||||
|
return []
|
||||||
|
if motion_ids is None:
|
||||||
|
motion_ids = [None for _ in texts]
|
||||||
|
```
|
||||||
|
note: defensive handling of empty inputs
|
||||||
|
|
||||||
|
anti_patterns:
|
||||||
|
- Bad: Allowing invalid values to propagate into heavy computation (e.g., non-string into embedding pipeline).
|
||||||
|
remediation: Fail fast with a typed exception and add unit tests to cover validations.
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Tech stack (Phase 1 authoritative)
|
||||||
|
|
||||||
|
language:
|
||||||
|
name: python
|
||||||
|
version: ">=3.13"
|
||||||
|
|
||||||
|
frameworks:
|
||||||
|
- streamlit: ">=1.48.0" # UI: Home.py, pages/..., app.py
|
||||||
|
|
||||||
|
database:
|
||||||
|
primary: duckdb
|
||||||
|
orm_or_adapter: ibis-framework[duckdb] # used for some parts
|
||||||
|
|
||||||
|
visualization:
|
||||||
|
- plotly
|
||||||
|
|
||||||
|
ml:
|
||||||
|
- scikit-learn
|
||||||
|
- scipy
|
||||||
|
- umap-learn
|
||||||
|
|
||||||
|
ai:
|
||||||
|
declared_dependency: openai # declared in pyproject but not observed imported; ai_provider uses requests
|
||||||
|
runtime_adapter: custom requests-based wrapper (ai_provider.py)
|
||||||
|
|
||||||
|
container:
|
||||||
|
- docker: Dockerfile FROM python:3.13-slim, EXPOSE 8501, CMD streamlit run Home.py
|
||||||
|
|
||||||
|
testing:
|
||||||
|
- pytest
|
||||||
|
|
||||||
|
ci:
|
||||||
|
- drone: .drone.yml present
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# System overview
|
||||||
|
|
||||||
|
This project is a Streamlit-based UI and data-processing pipeline that computes embeddings,
|
||||||
|
performs SVD over MP/motion voting matrices, fuses vector representations, and precomputes
|
||||||
|
a similarity cache for quick lookup in the UI.
|
||||||
|
|
||||||
|
Key subsystems:
|
||||||
|
- UI: Streamlit pages (Home.py, pages/*). Exposes interactive explorer and quizzes.
|
||||||
|
- Data ingestion: scripts and scraper/api_client.py (Tweede Kamer OData).
|
||||||
|
- Processing pipelines: pipeline/* (text embeddings, SVD, fusion).
|
||||||
|
- Similarity layer: similarity/compute.py and similarity/lookup.py storing precomputed neighbors.
|
||||||
|
- Storage: DuckDB (primary), with a JSON-file fallback used in tests/environments without duckdb.
|
||||||
|
- AI/Embedding provider: ai_provider.py (HTTP wrapper around an OpenRouter/OpenAI-compatible API).
|
||||||
|
|
||||||
|
Operational notes:
|
||||||
|
- Dockerfile exists; Streamlit default port 8501 exposed.
|
||||||
|
- Tests use pytest. CI uses Drone (.drone.yml).
|
||||||
|
- There is no lockfile present in the repository snapshot; add one (poetry.lock or requirements.txt) for reproducible installs.
|
||||||
Reference in New Issue
Block a user