igniter_lang 0.1.0.alpha.1
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 +65 -0
- data/RELEASE_NOTES.md +137 -0
- data/bin/igc +7 -0
- data/lib/igniter_lang/assembler.rb +717 -0
- data/lib/igniter_lang/classifier.rb +405 -0
- data/lib/igniter_lang/cli.rb +76 -0
- data/lib/igniter_lang/compilation_report.rb +99 -0
- data/lib/igniter_lang/compiler_orchestrator.rb +362 -0
- data/lib/igniter_lang/compiler_profile_contract_validator.rb +286 -0
- data/lib/igniter_lang/compiler_result.rb +77 -0
- data/lib/igniter_lang/diagnostics.rb +125 -0
- data/lib/igniter_lang/fragment_registry_compatibility_adapter.rb +129 -0
- data/lib/igniter_lang/internal_profile_assembly.rb +199 -0
- data/lib/igniter_lang/internal_profile_assembly_source_packet.rb +175 -0
- data/lib/igniter_lang/internal_profile_static_data_carrier.rb +286 -0
- data/lib/igniter_lang/oof_fragment_registry.rb +802 -0
- data/lib/igniter_lang/parser.rb +1736 -0
- data/lib/igniter_lang/runtime_smoke.rb +80 -0
- data/lib/igniter_lang/semanticir_emitter.rb +847 -0
- data/lib/igniter_lang/temporal_access_runtime.rb +437 -0
- data/lib/igniter_lang/temporal_executor.rb +457 -0
- data/lib/igniter_lang/typechecker.rb +821 -0
- data/lib/igniter_lang/version.rb +5 -0
- data/lib/igniter_lang.rb +27 -0
- metadata +72 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
require "time"
|
|
6
|
+
|
|
7
|
+
module IgniterLang
|
|
8
|
+
module TemporalAccessRuntime
|
|
9
|
+
module Capabilities
|
|
10
|
+
HISTORY_READ = "history_read"
|
|
11
|
+
BIHISTORY_READ = "bihistory_read"
|
|
12
|
+
BITEMPORAL_READ = BIHISTORY_READ
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def for_axis(axis)
|
|
17
|
+
case axis
|
|
18
|
+
when "single", "valid_time"
|
|
19
|
+
[HISTORY_READ]
|
|
20
|
+
when "bitemporal"
|
|
21
|
+
[BIHISTORY_READ]
|
|
22
|
+
else
|
|
23
|
+
[]
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def axis_for(access_node, input_node)
|
|
28
|
+
axis = access_node["axis"] || input_node["axis"]
|
|
29
|
+
axis == "single" ? "valid_time" : axis
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
module Canonical
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
def normalize(value)
|
|
37
|
+
case value
|
|
38
|
+
when Hash
|
|
39
|
+
value.keys.sort_by(&:to_s).each_with_object({}) { |key, out| out[key.to_s] = normalize(value[key]) }
|
|
40
|
+
when Array
|
|
41
|
+
value.map { |item| normalize(item) }
|
|
42
|
+
else
|
|
43
|
+
value
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def json(value)
|
|
48
|
+
JSON.generate(normalize(value))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def pretty(value)
|
|
52
|
+
"#{JSON.pretty_generate(normalize(value))}\n"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def hash(value)
|
|
56
|
+
"sha256:#{Digest::SHA256.hexdigest(json(value))}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def short_hash(value)
|
|
60
|
+
hash(value).split(":").last[0, 16]
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
module Option
|
|
65
|
+
ENCODING = {
|
|
66
|
+
"some" => { "kind" => "some", "value" => "<value>" },
|
|
67
|
+
"none" => { "kind" => "none" }
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
module_function
|
|
71
|
+
|
|
72
|
+
def some(value)
|
|
73
|
+
{ "kind" => "some", "value" => value }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def none
|
|
77
|
+
{ "kind" => "none" }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def some?(value)
|
|
81
|
+
value.fetch("kind") == "some"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def value(option)
|
|
85
|
+
option.fetch("value")
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
class AxisTypeError < StandardError
|
|
90
|
+
attr_reader :axis, :value
|
|
91
|
+
|
|
92
|
+
def initialize(axis, value)
|
|
93
|
+
@axis = axis
|
|
94
|
+
@value = value
|
|
95
|
+
super("#{axis} must be ISO8601 DateTime")
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class CapabilityError < StandardError
|
|
100
|
+
attr_reader :capability, :node
|
|
101
|
+
|
|
102
|
+
def initialize(capability, node)
|
|
103
|
+
@capability = capability
|
|
104
|
+
@node = node
|
|
105
|
+
super("temporal access requires capability: #{capability}")
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
class BackendContractError < StandardError
|
|
110
|
+
attr_reader :method_name, :axis
|
|
111
|
+
|
|
112
|
+
def initialize(method_name, axis)
|
|
113
|
+
@method_name = method_name
|
|
114
|
+
@axis = axis
|
|
115
|
+
super("temporal access backend must implement #{method_name} for #{axis}")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
class RuntimeMachineHook
|
|
120
|
+
def initialize(backend:, capabilities: nil)
|
|
121
|
+
@backend = backend
|
|
122
|
+
@capabilities = Array(capabilities || infer_capabilities(backend))
|
|
123
|
+
@evaluator = SemanticIRTemporalAccessEvaluator.new(backend)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def load_check(contract:, requirements: {})
|
|
127
|
+
nodes = contract.fetch("nodes", [])
|
|
128
|
+
temporal_inputs = temporal_inputs_for(nodes)
|
|
129
|
+
checks = nodes
|
|
130
|
+
.select { |node| node.fetch("kind") == "temporal_access_node" }
|
|
131
|
+
.map { |node| load_check_node(node, temporal_inputs, requirements) }
|
|
132
|
+
{
|
|
133
|
+
"kind" => "temporal_access_hook_load_check",
|
|
134
|
+
"status" => checks.all? { |check| check.fetch("status") == "ok" } ? "ok" : "blocked",
|
|
135
|
+
"checks" => checks
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def evaluate(access_node, temporal_inputs:, inputs:)
|
|
140
|
+
input_node = temporal_inputs.fetch(access_node.fetch("source_ref"))
|
|
141
|
+
axis = Capabilities.axis_for(access_node, input_node)
|
|
142
|
+
ensure_capabilities!(Capabilities.for_axis(axis), access_node)
|
|
143
|
+
ensure_backend_contract!(axis)
|
|
144
|
+
@evaluator.evaluate(access_node, temporal_inputs: temporal_inputs, inputs: inputs)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def load_check_node(access_node, temporal_inputs, requirements)
|
|
150
|
+
input_node = temporal_inputs.fetch(access_node.fetch("source_ref"))
|
|
151
|
+
axis = Capabilities.axis_for(access_node, input_node)
|
|
152
|
+
required = Capabilities.for_axis(axis)
|
|
153
|
+
declared = Array(requirements.dig("capabilities", "required_caps"))
|
|
154
|
+
missing_declared_caps = declared.empty? ? [] : required.reject { |capability| declared.include?(capability) }
|
|
155
|
+
missing_caps = required.reject { |capability| capability_available?(capability) }
|
|
156
|
+
missing_methods = backend_methods_for(axis).reject { |method_name| @backend.respond_to?(method_name) }
|
|
157
|
+
{
|
|
158
|
+
"node" => access_node.fetch("name"),
|
|
159
|
+
"axis" => axis,
|
|
160
|
+
"required_capabilities" => required,
|
|
161
|
+
"declared_capabilities" => declared,
|
|
162
|
+
"missing_declared_capabilities" => missing_declared_caps,
|
|
163
|
+
"missing_capabilities" => missing_caps,
|
|
164
|
+
"required_backend_methods" => backend_methods_for(axis).map(&:to_s),
|
|
165
|
+
"missing_backend_methods" => missing_methods.map(&:to_s),
|
|
166
|
+
"status" => missing_declared_caps.empty? && missing_caps.empty? && missing_methods.empty? ? "ok" : "blocked"
|
|
167
|
+
}
|
|
168
|
+
rescue KeyError => e
|
|
169
|
+
{
|
|
170
|
+
"node" => access_node.fetch("name", nil),
|
|
171
|
+
"axis" => nil,
|
|
172
|
+
"required_capabilities" => [],
|
|
173
|
+
"declared_capabilities" => [],
|
|
174
|
+
"missing_declared_capabilities" => [],
|
|
175
|
+
"missing_capabilities" => [],
|
|
176
|
+
"required_backend_methods" => [],
|
|
177
|
+
"missing_backend_methods" => [],
|
|
178
|
+
"status" => "blocked",
|
|
179
|
+
"error" => e.message
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def ensure_capabilities!(required, access_node)
|
|
184
|
+
required.each do |capability|
|
|
185
|
+
raise CapabilityError.new(capability, access_node) unless capability_available?(capability)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def ensure_backend_contract!(axis)
|
|
190
|
+
backend_methods_for(axis).each do |method_name|
|
|
191
|
+
raise BackendContractError.new(method_name, axis) unless @backend.respond_to?(method_name)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def backend_methods_for(axis)
|
|
196
|
+
case axis
|
|
197
|
+
when "valid_time"
|
|
198
|
+
[:read_as_of]
|
|
199
|
+
when "bitemporal"
|
|
200
|
+
[:bihistory_at]
|
|
201
|
+
else
|
|
202
|
+
[]
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def capability_available?(capability)
|
|
207
|
+
return true if @capabilities.include?(capability)
|
|
208
|
+
return @backend.supports_capability?(capability) if @backend.respond_to?(:supports_capability?)
|
|
209
|
+
|
|
210
|
+
false
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def infer_capabilities(backend)
|
|
214
|
+
capabilities = []
|
|
215
|
+
capabilities << Capabilities::HISTORY_READ if backend.respond_to?(:read_as_of)
|
|
216
|
+
capabilities << Capabilities::BIHISTORY_READ if backend.respond_to?(:bihistory_at)
|
|
217
|
+
capabilities
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def temporal_inputs_for(nodes)
|
|
221
|
+
nodes
|
|
222
|
+
.select { |node| node.fetch("kind") == "temporal_input_node" }
|
|
223
|
+
.to_h { |node| [node.fetch("name"), node] }
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
class SemanticIRTemporalAccessEvaluator
|
|
228
|
+
def initialize(backend)
|
|
229
|
+
@backend = backend
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def evaluate(access_node, temporal_inputs:, inputs:)
|
|
233
|
+
raise ArgumentError, "expected temporal_access_node" unless access_node.fetch("kind") == "temporal_access_node"
|
|
234
|
+
raise ArgumentError, "only point temporal access is supported" unless access_node.fetch("access") == "point"
|
|
235
|
+
|
|
236
|
+
input_node = temporal_inputs.fetch(access_node.fetch("source_ref"))
|
|
237
|
+
axis = normalized_axis(access_node, input_node)
|
|
238
|
+
case axis
|
|
239
|
+
when "valid_time"
|
|
240
|
+
evaluate_valid_time(access_node, input_node, inputs)
|
|
241
|
+
when "bitemporal"
|
|
242
|
+
evaluate_bitemporal(access_node, input_node, inputs)
|
|
243
|
+
else
|
|
244
|
+
raise ArgumentError, "unsupported temporal axis: #{axis}"
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
private
|
|
249
|
+
|
|
250
|
+
def evaluate_valid_time(access_node, input_node, inputs)
|
|
251
|
+
time_ref = access_node["time_ref"] || input_node.fetch("as_of_ref")
|
|
252
|
+
subject = render_ref(input_node.fetch("store_ref"), inputs)
|
|
253
|
+
result, observation = @backend.read_as_of(subject, inputs.fetch(time_ref))
|
|
254
|
+
envelope(access_node, "valid_time", result, observation, selected_ref_key: "selected_append_ref", rel: "selected_append")
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def evaluate_bitemporal(access_node, input_node, inputs)
|
|
258
|
+
valid_time_ref = access_node.fetch("valid_time_ref")
|
|
259
|
+
transaction_time_ref = access_node.fetch("transaction_time_ref")
|
|
260
|
+
history_ref = render_ref(input_node.fetch("history_ref") { input_node.fetch("store_ref") }, inputs)
|
|
261
|
+
result, observation = @backend.bihistory_at(
|
|
262
|
+
history_ref,
|
|
263
|
+
vt: inputs.fetch(valid_time_ref),
|
|
264
|
+
tt: inputs.fetch(transaction_time_ref),
|
|
265
|
+
node_name: access_node.fetch("name")
|
|
266
|
+
)
|
|
267
|
+
envelope(access_node, "bitemporal", result, observation, selected_ref_key: "selected_event_ref", rel: "selected_event")
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def envelope(access_node, axis, result, observation, selected_ref_key:, rel:)
|
|
271
|
+
selected_ref = observation[selected_ref_key]
|
|
272
|
+
{
|
|
273
|
+
"kind" => "temporal_access_evaluation",
|
|
274
|
+
"node" => access_node.fetch("name"),
|
|
275
|
+
"axis" => axis,
|
|
276
|
+
"result" => result,
|
|
277
|
+
"observation" => observation,
|
|
278
|
+
"evidence_links" => selected_ref ? [
|
|
279
|
+
{
|
|
280
|
+
"rel" => rel,
|
|
281
|
+
"from" => observation.fetch("observation_id"),
|
|
282
|
+
"to" => selected_ref
|
|
283
|
+
}
|
|
284
|
+
] : []
|
|
285
|
+
}
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def normalized_axis(access_node, input_node)
|
|
289
|
+
axis = access_node["axis"] || input_node["axis"]
|
|
290
|
+
return "valid_time" if axis == "single"
|
|
291
|
+
|
|
292
|
+
axis
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def render_ref(template, inputs)
|
|
296
|
+
template.gsub(/\{([^}]+)\}/) do
|
|
297
|
+
inputs.fetch(Regexp.last_match(1))
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
class MemoryBackend
|
|
303
|
+
attr_reader :append_observations, :events, :access_observations
|
|
304
|
+
|
|
305
|
+
def initialize
|
|
306
|
+
@append_observations = []
|
|
307
|
+
@events = Hash.new { |hash, key| hash[key] = [] }
|
|
308
|
+
@access_observations = []
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def seed_append_observations(observations)
|
|
312
|
+
observations.each do |observation|
|
|
313
|
+
append(observation.fetch("subject"), observation.fetch("valid_from"), observation.fetch("value"),
|
|
314
|
+
value_type: observation.fetch("value_type"))
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def append(subject, valid_from, value, value_type:)
|
|
319
|
+
payload = {
|
|
320
|
+
"kind" => "history_append_observation",
|
|
321
|
+
"subject" => subject,
|
|
322
|
+
"valid_from" => valid_from,
|
|
323
|
+
"value" => value,
|
|
324
|
+
"value_type" => value_type
|
|
325
|
+
}
|
|
326
|
+
observation = payload.merge(
|
|
327
|
+
"observation_id" => "obs/history_append/#{Canonical.short_hash(payload)}",
|
|
328
|
+
"observed_at" => valid_from,
|
|
329
|
+
"temporal" => {
|
|
330
|
+
"axis" => "valid_time",
|
|
331
|
+
"as_of" => valid_from,
|
|
332
|
+
"lifecycle" => "durable"
|
|
333
|
+
}
|
|
334
|
+
)
|
|
335
|
+
@append_observations << observation
|
|
336
|
+
observation
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def history_at(subject, as_of)
|
|
340
|
+
as_of_time = parse_axis!("as_of", as_of)
|
|
341
|
+
selected = @append_observations
|
|
342
|
+
.select { |obs| obs.fetch("subject") == subject && Time.iso8601(obs.fetch("valid_from")) <= as_of_time }
|
|
343
|
+
.max_by { |obs| Time.iso8601(obs.fetch("valid_from")) }
|
|
344
|
+
result = selected ? Option.some(selected.fetch("value")) : Option.none
|
|
345
|
+
observation = history_access_observation(subject, as_of, selected, result)
|
|
346
|
+
@access_observations << observation
|
|
347
|
+
[result, observation]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
def read_as_of(subject, as_of)
|
|
351
|
+
history_at(subject, as_of)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def seed(events)
|
|
355
|
+
events.each { |event| append_bihistory_event(event) }
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def append_bihistory_event(event)
|
|
359
|
+
history_ref = event.fetch("history_ref")
|
|
360
|
+
@events[history_ref] << event
|
|
361
|
+
@events[history_ref].sort_by! { |entry| [entry.fetch("valid_from"), entry.fetch("tx_from"), entry.fetch("event_id")] }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def bihistory_at(history_ref, vt:, tt:, node_name:)
|
|
365
|
+
vt_time = parse_axis!("vt", vt)
|
|
366
|
+
tt_time = parse_axis!("tt", tt)
|
|
367
|
+
selected = @events.fetch(history_ref, [])
|
|
368
|
+
.select { |event| covers_valid_time?(event, vt_time) && Time.iso8601(event.fetch("tx_from")) <= tt_time }
|
|
369
|
+
.max_by { |event| [Time.iso8601(event.fetch("tx_from")), event.fetch("event_id")] }
|
|
370
|
+
result = selected ? Option.some(selected.fetch("value")) : Option.none
|
|
371
|
+
observation = bihistory_access_observation(history_ref, vt, tt, node_name, selected, result)
|
|
372
|
+
@access_observations << observation
|
|
373
|
+
[result, observation]
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private
|
|
377
|
+
|
|
378
|
+
def parse_axis!(axis, value)
|
|
379
|
+
raise AxisTypeError.new(axis, value) unless value.is_a?(String)
|
|
380
|
+
|
|
381
|
+
Time.iso8601(value)
|
|
382
|
+
rescue ArgumentError
|
|
383
|
+
raise AxisTypeError.new(axis, value)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def covers_valid_time?(event, vt_time)
|
|
387
|
+
valid_from = Time.iso8601(event.fetch("valid_from"))
|
|
388
|
+
valid_until = Time.iso8601(event.fetch("valid_until"))
|
|
389
|
+
valid_from <= vt_time && vt_time < valid_until
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def history_access_observation(subject, as_of, selected, result)
|
|
393
|
+
payload = {
|
|
394
|
+
"kind" => "history_access_observation",
|
|
395
|
+
"subject" => subject,
|
|
396
|
+
"as_of" => as_of,
|
|
397
|
+
"access" => "point",
|
|
398
|
+
"selected_append_ref" => selected&.fetch("observation_id"),
|
|
399
|
+
"result" => result,
|
|
400
|
+
"option_encoding" => Option::ENCODING
|
|
401
|
+
}
|
|
402
|
+
payload.merge(
|
|
403
|
+
"observation_id" => "obs/history_access/#{Canonical.short_hash(payload)}",
|
|
404
|
+
"observed_at" => as_of,
|
|
405
|
+
"temporal" => {
|
|
406
|
+
"axis" => "valid_time",
|
|
407
|
+
"as_of" => as_of,
|
|
408
|
+
"lifecycle" => "session"
|
|
409
|
+
}
|
|
410
|
+
)
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def bihistory_access_observation(history_ref, vt, tt, node_name, selected, result)
|
|
414
|
+
payload = {
|
|
415
|
+
"kind" => "bihistory_access_observation",
|
|
416
|
+
"history_ref" => history_ref,
|
|
417
|
+
"node" => node_name,
|
|
418
|
+
"axis" => "bitemporal",
|
|
419
|
+
"valid_time" => vt,
|
|
420
|
+
"transaction_time" => tt,
|
|
421
|
+
"selected_event_ref" => selected&.fetch("event_id"),
|
|
422
|
+
"result" => result,
|
|
423
|
+
"option_encoding" => Option::ENCODING
|
|
424
|
+
}
|
|
425
|
+
payload.merge(
|
|
426
|
+
"observation_id" => "obs/bihistory_access/#{Canonical.short_hash(payload)}",
|
|
427
|
+
"observed_at" => tt,
|
|
428
|
+
"temporal" => {
|
|
429
|
+
"valid_time" => vt,
|
|
430
|
+
"transaction_time" => tt,
|
|
431
|
+
"lifecycle" => "audit"
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|