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,206 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pathname'
4
+
5
+ module Necropsy
6
+ class LoadGraph
7
+ OBSERVATION_LIMIT = 50
8
+
9
+ def self.record_unrooted_units(graph:, reachability:)
10
+ reached = (reachability.runtime_alive + reachability.external_alive).to_set
11
+ root_calls = graph.call_sites.group_by(&:caller_id)
12
+ units = graph.nodes.values.filter_map do |node|
13
+ next unless node.kind == :block_entry && !node.test
14
+ next if reached.include?(node.graph_id)
15
+
16
+ calls = root_calls.fetch(node.graph_id, [])
17
+ next if calls.empty?
18
+
19
+ {
20
+ 'definition_id' => node.graph_id,
21
+ 'file' => node.file,
22
+ 'top_level_calls' => calls.map(&:message).uniq.sort.first(OBSERVATION_LIMIT)
23
+ }
24
+ end.sort_by { |unit| unit.fetch('file') }
25
+ graph.observation['unrooted_load_units'] = {
26
+ 'count' => units.length,
27
+ 'units' => units.first(OBSERVATION_LIMIT),
28
+ 'truncated' => units.length > OBSERVATION_LIMIT
29
+ }
30
+ end
31
+
32
+ def initialize(graph:, project:)
33
+ @graph = graph
34
+ @project = project
35
+ @resolved = []
36
+ @unresolved = []
37
+ @dynamic = []
38
+ end
39
+
40
+ def apply
41
+ load_sites.each { |site| apply_site(site) }
42
+ graph.observation['load_graph'] = observation
43
+ end
44
+
45
+ private
46
+
47
+ attr_reader :graph, :project, :resolved, :unresolved, :dynamic
48
+
49
+ def load_sites
50
+ graph.call_sites
51
+ .select { |site| site.metadata['load_reference'].is_a?(Hash) }
52
+ .sort_by { |site| [site.file, site.line, site.call_site_id] }
53
+ end
54
+
55
+ def apply_site(site)
56
+ reference = site.metadata.fetch('load_reference')
57
+ return apply_dynamic_site(site, reference) unless reference['literal']
58
+
59
+ targets, strategy = resolve_literal(site, reference)
60
+ if targets.empty?
61
+ unresolved << site_record(site, reference).merge('reason' => 'external_or_missing')
62
+ return
63
+ end
64
+
65
+ targets.each do |target|
66
+ graph.add_edge(site.caller_id, target.graph_id, load_evidence(site, target, reference, strategy))
67
+ end
68
+ resolved << site_record(site, reference).merge(
69
+ 'strategy' => strategy,
70
+ 'target_files' => targets.map(&:file).sort
71
+ )
72
+ end
73
+
74
+ def apply_dynamic_site(site, reference)
75
+ targets = site.test ? file_roots : file_roots.reject(&:test)
76
+ targets.each do |target|
77
+ graph.add_edge(site.caller_id, target.graph_id, load_evidence(site, target, reference, 'dynamic_conservative'))
78
+ end
79
+ graph.add_blocker(dynamic_load_blocker(site, reference))
80
+ dynamic << site_record(site, reference).merge('candidate_load_units' => targets.length)
81
+ end
82
+
83
+ def resolve_literal(site, reference)
84
+ path = reference.fetch('path')
85
+ if reference.fetch('kind') == 'require_relative'
86
+ candidates = relative_candidates(File.dirname(site.file), path)
87
+ return [roots_for(candidates), 'require_relative']
88
+ end
89
+
90
+ candidates = require_candidates(path)
91
+ [roots_for(candidates), reference.fetch('kind') == 'autoload' ? 'autoload_lib_path' : 'require_lib_path']
92
+ end
93
+
94
+ def relative_candidates(base, path)
95
+ normalized = safe_relative_path(File.join(base, path))
96
+ source_candidates(normalized)
97
+ end
98
+
99
+ def require_candidates(path)
100
+ normalized = safe_relative_path(path)
101
+ return [] unless normalized
102
+
103
+ explicit = normalized.start_with?('lib/') ? source_candidates(normalized) : []
104
+ (explicit + source_candidates(File.join('lib', normalized))).uniq
105
+ end
106
+
107
+ def source_candidates(path)
108
+ return [] unless path
109
+
110
+ File.extname(path).empty? ? [path, "#{path}.rb"] : [path]
111
+ end
112
+
113
+ def safe_relative_path(path)
114
+ return if Pathname.new(path).absolute?
115
+
116
+ cleaned = Pathname.new(path).cleanpath.to_s
117
+ return if cleaned == '..' || cleaned.start_with?('../')
118
+
119
+ cleaned.delete_prefix('./')
120
+ rescue ArgumentError
121
+ nil
122
+ end
123
+
124
+ def roots_for(paths)
125
+ paths.filter_map { |path| file_roots_by_path[path] }.uniq(&:graph_id).sort_by(&:graph_id)
126
+ end
127
+
128
+ def file_roots
129
+ @file_roots ||= graph.nodes.values.select { |node| node.kind == :block_entry }.sort_by(&:graph_id)
130
+ end
131
+
132
+ def file_roots_by_path
133
+ @file_roots_by_path ||= file_roots.to_h { |node| [node.file, node] }
134
+ end
135
+
136
+ def load_evidence(site, target, reference, strategy)
137
+ exact = strategy == 'require_relative'
138
+ Evidence.new(
139
+ analyzer: :load_graph,
140
+ kind: :load_edge,
141
+ weight: exact ? 1.0 : 0.7,
142
+ details: "#{reference.fetch('kind')} loads #{target.file}",
143
+ metadata: {
144
+ 'call_site_id' => site.call_site_id,
145
+ 'load_kind' => reference.fetch('kind'),
146
+ 'strategy' => strategy,
147
+ 'target_definition_id' => target.graph_id
148
+ },
149
+ producer: :load_graph,
150
+ producer_version: Necropsy::VERSION,
151
+ grade: exact ? :exact : :conservative,
152
+ relation: :load_edge,
153
+ source: { 'call_site_id' => site.call_site_id, 'file' => site.file, 'line' => site.line },
154
+ assumptions: exact ? [] : [load_assumption(strategy)],
155
+ scope: { 'caller_definition_id' => site.caller_id, 'target_definition_id' => target.graph_id }
156
+ )
157
+ end
158
+
159
+ def load_assumption(strategy)
160
+ return 'dynamic_load_may_select_any_scanned_load_unit' if strategy == 'dynamic_conservative'
161
+
162
+ 'lib_is_on_the_ruby_load_path'
163
+ end
164
+
165
+ def dynamic_load_blocker(site, reference)
166
+ Blocker.new(
167
+ kind: :dynamic_load,
168
+ scope_kind: :definition,
169
+ scope_value: site.caller_id,
170
+ source: :load_graph,
171
+ reason: "#{reference.fetch('kind')} path is not a static string or symbol",
172
+ suggested_action: :make_load_path_literal,
173
+ metadata: {
174
+ 'caller_domain' => site.test ? 'test' : 'runtime',
175
+ 'caller_id' => site.caller_id,
176
+ 'call_site_id' => site.call_site_id,
177
+ 'file' => site.file,
178
+ 'line' => site.line,
179
+ 'load_kind' => reference.fetch('kind')
180
+ }
181
+ )
182
+ end
183
+
184
+ def site_record(site, reference)
185
+ {
186
+ 'call_site_id' => site.call_site_id,
187
+ 'caller_definition_id' => site.caller_id,
188
+ 'file' => site.file,
189
+ 'line' => site.line,
190
+ 'kind' => reference.fetch('kind'),
191
+ 'path' => reference['path']
192
+ }.compact
193
+ end
194
+
195
+ def observation
196
+ {
197
+ 'resolved_count' => resolved.length,
198
+ 'unresolved_literal_count' => unresolved.length,
199
+ 'dynamic_count' => dynamic.length,
200
+ 'resolved' => resolved.first(OBSERVATION_LIMIT),
201
+ 'unresolved_literals' => unresolved.first(OBSERVATION_LIMIT),
202
+ 'dynamic' => dynamic.first(OBSERVATION_LIMIT)
203
+ }
204
+ end
205
+ end
206
+ end