Rules and conventions to follow when writing code in this project. Clarity and readability are prioritized over clever or overly declarative code.
Balance Elixir's declarative capabilities with readable, clean code. If a pipe chain or pattern match makes the code harder to follow, use a simpler approach. Three clear lines are better than one cryptic expression.
Follow this order within every module:
defmodule MyModule do
use GenServer # 1. use/require/import
require Logger
alias Simulator.SomeModule
@some_attribute 30 # 2. Module attributes
@moduledoc """...""" # 3. @moduledoc
# Public API ------- # 4. Public functions with @doc
@doc """..."""
def start_link(opts) do
end
# Callbacks ------- # 5. GenServer/@impl callbacks
@impl true
def init(state) do
end
# Private --------- # 6. Private functions
defp helper do
end
endUse comment separators (# Section Name -----) to visually divide sections in larger modules.
- Every public function gets
@docwith a clear description - Every module gets
@moduledocexplaining its purpose and role - Use
@doc falsefor internal public functions - Include
## Examplesin@docwhen behavior isn't obvious - Keep comments minimal — only for non-obvious logic. The code should be self-explanatory
- Variables: descriptive snake_case (
agent_pid,new_position,structures_json) - Unused params: explicit underscore prefix (
_from,_opts,_map) - Temporary/transformed: prefix with
new_(new_state,new_executions) - Module attributes: descriptive snake_case (
@update_interval,@tick_interval) - No abbreviations unless universally understood (
pid,opts)
Use pipes for sequential data transformations. Do not overuse — single operations stay inline.
# Good: pipe for sequential transformation
%Simulation{}
|> Simulation.changeset(attrs)
|> Repo.insert()
# Good: pipe for building state
socket
|> assign(:simulation, simulation)
# Good: inline for single operation
def get_position(pid), do: Agent.get(pid, fn state -> state.position end)
# Bad: unnecessary pipe for one call
pid
|> PointAgent.get_position()Prefer pattern matching in function heads over body matching when possible:
# Good: function head
def compute_step(%{position: position, map: map} = state) do
...
end
# Good: function head with struct
def delete_simulation(%Simulation{} = simulation) do
...
end
# Good: channel join with string pattern
def join("simulation:" <> id, _params, socket) do
...
endUse case for branching on return values, cond for multiple conditions, with/else for chained operations that can fail:
# case for pattern matching results
case Map.fetch(state.executions, simulation.id) do
{:ok, _pid} -> {:reply, :already_running, state}
:error -> ...
end
# cond for multi-way branching
cond do
value < min -> min
value > max -> max
true -> value
end
# with/else for chained fallible operations
with {:ok, pid} <- SimulationExecutor.start_link(%{simulation: simulation}) do
...
else
{:error, reason} -> ...
end- Atom keys for internal state:
%{position: %{x: 0, y: 0}, algorithm: module} - String keys only for external data (registries, user input):
%{"random_walk" => RandomWalk} - Struct updates with
%{state | key: value}syntax Map.put/3when adding new keys to a plain map- Access struct fields with dot notation, never bracket syntax
- Keep functions short (5-15 lines). Extract helpers when a function grows beyond that
- Use
forcomprehensions for list generation,Enumfunctions for transformation - Mark unused parameters explicitly with
_
- Public API functions at the top, callbacks below
- Always use
@impl trueon callbacks - Use
Loggerfor production logging,IO.inspectonly for temporary debugging (withlabel:) - Register with meaningful names via
name:option
- Use
with/elsefor operations that can fail in sequence - Let processes crash on unexpected errors (OTP supervision handles recovery)
- Log errors with
Logger.error/1before returning error tuples - Never silently swallow errors
- Lists do not support index-based access — use
Enum.at/2, pattern matching, orList - Variables are immutable but can be rebound — always bind
if/case/condresults to variables - Never nest multiple modules in the same file
- Don't use
String.to_atom/1on user input (memory leak risk) - Predicate function names should end in
?, not start withis_ - Elixir does NOT support
if/else iforif/elsif— usecondorcase
- Router
scopeblocks include an optional alias prefixed to all routes — be mindful to avoid duplicate module prefixes Phoenix.Viewis not included — don't use it- Always use
~Hor.html.heextemplates, never~E - Use
Phoenix.Component.to_form/2for forms, never pass changesets directly to templates - Use the imported
<.input>component for form inputs - Use
<.icon name="hero-...">for icons (fromcore_components.ex) - Never write inline
<script>tags — all JS goes throughassets/js/app.js <.flash_group>must only be called inside thelayouts.exmodule
- DB uses SQLite via
ecto_sqlite3 - Always preload associations in queries when they'll be accessed in templates
- Remember
import Ecto.Querywhen writing queries or seeds Ecto.Changeset.validate_number/2does not support:allow_nil- Schema
:textcolumns use:stringtype - Use
Ecto.Changeset.get_field/2to access changeset fields - Fields set programmatically must not be in
castcalls - Never commit secrets or credentials to the repository
- Tailwind CSS v4 — no
tailwind.config.js, uses@import "tailwindcss"syntax inapp.css - Never use
@apply - DaisyUI is available but prefer Tailwind classes when practical
- Only
app.jsandapp.cssbundles are supported — import vendor deps into those files - No external script
srcor linkhrefin layouts - Never write inline
<script>tags in templates - JS: use
constby default,letonly when mutation is needed - JS: named functions for exports, arrow functions for callbacks
- JS: always clean up connections/state before re-initializing
- Use
{...}for interpolation in tag attributes and tag bodies - Use
<%= ... %>only for block constructs (if,cond,case,for) inside tag bodies - HEEx comments:
<%!-- comment --%> - Class attributes support lists:
class={["px-2", @flag && "py-5"]} - Wrap
ifinside class lists with parens:if(@cond, do: "a", else: "b") - Use
<%= for item <- @collection do %>for iteration, never<% Enum.each %> - Use
phx-no-curly-interpolationon tags that contain literal{/}in text content
- Architecture.md — System architecture, process tree, data flows, design decisions
- ALGORITHMS.md — How to implement and register movement algorithms
- MAPS.md — How to implement and register simulation maps
mix setup # Install deps, create DB, build assets
mix phx.server # Start dev server (port 4000)
mix test # Run all tests
mix test test/path.exs # Run single test file
mix test --failed # Re-run only previously failed tests
mix precommit # Lint (warnings as errors) + tests — run before committing
mix format # Format codeAlways run mix precommit before committing changes.