From e76e3406715380f7982b2935243f289af69cf5b3 Mon Sep 17 00:00:00 2001 From: Claudio Ortolina Date: Tue, 17 Feb 2026 10:29:35 +0000 Subject: [PATCH] Add project conventions to AGENTS.md --- AGENTS.md | 6 +++- project-conventions.md | 82 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 project-conventions.md diff --git a/AGENTS.md b/AGENTS.md index 5dace759..976e1f8c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,10 @@ # Project architecture -[Project architecture](architecture.md) +@architecture.md + +# Project conventions + +@project-conventions.md diff --git a/project-conventions.md b/project-conventions.md new file mode 100644 index 00000000..0c0aec2a --- /dev/null +++ b/project-conventions.md @@ -0,0 +1,82 @@ +# Project Conventions + +Rules extracted from commit history that are specific to this project and not already covered by `CLAUDE.md` usage rules. + +## Commit Messages + +- Imperative present tense, single-line, under 60 characters +- Describe intent/behavior, not implementation details +- Reverts use `Revert "Original message"` convention + +## Architecture + +- **Context modules own all queries.** LiveViews never query the database directly -- they call context functions. +- **Schemas hold pure accessor/helper functions** on the struct (e.g., `RecordSet.count_by_status/1`). No side effects in schemas. +- **LiveView standard structure:** `mount/3` sets `@current_section`, `handle_params/3` loads data and sets `@page_title` (via pattern-matched private `page_title/2`), `handle_info/2` receives LiveComponent messages. +- **LiveComponents communicate with parent** via `send(self(), {__MODULE__, msg})`. +- **Function components go in domain-specific modules:** `CoreComponents` for generic UI, `RecordComponents` for records, `ScrobbleComponents` for scrobbles, `SearchComponents` for search. +- **External API integrations** follow a three-module pattern: Facade (public API), API (Req HTTP client), Config (NimbleOptions). +- **Oban workers are thin wrappers** that delegate to context modules. `perform/1` should be minimal. +- **Shared utilities** live at parent namespace level (`MusicLibrary.Batch`, not `MusicLibrary.Records.Batch`). + +## Extraction / Refactoring + +- Extract when duplicated 3+ times. Identical template markup in 3+ places becomes a function component. +- Delete thin wrapper modules with a single caller -- inline them instead. +- Parameterize the differences when extracting shared logic. +- Private helpers go at module bottom, public functions first. + +## Template / UI + +- **Gettext wraps ALL user-facing strings.** Every commit that adds UI text must also update `.pot`/`.po` files. +- **`@current_section`** assign set in `mount/3` for nav highlighting. +- **`@page_title`** set in `handle_params/3` via a pattern-matched `page_title/2` helper. +- **Dark mode always paired:** `text-zinc-900 dark:text-zinc-100`, `bg-zinc-50 dark:bg-zinc-800`. +- **Wishlisted items get dimmed styling:** `opacity-60 hover:opacity-100 transition-opacity`. + +## Routes / Navigation + +- **Three routes per resource with show modals:** `:show`, `:edit` (at `/show/edit`), `:add_*` (at `/show/add-*`). +- **Modals close via `JS.patch`** back to the base route. +- **Search state in URL query params** via `push_patch`. +- **Filter empty params from URLs:** `Enum.filter(fn {_, v} -> v not in ["", nil] end)`. +- **Conditional links based on `purchased_at`:** determines `/collection/` vs `/wishlist/` paths. + +## Database + +- **Binary UUIDs everywhere:** `@primary_key {:id, :binary_id, autogenerate: true}`, `@foreign_key_type :binary_id`. +- **SQLite JSON patterns:** `json_each()` and `json_extract()` via `fragment` for JSON column queries. Expression-based indexes on `json_extract` for performance. +- **Materialized views via triggers** (SQLite lacks native materialized views). Use explicit `up`/`down` in migrations for non-reversible DDL. +- **Read-only schemas** for materialized/view tables: `@primary_key false`, no changeset functions, no timestamps. +- **Every `execute` provides both up and down SQL.** Every index has a comment explaining which query it helps. +- **Config-driven constants.** Pagination defaults and similar magic numbers live in `config/config.exs`, read via `Application.compile_env!/2` into module attributes. + +## Error Handling + +- **Toast notifications:** `put_toast/3` (arity 3) in LiveViews, `put_toast!/2` (arity 2) in LiveComponents. `:info` for success, `:error` for failures. +- **`handle_async` always handles three cases:** `{:ok, {:ok, result}}`, `{:ok, {:error, reason}}`, and `{:exit, reason}`. +- **Data cascade on upstream changes:** When artist metadata changes, regenerate dependent record embeddings. + +## Testing + +- **`DataCase`** for context tests, **`ConnCase`** for controller/LiveView tests. +- **Authenticated by default.** Use `@tag :logged_out` for public endpoints. +- **`@tag :capture_log`** on tests with expected error log output. +- **`Req.Test.stub/2`** for all HTTP mocking -- no real network calls. +- **Fixture modules** in `test/support/fixtures/` use `System.unique_integer([:positive])` for unique names and call through context functions (not raw `Repo.insert`). +- **Verify outcomes through context modules**, not just UI assertions. Delete tests assert both `refute has_element?` and `assert_raise Ecto.NoResultsError`. +- **PhoenixTest** (`visit/assert_has/click_link`) for declarative page assertions. **`Phoenix.LiveViewTest`** (`live/form/render_click/render_hook`) for interactive behavior. +- **`render_hook/3`** for testing JS hook interactions. + +## Tech Debt / Hygiene + +- **Clean up first, then enforce.** Remove all violations before enabling a lint rule. +- **Reverts are total.** Remove every trace: source, CSS, npm deps, config. +- **Never leak sensitive data in prod.** `show_sensitive_data_on_connection_error: false`. +- **Commits are small and single-purpose.** One logical change per commit. +- **Unused aliases are removed** when their module is no longer referenced. Aliases stay alphabetically sorted. + +## JavaScript + +- **Factory function pattern** for JS hooks when two hooks share logic. +- **Data attributes** (`data-*`) for HTML-to-JS communication. Hooks read `dataset` and `pushEvent` to the server.