igniter-contracts 0.5.2

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.
Files changed (64) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +239 -0
  3. data/lib/igniter/contracts/api.rb +92 -0
  4. data/lib/igniter/contracts/assembly/baseline_pack.rb +141 -0
  5. data/lib/igniter/contracts/assembly/const_pack.rb +29 -0
  6. data/lib/igniter/contracts/assembly/dsl_keyword.rb +21 -0
  7. data/lib/igniter/contracts/assembly/hook_result_policies.rb +47 -0
  8. data/lib/igniter/contracts/assembly/hook_spec.rb +73 -0
  9. data/lib/igniter/contracts/assembly/hook_specs.rb +74 -0
  10. data/lib/igniter/contracts/assembly/kernel.rb +220 -0
  11. data/lib/igniter/contracts/assembly/node_type.rb +26 -0
  12. data/lib/igniter/contracts/assembly/ordered_registry.rb +55 -0
  13. data/lib/igniter/contracts/assembly/pack.rb +13 -0
  14. data/lib/igniter/contracts/assembly/pack_manifest.rb +131 -0
  15. data/lib/igniter/contracts/assembly/path_access.rb +76 -0
  16. data/lib/igniter/contracts/assembly/profile.rb +133 -0
  17. data/lib/igniter/contracts/assembly/project_pack.rb +42 -0
  18. data/lib/igniter/contracts/assembly/registry.rb +57 -0
  19. data/lib/igniter/contracts/assembly/step_result_pack.rb +42 -0
  20. data/lib/igniter/contracts/assembly.rb +18 -0
  21. data/lib/igniter/contracts/contract.rb +135 -0
  22. data/lib/igniter/contracts/contractable.rb +288 -0
  23. data/lib/igniter/contracts/environment.rb +51 -0
  24. data/lib/igniter/contracts/errors.rb +47 -0
  25. data/lib/igniter/contracts/execution/baseline_normalizers.rb +23 -0
  26. data/lib/igniter/contracts/execution/baseline_runtime.rb +55 -0
  27. data/lib/igniter/contracts/execution/baseline_validators.rb +113 -0
  28. data/lib/igniter/contracts/execution/builder.rb +43 -0
  29. data/lib/igniter/contracts/execution/compilation_report.rb +46 -0
  30. data/lib/igniter/contracts/execution/compiled_graph.rb +21 -0
  31. data/lib/igniter/contracts/execution/compiler.rb +66 -0
  32. data/lib/igniter/contracts/execution/const_runtime.rb +15 -0
  33. data/lib/igniter/contracts/execution/diagnostics.rb +24 -0
  34. data/lib/igniter/contracts/execution/diagnostics_report.rb +40 -0
  35. data/lib/igniter/contracts/execution/diagnostics_section.rb +37 -0
  36. data/lib/igniter/contracts/execution/effect_invocation.rb +26 -0
  37. data/lib/igniter/contracts/execution/execution_request.rb +28 -0
  38. data/lib/igniter/contracts/execution/execution_result.rb +32 -0
  39. data/lib/igniter/contracts/execution/inline_executor.rb +19 -0
  40. data/lib/igniter/contracts/execution/mutable_named_values.rb +52 -0
  41. data/lib/igniter/contracts/execution/named_values.rb +48 -0
  42. data/lib/igniter/contracts/execution/operation.rb +42 -0
  43. data/lib/igniter/contracts/execution/runtime.rb +43 -0
  44. data/lib/igniter/contracts/execution/step_result.rb +51 -0
  45. data/lib/igniter/contracts/execution/step_result_diagnostics.rb +35 -0
  46. data/lib/igniter/contracts/execution/step_result_runtime.rb +51 -0
  47. data/lib/igniter/contracts/execution/step_result_validators.rb +44 -0
  48. data/lib/igniter/contracts/execution/structured_dump.rb +49 -0
  49. data/lib/igniter/contracts/execution/validation_finding.rb +28 -0
  50. data/lib/igniter/contracts/execution/validation_report.rb +46 -0
  51. data/lib/igniter/contracts/execution.rb +28 -0
  52. data/lib/igniter/contracts.rb +54 -0
  53. data/lib/igniter/lang/backend.rb +19 -0
  54. data/lib/igniter/lang/backends/ruby.rb +42 -0
  55. data/lib/igniter/lang/diagnostic_payload.rb +174 -0
  56. data/lib/igniter/lang/metadata_carrier_manifest.rb +112 -0
  57. data/lib/igniter/lang/metadata_manifest.rb +128 -0
  58. data/lib/igniter/lang/receipt_payload.rb +152 -0
  59. data/lib/igniter/lang/schema_compatibility_diagnostic.rb +300 -0
  60. data/lib/igniter/lang/types.rb +84 -0
  61. data/lib/igniter/lang/verification_report.rb +226 -0
  62. data/lib/igniter/lang.rb +27 -0
  63. data/lib/igniter-contracts.rb +3 -0
  64. metadata +103 -0
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Lang
5
+ class ReceiptPayload
6
+ SEMANTICS = {
7
+ report_only: true,
8
+ runtime_enforced: false,
9
+ execution_authorized: false,
10
+ operation_execution_authorized: false,
11
+ external_bridge_authorized: false,
12
+ provider_call_authorized: false,
13
+ real_data_export_authorized: false,
14
+ ledger_core: false
15
+ }.freeze
16
+
17
+ DEFAULT_REDACTION_POLICY = {
18
+ raw_ref_export: false,
19
+ hash_source_refs: true,
20
+ redacted_ref_kinds: []
21
+ }.freeze
22
+
23
+ RAW_REF_KEYS = %i[raw_ref raw_refs raw_source_ref raw_source_refs].freeze
24
+ RAW_REF_PREFIX = "raw:"
25
+
26
+ attr_reader :receipt_id,
27
+ :profile,
28
+ :payload,
29
+ :evidence_links,
30
+ :redaction_policy,
31
+ :metadata
32
+
33
+ def initialize(
34
+ receipt_id:,
35
+ profile:,
36
+ payload: {},
37
+ evidence_links: {},
38
+ redaction_policy: {},
39
+ metadata: {}
40
+ )
41
+ @receipt_id = require_value(:receipt_id, receipt_id)
42
+ @profile = require_value(:profile, profile)
43
+ @payload = deep_freeze(normalize_hash(payload, :payload))
44
+ @evidence_links = deep_freeze(normalize_hash(evidence_links, :evidence_links))
45
+ @redaction_policy = deep_freeze(normalize_redaction_policy(redaction_policy))
46
+ @metadata = deep_freeze(normalize_hash(metadata, :metadata))
47
+ reject_raw_refs!
48
+ freeze
49
+ end
50
+
51
+ def report_only?
52
+ true
53
+ end
54
+
55
+ def runtime_enforced?
56
+ false
57
+ end
58
+
59
+ def to_h
60
+ {
61
+ receipt_id: receipt_id,
62
+ profile: profile,
63
+ payload: payload,
64
+ evidence_links: evidence_links,
65
+ redaction_policy: redaction_policy,
66
+ semantics: SEMANTICS,
67
+ metadata: metadata
68
+ }
69
+ end
70
+
71
+ private
72
+
73
+ def require_value(name, value)
74
+ raise ArgumentError, "#{name} is required" if value.nil?
75
+
76
+ value
77
+ end
78
+
79
+ def normalize_redaction_policy(value)
80
+ policy = DEFAULT_REDACTION_POLICY.merge(normalize_hash(value, :redaction_policy))
81
+ raise ArgumentError, "redaction_policy.raw_ref_export must be true or false" unless [true, false].include?(policy[:raw_ref_export])
82
+
83
+ raise ArgumentError, "redaction_policy.raw_ref_export true is not supported in v0" if policy[:raw_ref_export]
84
+
85
+ policy[:redacted_ref_kinds] = Array(policy[:redacted_ref_kinds]).map(&:to_s)
86
+ policy
87
+ end
88
+
89
+ def reject_raw_refs!
90
+ raw_path = find_raw_ref(
91
+ payload: payload,
92
+ evidence_links: evidence_links,
93
+ metadata: metadata
94
+ )
95
+ raise ArgumentError, "raw refs are not allowed in receipt payloads at #{raw_path}" if raw_path
96
+ end
97
+
98
+ def find_raw_ref(value, path = [])
99
+ case value
100
+ when Hash
101
+ value.each do |key, entry|
102
+ normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
103
+ return (path + [normalized_key]).join(".") if RAW_REF_KEYS.include?(normalized_key)
104
+
105
+ nested = find_raw_ref(entry, path + [normalized_key])
106
+ return nested if nested
107
+ end
108
+ when Array
109
+ value.each_with_index do |entry, index|
110
+ nested = find_raw_ref(entry, path + [index])
111
+ return nested if nested
112
+ end
113
+ when String
114
+ return path.join(".") if value.start_with?(RAW_REF_PREFIX)
115
+ end
116
+
117
+ nil
118
+ end
119
+
120
+ def normalize_hash(value, name)
121
+ raise ArgumentError, "#{name} must be a hash" unless value.respond_to?(:to_h)
122
+
123
+ value.to_h.each_with_object({}) do |(key, entry), hash|
124
+ normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
125
+ hash[normalized_key] = normalize_value(entry)
126
+ end
127
+ end
128
+
129
+ def normalize_value(value)
130
+ case value
131
+ when Hash
132
+ normalize_hash(value, :value)
133
+ when Array
134
+ value.map { |entry| normalize_value(entry) }
135
+ else
136
+ value
137
+ end
138
+ end
139
+
140
+ def deep_freeze(value)
141
+ case value
142
+ when Array
143
+ value.map { |entry| deep_freeze(entry) }.freeze
144
+ when Hash
145
+ value.transform_values { |entry| deep_freeze(entry) }.freeze
146
+ else
147
+ value.freeze
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,300 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Lang
5
+ class SchemaCompatibilityDiagnostic
6
+ SEMANTICS = {
7
+ report_only: true,
8
+ runtime_enforced: false,
9
+ migration_execution_authorized: false,
10
+ ledger_core: false
11
+ }.freeze
12
+
13
+ REQUIRED_EVIDENCE_LINKS = %i[
14
+ compatibility_report_ref
15
+ semantic_image_ref
16
+ loaded_schema_descriptor_ref
17
+ ].freeze
18
+
19
+ REQUIRED_MIGRATION_PROFILE_KEYS = %i[
20
+ migration_receipt_ref
21
+ replaces_image_id
22
+ replacement_semantic_image_ref
23
+ replacement_schema_fingerprint
24
+ loaded_schema_fingerprint
25
+ migration_chain
26
+ replacement_image_lifecycle
27
+ migration_receipt_lifecycle
28
+ packet_links
29
+ post_migration_report_ref
30
+ post_migration_schema_decision
31
+ post_migration_compatibility_decision
32
+ ].freeze
33
+
34
+ REQUIRED_PACKET_LINKS = %i[
35
+ replaces
36
+ caused_by
37
+ produced_by
38
+ produced_in
39
+ has_supersedes
40
+ ].freeze
41
+
42
+ DECISIONS = %i[trusted provisional migrating blocked].freeze
43
+
44
+ attr_reader :diagnostic_id,
45
+ :contract_ref,
46
+ :old_schema_version,
47
+ :new_schema_version,
48
+ :old_schema_fingerprint,
49
+ :new_schema_fingerprint,
50
+ :schema_check_outcome,
51
+ :migration_available,
52
+ :compatibility_decision,
53
+ :evidence_links,
54
+ :migration_ref,
55
+ :migration_profile,
56
+ :profile_cases,
57
+ :metadata
58
+
59
+ def initialize(
60
+ diagnostic_id:,
61
+ contract_ref:,
62
+ old_schema_version:,
63
+ new_schema_version:,
64
+ old_schema_fingerprint:,
65
+ new_schema_fingerprint:,
66
+ schema_check_outcome:,
67
+ migration_available:,
68
+ compatibility_decision:,
69
+ evidence_links:,
70
+ migration_ref: nil,
71
+ migration_profile: nil,
72
+ metadata: {}
73
+ )
74
+ @diagnostic_id = require_value(:diagnostic_id, diagnostic_id)
75
+ @contract_ref = require_value(:contract_ref, contract_ref)
76
+ @old_schema_version = require_value(:old_schema_version, old_schema_version)
77
+ @new_schema_version = require_value(:new_schema_version, new_schema_version)
78
+ @old_schema_fingerprint = require_value(:old_schema_fingerprint, old_schema_fingerprint)
79
+ @new_schema_fingerprint = require_value(:new_schema_fingerprint, new_schema_fingerprint)
80
+ @schema_check_outcome = normalize_decision(:schema_check_outcome, schema_check_outcome)
81
+ @migration_available = normalize_boolean(:migration_available, migration_available)
82
+ @compatibility_decision = normalize_decision(:compatibility_decision, compatibility_decision)
83
+ @evidence_links = normalize_evidence_links(evidence_links)
84
+ @migration_ref = migration_ref
85
+ @migration_profile = normalize_migration_profile(migration_profile)
86
+ validate_migration_evidence!
87
+ @profile_cases = build_profile_cases.freeze
88
+ @metadata = deep_freeze(normalize_hash(metadata, :metadata))
89
+ freeze
90
+ end
91
+
92
+ def report_only?
93
+ true
94
+ end
95
+
96
+ def runtime_enforced?
97
+ false
98
+ end
99
+
100
+ def status
101
+ return :blocked if blocked?
102
+ return :migrating if schema_check_outcome == :migrating || compatibility_decision == :migrating
103
+ return :provisional if schema_check_outcome == :provisional || compatibility_decision == :provisional
104
+
105
+ :trusted
106
+ end
107
+
108
+ def blocked?
109
+ schema_check_outcome == :blocked ||
110
+ compatibility_decision == :blocked ||
111
+ profile_cases.any? { |entry| entry.fetch(:status) == :blocked }
112
+ end
113
+
114
+ def to_h
115
+ payload = {
116
+ diagnostic_id: diagnostic_id,
117
+ contract_ref: contract_ref,
118
+ old_schema_version: old_schema_version,
119
+ new_schema_version: new_schema_version,
120
+ old_schema_fingerprint: old_schema_fingerprint,
121
+ new_schema_fingerprint: new_schema_fingerprint,
122
+ schema_check_outcome: schema_check_outcome,
123
+ migration_available: migration_available,
124
+ compatibility_decision: compatibility_decision,
125
+ status: status,
126
+ evidence_links: evidence_links,
127
+ profile_cases: profile_cases,
128
+ semantics: SEMANTICS,
129
+ metadata: metadata
130
+ }
131
+ payload[:migration_ref] = migration_ref if migration_ref
132
+ payload[:migration_profile] = migration_profile if migration_profile
133
+ payload
134
+ end
135
+
136
+ private
137
+
138
+ def require_value(name, value)
139
+ raise ArgumentError, "#{name} is required" if value.nil?
140
+
141
+ value
142
+ end
143
+
144
+ def normalize_decision(name, value)
145
+ decision = require_value(name, value).to_sym
146
+ return decision if DECISIONS.include?(decision)
147
+
148
+ raise ArgumentError, "#{name} must be one of #{DECISIONS.join(", ")}"
149
+ end
150
+
151
+ def normalize_boolean(name, value)
152
+ return value if [true, false].include?(value)
153
+
154
+ raise ArgumentError, "#{name} must be true or false"
155
+ end
156
+
157
+ def normalize_evidence_links(value)
158
+ links = normalize_hash(value, :evidence_links)
159
+ REQUIRED_EVIDENCE_LINKS.each do |key|
160
+ raise ArgumentError, "evidence_links.#{key} is required" if links[key].nil?
161
+ end
162
+ deep_freeze(links)
163
+ end
164
+
165
+ def validate_migration_evidence!
166
+ return unless migration_available
167
+ return if migration_ref || evidence_links[:migration_descriptor_ref]
168
+
169
+ raise ArgumentError, "migration_available requires migration_ref or evidence_links.migration_descriptor_ref"
170
+ end
171
+
172
+ def normalize_migration_profile(value)
173
+ return nil if value.nil?
174
+
175
+ profile = normalize_hash(value, :migration_profile)
176
+ REQUIRED_MIGRATION_PROFILE_KEYS.each do |key|
177
+ raise ArgumentError, "migration_profile.#{key} is required" if profile[key].nil?
178
+ end
179
+
180
+ profile[:post_migration_schema_decision] =
181
+ normalize_decision(:post_migration_schema_decision, profile[:post_migration_schema_decision])
182
+ profile[:post_migration_compatibility_decision] =
183
+ normalize_decision(:post_migration_compatibility_decision, profile[:post_migration_compatibility_decision])
184
+ profile[:packet_links] = normalize_packet_links(profile[:packet_links])
185
+ deep_freeze(profile)
186
+ end
187
+
188
+ def normalize_packet_links(value)
189
+ packet_links = normalize_hash(value, :packet_links)
190
+ REQUIRED_PACKET_LINKS.each do |key|
191
+ raise ArgumentError, "migration_profile.packet_links.#{key} is required" if packet_links[key].nil?
192
+ end
193
+ packet_links
194
+ end
195
+
196
+ def build_profile_cases
197
+ return [] unless migration_profile
198
+
199
+ [
200
+ profile_case("P-1", "migration_receipt_ref", receipt_ref_present?),
201
+ profile_case("P-2", "replaces_image_id", value_present?(migration_profile[:replaces_image_id])),
202
+ profile_case("P-3", "packet_links.replaces", packet_ref_matches?(:replaces, :replaces_image_id)),
203
+ profile_case("P-4", "packet_links.caused_by", packet_ref_matches?(:caused_by, :migration_receipt_ref)),
204
+ profile_case("P-5", "packet_links.has_supersedes", !migration_profile.dig(:packet_links, :has_supersedes)),
205
+ profile_case("P-6", "replacement_schema_fingerprint", replacement_fingerprint_matches?),
206
+ profile_case("P-7", "post_migration_schema_decision", post_migration_schema_trusted?),
207
+ profile_case("P-8", "post_migration_compatibility_decision", post_migration_compatibility_trusted?),
208
+ profile_case("P-9", "migration_chain", migration_profile[:migration_chain] == []),
209
+ p10_case
210
+ ].map { |entry| deep_freeze(entry) }
211
+ end
212
+
213
+ def profile_case(code, field, passed)
214
+ {
215
+ code: code,
216
+ field: field.to_sym,
217
+ status: passed ? :trusted : :blocked
218
+ }
219
+ end
220
+
221
+ def p10_case
222
+ passed = !wrong_replacement_fingerprint?
223
+ case_entry = profile_case("P-10", "oof_code", passed || oof_mr3_blocked?)
224
+ case_entry[:status] = :blocked if wrong_replacement_fingerprint?
225
+ case_entry[:oof_code] = "OOF-MR3" if wrong_replacement_fingerprint?
226
+ case_entry
227
+ end
228
+
229
+ def receipt_ref_present?
230
+ receipt_ref = migration_profile[:migration_receipt_ref]
231
+ return false unless value_present?(receipt_ref)
232
+
233
+ linked_receipt = evidence_links[:migration_receipt_ref]
234
+ linked_receipt.nil? || linked_receipt == receipt_ref
235
+ end
236
+
237
+ def packet_ref_matches?(link_key, profile_key)
238
+ migration_profile.dig(:packet_links, link_key) == migration_profile[profile_key]
239
+ end
240
+
241
+ def replacement_fingerprint_matches?
242
+ migration_profile[:replacement_schema_fingerprint] == migration_profile[:loaded_schema_fingerprint]
243
+ end
244
+
245
+ def wrong_replacement_fingerprint?
246
+ !replacement_fingerprint_matches?
247
+ end
248
+
249
+ def post_migration_schema_trusted?
250
+ migration_profile[:post_migration_schema_decision] == :trusted
251
+ end
252
+
253
+ def post_migration_compatibility_trusted?
254
+ migration_profile[:post_migration_compatibility_decision] == :trusted
255
+ end
256
+
257
+ def oof_mr3_blocked?
258
+ migration_profile[:oof_code] == "OOF-MR3" &&
259
+ migration_profile[:post_migration_schema_decision] == :blocked &&
260
+ compatibility_decision == :blocked &&
261
+ schema_check_outcome == :blocked
262
+ end
263
+
264
+ def value_present?(value)
265
+ !value.nil? && value != ""
266
+ end
267
+
268
+ def normalize_hash(value, name)
269
+ raise ArgumentError, "#{name} must be a hash" unless value.respond_to?(:to_h)
270
+
271
+ value.to_h.each_with_object({}) do |(key, entry), hash|
272
+ normalized_key = key.respond_to?(:to_sym) ? key.to_sym : key
273
+ hash[normalized_key] = normalize_value(entry)
274
+ end
275
+ end
276
+
277
+ def normalize_value(value)
278
+ case value
279
+ when Hash
280
+ normalize_hash(value, :value)
281
+ when Array
282
+ value.map { |entry| normalize_value(entry) }
283
+ else
284
+ value
285
+ end
286
+ end
287
+
288
+ def deep_freeze(value)
289
+ case value
290
+ when Array
291
+ value.map { |entry| deep_freeze(entry) }.freeze
292
+ when Hash
293
+ value.transform_values { |entry| deep_freeze(entry) }.freeze
294
+ else
295
+ value.freeze
296
+ end
297
+ end
298
+ end
299
+ end
300
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Lang
5
+ module Types
6
+ class Descriptor
7
+ attr_reader :kind, :of, :dimensions, :metadata
8
+
9
+ def initialize(kind:, of:, dimensions: {}, metadata: {})
10
+ @kind = kind.to_sym
11
+ @of = of
12
+ @dimensions = dimensions.transform_keys(&:to_sym).freeze
13
+ @metadata = metadata.transform_keys(&:to_sym).freeze
14
+ freeze
15
+ end
16
+
17
+ def to_h
18
+ {
19
+ kind: kind,
20
+ of: serialize_type(of),
21
+ dimensions: serialize_dimensions,
22
+ metadata: metadata
23
+ }
24
+ end
25
+
26
+ def inspect
27
+ "#<#{self.class.name} #{to_h.inspect}>"
28
+ end
29
+
30
+ def ==(other)
31
+ other.is_a?(self.class) && to_h == other.to_h
32
+ end
33
+ alias eql? ==
34
+
35
+ def hash
36
+ to_h.hash
37
+ end
38
+
39
+ private
40
+
41
+ def serialize_dimensions
42
+ dimensions.to_h do |name, value|
43
+ [name, serialize_type(value)]
44
+ end
45
+ end
46
+
47
+ def serialize_type(value)
48
+ case value
49
+ when Descriptor
50
+ value.to_h
51
+ when Module
52
+ value.name
53
+ else
54
+ value.respond_to?(:to_h) ? value.to_h : value
55
+ end
56
+ end
57
+ end
58
+
59
+ class History
60
+ def self.[](of)
61
+ Descriptor.new(kind: :history, of: of)
62
+ end
63
+ end
64
+
65
+ class BiHistory
66
+ def self.[](of)
67
+ Descriptor.new(kind: :bi_history, of: of)
68
+ end
69
+ end
70
+
71
+ class OLAPPoint
72
+ def self.[](of, dimensions = {})
73
+ Descriptor.new(kind: :olap_point, of: of, dimensions: dimensions)
74
+ end
75
+ end
76
+
77
+ class Forecast
78
+ def self.[](of)
79
+ Descriptor.new(kind: :forecast, of: of)
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end