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,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'fileutils'
5
+ require 'optparse'
6
+ require 'yaml'
7
+ require_relative '../lib/necropsy'
8
+
9
+ root = File.expand_path('..', __dir__)
10
+ options = {
11
+ input: File.join(root, 'bench/golden/v1/reports'),
12
+ output: File.join(root, 'bench/audits/0.2.1/review_queue.yml'),
13
+ target: 300
14
+ }
15
+
16
+ OptionParser.new do |parser|
17
+ parser.banner = 'Usage: bundle exec ruby bench/review_queue.rb [options]'
18
+ parser.on('--input DIR', 'Directory containing normalized report JSON files') { |path| options[:input] = path }
19
+ parser.on('--output PATH', 'Review queue YAML output path') { |path| options[:output] = path }
20
+ parser.on('--target COUNT', Integer, 'Required reviewed high-candidate target') { |count| options[:target] = count }
21
+ end.parse!
22
+
23
+ reports = Dir.glob(File.join(File.expand_path(options.fetch(:input), root), '*.json')).to_h do |path|
24
+ [File.basename(path, '.json'), JSON.parse(File.read(path))]
25
+ end
26
+ raise Necropsy::Error, 'No normalized reports were found for review queue generation' if reports.empty?
27
+
28
+ queue = Necropsy::Bench::ReviewQueue.new(
29
+ reports: reports,
30
+ target_reviewed_high: options.fetch(:target)
31
+ ).call
32
+ output = File.expand_path(options.fetch(:output), root)
33
+ FileUtils.mkdir_p(File.dirname(output))
34
+ File.write(output, YAML.dump(queue))
35
+ puts "review queue: #{output} (#{queue.fetch('queued_candidates')} pending entries)"
data/bench/run.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require_relative '../lib/necropsy'
5
+ require_relative '../lib/necropsy/bench/seed_runner'
6
+
7
+ root = File.expand_path('..', __dir__)
8
+ options = {
9
+ manifest: File.join(__dir__, 'corpora/v1/manifest.yml'),
10
+ output: File.join(root, 'tmp/necropsy-benchmark/v1')
11
+ }
12
+
13
+ OptionParser.new do |parser|
14
+ parser.banner = 'Usage: bundle exec ruby bench/run.rb [options]'
15
+ parser.on('--manifest PATH', 'Corpus manifest path') { |path| options[:manifest] = path }
16
+ parser.on('--output DIR', 'Generated result directory') { |path| options[:output] = path }
17
+ parser.on('--update-golden REASON', 'Replace deterministic golden files with an audit reason') do |reason|
18
+ options[:update_golden_reason] = reason
19
+ end
20
+ end.parse!
21
+
22
+ runner = Necropsy::Bench::SeedRunner.new(
23
+ manifest_path: options.fetch(:manifest),
24
+ output_dir: options.fetch(:output)
25
+ )
26
+ summary = runner.call(update_golden_reason: options[:update_golden_reason])
27
+ puts "summary: #{File.join(File.expand_path(options.fetch(:output)), 'summary.json')}"
28
+ failed_corpus = summary.fetch('corpora').any? { |corpus| corpus['status'] == 'failed' }
29
+ golden_mismatch = summary.dig('golden', 'status') != 'match'
30
+ precision_failure = summary.dig('precision_gate', 'passed') == false
31
+ exit(failed_corpus || golden_mismatch || precision_failure ? 1 : 0)
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/ydah/necropsy/bench/schema/candidate-union-v1.schema.json",
4
+ "title": "Necropsy candidate union",
5
+ "type": "object",
6
+ "required": ["schema_version", "tool_runs", "summary", "candidates"],
7
+ "properties": {
8
+ "schema_version": { "const": 1 },
9
+ "tool_runs": { "type": "object", "additionalProperties": { "$ref": "#/$defs/toolRun" } },
10
+ "summary": { "type": "object" },
11
+ "candidates": {
12
+ "type": "array",
13
+ "items": { "$ref": "#/$defs/candidate" }
14
+ }
15
+ },
16
+ "$defs": {
17
+ "toolRun": {
18
+ "type": "object",
19
+ "required": ["status"],
20
+ "properties": {
21
+ "status": { "enum": ["generated", "snapshot", "skipped"] },
22
+ "version": { "type": ["string", "null"] },
23
+ "diagnostic": { "type": "string" },
24
+ "snapshot_sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
25
+ "provenance": { "type": ["object", "string"] }
26
+ }
27
+ },
28
+ "toolResult": {
29
+ "type": "object",
30
+ "required": ["candidate"],
31
+ "properties": {
32
+ "candidate": { "type": ["boolean", "null"] },
33
+ "status": { "enum": ["skipped"] },
34
+ "state": { "type": "string" },
35
+ "confidence": { "type": "string" },
36
+ "classification": { "type": "string" }
37
+ }
38
+ },
39
+ "label": {
40
+ "type": "object",
41
+ "required": ["value", "rationale"],
42
+ "properties": {
43
+ "value": { "enum": ["dead", "alive", "external", "unknown"] },
44
+ "rationale": { "type": "string", "minLength": 1 },
45
+ "reviewer": { "type": "string" }
46
+ }
47
+ },
48
+ "candidate": {
49
+ "type": "object",
50
+ "required": ["corpus", "id", "tool_results"],
51
+ "properties": {
52
+ "corpus": { "type": "string" },
53
+ "id": { "type": "string" },
54
+ "path": { "type": "string" },
55
+ "line": { "type": "integer", "minimum": 1 },
56
+ "label": { "$ref": "#/$defs/label" },
57
+ "tool_results": {
58
+ "type": "object",
59
+ "required": ["necropsy", "debride", "spoom", "type_aware"],
60
+ "properties": {
61
+ "necropsy": { "$ref": "#/$defs/toolResult" },
62
+ "debride": { "$ref": "#/$defs/toolResult" },
63
+ "spoom": { "$ref": "#/$defs/toolResult" },
64
+ "type_aware": { "$ref": "#/$defs/toolResult" }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,179 @@
1
+ # `.idea/impv.md` implementation matrix
2
+
3
+ Reviewed against the working implementation on 2026-08-12. “Safe equivalent” means the proposed
4
+ mechanism was not copied literally, but its removal-safety or operability goal is enforced by a
5
+ smaller reviewed contract. “No-go” is an explicit adversarial decision with a reconsideration gate
6
+ in the linked ADR; it does not mean silently deferred work.
7
+
8
+ ## 1–38: finite flow, Ruby semantics, and health
9
+
10
+ | # | Disposition | Evidence and decision |
11
+ |---:|---|---|
12
+ | 1 | Implemented | Dynamic send reads argument zero only; adversarial later literals, splats, forwarding, and finite names are covered in `flow_interpreter_spec.rb`. |
13
+ | 2 | Implemented | Unsupported/control-flow writes invalidate exact local facts in `flow_interpreter.rb`; monotonic safety specs compare candidate sets. |
14
+ | 3 | Implemented | No-else `if` joins the pre-branch environment. |
15
+ | 4 | Implemented | Case predicates/guards are evaluated and the unmatched path is joined. |
16
+ | 5 | Implemented | `and`/`or` use short-circuit path joins rather than unconditional right-side effects. |
17
+ | 6 | Implemented | Lambda capture uses an isolated environment; construction does not mutate outer locals. |
18
+ | 7 | Implemented | Return/break/next/raise-like termination is represented by transfer state and unreachable tails do not refine facts. |
19
+ | 8 | Implemented | Hash splats and unknown keys make the container partial/unknown. |
20
+ | 9 | Implemented | Symbol and string keys remain distinct and finite candidate keys are joined. |
21
+ | 10 | Implemented | `.new` is exact only for a proven core constructor path; overridden/unknown constructors remain hints. |
22
+ | 11 | Implemented | Modifier `private def`/`protected def` visits the nested definition without changing ambient visibility. |
23
+ | 12 | Implemented | Modifier `module_function def` creates the private instance definition and physical singleton copy without leaking mode. |
24
+ | 13 | Implemented | Positional and keyword default expressions are visited in method caller context. |
25
+ | 14 | Implemented | Every semantic call handler returns `CallTraversal(receiver, arguments, block)`; wrong contracts raise. |
26
+ | 15 | Implemented | Dynamic `define_method` gets an owner-scoped blocker and isolated synthetic body. |
27
+ | 16 | Implemented | `class << Constant` uses the constant singleton owner; a dynamic expression is blocked. |
28
+ | 17 | Implemented | Dynamic `def receiver.name` is isolated behind a dynamic singleton-definition blocker. |
29
+ | 18 | Implemented | Dynamic superclass expressions are visited and never substituted with `Object`. |
30
+ | 19 | Implemented | Scanner context carries lexical nesting separately from the definition owner. |
31
+ | 20 | Implemented | Pattern matching is ancestry control flow; unknown semantic shapes fail closed via the generated semantics matrix. |
32
+ | 21 | Implemented | `class_exec`, `module_exec`, and `instance_exec` use bounded owners or a synthetic blocked context. |
33
+ | 22 | Implemented | Dynamic attr/delegate/Forwardable/Struct/Data names emit generated-surface blockers and still traverse inputs. |
34
+ | 23 | Safe equivalent | `remove_method`/`undef_method` are activation blockers, so stale definitions are never treated as exact. Full activation simulation is intentionally not claimed. |
35
+ | 24 | Implemented | Alias/module-function relations retain physical source definitions when provable; ambiguity keeps duplicate/activation blockers. |
36
+ | 25 | Implemented | Visibility mutation considers physical definitions and retains uncertainty when activation order is not closed. |
37
+ | 26 | Implemented | Literal callback `if:`/`unless:` method names become callback roots; static disablement and dynamic conditions are distinguished. |
38
+ | 27 | Implemented | Callback blocks receive synthetic rooted definitions. |
39
+ | 28 | Implemented | Dynamic routes use owner, namespace, message, or global residual scope based on known context. |
40
+ | 29 | Implemented | Route read/encoding/parse failures create global `rails_route_health` blockers and degraded health. |
41
+ | 30 | Implemented | Oversized, unreadable, generated, and budget-skipped runtime references create a global blocker and degraded health. |
42
+ | 31 | Implemented | Discovery records unreadable paths and symlinks with domain provenance; unsafe runtime omissions block findings. |
43
+ | 32 | Implemented | `analysis_health` separates degraded/invalid analysis from findings; check, baseline, write quarantine, and release bench fail closed. |
44
+ | 33 | Implemented | Analyzer results are validated on a staged graph and committed atomically. |
45
+ | 34 | Implemented | Cache read/write rescue boundaries cannot execute the scan block twice. |
46
+ | 35 | Implemented | Pre/post source snapshots must match; a concurrent change makes analysis invalid. |
47
+ | 36 | Implemented | Normal check accepts exact v2 identity only; non-exact legacy migration produces an explicit review report. |
48
+ | 37 | Implemented | Baseline, ratchet, check, and benchmark gates use actionable classifications only. |
49
+ | 38 | Implemented | `bench --check` returns nonzero when release criteria fail. |
50
+
51
+ ## 39–72: graph semantics and additional static analysis
52
+
53
+ | # | Disposition | Evidence and decision |
54
+ |---:|---|---|
55
+ | 39 | Implemented | `paths.test` configures test domains, including `features/` and nested engine layouts. |
56
+ | 40 | Implemented | File-root identity is path/load-unit based; content digest is separate provenance. |
57
+ | 41 | Implemented | Definition and call-site identity schemas are versioned and reported with Ruby/Prism/tool versions. |
58
+ | 42 | Implemented | `load_graph.rb` adds only literal require/require_relative/autoload evidence and blocks unresolved load targets. |
59
+ | 43 | Implemented | Unrooted load units with side-effectful bodies are emitted as bounded diagnostics. |
60
+ | 44 | No-go | Shadow classification without a closed activation witness is unsafe; duplicates remain physical and blocked. See `necropsy_scope_decisions.md`. |
61
+ | 45 | Implemented | Runtime/test definition indexes are separate for ambiguity fallback. |
62
+ | 46 | Implemented | Partial/unknown resolutions use the smallest proven owner/namespace/message residual scope. |
63
+ | 47 | Safe equivalent | A flow-budget miss falls back at the affected site; only an unscoped semantic loss can widen a blocker. |
64
+ | 48 | Implemented | Dynamic ancestry is owner/descendant or namespace scoped; only Object/Kernel/unknown surfaces go global. |
65
+ | 49 | Implemented | CHA delegates lookup order to the canonical CallGraph APIs. |
66
+ | 50 | Implemented | Singleton lookup does not fall back to the owner's instance surface. |
67
+ | 51 | Implemented | Include/prepend/extend have distinct instance and singleton lookup relations. |
68
+ | 52 | No-go | Default RTA is non-pruning, so a second root-seeded fixed point has no safety benefit absent measured precision gain. See `necropsy_scope_decisions.md`. |
69
+ | 53 | Implemented | Legacy RTA pruning adds invalid health; CI cannot allow it as a degraded exception. |
70
+ | 54 | Safe equivalent | Factory-name evidence is rank-only and cannot refute targets; exact construction still requires receiver/core-constructor proof. |
71
+ | 55 | Implemented | Core protocol summaries transform receivers/arguments/elements and encode block-state semantics; user methods with the same name do not trigger them. |
72
+ | 56 | Implemented | Derived protocol operations are first-class call sites with identities, resolutions, blockers, and diagnostics. |
73
+ | 57 | Implemented | Unresolved dispatch derives owner-scoped `method_missing` and implicit-private `respond_to_missing?` calls, respecting overrides. |
74
+ | 58 | Safe equivalent | Name-only discounts were narrowed to VM hooks/core protocols; concrete implicit operations produce edges and numeric scores remain ranking only. |
75
+ | 59 | Safe equivalent | Resolution status/scope, health, world policy, blockers, and evidence grade form the actionability claims; confidence numbers cannot override them. |
76
+ | 60 | Implemented | Only analyzers declaring `complete_resolution` capability may emit a complete resolution. |
77
+ | 61 | No-go | An RBS provider lacks a reviewed stale/conflicting-signature target corpus; syntax fixtures are insufficient. See `necropsy_type_facts_adr.md`. |
78
+ | 62 | No-go | Sorbet/RBI ingestion has the same unproven open-world contract and a larger generated-RBI surface. See the type-facts ADR. |
79
+ | 63 | No-go | Demand-driven interprocedural points-to is excluded from removal decisions until the type/alias/load-order gate is met. |
80
+ | 64 | No-go | Interprocedural return summaries are not promoted without a purity/mutation corpus; local return facts stay bounded. |
81
+ | 65 | No-go | Argument-to-parameter propagation is not promoted without dispatch and mutation labels; call arguments remain recorded evidence. |
82
+ | 66 | No-go | Constructor ivar summaries are not promoted without aliasing/reopen coverage. |
83
+ | 67 | Safe equivalent | Literal Proc/lambda/block values flow locally and fail closed at method boundaries; interprocedural promotion follows the type-facts gate. |
84
+ | 68 | No-go | Cross-file constant facts require proven activation/load order; literal local containers and class objects remain available without that claim. |
85
+ | 69 | No-go | Physical definitions remain the removal/review unit; cycle collapsing can hide mixed-risk members. See `necropsy_scope_decisions.md`. |
86
+ | 70 | No-go | Per-definition why-not already exposes boundary sites/blockers; cluster frontier awaits the documented review-time gate. |
87
+ | 71 | Implemented | Why-not emits bounded, structured `suggested_next_evidence` for receiver, route, parser, runtime, and caller gaps. |
88
+ | 72 | Safe equivalent | Typed load evidence and unrooted-load diagnostics keep activation distinct without a second mutable graph; split only at the ADR gate. |
89
+
90
+ ## 73–96: Rails, frameworks, and external references
91
+
92
+ | # | Disposition | Evidence and decision |
93
+ |---:|---|---|
94
+ | 73 | Implemented | Rails-generated definitions are excluded from removal findings and summarized by source macro. |
95
+ | 74 | Safe equivalent | Both enum declaration forms, prefix/suffix, scopes, and instance-method flags are modeled; dynamic/version-dependent forms block the owner instead of guessing. |
96
+ | 75 | Implemented | Belongs-to/has-one and collection associations generate distinct APIs; invalid collection build/create methods are not invented. |
97
+ | 76 | Implemented | Attribute, class/mattr/cattr, store, and store_accessor declarations share generated-method handling and are never removal candidates. |
98
+ | 77 | Implemented | Rails scope and enum scopes are singleton generated methods; literal scope bodies attach calls to the generated definition. |
99
+ | 78 | Safe equivalent | Prism first verifies route DSL calls and static argument shapes; regex is restricted to the verified call slice. Unrelated Ruby strings cannot root routes. |
100
+ | 79 | Implemented | Canonical inflection blocks are parsed structurally for literal irregular/acronym/uncountable declarations; unsupported plural rules globally block pruning. |
101
+ | 80 | Implemented | Only executable ERB regions are parsed with Prism; HTML and ERB comments cannot root/block methods. |
102
+ | 81 | No-go | Pretending generic token extraction is a sound Haml/Slim/Jbuilder/Builder parser is rejected; unparsed inputs remain conservative blockers. See scope decisions. |
103
+ | 82 | Implemented | ActionCable hooks plus stream block/callback registrations are owner-scoped roots. |
104
+ | 83 | Implemented | ActiveJob/Sidekiq perform, serialization hooks, retry/discard, retry-in, and exhausted blocks are rooted declaratively. |
105
+ | 84 | Implemented | GraphQL resolver/subscription hooks and static `field` resolver methods are rooted; dynamic field methods block the GraphQL owner. |
106
+ | 85 | Safe equivalent | AMS, Blueprinter, and ViewComponent use the shared rule schema; generic presenter rooting is rejected because it has no runtime contract. |
107
+ | 86 | Implemented | Convention rule matching runs for every method family, not only `on_*`. |
108
+ | 87 | Implemented | Rule count is validated before any truncation and excess input is rejected. |
109
+ | 88 | Implemented | Gemfile/gemspec Prism calls and exact lock records enable non-Rails packs; comments/ordinary strings do not. |
110
+ | 89 | Implemented | Gemspec name and require_paths are parsed from the `Gem::Specification.new` block to find primary API files. |
111
+ | 90 | Safe equivalent | Format-aware strong contexts/comments/qualified owners reduce noise, while a generic barrier only adds uncertainty. Dedicated parsers require the ADR conformance gate. |
112
+ | 91 | Implemented | Text files up to the bounded streaming limit are scanned line by line. |
113
+ | 92 | Implemented | Global byte, match, and monotonic-time budgets degrade health instead of silently truncating. |
114
+ | 93 | Implemented | Qualified owner references block only the matching physical owner. |
115
+ | 94 | Implemented | Common short names require symbol/string, qualified, ERB, or structured DSL context. |
116
+ | 95 | No-go | A `trusted_generated` bypass would convert provenance into unsafe negative evidence; generated skips instead block globally. See scope decisions. |
117
+ | 96 | Safe equivalent | Skip reason, file/domain samples, counts, blocker source, and stable config key are report/why-not provenance; YAML source-line retention is not required for safety. |
118
+
119
+ ## 97–129: contracts, CLI, and performance
120
+
121
+ | # | Disposition | Evidence and decision |
122
+ |---:|---|---|
123
+ | 97 | Implemented | Top-level analysis health has complete/degraded/invalid status and structured reasons. |
124
+ | 98 | Implemented | Findings=1, configuration/execution=2, analysis health=3. |
125
+ | 99 | Implemented | Strict health and exact degraded-reason allowlists apply to every analysis command; invalid is never allowed. |
126
+ | 100 | Implemented | Reports include source snapshot and tool/runtime/config/identity provenance. |
127
+ | 101 | Safe equivalent | Packaged v2 JSON Schema, compatibility fields, migration notes, and schema specs define the compatibility policy without a second policy format. |
128
+ | 102 | Implemented | SARIF reachability witnesses are codeFlows. |
129
+ | 103 | Implemented | SARIF blocker/reference locations are relatedLocations. |
130
+ | 104 | Implemented | Summary separates actionable, diagnostic, blocked, and health counts. |
131
+ | 105 | Implemented | Check and health failures preserve JSON/YAML/NDJSON/SARIF/GitHub output; machine reports include structured health. |
132
+ | 106 | Implemented | `baseline migrate` is explicit; normal check will not silently migrate. |
133
+ | 107 | Implemented | Baseline writes use temporary write, fsync, and atomic rename. |
134
+ | 108 | Implemented | Duplicate identities and unknown classification/confidence values are rejected. |
135
+ | 109 | Implemented | Quarantine annotations carry and exactly match physical fingerprints at the definition. |
136
+ | 110 | Implemented | Generated methods from one macro are grouped into one macro diagnostic/annotation unit. |
137
+ | 111 | Implemented | Quarantine writes recheck source digests and atomically replace files. |
138
+ | 112 | Implemented | `--as-of` and SOURCE_DATE_EPOCH provide reproducible time. |
139
+ | 113 | Implemented | Fractions, days, limits, timeouts, and finite-number constraints are validated. |
140
+ | 114 | Implemented | Static analyzer uniqueness and dependency order are validated. |
141
+ | 115 | Safe equivalent | Model constructors bound enums/numbers/text/metadata; current producers are versioned and legacy is explicitly normalized to stable `unversioned`. See scope decisions. |
142
+ | 116 | Implemented | Custom analyzers require `trusted: true`, validate capabilities/results, and apply through the atomic staging contract. |
143
+ | 117 | No-go | A per-file fact cache failed the measured scan-share gate. See `necropsy_performance_adr.md`. |
144
+ | 118 | Implemented | Cache identity includes tool, Ruby engine/version, Prism, definition, call-site, config, inventory, and file content digests. |
145
+ | 119 | Implemented | Find-based discovery streams and prunes excluded directories before descent. |
146
+ | 120 | Safe equivalent | Fixed generated/cache directories are pruned at every depth; `paths.exclude` is not pruned because excluded Ruby still belongs to the default reference safety scope. |
147
+ | 121 | Implemented | Reverse subclass index and descendant cache avoid repeated whole-class scans. |
148
+ | 122 | Implemented | Instance and singleton lookup chains are cached and invalidated with graph indexes. |
149
+ | 123 | Implemented | Reachability and descendant traversal use head-index queues with stable ordering. |
150
+ | 124 | Implemented | Message, accepted/rejected target, and resolution indexes back why-not queries. |
151
+ | 125 | Implemented | Baseline comparison preindexes physical/logical fingerprints, body, symbol, and path. |
152
+ | 126 | Implemented | Benchmark gold labels are preindexed. |
153
+ | 127 | Implemented | NDJSON streams report, nodes, calls, edges, evidence, and metadata without nesting the graph payload. |
154
+ | 128 | No-go | Process-parallel parse is below the scan-share gate and adds worker/parity failure modes. See the performance ADR. |
155
+ | 129 | No-go | Candidate-specific template/reference caching is below its p95 share gate and risks stale blockers. See the performance ADR. |
156
+
157
+ ## 130–148: adversarial tests and evaluation
158
+
159
+ | # | Disposition | Evidence and decision |
160
+ |---:|---|---|
161
+ | 130 | Implemented | Metamorphic identity specs cover comments, whitespace, unrelated definitions, ordering, and cache modes. |
162
+ | 131 | Implemented | Safety invariants assert unknown syntax, parse errors, dynamic routes, and unreadable references cannot increase actionable candidates. |
163
+ | 132 | Implemented | CLI integration covers analyzer execution, validation, capability, and atomic-apply failures. |
164
+ | 133 | Implemented | Source mutation during scan/snapshot makes health invalid. |
165
+ | 134 | Implemented | Dynamic-send fixtures cover later literals, splats, keywords, forwarding, and finite symbols. |
166
+ | 135 | Implemented | Flow fixtures cover no-else, short circuit, loops, lambda, transfer, rescue, op assignment, hash splat, and multi-key joins. |
167
+ | 136 | Implemented | Modifier, attr visibility, singleton constant, and dynamic singleton fixtures are present. |
168
+ | 137 | Implemented | CHA/RTA fixtures cover lookup surfaces, puts arguments, container elements, block states, sort/sort_by, and user-name collisions. |
169
+ | 138 | Implemented | Deterministic generated Ruby programs compare static targets with TracePoint runtime targets as a test oracle only. |
170
+ | 139 | Implemented | CI covers supported Ruby versions and a locked minimum Prism suite for identity, lookup, blockers, and report contracts. |
171
+ | 140 | Implemented | Deterministic AST-shape mutation/fuzz specs assert no crash, unbounded canonicalization, or unsafe finding growth. |
172
+ | 141 | Implemented | Candidate-union evaluation reports per-project metrics and macro averages, not only micro totals. |
173
+ | 142 | Implemented | Reviewed labels require reviewer, rationale, reviewed_at, and source_revision provenance. |
174
+ | 143 | Safe equivalent | Static analyzer ablation enumerates every built-in analyzer; the release precision gate requires every declared default feature's on/off evidence and improvement. |
175
+ | 144 | Implemented | Safety mutation harness breaks parse, scope, health, and positive-only constraints and requires the corpus to detect each mutant. |
176
+ | 145 | Implemented | Repeated samples gate p95/max wall time, RSS, allocations, and artifact size. |
177
+ | 146 | Implemented | `--self-check` validates resolution scope, derived-call resolution, evidence relations, node endpoints, and blocker/actionable exclusion. |
178
+ | 147 | Implemented | Runtime collectors and CLI artifact IDs accept injected clock/random sources and are deterministic under SOURCE_DATE_EPOCH. |
179
+ | 148 | Implemented | `semantics` enumerates every installed Prism node plus Ruby hooks and Rails DSL states; unknown/future nodes default unsupported. |
@@ -0,0 +1,57 @@
1
+ # Migrating to 0.2.1
2
+
3
+ Version 0.2.1 favors a visible unknown state over a potentially unsafe deletion recommendation.
4
+ Most projects need no configuration change, but candidate counts and CI output can change.
5
+
6
+ ## Findings and confidence
7
+
8
+ Methods affected by unresolved runtime dispatch, incomplete Ruby source, or an analyzer failure now
9
+ appear as low-confidence `blocked` findings. They remain visible even when the normal report threshold
10
+ omits low-confidence dead-code candidates. JSON, human, GitHub, SARIF, `why`, and `explain` output
11
+ include the matching blocker or source diagnostic.
12
+
13
+ RTA is rank-only by default and no longer removes broader static call edges. A temporary compatibility
14
+ setting restores the old destructive pruning while a project compares results:
15
+
16
+ ```yaml
17
+ rta:
18
+ pruning: legacy
19
+ ```
20
+
21
+ Remove that setting after review; `rank_only` is the safe default.
22
+
23
+ ## Runtime evidence and quarantine
24
+
25
+ Coverage, TracePoint, and Coverband payloads only provide positive liveness evidence. An unobserved
26
+ method is not made more likely to be dead, and `dynamic.min_observation_days` no longer affects
27
+ classification. This intentionally has no compatibility switch because absence from a partial runtime
28
+ sample is not deletion evidence.
29
+
30
+ Quarantine expiry also no longer increases a finding's score. It is an operational review signal:
31
+
32
+ ```yaml
33
+ quarantine:
34
+ expiry: warn # warn | fail | ignore
35
+ ```
36
+
37
+ `fail` changes the check command's exit status for expired reviews; it does not change analysis.
38
+
39
+ ## Remote Coverband/Redis sources
40
+
41
+ `rediss://` now requires peer and hostname verification using the system CA store. Redis evidence is
42
+ bounded by DNS/connect/read/total deadlines and key, response, bulk, array, nesting, and payload limits.
43
+ Invalid limits are rejected during configuration. Credentials are redacted from domain errors.
44
+
45
+ If a private deployment previously depended on an unverified certificate, install its CA in the system
46
+ trust store before upgrading. Disabling TLS verification is not supported.
47
+
48
+ ## Auditing the change
49
+
50
+ The five-corpus safety audit can be reproduced from a clean worktree:
51
+
52
+ ```shell
53
+ NECROPSY_RUBOCOP_CORPUS=/path/to/rubocop-1.75.0 bundle exec ruby bench/audit.rb
54
+ ```
55
+
56
+ The command fails closed for stale reports, incomplete review policy, incompatible performance
57
+ provenance, missing RSS, failed adversarial suites, or unreviewed new high-confidence candidates.
@@ -0,0 +1,207 @@
1
+ # Migrating to 0.3.0
2
+
3
+ Version 0.3.0 introduces physical definition identities and structured call-site resolution. The
4
+ changes are additive for normal report consumers, but integrations that inspect the optional graph
5
+ or implement custom analyzers should review the compatibility rules below.
6
+
7
+ ## Report and graph JSON
8
+
9
+ The existing top-level report keys and legacy logical method name remain available. Definition
10
+ objects now include both identities:
11
+
12
+ - `id` and `symbol_id` are the logical name, such as `Billing::Charge#call`.
13
+ - `definition_id` is the physical definition identity and is stable across comment-only and line-only
14
+ edits. Reopened classes and repeated definitions have distinct physical IDs.
15
+ - `body_digest` and `ordinal` support review and future migration when a physical ID changes.
16
+
17
+ Graph edges, call-site callers, runtime matches, and entry points use physical definition IDs so that
18
+ one logical name never silently selects one of several definitions. `CallSite` keeps `caller_id` for
19
+ existing readers and also emits the explicit alias `caller_definition_id`; both contain the same
20
+ physical ID. Call sites additionally emit `call_site_id`.
21
+
22
+ Readers should ignore unknown keys, treat versioned IDs as opaque strings, and use `symbol_id` when
23
+ grouping physical definitions by their legacy logical name. Readers that previously assumed a single
24
+ node per logical name must handle more than one definition. Ambiguous runtime references are reported
25
+ as ambiguous instead of being assigned to the first definition.
26
+
27
+ The optional graph payload also adds `resolutions`, `resolution_conflicts`, and a single interned
28
+ evidence store. Existing edge objects and nested `evidences` remain present. The following additive
29
+ fields let new readers avoid duplicating evidence payloads:
30
+
31
+ - `edge_projection` names the projection used by the legacy `edges` array (`conservative`).
32
+ - `edge_relations` contains physical caller/callee IDs and stable `evidence_ids`.
33
+ - `evidence_records` contains each evidence payload once.
34
+ - `evidence_collisions` reports IDs that were supplied with conflicting payloads.
35
+
36
+ Existing evidence fields (`analyzer`, `kind`, `weight`, `details`, and `metadata`) remain present;
37
+ provenance fields are additive. Numeric `weight` is retained for display compatibility but must not
38
+ be interpreted as call resolution completeness. A conflicting evidence ID is quarantined, its graph
39
+ references are removed, and a conservative blocker is emitted instead of selecting one payload.
40
+
41
+ ## Evidence graph projections
42
+
43
+ The graph stores one physical edge relation and its evidence IDs, then derives views at query time:
44
+
45
+ - `conservative` includes exact, conservative, heuristic, observed, and grade-less legacy positive
46
+ evidence. It is the default for reachability and preserves pre-0.3.0 reachability behavior.
47
+ - `exact` includes exact evidence plus observed evidence only when the query supplies a revision and
48
+ the evidence scope has the same revision.
49
+ - `observed` includes only positive observed evidence and may be filtered by any supplied scope keys.
50
+
51
+ Unknown or partial residual relations remain scoped blockers; they are not expanded into speculative
52
+ mass edges. Legacy grade-less evidence is never promoted based on numeric weight and appears only in
53
+ the conservative projection. Callers using the Ruby API can pass `projection:` and `scope:` to graph
54
+ edge queries and the reachability engine.
55
+
56
+ ## World mode and root domains
57
+
58
+ The default remains `analysis.world: application`. Set `analysis.world: library` for gems and other
59
+ open-world libraries whose callers may be outside the repository. In library mode, every non-test
60
+ public or protected method is an `external` root; private methods are not implicitly protected.
61
+ Configured entry-point patterns are runtime roots in application mode and external roots in library
62
+ mode.
63
+
64
+ Roots now carry `runtime`, `test`, or `external` domains. Reachability keeps all three path sets
65
+ separate. Runtime and external walks cannot enter or bridge through test definitions, while a test
66
+ walk may still reach production definitions under test. The existing
67
+ `EntryPoint` constant and `node_id` accessor remain available; graph JSON adds `definition_id`,
68
+ `domain`, and `evidence` to each entry-point object. The `why` command reports the root domain,
69
+ reason, and provenance for its witness path.
70
+
71
+ `analysis.load_roots: known` is the compatibility default. Set it to `all` to conservatively root the
72
+ top-level block of every non-test analyzed Ruby file when the load graph is unknown. This can reduce
73
+ candidate yield, but avoids treating class-body registration reached from an unknown load unit as
74
+ dead. The scan cache schema is updated automatically.
75
+
76
+ ## Analysis, reference, and report scopes
77
+
78
+ Source and output filtering are now explicit and independent:
79
+
80
+ ```yaml
81
+ paths:
82
+ analyze: ["app/**", "lib/**"]
83
+ reference: ["**/*"]
84
+ report:
85
+ include: ["app/**"]
86
+ exclude: ["app/generated/**"]
87
+ ```
88
+
89
+ - `paths.analyze` selects Ruby definitions eligible for findings.
90
+ - `paths.reference` selects repository files used to discover callers and references. It defaults to
91
+ the whole repository. Ruby files in this scope are parsed into the graph, but their definitions are
92
+ not findings unless they are also in the analysis scope. Non-Ruby files are retained for reference
93
+ barriers.
94
+ - `report.include` and `report.exclude` only select displayed findings; they do not change graph
95
+ nodes, edges, roots, or reachability.
96
+
97
+ When `paths.analyze` is omitted, Necropsy preserves the pre-0.3.0 conventional Ruby scan scope.
98
+ Other Ruby sources found by the repository-wide reference scan can still contribute callers without
99
+ becoming findings themselves.
100
+
101
+ `paths.include` remains a compatibility alias for `paths.analyze`, and `paths.exclude` still removes
102
+ files from the analysis scope. Migrate `paths.include` to `paths.analyze` to make that intent clear.
103
+ When an analysis filter leaves a detected executable, test, route, task, or gem specification outside
104
+ the scope, Necropsy emits a warning and adds an `analysis_scope` report diagnostic. The diagnostic
105
+ also lists reference-only Ruby files and symlinks ignored during safe repository discovery.
106
+ If an explicit `paths.reference` leaves non-test Ruby files outside both scopes, all otherwise-dead
107
+ findings receive a `reference_scope_incomplete` blocker. The diagnostic reports excluded caller
108
+ counts and bounded samples; expand the reference scope before treating those findings as candidates.
109
+
110
+ Graph JSON adds `source_domains` (`analyze` or `reference`) and `scope_diagnostics`. These are
111
+ additive fields. Cache entries now include all selected reference files, so changing a non-Ruby
112
+ reference invalidates the scan cache automatically.
113
+
114
+ ## Non-Ruby reference barrier
115
+
116
+ After static reachability and before final scoring, Necropsy scans non-Ruby files selected by
117
+ `paths.reference` for otherwise-actionable method candidates. It recognizes direct method names,
118
+ symbol and string forms, lower-camel GraphQL-style names, and owner-qualified hints such as
119
+ `Billing::Charge#capture`. A match adds a definition-scoped `unparsed_external_reference` blocker,
120
+ so the method moves to `blocked` instead of remaining a dead-code candidate. Blocker metadata and
121
+ `why` / `explain` output include the matching file, line, match kind, and bounded source snippet.
122
+
123
+ The implementation uses a portable Ruby scanner and does not require ripgrep. Common names such as
124
+ `call` and `run` intentionally produce more blockers when they appear as bare text; safety takes
125
+ precedence over candidate yield until benchmark evidence justifies a dedicated parser. Parsed Ruby
126
+ files are excluded because their calls and references already flow through the graph. Whole-line and
127
+ inline comments, generated paths or generated comment headers, benchmark/tool metadata, known binary
128
+ extensions, NUL-containing files, invalid UTF-8, and files over 1 MiB are not searched. Reports expose
129
+ deterministic scanned/matched counts plus bounded skip reasons and samples; test-fixture matches are
130
+ retained as test-domain diagnostics and do not block runtime candidates.
131
+
132
+ ## Refutable `why-not` diagnostics
133
+
134
+ Use `necropsy why-not SYMBOL` for a candidate, blocked finding, or test-only definition. Human output
135
+ is intended for review; `--format json` emits the versioned `necropsy.why-not.v1` artifact. Its stable
136
+ top-level fields include the physical definition and same-name definitions, incoming call sites,
137
+ resolution status and rejected targets, matching and unknown/partial blockers, world/root policy,
138
+ non-Ruby matches, parse/analyzer failures, enabled analyzers and type providers, assumptions, and
139
+ suggested next evidence. `artifact_context` records the tool version, definition body digest,
140
+ configuration digest, and a bounded snapshot digest of the complete analyzed/reference source
141
+ inventory; an unavailable or size-limited snapshot is explicit rather than omitted. The artifact
142
+ also records risk flags, a recommended action, and a reachability witness or explicit absence
143
+ summary. Every collection reports total, returned, and truncated counts, including nested metadata.
144
+
145
+ Logical symbols with multiple physical definitions remain ambiguous. The diagnostic lists an
146
+ executable `why-not` command for each definition ID, so reviewers can inspect each body independently.
147
+ Missing symbols retain the existing partial-match suggestions.
148
+
149
+ ## Resolution and blockers
150
+
151
+ Every native static analyzer can return a `ResolutionRecord` per call site. Its status has explicit
152
+ semantics:
153
+
154
+ - `complete`: the target set is complete under the recorded assumptions; an empty set is valid.
155
+ - `partial`: known targets exist, but more targets may exist in the structured `unknown_scope`.
156
+ - `unknown`: no usable target was enumerated; the structured `unknown_scope` remains authoritative.
157
+
158
+ Every declared target must also have a corresponding physical graph edge from the call-site caller.
159
+ A missing edge is treated as an invalid resolution and blocks that physical target rather than making
160
+ it eligible as unreachable.
161
+
162
+ Partial and unknown results create scoped blockers. They are not converted into evidence that a
163
+ method is dead. Conflicting or malformed analyzer records are retained as diagnostics and produce
164
+ conservative blockers over the smallest safe scope. Evidence and resolutions record producer,
165
+ producer version, assumptions, grade, relation, source, and scope.
166
+
167
+ ## Custom analyzer compatibility
168
+
169
+ Existing `AnalyzerResult` constructors and existing edge/alive evidence continue to work. For a
170
+ static analyzer that omits `resolutions` (the legacy value is `nil`), Necropsy derives only
171
+ conservative `partial` or `unknown` records:
172
+
173
+ - an edge is associated with a call site only when its physical caller and either its `call_site_id`
174
+ or complete legacy call-site metadata identify exactly one site;
175
+ - logical target names that resolve to multiple physical definitions are not guessed;
176
+ - evidence weight never upgrades an adapted result to `complete`.
177
+
178
+ Native analyzers should return `resolutions: []` when an intentionally empty native result is desired,
179
+ or return explicit records with stable `call_site_id` and physical target IDs. Dynamic analyzers remain
180
+ positive-only and are not adapted into static completeness claims.
181
+
182
+ Legacy RTA pruning removes only the individual name-resolution or CHA evidence IDs rejected for the
183
+ analyzed call site. Evidence from observations or other producers on the same physical edge remains
184
+ attached, so pruning one producer cannot erase an independently supported relation.
185
+
186
+ The scan cache schema is versioned and is invalidated automatically. No manual cache removal or
187
+ configuration change is required.
188
+
189
+ ## Baseline schema v2
190
+
191
+ `necropsy baseline` now writes schema v2. Each entry stores the physical fingerprint,
192
+ `definition_id`, `body_digest`, `symbol_id`, and source path alongside the legacy logical
193
+ fingerprint. Existing schema v1 files are still read and are never rewritten by `check`.
194
+
195
+ Migration matching is deterministic and conservative: exact physical ID or fingerprint first,
196
+ then body digest, then the logical symbol and path hint. Classification remains part of the match.
197
+ If a step produces more than one current physical definition, `check` prints every candidate and
198
+ fails. Review those definitions and regenerate the baseline; Necropsy does not select the first
199
+ candidate. Removing the v2 baseline and restoring a v1 file restores the legacy reader path.
200
+
201
+ The report's existing `fingerprint` remains the logical fingerprint for JSON compatibility.
202
+ `logical_fingerprint` and `physical_fingerprint` are additive fields, and the top-level
203
+ `compatibility.finding_fingerprints` note documents their roles. SARIF likewise retains
204
+ `partialFingerprints.necropsy` and adds `necropsyPhysicalDefinition`, with both values repeated in
205
+ result properties. Benchmark JSON retains its legacy logical metrics and adds `identity_views` with
206
+ logical-symbol and physical-definition candidate inventories; repeated definitions count separately
207
+ in the physical view.
@@ -0,0 +1,13 @@
1
+ # Custom analyzer trust declaration
2
+
3
+ Custom analyzers execute Ruby code in the Necropsy process. They must now use a mapping with an explicit trust declaration:
4
+
5
+ ```yaml
6
+ analyzers:
7
+ custom:
8
+ - class: Company::Analyzer
9
+ require: config/company_analyzer.rb
10
+ trusted: true
11
+ ```
12
+
13
+ The former string form is rejected. `trusted: true` means that the analyzer and its required file are allowed to execute with the same privileges as Necropsy; it does not weaken result validation or atomic result application.