separation-of-concerns.mdc
| 1 | --- |
| 2 | description: Enforces strict separation of concerns — no inline CSS, JS, or template strings in Python, tests, or HTML |
| 3 | alwaysApply: true |
| 4 | --- |
| 5 | |
| 6 | # Strict Separation of Concerns |
| 7 | |
| 8 | MuseHub uses a three-layer architecture. Each layer must stay in its own file type. Crossing boundaries is always an anti-pattern. |
| 9 | |
| 10 | | What | Where | Never in | |
| 11 | |------|-------|----------| |
| 12 | | Markup (structure) | Jinja2 `.html` templates | Python, TypeScript | |
| 13 | | Styles | SCSS `_*.scss` partials → compiled `app.css` | `style=""` attributes, `<style>` tags, Python strings | |
| 14 | | Behaviour | TypeScript `static/js/**/*.ts` → compiled `app.js` | `<script>` tags in templates, `onchange=` / `onclick=` attributes, Python strings | |
| 15 | |
| 16 | ## ❌ Anti-patterns — never do these |
| 17 | |
| 18 | ```python |
| 19 | # Inline CSS in Python |
| 20 | ctx["style"] = "color: red; font-size: 14px" |
| 21 | |
| 22 | # HTML/template strings in Python |
| 23 | return HTMLResponse("<div class='card'>...</div>") |
| 24 | |
| 25 | # Script strings in Python |
| 26 | ctx["init_js"] = "const cfg = " + json.dumps(data) |
| 27 | ``` |
| 28 | |
| 29 | ```html |
| 30 | <!-- Inline styles in Jinja2 --> |
| 31 | <div style="color:red;margin:8px">...</div> |
| 32 | |
| 33 | <!-- Inline event handlers in Jinja2 --> |
| 34 | <button onclick="doThing()">click</button> |
| 35 | <select onchange="this.form.submit()"> |
| 36 | |
| 37 | <!-- <script> blocks with business logic in templates --> |
| 38 | <script> |
| 39 | const data = {{ items | tojson }}; |
| 40 | fetch('/api/...').then(r => r.json()).then(renderChart); |
| 41 | </script> |
| 42 | ``` |
| 43 | |
| 44 | ```python |
| 45 | # Tests asserting CSS class names or JS variable names |
| 46 | assert "tab-btn" in response.text # ❌ fragile — class name can change |
| 47 | assert "let sessions" in response.text # ❌ JS moved to .ts module |
| 48 | assert "pr.mergedAt" in response.text # ❌ JS implementation detail |
| 49 | assert "badge-merged" in response.text # ❌ CSS class, not semantic content |
| 50 | ``` |
| 51 | |
| 52 | ## ✅ Correct patterns |
| 53 | |
| 54 | ```html |
| 55 | <!-- Styles: use SCSS class names only --> |
| 56 | <div class="cr-stat-card">...</div> |
| 57 | |
| 58 | <!-- Behaviour: attach in TypeScript via addEventListener --> |
| 59 | <!-- In template: --> |
| 60 | <select id="sort-select" name="sort">...</select> |
| 61 | <!-- In .ts module: --> |
| 62 | document.getElementById('sort-select')?.addEventListener('change', handler); |
| 63 | |
| 64 | <!-- Data from server → client: use a typed JSON block, never inline script --> |
| 65 | <script id="page-data" type="application/json">{{ page_json | tojson }}</script> |
| 66 | ``` |
| 67 | |
| 68 | ```python |
| 69 | # Tests: assert status codes and semantic text content, not CSS or JS strings |
| 70 | assert response.status_code == 200 |
| 71 | assert "gabriel" in response.text # ✅ actual user data |
| 72 | assert "No pull requests" in response.text # ✅ visible UI text |
| 73 | # For behaviour, test the JSON API endpoints instead: |
| 74 | resp = await client.get("/api/v1/repos/{id}/credits") |
| 75 | assert resp.json()["totalContributors"] == 3 |
| 76 | ``` |
| 77 | |
| 78 | ## Allowed exceptions |
| 79 | |
| 80 | - `style=""` **only** for dynamic values that cannot be expressed as SCSS (e.g. `width: {{ pct }}%` for data-driven bars, `background: hsl({{ hue }}, ...)` for deterministic avatar colours). Keep it to a single property; never use it for layout or typography. |
| 81 | - `onchange="this.form.submit()"` on a plain `<select>` inside a `<form method="get">` is acceptable as a progressive-enhancement pattern (no JS dependency). |