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,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
|