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,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "diagnostics"
|
|
4
|
+
|
|
5
|
+
module IgniterLang
|
|
6
|
+
module CompilerResult
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def ok(format_version:, semantic_ir:, source_path:, report:, igapp_path:, contracts:, runtime_smoke:)
|
|
10
|
+
{
|
|
11
|
+
"kind" => "compiler_result",
|
|
12
|
+
"format_version" => format_version,
|
|
13
|
+
"status" => "ok",
|
|
14
|
+
"program_id" => semantic_ir.fetch("program_id"),
|
|
15
|
+
"source_path" => source_path.to_s,
|
|
16
|
+
"source_hash" => report.fetch("source_hash"),
|
|
17
|
+
"grammar_version" => report.fetch("grammar_version"),
|
|
18
|
+
"stages" => stages(report, assemble: "ok"),
|
|
19
|
+
"igapp_path" => igapp_path.to_s,
|
|
20
|
+
"compilation_report_ref" => report.fetch("program_id"),
|
|
21
|
+
"semantic_ir_ref" => report.fetch("semantic_ir_ref"),
|
|
22
|
+
"contracts" => contracts,
|
|
23
|
+
"diagnostics" => [],
|
|
24
|
+
"warnings" => Diagnostics.warnings(report.fetch("diagnostics", [])),
|
|
25
|
+
"runtime_smoke" => runtime_smoke,
|
|
26
|
+
"report" => report
|
|
27
|
+
}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def refusal(format_version:, status:, report:, source_path:, report_path:)
|
|
31
|
+
diagnostics = report.fetch("diagnostics", [])
|
|
32
|
+
{
|
|
33
|
+
"kind" => "compiler_result",
|
|
34
|
+
"format_version" => format_version,
|
|
35
|
+
"status" => status,
|
|
36
|
+
"program_id" => report.fetch("semantic_ir_ref", nil),
|
|
37
|
+
"source_path" => source_path.to_s,
|
|
38
|
+
"source_hash" => report.fetch("source_hash", nil),
|
|
39
|
+
"grammar_version" => report.fetch("grammar_version", nil),
|
|
40
|
+
"stages" => stages(report, assemble: "skipped"),
|
|
41
|
+
"igapp_path" => nil,
|
|
42
|
+
"contracts" => [],
|
|
43
|
+
"compilation_report_path" => report_path.to_s,
|
|
44
|
+
"diagnostics" => Diagnostics.errors(diagnostics),
|
|
45
|
+
"warnings" => Diagnostics.warnings(diagnostics),
|
|
46
|
+
"report" => report
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def strict_terminal(format_version:, status:, report:, source_path:, diagnostics:)
|
|
51
|
+
{
|
|
52
|
+
"kind" => "compiler_result",
|
|
53
|
+
"format_version" => format_version,
|
|
54
|
+
"status" => status,
|
|
55
|
+
"program_id" => report.fetch("semantic_ir_ref", nil),
|
|
56
|
+
"source_path" => source_path.to_s,
|
|
57
|
+
"source_hash" => report.fetch("source_hash", nil),
|
|
58
|
+
"grammar_version" => report.fetch("grammar_version", nil),
|
|
59
|
+
"stages" => stages(report, assemble: "skipped"),
|
|
60
|
+
"igapp_path" => nil,
|
|
61
|
+
"contracts" => [],
|
|
62
|
+
"compilation_report_path" => nil,
|
|
63
|
+
"diagnostics" => diagnostics,
|
|
64
|
+
"warnings" => Diagnostics.warnings(report.fetch("diagnostics", [])),
|
|
65
|
+
"report" => report
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def public_result(result)
|
|
70
|
+
result.reject { |key, _value| key == "report" }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def stages(report, assemble:)
|
|
74
|
+
report.fetch("stages").merge("assemble" => assemble)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module IgniterLang
|
|
4
|
+
module Diagnostics
|
|
5
|
+
CATEGORIES = {
|
|
6
|
+
parse_error: "parser_error",
|
|
7
|
+
parse_warning: "parser_warning",
|
|
8
|
+
classified: "classifier_oof",
|
|
9
|
+
typechecked: "typechecker_oof",
|
|
10
|
+
assembler: "assembler_refusal",
|
|
11
|
+
runtime_smoke: "runtime_smoke_failure"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def enrich(entries, category:, contract: nil)
|
|
17
|
+
Array(entries).map do |entry|
|
|
18
|
+
normalized = stringify_keys(entry)
|
|
19
|
+
diagnostic_contract = normalized.key?("contract") ? normalized.fetch("contract") : contract
|
|
20
|
+
node = normalized.fetch("node", nil)
|
|
21
|
+
path = normalized.fetch("path", nil) || path_for(diagnostic_contract, node, normalized)
|
|
22
|
+
span = span_for(normalized)
|
|
23
|
+
|
|
24
|
+
normalized.merge(
|
|
25
|
+
"category" => normalized.fetch("category", category),
|
|
26
|
+
"rule" => normalized.fetch("rule", "UNKNOWN"),
|
|
27
|
+
"severity" => normalized.fetch("severity", "error"),
|
|
28
|
+
"message" => normalized.fetch("message", "compiler diagnostic"),
|
|
29
|
+
"contract" => diagnostic_contract,
|
|
30
|
+
"node" => node,
|
|
31
|
+
"path" => path,
|
|
32
|
+
"span" => span
|
|
33
|
+
).reject { |key, _value| key == "line" || key == "col" }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def from_parse_errors(errors)
|
|
38
|
+
Array(errors).flat_map do |entry|
|
|
39
|
+
severity = stringify_keys(entry).fetch("severity", "error")
|
|
40
|
+
category = severity == "warning" ? CATEGORIES.fetch(:parse_warning) : CATEGORIES.fetch(:parse_error)
|
|
41
|
+
enrich([entry], category: category)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def from_classified(diagnostics, contract: nil)
|
|
46
|
+
enrich(diagnostics, category: CATEGORIES.fetch(:classified), contract: contract)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def from_typechecked(diagnostics, contract: nil)
|
|
50
|
+
enrich(diagnostics, category: CATEGORIES.fetch(:typechecked), contract: contract)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def from_assembler_refusal(refusal)
|
|
54
|
+
enrich(
|
|
55
|
+
[
|
|
56
|
+
{
|
|
57
|
+
"rule" => "ASSEMBLER-REFUSAL",
|
|
58
|
+
"severity" => "error",
|
|
59
|
+
"message" => refusal.respond_to?(:message) ? refusal.message : refusal.to_s
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
category: CATEGORIES.fetch(:assembler)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def from_runtime_smoke(smoke)
|
|
67
|
+
return [] if smoke.fetch("trusted", false)
|
|
68
|
+
|
|
69
|
+
enrich(
|
|
70
|
+
[
|
|
71
|
+
{
|
|
72
|
+
"rule" => "OOF-RUNTIME-SMOKE",
|
|
73
|
+
"severity" => "error",
|
|
74
|
+
"message" => "RuntimeMachine load/evaluate smoke failed",
|
|
75
|
+
"details" => smoke
|
|
76
|
+
}
|
|
77
|
+
],
|
|
78
|
+
category: CATEGORIES.fetch(:runtime_smoke)
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def warnings(entries)
|
|
83
|
+
Array(entries).select { |entry| entry.fetch("severity", nil) == "warning" }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def errors(entries)
|
|
87
|
+
Array(entries).reject { |entry| entry.fetch("severity", nil) == "warning" }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def stringify_keys(value)
|
|
91
|
+
case value
|
|
92
|
+
when Hash
|
|
93
|
+
value.each_with_object({}) { |(key, entry), out| out[key.to_s] = stringify_keys(entry) }
|
|
94
|
+
when Array
|
|
95
|
+
value.map { |entry| stringify_keys(entry) }
|
|
96
|
+
else
|
|
97
|
+
value
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def path_for(contract, node, entry)
|
|
102
|
+
return nil unless contract || node
|
|
103
|
+
|
|
104
|
+
parts = []
|
|
105
|
+
parts << "contract:#{contract}" if contract
|
|
106
|
+
parts << "#{node_kind(entry)}:#{node}" if node
|
|
107
|
+
parts.join("/")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def node_kind(entry)
|
|
111
|
+
entry.fetch("node_kind", nil) || entry.fetch("kind", nil) || "node"
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def span_for(entry)
|
|
115
|
+
span = entry.fetch("span", nil)
|
|
116
|
+
return span if span.is_a?(Hash)
|
|
117
|
+
|
|
118
|
+
line = entry.fetch("line", nil)
|
|
119
|
+
col = entry.fetch("col", nil) || entry.fetch("column", nil)
|
|
120
|
+
return nil unless line && col
|
|
121
|
+
|
|
122
|
+
{ "line" => line, "col" => col }
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fragment Registry Compatibility Adapter — internal direct-require-only helper.
|
|
4
|
+
#
|
|
5
|
+
# ISOLATION CONTRACT — this file must not:
|
|
6
|
+
# - be required from lib/igniter_lang.rb (direct-require-only; no root require)
|
|
7
|
+
# - be wired into the live classifier (classifier_wiring_authorized: false)
|
|
8
|
+
# - participate in live classifier dispatch (held_live_dispatch: true)
|
|
9
|
+
# - return results that become a ClassifiedProgram field, compiler input,
|
|
10
|
+
# report output, CLI/API output, or artifact metadata
|
|
11
|
+
# - touch parser, TypeChecker, SemanticIR, assembler, report, .igapp,
|
|
12
|
+
# runtime, Spark, or production behavior
|
|
13
|
+
#
|
|
14
|
+
# Authorized by: S3-R147-C1-A
|
|
15
|
+
# Track: fragment-registry-compatibility-adapter-helper-implementation-proof-v0
|
|
16
|
+
#
|
|
17
|
+
# Selection rules (R146 proof selection order, pinned exact):
|
|
18
|
+
# if oof present → oof
|
|
19
|
+
# elsif temporal → temporal
|
|
20
|
+
# elsif escape → escape
|
|
21
|
+
# elsif stream → escape (stream maps to escape)
|
|
22
|
+
# elsif epistemic → epistemic
|
|
23
|
+
# else → core
|
|
24
|
+
#
|
|
25
|
+
# API (exact R146 C1 shape; no field-name or result-structure change without
|
|
26
|
+
# a separate explicit delta review):
|
|
27
|
+
# IgniterLang::FragmentRegistryCompatibilityAdapter.project(input_hash) -> result_hash
|
|
28
|
+
|
|
29
|
+
module IgniterLang
|
|
30
|
+
class FragmentRegistryCompatibilityAdapter
|
|
31
|
+
FORMAT_VERSION = "0.1.0".freeze
|
|
32
|
+
KIND_INPUT = "fragment_registry_compatibility_adapter_helper_input".freeze
|
|
33
|
+
KIND_RESULT = "fragment_registry_compatibility_adapter_helper_result".freeze
|
|
34
|
+
|
|
35
|
+
# R146 proof selection rules in priority order.
|
|
36
|
+
# Must match the proof helper_result_shape.json rules_in_order exactly.
|
|
37
|
+
SELECTION_RULES = [
|
|
38
|
+
{ presence: "oof", selected: "oof" },
|
|
39
|
+
{ presence: "temporal", selected: "temporal" },
|
|
40
|
+
{ presence: "escape", selected: "escape" },
|
|
41
|
+
{ presence: "stream", selected: "escape" }, # stream → escape (not a direct fragment)
|
|
42
|
+
{ presence: "epistemic", selected: "epistemic" }
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
DEFAULT_SELECTED = "core".freeze
|
|
46
|
+
|
|
47
|
+
# Project fragment registry compatibility from an internal input hash.
|
|
48
|
+
#
|
|
49
|
+
# Input hash shape (kind: fragment_registry_compatibility_adapter_helper_input):
|
|
50
|
+
# contracts[] — array of { contract_ref, declaration_fragment_presence,
|
|
51
|
+
# current_selected_fragment }
|
|
52
|
+
# guarded_non_fragments[] — array of { name, classification_kind, selected_fragment }
|
|
53
|
+
# oof_projection_policy — { primary_semantics, blocked, loadable, capability }
|
|
54
|
+
# classifier_wiring_authorized — false (must remain false)
|
|
55
|
+
# source_r144 — { matrix_digest, ... }
|
|
56
|
+
#
|
|
57
|
+
# @param input_hash [Hash] Internal helper input conforming to R146 C1 shape.
|
|
58
|
+
# @return [Hash] Internal result conforming to R146 C1 result shape.
|
|
59
|
+
# NEVER touches classifier state, reports, public surfaces, or artifacts.
|
|
60
|
+
def self.project(input_hash)
|
|
61
|
+
contracts = Array(input_hash["contracts"])
|
|
62
|
+
guarded_non_fragments = Array(input_hash["guarded_non_fragments"])
|
|
63
|
+
oof_projection_policy = input_hash.fetch("oof_projection_policy", {})
|
|
64
|
+
|
|
65
|
+
r144_source_digest = input_hash.dig("source_r144", "matrix_digest")
|
|
66
|
+
r144_source_status = input_hash.dig("source_r144", "source_status") || "PASS"
|
|
67
|
+
|
|
68
|
+
rows = contracts.map do |contract|
|
|
69
|
+
ref = contract["contract_ref"]
|
|
70
|
+
presence = Array(contract["declaration_fragment_presence"])
|
|
71
|
+
current = contract["current_selected_fragment"]
|
|
72
|
+
selected = select_fragment(presence)
|
|
73
|
+
parity = selected == current ? "PASS" : "MISMATCH"
|
|
74
|
+
|
|
75
|
+
{
|
|
76
|
+
"contract_ref" => ref,
|
|
77
|
+
"declaration_fragment_presence" => presence,
|
|
78
|
+
"current_selected_fragment" => current,
|
|
79
|
+
"selected_fragment" => selected,
|
|
80
|
+
"parity" => parity
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
mismatches = rows.select { |r| r["parity"] == "MISMATCH" }
|
|
85
|
+
r144_preserved = mismatches.empty?
|
|
86
|
+
|
|
87
|
+
{
|
|
88
|
+
"kind" => KIND_RESULT,
|
|
89
|
+
"format_version" => FORMAT_VERSION,
|
|
90
|
+
"selected_fragment_projection" => {
|
|
91
|
+
"rows" => rows,
|
|
92
|
+
"mismatches" => mismatches,
|
|
93
|
+
"rules_in_order" => rules_in_order_description
|
|
94
|
+
},
|
|
95
|
+
"guarded_non_fragments" => guarded_non_fragments,
|
|
96
|
+
"oof_projection_policy" => oof_projection_policy,
|
|
97
|
+
"r144_parity" => {
|
|
98
|
+
"preserved" => r144_preserved,
|
|
99
|
+
"source_digest" => r144_source_digest,
|
|
100
|
+
"source_status" => r144_source_status
|
|
101
|
+
},
|
|
102
|
+
"held_live_dispatch" => true,
|
|
103
|
+
"classifier_wiring_authorized" => false
|
|
104
|
+
}
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# -------------------------------------------------------------------------
|
|
108
|
+
# Private class-level helpers
|
|
109
|
+
# -------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
# Select the fragment name for the given list of declared fragment presences,
|
|
112
|
+
# applying the R146 proof selection rules in priority order.
|
|
113
|
+
def self.select_fragment(presence_list)
|
|
114
|
+
SELECTION_RULES.each do |rule|
|
|
115
|
+
return rule[:selected] if presence_list.include?(rule[:presence])
|
|
116
|
+
end
|
|
117
|
+
DEFAULT_SELECTED
|
|
118
|
+
end
|
|
119
|
+
private_class_method :select_fragment
|
|
120
|
+
|
|
121
|
+
# Produce the canonical rules_in_order array for inclusion in the result shape.
|
|
122
|
+
def self.rules_in_order_description
|
|
123
|
+
SELECTION_RULES.map do |rule|
|
|
124
|
+
{ "if" => "#{rule[:presence]} present", "selected" => rule[:selected] }
|
|
125
|
+
end + [{ "otherwise" => DEFAULT_SELECTED }]
|
|
126
|
+
end
|
|
127
|
+
private_class_method :rules_in_order_description
|
|
128
|
+
end
|
|
129
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Internal profile assembly boundary.
|
|
4
|
+
#
|
|
5
|
+
# ISOLATION CONTRACT — this file must not:
|
|
6
|
+
# - be required from lib/igniter_lang.rb
|
|
7
|
+
# - integrate with parser/classifier/TypeChecker/SemanticIR/assembler/orchestrator
|
|
8
|
+
# - expose public API or CLI
|
|
9
|
+
# - read paths, manifests, loader reports, CompatibilityReports, or runtime state
|
|
10
|
+
# - write .igapp, .ilk, reports, sidecars, goldens, diagnostics, or CompilerResult fields
|
|
11
|
+
#
|
|
12
|
+
# Authorized by: LANG-R132-A
|
|
13
|
+
# Track: internal-profile-assembly-boundary-implementation-v0
|
|
14
|
+
|
|
15
|
+
require "digest"
|
|
16
|
+
require "json"
|
|
17
|
+
|
|
18
|
+
module IgniterLang
|
|
19
|
+
class InternalProfileAssembly
|
|
20
|
+
KIND = "internal_profile_assembly_result"
|
|
21
|
+
FORMAT_VERSION = "0.1.0"
|
|
22
|
+
|
|
23
|
+
SOURCE_PACKET_KIND = "compiler_profile_oof_registry_source_input"
|
|
24
|
+
IMPLEMENTATION_CANDIDATE = "implementation_candidate"
|
|
25
|
+
FINALIZED_INTERNAL = "finalized_internal"
|
|
26
|
+
|
|
27
|
+
FINALIZED_INTERNAL_MEANING =
|
|
28
|
+
"internal assembly state only; not PROP-036 finalization, not compiler_profile_id, " \
|
|
29
|
+
"and not manifest/profile identity"
|
|
30
|
+
|
|
31
|
+
CLOSED_SURFACE_ASSERTIONS = {
|
|
32
|
+
"root_require" => false,
|
|
33
|
+
"compiler_pipeline_usage" => false,
|
|
34
|
+
"public_api_cli" => false,
|
|
35
|
+
"loader_report" => false,
|
|
36
|
+
"compatibility_report" => false,
|
|
37
|
+
"igapp_mutation" => false,
|
|
38
|
+
"manifest_mutation" => false,
|
|
39
|
+
"prop036_mutation" => false,
|
|
40
|
+
"prop038_mutation" => false,
|
|
41
|
+
"runtime_behavior" => false,
|
|
42
|
+
"production_behavior" => false,
|
|
43
|
+
"spark_surface" => false
|
|
44
|
+
}.freeze
|
|
45
|
+
|
|
46
|
+
DIAG_INVALID_SOURCE_PACKET = "internal_profile_assembly.invalid_source_packet"
|
|
47
|
+
DIAG_INVALID_LIFECYCLE_STATE = "internal_profile_assembly.invalid_lifecycle_state"
|
|
48
|
+
DIAG_PACKET_MAPPING_FAILED = "internal_profile_assembly.packet_mapping_failed"
|
|
49
|
+
DIAG_PACKET_VALIDATION_FAILED = "internal_profile_assembly.packet_validation_failed"
|
|
50
|
+
|
|
51
|
+
def self.assemble(source_packet:, registry_validator: IgniterLang::OOFFragmentRegistry.new)
|
|
52
|
+
new(source_packet: source_packet, registry_validator: registry_validator).assemble
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def initialize(source_packet:, registry_validator:)
|
|
56
|
+
@source_packet = source_packet
|
|
57
|
+
@registry_validator = registry_validator
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def assemble
|
|
61
|
+
diagnostics = []
|
|
62
|
+
packet_hash = nil
|
|
63
|
+
helper_envelopes = nil
|
|
64
|
+
validation = nil
|
|
65
|
+
input_lifecycle_state = lifecycle_state_of(@source_packet)
|
|
66
|
+
|
|
67
|
+
unless source_packet_compatible?(@source_packet)
|
|
68
|
+
diagnostics << diag(DIAG_INVALID_SOURCE_PACKET,
|
|
69
|
+
"source_packet must support lifecycle_state, to_h, to_helper_envelopes, and validate_with")
|
|
70
|
+
return build_result(false, input_lifecycle_state, diagnostics, packet_hash, helper_envelopes, validation)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
if input_lifecycle_state != IMPLEMENTATION_CANDIDATE
|
|
74
|
+
diagnostics << diag(DIAG_INVALID_LIFECYCLE_STATE,
|
|
75
|
+
"source_packet lifecycle_state must be #{IMPLEMENTATION_CANDIDATE.inspect}, " \
|
|
76
|
+
"got #{input_lifecycle_state.inspect}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
begin
|
|
80
|
+
packet_hash = @source_packet.to_h
|
|
81
|
+
helper_envelopes = @source_packet.to_helper_envelopes
|
|
82
|
+
rescue StandardError => e
|
|
83
|
+
diagnostics << diag(DIAG_PACKET_MAPPING_FAILED, "#{e.class}: #{e.message}")
|
|
84
|
+
return build_result(false, input_lifecycle_state, diagnostics, packet_hash, helper_envelopes, validation)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
unless packet_hash.is_a?(Hash) && packet_hash["kind"] == SOURCE_PACKET_KIND
|
|
88
|
+
diagnostics << diag(DIAG_INVALID_SOURCE_PACKET,
|
|
89
|
+
"source_packet.to_h must return kind #{SOURCE_PACKET_KIND.inspect}")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
begin
|
|
93
|
+
validation = @source_packet.validate_with(registry_validator: @registry_validator)
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
diagnostics << diag(DIAG_PACKET_VALIDATION_FAILED, "#{e.class}: #{e.message}")
|
|
96
|
+
return build_result(false, input_lifecycle_state, diagnostics, packet_hash, helper_envelopes, validation)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
diagnostics.concat(internal_diagnostics_from(validation))
|
|
100
|
+
valid = diagnostics.empty? && validation.fetch("valid", false)
|
|
101
|
+
|
|
102
|
+
build_result(valid, input_lifecycle_state, diagnostics, packet_hash, helper_envelopes, validation)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def source_packet_compatible?(packet)
|
|
108
|
+
%i[
|
|
109
|
+
lifecycle_state
|
|
110
|
+
to_h
|
|
111
|
+
to_helper_envelopes
|
|
112
|
+
validate_with
|
|
113
|
+
].all? { |method_name| packet.respond_to?(method_name) }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def lifecycle_state_of(packet)
|
|
117
|
+
return nil unless packet.respond_to?(:lifecycle_state)
|
|
118
|
+
|
|
119
|
+
packet.lifecycle_state
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def build_result(valid, input_lifecycle_state, diagnostics, packet_hash, helper_envelopes, validation)
|
|
123
|
+
{
|
|
124
|
+
"kind" => KIND,
|
|
125
|
+
"format_version" => FORMAT_VERSION,
|
|
126
|
+
"valid" => valid,
|
|
127
|
+
"lifecycle_state" => valid ? FINALIZED_INTERNAL : non_final_lifecycle(input_lifecycle_state),
|
|
128
|
+
"input_lifecycle_state" => input_lifecycle_state,
|
|
129
|
+
"packet_kind" => packet_hash.is_a?(Hash) ? packet_hash["kind"] : nil,
|
|
130
|
+
"packet_digest" => packet_hash ? digest(packet_hash) : nil,
|
|
131
|
+
"helper_envelopes_digest" => helper_envelopes ? digest(helper_envelopes) : nil,
|
|
132
|
+
"profile_validation" => validation.is_a?(Hash) ? validation["profile_validation"] : nil,
|
|
133
|
+
"pack_descriptor_validations" => validation.is_a?(Hash) ? Array(validation["pack_descriptor_validations"]) : [],
|
|
134
|
+
"diagnostics" => diagnostics,
|
|
135
|
+
"finalized_internal_meaning" => FINALIZED_INTERNAL_MEANING,
|
|
136
|
+
"closed_surface_assertions" => deep_copy(CLOSED_SURFACE_ASSERTIONS)
|
|
137
|
+
}
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def internal_diagnostics_from(validation)
|
|
141
|
+
return [diag(DIAG_PACKET_VALIDATION_FAILED, "source_packet.validate_with must return a Hash")] unless validation.is_a?(Hash)
|
|
142
|
+
return [] if validation.fetch("valid", false)
|
|
143
|
+
|
|
144
|
+
diagnostics = []
|
|
145
|
+
profile_diags = Array(validation.dig("profile_validation", "source_diagnostics"))
|
|
146
|
+
diagnostics.concat(profile_diags.map { |source_diag| source_validation_diag("profile_validation", source_diag) })
|
|
147
|
+
|
|
148
|
+
Array(validation["pack_descriptor_validations"]).each_with_index do |pack_validation, index|
|
|
149
|
+
Array(pack_validation["source_diagnostics"]).each do |source_diag|
|
|
150
|
+
diagnostics << source_validation_diag("pack_descriptor_validations[#{index}]", source_diag)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
diagnostics << diag(DIAG_PACKET_VALIDATION_FAILED, "source packet validation failed") if diagnostics.empty?
|
|
155
|
+
diagnostics
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def non_final_lifecycle(input_lifecycle_state)
|
|
159
|
+
return IMPLEMENTATION_CANDIDATE if input_lifecycle_state == IMPLEMENTATION_CANDIDATE
|
|
160
|
+
|
|
161
|
+
"invalid"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def source_validation_diag(path, source_diag)
|
|
165
|
+
{
|
|
166
|
+
"code" => DIAG_PACKET_VALIDATION_FAILED,
|
|
167
|
+
"path" => path,
|
|
168
|
+
"source_code" => source_diag.fetch("code", nil),
|
|
169
|
+
"message" => source_diag.fetch("message", "source packet validation failed")
|
|
170
|
+
}
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def diag(code, message)
|
|
174
|
+
{
|
|
175
|
+
"code" => code,
|
|
176
|
+
"message" => message
|
|
177
|
+
}
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def digest(value)
|
|
181
|
+
Digest::SHA256.hexdigest(JSON.generate(canonicalize(value)))[0, 24]
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def canonicalize(value)
|
|
185
|
+
case value
|
|
186
|
+
when Hash
|
|
187
|
+
value.keys.sort.to_h { |key| [key, canonicalize(value[key])] }
|
|
188
|
+
when Array
|
|
189
|
+
value.map { |inner| canonicalize(inner) }
|
|
190
|
+
else
|
|
191
|
+
value
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def deep_copy(value)
|
|
196
|
+
Marshal.load(Marshal.dump(value))
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|