An Elixir implementation of AT Protocol-flavoured Merkle Search Trees (MST)

AGENTS.md#

Guidance for agentic coding assistants working in this repository.

Project Overview#

elixir-mst is an Elixir library implementing AT Protocol-flavoured Merkle Search Trees (MST).

Build / Lint / Test Commands#

# Compile
mix compile

# Format (run before committing)
mix format

# Check formatting without writing
mix format --check-formatted

# Lint
mix credo

# Run all tests
mix test

# Run a single test file
mix test test/mst/foo_test.exs

# Run a single test by line number
mix test test/mst/foo_test.exs:42

# Run doctests only
mix test --only doctest

# Generate docs
mix docs

No custom Mix aliases are defined. There is no CI pipeline — validate locally.

Project Structure#

TODO

Code Style#

Module Naming#

  • Domain acronyms are all-caps: MST.
  • Sub-modules follow Parent.Role.
  • Module file path mirrors module name exactly.

Structs#

Use TypedStruct with enforce: true for all structs. Every field must be typed. Use default: only where a sensible zero value exists.

typedstruct enforce: true do
  field :version, pos_integer(), default: 1
  field :roots, list(CID.t()), default: []
  field :blocks, %{CID.t() => binary()}, default: %{}
end

Typespecs#

  • Every public function must have @spec.
  • Every private function should have @spec where non-trivial.
  • Define named error type aliases at the top of each module, then reference them in @spec annotations:
@type header_error() :: {:error, :header, atom()}
@type block_error()  :: {:error, :block, atom()}
@type decode_error() :: header_error() | block_error()

Error Handling#

Consistent tagged-tuple convention — do not deviate:

  • Success: {:ok, value}
  • Simple error: {:error, reason}
  • Scoped error (CAR layer): {:error, :scope, :reason} — e.g. {:error, :header, :missing_roots}, {:error, :block, :cid_mismatch}

Use with chains for multi-step fallible operations; use else to remap errors when needed. Use Enum.reduce_while for fallible iteration — halt on first error.

Bang variants (parse_header!, validate_block!) are only acceptable inside StreamDecoder-style modules where the documented contract is raise-on-error. Do not mix raise and tuple-return styles in the same module without explicit documentation of the contract.

Pattern Matching and Guards#

  • Prefer multi-clause function heads for exhaustive dispatch over nested conditionals.
  • Use bit-syntax binary pattern matching for low-level binary parsing.
  • Pair guards with pattern matches for validation constraints:
when hash_size == @hash_size and byte_size(digest) == @hash_size

Module Attributes for Constants#

Use @ module attributes for all magic numbers and codec identifiers. Group them at the top of the module, after @moduledoc.

@codec_raw   0x55
@codec_drisl 0x71
@hash_sha256 0x12
@hash_size   32

Pipes#

Use pipes where they read naturally. Do not force them. Prefer with over pipes for error-prone chains. The primary pipe use-case is stream pipelines:

chunk_stream
|> StreamDecoder.decode_stream(opts)
|> Stream.map(&transform/1)

Documentation#

  • Every public module must have @moduledoc with a prose description and, where applicable, a Spec: <url> line linking to the relevant spec.
  • Every public function must have @doc with:
    • A prose description. Keep it high-level — do not repeat details already covered by the spec (e.g. byte-level encoding rules, magic constants, algorithm steps).
    • An ## Options section if the function accepts an options keyword list.
    • An ## Examples section with iex> doctests for the happy path and at least one error case.
  • Use dashes (-) for all Markdown lists in @moduledoc and @doc. Never use asterisks (*).

Protocol Implementations#

Implement String.Chars and Inspect for domain structs at the bottom of the file, outside the main module block — see cid.ex for the pattern.

Streaming#

Use Stream.transform/4 with explicit start/reduce/after arities (not the 3-arity shorthand) for stateful streaming parsers.

Section Separators#

Use # ---...--- comment separators (78 dashes) to group related functions visually, consistent with existing source files.

Test Style#

  • All test modules: use ExUnit.Case, async: true.
  • Pull doctests in at the top: doctest MST.ModuleName.
  • Use describe/test blocks — one describe per public function or logical group.
  • Shared fixtures: define as @ module attributes or defp helpers at the top of the test module with a brief comment on their purpose.
  • Assertions use pattern matching: assert {:ok, _} = ..., not {:ok, val} = ...; assert val == ....
  • For stream decoder raise tests: assert_raise RuntimeError, ~r/pattern/, fn -> ... end.
  • Do not couple decoder tests to encoder correctness — construct raw binaries directly in test helpers when testing a decoder in isolation.
  • Test file paths must mirror lib/ paths exactly.

Dependencies#

Dep Purpose
:dasl DASL primitives (CID, DRISL, CAR)
:typedstruct Typed struct DSL
:ex_doc Doc generation (dev only)
:credo Static analysis (dev + test)

No Dialyzer setup. No property-based testing. Do not add new dependencies without discussion — the dep surface is intentionally minimal.

Formatter#

.formatter.exs imports :typedstruct so typedstruct do ... end blocks format correctly. Default line length (98) applies. Always run mix format before committing.