rigortype 0.1.3 → 0.1.5

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +154 -33
  3. data/lib/rigor/analysis/check_rules.rb +10 -18
  4. data/lib/rigor/analysis/dependency_source_inference/boundary_cross_reporter.rb +75 -0
  5. data/lib/rigor/analysis/dependency_source_inference/builder.rb +47 -21
  6. data/lib/rigor/analysis/dependency_source_inference/gem_resolver.rb +1 -1
  7. data/lib/rigor/analysis/dependency_source_inference/index.rb +32 -3
  8. data/lib/rigor/analysis/dependency_source_inference/walker.rb +1 -1
  9. data/lib/rigor/analysis/dependency_source_inference.rb +1 -0
  10. data/lib/rigor/analysis/diagnostic.rb +0 -2
  11. data/lib/rigor/analysis/fact_store.rb +26 -6
  12. data/lib/rigor/analysis/result.rb +11 -3
  13. data/lib/rigor/analysis/rule_catalog.rb +2 -2
  14. data/lib/rigor/analysis/run_stats.rb +193 -0
  15. data/lib/rigor/analysis/runner.rb +498 -12
  16. data/lib/rigor/analysis/worker_session.rb +327 -0
  17. data/lib/rigor/builtins/imported_refinements.rb +364 -55
  18. data/lib/rigor/builtins/regex_refinement.rb +17 -12
  19. data/lib/rigor/cache/descriptor.rb +1 -1
  20. data/lib/rigor/cache/rbs_descriptor.rb +3 -1
  21. data/lib/rigor/cache/store.rb +39 -6
  22. data/lib/rigor/cli/diff_command.rb +1 -1
  23. data/lib/rigor/cli/sig_gen_command.rb +173 -0
  24. data/lib/rigor/cli/type_of_command.rb +1 -1
  25. data/lib/rigor/cli/type_scan_renderer.rb +1 -1
  26. data/lib/rigor/cli/type_scan_report.rb +2 -2
  27. data/lib/rigor/cli.rb +61 -3
  28. data/lib/rigor/configuration/dependencies.rb +2 -2
  29. data/lib/rigor/configuration.rb +131 -6
  30. data/lib/rigor/environment/bundle_sig_discovery.rb +198 -0
  31. data/lib/rigor/environment/class_registry.rb +12 -3
  32. data/lib/rigor/environment/lockfile_resolver.rb +125 -0
  33. data/lib/rigor/environment/rbs_collection_discovery.rb +126 -0
  34. data/lib/rigor/environment/rbs_coverage_report.rb +112 -0
  35. data/lib/rigor/environment/rbs_loader.rb +194 -6
  36. data/lib/rigor/environment/reflection.rb +152 -0
  37. data/lib/rigor/environment.rb +109 -6
  38. data/lib/rigor/flow_contribution/conflict.rb +2 -2
  39. data/lib/rigor/flow_contribution/element.rb +1 -1
  40. data/lib/rigor/flow_contribution/fact.rb +1 -1
  41. data/lib/rigor/flow_contribution/merge_result.rb +1 -1
  42. data/lib/rigor/flow_contribution/merger.rb +3 -3
  43. data/lib/rigor/flow_contribution.rb +2 -2
  44. data/lib/rigor/inference/acceptance.rb +35 -1
  45. data/lib/rigor/inference/block_parameter_binder.rb +0 -2
  46. data/lib/rigor/inference/builtins/method_catalog.rb +12 -5
  47. data/lib/rigor/inference/builtins/numeric_catalog.rb +15 -4
  48. data/lib/rigor/inference/coverage_scanner.rb +1 -1
  49. data/lib/rigor/inference/expression_typer.rb +77 -11
  50. data/lib/rigor/inference/fallback.rb +1 -1
  51. data/lib/rigor/inference/macro_block_self_type.rb +96 -0
  52. data/lib/rigor/inference/method_dispatcher/block_folding.rb +3 -5
  53. data/lib/rigor/inference/method_dispatcher/constant_folding.rb +29 -41
  54. data/lib/rigor/inference/method_dispatcher/iterator_dispatch.rb +1 -3
  55. data/lib/rigor/inference/method_dispatcher/kernel_dispatch.rb +4 -4
  56. data/lib/rigor/inference/method_dispatcher/literal_string_folding.rb +1 -1
  57. data/lib/rigor/inference/method_dispatcher/method_folding.rb +135 -0
  58. data/lib/rigor/inference/method_dispatcher/overload_selector.rb +7 -12
  59. data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +27 -11
  60. data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +46 -44
  61. data/lib/rigor/inference/method_dispatcher.rb +274 -5
  62. data/lib/rigor/inference/method_parameter_binder.rb +22 -14
  63. data/lib/rigor/inference/narrowing.rb +129 -12
  64. data/lib/rigor/inference/rbs_type_translator.rb +0 -2
  65. data/lib/rigor/inference/scope_indexer.rb +14 -9
  66. data/lib/rigor/inference/statement_evaluator.rb +7 -7
  67. data/lib/rigor/inference/synthetic_method.rb +86 -0
  68. data/lib/rigor/inference/synthetic_method_index.rb +82 -0
  69. data/lib/rigor/inference/synthetic_method_scanner.rb +521 -0
  70. data/lib/rigor/plugin/blueprint.rb +60 -0
  71. data/lib/rigor/plugin/io_boundary.rb +0 -2
  72. data/lib/rigor/plugin/loader.rb +5 -3
  73. data/lib/rigor/plugin/macro/block_as_method.rb +131 -0
  74. data/lib/rigor/plugin/macro/external_file.rb +143 -0
  75. data/lib/rigor/plugin/macro/heredoc_template.rb +201 -0
  76. data/lib/rigor/plugin/macro/trait_registry.rb +198 -0
  77. data/lib/rigor/plugin/macro.rb +31 -0
  78. data/lib/rigor/plugin/manifest.rb +102 -10
  79. data/lib/rigor/plugin/registry.rb +43 -2
  80. data/lib/rigor/plugin/services.rb +1 -1
  81. data/lib/rigor/plugin/type_node_resolver.rb +52 -0
  82. data/lib/rigor/plugin.rb +2 -0
  83. data/lib/rigor/rbs_extended/reporter.rb +91 -0
  84. data/lib/rigor/rbs_extended.rb +131 -32
  85. data/lib/rigor/scope.rb +25 -8
  86. data/lib/rigor/sig_gen/classification.rb +36 -0
  87. data/lib/rigor/sig_gen/generator.rb +1048 -0
  88. data/lib/rigor/sig_gen/layout_index.rb +108 -0
  89. data/lib/rigor/sig_gen/method_candidate.rb +62 -0
  90. data/lib/rigor/sig_gen/observation_collector.rb +391 -0
  91. data/lib/rigor/sig_gen/observed_call.rb +62 -0
  92. data/lib/rigor/sig_gen/path_mapper.rb +116 -0
  93. data/lib/rigor/sig_gen/renderer.rb +157 -0
  94. data/lib/rigor/sig_gen/type_elaborator.rb +92 -0
  95. data/lib/rigor/sig_gen/write_result.rb +48 -0
  96. data/lib/rigor/sig_gen/writer.rb +530 -0
  97. data/lib/rigor/sig_gen.rb +25 -0
  98. data/lib/rigor/trinary.rb +15 -11
  99. data/lib/rigor/type/bot.rb +6 -3
  100. data/lib/rigor/type/bound_method.rb +79 -0
  101. data/lib/rigor/type/combinator.rb +207 -3
  102. data/lib/rigor/type/constant.rb +13 -0
  103. data/lib/rigor/type/hash_shape.rb +0 -2
  104. data/lib/rigor/type/integer_range.rb +7 -7
  105. data/lib/rigor/type/refined.rb +18 -12
  106. data/lib/rigor/type/top.rb +4 -3
  107. data/lib/rigor/type/union.rb +20 -1
  108. data/lib/rigor/type.rb +1 -0
  109. data/lib/rigor/type_node/generic.rb +68 -0
  110. data/lib/rigor/type_node/identifier.rb +38 -0
  111. data/lib/rigor/type_node/indexed_access.rb +41 -0
  112. data/lib/rigor/type_node/integer_literal.rb +29 -0
  113. data/lib/rigor/type_node/name_scope.rb +52 -0
  114. data/lib/rigor/type_node/resolver_chain.rb +56 -0
  115. data/lib/rigor/type_node/string_literal.rb +32 -0
  116. data/lib/rigor/type_node/symbol_literal.rb +28 -0
  117. data/lib/rigor/type_node/union.rb +42 -0
  118. data/lib/rigor/type_node.rb +29 -0
  119. data/lib/rigor/version.rb +1 -1
  120. data/lib/rigor.rb +2 -0
  121. data/sig/rigor/analysis/check_rules/always_truthy_condition_collector.rbs +10 -0
  122. data/sig/rigor/analysis/check_rules/dead_assignment_collector.rbs +10 -0
  123. data/sig/rigor/analysis/dependency_source_inference/gem_resolver.rbs +25 -0
  124. data/sig/rigor/analysis/dependency_source_inference/index.rbs +9 -0
  125. data/sig/rigor/cli/diff_command.rbs +4 -0
  126. data/sig/rigor/cli/explain_command.rbs +4 -0
  127. data/sig/rigor/cli/sig_gen_command.rbs +4 -0
  128. data/sig/rigor/cli/type_scan_command.rbs +3 -0
  129. data/sig/rigor/environment.rbs +8 -2
  130. data/sig/rigor/inference/builtins/method_catalog.rbs +4 -0
  131. data/sig/rigor/inference/builtins/numeric_catalog.rbs +3 -0
  132. data/sig/rigor/inference/builtins.rbs +2 -0
  133. data/sig/rigor/plugin/access_denied_error.rbs +3 -0
  134. data/sig/rigor/plugin/base.rbs +6 -0
  135. data/sig/rigor/plugin/blueprint.rbs +7 -0
  136. data/sig/rigor/plugin/fact_store.rbs +11 -0
  137. data/sig/rigor/plugin/io_boundary.rbs +4 -0
  138. data/sig/rigor/plugin/load_error.rbs +6 -0
  139. data/sig/rigor/plugin/loader.rbs +20 -0
  140. data/sig/rigor/plugin/manifest.rbs +9 -0
  141. data/sig/rigor/plugin/registry.rbs +16 -0
  142. data/sig/rigor/plugin/services.rbs +3 -0
  143. data/sig/rigor/plugin/trust_policy.rbs +4 -0
  144. data/sig/rigor/plugin/type_node_resolver.rbs +3 -0
  145. data/sig/rigor/plugin.rbs +8 -0
  146. data/sig/rigor/scope.rbs +4 -2
  147. data/sig/rigor/type.rbs +28 -6
  148. data/sig/rigor.rbs +35 -2
  149. metadata +90 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3542aa2842a06783d02f8f35b7f31247eb8d10dce91214b31364839ad7087809
4
- data.tar.gz: 6beca5ec330dab3264cb64a110bd4e6a790e16a92c5f1cf10383799b988d2bb9
3
+ metadata.gz: b5960ec17b35768103e97d752f8cc6fd78fcb3f12e12fc43dfa41be07ec5317b
4
+ data.tar.gz: e79c9b25c973c8938e9b2f0a2741cca5195342619827b320ca521ec09e54321e
5
5
  SHA512:
6
- metadata.gz: 33d98371534cd4d193b39afe08dc5b442cd90e0909388a9e9a20413d8ae766665d8bb7a8573bb2236997a72ce178e35c7d2c62c64ceac804f1a31bf88b079950
7
- data.tar.gz: 0fca38a07730117fa58b42d8c14642e86540a492ce0513c4612dbf58049600bb8adc224fc32cdfc518ba581a870c478b8dfbbc02ce43bf78eb251c3d1dd771db
6
+ metadata.gz: af1e033a25410c0f87943f12d43ab18a3a0d2a79c01307c2117c2fc15be4c9db3cb28e6fec10ce598ef6a5bfa063227f280c023a0f7e9025b06c69946df4654d
7
+ data.tar.gz: 351b3275dd35f37a11d30a696627e23a6cdca31bfb94fa3eacae762d2de624e4a914c6f8f4eebdd7df0bd17fd9fac13fe55ac434957509b742771a00d352a981
data/README.md CHANGED
@@ -79,6 +79,12 @@ bundle exec rigor type-of lib/foo.rb:10:5
79
79
  # Report Scope#type_of coverage across a tree (handy when
80
80
  # diagnosing why a particular call site reads as `untyped`).
81
81
  bundle exec rigor type-scan lib
82
+
83
+ # Emit RBS skeletons from inference results — review with
84
+ # `--diff`, write to `sig/` with `--write`. ADR-14 sig-gen.
85
+ bundle exec rigor sig-gen --print lib/foo.rb
86
+ bundle exec rigor sig-gen --diff lib/foo.rb
87
+ bundle exec rigor sig-gen --write lib/foo.rb
82
88
  ```
83
89
 
84
90
  ### Sample output
@@ -145,6 +151,7 @@ out of the box, without you writing a single annotation.
145
151
  | **Intersection** (`Type::Intersection`) | Composition of multiple refinements | `non-empty-lowercase-string = non-empty-string ∩ lowercase-string` |
146
152
  | **Tuple / HashShape** | Heterogeneous arrays / known-key hashes that carry per-position / per-key types | `[1, "two", :three]` types as `Tuple[Constant<1>, Constant<"two">, Constant<:three>]`; `{name: "Alice", age: 30}` as `HashShape{name: Constant<"Alice">, age: Constant<30>}` |
147
153
  | **Union** (`Type::Union`) | "One of these literal values" — finite enums Rigor can enumerate | `Constant<:zero> \| Constant<:small> \| Constant<:large>` |
154
+ | **`Method` binding** (`Type::BoundMethod`) | The receiver / method-name pair `Object#method(:sym)` produces, so `.call` / `.()` / `[]` recover the precise backing dispatch | `"1".method(:to_i).call` resolves to `Constant<1>` instead of `untyped` |
148
155
  | **`Dynamic[T]`** | The gradual carrier — wraps a static facet with a "could be anything" admission | `Dynamic[Top]` is the conservative fallback Rigor uses when it cannot prove a narrower type |
149
156
 
150
157
  Each refinement / range / literal carrier **erases to its base
@@ -188,6 +195,12 @@ label = case n
188
195
  label # Constant<:zero> | Constant<:small>
189
196
  # | Constant<:large>
190
197
 
198
+ # Method bindings keep their receiver — `.method(:sym).call`
199
+ # round-trips through the original dispatch.
200
+ [:to_i, :to_f, :to_sym].map { |m| "1".method(m).call }
201
+ # Tuple[Constant<1>, Constant<1.0>, Constant<:"1">]
202
+ # — per-element fold + BoundMethod backward fold
203
+
191
204
  # RBS::Extended directives let you tighten beyond what RBS expresses.
192
205
  class Slug
193
206
  %a{rigor:v1:return: non-empty-string}
@@ -353,6 +366,10 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
353
366
  optional policies, per-element block fold over
354
367
  `map`, `select`, `filter_map`, `flat_map`, `find` /
355
368
  `find_index`, `count`, `any?` / `all?` / `none?`, `zip`.
369
+ `&:symbol` block-pass on these methods is treated as
370
+ `{ |x| x.symbol }` and dispatches against the element type
371
+ so `Hash#transform_values(&:freeze)` returns `Hash[K, V]`
372
+ instead of `Enumerator[...]`.
356
373
  - **Constant folding** — aggressive arithmetic / string /
357
374
  Symbol / Tuple-shaped `divmod` folding, cartesian fold over
358
375
  `Union[Constant…]`, integer-range arithmetic
@@ -370,26 +387,51 @@ sees `id` as `non-empty-string` (so `id.empty?` reduces to
370
387
  - **Refinement carriers** — `Type::Difference`,
371
388
  `Type::Refined`, `Type::Intersection` provide the
372
389
  imported-built-in catalogue end-to-end through
373
- `Builtins::ImportedRefinements`.
390
+ `Builtins::ImportedRefinements`. The parser accepts Symbol
391
+ / String literals and `|`-unions at type-arg position
392
+ (`pick_of[Shape, :a | :b]`, `Pick[T, "name" | "email"]`).
393
+ - **`Method` carrier (`Type::BoundMethod`)** —
394
+ `Object#method(:sym)` lifts into a binding carrier so
395
+ `.call` / `.()` / `[]` recover the precise dispatch
396
+ (`"1".method(:to_i).call` resolves to `Constant<1>`).
397
+ Reflective Method members (`#owner` / `#name` / `#arity`)
398
+ still resolve via the Method RBS sig.
374
399
  - **`RBS::Extended` directive routes** — `return:`, `param:`
375
400
  (call-site + body-side), `assert:` /
376
401
  `predicate-if-(true|false)` accept refinement payloads, and
377
402
  roll up into a single `Rigor::FlowContribution` bundle per
378
403
  method (the v0.1.0 plugin contribution merger reads bundles
379
404
  directly).
405
+ - **Opt-in gem-source inference (ADR-10)** — gems listed under
406
+ `dependencies.source_inference:` have their `lib/` walked.
407
+ Per-gem budget, per-gem-version cache slice,
408
+ `dynamic.dependency-source.*` diagnostic family covering
409
+ gem-not-found / budget-exceeded / config-conflict /
410
+ boundary-cross (the last surfaces RBS+gem-source overlap
411
+ on `mode: :full` gems for audit).
380
412
 
381
413
  The full per-release surface lives in
382
414
  [`CHANGELOG.md`](CHANGELOG.md). The internal contracts the
383
415
  analyzer guarantees live under
384
416
  [`docs/internal-spec/`](docs/internal-spec/).
385
417
 
386
- ## Plugins (v0.1.0)
418
+ ## Plugins
419
+
420
+ `v0.1.0` introduced the extension API; `v0.1.x` rounds it out
421
+ with the [ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin
422
+ fact channel (one plugin publishes a fact like `:model_index`,
423
+ another consumes it), [ADR-11](docs/adr/11-sorbet-input-adapter.md)
424
+ Sorbet ingestion, [ADR-13](docs/adr/13-typenode-resolver-plugin.md)
425
+ plugin-supplied type-vocabulary resolvers, and
426
+ [ADR-16](docs/adr/16-macro-expansion.md) macro / DSL expansion
427
+ substrate (declarative Tier A block-as-method / Tier B
428
+ trait-inlining-registry / Tier C heredoc-template / Tier D
429
+ external-file inclusion). **Twenty-four worked examples** ship
430
+ under [`examples/`](examples/) — each is a fully-shaped plugin
431
+ gem with a runnable demo and an end-to-end integration spec.
387
432
 
388
- `v0.1.0` adds an extension API so projects can teach Rigor about
389
- their own DSLs. Seven worked examples ship under
390
- [`examples/`](examples/) — each is a fully-shaped plugin gem
391
- with a runnable demo and an end-to-end integration spec, and
392
- each spotlights a different facet of the plugin contract:
433
+ **Plugin-contract teaching examples** (focus on a single
434
+ extension-point):
393
435
 
394
436
  - [`rigor-deprecations`](examples/rigor-deprecations/) —
395
437
  smallest possible plugin (~80 lines); config-driven rules.
@@ -403,21 +445,64 @@ each spotlights a different facet of the plugin contract:
403
445
  - [`rigor-units`](examples/rigor-units/) — local-variable flow
404
446
  tracking through arithmetic.
405
447
  - [`rigor-routes`](examples/rigor-routes/) — `Plugin::IoBoundary`
406
- reads under `TrustPolicy` plus cache producers (slice 2 +
407
- slice 6).
408
- - [`rigor-activerecord`](examples/rigor-activerecord/)
409
- validates `Model.find` / `.find_by` / `.where` against
410
- `db/schema.rb` and discovered AR model classes; combines
411
- DSL interpretation, multi-file `IoBoundary`, and chained
412
- cache producers the most architecturally complete example.
448
+ reads under `TrustPolicy` plus cache producers.
449
+ - [`rigor-typescript-utility-types`](examples/rigor-typescript-utility-types/)
450
+ `Plugin::TypeNodeResolver` chain wiring TS-canonical names
451
+ (`Pick` / `Omit` / `Partial` / `Required` / `Readonly`) onto
452
+ Rigor's shape-projection type functions.
453
+
454
+ **Macro expansion substrate consumers** (ADR-16 declarative
455
+ manifest entries, no walker code):
456
+
457
+ - [`rigor-sinatra`](examples/rigor-sinatra/) — **Tier A**
458
+ block-as-method. Recognises Sinatra's nine class-level HTTP
459
+ verb methods and narrows the route block's `self_type` so
460
+ bare `params` / `redirect` / `halt` resolve through
461
+ `Sinatra::Base`'s RBS.
462
+ - [`rigor-dry-struct`](examples/rigor-dry-struct/) — **Tier C**
463
+ heredoc-template. Synthesises a reader on every `Dry::Struct`
464
+ subclass for each `attribute :name, T` / `attribute? :name, T`
465
+ call.
466
+ - [`rigor-devise`](examples/rigor-devise/) — **Tier B**
467
+ trait-inlining registry mirroring `lib/devise/modules.rb`.
468
+ Each `devise :strategy_a, :strategy_b` call explodes the
469
+ included module's RBS instance methods onto the calling model
470
+ class (Devise's `user.valid_password?` returns the module's
471
+ authored `bool`).
472
+
473
+ **Rails ecosystem plugins** (Tier 1 + Tier 2 + Tier 3 + Sorbet):
474
+
475
+ - Tier 1: [`rigor-rails-routes`](examples/rigor-rails-routes/),
476
+ [`rigor-rails-i18n`](examples/rigor-rails-i18n/),
477
+ [`rigor-actionmailer`](examples/rigor-actionmailer/),
478
+ [`rigor-activejob`](examples/rigor-activejob/).
479
+ - Tier 2: [`rigor-actionpack`](examples/rigor-actionpack/)
480
+ (4 phases — routes / filters / renders / strong-params),
481
+ [`rigor-factorybot`](examples/rigor-factorybot/),
482
+ [`rigor-activerecord`](examples/rigor-activerecord/) —
483
+ publishes `:model_index` via ADR-9 for the other two
484
+ to consume.
485
+ - Tier 3: [`rigor-pundit`](examples/rigor-pundit/),
486
+ [`rigor-sidekiq`](examples/rigor-sidekiq/),
487
+ [`rigor-rspec`](examples/rigor-rspec/),
488
+ [`rigor-actioncable`](examples/rigor-actioncable/).
489
+ - Parallel: [`rigor-sorbet`](examples/rigor-sorbet/) — ingests
490
+ Sorbet `sig` / `T.let` / `T.cast` / `T.must` / `T.bind` /
491
+ `T.assert_type!` / `T.reveal_type` / `T.absurd` and RBI
492
+ files as type sources.
413
493
 
414
494
  [`examples/README.md`](examples/README.md) is the plugin
415
495
  authoring landing page — comparison table, recommended reading
416
496
  order, and the architectural map of which surface each example
417
497
  exercises. The binding contract for the plugin API lives in
418
- [`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md)
419
- and the slice-by-slice normative specs under
420
- [`docs/internal-spec/plugin*.md`](docs/internal-spec/).
498
+ [`docs/adr/2-extension-api.md`](docs/adr/2-extension-api.md);
499
+ the slice-by-slice normative specs are under
500
+ [`docs/internal-spec/plugin*.md`](docs/internal-spec/); the
501
+ sibling ADRs that extend it ride the same surface
502
+ ([ADR-9](docs/adr/9-cross-plugin-api.md) cross-plugin facts,
503
+ [ADR-11](docs/adr/11-sorbet-input-adapter.md) Sorbet adapter,
504
+ [ADR-13](docs/adr/13-typenode-resolver-plugin.md) TypeNode
505
+ resolver).
421
506
 
422
507
  ## Configuration
423
508
 
@@ -447,22 +532,58 @@ Common knobs the file exposes:
447
532
 
448
533
  ## Status
449
534
 
450
- Current released version: **`v0.0.8`** (the eighth preview).
451
- The analyzer is usable on real Ruby code today but the rule
452
- catalogue is deliberately narrow — Rigor's stance is to surface
453
- zero false positives while the inference surface stabilises.
454
- The roadmap is tracked in
455
- [`docs/MILESTONES.md`](docs/MILESTONES.md); release-by-release
456
- detail lives in [`CHANGELOG.md`](CHANGELOG.md).
457
-
458
- `v0.0.9` is the active development cluster on `master` and
459
- covers the persistent cache infrastructure (`.rigor/cache/`,
460
- `--cache-stats`, `--clear-cache`, `--no-cache`),
461
- paired-complement Refined narrowing, `literal-string` flow
462
- tracking, the `Rigor::FlowContribution` bundle struct, and
463
- six additional built-in catalogues (Random, Struct, Encoding,
464
- Regexp + MatchData, Proc / Method / UnboundMethod, Exception).
465
- The next release after `0.0.9` will be `0.1.0`.
535
+ Current released version: **`v0.1.4`**. The analyzer is usable
536
+ on real Ruby code today; the rule catalogue is deliberately
537
+ narrow — Rigor's stance is to surface zero false positives
538
+ while the inference surface stabilises. Forward-looking commitments
539
+ (in-flight cycle + queued work) live in
540
+ [`docs/ROADMAP.md`](docs/ROADMAP.md); the release-by-release
541
+ "what shipped" record is [`CHANGELOG.md`](CHANGELOG.md).
542
+
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.
577
+
578
+ Twenty-four worked plugin examples now ship under
579
+ [`examples/`](examples/) — see
580
+ [`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.
466
587
 
467
588
  ## Contributing
468
589
 
@@ -262,7 +262,7 @@ module Rigor
262
262
  # errors, internal analyzer errors) are NEVER
263
263
  # suppressed — they represent failures the user cannot
264
264
  # silence away.
265
- def filter_suppressed(diagnostics, comments:, disabled_rules:) # rubocop:disable Metrics/CyclomaticComplexity
265
+ def filter_suppressed(diagnostics, comments:, disabled_rules:)
266
266
  line_suppressions, file_suppressions = parse_suppression_comments(comments)
267
267
  disabled = expand_rule_tokens(disabled_rules)
268
268
 
@@ -329,7 +329,7 @@ module Rigor
329
329
  class << self
330
330
  private
331
331
 
332
- def undefined_method_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
332
+ def undefined_method_diagnostic(path, call_node, scope_index)
333
333
  return nil if call_node.receiver.nil?
334
334
 
335
335
  scope = scope_index[call_node]
@@ -429,7 +429,7 @@ module Rigor
429
429
  # by `undefined_method_diagnostic`; it returns nil
430
430
  # when the call's receiver / RBS coverage / call shape
431
431
  # disqualifies the rule.
432
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
432
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
433
433
  def wrong_arity_diagnostic(path, call_node, scope_index)
434
434
  return nil if call_node.receiver.nil?
435
435
  return nil unless plain_positional_call?(call_node)
@@ -459,7 +459,7 @@ module Rigor
459
459
 
460
460
  build_arity_diagnostic(path, call_node, class_name, min, max, actual)
461
461
  end
462
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
462
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
463
463
 
464
464
  def plain_positional_call?(call_node)
465
465
  arguments = call_node.arguments
@@ -534,7 +534,7 @@ module Rigor
534
534
  # and union receivers where every member already
535
535
  # disqualifies the call (avoid duplicating the
536
536
  # undefined-method diagnostic).
537
- def nil_receiver_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
537
+ def nil_receiver_diagnostic(path, call_node, scope_index)
538
538
  return nil if call_node.receiver.nil?
539
539
  # Safe-navigation calls (`recv&.method`) already
540
540
  # short-circuit on nil at runtime, so a nil-bearing
@@ -615,7 +615,7 @@ module Rigor
615
615
  # The diagnostic does NOT count toward `Result#error_count`
616
616
  # so a fixture peppered with `dump_type` calls still
617
617
  # passes `rigor check`.
618
- def dump_type_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity
618
+ def dump_type_diagnostic(path, call_node, scope_index)
619
619
  return nil unless rigor_testing_call?(call_node, :dump_type)
620
620
  return nil if call_node.arguments.nil? || call_node.arguments.arguments.empty?
621
621
 
@@ -644,7 +644,7 @@ module Rigor
644
644
  # is emitted; matching calls produce no output. This
645
645
  # lets a fixture document its expected types inline:
646
646
  # subsequent `rigor check` runs flag any drift.
647
- def assert_type_diagnostic(path, call_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
647
+ def assert_type_diagnostic(path, call_node, scope_index)
648
648
  return nil unless rigor_testing_call?(call_node, :assert_type)
649
649
  return nil if call_node.arguments.nil? || call_node.arguments.arguments.size < 2
650
650
 
@@ -972,7 +972,6 @@ module Rigor
972
972
  )
973
973
  end
974
974
 
975
- # rubocop:disable Metrics/ParameterLists
976
975
  def build_ivar_write_mismatch_diagnostic(path, node, class_name, ivar_name, first_class, other_class)
977
976
  location = node.name_loc || node.location
978
977
  Diagnostic.new(
@@ -985,7 +984,6 @@ module Rigor
985
984
  severity: :error
986
985
  )
987
986
  end
988
- # rubocop:enable Metrics/ParameterLists
989
987
 
990
988
  # Returns the dead-branch node for a literal-predicate
991
989
  # if/unless, or nil when no observable branch is dead.
@@ -1034,7 +1032,6 @@ module Rigor
1034
1032
  # (no splat / kw / block-pass / forwarded).
1035
1033
  # - Per-argument: skip when EITHER side is `Dynamic`
1036
1034
  # (the call cannot be statically refuted).
1037
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1038
1035
  def argument_type_diagnostic(path, call_node, scope_index)
1039
1036
  return nil if call_node.receiver.nil?
1040
1037
  return nil unless plain_positional_call?(call_node)
@@ -1059,15 +1056,13 @@ module Rigor
1059
1056
  return nil if method_def.nil? || method_def == true
1060
1057
  return nil unless method_def.method_types.size == 1
1061
1058
 
1062
- param_overrides = Rigor::RbsExtended.param_type_override_map(method_def)
1059
+ param_overrides = Rigor::RbsExtended.param_type_override_map(method_def, environment: scope.environment)
1063
1060
  mismatch = first_argument_mismatch(method_def.method_types.first, call_node, scope, param_overrides)
1064
1061
  return nil if mismatch.nil?
1065
1062
 
1066
1063
  build_argument_type_diagnostic(path, call_node, class_name, mismatch)
1067
1064
  end
1068
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1069
1065
 
1070
- # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1071
1066
  def first_argument_mismatch(method_type, call_node, scope, param_overrides)
1072
1067
  function = method_type.type
1073
1068
  return nil unless argument_check_eligible?(function)
@@ -1094,7 +1089,6 @@ module Rigor
1094
1089
  end
1095
1090
  nil
1096
1091
  end
1097
- # rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize
1098
1092
 
1099
1093
  def argument_check_eligible?(function)
1100
1094
  # See `arity_eligible?`: `UntypedFunction` lacks
@@ -1132,7 +1126,6 @@ module Rigor
1132
1126
  )
1133
1127
  end
1134
1128
 
1135
- # rubocop:disable Metrics/ParameterLists
1136
1129
  def build_arity_diagnostic(path, call_node, class_name, min, max, actual)
1137
1130
  location = call_node.message_loc || call_node.location
1138
1131
  range = min == max ? min.to_s : "#{min}..#{max}"
@@ -1147,7 +1140,6 @@ module Rigor
1147
1140
  severity: :error
1148
1141
  )
1149
1142
  end
1150
- # rubocop:enable Metrics/ParameterLists
1151
1143
 
1152
1144
  def build_undefined_method_diagnostic(path, call_node, receiver_type)
1153
1145
  location = call_node.message_loc || call_node.location
@@ -1186,7 +1178,7 @@ module Rigor
1186
1178
  # :maybe → emit at :warning. Promoted to :error
1187
1179
  # under `severity_profile: strict` per
1188
1180
  # ADR-8 § "Severity profile".
1189
- def return_type_mismatch_diagnostic(path, def_node, scope_index) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
1181
+ def return_type_mismatch_diagnostic(path, def_node, scope_index)
1190
1182
  return nil if def_node.body.nil?
1191
1183
 
1192
1184
  last_expr = body_last_expression(def_node.body)
@@ -1255,7 +1247,7 @@ module Rigor
1255
1247
  end
1256
1248
  return nil if method_def.nil?
1257
1249
 
1258
- override = Rigor::RbsExtended.read_return_type_override(method_def)
1250
+ override = Rigor::RbsExtended.read_return_type_override(method_def, environment: scope.environment)
1259
1251
  return override if override
1260
1252
 
1261
1253
  declared_return_union(method_def, scope.environment)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rigor
4
+ module Analysis
5
+ module DependencySourceInference
6
+ # ADR-10 slice 5c — per-run accumulator for the
7
+ # `dynamic.dependency-source.boundary-cross` `:info`
8
+ # diagnostic.
9
+ #
10
+ # The diagnostic fires when both an authoritative source
11
+ # (RBS today; plugins later) AND a `mode: :full` opt-in
12
+ # gem's source catalog resolve the same `(class_name,
13
+ # method_name)`. The dispatcher takes the authoritative
14
+ # source's answer (per ADR-10's tier order), but records
15
+ # the boundary crossing so the user can audit whether RBS
16
+ # and the gem source have drifted.
17
+ #
18
+ # The accumulator deduplicates per `(class_name,
19
+ # method_name, gem_name)` so a method called from many
20
+ # files yields one diagnostic.
21
+ #
22
+ # Used in the same pattern as
23
+ # {Rigor::RbsExtended::Reporter}: the dispatcher writes
24
+ # events into the per-run instance; {Rigor::Analysis::Runner}
25
+ # drains it at end-of-run into a flat
26
+ # `Rigor::Analysis::Diagnostic` list.
27
+ class BoundaryCrossReporter
28
+ Entry = Data.define(:class_name, :method_name, :gem_name, :rbs_display)
29
+
30
+ def initialize
31
+ @entries = []
32
+ @mutex = Mutex.new
33
+ end
34
+
35
+ # @return [Array<Entry>] frozen snapshot of the recorded
36
+ # boundary-cross events. Each entry is a Data with
37
+ # `class_name` (String), `method_name` (Symbol),
38
+ # `gem_name` (String), and `rbs_display` (String —
39
+ # the authoritative-side type's human-facing form,
40
+ # embedded into the diagnostic message).
41
+ def entries
42
+ @mutex.synchronize { @entries.dup.freeze }
43
+ end
44
+
45
+ def empty?
46
+ @mutex.synchronize { @entries.empty? }
47
+ end
48
+
49
+ # Records one boundary-cross event. Deduplicates on the
50
+ # `(class_name, method_name, gem_name)` triple — the
51
+ # diagnostic per-receiver-per-method-per-owning-gem is
52
+ # the actionable unit.
53
+ def record(class_name:, method_name:, gem_name:, rbs_display:)
54
+ entry = Entry.new(
55
+ class_name: class_name, method_name: method_name,
56
+ gem_name: gem_name, rbs_display: rbs_display
57
+ )
58
+ @mutex.synchronize do
59
+ return if @entries.any? { |existing| same_key?(existing, entry) }
60
+
61
+ @entries << entry
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def same_key?(existing, entry)
68
+ existing.class_name == entry.class_name &&
69
+ existing.method_name == entry.method_name &&
70
+ existing.gem_name == entry.gem_name
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -23,37 +23,63 @@ module Rigor
23
23
 
24
24
  # @param dependencies [Rigor::Configuration::Dependencies]
25
25
  # @return [Index]
26
- def build(dependencies) # rubocop:disable Metrics/MethodLength
26
+ def build(dependencies)
27
27
  return Index::EMPTY if dependencies.empty?
28
28
 
29
- resolved = []
30
- unresolvable = []
31
- catalog = {}
32
- class_to_gem = {}
33
- budget_exceeded = []
34
- budget = dependencies.budget_per_gem
35
-
29
+ state = BuildState.new
36
30
  dependencies.source_inference.each do |entry|
37
31
  next if entry.disabled?
38
32
 
39
- outcome = GemResolver.resolve(entry)
33
+ state.absorb(GemResolver.resolve(entry), dependencies.budget_per_gem, self)
34
+ end
35
+
36
+ state.to_index(dependencies)
37
+ end
38
+
39
+ # Per-build mutable accumulator. The original inline
40
+ # variables (`resolved` / `unresolvable` / `catalog` /
41
+ # `class_to_gem` / `budget_exceeded` / `gem_modes`)
42
+ # pushed the method past the AbcSize budget; the
43
+ # struct collects the same fields, narrows
44
+ # `absorb`'s branching, and yields one
45
+ # `Index.new(...)` call from `to_index`.
46
+ class BuildState
47
+ def initialize
48
+ @resolved = []
49
+ @unresolvable = []
50
+ @catalog = {}
51
+ @class_to_gem = {}
52
+ @budget_exceeded = []
53
+ @gem_modes = {}
54
+ end
55
+
56
+ def absorb(outcome, budget, builder)
40
57
  case outcome
41
58
  when GemResolver::Resolved
42
- resolved << outcome
43
- walked = walker_outcome_for(outcome, budget)
44
- catalog.merge!(walked.catalog)
45
- record_class_to_gem(walked.catalog, outcome.gem_name, class_to_gem)
46
- budget_exceeded << outcome.gem_name if walked.truncated?
47
- when GemResolver::Unresolvable then unresolvable << outcome
59
+ absorb_resolved(outcome, budget, builder)
60
+ when GemResolver::Unresolvable
61
+ @unresolvable << outcome
48
62
  end
49
63
  end
50
64
 
51
- Index.new(
52
- resolved_gems: resolved, unresolvable: unresolvable,
53
- method_catalog: catalog, budget_exceeded: budget_exceeded,
54
- class_to_gem: class_to_gem,
55
- budget_overrun_strategy: dependencies.budget_overrun_strategy
56
- )
65
+ def absorb_resolved(resolved, budget, builder)
66
+ @resolved << resolved
67
+ @gem_modes[resolved.gem_name] = resolved.mode
68
+ walked = builder.walker_outcome_for(resolved, budget)
69
+ @catalog.merge!(walked.catalog)
70
+ builder.record_class_to_gem(walked.catalog, resolved.gem_name, @class_to_gem)
71
+ @budget_exceeded << resolved.gem_name if walked.truncated?
72
+ end
73
+
74
+ def to_index(dependencies)
75
+ Index.new(
76
+ resolved_gems: @resolved, unresolvable: @unresolvable,
77
+ method_catalog: @catalog, budget_exceeded: @budget_exceeded,
78
+ class_to_gem: @class_to_gem,
79
+ budget_overrun_strategy: dependencies.budget_overrun_strategy,
80
+ gem_modes: @gem_modes
81
+ )
82
+ end
57
83
  end
58
84
 
59
85
  # ADR-10 5b — per-class reverse-lookup table (β budget
@@ -21,7 +21,7 @@ module Rigor
21
21
  # a String so it round-trips into cache descriptors
22
22
  # (slice 3) without leaking a `Gem::Version` instance
23
23
  # through public surfaces.
24
- Resolved = Data.define(:gem_name, :version, :gem_dir, :mode, :roots) do
24
+ class Resolved < Data.define(:gem_name, :version, :gem_dir, :mode, :roots)
25
25
  def descriptor_key
26
26
  [gem_name, version, mode].freeze
27
27
  end
@@ -16,7 +16,7 @@ module Rigor
16
16
  # by walking the resolved gems' `roots:`.
17
17
  class Index
18
18
  attr_reader :resolved_gems, :unresolvable, :method_catalog, :budget_exceeded,
19
- :class_to_gem, :budget_overrun_strategy
19
+ :class_to_gem, :budget_overrun_strategy, :gem_modes
20
20
 
21
21
  # @param method_catalog [Hash{[String, Symbol] => Symbol}]
22
22
  # the flat `(class_name, method_name) → :instance | :singleton`
@@ -38,10 +38,19 @@ module Rigor
38
38
  # budget-exceeded gem's classes degrade to
39
39
  # `Dynamic[top]` instead of falling through to the
40
40
  # user-class fallback.
41
- def initialize( # rubocop:disable Metrics/ParameterLists
41
+ # @param gem_modes [Hash<String, Symbol>] per-gem mode
42
+ # table (`gem_name → :disabled | :when_missing |
43
+ # :full`). ADR-10 slice 5c consults this through
44
+ # {#mode_for} to identify call sites where gem-source
45
+ # and RBS both contribute under `mode: :full`. The map
46
+ # is keyed on `gem_name` (not class) because re-opened
47
+ # classes belong to the first gem they appeared in
48
+ # per `class_to_gem`; `mode_for(class_name)` chains
49
+ # the two lookups.
50
+ def initialize(
42
51
  resolved_gems: [], unresolvable: [], method_catalog: {},
43
52
  budget_exceeded: [], class_to_gem: {},
44
- budget_overrun_strategy: :walker_cap
53
+ budget_overrun_strategy: :walker_cap, gem_modes: {}
45
54
  )
46
55
  @resolved_gems = resolved_gems.freeze
47
56
  @unresolvable = unresolvable.freeze
@@ -49,6 +58,7 @@ module Rigor
49
58
  @budget_exceeded = budget_exceeded.freeze
50
59
  @class_to_gem = class_to_gem.freeze
51
60
  @budget_overrun_strategy = budget_overrun_strategy
61
+ @gem_modes = gem_modes.freeze
52
62
  freeze
53
63
  end
54
64
 
@@ -59,6 +69,25 @@ module Rigor
59
69
  @class_to_gem[class_name]
60
70
  end
61
71
 
72
+ # ADR-10 slice 5c — per-class mode lookup. Chains
73
+ # `class_to_gem` + `gem_modes`; returns `nil` when the
74
+ # class isn't owned by any opt-in gem in this run.
75
+ def mode_for(class_name)
76
+ gem_name = @class_to_gem[class_name]
77
+ return nil if gem_name.nil?
78
+
79
+ @gem_modes[gem_name]
80
+ end
81
+
82
+ # ADR-10 slice 5c — true when the receiver class belongs
83
+ # to a gem the user opted into `mode: :full` for. The
84
+ # dispatcher consults this AFTER an authoritative-source
85
+ # (RBS / plugin) dispatch resolves so it can record the
86
+ # boundary-crossing for audit.
87
+ def full_mode?(class_name)
88
+ mode_for(class_name) == :full
89
+ end
90
+
62
91
  # Looks up the recorded method kind for a
63
92
  # `(class_name, method_name)` pair. Returns `:instance`
64
93
  # / `:singleton` when the walker observed a definition
@@ -41,7 +41,7 @@ module Rigor
41
41
  # this per-gem so the Runner can surface a single
42
42
  # `dynamic.dependency-source.budget-exceeded` warning
43
43
  # naming the affected gem(s).
44
- Outcome = Data.define(:catalog, :truncated) do
44
+ class Outcome < Data.define(:catalog, :truncated)
45
45
  def truncated? = truncated
46
46
  end
47
47
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "dependency_source_inference/boundary_cross_reporter"
3
4
  require_relative "dependency_source_inference/gem_resolver"
4
5
  require_relative "dependency_source_inference/index"
5
6
  require_relative "dependency_source_inference/walker"