Module: Textus::Manifest::Schema::Semantics
- Defined in:
- lib/textus/manifest/schema/semantics.rb
Overview
Cross-field rules and ADR migration hints. Called by Validator.validate! AFTER the structural dry-schema Contract passes. Operates on the raw hash.
Class Method Summary collapse
- .check!(raw) ⇒ Object
- .check_entries!(entries) ⇒ Object
- .check_lane_kind_consistency!(raw) ⇒ Object
- .check_lanes!(lanes) ⇒ Object
- .check_owner!(owner, path) ⇒ Object
- .check_owners!(lanes, entries) ⇒ Object
- .check_publish_block!(entry, path) ⇒ Object
- .check_retired_publish_keys!(entry, path) ⇒ Object
- .check_retired_render_keys!(entry, path) ⇒ Object
- .check_roles!(roles) ⇒ Object
- .check_rules!(rules) ⇒ Object
- .check_single_machine!(raw) ⇒ Object
- .check_single_queue!(raw) ⇒ Object
- .valid_owner?(token) ⇒ Boolean
- .walk(hash, allowed, path) ⇒ Object
Class Method Details
.check!(raw) ⇒ Object
11 12 13 14 15 16 17 18 19 20 21 |
# File 'lib/textus/manifest/schema/semantics.rb', line 11 def check!(raw) check_roles!(raw["roles"]) check_lanes!(raw["lanes"]) check_entries!(raw["entries"]) check_owners!(raw["lanes"], raw["entries"]) check_rules!(raw["rules"]) check_single_queue!(raw) check_single_machine!(raw) check_lane_kind_consistency!(raw) walk(raw["audit"], AUDIT_KEYS, "$.audit") if raw["audit"].is_a?(Hash) end |
.check_entries!(entries) ⇒ Object
65 66 67 68 69 70 71 72 73 74 |
# File 'lib/textus/manifest/schema/semantics.rb', line 65 def check_entries!(entries) Array(entries).each_with_index do |e, i| path = "$.entries[#{i}]" check_retired_publish_keys!(e, path) check_retired_render_keys!(e, path) walk(e, ENTRY_KEYS, path) check_publish_block!(e, path) walk(e["source"], SOURCE_KEYS, "#{path}.source") if e.is_a?(Hash) && e["source"].is_a?(Hash) end end |
.check_lane_kind_consistency!(raw) ⇒ Object
206 207 208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/textus/manifest/schema/semantics.rb', line 206 def check_lane_kind_consistency!(raw) held = Capabilities.resolve(raw["roles"]).values.flatten.uniq Array(raw["lanes"]).each_with_index do |z, i| verb = KIND_REQUIRES_VERB[z["kind"]] next if verb.nil? || held.include?(verb) raise BadManifest.new( "lane '#{z["name"]}' (#{z["kind"]}) at '$.lanes[#{i}]' " \ "needs a role with capability '#{verb}'; none declared", ) end end |
.check_lanes!(lanes) ⇒ Object
53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/textus/manifest/schema/semantics.rb', line 53 def check_lanes!(lanes) Array(lanes).each_with_index do |z, i| walk(z, LANE_KEYS, "$.lanes[#{i}]") next unless %w[quarantine derived].include?(z["kind"]) raise BadManifest.new( "lane kind '#{z["kind"]}' at '$.lanes[#{i}]' was folded into 'machine' (ADR 0091) — " \ "use `kind: machine`", ) end end |
.check_owner!(owner, path) ⇒ Object
146 147 148 149 150 151 152 153 154 |
# File 'lib/textus/manifest/schema/semantics.rb', line 146 def check_owner!(owner, path) return if owner.nil? return if valid_owner?(owner) raise BadManifest.new( "invalid owner '#{owner}' at '#{path}' " \ "(expected <archetype> or <archetype>:<subject>, archetype one of: #{Textus::Role::NAMES.join(", ")})", ) end |
.check_owners!(lanes, entries) ⇒ Object
141 142 143 144 |
# File 'lib/textus/manifest/schema/semantics.rb', line 141 def check_owners!(lanes, entries) Array(lanes).each_with_index { |z, i| check_owner!(z["owner"], "$.lanes[#{i}]") } Array(entries).each_with_index { |e, i| check_owner!(e["owner"], "$.entries[#{i}]") } end |
.check_publish_block!(entry, path) ⇒ Object
123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/textus/manifest/schema/semantics.rb', line 123 def check_publish_block!(entry, path) return unless entry.is_a?(Hash) && entry.key?("publish") block = entry["publish"] if block.is_a?(Hash) raise BadManifest.new( "publish: at '#{path}.publish' must be a list of targets (ADR 0094); the map form was retired.", ) end raise BadManifest.new("publish: must be a list of targets at '#{path}.publish'") unless block.is_a?(Array) block.each_with_index do |t, i| raise BadManifest.new("publish target ##{i} must be a mapping at '#{path}.publish'") unless t.is_a?(Hash) walk(t, %w[to tree template inject_boot], "#{path}.publish[#{i}]") end end |
.check_retired_publish_keys!(entry, path) ⇒ Object
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
# File 'lib/textus/manifest/schema/semantics.rb', line 76 def check_retired_publish_keys!(entry, path) return unless entry.is_a?(Hash) if entry.key?("publish_each") raise BadManifest.new( "publish_each was removed in 0.42.0 (ADR 0051) at '#{path}' — " \ "mirror the subtree with `publish: { tree: \"...\" }`.", ) end if entry.key?("publish_to") raise BadManifest.new( "publish_to was replaced by the publish: block in 0.43.0 (ADR 0052) at '#{path}' — " \ "use `publish: { to: [...] }`.", ) end if entry.key?("publish_tree") raise BadManifest.new( "publish_tree was replaced by the publish: block in 0.43.0 (ADR 0052) at '#{path}' — " \ "use `publish: { tree: \"...\" }`.", ) end return unless entry.key?("index_filename") raise BadManifest.new( "index_filename was removed in 0.43.0 (ADR 0053) at '#{path}'.", ) end |
.check_retired_render_keys!(entry, path) ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/textus/manifest/schema/semantics.rb', line 104 def check_retired_render_keys!(entry, path) return unless entry.is_a?(Hash) if entry.key?("template") raise BadManifest.new( "entry-level `template:` was removed at '#{path}' (ADR 0094): rendering is a " \ "publish concern — `publish: [{ to:, template: }]`.", ) end if entry.key?("inject_boot") raise BadManifest.new( "entry-level `inject_boot:` was removed at '#{path}' (ADR 0094).", ) end return unless entry.key?("provenance") raise BadManifest.new("entry-level `provenance:` was removed at '#{path}' (ADR 0094).") end |
.check_roles!(roles) ⇒ Object
23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
# File 'lib/textus/manifest/schema/semantics.rb', line 23 def check_roles!(roles) return if roles.nil? roles.each_with_index do |r, i| path = "$.roles[#{i}]" name = r["name"] unless Textus::Role::NAMES.include?(name) raise BadManifest.new( "unknown role name '#{name}' at '#{path}' (allowed: #{Textus::Role::NAMES.join(", ")})", ) end Array(r["can"]).each do |verb| next if CAPABILITIES.include?(verb) hint = %w[ingest fetch].include?(verb) ? " — the quarantine capability folded into 'converge' (ADR 0090)" : "" raise BadManifest.new( "unknown capability '#{verb}' for role '#{name}' at '#{path}' " \ "(known: #{CAPABILITIES.join(", ")})#{hint}", ) end end = roles.count { |r| Array(r["can"]).include?("author") } return if <= 1 raise BadManifest.new( "manifest declares #{} roles with the author capability; at most one is allowed", ) end |
.check_rules!(rules) ⇒ Object
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 |
# File 'lib/textus/manifest/schema/semantics.rb', line 166 def check_rules!(rules) Array(rules).each_with_index do |r, i| path = "$.rules[#{i}]" # Check retired keys BEFORE the generic walk so specific hints fire first. { "lifecycle" => "age GC moved to `retention:` rule", "materialize" => "removed (ADR 0093)" } .each do |old, hint| next unless r.is_a?(Hash) && r.key?(old) raise BadManifest.new("`#{old}:` was removed at '#{path}' (ADR 0093) — #{hint}.") end if r.is_a?(Hash) && r.key?("upkeep") raise BadManifest.new( "rule key `upkeep:` was removed (ADR 0093): move age-GC to `retention:` " \ "and production to the entry's `source:`", ) end walk(r, RULE_KEYS, path) FIELD_REGISTRY.each_value do || next unless [:sub_keys] value = r.is_a?(Hash) ? r[[:yaml_key]] : nil walk(value, [:sub_keys], "#{path}.#{[:yaml_key]}") if value.is_a?(Hash) end end end |
.check_single_machine!(raw) ⇒ Object
199 200 201 202 203 204 |
# File 'lib/textus/manifest/schema/semantics.rb', line 199 def check_single_machine!(raw) machines = Array(raw["lanes"]).select { |z| z["kind"] == "machine" }.map { |z| z["name"] } return if machines.size <= 1 raise BadManifest.new("at most one lane may declare kind: machine (found: #{machines.join(", ")})") end |
.check_single_queue!(raw) ⇒ Object
192 193 194 195 196 197 |
# File 'lib/textus/manifest/schema/semantics.rb', line 192 def check_single_queue!(raw) queues = Array(raw["lanes"]).select { |z| z["kind"] == "queue" }.map { |z| z["name"] } return if queues.size <= 1 raise BadManifest.new("at most one lane may declare kind: queue (found: #{queues.join(", ")})") end |
.valid_owner?(token) ⇒ Boolean
156 157 158 159 160 161 162 163 164 |
# File 'lib/textus/manifest/schema/semantics.rb', line 156 def valid_owner?(token) return false unless token.is_a?(String) && !token.empty? archetype, subject = token.split(":", 2) return false unless Textus::Role::NAMES.include?(archetype) return true if subject.nil? OWNER_SUBJECT_PATTERN.match?(subject) end |
.walk(hash, allowed, path) ⇒ Object
220 221 222 223 224 225 226 227 228 |
# File 'lib/textus/manifest/schema/semantics.rb', line 220 def walk(hash, allowed, path) return unless hash.is_a?(Hash) hash.each_key do |k| next if allowed.include?(k) raise BadManifest.new("unknown key '#{k}' at '#{path}'") end end |