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.
- checksums.yaml +7 -0
- data/README.md +239 -0
- data/lib/igniter/contracts/api.rb +92 -0
- data/lib/igniter/contracts/assembly/baseline_pack.rb +141 -0
- data/lib/igniter/contracts/assembly/const_pack.rb +29 -0
- data/lib/igniter/contracts/assembly/dsl_keyword.rb +21 -0
- data/lib/igniter/contracts/assembly/hook_result_policies.rb +47 -0
- data/lib/igniter/contracts/assembly/hook_spec.rb +73 -0
- data/lib/igniter/contracts/assembly/hook_specs.rb +74 -0
- data/lib/igniter/contracts/assembly/kernel.rb +220 -0
- data/lib/igniter/contracts/assembly/node_type.rb +26 -0
- data/lib/igniter/contracts/assembly/ordered_registry.rb +55 -0
- data/lib/igniter/contracts/assembly/pack.rb +13 -0
- data/lib/igniter/contracts/assembly/pack_manifest.rb +131 -0
- data/lib/igniter/contracts/assembly/path_access.rb +76 -0
- data/lib/igniter/contracts/assembly/profile.rb +133 -0
- data/lib/igniter/contracts/assembly/project_pack.rb +42 -0
- data/lib/igniter/contracts/assembly/registry.rb +57 -0
- data/lib/igniter/contracts/assembly/step_result_pack.rb +42 -0
- data/lib/igniter/contracts/assembly.rb +18 -0
- data/lib/igniter/contracts/contract.rb +135 -0
- data/lib/igniter/contracts/contractable.rb +288 -0
- data/lib/igniter/contracts/environment.rb +51 -0
- data/lib/igniter/contracts/errors.rb +47 -0
- data/lib/igniter/contracts/execution/baseline_normalizers.rb +23 -0
- data/lib/igniter/contracts/execution/baseline_runtime.rb +55 -0
- data/lib/igniter/contracts/execution/baseline_validators.rb +113 -0
- data/lib/igniter/contracts/execution/builder.rb +43 -0
- data/lib/igniter/contracts/execution/compilation_report.rb +46 -0
- data/lib/igniter/contracts/execution/compiled_graph.rb +21 -0
- data/lib/igniter/contracts/execution/compiler.rb +66 -0
- data/lib/igniter/contracts/execution/const_runtime.rb +15 -0
- data/lib/igniter/contracts/execution/diagnostics.rb +24 -0
- data/lib/igniter/contracts/execution/diagnostics_report.rb +40 -0
- data/lib/igniter/contracts/execution/diagnostics_section.rb +37 -0
- data/lib/igniter/contracts/execution/effect_invocation.rb +26 -0
- data/lib/igniter/contracts/execution/execution_request.rb +28 -0
- data/lib/igniter/contracts/execution/execution_result.rb +32 -0
- data/lib/igniter/contracts/execution/inline_executor.rb +19 -0
- data/lib/igniter/contracts/execution/mutable_named_values.rb +52 -0
- data/lib/igniter/contracts/execution/named_values.rb +48 -0
- data/lib/igniter/contracts/execution/operation.rb +42 -0
- data/lib/igniter/contracts/execution/runtime.rb +43 -0
- data/lib/igniter/contracts/execution/step_result.rb +51 -0
- data/lib/igniter/contracts/execution/step_result_diagnostics.rb +35 -0
- data/lib/igniter/contracts/execution/step_result_runtime.rb +51 -0
- data/lib/igniter/contracts/execution/step_result_validators.rb +44 -0
- data/lib/igniter/contracts/execution/structured_dump.rb +49 -0
- data/lib/igniter/contracts/execution/validation_finding.rb +28 -0
- data/lib/igniter/contracts/execution/validation_report.rb +46 -0
- data/lib/igniter/contracts/execution.rb +28 -0
- data/lib/igniter/contracts.rb +54 -0
- data/lib/igniter/lang/backend.rb +19 -0
- data/lib/igniter/lang/backends/ruby.rb +42 -0
- data/lib/igniter/lang/diagnostic_payload.rb +174 -0
- data/lib/igniter/lang/metadata_carrier_manifest.rb +112 -0
- data/lib/igniter/lang/metadata_manifest.rb +128 -0
- data/lib/igniter/lang/receipt_payload.rb +152 -0
- data/lib/igniter/lang/schema_compatibility_diagnostic.rb +300 -0
- data/lib/igniter/lang/types.rb +84 -0
- data/lib/igniter/lang/verification_report.rb +226 -0
- data/lib/igniter/lang.rb +27 -0
- data/lib/igniter-contracts.rb +3 -0
- 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
|