0001. Ramal.Change is the diff struct; the write-side hook is Ramal.Capture
Copy Markdown
View Source
Status: accepted, 2026-10-22. Issue: #2.
Context
The documents used Ramal.Change for two things. docs/DESIGN.md sections 2, 5 and 8, the README and issues #2 and #6
use it for the struct that diff/1 returns. The CLAUDE.md layout and issue #5 use it for the Ash change module that
captures creates, updates and destroys into the overlay. One module cannot be both a public data struct and an
Ash.Resource.Change implementation without being confusing, and the struct is public API the README already shows.
The overlay also needed a rule for what happens when a branch writes the same row twice, which the design left open ("the record is the full Ash struct as the action left it", nothing about combining).
Decision
Ramal.Changeis the struct%Ramal.Change{resource, key, kind, changes}. The write-side Ash change isRamal.Capture,@moduledoc false.CLAUDE.mdand the design are updated to say so.- The overlay records the net effect on the base, not the history (the log is the history). Creating a row and then
deleting it leaves no entry. Deleting a base row and creating the same key again is an
:updatedentry. So a:deletedentry always means the base had the row, anddiffcan rely on that. Ramal.Overlay.diff/3is computed against base snapshots and returns changes sorted by ref, because a map has no write order.Ramal.diff/1reorders them by the log. A missing snapshot is{:error, %Ramal.Error{reason: :missing_snapshot}}, a new reason in the documented list.
Consequences
- Nothing public is renamed: the README example stays valid.
- Issue #5 builds
Ramal.Capture, notRamal.Change. - The overlay never polices duplicate keys or updates of missing rows. The write layer must reject them first, as the data layer would on the real base.
- A branch cannot tell "created then deleted" from "never touched" by looking at the overlay alone. The log still can.