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

Configure Feed

Select the types of activity you want to include in your feed.

Initial commit

+366
+5
.formatter.exs
··· 1 + # Used by "mix format" 2 + [ 3 + inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 + import_deps: [:typedstruct] 5 + ]
+26
.gitignore
··· 1 + # The directory Mix will write compiled artifacts to. 2 + /_build/ 3 + 4 + # If you run "mix test --cover", coverage assets end up here. 5 + /cover/ 6 + 7 + # The directory Mix downloads your dependencies sources to. 8 + /deps/ 9 + 10 + # Where third-party dependencies like ExDoc output generated docs. 11 + /doc/ 12 + 13 + # If the VM crashes, it generates a dump, let's ignore it too. 14 + erl_crash.dump 15 + 16 + # Also ignore archive artifacts (built via "mix archive.build"). 17 + *.ez 18 + 19 + # Ignore package tarball (built via "mix hex.build"). 20 + mst-*.tar 21 + 22 + # Temporary files, for example, from tests. 23 + /tmp/ 24 + 25 + .direnv 26 + .envrc
+193
AGENTS.md
··· 1 + # AGENTS.md 2 + 3 + Guidance for agentic coding assistants working in this repository. 4 + 5 + ## Project Overview 6 + 7 + `elixir-mst` is an Elixir library implementing AT Protocol-flavoured Merkle Search Trees (MST). 8 + 9 + ## Build / Lint / Test Commands 10 + 11 + ```bash 12 + # Compile 13 + mix compile 14 + 15 + # Format (run before committing) 16 + mix format 17 + 18 + # Check formatting without writing 19 + mix format --check-formatted 20 + 21 + # Lint 22 + mix credo 23 + 24 + # Run all tests 25 + mix test 26 + 27 + # Run a single test file 28 + mix test test/mst/foo_test.exs 29 + 30 + # Run a single test by line number 31 + mix test test/mst/foo_test.exs:42 32 + 33 + # Run doctests only 34 + mix test --only doctest 35 + 36 + # Generate docs 37 + mix docs 38 + ``` 39 + 40 + No custom Mix aliases are defined. There is no CI pipeline — validate locally. 41 + 42 + ## Project Structure 43 + 44 + TODO 45 + 46 + ## Code Style 47 + 48 + ### Module Naming 49 + 50 + - Domain acronyms are all-caps: `MST`. 51 + - Sub-modules follow `Parent.Role`. 52 + - Module file path mirrors module name exactly. 53 + 54 + ### Structs 55 + 56 + Use `TypedStruct` with `enforce: true` for all structs. Every field must be 57 + typed. Use `default:` only where a sensible zero value exists. 58 + 59 + ```elixir 60 + typedstruct enforce: true do 61 + field :version, pos_integer(), default: 1 62 + field :roots, list(CID.t()), default: [] 63 + field :blocks, %{CID.t() => binary()}, default: %{} 64 + end 65 + ``` 66 + 67 + ### Typespecs 68 + 69 + - Every public function must have `@spec`. 70 + - Every private function should have `@spec` where non-trivial. 71 + - Define named error type aliases at the top of each module, then reference them 72 + in `@spec` annotations: 73 + 74 + ```elixir 75 + @type header_error() :: {:error, :header, atom()} 76 + @type block_error() :: {:error, :block, atom()} 77 + @type decode_error() :: header_error() | block_error() 78 + ``` 79 + 80 + ### Error Handling 81 + 82 + Consistent tagged-tuple convention — do not deviate: 83 + 84 + - Success: `{:ok, value}` 85 + - Simple error: `{:error, reason}` 86 + - Scoped error (CAR layer): `{:error, :scope, :reason}` — e.g. 87 + `{:error, :header, :missing_roots}`, `{:error, :block, :cid_mismatch}` 88 + 89 + Use `with` chains for multi-step fallible operations; use `else` to remap errors 90 + when needed. Use `Enum.reduce_while` for fallible iteration — halt on first 91 + error. 92 + 93 + Bang variants (`parse_header!`, `validate_block!`) are only acceptable inside 94 + `StreamDecoder`-style modules where the documented contract is raise-on-error. 95 + Do not mix raise and tuple-return styles in the same module without explicit 96 + documentation of the contract. 97 + 98 + ### Pattern Matching and Guards 99 + 100 + - Prefer multi-clause function heads for exhaustive dispatch over nested 101 + conditionals. 102 + - Use bit-syntax binary pattern matching for low-level binary parsing. 103 + - Pair guards with pattern matches for validation constraints: 104 + 105 + ```elixir 106 + when hash_size == @hash_size and byte_size(digest) == @hash_size 107 + ``` 108 + 109 + ### Module Attributes for Constants 110 + 111 + Use `@` module attributes for all magic numbers and codec identifiers. Group 112 + them at the top of the module, after `@moduledoc`. 113 + 114 + ```elixir 115 + @codec_raw 0x55 116 + @codec_drisl 0x71 117 + @hash_sha256 0x12 118 + @hash_size 32 119 + ``` 120 + 121 + ### Pipes 122 + 123 + Use pipes where they read naturally. Do not force them. Prefer `with` over pipes 124 + for error-prone chains. The primary pipe use-case is stream pipelines: 125 + 126 + ```elixir 127 + chunk_stream 128 + |> StreamDecoder.decode_stream(opts) 129 + |> Stream.map(&transform/1) 130 + ``` 131 + 132 + ### Documentation 133 + 134 + - Every public module must have `@moduledoc` with a prose description and, where 135 + applicable, a `Spec: <url>` line linking to the relevant spec. 136 + - Every public function must have `@doc` with: 137 + - A prose description. Keep it high-level — do not repeat details already 138 + covered by the spec (e.g. byte-level encoding rules, magic constants, 139 + algorithm steps). 140 + - An `## Options` section if the function accepts an options keyword list. 141 + - An `## Examples` section with `iex>` doctests for the happy path and at 142 + least one error case. 143 + - Use dashes (`-`) for all Markdown lists in `@moduledoc` and `@doc`. Never use 144 + asterisks (`*`). 145 + 146 + ### Protocol Implementations 147 + 148 + Implement `String.Chars` and `Inspect` for domain structs at the **bottom** of 149 + the file, outside the main module block — see `cid.ex` for the pattern. 150 + 151 + ### Streaming 152 + 153 + Use `Stream.transform/4` with explicit start/reduce/after arities (not the 154 + 3-arity shorthand) for stateful streaming parsers. 155 + 156 + ### Section Separators 157 + 158 + Use `# ---...---` comment separators (78 dashes) to group related functions 159 + visually, consistent with existing source files. 160 + 161 + ## Test Style 162 + 163 + - All test modules: `use ExUnit.Case, async: true`. 164 + - Pull doctests in at the top: `doctest MST.ModuleName`. 165 + - Use `describe/test` blocks — one `describe` per public function or logical 166 + group. 167 + - Shared fixtures: define as `@` module attributes or `defp` helpers at the top 168 + of the test module with a brief comment on their purpose. 169 + - Assertions use pattern matching: `assert {:ok, _} = ...`, not 170 + `{:ok, val} = ...; assert val == ...`. 171 + - For stream decoder raise tests: 172 + `assert_raise RuntimeError, ~r/pattern/, fn -> ... end`. 173 + - Do not couple decoder tests to encoder correctness — construct raw binaries 174 + directly in test helpers when testing a decoder in isolation. 175 + - Test file paths must mirror `lib/` paths exactly. 176 + 177 + ## Dependencies 178 + 179 + | Dep | Purpose | 180 + | -------------- | --------------------------------- | 181 + | `:dasl` | DASL primitives (CID, DRISL, CAR) | 182 + | `:typedstruct` | Typed struct DSL | 183 + | `:ex_doc` | Doc generation (dev only) | 184 + | `:credo` | Static analysis (dev + test) | 185 + 186 + No Dialyzer setup. No property-based testing. Do not add new dependencies 187 + without discussion — the dep surface is intentionally minimal. 188 + 189 + ## Formatter 190 + 191 + `.formatter.exs` imports `:typedstruct` so `typedstruct do ... end` blocks 192 + format correctly. Default line length (98) applies. Always run `mix format` 193 + before committing.
+21
README.md
··· 1 + # MST 2 + 3 + **TODO: Add description** 4 + 5 + ## Installation 6 + 7 + If [available in Hex](https://hex.pm/docs/publish), the package can be installed 8 + by adding `mst` to your list of dependencies in `mix.exs`: 9 + 10 + ```elixir 11 + def deps do 12 + [ 13 + {:mst, "~> 0.1.0"} 14 + ] 15 + end 16 + ``` 17 + 18 + Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc) 19 + and published on [HexDocs](https://hexdocs.pm). Once published, the docs can 20 + be found at <https://hexdocs.pm/mst>. 21 +
+27
flake.lock
··· 1 + { 2 + "nodes": { 3 + "nixpkgs": { 4 + "locked": { 5 + "lastModified": 1775423009, 6 + "narHash": "sha256-vPKLpjhIVWdDrfiUM8atW6YkIggCEKdSAlJPzzhkQlw=", 7 + "owner": "nixos", 8 + "repo": "nixpkgs", 9 + "rev": "68d8aa3d661f0e6bd5862291b5bb263b2a6595c9", 10 + "type": "github" 11 + }, 12 + "original": { 13 + "owner": "nixos", 14 + "ref": "nixos-unstable", 15 + "repo": "nixpkgs", 16 + "type": "github" 17 + } 18 + }, 19 + "root": { 20 + "inputs": { 21 + "nixpkgs": "nixpkgs" 22 + } 23 + } 24 + }, 25 + "root": "root", 26 + "version": 7 27 + }
+21
flake.nix
··· 1 + { 2 + inputs = { 3 + nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable"; 4 + }; 5 + 6 + outputs = {nixpkgs, ...}: let 7 + forSystems = fn: 8 + nixpkgs.lib.genAttrs [ 9 + "aarch64-linux" 10 + "aarch64-darwin" 11 + "x86_64-darwin" 12 + "x86_64-linux" 13 + ] (system: fn nixpkgs.legacyPackages.${system}); 14 + defaultForSystems = fn: forSystems (pkgs: {default = fn pkgs;}); 15 + in { 16 + devShells = defaultForSystems (pkgs: 17 + pkgs.mkShell { 18 + nativeBuildInputs = with pkgs; [elixir erlang]; 19 + }); 20 + }; 21 + }
+18
lib/mst.ex
··· 1 + defmodule MST do 2 + @moduledoc """ 3 + Documentation for `MST`. 4 + """ 5 + 6 + @doc """ 7 + Hello world. 8 + 9 + ## Examples 10 + 11 + iex> MST.hello() 12 + :world 13 + 14 + """ 15 + def hello do 16 + :world 17 + end 18 + end
+30
mix.exs
··· 1 + defmodule MST.MixProject do 2 + use Mix.Project 3 + 4 + def project do 5 + [ 6 + app: :mst, 7 + version: "0.1.0", 8 + elixir: "~> 1.18", 9 + start_permanent: Mix.env() == :prod, 10 + deps: deps() 11 + ] 12 + end 13 + 14 + # Run "mix help compile.app" to learn about applications. 15 + def application do 16 + [ 17 + extra_applications: [:logger] 18 + ] 19 + end 20 + 21 + # Run "mix help deps" to learn about dependencies. 22 + defp deps do 23 + [ 24 + {:dasl, "~> 0.1.0"}, 25 + {:typedstruct, "~> 0.5"}, 26 + {:ex_doc, "~> 0.34", only: :dev, runtime: false}, 27 + {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 28 + ] 29 + end 30 + end
+16
mix.lock
··· 1 + %{ 2 + "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 + "cbor": {:hex, :cbor, "1.0.2", "9b0af85af291a556e10a0ffd48ba9a21a75e711828fafd3af193d56d95f0907f", [:mix], [], "hexpm", "edbc9b4a16eb93a582437b9b249c340a75af03958e338fb43d8c1be9fc65b864"}, 4 + "credo": {:hex, :credo, "1.7.17", "f92b6aa5b26301eaa5a35e4d48ebf5aa1e7094ac00ae38f87086c562caf8a22f", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1eb5645c835f0b6c9b5410f94b5a185057bcf6d62a9c2b476da971cde8749645"}, 5 + "dasl": {:hex, :dasl, "0.1.0", "f72099f1946aa79a0215d6458b69d3e16caf0b876d9ae9c1b0fbd9002dd9239d", [:mix], [{:cbor, "~> 1.0.0", [hex: :cbor, repo: "hexpm", optional: false]}, {:typedstruct, "~> 0.5", [hex: :typedstruct, repo: "hexpm", optional: false]}, {:varint, "~> 1.4", [hex: :varint, repo: "hexpm", optional: false]}], "hexpm", "db733d0bcd217f4e4f9062e51f14f4c956aa87f15765ed1666bb6f9c78be795c"}, 6 + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, 7 + "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, 8 + "file_system": {:hex, :file_system, "1.1.1", "31864f4685b0148f25bd3fbef2b1228457c0c89024ad67f7a81a3ffbc0bbad3a", [:mix], [], "hexpm", "7a15ff97dfe526aeefb090a7a9d3d03aa907e100e262a0f8f7746b78f8f87a5d"}, 9 + "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 10 + "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, 11 + "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, 12 + "makeup_erlang": {:hex, :makeup_erlang, "1.0.3", "4252d5d4098da7415c390e847c814bad3764c94a814a0b4245176215615e1035", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "953297c02582a33411ac6208f2c6e55f0e870df7f80da724ed613f10e6706afd"}, 13 + "nimble_parsec": {:hex, :nimble_parsec, "1.4.2", "8efba0122db06df95bfaa78f791344a89352ba04baedd3849593bfce4d0dc1c6", [:mix], [], "hexpm", "4b21398942dda052b403bbe1da991ccd03a053668d147d53fb8c4e0efe09c973"}, 14 + "typedstruct": {:hex, :typedstruct, "0.5.4", "d1d33d58460a74f413e9c26d55e66fd633abd8ac0fb12639add9a11a60a0462a", [:make, :mix], [], "hexpm", "ffaef36d5dbaebdbf4ed07f7fb2ebd1037b2c1f757db6fb8e7bcbbfabbe608d8"}, 15 + "varint": {:hex, :varint, "1.5.1", "17160c70d0428c3f8a7585e182468cac10bbf165c2360cf2328aaa39d3fb1795", [:mix], [], "hexpm", "24f3deb61e91cb988056de79d06f01161dd01be5e0acae61d8d936a552f1be73"}, 16 + }
+8
test/mst_test.exs
··· 1 + defmodule MSTTest do 2 + use ExUnit.Case 3 + doctest MST 4 + 5 + test "greets the world" do 6 + assert MST.hello() == :world 7 + end 8 + end
+1
test/test_helper.exs
··· 1 + ExUnit.start()