jazari 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: faa1d648cc4474b0e9dbf576b32f4f856037c1f15592ceb84b73ce46a7aedc8b
4
- data.tar.gz: e74d3afaef8633e4c4fc66122fcfe57f1f1a9d862fe49005e828fd4ed22d9672
3
+ metadata.gz: 0c3e5b0a2641a7939f7353e3bec9cb18336272f6e0a9f333155cc36a09e888cd
4
+ data.tar.gz: cf778b406814acf997a9716bb5128dce9070d934d5fb61912906f6f5534f5eb0
5
5
  SHA512:
6
- metadata.gz: fed7a9910117ed6a53eebac98b15544b4864741b84da262320c21c4599363b8f017d6a5cd39be54798bff35b7542866802b5545a238dfaf086ed684f41901cc5
7
- data.tar.gz: 0bfa18d167b05a8189c1ed8c53a67303fb8b014eb83fa1ff260fb382a60d028d30aeec1c6372d484f1f1c09cc603c985f9fcfa99af38e95d6fc25943047eec4f
6
+ metadata.gz: 4985319ede9e2196979829901c3184e1f1ddba0ba2191b64ac9e4e3b2d498bd5cd568bf8d43664a3a2c6440fc3327ae7e8e940c3fa4319fad94529a542bb1eb9
7
+ data.tar.gz: f90b41f05765d28295d8ba704eba10ba92f730bcc246b30b879ed3fd4db2dee9476e2c08febda3137948b11986579ef0daa6c0e0fcd25501937f8ca3e3295b89
data/CHANGELOG.md CHANGED
@@ -8,6 +8,29 @@ codes, the resolved-value shape, how revisions are computed, and the schema the
8
8
  generator emits — changes to any of those are breaking even when the method
9
9
  signatures do not move.
10
10
 
11
+ ## [0.6.0] - 2026-08-15
12
+
13
+ ### Added
14
+
15
+ - **A step discovered mid-run is legal.** `tick` now accepts an item that was
16
+ added to the subject's own runbook after the run opened: it widens the run's
17
+ `checklist_snapshot` to carry the item and marks both the snapshot entry and
18
+ the tick with `post_snapshot: true`. Reported from a production deploy where
19
+ a host's canonical `check_item` had already committed when `tick` raised —
20
+ the caller was told a write failed while it stood, and retrying a
21
+ half-applied two-phase write is how a double record happens.
22
+
23
+ A **recipe** change mid-run is still refused. Those are different events: the
24
+ canon moving underneath an in-flight run is someone else's edit arriving
25
+ uninvited, and that is what the snapshot exists to hold out. A queue run has
26
+ no subject, so its checklist is the recipe and it never widens.
27
+
28
+ - `Jazari::ItemNotInSnapshot` (code `item_not_in_snapshot`), raised when the
29
+ item exists in the canon but post-dates the run's snapshot. It subclasses
30
+ `ItemNotFound`, so hosts rescuing the old class keep working, and hosts that
31
+ need to choose between failing, retrying and proceeding stop matching on the
32
+ message text.
33
+
11
34
  ## [0.5.2] - 2026-08-12
12
35
 
13
36
  ### Fixed
data/lib/jazari/errors.rb CHANGED
@@ -12,6 +12,13 @@ module Jazari
12
12
  class InvalidRunbook < Error; end
13
13
  class RevisionConflict < Error; end
14
14
  class ItemNotFound < Error; end
15
+
16
+ # The item exists — it just post-dates the snapshot this run froze at open.
17
+ # A subclass, so a host that already rescues ItemNotFound keeps working, and
18
+ # one that wants to tell "no such step" from "not this run's step" can.
19
+ # Without the distinction a caller cannot choose between failing, retrying,
20
+ # and proceeding, and ends up matching on prose.
21
+ class ItemNotInSnapshot < ItemNotFound; end
15
22
  class ReadOnlyTarget < Error; end
16
23
  class RunClosed < Error; end
17
24
  end
data/lib/jazari/runs.rb CHANGED
@@ -85,12 +85,15 @@ module Jazari
85
85
  end
86
86
 
87
87
  known = snapshot.map { |item| item["id"] }
88
- raise ItemNotFound, "unknown checklist item #{item_id}" unless known.include?(item_id.to_s)
88
+ late = admit_late_item!(record, snapshot, item_id) unless known.include?(item_id.to_s)
89
+ record.checklist_snapshot = snapshot + [ late ] if late
89
90
 
90
91
  ticks = stored(record.ticks).reject { |t| t["id"] == item_id.to_s }
91
- ticks << { "id" => item_id.to_s, "done" => done == true, "at" => now.utc.iso8601,
92
- "actor_ref" => resolve_actor_ref(actor_ref, fallback: record.actor_ref),
93
- "note" => note&.to_s }
92
+ tick = { "id" => item_id.to_s, "done" => done == true, "at" => now.utc.iso8601,
93
+ "actor_ref" => resolve_actor_ref(actor_ref, fallback: record.actor_ref),
94
+ "note" => note&.to_s }
95
+ tick["post_snapshot"] = true if late
96
+ ticks << tick
94
97
  record.ticks = ticks
95
98
  end
96
99
  end
@@ -158,6 +161,58 @@ module Jazari
158
161
  end
159
162
  private_class_method :mutate
160
163
 
164
+ # A step discovered mid-run is the ordinary case, not an anomaly: a deploy
165
+ # is exactly when a missing step is found. Refusing the tick pushes that
166
+ # work outside the record, which is the one thing the record exists to
167
+ # provide — so the snapshot WIDENS, and says that it did.
168
+ #
169
+ # It widens from the SUBJECT'S OWN runbook only, never from the recipe.
170
+ # Those are different events wearing the same shape:
171
+ #
172
+ # * someone performing this run added a step to this subject — legal, and
173
+ # the run should carry it;
174
+ # * the canon moved underneath an in-flight run — not legal, and the
175
+ # snapshot exists precisely to hold that line (see `open`).
176
+ #
177
+ # A queue run has no subject, so its checklist IS the recipe and it can
178
+ # never widen. That is the correct answer there, not a limitation.
179
+ def admit_late_item!(record, snapshot, item_id)
180
+ id = item_id.to_s
181
+ item = subject_checklist(record).find { |i| i["id"] == id }
182
+ unless item
183
+ # Distinguish "no such step anywhere" from "exists in the canon, but
184
+ # this run froze before it did" — the caller's response differs.
185
+ raise ItemNotInSnapshot, "checklist item #{item_id} post-dates run #{record.id}'s snapshot" if
186
+ recipe_item?(record, id)
187
+
188
+ raise ItemNotFound, "unknown checklist item #{item_id}"
189
+ end
190
+ raise InvalidRunbook, "run #{record.id} snapshot exceeds #{Checklist::MAX_ITEMS} items" if
191
+ snapshot.length >= Checklist::MAX_ITEMS
192
+
193
+ # Marked, so a reader can tell what the run opened against from what it
194
+ # picked up along the way. Widening silently would make the snapshot a
195
+ # record of the present, which is the opposite of its job.
196
+ item.merge("post_snapshot" => true)
197
+ end
198
+ private_class_method :admit_late_item!
199
+
200
+ def subject_checklist(record)
201
+ return [] unless record.subject_type && record.subject_id
202
+
203
+ runbook = Runbook.find_by(runbookable_type: record.subject_type,
204
+ runbookable_id: record.subject_id)
205
+ stored(runbook&.checklist)
206
+ end
207
+ private_class_method :subject_checklist
208
+
209
+ def recipe_item?(record, id)
210
+ RecipeRegistry.fetch(record.recipe_id).checklist.any? { |i| i[:id].to_s == id }
211
+ rescue Error
212
+ false
213
+ end
214
+ private_class_method :recipe_item?
215
+
161
216
  def find_days_run(attributes)
162
217
  Run.where(
163
218
  recipe_id: attributes[:recipe_id],
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jazari
4
- VERSION = "0.5.2"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jazari
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq