jazari 0.2.1 → 0.4.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: b3e1d3f594584f56c5ea72c8aacd01ce1aa9fc6ddd12b2b1f9c160072dfd9c63
4
+ data.tar.gz: f5d622adb6252f7465adb0aeab696c9876598bef6a368bf1845dd8a2c896b252
5
5
  SHA512:
6
- metadata.gz: b6ece9105ac8612a32c329d2581590132874a558f502580dc6d62af6df050c258468d011b7be99b9cf5abc1b7a56561ecd383993fe91f4467d15d211086fe927
7
- data.tar.gz: 84ac29347f6349d21e102ff66acf21387de68a95f149adf7267ccc77a2416fe3b1d7534fe0242b80cc8aec6aff36cf94f849597e12bd802aa18c59135144d66f
6
+ metadata.gz: dfc06b9155a1f706488fa36ee9661c92fb419a27c76cfe6f1e912be31a365ceb6249e24805a92f7e1b9d025511e0559332d6f391f604dbe24e569f46ab886cc1
7
+ data.tar.gz: f5f37bdc359b9626221055c908a5a0ab59f713ab2238ff5df99a82da9c787e403966ef591e65d25fab45ebd2d9dadb78e2a1fd8af860627c4d8661b589356668
data/CHANGELOG.md CHANGED
@@ -8,6 +8,97 @@ 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.4.0] - 2026-08-11
12
+
13
+ ### Added
14
+
15
+ - **`Jazari::RecipeFiles` — recipes as YAML or JSON, without files silently
16
+ overwriting operators.**
17
+
18
+ Nothing ever forbade files: `RecipeRegistry.seed!` takes plain hashes, so
19
+ `seed!(YAML.load_file(…))` already worked. What was missing was everything
20
+ that makes it *safe*.
21
+
22
+ - **`load(path)`** — a file or a directory of `.yml` / `.yaml` / `.json`.
23
+ Accepts one recipe, a list, or a list under a `recipes:` key. It validates
24
+ at LOAD, which is the whole reason it exists rather than "just call
25
+ `YAML.load_file`": an unknown key is a typo, and a typo that loads silently
26
+ becomes a recipe resolving to something nobody wrote. Unknown keys, missing
27
+ topics, bad `run_policy`, duplicate ids and malformed YAML all raise, naming
28
+ the file.
29
+ - **`dump(dir)`** — writes what is actually stored back out, one file per
30
+ recipe. Without this the loop never closes: an operator's runtime fix could
31
+ not be reviewed or committed, so runtime editing would quietly become the
32
+ thing you avoid rather than the thing the design is built around.
33
+ - **`drift(entries)`** — which stored recipes disagree with their file, and in
34
+ which fields. **Reported, never applied.**
35
+
36
+ **Files seed; they do not sync.** `seed!` stays create-if-missing, so a file
37
+ never overwrites a row an operator edited. That is the same rule the runbook
38
+ layer already follows — a customisation diverges rather than rebasing, because
39
+ silently overwriting a deliberate edit with a change nobody saw is the worst
40
+ available outcome. Applying files on every deploy would do exactly that, one
41
+ layer up. The cost is drift, so drift is made visible instead of resolved.
42
+
43
+ One place the loader is deliberately **stricter than the API**: a malformed
44
+ checklist id is an error rather than a fixup. `Checklist.normalize` replaces an
45
+ unusable id with a generated one, which is right when an id is absent and
46
+ opaque — but in a file someone wrote it, MCP addresses the step by it, and
47
+ documentation quotes it. Swapping it for a random token would create exactly
48
+ the file-versus-row disagreement this loader exists to prevent.
49
+
50
+ ### Fixed
51
+
52
+ - **The boundary check read heredoc bodies as code.** It stripped `"…"` and
53
+ `'…'` on the stated principle that a capitalised word inside a string is data,
54
+ but a fixture written as `<<~YML` had its own prose reported as constant
55
+ references. Heredoc bodies are now skipped. Verified still catching a planted
56
+ breach.
57
+
58
+ ## [0.3.0] - 2026-08-11
59
+
60
+ ### Added
61
+
62
+ - **`origin` on the runbook — provenance, so divergence means something.**
63
+ `custom?` answers "does a row exist", which a host adopting jazari cannot use
64
+ as a divergence signal: a backfill materializes a row for every subject at
65
+ once, so the morning after a migration everything reads as diverged and the
66
+ signal carries no information. Comparing content against the recipe does not
67
+ separate them either — a backfilled runbook *genuinely* differs, because it
68
+ carries the steps that subject actually had. Only provenance can.
69
+
70
+ `origin` is a nullable, host-defined string saying **why the row exists**.
71
+ `ResolvedRunbook` gains `#origin`, plus `#inherited?` (a row something claims
72
+ to have manufactured) and `#diverged?` (a row nobody claims, i.e. someone
73
+ decided it). `Jazari.customize` takes an optional `origin:`.
74
+
75
+ Two behaviours make the marker honest rather than decorative:
76
+
77
+ - **`customize` restates it, defaulting to nil.** Rewriting a procedure is a
78
+ decision, so an operator edit clears an inherited marker — the claim about
79
+ how the row came to exist stops being true the moment someone edits it.
80
+ - **Item operations preserve it.** `check_item`, `add_item` and `remove_item`
81
+ leave `origin` untouched, because performing a procedure is not rewriting
82
+ it. Without this, the first person to tick a box would silently convert a
83
+ migration artifact into a deliberate divergence.
84
+
85
+ Raised by a host adoption, where the backfill would otherwise have made six
86
+ of six subjects read as diverged on day one.
87
+
88
+ ### Changed
89
+
90
+ - **`ResolvedRunbook` carries a new member (`origin`).** Positional
91
+ construction and exhaustive destructuring break; keyword construction and
92
+ member access do not. It defaults to nil, so hosts that ignore it are
93
+ unaffected.
94
+
95
+ ### Migration
96
+
97
+ Already installed? `rails g jazari:upgrade` copies the one additive, nullable
98
+ column. It is safe to run ahead of any code that writes it — NULL is truthful
99
+ for every existing row, meaning "this predates provenance". New installs get
100
+ the column from `jazari:install`.
101
+
11
102
  ## [0.2.1] - 2026-08-10
12
103
 
13
104
  ### 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
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "json"
5
+
6
+ module Jazari
7
+ # Recipes as FILES — the version-controlled, reviewable form of the same data.
8
+ #
9
+ # This does not make files a second source of truth. `seed!` has always been
10
+ # create-if-missing, so a file is a SEED, not a sync: the row wins once it
11
+ # exists, because an operator editing a procedure at runtime is the whole
12
+ # reason recipes are data rather than code.
13
+ #
14
+ # That is deliberate and it is the same rule the runbook layer already follows —
15
+ # a customised runbook diverges permanently rather than rebasing, because
16
+ # silently overwriting a deliberate edit with a change nobody saw is the worst
17
+ # available outcome. Applying files on every deploy would do exactly that, one
18
+ # layer up.
19
+ #
20
+ # The cost of that choice is drift: a file and a row can disagree and nothing
21
+ # says so. So drift is REPORTED (`drift`) rather than resolved, and there is a
22
+ # way back out (`dump`) — edit at runtime, export, review the diff in a pull
23
+ # request, commit. The loop closes without anyone's work being overwritten.
24
+ module RecipeFiles
25
+ EXTENSIONS = %w[.yml .yaml .json].freeze
26
+
27
+ # Keys a recipe file may carry. Anything else is a typo, and a typo that
28
+ # loads silently becomes a recipe that resolves to something nobody wrote.
29
+ KEYS = %i[id version topic description checklist run_policy].freeze
30
+
31
+ module_function
32
+
33
+ # Reads one file or every recipe file in a directory. Returns plain hashes,
34
+ # ready for `RecipeRegistry.seed!` — which is why this is a loader and not a
35
+ # registry: producing the data and storing it are separate concerns.
36
+ def load(path)
37
+ entries = Array(paths_for(path)).flat_map { |file| parse(file) }
38
+ entries.each { |entry| validate!(entry) }
39
+ ids = entries.map { |entry| entry[:id] }
40
+ duplicated = ids.tally.select { |_, count| count > 1 }.keys
41
+ raise InvalidRunbook, "duplicate recipe ids: #{duplicated.join(', ')}" if duplicated.any?
42
+
43
+ entries
44
+ end
45
+
46
+ # Writes what is actually stored back out as YAML, one file per recipe.
47
+ # This is the half that makes runtime editing safe to allow: whatever an
48
+ # operator changed can be exported, diffed and committed.
49
+ def dump(directory, recipes: RecipeRecord.order(:recipe_id))
50
+ dir = File.expand_path(directory.to_s)
51
+ Dir.mkdir(dir) unless Dir.exist?(dir)
52
+ recipes.map do |record|
53
+ file = File.join(dir, "#{record.recipe_id}.yml")
54
+ File.write(file, YAML.dump(stringify(to_entry(record))))
55
+ file
56
+ end
57
+ end
58
+
59
+ # Which stored recipes disagree with their file definition, and how.
60
+ #
61
+ # Reported, never applied. A host decides what a difference means: on one
62
+ # fleet a file is the reviewed truth and a divergent row is an incident; on
63
+ # another the row is an operator's fix and the file is simply stale.
64
+ def drift(entries)
65
+ Array(entries).filter_map do |entry|
66
+ attributes = normalize(entry)
67
+ record = RecipeRecord.find_by(recipe_id: attributes[:id].to_s)
68
+ next { id: attributes[:id], state: :missing } if record.nil?
69
+
70
+ differing = KEYS.reject { |key| same?(key, attributes, record) }
71
+ next if differing.empty?
72
+
73
+ { id: attributes[:id], state: :differs, fields: differing }
74
+ end
75
+ end
76
+
77
+ # -- internals ---------------------------------------------------------
78
+
79
+ def paths_for(path)
80
+ expanded = File.expand_path(path.to_s)
81
+ return [ expanded ] if File.file?(expanded)
82
+ raise InvalidRunbook, "no such recipe path: #{path}" unless File.directory?(expanded)
83
+
84
+ Dir.children(expanded).sort
85
+ .select { |name| EXTENSIONS.include?(File.extname(name)) }
86
+ .map { |name| File.join(expanded, name) }
87
+ end
88
+
89
+ def parse(file)
90
+ raw = File.read(file)
91
+ data = if File.extname(file) == ".json"
92
+ JSON.parse(raw)
93
+ else
94
+ # safe_load: a recipe file is operational content, never a place to
95
+ # instantiate arbitrary objects.
96
+ YAML.safe_load(raw, permitted_classes: [], aliases: false)
97
+ end
98
+ # A file is either one recipe, a list of them, or a list under a `recipes:`
99
+ # key. `Array(hash)` would explode a single recipe into key/value pairs, so
100
+ # the Hash cases are named rather than coerced.
101
+ entries = if data.is_a?(Hash)
102
+ data.key?("recipes") ? Array(data["recipes"]) : [ data ]
103
+ else
104
+ Array(data)
105
+ end
106
+ entries.map { |entry| normalize(entry) }
107
+ rescue JSON::ParserError, Psych::SyntaxError => error
108
+ raise InvalidRunbook, "#{File.basename(file)}: #{error.message}"
109
+ end
110
+
111
+ def normalize(entry)
112
+ entry.to_h.transform_keys { |key| key.to_s.to_sym }
113
+ end
114
+
115
+ def validate!(entry)
116
+ unknown = entry.keys - KEYS
117
+ raise InvalidRunbook, "unknown recipe keys: #{unknown.join(', ')}" if unknown.any?
118
+ raise InvalidRunbook, "recipe id is required" if entry[:id].to_s.empty?
119
+ raise InvalidRunbook, "recipe #{entry[:id]} has no topic" if entry[:topic].to_s.empty?
120
+
121
+ policy = entry.fetch(:run_policy, RunPolicy::UNRESTRICTED).to_s
122
+ unless RunPolicy::ALL.include?(policy)
123
+ raise InvalidRunbook, "recipe #{entry[:id]} has unknown run_policy #{policy}"
124
+ end
125
+
126
+ # Reuse the one checklist validator rather than writing a second, laxer
127
+ # one here — a file must not be able to store an item the API would reject.
128
+ items = entry.fetch(:checklist, [])
129
+ Checklist.normalize(items)
130
+
131
+ # STRICTER than the API on one point, deliberately. `normalize` REPLACES an
132
+ # unusable id with a generated one, which is right when an id is absent and
133
+ # opaque. In a file it is neither: someone wrote it, MCP addresses the step
134
+ # by it, and documentation quotes it. Silently swapping it for a random
135
+ # token would put the file and the row into exactly the disagreement this
136
+ # loader exists to prevent — so a malformed id is an error, not a fixup.
137
+ Array(items).each do |item|
138
+ id = (item[:id] || item["id"]).to_s
139
+ next if id.empty? || id.match?(Checklist::ID_FORMAT)
140
+
141
+ raise InvalidRunbook, "recipe #{entry[:id]}: checklist id #{id.inspect} is not a valid token"
142
+ end
143
+
144
+ entry
145
+ end
146
+
147
+ def to_entry(record)
148
+ { id: record.recipe_id, version: record.version, topic: record.topic,
149
+ description: record.description, run_policy: record.run_policy,
150
+ checklist: Checklist.normalize(record.checklist) }
151
+ end
152
+
153
+ def stringify(value)
154
+ case value
155
+ when Hash then value.to_h { |key, inner| [ key.to_s, stringify(inner) ] }
156
+ when Array then value.map { |inner| stringify(inner) }
157
+ when Symbol then value.to_s
158
+ else value
159
+ end
160
+ end
161
+
162
+ def same?(key, attributes, record)
163
+ stored = case key
164
+ when :id then record.recipe_id
165
+ when :checklist then Checklist.normalize(record.checklist)
166
+ else record.public_send(key)
167
+ end
168
+ expected = key == :checklist ? Checklist.normalize(attributes.fetch(key, [])) : attributes[key]
169
+ return true if expected.nil? && key != :id
170
+
171
+ stringify(stored) == stringify(expected)
172
+ end
173
+ end
174
+ end
@@ -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.4.0"
5
5
  end
data/lib/jazari.rb CHANGED
@@ -8,6 +8,7 @@ require "jazari/targets"
8
8
  require "jazari/anchors"
9
9
  require "jazari/resolved_runbook"
10
10
  require "jazari/recipe_registry"
11
+ require "jazari/recipe_files"
11
12
  require "jazari/runs"
12
13
  require "jazari/operations"
13
14
  # The MCP layer is OPTIONAL. Descriptors are cheap and a host may want them to
@@ -103,9 +104,10 @@ module Jazari
103
104
 
104
105
  def resolve(target:) = Operations.resolve(target: target)
105
106
 
106
- def customize(target:, expected_revision:, topic:, description:, checklist:)
107
+ def customize(target:, expected_revision:, topic:, description:, checklist:, origin: nil)
107
108
  Operations.customize(target: target, expected_revision: expected_revision,
108
- topic: topic, description: description, checklist: checklist)
109
+ topic: topic, description: description, checklist: checklist,
110
+ origin: origin)
109
111
  end
110
112
 
111
113
  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.4.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
@@ -65,6 +67,7 @@ files:
65
67
  - lib/jazari/operations.rb
66
68
  - lib/jazari/railtie.rb
67
69
  - lib/jazari/recipe.rb
70
+ - lib/jazari/recipe_files.rb
68
71
  - lib/jazari/recipe_registry.rb
69
72
  - lib/jazari/resolved_runbook.rb
70
73
  - lib/jazari/runs.rb