Revision history: change sets + revisions + revert — the undo substrate (#48) (#61)
Build image / build-and-push (push) Successful in 5s

Co-authored-by: Steve Dudenhoeffer <[email protected]>
This commit was merged in pull request #61.
This commit is contained in:
2026-07-21 05:07:30 +00:00
committed by steve
parent 653f381c4b
commit f208da94d6
16 changed files with 2169 additions and 19 deletions
+91
View File
@@ -93,6 +93,97 @@ const (
CategoryFruit = "fruit"
CategoryTreeShrub = "tree_shrub"
CategoryCover = "cover"
// Where a change set came from. SourceAgent is what the history UI badges
// differently, so an autonomous edit is never mistaken for a hand edit.
SourceUI = "ui"
SourceAgent = "agent"
SourceAPI = "api"
// The entity types a revision can snapshot. Garden covers metadata edits
// only — garden creation and deletion are deliberately not revertible.
EntityGarden = "garden"
EntityObject = "object"
EntityPlanting = "planting"
OpCreate = "create"
OpUpdate = "update"
OpDelete = "delete"
)
// ChangeSet groups the revisions produced by one logical operation — the unit of
// undo. A revert is itself a change set (RevertsID names its target), so history
// is append-only and an undo can be undone.
type ChangeSet struct {
ID int64 `json:"id"`
GardenID int64 `json:"gardenId"`
ActorID int64 `json:"actorId"`
Source string `json:"source"`
Summary string `json:"summary"`
AgentRunID *string `json:"agentRunId,omitempty"`
RevertsID *int64 `json:"revertsId,omitempty"`
CreatedAt string `json:"createdAt"`
// Computed by the service for the history list, never persisted.
ActorName string `json:"actorName,omitempty"`
// RevertedByID is the change set that reverted this one, if any — what lets
// the list mark an entry as already undone.
RevertedByID *int64 `json:"revertedById,omitempty"`
// Counts tallies the revisions by (entity type, op) so the list can say
// "3 plops added, 1 bed moved" without loading every revision.
Counts []ChangeCount `json:"counts,omitempty"`
// Revisions is populated only when a single change set is loaded in full.
Revisions []Revision `json:"revisions,omitempty"`
}
// ChangeCount is one (entity type, op) tally within a change set.
type ChangeCount struct {
EntityType string `json:"entityType"`
Op string `json:"op"`
N int `json:"n"`
}
// Revision is one row-level change within a change set. Before/After are JSON
// row snapshots (nil for create/delete respectively) and include the row's
// version, which the revert guard compares against.
type Revision struct {
ID int64 `json:"id"`
ChangeSetID int64 `json:"changeSetId"`
Seq int64 `json:"seq"`
EntityType string `json:"entityType"`
EntityID int64 `json:"entityId"`
Op string `json:"op"`
Before *string `json:"before,omitempty"`
After *string `json:"after,omitempty"`
}
// RevertConflict reports one entity a revert deliberately left alone, because
// reverting it would have discarded a change made after the change set being
// reverted. The rest of the change set still reverts.
type RevertConflict struct {
EntityType string `json:"entityType"`
EntityID int64 `json:"entityId"`
// Reason is one of the ConflictReason* values below.
Reason string `json:"reason"`
// Name is a human label for the entity where one exists (an object's name),
// so the UI can say "the north bed" rather than "object 41".
Name string `json:"name,omitempty"`
}
// Why a revert skipped an entity.
const (
// ConflictChanged: the row was edited after the change set being reverted, so
// restoring the old snapshot would silently discard that edit.
ConflictChanged = "changed"
// ConflictMissing: the row is gone, so there is nothing to revert to.
ConflictMissing = "missing"
// ConflictExists: a row with that id exists again, so restoring the deleted
// snapshot would overwrite it.
ConflictExists = "exists"
// ConflictUnsupported: this kind of change has no inverse the revert knows how
// to apply. Reported rather than silently skipped, so a change recorded by a
// later feature that revert wasn't taught about is visible instead of quiet.
ConflictUnsupported = "unsupported"
)
// User is a pansy account. It may have a local password, OIDC identity, or both.