SIGNING OFF ON A VERSION — two columns and one comparison.
A row points at the approval that covers it. The approval says which version it covered. Whether the sign-off still applies is a comparison:
approved?(row) = row.<reference>.version_id == row.version_idThe content moves, the versions stop matching, the sign-off lapses. Nothing had to erase it.
Why the approval carries the version, and not the row a flag
A flag needs something to clear it on every path that writes the row — a hook, an invalidation pass, a trigger — and the failure mode of a missed clear is a row that reads as reviewed when nobody reviewed that content. Silent, and in the direction of falsely claiming review.
An equality that stops holding cannot be forgotten. That is the whole argument, and it is why this module is thirty lines rather than a state machine.
The library knows nothing about approval schemes
One signature, two reviewers in agreement, a decision with a recorded reason, an approval that expires — these are different HOST RESOURCES, not different configurations here. The contract is:
- the approving resource has a
version_id; - the approved node has a column pointing at it.
Nothing else is read, so nothing else can be constrained. Different cells may therefore use entirely different schemes, and neither is aware of the other.
This is a constraint on future changes, not just a description. The
pressure to add approved_by, approved_at, approval_note to a
library-owned notion will be constant, and every one of them would make a
host's scheme the library's business.
History arrives without being designed
Approvals are records with their own identity. A re-approval writes a NEW one and the reference moves; the previous approval stays exactly where it was, naming the version it covered and whoever made it. Ordering them by time is the whole audit trail.
What this does not do
It does not release a suspension. A :approval suspension is cleared by
ReactiveDag.Suspension.discharge/1 and a resumption, the same as any other
— see release/3 below, which is the small amount of glue that connects the
two.
Summary
Functions
Is this row's sign-off current?
The approval covering this row, or nil.
Release a :approval suspension, and cascade onward from the approved row.
Functions
Is this row's sign-off current?
false when the row has no approval reference, when the reference points at
nothing, or when the approval covered a version the row has since moved past.
Those three are deliberately one answer: from a consumer's side, "never
approved" and "approved, then changed" are both not approved now.
spec is [via: :approval_id, resource: MyApp.Approval] — the column
holding the reference, and what it points at.
The approval covering this row, or nil.
Returns it whether or not it is CURRENT — a reviewer looking at a row whose
sign-off lapsed needs to see who approved what, which is exactly the case
approved?/2 answers false for.
@spec release(ReactiveDag.Plan.t(), map(), keyword()) :: {:ok, ReactiveDag.Report.t()} | {:error, :not_approved}
Release a :approval suspension, and cascade onward from the approved row.
This is the glue between an approval and the graph, and it is deliberately thin: approving is the HOST's action, on the host's own resource, with the host's own rules about who may do it. What the library contributes is what happens next — the suspension is discharged and the change it held resumes.
Checks the sign-off is actually current before releasing. Approving version A and then releasing a row that has moved to version B would propagate content nobody signed, which is the one thing a gate exists to prevent — and it is a plausible mistake, because the two happen in different requests.
Returns {:ok, report}, or {:error, :not_approved} when the sign-off is
missing or lapsed.