# Guidelines for Pyex Development

## IMPORTANT: Environment setup (fresh sandbox)

If `mix` is not on `PATH` — a fresh claude.ai sandbox, CI runner, or
new container — install Elixir **before anything else**. Do NOT use
`apt` (ships Elixir 1.14 / OTP 25) and do NOT use asdf's erlang plugin
(compiles OTP from source — minutes on a single core). Install
precompiled binaries from builds.hex.pm. On Ubuntu 24.04 / amd64:

```bash
OTP=28.5.0.1 EX=1.19.5-otp-28 ROOT="$HOME/beam"   # bump patch versions as needed
mkdir -p "$ROOT"/{otp,elixir} && cd "$ROOT"
curl -fsSL -o otp.tar.gz "https://builds.hex.pm/builds/otp/amd64/ubuntu-24.04/OTP-${OTP}.tar.gz"
curl -fsSL -o elixir.zip "https://builds.hex.pm/builds/elixir/v${EX}.zip"
tar xf otp.tar.gz -C otp --strip-components=1
unzip -q elixir.zip -d elixir
( cd otp && ./Install -minimal "$(pwd)" >/dev/null )   # bakes the abs path into the release
```

Then set these in **every** shell (each Bash call is a fresh shell —
exports do not persist; re-apply them or write them to an env file you
`source`):

```bash
export PATH="$HOME/beam/otp/bin:$HOME/beam/elixir/bin:$PATH"
export ELIXIR_ERL_OPTIONS="+fnu"                            # precompiled OTP defaults to latin1; silences the locale warning
export HEX_CACERTS_PATH=/etc/ssl/certs/ca-certificates.crt  # precompiled OTP can't find the OS CA store; without this, Hex TLS fails with unknown_ca
```

Finally `mix local.hex --force && mix local.rebar --force`, then
`mix deps.get && mix compile`. Use `MIX_ENV=prod` to skip dev/test
deps (ex_doc, dialyxir, benchee, bandit) when you only need to run
code, not test it. Full prose lives in README `## Development`.

## IMPORTANT: Code Requirement

**Before committing OR opening a PR, run all four:**

1. `mix format --check-formatted`
2. `mix compile --warnings-as-errors`
3. `mix test`
4. `mix dialyzer` — required for any change to `lib/`

CI runs Dialyzer; skipping it locally means a PR comes back red. The
PLT takes ~40s on a cold build, then a few seconds per run. If the
PLT is missing, build it once with `mix dialyzer --plt`.

When Dialyzer reports findings *only* in pre-existing files you did
not touch (e.g. OTP-version-specific opaqueness warnings), call them
out in the PR body and confirm they were also present on `main` —
don't silently fix them.

## Project Overview

Pyex is a Python interpreter written in Elixir. Follow idiomatic Elixir conventions throughout.

## General Guidelines

- Use snake_case for variables/functions, PascalCase for modules
- **Acronyms stay uppercase in identifiers:** `HTML` not `Html`, `JSON`
  not `Json`, `JSX` not `Jsx`, `URL` not `Url`, `API` not `Api`,
  `HTTP` not `Http`, `CSS` not `Css`, `SQL` not `Sql`, `UUID` not
  `Uuid`, `YAML` not `Yaml`, `CSV` not `Csv`. This applies to module
  names (`Pyex.Stdlib.JSON`), type names, and any PascalCase
  identifier — never title-case an acronym.
- Return tagged tuples: `{:ok, result}` or `{:error, reason}`
- Use pattern matching over conditional logic when possible
- Use pipe operators (`|>`) for multi-step transformations
- Organize aliases alphabetically
- Use `with` statements for sequential operations that may fail

## Testing

- Run `mix test` or `mix test path/to/test_file.exs:line_number`
- Don't use mocks
- Test the happy path and edge cases

## Workflow

- **ALWAYS run `mix format` before committing any code changes**
- **ALWAYS run `mix compile --warnings-as-errors` before committing**
- **ALWAYS run `mix test` before committing**
- **ALWAYS run `mix dialyzer` before pushing or opening a PR** — CI
  fails the build on Dialyzer warnings; running it locally first
  catches dead clauses, impossible patterns, and spec mismatches
  before they cost a CI cycle.
- Run `mix format --check-formatted` to verify all files are properly formatted

## IMPORTANT: After opening a PR, you MUST check CI

Opening a PR is not the end of the task.  After `gh pr create`:

1. Run `gh pr checks <PR>` — every job must pass.
2. For any failing job, fetch the log via `gh run view <run-id> --log-failed`
   and treat the failure as part of the same task — fix it before declaring
   the work done.
3. CI runs Elixir 1.19.5 / OTP 27+28 (per AGENTS.md), which is a DIFFERENT
   version from typical local setups (1.20.x).  Dialyzer warnings, type
   inferences, and unused-filter reports can differ across versions.  In
   particular: `mix dialyzer --list-unused-filters` on local does NOT prove
   an ignore entry is dead on CI.  `.dialyzer_ignore.exs` intentionally
   holds entries for both versions — prune only when you've verified the
   entry is unused on BOTH 1.19 and 1.20.

Do not declare a PR done, hand off to the user, or move on to the next task
until CI is green or all failures are addressed.
