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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/MEASUREMENTS.md +27 -0
- data/README.md +182 -17
- data/bench/README.md +92 -0
- data/bench/audit.rb +113 -0
- data/bench/audits/0.2.1/audit.json +501 -0
- data/bench/audits/0.2.1/audit.md +55 -0
- data/bench/audits/0.2.1/baseline_performance.yml +16 -0
- data/bench/audits/0.2.1/config.yml +42 -0
- data/bench/audits/0.2.1/review.yml +56 -0
- data/bench/audits/0.2.1/review_queue.yml +3651 -0
- data/bench/corpora/v1/README.md +30 -0
- data/bench/corpora/v1/labels.yml +37 -0
- data/bench/corpora/v1/manifest.yml +63 -0
- data/bench/corpora/v1/rubocop.necropsy.yml +7 -0
- data/bench/corpora/v1/self.necropsy.yml +8 -0
- data/bench/corpora/v1/tools/debride.yml +42 -0
- data/bench/corpora/v1/tools/spoom.yml +12 -0
- data/bench/corpora/v1/tools/type_aware.yml +11 -0
- data/bench/golden/v1/candidate_union.json +21918 -0
- data/bench/golden/v1/metadata.json +12 -0
- data/bench/golden/v1/reports/dynamic_evidence.json +146 -0
- data/bench/golden/v1/reports/plain_ruby.json +135 -0
- data/bench/golden/v1/reports/rails.json +176 -0
- data/bench/golden/v1/reports/rubocop_1_75_0.json +26848 -0
- data/bench/golden/v1/reports/self.json +3449 -0
- data/bench/review_queue.rb +35 -0
- data/bench/run.rb +31 -0
- data/bench/schema/candidate-union-v1.schema.json +70 -0
- data/docs/impv_implementation_matrix.md +179 -0
- data/docs/migrations/0.2.1.md +57 -0
- data/docs/migrations/0.3.0.md +207 -0
- data/docs/migrations/0.4.0.md +13 -0
- data/docs/necropsy_performance_adr.md +43 -0
- data/docs/necropsy_scope_decisions.md +58 -0
- data/docs/necropsy_type_facts_adr.md +22 -0
- data/gemfiles/prism_min.gemfile +9 -0
- data/gemfiles/prism_min.gemfile.lock +50 -0
- data/lib/necropsy/analyzer.rb +121 -2
- data/lib/necropsy/analyzers/dynamic/coverage_collector.rb +69 -17
- data/lib/necropsy/analyzers/dynamic/coverage_importer.rb +99 -9
- data/lib/necropsy/analyzers/dynamic/coverband_importer.rb +69 -299
- data/lib/necropsy/analyzers/dynamic/coverband_payload_set.rb +149 -0
- data/lib/necropsy/analyzers/dynamic/observation_policy.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/redis_input_limits.rb +121 -0
- data/lib/necropsy/analyzers/dynamic/redis_nonblocking_io.rb +94 -0
- data/lib/necropsy/analyzers/dynamic/redis_payload_loader.rb +165 -0
- data/lib/necropsy/analyzers/dynamic/redis_transport.rb +217 -0
- data/lib/necropsy/analyzers/dynamic/runtime_reference.rb +96 -0
- data/lib/necropsy/analyzers/dynamic/trace_point_collector.rb +133 -23
- data/lib/necropsy/analyzers/dynamic/trace_point_importer.rb +3 -1
- data/lib/necropsy/analyzers/legacy_result_adapter.rb +226 -0
- data/lib/necropsy/analyzers/static/cha.rb +33 -73
- data/lib/necropsy/analyzers/static/name_resolution.rb +133 -12
- data/lib/necropsy/analyzers/static/rta.rb +237 -29
- data/lib/necropsy/ast_scanner/call_recording.rb +149 -30
- data/lib/necropsy/ast_scanner/call_site_creation.rb +54 -0
- data/lib/necropsy/ast_scanner/definition_creation.rb +43 -0
- data/lib/necropsy/ast_scanner/dsl_macros.rb +431 -41
- data/lib/necropsy/ast_scanner/method_definitions.rb +232 -50
- data/lib/necropsy/ast_scanner/references.rb +43 -9
- data/lib/necropsy/ast_scanner/ruby_semantics.rb +101 -15
- data/lib/necropsy/ast_scanner/traversal.rb +234 -71
- data/lib/necropsy/ast_scanner/value_definitions.rb +23 -13
- data/lib/necropsy/ast_scanner.rb +67 -6
- data/lib/necropsy/bench/candidate_union.rb +555 -0
- data/lib/necropsy/bench/claim_gate.rb +112 -0
- data/lib/necropsy/bench/evaluator.rb +329 -16
- data/lib/necropsy/bench/finding_facts.rb +152 -0
- data/lib/necropsy/bench/precision_gate.rb +144 -0
- data/lib/necropsy/bench/release_audit/adversarial_runner.rb +56 -0
- data/lib/necropsy/bench/release_audit/artifact_writer.rb +112 -0
- data/lib/necropsy/bench/release_audit/config_validator.rb +112 -0
- data/lib/necropsy/bench/release_audit/git_snapshot.rb +36 -0
- data/lib/necropsy/bench/release_audit/performance_gate.rb +165 -0
- data/lib/necropsy/bench/release_audit/run_provenance.rb +133 -0
- data/lib/necropsy/bench/release_audit.rb +360 -0
- data/lib/necropsy/bench/report_normalizer.rb +140 -0
- data/lib/necropsy/bench/review_queue.rb +154 -0
- data/lib/necropsy/bench/safety_mutation_harness.rb +59 -0
- data/lib/necropsy/bench/seed_runner.rb +408 -0
- data/lib/necropsy/bounded_canonicalizer.rb +218 -0
- data/lib/necropsy/cache/scan_cache.rb +85 -17
- data/lib/necropsy/call_site_identity.rb +54 -0
- data/lib/necropsy/cli.rb +220 -33
- data/lib/necropsy/clock.rb +40 -0
- data/lib/necropsy/confidence/scorer.rb +103 -58
- data/lib/necropsy/configuration.rb +224 -21
- data/lib/necropsy/convention_rules.rb +138 -0
- data/lib/necropsy/definition_identity/canonical_digest.rb +278 -0
- data/lib/necropsy/definition_identity.rb +37 -0
- data/lib/necropsy/diagnostics.rb +176 -36
- data/lib/necropsy/embedded_ruby.rb +55 -0
- data/lib/necropsy/entry_points/plain.rb +111 -10
- data/lib/necropsy/entry_points/rails.rb +322 -41
- data/lib/necropsy/entry_points/test.rb +6 -1
- data/lib/necropsy/flow_interpreter.rb +460 -0
- data/lib/necropsy/graph/blocker_matching.rb +338 -0
- data/lib/necropsy/graph/call_graph.rb +1099 -109
- data/lib/necropsy/graph/definition_index.rb +149 -0
- data/lib/necropsy/graph/dynamic_evidence_tracking.rb +206 -0
- data/lib/necropsy/graph/evidence_store.rb +213 -0
- data/lib/necropsy/graph/resolution_store.rb +497 -0
- data/lib/necropsy/graph_self_check.rb +79 -0
- data/lib/necropsy/guardrail/baseline.rb +350 -13
- data/lib/necropsy/guardrail/quarantine.rb +94 -9
- data/lib/necropsy/load_graph.rb +206 -0
- data/lib/necropsy/models.rb +878 -13
- data/lib/necropsy/performance_profiler.rb +108 -0
- data/lib/necropsy/project.rb +327 -25
- data/lib/necropsy/reachability/engine.rb +54 -13
- data/lib/necropsy/reference_barrier.rb +458 -0
- data/lib/necropsy/report.rb +113 -4
- data/lib/necropsy/reporter.rb +431 -15
- data/lib/necropsy/runner.rb +233 -18
- data/lib/necropsy/runtime_feedback.rb +136 -0
- data/lib/necropsy/semantics_matrix.rb +153 -0
- data/lib/necropsy/type_facts.rb +53 -0
- data/lib/necropsy/version.rb +1 -1
- data/lib/necropsy/why_not_explanation.rb +436 -0
- data/lib/necropsy/why_not_renderer.rb +197 -0
- data/lib/necropsy/world_policy.rb +90 -0
- data/lib/necropsy.rb +35 -2
- data/schema/necropsy-report-v2.schema.json +366 -0
- 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
|