rigortype 0.1.5 → 0.1.6

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -50
  3. data/lib/rigor/analysis/buffer_binding.rb +36 -0
  4. data/lib/rigor/analysis/check_rules.rb +11 -1
  5. data/lib/rigor/analysis/dependency_source_inference/index.rb +14 -1
  6. data/lib/rigor/analysis/dependency_source_inference/return_type_heuristic.rb +105 -0
  7. data/lib/rigor/analysis/dependency_source_inference/walker.rb +32 -12
  8. data/lib/rigor/analysis/project_scan.rb +39 -0
  9. data/lib/rigor/analysis/runner.rb +309 -22
  10. data/lib/rigor/analysis/worker_session.rb +14 -2
  11. data/lib/rigor/builtins/hkt_builtins.rb +342 -0
  12. data/lib/rigor/builtins/static_return_refinements.rb +120 -0
  13. data/lib/rigor/cache/store.rb +33 -3
  14. data/lib/rigor/cli/lsp_command.rb +129 -0
  15. data/lib/rigor/cli/type_of_command.rb +44 -5
  16. data/lib/rigor/cli.rb +74 -12
  17. data/lib/rigor/configuration.rb +38 -2
  18. data/lib/rigor/environment/hkt_registry_holder.rb +33 -0
  19. data/lib/rigor/environment/rbs_coverage_report.rb +1 -1
  20. data/lib/rigor/environment/rbs_loader.rb +45 -2
  21. data/lib/rigor/environment/reporters.rb +40 -0
  22. data/lib/rigor/environment.rb +106 -9
  23. data/lib/rigor/inference/acceptance.rb +48 -3
  24. data/lib/rigor/inference/expression_typer.rb +47 -0
  25. data/lib/rigor/inference/hkt_body.rb +171 -0
  26. data/lib/rigor/inference/hkt_body_parser.rb +363 -0
  27. data/lib/rigor/inference/hkt_reducer.rb +256 -0
  28. data/lib/rigor/inference/hkt_registry.rb +223 -0
  29. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +125 -30
  30. data/lib/rigor/inference/method_dispatcher/receiver_affinity.rb +87 -0
  31. data/lib/rigor/inference/method_dispatcher.rb +154 -3
  32. data/lib/rigor/inference/project_patched_methods.rb +70 -0
  33. data/lib/rigor/inference/project_patched_scanner.rb +210 -0
  34. data/lib/rigor/inference/scope_indexer.rb +156 -12
  35. data/lib/rigor/inference/statement_evaluator.rb +106 -6
  36. data/lib/rigor/inference/synthetic_method_scanner.rb +94 -16
  37. data/lib/rigor/language_server/buffer_table.rb +63 -0
  38. data/lib/rigor/language_server/completion_provider.rb +438 -0
  39. data/lib/rigor/language_server/debouncer.rb +86 -0
  40. data/lib/rigor/language_server/diagnostic_publisher.rb +167 -0
  41. data/lib/rigor/language_server/document_symbol_provider.rb +142 -0
  42. data/lib/rigor/language_server/folding_range_provider.rb +75 -0
  43. data/lib/rigor/language_server/hover_provider.rb +74 -0
  44. data/lib/rigor/language_server/hover_renderer.rb +312 -0
  45. data/lib/rigor/language_server/loop.rb +71 -0
  46. data/lib/rigor/language_server/project_context.rb +145 -0
  47. data/lib/rigor/language_server/selection_range_provider.rb +93 -0
  48. data/lib/rigor/language_server/server.rb +384 -0
  49. data/lib/rigor/language_server/signature_help_provider.rb +249 -0
  50. data/lib/rigor/language_server/synchronized_writer.rb +28 -0
  51. data/lib/rigor/language_server/uri.rb +40 -0
  52. data/lib/rigor/language_server.rb +29 -0
  53. data/lib/rigor/plugin/base.rb +63 -0
  54. data/lib/rigor/plugin/macro/heredoc_template.rb +125 -11
  55. data/lib/rigor/plugin/manifest.rb +54 -7
  56. data/lib/rigor/plugin/registry.rb +19 -0
  57. data/lib/rigor/rbs_extended/hkt_directives.rb +326 -0
  58. data/lib/rigor/rbs_extended.rb +82 -2
  59. data/lib/rigor/sig_gen/generator.rb +12 -3
  60. data/lib/rigor/type/app.rb +107 -0
  61. data/lib/rigor/type.rb +1 -0
  62. data/lib/rigor/version.rb +1 -1
  63. data/sig/rigor/environment.rbs +8 -4
  64. data/sig/rigor/inference.rbs +2 -0
  65. data/sig/rigor.rbs +3 -1
  66. metadata +54 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b5960ec17b35768103e97d752f8cc6fd78fcb3f12e12fc43dfa41be07ec5317b
4
- data.tar.gz: e79c9b25c973c8938e9b2f0a2741cca5195342619827b320ca521ec09e54321e
3
+ metadata.gz: 1a2903b58458e4dafb9acfb05574d615031a79b8b7b521b02029bd30b07ef433
4
+ data.tar.gz: cf13a88ee96fe6a3acb471fb8f8936407b28bae6bd92852e8263b11ec0738870
5
5
  SHA512:
6
- metadata.gz: af1e033a25410c0f87943f12d43ab18a3a0d2a79c01307c2117c2fc15be4c9db3cb28e6fec10ce598ef6a5bfa063227f280c023a0f7e9025b06c69946df4654d
7
- data.tar.gz: 351b3275dd35f37a11d30a696627e23a6cdca31bfb94fa3eacae762d2de624e4a914c6f8f4eebdd7df0bd17fd9fac13fe55ac434957509b742771a00d352a981
6
+ metadata.gz: 5761ae7907222592d4fd8a7c747e24f414c95fc2ce2ce2b0385a8277b0860aaa89c76c2da94b37575f3a79ac6dddbd4fd21fd16f8064ded1aecbb0e5fadb68f7
7
+ data.tar.gz: 8c98d8abd24b9eacef42ba5dbcade9481427a03b61c57306efb273b34ca7b2cbdc73535d3c7495aba90b13051274f9df715a6064f98e9ffb9bdbc995e573560e
data/README.md CHANGED
@@ -15,13 +15,32 @@ for any class it can find, and reports a small but trustworthy
15
15
  catalogue of bugs (undefined methods on typed receivers, wrong
16
16
  positional arity, provable `Integer / 0`, …).
17
17
 
18
- The differentiator is a richer type vocabulary than ordinary
19
- RBS expresses. Rigor reasons about *what values an expression
20
- actually produces* literal values, integer ranges,
21
- refinement-type carriers, per-position tuple / hash shapes
22
- not just *which class an object belongs to*. See **[Beyond
23
- `Integer` and `String`](#beyond-integer-and-string-rigors-richer-type-vocabulary)**
24
- for the full type-model story; the short pitch is below.
18
+ **Two design commitments drive Rigor.**
19
+
20
+ 1. **Types are facts, not wishes.** Hand-written type
21
+ annotations drift from the implementation the moment they
22
+ are written. Rigor infers from the code itself every
23
+ carrier in its type vocabulary is derived from what your
24
+ source actually produces, not from a signature you authored
25
+ and might forget to update. When you do want RBS in
26
+ `sig/`, [`rigor sig-gen`](docs/adr/14-rbs-sig-generation.md)
27
+ emits it from inference results so the written form starts
28
+ in sync with reality, and `tighter-return` candidates flag
29
+ the cases where an existing `.rbs` is already weaker than
30
+ what the implementation provably returns.
31
+ 2. **Programmable inference beyond unions.** A plain union
32
+ (`Integer | nil`) is not the type story Ruby needs. Rigor
33
+ reasons about *what values an expression actually
34
+ produces* — literal values, integer ranges, refinement
35
+ carriers, per-position tuple / hash shapes, bound-method
36
+ bindings — and exposes a plugin extension API plus an
37
+ [ADR-16](docs/adr/16-macro-expansion.md) macro / DSL
38
+ expansion substrate so Rails-shape DSLs are first-class
39
+ type sources rather than analysis blind spots.
40
+
41
+ See **[Beyond `Integer` and `String`](#beyond-integer-and-string-rigors-richer-type-vocabulary)**
42
+ for the full type-model story; the carrier-zoo table is the
43
+ short pitch.
25
44
 
26
45
  When you want tighter types than RBS expresses, refine them
27
46
  through the
@@ -532,7 +551,7 @@ Common knobs the file exposes:
532
551
 
533
552
  ## Status
534
553
 
535
- Current released version: **`v0.1.4`**. The analyzer is usable
554
+ Current released version: **`v0.1.5`**. The analyzer is usable
536
555
  on real Ruby code today; the rule catalogue is deliberately
537
556
  narrow — Rigor's stance is to surface zero false positives
538
557
  while the inference surface stabilises. Forward-looking commitments
@@ -540,50 +559,19 @@ while the inference surface stabilises. Forward-looking commitments
540
559
  [`docs/ROADMAP.md`](docs/ROADMAP.md); the release-by-release
541
560
  "what shipped" record is [`CHANGELOG.md`](CHANGELOG.md).
542
561
 
543
- `v0.1.4` (released 2026-05-14) delivered:
544
-
545
- - **[ADR-10](docs/adr/10-dependency-source-inference.md) closed
546
- end-to-end** — opt-in gem-source inference, per-gem budget,
547
- cache slice, and the `dynamic.dependency-source.boundary-cross`
548
- `:info` diagnostic that surfaces RBS / gem-source overlap
549
- under `mode: :full`.
550
- - **[ADR-11](docs/adr/11-sorbet-input-adapter.md) primary surface
551
- + per-call-site assertion gating** — `rigor-sorbet` ingests
552
- Sorbet `sig { ... }` blocks, `T.let` / `T.cast` / `T.must` /
553
- `T.bind` / `T.assert_type!` / `T.reveal_type` / `T.absurd`,
554
- and RBI files. Per-call-site `enforce_sigil` gates assertion
555
- recognisers by the caller file's `# typed:` sigil.
556
- - **[ADR-13](docs/adr/13-typenode-resolver-plugin.md) plugin
557
- TypeNode resolver + TypeScript-utility-type adapter** —
558
- `Plugin::TypeNodeResolver` extension point + five
559
- Rigor-canonical shape-projection type functions
560
- (`pick_of` / `omit_of` / `partial_of` / `required_of` /
561
- `readonly_of`) + the opt-in `rigor-typescript-utility-types`
562
- plugin mapping TS spellings onto the core functions.
563
- `Pick[T, :a | :b]` round-trips through the directive grammar.
564
- - **[ADR-14](docs/adr/14-rbs-sig-generation.md) — `rigor sig-gen`
565
- CLI** — emits RBS from inference results across five
566
- classifications (`new-file` / `new-method` / `tighter-return`
567
- / `equivalent` / `skipped`); `--params=untyped` default,
568
- `--params=observed` opt-in via `--observe=PATH`.
569
- - **`Method` carrier (`Type::BoundMethod`)** —
570
- `Object#method(:sym).call` / `.()` / `[]` round-trip with
571
- full precision instead of collapsing to `untyped`.
572
- - **Rails ecosystem (Tier 1 + Tier 2)** — `rigor-rails-routes`,
573
- `rigor-rails-i18n`, `rigor-actionmailer`, `rigor-activejob`,
574
- `rigor-actionpack` (4 phases), `rigor-factorybot`, and
575
- `rigor-activerecord` publishing `:model_index` via the
576
- ADR-9 cross-plugin fact channel.
562
+ `v0.1.5` (released 2026-05-16) delivered (full slice list in `CHANGELOG.md` § `[0.1.5]`):
563
+
564
+ - **ADR-15 Ractor migration end-to-end** (Phases 1–4c + 4b.x) — opt-in `rigor check --workers=N` parallelism; pool ≡ sequential proven on 14 real-world projects (31,840 files); spec-suite wall-clock 162s → 27s on 12 cores via `parallel_tests`.
565
+ - **[ADR-16](docs/adr/16-macro-expansion.md) macro / DSL expansion substrate** — four-tier declarative manifest contract (block-as-method, trait-inlining registry, heredoc-template, external-file) with Tier B/C precision promotion and three worked consumer plugins (`rigor-sinatra`, `rigor-devise`, `rigor-dry-struct`). Closes ROADMAP O2 at the WD13 floor.
566
+ - **Real-world Rails / Ruby survey** — fourteen projects swept; opt-in `rigor-activesupport-core-ext` RBS bundle delivers `−75 %` total diagnostics; built-in vendored gem RBS for six native-extension gems (`pg` / `mysql2` / `nokogiri` / `bcrypt` / `redis` / `idn-ruby`); Bundler-aware sig discovery; `RbsLoader#env` failure-memo (~550× speedup on a conflicting sig).
567
+ - **O4 Layer 3 target-project RBS source discovery (slices 1+2+3)** — `Gemfile.lock` parse + bundle-sig filter, `rbs_collection.lock.yaml` awareness, missing-gem `:info` diagnostic.
568
+ - **DEFAULT_LIBRARIES stdlib coverage expansion** — out-of-the-box RBS classes available 1,273 → 1,427 (+154); 31 additional stdlib libraries auto-load.
569
+ - **`is_a?(C)` lexical-nesting constant resolution** — predicate-narrowing now mirrors Ruby's `Module.nesting`-driven lookup.
577
570
 
578
571
  Twenty-four worked plugin examples now ship under
579
572
  [`examples/`](examples/) — see
580
573
  [`examples/README.md`](examples/README.md) for the comparison
581
- table. The current `[Unreleased]` cycle on `master` (release
582
- pending) also delivered the [ADR-16](docs/adr/16-macro-expansion.md)
583
- macro / DSL expansion substrate (four-tier declarative
584
- manifest contract + engine integration + Tier B/C precision
585
- promotion); see `CHANGELOG.md` `[Unreleased]` for the full
586
- landing notes.
574
+ table.
587
575
 
588
576
  ## Contributing
589
577
 
@@ -594,5 +582,3 @@ skill documentation contributors should know about.
594
582
  ## License
595
583
 
596
584
  Mozilla Public License Version 2.0. See [`LICENSE`](LICENSE).
597
- </content>
598
- </invoke>
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ # Binds one logical project path (the path the user is editing,
6
+ # e.g. `lib/foo.rb`) to a physical file containing the in-flight
7
+ # buffer bytes (e.g. `/tmp/9539itfeh2.rb`). When the runner /
8
+ # workers / pre-passes need to read source for the logical path,
9
+ # they read from the physical path instead; when they emit a
10
+ # `Diagnostic`, the path is the logical one so editors highlight
11
+ # the buffer the user is actually looking at.
12
+ #
13
+ # See `docs/design/20260516-editor-mode.md` for the design.
14
+ # The CLI surfaces this through paired `--tmp-file` /
15
+ # `--instead-of` flags on `rigor check` and `rigor type-of`;
16
+ # programmatic callers pass a `BufferBinding` to `Runner.new`.
17
+ BufferBinding = Data.define(:logical_path, :physical_path) do
18
+ # Returns the physical path to read bytes from when the caller
19
+ # is about to parse `path`. For non-logical paths returns the
20
+ # input unchanged. Cheap to call on every path; the binding is
21
+ # singular today (one buffer per run).
22
+ def resolve(path)
23
+ path == logical_path ? physical_path : path
24
+ end
25
+
26
+ # Returns the path the caller should report in user-facing
27
+ # output (diagnostics, run stats) when it currently holds the
28
+ # physical path. The inverse of `#resolve`. Non-physical paths
29
+ # pass through unchanged, so it is safe to stamp every
30
+ # outgoing path through this helper.
31
+ def display_path(path)
32
+ path == physical_path ? logical_path : path
33
+ end
34
+ end
35
+ end
36
+ end
@@ -944,8 +944,18 @@ module Rigor
944
944
  # / Constant / Tuple / HashShape; the wrapper exists so
945
945
  # the ivar rule can extend the envelope (or apply
946
946
  # different filters) without disturbing the call rules.
947
+ #
948
+ # `TrueClass` / `FalseClass` are both normalised to
949
+ # `"bool"` here so the common boolean-flag idiom
950
+ # (`@loaded = false` in `initialize` then `@loaded = true`
951
+ # on first work) doesn't fire the mismatch rule. A real
952
+ # `bool → String` drift still trips because the second
953
+ # write's `ivar_class_for` returns `"String"`.
947
954
  def ivar_class_for(type)
948
- concrete_class_name(type)
955
+ name = concrete_class_name(type)
956
+ return "bool" if %w[TrueClass FalseClass].include?(name)
957
+
958
+ name
949
959
  end
950
960
 
951
961
  def build_always_truthy_condition_diagnostic(path, predicate_node, polarity)
@@ -54,7 +54,7 @@ module Rigor
54
54
  )
55
55
  @resolved_gems = resolved_gems.freeze
56
56
  @unresolvable = unresolvable.freeze
57
- @method_catalog = method_catalog.freeze
57
+ @method_catalog = normalize_catalog(method_catalog).freeze
58
58
  @budget_exceeded = budget_exceeded.freeze
59
59
  @class_to_gem = class_to_gem.freeze
60
60
  @budget_overrun_strategy = budget_overrun_strategy
@@ -62,6 +62,19 @@ module Rigor
62
62
  freeze
63
63
  end
64
64
 
65
+ # Accepts both the post-heuristic `CatalogEntry` shape
66
+ # (the Walker's current output) and the legacy bare-Symbol
67
+ # `:instance` / `:singleton` shape (older tests + earlier
68
+ # in-tree callers). Symbol values are wrapped into
69
+ # `CatalogEntry(kind: value, return_type: nil)` so the
70
+ # downstream dispatcher always sees a single shape.
71
+ def normalize_catalog(catalog)
72
+ catalog.transform_values do |value|
73
+ value.is_a?(Symbol) ? Walker::CatalogEntry.new(kind: value) : value
74
+ end
75
+ end
76
+ private :normalize_catalog
77
+
65
78
  # @return [String, nil] the gem that owns `class_name`
66
79
  # (first-write-wins); `nil` when the class isn't in
67
80
  # any opt-in gem's catalog.
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+
5
+ require_relative "../../type"
6
+
7
+ module Rigor
8
+ module Analysis
9
+ module DependencySourceInference
10
+ # Walker enhancement (ADR-10 § "Open questions"): pulls a
11
+ # **heuristic** return type out of a method body's tail
12
+ # expression. The heuristic is intentionally narrow — only
13
+ # the trivially-decidable shapes contribute. Everything
14
+ # else returns `nil`, which the dispatcher consumes as
15
+ # "fall back to `Dynamic[top]`" (the pre-enhancement
16
+ # behaviour).
17
+ #
18
+ # The contract is a strict floor:
19
+ #
20
+ # - Last statement is a literal scalar (Integer / Float /
21
+ # Symbol / true / false / nil) → `Constant<value>`.
22
+ # - Last statement is a String literal → `Nominal[String]`
23
+ # (NOT `Constant<"x">`; a String literal is mutable under
24
+ # `# frozen_string_literal: false`, so the analyzer cannot
25
+ # claim object identity).
26
+ # - Last statement is an Array / Hash literal →
27
+ # `Nominal[Array]` / `Nominal[Hash]` (element-type
28
+ # inference stays deferred — too expensive for the
29
+ # heuristic tier).
30
+ # - Last statement is `self` → `nil` (the caller's receiver
31
+ # nominal would be more accurate but the walker doesn't
32
+ # carry the receiver context; deferred).
33
+ # - Anything else → `nil`.
34
+ #
35
+ # The dispatcher wraps the returned type in `Dynamic[T]`
36
+ # before returning to the user per ADR-10's `Dynamic`-origin
37
+ # contract. The wrapping is the dispatcher's responsibility,
38
+ # not the heuristic's.
39
+ module ReturnTypeHeuristic
40
+ module_function
41
+
42
+ # @param def_node [Prism::DefNode]
43
+ # @return [Rigor::Type, nil] heuristic return type, or
44
+ # `nil` when the body's tail expression doesn't match
45
+ # any of the recognised shapes.
46
+ def extract(def_node)
47
+ body = def_node.body
48
+ return nil if body.nil?
49
+
50
+ tail = tail_expression(body)
51
+ literal_return_type(tail)
52
+ end
53
+
54
+ # Extracts the last evaluated expression of the method
55
+ # body. A `Prism::DefNode`'s body is either a
56
+ # `StatementsNode` (multi-statement body) or a single
57
+ # expression node directly. We dig past `BeginNode` /
58
+ # rescue wrappers to the protected body's tail.
59
+ def tail_expression(node)
60
+ case node
61
+ when Prism::StatementsNode
62
+ tail_expression(node.body.last) unless node.body.empty?
63
+ when Prism::BeginNode
64
+ tail_expression(node.statements) if node.statements
65
+ when nil
66
+ nil
67
+ else
68
+ node
69
+ end
70
+ end
71
+ private_class_method :tail_expression
72
+
73
+ # The per-shape heuristic. Per the module docstring,
74
+ # immutable scalar literals fold to `Constant<value>`;
75
+ # mutable container literals (String, Array, Hash) fold
76
+ # to the appropriate Nominal; everything else returns
77
+ # nil.
78
+ def literal_return_type(node)
79
+ case node
80
+ when Prism::IntegerNode, Prism::FloatNode then Type::Combinator.constant_of(node.value)
81
+ when Prism::SymbolNode then symbol_constant(node)
82
+ when Prism::TrueNode then Type::Combinator.constant_of(true)
83
+ when Prism::FalseNode then Type::Combinator.constant_of(false)
84
+ when Prism::NilNode then Type::Combinator.constant_of(nil)
85
+ when Prism::StringNode then Type::Combinator.nominal_of("String")
86
+ when Prism::ArrayNode then Type::Combinator.nominal_of("Array")
87
+ when Prism::HashNode then Type::Combinator.nominal_of("Hash")
88
+ end
89
+ end
90
+ private_class_method :literal_return_type
91
+
92
+ # `Prism::SymbolNode#value` returns the Symbol's name as
93
+ # a String. We `.to_sym` it for the Constant carrier so
94
+ # `:foo` is `Constant<:foo>`, not `Constant<"foo">`.
95
+ def symbol_constant(node)
96
+ value = node.value
97
+ return nil if value.nil?
98
+
99
+ Type::Combinator.constant_of(value.to_sym)
100
+ end
101
+ private_class_method :symbol_constant
102
+ end
103
+ end
104
+ end
105
+ end
@@ -2,23 +2,26 @@
2
2
 
3
3
  require "prism"
4
4
 
5
+ require_relative "return_type_heuristic"
6
+
5
7
  module Rigor
6
8
  module Analysis
7
9
  module DependencySourceInference
8
10
  # Walks a resolved gem's `roots:` and collects the
9
- # `(class_name, method_name) → :instance | :singleton`
10
- # method catalog. The walker is the source of facts the
11
- # dispatcher tier (slice 2b-ii) consults to recognise a
11
+ # `(class_name, method_name) → CatalogEntry(kind,
12
+ # return_type)` method catalog. The walker is the source
13
+ # of facts the dispatcher tier consults to recognise a
12
14
  # method as defined by an opt-in gem and contribute a
13
- # `Type::Dynamic` return at the call site.
15
+ # `Type::Dynamic`-wrapped return at the call site.
14
16
  #
15
- # Slice 2b-i intentionally collects only the catalog, not
16
- # the inferred return type. The dispatcher tier returns
17
- # `Dynamic[top]` on a hit until slice 2b-ii wires return-
18
- # type inference; the visible payoff today is removing the
19
- # `call.undefined-method` diagnostic for opt-in gem methods
20
- # at receivers Rigor knows by `Nominal[T]` (typically
21
- # because the user authored an RBS skeleton).
17
+ # The dispatcher tier wraps every walker-contributed return
18
+ # in `Dynamic[T]` per ADR-10's gem-boundary contract. When
19
+ # the heuristic ({ReturnTypeHeuristic}) recognises the
20
+ # method body's tail expression, the dispatcher uses the
21
+ # heuristic's static facet; otherwise it falls back to
22
+ # `Dynamic[top]` (the pre-heuristic behaviour). The
23
+ # heuristic is intentionally narrow only literal-tail
24
+ # method bodies fold; everything else degrades silently.
22
25
  #
23
26
  # Hard exclusions are NOT user-configurable, per ADR-10
24
27
  # § "Hard exclusions": top-level `spec/`, `test/`, `bin/`,
@@ -45,6 +48,19 @@ module Rigor
45
48
  def truncated? = truncated
46
49
  end
47
50
 
51
+ # Per-method catalog entry. `kind` is `:instance` or
52
+ # `:singleton`; `return_type` is the
53
+ # {ReturnTypeHeuristic}-extracted static facet (a
54
+ # `Rigor::Type::*`) or `nil` when the heuristic declined.
55
+ # The dispatcher wraps a non-nil `return_type` in
56
+ # `Dynamic[T]`; a `nil` `return_type` falls back to
57
+ # `Dynamic[top]`.
58
+ class CatalogEntry < Data.define(:kind, :return_type)
59
+ def initialize(kind:, return_type: nil)
60
+ super
61
+ end
62
+ end
63
+
48
64
  # Sentinel for "no cap" — used by callers that don't
49
65
  # care about the budget (specs, tooling). Production
50
66
  # code MUST pass an integer.
@@ -175,7 +191,11 @@ module Rigor
175
191
 
176
192
  class_name = qualified_prefix.join("::")
177
193
  kind = node.receiver.is_a?(Prism::SelfNode) || in_singleton_class ? :singleton : :instance
178
- accumulator[[class_name, node.name]] ||= kind
194
+ key = [class_name, node.name]
195
+ return if accumulator.key?(key) # first walk wins
196
+
197
+ return_type = ReturnTypeHeuristic.extract(node)
198
+ accumulator[key] = CatalogEntry.new(kind: kind, return_type: return_type)
179
199
  end
180
200
 
181
201
  # Resolves a `Prism::ConstantPathNode` /
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ # Frozen snapshot of the project-wide state {Runner} consumes
6
+ # before per-file analysis fires: the loaded plugin registry
7
+ # (with `#prepare` already invoked), the dependency-source
8
+ # index, the synthetic-method and project-patched indexes
9
+ # produced by the pre-pass scanners, and the diagnostics
10
+ # those passes emitted.
11
+ #
12
+ # Owners (`Rigor::LanguageServer::ProjectContext`, future
13
+ # editor / sig-gen integrations) build a ProjectScan once
14
+ # per project-state generation via
15
+ # `Runner#prepare_project_scan` and pass it to
16
+ # `Runner.new(prebuilt: ...)` so per-buffer publishes skip
17
+ # the scanner walks and `#prepare` re-runs. When watched
18
+ # project files change, the owner discards the ProjectScan
19
+ # and a fresh one builds on next read.
20
+ #
21
+ # Editor mode v1 contract reminder: scanners observe the
22
+ # bytes that were on disk at scan time, NOT the in-flight
23
+ # buffer. Edits to a file that itself declares synthetic
24
+ # methods (or `pre_eval:`-listed patches) are NOT visible
25
+ # until the owner invalidates the scan — typically via
26
+ # `workspace/didChangeWatchedFiles`. This is the same
27
+ # trade-off the LSP made when slice 7 cached only the
28
+ # `Environment`; extending the cache to the pre-pass
29
+ # outputs preserves the contract.
30
+ ProjectScan = Data.define(
31
+ :plugin_registry,
32
+ :dependency_source_index,
33
+ :synthetic_method_index,
34
+ :project_patched_methods,
35
+ :plugin_prepare_diagnostics,
36
+ :pre_eval_diagnostics
37
+ )
38
+ end
39
+ end