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.
@@ -0,0 +1,457 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "json"
5
+
6
+ module IgniterLang
7
+ # Phase 1 TEMPORAL executor boundary.
8
+ #
9
+ # Authorized scope: History[T] valid_time, proof-local MemoryBackend only.
10
+ # All other surfaces (Ledger, BiHistory, stream, OLAP, writes, production
11
+ # cache) are explicitly excluded and refuse before any live call.
12
+ #
13
+ # Guard order (token-before-gate per S3-R15-C1-P amendment):
14
+ # approval_token → gate_state → backend_identity → scope → cache_key → kernel
15
+ #
16
+ # Live reads stay blocked until gate3_authorized: true + valid token.
17
+ module TemporalExecutor
18
+ # Phase 1 proof-local authority URI from gate3-decision-record-v0.md §Authority Registry.
19
+ # Source-code-parity verification only — not cryptographic authorization.
20
+ # Any token carrying this exact string passes AT-9; issuer identity is not verified.
21
+ # Replace with production signing (R2) before any non-proof deployment.
22
+ GATE3_AUTHORITY_REF =
23
+ "architect-supervisor://igniter-lang/gates/gate3/" \
24
+ "runtime-temporal-executor/restricted-history-valid-time-v0/2026-05-09"
25
+
26
+ PHASE1_FORMAT_VERSION = "0.1.0"
27
+ PHASE1_SCOPE = "History[T] valid_time"
28
+ PHASE1_MEMORY_BACKEND_CLASS = "IgniterLang::TemporalAccessRuntime::MemoryBackend"
29
+
30
+ # Reason codes emitted by this executor (stable identifiers for callers).
31
+ module ReasonCode
32
+ APPROVAL_MISSING = "runtime.executor_approval_missing"
33
+ APPROVAL_MALFORMED = "runtime.executor_approval_malformed"
34
+ AUTHORITY_UNTRUSTED = "runtime.executor_approval_authority_untrusted"
35
+ GATE3_CLOSED = "runtime.temporal_gate3_closed"
36
+ BACKEND_IDENTITY_BLOCKED = "runtime.phase1_backend_identity_blocked"
37
+ SCOPE_EXCLUSION = "runtime.temporal_scope_exclusion"
38
+ NON_TEMPORAL = SCOPE_EXCLUSION
39
+ CACHE_MISMATCH = "runtime.temporal_cache_schema_mismatch"
40
+ BIHISTORY_EXCLUDED = SCOPE_EXCLUSION
41
+ CORE_REFUSAL = SCOPE_EXCLUSION
42
+ EVALUATION_READY = "runtime.temporal_evaluation_ready"
43
+
44
+ # Deprecated string literals from pre-S3-R18-C2-P experiment fixtures.
45
+ # The lib/ executor emits SCOPE_EXCLUSION for all three scenarios — these old
46
+ # strings are NOT emitted. S3-R14-C2-P and S3-R15-C2-P experiments that check
47
+ # the old strings are sealed proof artifacts; do not retroactively update them.
48
+ # Phase 2 migration: remove this constant once all non-experiment callers are
49
+ # verified to use ReasonCode::SCOPE_EXCLUSION or "runtime.temporal_scope_exclusion".
50
+ LEGACY_ALIASES = {
51
+ "runtime.non_temporal_not_covered" => SCOPE_EXCLUSION,
52
+ "runtime.temporal_executor_bihistory_excluded" => SCOPE_EXCLUSION,
53
+ "runtime.temporal_executor_core_refusal" => SCOPE_EXCLUSION
54
+ }.freeze
55
+ end
56
+
57
+ # Phase1 — proof-local History[T] valid_time executor.
58
+ #
59
+ # Responsibilities:
60
+ # - Enforce approval_token before gate_state (AT-4/AT-5 order)
61
+ # - Validate authority_ref exactly against GATE3_AUTHORITY_REF (AT-9)
62
+ # - Enforce scope, cache-key, BiHistory, and CORE fragment guards
63
+ # - Emit temporal_live_read_observation unconditionally (AT-10)
64
+ # - Compose one CompatibilityReport-shaped hash per evaluation (AT-2)
65
+ # - Guarantee operation_check flags false for all blocked paths
66
+ #
67
+ # NOT responsible for: artifact loading, full CompatibilityReport composition
68
+ # pipeline, Ledger binding, production cache, production signing.
69
+ class Phase1
70
+ # Proof-local only. In-memory, not durable. Not an audit receipt.
71
+ # AT-10 emission is unconditional; persistence is deferred (see compatibility-report-persistence-audit-v0).
72
+ attr_reader :observations, :last_compatibility_report
73
+
74
+ # gate3_authorized: caller honor-system. Pass true only when a valid Architect
75
+ # decision (gate3-live-read-decision-addendum-v0) authorizes non-proof live reads.
76
+ # The lib/ class cannot verify the addendum exists; the caller is responsible.
77
+ # Default false = live reads blocked at construction regardless of backend or token.
78
+ def initialize(backend:, gate3_authorized: false)
79
+ @backend = backend
80
+ @gate3_authorized = gate3_authorized
81
+ @backend_identity_check = check_backend_identity(backend)
82
+ @observations = []
83
+ @last_compatibility_report = nil
84
+ end
85
+
86
+ # Evaluate a temporal contract.
87
+ #
88
+ # contract: assembled contract Hash (fragment_class, temporal_nodes, contract_id)
89
+ # token: ExecutorApprovalToken Hash
90
+ # inputs: Hash of contract input values
91
+ # as_of: ISO8601 datetime string
92
+ # requested_cache_key_fragment: expected "TEMPORAL" for Phase 1
93
+ #
94
+ # Returns a result Hash; never raises for guard failures.
95
+ def evaluate(contract, token:, inputs:, as_of:, requested_cache_key_fragment: "TEMPORAL")
96
+ contract_id = contract.fetch("contract_id")
97
+
98
+ # Step 1: approval_token (AT-4 + AT-9) — must fire before gate check
99
+ token_check = check_approval_token(token, contract_id)
100
+ if token_check[:blocked]
101
+ return build_refusal(token_check, contract_id: contract_id, as_of: as_of,
102
+ blocked_stage: "approval_token",
103
+ gate_open: @gate3_authorized,
104
+ token_ok: false,
105
+ cache_key_fragment: requested_cache_key_fragment)
106
+ end
107
+
108
+ # Step 2: gate_state (AT-5) — independent of token
109
+ unless @gate3_authorized
110
+ gate_check = { blocked: true,
111
+ reason_code: ReasonCode::GATE3_CLOSED,
112
+ message: "Gate 3 is closed for TEMPORAL evaluation",
113
+ context: { "gate" => "tbackend_gate3" } }
114
+ return build_refusal(gate_check, contract_id: contract_id, as_of: as_of,
115
+ blocked_stage: "gate_state",
116
+ gate_open: false,
117
+ token_ok: true,
118
+ cache_key_fragment: requested_cache_key_fragment)
119
+ end
120
+
121
+ # Step 2b: backend_identity — Phase 1 must not quietly bind Ledger
122
+ if @backend_identity_check[:blocked]
123
+ return build_refusal(@backend_identity_check, contract_id: contract_id, as_of: as_of,
124
+ blocked_stage: "backend_identity",
125
+ gate_open: true,
126
+ token_ok: true,
127
+ cache_key_fragment: requested_cache_key_fragment)
128
+ end
129
+
130
+ # Step 3: scope — TEMPORAL fragment only, before cache
131
+ unless contract.fetch("fragment_class") == "temporal"
132
+ scope_check = { blocked: true,
133
+ reason_code: ReasonCode::NON_TEMPORAL,
134
+ message: "Phase 1 executor handles only TEMPORAL fragment contracts",
135
+ context: { "expected_scope" => "history_valid_time",
136
+ "actual_fragment" => contract.fetch("fragment_class"),
137
+ "actual_surface" => contract.fetch("fragment_class") } }
138
+ return build_refusal(scope_check, contract_id: contract_id, as_of: as_of,
139
+ blocked_stage: "scope",
140
+ gate_open: true,
141
+ token_ok: true,
142
+ cache_key_fragment: requested_cache_key_fragment)
143
+ end
144
+
145
+ # Step 4: cache_key schema (AT-6) — before any backend access
146
+ unless requested_cache_key_fragment == "TEMPORAL"
147
+ cache_check = { blocked: true,
148
+ reason_code: ReasonCode::CACHE_MISMATCH,
149
+ message: "TEMPORAL evaluation cannot use a #{requested_cache_key_fragment}-shaped cache key",
150
+ context: { "gate" => "L-T5",
151
+ "expected_fragment" => "TEMPORAL",
152
+ "requested_fragment" => requested_cache_key_fragment } }
153
+ return build_refusal(cache_check, contract_id: contract_id, as_of: as_of,
154
+ blocked_stage: "cache_key",
155
+ gate_open: true,
156
+ token_ok: true,
157
+ cache_key_fragment: requested_cache_key_fragment)
158
+ end
159
+
160
+ # All preflight guards passed — compose report, run kernel
161
+ report = compose_report(contract_id: contract_id, gate_open: true,
162
+ token_ok: true, cache_key_ok: true,
163
+ readiness: "ready")
164
+ @last_compatibility_report = report
165
+
166
+ kernel_result = run_execution_kernel(contract, inputs: inputs, as_of: as_of)
167
+ kernel_result.merge("compatibility_report_id" => report.fetch("report_id"))
168
+ end
169
+
170
+ private
171
+
172
+ # Phase 1 permits the proof-local MemoryBackend or an explicitly
173
+ # identified non-Ledger backend. Ledger-backed adapters and wrappers that
174
+ # invoke Ledger package code require a Phase 2 Architect addendum.
175
+ def check_backend_identity(backend)
176
+ class_name = backend.class.name.to_s
177
+ if class_name == PHASE1_MEMORY_BACKEND_CLASS
178
+ return { blocked: false,
179
+ backend_identity: { "kind" => "proof_local_memory_backend",
180
+ "class_name" => class_name } }
181
+ end
182
+
183
+ unless backend.respond_to?(:phase1_backend_identity)
184
+ return backend_identity_blocked(
185
+ class_name,
186
+ "backend must be MemoryBackend or expose phase1_backend_identity"
187
+ )
188
+ end
189
+
190
+ identity = backend.phase1_backend_identity
191
+ return backend_identity_blocked(class_name, "phase1_backend_identity must return Hash") unless identity.is_a?(Hash)
192
+
193
+ phase1_allowed = identity_fetch(identity, "phase1_allowed") == true
194
+ ledger_backed = identity_fetch(identity, "ledger_backed") == true
195
+ invokes_ledger = identity_fetch(identity, "invokes_ledger_package") == true
196
+ package_adapter = identity_fetch(identity, "package_adapter") == true
197
+ family = identity_fetch(identity, "backend_family").to_s.downcase
198
+ kind = identity_fetch(identity, "kind").to_s.downcase
199
+
200
+ if phase1_allowed && !ledger_backed && !invokes_ledger && !package_adapter &&
201
+ !ledger_family?(family) && !ledger_kind?(kind) && !ledger_like_class_name?(class_name)
202
+ return { blocked: false, backend_identity: identity.merge("class_name" => class_name) }
203
+ end
204
+
205
+ backend_identity_blocked(
206
+ class_name,
207
+ "backend identity is not allowed for Phase 1",
208
+ identity: identity
209
+ )
210
+ end
211
+
212
+ def backend_identity_blocked(class_name, message, identity: nil)
213
+ context = { "backend_class" => class_name }
214
+ context["backend_identity"] = identity if identity
215
+ { blocked: true,
216
+ reason_code: ReasonCode::BACKEND_IDENTITY_BLOCKED,
217
+ message: message,
218
+ context: context }
219
+ end
220
+
221
+ def identity_fetch(identity, key)
222
+ return identity[key] if identity.key?(key)
223
+
224
+ symbol_key = key.to_sym
225
+ return identity[symbol_key] if identity.key?(symbol_key)
226
+
227
+ nil
228
+ end
229
+
230
+ def ledger_like_class_name?(class_name)
231
+ class_name.split("::").any? do |part|
232
+ part.start_with?("Ledger") || part == "IgniterLedger"
233
+ end
234
+ end
235
+
236
+ def ledger_family?(family)
237
+ family == "ledger" || family == "igniter-ledger" || family == "igniter_ledger"
238
+ end
239
+
240
+ def ledger_kind?(kind)
241
+ kind == "ledger" || kind.start_with?("ledger_") || kind.start_with?("igniter_ledger")
242
+ end
243
+
244
+ def phase1_backend_scope_label
245
+ identity = @backend_identity_check[:backend_identity] || {}
246
+ backend_kind = identity["kind"] || "phase1_backend"
247
+ "History[T].valid_time / #{backend_kind} / proof-local"
248
+ end
249
+
250
+ # AT-4 + AT-9: validate token structure and exact authority_ref match.
251
+ # Returns { blocked: false } on success, or { blocked: true, reason_code:, message: } on failure.
252
+ def check_approval_token(token, contract_id)
253
+ return { blocked: true, reason_code: ReasonCode::APPROVAL_MISSING,
254
+ message: "approval token required" } unless token
255
+ return { blocked: true, reason_code: ReasonCode::APPROVAL_MALFORMED,
256
+ message: "token must be Hash" } unless token.is_a?(Hash)
257
+
258
+ unless token.fetch("kind", nil) == "executor_approval_token" &&
259
+ token.fetch("version", nil) == "executor-approval-token-v1"
260
+ return { blocked: true, reason_code: ReasonCode::APPROVAL_MALFORMED,
261
+ message: "token kind/version invalid" }
262
+ end
263
+
264
+ # AT-9: exact authority_ref match against Gate 3 decision record
265
+ authority_ref = token.fetch("authority_ref", nil)
266
+ if authority_ref.nil?
267
+ return { blocked: true, reason_code: ReasonCode::APPROVAL_MALFORMED,
268
+ message: "missing authority_ref" }
269
+ end
270
+ unless authority_ref == GATE3_AUTHORITY_REF
271
+ return { blocked: true, reason_code: ReasonCode::AUTHORITY_UNTRUSTED,
272
+ message: "authority_ref does not match Gate 3 decision record",
273
+ context: { "expected" => GATE3_AUTHORITY_REF, "got" => authority_ref } }
274
+ end
275
+
276
+ unless token.fetch("gate", nil) == "tbackend_gate3"
277
+ return { blocked: true, reason_code: ReasonCode::APPROVAL_MALFORMED,
278
+ message: "token gate must be tbackend_gate3" }
279
+ end
280
+
281
+ { blocked: false }
282
+ end
283
+
284
+ # AT-12, AT-7, AT-10: execution kernel.
285
+ # Only reached after all preflight guards pass.
286
+ def run_execution_kernel(contract, inputs:, as_of:)
287
+ contract_id = contract.fetch("contract_id")
288
+
289
+ # AT-12: defense-in-depth — executor independently refuses non-TEMPORAL fragment
290
+ unless contract.fetch("fragment_class") == "temporal"
291
+ return { "kind" => "evaluation_refusal",
292
+ "status" => "blocked",
293
+ "guard_at" => "temporal_executor_phase1_kernel",
294
+ "reason_code" => ReasonCode::CORE_REFUSAL,
295
+ "contract_id" => contract_id,
296
+ "context" => { "expected_scope" => "history_valid_time",
297
+ "actual_fragment" => contract.fetch("fragment_class"),
298
+ "actual_surface" => contract.fetch("fragment_class") },
299
+ "gate" => "AT-12" }
300
+ end
301
+
302
+ temporal_nodes = contract.fetch("temporal_nodes", [])
303
+ access_nodes = temporal_nodes.select { |n| n["kind"] == "temporal_access_node" }
304
+
305
+ # AT-7: Phase 1 scope is valid_time only; BiHistory explicitly refused
306
+ bihistory = access_nodes.select { |n| n["axis"] == "bitemporal" }
307
+ if bihistory.any?
308
+ return { "kind" => "evaluation_refusal",
309
+ "status" => "blocked",
310
+ "guard_at" => "temporal_executor_phase1_kernel",
311
+ "reason_code" => ReasonCode::BIHISTORY_EXCLUDED,
312
+ "contract_id" => contract_id,
313
+ "context" => { "expected_scope" => "history_valid_time",
314
+ "actual_fragment" => "temporal",
315
+ "actual_surface" => "bihistory",
316
+ "actual_axis" => "bitemporal" },
317
+ "gate" => "AT-7" }
318
+ end
319
+
320
+ temporal_inputs = temporal_nodes
321
+ .select { |n| n["kind"] == "temporal_input_node" }
322
+ .to_h { |n| [n.fetch("name"), n] }
323
+ all_inputs = inputs.merge("as_of" => as_of)
324
+
325
+ results = access_nodes.map do |node|
326
+ evaluate_valid_time_node(node, temporal_inputs, all_inputs, contract_id)
327
+ end
328
+
329
+ { "status" => "ok",
330
+ "kind" => "temporal_evaluation_result",
331
+ "contract_id" => contract_id,
332
+ "results" => results,
333
+ "observations_emitted" => @observations.length,
334
+ "runtime_enforced" => true,
335
+ "scope" => phase1_backend_scope_label,
336
+ "excluded" => "Ledger, BiHistory, stream, OLAP, writes, production_cache" }
337
+ end
338
+
339
+ def evaluate_valid_time_node(access_node, temporal_inputs, inputs, contract_id)
340
+ source = access_node.fetch("source_ref")
341
+ template = temporal_inputs.fetch(source).fetch("store_ref")
342
+ as_of_ref = access_node["as_of_ref"] ||
343
+ access_node.dig("coordinate_refs", "as_of") ||
344
+ "as_of"
345
+ subject = render_ref(template, inputs)
346
+ as_of = inputs.fetch(as_of_ref)
347
+
348
+ result, backend_obs = @backend.read_as_of(subject, as_of)
349
+ backend_identity = @backend_identity_check[:backend_identity] || {}
350
+
351
+ # AT-10: unconditional observation per read — not gated on persistence readiness
352
+ @observations << {
353
+ "kind" => "temporal_live_read_observation",
354
+ "contract_id" => contract_id,
355
+ "node" => access_node.fetch("name"),
356
+ "axis" => "valid_time",
357
+ "subject" => subject,
358
+ "as_of" => as_of,
359
+ "result_present" => result.is_a?(Hash) && result["kind"] == "some",
360
+ "backend_identity" => backend_identity,
361
+ "backend_observation_ref" => backend_obs.fetch("observation_id"),
362
+ "persistence" => "proof_local"
363
+ }
364
+
365
+ { "node" => access_node.fetch("name"), "axis" => "valid_time",
366
+ "result" => result, "backend_observation" => backend_obs }
367
+ end
368
+
369
+ # Build a minimal CompatibilityReport-shaped hash for blocked paths.
370
+ def build_refusal(check, contract_id:, as_of:, blocked_stage:,
371
+ gate_open:, token_ok:, cache_key_fragment:)
372
+ report = compose_report(
373
+ contract_id: contract_id, gate_open: gate_open,
374
+ token_ok: token_ok, cache_key_ok: cache_key_fragment == "TEMPORAL",
375
+ readiness: "blocked", blocked_reason_code: check[:reason_code]
376
+ )
377
+ @last_compatibility_report = report
378
+
379
+ { "kind" => "evaluation_refusal",
380
+ "status" => "blocked",
381
+ "guard_at" => "temporal_executor_phase1",
382
+ "reason_code" => check[:reason_code],
383
+ "message" => check[:message],
384
+ "contract_id" => contract_id,
385
+ "as_of" => as_of,
386
+ "context" => check.fetch(:context, {}),
387
+ "blocked_stage" => blocked_stage,
388
+ "compatibility_report_id" => report.fetch("report_id"),
389
+ "operation_check" => no_live_operations }
390
+ end
391
+
392
+ # AT-2: compose one CompatibilityReport-shaped hash per evaluation.
393
+ # Single-report mode; split_fragments_allowed always false.
394
+ def compose_report(contract_id:, gate_open:, token_ok:, cache_key_ok:, readiness:,
395
+ blocked_reason_code: nil)
396
+ reason_code = readiness == "ready" ? ReasonCode::EVALUATION_READY : blocked_reason_code
397
+ body = {
398
+ "kind" => "compatibility_report",
399
+ "format_version" => PHASE1_FORMAT_VERSION,
400
+ "contract_id" => contract_id,
401
+ "composition" => {
402
+ "mode" => "single_report",
403
+ "single_report_required" => true,
404
+ "split_fragments_allowed" => false
405
+ },
406
+ "runtime_gate_check" => {
407
+ "gate" => "tbackend_gate3",
408
+ "decision" => gate_open ? "open" : "closed",
409
+ "authority_ref" => gate_open ? GATE3_AUTHORITY_REF : nil
410
+ },
411
+ "executor_approval_check" => {
412
+ "decision" => token_ok ? "ok" : "blocked",
413
+ "reason_code" => token_ok ? "runtime.executor_approval_token_valid" : ReasonCode::APPROVAL_MISSING
414
+ },
415
+ "cache_key_check" => {
416
+ "decision" => cache_key_ok ? "ok" : "blocked",
417
+ "fragment" => cache_key_ok ? "TEMPORAL" : "other",
418
+ "reason_code" => cache_key_ok ? "runtime.temporal_cache_key_valid" : ReasonCode::CACHE_MISMATCH
419
+ },
420
+ "evaluation_readiness" => {
421
+ "decision" => readiness,
422
+ "reason_code" => reason_code,
423
+ "blocks_before_executor" => readiness != "ready"
424
+ },
425
+ "runtime_enforced" => gate_open && token_ok && cache_key_ok && readiness == "ready",
426
+ "report_only" => !(gate_open && token_ok && cache_key_ok && readiness == "ready"),
427
+ "operation_check" => no_live_operations
428
+ }
429
+ body.merge("report_id" => "compat/phase1/#{short_report_hash(body)}")
430
+ end
431
+
432
+ def no_live_operations
433
+ { "temporal_executor_call_attempted" => false,
434
+ "live_tbackend_call_attempted" => false,
435
+ "ledger_call_attempted" => false,
436
+ "temporal_read_attempted" => false,
437
+ "cache_call_attempted" => false }
438
+ end
439
+
440
+ def short_report_hash(value)
441
+ Digest::SHA256.hexdigest(JSON.generate(canonical_normalize(value)))[0, 16]
442
+ end
443
+
444
+ def canonical_normalize(value)
445
+ case value
446
+ when Hash then value.keys.sort_by(&:to_s).each_with_object({}) { |k, h| h[k.to_s] = canonical_normalize(value[k]) }
447
+ when Array then value.map { |v| canonical_normalize(v) }
448
+ else value
449
+ end
450
+ end
451
+
452
+ def render_ref(template, inputs)
453
+ template.gsub(/\{([^}]+)\}/) { inputs.fetch(Regexp.last_match(1)) }
454
+ end
455
+ end
456
+ end
457
+ end