Architecture Decision Records, packaged as agent context. ADRs contain independently citable Rules with Correct/Wrong/Why examples, or a complete narrative decision whose sections need to be read together. Written for engineers and shaped for LLMs to consume: the format reads cleanly as a reference and drops into Cursor, Claude Code, Aider, and custom retrieval-based agents so the guidance loads automatically when you write or review code.
The rules are grouped into domains. Each domain is a self-contained set with its own pre-rendered bundles under dist/<domain>/; adopt one or several.
| Domain | Covers | ADRs |
|---|---|---|
software-design |
Language-agnostic module boundaries, information hiding, cohesion, coupling, and interface costs | 1 |
elixir-otp |
OTP, GenServer, supervision, BEAM scheduling, interprocess data, message routing, stateful-process testing | 13 |
elixir-conventions |
Structural dispatch, runtime algorithm selection, pipeline composition, structs, validation-primitive selection, with chains, streaming, effect compensation, structured errors |
9 |
elixir-ecto |
Transaction boundaries and the commit contract | 1 |
elixir-resilience |
Calls to external services under failure: upstream rate limits, shared backpressure, coordinated recovery | 1 |
elixir-code-anti-patterns |
The official Elixir code anti-patterns | 10 |
elixir-design-anti-patterns |
The official Elixir design anti-patterns | 6 |
elixir-macro-anti-patterns |
The official Elixir meta-programming anti-patterns | 5 |
How to organize responsibilities so a change can remain local: identify the knowledge a module owns, preserve its invariants, and compare the benefit of a boundary with the understanding and coordination its interface requires. The decision applies across languages and includes cases where keeping related code together produces the better design.
Working with GenServer and the BEAM rewards a precise mental model of how they actually behave. The wrong model produces the same predictable production failures: a callback blocks the processing loop, a process hoards state and pays for it in GC pauses, a cast pipeline grows an unbounded mailbox, terminate/2 cleanup is silently skipped on a brutal kill, and a shared PubSub topic makes message volume quadratic while every receiver looks like it is filtering. Humans build the wrong model. LLMs build the wrong model, often more confidently.
- ADR-001: Reach for Simpler Primitives Before GenServer
- ADR-002: Own State in the Process; Separate Transitions From Server Mechanics
- ADR-003: Keep GenServer State Small; Push Storage Out of Process
- ADR-004: Never Block the GenServer Processing Loop
- ADR-005: Get Slow Work Off the Processing Loop
- ADR-006: Use GenStage for Producer-Consumer Pipelines
- ADR-007: Design GenServers for Test Isolation
- ADR-008: Graceful Shutdown Requires trap_exit and a Realistic :shutdown
- ADR-009: Send Minimal Data Between Processes
- ADR-010: Supervise Every Long-Lived Process
- ADR-011: Test OTP Code Through Real Processes
- ADR-012: The Shape of GenServer State
- ADR-013: Scope PubSub Topics to the Entity, Not the Event Type
These are the decisions a reader makes constantly and rarely writes down: which construct expresses
a branch, whether an algorithm is chosen by the value or by the wiring, when a pipeline beats a
rebinding, what a domain entity is made of, which primitive validates a value on the way in, what
may appear inside a with, and what a failure looks like once it leaves the function that produced
it. Each rule names the compiler or runtime mechanism that separates the correct form from the
plausible one.
- ADR-001: Structural Dispatch Over Imperative Branching
- ADR-002: The Strategy Pattern in Elixir
- ADR-003: Compose with Pipes, Not Named Intermediates
- ADR-004: Use Structs for Domain Entities
- ADR-005: Keep
withChains Pure - ADR-006: Stream Pass-Through Data, Source to Sink
- ADR-007: Compensate Completed Effects in Fallible Chains
- ADR-008: Represent Domain Errors as a Structured Value
- ADR-009: Choose the Validation Primitive by the Value's Origin
Database access decisions. Currently one: the transaction wrapper whose commit is decided by inspecting what your callback returned, rather than by whether it happened to raise.
What happens at the boundary when the other side pushes back. HTTP clients model retry as a property of a request, but a rate limit applies to a provider-defined quota bucket. Callers in that bucket should share a known cooldown without stopping unrelated traffic.
The code-related anti-patterns from the official Elixir documentation, each codified as one rule: the mistake as the Wrong example, the refactoring as the Correct example, and the mechanism in the Why.
- ADR-001: Comments overuse
- ADR-002: Complex
elseclauses inwith - ADR-003: Complex extractions in clauses
- ADR-004: Dynamic atom creation
- ADR-005: Long parameter list
- ADR-006: Namespace trespassing
- ADR-007: Non-assertive map access
- ADR-008: Non-assertive pattern matching
- ADR-009: Non-assertive truthiness
- ADR-010: Structs with 32 fields or more
The design-related anti-patterns from the official Elixir documentation: return-type consistency, boolean and primitive obsession, exceptions for control flow, unrelated multi-clause functions, and library configuration.
- ADR-001: Alternative return types
- ADR-002: Boolean obsession
- ADR-003: Exceptions for control-flow
- ADR-004: Primitive obsession
- ADR-005: Unrelated multi-clause function
- ADR-006: Using application configuration for libraries
The meta-programming anti-patterns from the official Elixir documentation: compile-time dependencies (tracked and untracked), code-generation size, unnecessary macros, and use versus import.
- ADR-001: Compile-time dependencies
- ADR-002: Large code generation
- ADR-003: Unnecessary macros
- ADR-004:
useinstead ofimport - ADR-005: Untracked compile-time dependencies
The anti-pattern domains codify the patterns documented at https://hexdocs.pm/elixir/what-anti-patterns.html. The process anti-patterns are not a separate domain because
elixir-otpalready covers that ground: Code organization by process and Scattered process interfaces fall under ADR-001 and ADR-002, and Sending unnecessary data and Unsupervised processes are ADR-009 and ADR-010. Nothing is restated across domains.
Pre-rendered bundles live in dist/<domain>/, one set per domain. Pick your domain (the examples below use elixir-otp; substitute any domain from the table above) and your harness.
# from your project root
mkdir -p .cursor/rules
cp /path/to/adrs/dist/elixir-otp/cursor/*.mdc .cursor/rules/Each rule's behavior is set by its frontmatter:
- Rules with
globsauto-attach when you edit a matching file ("Apply to Specific Files"). - Rules without
globsare pulled in by the agent when theirdescriptionis relevant ("Apply Intelligently").
Reference: https://cursor.com/docs/context/rules
# from your project root
cp /path/to/adrs/dist/elixir-otp/claude-code/CLAUDE.md ./CLAUDE.md
mkdir -p .claude/rules
cp /path/to/adrs/dist/elixir-otp/claude-code/.claude/rules/*.md .claude/rules/If your project already has a CLAUDE.md, append the contents of the rendered one rather than overwriting. The per-file copy of the rules avoids touching any existing .claude/agents/, .claude/settings.json, or unrelated rule files.
Claude Code reads CLAUDE.md at the start of every session and picks up files in .claude/rules/ automatically. Rules with paths: frontmatter auto-attach when Claude reads matching files; rules without paths load into every session unconditionally.
Reference: https://code.claude.com/docs/en/memory
aider --read /path/to/adrs/dist/elixir-otp/bundle.mdAider has no auto-discovery; you always pass the file via --read. To avoid retyping the path on every invocation, put it in .aider.conf.yml:
# .aider.conf.yml
read:
- /path/to/adrs/dist/elixir-otp/bundle.mdbundle.md is the domain's ADRs concatenated into one file. It pays full token cost on every turn and gets cached if prompt caching is enabled. Use the harness-specific bundles above when you can; fall back to this when you can't.
References: https://aider.chat/docs/usage/conventions.html, https://aider.chat/docs/config/aider_conf.html
New retrievers should consume dist/<domain>/retrieval.jsonl. It is a model-neutral, versioned package of four record kinds:
adr_summaryroutes broad architectural questions and carries the ADR's Context, direct Decision prose, and Consequences.ruleis the minimum answer unit. Itsretrieval_textcontains the prescription and Why; itsdisplay_textpreserves the complete Rule, including examples.examplemakes Correct and Wrong code independently searchable while retaining explicit positive or negative polarity. Itshydrate_idpoints back to the complete Rule.supportingpreserves useful Decision subsections that are not numbered Rules.
The v1 Rule authoring grammar has exactly one Correct, one Wrong, and one Why region, in that order. A Correct or Wrong region may contain multiple fenced snippets, prose between those fences, and deeper Markdown headings; those pieces remain one example region. Repeating a Correct, Wrong, or Why label is rejected, and the generated Correct and Wrong record IDs therefore end in the per-polarity index :01.
A registry entry can explicitly declare structure: narrative when the decision's
sections need to travel together. The default remains structure: rules with
the grammar above. A narrative ADR produces an adr_summary and one supporting
record, <domain>:adr-NNN:supporting:decision, containing the complete Decision
through any review criteria before Consequences. Its examples remain inside that
record; they are not assigned standalone Correct or Wrong polarity. The supporting
record hydrates to itself and links to its ADR summary for Context. Narrative mode
rejects numbered Rule headings and Correct/Wrong/Why labels to prevent accidental
mixing of the two structures. Both modes use the existing retrieval-v1 schema.
The v1 contract is published as schema/retrieval-v1.schema.json, and dist/retrieval-catalog.json inventories every domain artifact with counts and SHA-256 checksums. Stable semantic record_id values support deterministic lookup for explicit citations such as elixir-otp ADR-005 Rule 2; content hashes report change, but are never identifiers. Future incompatible retrieval changes require a new schema and artifact name or an explicit migration; they must not silently redefine retrieval-v1 semantics.
For every record, source_start_line and source_end_line are the inclusive provenance envelope containing the source regions that contributed to display_text. Rule, example, and supporting display_text each draw from one contiguous source region; an adr_summary combines Context, direct Decision prose, and Consequences, so its contributing regions may be discontiguous and its envelope may contain intervening Rule text absent from display_text. retrieval_text may add routing metadata, parent Context, a Rule statement, or Why text outside that display envelope; the source line fields do not claim to bound those search-only enrichments. source_sha256 is the SHA-256 of the exact UTF-8 bytes of display_text, not a hash of every source line in that envelope. retrieval_sha256 likewise hashes the exact UTF-8 bytes of retrieval_text.
A recommended retrieval flow is:
- Resolve explicit ADR or Rule citations by
record_idbefore similarity search. - Embed
retrieval_textfor semantic search. - Build a lexical/BM25 index over both
retrieval_textanddisplay_text, then fuse semantic and lexical rankings, preferably with reciprocal rank fusion (RRF). - Filter or boost by accepted
status, an explicitly requesteddomain,tags, andapplies_to. - Collapse matches by
hydrate_idso several example hits do not crowd out other Rules. - Return the complete Rule and load its ADR parent's Context.
Never present a negative-polarity example as standalone guidance: hydrate it to its Rule so the prescription and Why travel with the anti-pattern. With only a few hundred records, exhaustive cosine or dot-product search is sufficient. Approximate-nearest-neighbor indexes, rerankers, embedding providers, and vector databases remain downstream choices.
The evaluation judgments under eval/ exercise terminology, paraphrases, code anti-patterns, hard negatives, cross-domain questions, citation routing, and abstention. tools/score_retrieval.exs scores a retriever's JSONL results without depending on any embedding vendor. Supply one result row per query, in this form:
{"query_id":"q:elixir-otp:adr-005:rule-03:scenario","results":[{"record_id":"elixir-otp:adr-005:rule-03","score":0.9}],"abstained":false}Then run:
elixir tools/score_retrieval.exs --queries eval --results results.jsonl --manifest dist --format jsondist/<domain>/adrs.jsonl remains one ADR per row, with its existing id, domain, title, description, tags, applies_to, and body fields. Existing consumers may continue embedding body; no migration is required. New integrations should prefer retrieval.jsonl because its rule-level hierarchy, polarity, source locations, and hydration links provide more precise results.
The applies_to shape (paths globs and content_match substrings) is the same routing data used to generate the harness-specific bundles, so a custom retriever can match those bundles' behavior by consuming it directly.
The ADRs use vault-conformant frontmatter (type: adr, id, title, status, date, tags, description). ISO date and optional updated values are intentionally quoted so YAML readers retain YYYY-MM-DD strings rather than implicitly converting them to date objects. Clone the repo into your vault's directory and existing semantic-retrieval tooling will index them.
adrs/<domain>/
├── adr-rules.yaml # path/content patterns per ADR
└── adr-NNN-*.md # one ADR per file
dist/
├── retrieval-catalog.json # domain inventory, counts, and checksums
└── <domain>/ # generated; do not edit by hand
├── cursor/ # Cursor .mdc rules
├── claude-code/ # CLAUDE.md + .claude/rules/
├── retrieval.jsonl # hierarchical retrieval-v1 records
├── adrs.jsonl # legacy one-ADR-per-row interface
└── bundle.md # concatenated
schema/
└── retrieval-v1.schema.json
eval/
├── <domain>/queries.jsonl # domain relevance judgments
└── cross-domain/queries.jsonl
tools/
├── build_dist.exs # regenerates and validates dist/
└── score_retrieval.exs # scores vendor-neutral result JSONL
Run elixir tools/build_dist.exs after editing the source ADRs or manifests. Requires Elixir 1.17 or later (uses Mix.install). The build parses and validates the complete corpus before replacing dist/, resolves ADR references across domains, validates generated retrieval rows, and emits deterministic artifacts. CI runs the tooling tests and blocks PRs when dist/ is out of sync with adrs/.
MIT.