# Changelog

All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project follows [Semantic Versioning](https://semver.org/) once it reaches 0.1.

## [Unreleased]

## [0.2.0] - 2026-10-23

Proposals, fast aggregates, many-to-many join resources, the `ramal_live` inspector, a benchmark, and a security pass. No breaking change to the public API of 0.1.0: a branch stored by 0.1.0 loads unchanged, and the public functions of 0.1.0 behave as before.

### Added

- `Ramal.propose/2`, `Ramal.approve/2` and `Ramal.reject/2`: a branch can be frozen as a proposal (status `:proposed`) that a decider approves, which merges it, or rejects. The proposer, the decider, the reasons, the replay count and the stale rows are recorded on the branch and stored with it; `list(domain, status: :proposed)` finds the pending ones. Telemetry `[:ramal, :branch, :propose | :approve | :reject]`. Branches stored by 0.1.0 load unchanged.
- `Ramal.aggregate/4` and `Ramal.aggregate!/4`: count, exists, sum, min, max and avg over the rows a read returns inside a branch, so a branch's creates, updates and destroys are included. Outside a branch they call the matching `Ash` function. `Ramal.Aggregate` holds the pure computation. The database aggregates the rows the overlay does not touch and the branch reads only the overlaid ones, so the cost follows the overlay and not the table; a query with a limit, an offset or a distinct reads every matching row. The rows the database aggregates are not fingerprinted for the merge staleness check (ADR 0013).
- Many-to-many join resources inside a branch are tested and documented (DESIGN section 4.1 point 7, ADR 0014): the join resource gets overlay entries keyed by its primary key, and a branch diffs, merges and stores them. A many_to_many load and `manage_relationship` do not see links the branch changed; both are listed as known limits.
- `ramal_live/`: a separate Mix project with a LiveView inspector for branches. `ramal_inspector "/ramal", domain: MyApp.Farm, pubsub: MyApp.PubSub` mounts it inside the host's own `live_session`, so it uses the host's LiveView socket and JavaScript; there is no asset pipeline, the CSS is inline and has light and dark themes. Open and proposed branches are columns with their `Ramal.diff/1` as before and after rows, and buttons to fork, merge, discard, approve and reject, each destructive one with an inline confirmation. `RamalLive.Broadcaster` turns Ramal's telemetry into PubSub messages so the columns update live. Core is unchanged. Decision in ADR 0015.
- `ramal_live/examples/farm_demo.exs`: a runnable demo of the inspector for a screen recording. One command starts a Phoenix Playground app on a seeded farm in the `ramal_dev` database; "Explore 4 alternatives" opens four branches that each move a herd to a different paddock while the columns fill in live, and merging one makes the others stale. How to run it is in `ramal_live/README.md`.
- `bench/`: a separate Mix project that measures fork, read, write, merge, fingerprinting and a tree search against two plain-Postgres baselines (a table copied per fork, and a shared delta table). `mix bench` runs it, `mix bench --quick` is a smoke run that CI compiles and executes. Results and their limits are in `bench/README.md`.
- `bench/` results rerun after fast aggregates: the tree search takes 183 ms instead of 5,705 ms (1.07 times the delta baseline instead of 23 times), and every table in `bench/README.md` now comes from that one run. The earlier raw samples stay in `bench/results/published/` next to the new ones.
- `ramal_live` is versioned with Ramal (0.2.0) and builds as a Hex package: `RAMAL_LIVE_HEX=1` makes its `mix.exs` depend on the published `ramal ~> 0.2.0` instead of the checkout next to it, and the package ships its `LICENSE` and builds its docs with ExDoc.
- Community files for the public launch: `CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), `SECURITY.md`, bug and feature issue templates, a pull request template that mirrors the rules in `CLAUDE.md`, and Dependabot for the root Mix project, `bench/` and GitHub Actions.
- A "Security model and defaults" section in `SECURITY.md`, and ADR 0016: what Ramal enforces and what it leaves to the host, how stored state is treated, and how to mount the inspector in production. CI runs `mix hex.audit` for the library, `bench/` and `ramal_live/`.
- `Ramal.Supervisor` options `:max_active_branches` and `:branch_rate`: optional per-domain limits on the open and proposed branches, and on the branches and forks created per window. `Ramal.branch/2` and `Ramal.fork/2` return `{:error, %Ramal.Error{reason: :limit_reached}}` past either, with the limit in `details`, and emit `[:ramal, :limit, :reached]`. Both are off by default, so nothing changes until a host sets them (ADR 0016, `SECURITY.md`).
- `c:Ramal.Storage.count/3`, an optional callback that counts a domain's branches by status without loading them. `Ramal.Storage.ETS` and `Ramal.Storage.Postgres` define it; a backend without it is counted with `list/3`.

### Security

- `ramal_live`: the `:actor` option of `ramal_inspector/2` is required. A router without it fails to compile with a message that says what to pass, and a route whose actor function does not exist raises when the page opens. A page with no actor would merge every branch as the actors the writes were captured with and record no decider; a tool with no users passes a function that returns a constant (ADR 0016).
- `ramal_live`: attributes marked `sensitive?: true` show as `[redacted]` in a branch's diff. The page printed every value, so a password hash or token written in a branch was visible to everyone who could open the inspector.
- `ramal_live`: when the `:actor` function returns `nil` the page refuses fork, merge, discard, approve and reject. Before, a session without a user merged as the recorded actors with no decider.
- `inspect/1` of a `Ramal.Branch` prints only its id, name, domain, status and creation time. The log holds the raw input of every captured write, which Ash does not redact, and a crash report prints the state of a branch process.
- `Ramal.Storage.Codec.decode_state/2` returns `{:error, %Ramal.Error{reason: :storage}}` for a malformed stored state instead of raising, never echoes stored values in the error, rejects a module name that is not a name (`Elixir.../../x`) before it touches the code path, and rejects a stored struct whose module is not a struct.

### Fixed

- `Ramal.merge/2` and `Ramal.approve/2` on resources backed by `Ash.DataLayer.Ets` returned a `CaseClauseError` instead of `{:error, %Ramal.Error{}}` when the staleness check or the replay failed. The error is now returned as with Postgres. ETS cannot roll back, so rows replayed before a failing entry stay written; the error message and `details.not_rolled_back` say so (ADR 0007).
- `docs/ROADMAP.md` and `bench/README.md` no longer say the DAPLab article was unreachable; they link it and state how the benchmark differs from theirs.
- An update that changes no attribute is now logged, so a merge replays it. Before, the rows its hooks or managed relationships wrote (the links of a `manage_relationship`, a record created in an `after_action`) were logged as nested under an update that was not logged, and the merge dropped them.
- A load of a many_to_many (or a limited has_many) inside a branch no longer loses a destination row the branch updated: the patched row keeps the source record Ash groups the load by.
- README: the DAPLab quotation now matches the article word for word, and the Neon and Dolt summary matches its findings.

## [0.1.0] - 2026-10-22

### Added

- Project scaffold: design document, roadmap, conventions, CI, test database setup, logo.
- `Ramal.Overlay`: the pure overlay of a branch (put, slice, patch, diff, merge for forks), `Ramal.Change` and `Ramal.Error`.
- Branch processes: `Ramal.Supervisor`, `Ramal.branch/2`, `run/2`, `run!/2`, `current/0`, `get/1`, `list/2` and `discard/1`, with idle unload, transparent rehydration, a heap limit, and telemetry for the branch lifecycle.
- `Ramal.Storage` behaviour and the `Ramal.Storage.ETS` backend.
- `Ramal.Resource` extension and `Ramal.Preparation`: reads inside a branch see rows created, updated and deleted in it, with limit, offset, sort, select and relationship loads applied to the patched rows. Telemetry for `[:ramal, :read, :patch]` and `[:ramal, :read, :unpatched_aggregate]`.
- `Ramal.Capture`: creates, updates and destroys inside `Ramal.run/2` land in the branch overlay and log instead of the data layer, with validations, policies and hooks still running. Notifications are dropped unless a resource sets `notify_in_branch? true`; bulk actions and upsert actions are refused; a failed action undoes its writes. `Ramal.Error` is now a Splode error with `Ramal.Error.find/1`. Telemetry for `[:ramal, :write, :capture]`.
- `Ramal.fork/2` and `Ramal.diff/1`: a fork starts with its parent's overlay, log and base and shares nothing afterwards; `diff/1` returns `Ramal.Change` structs in log order without querying the database. Reads record a fingerprint of every base row they return, for the merge staleness check.
- `Ramal.branch/2` returns `:no_branchable_resources` for a domain none of whose resources opted in.
- `Ramal.merge/2`: checks that the base rows the branch saw are unchanged (`on_stale: :error | :ignore | fun`), then replays the branch log through the real actions in one transaction, so validations and policies run again. Failures return `:stale` or `:merge_failed` and leave the base untouched; a merged branch cannot be merged again. Telemetry span `[:ramal, :merge]`.
- `Ramal.Storage.Postgres` and `mix ramal.gen.migration`: branches persist in a `ramal_branches` table and load back equal in a fresh VM. `Ramal.Storage.Codec` serializes a branch through the Ash embedded types without creating atoms. `c:Ramal.Storage.list/3` now returns `{:ok, branches} | {:error, error}`.
- Documentation pass for 0.1: the README example runs as a test (`test/readme_test.exs`), a test checks that every public module has an example and every public function a `@doc` and `@spec`, `mix docs` builds without warnings and includes the ADRs, and every **[confirm]** mark in the design is resolved.
- A multitenant resource used with a tenant inside a branch is refused with `:multitenancy_unsupported` (`Ramal.Resource.ensure_untenanted/2`), instead of mixing tenants in one overlay or failing at merge.
- Stored branches no longer hold the `sensitive?` attributes of an actor that is an Ash record (they are stored as `nil`); other actors are stored as before.

[Unreleased]: https://github.com/agustinrojas1/ramal/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/agustinrojas1/ramal/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/agustinrojas1/ramal/releases/tag/v0.1.0
