necropsy 0.2.0 → 0.3.0

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.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/MEASUREMENTS.md +27 -0
  4. data/README.md +182 -17
  5. data/bench/README.md +92 -0
  6. data/bench/audit.rb +113 -0
  7. data/bench/audits/0.2.1/audit.json +501 -0
  8. data/bench/audits/0.2.1/audit.md +55 -0
  9. data/bench/audits/0.2.1/baseline_performance.yml +16 -0
  10. data/bench/audits/0.2.1/config.yml +42 -0
  11. data/bench/audits/0.2.1/review.yml +56 -0
  12. data/bench/audits/0.2.1/review_queue.yml +3651 -0
  13. data/bench/corpora/v1/README.md +30 -0
  14. data/bench/corpora/v1/labels.yml +37 -0
  15. data/bench/corpora/v1/manifest.yml +63 -0
  16. data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
  17. data/bench/corpora/v1/self.necropsy.yml +8 -0
  18. data/bench/corpora/v1/tools/debride.yml +42 -0
  19. data/bench/corpora/v1/tools/spoom.yml +12 -0
  20. data/bench/corpora/v1/tools/type_aware.yml +11 -0
  21. data/bench/golden/v1/candidate_union.json +21918 -0
  22. data/bench/golden/v1/metadata.json +12 -0
  23. data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
  24. data/bench/golden/v1/reports/plain_ruby.json +135 -0
  25. data/bench/golden/v1/reports/rails.json +176 -0
  26. data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
  27. data/bench/golden/v1/reports/self.json +3449 -0
  28. data/bench/review_queue.rb +35 -0
  29. data/bench/run.rb +31 -0
  30. data/bench/schema/candidate-union-v1.schema.json +70 -0
  31. data/docs/impv_implementation_matrix.md +179 -0
  32. data/docs/migrations/0.2.1.md +57 -0
  33. data/docs/migrations/0.3.0.md +207 -0
  34. data/docs/migrations/0.4.0.md +13 -0
  35. data/docs/necropsy_performance_adr.md +43 -0
  36. data/docs/necropsy_scope_decisions.md +58 -0
  37. data/docs/necropsy_type_facts_adr.md +22 -0
  38. data/gemfiles/prism_min.gemfile +9 -0
  39. data/gemfiles/prism_min.gemfile.lock +50 -0
  40. data/lib/necropsy/analyzer.rb +121 -2
  41. data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +69 -17
  42. data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +99 -9
  43. data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +69 -299
  44. data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
  45. data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
  46. data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
  47. data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
  48. data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
  49. data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
  50. data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
  51. data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +133 -23
  52. data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
  53. data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
  54. data/lib/necropsy/analyzers/static/cha.rb +33 -73
  55. data/lib/necropsy/analyzers/static/name_resolution.rb +133 -12
  56. data/lib/necropsy/analyzers/static/rta.rb +237 -29
  57. data/lib/necropsy/ast_scanner/call_recording.rb +149 -30
  58. data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
  59. data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
  60. data/lib/necropsy/ast_scanner/dsl_macros.rb +431 -41
  61. data/lib/necropsy/ast_scanner/method_definitions.rb +232 -50
  62. data/lib/necropsy/ast_scanner/references.rb +43 -9
  63. data/lib/necropsy/ast_scanner/ruby_semantics.rb +101 -15
  64. data/lib/necropsy/ast_scanner/traversal.rb +234 -71
  65. data/lib/necropsy/ast_scanner/value_definitions.rb +23 -13
  66. data/lib/necropsy/ast_scanner.rb +67 -6
  67. data/lib/necropsy/bench/candidate_union.rb +555 -0
  68. data/lib/necropsy/bench/claim_gate.rb +112 -0
  69. data/lib/necropsy/bench/evaluator.rb +329 -16
  70. data/lib/necropsy/bench/finding_facts.rb +152 -0
  71. data/lib/necropsy/bench/precision_gate.rb +144 -0
  72. data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
  73. data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
  74. data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
  75. data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
  76. data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
  77. data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
  78. data/lib/necropsy/bench/release_audit.rb +360 -0
  79. data/lib/necropsy/bench/report_normalizer.rb +140 -0
  80. data/lib/necropsy/bench/review_queue.rb +154 -0
  81. data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
  82. data/lib/necropsy/bench/seed_runner.rb +408 -0
  83. data/lib/necropsy/bounded_canonicalizer.rb +218 -0
  84. data/lib/necropsy/cache/scan_cache.rb +85 -17
  85. data/lib/necropsy/call_site_identity.rb +54 -0
  86. data/lib/necropsy/cli.rb +220 -33
  87. data/lib/necropsy/clock.rb +40 -0
  88. data/lib/necropsy/confidence/scorer.rb +103 -58
  89. data/lib/necropsy/configuration.rb +224 -21
  90. data/lib/necropsy/convention_rules.rb +138 -0
  91. data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
  92. data/lib/necropsy/definition_identity.rb +37 -0
  93. data/lib/necropsy/diagnostics.rb +176 -36
  94. data/lib/necropsy/embedded_ruby.rb +55 -0
  95. data/lib/necropsy/entry_points/plain.rb +111 -10
  96. data/lib/necropsy/entry_points/rails.rb +322 -41
  97. data/lib/necropsy/entry_points/test.rb +6 -1
  98. data/lib/necropsy/flow_interpreter.rb +460 -0
  99. data/lib/necropsy/graph/blocker_matching.rb +338 -0
  100. data/lib/necropsy/graph/call_graph.rb +1099 -109
  101. data/lib/necropsy/graph/definition_index.rb +149 -0
  102. data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
  103. data/lib/necropsy/graph/evidence_store.rb +213 -0
  104. data/lib/necropsy/graph/resolution_store.rb +497 -0
  105. data/lib/necropsy/graph_self_check.rb +79 -0
  106. data/lib/necropsy/guardrail/baseline.rb +350 -13
  107. data/lib/necropsy/guardrail/quarantine.rb +94 -9
  108. data/lib/necropsy/load_graph.rb +206 -0
  109. data/lib/necropsy/models.rb +878 -13
  110. data/lib/necropsy/performance_profiler.rb +108 -0
  111. data/lib/necropsy/project.rb +327 -25
  112. data/lib/necropsy/reachability/engine.rb +54 -13
  113. data/lib/necropsy/reference_barrier.rb +458 -0
  114. data/lib/necropsy/report.rb +113 -4
  115. data/lib/necropsy/reporter.rb +431 -15
  116. data/lib/necropsy/runner.rb +233 -18
  117. data/lib/necropsy/runtime_feedback.rb +136 -0
  118. data/lib/necropsy/semantics_matrix.rb +153 -0
  119. data/lib/necropsy/type_facts.rb +53 -0
  120. data/lib/necropsy/version.rb +1 -1
  121. data/lib/necropsy/why_not_explanation.rb +436 -0
  122. data/lib/necropsy/why_not_renderer.rb +197 -0
  123. data/lib/necropsy/world_policy.rb +90 -0
  124. data/lib/necropsy.rb +35 -2
  125. data/schema/necropsy-report-v2.schema.json +366 -0
  126. metadata +85 -1
@@ -0,0 +1,218 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Necropsy
6
+ class BoundedCanonicalizer
7
+ MAX_DEPTH = 256
8
+ MAX_ITEMS = 1_000_000
9
+ MAX_STRING_BYTES = 64 * 1024 * 1024
10
+ MAX_TOTAL_BYTES = 256 * 1024 * 1024
11
+
12
+ class Error < StandardError; end
13
+ class CycleError < Error; end
14
+ class LimitError < Error; end
15
+ class UnsupportedTypeError < Error; end
16
+
17
+ def self.dump(value, **limits)
18
+ new(**limits).dump(value)
19
+ end
20
+
21
+ def initialize(max_depth: MAX_DEPTH, max_items: MAX_ITEMS, max_string_bytes: MAX_STRING_BYTES,
22
+ max_total_bytes: MAX_TOTAL_BYTES)
23
+ @max_depth = positive_limit(max_depth, :max_depth)
24
+ @max_items = positive_limit(max_items, :max_items)
25
+ @max_string_bytes = positive_limit(max_string_bytes, :max_string_bytes)
26
+ @max_total_bytes = positive_limit(max_total_bytes, :max_total_bytes)
27
+ end
28
+
29
+ # Kept for callers that need the canonical Ruby representation rather than bytes.
30
+ def call(value)
31
+ JSON.parse(dump(value), max_nesting: false)
32
+ end
33
+
34
+ def dump(value)
35
+ reset_state
36
+ output = binary_string
37
+ process([[:value, value, 0, output]])
38
+ output
39
+ rescue JSON::GeneratorError => e
40
+ raise Error, "Could not encode canonical payload: #{e.message}"
41
+ end
42
+
43
+ private
44
+
45
+ attr_reader :max_depth, :max_items, :max_string_bytes, :max_total_bytes, :active_containers
46
+
47
+ def reset_state
48
+ @active_containers = {}
49
+ @items = 0
50
+ @written_bytes = 0
51
+ end
52
+
53
+ def process(stack)
54
+ until stack.empty?
55
+ action, *arguments = stack.pop
56
+ send("process_#{action}", stack, *arguments)
57
+ end
58
+ rescue SystemStackError => e
59
+ raise CycleError, "Could not canonicalize payload: #{e.message}"
60
+ end
61
+
62
+ def process_value(stack, value, depth, output)
63
+ count_item!
64
+ check_depth!(depth)
65
+
66
+ case value
67
+ when Hash then start_hash(stack, value, depth, output)
68
+ when Array then start_array(stack, value, depth, output)
69
+ when Symbol then append(output, %(["symbol",#{encoded_string(canonical_string(value.to_s))}]))
70
+ when String then append(output, %(["string",#{encoded_string(canonical_string(value))}]))
71
+ when Integer then append(output, %(["integer",#{json_string(value.to_s)}]))
72
+ when Float then append(output, %(["float",#{json_string(canonical_float(value))}]))
73
+ when true, false then append(output, %(["boolean",#{value}]))
74
+ when nil then append(output, '["nil"]')
75
+ else start_object(stack, value, depth, output)
76
+ end
77
+ end
78
+
79
+ def start_array(stack, value, depth, output)
80
+ enter_container!(value)
81
+ append(output, '["array",[')
82
+ stack << [:array_next, value, 0, depth, output]
83
+ end
84
+
85
+ def process_array_next(stack, value, index, depth, output)
86
+ if index >= value.length
87
+ append(output, ']]')
88
+ leave_container(value)
89
+ return
90
+ end
91
+
92
+ append(output, ',') if index.positive?
93
+ stack << [:array_next, value, index + 1, depth, output]
94
+ stack << [:value, value.fetch(index), depth + 1, output]
95
+ end
96
+
97
+ def start_hash(stack, value, depth, output)
98
+ enter_container!(value)
99
+ stack << [:hash_next, value, value.each_pair, [], depth, output]
100
+ end
101
+
102
+ def process_hash_next(stack, value, iterator, pairs, depth, output)
103
+ key, item = iterator.next
104
+ pair = binary_string
105
+ append(pair, '[')
106
+ stack << [:hash_pair_complete, value, iterator, pairs, depth, output, pair]
107
+ stack << [:raw, pair, ']']
108
+ stack << [:value, item, depth + 1, pair]
109
+ stack << [:raw, pair, ',']
110
+ stack << [:value, key, depth + 1, pair]
111
+ rescue StopIteration
112
+ append(output, '["hash",[')
113
+ pairs.sort.each_with_index do |pair, index|
114
+ append(output, ',') if index.positive?
115
+ append(output, pair)
116
+ end
117
+ append(output, ']]')
118
+ leave_container(value)
119
+ end
120
+
121
+ def process_hash_pair_complete(stack, value, iterator, pairs, depth, output, pair)
122
+ pairs << pair
123
+ stack << [:hash_next, value, iterator, pairs, depth, output]
124
+ end
125
+
126
+ def start_object(stack, value, depth, output)
127
+ raise UnsupportedTypeError, "Unsupported canonical payload type: #{value.class}" unless value.respond_to?(:to_h)
128
+
129
+ enter_container!(value)
130
+ converted = value.to_h
131
+ append(output, %(["object",#{json_string(value.class.name.to_s)},))
132
+ stack << [:object_complete, value, output]
133
+ stack << [:value, converted, depth + 1, output]
134
+ rescue Error
135
+ raise
136
+ rescue StandardError => e
137
+ leave_container(value)
138
+ raise UnsupportedTypeError, "Could not convert #{value.class} to a canonical payload: #{e.class}"
139
+ end
140
+
141
+ def process_object_complete(_stack, value, output)
142
+ append(output, ']')
143
+ leave_container(value)
144
+ end
145
+
146
+ def process_raw(_stack, output, bytes)
147
+ append(output, bytes)
148
+ end
149
+
150
+ def encoded_string(value)
151
+ %([#{json_string(value.fetch(0))},#{json_string(value.fetch(1))}])
152
+ end
153
+
154
+ def canonical_string(value)
155
+ check_scalar_size!(value)
156
+ normalized = value.encode(Encoding::UTF_8)
157
+ ['UTF-8', normalized.b.unpack1('H*')]
158
+ rescue EncodingError
159
+ [value.encoding.name, value.b.unpack1('H*')]
160
+ end
161
+
162
+ def canonical_float(value)
163
+ raise UnsupportedTypeError, 'Canonical payload contains a non-finite float' unless value.finite?
164
+
165
+ value.to_s
166
+ end
167
+
168
+ def json_string(value)
169
+ JSON.generate(value)
170
+ end
171
+
172
+ def append(output, bytes)
173
+ @written_bytes += bytes.bytesize
174
+ raise LimitError, "Canonical payload exceeds maximum size #{max_total_bytes}" if @written_bytes > max_total_bytes
175
+
176
+ output << bytes.b
177
+ end
178
+
179
+ def enter_container!(value)
180
+ object_id = value.object_id
181
+ raise CycleError, "Canonical payload contains a cycle at #{value.class}" if active_containers.key?(object_id)
182
+
183
+ active_containers[object_id] = true
184
+ end
185
+
186
+ def leave_container(value)
187
+ active_containers.delete(value.object_id)
188
+ end
189
+
190
+ def check_scalar_size!(value)
191
+ return if value.bytesize <= max_string_bytes
192
+
193
+ raise LimitError, "Canonical string exceeds maximum size #{max_string_bytes}"
194
+ end
195
+
196
+ def check_depth!(depth)
197
+ return if depth <= max_depth
198
+
199
+ raise LimitError, "Canonical payload exceeds maximum depth #{max_depth}"
200
+ end
201
+
202
+ def count_item!
203
+ @items += 1
204
+ raise LimitError, "Canonical payload exceeds maximum item count #{max_items}" if @items > max_items
205
+ end
206
+
207
+ def positive_limit(value, name)
208
+ value = Integer(value)
209
+ raise ArgumentError, "#{name} must be positive" unless value.positive?
210
+
211
+ value
212
+ end
213
+
214
+ def binary_string
215
+ String.new(encoding: Encoding::BINARY)
216
+ end
217
+ end
218
+ end
@@ -2,11 +2,12 @@
2
2
 
3
3
  require 'fileutils'
4
4
  require 'json'
5
+ require 'digest'
5
6
 
6
7
  module Necropsy
7
8
  module Cache
8
9
  class ScanCache
9
- VERSION = 4
10
+ VERSION = 13
10
11
 
11
12
  def initialize(project:)
12
13
  @project = project
@@ -15,22 +16,28 @@ module Necropsy
15
16
  def fetch(files)
16
17
  return yield unless project.config.cache_enabled?
17
18
 
18
- metadata = cache_metadata(files)
19
+ metadata = safe_cache_metadata(files)
20
+ return yield unless metadata
21
+
19
22
  cached = read(metadata)
20
23
  return cached if cached
21
24
 
22
25
  result = yield
23
26
  write(metadata, result)
24
27
  result
25
- rescue SystemCallError, JSON::ParserError => e
26
- warn_cache("Cache unavailable: #{e.message}")
27
- yield
28
28
  end
29
29
 
30
30
  private
31
31
 
32
32
  attr_reader :project
33
33
 
34
+ def safe_cache_metadata(files)
35
+ cache_metadata(files)
36
+ rescue SystemCallError, JSON::ParserError => e
37
+ warn_cache("Cache unavailable: #{e.message}")
38
+ nil
39
+ end
40
+
34
41
  def read(metadata)
35
42
  payload = load_payload
36
43
  return nil unless payload
@@ -51,13 +58,18 @@ module Necropsy
51
58
 
52
59
  def write(metadata, result)
53
60
  FileUtils.mkdir_p(File.dirname(path))
54
- File.write(path, JSON.generate({
55
- 'version' => VERSION,
56
- 'metadata' => metadata,
57
- 'scan_result' => serialize_scan_result(result)
58
- }))
61
+ payload = JSON.generate(
62
+ 'version' => VERSION,
63
+ 'metadata' => metadata,
64
+ 'scan_result' => serialize_scan_result(result)
65
+ )
66
+ temporary = "#{path}.tmp-#{Process.pid}-#{Thread.current.object_id}"
67
+ File.write(temporary, payload)
68
+ File.rename(temporary, path)
59
69
  rescue StandardError => e
60
70
  warn_cache("Could not write cache: #{e.message}")
71
+ ensure
72
+ FileUtils.rm_f(temporary) if temporary && File.exist?(temporary)
61
73
  nil
62
74
  end
63
75
 
@@ -70,7 +82,8 @@ module Necropsy
70
82
  stat = File.stat(file)
71
83
  metadata[project.relative_path(file)] = {
72
84
  'size' => stat.size,
73
- 'mtime' => "#{stat.mtime.to_i}.#{stat.mtime.nsec}"
85
+ 'mtime' => "#{stat.mtime.to_i}.#{stat.mtime.nsec}",
86
+ 'content_sha256' => Digest::SHA256.file(file).hexdigest
74
87
  }
75
88
  end
76
89
  end
@@ -78,7 +91,16 @@ module Necropsy
78
91
  def cache_metadata(files)
79
92
  {
80
93
  'files' => file_metadata(files),
81
- 'configuration' => project.config.scan_cache_key
94
+ 'inventory' => project.scan_inventory_key,
95
+ 'configuration' => project.config.scan_cache_key,
96
+ 'environment' => {
97
+ 'necropsy_version' => Necropsy::VERSION,
98
+ 'ruby_engine' => RUBY_ENGINE,
99
+ 'ruby_version' => RUBY_VERSION,
100
+ 'prism_version' => Prism::VERSION,
101
+ 'definition_identity_version' => DefinitionIdentity::VERSION,
102
+ 'call_site_identity_version' => CallSiteIdentity::VERSION
103
+ }
82
104
  }
83
105
  end
84
106
 
@@ -89,7 +111,13 @@ module Necropsy
89
111
  'instantiated_classes' => result.instantiated_classes.to_a.sort,
90
112
  'uncertainties' => result.uncertainties.transform_values { |messages| Array(messages).map(&:to_s) },
91
113
  'class_infos' => result.class_infos.map(&:to_h),
92
- 'entrypoint_hints' => result.entrypoint_hints.map(&:to_h)
114
+ 'entrypoint_hints' => result.entrypoint_hints.map(&:to_h),
115
+ 'file_statuses' => result.file_statuses.transform_values(&:to_s),
116
+ 'source_errors' => result.source_errors.map(&:to_h),
117
+ 'source_domains' => result.source_domains.transform_values(&:to_s),
118
+ 'scope_diagnostics' => result.scope_diagnostics,
119
+ 'method_signatures' => result.method_signatures,
120
+ 'semantic_blockers' => result.semantic_blockers.map(&:to_h)
93
121
  }
94
122
  end
95
123
 
@@ -100,13 +128,45 @@ module Necropsy
100
128
  instantiated_classes: Set.new(Array(data['instantiated_classes'])),
101
129
  uncertainties: deserialize_uncertainties(data['uncertainties']),
102
130
  class_infos: Array(data['class_infos']).map { |info| deserialize_class_info(info) },
103
- entrypoint_hints: Array(data['entrypoint_hints']).map { |entry| deserialize_entry_point(entry) }
131
+ entrypoint_hints: Array(data['entrypoint_hints']).map { |entry| deserialize_entry_point(entry) },
132
+ file_statuses: (data['file_statuses'] || {}).transform_values(&:to_sym),
133
+ source_errors: Array(data['source_errors']).map { |error| deserialize_source_error(error) },
134
+ source_domains: (data['source_domains'] || {}).transform_values(&:to_sym),
135
+ scope_diagnostics: data['scope_diagnostics'] || {},
136
+ method_signatures: data['method_signatures'] || {},
137
+ semantic_blockers: Array(data['semantic_blockers']).map { |blocker| deserialize_blocker(blocker) }
138
+ )
139
+ end
140
+
141
+ def deserialize_blocker(data)
142
+ Blocker.new(
143
+ kind: data.fetch('kind').to_sym,
144
+ scope_kind: data.fetch('scope_kind').to_sym,
145
+ scope_value: data['scope_value'],
146
+ source: data['source'],
147
+ reason: data.fetch('reason'),
148
+ suggested_action: data.fetch('suggested_action', 'review').to_sym,
149
+ metadata: data['metadata'] || {}
150
+ )
151
+ end
152
+
153
+ def deserialize_source_error(data)
154
+ SourceError.new(
155
+ file: data['file'],
156
+ line: data['line']&.to_i || 1,
157
+ message: data['message'],
158
+ type: data['type'].to_sym
104
159
  )
105
160
  end
106
161
 
107
162
  def deserialize_node(data)
163
+ id = data.fetch('id')
108
164
  Node.new(
109
- id: data['id'],
165
+ id: id,
166
+ symbol_id: data['symbol_id'] || id,
167
+ definition_id: data['definition_id'] || id,
168
+ body_digest: data['body_digest'],
169
+ ordinal: data['ordinal'].to_i,
110
170
  kind: data['kind'].to_sym,
111
171
  file: data['file'],
112
172
  line: data['line'].to_i,
@@ -121,7 +181,8 @@ module Necropsy
121
181
 
122
182
  def deserialize_call_site(data)
123
183
  CallSite.new(
124
- caller_id: data['caller_id'],
184
+ call_site_id: data.fetch('call_site_id'),
185
+ caller_id: data['caller_definition_id'] || data.fetch('caller_id'),
125
186
  message: data['message'],
126
187
  receiver_kind: data['receiver_kind'].to_sym,
127
188
  receiver_name: data['receiver_name'],
@@ -144,12 +205,19 @@ module Necropsy
144
205
  includes: Array(data['includes']),
145
206
  prepends: Array(data['prepends']),
146
207
  extends: Array(data['extends']),
208
+ singleton_includes: Array(data['singleton_includes']),
209
+ singleton_prepends: Array(data['singleton_prepends']),
147
210
  dynamic: data['dynamic']
148
211
  )
149
212
  end
150
213
 
151
214
  def deserialize_entry_point(data)
152
- EntryPoint.new(node_id: data['node_id'], reason: data['reason'].to_sym)
215
+ Root.new(
216
+ definition_id: data['definition_id'] || data['node_id'],
217
+ domain: data['domain'],
218
+ reason: data['reason'].to_sym,
219
+ evidence: data['evidence']
220
+ )
153
221
  end
154
222
 
155
223
  def deserialize_uncertainties(data)
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+ require_relative 'bounded_canonicalizer'
5
+
6
+ module Necropsy
7
+ module CallSiteIdentity
8
+ VERSION = 1
9
+ PREFIX = "call:v#{VERSION}".freeze
10
+ SOURCE_ROLES = %i[
11
+ call initialize symbol_reference symbol_to_proc super alias_target delegate_target delegate_message
12
+ ].freeze
13
+ DERIVATIONS = %i[module_function rta_implicit].freeze
14
+
15
+ module_function
16
+
17
+ def source_id(caller_definition_id:, relative_path:, role:, message:, structural_digest:, ordinal:)
18
+ role = role.to_sym
19
+ raise ArgumentError, "invalid call site role: #{role.inspect}" unless SOURCE_ROLES.include?(role)
20
+
21
+ ordinal = Integer(ordinal)
22
+ raise ArgumentError, 'call site ordinal must be positive' unless ordinal.positive?
23
+
24
+ identifier(
25
+ 'source', caller_definition_id.to_s, relative_path.to_s, role.to_s, message.to_s,
26
+ structural_digest.to_s, ordinal
27
+ )
28
+ end
29
+
30
+ def derived_id(parent_call_site_id:, derivation:, caller_definition_id:, message:, discriminator: nil)
31
+ derivation = derivation.to_sym
32
+ raise ArgumentError, "invalid call site derivation: #{derivation.inspect}" unless DERIVATIONS.include?(derivation)
33
+
34
+ payload = [
35
+ 'derived', derivation.to_s, parent_call_site_id.to_s, caller_definition_id.to_s, message.to_s
36
+ ]
37
+ payload << discriminator.to_s unless discriminator.nil?
38
+ identifier(*payload)
39
+ end
40
+
41
+ def legacy_id(caller_definition_id:, message:, receiver_kind:, receiver_name:, file:, line:, test:, dynamic:,
42
+ metadata:)
43
+ identifier(
44
+ 'legacy', caller_definition_id.to_s, message.to_s, receiver_kind.to_s, receiver_name&.to_s,
45
+ file.to_s, line, test, dynamic, metadata
46
+ )
47
+ end
48
+
49
+ def identifier(*payload)
50
+ "#{PREFIX}:#{Digest::SHA256.hexdigest(BoundedCanonicalizer.dump(payload))}"
51
+ end
52
+ private_class_method :identifier
53
+ end
54
+ end