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,717 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "digest"
4
+ require "fileutils"
5
+ require "json"
6
+ require "pathname"
7
+
8
+ module IgniterLang
9
+ class AssemblyRefused < StandardError; end
10
+
11
+ class Assembler
12
+ ROOT = Pathname.new(File.expand_path("../..", __dir__))
13
+ DEFAULT_GOLDEN_DIR = ROOT / "experiments/source_to_semanticir_fixture/golden"
14
+ DEFAULT_OUT_DIR = ROOT / "experiments/igapp_assembler_proof/out"
15
+
16
+ # PROP-036: compiler_profile_id source contract constants.
17
+ # Used by validate_compiler_profile_source! only.
18
+ PROFILE_SOURCE_KIND = "compiler_profile_id_source"
19
+ PROFILE_SOURCE_NAMESPACE = "compiler_profile_unified"
20
+ PROFILE_SOURCE_ID_PATTERN = /\Acompiler_profile_unified\/sha256:[0-9a-f]{24,}\z/
21
+ PROFILE_SOURCE_SLOT_ORDER = %w[
22
+ core oof_registry fragment_registry escape_boundary contract_modifiers
23
+ temporal stream olap invariant assumptions evidence_observation pipeline
24
+ ].freeze
25
+
26
+ module Canonical
27
+ module_function
28
+
29
+ def normalize(value)
30
+ case value
31
+ when Hash
32
+ value.keys.sort_by(&:to_s).each_with_object({}) do |key, out|
33
+ out[key.to_s] = normalize(value[key])
34
+ end
35
+ when Array
36
+ value.map { |item| normalize(item) }
37
+ when Symbol
38
+ value.to_s
39
+ else
40
+ value
41
+ end
42
+ end
43
+
44
+ def json(value)
45
+ JSON.pretty_generate(normalize(value)) + "\n"
46
+ end
47
+
48
+ def hash(value)
49
+ "sha256:#{Digest::SHA256.hexdigest(JSON.generate(normalize(value)))}"
50
+ end
51
+
52
+ def short_hash(value)
53
+ hash(value).split(":").last[0, 16]
54
+ end
55
+ end
56
+
57
+ def initialize(golden_dir: DEFAULT_GOLDEN_DIR, out_dir: DEFAULT_OUT_DIR)
58
+ @golden_dir = Pathname.new(golden_dir)
59
+ @out_dir = Pathname.new(out_dir)
60
+ end
61
+
62
+ def assemble_case(case_name, compiler_profile_source: nil)
63
+ report = read_json(@golden_dir / "#{case_name}.compilation_report.json")
64
+ refuse!(case_name, "pass_result=#{report.fetch("pass_result")}") unless report.fetch("pass_result") == "ok"
65
+ refuse!(case_name, "semantic_ir_ref missing") unless report.fetch("semantic_ir_ref").is_a?(String)
66
+
67
+ semantic_ir = read_json(@golden_dir / "#{case_name}.semantic_ir.json")
68
+ validate_refs!(case_name, report, semantic_ir)
69
+ validate_semantic_ir!(case_name, semantic_ir)
70
+
71
+ artifact = build_artifact(case_name, report, semantic_ir, compiler_profile_source: compiler_profile_source)
72
+ write_artifact(case_name, artifact)
73
+ artifact_summary(case_name, artifact)
74
+ end
75
+
76
+ def assemble_artifacts(case_name:, report:, semantic_ir:, target_dir:, compiler_profile_source: nil)
77
+ refuse!(case_name, "pass_result=#{report.fetch("pass_result")}") unless report.fetch("pass_result") == "ok"
78
+ refuse!(case_name, "semantic_ir_ref missing") unless report.fetch("semantic_ir_ref").is_a?(String)
79
+ validate_refs!(case_name, report, semantic_ir)
80
+ validate_semantic_ir!(case_name, semantic_ir)
81
+
82
+ artifact = build_artifact(case_name, report, semantic_ir, compiler_profile_source: compiler_profile_source)
83
+ target = Pathname.new(target_dir)
84
+ write_artifact_to(target, artifact)
85
+ artifact_summary_for_target(case_name, artifact, target)
86
+ end
87
+
88
+ def refuse_case(case_name)
89
+ report = read_json(@golden_dir / "#{case_name}.compilation_report.json")
90
+ assemble_case(case_name)
91
+ raise "expected #{case_name} to refuse, but it assembled"
92
+ rescue AssemblyRefused => e
93
+ target = @out_dir / "#{case_name}.igapp"
94
+ {
95
+ "case" => case_name,
96
+ "status" => "refused",
97
+ "pass_result" => report.fetch("pass_result"),
98
+ "reason" => e.message,
99
+ "wrote_igapp" => target.exist?
100
+ }
101
+ end
102
+
103
+ private
104
+
105
+ def read_json(path)
106
+ JSON.parse(File.read(path))
107
+ end
108
+
109
+ def refuse!(case_name, reason)
110
+ raise AssemblyRefused, "#{case_name}: #{reason}"
111
+ end
112
+
113
+ # PROP-036: validate a compiler_profile_id_source object before assembler use.
114
+ # Raises AssemblyRefused with compiler_profile_source.* reason text on any
115
+ # invalid input. Must not emit loader/report status values.
116
+ def validate_compiler_profile_source!(case_name, source)
117
+ unless source.is_a?(Hash)
118
+ refuse!(case_name, "compiler_profile_source.malformed: source must be a Hash")
119
+ end
120
+
121
+ kind = source["kind"]
122
+ unless kind == PROFILE_SOURCE_KIND
123
+ refuse!(case_name, "compiler_profile_source.wrong_kind: #{kind.inspect}")
124
+ end
125
+
126
+ status = source["status"]
127
+ unless status == "finalized"
128
+ refuse!(case_name, "compiler_profile_source.unfinalized: status=#{status.inspect}")
129
+ end
130
+
131
+ namespace = source["profile_namespace"]
132
+ unless namespace == PROFILE_SOURCE_NAMESPACE
133
+ refuse!(case_name, "compiler_profile_source.unsupported_namespace: #{namespace.inspect}")
134
+ end
135
+
136
+ cid = source["compiler_profile_id"]
137
+ unless cid.is_a?(String) && cid.match?(PROFILE_SOURCE_ID_PATTERN)
138
+ refuse!(case_name, "compiler_profile_source.malformed_id: #{cid.inspect}")
139
+ end
140
+
141
+ slot_order = source["slot_order"]
142
+ unless slot_order == PROFILE_SOURCE_SLOT_ORDER
143
+ refuse!(case_name, "compiler_profile_source.slot_order_mismatch")
144
+ end
145
+
146
+ # Reconstruct finalization payload and verify digest + id consistency.
147
+ payload = {
148
+ "profile_namespace" => source["profile_namespace"],
149
+ "format_version" => source["format_version"],
150
+ "descriptor_digest" => source["descriptor_digest"],
151
+ "profile_kind" => source["profile_kind"],
152
+ "slot_order" => source["slot_order"],
153
+ "slot_assignments" => source.fetch("slot_assignments", {})
154
+ }
155
+ payload_hex = Digest::SHA256.hexdigest(JSON.generate(Canonical.normalize(payload)))
156
+ expected_payload_digest = "sha256:#{payload_hex}"
157
+ expected_profile_id = "#{PROFILE_SOURCE_NAMESPACE}/sha256:#{payload_hex[0, 24]}"
158
+
159
+ unless source["finalization_payload_digest"] == expected_payload_digest
160
+ refuse!(case_name, "compiler_profile_source.id_digest_mismatch: finalization_payload_digest")
161
+ end
162
+ unless cid == expected_profile_id
163
+ refuse!(case_name, "compiler_profile_source.id_digest_mismatch: compiler_profile_id")
164
+ end
165
+
166
+ if source["runtime_authority_granted"] == true
167
+ refuse!(case_name, "compiler_profile_source.runtime_authority_forbidden")
168
+ end
169
+ if source["dispatch_migration_authorized"] == true
170
+ refuse!(case_name, "compiler_profile_source.dispatch_migration_forbidden")
171
+ end
172
+ end
173
+
174
+ def validate_refs!(case_name, report, semantic_ir)
175
+ refuse!(case_name, "SemanticIR kind=#{semantic_ir.fetch("kind", nil)}") unless semantic_ir.fetch("kind") == "semantic_ir_program"
176
+ refuse!(case_name, "semantic_ir_ref mismatch") unless report.fetch("semantic_ir_ref") == semantic_ir.fetch("program_id")
177
+ return if semantic_ir.fetch("compilation_report_ref") == report.fetch("program_id")
178
+
179
+ refuse!(case_name, "compilation_report_ref mismatch")
180
+ end
181
+
182
+ def validate_semantic_ir!(case_name, semantic_ir)
183
+ semantic_ir.fetch("contracts").each do |contract|
184
+ refuse!(case_name, "OOF contract emitted: #{contract.fetch("contract_name")}") if contract.fetch("fragment_class") == "oof"
185
+ end
186
+
187
+ return unless JSON.generate(semantic_ir).include?("stdlib.numeric.")
188
+
189
+ refuse!(case_name, "unresolved stdlib.numeric operator in SemanticIR")
190
+ end
191
+
192
+ def build_artifact(case_name, report, semantic_ir, compiler_profile_source: nil)
193
+ # PROP-036: validate source object and extract id before building hash material.
194
+ # validate_compiler_profile_source! raises AssemblyRefused on any invalid input.
195
+ compiler_profile_id = if compiler_profile_source
196
+ validate_compiler_profile_source!(case_name, compiler_profile_source)
197
+ compiler_profile_source.fetch("compiler_profile_id")
198
+ end
199
+
200
+ contracts = semantic_ir.fetch("contracts").map { |contract| contract_file(contract) }
201
+ contract_ids = contracts.map { |contract| contract.fetch("contract_id") }.sort
202
+ fragment_classes = contracts.map { |contract| contract.fetch("fragment_class") }.uniq
203
+ fragment_class = fragment_classes.length == 1 ? fragment_classes.first : "mixed"
204
+ requirements = requirements_for(semantic_ir)
205
+ classified_ast = classified_ast_for(report, semantic_ir, contract_ids, fragment_class)
206
+ diagnostics = { "diagnostics" => report.fetch("diagnostics") }
207
+ compatibility_metadata = compatibility_metadata_for(report, semantic_ir)
208
+
209
+ artifact_material = {
210
+ "semantic_ir_program" => semantic_ir,
211
+ "contracts" => contracts,
212
+ "compilation_report" => report,
213
+ "requirements" => requirements,
214
+ "diagnostics" => diagnostics,
215
+ "classified_ast" => classified_ast,
216
+ "compatibility_metadata" => compatibility_metadata
217
+ }
218
+ # PROP-036: inject compiler_profile_id into hash material BEFORE artifact_hash
219
+ # is computed. Adding it after Canonical.hash is called is forbidden.
220
+ artifact_material["compiler_profile_id"] = compiler_profile_id if compiler_profile_id
221
+
222
+ artifact_hash = Canonical.hash(artifact_material)
223
+ contracts = contracts.map { |contract| contract.merge("artifact_hash" => artifact_hash) }
224
+ fragment_summary = fragment_summary_for(contracts)
225
+ contract_index = contract_index_for(contracts)
226
+
227
+ manifest = {
228
+ "kind" => "igapp_manifest",
229
+ "format_version" => "0.1.0",
230
+ "format" => "igapp_dir",
231
+ "program_id" => semantic_ir.fetch("program_id"),
232
+ "artifact_hash" => artifact_hash,
233
+ "language_version" => semantic_ir.fetch("format_version"),
234
+ "grammar_version" => semantic_ir.fetch("grammar_version"),
235
+ "schema_version" => "0.1.0",
236
+ "compiled_at" => "2026-05-06T00:00:00Z",
237
+ "assembler" => "igapp-assembler-proof-stage1-v0",
238
+ "semantic_ir_ref" => report.fetch("semantic_ir_ref"),
239
+ "compilation_report_ref" => semantic_ir.fetch("compilation_report_ref"),
240
+ "source_hash" => semantic_ir.fetch("source_hash"),
241
+ "source_path" => semantic_ir.fetch("source_path"),
242
+ "contracts" => contract_ids,
243
+ "contract_refs" => semantic_ir.fetch("contracts").to_h do |contract|
244
+ [contract.fetch("contract_name"), contract.fetch("contract_ref")]
245
+ end,
246
+ "fragment_class" => fragment_class,
247
+ "fragment_summary" => fragment_summary,
248
+ "contract_index" => contract_index,
249
+ "schema_descriptor" => { "trait_bounds" => [], "migrations" => [] },
250
+ "warnings" => [],
251
+ "diagnostics" => report.fetch("diagnostics")
252
+ }
253
+ # PROP-036: top-level manifest field; only present when a valid source is supplied.
254
+ manifest["compiler_profile_id"] = compiler_profile_id if compiler_profile_id
255
+
256
+ {
257
+ "case" => case_name,
258
+ "manifest" => manifest,
259
+ "semantic_ir_program" => semantic_ir,
260
+ "contracts" => contracts,
261
+ "compilation_report" => report,
262
+ "requirements" => requirements,
263
+ "diagnostics" => diagnostics,
264
+ "classified_ast" => classified_ast,
265
+ "projections" => { "projections" => [] },
266
+ "compatibility_metadata" => compatibility_metadata
267
+ }
268
+ end
269
+
270
+ def contract_file(contract_ir)
271
+ contract_id = contract_ir.fetch("contract_name")
272
+ input_ports = ports(contract_ir.fetch("inputs"))
273
+ output_ports = ports(contract_ir.fetch("outputs"))
274
+ semantic_nodes = contract_ir.fetch("nodes")
275
+ compute_nodes = semantic_nodes.filter_map do |node|
276
+ next unless compute_node?(node)
277
+
278
+ {
279
+ "node_id" => "node_#{node.fetch("name")}",
280
+ "name" => node.fetch("name"),
281
+ "kind" => node.fetch("kind"),
282
+ "fragment_class" => node.fetch("fragment"),
283
+ "type_tag" => type_name(node.fetch("type")),
284
+ "lifecycle" => "session",
285
+ "obs_kind" => "value_observation",
286
+ "dependencies" => node.fetch("deps").map { |dep| "input:#{dep}" },
287
+ "expression" => compat_expr(node.fetch("expr"))
288
+ }
289
+ end
290
+ temporal_nodes = semantic_nodes.filter_map do |node|
291
+ next unless temporal_node?(node)
292
+
293
+ temporal_node_file(node)
294
+ end
295
+ stream_nodes = semantic_nodes.filter_map do |node|
296
+ next unless stream_node?(node)
297
+
298
+ stream_node_file(node)
299
+ end
300
+
301
+ result = {
302
+ "contract_id" => contract_id,
303
+ "source_contract_ref" => contract_ir.fetch("contract_ref"),
304
+ "name" => contract_id,
305
+ "fragment_class" => contract_ir.fetch("fragment_class"),
306
+ "escape_set" => contract_ir.fetch("escape_boundaries"),
307
+ "lifecycle" => "session",
308
+ "input_ports" => input_ports,
309
+ "output_ports" => output_ports,
310
+ "compute_nodes" => compute_nodes,
311
+ "type_signature" => {
312
+ "inputs" => input_ports.to_h { |port| [port.fetch("name"), port.fetch("type_tag")] },
313
+ "outputs" => output_ports.to_h { |port| [port.fetch("name"), port.fetch("type_tag")] }
314
+ }
315
+ }
316
+ result["temporal_nodes"] = temporal_nodes unless temporal_nodes.empty?
317
+ result["stream_nodes"] = stream_nodes unless stream_nodes.empty?
318
+ result
319
+ end
320
+
321
+ def compute_node?(node)
322
+ node.key?("expr") && node.key?("type")
323
+ end
324
+
325
+ def temporal_node?(node)
326
+ %w[temporal_input_node temporal_access_node].include?(node.fetch("kind", nil))
327
+ end
328
+
329
+ def stream_node?(node)
330
+ %w[stream_input_node window_decl_node fold_stream_node].include?(node.fetch("kind", nil))
331
+ end
332
+
333
+ def temporal_node_file(node)
334
+ result = {
335
+ "node_id" => "node_#{node.fetch("name")}",
336
+ "name" => node.fetch("name"),
337
+ "kind" => node.fetch("kind"),
338
+ "fragment_class" => node.fetch("fragment", node.fetch("node_fragment_class", "temporal")),
339
+ "node_fragment_class" => node.fetch("node_fragment_class"),
340
+ "value_fragment_class" => node.fetch("value_fragment_class"),
341
+ "lifecycle" => node.fetch("lifecycle", "session"),
342
+ "obs_kind" => temporal_obs_kind(node),
343
+ "dependencies" => node.fetch("deps", []).map { |dep| "input:#{dep}" },
344
+ "required_capability" => node.fetch("required_capability"),
345
+ "required_caps" => node.fetch("required_caps", [node.fetch("required_capability")]),
346
+ "axis" => node.fetch("axis", node.fetch("temporal_axis", nil))
347
+ }
348
+ result["type_tag"] = type_name(node.fetch("type")) if node.key?("type")
349
+ result["result_type_tag"] = type_name(node.fetch("result_type")) if node.key?("result_type")
350
+ result["store_ref"] = node.fetch("store_ref") if node.key?("store_ref")
351
+ result["source_ref"] = node.fetch("source_ref") if node.key?("source_ref")
352
+ result["temporal_axis"] = node.fetch("temporal_axis") if node.key?("temporal_axis")
353
+ result["coordinate_refs"] = node.fetch("coordinate_refs") if node.key?("coordinate_refs")
354
+ result["as_of_ref"] = node.fetch("as_of_ref") if node.key?("as_of_ref")
355
+ result["valid_time_ref"] = node.fetch("valid_time_ref") if node.key?("valid_time_ref")
356
+ result["transaction_time_ref"] = node.fetch("transaction_time_ref") if node.key?("transaction_time_ref")
357
+ result["evidence_policy"] = node.fetch("evidence_policy") if node.key?("evidence_policy")
358
+ result
359
+ end
360
+
361
+ def temporal_obs_kind(node)
362
+ node.fetch("kind") == "temporal_input_node" ? "temporal_source_observation" : "temporal_access_observation"
363
+ end
364
+
365
+ def stream_node_file(node)
366
+ name = node.fetch("name", node.fetch("ref", nil))
367
+ result = {
368
+ "node_id" => "node_#{name}",
369
+ "name" => name,
370
+ "kind" => node.fetch("kind"),
371
+ "fragment_class" => node.fetch("fragment", node.fetch("result_fragment", "stream")),
372
+ "lifecycle" => "window",
373
+ "obs_kind" => stream_obs_kind(node),
374
+ "dependencies" => node.fetch("deps", []).map { |dep| "input:#{dep}" }
375
+ }
376
+ result["type_tag"] = type_name(node.fetch("type")) if node.key?("type")
377
+ result["result_type_tag"] = type_name(node.fetch("result_type")) if node.key?("result_type")
378
+ %w[
379
+ window_ref ref key window_kind bounded size period idle on_close stream_ref init fn_ref
380
+ bound event_binding escape_capability result_fragment
381
+ ].each do |key|
382
+ result[key] = node.fetch(key) if node.key?(key)
383
+ end
384
+ result
385
+ end
386
+
387
+ def stream_obs_kind(node)
388
+ node.fetch("kind") == "window_decl_node" ? "stream_window_observation" : "stream_replay_metadata"
389
+ end
390
+
391
+ def fragment_summary_for(contracts)
392
+ fragment_classes = contracts.map { |contract| contract.fetch("fragment_class") }.uniq.sort
393
+ {
394
+ "fragment_classes" => fragment_classes,
395
+ "max_fragment_class" => max_fragment_class(fragment_classes),
396
+ "precedence_high_to_low" => fragment_precedence
397
+ }
398
+ end
399
+
400
+ def max_fragment_class(fragment_classes)
401
+ fragment_precedence.find { |fragment| fragment_classes.include?(fragment) } || "core"
402
+ end
403
+
404
+ def fragment_precedence
405
+ %w[oof temporal stream escape core]
406
+ end
407
+
408
+ def contract_index_for(contracts)
409
+ contracts.sort_by { |contract| contract.fetch("contract_id") }.to_h do |contract|
410
+ entry = {
411
+ "contract_ref" => contract.fetch("source_contract_ref"),
412
+ "contract_path" => "contracts/#{snake_case(contract.fetch("contract_id"))}.json",
413
+ "fragment_class" => contract.fetch("fragment_class")
414
+ }
415
+ entry["temporal"] = temporal_contract_index(contract) if contract.fetch("fragment_class") == "temporal"
416
+ [contract.fetch("contract_id"), entry]
417
+ end
418
+ end
419
+
420
+ def temporal_contract_index(contract)
421
+ temporal_nodes = contract.fetch("temporal_nodes", [])
422
+ access_nodes = temporal_nodes.select { |node| node.fetch("kind") == "temporal_access_node" }
423
+ coordinates = access_nodes.flat_map { |node| temporal_coordinates_for(contract, node) }
424
+ axes = coordinates.map { |coordinate| coordinate.fetch("axis") }.uniq
425
+ required_caps = (
426
+ contract.fetch("escape_set", []).flat_map { |boundary| boundary.fetch("required_caps", []) } +
427
+ temporal_nodes.flat_map { |node| node.fetch("required_caps", []) }
428
+ ).uniq.sort
429
+ hint_axis = access_nodes.map { |node| node.fetch("axis", node.fetch("temporal_axis", nil)) }.compact.uniq
430
+ {
431
+ "axes" => axes.sort_by { |axis| temporal_axis_sort_key(axis) },
432
+ "required_capabilities" => required_caps,
433
+ "coordinates" => coordinates,
434
+ "cache_key_schema_hint" => {
435
+ "schema" => "runtime-cache-key-v1",
436
+ "fragment" => "TEMPORAL",
437
+ "axis" => hint_axis.length == 1 ? hint_axis.first : "mixed",
438
+ "coordinate_names" => coordinates.map { |coord| coord.fetch("name") }
439
+ }
440
+ }
441
+ end
442
+
443
+ def temporal_coordinates_for(contract, access_node)
444
+ coordinate_refs = access_node.fetch("coordinate_refs", {})
445
+ coordinate_refs.map do |axis_name, input_name|
446
+ {
447
+ "name" => input_name,
448
+ "axis" => coordinate_axis(access_node, axis_name),
449
+ "source_ref" => "input:#{input_name}",
450
+ "type" => input_type(contract, input_name)
451
+ }
452
+ end.sort_by { |coordinate| temporal_axis_sort_key(coordinate.fetch("axis")) }
453
+ end
454
+
455
+ def coordinate_axis(access_node, axis_name)
456
+ access_axis = access_node.fetch("axis", access_node.fetch("temporal_axis", nil))
457
+ access_axis == "bitemporal" ? axis_name : access_axis
458
+ end
459
+
460
+ def temporal_axis_sort_key(axis)
461
+ %w[valid_time transaction_time bitemporal].index(axis) || 99
462
+ end
463
+
464
+ def input_type(contract, input_name)
465
+ port = contract.fetch("input_ports").find { |input| input.fetch("name") == input_name }
466
+ port ? port.fetch("type_tag") : "Unknown"
467
+ end
468
+
469
+ def ports(port_irs)
470
+ port_irs.map do |port|
471
+ {
472
+ "name" => port.fetch("name"),
473
+ "type_tag" => type_name(port.fetch("type")),
474
+ "lifecycle" => port.fetch("lifecycle"),
475
+ "required" => true
476
+ }
477
+ end
478
+ end
479
+
480
+ def compat_expr(expr)
481
+ case expr.fetch("kind")
482
+ when "call"
483
+ {
484
+ "kind" => "apply",
485
+ "operator" => expr.fetch("fn"),
486
+ "operands" => expr.fetch("args").map { |arg| compat_expr(arg) }
487
+ }
488
+ when "ref"
489
+ { "kind" => "ref", "name" => expr.fetch("name") }
490
+ when "literal"
491
+ { "kind" => "literal", "value" => expr.fetch("value"), "type_tag" => type_name(expr.fetch("resolved_type")) }
492
+ when "field_access"
493
+ {
494
+ "kind" => "field_access",
495
+ "object" => compat_expr(expr.fetch("object")),
496
+ "field" => expr.fetch("field"),
497
+ "type_tag" => type_name(expr.fetch("resolved_type"))
498
+ }
499
+ else
500
+ expr
501
+ end
502
+ end
503
+
504
+ def type_name(type)
505
+ return type if type.is_a?(String)
506
+ return type.to_s unless type.is_a?(Hash)
507
+
508
+ if type.key?("constructor")
509
+ element = type.fetch("element_type", nil)
510
+ return element ? "#{type.fetch("constructor")}[#{type_name(element)}]" : type.fetch("constructor")
511
+ end
512
+
513
+ params = type.fetch("params", [])
514
+ return type.fetch("name") if params.empty?
515
+
516
+ "#{type.fetch("name")}[#{params.map { |param| type_name(param) }.join(",")}]"
517
+ end
518
+
519
+ def requirements_for(semantic_ir)
520
+ boundaries = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("escape_boundaries", []) }
521
+ required_caps = boundaries.flat_map { |boundary| boundary.fetch("required_caps", []) }.uniq.sort
522
+ fragments = semantic_ir.fetch("contracts").map { |contract| contract.fetch("fragment_class") }.uniq.sort
523
+ temporal_nodes = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("nodes", []) }
524
+ .select { |node| temporal_node?(node) }
525
+ temporal_access_nodes = temporal_nodes.select { |node| node.fetch("kind") == "temporal_access_node" }
526
+ temporal_axes = temporal_nodes.map { |node| node.fetch("axis", node.fetch("temporal_axis", nil)) }.compact.uniq.sort
527
+ temporal_caps = required_caps & %w[history_read bihistory_read]
528
+ stream_caps = required_caps & %w[stream_input]
529
+ stream_nodes = semantic_ir.fetch("contracts").flat_map { |contract| contract.fetch("nodes", []) }
530
+ .select { |node| stream_node?(node) }
531
+ stream_windows = stream_nodes.select { |node| node.fetch("kind") == "window_decl_node" }
532
+
533
+ {
534
+ "temporal" => {
535
+ "requires_as_of" => temporal_caps.any?,
536
+ "requires_valid_time" => temporal_caps.any?,
537
+ "requires_transaction_time" => required_caps.include?("bihistory_read"),
538
+ "requires_replay" => required_caps.include?("bihistory_read"),
539
+ "requires_snapshot" => false,
540
+ "min_consistency" => "strong",
541
+ "axes" => temporal_axes,
542
+ "coordinate_refs" => temporal_access_nodes.map do |node|
543
+ {
544
+ "node" => node.fetch("name"),
545
+ "axis" => node.fetch("axis", node.fetch("temporal_axis")),
546
+ "coordinates" => node.fetch("coordinate_refs", {})
547
+ }
548
+ end,
549
+ "windows" => stream_windows.map { |node| stream_window_requirement(node) },
550
+ "slices" => []
551
+ },
552
+ "lifecycle" => {
553
+ "min_lifecycle" => "local",
554
+ "has_audit" => temporal_caps.any?,
555
+ "has_window" => stream_caps.any?
556
+ },
557
+ "fragments" => fragments,
558
+ "capabilities" => {
559
+ "required_caps" => required_caps,
560
+ "effect_kinds" => effect_kinds_for(boundaries)
561
+ },
562
+ "effects" => [],
563
+ "ffi" => [],
564
+ "required_tbackend_caps" => {
565
+ "read_as_of" => temporal_caps.any?,
566
+ "append_atomic" => false,
567
+ "replay_enabled" => required_caps.include?("bihistory_read"),
568
+ "snapshot_enabled" => false,
569
+ "compact_enabled" => false,
570
+ "subscribe_enabled" => false,
571
+ "consistency" => "strong"
572
+ }
573
+ }
574
+ end
575
+
576
+ def effect_kinds_for(boundaries)
577
+ boundaries.flat_map { |boundary| boundary.fetch("produces", []) }.uniq.sort
578
+ end
579
+
580
+ def stream_window_requirement(node)
581
+ result = {
582
+ "ref" => node.fetch("ref"),
583
+ "kind" => node.fetch("window_kind", nil),
584
+ "bounded" => node.fetch("bounded", false)
585
+ }
586
+ %w[size period idle on_close].each do |key|
587
+ result[key] = node.fetch(key) if node.key?(key)
588
+ end
589
+ result.compact
590
+ end
591
+
592
+ def classified_ast_for(report, semantic_ir, contract_ids, fragment_class)
593
+ {
594
+ "kind" => "classified_program",
595
+ "format_version" => "0.1.0",
596
+ "program_id" => semantic_ir.fetch("program_id"),
597
+ "source_hash" => semantic_ir.fetch("source_hash"),
598
+ "source_path" => semantic_ir.fetch("source_path"),
599
+ "pass_result" => report.fetch("pass_result"),
600
+ "semantic_ir_ref" => report.fetch("semantic_ir_ref"),
601
+ "compilation_report_ref" => semantic_ir.fetch("compilation_report_ref"),
602
+ "fragment_class" => fragment_class,
603
+ "oof_count" => 0,
604
+ "contracts" => contract_ids,
605
+ "loadable_contracts" => contract_ids
606
+ }
607
+ end
608
+
609
+ def compatibility_metadata_for(report, semantic_ir)
610
+ metadata = {
611
+ "kind" => "igapp_compatibility_metadata",
612
+ "format_version" => "0.1.0",
613
+ "canonical_semantic_ir_ref" => semantic_ir.fetch("program_id"),
614
+ "compilation_report_ref" => report.fetch("program_id"),
615
+ "loader_shape" => "runtime_machine_memory_proof.prop0191_direct_v0",
616
+ "canonical_artifact" => "semantic_ir_program.json",
617
+ "runtime_compatibility_artifact" => nil,
618
+ "notes" => [
619
+ "semantic_ir_program.json preserves PROP-019.1 envelope",
620
+ "RuntimeMachine proof loader reads semantic_ir_program.json directly"
621
+ ]
622
+ }
623
+ if temporal_artifact?(semantic_ir)
624
+ metadata["runtime_execution"] = {
625
+ "status" => "unsupported",
626
+ "guard_policy" => "load_accept_evaluate_refuse",
627
+ "guard_at" => "evaluate",
628
+ "load" => {
629
+ "decision" => "accept_for_inspection",
630
+ "requires_contract_index" => true
631
+ },
632
+ "evaluate" => {
633
+ "decision" => "refuse_temporal_contract",
634
+ "reason_code" => "runtime.temporal_execution_unsupported"
635
+ },
636
+ "reason" => "temporal SemanticIR assembly proof preserves artifact shape only; RuntimeMachine temporal execution is out of scope"
637
+ }
638
+ metadata["notes"] += [
639
+ "temporal_input_node and temporal_access_node are preserved as non-compute contract nodes",
640
+ "temporal runtime execution requires a separate RuntimeMachine temporal adapter/hook slice"
641
+ ]
642
+ end
643
+ metadata
644
+ end
645
+
646
+ def temporal_artifact?(semantic_ir)
647
+ semantic_ir.fetch("contracts").any? do |contract|
648
+ contract.fetch("nodes", []).any? { |node| temporal_node?(node) }
649
+ end
650
+ end
651
+
652
+ def write_artifact(case_name, artifact)
653
+ target = @out_dir / "#{case_name}.igapp"
654
+ write_artifact_to(target, artifact)
655
+ end
656
+
657
+ def write_artifact_to(target, artifact)
658
+ FileUtils.rm_rf(target)
659
+ FileUtils.mkdir_p(target / "contracts")
660
+
661
+ write_json(target / "manifest.json", artifact.fetch("manifest"))
662
+ write_json(target / "semantic_ir_program.json", artifact.fetch("semantic_ir_program"))
663
+ write_json(target / "compilation_report.json", artifact.fetch("compilation_report"))
664
+ write_json(target / "requirements.json", artifact.fetch("requirements"))
665
+ write_json(target / "diagnostics.json", artifact.fetch("diagnostics"))
666
+ write_json(target / "classified_ast.json", artifact.fetch("classified_ast"))
667
+ write_json(target / "projections.json", artifact.fetch("projections"))
668
+ write_json(target / "compatibility_metadata.json", artifact.fetch("compatibility_metadata"))
669
+ artifact.fetch("contracts").each do |contract|
670
+ write_json(target / "contracts/#{snake_case(contract.fetch("contract_id"))}.json", contract)
671
+ end
672
+ end
673
+
674
+ def write_json(path, value)
675
+ File.write(path, Canonical.json(value))
676
+ end
677
+
678
+ def snake_case(value)
679
+ value.gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase
680
+ end
681
+
682
+ def artifact_summary(case_name, artifact)
683
+ {
684
+ "case" => case_name,
685
+ "status" => "assembled",
686
+ "igapp_dir" => (@out_dir / "#{case_name}.igapp").relative_path_from(ROOT).to_s,
687
+ "program_id" => artifact.fetch("manifest").fetch("program_id"),
688
+ "artifact_hash" => artifact.fetch("manifest").fetch("artifact_hash"),
689
+ "semantic_ir_ref" => artifact.fetch("manifest").fetch("semantic_ir_ref"),
690
+ "compilation_report_ref" => artifact.fetch("manifest").fetch("compilation_report_ref"),
691
+ "contracts" => artifact.fetch("manifest").fetch("contracts"),
692
+ "files" => artifact_files(case_name)
693
+ }
694
+ end
695
+
696
+ def artifact_summary_for_target(case_name, artifact, target)
697
+ {
698
+ "case" => case_name,
699
+ "status" => "assembled",
700
+ "igapp_dir" => target.to_s,
701
+ "program_id" => artifact.fetch("manifest").fetch("program_id"),
702
+ "artifact_hash" => artifact.fetch("manifest").fetch("artifact_hash"),
703
+ "semantic_ir_ref" => artifact.fetch("manifest").fetch("semantic_ir_ref"),
704
+ "compilation_report_ref" => artifact.fetch("manifest").fetch("compilation_report_ref"),
705
+ "contracts" => artifact.fetch("manifest").fetch("contracts"),
706
+ "files" => target.find.select(&:file?).map { |path| path.relative_path_from(target).to_s }.sort
707
+ }
708
+ end
709
+
710
+ def artifact_files(case_name)
711
+ target = @out_dir / "#{case_name}.igapp"
712
+ target.find.select(&:file?).map { |path| path.relative_path_from(target).to_s }.sort
713
+ end
714
+ end
715
+
716
+ IgappAssembler = Assembler
717
+ end