jazari 0.2.1 → 0.3.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: 7725f32aead889e14acc5cb3df2f91b8add8e478248244b1dc8b7893565d8b34
4
- data.tar.gz: 746f4d57b3e11aaa9252e089568981baa90ddab26fd41d27757a7db3f987227d
3
+ metadata.gz: c1e44727af4c1cfd7ca851c84618d9015f39aec562c3133301772f6d97b21076
4
+ data.tar.gz: 8c87563ee6acfbe91b8544274c981dbb98fb24ac05e77cb170f7e33033067f2d
5
5
  SHA512:
6
- metadata.gz: b6ece9105ac8612a32c329d2581590132874a558f502580dc6d62af6df050c258468d011b7be99b9cf5abc1b7a56561ecd383993fe91f4467d15d211086fe927
7
- data.tar.gz: 84ac29347f6349d21e102ff66acf21387de68a95f149adf7267ccc77a2416fe3b1d7534fe0242b80cc8aec6aff36cf94f849597e12bd802aa18c59135144d66f
6
+ metadata.gz: dec279a4cedbcf0377a512298300487316e04a6ac3dfa9059e37415edcfba07ba0015b11d1d8d2b08d6bf4fd33ec557d87a507a216a095d66a52bdc5a4441f6e
7
+ data.tar.gz: 148962f5b5f9762f35cc04ba285485c4e76e5de41ef086ea0dafcf4861cd2e9b1110c299ee39ddd45abe491e13a473c0477b66bac914306c7cc9540b6142e382
data/CHANGELOG.md CHANGED
@@ -8,6 +8,50 @@ 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.3.0] - 2026-08-11
12
+
13
+ ### Added
14
+
15
+ - **`origin` on the runbook — provenance, so divergence means something.**
16
+ `custom?` answers "does a row exist", which a host adopting jazari cannot use
17
+ as a divergence signal: a backfill materializes a row for every subject at
18
+ once, so the morning after a migration everything reads as diverged and the
19
+ signal carries no information. Comparing content against the recipe does not
20
+ separate them either — a backfilled runbook *genuinely* differs, because it
21
+ carries the steps that subject actually had. Only provenance can.
22
+
23
+ `origin` is a nullable, host-defined string saying **why the row exists**.
24
+ `ResolvedRunbook` gains `#origin`, plus `#inherited?` (a row something claims
25
+ to have manufactured) and `#diverged?` (a row nobody claims, i.e. someone
26
+ decided it). `Jazari.customize` takes an optional `origin:`.
27
+
28
+ Two behaviours make the marker honest rather than decorative:
29
+
30
+ - **`customize` restates it, defaulting to nil.** Rewriting a procedure is a
31
+ decision, so an operator edit clears an inherited marker — the claim about
32
+ how the row came to exist stops being true the moment someone edits it.
33
+ - **Item operations preserve it.** `check_item`, `add_item` and `remove_item`
34
+ leave `origin` untouched, because performing a procedure is not rewriting
35
+ it. Without this, the first person to tick a box would silently convert a
36
+ migration artifact into a deliberate divergence.
37
+
38
+ Raised by a host adoption, where the backfill would otherwise have made six
39
+ of six subjects read as diverged on day one.
40
+
41
+ ### Changed
42
+
43
+ - **`ResolvedRunbook` carries a new member (`origin`).** Positional
44
+ construction and exhaustive destructuring break; keyword construction and
45
+ member access do not. It defaults to nil, so hosts that ignore it are
46
+ unaffected.
47
+
48
+ ### Migration
49
+
50
+ Already installed? `rails g jazari:upgrade` copies the one additive, nullable
51
+ column. It is safe to run ahead of any code that writes it — NULL is truthful
52
+ for every existing row, meaning "this predates provenance". New installs get
53
+ the column from `jazari:install`.
54
+
11
55
  ## [0.2.1] - 2026-08-10
12
56
 
13
57
  ### Fixed
@@ -31,6 +31,12 @@ class CreateJazariTables < ActiveRecord::Migration[7.1]
31
31
  t.string :topic, null: false
32
32
  t.text :description, null: false, default: ""
33
33
  t.jsonb :checklist, null: false, default: []
34
+ # Provenance, host-defined and nullable. A runbook that exists because a
35
+ # backfill created it is a migration artifact; one that exists because an
36
+ # operator edited it is a decision. Both differ from the canon, so
37
+ # comparing content cannot tell them apart — only provenance can, and NULL
38
+ # honestly means "we did not record why".
39
+ t.string :origin
34
40
  t.integer :lock_version, null: false, default: 0
35
41
  t.timestamps
36
42
  t.index %i[runbookable_type runbookable_id], unique: true
@@ -0,0 +1,10 @@
1
+ # jazari 0.3.0 — provenance on the runbook.
2
+ #
3
+ # Nullable and additive, in one deploy, because nothing reads it yet and NULL is
4
+ # a truthful value: it means "this row predates provenance", which is exactly
5
+ # what every existing row is.
6
+ class AddJazariRunbookOrigin < ActiveRecord::Migration[7.1]
7
+ def change
8
+ add_column :jazari_runbooks, :origin, :string
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module Jazari
7
+ module Generators
8
+ # For hosts that already installed the tables. Schema changes are copied,
9
+ # never applied on the gem's own initiative, for the same reason install is
10
+ # explicit: a shared operations table changing shape inside someone else's
11
+ # `db:migrate` is not a change they agreed to.
12
+ class UpgradeGenerator < ::Rails::Generators::Base
13
+ include ::ActiveRecord::Generators::Migration
14
+
15
+ source_root File.expand_path("templates", __dir__)
16
+
17
+ desc "Copies the schema changes an already-installed host needs. PostgreSQL only."
18
+
19
+ def copy_migration
20
+ migration_template "add_jazari_runbook_origin.rb", "db/migrate/add_jazari_runbook_origin.rb"
21
+ end
22
+
23
+ def report
24
+ say ""
25
+ say "jazari: upgrade migration copied. It is additive and nullable, so it"
26
+ say "is safe to run ahead of the code that writes the column."
27
+ say ""
28
+ end
29
+ end
30
+ end
31
+ end
@@ -23,11 +23,14 @@ module Jazari
23
23
  default_value(recipe, target, last)
24
24
  end
25
25
 
26
- def customize(target:, expected_revision:, topic:, description:, checklist:)
26
+ # `origin` records WHY this row exists, for hosts that materialize runbooks
27
+ # themselves — a backfill, an import, a template. Leave it nil for an operator
28
+ # edit, which is the default because that is the ordinary case.
29
+ def customize(target:, expected_revision:, topic:, description:, checklist:, origin: nil)
27
30
  writable!(target)
28
31
  validate_document!(topic, description)
29
32
  items = Checklist.normalize(checklist)
30
- write(target, expected_revision) do |current|
33
+ write(target, expected_revision, origin: origin) do |current|
31
34
  current.merge(topic: topic, description: description, checklist: items)
32
35
  end
33
36
  end
@@ -112,25 +115,35 @@ module Jazari
112
115
  end
113
116
  private_class_method :writable!
114
117
 
115
- def write(target, expected_revision)
118
+ # `origin` distinguishes REWRITING the procedure from PERFORMING it, so the
119
+ # sentinel is not cosmetic. Rewriting it (customize) restates why the row
120
+ # exists, and passing nil there is an operator claiming it as their own.
121
+ # Ticking an item is doing the work the row already describes, and must
122
+ # leave that claim alone — otherwise the first person to check a box
123
+ # silently converts a migration artifact into a deliberate divergence.
124
+ KEEP_ORIGIN = :keep
125
+
126
+ def write(target, expected_revision, origin: KEEP_ORIGIN)
116
127
  record = find_runbook(target)
117
128
  if record
118
129
  record.with_lock do
119
130
  verify_custom_revision!(record, expected_revision)
120
131
  document = yield(current_document(record))
121
- record.update!(
132
+ attributes = {
122
133
  topic: document[:topic], description: document[:description],
123
134
  checklist: store_items(document[:checklist])
124
- )
135
+ }
136
+ attributes[:origin] = origin unless origin == KEEP_ORIGIN
137
+ record.update!(attributes)
125
138
  end
126
139
  else
127
- create_custom(target, expected_revision) { |current| yield(current) }
140
+ create_custom(target, expected_revision, origin: origin) { |current| yield(current) }
128
141
  end
129
142
  resolve(target: target)
130
143
  end
131
144
  private_class_method :write
132
145
 
133
- def create_custom(target, expected_revision)
146
+ def create_custom(target, expected_revision, origin: KEEP_ORIGIN)
134
147
  verify_default_revision!(target, expected_revision)
135
148
  recipe = RecipeRegistry.fetch(target.recipe_id)
136
149
  document = yield(
@@ -141,7 +154,9 @@ module Jazari
141
154
  runbookable: runbookable_for(target),
142
155
  recipe_id: recipe.id,
143
156
  topic: document[:topic], description: document[:description],
144
- checklist: store_items(document[:checklist])
157
+ checklist: store_items(document[:checklist]),
158
+ # A brand-new row has no prior claim to keep, so the sentinel means nil.
159
+ origin: (origin == KEEP_ORIGIN ? nil : origin)
145
160
  )
146
161
  rescue ActiveRecord::RecordNotUnique
147
162
  raise RevisionConflict, "another writer materialized this runbook first"
@@ -219,7 +234,7 @@ module Jazari
219
234
  state: "custom", revision: record.lock_version, topic: record.topic,
220
235
  description: record.description, checklist: stored_items(record.checklist),
221
236
  target_reference: target.public_reference, recipe: recipe.provenance,
222
- last_run: run_summary(last)
237
+ last_run: run_summary(last), origin: record.origin
223
238
  )
224
239
  end
225
240
  private_class_method :custom_value
@@ -4,21 +4,35 @@ module Jazari
4
4
  # Every operation returns this. Never an unsaved ActiveRecord object.
5
5
  ResolvedRunbook = Data.define(
6
6
  :state, :revision, :topic, :description, :checklist, :progress,
7
- :target_reference, :recipe, :last_run
7
+ :target_reference, :recipe, :last_run, :origin
8
8
  ) do
9
9
  def initialize(state:, revision:, topic:, description:, checklist:,
10
- target_reference:, recipe:, last_run: nil)
10
+ target_reference:, recipe:, last_run: nil, origin: nil)
11
11
  items = Checklist.freeze_items(checklist)
12
12
  super(
13
13
  state: state.freeze, revision: revision, topic: topic.freeze,
14
14
  description: description.freeze, checklist: items,
15
15
  progress: Checklist.progress(items).freeze,
16
16
  target_reference: target_reference.freeze, recipe: recipe.freeze,
17
- last_run: last_run.freeze
17
+ last_run: last_run.freeze, origin: origin&.freeze
18
18
  )
19
19
  end
20
20
 
21
21
  def default? = state == "default"
22
22
  def custom? = state == "custom"
23
+
24
+ # `custom?` answers "does a row exist", which is not the same question as
25
+ # "did someone decide this". A backfill materializes a row for every subject
26
+ # it touches, so a host reading `custom?` as divergence sees 100% divergence
27
+ # the morning after a migration and learns nothing from it.
28
+ #
29
+ # Comparing content against the canon does not separate them either: a
30
+ # backfilled runbook genuinely differs, because it carries the steps the
31
+ # subject actually had. Only provenance can, so only provenance is asked.
32
+ def inherited? = custom? && !origin.nil?
33
+
34
+ # Divergence someone chose: a row that exists, with nothing claiming to have
35
+ # manufactured it.
36
+ def diverged? = custom? && origin.nil?
23
37
  end
24
38
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Jazari
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/jazari.rb CHANGED
@@ -103,9 +103,10 @@ module Jazari
103
103
 
104
104
  def resolve(target:) = Operations.resolve(target: target)
105
105
 
106
- def customize(target:, expected_revision:, topic:, description:, checklist:)
106
+ def customize(target:, expected_revision:, topic:, description:, checklist:, origin: nil)
107
107
  Operations.customize(target: target, expected_revision: expected_revision,
108
- topic: topic, description: description, checklist: checklist)
108
+ topic: topic, description: description, checklist: checklist,
109
+ origin: origin)
109
110
  end
110
111
 
111
112
  def add_item(target:, expected_revision:, text:, required: true)
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nauman Tariq
@@ -56,6 +56,8 @@ files:
56
56
  - app/models/jazari/runbook.rb
57
57
  - lib/generators/jazari/install/install_generator.rb
58
58
  - lib/generators/jazari/install/templates/create_jazari_tables.rb
59
+ - lib/generators/jazari/upgrade/templates/add_jazari_runbook_origin.rb
60
+ - lib/generators/jazari/upgrade/upgrade_generator.rb
59
61
  - lib/jazari.rb
60
62
  - lib/jazari/anchors.rb
61
63
  - lib/jazari/checklist.rb