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 +4 -4
- data/CHANGELOG.md +44 -0
- data/lib/generators/jazari/install/templates/create_jazari_tables.rb +6 -0
- data/lib/generators/jazari/upgrade/templates/add_jazari_runbook_origin.rb +10 -0
- data/lib/generators/jazari/upgrade/upgrade_generator.rb +31 -0
- data/lib/jazari/operations.rb +24 -9
- data/lib/jazari/resolved_runbook.rb +17 -3
- data/lib/jazari/version.rb +1 -1
- data/lib/jazari.rb +3 -2
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c1e44727af4c1cfd7ca851c84618d9015f39aec562c3133301772f6d97b21076
|
|
4
|
+
data.tar.gz: 8c87563ee6acfbe91b8544274c981dbb98fb24ac05e77cb170f7e33033067f2d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/jazari/operations.rb
CHANGED
|
@@ -23,11 +23,14 @@ module Jazari
|
|
|
23
23
|
default_value(recipe, target, last)
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
data/lib/jazari/version.rb
CHANGED
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.
|
|
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
|