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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a3d6daf13d97a2824269d7dc2d255cf3a7844a36e83f7faec39a5e40000d924f
4
+ data.tar.gz: 6995a02e4283062ed8ad29cc4414f46a90ac184759db64d4bb68b7b2858fbd3d
5
+ SHA512:
6
+ metadata.gz: 8e2aaf2deb50cfc7db0befaa7e4d25e18312b490034fc1052bcc03143b4b967d9c6f76c989108232ac9a31b61e32542a6fd24babc7b79703cdc8ede6102b3a4e
7
+ data.tar.gz: 11fddc0cfd6811517dfce9a024c468403abafd9b98c1e68ca426ba1b2bce57e735cef32038a9349049d5d8e72ec036a85b68262483e29e7d592ea740ba665add
data/README.md ADDED
@@ -0,0 +1,239 @@
1
+ # igniter-contracts
2
+
3
+ Public embedded kernel package for Igniter:
4
+
5
+ - contracts and DSL
6
+ - graph model and compiler
7
+ - execution/runtime primitives
8
+ - diagnostics, events, and core extension seams
9
+
10
+ Primary entrypoints:
11
+
12
+ - `require "igniter-contracts"`
13
+ - `require "igniter/contracts"`
14
+ - `require "igniter/lang"` for the additive Lang foundation
15
+
16
+ Current proof path:
17
+
18
+ - [Contract Class DSL](../../docs/guide/contract-class-dsl.md)
19
+ - [Igniter Lang Foundation](../../docs/guide/igniter-lang-foundation.md)
20
+ - [Getting Started](../../docs/guide/getting-started.md)
21
+
22
+ Current implementation focus:
23
+
24
+ - `Kernel`
25
+ - `Profile`
26
+ - `Environment`
27
+ - `Registry` / `OrderedRegistry`
28
+ - `Pack` / `BaselinePack`
29
+
30
+ ## Intended Use
31
+
32
+ Use `igniter-contracts` when Igniter is embedded inside another host such as:
33
+
34
+ - Rails applications
35
+ - scripts and jobs
36
+ - existing service runtimes
37
+
38
+ This package is the lower-layer dependency that other runtime shapes should
39
+ build on top of. It should not pull:
40
+
41
+ - application hosting
42
+ - server/runtime containers
43
+ - cluster coordination
44
+ - web rendering or schema-rendering packages
45
+
46
+ It also should not depend on the legacy core implementation. Legacy code remains
47
+ reference/parity material during the rewrite, not the public architecture.
48
+
49
+ ## Current Shape
50
+
51
+ `igniter-contracts` starts from its own internal primitives:
52
+
53
+ - registries
54
+ - kernel/profile lifecycle
55
+ - environment sugar over a finalized profile
56
+ - packs
57
+ - a tiny baseline pack
58
+
59
+ ## Ergonomics
60
+
61
+ You can still work directly with `Kernel` and `Profile`, but the public facade
62
+ now gives two equal authoring paths.
63
+
64
+ For the low-level embedded kernel API, compile or run a block directly:
65
+
66
+ ```ruby
67
+ environment = Igniter::Contracts.with
68
+
69
+ result = environment.run(inputs: {}) do
70
+ const :tax_rate, 0.2
71
+ output :tax_rate
72
+ end
73
+ ```
74
+
75
+ For app code and human-edited contract files, use the class DSL:
76
+
77
+ ```ruby
78
+ class PriceContract < Igniter::Contract
79
+ define do
80
+ input :order_total, type: :numeric
81
+ input :country, type: :string
82
+
83
+ compute :gross_total, depends_on: %i[order_total country] do |order_total:, country:|
84
+ order_total * (country == "UA" ? 1.2 : 1.0)
85
+ end
86
+
87
+ output :gross_total
88
+ end
89
+ end
90
+
91
+ contract = PriceContract.new(order_total: 100, country: "UA")
92
+ contract.result.gross_total
93
+ contract.update_inputs(order_total: 150)
94
+ contract.output(:gross_total)
95
+ ```
96
+
97
+ Compute nodes may also use `call:` for service objects or callable classes:
98
+
99
+ ```ruby
100
+ compute :gross_total, depends_on: %i[order_total country], call: Pricing::GrossTotal
101
+ ```
102
+
103
+ Contractable services expose a small service protocol to compute nodes:
104
+
105
+ ```ruby
106
+ class BodyBatteryScorer
107
+ include Igniter::Contracts::Contractable
108
+
109
+ contractable :call do
110
+ role :migration_candidate
111
+ stage :shadowed
112
+ meta :domain, :wellness
113
+ input :sleep_hours
114
+ input :training_minutes
115
+ output :score
116
+ end
117
+
118
+ def call(sleep_hours:, training_minutes:)
119
+ sleep_score = observe(:sleep_score) { [[sleep_hours / 8.0, 1.0].min * 40, 0].max }
120
+ training_score = observe(:training_score) { training_minutes <= 45 ? 10 : 2 }
121
+
122
+ success(score: [[45 + sleep_score + training_score, 100].min, 0].max.round)
123
+ end
124
+ end
125
+
126
+ compute :body_battery,
127
+ depends_on: %i[sleep_hours training_minutes],
128
+ using: BodyBatteryScorer
129
+ ```
130
+
131
+ `using:` returns a normalized payload with `outputs`, `observations`, `error`,
132
+ and `success`. The service owns its internal implementation; Igniter owns the
133
+ graph boundary and result protocol.
134
+
135
+ Declared `input` and `output` names are validated at the protocol boundary.
136
+ Missing inputs or missing declared outputs return a failure payload instead of
137
+ raising through the caller. Extra outputs are allowed for now, which keeps
138
+ contractable services useful during migration and discovery.
139
+
140
+ `role`, `stage`, and `meta` travel in result metadata. Host layers such as
141
+ `igniter-embed` can use those fields as defaults for observation and migration
142
+ wrappers.
143
+
144
+ Use `output:` when the graph should expose one named service output as the
145
+ compute value:
146
+
147
+ ```ruby
148
+ compute :score,
149
+ depends_on: %i[sleep_hours training_minutes],
150
+ using: BodyBatteryScorer,
151
+ output: :score
152
+ ```
153
+
154
+ Additional helpers:
155
+
156
+ - `Igniter::Contracts.build_kernel(*packs)`
157
+ - `Igniter::Contracts.build_profile(*packs)`
158
+ - `Igniter::Contracts.with(*packs)`
159
+
160
+ ## Verification
161
+
162
+ Use focused package specs and runnable examples as the current proof path:
163
+
164
+ ```bash
165
+ bundle exec rspec packages/igniter-contracts/spec spec/current
166
+ ruby examples/run.rb smoke
167
+ ```
168
+
169
+ Focused contracts/lang checks:
170
+
171
+ ```bash
172
+ ruby examples/run.rb run contracts/class_pricing
173
+ ruby examples/run.rb run contracts/class_callable
174
+ ruby examples/run.rb run contracts/embed_class_registration
175
+ ruby examples/run.rb run contracts/contractable_shadow
176
+ ruby examples/run.rb run contracts/step_result
177
+ ruby examples/run.rb run contracts/lang_foundation
178
+ ```
179
+
180
+ For narrow changes, run RuboCop on the changed files. Full-project RuboCop
181
+ currently includes pre-existing archived/research offenses, so changed-file
182
+ lint is the practical gate for focused package slices; this is a caveat, not a
183
+ quality target.
184
+
185
+ ## Igniter Lang Foundation
186
+
187
+ `require "igniter/lang"` loads a small contracts-facing Lang namespace.
188
+ Currently this is an additive reference surface over the existing contracts
189
+ runtime:
190
+
191
+ - `Igniter::Lang.ruby_backend` wraps current compile, execute, diagnose, and
192
+ verify APIs.
193
+ - `History`, `BiHistory`, `OLAPPoint`, and `Forecast` are immutable
194
+ definition-time descriptors that can be attached as operation metadata.
195
+ - `VerificationReport` is read-only and follows current compilation findings.
196
+ Its `metadata` hash can carry generic report-only sections such as
197
+ `diagnostics`, `receipts`, `model_validity_reports`,
198
+ `scenario_comparison_reports`, and `review_receipts`.
199
+ - `MetadataManifest` reports declared `type:`, `return_type:`, `deadline:`,
200
+ and `wcet:` metadata.
201
+ - `DiagnosticPayload` is a generic report-only carrier for metadata-only
202
+ diagnostic hashes from projection, pipeline, availability, or future
203
+ operation profiles. It includes a redaction policy and rejects raw refs in v0.
204
+ - `ReceiptPayload` is a generic report-only carrier for metadata-only receipts
205
+ such as request, execution, idempotency, or external bridge receipt shapes.
206
+ It includes the same redaction defaults and does not authorize execution.
207
+ - `SchemaCompatibilityDiagnostic` is an immutable report-only compatibility
208
+ value object with required evidence links and an optional single-hop
209
+ `migration_profile`.
210
+
211
+ Metadata manifest fields are declared, not enforced. `return_type`, `deadline`,
212
+ and `wcet` appear in reports with `enforced: false`; they do not add runtime
213
+ checks, warnings, deadline monitoring, or `ExecutionResult` changes.
214
+ Verification metadata carrier sections are opaque serializable hashes. When
215
+ present, they require an explicit `redaction_policy`, force
216
+ `raw_ref_export: false`, reject raw refs, and produce a `carrier_manifest` with
217
+ section counts, profile names, and report-only enforcement flags. Future opaque
218
+ sections can be carried under `metadata[:custom_sections]`; OSINT-style
219
+ profiles and future compiler-pipeline proof profiles remain metadata-only
220
+ custom sections rather than public package API, including separated compiler
221
+ pass reports.
222
+ Generic diagnostic payloads are serialized through `VerificationReport` as
223
+ `diagnostic_payloads`; generic receipt payloads use `receipt_payloads`. Neither
224
+ surface authorizes package adapters, real data export, provider bridges,
225
+ operation execution, readiness checks, Ledger integration, or runtime
226
+ enforcement.
227
+ Schema compatibility diagnostics follow the same boundary:
228
+ `report_only: true`, `runtime_enforced: false`, and no migration execution.
229
+ The optional migration profile only serializes evidence, including blocked
230
+ OOF-MR3 wrong-fingerprint cases.
231
+
232
+ Try the compact proof:
233
+
234
+ ```bash
235
+ ruby examples/contracts/lang_foundation.rb
236
+ ```
237
+
238
+ See [Igniter Lang Foundation](../../docs/guide/igniter-lang-foundation.md) for
239
+ the short guide.
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ class << self
6
+ def build_kernel(*packs)
7
+ install_packs(Assembly::Kernel.new.install(BaselinePack), packs)
8
+ end
9
+
10
+ def build_profile(*packs)
11
+ build_kernel(*packs).finalize
12
+ end
13
+
14
+ def with(*packs)
15
+ Environment.new(profile: build_profile(*packs))
16
+ end
17
+
18
+ def default_kernel
19
+ @default_kernel ||= build_kernel
20
+ end
21
+
22
+ def default_profile
23
+ @default_profile ||= default_kernel.finalize
24
+ end
25
+
26
+ def compile(profile: default_profile, &block)
27
+ Execution::Compiler.compile(profile: profile, &block)
28
+ end
29
+
30
+ def validation_report(profile: default_profile, &block)
31
+ Execution::Compiler.validation_report(profile: profile, &block)
32
+ end
33
+
34
+ def compilation_report(profile: default_profile, &block)
35
+ Execution::Compiler.compilation_report(profile: profile, &block)
36
+ end
37
+
38
+ def execute(compiled_graph, inputs:, profile: default_profile)
39
+ execute_with(:inline, compiled_graph, inputs: inputs, profile: profile)
40
+ end
41
+
42
+ def execute_with(executor_name, compiled_graph, inputs:, profile: default_profile, runtime: Execution::Runtime)
43
+ executor = profile.executor(executor_name)
44
+ hook_spec = Assembly::HookSpecs.fetch(:executors)
45
+ invocation = Execution::ExecutionRequest.new(
46
+ compiled_graph: compiled_graph,
47
+ inputs: inputs,
48
+ profile: profile,
49
+ runtime: runtime
50
+ )
51
+ result = executor.call(invocation: invocation)
52
+ hook_spec.validate_result!(executor_name, result)
53
+ rescue KeyError
54
+ raise UnknownExecutorError, "unknown executor #{executor_name}"
55
+ end
56
+
57
+ def diagnose(result, profile: default_profile)
58
+ Execution::Diagnostics.build_report(result: result, profile: profile)
59
+ end
60
+
61
+ def apply_effect(effect_name, payload:, context: {}, profile: default_profile)
62
+ effect = profile.effect(effect_name)
63
+ invocation = Execution::EffectInvocation.new(
64
+ payload: payload,
65
+ context: context,
66
+ profile: profile
67
+ )
68
+ effect.call(invocation: invocation)
69
+ rescue KeyError
70
+ raise UnknownEffectError, "unknown effect #{effect_name}"
71
+ end
72
+
73
+ def reset_defaults!
74
+ @default_kernel = nil
75
+ @default_profile = nil
76
+ end
77
+
78
+ private
79
+
80
+ def install_packs(kernel, packs)
81
+ normalize_packs(packs).each do |pack|
82
+ kernel.install(pack)
83
+ end
84
+ kernel
85
+ end
86
+
87
+ def normalize_packs(packs)
88
+ packs.flatten.compact.uniq
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module BaselinePack
7
+ module_function
8
+
9
+ def manifest
10
+ PackManifest.new(
11
+ name: :baseline,
12
+ node_contracts: BASELINE_NODE_KINDS.keys.map { |kind| PackManifest.node(kind) },
13
+ registry_contracts: [
14
+ PackManifest.normalizer(:normalize_operation_attributes),
15
+ PackManifest.validator(:uniqueness),
16
+ PackManifest.validator(:outputs),
17
+ PackManifest.validator(:dependencies),
18
+ PackManifest.validator(:callables),
19
+ PackManifest.validator(:effect_dependencies),
20
+ PackManifest.validator(:effect_payload_builders),
21
+ PackManifest.validator(:effect_adapters),
22
+ PackManifest.validator(:types),
23
+ PackManifest.executor(:inline),
24
+ PackManifest.diagnostic(:baseline_summary)
25
+ ]
26
+ )
27
+ end
28
+
29
+ BASELINE_NODE_KINDS = {
30
+ input: NodeType.new(kind: :input, metadata: { category: :data }),
31
+ const: NodeType.new(kind: :const, metadata: { category: :value }),
32
+ compute: NodeType.new(kind: :compute, metadata: { category: :data }),
33
+ effect: NodeType.new(kind: :effect, metadata: { category: :effect }),
34
+ output: NodeType.new(kind: :output, metadata: { category: :terminal })
35
+ }.freeze
36
+
37
+ BASELINE_DSL_KEYWORDS = {
38
+ input: DslKeyword.new(:input, lambda { |name, builder:, **attributes|
39
+ builder.add_operation(kind: :input, name: name, **attributes)
40
+ }),
41
+ const: DslKeyword.new(:const, lambda { |name, value, builder:|
42
+ builder.add_operation(kind: :const, name: name, value: value)
43
+ }),
44
+ compute: DslKeyword.new(:compute, lambda { |name, builder:, **attributes, &block|
45
+ normalized_attributes = attributes.dup
46
+ normalized_attributes[:callable] = normalized_attributes.delete(:call) if normalized_attributes.key?(:call)
47
+ if normalized_attributes.key?(:using)
48
+ target = normalized_attributes.delete(:using)
49
+ output_name = normalized_attributes.delete(:output)&.to_sym
50
+ normalized_attributes[:callable] = lambda do |**values|
51
+ payload = Contractable.invoke(target, **values).to_h
52
+ output_name && payload.fetch(:success) ? payload.fetch(:outputs).fetch(output_name) : payload
53
+ end
54
+ end
55
+ normalized_attributes[:callable] = block if block
56
+ builder.add_operation(kind: :compute, name: name, **normalized_attributes)
57
+ }),
58
+ effect: DslKeyword.new(:effect, lambda { |name, using:, builder:, callable: nil, **attributes, &block|
59
+ normalized_attributes = attributes.dup
60
+ normalized_attributes[:using] = using.to_sym
61
+ normalized_attributes[:callable] = block if block
62
+ normalized_attributes[:callable] = callable if callable && !block
63
+ builder.add_operation(kind: :effect, name: name, **normalized_attributes)
64
+ }),
65
+ output: DslKeyword.new(:output, lambda { |name, builder:, **attributes|
66
+ builder.add_operation(kind: :output, name: name, **attributes)
67
+ })
68
+ }.freeze
69
+
70
+ BASELINE_DIAGNOSTICS = {
71
+ baseline_summary: Module.new do
72
+ module_function
73
+
74
+ def augment(report:, result:, profile:) # rubocop:disable Lint/UnusedMethodArgument
75
+ report.add_section(:baseline_summary, {
76
+ outputs: result.outputs.keys.sort,
77
+ state: result.state.keys.sort
78
+ })
79
+ end
80
+ end
81
+ }.freeze
82
+
83
+ def install_into(kernel)
84
+ install_nodes(kernel)
85
+ install_dsl_keywords(kernel)
86
+ install_normalizers(kernel)
87
+ install_validators(kernel)
88
+ install_runtime_handlers(kernel)
89
+ install_executors(kernel)
90
+ install_diagnostics(kernel)
91
+ kernel
92
+ end
93
+
94
+ def install_nodes(kernel)
95
+ BASELINE_NODE_KINDS.each do |key, value|
96
+ kernel.nodes.register(key, value)
97
+ end
98
+ end
99
+
100
+ def install_dsl_keywords(kernel)
101
+ BASELINE_DSL_KEYWORDS.each do |key, value|
102
+ kernel.dsl_keywords.register(key, value)
103
+ end
104
+ end
105
+
106
+ def install_normalizers(kernel)
107
+ kernel.normalizers.register(:normalize_operation_attributes, Execution::BaselineNormalizers.method(:normalize_operation_attributes))
108
+ end
109
+
110
+ def install_validators(kernel)
111
+ kernel.validators.register(:uniqueness, Execution::BaselineValidators.method(:validate_uniqueness))
112
+ kernel.validators.register(:outputs, Execution::BaselineValidators.method(:validate_outputs))
113
+ kernel.validators.register(:dependencies, Execution::BaselineValidators.method(:validate_dependencies))
114
+ kernel.validators.register(:callables, Execution::BaselineValidators.method(:validate_callables))
115
+ kernel.validators.register(:effect_dependencies, Execution::BaselineValidators.method(:validate_effect_dependencies))
116
+ kernel.validators.register(:effect_payload_builders, Execution::BaselineValidators.method(:validate_effect_payload_builders))
117
+ kernel.validators.register(:effect_adapters, Execution::BaselineValidators.method(:validate_effect_adapters))
118
+ kernel.validators.register(:types, Execution::BaselineValidators.method(:validate_types))
119
+ end
120
+
121
+ def install_runtime_handlers(kernel)
122
+ kernel.runtime_handlers.register(:input, Execution::BaselineRuntime.method(:handle_input))
123
+ kernel.runtime_handlers.register(:const, Execution::ConstRuntime.method(:handle_const))
124
+ kernel.runtime_handlers.register(:compute, Execution::BaselineRuntime.method(:handle_compute))
125
+ kernel.runtime_handlers.register(:effect, Execution::BaselineRuntime.method(:handle_effect))
126
+ kernel.runtime_handlers.register(:output, Execution::BaselineRuntime.method(:handle_output))
127
+ end
128
+
129
+ def install_executors(kernel)
130
+ kernel.executors.register(:inline, Execution::InlineExecutor.method(:call))
131
+ end
132
+
133
+ def install_diagnostics(kernel)
134
+ BASELINE_DIAGNOSTICS.each do |key, value|
135
+ kernel.diagnostics_contributors.register(key, value)
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module ConstPack
7
+ module_function
8
+
9
+ def manifest
10
+ PackManifest.new(
11
+ name: :const,
12
+ node_contracts: [PackManifest.node(:const)]
13
+ )
14
+ end
15
+
16
+ def install_into(kernel)
17
+ return kernel if kernel.nodes.registered?(:const)
18
+
19
+ kernel.nodes.register(:const, NodeType.new(kind: :const, metadata: { category: :value }))
20
+ kernel.dsl_keywords.register(:const, DslKeyword.new(:const) do |name, value, builder:|
21
+ builder.add_operation(kind: :const, name: name, value: value)
22
+ end)
23
+ kernel.runtime_handlers.register(:const, Execution::ConstRuntime.method(:handle_const))
24
+ kernel
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ class DslKeyword
7
+ attr_reader :name
8
+
9
+ def initialize(name, callable = nil, &block)
10
+ @name = name.to_sym
11
+ @callable = callable || block
12
+ raise ArgumentError, "keyword #{@name} requires a callable" unless @callable
13
+ end
14
+
15
+ def call(*args, builder:, **kwargs, &block)
16
+ @callable.call(*args, builder:, **kwargs, &block)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ module HookResultPolicies
7
+ module_function
8
+
9
+ def operations_array(result)
10
+ return "must return an Array of operations" unless result.is_a?(Array)
11
+
12
+ result.each_with_index do |operation, index|
13
+ message = validate_operation(operation)
14
+ return "must return an Array of operations; element #{index} #{message}" if message
15
+ end
16
+
17
+ nil
18
+ end
19
+
20
+ def execution_result(result)
21
+ return "must return an ExecutionResult" unless result.is_a?(Execution::ExecutionResult)
22
+
23
+ nil
24
+ end
25
+
26
+ def validation_findings(result)
27
+ return nil if result.nil?
28
+ return "must return an Array of ValidationFinding entries" unless result.is_a?(Array)
29
+
30
+ invalid_index = result.find_index { |entry| !entry.is_a?(Execution::ValidationFinding) }
31
+ return nil if invalid_index.nil?
32
+
33
+ "must return an Array of ValidationFinding entries; element #{invalid_index} is invalid"
34
+ end
35
+
36
+ def validate_operation(operation)
37
+ return "must be an Execution::Operation" unless operation.is_a?(Execution::Operation)
38
+ return "must use Symbol kind" unless operation.kind.is_a?(Symbol)
39
+ return "must use Symbol name" unless operation.name.is_a?(Symbol)
40
+ return "must use Hash attributes" unless operation.attributes.is_a?(Hash)
41
+
42
+ nil
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Igniter
4
+ module Contracts
5
+ module Assembly
6
+ class HookSpec
7
+ attr_reader :registry, :method_name, :required_keywords, :role, :return_policy
8
+
9
+ def initialize(registry:, method_name:, required_keywords:, role:, return_policy: :opaque,
10
+ result_validator: nil)
11
+ @registry = registry.to_sym
12
+ @method_name = method_name.to_sym
13
+ @required_keywords = required_keywords.map(&:to_sym).freeze
14
+ @role = role.to_sym
15
+ @return_policy = return_policy.to_sym
16
+ @result_validator = result_validator
17
+ freeze
18
+ end
19
+
20
+ def validate!(key, implementation)
21
+ unless implementation.respond_to?(method_name)
22
+ raise InvalidHookImplementationError,
23
+ "#{registry} entry #{key} must respond to ##{method_name}"
24
+ end
25
+
26
+ parameters = parameters_for(implementation)
27
+ return if accepts_required_keywords?(parameters)
28
+
29
+ missing = missing_keywords(parameters)
30
+ expected_keywords = required_keywords.map { |name| "#{name}:" }.join(", ")
31
+ missing_labels = missing.map { |name| "#{name}:" }.join(", ")
32
+
33
+ raise InvalidHookImplementationError,
34
+ "#{registry} entry #{key} must accept keywords #{expected_keywords}; missing #{missing_labels}"
35
+ end
36
+
37
+ def validate_result!(key, result)
38
+ return result unless @result_validator
39
+
40
+ message = @result_validator.call(result)
41
+ return result unless message
42
+
43
+ raise InvalidHookResultError,
44
+ "#{registry} entry #{key} (#{role}) #{message}"
45
+ end
46
+
47
+ private
48
+
49
+ def accepts_required_keywords?(parameters)
50
+ missing_keywords(parameters).empty?
51
+ end
52
+
53
+ def parameters_for(implementation)
54
+ if method_name == :call && implementation.respond_to?(:parameters)
55
+ implementation.parameters
56
+ else
57
+ implementation.method(method_name).parameters
58
+ end
59
+ end
60
+
61
+ def missing_keywords(parameters)
62
+ return [] if parameters.any? { |type, _name| type == :keyrest }
63
+
64
+ accepted = parameters.filter_map do |type, name|
65
+ name.to_sym if %i[key keyreq].include?(type) && name
66
+ end
67
+
68
+ required_keywords.reject { |keyword| accepted.include?(keyword) }
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end